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

时间:2011-12-16 06:56:08

标签: linux x86 kernel interrupted-exception

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

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的输出是:

  

浮点异常(核心转储)

所以看起来这不是由0陷阱代码划分的。

5 个答案:

答案 0 :(得分:5)

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

答案 1 :(得分:5)

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

set_intr_gate(X86_TRAP_DE, &divide_error);

我们可以看到set_trap_gate()set_intr_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); 
    
  3. 或者最后

    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字节数据结构是处理器定义的中断描述符。处理器使用它来识别在响应特定向量的中断接受时必须采取的动作。现在让我们看一下英特尔为 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
    +-----------------+-----------------+-----------------+-----------------+
    

    在这种格式中, SELECTOR:OFFSET 对定义了函数的地址(长格式),该函数将接受控制以响应中断接受。在我们的例子中,这是__KERNEL_CS:divide_error,其中divide_error()是除零除异常的实际处理程序。 P 标志指定该描述符应被视为OS正确设置的有效描述符,在我们的情况下,它处于凸起状态。 DPL - 指定使用软中断可以触发divide_error()功能的安全环。需要一些背景来理解该领域的作用。

    一般来说,有三种中断源:

    1. 从OS请求服务的外部设备。
    2. 处理器本身,当发现它收入进入异常状态时 请求操作系统帮助它从该状态退出。
    3. 程序在OS控制下的处理器上执行,该控制从OS请求一些特殊服务。
    4. 最后一种情况得到处理器的特殊支持,其形式为专用指令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()例程。如果将在用户空间中执行相同的指令,则内核将此作为保护违规并将控制权传递给常规保护错误异常处理程序(这是所有无效软中断的默认操作)。但是,如果处理器本身试图将某个值除以零而生成除零除异常,则无论发生错误划分的空间如何,控制都将传递给divide error()例程。 一般来说,通过软中断允许应用程序触发除零除异常不会是一个很大的危害。但是对于第一个它将是一个丑陋的设计,并且对于第二个,一些逻辑可以在场景后面,这依赖于除零事故只能通过实际不正确的除法操作生成的事实。

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

      最后一点关于测试程序的混乱输出

      Floating point exception (core dumped)  
      

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

答案 2 :(得分:3)

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

答案 3 :(得分:2)

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

我不太清楚你的问题。你会怎么做呢?

答案 4 :(得分:1)

部分问题的答案可以在“英特尔(R)64和IA-32架构和软件开发人员手册,第3A卷”的第6.12.1.1节中找到。

  
    

只有在异常或处理时,处理器才会检查中断或陷阱门的DPL         使用INT n,INT 3或INTO指令生成中断。在这里,CPL         必须小于或等于门的DPL。这种限制阻止了         使用a在特权级别3运行的应用程序或过程         软件中断以访问关键异常处理程序,例如页面错误         处理程序,提供那些处理程序放在更多特权代码中         细分(数字较低的特权级别)。对于硬件生成的中断         和处理器检测到的异常,处理器忽略中断的DPL         和陷阱。

  

这是Alex Kreimer回答的问题

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