【问题标题】:How are numbers greater than 2^32 handled by a 32 bit machine?32 位机器如何处理大于 2^32 的数字?
【发布时间】:2011-04-23 07:28:48
【问题描述】:

我试图了解涉及大于 232 的数字的计算如何在 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 行究竟发生了什么?

这是汇编级别的某种字符串连接吗?

【问题讨论】:

  • 使用pow 是计算2 的整数幂的一种非常讨厌且容易出错的方法。不要使用它。请注意,如果您的浮点减法没有作为 80 位 long double 完成,pow(2,64)-1 将等于 pow(2,64),然后将其转换为 unsigned long long 将无法正常工作。

标签: c gcc x86 32-bit


【解决方案1】:

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

【讨论】:

  • 是的,我明白这一点。我想知道printf 在第 1 - 4 行的准备情况。
  • subl $16, %esp 为 4 个 32 位参数腾出一些空间。第 1 行和第 2 行移动了 64 位参数(所以 2 个 32 位)。第 3 行设置格式字符串,然后将“1”作为参数推送(必须来自内部 printf() 调用)。
【解决方案2】:

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

2 到 64 次减 1 是 0xffffffffffffffff

作为 2 个 32 位整数,这是:0xffffffff0xffffffff
如果您将其视为一对 32 位有符号类型,则最终为:-1-1 .

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

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

在程序集中是这样写的:

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

顺便说一句,计算 2 的幂的更好方法是将1 左移。例如。 (1ULL &lt;&lt; 64) - 1.

【讨论】:

  • 转移是没用的。 -1ULL 将给出相同的结果。
【解决方案3】:

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

一般来说,64 位数字(甚至更大的值)可以与多个字一起存储,例如一个unsigned long long 使用两个dwords。要添加两个 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.

您可以随意重复addadcs 的序列,添加任意大的数字。减法也可以做同样的事情 - 只需使用 subsbb(借位减法)。

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

【讨论】:

    【解决方案4】:

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

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

    根据通常的调用约定,__printf_chk() (int flag) 的第一个参数是堆栈上的 32 位值(在 %esp 处,在 call 指令时)。下一个参数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);
    

    【讨论】:

      【解决方案5】:

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

      【讨论】:

      • 伟大的洞察力,从来没有这样想过!
      【解决方案6】:

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

      前4行是:

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

      以下是前 4 行发生的情况:

      .file   "size.c"
      

      这是一个汇编指令,表示我们将要启动一个名为“size.c”的新逻辑文件。

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

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

      .align 4
      

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

      .LC0:
      

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

      我希望我提供了正确的答案,因为我回答了 OP 的问题。

      【讨论】:

      • 回顾OPs的问题,即使是第一个版本的问题How are numbers greater than 2^32 handled by a 32 bit machine?title中也有这个问题,整个问题确实有2个问题,但我有感觉OP提出的真正问题是标题中的问题。可能 OP 认为第 1-4 行做了一些巫术魔法来处理大于 2^32 的数字。我同意这个问题可能需要一些工作。
      【解决方案7】:

      正如其他人指出的那样,您示例中的所有 64 位算法都已被优化掉。此答案侧重于标题中的问题。

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

      加法和减法是最简单的。我们一次一个地处理数字,从最不重要的数字开始,一直到最重要的数字。通常,第一个数字使用普通的加/减指令完成,后面的数字使用特定的“带进位加”或“带借位减”指令完成。状态寄存器中的进位标志用于将进位/借位位从一位数转移到下一位数。由于二进制补码有符号和无符号加减法是一样的。

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

      为了比较,我们从最重要的数字开始。如果相等,我们向下移动到下一位,直到结果相等。

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


      来自 gcc https://godbolt.org/g/NclqXC 的一些实际示例,汇编器采用 intel 语法。

      首先添加。将两个 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 和带有进位的 add 来添加另一个。结果留在 eax 和 edx 中以返回给调用者。

      现在将两个 64 位数字相乘以产生 64 位结果。同样,代码不会从有符号变为无符号。我添加了一些 cmets 以便更容易理解。

      在查看代码之前,让我们考虑一下数学。 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
      

      编译器认为除法太复杂而无法实现内联,而是调用库例程。

      【讨论】:

      • 一个真实的例子可能会有所帮助:add and mul of int64_t compiled with -m32, on the Godbolt compiler explorer。随意将该代码、asm 输出和 godbolt 链接合并到您的答案中。
      • 另外,“数字”可能是一个令人困惑的术语,因为大多数人会一直认为“十进制数字”。 GMP calls them "limbs".
      • 在有空间时总是更喜欢非缩短链接,因为它们不会腐烂。 meta.stackoverflow.com/a/319594/224132。很好的补充描述了扩展精度 asm 的工作原理。我通常写this compiles ([on the Godbolt compiler explorer][1])...之类的东西。使用 ctrl-L 在 SO 的 markdown 编辑器中创建超链接。您不需要将 URL 作为答案文本的一部分显示(通常不应该这样,以减少混乱)。
      • 不幸的是,godbolt 生成的非缩短链接似乎被破坏了。当我点击您的链接时,代码中缺少一个加号。添加加号并重新生成长链接会导致同样的问题。
      • 这不会发生在我身上。我的链接工作正常。你使用的是什么浏览器?您或许应该在 the issue tracker 上报告问题。
      猜你喜欢
      • 2014-12-14
      • 2013-07-25
      • 2013-04-17
      • 2012-01-07
      • 1970-01-01
      • 2019-04-13
      • 2014-05-27
      • 2012-05-27
      • 1970-01-01
      相关资源
      最近更新 更多