整理下 C 语言中调试、发布的流程。
编码规范
这里直接使用的是 Linux 的编码规范,中文可以查看 Linux 内核代码风格 。
$ scripts/checkpatch.pl --no-tree -f log.c
如果是通过命令行执行,则可以执行如下命令检查提交代码是否符和规范。
$ git diff agent/*.[ch] | contrib/checkpatch.pl --no-tree --no-signoff -
另外,也可以添加到 git 的 pre-commit
脚本中。
$ cat >.git/hooks/pre-commit
#!/bin/bash
exec git diff --cached agent/*.[ch] | contrib/checkpatch.pl --no-tree --no-signoff -
^D
$ chmod 755 .git/hooks/pre-commit
注意,pre-commit
不能添加到版本仓库中。
FAQ
ERROR: need consistent spacing around '*' (ctx:WxV)
#88: FILE: include/linux/pmem.h:167:
+static inline void flush_cache_pmem(void __pmem *addr, size_t size)
实际上是无法识别 __pmem
引起的,此时需要在脚本的 our $Sparse
中添加 __pmem
。
不检查
如果不需要检查一行记录,可以通过添加宏覆盖掉,例如使用 __SUPP(x)
宏,并在 foreach my ...
中增加如下语句。
if ($line=~/__SUPER/) {
next;
}
正式发布
Backtrace 会有如下的问题:
- 没有行信息,无法确认是那个函数的那一行出了问题;
- 没有入参以及本地变量;
- 打印参数名仅供参考,因为static和inline的会被删除,除非添加了
-rdynamic
参数;
前两者实际上没有太好的办法处理,而第 3 点实际上可以通过一些手段进行定位。首先,需要了解栈是如何生成的。
CPU 中的有指针保存栈顶的寄存器,而每个栈帧同时包含了其调用的栈帧以及函数返回的地址,然后再通过当前的指针地址,就开确定当前执行的是那个函数。
同样,这里面会有几个问题:
- 如果调用顺序为
foo()->bar()
,当bar()
返回类型为void
,那么可能直接忽略bar()
跳转到foo()
。 - 在打印栈时,会在内存中查找最近的符号表,对于
static
或者inline
类型的函数会被忽略。
通过上述内容的介绍基本上可以确认函数的执行顺序。
打印栈
最简答的是使用 GCC 的宏定义在日志中打印。
#define TRACE_MSG fprintf(stderr, __FUNCTION__ "() [%s:%d] here I am\n", __FILE__, __LINE__)
也可以通过 backtrace()
获取调用栈,然后使用 backtrace_symbols()
解析其地址,或者会返回相对最近函数的偏移以及返回地址。
另外,也可以通过 backtrace_symbols_fd()
将栈的信息直接打印到文件中。
触发位置
一般来说,需要在一些常见的异常信号中打印问题栈,例如 SIGSEGV
SIGBUS
SIGILL
SIGFPE
等信号处理中。那么在使用之前需要先了解在内核中,是如何处理信号的。
- 当内核需要向进程发送信号时,先分配一些所需要的信息添加到
struct task
结构体中,然后将signal-pending
置位。 - 在进程被调度到之前,内核会修改栈的结构,用来调用信号处理函数。
- 在用户态中,一般先进入的是 libc 的函数,然后才是用户定义的函数。
所以,在上述的调用栈中,实际上大致内容如下,真正的回调函数在 sigaction()
之前。
your_sig_handler()
sigaction() in libc.so
your_foobar()
假设有如下的报错:
[0]: ./a.out() [0x400772]
[1]: /lib64/libc.so.6(+0x35250) [0x7fc8014f7250]
[2]: ./a.out() [0x400824]
[3]: ./a.out() [0x40083a]
[4]: ./a.out() [0x400859]
[5]: /lib64/libc.so.6(__libc_start_main+0xf5) [0x7fc8014e3b35]
[6]: ./a.out() [0x400689]
可以通过 x
命令查看其调用地址的反汇编代码。
(gdb) x/20i 0x6f42a-30
BackTrace
在代码中,如果发生异常,直接 coredump 会导致文件过大,此时可以通过 backtrace
在代码中打印日志信息。
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <execinfo.h>
void log_backtrace(int signal)
{
(void) signal;
int i, frames;
void *callstack[128];
char **strs;
frames = backtrace(callstack, 128);
strs = backtrace_symbols(callstack, frames);
if (strs == NULL) {
printf("ERROR: backtrace_symbols failed, %s", strerror(errno));
exit(1);
}
for (i = 0; i < frames; ++i)
printf(" [%d]: %s\n", i, strs[i]);
free(strs);
exit(1);
}
int bar(void)
{
int *addr = NULL;
*addr = 3;
return 0;
}
int foo(void)
{
bar();
return 0;
}
int main(void)
{
signal(SIGSEGV, log_backtrace);
foo();
return 0;
}
符号表
直接通过 gcc -o test main.c
进行编译时,打印的信息不含具体是那个函数,都是 16 进制的地址,此时可以通过反汇编获取,不过比较麻烦。
可以通过 objdump -d test > test.s
命令直接反汇编。
此时可以在编译时添加 -rdynamic
参数,其作用是:
-rdynamic
Pass the flag -export-dynamic to the ELF linker, on targets that support it. This
instructs the linker to add all symbols, not only used ones, to the dynamic symbol
table. This option is needed for some uses of "dlopen" or to allow obtaining backtraces
from within a program.
另外,对于 C++ 其符号名是通过 mangle 之后的,为了获取具体的函数信息,需要 demangle 处理,例如:
c++filt "_Z16print_stacktracev"
当然,此时是无法查看函数是具体的哪一行的,可以利用 addr2line
查看,此时在编译时需要添加 -g
选项,例如:
addr2line -Cifp -a 0x400a29 -e test
-f
用来打印函数名,-C
同时 demangle 处理,-i
同时处理 inline 函数。
其它
LIB 库版本问题
通常是因为当前系统的依赖库版本太低,而软件编译时使用了较高版本库引起的,常见的有 glibc
pthread
等,一般报错如下。
/lib64/libc.so.6: version `GLIBC_2.14' not found
可以通过如下步骤进行排查。
----- 1. 当前系统支持版本
$ strings /lib64/libc.so.6 | grep GLIBC_
----- 2. 使用二进制文件依赖的版本
$ strings /your/binary/exec | grep GLIBC_
注意,如果在 strip
之前保存了符号表,实际上可以通过 nm BINARY
查看依赖库的版本号,不过 strip
之后则会丢失。