gcc/g++
GCC程序语言编译器自由软件,现在可以编译很多语言,gcc是GCC中的c编译器,而g++ 是GCC中的c++编译器。
指令格式:gcc 选项 文件名
公共选项
-o 文件名:指定输出文件名
预处理
作用
- 处理所有#开头(除pragma)的预处理指令
- 头文件包含(
#include
):将目标头文件的内容复制到当前文件中; - 宏定义(
#define
)及宏替换; - 条件编译(
#ifdef
、#ifndef
、#if 0
等);
- 删除注释
- 预编成一个
.i文件
选项
-E
只进行预处理-I路径
:添加头文件搜索路径,可多次使用-D宏名\[=值]
,常用于控制条件编译
gcc -DDEBUG -DVERSION=1.0 -E main.c -o main.i -I./include
编译
此步最耗时,因为要检查语法语义错误,对于这些控制选项也是最多的
作用
- 进行语法、语义检查
- 生成平台相关的汇编代码
.s文件
选项
-S
只进行到编译-O0/O1/O2/O3
优化级别递增,默认O0不优化-Wall
启用大多数警告,默认显示优先级最高的警告-w
禁止所有警告-g
生成调试信息,用于gdb调试-std=标准
指定语言标准,如c11
汇编
作用
- 将汇编代码转换为机器指令获得可重定位的目标文件.o
.o文件由以下部分主要组成:
- elf描述当前.o文件
- text、data、bss参见可执行文件
- symtab是符号表,符号有两种:
- 变量
- 函数及其形参(c语言只有函数,故c语言不支持重载)
上图为符号表,其中
- 第二列l为local,g为全局,即其他文件也可见,
- 第四列为符号地址,
- .data说明是位于数据区,即变量
- .text说明是位于函数区,即函数名和形参,
- UND代表没有在本文件中找到定义,需要链接器去其他文件找到并写入
- 第五列为符号,变量名不会改变,函数名如果带形参则会有一定改变
选项
- -c 只进行到汇编
链接
作用
- 所有.o文件段的合并,即text段合并等
- 符号表合并后,进行符号解析重定向,即所有对符号的引用,都要找到该符号定义的地方,如果没有找到或找到多个定义,都会报错
- 符号的重定向,给汇编指令(代码段)中所有的符号都填上虚拟地址 本质就是对主文件中的调用函数填上对应地址,查找函数地址顺序:主文件-->显示指定二进制文件-->显示指定库文件-->标准库中文件
- 静态库动态库都是按需链接,但是动态库运行时动态加载,使用-L指定动态库路径后,在该库中找到的符号都标记为该动态库的符号;之后在运行时,需要由动态连接器去固定位置加载.so动态库进内存,获得该符号地址后使用
- 动态库的“链接”是分阶段的:编译时静态解析符号,运行时动态加载代码
- 把多个二进制的目标文件链接成一个单独的可执行文件,和.o文件很像,但是多了一个program header,告知系统运行这个程序时加载哪些部分(代码段、数据段)到内存,且elf头部记录程序入口地址
- 找到依赖的库文件(静态与动态)
选项
-L
解决"库文件在哪里"的问题(系统标准库不需要指定),-l
解决"需要哪些库符号"的问题(涉及到库的都需要指定),一般第三方库需要两者配合使用
-L目录
添加库文件搜索路径-l库名
链接指定库,如-lpthread
,注意这里是小写的l
预处理器
对于#include,就是粘贴复制头文件中代码
obj单定义规则: 全局变量和函数只能有一个定义,类、模板以及内联函数可以在多个翻译单元中有完全相同的定义,所以不要将函数和变量定义放在头文件中。
代码高知h和.cpp应该成对出现
使用双引号时,预处理器知道这是编写的头文件。预处理器首先在当前目录中搜索头文件。如果找不到匹配的头文件,将搜索系统目录。
没有.h扩展名头文件,声明了std命名空间中的所有标识符,使用标准库头文件时,优先使用不带.h扩展名的版本,即std命名空间中的标识符,这也是为什么引入string之后,却使用std::string的原因
当引入其他目录下的头文件时,尽量别使用相对路径,而是更改单个编辑器的设置-I头文件路径,g++ -o main -I/source/includes main.cpp
对于ifdef、ifndef、endif,只对单文件使用
#if 0 可用于注释
对于#define,进行简单的文本替换
链接器
链接器将程序中的变量、函数等符号(Symbols) 转换为可执行文件中的内存地址
- 符号地址的分配方式
- 静态链接(非PIC) : 链接器为符号分配绝对虚拟地址(如
0x400520
),这些地址在程序加载时固定不变。 示例:main
函数可能位于0x401000
,其他函数按编译顺序依次排列。 - 动态链接(PIC) : 使用位置无关代码时,符号地址为相对于加载基址的偏移,通过全局偏移表GOT动态计算实际地址。
- 动态库函数的处理(PLT/GOT) 动态库函数(如
printf
)的调用通过以下机制实现:
- PLT(过程链接表) : 首次调用
printf@plt
时,PLT跳转到动态链接器(ld-linux.so
)解析函数真实地址,并更新到GOT中。 - GOT(全局偏移表) : 存储动态库函数的实际内存地址,后续调用直接跳转到GOT中的地址,避免重复解析。
// 首次调用触发地址解析 printf("Hello"); // 编译为 call printf@plt
- 未解析符号的报错规则
- 静态链接阶段: 若符号未在目标文件(
.o
)或静态库(.a
)中定义,直接报错undefined reference
。 - 动态链接阶段: 若动态库缺失或符号未找到(如运行时
libfoo.so
未安装),程序加载时报错cannot open shared object file
。
库
库本质是 目标文件(
.o
)的集合,即一堆函数的集合,但是去掉了编译(预处理编译汇编)过程(最耗时的部分)
静态库
有两个文件:头文件和库文件.a/.lib,库文件(二进制文件)提供函数的具体实现,链接时会查找库文件,将未描述的符号写入目标文件
制作lib库名.a
- 将.c生成.o文件
gcc -c add.c -o add.o
- 使用ar工具制作静态库
ar rcs lib库名.a add.o sub.o div.o
使用
- 源代码中引入库的头文件
- 编译静态库到可执行文件中
gcc test.c lib库名.a -o a.out
,不推荐,会出现下面问题
链接器按 从左到右的顺序 处理输入文件,且只在处理到该库时 检查当前未解析符号,并提取需要的
.o
文件,所以如果将库名和源文件顺序交换,则会报错,流程:
- 先处理库文件,此时没有任何未解析符号,因为没有编译test.c
- 连接器认为不需要该库中任何内容,跳过整个库
- 再编译test.c生成test.o,并发现存在未解析的函数(即找不到定义)
- 查找标准库,并将未解析的函数填上地址,如printf()
- 发现最终依然存在未解析的函数,报错
- 推荐
gcc test.c -o a.out -L. -l库名 -I头文件目录
动态库
动态库(
.so
或.dll
)在编译时也需要指出动态库文件以及路径,但不会被合并到可执行文件中,只是可执行文件按符号表中的符号依然需要在动态库中找到定义并标识为动态库符号;之后在程序运行到该符号需要地址时发现少了该库,如果该库没有转载到内存,则会转载到内容并获得所需符号的地址。动态库的代码(函数入口地址)可以被多个程序共享,减少内存占用,且更新库文件无需重新编译主程序。
制作 lib库名.so
(Linux)或 库名.dll
(Windows)
- 生成位置无关的目标文件:
gcc -c -fPIC add.c sub.c div.c # -fPIC 生成位置无关代码(Position Independent Code)
- 输出:
add.o
,sub.o
,div.o
- 将目标文件打包为动态库:
# Linux gcc -shared -o lib库名.so add.o sub.o div.o # Windows(MinGW) gcc -shared -o 库名.dll add.o sub.o div.o
- 输出:
lib库名.so
(Linux)或库名.dll
(Windows)
使用动态库
- 源代码中引入头文件
- 编译时链接动态库
gcc test.c -o a.out -L. -l库名 -I头文件目录
- 运行时加载动态库
- Linux:设置
LD_LIBRARY_PATH
环境变量export LD_LIBRARY_PATH=动态库路径//只会对当前bash生效,且如果关闭后会丢失
- Windows:将
.dll
文件放在以下任一目录:- 可执行文件所在目录
- 系统目录(如
C:\Windows\System32
) - PATH 环境变量包含的目录
- Linux:设置
编译后生成的可执行文件,如果系统中找不到动态库,会报错