Interrupt Service Routines
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函数以 ret 或 ret 结尾。 这个显而易见但却错误的解决方案导致了操作系统程序员中最“流行”的错误之一:三重故障(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
自版本4.5起,GCC支持 “asm goto” 语句。 它可以用来使ISR成为返回ISR入口点正确地址的函数。