🎊如果大家想粗略了解整个编译过程,那就只需要看红色部分,其他部分为对应红色部分的扩展。对于文章中出现的问题,欢迎大家积极在评论区或私信提出来。引用说明放在最后
一、关键步骤
将单片机的源代码转换成二进制文件(bin文件)的过程涉及几个关键步骤,这些步骤是编译过程中的标准组成部分,主要包括以下步骤:预处理、编译、汇编、链接、二进制转换。
二、详细过程介绍
假设有如下几个文件
config.h
#ifndef CONFIG_H
#define CONFIG_H
// 配置宏定义
#define DEBUG_MODE
#endif // CONFIG_H
type.h
#ifndef TYPES_H
#define TYPES_H
// 类型定义
typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
#endif // TYPES_H
function.c
#include "types.h"
void function1(void) {
uint8_t data = 0;
// 功能实现
}
main.c
#include "config.h"
#include "types.h"
#include "stdio.h"
extern void function1(void);
int main(void) {
#ifdef DEBUG_MODE
printf("Debug mode is enabled.\n");
#endif
function1();
return 0;
}
2.1 预处理
这是编译过程的第一步。预处理器采取预处理指令,处理诸如宏定义(#define)、条件编译(#ifdef, #ifndef, #endif)等。此步骤的输出是扩展的源代码,包含所有头文件和宏展开的结果。
main.c
经过预处理后main.i
//#include "config.h"
//#include "types.h"
typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
//#include "stdio.h"
int printf(const char *format, ...);
//stdio.h还有很多,不一一列出
extern void function1(void);
int main(void) {
//#ifdef DEBUG_MODE
printf("Debug mode is enabled.\n");
//#endif
function1();
return 0;
}
整理一下就变成这样,可以发现,所有预处理的东西
typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
int printf(const char *format, ...);
extern void function1(void);
int main(void) {
printf("Debug mode is enabled.\n");
function1();
return 0;
}
2.2 编译
在这一步,编译器将预处理后的源代码转换为汇编代码。编译器首先进行词法分析、语法分析和语义分析,生成抽象语法树(AST),然后将AST转换为目标机器的汇编指令。这一步是编译过程中最复杂的部分,它包括优化代码以提高效率。
- 词法分析:词法分析是编译过程的第一阶段,编译器读取源代码并将其分解成一系列的记号(Tokens)或词素。记号是源代码中的最小语法单位,如关键字、标识符、常量、运算符等。
- 语法分析:语法分析阶段关注源代码的结构和语法规则,确保代码符合语言的语法。编译器使用解析器(Parser)来处理记号序列,并构建出一个抽象语法树(AST)。AST 是源代码结构的树状表示,其中节点表示程序中的构造,如变量声明、循环、条件语句等。
- 语义分析:语义分析阶段检查AST中的节点是否符合编程语言的语义规则。这个阶段涉及类型检查、变量作用域和生命周期的确定、函数调用的合法性等。语义分析器会使用符号表(Symbol Table)来跟踪变量和函数的声明和定义。
- 中间表示:在AST生成之后,编译器会将其转换为中间表示形式。IR是一种低级别的编程语言,它抽象了目标机器的细节,同时保留了源代码的结构和操作。IR使得编译器可以在不同的优化阶段之前之后进行跨语言的转换。
- 优化:优化阶段的目标是改进程序的性能、大小或可靠性,同时不改变程序的语义。编译器使用多种优化技术,如死代码消除、循环展开、常量传播、指令调度等。优化可以在不同的级别上进行,包括IR级别和目标代码级别。
- 代码生成:代码生成阶段将优化后的中间表示转换为目标机器的汇编指令。这个阶段涉及寄存器分配、指令选择和指令调度。代码生成器必须考虑目标机器的架构特性,如指令集、寄存器数量和内存访问模式。
- 汇编代码输出:最终,编译器将生成的汇编代码输出到一个汇编语言文件中。这个文件包含了目标机器的汇编指令和伪指令,可以被汇编器进一步处理。
2.3 汇编
汇编器将编译步骤产生的汇编代码转换为机器代码。每一条汇编指令都对应一条或多条机器指令。汇编结果是一系列的机器指令,通常被存储在一个目标文件(如.o格式)中。
- 汇编器的主要任务是将汇编语言代码转换为机器代码。汇编语言是一种低级编程语言,它使用助记符来代表机器指令的操作码。
- 目标文件不仅包含机器代码,还包含其他信息,如符号表(用于链接过程中的符号解析)、重定位信息(用于在不同目标文件之间解决地址引用)和调试信息(用于程序调试)。
- 目标文件的格式可能因编译器和平台而异。例如,在 Unix-like 系统中,目标文件通常是 ELF(Executable and Linkable Format)或 COFF(Common Object File Format)格式。在 Windows 系统上,目标文件可能是 COFF 格式,而在嵌入式系统中,可能会使用其他格式,如 DWARF。
目标文件(.o文件)是编译过程中的一个中间产物,它包含了编译后的机器代码和链接过程中需要的信息,如符号表、重定位信息等。不同的操作系统和编译器可能使用不同的目标文件格式,但它们都是为了能够在链接阶段将多个目标文件合并成最终的可执行文件或库文件。
在Linux系统中,目标文件通常遵循ELF(Executable and Linkable Format)格式。ELF格式的目标文件可以是可重定位的,这意味着它们可以与其他目标文件或库文件链接。ELF格式也用于可执行文件和共享库(如.so文件)。ELF文件包含了多个段(sections),每个段都有特定的用途,例如:
- .text:存放编译后的机器指令。
- .data:存放初始化的全局变量和静态变量。
- .bss:存放未初始化的全局变量和静态变量,它们在程序启动时被自动初始化为零。
- .rodata:存放只读数据,如字符串常量。
- .symtab:符号表,包含了目标文件中定义和引用的所有符号(如函数和变量名)。
- .rel.text 和 .rel.data:重定位表,包含了需要在链接过程中解决的地址引用信息。
目标文件的格式和内容是为了支持编译、链接和后续的装载执行过程。在链接阶段,链接器会处理这些段,将它们合并成最终的可执行文件或库文件。例如,.text 段中的代码会被复制到可执行文件的代码段中,.data 段中的数据会被复制到数据段中,而符号表和重定位信息则用于解析不同目标文件之间的符号引用。
在Windows系统中,目标文件通常遵循COFF(Common Object File Format)或其变种PE(Portable Executable)格式。这些格式与ELF类似,也包含了用于链接和执行所需的各种信息。
2.4 链接
链接器的作用是将一个或多个目标文件合并成一个单一的可执行文件。在这一步中,还会解决外部引用,即将所有模块对外部函数和变量的引用与其定义关联起来。如果你的单片机项目中包含了多个源文件或者使用了外部库,链接器会将它们与你的主程序合并为一个完整的程序。
- 合并目标文件:链接器将多个目标文件中的代码和数据段合并到一个单一的文件中,这些目标文件可能来自于项目的多个源文件,或者是第三方库文件。
- 解析外部引用:每个目标文件可能包含对其他目标文件中定义的函数或变量的引用。链接器的任务是确保每个引用都被解析到正确的地址。如果一个函数或变量在其他目标文件中定义,链接器会更新引用,使其指向正确的内存位置。
- 处理符号:符号表包含了目标文件中定义的所有全局和静态变量、函数的名称和地址。链接器使用符号表来匹配每个引用的符号和它们的定义。
- 重定位:重定位是链接过程中的一个关键步骤,它涉及到修改代码和数据中的地址,以确保它们指向正确的位置。这包括对函数调用、全局变量访问和其他符号引用的地址进行更新。
- 处理库:链接器还会处理静态库(如 .a 文件)和动态库(如 .so 文件)。静态库在链接时被整合到最终的可执行文件中,而动态库则在程序运行时被加载。
- 生成可执行文件:链接器将所有合并后的代码、数据和必要的元信息(如程序入口点)打包成一个可执行文件。
- 处理特殊段:链接器还会处理特殊的段,如 .bss 段(未初始化的数据),这些段在目标文件中可能不包含实际的数据,但链接器会在可执行文件中为它们分配空间。
- 优化:一些链接器还提供优化功能,例如,删除未使用的代码和数据(称为死代码消除)。
- 生成调试信息:如果在编译时启用了调试选项,链接器会保留调试信息,使得开发者可以使用调试器来调试程序。
- 生成可执行文件:链接器将所有合并后的代码、数据和必要的元信息(如程序入口点)打包成一个可执行文件。在 Unix-like 系统中,这通常是 ELF 格式的文件。在常见嵌入式系统中,BIN、HEX、AXF文件格式更加常见。
2.5 二进制转换
最后,根据需要,可将链接器输出的可执行文件转换为纯二进制格式(bin文件)。这是一个可选步骤,具体取决于目标系统和开发环境。某些开发环境在链接阶段直接生成bin文件,而其他环境可能会生成hex文件或其他格式的文件,需要使用专门的工具将这些文件转换为bin格式。
三、引用
红色部分引用自:一招掌握C语言代码如何变成bin文件?