在这里插入图片描述

一、本章说明

1. 前置知识

本章接续 lesson15(8253 PIT + 抢占式多任务)。前面已经能进保护模式、处理中断并做任务切换,但物理内存地址仍依赖“假定机器有多少 RAM”的硬编码——换一台虚拟机配置就可能踩到空洞或设备预留区。

2. 本章目标

完成本章后,我们将能够:

  • 实模式下用 BIOS INT 15h, AX=E820h 探测真实物理内存布局(ARDS)
  • 把杂乱的内存段清洗为 4KB 物理页帧(Page Frame)
  • 用**位图(Bitmap)**实现物理内存分配器(PMM):pmm_init / pmm_alloc_page / pmm_free_page
  • 在内核中打印 ARDS、总内存与分配/释放结果,在 Bochs 中验证

3. 目录结构

lesson16/
├── Makefile
├── boot/
│   ├── loader.s        # 读盘 + E820 探测 + 进保护模式
│   └── pm.inc          # 段描述符宏
├── kernel/
│   ├── main.c        # 打印 ARDS / PMM 状态,演示 alloc/free
│   ├── pmm.c / pmm.h   # 位图物理页分配器
│   ├── kernel_end.s    # 导出 kernel_end(位图紧随内核放置)
│   ├── utilities.s     # 显存输出等(沿用前几章)
│   └── utilities.h
└── README.md

4. 与 lesson15 的差异

项目 lesson15 lesson16
引导 读盘后直接进 PM 进 PM 前增加 E820 探测
内存管理 无(栈/任务栈静态数组) Bitmap PMM
多任务 task.c / task.s + IRQ0 本章演示不再依赖调度
内核入口 创建 A/B 任务并切换 初始化 PMM 并打印分配结果

实模式读盘

INT 15h E820

计数@0x8000 / ARDS@0x8010

进入保护模式

pmm_init

bitmap 管理 4KB 页

alloc / free


二、为什么不能直接“使用”所有内存?

系统开机时,物理 RAM 并不是连续且完全可用的。部分区域被固件、显存或主板设备占用:

0x00000000 +-----------------------------------+
           | IVT / BIOS Data Area              |  <- 需谨慎使用
0x000A0000 +-----------------------------------+
           | Video RAM (VGA 640KB~1MB)         |  <- 不可作普通 RAM
0x00100000 +-----------------------------------+
           | Extended Memory(内核通常在此)    |  <- 可用 Usable
           | ...                               |
           +-----------------------------------+
           | ACPI / Reserved / Device MMIO     |  <- 硬件预留
           +-----------------------------------+

盲目向 0xA0000 写数据会花屏;写入 Reserved 区可能直接崩溃。因此:先探测、后分配


三、BIOS INT 15h(E820)探测物理内存

1. ARDS 是什么?

ARDS 全称 Address Range Descriptor Structure(地址范围描述符结构)。

可以把它理解成 BIOS 给出的一张「内存地契」:一条 ARDS = 物理地址空间里的一段连续区域,说明这段从哪开始、有多长、能不能当普通 RAM 用。

E820 不会一次把整张内存地图塞给你,而是迭代返回多条 ARDS;把它们拼起来,就是整机的物理内存布局。例如典型机器可能返回:

条目 大致范围 Type 含义
#0 0x00000000 起一小段 1 可用低端 RAM
#1 0x000A0000 附近 2 显存 / 设备预留,不可当堆用
#2 0x00100000 起大段 1 可用扩展内存(内核常在此)
3/4 等 ACPI 等特殊用途

内核只把 Type = 1(Usable) 的页交给分配器;其它类型一律视为不可分配。

如何判定「无效 / 空」条目(检测为 0)

BIOS 不会用「全 0 结构」作为官方结束符——正常结束看的是 EBX == 0(或 CF=1)
但缓冲区里仍可能出现全 0 槽位(未写入的残留、被跳过的洞)。判定方式:

/* type==0 或 length==0 → 无效,直接跳过 */
static inline int ards_is_valid(const ards_t* e)
{
    return e->type != 0 && e->length != 0;
}
字段 为 0 时含义
type == 0 不是合法 E820 类型(合法值从 1 起),视为空槽
length == 0 零长度区间,无意义,跳过
baselength 皆 0 典型的未初始化填充,与上面两条一并过滤

截图里 [6][7]type=0 就是这类脏槽。真正的条目数应以 loader 写入的 ARDS count 为准;若 count 被破坏,再按 ards_is_valid 过滤并钳制上限。

2. ARDS 结构(20 字节)

每条 ARDS 固定 20 字节(本课按经典 E820 请求;部分 BIOS 还可返回 24 字节扩展属性,此处不展开):

偏移 大小 名称 含义
0x00 8 BaseAddr 该段物理起始地址(64 位)
0x08 8 Length 该段长度(字节,64 位)
0x10 4 Type 1=可用 RAM;2=预留;3=ACPI 可回收;4=ACPI NVS;0=无效

本课约定内存地图布局(均在内核 0xC200 之前):

地址 内容
0x8000 uint16 有效 ARDS 条数
0x8010 ARDS 数组(每项 20 字节)

3. 迭代查询流程

每次调用返回一条 ARDS,并用 EBX 作为 continuation;EBX == 0 表示结束。

CF=1 或 EAX != 'SMAP'

CF=0 且 EAX == 'SMAP'

设置 EAX=0xE820, ECX=20, EDX='SMAP', EBX=0

INT 15h

检查返回值

失败 / 结束

保存 ARDS,DI+=20,计数+1

EBX == 0 ?

结束

继续循环

4. 本课实现(boot/loader.s

在读盘成功、enter_pm 之前调用 detect_memory

ARDS_COUNT  EQU 0x8000
ARDS_BUFFER EQU 0x8010

detect_memory:
    xor  ebx, ebx
    xor  ax, ax
    mov  ds, ax
    mov  es, ax
    mov  di, ARDS_BUFFER
    mov  word [ARDS_COUNT], 0

.e820_loop:
    mov  eax, 0xE820
    mov  ecx, 20
    mov  edx, 0x534D4150    ; 'SMAP'
    int  0x15
    jc   .e820_done

    cmp  eax, 0x534D4150
    jne  .e820_error

    ; type==0 或 length==0 → 跳过(不前进 DI,下次覆盖写)
    cmp  dword [es:di + 16], 0
    je   .e820_skip
    cmp  dword [es:di + 8], 0
    jne  .e820_accept
    cmp  dword [es:di + 12], 0
    je   .e820_skip

.e820_accept:
    add  di, 20
    inc  word [ARDS_COUNT]

.e820_skip:
    test ebx, ebx
    jnz  .e820_loop
.e820_done:
    ret

对应 C 侧判定(与 20 字节条目一一对应):

typedef struct {
    uint64_t base_addr;
    uint64_t length;
    uint32_t type;
} __attribute__((packed)) ards_t;

/* 检测是否为 0 / 无效条目 */
static inline int ards_is_valid(const ards_t* e)
{
    return e->type != 0 && e->length != 0;
}

四、物理页分配器(PMM)

x86 常见以 4KB 为一页。若物理上界为 128MB:

总页数 = 128 × 1024 × 1024 4096 = 32768 \text{总页数} = \frac{128 \times 1024 \times 1024}{4096} = 32768 总页数=4096128×1024×1024=32768

1. 位图约定

  • 1 bit ↔ 1 个 4KB 页
  • 0 = 空闲,1 = 已占用 / 不可用

8 bit 管理 (8 \times 4\text{KB} = 32\text{KB});管理 128MB 仅需约 4KB 位图。

2. 数据结构(kernel/pmm.h

typedef struct {
    uint8_t* bitmap;
    uint32_t bitmap_size;
    uint32_t total_pages;
    uint32_t free_pages;
    uint32_t max_mem_size;
} pmm_manager_t;

kernel_end 由链接末尾的 kernel_end.s 导出,位图放在页对齐后的内核末尾,避免踩内核代码/数据。

3. 初始化四步(pmm_init

  1. 扫描 type == 1 的 ARDS,得到最大物理上界 → total_pages / bitmap_size
  2. 位图默认全部置 0xFF(全部占用,安全第一)
  3. 再把可用段对应页 清 0,并累计 free_pages
  4. 低端内存 + 内核 + 位图自身 重新置 1

半页处理:可用段起始页向上对齐、结束页向下对齐,避免把非完整页标成空闲。

4. 分配与释放

void* pmm_alloc_page(void);   /* 找第一个空闲 bit,置 1,返回页物理地址 */
void  pmm_free_page(void* p); /* 页对齐检查后清 0,free_pages++ */

分配是线性扫描位图(教学实现,清晰优先;后续可改为 buddy / 空闲链表等)。

5. kernel_end

kernel_end.s 本身

链接时把一个空标签放到内核映像最后,标签的地址就是“内核结束地址”。
对于kernel_end.s本身:

  • global kernel_end:把符号导出,给 C 用
  • kernel_end::只是一个标签,后面没有任何指令/数据
  • 所以它不占有效内容,只是一个地址标记

它靠链接顺序“钉”在末尾:

kernel.bin: main.o utilities.o pmm.o kernel_end.o
	$(LD) $(LDFLAGS) -Ttext 0xC200 ... $(TARGET)main.o $(TARGET)utilities.o $(TARGET)pmm.o $(TARGET)kernel_end.o
链接器按命令行顺序排布:
[0xC200] main.o → utilities.o → pmm.o → kernel_end.o
                                    ↑
                              kernel_end 落在这里

-Ttext 0xC200 指定内核从 0xC200 开始;kernel_end.o 放在最后,所以 kernel_end 的值 ≈ 内核映像末尾地址。

c 侧使用

extern uint8_t kernel_end[];    /* 由 kernel_end.s 提供,链在内核映像末尾 */

    pmm_init(ards, ards_count, (uint32_t)kernel_end);
  • 声明成数组,取的是地址,不是内容
  • (uint32_t)kernel_end 就是内核结束地址
        uint32_t bitmap_addr = align_up(kernel_end_addr, PAGE_SIZE);

把位图放在 kernel_end 向上对齐到页边界之后,这样位图紧挨内核,又不会踩到内核代码/数据。

对齐说明

向上对齐是为了在页边界上安全地放下位图,且不踩内核。

假设 kernel_end = 0xC345,页大小 0x1000

方向 结果 问题
向下对齐 0xC000 落进内核,会覆盖代码/数据
不齐 0xC345 位图和内核尾巴挤在同一页
向上对齐 0xD000 越过内核,从整页开始

PMM 按整页管理(分配、回收、预留都是 PAGE_SIZE)。位图放在页对齐地址后:

  1. 内核最后那半页只属于内核,不会和位图混在一起
  2. 后面按页预留时更干净:
        uint32_t reserved_end = align_up(
            (uint32_t)pmm.bitmap + pmm.bitmap_size, PAGE_SIZE);
        uint32_t reserved_pages = reserved_end / PAGE_SIZE;

一句话:向下会覆盖内核;不齐会让一页里既有内核又有位图;向上才是“紧随内核之后、又按页对齐”的安全位置。


五、内核演示(kernel/kernel.c

void main(void)
{
    ards_t* ards = (ards_t*)ARDS_BUFFER_ADDR;       /* 0x8010 */
    uint16_t ards_count = *(uint16_t*)ARDS_COUNT_ADDR; /* 0x8000 */

    clear_screen();
    /* 打印若干条 ARDS */
    pmm_init(ards, ards_count, (uint32_t)kernel_end);

    /* 打印 Total Memory / Total Pages / Free Pages */
    void* p1 = pmm_alloc_page();
    void* p2 = pmm_alloc_page();
    pmm_free_page(p1);
    /* ... */
}

Bochs 中典型输出(内存大小随配置变化):

Lesson16: Physical Memory Manager (PMM)
========================================
ARDS count: ...
  [0] base=0x00000000 len=... type=1
  ...
=== PMM Init ===
PMM state:
mem=31MB pages=8176 used=113 free=8063_
bitmap@0x0000F000 size=1022_
regions:
  [USED] 0x00000000-0x00010000  [#]
  [FREE] 0x00010000-0x0009F000  [.]
  [USED] 0x0009F000-0x00100000  [#]
  [FREE] 0x00100000-0x01FF0000  [...................]
Allocated Page 1: 0x00010000_
Allocated Page 2: 0x00011000_
Freed Page 1. Free Pages: 8062_

构建与运行:

make
make run

在这里插入图片描述


六、本章小结

步骤 实现内容 关键点
1. 探测 loader.s 中 E820 迭代 实模式、SMAP、ARDS @ 0x8000
2. 页框 4KB 粒度 上界 → total_pages
3. 位图 先全占再按 ARDS 释放 保护内核与位图自身
4. API alloc / free bit 的 set / clear / test

至此内核掌握了物理页的分配权。以后可以建立页表,把“物理页”映射为进程可用的虚拟地址空间,从而实现隔离与更灵活的内存布局。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐