
编译器gcc/g++与调试器gdb/cgdb
友情专栏:【把Linux"聊"明白】
文章目录
- 编译器gcc/g++与调试器gdb/cgdb
- 前言:
- 一、编译器gcc/g++
-
- [1-1 gcc编译选项](#1-1 gcc编译选项)
-
- [1-1-1 预处理(进行宏替换)](#1-1-1 预处理(进行宏替换))
- [**3-2-2 编译(生成汇编)**](#3-2-2 编译(生成汇编))
- [**3-2-3 汇编(生成机器可识别代码)**](#3-2-3 汇编(生成机器可识别代码))
- [**3-2-4 连接(生成可执行文件或库文件)**](#3-2-4 连接(生成可执行文件或库文件))
- [1-2 动态链接和静态链接](#1-2 动态链接和静态链接)
-
- [1-2-1 静态链接](#1-2-1 静态链接)
- [1-2-2 动态链接](#1-2-2 动态链接)
- [1-2-3 库的概念](#1-2-3 库的概念)
- [1-3 静态库和动态库](#1-3 静态库和动态库)
-
- [1-3-1 静态库和动态库的介绍](#1-3-1 静态库和动态库的介绍)
- [1-3-2 静态库与动态库的区别](#1-3-2 静态库与动态库的区别)
- [1-3-3 理论验证](#1-3-3 理论验证)
- [1-4 gcc其它常用选项](#1-4 gcc其它常用选项)
- 二、调试器gdb/cgdb
-
- [2-1 预备知识与准备工作](#2-1 预备知识与准备工作)
- [2-2 常见调试命令](#2-2 常见调试命令)
- [2-3 常见调试技巧](#2-3 常见调试技巧)
- 总结
前言:
在Linux环境下进行C/C++开发,掌握编译器gcc/g++和调试器gdb/cgdb的使用是每个开发者必备的核心技能。本文将系统性地讲解从源代码到可执行程序的完整编译流程,以及如何利用调试工具快速定位和修复代码问题。
在学习gcc/g++的使用之前,需要我们对于C/C++程序从源代码到可执行程序这一过程,即预编译、编译、汇编与链接有所了解,可参考:《C/C++编译与链接详解》
本文将分为两大部分:第一部分详细解析gcc/g++编译器的各项功能和使用技巧,包括编译选项、动静态链接机制以及库文件的处理;第二部分深入探讨gdb/cgdb调试器的实战应用,从基础调试命令到高级调试技巧,帮助读者构建完整的程序调试能力体系。
一、编译器gcc/g++
1-1 gcc编译选项
格式:gcc [选项] 要编译的文件 [选项] [目标文件]
1-1-1 预处理(进行宏替换)
-
预处理功能主要包括宏定义、文件包含、条件编译、去注释等。
-
预处理指令是以
#开头的代码行。 -
实例:
gcc -E test.c -o test.i -
选项
-E:该选项的作用是让 gcc 在预处理结束后停止编译过程。 -
选项
-o:是指目标文件,".i" 文件为已经过预处理的 C 源始程序。
3-2-2 编译(生成汇编)
-
在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作。
-
在检查无误后,gcc 把代码翻译成汇编语言。
-
我们可以使用
-S选项来查看,该选项只进行编译不进行汇编,生成汇编代码。 -
实例:
gcc -S test.i -o test.s
3-2-3 汇编(生成机器可识别代码)
-
汇编阶段是把编译阶段生成的 ".s" 文件转成目标文件。
-
我们可使用选项
-c就可看到汇编代码已转化为 ".o" 的二进制目标代码。 -
实例:
gcc -c test.s -o test.o
3-2-4 连接(生成可执行文件或库文件)
-
生成的编译之后,就进入了连接阶段。
-
实例:
gcc test.s -o test
1-2 动态链接和静态链接
1-2-1 静态链接
在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个*.c文件会形成一个*.o文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。静态链接的缺点很明显:
- 浪费空间
因为每个可执行程序中对所有需要的目标文件都要有一份副本。
在静态链接 中,所有依赖的库函数(例如
printf())的机器码会直接被复制进每一个可执行文件。所以如果多个程序都调用了同一个库函数(例如 C 标准库中的
printf()),那么这些程序都会在自己的二进制文件中保存一份printf.o的副本。结果是:同一个函数的代码会在内存或磁盘中存在多份冗余拷贝,浪费空间。
- 更新困难
每当库函数的代码修改时,所有使用该库的程序都必须重新编译、重新链接。
如果库函数有 bug 或逻辑更新,静态链接的程序并不会自动使用新版库。
必须手动重新编译并重新生成可执行文件,才能包含更新后的函数代码。
优点是:静态链接的程序在运行时不依赖外部库文件,执行速度快、部署方便。
动态链接的出现解决了静态链接中提到问题。
1-2-2 动态链接
动态链接把程序拆成多个独立模块 ,在程序运行时 再把这些模块加载并链接在一起,形成完整的可执行程序。
不像静态链接那样在编译期 就把所有目标文件都打包成一个大的可执行文件。
例如:
test程序在运行时会依赖某个 C 语言的动态链接库(比如libc.so)。
当程序执行时,操作系统的动态链接器会在内存中加载这个共享库。
注 :
ldd命令用于打印程序或者库文件所依赖的共享库列表。
1-2-3 库的概念
- 我们的C程序中,并没有定义 "printf" 的函数实现,且在预编译中包含的 "stdio.h" 中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现 "printf" 函数的呢?
- 最后的答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径 "/usr/lib" 下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数 "printf" 了,而这也就是链接的作用。
1-3 静态库和动态库
1-3-1 静态库和动态库的介绍
-
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为 ".a"
-
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为 ".so",如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件,如下所示。
bashgcc test.o -o test -
gcc 默认生成的二进制程序,是动态链接的。(file命令进行查看)
注意:
Linux下,动态库XXX.so,静态库XXX.a
Windows下,动态库XXX.dll,静态库XXX.lib
对于libc.so的逐词解析
| 部分 | 含义 | 说明 |
|---|---|---|
| lib | "library" 的缩写 | 表示这是一个库文件(Library) 。几乎所有 Linux 下的库文件名都以 lib 开头。例如:libm.so(数学库),libpthread.so(线程库)。 |
| c | "C language" 的缩写 | 表示这是 C 语言标准库(C library) ,也叫 libc。它提供了 C 语言中最基础的函数实现。 |
| .so | "shared object" 的缩写 | 表示这是一个 共享对象文件(Shared Object) ,即动态链接库。它在程序运行时被加载,而不是在编译时直接打包进可执行文件。 |
有了上面的论述,我们来简单看一下两者的区别。
1-3-2 静态库与动态库的区别
- 动态链接生成的可执行文件体积较小,因为库代码不会被打包进程序,而是由多个程序共享。
- 静态链接的可执行文件不再依赖库文件;而动态链接程序在运行时必须依赖外部动态库(如 .so 文件)存在,否则无法运行。
- 静态链接程序在内存中会出现重复的库代码副本,而动态链接程序共享同一份库映像,节省内存。
- 动态链接通过共享库文件,减少重复代码加载,因此节省内存与磁盘空间,但会稍微增加启动时的加载开销(加载动态库)。
1-3-3 理论验证
前面我们只是再说理论知识,接下来我们来对一个程序分别来进行静态链接和动态链接,进行对比。
前面说过并测试,gcc默认是动态链接,所以我们先来进行动态链接:
下面来进行静态链接,如果我们是云服务器,一般C/C++的静态库并没有安装,先安装:
shell
# Centos
yum install glibc-static libstdc++-static -y
静态链接的指令只需要多加个-static即可:
可见,大小差别还是很大的。
对于动静态库我们就简单了解到这,后续还会有更深入的文章来讲解。
1-4 gcc其它常用选项
| 选项 | 说明 |
|---|---|
| -E | 只进行预处理(Preprocessing) ,不编译、不汇编、不链接。结果输出到标准输出(通常要用 > 重定向保存到文件)。 |
| -S | 只编译到汇编代码(Assembly) ,不进行汇编和链接。生成 .s 文件。 |
| -c | 只编译并汇编生成目标文件(Object File) ,不进行链接。生成 .o 文件。 |
| -o <文件名> | 指定输出文件名(可用于目标文件或可执行文件)。 |
| -static | 对生成的可执行文件采用完全静态链接 (不依赖动态库)。要求系统安装对应的静态库(如 glibc-static)。 |
| -g | 在生成的目标文件或可执行文件中加入调试信息,供 GDB(GNU 调试器)使用。 |
| -shared | 告诉编译器生成一个共享库(Shared Library) ,即 .so 文件;通常与 -fPIC 一起使用。 |
| -O0 / -O1 / -O2 / -O3 | 控制编译优化等级 。 - -O0:无优化(默认),编译最快、调试方便; - -O1:基本优化; - -O2:更激进的优化,几乎不影响调试; - -O3:最高级别优化,可能增加编译时间或代码体积。 |
| -w | 禁止所有警告信息输出。 |
| -Wall | 打开大多数常见的警告信息(建议总是加上)。 |
二、调试器gdb/cgdb
2-1 预备知识与准备工作
- 我们知道,程序的发布方式有两种, debug 模式和 release 模式, Linux gcc/g++ 出来的二进制程序,默认是 release 模式。
- 要使用gdb/cgdb调试,必须在源代码生成二进制程序的时候,加上 -g 选项,如果没有添加,程序无法被编译。
安装调试器
对于调试器的学习,我的建议是从 cgdb 入门 + 同步掌握 gdb 命令。因为gdb对于新手来说确实比较复杂一点,cgdb有类似 vim 的界面。当然,如果想直接使用gdb,要是没有问题的,我下面演示命令也是直接用gdb的。
安装cgdb,可以切换root身份或使用sudo指令进行安装:
shell
yum install -y cgdb
示例代码
要进行调试,我们先准备一个简单的C程序:
c
#include <stdio.h>
int Sum(int s, int e)
{
int result = 0;
for (int i = s; i <= e; i++)
{
result += i;
}
return result;
}
int main()
{
int start = 1;
int end = 100;
printf("I will begin\n");
int n = Sum(start, end);
printf("running done, result is: [%d-%d]=%d\n", start, end, n);
return 0;
}
注意
上面的代码编译时可能会出现下面的问题:

这是因为此gcc版本并未支持c99标准,所以我们这样编译即可:
shell
gcc test.c -o test -std=c99 -g
准备工作做完,下面,我们就来进行实操。
2-2 常见调试命令
开始 :gdb binFile
退出 :ctrl + d 或命令quit / q
调试命令速览表
| 命令 | 作用 | 示例 |
|---|---|---|
list/l |
显示源代码,从上次位置开始,每次列出 10 行 | list/l 10 |
list/l 函数名 |
列出指定函数的源代码 | list/l main |
list/l 文件名:行号 |
列出指定文件的源代码 | list/l mycmd.c:1 |
r/run |
从程序开始连续执行 | run |
n/next |
单步执行,不进入函数内部 | next |
s/step |
单步执行,进入函数内部 | step |
break/b [文件名:]行号 |
在指定行号设置断点 | break 10 break test.c:10 |
break/b 函数名 |
在函数开头设置断点 | break main |
info break/b |
查看当前所有断点的信息 | info break |
finish |
执行到当前函数返回,然后停止 | finish |
print/p 表达式 |
打印表达式的值 | print start+end |
p 变量 |
打印指定变量的值 | p x |
set var 变量=值 |
修改变量的值 | set var i=10 |
continue/c |
从当前位置开始连续执行程序 | continue |
delete/d breakpoints |
删除所有断点 | delete breakpoints |
delete/d breakpoints n |
删除序号为 n 的断点 | delete breakpoints 1 |
disable breakpoints |
禁用所有断点 | disable breakpoints |
enable breakpoints |
启用所有断点 | enable breakpoints |
info/i breakpoints |
查看当前设置的断点列表 | info breakpoints |
display 变量名 |
跟踪显示指定变量的值(每次停止时) | display x |
undisplay 编号 |
取消对指定编号变量的跟踪显示 | undisplay 1 |
until 行号 |
执行到指定行号(没断点时) | until 20 |
backtrace/bt |
查看当前执行栈的各级函数调用及参数 | backtrace |
info/i locals |
查看当前栈帧的局部变量值 | info locals |
quit |
退出 GDB 调试器 | quit |
命令演示:
-
list/llist/l + #表示以#为中心显示10行。


注意:不管你在
l/list后加某一个合理的参数,都是以参数为中心展示10行的。比如:
list/l 函数名或者list/l 文件名:行号 -
r/run从程序开始连续执行。
如果没有断点,直接运行结束。
实例:

-
break/b 函数名与break/b [文件名:]行号在函数开头或者指定位置设置断点 。
实例:

-
info break/b查看当前所有断点的信息
实例:


-
n/next与s/stepn/next单步执行,不进入函数内部,相当于vs中的F10,s/step单步执行,进入函数内部,相当于vs中的F11.自行测试吧。
-
delete/d breakpoints与delete/d breakpoints n
-
disable breakpoints n与disable breakpoints与enable breakpoints禁用与恢复断点

对于常用的命令就上面这些,多练习即可。
在我们学习gdb时,我们可以与vs环境下的调试进行对比理解,例如:
- r 相当于vs中的 F5;
- b 相当于vs中的 设置断点;
- n 相当于vs中的 F10;
- s 相当于vs中的 F11(在函数处);
- p/display 相当于vs中的 监视;
2-3 常见调试技巧
接下来我们学习三个实用的调试技巧。
watch
执行时监视一个表达式(如变量)的值。如果监视的表达式在程序运行期间的值发生变化,GDB会暂停程序的执行,并通知使用者。
比如:如果你有一些变量不应该修改,但是你怀疑它修改导致了问题,你可以watch它,如果变化了,就会通知你。
set var确定问题原因
例如,假设你在调试时发现某个变量值不正确,可能导致程序崩溃或结果错误。你可以使用 set var 来更改该变量的值,进而观察程序的行为变化,从而确定问题的原因。
基本用法:
gdb
set var <变量名> = <新值>
实例 :

条件断点
添加条件断点
gdb
b 9 if i == 30 # 9是行号,表示新增断点的位置
给已经存在的端点新增条件
gdb
condition 2 i== 30 #给2号断点,新增条件 i == 30
总结
本文系统性地介绍了Linux环境下C/C++开发中两个核心工具:编译器gcc/g++和调试器gdb/cgdb。通过深入理解编译过程的四个阶段(预处理、编译、汇编、链接)以及动静态链接机制,我们能够更好地掌控程序的构建过程。同时,掌握gdb/cgdb的调试技巧,能够显著提升排查和修复代码问题的效率。
如果本文对您有启发:
✅ 点赞 - 让更多人看到这篇硬核技术解析 !
✅ 收藏 - 实战代码随时复现
✅ 关注 - 获取Linux系列深度更新
您的每一个[三连]都是我们持续创作的动力!✨





