彙編程式碼的簡單分析
阿新 • • 發佈:2019-02-13
內容說明
本次的內容,是一次 MOOC 課程的作業。具體的,是在 Linux 下對一段簡單的 C 程式碼生成的彙編程式碼進行分析,進而瞭解計算機、CPU 的工作機制。
作業宣告
qianyizhou17 + 原創作品轉載請註明出處 + 《Linux 核心分析》MOOC 課程 http://mooc.study.163.com/course/USTC-1000029000
實驗準備
- 環境
Linux
需要介紹一下的是本次 MOOC 提供的實驗樓的環境,可以直接訪問 Linux 的環境,一系列操作也十分簡單,十分贊! - 原始碼
main.c
- 環境
int func_a(int x)
{
return x + 1000;
}
int func_b(int x)
{
return func_a(x);
}
int main(void)
{
return func_b(8) + 1;
}
- 生成彙編指令
$ gcc –S –o main.s main.c -m32
- 生成如下
.file "main.c"
.text
.globl func_a
.type func_a, @function
func_a:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
movl 8(%ebp), %eax
addl $1000, %eax
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size func_a, .-func_a
.globl func_b
.type func_b, @function
func_b:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call func_a
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size func_b, .-func_b
.globl main
.type main, @function
main:
.LFB2:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $4, %esp
movl $8, (%esp)
call func_b
addl $1, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE2:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits
其中以 “.” 開頭的為輔助資訊,在分析程式碼時可以刪除,整理後的彙編資訊如下:
func_a:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $1000, %eax
popl %ebp
ret
func_b:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call func_a
leave
ret
main:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl $8, (%esp)
call func_b
addl $1, %eax
leave
ret
程式碼分析前提
- 什麼是棧
在我的理解,棧是一片記憶體,用於暫時快取程式的指令。那麼為何不使用普通的連續結構,而使用 “棧” 結構?
利用棧的特性(後入先出),與函式呼叫的性質一致(最近呼叫的最先返回)。
棧底是高地址:即,逐漸壓棧,會使得訪問的棧地址逐漸變小,可能發生錯誤——棧溢位。
我們使用 ebp 來標識棧底,esp 標識當前的棧指標。 - 必要的彙編說明
- push
pushl X:
subl $4, %esp
movl X, (%esp) - pop
popl Y:
movl (%esp), Y
addl %4, %esp - call
call ADDR
pushl %eip(*)
movl ADDR, %eip(*) - enter
pushl %ebp
movl %esp, %ebp - leave
movl %ebp, %esp
popl %ebp - ret
popl %eip(*)
- push
- 必要的暫存器說明
- ebp
標識棧底 - esp
標識當前的棧指標 - eip
指令暫存器:儲存當前執行的指令地址 - eax
一般的,預設用於儲存函式返回值
- ebp
- 什麼是棧
程式碼分析
首先程式碼從 17 main 開始執行
執行 18、19 行,即執行 call 指令,儲存上一次的棧資訊:將上次的棧底入棧,並將棧底、棧指標重新指向下一地址,完成當前函式棧的初始化:
執行前
| |
ebp -> --------- 5000
. | |
.
.
| |
esp -> --------- 4000
執行後
| |
--------- 5000
. | |
.
.
| |
--------- 4000
| ebp 5000|
esp/ebp-> --------- 3996
執行 20、21 行,將立即數 8 儲存到 esp 所指向的空間中
| |
--------- 5000
. | |
.
.
| |
--------- 4000
| ebp 5000|
ebp-> --------- 3996
| 8 |
esp-> --------- 3992
執行 22 行:call func_b
| |
--------- 5000
. | |
.
.
| |
--------- 4000
| ebp 5000|
ebp-> --------- 3996
| 8 |
--------- 3992
| eip 23 |
esp-> --------- 3988
同時 eip 指向了 func_b 的地址:8。繼續執行時,將從 func_b 的下一個地址 9 開始執行。
執行第 9、10 行後:
| |
--------- 5000
. | |
.
.
| |
--------- 4000
| ebp 5000|
--------- 3996
| 8 |
--------- 3992
| eip 23 |
--------- 3988
| ebp 3996|
ebp/esp-> --------- 3984
以此類推,呼叫 call func_a,執行 func_a,第 5 行 addl $1000, %eax 執行後,eax 中儲存的值變為 1008。
執行 6 行,popl %ebp
執行前
eip 1
eax 1008
| |
--------- 5000
. | |
.
.
| |
--------- 4000
| ebp 5000|
--------- 3996
| 8 |
--------- 3992
| eip 23 |
--------- 3988
| ebp 3996|
--------- 3984
| 8 |
--------- 3980
| eip 15 |
--------- 3976
| ebp 3984|
ebp/esp-> --------- 3972
執行 popl %ebp 後
eip 1
eax 1008
| |
--------- 5000
. | |
.
.
| |
--------- 4000
| ebp 5000|
--------- 3996
| 8 |
--------- 3992
| eip 23 |
--------- 3988
| ebp 3996|
ebp -> --------- 3984
| 8 |
--------- 3980
| eip 15 |
esp -> --------- 3976
| ebp 3984|
--------- 3972
執行 ret 之後
eip 15
eax 1008
| |
--------- 5000
. | |
.
.
| |
--------- 4000
| ebp 5000|
--------- 3996
| 8 |
--------- 3992
| eip 23 |
--------- 3988
| ebp 3996|
ebp -> --------- 3984
| 8 |
esp -> --------- 3980
| eip 15 |
--------- 3976
| ebp 3984|
--------- 3972
注意,此時 eip 的值為 15,因此繼續執行的指令為 func_b 的 leave 指令
執行後
eip 15
eax 1008
| |
--------- 5000
. | |
.
.
| |
--------- 4000
| ebp 5000|
ebp -> --------- 3996
| 8 |
--------- 3992
| eip 23 |
esp -> --------- 3988
| ebp 3996|
--------- 3984
| 8 |
--------- 3980
| eip 15 |
--------- 3976
| ebp 3984|
--------- 3972
繼續執行 ret 後
eip 23
eax 1008
| |
--------- 5000
. | |
.
.
| |
--------- 4000
| ebp 5000|
ebp -> --------- 3996
| 8 |
esp -> --------- 3992
| eip 23 |
--------- 3988
| ebp 3996|
--------- 3984
| 8 |
--------- 3980
| eip 15 |
--------- 3976
| ebp 3984|
--------- 3972
addl $1, %eax 之後,eax 中儲存 1009。
繼續執行 leave,將當前的棧資訊恢復。
before leave after leave, before ret
eip 23 eip 23
eax 1009 eax 1009
| | | |
--------- 5000 ebp -> --------- 5000
. | | . | |
. .
. .
| | | |
--------- 4000 esp -> --------- 4000
| ebp 5000| | ebp 5000|
ebp -> --------- 3996 --------- 3996
| 8 | | 8 |
esp -> --------- 3992 --------- 3992
| eip 23 | | eip 23 |
--------- 3988 --------- 3988
| ebp 3996| | ebp 3996|
--------- 3984 --------- 3984
| 8 | | 8 |
--------- 3980 --------- 3980
| eip 15 | | eip 15 |
--------- 3976 --------- 3976
| ebp 3984| | ebp 3984|
--------- 3972 --------- 3972
呼叫 ret,恢復 eip:將 esp 所指向的指令值寫入 eip,main 函式結束。
- 總結
- 進入函式時,一般首先
pushl %ebp; movl %esp, %ebp
來進行棧資訊的儲存,包括 1)將上次的棧底資訊壓入棧; 2)將棧底、棧指標重置為下一個棧地址。 - 呼叫函式時,將 eip 的當前資訊壓入棧中,並更新 eip 使指向新的指令。
- 函式返回的過程,即是從棧中讀取 eip、ebp 的舊有資訊並恢復上一個函式棧的過程
- 其中,計算機可以使用機械的邏輯(依據 eip 讀取下一條指令,並逐條eip所指向指令即可,周而復始),而通過精緻的資料結構(棧)和基礎的CPU指令,便能夠實現複雜的邏輯!
- 函式執行、並呼叫新的函式時,其棧資訊基本如下:
- 進入函式時,一般首先
--------- 4000
| ebp DATA|
--------- XXXX
| |
| |
| |
--------- XXXX
| eip DATA|
--------- 39XX