C语言函数调用栈侦
2. 最簡C代碼分析
??? 為簡化問題,來分析一下最簡的c代碼生成的匯編代碼:
??? # vi test1.c
? ? ??
??? int main()
??? {
?? ?? ? return 0;
??? }???
????
??? 編譯該程序,產(chǎn)生二進(jìn)制文件:
??? # 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)的可執(zhí)行文件,動態(tài)鏈接并且符號表沒有去除。
??? 這正是Unix/Linux平臺典型的可執(zhí)行文件格式。
??? 用mdb反匯編可以觀察生成的匯編代碼:
??? # mdb test1
??? Loading modules: [ libc.so.1 ]
??? > main::dis??? ??? ??? ??? ??? ?? ; 反匯編main函數(shù),mdb的命令一般格式為? <地址>::dis
??? main:??? ??? ? pushl?? %ebp?? ??? ; ebp寄存器內(nèi)容壓棧,即保存main函數(shù)的上級調(diào)用函數(shù)的?;刂?/span>
??? main+1:??? ??? movl??? %esp,%ebp? ; esp值賦給ebp,設(shè)置main函數(shù)的?;?/span>
??? 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??? ; 設(shè)置函數(shù)返回值0
??? main+0x15:???? leave????????????? ; 將ebp值賦給esp,pop先前棧內(nèi)的上級函數(shù)棧的基地址給ebp,恢復(fù)原棧基址
??? main+0x16:???? ret??????????????? ; main函數(shù)返回,回到上級調(diào)用
??? >?
??? 注:這里得到的匯編語言語法格式與Intel的手冊有很大不同,Unix/Linux采用AT&T匯編格式作為匯編語言的語法格式
??? ???? 如果想了解AT&T匯編可以參考文章:Linux AT&T 匯編語言開發(fā)指南?
??? 問題:誰調(diào)用了 main函數(shù)?
?????
???? 在C語言的層面來看,main函數(shù)是一個程序的起始入口點,而實際上,ELF可執(zhí)行文件的入口點并不是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>????????????? ;在這里調(diào)用了main函數(shù)
??? _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寄存器保存函數(shù)返回值?
??? 實際上IA32并沒有規(guī)定用哪個寄存器來保存返回值。但如果反匯編Solaris/Linux的二進(jìn)制文件,就會發(fā)現(xiàn),都用EAX保存函數(shù)返回值。
??? 這不是偶然現(xiàn)象,是操作系統(tǒng)的ABI(Application Binary Interface)來決定的。
??? Solaris/Linux操作系統(tǒng)的ABI就是Sytem V ABI。
??? 概念:SFP (Stack Frame Pointer) 棧框架指針?
??? 正確理解SFP必須了解:
??? ??? IA32 的棧的概念
??? ??? CPU 中32位寄存器ESP/EBP的作用
??? ??? PUSH/POP 指令是如何影響棧的
??? ??? CALL/RET/LEAVE 等指令是如何影響棧的
??? 如我們所知:
??? 1)IA32的棧是用來存放臨時數(shù)據(jù),而且是LIFO,即后進(jìn)先出的。棧的增長方向是從高地址向低地址增長,按字節(jié)為單位編址。
??? 2) EBP是?;返闹羔?#xff0c;永遠(yuǎn)指向棧底(高地址),ESP是棧指針,永遠(yuǎn)指向棧頂(低地址)。
??? 3) PUSH一個long型數(shù)據(jù)時,以字節(jié)為單位將數(shù)據(jù)壓入棧,從高到低按字節(jié)依次將數(shù)據(jù)存入ESP-1、ESP-2、ESP-3、ESP-4的地址單元。
??? 4) POP一個long型數(shù)據(jù),過程與PUSH相反,依次將ESP-4、ESP-3、ESP-2、ESP-1從棧內(nèi)彈出,放入一個32位寄存器。
??? 5) CALL指令用來調(diào)用一個函數(shù)或過程,此時,下一條指令地址會被壓入堆棧,以備返回時能恢復(fù)執(zhí)行下條指令。
??? 6) RET指令用來從一個函數(shù)或過程返回,之前CALL保存的下條指令地址會從棧內(nèi)彈出到EIP寄存器中,程序轉(zhuǎn)到CALL之前下條指令處執(zhí)行
??? 7) ENTER是建立當(dāng)前函數(shù)的棧框架,即相當(dāng)于以下兩條指令:
??? ??? pushl?? %ebp
??? ??? movl??? %esp,%ebp
??? 8) LEAVE是釋放當(dāng)前函數(shù)或者過程的??蚣?#xff0c;即相當(dāng)于以下兩條指令:
??? ??? movl ebp esp
??? ??? popl? ebp
??? 如果反匯編一個函數(shù),很多時候會在函數(shù)進(jìn)入和返回處,發(fā)現(xiàn)有類似如下形式的匯編語句:?
????????
??? ??? pushl?? %ebp??? ??? ??? ; ebp寄存器內(nèi)容壓棧,即保存main函數(shù)的上級調(diào)用函數(shù)的?;刂?br /> ??? ??? movl??? %esp,%ebp??? ?? ; esp值賦給ebp,設(shè)置 main函數(shù)的?;?br /> ??? ??? ...........??? ??? ???? ; 以上兩條指令相當(dāng)于 enter 0,0
??? ??? ...........
??? ??? leave??? ??? ??? ??? ?? ; 將ebp值賦給esp,pop先前棧內(nèi)的上級函數(shù)棧的基地址給ebp,恢復(fù)原?;?br /> ??? ??? ret???????????????????? ; main函數(shù)返回,回到上級調(diào)用
??? 這些語句就是用來創(chuàng)建和釋放一個函數(shù)或者過程的??蚣艿?。
??? 原來編譯器會自動在函數(shù)入口和出口處插入創(chuàng)建和釋放棧框架的語句。
??? 函數(shù)被調(diào)用時:
??? 1) EIP/EBP成為新函數(shù)棧的邊界
??? 函數(shù)被調(diào)用時,返回時的EIP首先被壓入堆棧;創(chuàng)建棧框架時,上級函數(shù)棧的EBP被壓入堆棧,與EIP一道行成新函數(shù)棧框架的邊界
??? 2) EBP成為??蚣苤羔楽FP,用來指示新函數(shù)棧的邊界
??? ??蚣芙⒑?#xff0c;EBP指向的棧的內(nèi)容就是上一級函數(shù)棧的EBP,可以想象,通過EBP就可以把層層調(diào)用函數(shù)的棧都回朔遍歷一遍,調(diào)試器就是利用這個特性實現(xiàn) backtrace功能的
??? 3) ESP總是作為棧指針指向棧頂,用來分配??臻g
??? 棧分配空間給函數(shù)局部變量時的語句通常就是給ESP減去一個常數(shù)值,例如,分配一個整型數(shù)據(jù)就是 ESP-4
??? 4) 函數(shù)的參數(shù)傳遞和局部變量訪問可以通過SFP即EBP來實現(xiàn)?
??? 由于??蚣苤羔樣肋h(yuǎn)指向當(dāng)前函數(shù)的?;刂?#xff0c;參數(shù)和局部變量訪問通常為如下形式:
??? ??? +8+xx(%ebp)?? ?? ?? ; 函數(shù)入口參數(shù)的的訪問
??? ??? -xx(%ebp)??? ??? ?? ; 函數(shù)局部變量訪問
????????????
??? 假如函數(shù)A調(diào)用函數(shù)B,函數(shù)B調(diào)用函數(shù)C ,則函數(shù)??蚣芗罢{(diào)用關(guān)系如下圖所示:
+-------------------------+----> 高地址| EIP (上級函數(shù)返回地址) | +-------------------------+ +--> | EBP (上級函數(shù)的EBP) | --+ <------當(dāng)前函數(shù)A的EBP (即SFP框架指針) | +-------------------------+ +-->偏移量A | | Local Variables | || | .......... | --+ <------ESP指向函數(shù)A新分配的局部變量,局部變量可以通過A的ebp-偏移量A訪問 | f +-------------------------+| r | Arg n(函數(shù)B的第n個參數(shù)) | | a +-------------------------+| m | Arg .(函數(shù)B的第.個參數(shù)) || e +-------------------------+| | Arg 1(函數(shù)B的第1個參數(shù)) || o +-------------------------+| f | Arg 0(函數(shù)B的第0個參數(shù)) | --+ <------ B函數(shù)的參數(shù)可以由B的ebp+偏移量B訪問| +-------------------------+ +--> 偏移量B| A | EIP (A函數(shù)的返回地址) | | | +-------------------------+ --+ +--- | EBP (A函數(shù)的EBP) |<--+ <------ 當(dāng)前函數(shù)B的EBP (即SFP框架指針) +-------------------------+ || Local Variables | || .......... | | <------ ESP指向函數(shù)B新分配的局部變量+-------------------------+ || Arg n(函數(shù)C的第n個參數(shù)) | |+-------------------------+ || Arg .(函數(shù)C的第.個參數(shù)) | |+-------------------------+ +--> frame of B| Arg 1(函數(shù)C的第1個參數(shù)) | |+-------------------------+ || Arg 0(函數(shù)C的第0個參數(shù)) | |+-------------------------+ || EIP (B函數(shù)的返回地址) | |+-------------------------+ |+--> | EBP (B函數(shù)的EBP) | --+ <------ 當(dāng)前函數(shù)C的EBP (即SFP框架指針) | +-------------------------+| | Local Variables || | .......... | <------ ESP指向函數(shù)C新分配的局部變量| +-------------------------+----> 低地址 frame of C圖 1-1 ???????
??? 再分析test1反匯編結(jié)果中剩余部分語句的含義:
????????
??? # mdb test1
??? Loading modules: [ libc.so.1 ]
??? > main::dis??????????????????????? ; 反匯編main函數(shù)
??? main:??? ????? pushl?? %ebp????????????????????????????
??? main+1:??? ??? movl??? %esp,%ebp??????? ; 創(chuàng)建Stack Frame(??蚣?
??? main+3:??? ?? subl??? $8,%esp?????? ; 通過ESP-8來分配8字節(jié)堆??臻g
??? main+6:??? ?? andl??? $0xf0,%esp??? ; 使棧地址16字節(jié)對齊
??? main+9:??? ?? movl??? $0,%eax?????? ; 無意義
??? main+0xe:???? subl??? %eax,%esp???? ; 無意義
??? main+0x10:???? movl??? $0,%eax????????? ; 設(shè)置main函數(shù)返回值
??? main+0x15:???? leave????????????????? ? ; 撤銷Stack Frame(棧框架)
??? main+0x16:???? ret????????????????????? ; main 函數(shù)返回
??? >
??? 以下兩句似乎是沒有意義的,果真是這樣嗎?
??? ??? movl??? $0,%eax?
??? ??? subl???? %eax,%esp
???????
??? 用gcc的O2級優(yōu)化來重新編譯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????? ; 設(shè)置main返回值,使用xorl異或指令來使eax為0
??? main+0xb:???? leave
??? main+0xc:???? ret
??? >?
??? 新的反匯編結(jié)果比最初的結(jié)果要簡潔一些,果然之前被認(rèn)為無用的語句被優(yōu)化掉了,進(jìn)一步驗證了之前的猜測。
??? 提示:編譯器產(chǎn)生的某些語句可能在程序?qū)嶋H語義上沒有用處,可以用優(yōu)化選項去掉這些語句。
??? 問題:為什么用xorl來設(shè)置eax的值?
??? 注意到優(yōu)化后的代碼中,eax返回值的設(shè)置由 movl $0,%eax 變?yōu)?xorl %eax,%eax ,這是因為IA32指令中,xorl比movl有更高的運(yùn)行速度。
??? 概念:Stack aligned 棧對齊
??? 那么,以下語句到底是和作用呢?
??? ??? subl??? $8,%esp
?????? andl??? $0xf0,%esp???? ; 通過andl使低4位為0,保證棧地址16字節(jié)對齊
???????
??? 表面來看,這條語句最直接的后果是使ESP的地址后4位為0,即16字節(jié)對齊,那么為什么這么做呢?
??? 原來,IA32 系列CPU的一些指令分別在4、8、16字節(jié)對齊時會有更快的運(yùn)行速度,因此gcc編譯器為提高生成代碼在IA32上的運(yùn)行速度,默認(rèn)對產(chǎn)生的代碼進(jìn)行16字節(jié)對齊
??? ??? andl $0xf0,%esp 的意義很明顯,那么 subl $8,%esp 呢,是必須的嗎?
??? 這里假設(shè)在進(jìn)入main函數(shù)之前,棧是16字節(jié)對齊的話,那么,進(jìn)入main函數(shù)后,EIP和EBP被壓入堆棧后,棧地址最末4位二進(jìn)制位必定是1000,esp -8則恰好使后4位地址二進(jìn)制位為0000。看來,這也是為保證棧16字節(jié)對齊的。
??? 如果查一下gcc的手冊,就會發(fā)現(xiàn)關(guān)于棧對齊的參數(shù)設(shè)置:
??? -mpreferred-stack-boundary=n??? ; 希望棧按照2的n次的字節(jié)邊界對齊, n的取值范圍是2-12
??? 默認(rèn)情況下,n是等于4的,也就是說,默認(rèn)情況下,gcc是16字節(jié)對齊,以適應(yīng)IA32大多數(shù)指令的要求。
??? 讓我們利用-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字節(jié)對齊的,不需要用額外指令進(jìn)行對齊。
??? 那么,棧框架指針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)增加調(diào)式難度
??? ??? 由于SFP在調(diào)試器backtrace的指令中被使用到,因此沒有SFP該調(diào)試指令就無法使用。
??? 2)降低匯編代碼可讀性
??? ??? 函數(shù)參數(shù)和局部變量的訪問,在沒有ebp的情況下,都只能通過+xx(esp)的方式訪問,而很難區(qū)分兩種方式,降低了程序的可讀性。
??? ???
??? 問題:去除SFP有什么優(yōu)點呢?
??? ???
??? 1)節(jié)省??臻g
??? 2)減少建立和撤銷??蚣艿闹噶詈?#xff0c;簡化了代碼
??? 3)使ebp空閑出來,使之作為通用寄存器使用,增加通用寄存器的數(shù)量
??? 4)以上3點使得程序運(yùn)行速度更快
??? 概念:Calling Convention? 調(diào)用約定和 ABI (Application Binary Interface) 應(yīng)用程序二進(jìn)制接口
??? ?? ??
??? ??? 函數(shù)如何找到它的參數(shù)?
??? ??? 函數(shù)如何返回結(jié)果?
??? ??? 函數(shù)在哪里存放局部變量?
??? ??? 那一個硬件寄存器是起始空間?
??? ??? 那一個硬件寄存器必須預(yù)先保留?
??? Calling Convention? 調(diào)用約定對以上問題作出了規(guī)定。Calling Convention也是ABI的一部分。
??? 因此,遵守相同ABI規(guī)范的操作系統(tǒng),使其相互間實現(xiàn)二進(jìn)制代碼的互操作成為了可能。
??? 例如:由于Solaris、Linux都遵守System V的ABI,Solaris 10就提供了直接運(yùn)行Linux二進(jìn)制程序的功能。
??? 詳見文章:關(guān)注: Solaris 10的10大新變化?
??? ?? ?? ???
3. 小結(jié)
??? 本文通過最簡的C程序,引入以下概念:
??? ??? SFP 棧框架指針
??? ??? Stack aligned 棧對齊
??? ??? Calling Convention? 調(diào)用約定 和 ABI (Application Binary Interface) 應(yīng)用程序二進(jìn)制接口
??? 今后,將通過進(jìn)一步的實驗,來深入了解這些概念。通過掌握這些概念,使在匯編級調(diào)試程序產(chǎn)生的core dump、掌握C語言高級調(diào)試技巧成為了可能。
??? 為簡化問題,來分析一下最簡的c代碼生成的匯編代碼:
??? # vi test1.c
? ? ??
??? int main()
??? {
?? ?? ? return 0;
??? }???
????
??? 編譯該程序,產(chǎn)生二進(jìn)制文件:
??? # 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)的可執(zhí)行文件,動態(tài)鏈接并且符號表沒有去除。
??? 這正是Unix/Linux平臺典型的可執(zhí)行文件格式。
??? 用mdb反匯編可以觀察生成的匯編代碼:
??? # mdb test1
??? Loading modules: [ libc.so.1 ]
??? > main::dis??? ??? ??? ??? ??? ?? ; 反匯編main函數(shù),mdb的命令一般格式為? <地址>::dis
??? main:??? ??? ? pushl?? %ebp?? ??? ; ebp寄存器內(nèi)容壓棧,即保存main函數(shù)的上級調(diào)用函數(shù)的?;刂?/span>
??? main+1:??? ??? movl??? %esp,%ebp? ; esp值賦給ebp,設(shè)置main函數(shù)的?;?/span>
??? 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??? ; 設(shè)置函數(shù)返回值0
??? main+0x15:???? leave????????????? ; 將ebp值賦給esp,pop先前棧內(nèi)的上級函數(shù)棧的基地址給ebp,恢復(fù)原棧基址
??? main+0x16:???? ret??????????????? ; main函數(shù)返回,回到上級調(diào)用
??? >?
??? 注:這里得到的匯編語言語法格式與Intel的手冊有很大不同,Unix/Linux采用AT&T匯編格式作為匯編語言的語法格式
??? ???? 如果想了解AT&T匯編可以參考文章:Linux AT&T 匯編語言開發(fā)指南?
??? 問題:誰調(diào)用了 main函數(shù)?
?????
???? 在C語言的層面來看,main函數(shù)是一個程序的起始入口點,而實際上,ELF可執(zhí)行文件的入口點并不是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>????????????? ;在這里調(diào)用了main函數(shù)
??? _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寄存器保存函數(shù)返回值?
??? 實際上IA32并沒有規(guī)定用哪個寄存器來保存返回值。但如果反匯編Solaris/Linux的二進(jìn)制文件,就會發(fā)現(xiàn),都用EAX保存函數(shù)返回值。
??? 這不是偶然現(xiàn)象,是操作系統(tǒng)的ABI(Application Binary Interface)來決定的。
??? Solaris/Linux操作系統(tǒng)的ABI就是Sytem V ABI。
??? 概念:SFP (Stack Frame Pointer) 棧框架指針?
??? 正確理解SFP必須了解:
??? ??? IA32 的棧的概念
??? ??? CPU 中32位寄存器ESP/EBP的作用
??? ??? PUSH/POP 指令是如何影響棧的
??? ??? CALL/RET/LEAVE 等指令是如何影響棧的
??? 如我們所知:
??? 1)IA32的棧是用來存放臨時數(shù)據(jù),而且是LIFO,即后進(jìn)先出的。棧的增長方向是從高地址向低地址增長,按字節(jié)為單位編址。
??? 2) EBP是?;返闹羔?#xff0c;永遠(yuǎn)指向棧底(高地址),ESP是棧指針,永遠(yuǎn)指向棧頂(低地址)。
??? 3) PUSH一個long型數(shù)據(jù)時,以字節(jié)為單位將數(shù)據(jù)壓入棧,從高到低按字節(jié)依次將數(shù)據(jù)存入ESP-1、ESP-2、ESP-3、ESP-4的地址單元。
??? 4) POP一個long型數(shù)據(jù),過程與PUSH相反,依次將ESP-4、ESP-3、ESP-2、ESP-1從棧內(nèi)彈出,放入一個32位寄存器。
??? 5) CALL指令用來調(diào)用一個函數(shù)或過程,此時,下一條指令地址會被壓入堆棧,以備返回時能恢復(fù)執(zhí)行下條指令。
??? 6) RET指令用來從一個函數(shù)或過程返回,之前CALL保存的下條指令地址會從棧內(nèi)彈出到EIP寄存器中,程序轉(zhuǎn)到CALL之前下條指令處執(zhí)行
??? 7) ENTER是建立當(dāng)前函數(shù)的棧框架,即相當(dāng)于以下兩條指令:
??? ??? pushl?? %ebp
??? ??? movl??? %esp,%ebp
??? 8) LEAVE是釋放當(dāng)前函數(shù)或者過程的??蚣?#xff0c;即相當(dāng)于以下兩條指令:
??? ??? movl ebp esp
??? ??? popl? ebp
??? 如果反匯編一個函數(shù),很多時候會在函數(shù)進(jìn)入和返回處,發(fā)現(xiàn)有類似如下形式的匯編語句:?
????????
??? ??? pushl?? %ebp??? ??? ??? ; ebp寄存器內(nèi)容壓棧,即保存main函數(shù)的上級調(diào)用函數(shù)的?;刂?br /> ??? ??? movl??? %esp,%ebp??? ?? ; esp值賦給ebp,設(shè)置 main函數(shù)的?;?br /> ??? ??? ...........??? ??? ???? ; 以上兩條指令相當(dāng)于 enter 0,0
??? ??? ...........
??? ??? leave??? ??? ??? ??? ?? ; 將ebp值賦給esp,pop先前棧內(nèi)的上級函數(shù)棧的基地址給ebp,恢復(fù)原?;?br /> ??? ??? ret???????????????????? ; main函數(shù)返回,回到上級調(diào)用
??? 這些語句就是用來創(chuàng)建和釋放一個函數(shù)或者過程的??蚣艿?。
??? 原來編譯器會自動在函數(shù)入口和出口處插入創(chuàng)建和釋放棧框架的語句。
??? 函數(shù)被調(diào)用時:
??? 1) EIP/EBP成為新函數(shù)棧的邊界
??? 函數(shù)被調(diào)用時,返回時的EIP首先被壓入堆棧;創(chuàng)建棧框架時,上級函數(shù)棧的EBP被壓入堆棧,與EIP一道行成新函數(shù)棧框架的邊界
??? 2) EBP成為??蚣苤羔楽FP,用來指示新函數(shù)棧的邊界
??? ??蚣芙⒑?#xff0c;EBP指向的棧的內(nèi)容就是上一級函數(shù)棧的EBP,可以想象,通過EBP就可以把層層調(diào)用函數(shù)的棧都回朔遍歷一遍,調(diào)試器就是利用這個特性實現(xiàn) backtrace功能的
??? 3) ESP總是作為棧指針指向棧頂,用來分配??臻g
??? 棧分配空間給函數(shù)局部變量時的語句通常就是給ESP減去一個常數(shù)值,例如,分配一個整型數(shù)據(jù)就是 ESP-4
??? 4) 函數(shù)的參數(shù)傳遞和局部變量訪問可以通過SFP即EBP來實現(xiàn)?
??? 由于??蚣苤羔樣肋h(yuǎn)指向當(dāng)前函數(shù)的?;刂?#xff0c;參數(shù)和局部變量訪問通常為如下形式:
??? ??? +8+xx(%ebp)?? ?? ?? ; 函數(shù)入口參數(shù)的的訪問
??? ??? -xx(%ebp)??? ??? ?? ; 函數(shù)局部變量訪問
????????????
??? 假如函數(shù)A調(diào)用函數(shù)B,函數(shù)B調(diào)用函數(shù)C ,則函數(shù)??蚣芗罢{(diào)用關(guān)系如下圖所示:
+-------------------------+----> 高地址| EIP (上級函數(shù)返回地址) | +-------------------------+ +--> | EBP (上級函數(shù)的EBP) | --+ <------當(dāng)前函數(shù)A的EBP (即SFP框架指針) | +-------------------------+ +-->偏移量A | | Local Variables | || | .......... | --+ <------ESP指向函數(shù)A新分配的局部變量,局部變量可以通過A的ebp-偏移量A訪問 | f +-------------------------+| r | Arg n(函數(shù)B的第n個參數(shù)) | | a +-------------------------+| m | Arg .(函數(shù)B的第.個參數(shù)) || e +-------------------------+| | Arg 1(函數(shù)B的第1個參數(shù)) || o +-------------------------+| f | Arg 0(函數(shù)B的第0個參數(shù)) | --+ <------ B函數(shù)的參數(shù)可以由B的ebp+偏移量B訪問| +-------------------------+ +--> 偏移量B| A | EIP (A函數(shù)的返回地址) | | | +-------------------------+ --+ +--- | EBP (A函數(shù)的EBP) |<--+ <------ 當(dāng)前函數(shù)B的EBP (即SFP框架指針) +-------------------------+ || Local Variables | || .......... | | <------ ESP指向函數(shù)B新分配的局部變量+-------------------------+ || Arg n(函數(shù)C的第n個參數(shù)) | |+-------------------------+ || Arg .(函數(shù)C的第.個參數(shù)) | |+-------------------------+ +--> frame of B| Arg 1(函數(shù)C的第1個參數(shù)) | |+-------------------------+ || Arg 0(函數(shù)C的第0個參數(shù)) | |+-------------------------+ || EIP (B函數(shù)的返回地址) | |+-------------------------+ |+--> | EBP (B函數(shù)的EBP) | --+ <------ 當(dāng)前函數(shù)C的EBP (即SFP框架指針) | +-------------------------+| | Local Variables || | .......... | <------ ESP指向函數(shù)C新分配的局部變量| +-------------------------+----> 低地址 frame of C圖 1-1 ???????
??? 再分析test1反匯編結(jié)果中剩余部分語句的含義:
????????
??? # mdb test1
??? Loading modules: [ libc.so.1 ]
??? > main::dis??????????????????????? ; 反匯編main函數(shù)
??? main:??? ????? pushl?? %ebp????????????????????????????
??? main+1:??? ??? movl??? %esp,%ebp??????? ; 創(chuàng)建Stack Frame(??蚣?
??? main+3:??? ?? subl??? $8,%esp?????? ; 通過ESP-8來分配8字節(jié)堆??臻g
??? main+6:??? ?? andl??? $0xf0,%esp??? ; 使棧地址16字節(jié)對齊
??? main+9:??? ?? movl??? $0,%eax?????? ; 無意義
??? main+0xe:???? subl??? %eax,%esp???? ; 無意義
??? main+0x10:???? movl??? $0,%eax????????? ; 設(shè)置main函數(shù)返回值
??? main+0x15:???? leave????????????????? ? ; 撤銷Stack Frame(棧框架)
??? main+0x16:???? ret????????????????????? ; main 函數(shù)返回
??? >
??? 以下兩句似乎是沒有意義的,果真是這樣嗎?
??? ??? movl??? $0,%eax?
??? ??? subl???? %eax,%esp
???????
??? 用gcc的O2級優(yōu)化來重新編譯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????? ; 設(shè)置main返回值,使用xorl異或指令來使eax為0
??? main+0xb:???? leave
??? main+0xc:???? ret
??? >?
??? 新的反匯編結(jié)果比最初的結(jié)果要簡潔一些,果然之前被認(rèn)為無用的語句被優(yōu)化掉了,進(jìn)一步驗證了之前的猜測。
??? 提示:編譯器產(chǎn)生的某些語句可能在程序?qū)嶋H語義上沒有用處,可以用優(yōu)化選項去掉這些語句。
??? 問題:為什么用xorl來設(shè)置eax的值?
??? 注意到優(yōu)化后的代碼中,eax返回值的設(shè)置由 movl $0,%eax 變?yōu)?xorl %eax,%eax ,這是因為IA32指令中,xorl比movl有更高的運(yùn)行速度。
??? 概念:Stack aligned 棧對齊
??? 那么,以下語句到底是和作用呢?
??? ??? subl??? $8,%esp
?????? andl??? $0xf0,%esp???? ; 通過andl使低4位為0,保證棧地址16字節(jié)對齊
???????
??? 表面來看,這條語句最直接的后果是使ESP的地址后4位為0,即16字節(jié)對齊,那么為什么這么做呢?
??? 原來,IA32 系列CPU的一些指令分別在4、8、16字節(jié)對齊時會有更快的運(yùn)行速度,因此gcc編譯器為提高生成代碼在IA32上的運(yùn)行速度,默認(rèn)對產(chǎn)生的代碼進(jìn)行16字節(jié)對齊
??? ??? andl $0xf0,%esp 的意義很明顯,那么 subl $8,%esp 呢,是必須的嗎?
??? 這里假設(shè)在進(jìn)入main函數(shù)之前,棧是16字節(jié)對齊的話,那么,進(jìn)入main函數(shù)后,EIP和EBP被壓入堆棧后,棧地址最末4位二進(jìn)制位必定是1000,esp -8則恰好使后4位地址二進(jìn)制位為0000。看來,這也是為保證棧16字節(jié)對齊的。
??? 如果查一下gcc的手冊,就會發(fā)現(xiàn)關(guān)于棧對齊的參數(shù)設(shè)置:
??? -mpreferred-stack-boundary=n??? ; 希望棧按照2的n次的字節(jié)邊界對齊, n的取值范圍是2-12
??? 默認(rèn)情況下,n是等于4的,也就是說,默認(rèn)情況下,gcc是16字節(jié)對齊,以適應(yīng)IA32大多數(shù)指令的要求。
??? 讓我們利用-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字節(jié)對齊的,不需要用額外指令進(jìn)行對齊。
??? 那么,棧框架指針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)增加調(diào)式難度
??? ??? 由于SFP在調(diào)試器backtrace的指令中被使用到,因此沒有SFP該調(diào)試指令就無法使用。
??? 2)降低匯編代碼可讀性
??? ??? 函數(shù)參數(shù)和局部變量的訪問,在沒有ebp的情況下,都只能通過+xx(esp)的方式訪問,而很難區(qū)分兩種方式,降低了程序的可讀性。
??? ???
??? 問題:去除SFP有什么優(yōu)點呢?
??? ???
??? 1)節(jié)省??臻g
??? 2)減少建立和撤銷??蚣艿闹噶詈?#xff0c;簡化了代碼
??? 3)使ebp空閑出來,使之作為通用寄存器使用,增加通用寄存器的數(shù)量
??? 4)以上3點使得程序運(yùn)行速度更快
??? 概念:Calling Convention? 調(diào)用約定和 ABI (Application Binary Interface) 應(yīng)用程序二進(jìn)制接口
??? ?? ??
??? ??? 函數(shù)如何找到它的參數(shù)?
??? ??? 函數(shù)如何返回結(jié)果?
??? ??? 函數(shù)在哪里存放局部變量?
??? ??? 那一個硬件寄存器是起始空間?
??? ??? 那一個硬件寄存器必須預(yù)先保留?
??? Calling Convention? 調(diào)用約定對以上問題作出了規(guī)定。Calling Convention也是ABI的一部分。
??? 因此,遵守相同ABI規(guī)范的操作系統(tǒng),使其相互間實現(xiàn)二進(jìn)制代碼的互操作成為了可能。
??? 例如:由于Solaris、Linux都遵守System V的ABI,Solaris 10就提供了直接運(yùn)行Linux二進(jìn)制程序的功能。
??? 詳見文章:關(guān)注: Solaris 10的10大新變化?
??? ?? ?? ???
3. 小結(jié)
??? 本文通過最簡的C程序,引入以下概念:
??? ??? SFP 棧框架指針
??? ??? Stack aligned 棧對齊
??? ??? Calling Convention? 調(diào)用約定 和 ABI (Application Binary Interface) 應(yīng)用程序二進(jìn)制接口
??? 今后,將通過進(jìn)一步的實驗,來深入了解這些概念。通過掌握這些概念,使在匯編級調(diào)試程序產(chǎn)生的core dump、掌握C語言高級調(diào)試技巧成為了可能。
總結(jié)
- 上一篇: CRM客户关系管理系统开发第十九讲——实
- 下一篇: 商务阶段客户拜访技巧