x86架構: 硬件啟動過程分析(附引導啟動代碼)


用戶按下開機鍵,幾秒的時間,都經歷了啥?

1、cpu各個寄存器賦初始值,cs.base=0xffff0000, eip=0xfff0,其他寄存器都是0,這時cs:ip得到的物理地址:0xfffffff0;

     cpu上電后為啥會把cs:ip賦成這種初始值了? 可能是希望把BIOS-ROM放在可尋址4GB最高端,給操作系統和用戶程序大段完整的RAM空間,便於后者在運行時的內存管理

 

2、cpu跳轉到0xffff0執行。但由於該地址距離0xfffff(實模式下內存空間只有1M)僅16byte,空間十分有限,無法執行復雜邏輯,只能jmp到其0xf000:e05b繼續執行;

3、0xf000:e05b任然是BIOS的地址,繼續執行檢測代碼,看看內存(RAM)、顯示器、鍵盤、鼠標、硬盤等外設是否完好。如有問題,會發出長短不等的滴滴聲響,可憑此判斷故障類型

 4、外設檢測完,如果一切正常,會查找用戶設置的啟動順序。普通用戶首次安裝OS時一般選擇從CD/DVD啟動,裝OS;裝好后取出光盤,BIOS會自動從磁盤加載MBR到0x7c00;

 5、加載MBR到0x7c00后,jmp到這里繼續執行;由於只加載一個扇區,能執行的代碼不超過510字節(還有2字節是扇區結尾的標識:0xaa55),能干的事也有限,所以MBR一般會繼續從磁盤其他地方把os代碼都拷貝到內存,同時重定位代碼,完成os的加載;

6、繼續jmp到os代碼執行;

這6步中,1-4部不用我們操心,廠家的產品在出廠前已經做好;第5部,從磁盤的0柱面、0磁頭、1扇區加載MBR到內存0x7c00處也BIOS干的,不需要開發人員操心;真正需要開發人員編寫代碼的地方:

  • MBR的代碼,這部分代碼被bios加載到內存后需要做什么?
  • MBR代碼能運行的代碼不超過510字節,真正的os肯定不止這點代碼,剩余代碼怎么辦?

既然MBR能運行的代碼不超過510字節,能干的活有限,那么干脆簡單點,把os或用戶程序剩余代碼加載到內存,完成重定位,再跳到這些代碼執行,具體代碼如下:

1、MBR代碼

;MBR 主引導扇區
;開機上電后,BIOS會自動從0x7c00處執行

    lba_num equ 100;一共101個扇區,用戶程序在硬盤中的邏輯扇區號
    
SECTION mbr vstart=0x7c00 align=16;以16位對齊
                                  ;cs和IP已經運行到這里,不用再設置了
        mov ax,0
        mov ss,ax;堆棧段從0開始
        mov sp,ax;
        
        mov ax,[cs:phy_address];目前在cs段,如果不寫,默認讀ds段;
        mov dx,[cs:phy_address+0x2]
        mov bx,0x10
        div bx;相當於右移4bit,得到0x1000,就是段地址,放在ax
        mov ds,ax; 
        xor bx,bx;  
        
        mov si,lba_num
        xor di,di
        call read_disk;先讀第一個扇區,把用戶程序的頭部加載到內存,才能得到重定位表
        
        mov ax,[0];program_len分別放在ax和bx;
        mov dx,[2];從內存讀數據,不加段前綴的默認是ds;
        mov bx,512
        div bx;ax = 用戶程序的扇區個數  dx=扇區余數,也就是最后不滿一個扇區內偏移
        cmp dx,0; test dx,dx
        jnz cantDiv;不能被整除,說明有數據不滿一個扇區的數據,但也要占用一個扇區的空間
        dec ax;扇區數減一:前面已經讀了一個扇區。
        
        
cantDiv:
        cmp ax,0;已經讀完了,可以直接重定位
        jz  realloc;
        mov cx,ax;剩余扇區數放入cx,方便后續loop
        push ds
        
Continue_Read:
        inc si
        mov ax,ds
        add ax,0x20;基址增加0x20,相當於增加512byte,比如:ds:bx = 0000:0000 = 00000; ds:bx = 0020:0000 = 0200+0000=0x0200=512byte 
        mov ds,ax;往高地址挪一個扇區512byte
        xor bx,bx;偏移清零,通過段基址挪動
        call read_disk;相當於寄存器傳參
        loop Continue_Read
        pop ds


;---------------上面都是把數據從磁盤讀到內存,下面開始重定位------------------------------------
;先計算出用戶程序code_entry在內存的絕對地址
realloc:
        mov ax,[0x06];默認是ds段,此時已是0x1000;code_entry的section.code1.start低2字節
        mov dx,[0x08];code_entry的section.code1.start高2字節
        call reallocaddress
        mov [0x06],ax;把內存中的物理地址寫回去,這次得到絕對物理地址了;
        ;mov [0x06],ds
        mov cx,[0x0a];5個段需要重定位
        mov bx,0x0c;
        
;用戶程序每個section都計算出內存的絕對地址,然后寫回去        
reallocLoop:
        mov ax,[bx]
        mov dx,[bx+2]
        call reallocaddress
        mov [bx],ax;
        add bx,4
        loop reallocLoop
        
        jmp far [0x04];內存操作默認以ds基址,這里是0x10000;跳轉到用戶程序start變量地址
        ;mov ax, [0x04];得到offset,就是start的偏移地址
        ;jmp 0x1000:ax

;dx:ax 32位偏移地址,寄存器傳參
;輸出16位段基址,保存在ax
reallocaddress:
        push dx
        add ax,[cs:phy_address];注意:目前在cs段,不加從內存讀數據默認用ds,此處為用戶程序;ax=0x0000+[0x10006]=0x0020;
        add dx,[cs:phy_address+0x2];dx=0x0001
        shr ax,4;低16位地址的低4位去掉,高4位補零,得到段基址;ax=0x0002
        ror dx,4;高16位地址循環右移;dx=0x1000
        and dx,0xf000;取出最需要的4bit,其他清零;dx=0x1000
        or ax,dx;ax=0x1002
        pop dx 
        ret


;ds:bx 從硬盤讀數據到該物理地址
;di, si 是邏輯扇區號:邏輯扇區只用28位,所以di有4位是不用的;si是邏輯扇區低16位
;可以通過int 0x13中斷讀取,也可以通過磁盤控制器讀取;
read_disk:
        push ax
        push bx
        push cx
        push dx
        push si
        push di

        ;https://www.cnblogs.com/mlzrq/p/10223060.html 詳細說明
        mov dx,0x1f2;磁盤端口,指定讀取或寫入的扇區數
        mov al,1;每次讀一個扇區
        out dx,al;往端口寫入數據
        
        inc dx;0x1f3  lba地址的低8位,就是0-7位
        mov ax,si;
        out dx,al;先把低8位寫入端口,因為用戶程序被寫入了磁盤100號扇區,所以調用函數傳參數di=100
        
        inc dx;0x1f4  lba地址的中8位,就是8-15位
        mov al,ah
        out dx,al;
        
        inc dx;0x1f5  lba地址的高8位,就是16-23位
        mov ax,di;
        out dx,al;
        
        ;上面3個已經把前面24位填滿,這里填最高4位
        inc dx;0x1f6  lba地址的前4位,就是24-27位
        mov al,0xe0; 高4位是各種標志位: 0 CHS,1 LBA; 1; 0 從  1 主; 0; 這里是e;
        or al,ah
        out dx,al

        inc dx;0x1f7
        mov al,0x20;發送讀扇區的請求:0x20
        out dx,al
        
;------------------------------    and al,0x88 邏輯上出錯,先屏蔽試試    
waits:
        in al,dx; 從0x1f7讀取磁盤狀態,一共有8位;第7位:1表示busy   第3位:1表示准備好讀寫操作,所以在0xxx1xxx的時候才能讀寫,其他狀態都不行;
        and al,0x88;第7位和第3位保持不變,其他清零
        cmp al,0x08;
        jnz waits;狀態不等於0x08,說明沒准備好,繼續等待
        
        
        mov dx,0x01f0;數據端口,16位,需要ax接數據;每個扇區512byte,每次讀2byte,要讀256次
        mov cx,256;
        
        ;准備好了,開始讀磁盤
readw:
        in ax,dx;
        mov [bx],ax;
        add bx,2;每次讀2byte
        loop readw;

        pop di
        pop si
        pop dx
        pop cx
        pop bx
        pop ax

        ret
        
        


        phy_address          dd 0x10000;用戶程序拷貝到內存地址
                 
        times    510 - ($-$$) db 0; 
                             dw 0xaa55

2、用戶程序

;用戶程序
;段的數目並未限制,用戶可根據需求自行創建

;-------------------------------------------------------------------------------
SECTION header vstart=0;vstart=0連着寫,不能有空格
        program_len                dd    program_end; 
        code_entry                dw    start;變量偏移0x4;
                                dd    section.code1.start;code1段基址:變量偏移0x6
                                
        reallocate_item            dw    (header_end-code1Segment)/4 ;每個段偏移都是dd=4byte,變量偏移0xa
        
        ;重定位表,記錄重要段相對於程序起始位置的偏移
        code1Segment            dd  section.code1.start;變量偏移0xc
        data1Segment            dd  section.data1.start;變量偏移0x10  本section在文件中的真實偏移量(真實地址),或則說相對開始的偏移地址
        stack1Segment            dd  section.stack1.start;變量偏移0x14
        use1Segment            dd  section.use1.start;變量偏移0x18
        use1DataSegment        dd  section.use1Data.start;變量偏移0x1c
header_end:   ;有vstart = 0,header_end從vstart = 0開始算偏移


;-------------------------------------------------------------------------------
SECTION use1 align=16 vstart=0; 


;-------------------------------------------------------------------------------
SECTION use1Data align=16 vstart=0; 


use1Data_end:
;-------------------------------------------------------------------------------
SECTION code1 align=16 vstart=0; ;vstart=0連這些,不能有空格
;直接調用BIOS例程在顯示器打印
start:
    
    mov ax,[stack1Segment];初始化堆棧
    mov ss,ax
    mov ax,stacker_pointer;
    mov sp,ax;

    xor ah,ah
    mov al,0x03
    int 0x10;調用bios的0x10號中斷清屏
    
    ;AL=寫模式,BH=頁碼,BL=顏色,CX=字符串長度,DH=行,DL=列,ES:BP=字符串偏移量
    ;https://zh.wikipedia.org/wiki/INT_10H 有詳細說明
    mov ah,0x13
    mov al,1
    xor bh,bh
    mov bl,0x04
    mov cx, data1_end - msg;cx保存字符串長度
    mov dh,12;顯示的行號
    mov dl,25;顯示的列號
    mov bp,msg; es:bp指向需要打印的字符串
    push ax
    mov ax,[data1Segment]
    ;mov ax,cs;
    mov es,ax;es:bp 為串首地址
    pop ax
    int 0x10
    
    hlt;程序待機

;-------------------------------------------------------------------------------
SECTION data1 align=16 vstart=0

        msg db 'are you ready?', 0

data1_end:
;-------------------------------------------------------------------------------
SECTION stack1 align=16 vstart=0;

        resb 256; reserve byte,保留/分配256byte空間
stacker_pointer: ;棧底放在高地址
;-------------------------------------------------------------------------------
SECTION tail align=16; 這個段沒有vstart = 0,那就從開頭計算偏移,也就是SECTION header開始算;

program_end:   

說明: (1)SECTION用於定於段,沒有數量限制,開發人員可根據需求取舍

       (2)vstart=0表示該段內的標識都從0開始計算偏移。如果沒有 vstart=0,那么段內標識比如msg、start等都從程序開始處計算偏移;

                vstart=0千萬要緊挨着,不能有個空格,不能有個空格,不能有個空格,重要的事情說三遍。否則這種聲明無效,段內標識的偏移還是會從程序開頭處計算,導致后續邏輯出錯

      (3)段的數量沒限制,但是建議把代碼段和數據段分開,各種變量盡量在數據段聲明;代碼段聲明的變量因未隔離開,容易被cpu當成代碼執行,導致異常或邏輯錯亂

      (4)MBR為什么要用0xaa55了? 0xaa55=0b 1010 1010  0101  0101,看出來有啥特點了么?  0和1交叉呈現,就像梳子一樣;奇偶校驗總是為偶數,a和5與是0,或是f


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM