阅读本文可能需要一些基础,比如: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相似,代码不是优化级别越高越好:
- 开发过程中不要优化,因为这使得编译时间可能很长,开发快结束时再说;
- 要调试时,不雅优化,因为代码可能会被改写,导致跟踪调试困难;
- 运行代码的机器资源有限时,可以不优化,优化是提高代码运行效率,但它可能曾加代码的体积。
优化前后对比示例:
编写一个浮点计算的程序:
#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调试完整过程
先走一个完整的流程。
-
编译源代码:
在开始调试之前,需要使用调试选项编译源代码。在使用GCC编译源代码时,添加-g
选项,以生成包含调试信息的可执行文件。例如:gcc -g -o exp example.c
-
启动GDB:
在终端中进入程序所在的目录,然后输入以下命令启动GDB:gdb exp
这将启动GDB并将程序
exp
加载到调试环境中。
或者只输入gdb,先进入调试环境,然后使用:file exp
,载入文件。
不管哪种方式,都会先输出一堆信息,gdb版本号之类的,进去后回车,就可以输入相关命令(q是退出),在以(gdb)
开头的行,你可以执行各种命令:如设置断点、运行、调试等等。
-
设置断点:
断点是指程序中的一个位置,当执行到该位置时,程序将停止执行,以便您可以检查程序的状态。您可以使用以下命令在特定的行号上设置断点(2.2节详细介绍):break linenumber
例如:
-
运行程序:
在设置断点后,可以使用以下命令运行程序:run
程序将开始执行,直到遇到设置的断点或程序结束。
例如:
-
调试命令:
在程序执行过程中,可以使用以下常用命令来调试程序: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
)。
-
检查变量值:
在程序执行时,您可以使用print
命令来检查变量的值。例如,要检查名为count
的变量的值,可以输入print count
。 -
分析堆栈:
使用backtrace
命令可以查看当前函数调用的堆栈跟踪信息。这对于了解程序执行的控制流很有帮助。 -
内存调试:
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中的一项强大功能,允许在满足特定条件时触发断点,以便在程序执行过程中更有针对性地进行调试。这里单独拿出来详细介绍:
-
设置条件断点:
使用break
命令结合if
选项可以设置条件断点。语法如下:break <location> if <condition>
其中,
<location>
可以是函数名、源文件名和行号的组合,或者是函数内的具体行号。<condition>
是一个表示条件的表达式,当满足该条件时触发断点。 -
条件表达式:
条件表达式可以是任何可以在编程语言中使用的合法表达式。例如,可以使用变量、函数调用、运算符和比较操作符来构建条件。一些示例:i == 10
:当变量i
的值等于10时触发断点。x > 0 && y < 5
:当x
大于0且y
小于5时触发断点。strcmp(str, "example") == 0
:当字符串str
与"example"相等时触发断点。
-
调试条件断点:
当条件断点触发时,程序会在设置断点的位置停止执行,以便进行调试。在断点停止时,可以使用GDB提供的其他调试命令来查看和修改变量的值,分析程序状态以及执行其他调试操作。 -
修改条件:
可以使用condition <breakpoint_number> <new_condition>
命令来修改已设置的条件断点的条件。<breakpoint_number>
是断点的编号,可以使用info breakpoints
命令查看断点列表和编号。<new_condition>
是新的条件表达式。
例: b max if a<b
上图,当max函数中的参数a<b时设置断点,而我传入的实参是a>b,所以程序不会停止。
2.22 运行时表达式计算
在调试过程中,可能需要计算一些表达式的值,以便更好地理解程序状态。GDB提供了print
或p
命令来评估表达式。例如,p variable
将显示变量的值,而p func(5)
将计算函数func
在参数5上的返回值。
**例:**输出max
函数在参数5,4
的返回值(我程序中的实参是4,5
)
2.23 显示调试状态信息:info命令
info命令是GDB调试器中的一个命令,用于显示当前调试状态的信息。例如:
info args
:显示函数的参数列表(运行到一个函数时使用)。info locals
:显示当前函数的局部变量。info registers
:显示寄存器的值(info r
)。info threads
:显示当前线程的列表。info signals
:显示当前进程接收到的信号。
2.24 追踪执行流程
GDB允许追踪程序的执行流程,以便更好地理解代码中的控制流。通过命令step
,可以逐语句地执行程序,并进入函数调用。使用next
命令,可以跳过函数调用,直接执行下一条语句。而finish
命令则会执行当前函数的剩余部分,并返回到调用该函数的位置。
2.25 观察点
有时,希望在变量发生更改时自动停止程序的执行。GDB的观察点(watchpoint)功能可以帮助实现这一目标。通过watch
命令,可以设置观察点来监视变量的值。一旦变量的值发生更改,程序就会停止执行,让我们能够进一步分析问题。
2.26远程调试
恰好我有2台服务器,就不用windows连接了(windows可以在VS code中安装插件使用gdb,或者使用MinGW安装)。
(1)介绍
GDB提供了远程调试功能,允许开发人员在一个计算机上调试运行在另一个计算机上的程序。这对于开发嵌入式系统或远程服务器应用程序非常有用。下面是对GDB远程调试的详细介绍:
-
远程调试设置:
在进行远程调试之前,需要在目标计算机上运行调试服务器。调试服务器是一个在目标计算机上运行的程序,它与GDB建立通信,允许GDB远程控制和调试目标程序。通常,目标计算机上的调试服务器是由调试目标平台的供应商提供的。 -
连接到远程目标:
在本地计算机上,可以使用以下命令将GDB连接到远程目标:target remote <hostname>:<port>
其中,
<hostname>
是远程目标计算机的主机名或IP地址,<port>
是远程调试服务器的端口号。通过这个命令,GDB将建立与远程目标的连接。 -
远程调试命令:
一旦与远程目标建立连接,就可以使用GDB的标准调试命令来进行远程调试。例如,可以设置断点、运行程序、查看变量和回溯调用栈等。GDB会将调试命令发送到远程调试服务器,服务器会执行相应的操作,并将结果传递回GDB。 -
与本地调试的区别:
远程调试与本地调试类似,但存在一些区别。在远程调试中,由于目标程序在远程计算机上执行,因此调试服务器负责处理与目标程序的通信和控制。GDB作为客户端与调试服务器进行通信,将调试命令发送到服务器并接收服务器的响应。 -
注意事项:
- 远程调试需要保证本地计算机和远程目标计算机之间的网络连接正常。
- 调试服务器的配置和设置可能会因目标平台和调试工具的不同而有所不同,需要按照供应商的说明进行正确设置。
- 可能需要在防火墙或网络设备上打开相应的端口,以允许GDB与远程调试服务器进行通信。
远程调试是一个非常有用的工具,它使开发人员能够在实际运行环境中调试程序,更好地理解和解决问题。通过使用GDB的远程调试功能,开发人员可以在嵌入式系统、远程服务器或其他远程目标上进行高效的调试和分析,加快故障排除和开发过程。
(2)实操
- 调试服务器防火墙放行端口,我使用端口
7865
来远程调试; - 启动调试服务器,在远程服务器上运行调试服务器:
gdbserver :7865 ./exp
- 本地计算机连接调试服务器:
先进入gdb:
gdb
连接远程目标:
target remote xxx.xxx.xxx.xxx:7865
-
开始调试:
-
退出:本地gdb退出时,远程也会退出。
2.27 调试核心转储文件
当程序崩溃或出现错误时,GDB可以加载核心转储文件以进行调试。
核心转储文件是在程序异常终止时生成的内存映像,其中包含了有关程序状态的详细信息。通过core <corefile>
命令,我们可以加载核心转储文件并对程序进行调试。
2.28 GDB脚本化调试
GDB脚本测试是使用GDB自动化脚本来执行一系列调试操作和断言,以验证程序的行为和正确性。
这些脚本可以包含GDB命令和Python脚本,用于自动化调试流程和执行复杂的测试场景。
下面是一个简单的示例,演示如何编写和运行GDB脚本测试:
-
创建测试脚本文件:
创建一个新的文本文件,例如test_exp.gdb
,并在文件中编写GDB脚本。脚本可以包含GDB命令、Python脚本和断言语句。例如,以下是一个简单的测试脚本示例:# test_script.gdb file exp break main info b run print max(10,20) quit
-
运行测试脚本:
在终端中运行GDB,并使用-x
选项指定测试脚本文件的路径来执行测试。例如,使用以下命令运行测试脚本:$ gdb -x test_exp.gdb
源程序:
#include<stdio.h>
int max(int a,int b){
return a>b? a:b;
}
int main()
{
int i=0;
for(i=0;i<5;i++){
printf("* * *\n");
}
printf("%d is bigger between %d and %d\n",5,4,max(5,4));
puts("Done\n");
return 0;
}
把永远爱你写进诗的结尾~