X86&64 calling convention
2012-04-07 15:00:58| 分类: x86/64 Assembly |举报 |字号 订阅
1. 汇编函数参数传递方式
a. x86:__stdcall,_cdecl,__fastcall
1、__stdcall调用约定相当于16位动态库中经常使用的PASCAL调用约定。在32位的VC++5.0中PASCAL调用约定不再被支持(实际上它已被定义为__stdcall。除了__pascal外,__fortran和__syscall也不被支持),取而代之的是__stdcall调用约定。两者实质上是一致的,即函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈。
__stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。
2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。
_cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。
3、__fastcall调用约定是"人"如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
__fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。
在x86架构中,_cdecl方式参数传递通过从右到左压栈的方式,函数通过Eax寄存器返回值(除了返回浮点数,会用到ST0寄存器)。在linux中,自从gcc4.5后函数传递栈必须以16-bytes对齐。
如下面例子:
int function_name(int, int, int);
int a, b, c, x;
...
x = function_name(a, b, c);
产生汇编如下:
push c ; arg 3
push b ; arg 2
push a ; arg 1
call function_name ; jump to function_name's code
add esp, 12 ; pop function args (a, b, c) off the stack
mov x, eax ; fetch function return value
b.x86_64
(1) 参数个数少于7个:
f (a, b, c, d, e, f);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9
g (a, b)
a->%rdi, b->%rsi
有趣的是, 实际上将参数放入寄存器的语句是从右到左处理参数表的, 这点与32位的时候一致.
2) 参数个数大于 7 个的时候
H(a, b, c, d, e, f, g);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%rax
g->8(%esp)
f->(%esp)
2. 实践例子:在汇编中调用c函数
a. x86 : call.s
.section .data
output:
.ascii "hello world output addr:0x%x\n"
.section .text
.globl main
main:
pushl $output
pushl $output
call printf
addl $8, %esp
movl $1, %eax
movl $0, %ebx
int $0x80
#as -o call.o call.s
# ld -dynamic-linker /lib/ld-linux-x86.so.2 -o call_printf -lc call.o
#./call_printf
b. x86-64:call.s
.section .data
output:
.ascii "hello world output addr:0x%x\n"
data:
.int 0xa445a1a3
.section .text
.globl _start
_start:
mov $output, %rsi
mov $output, %rdi
call printf
movl $1, %eax
movl $0, %ebx
int $0x80
#as -o call.o call.s
# ld -dynamic-linker /lib/ld-linux-x86-64.so.2 -o call_printf -lc call.o
#./call_printf
3. calling conversion rules
(1). Caller Rules:图1是x86描述图,图2是x86-64描述图
Before call
a1. 在call函数前保存push会被函数改变的寄存器(返回值或x86-64下的参数传递寄存器):如eax, rdi等
a2. 传递参数给函数,x86通过push方式,x86-64通过寄存器传递方式。
a3. 利用call指令调用函数,该指令把return address 放在参数列表的顶部,然后跳转到函数代码。
After call
b1. 去掉传递参数, x86通过pop, x86-64不用处理
b2. 恢复被使用的寄存器值,通过pop的方式。
例子:
1. x86
pushl $19 #a2
pushl $20 #a2
call add #a3
addl $8, %esp #b1
movl %eax, %ebx
movl $1, %eax
int $0x80
2. x86-64
movl $19, %esi #a2
movl $20, %edi #a2
call add #a3
movl %eax, %ebx
movl $1, %eax
int $0x80
(1). Callee Rules
a1. 存储ebp,和esp
pushl %ebp
movl %esp, %ebp
a2. 在栈中分配本地局部变量 以及保存使用到的非易失寄存器如ebx
a3. 函数体返回值给eax
a4. 恢复esp,ebp以清除本地使用内存(变量),恢复非易失寄存器如ebx。
a5. ret指令结束函数
例子:
X86
.type add, @function
add:
pushl %ebp #a1
movl %esp, %ebp #a1
subl $4, %esp #a2
movl 8(%ebp), %eax #使用外部参数
movl %eax, -4(%ebp)
movl 12(%ebp), %eax
addl %eax, -4(%ebp) #利用内部变量
movl -4(%ebp), %eax #a3
movl %ebp, %esp #a4
popl %ebp #a4
ret #a5
X86-64
.type add, @function
add:
pushl %ebp #a1
movl %esp, %ebp #a1
subl $4, %esp #a2
movl %rdi, -4(%ebp) #使用外部参数和内部变量
addl %rsi, -4(%ebp)
movl -4(%ebp), %eax #a3
movl %ebp, %esp #a4
popl %ebp #a4
ret #a5