文章目录
一. 函数调用基本概念
函数f调用函数g, 则称f为caller,g为callee
栈帧: 程序进行时系统自动分配的栈区域
%rsp 是一个指针指向栈顶部(用来确认栈的位置),并会随程序运行动态变化
二. 函数调用需解决的5大问题
1. 怎么调用callee函数
在汇编代码中,执行函数调用采用的是call指令
和jump类似,它也有直接和间接之分
- call label (direct)
- call *operand (indirect)
执行call指令时,机器执行了以下操作:
- 将返回的地址存在了栈中
- 跳到了callee函数的路口
可以这么理解:call = push + jmp
push retaddr
jmp callee
举个栗子:
执行call前后%rip和%rsp的变化:
push指令含义:
2. 怎么返回caller函数
ret指令 (return缩写?)
机器执行的操作:
- 将返回地址从栈中弹出
- Jump到该return address(回到caller中)
ret = pop + jmp
pop retaddr
jmp retaddr
还是上面的例子,%rip和%rsp的变化:
栈帧的结构:
%rsp仅指向栈帧的最顶部!
返回前,Callee Frame内的各个数据出栈(%rsp上移),直到readdr
然后执行ret,pop readdr
3. 怎么传参以及怎么返回值
使用寄存器以及栈存储变量
系统使用了特定的寄存器用来传参
其中整数寄存器有6个, 分别是 %rdi, %rsi, %rdx, %rcx, %r8, %r9
(对应的字节数更少的表示同理,如%edi, %dl等)
特定的寄存器存返回值: %rax
? 如果参数个数多于6个呢
- 将多出的参数保存在caller的栈帧中
- 参数就保存在retaddr的上面
- 参数存放的顺序是从第N个到第7个(顺序很重要!)
即第N个参数先入栈,内存位置最大
第7个参数最后入栈,内存位置最小 - 所有的数据类型(大小)都被扩充为8的倍数
这些参数如何被callee访问: %rsp + 偏移量
调用前汇编代码呈现:
push argument N
…
push argument 7
movq argument 6 %r9
…
movq argument 1 %rdi
call callee
以下是一个调用实例:
void proc(long a1, long *a1p,
int a2, int *a2p,
short a3, short *a3p,
char a4, char *a4p)
{
*a1p += a1 ;
*a2p += a2 ;
*a3p += a3 ;
*a4p += a4 ;
}
4. 寄存器共用的问题怎么解决
由于寄存器被所有进程共用,要求:
- 只有1个进程处在active状态
意思是在该进程运行中通过中可以随意修改寄存器的值,而其它进程无法修改
那其它进程中储存在寄存器中的值被破坏了怎么办? - 将共用的寄存器内容彼此隔离
即要保存寄存器中的值到栈中,具体分为:- caller-save register
即寄存器值由caller保存,存在Caller Frame中
- callee-save register
即寄存器值由callee保存,存在Callee Frame中
- 不管是哪种save,只要考虑被进程使用的寄存器,不使用的不用save
-
Caller-save registers
- %rax, %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10, %r11
- Saved by caller
- Callee can use these registers freely
- The contents in these registers may be changed after return
- Caller must restore them if it tries to use them after calling
Caller-save registers一般用于函数的传参,以及用于返回值(%rax),即希望callee修改这些寄存器的情况
如果希望保留原寄存器中的值。则要进栈
- Callee-save registers
- –%rbx, %rbp, %r12-15
- Saved by callee
- Caller can use these registers freely
- Callee must save them before using
- Callee must restore them before return
callee-save registers一般用于确保caller中寄存器的值不会被修改,使得callee可以放心地使用寄存器进行操作
下面举个栗子:
long P(long x, long y)
{
long u = Q(y);
long v = Q(x);
return u + v;
}
P的汇编代码:
Line | Instruction | Description |
---|---|---|
1 | P: | Label definition for P |
2 | pushq %rbp | Save %rbp (callee-save register) |
3 | pushq %rbx | Save %rbx |
4 | subq $8, %rsp | Align stack frame |
5 | movq %rdi, %rbp | Save x (exists in callee-save register, safe) |
6 | movq %rsi, %rdi | Move y to the first argument |
7 | call Q | Call Q(y) |
8 | movq %rax, %rbx | Save the result |
9 | movq %rbp, %rdi | Move x to the first argument |
10 | call Q | Call Q(x) |
11 | addq %rbx, %rax | Add saved Q(y) to Q(x) |
12 | addq $8, %rsp | Deallocate the last part of the stack |
13 | popq %rbx | Restore %rbx |
14 | popq %rbp | Restore %rbp |
15 | ret | Return |
5. 局部变量存在哪里
存在栈上(即答)
不存在寄存器中的理由:
- 数量不够
- 变量是数组(连续存储) 以及 结构体(一组连续数据)
- 需要取地址(比如&a)
存放的位置: 就存在calle-save rigisters 的下面
释放:add %rsp
访问: %rsp + 偏移
举一个变量存放的栗子:
long call_proc()
{
long x1 = 1; int x2 = 2;
short x3 = 3; char x4 = 4;
proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
return (x1+x2)*(x3-x4);
}
leaq 17(%rsp), %rax Create &x4
movq %rax, 8(%rsp) Store &x4 as argument 8
movl $4, (%rsp) Store 4 as argument 7
leaq 18(%rsp), %r9 Pass &x3 as argument 6
movl $3, %r8d Pass 3 as argument 5
leaq 20(%rsp), %rcx Pass &x2 as argument 4
movl $2, %edx Pass 2 as argument 3
leaq 24(%rsp), %rsi Pass &x1 as argument 2
movl $1, %edi Pass 1 as argument 1
总结
本文介绍了函数调用是怎么在机器上实现的,并给出了相应的汇编代码.
更多文章:
数据在内存中的对齐问题
计算机编译程序的原理
汇编语句详解(持续更新)
关于位运算必须记住的事
C语言中的类型转换