让我们用一个简单的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
答案 0 :(得分:5)
这种编码背后的原因是因为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
答案 1 :(得分:2)
地址必须分为两部分,因为这个特定的常量不能用一条指令加载到寄存器中。
ARM documentation指定了某些指令(例如MOV
)中允许的直接常量的限制:
在ARM指令中,常量可以包含任何可以生成的值 通过将a 8位值向右旋转任意偶数位 32位字。
在32位Thumb-2指令中,常量可以是:
通过移动剩余的8位值可以产生的任何常数 32位字内的任意位数。
任何形式为0x00XY00XY的常量。
任何形式为0xXY00XY00的常量 任何形式为0xXYXYXYXY的常量。
值111111
(十六进制中的1B207
)不能表示为上述任何一个,因此编译器必须将其拆分。
110592
为1B000
因此它满足第一个条件(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
答案 2 :(得分:1)
编译器可能正在利用ARM immediate value encoding来减少代码大小。基本上110592是0x1B << 12
,这可以实现一些简化。看一下程序arm-none-eabi-objdump -d
的输出,检查每条指令的长度。