Skip to content

编译

C 语言是一种编译型语言,程序需要经过编译器的处理生成可执行文件后才能被计算机运行。C 语言的编译过程可以分为四个阶段:预处理、编译、汇编和链接。

  • 预处理: 在预处理阶段,编译器会对源代码进行处理,包括去掉注释、展开宏定义等。预处理器的主要工作就是将所有以#开头的指令都进行替换。例如 #define 定义一个宏时,预处理器就会将这个宏展开到所有使用它的地方。
  • 编译:在编译阶段,编译器会将预处理后的源代码转换成汇编代码,也就是将高级语言转换成低级语言。在这个过程中,编译器会进行语法检查、语义分析等操作,并将转换后的代码保存为目标文件。目标文件是一种二进制格式的文件,其中包含了可重定位的机器代码和符号表信息。
  • 汇编:在汇编阶段,汇编器将目标文件转换成机器码,这个过程主要是将汇编代码翻译成机器码指令,生成二进制目标文件。汇编器的输出是一组字节序列,这些字节序列代表了程序的指令和数据。
  • 链接: 在链接阶段,链接器将多个目标文件和库文件合并成一个可执行文件。在这个过程中,链接器会根据符号表信息对目标文件进行地址重定向,并将所有符号引用都解析为实际的地址。链接器的输出是一个可执行程序,其中包含了所有需要的代码和数据。

编译示例

c
#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, 执行以下命令进行文件的预处理

c
gcc -E main.c -o main.i

此时会生成一个 mian.i 的文件,打开查看里面的内容会发现,我们的代码在经过预处理之后,#include <stdio.h>中的内容都替换过来了。最下面的位置就是我们的代码了:

c
...
__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!");
}

预处理之后,我们将可以将其编译为汇编程序了

c
gcc -S main.i -o main.s

这里的 -S 就是预处理之后的文件,我们可以直接将其编译成汇编代码

c
	.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

有了汇编指令,我们就可以将汇编代码编译成二进制文件

c
gcc -c main.s -o main.o

但是生成的 main.o 文件无法直接运行它,我们还需要将他变成变成 windows 可以执行的程序

c
gcc main.o -o main

这里直接将刚生成的目标文件会编译成可执行文件 main.exe,此时可以正常运行了

整个编译过程如下:

c
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 语言程序。当然如果我们要更快速一点地完成编译,可以直接将源文件进行编译:

c
gcc main.c -o main

以上是单个文件的编译,如果是多个文件呢?编译时需要带上多个文件的路径,如下:

c
gcc test.c main.c -o main

一两个文件还可以这么编译,文件量大了之后,我们就需要借助更好的编译构建工具。

Make 和 CMake 构建

为了方便构建和管理工程,通常采用Make或CMake工具进行编译构建。Make是一个非常流行的构建工具,而CMake则是一个更加高级的构建系统生成工具。

Make

Make 是一种自动化构建工具,通过读取 Makefile 文件中的依赖关系,自动地执行编译、链接等操作,从而生成可执行程序。Makefile 文件中定义了目标、依赖和指令等内容。其中目标是指要构建的可执行文件或库文件,依赖是指目标文件所依赖的源文件或其他目标文件,指令是指运行编译和链接命令的具体步骤。

c
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 参数指定文件路径。

c
make
``

上述命令将会根据 Makefile 文件自动生成目标可执行程序 hello。如果想要重新编译,可以使用 clean 目标清除所有生成的 .o 和可执行程序:

```c
make clean

CMake

CMake 是一个跨平台的构建工具,类似于 Make 工具。它不直接构建代码,而是生成相应的构建脚本(如 Unix 下的 Makefile 或 Windows 下的 Visual Studio 项目),再由这些构建脚本来完成实际的编译过程。

CMake 配置文件通常被称为 CMakeLists.txt,与 Makefile 的语法有所不同。以下是一份简单的 CMakeLists.txt 文件示例:

c
cmake
cmake_minimum_required(VERSION 3.10)

project(hello)

add_executable(hello main.c func.c)

在该配置文件中,使用 project() 函数定义了项目名称,并通过 add_executable() 函数添加源代码文件来创建可执行程序。

与 Make 不同,CMake 支持多种编译系统和多种操作系统,并且可以使用图形化界面进行配置和管理。例如,在 Linux 系统下生成 Unix 的 Makefile 脚本可以使用以下命令:

c
cmake .

如果要生成 Visual Studio 项目,则需要在 Windows 系统下运行以下命令:

c
cmake -G "Visual Studio 15 2017" .

在生成 Makefile 或 Visual Studio 项目文件后,即可使用相应的编译工具来进行实际的编译过程。

总结

Make 和 CMake 都是常用的构建工具,它们能够自动化地编译程序,避免了手动重新编译的烦恼。两者语法有所不同,但都支持生成多种平台和操作系统下的构建脚本,并且可以通过命令行或图形化界面进行管理和配置。CMake 相比于 Make 具有更好的跨平台性和可移植性,在复杂项目中使用更加便捷。

Released under the MIT License.