程序编译过程

1 概述

C 程序的一般编译过程。下面是对每个阶段的简要说明:

  1. 源代码:您编写的 C 代码文件就是源代码。它包含了程序的逻辑和功能。

  2. 编译(Compilation):在编译阶段,编译器(如 GCC)将源代码转换为汇编语言(Assembly Language)的形式。它将源代码翻译成机器可以理解的低级指令。

  3. 汇编(Assembly):在汇编阶段,汇编器(如 GNU Assembler)将汇编语言代码转换为机器语言的目标代码。目标代码是由机器指令组成的二进制文件。

  4. 链接(Linking):在链接阶段,链接器(如 GNU ld)将目标代码与其他代码(如库文件)合并在一起。链接器解决了函数、变量引用和符号解析等问题,生成最终的可执行文件。

  5. 可执行文件(Executable):生成的可执行文件是一个二进制文件,它包含了程序的完整机器代码,可以直接在操作系统上运行。

  6. 装载(Loading):在装载阶段,操作系统将可执行文件加载到内存中,并为程序分配所需的资源(如内存空间)。

  7. 执行(Execution):在执行阶段,处理器按照程序中的指令顺序执行机器代码,从而运行程序并产生相应的结果。

总结起来,C程序的编译过程涉及源代码的编译、汇编和链接,生成最终的可执行文件。该可执行文件在装载后被操作系统执行,程序按照指令顺序执行,完成所需的功能。

 

2 编译阶段(生成.s文件)

  2.1 在编译阶段编译器对代码做了哪些处理?

在编译阶段,编译器会对代码进行处理,将你写的源代码转换为机器可以理解的形式。以下是编译器在编译阶段的主要处理步骤,用通俗易懂的方式解释:

  1. 预处理(Preprocessing):在编译的第一个阶段,编译器会对源代码进行预处理。预处理器会处理以#开头的预处理指令,例如包含其他文件、宏定义、条件编译等。预处理器会根据这些指令修改源代码,生成一个经过预处理的代码文件,这个文件会作为后续编译的输入。

  2. 分解代码:编译器会将源代码拆分成一系列的词法单元(编译器会将你写的代码分解成小块),比如变量、函数、标识符、关键字、运算符等等,就像把一段话拆分成独立的单词。通过识别这些词法单元,编译器可以更好地理解代码的语法规则,例如函数调用、条件语句、循环结构等。

  3. 检查语法:编译器会检查你的代码是否符合语法规则,就像语法检查器检查一段中文是否符合语法规范。它会确保你没有写错括号、分号等基本语法错误。

  4. 确定意义:编译器会检查你的代码是否有意义,就像一个语言翻译员会确认你的句子是否有意义。它会检查你是否使用了正确的变量、函数,以及它们是否被正确声明和使用。

  5. 生成中间代码:编译器会生成一种中间形式的代码,这种代码类似于一个计算器的运算步骤。中间代码是一种更接近机器代码的抽象形式,但并不是最终的可执行代码。

  6. 优化代码:编译器会尽可能地优化你的代码,使其更加高效。就像你用心思考如何更快地解决一个问题一样,编译器会尝试使用更聪明的方法来改进代码的性能。

  7. 生成目标代码(生成.s文件):最后,编译器将生成最终的机器代码,这是计算机可以直接执行的指令。这些指令是根据你的源代码和优化步骤生成的,它们会告诉计算机如何执行你的程序。

所以,编译阶段就是将你写的源代码翻译成机器可以理解和执行的代码的过程。编译器会确保代码是合法的、有意义的,并尽可能地优化代码以提高性能。最终,生成的目标代码可以在计算机上运行,并实现你的程序的功能。

2.2 预编译是在什么时候发生的,都做了些什么

预编译是在编译的早期阶段发生的,它是编译过程中的第一个阶段。预编译器在实际编译之前,对源代码进行预处理,主要完成以下几个任务:

  1. 文件包含:预编译器处理#include指令,将包含的文件内容插入到源文件中。这样可以将多个源文件合并成一个单一的文件,方便后续的编译处理。

  2. 宏替换:预编译器处理#define指令,将定义的宏进行替换。宏可以是简单的文本替换,也可以包含参数和逻辑操作。预编译器会将宏在代码中的引用处,替换为对应的宏定义内容。

  3. 条件编译:预编译器处理#if#ifdef#ifndef等条件编译指令,根据条件判断是否保留或忽略对应的代码块。这样可以根据不同的条件编译选项,选择性地编译不同的代码。

  4. 注释删除:预编译器会删除源代码中的注释,包括单行注释(//)和多行注释(/* */)。注释对于编译器来说是无关紧要的,因此在预编译阶段可以将其删除,减少后续处理的复杂性。

  5. 其他处理:预编译器还可能处理一些其他的预处理指令,如#pragma等。这些指令提供了一些编译器特定的功能或配置选项。

预编译的目的是为了准备代码供后续的编译处理使用。通过文件包含、宏替换和条件编译,预编译器可以将源代码整合在一起,展开宏定义,根据条件选择性地编译代码。预编译可以使代码更具可维护性和可重用性,并提供了一些灵活性和配置选项。预编译阶段处理后的代码通常会生成一个中间文件,这个文件会作为后续编译的输入。

3 汇编阶段

3.1 汇编阶段都做了什么(生成.o文件)

在编译过程中的汇编阶段,主要完成以下几个任务:

  1. 汇编器(Assembler)将汇编文件(.s文件)作为输入,将汇编语言的代码转换为机器指令的二进制表示形式。

  2. 汇编器会对汇编代码中的每一条汇编指令进行解析,并生成对应的机器指令。它将汇编指令中的助记符(mnemonic)和操作数(operands)转换为具体的二进制表示。

  3. 汇编器还会处理符号和标签,将它们解析为对应的内存地址或偏移量。符号和标签通常用于表示变量、函数、跳转目标等。汇编器会将这些符号和标签替换为相应的内存地址或偏移量的值。

  4. 汇编器还会进行一些额外的处理,例如为指令生成适当的操作码、操作数验证和错误检查等。

  5. 最终,汇编器会生成一个机器代码文件,其中包含了计算机可以直接执行的机器指令的二进制表示形式。

换句话说,汇编阶段将汇编语言的代码转换为机器指令的二进制表示形式,使得计算机能够直接执行这些指令。这是将中间代码转换为最终可执行代码的最后一步。生成的机器代码文件可以被操作系统加载并执行,实现我们在源代码中定义的功能。

4 链接阶段

4.1 链接阶段都做了什么

在链接阶段,主要完成以下几个任务:

  1. 符号解析与重定位:链接器(Linker)会解析目标文件(编译后生成的目标代码文件或库文件)中的符号引用。它会寻找符号的定义,并将引用与定义进行关联。如果有多个目标文件中存在相同的符号定义,链接器会解决符号冲突问题。类比为在组装过程中,找到并连接零件的对应位置。

  2. 符号表生成:链接器会生成一个符号表,记录所有符号的名称、地址和其他属性。这个符号表将用于后续的符号解析和重定位。类比为制作一个清单,记录所有零件的名称和位置。

  3. 代码合并:链接器会将多个目标文件中的代码段合并成一个单一的可执行文件。它会根据符号表将相应的代码段组合在一起,确保正确的代码执行顺序。类比为将多个零件组合在一起,形成完整的机器。

  4. 重定位:链接器会对目标文件中的地址进行重定位。由于每个目标文件的地址空间可能不同,链接器需要调整代码和数据的地址,以使其适应最终的加载地址。类比为将机器上的组件位置调整到正确的位置,以确保一切正常工作。类比为将机器上的组件位置调整到正确的位置,以确保一切正常工作。

  5. 库文件链接:链接器还会将库文件与目标文件进行链接,解析库文件中的符号引用并将其与符号定义关联起来。静态库会被链接到可执行文件中,而动态库会在运行时进行动态链接。类比为将其他专门功能的设备(如打印机)与机器连接起来,使其能够一起工作。

  6. 符号解析优化:链接器可能会进行一些符号解析的优化,例如未使用符号的剔除、重复符号的消除等,以减小可执行文件的大小和提高执行效率。类比为对机器进行一些调整和优化,使其更加高效。

  7. 生成可执行文件或共享库:最终,链接器将完成链接过程,并生成一个可执行文件或共享库。这个文件包含了链接后的代码和数据,可以直接在操作系统上加载和执行。类比为组装完成的机器可以正常运行了

链接阶段是将多个目标文件和库文件组合起来,解决符号引用和重定位问题,最终生成可执行文件或共享库的过程。它是将编译产生的中间代码转换为最终可执行代码的最后一步。

 

4.2 链接阶段生成了哪些文件

在链接阶段,主要生成以下两种文件:

  1. 可执行文件(Executable File):链接器将多个目标文件(包括编译后的目标代码文件(汇编文件.s)和库文件)合并并进行符号解析和重定位后,生成一个可执行文件。这个可执行文件通常具有特定的格式,如ELF(Executable and Linkable Format)或PE(Portable Executable)等。可执行文件包含了链接后的机器代码和数据,可以直接在操作系统上加载和执行。

  2. 共享库(Shared Library):链接器也可以生成一个共享库文件,也称为动态链接库(Dynamic Link Library,DLL)或共享对象文件(Shared Object File,SO)。共享库包含了链接后的代码和数据,可以被多个可执行文件共享使用。在运行时,操作系统会将共享库加载到内存中,并将其中的函数和数据供多个程序使用。共享库可以提供模块化和可复用的功能,减少可执行文件的体积。

这两种文件都是链接阶段的输出,可执行文件用于直接运行程序,而共享库用于提供可共享的功能。它们是将编译和汇编生成的目标文件组合起来,完成最终的链接过程所生成的文件。

4.3 共享库如何生成

生成共享库(Shared Library)通常包括以下步骤:

  1. 编写源代码:首先,编写包含所需功能的源代码文件。这些源代码文件可以包括函数定义、类定义以及相关的头文件。

  2. 编译为目标文件(.o文件):使用编译器将源代码文件编译为目标文件(Object File)。目标文件是编译器根据源代码生成的二进制文件,包含了编译后的机器代码和相关的符号信息。

 

gcc -c myfile.c -o myfile.o

上述命令将 myfile.c 编译为 myfile.o 目标文件。

创建共享库:使用链接器将多个目标文件链接为一个共享库文件。链接器会解析目标文件中的符号引用,并将其与其他目标文件中的符号定义关联起来。

 

gcc -shared -o mylibrary.so myfile1.o myfile2.o

上述命令将 myfile1.omyfile2.o 链接为一个共享库文件 mylibrary.so-shared 参数指定生成共享库。

安装共享库:将生成的共享库文件安装到系统中,以便其他程序可以使用它。安装共享库通常涉及将库文件复制到系统目录,以及更新系统的库缓存。

 

sudo cp mylibrary.so /usr/lib/ sudo ldconfig

  1. 上述命令将 mylibrary.so 复制到 /usr/lib/ 目录,并通过 ldconfig 更新库缓存。

完成上述步骤后,生成的共享库文件就可以在其他程序中使用了。其他程序可以使用动态链接的方式加载共享库,并调用其中的函数和使用其中的功能。共享库提供了一种模块化和可复用的方式,使得多个程序可以共享和重用其中的代码和功能。

5 装载阶段

5.1 在装载阶段,操作系统都做了什么

在装载(Loading)阶段,操作系统主要执行以下任务:

  1. 内存分配:操作系统为程序分配内存空间,以便加载程序的指令和数据。这涉及将程序的代码段、数据段、堆、栈等部分分配到合适的内存地址。

  2. 符号解析:操作系统将程序中引用的符号(如函数、变量)与其在内存中的实际位置进行关联。这包括解析程序的重定位表,将符号引用替换为实际地址,以便程序能够正确地访问和执行。

  3. 段重定位:如果程序的代码、数据或其他段在内存中的位置与其在编译时分配的位置不一致,操作系统会执行段重定位操作,调整这些段的地址,使其正确映射到实际的内存位置。

  4. 动态链接:如果程序依赖于共享库(Shared Library),操作系统会进行动态链接,将共享库加载到内存中,并将程序中对共享库的符号引用解析为库中的实际地址。这允许多个程序共享一个库的实例,减少内存占用并提高代码的重用性。

  5. 权限设置:操作系统会为加载的程序设置适当的内存权限,以确保程序只能在分配的内存空间内进行访问和执行,保护系统的安全性和稳定性。

  6. 启动程序:最后,操作系统会将控制权转移给加载的程序的入口点,使其开始执行。这意味着程序的指令和数据现在已经在内存中,并准备就绪,可以被处理器执行。

通过这些操作,操作系统能够将程序从存储介质(如硬盘)加载到内存中,并使其能够正确、安全地执行。操作系统的装载阶段是为了满足程序的运行需求,将程序的指令和数据加载到内存中,并进行必要的地址解析和权限设置,以便程序能够顺利执行。

6 执行阶段

6.1 在执行阶段,处理器做了什么

在执行阶段,处理器(CPU)主要执行以下任务:

  1. 取指令(Instruction Fetch):处理器从内存中获取下一条指令的内容。它根据程序计数器(Program Counter,PC)中存储的地址,访问内存并将指令加载到指令寄存器(Instruction Register,IR)中。

  2. 解码指令(Instruction Decode):处理器解码指令寄存器中的指令内容,确定指令的类型和操作数。它识别指令中的操作码,并根据操作码决定下一步的操作。

  3. 执行指令(Instruction Execution):处理器执行指令中指定的操作。根据指令类型,它可能执行算术或逻辑运算、数据传输、条件分支、函数调用等操作。

  4. 存储访问(Memory Access):如果指令需要读取或写入内存中的数据,处理器会执行相应的存储访问操作。它将地址和数据发送到内存控制器,从或向内存中读取或写入数据。

  5. 状态更新(Status Update):处理器根据指令的结果更新相关的状态信息。这可能包括更新标志寄存器、程序计数器等。状态更新的目的是记录当前指令的执行情况,为下一条指令的执行提供必要的信息。

  6. 控制转移(Control Transfer):根据条件判断或跳转指令的执行结果,处理器可能会改变程序计数器的值,将控制权转移到不同的指令地址。这使得程序可以按照特定的逻辑流程执行不同的指令序列。

  7. 中断处理(Interrupt Handling):在执行过程中,如果发生硬件中断或软件中断(如系统调用),处理器会中断当前的执行流程,保存当前的上下文,并转到中断处理程序进行处理。中断处理完成后,处理器恢复之前的执行状态,并继续执行被中断的指令。

处理器在执行阶段负责解析和执行指令,访问内存中的数据,更新状态信息,并根据控制流程执行不同的操作。它是计算机的核心组件之一,负责执行程序的实际计算和操作。处理器的工作原理涉及多个流水线阶段和执行单元,以高效地完成指令的执行任务。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值