编译与调试
编译与调式
gcc编译器
当我们进行编译的时候,要使用一系列的工具,我们称之为工具链。SDK就是编译工具链的简写,我们所使用的是gcc系列编译工具链
- 对于.c格式的C文件,可以采用gcc或g++编译
- 对于 .cc、.cpp格式的C++文件,应该采用g++进行编译
编译过程
使用gcc编译程序的过程是预处理–>编译–>汇编–>链接。期间所使用的工具依次是预处理器,编译器,汇编器as,链接器ld

- 预处理: C 编译器对各种预处理命令进行处理,包括头文件包含、宏定义的扩展、条件编译的选择等;
- 编译:gcc将c文件编译成机器语言的汇编文件;
- 汇编:as将汇编代码翻译成了机器码,但是还不可以运行;
- 链接:ld将目标文件和外部符号进行连接,得到一个可执行二进制文件
软链接与硬链接
文件系统

驻留于同一物理设备上的不同文件系统,其类型、大小以及参数设置(比如,块大小)都可以有所不同。这也是将一块磁盘划分为多个分区的原因之一。
在文件系统中,用来分配空间的基本单位是逻辑块,即文件系统所在磁盘设备上若干连续的物理块。
文件系统由以下几部分组成(了解):
- 引导块:总是作为文件系统的首块。引导块不为文件系统所用,只是包含用来引导操
作系统的信息。 - 超级块:紧随引导块之后的一个独立块,包含与文件系统有关的参数信息,其中包括:
- i 节点表容量;
- 文件系统中逻辑块的大小;
- 以逻辑块计,文件系统的大小;
- i 节点(index索引)表:文件系统中的每个文件或目录在i 节点表中都对应着唯一一条记录。这条记
录登记了关乎文件的各种信息。 - 数据块:文件系统的大部分空间都用于存放数据,以构成驻留于文件系统之上的文件
和目录。
i 节点
文件系统的i节点表会包含一个i节点。文件的i 节点号是ls –li
命令所显示的第一列
i 节点所维护的信息如下所示(了解):
- 文件类型(比如,常规文件、目录、符号链接,以及字符设备等)。
- 文件属主(亦称用户 ID 或UID)。
- 文件属组(亦称为组 ID 或GID)。
- 3 类用户的访问权限:属主(有时也称为用户)、属组以及其他用户(属主和属组用户
之外的用户)。 - 3 个时间戳:对文件的最后访问时间(ls –lu 所显示的时间)、对文件的最后修改时间
(也是ls –l 所默认显示的时间),以及文件状态的最后改变时间(ls –lc 所显示的最后
改变i 节点信息的时间)。 - 指向文件的硬链接数量。
- 文件的大小,以字节为单位。
- 实际分配给文件的块数量,以 512 字节块为单位。
- 指向文件数据块的指针
目录
在文件系统中,目录的存储方式类似于普通文件。目录与普通文件的区别有两点:
- 在其i-node 条目中,会将目录标记为一种不同的文件类型**(linux七种文件类型之一)**
- 目录是经特殊组织而成的文件。(目录数据块存放的数据)本质上说就是一个表格,包含文件名和i-node 编号。

以文件/etc/passwd 为例,展示i-node 和目录结构之间的关系

(文件系统根目录(/)总是存储在i-node 条目2中,所以内核在解析路径名时就知道该从哪里着手。)
文件i-node中存储的信息列表中未包含文件名,而仅通过目录列表内的一个映射来定义文件名称。
硬链接
能够在相同或者不同目录中创建多个名称,每个均指向相同的i-node 节点。这些名称称为链接,有时也称之为硬链接。
可在shell 中利用ln 命令为一个已存在的文件创建新的硬链接ln 源文件 目标文件
bzd@bzd-virtual-machine:~/linux/link/hardlink$ vim a.txt |


ls–li 命令所示i-node 编码(即第一列)相同。名称a.txt和a_1指向相同的i-node 条目,因此均指向相同文件。ls–li 命令所示内容的第三列为对i-node 链接的计数。
同一文件的所有名字(链接 )地位平等。
若移除其中一个文件名,另一文件名以及文件本身将继续存在。
修改其中一个文件名中的内容,另一个也会随着改变。
创建硬链接没有真正创建一个文件,只是在目录的 block 中加了一个关联数据,通常不会增加 inode 和 block 的数量。(当目录的 block 被填满时,还是会新增一个block,一般硬链接用掉的关联数据很小,所以通常不会增加 block)
特点
通常不会增加 inode 和 block 的数量
因为目录条目(硬链接)对文件的指代采用了i-node 编号,而i-node 编号的唯一性仅在一个文件系统之内才能得到保障,所以硬链接必须与其指代的文件驻留在同一文件系统中
不能为目录创建硬链接,从而避免出现令诸多系统程序陷于混乱的链接环路。
仅当i-node 的链接计数降为0 时,也就是移除了文件的所有名字时,才会删除(释放)文件的i-node 记录和数据块
软链接
软链接,有时也称为符号链接,是一种特殊的文件类型(linux七种文件类型之一),其数据是另一文件的名称。


特点
因为符号链接指代一个文件名,而非i-node 编号,所以可以用其来链接不同文件系统中的一个文件。
可以为目录创建符号链接
软链接(使用相对路径创建的)移动到其他目录下,不能找到源文件,推荐使用源文件的绝对路径创建软链接
创建符号链接时会为其赋予所有权限,实质上是由符号链接所指代文件的所有权和权限来决定
如果移除了符号链接所指向的文件名,符号链接本身还将继续存在,尽管无法再对其进行解引用(下溯)操作,也将此类链接称之为悬空链接
静态库与动态库
目标库
构建程序的一种方式是简单地将每一个源文件编译成目标文件,然后将这些目标文件链接在一起组成一个可执行程序,
bzd@bzd-virtual-machine:~/linux/lib$ ls |
其缺点如下:
- 在链接的时候仍然需要为所有目标文件命名
- 大量的目标文件会散落在系统上的各个目录中,从而造成目录中内容的混乱。
解决:可以将一组目标文件组织成一个被称为对象库的单元
库文件
库文件是存储一组可重用代码和数据的文件,适用于链接阶段。已经编写并编译好专门用来给用户使用的二进制代码。根据不同的类型,库文件可以分为静态库和动态库。
静态库
静态库实际上就是一个保存所有被添加到其中的目标文件的副本的文件。在 UNIX/Linux 系统中通常是 .a
(惯例:静态库的名称的形式为libname.a),在 Windows 系统中是 .lib
。
如果在编译某个程序时链接静态库,则链接器将会搜索静态库并直接拷贝到该程序的可执行二进制文件到当前文件中;
特点
- 省心:不需要在运行时依赖外部库,增强了程序的独立性。(在运行时删除外部库,程序依然能运行)
- 体积大:生成的可执行文件比较大,因为包含了所有的库代码。
- 更新繁琐:如果需要修改一个静态库中的一个目标模块,那么所有使用那个模块的可执行文件都必须要重新进行链接以合并这个变更。这个缺点还会导致系统管理员需要弄清楚哪些应用程序链接了这个库。
使用静态库
使用 ar命令能够创建和维护静态库,使用自定义静态库步骤如下:
生成静态库
生成目标文件
bzd@bzd-virtual-machine:~/linux/lib$ gcc -c add.c mul.c
使用 ar命令能够创建和维护静态库
bzd@bzd-virtual-machine:~/linux/lib$ ar crsv libcompute.a add.o mul.o
a - add.o
a - mul.o
将库文件(移动)拷贝到/lib或者/usr/lib下(系统默认搜索库路径)
bzd@bzd-virtual-machine:~/linux/lib$ sudo mv libcompute.a /usr/lib
使用静态库
bzd@bzd-virtual-machine:~/linux/lib$ ls
main.c main.o
bzd@bzd-virtual-machine:~/linux/lib$ gcc main.o -lcompute -o main
bzd@bzd-virtual-machine:~/linux/lib$ ls
main main.c main.o
bzd@bzd-virtual-machine:~/linux/lib$ ./main
a add b=12
a mul b=20
Hello,World
动态库
又名共享库,共享库的关键思想是目标模块的单个副本由所有需要这些模块的程序共享。目标模块不会被复制到链接过的可执行文件中,当第一个需要共享库中的模块的程序启动时,库的单个副本就会在运行时被加载进内存。当后面使用同一共享库的其他程序启动时,它们会使用已经被加载进内存的库的副本。使用共享库意味着可执行程序需要的磁盘空间和虚拟内存(在运行的时候)更少了。
特点
- 体积小:目标模块的单个副本由所有需要这些模块的程序共享
- 更新方便:由于目标模块没有被复制进可执行文件中,而是在共享库中集中维护的,因此在修改目标模块时无需重新链接程序就能够看到变更,甚至在运行着的程序正在使用共享库的现有版本的时候也能够进行这样的变更。
- 依赖问题:在运行时依赖外部库,确保所有必要的库在运行环境中可用
使用动态库
生成动态库
生成目标文件
gcc -c add.c mul.c -fpic
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ ls
add.c main.c main.o mul.c
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ gcc -c add.c mul.c -fpic
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ ls
add.c add.o main.c main.o mul.c mul.o生成动态库
gcc -shared add.o mul.o -o libcompt.so
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ gcc -shared add.o mul.o -o libcompt.so
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ ls
add.c add.o libcompt.so main.c main.o mul.c mul.o
将库文件(移动)拷贝到/lib或者/usr/lib下(系统默认搜索库路径)
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ sudo cp libcompt.so /usr/lib
[sudo] bzd 的密码:使用动态库
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ gcc main.o -o main -lcompt
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ ls
add.c add.o libcompt.so main main.c main.o mul.c mul.o
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ ./main
a add b=12
a mul b=20
Hello,World
#### 产品更新 |

更改程序的依赖:使用软链接
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ gcc main.c -llncmpt -o main.out
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ ls
add.c libcompt.so libcompt.so.0.1 main main.c main.out mul.c sub.c
bzd@bzd-virtual-machine:~/linux/lib/sharedlib$ ./main.out
a add b=12
a mul b=20
a sub b=8
Hello,World退回旧版本和后续更新
退回旧版本和后续更新只需更改软链接的指向即可
bzd@bzd-virtual-machine:/usr/lib$ sudo ln -sf libcompt.so liblncmpt.so
结果
**查看库的依赖关系:ldd main **
使用软链接之前

使用软链接之后

makefile
增量编译
增量编译是一种编译技术,它允许只对源代码中发生变化的部分进行重新编译,而不是重新编译整个程序。这种方法可以显著减少编译时间,提高开发效率,特别是在软件开发和调试阶段。
例:现有两个c语言文件main.c和add.c,使用gcc -c 已经翻译成二进制文件main.o和add.o,并使用gcc进行链接生成可执行的二进制文件。
现对add.c进行修改,修改后的add.c文件需要重新预处理–>编译–>汇编–>链接,对main.o而言则不需要重新编译
//add.c |
bzd@bzd-virtual-machine:~/linux/makefile$ vim add.c |
一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中。由于文件非常多,分布比较广,编译这些源文件的命令非常的复杂,此外,为了减少不必要的编译时间,工程中主要采用增量编译的模式,这也对编译命令脚本的设计带来了风险。
Makefile是一种按照增量编译模式设计的命令脚本。它建立了各个文件(可执行程序-目标文件-库文件-源代码文件等等)之间的依赖关系,根据依赖关系和修改时间,来决定哪些命令需要定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为Makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令
使用Makefile的步骤非常简单,先建立一个名为makefile或者是Makefile的文件,然后在里面写入符合语法规则的编译命令,完成以后只需要在文件所在目录使用make命令,make命令会寻找名为makefile文件,然后运行编译命令
书写规则
目标:依赖条件 |
如
main:main.o add.o |
增量编译:对add.c进行修改后make,结果如图所示,make只对修改过的文件进行编译

make默认会根据指定的目标(通常是第一个目标)及其依赖关系构建项目,如果存在多个目标,make可指定要生成的(多个)目标
main:main.o add.o |

伪目标
伪目标:不是生成程序所必须的可执行文件或者依赖文件,更加类似于实现其他功能的命令,例如清理二进制文件,重新生成代码等等,如下图中的clean、rebuild
伪目标通常会被无条件执行,每次运行 make
时,即使没有任何依赖的文件需要更新,它们的命令也会被执行。(在默认情况下,伪目标 test
只有在你显式要求时才会被执行。这体现了伪目标的特性:即使它的命令会在每次调用时执行,但只有在明确指定目标时才会发生。)
main:main.o add.o |
.PHONY只是一个标识,标识makefile文件中存在哪些伪目标,一种规范

变量
自定义变量
Makefile 文件中定义变量的基本语法如下:
变量名称 := 值列表 |
变量的名称可以由大小写 字母、阿拉伯数字和下划线构成。等号左右的空白符没有明确的要求,因为在执行 make 的时候多余的空白符会被自动的删除。至于值列表,既可以是零项,又可以是一项或者是多项。可以用 “$(OBJ)”(常用) 或者是 “${OBJ}” 来引用变量
自动变量
自动变量是一种特殊的变量,它们在规则中自动取值,只和当前规则有关。主要的自动变量包括:
- $@:表示目标文件的名称。
- $<:表示第一个依赖文件的名称。
- $^:表示所有依赖文件的名称,使用空格分隔。
预定义变量
预定义变量是一些由 make
自动提供的内部定义好的变量,它们用于控制构建过程。常用的预定义变量如下:
变量名 | 功能 | 默认含义 |
---|---|---|
AS |
汇编程序 | as |
CC |
C 编译器 | cc |
CPP |
C 预编译器 | $(CC) -E |
CXX |
C++ 编译器 | g++ |
RM |
删除 | rm -f |
使用变量可将上面的makefile改写为
OUT:=main |
bzd@bzd-virtual-machine:~/linux/makefile$ make |
通配符和模式匹配
通配符
makefile规则的命令部分是采用bash命令的,所以在这里就可以使用bash的规则来应用通配
符,通配符用于匹配文件名。常用的通配符有:
通配符 | 说明 | 示例 |
---|---|---|
* |
匹配零个或多个字符 | *.c 匹配所有 C 文件 |
? |
匹配一个任意字符 | file?.c 匹配 file1.c 、file2.c |
[...] |
匹配方括号内的任意一个字符 | file[1-3].c 匹配 file1.c 、file2.c 、file3.c |
** |
递归匹配零个或多个目录 | src/**/*.c 匹配 src 目录及其子目录中的所有 C 文件 |
模式匹配
makefile允许对目标文件名和依赖文件名进行类似正则表达式运算的模式匹配,主要使用的是%匹配符(%表示在依赖文件列表当中匹配任意字符)
例如:对于模式规则“%.o : %.c”,它表示的含义是:所有的.o文件依赖于对应的.c文件。
使用模式匹配对上面的makefile进行修改:
OUT:=main |
两个函数
wildcard 函数
wildcard 函数可以使用通配符,找到所有满足通配符的文件名
SRCS = $(wildcard src/*.c) |
这行代码会将 src
目录下所有的 .c
文件列表赋值给 SRCS
变量。
patsubst函数
patsubst 函数用于模式替换,可以将一组文件名中的某部分替换为另一部分。
OBJS = $(patsubst %.c,%.o,$(SRCS)) |
这行代码会将 SRCS
中的所有 .c
文件名替换为相应的 .o
文件名。
使用两个函数对上面的makefile进行修改:
OUT:=main |
再扩展一个函数文件后,makefile中的内容仍不需修改

扩展(变量的定义)
= 定义变量会在执行的时候出现字符串替代,所以出现递归定义的时候,会进行递归展开。
out = hello #在运行的时候才会进行字符串替代 |
bzd@bzd-virtual-machine:~$ make |
只希望进行一次字符串替换,这种情况可以采用 := 来定义变量,这也是工作当中的主流用法
out := hello #在定义完成的时候会进行字符串替代 |
bzd@bzd-virtual-machine:~$ make |
gdb
1. 启动调试
gdb <可执行文件>
启动GDB并加载目标程序gdb --args <可执行文件> <参数>
直接附加运行参数(调试带参数的程序)
2. 断点管理
break <函数名>
或b main
在指定函数处设置断点break <文件名:行号>
或b test.cpp:20
在指定文件的某行设置断点info breakpoints
或i b
查看所有断点信息delete <断点编号>
或d 2
删除指定编号的断点
3. 执行控制
run
或r
启动程序运行(遇到断点暂停)continue
或c
继续运行到下一个断点next
或n
单步执行(不进入函数内部)step
或s
单步执行(进入函数内部)finish
执行完当前函数并返回到调用处
4. 查看变量与内存
print <变量>
或p x
打印变量值(支持表达式,如p *ptr
)display <变量>
每次暂停时自动显示变量值x/<格式> <地址>
查看内存内容(如x/10xw 0x7fffffffdcc0
查看10个4字节十六进制值)info locals
显示当前栈帧的局部变量info registers
查看寄存器值
5. 堆栈跟踪
backtrace
或bt
显示当前调用堆栈frame <编号>
或f 2
切换到指定堆栈帧up
/down
在堆栈帧间上下移动
6. 调试崩溃问题
core <core文件>
分析崩溃生成的core dump文件bt full
显示完整堆栈信息(含局部变量)info signals
查看信号处理状态(如段错误SIGSEGV)
7. 多线程调试
info threads
查看所有线程信息thread <线程号>
切换到指定线程thread apply all bt
打印所有线程的堆栈
8. 其他实用命令
list
或l
显示当前行附近的源码watch <变量>
设置数据监视点(变量被修改时暂停)set var <变量>=<值>
动态修改变量值(如set var x=5
)quit
或q
退出GDB
core文件
当程序运行过程中出现Segmentation fault (core dumped)错误时,程序停止运行,并产生core文件。core文件是程序运行状态的内存映象。使用gdb调试core文件,可以帮助我们快速定位程序出现段错误的位置。当然,可执行程序编译时应加上-g编译选项,生成调试信息。
当程序访问的内存超出了系统给定的内存空间,就会产生Segmentation fault (core dumped),因此,段错误产生的情况主要有: (1)访问不存在的内存地址; (2)访问系统保护的内存地址; (3)数组访问越界等。
core dumped又叫核心转储, 当程序运行过程中发生异常, 程序异常退出时, 由操作系统把程序当前的内存状况存储在一个core文件中, 叫core dumped。
gdb调试core文件的步骤
使用gdb调试core文件来查找程序中出现段错误的位置时,要注意的是可执行程序在编译的时候需要加上-g编译命令选项。
gdb调试core文件的步骤
1)启动gdb,进入core文件,命令格式:gdb [exec file] [core file]。 用法示例:gdb ./test test.core。
(2)在进入gdb后,查找段错误位置:where或者bt 用法示例:

可以定位到源程序中具体文件的具体位置,出现了段错误。
控制core文件是否生成
(1)使用ulimit -c命令可查看core文件的生成开关。若结果为0,则表示关闭了此功能,不会生成core文件。
( 2) 使用ulimit -c filesize命令,可以限制core文件的大小(filesize的单位为KB)。如果生成的信息超过此大小,将会被裁剪,最终生成一个不完整的core文件。在调试此core文 件的时候,gdb会提示错误。比如:ulimit -c 1024。
(3)使用ulimit -c unlimited,则表示core文件的大小不受限制。