了解x86下的特权级原理

特权级

为什么TSS中只有3个栈信息:因为任务处于最低的3特权级,而TSS中记录的栈是用于特权级从低特权级变成高特权级时,CPU会自动进行栈更新的作用,所以3特权级没有比它更低的特权级了,不需要放在TSS中。

那高特权返回到低特权级的情况下,cpu如何找到3特权级栈呢?
方法是:一般是用户程序调用函数进入依从代码段下的原函数前,在高特权级栈中记录返回栈指针和返回地址,之后retf或iret返回时可以从栈中恢复。

CPL: CPU当前特权级,等于CS段描述符的RPL值
DPL:“如果你想访问我,你应该具备什么样的权限”
RPL:用什么权限去访问一个段 ,用于解决CPU“越权”问题

平坦模式下,用户进程不需要再提供选择子,所以调用门可以用中断门代替了 ??

在调用内核服务例程时,段寄存器可能会加载到0特权级的段,但是在retf返回时CPU并不会压入段寄存器或者重新加载用户的段寄存器,这就导致返回到用户态后,存在段寄存器(CS和SS除外)还是内核特权级别的危险。

解决方法:

  1. Linux调用内核例程使用中断门,在中断处理程序返回前手动更新所有寄存器(恢复上下文)。
  2. 处理器中进行特权级检查,如果某个寄存器中选择子所指向的数据段描述符的 DPL 权限比返回后的 CPL (CS.RPL)高,处理器将把数值 0 填充到相应的段寄存器。
  3. 填充的0的原理是:GDT中第0个描述符是无效的,避免描述符没有初始化,出现错误访问,CPU设计了**“利用选择子为 0 引发异常”**的机制,所以填充0就是可以引发异常。

1:调用门及特权级检查

在这里插入图片描述

调用门描述符位与GDT和IDT中,因此调用门的方法为call/jmp + 调用门的选择子
但是call实现从低到高特权级,jmp只能实现平级代码跳转

call指令会在栈中压入返回地址,因此调用完内核例程可以返回到用户;jmp由于不会压入返回地址,所以只能在平级的代码段之间跳转。

1.1:调用门特权级检查

通过调用门进行程序的转移控制时,CPU会检查以下这几个字段:1.当前代码段的CPL;2.调用门描述符中的DPL=DPL_GATE;3.调用门选择子中的RPL;4.目的代码段描述符的DPL=DPL_CODE;5.目标代码段描述符中的一致性标志(一致与非一致下面会提到)
在这里插入图片描述
在这里插入图片描述

  1. 数值上 DPL_GATE >= CPL >= DPL_CODE
  2. 数值上 RPL <= DPL_GATE

一致性代码段在转移后CPL不会有变化,看作同级跳转,call和jmp都用上述公式
非一致性代码段则不同,看作跨级跳转,call支持跨级(使用上述公式),但jmp不支持,只能同级跳转,公式为

  1. 数值上 DPL_GATE >= CPL = DPL_CODE (跳转后CPL会变成DPL_CODE )
  2. 数值上 RPL <= DPL_GATE

1.2:不通过调用门,直接访问一般数据段和代码段时的特权检查规则

受访者为代码段时:

  1. 目标为非一致性代码段时:数值上 CPL=RPL=目标代码段 DPL
  2. 目标为一致性代码段时,数值上(CPL>=目标代码段 DPL && RPL>=目标代码段 DPL),即从低到高特权级, 高特权级程序不信任低特权级的程序,所以不能从高到低

受访者为数据段

  1. 数值上(CPL <= 目标数据段 DPL && RPL <= 目标数据段 DPL),高特权级有权利访问低特权级的数据
  2. 特殊情况:栈段加载选择子 CPL = RPL = 目标数据段 DPL

例子:mov ds,ax ,满足 RPL <= DPL && CPL <= DPL,选择子才会加载到ds

  1. RPL为ax这个选择子的低2位,即访问者以什么级别来访问
  2. CPL为当前CPU的特权级
  3. DPL为ax选择子所指向的段描述符的DPL,即想要访问者达到的级别

2:RPL

由来概要:直接举个例子,用户调用读取硬盘数据的函数,提供的参数需要加载数据的缓冲区所在的数据段选择子,一般会是用户的数据段,但别有用心的人知道了内核数据段选择子并压入栈中的话,就会破坏内核数据(在cpl=0下,加载dpl=0的数据段是可以被允许的)
但现在我们引进了RPL即请求特权级,解决方法有两步

  1. 利用操作系统的控制权,用arpl指令强制修改选择子的RPL为用户进程的CPL
  2. 设置特权级检查规则:进行段寄存器的加载时,若CPL<=DPL,RPL<=DPL(DPL是选择子对应的段描述符的DPL)

2.1:分辨CPL和RPL

CPL和RPL不是对同一个程序而言的,正如上面的例子,RPL是用户进程提供的缓冲区数据段选择子中的,而写数据时 CPL 指的是内核,不是一个程序
RPL是在选择子中的,可能是当前进行的程序,也可能不是,像上面的缓冲区;CPL肯定是当前进行的程序(一致性代码段除外)

下面是RPL的生活例子:

报考驾校也要有个年龄限制,即使考 C 本 B 本也要分年龄的。假如某个 小学生 A (用户进程)特别喜欢开车,他就是想考个驾照,可驾校的门卫(调用门〉一看他年龄太小都不 让他进门,连填写报名登记表的机会都没有,怎么办?于是他就求他的长辈 B (内核〉帮他去报名,长辈 的年龄肯定够了,门卫对他放行,他来到驾校招生办公室后,对招生人员说要帮别人报名。人家招生人员 对 B 说,好吧,帮别人代报名需要出示对方的身份证(RPL),于是长辈 B 就把小学生 A 的身份证(现在 小孩子就可以申请身份证,只是年龄越小有效期越短,因为小孩子长得快嘛)拿出来了,招生人员一看, 年纪这么小啊,不到法制学车年纪呢,拒绝接收。这时候驾校招生人员的安全意识开始泛滥了,以纵容小 孩子危险驾驶为名把长辈 B 批评了一顿(引发异常)。

补充

进程通过虚拟地址页变化访问物理地址时会进行特权级检查吗?

答:不会

在进程调度函数中,switch_to(cur, next)执行前会进行页表激活process_activate(next),页表激活的过程分为CR3赋值为新进程的页目录表的物理地址和更新tss中的内核栈。

//激活pthread的页表
void page_dir_activate(struct task_struct* pthread)
{
    uint32_t page_phyaddr = 0x100000;    //内核的页表的物理地址
    //如果当前
    if(pthread->pgdir != NULL){
        page_phyaddr = addr_v2p((uint32_t)pthread->pgdir);    //将虚拟地址转化为物理地址
    }
    asm volatile("movl %0,%%cr3": :"r"(page_phyaddr) :"memory");
}

//激活进程或者线程的页表,更新tss中esp0为进程的特权级为0的栈
void process_activate(struct task_struct* pthread)
{
    ASSERT(pthread != NULL);
    page_dir_activate(pthread);

    //内核线程特权级栈为0,而cpu进入中断不需要从tss中获取0级栈
    if(pthread->pgdir)
        update_tss_esp(pthread);
}

既然CR3已经指向新进程的页目录表了,就代表之后所有虚拟地址的映射转化都是默认建立在这个页目录的前提上的(用户进程陷入内核CR3也不会改变),因此特权级检查就没有必要了,因为不管访问4G虚拟空间中的任何虚拟地址都是属于该进程的,那我们在学习分页机制时的PTE和PDE的属性位U/S有什么用呢?
在这里插入图片描述
在这里插入图片描述

R/W位的检查属于页保护机制,检查地址空间数据的读写/执行权限,这就像老板(内核)在公司关心员工(进程)的工作,员工在家关心自己家人(进程空间内的数据)的生活(读/写/执行权限)。

内核的页表为什么会设置成用户级别的?

在这里插入图片描述


标题:了解x86下的特权级原理
作者:abandon
地址:HTTPS://www.songsci.com/articles/2021/06/25/1645758319482.html

    评论
    0 评论
avatar

取消