官术网_书友最值得收藏!

  • 30天自制操作系統
  • (日)川合秀實
  • 4347字
  • 2020-03-11 14:01:54

5 通往32位模式之路

我們一直都沒有說明asmhead.nas中的如同謎一樣的大約100行程序。等筆者回過神兒來,已經到了可以說明的時候了。現在就是個好機會,我們來具體看看。

在沒有說明的這段程序中,最開始做的事情如下:

asmhead.nas節選

; PIC關閉一切中斷
;   根據AT兼容機的規格,如果要初始化PIC,
;   必須在CLI之前進行,否則有時會掛起。
;   隨后進行PIC的初始化。

        MOV      AL,0xff
        OUT      0x21, AL
        NOP                        ; 如果連續執行OUT指令,有些機種會無法正常運行
        OUT      0xa1, AL

        CLI                        ; 禁止CPU級別的中斷

這段程序等同于以下內容的C程序。

io_out(PIC0_IMR, 0xff); /* 禁止主PIC的全部中斷 */
io_out(PIC1_IMR, 0xff); /* 禁止從PIC的全部中斷 */
io_cli(); /* 禁止CPU級別的中斷*/

如果當CPU進行模式轉換時進來了中斷信號,那可就麻煩了。而且,后來還要進行PIC的初始化,初始化時也不允許有中斷發生。所以,我們要把中斷全部屏蔽掉。

順便說一下,NOP指令什么都不做,它只是讓CPU休息一個時鐘長的時間。

■■■■■

再往下看,會看到以下部分。

asmhead.nas節選(續)

; 為了讓CPU能夠訪問1MB以上的內存空間,設定A20GATE

        CALL     waitkbdout
        MOV      AL,0xd1
        OUT      0x64, AL
        CALL     waitkbdout
        MOV      AL,0xdf          ; enable A20
        OUT      0x60, AL
        CALL     waitkbdout

這里的waitkbdout,等同于wait_KBC_sendready(以后還會詳細說明)。這段程序在C語言里的寫法大致如下:

#define KEYCMD_WRITE_OUTPORT     0xd1
#define KBC_OUTPORT_A20G_ENABLE 0xdf

    /* A20GATE的設定 */
    wait_KBC_sendready();
    io_out8(PORT_KEYCMD, KEYCMD_WRITE_OUTPORT);
    wait_KBC_sendready();
    io_out8(PORT_KEYDAT, KBC_OUTPORT_A20G_ENABLE);
    wait_KBC_sendready(); /* 這句話是為了等待完成執行指令 */

程序的基本結構與init_keyboard完全相同,功能僅僅是往鍵盤控制電路發送指令。

這里發送的指令,是指令鍵盤控制電路的附屬端口輸出0xdf。這個附屬端口,連接著主板上的很多地方,通過這個端口發送不同的指令,就可以實現各種各樣的控制功能。

這次輸出0xdf所要完成的功能,是讓A20GATE信號線變成ON的狀態。這條信號線的作用是什么呢?它能使內存的1MB以上的部分變成可使用狀態。最初出現電腦的時候,CPU只有16位模式,所以內存最大也只有1MB。后來CPU變聰明了,可以使用很大的內存了。但為了兼容舊版的操作系統,在執行激活指令之前,電路被限制為只能使用1MB內存。和鼠標的情況很類似喲。A20GATE信號線正是用來使這個電路停止從而讓所有內存都可以使用的東西。

最后還有一點,“wait_KBC_sendready(); ”是多余的。在此之后,雖然不會往鍵盤送命令,但仍然要等到下一個命令能夠送來為止。這是為了等待A20GATE的處理切實完成。

■■■■■

我們再往下看。

asmhead.nas節選(續)

; 切換到保護模式

[INSTRSET "i486p"]                ; “想要使用486指令”的敘述
        LGDT [GDTR0] ; 設定臨時GDT
        MOV EAX, CR0
        AND EAX,0x7fffffff;設bit31為0(為了禁止分頁)
        OR EAX,0x00000001;設bit0為1(為了切換到保護模式)
        MOV CR0, EAX
        JMP pipelineflush

pipelineflush:
        MOV      AX,1*8           ;  可讀寫的段 32bit
        MOV      DS, AX
        MOV      ES, AX
        MOV      FS, AX
        MOV      GS, AX
        MOV      SS, AX

INSTRSET指令,是為了能夠使用386以后的LGDT, EAX, CR0等關鍵字。

LGDT指令,不管三七二十一,把隨意準備的GDT給讀進來。對于這個暫定的GDT,我們以后還要重新設置。然后將CR0這一特殊的32位寄存器的值代入EAX,并將最高位置為0,最低位置為1,再將這個值返回給CR0寄存器。這樣就完成了模式轉換,進入到不用頒的保護模式。CR0,也就是control register 0,是一個非常重要的寄存器,只有操作系統才能操作它。

保護模式本來的說法應該是“protected virtual address mode”,翻譯過來就是“受保護的虛擬內存地址模式”。與此相對,從前的16位模式稱為“real mode”,它是“real address mode”的省略形式,翻譯過來就是“實際地址模式”。這些術語中的“virtual”,“real”的區別在于計算內存地址時,是使用段寄存器的值直接指定地址值的一部分呢,還是通過GDT使用段寄存器的值指定并非實際存在的地址號碼。與先前的16位模式不同,段寄存器的解釋不是16倍,而是能夠使用GDT。這里的“保護”,來自英文的“protect”。在這種模式下,應用程序既不能隨便改變段的設定,又不能使用操作系統專用的段。操作系統受到CPU的保護,所以稱為保護模式。

在保護模式中,有帶保護的16位模式,和帶保護的32位模式兩種。我們要使用的,是帶保護的32位模式。

講解CPU的書上會寫到,通過代入CR0而切換到保護模式時,要馬上執行JMP指令。所以我們也執行這一指令。為什么要執行JMP指令呢?因為變成保護模式后,機器語言的解釋要發生變化。CPU為了加快指令的執行速度而使用了管道(pipeline)這一機制,就是說,前一條指令還在執行的時候,就開始解釋下一條甚至是再下一條指令。因為模式變了,就要重新解釋一遍,所以加入了JMP指令。

而且在程序中,進入保護模式以后,段寄存器的意思也變了(不再是乘以16后再加算的意思了),除了CS以外所有段寄存器的值都從0x0000變成了0x0008。CS保持原狀是因為如果CS也變了,會造成混亂,所以只有CS要放到后面再處理。0x0008,相當于“gdt + 1”的段。

■■■■■

我們再往下讀程序。

asmhead.nas節選(續)

; bootpack的轉送

        MOV      ESI, bootpack     ; 轉送源
        MOV      EDI, BOTPAK       ; 轉送目的地
        MOV      ECX,512*1024/4
        CALL     memcpy

; 磁盤數據最終轉送到它本來的位置去

; 首先從啟動扇區開始

        MOV      ESI,0x7c00       ; 轉送源
        MOV      EDI, DSKCAC       ; 轉送目的地
        MOV      ECX,512/4
        CALL     memcpy

; 所有剩下的

        MOV      ESI, DSKCAC0+512 ; 轉送源
        MOV      EDI, DSKCAC+512  ; 轉送目的地
        MOV      ECX,0
        MOV      CL, BYTE [CYLS]
        IMUL     ECX,512*18*2/4  ; 從柱面數變換為字節數/4
        SUB      ECX,512/4        ; 減去IPL
        CALL     memcpy

簡單來說,這部分程序只是在調用memcpy函數。為了讓大家掌握這段程序的大意,我們將這段程序寫成了C語言形式。雖然寫法本身可能不很正確,但有助于大家抓住程序的中心思想。

memcpy(bootpack,     BOTPAK,      512*1024/4);
memcpy(0x7c00,       DSKCAC,      512/4      );
memcpy(DSKCAC0+512, DSKCAC+512, cyls * 512*18*2/4-512/4);

函數memcpy是復制內存的函數,語法如下:

memcpy(轉送源地址,轉送目的地址,轉送數據的大小);

轉送數據大小是以雙字為單位的,所以數據大小用字節數除以4來指定。在上面3個memcpy語句中,我們先來看看中間一句。

memcpy(0x7c00, DSKCAC, 512/4);

DSKCAC是0x00100000,所以上面這句話的意思就是從0x7c00復制512字節到0x00100000。這正好是將啟動扇區復制到1MB以后的內存去的意思。下一個memcpy語句:

memcpy(DSKCAC0+512, DSKCAC+512, cyls * 512*18*2/4-512/4);

它的意思就是將始于0x00008200的磁盤內容,復制到0x00100200那里。

上文中“轉送數據大小”的計算有點復雜,因為它是以柱面數來計算的,所以需要減去啟動區的那一部分長度。這樣始于0x00100000的內存部分,就與磁盤的內容相吻合了。順便說一下,IMULIMUL,來自英文“integer multipule”(整數乘法)。是乘法運算,SUBSUB,來自英文“substract”(減法)。是減法運算。它們與ADD(加法)運算同屬一類。

現在我們還沒說明的函數就只有有程序開始處的memcpy了。bootpack是asmhead.nas的最后一個標簽。haribote.sys是通過asmhead.bin和bootpack.hrb連接起來而生成的(可以通過Makefile確認),所以asmhead結束的地方,緊接著串連著bootpack.hrb最前面的部分。

memcpy(bootpack, BOTPAK, 512*1024/4);
  → 從bootpack的地址開始的512KB內容復制到0x00280000號地址去。

這就是將bootpack.hrb復制到0x00280000號地址的處理。為什么是512KB呢?這是我們酌情考慮而決定的。內存多一些不會產生什么問題,所以這個長度要比bootpack.hrb的長度大出很多。

■■■■■

后面還剩50行程序,我們繼續往下看。

asmhead.nas節選(續)

; 必須由asmhead來完成的工作,至此全部完畢
;   以后就交由bootpack來完成

; bootpack的啟動

        MOV      EBX, BOTPAK
        MOV      ECX, [EBX+16]
        ADD      ECX,3             ; ECX += 3;
        SHR      ECX,2             ; ECX /= 4;
        JZ       skip              ; 沒有要轉送的東西時
        MOV      ESI, [EBX+20]     ; 轉送源
        ADD      ESI, EBX
        MOV      EDI, [EBX+12]     ; 轉送目的地
        CALL     memcpy
skip:
        MOV      ESP, [EBX+12]     ; 棧初始值
        JMP      DWORD 2*8:0x0000001b

結果我們仍然只是在做memcpy。它對bootpack.hrb的header(頭部內容)進行解析,將執行所必需的數據傳送過去。EBX里代入的是BOTPAK,所以值如下:

[EBX + 16]......bootpack.hrb之后的第16號地址。值是0x11a8
[EBX + 20]......bootpack.hrb之后的第20號地址。值是0x10c8
[EBX + 12]......bootpack.hrb之后的第12號地址。值是0x00310000

上面這些值,是我們通過二進制編輯器,打開harib05d的bootpack.hrb后確認的。這些值因harib的版本不同而有所變化。

SHR指令是向右移位指令,相當于“ECX >>=2; ”,與除以4有著相同的效果。因為二進制的數右移1位,值就變成了1/2;左移1位,值就變成了2倍。這可能不太容易理解。還是拿我們熟悉的十進制來思考一下吧。十進制的時候,向右移動1位,值就變成了1/10(比如120 → 12);向左移動1位,值就變成了10倍(比如3 → 30)。二進制也是一樣。所以,向右移動2位,正好與除以4有著同樣的效果。

JZ是條件跳轉指令,來自英文jump if zero,根據前一個計算結果是否為0來決定是否跳轉。在這里,根據SHR的結果,如果ECX變成了0,就跳轉到skip那里去。在harib05d里,ECX沒有變成0,所以不跳轉。

而最終這個memcpy到底用來做什么事情呢?它會將bootpack.hrb第0x10c8字節開始的0x11a8字節復制到0x00310000號地址去。大家可能不明白為什么要做這種處理,但這個問題,必須要等到“紙娃娃系統”的應用程序講完之后才能講清楚,所以大家現在不懂也沒關系,我們以后還會說明的。

最后將0x310000代入到ESP里,然后用一個特別的JMP指令,將2 * 8代入到CS里,同時移動到0x1b號地址。這里的0x1b號地址是指第2個段的0x1b號地址。第2個段的基地址是0x280000,所以實際上是從0x28001b開始執行的。這也就是bootpack.hrb的0x1b號地址。

這樣就開始執行bootpack.hrb了。

■■■■■

下面要講的內容可能有點偏離主題,但筆者還是想介紹一下“紙娃娃系統”的內存分布圖。

0x00000000-0x000fffff : 雖然在啟動中會多次使用,但之后就變空。(1MB)
0x00100000-0x00267fff : 用于保存軟盤的內容。(1440KB)
0x00268000-0x0026f7ff : 空(30KB)
0x0026f800-0x0026ffff : IDT(2KB)
0x00270000-0x0027ffff : GDT(64KB)
0x00280000-0x002fffff : bootpack.hrb(512KB)
0x00300000-0x003fffff : 棧及其他(1MB)
0x00400000-              : 空

這個內存分布圖當然是筆者所做出來的。為什么要做成這呢?其實也沒有什么特別的理由,覺得這樣還行,跟著感覺走就決定了。另外,雖然沒有明寫,但在最初的1MB范圍內,還有BIOS, VRAM等內容,也就是說并不是1MB全都空著。

從軟盤讀出來的東西,之所以要復制到0x00100000號以后的地址,就是因為我們意識中有這個內存分布圖。同樣,前幾天,之所以能夠確定正式版的GDT和IDT的地址,也是因為這個內存分布圖。

如果一開始就制作內存分布圖,那么做起操作系統來就會順利多了。

■■■■■

關于內存分布圖就講這么多,還是讓我們回到asmhead.nas的說明上來吧。

asmhead.nas節選(續)

waitkbdout:
        IN        AL,0x64
        AND       AL,0x02
        IN        AL,0x60         ; 空讀(為了清空數據接收緩沖區中的垃圾數據)
        JNZ      waitkbdout       ; AND的結果如果不是0,就跳到waitkbdout
        RET

這就是waitkbdout所完成的處理。基本上,如前面所說的那樣,它與wait_KBC_sendready相同,但也添加了部分處理,就是從Ox60號設備進行IN的處理。也就是說,如果控制器里有鍵盤代碼,或者是已經累積了鼠標數據,就順便把它們讀取出來。

JNZ與JZ相反,意思是“jump if not zero”。

■■■■■

只剩下一點點內容了,下面是memcpy程序。

asmhead.nas節選(續)

memcpy:
        MOV      EAX, [ESI]
        ADD      ESI,4
        MOV      [EDI], EAX
        ADD      EDI,4
        SUB      ECX,1
        JNZ      memcpy           ; 減法運算的結果如果不是0,就跳轉到memcpy
        RET

這是復制內存的程序。不用筆者解釋,大家也能明白。

■■■■■

最后是剩下來的全部內容。

asmhead.nas節選(續)

ALIGNB 16
GDT0:
RESB     8                 ; NULL selector
DW       0xffff,0x0000,0x9200,0x00cf ; 可以讀寫的段(segment)32bit
DW       0xffff,0x0000,0x9a28,0x0047 ; 可以執行的段(segment)32bit(bootpack用)

DW       0
GDTR0:
DW       8*3-1
DD       GDT0

ALIGNB  16
bootpack:

ALIGNB指令的意思是,一直添加DBO,直到時機合適的時候為止。什么是“時機合適”呢?大家可能有點不明白。ALIGNB 16的情況下,地址能被16整除的時候,就稱為“時機合適”。如果最初的地址能被16整除,則ALIGNB指令不作任何處理。

如果標簽GDT0的地址不是8的整數倍,向段寄存器復制的MOV指令就會慢一些。所以我們插入了ALIGNB指令。但是如果這樣,“ALIGNB 8”就夠了,用“ALIGNB 16”有點過頭了。最后的“bootpack:”之前,也是“時機合適”的狀態,所以筆者就適當加了一句“ALIGNB 16”。

GDT0也是一種特定的GDT。0號是空區域(null sector),不能夠在那里定義段。1號和2號分別由下式設定。

set_segmdesc(gdt + 1, 0xffffffff,   0x00000000, AR_DATA32_RW);
set_segmdesc(gdt + 2, LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER);

我們用紙筆事先計算了一下,然后用DW排列了出來。

GDTR0是LGDT指令,意思是通知GDT0說“有了GDT喲”。在GDT0里,寫入了16位的段上限,和32位的段起始地址。

■■■■■

到此為止,關于asmhead.nas的說明就結束了。就是說,最初狀態時,GDT在asmhead.nas里,并不在0x00270000~0x0027ffff的范圍里。IDT連設定都沒設定,所以仍處于中斷禁止的狀態。應當趁著硬件上積累過多數據而產生誤動作之前,盡快開放中斷,接收數據。

因此,在bootpack.c的HariMain里,應該在進行調色板(palette)的初始化以及畫面的準備之前,先趕緊重新創建GDT和IDT,初始化PIC,并執行“io_sti(); ”。

bootpack.c節選

void HariMain(void)
{
    struct BOOTINFO *binfo = (struct BOOTINFO *) ADR_BOOTINFO;
    char s[40], mcursor[256], keybuf[32], mousebuf[128];
    int mx, my, i;
    struct MOUSE_DEC mdec;

    init_gdtidt();
    init_pic();
    io_sti(); /* IDT/PIC的初始化已經完成,于是開放CPU的中斷 */
    fifo8_init(&keyfifo, 32, keybuf);
    fifo8_init(&mousefifo, 128, mousebuf);
    io_out8(PIC0_IMR, 0xf9); /* 開放PIC1和鍵盤中斷(11111001) */
    io_out8(PIC1_IMR, 0xef); /* 開放鼠標中斷(11101111)  */

    init_keyboard();

    init_palette();
    init_screen8(binfo->vram, binfo->scrnx, binfo->scrny);

■■■■■

夜已經深了,今天就到此為止。在考慮明天要做什么的同時,筆者也決定要睡覺了。晚安!

主站蜘蛛池模板: 湖南省| 米脂县| 陆河县| 鹤壁市| 雅安市| 静乐县| 溧水县| 三亚市| 虹口区| 枣阳市| 廉江市| 孝感市| 石城县| 闻喜县| 甘南县| 新密市| 台北市| 胶州市| 武汉市| 江安县| 贵定县| 普陀区| 凤庆县| 桐乡市| 安顺市| 安吉县| 织金县| 刚察县| 和平县| 天全县| 中方县| 陆丰市| 盐山县| 徐水县| 白水县| 江口县| 兴安盟| 东阳市| 简阳市| 凭祥市| 和林格尔县|