4

我在 c 中编写了一个非常简单的 memset,它在 -O2 之前都可以正常工作,但在 -O3 之前就不行了……

内存集:

void * memset(void * blk, int c, size_t n)
{
    unsigned char * dst = blk;

    while (n-- > 0)
        *dst++ = (unsigned char)c;

    return blk;
}

...在使用 -O2 时编译为该程序集:

20000430 <memset>:
20000430:       e3520000        cmp     r2, #0                  @ compare param 'n' with zero
20000434:       012fff1e        bxeq    lr                      @ if equal return to caller
20000438:       e6ef1071        uxtb    r1, r1                  @ else zero extend (extract byte from) param 'c'
2000043c:       e0802002        add     r2, r0, r2              @ add pointer 'blk' to 'n'
20000440:       e1a03000        mov     r3, r0                  @ move pointer 'blk' to r3
20000444:       e4c31001        strb    r1, [r3], #1            @ store value of 'c' to address of r3, increment r3 for next pass
20000448:       e1530002        cmp     r3, r2                  @ compare current store address to calculated max address
2000044c:       1afffffc        bne     20000444 <memset+0x14>  @ if not equal store next byte
20000450:       e12fff1e        bx      lr                      @ else back to caller

这对我来说很有意义。我注释了这里发生的事情。

当我用 -O3 编译它时,程序崩溃了。我的 memset 反复调用自己,直到它吃掉整个堆栈:

200005e4 <memset>:
200005e4:       e3520000        cmp     r2, #0                  @ compare param 'n' with zero
200005e8:       e92d4010        push    {r4, lr}                @ ? (1)
200005ec:       e1a04000        mov     r4, r0                  @ move pointer 'blk' to r4 (temp to hold return value)
200005f0:       0a000001        beq     200005fc <memset+0x18>  @ if equal (first line compare) jump to epilogue
200005f4:       e6ef1071        uxtb    r1, r1                  @ zero extend (extract byte from) param 'c'
200005f8:       ebfffff9        bl      200005e4 <memset>       @ call myself ? (2)
200005fc:       e1a00004        mov     r0, r4                  @ epilogue start. move return value to r0
20000600:       e8bd8010        pop     {r4, pc}                @ restore r4 and back to caller

我无法弄清楚这个优化版本应该如何在没有任何strb或类似的情况下工作。如果我尝试将内存设置为“0”或其他内容并不重要,因此该函数不仅在 .bss(零初始化)变量上调用。

(1) 这是一个问题。当函数因为 'n' 为零而没有提前退出时,这个推送会在没有匹配的 pop 的情况下无休止地重复,因为它由 (2) 调用。我用UART打印验证了这一点。r2 也从未被触及,那么为什么与零的比较应该成为真的?

请帮助我了解这里发生了什么。编译器是否假设了我可能无法满足的先决条件?

背景:我在我的裸机项目中使用需要 memset 的外部代码,所以我自己开发了。它只在启动时使用一次,而不是性能关键。

/edit:使用以下选项调用编译器:

arm-none-eabi-gcc -O3 -Wall -Wextra -fPIC -nostdlib -nostartfiles -marm -fstrict-volatile-bitfields -march=armv7-a -mcpu=cortex-a9 -mfloat-abi=hard -mfpu=neon-vfpv3
4

1 回答 1

3

你的第一个问题(1)。如果要进行嵌套函数调用,则根据调用约定,您需要保留链接寄存器,并且需要 64 位对齐。该代码使用 r4 ,因此这是保存的额外寄存器。那里没有魔法。

你的第二个问题(2)它没有调用你的 memset,它正在优化你的代码,因为它认为它是一个低效的 memset。Fuz 已为您的问题提供了答案。

重命名函数

00000000 <xmemset>:
   0:   e3520000    cmp r2, #0
   4:   e92d4010    push    {r4, lr}
   8:   e1a04000    mov r4, r0
   c:   0a000001    beq 18 <xmemset+0x18>
  10:   e6ef1071    uxtb    r1, r1
  14:   ebfffffe    bl  0 <memset>
  18:   e1a00004    mov r0, r4
  1c:   e8bd8010    pop {r4, pc}

你可以看到这个。

如果您按照 Fuz 的建议使用 -ffreestanding,那么您会看到这个或类似的东西

00000000 <xmemset>:
   0:   e3520000    cmp r2, #0
   4:   012fff1e    bxeq    lr
   8:   e92d41f0    push    {r4, r5, r6, r7, r8, lr}
   c:   e2426001    sub r6, r2, #1
  10:   e3560002    cmp r6, #2
  14:   e6efe071    uxtb    lr, r1
  18:   9a00002a    bls c8 <xmemset+0xc8>
  1c:   e3a0c000    mov r12, #0
  20:   e3520023    cmp r2, #35 ; 0x23
  24:   e7c7c01e    bfi r12, lr, #0, #8
  28:   e1a04122    lsr r4, r2, #2
  2c:   e7cfc41e    bfi r12, lr, #8, #8
  30:   e7d7c81e    bfi r12, lr, #16, #8
  34:   e7dfcc1e    bfi r12, lr, #24, #8
  38:   9a000024    bls d0 <xmemset+0xd0>
  3c:   e2445009    sub r5, r4, #9
  40:   e1a03000    mov r3, r0
  44:   e3c55007    bic r5, r5, #7
  48:   e3a07000    mov r7, #0
  4c:   e2851008    add r1, r5, #8
  50:   e1570005    cmp r7, r5
  54:   f5d3f0a0    pld [r3, #160]  ; 0xa0
  58:   e1a08007    mov r8, r7
  5c:   e583c000    str r12, [r3]
  60:   e583c004    str r12, [r3, #4]
  64:   e2877008    add r7, r7, #8
  68:   e583c008    str r12, [r3, #8]
  6c:   e2833020    add r3, r3, #32
  70:   e503c014    str r12, [r3, #-20] ; 0xffffffec
  74:   e503c010    str r12, [r3, #-16]
  78:   e503c00c    str r12, [r3, #-12]
  7c:   e503c008    str r12, [r3, #-8]
  80:   e503c004    str r12, [r3, #-4]
  84:   1afffff1    bne 50 <xmemset+0x50>
  88:   e2811001    add r1, r1, #1
  8c:   e483c004    str r12, [r3], #4
  90:   e1540001    cmp r4, r1
  94:   8afffffb    bhi 88 <xmemset+0x88>
  98:   e3c23003    bic r3, r2, #3
  9c:   e1520003    cmp r2, r3
  a0:   e0466003    sub r6, r6, r3
  a4:   e0803003    add r3, r0, r3
  a8:   08bd81f0    popeq   {r4, r5, r6, r7, r8, pc}
  ac:   e3560000    cmp r6, #0
  b0:   e5c3e000    strb    lr, [r3]
  b4:   08bd81f0    popeq   {r4, r5, r6, r7, r8, pc}
  b8:   e3560001    cmp r6, #1
  bc:   e5c3e001    strb    lr, [r3, #1]
  c0:   15c3e002    strbne  lr, [r3, #2]
  c4:   e8bd81f0    pop {r4, r5, r6, r7, r8, pc}
  c8:   e1a03000    mov r3, r0
  cc:   eafffff6    b   ac <xmemset+0xac>
  d0:   e1a03000    mov r3, r0
  d4:   e3a01000    mov r1, #0
  d8:   eaffffea    b   88 <xmemset+0x88>

看起来它只是内联 memset,它不知道你的代码(更快的那个)。

因此,如果您希望它使用您的代码,请坚持使用 -O2。你的效率很低,所以不知道为什么你需要把它推得更远。

20000444:       e4c31001        strb    r1, [r3], #1            @ store value of 'c' to address of r3, increment r3 for next pass
20000448:       e1530002        cmp     r3, r2                  @ compare current store address to calculated max address
2000044c:       1afffffc        bne     20000444 <memset+0x14>  @ if not equal store next byte

如果没有用其他东西替换你的代码,它不会比这更好。

Fuz 已经回答了这个问题:

使用 -fno-builtin-memset 编译。编译器识别出该函数实现了 memset,因此将其替换为对 memset 的调用。在编写裸机代码时,通常应该使用 -ffreestanding 进行编译。我相信这也解决了这类问题

它正在用 memset 替换您的代码,如果您不希望它这样做,请使用 -ffreestanding。

如果您想超越这一点并想知道为什么 -fno-builtin-memset 不起作用,这是 gcc 人员的一个问题,请提交一张票,让我们知道他们在说什么(或者只查看编译器源代码)。

于 2020-05-18T13:15:08.157 回答