caller: 主調(diào)函數(shù)
callee: 被調(diào)函數(shù)
1 x86-64 / Inter 處理器: 1 個 CPU
有 16 個 64 位 通用目的 寄存器
, 存 整數(shù)
和 指針
1.1 運(yùn)行時棧: 3 類 寄存器
為確保 callee `不會覆蓋 caller 稍后要用的 寄存器值`,
x86-64 有一組 `規(guī)范:` 何時 use 哪個 寄存器
(1) callee 保存 寄存器
%bp
可作 幀指針, 存儲 `當(dāng)前棧幀 的 棧底;
%bp 入棧`, 通常是為了 `保存 前 1 棧幀 的 棧底`**
%bx
%r12-15
(2) caller 保存 寄存器
%ax : 返回值
%di %si %dx %cx %r8 %r9 : 第 1-6 個參數(shù)
//前1-4個分別是
d s d c //連接
i i x x
(3) 棧指針
%sp
1.2 callee / caller 保存 寄存器
(1) callee 保存 寄存器
`P 作為` main 的 `被調(diào)用者`
callee 保存
, 保證 其 ( callee~器 ) 值
在 callee 返回
時 與 callee 被調(diào)用
時 相同
(2) caller 保存 寄存器
保存
caller~器 是 caller 的責(zé)任
<=
`P 作為` Q 的 `調(diào)用者`
1) caller 在 caller~器 中 store 舊值
2) caller 調(diào)用 callee
3) callee 可隨意修改 之
4) 調(diào)完 后, caller 還要用 stored 舊值
1.3 如何保證 函數(shù)調(diào)用 正確進(jìn)行
答:只需讓
正在運(yùn)行的過程
在 需要時 ( 什么情況下需要 ? ) 正確保存 ( 如何保存 ? )
callee / caller ~器
(1) 若 callee
改 callee~器 ( %rbx )
, 由 callee
通過 將 callee~器 ( %rbx ) 入/出 ( callee ) 棧
來 保存
callee~器
// main 調(diào) P
1) callee 將 %rbx (舊值) `push 到 callee 棧`
2) callee 中 mod %rbp
3) callee 返回前, `pop %rbp (舊值)` from callee 棧
(2) 若 callee
改 caller~器 ( %rdi )
且 caller 調(diào)用 callee 后 還要用 caller~器
, 由 caller
通過 將 caller~器 ( %rdi ) 保存到 callee~器 ( %rbx )
來 保存
caller~器
P ( caller ) 調(diào) 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 調(diào) Q
3) Q 改 %rdi
4) `P 用 %rbx 恢復(fù) %rdi ( 舊值 ), 再 use %rdi ( 舊值 )`
##1.4 eg
// eg1: 函數(shù)調(diào)用 底層機(jī)制
// 函數(shù)調(diào)用 過程中 callee / caller 保存寄存器 如何 被 callee / caller 保存?
// long P(long x):
// return x + Q(0);
//.c
#include <stdio.h>
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 調(diào) 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 調(diào) Q + Q 改 %rdi + Q 返回后, P 還要用 %rdi (舊值):`
long P(long x):
return x + Q(0);
`若 由 callee 保存`
Q 保存 %rdi 到 %rbx -> until Q 返回 P 時, 才能用 %rbx 恢復(fù) %rdi ( 舊值 )
=> `Q (作為 P 的 callee) 不能 通過 入/出 Q 棧 來 保存 %rbx`
=> Q 返回 P 時, %rbx 的 值被改了
保存了 %rdi, 卻把 %rbp 值丟了
2 匯編:試圖最大化一段代碼的性能
PC:
程序計數(shù)器, 存 下一條指令(在內(nèi)存中)的地址
linux下 由 .c 文件 得到匯編文件 .s:
gcc -Og -S hello.c
2.1 三種操作數(shù)
立即數(shù) immediate
寄存器 register
內(nèi)存引用
2.2 數(shù)據(jù)傳送指令 : 將數(shù)據(jù)從一個位置復(fù)制到另一個位置
1. 指令結(jié)構(gòu)
最后2個字符:分別是 源和目的的大小
倒數(shù)第3個字符:z - 零擴(kuò)展,s-符號擴(kuò)展
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方 非 內(nèi)存
時,傳送
的是 Immediate/Register 本身的值
(4) 1方 為 內(nèi)存
, 即 用 offset( ... )
形式時译荞,這1方交互的是 () 中的 值 和 偏移 形成的 內(nèi)存地址 上的值
3. 指針的 間接引用 pointer dereferencing**
x = *xp; // `讀` sp 所指內(nèi)存中的值 到 x
*xp = y; // `寫` y 的值 到 sp 所指內(nèi)存
2.3 壓入 和 彈出 棧數(shù)據(jù)
棧指針 %rsp:
保存 棧頂
元素地址
入棧: 棧指針 減8, 值 寫 到棧頂
pushq %rbp
<=>
subq $8, %rsp
movq %rbp, (%rsp) // write %rbp on stack
出棧:讀 棧頂數(shù)據(jù), 棧指針加8
popq %rax
<=>
movq (%rsp), %rax // read %rax from stack
addq $8, %rsp
2.4 算術(shù)
1. 加載有效地址
leaq: 將 有效地址 寫到 目的操作數(shù)
note: 形式 是 從內(nèi)存 `讀數(shù)據(jù)` 到寄存器
leaq 7(%rdx, %rdx, 4), %rax
將 %rax 的值 設(shè)為 5x + 7, 假定 %rdx 值為 x
2. 二元操作
`第2操作數(shù): 既是源又是目的`
必須是寄存器或內(nèi)存
當(dāng)為內(nèi)存地址時,
CPU 從內(nèi)存中讀出值,
執(zhí)行操作,
結(jié)果寫入內(nèi)存
2.5 跳轉(zhuǎn)
jmp *%rax // 新地址為 %rax
jmp *(%rax) // 新地址為 以 %rax 的值為地址的內(nèi)存中的值
3 過程/函數(shù)調(diào)用 的機(jī)制
函數(shù) 如何 保存現(xiàn)場 并 返回
`過程`: 抽象機(jī)制
函數(shù) function, 方法method, 子例程 subroutine, 處理函數(shù) handler
P 調(diào)用 Q, Q 執(zhí)行后 返回到 P:
1)傳遞控制
1) `進(jìn)入 Q 時`, PC 設(shè)為 `Q 第1條指令的地址`
2) `Q 返回 時`, PC 設(shè)為 `Q 的 返回地址`:
P 中 調(diào)用 Q 指令 后面 那條指令的地址
2)傳遞數(shù)據(jù)
P 給 Q 提供 參數(shù), Q 向 P 返回1個值
3)分配 和 釋放內(nèi)存
Q 開始時 要為 `loacl 變量 分配空間`,
Q 返回前 必須釋放這些空間
3.1 運(yùn)行時棧
棧幀結(jié)構(gòu)
中 被保存的 register: push 到 棧幀 的 register
C 語言 過程調(diào)用機(jī)制
的關(guān)鍵:使用 棧
提供的 后進(jìn)先出
的 內(nèi)存管理
棧幀:
棧上分配的空間
例: P 調(diào) Q, Q 正在執(zhí)行
1) `P 及 P 的 調(diào)用鏈 中 過程`, 都暫時被 `掛起`
2) 系統(tǒng)分配 P 的棧幀:
將 棧指針減小/增加 可以 在棧上 分配/釋放 空間
1. 何時必須 用棧傳遞诫欠?
3種情形之一:
(1)需保存 被保存的寄存器
: 即 被調(diào)用者保存寄存器
(2)存在
loacl variable必須通過 棧 傳遞
1)寄存器 不夠存 所有 local variable
2)local variable 用 取址運(yùn)算符 &:
因?yàn)?`只能對 內(nèi)存 取地址`
3)local variable 為 數(shù)組或結(jié)構(gòu):
數(shù)組和結(jié)構(gòu)占用的空間是 一段連續(xù)的內(nèi)存
(3) 該過程 又調(diào)用新過程
, 而 寄存器 不夠存
調(diào)用 新過程 所用的所有 arg(7-n個arg)
2. P 調(diào) Q, Q 的 返回地址 作為 P 的 棧幀 的一部分, 因?yàn)樗娣?與 P相關(guān)的狀態(tài)
- P 調(diào) Q 時的
參數(shù)傳遞:
每個
棧幀都
基于一個
函數(shù), 棧幀 隨著 函數(shù)的生命周期產(chǎn)生抖拴、發(fā)展和消亡
(1)`寄存器傳遞:` 最多傳 `6個整數(shù)值(指針 和 整數(shù))`
(2)`棧傳遞: 第7-n個參數(shù), 參數(shù) 7->n 反順序壓棧
=> 參數(shù) 7 在棧頂`
棧幀結(jié)構(gòu)中用到2個寄存器來定位 當(dāng)前幀的空間,
實(shí)際上 未必一定需要幀指針:
`%ebp/%esp : 幀/棧`指針,
總是指向當(dāng)前幀的底/頂部
`編譯器 根據(jù) 匯編指令集` 規(guī)則 `小心調(diào)整 %ebp %esp 的值`
3.2 轉(zhuǎn)移控制
Q 的返回地址 : P 中 call Q 指令的 下一條指令的地址
1. 如何將 控制 從 P 轉(zhuǎn)到 Q ?
(1) call Q
1) Q 返回地址 壓棧
2) 設(shè) PC
為 Q 第1條指令 地址
(2)去執(zhí)行: 執(zhí)行 下一條指令 / PC 所指 指令
2. 如何將 控制 從 Q 返回到 P ?
(1) ret
1) 彈棧 出 Q 返回地址
2) 設(shè)給 PC
(2)去執(zhí)行
3.3 數(shù)據(jù)傳送
1. P 調(diào)用 Q
(1)參數(shù)傳遞 : P 把
`參數(shù) 1-6 復(fù)制到 適當(dāng)?shù)募拇嫫鱜;
`參數(shù) 7-n 用 棧傳遞, 數(shù)據(jù)大小 都向 指針大小`
= 8 (64位系統(tǒng)) 的 倍數(shù)對齊
(2) call Q: 控制轉(zhuǎn)移到 Q
2. Q 返回 P: P 可訪問 寄存器 %rax 中 返回值
3.4 函數(shù)調(diào)用時 參數(shù)的傳遞: 寄存器傳遞/棧傳遞
1. 棧 上 局部存儲
(1) `棧就是一段內(nèi)存, 用來進(jìn)行 內(nèi)存管理``
棧 的 意義: 函數(shù)調(diào)用
中 保存/恢復(fù) 被保存的寄存器 / local variable / 實(shí)參argument / 返回地址 等
(2) 不需要 出棧/入棧 就能 讀取/寫入 棧中任何位置的內(nèi)存值
, 通過 `棧指針 加 偏移 加 讀/寫 操作` 即可
// eg3
// long call_proc():
// proc(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
//主調(diào)函數(shù) .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: 為調(diào)用 proc 作準(zhǔn)備
// alloc 32-byte stack frame, 不算 call_proc 中 被調(diào)函數(shù) 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
圖中的數(shù)字為 `字節(jié)序號`,
64 位OS,
`指針大小 = 地址總線 = 64位 = 8 Byte`
(3) call_proc 匯編 大部分是為 調(diào)用 proc 作準(zhǔn)備:
1) 棧上為 局部變量 x1 - x4 建立棧幀, 即 分配存儲空間
2) `leaq 指令 生成到 到這些變量 內(nèi)存的 指針`
3) 為 參數(shù)8-7 建立棧幀
4) 參數(shù) 1-6 加載至 寄存器
call_proc:
執(zhí)行 call proc 指令:
proc 返回地址 入棧,
subq $8, %rsp // 在棧上 分配空間
proc:
執(zhí)行 ret 指令:
proc 返回地址 出棧,
addq $8, %rsp // 在棧上 釋放空間
note: 書中畫成下圖, 應(yīng)該不對
//note: 1個函數(shù)也可以直接在 linux 下 進(jìn)行匯編,得到.s文件
//被調(diào)函數(shù) .c
#include <stdio.h>
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: // 函數(shù) 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位系統(tǒng), 指針 為 8 Byte
2. 寄存器 中 局部存儲
//eg5
// long P(long x, long y):
// return Q(x) + Q(y);
#include <stdio.h>
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 參數(shù)個數(shù)<7, 所有 參數(shù)都通過 寄存器傳遞了
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的參數(shù)
x : %rdi
y : %rsi
(2) P 中 local 變量
P正在運(yùn)行 行為和棧幀變化:
1)`P 保存 調(diào)~器 %rdi / %rax 到 被~器 %rbp / %rbx`
1> %rdi:
先存舊值x
-> P調(diào)Q(y)時, 被P修改為新值y
-> P調(diào)Q(x)時, P又要用其舊值x
note: `P不修改 %rsi => P 不用保存 %rsi`
2> %rax:
P 調(diào) Q(y)時, 先存 %rax 舊值 Q(y)
-> P 調(diào) 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. 數(shù)據(jù)對齊
簡化 CPU 與 內(nèi)存系統(tǒng)
間 接口 硬件設(shè)計
1. Intel x86-64 建議的 `對齊原則:`
(1) K 字節(jié) 基本對象 的 地址
必須是 K 的倍數(shù)
每種類型的對象 都滿足 自己的對齊限制, 就能 保證對齊
(2) struct 類型的 指針:
`成員 j: 4 字節(jié)對齊 => 其 地址是 4 的倍數(shù)`