GCC编译ARM Thumb时LDR加载.rodata指针为何多出ADDS指令
为什么ARM Thumb指令集下GCC加载.rodata段指针后会生成额外ADDS指令
问题复现
测试代码
const char padding[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; const char myTable[] = { 1, 2, 3, 4 }; int keepPadding() { return (int)(&padding); } int foo() { return (int)(&myTable); // 核心观测代码段 }
编译参数
使用arm-none-eabi-gcc 11.2.1版本,针对Cortex-M0内核Thumb指令集编译,优化等级为-Os:
arm-none-eabi-gcc -Os -c -mcpu=cortex-m0 -mthumb temp.c -S
观测到的汇编现象
生成的汇编中foo函数没有直接从常量池加载myTable的地址,而是先加载.rodata段锚点.LANCHOR0,再通过adds指令加10字节偏移得到目标地址:
foo: @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 @ link register save eliminated. ldr r0, .L5 @ sp needed adds r0, r0, #10 bx lr .L6: .align 2 .L5: .word .LANCHOR0 .size foo, .-foo .align 1 .global bar .syntax unified .code 16 .thumb_func .type bar, %function ... myTable: .ascii "\001\002\003\004"
补充观测结论
- 去掉
const修饰将myTable放入.data段时,不会生成该额外ADDS指令 - 若
myTable位于.rodata段偏移0位置,ADDS指令也会消失;实际项目中.rodata段通常存在多个前置对象,示例中添加padding数组就是为了复现真实编译场景
底层成因
这个行为是GCC的段锚点(Section Anchors)优化在Cortex-M0等无MOVW/MOVT宽立即数加载指令的ARMv6-M架构上的正常表现:
- 对不支持ARM/Thumb状态切换、也没有32位立即数直接加载指令的Cortex-M0来说,所有32位地址都必须存储在函数附近的常量池中,再通过LDR指令读取加载。
- 开启-Os体积优化时,GCC不会为同一个编译单元内同一段的每个对象单独在常量池存储完整地址,而是会选定一个段起始锚点(也就是汇编中的
.LANCHOR0),同段内其他对象的地址都通过「锚点地址+编译期固定偏移」计算得到。这个设计的初衷是减少常量池占用的Flash空间:如果同一段有N个对象被取地址,原本需要N个4字节的常量池条目,使用锚点方案仅需要1个条目,剩余地址计算用1个16位字长的ADDS指令即可完成,总固件体积更小。 - 该优化仅对.rodata生效的原因是:.data段的运行时地址需要在启动阶段从Flash复制到RAM,重定位逻辑更复杂,GCC默认不会对.data段做锚点合并;而.rodata是只读段,加载地址和运行地址完全一致,链接阶段仅需要修正锚点地址,所有偏移计算都是编译期确定的固定值,不会额外增加重定位开销。
- 生成的ADDS指令并非冗余:如果直接在常量池存储
myTable的绝对地址,当多个函数都取同一段内不同.rodata对象的地址时,常量池会明显膨胀,反而增大总固件体积,当前代码形态是GCC在-Os等级下优先缩小代码体积的权衡结果。
消除额外ADDS指令的方法
如果场景中执行效率的优先级高于代码体积,可以通过以下方式调整:
- 给需要高频取地址的const对象加上
__attribute__((section(".rodata.<自定义名称>")))单独分配独立段,让它成为所在段的锚点,偏移为0时ADDS指令会自动消失 - 编译时添加
-fno-section-anchors参数关闭全局段锚点优化,此时GCC会为每个取地址的对象单独生成常量池条目,不会再生成偏移计算指令,但会略微增加常量池占用的Flash空间 - 调整编译单元内const对象的定义顺序,把最常被取地址的对象放在所有const定义的最靠前位置,让它成为默认的段锚点,也能消除对应位置的ADDS指令
内容的提问来源于stack exchange,提问作者Mike
相关产品推荐
相关产品推荐

