1 core文件的配置
默认情况下,如果程序崩溃了是不会生成core文件的,因为生成core文件受到系统ulimit配置的影响。
ulimit -c是core文件的大小,默认为0,因此,就不会生成core文件,因此,为了能够生成core文件,可以使用ulimit -c filesize命令使得程序在崩溃时能够生成core文件。
默认情况下,会在崩溃的程序所在的路径下生成core文件,当然也可以通过修改/proc/sys/kernel/core-pattern将core文件放到某个路径下。
2 gdb查看堆栈
当有了core文件后,就可以用gdb查看core文件,得到core的堆栈信息。
gdb binary core.11111
binary就是执行时崩溃的二进制程序,core.11111就是崩溃时产生的core文件。
然后执行bt命令就可以查看程序崩溃时的堆栈,并定位到具体崩溃的业务代码。
3 breakpoint & watchpoint
在使用gdb进行调试时,通常需要使用断点,即breakpoint,在使用gdb运行程序时,可以执行以下命令增加断点:
b func_name
然后使用info b就可以查看断点信息,注意,如果程序需要加载其他的so,此时还拿不到so中的符号信息,gdb会提示:Make breakpoint pending on future shared library load?
使用breakpoint可以让程序运行到某个地方暂停,然后查看此时的变量或者内存的情况,但是,有时候,某个变量由于某个bug导致变成了非预期的值,就可以使用watchpoint对变量进行监控,当变量的值变化时程序就会暂停。
如果需要对变量进行监控,那gdb肯定是需要有该变量的信息,监控变量相当于监控某块内存空间,是需要程序启动才能添加watchpoint的,可以在程序最开始打breakpoint,当程序暂停时再打watchpoint:
w variable_name
3.1 breakpoint
通过break命令添加断点时,有三种方式可以指定断点所在的地方:
- 指定函数名或者类的方法名,例如,某个文件中的函数是Person:Eat(),那么可以使用
b Person:Eat
在这个函数的入口处加断点 - 指定所在的文件以及行号,例如,某个源文件person.cpp的100行,那么可以使用
b person.cpp:100
在这里加断点,唯一需要注意的是文件名是用全路径还是相对路径,如果不好确定,可以先用函数名的方式加断点,然后运行,当程序暂停时,gdb会打印暂停的文件名和行号,文件名可以从这里取
除了常规的断点,还有两种特殊的断点:
- 临时断点:tbreak ,只会在这里停止一次,之后断点自动删除
- 条件断点:break if ,当条件满足时停止
临时断点和条件断点的结合:
比如某个采集进程信息的程序,需要在程序启动后,查看某个特定的进程,那么,可以先用tbreak让程序在比较靠前的位置停止,当程序停止时,再执行用例,然后用条件断点,让程序在收到该进程信息时停止。
4 debug vs release
debug和release是程序发布的两种模式,两者的主要区别是:
- debug版本包含调试信息,即编译时会带上-g选项,而release不包含调试信息
- debug版本是默认的优化级别,也就是编译时不带-O选项,而release为了性能的考虑,会用较高的优化级别,一般会带上-O选项
- debug版本不包含壳,release版本会加壳,是为了对程序进行加密和反混淆,提高程序被破解的难度
当使用gdb进行调试时,如果不加-g的话,程序就不带调试信息,例如,变量的名称和文件名,那么在用gdb进行调试时,就无法通过函数或者变量名添加断点,因此,在使用gdb进行调试之前编译选项需要加上-g。
5 break_pad的使用
break_pad是Google提供的一款可以上报coredump文件的工具,客户端程序可以集成该库,在程序core时,获取当前堆栈信息,可以用于在不方便登录到这些环境只能远程分析的场景。
5.1 升级gcc
5.2 breakpad的简单使用
git clone https://github.com/google/breakpad.git
cd breakpad
git clone https://github.com/getsentry/linux-syscall-support.git src/third_party/lss
然后在breakpad目录执行./configure&&make进行编译,然后在src目录下编写以下程序:
#include "client/linux/handler/exception_handler.h"
static bool dumpCallback(const google_breakpad::MinidumpDescriptor& descriptor, void*context, bool succeeded) {
printf("Dump path: %s\n", descriptor.path());
return succeeded;
}
void crash() {
volatile int* a = (int*)NULL;
*a = 1;
}
int main() {
google_breakpad::MinidumpDescriptor descriptor("/tmp");
google_breakpad::ExceptionHandler eh(descriptor, NULL, dumpCallback, NULL, true, -1);
crash();
return 0;
}
执行以下命令进行编译:
cp client/linux/libbreakpad_client.a /usr/local/lib
g++ test.c -o test -lbreakpad_client -I./
然后执行./test就可以在/tmp目录下生成dump文件,那么在实际中如何使用breakpad呢?
5.3 breakpad的实际使用
这里又要提到上面的debug和release:debug是开发和功能测试过程中用的构建模式,编译时会带上-g选项,使用默认的优化级别_O0,release是性能测试和实际发布的构建模式,编译时会去掉-g选项,并使用高优化级别,例如-O2,同时,为了反逆向,还会对二进制进行加壳操作,提高外部对二进制分析的门槛。
# 生成debug程序
g++ test.c -g -o test.debug -lbreakpad_client -I./
# 生成release程序
g++ test.c -O2 -o test.release -lbreakpad_client -I./
下面要用到dump_syms(位于breakpad/src/tools/linux/dump_syms/dump_syms),该工具可以将二进制的符号导出来用于后续分析。
./tools/linux/dump_syms/dump_syms test.release > test.sym
当程序使用release模式进行编译后,还需要对二进制进行strip去掉里面的符号,一方面可以减小二进制的大小,另一方面当然也是防止程序被逆向。
以上面的程序为例,对生成的二进制进行strip:
strip -o test.user test.release
会发现,进行strip前,test.release二进制大小为1.6M,进行strip后,test.user二进制大小为107K。
现在为止,我们就有两个文件:其中test.user是不带调试信息并且去掉符号信息的二进制,在进行一些加密的操作后就可以给用户使用,另一个是test.sym符号文件,可以用于后续分析。
下一步就是执行test.user,此时会在/tmp目录下生成dmp文件,下面对该文件进行分析,假设生成的dump文件路径为:/tmp/d5f4ae04-2bdd-4460-a63bff89-0790bde8.dmp。
先使用minidump_stackwalk工具将dump文件和符号文件进行展开:
./processor/minidump_stackwalk /tmp/d5f4ae04-2bdd-4460-a63bff89-0790bde8.dmp ./test.sym > dump.result
查看dump.result文件,然后使用addr2line得到地址所在的程序的地方:
addr2line -f -C -e test.debug 0x405f
但是会发现,在release版本的情况下,由于加了优化级别,通过地址查不到具体导致crash的代码行,而用debug版本的情况下,是可以定位到具体报错的行。
6 定位容器中的core
如果在宿主机上用gdb定位容器中产生的core文件,会出现找不到so的问题,比如:
一方面,可能宿主机本地确实没有这些so,另一方方面,容器中使用的so跟宿主机的也可能不同。
gdb下面的提示也给出了解决办法:
- set solib-search-path:设置so的搜索路径
- set sysroot:设置so的根路径
将命令放到~/.gdbinit中:
set sysroot /root/gdb_root
将容器中对应路径的so拷贝到/root/gdb_root中,例如,当缺少/usr/lib/libgcc_s.so.1时,就将容器中的/usr/lib/libgcc_s.so.1放到宿主机的/root/gdb_root/usr/lib/libgcc_s.so.1,然后用gdb开启调试,此时就可以读取到对应的so,但是这种方式就需要将依赖的so都下载放到这里。
而另一个命令,set solib-search-path
则适合用于会通过路径去查找so的场景,如果有时候就是查找特定路径的so,则该命令没啥用。