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

编译器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文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。静态链接的缺点很明显:

  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 就可以生成可执行文件,如下所示。

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

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/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 来更改该变量的值,进而观察程序的行为变化,从而确定问题的原因。
基本用法

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系列深度更新
您的每一个[三连]都是我们持续创作的动力!✨

相关推荐
Java天梯之路2 小时前
04 数据类型转换
java
LCG元2 小时前
Linux 软件安装大全:apt/yum/dpkg/rpm/snap 到底用哪个?
linux
倦王2 小时前
Linux一些基本命令--黑马学习
linux·运维·服务器
im_AMBER2 小时前
React 11 登录页项目框架搭建
前端·学习·react.js·前端框架
小武~3 小时前
嵌入式Linux系统性能优化:深入剖析I/O性能瓶颈
linux·运维·性能优化
dragoooon343 小时前
[Linux——Lesson21.进程信号:信号概念 & 信号的产生]
linux·运维·服务器
Acrelhuang3 小时前
小小电能表,如何撬动家庭能源革命?
java·大数据·开发语言·人工智能·物联网
jyd01243 小时前
MongoDB 与 Java 实体类型 LocalTime 时区转换问题解决方案
java·数据库·mongodb
头发还没掉光光3 小时前
Linux网络初始及网络通信基本原理
linux·运维·开发语言·网络·c++