概述
Windows调试流程
|
1 2 3 4 5 6 7 8 9 10 |
Windows/WinDbg 流程: 1. 程序崩溃 → 生成 .dmp 文件 2. 打开 WinDbg → 加载 .dmp 3. 设置符号路径 → .sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols 4. 设置源码路径 → .srcpath C:\MyProject\src 5. 加载符号 → .reload 6. 分析崩溃 → !analyze -v 7. 查看调用栈 → k / kp / kv 8. 查看变量 → dv / dt 9. 定位到源码行 |
Linux/GDB 的对应流程:
|
1 2 3 4 5 6 7 8 9 |
1. 程序崩溃 → 生成 core dump 文件 2. gdb ./program core → 加载 core dump 3. 调试信息(DWARF)直接嵌在二进制文件里(编译时 -g 生成) → 不需要像 WinDbg 那样单独设置符号路径 → 如果符号被 strip 掉了,需要用 debug info 文件 4. 源码路径 → directory /path/to/source(如果编译路径和当前不一致) 5. 分析崩溃 → bt(backtrace,等于 WinDbg 的 k) 6. 查看变量 → print / info locals 7. 定位到源码行 → list / frame |
区别
Windows把调试信息放在独立的.pdb文件里,需要你手动指定路径Linux把调试信息(DWARF)直接嵌入.o/ 可执行文件 /.so中(编译时加-g选项),所以大部分情况下你不需要额外设置符号路径,GDB自动就能找到
Linux调试
调试信息的生成与管理
- 编译时生成调试信息
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# -g:生成调试信息(对应 MSVC 的 /Zi 生成 .pdb) # 这是最重要的一步,没有 -g 就没法看到源码和变量名 g++ -g -O0 main.cpp -o program # -g 的不同级别 g++ -g0 main.cpp -o program # 不生成调试信息(默认) g++ -g1 main.cpp -o program # 最小调试信息(只有函数名和外部变量) g++ -g main.cpp -o program # 标准调试信息(= -g2) g++ -g3 main.cpp -o program # 最详细(包括宏定义展开信息) # -O0:禁止优化 # 调试时一定要用 -O0,否则编译器会: # - 把变量优化掉(GDB 显示 <optimized out>) # - 重排代码顺序(单步执行时跳来跳去) # - 内联函数(调用栈看不到被内联的函数) g++ -g -O0 main.cpp -o program # 调试用:最佳体验 g++ -g -O2 main.cpp -o program # 发布用:有调试信息但优化过,调试体验差 # CMake 中的等价设置 cmake -DCMAKE_BUILD_TYPE=Debug .. # -g -O0 cmake -DCMAKE_BUILD_TYPE=Release .. # -O3(无调试信息) cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo .. # -O2 -g(有调试信息的 Release) |
- 调试信息存在哪里
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# 检查二进制文件是否包含调试信息 file program # 输出包含 "with debug_info" 说明有调试信息 # 输出包含 "stripped" 说明调试信息被去掉了 readelf --debug-dump=info program | head -20 # 能看到 DWARF 信息就说明有调试信息 # 查看调试信息的大小 size --format=gnu program # .debug_info / .debug_line / .debug_str 等节的大小 # ===== 分离调试信息(对应 Windows 的 .pdb 分离)===== # 有时候你需要把调试信息从二进制中剥离(发布给用户的版本不需要调试信息) # 步骤 1:生成带调试信息的二进制 g++ -g -O2 main.cpp -o program # 步骤 2:把调试信息提取到独立文件 objcopy --only-keep-debug program program.debug # program.debug 就相当于 Windows 的 .pdb 文件 # 步骤 3:从原二进制中去掉调试信息 strip program # 现在 program 变小了,但没法直接调试 # 步骤 4:在 program 中记录一个链接,指向 program.debug objcopy --add-gnu-debuglink=program.debug program # 调试时 GDB 会自动找到 program.debug gdb ./program # GDB 按以下顺序搜索调试信息文件: # 1. 当前目录 # 2. 二进制文件所在目录 # 3. 二进制文件所在目录下的 .debug/ 子目录 # 4. /usr/lib/debug/ 下对应路径 # 手动指定调试信息文件搜索路径(对应 WinDbg 的 .sympath) (gdb) set debug-file-directory /path/to/debug/files # ===== 源码路径映射(对应 WinDbg 的 .srcpath)===== # 如果你在机器 A 上编译,在机器 B 上调试,源码路径可能不同 # 编译时记录的路径是 /home/alice/project/src/main.cpp # 调试机上源码在 /home/bob/code/src/main.cpp # 方法 1:告诉 GDB 去哪找源码 (gdb) directory /home/bob/code/src # 方法 2:路径替换(更精确) (gdb) set substitute-path /home/alice/project /home/bob/code # 把所有 /home/alice/project 开头的路径替换为 /home/bob/code # 方法 3:编译时用相对路径 g++ -g -fdebug-prefix-map=/home/alice/project=. main.cpp -o program # 调试信息里记录的是相对路径,在哪台机器上都能用 |
完整的调试会话
- 示例程序
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// main.cpp #include <cstdio> #include <cstdlib> #include <vector> struct Node { int value; Node* next; }; Node* create_list(int n) { Node* head = nullptr; for (int i = n; i > 0; i--) { Node* node = new Node{i, head}; head = node; } return head; } int sum_list(Node* head) { int sum = 0; Node* curr = head; while (curr != nullptr) { sum += curr->value; curr = curr->next; } return sum; } void buggy_function(int* arr, int size) { for (int i = 0; i <= size; i++) { // 故意的越界 bug:<= 应该是 arr[i] = i * 10; } } int main(int argc, char* argv[]) { printf("Creating list...\n"); Node* list = create_list(5); printf("Sum = %d\n", sum_list(list)); int arr[5]; buggy_function(arr, 5); printf("Done.\n"); return 0; } |
- 启动
GDB
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# ===== 启动方式 ===== # 方式 1:直接调试程序 gdb ./program # 方式 2:调试程序并传递命令行参数 gdb --args ./program arg1 arg2 # 对应 WinDbg 的 File → Open Executable 时填参数 # 方式 3:附加到运行中的进程(对应 WinDbg 的 Attach to Process) gdb -p 12345 # 或者 gdb ./program (gdb) attach 12345 # 方式 4:调试 core dump(对应 WinDbg 的 Open Crash Dump) gdb ./program core # 或者 gdb ./program /var/crash/core.12345 # 方式 5:启动时执行 GDB 命令(自动化调试) gdb -ex "break main" -ex "run" ./program # -ex 相当于启动后自动输入命令 # 方式 6:从命令文件执行(批量命令) gdb -x commands.gdb ./program # commands.gdb 内容: # break main # run # bt # quit # 启动后你会看到 (gdb) 提示符,接下来所有操作都在这里输入 |
- 运行控制
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# ===== 启动和停止(对应 VS 的 F5/Shift+F5)===== (gdb) run # 运行程序(≈ F5) (gdb) run arg1 arg2 # 带参数运行 (gdb) start # 运行到 main 的第一行暂停 # (≈ 在 main 设断点然后 F5) (gdb) continue # 继续运行(≈ F5,从断点继续) (gdb) c # continue 的简写 (gdb) kill # 杀掉被调试的进程 (gdb) quit # 退出 GDB(简写 q) # ===== 单步执行 ===== (gdb) next # 单步跳过(≈ F10,不进入函数) (gdb) n # 简写 (gdb) step # 单步进入(≈ F11,进入函数) (gdb) s # 简写 (gdb) finish # 执行完当前函数并返回(≈ Shift+F11) # 等价于 WinDbg 的 gu(go up) (gdb) until # 运行到当前循环结束 (gdb) until 42 # 运行到第 42 行 (gdb) nexti # 单步执行一条机器指令(不进入 call) (gdb) ni # 简写(≈ WinDbg 的 p) (gdb) stepi # 单步执行一条机器指令(进入 call) (gdb) si # 简写(≈ WinDbg 的 t) # ===== 反向调试(GDB 独有的神技,WinDbg 没有)===== # 先开启记录 (gdb) target record-full # 开启执行记录 (gdb) run # 程序运行到某个断点后... (gdb) reverse-next # 反向单步!回到上一行 (gdb) reverse-step # 反向单步进入 (gdb) reverse-continue # 反向继续运行到上一个断点 # 这对定位"什么时候某个值被改坏了"非常有用 # 但会让程序运行变慢很多,只在小程序上使用 |
- 断点
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# ===== 设置断点(对应 VS 的 F9 / WinDbg 的 bp)===== # 按函数名 (gdb) break main # 在 main 函数开头 (gdb) break sum_list # 在 sum_list 函数开头 (gdb) b sum_list # 简写 (gdb) break Node::Node # C++ 构造函数 (gdb) break std::vector<int>::push_back # 标准库函数 # 按文件名:行号 (gdb) break main.cpp:25 # main.cpp 的第 25 行 (gdb) b main.cpp:25 # 简写 # 按地址(对应 WinDbg 的 bp 地址) (gdb) break *0x4005a0 # 在指定地址设断点 # 条件断点(非常有用!) (gdb) break main.cpp:15 if i == 3 # 只有当 i == 3 时才停下 # 对应 WinDbg 的条件断点 bp address ".if (poi(i)==3) {} .else {gc}" (gdb) break sum_list if head == 0x0 # 当 head 是空指针时停下 # 临时断点(触发一次后自动删除) (gdb) tbreak main.cpp:25 # 对应 WinDbg 的 bu /1 # ===== 管理断点 ===== (gdb) info breakpoints # 列出所有断点(简写 i b) # 输出类似: # Num Type Disp Enb Address What # 1 breakpoint keep y 0x4005a0 in main at main.cpp:30 # 2 breakpoint keep y 0x400520 in sum_list at main.cpp:20 # 3 breakpoint keep n 0x400480 in create_list at main.cpp:10 (gdb) delete 2 # 删除 2 号断点 (gdb) delete # 删除所有断点(会确认) (gdb) disable 3 # 禁用 3 号断点(不删除) (gdb) enable 3 # 重新启用 # 给已有断点添加条件 (gdb) condition 1 argc > 2 # 给 1 号断点添加条件 # 断点命中时自动执行命令 (gdb) commands 1 # 设置 1 号断点的自动命令 > print sum # 每次命中时打印 sum 的值 > continue # 然后自动继续(不需要手动按 continue) > end # 这样断点 1 每次命中都会打印 sum 然后继续 # 类似 WinDbg 的 bp address "dv sum; gc" # ===== 观察点(数据断点,对应 WinDbg 的 ba)===== # 当某个内存位置被读/写时触发,不需要知道是哪行代码改的 (gdb) watch sum # 当 sum 被修改时中断 # 输出:Hardware watchpoint 4: sum # 之后每次 sum 的值变化,GDB 都会停下来告诉你: # Old value = 3 # New value = 6 # 这就是你追踪"某个值在哪里被改坏了"的利器 (gdb) watch *0x7fffffffe100 # 监视某个地址 (gdb) rwatch sum # 当 sum 被读取时中断(read watch) (gdb) awatch sum # 当 sum 被读或写时中断(access watch) # 注意:硬件观察点数量有限(x86 通常只有 4 个) # 超过限制 GDB 会用软件观察点(非常慢) # ===== 捕获点(Catchpoint)===== (gdb) catch throw # 当 C++ 异常被抛出时中断 # ≈ WinDbg 的 sxe eh (gdb) catch catch # 当 C++ 异常被捕获时中断 (gdb) catch syscall write # 当调用 write 系统调用时中断 (gdb) catch signal SIGSEGV # 当收到段错误信号时中断 (gdb) catch fork # 当进程 fork 时中断 (gdb) catch load libfoo.so # 当加载某个共享库时中断 |
- 查看信息
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# ===== 调用栈(对应 WinDbg 的 k / kp / kv)===== (gdb) backtrace # 查看调用栈 (gdb) bt # 简写 # 输出: # #0 sum_list (head=0x55555556b2a0) at main.cpp:22 # #1 0x0000555555555260 in main (argc=1, argv=0x7fffffffe0a8) at main.cpp:35 # # #0 是当前帧,#1 是调用者,依此类推 (gdb) bt full # 调用栈 + 每帧的所有局部变量 # ≈ WinDbg 的 kv + 每帧 dv (gdb) bt 5 # 只显示前 5 帧 (gdb) bt -5 # 只显示最后 5 帧 # 切换栈帧(查看不同层级的调用者) (gdb) frame 1 # 切换到 #1 帧(main 函数) (gdb) f 1 # 简写 (gdb) up # 向上一帧(从被调用者到调用者) (gdb) down # 向下一帧 # 切换帧后,print/list 命令都会在对应帧的上下文中工作 # ===== 查看变量(对应 WinDbg 的 dv / dt / ??)===== (gdb) print sum # 打印变量值(简写 p) # $1 = 15 (gdb) print head # 打印指针 # $2 = (Node *) 0x55555556b2a0 (gdb) print *head # 解引用指针(≈ WinDbg 的 dt) # $3 = {value = 1, next = 0x55555556b2c0} (gdb) print head->value # 访问成员 # $4 = 1 (gdb) print head->next->next->value # 链式访问 # $5 = 3 (gdb) print arr[0]@5 # 打印数组的 5 个元素 # $6 = {0, 10, 20, 30, 40} # @n 语法:从某个地址开始打印 n 个元素 (gdb) print (int[5])arr # 以数组类型打印 (gdb) print/x sum # 十六进制格式 (gdb) print/t sum # 二进制格式 (gdb) print/o sum # 八进制格式 (gdb) print/c ch # 字符格式 (gdb) print/f val # 浮点格式 (gdb) print/a addr # 地址格式(符号+偏移) (gdb) print/s str # 字符串格式 # 打印表达式(GDB 支持在 print 中写 C/C++ 表达式) (gdb) print sum + 100 (gdb) print sizeof(Node) (gdb) print (double)sum / 3 (gdb) print sum_list(head) # 甚至可以调用函数!(谨慎使用) # ===== 批量查看变量 ===== (gdb) info locals # 当前函数的所有局部变量(≈ WinDbg 的 dv) (gdb) info args # 当前函数的所有参数 (gdb) info variables # 所有全局/静态变量(输出很多) (gdb) info variables sum # 搜索名字包含 sum 的变量 # ===== 查看类型信息(对应 WinDbg 的 dt -v)===== (gdb) ptype Node # 显示 Node 结构体的定义 # type = struct Node { # int value; # Node *next; # } (gdb) ptype head # 显示变量的类型 (gdb) whatis sum # 显示变量的类型(更简洁) # ===== 查看源码(对应 VS 的代码窗口)===== (gdb) list # 显示当前位置附近的源码(10 行) (gdb) l # 简写 (gdb) list main.cpp:20 # 显示指定位置的源码 (gdb) list sum_list # 显示某个函数的源码 (gdb) list 1,50 # 显示第 1-50 行 (gdb) set listsize 30 # 设置每次 list 显示的行数 # ===== 查看内存(对应 WinDbg 的 d / db / dd / dq)===== (gdb) x/10x head # 从 head 地址开始,显示 10 个十六进制值 (gdb) x/10d head # 十进制 (gdb) x/10c head # 字符 (gdb) x/s str # 以字符串显示 (gdb) x/10i main # 反汇编 main 开头的 10 条指令 # x 命令格式:x/NFU address # N = 数量 # F = 格式(x十六进制 d十进制 u无符号 o八进制 t二进制 c字符 s字符串 i指令) # U = 单位(b字节 h半字 w字 g双字) (gdb) x/4xg $rsp # 查看栈顶的 4 个 8 字节值(十六进制) # ≈ WinDbg 的 dq @rsp L4 (gdb) x/20xb head # 从 head 地址开始,显示 20 个字节 # ≈ WinDbg 的 db address L20 # ===== 查看寄存器(对应 WinDbg 的 r)===== (gdb) info registers # 所有通用寄存器 (gdb) i r # 简写 (gdb) info registers rax rbx rsp rip # 指定寄存器 (gdb) print $rax # 打印单个寄存器值 (gdb) print/x $rip # 十六进制打印 RIP(当前指令地址) # ===== 查看汇编(对应 WinDbg 的 u)===== (gdb) disassemble # 反汇编当前函数 (gdb) disas # 简写 (gdb) disas sum_list # 反汇编指定函数 (gdb) disas /m sum_list # 汇编和源码交错显示(非常有用) (gdb) disas /s sum_list # 类似 /m 但显示更完整的源码信息 (gdb) disas 0x400520,0x400560 # 反汇编地址范围 # 切换汇编语法(默认 AT&T,你可能更习惯 Intel) (gdb) set disassembly-flavor intel # 切换到 Intel 语法 (gdb) set disassembly-flavor att # 切回 AT&T 语法 # 建议加到 ~/.gdbinit 里永久生效 # ===== 查看线程(对应 WinDbg 的 ~ 命令)===== (gdb) info threads # 列出所有线程 # 输出: # Id Target Id Frame # * 1 Thread 0x7ffff7fc1740 main () at main.cpp:30 # 2 Thread 0x7ffff6fc0700 worker () at worker.cpp:15 # * 号标记当前线程 (gdb) thread 2 # 切换到线程 2(≈ WinDbg 的 ~2s) (gdb) thread apply all bt # 查看所有线程的调用栈 # ≈ WinDbg 的 ~*k (gdb) thread apply all bt full # 所有线程的调用栈 + 局部变量 |
- 修改程序状态
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# ===== 修改变量值 ===== (gdb) set var sum = 100 # 修改变量(≈ WinDbg 的 ed / eq) (gdb) set var head->value = 999 # 修改结构体成员 (gdb) set $rax = 0 # 修改寄存器 (gdb) set {int}0x7ffe1234 = 42 # 修改指定地址的内存 # ===== 强制函数返回 ===== (gdb) return # 立即从当前函数返回(不执行剩余代码) (gdb) return 42 # 返回指定值 # ===== 跳转执行 ===== (gdb) jump main.cpp:35 # 跳到指定行继续执行(危险!跳过的代码不执行) (gdb) jump *0x4005a0 # 跳到指定地址 # ===== 调用函数 ===== (gdb) call printf("debug: sum=%d\n", sum) # 在调试过程中直接调用函数(非常实用但要小心副作用) (gdb) call sum_list(head) # 调用你自己的函数,看返回值 |
Core Dump 调试
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# ===== Step 1:开启 core dump ===== # 默认情况下 core dump 可能被禁用 ulimit -c unlimited # 取消 core 文件大小限制 # 永久生效:在 ~/.bashrc 中加入这一行 # 查看当前设置 ulimit -c # 0 = 禁用,unlimited = 无限制 # core 文件保存位置 cat /proc/sys/kernel/core_pattern # 默认可能是 "core" (保存在当前目录) # 或者 "|/usr/share/apport/apport ..." (Ubuntu 的崩溃报告接管) # 设置 core 文件的路径和命名格式 sudo bash -c 'echo "/tmp/core.%e.%p.%t" > /proc/sys/kernel/core_pattern' # %e = 程序名, %p = PID, %t = 时间戳 # 生成类似 /tmp/core.program.12345.1672531200 # Ubuntu 特殊处理:Ubuntu 用 apport 接管了 core dump # 如果你找不到 core 文件,先禁用 apport: sudo systemctl disable apport.service sudo bash -c 'echo "core" > /proc/sys/kernel/core_pattern' # ===== Step 2:让程序崩溃生成 core ===== ./program # 运行程序,如果段错误就会生成 core 文件 # Segmentation fault (core dumped) ← 看到 "core dumped" 说明已生成 # 也可以手动生成 core(不杀掉进程) gcore 12345 # 给运行中的进程 12345 生成 core dump # 生成 core.12345 文件,进程继续运行 # ===== Step 3:用 GDB 分析 core ===== gdb ./program /tmp/core.program.12345.1672531200 # GDB 会自动加载 core dump 并定位到崩溃点 # 输出类似: # Core was generated by `./program'. # Program terminated with signal SIGSEGV, Segmentation fault. # #0 0x0000555555555189 in buggy_function (arr=0x7fffffffdfa0, size=5) # at main.cpp:28 # 现在你已经在崩溃现场了,可以做所有正常调试操作: (gdb) bt # 查看崩溃时的完整调用栈 # #0 buggy_function (arr=0x7fffffffdfa0, size=5) at main.cpp:28 # #1 main (argc=1, argv=0x7fffffffe0a8) at main.cpp:36 (gdb) bt full # 调用栈 + 所有局部变量 (gdb) frame 0 # 切到崩溃的帧 (gdb) list # 看崩溃位置的源码 (gdb) info locals # 看局部变量 (gdb) print i # 看循环变量 (gdb) print size # 看参数 (gdb) print arr[0]@6 # 看数组内容 (gdb) info registers # 看崩溃时的寄存器状态 (gdb) x/10i $rip # 看崩溃时正在执行的指令 # ===== 分析思路(对应 WinDbg 的 !analyze -v)===== # GDB 没有 !analyze -v 这样的自动分析命令 # 你需要自己分析,一般步骤是: # 1. bt 看调用栈,确定崩溃在哪个函数 # 2. 切到崩溃帧,看源码和变量 # 3. 看崩溃时的指令(disas)确认是什么操作崩溃了 # 4. 结合变量值和指令推断原因(空指针?越界?栈溢出?) # 注意:core dump 是静态快照,不能 run/continue/step # 只能查看崩溃瞬间的状态 |
调试共享库中的崩溃
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 如果崩溃发生在共享库(.so)里 (gdb) info sharedlibrary # 查看加载了哪些共享库 # 输出: # From To Syms Read Shared Object Library # 0x7ffff7a00000 0x7ffff7b50000 Yes /lib/x86_64-linux-gnu/libc.so.6 # 0x7ffff7c00000 0x7ffff7c20000 Yes ./libmylib.so # 如果某个库显示 "No" (Syms Read),说明没有调试信息 # 需要安装对应的 debug 包: sudo apt install libc6-dbg # glibc 的调试符号 # Ubuntu/Debian 的调试符号包一般叫 xxx-dbg 或 xxx-dbgsym # 手动加载符号文件 (gdb) symbol-file ./program # 加载主程序符号 (gdb) add-symbol-file ./libmylib.so.debug 0x7ffff7c00000 # 加载库的符号 |
高级功能
自动化与脚本
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# ===== .gdbinit 配置文件(GDB 的启动脚本)===== # GDB 启动时自动执行 ~/.gdbinit 的命令 cat > ~/.gdbinit << 'EOF' # 使用 Intel 汇编语法 set disassembly-flavor intel # 历史命令保存 set history save on set history size 10000 set history filename ~/.gdb_history # 打印设置 set print pretty on # 结构体格式化打印 set print array on # 数组每个元素一行 set print object on # 打印对象时显示实际类型(多态) set print vtbl on # 显示虚函数表 set print demangle on # C++ 名称反修饰 set print elements 100 # 数组/字符串最多显示 100 个元素 # 允许调试 LLVM 这样的大项目 set pagination off # 关闭分页(不需要按回车翻页) set confirm off # 关闭确认提示 # 子进程处理 set follow-fork-mode child # fork 后跟踪子进程 set detach-on-fork off # fork 后不分离父进程 # 线程 set scheduler-locking off # 默认所有线程都运行 # 安全设置(GDB 会加载当前目录的 .gdbinit,这可能是安全风险) # 如果需要,在 ~/.gdbinit 中加入: # add-auto-load-safe-path /path/to/your/project EOF # ===== 自定义命令 ===== # 可以在 .gdbinit 中定义自己的命令 define print_list set $node = $arg0 set $i = 0 while $node != 0 printf "[%d] value = %d, addr = %p\n", $i, $node->value, $node set $node = $node->next set $i = $i + 1 end end document print_list Print all nodes in a linked list. Usage: print_list head_ptr end # 使用: # (gdb) print_list head # [0] value = 1, addr = 0x55555556b2a0 # [1] value = 2, addr = 0x55555556b2c0 # ... # ===== Python 脚本扩展(GDB 内嵌 Python)===== (gdb) python > import gdb > for symbol in gdb.lookup_global_symbol('main').symtab.global_block(): > if symbol.is_function: > print(f"Function: {symbol.name}") > end # 也可以写在 .py 文件里然后 source (gdb) source my_debug_helpers.py |
GDB TUI 模式
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# TUI 给 GDB 加了一个分屏界面,同时看源码和命令 # 对从 VS 过来的你可能更习惯 # 启动 TUI (gdb) tui enable # 或者启动时 gdb -tui ./program (gdb) layout src # 源码 + 命令 (gdb) layout asm # 汇编 + 命令 (gdb) layout split # 源码 + 汇编 + 命令 (gdb) layout regs # 寄存器 + 源码 + 命令 # TUI 快捷键 # Ctrl+x, a 开关 TUI # Ctrl+x, 1 单窗口布局 # Ctrl+x, 2 双窗口布局 # Ctrl+l 刷新屏幕(显示乱了的时候用) # 上下方向键 在 TUI 中是滚动源码,不是命令历史 # Ctrl+p/n 在 TUI 中用这两个键浏览命令历史 |
GDB + AddressSanitizer
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# ASan 能检测内存错误,配合 GDB 可以精确定位 # 编译时开启 ASan g++ -g -O0 -fsanitize=address main.cpp -o program # 直接运行会输出详细的错误报告: ./program # ================================================================= # ==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address ... # #0 0x4005a0 in buggy_function main.cpp:28 # #1 0x400620 in main main.cpp:36 # Address 0x7ffe... is located in stack of thread T0 # 用 GDB 调试 ASan 检测到的错误: # ASan 检测到错误时会调用 __asan::ReportGenericError # 在这里设断点可以精确停在错误发生的位置 gdb ./program (gdb) break __asan::ReportGenericError (gdb) run # 停下后 bt 看调用栈,frame 切到你的代码,print 看变量 |
调试 LLVM
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# ===== 编译 Debug 版 LLVM ===== cmake -S llvm -B build-debug -G Ninja \ -DCMAKE_BUILD_TYPE=Debug \ -DLLVM_ENABLE_PROJECTS="clang" \ -DLLVM_TARGETS_TO_BUILD="X86" \ -DLLVM_USE_SPLIT_DWARF=ON ninja -C build-debug clang opt # ===== 调试 clang 编译过程 ===== gdb --args ./build-debug/bin/clang -emit-llvm -S -O2 test.c -o test.ll (gdb) break clang::CodeGen::CodeGenFunction::EmitFunctionBody (gdb) run # 停在 Clang 生成函数体 IR 的地方 # ===== 调试 opt(Pass 执行)===== gdb --args ./build-debug/bin/opt --passes=mem2reg test.ll -S -o out.ll (gdb) break llvm::PromoteMemToReg (gdb) run # 停在 mem2reg 的核心函数 # ===== 调试你自己的 LLVM Pass ===== gdb --args ./build-debug/bin/opt \ --load-pass-plugin=./build/libMyPass.so \ --passes=my-pass \ test.ll -S -o /dev/null (gdb) break MyPass::run (gdb) run # ===== 在 LLVM IR 层面设断点 ===== # LLVM 有专门的调试辅助功能 (gdb) break llvm::Value::dump # 然后在代码中调用 value->dump() 就会打印 IR # 打印 LLVM IR 对象 (gdb) call F->dump() # 打印 Function 的 IR (gdb) call BB->dump() # 打印 BasicBlock 的 IR (gdb) call I->dump() # 打印 Instruction 的 IR (gdb) call M->dump() # 打印整个 Module 的 IR # dump() 是 LLVM 专门为调试提供的方法,在 GDB 中直接调用非常方便 |
其他
WinDbg → GDB 命令对照
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
功能 WinDbg GDB ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 运行 g run / continue 单步跳过 p next (n) 单步进入 t step (s) 执行到返回 gu finish 设断点 bp break (b) 条件断点 bp addr ".if" break ... if ... 数据断点 ba watch / rwatch / awatch 列出断点 bl info breakpoints 删除断点 bc delete 禁用断点 bd disable 调用栈 k / kp / kv bt / bt full 切换帧 .frame N frame N 查看局部变量 dv info locals 打印变量 ?? / dt print (p) 查看类型 dt -v ptype 查看内存 db/dd/dq x/xb x/xw x/xg 查看寄存器 r info registers 反汇编 u / uf disassemble 查看源码 .lines / lsf list (l) 查看线程 ~ info threads 切换线程 ~Ns thread N 所有线程调用栈 ~*k thread apply all bt 加载符号 .sympath set debug-file-directory 源码路径 .srcpath directory / set substitute-path 加载 dump .opendump gdb program core 自动分析 !analyze -v (无直接对应,手动 bt + info) 搜索内存 s find 查看模块 lm info sharedlibrary 附加进程 .attach attach PID 异常断点 sxe catch throw / catch signal 退出 q quit (q) |
具体的编译选项
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# ===== Debug 版本 ===== g++ -g -O0 -DDEBUG -fno-omit-frame-pointer main.cpp -o program_debug # -g 生成 DWARF 调试信息(≈ MSVC /Zi) # -O0 禁止优化(≈ MSVC /Od) # -DDEBUG 定义 DEBUG 宏(可选,你自己代码里用) # -fno-omit-frame-pointer 保留帧指针(让 bt 调用栈更完整) # Release 模式下编译器会省掉帧指针以腾出一个寄存器 # Debug 版特点: # - 文件大(DWARF 调试信息可能比代码本身还大) # - 运行慢(没有任何优化) # - 调试体验好(变量不会被优化掉,单步执行不跳行) # - assert() 生效 # ===== Release 版本 ===== g++ -O2 -DNDEBUG main.cpp -o program_release # -O2 开启优化(≈ MSVC /O2) # -DNDEBUG 定义 NDEBUG 宏(禁用 assert(),C/C++ 标准行为) # 没有 -g 不生成调试信息 # Release 版特点: # - 文件小 # - 运行快 # - 无法用 GDB 看到源码和变量名 # - assert() 被编译掉了 # ===== RelWithDebInfo(带调试信息的 Release,实际开发中最常用)===== g++ -O2 -g -DNDEBUG main.cpp -o program_reldbg # -O2 和 -g 同时使用 # 既有优化(运行快),又有调试信息(能用 GDB) # 代价: # - 文件比纯 Release 大(多了 DWARF) # - GDB 调试体验不如纯 Debug(变量可能被优化掉) # 这是生产环境排查问题时最实用的配置 # ===== MinSizeRel(最小体积 Release)===== g++ -Os -DNDEBUG main.cpp -o program_small # -Os 优化目标是最小体积而不是最快速度 # 嵌入式场景常用 |
优化级别详解
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# Linux(GCC/Clang)的优化级别比 Windows(MSVC)更细 -O0 无优化(Debug 用) ≈ MSVC /Od 编译最快,调试体验最好,运行最慢 -O1 基础优化 ≈ MSVC /O1 开启不增加编译时间的优化 比 -O0 快不少,调试体验还可以 -O2 标准优化(Release 推荐) ≈ MSVC /O2 大部分优化都开启了:内联、循环优化、向量化等 性能和编译时间的最佳平衡 -O3 激进优化 在 -O2 基础上增加更激进的优化(如更激进的内联、循环展开) 不一定比 -O2 快(代码膨胀可能导致缓存命中率下降) 编译时间明显更长 -Os 体积优化 类似 -O2 但禁用会增大代码体积的优化 ≈ MSVC /O1(MSVC 的 /O1 侧重体积) -Oz 极致体积优化(Clang 独有) 比 -Os 更激进地缩小体积 -Ofast 不严格遵守标准的极致速度优化 开启 -O3 + 放宽浮点精度要求(-ffast-math) 可能改变程序的浮点计算结果,科学计算慎用 # 对编译器开发者来说,你需要理解这些优化级别背后对应哪些 Pass # 查看 -O2 开启了哪些 Pass: clang -O2 -mllvm -print-pipeline-passes test.c -o /dev/null 2>&1 |
CMake 中的配置
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# CMake 预定义了四种构建类型,对应上面的编译选项组合 cmake -DCMAKE_BUILD_TYPE=Debug .. # 等价于:-g -O0 # LLVM Debug 版:编译慢(1-2小时),占磁盘 80-100GB,可以用 GDB 调试 LLVM 本身 cmake -DCMAKE_BUILD_TYPE=Release .. # 等价于:-O3 -DNDEBUG # LLVM Release 版:编译较快,占磁盘 15-20GB,无法调试 LLVM 本身 cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo .. # 等价于:-O2 -g -DNDEBUG # 推荐的折中方案:能调试,运行也不算太慢,占磁盘 30-50GB cmake -DCMAKE_BUILD_TYPE=MinSizeRel .. # 等价于:-Os -DNDEBUG # 最小体积,编译器开发中很少用 # 你也可以自己指定编译选项覆盖默认的 cmake -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_FLAGS="-g" \ -DCMAKE_CXX_FLAGS="-g" .. # 这样得到的是 "带调试信息的 Release",和 RelWithDebInfo 类似 |
LLVM 开发中的实际构建策略
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# 推荐维护两个构建目录 # 目录 1:Release + Assertions(日常开发用) cmake -S llvm -B build-release -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_ASSERTIONS=ON \ -DLLVM_ENABLE_PROJECTS="clang;lld" \ -DLLVM_TARGETS_TO_BUILD="X86;RISCV" # 编译快,运行快,但 LLVM_ENABLE_ASSERTIONS=ON 保留了断言检查 # 你写的 Pass 如果有 bug,断言会提前报错而不是产生奇怪的结果 # 这是 LLVM 社区推荐的日常开发配置 # 目录 2:Debug(需要用 GDB 调试 LLVM 本身时) cmake -S llvm -B build-debug -G Ninja \ -DCMAKE_BUILD_TYPE=Debug \ -DLLVM_ENABLE_PROJECTS="clang" \ -DLLVM_TARGETS_TO_BUILD="X86" \ -DLLVM_USE_SPLIT_DWARF=ON \ -DLLVM_OPTIMIZED_TABLEGEN=ON \ -DLLVM_PARALLEL_LINK_JOBS=2 # 编译慢,占空间大,但能在 GDB 里完美调试 LLVM 的每一行代码 # LLVM_USE_SPLIT_DWARF=ON 把调试信息拆分到 .dwo 文件(节省链接时间) # LLVM_OPTIMIZED_TABLEGEN=ON 单独优化编译 TableGen(否则 Debug 版 TableGen 极慢) # 只在需要的时候编译:ninja -C build-debug clang opt |
strip 命令——事后去掉调试信息
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# Linux 上可以随时从二进制中去掉调试信息(Windows 上 .pdb 是独立文件,删掉即可) g++ -g -O2 main.cpp -o program ls -lh program # -rwxr-xr-x 1 user user 1.2M program ← 包含 DWARF strip program ls -lh program # -rwxr-xr-x 1 user user 120K program ← 小了 10 倍 # 也可以只去掉调试信息,保留符号表 strip --strip-debug program # 保留函数名(bt 还能看到函数名),但看不到源码行号和变量 # 也可以只去掉特定的 section strip --remove-section=.comment program # 检查是否被 strip 过 file program # 未 strip:... not stripped # 已 strip:... stripped # 发布流程中的常见做法(对应 Windows 的"保留 .pdb + 发布 Release"): g++ -g -O2 main.cpp -o program # 编译带调试信息的版本 objcopy --only-keep-debug program program.debug # 提取调试信息 strip program # strip 原始二进制 objcopy --add-gnu-debuglink=program.debug program # 添加 debug link # 发布 program(小),保留 program.debug(用于分析 core dump) # 用户的 core dump 拿回来后: # gdb ./program core → GDB 自动通过 debuglink 找到 program.debug |
assert
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <cassert> void process(int* ptr) { assert(ptr != nullptr); // Debug 版:空指针时程序终止并打印错误 // Release 版(-DNDEBUG):这行被完全删除 *ptr = 42; } // assert 在 Debug 和 Release 下的行为差异: // // Debug(没有 -DNDEBUG): // assert(ptr != nullptr); 展开为: // if (!(ptr != nullptr)) { // fprintf(stderr, "Assertion failed: ptr != nullptr, file main.cpp, line 4\n"); // abort(); // 发送 SIGABRT,生成 core dump // } // // Release(有 -DNDEBUG): // assert(ptr != nullptr); 展开为: // ((void)0); // 完全消失,不生成任何代码 // 永远不要在 assert 里做有副作用的操作: assert(do_important_work() == 0); // 错误!Release 版 do_important_work() 不会被调用 // LLVM 有自己的断言宏,行为更可控: // llvm_unreachable("message") — 标记不应该到达的代码路径 // assert(condition && "message") — LLVM 中常见的写法,带错误信息 |
问题
Linux二进制debug、release区分
- 概述
Windows上,Debug和Release的区别非常显式Visual Studio有明确的配置下拉框,编译出来的文件名/路径都不同(Debug/Release文件夹)- 甚至链接的运行时库都不一样(
msvcrtd.dll vs msvcrt.dll)
Linux上没有这么"正式"的框架,Debug和Release本质上只是编译选项的组合不同- 同一个源码,加不同的
gcc/clang参数,就产生Debug或Release版本的二进制
- 同一个源码,加不同的
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Windows Linux ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 调试信息 .pdb 独立文件 DWARF 嵌在二进制中 /Zi 生成 -g 生成 优化级别 /Od (禁止) /O2 (优化) -O0 (禁止) -O2 -O3 (优化) 运行时库 msvcrtd.dll (Debug) 没有 Debug/Release 区分 msvcrt.dll (Release) 都链接同一个 libc.so Debug 宏 _DEBUG 自动定义 需要手动 -DNDEBUG(Release 时) NDEBUG (Release) 不加就保留 assert 输出文件名 通常不同目录 同一个文件名,区别在编译选项 Debug/app.exe 就是 ./app Release/app.exe 文件大小差异 Debug 版大很多 Debug 版大很多(DWARF 很占空间) |
- 快速对照表
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Debug RelWithDebInfo Release ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ GCC/Clang -g -O0 -g -O2 -DNDEBUG -O2 -DNDEBUG MSVC /Zi /Od /Zi /O2 /DNDEBUG /O2 /DNDEBUG 调试信息 有(完整) 有(部分受优化影响) 无 优化 无 有 有 assert 生效 禁用 禁用 文件大小 最大 中等 最小 运行速度 最慢 快 快 调试体验 最好 能用但有时看到 不能调试 <optimized out> 编译速度 最快 中等 中等 适用场景 日常开发调试 生产环境排查问题 最终发布 |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 编译器扩展语法:一07/06
- ♥ Linux 高性能服务器编程:I/O复用二12/12
- ♥ 51CTO:Linux C++网络编程一08/13
- ♥ Makefile记述一08/15
- ♥ Linux调试工具记录08/13
- ♥ 51CTO:Linux C++网络编程二08/14