程式的機器級表示
時隔一年把 CS:APP 再看一遍,尤其針對棧的執行機制加深理解。
訪問資訊
16個通用暫存器
一個 x86-64 CPU 包含一組16個儲存64位值的通用目的暫存器
。雖然是通用暫存器,但也有一些約定成俗的用法。r8 r9 ... 為80386之後擴充套件的8個暫存器
- \(rax\), 返回值
- \(rbx\), 被呼叫者儲存
- \(rcx\), 第4個引數
- \(rdx\), 第3個引數
- \(rsi\), 第2個引數
- \(rdi\), 第1個引數
- \(rbp\), 被呼叫者儲存
- \(rsp\), 棧指標。最為特殊, 用來指明棧的結束位置(棧頂)
- \(r8\), 第5個引數
- \(r9\), 第6個引數
- \(r10\), 呼叫者儲存
- \(r11\), 呼叫者儲存
- \(r12\), 被呼叫者儲存
- \(r13\), 被呼叫者儲存
- \(r14\), 被呼叫者儲存
- \(r15\), 被呼叫者儲存
運算元指令符
大多數指令有一個或多個運算元(operand
)指示出執行一個操作中要使用的源運算元,以及放置結果的目的運算元。根據源資料和目的位置的取值可分為三種型別
- 立即數(
immediate
), 用來表示常數值 - 暫存器(
register
), 表示某個暫存器的內容 - 記憶體引用, 根據計算出來的地址訪問某個記憶體位置
多種定址模式
- 在 ATT 格式的彙編程式碼中,立即數的寫法為 \(\$\) 後跟一個標準 C 表示法的整數
- 用符號 \(r_a\) 來表示任意暫存器 \(a\), 用引用 \(R[r_a]\) 來表示它的值, 這裡將暫存器集合看成一個陣列,用暫存器識別符號作為索引
- 將記憶體看作一個很大的位元組陣列,用符號 \(M_b[Addr]\) 表示對儲存在記憶體中可以地址 \(Addr\) 開始的 \(b\) 個位元組值的引用, 下表省略下標 \(b\)
型別 | 格式 | 運算元值 | 名稱 |
---|---|---|---|
立即數 | $$Imm $ | \(Imm\) | 立即數定址 |
暫存器 | \(r_a\) | \(R[r_a]\) | 暫存器定址 |
儲存器 | \(Imm\) | \(M[Imm]\) | 絕對定址 |
儲存器 | \((r_a)\) | \(M[R[r_a]]\) | 間接定址 |
儲存器 | \(Imm(r_b)\) | \(M[Imm + R[r_a]]\) | (基址 + 偏移值)定址 |
儲存器 | \((r_b, r_i)\) | \(M[R[r_b] + R[r_i]]\) | 變址定址 |
儲存器 | \(Imm(r_b, r_i)\) | \(M[Imm + R[r_b] + R[r_i]]\) | 變址定址 |
儲存器 | \((, r_i, s)\) | \(M[R[r_i] \cdot s ]\) | 比例變址定址 |
儲存器 | \(Imm(, r_i, s)\) | \(M[Imm + R[r_i] \cdot s ]\) | 比例變址定址 |
儲存器 | $ (r_b, r_i, s)$ | \(M[R[r_b] + R[r_i] \cdot s ]\) | 比例變址定址 |
儲存器 | $ Imm(r_b, r_i, s)$ | \(M[Imm + R[r_b] + R[r_i] \cdot s ]\) | 比例變址定址 |
資料傳輸指令
指令 | 效果 | 描述 |
---|---|---|
\(MOV \quad S, D\) | \(D \leftarrow S\) | 傳送 |
\(movabsq \quad I, R\) | \(R \leftarrow I\) | 傳送絕對的四字 |
\(MOVZ \quad S, R\) | \(R \leftarrow 零擴充套件(S)\) | 以零進行擴充套件進行轉送 |
\(MOVS \quad S, R\) | \(R \leftarrow 符號擴充套件(S)\) | 轉送符號擴充套件的位元組 |
\(movsbw \quad S, R\) | 將符號擴充套件的位元組傳送到字 | |
\(ctlq\) | \(\%rax \leftarrow 符號擴充套件(\%eax)\) | 把 %eax 符號擴充套件到 %rax |
算術和邏輯操作指令
指令 | 效果 | 描述 |
---|---|---|
\(leaq \quad S, D\) | \(D \leftarrow \&S\) | 載入有效地址 |
\(INC \quad D\) | \(D \leftarrow D + 1\) | 加 1 |
\(DEC \quad D\) | \(D \leftarrow D - 1\) | 減 1 |
\(NEG \quad D\) | \(D \leftarrow -D\) | 取負 |
\(NOT \quad D\) | \(D \leftarrow \sim D\) | 取反 |
\(ADD \quad S, D\) | \(D \leftarrow D + S\) | 加 |
\(SUB \quad S, D\) | \(D \leftarrow D - S\) | 減 |
\(IMUL \quad S, D\) | \(D \leftarrow D * S\) | 乘 |
\(XOR \quad S, D\) | \(D \leftarrow D\) ^ \(S\) | 異或 |
\(OR \quad S, D\) | \(D \leftarrow D \mid S\) | 或 |
\(AND \quad S, D\) | \(D \leftarrow D \& S\) | 與 |
\(SAL \quad k, D\) | \(D \leftarrow D << k\) | 左移 |
\(SHL \quad k, D\) | \(D \leftarrow D << k\) | 左移, 等同於 SAL |
\(SAR \quad k, D\) | \(D \leftarrow D >>_A k\) | 算術左移(考慮符號) |
\(SHR \quad k, D\) | \(D \leftarrow D >>_L k\) | 邏輯 |
特殊的算術操作
支援兩個 64 位數字的全 128(8字, oct word) 位乘積以及整數除法的指令, 可以看到除法是分步對高低64位操作的
指令 | 效果 | 描述 |
---|---|---|
\(imulq \quad S\) | \(R[\%rdx]:R[\%rax] \leftarrow S \times R[\%rax]\) | 有符號全乘法 |
\(mulq \quad S\) | \(R[\%rdx]:R[\%rax] \leftarrow S \times R[\%rax]\) | 無符號全乘法 |
\(clto \quad S\) | \(R[\%rdx]:R[\%rax] \leftarrow 符號擴充套件R[\%rax]\) | 轉換為8字 |
\(idivq \quad S\) | \(R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] mod S \\ R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] \div S\) | 有符號除法法 |
\(divq \quad S\) | \(R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] mod S \\ R[\%rdx] \leftarrow R[\%rdx]:R[\%rax] \div S\) | 無符號除法法 |
控制
條件碼
- \(CF\): 進位標誌。最近的操作使最高位產生了進位。可用來檢查無符號操作的溢位。
- \(ZF\): 零標誌。最近的操作得出的結果為 0.
- \(SF\): 符號標誌。最近的操作得到的結果為負數
- \(OF\): 溢位標誌。最近的操作導致一個補碼溢位(正溢位或者負溢位)
條件碼會發生改變的操作
比較和測試指令
這兩個系列指令不修改任何暫存器的值,只設定條件碼
指令 | 效果 | 描述 |
---|---|---|
\(CMP \quad S_1, S_2\) | \(S_2-S_1\) | 比較 |
\(TEST \quad S_1, S_2\) | \(S_1 \& S_2\) | 測試 |
訪問條件碼
指令 | 同義名 | 效果 | 描述 |
---|---|---|---|
\(sete \quad D\) | \(setz\) | \(D \leftarrow ZF\) | 相等/零 |
\(setne \quad D\) | \(setnz\) | \(D \leftarrow \sim ZF\) | 不等/非零 |
\(sets \quad D\) | \(D \leftarrow SF\) | 負數 | |
\(setns \quad D\) | \(D \leftarrow \sim SF\) | 負數 | |
\(setg \quad D\) | \(setnle\) | \(D \leftarrow \sim(SF \land OF) \& \sim ZF\) | 大於(有符號>) |
\(setge \quad D\) | \(setnl\) | \(D \leftarrow \sim(SF \land OF)\) | 大於等於(有符號 >=) |
\(setl \quad D\) | \(setnge\) | \(D \leftarrow SF \land OF\) | 小於(有符號<) |
\(setle \quad D\) | \(setng\) | \(D \leftarrow \sim(SF \land OF) \mid ZF\) | 小於等於(有符號 <=) |
\(seta \quad D\) | \(setnbe\) | \(D \leftarrow \sim CF \& \sim ZF\) | 大於(無符號>) |
\(setae \quad D\) | \(setnb\) | \(D \leftarrow \sim CF\) | 大於等於(無符號>=) |
\(setb \quad D\) | \(setnae\) | \(D \leftarrow CF\) | 小於(無符號<) |
\(setbe \quad D\) | \(setna\) | \(D \leftarrow CF \mid ZF\) | 小於等於(無符號<=) |
跳轉指令
訪問條件碼
指令 | 同義名 | 跳轉條件 | 描述 |
---|---|---|---|
\(jmp \quad Label\) | 1 | 直接跳轉 | |
\(jmp \quad *Operand\) | 1 | 間接跳轉 | |
\(je \quad Label\) | \(jz\) | \(ZF\) | 相等/零 |
\(jne \quad Label\) | \(jnz\) | \(\sim ZF\) | 不相等/非零 |
\(js \quad Label\) | \(SF\) | 負數 | |
\(jns \quad Label\) | \(\sim SF\) | 非負數 | |
\(jg \quad D\) | \(jnle\) | \(D \leftarrow \sim(SF \land OF) \& \sim ZF\) | 大於(有符號>) |
\(jge \quad D\) | \(jnl\) | \(D \leftarrow \sim(SF \land OF)\) | 大於等於(有符號 >=) |
\(jl \quad D\) | \(jnge\) | \(D \leftarrow SF \land OF\) | 小於(有符號<) |
\(jle \quad D\) | \(jng\) | \(D \leftarrow \sim(SF \land OF) \mid ZF\) | 小於等於(有符號 <=) |
\(ja \quad D\) | \(jnbe\) | \(D \leftarrow \sim CF \& \sim ZF\) | 大於(無符號>) |
\(jae \quad D\) | \(jnb\) | \(D \leftarrow \sim CF\) | 大於等於(無符號>=) |
\(jb \quad D\) | \(jnae\) | \(D \leftarrow CF\) | 小於(無符號<) |
\(jbe \quad D\) | \(jna\) | \(D \leftarrow CF \mid ZF\) | 小於等於(無符號<=) |
跳轉指令一般將目標指令的地址與緊跟在跳轉指令後面那條指令之間的差作為編碼
有下C程式碼
int foo() {
for (int i = 0; i < 3; i++)
if (i == 1)
return 1;
return 0;
}
反彙編二進位制程式碼
think@pc$ gcc -O0 -c foo.c
think@pc$ objdump -S foo.o
foo.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <foo>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
b: eb 11 jmp 1e <foo+0x1e>
d: 83 7d fc 01 cmpl $0x1,-0x4(%rbp)
11: 75 07 jne 1a <foo+0x1a>
13: b8 01 00 00 00 mov $0x1,%eax
18: eb 0f jmp 29 <foo+0x29>
1a: 83 45 fc 01 addl $0x1,-0x4(%rbp)
1e: 83 7d fc 02 cmpl $0x2,-0x4(%rbp)
22: 7e e9 jle d <foo+0xd>
24: b8 00 00 00 00 mov $0x0,%eax
29: 5d pop %rbp
2a: c3 retq
看第一個跳轉指令在地址 b
, 跳轉的地址為 1e
, 其值為 11 + d
, 這裡比較特殊的是
- 地址是無符號型別
- 相對地址為有符號型別
看記憶體地址為 0x24
的那條 jle
指令, 其跳轉地址為 d = 24(unsigned) + e9(-17,signed)
。與 PC計數器 指向下一條執行的指令的現象相符合,這樣就可以比較輕易的完成連結操作。
過程
對於我們一般的認識就是過程可以理解為函式呼叫。
過程的機器級支援需要處理多種屬性
- 傳遞控制。在程式進入過程\(Q\)時\(PC\)必須設定為\(Q\)的程式碼起始地址,返回時要把\(PC\)設定為\(P\)中呼叫\(Q\)後面的那條指令的地址。
- 傳遞引數。\(P\)必須能夠向\(Q\)提供一個或者多個引數,\(Q\)必須能夠向\(P\)返回一個值。
- 分配和釋放記憶體。在開始時,Q可能需要為區域性變數分配空間,而在返回前,又必須釋放這些分配的記憶體。
棧的彈出和壓入指令
指令 | 效果 | 描述 |
---|---|---|
\(pushq \quad S\) | \(R[\%rsp] \leftarrow R[\%rsp] - 8 \\ M[R[\%rsp]] \leftarrow S\) | 四字入棧 |
\(popq \quad D\) | \(D \leftarrow M[R[\%rsp]] \\ R[\%rsp] \leftarrow R[\%rsp] + 8\) | 四字出棧 |
一個簡單的示例程式碼
// c 程式碼
long three_n_sum(long a1, long a2, long a3) { return a1 + a2 + a3; }
long sum(long a1, long a2, long a3, long a4, long a5, long a6, long a7, long a8) {
long b1 = three_n_sum(a1, a2, a3);
long b2 = three_n_sum(a4, a5, a6);
long b3 = three_n_sum(a7, a8, 0);
long b = b1 + b2 + b3;
return b;
}
int main() { long s = sum(1, 2, 3, 4, 5, 6, 7, 8); }
// 反彙編的二進位制程式碼
0000000000001125 <three_n_sum>:
1125: 55 push %rbp
1126: 48 89 e5 mov %rsp,%rbp
1129: 48 89 7d f8 mov %rdi,-0x8(%rbp)
112d: 48 89 75 f0 mov %rsi,-0x10(%rbp)
1131: 48 89 55 e8 mov %rdx,-0x18(%rbp)
1135: 48 8b 55 f8 mov -0x8(%rbp),%rdx
1139: 48 8b 45 f0 mov -0x10(%rbp),%rax
113d: 48 01 c2 add %rax,%rdx
1140: 48 8b 45 e8 mov -0x18(%rbp),%rax
1144: 48 01 d0 add %rdx,%rax
1147: 5d pop %rbp
1148: c3 retq
0000000000001149 <sum>:
1149: 55 push %rbp
114a: 48 89 e5 mov %rsp,%rbp
114d: 48 83 ec 50 sub $0x50,%rsp
1151: 48 89 7d d8 mov %rdi,-0x28(%rbp)
1155: 48 89 75 d0 mov %rsi,-0x30(%rbp)
1159: 48 89 55 c8 mov %rdx,-0x38(%rbp)
115d: 48 89 4d c0 mov %rcx,-0x40(%rbp)
1161: 4c 89 45 b8 mov %r8,-0x48(%rbp)
1165: 4c 89 4d b0 mov %r9,-0x50(%rbp)
1169: 48 8b 55 c8 mov -0x38(%rbp),%rdx
116d: 48 8b 4d d0 mov -0x30(%rbp),%rcx
1171: 48 8b 45 d8 mov -0x28(%rbp),%rax
1175: 48 89 ce mov %rcx,%rsi
1178: 48 89 c7 mov %rax,%rdi
117b: e8 a5 ff ff ff callq 1125 <three_n_sum>
1180: 48 89 45 f8 mov %rax,-0x8(%rbp)
1184: 48 8b 55 b0 mov -0x50(%rbp),%rdx
1188: 48 8b 4d b8 mov -0x48(%rbp),%rcx
118c: 48 8b 45 c0 mov -0x40(%rbp),%rax
1190: 48 89 ce mov %rcx,%rsi
1193: 48 89 c7 mov %rax,%rdi
1196: e8 8a ff ff ff callq 1125 <three_n_sum>
119b: 48 89 45 f0 mov %rax,-0x10(%rbp)
119f: 48 8b 45 18 mov 0x18(%rbp),%rax
11a3: ba 00 00 00 00 mov $0x0,%edx
11a8: 48 89 c6 mov %rax,%rsi
11ab: 48 8b 7d 10 mov 0x10(%rbp),%rdi
11af: e8 71 ff ff ff callq 1125 <three_n_sum>
11b4: 48 89 45 e8 mov %rax,-0x18(%rbp)
11b8: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11bc: 48 8b 45 f0 mov -0x10(%rbp),%rax
11c0: 48 01 c2 add %rax,%rdx
11c3: 48 8b 45 e8 mov -0x18(%rbp),%rax
11c7: 48 01 d0 add %rdx,%rax
11ca: 48 89 45 e0 mov %rax,-0x20(%rbp)
11ce: 48 8b 45 e0 mov -0x20(%rbp),%rax
11d2: c9 leaveq
11d3: c3 retq
00000000000011d4 <main>:
11d4: 55 push %rbp
11d5: 48 89 e5 mov %rsp,%rbp
11d8: 48 83 ec 10 sub $0x10,%rsp
11dc: 6a 08 pushq $0x8
11de: 6a 07 pushq $0x7
11e0: 41 b9 06 00 00 00 mov $0x6,%r9d
11e6: 41 b8 05 00 00 00 mov $0x5,%r8d
11ec: b9 04 00 00 00 mov $0x4,%ecx
11f1: ba 03 00 00 00 mov $0x3,%edx
11f6: be 02 00 00 00 mov $0x2,%esi
11fb: bf 01 00 00 00 mov $0x1,%edi
1200: e8 44 ff ff ff callq 1149 <sum>
1205: 48 83 c4 10 add $0x10,%rsp
1209: 48 89 45 f8 mov %rax,-0x8(%rbp)
120d: b8 00 00 00 00 mov $0x0,%eax
1212: c9 leaveq
1213: c3 retq
1214: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
121b: 00 00 00
121e: 66 90 xchg %ax,%ax
轉移控制
將控制從函式\(P\)轉移到\(Q\)只需要簡單的把\(PC\)設定為\(Q\)的程式碼起始位置。
程式的返回處理器需要記錄\(P\)的執行的程式碼位置,這個資訊由指令\(call \, Q\)來記錄
call
指令將地址 A 壓入棧中,並將\(PC\)設定為\(Q\)的起始地址,壓入的地址 A 是緊跟在 call 指令後面的那條指令的地址
,被稱為返回地址。
執行時棧
一個函式呼叫棧的變化,用函式 sum()
做示例
- 將 \(\%rbx\) 入棧儲存上一個棧基地址,設定新的棧幀的基地址,分配棧記憶體(sub $0x50,%rsp)
- 將暫存器儲存在棧中,設定引數,最多六個引數儲存在暫存器中(參考main函式)
- 將 \(PC\) 設定為 1149, 壓入call之後的指令地址 1205, 跳轉
- 呼叫子例程則重複以上動作
- 返回則執行
leaveq
等價於movl %ebp %esp
popl %ebp
;ret
彈出返回地址並且跳轉
用一張圖來表示棧的變化, 觀察彙編程式碼地址 119f
和 11ab
, 在第三次呼叫 three_n_sum()
時引數的取值時存在於 main 函式棧幀中,而引數都存在於棧頂位置也利於子例程取值。
上圖是CS:APP中的圖,我用processon畫的,折騰後面上圖2中的棧結構真的是麻煩,書裡的圖文不符,只能根據這個彙編程式碼重畫一下。
後面又在 15213 的課件中找到一張圖,在我自己的圖上棧幀的大小表示上又和課件的圖有出入,拿 sum()
來看,%ebp 入棧後,棧指標繼續分配棧上記憶體,向地址減小 0x50,課件中的棧幀包含了那個 %ebp,而我自己做的圖單獨列出了,這樣看應該是課件中的圖準確點,一個棧幀應該包含當前棧的所有資訊(棧上記憶體 + 棧基地址 + 返回地址),不過processon上的圖麼有了,這裡就插個眼再傳個課件的圖吧。
)
參考
- Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 1: Basic Architecture, Intel 開發手冊
- 15213, 計算機系統導論課程
- CS:APP 3e, 深入理解計算機系統