裸机嵌入式 (STM32 等)和操作系统程序 (Linux 等)程序启动对比

发布于:2025-06-29 ⋅ 阅读:(17) ⋅ 点赞:(0)

🚀 代码段的存放位置(.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,给进程分配虚拟内存,并映射到空闲物理页


网站公告

今日签到

点亮在社区的每一天
去签到