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
2pushq %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
41callee:
.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 |
相关指令
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