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