【OS学习笔记】十 实模式:实现一个程序加载器-程序加载器如何将用户程序加载到内存并执行
上一篇文章學(xué)習(xí)了以下內(nèi)容:
- 用一種不同的分段方法,從另一個不同的的角度理解處理器的分段內(nèi)存訪問機(jī)制
- 使用循環(huán)和條件轉(zhuǎn)移指令來優(yōu)化主引導(dǎo)扇區(qū)代碼
點擊鏈接查看上一篇文章:點擊鏈接查看
對于主引導(dǎo)扇區(qū)部分。大概前幾篇文章已經(jīng)學(xué)的差不多了。現(xiàn)在是時候跳過主引導(dǎo)扇區(qū)去學(xué)習(xí)其他部分內(nèi)容。本篇文章記錄學(xué)習(xí)以下內(nèi)容:
- 學(xué)習(xí)操作系統(tǒng)加載應(yīng)用程序的過程,演示段的重定位方法,最終徹底理解8086的分段內(nèi)存管理機(jī)制
- 深入理解程序的加載與段地址的重定位過程
- 學(xué)習(xí)X86處理器過程調(diào)用的程序執(zhí)行機(jī)制
1、主引導(dǎo)扇區(qū)過后是什么
主引導(dǎo)扇區(qū)是處理器邁向廣闊天地的第一塊跳板。離開主引導(dǎo)扇區(qū)后,前方通常就是操作系統(tǒng)。
和主引導(dǎo)扇區(qū)一樣,操作系統(tǒng)也是位于硬盤上的。操作系統(tǒng)需要安裝到硬盤上。這個安裝的過程不僅需要將操作系統(tǒng)的指令和數(shù)據(jù)寫入硬盤,通常還要更新主引導(dǎo)扇區(qū)的內(nèi)容。好讓主引導(dǎo)扇區(qū)直接連著操作系統(tǒng)。
我們前面寫的主引導(dǎo)扇區(qū)一直都是在顯示字符串和做加法。這這太過簡單。不過作為初學(xué),很有必要。
操作系統(tǒng)通常肩負(fù)著處理器管理、內(nèi)存分配、程序加載、進(jìn)程調(diào)度等的任務(wù)。想要自己寫一個操作系統(tǒng),還是相當(dāng)困難的。但是我們可以模擬一下操作系統(tǒng)的一些功能,寫一個小程序。比如,我們可以模擬操作系統(tǒng)加載用戶程序到內(nèi)存的這一過程。
我們知道編譯好的程序通常都是存放在硬盤這樣的載體上,需要加載到內(nèi)存之后才能運行。這個過程很復(fù)雜。首先需要讀取硬盤,然后決定把它加載到內(nèi)存的什么位置。最重要的是程序通常都是分段的,載入內(nèi)存之后,還需要重新計算它的段地址。這叫做段的重定位。
那么本篇文章目的就是:把主引導(dǎo)扇區(qū)改造成一個加載器程序,它的功能是加載用戶程序,并執(zhí)行該程序(將處理器的控制權(quán)交給用戶程序)。
說明:參考的原書X86匯編在這里講了很多如何從硬盤讀數(shù)據(jù)這種太底層的操作。對于一個只是想了解底層原理的應(yīng)用程序開發(fā)人員不必過多了解對硬件的操作。所以我對硬件的操作極其簡單的一筆概括(本人是Java后臺開發(fā)工作)。想要深入理解硬件的操作請參考原書籍。
2、代碼清單
本篇文章的匯編代碼較多。加載程序有150行,而用戶程序也達(dá)到了行。
所以本文不直接貼代碼。而是在講解的時候分段貼出代碼。整個書本的代碼我上傳到CSDN資源。點擊下載觀看:點擊下載
本文的代碼是
- 8-1 (主引導(dǎo)扇區(qū)程序/加載器),源程序文件:c08_mbr.asm
- 8-2(被加載的用戶程序),源程序文件:c08.asm
3、分析
3.1 用戶程序的結(jié)構(gòu)(代碼8-2)
3.11 整體結(jié)構(gòu)
處理器的工作模式是將內(nèi)存分成邏輯上的段。指令的獲取和數(shù)據(jù)的訪問一律按“段地址:偏移地址”的方式進(jìn)行訪問。相對應(yīng)的,一個規(guī)范的應(yīng)用程序,應(yīng)當(dāng)包含代碼段、數(shù)據(jù)段、附加段和棧段。
這樣一來,段的劃分和段與段之間的界限在程序加載到內(nèi)存之前就已經(jīng)劃分好了。
還記得在前幾篇文章中我們寫的主引導(dǎo)扇區(qū)程序的匯編代碼,都是整個程序作為一個分段。這樣導(dǎo)致數(shù)據(jù)段與代碼段都是重合的。這樣很容易出錯。所以今天我們的用戶程序,就不會只有一個段。而是采用多個段來寫。
我們先用以下圖示來給出一個合格的程序應(yīng)該有哪些段。
NASM編譯器使用匯編指令“SECTION”或者“SEGMENT”來定義段。align用于指定段的對其方式。vstart用于指定在在某一個段內(nèi)的指令的匯編地址是從該段所在的段頭開始計算而不是從程序的最開始的頭開始計算。
所以可以看出圖中"program_end:"這個標(biāo)號的匯編地址是從程序的開頭開始計算的。所以program_end可以代表整個程序的長度。
由圖中可以看出一個合格的程序大概需要有這些段:分別是header code data extra stack trail
我們再前面的文章中,也學(xué)習(xí)了代碼段,數(shù)據(jù)段,棧段,附加段。trail段很好理解,一般在結(jié)尾給一個標(biāo)號用于代表整個程序的長度的。
段的匯編地址其實就是段內(nèi)的第一個元素的匯編地址。
可以用如下方法得到段的匯編地址:
section.段名稱.start3.12 用戶程序的頭結(jié)構(gòu)
上面大概知道了我們用戶程序的整體分段結(jié)構(gòu)。瀏覽一下本章的代碼8-2,我們會發(fā)現(xiàn)我們的用戶程序一共定義了7個段,分別是:第7行定義的header段,27行定義的code_1段,163行定義的code_2段,173行定義的data_1段,194行定義的data_2段,201行定義的stack段和208行定義的trail段。
一般情況下,加載器程序和用戶程序是由不同的公司不同的人開發(fā)的。所以加載器與用戶程序?qū)嶋H上彼此并不知道彼此長什么樣。他們并不了解彼此的結(jié)構(gòu)與功能。
那么加載器該如何加載用戶程序呢?
首先用戶程序中必須得有一些信息,加載器可以利用這些信息將用戶程序加載到內(nèi)存中去。
實際上在用戶程序中,有這么一個段,叫做頭部。它里面包含了一些重要的信息,加載器利用這些信息足以將用戶程序加載到內(nèi)存中進(jìn)行運行。顧名思義,頭部,在用戶程序的開頭位置。如下圖:
用戶程序為了能夠讓加載器將自己加載到內(nèi)存中去,必須包含以下介個方面:
- 用戶程序的尺寸,即以字節(jié)為單位的大小
- 應(yīng)用程序的入口點,包括段地址和偏移地址
- 段重定位表以及每個表的表項。因為用戶程序一般不止一個段,比較大的程序可能包含多個代碼段和數(shù)據(jù)段。在程序沒有加載進(jìn)內(nèi)存之前,各個段都有自己的段地址(即匯編地址),但是程序加載進(jìn)內(nèi)存之后,一般來說加載到哪個內(nèi)存地址是不知道的,所以說此時各個段的實際內(nèi)存地址肯定變了,所以此時需要對段進(jìn)行重定位。
以下是我們的用戶程序8-2中的頭部代碼:
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;段重定位表項個數(shù)[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:注意:每段代碼后面的[]括號,里面的值是當(dāng)前行的匯編地址。
我們可以看到我們的用戶程序頭部中包含以下信息:
program_end代表程序的整個長度。因為在程序的最后一行如下圖:
可以看到,program_end這個標(biāo)號的匯編地址,就是整個程序的長度的大小。
程序的入口點的地址。包括段地址section.code_1.start和偏移地址start。可以看到在段code_1中,有一個標(biāo)號start。該程序就是從start處開始執(zhí)行的。相當(dāng)于程序的入口點。
段重定位表。可以看出,段重定位表項將各個段的匯編地址都記錄在內(nèi),用于在加載到內(nèi)存地址時的重定位工作。trail段沒有記錄在內(nèi),是因為trail段只用于標(biāo)識程序的結(jié)尾,從而標(biāo)記程序的整個大小,并沒有數(shù)據(jù)與指令可以給CPU執(zhí)行。
我們知道了用戶程序的大概分段以及用戶程序的頭部信息,就足以。
3.2 加載器的工作流程
上面的用戶程序是8-2.現(xiàn)在我們的加載器程序是8-1.注意區(qū)分程序文件。
從大的角度來說,加載器要家在一個程序到內(nèi)存中并使之執(zhí)行,需要做兩件事情:
那么我們的加載器應(yīng)該將用戶程序加載到什么位置呢?首先我們再來回顧一下整個的1M的內(nèi)存空間的布局情況,如下圖:
如圖可知,我們可以在0x10000-0x9FFFF范圍內(nèi)加載用戶程序。車不多500多KB。事實上,如果將低端的內(nèi)存空間合理安排一下,還可以騰出更多空降,但是沒必要,這里我們用不了那么多。
所以在這里我們將用戶程序加載到0x10000這個物理地址處。在源程序(8-1)的151行有如下代碼:phy_base dd 0x10000
3.21 準(zhǔn)備加載用戶程序(加載與重定位)
3.211、加載用戶程序(從硬盤讀)
我們已經(jīng)知道了將用戶程序加載到具體的物理地址的位置。接下來就可以加載了。
我們的主引導(dǎo)扇區(qū)程序(加載器程序)在這只定義了一個段:SECTION mbr align=16 vstart=0x7c00
vstart=0x7c00子句代表段內(nèi)所有元素的匯編地址都將從0x7c00開始計算。否則,因為主引導(dǎo)扇區(qū)的實際加載地址是0x0000:0x7c00,當(dāng)我們引用一個標(biāo)號時還需要加上那個落差0x7c00.
代碼清單8-1第12-14行:
mov ax,0 mov ss,axmov sp,ax用于初始化棧段寄存器SS和棧指針寄存器SP。棧的段地址是0x0000,段的長度是64KB,棧指針將在段內(nèi)0xFFFF-0x0000之間變化。
代碼清單8-1第16-21行用于取得一個真實的物理地址。這個地址是用戶程序的加載地址。并將DS和ES指向該地址的段地址,用于后期的操作。
mov ax,[cs:phy_base] ;計算用于加載用戶程序的邏輯段地址 mov dx,[cs:phy_base+0x02]mov bx,16 div bx mov ds,ax ;令DS和ES指向該段以進(jìn)行操作mov es,ax好了到目前為止。加載器已經(jīng)準(zhǔn)備好了一個狀態(tài)。這個狀態(tài)是它已經(jīng)取得了用戶程序的加載地址(真實的物理地址),并且用DS于ES來指向這個個地址的段地址,以方便后期的操作。
那么接下來,就是讀取硬盤上的用戶程序了。說白了就是訪問其他硬件。那么我們對如何訪問硬盤以及從硬盤上讀取數(shù)據(jù)并不感興趣。所以這里直接略過這部分的匯編代碼的詳細(xì)解說(感興趣的話可以閱讀原書籍內(nèi)容)。
但是有一點內(nèi)容可以說明,就是從硬盤上讀數(shù)據(jù)不是一下子就能讀完的。所以在這里,設(shè)置了一個過程調(diào)用,在需要讀數(shù)據(jù)的時候直接調(diào)用相關(guān)的讀書的匯編代碼即可,不需要重復(fù)寫讀數(shù)據(jù)的代碼。
處理器支持過程調(diào)用的指令機(jī)制。過程實際上就是一段普通的代碼。處理器可以用過程調(diào)用指令轉(zhuǎn)移到這段代碼執(zhí)行,然后再遇到過程返回指令時重新返回到調(diào)用出的下一條指令接著執(zhí)行。
如下圖是一個過程調(diào)用示意圖:
在調(diào)用其他過程之前,由于其他過程可能會使用一些寄存器,所以在這之前需要將這些寄存器的值先存起來,一般使用棧來保存這些值。在調(diào)用過程之后,再使用pop指令將之前保存過的寄存器的值在彈出來。
一般過程調(diào)用的指令是call指令。例如代碼清單8-1中的24-27行就是用于讀取程序的起始部分。
xor di,dimov si,app_lba_start ;程序在硬盤上的起始邏輯扇區(qū)號 xor bx,bx ;加載到DS:0x0000處 call read_hard_disk_0在read_hard_disk_0這個標(biāo)號下的代碼,首先需要push一些寄存器:
在最后的時候,將這些寄存器恢復(fù):
如下圖所示是調(diào)用前后棧的變化:
在call read_hard_disk_0指令執(zhí)行前,棧指針位于箭頭1 所指示的位置;call指令執(zhí)行后,由于壓入了IP的內(nèi)容,故棧指針移動到箭頭2 所指示的位置處;進(jìn)入過程后,出于保護(hù)現(xiàn)場的目的,壓入了4個通用寄存器AX,BX,CX,DX,此時棧指針繼續(xù)向低地址方向推進(jìn)到箭頭3 所指示的位置。
在過程的最后,是恢復(fù)現(xiàn)場,連續(xù)反序彈出4個通用寄存器內(nèi)容。此時棧指針又回到進(jìn)入過程內(nèi)部的位置,即箭頭2 處。最后,ret指令執(zhí)行時,由于處理器自動彈出一個字到IP寄存器,故過程返回后的瞬間,棧指針仍舊回到過程調(diào)用前,即箭頭1 所指示的位置。然后處理器就繼續(xù)之前的代碼進(jìn)行執(zhí)行。
再回到上一段代碼的意思,它是讀程序的開始的一部分。
為什么要先讀取程序的開始一部分呢(實際上是一個扇區(qū)512字節(jié)的大小)。因為這里面包含了程序的頭部。加載器需要先將頭部讀進(jìn)來,然后才能判斷整個源程序的大小(防止多讀或者少讀),從而接著讀剩下的代碼。
代碼清單8-2,30-55行,首先根據(jù)剛剛讀的程序頭,來計算用戶程序的總長度。然后將整個程序代碼加載進(jìn)內(nèi)存當(dāng)中。這里的代碼我就不貼了,可以自己看源碼。下面我們先來看一下程序頭部的各個條目在內(nèi)存中目前的地址(偏移地址)。如下圖:
由圖中可知用戶程序的總長度位于最開始的偏移地址為0的地方。并占有兩個字。由此對比30-55行代碼,將會更加清晰明了。
好了,整個用戶程序已經(jīng)被加載器加載到內(nèi)存中了。那么接下來要做的就是對整個用戶程序進(jìn)行重定位工作了。
3.212、重定位用戶程序
整個用戶程序已經(jīng)被加載器加載到內(nèi)存中了。那么接下來要做的就是對整個用戶程序進(jìn)行重定位工作了。
實際上就是確定每個段的段地址即可(并不需要知道每一條指令的地址)。重定位實際上就是在現(xiàn)在這個真實的物理內(nèi)存上計算出各個段的段地址,然后將真實的段地址再覆蓋程序頭部的各個段原來的匯編段地址即可。
由于用戶程序的各個段的匯編地址是可以得出來的,所以我們可以計算各個段的長度。知道了各個段的長度,然后又知道用戶程序在內(nèi)存中的起始位置地址phy_base。那么就可以很容易計算出各個段在內(nèi)存中的地址。如下圖,清晰明了:
以上圖示清晰的展示了內(nèi)存中的各個段與源程序的匯編地址的表示的段的關(guān)系。
源程序58-62行重定位了用戶程序的入口點的代碼段。
65-74行,重定位其他各個段。
3.22 將控制權(quán)交給用戶程序
76行代碼:jmp far [0x04] ;轉(zhuǎn)移到用戶程序
當(dāng)對用戶程序的各個段進(jìn)行了重定位后。就將控制權(quán)交給用戶程序了。我們在此前知道用戶程序的頭結(jié)構(gòu)在內(nèi)存中的結(jié)構(gòu)如下:
由此得知用處程序的入口點地址存在于內(nèi)存的0x04處。所以上面來一個段間遠(yuǎn)跳轉(zhuǎn),將執(zhí)行流跳轉(zhuǎn)到內(nèi)存偏移地址為0x04處。從而開始整個用戶程序的執(zhí)行。
就提是先訪問DS所指向的數(shù)據(jù)段,從偏移地址0x04處取出兩個字,并分別傳送到代碼段寄存器CS與指令指針寄存器IP,以替代他們原先的內(nèi)容。于是處理器就自行轉(zhuǎn)移到指定的位置開始執(zhí)行指令。
至此,我們已經(jīng)將用戶程序運行起來了。真是相當(dāng)?shù)牟蝗菀装?#xff01;!!
4、總結(jié)
本片文章學(xué)會以下內(nèi)容
-
用戶程序的分段結(jié)構(gòu)大致模樣
-
用戶程序的頭部結(jié)構(gòu)
-
加載器是如何加載用戶程序到內(nèi)存的
- 首先讀用戶程序開頭一部分(一般是512字節(jié)),從而獲取程序頭部
- 再根據(jù)用戶程序頭部讀取剩余的代碼
-
將用戶程序加載到內(nèi)存后還需要對用戶程序的各個段進(jìn)行重定位
- 根據(jù)各個段的長度以及用戶程序在內(nèi)存中的起始位置計算重定位后的地址
-
重定位后,將控制權(quán)交給用戶程序。這里直接給一個遠(yuǎn)跳轉(zhuǎn)指令,跳轉(zhuǎn)到用戶程序的入口點執(zhí)行即可。
以上內(nèi)容對理解程序的結(jié)構(gòu)非常有幫助。
筆記記得不是很全,像匯編的語法以及如何將代碼寫到虛擬硬盤的主引導(dǎo)扇區(qū)這些都沒有寫。如果又不懂的可以加我聯(lián)系方式一起交流。
學(xué)習(xí)探討加個人:
qq:1126137994
微信:liu1126137994
總結(jié)
以上是生活随笔為你收集整理的【OS学习笔记】十 实模式:实现一个程序加载器-程序加载器如何将用户程序加载到内存并执行的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android 非root app 捕捉
- 下一篇: @value 静态变量_面试官:为什么静