Keil uVision下无需修改链接脚本获取ARM C自定义段首尾符号及防移除
Keil ARM环境下不依赖自定义链接脚本的自定义函数表遍历实现方案
先理清你之前尝试时遇到问题的根源
- 用
__attribute__((section("ARM.at.<address>")))会导致地址重叠,本质是因为ARM.at.是Keil armlink链接器保留的专用段前缀,所有带这个前缀的段都会被强制放到指定的绝对地址上,不会自动按顺序排列,同前缀段必然重叠,根本不适合做自动排列的连续函数表。 - 反汇编找不到段首尾标识,不是Keil没生成边界符号,是找错了符号命名规则:armlink生成的段边界符号根本不是裸的
__section_start形式,而是固定格式的Image$$<段名>$$<段属性>$$Start/Limit,带着Image前缀,找双下划线开头的符号自然找不到。 - 哪怕加了
volatile、used属性,自定义段还是被链接器剔除,是因为这两个属性全是编译阶段的优化抑制标记,管不到链接器的段级垃圾收集逻辑。armlink默认开启的GC规则是:只要一个段没有被其他代码段直接引用,不管段内的符号加了多少编译期属性,整个段都会被当垃圾回收。
零自定义链接脚本的实现逻辑
Keil自带的编译器和链接器本身就支持跨模块自动收集连续段的功能,完全不需要手写scatter链接脚本,核心机制是:
- 自定义段里存放固定大小的函数指针(不直接存函数本身的机器码,避免长度不匹配导致的遍历错误)
- 用编译器属性把这些函数指针分散放到同一个自定义段名下,跨模块的表项会被链接器自动排列成连续的内存块
- 直接使用链接器自动生成的段边界符号做遍历起点和终点,不需要手动定义符号
- 用链接器级别的保留标记阻止GC回收自定义段
具体实现步骤
1. 定义跨模块的段标记宏
typedef void (*func_t)(void); // 两个属性的作用: // section: 指定当前符号要放置的自定义段名(不要用ARM开头的保留前缀) // retain: 链接器级别的保留标记,告诉armlink不要回收这个段(armclang V6编译器支持,即Keil MDK5.20以上默认搭载的编译器) #define EXPORT_TO_FUNC_TABLE __attribute__((section(".custom_func_table"), retain))
如果使用老版本ARMCC V5编译器,没有retain属性,可以直接在Keil项目的「Options for Target」→「Linker」→「Misc controls」输入框里加--keep=.custom_func_table**,效果完全等价,不需要修改或手写链接脚本。
2. 跨模块添加表项
不需要集中维护一个全局数组,各个独立模块可以直接往自定义段里添加自己的函数指针:
// 模块A的代码 void func_a(void) { // 具体功能逻辑 } const func_t entry_a EXPORT_TO_FUNC_TABLE = func_a; // 模块B的代码 void func_b(void) { // 具体功能逻辑 } const func_t entry_b EXPORT_TO_FUNC_TABLE = func_b;
所有带entry_标记的符号,都会被链接器自动排到.custom_func_table这个连续的只读内存块里。
3. 遍历执行的init函数
直接引用链接器自动生成的段边界符号,不需要手动定义起止地址:
// 声明armlink自动生成的段边界符号,命名严格遵循armlink规则: // Image$$<自定义段名>$$<段属性>$$Start/Limit // 因为表项是const的函数指针,属于只读段,所以属性为RO extern uint32_t Image$$.custom_func_table$$RO$$Start; extern uint32_t Image$$.custom_func_table$$RO$$Limit; void init_func(void) { // 把段边界地址转成函数指针类型 func_t *iter = (func_t *)&Image$$.custom_func_table$$RO$$Start; func_t *end = (func_t *)&Image$$.custom_func_table$$RO$$Limit; // 按指针大小逐表项遍历(32位ARM下每次移动4字节,和函数指针长度匹配) while (iter < end) { (*iter)(); iter++; } }
常见问题说明
- 不能把函数本身的代码直接放到自定义段里遍历:因为每个函数的机器码长度不固定,按固定的指针步长遍历的话,会直接把函数内部的机器码当函数地址跳转,引发跑飞。自定义段只存固定长度的函数指针,才能保证遍历步长正确。
used属性无法阻止链接器删段:used的作用是告诉编译器,这个符号在当前编译单元里哪怕没被直接引用,也不要优化掉,但到了链接阶段,armlink的GC逻辑根本不会识别编译期属性,只要段没有被其他代码引用就会回收。必须配合retain属性或者Keil GUI里的keep配置,才能阻止链接器删除自定义段。- 反汇编找边界符号的时候,直接搜
Image$$.custom_func_table关键词,就能定位到对应的Start和Limit符号,不需要找双下划线开头的自定义符号。
内容的提问来源于stack exchange,提问作者Martel
相关产品推荐
相关产品推荐

