gbd调试器的使用
环境:Win11+Kali子系统
启动
启动gdb
1 | ┌──(root㉿Executor)-[/home/kali] |
看到命令提示符号变成(gdb)
则启动成功
安静启动gdb -q
1 | ┌──(root㉿Executor)-[/home/kali] |
1 | --silent也可以写成-q,-quiet |
分屏启动gdb -tui
1 | ┌──(root㉿Executor)-[/home/kali/mydir] |
上方窗口是源代码窗口,下方是gbd命令行窗口
这样启动不需要另开一个终端观察源代码
并且当程序在端点停下的时候上方窗口也会显示当前程序停止的位置
上方的源代码窗口使用上下箭头移动视野
分屏+安静+指定调试程序gdb -tui -q <prog_name>
注意使用gdb调试的文件必须是可执行文件(windows上的.exe或者linux上的.out等)
并且在编译该可执行文件的时候==必须加入-g选项==生成gbd调试信息
如果不使用-g生成了.out文件然后使用gdb调试则
1
2
3
4
5
6
7
8 ┌──(root㉿Executor)-[/home/kali/mydir]
└─# gcc main.c -Og -o main.out
┌──(root㉿Executor)-[/home/kali/mydir]
└─# gdb --silent main.out
Reading symbols from main.out...
(No debugging symbols found in main.out) #报告没有调试信息
(gdb)使用-tui打开,源代码都看不到
1 | ┌──(root㉿Executor)-[/home/kali/mydir] |
带参数启动--args <程序> <参数1> <参数2>...
1 | ┌──(root㉿Executor)-[/home/kali/mydir] |
这里指定的参数1 2 3
4将会作为main.out
执行时的命令行参数
启动后运行前
加载需要调试的程序
当在命令行直接使用gdb命令打开gdb调试器时,此时是没有指定需要调试的程序的
工作目录pwd
默认工作目录是打开gdb的位置,gdb启动后也可以使用pwd
命令观察当前工作目录
1 | ┌──(root㉿Executor)-[/home/kali/mydir] |
指定调试程序位置file <prog_name>
对于当前目录下的程序可以直接使用程序名
1 | ┌──(root㉿Executor)-[/home/kali/mydir] |
对于其他目录下的可以使用绝对或者相对位置
1 | (gdb) file /home/kali/mydir/main.out |
查看信息
查看当前工作目录pwd
1 | (gdb) pwd |
查看是否找到目标程序文件list
1 | (gdb) list |
查看调试程序语言show language
1 | (gdb) show language |
查看源文件信息info source
1 | (gdb) info source |
查看可以设置的程序语言set language
1 | (gdb) set language |
查看程序运行状态info program
1 | (gdb) info prog |
设置信息
设置命令行参数set args <参数1> <参数2>...
1 | (gdb) set args 1 2 3 |
如果在启动时有指定参数,此时再用set指定参数则会覆盖启动时设置的参数
设置语言'set language <语言>'
1 | (gdb) set language c |
运行
运行程序run
命令行参数使用启动时指定的参数或者set args设置的参数,如果都没有给定则无参数执行
如果有断点则程序在第一个断点处停止,否则直接运行完.
带参数运行run <参数1> <参数2>...
此参数将会直接作为运行参数,覆盖前面设置的参数
main停止运行start
start
相当于在main函数处下了断点然后run
,自动在main开始前停下
运行时
断点
设置断点b <行号>
断点可以运行前设置也可以运行时设置
1 | (gdb) b 6 |
如果以-tui分屏打开,则设置好的断点会显示在行号左侧,大写的B+>意味当前程序暂停的断点
b <函数名>
直接给函数下断点
1 | (gdb) b main |
删除断点
delete <断点编号>
注意端点编号不是行号
删除全部断点则不指定编号,直接delete
删除指定行上的断点clear <行号>
条件断点b if <条件>
比如如果没有输入命令行参数时才给main函数下断点
1 | (gdb) b main if argc==1 #用户没有输入时argc=1,第一个参数是当前程序位置 |
查看断点信息info b <断点号>
1 | Breakpoint 11 at 0x555555555139: file main.c, line 5. |
info b
查看所有断点信息
1 | (gdb) info b |
查看信息
print命令
查看函数信息p <函数名>
函数信息也可以在运行前查看
1 | (gdb) p main |
1 | {返回值类型(参数1类型,参数2类型)} 函数地址 <函数名> |
1 | (gdb) whatis main |
查看变量信息p <变量名>
查看变量信息必须是程序在该变量下文的断点处停下
即当前程序的运行位置必须已经经过变量,并且变量没有消亡
比如函数中的局部变量在函数返回之后就会消亡,只能在函数中断点然后查看断点之前的变量
如图调试一个用循环计算阶乘的函数,将断点下在第10行result*=n
处
当程序第一次执行到次时会停在result*=n
==执行前==的状态
如图第一次在第10行停下,打印result=1
查看寄存器信息p $<寄存器名>
对于刚才的fact循环求阶乘函数,最后返回值是result,可想而知,该值是存放在rax寄存器中的
1 | int fact(int n){ |
下面调试程序验证猜想
还是将断点下到第10行while循环中
逐次进行循环,观察rax寄存器中的值
与result的变化是一致的
也可以查看程序计数器rip
中的值,观察程序当前进行位置
用objdump反编译然后观察fact+13处的指令
fact+13=0x1139+0x13=0x114c
,该位置是一个test %edi,%edi
指令,而n作为第一个参数是存放在edi寄存器中的
紧接着114e处jg 1146
意味如果R[%edi]=n>0
则跳转1146位置,
而1146位置在114e上方,相当于跳进了循环,
也就是说0x114c处相当于循环判断while(n>0)
即程序在第10行的断点停下时rip中是第9行中的条件判断指令
x/<大小> <位置>检查字节或者字
x/20b fact
检查fact函数的前20个字节
与objdump得到的反汇编是一样的
x/2g 0x555555555139 检查从0x555555555139地址开始的双字
info命令
查看所有寄存器信息info registers
其中rax
存放result,rdi
存放n
查看栈帧'info frame'
disas命令
反汇编当前程序暂停的函数disas
首先要在函数里下断点,然后程序在该断点暂停时使用disas可以观察当前函数的反汇编信息
可见反汇编信息中也会有当前断点位置信息
如此就不用再开一个终端使用objdump观察了
反汇编指定名称的函数disas <函数名>
此方法不需要在函数中下断点
1 | (gdb) disas fact |
反汇编某个地址附近的函数disas <地址>
1 | disas 0x000055555555514c |
继续执行
执行有多种情况,通常会与断点或者一些逻辑结构联合使用,
比如在断点处停下或者不停下
在循环处,在函数中都有特殊的命令决定如何执行
1 | 源代码层面 |
源代码层面
以一个递归求阶乘的程序为例子
1 | ┌──(root㉿Executor)-[/home/kali/mydir] |
next单步步过
在main函数处下断点,使得程序上来先停一下,让我们有机会一行一行地执行
1 | (gdb) b main |
第一个n命令使得程序断在15:ans=fact(123456)
第二个n命令使得程序断在16:printf("%d",ans)
此时使用print命令观察ans的值发现其确实是9的阶乘,即第15行是自动执行然后返回了值的,单步步过只是忽略了执行细节,只要函数的执行后果
step单步步入
还是在main函数处下断点,然后使用step
第一个step命令会让程序断在15:ans=fact(9)
这与next是相同的
但是下一个step会进入step并在都5行停下
continue执行到下一个断点
在main函数开始(line 13)和main函数中打印ans前(line16)各打一个断点
run运行之后会在13行停下,然后在输入c命令则会直接在16行停下
此时print命令打印ans值发现为362880确实是9的阶乘,即两个断点之间的所有程序都被执行过了
finish一直运行到当前函数返回
分两种情况,有没有进入函数
使用step命令让程序在第一层递归函数==入口前==停下,此时使用finish
发现程序直接返回到了main函数中,并且带着返回值362880
恰好是9的阶乘,说明递归函数各层都执行了
现在让程序在第一层递归函数的==内部==停下
发现进入的递归函数的第二层
return 放弃函数未执行的部分,直接返回到调用者
fact(6)返回到fact(7)
然后一直使用finish命令返回main函数
发现ans值并没有被正确地计算
即return会放弃下文
机器码层面
until 在循环体的机器码的最高地址时挑出循环到第一条高于循环地址的指令
调试一个使用循环计算阶乘的函数fact
1 | ┌──(root㉿Executor)-[/home/kali/mydir] |
在main函数打一个断点方便单步调试
一直使用step单步步入命令,直到第一次到达while条件判断的时候,使用disas观察反汇编代码
此时对应指令fact+19位置
此时再使用一次单步步入,进入循环,line 12:result*=n
对应指令fact+13位置
即源代码的执行顺序和机器码相反
显然是由于刚才的fact+21的jg条件跳转满足,跳到了fact+13
此时使用until只是相当于step命令,因为until只会在循环的机器码层面的最大地址处才会有快速执行循环的作用
继续disas观察反汇编发现line13:--n
对应反汇编的fact+16
此时再使用u
源代码层面进行循环条件判断,对应机器码层面test判断,而fact+19就是循环体在机器层面的最高地址
此时再使用u
直接返回了main函数,这是因为fact函数中while循环结束立刻就返回了,对应机器码
第23行是循环外首条高于循环的地址,该条指令又是返回,因此返回了main函数
返回main后打印ans值发现是362880是9的阶乘,证明until指令会执行循环体剩下的部分