caller: 主调函数
callee: 被调函数
1 x86-64 / Inter 处理器: 1 个 CPU
有 16 个 64 位 通用目的 寄存器
, 存 整数
和 指针
1.1 运行时栈: 3 类 寄存器
为确保 callee `不会覆盖 caller 稍后要用的 寄存器值`,
x86-64 有一组 `规范:` 何时 use 哪个 寄存器
(1) callee 保存 寄存器
%bp
可作 帧指针, 存储 `当前栈帧 的 栈底;
%bp 入栈`, 通常是为了 `保存 前 1 栈帧 的 栈底`**
%bx
%r12-15
(2) caller 保存 寄存器
%ax : 返回值
%di %si %dx %cx %r8 %r9 : 第 1-6 个参数
//前1-4个分别是
d s d c //连接
i i x x
(3) 栈指针
%sp
1.2 callee / caller 保存 寄存器
(1) callee 保存 寄存器
`P 作为` main 的 `被调用者`
callee 保存
, 保证 其 ( callee~器 ) 值
在 callee 返回
时 与 callee 被调用
时 相同
(2) caller 保存 寄存器
保存
caller~器 是 caller 的责任
<=
`P 作为` Q 的 `调用者`
1) caller 在 caller~器 中 store 旧值
2) caller 调用 callee
3) callee 可随意修改 之
4) 调完 后, caller 还要用 stored 旧值
1.3 如何保证 函数调用 正确进行
答:只需让
正在运行的过程
在 需要时 ( 什么情况下需要 ? ) 正确保存 ( 如何保存 ? )
callee / caller ~器
(1) 若 callee
改 callee~器 ( %rbx )
, 由 callee
通过 将 callee~器 ( %rbx ) 入/出 ( callee ) 栈
来 保存
callee~器
// main 调 P
1) callee 将 %rbx (旧值) `push 到 callee 栈`
2) callee 中 mod %rbp
3) callee 返回前, `pop %rbp (旧值)` from callee 栈
(2) 若 callee
改 caller~器 ( %rdi )
且 caller 调用 callee 后 还要用 caller~器
, 由 caller
通过 将 caller~器 ( %rdi ) 保存到 callee~器 ( %rbx )
来 保存
caller~器
P ( caller ) 调 Q (callee)
`Q 会 改 %rdi` + Q 返回后, `P 还要用 %rdi (旧值)`
=>
1) `P 保存 %rdi (旧值) 到 %rbx`
=> `P 改 %rbx
=> P 要先保存 %rbx ( 给 P 的 caller 用, 此时 P 又作为 callee ):
将 %rbx push 到 P 栈帧, P 返回前 pop %rbx`
2) P 调 Q
3) Q 改 %rdi
4) `P 用 %rbx 恢复 %rdi ( 旧值 ), 再 use %rdi ( 旧值 )`
##1.4 eg
// eg1: 函数调用 底层机制
// 函数调用 过程中 callee / caller 保存寄存器 如何 被 callee / caller 保存?
// long P(long x):
// return x + Q(0);
//.c
#include
long Q(long x)
{
x = 2;
return x;
}
long P(long x)
{
// 2-1 %rdi: 旧值 x
int y;
// 2-2 %rdi 被 Q (作 P 的 callee) 修改: 新值为0
y = Q(0);
// 2-3 P 调 Q 之后, P (作 Q 的 caller) 还要用 %rdi 旧值x
return x + y;
}
int main()
{
long x = 10;
long y = P(x);
printf("%ld", y);
}
//.s
Q:
movl $2, %eax
ret
P:
pushq %rbx // 1-2 `由 P (作 main 的 callee) 保存 %rbx`: 通过 入/出 P 栈帧
movq %rdi, %rbx // 1-1 %rbx 被 P (作 main 的 callee) 修改 / 2-4 `由 P ( 作 Q 的 caller ) 保存 %rdi` ( 旧值 ) 到 %rbx => 1
movl $0, %edi
call Q
cltq
addq %rbx, %rax
popq %rbx
ret
// eg2: caller 保存寄存器 无需保存 的 case
//.c
long P(long x)
{
// 1. %rdi: 旧值 x
// 2. %rdi 被改: 新值 x + 1
return Q(x+1);
// 3. 之后, P ( caller ) 不再用 %rdi 旧值 x => P ( caller ) 不用保存 %rdi
}
//.s
P: // P 不用保存 %rdi
addq $1, %rdi
call Q
ret
1.5 为什么 caller~器 要由 caller ( 而不是 callee ) 保存
?
答:
eg:
`P 调 Q + Q 改 %rdi + Q 返回后, P 还要用 %rdi (旧值):`
long P(long x):
return x + Q(0);
`若 由 callee 保存`
Q 保存 %rdi 到 %rbx -> until Q 返回 P 时, 才能用 %rbx 恢复 %rdi ( 旧值 )
=> `Q (作为 P 的 callee) 不能 通过 入/出 Q 栈 来 保存 %rbx`
=> Q 返回 P 时, %rbx 的 值被改了
保存了 %rdi, 却把 %rbp 值丢了
2 汇编:试图最大化一段代码的性能
PC:
程序计数器, 存 下一条指令(在内存中)的地址
linux下 由 .c 文件 得到汇编文件 .s:
gcc -Og -S hello.c
2.1 三种操作数
立即数 immediate
寄存器 register
内存引用
2.2 数据传送指令 : 将数据从一个位置复制到另一个位置
1. 指令结构
最后2个字符:分别是 源和目的的大小
倒数第3个字符:z - 零扩展,s-符号扩展
2. MOV 系列指令的 5种情形:src -> dst
//Immediate -> Register :
movl $0x4050, %eax // 0x4050 -> %eax
//Immediate -> Memeory :
movb $-17, (%rsp) // -17 -> * %rsp
//Register -> Register :
movw %bp, %sp // %bp -> %sp
//Register -> Memeory :
movq %rax, -12(%rbp) // %rax -> *(%rsp - 12)
//Memory -> Register :
movb (%rdi, %rcx), %al // *(%rdi + %rcx) -> %al
(1) dst 必须是 Register/Memory
(2) Memory 不能到 Memory
(3) 1方 非 内存
时,传送
的是 Immediate/Register 本身的值
(4) 1方 为 内存
, 即 用 offset( ... )
形式时,这1方交互的是 () 中的 值 和 偏移 形成的 内存地址 上的值
3. 指针的 间接引用 pointer dereferencing**
x = *xp; // `读` sp 所指内存中的值 到 x
*xp = y; // `写` y 的值 到 sp 所指内存
2.3 压入 和 弹出 栈数据
栈指针 %rsp:
保存 栈顶
元素地址
入栈: 栈指针 减8, 值 写 到栈顶
pushq %rbp
<=>
subq $8, %rsp
movq %rbp, (%rsp) // write %rbp on stack
出栈:读 栈顶数据, 栈指针加8
popq %rax
<=>
movq (%rsp), %rax // read %rax from stack
addq $8, %rsp
2.4 算术
1. 加载有效地址
leaq: 将 有效地址 写到 目的操作数
note: 形式 是 从内存 `读数据` 到寄存器
leaq 7(%rdx, %rdx, 4), %rax
将 %rax 的值 设为 5x + 7, 假定 %rdx 值为 x
2. 二元操作
`第2操作数: 既是源又是目的`
必须是寄存器或内存
当为内存地址时,
CPU 从内存中读出值,
执行操作,
结果写入内存
2.5 跳转
jmp *%rax // 新地址为 %rax
jmp *(%rax) // 新地址为 以 %rax 的值为地址的内存中的值
3 过程/函数调用 的机制
函数 如何 保存现场 并 返回
`过程`: 抽象机制
函数 function, 方法method, 子例程 subroutine, 处理函数 handler
P 调用 Q, Q 执行后 返回到 P:
1)传递控制
1) `进入 Q 时`, PC 设为 `Q 第1条指令的地址`
2) `Q 返回 时`, PC 设为 `Q 的 返回地址`:
P 中 调用 Q 指令 后面 那条指令的地址
2)传递数据
P 给 Q 提供 参数, Q 向 P 返回1个值
3)分配 和 释放内存
Q 开始时 要为 `loacl 变量 分配空间`,
Q 返回前 必须释放这些空间
3.1 运行时栈
栈帧结构
中 被保存的 register: push 到 栈帧 的 register
C 语言 过程调用机制
的关键:使用 栈
提供的 后进先出
的 内存管理
栈帧:
栈上分配的空间
例: P 调 Q, Q 正在执行
1) `P 及 P 的 调用链 中 过程`, 都暂时被 `挂起`
2) 系统分配 P 的栈帧:
将 栈指针减小/增加 可以 在栈上 分配/释放 空间
1. 何时必须 用栈传递?
3种情形之一:
(1)需保存 被保存的寄存器
: 即 被调用者保存寄存器
(2)存在
loacl variable必须通过 栈 传递
1)寄存器 不够存 所有 local variable
2)local variable 用 取址运算符 &:
因为 `只能对 内存 取地址`
3)local variable 为 数组或结构:
数组和结构占用的空间是 一段连续的内存
(3) 该过程 又调用新过程
, 而 寄存器 不够存
调用 新过程 所用的所有 arg(7-n个arg)
2. P 调 Q, Q 的 返回地址 作为 P 的 栈帧 的一部分, 因为它存放 与 P相关的状态
- P 调 Q 时的
参数传递:
每个
栈帧都
基于一个
函数, 栈帧 随着 函数的生命周期产生、发展和消亡
(1)`寄存器传递:` 最多传 `6个整数值(指针 和 整数)`
(2)`栈传递: 第7-n个参数, 参数 7->n 反顺序压栈
=> 参数 7 在栈顶`
栈帧结构中用到2个寄存器来定位 当前帧的空间,
实际上 未必一定需要帧指针:
`%ebp/%esp : 帧/栈`指针,
总是指向当前帧的底/顶部
`编译器 根据 汇编指令集` 规则 `小心调整 %ebp %esp 的值`
3.2 转移控制
Q 的返回地址 : P 中 call Q 指令的 下一条指令的地址
1. 如何将 控制 从 P 转到 Q ?
(1) call Q
1) Q 返回地址 压栈
2) 设 PC
为 Q 第1条指令 地址
(2)去执行: 执行 下一条指令 / PC 所指 指令
2. 如何将 控制 从 Q 返回到 P ?
(1) ret
1) 弹栈 出 Q 返回地址
2) 设给 PC
(2)去执行
3.3 数据传送
1. P 调用 Q
(1)参数传递 : P 把
`参数 1-6 复制到 适当的寄存器`;
`参数 7-n 用 栈传递, 数据大小 都向 指针大小`
= 8 (64位系统) 的 倍数对齐
(2) call Q: 控制转移到 Q
2. Q 返回 P: P 可访问 寄存器 %rax 中 返回值
3.4 函数调用时 参数的传递: 寄存器传递/栈传递
1. 栈 上 局部存储
(1) `栈就是一段内存, 用来进行 内存管理``
栈 的 意义: 函数调用
中 保存/恢复 被保存的寄存器 / local variable / 实参argument / 返回地址 等
(2) 不需要 出栈/入栈 就能 读取/写入 栈中任何位置的内存值
, 通过 `栈指针 加 偏移 加 读/写 操作` 即可
// eg3
// long call_proc():
// proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
//主调函数 .c
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);
}
//.s
call_proc
// set up arguments to proc: 为调用 proc 作准备
// alloc 32-byte stack frame, 不算 call_proc 中 被调函数 proc 返回地址 所占空间,
// 因 call proc 里包含 proc 返回地址 压栈
subq $32, %rsp
// 1. local variable space in stack
movq $1, 24(%rsp) // store 1 in &x1
movl $2, 20(%rsp)
movw $3, 18(%rsp)
movb $4, 17(%rsp)
// 2. argument 7-8 : pass by stack
leaq 17(%rsp), %rax // create &x4, %rax 只用作普通寄存器, 作存储用
movq %rax, 8(%rsp) // store &x4 as argunent 8
movl $4, (%rsp) // store 4 as argunent 7
// 3. argument1-6 : pass by register
leaq 18(%rsp), %r9 // pass &x3 as argunent 6
movl $3, %r8d // pass 3 as argunent 5
leaq 20(%rsp), %rcx
movl $2, %edx
leaq 24(%rsp), %rsi
movl $1, %edi
// 4. call proc()
call proc
// 5. Retrive changes to memory, %rdx %eax %ecx 这里只用作普通存储器, 作存储用
movslq 20(%rsp), %rdx // get x2 and convert to long
addq 24(%rsp), %rdx // compute x1+x2
movswl 18(%rsp), %eax
movsbq 17(%rsp), %ecx
subl %ecx, %eax
cltq //convert to long
imulq %rdx, %rax
addq $32, %rsp
ret
图中的数字为 `字节序号`,
64 位OS,
`指针大小 = 地址总线 = 64位 = 8 Byte`
(3) call_proc 汇编 大部分是为 调用 proc 作准备:
1) 栈上为 局部变量 x1 - x4 建立栈帧, 即 分配存储空间
2) `leaq 指令 生成到 到这些变量 内存的 指针`
3) 为 参数8-7 建立栈帧
4) 参数 1-6 加载至 寄存器
call_proc:
执行 call proc 指令:
proc 返回地址 入栈,
subq $8, %rsp // 在栈上 分配空间
proc:
执行 ret 指令:
proc 返回地址 出栈,
addq $8, %rsp // 在栈上 释放空间
note: 书中画成下图, 应该不对
//note: 1个函数也可以直接在 linux 下 进行汇编,得到.s文件
//被调函数 .c
#include
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;
}
//.s
proc: // 函数 proc 没有栈帧
movq 16(%rsp), %rax
addq %rdi, (%rsi)
addl %edx, (%rcx)
addw %r8w, (%r9)
movl 8(%rsp), %edx
addb %dl, (%rax)
ret
// eg4
// caller():
// long sum = swap_add(&arg1, &arg2);
64位系统, 指针 为 8 Byte
2. 寄存器 中 局部存储
//eg5
// long P(long x, long y):
// return Q(x) + Q(y);
#include
long Q(long x)
{
return 2*x;
}
long P(long x, long y)
{
long u = Q(y);
long v = Q(x);
return u + v;
}
int main()
{
long x = 10;
long y = 20;
long z = P(x, y);
printf("%ld", z);
}
Q: // note:Q 无栈帧, Q 参数个数<7, 所有 参数都通过 寄存器传递了
leaq (%rdi,%rdi), %rax
ret
P:
// x in %rdi
// y in %rsi
pushq %rbp // save %rbp
pushq %rbx // save %rbx
movq %rdi, %rbp // save x(%rdi) to %rbp
movq %rsi, %rdi // move y(%rsi) to first argument %rdi
call Q // call Q(y)
movq %rax, %rbx // Save Q(y)(saved in %rax) result to %rbx
movq %rbp, %rdi // move x(saved in %rbp) to first argument %rdi
call Q // call Q(x)
addq %rbx, %rax // add Q(y)(saved in %rbx) to Q(x) (saved in %rax)
popq %rbx // restore %rbx
popq %rbp // restore %rbp
ret
这里, 只着重分析 P的 汇编 和 栈帧变化:
(1) P的参数
x : %rdi
y : %rsi
(2) P 中 local 变量
P正在运行 行为和栈帧变化:
1)`P 保存 调~器 %rdi / %rax 到 被~器 %rbp / %rbx`
1> %rdi:
先存旧值x
-> P调Q(y)时, 被P修改为新值y
-> P调Q(x)时, P又要用其旧值x
note: `P不修改 %rsi => P 不用保存 %rsi`
2> %rax:
P 调 Q(y)时, 先存 %rax 旧值 Q(y)
-> P 调 Q(x) 时, %rax 被 P 修改为新值Q(x)
-> 返回 u+v 时, P又要用其旧值 u=Q(y)
2)`P 保存 被~器 %rbp %rbx 到 P 的 栈帧`
P 保存 %rdi / %rax 到 %rbp / %rbx,
即 P 会修改 被~器 %rbp %rbx
=> P 要先保存 %rbp %rbx
(3) P的栈帧变化:
4. 数据对齐
简化 CPU 与 内存系统
间 接口 硬件设计
1. Intel x86-64 建议的 `对齐原则:`
(1) K 字节 基本对象 的 地址
必须是 K 的倍数
每种类型的对象 都满足 自己的对齐限制, 就能 保证对齐
(2) struct 类型的 指针:
`成员 j: 4 字节对齐 => 其 地址是 4 的倍数`