🚀 代码段的存放位置(.text 段)
🌟 在嵌入式裸机程序(如 STM32)
💡 代码段 .text 放在 Flash(ROM、程序存储器)
- 因为嵌入式芯片 RAM 很小,不足以装整个程序代码。
- Flash(或 ROM)是非易失性存储,程序烧进去就能掉电保留。
- MCU 在上电时直接从 Flash 取指令执行。
👉 内存布局(常见 STM32):
Flash(代码区)
+---------------------+
| 矢量表 + 启动代码 |
| .text (代码) |
| .rodata (常量) |
| .data 初始镜像 |
+---------------------+
RAM(数据区)
+---------------------+ <- SP 初始值 (栈顶)
| 栈 |
| 堆 |
| .bss (清零变量) |
| .data (初始化变量) |
+---------------------+
✅ .text 始终在 Flash,MCU 从 Flash 执行代码,不需要先搬到 RAM。
🌟 在一般 GCC + 操作系统程序(如 Linux)
💡 代码段 .text 放在磁盘的可执行文件中,加载时由操作系统映射到 RAM(内存)
- 可执行文件(如 ELF)放在磁盘。
- 操作系统 loader(加载器)读取文件,把 .text 映射进虚拟内存(通常是物理 RAM)。
- CPU 从 RAM(内存)取指执行(即便你看到映射自文件,本质还是在内存中执行)。
👉 内存布局(Linux 程序):
磁盘
+---------------------+
| 可执行文件 (ELF) |
| 包含 .text/.data/... |
+---------------------+
运行时内存(RAM)
+---------------------+
| .text 映射到 RAM |
| .rodata 映射到 RAM |
| .data 初始化到 RAM |
| .bss 已清零 RAM 区 |
| 堆 |
| 栈 |
+---------------------+
✅ 代码段 .text 在运行时实际在 RAM(内存)中执行。
✅ 操作系统支持虚拟内存:可以映射、保护、共享代码段等。
🌈 两者最大区别
特性 | 裸机嵌入式 (STM32 等) | 操作系统程序 (Linux 等) |
---|---|---|
代码 (.text) 存放位置 | 存在 Flash / ROM | 存在磁盘文件,加载进 RAM |
执行来源 | MCU 从 Flash 取指 | CPU 从内存取指(可能映射文件) |
是否需要搬移代码到 RAM 才能运行 | ❌ 不需要 | ✅ loader 搬进内存执行 |
内存资源 | 非常有限 (Flash + 小RAM) | 大RAM + 磁盘 + 虚拟内存 |
启动方式 | 启动代码直接跑在硬件上 | OS loader 加载可执行文件 |
💡 特殊情况:嵌入式中代码段放 RAM 的场景
你也许会想到:
👉 有时嵌入式程序确实会把 .text 搬到 RAM:
- 为了跑在高速 SRAM 里,加快速度(例如某些 STM32 内部高速 RAM)。
- 某些代码需要在写 Flash 时运行(写 Flash 时不能同时取指)。
- Bootloader / 特殊场景需要动态重映射。
💡 这种情况下链接脚本会明确把 .text 放 RAM,启动代码负责拷贝。
例如:
.text :
{
*(.text*)
} > RAM AT > FLASH
👉 意思是:运行时地址在 RAM,但初始值在 Flash,启动代码负责搬。
✅ 结论
✔ 操作系统程序:代码最终会映射到内存(RAM),CPU 从 RAM 取指。
✔ 裸机嵌入式程序:代码直接存在 Flash,CPU 从 Flash 执行。
✔ 区别本质:有无 OS loader + 存储介质不同 + 内存资源不同。
u-boot 和 STM32 裸机启动的相似点:
特点 | STM32 裸机启动 | ARM 的 u-boot |
---|---|---|
是否有操作系统 | 没有 | 没有 |
代码存放位置 | 代码直接存放在 Flash 中 | 代码也通常存放在 Flash 或 SPI/NAND 闪存中 |
启动方式 | MCU 上电后从 Flash 直接执行 | CPU 复位后从 Flash 或 ROM 执行启动代码 |
内存初始化 | 启动文件负责复制 .data,清零 .bss | 启动代码负责初始化内存和拷贝数据段 |
堆栈设置 | 启动代码设置栈指针 | 启动代码设置堆栈指针 |
加载程序 | 直接运行 | u-boot 负责加载 Linux 内核或其他程序到 RAM 并启动 |
具体说:
- u-boot 是一个裸机程序(bootloader),它没有操作系统,也就是说它需要自己完成内存初始化、堆栈设置、Flash 读取等工作。
- u-boot 代码存放在非易失存储(Flash 等),启动时 CPU 直接从 Flash 取指。
- 它会完成类似 STM32 启动代码的工作,比如初始化 RAM,拷贝必要的数据,配置硬件,最终加载 Linux 内核或其他程序到 RAM 运行。
所以,u-boot 本质上和 STM32 裸机程序的启动流程很类似,都是“无操作系统,启动代码负责初始化内存和执行环境”,区别是 u-boot 体积更大、功能更复杂,还要支持设备驱动、文件系统、网络等功能。
U-Boot 是否使用 MMU?
1. 什么是 MMU?
- MMU(内存管理单元)是硬件组件,负责虚拟地址和物理地址转换,支持内存保护和分页。
- Linux 等现代操作系统依赖 MMU 来实现虚拟内存管理。
- 对于裸机程序和启动加载器来说,是否开启 MMU 是设计的一个选项。
2. U-Boot 默认行为
- 在大多数平台上,U-Boot 启动时默认是关闭 MMU 的。
- U-Boot 是一个启动加载器(Bootloader),它的任务是完成硬件初始化,加载操作系统镜像到内存,然后跳转执行操作系统。
- 关闭 MMU 可以简化启动代码,减少初始化复杂度。
3. U-Boot 是否可以开启 MMU?
- 是的,U-Boot 支持在启动阶段根据需要开启 MMU。
- 对于一些需要 MMU 功能的高级平台或特定功能,U-Boot 可以配置并启用 MMU。
- 启用 MMU 后,U-Boot 可以使用虚拟地址进行内存访问,支持更复杂的内存管理。
4. 具体情况举例
ARM Cortex-A 系列(带 MMU)
- U-Boot 通常会配置页表,并在加载操作系统之前开启 MMU。
- 这样可以为 Linux 或其他操作系统提供正确的内存映射环境。
ARM Cortex-M 系列(无 MMU)
- U-Boot 不存在,因为 Cortex-M 用于裸机程序,启动代码简单,没有 MMU。
5. 总结
方面 | 说明 |
---|---|
默认状态 | U-Boot 启动时通常关闭 MMU |
MMU 使用 | 可根据平台需求配置并开启 MMU |
作用 | 为操作系统启动做好虚拟内存准备 |
Cortex-M 设备 | 无 MMU,U-Boot 不适用 |
通常 U-Boot 在搬运(加载)Linux内核到内存时是关闭MMU的,原因和流程大致如下:
1. U-Boot启动初期 — MMU一般是关闭的
- CPU上电复位后,MMU默认关闭,U-Boot作为第一个启动阶段的引导程序,也通常保持MMU关闭。
- 关闭MMU时,CPU使用的是物理地址访问内存,U-Boot可以直接通过物理地址读写Flash和RAM。
2. U-Boot搬运内核(Copy Kernel)过程
- U-Boot从Flash(或者其他存储介质)读取Linux内核镜像(二进制文件)到指定的物理内存地址。
- 这一步是物理地址访问,MMU关闭时,U-Boot直接用物理地址操作内存,没有地址转换。
- 这样避免了复杂的页表配置,代码简单、执行快速。
3. U-Boot开启MMU时机
- 在完成内核加载、设备初始化等必要操作后,U-Boot才可能配置并开启MMU。
- 开启MMU主要是为了给操作系统(比如Linux)准备好虚拟内存环境。
- Linux内核入口通常假定MMU已经启用,或者启动代码会在早期初始化MMU。
4. 具体流程示意
CPU复位 -> MMU关闭 -> U-Boot运行
↓
U-Boot关闭MMU状态下从Flash加载内核到RAM
↓
U-Boot(根据平台需要)配置并开启MMU
↓
跳转内核入口,内核启动(此时MMU已开启或由内核早期启动代码开启)
5. 额外说明
- 某些特殊平台或配置下,U-Boot可能会提前开启MMU(比如需要MMU才能访问某些内存或设备),但这不是常见做法。
- 裸机启动(无操作系统)时,MMU通常一直关闭。
Linux内核是怎么识别和管理自己可用的RAM空间,也就是区分“内核占用”与“空闲RAM”的:
Linux内核识别和管理可用RAM的原理
1. 启动时内核获取物理内存信息
当Linux内核启动时,启动引导程序(如U-Boot)会传递物理内存布局给内核。
这通常通过如下方式传递:
- ATAGs(旧平台)
- Device Tree Blob (DTB)(现代ARM平台)
- 或者通过固件接口(UEFI)等
这些结构告诉内核哪些物理地址范围是可用的RAM,哪些区域是保留的(比如MMIO,外设寄存器地址等)。
2. 内核使用内存管理子系统
- 内核启动时,会根据启动参数和传入的内存地图(memory map)调用
memblock
子系统(早期阶段)进行物理内存管理。 - 然后初始化 伙伴系统(buddy allocator),它管理物理页的分配和释放。
3. 内核自己代码和数据占用的内存
- 内核本身的代码、只读数据(.text、.rodata)、已初始化数据(.data)和未初始化数据(.bss)都会被映射到物理内存的特定区域。
- 这些内存区域在内核启动阶段就被占用,不可用于其他用途。
4. 剩余内存被标记为空闲内存
- 除去内核自身占用的物理内存区块,剩下的内存都被内核标记为“空闲”,供内核后续动态分配(内核堆、页缓存、用户空间进程等)。
- 这个过程在内核启动早期完成。
5. 简化流程图
引导程序传递内存地图 ---> 内核初始化memblock系统 ---> 标记内核占用区域
---> 伙伴系统初始化(空闲内存管理)
---> 其余内存区块标记为空闲
6. 内核如何避免占用冲突?
- 内核链接脚本中定义了内核镜像在物理内存中的位置(起始地址和大小)。
- 启动代码中,符号(如
_start
,_end
)告诉内核自己占用内存范围。 - 内核memblock系统会把这块内存标记为“保留”,不分配给其它用途。
7. 举例
假设系统RAM是0x80000000 ~ 0x80800000(8MB),
- 内核代码占用0x80000000 ~ 0x80040000 (256KB)
- 其余的7.75MB标记为空闲
总结
步骤 | 说明 |
---|---|
内存地图传递 | U-Boot等引导程序传递物理内存可用范围 |
内核占用内存范围 | 由链接脚本和启动代码符号确定内核占用内存区块 |
空闲内存管理 | memblock 和 buddy allocator 管理剩余可用内存 |
🌟 大致流程
1️⃣ U-Boot 提供可用 RAM 信息给内核
U-Boot 在启动 Linux 时会传递 物理内存信息(RAM 的起始地址、大小)。
具体形式:
- Device Tree (DTB) 文件:现代 ARM 系统通用。
- ATAGs:老式 ARM 系统。
内核启动代码解析这些结构,知道哪些物理地址范围是 RAM,哪些是保留或外设地址。
2️⃣ 内核初始化物理内存管理
内核早期阶段(还没开启 MMU 时),使用 memblock 子系统 管理物理内存范围。
memblock 记录:
- 哪些物理内存区块可用。
- 哪些区块已被内核自己占用(内核代码、BSS、数据段等)。
- 哪些区块是保留或不可用的。
内核自己的内存区块会标记为 保留,防止后续分配覆盖它。
3️⃣ 内核设置 MMU 和虚拟地址空间
- 内核会建立页表,把物理内存映射到内核虚拟地址空间(比如 ARM 上虚拟地址从
0xC0000000
开始)。 - 这时候 MMU 开启,内核代码从物理地址跳到虚拟地址执行。
4️⃣ 内核管理 ELF 程序(用户态进程)
当 Linux 内核加载用户程序(比如 ELF 文件)时:
- 内核会在虚拟内存空间(由MMU提供)为进程分配地址空间。
- 页表把用户进程的虚拟地址映射到实际的物理内存(也就是 memblock 和伙伴系统管理的那片空闲RAM)。
- 内核负责加载 ELF 各段到合适的物理内存,并建立虚拟到物理的映射。
🌈 简化流程图
U-Boot 提供内存地图(DTB / ATAGs)
↓
内核 memblock 记录物理内存分布(哪些可用,哪些保留)
↓
内核构建页表,启动 MMU,映射内核虚拟地址
↓
内核加载用户 ELF,给进程分配虚拟内存,并映射到空闲物理页