在計算機加電以後,計算機首先會讀取硬盤的主引導扇區,作一些必要的初始化工做,可是硬盤的一個扇區只有512字節,因此咱們要實現更多的功能,就要有用戶程序,咱們須要把控制權限交給用戶程序(操做系統暫且也算一種用戶程序吧)。app
在加載用戶程序的過程當中,主要分爲如下幾個大步驟:oop
用戶程序頭部:操作系統
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:
從硬盤讀取信息,須要五個步驟:rest
這個數值要寫入0x1f2端口,這是一個8位寄存器,因此能夠使用code
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表示請求讀it
mov al,0x20 mov dx,0x1f7 out dx,al
4.等待硬盤空閒
0x1f7這個端口,除了能夠請求讀意外,還能表示硬盤的狀態,第7位0表示空閒,1表示繁忙,第3位爲1表示準備好進行數據傳輸,因此這裏咱們要等待硬盤空閒才能夠傳輸數據:io
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]
至此,咱們的加載器就基本完成了,能夠用它來加載任何符合咱們規定格式的用戶程序(用戶程序頭部)。