首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【把Linux“聊”明白】编译器gcc/g++与调试器gdb/cgdb:从编译原理到高效调试

【把Linux“聊”明白】编译器gcc/g++与调试器gdb/cgdb:从编译原理到高效调试

作者头像
苏兮
发布2026-01-13 18:09:09
发布2026-01-13 18:09:09
2250
举报

前言:

在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 预处理(进行宏替换)

预处理功能主要包括宏定义、文件包含、条件编译、去注释等。

预处理指令是以 # 开头的代码行。

实例:

代码语言:javascript
复制
gcc -E test.c -o test.i

选项 -E:该选项的作用是让 gcc 在预处理结束后停止编译过程。

选项 -o:是指目标文件,“.i” 文件为已经过预处理的 C 源始程序。


3-2-2 编译(生成汇编)

在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作。

在检查无误后,gcc 把代码翻译成汇编语言。

我们可以使用 -S 选项来查看,该选项只进行编译不进行汇编,生成汇编代码。

实例:

代码语言:javascript
复制
gcc -S test.i -o test.s

3-2-3 汇编(生成机器可识别代码)

汇编阶段是把编译阶段生成的 “.s” 文件转成目标文件。

我们可使用选项 -c 就可看到汇编代码已转化为 “.o” 的二进制目标代码。

实例:

代码语言:javascript
复制
gcc -c test.s -o test.o

3-2-4 连接(生成可执行文件或库文件)

生成的编译之后,就进入了连接阶段。

实例:

代码语言:javascript
复制
gcc test.s -o test

1-2 动态链接和静态链接

1-2-1 静态链接

在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个*.c文件会形成一个*.o文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。静态链接的缺点很明显:

  1. 浪费空间

因为每个可执行程序中对所有需要的目标文件都要有一份副本。 在静态链接中,所有依赖的库函数(例如 printf())的机器码会直接被复制进每一个可执行文件。 所以如果多个程序都调用了同一个库函数(例如 C 标准库中的 printf()),那么这些程序都会在自己的二进制文件中保存一份 printf.o 的副本。 结果是:同一个函数的代码会在内存或磁盘中存在多份冗余拷贝,浪费空间。

  1. 更新困难

每当库函数的代码修改时,所有使用该库的程序都必须重新编译、重新链接。 如果库函数有 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 就可以生成可执行文件,如下所示。

代码语言:javascript
复制
gcc 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++的静态库并没有安装,先安装:

代码语言:javascript
复制
# 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指令进行安装:

代码语言:javascript
复制
yum install -y cgdb

示例代码

要进行调试,我们先准备一个简单的C程序:

代码语言:javascript
复制
#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标准,所以我们这样编译即可:

代码语言:javascript
复制
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/l list/l + #表示以#为中心显示10行。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

注意:不管你在l/list后加某一个合理的参数,都是以参数为中心展示10行的。

比如:list/l 函数名 或者list/l 文件名:行号

  • r/run 从程序开始连续执行。 如果没有断点,直接运行结束。 实例:
在这里插入图片描述
在这里插入图片描述
  • break/b 函数名break/b [文件名:]行号 在函数开头或者指定位置设置断点 。 实例:
在这里插入图片描述
在这里插入图片描述
  • info break/b 查看当前所有断点的信息 实例:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  • n/nexts/step n/next 单步执行,不进入函数内部,相当于vs中的F10,s/step 单步执行,进入函数内部,相当于vs中的F11. 自行测试吧。
  • delete/d breakpointsdelete/d breakpoints n
在这里插入图片描述
在这里插入图片描述
  • disable breakpoints ndisable breakpointsenable 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 来更改该变量的值,进而观察程序的行为变化,从而确定问题的原因。

基本用法

代码语言:javascript
复制
set var <变量名> = <新值>

实例

在这里插入图片描述
在这里插入图片描述

条件断点

添加条件断点

代码语言:javascript
复制
b 9 if i == 30 # 9是行号,表示新增断点的位置

给已经存在的端点新增条件

代码语言:javascript
复制
condition 2 i== 30 #给2号断点,新增条件 i == 30

总结

本文系统性地介绍了Linux环境下C/C++开发中两个核心工具:编译器gcc/g++和调试器gdb/cgdb。通过深入理解编译过程的四个阶段(预处理、编译、汇编、链接)以及动静态链接机制,我们能够更好地掌控程序的构建过程。同时,掌握gdb/cgdb的调试技巧,能够显著提升排查和修复代码问题的效率。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • 一、编译器gcc/g++
    • 1-1 gcc编译选项
      • 1-1-1 预处理(进行宏替换)
      • 3-2-2 编译(生成汇编)
      • 3-2-3 汇编(生成机器可识别代码)
      • 3-2-4 连接(生成可执行文件或库文件)
    • 1-2 动态链接和静态链接
      • 1-2-1 静态链接
      • 1-2-2 动态链接
      • 1-2-3 库的概念
    • 1-3 静态库和动态库
      • 1-3-1 静态库和动态库的介绍
      • 1-3-2 静态库与动态库的区别
      • 1-3-3 理论验证
    • 1-4 gcc其它常用选项
  • 二、调试器gdb/cgdb
    • 2-1 预备知识与准备工作
    • 2-2 常见调试命令
    • 2-3 常见调试技巧
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档