--- title: 'C Library 庫' disqus: kyleAlien --- C Library 庫 === ## Overview of Content :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**了解 C 語言函式庫 | 靜態、動態函式庫 | 使用與編譯 | Library 庫知識**](https://devtechascendancy.com/understanding-c-library-static-dynamic/) ::: [TOC] ## C Library 庫 - 概述 簡單來說就是為了要覆用函數,所以產生出了 Library;如常見的就有 `glibc` Library 庫又分為 ^1.^ **靜態庫**、^2.^ **動態庫** :::info * 為何不用 `Open Source` 就好了呢 ? 因為無法商業化,廠商如果要保全自己開發的程式,最安全的方式就是提用 Library 庫,這樣既可以得到商業利益,又可以提供給使用者 ::: ### 靜態 Library 庫 - .a * 靜態 Library 庫出現的比較早,**附檔名為 `.a`**,它的概念如下: 1. 將原程式 **經過匯編,但不進行連結** 2. 透過 `ar` 工具,將許多 `.o` 文件歸檔至 `.a` 文件 3. 提供給使用者 `.o` 文件、`.h` 頭文件,就可以直接使用 > ![](https://i.imgur.com/MUvSr7V.png) * 每個使用到靜態 Library 的應用程式,都會複製一份檔案到應用中 這個缺點是 1. **佔用空間,較率較低** 2. 在更新 library 時,每個依賴於靜態 library 的程式都必須重新編譯(因為他們都獨自有一個完整的靜態檔案在自己的空間) > ![](https://i.imgur.com/HqYdxEp.png) ### 動態 Library 庫 - .so * 動態 Library 庫,**附檔名為 `.so`**,它的效率更高,概念如下: 1. `.so` 中存的是 **鏈結標記**,在需要時才去記憶體中尋找 2. `.so` 在設備中只存在一份在記憶體中,類似於一個虛擬文件,應用在使用時,才去記憶體中尋找對應的標記 3. 當 `.so` 更新時,不需要每個應用都重新編譯(除非 `.so` 有新的符號連結) > ![](https://i.imgur.com/nEHWh4j.png) ## Library 使用 在使用 Library 時有幾個步驟需要注意 1. 引入 Library 的頭文件 (`.h` 檔案) 2. 有些函數鏈結時需要使用 `-l` + xxx 的方式指定具體連結的 Library 3. 動態 Library 大部分會使用 `-L` 來指定動態 Library 存放的 **目錄** ### 編譯動態庫 - 動態連結 以下來動態連結系統庫 ```c= #include <stdio.h> int main(void) { printf("Hello C library."); return 0; } ``` * 一般我們用 `gcc` 命令來編譯 `.c` 文件時,**預設使用動態 Library 鏈結**:所以編譯出的檔案大小並不大 (因為只有鏈結符號) ```shell= gcc Hello.c -o Hello_1.out ``` > 大小為:`16,064` Byte > > ![](https://i.imgur.com/0x2Uy3y.png) ### 編譯靜態庫 - 靜態連結 static 以下來靜態連結系統庫 ```c= #include <stdio.h> int main(void) { printf("Hello C library."); return 0; } ``` * **透過選項 `-static`,限定 gcc 使用靜態鏈結**:可以發現編譯出來的執行檔相當大,因為靜態鏈結會將所有資料都複製進應用中 ```shell= gcc Hello.c -static -o Hello_2.out ``` > 大小為:`896,216` Byte > > ![](https://i.imgur.com/sHDwn4y.png) :::info * 編譯時有時不需要標頭檔 ? 其實都是需要標頭檔 (`.h`) 的,只是有些標頭檔已經存在系統中,可以在系統中發現,所以不用特別指定 > 像是:`stdio.h` 標頭檔就是系統內建,所以不自己手動需要添加 > > ![](https://i.imgur.com/lLskPOV.png) ::: ### 查詢系統 Library 函數 * 可以透過 `man 3 <API 名稱>` 查看相關資料,指令中的 `3` 代表 section,是查看 library 如何使用,而其他數字代表的意思請見下圖 ```shell= man man ``` > ![](https://i.imgur.com/HddRyfL.png) 1. Library calls 查看 `memcpy`:可以看到該函數的原形,要引入哪個標頭檔... 等等 ```shell= man 3 memcpy ``` > ![](https://i.imgur.com/ijViQ1T.png) 2. System calls 查看 `mmp` ```shell= man 2 mmap ``` > ![](https://i.imgur.com/Q0eUKZt.png) ### gcc 手動鏈結 - 系統不常用函式庫 ```c= #include <stdio.h> #include <math.h> int main(void) { int a = 100; double b = sqrt(a); printf("result: %lf\n", b); return 0; } ``` > 編譯時出錯,找不到 `sqrt` 函數 > > ![](https://i.imgur.com/ljsEm4n.png) * Library calls 查看 sqrt (平方),在檔案編譯時需要使用 `-lm` 去做鏈結才能正常編譯成功,因為鏈結器預設會去找常用的 Library `<math.h>` 並不常用,所以 **編譯時需手動鏈結** ```shell= man 3 sqrt ``` > ![](https://i.imgur.com/1cUxbqa.png) * 修正編譯指令,**使用 `-lm` 告訴編譯器去 ++libm++ 中尋找函數** ```shell= gcc Math_Test.c -lm -o Math_Test.out ``` :::info 這裡就符合上面提到的特殊 Library 需要使用 `-lxxx` 去做鏈結 ::: ## Hand made 透過 Linux 提供的工具就可以自己來手動包動、靜態 library ### Hand made - static library :::info 打包工具使用 `ar` ::: * **靜態 Library Source**:主要分為兩個重點;^1.^ 程式實作它最終會被編譯成二進制文件、另一個則是辨識二進制文件的 ^2.^ 頭文件 1. **程式實作**:`MyStaticLib.c` ```c= // MyStaticLib.c int add(int a, int b) { return a + b; } static int _sub(int a, int b, int reverse) { if(reverse) { return b - a; } else { return a - b; } } int sub_1(int a, int b) { return _sub(a, b, 0); } int sub_2(int a, int b) { return _sub(a, b, 1); } ``` 2. **頭文件**:`MyHead.h` ```c= // MyHead.h int add(int a, int b); int sub_1(int a, int b); int sub_2(int a, int b); ``` * 編譯打包成靜態 Library 1. 匯編 Source Code (必須在匯編階段,所以使用 `-c` option 指定) ```shell= gcc -c MyStaticLib.c -o MyStaticLib.o ``` 2. **使用 `ar -rc` 集成靜態 Library** ```shell= ## ar -rc <lib 名稱> <.o 檔案_1> <.o 檔案_2> ... ar -rc libMyStatic.a MyStaticLib.o ``` > ![](https://i.imgur.com/M1taZwn.png) * 使用靜態 Library:引入 Library 頭檔案 ```c= #include "MyHead.h" // 引入 Library 頭檔案 #include <stdio.h> int main(void) { int a = 10, b = 20; // 直接使用 Library printf("a - b: %d\n", add(a, b)); printf("a - b: %d\n", sub_1(a, b)); printf("b - a: %d\n", sub_2(a, b)); return 0; } ``` 1. **嘗試編譯**:透過上面的學習我們可以知道要找到目標 library 必須使用 `-lxxx` 鏈結,這裡我們使用以下指令 :::success * 使用 `-l` 鏈結 `.a` 時:去除前綴 `lib` & 副檔名 `.so` > e.g:鏈結 `libMyStatic.a` 就需要寫成 `-lMyStatic` 來鏈結 ::: ```shell= ## -l<目標 Library> ## -l <目標 Library> (中間有沒有空格都可以) gcc UseStaticLib.c -l MyStatic -o UseStaticLib.out ``` > 編譯失敗:因為在預設庫資料夾中,找不到 `MyStatic` 庫 2. 添加指定路徑後再次編譯: 透過提示我們可以知道編譯器在 `/usr/bin/ld` 目錄下找不到目標鏈結,在這裡我們就必須使用 `-L` 來指定 Library 位置 ```shell= ## -L<目標 Library 位置> ## -L <目標 Library 位置> (中間有沒有空格都可以) gcc UseStaticLib.c -l MyStatic -L . -o UseStaticLib.out ``` * 執行編譯好的 `UseStaticLib.out` 檔案 ```shell= ./UseStaticLib.out ``` > ![](https://i.imgur.com/kpGwuU2.png) ### nm - 查看靜態 Library 符號 * 透過 `nm` 指令就可以查看靜態 Library 符號 ```shell= nm MyStaticLib.a ``` > ![](https://i.imgur.com/a9NySzy.png) ### Hand made - dynamic library :::info * 動態鏈結檔在不同平台下有不同的附檔名 | Window | Linux | | -------- | -------- | | .dll | .so | * 程式就使用上面靜態 Library Source Code * 發布動態 Library 時,依樣要發布 Header 檔案 ::: * 編譯 `.so` 動態 Library 1. 我們將上面的 `MyStaticLib.c` 再次匯編,並指定檔案名稱為 `MySharedLib.o` ```shell= gcc -c MyStaticLib.c -o MySharedLib.o -fPIC ``` :::success * `-fPIC` 表示位置將該檔案編譯成 **位置無關碼** ::: 2. 編譯成動態 Library `libMyShared.so` ```shell= gcc MySharedLib.o -shared -o libMyShared.so ``` > ![](https://i.imgur.com/X9WaPS6.png) * 編譯應用使用的檔案 `UseSharedLib.c` (內容同靜態檔案,只是複製過來並改名) ```shell= gcc UseSharedLib.c -L . -l MyShared -o UseSharedLib.out ``` :::warning * 運行依賴動態檔案的應用出錯 ??! 因為在運行時才加載進 RAM 中,但加載 lib 的位置 `/usr/lib` 找不到目標檔案 > ![](https://i.imgur.com/0YbX3Ua.png) * 解決方式有幾種 1. 將 `libMyShared.so` 放置到 `/usr/lib` 目錄下 2. 修改變量區域變量 `LD_LIBRARY_PATH`,**串上 so 所在的資料夾** ```shell= export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD && ./UseSharedLib.out ``` > ![](https://i.imgur.com/qkJs482.png) 3. 在編譯時決定運行時要尋找的庫的位置(`-Wl,-rpath` 下面有範例) ::: ### ldd - 查看動態 Library 依賴/位置 * 使用 `ldd` 指令就可以查看動態 Library 的依賴 ```shell= ldd UseSharedLib.out ``` > ![](https://i.imgur.com/3gyHTm4.png) * 查找系統應用的動態連結關係 ```shell= ldd /bin/bash ``` 使用 Pair 的方式去看:**左邊是連接的動態庫名,右邊則是該動態庫真實位置** > Pair<庫名, 該庫的位置> > > ![](https://hackmd.io/_uploads/BycGvfP2n.png) :::info * 最後一行的 `/lib/ld-linux-aarch64.so.1` 標明了該指令 ldd 的 `ls.so` 實際位置 ::: ## 其他相關知識點 ### `ld.so` 找函式庫 - 順序 * 共享函式庫強大,但它卻是管理困難、連接複雜的,我們往往在使用時會遇到以下困難 1. 作為應用使用者:**該應用需要什麼函式庫** > 可以使用 `ldd` 命令查看該應用所需的函示庫 2. 作用應用開發者:**如何找到共享函示庫** 3. **如何連接函式庫** * `ld.so` 尋找函式庫的方式 1. **查看 `LD_LIBRARY_PATH` 環境變量是否有賦予值**,如果有的話 `ld.so` 會優先尋找該目錄下的庫 2. 看看要運行的應用是否有 **預先設定好的執行時函式庫搜尋路徑**(`runtime library search path`) > 使用 `-Wl,-rpath` 設定,下面小節有範例 3. 參考系統當前的 **快取 `/etc/ld.so.cache`** ```shell= cat /etc/ld.so.cache | sed 's/.so/.so\n/g' ``` > ![](https://hackmd.io/_uploads/H1muqMPnn.png) :::success * `/etc/ld.so.cache` 的來源 它的來源是 `/etc/ld.so.conf` 目錄列表,而它的內容又會指向 `/etc/ld.so.conf.d` 目錄,所以是該目錄下所有 config 檔案 > ![](https://hackmd.io/_uploads/B1rzozD22.png) * 如果有改動該目錄,需使用以下命令去刷新 ```shell= ldconfig -v ``` > 不過建議不要肆意添加內容進去,容易導致混淆的風險 ::: ### 編譯時設定 - 共享庫的位置 * 編譯時設定共享 (`.so`) 函式庫的路徑,範例如下 1. **創建共享函示庫(libhello.so)** * 共享函式庫標頭檔 `hello.h` ```c= // hello.h #ifndef HELLO #define HELLO void sayHello(void); #endif ``` * 共享函式庫源碼 `hello.c` ```c= // hello.c #include <stdio.h> void sayHello(void) { printf("Hello world, ln~ \n"); } ``` * 編譯 `-c` 與位置無關代碼 `-fPIC` ```shell= gcc -c hello.c -o hello.o -fPIC ``` * 編譯共享庫 `libhello.so` ```shell= gcc -shared hello.o libhello.so ``` 2. **透過標頭檔使用 `libhello.so` 共享庫** ```c= #include "./hello.h" int main(void) { sayHello(); return 0; } ``` 3. **連接 `libhello.so` 共享庫**:使用 `-Wl,-rpath` 選項對連接器傳入設定,設定共享庫的路徑為當位置(以這個例子來說是這樣的) ```shell= cc main.c -o main.o -Wl,-rpath="$PWD" -L . -lhello ./main.o ``` > ![](https://hackmd.io/_uploads/B1tNzQPnn.png) 最後使用 `ldd` 命令進行驗證,確定連接的位置就是我們指定的目錄 > ![](https://hackmd.io/_uploads/S1nKG7P2n.png) ## 更多的 C 語言相關文章 關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言 ### C 語言基礎 * **C 語言基礎**:有關於到 C 語言的「語言基礎、細節」 :::info * [**理解C語言中的位元操作:位元運算基礎與宏定義**](https://devtechascendancy.com/bitwise-operations-and-macros-in-c/) * [**C 語言解析:void 意義、NULL 意義 | main 函數調用、函數返回值意義 | 臨時變量的產生**](https://devtechascendancy.com/meaning_void_null_return-value_temp-vars/) * [**C 語言中的 Struct 定義、初始化 | 對齊、大小端 | Union、Enum**](https://devtechascendancy.com/c-struct_alignment_endianness_union_enum/) * [**C 語言儲存類別、作用域 | 修飾語、生命週期 | 連結屬性**](https://devtechascendancy.com/c-storage-scope-modifiers-lifecycle-linkage/) * [**指標 & Array & typedef | 指標應用的關鍵 9 點 | 指標應用、細節**](https://devtechascendancy.com/pointers-arrays-const-typedef-sizeof-null/) ::: ### 編譯器、系統開念 * **編譯器、系統開念**:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節 :::warning * [**理解電腦記憶體管理 | 深入瞭解記憶體 | C 語言程式與記憶體**](https://devtechascendancy.com/computer-memory_manager-c-explained/) * [**C 語言記憶體區塊規劃 | Segment 段 | 字符串特性**](https://devtechascendancy.com/c-memory-segmentation-string-properties/) * [**編譯器的角度看程式 | 低階與高階、作業系統、編譯器、直譯器、預處理 | C語言函數探討**](https://devtechascendancy.com/compiler-programming-os-c-functions/) ::: ### C 語言與系統開發 * **C 語言與系統開發**:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用 :::danger * [**了解 C 語言函式庫 | 靜態、動態函式庫 | 使用與編譯 | Library 庫知識**](https://devtechascendancy.com/understanding-c-library-static-dynamic/) * [**Linux 宏拓展 | offsetof、container_of 宏、鏈表 | 使用與分析**](https://devtechascendancy.com/linux-macro_offsetof_containerof_list/) ::: ## Appendix & FAQ :::info ::: ###### tags: `C` `Linux Shell`