x86-architecture gcc calling convention

x86架构常见寄存器

register 8 bit 16 bit 32 bit 64 bit 描述
Accumulator register AH:AL AX EAX RAX
Base register BH:BL BX EBX RBX
Counter register CH:CL CX ECX RCX
Data register DH:DL DX EDX RDX
Stack Pointer register SP ESP RSP 栈顶指针,始终指向栈顶元素
Stack Base Pointer register BP EBP RBP 栈基指针,用于维护一个栈帧,在Intel的术语中叫帧指针,表示一个栈帧的开始地址
Source Index register SI ESI RSI
Destination Index register DI EDI RDI
Instruction Pointer IP EIP RIP
64位新增 R8-R15

段寄存器

  • Stack Segment(SS):指向栈的指针
  • Code Segment(CS):指向代码的指针
  • Data Segment(DS):指向数据的指针

常见组合:

register 描述
CS:IP (CS is Code Segment, IP is Instruction Pointer) points to the address where the processor will fetch the next byte of code.
SS:SP (SS is Stack Segment, SP is Stack Pointer) points to the address of the top of the stack, i.e. the most recently pushed byte.
DS:SI (DS is Data Segment, SI is Source Index) is often used to point to string data that is about to be copied to ES:DI.
ES:DI (ES is Extra Segment, DI is Destination Index) is typically used to point to the destination for a string copy, as mentioned above.

当我们调用一个函数和一个函数被调用,它们之间的参数和返回值是怎样传递的?gcc是怎样使用x86中的寄存器?

 调用规范

为了允许单独的程序员共享代码并开发供许多程序使用的库,并简化子例程的使用,程序员通常采用通用的调用约定。调用约定其实就是约定函数间如何调用和返回。例如,给定一组调用约定规则,程序员无需检查子例程的定义来确定应如何将参数传递给该子例程。 此外,给定一组调用约定规则,可以使高级语言编译器遵循这些规则,从而允许手动编码的汇编语言例程和高级语言例程相互调用。

下面我们会描述下C语言的调用约定。C调用约定非常依赖CPU硬件实现的栈结构,基于如下汇编指令push,pop,call,ret。

调用者约定
  • 在调用子例程这前,调用者应该保存一些特定寄存器的内容,这些内容称为caller-saved。被调用者callee允许对这些寄存器进行修改,如果调用者caller在子例程返回之后依然要使用这些值,caller必须把这些值push到栈中。
  • 为了把参数传递给子例程,在调用它们之前需先把参数入栈(现在的CPU设计并不是所有参数都得入栈,x86-64位在参数超过6个的情况下才会将6个之外的参数入栈),参数的入栈顺序为从右到左。
  • 使用call指令调用子例程。这个指令将返回地址压入栈中(在所参数之上)
    当子例程返回后,caller可从寄存器EAX中取得子例程的返回值,为了恢复调用前的状态,调用者应该做如下处理:
  • 移除栈中的参数。
  • 从栈中恢复caller-saved的寄存器内容。
被调用者(callee)约定
  • 将%rbp入栈,并将%rsp 赋值给%rbp
    1
    2
    pushq   %rbp             # 将调用者的rbp入栈
    movq %rsp, %rbp # 初始化一个新的栈帧

这个初始化操作维护了base pointer,rbp。rbp用于直接在栈中根据偏移量获取参数和局部变量。

  • 然后,将局部变量入栈,同时修改rsp寄存器的值。
  • 保存callee-saved的寄存器值,将这些值做入栈操作。
    当子例程执行完毕进行返回时,必须做如下操作:
  • 将返回值放在rax寄存器中
  • 恢复callee-saved寄存器的值
  • 销毁局部变量,一般通过修改栈指针的值来进行
  • 恢复调用者的rbp值,从栈中弹出rbp
  • 最后执行ret指令,这条指令会将return address从栈中移除

我们先来看一段代码,保存为cdecl.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int callee(int a, int b, int c,int d, int d, int f, int g){
return 0;
}

int caller(void)
{
return callee(1, 2, 3, 4, 5, 6, 7) +5;
}

int main()
{
return 0;
}

将上面这段代码编译为汇编代码,使用命令gcc -S cdecl.c,会生成一个名称为cdecl.s的文件。下面是调用者部分的汇编代码,采用AT&T格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
callee:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
movl %ecx, -16(%rbp)
movl %r8d, -20(%rbp)
movl %r9d, -24(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret

caller:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq $7
movl $6, %r9d
movl $5, %r8d
movl $4, %ecx
movl $3, %edx
movl $2, %esi
movl $1, %edi
call callee
addq $8, %rsp
addl $5, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc

由上汇编代码可以看到第一个参数放入到了寄存器edi,第二个参放入到了寄存器esi,第三个参数放入寄存器edx,依此类推。

gcc对x86寄存器的调用规则
Register Purpose Saved across calls Saved across calls
%rax temp register; return value No
%rbx callee-saved Yes
%rcx used to pass 4th argument to functions No
%rdx used to pass 3rd argument to functions No
%rsp stack pointer Yes
%rbp callee-saved; base pointer Yes
%rsi used to pass 2nd argument to functions No
%rdi used to pass 1st argument to functions No
%r8 used to pass 5th argument to functions No
%r9 used to pass 6th argument to functions No
%r10-r11 temporary No
%r12-r15 callee-saved registers Yes
函数调用时栈中的内容
栈地址 描述
16(%ebp) - third function parameter
12(%ebp) - second function parameter
8(%ebp) - first function parameter
4(%ebp) - old %EIP (the function’s “return address”)
0(%ebp) - old %EBP (previous function’s base pointer)
-4(%ebp) - first local variable
-8(%ebp) - second local variable
-12(%ebp) - third local variable

image

相关指令

push ax;将一个寄存器中的数据入栈
pop ax; 出栈,用一个寄存器接收出栈的数据.
ret指令用栈中数据,修改IP的内容,从而实现近转移。CPU执行ret指令时,进行下面两步操作:
(1)(IP) = ((ss)*16+(sp)) (2)(sp) = (sp) + 2 #16位cpu
call 标号;将当前的IP压栈后,转到标号处执行指令


Ref:
1.X86_Assembly-X86_Architecture
2.X86 Assembly/16, 32, and 64 Bits
3.http://unixwiz.net/techtips/win32-callconv-asm.html
4.http://unixwiz.net/techtips/win32-callconv.html
5.X86_calling_conventions
6.http://flint.cs.yale.edu/cs421/papers/x86-asm/asm.html#calling
7.X86-64 Architecture Guide
8.X86_assembly_language
9.https://pages.hep.wisc.edu/~pinghc/x86AssmTutorial.htm