第24章 32位系统处理64位数据

32位系统的通用寄存器GPR都只能容纳32位数据,所以这种平台必须把64位数据转换为一对32位数据才能进行运算。[1]

24.1 64位返回值

本节围绕下述程序进行演示。

#include <stdint.h>

uint64_t f ()
{
       return 0x1234567890ABCDEF;
};
24.1.1 x86

32位系统用寄存器组合EDX:EAX来回传64位值。

指令清单24.1 Optimizing MSVC 2010

_f         PROC
           mov      eax, -1867788817       ; 90abcdefH
           mov      edx, 305419896         ; 12345678H
           ret      0
_f         ENDP
24.1.2 ARM

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
24.1.3 MIPS

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

24.2 参数传递及加减运算

本节围绕下列程序进行演示。

#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;
};
24.2.1 x86

使用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.2.2 ARM

指令清单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.2.3 MIPS

指令清单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)存储在既定寄存器里,继而在下一步的高数权位运算中纳入借进位信息。

24.3 乘法和除法运算

#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;
};
24.3.1 x86

使用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的编译结果之间几乎没有区别。

24.3.2 ARM

在编译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
24.3.3 MIPS

在启用优化选项的情况下编译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指令。或许这是因为乘法运算的时间较长,且运算之后需要较长等待时间的缘故;不过这一观点尚无法证实。

24.4 右移

#include <stdint.h>

uint64_t f (uint64_t a)
{
         return a>>7;
};
24.4.1 x86

指令清单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位进行位移操作,并用零补充位移产生的空位了。

24.4.2 ARM

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
24.4.3 MIPS

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

24.5 32位数据转换为64位数据

#include <stdint.h>

int64_t f (int32_t a)
{
         return a; 
};
24.5.1 x86

指令清单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.5.2 ARM

指令清单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位。

24.5.3 MIPS

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节。