上一篇日志主要講解了對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 ; 注意?。。。。∠到y調用默認有三個參數,分別利用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文件的描述就到此為止。之后還會對進程切換做進一步的闡釋。