编译
C 语言是一种编译型语言,程序需要经过编译器的处理生成可执行文件后才能被计算机运行。C 语言的编译过程可以分为四个阶段:预处理、编译、汇编和链接。
- 预处理: 在预处理阶段,编译器会对源代码进行处理,包括去掉注释、展开宏定义等。预处理器的主要工作就是将所有以#开头的指令都进行替换。例如 #define 定义一个宏时,预处理器就会将这个宏展开到所有使用它的地方。
- 编译:在编译阶段,编译器会将预处理后的源代码转换成汇编代码,也就是将高级语言转换成低级语言。在这个过程中,编译器会进行语法检查、语义分析等操作,并将转换后的代码保存为目标文件。目标文件是一种二进制格式的文件,其中包含了可重定位的机器代码和符号表信息。
- 汇编:在汇编阶段,汇编器将目标文件转换成机器码,这个过程主要是将汇编代码翻译成机器码指令,生成二进制目标文件。汇编器的输出是一组字节序列,这些字节序列代表了程序的指令和数据。
- 链接: 在链接阶段,链接器将多个目标文件和库文件合并成一个可执行文件。在这个过程中,链接器会根据符号表信息对目标文件进行地址重定向,并将所有符号引用都解析为实际的地址。链接器的输出是一个可执行程序,其中包含了所有需要的代码和数据。
编译示例
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
环境变量配置
windows 将 mingw 的 bin 目录配置到环境变量 path 中,我当前用的是 CLion 自带的 mingw,路径是: D:\Program Files\JetBrains\CLion 2023.1.2\bin\mingw\bin
将 CLion 的 Terminal 配置为 PowerShell, 配置 settings -> terminal -> shell path 为 PowerShell,配置完毕后重启 CLion, 在 PowerShell 中执行 gcc --help, 如果不生效需要重启电脑。
gcc 编译过程
在 CLion 中打开 Terminal, 执行以下命令进行文件的预处理
gcc -E main.c -o main.i
此时会生成一个 mian.i 的文件,打开查看里面的内容会发现,我们的代码在经过预处理之后,#include <stdio.h>中的内容都替换过来了。最下面的位置就是我们的代码了:
...
__attribute__ ((__dllimport__)) int __attribute__((__cdecl__)) _wscanf_l(const wchar_t *_Format,_locale_t _Locale,...);
# 1559 "D:/Program Files/JetBrains/CLion 2023.1.2/bin/mingw/x86_64-w64-mingw32/include/stdio.h" 2 3
# 2 "main.c" 2
# 3 "main.c"
int main() {
printf("Hello world!");
}
预处理之后,我们将可以将其编译为汇编程序了
gcc -S main.i -o main.s
这里的 -S 就是预处理之后的文件,我们可以直接将其编译成汇编代码
.file "main.c"
.text
.def printf; .scl 3; .type 32; .endef
.seh_proc printf
printf:
pushq %rbp
.seh_pushreg %rbp
pushq %rbx
.seh_pushreg %rbx
subq $56, %rsp
.seh_stackalloc 56
leaq 48(%rsp), %rbp
.seh_setframe %rbp, 48
.seh_endprologue
movq %rcx, 32(%rbp)
movq %rdx, 40(%rbp)
movq %r8, 48(%rbp)
movq %r9, 56(%rbp)
leaq 40(%rbp), %rax
movq %rax, -16(%rbp)
movq -16(%rbp), %rbx
movl $1, %ecx
movq __imp___acrt_iob_func(%rip), %rax
call *%rax
movq %rbx, %r8
movq 32(%rbp), %rdx
movq %rax, %rcx
call __mingw_vfprintf
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
addq $56, %rsp
popq %rbx
popq %rbp
ret
.seh_endproc
.def __main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.LC0:
.ascii "Hello world!\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $32, %rsp
.seh_stackalloc 32
.seh_endprologue
call __main
leaq .LC0(%rip), %rax
movq %rax, %rcx
call printf
movl $0, %eax
addq $32, %rsp
popq %rbp
ret
.seh_endproc
.ident "GCC: (GNU) 11.2.0"
.def __mingw_vfprintf; .scl 2; .type 32; .endef
有了汇编指令,我们就可以将汇编代码编译成二进制文件
gcc -c main.s -o main.o
但是生成的 main.o 文件无法直接运行它,我们还需要将他变成变成 windows 可以执行的程序
gcc main.o -o main
这里直接将刚生成的目标文件会编译成可执行文件 main.exe,此时可以正常运行了
整个编译过程如下:
PS D:\develop\learn\cpt> gcc -E main.c -o main.i
PS D:\develop\learn\cpt> gcc -S main.i -o main.s
PS D:\develop\learn\cpt> gcc -c main.s -o main.o
PS D:\develop\learn\cpt> gcc main.o -o main
PS D:\develop\learn\cpt> .\main.exe
Hello world!
以上步骤就是手动编译一个 C 语言程序。当然如果我们要更快速一点地完成编译,可以直接将源文件进行编译:
gcc main.c -o main
以上是单个文件的编译,如果是多个文件呢?编译时需要带上多个文件的路径,如下:
gcc test.c main.c -o main
一两个文件还可以这么编译,文件量大了之后,我们就需要借助更好的编译构建工具。
Make 和 CMake 构建
为了方便构建和管理工程,通常采用Make或CMake工具进行编译构建。Make是一个非常流行的构建工具,而CMake则是一个更加高级的构建系统生成工具。
Make
Make 是一种自动化构建工具,通过读取 Makefile 文件中的依赖关系,自动地执行编译、链接等操作,从而生成可执行程序。Makefile 文件中定义了目标、依赖和指令等内容。其中目标是指要构建的可执行文件或库文件,依赖是指目标文件所依赖的源文件或其他目标文件,指令是指运行编译和链接命令的具体步骤。
hello: main.o func.o
gcc -o hello main.o func.o
main.o: main.c
gcc -c main.c
func.o: func.c
gcc -c func.c
clean:
rm -rf *.o hello
上述 Makefile 中定义了三个目标:hello、main.o 和 func.o。其中 hello 是最终要生成的可执行程序,在该目标下有一个依赖列表:main.o 和 func.o。表示在生成 hello 之前需要先编译 main.c 和 func.c 两个源代码文件,并将它们分别链接成相应的 .o 目标文件。
当运行 make 命令时,默认会以当前目录下名为 Makefile 或者 makefile 的文件为 Makefile 文件。如果 Makefile 文件名不是这两者之一,也可以通过 -f 参数指定文件路径。
make
``
上述命令将会根据 Makefile 文件自动生成目标可执行程序 hello。如果想要重新编译,可以使用 clean 目标清除所有生成的 .o 和可执行程序:
```c
make clean
CMake
CMake 是一个跨平台的构建工具,类似于 Make 工具。它不直接构建代码,而是生成相应的构建脚本(如 Unix 下的 Makefile 或 Windows 下的 Visual Studio 项目),再由这些构建脚本来完成实际的编译过程。
CMake 配置文件通常被称为 CMakeLists.txt,与 Makefile 的语法有所不同。以下是一份简单的 CMakeLists.txt 文件示例:
cmake
cmake_minimum_required(VERSION 3.10)
project(hello)
add_executable(hello main.c func.c)
在该配置文件中,使用 project() 函数定义了项目名称,并通过 add_executable() 函数添加源代码文件来创建可执行程序。
与 Make 不同,CMake 支持多种编译系统和多种操作系统,并且可以使用图形化界面进行配置和管理。例如,在 Linux 系统下生成 Unix 的 Makefile 脚本可以使用以下命令:
cmake .
如果要生成 Visual Studio 项目,则需要在 Windows 系统下运行以下命令:
cmake -G "Visual Studio 15 2017" .
在生成 Makefile 或 Visual Studio 项目文件后,即可使用相应的编译工具来进行实际的编译过程。
总结
Make 和 CMake 都是常用的构建工具,它们能够自动化地编译程序,避免了手动重新编译的烦恼。两者语法有所不同,但都支持生成多种平台和操作系统下的构建脚本,并且可以通过命令行或图形化界面进行管理和配置。CMake 相比于 Make 具有更好的跨平台性和可移植性,在复杂项目中使用更加便捷。