---
# System prepended metadata

title: C Library 庫
tags: [Linux Shell, C]

---

---
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`
