如果程序中的地址用的是物理地址,那么程序就必须在特定的内存位置才能执行,换个位置就不能执行了:
一个程序在内存中换个地方就不能正确执行了,这岂不是笑话。况且现在的程序在哪块内存中执行都是操作系统分配的,程序本身是不能决定的。
为了解决上述问题,就出现了逻辑地址:
“程序起始地址”就是所谓的段地址。段地址由操作系统存分配,用户程序只需使用偏移地址,CPU执行时会自动将偏移地址加上段地址转化成物理地址。“段地址:偏移地址”的形式称为逻辑地址。
逻辑地址并不是唯一的。例如,0000:7c00,也可以写成7c00:0000,因为它们的物理地址都是7c00+0000=7c00,而偏移地址起始为0在编程的时候比较方便。
实际上,现在的一个程序有好几个段,如数据段用来存储数据、代码段用来存储代码、栈段用来存储栈等,为了编程方便这些段的偏移地址都是0。
中断发生时,处理器根据中断号从中断向量表中取出处理程序的逻辑地址,保护现场后跳转到中断处理程序执行。
没有保护模式之前,用户程序可以随意修改段寄存器,这就意味着用户程序可以访问和修改内存中所有的数据,包括中断向量表、其它程序的数据、操作系统的数据等。例如:
mov ax,0 ;段地址为0的一段区域,是中断向量表
mov ds,ax
mov byte [0x30],0
这种修改可能是无意的,比如程序编写错误,也可能是有意的,比如木马程序。所以程序的权限必须加以限制。
为了解决程序可以直接设置段寄存器,从而访问任意内存的问题,提出了保护模式。
在保护模式下,用描述符对每个段的长度和特权级进行指定。如果访问超过段界限的内存区域时,处理器会引发异常中断。
操作系统加载应用程序时,会为其在描述符表中创建描述符。在跳转到应用程序时,跳转指令会直接或间接的给出段地址,也就是修改了段寄存器。
保护模式虽然可以保证应用只能访问自己的内存,但还有几个问题没有解决:
为了解决以上问题,引入特权级概念,特权级的原则如下:
引入特权级后,再来回看未解决的问题:
操作系统为了管理多个任务,会为每一个任务创建任务控制块TCB,在TCB中记录了该任务的详尽信息。所有的任务任务控制块TCB都会被放到一个链表中成为TCB链表。
任务控制块的内容是操作系统决定的,不同的操作系统各有不同,但一定包括TSS和LDT的地址。TSS用来保存该任务的寄存器和栈的信息,当从其它任务切换回该任务时,会从TSS中恢复寄存器和栈。LDT是局部描述符表,作用GDT一样,是用来限制访存的范围,多任务时GDT只保存操作系统的描述符,每个任务的描述符会保存在各自的LDT中。
保护模式下的中断处理原理上与实模式的类似,都是根据中断号找对应的处理函数,不同的是保护模式要引入保护机制,所以增加了GDT/LDT表,中断描述符表变复杂了,在内存中的位置由寄存器IDTR指定。
因为段的大小不一,系统运行一段时间后,会出现有很多小的空间,虽然总的剩余空间很多但是这些小空间都不连续,无法利用,造成浪费。还有一个问题是,连续分配内存不能动态的增加。
为了解决这个问题,引入了分页机制,把内存划分为大小相等的页。每个段可以包含多个页,因为多个页可以不连续,所以可以充分利用内存。对于需要动态增加内存的程序,也可以随时增加页。
开启分页机制后,原来的分段机制还在,相当于在分段的基础上再做分页。
为了根据线性地址找到页的物理地址,操作系统必须维护一张表,把线性地址转换成物理地址,单级页表会导致页表占用的内存较大,所以通常使用多级页表:
分页后还有一个问题,用户任务调用内核服务必须能够访问GDT才行,现在分页后,任务的页目录中只有任务的页表,任务是根本访问不到GDT。而且切换到内核是不切换CR3寄存器的,所以每个任务都必须能访问到内核。
多级页表可以很方便的解决这个问题。想让用户任务能访问内核的内存,只需要将内核的页表添加到用户任务的页目录中即可。
参考: