C語(yǔ)言的那些小秘密之函數的調用關(guān)系
函數參數的壓棧是從右向左的,即先壓最后一個(gè)參數,在壓倒數第二個(gè),以此類(lèi)推,最后才壓入第一個(gè)參數。為了加深大家的印象,下面我給出一個(gè)測試代碼:
本文引用地址:http://dyxdggzs.com/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;
}
運行結果為:

比較打印出來(lái)的地址可以看出參數z的地址是最大的,x的地址最小。
參數的壓棧工作完成之后,接下來(lái)就依次是EIP、EBP、臨時(shí)變量的壓棧操作了。最后壓入的是被調用函數本身,并為它分配臨時(shí)的變量空間,而對于不同版本的gcc的處理方式各有不同,老版本的gcc第一個(gè)臨時(shí)變量放在最高的地址,第二個(gè)其次,依次順序分布,新版本的gcc則與之相反。
實(shí)現backtrace()函數的調用關(guān)系,其步驟如下:
1.獲取當前函數的EBP;
2.通過(guò)EBP獲得調用者得EIP;
3.通過(guò)EBP獲得上一級的EBP;
4.重復這個(gè)過(guò)程,知道結束。
自己實(shí)現的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 %pn", 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
在此重點(diǎn)介紹下backtrace()函數的實(shí)現原理。
通過(guò) int* p = &n;來(lái)獲取第一個(gè)臨時(shí)變量的位置,因為我使用的是新版本的gcc,有5個(gè)臨時(shí)變量,所以EIP的值存放在p[6]中,EBP的的值存放在p[5],通過(guò) buffer[i] = (void*)eip;可以把eip的強制轉換為可以指向任意類(lèi)型的指針, 接下來(lái)通過(guò) p = (int*)ebp;來(lái)獲得上一個(gè)函數的ebp,獲得ebp之后由ebp和eip的位置關(guān)系可以得到eip,由于ebp指向的單元存儲的是上一個(gè)函數的ebp,所以用一個(gè)簡(jiǎn)單的for循環(huán)就能實(shí)現了。
另外在頭文件"execinfo.h"中除了聲明backtrace()函數外,還有如下兩個(gè)函數也用于獲取當前線(xiàn)程的函數調用堆棧。
char ** backtrace_symbols (void *const *buffer, int size)
backtrace_symbols將從backtrace函數獲取的信息轉化為一個(gè)字符串數組. 參數buffer應該是從backtrace函數獲取的數組指針,size是該數組中的元素個(gè)數(backtrace的返回值)
函數返回值是一個(gè)指向字符串數組的指針,它的大小同buffer相同.每個(gè)字符串包含了一個(gè)相對于buffer中對應元素的可打印信息.它包括函數名,函數的偏移地址,和實(shí)際的返回地址
現在,只有使用ELF二進(jìn)制格式的程序和苦衷才能獲取函數名稱(chēng)和偏移地址.在其他系統,只有16進(jìn)制的返回地址能被獲取.另外,你可能需要傳遞相應的標志給鏈接器,以能支持函數名功能(比如,在使用GNU ld的系統中,你需要傳遞(-rdynamic)),該函數的返回值是通過(guò)malloc函數申請的空間,因此調用這必須使用free函數來(lái)釋放指針。
注意:如果不能為字符串獲取足夠的空間函數的返回值將會(huì )為NULL。
Function:void backtrace_symbols_fd (void *const *buffer, int size, int fd)
backtrace_symbols_fd與backtrace_symbols 函數具有相同的功能,不同的是它不會(huì )給調用者返回字符串數組,而是將結果寫(xiě)入文件描述符為fd的文件中,每個(gè)函數對應一行.它不需要調用malloc函數,因此適用于有可能調用該函數會(huì )失敗的情況。
還是那句話(huà),以上內容難免有誤,如有錯誤,請糾正。
c語(yǔ)言相關(guān)文章:c語(yǔ)言教程
評論