阅读本文可能需要一些基础,比如:C语言基础、Linux基础操作、vim、防火墙等。篇幅有限,本文讲的“比较浅显”。
通过本文你将学会:
- gcc编译
- gdb调试
一、使用GCC编译C程序
1.1 准备工作
1.2 编译源代码
1.3 gcc常用选项
1.31 只生成目标文件:-c
1.32 指定生成可执行文件名称:-o
1.33 代码优化:-O
1.34 显示警告信息:-Wall
1.35 将警告视为错误:-Werror
1.36 指定C语言标准:-std
1.37 添加包含文件目录:-I
1.38 库文件目录:-L
1.39 指定链接库:-l
◐生成调试信息:-g
1.4 大型项目
二、使用GDB调试
2.1 gdb调试完整过程
2.2 一些进阶用法
2.21 break与条件断点
2.22 运行时表达式计算
2.23 显示调试状态信息:info命令
2.24 追踪执行流程
2.25 观察点
2.26远程调试
(1)介绍
(2)实操
2.27 调试核心转储文件
2.28 GDB脚本化调试
一、使用GCC编译C程序
当谈到C语言编译器时,GNU Compiler Collection(GCC)是最常用和广泛支持的工具之一。GCC是一个强大的编译器套件,支持多种编程语言,包括C、C++、Objective-C、Fortran和Ada等。还支持交叉编译,即在一个平台下编译另一个平台上的程序(GO语言也可以)。本节将介绍GCC的基本用法和一些常见选项。
1.1 准备工作
(1)安装GCC:
要使用GCC,首先需要安装它。GCC通常在大多数Linux发行版中默认安装,可以通过在终端中运行gcc --version来检查GCC是否已安装。如果系统中未安装GCC,可以通过在终端中运行适当的包管理器命令(如apt、yum或brew)来安装它。
root@CQUPTLEI:~/Linux_test/LinuxC_learn/gcc_learn# gcc --version gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0 Copyright (C) 2019 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
(2)编写源代码:
在使用GCC之前需要编写C源代码文件。我已经创建了一个名为example.c
的文件,其中包含以下代码:
#include<stdio.h> int main() { puts("Haha,I am cat"); return 0; }
1.2 编译源代码
代码编译完整过程:预处理->编译->汇编->链接
(1)编译源代码:
要使用GCC编译源代码,打开终端并导航到源代码所在的目录。然后使用以下命令编译代码(不进入目录,给出文件的完整路径也可以,不建议):
gcc -o output example.c
上述命令中,-o选项用于指定生成的可执行文件的名称,ouputput是输出文件的名称,example.c是输入源代码文件的名称。
这样写也可的:gcc example.c -o new,-o后面紧跟输出文件名就可以了。
(2)运行可执行文件:
在成功编译后,可以在终端中运行生成的可执行文件:
./new
将在终端中看到输出:Haha,I am cat
运行可执行文件,直接用它的名字就可以了,前面加上./是为了指明可执行文件的路径,即当前目录下面,你直接换成绝对路径也可以的。若果想要不加路径,只用名称来运行,就需将它的路径添加到环境变量了,因为你在命令行输入一个东西,他都会在环境变量中去寻找,没添加环境变量之前,系统根本不认识这个东西,所以,./可执行文件名 是常用的方式。
1.3 gcc常用选项
GCC的常见选项:
-c:只编译源代码,生成目标文件(xx.o)而不进行链接。
-E:只进行预处理,生成预处理后的源代码文件。
-O:优化生成的代码,可以使用-O1、-O2或-O3进行不同级别的优化(是大写字母O)。
-g:生成调试信息,以便进行源代码级调试。
-Wall:显示编译时的警告信息。
-std:指定所使用的C语言标准,如-std=c11。
-I:指定包含头文件的目录。
-L:指定链接库文件的目录。
-l:链接指定的库文件。
1.31 只生成目标文件:-c
这个选项告诉gcc只编译源文件,而不进行链接操作。它生成目标文件(通常是以.o为扩展名),可以在后续的链接阶段使用。
1.32 指定生成可执行文件名称:-o
使用这个选项指定生成的可执行文件的名称(Linux不看后缀)。例如,-o myprog将生成名为myprog的可执行文件。
注意不能和源代码名称相同,比如:
gcc -o hello.c hello.c
1.33 代码优化:-O
这个选项用于控制优化级别。可以使用不同的级别,如-O0(关闭优化)到-O3(最高优化级别)。更高的优化级别可能会增加编译时间,但可以生成更高效的代码。(字母O,markdown显示有问题)
与Visual中的debug和release相似,代码不是优化级别越高越好:
1.开发过程中不要优化,因为这使得编译时间可能很长,开发快结束时再说;
2.要调试时,不雅优化,因为代码可能会被改写,导致跟踪调试困难;
3.运行代码的机器资源有限时,可以不优化,优化是提高代码运行效率,但它可能曾加代码的体积。
优化前后对比示例:
编写一个浮点计算的程序:
#include<stdio.h> int main(){ double counter; double res; double tmp; for(counter=0;counter<2000.0*2000.0*2000.0/20.0+2023;counter+=(5-1)/4){ tmp=counter/2023; res=counter; } printf("res is: %f\n",res); return 0; }
(1)不使用优化
使用time记录运行时间:
root@CQUPTLEI:~/Linux_test/LinuxC_learn/gcc_learn# time ./cau res is: 400002022.000000 real 0m1.489s user 0m1.488s sys 0m0.000s
(2)使用-O2优化
root@CQUPTLEI:~/Linux_test/LinuxC_learn/gcc_learn# gcc -O2 -o cau complex.cau.c root@CQUPTLEI:~/Linux_test/LinuxC_learn/gcc_learn# time ./cau res is: 400002022.000000 real 0m0.597s user 0m0.597s sys 0m0.000s
可见,代码运行效率明显提升。
文件大小对比:
-rwxr-xr-x 1 root root 16712 May 21 18:34 cau # 优化后 -rwxr-xr-x 1 root root 16704 May 21 18:38 cau # 优化前
这个示例中,优化后的可执行文件的大小增加的比较少,因为程序本身就及其简单,但如果是一个项目,差距就可能很大了。
1.34 显示警告信息:-Wall
这个选项打开了gcc的警告功能,以便在编译过程中显示更多的警告信息。它可以帮助你发现潜在的问题或不规范的代码。
将上诉代码的main改为void型,开启-Wall选项:
root@CQUPTLEI:~/Linux_test/LinuxC_learn/gcc_learn# gcc -o cau complex.cau.c root@CQUPTLEI:~/Linux_test/LinuxC_learn/gcc_learn# gcc -Wall -o cau complex.cau.c complex.cau.c:2:7: warning: return type of ‘main’ is not ‘int’ [-Wmain] 2 | void main(){ | ^~~~ complex.cau.c: In function ‘main’: complex.cau.c:5:9: warning: variable ‘tmp’ set but not used [-Wunused-but-set-variable] 5 | double tmp; | ^~~
警告信息(标注的有warining字样):
main函数返回类型不是int
变量定义了却没有使用
使用这个选项,只要程序没有错误,只有警告的话,只会显示警告信息,并且能够完成编译。上面编译后可以成功运行的。
1.35 将警告视为错误:-Werror
将所有警告视为错误。当使用此选项时,任何警告都将导致编译过程中止。
这个选项要和-Wall选项一起使用,否则无效。 一起使用时会将原来的warning信息变成error信息,并停止编译:
gcc -Wall -Werror -o cau complex.cau.c
警告严格来讲不是错误,却可能是一些潜在错误的栖身之所,你的程序很有可能因为忽略了某些警告而发生错误。
要写出健壮的代码,也要注意处理警告。
1.36 指定C语言标准:-std
用于指定编译时要使用的C或C++标准。例如,-std=c11
表示使用C11标准进行编译。
查看gcc默认标准可以用:
gcc -dM -E - < /dev/null | grep __STDC_VERSION__
输出:#define __STDC_VERSION__ 201710L
该宏定义表示我的gcc默认c17标准。
现在的C语言标准有C89、C99、C11、C17和C2x。这些标准的主要区别在于它们引入了哪些新特性,以及它们对现有特性的修改和改进。例如,C99标准引入了一些新的数据类型,如long long int和_Bool,以及一些新的库函数,如snprintf()和vsnprintf()。C11标准引入了一些新的特性,如泛型选择表达式和多线程支持。
1.37 添加包含文件目录:-I
字母 i 的大写。
这个选项用于添加包含文件的目录。指定-I选项后,编译器将在指定的目录中查找头文件。
1.38 库文件目录:-L
用这个选项指定链接时要搜索库文件的目录。编译器将在指定的目录中查找库文件。
1.39 指定链接库:-l
通过这个选项指定要链接的库。例如,-lm表示链接数学库。
◐生成调试信息:-g
这个选项生成调试信息,使得在调试程序时可以进行源代码级别的调试。
1.4 大型项目
当一个项目有很多源程序、头文件、依赖库与、资源文件…时,其实就不建议在命令行使用gcc编译了,那将会是很长一段命令,都敲烦了,通常项目的编译通过Makefile
来实现。
二、使用GDB调试
在图形化的IDE中进行调试是一件很简单的事情,在命令行,可以使用gdb调试,其功能也十分强大。
GDB(GNU Debugger)是一个功能强大的调试器,用于调试C、C++和其他编程语言的程序。它提供了一组丰富的功能,帮助开发者定位和修复程序中的错误。下面将详细介绍GDB的使用方法和一些常见的调试技巧。
gdb命令基本语法:
gdb # 直接进入gdb调试环境 gbd programname # 对programname进行调试
gdb参数:
-g:在可执行文件中包含调试信息,以便GDB能够进行源代码级别的调试。
-tui:以文本用户界面(TUI)模式启动GDB,该模式提供了源代码窗口和调试器命令窗口。
-b:指定调试器使用的调试文件格式,如ELF、COFF等。
-ex:在启动GDB后立即执行指定的命令。
-core <core文件>:指定要调试的核心转储文件。
-x <脚本文件>:从指定的文件中读取GDB命令,可以用于自动执行一系列的调试命令。
-args <可执行文件> <参数>:指定要调试的可执行文件及其命令行参数。
-p <进程ID>:连接到指定的正在运行的进程进行调试。
2.1 gdb调试完整过程
先走一个完整的流程。
1.编译源代码:
在开始调试之前,需要使用调试选项编译源代码。在使用GCC编译源代码时,添加-g选项,以生成包含调试信息的可执行文件。例如:
gcc -g -o exp example.c
2.启动GDB:
在终端中进入程序所在的目录,然后输入以下命令启动GDB:
gdb exp
这将启动GDB并将程序exp加载到调试环境中。
或者只输入gdb,先进入调试环境,然后使用:file exp,载入文件。
不管哪种方式,都会先输出一堆信息,gdb版本号之类的,进去后回车,就可以输入相关命令(q是退出),在以(gdb)开头的行,你可以执行各种命令:如设置断点、运行、调试等等。
3.设置断点:
断点是指程序中的一个位置,当执行到该位置时,程序将停止执行,以便您可以检查程序的状态。您可以使用以下命令在特定的行号上设置断点(2.2节详细介绍):
break linenumber
例如:
4.运行程序:
在设置断点后,可以使用以下命令运行程序:
run
程序将开始执行,直到遇到设置的断点或程序结束。
例如:
5.调试命令:
在程序执行过程中,可以使用以下常用命令来调试程序:
run:从头运行程序(简写r)。
break:继续设置断点(简写b)。
next:执行下一行代码(简写n)。
step:进入函数调用,逐行执行函数内部的代码(简写s)。
print variable:打印变量variable的值(简写p)。
watch variable: 监视变量variable的值,当变量的值发生改变时,停止程序的执行(简写w)。
continue:继续执行程序直到下一个断点或程序结束(简写c)。
backtrace:显示当前函数调用的堆栈跟踪信息(简写bt)。
quit:退出GDB调试器(简写q)。
finish: 执行到当前函数返回为止(简写fin)
6.检查变量值:
在程序执行时,您可以使用print命令来检查变量的值。例如,要检查名为count的变量的值,可以输入print count。
7.分析堆栈:
使用backtrace命令可以查看当前函数调用的堆栈跟踪信息。这对于了解程序执行的控制流很有帮助。
8.内存调试:
GDB还提供了一些命令来检查和修改程序的内存状态。例如,watch命令可以设置内存访问断点,x命令可以以不同的格式显示内存内容。
2.2 一些进阶用法
上面的调试命令,多加练习才能熟练,这里分享一些高级调试方法。
2.21 break与条件断点
(1)基本用法:
break <location>:在指定的位置设置断点。位置可以是函数名、源文件名和行号的组合,也可以是函数内的具体行号。
break <line_number>:在指定的行号设置断点。
break <filename>:<line_number>:在指定的源文件和行号设置断点。
例:
(2)断点类型:
break命令默认设置的是常规断点(regular breakpoint),即程序执行到该位置时停止。可以使用以下选项指定不同类型的断点:
break if <condition>:在满足特定条件时触发断点,条件断点。
break unless <condition>:在不满足特定条件时触发断点。
tbreak <location>:设置临时断点(temporary breakpoint),即断点只会在首次触发后被自动删除。
rbreak <regexp>:根据正则表达式匹配函数名来设置断点。
(3) 其他选项:
break命令还支持一些其他选项来提供更多的控制和灵活性:
-t:在设置断点时显示追踪(trace)信息。
-h:设置硬件断点(hardware breakpoint),如果硬件支持的话。
-a:设置断点时自动调整地址,以适应可执行文件的加载地址。
-p:指定断点命令,即在触发断点时执行指定的GDB命令。
-f:在设置断点时强制断点即使警告被设置为错误。
(4)断点的禁用、启用与删除
在GDB中,可以使用disable和enable命令来禁用和启用断点。这两个命令的语法如下:
disable [breakpoint-number] enable [breakpoint-number]
其中,breakpoint-number表示断点编号。如果不指定断点编号,则禁用或启用所有断点。
例如,要禁用编号为1的断点,可以使用以下命令:
(gdb) disable 1
要启用编号为1的断点,可以使用以下命令:
(gdb) enable 1
可以使用delete命令来删除断点。该命令的语法如下:
delete [breakpoints num] [range...]
其中,num表示断点编号,range表示断点范围。如果不指定参数,则删除所有断点。
例如,要删除所有断点,可以使用以下命令:
(gdb) delete
(5)查看所有断点信息
info b
分别是:编号、类型、展示、启用状态、地址、在文件中的位置
(6)条件断点
条件断点是GDB中的一项强大功能,允许在满足特定条件时触发断点,以便在程序执行过程中更有针对性地进行调试。这里单独拿出来详细介绍:
1.设置条件断点:
使用break命令结合if选项可以设置条件断点。语法如下:
break <location> if <condition>
其中,<location>可以是函数名、源文件名和行号的组合,或者是函数内的具体行号。<condition>是一个表示条件的表达式,当满足该条件时触发断点。
2.条件表达式:
条件表达式可以是任何可以在编程语言中使用的合法表达式。例如,可以使用变量、函数调用、运算符和比较操作符来构建条件。一些示例:
i == 10:当变量i的值等于10时触发断点。
x > 0 && y < 5:当x大于0且y小于5时触发断点。
strcmp(str, "example") == 0:当字符串str与"example"相等时触发断点。
3.调试条件断点:
当条件断点触发时,程序会在设置断点的位置停止执行,以便进行调试。在断点停止时,可以使用GDB提供的其他调试命令来查看和修改变量的值,分析程序状态以及执行其他调试操作。
4.修改条件:
可以使用condition <breakpoint_number> <new_condition>命令来修改已设置的条件断点的条件。<breakpoint_number>是断点的编号,可以使用info breakpoints命令查看断点列表和编号。<new_condition>是新的条件表达式。
例: b max if a<b
上图,当max函数中的参数a<b时设置断点,而我传入的实参是a>b,所以程序不会停止。