- 固件
- 嵌入式
- 操作系统
- 驱动开发
【免费下载链接】coreboot
Read-only mirror of https://review.coreboot.org/coreboot.git. Synced every hour. We don't handle Pull Requests.
导读
本文以 coreboot 官方文档 Documentation/lib/rmodules.md 为主体,结合仓库内 rmodtool、lib/rmodule.c、链接脚本及 postcar / ramstage / SMM 的加载调用链,系统讲解 x86 平台上"可重定位模块"(relocatable modules,简称 rmodules)的设计动机、构建期转换流程、运行时加载与重定位原理,以及module_parameters传参机制。读完本文,你将掌握 rmodule 的 ELF 三段式布局(.header/.program/.relocs)、rmodule_stage_load与 CBMEM 协作的加载路径,并能理解为什么 postcar 与 ramstage 可以随意安放在运行时才确定的地址。
rmodules 是什么:为什么 coreboot 需要"可重定位"的可执行体
按文档定义,可重定位模块目前只在 x86 上使用。rmodule 本质是一类特殊的可执行文件(executable),它允许"在内存中的任意位置被执行"——这里的"任意位置"指模块不必运行在编译/链接期已知的固定地址上。
对于 coreboot 的 bootblock 和 romstage 等阶段,它们在构建期就知道自己将要被执行的地址(例如从 flash 映射地址启动、在 cache-as-ram 中运行)。但对另一些可执行体,运行时所在的具体地址在构建期是未知的,典型代表就是:
- postcar:负责拆除 cache-as-ram(CAR)并为 ramstage 准备环境的过渡阶段;
- ramstage:完整的运行时阶段,需要与后续加载的 payload 共存于内存。
rmodule 的通用流程是:模块在执行前才分配所需内存空间。空间分配充足后,CBMEM 返回分配区域的地址,此时模块在内存中的真实位置已知,就可以基于该位置修复(fix up)模块内的全部重定位条目,完成重定位。整个流程对应文档中的表述:
Relocatable modules usually allocate the space for the modules just before they are supposed to be executed. After enough space is allocated, CBMEM will return the location of the allocated space. Now the relocation can be done by fixing up all relocation entries in the relocatable module based on the location of the binary.
从源码结构看,这一"先分配、后重定位"的思想贯穿了 src/lib/rmodule.c 中rmodule_stage_load→cbfs_alloc→rmodule_load的完整链路,下文将逐一展开。
构建期:rmodtool 如何把普通 ELF 变成 rmodule
rmodtool 的用法
文档明确指出构建期使用rmodtool(util/cbfstool/rmodtool.c)来生成 rmodule。这是一个命令行的 ELF 转换工具,源码中定义的参数如下:
-i, --inelf <name>:输入 ELF 文件;-o, --outelf <name>:输出 ELF 文件;-v, --verbose:打开详细输出;-h, --help:打印用法。
典型调用形式:
rmodtool -i input.elf -o output.elf.rmod其内部实现(见rmodule_create()与 util/cbfstool/rmodule.c)依次完成四件事:
rmodule_init():解析输入 ELF,做架构与格式校验;rmodule_collect_relocations():收集应用到程序段的重定位条目;populate_rmodule_info():从符号表解析_rmodule_params/_ermodule_params/_bss/_ebss等地址;write_elf():输出带.header、.program、.relocs三个新段的 ELF。
如文档所述:"rmodtool basically takes an ELF file as an input and writes an ELF as output. It basically does a simple conversion from one ELF file to another slightly changed ELF file."
输入约束:只允许一个可加载程序段
rmodtool 在转换前会做若干强制校验(见 util/cbfstool/rmodule.c 的rmodule_init()与write_elf()):
- 输入必须是可执行文件:
pelf->ehdr.e_type != ET_EXEC时直接报错 "ELF is not an executable"; - 只能有一个 PT_LOAD 段:
find_program_segment()统计所有PT_LOAD程序头,若为 0 或大于 1 均报错(write_elf()中还有ctx->nsegments != 1的二次校验,即 "Multiple loadable segments is not supported"); - 架构必须受支持:当前支持 EM_386、EM_X86_64、EM_ARM、EM_AARCH64 四类(
reloc_ops[]表),x86 是 rmodule 的主力平台; - 重定位类型必须合法:每种架构有独立的
valid_type()检查,例如 x86_64 下拒绝R_AMD64_32S,因为它在 long mode 下只能安全引用 2 GiB 以下的地址;binutils 2.31 引入的R_AMD64_PLT32也被接受并按 PC32 处理。
之后filter_relocation_sections()会筛选出"作用域落在可加载程序段内"的重定位节,并明确跳过.debug等调试节;对绝对符号、未定义弱符号、未定义符号对应的重定位会单独省略(relocation_for_absolute_symbol()、relocation_for_weak_extern_symbols()、relocation_for_undefined_symbol())。
重定位收集:写入.reloc段
对每一个通过校验的重定位条目,工具依据架构的should_emit()决定是否发射(例如 x86_64 下只发射绝对重定位R_AMD64_64与R_AMD64_32)。收集到的条目地址(r->r_offset)写入ctx->emitted_relocs,最后按地址升序qsort,再统一写入输出 ELF 的新段.relocs。这与文档描述完全一致:
The rmodtool will then write all these relocation entries in a new ELF section called ".reloc". After that the ELF relocation table will be cleared.
最终产物:三个核心段
rmodtool 输出的 rmodule ELF 固定包含三个段(见write_elf()的注释与add_section()调用):
+------------------+ | rmodule header | .header +------------------+ | program | .program(可加载代码与数据) +------------------+ | relocations | .relocs(重定位条目表) +------------------+.header:即struct rmodule_header(详见下文);.program:输入 ELF 中 PT_LOAD 段的文件内容(p_filesz字节);.relocs:重定位条目数组(32 位平台每个条目 4 字节、64 位平台每个条目 8 字节)。
rmodule 的二进制布局:rmodule_header 详解
rmodule 的扁平二进制(flat blob)以 src/commonlib/include/commonlib/rmodule-defs.h 中定义的struct rmodule_header开头,其字段全部由链接脚本与 rmodtool 在构建期填好:
| 字段 | 含义 |
|---|---|
magic | 魔数RMODULE_MAGIC(0xf8fe),加载时校验 |
version | 版本号,当前为RMODULE_VERSION_1(1) |
type | 模块类型 |
payload_begin_offset/payload_end_offset | 程序段(payload)在扁平 blob 中的字节范围 |
relocations_begin_offset/relocations_end_offset | 重定位表在扁平 blob 中的字节范围 |
module_link_start_address | 链接期起始地址,是计算所有重定位偏移的基准 |
module_program_size | 程序运行期占用的连续内存大小(含 bss) |
module_entry_point | 程序入口点 |
parameters_begin/parameters_end | 可选参数结构(module_parameters)范围 |
bss_begin/bss_end | bss 段范围,供加载器清零 |
padding[4] | 预留增长空间 |
rmodule_parse()(src/lib/rmodule.c)在运行时正是依据该头部完成魔法数/版本校验,并据此定位 payload 与重定位表的真实位置。
链接脚本:从地址 0 开始链接
src/lib/rmodule.ld 是 rmodule 专用的链接脚本,其关键设计是SET_COUNTER(rmodule, 0x00000000)——从 0 地址开始链接,这样所有内部地址引用都是"相对 0 的偏移量",运行期只需加上实际加载基址即可完成重定位。脚本注释明确写道:
This linker script is used to link rmodules (relocatable modules). It links at zero so that relocation fixups are easy when placing the binaries anywhere in the address space.
其中.module_parameters与 bss 段的布局由 src/lib/program.ld 承接:ENV_RMODULE || ENV_POSTCAR时在.data段内布置_rmodule_params/_ermodule_params符号并KEEP(*(.module_parameters)),随后依次排列 data 与 bss(_bss/_ebss)。这些符号名与 rmodtool 中populate_rmodule_info()按名查找的符号一一对应。
构建系统的两类用法:coreboot stages 与 simple binaries
文档将 rmodule 分为两类:
- coreboot stages:postcar、ramstage;
- simple binaries:smm、smmstub、sipi_vector。
二者的关键差异在于simple binaries 总是被直接链接进某个 stage。因为 rmodule 本身就是 ELF,可以像普通目标文件一样被链接进需要它的 stage(通常是 postcar 或 ramstage)。它们虽然不再是"独立模块",但依然保留接收rmodule_parameters的能力。文档解释这样做的动机:
Since the simple binaries are usually very small, linking them directly into the stage (e.g. ramstage or postcar) avoids having to fetch them from CBFS and running all that code to fetch a few hundred bytes of code.
构建系统对 simple binaries 的处理流程(文档四步):
- 从源文件创建 rmodule(一个 ELF 文件);
- 用
objcopy -O binary去掉所有不可加载的 ELF 头与节; - 从该 binary 生成一个目标文件——扩展名自创为
.manual——以便链接进目标 stage; - 把生成的
.manual文件作为"源文件"加入目标 stage。
仓库中 src/cpu/x86/smm/Makefile.mk 是这一流程的典型实例:
# 先经 rmodule_link 生成 smmstub.elf.rmod $(obj)/smmstub/smmstub: $(obj)/smmstub/smmstub.elf.rmod $(OBJCOPY_smmstub) -O binary $< $@ $(call src-to-obj,ramstage,$(obj)/cpu/x86/smm/smmstub.manual): $(obj)/smmstub/smmstub cd $(dir $<); $(OBJCOPY_smmstub) -I binary $(notdir $<) $(target-objcopy) $(abspath $@)可以看到 smmstub 先被rmodule_link出.elf.rmod,再objcopy -O binary剥离成裸二进制,最后以smmstub.manual形式作为ramstage-srcs链接进 ramstage。SMM 加载器随后通过rmodule_parse(&_binary_smmstub_start, &smm_stub)直接在 stage 内解析这段内嵌 rmodule(见 src/cpu/x86/smm/smm_module_loader.c 的smm_module_setup_stub()),完全无需访问 CBFS。
而rmodule_link宏本身定义于 src/lib/Makefile.mk:它使用RMODULE_LDFLAGS := -z defs -Bsymbolic(禁止未定义符号、内部符号自绑定),并以 src/lib/rmodule.ld 作为链接脚本生成<name>.elf.rmod。此外 src/arch/x86/Kconfig 中select RELOCATABLE_MODULES表明 x86 平台默认启用该机制,ramstage-y += rmodule.c、romstage-y += rmodule.c等则把运行时加载代码编入相应阶段。
运行时:加载、重定位与跳转
公共 API 一览
src/include/rmodule.h 定义了 rmodule 的公共 API:
rmodule_parse(void *ptr, struct rmodule *m):校验魔数与版本,解析头部;rmodule_parameters(const struct rmodule *m):返回module_parameters段的运行时地址;rmodule_entry(const struct rmodule *m)/rmodule_entry_offset():返回入口点及其相对链接基址的偏移;rmodule_memory_size():返回运行期内存占用;rmodule_load(void *loc, struct rmodule *m):把模块加载到指定地址并重定位;rmodule_load_alignment():默认 4 KiB 对齐;rmodule_calc_region():基于区域分配器计算区域大小、安放偏移与加载偏移;rmodule_stage_load(struct rmod_stage_load *rsl):面向 stage 的高层包装,依赖动态 CBMEM;RMODULE_ENTRY(entry_)宏:在启用CONFIG(RELOCATABLE_MODULES)时把_start别名为指定入口,保证 rmodule 入口统一为_start。
模块类型常量也在该头文件中定义:RMODULE_TYPE_SMM、RMODULE_TYPE_SIPI_VECTOR、RMODULE_TYPE_STAGE、RMODULE_TYPE_VBOOT。
rmodule_load:拷贝、重定位、清 bss 三步走
rmodule_load()(src/lib/rmodule.c)的注释把流程讲得很清楚:
- 拷贝 payload到目标基址(若加载地址与 payload 所在位置相同则跳过拷贝);
- 调整重定位:遍历重定位表,把所有需要修正的位置加上调整量;
- 最后清零 bss——因为重定位条目所在的内存区域就是 bss 所在区域,若模块就地重定位,必须先处理重定位再清 bss,否则会抹掉待修正的数据。
重定位本身正如文档所言"只是一个简单循环":
adjustment = (uintptr_t)rmodule_load_addr(module, 0); while (num_relocations > 0) { adjust_loc = rmodule_load_addr(module, *reloc); *adjust_loc += adjustment; /* 链接期地址 + 加载偏移 */ reloc++; num_relocations--; }每个重定位条目就是一个地址值,指向"需要被修正的内存位置";修正方式是简单加法,把"链接期期望地址"与"实际加载地址"之间的偏移加到该位置上。加载完成后还会调用prog_segment_loaded()记录该内存段,供后续的缓存/安全处理(如 MTRR、stage cache)使用。
rmodule_stage_load:CBMEM 分配 + 解析 + 加载
对 coreboot stage(postcar、ramstage),使用rmodule_stage_load()包装函数。其流程为(src/lib/rmodule.c):
- 通过
prog_locate_hook()定位程序; - 调用
cbfs_alloc()并传入分配回调rmodule_cbfs_allocator():该回调先从 CBFS 元数据中读取 stage header 的memlen,再以cbmem_add(rsl->cbmem_id, region_size)在 CBMEM 中分配内存; rmodule_parse()解析 rmodule 头部;rmodule_load(rmod_loc + sizeof(struct rmodule_header), &rmod_stage)在分配区域内完成加载与重定位;- 通过
prog_set_area()、rsl->params = rmodule_parameters()、prog_set_entry()把参数与入口交给struct prog,随后由prog_run()跳转执行。
rmodule_cbfs_allocator()对布局的处理非常讲究:rmodule 的 header 与 program 是打包连续的,但 program 需要落在 4 KiB 页边界上,因此分配时预留了一页(region_alignment = MIN_UNSAFE(DYN_CBMEM_ALIGN_SIZE, 4096))用于容纳 header,并把返回地址调整为stage_region + region_alignment - sizeof(struct rmodule_header)。其内存布局示意(源码注释):
+--------------------------------+ region_alignment + region_size | >= 0 bytes from alignment | +--------------------------------+ program end (4KiB aligned) | program size | +--------------------------------+ program_begin (4KiB aligned) | sizeof(struct rmodule_header) | +--------------------------------+ rmodule header start | >= 0 bytes from alignment | +--------------------------------+ region_alignment真实调用链:postcar 与 ramstage
postcar:src/arch/x86/postcar_loader.c 的load_postcar_cbfs()构造struct rmod_stage_load:
struct rmod_stage_load rsl = { .cbmem_id = CBMEM_ID_AFTER_CAR, .prog = prog, }; if (rmodule_stage_load(&rsl)) { /* die */ }随后通过rsl.params拿到 postcar 的参数区(即栈指针与 MTRR 上下文),再stage_cache_add(STAGE_POSTCAR, prog)把 postcar 存入 stage cache 以便 S3 恢复。postcar 运行时会重新初始化 CBMEM 并调用run_ramstage()(见 src/arch/x86/postcar.c)。
ramstage:src/lib/prog_loaders.c 的load_relocatable_ramstage()同样构造rmod_stage_load,以CBMEM_ID_RAMSTAGE为 CBMEM 标识调用rmodule_stage_load();S3 恢复路径则直接从 stage cache 取回已重定位的 ramstage(run_ramstage_from_resume())。只有ENV_X86走 rmodule 路径,其他架构仍用传统cbfs_prog_stage_load()。
SMM:src/cpu/x86/smm/smm_module_loader.c 中,smmstub 与 SMI handler 都用rmodule_load()加载到 SMRAM/TSEG 内计算好的地址(smm_stub_loc = smbase + SMM_ENTRY_OFFSET),并用rmodule_parameters()取回smm_stub_params/smm_runtime进行参数填充(栈顶、栈大小、C handler 地址、CR3、APIC ID 映射、save state 布局等)。
module_parameters:在跳转前向 rmodule 传参
机制原理
module_parameters是 rmodule ELF 内部的一个 section,作用是在跳转执行前向 rmodule 传递运行时信息(文档语)。两端分工明确:
- 调用方(caller):通过
rmodule_parameters()(src/lib/rmodule.c)取得module_parameters的运行时地址。该函数先检查模块是否已加载、参数段是否为空(parameters_begin == parameters_end即无参数),再按头部记录的地址换算为实际内存地址; - 被调用方(callee,即 rmodule 自身):通常用编译器属性把该 section 附加到特定类型的实例上,文档给出的示例即:
static const volatile __attribute((aligned(4), __section__(".module_parameters"))) struct smm_runtime smm_runtime;仓库中的真实实例
- SMM runtime 参数:src/cpu/x86/smm/smm_module_handler.c 中正是用上述属性声明
struct smm_runtime smm_runtime,运行期由setup_smihandler_params()填充smbase、smm_size、save_state_size、num_cpus、gnvs_ptr、cbmemc等字段,SMM handler 侧则读取这些字段获得 TSEG 区域、GNVS 指针与控制台缓冲; - SMM stub 参数:src/cpu/x86/smm/smm_stub.S 以汇编
.section ".module_parameters", "aw", @progbits显式声明参数区,src/cpu/x86/smm/smm_module_loader.c 加载后即struct smm_stub_params *stub_params = rmodule_parameters(&smm_stub)并写入栈与入口信息; - SIPI vector:src/cpu/x86/sipi_vector.S 同样声明
.module_parameters段(ap_start_params),并在_start中用sub $(_start), %ebx计算实际加载基址,正是利用了"rmodule 从 0 链接、_start即加载基址代理"的特性。
注意参数区在链接脚本中的位置:ENV_RMODULE || ENV_POSTCAR时_rmodule_params/_ermodule_params包裹.module_parameters(src/lib/program.ld),而 rmodtool 则按这两个符号把参数段起止写入rmodule_header的parameters_begin/parameters_end(util/cbfstool/rmodule.c 的populate_rmodule_info())。值得注意的是文档示例中smm_runtime被声明为static const volatile——它在 SMM handler 侧也遵循这一模式(该实例声明见 src/cpu/x86/smm/smm_module_handler.c 第 28 行)。
反向转换:cbfstool 如何把 rmodule 还原为 ELF
rmodule 机制不仅用于构建与加载,也服务于调试与工具链。rmodule_stage_to_elf()(util/cbfstool/rmodule.c)能从扁平 blob 反向还原出 ELF:先反序列化rmodule_header(校验RMODULE_MAGIC与RMODULE_VERSION_1,并检查各 offset 不越界),再以.program为可加载节、以.empty为 NOBITS 补齐 bss,并重建_rmodule_params、_ermodule_params、_bss、_ebss符号及重定位(跳过链接地址以下的条目)。util/cbfstool/cbfs_image.c 在处理 stage 时优先尝试该路径,失败才回退到传统 SELF 格式——这使cbfstool既能读传统 stage 也能读 rmodule stage。
x86 为什么用 rmodules:与 payload 的内存布局解耦
文档在 "x86 why rmodules" 一节(原文标注//TODO)点明了动机:postcar 与 ramstage 不能与 payload 在内存安放上产生冲突。payload 的位置通常是固定的(例如 SeaBIOS 或 LinuxBoot 被安放在约定地址),因此 coreboot 需要把 postcar/ramstage 安放到"运行时才能确定"的空闲内存区域——只有可重定位的 rmodule 才能做到这一点:
x86: postcar and ramstage cannot conflict with payload regarding memory placement. Therefore payload location is usually fixed and postcar/ramstage can be placed at a location in memory that is figured out at runtime.
这一设计使固件各阶段在内存中"见缝插针":CBMEM 充当运行时分配器,rmodule 的重定位表充当"地址修正清单",二者配合即可把任意编译产物安放到任意可用地址,同时保持内部绝对地址引用完全正确。
总结
rmodules 是 coreboot x86 平台阶段加载的关键机制,其完整工作链路可概括为:
- 构建期:
rmodtool校验并转换 ELF(单一 PT_LOAD 段、受支持的架构与重定位类型),把重定位条目抽离到.relocs段,产出.header+.program+.relocs三段式 rmodule ELF; - 集成期:stage 类 rmodule 从 CBFS 加载,simple binaries 经
objcopy -O binary与.manual目标文件直接内嵌进所属 stage; - 运行期:
rmodule_stage_load通过 CBMEM 分配空间,rmodule_load依次完成 payload 拷贝、重定位修正、bss 清零,最终由prog_run跳转执行; - 传参:
module_parameterssection 让调用方在执行前把运行时数据(栈、入口、SMM 配置等)安全传递给 rmodule。
结合仓库中 src/lib/rmodule.c、src/include/rmodule.h、util/cbfstool/rmodule.c、src/lib/rmodule.ld 以及 postcar / ramstage / SMM 的加载代码,可以完整印证文档所描述的设计与实现。若需更深入的背景,可继续阅读 Documentation/getting_started/architecture.md 与 Documentation/getting_started/devicetree.md 了解阶段与 CBMEM 的整体架构。
- 固件
- 嵌入式
- 操作系统
- 驱动开发
【免费下载链接】coreboot
Read-only mirror of https://review.coreboot.org/coreboot.git. Synced every hour. We don't handle Pull Requests.
相关推荐
Tether 重定位(Repositioning)机制全解:Tether.position() 手动重定位与隐藏元素定位实战
Tether 重定位(Repositioning)机制全解:Tether.position 手动重定位与隐藏元素定位实战 导读 Tether 是一个用于管理浮层
前端UI组件uv 虚拟环境创建机制深度解析:uv-virtualenv 的激活脚本模板、占位符与可重定位环境
uv 虚拟环境创建机制深度解析:uv virtualenv 的激活脚本模板、占位符与可重定位环境 uv virtualenv 是 uv 工作区中负责“从零创建一
包管理器开发工具CLIIDA Pro 重定位信息编程指南:深入解析 ida_fixup 模块的加载器 Fixup API
IDA Pro 重定位信息编程指南:深入解析 ida_fixup 模块的加载器 Fixup API 导读 本文以 IDA Pro 官方 IDAPython 文档
逆向工程MCP 服务AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考