Реализация трассировки стека вызовов

C++

Введение

В таких языках, как python и java, как только в программе возникает исключение, как показано на рисунке ниже, будет напечатан стек вызовов при возникновении исключения.В c/c++, если необходимо реализовать подобную функцию, нам нужно полагаться на такие библиотеки, как libbacktrace, для печати стека вызовов.

image.png

Как реализовать библиотеку, такую ​​​​как libbacktrace, в c/c++ для печати стека вызовов функций? В этой статье будет представлена ​​реализация обратной трассировки naive и объяснен принцип реализации обратной трассировки.

Основа трассировки стека вызовов — кадр стека (stack frame)

Фрейм стека (stack frame) содержит информацию о вызове функции, как показано на рисунке ниже, она хранится в области стека в структуре памяти c.

image.png

Кадр стека является основным ключом для реализации обратной трассировки. В нем хранятся локальные переменные вызова функции и записывается контекст вызова функции, такой как адрес возврата. Конкретный макет выглядит следующим образом.

image.png

Регистр 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.

image.png

Для получения более подробной информации, пожалуйста, обратитесь к следующей статье

journey-to-the-stack

Чжиху: Как стек перемещается и извлекается во время вызовов функций?

stack frameout layout

Получить информацию о стеке вызовов

Постоянно беря адрес регистра 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();
}

Результат выглядит следующим образом

image.png

libbacktrace использует libunwind для восстановления стека вызовов. Если приведенный выше код будет использоваться в C++, вам необходимо использовать demangle для восстановления имени символа функции C++.