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

6 篇文章 0 订阅

先来看一段小程序

#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


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值