C 语言从源码生成指令序列(二进制执行文件)经过以下过程:预处理 --> 编译 --> 汇编 --> 链接 --> 执行

一、预处理

预处理就是复制粘贴过程,去掉无需编译的代码,包括注释,行尾的 \ 链接处理,条件编译的#ifdef、#ifnde 语句。

二、编译

编译的过程有一下几个阶段:词法分析,语法分析,语义分析,中间代码生成,优化,目标代码分析

  1. 词法分析:记录关键、常数、字符串、运算符、大括号……并记录位置。
  2. 语法分析:反应程序层次结构(树状结构),报告语法错误(漏分号)。
  3. 语义分析:算数类型转换,报告未定义引用,运算符类型操作不匹配。
  4. 静态程序分析:不运行程序情况下分析语法错误。
  5. 中间代码生成:中间代码(IR 状态机)是面向编译场景的指令集(理解成状态机)。
  6. 优化:优化的目的是为了状态少(变量少),激励事件少(语句少),尽可能简化。典型例子就是 volatile 关键字的应用。
  7. 目标代码生成:将中间代码(IR 状态机)翻译成有处理器的 ISA 状态机。

三、汇编

根据指令集手册,把编译后的汇编代码(指令的符号化表示)翻译成二进制目标文件(指令的编码表示)、二进制文件(.o 文件)不能用文本编辑器打开。

四、链接

裸机平台:将众多的 .o 文件通过链接脚本,链接到一个指定的内存地址,最后生成 .elf 文件,通过 .elf 文件中描述的数据布局规则生成 .bin 文件,有了我们期望的数据布局之后,程序就会访问正确地址里面的数据。

操作系统平台:将众多的 .o 文件通过链接脚本,链接到一个指定的内存地址,最后生成可执行文件。

链接过程中,程序被划分为多个段(sections),每个段存储不同类型的数据或指令。以下是常见的段:

  • .text 段存储程序的代码指令,包括函数实现、程序逻辑、跳转指令等内容。它的特点是只读且可执行,程序运行时,CPU 会从 .text 段读取这些指令并执行。函数的实现代码都会存储在 .text 段中。
  • .data 段用于存储已初始化的全局变量和静态变量,这些变量在程序启动时就已经有明确的初始值。该段是可读写的,程序运行时可以对其中的变量值进行修改。
  • .bss 段存储未初始化的全局变量和静态变量,或者显式初始化为零的变量。该段不会占用可执行文件的空间,但在程序运行时会为这些变量分配内存,并自动初始化为零。
  • .rodata 段用于存储只读数据,如常量和字符串字面量,包括由 const 修饰的全局变量或静态变量。该段的内容是只读的,程序运行时无法修改。

以下是一个包含各个段数据的简单示例,包括 .text.data.bss.rodata 段。

源代码文件:main.c

#include <stdio.h>

// .data 段:已初始化的全局变量
int global_initialized = 42;

// .bss 段:未初始化的全局变量
int global_uninitialized;

// .rodata 段:只读的全局常量
const char* greeting = "Hello, world!";

// .text 段:函数代码
int main() {
    printf("%s\n", greeting);  // 输出存储在 .rodata 的字符串
    printf("Global initialized: %d\n", global_initialized); // .data 段数据
    printf("Global uninitialized: %d\n", global_uninitialized); // .bss 段数据
    example_function();  // 调用函数,使用栈段
    return 0;
}

链接脚本文件:linker.ld

MEMORY {
    DDR (rwx) : ORIGIN = 0x30000000, LENGTH = 256M  /* DDR 内存区域 */
}

SECTIONS {
    /* .text 段:代码段 */
    .text : {
        *(.text)           /* 代码段 */
    } > DDR = 0x30000000   /* 将 .text 段加载到 DDR 地址 0x30000000 */

    /* .data 段:已初始化的全局变量 */
    .data : {
        *(.data)           /* 已初始化的全局变量 */
    } > DDR = 0x30001000   /* 将 .data 段加载到 DDR 地址 0x30001000 */

    /* .bss 段:未初始化的全局变量 */
    .bss : {
        *(.bss)            /* 未初始化的全局变量 */
    } > DDR = 0x30002000   /* 将 .bss 段加载到 DDR 地址 0x30002000 */

    /* .rodata 段:只读常量数据 */
    .rodata : {
        *(.rodata)         /* 只读数据 */
    } > DDR = 0x30003000   /* 将 .rodata 段加载到 DDR 地址 0x30003000 */
}

链接生成 ELF 文件 main.elf,查看 ELF 文件段信息: readelf -S main.elf

Section Headers:
  [Nr] Name              Type            Address          Offset
  [ 0] .text             PROGBITS        0x30000000       0x00000000
  [ 1] .data             PROGBITS        0x30001000       0x00001000
  [ 2] .bss              NOBITS          0x30002000       0x00002000
  [ 3] .rodata           PROGBITS        0x30003000       0x00003000
  ...

五、执行

裸机平台:裸机设备中,将 .bin 文件烧录到 Flash/SD 等存储设备的指定地址后运行程序。

操作系统平台:可执行文件可以直接在操作系统(如 Linux)上运行的最终程序。

六、不同类型文件的区别

文件类型

是否可执行

用途

生成阶段

.o 文件

编译后未链接的目标文件,需要与其他文件链接后生成可执行文件。 中间产物,供链接使用。

编译阶段

.elf 文件

取决于类型

包含完整程序逻辑的汇编代码,并且带有符号信息和段信息(适合调试和分析)。主要用于调试、烧录、生成 .bin 文件。

链接阶段

.bin 文件

纯机器码的二进制文件, 不包含 .elf 的头部、符号表等元数据。 通常需要特定硬件或引导程序加载运行。用于烧录到硬件运行。

从 .elf 文件转换而来,将 .elf 文件转换为 .bin 文件时,通常会去掉 .elf 文件的头部、符号信息等,仅保留需要加载到内存中的数据和代码。主要过程是将 .elf 文件中的 .text(代码段)、.data(数据段)、.bss(未初始化数据段)等节提取出来,并按照它们在内存中的布局打包成一个纯粹的二进制文件

可执行文件

已完成链接,包含入口点,操作系统可以直接加载运行。

链接后直接生成或加工自 .elf 文件

七、裸机芯片启动并运行

CPU 启动过程

芯片出厂的时候内部会有一个 Boot ROM,Boot ROM 里面有芯片出厂自带的程序,用于最初阶段的程序引导,具体如下。

上电之前,我们会设置芯片的启动模式,也就是将 BOOT MODE1、BOOT MODE0 等芯片启动相关的引脚进行拉高或者拉低操作。

上电之后,根据我们设置芯片引脚的电平,Boot ROM 的代码会在片上 SRAM 中运行,初始化一些最基本的外设和时钟保证芯片运行,例如,初始化能让 CPU 运行最基本的时钟。CPU 运行之后,这段程序会首先将 .bin 文件的头部信息(IVT、Boot Data、DCD)读取到片上 SRAM 中,并解析头部信息,这段头部信息包含了用户代码存放的指定地址、一些外设寄存器的初始化配置等,例如,DCD 中包含 DDR 寄存器的初始化配置,在初始化完 DDR 之后,会将 .bin 文件的内容从外部存储器(SD 卡)搬运到 DDR 的指定地址中,后续开始执行用户自定义的程序。

CPU 运行过程

取指:以运行内存是 DDR 为例,将 DDR 中存储 .bin 文件中的二进制数字,按照指定长度读取到特殊寄存器——指令加载寄存器。

译码:内部电路读取并解释指令寄存器中的位序列,也就是对取出的二进制数字,按照指定架构的指令集进行译码。

执行:按照译码的结果执行对应的运算操作,我们写的汇编代码,就是控制 CPU 的执行阶段。在 CPU 执行的时候,所有的运算操作都需要一个中间介质——通用寄存器(R0、R1、R2 等),我们需要先将进行运算的数值存入到寄存器中,然后才可以进行运算。

递增程序计数器:执行完程序后,特殊寄存器——地址寄存器会只想 CPU 执行下一个指令的位置,用于下一次取指。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐