在python java這些語言中,一旦程序發生了異常,以下圖,會打印異常發生時的調用棧,而在c/c++中若是須要實現相似的功能則要咱們依靠libbacktrace之類的庫去打印調用棧,html
在c/c++中如何去實現一個相似libbacktrace這樣的庫來打印函數調用棧呢?本文將介紹一種naive的backtrace實現,講解backtrace實現的原理。java
棧幀(stack frame)保存着函數調用信息,以下圖它存儲於c內存佈局中的棧區。python
棧幀是實現backtrace的核心關鍵,它會存儲着函數調用時的局部變量同時記錄了函數調用的上下文如返回地址,其具體佈局以下圖。linux
ebp寄存器存儲着棧基址指針,esp寄存器存儲着當前棧頂指針,咱們將ebp-esp之間的內存稱爲棧幀。c++
想要具體瞭解函數調用時,棧幀是如何變化的咱們能夠從彙編代碼進行了解,咱們將以下的代碼進行反彙編api
// file:naive.c
int add(int a, int b){
return a + b;
}
int main(int argc, char* argv[]){
add(1,2);
}
// 反彙編命令 gcc -m32 -S -O0 -masm=intel naive.c
複製代碼
獲得以下代碼ide
_add: ## @add
push ebp
mov ebp, esp
mov eax, dword ptr [ebp + 12]
add eax, dword ptr [ebp + 8]
pop ebp
ret
_main: ## @main
push ebp
mov ebp, esp
sub esp, 24 # 預留棧空間存儲局部變量
mov eax, dword ptr [ebp + 12]
mov ecx, dword ptr [ebp + 8]
mov dword ptr [esp], 1 # 設置局部變量1,2
mov dword ptr [esp + 4], 2
mov dword ptr [ebp - 4], eax ## 4-byte Spill
mov dword ptr [ebp - 8], ecx ## 4-byte Spill
call _add
xor ecx, ecx
mov dword ptr [ebp - 12], eax ## 4-byte Spill
mov eax, ecx
add esp, 24
pop ebp
ret
複製代碼
在彙編中使用call _add時,它會將下一條地址推入棧中,並跳轉至函數位置,即call _add
至關於兩條指令,push pc; jmp _add
, 在使用call _add 指令後,此時棧頂(esp指向的地址)存儲着xor ecx, ecx指令的地址。函數
在進入_add後,會將當前棧基址推入棧中,並經過mov ebp, esp造成新的棧幀。佈局
如上圖咱們在32位的程序中能夠經過[ebp+4]獲得函數的返回地址,同時此時ebp指向的地址的值是保存的ebp值。post
更加詳細的能夠參考以下文章
經過不斷取寄存器ebp地址,咱們可以得到一個個相連的棧幀,咱們如何獲取ebp的值呢,經過純c代碼難以實現這個目標,所以最終我使用了內聯彙編來實現
typedef void* ptr_t;
inline ptr_t* get_ebp(){
ptr_t* reg_ebp;
asm volatile(
"movq %%rbp, %0 \n\t"
: "=r" (reg_ebp)
);
return reg_ebp;
}
複製代碼
可是咱們經過函數的棧幀仍然沒法獲取完整的調用棧信息,咱們須要還原到底是哪一個函數調用的,所以須要用過返回地址來獲取調用函數。
全部函數其實都是有一個範圍的,它存儲在函數的代碼區,咱們經過記錄函數的地址,並在其中尋找離返回地址最近並低於返回地址的函數地址就是其調用的函數。最初我是經過以下代碼來記錄函數地址的
typedef struct {
char *function_name;
int *function_address;
}function_record_t;
typedef struct{
int now_size;
int max_size;
function_record_t *records;
}function_record_vec_t;
function_record_vec_t vec;
int function_record_vec_init(function_record_vec_t *self){
self->now_size = 0;
self->max_size = 5;
self->records = (function_record_t *)malloc(sizeof(function_record_t) * self->max_size);
if(!self->records) return 0;
return 1;
}
int function_record_vec_push(function_record_vec_t *self, function_record_t record){
if(self->now_size == self->max_size){
self->max_size = self->max_size << 1;
self->records = (function_record_t *)realloc(self->records, sizeof(function_record_t) * self->max_size); if(!self->records) return 0;
}
self->records[self->now_size++] = record;
return 1;
}
// 尋找匹配函數信息,須要本身手動記錄全部函數信息
function_record_t * find_best_record(int *return_address){
for(int i=0; i<vec.now_size; i++){
if(vec.records[i].function_address < return_address)
{
return vec.records+i; // 返回最符合要求的函數地址
}
}
}
int main(void){
function_record_vec_init(&vec);
function_record_t main_f = {"main", &main};
function_record_vec_push(&vec, main_f);
// 省略記錄全部函數地址和名字的過程
qsort(vec.records, vec.now_size, sizeof(function_record_t), compare_record);//地址從低到高排序
}
複製代碼
這種方式實在過於愚蠢,因而我開始尋找可以直接經過地址獲取調用函數信息的api,linux系統中的dladdr剛好能符合個人需求,所以上面的代碼可以簡化成下面的版本
void identify_function_ptr( void *func) {
Dl_info info;
int rc;
rc = dladdr(func, &info);
if (!rc) {
printf("Problem retrieving program information for %x: %s\n", func, dlerror());
}
printf("Address located in function %s within the program %s\n", info.dli_fname, info.dli_sname);
}
複製代碼
傳入一個地址,就可以獲取這個地址最有可能在哪一個函數中。
最終代碼以下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
void identify_function_ptr( void *func) {
Dl_info info;
int rc;
rc = dladdr(func, &info);
if (!rc) {
printf("Problem retrieving program information for %x: %s\n", func, dlerror());
}
printf("Address located in function %s within the program %s\n", info.dli_fname, info.dli_sname);
}
typedef void* ptr_t;
typedef struct _frame_t{
ptr_t return_address;
ptr_t ebp;
struct _frame_t *next_frame;
}frame_t;
int frame_init(frame_t *self, ptr_t ebp, ptr_t return_address){
self->return_address = return_address;
self->ebp = ebp;
self->next_frame = NULL;
}
void back_trace(){
ptr_t* reg_ebp;
asm volatile(
"movq %%rbp, %0 \n\t"
: "=r" (reg_ebp)
);
frame_t* now_frame=NULL;
while(reg_ebp){
frame_t *new_frame = (frame_t *) malloc(sizeof(frame_t));
frame_init(new_frame, (ptr_t)reg_ebp, (ptr_t)(*(reg_ebp+1)));
new_frame->next_frame = now_frame;
now_frame = new_frame;
reg_ebp = (ptr_t)(*reg_ebp);
}
while(now_frame){
identify_function_ptr((ptr_t)now_frame->return_address);
now_frame = now_frame->next_frame;
}
}
void two(){
back_trace();
}
void one(){
two();
}
int main(void){
one();
}
複製代碼
其結果以下
libbacktrace依賴libunwind來實現對調用棧的還原。上面的代碼若是要對c++使用,須要使用demangle還原c++函數的符號名。