Введение
В таких языках, как python и java, как только в программе возникает исключение, как показано на рисунке ниже, будет напечатан стек вызовов при возникновении исключения.В c/c++, если необходимо реализовать подобную функцию, нам нужно полагаться на такие библиотеки, как libbacktrace, для печати стека вызовов.
Как реализовать библиотеку, такую как libbacktrace, в c/c++ для печати стека вызовов функций? В этой статье будет представлена реализация обратной трассировки naive и объяснен принцип реализации обратной трассировки.
Основа трассировки стека вызовов — кадр стека (stack frame)
Фрейм стека (stack frame) содержит информацию о вызове функции, как показано на рисунке ниже, она хранится в области стека в структуре памяти c.
Кадр стека является основным ключом для реализации обратной трассировки. В нем хранятся локальные переменные вызова функции и записывается контекст вызова функции, такой как адрес возврата. Конкретный макет выглядит следующим образом.
Регистр ebp хранит указатель базового адреса стека, а регистр esp хранит текущий указатель вершины стека Мы называем память между ebp и esp кадром стека.
Чтобы понять, как меняется кадр стека при вызове функции, мы можем понять это из ассемблерного кода.Мы разберем следующий код
// 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
получить следующий код
_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
При использовании вызова _add в ассемблере он помещает следующий адрес в стек и переходит к местоположению функции, т.е.call _add
Эквивалентно двум инструкциям,push pc; jmp _add
, После использования инструкции call _add вершина стека (адрес, на который указывает esp) сохраняет адрес инструкции xor ecx, ecx.
После ввода _add текущий базовый адрес стека будет помещен в стек, а новый кадр стека будет сформирован через mov ebp, esp.
Как показано на рисунке выше, мы можем получить обратный адрес функции через [ebp+4] в 32-битной программе, а значение адреса, на которое указывает ebp, является сохраненным значением ebp.
Для получения более подробной информации, пожалуйста, обратитесь к следующей статье
Чжиху: Как стек перемещается и извлекается во время вызовов функций?
Получить информацию о стеке вызовов
Постоянно беря адрес регистра 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);//地址从低到高排序
}
Этот метод слишком тупой, поэтому я начал искать апи, которое может напрямую получить информацию о вызывающей функции через адрес.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++.