为什么在ARM thumb指令集上,GCC在加载.rodata指针时会产生多余的ADDS指令?

4
这段代码:
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);  // <-- this is the part I'm looking at
}

以下是针对thumb指令集的汇编代码(为了清晰起见进行了缩写),请特别注意foo函数中第二条指令的adds操作:

...
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"


似乎它正在加载到 .rodata 的顶部位置一个指针(ldr),然后通过编程方式偏移至 myTable 的位置(adds)。但是为什么不直接加载表格本身的地址呢?
注意:当我删除 const 时,它似乎会在没有 ADDS 指令的情况下执行(使用 .data 中的 myTable)。
问题的背景是,我正在尝试手动优化一些 C 固件,并注意到这个看起来多余的 adds 指令,所以我想知道是否有一种重构我的代码的方法来摆脱它。
注意:所有这些都是针对 ARM Thumb 指令集编译的,如下所示(使用 arm-none-eabi-gcc 版本 11.2.1):
arm-none-eabi-gcc -Os -c -mcpu=cortex-m0 -mthumb temp.c -S

注意:这里的示例代码旨在表示一个更大代码库的片段。如果myTable是唯一编译的内容,则它会落在.rodata的偏移量0处,并且adds指令将消失,但这并不是真实场景的典型情况。为了表示产生这种汇编语言的典型真实世界情况,我在表格前添加了填充物。

另请参见这里在Godbolt上的重现


1
什么gcc版本?我在godbolt上无法得到任何生成您汇编代码的版本,它总是一个单独的ldr - Jester
1
gcc 可能会尝试加载文件中所有常量的基地址,然后添加单个偏移量以减少 ldr 指令的数量。但我不确定。 - fuz
1
arm-none-eabi-gcc 版本为 11.2.1。完整的代码示例在此处:https://gist.github.com/coder-mike/d2ccf6e5c9c1dfafec68c295cc82f8c7 - Mike
所以通过保持填充和填充,我可以轻松地让它在9.x.x上重复。在-S输出中,添加是存在的。显然,它将要加载的基地址设置为填充的起始位置,然后再加上10。 - old_timer
它在rodata开头生成了.LANCHOR,因此需要添加10。我想这是为了优化常量的ldr指令。也许在链接后,如果该地址恰好可以使用立即数而不是相对于pc的方式加载到r0中,则会发生这种情况。 - old_timer
显示剩余8条评论
1个回答

2
这是需要翻译的内容:

最初的问题只包含以下内容:

const char myTable[] = { 1, 2, 3, 4 };
int foo() {
  return (int)(&myTable);
}


arm-none-eabi-gcc -Os -c -mthumb so.c -o so.o
arm-none-eabi-objdump -D so.o

但是它没有产生广告:
Disassembly of section .text:

00000000 <foo>:
   0:   4800        ldr r0, [pc, #0]    ; (4 <foo+0x4>)
   2:   4770        bx  lr
   4:   00000000    andeq   r0, r0, r0

Disassembly of section .rodata:

00000000 <myTable>:
   0:   04030201    streq   r0, [r3], #-513 ; 0xfffffdff

问题已经被编辑为显示可重复的示例,因此本答案也随之进行了编辑。但是我将保留答案,以便朝着相同的解决方案进行工作。可能感兴趣的是,要到达锚点需要几个组件来避免问题被优化掉。 根据您的问题和这个描述:
const char padding[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
const char myTable[] = { 1, 2, 3, 4 };
int foo() {
  return (int)(&myTable);
}

很明显为什么myTable的偏移量是10。

但是填充被优化掉了,所以最终结果仍然相同。

因此:

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 -Os -c -mthumb so.c -S


foo:
    @ Function supports interworking.
    @ 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
    .global myTable
    .global padding
    .section    .rodata
    .set    .LANCHOR0,. + 0
    .type   padding, %object
    .size   padding, 10
padding:
    .space  10
    .type   myTable, %object
    .size   myTable, 4
myTable:
    .ascii  "\001\002\003\004"
    .ident  "GCC: (GNU) 11.2.0"

它生成一个锚点,然后从锚点引用而不是直接引用标签。

我猜测这是为了优化ldr。让我们试一下:

 arm-none-eabi-gcc -Os -c -mthumb -mcpu=cortex-m4 so.c -S

foo:
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    @ link register save eliminated.
    ldr r0, .L5
    bx  lr
.L6:
    .align  2
.L5:
    .word   .LANCHOR0+10
    .size   foo, .-foo

00000008 <foo>:
   8:   4800        ldr r0, [pc, #0]    ; (c <foo+0x4>)
   a:   4770        bx  lr
   c:   0000000a    .word   0x0000000a

是的,这样解决了问题,但是如何链接它呢?
Disassembly of section .rodata:

00000000 <padding>:
    ...

0000000a <myTable>:
   a:   04030201    streq   r0, [r3], #-513 ; 0xfffffdff

Disassembly of section .text:

00000010 <keepPadding>:
  10:   4800        ldr r0, [pc, #0]    ; (14 <keepPadding+0x4>)
  12:   4770        bx  lr
  14:   00000000    andeq   r0, r0, r0

00000018 <foo>:
  18:   4801        ldr r0, [pc, #4]    ; (20 <foo+0x8>)
  1a:   300a        adds    r0, #10
  1c:   4770        bx  lr
  1e:   46c0        nop         ; (mov r8, r8)
  20:   00000000    andeq   r0, r0, r0

我原本希望链接器可以替换pc相对加载指令并将其转换为mov r0,#0...这样可以保留加载指令,这对于不是Cortex-M(甚至不是Cortex-M)的系统可能是一种优化。

注意:这也可以工作。

arm-none-eabi-gcc -Os -c -mthumb -fno-section-anchors so.c -o so.o

00000008 <foo>:
   8:   4800        ldr r0, [pc, #0]    ; (c <foo+0x4>)
   a:   4770        bx  lr
   c:   00000000    andeq   r0, r0, r0
foo:
    @ Function supports interworking.
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    @ link register save eliminated.
    ldr r0, .L5
    @ sp needed
    bx  lr
.L6:
    .align  2
.L5:
    .word   myTable
    .size   foo, .-foo
    .global myTable
    .section    .rodata
    .type   myTable, %object
    .size   myTable, 4
myTable:
    .ascii  "\001\002\003\004"
    .global padding
    .type   padding, %object
    .size   padding, 10

由于锚点未被使用,因此直接使用了myTable的地址。

从我的角度来看,“为什么”是因为使用了锚点,并且前面的填充导致myTable与锚点偏移。因此,加载程序会先加载锚点地址,然后通过添加来得到从锚点到表格的地址。

为什么要使用锚点?由读者或其他人自行决定。


有趣的是,使用-mcpu=cortex-m4时省略了adds指令,但当我尝试使用-mcpu=cortex-m0时却没有省略。 - Mike
1
-fno-section-anchors 对我很有效。它将我的真实代码从8,436B减少到8,424B(只有12B的差异,哈哈)。可能是因为我没有太多的表格。但重点是它对整体大小没有不良影响。我很好奇为什么编译器默认情况下不使用它。 - Mike
1
这看起来是一个不错的答案,但帖子中的元建议并不合适 - 请记住,帖子是为更广泛的读者群体而写的,因此对原始帖子(或原始发布者)的抱怨并不理想。这里的信息似乎有点恼怒 - 如果您在 Stack Overflow 上帮助人们感到疲惫或烦躁,那么也许值得休息一下。 - halfer
1
@halfer 是正确的。但是问题并没有展示出具体的问题。我猜我可以编辑这个问题,但我认为这不是正确的做法,应该由提问者自己来完成。而且显然他已经这样做了。现在也许这个回答需要重新写一下... - old_timer
因此,这不仅是读者的练习,而且是一个研究项目,让读者理解gcc何时使用锚点的问题。 - old_timer
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接