程序的裝載和鏈接

今天跑去計算機學院上OS課的時候瞭解了下程序的裝載和鏈接,雖然很理論,但還是決定記下來,再加些自己的理解。

回來第一件想弄清楚的就是邏輯地址,虛擬地址和物理地址。我去網上查了一下,在http://bbs.chinaunix.net/thread-2083672-1-1.html看到瞭解釋,我覺得解釋的很好,就copy下了((__) )


###物理地址(physical address)

用於內存芯片級的單元尋址,與處理器和CPU連接的地址總線相對應。 ——這個概念應該是這幾個概念中最好理解的一個,但是值得一提的是,雖然可以直接把物理地址理解成插在機器上那根內存本身,把內存看成一個從0字節一直到最大空量逐字節的編號的大數組,然後把這個數組叫做物理地址,但是事實上,這只是一個硬件提供給軟件的抽像,內存的尋址方式並不是這樣。所以,說它是「與地址總線相對應」,是更貼切一些,不過拋開對物理內存尋址方式的考慮,直接把物理地址與物理的內存一一對應,也是可以接受的。也許錯誤的理解更利於形而上的抽像。

###虛擬內存(virtual memory)

這是對整個內存(不要與機器上插那條對上號)的抽像描述。它是相對於物理內存來講的,可以直接理解成「不直實的」,「假的」內存,例如,一個0x08000000內存地址,它並不對就物理地址上那個大數組中0x08000000 - 1那個地址元素; 之所以是這樣,是因為現代操作系統都提供了一種內存管理的抽像,即虛擬內存(virtual memory)。進程使用虛擬內存中的地址,由操作系統協助相關硬件,把它「轉換」成真正的物理地址。這個「轉換」,是所有問題討論的關鍵。 有了這樣的抽像,一個程序,就可以使用比真實物理地址大得多的地址空間。(拆東牆,補西牆,銀行也是這樣子做的),甚至多個進程可以使用相同的地址。不奇怪,因為轉換後的物理地址並非相同的。 ——可以把連接後的程序反編譯看一下,發現連接器已經為程序分配了一個地址,例如,要調用某個函數A,代碼不是call A,而是call 0x0811111111 ,也就是說,函數A的地址已經被定下來了。沒有這樣的「轉換」,沒有虛擬地址的概念,這樣做是根本行不通的。打住了,這個問題再說下去,就收不住了。

###邏輯地址(logical address)

Intel為了兼容,將遠古時代的段式內存管理方式保留了下來。邏輯地址指的是機器語言指令中,用來指定一個操作數或者是一條指令的地址。以上例,我們說的連接器為A分配的0x08111111這個地址就是邏輯地址。 ——不過不好意思,這樣說,好像又違背了Intel中段式管理中,對邏輯地址要求,「一個邏輯地址,是由一個段標識符加上一個指定段內相對地址的偏移量,表示為 [段標識符:段內偏移量],也就是說,上例中那個0x08111111,應該表示為[A的代碼段標識符: 0x08111111],這樣,才完整一些。 線性地址(linear address)或也叫虛擬地址(virtual address) 跟邏輯地址類似,它也是一個不真實的地址,如果邏輯地址是對應的硬件平臺段式管理轉換前地址的話,那麼線性地址則對應了硬件頁式內存的轉換前地址。

CPU將一個虛擬內存空間中的地址轉換為物理地址,需要進行兩步:首先將給定一個邏輯地址(其實是段內偏移量,這個一定要理解!!!),CPU要利用其段式內存管理單元,先將為個邏輯地址轉換成一個線程地址,再利用其頁式內存管理單元,轉換為最終物理地址。


下面就是就說重點了(__)

###程序的裝入

最早的時候是沒有物理地址和邏輯地址之分的,意思就是隻有物理地址,程序的操作都是直接對物理地址進行操作的,也叫絕對地址,所以程序的載入方式叫做絕對載入方式,這種時候程序都是在單道環境下跑的;後來出現了多道程序,怎麼辦呢?假設有A,B,C三個模塊都要裝載,A的0號地址對應物理地址的0號,那麼B和C的0號地址怎麼辦呢(因為程序裡面給出的都是絕對地址)??於是就出現了可重定位的裝入方式。可重定位:把裝入時對目標程序中指令和數據修改過程稱為可重定位。其實也就是一個邏輯地址轉換成物理地址的過程,這樣A的地址還是從0,A的末地址可能是100,B的起始地址在加載的時候就變成了101,然後裡面對特定地址的操作都改了,比如在原來08處,指令是movax,1000,那對應過去就是在0x109處執行該指令。這樣雖然是解決了程序的邏輯地址轉化成物理地址,但是問題又出現了,就是在程序運行的時候我們要修改指令和數據怎麼辦呢??上面的加載方式一旦到了內存裡面就是變成了「綁定」的程序,移動都不能移動。這就又出現了動態運行時候加載程序方式:動態運行時的裝入程序把程序裝載到內存裡後,不直接修改裝入模塊中的地址,而是真正到執行的時候才進行地址的轉化。因此在執行到要修改程序指令或者數據之前都是相對地址,不會影響程序的移動。

###程序的鏈接

  • 靜態鏈接方式:比如我們需要CALL函數A,B,C長度為L,M,N,那麼鏈接的時候在內存裡就是安裝地址(假設起始地址是0)0,L,L+M這樣整合起來形成可執行文件,從這以後這個可執行文件也變成了「綁定文件」,你就不能拆開它了,要注意的是剛才的裝入內存是為了生成!比如程序中的有些庫函數就是在鏈接的時候的做靜態鏈接。這樣對程序的提升性實在是太差了,一個模塊要是想升級下,整個可執行文件都得大動干戈,而且還很浪費磁盤空間。總之就是用靜態鏈接的形式生成可執行文件的話,在載入內存之前是把所有要用到的東西都整合到一起,等到一切準備就緒再去執行!

  • 裝入時的動態連接:得到用戶所編譯後的目標模塊後,在裝入內存的時候是邊裝入邊鏈接的,比如在執行A模塊中需要B模塊了,那就去動態地尋找B模塊,與前面一股腦兒都整合到一起可好多了,起碼從程序的更新升級變好了很多,我想程序的C模塊升級下,不需要對整個可執行文件進行調整,只需要修改C模塊,因為這時候各個模塊都是相互獨立的。在動態鏈接的時候內存中需要C模塊的時候自動載入,同樣不需要的時候想丟就丟。但就是可能在實際程序運行的時候有些模塊是可能會運行,所以也就有很多無謂的裝入。

運行時動態鏈接:這個才是現在的主流,很多.dll文件都是這樣,在程序運行的時候需要了什麼模塊就去找什麼模塊,叫OS把它裝入內存,這個與與上面的裝入時的動態鏈接可有區別,上面的那個是在裝入程序的時候去加載所需模塊,但實際上程序運行的時候所需的模塊比裝入的模塊少多了,因為上面裝入的時候是可能需要的模塊。但是採用運行時動態加載就指定了所需什麼模塊就加載什麼模塊。節省了裝入的時間,也節省了磁盤空間。


书籍推荐