【Linux】動靜態庫鏈接原理

📝前言:

這篇文章我們來講講Linux——動靜態庫鏈接原理

🎬個人簡介:努力學習ing
📋個人專欄:Linux
🎀CSDN主頁 愚潤求學
🌄其他專欄:C++學習筆記,C語言入門基礎,python入門基礎,C++刷題專欄


目錄

  • 一,目標文件
  • 二,ELF文件
    • ELF文件格式的特點
    • 1. ELF形成可執行
    • 2. ELF可執行加載
      • 具體查看
        • Section查看
        • Segment查看
        • ELF Header查看
  • 三,理解鏈接與加載
    • 1. 靜態鏈接與靜態庫加載
      • 查看編譯后的符號表
        • 符號表
      • 查看反匯編目標文件的內容
    • 2. ELF加載與進程地址空間
      • 靜態鏈接總結
    • 3. 動態鏈接與動態庫加載
      • 動態鏈接器
      • 庫間的依賴
      • PLT

一,目標文件

在這里插入圖片描述
我們都知道,形成可執行需要經過 編譯 + 鏈接兩個步驟。當.c文件經過編譯后形成的.o文件就叫做可重定位/可重定向目標文件。
當我們只有一個.c文件被修改時,我們只需要對修改的文件進行重新編譯就行了,其他文件不需要。

二,ELF文件

.o文件,動靜態庫,可執行文件,內核轉儲(core dumps)都是ELF格式的二進制文件。

ELF文件格式的特點

ELF文件被劃分成很多個
在這里插入圖片描述

  • ELF Header:描述文件的全局屬性,主要作用是定位?件的其他部分
  • Program Header Table 列舉了所有有效的段(segments)和他們的屬性。表里記著每個段的開始的位置和位移(offset)、長度。(鏈接階段,有合并以后才會生成 Program Headers)【segments是什么后面講】
  • 用來描述整個ELF文件
  • Section就是,不同的數據會被存儲到不同的節中。如代碼節存儲了可執行代碼,數據節存儲了全局變量和靜態數據等
  • Section Header Table用來描述每個節的信息

1. ELF形成可執行

  1. 將多份 C/C++ 源代碼,翻譯成為?標 .o?件
  2. 將多份 .o ?件section進行合并(合并是:鏈接的過程之一)

在這里插入圖片描述
簡單來說,就是把多個.o文件 中具有相同特性的Section合并成一個大的Segment

2. ELF可執行加載

  • 一個ELF文件在加載到內存的時候,也會把這個文件中具有相同特性(比如:把只讀的代碼段和只讀數據合并)的Section合并,形成segment
  • 這個合并?作也已經在形成ELF的時候,合并?式已經確定了,具體合并原則被記錄在了ELF的 程序頭表(Program header table) 中

為什么要將Section合并?

  1. 為了減少頁面碎片,提高內存使用效率。如果不進行合并,假設頁面大小為 4096 字節(內存塊基本大小,加載,管理的基本單位),如果.text部分為4097字節,.init部分為 512 字節,那么它們將占用 3 個頁面(.text兩個 + .init一個),而合并后,它們只需 2 個頁面。
  2. 將具有相同屬性的section合并成?個大的segment,可以實現不同的訪問權限,從而優化內存管理和權限訪問控制

具體查看

Section查看

查看可執行程序的Section(我的可執行名稱叫test):

readelf -S test

在這里插入圖片描述
我們可以看到Section header table對每個Section的描述

查看可執行程序的Segment

readelf -l test

在這里插入圖片描述
在圖片中,我們就可以看到有哪些Section被合并成了一個Segment

提幾個重要的Section

  • text節 :保存了程序代碼指令的代碼節。
  • data節 :保存了初始化的全局變量和局部靜態變量等數據。
  • .rodata節 :保存了只讀的數據,如一行C語?代碼中的字符串。
  • .bss節 :為未初始化的全局變量和局部靜態變量預留位置(對于未初始化的全局變量,我們沒必要真正開辟空間,只需要在.bss里面描述出有多少未初始化的就行)
  • .symtab節 : Symbol Table 符號表,就是源碼里面那些函數名、變量名和代碼的對應關系。
  • .got.plt節 (全局偏移表 - 過程鏈接表):.got節保存了全局偏移表。.got節和.plt節?起提供了對導?的共享庫函數的訪問??,由動態鏈接器在運行時進行修改。
Segment查看

我們還可以看到其他信息:
在這里插入圖片描述
我們可以看到Program Header table對每個段的描述

你會不會很好奇,為什么可執行程序既有Section又有Segment
其實這只是ELF 文件提供 2 個不同的視圖/視角來讓我們理解這兩個部分:

  • Section 是鏈接視圖(Linking View),面向開發者/工具鏈。用于編譯和鏈接階段,供編譯器、鏈接器和調試工具使用
  • Segment 是執行視圖(Execution View),面向操作系統。用于程序加載和運行時,指導操作系統如何將文件映射到內存
ELF Header查看

用命令:

readelf -h test

在這里插入圖片描述

  • 我們可以看到ELF Header保存著一些大小 / 入口信息,用于定位?件的其他部分
  • 系統通過Magic來判斷文件是不是ELF的格式。Entry point(標識可執行程序的入口地址【虛擬地址】)

三,理解鏈接與加載

1. 靜態鏈接與靜態庫加載

因為靜態庫就是都是.o文件打包的,并且靜態庫在形成可執行的時候,會把庫中的函數實現直接拷貝一份到可執行里面。所以研究靜態鏈接,本質上是在研究.o文件是如何鏈接的。

test.c文件內容

  1 #include "mystring.h"                                                                                                                                                                                        2 3 int main()4 {5     char* msg = (char*)"hello world\n";6     print(msg); // 調用自定義的print7     return 0;8 }

查看編譯后的符號表

符號表

符號表用于記錄了目標文件中定義和引用的符號相關信息,如:函數名、變量名、全局常量名等。
會用一個長字符串表來存儲,像這樣:
在這里插入圖片描述
然后通過\0來劃分他們,通過\0我們可以記錄每個符號在串中的起始結束下標,就可以很快得到這個符號的名稱。

readelf -s test.o

在這里插入圖片描述
可以發現printUND的,就是:沒有定義

查看反匯編目標文件的內容

 objdump -d test.o

在這里插入圖片描述

  • 這里,調用print函數,但是它的跳轉地址被設置成了 0
  • 這是因為:在編譯 test.c 的時候,編譯器知道有print函數(因為有聲明)但是不知道具體的實現(即:不知道print在內存的哪里)。因此,編譯器只能將這兩個函數的跳轉地址先暫時設為0
  • 鏈接的時候!.o文件被合并,就會修改call中不確定的地址。(這就是靜態鏈接,也是外部符號的地址重定位步驟)

在這里插入圖片描述

2. ELF加載與進程地址空間

—個ELF程序,在沒有被加載到內存的時候,有沒有地址呢?
答案:有的,有虛擬地址

—個ELF程序,在沒有被加載到內存的時候,采用"平坦模式"(就是地址下標從 0 開始連續編址),對自己的代碼和數據進行統?編址
在這里插入圖片描述
最左側的就是ELF的虛擬地址!嚴格意義上應該叫做邏輯地址(起始地址 + 偏移量)

  • 進程的mm_structvm_area_struct在進程剛剛創建的時候,就是用ELF的統一編址的信息來初始化的。(每個segment有自己的起始地址和自己的長度,用來初始化內核結構中的[start, end]等范圍數據)
  • 同時,記載到內存中的可執行文件也有對應的物理內存地址
  • 這樣mm_struct的虛擬地址有了,程序的物理內存地址也有了,就可以填寫頁表了!!!
  • 所以:虛擬地址機制,不光OS要?持,編譯器也要支持

靜態鏈接總結

在這里插入圖片描述
通過這張圖梳理一遍靜態鏈接:

  • 首先,ELF文件在沒有加載到內存時,已經有了統一編址
  • 鏈接前,.o文件彼此不知道對方,所以沒有辦法call函數調用的具體地址
  • 在鏈接階段,會把可執行程序中需要的靜態庫的庫方法,拷貝一份給可執行程序。(這個時候,方法有了明確的地址,就可以進行地址重定位,把call的內容修改成具體的方法地址)
  • 當程序加載到內存中時,用統一編址初始化mm_struct,再結合實際物理內存地址,就可以構建好頁表
  • 并將程序的入口Entry被傳入到CPU的寄存器EIP中,就可以拿著EIP中的Entry進入程序并執行

也就是說:靜態鏈接在鏈接階段,已經完成了地址重定位操作,運行階段已經不需要靜態庫了,所以是編譯時(鏈接階段)鏈接!

3. 動態鏈接與動態庫加載

  • 對于動態鏈接,動態庫并不會直接拷貝到可執行程序的代碼中。
  • 所有程序是共用內存中的一份動態庫代碼的

那么,進程之間,又是如何共享庫的呢?
在這里插入圖片描述
先不挖細節,先說整體輪廓:

  • 動態庫也是文件,需要獨立加載到內存中,有自己的內存區域
  • 當動態庫加載到內存中時,動態庫的ELF格式會用來初始化進程mm_struct的共享區
  • 當在運行代碼區的代碼時,遇到了動態庫的方法,就會從代碼區跳轉到共享區,得到對應方法的虛擬地址,然后就可以用虛擬地址通過頁表映射找到內存中的代碼了

看似沒啥問題,但是,我們把目標放在從代碼區跳轉到共享區這一步:
如果要跳轉,則代碼區應該知道對應方法的內存地址。可是,如果動態庫是獨立的文件,只有程序加載的時候,動態庫才能真真被加載ELF的虛擬內存地址里。才能有對應方法的地址。所以動態鏈接,也就被推遲到了加載時

所以:

  • 因為動態庫也是獨立的文件,也要加載到進程的mm_struct ,但是在加載之前,動態庫還沒有映射到mm_struct上(即:動態庫的同一編址還沒有用來初始化對應的mm_struct里面對應的區域)
  • 所以在編譯鏈接時:可執行程序里面的代碼段,就不知道對應動態庫方法call。(無法像靜態鏈接一樣,直接填上方法具體的地址)
  • 只能等到程序加載到內存里以后,再填上。(這就是加載時鏈接
  • 但是,因為當可執行程序加載到內存中以后,代碼區具有只讀性,無法修改。所以我們需要借助一個中間層,來修改call的地址。
  • 這個中間層就是GOT(全局偏移量表),我們讓GOT表位于.data區(可修改),每一個位置存放著:方法 + 對應方法的庫名稱【本質是:方法在庫中的偏移量 + 庫名稱】
  • 而,原來的代碼中call:GOT表的起始地址 + 要調用的方法在GOT表中的偏移量
  • 當我們加載程序時,動態庫被加載到了mm_struct,就知道了動態庫的虛擬起始地址。
  • GOT表就會被修改,里面每個位置存儲的(通過:方法在庫中的偏移量 + 庫名稱)就變成了對應方法的絕對虛擬地址【這樣就相當于間接改了代碼區的call】,此時頁表也會被填寫
  • 這就完成了重定位,完成了動態鏈接

和文件系統關聯起來:
在這里插入圖片描述
這種?式實現的動態鏈接就被叫做 PIC 地址?關代碼 。換句話說,我們的動態庫不需要做任何修改,被加載到任意內存地址都能夠正常運?,并且能夠被所有進程共享,這也是為什么之前我們給編譯器指定-fPIC參數的原因,PIC=相對編址+GOT

下面在談幾個更細節的知識

動態鏈接器

【以下內容由AI生成】
在這里插入圖片描述
/lib64/ld - linux - x86 - 64.so.2這就是動態連接器,加載動態庫、符號解析與重定位、處理庫依賴、初始化庫函數…都是由它完成的。

在C/C++程序中,當程序開始執行時,并不會直接跳轉到 main 函數。實際上,程序的入口是 _start ,這是?個由C運行時庫(通常是glibc)或鏈接器(如ld)提供的特殊函數。在 _start 函數中,會執??系列初始化操作,其中就包括動態鏈接:
_start 函數會調?動態鏈接器的代碼來解析和加載程序所依賴的動態庫(shared libraries)。動態鏈接器會處理所有的符號解析和重定位,確保程序中的函數調?和變量訪問能夠正確地映射到動態庫中的實際地址。

庫間的依賴

庫也會調?其他庫!!庫之間是有依賴的,如何做到庫和庫之間互相調?也是與地址?關的呢?
答:庫中也有.GOT,和可執行?樣。
在這里插入圖片描述

PLT

PLT:延遲綁定(Lazy Binding)
作用:

  • 避免在程序啟動時解析所有動態庫函數(如果庫函數很多的話,就很浪費時間,因為有些庫函數可能沒被使用)
  • 而是在函數首次被調用時才進行地址解析

更具體的比較復雜,就不講述了。


🌈我的分享也就到此結束啦🌈
要是我的分享也能對你的學習起到幫助,那簡直是太酷啦!
若有不足,還請大家多多指正,我們一起學習交流!
📢公主,王子:點贊👍→收藏?→關注🔍
感謝大家的觀看和支持!祝大家都能得償所愿,天天開心!!!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/81317.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/81317.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/81317.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

第八節第三部分:認識枚舉、枚舉的作用和應用場景

認識枚舉 枚舉的概述 枚舉的特點 枚舉的應用場景 代碼: 代碼一:認識枚舉 A(枚舉) package com.d6_enum;public enum A {//注意:枚舉類的第一行必須羅列的是枚舉對象的名字X,Y,Z;private String name;public String…

Android framework 中間件開發(二)

上篇文章中我們講述了怎么去開發中間件 Android framework 中間件開發(一) 這篇我們講一下怎么打包中間件給外部應用使用 目錄 1.新建項目 2.編寫jar包代碼 3.打包jar包 4.使用jar包 我們可以直接將系統編譯出來的framework的jar包拿出來直接用,但是為了安全起見,防止用戶調用…

FC7300 IO 無法正常輸出高低電平問題排查

現象:Port、Dio配置正常的情況下,IO寫或者翻轉函數正常執行后,IO電平未按照預期切換電平。 排查: 第一步:檢查PORTx_PCRy寄存器值: DWP: 域寫保護:此字段指示允許哪個內核或 DMA 寫…

7 個正則化算法完整總結

哈嘍!我是我不是小upper~之前和大家聊過各類算法的優缺點,還有回歸算法的總結,今天咱們來深入聊聊正則化算法!這可是解決機器學習里 “過擬合” 難題的關鍵技術 —— 想象一下,模型就像個死記硬背的學生&am…

如何有效的開展接口自動化測試?

🍅 點擊文末小卡片,免費獲取軟件測試全套資料,資料在手,漲薪更快 一、簡介 接口自動化測試是指使用自動化測試工具和腳本對軟件系統中的接口進行測試的過程。其目的是在軟件開發過程中,通過對接口的自動化測試來提高測…

我設計的一個安全的 web 系統用戶密碼管理流程

作為一名有多年經驗的前端,在剛開始學習web后端的時候,就對如何設計一個安全的 web 系統用戶密碼管理流程有很多疑問。之前自己也實踐過幾種方法,但一直覺得不是十分安全。 我們知道,用戶在注冊或登錄界面填寫的密碼是明文的&…

煉丹學習筆記3---ubuntu2004部署運行openpcdet記錄

前言 環境 cuda 11.3 python 3.8 ubuntu2004 一、cuda環境檢測 ylhy:~/code_ws/OpenPCDet/tools$ nvcc -V nvcc: NVIDIA (R) Cuda compiler driver Copyright (c) 2005-2021 NVIDIA Corporation Built on Sun_Mar_21_19:15:46_PDT_2021 Cuda compilation tools, release 11.3…

在 Linux 系統中過濾文件中的字符串

在 Linux 系統中過濾文件中的字符串,可以使用多種命令行工具實現。以下是幾種常見方法及詳細說明: 一、使用 grep 命令(最常用) grep 是 Linux 中最強大的文本搜索工具,支持正則表達式。 基礎語法: grep…

基于PXIE 總線架構的Kintex UltraScale 系列FPGA 高性能數據預處理板卡

基于PXIE 總線架構的Kintex UltraScale 系列FPGA 高性能數據預處理板卡 一款基于3U PXIE 總線架構的高性能數據預處理FMC 載板,板卡具有1 個FMC(HPC)接口,1 個X8 GTH 背板互聯接口,可以實現1 路PCIe x8。板卡采用Xili…

Java 使用 PDFBox 提取 PDF 文本并統計關鍵詞出現次數(附Demo)

目錄 前言1. 基本知識2. 在線URL2.1 英文2.2 混合 3. 實戰 前言 爬蟲神器,無代碼爬取,就來:bright.cn Java基本知識: java框架 零基礎從入門到精通的學習路線 附開源項目面經等(超全)【Java項目】實戰CRUD…

Vue百日學習計劃Day16-18天詳細計劃-Gemini版

重要提示: 番茄時鐘: 每個番茄鐘為25分鐘學習,之后休息5分鐘。每完成4個番茄鐘,進行一次15-30分鐘的長休息。動手實踐: DOM 操作和事件處理的理解高度依賴于實際編碼。請務必在瀏覽器中創建 HTML 頁面,并配…

SearchClassUtil

路徑掃描工具SearchClassUtil,用于掃描指定包(XXXX)下的所有.class文件,并將它們的全限定類名(如tomcat.SearchClassUtil)收集到列表中返回。該工具使用遞歸文件遍歷和反射機制,是實現 Spring 框…

云服務器的運用自如

云服務器的運用自如:從基礎到高階的實戰指南(2025版) 云服務器作為數字化轉型的核心工具,其靈活性和高效性已覆蓋從個人開發者到企業級應用的廣泛場景。以下是基于當前技術趨勢的云服務器深度運用策略,涵蓋核心應用、…

解密企業級大模型智能體Agentic AI 關鍵技術:MCP、A2A、Reasoning LLMs-docker MCP解析

解密企業級大模型智能體Agentic AI 關鍵技術:MCP、A2A、Reasoning LLMs-docker MCP解析 這里面有很重要的原因其中一個很其中一個原因是因為如果你使用docker的方式,你可以在虛擬環境下就類似于這個沙箱的這個機制可以進行隔離。這對于安全,…

快慢指針算法(Floyd 判圈算法)

快慢指針(又稱龜兔賽跑算法)是一種常用的鏈表操作技巧,通過兩個移動速度不同的指針遍歷鏈表,用于解決鏈表中環檢測、中點查找等問題。以下是其核心應用場景和實現方法: 1. 鏈表環檢測 問題描述: 判斷鏈表中…

獨立開發者利用AI工具快速制作產品MVP

在當今快速發展的科技時代,獨立開發者面臨著前所未有的機遇與挑戰。曾經需要花費數天甚至數周才能完成的產品MVP(Minimum Viable Product,最小可行性產品),如今借助強大的AI工具,可以在短短1小時內實現。 …

Spark處理過程-轉換算子和行動算子

(一)RDD的處理過程 RDD經過一系列的“轉換”操作,每一次轉換都會產生不同的RDD,以供給下一次“轉換”操作使 用,直到最后一個RDD經過“行動”操作才會真正被計算處理。 1.延遲。RDD中所有的轉換都是延遲的&…

設置環境變量啟動jar報

1. 環境變量設置 set PATHC:\Program Files\java17\jdk-17.0.9\bin;%PATH%2. 啟動jar java -jar jar包名3. 記錄原因 PATH路徑前添加java執行文件路徑才會管用。添加后可以試試以下命令 直接輸入PATH 回車 PATH進行java版本測試 java -version

589. N叉樹的前序遍歷迭代法:null指針與棧的巧妙配合

一、題目描述 給定一個N叉樹的根節點,返回其節點值的前序遍歷結果。前序遍歷的定義是:先訪問根節點,再依次遍歷每個子節點(從左到右)。例如,對于如下N叉樹: 1/ | \3 2 4 / \ 5 6前序遍歷結果…

顯性知識的主要特征

有4個主要特征: 客觀存在性靜態存在性可共享性認知元能性