Featured image of post 用 GCC 实现模块自动注册初始化机制

用 GCC 实现模块自动注册初始化机制

今天我们要聊一个看起来简单,但背后藏了很多 C 语言高级技巧 & GCC 黑魔法 的小例子 👇:

 1#include <stdio.h>
 2
 3typedef void (*init_t)(void);
 4
 5#define module_init(fn) \
 6    const init_t __embedi_##fn __attribute__((section(".embedi_init"))) \
 7    __attribute__((used)) = fn
 8
 9static void module_one_init(void)
10{
11    printf("%s call\n", __FUNCTION__);
12}
13
14module_init(module_one_init);
15
16int main(void)
17{
18    return 0;
19}

别看它代码不多,但它其实实现了一个非常实用的机制:模块化自动注册 & 初始化函数管理! 🎯


🧩 一、代码逐行拆解(小白友好版)

1. 引入头文件

1#include <stdio.h>

标准输入输出库,为了使用 printf 打印日志。


2. 定义初始化函数类型

1typedef void (*init_t)(void);

定义了一个函数指针类型 init_t,用于指向那些无参数、无返回值的初始化函数。 你可以把它理解为一个“万能插座”,所有模块的初始化函数都可以“插”进来。⚡


3. 定义模块注册宏

1#define module_init(fn) \
2    const init_t __embedi_##fn __attribute__((section(".embedi_init"))) \
3    __attribute__((used)) = fn

🔥 这是整个机制的核心!

它利用 GCC 的宏和编译器扩展属性,实现了一种叫做 “模块自动注册” 的机制,让模块在编译期就把自己注册进一个特定的段(section)中,便于后续统一管理。


3.1 宏展开示例

比如你写了:

1module_init(module_one_init);

它会被 GCC 预处理并展开为类似如下的代码:

1const init_t __embedi_module_one_init 
2__attribute__((section(".embedi_init"))) 
3__attribute__((used)) 
4= module_one_init;

我们来拆解它的关键部分:


✅ a. 变量名:__embedi_module_one_init
  • 类型为 init_t(即函数指针);
  • 名称通过宏拼接生成:__embedi_ + 传入的函数名;
  • 前缀 __embedi_ 是为了避免命名冲突,规范且安全 👍。

✅ b. 属性:section(".embedi_init")

这是 GCC 提供的扩展属性,用于将变量放入一个自定义的段(Section) ,名字叫 .embedi_init

  • 在 ELF 格式的可执行文件中,代码和数据被分成多个段,比如:

    • .text:存放代码
    • .data:已初始化全局变量
    • .bss:未初始化全局变量

通过 section(".embedi_init"),我们可以把所有模块的初始化函数指针集中放到一个专属段里,方便后续统一查找和调用。🎯


✅ c. 属性:used

告诉编译器: “这个变量虽然看起来没人直接用,但我需要它!请不要优化掉!”

  • 如果没有这个属性,编译器可能会认为这个全局变量没有实际用途,就把它删了!🗑️
  • 但我们希望它留下来,因为它里面存着模块初始化函数的地址!

✅ d. 变量赋值:= fn

将你传入的模块初始化函数(比如 module_one_init),赋值给这个函数指针变量,存入 .embedi_init 段中。


4. 定义一个模块初始化函数

1static void module_one_init(void)
2{
3    printf("%s call\n", __FUNCTION__);
4}

这是一个普通的模块初始化函数,功能是打印自己的函数名。

  • 它被声明为 static,表示仅在当前文件内可见,避免命名冲突;
  • 在实际项目中,这里可以是初始化硬件、注册服务、加载配置等逻辑 🛠️。

5. 使用宏注册模块

1module_init(module_one_init);

这一行是关键!

它调用了 module_init 宏,把 module_one_init 函数注册进 .embedi_init 段中,等待“被调用”。

展开后相当于:

1const init_t __embedi_module_one_init = module_one_init;

并且该变量被放入 .embedi_init 段,且不会被编译器优化掉 ✅。


6. main 函数

1int main(void)
2{
3    return 0;
4}

程序入口,但目前它什么也没做!😅

我们虽然注册了初始化函数,但并没有真正去调用它!

所以现在的结果是:函数注册成功,但无人调用,它只能默默等待… 💤


❓ 问题:注册了函数,但没调用,有啥用?

好问题!👏

其实,这段代码展示的是一种非常有用、且被广泛使用的编程思想,叫做:

模块化自动注册机制(Module Auto-Registration)

它的核心价值在于:


✅ 你只需写:

1module_init(your_init_function);

✅ 编译器 + 宏 + 链接器会自动帮你:

  • 把你的初始化函数指针,放到一个特定的段(比如 .embedi_init);
  • 后续你可以通过遍历该段,一次性调用所有模块的初始化函数;

这就相当于你搭建了一个 “自动化插件系统”或“驱动注册框架” 的基础设施!🔧


🌟 实际应用场景

这种技巧在许多大型系统或框架中都有应用,比如:

场景说明
Linux 内核模块驱动通过module_init()注册,系统启动时自动加载调用
插件系统每个插件在加载时注册自己,主程序统一管理
游戏引擎模块渲染、音频、物理等子系统各自初始化,通过统一机制调用
嵌入式系统 / 驱动框架模块自行注册初始化逻辑,由框架统一调度

是不是突然发现,这个“小技巧”其实是个“大杀器”?🔥


🛠️ 如何真正调用这些初始化函数?

目前代码只实现了注册,还没实现调用

那怎么才能让这些函数真正被执行呢?下面介绍几种方式,从简单到高级:


方法 1:手动函数指针数组(适合少量模块,扩展性差 😅)

你可以手动维护一个函数指针数组:

 1void (*init_funcs[])() = {
 2    module_one_init,
 3    NULL  // 结束标记
 4};
 5
 6int main() {
 7    for (int i = 0; init_funcs[i] != NULL; i++) {
 8        init_funcs[i]();
 9    }
10    return 0;
11}
  • 优点:简单直接;
  • 缺点:每新增一个模块,都得手动加一行,不适合大型项目。

方法 2:利用自定义段 + 链接器符号(推荐 ✅,扩展性强)

更优雅的方式,是利用 GCC + 链接器,自动获取 .embedi_init 段的起始地址和结束地址,然后遍历该段中的所有函数指针并调用。

步骤如下:


① 定义段的起始和结束符号(在链接脚本或代码中)

你需要在链接时导出 .embedi_init 段的起始和结束地址。一种常见方法是:

在代码中定义两个符号,分别标记该段的起始和结尾:

1// 在某个.c文件中定义(或通过链接脚本导出)
2extern const init_t __start_embedi_init;
3extern const init_t __stop_embedi_init;

注意:符号名中的 __start___stop_ 是 GCC / 链接器的约定(常见于 .rodata.init_array 等段),但如果你使用的是自定义段 .embedi_init,需要确保链接器脚本中正确定义了这些符号!


② 修改或编写链接脚本(可选,高级用户)

如果你想完全控制段的布局与符号导出,可以自定义链接脚本(Linker Script) ,并确保其中包含类似如下内容:

1SECTIONS
2{
3    .embedi_init : {
4        __start_embedi_init = .;
5        *(.embedi_init)
6        __stop_embedi_init = .;
7    }
8}

然后确保该脚本被正确使用(通过 -T your_script.ld 参数传递给链接器)。


③ 在代码中遍历并调用

一旦链接器导出了 __start_embedi_init__stop_embedi_init,你就可以这样写:

 1extern const init_t __start_embedi_init[];
 2extern const init_t __stop_embedi_init[];
 3
 4int main(void)
 5{
 6    const init_t *fn_ptr = __start_embedi_init;
 7    const init_t *end_ptr = __stop_embedi_init;
 8
 9    while (fn_ptr < end_ptr) {
10        (*fn_ptr)();
11        fn_ptr++;
12    }
13
14    return 0;
15}

🎉 这样就能自动调用所有注册在 .embedi_init 段中的模块初始化函数了!

  • 优点:完全自动化、无需手动维护列表、扩展性极强
  • 适用场景:中大型项目、模块化系统、插件架构等。

✅ 总结 & 学习收获

知识点说明
module_init(fn)用于将模块初始化函数自动注册到自定义段
typedef void (*init_t)(void)定义统一的初始化函数指针类型
GCC 属性section将变量放入自定义段,集中管理
GCC 属性used防止编译器优化掉未显式引用的全局变量
自定义段.embedi_init所有模块初始化函数指针的存储区域
调用方式手动数组(简单) or 链接器符号遍历(推荐,自动且扩展性强)
应用场景模块化系统、驱动框架、插件系统、嵌入式初始化等

🧠 延伸阅读推荐

  • 《程序员的自我修养 —— 链接、装载与库》📚
  • Linux 内核中的 module_init() 实现机制
  • ELF 文件格式与自定义段管理
  • GCC 扩展属性官方文档
  • 链接器脚本(Linker Script)入门指南