Linux動靜態(tài)庫的制作與使用
一、靜態(tài)庫
對于我們自己寫的一份源代碼,別人如果想使用,可以有以下兩種做法:第一種就是將我們的源代碼直接給別人拷貝一份,但是如果你覺得自己的代碼寫的非常厲害,不想讓別人知道,或者別人嫌拷貝太麻煩了,那么就需要采用第二種做法;第二種做法就是,將我們自己寫的源代嘛想辦法打包成庫,然后將這個庫和對應的頭文件提供給使用者。注意,頭文件是必不可少的,頭文件就相當于該庫中方法的使用說明書,如果不提供頭文件,別人大概率是不知道該庫是如何使用的。
1.1 靜態(tài)庫的制作
// add.h #pragma once int add(int x, int y);
// add.c
#include "add.h"
int add(int x, int y)
{
return x + y;
}
// sub.h #pragma once int sub(int x, int y);
// sub.c
#include "sub.h"
int sub(int x, int y)
{
return x - y;
}
// mul.h #pragma once int mul(int x, int y);
// mul.c
#include "mul.h"
int mul(int x, int y)
{
return x * y;
}
// div.h #pragma once extern int myerrno; // 聲明一個可以被其它源文件使用的變量 int div(int x, int y);
// div.c
#include "div.h"
int myerrno = 0; // 定義
int div(int x, int y)
{
if(y == 0)
{
myerrno = -1;
return myerrno;
}
return x / y;
}
1.2 靜態(tài)庫的生成
靜態(tài)庫的生成指令:

靜態(tài)庫本質(zhì)上就是對 .o 文件進行打包,所以首先要將 .c 文件編譯成 .o 文件,然后進行打包。ar 是 gnu 歸檔工具,可以使用它來生成一個靜態(tài)庫,它執(zhí)行的工作就是把一個或者多個 .o 文件打包,生成一個 .a 庫文件。-rc 表示 replace and creat,.a 庫文件中如果有待打包的 .o 文件就替換,沒有就創(chuàng)建。
靜態(tài)庫生成示意圖:

1.3 靜態(tài)庫的發(fā)布
發(fā)布庫:


發(fā)布庫就是把 lib 目錄拷貝給別人。
1.4 靜態(tài)庫的使用
首先創(chuàng)建一個 test 目錄,先講上面發(fā)布的 lib 目錄拷貝到 test 目錄中,然后在 test 目錄中創(chuàng)建一個 main.c 進行測試。目錄結(jié)構(gòu)如下圖所示:

// main.c
#include "add.h"
#include "sub.h"
#include "mul.h"
#include "div.h"
#include <stdio.h>
int main()
{
printf("1 + 2 = %d\n", add(1, 2));
printf("1 - 2 = %d\n", sub(1, 2));
printf("1 * 2 = %d\n", mul(1, 2));
printf("1 / 2 = %d\n", div(1, 2));
return 0;
}

編譯 main.c 時報錯,說找不到對應的頭文件。此時就需要再來認識一下包含頭文件的兩種方式了。在使用庫中的頭時,一般用 <> 來包含頭文件,<> 表示到系統(tǒng)指定目錄下去查找頭文件。在使用自己寫的頭文件時,一般使用 "",表示在當前源文件的統(tǒng)計目錄下查找頭文件,找打了就用,沒找到再去系統(tǒng)指定目錄下進行查找,所以對于庫提供的頭文件我們也可以使用 "" 進行包含。但是上面代碼中我們使用的就是 "",并且 add.h 就在 lib/include 目錄下,lib 目錄和 main.c 同處 test 目錄下,為什么會報錯呢?因為用 "" 包含的頭文件,會告訴編譯器在 main.c 的同級目錄下進行查找,也就是在 test 目錄下進行查找,并不會深入到 test 中的 lib 目錄去查找。

解決上面報錯的方法有三種。第一種,將我們發(fā)布的 lib 庫中的頭文件拷貝到系統(tǒng)的指定路徑下;第二種,在代碼中補全路徑,如 #include "/lib/include/add.h";第三種,在執(zhí)行 gcc 指令編譯的時候加上 -I 選項,指定編譯器搜索頭文件的路徑。
第三種解決方案示意圖:

此時編譯仍然沒有成功,但是沒有報頭文件找不到的錯誤了?,F(xiàn)在是鏈接出錯,可以編譯形成 .o 文件,如下圖所示:

鏈接報錯還是因為 gcc 在進行編譯鏈接的時候,只會去默認路徑下查找打包形成的庫文件,不會去我們的 lib/mymathlib 目錄下查找,這樣就導致 gcc 編譯器找不到我們打包的庫 libmymath.a ,最終鏈接時就會報錯。

解決鏈接有兩種方法。方法一:將我們的庫拷貝到系統(tǒng)的指定路徑下,并不能完全解決,還需要指定庫的名稱,下面會講;方法二:在使用 gcc 的時候添加對應的選項。方法二示意圖,如下所示:

其中 -L 選項指定了庫的搜索路徑,-l 選項指定了待搜索的庫的名稱。 為什么在搜索頭文件的時候僅需指定路徑呢?因為在代碼中已經(jīng)寫了頭文件的具體名稱,所以僅需指定頭文件的路徑即可。而一個路徑下可以有多個庫,如果只指定路勁,編譯器還是不知道該去鏈接哪個庫,因此還要在后面使用 -l 選項指定待鏈接的庫的具體名稱,注意:去掉前綴 lib 和 后綴 .a 才是一個庫的名稱,建議 -l 后面緊跟庫的名稱。一般在使用第三方庫的時候,可能不需要帶 -I 或者 -L,但是 -l 指定庫的名稱是一定需要到,因為 gcc 默認只能找到系統(tǒng)調(diào)用和語言層面的庫。
小Tips:在動態(tài)庫和靜態(tài)庫都有的情況下,gcc 默認鏈接動態(tài)庫,如果系統(tǒng)中只提供靜態(tài)庫,gcc 則只能對該庫進行靜態(tài)鏈接。如果有需要,gcc 可以鏈接多個庫。
1.5 靜態(tài)庫的安裝

庫的安裝本質(zhì)上就是把頭文件和庫文件拷貝到系統(tǒng)的特定目錄下。還可以通過在指定目錄下創(chuàng)建軟鏈接的方式,如下圖所示:

小Tips:此時包含頭文件前面應該加上軟鏈接的名字,如:#include <myinc/add.h> 這種形式。
二、動態(tài)庫
2.1 動態(tài)庫的制作
// myprintf.h #pragma once #include <stdio.h> void Print();
// myprintf.c
#include "myprintf.h"
void Print()
{
printf("Hello Linux\n");
}
// mylog.h #pragma once #include <stdio.h> void Log(const char* info);
// mylog.c
#include "mylog.h"
void Log(const char* info)
{
printf("log: %s\n", info);
}
2.2 動態(tài)庫的生成

小Tips:在編譯生成 .o 文件的時候,要加上 -fPIC 選項,該選項表示產(chǎn)生位置無關(guān)碼(position independent code)。將 .o 文件打包生成動態(tài)庫,繼續(xù)使用 gcc,需要帶 -shared 選項,表示生成共享庫格式。其次需要注意動態(tài)庫的命名規(guī)則是 libxxx.so。動態(tài)庫是可執(zhí)行程序的一種,將來是需要被加載到內(nèi)存的,因此它帶了 x 選項,而靜態(tài)庫的使用本質(zhì)是把靜態(tài)庫中的二進制代碼拷貝一份去使用,靜態(tài)庫是不需要被加載到內(nèi)存的,因此靜態(tài)庫沒有可執(zhí)行權(quán)限。
2.3 動態(tài)庫的發(fā)布
dy-lib=libmymethod.so static-lib=libmymath.a .PHONY:all all:$(dy-lib) $(static-lib) $(static-lib):add.o sub.o mul.o div.o ar -rc $@ add.o sub.o mul.o div.o $(dy-lib):myprintf.o mylog.o gcc -shared -o $@ $^ myprintf.o:myprintf.c gcc -fPIC -c $^ mylog.o:mylog.c gcc -fPIC -c $^ add.o:add.c gcc -c $^ sub.0:sub.c gcc -c $^ mul.o:mul.c gcc -c $^ div.o:div.c gcc -c $^ .PHONY:clean clean: rm -rf *.o *.a mylib *.so .PHONY:output output: mkdir -p mylib/include mkdir -p mylib/lib cp *.h mylib/include cp *.a mylib/lib cp *.so mylib/lib

小Tips:上面不是單純的發(fā)布動態(tài)庫,而是將動靜態(tài)庫同時發(fā)布。
2.4 動態(tài)庫的使用
#include "myprintf.h"
#include "mylog.h"
#include <stdio.h>
int main()
{
Print();
Log("Hello log function!");
return 0;
}

上圖中按照靜態(tài)庫的使用方法去使用動態(tài)庫,可以成功生成可執(zhí)行文件,但是可執(zhí)行文件在運行的時候出錯了。

在使用 ldd 查看可執(zhí)行程序運行所需的共享庫時發(fā)現(xiàn),libmymethod.so 后面指向 not found。為什么會這樣呢?我們在使用 gcc 進行編譯的時候,不是已經(jīng)通過 -L 和 -l 選項告訴編譯器動態(tài)庫所在的路徑和名字,為什么還是找不到呢?原因正如前面所述,我們僅僅是告訴了編譯器所需的動態(tài)庫在哪里,而可執(zhí)行程序運行靠的是加載器,上面的 not found 表示加載器不知道動態(tài)庫在哪里。這也從側(cè)面印證了靜態(tài)庫是不會加載到內(nèi)存中的,所以使用靜態(tài)庫只需要告訴編譯器靜態(tài)庫在哪里即可。
解決該問題的方法有四種。第一種,將庫文件拷貝到系統(tǒng)默認的庫路徑(/lib64、/usr/lib64);第二種,在系統(tǒng)默認的庫路徑(/lib64、/usr/lib64)下建立軟鏈接;第三種,將自己庫所在的路徑,添加到系統(tǒng)的環(huán)境變量 LD_LIBRARY_PATH 中,該環(huán)境變量就是專門用來搜索動態(tài)庫的;第四種,如果想讓我們的庫和系統(tǒng)、語言自帶的庫一樣,在程序運行的時候可以自動被找到,那我們可以在 /etc/ld.so.conf.d 路徑下添加一個 .conf 結(jié)尾的配置文件,該配置文件里面的內(nèi)容就是我們自己動態(tài)庫所在的路徑。添加完后執(zhí)行 ldconfig 指令,將所有的配置文件重現(xiàn)加載一下,然后程序就能夠正常運行啦。


小Tips:這樣添加,當系統(tǒng)重啟后新添加的境變量就沒有了,如果想讓系統(tǒng)啟動時自動添加該路徑到 LD_LIBRARY_PATH 環(huán)境變量中,可以通過修改 ~/.bash_profile 中的配置去實現(xiàn),具體如下圖所示:


小Tips:加載器是不需要知道庫的名字的,只需要知道庫的路徑即可。
2.5 動態(tài)庫是如何被加載和共享的?
動態(tài)庫在進程運行的時候是需要被加載到內(nèi)存的,常見的動態(tài)庫被所有的可執(zhí)行程序(動態(tài)鏈接的),都要使用,因此,動態(tài)庫在系統(tǒng)中加載之后,會被所有進程共享。

首先我們需要知道,一個進程可以鏈接多個動態(tài)庫,同理,當系統(tǒng)中存在多個進程的時候,那么此時系統(tǒng)中一定是存在多個動態(tài)庫的。操作系統(tǒng)一定會通過“先描述,再組織”的方式將系統(tǒng)中所有的動態(tài)庫管理起來。所以對操作系統(tǒng)而言,所有庫的加載情況,它非常清楚。A.exe 在編譯鏈接的時候采用的是動態(tài)庫,A 進程在運行的時候,CPU 按照從上往下的順序執(zhí)行代碼,遇到了一個庫函數(shù),假設(shè)就為 printf,此時操作系統(tǒng)發(fā)現(xiàn) printf 所在的動態(tài)庫并沒有被加載到內(nèi)存中,因此就會將這個動態(tài)庫加載到內(nèi)存,因為動態(tài)庫也是文件,也有 inode,所以這本質(zhì)上就是文件的加載,將動態(tài)庫加載到內(nèi)存之后,操作系統(tǒng)會在 A 進程的頁表上建立該動態(tài)庫與 A 進程地址空間中共享區(qū)的映射關(guān)系,然后 CPU 就又代碼段跳轉(zhuǎn)到共享區(qū)去執(zhí)行動態(tài)庫中關(guān)于 printf 的代碼,執(zhí)行完后跳轉(zhuǎn)會代碼段繼續(xù)執(zhí)行后續(xù)代碼。與此同時,B.exe 經(jīng)過編譯鏈接(用動態(tài)庫),然后被加載到內(nèi)存,成為 B 進程,CPU 在執(zhí)行 B 進程代碼的時候,也遇到了 printf 函數(shù),此時因為在 A 進程執(zhí)行的時候,就把 printf 所在的動態(tài)庫加載到了內(nèi)存,所以此時操作系統(tǒng)并不會再去把這個動態(tài)庫加載一遍,而是直接在 B 進程的頁表中建立映射關(guān)系。此時一個動態(tài)庫被加載到內(nèi)存中,就同時被兩個進程所使用,因此動態(tài)庫也被叫做共享庫。
一個問題:現(xiàn)在我們知道了動態(tài)庫是可以被多個進程共享的。那動態(tài)庫中的全局變量例如 errno 該怎么辦?我們知道,errno 是 C 語言為我們提供的一個錯誤碼,一般在調(diào)用庫函數(shù)失敗的時候,該錯誤碼會被設(shè)置,那動態(tài)庫是被共享的,豈不意味著 errno 也可能是被多個進程共享的,那在 A 進程中執(zhí)行庫函數(shù)失敗,假設(shè) errno 被設(shè)置成 1,在 B 進程中 errno 也是 1 嘛?這顯然是不合理的。實際上,當要修改 errno 的時候,操作系統(tǒng)會通過引用計數(shù)去判斷該動態(tài)庫是否被多個進程共享,如果該庫被多個進程共享,操作系統(tǒng)會發(fā)生寫時拷貝。
三、再來認識地址
3.1 邏輯地址的引入
一個 .c 源文件在被編譯成為 .exe 可執(zhí)行程序的時候,會加上地址??梢赃@樣來理解,一個 .c 源文件首先會編譯成為匯編文件,將我們的 C 語言轉(zhuǎn)化成一條條匯編指令,接著會把匯編指令轉(zhuǎn)化成機器碼,對應的匯編文件和機器碼文件其實都已經(jīng)加上了地址,最終 .exe 中也是包含地址的。.exe 文件本質(zhì)上就是由各種段構(gòu)成的,現(xiàn)如今的 .exe 文件中的編址都采用平坦模式,即 .exe 文件已經(jīng)按照程序地址空間的格式進行分段編址。
3.2 CPU 是如何知道指令位置的
上面說過,可執(zhí)行程序內(nèi)部是有邏輯地址的,在可執(zhí)行程序加載到內(nèi)存之后,每一條指令還會有自己對應的物理地址,因為物理內(nèi)存它本身就是有地址的,無論可執(zhí)行程序是否加載到內(nèi)存中。此時可執(zhí)行程序已經(jīng)被加載到了內(nèi)存,CPU 是如何知道該可執(zhí)行程序的第一條指令在哪兒的呢?在編譯形成可執(zhí)行程序的時候,除了形成代碼段、數(shù)據(jù)段、.bss 段外,還會形成一個文件頭,這里面就存儲了可執(zhí)行程序的入口地址,這個地址是邏輯地址(虛擬地址)。在 CPU 中有一個寄存器,一般管它叫做 PC 指針,它里面存儲的就是接下來要執(zhí)行指令的地址。實際上,最初并不急著把可執(zhí)行程序全部加載到內(nèi)存,只需要將可執(zhí)行文件的頭部加載到內(nèi)存即可,CPU 通過頭部獲取到可執(zhí)行程序的入口地址,然后拿著該地址去查頁表,發(fā)現(xiàn)并沒有建立內(nèi)存映射,此時操作系統(tǒng)會發(fā)生缺頁中斷,將對應的程序加載到內(nèi)存,接下來就好辦了,CPU 通過內(nèi)置的指令集,先天就知道每條指令的長度,然后他會按順序往后執(zhí)行,遇到函數(shù)調(diào)用指令,或者一些跳轉(zhuǎn)指令,也是根據(jù)虛擬地址去頁表中查找映射關(guān)系,發(fā)生缺頁中斷。因此可以得出一結(jié)論,CPU 是通過虛擬地址轉(zhuǎn)物理地址去執(zhí)行可執(zhí)行程序中的指令,訪問可執(zhí)行程序中的變量。
3.3 一個庫函數(shù)是如何被找到并且執(zhí)行的
結(jié)合上面兩點,可以得出,可執(zhí)行程序內(nèi)部有邏輯地址,CPU 是通過虛擬地址轉(zhuǎn)物理地址去執(zhí)行指令的。那一個可執(zhí)行程序是如何加載并使用動態(tài)庫的呢?以程序中調(diào)用 printf 函數(shù)為例,按照上面兩小節(jié)的說法,在可執(zhí)行程序中,printf 函數(shù)有一個固定的邏輯地址,假設(shè)為 0x11223344,而 CPU 是通過虛擬地址查找物理地址去執(zhí)行指令,即通過 0x11223344 這個虛擬地址去映射找到物理地址,然后執(zhí)行 printf 函數(shù),那是否意味著,在程序地址空間角度,動態(tài)庫需要被加載到動態(tài)區(qū)的固定位置,這樣才能保證 printf 函數(shù)的地址是 0x11223344。如果按照上面兩小節(jié)說的,對于一個進程來說,動態(tài)庫是必須加載到固定的位置,但是這幾乎是不可能的,因為一個可執(zhí)行程序可能同時使用多個庫,每個庫的大小不一,并且每個庫中都獨立編址,可能該進程還使用了 B 庫中的某個函數(shù),該函數(shù)在 B 庫中為編址也是 0x11223344。所以很難做到將一個動態(tài)庫加載到固定位置。因此我們需要想辦法讓庫可以在虛擬內(nèi)存中共享區(qū)的任意位置進行加載,實現(xiàn)方法是,在動態(tài)庫內(nèi)部,不采用絕對編址,而是采用相對編址,對于動態(tài)庫中的函數(shù)只需要知道其在庫中的偏移量即可。0x11223344 就不再表示 printf 的絕對地址,而是它相對于這個庫起始位置的偏移量,此時就可以實現(xiàn)把庫加載到虛擬內(nèi)存共享區(qū)的任意位置。之后,操作系統(tǒng)只需要記住每一個庫在虛擬內(nèi)存中的起始地址即可,當要執(zhí)行某個庫函數(shù)的時候,只需要用該函數(shù)所在庫的起始地址加上該函數(shù)的相對地址(也就是偏移量),就可以知道該函數(shù)在程序地址空間中的虛擬地址,然后再拿著這個虛擬地址去查頁表,找到該函數(shù)在物理內(nèi)存中的地址,然后執(zhí)行庫函數(shù)。-fPIC 選項,就是讓編譯器在形成動態(tài)庫文件的時候,直接用偏移量對庫中的函數(shù)進行編址。靜態(tài)庫是直接拷貝到可執(zhí)行程序中的,無需加載到物理內(nèi)存中,因此靜態(tài)庫中的函數(shù)就被當做了我們自己寫的函數(shù)一樣,直接采用絕對編址。
四、結(jié)語
以上就是Linux動靜態(tài)庫的制作與使用的詳細內(nèi)容,更多關(guān)于Linux動靜態(tài)庫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
CentOS8 安裝 jdk8 / java8的教程(推薦)
CentOS8上使用 yum 直接安裝,環(huán)境變量自動配置好 ,本文主要給大家介紹 CentOS8 安裝 jdk8 / java8的教程,非常不錯,具有一定的參考借鑒價值,需要的朋友參考下吧2019-10-10
禁止apache顯示目錄索引的常見方法(apache禁止列目錄)
下面說下禁止禁止Apache顯示目錄索引的常見的3種方法。要實現(xiàn)禁止Apache顯示目錄索引,只需將Option中的Indexes去掉即可,具體方法看下面說明2014-02-02
phpStudy環(huán)境下安裝配置SSL證書實現(xiàn)加密訪問(Https)
phpStudy環(huán)境下安裝配置SSL證書,本文主要介紹了phpStudy環(huán)境下安裝配置SSL證書實現(xiàn)加密訪問,具有一定的參考價值,感興趣的可以了解一下2023-09-09
linux下通過rsync+inotify 實現(xiàn)數(shù)據(jù)實時備份(遠程容災備份系統(tǒng))
linux下為了數(shù)據(jù)安全或者網(wǎng)站同步鏡像,不得不考慮一些實時備份的問題,這篇linux下通過rsync+inotify 實現(xiàn)數(shù)據(jù)實時備份配置過程記錄下來,防止遺忘2013-09-09
Linux系統(tǒng)創(chuàng)建TCP連接流程介紹
大家好,本篇文章主要講的是Linux系統(tǒng)創(chuàng)建TCP連接流程,感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下哦,方便下次瀏覽2021-12-12
centos7利用yum安裝lnmp的教程(linux+nginx+php7.1+mysql5.7)
lnmp相信不用多介紹了,大家應該都知道,下面這篇文章主要給大家介紹了關(guān)于centos7利用yum安裝lnmp(linux+nginx+php7.1+mysql5.7)的相關(guān)資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面來一起看看吧。2018-03-03
Ubuntu安裝MySQL5.7并配置數(shù)據(jù)存儲路徑的方法步驟
這篇文章主要介紹了Ubuntu安裝MySQL5.7并配置數(shù)據(jù)存儲路徑的方法步驟,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-06-06

