MCU编译产物BIN文件段结构解析

1. 概述

MCU(微控制器单元)的编译过程通常包括:预处理、编译、汇编、链接等步骤。最终生成的二进制文件(BIN文件)包含了程序运行所需的所有代码和数据。这些内容按照不同的属性被组织成多个"段"(Section),每个段在内存中有特定的位置和用途。

2. 编译流程简述

源代码(.c/.h) 
  → 编译(.o目标文件)
  → 链接(根据链接脚本合并段)
  → 可执行文件(.elf)
  → 格式转换(.bin/.hex)

链接器(Linker)根据链接脚本(Linker Script)将各个目标文件的段进行合并、排序和定位,最终生成可执行文件。BIN文件是ELF文件的纯二进制提取版本,包含了需要写入到Flash的所有数据。

3. 常见段类型详解

3.1 .text 段(代码段)

作用:存放程序的执行代码(机器指令)

特点

示例代码对应

int add(int a, int b) {
    return a + b;  // 这段代码会被编译成机器码存放在.text段
}

在链接脚本中的定义

.text :
{
    *(.text)        /* 所有目标文件的.text段 */
    *(.text.*)      /* 子段 */
    . = ALIGN(4);   /* 4字节对齐 */
} >FLASH            /* 存放在Flash中 */

3.2 .rodata 段(只读数据段)

作用:存放只读数据,如字符串常量、全局const变量

特点

示例代码对应

const char* message = "Hello World";  // 字符串常量存放在.rodata段
const int table[] = {1, 2, 3, 4};     // const数组存放在.rodata段

在链接脚本中的定义

.text :
{
    *(.text)
    *(.rodata)      /* 只读数据通常与代码段一起存放 */
    *(.rodata*)
} >FLASH

3.3 .data 段(已初始化数据段)

作用:存放已初始化的全局变量和静态变量

特点

示例代码对应

int global_var = 100;           // 存放在.data段
static int static_var = 200;     // 存放在.data段

内存布局说明

在链接脚本中的定义

.data :
{
    *(.data .data.*)
    . = ALIGN(4);
} >RAM AT>FLASH    /* 运行时在RAM,初始值在Flash */

启动代码需要完成的工作

// 伪代码示例
extern uint32_t _sdata;    // .data段的起始地址(RAM)
extern uint32_t _edata;    // .data段的结束地址(RAM)
extern uint32_t _sidata;   // .data段在Flash中的初始值地址

// 将.data段从Flash复制到RAM
uint32_t* src = &_sidata;
uint32_t* dst = &_sdata;
while (dst < &_edata) {
    *dst++ = *src++;
}

3.4 .bss 段(未初始化数据段)

作用:存放未初始化的全局变量和静态变量,以及初始化为0的变量

特点

示例代码对应

int uninit_var;              // 存放在.bss段
static int static_uninit;    // 存放在.bss段
int zero_var = 0;            // 也存放在.bss段(因为值为0)

在链接脚本中的定义

.bss :
{
    . = ALIGN(4);
    *(.bss*)
    *(COMMON)                 /* 未初始化的全局变量 */
    . = ALIGN(4);
} >RAM

启动代码需要完成的工作

// 伪代码示例
extern uint32_t _sbss;   // .bss段的起始地址
extern uint32_t _ebss;   // .bss段的结束地址

// 将.bss段清零
uint32_t* ptr = &_sbss;
while (ptr < &_ebss) {
    *ptr++ = 0;
}

3.5 .init 和 .fini 段(初始化和终结段)

作用

特点

在链接脚本中的定义

.init :
{
    KEEP(*(SORT_NONE(.init)))  /* KEEP确保不被优化掉 */
} >FLASH

.fini :
{
    KEEP(*(SORT_NONE(.fini)))
} >FLASH

3.6 .vector 段(中断向量表)

作用:存放中断向量表,定义了各个中断服务程序的入口地址

特点

在链接脚本中的定义

.vector :
{
    *(.vector);        /* 中断向量表 */
    . = ALIGN(64);     /* 可能需要64字节对齐 */
} >FLASH

3.7 .stack 段(栈段)

作用:定义程序栈空间

特点

在链接脚本中的定义

.stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
{
    PROVIDE(_susrstack = .);
    . = . + __stack_size;      /* 栈大小 */
    PROVIDE(_eusrstack = .);
} >RAM

3.8 .heap 段(堆段)

作用:定义动态内存分配空间(malloc/free使用)

特点

4. 段的内存布局示例

典型的MCU内存布局如下:

Flash (ROM) 布局:
┌─────────────────┐
│  .vector        │  0x00000000  中断向量表
├─────────────────┤
│  .init          │              初始化代码
├─────────────────┤
│  .text          │              程序代码
│  + .rodata      │              只读数据
├─────────────────┤
│  .data (LMA)    │              .data段的初始值(需要复制到RAM)
├─────────────────┤
│  .fini          │              终结代码
└─────────────────┘

RAM 布局:
┌─────────────────┐
│  .data (VMA)    │  0x20000000  已初始化数据(从Flash复制)
├─────────────────┤
│  .bss           │              未初始化数据(启动时清零)
├─────────────────┤
│  .heap          │              堆空间
├─────────────────┤
│  ...            │
├─────────────────┤
│  .stack         │              栈空间(从高地址向低地址增长)
└─────────────────┘

5. 其他常见段

5.1 .sdata / .sbss(小数据段)

作用:存放可以通过全局指针(GP)快速访问的小数据

5.2 .preinit_array / .init_array / .fini_array

作用:存放C++全局对象的构造函数和析构函数指针数组

执行顺序

  1. .preinit_array(最先)
  2. .init段中的代码
  3. .init_array(构造函数)
  4. main()函数
  5. .fini_array(析构函数)
  6. .fini段中的代码

5.3 .ARM.exidx / .ARM.extab

作用:ARM架构的异常处理索引表

6. 如何查看BIN文件中的段信息

6.1 使用objdump工具

# 查看ELF文件的段信息
riscv-none-embed-objdump -h firmware.elf

# 输出示例:
# Idx Name          Size      VMA       LMA       File off  Algn
#   0 .init         00000020  00000000  00000000  00001000  2**2
#   1 .text         00001234  00000020  00000020  00001020  2**2
#   2 .data         00000040  20000000  00001260  00002260  2**2

6.2 使用readelf工具

# 查看ELF文件头信息
riscv-none-embed-readelf -S firmware.elf

# 查看程序头信息
riscv-none-embed-readelf -l firmware.elf

6.3 使用size工具

# 查看各段大小
riscv-none-embed-size firmware.elf

# 输出示例:
#    text    data     bss     dec     hex filename
#    1234      64     256    1554     612 firmware.elf

6.4 使用hexdump查看BIN文件

# 以十六进制查看BIN文件内容
hexdump -C firmware.bin | head -20

7. BIN文件与ELF文件的区别

7.1 ELF文件(Executable and Linkable Format)

7.2 BIN文件(Binary Image)

转换命令

# 从ELF生成BIN文件
riscv-none-embed-objcopy -O binary firmware.elf firmware.bin

# 从ELF生成HEX文件(Intel Hex格式)
riscv-none-embed-objcopy -O ihex firmware.elf firmware.hex

8. 段对齐的重要性

8.1 为什么需要对齐?

8.2 对齐示例

.text :
{
    . = ALIGN(4);    /* 4字节对齐 */
    *(.text)
    . = ALIGN(4);
} >FLASH

9. 优化建议

9.1 减少代码段大小

9.2 减少数据段大小

9.3 合理配置栈和堆

10. 总结

MCU编译生成的BIN文件包含了程序运行所需的所有段:

  1. 代码相关:.text(代码)、.rodata(只读数据)
  2. 数据相关:.data(已初始化数据)、.bss(未初始化数据)
  3. 系统相关:.vector(中断向量表)、.init/.fini(初始化/终结)
  4. 内存管理:.stack(栈)、.heap(堆)

理解这些段的作用和布局,有助于:

通过工具分析编译产物,可以更好地理解程序的存储和运行机制。