用户程序可以在由操作系统加载时通过指定整个eflags设置,操作系统如何设置自己的IOPL呢,即使内核IOPL为0也得写进去eflags寄存器中才生效。可惜的是,没有直接读写eflags寄存器的指令,不过可以通过将栈中数据弹出到eflags寄存器中来实现修改。可以先用pushf指令将eflags整体压入栈,然后在栈中修改相应位,再用popf指令弹出到eflags寄存器中。另外一个可利用栈的指令是iretd,用iretd指令从中断返回时,会将栈中相应位置的数据当成eflags的内容弹出到eflags寄存器中。所以可以改变IOPL的指令只有popf和iretd指令,依然是只有在0特权下才能执行。如果在其它特权级下执行此指令,处理器也不会引发异常,只是没任何反应。
接下来看看IO位图是怎么回事。
假如,数值上CPL <= IOPL,程序既可以执行IO特权指令,又可以操作所有的IO端口。倘若数值上CPL > IOPL,程序也不是完全无法进行任何IO操作,有点奇怪是不,似乎和咱们的逻辑不符,其实这样是有道理的。
之前说过,IOPL是所有IO端口的开关,不过,这个开关还留有余地,如果将开关打开,便可以访问全部65536个端口,如果开关被关上,即数值上CPL > IOPL,则可以通过IO位图来设置部分端口的访问权限。也就是说,先在整体上关闭,再从局部上打开。这有点像设置防火墙的规则,先默认为全部禁止访问,想放行哪些端口再单独打开。
处理器为什么允许这么做呢?原因是为了提速。
如果所有IO端口访问都要经过内核的话,由低特权级转向高特权级时是需要保存任务上下文环境的,这个过程也是要消耗处理器时间,随着端口访问多起来,时间成本还是很可观的。这一典型的应用就是硬件的驱动程序,它位于特权级1。
什么是驱动程序?
驱动程序就是通过in、out等IO指令直接访问硬件的程序,它为上层程序提供对硬件的控制访问,相当于硬件的代理,程序员通过它就免去了学习硬件控制的相关知识,简化了程序设计。
所以说,驱动程序肯定是要直接控制IO端口的,尽管它可以像linux那样位于0特权级,但它位于1特权级时,依然可以直接操作硬件端口。
即使是在3特权级下,也要考虑某些需要快速反应的场合,比如某个应用程序需要快速的以硬件交互,所以处理器允许通过I/O位图来为3特权级程序打开某些端口的控制。这些规则同样适用于2特权级,也就是说在任意特权级下,处理器都可以通过I/O位图为相应特权级的程序开启特定的端口。
欲知I/O位图是怎么回事,咱们先把位图的概念明确。
位图就是bit map,map就是映射,建立的是某种对应关系,像地图那样,图上某个区域代表实际地理范围,bit就是位,bit map就是用一个bit映射到某个实际的对象。位图这种结构的操作单位就是bit,所以位图其实就是一串二进制01数字,对位图的操作也就是读写相应的位,处理器中对内存的访问是以字节为单位,不能直接操作位,所以对于操作位图,简单说来就是先将该位所在的字节读到内存,若是想将该位置为1,可以用1对该位进行或’运算,若想将该位清0,可以用0对该位进行’与’运算,以后咱们少不了操作位图,到时候再实践。
intel处理器最大支持65536个端口,它允许任务通过I/O位图来开启特定端口,位图中的每一bit代表相应的端口,比如第0个bit表示第0个端口,第65535个bit表示第65535个端口,65536个端口号占用的位图大小是63356/8=8192字节,即8KB。I/O位图中如果相应bit被置为0,表示相应端口可以访问,否则为1的话,表示该端口禁止访问。如图
再次声明,I/O位图只是在数值上CPL > IOPL,即当前特权级比IOPL低时才有效,若当前特权级大于等于IOPL,任何端口都可直接访问不受限制。