这是第三部分 chapter Linux内核中有关中断和异常处理 在前面的内容中 part 我们停止了 setup_arch
函数 arch/x86/kernel/setup.c 源代码文件.
我们已经知道该函数执行特定于体系结构的东西的初始化。 在我们的例子中,setup_arch
函数执行与x86_64 architecture相关的初始化工作。 setup_arch
是一个大功能,在上一部分中,我们停止了以下两个异常的两个异常处理程序的设置:
#DB
- 调试异常,将控制从中断的进程转移到调试处理程序;#BP
- 由int
指令引起的断点异常。
这些异常允许x86_64体系结构具有早期异常处理功能,以便于通过kgdb 进行调试
正如您记得的,我们在early_trap_init
函数中设置了这些异常处理程序:
void __init early_trap_init(void)
{
set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK);
set_system_intr_gate_ist(X86_TRAP_BP, &int3, DEBUG_STACK);
load_idt(&idt_descr);
}
来自 arch/x86/kernel/traps.c. 我们已经在上一部分中看到了set_intr_gate_ist
和set_system_intr_gate_ist
函数的实现,现在我们将看看这两个异常处理程序的实现。
Ok,我们在early_trap_init
函数中为#DB
和#BP
异常设置了异常处理程序,现在是时候考虑它们的实现了。但是,在执行此操作之前,我们首先看一下这些异常的详细信息。
第一个异常-#DB
或debug
异常是在发生调试事件时发生的。例如-尝试更改debug register 的内容。debug register是从 Intel 80386 处理器开始在x86
处理器中提供的特殊寄存器,从CPU扩展的名可以知道,这些寄存器中的正在调试这些功能。
这些寄存器允许在代码上设置断点,并读取或写入数据以对其进行跟踪。debug register只能在特权模式下访问,以任何其他特权级别执行时尝试读取或写入调试寄存器都会导致general protection fault 异常。这就是为什么我们对#DB
异常使用了set_intr_gate_ist
,而不对set_system_intr_gate_ist
使用。
#DB
异常的记录编号为1(我们将其作为X86_TRAP_DB传递),并且正如我们在规范中可能会看到的那样,该异常没有错误代码:
+-----------------------------------------------------+
|Vector|Mnemonic|Description |Type |Error Code|
+-----------------------------------------------------+
|1 | #DB |Reserved |F/T |NO |
+-----------------------------------------------------+
第二个异常是处理器执行int 3 指令时发生的#BP
或breakpointv
异常。 与DB
异常不同,#BP
异常可能在用户空间中发生。 我们可以将其添加到代码中的任何位置,让我们看一下简单的程序:
// breakpoint.c
#include <stdio.h>
int main() {
int i;
while (i < 6){
printf("i equal to: %d\n", i);
__asm__("int3");
++i;
}
}
如果我们编译并运行该程序,我们将看到以下输出:
$ gcc breakpoint.c -o breakpoint
i equal to: 0
Trace/breakpoint trap
但是,如果将其与gdb一起运行,我们将看到断点并可以继续执行程序:
$ gdb breakpoint
...
...
...
(gdb) run
Starting program: /home/alex/breakpoints
i equal to: 0
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000400585 in main ()
=> 0x0000000000400585 <main+31>: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1
(gdb) c
Continuing.
i equal to: 1
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000400585 in main ()
=> 0x0000000000400585 <main+31>: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1
(gdb) c
Continuing.
i equal to: 2
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000000000400585 in main ()
=> 0x0000000000400585 <main+31>: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1
...
...
...
从这一刻起,我们对这两个异常有所了解,我们可以把关注点转移到它们的处理程序。
正如您之前可能注意到的那样,set_intr_gate_ist
和set_system_intr_gate_ist
函数在其第二个参数中使用异常处理程序的地址。 否则,我们的两个异常处理程序将是:
debug
;int3
.
你在C代码中找不到这些功能。这些所有功能都可以在内核的*.c/*.h
文件中找到,这些功能的定义位于arch/x86/include/asm/traps.h内核头文件:
asmlinkage void debug(void);
and
asmlinkage void int3(void);
您可能会在这些函数的定义中注意到asmlinkage
指令。 该指令是gcc 的特殊说明符。 实际上,对于从汇编中调用的C函数,我们需要显式声明函数调用约定。在我们的例子中,如果函数使用asmlinkage
描述符创建,则gcc
将编译该函数以从堆栈中检索参数。
So, both handlers are defined in the arch/x86/entry/entry_64.S assembly source code file with the idtentry
macro:
因此,这两个处理程序都在arch/x86/entry/entry_64.S 汇编源代码文件中定义idtentry
宏:
idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK
and
idtentry int3 do_int3 has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK
每个异常处理程序可以由两部分组成。 第一部分是通用部分,所有异常处理程序都相同。 异常处理程序应将general purpose registers 保存在堆栈上,如果异常来自用户空间,则应切换到内核堆栈,并将控制权转移到异常的第二部分 处理程序。 异常处理程序的第二部分完成某些工作取决于某些异常。 例如,页面错误异常处理程序应找到给定地址的虚拟页面,无效的操作码异常处理程序应发送SIGILL
signal 等。
正如我们所见,异常处理程序从arch/x86/kernel/entry_64.S 汇编源代码文件,因此让我们看一下该宏的实现。 我们可以会看到,idtentry
宏接受五个参数:
*sym
用globl name
定义全局符号,它将作为异常处理程序的入口;
*do_sym
符号名称,代表异常处理程序的辅助条目;
*has_error_code
有关异常错误代码的存在的信息。
最后两个参数是可选的:
*paranoid
-向我们展示了如何检查当前模式(稍后将详细解释);
*shift_ist
-显示我们是在“中断堆栈表”上运行的异常。
idtentry
宏的定义如下:
.macro idtentry sym do_sym has_error_code:req paranoid=0 shift_ist=-1
ENTRY(\sym)
...
...
...
END(\sym)
.endm
Before we will consider internals of the idtentry
macro, we should to know state of stack when an exception occurs. As we may read in the Intel® 64 and IA-32 Architectures Software Developer’s Manual 3A, the state of stack when an exception occurs is following:
在考虑identry
宏的内部之前,我们应该知道发生异常时的堆栈状态。 正如我们可能会在Intel®64 and IA-32 Architectures Software Developer's Manual 3A ,则发生异常时的堆栈状态如下:
+------------+
+40 | %SS |
+32 | %RSP |
+24 | %RFLAGS |
+16 | %CS |
+8 | %RIP |
0 | ERROR CODE | <-- %RSP
+------------+
现在我们可以开始考虑idtmacro
的实现了。 #DB
和BP
异常处理程序都定义为:
idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK
idtentry int3 do_int3 has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK
如果我们看一下这些定义,我们可能知道编译器将生成两个带有debug
和int3
名称的例程,并且这两个异常处理程序在经过一些准备后将调用do_debug
和do_int3
辅助处理程序。 第三个参数定义了错误代码的存在,并且我们可以看到我们的两个异常都没有它们。 如上图所示,如果有异常,处理器会将错误代码压入堆栈。 在我们的例子中,debug
和int3
异常没有错误代码。 这可能会带来一些困难,因为对于提供错误代码的异常和未提供错误代码的异常,堆栈的外观会有所不同。 这就是为什么idtentry
宏的实现始于在异常未提供的情况下将伪造的错误代码放入堆栈的原因:
.ifeq \has_error_code
pushq $-1
.endif
这不仅是伪造的错误代码。此外,“-1”还代表无效的系统调用号码,因此系统调用重启逻辑将不会被触发。
idtentry
宏shift_ist
和paranoid
的最后两个参数允许您知道是否从Interrupt Stack Table
运行在堆栈上的异常处理程序。您可能已经知道系统中的每个内核线程都有自己的堆栈。除了这些堆栈外,还有一些专用堆栈与系统中的每个处理器相关联。这些堆栈之一是-异常堆栈。 x86_64 架构提供了称为中断堆栈表
的特殊功能。此功能允许针对指定事件(例如原子异常(如double fault)等)切换到新堆栈。因此,使用shift_ist参数可以让我们知道是否需要为异常处理程序打开IST堆栈。
第二个参数paranoid
定义了一种方法,该方法可以帮助我们知道我们是来自用户空间还是来自异常处理程序。确定这一点的最简单方法是通过CS
段寄存器中的CPL
或Current Privilege Level
。如果等于3
,则来自用户空间;如果为零,则来自内核空间:
testl $3,CS(%rsp)
jnz userspace
...
...
...
// 我们来自内核空间
但是不幸的是,这种方法不能100%的保证。如内核文档中所述:
if we are in an NMI/MCE/DEBUG/whatever super-atomic entry context, which might have triggered right after a normal entry wrote CS to the stack but before we executed SWAPGS, then the only safe way to check for GS is the slower method: the RDMSR.
换句话说,例如,NMI
可能发生在swapgs 指令的关键部分内。 这样,我们应该检查MSR_GS_BASE
模型专用寄存器 的值,该值存储指向每个cpu区域开始的指针。 因此,要检查我们是否来自用户空间,我们应该检查MSR_GS_BASE
模型特定寄存器的值,如果它是负数,则来自内核空间,否则来自用户空间:
movl $MSR_GS_BASE,%ecx
rdmsr
testl %edx,%edx
js 1f
在前两行代码中,我们将模型专用寄存器MSR_GS_BASE
的值读入edx:eax对。 我们不能从用户空间为gs设置负值。 但是从另一面我们知道,物理内存的直接映射是从虚拟地址0xffff880000000000
开始的。 这样,MSR_GS_BASE
将包含从0xffff880000000000
到0xffffc7ffffffffff
的地址。 执行完rdmsr
指令后,%edx
寄存器中的最小可能值为-0xffff8800
,即无符号4个字节的-30720
。 这就是指向每个CPU
区域开始的内核空间gs
包含负值的原因。
将伪错误代码压入堆栈后,我们应该使用以下命令为通用寄存器分配空间:
ALLOC_PT_GPREGS_ON_STACK
在arch / x86 / entry / calling.h 头文件中定义的宏。 该宏仅在堆栈上分配15 * 8字节空间以保留通用寄存器:
.macro ALLOC_PT_GPREGS_ON_STACK addskip=0
addq $-(15*8+\addskip), %rsp
.endm
因此,在执行ALLOC_PT_GPREGS_ON_STACK
之后,堆栈将如下所示:
+------------+
+160 | %SS |
+152 | %RSP |
+144 | %RFLAGS |
+136 | %CS |
+128 | %RIP |
+120 | ERROR CODE |
|------------|
+112 | |
+104 | |
+96 | |
+88 | |
+80 | |
+72 | |
+64 | |
+56 | |
+48 | |
+40 | |
+32 | |
+24 | |
+16 | |
+8 | |
+0 | | <- %RSP
+------------+
在为通用寄存器分配空间之后,我们进行一些检查以了解异常是否来自用户空间,如果是,则应移回中断的进程堆栈或保留在异常堆栈上:
.if \paranoid
.if \paranoid == 1
testb $3, CS(%rsp)
jnz 1f
.endif
call paranoid_entry
.else
call error_entry
.endif
让我们考虑一下所有情况
首先,让我们考虑一个异常具有像我们的debug
和int3
异常这样的paranoid = 1
的情况。 在这种情况下,如果来自用户空间,否则我们将从CS段寄存器中检查选择器,并跳转到1f
标签上,否则将以其他方式调用paranoid_entry
。
Let's consider first case when we came from userspace to an exception handler. As described above we should jump at 1
label. The 1
label starts from the call of the
call error_entry
该例程将所有通用寄存器保存在堆栈中先前分配的区域中:
SAVE_C_REGS 8
SAVE_EXTRA_REGS 8
这两个宏都在arch/x86/entry/calling.h 头文件中定义并移动 通用寄存器的值到堆栈中的某个位置,例如:
.macro SAVE_EXTRA_REGS offset=0
movq %r15, 0*8+\offset(%rsp)
movq %r14, 1*8+\offset(%rsp)
movq %r13, 2*8+\offset(%rsp)
movq %r12, 3*8+\offset(%rsp)
movq %rbp, 4*8+\offset(%rsp)
movq %rbx, 5*8+\offset(%rsp)
.endm
执行SAVE_C_REGS
和SAVE_EXTRA_REGS
之后,堆栈将如下所示:
+------------+
+160 | %SS |
+152 | %RSP |
+144 | %RFLAGS |
+136 | %CS |
+128 | %RIP |
+120 | ERROR CODE |
|------------|
+112 | %RDI |
+104 | %RSI |
+96 | %RDX |
+88 | %RCX |
+80 | %RAX |
+72 | %R8 |
+64 | %R9 |
+56 | %R10 |
+48 | %R11 |
+40 | %RBX |
+32 | %RBP |
+24 | %R12 |
+16 | %R13 |
+8 | %R14 |
+0 | %R15 | <- %RSP
+------------+
在内核将通用寄存器保存在堆栈中之后,应该使用以下命令再次检查来自用户空间:
testb $3, CS+8(%rsp)
jz .Lerror_kernelspace
因为如果报告文档中描述的%RIP
被截断,我们可能有潜在的错误。 无论如何,在两种情况下,都将执行SWAPGS 指令,并且将交换 MSR_KERNEL_GS_BASE
和 MSR_GS_BASE
中的值。 从这一刻开始,%gs
寄存器将指向内核结构的基址。 因此,调用了SWAPGS
指令,这是error_entry
路由的要点。
现在我们可以回到idtentry
宏。 调用error_entry
之后,我们可能会看到以下汇编代码:
movq %rsp, %rdi
call sync_regs
在这里,我们将堆栈指针%rdi
寄存器的基地址放入其中,这将是sync_regs
的第一个参数(根据x86_64 ABI ) 函数并调用arch / x86 / kernel / traps.c 源代码中定义的函数 文件:
asmlinkage __visible notrace struct pt_regs *sync_regs(struct pt_regs *eregs)
{
struct pt_regs *regs = task_pt_regs(current);
*regs = *eregs;
return regs;
}
此函数采用在[arch/x86/include/asm/processor.h]中定义的task_ptr_regs
宏的结果(https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/ include / asm / processor.h)头文件,将其存储在堆栈指针中并返回。 宏task_ptr_regs
扩展为thread.sp0
的地址,该地址表示指向普通内核堆栈的指针:
#define task_pt_regs(tsk) ((struct pt_regs *)(tsk)->thread.sp0 - 1)
正如来自用户空间一样,这意味着异常处理程序将在实际流程上下文中运行。 从sync_regs
获取堆栈指针后,我们切换堆栈:
movq %rax, %rsp
异常处理程序将调用辅助处理程序之前的最后两个步骤是:
1.传递指向pt_regs
结构的指针,该结构包含保留的通用寄存器到%rdi
寄存器:
movq %rsp, %rdi
因为它将作为辅助异常处理程序的第一个参数传递。
2.将错误代码传递到%rsi
寄存器,因为它将是异常处理程序的第二个参数,并在堆栈上将其设置为-1,其目的与我们之前相同 防止重新启动系统调用 :
.if \has_error_code
movq ORIG_RAX(%rsp), %rsi
movq $-1, ORIG_RAX(%rsp)
.else
xorl %esi, %esi
.endif
另外,如果异常不提供错误代码,可能会看到我们将上面的%esi
寄存器清零了。
最后,我们只调用辅助异常处理程序:
call \do_sym
which:
dotraplinkage void do_debug(struct pt_regs *regs, long error_code);
将用于debug
异常和:
dotraplinkage void notrace do_int3(struct pt_regs *regs, long error_code);
将用于int 3
例外。 在本部分中,我们将看不到辅助处理程序的实现,因为它们非常具体,但是在下一部分中将看到其中的一些。
我们只是考虑了在用户空间中发生异常的第一种情况。 我们考虑最后两个。
在这种情况下,内核空间中发生了异常,并且为该异常使用paranoid = 1
定义了idtentry
宏。 paranoid
的值意味着我们应该使用在本部分开头看到的更慢的方式来检查我们是否真的来自内核空间。 paranoid_entry
路由使我们知道这一点:
ENTRY(paranoid_entry)
cld
SAVE_C_REGS 8
SAVE_EXTRA_REGS 8
movl $1, %ebx
movl $MSR_GS_BASE, %ecx
rdmsr
testl %edx, %edx
js 1f
SWAPGS
xorl %ebx, %ebx
1: ret
END(paranoid_entry)
如您所见,此功能代表了我们之前介绍的功能。 我们使用第二(慢)方法来获取有关被中断任务的先前状态的信息。 当我们检查并在来自用户空间的情况下执行SWAPGS
时,我们应该做与之前相同的操作:我们需要将指针指向一个结构,该结构将通用寄存器保存到%rdi
( 将是辅助处理程序的第一个参数),如果异常将其提供给%rsi(将是辅助处理程序的第二个参数),则放置错误代码:
movq %rsp, %rdi
.if \has_error_code
movq ORIG_RAX(%rsp), %rsi
movq $-1, ORIG_RAX(%rsp)
.else
xorl %esi, %esi
.endif
调用异常的辅助处理程序之前的最后一步是清理新的IST
堆栈帧:
.if \shift_ist != -1
subq $EXCEPTION_STKSZ, CPU_TSS_IST(\shift_ist)
.endif
您可能还记得我们将shift_ist
作为iddentry
宏的参数传递了。 在这里,我们检查其值,如果其值不等于-1,则通过shift_ist
索引从中断堆栈表
中获取指向堆栈的指针并进行设置。
在第二种方法的结尾,我们只是像以前一样调用辅助异常处理程序:
call \do_sym
在辅助处理程序完成工作之后,我们将返回到idtentry
宏,下一步将跳转到error_exit
:
jmp error_exit
在相同的arch/x86/entry/entry_64.S 汇编源代码中定义的error_exit
函数 文件,此功能的主要目标是从用户空间或内核空间知道我们的位置,并根据此位置执行SWPAGS
。 将寄存器恢复到先前的状态,并执行iret
指令将控制权转移到中断的任务。
That's all.