第五章 保护模式进阶,向内核迈进

获取物理内存容量

学习Linux获取内存的方法

通过调用BIOS中断0x15实现,分别是BIOS中断0x15的三个子功能,子功能号要存放到寄存器EAX或AX中:
在这里插入图片描述

BIOS中断是实模式下的方法,只能进入保护模式前调用。可以在实模式下用这三种方法检测完内存容量后再进入保护模式。

BIOS中断可以返回已安装的硬件信息,由于BIOS及其中断也只是一组软件,它要访问硬件也要依靠硬件提供的接口。所以,获取内存信息,其内部是通过连续调用硬件的应用程序接口(API)来获取内存信息的

利用BIOS中断0x15子功能0xe820获取内存

BIOS中断0x15子功能0xe820能够获取系统的内存布局。
系统内存各部分的类型属性不同,BIOS按照类型属性划分这片系统内存,在查询时也是按类型返回内存信息。子功能0xe820能够返回多个属性字段。

地址范围描述符结构(ARDS)用于存储描述内存信息内容的地址范围描述符。每次int 0x15之后,BIOS就会返回这样一个结构的数据,这个数据描述了一个类型的内存范围信息。正常情况下,不会出现较大的内存区域不可用情况,所以在所有返回的ARDS结构里,此值最大的内存块一定是操作系统可使用的部分,即主板上配置的物理内存

在这里插入图片描述

BIOS中断0x15子功能0xe820调用方法:

  1. 填写好“调用前输入”中列出的寄存器;
  2. 执行中断调用 int 0x15。
  3. 在CF位为0的情况下,“返回后输出”中对应的寄存器便会有对应的结果。

在这里插入图片描述

利用BIOS中断0x15子功能0xe801获取内存

最大只能识别4GB内存。
检测到的内存分别放到两组寄存器中。
低于15MB的内存以1KB为单位大小来记录,单位数量在寄存器AX和CX中记录。实际内存容量=AX1024。
16MB~4GB是以64KB为单位大小来记录,单位数量在寄存器BX和DX中记录。实际内存容量=BX
64*1024。

子功能0xE801的调用方法:

  1. 将AX寄存器写入0xE801。
  2. 执行中断调用int 0x15。
  3. 在CF位为0的情况下,“返回后输出”中对应的寄存器便会有对应的结果。

在这里插入图片描述

利用BIOS中断0x15子功能0x88获取内存

只能识别最大64MB的内存。即使内存容量大于64MB,也只会显示63MB。此中断只会显示1MB之上的内存,不包括这1MB。

0x15子功能0x88的调用方法:

  1. 将AX寄存器写入0x88。
  2. 执行中断调用int 0x15。
  3. 在CF位为0的情况下,“返回后输出”中对应的寄存器便会有对应的结果。

在这里插入图片描述

实战内存容量检测

内存容量检测(实验)

启用内存分页机制,畅游虚拟空间

内存为什么要分页

CPU加载内存段的过程:
段描述符表描述了对应段的具体信息。CPU允许在描述符表中已注册的段不在内存中存在。当段不存在时,CPU会抛出异常引发中断,中断将相应的段从外存(如硬盘)中载入到内存,再将标志位置1(即段存在),再让CPU重新检查。

在这里插入图片描述

内存段移出到外存上:

当内存不够用时,就可以将使用频率低的段换出到硬盘,等用到的时候再从硬盘中载入到内存。这样就可以腾空一直占用内存却不经常使用是进程的段了。(CPU访问内存是用段基址定位的,如果段在内存中,就会一直占用一部分内存,但是如果它被移到硬盘,就不占内存了,而该段中的进程的地址是相对的,只要地址的相对位置不变,就不会影响执行。内存中的数据是二进制,段被换出到硬盘上以二进制形式存储,数据内容都是一样的,只是存储介质不同而已,无非就是一段二进制数据在内存和外存之间拷贝来拷贝去而已。)

段描述符A位由CPU置1,但清0工作是由操作系统完成的,操作系统每发现该位为1就将该位清零,这样就可以统计该段的使用频率。当物理内存不足时,可以将使用频率低的段换出到硬盘,以腾出内存空间给新的进程。当段被换出到硬盘后,操作系统将该段描述符的P位置置0。下次执行时,如果访问这个段就回到了上述CPU加载内存段的过程。

不足:
虽然能解决内存不足的问题,但是线性地址连续而物理地址不连续,这样的话就会执行的很慢。因此需要将线性地址映射到任意物理地址。也就是分页机制所做的事。

一级页表

没有分页机制的时候,CPU默认线性地址=物理地址,线性地址就是“段基址:段内偏移地址”。打开分页机制之后,段部件输出的线性地址不等于物理地址,我们称之为虚拟地址(线性地址)。CPU要想拿到物理地址需要在页表中查找。

在这里插入图片描述
在这里插入图片描述

分页机制原理:

线性地址就是“段基址:段内偏移地址”,为了方便寻址提高效率,线性地址是连续的。在分页机制之前,线性地址等于物理地址,但是这样的话就只能让物理地址也连续。但是由于物理内存不够,需要将段移来移去,不能保证物理地址连续。为了让线性地址能映射到任意物理地址,添加了分页机制。其实就是将线性地址与任意物理内存地址关联起来。这个时候线性地址不等于物理地址,所以也叫虚拟地址。

4GB的线性地址空间先映射到4GB的虚拟地址空间,在这里分页机制将大小不同的段拆分成大小相同的以页为单位的小块内存,操作系统为这些虚拟页分配真实的物理内存页,它查找物理内存中可用的页,然后在页表中登记这些物理页地址,这样就完成了虚拟页到物理页的映射,每个进程都以为自己独享4GB地址空间

在这里插入图片描述

分页机制的本质就是将大小不同的大内存段拆分成大小相等的小内存块。
内存块数 * 内存块大小=4GB。
页表所占内存=页大小 * 内存块数。
(要使得页表所占内存尽可能小且能包含4GB(32位)的地址)

CPU中采用的页大小为4KB,4GB地址空间被划分成4GB/4KB=1M个页。
在这里插入图片描述

一级页表模型:

一级页表

用线性地址找到页表中对应的页表项:
将页表的物理地址加载到寄存器cr3中,页表中页表项的地址就是物理地址。用线性地址的高20位作为页表项的索引。
在这里插入图片描述
地址转换:
在这里插入图片描述
用线性地址的高20位在页表中索引页表项,用线性地址的低12位与页表项中的物理地址相加,所求的和便是最终线性地址对应的物理地址。
因为页个数有20位,页大小为12位。那么索引范围就是20位,就用高20位表示,索引到了页的基地址,剩下的12位则用来填4KB大小的内存地址,在这个基地址下可以放12位的地址个数。也可以看作是“页基址(高20):页内偏移地址(低12)”
在这里插入图片描述

二级页表

为什么需要二级页表的原因:
一级页表占用空间大,表示的内存少。页表项需要提前建好。

在这里插入图片描述

标准页尺寸是4KB,4GB线性地址空间最多有1M个标准页。一级页表将1M个标准页放置在一张页表中,二级页表将1M个标准页平均放置在1K个页表中。二级页表使用页目录表存储页表,存储在页目录表中的页表的物理地址叫页目录项(PDE),4字节。页目录表大小=1K*4B=4KB。

在这里插入图片描述

在这里插入图片描述

二级页表转换:
页目录表-页目录项(页表地址)-页表-页表项(物理地址)
2 ^ 10 * 2 ^ 10 * 2 ^12
第31-22位定位页表,第21-12位定位物理页,第11-0位用于页内偏移量。

在这里插入图片描述
在这里插入图片描述

页表目录和页表项:

在这里插入图片描述
在这里插入图片描述

启动分页机制:

在这里插入图片描述

寄存器cr3用于存储页物理地址,又称其为页目录基址寄存器(PDBR)

在这里插入图片描述
启动分页机制的开关是将控制寄存器cr0的PG位置1。

规划页表之操作系统与用户进程的关系

当用户进程需要访问硬件相关的资源时,需要向操作系统申请,由操作系统去做,之后将结果返回给用户进程。进程可以有无限个,但操作系统只有一个,所以操作系统必须“共享”给所有用户进程。
用户进程相当于是一个半成品,还有部分功能需要操作系统,配合上操作系统才能实现一个完整的程序。

在这里插入图片描述

页表的设计是根据内存分布情况来决定的。在用户进程4GB虚拟地址空间的高3GB以上的部分划分给操作系统,0~3GB是用户进程自己的虚拟空间。为了实现共享操作系统,让所有用户进程3GB ~ 4GB的虚拟地址空间指向同一个操作系统,也就是所有进程的虚拟地址3GB ~ 4GB本质上都是指向的同一片物理页地址,这片物理页上是操作系统的实体代码。(每个进程都有4GB虚拟地址)

启用分页机制

启用分页机制(实验)

用虚拟地址访问页表

进入分页机制后,访问任何物理地址都需要通过虚拟地址进行。

页表是一种动态的数据结构,可以往里面添加页表项(申请内存),或者对某个页表项清零(释放内存)。

用虚拟地址访问页表自身:

  1. 让虚拟地址直接与物理地址一一对应。
  2. 让虚拟地址与物理地址乱序映射。

开启分页之后,我们可以使用 info tab 命令查看虚拟地址与物理地址的映射。cr3寄存器显示的就是页目录表的物理地址。
在这里插入图片描述

快表TLB简介

在这里插入图片描述

加载内核

用C语言写内核

汇编语言和机器指令几乎是一对一的,即一名汇编代码只对应一句具体的机器码,不会有更多对应的选择,所以可以认为汇编指令就是机器指令。C语言的编译过程是先将C语言代码转换成汇编代码,然后再将汇编代码转换成机器指令。所以用C语言写出来的程序,最终可以转换成对应的一句或多句汇编指令。

生成C语言程序的过程:
先将源程序编译成目标文件,再将目标文件链接成二进制可执行文件。
gcc -c -o file.o file.c

在这里插入图片描述
经过gcc编译后的目标文件是待重定位文件,文件中的符号还没有安排地址,需要等和其他文件一起“组成”可执行文件时再重新定位。

在这里插入图片描述

链接:

ld main.o -Ttext 0xc0001500 -e main -o kernel.bin

-Ttext 指定起始虚拟地址为0xc0001500
-o 指定输出的文件名
-e指定程序的起始地址,其参数可以是数字形式的地址,也可以是符号名,指定程序是从哪里开始的。也就是提供程序的入口。
由于程序内的地址是在链接阶段编排的,所以在链接阶段必须明确入口地址才行,于是链接器规定,默认只把名为_start的函数作为程序的入口地址,即默认的entry symbol是_start,除非另行指定。

二进制程序的运行方法

任何应用程序都需要被载入到内存后才能运行,这是CPU等其他硬件的运行机制决定的,不过它们通常位于磁盘等外存设备中,在使用时,需要从外存中将其调入到内存后才行。

加载用户程序就是通过操作系统去调用它。在此之前,我们都是把应用程序放在固定位置方便寻找,但是这样不够灵活。由于每个程序是单独存在的,所以程序的入口地址信息需要与程序绑定,最简单的办法就是在程序文件中专门腾出个空间来写入这些程序的入口地址,主调程序在该程序文件的相应空间中将程序的入口信息读出来,将其加载到相应的入口地址,跳转过去就行了。

所以就有了文件头(应该就是类似于头文件吧)。文件头用来描述程序的布局等信息,它属于信息的信息,也就是元数据。将这种具有程序头格式的程序文件从外存读入到内存后,从该程序文件的程序头读出入口地址,需要直接跳进入口地址执行,跨过程序头才行。

在这里插入图片描述

elf格式的二进制文件

ELF是指可执行链接格式。在ELF规范中,把符合ELF格式协议的文件统称为“目标文件”或ELF文件。
在这里插入图片描述

段和节是程序中最重要的部分。段是由节来组成的,多个节经过链接之后就被合并成一个段来。段和节的信息也是用header来描述的,程序头是program header,节头是section header。程序中段的大小和数量是不固定的,节的大小和数量也不固定,因此使用程序头表和节头表来描述它们,这两个表汇总了程序头和节头,表中元素是头信息。

在表中,每个成员都统称为条目,一个条目代表一个段或一个节的头描述信息。对于程序头表,它本质上就是用来描述段的。

需要在一个固定的位置,用一个固定大小的数据结构来描述程序头表和节头表的大小及位置,这个数据结构便是ELF header,它位于文件最开始的部分,并具有固定大小。

EFL文件是个用来描述各种“头”的“头”,程序头表和节头表中的元素也是程序头和节头。elf文件格式的核心思想就是头中嵌头,是种层次化结构的格式。

ELF文件分为文件头和文件体两部分。先用个ELF header从“全局上”给出程序文件的组织结构,概要出程序中其他头表的位置大小等信息,如程序头表的大小及位置、节头表的大小及位置。然后,各个段和节的位置、大小等信息再分别从“具体的”程序头表和节头表中予以说明。

ELF文件格式的作用体现在两方面,一是链接阶段,另一方面是运行阶段。无论是在待重定位文件还是可执行文件中,文件最开头的部分必须是 ELF header。接着是程序头表,这对可执行文件是必须存在的,而对待重定位文件是可选的。其他成员位置要取决于各头表中的说明。
在这里插入图片描述
在这里插入图片描述

ELF header 结构:
在这里插入图片描述

e_ident[16]是16字节大小的数组,用来表示elf字节等信息,开头的4个字节是固定不变的,是elf文件的魔数,它们分别是0x7f,以及字符串ELF的ASCII码:0x45,0x4c,0x46。

e_type占2字节,是用来指定elf目标文件的类型。

e_machine占2字节,用来描述elf目标文件的体系结构类型,也就是说要在哪种硬件平台上才能运行。

在这里插入图片描述

程序头表中的条目的数据结构:
在这里插入图片描述
在这里插入图片描述

elf文件实例

在这里插入图片描述
(划细线属于elf header范围,粗下划线属于program header 范围)

在这里插入图片描述

在这里插入图片描述

将内核载入内存

将内核载入内存(实验)

特权级深入浅出

特权级的那点事

计算机可以分为两部分,访问者和受访者。访问者是动态的,具有能动性,它主动去访问各种资源。受访者是静态的,它是被访问的资源,只能等着访问者访问。访问者的特权级可以变,受访者的特权不能变。

建立特权机制是为了通过特权来检查合法性,检查访问者的特权级和受访者的特权级是否匹配。

特权级按照权力从大到小分为0、1、2、3级,0级特权能力最大,3级特权能力最小。0级特权是我们操作系统内核所在的特权级。

在这里插入图片描述

TSS简介

TTS是任务状态段,它是处理器在硬件上原生支持多任务的一种实现方式。TTS是一种数据结构,它用于存储任务的环境。
TTS是每个任务都有的结构,它用于一个任务的标识,相当于身份证,程序拥有此结构才能运行,这是处理器硬件上用于任务管理的系统结构,处理器能够识别其中每个字段。

在这里插入图片描述

在没有操作系统的情况下,可以认为进程就是任务,任务就是一段在处理器上运行的程序。有了操作系统之后,程序分为用户部分和内核部分。一个任务按特权级来划分的话,实质上是被分成了3特权级的用户程序和0特权级的内核程序,这两部分加在一起才是能让处理器完整运行的程序,也就是说完整的任务要经历这两种特权的变换。

每个任务的每个特权级下只能有一个栈,不存在一个任务的某个特权级下存在多个同特权级栈的情况。一个任务最多有4个栈。一个任务可以拥有的栈的数量取决于当前特权级是否还有进一步提高的可能,即取决于它最低的特权级别。

一个TSS中只有3个栈:ss0和esp0,ss1和esp1,ss2和esp2,它们分别代表0级栈的段选择子和偏移量、1级栈的段选择子和偏移量、2级栈的段选择子和偏移量。因为除了调用返回外,处理器只能由低特权级向高特权级转移,TSS中所记录的栈是转移后的高特权级,只用于向更高特权级转移时提供相应特权的栈地址。当由高特权返回低特权级的情况,处理器是不需要在TSS中去寻找低特权级目标栈的。当处理器由低向高特权级转移时,它自动地把当时低特权级的栈地址压入了转移后的高特权级所在的栈中,所以,当用返回指令如retf或iret从高特权级向低特权级返回时,处理器可以从当前使用的高特权级的栈中获取低特权级的栈段选择子及偏移量。由高特权级返回低特权级的过程称为“向外层转移”。

CPL和DPL入门

访问者就是指令,指令的访问请求看他有没有特权级,特权级就在代码段寄存器CS中选择子的RPL位的值。在任意时刻,当前特权级CPL保存在CS选择子中的RPL部分。代码是执行者,它表示访问的请求者,所以CPL只存放在代码段寄存器CS中低2位的RPL中。
CPL是当前CPU所处的特权级,也就是当前特权级。


在这里插入图片描述

DPL是描述符特权级,是段描述符所代表的内存区域的“门槛”权限。访问者任何时候都不允许访问比自己特权更高的资源。对于受访者是数据段来说,只有访问者的权限大于等于该DPL表示的最低权限才能够继续访问。对于受访者是代码段来说,只有访问者的权限等于该DPL表示的最低权限才能够继续访问,即只能平级访问,任何权限大于或小于它的访问都将被CPU拒之门外。

唯一一种处理器会从高特权级降到低特权级运行的情况:处理器从中断处理程序中返回到用户态的时候。中断处理都是在0特权级下进行的。

**一致性代码段:**转移后的特权级与转移前的特权级一致。
在这里插入图片描述

门、调用门与RPL序

门结构用来记录一段程序起始地址的描述符。
门描述符用来描述一段程序,同段描述符类似,都是8字节大小的数据结构。
门是用来实现从低特权级的代码段转向高特权级的代码段。

门的“门槛”的访问者的下限,访问者的特权级再低也不能比门描述符的特权级DPL低。否则访问者连门都进不去。门描述符相当于数据段描述符一样,只允许比自己特权级高或相同特权级的程序访问。
门的“门框”是访问者特权级的上限,访问者的特权级再高也不能比门描述符中目标程序所在代码段的DPL高,否则就变成特权级由高转低了。
门结构存在的目的就是为了让处理器提升特权级,这样处理器才能够做一些低 特权级下无法完成的工作。

在这里插入图片描述
在这里插入图片描述

调用门执行流程:

在这里插入图片描述

调用门的过程保护

调用门的过程:从特权级3到特权级0
用户进程为调用门提供两个参数,将这两个参数压入特权级3栈中->确定新特权级使用的栈->检查新栈段选择子中的描述符的DPL和TYPE->切换到新栈,将旧栈段选择子和指针临时保存->使用新栈后压入旧栈段选择子和指针->压入之前的两个参数->压入当前代码段的CS和EIP->把门描述符中的选择子加载到代码段CS中,偏移量装载到EIP中。
在这里插入图片描述

调用门返回过程:

在这里插入图片描述

在这里插入图片描述
(就是在从低到高时把DS的权限提高了,但是返回变成从高到低时又忘记改回来了,这样DS就能访问高特权的数据了,越权。于是往里面填0,第0个段描述符是不可用的,这样处理器就可以引发异常)
在这里插入图片描述
在这里插入图片描述

RPL的前世今生

当用户程序需要访问比他特权级高的数据时,他就需要调用门或者其他使它可以访问到这个数据,但是这个数据其实不是由它直接访问的,因为它没有这个权限,它是让特权级更高的内核来帮忙访问然后写入特定缓存中。
当处理器需要内核数据时,它要变成0特权级,而此时它就可以为所欲为,缺乏安全保证。所以必须弄清楚真正的请求者是谁,内核程序只是代替用户程序来拿数据的。
在这里插入图片描述

RPL是请求特权级,它代表真正请求者的特权级,代表真正资源需求者的CPL。RPL的引入是为了避免低特权级的程序访问高特权级的资源。
在这里插入图片描述
在这里插入图片描述

使用arpl指令来修改选择子中的RPL。
在这里插入图片描述

在这里插入图片描述

CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。
RPL是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。
DPL存储在段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。当进程访问一个段时,需要进程特权级检查,一般要求DPL >= max {CPL, RPL}。
也就是说,CPL是当前程序的权限,RPL是对段访问的权限(请求者的权限),DPL是访问这个段需要的权限(访问者权限)。在访问段(DPL)时,虽然内核(CPL)可以访问,但是它其实是替用户程序(RPL)访问的,但用户程序是不能访问的
在这里插入图片描述

IO特权级

除了代码访问和数据访问有特权级,指令和IO口读写也有特权级。有些指令只有在0特权级下才能使用,如hlt,lgdt,lidt,ltr,popf等。IO读写特权由标志寄存器eflags中的IOPL位的IO位图决定,IO相关的指令只有在当前特权级(CPL)大于等于IOPL时才能执行。
在这里插入图片描述

**eflags寄存器:**每个任务都有自己的eflags,其中的IOPL表示了当前任务要想执行全部IO指令的最低特权级。

在这里插入图片描述

IOPL设置:

在这里插入图片描述
驱动程序:

在这里插入图片描述
IO位图: IOPL是所有IO端口的开关,如果开关被关上,则可以通过IO位图来设置部分端口的访问权限

位图就是一个用bit映射到某个实际的对象。位图这种结构的操作单位就是bit,所以位图就是一串二进制01数字,对位图的操作也就是读写相应的位,处理器中对内存的访问是以字节为单位的,不能直接操作位,所以要先将该位所在的字节读到内存,若是想将该位置1,可用1对该位进行或操作,若想将该位清0可以用0对该位进行与操作。

位图中的每一bit代表一个端口。0表示可以访问,1表示禁止访问。IO位图只有在CPL>IOPL才有效

在这里插入图片描述

TSS如果有位图将位于TSS顶端,如果偏移地址不在此范围则表示没有IO位图。

在这里插入图片描述

**IO位图结尾的0xff:**0xff就是说这几个端口都不能访问。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值