这个banner太好看了再用一次
在尝试进行jyyos实验时,jyy老师提到:如果你写一小段代码,能够打印出 call stack frame,那会极大程度简化你们调试的过程——虽然这不是必须的。
由于自己在过去确实对在出错时打印调用栈
感兴趣,但是当时水平不足没有后文,在自认为有一定水平提升的情况下这次来花点时间尝试一下。
数据结构
为了能够实现调用栈追踪,并在出错时打印,我们需要思考如何设计数据结构,当然既然名字带栈我们就用栈实现!
调用信息
应该保存什么样的信息?这确实是一个很重要的问题,但是限于标准C语言贫瘠的反射支持(大概),一切从简。所以我们只保存三个信息:函数名,函数所处文件(用于模块化开发)和函数所在行号。
1 2 3 4 5 6
| typedef struct call_stack_trace_item { char name[256]; char file[PATH_MAX]; size_t line; } csti_t;
|
这些member都有标准的宏进行预编译阶段的展开(应该都是c标准中的吧)。分别是__func__
、__FILE__
和__LINE__
。
怎么实现
如何将这个普通的栈和函数调用联系起来
一个容易想到的实现是
这样不仅实现简单,在我们中断正常的运行过程后,还可以直接遍历调用栈并打印。
如何中断运行过程并输出栈
我们有assert
关键字(实现是个宏)可以中断当前控制并打印发生异常的行号等相关信息。虽然我们可以展开这个宏研究后自己实现一个效果更好的,但是简单起见我们进行一个直接的wrap。
我们包装了的assert,在行为上,当condition为false时则从栈底部遍历存储调用信息并打印,最后再调用assert,这样就实现了中断运行过程并输出调用栈信息。
如何化简
用宏狠狠包装一下。
C语言有黑魔法——宏。所以我们可以利用宏将上述一系列行为封装为一个宏。而我们只需要多写一些字符来调用宏。
1 2 3 4 5 6 7 8 9 10
| #define _cst_(func_cond) \ do \ { \ csti_t tmpcsfi = {.line = __LINE__}; \ strcpy(tmpcsfi.name, __func__); \ strcpy(tmpcsfi.file, __FILE__); \ call_stack_trace_push(&cst_stack, &tmpcsfi, 1); \ func_cond; \ call_stack_trace_pop(&cst_stack, &tmpcsfi, 1); \ } while (0)
|
在我这个实现中,call_stack_trace_push
是自己实现的上文中的结构体的栈的基础操作,我们不需要知道实现细节,只需要知道pop和push即可。这个宏的行为是创建一个临时结构体,并将结构体中的数据push进一个名为cst_stack
的实例化过的栈中。由于宏在预处理期展开的性质,这几个宏的参数在_cst_宏展开后被预处理展开为调用者函数的相关信息。而调用者需要包装一下自己调用函数的语句从foo()
变为_cst_(foo())
。是不是用起来很简单。
我们包装了的assert
叫做_cst_assert
。以下是它的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #define _cst_assert(cond) \ do \ { \ if (!cond) \ { \ puts("\nOops, something is wrong!"); \ struct winsize w; \ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == -1) \ { \ perror("ioctl"); \ exit(1); \ } \ printf(dump_msg); \ printns("=", w.ws_col - sizeof(dump_msg) / sizeof(char) + 2); \ putchar('\n'); \ for (size_t i = 0; i < cst_stack._size; i++) \ { \ for (size_t j = 0; j < i; j++) \ { \ printf("-- "); \ } \ printf("calling %s: %s:%d \n", cst_stack._ptr[i].name, cst_stack._ptr[i].file, cst_stack._ptr[i].line); \ } \ assert(cond); \ } \ } while (0)
|
其中printns
是一个宏,用于重复打印字符串n次。
当然看着有点长,我们化简一下
1 2 3 4 5 6 7
| #define _cst_assert(cond) if(!cond) { puts("提示信息"); }
|
这样在使用时,被调用者如果想打断当前程序流并输出调用栈,只需要使用_cst_assert(cond)
即可。
示例
源码(仅在linux gcc环境下编译成功)
为了更好看的打印格式,虽然并没多好看,我使用了linux的非标准库函数,你可以将ioctl
相关源码删除。理论上并不限制os。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
| #include <stdio.h> #include <stdlib.h> #include <linux/limits.h> #include "Stack.h" #include <string.h> #include <assert.h> #include <sys/ioctl.h> #include <unistd.h> typedef struct call_stack_trace_item { char name[256]; char file[PATH_MAX]; size_t line; } csti_t;
EXPAND_STACK_IMPLEMENT(call_stack_trace, csti_t, 32)
call_stack_trace_Stack cst_stack;
#define printns(str, n) \ { \ for (size_t i = 0; i < n; i++) \ { \ printf(str); \ fflush(stdout); \ } \ }
#define _cst_(func_cond) \ do \ { \ csti_t tmpcsfi = {.line = __LINE__}; \ strcpy(tmpcsfi.name, __func__); \ strcpy(tmpcsfi.file, __FILE__); \ call_stack_trace_push(&cst_stack, &tmpcsfi, 1); \ func_cond; \ call_stack_trace_pop(&cst_stack, &tmpcsfi, 1); \ } while (0)
const char dump_msg[] = "\ncalling stack dump ";
#define _cst_assert(cond) \ do \ { \ if (!cond) \ { \ puts("\nOops, something is wrong!"); \ struct winsize w; \ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == -1) \ { \ perror("ioctl"); \ exit(1); \ } \ printf(dump_msg); \ printns("=", w.ws_col - sizeof(dump_msg) / sizeof(char) + 2); \ putchar('\n'); \ for (size_t i = 0; i < cst_stack._size; i++) \ { \ for (size_t j = 0; j < i; j++) \ { \ printf("-- "); \ } \ printf("calling %s: %s:%d \n", cst_stack._ptr[i].name, cst_stack._ptr[i].file, cst_stack._ptr[i].line); \ } \ assert(cond); \ } \ } while (0)
int hello10() { puts("hello world3"); _cst_assert(1 == 0); } int hello9() { puts("hello world3"); _cst_(hello10()); } int hello8() { puts("hello world3"); _cst_(hello9()); } int hello7() { puts("hello world3"); _cst_(hello8()); } int hello6() { puts("hello world3"); _cst_(hello7()); } int hello5() { puts("hello world3"); _cst_(hello6()); } int hello4() { puts("hello world3"); _cst_(hello5()); }
int hello3() { puts("hello world3"); _cst_(hello4()); } int hello2() { puts("hello world2"); _cst_(hello3()); }
int hello() { puts("hello world"); _cst_(hello2()); }
int main(int argc, char const *argv[]) { call_stack_trace_init(&cst_stack); _cst_(hello2());
return 0; }
|
输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| hello world2 hello world3 hello world3 hello world3 hello world3 hello world3 hello world3 hello world3 hello world3
Oops, something is wrong!
calling stack dump ============================================================================================================================== calling main: /path/to/demo/demo.c:128 -- calling hello2: /path/to/demo/demo.c:116 -- -- calling hello3: /path/to/demo/demo.c:111 -- -- -- calling hello4: /path/to/demo/demo.c:105 -- -- -- -- calling hello5: /path/to/demo/demo.c:100 -- -- -- -- -- calling hello6: /path/to/demo/demo.c:95 -- -- -- -- -- -- calling hello7: /path/to/demo/demo.c:90 -- -- -- -- -- -- -- calling hello8: /path/to/demo/demo.c:85 -- -- -- -- -- -- -- -- calling hello9: /path/to/demo/demo.c:80 callstackTrace: /path/to/demo/demo.c:75: hello10: Assertion `1 == 0' failed.
|
由于使用了特别的格式文件路径:行号
可以在vscode中直接点击跳转到对应文件行号,更好用了。