从 C语言-->二进制程序-->芯片启动运行 过程
C 语言从源码生成指令序列(二进制执行文件)经过以下过程:预处理 --> 编译 --> 汇编 --> 链接 --> 执行
一、预处理
预处理就是复制粘贴过程,去掉无需编译的代码,包括注释,行尾的 \ 链接处理,条件编译的#ifdef、#ifnde 语句。
二、编译
编译的过程有一下几个阶段:词法分析,语法分析,语义分析,中间代码生成,优化,目标代码分析
- 词法分析:记录关键、常数、字符串、运算符、大括号……并记录位置。
- 语法分析:反应程序层次结构(树状结构),报告语法错误(漏分号)。
- 语义分析:算数类型转换,报告未定义引用,运算符类型操作不匹配。
- 静态程序分析:不运行程序情况下分析语法错误。
- 中间代码生成:中间代码(IR 状态机)是面向编译场景的指令集(理解成状态机)。
- 优化:优化的目的是为了状态少(变量少),激励事件少(语句少),尽可能简化。典型例子就是 volatile 关键字的应用。
- 目标代码生成:将中间代码(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)上运行的最终程序。
六、不同类型文件的区别
|
文件类型 |
是否可执行 |
用途 |
生成阶段 |
|
|
否 |
编译后未链接的目标文件,需要与其他文件链接后生成可执行文件。 中间产物,供链接使用。 |
编译阶段 |
|
|
取决于类型 |
包含完整程序逻辑的汇编代码,并且带有符号信息和段信息(适合调试和分析)。主要用于调试、烧录、生成 .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 执行下一个指令的位置,用于下一次取指。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)