前言
在平时写代码时,我们大多数时间都是直接使用集成开发环境,比如Visual Stdio
,这样的IDE一般都是将编译和链接过程一步完成,虽然很方便,但是却不利于我们理解代码的执行过程,以至于很多相关的bug无处下手。
这个系列就借阅读<<程序员的自我修养>>这本书的读书笔记,以及对其中的一些实战,尽量搞清楚这些原理,让写代码的时候能够做到"知其然,知其所以然"。
正文
我们就从最熟悉的Hello World
说起,使用C语言在Linux
下完成一个Hello World
程序非常容易,首先是代码:
C++
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
假设这里的的文件名是hello.c
,那么在命令行执行如下编译和运行指令即可:
ruby
zhangyuanhao@zhangyuanhao-VirtualBox:~/Desktop/CPlusStudy/gcc$ gcc hello.c
zhangyuanhao@zhangyuanhao-VirtualBox:~/Desktop/CPlusStudy/gcc$ ./a.out
Hello World
就是gcc
这个跟文件名这个简单的编译流程,其实可以分解为4个步骤,分别是预处理(Prepressing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。
这个过程涉了一些文件以及处理器如下图所示:
而搞懂整个编译流程,也就是搞明白图中的所有过程。
预编译
第一个流程就是把源代码文件hello.c
和一些头文件如stdio.h
一起通过预处理器cpp
编译成一个.i
文件。这个过程在实际写代码的时候非常有用,它可以实现源码级别的抽象,这个在后面关于C++的学习中细说。
这一步的编译过程,相当于如下指令:
ruby
zhangyuanhao@zhangyuanhao-VirtualBox:~/Desktop/CPlusStudy/gcc$ gcc -E hello.c -o hello.i
或者
cpp hello.c > hello.i
关于gcc
指令的用法,后面会不断地学习和熟悉,这里的-E
就是表示进行预编译,而-o
表示输出文件,而cpp
命令的>
符号是重定向符号,即把输出结果保存到hello.i
中。
预编译的输入其实就是文件,而处理的代码就是文件中以#
开始的预编译指令,主要的规则如下:
- 将所有的
#define
删除,并且展开所有的宏定义。 - 处理所有的条件编译指令,比如
#if
、#ifdef
、#elif
、#else
、#endif
等。 - 处理
#include
预编译指令,也就是将被包含的文件插入到该预编译的位置,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件,这时就要避免头文件被多次包含。 - 删除所有的注释。
- 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或者警告可以显示行号。
- 保留所有的
#pragma
编译器指令,因为编译器要用到。关于#pragma
指令还是非常有用的,用于向编译器传递指示或者指令,用于控制编译过程、设置编译选项、调整编译器行为等。比如产生在编译时的提示信息。
编译
第二个流程就是编译,这个过程是将预处理产生的文件进行一系列的词法分析、语法分析、语义分析以及优化,最终产生汇编代码文件,这个部分是最核心且复杂的。
对于gcc
编译套件来说,可以使用下面命令:
ruby
zhangyuanhao@zhangyuanhao-VirtualBox:~/Desktop/CPlusStudy/gcc$ gcc -S hello.i -o hello.s
同时在现代的GCC中会把预编译和编译这2个步骤给合并成一个,使用一个叫做cc1
的程序可以直接完成,命令如下:
xml
zhangyuanhao@zhangyuanhao-VirtualBox:~/Desktop/CPlusStudy/gcc$ /usr/lib/gcc/x86_64-linux-gnu/11/cc1 hello.c
main
Analyzing compilation unit
Performing interprocedural optimizations
<*free_lang_data> {heap 952k} <visibility> {heap 952k} <build_ssa_passes> {heap 952k} <opt_local_passes> {heap 952k} <remove_symbols> {heap 952k} <targetclone> {heap 952k} <free-fnsummary> {heap 952k}Streaming LTO
<whole-program> {heap 952k} <fnsummary> {heap 952k} <inline> {heap 952k} <modref> {heap 952k} <free-fnsummary> {heap 952k} <single-use> {heap 952k} <comdats> {heap 952k}Assembling functions:
<simdclone> {heap 952k} main
Time variable usr sys wall GGC
phase setup : 0.00 ( 0%) 0.00 ( 0%) 0.00 ( 0%) 1298k ( 68%)
phase parsing : 0.01 (100%) 0.00 ( 0%) 0.01 (100%) 531k ( 28%)
lexical analysis : 0.01 (100%) 0.00 ( 0%) 0.00 ( 0%) 0 ( 0%)
parser (global) : 0.00 ( 0%) 0.00 ( 0%) 0.01 (100%) 329k ( 17%)
TOTAL : 0.01 0.00 0.01 1896k
这2个方法都可以得到汇编输出文件hello.s
,对于C语言来说,这个预编译和编译的程序是cc1
,对于C++来说,对应的程序是cc1plus
,所以实际上gcc这个命令只是对这些后台程序的包装,根据不同的参数要求去调用不同的程序。
打开hello.s
如下所示:
perl
.file "hello.c"
.text
.section .rodata
.LC0:
.string "hello world"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rax
movq %rax, %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
.section .note.GNU-stack,"",@progbits
后面文章会一一分析这些字段。
汇编
汇编器就是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编的过程相比于编译就简单多了,它没有复杂的语法和语义,只需要根据汇编指令和机器指令的对照表进行翻译即可。
同样可以使用汇编器as
来进行汇编:
ruby
zhangyuanhao@zhangyuanhao-VirtualBox:~/Desktop/CPlusStudy/gcc$ as hello.s -o hello.o
当然也可以使用GCC的不同参数来完成:
ruby
zhangyuanhao@zhangyuanhao-VirtualBox:~/Desktop/CPlusStudy/gcc$ gcc -c hello.c -o hello.o
对于.o
文件,它是标准的ELF文件,这里就不仔细说了,后面会详细分析这类文件。
链接
链接是最容易忽略和出错的环节,也是一个比较让人费解的过程,为何不直接输出可执行文件而是一个目标文件呢?
就比如这里简单的Hello World
程序,想要使用链接器ld
进行链接,需要执行下面指令:
ruby
zhangyuanhao@zhangyuanhao-VirtualBox:~/Desktop/CPlusStudy/gcc$ ld -static /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/11/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/11 -L/lib hello.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-linux-gnu/11/crtend.o /lib/x86_64-linux-gnu/crtn.o
zhangyuanhao@zhangyuanhao-VirtualBox:~/Desktop/CPlusStudy/gcc$ ls
a.out hello.c hello.o hello.s
zhangyuanhao@zhangyuanhao-VirtualBox:~/Desktop/CPlusStudy/gcc$ ./a.out
hello world
不得不说这是一个复杂的过程,其中的参赛非常多,看到最后的Hello World
打印我相信又是另一种感受,会觉得整个编译过程非常不简单。
同样的,关于这个复杂的链接过程,也是放到后面文章仔细探讨。
总结
虽然本篇文章只展示了一个Hello World
,但是从过程我们可以知道这其中涉及了很多知识点,后面会对每一步都尽量做到了解每一个细节,真正理解代码的执行。