在 Zircon 中,内核不直接参与正常的程序加载。 相反,内核提供 构建哪个用户空间程序加载,例如 虚拟内存对象、进程、 虚拟内存地址区域和线程。
ELF 和系统 ABI
标准 Zircon 用户空间环境使用可执行且可链接的格式 (ELF) 并提供了动态链接器和 基于 ELF 的 C/C++ 执行环境。Zircon 进程 只通过 Zircon vDSO(即 由内核以 ELF 格式提供,并使用 C/C++ 调用规范 是基于 ELF 的系统所共有的。
用户空间代码(在提供相应功能的情况下)可以使用系统调用来 直接创建进程并加载程序,而无需 但 Zircon 的机器代码标准 ABI 使用 ELF(如此处所述)。
后台:传统 ELF 程序正在加载
ELF 原为
随 Unix System V Release 4 引入,并成为了通用标准
可执行文件格式。在这些系统中
内核使用 POSIX 将程序加载与文件系统访问集成在一起
execve
API。这些系统加载 ELF 程序的方式有一些变化,但
大多数受访者都遵循以下模式:
- 内核按名称加载文件,并检查文件是 ELF 还是
文件支持的其他类型的文件。这是
#!
脚本的位置 已完成处理,且支持非 ELF 格式(如果存在)。 - 内核会根据其
PT_LOAD
程序映射 ELF 映像 标头。对于ET_EXEC
文件,这会将节目片段置于 在p_vaddr
中指定的内存中的固定地址。对于ET_DYN
则系统选择程序第一个PT_LOAD
进行加载,随后的细分会按照 相对于第一个细分的p_vaddr
的p_vaddr
。通常, 随机选择基地址 (ASLR)。 - 如果有
PT_INTERP
节目头文件,则其内容( 由p_offset
和p_filesz
提供的 ELF 文件中的字节)查找 作为文件名,用于查找另一个名为 ELF 解释器的 ELF 文件。 此文件必须是ET_DYN
文件。内核加载它的方式与 已加载可执行文件,但始终位于其自行选择的位置。 解释器程序通常是具有名称 (例如/lib/ld.so.1
或/lib/ld-linux.so.2
),但内核会加载 指定文件名。 内核为初始线程设置堆栈和寄存器, 在选定的入口点地址启动通过 PC 运行的线程。
- 入口点是 ELF 文件头文件中的
e_entry
值。 根据基本地址进行调整。当存在PT_INTERP
时, 是干预器的,而不是主可执行文件。 - 存在寄存器和堆栈内容的汇编级协议
由内核为程序设置此变量来接收其参数,
环境字符串和有用值的辅助矢量。时间
之前有一个
PT_INTERP
,包括基础地址、入口点 以及来自主可执行文件 ELF 的程序标头表地址 标头。通过此信息,动态链接器可找到 在内存中保存可执行文件的 ELF 动态链接元数据并执行自己的工作。 当动态链接启动完成后,动态链接器将跳转到 主可执行文件的入口点地址。
- 入口点是 ELF 文件头文件中的
Zircon 程序加载受这一传统启发,
。传统模式中加载
是动态链接器的
随机选择的基准地址不得与固定地址相交
由 ET_EXEC
可执行文件使用。Zircon 不支持
只加载固定地址的程序(ELF ET_EXEC
文件)
与位置无关的可执行文件或 PIE,即 ELF ET_DYN
文件。
文件系统不属于 Zircon API 的较低层。相反, 程序加载基于 VMO 和 IPC 通过通道使用的协议。
节目加载请求开头为:
- 包含可执行文件(
ZX_RIGHT_READ
和 需拥有ZX_RIGHT_EXECUTE
项权限) - 参数字符串列表(在 C/C++ 程序中变为
argv[]
) - 环境字符串列表(在 C/C++ 程序中变为
environ[]
) - 初始标识名列表,每个标识名都有 标识名信息条目
系统会处理三种类型的文件:
以 #!
开头的脚本文件
文件的第一行以 #!
开头,长度不得超过 127
个字符长。#!
后的第一个非空白字词是
脚本解释器名称。如果这之后还有什么
它们会变成脚本解释器参数。
- 脚本解释器名称会添加到原始参数的前面
列表(变为
argv[0]
)。 - 如果存在脚本解释器参数,该参数将插入到
解释器名称和原始参数列表(将变为
argv[1]
, (原始argv[0]
变为argv[2]
)。 - 程序加载器通过 加载器服务以获取新的 VMO。
- 程序加载会在该脚本解释器 VMO 上重启,并显示
参数列表经过修改,但其他一切都保持不变。VMO 句柄
就会关闭;仅使用脚本解释器
获取要使用的原始
argv[0]
字符串,而不是原始 VMO。 嵌套数量上限(目前为 5 个) 允许此类重启,除非程序加载失败。
不含 PT_INTERP
的 ELF ET_DYN
文件
- 系统会为第一个
PT_LOAD
段随机选择一个基本地址 然后,在每个PT_LOAD
段中相对于该基准地址进行映射。 为此,需要创建一个涵盖以下内容的 VMAR: 从第一页第一页到最后一页的整个范围 最后一个细分对应的页面。 - 创建一个 VMO 并将其映射到另一个随机地址以保存该堆栈
初始线程的状态如果有
PT_GNU_STACK
计划标题 具有非零p_memsz
,用于确定堆栈的大小(舍入) (最多为整页)。否则,会使用合理的默认堆栈大小。 - vDSO 会映射到进程 (另一个包含 ELF 映像的 VMO),也是在随机基础地址。
- 系统会使用
zx_thread_create()
在进程中创建一个新线程。 系统会创建一个新的渠道,称为引导加载程序 频道。程序加载器向此通道写入一条消息 采用
processargs
协议格式。这个 引导加载程序消息包含参数和环境字符串, 原始请求中的初始句柄。这个列表经过扩充, 带有以下标识名:- 新进程本身
- 其根 VMAR
- 其初始线程
- VMAR(涵盖加载可执行文件的位置)
- 刚刚为堆栈创建的 VMO
- 默认作业(可选),以便新的 进程本身可以创建
- vDSO VMO 和 vDSO VMO,以便新进程 它会自行进行系统调用
然后,程序加载器会关闭其通道末尾。
初始线程通过
zx_process_start()
系统调用启动:entry
将新线程的 PC 从可执行文件e_entry
ELF 标头,根据基础地址进行调整。stack
会将新线程的堆栈指针设置为 堆栈映射。arg1
会将句柄传输到引导渠道, C ABI 中的第一个参数寄存器。arg2
将 vDSO 的基础地址传递到第二个参数中 在 C ABI 中注册。
因此,程序入口点可以编写为 C 函数:
noreturn void _start(zx_handle_t bootstrap_channel, uintptr_t vdso_base);
包含 PT_INTERP
的 ELF ET_DYN
文件
在这种情况下,程序加载器不会直接使用包含
ELF 可执行文件。PT_INTERP
相反,
使用 PT_INTERP
内容作为 ELF 解释器的名称。这个
名称用于向 loader service 发出的请求,
获得一个包含 ELF 解释器的新 VMO,这是另一个 ELF
ET_DYN
文件。然后加载该 VMO,而不是主可执行文件
VMO。启动方式如上所述,但存在以下区别:
额外消息 在
processargs
协议中编写的代码为 引导加载程序渠道,并位于主引导消息之前。通过 ELF 解释器应使用此加载器引导消息 以便能够自行完成工作,然后留下第二个引导加载程序 并将引导频道句柄交给 主程序的入口点加载器引导消息 仅包含由程序加载器添加的必要句柄, 主引导消息中的完整集合,以及下列内容:- 主 ELF 可执行文件的原始 VMO 句柄
加载器服务的通道句柄
通过这些元素,ELF 解释器可以自行加载 可执行文件,并使用加载器服务 供共享库加载额外的 VMO该消息还 包含参数和环境字符串,这让 ELF 可以 解释器在其日志消息中使用
argv[0]
,并检查 环境变量,例如LD_DEBUG
。
PT_GNU_STACK
计划头文件会被忽略。相反,程序 加载器会选择一个最小的堆栈大小 包含 loader 引导加载程序消息,并且为 ELF 解释器的启动代码,以用作调用框架。这个 “呼吸室”源代码中的大小为PTHREAD_STACK_MIN
, 调整后,引导加载程序消息大小较小时,整个堆栈 一个网页,但经过精心的动态链接器实现 所需的工作空间动态链接器应读取 主可执行文件的PT_GNU_STACK
并切换到一个合理的堆栈, 在跳转到主可执行文件的条目之前,正常使用情况下的大小 。
processargs 协议
<zircon/processargs.h>
定义了
引导加载程序通道上发送的引导加载程序消息的协议,
程序加载器。当进程启动时,它会有一个句柄
引导通道,并且可以通过
vDSO。该进程只有这一个标识名
只能看到全局系统信息和自己的内存,
信息和句柄。
processargs
协议是一种单向协议,适用于通过
引导加载程序通道。新进程永远不会回写到
频道程序加载器通常会发送消息,然后关闭
在新流程开始前就已经结束。这些
消息必须传达新进程将需要的所有内容,但
接收和解码此格式消息的代码必须以
限制环境。如果存在以下情况,则堆分配无法实现且非常重要
图书馆设施可能无法使用。
如需了解详情,请参阅头文件 消息格式的详细信息预计此临时协议 最终将被基于 IDL 的正式协议所取代, 将保持简单,以便通过简单的手写 代码。
引导消息会传达以下信息:
- 初始标识名
- 与每个句柄对应的 32 位句柄信息条目
- 句柄信息条目可引用的名称字符串列表
- 参数字符串列表(在 C/C++ 程序中变为
argv[]
) - 环境字符串列表(在 C/C++ 程序中变为
environ[]
)
处理信息输入
句柄有多种用途,由句柄信息条目类型表示:
- 进行系统调用进程的基本句柄: process、VMAR、 thread、job
- channel 传递到加载器服务
- vDSO VMO
- 与文件系统相关的句柄:当前目录、文件描述符、名称 空格绑定(这些绑定将索引编码为名称字符串列表)
- 系统进程的特殊句柄: resource、VMO
- 用于更高层协议或私有协议的其他类型
其中大部分通过程序加载器传递, 而无需了解它们的用途
加载器服务
在动态链接系统中,可执行文件是指 包含共享库和插件的运行时附加文件。通过 动态链接器作为 ELF 解释器进行加载, 您需要负责获取所有这些额外文件的访问权限 在主程序的入口点获得控制之前建立动态链接。
Zircon 的所有标准用户空间都使用动态链接,详细程度可达
userboot
加载的第一个进程。设备驱动程序和
文件系统是通过以这种方式加载的用户空间程序实现的。因此
程序加载无法用更高级别的抽象定义
例如文件系统范式
为
传统系统的做法。
相反,程序加载仅基于 VMO 和
一个基于通道的简单协议。
此加载器服务协议是动态链接器获取 VMO 的方式 表示需要作为共享库加载的其他文件。
这是一种简单的 RPC 协议,在
<zircon/processargs.h>
。
发送加载器服务的代码
在动态链接器启动期间接收请求和收到的回复
无法使用重要的图书馆设施
ELF 解释器会在
processargs
引导消息,由句柄信息条目标识
PA_HND(PA_LDSVC_LOADER, 0)
。所有请求都是发出的同步 RPC
与 zx_channel_call()
共享。请求和回复均以
zx_loader_svc_msg_t
标头;有些包含其他数据;有些包含
VMO 句柄请求操作码如下:
LOADER_SVC_OP_LOAD_SCRIPT_INTERP
:字符串 ->VMO 句柄程序加载程序从以下网址发送脚本解释器名称:
#!
脚本并获取要执行的 VMO,以替代 脚本。LOADER_SVC_OP_LOAD_OBJECT
:字符串 ->VMO 句柄动态链接器发送对象的名称(共享库或 插件),并获取包含该文件的 VMO 句柄。
LOADER_SVC_OP_CONFIG
:string ->reply ignored
动态链接器会发送一个标识其加载配置的字符串。 这会影响
LOADER_SVC_OP_LOAD_OBJECT
请求决定了为 。LOADER_SVC_OP_DEBUG_PRINT
:字符串 ->reply ignored
这是一个简单的临时日志记录工具,用于调试 动态链接器和早期程序启动问题方便 因为前期启动代码使用了加载器服务, 一些其他手柄或复杂的设施。这将 以后会被一些简单易用的日志记录工具所取代, 不会通过加载程序服务进行处理
LOADER_SVC_OP_LOAD_DEBUG_CONFIG
:字符串 ->VMO 句柄这是一项面向开发者的功能,可能 通常可在生产环境运行时使用。
程序运行时发送一个字符串,命名为调试配置 获取一个 VMO 来读取配置数据通过 Sanitizer 运行时使用它允许将大型选项文本 而不是直接传入环境字符串。
LOADER_SVC_OP_PUBLISH_DATA_SINK
:字符串、VMO 句柄 ->reply ignored
这是一项面向开发者的功能,可能 通常可在生产环境运行时使用。
程序运行时发送名为“数据接收器”的字符串,然后进行传输 它想要在此处发布的 VMO 的唯一句柄。数据接收器 字符串用于标识数据类型,VMO 的对象名称可以 明确标识此 VMO 中的数据集。客户端必须 将唯一的句柄转移到 VMO(这可防止 VMO 被 但其可能仍具有 VMO 已映射并继续向其写入数据。代码插桩 运行时使用此方法提供大型二进制轨迹结果。
Zircon 的标准 ELF 动态链接器
上述 ELF 规范和
processargs
和 loader service 协议是永久性的,
用于加载程序的系统 ABI。程序可以使用任何
符合基本 ELF 格式惯例的机器代码可执行文件。通过
实现可以使用 vDSO 系统调用。
ABI、processargs
数据和加载器服务设施,如屏幕所示
合适。关于所处理内容以及程序通过
这些协议依赖于更高级别的程序环境。锆石
系统进程使用 ELF 解释器来实现基本 ELF 动态
链接,以及加载器服务的简单实现。
Zircon 的标准 C 库和动态链接器
最初派生的统一实现方案
来自musl
。它通过
PT_INTERP
字符串 ld.so.1
。它使用 DT_NEEDED
字符串命名
共享库服务作为加载器服务 对象名称。
简单的加载器服务将请求映射到文件系统访问:
- 脚本解释器和调试配置名称必须以
/
开头 用作绝对文件名 - 数据接收器名称变成
/tmp
中的子目录,每个 VMO published 会变为该子目录中的文件,其中包含 VMO 的对象名称 - 将对象名称作为系统
lib/
目录中的文件进行搜索。 - load configuration 字符串被视为子目录名称,
可以选择性地后跟
!
字符。中以该名称命名的子目录 搜索的系统lib/
目录会在lib/
本身之前搜索。 如果存在!
后缀,则系统只会搜索这些子目录。 例如,排错程序运行时使用asan
,因为该插桩 与未经插桩的库代码兼容,但dfsan!
,因为 插桩要求进程中的所有代码 插桩。
使用
LLVM AddressSanitizer
由 PT_INTERP
字符串 asan/ld.so.1
标识。此版本
先加载加载配置字符串 asan
,然后再加载共享库。
当 SanitizerCoverage 时
它向数据接收器名称 sancov
发布一个 VMO,并使用
包含进程 KOID 的 VMO 名称。