天天看點

硬碟的通路,程式重定位和加載

使用者程式的結構

  NASM編譯器使用彙編指令 SECTION 或者 SEGMENT 來定義段。它的一般格式是:

SECTION 段名稱
;或者
SEGMENT 段名稱
           

  NASM對段的數量沒有限制,不過Intel處理器要求段在記憶體中的其實實體位址起碼是16位元組對齊的。相應的在段定義使用:

align=子句	;用于定義某個SECTION的彙編位址對齊方式。
           

  為了友善取得該段的彙編位址,NASM編譯器提供了以下的表達方式:

section.段名稱.start		;
           

使用者程式頭部

硬碟的通路,程式重定位和加載

  頭部需要在源程式以一個段地形式出現在整個源程式的開頭:

SECTION header vstart=0
           

使用者的頭部起碼要包含以下資訊。

  • 使用者程式的尺存,即以位元組為機關的大小;加載器需要根據這一資訊來決定,讀取多少個邏輯扇區。
  • 應用程式的入口,包括段位址和偏移位址;必須在頭部給出第一條指令的段位址和偏移位址。
  • 段重定位表;使用者程式可能包含不止一個段,加載到記憶體後,每個段的位址必須重新确定一下。

外圍裝置及其接口

  總線技術(Bus),總線就是一排連接配接所有裝置的線路,每個連接配接到這個線路上的器件都必須擁有電子開關。以至于他們随時都能夠同這排電線連接配接,或者從這排電線上斷開。

  輸入輸出控制裝置集中器(I/O Controller Hub,ICH)晶片,該晶片是連接配接不同的總線,并協調各個I/O接口對處理器的通路,在個人計算機上,這塊晶片就是耳熟能詳的南橋。

硬碟的通路,程式重定位和加載

  如圖,處理器通過局部總線連接配接到 ICH 内部的處理器接口電路,然後在ICH的内部,又通過總線與各個I/O相連。在ICH的内部,內建了一些正常的外圍裝置接口,如USB,PATA(IDE),SATA,老式的總線接口(LPC)、時鐘等。每個裝置都有自己的I/O接口電路同ICH相連。為了友善,主機闆上都有這些I/O接口的插槽,每個裝置的I/O電路都設計成插卡式,可以友善插拔。

  ICH還對 PCI(PCI Express) 總線的支援,這條總線向外延伸,連接配接這主機闆上的若幹個拓展槽,比如顯示卡就可以插在PCI上,然後把顯示器接在顯示卡上。除了局部總線和PCI Express總線,每個I/O接口卡可能不止連接配接一個裝置,這些線路涉及複用和仲裁問題,是以他們自己有一套獨立的總線體系,稱為通訊總線或者裝置總線,比如USB總線和SATA總線。

  當處理器想通路某個裝置的時候,ICH會接到通知,然後負責提供傳輸通道和其他輔助支援,并不允許其他裝置和總線連接配接,反過來,某個裝置想要連接配接處理器,也是一樣的。

I/0端口和端口通路

  處理器是通過端口(Port) 來和外圍裝置打交道的。本質上,端口就是一些寄存器,類似于處理器内部的寄存器,不同之處僅在于,這些寄存器位于I/O接口電路中。端口是處理器和外圍裝置通過I/O交流的視窗,每一個I/O接口可能擁有很多個端口,分别用于不同的目的。端口在不同的計算機系統有着不同的實作方式,在一些計算機系統中,端口号是映射到記憶體位址空間的,比如0x00000 ~ 0xE0000是真實的實體記憶體位址,而0xE0001 ~ 0xFFFFF是從很多I/O接口那裡映射過來的,當通路這部分位址的時候,實際上是在通路I/O接口。

  而在另一些計算機系統中,端口是獨立編制的,不和記憶體發生關系,在這種計算機中,處理器的位址線及連接配接記憶體,也連接配接着每一個I/O接口,但是,處理器還有一個特殊的引腳M/IO#,在這裡,”#”表示低電平有效,也就是說,處理器通路記憶體的時候,他會讓M/IO#引腳呈現高電平,這個時候與記憶體相關的電路就會被打開,相反,如果處理器通路I/O接口,那麼M/IO#引腳呈現低電,記憶體電路會被禁止。與此同時,處理器發出的位址和M/IO#信号一起用來打開某個I/O接口,如果該I/O接口配置設定的端口号和處理器位址吻合的話。

  在獨立編制的系統中,存在65536個端口(端口号從0~65535),因為是獨立編制,是以不能使用類似mov指令通路端口,取代的是in(端口讀)和out(端口發送)指令來進行讀和寫。8位的端口使用寄存器AL故隻能通路0 ~255(0x00 ~ 0xff)16位的端口使用寄存器AX。

通過硬碟控制器端口讀扇區資料

  硬碟讀寫的基本機關是扇區,最經典的方式是向硬碟控制器分别發送磁頭号,柱面号,和扇區号(扇區在某個柱面上的編号),這稱為CHS模式。而另一種方式是所有的扇區統一從0編号編址,就是邏輯位址。

  • LBA号=C磁頭總數每道扇區數+H*每道扇區數+(S-1)

  LBA28采用28個比特來表示邏輯扇區号,從邏輯扇區(0x0000000~0xFFFFFFF),一共可以表示228=268435456個扇區,每個扇區可以有512個位元組,是以LBA28可以管理128GB的硬碟。現在普遍采用的是LBA48,采用48個比特來表示邏輯扇區号,這樣一來就可以表示131072TB的硬碟了。

這裡采用LBA28通路硬碟。

  第一步,設定要讀取的扇區數量。這個數值要寫入0x1f2端口,這是個8位的端口,是以每次最多讀寫255個扇區。

mov dx,0x1f2
mov al,0x01
out dx,al
           

  如果寫入的值為0,則表示要讀取256個扇區,每讀取一個扇區,這個數值就會減1,如果讀寫的過程中發生錯誤,那麼這個端口就包含這尚未讀取的硬碟數。

  第二步,設定起始LBA扇區号(操作端口0x1f3-0x1f7),扇區的讀寫是連續的,是以隻要給出第一個扇區的編号就可以讀取所有的扇區了,28個扇區号将會被分成4段(每個段8位),分别寫入0x1f3-0x1f6端口,其中0x1f3端口存放的是0-7位,0x1f4端口存放8-15位,0x1f5存放16-23位,0x1f6存放24-27位,最後,0x1f6高4位用來訓示是主盤或者是從盤,以及是采用CHS模式還是LBA模式。

mov dx,0x1f3
mov al,0x02
out dx,al ;LBA 位址 7~0
inc dx ;0x1f4
mov al,0x00
out dx,al ;LBA 位址 15~8
inc dx ;0x1f5
out dx,al ;LBA 位址 23~16
inc dx ;0x1f6
mov al,0xe0 ;LBA 模式,主硬碟,以及 LBA 位址 27~24
out dx,al
           

  第三步,向端口0x1f7寫入0x20,請求端口讀(寫入0x30就是請求端口寫)

mov dx,0x1f7
mov al,0x20 ;讀指令
out dx,al
           

  第四步,等待讀寫操作完成。0x1f7既是指令端口,又是狀态端口。在硬碟内部操作期間,它會将0x1f7端口的第7位置1,表明硬碟在忙,一旦硬碟準備好了,它再将這個位清零,同時将第三位置1,表明這個時候主機可以向硬碟發送資料或者從硬碟接受資料了。

mov dx,0x1f7
.waits:
    in al,dx
	and al,0x88
	cmp al,0x08
	jnz .waits ;不忙,且硬碟已準備好資料傳輸
           
硬碟的通路,程式重定位和加載

  第五步,連續取出資料(操作端口0x1f0)。0x1f0是硬碟的資料接口,而且這個端口還是16位的,一旦硬碟準備就緒,就可以連續地從這個接口寫入或者讀取資料了。

mov cx,256 ;總共要讀取的字數
	mov dx,0x1f0
.readw:
	in ax,dx
	mov [bx],ax
 	add bx,2
 	loop .readw
           

  最後,0x1f1是一個錯誤代碼寄存器,包含硬碟驅動器最後一次執行指令後的狀态(錯誤原因)。

加載使用者程式

  加載器首先是在硬碟上讀取使用者檔案(程式),因為我們肯定是要事先知道使用者頭部的(總長度,所有段位址等),是以要預先讀取一個扇區,又因為每個扇區是512個位元組,但是使用者程式可能很大,而在實模式下,每個段的大小最大是64KB,是以每加載一個扇區,都重新設定段(每個段位址隻要往後移動0x20就可以了)。

代碼清單

;代碼清單8-2
         ;檔案名:c08.asm
         ;檔案說明:使用者程式      
;===============================================================================

SECTION header vstart=0                     ;定義使用者程式頭部段 

    program_length  dd program_end          ;程式總長度[0x00]

    ;使用者程式入口點

    code_entry      dw start                ;偏移位址[0x04]

                    dd section.code_1.start ;段位址[0x06] 

    realloc_tbl_len dw (header_end-code_1_segment)/4

                                            ;段重定位表項個數[0x0a]    

    ;段重定位表           

    code_1_segment  dd section.code_1.start ;[0x0c]

    code_2_segment  dd section.code_2.start ;[0x10]

    data_1_segment  dd section.data_1.start ;[0x14]

    data_2_segment  dd section.data_2.start ;[0x18]

    stack_segment   dd section.stack.start  ;[0x1c]    

    header_end:                

;===============================================================================

SECTION code_1 align=16 vstart=0         ;定義代碼段1(16位元組對齊) 

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
;-------------------------------------------------------------------------------
  start:

         ;初始執行時,DS和ES指向使用者程式頭部段

         mov ax,[stack_segment]           ;設定到使用者程式自己的堆棧 

         mov ss,ax

         mov sp,stack_end         

         mov ax,[data_1_segment]          ;設定到使用者程式自己的資料段

         mov ds,ax

         mov bx,msg0

         call put_string                  ;顯示第一段資訊 

         push word [es:code_2_segment]

         mov ax,begin

         push ax                          ;可以直接push begin,80386+
        
         retf                             ;轉移到代碼段2執行          

  continue:
         mov ax,[es:data_2_segment]       ;段寄存器DS切換到資料段2 

         mov ds,ax         

         mov bx,msg1

         call put_string                  ;顯示第二段資訊 

         jmp $ 
;===============================================================================
SECTION code_2 align=16 vstart=0          ;定義代碼段2(16位元組對齊)

  begin:
         push word [es:code_1_segment]

         mov ax,continue

         push ax                          ;可以直接push continue,80386+
        
         retf                             ;轉移到代碼段1接着執行         
;===============================================================================

SECTION data_1 align=16 vstart=0

    msg0 db '  This is NASM - the famous Netwide Assembler. '

         db 'Back at SourceForge and in intensive development! '

         db 'Get the current versions from http://www.nasm.us/.'

         db 0x0d,0x0a,0x0d,0x0a

         db '  Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a

         db '     xor dx,dx',0x0d,0x0a

         db '     xor ax,ax',0x0d,0x0a

         db '     xor cx,cx',0x0d,0x0a

         db '  @@:',0x0d,0x0a

         db '     inc cx',0x0d,0x0a

         db '     add ax,cx',0x0d,0x0a

         db '     adc dx,0',0x0d,0x0a

         db '     inc cx',0x0d,0x0a

         db '     cmp cx,1000',0x0d,0x0a

         db '     jle @@',0x0d,0x0a

         db '     ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a

         db 0
===============================================================================

SECTION data_2 align=16 vstart=0

    msg1 db '  The above contents is written by LeeChung. '

         db '2011-05-06'

         db 0
;===============================================================================
SECTION stack align=16 vstart=0           
         resb 256
stack_end:  
;===============================================================================
SECTION trail align=16

program_end:
           
;代碼清單8-1
         ;檔案名:c08_mbr.asm
         ;檔案說明:硬碟主引導扇區代碼(加載程式) 
         app_lba_start equ 100           ;聲明常數(使用者程式起始邏輯扇區号)
                                         ;常數的聲明不會占用彙編位址
     
SECTION mbr align=16 vstart=0x7c00                                     

         ;設定堆棧段和棧指針 

         mov ax,0      

         mov ss,ax

         mov sp,ax
         mov ax,[cs:phy_base]            ;計算用于加載使用者程式的邏輯段位址 
         mov dx,[cs:phy_base+0x02]

         mov bx,16        

         div bx            

         mov ds,ax                       ;令DS和ES指向該段以進行操作

         mov es,ax                        

         ;以下讀取程式的起始部分 

         xor di,di

         mov si,app_lba_start            ;程式在硬碟上的起始邏輯扇區号 

         xor bx,bx                       ;加載到DS:0x0000處 

         call read_hard_disk_0
         ;以下判斷整個程式有多大

         mov dx,[2]                      ;曾經把dx寫成了ds,花了二十分鐘排錯 
         mov ax,[0]
         mov bx,512                      ;512位元組每扇區
         div bx
         cmp dx,0
         jnz @1                          ;未除盡,是以結果比實際扇區數少1 
         dec ax                          ;已經讀了一個扇區,扇區總數減1 
   @1:
         cmp ax,0                        ;考慮實際長度小于等于512個位元組的情況 

         jz direct

         ;讀取剩餘的扇區

         push ds                         ;以下要用到并改變DS寄存器 
         mov cx,ax                       ;循環次數(剩餘扇區數)
   @2:
         mov ax,ds

         add ax,0x20                     ;得到下一個以512位元組為邊界的段位址
         mov ds,ax                               
         xor bx,bx                       ;每次讀時,偏移位址始終為0x0000 

         inc si                          ;下一個邏輯扇區 

         call read_hard_disk_0

         loop @2                         ;循環讀,直到讀完整個功能程式 

         pop ds                          ;恢複資料段基址到使用者程式頭部段 
   
         ;計算入口點代碼段基址 

   direct:

         mov dx,[0x08]

         mov ax,[0x06]

         call calc_segment_base

         mov [0x06],ax                   ;回填修正後的入口點代碼段基址       

         ;開始處理段重定位表

         mov cx,[0x0a]                   ;需要重定位的項目數量

         mov bx,0x0c                     ;重定位表首位址

 realloc:

         mov dx,[bx+0x02]                ;32位位址的高16位 

         mov ax,[bx]

         call calc_segment_base

         mov [bx],ax                     ;回填段的基址

         add bx,4                        ;下一個重定位項(每項占4個位元組) 
         loop realloc 

         jmp far [0x04]                  ;轉移到使用者程式  

;-------------------------------------------------------------------------------

read_hard_disk_0:                        ;從硬碟讀取一個邏輯扇區
                                         ;輸入:DI:SI=起始邏輯扇區号
                                         ;      DS:BX=目标緩沖區位址

         push ax

         push bx

         push cx

         push dx      

         mov dx,0x1f2

         mov al,1

         out dx,al                       ;讀取的扇區數

         inc dx                          ;0x1f3

         mov ax,si

         out dx,al                       ;LBA位址7~0

         inc dx                          ;0x1f4

         mov al,ah

         out dx,al                       ;LBA位址15~8

         inc dx                          ;0x1f5

         mov ax,di

         out dx,al                       ;LBA位址23~16

         inc dx                          ;0x1f6

         mov al,0xe0                     ;LBA28模式,主盤

         or al,ah                        ;LBA位址27~24

         out dx,al
         inc dx                          ;0x1f7

         mov al,0x20                     ;讀指令

         out dx,al
  .waits:

         in al,dx

         and al,0x88

         cmp al,0x08

         jnz .waits                      ;不忙,且硬碟已準備好資料傳輸 

         mov cx,256                      ;總共要讀取的字數

         mov dx,0x1f0

  .readw:

         in ax,dx

         mov [bx],ax

         add bx,2

         loop .readw
         pop dx

         pop cx

         pop bx

         pop ax
    
         ret
;-------------------------------------------------------------------------------
calc_segment_base:                       ;計算16位段位址
                                         ;輸入:DX:AX=32位實體位址
                                         ;傳回:AX=16位段基位址 

         push dx                                   

         add ax,[cs:phy_base]

         adc dx,[cs:phy_base+0x02]

         shr ax,4

         ror dx,4

         and dx,0xf000

         or ax,dx
      
         pop dx
         ret
;-------------------------------------------------------------------------------

         phy_base dd 0x10000             ;使用者程式被加載的實體起始位址

 times 510-($-$$) db 0
                  db 0x55,0xaa
           

繼續閱讀