顯示函式的呼叫關系是除錯器的必備功能,如果我們在程式的運行中出現了崩潰的情況,通過函式的呼叫關系可以快速定位問題的根源,懂得函式呼叫關系的實作原理也可以擴充自己的知識面,在沒有除錯器的情況下,我們也可以自己來實作顯示函式的呼叫關系。在我們自己動手寫backtrace函式之前,先來看看glibc提供的backtrace函式的使用。代碼如下:
本文參考地址:http://www.xxylyg.com/jiaoxuekeyan/news/2.html
#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 < size; i++)
{
printf("called by %p\n", buffer[i]);
}
return;
}
static void call1()
{
call2();
return;
}
static void call()
{
call1();
return;
}
int main(int argc, char* argv[])
{
call();
return 0;
}
在此先講解下backtrace()函式的使用:
int backtrace(void **buffer,int size)
該函式用來獲取當前執行緒的呼叫堆疊,獲取的資訊將會被存放在buffer中,它是一個指標串列。引數 size 用來指定buffer中可以保存多少個void* 元素。函式回傳值是實際獲取的指標個數,最大不超過size大小,在buffer中的指標實際是從堆疊中獲取的回傳地址,每一個堆疊框架有一個回傳地址。
接下來的任務就是編譯運行了。
root@ubuntu:/home/shiyan# gcc -g -Wall sss.c -o p
root@ubuntu:/home/shiyan# ./p
輸出結果為:
called by 0x8048440
called by 0x804847d
called by 0x804848a
called by 0x8048497
上面的運行結果就是呼叫者的地址,看起來還不是那么的直觀,我們使用addr2line工具來實作地址到源代碼位置的轉換。
運行
root@ubuntu:/home/shiyan# ./p |awk '{print "addr2line "$3" -e p"}'>t.sh;. t.sh;rm -f t.sh
輸出結果為:
/home/shiyan/sss.c:12
/home/shiyan/sss.c:27
/home/shiyan/sss.c:34
/home/shiyan/sss.c:40
接下來看看在堆疊中資料的結構。
函式引數的壓堆疊是從右向左的,即先壓最后一個引數,在壓倒數第二個,以此類推,最后才壓入第一個引數。為了加深大家的印象,下面我給出一個測驗代碼:
本文參考地址:http://www.eepw.com.cn/article/270700.htm
#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;
}
運行結果為:
比較列印出來的地址可以看出引數z的地址是最大的,x的地址最小。
引數的壓堆疊作業完成之后,接下來就依次是EIP、EBP、臨時變數的壓堆疊操作了。最后壓入的是被呼叫函式本身,并為它分配臨時的變數空間,而對于不同版本的gcc的處理方式各有不同,老版本的gcc第一個臨時變數放在最高的地址,第二個其次,依次順序分布,新版本的gcc則與之相反。
實作backtrace()函式的呼叫關系,其步驟如下:
1.獲取當前函式的EBP;
2.通過EBP獲得呼叫者得EIP;
3.通過EBP獲得上一級的EBP;
4.重復這個程序,知道結束。
自己實作的backtrace()函式,代碼如下:
#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 < size; i++)
{
buffer[i] = (void*)eip;
p = (int*)ebp;
ebp = p[0];
eip = p[1];
}
return size;
}
static void call2()
{
int i = 0;
void* buffer[MAX_LEVEL] = {0};
int size=backtrace(buffer, MAX_LEVEL);
for(i = 0; i < size; i++)
{
printf("called by %p\n", buffer[i]);
}
return;
}
static void call1()
{
call2();
return;
}
static void call()
{
call1();
return;
}
int main(int argc, char* argv[])
{
call();
return 0;
}
運行結果如下:
root@ubuntu:/home/shiyan# gcc -g bac.c -o tt
root@ubuntu:/home/shiyan# ./tt
called by 0x8048491
called by 0x80484ce
called by 0x80484db
called by 0x80484e8
轉換為源代碼位置:root@ubuntu:/home/shiyan# ./tt |awk '{print "addr2line "$3" -e tt"}'>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()函式的實作原理。
通過 int* p = &n;來獲取第一個臨時變數的位置,因為我使用的是新版本的gcc,有5個臨時變數,所以EIP的值存放在p[6]中,EBP的的值存放在p[5],通過 buffer[i] = (void*)eip;可以把eip的強制轉換為可以指向任意型別的指標, 接下來通過 p = (int*)ebp;來獲得上一個函式的ebp,獲得ebp之后由ebp和eip的位置關系可以得到eip,由于ebp指向的單元存盤的是上一個函式的ebp,所以用一個簡單的for回圈就能實作了。
另外在頭檔案"execinfo.h"中除了宣告backtrace()函式外,還有如下兩個函式也用于獲取當前執行緒的函式呼叫堆疊。
char ** backtrace_symbols (void *const *buffer, int size)
backtrace_symbols將從backtrace函式獲取的資訊轉化為一個字串陣列. 引數buffer應該是從backtrace函式獲取的陣列指標,size是該陣列中的元素個數(backtrace的回傳值)
函式回傳值是一個指向字串陣列的指標,它的大小同buffer相同.每個字串包含了一個相對于buffer中對應元素的可列印資訊.它包括函式名,函式的偏移地址,和實際的回傳地址
現在,只有使用ELF二進制格式的程式和苦衷才能獲取函式名稱和偏移地址.在其他系統,只有16進制的回傳地址能被獲取.另外,你可能需要傳遞相應的標志給聯結器,以能支持函式名功能(比如,在使用GNU ld的系統中,你需要傳遞(-rdynamic)),該函式的回傳值是通過malloc函式申請的空間,因此呼叫這必須使用free函式來釋放指標。
注意:如果不能為字串獲取足夠的空間函式的回傳值將會為NULL。
Function:void backtrace_symbols_fd (void *const *buffer, int size, int fd)
backtrace_symbols_fd與backtrace_symbols 函式具有相同的功能,不同的是它不會給呼叫者回傳字串陣列,而是將結果寫入檔案描述符為fd的檔案中,每個函式對應一行.它不需要呼叫malloc函式,因此適用于有可能呼叫該函式會失敗的情況。
uj5u.com熱心網友回復:
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/103006.html
標籤:基礎類
