GDB的基础和使用
GDB 的安装与配置
安装 gdb
如果系统已经预装好了就不要再安装了
sudo apt install gdb
安装 gdb-multiarch
sudo apt install gdb-multiarch
安装 GDB 插件
这些类似的插件其实大同小异,安装插件只是因为原生的 GDB 没有高亮、观察寄存器和堆栈信息不方便
注意:这几个插件并不兼容,但可以通过更改
.gdbinit
文件来切换使用,当你要使用某一个插件的时候,将.gdbinit
文件中其他两个插件注释掉在后面会介绍怎么使用脚本来进行切换 gdb 插件,就不需要手动去注释了
我这里统一将 GDB 插件安装在 /opt/gdb_plugins/
目录下
如果是 Ubuntu 16.04 这样的老版本安装 GDB 插件,请参照《Ubuntu16.04虚拟机环境搭建》一文的《GDB 配置》部分
安装 peda
安装方法:
sudo git clone https://github.com/longld/peda.git /opt/gdb_plugins/peda
sudo echo "source /opt/gdb_plugins/peda/peda.py" >> ~/.gdbinit
安装 pwndbg
pwndbg
与Pwngdb
需要一起搭配使用以下方法默认安装的是当前最新版
pwndbg
,如果想要使用旧版本,请到官网手动下载指定版本的源代码:Releases · pwndbg/pwndbg
安装方法:
sudo git clone https://github.com/pwndbg/pwndbg /opt/gdb_plugins/pwndbg
cd /opt/gdb_plugins/pwndbg
sudo ./setup.sh
在安装
pwndbg 2024.02.14
这种较新版本时,需要安装 python 3.12 环境以及虚拟环境,否则可能会报如下错误:Creating virtualenv in path: ./.venv ./setup.sh: line 199: /usr/bin/python3.12: No such file or directory
Creating virtualenv in path: ./.venv The virtual environment was not created successfully because ensurepip is not available. On Debian/Ubuntu systems, you need to install the python3-venv package using the following command. apt install python3.12-venv You may need to use sudo with that command. After installing the python3-venv package, recreate your virtual environment. Failing command: /opt/gdb_plugins/pwndbg/.venv/bin/python3.12
安装所需环境后,再次执行
sudo ./setup.sh
即可解决:sudo apt install python3.12 python3.12-venv
另外,现在新版本的 pwndbg
支持以 deb 的形式安装使用:
wget https://github.com/pwndbg/pwndbg/releases/download/2024.02.14/pwndbg_2024.02.14_amd64.deb
sudo dpkg -i pwndbg_2024.02.14_amd64.deb
安装后,可以直接在终端启动:
pwndbg
deb 安装的 pwndbg
不再是以 GDB 插件的形式存在,而是一个集成了 GDB 的独立工具,其 GDB 版本为 GNU gdb (GDB) 13.2
:
安装 Pwngdb
安装方法:
sudo git clone https://github.com/scwuaptx/Pwngdb.git /opt/gdb_plugins/Pwngdb
cd /opt/gdb_plugins/Pwngdb
sudo cp .gdbinit ~/
sudo vim ~/.gdbinit
在 ~/.gdbinit
的第二行插入:(记得插入在 source /opt/gdb_plugins/Pwngdb/pwngdb.py
这一句的前面)
source /opt/gdb_plugins/pwndbg/gdbinit.py
如果 pwndbg 的路径不是 /opt/gdb_plugins/pwndbg
,请按照自己的实际路径修改
配置 pwndbg 分屏调试
由于 pwndbg 输出的信息较多,经常在一页上看不全,需要上下翻找,眼花缭乱
我们可以设置 pwndbg 分屏调试,一边屏幕输入命令,一边屏幕查看输出信息,提高效率
方法一:修改 gdbinit
配置很简单,先后打开两个终端
假设先打开的一个终端用于开启 gdb 调试并输入调试命令,后打开的一个终端用于输出调试信息
在两个终端分别输入 tty
,先打开的终端为 /dev/pts/19
,后打开的为 /dev/pts/20
(以自己的实际输出信息为主)
修改 ~/.gdbinit
中的内容:
sudo gedit ~/.gdbinit
在 ~/.gdbinit
末尾加入一句:
set context-output xxx
# 这里的 xxx 就是用于输出调试信息的分屏,我这里是:/dev/pts/20
注意:如果你开了多个终端,就设置为实际想要用于输出调试信息的分屏
保存退出
在先打开的终端中开启 gdb 并输入调试命令,在后打开的终端中即可输出调试信息
然后我们将屏幕调整一下:
设置分屏后,如果只开启一个终端,使用 gdb 可能会遇到如下报错:
Exception occurred: context: [Errno 13] 权限不够: '/dev/pts/20' (<class 'PermissionError'>) For more info invoke `set exception-verbose on` and rerun the command or debug it by yourself with `set exception-debugger on`
再开启一个终端即可解决 (新开启的终端需为 /dev/pts/20)
方法二:gdb 临时设置
由于有时候新开启的终端并不是我们在 ~/.gdbinit
中设置的那个终端,频繁更改 ~/.gdbinit
中的内容未免太过麻烦
所以,我们也可以不在 ~/.gdbinit
中设置,而是先在一个终端中启动 gdb 调试,然后再另开一个新的终端,使用 tty
查看新的终端的分屏信息:
tty
# 假设输出为:/dev/pts/18
然后在 gdb 中直接设置输出调试信息的分屏: (以自己上一步实际的分屏信息为主)
(gdb) set context-output /dev/pts/18
这样就可以避免新打开的终端与我们在 ~/.gdbinit
中设置的终端不一致的问题
安装 gef
安装方法:
sudo git clone https://github.com/hugsy/gef /opt/gdb_plugins/gef
sudo echo "source /opt/gdb_plugins/gef/gef.py" >> ~/.gdbinit
验证安装
- 自用的
~/.gdbinit
文件内容示例:
# source /opt/gdb_plugins/peda/peda.py
source /opt/gdb_plugins/pwndbg/gdbinit.py
# source /opt/gdb_plugins/gef/gef.py
source /opt/gdb_plugins/Pwngdb/pwngdb.py
source /opt/gdb_plugins/Pwngdb/angelheap/gdbinit.py
define hook-run
python
import angelheap
angelheap.init_angelheap()
end
end
终端输入
gdb
:
- 当启用 peda 时,会出现:
gdb-peda$
- 当启用 pwndbg 时,会出现:
pwndbg>
- 当启用 gef 时,会出现:
gef➤
如果验证出现上述输出内容,并且在输出内容之前没有任何报错提示,则说明安装成功
如果没有安装成功,输入
gdb
会显示默认的:(gdb)
脚本自动切换 GDB 插件
这个脚本可以自动定位
~/.gdbinit
文件中"# this place is controled by user's shell"
这一句所在的位置,并随着用户对于插件的选择,自动更改~/.gdbinit
文件里的内容,更改的内容会放在"# this place is controled by user's shell"
这一句的上一行如果你安装 gdb 插件是按照我的教程来的,那下面的东西你可以不需要做任何改动,否则可能会有所修改
- 这一步非常重要!!!
首先,你需要自己手动在 ~/.gdbinit
文件的第二行加上 "# this place is controled by user's shell"
这一句
然后把原本 ~/.gdbinit
文件中的内容全部放到 "# this place is controled by user's shell"
这一句后面
记得把插件全部注释掉
修改好的 ~/.gdbinit
文件类似于我这样:
(第一行空出来,什么都不写)
# this place is controled by user's shell
# source /opt/gdb_plugins/peda/peda.py
# source /opt/gdb_plugins/pwndbg/gdbinit.py
# source /opt/gdb_plugins/gef/gef.py
source /opt/gdb_plugins/Pwngdb/pwngdb.py
source /opt/gdb_plugins/Pwngdb/angelheap/gdbinit.py
define hook-run
python
import angelheap
angelheap.init_angelheap()
end
end
- 新建一个名为 gdb.sh 的文件:
sudo vim gdb.sh
并将下面的脚本内容添加进去
可能改动的地方都做了标注,如果你的插件是按照我的教程来安装的,那就直接用就行了
#!/bin/bash
function Mode_change {
name=$1
gdbinitfile=~/.gdbinit # 这个路径按照你的实际情况修改
peda="source /opt/gdb_plugins/peda/peda.py" # 这个路径按照你的实际情况修改
gef="source /opt/gdb_plugins/gef/gef.py" # 这个路径按照你的实际情况修改
pwndbg="source /opt/gdb_plugins/pwndbg/gdbinit.py" # 这个路径按照你的实际情况修改
sign=$(cat $gdbinitfile | grep -n "# this place is controled by user's shell")
# 此处上面的查找内容要和你自己的保持一致
pattern=":# this place is controled by user's shell"
number=${sign%$pattern}
location=$[number-1]
parameter_add=${location}i
parameter_del=${location}d
message="TEST"
if [ $name -eq "1" ];then
sed -i "$parameter_del" $gdbinitfile
sed -i "$parameter_add $pwndbg" $gdbinitfile
echo -e "Please enjoy the pwndbg!\n"
elif [ $name -eq "2" ];then
sed -i "$parameter_del" $gdbinitfile
sed -i "$parameter_add $peda" $gdbinitfile
echo -e "Please enjoy the peda!\n"
else
sed -i "$parameter_del" $gdbinitfile
sed -i "$parameter_add $gef" $gdbinitfile
echo -e "Please enjoy the gef!\n"
fi
}
echo -e "Please choose one mode of GDB?\n1.pwndbg 2.peda 3.gef"
read -p "Input your choice:" num
if [ $num -eq "1" ];then
Mode_change $num
elif [ $num -eq "2" ];then
Mode_change $num
elif [ $num -eq "3" ];then
Mode_change $num
else
echo -e "Error!\nPleasse input right number!"
fi
gdb $1 $2 $3 $4 $5 $6 $7 $8 $9
- 把 gdb.sh 文件移到
/usr/bin/
目录下,类似于 Windows 的环境变量,在这个目录下可以通过 cmd 直接调用:
sudo mv ./gdb.sh /usr/bin/
- 给 gdb.sh 文件增加执行权限:
cd /usr/bin/
sudo chmod a+x gdb.sh
- 以后再打开 gdb 的时候,直接输入
gdb.sh
即可:
我的脚本也是根据 GDB插件控制——切换pwndbg,peda,gef - 简书 (jianshu.com) 修改的
所以,如果你是按照我的教程来安装的,那就没什么问题
但如果你不是,那可能需要对脚本做一些修改,可以自己去参考一下上面的简书链接
GDB 的使用方法
首先要保证使用 gcc 编译时加上参数 -g 生成调试信息
一些可缩写的等价指令:
操作 | 完整指令 | 简洁指令 |
---|---|---|
下断点 | break | b |
下临时断点 | tbreak | tb |
为已设断点添加条件 | condition | cond |
删除断点 | detele | d |
运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令 | run | r |
单步执行,跳过子函数 | next | n |
单步执行,进入子函数 | step | s |
直接执行到下一断点或程序结束 | continue | c |
查看信息 | info | i |
列出程序源码,一次可列出 10 行 | list | l |
打印变量的值 | p | |
显示当前函数调用栈的完整信息,包括调用函数和它们的参数 | backtrace | bt |
查看栈帧中某一帧的信息 | frame | f |
设置监视点 | watch | wat |
设置要自动显示的变量、表达式或函数的值,可以在调试过程中持续监视特定变量的值 | display | disp |
运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息 | finish | fi |
执行程序,直到达到指定行号,可用于退出循环体,而不必逐行执行 | until | u |
退出调试 | quit | q |
反汇编 | disassemble | disass |
其他指令:
操作 | 完整指令 |
---|---|
启动程序并在 main() 函数处停止(等价于先 b main 再 r) | start |
清除断点,与 delete 类似,但只需要行号,无需断点编号 | clear |
禁用断点 | disable |
启用断点 | enable |
终止调试 | kill |
将当前程序执行流跳转到指定行或地址 | jump |
查看当前栈帧上面的栈帧 | up |
查看当前栈帧下面的栈帧 | down |
查看当前目录 | pwd |
修改数值 | set |
gdb 中什么都不输入,直接回车,表示:重复上一步的指令
启动 GDB 调试
启动 GDB 可添加的参数:(可选)
--args
:指定启动参数--quiet
:不打印 gdb 版本信息--directory=DIR
:指定源码的搜索路径示例:
gdb --args --quiet --directory=DIR ./testApp a b c
一般程序调试与附加调试
- 调试可执行文件
gdb 文件名
- 调试正在运行的进程(附加调试)
gdb attach 进程pid
其他指令:
(gdb) gdb 文件名 进程pid
(gdb) gdb -p 进程pid
也可以先进入 GDB,再调试进程:
gdb
(gdb) attach 进程pid
快速获取进程号 pid:
pidof 文件名
- 通过 Pwntools 附加 GDB 调试:
io = process("程序路径")
gdb.attach(io)
pause()
core 文件调试
gdb 出现崩溃的文件名 core文件名
程序在运行之后发生了错误,会生成一个 core 文件,通过调试 core 文件,可以分析程序崩溃的原因
其中,core 文件名大多数是 core.进程pid
的形式
- 设置 core 文件的大小,直接在命令终端输入命令即可,大小可作为开关使用
Linux 系统默认 core 文件的大小限制为 0,即产生 segmentation-fault 段错误时不会生成 core 文件
ulimit -c 0 # 将 core 文件大小设置为 0,此时将不生成 core 文件
ulimit -c 2 # 将 core 文件大小设置为 2 KB,自动生成 core 文件,文件大小达到 2 KB 时发生截断
ulimit -c unlimited # 将 core 文件大小设置为无上限,自动生成 core 文件,文件大小不添加特殊限制
- 设置 core 文件生成目录、命名规则
修改 /proc/sys/kernel/core_pattern
文件内容可以设置 core 文件名包含的信息内容和目录(目录必须存在),注意使用 echo
或 sysctl
命令,vim 可能不能成功编辑内容
echo "/home/core/core_%p_%t" > core_pattern # 将 core 文件统一生成到 /home/core 目录下,产生的文件名为 core_进程pid_时间
sysctl -w "kernel.core_pattern=core_%p_%t" >/dev/null # 文件生成到默认目录(进程目录),文件名格式为 core_进程pid_时间
信息内容参数包括:
参数 | 含义 |
---|---|
%p | 进程 pid |
%u | 当前 uid |
%g | 当前 gid |
%s | 导致产生 core 的信号 |
%t | core 文件生成时的 unix 时间 |
%h | 主机名 |
%e | 命令名 |
- 设置文件名中以进程 PID 作为扩展名
修改 /proc/sys/kernel/core_uses_pid
文件,文件内容为 1,添加进程 PID 作为文件扩展,为 0 不添加
echo "1" > /proc/sys/kernel/core_uses_pid # 以进程 PID 作为 core 文件扩展名,生成的文件名称类似于 core.13125
远程 GDB 调试
- 在被调试主机上,启动 gdbserver:
gdbserver IP地址:端口号 文件名 传递参数(可选)
# 远程调试正在运行的进程
gdbserver IP地址:端口号 --attach 进程pid
# 利用 ps | grep 进程名 | awk '{print $1}' 快速获取进程 pid
gdbserver IP地址:端口号 --attach `ps | grep 进程名 | awk '{print $1}'`
一般可以使用 127.0.0.1:端口号
、localhost:端口号
,也可以省略 IP 地址直接使用 :端口号
端口号可以任意指定,一般选择大于 1024 的端口号(尽量避免端口号冲突)
- 在调试主机上,通过 gdb 连接:
gdb 文件名
(gdb) target remote IP地址:端口号
- 如果无法连接,可能是被调试主机的防火墙问题
查看 gdbserver 端口是否被防火墙过滤(在调试主机上运行,端口号为 gdbserver 对应端口号):
nmap -p 端口号 IP地址
如果显示端口状态为 filtered
说明被过滤
Linux 关闭 gdbserver 对应端口的防火墙(在被调试主机上运行,端口号为 gdbserver 对应端口号):
iptables -A INPUT -p tcp --dport 端口号 -j ACCEPT
或者在本地的情况下,直接关闭防火墙也可以(真实环境下不建议)
fork 多进程调试
查看 GDB 当前设置的多进程调试状态:
(gdb) show follow-fork-mode
(gdb) show detach-on-fork
设置 fork 子进程调试的参数:
(gdb) set follow-fork-mode parent|child
# parent: fork 之后继续调试父进程,子进程不受影响
# child: fork 之后调试子进程,父进程不受影响
(gdb) set detach-on-fork on|off
# on: 只有当前被调试的进程能够执行
# off: gdb 将控制父进程和子进程
参数的详细说明见下表:
follow-fork-mode 参数 | detach-on-fork 参数 | 含义 |
---|---|---|
parent | on | 只调试父进程(GDB 模式) |
child | on | 只调试子进程 |
parent | off | 同时调试两个进程, GDB 调试父进程,子进程阻塞在 fork 处 |
child | off | 同时调试两个进程, GDB 调试子进程,父进程阻塞在 fork 处 |
查看正在调试的进程:
(gdb) info inferior
切换调试的进程,假设 info inferior
获取到的进程编号为 Num_id
:
(gdb) inferior Num_id
pthread 多线程调试
查看 GDB 当前设置的多线程调试状态:
(gdb) show follow-fork-mode
(gdb) show detach-on-fork
设置 fork 子进程调试的参数:
(gdb) set scheduler-locking on|off|on step
# on: 只有当前被调试的线程能够执行
# off: gdb 将控制父线程和子线程
# on step:在单步执行的时候,除了next过一个函数的情况以外,只有当前线程会执行
查看正在调试的线程:
(gdb) info thread
切换调试的线程,假设 info threads
获取到的线程编号为 Num_id
:
(gdb) thread Num_id
让一个或者多个线程执行 GDB 命令 command
:
(gdb) thread apply Num_id1 Num_id2 ... command
让所有被调试线程执行GDB命令 command
:
(gdb) thread apply all command
在所有线程中相应的行上设置断点:
(gdb) break thread_test.c:123 thread all
断点
下断点是调试程序时的重要手段,能够帮助定位和修复程序中的错误和问题。它允许开发者在程序执行期间以精细的粒度检查程序的运行状态,从而更轻松地进行调试
断点除 break 外,还有 tbreak,简写为:tb
tbreak 用于添加一个临时断点,断点一旦被触发就自动删除,使用方法同 break
下断点
gdb 下断点的指令为:
break xxx
,也可以简写为:b xxx
下断点的位置都是在将要执行这一句但还未执行的时候
- 直接断在
main()
函数的入口处,因为 main 是一个全局的符号,也可以断在其他函数的入口处,例如:fun_name()
(gdb) break main
(gdb) break fun_name
- 在源代码 test.c 的第 line 行下断点
(gdb) break /绝对路径/test.c:line
(gdb) break 'test.c':line
(gdb) break line
- 根据运行时的地址设置断点,例如:0xDEADBEEF,地址前需要加上
*
号
(gdb) break *0xDEADBEEF
- 设置条件断点,当 a > b 时设置函数
fun_name()
断点 (条件断点只支持简单的数据判断)
(gdb) break fun_name if a>b
为已设断点添加条件,假设已存在的断点编号为 index:
(gdb) condition index a>b
(gdb) cond index a>b
与 break if
类似,只是 condition
只能用在已存在的断点上
- 在当前程序暂停位置的前/后 offset 行处下断点
(gdb) break -/+ offest
删除断点
删除断点的指令有
detele xxx
和clear xxx
两种:delete 是全局的,不受栈帧的影响;
delete 命令可以删除所有断点,包括观察点和捕获点等clear 命令受到当前栈帧的制约,删除的是将要执行的下一处指令的断点;
clear 命令不能删除观察点和捕获点
- 删除所有断点
(gdb) delete
- 删除编号为 index 的断点
(gdb) delete index
- 删除编号为 index1 - index2 的断点(包括 index2)
(gdb) delete index1-index2
- 删除编号为 index1 - index2 的断点(包括 index2)和编号为 index3 - index4 的断点(包括 index4)
(gdb) delete index1-index2 index3-index4
- 删除源代码第 line 行的断点,无需断点编号
(gdb) clear line
- 删除
fun_name()
函数的断点(只会删除函数入口处的断点,即:b fun_name
所创建的断点,函数内部断点不会删除)
(gdb) clear fun_name
这会删除所有的 fun_name()
函数断点,如果有多个同名函数断点,这些同名函数断点都会被删除
禁用和启用断点
断点的 Enb 参数有 y 和 n 两种,y 表示断点启用,n 表示断点禁用
- 禁用或启用编号为 index 的断点
(gdb) disable index
(gdb) enable index
- 禁用或启用编号为 index1 - index2 的断点(包括 index2)
(gdb) disable index1-index2
(gdb) enable index1-index2
- 禁用或启用编号为 index1 - index2 的断点(包括 index2)和编号为 index3 - index4 的断点(包括 index4)
(gdb) disable index1-index2 index3-index4
(gdb) enable index1-index2 index3-index4
运行程序
控制程序执行
以下指令主要用于控制程序执行,不需要参数
操作 | 完整指令 | 简洁指令 |
---|---|---|
运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令 | run | r |
单步执行,跳过子函数 | next | n |
单步执行,进入子函数 | step | s |
直接执行到下一断点或程序结束 | continue | c |
执行程序,直到达到指定行号,可用于退出循环体,而不必逐行执行 | until | u |
退出调试 | quit | q |
另外,还有 ni
和 si
,也是单步执行的命令,用法与 n 和 s 相同,只不过 ni 和 si 的单步执行是针对汇编代码的
注意:until 指令可以设置参数,让程序运行至源代码的某行,可以不仅仅用来跳出循环
- 运行程序至源代码第 line 行
(gdb) until line
- 终止调试的程序
(gdb) kill
跳转指令
jump:将当前程序执行流跳转到指定行或地址,简写为 j
jump 命令有两点需要注意的:
- 中间跳过的代码是不会执行的,例如变量的初始化等,因此很可能会导致程序崩溃或出现其它 Bug
- 如果 jump 跳转到的位置后续没有断点,那么 gdb 会直接执行自跳转处开始的后续代码
- 跳转到源代码第 line 行的位置
(gdb) jump line
- 跳转到距离当前代码下 offest 行的位置
(gdb) jump +offest
- 跳转到某个地址,例如:0xDEADBEEF,地址前需要加上
*
号
(gdb) jump *0xDEADBEEF
退出函数
实际调试时,在某个函数中调试一段时间后,可能不需要再一步步执行到函数返回处,希望直接执行完当前函数,这时可以使用 finish 命令
return 和 finish 都是退出函数,但也有差别:
- return 命令:立即退出当前函数,剩下的代码不会执行了,return 还可以指定函数的返回值
- finish 命令:会继续执行完该函数剩余代码再正常退出
查看程序相关信息
查看程序状态
gdb 查看程序状态的指令为:
info xxx
,也可以简写为:i xxx
info 命令用于检索有关程序状态的信息,例如变量、函数、堆栈、线程等
- 查看断点信息
(gdb) info break
(gdb) info b
- 查看当前函数中的本地变量
(gdb) info locals
- 查看当前函数的参数
(gdb) info args
- 查看当前设置的监视点列表(用于监视变量的值变化)
(gdb) info watch
- 查看当前已设置的 display
(gdb) info display
- 查看程序中的线程列表以及它们的状态
(gdb) info threads
(gdb) info th
- 查看函数调用栈帧的基本信息,包括堆栈帧的数量、堆栈帧的地址范围和其他有关堆栈帧的信息
(gdb) info stack
效果与 backtrace 相同:
(gdb) backtrace
(gdb) bt
其中,#0 项是当前执行的函数(栈帧)
- 查看当前函数调用的栈帧信息
(gdb) info frame
(gdb) info f
查看栈帧第 n 帧的信息:
(gdb) frame n
(gdb) f n
(gdb) info frame n
查看地址为 address 的桢的相关信息:
(gdb) frame address
(gdb) info frame address
查看当前栈帧上/下面第 n 桢的信息:
(gdb) up n
(gdb) down n
- 查看所有寄存器的当前值
(gdb) info registers
(gdb) info r
单独查看某个寄存器的值,例如:RAX
(gdb) info r rax
- 查看已加载的共享库信息
(gdb) info sharedlibrary
(gdb) info sh
- 查看有关目标程序和调试器的信息
(gdb) info target
(gdb) info tar
- 查看可执行文件的所有函数名称
(gdb) info functions
(gdb) info fun
只显示函数名带有 abcd 的函数名称:
(gdb) info functions abcd*
(gdb) info fun abcd*
- 查看程序是否运行
(gdb) info program
- 查看当前目录
(gdb) pwd
(pwndbg) !pwd
查看变量的值
gdb 查看变量的值的指令为:
print xxx
,也可以简写为:p xxx
print 命令可允许自定义输出格式:
(gdb) print/参数 变量名
参数 | 功 能 |
---|---|
/x | 以十六进制的形式打印出整数 |
/d | 以有符号、十进制的形式打印出整数 |
/u | 以无符号、十进制的形式打印出整数 |
/o | 以八进制的形式打印出整数 |
/t | 以二进制的形式打印出整数 |
/f | 以浮点数的形式打印变量或表达式的值 |
/c | 以字符形式打印变量或表达式的值 |
- 查看变量、数组、结构体成员或任何合法的表达式的值
查看变量 variable 的值:
(gdb) print variable
查看数组元素:
(gdb) print array[3]
查看结构体成员:
(gdb) print struct.member
查看合法表达式的值:
(gdb) print variable + 5
- 查看 array[] 数组中,自第 m 个元素起总共 n 个数组元素的值
int array[5] = {1,2,3,4};
(gdb) print array[m-1]@n
当 m = 1,n = 2 时,输出:$1 = {1, 2}
- 当程序中包含多个作用域不同但名称相同的变量时(全局变量、局部变量),可以借助
::
运算符明确指定要查看的目标变量
例如源文件 main.c :
1. #include <stdio.h>
2.
3. int num = 10;
4.
5. int main()
6. {
7. int num = 20;
8. return 0;
9. }
当程序执行到第 8 行 return 0;
时
- 指定具体的文件名,查看全局变量 num
(gdb) print 'main.c'::num
- 指定具体的函数的函数名,查看局部变量 num
(gdb) print main::num
当然,由于程序执行到第 8 行 return 0;
时,gdb 调试就暂停在 main()
函数中
因此即便不指明 main::
,直接使用 print num
,这里的 num 默认指代的也是局部变量 num
- 查看变量 variable 的地址
(gdb) print &variable
- 查看某个地址 address 上的值
(gdb) print *address
- 查看某个寄存器的值,例如:RAX
(gdb) print $rax
- 还可以直接拿 gdb 当计算器用,例如:计算偏移 0xdd - 0x55 的值
(gdb) print 0xdd-0x55
查看内存数据
x 用于查看内存中的数据,允许以不同的格式打印内存中的内容
(gdb) x/[n/f/u] [表达式]
其中,n、f、u 是可选的参数
n 是一个正整数,表示从当前地址开始,向后显示 n 个地址的内容
f 表示显示的格式,具体可选参数如下:
参数 | 含义 |
---|---|
x | 按十六进制格式显示变量 |
d | 按十进制格式显示变量 |
u | 按十六进制格式显示无符号整型 |
o | 按八进制格式显示变量 |
t | 按二进制格式显示变量 |
a | 按十六进制格式显示变量 |
c | 按字符格式显示变量 |
f | 按浮点数格式显示变量 |
除此之外,s 可指定以字符串形式输出,i 可指定以汇编形式输出
例如:(gdb) x/20s $rbp (gdb) x/20i $rip
- u 表示从当前地址往后请求的字节数,如果不指定的话,GDB 默认是 4 字节,可指定的字节数格式如下:
格式 | 含义 |
---|---|
b | 以字节(8 位)格式打印 |
h | 以半字(16 位)格式打印 |
w | 以字(32 位)格式打印 |
g | 以长字(64 位)格式打印 |
s | 以零结尾的字符串格式打印 |
- 查看内存地址 0x0804a010 开始的 5 个内存数据,以十六进制格式输出
(gdb) x/5x 0x0804a010
- 从变量 len 的首地址开始,打印 4 个字节,以十六进制的形式
(gdb) x/4xb &len
- 查看栈空间的内容,输出 20 条,以字(32 位)格式输出
(gdb) x/20w $rbp
查看栈空间数据
- 查看程序栈空间的数据
(pwndbg) stack
- 显示 n 条栈空间数据
(pwndbg) stack n
查看栈上的返回地址
- 查看包含返回地址的栈地址
(pwndbg) retaddr
查看 Canary 的值
- 查看栈上的 Canary 的值
(pwndbg) canary
查看 PLT 和 GOT 表
- 查看程序的 PLT 表
(pwndbg) plt
- 查看程序的 GOT 表
(pwndbg) got
查看虚拟内存空间
- 查看程序各段地址的权限
(gdb) vmmap
修改程序内的数据
set 用于更改或设置各种调试器选项和变量,以自定义 gdb 的行为和功能,这些选项和变量可以影响调试会话的方式
- 通过 set 修改某个变量 num 的值为 1 (需在变量前加上 variable,可以简写为 var)
(gdb) set variable num=1
(gdb) set var num=1
修改数组元素的值同理
通过 print 指令也可以修改某个变量 num 的值为 1
(gdb) print num=1
- 修改存储地址在
0x7fffffffde90
,指定类型为 int 的值为 1
(gdb) set {int}0x7fffffffde90=1
# 修改前:
# 0x7fffffffde90: 0x010203040506
# 修改后:
# 0x7fffffffde90: 0x010200000001
也可以通过指针来实现:
(gdb) set *0x7fffffffde90=1
注意:set 命令一次修改 4 个字节
例如:将内存地址
0x7fffffffde90
处存放的0x010203040506
改为0xabcdef070809
# 如果对内存数据不熟悉的话,可以先查看每个字节对应的地址(一般为小端序) (gdb) x/8xb # 输出为: # 0x7fffffffde90: 0x06 0x05 0x04 0x03 0x02 0x01 0x00 0x00 # 则,对应的地址关系为: # 0x7fffffffde90 处存放 0x06,0x7fffffffde91 处存放 0x05,0x7fffffffde92 处存放 0x04 ...... 以此类推,0x7fffffffde97 处存放 0x00 # 由于一次修改 4 字节,这里需要修改 6 字节(其实一个内存地址处一般存放的是 8 字节,这里的 6 字节其实最高位还有 2 字节为 0x00 0x00 省略没写),因此可以分为两步 # 1. 首先修改低位的 4 字节 (gdb) set *0x7fffffffde90 = 0xef070809 # 2. 然后修改高位的 2 字节,如果加上最高位的 2 字节就是 0x0000abcd (gdb) set *0x7fffffffde94 = 0xabcd
通过指针寄存器来指定内存地址,例如地址为 rbp - 10
:(寄存器前注意加上 '$'
)
(gdb) set {int}($rbp-10)=1
也可以通过指针来实现: (注意要转换 $rbp-10
的类型,使用 int 还是 long 根据程序位数来确定)
(gdb) set *((unsigned int)$rbp-10)=1
(gdb) set *((unsigned long)$rbp-10)=1
具体变量的类型和地址可以通过 print 指令查看,例如变量 num:
(gdb) print &num
# 输出为:
# $1 = (int *) 0x7fffffffde90
- 修改某个寄存器的值,例如:RAX
(gdb) set var $rax=1
- 指定函数运行时的参数,例如:10、20、30
(gdb) set args 10 20 30
查看运行参数:
(gdb) shows args
查看 GDB 配置信息
可以通过 show 指令来获取 GDB 本身设置相关的一些信息
- 查看设置好的运行参数
(gdb) show args
- 查看程序的运行路径
(gdb) show paths
- 查看环境变量
(gdb) show environment 变量名
监视程序
设置监视点
watch 用于设置监视点可以监视变量的值,当变量的值发生更改时,gdb 会中断程序的执行,简写为:wat
注意:当设置的观察点是一个局部变量时,局部变量失效后,观察点也会失效
- 为某个变量或表达式设置监视点
监视变量 variable 的值:
(gdb) watch variable
监视数组元素的值:
(gdb) watch array[3]
监视表达式的值:
(gdb) watch variable + 5
- 指定条件监视点,只在特定条件 variable == 42 满足时中断:
(gdb) watch variable == 42
监视点类似于断点,也可以通过 info 来进行查看,使用 delete 进行删除,管理方法与断点相同
持续监视变量
display 设置要自动显示的变量、表达式或函数的值,而无需手动输入 print 命令,可以在调试过程中持续监视特定变量的值,简写为:disp
每次 gdb 中断,都会自动输出这些被监视变量或内存的值
- 为某个变量或表达式设置监视点
监视变量 variable 的值:
(gdb) display variable
监视数组元素的值:
(gdb) display array[3]
监视表达式的值:
(gdb) display variable + 5
- 删除设置的 display,假设编号为 index (可通过
info display
查看)
(gdb) undisplay index
查看源代码
list 命令默认只会输出 10 行源代码,也可以使用如下命令修改:
show listsize
:查看 list 命令显示的代码行数set listsize count
:设置 list 命令显示的代码行数为 count
- 列出程序的源代码,默认每次显示 10 行
(gdb) list
- 显示以行号 line 为中心的 10 行源代码(前后各 5 行)
(gdb) list line
- 显示函数名 fun_name 所在函数的源代码
(gdb) list fun_name
- 显示从行号 x - y 的源代码(没有显示 10 行的限制)
(gdb) list x,y
查看汇编代码
- 将某个函数
fun_name()
完整反汇编
(gdb) disassemble fun_name
- 指定要反汇编的地址
当仅指定一个地址 address1 时,将反汇编包含给定地址的整个函数,包括其上方的指令:
(gdb) disassemble address1
指定要反汇编的起始地址 address1 和结束地址 address2,只会反汇编起始地址和结束地址之间的指令:
(gdb) disassemble address1,address2
- 指定从地址 address1 或函数
fun_name()
开始,长度为字节数 len 进行反汇编
(gdb) disassemble fun_name,+len
(gdb) disassemble address1,+len
- 同时显示函数
fun_name()
的反汇编指令和相对应的源代码
(gdb) disassemble /m fun_name
- 显示函数
fun_name()
的反汇编指令和汇编指令的字节码
(gdb) disassemble /r fun_name
- 显示 RIP 寄存器所在位置的汇编代码
(gdb) disassemble $rip
窗口布局
- 分割窗口,可以一边查看代码,一边测试
(gdb) layout
- 显示源代码窗口
(gdb) layout src
- 显示反汇编窗口
(gdb) layout asm
- 显示源代码/反汇编和 CPU 寄存器窗口
(gdb) layout regs
- 同时显示源代码和反汇编窗口
(gdb) layout split
- 切换到下/上一个布局模式
(gdb) layout next
(gdb) layout prev
一些关于窗口布局的快捷键:
- 刷新窗口:Ctrl + L
- 显示一个窗口:先按下 Ctrl + X,然后松手,再单独按一个 1
- 显示两个窗口:先按下 Ctrl + X,然后松手,再单独按一个 2
- 关闭窗口布局:先按下 Ctrl + X,然后松手,再单独按一个 A
堆相关命令
以下命令主要适用于 pwndbg 插件
参考文章:
pwndbg 基本操作指令 - MuRKuo - 博客园
pwndbg部分命令用法搬运_pwndbg vis-CSDN博客
- 显示
main_arena
特定地址的arena
的详细信息
(pwndbg) arena
显示所有 arena
的基本信息:
(pwndbg) arenas
(pwndbg) arenainfo
- 查看所有种类的
bins
堆块的链表情况
(pwndbg) bins
查看 top_chunk
的地址和大小:
(pwndbg) top_chunk
查看指定地址处的 malloc_chunk
:
(pwndbg) malloc_chunk address fake # 如果这个 chunk 是一个 fake chunk 的时候需要加上 fake 选项
单独查看 fast bin
链表:
(pwndbg) fastbins
单独查看 large bin
链表:
(pwndbg) largebins
单独查看 small bin
链表:
(pwndbg) smallbins
单独查看 unsorted bin
链表:
(pwndbg) unsortedbin
单独查看 tcache bin
链表:
(pwndbg) tcachebins
查看 malloc 线程缓存信息 tcache
的详细信息:
(pwndbg) tcache
- 以数据结构的形式显示所有堆块
(pwndbg) heap
可视化指定地址的堆块(默认大小为 10):
(pwndbg) vis_heap_chunks
(pwndbg) vis
查看堆的起始地址:
(pwndbg) heapbase
显示堆的信息:
# 和 bins 的挺像的,没有 bins 好用
(pwndbg) heapinfo
(pwndbg) heapinfoall
显示堆结构:
(pwndbg) parseheap
提示所有操作堆的地方:
(pwndbg) tracemalloc
- 打印出 Glibc 中的
mp_ structure
(pwndbg) mp