You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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链接脚本,核心机制是:

  1. 自定义段里存放固定大小的函数指针(不直接存函数本身的机器码,避免长度不匹配导致的遍历错误)
  2. 用编译器属性把这些函数指针分散放到同一个自定义段名下,跨模块的表项会被链接器自动排列成连续的内存块
  3. 直接使用链接器自动生成的段边界符号做遍历起点和终点,不需要手动定义符号
  4. 用链接器级别的保留标记阻止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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.28 22:06:10