bootsect.s 分析—— Linux-0.11 學習筆記(一)

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

bootsect.s分析—— Linux-0.11學習筆記(一)

為了節省篇幅,完整的程式碼就不貼了。感興趣的朋友可以去下載,下載地址是:
http://oldlinux.org/Linux.old/

本文,我打算詳解bootsect.s。如有紕繆,還請各位看官斧正。關於如何講好程式碼,我暫時沒有找到什麼好的展示方法。姑且貼一段、註釋一段、講一段吧。為了不使程式碼片太長,我刪去了一些原來的註釋。

一些符號常量


SYSSIZE = 0x3000  ;system模組的長度


.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

SETUPLEN = 4                ! setup模組的長度,4個扇區
BOOTSEG  = 0x07c0           ! original address of boot-sector
INITSEG  = 0x9000           ! bootsect把自身搬運到0x90000
SETUPSEG = 0x9020           ! setup模組被載入到 0x90200
SYSSEG   = 0x1000           ! system模組被載入到0x10000
ENDSEG   = SYSSEG + SYSSIZE ! where to stop loading, 0x1000 + 0x3000 = 0x4000, 停止載入的段地址

ROOT_DEV = 0x306            !第2個硬碟的第1個分割槽

ROOT_DEV = 0x306 ,這裡的0x306表示第2個硬碟的第1個分割槽,當年Linus是在第2個硬碟的第1個分割槽上安裝了Linux-0.11作業系統。

老式Linux裝置號的命名規則

裝置號 = 主裝置號 * 256 + 次裝置號

或者說:

dev_no = (major << 8) + minor

這裡的主裝置號是事先定義好的(1-記憶體,2-磁碟,3-硬碟,4-ttyx,5-tty,6-並行口,7-非命名管道)。譬如對於硬碟,主裝置號為3,因此3*256+0=0x300即為系統中第一個硬碟的裝置號。更多的例子如下表:

裝置號 裝置檔案 對應的裝置
0x300 /dev/hd0 系統中第一個硬碟
0x301 /dev/hd1 系統中第一個硬碟的第一分割槽
0x302 /dev/hd2 系統中第一個硬碟的第二分割槽
0x303 /dev/hd3 系統中第一個硬碟的第三分割槽
0x304 /dev/hd4 系統中第一個硬碟的第四分割槽
0x305 /dev/hd5 系統中第二個硬碟
0x306 /dev/hd6 系統中第二個硬碟的第一分割槽
0x307 /dev/hd7 系統中第二個硬碟的第二分割槽
0x308 /dev/hd8 系統中第二個硬碟的第三分割槽
0x309 /dev/hd9 系統中第二個硬碟的第四分割槽

bootsect 把自己搬運到 0x90000,並跳轉

entry _start
_start:
    mov ax,#BOOTSEG 
    mov ds,ax      !ds = 0x07c0
    mov ax,#INITSEG
    mov es,ax      !ex = 0x9000
    mov cx,#256    !搬運256次
    sub si,si      !si = 0
    sub di,di      !di = 0
                   !ds:si=0x07c0:0x0, es:di=0x9000:0x0
    rep
    movw           !每次搬運2個位元組
    jmpi go,INITSEG   !跳轉到 0x9000:go

以上程式碼表示把ds:si處(實體地址0x7c00)的內容搬運到es:di(實體地址0x90000),一共搬運512位元組,即主引導扇區把自己移動到了0x90000處。

對於movw指令,可以參考我的博文。
http://blog.csdn.net/longintchar/article/details/50949923

我的疑問是,Linus為什麼沒有清除DF標誌呢?是不是設定DF=0會更嚴謹呢?

jmpi go,INITSEG段間跳轉,INITSEG是段地址,go是偏移地址。這句話執行完,CPU就一下子跑到了0x9000:go處執行了。(下圖中左邊的藍色箭頭,點選圖片可放大)

這裡寫圖片描述

跳轉後繼續執行下面的指令,設定ds,es,ss和sp.

go: mov ax,cs
    mov ds,ax
    mov es,ax     !ds=es=cs=0x9000
    mov ss,ax
    mov sp,#0xFF00  
                  !es:sp = 0x9000:0xff00 ,棧的設定  

載入 setup 模組到 0x90200

load_setup:
    mov dx,#0x0000      ! 驅動器號(DL)0,磁頭號(DH)0
    mov cx,#0x0002      ! 起始扇區號2, 磁軌號0
    mov bx,#0x0200      ! 偏移地址0x200
    mov ax,#0x0200+SETUPLEN ! 功能號AH=0x02,AL=要讀的扇區數目=SETUPLEN=4 
    int 0x13            ! read it
    jnc ok_load_setup   ! ok - continue
    mov dx,#0x0000      !需要復位的驅動器號=DL=0
    mov ax,#0x0000      !功能號AH=0
    int 0x13            ! 復位磁碟
    j   load_setup

以上程式碼利用INT 13H, AH=02H把setup模組從磁碟(2~5扇區)載入到0x90200後面。
注意:柱面號和磁頭號都從0開始,扇區號從1開始。

INT 13H AH=02H:讀扇區

此功能從磁碟上把一個或更多的扇區內容讀進記憶體。這是一個低階功能,在一個操作中讀取的全部扇區必須在同一條磁軌上。

引數 說明
入口引數
AH =02H ,指明呼叫讀扇區功能。
AL 要讀的扇區數目,不允許使用讀磁軌末端以外的數值,也不允許使該暫存器為0。
DL 需要進行讀操作的驅動器號,0表示軟盤,80H表示硬碟。
DH 所讀磁碟的磁頭號。
CH 磁軌號的低8位數(磁軌號共10位)。
CL 低5位放入所讀起始扇區號,位7-6表示磁軌號的高2位。
ES:BX 讀出資料的緩衝區地址。
返回引數
CF =0,操作成功;=1,操作失敗。
AH 錯誤返回碼。
AL 實際讀到的扇區數。

INT 13H AH=00H:磁碟控制器復位

此功能用於復位磁碟(軟盤和硬碟)。當磁碟I/O功能呼叫出現錯誤時,需要呼叫此功能。

引數 說明
入口引數
AH =00H,指明呼叫復位磁碟功能。
DL 需要復位的驅動器號。軟盤:00H-7FH;硬碟:80H-FFH
返回引數
CF =0,操作成功;=1,則操作失敗
AH 錯誤返回碼。

獲得磁碟驅動器引數(主要是每磁軌的扇區數量)

ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track

    mov dl,#0x00    !驅動器號為0,說明是軟盤
    mov ax,#0x0800  ! AH=8 is get drive parameters
    int 0x13
    mov ch,#0x00    !這裡用不上軟盤的最大磁軌號,可以使CH=0
    seg cs          !把段超越字首設定為cs,隻影響下一條語句
    mov sectors,cx  
    !儲存每磁軌最大扇區數。對於軟盤,最大磁軌號不會超過256,所以CH足以表示,CL[7:6]為0
    !以上兩句可以寫為  mov cs:[sectors], cx
    mov ax,#INITSEG
    mov es,ax       !因為上面ES的值被修改,所以令ES=0x9000

INT 13H AH=08H:讀取驅動器引數

引數 說明
入口引數
AH =08H,讀取驅動器引數
DL 驅動器號(如果是硬碟則[7]=1)
返回引數
CF 0-操作成功;1-操作失敗
AH 錯誤返回碼
BL 驅動器型別
CH 最大磁軌號的[7:0]
CL[7:6] 最大磁軌號的[9:8]
CL[5:0] 每磁軌最大扇區數
DH 最大磁頭數
DL 驅動器數量
ES:DI 指向軟碟機磁碟參數列

列印 “Loading system …”

    mov ah,#0x03    !讀游標的位置
    xor bh,bh       !bh=頁號
    int 0x10

我們主要是用行號(DH中)和列號(DL中)。

INT 10H AH=03H:獲取游標位置和形狀

引數 說明
入口引數
AH =03H,讀游標的位置
BH 頁號
返回引數
CH 行掃描開始
CL 行掃描結束
DH 行號
DL 列號

INT 10H AH=13H:在Teletype模式下顯示字串

引數 說明
入口引數
AH =13H,在Teletype模式下顯示字串
BH 頁碼
BL 屬性(若 AL=00H 或 01H)
CX 要顯示的字串的長度
DH、DL 座標(行、列)
ES:BP 指向要顯示的字串
AL 顯示輸出方式
返回引數

對於顯示輸出方式,解釋如下:

取值 說明 字串格式
0 字串中只含顯示字元,顯示屬性在BL中;顯示後,游標位置不變 char1,char2,……,charN
1 字串中只含顯示字元,顯示屬性在BL中;顯示後,游標位置跟隨字串改變 char1,char2,……,charN
2 字串中含有顯示字元和顯示屬性;顯示後,游標位置不變 char1,attri1,char2,attri2,……,charN,attriN
3 字串中含有顯示字元和顯示屬性;顯示後,游標位置跟隨字串改變 char1,attri1,char2,attri2,……,charN,attriN
    mov cx,#24          ! 24個字元
    mov bx,#0x0007      ! page 0, attribute 7 (normal)
    mov bp,#msg1
    mov ax,#0x1301      ! write string, move cursor
    int 0x10
msg1:
    .byte 13,10
    .ascii "Loading system ..."
    .byte 13,10,13,10

13是回車,10是換行。它們的區別如下表。

回車和換行

中文名稱 英文名稱 字母簡寫 ASCII碼 來源
回車 carriage return CR 0x0D=13D “車”指的是紙車,它帶著紙向左移動。在開始打第一個字之前,要把紙車拉到最右邊,使彈簧收緊。隨著打字的進行,彈簧把紙車推向左邊。把紙車拉到最右邊,叫做“回車”。
換行 line feed LF 0x0A=10D 換行的概念是,打字機左邊有個”把手”,扳動一下把手,紙就會上移一行。

載入 system 到 0x10000

! we want to load the system (at 0x10000)

    mov ax,#SYSSEG  ! SYSSEG=0x1000
    mov es,ax       ! segment of 0x010000
    call    read_it
    call    kill_motor

3~5行,把system模組載入到0x10000。

第6行,關閉驅動器馬達。

過程read_it

這個過程的功能是把還未讀取的扇區載入到es:0x0000處。注意:es必須是0x1000的整數倍,否則會陷入死迴圈。每讀64KB,都會使es的值增加0x1000,當es=0x4000的時候,停止讀取。

sread:  .word 1+SETUPLEN !當前磁軌已經讀取的扇區數, 前面的1表示引導扇區bootsect.s
head:   .word 0          ! current head,當前磁頭號
track:  .word 0          ! current track,當前磁軌號

read_it:
    mov ax,es
    test ax,#0x0fff     !使ax與0xfff按位與,測試es是否為0x1000的整數倍
die:    jne die         !結果不為0(說明es不是0x1000的整數倍)則陷入死迴圈
    xor bx,bx           ! bx(作為段內偏移地址)清零
rp_read:
    mov ax,es
    cmp ax,#ENDSEG      ! 實際上求(ax-ENDSEG)
    jb ok1_read         ! 當CF=1(ax<ENDSEG, 有借位)時跳轉到ok1_read
    ret                 ! 當ax>=ENDSEG時返回(我認為不會出現大於的情況)
ok1_read:
    seg cs
    mov ax,sectors      ! 這兩句相當於 mov ax, cs:[sectors]; 獲得每磁軌扇區數
    sub ax,sread        ! ax = ax - sread, 得出本磁軌未讀扇區數
    mov cx,ax
    shl cx,#9           ! cx乘以512,求出位元組數
    add cx,bx           ! 以上3行相當於 cx = ax * 512 + bx
                        ! 假設再讀ax個扇區,cx就是段內共讀入的位元組數
    jnc ok2_read        ! 若cx < 0x10000(CF=0,沒有進位)則跳轉到ok2_read
    je ok2_read         ! 若cx = 0(ZF=1),說明剛好讀入64KB,則跳轉到ok2_read
    xor ax,ax            ! ax = 0x0000
    sub ax,bx            ! 求bx對0x10000的補數,結果在ax中
    shr ax,#9            ! 除以512,得到扇區數,AL作為引數,傳給read_track
ok2_read:               
    call read_track  !呼叫read_track過程,用AL傳參,讀取AL個扇區到ES:BX
    mov cx,ax        !cx是該次操作已經讀取的扇區數
    add ax,sread     !ax是當前磁軌已經讀取的扇區數
    seg cs
    cmp ax,sectors   
    jne ok3_read     !如果當前磁軌還有扇區未讀,跳轉到ok3_read
    mov ax,#1        !說明當前磁軌的扇區都已讀完
    sub ax,head      !ax = 1 - 磁頭號
    jne ok4_read     !不為0則跳轉到 ok4_read,說明磁頭號為0
    inc track        !說明磁頭號為1,磁軌號增加1
ok4_read:
    mov head,ax  !更新磁頭號(如果是37行跳轉過來,則 head=1;否則 head=0)
    xor ax,ax    !ax=0, 因為更換了磁軌,所以當前磁軌已讀扇區數置0
ok3_read:
    mov sread,ax      !更新當前磁軌已經讀取的扇區數
    shl cx,#9
    add bx,cx         !更新偏移地址
    jnc rp_read       !沒有進位,則跳轉到rp_read
    mov ax,es         !有進位,說明BX達到了64KB邊界
    add ax,#0x1000    
    mov es,ax         !es增加0x1000
    xor bx,bx         !bx = 0
    jmp rp_read       !繼續讀取

以上彙編程式碼看起來實在是費勁。為了便於理解,寫成C語言虛擬碼如下:

void read_it(es)//引數是es
{ 
    if((es & 0xFFF) != 0) //es 必須是0x1000的倍數,否則進入死迴圈
        while(1);  //dead loop

    bx = 0;
    while(es < ENDSEG){
        // 1. 看看要讀多少個扇區,用ax表示
        // 2. sread:本磁軌已經讀取的扇區數 
        ax = SECTORS - sread;
        if((ax * 512 + bx) > 0x10000){
            ax = (0x10000 - bx) / 512;
        }

        read_track(ax); //呼叫讀扇區過程,al:要讀的扇區數,es:bx->緩衝區
        cx = ax; //該次操作讀取的扇區數   
        ax += sread; //ax是本磁軌已讀取的扇區總數

        if(ax==SECTORS){
            //本磁軌的扇區全部讀完
            if(head == 1){ //0和1磁頭都已經讀完,更新磁軌
                ++track;
                head = 0; //從0磁頭開始
            }
            else{
                head = 1; //切換到1磁頭          
            }       
            ax = 0; //本磁軌已讀扇區數置為0
        }

        sread = ax; //更新本磁軌已讀扇區數
        bx += cx * 512; 更新偏移地址bx

        if(bx == 0x10000)
        {
            //如果偏移地址到達0x10000,則更新es,並使bx=0
            es += 0x1000;
            bx = 0;
        }
    }
    return;
}

過程read_track

讀取AL個扇區到ES:BX。此過程的入口引數是:

AL-要讀的扇區數目

ES:BX-緩衝區地址

read_track:
    push ax
    push bx
    push cx
    push dx
    mov dx,track  !當前磁軌號
    mov cx,sread  !已經讀取的扇區數
    inc cx        !CL是起始扇區號
    mov ch,dl     !CH是磁軌號----
    mov dx,head   !當前磁頭號
    mov dh,dl     !DH是磁頭號
    mov dl,#0      !DL是驅動器號,0表示軟盤
    and dx,#0x0100 !DH是磁頭號,不是0就是1
    mov ah,#2      !功能號2,讀扇區
    int 0x13
    jc bad_rt       !CF=1,表示出錯,復位磁碟
    pop dx
    pop cx
    pop bx
    pop ax
    ret
bad_rt: mov ax,#0   !AH=0,磁碟復位功能
    mov dx,#0       !DL=0,驅動器號
    int 0x13
    pop dx
    pop cx
    pop bx
    pop ax
    jmp read_track  !重新讀取

過程kill_motor

kill_motor:
    push dx
    mov dx,#0x3f2 !軟盤控制器的埠-數字輸出暫存器埠,只寫
    mov al,#0     !驅動器A,關閉FDC,禁止DMA和中斷請求,關閉馬達
    outb          !將al的值寫入埠dx
    pop dx
    ret

DOR(數字輸出暫存器)

DOR是一個8位暫存器,他控制驅動器馬達的開啟、驅動器選擇、啟動/復位FDC以及允許/禁止DMA及中斷請求。

Name Description
7 MOT_EN3 Driver D motor:1-start;0-stop
6 MOT_EN2 Driver C motor:1-start;0-stop
5 MOT_EN1 Driver B motor:1-start;0-stop
4 MOT_EN0 Driver A motor:1-start;0-stop
3 DMA_INT DMA and IRQs; 1 enable; 0-disable
2 RESET 0= enter reset mode;1= normal operation
1 and 0 DRV_SEL1, DRV_SEL0 “Select” drive number for next access

確認根檔案系統裝置號

    seg cs
    mov ax,root_dev     !ax = ROOT_DEV
    cmp ax,#0   
    jne root_defined    !如果 ROOT_DEV 不等於0則跳轉到 root_defined
    seg cs
    mov bx,sectors       ! 取每磁軌扇區數
    mov ax,#0x0208       ! /dev/ps0 - 1.2Mb
    cmp bx,#15           ! 判斷每磁軌扇區數是否等於15
    je  root_defined     ! 說明是1.2MB的軟盤
    mov ax,#0x021c       ! /dev/PS0 - 1.44Mb
    cmp bx,#18           ! 判斷每磁軌扇區數是否等於18
    je  root_defined     ! 說明是1.44MB的軟盤
undef_root:
    jmp undef_root       ! 死迴圈
root_defined:
    seg cs
    mov root_dev,ax      ! 將檢查過的裝置號儲存到 root_dev 中

在Linux中軟碟機的主裝置號是2,次裝置號 = type * 4 + nr.

其中,nr等於0~3時分別對應軟碟機A、B、C、D;type是軟碟機的型別,比如2表示1.2MB,7表示1.44MB等。

因為是可引導的驅動器,所以肯定是A驅。對於1.2MB,裝置號 = 2 << 8 + 2 * 4 + 0 = 0x208;對於1.44MB,裝置號 = 2 << 8 + 7 * 4 + 0 = 0x21C.

.org 508
root_dev:
    .word ROOT_DEV !這裡存放根檔案系統所在裝置號(init/main.c中會用)

ROOT_DEV到底有何用,怎麼用,這裡先存疑,後面再探究。

跳轉到 setup 去執行

jmpi    0,SETUPSEG   !到此本程式就結束了。

段間跳轉,跳轉到0x9020:0x0000(setup.s程式開始處)去執行。

程式碼分析到這裡,就差不多明白了。雖然是一個引導扇區,編譯後只有512位元組,可是涉及的知識點還真不少。真是太佩服Linus了,一個大學生就能寫出這樣的程式碼,實屬出眾。


參考資料

[1]《Linux核心完全剖析》(趙炯,2006)
[2] https://github.com/Wangzhike/HIT-Linux-0.11/blob/master/1-boot/OS-booting.md
[3] https://wiki.osdev.org/Floppy_Disk_Controller#DOR_bitflag_definitions

相關文章