Zircon 程序加载和动态链接

在 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 程序的方式有一些变化,但 大多数受访者都遵循以下模式:

  1. 内核按名称加载文件,并检查文件是 ELF 还是 文件支持的其他类型的文件。这是 #! 脚本的位置 已完成处理,且支持非 ELF 格式(如果存在)。
  2. 内核会根据其 PT_LOAD 程序映射 ELF 映像 标头。对于 ET_EXEC 文件,这会将节目片段置于 在 p_vaddr 中指定的内存中的固定地址。对于ET_DYN 则系统选择程序第一个 PT_LOAD 进行加载,随后的细分会按照 相对于第一个细分的 p_vaddrp_vaddr。通常, 随机选择基地址 (ASLR)。
  3. 如果有 PT_INTERP 节目头文件,则其内容( 由 p_offsetp_filesz 提供的 ELF 文件中的字节)查找 作为文件名,用于查找另一个名为 ELF 解释器的 ELF 文件。 此文件必须是 ET_DYN 文件。内核加载它的方式与 已加载可执行文件,但始终位于其自行选择的位置。 解释器程序通常是具有名称 (例如 /lib/ld.so.1/lib/ld-linux.so.2),但内核会加载 指定文件名。
  4. 内核为初始线程设置堆栈和寄存器, 在选定的入口点地址启动通过 PC 运行的线程。

    • 入口点是 ELF 文件头文件中的 e_entry 值。 根据基本地址进行调整。当存在 PT_INTERP 时, 是干预器的,而不是主可执行文件。
    • 存在寄存器和堆栈内容的汇编级协议 由内核为程序设置此变量来接收其参数, 环境字符串和有用值的辅助矢量。时间 之前有一个 PT_INTERP,包括基础地址、入口点 以及来自主可执行文件 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[]

处理信息输入

句柄有多种用途,由句柄信息条目类型表示:

其中大部分通过程序加载器传递, 而无需了解它们的用途

加载器服务

在动态链接系统中,可执行文件是指 包含共享库和插件的运行时附加文件。通过 动态链接器作为 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 规范和 processargsloader 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 AddressSanitizerPT_INTERP 字符串 asan/ld.so.1 标识。此版本 先加载加载配置字符串 asan,然后再加载共享库。 当 SanitizerCoverage 时 它向数据接收器名称 sancov 发布一个 VMO,并使用 包含进程 KOID 的 VMO 名称。