今天我们要聊一个看起来简单,但背后藏了很多 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)入门指南