我试图了解如何在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行确实发生了什么?
这是程序集级别的某种字符串连接吗?
答案 0 :(得分:19)
__printf_chk
是printf
的包装器,它检查堆栈溢出,并采用额外的第一个参数,一个标志(例如,见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);
答案 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.
您可以根据需要重复此add
和adc
s序列,以添加任意大数字。减法可以做同样的事情 - 只需使用sub
和sbb
(借用减法)。
乘法和除法要复杂得多,编译器通常会产生一些小辅助函数,以便在将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位整数,它是:0xffffffff
和0xffffffff
,如果你将它作为一对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的确切要求。