Linux嵌入式开发学习小记1:启动流程、C环境、链接脚本

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>不在最开始


暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇