1. 程式人生 > >程式的裝入和連結

程式的裝入和連結

1. 地址相關概念

1. 實體地址(physical address)

      實體記憶體真實存在的插在主機板記憶體槽上的記憶體條的容量的大小.

      記憶體是由若干個儲存單元組成的,每個儲存單元有一個編號,這種編號可唯一標識一個儲存單元,稱為記憶體地址(或實體地址)。我們可以把記憶體看成一個從0位元組一直到記憶體最大容量逐位元組編號的儲存單元陣列,即每個儲存單元與記憶體地址的編號相對應。

  
2. 虛擬記憶體(Virtual memory)(也叫虛擬儲存器)

        虛擬記憶體地址就是每個程序可以直接定址的地址空間,不受其他程序干擾。每個指令或資料單元都在這個虛擬空間中擁有確定的地址。      

      虛擬記憶體就是程序中的目的碼,資料等虛擬地址組成的虛擬空間     

      虛擬記憶體不考慮實體記憶體的大小和資訊存放的實際位置,只規定程序中相互關聯資訊的相對位置。每個程序都擁有自己的虛擬記憶體,且虛擬記憶體的大小由處理機的地址結構和定址方式決定。

       如直接定址,如果cpu的有效地址長度為16位,則其定址範圍0 -64k。

       再比如32位機器可以直接定址4G空間,意思是每個應用程式都有4G記憶體空間可用。但是顯然機器記憶體罕有如此之大,可以支援每個程式使用4G記憶體的。
      虛擬記憶體與實體記憶體的區別:虛擬記憶體就與實體記憶體相反,是指根據系統需要從硬碟虛擬地勻出來的記憶體空間,是一種計算機系統記憶體管理技術

,屬於計算機程式,而實體記憶體為硬體。因為有時候當你處理大的程式時候系統記憶體不夠用,此時就會把硬碟當記憶體來使用,來交換資料做快取區,不過實體記憶體的處理速度是虛擬記憶體的30倍以上。

3. 邏輯地址(logical address)

        源程式經過彙編或編譯後,形成目的碼,每個目的碼都是以0為基址順序進行編址的,原來用符號名訪問的單元用具體的資料——單元號取代。這樣生成的目標程式佔據一定的地址空間,稱為作業的邏輯地址空間,簡稱邏輯空間。

       在邏輯空間中每條指令的地址和指令中要訪問的運算元地址統稱為邏輯地址。即應用程式中使用的地址。要經過定址方式的計算或變換才得到記憶體中的實體地址。

       很簡單,邏輯地址就是你源程式裡使用的地址,或者原始碼經過編譯以後編譯器將一些標號,變數轉換成的地址,或者相對於當前段的偏移地址。

      邏輯地址是指由程式產生的與段相關的偏移地址部分。例如,你在進行C語言指標程式設計中,可以讀取指標變數本身值(&操作),實際上這個值就是邏輯地址,它是相對於你當前程序資料段的地址,不和絕對實體地址相干。只有在Intel真實模式下,邏輯地址才和實體地址相等(因為真實模式沒有分段或分頁機制,Cpu不進行自動地址轉換);邏輯也就是在Intel保護模式下程式執行程式碼段限長內的偏移地址(假定程式碼段、資料段如果完全一樣)。應用程式設計師僅需與邏輯地址打交道,而分段和分頁機制對您來說是完全透明的,僅由系統程式設計人員涉及。應用程式設計師雖然自己可以直接操作記憶體,那也只能在作業系統給你分配的記憶體段操作。

     過有些資料是直接把邏輯地址當成虛擬地址,兩者並沒有明確的界限

    在linux核心,虛擬地址是3G-4G這段地址,它與實體地址通過頁表來對映,邏輯地址是指3G-3G+main_memory_size這段虛擬地址,它與實體地址的對映是線性的,當然也可以通過頁表對映。所以邏輯地址是虛擬地址的一部分。

        邏輯地址的組成:是由一個段識別符號加上一個指定段內相對地址的偏移量,表示為 [段識別符號:段內偏移量]

   

                      圖4.1  作業的名空間、邏輯地址空間和裝入後的物理空間

4. 線性地址或Linux下也叫虛擬地址(virtual address)

這個地址很重要,也很不容易理解。分段機制下CPU定址是二維的地址即,段地址:偏移地址,CPU不可能認識二維地址,因此需要轉化成一維地址即,段地址*16+偏移地址,這樣得到的地址便是線性地址(在未開啟分頁機制的情況下也是實體地址)。這樣有什麼意義呢?或者說這個一維地址的計算方法隨便一個學計算機的人都知道,但是你真的理解它的意思嗎?要想理解它的意思,必須要知道什麼是地址空間,下文詳述。

       線性地址是邏輯地址到實體地址變換之間的中間層。程式程式碼會產生邏輯地址,或者說是段中的偏移地址,加上相應段的基地址就生成了一個線性地址。如果啟用了分頁機制,那麼線性地址可以再經變換以產生一個實體地址。若沒有啟用分頁機制,那麼線性地址直接就是實體地址。Intel 80386的線性地址空間容量為4G(2的32次方即32根地址匯流排定址)。

       跟邏輯地址類似,它也是一個不真實的地址,如果邏輯地址是對應的硬體平臺段式管理轉換前地址的話,那麼線性地址則對應了硬體頁式記憶體的轉換前地址。

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

程式如何執行

       在多道程式環境下,要使程式執行,必須先為之建立程序。而建立程序的第一件事,便是將程式和資料裝入記憶體。如何將一個使用者源程式變為一個可在記憶體中執行的程式,通常都要經過以下幾個步驟:

        首先是要編譯,由編譯程式(Compiler)將使用者原始碼編譯成cpu可執行的目的碼,產生了若干個目標模組(Object  Module)(即若干程式段),

        其次是連結,由連結程式(Linker)將編譯後形成的一組目標模組(程式段),以及它們所需要的庫函式連結在一起,形成一個完整的裝入模組(Load  Module);

        最後是裝入,由裝入程式(Loader)將裝入模組裝入記憶體。圖 4-2 示出了這樣的三步過程。

         

                                                                       圖4-2  對使用者程式的處理步驟

2. 程式的裝入(地址的變換)

         為了闡述上的方便,我們先介紹一個無需進行連結的單個目標模組的裝入過程。該目標模組也就是裝入模組。在將一個裝入模組裝入記憶體時,可以有絕對裝入方式、可重定位裝入方式和動態執行時裝入方式,下面分別簡述之。

1.絕對裝入方式(Absolute Loading Mode)

          
         在編譯時,如果知道程式將駐留在記憶體的什麼位置,那麼,編譯程式將產生絕對地址的目的碼。即按照實體記憶體的位置賦予實際的實體地址。例如,事先已知使用者程式(程序)駐留在從R處開始的位置,則編譯程式所產生的目標模組(即裝入模組)便從R處開始向上擴充套件。絕對裝入程式按照裝入模組中的地址,將程式和資料裝入記憶體。裝入模組被裝入記憶體後,由於程式中的邏輯地址與實際記憶體地址完全相同,故不須對程式和資料的地址進行修改。程式中所使用的絕對地址,既可在編譯或彙編時給出,也可由程式設計師直接賦予。

        這個方式的優點:是CPU執行目的碼快。

       缺點:1)是由於記憶體大小限制,能裝入記憶體併發執行的程序數大大減少
                   2)編譯程式必須知道記憶體的當前空閒地址部分和其地址,並且把程序的不同程式段連續地存放起來,編譯非常複雜。由於程式  

       因此,通常是寧可在程式中採用符號地址,然後在編譯或彙編時,再將這些符號地址轉換為絕對地址。

       如何把虛擬記憶體地址空間變換到記憶體唯一的一維物理線性空間?涉及到兩個問題:
        一是虛擬空間的劃分問題。
        二是把虛擬空間中已經連結和劃分好的內容裝入記憶體,並將虛擬空間地址對映記憶體地址的問題。即地址對映。
        地址對映就是建立虛擬地址與記憶體地址的關係。


2.靜態地址重定位(可重定位裝入方式 Relocation Loading Mode)


       絕對裝入方式只能將目標模組裝入到記憶體中事先指定的位置。在多道程式環境下,編譯程式不可能預知所編譯的目標模組應放在記憶體的何處,因此,絕對裝入方式只適用於單道程式環境。在多道程式環境下,所得到的目標模組的起始地址通常是從 0 開始的,程式中的其它地址也都是相對於起始地址計算的。此時應採用可重定位裝入方式,根據記憶體的當前情況,將裝入模組裝入到記憶體的適當位置。 

       靜態地址重定位即在程式裝入對目的碼裝入記憶體的過程中完成,是指在程式開始執行前,程式中指令和資料的各個地址均已完成重定位,即完成虛擬地址到記憶體地址對映。地址變換通常是在裝入時一次完成的,以後不再改變。

       值得注意的是, 在採用可重定位裝入程式將裝入模組裝入記憶體後, 會使裝入模組中的所有邏輯地址與實際裝入記憶體的實體地址不同,圖4-3示出了這一情況。

    

                    圖4-3  作業裝入記憶體時的情況

     例如,在使用者程式的 1000 號單元處有一條指令LOAD 1,2500,該指令的功能是將 2500 單元中的整數 365 取至暫存器 1。但若將該使用者程式裝入到記憶體的 10000~15000號單元而不進行地址變換, 則在執行11000號單元中的指令時,它將仍從 2500 號單元中把資料取至暫存器1而導致資料錯誤。由圖4-3 可見,正確的方法應該是將取數指令中的地址 2500 修改成 12500,即把指令中的相對地址 2500 與本程式在記憶體中的起始地址 10000 相加,才得到正確的實體地址12500。除了資料地址應修改外,指令地址也須做同樣的修改,即將指令的相對地址 1000 與起始地址 10000 相加,得到絕對地址 11000。

優點:無需硬體支援

缺點:1)程式重定位之後就不能在記憶體中搬動了;

            2)要求程式的儲存空間是連續的,不能把程式放在若干個不連續的區域中。

3.動態地址重地位(動態執行時裝入方式 Dynamic Run-time Loading) 


        可重定位裝入方式可將裝入模組裝入到記憶體中任何允許的位置,故可用於多道程式環境;但這種方式並不允許程式執行時在記憶體中移動位置。因為,程式在記憶體中的移動,意味著它的物理位置發生了變化, 這時必須對程式和資料的地址(是絕對地址)進行修改後方能執行。然而,實際情況是,在執行過程中它在記憶體中的位置可能經常要改變,此時就應採用動態執行時裝入的方式。

     動態地址重定位:不是在程式執行之前而是在程式執行過程中進行地址變換。更確切的說,是把這種地址轉換推遲到程式真正要執行時才進行,即在每次訪問記憶體單元前才將要訪問的程式或資料地址變換成記憶體地址。動態重定位可使裝配模組不加任何修改而裝入記憶體。為使地址轉換不影響指令的執行速度,這種方式需要一個重定位暫存器的支援,

優點:1)目標模組裝入記憶體時無需任何修改,因而裝入之後再搬遷也不會影響其正確執行,這對於儲存器緊縮、解決碎片問題是極其有利的;

      2)一個程式由若干個相對獨立的目標模組組成時,每個目標模組各裝入一個儲存區域,這些儲存區域可以不是順序相鄰的,只要各個模組有自己對應的定位暫存器就行。

缺點:需要硬體支援。

3. 程式的連結

  源程式經過編譯後,可得到一組目標模組,再利用連結程式將這組目標模組連結,形成裝入模組。根據連結時間的不同,可把連結分成如下三種:
       (1) 、 靜態連結。在程式執行之前,先將各目標模組及它們所需的庫函式,連結成一個完整的裝配模組,以後不再拆開。我們把這種事先進行連結的方式稱為靜態連結方式。
       (2)、  裝入時動態連結。這是指將使用者源程式編譯後所得到的一組目標模組,在裝入記憶體時,採用邊裝入邊連結的連結方式。
       (3)、  執行時動態連結。這是指對某些目標模組的連結,是在程式執行中需要該(目標)模組時,才對它進行的連結。

 
1.靜態連結方式(Static Linking)

       我們通過一個例子來說明在實現靜態連結時應解決的一些問題。在圖 4-4(a)中示出了經過編譯後所得到的三個目標模組A、B、C,它們的長度分別為 L、M和N。在模組A中有一條語句CALL B,用於呼叫模組B。在模組B中有一條語句CALL C,用於呼叫模組C。B和C都屬於外部呼叫符號,在將這幾個目標模組裝配成一個裝入模組時,須解決以下兩個問題:  
         (1)  對相對地址進行修改。在由編譯程式所產生的所有目標模組中,使用的都是相對地址,其起始地址都為 0,每個模組中的地址都是相對於起始地址計算的。在連結成一個裝入模組後,原模組B和 C在裝入模組的起始地址不再是 0,而分別是 L和 L+M,所以此時須修改模組B和C中的相對地址,即把原B中的所有相對地址都加上 L,把原 C中的所有相對地址都加上L+M。 
          (2)  變換外部呼叫符號。將每個模組中所用的外部呼叫符號也都變換為相對地址,如把B 的起始地址變換為 L,把 C 的起始地址變換為 L+M,如圖 4-4(b)所示。這種先進行連結所形成的一個完整的裝入模組,又稱為可執行檔案。通常都不再拆開它,要執行時可直接將它裝入記憶體。這種事先進行連結,以後不再拆開的連結方式,稱為靜態連結方式。

       

                                     圖  4-4  程式連結示意圖

2.裝入時動態連結(Load-time Dynamic Linking) 


       使用者源程式經編譯後所得的目標模組,是在裝入記憶體時邊裝入邊連結的,即在裝入一個目標模組時,若發生一個外部模組呼叫事件,將引起裝入程式去找出相應的外部目標模組,並將它裝入記憶體,還要按照圖4-4所示的方式來修改目標模組中的相對地址。裝入時動態連結方式有以下優點:
        (1) 、 便於修改和更新。對於經靜態連結裝配在一起的裝入模組,如果要修改或更新其中的某個目標模組,則要求重新開啟裝入模組。這不僅是低效的,而且有時是不可能的。若採用動態連結方式,由於各目標模組是分開存放的,所以要修改或更新各目標模組是件非常容易的事。
        (2)、  便於實現對目標模組的共享。在採用靜態連結方式時,每個應用模組都必須含有其目標模組的拷貝,無法實現對目標模組的共享。但採用裝入時動態連結方式,OS則很容易將一個目標模組連結到幾個應用模組上,實現多個應用程式對該模組的共享。

3.執行時動態連結(Run-time Dynamic Linking)


        在許多情況下,應用程式在執行時,每次要執行的模組可能是不相同的。但由於事先無法知道本次要執行哪些模組,故只能是將所有可能要執行到的模組都全部裝入記憶體,並在裝入時全部連結在一起。顯然這是低效的,因為往往會有些目標模組根本就不執行。比較典型的例子是作為錯誤處理用的目標模組,如果程式在整個執行過程中都不出現錯誤,則顯然就不會用到該模組。 近幾年流行起來的執行時動態連結方式,是對上述在裝入時連結方式的一種改進。這種連結方式是將對某些模組的連結推遲到程式執行時才進行連結,亦即,在執行過程中,當發現一個被呼叫模組尚未裝入記憶體時,立即由OS去找到該模組並將之裝入記憶體,把它連結到呼叫者模組上。凡在執行過程中未被用到的目標模組,都不會被調入記憶體和被連結到裝入模組上,這樣不僅可加快程式的裝入過程,而且可節省大量的記憶體空間。

4. Windows NT動態連結庫

4.1. 構造動態連結庫

        DLL是包含函式和資料的模組,它的呼叫模組可為EXE或DLL,它由呼叫模組在執行時載入;載入時,它被對映到呼叫程序的地址空間。在VC中有一類工程用於建立DLL。

      •庫程式檔案 .C:相當於給出一組函式定義的原始碼;       •模組定義檔案 .DEF:相當於定義連結選項,也可在原始碼中定義;如:DLL中函式的引入和引出(dllimport和dllexport)。       •編譯程式利用 .C檔案生成目標模組 .OBJ       •庫管理程式利用 .DEF檔案生成DLL輸入庫 .LIB和輸出檔案 .EXP       •連結程式利用 .OBJ和 .EXP檔案生成動態連結庫 .DLL。

 4.2. DLL的裝入方法

1)裝入時動態連結(load-time):            –在程式設計時顯式呼叫某個DLL函式,該DLL函式在可執行檔案中稱為引入(import)函式。           –連結時需利用 .LIB檔案。在可執行檔案中為引入的每個DLL建立一個IMAGE_IMPORT_DESCRIPTOR結構。

      在裝入時由系統根據該DLL對映在程序中的地址改寫Import Address Table中的各項函式指標。Hint是DLL函式在DLL檔案中的序號,當DLL檔案修改後,就未必指向原先的DLL函式。在裝入時,系統會查詢相應DLL,並把它對映到程序地址空間,獲得DLL中各函式的入口地址,定位本程序中對這些函式的引用

裝入時動態連結過程

 (注:Import Address Table是在裝入時依據DLL模組的載入位置確定)。

DLL函式的呼叫過程:

2)執行時動態連結(run-time):        在程式設計時通過LoadLibrary(給出DLL名稱,返回裝入和連結之後該DLL的控制代碼), FreeLibrary, GetProcAddress(其引數包括函式的符號名稱,返回該函式的入口指標)等API來使用DLL函式。這時不再需要引入庫(import library)。       –LoadLibrary或LoadLibraryEx把可執行模組對映到呼叫程序的地址空間,返回模組控制代碼;       –GetProcAddress獲得DLL中特定函式的指標,返回函式指標;       –FreeLibrary把DLL模組的引用計數減1;當引用計數為0時,拆除DLL模組到程序地址空間的對映; 執行時動態連結的例子 [cpp] view plain copy print?
  1. HINSTANCE hInstLibrary;//模組控制代碼定義
  2. DWORD (WINAPI *InstallStatusMIF)(char*, char*, char*, char*, char*, char*, char*, BOOL);//函式指標定義
  3. if (hInstLibrary = LoadLibrary("ismif32.dll"))//對映
  4.      {  
  5.        InstallStatusMIF = (DWORD (WINAPI *)(char*,char*,char*, char*, char*, char*, char*, BOOL)) GetProcAddress(hInstLibrary, "InstallStatusMIF");//獲得函式指標
  6.         if (InstallStatusMIF)  
  7.         {  
  8.         if (InstallStatusMIF(“office97”, “Microsoft”, “Office 97”, “999.999”, “ENU”, “1234”, ”Completed successfully”, TRUE) !=0)//呼叫DLL模組中的函式
  9.             {  
  10.             }  
  11.         }  
  12.         FreeLibrary(hInstLibrary);//拆除對映
  13.      }