本章代碼有一定難度葱椭,請(qǐng)參看視頻獲得更詳細(xì)的代碼開發(fā)流程:
Linux kernel Hacker, 從零構(gòu)建自己的內(nèi)核
上一節(jié)鸿秆,我們繪制了鼠標(biāo)圖案,遺憾的是帚稠,鼠標(biāo)箭頭是死的谣旁,動(dòng)不了,要想讓鼠標(biāo)移動(dòng)滋早,我們需要為內(nèi)核建立中斷機(jī)制榄审。當(dāng)我們移動(dòng)鼠標(biāo)時(shí),鼠標(biāo)會(huì)給CPU發(fā)送信號(hào)杆麸,CPU接收到信號(hào)后搁进,終止當(dāng)前的運(yùn)算浪感,執(zhí)行內(nèi)核給定的代碼以處理鼠標(biāo)發(fā)送的信號(hào),在這段代碼中饼问,內(nèi)核根據(jù)鼠標(biāo)發(fā)送過來的相關(guān)信息篮撑,重新繪制鼠標(biāo)圖像,那么匆瓜,屏幕中的鼠標(biāo)就可以根據(jù)鼠標(biāo)硬件的挪動(dòng)而發(fā)送改變了赢笨。
這樣,我們就面臨兩個(gè)問題驮吱,一是鼠標(biāo)如何給CPU發(fā)送信號(hào)茧妒,二是CPU在接收信號(hào)后,怎么去執(zhí)行內(nèi)核提供的一段代碼左冬。我們先看第一個(gè)問題的處理桐筏,外設(shè)硬件要給CPU發(fā)送信號(hào),需要通過專門的處理芯片拇砰,這個(gè)芯片叫可編程控制器梅忌,俗稱8259A:
####****中斷信號(hào)的發(fā)送機(jī)制
從上圖可以得知,每一個(gè)8259A控制器有8根中斷信號(hào)線除破,總共可以接入15個(gè)外設(shè)硬件牧氮,一般情況下,鼠標(biāo)接入的是從8259A所對(duì)應(yīng)的IRQ4這根信號(hào)線瑰枫,鼠標(biāo)發(fā)送信號(hào)時(shí)踱葛,先通過管線IRQ4將信號(hào)傳遞到從8259A,然后通過管線IRQ2傳遞到主8259A,最后信號(hào)再傳遞給CPU,鍵盤產(chǎn)生的中斷通過主8259A的IRQ1管線向CPU發(fā)送信號(hào).
既然有硬件光坝,那就需要對(duì)其初始化后才能使用尸诽,對(duì)硬件的控制我們前面已經(jīng)說過,需要通過端口發(fā)送命令來完成盯另,要配置這兩個(gè)控制器性含,我們需要對(duì)指定端口發(fā)送1字節(jié)的數(shù)據(jù),這個(gè)一字節(jié)(8 bit)的數(shù)據(jù)鸳惯,我們稱之為ICW(initialization control word).主8259A對(duì)應(yīng)的端口地址是20h,21h, 從8259A對(duì)應(yīng)的端口是A0h和A1h. 對(duì)端口發(fā)送數(shù)據(jù)時(shí)商蕴,順序是定死的,不能違背:
- 往端口20h(主片)或A0h(從片)發(fā)送ICW1
- 往端口21h(主片)或A1h(從片)發(fā)送ICW2
- 往端口21h(主片)或A1h(從片)發(fā)送ICW3
- 往端口20h(主片)或A0h(從片)發(fā)送ICW4
接下來我們可以看看每個(gè)ICW的結(jié)構(gòu)和意義:
ICW1[0...7]:
ICW1[0] 設(shè)置為1表示需要發(fā)送ICW4,0表示不需要發(fā)送ICW4.
ICW1[1] 設(shè)置為1表示單個(gè)8259, 0表示級(jí)聯(lián)8259
ICW1[2] 設(shè)置為1表示4字節(jié)中斷向量悲敷,0表示8字節(jié)中斷向量
ICW1[3] 設(shè)置為1表示中斷形式是水平觸發(fā)究恤,0表示邊沿觸發(fā)
ICW1[4] 必須設(shè)置為1
ICW1[5,6,7] 必須設(shè)置為0
ICW2[0...7]:
ICW2[0,1,2] 對(duì)于80X86架構(gòu)必須設(shè)置為0
ICW2[3...7]: 80X86中斷向量
ICW3[0...7](主片):
ICW3[0] 設(shè)置為1, IR0級(jí)聯(lián)從片后德,0無從片
ICW3[1] 設(shè)置為1, IR1級(jí)聯(lián)從片抄腔,0無從片
ICW3[2] 設(shè)置為1瓢湃, IR2級(jí)聯(lián)從片理张,0無從片
ICW3[3] 設(shè)置為1, IR3級(jí)聯(lián)從片绵患,0無從片
ICW3[4] 設(shè)置為1雾叭, IR4級(jí)聯(lián)從片,0無從片
ICW3[5] 設(shè)置為1落蝙, IR5級(jí)聯(lián)從片织狐,0無從片
ICW3[6] 設(shè)置為1, IR6級(jí)聯(lián)從片筏勒,0無從片
ICW3[7] 設(shè)置為1移迫, IR7級(jí)聯(lián)從片,0無從片
ICW3[0...7](從片):
ICW3[0,1,2] 從片連接主片的IR號(hào)
ICw3[3...7] 必須是0
ICW4[0...7]:
ICW4[0] 設(shè)置為1管行,表示x86模式厨埋,0表示MCS 80/85模式
ICW4[1] 設(shè)置為1,自動(dòng)EOI捐顷;0 正常EOI
ICW4[2,3] 表示主從緩沖模式
ICW4[4] 1表示SFNM模式; 0 sequential 模式
ICW4[5,6,7] 設(shè)置為0
上面一些概念大家可能不明白荡陷,不用擔(dān)心,繼續(xù)往下走迅涮,后面我再對(duì)應(yīng)解釋废赞。下面我們通過代碼配置兩個(gè)中斷控制器:
1: 先向主8259A發(fā)生ICW1:
mov al, 011h
out 02h, al
011h 對(duì)應(yīng)的二進(jìn)制是00010001,對(duì)應(yīng)ICW1的說明,由于ICW1[0]=1表示需要發(fā)送ICW4, ICW1[1] = 0,說明有級(jí)聯(lián)8259A(我們買來的電腦都是級(jí)聯(lián)的)叮姑,
ICW1[2] =0 表示用8字節(jié)來設(shè)置中斷向量號(hào)蛹头,ICW1[3]=0表示中斷形式是邊沿觸發(fā),ICW[4]必須設(shè)置為1戏溺,ICW[5,6,7]必須是0.
2: 向從8259A發(fā)送ICW1:
out A0h, al
3: 向主8259A發(fā)送ICW2:
mov al, 20h
out 021h, al
20h 分解成ICW2 是, ICW2[0,1,2] = 0, 這是強(qiáng)制要求的渣蜗,也就是ICW2的值不能是0x21,0x22之類,只要前三位不是0就不行旷祸,整個(gè)ICW2 = 0x20,這樣的話耕拷,當(dāng)主8259A對(duì)應(yīng)的IRQ0管線向CPU發(fā)送信號(hào)時(shí),CPU根據(jù)0x20這個(gè)值去查找要執(zhí)行的代碼托享,IRQ1管線向CPU發(fā)送信號(hào)時(shí)骚烧,CPU根據(jù)0x21這個(gè)值去查找要執(zhí)行的代碼,依次類推闰围。
4: 向從8259A發(fā)送ICW2:
mov al, 028h
out A1h, al
028h分解成ICW2是ICW[0,1,2]=0,這是強(qiáng)制要求赃绊,整個(gè)ICW2為0x28,表示當(dāng)從8259A的IRQ0管線發(fā)送信號(hào)時(shí)羡榴,CPU根據(jù)數(shù)據(jù)0x28去查找要執(zhí)行的代碼碧查,IRQ1管線發(fā)送信號(hào)時(shí),CPU根據(jù)數(shù)據(jù)0x29去查詢要執(zhí)行的代碼,以此類推忠售。
5: 向主8259A發(fā)送ICW3
mov al, 04h
out 21h, al
04h 分解成ICW3 相當(dāng)于ICW[2] = 1, 這表示從8259A通過主IRQ2管線連接到
主8259A控制器传惠,如上圖所示
6: 向從8259A 發(fā)送 ICW3
mov al, 02h
out Alh , al
根據(jù)從片的ICW3, 將02h對(duì)應(yīng)過來是, ICW[0,1,2] = 2, 表示當(dāng)前從片是從IRQ2管線接入主8259A芯片的稻扬,如上圖卦方。
7: 向主8259A發(fā)送ICW4:
mov al, 002h
out 021h, al
001h 對(duì)應(yīng)的ICW4為,ICW4[0]=1表示當(dāng)前CPU架構(gòu)師80X86泰佳,ICW4[1]=1表示自動(dòng)EOI, 如果這位設(shè)置成0的話盼砍,那么中斷響應(yīng)后,代碼要想繼續(xù)處理中斷逝她,就得主動(dòng)給CPU發(fā)送一個(gè)信號(hào)浇坐,如果設(shè)置成1,那么代碼不用主動(dòng)給CPU發(fā)送信號(hào)就可以再次處理中斷汽绢。
8: 向從8259A發(fā)送ICW4,原理同上:
out 0A1h, al
當(dāng)上面的配置完成后吗跋,我們還需要再向兩個(gè)芯片分別發(fā)送一個(gè)字節(jié),叫OCW(operation control word), 一個(gè)OCW是一字節(jié)數(shù)據(jù)宁昭,也就是8bit,每一bit設(shè)置作用是跌宛,當(dāng)OCW[i] = 1 時(shí),屏蔽對(duì)應(yīng)的IRQ(i)管線的信號(hào),例如OCW[0]=1, 那么IRQ0管線的信號(hào)將不會(huì)被CPU接收积仗,以此類推疆拘。配置代碼如下:
mov al, 11111101b
out 21h, al
表示CPU只接收主8259A, IRQ1管線發(fā)送的信號(hào),其他管線發(fā)送信號(hào)一概忽略寂曹,IRQ1對(duì)應(yīng)的是鍵盤產(chǎn)生的中斷哎迄。
mov al, 11111111b
out 0A1h, al
上面代碼使得CPU忽略所有來自從8259A芯片的信號(hào)。
當(dāng)我們移動(dòng)鼠標(biāo)時(shí)隆圆,鼠標(biāo)是通過從8259A的IRQ4管線向CPU發(fā)送信號(hào)漱挚。
綜合以上,我們得到的初始化代碼如下:
init8259A:
init8259A:
mov al, 011h
out 02h, al
call io_delay
out 0A0h, al
call io_delay
mov al, 020h
out 021h, al
call io_delay
mov al, 028h
out 0A1h, al
call io_delay
mov al, 004h
out 021h, al
call io_delay
mov al, 002h
out 0A1h, al
call io_delay
mov al, 002h
out 021h, al
call io_delay
out 0A1h, al
call io_delay
mov al, 11111101b;允許接收鍵盤中斷
out 021h, al
call io_delay
mov al, 11111111b
out 0A1h, al
call io_delay
ret
io_delay:
nop
nop
nop
nop
ret
####****中斷代碼的執(zhí)行機(jī)制
前面我們處理了硬件如何發(fā)送信號(hào)的問題渺氧,接下來旨涝,我們看看,當(dāng)CPU接收到信號(hào)后侣背,如何執(zhí)行內(nèi)核指定的代碼白华。要執(zhí)行相應(yīng)代碼,CPU必須知道代碼所在的內(nèi)存位置贩耐,這個(gè)信息是通過中斷描述符表來實(shí)現(xiàn)的弧腥,我們看看中斷描述符的數(shù)據(jù)結(jié)構(gòu):
struct GATE_DESCRIPTOR {
short offset_low;
short selector;
char dw_count;
char attribute;
short offset_high;
};
中斷描述符跟前面說到的全局描述符類似,也是用于描述內(nèi)存性質(zhì)的潮太,只不過它專門用于描述可執(zhí)行代碼所在的內(nèi)存管搪, offset_low 和 offset_high 合在一起作為中斷函數(shù)在代碼執(zhí)行段中的偏移,selector 用來指向全局描述符表中的某個(gè)描述符,中斷函數(shù)的代碼就處于該描述符所指向的段中抛蚤,dw_count設(shè)置為0台谢,attribute設(shè)置為08Eh寻狂,我們看看如何在內(nèi)核中加載中斷描述符表:
;Gate selecotor, offset, DCount, Attr
%macro Gate 4
dw (%2 & 0FFFFh)
dw %1
dw (%3 & 1Fh) | ((%4 << 8) & 0FF00h)
dw ((%2>>16) & 0FFFFh)
%endmacro
上面匯編代碼中岁经,%2對(duì)應(yīng)的是4字節(jié)的地址偏移,把地址偏移的低2字節(jié)放到中斷門的前兩字節(jié)蛇券,接下來的一字節(jié)是宏定義的第一個(gè)參數(shù)缀壤,是中斷代碼所在的代碼段的全局描述符,第三行設(shè)置中斷描述符的屬性纠亚,當(dāng)前寫死為08Eh,最后一行設(shè)置中斷代碼偏移的高二字節(jié)塘慕。
在內(nèi)核代碼里,當(dāng)全局描述符表加載到CPU后蒂胞,就是我們加載中斷描述符表的時(shí)機(jī)了图呢,首先我們要初始化一個(gè)中斷描述符:
LABEL_IDT:
%rep 255
Gate SelectorCode32, SpuriousHandler,0, DA_386IGate
%endrep
IdtLen equ $ - LABEL_IDT
IdtPtr dw IdtLen - 1
dd 0
上面代碼中,我們通過指令%rep 255 重復(fù)定義255個(gè)中斷描述符骗随,這么說來蛤织,CPU其實(shí)可以支持255種中斷,其中兩個(gè)8259A芯片的15個(gè)中斷信號(hào)就包含在255個(gè)中斷中鸿染,SpuriousHandler是中斷代碼的入口指蚜,我們把255個(gè)中斷的處理代碼都設(shè)置成SpuriousHandler,也就是無論哪個(gè)中斷發(fā)生,都調(diào)用這個(gè)函數(shù)來處理:
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_IDT
mov dword [IdtPtr + 2], eax
lidt [IdtPtr]
上面代碼跟以前我們加載全局描述符表是一樣的,由于加載全局描述符時(shí)我們使用指令cli關(guān)閉了中斷功能涨椒,因此我們需要回復(fù)中斷功能摊鸡,CPU才能相應(yīng)來自8259A芯片的信號(hào):
[SECTION .s32]
[BITS 32]
LABEL_SEG_CODE32:
;initialize stack for c code
mov ax, SelectorStack
mov ss, ax
mov esp, TopOfStack
mov ax, SelectorVram
mov ds, ax
mov ax, SelectorVideo
mov gs, ax
sti
%include "write_vga_desktop.asm"
jmp $
上面的代碼通過運(yùn)行指令sti 恢復(fù)中斷功能。最后再看看SpuriousHandler的實(shí)現(xiàn):
_SpuriousHandler:
SpuriousHandler equ _SpuriousHandler - $$
call intHandlerFromC
iretd
當(dāng)點(diǎn)擊鍵盤蚕冬,引發(fā)中斷時(shí)免猾,_SpuriousHandler的代碼被調(diào)用,它又調(diào)用了C模塊實(shí)現(xiàn)的函數(shù)intHandlerFromC囤热。我們看看C語言怎么實(shí)現(xiàn)intHandlerFromC的:
void intHandlerFromC(char* esp) {
char*vram = bootInfo.vgaRam;
int xsize = bootInfo.screenX, ysize = bootInfo.screenY;
boxfill8(vram, xsize, COL8_000000, 0,0,32*8 -1, 15);
showString(vram, xsize, 0, 0, COL8_FFFFFF, "PS/2 keyboard");
for (;;) {
io_hlt();
}
show_char();
}
上面函數(shù)先繪制一個(gè)背景為黑色的矩形猎提,在矩形里用白色的字體顯示字符串"PS/2 keyboard"。當(dāng)上面的代碼編譯后赢乓,啟動(dòng)虛擬機(jī)加載內(nèi)核忧侧,初始畫面如下:
然后隨便點(diǎn)擊鍵盤一個(gè)按鈕,結(jié)果如下:
可見牌芋,我們中斷機(jī)制的設(shè)置完全正確蚓炬,CPU能夠接收8259A芯片,同時(shí)CPU能夠正確的執(zhí)行內(nèi)核提交的中斷處理函數(shù)躺屁。