上一篇文章说到 main.c 完成了所有的初始化并打开了登录 shell,现在就来具体分析每个初始化函数的实现细节。先整个 trap_init (main.c Line 128) 函数看看,该函数主要涉及 kernel 目录下的这些文件:

  • asm.s
  • trap.c
  • system_call.s

首先,trap_init 是定义在 trap.c 第 181 行的,该函数设置了各硬件异常的处理中断向量,set_trap_gate 与 set_system_gate 的区别在于前者设置的特权级为 0,后者为 3。因此由 set_system_gate 设置的 3,4,5 号陷阱门可以由任何程序调用。这两个函数的第一个参数是中断号,第二个参数是中断预处理程序的地址。trap_init 中涉及的中断处理程序除了 page_fault 定义在 mm/page.s 之外,其余都定义在 kernel 目录下的 asm.s 与 system_call.s 中

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
// trap.c Line 181
void trap_init(void)
{
int i;

set_trap_gate(0,&divide_error); // 除数为 0 时产生
set_trap_gate(1,&debug); // 程序单步跟踪调试,EFLAGS 的 T 标志为 1 时产生该中断
set_trap_gate(2,&nmi); // 由不可屏蔽中断 NMI 产生
set_system_gate(3,&int3); // 断点指令 INT 3 产生,与 debug 处理相同
set_system_gate(4,&overflow); // OF 标志位引发
set_system_gate(5,&bounds); // 未通过地址边界检查引发
set_trap_gate(6,&invalid_op); // 无效指令的操作码(opcode)
set_trap_gate(7,&device_not_available); // 协处理器设备不存在
set_trap_gate(8,&double_fault); // 双中断出错
set_trap_gate(9,&coprocessor_segment_overrun); // 协处理器段超出
set_trap_gate(10,&invalid_TSS); // 无效 TSS 引发
set_trap_gate(11,&segment_not_present); // 选择子指示的段不存在
set_trap_gate(12,&stack_segment); // 堆栈段不存在或寻址超出堆栈段
set_trap_gate(13,&general_protection); // 未通过特权级保护检查引发
set_trap_gate(14,&page_fault); // 页错误
set_trap_gate(15,&reserved); // 保留
set_trap_gate(16,&coprocessor_error); // 协处理器发出 error 信号引发
for (i=17;i<48;i++)
set_trap_gate(i,&reserved); // 17 ~ 47 均先设置为保留
set_trap_gate(45,&irq13); // 协处理器中断
outb_p(inb_p(0x21)&0xfb,0x21); // 允许 8259A 主片的 IRQ2 中断请求
outb(inb_p(0xA1)&0xdf,0xA1); // 允许 8259A 从片的 IRQ13 中断请求
set_trap_gate(39,&parallel_interrupt); // 设置并行口 1 的中断向量
}

set_trap_gate 与 set_system_gate 定义在 include/asm/system.h 中,它们都调用了 _set_gate 用于将中断描述符填入对应的 IDT 表项中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// include/asm/system.h Line 22
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \ // ax = dx = 中断处理程序地址低 16 位
"movw %0,%%dx\n\t" \ // dx = 描述符高 4 字节的低 16 位
"movl %%eax,%1\n\t" \ // 此时 eax 为描述符低 4 字节,放入 IDT 表项低 4 字节
"movl %%edx,%2" \ // edx 为描述符高 4 字节,放入 IDT 表项高 4 字节
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ // 对应 %0,P 位为 1,DPL 与 TYPE 移到相应位
"o" (*((char *) (gate_addr))), \ // 限定符 o 表示内存单元,%1 指的就是这个内存单元
"o" (*(4+(char *) (gate_addr))), \ // 这是 %2 对应的内存单元
"d" ((char *) (addr)),"a" (0x00080000)) // edx = (char *)addr,eax = 0x00080000

// Line 36
#define set_trap_gate(n,addr) \ // &idt[n] 表示 IDT 第 n 项描述符的地址,15 表示陷阱门
_set_gate(&idt[n],15,0,addr) // 0 表示特权级为 0,addr 为中断处理程序的地址

#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr) // 这里特权级为 3

以上是填写 IDT 中断描述符的过程,现在来看这些中断服务程序是怎么实现的,先看 asm.s。因为有的中断过程会产生错误号,故不产生错误号与产生错误号的中断过程预处理方式有一些区别,如图所示(上为高地址):

有无错误号的栈
以没有错误号为例,int 指令会将 ss esp eflags cs eip 依次压入栈中,然后预处理程序压入各寄存器保存现场,同时将真正的中断服务程序的地址赋给 eax,再将 0 压入栈中来占位,最后压入一个指向中断返回地址的指针


asm.s

以下为 asm.s 中无错误号的中断预处理程序:

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
68
69
70
71
/* Line 14 */
.globl divide_error,debug,nmi,int3,overflow,bounds,invalid_op
.globl double_fault,coprocessor_segment_overrun
.globl invalid_TSS,segment_not_present,stack_segment
.globl general_protection,coprocessor_error,irq13,reserved /* 首先定义了这些全局标识符 */

divide_error: /* 0 号中断 */
pushl $do_divide_error /* 将实际处理 0 号中断的程序压栈 */
no_error_code: /* 没有错误码的处理,这里以 divide_error 为例 */
xchgl %eax,(%esp) /* xchg 交换两个操作数内容,即 eax = &do_divide_error,eax 交换入栈 */
pushl %ebx
pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds
push %es
push %fs /* 保存现场 */
pushl $0 /* 错误码用 0 填充 */
lea 44(%esp),%edx /* esp + 44 指向返回地址 */
pushl %edx /* 将该指针压入栈中 */
movl $0x10,%edx /* 加载内核数据段选择符至 ds,es,fs */
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
call *%eax /* 调用 do_divide_error */
addl $8,%esp /* 栈平衡 */
pop %fs
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax /* 还原现场 */
iret /* 中断返回 */

debug: /* 1 号中断,处理方式与 3 号中断相同 */
pushl $do_int3 /* 同样先压处理函数 */
jmp no_error_code /* 再跳到 no_error_code,以下同理 */

nmi: /* 2 号中断 */
pushl $do_nmi
jmp no_error_code

int3: /* 3 号中断 */
pushl $do_int3
jmp no_error_code

overflow: /* 4 号中断 */
pushl $do_overflow
jmp no_error_code

bounds: /* 5 号中断 */
pushl $do_bounds
jmp no_error_code

invalid_op: /* 6 号中断 */
pushl $do_invalid_op
jmp no_error_code

coprocessor_segment_overrun: /* 9 号中断 */
pushl $do_coprocessor_segment_overrun
jmp no_error_code

reserved: /* 15 号中断,同时也是其它保留中断的入口 */
pushl $do_reserved
jmp no_error_code

然后是关于数学协处理器的硬件中断处理,coprocessor_error 定义在 system_call.s 中

1
2
3
4
5
6
7
8
9
10
11
12
/* Line 85 */
irq13: /* 45 号中断 */
pushl %eax /* 保存 eax */
xorb %al,%al
outb %al,$0xF0
movb $0x20,%al
outb %al,$0x20 /* 向 8259A 主片发送中断结束(EOI)信号 */
jmp 1f
1: jmp 1f
1: outb %al,$0xA0 /* 向 8259A 从片发送中断结束信号 */
popl %eax /* 还原 eax 的值 */
jmp coprocessor_error /* 在 system_call.s 中 */

asm.s 最后部分是对于有错误号中断的处理

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
double_fault:			/* 8 号中断 */
pushl $do_double_fault
error_code:
xchgl %eax,4(%esp) /* eax = 错误号,eax 原值入栈 */
xchgl %ebx,(%esp) /* ebx = 处理函数地址,ebx 原值入栈 */
pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds
push %es
push %fs /* 保存现场 */
pushl %eax /* 错误号 */
lea 44(%esp),%eax /* esp + 44 指向中断返回地址 */
pushl %eax /* 入栈 */
movl $0x10,%eax /* ds,es,fs 指向内核数据段 */
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
call *%ebx /* 调用中断处理函数 */
addl $8,%esp /* 栈平衡 */
pop %fs
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax /* 恢复现场*/
iret /* 中断返回 */

invalid_TSS: /* 10 号中断 */
pushl $do_invalid_TSS
jmp error_code

segment_not_present: /* 11 号中断 */
pushl $do_segment_not_present
jmp error_code

stack_segment: /* 12 号中断 */
pushl $do_stack_segment
jmp error_code

general_protection: /* 13 号中断 */
pushl $do_general_protection
jmp error_code

system_call.s

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
/* Line 33 */
SIG_CHLD = 17 /* SIG_CHLD 信号 */

EAX = 0x00 /* 栈中各保存寄存器相对 esp 的偏移 */
EBX = 0x04
ECX = 0x08
EDX = 0x0C
FS = 0x10
ES = 0x14
DS = 0x18
EIP = 0x1C
CS = 0x20
EFLAGS = 0x24
OLDESP = 0x28
OLDSS = 0x2C

/* task_stuct 结构体各字段在结构体中的偏移值 */
state = 0 /* 进程状态码 */
counter = 4 /* 时间片 */
priority = 8 /* 运行优先数 */
signal = 12 /* 信号位图 */
sigaction = 16 /* sigaction 结构体长度为 16 字节 */
blocked = (33*16) /* 受阻塞信号位图的偏移量 */

/* sigaction 结构体各字段在结构体中的偏移值 */
sa_handler = 0 /* 信号处理过程的句柄 */
sa_mask = 4 /* 信号屏蔽码 */
sa_flags = 8 /* 信号集 */
sa_restorer = 12 /* 恢复函数指针 */

nr_system_calls = 86 /* 系统调用总数 */

nr_system_calls 表示提供的系统调用总数。Linux 系统对外仅提供 int 0x80 指令来调用系统中断服务程序,eax 中为中断号,该中断号不能超出 85(nr_system_calls 从 1 计数),ebx,ecx,edx 用于传递参数

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
/* Line 67 */
.globl system_call,sys_fork,timer_interrupt,sys_execve
.globl hd_interrupt,floppy_interrupt,parallel_interrupt
.globl device_not_available, coprocessor_error /* 定义全局描述符 */

.align 4
bad_sys_call: /* 处理中断号超出系统调用总数的中断请求 */
movl $-1,%eax /* eax 赋值 -1,直接中断返回 */
iret
.align 4
reschedule: /* 重新执行调度程序 */
pushl $ret_from_sys_call /* 调度程序返回时来到 ret_from_sys_call 执行 */
jmp schedule /* 定义在 kernel/sched.c */
.align 4
system_call: /* int 0x80 对应的中断处理程序 */
cmpl $nr_system_calls-1,%eax /* 判断是否超出系统调用总数 */
ja bad_sys_call /* 超出了跳到 bad_sys_call */
push %ds
push %es
push %fs
pushl %edx
pushl %ecx
pushl %ebx /* edx,ecx,ebx 压参作为系统调用的参数 */
movl $0x10,%edx /* ds,es 指向内核数据段 */
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx /* fs 指向局部数据段 */
mov %dx,%fs
call sys_call_table(,%eax,4) /* 调用 sys_call_table[4 * eax] */
pushl %eax /* 系统调用返回值入栈 */
movl current,%eax /* eax = 当前任务结构体指针 */
cmpl $0,state(%eax) /* 判断当前任务是否在就绪状态 */
jne reschedule /* 不在的话执行调度程序 */
cmpl $0,counter(%eax) /* 判断当前任务时间片是否用完 */
je reschedule /* 用完也执行调度程序 */
ret_from_sys_call: /* 否则从中断返回继续执行调度选出的当前任务 */
movl current,%eax /* 取当前任务的结构体指针 */
cmpl task,%eax /* 与 task[0] 的地址进行比较,判断是否为进程 0 */
je 3f /* 如果是进程 0,就不需要进行信号处理,直接返回 */
cmpw $0x0f,CS(%esp) /* 判断原调用程序的代码段选择子是否为 0x0f(RPL=3,LDT,代码段) */
jne 3f /* 来确定是否为用户任务,如果是某个中断服务程序则直接返回 */
cmpw $0x17,OLDSS(%esp) /* 如果原调用程序的堆栈选择符不在用户(局部)段中,也直接返回 */
jne 3f
movl signal(%eax),%ebx /* ebx = 当前任务信号位图 */
movl blocked(%eax),%ecx /* ecx = 阻塞(屏蔽)信号位图 */
notl %ecx /* 按位取反得到允许的信号位图 */
andl %ebx,%ecx /* 与当前任务信号位图按位与,得到本次中断处理后的信号位图 */
bsfl %ecx,%ecx /* 从 0 位开始扫描位图,遇到有 1 的位将其偏移(0-31 位)保存在 ecx 中 */
je 3f /* 如果没有信号则直接返回 */
btrl %ecx,%ebx /* 将 ebx 第 ecx 位复位(ebx 为原当前任务信号位图) */
movl %ebx,signal(%eax) /* 重新设置当前任务位图 */
incl %ecx /* 将信号值范围调整为 1 ~ 32 */
pushl %ecx /* 压栈作为 do_signal 的参数 */
call do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds /* 恢复现场 */
iret /* 中断返回 */

sys_call_table 数组定义在 linux/sys.h 中,每个元素对应其中断服务程序的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// linux/sys.h Line 93
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid, sys_sigsuspend, sys_sigpending, sys_sethostname,
sys_setrlimit, sys_getrlimit, sys_getrusage, sys_gettimeofday,
sys_settimeofday, sys_getgroups, sys_setgroups, sys_select, sys_symlink,
sys_lstat, sys_readlink, sys_uselib };

然后是 system_call.s 的剩余部分

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/* Line 130 */
.align 4
coprocessor_error: /* 45号中断,从 asm.s 跳过来执行 */
push %ds
push %es
push %fs
pushl %edx
pushl %ecx
pushl %ebx
pushl %eax /* 保存现场 */
movl $0x10,%eax /* ds,es 指向内核数据段 */
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax /* fs 指向局部数据段 */
mov %ax,%fs
pushl $ret_from_sys_call /* 作为 math_error 的返回地址 */
jmp math_error /* 定义在 kernel/math/error.c 中 */

.align 2
/* 7 号中断,触发该中断的情况有两种,一是当 CR0 中 的 EM 位置位,CPU 执行了一个协处理器指令,
* 该中断用于模拟导致异常的指令;二是 CR0 的 MP 与 TS 置位,CPU 遇到一个协处理器指令或 WAIT,
* 该中断用于更新协处理器状态
*/
device_not_available:
push %ds
push %es
push %fs
pushl %edx
pushl %ecx
pushl %ebx
pushl %eax /* 保存现场 */
movl $0x10,%eax /* ds,es 指向内核数据段 */
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax /* fs 指向局部数据段 */
mov %ax,%fs
pushl $ret_from_sys_call /* 作为中断服务程序的返回地址 */
clts /* TS 位复位 */
movl %cr0,%eax /* 取 CR0内容 */
testl $0x4,%eax /* 测试 EM 是否置位 */
je math_state_restore /* 没有置位则恢复协处理器状态 */
pushl %ebp
pushl %esi
pushl %edi
call math_emulate /* 否则执行数学仿真模拟程序 math_emulate */
popl %edi
popl %esi
popl %ebp
ret

.align 4
/* int 0x20 时钟中断,频率为 100Hz(include/linux/sched.h),即每 10 ms 触发一次 */
timer_interrupt:
push %ds
push %es
push %fs
pushl %edx
pushl %ecx
pushl %ebx
pushl %eax /* 保存现场 */
movl $0x10,%eax /* ds,es 指向内核数据段 */
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax /* fs 指向局部数据段 */
mov %ax,%fs
incl jiffies /* jiffies 自增 1 */
movb $0x20,%al
outb %al,$0x20 /* 给 8259A 主片发送 EOI,结束本次中断 */
movl CS(%esp),%eax /* 取保存的代码段选择子 */
andl $3,%eax /* 将 CPL 保留下来 */
pushl %eax /* 为调用 do_timer 压参 */
call do_timer /* 定义在 kernel/sched.c 中,完成任务切换、计时等工作 */
addl $4,%esp /* 栈平衡 */
jmp ret_from_sys_call /* 跳去设置信号并返回 */

.align 4
sys_execve: /* sys_execve 系统调用 */
lea EIP(%esp),%eax /* 取保存的用户程序返回地址指针 */
pushl %eax /* 压参 */
call do_execve
addl $4,%esp /* 栈平衡 */
ret

.align 4
sys_fork: /* sys_fork 系统调用,创建子进程 */
call find_empty_process /* 为新进程寻找空闲的 pid 及空闲的任务号 */
testl %eax,%eax /* 判断返回结果是否为负数 */
js 1f /* 若为负数直接返回 */
push %gs /* 否则压参,并调用 copy_process() */
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call copy_process
addl $20,%esp
1: ret

hd_interrupt: /* int 0x2E 硬盘中断 */
pushl %eax
pushl %ecx
pushl %edx
push %ds
push %es
push %fs /* 保存现场 */
movl $0x10,%eax /* ds,es 指向内核数据段 */
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax /* fs 指向局部数据段 */
mov %ax,%fs
movb $0x20,%al
outb %al,$0xA0 /* 向 8259A 主片发送 EOI */
jmp 1f /* 起到延时作用 */
1: jmp 1f
1: xorl %edx,%edx /* 清空 edx */
xchgl do_hd,%edx /* 交换二者的值使得 do_hd 函数指针变为 NULL,而 edx = do_hd */
testl %edx,%edx /* 测试该函数指针是否为空 */
jne 1f /* 不为空就跳到 1f 处调用 do_hd */
movl $unexpected_hd_interrupt,%edx /* 否则将 edx 指向 unexpected_hd_interrupt */
1: outb %al,$0x20 /* 向 8259A 主片发送 EOI */
call *%edx /* 调用 edx 指向的函数 */
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax /* 恢复现场 */
iret /* 中断返回 */

floppy_interrupt: /* int 0x26 软驱中断,处理方法与硬盘中断类似 */
pushl %eax
pushl %ecx
pushl %edx
push %ds
push %es
push %fs /* 保存现场 */
movl $0x10,%eax /* ds,es 指向内核数据段 */
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax /* fs 指向局部数据段 */
mov %ax,%fs
movb $0x20,%al
outb %al,$0x20 /* 向 8259A 主片发送 EOI */
xorl %eax,%eax
xchgl do_floppy,%eax
testl %eax,%eax
jne 1f
movl $unexpected_floppy_interrupt,%eax
1: call *%eax
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax /* 恢复现场 */
iret /* 中断返回 */

parallel_interrupt: /* int 0x27 并行口中断 */
pushl %eax /* 没有管这个中断,单纯发送一个 EOI */
movb $0x20,%al
outb %al,$0x20
popl %eax
iret

traps.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
27
28
29
30
31
32
33
34
// Line 13
#include <string.h> // 包含一些头文件

#include <linux/head.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/system.h>
#include <asm/segment.h>
#include <asm/io.h>

// Line 39
int do_exit(long code); //定义一些函数原型

void page_exception(void);

void divide_error(void);
void debug(void);
void nmi(void);
void int3(void);
void overflow(void);
void bounds(void);
void invalid_op(void);
void device_not_available(void);
void double_fault(void);
void coprocessor_segment_overrun(void);
void invalid_TSS(void);
void segment_not_present(void);
void stack_segment(void);
void general_protection(void);
void page_fault(void);
void coprocessor_error(void);
void reserved(void);
void parallel_interrupt(void);
void irq13(void);

接着定义了三个宏,分别用于从段指定位置中读取 1 个字节、读取 4 个字节和取 fs 段寄存器的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Line 22
#define get_seg_byte(seg,addr) ({ \
register char __res; \
__asm__("push %%fs;mov %%ax,%%fs;movb %%fs:%2,%%al;pop %%fs" \
:"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})

#define get_seg_long(seg,addr) ({ \
register unsigned long __res; \
__asm__("push %%fs;mov %%ax,%%fs;movl %%fs:%2,%%eax;pop %%fs" \
:"=a" (__res):"0" (seg),"m" (*(addr))); \
__res;})

#define _fs() ({ \
register unsigned short __res; \
__asm__("mov %%fs,%%ax":"=a" (__res):); \
__res;})

然后定义一个 die 函数,用于输出中断/异常的信息,其三个参数分别为错误名称的字符串指针,出错而被中断的程序在栈中信息的指针(第一张图中的 esp0)及错误码。printk 定义在 kernel/printk.c 中,内核态下不能使用 printf,因其会改变 fs 的值,所以新定义了一个 printk 先将 fs 的值保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void die(char * str,long esp_ptr,long nr)
{
long * esp = (long *) esp_ptr;
int i;

printk("%s: %04x\n\r",str,nr&0xffff); // 打印错误名及错误码低 2 字节
printk("EIP:\t%04x:%p\nEFLAGS:\t%p\nESP:\t%04x:%p\n",
esp[1],esp[0],esp[2],esp[4],esp[3]); // 打印用户任务的 CS:EIP EFLAGS SS:ESP
printk("fs: %04x\n",_fs()); // 打印 fs 的值,下一行打印段基址及段限长
printk("base: %p, limit: %p\n",get_base(current->ldt[1]),get_limit(0x17));
if (esp[4] == 0x17) { // 如果 ss 指向用户栈,则打印用户栈中 16 个字节的数据
printk("Stack: ");
for (i=0;i<4;i++)
printk("%p ",get_seg_long(0x17,i+(long *)esp[3]));
printk("\n");
}
str(i); // 取当前任务号存放在 i 中(include/linux/sched.h)
printk("Pid: %d, process nr: %d\n\r",current->pid,0xffff & i); // 输出进程号及任务号
for(i=0;i<10;i++) // 输出 CS:EIP 处 10 字节的内容
printk("%02x ",0xff & get_seg_byte(esp[1],(i+(char *)esp[0])));
printk("\n\r");
do_exit(11);
}

紧接着定义 asm.s 中调用的那些 do 开头的中断处理程序,这些处理程序都通过 die 来输出信息

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
68
69
70
71
72
73
74
75
76
77
78
// Line 87
void do_double_fault(long esp, long error_code)
{
die("double fault",esp,error_code);
}

void do_general_protection(long esp, long error_code)
{
die("general protection",esp,error_code);
}

void do_divide_error(long esp, long error_code)
{
die("divide error",esp,error_code);
}

// Line 119
void do_nmi(long esp, long error_code)
{
die("nmi",esp,error_code);
}

void do_debug(long esp, long error_code)
{
die("debug",esp,error_code);
}

void do_overflow(long esp, long error_code)
{
die("overflow",esp,error_code);
}

void do_bounds(long esp, long error_code)
{
die("bounds",esp,error_code);
}

void do_invalid_op(long esp, long error_code)
{
die("invalid operand",esp,error_code);
}

void do_device_not_available(long esp, long error_code)
{
die("device not available",esp,error_code);
}

void do_coprocessor_segment_overrun(long esp, long error_code)
{
die("coprocessor segment overrun",esp,error_code);
}

void do_invalid_TSS(long esp,long error_code)
{
die("invalid TSS",esp,error_code);
}

void do_segment_not_present(long esp,long error_code)
{
die("segment not present",esp,error_code);
}

void do_stack_segment(long esp,long error_code)
{
die("stack segment",esp,error_code);
}

void do_coprocessor_error(long esp, long error_code)
{
if (last_task_used_math != current)
return;
die("coprocessor error",esp,error_code);
}

void do_reserved(long esp, long error_code)
{
die("reserved (15,17-47) error",esp,error_code);
}

其中有一个例外,那就是 int 3 的处理程序,它的参数是进入中断后压入栈中的寄存器值,作用是输出寄存器值等信息方便调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void do_int3(long * esp, long error_code,
long fs,long es,long ds,
long ebp,long esi,long edi,
long edx,long ecx,long ebx,long eax)
{
int tr;

__asm__("str %%ax":"=a" (tr):"0" (0)); // tr = ax = 任务寄存器 TR 的值
printk("eax\t\tebx\t\tecx\t\tedx\n\r%8x\t%8x\t%8x\t%8x\n\r",
eax,ebx,ecx,edx);
printk("esi\t\tedi\t\tebp\t\tesp\n\r%8x\t%8x\t%8x\t%8x\n\r",
esi,edi,ebp,(long) esp);
printk("\n\rds\tes\tfs\ttr\n\r%4x\t%4x\t%4x\t%4x\n\r",
ds,es,fs,tr);
printk("EIP: %8x CS: %4x EFLAGS: %8x\n\r",esp[0],esp[1],esp[2]);
}