1. 程式人生 > >使用匯編語言編寫載入器(載入使用者程式)

使用匯編語言編寫載入器(載入使用者程式)

使用匯編語言編寫載入器載入指定格式的使用者程式

在計算機加電之後,計算機首先會讀取硬碟的主引導扇區,做一些必要的初始化工作,但是硬碟的一個扇區只有512位元組,所以我們要實現更多的功能,就要有使用者程式,我們需要把控制權限交給使用者程式(作業系統暫且也算一種使用者程式吧)。

在載入使用者程式的過程中,主要分為以下幾個大步驟:

  • 一.從硬碟讀取使用者程式,並載入到記憶體中的指定位置(自定義)。
  • 二.重定位使用者程式(段地址)
  • 三.將控制權交給使用者程式

使用者程式頭部:

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:  

一.讀取使用者程式到記憶體

從硬碟讀取資訊,需要五個步驟:

  1. 設定要讀取的扇區數量

這個數值要寫入0x1f2埠,這是一個8位暫存器,所以可以使用

out dx,al

2.設定要讀取的起始LBA扇區號。這裡使用LBA28。28位的扇區號,分給四個埠,0x1f3-0x1f6,從低到高依次儲存,最後一個埠也就是0x1f6低四位儲存扇區號的最高四位,剩下四位高三位111表示LBA模式,最低以為0表示從盤,1表示主盤。例如,如果其實扇區是100,也就是0x60,那麼設定的程式碼如下:

mov dx,0x1f3
mov al,0x60
out dx,al
inc dx
xor al,al
out dx,al
inc dx
out dx,al
inc dx
mov al,0xe0
out dx,al

3.請求讀寫次數值寫入0x1f7埠,0x20表示請求讀

mov al,0x20
mov dx,0x1f7
out dx,al

4.等待硬碟空閒0x1f7這個埠,除了可以請求讀意外,還能表示硬碟的狀態,第7位0表示空閒,1表示繁忙,第3位為1表示準備好進行資料傳輸,所以這裡我們要等待硬碟空閒才可以傳輸資料:

waits:
    in al,dx
    and al,1000_1000B    ;保留第3位和第7位
    cmp 0000_1000B    ;第7位為0第3位為1才可以進行讀取
    jne waits        ;不相等就迴圈等待

5.讀資料從硬碟讀取資料,通過0x1f0埠,這是一個16位暫存器。硬碟是典型的塊裝置,所以一次必須讀取512位元組,或者它的倍數。比如,我們要讀取一個扇區的資料,並存放在ds:0開始的記憶體空間:

    mov dx,0x1f0
    mov cx,256    ;讀取一個扇區,512位元組,即256字
    xor bx,bx
read_word
    in ax,dx
    mov [bx],ax
    add bx,2
    loop read_word

6.檢查使用者程式是否讀取完整。剛才我們只讀取了一個扇區,即512位元組,我們並不能確定使用者程式是否已經完全讀完,但是我們已經讀取了使用者程式的頭部,這裡我們規定使用者程式頭部的第一個雙字必須定義使用者程式的長度.所以我們從ds:0的位置讀取兩個字,第一個字放在ax,第二個字放在dx,這樣dx:ax代表了使用者程式的總長度,用這個數除以512得到商和餘數,可以知道使用者程式的讀取進度,進而來決定是繼續讀取還是跳轉到後邊的步驟(重定位)。注意:由於一個邏輯段最大是64kb,從0x0000-0xffff,但是使用者程式可能超過這個範圍,為了避免這種事情發生,我們每讀一個扇區,便把段地址加0x20(512),這樣便可以連續存放且不用擔心超過邏輯段大小。

    ;檢查使用者程式是否讀取完整
    xor bx,bx
    mov ax,[bx]
    mov dx,[bx+2]
    mov bx,512
    div bx
    
    cmp dx,0
    jne cmp_ax
    dec ax
    
cmp_ax:
    cmp ax,0
    je  redirect_entry    ;跳轉到重定位
;讀取剩餘扇區
    push ds
    
    mov cx,ax
    mov si,start_sector    ;start_sector是定義的一個常數,這裡等於100(使用者程式在100扇區開始)
    
read_rest:
    mov ax,ds
    add ax,0x20
    mov ds,ax
    inc si
    call read_disk
    loop read_rest
    
    pop ds

二.重定位使用者程式

使用者程式在編寫的時候都是分段的,重定位的目的便是確定每個段的實際段地址。這裡我們規定使用者程式頭部中定義了每個段的段首位置(彙編地址),轉換成16位的段地址並重新寫入。

;重定位使用者程式
;重定位使用者入口點的段地址
redirect_entry:
    mov ax,[0x06]    ;讀取頭部入口點地址資訊
    mov dx,[0x08]
    call calc_seg_base
    mov [0x06],ax
    
;重定位其他段的段地址
    mov cx,[0x0a]
    mov bx,0x0c
redirect_other_seg:
    mov ax,[bx]
    mov dx,[bx+2]
    call calc_seg_base
    mov [bx],ax
    add bx,4
    loop redirect_other_seg
;過程計算段地址
;已知dx:ax實體地址,求出段地址並存放在ax中返回    
calc_seg_base:
    push dx
    
    add ax,[cs:usr_app_base]
    adc dx,[cs:usr_app_base+2]
    shr ax,4
    ror dx,4
    and dx,0xf000
    or  ax,dx
    
    pop dx
    
    ret

三.將控制權交給使用者程式

通過jmp far 命令進行段間跳轉,這裡是,跳轉到重定位後的入口點位置,存放在ds:0x04處

jmp far [0x04]

至此,我們的載入器就基本完成了,可以用它來載入任何符合我們規定格式的使用者程式(使用者程式頭部)。