Linux嵌入式开发学习小记1:启动流程、C环境、链接脚本
keruone 2025 11 07
设备 正点原子I.MX6U-MINI
1. i.MX6ULL SD 卡启动流程
阶段 1:Boot ROM 启动(硬件层面)
- 复位触发:芯片上电或复位后,CPU 从内部固化 Boot ROM 起始地址开始执行
- 最小初始化:初始化内部 SRAM、系统时钟、SD 卡控制器等基础硬件
- 启动介质选择:根据 BootCFG 引脚电平确定从 SD 卡启动
阶段 2:读取 IVT(镜像向量表)
- 定位 IVT:从 SD 卡固定偏移地址(通常是 1KB 处,即扇区 2 的开始)读取 IVT
- 验证镜像:检查 IVT 结构是否符合 i.MX 规范
- 解析地址:从 IVT 中获取:
- 镜像在 RAM 中的加载地址
- DCD(Device Configuration Data)地址
- 用户代码入口地址
阶段 3:镜像复制到 RAM
- 计算大小:根据 IVT 和 Boot Data 确定完整镜像大小
- 整体复制:将 SD 卡中从 IVT 开始的整个镜像(IVT+DCD+用户代码)复制到 RAM 指定地址
阶段 4:执行 DCD 配置
- 解析 DCD:Boot ROM 解析 RAM 中的 DCD 表格
- 硬件配置:根据 DCD 指令配置寄存器(DDR 时序、时钟、引脚复用等)
- 完成初始化:确保硬件(特别是 DDR)正常工作
阶段 5:跳转到用户代码
- 跳转执行:CPU 跳转到 IVT 指定的用户代码入口地址
- 开始执行:执行
_start函数,进入用户程序逻辑
核心要点
- DCD 是数据表格:不是可执行代码,而是寄存器配置信息
- 整体加载:镜像完整复制到 RAM 后统一访问
- 地址关联:链接地址必须与加载地址一致
2. C语言运行环境构建
阶段 1:设置处理器模式
- 处于 SVC(supervisor) 特权模式,可访问全部资源
- CPSR 寄存器[4:0]位,可设置运行的具体模式
- 其中0b10011,即0x13表示 SVC 模式
阶段 2:设置 sp 指针(可内部、外部 RAM )
其中再启动过程中,DCD包含初始化DDR,所以可以使用外部RAM
i.MX6ULL的DDR起始地址位 0x80000000
– 如果是 256M,结束地址为 0x8FFFFFFF
– 如果是 512M,结束地址为 0x9FFFFFFF
– sp 指针 可在 DDR 范围内(注意整个栈不能超过DDR范围外)
– 注意为递减满栈
阶段 3:跳转到 C 语言
使用 b 指令跳转到main
b指令为cortex-A汇编代码
3. 链接脚本
3.1 引子
直接的makefile链接脚本为
arm-linux-gnueabihf-ld -Ttext 0x87800000 $^ -o xxxx.elf
上述命令通过 -Ttext 选项设置了代码段(.text)的起始虚拟地址为 0x87800000。链接器会从这个地址开始,依次放置代码段的内容。
然而,-Ttext 这种方式只适用于最基础的链接需求。在实际项目中,我们往往需要对程序的内存布局进行更精细的控制,例如:
– 将代码段(.text)放置在 Flash 或 ROM 中。
– 将已初始化的数据段(.data)放置在 RAM 中。
– 将未初始化的数据段(.bss)也放置在 RAM 中,但可能在 .data 之后。
– 将只读数据(.rodata)放置在特定的 Flash 区域。
– 为了满足这种将不同段(sections)放置在不同内存区域的复杂需求,我们就需要使用一个链接脚本(Linker Script) 文件来详细描述这种布局。
3.2 基础概念介绍
在讲链接脚本之前,得先搞清楚 .text、.data、.bss 这些“段”(Section)是啥。
这得从程序编译后的“样子”说起。我们熟悉的“预处理、编译、汇编、链接”,其实链接之前的步骤,生成的是一个个目标文件(.o 文件)。这些目标文件里,为了方便管理不同性质的数据,就把代码和数据分门别类地放进了不同的“段”里。
– .text 段:存放程序的指令,也就是你的 C/C++ 代码编译后生成的机器码。这部分通常是只读的,防止程序意外修改自己的指令。
– .data 段:存放已初始化的全局变量和静态变量。什么叫“已初始化”?就是你在代码里这样定义的:int global_var = 10; 或 static char str[] = "hello";。这些变量有确定的初始值,所以它们的值也会被“塞”进程序文件里。
rodata 就是只读数据 (read only data)
– .bss 段:存放未初始化的全局变量和静态变量。比如int uninit_var;或static float arr[100];。因为它们没有初始值(或者说初始值是 0),所以不需要在程序文件里占用空间来存储一堆零,只需要在链接时预留出相应的内存空间即可。BSS 的全称是 “Block Started by Symbol”。
⚠️ 注意:这里说的“未初始化”是指在源码中没有显式赋值。对于 C 语言,像 int x; 这样的声明,在编译后的目标文件(.o)中并不会直接进入 .bss 段,而是被标记为一个特殊的 COMMON 符号。链接器在最终链接时,会将所有同名的 COMMON 符号合并,并统一分配到 .bss 段中。这种机制允许在多个源文件中声明同一个未初始化的全局变量。因此,完整的流程是:未初始化的全局变量 (C源码) → COMMON符号 (目标文件.o) → .bss段 (最终可执行文件)。
这些标签(段名)是在汇编阶段(Assembly)产生的。 当汇编器(assembler)处理汇编代码(.s 或 .S 文件)时,它会根据汇编指令中的指示(比如 .section .text)将代码和数据分配到对应的段里,最终生成包含这些段的目标文件(.o)。
链接器(Linker)的任务,就是把多个目标文件(.o)和库文件中各种同名的段“合并”起来,形成最终可执行文件或库文件中的一个或几个段,并确定它们在内存中的最终位置。而链接脚本,就是我们给链接器的一份“地图”,精确指导它要把 .text、.data、.bss 等段放在内存的哪个地址上。
3.3 简单介绍
SECTIONS{
. = 0x10000000;
.text : {
start.o(.text) /* 优先放 start.o 的代码,确保 _start 在最前 */
main.o(.text) /* 放 main.o 的代码 */
*(.text) /* 其他所有目标文件的代码 */
}
. = 0x30000000;
.data ALIGN(4) : {*(.data)}
.bss ALIGN(4) : {*(.bss)}
}
其中:
– SECTIONS{} 是必不可少的
– 单个 . 表示 定位计数器,默认为0。直白点讲就是设置在它之后的片段的起始地址
– 单个 . 使用 = 并;结尾,但是具体的段使用 : 且无特殊符号结尾。
– * 表示通配符,如 *(.text) 表示 所有输入文件 中的 .text 段。其它段同理
– ALIGN(n)表示某个段的起始地址做字节对齐,即起始地址要能被 n 整除
– 如上文示例 start.o(.text)表示start.o文件的.text段。此时对于后文的通配符,链接器会自动去重。
如果你没有添加类似
(.text),而是直接写 start.o 或 main.o(而不指定具体段),确实可能将目标文件中的.data、.bss等非代码段也混入.text段中。
3.4 结合正点原子案例继续分析
SECTIONS{ /* 开始定义输出文件的内存段(Sections)布局 */
. = 0x87000000; /* 设置链接器的当前位置计数器(Location Counter)为虚拟地址 0x87000000。这是后续所有段的起始基准地址。 */
.text :{ /* 定义输出文件中的 .text 段 */
start.o(.text) /* 将输入文件 start.o 中的 .text 段内容放在当前地址计数器指向的位置 */
main.o(.text) /* 将输入文件 main.o 中的 .text 段内容紧跟在 start.o 的 .text 段之后 */
*(.text) /* 将所有其他输入文件(除了上面明确列出的)中的 .text 段内容放在后面 */
} /* .text 段定义结束 */
.rodata ALIGN(4) : {*(.rodata*)} /* 定义输出文件中的 .rodata 段,要求其起始地址按 4 字节对齐,然后将所有输入文件中名称以 .rodata 开头的段(如 .rodata, .rodata.str1.1 等)合并进来 */
.data ALIGN(4) : {*(.data)} /* 定义输出文件中的 .data 段,要求其起始地址按 4 字节对齐,然后将所有输入文件中的 .data 段内容合并进来 */
__bss_start = .; /* 定义一个符号 __bss_start,其值等于当前位置计数器(即 .data 段结束后的地址),用来标记 .bss 段的起始位置 */
.bss ALIGN(4) : {*(.bss) *(COMMON)} /* 定义输出文件中的 .bss 段,要求其起始地址按 4 字节对齐,然后将所有输入文件中的 .bss 段和 COMMON 符号(未初始化的全局/静态变量)合并进来 */
__bss_end = .; /* 定义一个符号 __bss_end,其值等于当前位置计数器(即 .bss 段结束后的地址),用来标记 .bss 段的结束位置 */
} /* SECTIONS 块定义结束 */
详细内容已在注释中,不过:
1. 为什么 COMMON 前面没有 “.”,但是如.bss之类的有?
– .bss 是一个“段名”(section name),而 COMMON 是一个“伪段名”或“符号类名”(symbol class),不是实际存在的段。
2. __bss_start 和 __bss_end 是两个由链接器脚本定义的符号(Symbol),它们本身不占用程序文件的空间,但会被赋予一个具体的内存地址值。它们的作用是为C语言运行时或操作系统提供关于 .bss 段位置和范围的关键信息。
3. 单个. 是动态变化的:它不是一个固定的数字,而是一个随着链接器处理每个段而不断递增的指针。每处理一个段,. 就会增加该段的大小。
4. 注意,各个**段名**和”:”之间一定要留一个空格,否则会报语法错误
3.5 链接脚本后 command 模样
原始脚本为:
arm-linux-gnueabihf-ld -Ttext 0x87800000 $^ -o xxxx.elf
假设你的脚本叫做yyy.lds,那么现在可以更改为:
arm-linux-gnueabihf-ld -Tyyy.lds $^ -o xxxx.elf
此时,就可以放心使用-O2参数优化了,不然直接使用的话,可能会导致<_start>不在最开始