(一)
1.ctrl+x 打開IDA的交叉引用窗口,查看當前語句是從哪一個語句跳轉過來的。
call fun() 調用函數fun() call語句會先把call的下一條語句入棧,然後jmp到call函數的位置,下條語句的這個地址就稱爲返回地址。
2. OD
F8 執行下條語句 call語句的話F8是直接執行CALL的內容
F7是跟進去,進入CALL裏看這個CALL 的具體代碼
F9 執行到斷點位置
棧的話,從棧底到棧頂是高地址到低地址 ,棧中保存函數額返回地址後,還會保存父函數的ebp的地址,然後爲局部變量分配空間 (debug格式下分配 空間會大於實際需要的大小)。分配完之後,局部變量空間的值會全部變成CCCCC,程序的容錯性保持自身的健壯性,使用CCC填充滿這個區域。int 三斷點(INT3斷點是斷點的一種,在諸如Ollydbg中的快捷鍵是F2,是一種很常用的斷點類型。) strcpy(name,"hello") 先入棧他的右邊的參數,再入棧左邊的參數,
name入棧存的是分配的局部變量區的首址。
我是初學小白,有什麼地方寫錯了還請各位指正~~~持續更新中...........
(二)
1.EIP寄存器裏存儲的是CPU下次要執行的指令的地址。
2.EBP寄存器裏存儲的是是棧的棧底指針,通常叫棧基址,這個是一開始進行fun()函數調用之前,由ESP傳遞給EBP的。(在函數調用前你可以這麼理解:ESP存儲的是棧頂地址,也是棧底地址。)
3.ESP寄存器裏存儲的是在調用函數fun()之後,棧的棧頂。並且始終指向棧頂。 堆棧是一種簡單的數據結構,是一種只允許在其一端進行插入或刪除的線性表。
允許插入或刪除操作的一端稱爲棧頂,另一端稱爲棧底,對堆棧的插入和刪除操作被稱入棧和出棧。
有一組CPU指令可以實現對進程的內存實現堆棧訪問。其中,POP指令實現出棧操作,PUSH指令實現入棧操作。
CPU的ESP寄存器存放當前線程的棧頂指針,
EBP寄存器中保存當前線程的棧底指針。
CPU的EIP寄存器存放下一個CPU指令存放的內存地址,當CPU執行完當前的指令後,從EIP寄存器中讀取下一條指令的內存地址,然後繼續執行。
EIP寄存器裏存儲的是CPU下次要執行的指令的地址。
也就是調用完fun函數後,讓CPU知道應該執行main函數中的printf("函數調用結束")語句了。
2.EBP寄存器裏存儲的是是棧的棧底指針,通常叫棧基址,這個是一開始進行fun()函數調用之前,由ESP傳遞給EBP的。(在函數調用前你可以這麼理解:ESP存儲的是棧頂地址,也是棧底地址。)
3.ESP寄存器裏存儲的是在調用函數fun()之後,棧的棧頂。並且始終指向棧頂。 堆棧是一種簡單的數據結構,是一種只允許在其一端進行插入或刪除的線性表。
允許插入或刪除操作的一端稱爲棧頂,另一端稱爲棧底,對堆棧的插入和刪除操作被稱入棧和出棧。
有一組CPU指令可以實現對進程的內存實現堆棧訪問。其中,POP指令實現出棧操作,PUSH指令實現入棧操作。
CPU的ESP寄存器存放當前線程的棧頂指針,
EBP寄存器中保存當前線程的棧底指針。
CPU的EIP寄存器存放下一個CPU指令存放的內存地址,當CPU執行完當前的指令後,從EIP寄存器中讀取下一條指令的內存地址,然後繼續執行。 esp和ebp區別
問題: push ebp
(轉)gdb反彙編小結
如果在Linux平臺可以用gdb進行反彙編和調試。(轉)
2. 最簡C代碼分析
爲簡化問題,來分析一下最簡的c代碼生成的彙編代碼:
# vi test1.c
int main()
{
return 0;
}
編譯該程序,產生二進制文件:
# gcc test1.c -o test1
# file test1
test1: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped
test1是一個ELF格式32位小端(Little Endian)的可執行文件,動態鏈接並且符號表沒有去除。
這正是Unix/Linux平臺典型的可執行文件格式。
用mdb反彙編可以觀察生成的彙編代碼:
# mdb test1
Loading modules: [ libc.so.1 ]
> main::dis ; 反彙編main函數,mdb的命令一般格式爲 <地址>::dis
main: pushl %ebp ; ebp寄存器內容壓棧,即保存main函數的上級調用函數的棧基地址
main+1: movl %esp,%ebp ; esp值賦給ebp,設置main函數的棧基址
main+3: subl $8,%esp
main+6: andl $0xf0,%esp
main+9: movl $0,%eax
main+0xe: subl %eax,%esp
main+0x10: movl $0,%eax ; 設置函數返回值0
main+0x15: leave ; 將ebp值賦給esp,pop先前棧內的上級函數棧的基地址給ebp,恢復原棧基址
main+0x16: ret ; main函數返回,回到上級調用
>
注:這裏得到的彙編語言語法格式與Intel的手冊有很大不同,Unix/Linux採用AT&T彙編格式作爲彙編語言的語法格式
如果想了解AT&T彙編可以參考文章:Linux AT&T 彙編語言開發指南
問題:誰調用了 main函數?
在C語言的層面來看,main函數是一個程序的起始入口點,而實際上,ELF可執行文件的入口點並不是main而是_start。
mdb也可以反彙編_start:
> _start::dis ;從_start 的地址開始反彙編
_start: pushl $0
_start+2: pushl $0
_start+4: movl %esp,%ebp
_start+6: pushl %edx
_start+7: movl $0x80504b0,%eax
_start+0xc: testl %eax,%eax
_start+0xe: je +0xf <_start+0x1d>
_start+0x10: pushl $0x80504b0
_start+0x15: call -0x75 <atexit>
_start+0x1a: addl $4,%esp
_start+0x1d: movl $0x8060710,%eax
_start+0x22: testl %eax,%eax
_start+0x24: je +7 <_start+0x2b>
_start+0x26: call -0x86 <atexit>
_start+0x2b: pushl $0x80506cd
_start+0x30: call -0x90 <atexit>
_start+0x35: movl +8(%ebp),%eax
_start+0x38: leal +0x10(%ebp,%eax,4),%edx
_start+0x3c: movl %edx,0x8060804
_start+0x42: andl $0xf0,%esp
_start+0x45: subl $4,%esp
_start+0x48: pushl %edx
_start+0x49: leal +0xc(%ebp),%edx
_start+0x4c: pushl %edx
_start+0x4d: pushl %eax
_start+0x4e: call +0x152 <_init>
_start+0x53: call -0xa3 <__fpstart>
_start+0x58: call +0xfb <main> ;在這裏調用了main函數
_start+0x5d: addl $0xc,%esp
_start+0x60: pushl %eax
_start+0x61: call -0xa1 <exit>
_start+0x66: pushl $0
_start+0x68: movl $1,%eax
_start+0x6d: lcall $7,$0
_start+0x74: hlt
>
問題:爲什麼用EAX寄存器保存函數返回值?
實際上IA32並沒有規定用哪個寄存器來保存返回值。但如果反彙編Solaris/Linux的二進制文件,就會發現,都用EAX保存函數返回值。
這不是偶然現象,是操作系統的ABI(Application Binary Interface)來決定的。
Solaris/Linux操作系統的ABI就是Sytem V ABI。
概念:SFP (Stack Frame Pointer) 棧框架指針
正確理解SFP必須瞭解:
IA32 的棧的概念
CPU 中32位寄存器ESP/EBP的作用
PUSH/POP 指令是如何影響棧的
CALL/RET/LEAVE 等指令是如何影響棧的
如我們所知:
1)IA32的棧是用來存放臨時數據,而且是LIFO,即後進先出的。棧的增長方向是從高地址向低地址增長,按字節爲單位編址。
2) EBP是棧基址的指針,永遠指向棧底(高地址),ESP是棧指針,永遠指向棧頂(低地址)。
3) PUSH一個long型數據時,以字節爲單位將數據壓入棧,從高到低按字節依次將數據存入ESP-1、ESP-2、ESP-3、ESP-4的地址單元。
4) POP一個long型數據,過程與PUSH相反,依次將ESP-4、ESP-3、ESP-2、ESP-1從棧內彈出,放入一個32位寄存器。
5) CALL指令用來調用一個函數或過程,此時,下一條指令地址會被壓入堆棧,以備返回時能恢復執行下條指令。
6) RET指令用來從一個函數或過程返回,之前CALL保存的下條指令地址會從棧內彈出到EIP寄存器中,程序轉到CALL之前下條指令處執行
7) ENTER是建立當前函數的棧框架,即相當於以下兩條指令:
pushl %ebp
movl %esp,%ebp
8) LEAVE是釋放當前函數或者過程的棧框架,即相當於以下兩條指令:
movl ebp esp
popl ebp
如果反彙編一個函數,很多時候會在函數進入和返回處,發現有類似如下形式的彙編語句:
pushl %ebp ; ebp寄存器內容壓棧,即保存main函數的上級調用函數的棧基地址
movl %esp,%ebp ; esp值賦給ebp,設置 main函數的棧基址
........... ; 以上兩條指令相當於 enter 0,0
...........
leave ; 將ebp值賦給esp,pop先前棧內的上級函數棧的基地址給ebp,恢復原棧基址
ret ; main函數返回,回到上級調用
這些語句就是用來創建和釋放一個函數或者過程的棧框架的。
原來編譯器會自動在函數入口和出口處插入創建和釋放棧框架的語句。
函數被調用時:
1) EIP/EBP成爲新函數棧的邊界
函數被調用時,返回時的EIP首先被壓入堆棧;創建棧框架時,上級函數棧的EBP被壓入堆棧,與EIP一道行成新函數棧框架的邊界
2) EBP成爲棧框架指針SFP,用來指示新函數棧的邊界
棧框架建立後,EBP指向的棧的內容就是上一級函數棧的EBP,可以想象,通過EBP就可以把層層調用函數的棧都回朔遍歷一遍,調試器就是利用這個特性實現 backtrace功能的
3) ESP總是作爲棧指針指向棧頂,用來分配棧空間
棧分配空間給函數局部變量時的語句通常就是給ESP減去一個常數值,例如,分配一個整型數據就是 ESP-4
4) 函數的參數傳遞和局部變量訪問可以通過SFP即EBP來實現
由於棧框架指針永遠指向當前函數的棧基地址,參數和局部變量訪問通常爲如下形式:
+8+xx(%ebp) ; 函數入口參數的的訪問
-xx(%ebp) ; 函數局部變量訪問
假如函數A調用函數B,函數B調用函數C ,則函數棧框架及調用關係如下圖所示:
+-------------------------+----> 高地址 | EIP (上級函數返回地址) | +-------------------------+ +--> | EBP (上級函數的EBP) | --+ <------當前函數A的EBP (即SFP框架指針) | +-------------------------+ +-->偏移量A | | Local Variables | | | | .......... | --+ <------ESP指向函數A新分配的局部變量,局部變量可以通過A的ebp-偏移量A訪問 | f +-------------------------+ | r | Arg n(函數B的第n個參數) | | a +-------------------------+ | m | Arg .(函數B的第.個參數) | | e +-------------------------+ | | Arg 1(函數B的第1個參數) | | o +-------------------------+ | f | Arg 0(函數B的第0個參數) | --+ <------ B函數的參數可以由B的ebp+偏移量B訪問 | +-------------------------+ +--> 偏移量B | A | EIP (A函數的返回地址) | | | +-------------------------+ --+ +--- | EBP (A函數的EBP) |<--+ <------ 當前函數B的EBP (即SFP框架指針) +-------------------------+ | | Local Variables | | | .......... | | <------ ESP指向函數B新分配的局部變量 +-------------------------+ | | Arg n(函數C的第n個參數) | | +-------------------------+ | | Arg .(函數C的第.個參數) | | +-------------------------+ +--> frame of B | Arg 1(函數C的第1個參數) | | +-------------------------+ | | Arg 0(函數C的第0個參數) | | +-------------------------+ | | EIP (B函數的返回地址) | | +-------------------------+ | +--> | EBP (B函數的EBP) | --+ <------ 當前函數C的EBP (即SFP框架指針) | +-------------------------+ | | Local Variables | | | .......... | <------ ESP指向函數C新分配的局部變量 | +-------------------------+----> 低地址 frame of C 圖 1-1
再分析test1反彙編結果中剩餘部分語句的含義:
# mdb test1
Loading modules: [ libc.so.1 ]
> main::dis ; 反彙編main函數
main: pushl %ebp
main+1: movl %esp,%ebp ; 創建Stack Frame(棧框架)
main+3: subl $8,%esp ; 通過ESP-8來分配8字節堆棧空間
main+6: andl $0xf0,%esp ; 使棧地址16字節對齊
main+9: movl $0,%eax ; 無意義
main+0xe: subl %eax,%esp ; 無意義
main+0x10: movl $0,%eax ; 設置main函數返回值
main+0x15: leave ; 撤銷Stack Frame(棧框架)
main+0x16: ret ; main 函數返回
>
以下兩句似乎是沒有意義的,果真是這樣嗎?
movl $0,%eax
subl %eax,%esp
用gcc的O2級優化來重新編譯test1.c:
# gcc -O2 test1.c -o test1
# mdb test1
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: subl $8,%esp
main+6: andl $0xf0,%esp
main+9: xorl %eax,%eax ; 設置main返回值,使用xorl異或指令來使eax爲0
main+0xb: leave
main+0xc: ret
>
新的反彙編結果比最初的結果要簡潔一些,果然之前被認爲無用的語句被優化掉了,進一步驗證了之前的猜測。
提示:編譯器產生的某些語句可能在程序實際語義上沒有用處,可以用優化選項去掉這些語句。
問題:爲什麼用xorl來設置eax的值?
注意到優化後的代碼中,eax返回值的設置由 movl $0,%eax 變爲 xorl %eax,%eax ,這是因爲IA32指令中,xorl比movl有更高的運行速度。
概念:Stack aligned 棧對齊
那麼,以下語句到底是和作用呢?
subl $8,%esp
andl $0xf0,%esp ; 通過andl使低4位爲0,保證棧地址16字節對齊
表面來看,這條語句最直接的後果是使ESP的地址後4位爲0,即16字節對齊,那麼爲什麼這麼做呢?
原來,IA32 系列CPU的一些指令分別在4、8、16字節對齊時會有更快的運行速度,因此gcc編譯器爲提高生成代碼在IA32上的運行速度,默認對產生的代碼進行16字節對齊
andl $0xf0,%esp 的意義很明顯,那麼 subl $8,%esp 呢,是必須的嗎?
這裏假設在進入main函數之前,棧是16字節對齊的話,那麼,進入main函數後,EIP和EBP被壓入堆棧後,棧地址最末4位二進制位必定是1000,esp -8則恰好使後4位地址二進制位爲0000。看來,這也是爲保證棧16字節對齊的。
如果查一下gcc的手冊,就會發現關於棧對齊的參數設置:
-mpreferred-stack-boundary=n ; 希望棧按照2的n次的字節邊界對齊, n的取值範圍是2-12
默認情況下,n是等於4的,也就是說,默認情況下,gcc是16字節對齊,以適應IA32大多數指令的要求。
讓我們利用-mpreferred-stack-boundary=2來去除棧對齊指令:
# gcc -mpreferred-stack-boundary=2 test1.c -o test1
> main::dis
main: pushl %ebp
main+1: movl %esp,%ebp
main+3: movl $0,%eax
main+8: leave
main+9: ret
>
可以看到,棧對齊指令沒有了,因爲,IA32的棧本身就是4字節對齊的,不需要用額外指令進行對齊。
那麼,棧框架指針SFP是不是必須的呢?
# gcc -mpreferred-stack-boundary=2 -fomit-frame-pointer test1.c -o test
> main::dis
main: movl $0,%eax
main+5: ret
>
由此可知,-fomit-frame-pointer 可以去除SFP。
問題:去除SFP後有什麼缺點呢?
1)增加調式難度
由於SFP在調試器backtrace的指令中被使用到,因此沒有SFP該調試指令就無法使用。
2)降低彙編代碼可讀性
函數參數和局部變量的訪問,在沒有ebp的情況下,都只能通過+xx(esp)的方式訪問,而很難區分兩種方式,降低了程序的可讀性。
問題:去除SFP有什麼優點呢?
1)節省棧空間
2)減少建立和撤銷棧框架的指令後,簡化了代碼
3)使ebp空閒出來,使之作爲通用寄存器使用,增加通用寄存器的數量
4)以上3點使得程序運行速度更快
概念:Calling Convention 調用約定和 ABI (Application Binary Interface) 應用程序二進制接口
函數如何找到它的參數?
函數如何返回結果?
函數在哪裏存放局部變量?
那一個硬件寄存器是起始空間?
那一個硬件寄存器必須預先保留?
Calling Convention 調用約定對以上問題作出了規定。Calling Convention也是ABI的一部分。
因此,遵守相同ABI規範的操作系統,使其相互間實現二進制代碼的互操作成爲了可能。
例如:由於Solaris、Linux都遵守System V的ABI,Solaris 10就提供了直接運行Linux二進制程序的功能。
詳見文章:關注: Solaris 10的10大新變化
3. 小結
本文通過最簡的C程序,引入以下概念:
SFP 棧框架指針
Stack aligned 棧對齊
Calling Convention 調用約定 和 ABI (Application Binary Interface) 應用程序二進制接口
今後,將通過進一步的實驗,來深入瞭解這些概念。通過掌握這些概念,使在彙編級調試程序產生的core dump、掌握C語言高級調試技巧成爲了可能。