通常要调试某个程序,为了能清晰地看到调试的每一行代码、调用的堆栈信息、变量名和函数名等信息,须要调试程序含有调试符号信息。使用 gcc 编译程序时,若是加上 -g 选项便可在编译后的程序中保留调试符号信息。如下命令将生成一个带调试信息的程序 hello_world。redis
gdb -g -o hello_world hello_world.c
固然咱们能够经过gdb来判断程序是否带有调试信息:数组
gdb hello_world
若是gdb 加载成功之后,会显示以下信息:sass
Reading symbols from /root/testclient/hello_server...done
session
咱们也可使用 Linux 的 strip 命令移除掉某个程序中的调试信息。多线程
strip hello_world
调试时建议关闭编译器的程序优化选项,由于程序优化后调试显示的代码和实际代码可能就会有差别了,这会给排查问题带来困难。函数
gdb -g -O0 hello_world world_world.c
gdb hello_world // gdb + 程序名
当一个程序已经启动,咱们想调试这个程序,但又不想重启这个程序时,能够经过使用 gdb attach 进程ID 来将gdb调试器附加到想要调试的程序上。优化
gdb attach 进程ID
当⽤ gdb attach 上⽬标进程后,调试器会暂停下来,此时可使⽤ continue 命令让程序继续运⾏,或者加上相应的断点再继续运⾏程序。当调试完程序想结束这次调试时,⽽且不对当前进程有任何影响,能够在 GDB 的命令⾏界⾯输⼊ detach 命令 让程序与 GDB 调试器分离。ui
(gdb) detach
Linux 系统默认是不开启程序崩溃产⽣ core ⽂件这⼀机制的,咱们可使⽤ ulimit -c 命令来查看系统是否开启了这⼀机制。使用ulimit -c unlimited 直接将core文件的大小修改为不限制大小。而后就能够经过如下命令调试core文件:spa
gdb filename corename
经过调试core文件能够看到程序崩溃的地方,使用bt命令查看崩溃时的调用堆栈,进一步分析找到崩溃的缘由。当有多个程序崩溃时,有时很难经过core文件的名称来判断对应的core文件。咱们能够本身修改core文件的名称来解决该问题。经过修改/proc/sys/kernel/core_uses_pid
能够控制产生的 core 文件的文件名,修改方式以下:命令行
echo "/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
文件名各个参数的说明以下:
参数名称 | 参数含义(中文) |
---|---|
%p | 添加 pid 到 core 文件名中 |
%u | 添加当前 uid 到 core 文件名中 |
%g | 添加当前 gid 到 core 文件名中 |
%s | 添加致使产生 core 的信号到 core 文件名中 |
%t | 添加 core 文件生成时间(UNIX)到 core 文件名中 |
%h | 添加主机名到 core 文件名中 |
%e | 添加程序名到 core 文件名中 |
假设如今的程序叫 test,咱们设置该程序崩溃时的 core 文件名以下:
echo "/root/testcore/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
那么最终会在 /root/testcore/ 目录下生成的 test 的 core 文件名格式以下:
-rw-------. 1 root root 409600 Jan 14 13:54 core-test-13154-1547445291
命令名称 | 命令缩写 | 命令说明 |
---|---|---|
run | r | 运行一个程序 |
continue | c | 让暂停的程序继续运行 |
next | n | 运行到下一行 |
step | s | 若是有调用函数,进入调用的函数内部,至关于 step into |
until | u | 运行到指定行停下来 |
finish | fi | 结束当前调用函数,到上一层函数调用处 |
return | return | 结束当前调用函数并返回指定值,到上一层函数调用处 |
jump | j | 将当前程序执行流跳转到指定行或地址 |
p | 打印变量或寄存器值 | |
backtrace | bt | 查看当前线程的调用堆栈 |
frame | f | 切换到当前调用线程的指定堆栈,具体堆栈经过堆栈序号指定 |
thread | thread | 切换到指定线程 |
break | b | 添加断点 |
tbreak | tb | 添加临时断点 |
delete | del | 删除断点 |
enable | enable | 启用某个断点 |
disable | disable | 禁用某个断点 |
watch | watch | 监视某一个变量或内存地址的值是否发生变化 |
list | l | 显示源码 |
info | info | 查看断点 / 线程等信息 |
ptype | ptype | 查看变量类型 |
disassemble | dis | 查看汇编代码 |
set args | 设置程序启动命令行参数 | |
show args | 查看设置的命令行参数 |
前面说的 gdb filename 命令只是附加的一个调试文件,并无启动这个程序,须要输⼊ run 命令(简写为 r)启动这个程序。
当 GDB 触发断点或者使⽤ Ctrl + C 命令中断下来后,想让程序继续运⾏,只要输⼊ continue 命令便可(简写为 c)。
break 命令(简写为 b)即咱们添加断点的命令,可使⽤如下⽅式添加断点:
backtrace 命令(简写为 bt)⽤来查看当前调⽤堆栈。查看调用的堆栈信息后可使⽤ frame + 堆栈编号 命令(简写为 f),切换⾄指定堆栈顶部。
在程序中加了不少断点,⽽咱们想查看加了哪些断点时,可使⽤ info break 命令(简写为 info b)。
(gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000423450 in main at server.c:3709 breakpoint already hit 1 time 2 breakpoint keep y 0x000000000049c1f0 in _redisContextConnectTcp at net.c:267
由上面的内容片断能够知道,目前一共增长了2个断点,断点1触发1次,断点2未触发过。咱们想禁⽤某个断点时,使⽤“ disable 断点编号 ”就能够禁⽤这个断点了,同理,被禁⽤的断点也可使⽤“ enable 断点编号 ”从新启⽤。使⽤“delete 编号”能够删除某个断点,若是输⼊ delete 不加命令号,则表示删除全部断点。
第⼀次输⼊ list 命令会显示断点处先后的代码,继续输⼊ list 指令会以递增⾏号的形式继续显示剩下的代码⾏,⼀直到⽂件结束为⽌。固然 list 指令还能够往前和日后显示代码,命令分别是“list + (加号) ”和“list - (减号) ”。
经过 print + 变量名 能够打印出指定变量的值,print 命令也能够显示进⾏⼀定运算的表达式计算结果值,甚⾄能够显示⼀些函数的执⾏结果值。举个例子,咱们可使用 p a+b+c 来打印这三个变量的结果值;也可使用 p func() 命令输出一个可执行函数 func() 的执行结果。
print 命令不只能够输出表达式结果,同时也能够修改变量的值,咱们尝试将端⼝号从 6379 改为 6400 试试:
(gdb) p server.port=6400 $24 = 6400 (gdb) p server.port $25 = 6400 (gdb)
ptype 命令,其含义是“print type”,就是输出⼀个变量的类型。
⽤ info thread命令来查看当前进程有哪些线程,分别中断在何处。
(gdb) info thread Id Target Id Frame 4 Thread 0x7fffef7fd700 (LWP 53065) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 3 Thread 0x7fffefffe700 (LWP 53064) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 2 Thread 0x7ffff07ff700 (LWP 53063) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0 * 1 Thread 0x7ffff7fec780 (LWP 53062) "redis-server" 0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
经过 info thread 的输出能够知道 redis-server 正常启动后,⼀共产⽣了 4 个线程,包括⼀个主线程和三个⼯做线程,线程编号(Id 那⼀列)分别是 四、 三、 二、 1。三个⼯做线程(二、 三、 4)分别阻塞在 Linux API pthread_cond_wait 处,⽽主线程(1)阻塞在 epoll_wait 处。当有多个线程时,咱们可使用 backtrace 命令查看调用堆栈,经过过堆栈判断 GDB 做用在哪一个线程上面。如何切换到其余线程呢?能够经过“thread 线程编号”切换到具体的线程上去。例如,想切换到线程 2 上去,只要输⼊ thread 2 便可。
info 命令还能够⽤来查看当前函数的参数值,组合命令是 info args。
next 命令(简写为n)是让 GDB 调到下⼀条命令去执⾏,这⾥的下⼀条命令不⼀定是代码的下⼀⾏,⽽是根据程序逻辑跳转到相应的位置。这⾥有⼀个⼩技巧,在 GDB 命令⾏界⾯若是直接按下回⻋键,默认是将最近⼀条命令从新执⾏⼀遍,所以,当使⽤ next 命令单步调试时,没必要反复输⼊ n 命令,直接回⻋就能够了。
step 命令(简写为 s)就是“单步步⼊”(step into),顾名思义,就是遇到函数调⽤,进⼊函数内部。
finish 命令会执⾏函数到正常退出该函数;⽽ return 命令是⽴即结束执⾏当前函数并返回,也就是说,若是当前函数还有剩余的代码未执⾏完毕,也不会执⾏了。
until 命令(简写为 u)能够指定程序运⾏到某⼀⾏停下来。好比直接输入 u 1888,就能够快速执行完中间的内容,直接跳到1888行。固然也可使用断点的方式,可是使用until命令会更便捷。
不少程序须要咱们传递命令⾏参数。在 GDB 调试中,不少⼈会以为可使⽤ gdb filename args 这种形式来给 GDB 调试的程序传递命令⾏参数,这样是不⾏的。正确的作法是在⽤ GDB 附加程序后,在使⽤ run 命令以前,使⽤“ set args 参数内容 ”来设置命令⾏参数
若是单个命令⾏参数之间含有空格,可使⽤引号将参数包裹起来。
(gdb) set args "999 xx" "hu jj" (gdb) show args Argument list to give program being debugged when it is started is ""999 xx" "hu j j"". (gdb)
若是想清除掉已经设置好的命令⾏参数,使⽤ set args 不加任何参数便可。
(gdb) set args (gdb) show args Argument list to give program being debugged when it is started is "". (gdb)
tbreak 命令也是添加⼀个断点,第⼀个字⺟“t”的意思是 temporarily(临时的),也就是说这个命令加的断点是临时的,所谓临时断点,就是⼀旦该断点触发⼀次后就会⾃动删除。添加断点的⽅法与上⾯介绍的 break命令⼀模⼀样,这⾥再也不赘述。
watch 命令是⼀个强⼤的命令,它能够⽤来监视⼀个变量或者⼀段内存,当这个变量或者该内存处的值发⽣变化时, GDB 就会中断下来。被监视的某个变量或者某个内存地址会产⽣⼀个 watch point(观察点)。
display 命令监视的变量或者内存地址,每次程序中断下来都会⾃动输出这些变量或内存的值。例如,假设程序有⼀些全局变量,每次断点停下来我都但愿 GDB 能够⾃动输出这些变量的最新值,那么使⽤“ display变量名 ”设置便可。
当使⽤ print 命令打印⼀个字符串或者字符数组时,若是该字符串太⻓, print 命令默认显示不全的,咱们能够经过在 GDB 中输⼊ set print element 0 命令设置⼀下,这样再次使⽤ print 命令就能完整地显示该变量的全部字符串了。
void prog_exit(int signo) { std::cout << "program recv signal [" << signo << "] to exit." << std::endl; } int main(int argc, char* argv[]) { //设置信号处理 signal(SIGCHLD, SIG_DFL); signal(SIGPIPE, SIG_IGN); signal(SIGINT, prog_exit); signal(SIGTERM, prog_exit); int ch; bool bdaemon = false; while ((ch = getopt(argc, argv, "d")) != -1) { switch (ch) { case 'd': bdaemon = true; break; } } if (bdaemon) daemon_run(); //省略⽆关代码... }
在这个程序中,咱们接收到 Ctrl + C 信号(对应信号 SIGINT)时会简单打印⼀⾏信息,⽽当⽤ GDB 调试这个程序时,因为 Ctrl + C 默认会被 GDB 接收到(让调试器中断下来),致使⽆法模拟程序接收这⼀信号。解决这个问题有两种⽅式:在 GDB 中使⽤ signal 函数⼿动给程序发送信号,这⾥就是 signal SIGINT;改变 GDB 信号处理的设置,经过 handle SIGINT nostop print 告诉 GDB 在接收到 SIGINT 时不要停⽌,并把该信号传递给调试⽬标程序 。
(gdb) handle SIGINT nostop print pass SIGINT is used by the debugger. Are you sure you want to change it? (y or n) y Signal Stop Print Pass to program Description SIGINT No Yes Yes Interrupt (gdb)
假设如今有 5 个线程,除了主线程,⼯做线程都是下⾯这样的⼀个函数:
void thread_proc(void* arg) { //代码⾏1 //代码⾏2 //代码⾏3 //代码⾏4 //代码⾏5 //代码⾏6 //代码⾏7 //代码⾏8 //代码⾏9 //代码⾏10 //代码⾏11 //代码⾏12 //代码⾏13 //代码⾏14 //代码⾏15 }
为了能说清楚这个问题,咱们把四个⼯做线程分别叫作 A、 B、 C、 D。假设 GDB 当前正在处于线程 A 的代码⾏ 3 处,此时输⼊ next 命令,咱们指望的是调试器跳到代码⾏ 4 处;或者使⽤“u 代码⾏10”,那么咱们指望输⼊ u 命令后调试器能够跳转到代码⾏ 10 处。可是在实际状况下, GDB 可能会跳转到代码⾏ 1 或者代码⾏ 2 处,甚⾄代码⾏ 1三、代码⾏ 14 这样的地⽅也是有可能的,这不是调试器 bug,这是多线程程序的特色,当咱们从代码⾏ 4 处让程序 continue 时,线程A 虽然会继续往下执⾏,可是若是此时系统的线程调度将 CPU 时间⽚切换到线程 B、 C 或者 D 呢?那么程序最终停下来的时候,处于代码⾏ 1 或者代码⾏ 2 或者其余地⽅就不奇怪了,⽽此时打印相关的变量值,可能就不是咱们须要的线程 A 的相关值。
为了解决调试多线程程序时出现的这种问题, GDB 提供了⼀个在调试时将程序执⾏流锁定在当前调试线程的命令: set scheduler-locking on。固然也能够关闭这⼀选项,使⽤ set scheduler-locking off。
所谓条件断点,就是满⾜某个条件才会触发的断点,这⾥先举⼀个直观的例⼦
void do_something_func(int i) { i ++; i = 100 * i; } int main() { for(int i = 0; i < 10000; ++i) { do_something_func(i); } return 0; }
在上述代码中,假如咱们但愿当变量 i=5000 时,进⼊ do_something_func() 函数追踪⼀下这个函数的执⾏细节。添加条件断点的命令是 break [lineNo] if [condition],其中 lineNo 是程序触发断点后须要停下的位置, condition 是断点触发的条件。这⾥能够写成 break 11 if i==5000,其中, 11 就是调⽤ do_something_fun() 函数所在的⾏号。固然这⾥的⾏号必须是合理⾏号,若是⾏号⾮法或者⾏号位置不合理也不会触发这个断点。
在实际的应⽤中,若有这样⼀类程序,如 Nginx,对于客户端的链接是采⽤多进程模型,当 Nginx 接受客户端链接后,建立⼀个新的进程来处理这⼀路链接上的信息来往,新产⽣的进程与原进程互为⽗⼦关系,那么如何⽤ GDB 调试这样的⽗⼦进程呢?⼀般有两种⽅法:⽤ GDB 先调试⽗进程,等⼦进程 fork 出来后,使⽤ gdb attach 到⼦进程上去,固然这须要从新开启⼀个 session 窗⼝⽤于调试, gdb attach 的⽤法在前⾯已经介绍过了;GDB 调试器提供了⼀个选项叫 follow-fork,可使⽤ show follow-fork mode 查看当前值,也能够经过set follow-fork mode 来设置是当⼀个进程 fork 出新的⼦进程时, GDB 是继续调试⽗进程仍是⼦进程取值是 child),默认是⽗进程( 取值是 parent)。