文章目录
- 一、调试程序
- 1、gdb 启动
- (1) 使用名称拉起进程
- (2) 附加到已运行进程
- (3) 先进gdb再attach运行中的进程
- 2、设置断点
- (1)按行号设断点
- (2) 按函数名设断点
- (3) 地址断点
- (4) 条件断点
- (5) 临时断点(命中一次后自动删除)
- (6) 正则表达式断点
- B、断点查看与管理
- 启用 / 禁用
- 删除断点
- C、观察点(Watchpoint)
- D、捕获点(Catchpoint)
- E、断点命令(命中后自动执行)
- 保存与恢复断点
- 七、实战技巧
- 1. 循环中跳过前 N 次
- 2. 条件断点 vs ignore
- 3. 断点命中后打印再继续
- 4. 在共享库未加载时设断点
- 5. 多线程断点
- 八、常用快捷键速查
- 九、注意事项
- 2、SIGPIPE 管道破裂信号
- (1) gdb 调试过程中为什么打印 SIGPIPE
- 1. 直接原因:程序收到了 `SIGPIPE` 信号
- 2. GDB 的机制:为什么会中断并打印后续信息?
- 3. 如何解决?
- (2) 另一个例子,管道破裂于continue
- 2、handle 定义 GDB 的信号拦截行为
- 核心参数
- 关于 `nopass` 与 `pass`
- 常见的实际应用
- 信号范围的指定
- 与之前 `SIGPIPE` 案例的关联
- 3、常见的 signal
- 4、gdb中的常用命令
- 二、调试Coredump
一、调试程序
1、gdb 启动
(1) 使用名称拉起进程
cd/work_dir gdb ./h530_daemon gdb h530_daemon(2) 附加到已运行进程
gdb-p1234gdb-p$(pgrep h530_daemon)gdb--pid$(pgrep h530_daemon)gdb--pid=$(pgrep h530_daemon)(3) 先进gdb再attach运行中的进程
pgrep h5u_autorun#查看PID 结果为1241gdb#启动gdb(gdb)attach1241#在gdb内 attach 运行中的进程2、设置断点
(1)按行号设断点
break文件:行breakfile.c:10#指定文件的行,简写 bb file.c:func#文件中某函数(2) 按函数名设断点
b 函数 b function_name#查找范围是全局所有符号,有同名函数时会提示让你选具体哪个b main(3) 地址断点
无符号时的唯一手段
b *0x4005d6#内存地址处b *main#main 函数入口地址(4) 条件断点
b funcifi==5#i等于5时中断b file.c:20ifx>100condition1i==5#给已存在的1号断点加条件condition1#查看1号断点条件condition1i==5#修改条件(5) 临时断点(命中一次后自动删除)
tbreak func tbreak file.c:20(6) 正则表达式断点
rbreak ^func_.*#匹配所有 func_ 开头的函数rbreak file.c:.*#文件中所有函数B、断点查看与管理
info breakpoints# 简写 info b,列出所有断点info b# 显示编号、类型、使能、地址、位置、命中次数输出字段说明:
| 字段 | 含义 |
|---|---|
| Num | 断点编号 |
| Type | breakpoint / watchpoint / catchpoint |
| Disp | 命中后是否保留(keep / del) |
| Enb | 是否启用(y / n) |
| Address | 内存地址 |
| What | 位置(文件:行号 或 函数) |
启用 / 禁用
disable1# 禁用1号断点disable# 禁用所有enable1enableonce1# 启用一次后自动禁用enabledelete1# 启用一次后自动删除删除断点
delete1# 删除1号断点delete# 删除所有(会提示确认)clearfunc# 删除函数处断点clearfile.c:20# 删除指定行断点C、观察点(Watchpoint)
监视变量/表达式的值变化:
watchvar# 变量被写时中断watchexpr# 表达式值变化时中断rwatch var# 变量被读时中断awatch var# 被读或被写时中断info watchpoints# 查看所有观察点注意:
watch在变量离开作用域后会自动删除;硬件观察点数量有限(通常4个)。
D、捕获点(Catchpoint)
捕获特定事件:
catch throw# C++ 抛异常catch catch# C++ 捕获异常catchexec# exec 调用catch fork# fork 调用catch vfork catch syscall# 系统调用catch syscallopen# 特定系统调用catch signal SIGINT# 信号catch load# 加载动态库catch unloadE、断点命令(命中后自动执行)
给断点绑定命令序列:
b func commands1# 为1号断点设置命令>print x>continue>end常用技巧:
commands1silent# 不打印断点命中信息printf"x=%d\n", xcontinueend保存与恢复断点
save breakpoints file.bp# 保存所有断点到文件sourcefile.bp# 恢复断点七、实战技巧
1. 循环中跳过前 N 次
b func ignore1100# 忽略前100次命中info breakpoints# 查看 ignore 次数2. 条件断点 vs ignore
- 条件复杂、命中稀疏 → 用
condition - 单纯跳过前N次 → 用
ignore(性能更好,GDB 不发信号)
3. 断点命中后打印再继续
b func commands silent bt3# 打印3层调用栈continueend4. 在共享库未加载时设断点
setbreakpoint pending on# 允许对未定义符号设断点b mylib_func# 库加载后自动生效5. 多线程断点
b func thread2# 只在2号线程中断b funcifthread_id==3八、常用快捷键速查
| 命令 | 简写 | 作用 |
|---|---|---|
| break | b | 设断点 |
| tbreak | tb | 临时断点 |
| info breakpoints | i b | 查看断点 |
| delete | d | 删除断点 |
| disable | dis | 禁用断点 |
| enable | en | 启用断点 |
| condition | cond | 条件断点 |
| ignore | — | 忽略N次 |
| watch | — | 观察点 |
| catch | — | 捕获点 |
| commands | — | 断点命令 |
九、注意事项
- 编译需带调试信息:
gcc -g -O0 - 断点行号会因优化错位:
-O2下行号可能不准,建议-O0 - 内联函数:
b inline_func可能失败,改用地址或加-fno-inline - 模板函数:需用完整实例化名,如
b std::vector<int>::push_back - 条件断点有开销:GDB 每次命中都要求值,大量循环会变慢
- 硬件断点:
hbreak使用硬件寄存器,数量有限但可用于只读内存 - pending 断点:库未加载时设的断点,
info b显示PENDING
一句话记忆:
b设断点,i b看断点,d删断点,dis/en开关断点,condition/ignore控命中,watch看变量,catch抓事件,commands自动跑。
2、SIGPIPE 管道破裂信号
(1) gdb 调试过程中为什么打印 SIGPIPE
这张截图展示了在使用 GDB 调试程序时,程序因为收到了SIGPIPE信号(管道破裂)而中断的现象。
以下是对“为什么打印这些”的详细解释,分为直接原因和GDB 的机制两部分:
1. 直接原因:程序收到了SIGPIPE信号
- 红框内容:
received signal SIGPIPE, Broken pipe.- SIGPIPE:这是一个信号(Signal)。在 Linux/Unix 系统中,当你试图向一个已经关闭了读取端的管道(Pipe)或 Socket写入数据时,系统就会向当前进程发送
SIGPIPE信号。 - 默认行为:
SIGPIPE的默认处理动作是终止进程。 - 触发场景:你的程序里有一个线程叫
monitor_tcp_cli,它正在操作网络连接(TCP)。这通常发生在:- 客户端(对端)已经关闭了连接(断开了 TCP)。
- 你的程序没有检测到这个断开,仍然尝试向这个连接
send()或write()数据。 - 内核发现管道/连接已断,于是抛出
SIGPIPE。
- SIGPIPE:这是一个信号(Signal)。在 Linux/Unix 系统中,当你试图向一个已经关闭了读取端的管道(Pipe)或 Socket写入数据时,系统就会向当前进程发送
2. GDB 的机制:为什么会中断并打印后续信息?
- GDB 的默认设置:
- 在使用 GDB 调试时,GDB 默认会捕获所有的信号。当程序运行中收到信号(包括
SIGPIPE),GDB 会暂停程序的执行,并把控制权交给你(调试者),让你决定是忽略该信号还是继续运行。
- 在使用 GDB 调试时,GDB 默认会捕获所有的信号。当程序运行中收到信号(包括
- 线程切换信息:
[Switching to Thread 0x41fc3a0 (LWP 19299)]- 由于 GDB 正在执行你输入的命令(看起来是在调用
statvfs),但此时另一个线程(monitor_tcp_cli)突然收到了信号。GDB 为了展示错误发生的位置,自动切换到了那个出问题的线程上下文。
- 由于 GDB 正在执行你输入的命令(看起来是在调用
- 蓝色文字部分:
The program received a signal in another thread while making a function call from GDB.- 这解释了为什么你原本想调用的函数(
statvfs)没有返回结果。因为 GDB 在“帮”你调用函数时,程序里的其他线程“炸”了(收到了信号),导致 GDB 放弃了当前的函数调用评估。 Evaluation of the expression containing the function (malloc) will be abandoned.- 这里提到了
malloc,说明你调用的statvfs内部或者 GDB 内部正在尝试分配内存,但因为信号中断,这个调用链被强行终止了。
3. 如何解决?
如果你不想让 GDB 在这种信号上停下来,或者想修复代码,有两种思路:
方法 A:在 GDB 中忽略该信号(临时调试用)
你可以告诉 GDB 不要捕获SIGPIPE,让程序按照操作系统的默认逻辑处理(或者程序自己处理)。在 GDB 中输入:
(gdb)handle SIGPIPE nostop noprint passnostop: 收到信号时不暂停程序。noprint: 不打印提示信息。pass: 把信号传递给程序处理(通常程序会忽略它或做错误处理)。
方法 B:修改代码(根本解决)
这是网络编程中的经典问题。在处理 TCP Socket 时,应该由程序自己处理SIGPIPE,而不是让它杀掉进程。
设置忽略 SIGPIPE(最简单):
在程序初始化时(或main函数开头)加入:#include<signal.h>signal(SIGPIPE,SIG_IGN);这样,当
write/send到断开的连接时,不会触发SIGPIPE信号,而是返回-1,并将errno设置为EPIPE。你需要检查send的返回值来处理错误。使用
MSG_NOSIGNAL标志(推荐):
在调用send函数时,加上MSG_NOSIGNAL标志:send(sockfd,buf,len,MSG_NOSIGNAL);这样可以避免发送
SIGPIPE信号,直接通过返回值报错。
总结:
截图显示的是你的程序在尝试向一个已经断开的 TCP 连接写数据,导致系统发出了“断开连接”的信号,GDB 捕获到这个信号后暂停了程序,并打断了你正在进行的手动函数调用。
(2) 另一个例子,管道破裂于continue
2、handle 定义 GDB 的信号拦截行为
handle 本质是:用 stop/print/pass 三个开关,为指定信号构建 GDB 拦截/转 发策略。
使用格式:
handle<信号范围><行为关键词>[<行为关键词>...]参数由三部分组成:
操作对象(信号范围):指定要配置哪些信号。
拦截策略(GDB 行为):GDB 自己该怎么处理这个信号(停不停、打不打印)。
转发策略(程序行为):要不要把这个信号告诉正在调试的程序(pass/nopass)。
handle命令用于告诉 GDB 如何处理各种信号(Signal)。你可以针对某个信号设定一系列“关键词”来控制 GDB 的行为,比如是否暂停程序、是否打印提示、是否把信号传递给正在调试的程序。
核心参数
你提到的nostop、noprint、pass就是控制这些行为的开关。它们可以自由组合,具体含义如下:
| 关键词 | 含义 |
|---|---|
stop | 收到该信号时,暂停程序,把控制权交给你调试。 |
nostop | 收到该信号时,不暂停程序,但 GDB 可能会打印一条提示信息。 |
print | 收到该信号时,在 GDB 中打印一条信息。 |
noprint | 收到该信号时,不打印任何信息。 |
pass | 允许程序看到这个信号。程序可以自行处理,也可能因未处理而终止。 |
nopass | 不允许程序看到这个信号。 |
ignore | 与nopass同义。 |
noignore | 与pass同义。 |
关于nopass与pass
你之前问到的nopass确实可以用,但正如我们分析过的,它会让程序“感觉不到”信号的存在,导致send/write等函数失败后,代码无法通过errno感知到EPIPE错误,从而陷入逻辑错乱。
而在信号调试场景中,pass才是让程序能“看到”信号并做出反应的关键。
常见的实际应用
在调试网络程序时,最常见的做法是将SIGPIPE的处理方式设置为nostop noprint pass,把SIGPIPE信号交给程序自己去处理(比如忽略它,或者检查send的返回值),而不是让 GDB 每次都中断下来。
信号范围的指定
除了直接指定单个信号,你还可以指定一个范围来处理多个信号,例如handle SIGIO-SIGKILL,或者使用all来设置所有信号。
与之前SIGPIPE案例的关联
对SIGPIPE来说,推荐在代码中用signal(SIGPIPE, SIG_IGN)或在send时加上MSG_NOSIGNAL来处理。GDB 中的handle SIGPIPE调试命令只是一个用来配合代码修改的临时手段,让你在调试时不会被信号中断打断。
按信号数值从小到大排序,并将“终止+核心”明确写为“终止 + Core Dump”后的完整表格如下:
3、常见的 signal
按信号数值从小到大排序,英文全称列去掉“Signal”后的完整表格如下:
| 值 | 信号名称 | 英文全称 | 类别 | 默认动作 | 说明 |
|---|---|---|---|---|---|
| 1 | SIGHUP | Hang Up | 控制/定时类 | 终止 | 控制终端挂起(用户登出)或控制进程结束 |
| 2 | SIGINT | Interrupt | 进程终止类 | 终止 | 从键盘发出的中断(通常由 Ctrl+C 触发) |
| 3 | SIGQUIT | Quit | 进程终止类 | 终止 + Core Dump | 从键盘发出的退出(通常由 Ctrl+\ 触发) |
| 4 | SIGILL | Illegal Instruction | 错误/异常类 | 终止 + Core Dump | 执行了非法硬件指令 |
| 6 | SIGABRT | Abort | 错误/异常类 | 终止 + Core Dump | 由 abort() 函数触发的非正常终止 |
| 7 | SIGBUS | Bus Error | 错误/异常类 | 终止 + Core Dump | 总线错误(如内存对齐问题) |
| 8 | SIGFPE | Floating-Point Exception | 错误/异常类 | 终止 + Core Dump | 浮点运算异常(如除以零) |
| 9 | SIGKILL | Kill | 进程终止类 | 终止 | 最强终止信号,无法被捕获、阻塞或忽略 |
| 10 | SIGUSR1 | User-defined 1 | 控制/定时类 | 终止 | 留给用户自定义用途 |
| 11 | SIGSEGV | Segmentation Violation | 错误/异常类 | 终止 + Core Dump | 无效的内存引用(段错误) |
| 12 | SIGUSR2 | User-defined 2 | 控制/定时类 | 终止 | 留给用户自定义用途 |
| 13 | SIGPIPE | Pipe | 错误/异常类 | 终止 | 向没有读取端的管道或套接字写入数据 |
| 14 | SIGALRM | Alarm | 控制/定时类 | 终止 | 来自 alarm() 函数的超时信号 |
| 15 | SIGTERM | Terminate | 进程终止类 | 终止 | 标准的终止请求,程序可捕获并执行清理 |
| 17 | SIGCHLD | Child | 控制/定时类 | 忽略 | 子进程结束或停止时通知父进程 |
| 18 | SIGCONT | Continue | 停止/继续类 | 继续 | 让暂停的进程继续执行 |
| 19 | SIGSTOP | Stop | 停止/继续类 | 停止 | 无条件停止进程,同样无法被忽略 |
| 20 | SIGTSTP | Terminal Stop | 停止/继续类 | 停止 | 从终端发出的停止信号(通常由 Ctrl+Z 触发) |
4、gdb中的常用命令
gdb ./a.out # 启动调试
(gdb) break main # 在 main 函数下断点
(gdb) run # 运行程序
(gdb) next # 执行下一行(不进入函数)
(gdb) step # 执行下一行(进入函数)
(gdb) print x # 打印变量 x 的值
(gdb) continue # 继续运行到下一个断点
(gdb) backtrace # 看调用栈(程序在哪崩溃的)
(gdb) quit # 退出