异常的分类

  • 错误(fault):错误是一种可以被修正的以上。只要错误被修正,处理器可将程序或任务的运行环境还原至异常发生前(已在栈中保存的CS和EIP寄存器值),并重新执行产生异常的指令,也就是异常的返回地址指向错误产生的指令,而不是其后的位置
  • 陷阱(trap):陷阱异常同样允许处理器继续执行任务或程序,只不过处理器对跳过产生异常的指令,即陷阱异常的返回地址指向诱发陷阱指令之后的地址
  • 终止(abort):终止异常用于报告非常严重的错误,它往往无法准确提供产生异常的位置,同时页不允许程序或任务继续执行,典型的终止异常有硬件错误或系统表存在不合逻辑、非法值。

INTEL处理器目前支持的异常/中断:

Screenshot_20220722_234450

Screenshot_20220722_234501

系统异常处理

处理器采用类似汇编指令CALL的调用方法来执行异常/中断处理程序。当处理器捕获到异常/中断时,便会根据异常/中断向量号(Interrupt Vector)从中断描述符表IDT索引出对应的门描述符,再由门描述符定位到处理程序的位置。如果向量号索引到一个中断门或陷阱门,处理器将会像执行CALL指令访问调用门一般,去执行异常/中断处理程序。如果向量号索引到一个任务门,处理器将会发生任务切换,转而执行异常任务或中断任务,这个过程就像执行CALL指令访问调用任务门一样。

异常/中断的处理步骤:

Screenshot_20220723_004245

处理器会根据异常/中断向量号从中断描述符表IDT检索出对应门描述符(中断门或陷阱门),并读取门描述符保存的段选择子。随后从GDT或LDT描述符中检索出处理程序所在的代码段,再根据门描述符记录的段内偏移量,来确定异常/中断异常处理程序的入口地址。

处理器在执行异常/中断处理程序时,会检测异常/中断处理程序所在的代码段的特权级,并与代码段寄存器的特权级进行比较

  • 如果异常/中断处理程序的特权级更高,则会在异常/中断处理程序执行前切换栈空间,

    • 处理器会从任务状态段TSS中取出对应特权级的栈段选择子和栈指针,并将它们作为异常/中断处理程序的栈空间进行切换。在栈空间切换过程中,处理器将自动把切换前的SS和ESP寄存器压入异常/中断串处理程序栈
    • 在栈空间切换过程中,处理器还会保存被中断程序的EFLAGS、CS和EIP寄存器值到异常/中断处理程序栈
    • 如果异常会产生错误码,则将其保存在异常任务栈内,位于EIP寄存器之后
  • 如果异常/中断处理程序的特权级与代码段寄存器的特权级相等

    • 处理器将保存被中断程序的EFLGAS、CS和EIP寄存器值到栈中
    • 如果异常会产生错误码,则将其保存在异常栈内,位于EIP寄存器之后

Screenshot_20220723_005856

处理器必须借助IRET指令才能从异常/中断处理程序返回。IRET指令会还原之前保存的EFLAGS寄存器的值。EFLAGS寄存器的IOPL标志位只有在CPL=0时才可被还原,而IF标志位只有在CPL<=IOPL时候才能改变。如果在执行处理程序时发生过栈空间切换,那么执行IRET指令将切换回被中断程序栈。

异常/中断处理的标志位。当处理器穿过中断门执行异常/中断处理程序时,处理器会在标志寄存器EFLAGS入栈后复位TF标志位,以关闭单步调试功能(处理器还会复位VM、RF和NT标志位)。在执行IRET指令过程中,处理器还会被中断程序的标志寄存器EFLAGS,进而相继还原TF、VM、RF、NT等标志位。

  • 当处理器穿过中断门执行异常/中断处理程序时,处理器将复位IF标志位,以防止其他中断请求干扰异常/中断处理程序。处理器在随后执行的IRET指令时,将栈中保存的EFLAGS寄存器值还原,进而置位IF标志位。
  • 当处理器穿过陷阱门执行异常/中断处理程序时,处理器却不对复位IF标志位。

中断和异常向量在同一张IDT内,IDT表的前32个向量号被异常占用,而且每个异常的向量号固定不能更改,从向量号32开始被中断处理程序所用。

当异常/中断发生时,执行ignore_int模块,显示unknow interrupt or fault at RIP提示信息。

设置IDT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
setup_IDT:
# 使用lea取出ignore_int标识符的基地址
# 这个基地址要被拆分成第0-15位和第48-63位,分别放在EAX和EDX寄存器中
# 然后在将EAX的值加載到中断描述符的低位,將EDX的值加載到中断描述符的高位。
# 假设ignore_int地址为0x222 3333 4444 5555
leaq ignore_int(%rip), %rdx
# 位数:
# 0-1 RPL 请求特权级
# 1-2 TI 指示目标段描述符所在描述符表类型
# 3-15 用于索引目标段描述符
# 2
# 段选择符,我们要选用代码段的段选择符,所以使用0008h号GDT段选择符。
# %rax = 0x0000 0000 0008 0000 = 0b ... 0000 0000 0000 1000 ...
movq $(0x08 << 16), %rax
# 段内偏移 15:00 %ax = ignore_int 函数的低16位
# %ax = 0x5555
# %rax = 0x0000 0000 0008 5555
movw %dx, %ax
# %rcx = 0x0000 8E00 0000 0000 = 0b00 ... 1000 1110 ...
# 32-34 IST ( Interrupt Stack Table,中断枝表)是IA-32e模式为任务状态段引人的新型战指针,其功能与 RSP相同,只不过IST切换中断棋指针时不会考虑特权级切换。
# 35-39:0
# 40-43:Type 第40-43位为段描述符类型标志(TYPE),我们设置的是1110.即将此段描述符标记为“386中断门”。
# 44-44:0
# 45-46:DPL 描述符特权级
# 47-48:P 指定调用门描述符是否有效
movq $(0x8e00 << 32), %rcx
# %rax = 0x0000 8E00 0008 5555
# %rcx = 0x0000 8E00 0000 0000
addq %rcx, %rax
# %ecx = ignore_int 函数的低32位 0x4444 5555 %rcx = 0x0000 8E00 4444 5555
movl %edx, %ecx
# %ecx = 0x0000 4444 %rcx = 0x0000 8E00 0000 4444
shrl $16, %ecx
# %rcx = 4444 0000 0000 0000
shlq $48, %rcx
# %rax = 0x4444 8E00 0008 5555
addq %rcx, %rax
# %rdx = 0x0000 0000 2222 3333
shrq $32, %rdx
leaq IDT_Table(%rip), %rdi
mov $256, %rcx

# 将256个中断描述符同一初始化
# rax寄存器保存低8字节,rdx寄存器保存高8字节,循环256次
rp_sidt:
movq %rax, (%rdi)
movq %rdx, 8(%rdi)
addq $0x10, %rdi
dec %rcx
jne rp_sidt

setup_IDT负责初始化中断描述符表IDT内的每个中断描述符(共256项)。它将ignore_int模块的起始地址和其他配置信息,有序地格式化成IA-32e模式的中断门描述符结构信息,并把结构信息保存到RAX寄存器(结构信息的低8字节)和RDX寄存器(结构信息的高8字节)中。最后借助rp_sidt模块将这256个中断描述符项统一初始化。

中断描述符格式如下:

Screenshot_20220724_012727

IDT初始化完毕之后,我们还需要对任务状态段描述符TSS Descriptor进行初始化。

设置TSS

TSS描述如下:

Screenshot_20220724_014307

64位TSS:

Screenshot_20220724_015431

对任务状态段描述符TSS Descriptor进行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
setup_TSS64:
leaq TSS64_Table(%rip), %rdx
xorq %rax, %rax
xorq %rcx, %rcx
movq $0x89, %rax
shlq $40, %rax
movl %edx, %ecx
shrl $24, %ecx
shlq $56, %rcx
addq %rcx, %rax
xorq %rcx, %rcx
movl %edx, %ecx
andl $0xffffff, %ecx
shlq $16, %rcx
addq %rcx, %rax
addq $103, %rax
leaq GDT_Table(%rip), %rdi
movq %rax, 64(%rdi)
shrq $32, %rdx
movq %rdx, 72(%rdi)

mov $0x40, %ax
ltr %ax

movq go_to_kernel(%rip), %rax
pushq $0x08
pushq %rax
lretq

go_to_kernel:
.quad start_kernel

这部分程序负责初始化GDT(IA-32e模式)内的TSS Descriptor,并通过LTR汇编指令吧TSS Descriptor的选择子加载到TR寄存器中。因为当前内核程序已经运行于0特权级,即使产生异常页不会切换任务栈,从而无需访问TSS,那么暂且无需初始化TSS。在特权级无变化的情况下,即使不加载TSS Descriptor的选择子到TR寄存器,异常仍可以被捕获并处理。

中断处理

中断处理模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# ignore int
ignore_int:
# CLD指令即告诉程序si,di向內存地址增大的方向走。
cld
pushq %rax
pushq %rbx
pushq %rcx
pushq %rdx
pushq %rbp
pushq %rdi
pushq %rsi

pushq %r8
pushq %r9
pushq %r10
pushq %r11
pushq %r12
pushq %r13
pushq %r14
pushq %r15

movq %es, %rax
pushq %rax
movq %ds, %rax
pushq %rax

movq $0x10, %rax
movq %rax, %ds
movq %rax, %es

leaq int_msg(%rip), %rax
pushq %rax
movq %rax, %rdx
movq $0x00000000, %rsi
movq $0x00ff0000, %rdi
movq $0, %rax
callq color_printk
addq $0x8, %rsp

loop:
jmp loop

popq %rax
movq %rax, %ds
popq %rax
movq %rax, %es

popq %r15
popq %r14
popq %r13
popq %r12
popq %r11
popq %r10
popq %r9
popq %r8

popq %rsi
popq %rdi
popq %rbp
popq %rdx
popq %rcx
popq %rbx
popq %rax
iretq

int_msg:
.asciz "unknown interrupt or fault at RIP\n"

这段程序先保存各个寄存器的值,然后将DSES段寄存器设置成内核数据段,紧接着将color_printk函数准备参数,并采用寄存器传递方式传递参数。显示提示信息后,再执行JMP指令死循环在ignore_int模块中。

触发#DE(除0)异常:

1
2
3
4
5
6
7
void start_kernel(void) {
...
i = 1 / 0;
while (1) {
;
}
}

运行结果:

Screenshot_20220724_010331

系统异常处理

初始化IDT

为各个异常量身定制处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void sys_vector_init() {
set_trap_gate(0, 1, divide_error);
set_trap_gate(1, 1, debug);
set_intr_gate(2, 1, nmi);
set_system_gate(3, 1, int3);
set_system_gate(4, 1, overflow);
set_system_gate(5, 1, bounds);
set_trap_gate(6, 1, undefined_opcode);
set_trap_gate(7, 1, dev_not_available);
set_trap_gate(8, 1, double_fault);
set_trap_gate(9, 1, coprocessor_segment_overrun);
set_trap_gate(10, 1, invalid_TSS);
set_trap_gate(11, 1, segment_not_present);
set_trap_gate(12, 1, stack_segment_fault);
set_trap_gate(13, 1, general_protection);
set_trap_gate(14, 1, page_fault);

// 15 intel reserved. do not use
set_trap_gate(16, 1, x87_FPU_error);
set_trap_gate(17, 1, alignment_check);
set_trap_gate(18, 1, machine_check);
set_trap_gate(19, 1, SIMD_exception);
set_trap_gate(20, 1, virtualization_exception);
}

这段程序为各个异常向量配置了处理函数和栈指针,此处使用64为TSS里的IST1区域来记录栈基地址。函数set_intr_gateset_trap_gateset_system_gate分别用于初始化IDT内的各表项,这些函数会根据异常的功能,把描述符配置为DPL=0的中断门和陷阱门或者DPL=3的陷阱门。

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline void set_intr_gate(unsigned int n, unsigned char ist, void *addr) {
_set_gate(IDT_Table + n, 0x8e, ist, addr); // P, DPL=0, TYPE=E
}

static inline void set_trap_gate(unsigned int n, unsigned char ist, void *addr) {
_set_gate(IDT_Table + n, 0x8f, ist, addr); // P, DPL=0, TYPE=F
}

static inline void set_system_gate(unsigned int n, unsigned char ist, void *addr) {
_set_gate(IDT_Table + n, 0xef, ist, addr); // P, DPL=3, TYPE=F
}

static inline void set_system_intr_gate(unsigned int n, unsigned char ist,
void *addr) {
_set_gate(IDT_Table + n, 0xee, ist, addr); // P, DPL=3, TYPE=E
}

_set_gate来初始化IDT内的各个表项,这个宏函数的参数IDT_Table是内核执行头文件head.S内声明的标识符.global IDT_Table,在gate.h文件中使用代码extern struct gate_struct IDT_Table[];将其声明为外部变量供_set_gate等函数使用。

_set_gate函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define _set_gate(gate_selector_addr, attr, ist, code_addr)                    \
do { \
unsigned long __d0, __d1; \
__asm__ __volatile__("movw %%dx, %%ax \n\t" \
"andq $0x7, %%rcx \n\t" \
"addq %4, %%rcx \n\t" \
"shlq $32, %%rcx \n\t" \
"addq %%rcx, %%rax \n\t" \
"xorq %%rcx, %%rcx \n\t" \
"movl %%edx, %%ecx \n\t" \
"shrq $16, %%rcx \n\t" \
"shlq $48, %%rcx \n\t" \
"addq %%rcx, %%rax \n\t" \
"movq %%rax, %0 \n\t" \
"shrq $32, %%rdx \n\t" \
"movq %%rdx, %1 \n\t" \
: "=m"(*((unsigned long *)(gate_selector_addr))), \
"=m"(*(1 + (unsigned long *)(gate_selector_addr))), \
"=&a"(__d0), "=&d"(__d1) \
: "i"(attr << 8), "3"((unsigned long *)(code_addr)), \
"2"(0x8 << 16), "c"(ist) \
: "memory"); \
} while (0)

该宏函数通过内嵌汇编语句(使用64位汇编指令和通用寄存器)实现,其主要作用是初始化中断描述符表内的门描述符(每个门描述符16B)。

异常处理调用过程

异常的处理过程会涉及程序执行现场的保存工作,由于C语言无法实现寄存器压栈操作,那么就必须先借助汇编语句在异常处理程序的入口处保存程序的现场环境,然后再执行C语言的异常处理函数。

定义各个寄存器(程序执行现场)在栈中的保存顺序(基于栈指针的偏移值):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "linkage.h"

R15 = 0x00
R14 = 0x08
R13 = 0x10
R12 = 0x18
R11 = 0x20
R10 = 0x28
R9 = 0x30
R8 = 0x38
RBX = 0x40
RCX = 0x48
RDX = 0x50
RSI = 0x58
RDI = 0x60
RBP = 0x68
DS = 0x70
ES = 0x78
RAX = 0x80
FUNC = 0x88
ERRORCODE = 0x90
RIP = 0x98
CS = 0xa0
RFLAGS = 0xa8
OLDRSP = 0xb0
OLDSS = 0xb8

异常处理程序或者中断处理程序,在处理程序的起始处都必须保存被中断程序的执行现场,上面这些符号常量定义了栈中各个寄存器相对于栈顶(进程执行现场保存完毕时的栈顶地址)的增量偏移。由于栈向下生长,借助当前栈指针寄存器RSP加符号常量,便可取得程序执行现场的寄存器值。OLDSSOLDRSPRFLAGSCSRIP等符号常量,用于有特权级切换的场景;如果没有特权级切换,则只有RFLAGSCSRIP等符号常量可用。符号常量ERRORCODE必须根据异常的实际功能才可确定是否有错误码入栈,并且在返回被中断程序时必须手动弹出栈中的错误码(IRET指令无法自动弹出错误码)

还原被中断程序的执行现场:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
RESTORE_ALL:
popq %r15
popq %r14
popq %r13
popq %r12
popq %r11
popq %r10
popq %r9
popq %r8
popq %rbx
popq %rcx
popq %rdx
popq %rsi
popq %rdi
popq %rbp
popq %rax
movq %rax, %ds
popq %rax
movq %rax, %es
popq %rax
addq $0x10, %rsp
iretq

根据保存程序执行现场时的寄存器压栈顺序,从栈中反向弹出各个寄存器的值。64位汇编中PUSH CS/DS/ES/SSPOP DS/ES/SS都是无效指令,所以使用popq %rax; movq %rax, %ds;来替代。汇编代码addq $0x10, %rsp将栈指针向上移动16B,目的是弹出栈中变量FUNCERRORCODE,之后便可以执行iretq还原被中断程序的执行现场,该指令可以自行判断还原过程是否设计特权级切换,如果是就将OLDSSOLDRSP从栈中弹出。

程序执行现场的保存过程和异常处理函数,#DE异常的处理模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ENTRY(divide_error)
pushq $0
pushq %rax
leaq do_divide_error(%rip), %rax
xchgq %rax, (%rsp)

error_code:
pushq %rax
movq %es, %rax
pushq %rax
movq %ds, %rax
pushq %rax
xorq %rax, %rax

pushq %rbp
pushq %rdi
pushq %rsi
pushq %rdx
pushq %rcx
pushq %rbx
pushq %r8
pushq %r9
pushq %r10
pushq %r11
pushq %r12
pushq %r13
pushq %r14交互
pushq %r15

cld
movq ERRORCODE(%rsp), %rsi
movq FUNC(%rsp), %rdx

movq $0x10, %rdi
movq %rdi, %ds
movq %rdi, %es

movq %rsp, %rdi

callq *%rdx
jmp ret_from_exception

由于#DE异常不会产生错误码,但是为了确保所有异常处理程序的寄存器压栈顺序一致,便向栈中压入数值0来占位。之后将RAX寄存器值压入栈中,再将异常处理函数do_divide_error的起始地址存入RAX寄存器,并借助汇编代码xchgq将RAX寄存器与栈中的值交互。将do_divide_error函数的起始地址存入栈中,而且还恢复了RAX寄存器的值。

进程的执行现场保存完毕后,就可以执行对应异常处理函数。由于被中断程序可能运行在应用层(3特权级),而异常处理程序运行与内核层(0特权级),那么进入内核层后,DSES段寄存器应该重新加载为内核层数据段。紧接着把异常处理函数的起始地址装入RDX寄存器,将错误码和栈指针分别存入RSIRDI寄存器,供异常处理函数使用,并使用汇编代码callq调用异常处理函数。AT&T汇编中,CALLJMP指令的操作数前缀中含有符号*,则表示调用/跳转的目标是绝对地址,否则调用/跳转的目标是相对地址。

异常处理函数执行完毕后,跳转到ret_from_exception还原被中断程序的执行现场:

1
2
3
ret_from_exception:
ENTRY(ret_from_intr)
jmp RESTORE_ALL

当前仅负责还原被中断程序的执行现场。

NMI不可屏蔽中断处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ENTRY(nmi)
pushq %rax
cld
pushq %rax

pushq %rax
movq %es, %rax
pushq %rax
movq %ds, %rax
pushq %rax
xorq %rax, %rax

pushq %rbp
pushq %rdi
pushq %rsi
pushq %rdx
pushq %rcx
pushq %rbx
pushq %r8
pushq %r9
pushq %r10
pushq %r11
pushq %r12
pushq %r13
pushq %r14
pushq %r15

movq $0x10, %rdx
movq %rdx, %ds
movq %rdx, %es

movq $0, %rsi
movq %rsp, %rdi

callq do_nmi
jmp RESTORE_ALL

#NMI不可屏蔽中断不是异常,而是一个外部中断,从而不会产生错误码。#NMI应该执行中断处理过程,所以与异常处理相似。

#TS异常处理过程:

1
2
3
4
5
ENTRY(invalid_TSS)
pushq %rax
leaq do_invalid_TSS(%rip), %rax
xchgq %rax, (%rsp)
jmp error_code

有错误码的#TS异常处理模块无需项栈中压入数值0占位,而是直接使用同一返回模块,其他执行步骤与#DE异常一致。

#PF异常处理模块:

1
2
3
4
5
ENTRY(page_fault)
pushq %rax
leaq do_page_fault(%rip), %rax
xchgq %rax, (%rsp)
jmp error_code

#PF处理过程与#TS一致,区别在于错误码的位图格式不同。

异常处理函数

#DE

#DE异常的处理函数:

1
2
3
4
5
6
7
8
9
10
void do_divide_error(unsigned long rsp, unsigned long error_code) {
unsigned long *p = NULL;
p = (unsigned long *)(rsp + 0x98);
color_printk(
RED, BLACK,
"do_divide_error(0), ERROR_CODE:%#018lx,RSP:%#018lx,RIP:%#018lx\n",
error_code, rsp, *p);
while (1)
;
}

#DE异常处理目前只有打印错误信息功能,即显示错误码、栈指针和异常产生的错误地址。由于#DE异常没有错误码,这里会显示之前入栈的0值。其中代码p = (unsigned long *)(rsp + 0x98);中数值0x98对应着上文的符号常量RIP=0x98,意思是将栈指针寄存器RSP(异常处理模块将栈指针寄存器RSP的值作为参数存入RDI寄存器)的值向上索引0x98个字节,以获取被中断程序执行现场的RIP寄存器值,并将其作为产生异常指令的地址值。然后借助while(1);保持死循环。

#NMI

#NMI的处理函数:

1
2
3
4
5
6
7
8
9
void do_nmi(unsigned long rsp, unsigned long error_code) {
unsigned long *p = NULL;
p = (unsigned long *)(rsp + 0x98);
color_printk(RED, BLACK,
"do_nmi(2), ERROR_CODE:%#018lx,RSP:%#018lx,RIP:%#018lx\n",
error_code, rsp, *p);
while (1)
;
}

#DE相同,目前只有打印错误信息的功能。

#TS

如果异常产生的原因(外部中断或INT n指令不会产生错误码),关系到一个特殊的段选择子或IDT向量,那么处理器就会在异常处理程序栈中存入错误码。指向IRET指令并不会在异常返回过程中弹出错误码,因此在异常返回前必须手动将错误码从栈中弹出。

根据中断门、陷阱门或任务门的操作数位宽,错误码可以是一个字或双字,为了保证双字错误码入栈时的栈对齐,错误码的高半部分被保留。这个格式与段选择子相似,只不过段选择子中的TI标志位与RPL区域此刻已变为错误码的三个标志。

Screenshot_20220724_214223

  • EXT。如果该位被置位,说明异常是在向程序投递外部事件的过程中触发,如一个中断或一个更早期的异常。
  • IDT。如果该位被置位,说明错误码的段选择子部分记录的是中断描述符IDT内的门描述符;而复位则说明其记录的是描述符表GDT/IDT内的描述符。
  • TI。只有当IDT标志位复位时才有效。如果被置位,则说明错误码的段选择子部分记录的是局部描述符表LDT内的段描述符或门描述符;而复位则说明它记录的是全局描述符表GDT的描述符。

错误码的段选择子部分可以所以IDT、GDT或LDT等描述符表内的段描述符或门描述符。在某些条件下,错误码是NULL值(除EXT位外所有都被清零),这表明并非由引用特殊段或访问NULL段描述符而产生的。

#TS异常处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void do_invalid_TSS(unsigned long rsp, unsigned long error_code) {
unsigned long *p = NULL;
p = (unsigned long *)(rsp + 0x98);
color_printk(
RED, BLACK,
"do_invalid_TSS(10), ERROR_CODE:%#018lx,RSP:%#018lx,RIP:%#018lx\n",
error_code, rsp, *p);
if (error_code & 0x01) {
color_printk(
RED, BLACK,
"The exception occurred during delivery of an event external to the "
"program, such as an interrupt or an earlier exception.\n");
}
if (error_code & 0x02) {
color_printk(RED, BLACK, "Refers to a gate descriptor in the IDT.\n");
} else {
color_printk(RED, BLACK,
"Refers to a descriptor in the IDT or the current LDT.\n");
}
if ((error_code & 0x02) == 0) {
if (error_code & 0x04) {
color_printk(RED, BLACK,
"Refers to a segment or gate descriptor in the LDT.\n");
} else {
color_printk(RED, BLACK, "Refers to a descriptor in the current GDT.\n");
}
}
color_printk(RED, BLACK, "Segment Selector Index:%#010x\n",
error_code & 0xfff8);
while (1)
;
}

TS异常处理函数do_invalid_TSS首先会显示异常的错误码值、栈指针值、异常产生的程序地址等日志信息,然后解析错误码显示详细信息。

#PF

处理器为页错误异常提供了两条信息,来帮助诊断异常产生的原因以及恢复方法。

  • 栈中的错误码。页错误页异常的错误码格式与其他异常完全不同,处理器使用了5个标志来描述页错误异常。
    • P标志指示异常是否由一个不存在的页所引发(p=0),或者进入了违规区域(p=1),或者使用保留位(p=1)
    • W/R标志位指示异常是否由读取页(W/R=0)或写入页(W/R=1)所产生
    • U/S标志位指示异常是否由用户模式(U/S=1)或超级模式(U/S=0)所产生
    • 当CR4控制寄存器的PSE标志位或PAE标志位被置位时,处理器将检测页表项的保留位,RSVD标志位指示异常是否由保留位所产生
    • I/D标志位指示异常是否由获取指令所产生
  • CR2寄存器。CR2控制寄存器保存触发异常时的线性地址,异常处理程序可根据线性地址定位到页目录项和页表项。页处理错误程序应该在第二个页错误发生前保存CR2寄存器的值,以免再次触发页错误异常。

Screenshot_20220724_215114

在异常触发时,处理器会将CS和EIP寄存器的值保存到处理程序栈内,通常情况下这两个寄存器值指向触发异常的指令。如果#PF异常发生在任务切换期间,那么CS和EIP寄存器可能指向新任务的第一条指令。

#PF异常处理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void do_page_fault(unsigned long rsp, unsigned long error_code) {
unsigned long *p = NULL;
unsigned long cr2 = 0;
__asm__ __volatile__("movq %%cr2, %0" : "=r"(cr2)::"memory");

p = (unsigned long *)(rsp + 0x98);
color_printk(RED, BLACK,
"do_page_fault(14), "
"ERROR_CODE:%#018lx,RSP:%#018lx,RIP:%#018lx\n",
error_code, rsp, *p);
if (error_code & 0x01) {
color_printk(RED, BLACK, "Page Not-Present.\t");
}
if (error_code & 0x02) {
color_printk(RED, BLACK, "Wirte Cause Fault.\t");
} else {
color_printk(RED, BLACK, "Read Cause Fault.\t");
}
if (error_code & 0x04) {
color_printk(RED, BLACK, "Fault int user(3)\t");
} else {
color_printk(RED, BLACK, "Fault in supervisor(0,1,2)\t");
}
if (error_code & 0x08) {
color_printk(RED, BLACK, ",Reserved Bit Cause Fault\t");
}
if (error_code & 0x10) {
color_printk(RED, BLACK, ",Instruction fetch Case Fault");
}
color_printk(RED, BLACK, "\n");
color_printk(RED, BLACK, "CR2:%#018lx\n", cr2);
while (1)
;
}

do_page_fault首先将CR2控制寄存器的值保存到变量cr2中,C不支持寄存器操作,所以使用内联汇编将CR2寄存器的值复制到cr2变量中。

触发异常

借助i=1/0触发#DE异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define load_TR(n)                                                             \
do { \
__asm__ __volatile__("ltr %%ax" : : "a"(n << 3) : "memory"); \
} while(0)

void start_kernel(void) {
...
load_TR(8);
set_tss64(0xffff800000007c00, 0xffff800000007c00, 0xffff800000a00000,
0xffff800000007c00, 0xffff800000007c00, 0xffff80ng' to your Pictures folder. 0000007c00,
0xffff800000007c0, 0xffff800000007c00, 0xffff800000007c00,
0xffff800000007c00);
sys_vector_init();

i = 1 / 0; // divide 0 exception
// i = *(int*) 0xffff80000aa00000; // #PF exception
while (1) {
;
}
}

这段程序通过load_TR宏将TSS段描述符的段选择子加载到TR寄存器,而函数set_tss64则负责配置TSS段内各个RSP和IST项。

TSS段描述符被加载到TR寄存器后,其B标志(Busy)会被置位,如果重复加载此描述符则产生#TS异常。所以需要将加载TSS段选择子的汇编代码删除。

1
2
// mov $0x40, %ax
// ltr %ax

运行结果如下:

Screenshot_20220724_220733

将代码中改为i = *(int*) 0xffff80000aa00000;,可触发PF异常。

运行结果如下:

Screenshot_20220724_220816