Zephyr静态注册机制详解:SYS_INIT和BT_CONN_CB_DEFINE为什么没人调用也执行

发布时间:2026/10/9 7:47:57
Zephyr静态注册机制详解:SYS_INIT和BT_CONN_CB_DEFINE为什么没人调用也执行 在 Nordic 的 nRF Connect SDK 里写 BLE 应用的时候我在conn.c里翻过一段让我愣了一下的代码应用源文件里就一行BT_CONN_CB_DEFINE(my_callbacks)这个结构体在整个工程里没有任何一行代码显式调用它可是蓝牙一连接里面的connected回调就执行了。注册了一个结构体没人取、没人调它自己就生效了。这不是灵异事件是 Zephyr 的一套通用基础设施在干活——静态注册iterable sections。你在 NCS 里天天在用它只是可能没意识到SYS_INIT为什么不用在main()里调就自动执行SHELL_CMD_REGISTER为什么加一行就多一条命令BT_GATT_SERVICE_DEFINE定义的服务为什么自动生效全是同一套机制。这篇文章把它拆开讲清楚。机制一句话任意源文件里用一个宏声明一个结构体编译期它被放进专属链接器 section链接期被收集到一块连续内存并打上起止边界符号运行期框架用遍历宏在边界内逐个取出调用——无需显式register()写完宏就自动生效。一、先看物理布局注册的东西到底放在哪第一个要回答的问题这些注册项在固件镜像里是怎么摆的是和普通代码混在一起还是有专门的地方有专门的地方。用objdump -h看一个 nRF54 NCS 构建的 BLE 应用固件输出节选如下Idx Name Size VMA LMA 0 rom_start 0000047c 00011800 1 text 00054418 00011c80 - 代码 3 initlevel 000000e8 000660a0 - SYS_INIT 注册项独立分区 4 device_area 00000168 00066188 - 设备对象独立分区 12 bt_l2cap_fixed_chan_area ... - 蓝牙 L2CAP 通道 13 bt_conn_cb_area 0000028 00066c34 - BT_CONN_CB_DEFINE 回调 14 bt_gatt_service_static_area ... - GATT 服务 17 shell_root_cmds_area ... - shell 命令 22 rodata 00014120 00066fe0 - 普通只读数据三个观察每类注册数据是一个独立的、命名的输出 sectioninitlevel、device_area、bt_conn_cb_area、shell_root_cmds_area……各有自己的名字、大小、地址不是混在.text或.rodata里。它们集中夹在.text代码和.rodata普通只读数据之间占一段连续 flash。BT_CONN_CB_DEFINE的注册项就在bt_conn_cb_area里——我那个没人调用却执行的回调物理上就躺在这里。为什么不直接放 .rodata.rodata是编译器自动收集的普通只读数据——字符串、const 变量什么类型都有混在一起。而框架要在运行期遍历注册项需要三样东西.rodata给不了确定的起止边界才能知道从哪遍历到哪同类型连续排列指针步进才能正好踩到下一个元素不被链接器当垃圾丢弃——这些变量没有被任何代码直接引用链接器的--gc-sections默认会把它们扔掉。所以 Zephyr 给每类注册数据开一个独立的命名 section单独收集、单独保活。分区大小是多少没有预设是收集多少算多少ITERABLE_SECTION_ROM(type, ...)在链接器脚本里不写死大小。一个分区的大小 链接期收集到的所有同类元素之和由有多少个源文件用了注册宏决定。拿initlevelSYS_INIT 分区实测验证nm取出边界符号地址_init_list起 0x660a0、止 0x66188大小 0xe8 232 字节再看每个__init_*符号大小都是 8 字节一个struct init_entry共 29 个——29 × 8 232严丝合缝。没有任何预设容量注册多少分区就多大。二、三层拆解编译期、链接期、运行期各干什么机制分三层协作每层只干一件事。编译期宏把变量塞进专属 section以STRUCT_SECTION_ITERABLE(my_data, d1)为例展开链走三层#define TYPE_SECTION_ITERABLE(type, varname, secname, postfix) \ Z_DECL_ALIGN(type) varname \ __in_section(_##secname, static, _CONCAT(postfix, _)) __used __noasan /* Z_DECL_ALIGN(type) __aligned(__alignof(type)) type */ /* ___in_section(a,b,c) __attribute__((section(. a . b . c))) */最终完全展开__aligned(__alignof(struct my_data)) struct my_data d1 __attribute__((section(.my_data.static._d1_))) __attribute__((__used__)) { .a 1, .b 2 };两个细节值得注意section 名拼成.my_data.static._d1_——类型名 固定的 static 段 变量名这是 Zephyr 的命名规范后面链接器就按这个模式通配收集__used__防止编译器把这个没被直接引用的变量优化掉。加const前缀就放 ROM省 RAMXIP 系统直接在 flash 上读不加放 RAM。对应地链接器侧要用ITERABLE_SECTION_ROM或ITERABLE_SECTION_RAM。链接期收集、排序、打边界链接器脚本里的声明长这样my_data_area : { _my_data_list_start .; KEEP(*(SORT_BY_NAME(.my_data.static.*))); _my_data_list_end .; } ROMABLE_REGION三件事通配收集所有源文件里.my_data.static.*的输入 section不管在哪个 .c 里全收进my_data_area这一个输出 section——这就是注册项散在各源文件也能聚起来的原因按名排序SORT_BY_NAME按变量名字典序排遍历顺序稳定不依赖链接顺序打边界符号_my_data_list_start/_my_data_list_end钉在首尾这就是运行期遍历的起止点。KEEP则阻止--gc-sections把整段当垃圾丢掉和编译期的__used__是双保险。运行期在两个边界之间遍历STRUCT_SECTION_FOREACH(my_data, d)展开后等价于for (struct my_data *d _my_data_list_start; d _my_data_list_end; d) { /* 你的循环体 */ }起点取边界符号终点和边界符号比较指针每次自动跳sizeof(struct my_data)——全程不需要知道有几个元素。新增一个注册宏_list_end自动右移循环自动多跑一次删一个就少跑一次。代码和链接器脚本都不用改。这就是我那个蓝牙回调的秘密BT_CONN_CB_DEFINE(my_callbacks)把结构体放进bt_conn_cb_areaconn.c里的事件处理代码用STRUCT_SECTION_FOREACH遍历这个分区、逐个调用回调——没人调用是错觉调用方在框架里而且不点名调你是把整个分区的人都叫一遍。三、你身边全是它的应用这套机制是 Zephyr 的通用基础设施NCS 里遍地都是你写过的宏它进的那个分区谁在遍历SYS_INIT(fn, level, prio)initlevel启动流程按 level/prio 逐个调用BT_CONN_CB_DEFINE(...)bt_conn_cb_area蓝牙连接事件分发BT_GATT_SERVICE_DEFINE(...)bt_gatt_service_static_areaGATT 服务注册SETTINGS_STATIC_HANDLER_DEFINE(...)settings_handler_static配置加载时按名匹配回调SHELL_CMD_REGISTER(...)shell_root_cmdsshell 启动时识别命令理解了这一层NCS 里那些写一行宏就自动生效的写法就都不神秘了——它们全是同一套编译期分区 链接期收集 运行期遍历。四、几个常见疑问Q为什么__used__和KEEP都要一个不够吗不够。变量没被代码直接引用编译器会优化掉__used__拦链接器--gc-sections会丢 sectionKEEP拦。两道关卡在不同阶段缺一个都活不到运行期。Q多个注册项的执行顺序谁说了算SORT_BY_NAME按变量名字典序和链接顺序无关多次构建稳定。需要特定顺序就用命名控制01_handler、02_handler。Q注册项跨多个源文件能收集到一起吗能这正是机制的核心——各源文件的输入 section 名遵守同一命名模式链接器一个通配全收。Qsection 名里那段static是什么表示静态注册编译期固定区别于运行期动态注册。链接器的收集通配模式就是按这个命名约定写的自定义分区也必须遵守。写在最后回到开头那行没人调用却执行的宏背后是 Zephyr 用链接器做出来的一个注册表系统——编译期挂标签链接期归堆运行期扫堆。掌握它之后自然会有下一个问题能不能自己开一个这样的分区让业务模块也做到注册即生效能。下一篇就讲怎么在 NCS 工程里自定义一个 iterable section从定义结构体、写链接器片段、接 CMake 到遍历调用附完整可跑的示例和踩坑排查清单。有用的话点个在看让更多工程师看到。你在 NCS 里还发现过哪些写一行宏就自动生效的写法评论区聊聊。标签ZephyrNordic蓝牙开发链接器嵌入式