程序的機器級表示
時隔一年把 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
, 這里比較特殊的是
- 地址是無符號類型
- 相對地址為有符號類型
看內存地址為 0x22
的那條 jle
指令, 其跳轉地址為 d = 0x24(unsigned) + 0xe9(-0x17,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, 深入理解計算機系統