为什么linux内核使用陷阱门来处理divide_error异常?

use*_*294 9 linux x86 kernel interrupted-exception

在内核2.6.11.5中,除零异常处理程序设置为:

set_trap_gate(0,&divide_error);
Run Code Online (Sandbox Code Playgroud)

根据"了解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;
}
Run Code Online (Sandbox Code Playgroud)

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

浮点异常(核心转储)

因此看起来这不是由0陷阱代码划分处理的.

Ale*_*mer 5

仅当使用int指令调用软件中断时,才会查看IDT中的DPL位.除以零是由CPU触发的软件中断,因此在这种情况下DPL没有效果


Zar*_*trA 5

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

set_intr_gate(X86_TRAP_DE, &divide_error);
Run Code Online (Sandbox Code Playgroud)

我们可以看到set_trap_gate()被替换为set_intr_gate().如果在下一轮我们扩展此调用,我们将实现:

_set_gate(X86_TRAP_DE, GATE_INTERRUPT, &divide_error, 0, 0, __KERNEL_CS);
Run Code Online (Sandbox Code Playgroud)

_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); 
    
    Run Code Online (Sandbox Code Playgroud)

或者最后

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

正如我们在描述符结构的末尾所看到的,我们将在内存中拥有下一个8字节的数据结构

[0xXXXXYYYY][0xYYYY8E00], where X denotes digits of kernel code segment selector number, and Y denotes digits of address of the divide_error routine.
Run Code Online (Sandbox Code Playgroud)

这些8字节数据结构是处理器定义的中断描述符.处理器使用它来识别在响应特定向量的中断接受时必须采取的动作.现在让我们看一下英特尔为x86处理器系列定义的中断描述符的格式:

                              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
+-----------------+-----------------+-----------------+-----------------+
Run Code Online (Sandbox Code Playgroud)

在这种格式中,SELECTOR:OFFSET对定义了函数的地址(长格式),它将响应中断接受而采取控制.在我们的例子中,这是__KERNEL_CS:divide_error其中的divide_error()是该司的实际处理零异常. P标志指定该描述符应被视为OS正确设置的有效描述符,在我们的例子中它处于凸起状态.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
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,计算机中的时间将比现实生活中的时间快得多,然后您期望并且您的系统期望.结果,您的系统会非常强烈地行为不端.正如您所看到的那样,处理器和外部设备被视为可信任,但用户模式应用程序并非如此.在我们的除零除异常的情况下,Linux指定只能通过来自环0的软中断触发此异常,或者换句话说,仅来自内核.因此,如果int 0指令将在内核空间中执行,则处理器将控制权传递给divide_error()例程.如果将在用户空间中执行相同的指令,则内核将此作为保护违规并将控制权传递给常规保护错误异常处理程序(这是所有无效软中断的默认操作).但是如果处理器本身试图将某个值除以零而产生除零除异常,则divide error()无论发生错误划分的空间如何,控制都将被传递给例程.一般来说,允许应用程序通过软中断触发Trig by Zero异常不会是一个很大的危害.但是对于第一个,它将是一个丑陋的设计,而对于第二个,一些逻辑可以在场景后面,这依赖于除零事故只能通过实际不正确的除法操作生成的事实.

TYPE字段指定处理器在响应中断接受时必须采取的辅助操作.在现实生活中,仅使用两种类型的异常描述符:中断描述符和陷阱描述符.它们仅在一个方面有所不同.中断描述符强制处理器禁用将来的中断接受,并且陷阱描述符不会这样做.如果说实话,我不知道,为什么Linux内核使用中断描述符进行除零异常处理.至于我,陷阱描述符更适合那里.

关于测试程序混乱输出的最后一点注意事项

Floating point exception (core dumped)  
Run Code Online (Sandbox Code Playgroud)

由于历史原因,Linux内核通过向尝试除以零的进程发送SIGFPE(读取SIGnal浮点异常)信号来回复除零除异常.是的,不是SIGDBZ(读取SIGnal Division By Zero).我知道这很令人困惑.这种行为的原因是Linux模仿原始UNIX行为(我认为这种行为在POSIX中被冻结)和原始UNIX一些为什么将"除以零"异常视为"浮点异常".我不知道为什么.

  • SIGFPE 对于一个有用的概念来说是一个不幸的名字:算术异常信号。POSIX / SUS 要求,如果由于算术而传递信号,则该信号必须是 SIGFPE。讽刺的是,在 x86 Linux 上,FP 异常默认被屏蔽,因此只有整数除法可以捕获。[在哪些平台上整数除以零会触发浮点异常?](//stackoverflow.com/q/37262572)。另外[为什么整数除以零会导致浮点异常?](//stackoverflow.com/q/16928942) (2认同)

Bas*_*tch 2

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

我不太明白你的问题。否则你会如何实施它?