Linux内核学习之内存寻址
0. 前言
本文是以x86为例,讲解Linux中的寻址方法,在读本文前,建议先了解x86硬件分段、分页的实现,详情可以参考我另一篇文章x86架构学习之操作模式、内存寻址、特权级
1. Linux中的分段
介绍过硬件分段的原理,接下来就是软件如何配置硬件,即如何设计全局描述符表(或局部描述符表),然后把描述符表的基地址给到gdtr(或ldtr)寄存器。
Linux最初设计目标是让它可以移植到大多数流行处理器上,然而,多数处理器,比如RISC体系结构对分段的支持非常有限,因此,Linux重点使用的分页,在x86上,它通过合适的设置,让逻辑地址通过分段单元后,得到的线性地址与逻辑地址相同,从而在宏观上绕过了分段单元。
Linux配置的四个段描述符(用户代码段、数据段,内核代码段、数据段),都映射到了线性地址0。四个段描述符放在全局描述符表中,其他的还有一些描述符
段选择符 | 全局描述符表 | 段选择符 | 全局描述符表 |
---|---|---|---|
0x0 | null | 0x80 | TSS |
NU(not used) | 0x88 | LDT | |
NU | 0x90 | PNPBIOS 32-bit code | |
NU | 0x98 | PNPBIOS 16-bit code | |
NU | 0xa0 | PNPBIOS 16-bit data | |
NU | 0xa8 | PNPBIOS 16-bit data | |
TLS1 | 0xb0 | PNPBIOS 16-bit data | |
TLS2 | 0xb8 | APMBIOS 32-bit code | |
TLS3 | 0xc0 | APMBIOS 16-bit code | |
NU | 0xc8 | null | |
NU | NU | ||
NU | NU | ||
0x60 | kernel code | 0x0 | NU |
0x68 | kernel data | 0x0 | NU |
0x73 | user code | 0x0 | NU |
0x7b | user data | 0x0 | double fault TSS |
- 1个默认局部描述符段,由所有进程共享,内控提供modify_ldt系统调用,供进程创建自己的局部段描述符表。
- 3个线程局部存储段(TLS,thread-local storage),由内核提供的系统调用set_thread_area()和get_thread_area(),共用户程序使用。
- 与BIOS高级电源管理(APM)相关的3段
- 与BIOS即插即用(PnP)相关的5个段
- 处理硬件双重错误的特殊TSS段,双重错误,即在异常中再次触发了异常。
ps1:这里没看到栈段的描述符,因此个人推测,Linux将栈段选择符配置成了和数据段选择符一样,两者使用相同的段描述符。
ps2:每个CPU有自己独立的gdtr和MMU,因此多核的处理器,Linux会配置多个全局描述符表。
Linux的4个主要代码、数据段描述符配置如下
段 | 线性基地址 | 大小 | DPL |
---|---|---|---|
用户代码段 | 0 | 4GB | 3 |
用户数据段 | 0 | 4GB | 3 |
内核代码段 | 0 | 4GB | 0 |
内核数据段 | 0 | 4GB | 0 |
2. Linux中的分页
和分段相同,Linux中的分页,其实也就是配置分页单元,即初始化各级页表的表项内容(以及后续更新表项),如页与页框的映射关系,页的权限等等,之后只需把最上级的页目录(二级页表中是页目录,三级页表中是页目录指针表,四级页表中是页全局目录)的基地址给到cr3寄存器即可。
ps:为了实现进程地址空间隔离,Linux为每个进程配备自己的页表集,当进程切换时,cr3被保存到前一个进程的进程描述符中,后一个进程的进程描述中的保存值则被加载到cr3。