突破BIOS無法連續讀取軟盤72扇區的限制

具體代碼調試和講解請參看視頻:
Linux kernel Hacker, 從零構建自己的內核

一直以來,我們的操作系統加載器,秉承簡單夠用的原則,只要能把編譯好的二進制內核送進內存就可以了,所以加載器的算法是,連續讀取軟盤扇區,將扇區的內容寫入到從0x8000 開始的內存中。以下是我們內核加載器的代碼:

org  0x7c00;

LoadAddr EQU  08000h   


entry:
    mov  ax, 0
    mov  ss, ax
    mov  ds, ax
    mov  es, ax
    
    
    mov          BX, LoadAddr       ; ES:BX 數據存儲緩沖區
    mov          CH, 1        ;CH 用來存儲柱面號
    mov          DH, 0        ;DH 用來存儲磁頭號
    
readFloppy:
   
    cmp          byte [load_count], 0
    je           beginLoad

    
    mov          CL, 1        ;CL 用來存儲扇區號

    mov          AH, 0x02      ;  AH = 02 表示要做的是讀盤操作
    mov          AL,  18        ; AL 表示要練習讀取幾個扇區
    mov          DL, 0         ;驅動器編號,一般我們只有一個軟盤驅動器,所以寫死   
                               ;為0
    INT          0x13          ;調用BIOS中斷實現磁盤讀取功能

    inc          CH
    dec          byte [load_count]
    JC           fin
    add          bx, 512 * 18
    jmp          readFloppy
    
beginLoad:
    jmp          LoadAddr


load_count db 3 ;連續讀取幾個柱面

fin:
    HLT
    jmp  fin

load_count 指的是要讀取的軟盤柱面數。一個1.44M軟盤,其中一個磁面有80個柱面,一個柱面有有兩面,上面和背面,每面對應一個磁道,一個磁道有18個扇區,一個扇區有512字節。上面代碼中,load_cout 的值設置為3, 也就是程序要連續讀取3個柱面,也就是要將軟盤中大約 3* 18 * 512 字節,也就是27k的內容寫入地址為08000h的內存中。

隨著我們開發的操作系統功能越來越強大,其代碼量也越來越大,現在內核編譯后,已經接近15k了,超過27k是遲早的事情,一旦超過27k,那么我們的軟盤在往內存拷貝內核時,就需要連續將4個柱面,也就是72扇區的數據寫入到內存中。按照設想,我們只要把上面load_cout的值改成4就可以了。

然而一旦改成4,問題就出現了,因為讀取4個柱面,也就要連續向內存讀入18*4 = 72個扇區的內容,現在大多數BIOS提供的int 013h軟盤讀取中斷功能,一旦發現調用代碼要讀取的內容有72扇區以上,它就會返回失敗,如果有同學嘗試著把上面代碼的load_cout該成4,然后再運行程序,就會發現內核加載失敗,也就是int 03h 的中斷調用返回失敗,這樣一來,一旦我們的內核大小超過27k, 的話,我們現在的加載器就無法正確加載了。

如果你使用的是虛擬機Bochs 來運行上面的加載器代碼,那么連續讀取軟盤超過72扇區時,Bochs提供的Bios調用會返回失敗。我們看看Bochs源碼中有個Bios功能的函數如下(rombios.c):

 void
 7273 int13_diskette_function(DS, ES, DI, SI, BP, ELDX, BX, DX, CX, AX, IP, CS, FLAGS)
 7274   Bit16u DS, ES, DI, SI, BP, ELDX, BX, DX, CX, AX, IP, CS, FLAGS;
 7275 {
            ......
            
 7337       if ((drive > 1) || (head > 1) || (sector == 0) ||
 7338           (num_sectors == 0) || (num_sectors > 72)) {
 7339         BX_INFO("int13_diskette: read/write/verify: parameter out of range\n");
 7340         SET_AH(1);
 7341         set_diskette_ret_status(1);
 7342         SET_AL(0); // no sectors read
 7343         SET_CF(); // error occurred
 7344         return;
 7345       }
            ....
    }

這個函數模擬的就是軟盤讀取BIOS int 013h中斷功能,當我們的代碼連續讀取幾個柱面的扇區時,我懷疑Bochs會把這些要讀的扇區請求積累起來,然后把要讀取的扇區一次性進行寫入,而不是請求一次就執行一次讀取動作,因此代碼中的條件判斷num_sectors > 72 就會成立,于是連續讀取超過4個柱面也就是72扇區,Bochs模擬器就會返回失敗。由于當前很多虛擬機都大量使用Bochs的源代碼,或是實現機制類似,我在mac上用的是parallels ,它的反應跟Bochs一樣,也是連續讀取軟盤超過72扇區時,返回了錯誤,因此在前面的加載器代碼中,一旦連續讀取4個柱面以上時,讀取請求就會返回失敗。

為了繞過這個限制,現在我們加載器的做法是,不再一次連續讀取18個扇區,而是一次讀取一個扇區,把這個扇區的數據先讀入一個給定的,大小為512字節的緩沖區內,然后再把該緩沖區的內容,拷貝到指定的內存中,也就是我們要多做一次沒有意義的拷貝工作。

由于我們的內核要加載到內存08000h, 因此,我將08000h前512字節,也就是起始地址為07E00h開始的512字節內存作為軟盤一個扇區數據的緩沖區,每次從軟盤讀入一個扇區數據時,先把數據寫入到這個緩沖區,然后再把這個緩沖區的數據拷貝到08000h之后的地址,代碼如下:

org  0x7c00;

LoadAddr EQU  08000h 
BufferAddr EQU 7E0h

BaseOfStack     equ 07c00h

entry:
    mov  ax, 0
    mov  ss, ax
    mov  ds, ax
    
    mov  ax, BufferAddr
    mov  es, ax
   
    mov  ax, 0
    mov  ss, ax
    mov  sp, BaseOfStack
    mov  di, ax
    mov  si, ax
    

    mov          BX, 0       ; ES:BX 數據存儲緩沖區
    mov          CH, 1        ;CH 用來存儲柱面號
    mov          DH, 0        ;DH 用來存儲磁頭號
    mov          CL, 0        ;CL 用來存儲扇區號
    
    
;每次都把扇區寫入地址 07E00處

readFloppy:
    
    cmp          byte [load_count], 0
    je           beginLoad

    mov          bx, 0
    inc          cl
    mov          AH, 0x02      ;  AH = 02 表示要做的是讀盤操作
    mov          AL,  1        ; AL 表示要讀取幾個扇區
    mov          DL, 0         ;驅動器編號,一般我們只有一個軟盤驅動器,所以寫死   
                               ;為0
    INT          0x13          ;調用BIOS中斷實現磁盤讀取功能
    JC           fin


;把剛寫入07E00的一個扇區的內容寫入從08000h開始的地址

copySector:
    push si
    push di
    push cx

    mov  cx, 0200h  ;緩沖區數據大小,也就是512字節
    mov  di, 0
    mov  si, 0
    mov  ax, word [load_section];es
    mov  ds, ax
   
copy:
    cmp  cx, 0
    je   copyend

    mov  al, byte [es:si] ;es:si指向07E00
    mov  byte [ds:di], al

    inc  di
    inc  si
    dec  cx
    jmp  copy

copyend:
    pop cx
    pop di
    pop si

    mov bx, ds
    add bx, 020h
    mov ax, 0
    mov ds, ax
    mov word [load_section], bx
    mov bx, 0
    
    ;end of copySector

    cmp          cl, 18
    jb           readFloppy

    inc          CH
    mov          cl, 0
    dec          byte [load_count]
    jmp          readFloppy
    
beginLoad:

    mov  ax, 0
    mov  ds, ax
  
    jmp          LoadAddr


load_count db 10 ;連續讀取幾個柱面
load_section dw 0800h

fin:

    HLT
    jmp  fin

這段代碼跟開頭的代碼,不同之處在于,第一段代碼,是連續從軟盤讀取扇區數據到指定的內存里,這么做可能存在一個隱性的難以發現的問題:

一是編譯器可能會對代碼進行優化,最終編譯出來的二進制代碼可能給匯編代碼的原意有所不同,編譯后的代碼可能會把所以讀請求積攢起來,然后一次發出讀取命令,由于代碼要讀取4個柱面,每次讀取18個扇區,最終編譯的代碼可能是把4個柱面,總共72個扇區積攢起來,然后一次讀取,這樣的話,就可能違反了Bochs虛擬機的讀取限制。

二是,在讀取數據時,只要讀取的數據不需要立刻使用的話,CPU可能會將數據讀取的請求積累起來,當有數據請求時,才把所有積攢起來的數據讀取請求一次發出,從而提高讀寫效率。

無論是那種情況,都有可能造成一次讀取超過72扇區的請求,從而被虛擬機拒絕。改動后的代碼是,當讀取一個扇區的數據后,程序立馬進行數據拷貝,這樣的話,上面提到的優化機制就不能產生作用,數據的讀取請求就不會被積攢起來,因此就不會遭遇Bochs虛擬機的讀取限制。

我們分析下第二段代碼,BufferAddr 指的是軟盤扇區的數據讀取后要寫入的緩沖區地址,注意它的值是0x7E0, 為什么是0x7E0,而不是0x7E00呢,這是因為這個值會直接付給段寄存器es, 在實模式下,尋址方式是 段地址:段偏移,轉換成直接地址就是 段地址*16 + 段偏移,由于0x7E0 * 16 = 0x7E00, 因此把0x7E00付給段寄存器,就需要把最后一個0去掉,也就是0x7E00 要除以16.

readFloppy 這段代碼通過int 013h調用,讓BIOS從軟盤中讀取一個扇區的數據,然后把數據寫入到起始地址為0x7E00的緩沖區,接著copySector這段代碼把剛寫入緩沖區的數據拷貝到內核的加載地址,也就是08000h之后,變量load_section 用來存儲的是內核加載的起始地址,由于這個值要付給段寄存器ds,所以它的實際值是0800h而不是08000h,每次往這個地址寫入512字節的數據后,下次寫入時,地址要往下偏移512字節,512除以16等于32,因此要把load_section的值加上32,也就是020h.

有了上面改動后,加載代碼能讀取任意多個扇區數據到內存而不用擔心Bochs虛擬機的限制,雖然我用的是Virtual Box, 但據說Virtual Box使用的也是Bochs代碼,所以當我連續加載扇區超過72時,Virtual Box對代碼的數據讀取請求也返回識別,使用上面的修改后,數據的讀取問題也能得到解決了。

Message Box的計時器效果

最后,我們實現一個計數器效果,在write_vga_desktop.c里,根據下面代碼進行更改:

void CMain(void) {
...

for(;;) {
       char* pStr = intToHexStr(counter);
       counter++;
       boxfill8(shtMsgBox->buf, 160, COL8_C6C6C6, 40, 28, 119, 43);
       showString(shtctl, shtMsgBox, 40, 28, COL8_000000,pStr);

       io_cli();
       if (fifo8_status(&keyinfo) + fifo8_status(&mouseinfo)  == 0) {
 
           io_sti();
       }
...
}

我們在主循環里,讓一個變量從0開始自加,然后把其結果顯示在Message Box的主窗體里,如果大家按照上面代碼更改后,會發現界面有明顯的閃爍效果。這是我們當前圖層的刷新機制導致的,在后面課程中,我將與大家研究如何消除這種令人痛苦的閃爍現象。


這里寫圖片描述
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容