Loading

1 什么是编译

在C/C++语言中,编译就是将源代码转换为机器代码的一个过程,以便计算机可以理解和执行。

编译过程通常包括以下几个步骤:

  1. 预处理(Pre-Processing):
    • 所作工作:展开宏、头文件、替换条件编译、删除注释、空白,最终生成一个无预处理指令的代码文件
    • 目标文件:若采用GCC编译,则其会通过预处理器(CPP)将 .c 源文件生成 .i 预处理文件
  2. 编译(Compilation)
    • 所作工作:检查语法规范,将预处理后的代码翻译成汇编代码(汇编代码是与特定计算机架构相关的低级代码)
    • 目标文件:若采用GCC编译,则其会通过编译器(CCL)将 .i 预处理文件生成 .s 汇编文件
  3. 汇编(Assembly)
    • 所作工作:将汇编代码转换为机器代码
    • 目标文件:若采用GCC编译,则其会通过汇编器(AS)将 .s 汇编文件生成 .o 或 .obj 目标文件,这种格式是计算机能直接执行的二进制格式
  4. 链接(Linking)
    • 所作工作:处理不同模块间的引用,数据段合并,数据地址回填,使得所有代码可以协同工作
    • 目标文件:若采用GCC编译,则其会通过链接器(LD)将多个目标文件和库文件链接生成一个可执行文件或库文件

2 编译过程的中间文件

  • .c:源代码文件
  • .i:预处理文件
  • .s:汇编文件
  • .o:目标文件
  • .a:Linux下的静态库文件,由多个.o文件链接而成
  • .so:Linux下的动态库文件,由多个.o文件链接而成
  • .lib:Windows下的静态库文件,由多个.o文件链接而成
  • .dll:Windows下的动态库文件,由多个.o文件链接而成
  • .dll.a:.dll文件的导出声明文件
  • .exe:Windows下的可执行文件,由多个目标文件和库文件链接而成

3 GCC

GCC(GNU Compiler Collection)是由GNU开发的编译器套件,也可以简单认为其就是编译器。它可以编译多种语言(包括C、C++、Objective-C、Fortran、Java等等)。当我们的程序只有一个源文件时,可以直接用gcc命令编译它。

3.1 基本用法

gcc [options] [filenames]
gcc 命令用于调用GCC编译器,options作为参数用于指示GCC的具体操作,filenames用于指定操作的具体文件

options的不同参数与功能

为空时:指示编译器对输入文件进行全链路操作,生成可执行文件

gcc main.c -o main  // 调用GCC将输入文件生成可执行文件main.exe
gcc main.c          // 没有 -o, GCC 生成可执行文件名称默认 a.exe / a.out
gcc main.i
gcc main.s
gcc main.o

-o:指示编译器为操作后的输出文件指定文件名称

gcc main.c -o main  // -o 区分大小写
gcc main.c          // 没有 -o, GCC 生成可执行文件名称默认 a.exe / a.out

-E:指示编译器仅对输入文件进行预处理

gcc -E main.c -o main.i  // -E 区分大小写

-S:指示编译器仅对输入文件进行编译

gcc -S main.i -o main.s  // -S 区分大小写
gcc -S main.c -o main.s

-c:指示编译器仅对输入文件进行汇编,通常用于编译不包含主程序的子程序文件

gcc -c main.s -o main.o  // -c 不区分大小写
gcc -c main.i -o main.o
gcc -c main.c -o main.o

-g:指示编译器在生成可执行文件时,同时产生能被GNU调试器GDB使用的调试信息,以调试程序

gcc -g main.c // -g 区分大小写
gcc -g main.i
gcc -g main.s
gcc -g main.0

-O:指示编译器对源代码进行优化。这些优化在大多数情况下都会使程序执行的更快。所谓优化,就是例如省略掉代码中从未使用过的变量、直接将常量表达式用结果值代替等,这些操作会缩减目标文件所包含的代码量,提高最终生成的可执行文件的运行效率。

# -O0 不做优化
# -O1 默认优化,同时减小代码的长度和执行时间,效果等价于 -O
# -O2 除了完成-O1的优化之外,还进行一些额外的调整工作,如指令调整
# -O3 除了完成-O2的优化之外,还进行一些包括循环展开和特性处理相关的优化工作
# -On 使得代码编译的速度变慢,但通常产生的代码执行速度会更快
gcc -g -O3 main.c -o main
gcc main.c -o main -g -O0

-l:指示编译器用指定的库来链接程序。-l 参数后紧接的就是库名,在/lib和/usr/lib和/usr/local/lib里的库直接用-l参数就能链接。

# 链接glog库
gcc -lglog main.c

-L:指示编译器在指定的目录寻找库文件。若库文件没放在上面三个目录里,需要使用 -L 参数指定库文件所在目录,-L参数后紧接的就是库文件所在的目录名。

# 链接mytest库,libmytest.so在/home/bing/mytestlibfolder目录下
gcc -L/home/bing/mytestlibfolder -lmytest main.c

-I:指示编译器在指定的目录寻找头文件。/usr/include等系统目录一般是不用指定的,GCC知道去那里找,但是若头文件不在系统目录里,则需要 -I 参数指定。比如头文件放在/myinclude目录里,那编译命令行就要加上-I/myinclude 参数。如果不加则会得到一个”xxxx.h: No such file or directory”的错误。-I 参数可以指定相对路径,比如头文件在当前目录,可以用-I.来指定。

gcc -I./include main.c -o main
gcc -I/d/1Document/3Code/test1/include main.c -o main

-Wall:指示编译器打印警告信息

gcc main.c -I./include -Wall

-w:指示编译器关闭警告信息

gcc main.c -w

-std:指示编译器使用指定的标准编译

# 使用 c11 标准编译
gcc main.c -std=c11

-v:指示编译器打印执行时的详细过程以及相关程序的版本号

gcc main.c -v

-D:指示编译器在预编译时定义指定的宏

gcc main.c -o main ./src/NXL_utils.c -I./include -DDEBUG  // 编译器在预编译时会定义 DEBUG 宏(真是用处多多)

-M:指示编译器列出文件对其他文件的依赖关系
-MM:指示编译器列出文件对其他文件的依赖关系,但是不包含系统头文件
-MMD:指示编译器列出文件对其他文件的依赖关系,但是不包含系统头文件,并且将依赖关系导入到.d文件里面

gcc -M foo.c
gcc -MM foo.c
gcc -MMD foo.c

3.2 进阶技巧

3.2.1 直接编译

最初的目录结构:

.
├── include
│   ├── NXL_Standard_Types.h
│   └── NXL_Utils.h
├── main.c
├── readme.md
└── src
    └── NXL_Utils.c

2 directories, 5 files 

最简单的编译 main.c 为可执行文件 main.exe:

gcc main.c ./src/NXL_utils.c -I./include -o main

增加一大堆参数编译 main.c 为可执行文件 main.exe:

gcc main.c ./src/NXL_utils.c -I./include -o main -std=c11 -Wall -g -O2

执行 main.exe:

./main

3.2.2 编译静态库并链接生成可执行文件

在根目录下创建bin子目录用于存储可执行文件:

mkdir -p bin // -p: 确保目录名称存在,不存在就创建一个

在根目录下创建output子目录用于存储编译过程的中间文件(.o / .a):

mkdir -p output

现目录结构:

.
├── bin
├── include
│   ├── NXL_Standard_Types.h
│   └── NXL_Utils.h
├── main.c
├── output
├── readme.md
└── src
    └── NXL_Utils.c

4 directories, 5 files

进入output子目录,汇编 NXL_Utils.c,生成对应的 NXL_Utils.o 文件:

cd output
gcc -c ../src/NXL_Utils.c -I../include // 在output子目录下执行汇编操作

生成静态库libNXL_Utils.a:

ar -rcs libNXL_Utils.a NXL_Utils.o // 在output子目录下生成静态库文件 libNXL_Utils.a

回到上级根目录,链接,生成可执行文件: staticmain

gcc main.c -I./include -L./output -lNXL_Utils -o ./bin/staticmain  // 链接生成可执行文件并且输出到bin目录

编译静态库并链接生成可执行文件后的目录结构:

.
├── bin
│   └── staticmain.exe      
├── include
│   ├── NXL_Standard_Types.h
│   └── NXL_Utils.h
├── main.c
├── output
│   ├── libNXL_Utils.a
│   └── NXL_Utils.o
├── readme.md
└── src
    └── NXL_Utils.c

4 directories, 8 files

3.2.3 编译动态库并链接生成可执行文件

现目录结构:

.
├── bin
├── include
│   ├── NXL_Standard_Types.h
│   └── NXL_Utils.h
├── main.c
├── output
├── readme.md
└── src
    └── NXL_Utils.c

4 directories, 5 files

先生成 NXL_Utils.o 文件:

gcc -c ./src/NXL_Utils.c -I./include -o ./output/NXL_Utils.o

再生成 libNXL_Utils.so 文件:

gcc -shared -fPIC ./output/NXL_Utils.o -o ./output/libNXL_Utils.so

也可以直接生成动态库 libNXL_Utils.so:

gcc -shared -fPIC ./src/NXL_Utils.c -I./include -o ./output/libNXL_Utils.so

链接,生成可执行文件:sharemain

gcc main.c -I./include -L./output -lNXL_Utils -o ./bin/sharemain

编译动态库并链接生成可执行文件后的目录结构:

.
├── bin
│   └── sharemain.exe       
├── include
│   ├── NXL_Standard_Types.h
│   └── NXL_Utils.h
├── main.c
├── output
│   ├── libNXL_Utils.so     
│   └── NXL_Utils.o
├── readme.md
└── src
    └── NXL_Utils.c

4 directories, 8 files

3.3 总结

虽然gcc编译指令使用起来非常方便,但也仅仅是针对源文件数量较少,类型较为单一的情况。如果我们要使用gcc来编译一个大型的项目,其中包括了很多的源文件,甚至还有不同类型的源文件时,用gcc命令逐个去编译,就很容易混乱而且工作量大,所以就出现了Make工具。关于Make工具的用法,博主会在该专栏的下一篇文章讲解。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

👤本站访客数: 👁️本站访问量: