Linux内核学习(一)8086编程模型

本文主要介绍Intel8086系列的编程模型,包括分段与分页机制、任务切换过程以及中断处理系统。作为Linux内核学习曲线的起点,本文的侧重点在于对于每个主题,硬件上是如何实现的,以及为软件(操作系统)提供了什么样的接口。本文不会介绍x86汇编语言的知识,有关知识请自行参考资料。

需要说明的是,Linux内核基于80x86,因此需要遵循8086的编程模型,但在很多地方实现了自己的策略。因此,如果发现8086中很多讲解的典型场景有与Linux内核不一致的地方,请不要惊讶。

本文主要参考《Intel80386 Programmers’ perference manual》。

1. 分段机制的安全模型

80x86的保护模式和分段机制有效结合,形成了一套安全体系。

1.1 名词解释

分段机制:指令和数据的地址由段基地址和段内偏移两部分组成。有关分段和分页的概念,请参考任一本操作系统或80x86汇编或架构方面的书籍。

保护模式:保护模式下系统拥有一张段描述符表,寻址时以段寄存器值为索引,查找段描述符表获得段基地址。段描述符包含段基地址、段界限长度、段特权级DPL。其中,段界限和DPL共同决定了保护模式下的安全机制。

特权级:一个取值0~3的数字,用来标识当前CPU或者某个代码段/数据段的特权级。CPU当前特权级称为CPL;代码段或数据段或者其它类型段的特权级值位于段描述符里,称为DPL。特权级值越小,特权级越高。因此,0最高,3最低。

1.2 基本安全机制

如果所有的代码均顺序执行,并且不需要访问其它地方的数据,那么也就没有必要进行保护了。因此,安全机制包含两层含义:访问数据;指令跳转。

8086以段为安全对象,因此如果访问的数据或者跳转的指令与原指令在同一段,只需要检查是否超过段界限长度即可。

针对跨越段的数据访问,有如下策略:

规则1:如果访问的是数据段,那么只能高权限代码访问低权限的数据。因此需要CPL <= DPL。此外,段寄存器不仅保存了段索引,还保存了2bit的RPL,在访问数据时还要保证RPL<=DPL。[为什么这样要求,留到后面讲述调用门时说明。这其实跟操作系统的权限代理有关]。

规则2:如果访问的是代码段(谈的是数据访问,怎么会访问代码段?考虑到有时候在代码段存放一些只读的数据),那么需要考虑目标代码段的一致性1。如果是一致性代码段,那么访问总是成功;如果是非一致性代码段,那么需要遵从规则1。

针对跨越段的指令跳转,有如下策略:

规则1:如果跳转到非一致性代码段,那么要求CPL = DPL。也就是说,不允许跨越特权级的代码跳转。这也很好理解,要是允许跨越特权级跳转,那我随便跳到高特权级的代码段,进而也就能随便地访问数据了,谈何保护。

规则2:对于某些场景(比如说用户级代码需要用到系统态的一些公共服务,一般情况下还是要跳转回去的),会允许低特权级代码跳转到高特权级代码,也就是CPL >= DPL。但是为了防止与规则1冲突,在跳转之后CPL不能变,不能因为暂时的跳转而升级代码的权限。因为CPU的当前特权级没有改变,所以这样的目标代码段,称作一致性代码段

聪明的读者发现没有,如果按照上面列出的安全规则,CPU在运行时的特权级CPL好像从来都没有变化过:要么访问特权级一样的非一致性代码段,要么访问特权级高但是CPL保持不变的一致性代码段。那说好的用户态和内核态呢?别急,Intel程序员已经考虑到了!答案就是门!

1.3 调用门Call Gate

门(Gate)是一种特殊的段。有多特殊?你只需要为它定义好段描述符就好,在内存中不再占用其它空间,这点不像数据段或者代码段。程序世界的“门”就像是科幻电影里的门一样:拿粉笔在墙上(位于现在所处的位置)画一个形状(定义门描述符符),然后拧开门把手就能进入另外一个世界(另外一个代码段)!

言归正传,如果我们想跳转到特权级高的代码,而且要求CPL同时改变成新代码段的DPL,怎么办?只需要为目标代码段定义一个Call Gate。Call Gate描述符格式如下:

OFFSET[31:16]

DPL

COUNT

目标段选择子

OFFSET[15:0]

其中,OFFSET定义了目标指令在目标段内偏移地址;DPL是调用门的DPL,不是目标段的DPL,后者位于目标段的段描述符。

调用CALL指令,调用门的访问规则是这样的:

规则1:将调用门看作是数据段(因为你只需要访问它),所以你需要CPL <= DPLgate

规则2:CPL>= DPLtarget

跳转之后,CPL变成DPLtarget,而不管目标代码段是一致性还是非一致性。

调用JMP指令,则只有目标代码段是一致性时才适用以上规则,否则只有CPL与DPLtarget相等才能跳转。

好了,现在可以讲清楚RPL是干嘛的了!利用CallGate会造成CPL的变更,或者更准确地说,代码权限得到了提升。还是这个场景:用户态A需要调用内核态B的公共服务接口。这时候,内核态(高特权级)的代码是代理者,代理用户态程序去执行某项服务,可能需要访问其它一些数据或代码C。而正好这部分数据或代码C是关键代码,是不允许A访问的,那么内核态就需要知道原来用户态A的权限,而这个权限就保存在访问C时的段寄存器(或者叫段选择子)里。

2. 任务切换

现代操作系统必须要支持多任务并发。Intel在设计80386时也提供了快速切换任务的机制。这种机制要求1)任务切换迅速;2)对原来的任务要维护现场,能够返回到原来任务继续运行。本节主要描述如何实现任务切换机制。

2.1 名词解释

任务状态段TSS

保存了管理某个任务的所有信息,包括所有寄存器值(通用寄存器/段寄存器/EFLAGS)、CR3(页目录地址)、栈顶指针(ESP0~2,分别对应特权级0~2)、IO映射基址等。此外,为了能够切换回之前的任务,TSS还包括之前任务的TSS地址。


TSS描述符


TSS作为一个段,在GDT中当然要有对应的段描述符啦,就是TSS描述符,格式如上所示。有以下几点比较有意思,值得提一下:

P.1 DPL字段指定了访问TSS描述符需要的权限,要求CPL<=DPL。能够访问TSS描述符,意味着能够执行任务切换。

P.2 TYPE字段指定了该TSS代表的任务是否已经在运行。CPU决不会切换到一个已经在运行的任务。即每个任务在系统中只能有一个实例。这跟一个程序在系统中有多个进程不同,同一个程序的每次运行都产生一个不同的进程(任务)。

P.3 能够访问TSS描述符不代表能够读或修改TSS段。要想修改TSS段,需要重新定义一个与该TSS段拥有相同地址空间的可读或可写的数据段。

TR寄存器

TR寄存器保存了TSS描述符的段索引值。跟所有的段寄存器一样,TR寄存器也包含可见部分和隐藏部分:16bit可见部分保存索引值;64bit隐藏部分缓存了当前任务的TSS描述符。这样,在任务不发生切换的情况下,只需要访问TR寄存器的隐藏部分即可获得TSS的地址,无需去访问GDT。

LTR/STR命令分别用来加载/保存TR寄存器的值。

任务门


首先需要明确的是,一个门一般是一个段描述符表(GDT / LDT / IDT)的表项。任务门作为一个段描述符表项,它的结构如上图所示。图中,SELECTOR包含一个TSS描述符的索引,指向了GDT中的一个TSS描述符。因此,它的作用也是执行任务切换。既然TSS描述符能够执行任务切换,为什么要多此一举再创建个任务门呢?而且通过任务门访问一个TSS段,需要两次索引(任务门->TSS描述符->TSS段),不是显得更累赘吗?

还是那句话,计算机的世界里,存在即合理。任务门可以解决TSS描述符的几个局限性:

P.1TSS描述符只能位于GDT中,但TSSGate可以位于GDT,LDT,甚至IDT;

P.2一个TSS段一般只在GDT中定义一个TSS描述符,且为了安全起见,它的DPL一般定义为0。而TSSGate的DPL可以设定为任意值。而且,通过TSS Gate访问TSS时检查的是TSSGate的DPL,而不是TSS描述符的DPL。这就给操作系统极大的自由,去设置不同代码访问同一个TSS段的权限。(访问TSS Gate的权限:CPL<= DPL && RPL <= DPL)。

2.2 任务切换过程

考虑通常情况下的任务切换,即调用CALL/JMP命令,目的地址是一个TSS描述符或者一个TSSGate,包含3个步骤:

Step1权限检查和状态检查(新任务是否已运行);

Step2查看tr寄存器获得当前任务TSS地址。将当前任务的现场(所有寄存器值)保存到TSS。

Step3加载新任务的TSS描述符到TR寄存器。根据新任务TSS中寄存器的值恢复新任务的执行环境,同时新任务的状态寄存器的TS位(Task Switched)置1。

新任务和旧任务的运行级别没有任何关联。因为新任务的CPL来自于TSS段的CS段寄存器的RPL字段。

3. 中断机制

关于中断,听到最多的就是中断和异常。Intel官方文档是这样解释中断和异常的:中断(Interrupts)和异常(exceptions)是不同寻常的CPU控制转移方式,工作起来就像自行调用(无需编程)的CALL指令一样。它们打断正在执行的代码,转而去处理来自外部的事件或者来自内部的异常状况。中断和异常的差异也在此:一个来自CPU外部的事件,一个来自CPU执行某条指令时自己产生的异常。

中断分为两类:可屏蔽中断和不可屏蔽中断。这两种中断由中断源连接到CPU的引脚来决定。连接到CPU的INTR引脚的中断可屏蔽;来自NMI引脚的中断不可屏蔽。

异常也分为两类:处理器检测到的异常和编程引起的异常。前者好理解,例如除数为0、缺页、权限不够等等情况,按照严重情况又可以分为故障(fault)、陷阱(trap)和终止(abort);后者是指特殊指令引起的特定的异常,例如int3指令,bound指令,或者int0x80等等,一般又叫做“软中断”。

当然,不管是中断还是异常,都有一个中断号。不可屏蔽中断和异常的中断号为0~31,可屏蔽中断的中断号为32~255。

实模式下和保护模式下的中断机制有很大不同。在实模式下,内存的起始处(0x0)必须放置一张中断向量表,这样当中断发生时,CPU自动保存相关寄存器值后跳转到中断向量表定义的中断处理程序(ISR)。在保护模式下,更复杂一点,系统不再强制必须要在内存起始处设置中断向量表,而是采用类似描述符表的中断描述符表IDT。IDT可以放在内存的任何地方,只要在IDTR寄存器里保存地址就行。IDT的表项可以是中断门,陷阱门,或者任务门。

新概念很多,但不要着急,还是一点一点地来介绍,毕竟在计算机的世界里,被程序员创造出来的东西,总有它存在的意义。

3.1 中断门 & 陷阱门

从一个描述符表项跳转到某条指令,这跟CALL指令的过程很类似。IDT的表项应该是指向中断处理程序的起始地址,即要包含中断处理程序的所在段的段选择子和段内偏移,跟调用门的格式很像。因此,我们把IDT的这种表项称为中断门或陷阱门。

根据第2章的讨论结果,任何时候CPU均是代替某个任务在执行。这个任务的信息保存在TSS段,而TSS的地址则由tr寄存器指定。当中断发生时,这时候CPU要终止当前的任务,但又不会切换到另外的任务,只是跳转到一段特定的代码,即中断处理程序。因此,CPU在执行中断处理程序时仍然处在之前进程的上下文中。这其实关系到在ISR中CPU使用的栈,因为仍处在之前进程上下文中,所以使用的栈当然也是之前进程的栈。但是需要明确的是,虽然我们说此时CPU仍然处于被中断进程的进程上下文中,但是此时CPU绝不会访问该进程的任何数据和代码,只是用它的栈而已。为了以示区别,我们称此时CPU处于中断上下文。

第2章还提到,每个进程均拥有4个栈,分别对应4种特权级。所以,如果ISR的特权级与被中断进程的特权级不一致的话,就会发生栈的切换:从该进程的TSS段查找到对应特权级的栈的地址(SS:ESP),切换到新栈,为了处理完之后恢复现场,还要把旧栈地址、EFLAGS、CS:EIP的值压入新栈。

再说一下从中断返回时的过程。ISR执行IRET指令,可以返回到之前被中断的进程。这与调用CALL再调用RET返回类似,但也有所不同:RET返回之后EIP指向的是CALL指令的下一条指令;而IRET返回之前旧EIP指向的是之前被中断的指令,因此IRET返回之后旧EIP要再加4,才指向下一条指令。

中断门和陷阱门也有区别:中断门会导致IF标志被复位(为0),意味着在CPU处理中断时会屏蔽中断。而陷阱门不会去复位IF标志。

3.2 任务门

中断门或陷阱门的特点是,中断处理程序运行时使用的栈是被中断任务(进程)的内核栈。对Linux内核来说,当“doublefault”发生时,内核在该异常对应的IDT表项中放置的是任务门。这就导致处理该异常时切换到新的任务,也就意味着新的内核栈。Linux内核的策略是,用自己的私有栈来处理“double fault”类型异常。

注:什么是“doublefault”?考虑以下情景:在异常发生后跳转到异常处理程序的时候再次发生异常。有时候处理器能够顺序地执行这两个异常,保证系统正常运行,但有时候处理器会当掉,称为“Double fault”。

4. I/O系统

Input/Output系统一般用来与外设进行通信。外部设备一般拥有自己的一套寄存器(包括数据寄存器或者控制寄存器)。所有外设的寄存器在处理器看来都是一个个端口(port)。处理器的I/O系统主要是指处理器对I/O端口的寻址方式。

理论上有两种I/O端口寻址方式:1)单独地址空间,利用特殊指令(IN/OUT)寻址;2)映射到内存地址空间,利用一般内存寻址指令访问I/O系统。80386支持这两种方式。

映射到内存地址空间的方式就不多说了,这里主要讲讲单独的I/O地址空间寻址时需要注意的地方,主要是权限检查。

外设的端口分为8bit,16bit或32bit。利用IN/OUT指令访问端口的时候,根据使用的寄存器(AL,AX,EAX)来判定访问端口的位数。如果32bit端口号是4字节对齐的,那么访问一次也只需要1个总线周期。

EFLAGS状态寄存器的IOPL字段指定了访问IO地址空间的权限:如果CPL<=IOPL,那么可以访问任何的端口;否则检查当前任务TSS段的I/O端口权限位图(I/OPermission Bit Map),查看对应的端口是否有权限(若为0,这有权限)。

回顾2.1节描述TSS结构的那张图,在66字节偏移处有个”I/O map base”字段。该字段指向了TSS段内I/O端口权限位图的起始地址。因此,可以给不同的任务指定不同的端口访问权限。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值