让我们看一个简单的 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 具有可变大小的指令——从 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
地址必须分成两部分,因为这个特定常量无法使用单个指令加载到寄存器中。
ARM 文档指定了某些指令中允许的立即常量的限制(例如
MOV
):
在ARM指令中,常量可以是任何可以产生的值 通过将 8 位值右移任意偶数位 32 位字。
在 32 位 Thumb-2 指令中,常量可以是:
可以通过将 8 位值左移产生的任何常数 32 位字中的任意位数。
任何 0x00XY00XY 形式的常量。
任何 0xXY00XY00 形式的常量。
任何 0xXYXYXYXY 形式的常量。
值
111111
(十六进制的1B207
)不能表示为上述任何一个,因此编译器必须将其拆分。
110592
是 1B000
,因此它满足第一个条件(8 位值 0x1B 左移 12 位),并且可以使用 MOV
指令加载。
另一方面,
STR
指令对于所使用的偏移量有一组不同的限制。特别是,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
编译器可能利用 ARM 立即值编码来减少代码大小。基本上 110592 是
0x1B << 12
,这可以进行一些简化。查看程序 arm-none-eabi-objdump -d
的输出,检查每条指令的长度。