前言
本篇博客我們來認識下庫方面的知識
💓 個人主頁:zkf
? 文章專欄:Linux
若有問題 評論區見📝
🎉歡迎大家點贊👍收藏?文章
目錄
1.什么是庫
2.靜態庫
2.1靜態庫的生成
2.2靜態庫的使用
3.動態庫
3.1動態庫?成
3.2動態庫的使用
3.3庫運?搜索路徑
4.目標文件
5.ELF文件
6.ELF從形成到加載輪廓
6.1ELF形成可執行文件
6.2ELF可執??件加載
7.理解連接與加載
7.1靜態鏈接
7.2ELF加載與進程地址空間
7.2.1虛擬地址/邏輯地址
7.2.2重新理解進程虛擬地址空間
7.3動態鏈接與動態庫加載
7.3.1進程如何看到動態庫
7.3.2進程間如何共享庫的
7.3.3動態鏈接
7.3.4全局偏移量表GOT(global offset table)
8.總結
1.什么是庫
庫是寫好的現有的,成熟的,可以復?的代碼。現實中每個程序都要依賴很多基礎的底層庫,不可能每個?的代碼都從零開始,因此庫的存在意義?同尋常。
本質上來說庫是?種可執?代碼的?進制形式,可以被操作系統載?內存執?。庫有兩種:
靜態庫 .a[Linux]、.lib[windows]
動態庫 .so[Linux]、.dll[windows]
2.靜態庫
靜態庫(.a):程序在編譯鏈接的時候把庫的代碼鏈接到可執??件中,程序運?的時候將不再
需要靜態庫。
?個可執?程序可能?到許多的庫,這些庫運?有的是靜態庫,有的是動態庫,?我們的編譯默
認為動態鏈接庫,只有在該庫下找不到動態.so的時候才會采?同名靜態庫。我們也可以使? gcc
的 -static 強轉設置鏈接靜態庫。
2.1靜態庫的生成
// Makefilelibmystdio.a:my_stdio.o my_string.o@ar -rc $@ $^@echo "build $^ to $@ ... done"%.o:%.c@gcc -c $<@echo "compling $< to $@ ... done".PHONY:cleanclean:@rm -rf *.a *.o stdc*@echo "clean ... done".PHONY:outputoutput:@mkdir -p stdc/include@mkdir -p stdc/lib@cp -f *.h stdc/include@cp -f *.a stdc/lib@tar -czf stdc.tgz stdc@echo "output stdc ... done"
ar 是 gnu 歸檔?具, rc 表? (replace and create)
$ ar -tv libmystdio.arw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.orw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o
t: 列出靜態庫中的?件
v:verbose 詳細信息
2.2靜態庫的使用
// 任意?錄下,新建// main.c,引?庫頭?件#include "my_stdio.h"#include "my_string.h"#include <stdio.h>int main(){const char *s = "abcdefg";printf("%s: %d\n", s, my_strlen(s));mFILE *fp = mfopen("./log.txt", "a");if(fp == NULL) return 1;mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfwrite(s, my_strlen(s), fp);mfclose(fp);return 0;}// 場景1:頭?件和庫?件安裝到系統路徑下$ gcc main.c -lmystdio// 場景2:頭?件和庫?件和我們??的源?件在同?個路徑下$ gcc main.c -L. -lmymath// 場景3:頭?件和庫?件有??的獨?路徑$ gcc main.c -I頭?件路徑 -L庫?件路徑 -lmymath
-L: 指定庫路徑
-I: 指定頭?件搜索路徑
-l: 指定庫名
測試?標?件?成后,靜態庫刪掉,程序照樣可以運?
關于 -static 選項,稍后介紹
庫?件名稱和引?庫的名稱:去掉前綴 lib ,去掉后綴 .so , .a ,如: libc.so -> c
3.動態庫
動態庫(.so):程序在運?的時候才去鏈接動態庫的代碼,多個程序共享使?庫的代碼。
?個與動態庫鏈接的可執??件僅僅包含它?到的函數??地址的?個表,?不是外部函數所在?
標?件的整個機器碼
在可執??件開始運?以前,外部函數的機器碼由操作系統從磁盤上的該動態庫中復制到內存中,
這個過程稱為動態鏈接(dynamic linking)
動態庫可以在多個程序間共享,所以動態鏈接使得可執??件更?,節省了磁盤空間。操作系統采
?虛擬內存機制允許物理內存中的?份動態庫被要?到該庫的所有進程共?,節省了內存和磁盤空
間。
3.1動態庫?成
// Makefilelibmystdio.so:my_stdio.o my_string.ogcc -o $@ $^ -shared%.o:%.cgcc -fPIC -c $<.PHONY:cleanclean:@rm -rf *.so *.o stdc*@echo "clean ... done".PHONY:outputoutput:@mkdir -p stdc/include@mkdir -p stdc/lib@cp -f *.h stdc/include@cp -f *.so stdc/lib@tar -czf stdc.tgz stdc@echo "output stdc ... done"
shared: 表??成共享庫格式
fPIC:產?位置?關碼(position independent code) ?
庫名規則:libxxx.so
3.2動態庫的使用
// 場景1:頭?件和庫?件安裝到系統路徑下$ gcc main.c -lmystdio// 場景2:頭?件和庫?件和我們??的源?件在同?個路徑下$ gcc main.c -L. -lmymath // 從左到右搜索-L指定的?錄// 場景3:頭?件和庫?件有??的獨?路徑$ gcc main.c -I頭?件路徑 -L庫?件路徑 -lmymath$ ldd libmystdio.so // 查看庫或者可執?程序的依賴linux-vdso.so.1 => (0x00007fffacbbf000)libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000)/lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)
3.3庫運?搜索路徑
問題
$ ldd a.outlinux-vdso.so.1 => (0x00007fff4d396000)libmystdio.so => not foundlibc.so.6 => /lib64/libc.so.6 (0x00007fa2aef30000)/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)
為什么此刻mystdio動態庫找不到
解決方法
拷? .so ?件到系統共享庫路徑下, ?般指 /usr/lib、/usr/local/lib、/lib64 或者開
篇指明的庫路徑等
向系統共享庫路徑下建?同名軟連接
更改環境變量: LD_LIBRARY_PATH
4.目標文件
編譯和鏈接這兩個步驟,在Windows下被我們的IDE封裝的很完美,我們?般都是?鍵構建?常?便, 但?旦遇到錯誤的時候呢,尤其是鏈接相關的錯誤,很多?就束??策了。在Linux下,我們之前也了解過如何通過gcc編譯器來完成這?系列操作。

接下來我們深?探討?下編譯和鏈接的整個過程,來更好的理解動靜態庫的使?原理。
先來回顧下什么是編譯呢?編譯的過程其實就是將我們程序的源代碼翻譯成CPU能夠直接運?的機器
代碼。
?如:在?個源?件 hello.c ?便簡單輸出"hello world!",并且調??個run函數,?這個函數被
定義在另?個原?件 code.c 中。這?我們就可以調? gcc -c 來分別編譯這兩個原?件。
在編譯之后會?成兩個擴展名為 .o 的?件,它們被稱作?標?件。要注意的是如果我們
修改了?個原?件,那么只需要單獨編譯它這?個,?不需要浪費時間重新編譯整個?程。?標?件是?個?進制的?件,?件的格式是 ELF ,是對?進制代碼的?種封裝。
5.ELF文件
要理解編譯鏈鏈接的細節,我們不得不了解?下ELF?件。其實有以下四種?件其實都是ELF?件:
可重定位?件(Relocatable File) :即 xxx.o ?件。包含適合于與其他?標?件鏈接來創
建可執??件或者共享?標?件的代碼和數據。
可執??件(Executable File) :即可執?程序。
共享目標?件(Shared Object File) :即 xxx.so?件。
內核轉儲(core dumps) ,存放當前進程的執?上下?,?于dump信號觸發。
?個ELF?件由以下四部分組成:
ELF頭(ELF header) :描述?件的主要特性。其位于?件的開始位置,它的主要?的是定位?
件的其他部分。
程序頭表(Program header table) :列舉了所有有效的段(segments)和他們的屬性。表?
記著每個段的開始的位置和位移(offset)、?度,畢竟這些段,都是緊密的放在?進制?件中,
需要段表的描述信息,才能把他們每個段分割開。
節頭表(Section header table) :包含對節(sections)的描述。
節(Section ):ELF?件中的基本組成單位,包含了特定類型的數據。ELF?件的各種信息和
數據都存儲在不同的節中,如代碼節存儲了可執?代碼,數據節存儲了全局變量和靜態數據等。
最常?的節:
代碼節(.text):?于保存機器指令,是程序的主要執?部分。
數據節(.data):保存已初始化的全局變量和局部靜態變量。

6.ELF從形成到加載輪廓
6.1ELF形成可執行文件
step-1:將多份 C/C++ 源代碼,翻譯成為?標 .o ?件 + 動靜態庫(ELF)
step-2:將多份 .o ?件section進?合并

📌 注意:實際合并是在鏈接時進?的,但是并不是這么簡單的合并,也會涉及對庫合并,此處不做過多追究
6.2ELF可執??件加載
?個ELF會有多種不同的Section,在加載到內存的時候,也會進?Section合并,形成segment
合并原則:相同屬性,?如:可讀,可寫,可執?,需要加載時申請空間等.
這樣,即便是不同的Section,在加載到內存中,可能會以segment的形式,加載到?起
很顯然,這個合并?作也已經在形成 ELF 的時候,合并?式已經確定了,具體合并原則被記錄在
了 ELF 的 程序頭表(Program header table) 中
📌 為什么要將section合并成為segmentSection合并的主要原因是為了減少??碎?,提?內存使?效率。如果不進?合并,假設????為4096字節(內存塊基本??,加載,管理的基本單位),如果.text部分為4097字節,.init部分為512字節,那么它們將占?3個??,?合并后,它們只需2個??。此外,操作系統在加載程序時,會將具有相同屬性的section合并成?個?的segment,這樣就可以實現不同的訪問權限,從?優化內存管理和權限訪問控制。
對于 程序頭表 和 節頭表 ?有什么?呢,其實 ELF ?件提供 2 個不同的視圖/視?來讓我們理解這
兩個部分:
鏈接視圖(Linking view) - 對應節頭表 Section header table
?件結構的粒度更細,將?件按功能模塊的差異進?劃分,靜態鏈接分析的時候?般關注的
是鏈接視圖,能夠理解 ELF ?件中包含的各個部分的信息。
為了空間布局上的效率,將來在鏈接?標?件時,鏈接器會把很多節(section)合并,規整
成可執?的段(segment)、可讀寫的段、只讀段等。合并了后,空間利?率就?了,否
則,很?的很?的?段,未來物理內存?浪費太?(物理內存?分配?般都是整數倍?塊給
你,?如4k),所以,鏈接器趁著鏈接就把?塊們都合并了。
執?視圖(execution view) - 對應程序頭表 Program header table
告訴操作系統,如何加載可執??件,完成進程內存的初始化。?個可執?程序的格式中,
?定有 program header table 。
說?了就是:?個在鏈接時作?,?個在運?加載時作?。

從 鏈接視圖 來看:
命令 readelf -S hello.o 可以幫助查看ELF?件的 節頭表。
.text節 :是保存了程序代碼指令的代碼節。
.data節 :保存了初始化的全局變量和局部靜態變量等數據。
.rodata節 :保存了只讀的數據,如??C語?代碼中的字符串。由于.rodata節是只讀的,所
以只能存在于?個可執??件的只讀段中。因此,只能是在text段(不是data段)中找到.rodata
節。
.BSS節 :為未初始化的全局變量和局部靜態變量預留位置
.symtab節 : Symbol Table 符號表,就是源碼??那些函數名、變量名和代碼的對應關系。
.got.plt節 (全局偏移表-過程鏈接表):.got節保存了全局偏移表。.got節和.plt節?起提供
了對導?的共享庫函數的訪問??,由動態鏈接器在運?時進?修改。對于GOT的理解,我們后
?會說。
使? readelf 命令查看 .so ?件可以看到該節。
從 執?視圖 來看:
告訴操作系統哪些模塊可以被加載進內存。
加載進內存之后哪些分段是可讀可寫,哪些分段是只讀,哪些分段是可執?的
對于 ELF HEADER 這部分來說,我們只?知道其作?即可,它的主要?的是定位?件的其他部分。
7.理解連接與加載
7.1靜態鏈接
?論是??的 .o , 還是靜態庫中的 .o ,本質都是把.o?件進?連接的過程
所以:研究靜態鏈接,本質就是研究 .o 是如何鏈接的
靜態鏈接就是把庫中的.o進?合并,和上述過程?樣
所以鏈接其實就是將編譯之后的所有?標?件連同?到的?些靜態庫運?時庫組合,拼裝成?個獨?的可執??件。其中就包括我們之前提到的地址修正,當所有模塊組合在?起之后,鏈接器會根據我們的.o?件或者靜態庫中的重定位表找到那些需要被重定位的函數全局變量,從?修正它們的地址。這其實就是靜態鏈接的過程。

所以,鏈接過程中會涉及到對.o中外部符號進?地址重定位。
7.2ELF加載與進程地址空間
7.2.1虛擬地址/邏輯地址
?個ELF程序,在沒有被加載到內存的時候,有沒有地址呢?
進程mm_struct、vm_area_struct在進程剛剛創建的時候,初始化數據從哪?來的?
答案:
?個ELF程序,在沒有被加載到內存的時候,本來就有地址,當代計算機?作的時候,都采?"平坦
模式"進??作。所以也要求ELF對??的代碼和數據進?統?編址,下?是 objdump -S 反匯編
之后的代碼

最左側的就是ELF的虛擬地址,其實,嚴格意義上應該叫做邏輯地址(起始地址+偏移量), 但是我們
認為起始地址是0.也就是說,其實虛擬地址在我們的程序還沒有加載到內存的時候,就已經把可執
?程序進?統?編址了.
進程mm_struct、vm_area_struct在進程剛剛創建的時候,初始化數據從哪?來的?從ELF各個
segment來,每個segment有??的起始地址和??的?度,?來初始化內核結構中的[start, end]
等范圍數據,另外在?詳細地址,填充?表.
所以:虛擬地址機制,不光光OS要?持,編譯器也要?持.
7.2.2重新理解進程虛擬地址空間
ELF 在被編譯好之后,會把??未來程序的??地址記錄在ELF header的Entry字段中:

7.3動態鏈接與動態庫加載
7.3.1進程如何看到動態庫
7.3.2進程間如何共享庫的
7.3.3動態鏈接
動態鏈接其實遠?靜態鏈接要常?得多。?如我們查看下 hello 這個可執?程序依賴的動態庫,會發現它就?到了?個c動態鏈接庫:
ldd main.exelinux-vdso.so.1 => (0x00007ffefd43f000)libc.so.6 => /lib64/libc.so.6 (0x00007f533380b000)/lib64/ld-linux-x86-64.so.2 (0x00007f5333bd9000)
這?的 libc.so 是C語?的運?時庫,??提供了常?的標準輸?輸出?件字符串處理等等這些功
能。
那為什么編譯器默認不使?靜態鏈接呢?靜態鏈接會將編譯產?的所有?標?件,連同?到的各種
庫,合并形成?個獨?的可執??件,它不需要額外的依賴就可以運?。照理來說應該更加?便才對是吧?
靜態鏈接最?的問題在于?成的?件體積?,并且相當耗費內存資源。隨著軟件復雜度的提升,我們的操作系統也越來越臃腫,不同的軟件就有可能都包含了相同的功能和代碼,顯然會浪費?量的硬盤空間。
這個時候,動態鏈接的優勢就體現出來了,我們可以將需要共享的代碼單獨提取出來,保存成?個獨?的動態鏈接庫,等到程序運?的時候再將它們加載到內存,這樣不但可以節省空間,因為同?個模塊在內存中只需要保留?份副本,可以被不同的進程所共享。
動態鏈接到底是如何?作的??
?先要交代?個結論,動態鏈接實際上將鏈接的整個過程推遲到了程序加載的時候。?如我們去運??個程序,操作系統會?先將程序的數據代碼連同它?到的?系列動態庫先加載到內存,其中每個動態庫的加載地址都是不固定的,操作系統會根據當前地址空間的使?情況為它們動態分配?段內存。 當動態庫被加載到內存以后,?旦它的內存地址被確定,我們就可以去修正動態庫中的那些函數跳轉地址了。
我們的可執?程序被編譯器動了?腳
在C/C++程序中,當程序開始執?時,它?先并不會直接跳轉到 main 函數。實際上,程序的??點是 _start ,這是?個由C運?時庫(通常是glibc)或鏈接器(如ld)提供的特殊函數。
在 _start 函數中,會執??系列初始化操作,這些操作包括:
1. 設置堆棧:為程序創建?個初始的堆棧環境。
2. 初始化數據段:將程序的數據段(如全局變量和靜態變量)從初始化數據段復制到相應的內存位
置,并清零未初始化的數據段。
3. 動態鏈接:這是關鍵的?步, _start 函數會調?動態鏈接器的代碼來解析和加載程序所依賴的
動態庫(shared libraries)。動態鏈接器會處理所有的符號解析和重定位,確保程序中的函數調
?和變量訪問能夠正確地映射到動態庫中的實際地址。
動態鏈接器:
動態鏈接器(如ld-linux.so)負責在程序運?時加載動態庫。
當程序啟動時,動態鏈接器會解析程序中的動態庫依賴,并加載這些庫到內存中。
環境變量和配置?件:
Linux系統通過環境變量(如LD_LIBRARY_PATH)和配置?件(如/etc/ld.so.conf及其?配置
?件)來指定動態庫的搜索路徑。
這些路徑會被動態鏈接器在加載動態庫時搜索。
緩存?件:
為了提?動態庫的加載效率,Linux系統會維護?個名為/etc/ld.so.cache的緩存?件。
該?件包含了系統中所有已知動態庫的路徑和相關信息,動態鏈接器在加載動態庫時會?先
搜索這個緩存?件。
4. 調? __libc_start_main :?旦動態鏈接完成, _start 函數會調?
__libc_start_main (這是glibc提供的?個函數)。 __libc_start_main 函數負責執?
?些額外的初始化?作,?如設置信號處理函數、初始化線程庫(如果使?了線程)等。
5. 調? main 函數:最后, __libc_start_main 函數會調?程序的 main 函數,此時程序的執
?控制權才正式交給??編寫的代碼。
6. 處理 main 函數的返回值:當 main 函數返回時, __libc_start_main 會負責處理這個返回
值,并最終調? _exit 函數來終?程序。
上述過程描述了C/C++程序在 main 函數之前執?的?系列操作,但這些操作對于?多數程序員來說是透明的。程序員通常只需要關注 main 函數中的代碼,?不需要關?底層的初始化過程。然?,了解這些底層細節有助于更好地理解程序的執?流程和調試問題
動態庫為了隨時進?加載,為了?持并映射到任意進程的任意位置,對動態庫中的?法,統?編址, 采?相對編址的?案進?編制的(其實可執?程序也?樣,都要遵守平坦模式,只不過exe是直接加載的)。
動態庫也是?個?件,要訪問也是要被先加載,要加載也是要被打開的讓我們的進程找到動態庫的本質:也是?件操作,不過我們訪問庫函數,通過虛擬地址進?跳轉訪問的,所以需要把動態庫映射到進程的地址空間中
庫已經被我們映射到了當前進程的地址空間中庫的虛擬起始地址我們也已經知道了庫中每?個?法的偏移量地址我們也知道所有:訪問庫中任意?法,只需要知道庫的起始虛擬地址+?法偏移量即可定位庫中的?法?且:整個調?過程,是從代碼區跳轉到共享區,調?完畢在返回到代碼區,整個過程完全在進程地址空間中進?的.
7.3.4全局偏移量表GOT(global offset table)
?注意:也就是說,我們的程序運?之前,先把所有庫加載并映射,所有庫的起始虛擬地址都應該提前知道然后對我們加載到內存中的程序的庫函數調?進?地址修改,在內存中?次完成地址設置(這個叫做加載地址重定位)等等,修改的是代碼區?不是說代碼區在進程中是只讀的嗎?怎么修改?能修改嗎?
所以:動態鏈接采?的做法是在 .data (可執?程序或者庫??)中專?預留??區域?來存放函數的跳轉地址,它也被叫做全局偏移表GOT,表中每?項都是本運?模塊要引?的?個全局變量或函數的地址。
因為.data區域是可讀寫的,所以可以?持動態進?修改

1. 由于代碼段只讀,我們不能直接修改代碼段。但有了GOT表,代碼便可以被所有進程共享。但在不同進程的地址空間中,各動態庫的絕對地址、相對位置都不同。反映到GOT表上,就是每個進程的每個動態庫都有獨?的GOT表,所以進程間不能共享GOT表。
2. 在單個.so下,由于GOT表與 .text 的相對位置是固定的,我們完全可以利?CPU的相對尋址來找到GOT表。
3. 在調?函數的時候會?先查表,然后根據表中的地址來進?跳轉,這些地址在動態庫加載的時候會被修改為真正的地址。
4. 這種?式實現的動態鏈接就被叫做 PIC 地址?關代碼 。換句話說,我們的動態庫不需要做任何修改,被加載到任意內存地址都能夠正常運?,并且能夠被所有進程共享,這也是為什么之前我們給編譯器指定-fPIC參數的原因,PIC=相對編址+GOT。
8.總結
靜態鏈接的出現,提?了程序的模塊化?平。對于?個?的項?,不同的?可以獨?地測試和開發
??的模塊。通過靜態鏈接,?成最終的可執??件。
我們知道靜態鏈接會將編譯產?的所有?標?件,和?到的各種庫合并成?個獨?的可執??件,
其中我們會去修正模塊間函數的跳轉地址,也被叫做編譯重定位(也叫做靜態重定位)。
?動態鏈接實際上將鏈接的整個過程推遲到了程序加載的時候。?如我們去運??個程序,操作系
統會?先將程序的數據代碼連同它?到的?系列動態庫先加載到內存,其中每個動態庫的加載地址
都是不固定的,但是?論加載到什么地?,都要映射到進程對應的地址空間,然后通過.GOT?式進?調?(運?重定位,也叫做動態地址重定位)。
結束語
動靜態庫相關知識總結完畢
感謝觀看!!