分析简单的ARM汇编代码

时间:2015-01-05 22:42:15

标签: assembly arm

我开始使用GNU工具链研究ARM汇编语言,并使用以下代码在C中创建一个非常简单的函数示例:

#include <stdint.h>

    uint32_t *a;
    uint32_t *b;
    uint32_t *c;

     __attribute__((naked)) void f() {

             *a += *c;
             *b += *c;
       }

在终端中使用此命令后查看汇编代码:

arm-none-eabi-gcc -O1 -S -std=c99 example.c -o -

这就是结果:

    @ Function supports interworking.
    @ Naked Function: prologue and epilogue provided by programmer.
    @ args = 0, pretend = 0, frame = 0
    @ frame_needed = 0, uses_anonymous_args = 0
    ldr r3, .L2
    ldr r2, .L2+4
    ldr r3, [r3]
    ldr r2, [r2] ; r2 <- &(*c)
    ldr ip, [r3]
    ldr r0, [r2] ; r0 <- *c
    ldr r1, .L2+8
    add r0, ip, r0
    str r0, [r3]
    ldr r3, [r1]
    ldr r2, [r2] ; why make the same thing 
    ldr r1, [r3]
    add r2, r1, r2
    str r2, [r3]
.L3:
    .align  2
.L2:
    .word   a
    .word   c
    .word   b
    .size   f, .-f
    .comm   c,4,4
    .comm   b,4,4
    .comm   a,4,4

我的问题是为什么编译器加载指针c的地址的两倍,如果我很清楚,那就是行

ldr  r2, [r2] 

我找不到编译器复制此代码的充分理由。 提前谢谢。

3 个答案:

答案 0 :(得分:5)

如果您的指针别名,则需要两个解引用。如果您有a == c,请考虑算法的作用。如果他们不能使用别名,则需要添加一些restrict个关键字。以下是优化预期方式的示例:

#include <stdint.h>

void f(uint32_t * restrict a, uint32_t * restrict b, uint32_t * restrict c)
{
    *a += *c;
    *b += *c;
}

汇编输出(评论我的):

00000000 <f>:
   0:   e5922000    ldr r2, [r2]     // r2 = *c
   4:   e5903000    ldr r3, [r0]     // r3 = *a
   8:   e0833002    add r3, r3, r2   // r3 = r3 + r2 = *a + *c
   c:   e5803000    str r3, [r0]     // *a = r3 = *a + *c
  10:   e5910000    ldr r0, [r1]     // r0 = *b
  14:   e0800002    add r0, r0, r2   // r0 = r0 + r2 = *b + *c
  18:   e5810000    str r0, [r1]     // *b = r0 = *b + *c
  1c:   e12fff1e    bx  lr

编辑:这是一个更像原始示例的示例,第一个没有restrict个关键字,第二个是GCC的输出格式。

示例一(没有restrict个关键字)代码:

#include <stdint.h>

__attribute__((naked))
void f(uint32_t *a, uint32_t *b, uint32_t *c)
{
    *a += *c;
    *b += *c;
}

输出:

f:
    ldr ip, [r0, #0]
    ldr r3, [r2, #0]
    add r3, ip, r3
    str r3, [r0, #0]
    ldr r0, [r1, #0]
    ldr r3, [r2, #0]
    add r3, r0, r3
    str r3, [r1, #0]

示例二(restrict个关键字)代码:

#include <stdint.h>

__attribute__((naked))
void f(uint32_t * restrict a, uint32_t * restrict b, uint32_t * restrict c)
{
    *a += *c;
    *b += *c;
}

输出:

f:
    ldr r3, [r2, #0]
    ldr ip, [r1, #0]
    ldr r2, [r0, #0]
    add r2, r2, r3
    add r3, ip, r3
    str r2, [r0, #0]
    str r3, [r1, #0]

c的第二次解除引用不在第二个程序中,将其缩短一个指令。

答案 1 :(得分:0)

连续执行ldr rX, [rX]意味着双重解除引用rX指向的任何内容。

如果我的问题是正确的,那么首先要说的是:

ldr r2, [r2] ; r2 <- &(*c)

然后第二个变成

ldr r2, [r2] ; r2 <- *(r2)

如果那不是GCC docs的问题(参见粗体部分):

  

     

此属性可在ARM,AVR,MCORE,MSP430上获得,   NDS32,RL78,RX和SPU端口。它允许编译器构造   必要的功能声明,同时允许身体的   函数是汇编代码。指定的功能不会   编译器生成的序言/结尾序列。 仅限基本asm   语句可以安全地包含在裸函数中(参见Basic Asm)。   使用Extended asm或Basic asm和“C”代码的混合可能   似乎工作,他们不能依赖于可靠和工作   不受支持。

答案 2 :(得分:0)

add会破坏r0,所以我们失去了c的值并且必须重新加载它

ldr r2, .L2+4   get address of .data location of *c from .text
...
ldr r2, [r2] ; r2 = pointer to c
...
ldr r0, [r2] ; r0  = c
...
add r0, ip, r0 ; this destroys r0 it no longer holds the value of c
...
ldr r2, [r2] ; need the value of c again to add to b

有趣的是,不同版本的gcc和/或不同的优化选择不同的寄存器组合。但是附加负载的序列相同。这里最重要的是它为什么这样做:

add r0, ip, r0
str r0, [r3]

而不是

add ip, ip, r0
str ip, [r3]

然后不需要重新加载c?

窥视孔优化器的细微差别是我的猜测。另一个相关的问题是为什么在完成存储之前开始弄乱** b?如果没有这样做,它将有另一个免费注册。 (毫无疑问是另一种优化)

另一个有趣的观点是我的gcc编译器中至少有一个产生了这个:

00001000 <_start>:
    1000:   eaffffff    b   1004 <fun>

00001004 <fun>:
    1004:   e59f2034    ldr r2, [pc, #52]   ; 1040 <fun+0x3c>
    1008:   e59f3034    ldr r3, [pc, #52]   ; 1044 <fun+0x40>
    100c:   e5921000    ldr r1, [r2]
    1010:   e5932000    ldr r2, [r3]
    1014:   e591c000    ldr ip, [r1]
    1018:   e5920000    ldr r0, [r2]
    101c:   e59f3024    ldr r3, [pc, #36]   ; 1048 <fun+0x44>
    1020:   e08c0000    add r0, ip, r0
    1024:   e5933000    ldr r3, [r3]
    1028:   e5810000    str r0, [r1]
    102c:   e5922000    ldr r2, [r2]
    1030:   e5931000    ldr r1, [r3]
    1034:   e0812002    add r2, r1, r2
    1038:   e5832000    str r2, [r3]
    103c:   e12fff1e    bx  lr
    1040:   00009054    andeq   r9, r0, r4, asr r0
    1044:   00009050    andeq   r9, r0, r0, asr r0
    1048:   0000904c    andeq   r9, r0, ip, asr #32

Disassembly of section .bss:

0000904c <__bss_start>:
    904c:   00000000    andeq   r0, r0, r0

00009050 <c>:
    9050:   00000000    andeq   r0, r0, r0

00009054 <a>:
    9054:   00000000    andeq   r0, r0, r0

无论有没有裸体,你都会得到同样的东西,为什么gcc如此绝望地使用每个一次性寄存器而不是使用堆栈。注意在你的编译中它添加一个然后存储它在我的它添加一个然后加载* b然后存储一个。它不仅在序列中向上移动了** b的负载,而且在完成a的结果之前还加载了* b。

所以除了在函数末尾删除bx lr之外,裸体事物在这里没有帮助。您可以/应该尝试的是-fdump-rtl-all在gcc命令行上(制作大量文件)并逐步查看gcc开始的位置以及更改内容的位置,这可能会决定输出或是否不是在编译器的内部,然后在后端,窥孔优化器重新安排了事情,不知道命令行是什么来转储它。

底线是,虽然长期(成千上万,数十万,数百万行代码),编译器/ optmizer将胜过人类,但很容易捕获优化代码的孤立部分可以手动调整一点点&#34;更好&#34;取决于你的定义更好。请注意,较少的指令并不总是更好。