一、实验目的
- 掌握使用 gcc 分步编译 c 代码为可执行程序步骤以及 gcc 常用选项作用
- 掌握 linux 系统下 gcc 编译器和 gdb 调试器的配合
- 掌握 Linux 系统下 gdb 调试器的使用
二、实验现象
1.对比添加-g选项编译前面可执行程序的大小,添加-g生成的可执行程序会比较大,因为其中包括调试信息,如果要使用gdb对源码进行调试必须添加-g进行编译。
2.对比gdb各命令的执行效果
run 重新开始运行程序,但是以前设置的断开信息不会丢失
break 可用来设置一个断开,让程序运行遇到断点停止
continue 继续全速运行程序直到遇到断点停下来
step 如果当前代码是调用一个函数,则会进入到函数源码单步运行
next 和 s 命令相似,不同点是把前代码是函数调用时,并不会进入函数源码内部单步执行
print 可以输出变量的值
finish 可以快速跳过当前函数
quit 可以结束gdb 调试
三、实验准备
- 系统需求:安装有 Windows 7 及以上版本的操作系统的电脑;
- 硬件配置:内存 4G+,硬盘 200G+,处理器 I3 以上;
- 软件需求:安装 VMware 虚拟机、以及在 VMware 中已经安装好Ubuntu 18.04;
四、Linux GDB调试实验流程
1、要使用 gdb 调试器来调试程序,和编译程序时候也需要给 gcc 编译器指定编译选项才可以。本小节先通过一个简单的c 程序来学习gcc 分步编译源文件到可执行程序的分步编译方法以及常用选项。
2、使用vim或gedit编辑器编写测试代码,具体内容如下:
@:01_gcc_test$ gedit hello.c
编写内容如下:
#include <stdio.h> int main (void) { printf("hello world\n") ; return 0; }
3、gcc编译文件
gcc 指令的一般格式为:gcc [选项] 要编译的文件 [选项] [目标文件]
例:使用 gcc 编译命令,编译 hello.c 生成可执行文件 hello,并运行 hello
@:01_gcc_test$ gcc hello.c -o hello @:01_gcc_test$ ./hello hello world
上面的命令一步由.c 文件生成了可执行文件,将gcc 的四个编译流程:预处理、编译、汇编、连接一步完成,下面将介绍四个流程分别做了什么工作。
4、-E 选项的作用:只进行预处理,不做其他处理。
例:只对 hello.c 文件进行预处理,生成文件 hello.i,并查看
@:01_gcc_test$ gcc -E hello.c -o hello.i @:01_gcc_test$ ls hello hello.c hello.i
通过查看可以看到头文件包含部分代码#include <stdio.h>经过预处理阶段之后,编译器已将 stdio.h的容贴了进来。
5、-S 选项的使用
-S 选项的作用:只是编译不汇编,生成汇编代码
例:将 hello.i 文件只进行编译而不进行汇编,生成汇编代码 hello.s
@:01_gcc_test$ gcc -S hello.i -o hello.s @:01_gcc_test$ ls hello hello.c hello.i hello.s
6、-c 选项的使用
-c 选项的作用:只是编译不连接,生成目标文件.o
例:将汇编代码 hello.s 只编译不成 hello.o 文件
@:01_gcc_test$ gcc -c hello.s -o hello.o @:01_gcc_test$ ls hello hello.c hello.i hello.o hello.s
7、将编译好的hello.o 库,生成可执行文件hello
@:01_gcc_test$ gcc -o helloo hello.o @:01_gcc_test$ ./hello hello world
8、-static 选项的使用
-static 选项的作用:静态库
例:比较 hello.c 连接动态库生成的可执行文件 hello 和静态库生成的可执行文件 hello1 的大小默认是使用动态库链接:
@:01_gcc_test$ gcc -o hello hello.c
添加 -static 编译时会使用静态库链接:
@:01_gcc_test$ gcc -static -o hello_static hello.c
对比使用静态库和动态库链接的最后可执行文件大小:
@:01_gcc_test$ ls -lh hello hello_static -rwxr-xr-x 1 8.2K 2 月 2 14:31 hello -rwxr-xr-x 1 826K 2 月 2 14:32 hello_static
结果显示静态库的可执行文件 hello_static 比动态库的可执行文件 hello 要大的多,但是它们的执行效果是一样的。
9、-g 选项的使用
-g 选项的作用:在可执行程序中包含标准调试信息
例:将 hello.c 编译成包含标准调试信息的可执行文件 hello2
@:01_gcc_test$ gcc -g hello.c -o hello2
对比添加-g 选项前后生成的可执行程序文件大小:
@:01_gcc_test$ ls hello hello2 -hl -rwxr-xr-x 1 8.2K 2 月 2 14:31 hello -rwxr-xr-x 1 11K 2 月 2 14:35 hello2
带有标准调试信息的可执行文件可以使用 gdb 调试器进行调试,以便找出逻辑错误。
10、-O2 选项的使用
-O2 选项的作用:完成程序的优化工作
例:将 hello.c 用 O2 优化选项编译成可执行文件 hello3,和正常编译产生的可执行文件 hello 进行比较
@:01_gcc_test$ gcc -O2 hello.c -o hello3
五、Linux GDB 调试器
接着上小节,本节编写一个稍微复杂一点的代码来学习 gdb 调试程序的方法,我们新创建一个新的代码目录,在新目录中编写测试代码。
@:01_gcc_test$ cd .. @:syslab04$ pwd
1、先用 vim 或 gedit 编辑器编辑文件 test.c 用于 gdb 调试器调试,容如下:
#include <stdio.h> int main(void) { int sum(int sum); int i,result=0; sum(100); for(i=1;i<=100;i++){ result+=i; } printf("The sum in main function is %d\n",result); return 0; } int sum(int num) { int i,n=0; for(i=0;i<=num;i++){ n+=i; } printf("The sum in sum function is %d\n",n); }
2、将test.c 文件编译成包含标准调试信息的文件 test
@:02_gdb_test$ gcc -g -o test test.c //#注意必须添加 -g 选项 @:02_gdb_test$ ls test test.c
3、启动gdb 进行调试
@:02_gdb_test$ gdb test GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git 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. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details.
可以看到 gdb 启动界面中显示了 gdb 的版本、自由软件等信息,然后进入了有”gdb”开头的命令行界面
4、l(list)命令
l 命令用于查看文件
(gdb) list 1 #include <stdio.h> 2 int main(void) 3 { 4 int sum(int sum); 5 int i,result=0; 6 sum(100); 7 for(i=1;i<=100;i++){ 8 result+=i; 9 } 10 printf("The sum in main function is %d\n",result); (gdb)
可以看到每行代码面前都有对应的行号,这样方便我们设置断点。
5、b(breakpoint)命令
b 用于设置断点,断点调试时调试程序的一个非常重要的手段,设置方法:在”b”命令之后加上对应的行号,如下:
(gdb) b 6 Breakpoint 1 at 0x400535: file a.c, line 6. (gdb)
6、info 命令
info break 命令用于查看断点情况,设置好断点后可以用它来查看
(gdb) info break Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000400535 in main at a.c:6 (gdb)
再在第 7 行增加一个断点:
(gdb) b 7 Breakpoint 2 at 0x40053f: file a.c, line 7. (gdb)
查看当前断点情况:
(gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000000000400535 in main at a.c:6 2 breakpoint keep y 0x000000000040053f in main at a.c:7 (gdb)
7、r(run)命令
r 命令用于运行代码,默认是从首行开始运行,也可以在 r 后面加上行号,从程序中指定行开始运行。
(gdb) r Starting program: /home/huzhiyuan/work/ddd/test Breakpoint 1, main () at a.c:6 6 sum(100); (gdb)
执行 r 命令后,可以看到程序停止在第 1 个断点处(第 6 行)。
7、c(continue)命令
当已经使用 r 命令运行程序,程序在某个断点处停止下来了,想让程序继续运行可以输入这个命令, 前面程序分别在 6,7 两行设置了两个断点,使用 r 命令程序停在第 6 行,要让程序停止在下一断点(第 7 行),输入 c 命令执行即可。
(gdb) c Continuing. The sum in sum function is 5050 Breakpoint 2, main () at a.c:7 7 for(i=1;i<=100;i++){ (gdb)
可以看到程序运行到断点处就停止在第 2 个断点(第 7 行)。
8、p(print)命令
p 命令用于查看变量的值,在调试的时候我们经常要查看某个变量当前的值与我们逻辑设定的值是否
(gdb) p result $1 = 0 (gdb) p num $2 = num (gdb) p i $3 = 0 (gdb)
9、s(step)命令
s 命令用于单步运行,另外 n(next)命令也用于单步运行,他们的区别在于:如果有函数调用的时候,
s 会进入该函数而n 不会进入该函数。
当前程序已经运行到第 7 行,现在我们也可以重启程序,让程序从头开始运行,输入 r 命令执行,然后输入 y 确认,程序会从头开始运行,直到遇到第一个断点(第 6 行)
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/huzhiyuan/work/ddd/test Breakpoint 1, main () at a.c:6 6 sum(100); (gdb)
输入 s 命令,进入 sum 函数内部执行,如下:
(gdb) s sum (num=100) at a.c:16 16 int i,n=0; (gdb)
可以看到进入了 sum 子函数,这时候就能看到 num 的值为 100。
10、n(next)命令
n 命令用于单步运行,为了测试 s 和 n 命令区别,我们重新运行程序,在第 6 行处,不使用s 命令, 而使用 n 命令测试看看结果如果,操作如下所示:
下面是 n 命令的使用:
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/huzhiyuan/work/ddd/test Breakpoint 1, main () at a.c:6 6 sum(100); (gdb) n The sum in sum function is 5050 Breakpoint 2, main () at a.c:7 7 for(i=1;i<=100;i++){ (gdb)
和 s 命令的运行效果对比会发现,使用 n 命令后,程序显示函数 sum 的运行结果并向下执行,而使用
s 命令后则会进入到sum 函数之中单步运行。
11、finish 命令
finish 命令用于运行程序,直到当前函数结束。例如我们进入了 sum 函数,使用 finish 命令的情况。同样,我们先重新运行程序,让程序在第 6 行停止,然后使用 s 命令进入函数内部,使用 finsh 命令快速执行完成函数代码退出函数。
(gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/huzhiyuan/work/ddd/test Breakpoint 1, main () at a.c:6 6 sum(100); (gdb) s sum (num=100) at a.c:16 16 int i,n=0; (gdb) s 17 for(i=0;i<=num;i++){ (gdb) p num $4 = 100 (gdb) finish Run till exit from #0 sum (num=100) at a.c:17 The sum in sum function is 5050 Breakpoint 2, main () at a.c:7 7 for(i=1;i<=100;i++){ Value returned is $5 = 32 (gdb)
13、q(quit)命令
q 命令用于退出gdb 调试器
(gdb) q @:02_gdb_test$
五、Linux GDB 实验原理
使用GDB 可以调试C、C++、JAVA、汇编程序等。GDB 支持通过串口进行在线调试。
通过串口在线的调试方式,可以支持各种 CPU。调试器使用:
1、监视程序中变量的值
2、设置断点以使程序在指定的代码行上停止执行
3、单步执行代码
4、检测内存中的数据
大多数复杂Linux 程序一样,GDB 是通过内部命令来完成调试工作的。help 命令可以例出 gdb 的命令种类,如果要看种类中的命令,可以使用 help <class> 命令,如:help breakpoints,查看设置断点的所有命令。也可以直接help <command>来查看命令的帮助。gdb 中,输入命令时,可以不用打全命令,只用打命令的前几个
字符就可以了,当然,命令的前几个字符应该要标志着一个唯一的命令
在 gdb 下,你可以敲击两次 TAB 键来补齐命令的全称,如果有重复的,那么gdb 会把其例出来。
file |
装入想要调试的可执行文件. |
kill |
终止正在调试的程序. |
list |
列出产 Th 执行文件的源代码的一部分. |
next |
执行一行源代码但不进入函数内部. |
step |
执行一行源代码而且进入函数内部. |
run |
执行当前被调试的程序 |
c |
继续运行程序 |
quit |
终止 gdb |
watch |
使你能监视一个变量的值而不管它何时被改变. |
backtrace |
栈跟踪,查出代码被谁调用 |
查看变量的值 |
|
make |
使你能不退出 gdb 就可以重新产Th 可执行文件. |
shell |
使你能不离开 gdb 就执行 UNIX shell 命令 |
whatis |
显示变量或函数类型 |
break |
在代码里设置断点, 这将使程序执行到这里时被挂起. |
info break |
显示当前断点清单,包括到达断点处的次数等。 |
info files |
显示被调试文件的详细信息。 |
info func |
显示所有的函数名称。 |
info local |
显示当函数中的局部变量信息。 |
info prog |
显示被调试程序的执行状态。 |
delete [n] |
删除第 n 个断点 |
disable[n] |
关闭第 n 个断点 |
enable[n] |
开启第 n 个断点 |
ptype |
显示结构定义 |
set variable |
设置变量的值 |
call name(args) |
调用并执行名为 name,参数为 args 的函数 |
finish |
终止当前函数并输出返回值 |
return value |
停止当前函数并返回value 给调用者 |
总结
今天详细总结了GDB的操作与实现流程,欢迎关注,交流心得!!!。