一般要调试某个程序,为了能清晰地看到调试的每一行代码、调用的堆栈信息、变量名和函数名等信息,需要调试程序含有调试符号信息。使用 gcc 编译程序时,如果加上 -g 选项即可在编译后的程序中保留调试符号信息。以下命令将生成一个带调试信息的程序 hello_world。
gdb -g -o hello_world hello_world.c
当然我们可以通过gdb来判断程序是否带有调试信息:
gdb hello_world
如果gdb 加载成功以后,会显示如下信息:
Reading symbols from /root/testclient/hello_server...done
我们也可以使用 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 调试器分离。
(gdb) detach
Linux 系统默认是不开启程序崩溃产? core ?件这?机制的,我们可以使? ulimit -c 命令来查看系统是否开启了这?机制。使用ulimit -c unlimited 直接将core文件的大小修改成不限制大小。然后就可以通过以下命令调试core文件:
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 那?列)分别是 4、 3、 2、 1。三个?作线程(2、 3、 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 处,甚?代码? 13、代码? 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)。
原文:https://www.cnblogs.com/lizhimin123/p/10416975.html