如何通过32位机器处理大于2 ^ 32的数字?

时间:2010-10-09 21:17:53

标签: c gcc x86 32-bit

我试图了解如何在32位机器上进行涉及大于2 32 的数字的计算。

C代码

$ cat size.c
#include<stdio.h>
#include<math.h>

int main() {

    printf ("max unsigned long long = %llu\n",
    (unsigned long long)(pow(2, 64) - 1));
}
$

gcc输出

$ gcc size.c -o size
$ ./size
max unsigned long long = 18446744073709551615
$

对应的汇编代码

$ gcc -S size.c -O3
$ cat size.s
    .file   "size.c"
    .section    .rodata.str1.4,"aMS",@progbits,1
    .align 4
.LC0:
    .string "max unsigned long long = %llu\n"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $16, %esp
    movl    $-1, 8(%esp)   #1
    movl    $-1, 12(%esp)  #2
    movl    $.LC0, 4(%esp) #3
    movl    $1, (%esp)     #4
    call    __printf_chk
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits
$

1 - 4行确实发生了什么?

这是程序集级别的某种字符串连接吗?

7 个答案:

答案 0 :(得分:19)

__printf_chkprintf的包装器,它检查堆栈溢出,并采用额外的第一个参数,一个标志(例如,见here。)

pow(2, 64) - 1已经优化为0xffffffffffffffff,因为参数是常量。

根据通常的调用约定,__printf_chk()int flag)的第一个参数是堆栈上的32位值(%espcall指令)。下一个参数const char * format是一个32位指针(堆栈上的下一个32位字,即%esp+4)。正在打印的64位数量占用接下来的两个32位字(%esp+8%esp+12):

pushl   %ebp                 ; prologue
movl    %esp, %ebp           ; prologue
andl    $-16, %esp           ; align stack pointer
subl    $16, %esp            ; reserve bytes for stack frame
movl    $-1, 8(%esp)   #1    ; store low half of 64-bit argument (a constant) to stack
movl    $-1, 12(%esp)  #2    ; store high half of 64-bit argument (a constant) to stack
movl    $.LC0, 4(%esp) #3    ; store address of format string to stack
movl    $1, (%esp)     #4    ; store "flag" argument to __printf_chk to stack
call    __printf_chk         ; call routine
leave                        ; epilogue
ret                          ; epilogue

编译器已经有效地重写了这个:

printf("max unsigned long long = %llu\n", (unsigned long long)(pow(2, 64) - 1));

......进入这个:

__printf_chk(1, "max unsigned long long = %llu\n", 0xffffffffffffffffULL);

...并且,在运行时,调用的堆栈布局如下所示(将堆栈显示为32位字,地址从图的底部向上增加):

        :                 :
        :     Stack       :
        :                 :
        +-----------------+
%esp+12 |      0xffffffff | \ 
        +-----------------+  } <-------------------------------------.
%esp+8  |      0xffffffff | /                                        |
        +-----------------+                                          |
%esp+4  |address of string| <---------------.                        |
        +-----------------+                 |                        |
%esp    |               1 | <--.            |                        |
        +-----------------+    |            |                        |
                  __printf_chk(1, "max unsigned long long = %llu\n", |
                                                    0xffffffffffffffffULL);

答案 1 :(得分:6)

类似于我们处理大于9的数字的方式,只有0到9的数字。 (使用位置数字)。假设这个问题是一个概念问题。

答案 2 :(得分:3)

在你的情况下,编译器知道2 ^ 64-1只是0xffffffffffffffff,因此它将-1(低dword)和-1(高dword)作为printf的参数推入堆栈。这只是一个优化。

通常,64位数字(甚至更大的值)可以与多个单词一起存储,例如unsigned long long使用两个dword。要添加两个64位数字,需要执行两次加法 - 一次在低32位,一次在高32位,加上进位:

; Add 64-bit number from esi onto edi:
mov     eax, [esi] ; get low 32 bits of source
add     [edi], eax ; add to low 32 bits of destination
; That add may have overflowed, and if it did, carry flag = 1.
mov     eax, [esi+4] ; get high 32 bits of source
adc     [edi+4], eax ; add to high 32 bits of destination, then add carry.

您可以根据需要重复此addadc s序列,以添加任意大数字。减法可以做同样的事情 - 只需使用subsbb(借用减法)。

乘法和除法要复杂得多,编译器通常会产生一些小辅助函数,以便在将64位数相乘时处理这些函数。支持非常大的整数的GMP这样的包使用SSE / SSE2来加快速度。有关乘法算法的更多信息,请查看this Wikipedia article

答案 3 :(得分:2)

正如其他人所指出的那样,你的例子中的所有64位aritmetic已被优化掉了。这个答案集中在标题中的问题。

基本上我们将每个32位数字视为一个数字并在基数4294967296中工作。通过这种方式,我们可以处理大数字。

加法和减法是最简单的。我们一次完成一个数字,从最不重要的数字开始到最重要的数字。通常,第一个数字是使用正常的加/减指令完成的,后来的数字是使用特定的&#34;使用随机数添加&#34;或&#34;减去借款&#34;指令。状态寄存器中的进位标志用于将进位/借位从一位数转移到下一位。由于二进制补码有符号和无符号加法和减法是相同的。

乘法有点棘手,乘以两个32位数字可以产生64位结果。大多数32位处理器的指令会将两个32位数相乘,并在两个寄存器中产生64位结果。然后需要增加将结果组合成最终答案。由于二进制补码有符号和无符号乘法是相同的,只要期望的结果大小与参数大小相同。如果结果大于参数,则需要特别小心。

对于比较,我们从最重要的数字开始。如果它相等,我们向下移动到下一个数字,直到结果相等。

分区太复杂了我无法在这篇文章中描述,但是有很多算法的例子。例如http://www.hackersdelight.org/hdcodetxt/divDouble.c.txt

来自gcc https://godbolt.org/g/NclqXC的一些实际例子,汇编程序是英特尔语法。

首先添加。添加两个64位数字并生成64位结果。对于有符号和无符号版本,asm都是相同的。

int64_t add64(int64_t a, int64_t b) { return a +  b; }
add64:
    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

这很简单,将一个参数加载到eax和edx中,然后使用add添加另一个参数,然后使用carry添加。结果留在eax和edx中以返回给调用者。

现在将两个64位数相乘以产生64位结果。代码也没有从有符号变为无符号。我已添加了一些评论,以便更容易理解。

在我们查看代码之前,请考虑数学。 a和b是64位数字我将使用lo()表示64位数字的低32位,hi()表示64位数字的高32位。

(a * b)=(lo(a)* lo(b))+(hi(a)* lo(b)* 2 ^ 32)+(hi(b)* lo(a)* 2 ^ 32)+(hi(b)* hi(a)* 2 ^ 64)

(a * b)mod 2 ^ 64 =(lo(a)* lo(b))+(lo(hi(a)* lo(b))* 2 ^ 32)+(lo(hi(b) )* lo(a))* 2 ^ 32)

lo((a * b)mod 2 ^ 64)= lo(lo(a)* lo(b))

hi((a * b)mod 2 ^ 64)= hi(lo(a)* lo(b))+ lo(hi(a)* lo(b))+ lo(hi(b)* lo (a))的

uint64_t mul64(uint64_t a, uint64_t b) { return a*b; }
mul64:
    push    ebx                      ;save ebx
    mov     eax, DWORD PTR [esp+8]   ;load lo(a) into eax
    mov     ebx, DWORD PTR [esp+16]  ;load lo(b) into ebx
    mov     ecx, DWORD PTR [esp+12]  ;load hi(a) into ecx  
    mov     edx, DWORD PTR [esp+20]  ;load hi(b) into edx
    imul    ecx, ebx                 ;ecx = lo(hi(a) * lo(b))
    imul    edx, eax                 ;edx = lo(hi(b) * lo(a))
    add     ecx, edx                 ;ecx = lo(hi(a) * lo(b)) + lo(hi(b) * lo(a))
    mul     ebx                      ;eax = lo(low(a) * lo(b))
                                     ;edx = hi(low(a) * lo(b))
    pop     ebx                      ;restore ebx.
    add     edx, ecx                 ;edx = hi(low(a) * lo(b)) + lo(hi(a) * lo(b)) + lo(hi(b) * lo(a))
    ret

最后,当我们尝试分裂时,我们会看到。

int64_t div64(int64_t a, int64_t b) { return a/b; }
div64:
    sub     esp, 12
    push    DWORD PTR [esp+28]
    push    DWORD PTR [esp+28]
    push    DWORD PTR [esp+28]
    push    DWORD PTR [esp+28]
    call    __divdi3
    add     esp, 28
    ret

编译器已经确定除法太复杂而无法内联实现,而是调用库例程。

答案 4 :(得分:1)

编译器实际上对代码进行了静态优化。 第1#2#3行是printf()

的参数

答案 5 :(得分:1)

正如@Pafy所提到的,编译器已将其评估为常量。

2到64th减1是0xffffffffffffffff

作为2个32位整数,它是:0xffffffff0xffffffff,如果你将它作为一对32位有符号类型,则最终为:{{1 }和-1

因此,对于您的编译器,生成的代码恰好等同于:

-1

在集会中它的写法如下:

printf("max unsigned long long = %llu\n", -1, -1);

顺便说一下,计算2的幂的更好方法是向左移movl $-1, 8(%esp) #Second -1 parameter movl $-1, 12(%esp) #First -1 parameter movl $.LC0, 4(%esp) #Format string movl $1, (%esp) #A one. Kind of odd, perhaps __printf_chk #in your C library expects this. call __printf_chk 。例如。 1

答案 6 :(得分:0)

此线程中没有人注意到OP要求解释前4行,而不是第11-14行。

前4行是:

    .file   "size.c"
    .section    .rodata.str1.4,"aMS",@progbits,1
    .align 4
.LC0:

这是前4行中发生的事情:

.file   "size.c"

这是一个汇编程序指令,表示我们即将启动一个名为&#34; size.c&#34;的新逻辑文件。

.section    .rodata.str1.4,"aMS",@progbits,1

这也是程序中只读字符串的指令。

.align 4

此指令将位置计数器设置为始终为4的倍数。

.LC0:

这是一个标签LC0,可以跳转到,例如。

我希望我能正确回答这个问题,因为我回答了OP的确切要求。