gdb

背景

不管是C开发还是做pwn的ctf题,调试总是少不了的,gdb又是很多大佬经常用来分析的工具,不学是不行的啊,醉了。现在文章的内容只是gdb工具很少的一部分,但对于初学者仍有参考价值。

参考链接:

  • 《Linux C编程:一站式学习》

测试环境:

  • centos 7
  • GNU gdb (GDB) Red Hat Enterprise Linux 8.2-15.el8

调试的基本思想分析现象-假设错误原因-产生新的现象去验证假设

基础命令

书籍中的实例:

#include <stdio.h>

int add_range(int low, int high)
{
	int i, sum = 0;
	for (i = low; i <= high; i++)
		sum = sum +  i;
	return sum;
}

int main(void)
{
	int result[100];
	result[0] = add_range(1, 10);
	result[1] = add_range(1, 100);
	printf("result[0] = %d
result[1] = %d
", result[0], result[1]);
	return 0;
}

从main函数开始阅读本程序,定义了一个int型的数组,数组长度为100,add_range函数的作用是将传入的值进行累加,之后将得出的值传入到result数组中,但只传入两个值,最后将结果输出出来。

在程序编译时,需要加上-g选项,生成的可执行文件才能进行源码级调试。参考链接:https://blog.csdn.net/qq_40860852/article/details/89388723

[root@i-co6yw0e5 c]# gcc -g for_gdb.c -o test
[root@i-co6yw0e5 c]# ./test 
result[0] = 55
result[1] = 5050
[root@i-co6yw0e5 c]# gdb --version
GNU gdb (GDB) Red Hat Enterprise Linux 8.2-15.el8
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

注:gdb的-g参数不是将源代码嵌入到可执行文件中,所以在调试的时候,源代码不可删除,不然会存在gdb找不到源文件的情况。

使用gdb开始调试编译后的可执行程序,命令gdb 可执行程序名。使用help命令,可以查看命令的类别。

[root@i-co6yw0e5 c]# gdb test
GNU gdb (GDB) Red Hat Enterprise Linux 8.2-15.el8
………………

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from test...done.
(gdb) help  #help命令
List of classes of commands:

aliases -- Aliases of other commands
breakpoints -- Making program stop at certain points
………………
tracepoints -- Tracing of program execution without stopping the program
user-defined -- User-defined commands

也可以进一步查看某一类别有哪些命令,例如files:

(gdb) help files
Specifying and examining files.

List of commands:

add-symbol-file -- Load symbols from FILE
……
symbol-file -- Load symbol table from executable file FILE
……

使用list命令,从第一行开始列出代码,一次只显示10行,如果要继续显示,可以再输入list或者直接回车,直接回车表示重复上一条命令。

(gdb) list
1	#include <stdio.h>
2	
3	int add_range(int low, int high)
4	{
5		int i, sum = 0;
6		for (i = low; i <= high; i++)
7			sum = sum +  i;
8		return sum;
9	}
10	

gdb很多命令有简写形式,list可以简写为l,列出一个函数的源代码,也可以使用函数名作为参数:

(gdb) l main
7			sum = sum +  i;
8		return sum;
9	}
10	
11	int main(void)
12	{
13		int result[100];
14		result[0] = add_range(1, 10);
15		result[1] = add_range(1, 100);
16		printf("result[0] = %d
result[1] = %d
", result[0], result[1]);

使用start命令开始执行程序,gdb停在main函数中变量定义后的第一条语句,列出的语句是即将执行的下一条语句,可以使用next(简写n)控制语句一条一条的执行。

(gdb) start
Temporary breakpoint 1 at 0x4005d1: file for_gdb.c, line 14.
Starting program: /root/c/test 
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-151.el8.x86_64

Temporary breakpoint 1, main () at for_gdb.c:14
14		result[0] = add_range(1, 10);
(gdb) n
15		result[1] = add_range(1, 100);
(gdb) 
16		printf("result[0] = %d
result[1] = %d
", result[0], result[1]);
(gdb) 
result[0] = 55
result[1] = 5050
17		return 0;

从结果上看,虽然控制了程序的执行过程,但是并不详细,可以使用step(简写s)进入到add_range函数中跟踪执行,使用start命令重新执行可执行程序。

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 2 at 0x4005d1: file for_gdb.c, line 14.
Starting program: /root/c/test 

Temporary breakpoint 2, main () at for_gdb.c:14
14		result[0] = add_range(1, 10);
(gdb) s
add_range (low=1, high=10) at for_gdb.c:5
5		int i, sum = 0;
(gdb) backtrace
#0  add_range (low=1, high=10) at for_gdb.c:5
#1  0x00000000004005e0 in main () at for_gdb.c:14

这次停在了add_range函数中变量定义之后的第一条语句,通过backtrace(简写bt),可以查看函数调用的栈帧。从bt命令返回的内容中可以看到,add_range函数是被main函数调用的,main函数传进来的值是low=1,high=10main函数栈帧编号为1add_range的栈帧编号为0。可以通过info(简写i)来查看add_range函数局部变量的值。info的命令内容有很多,可以使用help info命令查看使用方法。

(gdb) i locals
i = 0
sum = 0

如果想要查看main函数当前局部变量的值,可以使用frame(简写f)选择1号栈帧,然后再查看局部变量的值。

(gdb) f 1
#1  0x00000000004005e0 in main () at for_gdb.c:14
14		result[0] = add_range(1, 10);
(gdb) i locals
result = {-134223960, 32767, 1, 0, 0, 0, 0, 0, 0, 0, 1700966438, 0, -7032, 32767, -6976, 32767, -134223080,…… , 0, 4195949, 0, -136447344, 32767, 0, 0, 4195872, 0, 4195504, 0, -6784, 32767, 0, 0}

result数组中的值是杂乱的,因为未经初始化的局部变量具有不确定的值。使用s命令继续运行程序,然后使用print(简写p)打印变量sum的值。

(gdb) s
7			sum = sum +  i;
(gdb) 
6		for (i = low; i <= high; i++)
(gdb) p sum
$1 = 1
(gdb) s
7			sum = sum +  i;
(gdb) 
6		for (i = low; i <= high; i++)
(gdb) p sum
$2 = 3

如果程序运行一切正常,再继续往下跟踪没有意义,可以使用finish命令让程序一直运行到当前函数返回为止,当前正在进行赋值操作。

(gdb) finish
Run till exit from #0  add_range (low=1, high=10) at for_gdb.c:6
main () at for_gdb.c:14
14		result[0] = add_range(1, 10);
Value returned is $3 = 55 #赋值

使用s命令继续执行,将进入到add_range(1, 100)的执行步骤中,如果在执行中发现变量有误,可以使用set命令修改变量的值,再使用finish命令查看有没有其他错误。

(gdb) s
15		result[1] = add_range(1, 100);
(gdb) 
add_range (low=1, high=100) at for_gdb.c:5
5		int i, sum = 0;
(gdb) p sum
$7 = 55
(gdb) s
6		for (i = low; i <= high; i++)
(gdb) 
7			sum = sum +  i;
(gdb) p sum
$8 = 0
(gdb) set var sum = 100
(gdb) s
6		for (i = low; i <= high; i++)
(gdb) p sum
$9 = 101
(gdb) finish
Run till exit from #0  add_range (low=1, high=100) at for_gdb.c:6
main () at for_gdb.c:15
15		result[1] = add_range(1, 100);
Value returned is $10 = 5150

修改变量的值除了使用set命令,也可以使用print命令,因为print命令后面可以跟表达式,而赋值和函数调用也可以是表达式,所以可以用print命令修改变量的值或者调用函数。(实验显示'printf' has unknown return type; )

总结本节所使用的命令:

命令 描述
backtrace,bt 查看各级函数调用及参数
finish 连续运行到当前函数返回为止,然后停下来等待命令
frame,f (帧编号) 选择栈帧
info,i (locals) 查看当前栈帧局部变量的值
list,l 列出源代码,接着上次的位置往下列,每次列10行
list 行号 列出从第几行开始的源代码
list 函数名 列出某个函数的源代码
next,n 执行下一行语句
print,p 打印表达式的值,通过表达式可以修改变量的值,或者调用函数
quit,q 推出gdb环境
set var 修改变量的值
start 开始执行程序,停在main函数第一行语句前面等待命令
step,s 执行下一行语句,如果有函数调用则进入函数中

断点

书籍中的实例:

#include <stdio.h>

int main(void)
{       
        int sum = 0, i = 0;
        char input[5];
        while(1)
        {       
                scanf("%s", input);
                for (i = 0; input[i] !=''; i++)
                {       
                        sum = sum * 10 + input[i] - '0';
                }       
                printf("input = %d
", sum);
        }       
        return 0;
}       

首先从键盘读入一串数字存储到字符数组input中,然后经过计算后再打印出来。数组的最后一位会添加,这个我们不需要,然后由字符型(比如'1'),转换为int型,可以使用-'0'操作,然后再一直循环下去。编译并运行程序。

[root@i-co6yw0e5 c]# ./duandian 
123
input = 123
134
input = 123134
(ctrl + c 退出程序)

从结果中可以看到,第一次结果是对的,但第二次结果是不对的,这是因为sum一直保存了上一次的结果。在使用gdb进行调试的时候,可以使用display命令来跟踪显示每次停下来时sum的值。

(gdb) display sum
2: sum = 0
(gdb) n
12				sum = sum * 10 + input[i] - '0';
2: sum = 0
(gdb) 
10			for (i = 0; input[i] !=''; i++)
2: sum = 1
(gdb) 
12				sum = sum * 10 + input[i] - '0';
2: sum = 1
(gdb) 
10			for (i = 0; input[i] !=''; i++)
2: sum = 12
(gdb) 
12				sum = sum * 10 + input[i] - '0';
2: sum = 12

undisplay命令可以取消跟踪显示,变量sum的编号为2,可以使用undisplay 2命令取消它的跟踪显示。

(gdb) undisplay 2
(gdb) n
10			for (i = 0; input[i] !=''; i++)

如果不像一步一步跟踪这个循环,可以使用break(简写b)命令设置一个断点(breakpoint)。比如在第9行,b 9

(gdb) l
1	#include <stdio.h>
2	
3	int main(void)
4	{
5		int sum = 0, i = 0;
6		char input[5];
7		while(1)
8		{
9			scanf("%s", input);
10			for (i = 0; input[i] !=''; i++)
(gdb) b 9
Note: breakpoint 3 also set at pc 0x40060c.
Breakpoint 5 at 0x40060c: file duandian.c, line 9.
(gdb) display sum
3: sum = 0
(gdb) n

Breakpoint 3, main () at duandian.c:9
9			scanf("%s", input);
3: sum = 0
(gdb) 
123
10			for (i = 0; input[i] !=''; i++)
3: sum = 0
(gdb) 
12				sum = sum * 10 + input[i] - '0';
3: sum = 0
(gdb) c
Continuing.
input = 123

Breakpoint 3, main () at duandian.c:9
9			scanf("%s", input);
3: sum = 123
(gdb) 
Continuing.
345
input = 123345

Breakpoint 3, main () at duandian.c:9
9			scanf("%s", input);
3: sum = 123345

break命令的参数也可以是函数名,表示在某个函数的开头设置断点。设置断点后,使用continue(简写c)命令连续运行,而不是单步运行,程序到达断点会自动停下来,这样就可以停在下一次循环的开头。

通过上面的调试,可以发现出错的原因是因为新的循环没有把sum归零。至于应该在哪里设置断点,怎么知道哪些代码可以跳过而哪些代码要慢慢跟踪,也要通过对错误现象的分析和假设来确定。

可以通过info breakpoints命令来查看已经设置的断点。

(gdb) i breakpoints
Num     Type           Disp Enb Address            What
3       breakpoint     keep y   0x000000000040060c in main at duandian.c:9
	breakpoint already hit 3 times
5       breakpoint     keep y   0x000000000040060c in main at duandian.c:9
	breakpoint already hit 3 times

每个断点都有一个编号,可以通过指定编号删除某个断点。

(gdb) delete breakpoint 3
(gdb) delete breakpoint 5
(gdb) i breakpoints
No breakpoints or watchpoints.

如果有断点暂时不用,可以将其禁用而不必删除,之后用的时候再启用。

(gdb) b 9
Breakpoint 6 at 0x40060c: file duandian.c, line 9.
(gdb) i breakpoints
Num     Type           Disp Enb Address            What
6       breakpoint     keep y   0x000000000040060c in main at duandian.c:9
(gdb) disable breakpoints 6
(gdb) i breakpoints
Num     Type           Disp Enb Address            What
6       breakpoint     keep n   0x000000000040060c in main at duandian.c:9

区别就在于Enb下面的y/n

gdb的断点功能非常灵活,可以设置在满足某个条件时才激活,break 9 if sum != 0,然后使用run命令(简写r),重新从程序开头连续运行。

命令 描述
break,b(行号) 在某一行设置断点
break 函数名 在某个函数开头设置断点
break……if…… 设置条件断点
continue,c 从当前位置开始连续运行程序
delete breakpoints 断点号 删除断点
display 变量名 跟踪查看某个变量,每次停下来都显示它的值
disable breakpoints 断点号 禁用断点
enable 断点号 启用断点
info,i (breakpoints) 查看当前设置了哪些断点
run,r 从头开始连续运行程序
undisplay 跟踪显示号 取消跟踪显示

观察点

书籍中的实例:

#include <stdio.h>

int main(void)
{       
        int sum = 0, i = 0;
        char input[5];
        while(1)
        {       
                sum = 0;
                scanf("%s", input);
                for(i = 0; input[i] != ''; i++)
                        sum = sum * 10 + input[i] - '0';
                printf("input = %d
", sum);
        }       
        return 0;
}

上面代码修正了2中sum不归零的问题,但是因为使用了scanf函数,如果输入的字符串过长,会造成数组越界,而数组访问越界是不会检查的,所以scanf会写出界,执行结果如下所示:

[root@i-co6yw0e5 c]# ./guancha 
123
input = 123
67
input = 67
12345
input = 123407

input数组只有5个元素,当我们输入12345后,scanf还会在数组末尾自动添加,使用x命令可以查看的更清楚一点。

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 6 at 0x4005fe: file guancha.c, line 5.
Starting program: /root/c/guancha 

Temporary breakpoint 6, main () at guancha.c:5
5		int sum = 0, i = 0;
(gdb) n
9			sum = 0;
(gdb) 
10			scanf("%s", input);
(gdb) 
12345
11			for(i = 0; input[i] != ''; i++)
(gdb) x/7b input
0x7fffffffe483:	0x31	0x32	0x33	0x34	0x35	0x00	0x00

x命令打印指定存储单元的内容,7b是打印格式,b表示每个字节一组,7表示打印7组,从input数组的第一个字节开始连续打印7个字节。从结果可以看出,前5个字节是input数组的存储单元字节,打印的是十六进制ASCII码的1~5,第6个字节是写出界的''

从执行12345的结果看,第5个字符错了,也就是i从0到3的循环没有错,我们可以设置一个条件断点,从i等于4开始单步调试。

(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 11 at 0x4005fe: file guancha.c, line 5.
Starting program: /root/c/guancha 

Temporary breakpoint 11, main () at guancha.c:5
5		int sum = 0, i = 0;
(gdb) b 12 if i == 4
Breakpoint 12 at 0x400632: file guancha.c, line 12.
(gdb) c
Continuing.
12345

Breakpoint 12, main () at guancha.c:12
12				sum = sum * 10 + input[i] - '0';
(gdb) p sum
$5 = 1234

从上面的结果看,在i=4之前,sum的值为1234是没有错的,但最终结果却是123407,说明input[4]的值不是'5'了。这种推理,行,但是不严谨,看。

(gdb) x/7b input
0x7fffffffe483:	0x31	0x32	0x33	0x34	0x35	0x04	0x00

input[4]的值确实为0x35。产生123407还有一种可能,这个数是123450减掉一个数得到的,按道理讲,input此时应该是字符串的末尾了,不应该有下一次循环,但控制条件是input[i] != '',而本应该是0x00的位置变成了0x04,因此循环不会结束。

(gdb) p sum
$5 = 1234
(gdb) x/7b input
0x7fffffffe483:	0x31	0x32	0x33	0x34	0x35	0x04	0x00
(gdb) n
11			for(i = 0; input[i] != ''; i++)
(gdb) p sum
$6 = 12345
(gdb) x/7b input
0x7fffffffe483:	0x31	0x32	0x33	0x34	0x35	0x04	0x00
(gdb) n
12				sum = sum * 10 + input[i] - '0';
(gdb) x/7b input
0x7fffffffe483:	0x31	0x32	0x33	0x34	0x35	0x05	0x00

单步执行一步,此时sum的值就是我们期望输出的12345,注意列出的for循环内容是下一步要执行的内容,所以这一步其实执行的是列出for循环上一步的sum = sum * 10 + input[i] - '0';。继续单步执行,这一步是for循环,判断循环条件,因为'5'后面的值是0x04,所以又进入到了循环体中。同时,input数组中的0x04又变为了0x05,这个原因没有解释。

下一步,对sum求值,也就是这一步,求出来了123407,这个数是12345*10+0x05-0x30得到的。在进行多余一次的循环后,下次一定会退出循环,因为0x05后面是''

input[4]后面那个字节是什么时候变的?可以使用观察点(watchpoint)来观察。断点是当程序执行到某一代码行时中断,而观察点是当程序访问某个存储单元时中断,如果我们不知道某个存储单元是在哪里被改动的,这时候观察点尤其有用。

接下来我们删除之前的断点,防止其对我们的操作有影响。重新运行程序,使用watch命令设置观察点,跟踪input[4]后面的那个字节(可以用input[5]表示,虽然这是访问越界)。

(gdb) delete breakpoints
Delete all breakpoints? (y or n) y
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 13 at 0x4005fe: file guancha.c, line 5.
Starting program: /root/c/guancha 

Temporary breakpoint 13, main () at guancha.c:5
5		int sum = 0, i = 0;
(gdb) n
9			sum = 0;
(gdb) 
10			scanf("%s", input);
(gdb) 
12345
11			for(i = 0; input[i] != ''; i++)
(gdb) watch input[5] #设置观察点
Hardware watchpoint 14: input[5]
(gdb) i watchpoints
Num     Type           Disp Enb Address            What
14      hw watchpoint  keep y                      input[5]
(gdb) c
Continuing.

Hardware watchpoint 14: input[5]

Old value = 0 '00'
New value = 1 '01'
0x0000000000400659 in main () at guancha.c:11
11			for(i = 0; input[i] != ''; i++)
(gdb) 
Continuing.

Hardware watchpoint 14: input[5]

Old value = 1 '01'
New value = 2 '02'
0x0000000000400659 in main () at guancha.c:11
11			for(i = 0; input[i] != ''; i++)
(gdb) 
Continuing.

Hardware watchpoint 14: input[5]

Old value = 2 '02'
New value = 3 '03'
0x0000000000400659 in main () at guancha.c:11
11			for(i = 0; input[i] != ''; i++)
(gdb) 
Continuing.

Hardware watchpoint 14: input[5]

Old value = 3 '03'
New value = 4 '04'
0x0000000000400659 in main () at guancha.c:11
11			for(i = 0; input[i] != ''; i++)

从上面内容可以看到,每次回到for循环开头前,该值都会发生增1现象,而循环变量i正是在每次回到循环开头之前加1,所以input[5]就是变量i的存储单元,i的存储单元是紧跟在input数组后面的。

书中的试题:

#include <stdio.h>

int main(void)
{
    int sum = 0, i = 0;
    char input[5];
    while(1)
    {
        sum = 0;
        scanf("%s", input);
        for(i = 0; input[i] != ''; i++)
        {
            if(input[i] < '0' || input[i] > '9')
            {
                printf("Invalid input
");
                sum = -1;
                break;
            }
            sum = sum * 10 + input[i] - '0';
        }
        printf("input = %d
", sum);
    }
    return 0;
}

这个题就不详细讲了,这个出现在判断的时候。因为多进行了一次循环,也就是input[5]进入到了循环体中,而input[5]的值为'05',注意,它的值是要比'0'的ascii码值(48)小的,所以最终输出Invalid input, input = -1

(gdb) 
13				if(input[i] < '0' || input[i] > '9')
1: i = 5
2: input[i] = 5 '05'
(gdb) 
15					printf("Invalid input
");
1: i = 5
2: input[i] = 5 '05'
(gdb) 
Invalid input
16	

本节用到的gdb命令。

命令 描述
watch 设置观察点
info,i (watchpoints) 查看设置了哪些观察点
x 从某个位置开始打印存储单元的内容,全部当成字节看,而不区分哪个字节属于哪个变量

段错误

参考链接:

当程序试图访问不允许访问的内存位置,或试图以不允许的方式访问内存位置(例如尝试写入只读位置,或覆盖部分操作系统)时会发生段错误。

书籍中的实例一:

#include <stdio.h>

int main(void)
{
	int man = 0;
	scanf("%d", man);
	return 0;
}

在gdb中运行,遇到段错误,程序会自动停下来。

(gdb) r
Starting program: /root/c/duan 
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-151.el8.x86_64
1234

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7a6e19c in __GI__IO_vfscanf () from /lib64/libc.so.6

gdb显示段错误出现在__GI__IO_vfscanf函数中,用bt命令可以看到这个函数是被scanf函数调用的,所以是scanf这一行代码引发的段错误,原因是man前面少了一个&

书籍中的实例二:

#include <stdio.h>

int main(void)
{
	int sum = 0, i = 0;
	char input[5];
	scanf("%s", input);
	for(i = 0; input[i] != ''; i++)
	{
		if(input[i] < '0' || input[i] > '9')
		{
			printf("Invalid input!
");
			sum = -1;
			break;
		}
		sum = sum * 10 + input[i] - '0';
	}
	printf("input = %d
", sum);
	return 0;
}

编译并使用gdb进行调试。

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/c/duan2 
123dsfasgeg43rt5dfgwerg34gsdgf
Invalid input!
input = -1

Program received signal SIGSEGV, Segmentation fault.
0x00000000004006f9 in main () at duan2.c:20
20	}
(gdb) l
15			}
16			sum = sum * 10 + input[i] - '0';
17		}
18		printf("input = %d
", sum);
19		return 0;
20	}

gdb提示段错误出现在20行,但是20行只有main函数结束的花括号。这可以算是一条规律,如果某个函数的局部变量发生访问越界,有可能并不立即产生段错误,而是在函数返回时产生段错误。

本博客虽然很垃圾,但所有内容严禁转载
原文地址:https://www.cnblogs.com/ahtoh/p/15246820.html