軟硬鏈接
介紹
軟鏈接
通過下圖可以看出軟鏈接和原始文件是兩個獨立的文件,因為軟鏈接有著自己的inode編號:
具有獨立的 inode ,也有獨立的數據塊,它的數據塊里面保存的是指向的文件的路徑,公用 inode
硬鏈接
通過下圖可以看出硬鏈接和原始文件是同一個文件,因為二者的inode
編號是相同的,并且創建完硬鏈接后改變了原始文件的引用計數:
觀察 inode 編號可以發現,軟硬鏈接的區別:是否具有獨立的Inode。
軟鏈接具有獨立的Inode:可以被當作獨立的文件看待。
如果想刪除一個軟鏈接或者硬鏈接,可以使用刪除命令rm
,也可以使用unlink
命令,例如刪除上面的硬鏈接:
軟硬鏈接的使用場景?
如果對一個文件既創建了軟鏈接,也創建了一個硬鏈接,那么刪除原文件時,軟鏈接將失效,但是硬鏈接不會:
此時再訪問軟鏈接指向的文件中的內容就會無效,但不會影響硬鏈接
因為對于存在硬鏈接的文件來說,刪除原文件就是減少其引用計數,只要引用計數不為0,那么該文件就不會被認為失效,而創建硬鏈接會增加原文件的引用計數,所以此時刪除原文件就只是讓原文件的引用計數從2變為1,從而保證文件還在硬盤上存在
從上面刪除文件的例子可以看出,使用硬鏈接可以做到對原文件的備份
我們新建一個目錄,引用計數是2,新建一個普通文件,引用計數為1
empty目錄里面有兩個隱藏文件,其中一個是 .? 表示當前路徑,文件名不同,inode相同
在empty里面再新建一個目錄dir,引用計數變為3,新建目錄dir里面有隱藏文件 ..?表示上級路徑
和empty的inode相同,相當于硬鏈接
linux不允許對目錄新建硬鏈接(會形成環狀路徑)
下面看一下軟鏈接的應用場景:
軟鏈接就像windows下的快捷方式,路徑直接跳轉
當前目錄下有一個test.cc文件,g++編譯形成可執行程序,直接運行可以看到運行結果
但是上面的運行需要帶./
限定才能正常運行a.out
文件,在linux命令行與環境變量提到之所以需要./
作為限定是因為Linux默認查找可執行文件的路徑是/bin
路徑下,而當前a.out
文件并不在該目錄。當時解決這個問題的辦法就是將a.out
移動到/bin
路徑下
實際上,
/bin
也是一個軟鏈接,該鏈接指向的原文件是/usr/bin
在軟鏈接部分,就可以通過為a.out
創建軟鏈接,再將軟鏈接移動到/bin
路徑下即可執行a.out
文件:此時的軟鏈接指向的原文件需要使用絕對路徑
這時候直接寫a.out不用帶./就可以運行test.txt程序了
通過上面的例子可以看出,軟鏈接的作用主要是相當于一個快捷方式?,軟鏈接里面保存的是與文件所處路徑的映射關系
動靜態庫
創建靜態庫與使用
我們自己封裝了兩個簡單的庫,stdio,string庫,為了將我們的庫給別人使用,并且不暴露源碼,我們可以將這兩個庫制作成靜態庫
制作靜態庫的兩種方式
方式一:將靜態庫放到/usr/lib64
目錄下,將頭文件放到/usr/include
目錄下 (安裝到系統里)
制作步驟如下:
1.將需要打包為靜態庫的.c文件編譯生成.o文件
2.使用ar -rc lib庫名.a 指定的.o文件生成靜態庫,注意靜態庫的名稱一定要以lib開頭,后綴為.a
3.將頭文件使用cp命令拷貝到/usr/include目錄下,使用cp命令將靜態庫拷貝到/usr/lib64目錄下。
這一步僅僅只是完成靜態庫的安裝,如果直接編譯要生成可執行程序的文件會出現鏈接報錯
4.編譯要生成可執行程序的文件時使用gcc 文件名 -l+庫名稱(去掉lib和.a)
方法二:?通過編譯器選項指定靜態庫路徑,并且使用當前目錄下的頭文件
默認情況下,gcc不會在當前目錄查找需要的靜態庫,也不會在指定目錄中自動查找需要的靜態庫,所以需要指定靜態庫路徑和靜態庫名稱
當前目錄下存在靜態庫、頭文件和用于生成可執行程序的源文件:?
使用下面的指令指定靜態庫所在位置:
gcc 文件名 -L路徑 -l靜態庫(去掉lib和.a)
?方式3:通過編譯器選項指定頭文件和靜態庫文件的位置
使用Makefile
自動化生成靜態庫和.o
文件
使用下面的指令指定頭文件所在路徑和靜態庫所在路徑
gcc 文件名 -I.h所在位置 -L靜態庫路徑 -l靜態庫名稱(去掉lib和.a)
運行結果如下:?
創建動態庫與使用?
創建動態庫的命令不再是ar
而是直接使用gcc
,但是在生成動態庫前必須保證.o
文件具有絕對地址,即對.o
文件的生成方式需要改變:使用-fPIC
選項生成帶有與位置無關碼的.o
文件,即:
運行結果如下
動態庫前兩種創建方式與靜態庫一樣
第三種創建方式與靜態庫不同
?gcc編譯動態庫時,并沒有把代碼加載到程序里,靜態庫加載到程序里面了,直接運行即可。
動態庫沒有加載到程序里,運行時找不到 libmystdio ,系統找動態庫默認從lib64找
如何給系統指定路徑,查找自己的動態庫:
1.拷貝到系統默認路徑下(與靜態庫使用第一種方法相同)
2.在系統路徑,建立軟鏈接
3.linux系統中,OS查找動態庫,環境變量,LD_LIBRARY_PATH
4.?ldconfig?案:配置/ etc/ld.so.conf.d/ ,ldconfig更新? (系統級別)
動靜態庫同時使用的細節
1.同時存在動靜態庫時,gcc/g++默認使用動態庫
如果想使用靜態庫,編譯時應該帶上 -static?
2.如果強制靜態鏈接,必須提供對應的靜態庫
3.如果只提供靜態庫,但是連接方式是動態鏈接的,gcc/g++只能針對.a局部性采用靜態鏈接
動態庫的加載
先探討?下編譯和鏈接的整個過程,來更好的理解動靜態庫的使用原理
ELF的形成與加載
編譯的過程其實就是將我們程序的源代碼翻譯成CPU能夠直接運行的機器代碼。在編譯之后會?成兩個擴展名為 .o 的文件,它們被稱作目標文件
目標文件是?個?進制的文件,文件的格式是 ELF ,是對?進制代碼的?種封裝
ELF文件
以下四種都是ELF文件:
1.可重定位文件(xxx.o文件) 2.可執行程序? 3.共享目標文件(.so文件)? 4.內核轉儲(core dumps)
ELF文件由以下四部分組成:
ELF頭(ELF header) :描述?件的主要特性。其位于?件的開始位置,它的主要?的是定位文件的其他部分。
? 程序頭表(Program header table) :列舉了所有有效的段(segments)和他們的屬性。表?
記著每個段的開始的位置和位移(offset)、?度,畢竟這些段,都是緊密的放在?進制?件中,需要段表的描述信息,才能把他們每個段分割開。
? 節頭表(Section header table) :包含對節(sections)的描述。
? 節(Section ):ELF?件中的基本組成單位,包含了特定類型的數據。ELF?件的各種信息和數據都存儲在不同的節中,如代碼節存儲了可執?代碼,數據節存儲了全局變量和靜態數據等
最常見的節:
代碼節(.text) : 用于保存機器指令,是程序的主要執行部分
數據節(.data) :保存已初始化的全局變量和局部靜態變量
鏈接就是將一個一個的相同屬性的section合并
對任何一個文件,文件的內容就是一個巨大的“一維數組”,標識文件任何一個區域,用偏移量+大小的方式
動態庫的加載
使用動態庫的可執行程序在調用動態庫中的方法時需要知道動態庫的地址,動態庫還未加載到內存之前,先使用一些內容進行占位,等到執行到指定的動態庫代碼再加載動態庫,此時就形成了動態庫的虛擬地址和物理地址映射關系,根據這個虛擬地址替換掉進程中調用動態庫代碼的占位內容即可。這個過程也被稱為地址重定位
看似上面的思路好像沒問題,實際上,虛擬地址空間的代碼區是不可寫的,也就是說,如果進程的代碼加載到虛擬地址空間就不無法再更改其中的內容,那么此時又是如何做到使用動態庫加載到內存之后的虛擬地址替換進程調用動態庫代碼的位置的內容
其實,進程調用動態庫代碼的位置的內容并不是直接寫動態庫的地址,而是寫入一個GOT表的地址,這個表中存儲的就是指定動態庫和對應的虛擬地址的映射關系,進程在調用動態庫代碼的位置此時只需要寫上調用的是GOT表中的哪一個動態庫的下標即可,剩下的就交給GOT表來進行,即當動態庫加載到內存后,虛擬地址填充到GOT表指定動態庫對應下標即可。這也就是所謂的「生成與位置無關碼」
所以,一個動態庫之所以可以只加載一次而可以被任何進程所調用,本質就是因為這個GOT表,只需要知道這個GOT表的地址和對應庫的下標,即可調用對應動態庫中的內容
重談地址空間--可執行程序,加載問題
可執行程序是有地址的
CPU要執行進程中的代碼,就需要知道對應代碼的地址,所以在磁盤的可執行程序中,盡管其未加載到內存,但是在編譯鏈接時就已經形成了地址,使用下面的指令對main
程序進行反匯編可以看到每一個步驟對應的虛擬地址
注意,不是物理地址,因為此時可執行程序還沒有被加載到內存,只有被加載到內存后,才有物理地址。程序在加載到內存之前只有虛擬地址或邏輯地址,只有在加載到物理內存后才會被分配對應的物理地址。
objdump -S指令顯示目標文件的詳細信息
平坦模式:邏輯地址=起始地址+偏移量?
平坦模式(Flat Mode)是指在計算機系統中的一種內存管理模式,其中整個地址空間被看作是單一的、連續的線性空間。在這種模式下,所有代碼和數據都位于一個大的、平坦的地址范圍內,沒有分段或分區的概念。這種模式簡化了編程模型,使得編譯器和程序員不需要處理復雜的段選擇符或偏移量計算
ELF在沒有加載到內存的時候就已經按照[000...000,FFF...FFF](虛擬地址)進行編址了?
?結論:編譯器編譯,就已經形成虛擬地址了
當可執行程序加載到內存之后,其ELF中的LOAD部分的內容就會分別被加載到指定的區域,例如.text的內容被加載到代碼區、.data的內容被加載到數據區等,這個過程就完成了虛擬地址空間的初始化
但是只有初始化還不夠,為了保證物理地址和可執行程序的虛擬地址可以匹配,此時就需要頁表進行對應的映射
上面整個過程完成,一個可執行程序就從硬盤加載到內存,變為了一個可以被CPU調度的進程
接著,CPU要執行這個進程,PC寄存器就需要找到第一條指令的地址(即找到入口地址),這個地址在ELF頭中可以看到Entry point address字段:
但是這個地址依舊是虛擬地址,所以依舊需要使用頁表進行映射,對應著的就是反匯編代碼中的<_start>地址(此處<_start>相當于關于C語言函數棧幀:main函數被其他函數調用的__tmainCRTStartup())
所以,不論是進程還是CPU的PC寄存器,二者訪問到的都是虛擬地址,但是這個虛擬地址要和物理地址在頁表中建立映射關系。同時CPU內部還有一個寄存器,稱為CR3寄存器,其中存儲的就是頁表本身的物理地址,這個寄存器是操作系統本身使用的。有了CR3寄存器后,就需要一個硬件配合其完成查表的工作,這個硬件稱為MMU,也是在CPU內部。還有一個寄存器EIP,將pc指針中的虛擬地址,通過MMU查表轉化為物理地址
虛擬空間是操作系統,CPU,編譯器協作下的產物,通過上面的過程,再次思考為什么需要有虛擬地址和虛擬地址空間:編譯器在編譯代碼時不再需要考慮物理內存,完成操作系統和編譯器進行解耦合。
理解虛擬地址空間的區域劃分
前面提到,虛擬地址空間初始化時會由ELF文件中的內容對指定區域進行初始化,但是并沒有看到ELF文件中存在對棧、堆和共享區進行初始化的部分,這些部分如何進行的初始化就是下面需要討論的問題
實際上,虛擬地址空間中還存在一個結構,稱為vm_area_struct,即虛擬區域結構,其對應的部分源碼如下:
struct vm_area_struct {struct mm_struct * vm_mm; /* The address space we belong to. */unsigned long vm_start; /* Our start address within vm_mm. */unsigned long vm_end; /* The first byte after our end addresswithin vm_mm. *//* linked list of VM areas per task, sorted by address */struct vm_area_struct *vm_next;
};
真正的棧、堆和共享區都是vm_area_struct結構對象,有著自己的vm_start和vm_end用于標記區域的開始和結束,每一個vm_area_struct結構對象通過鏈表進行鏈接。所以,CPU在訪問棧、堆和共享區時實際上訪問的也是對應的vm_area_struct對象的虛擬地址,在頁表中也存在著這些虛擬地址和物理地址的映射
有了上面這種思想,當一個可執行程序有很多內容時,操作系統可以考慮先加載一部分的Section形成vm_area_struct對象,再根據需要加載后面的Section,這也就實現了Section的懶加載
所以,如果有多個動態庫需要加載,本質上就是創建一個vm_area_struct結構對象鏈接到指定的區域
?