物理地址(0x00000000 开始)
│
├── 0x00010000 开始加载内核代码、数据(.text/.data/.bss)
│ ↓↓↓
│ ±------------------+
│ | 内核代码段 .text|
│ | 只读数据段 .rodata|
│ | 初始化数据段 .data|
│ | BSS段(未初始化)|
│ ±------------------+ ← e_data
│
├── 虚拟地址 0x80000000 开始(first_task 映射的起始地址)
│ ↓↓↓
│ ±--------------------+
│ | 用户态 first_task |
│ | 包括 lib_syscall 等 |
│ ±--------------------+ ← mem_free_start(空闲内存)
SECTIONS
{
PROVIDE(kernel_base = 0x0);
. = 0x00010000;
PROVIDE(s_text = .);
.text : {
*(EXCLUDE_FILE(*first_task* *lib_syscall*) .text)
}
.rodata : {
*(EXCLUDE_FILE(*first_task* *lib_syscall*) .rodata)
}
PROVIDE(e_text = .);
. = ALIGN(4096);//必须页边界对齐
PROVIDE(s_data = .);
.data : {
*(EXCLUDE_FILE(*first_task* *lib_syscall*) .data)
}
.bss : {
*(EXCLUDE_FILE(*first_task* *lib_syscall*) .bss)
}
e_data = .;
/* 初始进程的配置:接紧着在低端1MB内存开始存储,但是运行时搬运到0x80000000处 */
. = 0x80000000;
PROVIDE(s_first_task = LOADADDR(.first_task));
.first_task : AT(e_data) {
*first_task_entry*(.text .data. rodata .data)
*first_task*(.text .data. rodata .data)
*lib_syscall*(.text .data .bss)
}
PROVIDE(e_first_task = LOADADDR(.first_task) + SIZEOF(.first_task));
PROVIDE(mem_free_start = e_first_task); /*链接脚本,用extern调用*/
}
你这段 linker script
(链接脚本)写得非常完整!👏
这里结合你之前的问题,我们来做一个全景解析,特别说明 SECTIONS
中每一段在干嘛、为什么这么写、它背后的内核内存布局含义是什么。
✅ 一、整体结构图解(大局观)
物理地址(0x00000000 开始)
│
├── 0x00010000 开始加载内核代码、数据(.text/.data/.bss)
│ ↓↓↓
│ +-------------------+
│ | 内核代码段 .text|
│ | 只读数据段 .rodata|
│ | 初始化数据段 .data|
│ | BSS段(未初始化)|
│ +-------------------+ ← e_data
│
├── 虚拟地址 0x80000000 开始(first_task 映射的起始地址)
│ ↓↓↓
│ +---------------------+
│ | 用户态 first_task |
│ | 包括 lib_syscall 等 |
│ +---------------------+ ← mem_free_start(空闲内存)
✅ 二、关键点逐段分析
🔸 PROVIDE(kernel_base = 0x0);
- 定义一个符号
kernel_base
为 0 - 通常表示 内核的物理起始地址,供内核代码中引用
🔸 . = 0x00010000;
- 指定 内核段的起始加载地址是 64KB(
0x10000
) - BIOS/MBR 占用 0x0000~0x7C00,0x00010000 是比较标准的内核加载起始
🔸 .text
, .rodata
, .data
, .bss
段
.text : {
*(EXCLUDE_FILE(*first_task* *lib_syscall*) .text)
}
💡 把所有目标文件的
.text
段都合并过来,但排除掉名字中包含first_task
和lib_syscall
的目标文件。
📌 意义是:
- 把内核代码和数据放入内核空间
.text/.rodata/.data/.bss
- 不要把用户态的
first_task
和lib_syscall
也一起混进去
🔸 e_data = .;
e_data
是所有内核段结束的地方- 它就是下一个加载段(first_task)放置的物理地址
🔸 . = 0x80000000;
- 指定
.first_task
的虚拟地址起始位置是 0x80000000 - 这是用户空间或用户任务运行时所在的高虚拟地址
🔸 .first_task : AT(e_data)
.first_task : AT(e_data) {
*first_task_entry*(.text .data .rodata .data)
*first_task*(.text .data .rodata .data)
*lib_syscall*(.text .data .bss)
}
- 定义
.first_task
段,从虚拟地址0x80000000
开始 - 但实际加载在物理地址
e_data
- 包含内容:
- 用户进程入口(
first_task_entry
) - 用户任务本体(
first_task
) - 系统调用接口(
lib_syscall
)
- 用户进程入口(
🧠 这是典型的:
运行地址 ≠ 加载地址
程序加载后要“搬运”到虚拟地址运行(通常在页表映射完成后)
🔸 PROVIDE(...)
符号的用途
PROVIDE(s_first_task = LOADADDR(.first_task));
PROVIDE(e_first_task = LOADADDR(.first_task) + SIZEOF(.first_task));
PROVIDE(mem_free_start = e_first_task);
这几行定义了几个关键符号:
符号 | 含义 |
---|---|
s_first_task | .first_task 段的加载地址(物理地址) |
e_first_task | .first_task 段的物理结束地址 |
mem_free_start | 第一个任务结束后的可用物理内存开始地址,用于后续内存分配 |
在 C 中你可以这样使用这些符号:
extern char mem_free_start[];
memory_init(mem_free_start); // 初始化内存分配器
✅ 总结一下这份链接脚本干了什么?
目的 | 实现方式 |
---|---|
保留内核低端物理空间 | . = 0x10000 |
按模块组织段 | .text , .data , .bss , .rodata |
排除用户进程代码 | EXCLUDE_FILE(*first_task*) |
用户任务运行于高虚拟地址 | . = 0x80000000; .first_task |
支持虚实地址分离加载 | .first_task : AT(e_data) |
导出关键地址符号供内核使用 | PROVIDE(...) |
🧠 面试总结金句:
这份链接脚本通过
EXCLUDE_FILE
和虚拟地址定位,清晰地区分了内核段与用户进程段,并通过AT()
实现虚拟地址和加载地址分离,确保用户任务可以在高地址运行,同时保留了内核的低端物理布局。
要不要我给你画个完整的“内核加载段 vs 用户任务段(虚拟地址映射)”的对比图?📊 非常适合做注释笔记或面试展示~