C 02_C语言编译过程

一、C语言编译详解

1.1 可执行程序(Executable Program)

我们平时所说的程序是指鼠标双击后或在命令行终端输入程序命令就可以直接运行的程序,这样的程序被称为可执行程序(Executable Program)

  • 在 Windows 系统下,可执行程序的后缀有.exe和.com(其中.exe比较常见);
  • 在类 UNIX 系统(Linux、Mac OS 等)下,可执行程序没有特定的后缀,系统根据文件的头部信息来判断是否是可执行程序。

可执行程序 的内部是一系列计算机指令(机器代码)和数据的集合,它们都是以二进制形式进行存储的,CPU 可以直接识别,毫无障碍;但是对于程序员,它们非常晦涩,难以记忆和使用。

在计算机发展的初期,程序员就是使用这样的二进制指令来编写程序的,那个年代还没有高级编程语言。

使用二进制指令编程对程序员来说简直是噩梦,尤其是当程序比较大的时候,不但编写麻烦,需要频繁查询指令手册,而且出错会异常苦恼,要直接面对一堆二进制数据,让人眼花缭乱。另外,用二进制指令编程步骤繁琐,要考虑各种边界情况和底层问题,开发效率十分低下。

1.2 程序编译(Compile)简介

随着计算机的发展,计算机科学家们开发出了各种各样的高级编程语言来提高自己的生产力(编程效率),例如汇编、C语言、C++、Java、Python、Go语言等,都是在逐步提高开发效率。

计算机编程人员首先使用高级编程语言(如:C语言、C++、Golang等)编写出程序源代码,这些程序源代码是由固定的词汇和符号按照固定的格式组织起来的一条条语句和语句块组成的功能逻辑单元,语句简单直观,程序员容易识别和理解,但是对于CPU,C语言代码就是天书,根本不认识,CPU只认识几百个二进制形式的指令。这就需要一个工具,将C语言代码转换成CPU能够识别的二进制指令(机器代码),也就是将代码加工成 可执行程序 的格式。这个转换过程称为 编译(Compile)。使用的转换工具是一个特殊的软件,叫做编译器(Compiler)

编译器 能够识别代码中的词汇、句子以及各种特定的格式,并将它们转换成计算机能够识别的二进制形式代码,这个过程称为 编译(Compile)

C 语言的编译(Compile)过程包括四个步骤:

  • 预处理
  • 编译
  • 汇编
  • 连接

C程序编译的完整过程: alt 属性文本

C语言的编译器有很多种,不同的平台下有不同的编译器,例如:

  • Windows 下常用的是微软开发的 Visual C++,它被集成在 Visual Studio 中,一般不单独使用;
  • Linux 下常用的是 GUN 组织开发的 GCC,很多 Linux 发行版都自带 GCC;
  • Mac 下常用的是 LLVM/Clang,它被集成在 Xcode 中(Xcode 以前集成的是 GCC,后来由于 GCC 的不配合才改为 LLVM/Clang,LLVM/Clang 的性能比 GCC 更加强大)。

代码语法正确与否,编译器说了才算,学习C语言,从某种意义上说就是学习如何使用编译器。 编译器可以 100% 保证你的代码从语法上讲是正确的,因为哪怕有一点小小的错误,编译也不能通过,编译器会告诉你哪里错了,便于你的更改。

1.3 预处理(Preprocessing) .c->.i

预处理 又称 预编译 ,该步骤是做一些代码文本的替换工作,即在编译前的一些预处理工作。 编译过程的第一步预就是 预处理,这一步由 预处理器(Preprocessor) 完成,预处理结束后会产生一个后缀为(.i)的临时文件(中间文件)。

预处理器主要完成以下任务

  • 文件包含(#include 语句)
  • 宏扩展(#define 语句)
  • 条件编译(#if、#elif、#ifdef、#ifndef等)
  • #pragma预处理指令
  • 删除所有的注释

预处理器(Preprocessor)会在编译过程中删除所有注释,因为注释不属于程序代码,它们对程序的运行没有特别作用。

文件包含:C语言中的文件包含是在预处理期间将另一个包含一些预写代码的文件添加到我们的C程序中。它是使用 #include 指令完成的。在预处理期间包含文件会导致在源代码中添加文件名的全部内容,从而替换 #include<文件名> 指令,创建新的中间文件。

宏扩展:宏是使用 #define 指令定义的一些常量值或表达式。宏调用会导致宏扩展。预处理器创建一个中间文件,其中一些预先编写的汇编级指令替换定义的表达式或常量(基本上是匹配的标记)。为了区分原始指令和宏扩展产生的程序集指令,在每个宏展开语句中添加了一个“+”号。

条件编译: 可以根据指定的条件选择性地编译代码。这使得开发者可以根据不同的需求编写不同的代码版本,实现跨平台和多配置的灵活性。

#pragma预处理指令: 是一种预处理指令,它的作用是设置编译器的状态或者指定编译器完成一些特定的动作。

1.4 编译(Compile).i->.s

编译器能够识别代码中的词汇、符号、语句以及各种特定的格式,并将它们转换成计算机CPU能够识别的二进制形式的指令(机器代码),这个转换的过程称为 编译(Compile)

编译也可以理解为“翻译”,将高级语言翻译计算机CPU能够识别的二进制形式的指令(机器代码),它是一个复杂的过程。

C 语言中的编译阶段使用内置编译器软件将 .i 临时文件转换为具有汇编级指令(低级代码)的 .s 汇编文件 ,这是一个复杂的过程,大致包括词法分析、语法分析、语义分析、性能优化、生成汇编指令文件五个步骤,期间涉及到复杂的算法和硬件架构。

汇编代码是一种简单的英文语言,用于编写低级指令(在微控制器程序中,我们使用汇编语言)。整个程序代码由编译器软件一次性解析(语法分析),并通过终端窗口告诉我们源代码中存在的任何语法错误或警告。

1.5 汇编(Assemble).s->.o

汇编(Assemble) 是使用汇编程序将程序汇编代码(.s 文件)转换为机器可理解的代码(二进制/十六进制形式)。汇编程序是一个预先编写的程序,它将汇编代码转换为机器代码。它从程序汇编代码文件中获取基本指令,并将其转换为特定于计算机类型(称为目标代码)的二进制/十六进制代码。

生成的文件与程序汇编代文件同名,在 DOS 中是扩展名为 .obj 的对象文件,在 UNIX 操作系统中是扩展名为 .o。

1.6 链接(Link).o -> 可执行文件

C语言代码经过编译以后,并没有生成最终的可执行文件(.exe 文件),而是生成了一种叫做目标文件(Object File)的中间文件(或者说临时文件)。目标文件也是二进制形式的,它和可执行文件的格式是一样的。对于 Visual C++,目标文件的后缀是.obj;对于 GCC,目标文件的后缀是.o。

目标文件经过 链接(Link) 以后才能变成可执行文件。

既然目标文件和可执行文件的格式是一样的,为什么还要再链接一次呢,直接作为可执行文件不行吗?

这是因为编译只是将我们自己写的代码变成了二进制形式,它还需要和系统组件(比如标准库、动态链接库等)结合起来,这些组件都是程序运行所必须的。

链接(Link) 其实就是一个“打包”的过程,它将所有二进制形式的目标文件和系统组件组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做 链接器(Linker)

随着我们学习的深入,我们编写的代码越来越多,最终需要将它们分散到多个源文件中,编译器每次只能编译一个源文件,生成一个目标文件,这个时候,链接器除了将目标文件和系统组件组合起来,还需要将编译器生成的多个目标文件组合起来。

再次强调,编译是针对一个源文件的,有多少个源文件就需要编译多少次,就会生成多少个目标文件

链接(Link) 是将库文件包含在我们的程序中的过程。库文件是一些预定义的文件,其中包含机器语言中的函数定义,这些文件的扩展名为 .lib、.a 或 .so ,这些文件中有一些未知语句写入操作系统无法理解的对象 (.o/.obj) 文件中。你可以把它理解为一本书,里面有一些你不知道的单词,你会用字典来找到这些单词的含义。同样,我们使用库文件来为对象文件中的一些未知语句赋予意义。链接过程会生成一个可执行文件,在 Windows 系统中其扩展名为 .exe (在 DOS 中为 .out),在 UNIX 操作系统中为 .out。

链接器将库文件与对象文件链接以生成可执行文件。

1.7 编译总结

C语言编译过程中程序的流程图: alt 属性文本

  • C中的编译过程也称为将人类可理解代码(C程序)转换为机器可理解代码(二进制代码)的过程。
  • C语言的编译过程包括四个步骤:预处理、编译、汇编和链接。
  • 预处理器执行删除注释、宏扩展、文件包含。这些命令在编译过程的第一步执行。
  • 编译器可以提高程序的性能,并将中间文件转换为汇编文件。
  • 汇编程序有助于将汇编文件转换为包含机器代码的对象文件。
  • 链接器用于将库文件与对象文件链接。这是编译中生成可执行文件的最后一步。

二、ELF文件

2.1 ELF文件简介

ELF 格式详解:https://blog.csdn.net/kunkliu/article/details/129648744

在计算机科学中,ELF 是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件的文件格式。全称 “Executable and Linkable Format”,即可执行可链接文件格式,目前常见的Linux、 Android可执行文件、共享库(.so)、目标文件( .o)以及Core 文件(吐核)均为此格式。

ELF 是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的,也是Linux的主要可执行文件格式。

1999年,被86open项目选为x86架构上的类Unix操作系统的二进制文件标准格式,用来取代COFF。因其可扩展性与灵活性,也可应用在其它处理器、计算机系统架构的操作系统上。

ELF文件 由4部分组成,分别是 ELF头(ELF header)程序头表(Program header table)节(Section)节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必顺序安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由 ELF头 中的各项值来决定。

程序头表(Program header table) 描述的是一个段在文件中的位置、大小以及它被放进内存后所在的位置和大小。

三、C/C++编译概述

3.1 GCC编译概述

GCC(GNU Compiler Collection)是一个广泛使用的编译器套件,支持多种编程语言,包括C、C++、Objective-C、Fortran等。GCC提供了分阶段编译的选项,允许用户将编译过程分解为多个阶段,以便进行更灵活的控制和优化。下面是一些常用的GCC分阶段编译的命令选项:

  • 预处理阶段(Preprocessing)

    • -E:只进行预处理,生成预处理后的源代码。
    • -P:只进行预处理,并将结果写入文件。
  • 编译阶段(Compilation)

    • -S:生成汇编代码(Assembly)文件。
    • -c:编译源代码为目标文件(Object File)。
  • 汇编阶段(Assembly)

    • -C:只进行汇编,生成目标文件。
    • -S:生成汇编代码文件。
  • 链接阶段(Linking)

    • -o :指定输出文件名。
    • -l :链接指定的库文件。
    • -L :添加库文件搜索路径。
    • -Wl :传递额外的选项给链接器。
    • -shared:生成共享库(Shared Library)。

可以根据需要将这些选项组合起来使用,以实现所需的分阶段编译过程。例如,以下命令将源文件 demo.c 进行预处理、编译和链接,生成可执行文件 output:

1
2
gcc -o output -c demo.c
gcc -o output demo.o

GCC的分阶段编译选项可以根据编译器的版本和所用语言而有所不同。建议查阅相应版本的GCC文档或使用 gcc –help 命令来获取更详细的选项列表和说明。GCC提供了多个选项用于实现分阶段编译,包括预处理、编译、汇编和链接等阶段。可以根据需要组合这些选项来控制编译过程。注意不同的GCC版本和语言可能会有不同的选项。

Licensed under CC BY-NC-SA 4.0