📅  最后修改于: 2023-12-03 14:56:58.339000             🧑  作者: Mango
无论是在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程序背后的一些基本概念,并通过一个实际的例子演示了这些概念的应用,希望可以为读者们提供更加深入和丰富的认识。同时,也建议各位程序员在平时的学习与工作中多关注编译这一块内容,增加自己对计算机系统底层的理解和把握。