32位系统的通用寄存器GPR都只能容纳32位数据,所以这种平台必须把64位数据转换为一对32位数据才能进行运算。[1]
本节围绕下述程序进行演示。
#include <stdint.h>
uint64_t f ()
{
return 0x1234567890ABCDEF;
};
32位系统用寄存器组合EDX:EAX来回传64位值。
指令清单24.1 Optimizing MSVC 2010
_f PROC
mov eax, -1867788817 ; 90abcdefH
mov edx, 305419896 ; 12345678H
ret 0
_f ENDP
ARM系统用寄存器组合R0-R1来回传64位值。其中,返回值的高32位存储在R1寄存器中,低32位存储于R0寄存器中。
指令清单24.2 Optimizing Keil 6/2013 (ARM mode)
||f|| PROC
LDR r0,|L0.12|
LDR r1,|L0.16|
BX lr
ENDP
|L0.12|
DCD 0x90abcdef
|L0.16|
DCD 0x12345678
MIPS系统使用V0-V1($2-$3)寄存器对来回传64位值。其中,返回值的高32位存储在V0($2)寄存器中,低32位存储于$V1($3)寄存器中。
指令清单24.3 Optimizing GCC 4.4.5 (assembly listing)
li $3,-1867841536 # 0xffffffff90ab0000
li $2,305397760 # 0x12340000
ori $3,$3,0xcdef
j $31
ori $2,$2,0x5678
指令清单24.4 Optimizing GCC 4.4.5 (IDA)
lui $v1, 0x90AB
lui $v0, 0x1234
li $v1, 0x90ABCDEF
jr $ra
li $v0, 0x12345678
本节围绕下列程序进行演示。
#include <stdint.h>
uint64_t f_add (uint64_t a, uint64_t b)
{
return a+b;
};
void f_add_test ()
{
#ifdef __GNUC__
printf ("%lld\n", f_add(12345678901234, 23456789012345));
#else
printf ("%I64d\n", f_add(12345678901234, 23456789012345));
#endif
};
uint64_t f_sub (uint64_t a, uint64_t b)
{
return a-b;
};
使用MSVC 2012(启用选项/Ox/Ob1)编译上述程序,可得到如下所示的代码。
指令清单24.5 Optimizing MSVC 2012 /Ob1
_a$ = 8 ; size = 8
_b$ = 16 ; size = 8
_f_add PROC
mov eax, DWORD PTR _a$[esp-4]
add eax, DWORD PTR _b$[esp-4]
mov edx, DWORD PTR _a$[esp]
adc edx, DWORD PTR _b$[esp]
ret 0
_f_add ENDP
_f_add_test PROC
push 5461 ; 00001555H
push 1972608889 ; 75939f79H
push 2874 ; 00000b3aH
push 1942892530 ; 73ce2ff_subH
call _f_add
push edx
push eax
push OFFSET $SG1436 ; ’%I64d’, 0aH, 00H
call _printf
add esp, 28
ret 0
_f_add_test ENDP
_f_sub PROC
mov eax, DWORD PTR _a$[esp-4]
sub eax, DWORD PTR _b$[esp-4]
mov edx, DWORD PTR _a$[esp]
sbb edx, DWORD PTR _b$[esp]
ret 0
_f_sub ENDP
在f1_test()函数中,每个64位数据都被拆分为了2个32位数据。在内存中,高32位数据在前(高地址位),低32位在后。
加减法运算的处理方法也完全相同。
在进行加法运算时,先对低32位相加。如果产生了进位,则设置CF标识位。然后ADC指令对高32位进行运算。如果此时CF标识位的值为1,则再把高32位的运算结果加上1。
减法运算也是分步进行的。第一次的减法运算可能影响CF标识位。第二次减法运算会根据CF标识位把借位代入计算结果。
在32位系统中,函数在返回64位数据时都使用EDX:EAX寄存器对。当f_add()函数的返回值传递给printf()函数时,就可以清楚地观测到这一现象。
使用GCC 4.8.1(启用选项–O1–fno–inline)编译上述程序,可得到如下所示的代码。
指令清单24.6 GCC 4.8.1 -O1 -fno-inline
_f_add:
mov eax, DWORD PTR [esp+12]
mov edx, DWORD PTR [esp+16]
add eax, DWORD PTR [esp+4]
adc edx, DWORD PTR [esp+8]
ret
_f_add_test:
sub esp, 28
mov DWORD PTR [esp+8], 1972608889 ; 75939f79H
mov DWORD PTR [esp+12], 5461 ; 00001555H
mov DWORD PTR [esp], 1942892530 ; 73ce2ff_subH
mov DWORD PTR [esp+4], 2874 ; 00000b3aH
call _f_add
mov DWORD PTR [esp+4], eax
mov DWORD PTR [esp+8], edx
mov DWORD PTR [esp], OFFSET FLAT:LC0 ; "%lld\12\0"
call _printf
add esp, 28
ret
_f_sub:
mov eax, DWORD PTR [esp+4]
mov edx, DWORD PTR [esp+8]
sub eax, DWORD PTR [esp+12]
sbb edx, DWORD PTR [esp+16]
ret
GCC的编译结果和MSVC的编译结果相同。
指令清单24.7 Optimizing Keil 6/2013 (ARM mode)
f_add PROC
ADDS r0,r0,r2
ADC r1,r1,r3
BX lr
ENDP
f_sub PROC
SUBS r0,r0,r2
SBC r1,r1,r3
BX lr
ENDP
f_add_test PROC
PUSH {r4,lr}
LDR r2,|L0.68| ; 0x75939f79
LDR r3,|L0.72| ; 0x00001555
LDR r0,|L0.76| ; 0x73ce2ff2
LDR r1,|L0.80| ; 0x00000b3a
BL f_add
POP {r4,lr}
MOV r2,r0
MOV r3,r1
ADR r0,|L0.84| ; "%I64d\n"
B __2printf
ENDP
|L0.68|
DCD 0x75939f79
|L0.72|
DCD 0x00001555
|L0.76|
DCD 0x73ce2ff2
|L0.80|
DCD 0x00000b3a
|L0.84|
DCB "%I64d\n",0
首个64位值被拆分存储到R0和R1寄存器里,第二个64位值则存储于R2和R3寄存器对。ARM平台的指令集里有可进行进位加法运算的ADC指令和借位减法运算的SBC指令。
需要注意的是:在对64位数据的低32位数据进行加减运算时,需要使用ADDS和SUBS指令。指令名词中的-S后缀代表该指令会设置进(借)位标识(Carry Flag)。它们设置过的进借位标识将被高32位运算的ADC/SBC指令读取并纳入运算结果之中。
没有-S后缀的ADD和SUB指令则不会设置借/进位标识位。
指令清单24.8 Optimizing GCC 4.4.5 (IDA)
f_add:
; $a0 - high part of a
; $a1 - low part of a
; $a2 - high part of b
; $a3 - low part of b
addu $v1, $a3, $a1 ; sum up low parts
addu $a0, $a2, $a0 ; sum up high parts
; will carry generated while summing up low parts?
; if yes, set $v0 to 1
sltu $v0, $v1, $a3
jr $ra
; add 1 to high part of result if carry should be generated:
addu $v0, $a0 ; branch delay slot
; $v0 - high part of result
; $v1 - low part of result
f_sub:
; $a0 - high part of a
; $a1 - low part of a
; $a2 - high part of b
; $a3 - low part of b
subu $v1, $a1, $a3 ; subtract low parts
subu $v0, $a0, $a2 ; subtract high parts
; will carry generated while subtracting low parts?
; if yes, set $a0 to 1
sltu $a1, $v1
jr $ra
; subtract 1 from high part of result if carry should be generated:
subu $v0, $a1 ; branch delay slot
; $v0 - high part of result
; $v1 - low part of result
f_add_test:
var_10 = -0x10
var_4 = -4
lui $gp, (__gnu_local_gp >> 16)
addiu $sp, -0x20
la $gp, (__gnu_local_gp & 0xFFFF)
sw $ra, 0x20+var_4($sp)
sw $gp, 0x20+var_10($sp)
lui $a1, 0x73CE
lui $a3, 0x7593
li $a0, 0xB3A
li $a3, 0x75939F79
li $a2, 0x1555
jal f_add
li $a1, 0x73CE2FF2
lw $gp, 0x20+var_10($sp)
lui $a0, ($LC0 >> 16) # "%lld\n"
lw $t9, (printf & 0xFFFF)($gp)
lw $ra, 0x20+var_4($sp)
la $a0, ($LC0 & 0xFFFF) # "%lld\n"
move $a3, $v1
move $a2, $v0
jr $t9
addiu $sp, 0x20
$LC0: .ascii "%lld\n"<0>
MIPS 处理器没有标识位寄存器。这种平台的加减运算完全不会存储借/进位信息。它的指令集里也没有x86指令集里的那种ADC或SBB指令。当需要存储借/进位信息时,编译器通常使用SLTU指令把有关信息(也就是0或1)存储在既定寄存器里,继而在下一步的高数权位运算中纳入借进位信息。
#include <stdint.h>
uint64_t f_mul (uint64_t a, uint64_t b)
{
return a*b;
};
uint64_t f_div (uint64_t a, uint64_t b)
{
return a/b;
};
uint64_t f_rem (uint64_t a, uint64_t b)
{
return a % b;
};
使用MSVC 2012(启用选项/Ox/Ob1)编译上述程序,可得到如下所示的代码。
指令清单24.9 Optimizing MSVC 2012 /Ob1
_a$ = 8 ; size = 8
_b$ = 16 ; size = 8
_f_mul PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _b$[ebp+4]
push eax
mov ecx, DWORD PTR _b$[ebp]
push ecx
mov edx, DWORD PTR _a$[ebp+4]
push edx
mov eax, DWORD PTR _a$[ebp]
push eax
call __allmul ; long long multiplication
pop ebp
ret 0
_f_mul ENDP
_a$ = 8 ; size = 8
_b$ = 16 ; size = 8
_f_div PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _b$[ebp+4]
push eax
mov ecx, DWORD PTR _b$[ebp]
push ecx
mov edx, DWORD PTR _a$[ebp+4]
push edx
mov eax, DWORD PTR _a$[ebp]
push eax
call __aulldiv ; unsigned long long division
pop ebp
ret 0
_f_div ENDP
_a$ = 8 ; size = 8
_b$ = 16 ; size = 8
_f_rem PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _b$[ebp+4]
push eax
mov ecx, DWORD PTR _b$[ebp]
push ecx
mov edx, DWORD PTR _a$[ebp+4]
push edx
mov eax, DWORD PTR _a$[ebp]
push eax
call __aullrem ; unsigned long long remainder
pop ebp
ret 0
_f_rem ENDP
乘除运算复杂很多。所以编译器通常借助标准库函数来处理乘除运算。
如需了解库函数的各种详细信息,请参见附录E。
使用GCC 4.8.1(启用选项-O3-fno-inline)编译上述程序,可得到如下所示的代码。
指令清单24.10 Optimizing GCC 4.8.1 -fno-inline
_f_mul:
push ebx
mov edx, DWORD PTR [esp+8]
mov eax, DWORD PTR [esp+16]
mov ebx, DWORD PTR [esp+12]
mov ecx, DWORD PTR [esp+20]
imul ebx, eax
imul ecx, edx
mul edx
add ecx, ebx
add edx, ecx
pop ebx
ret
_f_div:
sub esp, 28
mov eax, DWORD PTR [esp+40]
mov edx, DWORD PTR [esp+44]
mov DWORD PTR [esp+8], eax
mov eax, DWORD PTR [esp+32]
mov DWORD PTR [esp+12], edx
mov edx, DWORD PTR [esp+36]
mov DWORD PTR [esp], eax
mov DWORD PTR [esp+4], edx
call ___udivdi3 ; unsigned division
add esp, 28
ret
_f_rem:
sub esp, 28
mov eax, DWORD PTR [esp+40]
mov edx, DWORD PTR [esp+44]
mov DWORD PTR [esp+8], eax
mov eax, DWORD PTR [esp+32]
mov DWORD PTR [esp+12], edx
mov edx, DWORD PTR [esp+36]
mov DWORD PTR [esp], eax
mov DWORD PTR [esp+4], edx
call ___umoddi3 ; unsigned modulo
add esp, 28
ret
GCC把乘法运算进行内部展开处理,大概是它认为这样的效率更高一些。另外它所调用的库函数的名称也和MSVC不同(参见附录D)。除此之外,这段汇编指令和MSVC的编译结果之间几乎没有区别。
在编译Thumb模式的程序时,Keil调用库函数进行仿真运算。
指令清单24.11 Optimizing Keil 6/2013 (Thumb mode)
||f_mul|| PROC
PUSH {r4,lr}
BL __aeabi_lmul
POP {r4,pc}
ENDP
||f_div|| PROC
PUSH {r4,lr}
BL __aeabi_uldivmod
POP {r4,pc}
ENDP
||f_rem|| PROC
PUSH {r4,lr}
BL __aeabi_uldivmod
MOVS r0,r2
MOVS r1,r3
POP {r4,pc}
ENDP
相比之下,在编译ARM模式的程序时,Keil能够直接进行64位乘法运算。
指令清单24.12 Optimizing Keil 6/2013 (ARM mode)
||f_mul|| PROC
PUSH {r4,lr}
UMULL r12,r4,r0,r2
MLA r1,r2,r1,r4
MLA r1,r0,r3,r1
MOV r0,r12
POP {r4,pc}
ENDP
||f_div|| PROC
PUSH {r4,lr}
BL __aeabi_uldivmod
POP {r4,pc}
ENDP
||f_rem|| PROC
PUSH {r4,lr}
BL __aeabi_uldivmod
MOV r0,r2
MOV r1,r3
POP {r4,pc}
ENDP
在启用优化选项的情况下编译MIPS程序时,GCC能够直接使用汇编指令进行64位乘法运算。但是在进行64位除法运算时,编译器还是会用库函数进行处理。
指令清单24.13 Optimizing GCC 4.4.5 (IDA)
f_mul:
mult $a2, $a1
mflo $v0
or $at, $zero ; NOP
or $at, $zero ; NOP
mult $a0, $a3
mflo $a0
addu $v0, $a0
or $at, $zero ; NOP
multu $a3, $a1
mfhi $a2
mflo $v1
jr $ra
addu $v0, $a2
f_div:
var_10 = -0x10
var_4 =-4
lui $gp, (__gnu_local_gp >> 16)
addiu $sp, -0x20
la $gp, (__gnu_local_gp & 0xFFFF)
sw $ra, 0x20+var_4($sp)
sw $gp, 0x20+var_10($sp)
lw $t9, (__udivdi3 & 0xFFFF)($gp)
or $at, $zero
jalr $t9
or $at, $zero
lw $ra, 0x20+var_4($sp)
or $at, $zero
jr $ra
addiu $sp, 0x20
f_rem:
var_10 = -0x10
var_4 =-4
lui $gp, (__gnu_local_gp >> 16)
addiu $sp, -0x20
la $gp, (__gnu_local_gp & 0xFFFF)
sw $ra, 0x20+var_4($sp)
sw $gp, 0x20+var_10($sp)
lw $t9, (__umoddi3 & 0xFFFF)($gp)
or $at, $zero
jalr $t9
or $at, $zero
lw $ra, 0x20+var_4($sp)
or $at, $zero
jr $ra
addiu $sp, 0x20
上述程序中夹杂着大量的NOP指令。或许这是因为乘法运算的时间较长,且运算之后需要较长等待时间的缘故;不过这一观点尚无法证实。
#include <stdint.h>
uint64_t f (uint64_t a)
{
return a>>7;
};
指令清单24.14 Optimizing MSVC 2012 /Ob1
_a$ = 8 ; size = 8
_f PROC
mov eax, DWORD PTR _a$[esp-4]
mov edx, DWORD PTR _a$[esp]
shrd eax, edx, 7
shr edx, 7
ret 0
_f ENDP
指令清单24.15 Optimizing GCC 4.8.1 -fno-inline
_f:
mov edx, DWORD PTR [esp+8]
mov eax, DWORD PTR [esp+4]
shrd eax, edx, 7
shr edx, 7
ret
位移运算仍然分为2步:第一步处理低32位数据,第二步处理高32位数据。需要注意的是处理低32位数据的指令——SHRD指令。这个指令不仅可以把EAX里的低32位数据右移7位运算,而且还能从EDX寄存器里读取高32位中的低7位、用它填补到低32位数据的高位。如此一来,就可直接使用最普通的SHR指令对高32位进行位移操作,并用零补充位移产生的空位了。
ARM的指令集比x86的小,没有SHRD之类的指令。因此,Keil把它拆分为位移和或运算,以进行等效处理。
指令清单24.16 Optimizing Keil 6/2013 (ARM mode)
||f|| PROC
LSR r0,r0,#7
ORR r0,r0,r1,LSL #25
LSR r1,r1,#7
BX lr
ENDP
指令清单24.17 Optimizing Keil 6/2013 (Thumb mode)
||f|| PROC
LSLS r2,r1,#25
LSRS r0,r0,#7
ORRS r0,r0,r2
LSRS r1,r1,#7
BX lr
ENDP
GCC for MIPS采用了Keil for Thumb的编译手段。
指令清单24.18 Optimizing GCC 4.4.5 (IDA)
f:
sll $v0, $a0, 25
srl $v1, $a1, 7
or $v1, $v0, $v1
jr $ra
srl $v0, $a0, 7
#include <stdint.h>
int64_t f (int32_t a)
{
return a;
};
指令清单24.19 Optimizing MSVC 2012
_a$ = 8
_f PROC
mov eax, DWORD PTR _a$[esp-4]
cdq
ret 0
_f ENDP
在这种情况下,我们需要把32位有符号数参量扩展为64位有符号数。无符号数的转换过程较为直接:高位设置为0就万事大吉。但是对于有符号数来说,最高的一位是符号位,不能一概而论地把高位都填零。这里使用了CDQ指令进行有符号数的格式转换。它把EAX的值扩展为64位,将结果存储到EDX:EAX寄存器对中。换句话说,CDQ指令获取EAX寄存器中的符号位(最高位),并按照符号位的不同把高32位都设为0或1。CDQ指令的作用和MOVSX指令相似。
GCC生成的汇编指令使用了inlines乘法运算。其他部分和MSVC的编译结果大体相同。
指令清单24.20 Optimizing Keil 6/2013 (ARM mode)
||f|| PROC
ASR r1,r0,#31
BX lr
ENDP
Keil for ARM的编译方法与MSVC不同:它把参数向右位移31位,只用了算术右移指令。MSB是符号位,而算术位移指令会用符号位填补位移产生的空缺位。所以在执行“ASRr1,r0,#31”指令的时候,如果输入值是负数,那么R1就是0xFFFFFFFF;否则R1的值会是零。在生成64位值的时候,高32位应当存储在R1寄存器里。
换句话说,这个指令用最高数权位填充R1寄存器以形成64位值的高32位。
GCC for MIPS的编译方法和Keil编译ARM程序的方法相同。
指令清单24.21 Optimizing GCC 4.4.5 (IDA)
f:
sra $v0, $a0, 31
jr $ra
move $v1, $a0
[1] 16位系统处理32位数据时同样采取了这种拆分数据的处理方法,详情请参见本书53.4节。