一. 逻辑地址和线性地址
下面是一条普通的 eax赋值指令
> mov 0x80495b0, %eax
首先 这条指令 真正会被执行成 mov ds:0x80495b0, %eax
在保护模式下,ds 是个段选择子,通过 GDT在 表 找到对应的 线性起始地址 再 加上 偏移地址 计算出 线性地址,在 保护模式未开启分页的情况下 线性地址就直接对应到物理地址,如果在 开启了分页机制 通过分页映射到一个物理地址。
CPU 分页机制
页内部连续的线性地址映射到连续的物理地址中
32位 下 分页大小为 4Kb.CPU分页 要借助 页表,页表 存放在内存中
保护模式下,控制寄存器CR0的最高位PG位控制分页的开启 PG =1 此时通过页表才能把线性地址转换为物理地址,
PG = 0 分页机制无效,线性地址就直接作为 物理地址。
每个任务可以有自己的页目录表和页表
为了节约内,8086 CPU 的设计者将线性地址通过页目录表和页表两级查找转换成物理地址。
-
32模式下线性地址通过3个部分组成:
-
最高10位 Directory 页目录表偏移量
-
页目录表的大小为4KB(32位模式下),记录一个地址需要4K在32位模式下,如果 内存被拆成 4KB 为最小单位,那么 4G = 2 的 20次方 * 4KB = 如果页表是一个大数组,每个页数组记录连续4KB的起始位置,那么 每个地址32位地址栈4个字节 需要 = 4M的大小存放,Intel觉得这样太耗费空间了,将其拆分成 2个表 通过计算编程一个线性地址。
下面来个直观的例子:
-
-
中间10位 Table 页表偏移量
-
最后12位Ofsset是屋里也内侧字节偏移量。
总结 : 1个一目录占 4个字节,每条记录 栈4个字节 32 位 其中只有 前10位表示表示页表的起始地址一个进程都会有自己独立的页目录,一个页目录对应多个 页表,通过线性地址的 前 10位找到页目录的起始索引 -> 通过线性地址的后 10位 取得页表的索引,通过页表的索引的20bit 索引到物理页的 地址 -> 线性地址的最后12位为 4K内的偏移
在物理页内找到的 20位 + 线性地址最后12位 这样就能索引到 4G的内存。
值得注意的是,页表不需要一开始就分配,当CPU 执行程序 查找页表时没分配时触发系统异常中断,有操作系统处理再行分配,同时页表可以放在内存不连续处。
-
一、虚拟地址、物理地址、逻辑地址概述
物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
虚拟内存(virtual memory)
这是对整个内存(不要与机器上插那条对上号)的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;
之所以是这样,是因为现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。这个“转换”,是所有问题讨论的关键。
有了这样的抽像,一个程序,就可以使用比真实物理地址大得多的地址空间。(拆东墙,补西墙,银行也是这样子做的),甚至多个进程可以使用相同的地址。不奇怪,因为转换后的物理地址并非相同的。
——可以把连接后的程序反编译看一下,发现连接器已经为程序分配了一个地址,例如,要调用某个函数A,代码不是call A,而是call 0x0811111111 ,也就是说,函数A的地址已经被定下来了。没有这样的“转换”,没有虚拟地址的概念,这样做是根本行不通的。
打住了,这个问题再说下去,就收不住了。
逻辑地址(logical address)
Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说的连接器为A分配的0x08111111这个地址就是逻辑地址。
——不过不好意思,这样说,好像又违背了Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些”
32位保护模式下 开启分页:
;========================================;
; 开启分页机制
; 加载页到1M以上空间,页目录地址:0x100000 1M处
; 页表开始位置:0x101000 1M + 4KB(一个页目录)处
;
;========================================;
SwitchPage:
xor edx,edx ;除法初始化edx寄存器
mov eax,[ddMemSize] ;获取内存大小
mov ebx,0x400000 ; 一个页表 有1024 项 每一项 表示4KB 所以 是 1024 * 4KB = 4MB 一个页表能表示4MB大小
div ebx ;计算 你当前拥有内存需要几个页表 才能表示 内存大小/每页页表所能表示的大小
mov ecx,eax ;ecx 表示 所需页表PDE的格式
test edx,edx ;如果 不能整除 就要再添加一个页表
jz .no_remainder;如果 能整除
inc ecx ;多加一个 表
.no_remainder:
push ecx
;首先初始化页目录
mov ax,SelectorData
mov es,ax
mov edi,PDE_ORIGIN_ADDRESS ;页目录起始位置 页目录占4K
xor eax,eax
;分页的 地址 和属性 占 32字节 前20字节指向物理页的前10位页表起始位置 前 20位 页存在属性 | U/S 属性位值,用户级 | R/W 属性位值,读/写/执行
mov eax,PTE_ORIGIN_ADDRESS | PG_P | PG_US_U | PG_RW_W
.SetupPDE:
stosd
add eax,0x1000 ;每个页表地址 相差4K 4096个字节
loop .SetupPDE ;循环 直到ecx为0
pop eax ;取出页表个数
mov ebx,0x400;每个页表可以存放 1024 个PTE
mul ebx ;页表个数 * 1024 得到需要多少个PTD
mov ecx,ebx ;eax = PTE个数,用于循环
mov edi,PTE_ORIGIN_ADDRESS ;页表首地址
xor eax,eax
;页表从0 开始
mov eax,PG_P | PG_US_U | PG_RW_W
.SetupPTE:
stosd ;将ds:eax -> ds:edi
add eax,0x1000 ;页指向 物理内存 从 0 ,4K ,8K ....;
loop .SetupPTE
;设置CR3 寄存器 和CR0 开启分页机制
mov eax,PDE_ORIGIN_ADDRESS
mov cr3,eax
mov eax,cr0;将CR0 PF位置位
or eax,0x80000000
mov cr0,eax
jmp short .SetupWating
;给CPU 一点延迟 让分页机制 生效
.SetupWating:
nop
nop
ret