上一篇日志主要讲解了对8259A以及中断向量表的初始化。
下面的程序主要是时钟中断、硬盘中断以及系统调用入口函数的实现。
1 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2 ; 每个进程的内核态堆栈顶部栈帧应该是这样的
3 ; ss
4 ; esp
5 ; eflags
6 ; cs
7 ; eip
8 ; eax
9 ; ecx
10 ; edx
11 ; ebx
12 ; ebp
13 ; esi
14 ; edi
15 ; ds
16 ; es
17 ; fs
18 ; gs
19 ; 其中ss、esp、eflags、cs、eip是在发生中断时CPU自动压栈的
20 ; 而其他的是由中断程序压栈的,这个顺序不能改变,否则后果自负
21 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
22
23 CS_OFFSET equ 0x30
24 ESP_OFFSET equ 0x38
25 SS_OFFSET equ 0x3c
26
27 ; 一个宏,因为所有的irq中断函数都是先保存现场并将数据段等堆栈段
28 ; 切换到内核态,因此,该操作所有的irq中断入口函数均相同
29 ; 故写成宏节省空间^_!
30 %macro save_all 0
31 push eax
32 push ecx
33 push edx
34 push ebx
35 push ebp
36 push esi
37 push edi
38 push ds
39 push es
40 push fs
41 push gs
42 mov si, ss
43 mov ds, si
44 mov es, si
45 mov gs, si
46 mov fs, si
47 %endmacro
48
49 ; 一个宏,恢复现场
50 %macro recover_all 0
51 pop gs
52 pop fs
53 pop es
54 pop ds
55 pop edi
56 pop esi
57 pop ebp
58 pop ebx
59 pop edx
60 pop ecx
61 pop eax
62 %endmacro
63
64 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
65 ; 时钟中断处理程序
66 ; 这是整个系统中最要求“速度快”的程序,因为时钟中断没隔1/HZ(s)
67 ; 就发生一次,大概它是整个系统调用最频繁的函数,所以需要该函数
68 ; 尽量短,没有必要的函数调用尽量避免。
69 ; 另外判断中断重入minix和linux采取的方法也是不一样的,minix采用
70 ; 一个全局变量,类似于信号量的概念;而linux的方法则比较简单,它
71 ; 直接获取存储在内核堆栈中的cs段寄存器的RPL值来判断被中断的程序
72 ; 是内核态程序的还是用户态的进程;我们打算采用linux的办法,虽然
73 ; minix方法更酷,但是linux的显然更加简单:)
74 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
75 int_clock:
76 save_all
77 ; 增加心跳数
78 inc dword [boot_heartbeat]
79
80 ; 发送EOI指令结束本次中断
81 mov ax, 0x20
82 out 0x20, al
83 sti
84
85 mov eax, [esp + CS_OFFSET]
86 and eax, 0x03
87 cmp eax, 0x0 ; 如果CS段寄存器的RPL为0,则说明是由内核态进入时钟中断,则是中断重入
88 je return
89 call pre_schedule
90 return:
91 recover_all
92 iretd
93
94 int_keyboard:
95 save_all
96
97 recover_all
98 iretd
99
100 int_serial_port2:
101 save_all
102
103 recover_all
104 iretd
105
106 int_serial_port1:
107 save_all
108
109 recover_all
110 iretd
111
112 int_lpt2:
113 save_all
114
115 recover_all
116 iretd
117
118 int_floppy:
119 save_all
120
121 recover_all
122 iretd
123
124 int_lpt1:
125 save_all
126
127 recover_all
128 iretd
129
130 int_rtc:
131 save_all
132
133 recover_all
134 iretd
135
136 int_ps_2_mouse:
137 save_all
138
139 recover_all
140 iretd
141
142 int_fpu_fault:
143 save_all
144
145 recover_all
146 iretd
147
148 ;硬盘中断处理程序
149 int_at_win:
150 save_all
151
152 mov byte [gs:0xb8006], 'e'; 试验硬盘中断是否成功:)
153
154 ; 发送EOI指令给从8259A结束本次中断
155 mov ax, 0x20
156 out 0xa0, al
157 nop
158 nop
159 ; 发送EOI指令给主8259A结束本次中断
160 out 0x20, al
161 nop
162 nop
163
164 ; 调用该函数使buf_info缓冲区生效
165 call validate_buffer
166
167 recover_all
168 iretd
169
170 ; 默认的中断处理函数,所有的未定义中断都会调用此函数
171 int_default:
172 save_all
173 recover_all
174 iretd
175
176 ; 注意从系统调用返回时不需要从栈中弹出eax的值,因为eax保存着调用
177 ; 对应系统调用之后的返回值
178 %macro recover_from_sys_call 0
179 pop gs
180 pop fs
181 pop es
182 pop ds
183 pop edi
184 pop esi
185 pop ebp
186 pop ebx
187 pop edx
188 pop ecx
189 add esp, 4 * 1
190 %endmacro
191
192 ; 系统调用框架,系统调用采用0x30号中断向量,利用int 0x30指令产
193 ; 生一个软中断,之后便进入sys_call函数,该函数先调用save_all框
194 ; 架保存所有寄存器值,然后调用对应系统调用号的入口函数完成系统调用
195 ; 注意!!!!!系统调用默认有三个参数,分别利用ebx、ecx、edx来
196 ; 传递,其中eax保存系统调用号
197 sys_call:
198 save_all
199
200 sti
201
202 push ebx
203 push ecx
204 push edx
205 call [sys_call_table + eax * 4]
206 add esp, 4 * 3
207
208 recover_from_sys_call
209
210 cli
211
212 iretd
213
目前不打算对时钟中断处理函数、硬盘中断处理函数以及系统调用入口框架做解释,因为后序部分将会专门分章节进行讲解。这里只说在发生类似时钟中断、硬盘中断以及软中断int X的系统调用时,CPU如何处理的。
初始情况下假设CPU正在用户态执行某一个进程a,此时的CS、DS、ES、FS、SS均指向用户态进程a的段基地址。
当时钟中断等中断抑或int X的系统调用到来的时候,CPU会自动从TSS中寻找用户态进程a预先保存的ring0下的SS0和ESP0,然后将SS和ESP寄存器值转换成SS0和ESP0,即切换到核心态堆栈段(注意,每个进程都可能会有一个自己独立的ring0堆栈段,这样可以更好的实现进程切换时对内核态的保护,linux对此的做法是在创建一个进程的时候在进程页的末尾申请一块空间作为该进程对应的ring0堆栈段),然后将用户态下的SS、ESP、EFLAGS、CS、EIP的值保存在新的SS0:ESP0堆栈段中。注意以上过程都是CPU自动完成的,然后再通过save_all宏手工将eax、ecx、edx、ebx、ebp、esi、edi、ds、es、fs、gs压入堆栈,然后再执行相应的中断处理程序。完成之后会通过recover_all再按序恢复所有的常规寄存器。然后调用iretd命令从堆栈中弹出EIP、CS、EFLAGS、ESP、SS寄存器,然后再重新恢复进程a的运行。这一过程需要对GDT、IDT、TSS以及保护模式下的中断门、陷阱门有所了解才可以。
不过还有一种情况此处没有涉及:当发生进程切换的时候现场保护与恢复的过程如何呢?这一过程将在后面叙述。
1 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
2 ; 以下为库函数
3 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
4
5 ; 对端口进行写操作
6 ; void out_byte(unsigned short port, unsigned char value);
7 out_byte:
8 mov edx, [esp + 4 * 1]
9 mov al, [esp + 4 * 2]
10 out dx, al
11 nop
12 nop
13 ret
14
15 ; 对端口进行读操作
16 ; uint8 in_byte(unsigned short port);
17 in_byte:
18 mov edx, [esp + 4 * 1]
19 xor eax, eax
20 in al, dx
21 nop
22 nop
23 ret
24
25 ; 对从指定端口进行读操作,读出的n个字节数据放入buf缓冲区中
26 ; void read_port(uint16 port, void* buf, int n);
27 read_port:
28 mov edx, [esp + 4 * 1] ; port
29 mov edi, [esp + 4 * 2] ; buf
30 mov ecx, [esp + 4 * 3] ; n
31 shr ecx, 1
32 cld
33 rep insw
34 ret
35
36 ; 对从指定端口进行写操作,数据源在buf缓冲区中,写n个字节
37 ; void write_port(uint16 port, void* buf, int n);
38 write_port:
39 mov edx, [esp + 4 * 1] ; port
40 mov edi, [esp + 4 * 2] ; buf
41 mov ecx, [esp + 4 * 3] ; n
42 shr ecx, 1
43 cld
44 rep outsw
45 ret
46
47 ; 安装指定中断号的中断处理程序
48 ; extern int install_int_handler(uint8 INT_IV, void* handler);
49 install_int_handler:
50 mov eax, [esp + 4 * 1] ; 中断向量号
51 mov ebx, [esp + 4 * 2] ; 中断程序入口
52 cmp eax, 256
53 jae failed
54 cmp eax, 0
55 jbe failed
56 push PRIVILEGE_KERNEL
57 push ebx
58 push INT_GATE_386
59 push eax
60 call init_idt
61 add esp, 4 * 4
62 failed:
63 ret
64
65 ; 卸载指定中断号的中断处理程序
66 ; extern int uninstall_int_handler(uint8 INT_IV);
67 uninstall_int_handler:
68 mov eax, [esp + 4 * 1] ; 中断向量号
69 cmp eax, 256
70 jae failed
71 cmp eax, 0
72 jbe failed
73 push PRIVILEGE_KERNEL
74 push int_default
75 push INT_GATE_386
76 push eax
77 call init_idt
78 add esp, 4 * 4
79 ret
80
81 ; 安装指定中断号的系统调用入口
82 ; extern int install_sys_call_handler(uint8 INT_IV, void* handler);
83 install_sys_call_handler:
84 mov eax, [esp + 4 * 1] ; 中断向量号
85 mov ebx, [esp + 4 * 2] ; 中断程序入口
86 cmp eax, 256
87 jae failed_inst_sys
88 cmp eax, 0
89 jbe failed_inst_sys
90 push PRIVILEGE_USER
91 push ebx
92 push INT_TRAP_386
93 push eax
94 call init_idt
95 add esp, 4 * 4
96 failed_inst_sys:
97 ret
98
99 ; 打开对应向量号的硬件中断
100 ; 注意,这里传入的参数是硬件中断对应的中断向量号
101 ; 需要将该中断向量号转化为在8259A上的索引号
102 ; void enable_hwint(uint8 IV);
103 enable_hwint:
104 mov ecx, [esp + 4 * 1]
105 cmp cl, IRQ0_IV
106 jae master_1
107 jmp ret_1
108 master_1:
109 cmp cl, IRQ8_IV
110 jae slave_1
111 push MASTER_CTL_MASK_8259
112 call in_byte
113 add esp, 4 * 1
114 sub cl, IRQ0_IV
115 mov bl, 1
116 shl bl, cl
117 xor bl, 0xff
118 and al, bl
119 push eax
120 push MASTER_CTL_MASK_8259
121 call out_byte
122 add esp, 4 * 2
123 jmp ret_1
124 slave_1:
125 cmp cl, IRQ15_IV
126 ja ret_1
127 push SLAVE_CTL_MASK_8259
128 call in_byte
129 add esp, 4 * 1
130 sub cl, IRQ8_IV
131 mov bl, 1
132 shl bl, cl
133 xor bl, 0xff
134 and al, bl
135 push eax
136 push SLAVE_CTL_MASK_8259
137 call out_byte
138 add esp, 4 * 2
139 ret_1:
140 ret
141
142 ; 关闭对应向量号的硬件中断
143 ; 注意,这里传入的参数是硬件中断对应的中断向量号
144 ; 需要将该中断向量号转化为在8259A上的索引号
145 ; void disable_hwint(uint8 IV);
146 disable_hwint:
147 mov ecx, [esp + 4 * 1]
148 cmp cl, IRQ0_IV
149 jae master_2
150 jmp ret_2
151 master_2:
152 cmp cl, IRQ8_IV
153 jae slave_2
154 push MASTER_CTL_MASK_8259
155 call in_byte
156 add esp, 4 * 1
157 sub cl, IRQ0_IV
158 mov bl, 1
159 shl bl, cl
160 or al, bl
161 push eax
162 push MASTER_CTL_MASK_8259
163 call out_byte
164 add esp, 4 * 2
165 jmp ret_2
166 slave_2:
167 cmp cl, IRQ15_IV
168 ja ret_2
169 push SLAVE_CTL_MASK_8259
170 call in_byte
171 add esp, 4 * 1
172 sub cl, IRQ8_IV
173 mov bl, 1
174 shl bl, cl
175 or al, bl
176 push eax
177 push SLAVE_CTL_MASK_8259
178 call out_byte
179 add esp, 4 * 2
180 ret_2:
181 ret
182
183 [SECTION .data]
184 idt:
185 ; idt表共可存放256个中断门描述符
186 times 256 * 8 db 0
187
188 idtr: dw $ - idt - 1
189 dd idt
190
上面这段函数比较简单,是一些库函数,主要包括对端口进行读、写操作;以及安装或者卸载中断处理程序,方法就是通过接受中断号,将IDT中对应的中断描述符置空或初始化;还有安装系统调用,安装系统调用和安装中断处理程序几乎相同,唯一的区别就是门描述符的类型以及门描述符的特权级不同,中断处理程序是中断门,对应的门描述符DPL为0,系统调用是陷阱门,对应的DPL为3,这是因为中断门只需要检查CPL>=处理程序的DPL,而陷阱门除了检查该条件以外还检查CPL<=陷阱门描述符的DPL。这样做的原因是陷阱门是由程序引起的,诸如系统调用之类的,需要从程序中跳入;而中断是硬件引起的。
再后面函数就是打开或关闭8259A上的硬件中断。
关于init.s文件的描述就到此为止。之后还会对进程切换做进一步的阐释。