QNX 動態鏈接

QNX 動態鏈接

  在一個典型的系統中,會運行許多程序。每個程序都依賴於一些函數,其中一些是標準的C庫函數,如printf()、malloc()、write()等。
  如果每個程序都使用標準的C庫,那麼每個程序通常都有這個特定庫的惟一副本。不幸的是,這導致了資源的浪費。由於C庫是公共的,所以讓每個程序引用該庫的公共實例比讓每個程序包含該庫的副本更有意義。這種方法有幾個優點,其中最重要的是節省了所需的系統總內存。

靜態鏈接

  術語靜態鏈接意味着程序和它所鏈接的特定庫在鏈接時由鏈接器組合在一起。這意味着程序和特定庫之間的綁定是固定的,並且在程序運行之前就已經知道了。這也意味着我們不能改變這個綁定,除非我們用庫的新版本重新鏈接程序。
  如果您不確定某個庫的正確版本在運行時是否可用,或者您正在測試某個庫的新版本,但又不希望將其安裝爲共享的,那麼您可以考慮靜態地鏈接一個程序。
  靜態鏈接的程序是針對對象(庫)的歸檔文件鏈接的,這些對象(庫)的擴展名通常是.a。這種對象集合的一個例子是標準C庫libc.a

動態鏈接

  動態鏈接這個術語意味着程序和它引用的特定庫在鏈接時不會被鏈接器組合在一起。相反,鏈接器將信息放入可執行文件中,告訴加載程序共享對象模塊代碼所在位置,以及應該使用哪個運行時鏈接器來查找和綁定引用。這意味着程序和共享庫之間的綁定是在運行時完成的——在程序啓動之前,找到並綁定適當的共享庫。
  這種類型的程序稱爲部分綁定的可執行程序,因爲它沒有被完全解析——鏈接器在鏈接時並沒有導致程序中所有引用的符號都與庫中的特定代碼相關聯。相反,鏈接者只是簡單地說:這個程序在一個特定的共享對象中調用了一些函數,所以我只需要記下這些函數在哪個共享對象中,然後繼續。實際上,這將綁定延遲到運行時。
  動態鏈接的程序被鏈接到具有擴展名的共享對象上。此類對象的一個示例是標準C庫的共享對象版本,即libc.so。
  您可以使用編譯器驅動程序qcc的命令行選項來告訴工具鏈是靜態鏈接還是動態鏈接。然後,此命令行選項確定所使用的擴展名(.a或.so)。

在運行時擴展代碼

  更進一步說,程序在運行之前可能不知道需要調用哪些函數。雖然這一開始看起來有點奇怪(畢竟,一個程序怎麼可能不知道它將調用什麼函數呢?),但它確實是一個非常強大的功能。
  考慮一個通用的磁盤驅動程序。它啓動、探測硬件並檢測硬盤。然後驅動程序將動態加載io-blk代碼來處理磁盤塊,因爲它發現了一個面向塊的設備。現在驅動程序已經在塊級別訪問了磁盤,它發現磁盤上有兩個分區:一個DOS分區和一個電力安全分區。我們沒有強制磁盤驅動程序包含它可能遇到的所有可能的分區類型的文件系統驅動程序,而是保持簡單:它沒有任何文件系統驅動程序!在運行時,它檢測到兩個分區,然後知道應該加載fs-dos.so和fs-qnx6.so文件系統代碼來處理這些分區。通過延遲決定哪個函數調用,我們增強了磁盤驅動程序的靈活性(並減少了它的大小).

如何使用共享庫

  爲了理解程序如何使用共享庫,我們首先查看可執行文件的格式,然後檢查程序啓動時發生的步驟。

ELF format

  QNX中微子RTOS使用ELF(可執行和鏈接格式)二進制格式,該格式目前在SVR4 Unix系統中使用。ELF不僅簡化了創建共享庫的任務,而且增強了模塊在運行時的動態加載。
  在下面的關係圖中,我們展示了ELF文件的兩個視圖:鏈接視圖和執行視圖。鏈接視圖,當程序或庫被鏈接時使用,涉及各種sections(包含object文件)。Section含大量的object文件信息:數據data、指令instruction、重定位信息relocation info、符號symbol、調試信息debug info等。執行視圖,程序運行時使用,涉及各種segments。
  在鏈接時,程序或庫是通過將具有相似屬性的section合併到段Segments中來構建的。通常,所有可執行的和只讀的數據section被合併到一個“text”段segments中,而數據和“BSS”被合併到“data”段segments中。這些段稱爲加載段,因爲它們需要在進程創建時加載到內存中。其他部分(如符號信息和調試部分)被合併到其他非加載段中。
Figure 37: Object file format: linking view and execution view.

ELF without COFF

  ELF加載器的大多數實現都派生於COFF(Common Object File Format)加載器。它們在加載時使用ELF對象的鏈接視圖。這是低效的,因爲程序加載器必須使用節來加載可執行文件。一個典型的程序可能包含大量的節,每個節都必須位於程序中,並分別裝入內存。
  然而,QNX中微子完全不依賴於COFF加載sections的技術。在開發我們的ELF實現時,我們直接按照ELF規範工作,並將效率放在首位。ELF加載器使用程序的“執行視圖”。通過使用執行視圖,加載器的任務大大簡化了:它所要做的就是將程序或庫的加載段(通常是兩個)複製到內存中。因此,流程創建和庫加載操作要快得多。

典型進程的內存佈局

  下圖顯示了一個典型進程的內存佈局。進程加載段(對應於圖中的文本和數據)在進程的基本地址加載。主堆棧位於下面並向下擴展。創建的任何其他線程都有自己的堆棧,位於主堆棧之下。每個堆棧由一個保護頁分隔,以檢測堆棧溢出。堆位於進程之上,並向上增長。
Figure38

  在進程地址空間的中間,爲共享對象保留了一個大區域。共享庫位於地址空間的頂部,並向下擴展。創建新進程時,進程管理器首先將可執行文件中的兩個段映射到內存中。然後對程序的ELF頭進行解碼。如果程序頭指示可執行文件鏈接到共享庫,則進程管理器將從程序頭提取動態解釋器的名稱。動態解釋器指向一個包含運行時鏈接共享庫。進程管理器將在內存中加載這個共享庫,然後將控制權傳遞給這個庫中的代碼。

Runtime linker

  當針對共享對象鏈接的程序啓動時,或者當程序請求動態加載共享對象時,將調用運行時鏈接器。運行時鏈接器包含在C運行時庫中。
  運行時鏈接器在加載共享庫時執行幾個任務(.so file):

  1. 如果請求的共享庫還沒有加載到內存中,運行時鏈接器會加載它:
  • 如果共享庫名是完全限定的(即,以斜線開頭),它直接從指定位置加載。如果在那裏找不到,則不執行進一步的搜索。
  • 如果它不是一個完全限定的路徑名,運行時鏈接器將按如下方式搜索它:
    1. 如果可執行文件的動態部分包含DT_RPATH標記,則搜索由DT_RPATH指定的路徑。
    2. 如果沒有找到共享庫,則運行時鏈接器僅在程序未標記爲setuid的情況下,在LD_LIBRARY_PATH指定的目錄中搜索它。
    3. 如果仍然沒有找到共享庫,那麼運行時鏈接器將根據LD_LIBRARY_PATH環境變量(爲procnto指定的默認庫搜索路徑)(即 CS_LIBPATH配置字符串)。如果沒有指定,則將默認的庫路徑設置爲映像文件系統的路徑。
  1. 一旦找到請求的共享庫,它就會被加載到內存中。對於ELF共享庫,這是一個非常有效的操作:運行時鏈接器只需要使用兩次mmap()調用來將兩個加載段映射到內存中。
  2. 然後,共享庫被添加到進程已加載的所有庫的內部列表中。運行時鏈接器維護這個列表。
  3. 運行時鏈接器然後解碼共享對象的動態部分。
      此動態部分向鏈接器提供關於此庫所鏈接的其他庫的信息。它還提供了關於需要應用的重新定位和需要解析的外部符號的信息。運行時鏈接器將首先加載任何其他需要的共享庫(它們本身可能引用其他共享庫)。然後它將處理每個庫的重新定位。其中一些重定位是庫的本地重定位,而其他重定位則需要運行時鏈接器來解析全局符號。在後一種情況下,運行時鏈接器將在庫列表中搜索此符號。在ELF文件中,散列表用於符號查找,因此速度非常快。查找符號庫的順序非常重要,我們將在下面的符號名稱解析一節中看到。
      一旦應用了所有重定位,就會調用在共享庫的init部分註冊的任何初始化函數。這在c++的一些實現中用於調用全局構造函數。

在運行時加載共享庫

  通過使用dlopen()調用,進程可以在運行時加載共享庫,該調用指示運行時鏈接器加載該庫。加載庫之後,程序可以使用dlsym()調用來確定其地址,從而調用庫中的任何函數。
Note:請記住:共享庫只對動態鏈接的進程可用。
  該程序還可以使用dladdr()調用來確定與給定地址相關聯的符號。最後,當進程不再需要共享庫時,它可以調用dlclose()從內存中卸載該庫。

符號名稱解析

  當運行時鏈接器加載共享庫時,必須解析該庫中的符號。符號解析的順序和範圍很重要。如果共享庫調用的函數恰好在程序加載的多個庫中以相同的名稱存在,則搜索這些庫中此符號的順序至關重要。這就是爲什麼OS定義了幾個加載庫時可以使用的選項。
  所有具有全局作用域的對象(可執行程序和庫)都存儲在一個內部列表(全局列表)中。默認情況下,任何全局作用域對象都將其所有符號提供給任何加載的共享庫。全局列表最初包含可執行文件和在程序啓動時加載的任何庫。
  默認情況下,當使用dlopen()調用加載一個新的共享庫時,該庫中的符號通過以下順序搜索解析:

  1. 加載的共享庫
  2. LD_PRELOAD環境變量指定的庫列表。您可以在運行程序時使用此環境變量來添加或更改功能。對於setuid或setgid ELF二進制文件,只加載包含在標準搜索目錄中也setuid的庫。
  3. 全局列表
  4. 共享庫引用的任何依賴對象(即,共享庫鏈接到的任何其他庫)
      當dlopen()'ing一個共享庫時,運行時鏈接器的作用域行爲可以通過兩種方式改變:
  5. 當程序加載一個新庫時,它可以通過將RTLD_GLOBAL標誌傳遞給dlopen()調用,指示運行時鏈接器將庫的符號放在全局列表中。這將使庫的符號對隨後加載的任何庫都可用。
  6. 可以修改解析共享庫中的符號時搜索的對象列表。如果將RTLD_GROUP標誌傳遞給dlopen(),則只搜索庫直接引用的對象的符號。如果傳遞RTLD_WORLD標誌,只搜索全局列表中的對象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章