Nuttx - ELF Loader
从 exec() 系统调用到 ELF 文件解析、段加载、符号重定位、地址环境创建,完整解析 NuttX 如何将文件系统中的 ELF 可执行文件变为一个独立运行的任务。
阅读指南: 本文回答以下问题:NuttX 的 binfmt 框架如何识别和加载不同格式的二进制文件?ELF 文件的哪些信息被用来决定内存布局?重定位阶段如何将未定义符号链接到内核导出表?Kernel Build 模式下地址环境如何为每个进程创建独立的虚拟地址空间?读完后,你将能够从源码级别理解 NuttX ELF 加载的完整链路,具备独立调试 ELF 加载失败问题的能力,并能为新架构移植重定位支持。
1. 引言:为什么需要 ELF 加载器
嵌入式 RTOS 传统上将所有代码静态链接为一个单体镜像烧写到 Flash。这意味着每次修改一个应用都要重新编译整个系统。NuttX 的 ELF 加载器打破了这个限制——它允许在运行时从文件系统加载独立编译的 ELF 可执行文件,像 Linux 一样用 exec() 启动新进程。
NuttX 官方文档(Documentation/components/binfmt.rst)这样描述 Binary Loader 的设计意图:
The purpose of a binary loader is to load and execute modules in various binary formats that reside in a file system. Loading refers to instantiating the binary module in some fashion, usually copy all or some of the binary module into memory and then linking the module with other components.
要实现这个目标,ELF 加载器需要解决四个核心问题:
- 格式识别:如何判断文件是合法的 ELF 二进制?
- 内存布局:代码段和数据段分别放在哪里、分配多大空间?
- 符号链接:ELF 中引用的内核函数(如
printf、open)地址在哪? - 地址隔离:Kernel Build 模式下,每个进程如何拥有独立虚拟地址空间?
接下来从 binfmt 框架的整体架构开始,逐步深入每个环节。
2. binfmt 框架:注册与分发
NuttX 支持多种二进制格式(ELF、NXFLAT、Builtin),它们通过统一的 binfmt 框架注册和分发。这个框架的核心是一个链表——每种格式注册一个 handler,加载时依次尝试。
2.1 数据结构
文件:include/nuttx/binfmt/binfmt.h:121-137
1 | struct binfmt_s |
每个二进制格式只需实现两个回调:load 负责识别并加载文件,unload 负责卸载时释放资源。框架通过全局链表 g_binfmts 管理所有已注册的 handler。
文件:binfmt/binfmt_globals.c:47
1 | FAR struct binfmt_s *g_binfmts; |
2.2 ELF 格式注册
文件:binfmt/elf.c:73-78
1 | static struct binfmt_s g_elfbinfmt = |
系统启动时,binfmt_initialize() 调用 elf_initialize() 将 ELF handler 注册到链表头部:
文件:binfmt/elf.c:274-289
1 | int elf_initialize(void) |
register_binfmt() 的实现极为简单——头插法加入链表:
文件:binfmt/binfmt_register.c:57-71
1 | int register_binfmt(FAR struct binfmt_s *binfmt) |
2.3 加载分发流程
当用户调用 exec() 时,框架遍历 g_binfmts 链表,逐个尝试每个 handler 的 load 回调。第一个返回 OK 的 handler 获胜,后续 handler 不再尝试。
文件:binfmt/binfmt_loadmodule.c:101(load_absmodule())
1 | for (binfmt = g_binfmts; binfmt; binfmt = binfmt->next) |
如果 ELF handler 发现文件不是 ELF 格式(magic 不匹配),会返回 -ENOEXEC,框架继续尝试下一个 handler。这种设计让多种格式可以和平共存。
了解了框架如何找到 ELF handler 并调用它,下面进入 ELF 加载的第一步——初始化与头部验证。
3. 初始化:打开文件、验证 ELF 头
ELF 加载从 elf_loadbinary() 开始,第一步是调用 libelf_initialize() 打开文件并验证 ELF 头。
文件:libs/libc/elf/elf_init.c:99-158
1 | int libelf_initialize(FAR const char *filename, |
这段代码完成三件事:(1) 打开文件并获取文件大小/UID/GID/权限;(2) 从偏移 0 读取 52 字节的 ELF 头(32 位系统);(3) 验证头部合法性。
3.1 ELF 头验证
文件:libs/libc/elf/elf_verify.c:64-102
1 | int libelf_verifyheader(FAR const Elf_Ehdr *ehdr) |
验证分三层:
- Magic 校验:前 4 字节必须是
\x7fELF - 类型校验:必须是
ET_REL(可重定位)、ET_DYN(共享对象)或ET_EXEC(可执行文件)之一 - 架构校验:调用架构特定的
up_checkarch()确认e_machine、字长和字节序匹配
实例:ARMv7-A 上的架构校验
文件:libs/libc/machine/arm/armv7-a/arch_elf.c:56-104
对于 qemu-armv7a 板级,up_checkarch() 检查:
e_machine == EM_ARM(值为 40)e_ident[EI_CLASS] == ELFCLASS32(32 位 ELF)e_ident[EI_DATA] == ELFDATA2LSB(小端序)e_entry对齐:Thumb 模式要求 2 字节对齐,ARM 模式要求 4 字节对齐
如果加载一个 x86_64 的 ELF 文件到 ARMv7-A,up_checkarch() 会在 e_machine 检查处失败,返回 -ENOEXEC。
头部验证通过后,加载器知道这是一个合法的 ARM ELF 文件。下一步是将代码和数据实际加载到内存中。
4. 段加载:从文件到内存
验证通过后,elf_loadbinary() 调用 libelf_load_with_addrenv()(或 libelf_load())进入段加载阶段。这一阶段要解决三个问题:代码段多大?数据段多大?分别放到哪块内存?
4.1 加载 Section/Program Headers
文件:libs/libc/elf/elf_load.c:554
1 | ret = libelf_loadhdrs(loadinfo); |
libelf_loadhdrs() 从 ELF 头的 e_shoff 和 e_phoff 处读取所有 Section Header 和 Program Header 到内存。后续所有操作都基于这些缓存的头部数据。
4.2 计算内存需求
文件:libs/libc/elf/elf_load.c:151-246
1 | static void libelf_elfsize(FAR struct mod_loadinfo_s *loadinfo, bool alloc) |
逻辑非常清晰:遍历所有 SHF_ALLOC 段,按 SHF_WRITE 标志分为 text(只读/可执行)和 data(可写)两类,分别累加对齐后的大小。
对于 ET_DYN(动态共享对象),则基于 Program Header 的 PT_LOAD 段计算,按 PF_X(可执行)标志区分。
4.3 内存分配策略
分配策略因 ELF 类型和构建模式而异:
| ELF 类型 | 构建模式 | 分配方式 |
|---|---|---|
ET_REL / ET_EXEC |
FLAT | lib_memalign() 分别分配 text 和 data |
ET_DYN |
FLAT | lib_memalign() 一次性分配 text + segpad + data(保持相对偏移) |
| 任意 | KERNEL (CONFIG_ARCH_ADDRENV) |
up_addrenv_create() 创建虚拟地址空间 |
为什么 ET_DYN 必须连续分配? 动态共享对象使用 GOT(Global Offset Table)进行 PIC 寻址,GOT 中的偏移依赖 text 和 data 之间的固定距离。如果分开分配,所有 GOT 条目都会失效。
下图展示了 ELF 文件从磁盘到虚拟地址空间再到物理内存的映射关系(以 qemu-armv7a knsh 配置为例):

实例:qemu-armv7a knsh 配置的地址分配
根据 boards/arm/qemu/qemu-armv7a/configs/knsh/defconfig:
1 | CONFIG_ARCH_TEXT_VBASE=0x80000000 |
当加载一个 ELF 可执行文件时:
libelf_addrenv_alloc()调用up_addrenv_create(textsize, datasize, heapsize)创建页表up_addrenv_vtext()返回0x80000000作为 text 虚拟基址up_addrenv_vdata()返回0x80100000作为 data 虚拟基址- 物理页从 page pool(
0x40300000,大小 13MB)中分配
4.4 将段数据读入内存
文件:libs/libc/elf/elf_load.c:331-526
分配完内存后,libelf_loadfile() 将文件中的段内容复制到已分配的内存区域(以下为关键逻辑摘录,省略了 CONFIG_ARCH_USE_SEPARATED_SECTION、CONFIG_LIBC_ELF_LOADTO_LMA 等条件分支):
1 | static inline int libelf_loadfile(FAR struct mod_loadinfo_s *loadinfo) |
关键细节:
SHT_NOBITS段(BSS)不需要从文件读取——只需memset清零。BSS 段在 ELF 文件中不占空间,但运行时需要实际内存。sh_addr被覆写为内存地址——后续重定位阶段需要知道每个段在内存中的实际位置,而不是文件中的 VMA。原始 VMA 保存在sh_offset中。- 指针按对齐递进——每加载完一个段,text/data 指针前进到下一个对齐边界,确保后续段的对齐要求被满足。
段加载完成后,代码和数据已在内存中就位,但其中的符号引用仍指向错误的地址。下一步是符号绑定与重定位。
5. 符号绑定与重定位
段加载完成后,ELF 文件中的函数调用和全局变量引用仍然使用编译时的占位值。重定位阶段的任务是:找到所有需要修改的位置,查询符号的真实地址,然后修补(patch)指令或数据字。
5.1 何时需要绑定
文件:binfmt/elf.c:125-149
1 | if (loadinfo.ehdr.e_type == ET_REL || loadinfo.gotindex >= 0) |
- **
ET_REL**(可重定位文件):必须绑定,入口点 = textalloc + e_entry - **
ET_DYN**(有 GOT):必须绑定,入口点 = textalloc + e_entry - **
ET_EXEC**(完全链接):不需要绑定,入口点直接使用 e_entry 绝对地址
5.2 绑定主流程
文件:libs/libc/elf/elf_bind.c:915
libelf_bind() 是符号绑定的入口。简化后的流程:
1 | libelf_bind() |
这条流程的核心设计是”地址空间包裹”:绑定操作的前后各有一次地址空间切换。进入时 addrenv_select() 使 CPU 能访问目标进程的虚拟页,同时赋予 .text 写权限以允许修补指令;退出时 addrenv_restore() 撤销写权限并恢复内核地址空间。中间的 libelf_relocate() 循环是计算密集型操作——遍历所有重定位段,对每个条目执行符号查找 + 架构特定修补 + GOT 更新。最后 up_coherent_dcache() 确保 I-cache 能看到修补后的指令,防止 CPU 从旧缓存行取指。
5.3 符号解析:libelf_symvalue()
每个重定位条目引用一个符号。libelf_symvalue() 负责把符号名解析为内存地址。
文件:libs/libc/elf/elf_symbols.c:340-457
解析策略基于 sym->st_shndx:
st_shndx |
含义 | 处理方式 |
|---|---|---|
SHN_ABS |
绝对符号 | 直接使用 st_value,无需修改 |
SHN_UNDEF |
未定义符号(外部引用) | 在内核导出表 + 已加载模块中查找 |
| 其他值 | 定义在某个段中 | st_value += shdr[st_shndx].sh_addr(加上段基址) |
实例:三种 st_shndx 的解析结果
假设 hello.elf 加载后 .text 段基址为 0x80000000,.data 段基址为 0x80100000:
- 符号
main:st_shndx = 3(.text 段索引),st_value = 0x1A0→ 解析后st_value = 0x80000000 + 0x1A0 = 0x800001A0 - 符号
printf:st_shndx = SHN_UNDEF,st_value = 0→ 从内核导出表查到printf = 0x40001000,解析后st_value = 0x40001000 - 符号
__aeabi_unwind_cpp_pr0:st_shndx = SHN_ABS,st_value = 0→ 不修改,保持st_value = 0
对于 SHN_UNDEF(未定义符号),解析顺序为:
- 遍历已加载模块的导出表:
libelf_registry_foreach(libelf_symcallback, ...) - 搜索内核全局符号表:
symtab_findbyname(exports, name, nexports)
如果两层查找都失败,返回 -ENOENT,整个加载过程失败。 这意味着:如果你的 ELF 程序调用了一个内核没有导出的函数,加载时就会报错,而不是运行时崩溃。
与 Linux 的关键区别: Linux 使用动态链接器(ld.so)在用户空间完成符号解析。NuttX 在内核中一次性完成——没有独立的动态链接器进程。这使得加载更简单快速,但也意味着所有需要的符号必须在加载时就可用。
5.4 重定位处理:libelf_relocate()
文件:libs/libc/elf/elf_bind.c:180
libelf_relocate() 处理 SHT_REL 类型的重定位段。ARM 使用 REL(不带 addend),addend 嵌入在指令编码中。
核心循环的关键步骤(摘录自 elf_bind.c:306-411,省略了批量读取和缓存管理代码):
1 | for (i = 0; i < nrels; i++) |
每次重定位都涉及一次符号查找和一次架构特定的指令修补。为了优化性能,NuttX 使用 LRU 缓存(大小由 CONFIG_LIBC_ELF_SYMBOL_CACHECOUNT 控制)避免重复读取相同符号。
下图详细展示了单次重定位处理的内部交互,包括 LRU 缓存查询、三种 st_shndx 的符号解析路径,以及 ARM 架构的指令修补分支:

符号绑定完成后,所有指令中的地址引用都已指向正确位置。但具体的指令修补逻辑是高度架构相关的——下面以 ARM 为例详细展示。
6. ARM 架构重定位实现
ARM 的重定位处理在 libs/libc/machine/arm/armv7-a/arch_elf.c 中实现。这个文件的核心是 up_relocate() 函数——一个 477 行的 switch-case,处理 ARM 指令集特有的各种编码格式。
6.1 支持的重定位类型总览
| 重定位类型 | ID | 作用 | 典型场景 |
|---|---|---|---|
R_ARM_NONE |
0 | 无操作 | 占位 |
R_ARM_PC24 / R_ARM_CALL / R_ARM_JUMP24 |
1/28/29 | ARM 24-bit PC-relative 分支 | bl function / b label |
R_ARM_ABS32 / R_ARM_TARGET1 |
2/38 | 32-bit 绝对地址 | 全局变量引用、函数指针 |
R_ARM_V4BX |
40 | BX → MOV PC 转换 | ARMv4 兼容 |
R_ARM_PREL31 |
42 | 31-bit PC-relative | 异常表 (.ARM.exidx) |
R_ARM_MOVW_ABS_NC / R_ARM_MOVT_ABS |
43/44 | ARM MOVW/MOVT 立即数 | 32-bit 地址加载(MOVW+MOVT 对) |
R_ARM_THM_MOVW_ABS_NC / R_ARM_THM_MOVT_ABS |
47/48 | Thumb MOVW/MOVT 立即数 | Thumb 模式 32-bit 地址加载 |
R_ARM_THM_CALL / R_ARM_THM_JUMP24 |
10/30 | Thumb 24-bit BL/B.W | Thumb 模式函数调用 |
6.2 R_ARM_ABS32:最简单的重定位
文件:libs/libc/machine/arm/armv7-a/arch_elf.c:198-208
1 | case R_ARM_ABS32: |
这是最直观的重定位:目标地址处有一个 32-bit 数据字(通常是编译器放入的占位值 0 或段内偏移),直接加上符号的绝对地址即可。
实例:全局变量引用
假设 ELF 中有一个对内核导出符号 g_uart_base 的引用:
- 重定位前:
*(uint32_t *)0x80000100 = 0x00000000(占位零值) sym->st_value = 0x40009000(UART 寄存器基址,从内核符号表查到)- 重定位后:
*(uint32_t *)0x80000100 = 0x40009000
6.3 R_ARM_CALL / R_ARM_JUMP24:ARM 分支指令重定位
文件:libs/libc/machine/arm/armv7-a/arch_elf.c:159-196
1 | case R_ARM_PC24: |
ARM BL/B 指令编码:高 8 位是条件码+操作码,低 24 位是有符号字偏移(实际字节偏移 = 字偏移 * 4)。算法步骤:
- 提取原始偏移:从指令低 24 位取出,左移 2 位得到字节偏移,符号扩展
- 计算新偏移:
new_offset = old_offset + symbol_address - relocation_address - 范围检查:ARM 分支范围为 +/-32MB(26 位有符号),超出报错
- 回写指令:右移 2 位后写入低 24 位
实例:BL printf 重定位
假设:
- 重定位地址
addr = 0x80000040(ELF .text 中的 BL 指令位置) - 指令原始值
0xEBFFFFFE(BL -8,编译时的占位值) sym->st_value = 0x40001000(printf 在内核中的地址)
计算过程:
1 | Step 1: offset = (0x00FFFFFE) << 2 = 0x03FFFFF8 |
如果跳转目标超出 +/-32MB 范围,up_relocate() 返回 -EINVAL,加载失败。这是 ARM 架构的硬限制——如果用户程序虚拟地址与内核符号距离超过 32MB,需要使用 veneer(跳板)或不同的重定位方式。
6.4 R_ARM_THM_CALL:Thumb 分支重定位
文件:libs/libc/machine/arm/armv7-a/arch_elf.c:340-467
Thumb 的 BL 是一条 32-bit 指令(由两个 16-bit halfword 组成),分支偏移编码在 S/J1/J2/imm10/imm11 五个字段中。解码公式:
1 | offset[24] = S |
回写时的编码公式:
1 | S = offset[24] |
分支范围为 +/-16MB(25 位有符号)。此外,如果目标符号类型是 STT_FUNC,偏移必须是奇数(bit[0]=1),表示目标是 Thumb 代码——这是 ARM-Thumb 互操作的要求。
6.5 R_ARM_MOVW_ABS_NC / R_ARM_MOVT_ABS:立即数加载
文件:libs/libc/machine/arm/armv7-a/arch_elf.c:237-258
ARM 加载 32-bit 常量通常用 MOVW+MOVT 指令对:MOVW 加载低 16 位,MOVT 加载高 16 位。16-bit 立即数编码在指令的 imm4(bit[19:16]) 和 imm12(bit[11:0]) 字段中。
1 | case R_ARM_MOVW_ABS_NC: |
实例:MOVW + MOVT 加载 0x40009000
1 | MOVW R0, #0x9000 → R_ARM_MOVW_ABS_NC, sym=0x40009000 |
重定位完成后,R0 将包含完整的 32-bit 地址 0x40009000。
了解了重定位如何修补指令,下面看 Kernel Build 模式下地址环境如何为每个进程提供独立的虚拟地址空间。
7. 地址环境:进程级虚拟地址隔离
在 CONFIG_BUILD_KERNEL 模式下,每个用户进程拥有独立的虚拟地址空间。ELF 加载器需要为新进程创建 MMU 页表,分配物理页,并建立虚拟地址映射。这由 libelf_addrenv_* 系列函数完成。
7.1 分配地址环境
文件:libs/libc/elf/elf_addrenv.c:83-143
1 | int libelf_addrenv_alloc(FAR struct mod_loadinfo_s *loadinfo, |
up_addrenv_create() 是架构特定函数,在 ARMv7-A 上会:
- 分配 L1 页表(16KB,4096 个条目)
- 为 text/data/heap 区域分配 L2 页表
- 从 page pool 分配物理页并填充页表条目
7.2 临时激活地址空间
加载段数据和执行重定位时,需要写入新进程的虚拟地址。但此时 CPU 运行在内核地址空间中,新进程的页表尚未激活。libelf_addrenv_select() 解决这个问题:
文件:libs/libc/elf/elf_addrenv.c:159-183
1 | int libelf_addrenv_select(FAR struct mod_loadinfo_s *loadinfo) |
关键设计:.text 段运行时应该是只读+可执行(W^X 安全原则),但加载阶段需要写入数据和修补重定位。解决方案是临时赋予 .text 写权限(PROT_READ | PROT_WRITE | PROT_EXEC),加载完成后恢复为只读+可执行(PROT_READ | PROT_EXEC)。
7.3 恢复地址空间
文件:libs/libc/elf/elf_addrenv.c:200-224
1 | int libelf_addrenv_restore(FAR struct mod_loadinfo_s *loadinfo) |
如果没有地址环境会怎样? 在 FLAT build 模式下(无 MMU 或不启用地址隔离),所有进程共享同一地址空间。此时 libelf_load() 直接用 lib_memalign() 从内核堆分配内存,无需页表操作。代码更简单,但没有进程间内存保护。
段加载和重定位都完成后,ELF 文件已经成为一段可执行的内存映像。最后一步是创建任务并让它运行起来。
8. 任务创建与执行
elf_loadbinary() 返回后,控制回到 exec_internal(),后者调用 exec_module() 完成从”内存中的代码”到”可调度的任务”的转变。
文件:binfmt/binfmt_execmodule.c:152-354
8.1 关键步骤
以下是 exec_module() 的核心路径摘录(binfmt/binfmt_execmodule.c:182-354,省略了错误处理和条件编译分支):
1 | /* Allocate a TCB for the new task */ |
8.2 入口点确定
入口点在 elf_loadbinary() 中确定:
| 条件 | 入口点计算 | 说明 |
|---|---|---|
ET_REL 或有 GOT |
textalloc + ehdr.e_entry |
e_entry 是相对于 .text 起始的偏移 |
ET_EXEC |
ehdr.e_entry 直接使用 |
完全链接,e_entry 是绝对虚拟地址 |
实例:qemu-armv7a 上加载 hello 程序
假设 hello ELF 的 e_entry = 0x000001A0,加载后 textalloc = 0x80000000:
- 入口点 =
0x80000000 + 0x000001A0 = 0x800001A0 nxtask_init()设置初始栈帧,使任务首次被调度时从0x800001A0开始执行- 该地址对应 hello 程序的
main()函数(或 C runtime 的_start)
8.3 exec_swap:进程替换
当通过 exec() 调用(而非 posix_spawn())时,新任务替换当前任务:
1 | if (!spawn) |
exec_swap() 交换两个 TCB 的 PID 和进程组信息,实现 POSIX 语义:exec() 后进程 PID 不变,但执行的代码完全不同。
8.4 任务激活
1 | nxtask_activate(tcb); |
这使新任务进入 ready-to-run 队列。当调度器下次选择它时,CPU 将跳转到 ELF 的入口点开始执行用户代码。至此,一个文件系统中的 ELF 文件变成了一个独立运行的进程。
9. 完整调用链总览
从用户调用 exec("/system/bin/hello", ...) 到 hello 程序开始运行,完整时序如下图所示:

对应的文本形式调用链:
1 | exec() binfmt/binfmt_exec.c:266 |
这条完整链路体现了 NuttX ELF 加载的分层设计:binfmt 框架负责格式分发和任务创建(顶层 exec() / exec_module()),libelf 库负责 ELF 格式的通用解析(段加载、符号解析),而架构特定代码只在最底层的 up_relocate() 和 up_addrenv_create() 中出现。移植新架构时只需实现这两个函数——其余 900+ 行的加载逻辑完全复用。注意链路中有两次地址空间切换:一次在 libelf_load_with_addrenv() 中(写入段数据),一次在 libelf_bind() 中(修补重定位)。每次切换都遵循 select → 操作 → restore 的对称模式。
10. NuttX vs Linux vs FreeRTOS 对比
| 特性 | NuttX ELF Loader | Linux ELF Loader | FreeRTOS |
|---|---|---|---|
| 加载器位置 | 内核中 (binfmt) | 内核 + 用户空间 (ld.so) | 无内置(需第三方库) |
| 支持的 ELF 类型 | ET_REL / ET_DYN / ET_EXEC | ET_DYN / ET_EXEC | 通常仅 ET_REL |
| 动态链接器 | 无(内核一次性完成) | ld-linux.so(用户空间) | 无 |
| 地址隔离 | 可选 (CONFIG_ARCH_ADDRENV) | 必需 (MMU) | 无 |
| 符号表来源 | 编译时生成的静态导出表 | 共享库 .dynsym | 手动提供 |
| 重定位时机 | 加载时全部完成 | 加载时 + 延迟绑定 (PLT/GOT) | 加载时 |
| C++ 构造器 | 支持 (.init_array) | 支持 (.init_array + .init) | 依实现而定 |
| 代码大小 | ~3000 行 (libs/libc/elf/) | ~10000+ 行 (fs/binfmt_elf.c + ld.so) | N/A |
关键设计差异:
NuttX 没有独立的动态链接器。Linux 的
ld.so在用户空间运行,支持延迟绑定(PLT stub 第一次调用时才解析符号)。NuttX 在加载时就完成所有重定位——更简单、更确定性,但无法支持延迟绑定优化。NuttX 的符号表是静态的。内核导出哪些符号在编译时就决定了(通过
CONFIG_SYMTAB_ORDEREDBYNAME+ 编译生成的符号数组)。Linux 的共享库可以动态加载/卸载,符号表动态变化。NuttX 支持 ET_REL 加载。Linux 内核模块也用 ET_REL,但用户空间可执行文件必须是 ET_DYN/ET_EXEC。NuttX 三种都支持,给嵌入式开发提供了更大灵活性。
11. 关键要点
binfmt 框架是可扩展的——通过链表注册机制,ELF/NXFLAT/Builtin 三种格式和平共存,加载时自动识别。
ELF 加载分三阶段:初始化(验证头)→ 段加载(分配内存 + 复制数据)→ 绑定(符号解析 + 重定位)。每个阶段失败都会回滚前序操作。
重定位是架构强相关的——ARM 的 10+ 种重定位类型对应不同的指令编码格式。每种类型的提取/计算/回写逻辑完全不同。
Kernel Build 模式下每个 ELF 进程有独立页表——
libelf_addrenv_alloc()创建地址空间,.text 临时可写用于重定位,完成后恢复为只读+可执行。符号查找失败 = 加载失败——与 Linux 延迟绑定不同,NuttX 在加载时就验证所有外部引用。如果内核没有导出某个函数,加载立即报错
-ENOENT。up_coherent_dcache()不可省略——重定位修改了 .text 段的指令字,必须刷新 D-cache 并无效化 I-cache,否则 CPU 可能执行旧的(未修补的)指令。
12. 参考文件
| 文件 | 内容 | 关键行号 |
|---|---|---|
binfmt/elf.c |
ELF 格式注册、elf_loadbinary 入口 | 73-78, 93-238, 274-289 |
binfmt/binfmt_exec.c |
exec() / exec_internal() 入口 | 77, 266 |
binfmt/binfmt_globals.c |
g_binfmts 全局链表头 | 47 |
binfmt/binfmt_register.c |
register_binfmt() 链表操作 | 57-71 |
binfmt/binfmt_loadmodule.c |
load_module() 遍历格式链表 | 101, 160 |
binfmt/binfmt_execmodule.c |
exec_module() 创建任务 | 152-354 |
binfmt/binfmt_initialize.c |
启动时注册所有格式 | 49-78 |
libs/libc/elf/elf_init.c |
libelf_initialize() 打开+验证 | 99-158 |
libs/libc/elf/elf_verify.c |
libelf_verifyheader() 三层验证 | 64-102 |
libs/libc/elf/elf_load.c |
段大小计算 + 内存分配 + 段加载 | 151-246, 331-526, 545-777 |
libs/libc/elf/elf_bind.c |
libelf_bind() + libelf_relocate() | 180, 915 |
libs/libc/elf/elf_symbols.c |
libelf_symvalue() 符号解析 | 340-457 |
libs/libc/elf/elf_addrenv.c |
地址环境分配/激活/恢复 | 83-143, 159-183, 200-224 |
libs/libc/machine/arm/armv7-a/arch_elf.c |
ARM up_relocate() 所有重定位类型 | 56-104, 128-477 |
include/nuttx/binfmt/binfmt.h |
struct binary_s, struct binfmt_s | 63-137 |
include/nuttx/lib/elf.h |
struct mod_loadinfo_s | 196-254 |
include/elf32.h |
Elf32_Ehdr, Elf32_Shdr, Elf32_Rel, Elf32_Sym | 75-134 |
boards/arm/qemu/qemu-armv7a/configs/knsh/defconfig |
Kernel Build ELF 配置 | 全文 |
| 外部参考 | 内容 |
|---|---|
NuttX 官方文档 Documentation/components/binfmt.rst |
Binary Loader 设计意图、API 说明 |
| ARM ELF specification (ARM IHI 0044F) | ARM 重定位类型定义、指令编码 |
| ELF specification (Tool Interface Standard) | ELF 文件格式、段类型、符号绑定规则 |