您的位置:首页 > 其它

从IA32到X86-64的扩展所导致的函数传参栈模型的变化

2016-01-29 14:36 323 查看
先来看一段小程序

#include <stdio.h>
int main(){
float f = 2.5;
int i = 2;
printf("%d\n%f\n%d\n%f\n\n", f, f, i, i);
//printf("%d\n%f\n%f\n%d\n\n", f, f, i, i);
//printf("%d\n%d\n%f\n%f\n\n", f, f, i, i);
//printf("%f\n%f\n%d\n%d\n\n", f, f, i, i);
//printf("%f\n%d\n%f\n%d\n\n", f, f, i, i);
//printf("%f\n%d\n%d\n%f\n\n", f, f, i, i);
return 0;
}


这段程序的输出是什么呢?如果我们使用IA32的栈模型分析,就会是如下图的样子



f=2.5本来应该是0X40200000,但是传参数的时候浮点类型默认转换为double变为0X4002000000000000(double和float的格式参考IEEE754),下面是在32-bit win7下面的汇编代码(intel格式):

00410970   push        ebp
00410971   mov         ebp,esp
00410973   sub         esp,48h
00410976   push        ebx
00410977   push        esi
00410978   push        edi
00410979   lea         edi,[ebp-48h]
0041097C   mov         ecx,12h
00410981   mov         eax,0CCCCCCCCh
00410986   rep stos    dword ptr [edi]
4:
5:        float f = 2.5;
00410988   mov         dword ptr [ebp-4],40200000h
6:        int i = 2;
0041098F   mov         dword ptr [ebp-8],2
7:        printf("%d\n%f\n%d\n%f\n",f,f,i,i);
00410996   mov         eax,dword ptr [ebp-8]
00410999   push        eax
0041099A   mov         ecx,dword ptr [ebp-8]
0041099D   push        ecx
0041099E   fld         dword ptr [ebp-4]
004109A1   sub         esp,8
004109A4   fstp        qword ptr [esp]
004109A7   fld         dword ptr [ebp-4]
004109AA   sub         esp,8
004109AD   fstp        qword ptr [esp]
004109B0   push        offset string "a=%f,b=%d\n" (00427010)
004109B5   call        printf (004010a0)
004109BA   add         esp,1Ch


如果是32-bit的平台,那么输出就是按照上图栈模型读取参数,结果就是输出0 0.000000 1074003968(0x40020000) 0.000000,浮点数之所以是0.000000,因为此时读出来的都不是规格化的浮点数(IEEE规定浮点数阶码不为全0或全1时为规格化,全0时表示很接近0的非规格化数,全1表示其他的),这里的两个浮点数都是接近0的很小的数,精度有限,直接输出0.000000

在X86-64平台下是不是这样的呢?结果出乎意料,能输出正确的结果:


从上面看出,不管怎么输出,都能找到2.5和2,就好象不是从栈中取出来一样。说明X86-64并不和IA32一样,看一下汇编代码(ubuntu14.04 gcc)(AT&T格式):

.file	"test_formatp.c"
.section	.rodata
.LC1:
.string	"%d\n%f\n%d\n%f\n\n"
.text
.globl	main
.type	main, @function
main:
.LFB0:
.cfi_startproc
pushq	%rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq	%rsp, %rbp
.cfi_def_cfa_register 6
subq	$16, %rsp
movl	.LC0(%rip), %eax
movl	%eax, -8(%rbp)
movl	$2, -4(%rbp)
movss	-8(%rbp), %xmm1
cvtps2pd	%xmm1, %xmm1
movss	-8(%rbp), %xmm0
cvtps2pd	%xmm0, %xmm0
movl	-4(%rbp), %edx
movl	-4(%rbp), %eax
movl	%eax, %esi
movl	$.LC1, %edi
movl	$2, %eax
call	printf
movl	$0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size	main, .-main
.section	.rodata
.align 4
.LC0:
.long	1075838976
.ident	"GCC: (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4"
.section	.note.GNU-stack,"",@progbits
基本上完全不一样,没有严格按照顺序将参数压栈,经查证,X86-64扩展了IA32的寄存器数目,并且带浮点运算的程序会用到专用的浮点运算协处理器(包括SSE,%xmm寄存器等),这个可以查阅amd64 ABI文档。规定整数类型的参数通过寄存器%rdi %rsi %rdx %rcx %r8 %r9来传递,多余的参数通过栈来传递。浮点类型的参数通过%xmm0~%xmm7来传递。因此这里的两个i分别传递给%esi %edx (第一个字符串地址传递给%edi),两个f分别传递给%xmm0和xmm1,%eax表示使用的%xmm寄存器的个数。所以,printf并不从栈中取参数,而是直接从指定寄存器中取,因此,这里不管前面的格式化串如何,只用是按两个d和两个f来,他都会通过找%esi和%edx以及%xmm0和%xmm1来读取参数,编译器会对printf的参数进行优化和重新排列,printf对格式化串儿的解析过程只会按顺序看有哪几个整数哪几个浮点数,默认程序员给出前后匹配的参数。并且一般不匹配的时候,编译器会给出警告,但是不负责当错误来处理。



有了以上分析,我们再看看下面这个例子:

#include <stdio.h>
int main(){
int a = 10, d = 100;
float f = 2.5;
printf("f=%f,d=%d\n", f, d);
printf("f=%f,d=%f\n", f, d);
printf("f=%d,d=%d\n", f, d);
printf("f=%d,d=%f\n", f, d);
return 0;
}
在我的win7 32bit(传统栈模型)下面可以断定输出(读者可以自行分析)

f=2.500000,d=100

f=2.500000,d=0.000000

f=0,d=1074003968

f=0,d=0.000000

在ubuntu14.04 X86-64下面输出第一个打印和第四个打印肯定是f=2.500000(%xmm0),d=100(%esi)和f=100(%esi),d=2.500000(%xmm0);第二个打印f=2.500000(%xmm0),d=?(%xmm1);第三个打印f=100(%esi),d=?(%edx);



第三个打印的d的确是从%edx取出的,而%edx并没有用于传递真实的d,因此打印出随机的结果。以下是调试时候的验证结果:



综上所述,对于不同的平台(32-bit和64-bit cpu OS以及不同的编译环境),函数的传参模型并不像书本上讲的那样死板,特别是64-bit处理器的使用,寄存器的扩展,编译器已经充分对代码做了底层优化来使用扩展的计算能力(包括SSE)。

1.对于32-bit平台,可以使用传统的栈模型来分析参数传递过程。

2.对于64-bit平台,需要了解ABI以及相关文档,查看传参模型。如本例中的AMD64 ABI 就规定整数类型的参数通过寄存器%rdi %rsi %rdx %rcx %r8 %r9来传递,多余的参数通过栈来传递。浮点类型的参数通过%xmm0~%xmm7来传递。

参考:

《深入理解计算机系统》
http://blog.codinglabs.org/articles/trouble-of-x86-64-platform.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: