顯示函數(shù)的調(diào)用關(guān)系是調(diào)試器的必備功能,如果我們在程序的運(yùn)行中出現(xiàn)了崩潰的情況,通過函數(shù)的調(diào)用關(guān)系可以快速定位問題的根源,懂得函數(shù)調(diào)用關(guān)系的實現(xiàn)原理也可以擴(kuò)充自己的知識面,在沒有調(diào)試器的情況下,我們也可以自己來實現(xiàn)顯示函數(shù)的調(diào)用關(guān)系。在我們自己動手寫backtrace函數(shù)之前,先來看看glibc提供的backtrace函數(shù)的使用。代碼如下: #include #include #include #define MAX_LEVEL 4 static void call2() { int i = 0; void* buffer[MAX_LEVEL] = {0}; int size=backtrace(buffer, MAX_LEVEL); for(i = 0; i t.sh;. t.sh;rm -f t.sh 輸出結(jié)果為: /home/shiyan/sss.c:12 /home/shiyan/sss.c:27 /home/shiyan/sss.c:34 /home/shiyan/sss.c:40 接下來看看在棧中數(shù)據(jù)的結(jié)構(gòu)。 函數(shù)參數(shù)的壓棧是從右向左的,即先壓最后一個參數(shù),在壓倒數(shù)第二個,以此類推,最后才壓入第一個參數(shù)。為了加深大家的印象,下面我給出一個測試代碼: #include void turn(int x, int y, int z) { printf("x = %d at [%X]\n", x, &x); printf("y = %d at [%X]\n", y, &y); printf("z = %d at [%X]\n", z, &z); } int main(int argc, char *argv[]) { turn(1, 2, 3); return 0; } 運(yùn)行結(jié)果為: 比較打印出來的地址可以看出參數(shù)z的地址是最大的,x的地址最小。 參數(shù)的壓棧工作完成之后,接下來就依次是EIP、EBP、臨時變量的壓棧操作了。最后壓入的是被調(diào)用函數(shù)本身,并為它分配臨時的變量空間,而對于不同版本的gcc的處理方式各有不同,老版本的gcc第一個臨時變量放在最高的地址,第二個其次,依次順序分布,新版本的gcc則與之相反。 實現(xiàn)backtrace()函數(shù)的調(diào)用關(guān)系,其步驟如下: 1.獲取當(dāng)前函數(shù)的EBP; 2.通過EBP獲得調(diào)用者得EIP; 3.通過EBP獲得上一級的EBP; 4.重復(fù)這個過程,知道結(jié)束。 自己實現(xiàn)的backtrace()函數(shù),代碼如下: #include #define MAX_LEVEL 4 #define OFFSET 4 int backtrace(void** buffer, int size) { int n = 0x23f; int* p = &n; int i = 0; int ebp = p[1 + OFFSET]; int eip = p[2 + OFFSET]; for(i = 0; i t.sh;. t.sh;rm -f t.sh root@ubuntu:/home/shiyan# ./tt |awk '{print "addr2line "$3" -e tt"}'>t.sh;. t.sh;rm -f t.sh /home/shiyan/bac.c:32 /home/shiyan/bac.c:47 /home/shiyan/bac.c:54 /home/shiyan/bac.c:60 在此重點介紹下backtrace()函數(shù)的實現(xiàn)原理。 通過 int* p = &n;來獲取第一個臨時變量的位置,因為我使用的是新版本的gcc,有5個臨時變量,所以EIP的值存放在p[6]中,EBP的的值存放在p[5],通過 buffer[i ] = (void*)eip;可以把eip的強(qiáng)制轉(zhuǎn)換為可以指向任意類型的指針, 接下來通過 p = (int*)ebp;來獲得上一個函數(shù)的ebp,獲得ebp之后由ebp和eip的位置關(guān)系可以得到eip,由于ebp指向的單元存儲的是上一個函數(shù)的ebp,所以用一個簡單的for循環(huán)就能實現(xiàn)了。 另外在頭文件"execinfo.h"中除了聲明backtrace()函數(shù)外,還有如下兩個函數(shù)也用于獲取當(dāng)前線程的函數(shù)調(diào)用堆棧。 char ** backtrace_symbols (void *const *buffer, int size) backtrace_symbols將從backtrace函數(shù)獲取的信息轉(zhuǎn)化為一個字符串?dāng)?shù)組. 參數(shù)buffer應(yīng)該是從backtrace函數(shù)獲取的數(shù)組指針,size是該數(shù)組中的元素個數(shù)(backtrace的返回值) 函數(shù)返回值是一個指向字符串?dāng)?shù)組的指針,它的大小同buffer相同.每個字符串包含了一個相對于buffer中對應(yīng)元素的可打印信息.它包括函數(shù)名,函數(shù)的偏移地址,和實際的返回地址 現(xiàn)在,只有使用ELF二進(jìn)制格式的程序和苦衷才能獲取函數(shù)名稱和偏移地址.在其他系統(tǒng),只有16進(jìn)制的返回地址能被獲取.另外,你可能需要傳遞相應(yīng)的標(biāo)志給鏈接器,以能支持函數(shù)名功能(比如,在使用GNU ld的系統(tǒng)中,你需要傳遞(-rdynamic)),該函數(shù)的返回值是通過malloc函數(shù)申請的空間,因此調(diào)用這必須使用free函數(shù)來釋放指針。 注意:如果不能為字符串獲取足夠的空間函數(shù)的返回值將會為NULL。 Function:void backtrace_symbols_fd (void *const *buffer, int size, int fd) backtrace_symbols_fd與backtrace_symbols 函數(shù)具有相同的功能,不同的是它不會給調(diào)用者返回字符串?dāng)?shù)組,而是將結(jié)果寫入文件描述符為fd的文件中,每個函數(shù)對應(yīng)一行.它不需要調(diào)用malloc函數(shù),因此適用于有可能調(diào)用該函數(shù)會失敗的情況。 還是那句話,以上內(nèi)容難免有誤,如有錯誤,請糾正。 |