天天看點

仿照着寫個bootloader(四) 中斷向量

    這是x86 bootloader的第四篇,實模式的最後一篇,後面就要開啟A20線-分頁記憶體等進入32bit保護模式。前陣子看到protues帶有8086 CPU,琢磨着等這邊bootloader結束了,嘗試一下在protues上仿真。

    8086 CPU在進入實模式前,中斷表跟8051長的有點神似,從0x0000開始每4B為一個中斷向量,4B空間肯定不夠進行中斷事件,于是,這4B空間被安排為中斷處理函數的在記憶體中的位址,其中低2B為函數偏移,高2B為函數段位址;而傳統8051從0x0000開始每2B為一個JMP跳轉,差不多長的如下:

ORG 0000H
LJMP MAIN
ORG 003BH
LJMP PCA_ISR      

當8051中斷發生時,如PCA中斷,mcu會去003BH處取指令,然後跳轉到PCA_ISR處執行;8086為了顯示他的高貴,沒這麼處理:當遇到中斷,從中斷代理晶片處取出中斷号,然後把中斷号*4,得到中斷入口點的位址,從中取出偏移和段位址裝入IP:CS,于是CPU到中斷處理函數中執行。這顯然是一個自動裝載的過程,不像8051一樣用LJMP。

    關于中斷代理晶片,我到有很多想說。2個月前看<深入Linux裝置驅動程式核心機制>(不可否認書在良莠不齊的國内圖書市場上是本像樣的書),對5.2節 PIC與軟體中斷号一節中的部分内容不太了解:“2)将外設的中斷引腳編号映射到處理器可見的軟體中斷号irq”及"軟體中斷号irq,它是發生裝置中斷時處理器從PIC中讀到的中斷号碼,在作業系統建立的中斷處理架構内,會使用這個irq号來辨別一個外設的中斷并調用對應的中斷處理例程"。如果PIC指的是8259,那麼,應該可以這麼了解以上兩句話:1)首先,8259的中斷号是是可配置的,可以指定主片的中斷好從0x08開始,他的每個引腳IR0-IR7對應的中斷号分别為0x08-0x0E。是以,“2)将外設的中斷引腳編号映射到處理器可見的軟體中斷号irq”中的軟體中斷号應該就是從8259輸出的經過自定義配置的中斷号;2)而"...使用這個irq号來辨別一個外設的中斷并調用對應的中斷處理例程" 這句中的irq好,應指前面的自定義配置的中斷号,用這個8259的輸出到中斷向量表中取ISR位址。

    下面來調試一些中斷處理過程。調試的代碼還是用原作者提供的bootloader,相對容易檢視。

    先來觀察一下中斷向量表長成什麼樣:

上電複位後,執行第一條指令前,中斷向量表還是空的:

仿照着寫個bootloader(四) 中斷向量

BIOS初始化結束,準備執行Bootloader時,中斷向量表已經被簡單的安裝完畢,簡單到什麼程度呢?多數中斷向量表項中的段位址:偏移的值為0xf000ff53,都說這個指向一個ISR過程,檢視這個位址的内容是一個中斷傳回:

仿照着寫個bootloader(四) 中斷向量

圖中顯示0x00000 為0xf000ff53 0xf000為高2B的段位址,0xff53為低2B的偏移。反彙編0xfff53的結果為iref

程式初始化RTC中斷處理程式後,中斷号0x70H(位址0x1c0處):

仿照着寫個bootloader(四) 中斷向量

此時已經指向了RTC的中斷處理。

最後,來調試一下中斷處理過程,為了調試RTC中斷處理函數,必須打開RTC周期更新中斷功能,要不然隻能進入RTC中斷1次,開始時我就遇到這種情況,各種想不通。

調試ISR,隻要在中斷入口下斷點即可。不過調試ISR不是重點,重點是檢視發生ISR時的堆棧變化:

27                                  new_int_0x70:
    28 00000000 50                            push ax
    29 00000001 53                            push bx
    30 00000002 51                            push cx
    31 00000003 52                            push dx
    32 00000004 06                            push es      

這是作者在中斷入口壓入堆棧的寄存器,并且在觸發中斷時會壓入flag/cs/ip,但作者沒有明确的說這些寄存器的入棧順序是以隻能調試觀察了:

仿照着寫個bootloader(四) 中斷向量

中斷發生時ss:sp指向0x10300,堆棧是向下生長,而且中斷發生時一共入棧了8個字,是以從0x10300-0x1030f是寄存器的内容。位址越高的最先入棧。

可見,入棧次序是0x0246:flag-0x1002:cs-0x00ed:ip[檢視lst檔案0x00ed處正好是hlt指令]

169 000000E2 B900B8                        mov cx,0xb800
   170 000000E5 8ED9                          mov ds,cx
   171 000000E7 C606C20740                    mov byte [12*160 + 33*2],'@'       ;螢幕第12行,35列
   172                                         
   173                                   .idle:
   174 000000EC F4                            hlt                                ;使CPU進入低功耗狀态,直到用中斷喚醒
   175 000000ED F616C307                      not byte [12*160 + 33*2+1]         ;反轉顯示屬性 
   176 000000F1 E9F8FF                        jmp .idle      

-0x008e:ax-0x004d:bx...

以此,可以得出結論,中斷發生時8086會依次往堆棧中壓入FLAG-CS-IP!

OVER

補上作者的代碼:

;代碼清單9-1
         ;檔案名:c09_1.asm
         ;檔案說明:使用者程式 
         ;建立日期:2011-4-16 22:03
         
;===============================================================================
SECTION header vstart=0                     ;定義使用者程式頭部段 
    program_length  dd program_end          ;程式總長度[0x00]
    
    ;使用者程式入口點
    code_entry      dw start                ;偏移位址[0x04]
                    dd section.code.start   ;段位址[0x06] 
    
    realloc_tbl_len dw (header_end-realloc_begin)/4
                                            ;段重定位表項個數[0x0a]
    
    realloc_begin:
    ;段重定位表           
    code_segment    dd section.code.start   ;[0x0c]
    data_segment    dd section.data.start   ;[0x14]
    stack_segment   dd section.stack.start  ;[0x1c]
    
header_end:                
    
;===============================================================================
SECTION code align=16 vstart=0           ;定義代碼段(16位元組對齊) 
new_int_0x70:
      push ax
      push bx
      push cx
      push dx
      push es
      
  .w0:                                    
      mov al,0x0a                        ;阻斷NMI。當然,通常是不必要的
      or al,0x80                          
      out 0x70,al
      in al,0x71                         ;讀寄存器A
      test al,0x80                       ;測試第7位UIP 
      jnz .w0                            ;以上代碼對于更新周期結束中斷來說 
                                         ;是不必要的 
      xor al,al
      or al,0x80
      out 0x70,al
      in al,0x71                         ;讀RTC目前時間(秒)
      push ax

      mov al,2
      or al,0x80
      out 0x70,al
      in al,0x71                         ;讀RTC目前時間(分)
      push ax

      mov al,4
      or al,0x80
      out 0x70,al
      in al,0x71                         ;讀RTC目前時間(時)
      push ax

      mov al,0x0c                        ;寄存器C的索引。且開放NMI 
      out 0x70,al
      in al,0x71                         ;讀一下RTC的寄存器C,否則隻發生一次中斷
                                         ;此處不考慮鬧鐘和周期性中斷的情況 
      mov ax,0xb800
      mov es,ax

      pop ax
      call bcd_to_ascii
      mov bx,12*160 + 36*2               ;從螢幕上的12行36列開始顯示

      mov [es:bx],ah
      mov [es:bx+2],al                   ;顯示兩位小時數字

      mov al,':'
      mov [es:bx+4],al                   ;顯示分隔符':'
      not byte [es:bx+5]                 ;反轉顯示屬性 

      pop ax
      call bcd_to_ascii
      mov [es:bx+6],ah
      mov [es:bx+8],al                   ;顯示兩位分鐘數字

      mov al,':'
      mov [es:bx+10],al                  ;顯示分隔符':'
      not byte [es:bx+11]                ;反轉顯示屬性

      pop ax
      call bcd_to_ascii
      mov [es:bx+12],ah
      mov [es:bx+14],al                  ;顯示兩位小時數字
      
      mov al,0x20                        ;中斷結束指令EOI 
      out 0xa0,al                        ;向從片發送 
      out 0x20,al                        ;向主片發送 

      pop es
      pop dx
      pop cx
      pop bx
      pop ax

      iret

;-------------------------------------------------------------------------------
bcd_to_ascii:                            ;BCD碼轉ASCII
                                         ;輸入:AL=bcd碼
                                         ;輸出:AX=ascii
      mov ah,al                          ;分拆成兩個數字 
      and al,0x0f                        ;僅保留低4位 
      add al,0x30                        ;轉換成ASCII 

      shr ah,4                           ;邏輯右移4位 
      and ah,0x0f                        
      add ah,0x30

      ret

;-------------------------------------------------------------------------------
start:
      mov ax,[stack_segment]
      mov ss,ax
      mov sp,ss_pointer
      mov ax,[data_segment]
      mov ds,ax
      
      mov bx,init_msg                    ;顯示初始資訊 
      call put_string

      mov bx,inst_msg                    ;顯示安裝資訊 
      call put_string
      
      mov al,0x70
      mov bl,4
      mul bl                             ;計算0x70号中斷在IVT中的偏移
      mov bx,ax                          

      cli                                ;防止改動期間發生新的0x70号中斷

      push es
      mov ax,0x0000
      mov es,ax
      mov word [es:bx],new_int_0x70      ;偏移位址。
                                          
      mov word [es:bx+2],cs              ;段位址
      pop es

      mov al,0x0b                        ;RTC寄存器B
      or al,0x80                         ;阻斷NMI 
      out 0x70,al
      mov al,0x12                        ;設定寄存器B,禁止周期性中斷,開放更 
      out 0x71,al                        ;新結束後中斷,BCD碼,24小時制 

      mov al,0x0c
      out 0x70,al
      in al,0x71                         ;讀RTC寄存器C,複位未決的中斷狀态

      in al,0xa1                         ;讀8259從片的IMR寄存器 
      and al,0xfe                        ;清除bit 0(此位連接配接RTC)
      out 0xa1,al                        ;寫回此寄存器 

      sti                                ;重新開放中斷 

      mov bx,done_msg                    ;顯示安裝完成資訊 
      call put_string

      mov bx,tips_msg                    ;顯示提示資訊
      call put_string
      
      mov cx,0xb800
      mov ds,cx
      mov byte [12*160 + 33*2],'@'       ;螢幕第12行,35列
       
 .idle:
      hlt                                ;使CPU進入低功耗狀态,直到用中斷喚醒
      not byte [12*160 + 33*2+1]         ;反轉顯示屬性 
      jmp .idle

;-------------------------------------------------------------------------------
put_string:                              ;顯示串(0結尾)。
                                         ;輸入:DS:BX=串位址
         mov cl,[bx]
         or cl,cl                        ;cl=0 ?
         jz .exit                        ;是的,傳回主程式 
         call put_char
         inc bx                          ;下一個字元 
         jmp put_string

   .exit:
         ret

;-------------------------------------------------------------------------------
put_char:                                ;顯示一個字元
                                         ;輸入:cl=字元ascii
         push ax
         push bx
         push cx
         push dx
         push ds
         push es

         ;以下取目前光标位置
         mov dx,0x3d4
         mov al,0x0e
         out dx,al
         mov dx,0x3d5
         in al,dx                        ;高8位 
         mov ah,al

         mov dx,0x3d4
         mov al,0x0f
         out dx,al
         mov dx,0x3d5
         in al,dx                        ;低8位 
         mov bx,ax                       ;BX=代表光标位置的16位數

         cmp cl,0x0d                     ;回車符?
         jnz .put_0a                     ;不是。看看是不是換行等字元 
         mov ax,bx                       ; 
         mov bl,80                       
         div bl
         mul bl
         mov bx,ax
         jmp .set_cursor

 .put_0a:
         cmp cl,0x0a                     ;換行符?
         jnz .put_other                  ;不是,那就正常顯示字元 
         add bx,80
         jmp .roll_screen

 .put_other:                             ;正常顯示字元
         mov ax,0xb800
         mov es,ax
         shl bx,1
         mov [es:bx],cl

         ;以下将光标位置推進一個字元
         shr bx,1
         add bx,1

 .roll_screen:
         cmp bx,2000                     ;光标超出螢幕?滾屏
         jl .set_cursor

         mov ax,0xb800
         mov ds,ax
         mov es,ax
         cld
         mov si,0xa0
         mov di,0x00
         mov cx,1920
         rep movsw
         mov bx,3840                     ;清除螢幕最底一行
         mov cx,80
 .cls:
         mov word[es:bx],0x0720
         add bx,2
         loop .cls

         mov bx,1920

 .set_cursor:
         mov dx,0x3d4
         mov al,0x0e
         out dx,al
         mov dx,0x3d5
         mov al,bh
         out dx,al
         mov dx,0x3d4
         mov al,0x0f
         out dx,al
         mov dx,0x3d5
         mov al,bl
         out dx,al

         pop es
         pop ds
         pop dx
         pop cx
         pop bx
         pop ax

         ret

;===============================================================================
SECTION data align=16 vstart=0

    init_msg       db 'Starting...',0x0d,0x0a,0
                   
    inst_msg       db 'Installing a new interrupt 70H...',0
    
    done_msg       db 'Done.',0x0d,0x0a,0

    tips_msg       db 'Clock is now working.',0
                   
;===============================================================================
SECTION stack align=16 vstart=0
           
                 resb 256
ss_pointer:
 
;===============================================================================
SECTION program_trail
program_end:      
section ProgHead align=16 vstart=0
MagicNum  db 'M',0x2e,'A',0x2e,'G',0x2e,'I',0x2e,'C',0x2e
times 6 db 0x00
ProgSize  dd ProgEnd
CodeEntry   dw start
CodeSeg   dd section.CodeSeg.start
ProgSegNums dw (ProgSegEntryTabEnd-ProgSegEntryTab)/4
ProgSegEntryTab:
CodeSegEntry dd section.CodeSeg.start
DataSegEntry dd section.DataSeg.start
StackSegEntry dd section.StackSeg.start
ProgSegEntryTabEnd:

Arg1Off   equ 0x04 ;第一個參數相對bp偏移
Arg2Off   equ 0x06 ;第二個參數相對bp偏移
Arg3Off   equ 0x08 ;第三個參數相對bp偏移
Arg4Off   equ 0x0A ;第四個參數相對bp偏移

;中斷号
;rtc
RTCInt    equ 0x70
section CodeSeg align=16 vstart=0x0000

Intx70RTC:
    push ax
      push bx
      push cx
      push dx
      push es
    ;下面這段也是照抄的,I am a lazy man
  .w0:                                    
      mov al,0x0a                        ;阻斷NMI。當然,通常是不必要的
      or al,0x80                          
      out 0x70,al
      in al,0x71                         ;讀寄存器A
      test al,0x80                       ;測試第7位UIP 
      jnz .w0                            ;以上代碼對于更新周期結束中斷來說 
                                         ;是不必要的 
      xor al,al
      or al,0x80
      out 0x70,al
      in al,0x71                         ;讀RTC目前時間(秒)
      push ax

      mov al,2
      or al,0x80
      out 0x70,al
      in al,0x71                         ;讀RTC目前時間(分)
      push ax

      mov al,4
      or al,0x80
      out 0x70,al
      in al,0x71                         ;讀RTC目前時間(時)
      push ax

      mov al,0x0c                        ;寄存器C的索引。且開放NMI 
      out 0x70,al
      in al,0x71                         ;讀一下RTC的寄存器C,否則隻發生一次中斷
                                         ;此處不考慮鬧鐘和周期性中斷的情況 
      mov ax,0xb800
      mov es,ax

      pop ax
      call bcd_to_ascii
      mov bx,24*160 + 60*2               ;從螢幕上的12行36列開始顯示

      mov [es:bx],ah
      mov [es:bx+2],al                   ;顯示兩位小時數字

      mov al,':'
      mov [es:bx+4],al                   ;顯示分隔符':'
      not byte [es:bx+5]                 ;反轉顯示屬性 

      pop ax
      call bcd_to_ascii
      mov [es:bx+6],ah
      mov [es:bx+8],al                   ;顯示兩位分鐘數字

      mov al,':'
      mov [es:bx+10],al                  ;顯示分隔符':'
      not byte [es:bx+11]                ;反轉顯示屬性

      pop ax
      call bcd_to_ascii
      mov [es:bx+12],ah
      mov [es:bx+14],al                  ;顯示兩位小時數字
      
      mov al,0x20                        ;中斷結束指令EOI 
      out 0xa0,al                        ;向從片發送 
      out 0x20,al                        ;向主片發送 
    
    pop es
      pop dx
      pop cx
      pop bx
      pop ax
  iret
bcd_to_ascii:                            ;BCD碼轉ASCII
                                         ;輸入:AL=bcd碼
                                         ;輸出:AX=ascii
      mov ah,al                          ;分拆成兩個數字 
      and al,0x0f                        ;僅保留低4位 
      add al,0x30                        ;轉換成ASCII 

      shr ah,4                           ;邏輯右移4位 
      and ah,0x0f                        
      add ah,0x30

      ret
    
put_string:                              ;顯示串(0結尾)。
                                         ;輸入:DS:BX=串位址
         mov cl,[bx]
         or cl,cl                        ;cl=0 ?
         jz .exit                        ;是的,傳回主程式 
         call put_char
         inc bx                          ;下一個字元 
         jmp put_string

   .exit:
         ret
     
;-------------------------------------------------------------------------------
put_char:                                ;顯示一個字元
                                         ;輸入:cl=字元ascii
         push ax
         push bx
         push cx
         push dx
         push ds
         push es

         ;以下取目前光标位置
         mov dx,0x3d4
         mov al,0x0e
         out dx,al
         mov dx,0x3d5
         in al,dx                        ;高8位 
         mov ah,al

         mov dx,0x3d4
         mov al,0x0f
         out dx,al
         mov dx,0x3d5
         in al,dx                        ;低8位 
         mov bx,ax                       ;BX=代表光标位置的16位數

         cmp cl,0x0d                     ;回車符?
         jnz .put_0a                     ;不是。看看是不是換行等字元 
         mov ax,bx                       ;此句略顯多餘,但去掉後還得改書,麻煩 
         mov bl,80                       
         div bl
         mul bl
         mov bx,ax
         jmp .set_cursor

 .put_0a:
         cmp cl,0x0a                     ;換行符?
         jnz .put_other                  ;不是,那就正常顯示字元 
         add bx,80
         jmp .roll_screen

 .put_other:                             ;正常顯示字元
         mov ax,0xb800
         mov es,ax
         shl bx,1
         mov [es:bx],cl

         ;以下将光标位置推進一個字元
         shr bx,1
         add bx,1

 .roll_screen:
         cmp bx,2000                     ;光标超出螢幕?滾屏
         jl .set_cursor

         mov ax,0xb800
         mov ds,ax
         mov es,ax
         cld
         mov si,0xa0
         mov di,0x00
         mov cx,1920
         rep movsw
         mov bx,3840                     ;清除螢幕最底一行
         mov cx,80
 .cls:
         mov word[es:bx],0x0720
         add bx,2
         loop .cls

         mov bx,1920

 .set_cursor:
         mov dx,0x3d4
         mov al,0x0e
         out dx,al
         mov dx,0x3d5
         mov al,bh
         out dx,al
         mov dx,0x3d4
         mov al,0x0f
         out dx,al
         mov dx,0x3d5
         mov al,bl
         out dx,al

         pop es
         pop ds
         pop dx
         pop cx
         pop bx
         pop ax

         ret
     
MountIntVec:
    push bp
    mov bp,sp
    ;儲存es
    mov ax,es
    push ax
    
    xor ax,ax
    ;es指向0x0000
    mov es,ax
    ;獲得中斷号
    mov bx,[bp+Arg1Off]
    ;中斷号左移2位(*4),獲得在中斷向量表中的位址
    shl bx,0x02

    ;Int70RTC
    ;低2B存放中斷處理函數的偏移
    mov ax,[bp+Arg2Off];Intx70RTC
    mov word [es:bx],ax
    mov ax,cs ;目前cs段位址是0x0000
    ;高2B存放中斷處理函數的段位址
    mov [es:bx+0x02],ax

    ;恢複es
    pop ax
    mov es,ax
    mov sp,bp
    pop bp
    ret
    
InitRTC:
;以下這段完全抄書的,設定寄存器太單調   
    mov al,0x0b                        ;RTC寄存器B
    or al,0x80                         ;阻斷NMI 
    out 0x70,al
    mov al,0x12                        ;設定寄存器B,禁止周期性中斷,開放更 
    out 0x71,al                        ;新結束後中斷,BCD碼,24小時制 

    mov al,0x0c
    out 0x70,al
    in al,0x71                         ;讀RTC寄存器C,複位未決的中斷狀态

    in al,0xa1                         ;讀8259從片的IMR寄存器 
    and al,0xfe                        ;清除bit 0(此位連接配接RTC)
    out 0xa1,al                        ;寫回此寄存器  
    ret
;執行到這,要設定資料段堆棧段等資訊,
;加載器在加載時,設定ds/es指向LoadPhyBase
;既然這樣,加載器跳轉過來後ds:[n]能正确通路
;自己定義的使用者頭
start:
;現在section.CodeSeg.start,section.DataSeg.start中的内容是段基位址
;開始時,我先加載了ds,再用[ds:StackSegEntry],出錯了
;因為ds改變後不能在用
;mov ax,[StackSegEntry]
;mov ss,ax
;語句加載ss了 因為ds指向其他段,不再是LoadPhyBase,是以應該在ds改變前先加載ss段

;設定堆棧段
mov ax,[StackSegEntry]
mov ss,ax
mov sp,stack_end
;設定資料段,附加段
mov ax,[DataSegEntry]
mov ds,ax
mov es,ax

mov bx,Logo
call put_string

mov bx,MountMsg
call put_string

cli
;中斷服務曆程的偏移
push Intx70RTC
;安裝rtc向量
push word RTCInt
call MountIntVec
add sp,0x04

call InitRTC

sti

mov bx,MountEndMsg
call put_string

hlt
jmp $
times 0x100 db 0xAA

section DataSeg align=16 vstart=0x0000
    Logo db '  congratulations!  ',0x0d,0x0a
     db '  YZ Loader complete loading program  ',0x0d,0x0a
     db '  Author: Hanyj  ',0x0d,0x0a
     db '  2014-12-16  ',0x0d,0x0a
         db 0
  MountMsg db ' Mount interrupt... ',0x0d,0x0a
       db 0
  MountEndMsg db ' Mount interrupt vector complete! ',0x0d,0x0a
        db 0
section StackSeg align=16 vstart=0x0000
  resb 256

;彙編中的标号都表示偏移位置,偏移堆棧基址256,堆棧向下生長,即堆棧保留了256位元組。
;好吧,我承認stack_end這段我完全抄書的
stack_end: 

section tail align=16
ProgEnd:      

繼續閱讀