gcc

ZaynPei Lv6

GCC 编译的四个阶段

一个 C 语言源文件要变成可执行文件,需要经过预处理 (Preprocessing)、编译 (Compilation)、汇编 (Assembly) 和 链接 (Linking) 这四个核心步骤。

当你执行一个简单的命令 gcc hello.c -o hello 时,GCC 实际上在后台为你自动完成了所有四个步骤, 分别是:

第一步预处理 (Preprocessing): 这个阶段处理源代码中以 # 开头的指令。它不关心代码的语法,只是对文本进行简单的替换和处理

  • 输入文件: C 源文件,例如 hello.c
  • 输出文件: 经过预处理的 C 文件,通常以 .i 为后缀,例如 hello.i
  • GCC 命令: gcc -E hello.c -o hello.i
    • 因为这里只展示预处理, -E 选项告诉 GCC 在预处理完成后就停止,不要进行后续步骤。
    • -o 选项指定输出文件的名称, 这里是 hello.i
  • 主要工作内容:
    • 展开宏定义: 将所有 #define 定义的宏进行文本替换。
    • 处理条件编译: 根据 #if, #ifdef, #else, #endif 等指令,选择性地保留或移除代码段。
    • 包含头文件: 将 #include 指定的头文件内容原封不动地插入到源文件中。
    • 删除注释和多余空白: 移除代码中所有的注释 (// … 和 /* … */)、不必要的空行和空白字符。

经过这个阶段,hello.i 文件会变成一个不包含任何宏、头文件引用或注释的“纯净”C代码文件

第二步编译 (Compilation) 这是整个编译过程中最核心的阶段,它将预处理后的 C 代码翻译特定于目标平台汇编代码。

  • 输入文件: 预处理后的文件,例如 hello.i
  • 输出文件: 汇编代码文件,通常以 .s 为后缀,例如 hello.s
  • GCC 命令: gcc -S hello.i -o hello.s
    • -S 选项告诉 GCC 在生成汇编代码后就停止。
  • 主要工作内容:
    • 词法分析: 将源代码分解成一个个独立的“记号”(tokens),如关键字、标识符、操作符等。
    • 语法分析: 根据 C 语言的语法规则,将记号组织成一棵“语法树”,并检查是否存在语法错误(例如,括号不匹配、缺少分号等)。
    • 语义分析: 检查代码的语义是否正确(例如,类型是否匹配、变量是否已声明)。
    • 代码优化: 对生成的中间代码进行优化,以提高程序的运行速度和减小体积。
    • 生成汇编代码: 将优化后的代码转换成目标平台的汇编语言。 > 编译器优化

这个阶段涉及复杂的分析和优化过程,通常是整个流程中消耗时间和系统资源最多的部分。

第三步汇编 (Assembly): 汇编阶段将人类可读的汇编代码转换成机器可以执行的二进制指令

  • 输入文件: 汇编代码文件,例如 hello.s
  • 输出文件: 可重定位目标文件 (Relocatable Object File),在 Linux/macOS 中通常以 .o 为后缀,在 Windows 中为 .obj,例如 hello.o
  • GCC 命令: gcc -c hello.s -o hello.o
    • -c 选项告诉 GCC 在生成目标文件后就停止。
    • 由于汇编过程也包含了预处理和编译两个阶段,所以 -c 选项也会触发这两个阶段的执行, 也就是可以 gcc -c hello.c -o hello.o 来直接生成目标文件。
  • 主要工作内容:
    • 指令翻译: 将每一条汇编指令(如 mov, add, jmp)翻译成其对应的二进制机器码。
    • 生成目标文件: 将机器码以及符号表、重定位信息等打包成一个特定格式的目标文件(在 Linux 中通常是 ELF 格式)。此时,文件中的代码和数据地址都是相对的,而不是最终的绝对内存地址。

第四步链接 (Linking): 链接是创建可执行文件的最后一步。它将多个目标文件以及程序所需的库文件(如标准库)的二进制代码(.o 文件)组合在一起。

  • 输入文件: 一个或多个目标文件 (.o) 和所需的库文件。
  • 输出文件: 可执行文件,在 Linux/macOS 中默认为 a.out,也可以通过 -o 指定名称(如 hello)。
  • GCC 命令: gcc hello.o -o hello
    • 不带 -E, -S, -c 等选项时,GCC 默认执行所有四个步骤,并最终进行链接。
  • 主要工作内容:
    • 合并段: 将所有输入目标文件中的相同类型的段(如代码段 .text、数据段 .data)合并到最终可执行文件中的一个大段中。
    • 符号解析与地址回填 (重定位):
      • 解析: 找到代码中引用的函数和变量(例如 printf 函数)在其他目标文件或库文件中的确切位置。
      • 回填: 在汇编阶段,函数调用和变量访问的地址是未知的。链接器会计算出它们在虚拟内存中的最终地址,并修改代码中的占位符,使其指向正确的内存地址。这个过程就是图片中提到的地址回填。

gcc编译常用参数

当头文件和源码不在一个目录下时,需要指定头文件的搜索路径 - -I 选项: 用于指定头文件的搜索路径。可以多次使用这个选项来添加多个路径。 - 例如:gcc -I /path/to/headers -o hello hello.c - 这会告诉 GCC 在编译 hello.c 时,先去 /path/to/headers 目录下查找头文件。

  • -c 只做预处理,编译,汇编。得到二进制文件
  • -g 编译时添加调试文件,用于gdb调试
  • -Wall 显示所有警告信息
  • -D 向程序中“动态”注册宏定义
  • -l 指定动态库库名
  • -L 指定动态库路径

静态库和动态库

静态库在文件中静态展开, 动态库在运行时才被加载到内存中 前者在编译时就被链接到可执行文件中, 因此内存占用大, 但是速度快; 后者在运行时才被链接到可执行文件中, 因此内存占用小, 但是速度较慢

静态库制作及使用

静态库名字以lib开头,以.a结尾 例如:libmylib.a

静态库生成指令: ar rcs [lib名称.a] [用于生成库的.o文件] ar rcs libmylib.a file1.o file2.o

制作静态库的步骤: 1. 编写静态库的源文件 2. 编译源文件生成.o文件(二进制指令), 例如: gcc -c add.c -o add.o 3. 使用ar命令将.o文件打包成静态库

静态库的使用:gcc test.c libmylib.a -o a.out, 如果静态库和可执行文件不在同一个目录下, 则需要指定静态库的路径, 例如: gcc test.c ./lib/libmylib.a -o a.out

还要注意的一点是, 静态库在引入使用时需要配套的头文件, 也就是代码最上方的#include . 因为头文件是给编译器看的, 编译器需要根据头文件中的函数声明和变量定义来进行编译; 而静态库中的.o文件是给链接器看的, 链接器需要根据.o文件中的符号表来进行链接

如果没有头文件: 编译器会因为找不到函数的声明而报错,提示“函数未定义”或“隐式声明”(undeclared/implicit function),导致编译失败。编译器根本不知道这个函数的存在,也就无法生成正确的目标文件 (.o)。

当你从第三方获取一个库时,通常会得到 .a (或 .lib) 文件和一系列的 .h 文件,这两部分都必须正确配置到你的项目中才能成功使用。 在平时使用标准库编写代码过程中, 我们在引入头文件后直接编译源文件, 编译器会自动帮我们完成静态库的链接工作, 即根据头文件中的函数声明去查找对应的静态库文件(.a)并进行链接。

动态库制作

制作动态库的步骤:

  1. 编译生成位置无关的.o文件 gcc -c add.c -o add.o -fPIC 使用这个参数过后,生成的函数就和位置无关,挂上@plt标识,等待动态绑定. 这里的-fPIC是Position Independent Code的缩写, 表示生成位置无关的代码, 可以被加载到任意内存地址执行.
  2. 使用 gcc -shared制作动态库 gcc -shared -o [lib库名.so] [用于生成库的.o文件]
  3. 链接可执行程序, 同时指定所使用的动态库。 gcc test.c -o a.out -l mymath -L ./lib -I /path/to/headers -Wl,-rpath,‘$ORIGIN/lib’
  • -l:指定库名
  • -L:指定库路径, 只在链接时使用, 运行时需要通过-Wl,-rpath,’$ORIGIN/lib’指定动态库的搜索路径
  • -I:可能会需要指定头文件的搜索路径
  • -Wl,-rpath,‘$ORIGIN/lib’:指定动态库的搜索路径, 这里的ORIGIN −  − Wl, −rpath,ORIGIN/lib’在可执行文件中直接指定动态库的搜索路径.
  1. 运行可执行程序./a.out 注意 > 运行时, 动态库需要在系统的库搜索路径中, 如果不加-Wl,-rpath,‘$ORIGIN/lib’, 则需要通过设置LD_LIBRARY_PATH环境变量来指定动态库的搜索路径. 或者移动自制的动态库到系统的库搜索路径中.

静态库和动态库的函数地址解析