- 固件
- 嵌入式
- 操作系统
- 驱动开发
【免费下载链接】coreboot
Read-only mirror of https://review.coreboot.org/coreboot.git. Synced every hour. We don't handle Pull Requests.
本文基于 coreboot 仓库文档 Customizing Module Behavior,讲解 coreboot 中"更具体的模块如何定制更基础模块行为"的核心机制:以__weak弱符号为载体的静态依赖注入。读完本文,你能理解 coreboot 从 commonlib 到 mainboard 的分层定制模型、弱符号被链接器替换的底层原理、构建系统如何确保"同一符号只链接一份实现",以及如何用git grep -w __weak系统地找出所有定制点,并判断何时应该改用函数指针等更安全的模式。
一、coreboot 模块的分层与定制方向
coreboot 的模块按"从基础到具体"可分为若干层次(该列表并不完整):
- commonlib:被众多模块共享的库代码与辅助代码;
- 平台代码(Platform code):支撑某一计算平台的代码,例如
arch/x86(包含 bootblock、romstage、ramstage)或arch/arm64; - drivers、CPU、SoC、North-/Southbridge 代码:支撑某一具体 CPU 平台的代码,如 "Intel Alder Lake";
- mainboard:主板专用的初始化代码、配置数据与钩子(hooks);
- Mainboard variants(主板变体,可选):一块主板可以有多个仅在细节上不同的变体。
软件开发中一个常见的模式是:让"更具体"的模块能够定制"更基础"的模块的行为(或者说"钩住"它们)。许多开发者熟悉的做法是运行时动态注册函数指针(依赖注入)。而 coreboot 展现的是该模式的静态变体:不在运行时注册函数指针,而是借助链接器(linker)在构建期静态地实现可定制性。
二、定制点:以弱符号提供的默认实现
各类模块通过"定制点(customization points)"来开放定制能力:它们是带有默认实现或空操作(no-op)实现的函数,被声明为__weak弱符号。更具体的实现可以按需要覆盖它们。文档同时提醒:在某些特定场景之外,用弱函数取代函数指针有缺点,因此不被鼓励(详见第五节)。
以 src/lib/bootblock.c 为例,它提供了四个弱符号默认实现:
__weak void bootblock_mainboard_early_init(void) { /* no-op */ } __weak void bootblock_soc_early_init(void) { /* do nothing */ } __weak void bootblock_soc_init(void) { /* do nothing */ } __weak void bootblock_mainboard_init(void) { /* do nothing */ }各主板实现可以自由地重新定义这些符号,完成自己需要的早期初始化(例如早期 GPIO 初始化)。这些弱符号的声明位于 src/include/bootblock_common.h,头文件注释明确说明了调用顺序:
/* * These are defined as weak no-ops that can be overridden by mainboard/SoC. * The 'early' variants are called prior to console initialization. Also, the * SoC functions are called prior to the mainboard functions. */ void bootblock_mainboard_early_init(void); void bootblock_mainboard_init(void); void bootblock_soc_early_init(void); void bootblock_soc_init(void);2.1 默认实现与调用时机
在 src/lib/bootblock.c 的bootblock_main_with_timestamp()中可以看到调用链:先采集TS_BOOTBLOCK_START时间戳,然后依次调用bootblock_soc_early_init()、bootblock_mainboard_early_init()("early" 变体在控制台初始化之前调用,SoC 先于主板),随后在CONFIG(BOOTBLOCK_CONSOLE)使能时才做console_init(),再调用bootblock_soc_init()与bootblock_mainboard_init(),最后run_romstage()进入下一阶段。这正是"基础代码调用定制点、具体代码提供实现"的静态注入点:如果主板没有提供自己的实现,链接器就使用lib/bootblock.c中的空操作。
2.2 真实的主板覆盖示例
仓库中mainboard/目录下有大量文件实现了bootblock_mainboard_early_init(),通常命名为early_init.c或bootblock.c(这只是惯例)。例如 src/mainboard/asrock/spc741d8/bootblock.c 覆盖了该符号,用于:
- 通过 PCR 寄存器(
PCR_DMI_LPCIOD/PCR_DMI_LPCIOE)在 PCH 上打开 LPC I/O 解码窗口; - 通过 PCI 配置空间(
LPC_IO_DECODE、LPC_IO_ENABLES、LPC_ESPI_CS1_ROUTING)使能 com1(0x3f8)、com2(0x2f8)和 SuperIO 端口; - 进入 Nuvoton NCT6791D 的 PnP 配置态,禁用其 UART1,避免与 AST2600 的串行口冲突;
- 使能 AST2600 SuperIO 的 UART1 作为调试串口。
这些都是"必须极早完成、但控制台尚未可用"的初始化,正符合 weak no-op 钩子的使用定位。
2.3 链接器行为:覆盖而非报错
弱符号的关键语义由链接器保证:
- 只要存在非 weak 的同名定义,链接器总是优先取非 weak 版本并丢弃 weak 版本,而不是抛出 "duplicate definition"(重复定义)错误;
- 只有当其他定义都不存在时,链接器才采用
__weak实现,而不是抛出 "undefined symbol"(未定义符号)错误。
因此,基础代码可以放心地调用定制点:无论主板是否提供实现,链接都能成功,运行时行为自然取决于哪份实现被链接进来。
三、Kconfig 与 Makefile 的角色
弱符号机制只解决"链接时如何选择实现",而选择哪份实现参与编译则由构建系统完成(详见 build system 文档)。git grep可以发现mainboard/目录下众多文件都实现了bootblock_mainboard_early_init(void),但对于任何一个给定的构建配置,只有其中一份源文件会被编译并链接进最终固件。
具体机制体现在各主板的Makefile.mk中。例如 src/mainboard/asrock/spc741d8/Makefile.mk:
bootblock-y += bootblock.c romstage-y += romstage.c ramstage-y += ramstage.c而 src/mainboard/supermicro/x9scl/Makefile.mk 则是:
bootblock-y += early_init.c romstage-y += early_init.ccoreboot 的构建流程会根据 Kconfig 中选择的MAINBOARD只拉取对应mainboard/<厂商>/<型号>/Makefile.mk声明的源文件,于是同一符号的多份候选实现中只有被选中的那份进入编译,从根源上避免了重复定义。
此外,文档给出一条重要的可读性建议:当某段代码只在特定 Kconfig 选项使能时才被包含时,调用它的语句本身也应当用该 Kconfig 选项条件化,以提高可读性。bootblock.c中的调用本身就遵循了这一惯例:
if (CONFIG(BOOTBLOCK_CONSOLE)) { console_init(); exception_init(); }四、如何识别所有定制点
文档坦承:目前没有一份穷尽式的定制点清单。但有一个简单的发现方法:
git grep -w __weak这会列出所有.c文件中的默认(weak)定义;再到.h文件中查找对应声明,往往能发现更多说明文档。以仓库实际为准,src/lib/下就有大量这样的定制点,例如:
- src/lib/smbios_defaults.c:约 27 个 weak 函数构成 SMBIOS 表的默认值,如 smbios_mainboard_product_name() 默认返回
CONFIG_MAINBOARD_SMBIOS_PRODUCT_NAME,而smbios_mainboard_bios_version()默认返回NULL。主板只需覆盖自己关心的一项,其余字段自动沿用默认值——这是文档中"SMBIOS table overrides"这一典型用法的直接体现; - 其他如
src/lib/coreboot_table.c、src/lib/gpio.c、src/lib/cbfs.c、src/lib/timestamp.c等文件中也各自定义了多个 weak 定制点,覆盖 coreboot table 字段、GPIO 默认行为、console/时间戳等场景。
实践建议是:遇到某个行为想定制时,先git grep -w __weak <符号名>或通读所在模块的 weak 定义,确认是否存在现成定制点;若.h文件中有文档注释(如bootblock_common.h那样),可优先以它为准理解调用顺序与语义。
五、适用场景与不适用场景
弱符号带来的定制非常轻量——不需要 RAM 已经可用就能挂上钩子,这对 bootblock 这类最早期阶段尤其重要。但文档也明确指出了它的缺点:
- 可发现性差(bad discoverability):没有中央清单,定制点只能靠 grep 发现;
- 错误覆盖难以察觉:当错误的 override 被误包含进构建,或本该被覆盖却用了默认实现时,行为会"出人意料",而且不会产生任何构建错误来提示问题,这类问题极难调试。
文档给出的经验法则是:范围有限或用途明确的用法是可以接受的,典型场景包括:
- mainboard variants(主板变体):同一主板不同细节变体之间做小范围差异化;
- SMBIOS 表覆盖(如上文
smbios_defaults.c的用法); - 公共代码中的钩子:供 mainboard 或 SoC 覆盖的 hooks(如
bootblock_mainboard_early_init这类 bootblock 钩子)。
在这些场景之外,通常应改用其他模式,例如函数指针。与 weak 符号"静默地以意外方式失败"不同,函数指针模式在使用错误时会产生清晰的构建错误,错误定位成本显著更低。选择哪种模式时,可以按"错误代价"来权衡:早期启动钩子选 weak 符号换取 RAM-free 可用性;其余场合优先让链接器/编译器帮你把错误显式化。
小结与延伸阅读
- 机制本质:
__weak默认实现 + 主板/SoC 的具体覆盖 + 链接器择优 + Makefile/Kconfig 控制哪份源码参与编译; - 关键文件:src/lib/bootblock.c(默认 no-op 与调用链)、src/include/bootblock_common.h(声明与顺序注释)、src/lib/smbios_defaults.c(SMBIOS 覆盖示例);
- 识别方法:
git grep -w __weak; - 设计取舍:早期钩子与变体差异用 weak 符号;其他场景优先函数指针等能产生构建错误的模式;
- 深入阅读:构建系统文档、Kconfig 文档、架构文档。
- 固件
- 嵌入式
- 操作系统
- 驱动开发
【免费下载链接】coreboot
Read-only mirror of https://review.coreboot.org/coreboot.git. Synced every hour. We don't handle Pull Requests.
相关推荐
grok-build 0.2.110 更新解析:Extensions 删除确认、会话创建失败兜底与自动压缩登录重试机制
grok build 0.2.110 更新解析:Extensions 删除确认、会话创建失败兜底与自动压缩登录重试机制 导读 本文基于 grok build(S
人工智能大模型AI Agent代码智能体CLI工具调用MCP ClientsGrumpy中的弱引用机制:Go实现的Python weakref模块
Grumpy中的弱引用机制:Go实现的Python weakref模块 1. 弱引用(Weak Reference)基础 弱引用(Weak Reference)
编译器语言运行时开发工具KernelSU实战指南:从内核级Root到模块管理的深度解析与避坑技巧
KernelSU实战指南:从内核级Root到模块管理的深度解析与避坑技巧 KernelSU作为基于内核的Android root解决方案,以其独特的系统级权限管
操作系统驱动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考