Blog

Symbol.toPrimitive

Content #

下面的JavaScript代码的输出会是什么?

var o = {
    valueOf : () => {console.log("valueOf"); return {}},
    toString : () => {console.log("toString"); return {}}
}
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
console.log(o + "")

输出: toPrimitive hello 在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

From #

特权指令(Privileged Instructions)

Content #

不同特权级别的程序,所担负的职责以及在系统中扮演的角色是不一样的。计算机系统的脆弱性在于一条指令就能改变它的整体运行状态,比如停机指令hlt 和对控制寄存器CR0 的写操作,像这样的指令只能由最高特权级别的程序来做。

因此,那些只有在当前特权级CPL 为0 时才能执行的指令,称为特权指令(Privileged Instructions)。典型的特权指令包括

  1. 加载全局描述符表的指令lgdt(它在实模式下也可执行)
  2. 加载局部描述符表的指令lldt
  3. 加载任务寄存器的指令ltr
  4. 读写控制寄存器的mov指令
  5. 停机指令hlt。

From #

操作系统负责设置RPL字段

Content #

操作系统的编写者很清楚段选择子的来源,即,真正的请求者是谁。当它自己读写一个段时,这没有什么好说的;当它提供一个服务例程时,3 特权级别的用户程序给出的选择子在哪里,也是由它定的,它也知道。

在这种情况下,它所要做的,就是将该选择子的RPL 字段设置为请求者的特权级(可以使用arpl 指令)。剩下的工作就看处理器了。

每当处理器执行一个将段选择子传送到段寄存器(DS、ES、FS、GS)的指令,比如:mov ds, ax 时,会检查以下两个条件是否都能满足。

  1. 当前特权级CPL 高于或者和数据段描述符的DPL 相同。即,在数值上,CPL≤数据段描述符的DPL;
  2. 请求特权级RPL 高于或者和数据段描述符的DPL 相同。即,在数值上,RPL≤数据段描述符的DPL。

如果以上两个条件不能同时成立,处理器就会阻止这种操作,并引发异常中断。

按照Intel 公司的说法,引入RPL 的意图是“确保特权代码不会代替应用程序访问一个段,除非应用程序自己拥有访问那个段的权限”。多数读者都只在字面上理解这句话的意思,而没有意识到,这句话只是如实地描述了处理器自己的工作,并没有保证它可以鉴别RPL 的有效性。

From #

在特权级检查中引入RPL的必要性

Content #

想象一下,应用程序的编写者通过钻研,知道了操作系统数据段的选择子,而且希望用这个选择子访问操作系统的数据段。 在特权级检查中引入RPL 的必要性

应用程序无法直接访问操作系统数据段,但它可以借助于调用门。调用门工作在目标代码段的特权级上,一旦处理器的执行流离开应用程序,通过调用门进入操作系统例程时,当前特权级从3 变为0。当那个不怀好意的程序将一个指向操作系统数据段的选择子通过CX 寄存器作为参数传入调用门时,因为当前特权级已经从3 变为0,可以从硬盘读出数据,并且允许向操作系统数据段写入扇区数据,他得逞了!

处理器的智商很低,它不可能知道谁是真正的请求者。作为最聪明的灵长类动物,你当然可以通过分析程序的行为来区分它们,但处理器不能。

因此,当指令 mov ds, ax 或者 mov ds, cx 执行时,AX 或者CX 寄存器中的选择子可能是操作系统自己提供的,也可能来自于恶意的用户程序,这两种情况要区别对待,但已经超出了处理器的能力和职权范围。怎么办?

看得出来,单纯依靠处理器硬件无法解决这个难题,但它可以在原来的基础上多增加一种检查机制,并把如何能够通过这种检查的自由裁量权交给软件(的编写者)。引入请求特权级(RPL)的原因是处理器在遇到一条将选择子传送到段寄存器的指令时,无法区分真正的请求者是谁。

但是,引入RPL 本身并不能完全解决这个问题,这只是处理器和操作系统之间的一种协议,处理器负责检查请求特权级RPL,判断它是否有权访问,但前提是提供了正确的RPL;内核或者操作系统负责鉴别请求者的身份,并有义务保证RPL 的值和它的请求者身份相符,因为这是处理器无能为力的。

因此,在引入RPL 这件事上,处理器的潜台词是,仅依靠现有的CPL 和DPL,无法解决由请求者不同而带来的安全隐患。那么,好吧,再增加一道门卫,但前提是,操作系统只将通行证发放给正确的人。

From #

请求特权级别RPL和当前特权级CPL不相同的例子

Content #

在绝大多数时候,请求者都是当前程序自己,因此,CPL=RPL。要判断请求者是谁,最简单的方法就是看谁提供了选择子。

以下是两个典型的例子: jmp dword 0x0010:flush 在这里,提供选择子0x0010 的是当前程序自己。再比如: mov eax, 0x0008 mov ds, eax 这同样是当前程序自己拿着段选择子0x0008 来“请求”代入段寄存器DS,以便在随后的指令中访问该段中的数据。

但是,在一些并不多见的情况下,RPL 和CPL 并不相同。如下图所示,特权级为 3 的应用程序希望从硬盘读一个扇区,并传送到自己的数据段,因此,数据段描述符的DPL 同样会是3。 请求特权级RPL 和当前特权级CPL 不相同的例子

由于I/O 特权级的限制,应用程序无法自己访问硬盘。好在位于0 特权级的操作系统提供了相应的例程,但必须通过调用门才能使用,因为特权级间的控制转移必须通过门。假设,通过调用门使用操作系统例程时,必须传入3 个参数,分别是CX 寄存器中的数据段选择子、EBX 寄存器中的段内偏移,以及EAX 中的逻辑扇区号。

高特权级别的程序可以访问低特权级别的数据段,这是没有问题的。因此,操作系统例程会用传入的数据段选择子代入段寄存器,以便代替应用程序访问那个段: mov ds, cx 在执行这条指令时,CX 寄存器中的段选择子,其RPL 字段的值是3,当前特权级 CPL 已经变成0,因为通过调用门实施控制转移可以改变当前特权级。显然,请求者并非当前程序,而是特权级为3 的应用程序,RPL 和CPL 并不相同。

From #

保护模式的三种权限类型 CPL的值存放在CS寄存器Selector域的RPL。

特权级检查规则

Content #

我们来总结一下基本的特权级检查规则。

  1. 将控制直接转移到非依从的代码段要求当前特权级CPL 和请求特权级 RPL 都等于目标代码段描述符的DPL。即,在数值上

CPL = 目标代码段描述符的DPL RPL = 目标代码段描述符的DPL 一个典型的例子就是使用jmp 指令进行控制转移: jmp 0x0012:0x00002000 因为两个代码段的特权级相同,故,转移后当前特权级不变。

  1. 将控制直接转移到依从的代码段要求当前特权级CPL 和请求特权级RPL 都低于,或者和目标代码段描述符的 DPL 相同。即,在数值上,

CPL >= 目标代码段描述符的DPL RPL >= 目标代码段描述符的DPL 控制转移后,当前特权级保持不变。通过门实施的控制转移,其特权级检查规则另外处理。

  1. 高特权级别的程序可以访问低特权级别的数据段,但低特权级别的程序不能访问高特权级别的数据段。访问数据段之前,肯定要对段寄存器DS、ES、FS 和GS 进行修改,比如: mov fs, ax 在这个时候,要求当前特权级CPL 和请求特权级RPL 都必须高于,或者和目标数据段描述符的DPL 相同。即,在数值上,

CPL <= 目标代码段描述符的DPL RPL <= 目标代码段描述符的DPL

  1. 栈段的特权级别必须和当前特权级CPL 相同。随着程序的执行,要对段寄存器SS 的内容进行修改时,必须进行特权级检查。以下就是一个修改段寄存器SS 的例子: mov ss, ax 在对段寄存器SS 进行修改时,要求当前特权级CPL 和请求特权级RPL 必须等于目标栈段描述符的DPL。即,在数值上,

CPL = 目标代码段描述符的DPL RPL = 目标代码段描述符的DPL

0 特权级是最高的特权级别,当一个系统的各个部分都位于0 特权级时,各种特权级检查总能够获得通过,就像这种检查和检验并不存在一样。所以,处理器的设计者建议,如果不需要使用特权机制的话,可以将所有程序的特权级别都设置为0,就像我们一直所做的那样。

From #

jmp far (保护模式下)

Content #

保护模式下的 far jump 共有三种:

  1. A far jump to a conforming or non-conforming code segment. 若目的段的特权级与当前特权级相同,则与实模式下的far jump没什么区别。若目的段的特权级与当前特权级不同,并且目的段为non-conforming,则产生异常。
  2. A far jump through a call gate. 忽略指令中offset部分。CPU跳转到call gate descriptor指定的段,执行 call gate中指定位移处的指令。不会有栈切换。
  3. A task switch. 忽略指令中offset部分。目的操作数指定了task gate中task的段选择符。 task gate中包括了指向目的task的TSS,会直接切换到对应的task。

低特权级调用高特权级的两种方法

Content #

为了让特权级低的应用程序可以调用特权级高的操作系统例程,处理器也提供了相应的解决办法。

将高特权级的代码段定义为依从的(conforming) #

保护模式段描述符给出了段描述符的TYPE 字段。代码段描述符的TYPE 字段有C 位,如果C=1,则这样的代码段称为依从的代码段,可以从特权级比它低的程序调用并进入。不过,这是有条件的。当前特权级CPL 必须低于,或者和目标代码段描述符的DPL 相同。即,在数值上, CPL >= 目标代码段描述符的DPL

使用门 #

门(Gate)是另一种形式的描述符,称为门描述符,简称门。和段描述符不同,段描述符用于描述内存段,门描述符则用于描述可执行的代码,比如一段程序、一个过程(例程)或者一个任务。事际上,根据不同的用途,门的类型有好几种。不同特权级之间的过程调用可以使用调用门;中断门/陷阱门是作为中断处理过程使用的;任务门对应着单个的任务,用来执行任务切换。这里的重点是调用门(Call Gate)。

所有描述符都是64 位的,调用门描述符也不例外。在调用门描述符中,定义了目标过程(例程)所在代码段的选择子,以及段内偏移。要想通过调用门进行控制转移,可以使用jmp far 或者call far 指令,并把调用门描述符的选择子作为操作数。

使用 jmp far指令,可以将控制通过门转移到比当前特权级高的代码段,但不改变当前特权级别。但是,如果使用call far 指令,则当前特权级会提升到目标代码段的特权级别。也就是说,处理器是在目标代码段的特权级上执行的。

但是,除了从高特权级别的例程(通常是操作系统例程)返回外,不允许从特权级高的代码段将控制转移到特权级低的代码段,因为操作系统不会引用可靠性比自己低的代码。

From #