当前位置: 首页 > news >正文

gdb文档_第二章

一、实践题

1. 多初始化文件冲突

在 ~/.config/gdb/gdbinit 中设置 set print array-indexes on,在 ./.gdbinit 中设置 set print array-indexes off。启动GDB调试程序时,print 命令是否会显示数组索引?解释原因。
要求:通过实验验证并分析加载顺序。

答案: GDB 初始化文件存在冲突设置时,最终生效的设置取决于文件的加载顺序。根据GDB的规则,当前目录下的 .gdbinit 中的设置 set print array-indexes off 会覆盖 ~/.config/gdb/gdbinit 中的 on 设置。因此,启动GDB后,print 命令默认不会显示数组索引

下面通过一个实验来验证和分析。

🔍 实验验证与分析

第一步:准备初始化文件
  1. ~/.config/gdb/gdbinit 中设置开启数组索引显示:

    echo "set print array-indexes on" > ~/.config/gdb/gdbinit
    
  2. 在项目目录 ./.gdbinit 中设置关闭数组索引显示:

    echo "set print array-indexes off" > ./.gdbinit
    
第二步:验证文件加载

GDB 会按照以下顺序加载初始化文件:

  1. 用户主目录的初始化文件(例如 ~/.gdbinit,但你的路径是 ~/.config/gdb/gdbinit,需确保GDB能识别此路径,或通过 -iex 提前设置)。
  2. 当前目录的初始化文件(./.gdbinit)。

关键点:后加载的文件中的设置会覆盖先加载文件中的冲突设置。因此,./.gdbinit 中的 off 设置会覆盖 ~/.config/gdb/gdbinit 中的 on 设置。

第三步:测试命令效果

编写一个简单的C程序用于测试:

// test_array.c
int main() {int arr[3] = {10, 20, 30};return 0; // 在此设置断点
}

编译并调试:

gcc -g test_array.c -o test_array
gdb ./test_array

在GDB中操作:

# 在main函数返回前设置断点
(gdb) b test_array.c:4
Breakpoint 1 at 0x113d: file test_array.c, line 4.# 运行程序
(gdb) run
Starting program: /tmp/test_arrayBreakpoint 1, main () at test_array.c:4
4        return 0;# 打印数组,观察输出
(gdb) print arr
$1 = {10, 20, 30}

如果输出是 {10, 20, 30} 而不是像 [0] = 10, [1] = 20, [2] = 30 这样带索引的形式,就说明 set print array-indexes off 生效了。

第四步:检查加载顺序和最终设置

在GDB中,你可以使用 show commands 来查看初始化文件中的命令是否被执行,或者直接验证 print array-indexes 的当前状态:

(gdb) show print array-indexes
Printing of array indexes is off.

这将直接显示当前设置是 off,证实了后加载的 ./.gdbinit 覆盖了先前设置。

💎 结论

GDB初始化文件的加载顺序是:主目录配置 → 当前目录配置。后加载的配置会覆盖先加载的冲突设置。因此,当 ~/.config/gdb/gdbinit./.gdbinit 都对 print array-indexes 进行设置时,后加载的 ./.gdbinit 中的 set print array-indexes off 会生效,导致 print 命令不显示数组索引。

⚠️ 注意

  • 确保 ~/.config/gdb/gdbinit 是GDB会读取的路径。GDB默认在主目录查找 .gdbinit 文件。若你使用 ~/.config/gdb/gdbinit,需确认GDB配置或环境变量(如 GDBINIT)将其包含在加载路径中,否则该文件可能不会被加载。
  • 若要确保特定设置生效,需要了解其加载顺序,或将所有统一配置放在一个文件中避免冲突。
  • 使用 gdb -nx 可以暂时不加载任何初始化文件,用于调试或验证初始化文件本身带来的问题。

2. 环境变量隔离调试

程序依赖环境变量 LD_PRELOAD=libcustom.so 但直接运行崩溃。如何在不修改系统环境的前提下,使用GDB调试该程序?
要求:给出完整命令序列并说明原理。

答案: 理解你的需求了。你希望在不改变系统全局环境的情况下,让 GDB 在调试时临时地、隔离地设置 LD_PRELOAD 环境变量。下面是详细的步骤和原理说明。

🛠️ 操作步骤与命令序列

  1. 编译程序(确保带调试信息):如果程序是你自己编译的,务必加上 -g 选项,否则 GDB 将无法获取有效的调试信息。

    gcc -g -o your_program your_program.c
    
  2. 启动 GDB 并设置环境:在终端中按顺序执行以下命令。

    # 启动GDB,加载你的程序
    gdb ./your_program# 在GDB内部设置临时的LD_PRELOAD环境变量,指定你的库路径
    (gdb) set environment LD_PRELOAD /path/to/your/libcustom.so# (可选) 检查环境变量是否设置成功
    (gdb) show environment LD_PRELOAD# 设置你可能需要的断点,例如在main函数处
    (gdb) break main# 运行程序,这将在你设定的LD_PRELOAD环境下启动
    (gdb) run
    

    程序现在会在 libcustom.so 被预加载的情况下运行,并在断点处暂停,等待你的调试命令。

  3. 进行调试:接下来你就可以使用常用的 GDB 命令进行调试了。

    # 继续执行程序
    (gdb) continue
    # 或单步执行
    (gdb) next
    # 打印变量值
    (gdb) print variable_name
    # 查看回溯(调用栈)
    (gdb) backtrace
    

💡 核心原理

  • set environment 命令的隔离性:在 GDB 中使用 set environment 设定的环境变量(如 LD_PRELOAD),仅对当前 GDB 调试会话中启动的程序有效。它不会修改你系统 Shell 的环境变量,也不会影响其他任何程序。调试会话结束后,这个设置自然失效。
  • LD_PRELOAD 的作用机制LD_PRELOAD 是一个强大的 Linux 环境变量。它指示动态链接器在加载其他所有共享库之前,先加载指定的库(这里是 libcustom.so)。这使得该库中的函数可以“覆盖”(Hook)标准库(如 libc)中的同名函数。这常用于调试、性能分析或修改程序行为。
  • GDB 的操控能力:GDB 在启动程序时,可以控制程序的所有初始状态,包括其环境变量。set environment 命令就是在程序真正运行前,为其定制运行环境。

⚠️ 注意事项

  • 库的依赖与路径:确保 libcustom.so 的路径是正确的,并且它本身所依赖的其他库也能被系统找到(例如,如果它是通过 LD_LIBRARY_PATH 寻找依赖,你可能也需要在 GDB 中设置该变量)。
  • SUID/SGID 限制:如果调试的程序设置了 SUID 或 SGID 权限位,出于安全原因,系统会忽略 LD_PRELOAD 环境变量。这种情况下此方法无效。
  • 静态编译程序:如果程序是静态编译的(不依赖动态链接库),LD_PRELOAD 机制将不起作用。
  • 调试信息是关键:务必确保你调试的程序和 libcustom.so(如果可能且你需要调试它)在编译时都加上了 -g 选项以包含调试符号,否则 GDB 无法显示有意义的源代码和变量信息。

🔄 替代方法:在外部命令中设置

你也可以在启动 GDB 时,直接在一条命令中设置环境变量,这同样不会影响系统全局环境:

LD_PRELOAD=/path/to/libcustom.so gdb ./your_program

在 GDB 内部,你仍然可能需要使用 run 来启动程序。这种方式与在 GDB 内使用 set environment 效果类似。

3. 多进程调试

父进程PID为1234,子进程PID为5678。如何同时调试父子进程?
要求:提供两种方案(如 attach 和 follow-fork-mode),并说明优缺点。

答案: 以下是同时调试父子进程的两种方案,结合具体操作步骤和优缺点分析:

方案一:Attach 附加进程法
操作步骤

调试父进程
gdb -p 1234

设置断点后继续运行(如 continue)。

调试子进程
新开终端附加到子进程:
gdb -p 5678

优点

灵活性强:可随时附加到任意进程,无需提前配置。
适用性广:支持调试已运行的进程,不受程序启动阶段限制。

缺点

操作繁琐:需手动切换终端和多次附加。
可能错过早期代码:若子进程初始化逻辑在附加前已执行,无法调试。

方案二:Follow-Fork-Mode 模式
操作步骤

启动 GDB 并配置
gdb ./your_program
(gdb) set follow-fork-mode child # 跟踪子进程
(gdb) set detach-on-fork off # 同时控制父子进程
(gdb) break child_process # 在子进程函数设断点
(gdb) run

切换进程上下文
子进程暂停后,通过 info inferiors 查看进程列表,用 inferior 切换到父进程调试。

优点

完整生命周期调试:从 fork() 开始即可捕获父子进程行为。
无需手动附加:自动暂停子进程,避免错过初始化逻辑。

缺点

进程阻塞风险:未跟踪的进程会被暂停(如父进程),需手动恢复。
版本限制:需 Linux 内核 ≥2.5.60 和 GDB ≥7.0。

方案对比与选择建议

场景推荐方案原因调试已运行进程Attach 法无需重启程序,灵活附加调试进程创建逻辑Follow-Fork-Mode捕获 fork() 后的完整上下文简单问题快速排查Attach 法操作直接,适合单次调试复杂进程树(如多层级fork)Follow-Fork-Mode通过 info inferiors 管理多个进程
调试技巧

避免子进程退出:在子进程代码开头添加 sleep 循环,方便附加:
if (pid == 0) { while(1) sleep(10); } // 子进程循环

权限问题解决:若附加失败,尝试临时放宽 ptrace 限制:
sudo sysctl kernel.yama.ptrace_scope=0

4. 带特殊字符的命令行参数

程序需传入参数 file=“data.txt”(含双引号)。如何在GDB的 run 命令中正确传递?
要求:写出转义后的命令,并解释为何不能直接输入。

答案: 在GDB的 run 命令中传递包含特殊字符(如双引号)的参数时,需要正确转义以确保参数被准确解析。对于参数 file="data.txt"(要求双引号作为参数的一部分),正确的转义方式如下:

🛠️ 转义后的命令

在GDB中,你应该使用反斜杠 \ 来转义双引号:

(gdb) run file=\"data.txt\"

或者,你也可以使用单引号将整个参数包裹起来,这在许多Shell环境中是常见的做法,但请注意GDB对参数的处理方式可能略有不同。不过,在GDB内部直接使用 run 命令时,更直接和可靠的方法是转义双引号本身:

(gdb) run 'file="data.txt"'

验证参数是否正确传递:执行后,在程序的 main 函数入口设置断点(break main),然后运行。程序在断点处暂停后,使用 print argv[1](假设这是第一个参数)来查看实际接收到的参数字符串,确认引号是否包含在内。

⚠️ 为何不能直接输入?

不能直接输入 run file="data.txt" 的主要原因在于:

  1. Shell和GDB的参数解析规则:双引号 " 在大多数Shell和命令行解析器中具有特殊含义,它用于定义字符串的边界。直接输入 "data.txt",Shell或GDB的解析器会认为 "data.txt" 是一个完整的字符串,并期望移除引号后将内部的 data.txt 作为参数值传递给程序。这意味着你的程序通过 argv 获取到的参数将不包含双引号,仅仅是 file=data.txt(如果参数是 file="data.txt",则获取到的可能是 file=data.txt),这与你的预期不符。
  2. 防止参数被拆分:如果你的参数中包含空格,例如 file="my data.txt",不加转义直接输入 run file="my data.txt",GDB可能会将其解析为三个单独的部分:file=mydata.txt,这会导致参数数量错误和内容错误。

因此,通过反斜杠 \ 对双引号进行转义,是明确告诉GDB的解析器:这里的双引号是一个普通的字面字符,而不是用于定义字符串的特殊符号。这样就能确保双引号作为参数值的一部分完整地传递给你的程序。

💡 其他注意事项

  • 使用 set args:如果你需要多次运行程序并使用相同的参数,可以先使用 set args 命令预设参数,之后只需输入 run 即可。转义规则相同:
    (gdb) set args file=\"data.txt\"
    (gdb) run
    
  • 调试时检查参数:在GDB中,程序运行后(如在main函数开始处设置断点),可以使用 print argcprint argv[1](或循环打印argv数组)来检查参数是否按预期传递。

希望这些解释能帮助你顺利调试!

5. 禁用地址随机化

程序因堆溢出崩溃,但每次崩溃地址不同。如何固定堆地址以稳定复现问题?
要求:给出GDB命令和底层原理(如ASLR机制)。
程序因堆溢出崩溃但每次崩溃地址不同,这通常是由于 ASLR(地址空间布局随机化) 机制在起作用。ASLR 是一种安全技术,通过随机化内存地址(包括堆、栈、共享库等)的布局,增加攻击者预测内存地址的难度,从而防范基于内存的漏洞利用。

答案: 要稳定复现问题,就需要固定这些内存地址。以下是具体的操作方法和原理说明。

📌 1. 禁用 ASLR

ASLR 是导致每次运行时内存地址不同的主要原因。禁用它可以获得固定的内存布局。

方法一:在 GDB 中直接禁用(临时生效)
GDB 默认会禁用被调试程序的 ASLR,这是最简单的方法。

gdb ./your_program
(gdb) run
  • 原理:GDB 在调试模式下启动程序时,默认会关闭系统的 ASLR 机制,以便于调试。这样每次运行程序,其内存布局(包括堆地址)都会保持一致。

方法二:在系统层面禁用(影响所有程序)
通过修改 /proc/sys/kernel/randomize_va_space 来调整 ASLR 设置。

# 查看当前ASLR设置
cat /proc/sys/kernel/randomize_va_space# 临时禁用ASLR(重启后失效)
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space# 重新启用ASLR
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
  • 值的含义
    • 0:完全禁用 ASLR。
    • 1:保留的随机化(已废弃)。
    • 2:完全启用 ASLR(默认值)。
  • 注意:这是全局设置,会影响系统上运行的所有程序,不建议在生产环境中长期使用

方法三:使用 setarch 命令(针对单个进程)
setarch 命令可以在启动程序时临时改变进程的架构环境,包括禁用 ASLR。

# 使用 setarch 启动程序,禁用ASLR
setarch $(uname -m) --addr-no-randomize ./your_program# 或者与GDB结合使用
setarch $(uname -m) --addr-no-randomize gdb ./your_program
  • 原理setarch 命令的 --addr-no-randomize 参数会为当前启动的进程关闭 ASLR,而不影响系统全局设置。

🔍 2. 控制堆内存分配

即使禁用了 ASLR,堆内存的分配行为也可能受到分配器自身策略、线程调度等因素的轻微影响。为了最大化复现的稳定性,可以尝试控制堆分配。

  • 使用固定大小的输入:确保每次运行程序时,触发堆溢出的输入数据完全相同。任何微小的差异都可能导致内存分配器做出不同的决策。
  • 考虑使用自定义分配器:对于复杂的项目,在调试阶段可以考虑使用简单的、行为确定的内存分配器(例如,预先分配一大块内存并自行管理),以替代系统的 malloc/free,但这通常需要修改代码。

🛠️ 3. GDB 调试技巧

在固定地址后,利用 GDB 深入分析堆溢出问题。

  • 在内存分配和释放函数处设置断点:这有助于理解内存的分配和释放过程,以及溢出发生时堆的状态。
    (gdb) break malloc
    (gdb) break free
    
  • 使用 watchpoint 监测关键内存写入:如果你知道大概哪个变量或内存区域被溢出覆盖,可以设置观察点。
    (gdb) watch *(int*)0x604250 # 监视特定地址的写入
    
  • 检查堆元数据:堆溢出常常会破坏内存分配器的元数据(例如 glibcchunk 结构)。在崩溃后,使用 x 命令检查崩溃点附近的内存。
    (gdb) x/20xg 0x604240 - 0x10 # 查看疑似被破坏的堆块及其元数据
    
  • 回溯调用栈:崩溃时,立即使用 bt(backtrace)命令查看函数调用栈,定位问题代码。
    (gdb) bt
    

⚙️ 4. 其他增强稳定性的方法

  • 使用 MALLOC_CHECK_ 环境变量:Glibc 提供了 MALLOC_CHECK_ 环境变量来检测堆内存问题。

    MALLOC_CHECK_=3 ./your_program
    
    • 值的作用
      • 0:忽略错误。
      • 1:在 stderr 上打印错误信息。
      • 2:在检测到错误时立即中止程序。
      • 3:组合 1 和 2,打印信息并中止。
    • 这可以帮助你更早地发现堆错误,有时能提供更详细的诊断信息。
  • 编写 GDB 脚本自动化调试:如果崩溃需要复杂的操作序列才能触发,可以将一系列 GDB 命令(如设置断点、运行、打印状态等)写入一个脚本文件(例如 debug_script.gdb),然后让 GDB 自动执行。

    gdb -x debug_script.gdb ./your_program
    

💎 总结

为了稳定复现堆溢出崩溃,核心步骤是禁用 ASLR确保输入一致性

方法命令/操作优点缺点
在GDB中运行gdb ./progrun简单,无需系统权限仅对当前调试会话有效
使用 setarchsetarch $(uname -m) --addr-no-randomize ./prog针对单个进程,不影响系统需要记住命令
系统全局禁用echo 0 > /proc/sys/kernel/randomize_va_space对所有程序生效不安全,需谨慎使用

底层原理:ASLR 通过随机化内存地址布局来增加攻击难度。禁用它后,程序(包括堆、栈、库等)每次加载到内存中的起始地址都变得固定且可预测,从而使得堆溢出的效果(如覆盖特定函数指针或元数据)能够被稳定复现。

希望这些方法能帮助你稳定地复现和调试问题!

二、多选题

GDB初始化文件加载顺序(多选)
A. system.gdbinit → ~/.gdbinit → ./.gdbinit
B. ~/.config/gdb/gdbinit 优先级高于 ~/.gdbinit
C. 工作目录的 .gdbinit 总是最后加载
D. 使用 -nx 参数可跳过所有初始化文件

环境变量传递(多选)
A. set env FOO=BAR 影响后续 run
B. unset env PATH 会清空程序的PATH变量
C. show env 显示当前GDB进程的环境变量
D. SHELL 变量控制 run 命令的参数字符串解析

exec-wrapper 与 startup-with-shell(多选)
A. set exec-wrapper env 可包装环境变量
B. set startup-with-shell off 禁用shell解析参数
C. 二者均可用于重定向I/O(如 > log.txt)
D. exec-wrapper 适用于远程调试

命令行参数处理(多选)
A. run arg1 “arg2” 会保留 arg2 的双引号
B. set args --option=value 需转义等号
C. show args 显示上次 run 使用的参数
D. set args 修改后需重启程序生效

地址随机化(多选)
A. set disable-randomization on 默认启用
B. 禁用随机化后堆地址固定
C. 不影响栈地址的随机性
D. 对PIE(位置无关可执行文件)无效

答案
实践题答案

会显示索引。GDB初始化文件加载顺序:system.gdbinit → ~/.config/gdb/gdbinit → ~/.gdbinit → ./.gdbinit。后加载的文件覆盖前者,最终生效的是 ./.gdbinit 的 off 设置。
验证命令:
echo “set print array-indexes on” > ~/.config/gdb/gdbinit
echo “set print array-indexes off” > ./.gdbinit
gdb -q ./program
(gdb) print array # 观察输出是否含索引

使用 exec-wrapper:
(gdb) set exec-wrapper env ‘LD_PRELOAD=libcustom.so’
(gdb) run

原理:exec-wrapper 在程序启动前注入环境变量,不污染全局环境。

方案对比:

方案1:attach 父子进程
(gdb) attach 1234 # 调试父进程
(gdb) add-inferior # 新建调试会话
(gdb) attach 5678 # 调试子进程

优点:灵活控制;缺点:需手动切换会话。
方案2:set follow-fork-mode child
(gdb) set follow-fork-mode child
(gdb) run # 自动附加到子进程

优点:自动跟踪;缺点:无法同时调试父进程。

转义命令:
(gdb) run file=“data.txt”

原因:双引号是shell元字符,未转义会导致参数被截断。

禁用ASLR:
(gdb) set disable-randomization on
(gdb) run

原理:关闭操作系统的地址空间布局随机化(ASLR),使堆、栈地址固定。

多选题答案

A、B
解析:C错(工作目录文件可能被跳过),D错(-nx 跳过初始化文件但保留早期配置)。
A、B、D
解析:C错(show env 显示目标程序的环境)。
A、B
解析:C错(二者不处理I/O重定向),D错(exec-wrapper 仅限本地)。
B、D
解析:A错(双引号被剥离),C错(show args 显示下次 run 的参数)。
A、D
解析:B错(堆地址仍可能变化),C错(栈地址也会固定)。

http://www.dtcms.com/a/394289.html

相关文章:

  • 基础IO
  • Linux开发工具
  • DIDCTF-2023陇剑杯
  • 软件设计师软考备战:第四篇 计算机网络技术
  • 基于 GEE 利用 Sentinel-1 SAR 数据计算标准化双极化水体指数(SDWI)实现水体智能识别
  • 120-armv8_a_power_management:高级架构电源管理指南
  • 【MySQL初阶】02-库的操作
  • Kafka面试精讲 Day 20:集群监控与性能评估
  • 【C语言】数字模式求和算法的巧妙实现:深入解析循环与累加的艺术
  • 关系型数据库对比
  • 手机可视化方案(针对浓度识别)
  • LLM在应用计量经济学和因果推断中作用的思考
  • Redis 事务机制详解:从原理到实战
  • 【精品资料鉴赏】130页PPT汽车智能制造企业数字化转型SAP解决方案参考
  • 【区间贪心】P2859 [USACO06FEB] Stall Reservations S|普及+
  • Java进阶教程,全面剖析Java多线程编程,阻塞队列方式实现等待唤醒机制,笔记17
  • 【SAP小说】阿根廷项目的SAP突围:2025阿根廷平行账项目纪实
  • 具有广泛宿主范围的噬菌体在生态系统中很常见
  • 【Linux】进程概念(四):Linux进程优先级与进程调度的核心逻辑
  • @ModelAttribute 和@RequestBody有什么区别
  • npm玩转技巧
  • 柔性精密测量技术在小型化载荷微小应变监测方面的应用
  • 命令注入(Command Injection)漏洞学习笔记
  • 268-基于Django的热门游戏榜单数据分析系统
  • C++篇 类和对象(2)万能工具怎么用?
  • MySQL 多实例部署与主从、读写分离配置
  • C++初阶(10)string类
  • 高性能开源 Web 服务器软件--Nginx
  • 软考中级习题与解答——第十章_多媒体技术(2)
  • 【字符串】1.最⻓公共前缀(easy)