%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # 二进制文件的调试信息 编译 C/C++ 程序时需要带有 **`-g` 选项**(`debug` 模式):**将==调试信息==加入到可执行文件中**, 从而使用 gdb 调试时才能**看见程序的函数名、局部变量名**等,否则显示的将**全是运行时的==内存地址==**。 > [!NOTE] 用 `gdb` 打开二进制文件后,可判断该可执行文件中是否包含 **==调试信息==(debugging symbols)** > > (1)无调试信息 > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-7442928D06F39C0120CCC7265920DCBE.png|547]] > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-DE8CEAF997631EE439E19C825F06621F.png|471]] > > (2)有调试信息 (编译时带有`-g`选项) > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-D8708F1C2C5F237DCD8CB8931519D241.png|543]] > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-4340518DDD85BCF7426C16DA0B67AEE1.png|460]] > > > [!caution] 对于未包含调试信息的==外部库函数==, gdb 无法进入这些函数调用内部! <br><br><br> # GDB 调试器 **gdb**,全称为**GNU Debugger**,Linux/Unix 的一个 **==命令行模式==的调试器**,主要用于**调试 C/C++ 程序**。(也可用于 Go、Java、Objective-C 等) gdb 的主要功能: - **启动程序,暂停在指定位置**: - 可以指定程序在某个特定的起点或特定的条件下暂停执行。 - **设置断点**: - 可以在特定的代码行、函数或条件满足时暂停程序执行。 - **单步执行**: - 可以逐行或逐指令地执行代码,便于精确定位问题。 - **查看程序状态**: - 可以查看变量、寄存器、内存等程序内部状态,帮助分析程序行为。 - **修改程序状态**: - 可以在调试时修改变量的值、改变程序的执行路径等,测试不同的代码路径。 - **调试核心转储文件**: - 可以加载和分析程序崩溃时生成的核心转储文件,了解程序崩溃前的状态。 <br> # gdb 运行选项参数 - `gdb [OPTIONS] prog`: 启动 gdb 并加载程序 `[prog]` - `-s file`:读取 file 的**符号表** - `--args prog [arglist]`:**以参数 `[arglist]` 运行程序**(此项必须放在最后,由此**最后的参数列表都是传递给程序而不是 gdb**) - `-c file` | `--core=file`: **读取 core dump 文件进行分析** - `-p <pid>`:附加到 **正在==运行中==的进程** 进行调试 > [!NOTE] `sudo gpt -p <pid>` 等价于进入 gdb 后执行内部命令 `attach <pid>`。 > > 背后实现是系统调用 `ptrace(PTRACE_ATTACH, pid, NULL, NULL)`, > 会**向目标进程发送 ==`SIGSTOP` 信号==**,**暂停目标进程**,使其变为`TASK_TRACED` 状态。 > > gd 通过 `waitpid()` 来**等待目标进程暂停**。 <br> # gdb 内置命令 > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-1158774FD44DF5FE41C06D8BBA853728.png|656]] - `quit`:退出 gdb ### 设置程序运行参数 - `set agrs <arg>...`: 设置可执行程序的运行参数,**设置完成后再通过 `run` 启动可执行程序**。 ```shell (gdb) set args arg1 arg2 arg3 (gdb) run ``` ### 代码查看 - **查看源代码**:`list | l [LINE]>` ,列出**当前执行点 or 指定行 `LINE` 附近的 10 行代码** - `l`(无参数):向后翻页—**列出 "上一次所列代码" ==之后==的代码**(默认 10 行) - `l -`: 向前翻页—**列出 "上一次所列代码" ==之前==的代码**(默认 10 行) - `[LINE]`:目标行可按以下形式给出: - `LINENUM`:**当前文件中的行号** - `FUNCTION`:**当前文件中的函数** - `FILE:LINENUM`:**指定文件中的行号** - `FILE:FUNCTION`:**指定文件中的函数** - `*ADDRESS`:**包含该地址指令的代码行** > [!tip] 使用一次 `f` 命令后,再使用 `l` 会重新定位回到 "当前暂停位置所对应的代码行" ### 反汇编代码 - **反汇编代码**:`disas [地址|函数名]` - `disas`(无参数):**反汇编当前函数** - `disas [函数名]`:反汇编**指定函数** - `disas [地址]`:反汇编**位于指定地址附近的函数** - `disas [起始地址, 终止地址]`: 反汇编**指定地址范围内的代码** ### 执行相关 - **运行程序/终止运行** :`run|r`,`kill` - **单步调试**(逐 **==语句==**,会进入函数调用内部):`step | s [N]` - 参数`[N]`: **执行 N 步**,默认**单步** - **单步调试**(逐 **==过程==**,不进入函数内部):`next | n [N]` - 参数`[N]`: **执行 N 步**,默认**单步** - **单步调试**(逐 **==汇编指令==**,会进入函数调用内部):`stepi | si [N]`: - 参数`[N]`: **执行 N 步**,默认**单步** - **单步调试**(逐 **==汇编指令==**,但不进入函数内部):`nexti | ni [N]` - 参数`[N]`: **执行 N 步**,默认**单步** - **继续运行,直至==遇见断点==**: `continue | c [N]`: - 参数`[N]`:表示将**跳过 `N-1` 个断点**,直至遇见第 N 个断点时才暂定 - **继续运行,直至==当前函数返回==**:`finish | fin` ### 当前代码暂停位置 > [!tip] `f` 命令:查看当前所位于的代码位置——语句粒度 > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-9F2FBCF36ED6DC9FBE2B80EAE83A51AF.png|506]] > > > [!NOTE] `x/i $pc`:查看**当前 "待执行" 的下一条指令**(即程序计数器 `pc` 中的指令)——机器指令粒度 > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-80299C709AFC7DEDDBE18E2C11D92046.png|505]] ### 断点相关 - **查看所有断点**:`info breakpoints`,简写为 `i b` - **设置断点**: `break | br | b [位置]` - `b [行号|函数名]` - `b *<地址>`:在指定地址处设置断点 - `b 文件名:[行号|函数名]:标签`,适用于有多个文件的情况 - **设置观察点**:`watch [变量名]` - 当**指定变量的值发生变化时触发断点** - **删除断点/观察点**: `delete | del | d [断点序号...]` - `d` :删除所有断点 - `d watchpoints` :删除所有观察点 - `d <n>`: 删除断点 `n`(按断点编号), - **启用断点**:`<enable|en> b [n]` 启用全部 or 编号为 `n` 的断点 - **禁用断点**:`<disable|dis> b [n]` 禁用全部 or 编号为 `n` 的断点 > [!example] > > ```shell > # 设置断点 > b main # 在main函数设置端点 > b *main+4 # 在main函数之后的第四个字节地址处设置断点 > b *0x400540 # 在地址0x400540处设置断点 > b factorial.c:fact:the_top # 在"factorial.c"文件中的"fact"函数中的"the_top"标签处设置断点 > > # 删除断点 > d # 删除所有断点 > d 1 # 删除序号为1的断点 > d 1 2 4 # 删除序号为1,2,4的三个断点 > > # 禁用/启用断点 > dis b 2 # 禁用序号为2的断点 > en b 2 # 禁用序号为1的断点 > ``` > ### 调用栈相关 - 查看**函数调用栈**:`backtrace | bt` - `bt full` :显示**各调用栈下的完整局部变量信息** - **切换到特定栈帧**:`frame|f <栈帧号>` - **向上移动一个栈帧**:`up` - **向下移动一个栈帧**:`down` - 查看当前 **函数栈帧** 信息: - `info frame`,简写为 **`i f`**,同 `frame | f` 命令 - `info args`:查看当前栈帧内的**所有参数** - `info locals`:查看当前栈帧内的**所有局部变量** > [!note] > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-C4588157081F047DCC881080E8006F82.png|780]] ### 变量相关 - **查看变量值**:`print|p <变量名>` - **查看所有==全局变量==值**:`info variables` - **查看所有==局部变量==值**:`info locals` - **设置变量值**:`set var <变量名> = <值>`(在调试过程中修改变量值) - 设置指针所指内存处值:`set *ptr = 30`; - **自动打印变量值**:`display <变量名>` (设置后,每次程序暂停时都会**打印变量的值**,可重复该命令设置多个变量的值) - **移除自动打印**: `undisplay <Number>` - **查看当前 `display` 列表**:`info display` ### 类型相关 - 查看变量或类类型定义:`ptype <变量名/类型名>` > [!example] > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-F19DFE8B08C7CDEF4A5D78BDA6B16215.png|723]] ### 查看数据值 - **查看值**:`print|p [/t|/x|/d] <寄存器名 | 变量名 | 立即数 | 指定地址>` - `/t`:以二进制输出 - `/x`:以十六进制输出 - `/d`:以十进制输出 > [!example] > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-CBAB545A9D03FBD6CB644D31D43C30DF.png|539]] > ### 查看内存内容 `x/[NFS] <地址 | 函数名 | 指针名>` 查看**从==指定内存地址起始==的==指定字节数==的内容**: - `N`:所要查看/显示的 **==单元==数量**(默认为 1) - `F`:指定 **==单元==的显示格式**(默认) - `o` :八进制; `x`:十六进制;`z`:十六进制且补前导 0; `d`:十进制;`b`:二进制 - `f`:浮点数;`a`:地址; `i`:指令;`c`:字符;`s`:字符串 - `S`:指定**单元的==单位==**(以多少字节为一个 **==显示单元==**) - `b`:字节; - `h`:半字(2 字节); - `w`:**==字==**(**==4 字节==**——注意,这里 gdb 的该项参数实际上是 **DWORD 双字 32 位 4 字节**,而不是 16 位 2 字节); - `g` :**8 字节**(giant word) > [!example] > > - `x/14xb func` 表示查看**函数 `func` 地址起始的 14 个字节内容**,以 16 进制格式显示。 > > > > > - `x/s 0x402378`: 查看**内存地址** `0x402378` 所存的 **"==字符串=="内容**(内存内容按 "字符串" 解释) > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-A782D876CC5C170D03EDCA1430E36E52.png|512]] > > > - `x/i 0x402470`:查看**内存地址** `0x402470` 处的所存的 "**==指令==**"(内存内容按 "指令" 解释) > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-41533553C78538708D4EC1FF8670AE90.png|324]] > > - `x/i $pc` 或 `x/i $rip`:查看**程序计数器 PC**(也即指令**指针寄存器 `%rip`**)中所存的 "**==下一条待执行指令==**" > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-26EAE315E446E29F7A63054CE118F197.png|420]] > > > - `x/a 0x402470`:查看**内存地址** `0x402470` 处所存的 "**==地址==**"(内存内容按 "地址" 解释) > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-5A7D59DE68B4E7435B74831323FC83BB.png|387]] > > > - `x/w $rbx`:以 4 字节为一个单元,查看**寄存器 `%rax`中 ==所存地址==** 处的 **==内存==内容** > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-F4555F54370357C260E9AA07BE2DD396.png|346]] > > - `x/6w $rsp`:查看当前**栈指针`%rsp`** 处起始的 **6 个 4 字节数据值**(`%rsp` ~ `%rsp+0x14`) > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-245B2894DF44362F9BA5CD5C599D341B.png|372]] > > - `x/10s ptr - 10:`: 查找指针 `ptr` 所指地址负偏移 100 字节后的 10 个字符串内容 > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-E9BDC02AC27BEAA22A4D0D5E3A926690.png|665]] > ### 寄存器相关 - **查看所有寄存器的值**:`info registers`,简写为 **`i r`** - **查看指定寄存器值**: - `print|p $rax` ,查看寄存器 `%rax` 值(**寄存器名不带`%`**) - `i f edi`:查询寄存器 `%edi` 的值 - **设置寄存器值**: - `set $pc = 0x401242` > [!NOTE] 通过修改程序计数器 `%pc`(即指令指针寄存器`%rip`)跳转执行指定指令 > > 命令:`set $pc = 0x401242` ,等价于 `set $rip = 0x401242` > > 执行后,pc 中下一条指令地址为 `0x401242`,因此**调用 `si` 后将跳转到执行 `0x401242`地址处的指令**。 > [!example] 查看寄存器值 > > 查看所有寄存器值: > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-5D6049B4F3EB0E65C3B22561A7206EAF.png|376]] > > 查看指定寄存器的值: > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-E90641F532CD7A61DA8D885A82A34536.png|368]] > > > ### 线程相关 - `thread`: **查看当前线程** - `info threads`:**查看所有线程信息** - `thread [n]`:**切换到某个线程** - `thread aplly [all|n] [command]`:对**所有线程**或**指定线程**执行 gdb 命令 - `interrupt`:**中断正在运行的所有线程**(`continue` 恢复) - `break [] thread <n>`: 为指定线程设置断点 > [!example] 对所有线程或指定线程应用 gdb 命令 > > ```shell > (gdb) thread apply all bt # 查看所有线程的调用栈 > (gdb) thread apply 2 bt # 查看线程2的调用栈 > (gbd) thread apply all next # 所有线程执行一步 > ``` > [!example] > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-E2D9F4ED660F3FE972008B78B962FC95.png|952]] ### 调试运行中的进程 - `attach <pid>`:附加到指定进程,进行调试 - `detach`: 退出 gdb(不终止进程) - `quit`:退出 gbb 并终止进程 - `ctrl + c`:**暂停进程**(通过 `ptrace(PTRACE_INTERRUPT, pid, 0, 0)` 使进程进入 `Stopped` 暂停状态) > [!note] 附加到进程进行调试 : > > - 方式一:启动时附加 `sudo gdb -p <pid>` > - 方式二:sudo 启动 gdb 后执行内部命令 `attach <pid>`,即:`(gdb) attach <pid>` > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-0F7EB25C08737842D9C886B315C420C9.png|693]] > > > 需要 root 权限运行,否则报错如下: > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-B3D6C2B4473D5A18EA07445EB7DD15D6.png|560]] > > > 同时,**每个进程只能同时被一个 `ptrace`跟踪**,例如 `vscode` debug 跟踪后就无法再用 gdb 进行 attach,会报错如下: > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-1DDF2553918213647BE642A89AC497A5.png|536]] > ## 自定义命令 - `define <命令名>`,随后每行输入一个命令,输入 `end` 结束。 ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-EA9D5A7E7117D44A0F6A2D4BA7342AD6.png|413]] ### 切换到 TUI 界面 - `layout split`:切换到 TUI 界面,可**对照查看==源代码==和==机器指令==** - ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-E2E740AC7B4CB8E153CEB2D070E21047.png|509]] --- ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-E8AD8C012B356452F178666E1EF48F6F.png|559]] <br><br><br> # 关于 Core Dump 文件 > Core dump,核心转储文件 core dump **是操作系统在程序异常终止时生成的文件**,包含 **程序在崩溃时的==内存内容==、==寄存器状态==、==调用堆栈==以及其他相关的调试信息**。 Core dump 提供了**程序崩溃时的详细状态信息**,因此**可用于调试寻找程序崩溃原因**。 ## 生成 Core Dump 文件 大多数 Linux 发行版**默认关闭了核心转储功能**,需**通过`ulimit`命令启用**: - `ulimit -c` :查看当前**核心转储功能**是否有效 - `ulimit -c unlimited`:**==启用==核心转储功能**(**仅在==当前 shell 会话==及其子进程有效**) 启用该功能后,当一个程序崩溃时**系统会自动成一个核心转储文件**,通常命名为 `core` 或 `core.<pid>`。 > [!NOTE] `-c` 选项表示**内核转储文件的大小限制** > > - 其值为 0 时,表示**内核转储无效**。 => 命令 `ulimit -c 0` 为关闭核心转储功能 > - 其值为 unlimited,表示不限制,故生效。 > [!NOTE] 当一个多进程或多线程程序崩溃时,每个崩溃的**==线程或子进程==**都会生成一个核心转储文件。 > [!example] `core_dump` 文件示例 > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-6B8B08519D85FA62265E5993FF62C102.png|998]] > #### 永久启用 core dump 功能 > [!caution] `ulimit -c unlimited` 仅在 "当前 shell 会话及其子进程" 中有效。 要永久启用系统 core dump 功能,需执行以下命令,向 `/etc/security/limits.conf` 中输入内容[^1]: ```shell sudo bash -c "cat << EOF > /etc/security/limits.conf * soft core unlimited * hard core unlimited EOF ``` <br> ## 设置 Core Dump 文件生成路径及文件名 核心转储文件的**默认生成路径**为 "**运行程序时的 当前工作目录`pwd`"**。 可通过 `sysctl` 变量 `kernel.core_pattern` **指定核心转储文件的==生成路径和文件名==**: - `kernel.core_pattern=core.%p.%e.%s.%t`:**在当前工作目录中生成** - `kernel.core_pattern=/var/core/core.%p.%e.%s.%t`:**在==指定目录==下生成**(指定**绝对路径前缀**,这里为 `/var/core/`) 设置方式为**在 `/etc/sysctl.conf` 文件中指定变量 `kernel.core_pattern` 值**: - `sudo sh -c 'echo "kernel.core_pattern=core.%p.%e.%s.%t" >> /etc/sysctl.conf'` ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-7FE817BA24C3E4A2F86787CDE1A84F23.png|886]] > [!Info] `kernel.core_pattern` 占位符含义 > > 参见 [^2] > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-5C8D4D7314A4FF5043EED0654CDB6A52.png|513]] > <br><br> ## Core Dump 文件未生成的原因 核心转储文件通常会在 "**当前工作目录**" 下生成,如果**设置 `ulimit -c unlimited` 后仍不存在**, 可**检查 Linux 内核参数**—— `cat /proc/sys/kernel/core_pattern` 文件, 其中记录了**核心转储文件的保存路径**。 若显示路径为 `/usr/share/apport/apport`(如下图),表明 **==系统使用了 Apport 错误报告工具来处理核心转储==**,其会**拦截核心转储文件**并生成报告。 ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-A581CED204369199D609FC1F3C6D9AC3.png|826]] > [!example] 测试核心转储是否可正常生成 > > 若可正常生成,下述命令执行后,将在当前目录下得到核心转储文件。 > > ```shell > sleep 9999 & > kill -SIGSEGV %1 # 向最近一个放入后台的作业发送 `-SIGSEGV` 段错误信号,触发进程崩溃 > ``` > > ### 解决方式 ###### 方式一:临时禁用 Apport ```shell # 停止apport 服务 sudo service apport start # 重新开启apport 服务 sudo service apport stop ``` ###### 方式二:修改核心转储的路径 **执行下列命令设置**[^1],设置后 core dump 文件**将会在程序所在目录下生成**。 ```shell title:enable_core_dump.sh # 检查`/etc/sysctl.conf`中是否存在 `kernel.core_pattern`一行内容, 若不存在则向该文件中追加写入. if ! grep -qi 'kernel.core_pattern' /etc/sysctl.conf; then sudo sh -c 'echo "kernel.core_pattern=core.%p.%u.%s.%e.%t" >> /etc/sysctl.conf' sudo sysctl -p # 保证`/etc/sysctl.conf`文件内容立即生效. fi ulimit -c unlimited ``` <br> ### 关于 Apport `apport` 是 Ubuntu 及其衍生发行版中的**一个错误报告和崩溃处理工具**,其会**拦截核心转储、收集程序时崩溃信息并生成错误报告文件**。 主要功能: 1. **崩溃报告**: 当程序崩溃时,`apport` 会拦截核心转储,收集调试信息,并生成详细的崩溃报告。这些报告通常包括调用堆栈、寄存器状态、内存内容和其他有助于诊断的问题信息。 2. **自动报告**: `apport` 可以自动将崩溃报告发送到 Ubuntu 的错误跟踪系统(Launchpad)。这帮助开发者和维护者跟踪和修复系统中的错误。 3. **用户提示**: 在桌面环境中,`apport` 会显示一个对话框,通知用户程序已崩溃,并询问是否要提交错误报告。这让普通用户也能够轻松地报告系统中的问题。 4. **报告管理**: `apport` 提供了命令行工具(如 `apport-cli` 和 `apport-bug`)和一个图形界面(`apport-gtk`),用于查看和管理崩溃报告。 #### 查看崩溃报告 `apport` 生成的崩溃报告通常存储在 `/var/crash` 目录下, 可使用命令行工具 `apport-cli` 查看报告:`sudo apport-cli /var/crash/your_crash_file.crash` #### 启用/关闭 apport 服务 - 方式一:通过以下命令**启动或停止** **`apport` 服务**: - `sudo service apport start` - `sudo service apport stop` - 方式二:**编辑 `/etc/default/apport` 文件**,设置 `enabled=0` 表示禁用。 <br><br><br> # gdb 分析核心转储文件 (Core Dump) 使用 GDB 分析核心转储文件——启动 GDB 并**加载 "可执行文件" 及其崩溃时生成的 "核心转储文件"**。 - `gdb <program> <core_dump_file>` (两个命令等价) - `gdb -c <core_dump_file> <program>` gdb 需要程序中的**符号表**、**调试信息**(如有)、**地址映射**,从而**解析 core 中的指令地址,确定崩溃发生的位置**。 > [!example] gdb 分析核心转储文件示例一 > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-69CFC8ED17568C6F05C17B9C2D869D3D.png|713]] > > 如上图,在执行文件中第 2 行代码 `*((int*) 0) = 0` 时,**发生了段错误**。 > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-6652315E1157CE17A16A106EF421CA68.png|721]] > > 如上图,发生了**除 0 导致的算术异常**。 > [!example] gdb 分析核心转储文件:示例二 > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-C9F9A03615D4F7B7F5B349032EE4522B.png|825]] > > <br><br><br> # gdb 调试备注 > [!NOTE] 若程序**正阻塞** 或陷入**无限循环**,导致 **gbd 命令无响应**,可 `<C-c>` 发送中断信号,查看当前程序执行状态 > > 若成功中断,则 **gdb 将显示阻塞所在行**(例如系统调用 `read()`, `write()`, `epoll_wait()` 等操作处),如下: > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-5FA943462F246B5806DFDDF5FDB521BF.png|730]] > > 若 `<C-c>` 无效,则程序可能处于不可中断状态。 > [!NOTE] gdb 中可见**函数接收的 "传入参数值"** 以及该参数的 "**当前值**" > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-D219EB5FE4D713DE97F5E7A58A389311.png|896]] > > ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-3820525EDE9BB8620C22688468AFAF02.png|895]] > > #### 调试建议 ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-4912A9FC4F196DF45019FCD56D464A1A.png|500]] ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-3EF0A27159CF6A31B8EA30CBEEB0F2C5.png|493]] <br><br><br> # gdb 多线程调试 ### 示例:死锁检测 源代码: ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-5D5BDC8AE9DF7AD707B47E6C454065EE.png|813]] 运行触发死锁: ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-E053B5B7C2C49761863CE267A17B9F4B.png|774]] gdb 调试分析过程: ![[_attachment/05-工具/GNU 工具/gdb 调试器.assets/IMG-gdb 调试器-C22BE3D3F8CA4590E103FE66EE6DEF6C.png|916]] <br><br><br> # gdb 实现原理 gdb 主要基于 Linux 提供的 **==`ptrace` 系统调用==** 实现调试功能,该系统调用支持: - **读取/修改目标进程的内存**; - **读取/修改目标进程的寄存器**(比如程序计数器 PC、栈指针 SP 等等); - **设置断点**(修改指令,比如在目标指令位置插入**int3 指令**)。 - **捕获目标进程的信号**(比如 SIGSEGV 段错误)进行处理。 - **单步执行**(single step)、**继续执行**(continue)等。 例如: - `ptrace(PTRACE_ATTACH, pid, NULL, NULL)` 将 **`gdb` 附加到目标进程上**,系统调用会**向目标进程发送 `SIGSTOP(19)` 信号使==强制暂停==**。 - gdb 通过 `waitpid()` 等待目标进程被暂停; - `ptrace(PTRACE_CONT, pid, NULL, NULL)` **向目标进程发送 `SIGCONT(18)` 信号**,**让目标进程继续执行**, #### gdb attach 附加到运行进程的原理 1. gdb 执行 `ptrace(PTRACE_ATTACH, pid, NULL, NULL)` 系统调用,**向目标进程==发送 `SIGSTOP(19)` 信号==使之暂停**。 2. gdb **通过 `waitpid()` 等待目标进程停止**。 此后,gdb 可以继续**通过 `ptrace` 读取目标进程的寄存器、内存,设置断点**等。 #### gdb 设置断点的原理 > 本质上是 "**==修改目标指令==**",将 "**目标地址的机器码**" 替换为 "**==断点中断指令==**"(`int3`)。 1. gdb 设置断点: 1. 使用 `ptrace(PTRACE_PEEKDATA, ...)` **读取目标指令地址的==原始指令字节==**。 2. 替换该地址 "**==第一个字节==**" 为 `0xcc`(即 `int3` 指令) 3. **保存原始字节**,以便后续恢复。 2. 程序运行触发断点——程序执行到 **`int3` 指令**,**==触发 CPU 中断==**,转入内核态,跳转执行**中断处理程序**: 1. 向**触发中断的进程**发送 **==`SIGTRAP(5)`信号==**; 2. **检查目标进程 `task->struct` 中的 ==`ptrace` 状态标志位==**。 3. 内核发现进程**处在 `ptrace` 附加调试状态**,故 **==暂停==(挂起)目标进程**,通知 gdb。 4. gdb **从 `waitpid()` 返回**,检查 **==`SIGTRAP` 信号==** 得知命中断点。 3. gdb **恢复原始指令** 1. gdb 读取**指令指针 `%rip`**(即 PC),令其**回退一步**。 2. gdb 修改目标地址的 `0xCC` **还原为==原始指令==**。 3. gdb 执行 `ptrace(PTRACE_SINGLESTEP, pid, ....)` ,**执行==单步运行==**。 4. **单步运行**了原始指令后,**再次重新向==目标指令的地址==写入 `0xCC`,使断点再次生效**。 > [!example] 示例:gbd 通过 `ptrace` 读取 `%rip`,回退指令指针。 > > ```c > struct user_regs_struct regs; > ptrace(PTRACE_GETREGS, pid, NULL, &regs); > printf("RIP: 0x%llx\n", regs.rip); > regs.rip -= 1; // 回退指令指针 > ptrace(PTRACE_SETREGS, pid, NULL, &regs); > ``` > [!info] x86、x86-64 下的 `int3` 指令 > > 该指令为 "**==断点中断指令==**",机器码为 `0xCC`,会触发 CPU 中断,对应中断号为 `#BP`(Breakpoint Exception),编号为 `I3`。 > # DDD 调试器 DDD:GDB 的一个扩展,提供了图形界面 <br><br> # 参考资料 CoreDump 调试入门: - [GDB debugging tutorial for beginners - Linux Tutorials - Learn Linux Configuration](https://linuxconfig.org/gdb-debugging-tutorial-for-beginners) # Footnotes [^1]: [GDB debugging tutorial for beginners - Linux Tutorials - Learn Linux Configuration](https://linuxconfig.org/gdb-debugging-tutorial-for-beginners) [^2]: 《Debug Hacks》