C/C++编译流程

前言

在平时写代码时,我们大多数时间都是直接使用集成开发环境,比如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,但是从过程我们可以知道这其中涉及了很多知识点,后面会对每一步都尽量做到了解每一个细节,真正理解代码的执行。

相关推荐
lxyzcm11 分钟前
C++23新特性解析:[[assume]]属性
java·c++·spring boot·c++23
蜀黍@猿30 分钟前
C/C++基础错题归纳
c++
雨中rain44 分钟前
Linux -- 从抢票逻辑理解线程互斥
linux·运维·c++
ALISHENGYA2 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战项目二)
数据结构·c++·算法
arong_xu2 小时前
现代C++锁介绍
c++·多线程·mutex
汤姆和杰瑞在瑞士吃糯米粑粑2 小时前
【C++学习篇】AVL树
开发语言·c++·学习
DARLING Zero two♡2 小时前
【优选算法】Pointer-Slice:双指针的算法切片(下)
java·数据结构·c++·算法·leetcode
CodeClimb2 小时前
【华为OD-E卷-木板 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
奶香臭豆腐3 小时前
C++ —— 模板类具体化
开发语言·c++·学习
不想当程序猿_3 小时前
【蓝桥杯每日一题】分糖果——DFS
c++·算法·蓝桥杯·深度优先