从IA32到X86-64的扩展所导致的函数传参栈模型的变化
2016-01-29 14:36
323 查看
先来看一段小程序
这段程序的输出是什么呢?如果我们使用IA32的栈模型分析,就会是如下图的样子
f=2.5本来应该是0X40200000,但是传参数的时候浮点类型默认转换为double变为0X4002000000000000(double和float的格式参考IEEE754),下面是在32-bit win7下面的汇编代码(intel格式):
如果是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格式):
有了以上分析,我们再看看下面这个例子:
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
#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
相关文章推荐
- Lind.DDD.Events领域事件介绍
- JS禁止用F5键
- SIMCOM WCDMA 添加,不能用操作步骤
- video_register_device 创建节点的顺序/dev/video11,2,3
- 微信之父张小龙的2359篇日记
- 静态方法内无法访问非静态字段
- String字符串补0操作常见方法
- Python学习之基本概念
- fb flux 实例分析
- spark core源码分析2 master启动流程
- 官方详解/proc/net/tcp
- 面试题总结-搜狐手机网Python开发工程师
- Http、TCP/IP协议与Socket的区别
- redis java客户端Jedis 实现 连接池 + 简单的负载均衡
- 用“逐步排除”的方法定位Java服务线上“系统性”故障
- 宇宙中最强大的开发环境免费了!
- 过滤器和拦截器的区别
- selenium grid解决多台电脑进行并发执行测试脚本
- Python学习小计
- 怎样彻底删除系统服务项