为什么linux内核使用陷阱门来处理divide_errorexception?

内核2.6.11.5中,除零exception处理器被设置为:

set_trap_gate(0,&divide_error); 

根据“了解Linux内核”,英特尔陷阱门不能被用户模式进程访问。 但是用户模式进程很可能也会产生divide_error 。 那么为什么Linux以这种方式实现呢?

[编辑]我认为问题仍然是开放的,因为set_trap_gate()设置IDT条目的DPL值为0,这意味着只有CPL = 0(读取内核)代码可以执行它,所以我不清楚这个处理程序可能是从用户模式调用:

 #include<stdio.h> int main(void) { int a = 0; int b = 1; b = b/a; return b; } 

这是用gcc div0.c编译的。 而./a.out的输出是:

浮点exception(核心转储)

所以这看起来不像是由0陷阱代码处理的。

只有使用int指令调用软件中断时,才能查看IDT中的DPL位。 除以零是由CPU触发的软件中断,因此在这种情况下DPL不起作用

我手上有Linux内核3.7.1的源代码,因此我将尝试在这些源代码的基础上提供你的问题的答案。 我们在代码中有什么 在arch\x86\kernel\traps.c我们有函数early_trap_init() ,其中可以找到下一个代码行:

 set_intr_gate(X86_TRAP_DE, &divide_error); 

正如我们所看到的, set_trap_gate()set_trap_gate()所取代。 如果在下一个回合中我们扩大这个呼吁,我们将实现:

 _set_gate(X86_TRAP_DE, GATE_INTERRUPT, &divide_error, 0, 0, __KERNEL_CS); 

_set_gate是一个负责两件事情的例程:

  1. 构建IDT描述符
  2. 将构造的描述符安装到IDT描述符数组中的目标单元中。 第二个只是记忆复制,对我们来说并不有趣。 但是如果我们看看它如何从提供的参数构造描述符,我们将看到:

     struct desc_struct{ unsigned int a; unsigned int b; }; desc_struct gate; gate->a = (__KERNEL_CS << 16) | (&divide_error & 0xffff); gate->b = (&divide_error & 0xffff0000) | (((0x80 | GATE_INTERRUPT | (0 << 5)) & 0xff) << 8); 

或者最后

 gate->a = (__KERNEL_CS << 16) | (&divide_error & 0xffff); gate->b = (&divide_error & 0xffff0000) | (((0x80 | 0xE | (0 << 5)) & 0xff) << 8); 

正如我们在描述符构造结束时可以看到的,我们将在内存中拥有下一个8字节的数据结构

 [0xXXXXYYYY][0xYYYY8E00], where X denotes digits of kernel code segment selector number, and Y denotes digits of address of the divide_error routine. 

这些8字节的数据结构是处理器定义的中断描述符。 处理器使用它来确定必须采取什么行动来回复接受具有特定向量的中断。 现在我们来看看Intel处理器家族定义的中断描述符的格式:

  80386 INTERRUPT GATE 31 23 15 7 0 +-----------------+-----------------+---+---+---------+-----+-----------+ | OFFSET 31..16 | P |DPL| TYPE |0 0 0|(NOT USED) |4 |-----------------------------------+---+---+---------+-----+-----------| | SELECTOR | OFFSET 15..0 |0 +-----------------+-----------------+-----------------+-----------------+ 

在这种格式中, SELECTOR:OFFSET对定义了函数的地址(以长格式),它将控制回应中断接受。 在我们的例子中,这是__KERNEL_CS:divide_error ,其中divide_error()是Division By Zero例外的实际处理程序。 P标志指定的是描述符应该被认为是一个有效的描述符,由操作系统正确设置,在我们的情况下它处于提升状态。 DPL – 指定使用软中断可以触发divide_error()函数的安全环。 一些背景需要了解这个领域的作用。

通常有三种中断源:

  1. 从OS请求服务的外部设备。
  2. 处理器本身,当发现它收入进入异常状态时,请求操作系统帮助它从这个状态中跳出来。
  3. 在OS控制下在处理器上执行的程序,其从OS请求一些特殊服务。

最后一种情况由处理器以专用指令int XX的形式提供特殊支持。 每次程序需要OS服务时,都会设置描述请求的参数,并用描述中断向量的参数发出int指令,这些参数供OS提供服务使用。 通过发出int指令产生的中断称为软中断。 所以在这里,处理器只有在处理软中断时才考虑DPL字段,在处理器本身或外部设备产生中断的情况下完全忽略它们。 DPL是一个非常重要的功能,因为它禁止应用程序模拟设备,并由此暗示系统的行为。

想象一下,例如,一些应用程序会做这样的事情:

 for(;;){ __asm int 0xFF; //where 0xFF is vector used by system timer, to notify the kernel that the another one timer tick was occurred } 

在这种情况下,计算机上的时间将会在现实生活中变得更快,那么您期望的和您的系统所期望的。 因此,你的系统会非常强烈地行事。 正如你所看到的,处理器和外部设备被认为是可信的,但用户模式应用程序并不是这种情况。 在我们的除零除法的情况下,Linux规定这个异常只能由来自环0的软中断触发,换句话说,只能从内核触发。 因此,如果int 0指令将在内核空间中执行,处理器将把控制权交给divide_error()例程。 如果在用户空间中执行相同的指令,内核会将其作为保护违规,并将控制传递给General Protection Fault异常处理程序(这是所有无效软中断的默认操作)。 但是,如果由处理器自身产生的除零除法异常试图将某个值除以零,则控制将被传递到divide error()例程,而不管发生错误分割的空间。 一般来说,允许应用程序通过软中断来触发除零除法将不会有很大的危害。 但是第一个是丑陋的设计,第二个逻辑可能在幕后,这依赖于通过实际上错误的分割操作才能生成除零除的例外。

TYPE字段指定处理器为响应中断接受而必须采取的辅助操作。 在现实生活中,只使用两种类型的异常描述符:中断描述符和陷阱描述符。 它们仅在一个方面有所不同。 中断描述符强制处理器禁止未来的中断接受,陷阱描述符不这样做。 如果说实话,我没有意识到,为什么Linux内核使用中断描述符进行Division By Zero异常处理。 至于我,陷阱描述符更适合那里。

关于测试程序混淆输出的最后一个注记

 Floating point exception (core dumped) 

由于历史的原因,Linux内核通过发送SIGFPE (读取信号浮点异常)信号给尝试除以零的进程来回答除零除法异常。 是的,不是SIGDBZ (阅读SIGnal除零)。 我知道这是足够混乱。 这种行为的原因是Linux模仿原始的UNIX行为(我认为这种行为被冻结在POSIX中),原来的UNIX有些为什么认为“Division By Zero”异常作为“浮点异常”。 我不知道为什么。

用户模式代码没有业务访问系统表(如段和中断描述符表),它们不打算在操作系统内核之外进行操作,也没有必要。 Linux处理程序中的例外除零,常规保护异常,页面错误和其他拦截异常来源于用户模式和内核模式代码。 它们可以根据来源的不同来处理它们,但是中断描述符表只包含一个针对每种异常(例如上面)的处理程序的地址。 每个处理程序都知道如何处理它的异常。

内核不在用户模式下运行。 它必须处理由用户模式程序产生的陷阱(例如,在用户区的linux进程)。 内核代码预计不会被零除。

我不明白你的问题。 你会怎么实现呢?

“英特尔(R)64和IA-32体系结构和软件开发人员手册”第3A卷第6.12.1.1节中提供了有关部分问题的答案。

只有在INT INT,INT 3或INTO指令产生异常或中断时,处理器才检查中断或陷阱门的DPL。 在这里,CPL必须小于或等于门的DPL。 如果应用程序或程序运行于特权级别3,则可以使用软件中断来访问关键的异常处理程序,例如页面错误处理程序,只要这些处理程序位于更多特权代码段(数字级别较低的特权级别)。 对于硬件生成的中断和处理器检测到的异常,处理器会忽略中断和陷阱门的DPL。

这就是Alex Kreimer的回答

关于消息。 我不完全确定,但似乎操作系统发送SIGFPE信号的过程。