【问题标题】:Difference in ARM and x86 assembly code generated by GCCGCC生成的ARM和x86汇编代码的区别
【发布时间】:2014-03-24 23:08:33
【问题描述】:

让我们用一个简单的 C 代码来设置一个寄存器:

int main()
{
    int *a = (int*)111111;
    *a = 0x1000;
    return 0;
}

当我使用 1 级优化为 ARM (arm-none-eabi-gcc) 编译此代码时,汇编代码类似于:

mov     r2, #4096
mov     r3, #110592
str     r2, [r3, #519]
mov     r0, #0
bx      lr

看起来地址 111111 被解析到最近的 4K 边界 (110592) 并移动到 r3,然后通过将 519 与 110592 (=111111) 相加来存储值 4096(0x1000)。为什么会这样?

在 x86 中,组装很简单:

movl    $4096, 111111
movl    $0, %eax
ret

【问题讨论】:

  • 我想提醒您不要根据生成的汇编指令的数量来评估这些架构。不同的汇编指令的数量并不能告诉您使用了多少 字节 代码,也不能告诉您任何有关执行时间的信息。 x86 ISA 是在人类仍然进行大量汇编编码的时代设计的,因此即使在其他方面效率低下,拥有“直截了当”的指令也很重要。
  • x86 使用可变字长指令,因此您可以使用任何 32 位或 64 位立即数的单条指令。 Arm 是固定指令长度,因此您不能立即获得任何 32 或 64 位,它需要多个指令和/或它需要具有该值的位置的 pc 相对负载。编译器将选择一个或另一个选项。

标签: gcc assembly compiler-construction x86 arm


【解决方案1】:

编译器可能正在利用ARM immediate value encoding 来减少代码大小。基本上 110592 是0x1B << 12,这可以进行一些简化。查看程序的arm-none-eabi-objdump -d 的输出以检查每条指令的长度。

【讨论】:

  • 这并没有减少代码大小:替代方法是从内存中加载地址。编译器在这里选择使用立即常量来形成地址以节省加载 - 这可能会错过缓存,并且可能需要比用于加载立即常量的单个周期更长的时间。
  • 立即常量使用 12 位,而不是 32 位。它可以在完整的 32 位指令中对常量进行编码。因此,一条 32 位指令替换了 32 位内存,其中包含常量加上 16 位(在 ARM 模式下甚至是 32 位)指令。
【解决方案2】:

这种编码背后的原因是因为 x86 具有可变大小的指令——从 1 字节到 16 字节(甚至可能更多带有前缀)。

ARM 指令是 32 位宽(不包括 Thumb 模式),这意味着根本不可能在单个操作码中编码所有 32 位宽的常量(立即数)。

固定大小的架构通常使用几种方法来加载大常量:

1)  movi  #r1, Imm8  ; // Here Imm8 or ImmX is simply X least significant bits
2)  movhi #r1, Imm16 ; // Here Imm16 loads the 16 MSB of the register
3)  load  #r1, (PC + ImmX);  // use PC-relative address to put constant in code
4)  movn  #r1, Imm8 ;  // load the inverse of Imm8 (for signed constants) 
5)  mov(i/n) #1, Imm8 << N;       // where N=0,8,16,24

可变大小的架构 OTOH 可以将所有常量放在一条指令中:

xx xx xx 00 10 00 00 11 11 11 00 ; // assuming that it takes 3 bytes to encode
                                 ; // the instruction and the addressing mode
; added with 4 bytes to encode the 4096 and 4 bytes to encode 0x00111111

【讨论】:

  • mov [dword], dwordC7 05 dword dword,顺便说一下(在 32 位模式下)
【解决方案3】:

地址必须被分成两部分,因为这个特定的常数不能用一条指令加载到寄存器中。

ARM documentation 指定了某些指令(例如MOV)中允许的立即数常量的限制:

在 ARM 指令中,常量可以有任何可以产生的值 通过将一个 8 位值右旋转一个内的任意偶数位 32 位字。

在 32 位 Thumb-2 指令中,常量可以是:

任何可以通过将 8 位值左移得到的常数 32 位字中的任意位数。

0x00XY00XY 形式的任何常量。
0xXY00XY00 形式的任何常量。
0xXYXYXYXY 形式的任何常量。

111111(十六进制的1B207)不能表示为上述任何一个,因此编译器必须对其进行拆分。

1105921B000,所以它满足第一个条件(8 位值 0x1B 左移 12 位),可以使用 MOV 指令加载。

另一方面,STR 指令具有 a different set of limitations 用于使用的偏移量。特别是,519 (0x207) 属于 ARM 模式下字存储/加载所允许的 -4095 到 4095 范围。


在这种特定情况下,编译器设法将常量分成两部分。如果您的立即数有更多位,它可能必须生成更多指令,或使用文字池加载。例如,如果我使用0xABCDEF78,我会得到这个(对于 ARMv7):

movw    r3, #61439
movt    r3, 43981
mov     r2, #4096
str     r2, [r3, #-135]
mov     r0, #0
bx      lr

对于没有 MOVW/MOVT 的架构(例如 ARMv4),GCC 似乎回退到文字池:

    mov     r2, #4096
    ldr     r3, .L2
    str     r2, [r3, #-135]
    mov     r0, #0
    bx      lr
.L3:
    .align  2
.L2:
    .word   -1412567041

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-05-20
    • 1970-01-01
    • 1970-01-01
    • 2014-02-03
    • 1970-01-01
    • 1970-01-01
    • 2020-03-03
    • 1970-01-01
    相关资源
    最近更新 更多