📜  编译C程序:-幕后(1)

📅  最后修改于: 2023-12-03 14:56:58.339000             🧑  作者: Mango

编译C程序:-幕后

无论是在Linux、Windows还是MacOS,编写代码并将其转换为可执行程序都需要经过一系列的步骤。在C语言中,这个过程被称为编译。相信大多数程序员都对这个过程有了基本的了解,但是这篇文章将会从理论和实践两个方面来深入地探究编译C程序的幕后过程。

理论
预处理

在编译阶段的第一步,预处理器会扫描整个源代码并根据预处理指令来展开代码。在C语言中,预处理器指令以#开头,例如#include、#define等。预处理器处理后的代码被称为扩展程序(expanded program),实际上就是源代码中所有宏定义被替换成对应的代码。

编译

经过预处理的代码被送入编译器进行编译。编译器将读取源代码并将其转化为中间代码(又称为汇编语言)。中间代码是一种低级的、平台无关的代码,其语法与汇编语言类似,但可以被转换成不同平台的二进制代码。中间代码的生成是编译的核心过程之一。

汇编

在得到中间代码后,编译器运行汇编器将其转化为汇编语言。汇编语言是一种平台相关的代码,与特定平台的CPU指令集相关。因此,汇编语言的生成是编译的第二个核心步骤。

链接

在生成汇编代码后,链接器会将其与库文件中的代码合并,最终生成可执行文件。在C语言中,库文件通常包含标准库和其他可重用函数的实现。

实践

以上的理论是抽象的,很难给我们直观的体验。接下来,我们将通过一个简单的C程序来看看编译的实际过程。

假设我们将下面的代码保存为hello.c:

#include <stdio.h>

int main() {
    printf("Hello, world\n");
    return 0;
}
第一步:预处理

我们可以通过以下命令来执行预处理步骤:

$ gcc -E hello.c -o hello.i

这个命令会执行预处理并将结果输出到hello.i文件中。我们可以用如下命令查看预处理后的代码:

$ cat hello.i

预处理后的代码如下所示:

# 1 "hello.c"
# 1 "<built-in>"
# 1 "<命令行>"
# 1 "hello.c"

# 1 "/Library/Developer/CommandLineTools/usr/include/stdc-predef.h" 1 3 4
# 1 "hello.c" 2

# 1 "/Library/Developer/CommandLineTools/usr/include/stdio.h" 1 3 4
# 64 "/Library/Developer/CommandLineTools/usr/include/stdio.h" 3 4
typedef struct __sFILE FILE;
# 92 "/Library/Developer/CommandLineTools/usr/include/stdio.h" 3 4
extern FILE *stdout;


int main() {
    printf("Hello, world\n");
    return 0;
}

我们可以看到,预处理器将我们的代码展开了,头文件"stdio.h"和类型FILE被包含在内。

第二步:编译

接下来我们使用以下命令来执行编译步骤:

$ gcc -S hello.i -o hello.s

这个命令会将hello.i转化为中间代码(汇编代码)并将结果输出到hello.s文件中。我们可以用如下命令查看编译后的代码:

$ cat hello.s

编译后的代码如下所示:

	.section	__TEXT,__text,regular,pure_instructions
	.build_version darwin, 20, 0 sdk_version 11.0
	.globl	_main
	.p2align	4, 0x90
_main:
	pushq	%rbp
	movq	%rsp, %rbp
	leaq	L_.str(%rip), %rdi
	movl	$0, %eax
	callq	_printf
	xorl	%eax, %eax
	popq	%rbp
	retq
	.section	__TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
	.asciz	"Hello, world\n"

.subsections_via_symbols

编译器将代码转化为汇编语言,这段汇编代码是跨平台的、与CPU相关的低级代码。在这个简单的示例中,编译器直接将printf转换成汇编语言。

第三步:汇编

现在我们使用以下命令来执行汇编步骤:

$ gcc -c hello.s -o hello.o

这个命令会将汇编代码转化为二进制目标代码(也被称为目标文件),并将结果输出到hello.o文件中。我们可以用如下命令查看汇编后的代码:

$ hexdump -C hello.o

输出的结果如下所示:

00000000  cf fa ed fe 07 00 00 01  03 00 20 00 00 00 00 00  |.......... .....|
00000010  00 00 00 00 00 00 00 00  02 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 40 00 00 00  |............@...|
00000030  00 00 28 00 00 00 00 00  00 00 00 00 31 00 00 00  |..(.........1...|
00000040  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
00000050  00 00 00 00 06 00 00 00  00 10 00 00 00 00 00 00  |................|
00000060  00 00 00 00 00 10 00 00  00 00 00 00 02 00 00 00  |................|
00000070  47 4c 4f 42 00 00 00 00  00 00 00 00 00 00 00 00  |GLOB............|
00000080  00 00 00 00 01 00 00 00  07 00 00 00 00 00 00 00  |................|
00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000240  48 65 6c 6c 6f 2c 20 77  6f 72 6c 64 0a 00 00 00  |Hello, world....|
00000250

在这一步中,编译器生成了机器代码。我们使用hexdump命令来查看生成的目标文件。可以看到,目标文件中有一些头部信息,但大部分是机器代码。

第四步:链接

最后,我们使用以下命令来执行链接步骤:

$ gcc hello.o -o hello

这个命令会将“hello.o”目标文件链接到所有必要的库文件中以生成可执行文件“hello”。我们可以使用以下命令来运行它:

$ ./hello

输出的结果如下所示:

Hello, world

这里我们可以看到,编译、汇编和链接步骤的结果最终形成了可执行文件。

结论

在这篇文章中,我们介绍了编译C程序背后的一些基本概念,并通过一个实际的例子演示了这些概念的应用,希望可以为读者们提供更加深入和丰富的认识。同时,也建议各位程序员在平时的学习与工作中多关注编译这一块内容,增加自己对计算机系统底层的理解和把握。