目录

分段机制

特权级检查

GDT和LDT

堆栈切换

分页机制

中断


分段机制

  实模式中cs是一个实实在在的段首地址,ip为cs所指向段的偏移,所以cs<<4+ip是当前cpu执行的指令。我自己学汇编的时候,整天念叨着段地址左移4位加上偏移才是实模式的地址,但到了实际的代码中看到一个实实在在的左移指令时候还是没想到这点。

  保护模式中的cs是对一个数组的索引,也就是对GDT或者LDT表的索引。这两个表的数组元素都是8个字节的长度,存储的是:

  1. 段描述符,有段的信息,比如说段的类型、段的首地址、访问这个段的最低权限等等。主要是分为两大类,代码段和数据段;
  2. 门描述符,门描述符就像一道关卡,放着你要访问段的索引和相对于段首的偏移。但是你得符合这道关卡的约束,才能得到这个索引,并根据这个索引访问你想要的段;否则的话cpu就会报通用保护的异常。从这个角度看,门描述符仅仅是一份记录,记录着段的索引,并不涉及到具体的段的位置。所以intel对于使用门描述符来定位的访问首先在通过门的检查中按照访问数据段的检查方式来判断本次访问是否有资格获得段的索引,如果通过再按照段的代码段的访问方式来验证一遍。

特权级检查

  数值越大,特权级越低,下面说的低特权级就是说选择子低两位值较高。特权级检查是保护模式下的属性,有关的几个名词是CPL、RPL、DPL。

  RPL是对于代码跳转那一瞬间而言的,存在于指定的或者默认的选择子中。我自己是一直以为cs或ss就是选择子,所以一直困惑于CPL和RPL的区别,因为这两个玩意怎么可能既是CPL,又是RPL呢。

  比如说

  jmp abcd:fg

  此时指明了abcd就是一个选择子

  jmp hijk

  此时没有指明段前缀,cs一般就是默认的段选择子

  在产生跳转的那一瞬间,参与安全检查的叫做RPL,因为有可能你的cs的特权级很高,但是你用显示跳转的方式,拿上面的abcd来说,可能这个选择子的特权级就没有原本的cs特权级高。那intel为何多此一举搞一个RPL的概念呢?由于RPL的存在,通过max{cpl,rpl},我们不用改变自身的特权级就能临时伪装一个低特权级的身份。

  CPL是一个常态的概念,一直存在于cs或者ss中。RPL只参与比较,比较后并不赋值给选择子。

  DPL是一个段或者一个门描述符的特权级的描述。

  门描述符和段描述符的区别就是:

  1. 段是赤裸裸的告诉你哪里有代码哪里有数据,根据选择子找到我,并且通过cpu的检查,你就可以在我圈的地里面随意走动了
  2. 门描述符,是一份数据的记录,这分数据记录的是一种结构体,该结构体成为门描述符,里面存放的有我们想用的代码或者数据的段选择子。只有通过这个检查,获得这份数据记录,才能得到段选择子,再去选择段。我这样说,就是说明,再访问门描述符的时候,cpu是按照访问数据段要求来判断我们时候有资格获得这份数据,如果有资格,那么再按照访问代码去检查。

对于数据段的访问

    max{cpl,rpl}<=DPL,符合

对于代码段的访问

    有几个原则:

    1. 高特权级不能访问低特权级
    2. 一致代码段:控制权转移前后特权级不变
    3. 非一致代码段:控制权转移前后特权级=DPL
    4. 是否是一致代码段,代码或者手工指定描述符的属性

    对代码段的访问,分为两种情况:

不通过调用门(普通跳转),不能使特权级发生跃迁

  jmp或者call后面跟着48位的全指针(其中32偏移+16选择子),并且选择子指向的是段描述符(由cpu硬件读取你所指向的段描述符的属性自行判断)。

      访问一致性代码:

        要求:CS.CPL>=DestnationCodeDesc.DPL

        访问后:CS.CPL=oldCS.CPL

        纵使它执行了高特权级的代码,也是不会发生变化,这是cpu硬件赋予一致代码段的功能

      访问非一致性代码:

        要求:CS.CPL=DestnationCodeDesc.DPL

        访问后:CS.CPL=oldCS.CPL

        这就保证了前后特权级一致,没有发生跃迁

通过调用门

  jmp或者call后面跟着选择子(如果有线性偏移也被忽略,因为门描述符记录了线性偏移),并且选择子指向的是门描述符(由cpu硬件读取你所指向的段描述符的属性自行判断)。

  如上所述,我们要得到门描述符的记录数据,就要按照访问数据段的要求做一次检查,也就是要符合max{cpl,rpl}<=DPL。通过之后,cpu会将段选择子中RPL字段清零(也就是最高级),也就是下面的检查忽略RPL的作用,再去按照访问代码段的规则进行检查

      访问一致性代码:

        要求:

          CS.CPL>=DestnationCodeDesc.DPL

          RPL<=DPL(事实上,永远满足)

        访问后:CS.CPL=oldCS.CPL

      纵使它执行了高特权级的代码,也是不会发生变化,这是cpu硬件赋予一致代码段的功能

      访问非一致性代码:

      Jmp的形式:

      要求:

        CS.CPL=DestnationCodeDesc.DPL

        RPL<=DPL(事实上,永远满足)

      访问后:CS.CPL=oldCS.CPL

      这就保证了前后特权级一致,没有发生跃迁

      Call的形式:

        要求:CS.CPL>=DestnationCodeDesc.DPL

          如果:CS.CPL=DestnationCodeDesc.DPL

          访问后:CS.CPL=DestnationCodeDesc.DPL

          如果:CS.CPL>DestnationCodeDesc.DPL

          访问后:CS.CPL=DestnationCodeDesc.DPL

这是我们见到的唯一能使特权级发生变化的跳转方式。

      以上,在检查不通过的时候会报异常(不过我忘了这个总结是在哪个博客了看到的,自己分个更详细的类)

      不过对于通过门描述符访问的指令,代码段的偏移地址已经在门描述符里面写死了,所以即便该指令后面还跟着偏移地址也不会被CPU使用。


GDT和LDT

  GDT里面存放的是全局共享的段的描述,LDT是属于进程或者任务独有的。
  在实验中,每个进程都有一个LDT描述符,也就是说,每个进程都被当做一个任务看待。在进程结构体中保存的有LDT选择子,有SP0指针,都会在进程初始化时候被填充,关于进程切换很有意思,到时候我们会看一点代码。

  一般CS的可见部分的高13位作为在GDT或者LDT里面的索引。第2位TI=0指明本次寻址用GDT,TI=1指明本次寻址用LDT。在GDT中寻址很清楚。在LDT中寻址要中转一下,因为LDT也是一个段,所以也要有段描述符在GDT中保存,那么这个LDT在描述符在GDT表中的索引却不是直接放在CS的高13位里面,而是在任务切换的时候被系统加载进ldtr里面了(这一点自己开始没搞清楚,一直以为找LDT段还是通过CS寻找,这个过程中纠结不已),这个寄存器的高13位指明了本次任务的LDT描述符在GDT表中的位置。等系统用这个索引找到LDT段后,再用CS的高13位在LDT段中做索引,才找到和任务相关的局部代码段或者数据段的描述符,再进行跳转或者访问。


堆栈切换

  当低特权级因为某些事件需要跳到高特权级的时候,就需要切换堆栈。以后就简单称为用户态和内核态算了。

  为什么用户态的程序跳到内核态就要切换堆栈呢?不切换用同一个esp感觉应该也不会出事情啊。当然,如果能保证控制力非常精准用同一个栈也不是不可以,要不然纯汇编的程序员岂不是郁闷死了。还是为了保护两个字。就拿进程间的切换来说,有不同的栈岂不是不用担心数据覆盖什么的了,而且也不用考虑进程抢占时候栈的处理了。

  用户态的程序在进入内核态的时候要把栈切换到内核态的栈,那么内核态的栈指针从何而来呢?Intel将esp0放在TSS中。当要切换用户态到内核态的时候,CPU自动从TSS段中取出esp0加载进esp里面,然后将用户态的ss、esp,还有eflags、cs、eip一次压入此时esp指向的栈空间。根据需要再决定是否要对其它几个寄存器执行压栈操作。

  那么TSS里面的esp0又是从哪里来的呢?是从进程控制块(PCB,也就是进程在系统中的户口本)里面,在进程调度的时候能够用的上(也就是一个进程的内核态切换到另一个进程的内核态,中断是用户态切换到内核态,或者是同一个进程的内核态的压栈操作)。当发生进程调度的时候,调度程序是操作系统设计者写的,自然知道每个PCB的哪个位置保存了该进程的内核态指针,然后把这个值载入TSS的esp0,用iret指令返回就好了。


分页机制

  对于32位的cpu,一般来说都是二级页表映射。一个页面的大小是固定的4K,每个页面的首地址是4K对齐的,后12位不参与寻址,但是用来保存一些页面的属性,比如说是否在内存中,读写属性等等。

  对于页表来说,第一级是页目录,每个页目录存放一个页面的物理地址,对应一个页面,这个页面有4KB/4B=1K个项,每个项都存放一个地址,这个地址地址指向一个页面,这个页面被程序真正用来存放数据或者代码。页目录项和页表项里面存放的都是物理地址。然后在程序中给的地址是线性地址,高10位在页目录表中做索引,找到对应的二级页表所在,中间10位在二级页表中做索引,找到存放数据的页面,低12位用来在存放数据的页面做偏移,得到这个地址所对应的真正的物理地址。


中断

  中断主要分为外部中断和int n产生的中断,int n产生的中断和调用门相似,相当于跳转到另一段代码中,Linux中int 0x80就是作为调用门来陷入内核的。而对于外部中断,不过是CPU的INTR引脚接收到了一个电信号,在CPU执行完一条指令后这个电信号会被检测到,然后CPU会给8259A一个响应,让其把中断产生的号发送过来。8259A在发送给CPU中断号之后,会按照优先级屏蔽比这个中断优先级低的中断信号。CPU得到这个中断号之后,会到idtr指定的IDT表中用这个号作为索引找到响应的门描述符,然后执行响应的代码。

  在系统初始化的时候需要注意的是,BIOS初始化所占用的中断号和intel规定的用来响应中断或者异常所占用的编号有冲突,没办法,BIOS占用的中断号必须让出来,不然外部挂接的设备产生的中断信号将被解释为intel规定所对应的异常,即使你把这个中断号对应的处理程序绑定为正确的程序,那么由于CPU会做不同的变化,从而得不到我们想要的效果。

  比如说BISO设置的8259A让0x9号中断对应于键盘事件,如果我们在跳入保护模式不改过来,那么在发生键盘输入事件的时候,0x9号中断将被cpu寻到保护模式下IDT表的协处理器段越界的fault异常,即便你讲键盘中断函数绑定在9号中断上,而且也正确的读取到了键盘的按键,但是cpu做的处理可能不再是顺着原有流程走了,那么没法保证,有冲突的中断号能按照预期想法工作。

  Intel用了前32个IDT号,所以我们要编程8259A,将BIOS占用的几个编号挪到0x20开始的中断。以前在学《微机原理与接口技术》的时候,看到8259A还有编程时候用的ICW命令都是感觉到云里雾里的,不知道其意义所在,现在却是有点明白了。8259A的使命应该就是按照ICW的设置将每个引脚产生的高低电平信号转换为一个编号。就像BIOS可以将8259A设置为将八个引脚的电信号分别转化成0x8-0xf一样,我们自然能将主从(两个)芯片占用的编号设置为0x20-0x2f。其中主从片的控制寄存器的地址分别为0x20和0xA0,作者的书中92页有很详细的解释,不过高低位不要忽略了,以免对照不上每个字段的含义。

  在转移BIOS占用的中断号后,先暂时屏蔽所有的外部中断,因为此时的中断表还没有建立,如果响应了中断,cpu将会按照idtr中的值访问中断处理函数,不出意外的话立即就会崩溃。

  屏蔽中断时往主片的0x21或者从片的0xA1端口地址写入OCW1,该字节每个位对应一个8259A的屏蔽操作,置为1则屏蔽这个引脚的电信号。另外在每次相应一个中断后,还要发送一次EOI指令,以便继续接收中断,发送EOI是通过往端口0x20或0xA0写OCW2来实现的,对于EOI来讲就是OCW2的内容就是0x20。

实践中需要了解的cpu特性的更多相关文章

  1. 编程实践中C语言的一些常见细节

    对于C语言,不同的编译器采用了不同的实现,并且在不同平台上表现也不同.脱离具体环境探讨C的细节行为是没有意义的,以下是我所使用的环境,大部分内容都经过测试,且所有测试结果基于这个环境获得,为简化起见, ...

  2. 实践中总结——理解haslayout和BFC

    1.HASLAYOUT 首先,haslayout翻译成中文就是:有布局. 所谓布局,指的是一个元素可以对本身和里边的元素进行尺寸计算和定位.这里只是谈IE6/7,据说微软之所以不是对所有元素默认有布局 ...

  3. 谈谈在DevOps实践中,感觉最重要的这三个技术……

    从国内众多DevOps实践中,我们能看到下面三个技术尤其重要和火热: 容器:容器从根本上解决了软件对环境的依懒性,解决了各个环境之间的差异问题:它可以加速部署的速度,提高部署的效率:降低部署的成本.容 ...

  4. 你应当如何学习C++以及编程(细节是必要的,但不是重要的,把时间用在集中精力去解决问题,而不是学习新技术,那样练不成高手。在实践中提高才是最重要的。最最重要的内功还是长期学习所磨练出来的自学能力)good

    最近在学习Qt但由于没有C++的基础,感觉学的很吃力.看到pongba的这篇文章感觉不错就弄过来了, 原文地址:http://blog.csdn.net/qter_wd007/article/deta ...

  5. 03--(二)编程实践中C语言的一些常见细节

    编程实践中C语言的一些常见细节(转载) 对于C语言,不同的编译器采用了不同的实现,并且在不同平台上表现也不同.脱离具体环境探讨C的细节行为是没有意义的,以下是我所使用的环境,大部分内容都经过测试,且所 ...

  6. DPC++中的现代C++语言特性

    Ⅰ DPC++简介 DPC++是Data Parallel C++(数据并行C++)的首字母缩写,它是Intel为了将SYCL引入LLVM和oneAPI所开发的开源项目.SYCL是为了提高各种加速设备 ...

  7. 记一次小团队Git实践(中)

    对于初学者,从使用上先入手,往往学的最快,并从中汲取教训,再回头更深入的学习,效果尤佳. 安装git 安装git自不必说,mac已经内置了git,linux下一个命令就能搞定,windows下需要下载 ...

  8. 简单理解ECMAScript2015中的箭头函数新特性

    箭头函数(Arrow functions),是ECMAScript2015中新加的特性,它的产生,主要有以下两个原因:一是使得函数表达式(匿名函数)有更简洁的语法,二是它拥有词法作用域的this值,也 ...

  9. 转:C4项目中验证用户登录一个特性就搞定

    转:C4项目中验证用户登录一个特性就搞定   在开发过程中,需要用户登陆才能访问指定的页面这种功能,微软已经提供了这个特性.     // 摘要:    //     表示一个特性,该特性用于限制调用 ...

随机推荐

  1. Impala中多列转为一行

    之前有一位朋友咨询我,Impala中怎样实现将多列转为一行,事实上Impala中自带函数能够实现,不用自己定义函数. 以下我開始演示: -bash-4.1$ impala-shell Starting ...

  2. [k8s]k8s api-server启动systemd参数分析

    默认2个参数就可以启动(必需) kube-apiserver \ --service-cluster-ip-range=10.254.0.0/16 \ --etcd-servers=http://19 ...

  3. Android Studio怎样查看branch列表及切换branch

    针对Android Studio的系列文章,都是一个小问题为一篇,并没有整理到一起,主要是方便大家依据自己的须要来查找,同一时候为了便于大家理解,都会直接上图. 我这里使用的版本号控制工具是git,由 ...

  4. Http basic Auth 认证方式帮助类

    BasicAuthenticationUtil import java.io.IOException; import java.security.MessageDigest; import javax ...

  5. Oracle之标示符无效

    一.引言 今天使用Oracle客户端执行一条sql语句 order by colname3 结果一直提示标示符无效,以为是自己把列名写错了打开表的列,一个字母一个字母的比对,还是没有错 二.原因及解决 ...

  6. python学习笔记2---函数

    函数主要是为了代码复用. 函数分为两种:系统库预定义函数,自定义函数. 函数格式: def functionName(): statement 函数调用: funtionName() 函数的参数:形参 ...

  7. STL源代码剖析——STL算法stl_algo.h

    前言 在前面的博文中剖析了STL的数值算法.基本算法和set集合算法.本文剖析STL其它的算法,比如排序算法.合并算法.查找算法等等.在剖析的时候.会针对函数给出一些样例说明函数的使用.源代码出自SG ...

  8. ubuntu设置静态ip地址

    每次设置都忘了之前怎么设置的,所以今天记录下来. 1. 找到文件并作如下修改: sudo vim /etc/network/interfaces 修改如下部分: auto eth0iface eth0 ...

  9. Unity3D中简单的C#异步Socket实现

    Unity3D中简单的C#异步Socket实现 简单的异步Socket实现..net框架自身提供了很完善的Socket底层.笔者在做Unity3D小东西的时候需要使用到Socket网络通信.于是决定自 ...

  10. LNMP笔记:php-fpm – 启动参数及重要配置详解

    约定几个目录/usr/local/php/sbin/php-fpm/usr/local/php/etc/php-fpm.conf/usr/local/php/etc/php.ini php-fpm的启 ...