Interrupt Service Routines

来自osdev
跳到导航 跳到搜索

x86体系结构是一个中断驱动的系统。 外部事件触发中断 - 中断正常控制流,并调用中断服务例程(ISR-Interrupt Service Routine)。

这样的事件可以由硬件或软件触发。 硬件中断的一个例子是键盘: 每按一次键,键盘就会触发IRQ1(中断请求1),并调用相应的中断处理程序。 定时器和磁盘请求完成是硬件中断的其他可能来源。

软件驱动的中断由int操作码触发; 例如,MS-DOS提供的服务由触发 INT 21h的软件调用,并在CPU寄存器中传递适用的参数。

为了让系统知道当某个中断发生时调用哪个中断服务例程,当您处于 保护模式 时,ISR的偏移量存储在 中断描述符表-Interrupt Descriptor Table 中, 或者在实模式下的中断向量表中。

ISR由CPU直接调用,并且用于调用ISR的协议不同于调用C函数那样的调用。 最重要的是,ISR必须以 iret 操作码 (或 iretq 在长模式下-是的,即使是使用intel语法) 结束,而通常的C函数以 retret 结尾。 这个显而易见但却错误的解决方案导致了操作系统程序员中最“流行”的错误之一:三重故障(triple-fault)。

调用处理程序的时间

x86

当CPU调用中断处理程序时,CPU将这些值按以下顺序推送到堆栈上:

EFLAGS -> CS -> EIP

CS值用两个字节填充,形成一个双字(doubleword)。

如果门类型不是陷阱门,CPU将清除中断标志。 如果中断是异常,则CPU会将错误代码作为双字推送到堆栈上。

CPU将把相关IDT描述符中的段选择器值加载到CS中。

x86-64

当CPU调用中断处理程序时,它会将RSP寄存器中的值更改为IST中指定的值,如果没有,堆栈将保持不变。 到新堆栈上,CPU按以下顺序推送这些值:

SS:RSP (original RSP) -> RFLAGS -> CS -> RIP

CS被填充成一个四字(quadword)。

如果中断是从不同的Ring调用的,则SS设置为0,表示空选择器。 CPU将修改RFLAGS寄存器,将TF,NT和RF位设置为0。 如果门类型是陷阱门,CPU将清除中断标志。

如果中断是异常,CPU将把一个错误代码推入堆栈,并用字节填充以形成一个四字。

CPU会将段选择器值从关联的IDT描述符加载到CS中,并检查以确保CS是有效的代码段选择器。

问题

许多人回避汇编,希望尽可能多地使用他们最喜欢的高级语言。 GCC (以及其他编译器) 允许您添加内联汇编,因此许多程序员都想编写这样的ISR:

/*如何不编写中断处理程序*/
void interrupt_handler(void)
{
    asm("pushad"); /*保存寄存器。*/
    /* 做点什么 */
    asm("popad");  /*恢复寄存器*/
    asm("iret");   /*这将是三重故障!*/
}

这行不通。 编译器不明白发生了什么。 它不理解在ASM语句之间需要保留寄存器和堆栈;优化器可能会损坏函数。 此外,编译器会在函数之前和之后添加堆栈处理代码,这与iret一起导致类似于以下内容的汇编代码:

push   %ebp
mov    %esp,%ebp
sub    $<size of local variables>,%esp
pushad
# C code comes here
popad
iret
# “leave” 如果使用局部变量,则“pop %ebp”。
leave
ret

这将如何打乱堆栈应该是显而易见的(ebp被push,但从未pop)。 不要这样做。 相反,以下是你的选择。

解决方案

纯汇编

充分了解汇编,在其中编写中断处理程序。;-)

两阶段汇编包装

编写一个汇编包装器调用C函数来完成实际工作,然后执行iret。

/* filename: isr_wrapper.s */
.globl   isr_wrapper
.align   4

isr_wrapper:
    pushad
    cld /* sysV ABI后面的C代码要求在函数输入时清除DF* /
    call interrupt_handler
    popad
    iret
/* 文件名:interrupt_handler.c*/
void interrupt_handler(void)
{
    /* 做点什么 */
}

特定于编译器的中断指令

某些处理器的一些编译器具有允许您声明例程中断、提供#pragma中断或专用宏的指令。 Clang 3.9,Borland C,Watcom C/C,Microsoft C 6.0和免费Pascal编译器1.9.* 以上和GCC提供这一点。 Visual C++提供了一个替代的Naked Functions

Clang

从版本3.9开始,它支持x86/x86-64目标的中断属性

struct interrupt_frame
{
    uword_t ip;
    uword_t cs;
    uword_t flags;
    uword_t sp;
    uword_t ss;
};

__attribute__ ((interrupt))
void interrupt_handler(struct interrupt_frame *frame)
{
    /* do something */
}

Borland C

/* Borland C */
void interrupt interrupt_handler(void)
{
    /* do something */
}

Watcom C/C++

/* Watcom C/C++ */
void _interrupt interrupt_handler(void)
{
    /* do something */
}

Naked Functions

有些编译器可以用来制作中断例程,但需要您手动处理堆栈和返回操作。 这样做需要在没有尾声或序言的情况下生成函数。 这称为使函数naked -这是在Visual C中通过将属性 “_ declspec(naked)” 添加到函数来完成的。 您需要验证是否包含返回操作(如“iretd”),因为这是尾声的一部分,编译器现在已被指示不包含该操作。

如果您打算使用局部变量,则必须按照编译器期望的方式设置堆栈帧;但是,由于ISR是不可重入(non-reentrant)的,您可以简单地使用静态变量。

Visual C++

Visual C还提供了 __LOCAL_SIZE汇编程序宏,它会告知您堆栈上的对象需要多少空间用于函数。

/* Microsoft Visual C++ */
void _declspec(naked) interrupt_handler()
{
    _asm pushad;

    /* do something */

    _asm{
        popad
        iretd
    }
}

GCC / G++

在线文档说,通过使用GCC的函数属性,他们增加了使用__attribute__((interrupt))在C接口中编写中断处理程序的能力。 所以替代如下做法:

/* 黑魔法-强烈不推荐!*/
void interrupt_handler() {
    __asm__("pushad");
    /* do something */
    __asm__("popad; leave; iret"); /*黑魔法*/
}

你可以有正确的(GCC)形式:

struct interrupt_frame;

__attribute__((interrupt)) void interrupt_handler(struct interrupt_frame* frame)
{
    /* do something */
}

GCC的文档指出,如果使用中断属性(interrupt attribute),则在x86和x86-64体系结构上将使用iret指令代替ret。 它还说,“因为GCC不保留SSE、MMX或x87状态,所以GCC选项-mgeneral regs只能用于编译中断和异常处理程序。”

黑魔法

查看上面错误的代码,跳过了正确的C函数退出代码,破坏了堆栈。 现在,考虑这个代码片段,其中手动添加退出代码:

/*黑魔法-强烈反对!*/
void interrupt_handler() {
    __asm__("pushad");
    /* 做点什么 */
    __asm__("popad; leave; iret"); /*黑魔法*/
}

相应的输出看起来有点像这样:

push   %ebp
mov    %esp,%ebp
sub    $<size of local variables>,%esp
pushad
# C code comes here
popad
leave
iret
leave # dead code
ret   # dead code

这假设 leave 是正确的函数结束处理-您正在 “手工” 执行函数返回代码,而将编译器生成的处理保留为 “死代码”。 不用说,对编译器内部的这种假设是危险的。 这段代码可以在不同的编译器上中断,甚至可以在同一编译器的不同版本上中断。 因此,强烈不鼓励它,这里仅出于完整性而列出。

汇编Goto
全文:ISRS、PIC和MULTI TASKING

自版本4.5起,GCC支持 “asm goto” 语句。 它可以用来使ISR成为返回ISR入口点正确地址的函数。