setup.s 分析—— Linux-0.11 學習筆記(二)

ARM的程式設計師敲著詩歌的夢發表於2020-04-04

更新記錄

版本 時間 修訂內容
1.0 2018-4-14 增加了“獲取顯示模式”這一節,AL取值的表格

標題: setup.s 分析—— Linux-0.11 學習筆記(二)

老規矩,為了節省篇幅,完整的程式碼就不貼了。

定義符號常量

INITSEG  = 0x9000   ! bootsect.s 的段地址
SYSSEG   = 0x1000   ! system loaded at 0x10000 
SETUPSEG = 0x9020   ! 本程式的段地址

注意:以上這些引數最好和 bootsect.s 中的相同。

獲取一些引數儲存在 0x90000 處

儲存游標的位置

    mov ax,#INITSEG  !INITSEG = 0x9000
    mov ds,ax        ! ds = 0x9000
    mov ah,#0x03     ! 功能號=3,獲取游標的位置
    xor bh,bh        ! bh = 頁號 = 0(輸入)
    int 0x10        ! 輸出: DH=行號,DL=列號
    mov [0],dx       ! 儲存游標的行號和列號到 0x90000,共佔2位元組.

獲取從 1M 處開始的擴充套件記憶體大小

    ! 利用 BIOS 中斷 0x15 功能號 ah = 0x88 取系統所含擴充套件記憶體大小,並儲存在記憶體 0x90002 處
    ! 返回:ax=從0xl00000(lM)處開始的擴充套件記憶體大小(KB).若出錯則CF置位,ax=出錯碼
    mov ah,#0x88
    int 0x15
    mov [2],ax ! ax = 從1M處開始的擴充套件記憶體大小

獲取顯示模式

    ! 獲取顯示卡當前的顯示模式
    ! 呼叫 BIOS 中斷 0x10,功能號 ah = 0x0f
    ! 返回: ah=字元列數; al=顯示模式;bh=當前顯示頁。
    ! 0x90004(l個字)存放當前頁;0x90006(1位元組)存放顯示模式;0x90007(1位元組)存放字元列數。
    mov ah,#0x0f
    int 0x10
    mov [4],bx      ! bh = 當前顯示頁
    mov [6],ax      ! al = 顯示模式, ah = 字元列數(視窗寬度)

AL 取值的含義如下表

AL Type Format Cell Colors Adapter Addr Monitor
0 text 40x25 8x8* 16/8 (shades) CGA,EGA b800 Composite
1 text 40x25 8x8* 16/8 CGA,EGA b800 Comp,RGB,Enh
2 text 80x25 8x8* 16/8 (shades) CGA,EGA b800 Composite
3 text 80x25 8x8* 16/8 CGA,EGA b800 Comp,RGB,Enh
4 graphic 320x200 8x8 4 CGA,EGA b800 Comp,RGB,Enh
5 graphic 320x200 8x8 4 (shades) CGA,EGA b800 Composite
6 graphic 640x200 8x8 2 CGA,EGA b800 Comp,RGB,Enh
7 text 80x25 9x14* 3 (b/w/bold) MDA,EGA b000 TTL Mono
8,9,0aH PCjr modes
0bH,0cH (reserved; internal to EGA BIOS)
0dH graphic 320x200 8x8 16 EGA,VGA a000 Enh,Anlg
0eH graphic 640x200 8x8 16 EGA,VGA a000 Enh,Anlg
0fH graphic 640x350 8x14 3 (b/w/bold) EGA,VGA a000 Enh,Anlg,Mono
10H graphic 640x350 8x14 4 or 16 EGA,VGA a000 Enh,Anlg
11H graphic 640x480 8x16 2 VGA a000 Anlg
12H graphic 640x480 8x16 16 VGA a000 Anlg
13H graphic 640x480 8x16 256 VGA a000 Anlg

Notes: With EGA, VGA, and PCjr you can add 80H to AL to initialize a video mode without clearing the screen.

*The character cell size for modes 0-3 and 7 varies, depending on the hardware. On modes 0-3: CGA=8x8, EGA=8x14, and VGA=9x16. For mode 7, MDPA and EGA=9x14, VGA=9x16, LCD=8x8.

檢查顯示方式(EGA/VGA)並獲取引數

    ! 檢查顯示方式(EGA/VGA)並獲取引數。
    ! 呼叫 BIOS 中斷 0x10,功能號: ah = 0xl2,子功能號: bl = 0xl0
    ! 返回:bh=顯示狀態。 0x00-彩色模式,I/O 埠=0x3dX
    !                  0x01-單色模式,I/O 埠=0x3bX
    ! bl = 安裝的顯示記憶體。0x00 - 64k
    !                   0x01 - 128k
    !                   0x02 - 192k
    !                   0x03 - 256k
    ! cx = 顯示卡特性引數。
    !
    mov ah,#0x12 ! 功能號
    mov bl,#0x10 ! 子功能號
    int 0x10
    mov [8],ax      ! 我也不知道這個是什麼(╯︵╰)
    mov [10],bx     ! bh=顯示狀態(單色模式/彩色模式),bl=已安裝的視訊記憶體大小
    mov [12],cx     ! ch=特性聯結器位元位資訊,cl=視訊開關設定資訊

關於返回引數的詳細解釋,還是看這張圖吧,圖片來自趙炯博士的《Linux核心完全剖析》(機械工業出版社,2006)。

BIOS 視訊中斷 0x10

這裡寫圖片描述

複製硬碟參數列

複製 HD0 的硬碟參數列


! 複製 hd0 的硬碟參數列,參數列地址是中斷向量0x41的值,表長度16B
! 中斷向量在中斷向量表中的位置 = 中斷型別號N × 4
! (N*4)的字單元存放偏移地址;
! (N*4+2)的字單元存放段基址。

    mov ax,#0x0000
    mov ds,ax      ! ds=0
! 將記憶體[4*0x41]處的低2位元組(偏移地址)傳給si,高2位元組(段地址)傳給ds
    lds si,[4*0x41]
    mov ax,#INITSEG
    mov es,ax          !es = 0x9000
    mov di,#0x0080
    mov cx,#0x10       !重複16次
! ds:si --> es:di(0x9000:0x0080),共傳送16B
    rep
    movsb

複製 HD1 的硬碟參數列

! 複製 hd1 的硬碟參數列,參數列地址是中斷向量0x46的值,表長度16B
! 道理同上一小節,此處不贅述
    mov ax,#0x0000
    mov ds,ax
    lds si,[4*0x46]
    mov ax,#INITSEG ! INITSEG = 0x9000
    mov es,ax
    mov di,#0x0090
    mov cx,#0x10
! ds:si --> es:di(0x9000:0x0090),共傳送16B
    rep
    movsb

檢查系統是否有第2個硬碟

! 檢查系統是否有第2個硬碟,如果沒有就把第2個參數列清零
! 利用 BIOS 中斷呼叫 0x13 的取盤型別功能,功能號 ah = 0xl5;
! 輸入: dl=驅動器號(0x8X 是硬碟:0x80 指第 1 個硬碟,0x812 個硬碟)
! 輸出: ah=型別碼;00-沒有這個盤,CF 置位;
!                  01-是軟碟機,沒有 change-line 支援;
!                  02 -是軟碟機(或其他可移動裝置),有 change-line 支援;
!                  03 -是硬碟。
!
    mov ax,#0x01500 ! 功能號 ah=0x15,讀取盤型別
    mov dl,#0x81    ! dl=驅動器號,0x81代表第2個硬碟
    int 0x13        
    jc  no_disk1    ! CF置位,表示沒有這個盤
    cmp ah,#3       
    je  is_disk1    ! ah=3表示存在第2個硬碟,跳轉到is_disk1
no_disk1:
! 清空第2個表
    mov ax,#INITSEG
    mov es,ax
    mov di,#0x0090  ! es:di = 0x9000:0x0090
    mov cx,#0x10
    mov ax,#0x00    ! AL=0
    rep
    stosb           ! Store AL at address es:di
is_disk1:

關中斷

! 為進入保護模式做準備

    cli         ! no interrupts allowed !

移動 system 模組到 0x00000

bootsect.s 載入程式將 system 模組讀入到 0xl0000 開始的位置。由於當時假設 system 模組最大長度不會超過 0x80000 (512KB),即其末端不會超過記憶體地址 0x90000,所以 bootsect.s 會把自己移動到0x90000 開始的地方,並把 setup 載入到它的後面。下面這段程式的用途是再把整個 system 模組移動到 0x00000 位置,即把從 0x10000 到 0x8ffff 的記憶體資料塊(共512KB)整塊地向記憶體低端移動了0x10000(64KB)。

! 從程式碼實現來看,是一小塊(0x10000B=64KB)一小塊移動的,共移動8小塊。
    mov ax,#0x0000
    cld             ! 'direction'=0, movs moves forward
do_move:
    mov es,ax       ! es是目的段地址
    add ax,#0x1000
    cmp ax,#0x9000   !ax==0x9000 時結束移動
    jz  end_move
    mov ds,ax       ! ds是源段地址,dses0x1000
    sub di,di        ! di = 0
    sub si,sisi = 0
    mov cx,#0x8000  ! 重複 0x8000次
    rep             ! ds:si --> es:di
    movsw           ! 每次移動2B.
    jmp do_move    ! 本輪一共移動 0x8000*2B = 0x10000B=64KB. 準備下一輪移動

end_move:

上面的彙編程式碼寫成偽C語言程式碼如下:

ax = 0;
cld;

while(1){
    es = ax;
    ax += 0x1000;
    if(ax == 0x9000)
        break;  //結束移動
    ds = ax;
    di = si = 0;
    for(int i=0; i<0x8000; ++i){
       memcpy(es:di, ds:si, 2);
       di += 2;
       si += 2;
    }
}

搬運示意圖如下:
這裡寫圖片描述

載入IDT

end_move:
    mov ax,#SETUPSEG    
    mov ds,ax            !ds = 0x9020,指向本程式段,setup.s 被載入到 0x90200
    !idt_48 標號處的內容如下
    !idt_48:
    !        .word   0          ! idt 界限值=0
    !       .word   0,0         ! idt 基地址=0L

    lidt    idt_48      ! load idt with 0,0

載入GDT

    !gdt_48 標號處的內容如下
    !gdt_48:
    !.word  0x800        ! 0x800 = 2048, 2048/8=256,可容納256個描述符, 其實0x7ff即可
    !.word  512+gdt,0x9  ! setup.s被載入到0x90200, gdt base = 0x90200+gdt = 0x90000+512+gdt
    lgdt    gdt_48      

開啟A20

什麼是A20?為什麼要開啟?可以參考我的博文: 關於A20

PC機主機板上的鍵盤介面是專用介面,它可以看作是常規串列埠的一個簡化版本。該介面被稱為鍵盤控制器,它使用序列通訊協議接收鍵盤發來的掃描碼資料。主機板上所採用的鍵盤控制器是 Intel 8042 晶片或其相容晶片。現今的主機板上已經不包括獨立的 8042 晶片了,但是主機板上其他積體電路會為相容目的而模擬 8042 晶片的功能。另外,該晶片輸出埠 P2 各位被分別用於其他目的。bit_0 (P20引腳)用於實現 CPU 的復位操作(低電平導致復位),bit_1(P21 引腳)用於控制 A20 訊號線的開啟與否,為1時就開啟(選通)A20 訊號線,為0則禁止 A20 訊號線。

    call empty_8042  ! 等待輸入緩衝器為空
    mov al,#0xD1        
    out #0x64,al     
    call    empty_8042   ! 等待輸入緩衝器為空,即命令被接受
    mov al,#0xDF         ! A20 on
    out #0x60,al
    call    empty_8042   ! 等待輸入緩衝器為空,即引數被接受

mov al,#0xD1

0xD1是命令碼,表示寫8042的輸出埠P2,原IBM PC使用P2的bit_1控制A20門。此命令後面帶一個位元組的引數,這個引數由埠0x60寫入。要開啟A20,就要使引數的b1=1,另外還要使b0=1,否則系統會復位。

mov al,#0xDF

0xDF是引數,寫成2進位制是1101_1111,可以看出,b0=1,b1=1。

至於其他bit的值是怎麼得來的,我也不知道。(T▽T)

至於機器是否真正開啟了A20地址線,我們還需要在進入保護模式之後再測試一下。這個工作放在了head.s程式中。head.s的程式碼我們們以後再分析。

empty_8042:
    .word   0x00eb,0x00eb !機器碼,跳轉到下一句,為了延時
    in  al,#0x64    ! 8042 status port
    test al,#2      ! is input buffer full?
    jnz empty_8042  ! yes - loop
    ret

解釋一下empty_8042這個過程。

in al,#0x64讀埠 0x64 到 AL.

讀埠0x64就是讀8042的狀態暫存器(一個8bit的只讀暫存器),bit_1為1時表示輸入緩衝器滿,為0時表示輸入緩衝器空。要向8042寫命令(通過0x64埠寫入),必須當輸入緩衝器為空時才可以

test al,#2用於檢測bit_1,如果為1,則跳轉到empty_8042標號處繼續檢測,直到bit_1為0才返回。

所以empty_8042這個過程就是為了等待輸入緩衝器為空。

設定8259

    ; ICW1 
    mov al,#0x11        ! initialization sequence
    out #0x20,al        ! send ICW1 to Master
    .word   0x00eb,0x00eb       ! jmp $+2, jmp $+2
    out #0xA0,al        ! send ICW1 to Slave
    .word   0x00eb,0x00eb
    ;------------------------------------------------------
    ; ICW2
    mov al,#0x20        ! 送主晶片ICW2命令字,設定起始中斷號,要送奇埠 
    out #0x21,al
    .word   0x00eb,0x00eb
    mov al,#0x28        ! 送從晶片ICW2命令字,設定起始中斷號,要送奇埠
    out #0xA1,al
    .word   0x00eb,0x00eb
    ;-------------------------------------------------------
    ; ICW3
    mov al,#0x04        ! 8259-1 is master
    out #0x21,al
    .word   0x00eb,0x00eb
    mov al,#0x02        ! 8259-2 is slave
    out #0xA1,al
    .word   0x00eb,0x00eb
    ;------------------------------------------------------
    ; ICW4
    mov al,#0x01        
    out #0x21,al
    .word   0x00eb,0x00eb
    out #0xA1,al
    .word   0x00eb,0x00eb
    ;------------------------------------------------------
    mov al,#0xFF        ! mask off all interrupts for now
    out #0x21,al
    .word   0x00eb,0x00eb
    out #0xA1,al

字(0x00eb)是直接使用機器碼錶示的一條相對跳轉指令,起延時作用。0xeb是直接近跳轉指令的操作碼,帶1個位元組的相對位移值。因此跳轉範圍是 -128到 +127. CPU 通過把這個相對位移值加到 EIP 暫存器中就形成一個新的有效地址。注意:執行某條指令的時候,EIP會指向它的下一條指令。所以,CPU執行0x00eb的時候,會把EIP的值加上 0 ,其實就是下一條指令的地址,然後跳轉到那裡去執行。

0x00eb,0x00eb這兩條指令共可提供 14~20 個 CPU 時鐘週期的延遲時間。在 as86 中沒有表示相應指令的助記符,因此 Linus 在 setup.s 等一些彙編程式中就直接使用機器碼來表示這種指令。另外,每個空操作指令 N0P 的時鐘週期數是 3 個,因此若要達到相同的延遲效果就需要 6 至 7 個 N0P 指令。

關於 8259A 的知識可以參考我的博文 : 詳解8259A
對於每個命令字的埠,我列了一張速查表。

命令字 A0 主片埠地址 從片埠地址 備註
ICW1 0 0x20 0xA0 D4 = 1
ICW2 1 0x21 0xA1
ICW3 1 0x21 0xA1
ICW4 1 0x21 0xA1
OCW1 1 0x21 0xA1
OCW2 0 0x20 0xA0 D4-D3 = 00
OCW3 0 0x20 0xA0 D4-D3 = 01

ICW1

mov al,#0x11
out #0x20,al

向主片寫入0x11 = 0001_0001b, 表示初始化命令開始,它是 ICW1 命令字。 對照表格可以知道——邊沿觸發、 多片8259級聯、最後要傳送 ICW4 命令字。

ICW1 含義
D0 1:需要ICW4 0:不需要ICW4
D1 1:單片 0:級聯
D2 =0;
D3 1:電平觸發 0:邊沿觸發
D4 =1
D7-D5 =000

ICW2

mov al,#0x20        ! start of hardware int's (0x20)
out #0x21,al

送主晶片 ICW2 命令字,設定起始中斷號為0x20,則主片 0~7 級對應的中斷號是 0x20~0x27;

mov al,#0x28        ! start of hardware int's 2 (0x28)
out #0xA1,al

送從晶片 ICW2 命令字,設定起始中斷號為0x28,則從片 8~15 級對應的中斷號是 0x28~0x2F;

ICW3

mov al,#0x04        ! 8259-1 is master
out #0x21,al
.word   0x00eb,0x00eb
mov al,#0x02        ! 8259-2 is slave
out #0xA1,al
.word   0x00eb,0x00eb

1~2行:送主晶片 ICW3 命令字,0x04 = 0000_0100b,表示主晶片的 IR2 連從晶片的 INT。

4~5行:送從晶片 ICW3 命令字,表示從晶片的 INT 連到主晶片的 IR2 引腳上。

ICW4

mov al,#0x01        
out #0x21,al
.word   0x00eb,0x00eb
out #0xA1,al
.word   0x00eb,0x00eb

送 ICW4 命令字。普通 E0I(需傳送指令來複位)、非緩衝方式、非特殊全巢狀。

ICW4 含義
D7-D5 =0
D4 1:特殊全巢狀 0:非特殊全巢狀
D3-D2 0X:非緩衝 10:緩衝-從片 11:緩衝-主片
D1 1:自動 EOI 0:普通 EOI
D0 =1

OCW1

mov al,#0xFF        
out #0x21,al           ! 遮蔽主片所有中斷請求
.word   0x00eb,0x00eb
out #0xA1,al           ! 遮蔽從片所有中斷請求。

OCW1 用於對8259的中斷遮蔽暫存器進行讀/寫操作,若Di=1,則遮蔽對應中斷請求級IRi.

進入保護模式

下面設定並進入32位保護模式執行。

首先載入機器狀態字(lmsw,Load Machine Status Word),也稱控制暫存器 CR0,其位元位 0 置 1 將使 CPU 切換到保護模式,並且執行在特權級0,即當前特權級 CPL = 0。此時各個段暫存器仍然指向與實地址模式中相同的線性地址處(在實地址模式下線性地址與實體地址相同)。在設定該位元位後,隨後一條指令必須是一條段間跳轉指令,用於重新整理CPU當前指令佇列。因為 CPU 是在執行一條指令之前就已從記憶體讀取該指令並對其進行譯碼。然而在進入保護模式以後那些屬於真實模式的預先取得的指令資訊就變得不再有效。而一條段間跳轉指令就會重新整理 CPU 的當前指令佇列,即丟棄這些無效資訊。另外,Intel手冊上建議 80386 或以上 CPU 應該使用指令 mov cr0,ax 切換到保護模式。lmsw 指令僅用於相容以前的 286 CPU。

mov ax,#0x0001  ! Protection Enable (bit 0 of CR0).
lmsw ax         ! 實際上lmsw指令僅僅載入CR0的低4位,由低到高分別是PE,MP,EM,TS
jmpi 0,8        ! jmp offset 0 of segment 8 (cs)

實際上lmsw指令僅僅載入CR0的低4位,由低到高分別是PE,MP,EM,TS. 這裡我們僅關注 PE,其他的都設為0.

jmpi 0,8 段間跳轉指令。執行後,CS=8,IP=0.

關於這裡的段間跳轉,要多說幾句。
即使是在真實模式下,段暫存器的描述符快取記憶體器也被用於訪問記憶體,僅低20位有效,高12位是全零。當處理器進入保護模式後,這些內容依然殘留著,但不影響使用,程式可以繼續執行。但是,這些殘留的內容在保護模式下是無效的,遲早會在執 行某些指令的時候出問題。因此,比較安全的做法是儘快重新整理 CS、SS、DS 、ES 、FS 和 GS 的內容,包括它們的段選擇器和描述符快取記憶體器。

在進入保護模式之前,有很多指令已經進入了流水線。因為處理器工作在真實模式下,所以它們都是按16位運算元和16位地址長度進行譯碼的,即使是那些用 bits 32 編譯的指令。進入保護模式後,受CS 段描述符快取記憶體器中真實模式殘留內容的影響,處理器進入16位保護模式工作。如果保護模式下的程式碼是16位的,影響可能不大,但如果是用 bits 32 編譯的,那麼,由於對運算元和預設地址大小的解釋不同,指令的執行結果可能會不正確,所以必須清空流水線。同時,那些通過亂序執行得到的中間結果也是無效的,必須清理掉,讓處理器序列化執行,即重新按指令的自然順序執行。

怎麼辦呢?這裡有一個兩全其美的方案,那就是使用段間跳轉指jmpi。處理器最怕轉移指令,遇到這種指令,一般會淸空流水線,並序列化執行;另一方面,段間跳轉會重新載入段選擇器CS,並重新整理描述符快取記憶體器中的內容。

jmpi 0,8 中的 “8 ”是保護模式下的段選擇子,用於選擇描述符表(GDT或LDT)和描述符表項以及所要求的特權級。段選擇子長度為16位(2位元組)。

段選擇子
b1-b0 請求特權級(RPL)
b2 0:全域性描述符表 1:區域性描述符表
b15-b3 描述符表項的索引, 指出選擇第幾項描述符(從0開始)

位0-1表示請求特權級(RPL),Linux作業系統只用到兩級——0級(核心級)和3級(使用者級);位2 用於選擇全域性描述符表還是區域性描述符表;位3-15是描述符表項的索引,指出選擇第幾項描述符。所以段選擇子8(= 0000_0000_0000_1000b)表示請求特權級0、使用全域性描述符表GDT中第1個段描述符項(GDT表在後文分析),該項是一個程式碼段描述符,指出程式碼段的基地址是0,又因為偏移值是0,所以這個跳轉指令會跳轉到0地址,即執行system模組。

從邏輯地址到線性地址的轉換規則如下圖:
這裡寫圖片描述

到這裡,setup.s 檔案就分析完了。不過還剩一個小尾巴,就是檔案末尾定義的GDT表。

gdt:
    .word   0,0,0,0     ! dummy

    .word   0x07FF      ! 8Mb - limit=2047 (2048*4096=8Mb)
    .word   0x0000      ! base address=0
    .word   0x9A00      ! code read/exec
    .word   0x00C0      ! granularity=4096, 386

    .word   0x07FF      ! 8Mb - limit=2047 (2048*4096=8Mb)
    .word   0x0000      ! base address=0
    .word   0x9200      ! data read/write
    .word   0x00C0      ! granularity=4096, 386

有了這個小程式,分析段描述符再也不用發愁了,So easy !
80x86描述符總結及解析描述符的小程式

索引號 描述符型別 基地址 段界限 粒度 P DPL 備註 選擇子
0 空描述符 - - - - - - -
1 程式碼段 0 0X7FF 4KB 1 0 程式碼段,非一致性,可讀 0x08
2 資料段 0 0X7FF 4KB 1 0 資料段,向上擴充套件,可寫 0x10

參考資料
1《Linux核心完全剖析》(趙炯,機械工業出版社,2006)
2《x86組合語言:從真實模式到保護模式》(李忠,2013)
3 http://webpages.charter.net/danrollins/techhelp/0114.HTM

相關文章