了解 C 語言函式庫 | 靜態、動態函式庫 | 使用與編譯 | Library 庫知識

了解 C 語言函式庫 | 靜態、動態函式庫 | 使用與編譯 | Library 庫知識

Overview of Content

本文深入介紹了C語言庫的使用及相關知識。首先,我們將概述庫的概念,包括靜態庫(.a 檔案)和動態庫(.so檔 案)。然後,我們將深入探討如何使用這些庫,包括如何編譯動態庫以及如何編譯靜態庫並進行靜態連結。我們還將討論如何查詢系統庫函數以及使用 gcc 手動連結系統不常用的庫。

接著,我們將探討手工製作C語言庫的過程,包括製作靜態庫和動態庫。我們還將介紹使用"nm"命令查看靜態庫符號以及使用"ldd"命令查看動態庫依賴和位置的方法。

最後,我們將進一步探討有關庫的更多知識,包括 ld.so 如何查找庫的順序以及如何在編譯時設定共享庫的位置。

通過本文,讀者將全面了解C語言庫的使用和編譯過程,並掌握有關靜態庫和動態庫的重要知識。


C Library 認識

簡單來說就是為了要覆用函數,所以產生出了 Library;如常見的就有 glibc

Library 庫又分為 1. 靜態庫2. 動態庫

● 為何不用 Open Source 就好了呢 ?

因為無法商業化,廠商如果要保全自己開發的程式,最安全的方式就是提用 Library 庫,這樣既可以得到商業利益,又可以提供給使用者

靜態 Library 庫 - .a

● 靜態 Library 庫出現的比較早,附檔名為 .a,它的概念如下:

A. 將原程式 經過匯編,但不進行連結

B. 透過 ar 工具,將許多 .o 文件歸檔至 .a 文件

C. 提供給使用者 .o 文件、.h 頭文件,就可以直接使用

● 每個使用到靜態 Library 的應用程式,都會複製一份檔案到應用中

這個缺點是

A. 佔用空間,較率較低

B. 在更新 library 時,每個依賴於靜態 library 的程式都必須重新編譯(因為他們都獨自有一個完整的靜態檔案在自己的空間)

動態 Library 庫 - .so

● 動態 Library 庫,附檔名為 .so,它的效率更高,概念如下:

A. .so 中存的是 鏈結標記,在需要時才去記憶體中尋找

B. .so 在設備中只存在一份在記憶體中,類似於一個虛擬文件,應用在使用時,才去記憶體中尋找對應的標記

C. 當 .so 更新時,不需要每個應用都重新編譯(除非 .so 有新的符號連結)


C Library 使用

在使用 Library 時有幾個步驟需要注意

A. 引入 Library 的頭文件 (.h 檔案)

B. 有些函數鏈結時需要使用 -l + xxx 的方式指定具體連結的 Library

C. 動態 Library 大部分會使用 -L 來指定動態 Library 存放的 目錄

編譯動態庫 - 動態連結

以下來動態連結系統庫


#include <stdio.h>

int main(void) {
	
	printf("Hello C library.");

	return 0;
}

● 一般我們用 gcc 命令來編譯 .c 文件時,預設使用動態 Library 鏈結:所以編譯出的檔案大小並不大 (因為只有鏈結符號)


gcc Hello.c -o Hello_1.out

大小為:16,064 Byte

編譯靜態庫 - 靜態連結 static

以下來靜態連結系統庫


#include <stdio.h>

int main(void) {
	
	printf("Hello C library.");

	return 0;
}

透過選項 -static,限定 gcc 使用靜態鏈結:可以發現編譯出來的執行檔相當大,因為靜態鏈結會將所有資料都複製進應用中


gcc Hello.c -static -o Hello_2.out

大小為:896,216 Byte

● 編譯時有時不需要標頭檔 ?

其實都是需要標頭檔 (.h) 的,只是有些標頭檔已經存在系統中,可以在系統中發現,所以不用特別指定

像是:stdio.h 標頭檔就是系統內建,所以不自己手動需要添加

查詢系統 Library 函數

● 可以透過 man 3 <API 名稱> 查看相關資料,指令中的 3 代表 section,是查看 library 如何使用,而其他數字代表的意思請見下圖


man man

A. Library calls 查看 memcpy:可以看到該函數的原形,要引入哪個標頭檔... 等等


man 3 memcpy

B. System calls 查看 mmp


man 2 mmap

gcc 手動鏈結 - 系統不常用函式庫


#include <stdio.h>
#include <math.h>

int main(void) {

	int a = 100;
	double b = sqrt(a);
	printf("result: %lf\n", b);

	return 0;
}

編譯時出錯,找不到 sqrt 函數

● Library calls 查看 sqrt (平方),在檔案編譯時需要使用 -lm 去做鏈結才能正常編譯成功,因為鏈結器預設會去找常用的 Library

<math.h> 並不常用,所以 編譯時需手動鏈結


man 3 sqrt

● 修正編譯指令,使用 -lm 告訴編譯器去 libm 中尋找函數


gcc Math_Test.c -lm -o Math_Test.out

這裡就符合上面提到的特殊 Library 需要使用 -lxxx 去做鏈結


Hand made C Library

透過 Linux 提供的工具就可以自己來手動包動、靜態 library

Hand made - static library

打包工具使用 ar

靜態 Library Source:主要分為兩個重點;1. 程式實作它最終會被編譯成二進制文件、另一個則是辨識二進制文件的 2. 頭文件

A. 程式實作MyStaticLib.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);
}

B. 頭文件MyHead.h


// MyHead.h

int add(int a, int b);

int sub_1(int a, int b);

int sub_2(int a, int b);

● 編譯打包成靜態 Library

A. 匯編 Source Code (必須在匯編階段,所以使用 -c option 指定)


gcc -c MyStaticLib.c -o MyStaticLib.o

B. 使用 ar -rc 集成靜態 Library


## ar -rc <lib 名稱> <.o 檔案_1> <.o 檔案_2> ...

ar -rc libMyStatic.a MyStaticLib.o

● 使用靜態 Library:引入 Library 頭檔案


#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;
}

A. 嘗試編譯:透過上面的學習我們可以知道要找到目標 library 必須使用 -lxxx 鏈結,這裡我們使用以下指令

● 使用 -l 鏈結 .a 時:去除前綴 lib & 副檔名 .so

e.g:鏈結 libMyStatic.a 就需要寫成 -lMyStatic 來鏈結


## -l<目標 Library>
## -l <目標 Library>    (中間有沒有空格都可以)

gcc UseStaticLib.c -l MyStatic -o UseStaticLib.out

編譯失敗:因為在預設庫資料夾中,找不到 MyStatic

B. 添加指定路徑後再次編譯:

透過提示我們可以知道編譯器在 /usr/bin/ld 目錄下找不到目標鏈結,在這裡我們就必須使用 -L 來指定 Library 位置


## -L<目標 Library 位置>
## -L <目標 Library 位置>    (中間有沒有空格都可以)

gcc UseStaticLib.c -l MyStatic -L . -o UseStaticLib.out

● 執行編譯好的 UseStaticLib.out 檔案


./UseStaticLib.out

nm - 查看靜態 Library 符號

● 透過 nm 指令就可以查看靜態 Library 符號


nm MyStaticLib.a

Hand made - dynamic library

● 動態鏈結檔在不同平台下有不同的附檔名

| Window | Linux |

| -------- | -------- |

| .dll | .so |

● 程式就使用上面靜態 Library Source Code

● 發布動態 Library 時,依樣要發布 Header 檔案

● 編譯 .so 動態 Library

A. 我們將上面的 MyStaticLib.c 再次匯編,並指定檔案名稱為 MySharedLib.o


gcc -c MyStaticLib.c -o MySharedLib.o -fPIC

-fPIC

表示位置將該檔案編譯成 位置無關碼

B. 編譯成動態 Library libMyShared.so


gcc MySharedLib.o -shared -o libMyShared.so

● 編譯應用使用的檔案 UseSharedLib.c (內容同靜態檔案,只是複製過來並改名)


gcc UseSharedLib.c -L . -l MyShared -o UseSharedLib.out

● 運行依賴動態檔案的應用出錯 ??!

因為在運行時才加載進 RAM 中,但加載 lib 的位置 /usr/lib 找不到目標檔案

● 解決方式有幾種

A. 將 libMyShared.so 放置到 /usr/lib 目錄下

B. 修改變量區域變量 LD_LIBRARY_PATH串上 so 所在的資料夾


export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD && ./UseSharedLib.out

C. 在編譯時決定運行時要尋找的庫的位置(-Wl,-rpath 下面有範例)

ldd - 查看動態 Library 依賴/位置

● 使用 ldd 指令就可以查看動態 Library 的依賴


ldd UseSharedLib.out

● 查找系統應用的動態連結關係


ldd /bin/bash

使用 Pair 的方式去看:左邊是連接的動態庫名,右邊則是該動態庫真實位置

Pair<庫名, 該庫的位置>

● 最後一行的 /lib/ld-linux-aarch64.so.1 標明了該指令 ldd 的 ls.so 實際位置


Library 庫的更多知識

ld.so 找函式庫 - 順序

● 共享函式庫強大,但它卻是管理困難、連接複雜的,我們往往在使用時會遇到以下困難

A. 作為應用使用者:該應用需要什麼函式庫

可以使用 ldd 命令查看該應用所需的函示庫

B. 作用應用開發者:如何找到共享函示庫

C. 如何連接函式庫

ld.so 尋找函式庫的方式

A. 查看 LD_LIBRARY_PATH 環境變量是否有賦予值,如果有的話 ld.so 會優先尋找該目錄下的庫

B. 看看要運行的應用是否有 預先設定好的執行時函式庫搜尋路徑runtime library search path

使用 -Wl,-rpath 設定,下面小節有範例

C. 參考系統當前的 快取 /etc/ld.so.cache


cat /etc/ld.so.cache | sed 's/.so/.so\n/g'

/etc/ld.so.cache 的來源

它的來源是 /etc/ld.so.conf 目錄列表,而它的內容又會指向 /etc/ld.so.conf.d 目錄,所以是該目錄下所有 config 檔案

● 如果有改動該目錄,需使用以下命令去刷新


ldconfig -v

不過建議不要肆意添加內容進去,容易導致混淆的風險

編譯時設定 - 共享庫的位置

● 編譯時設定共享 (.so) 函式庫的路徑,範例如下

A. 創建共享函示庫(libhello.so)

● 共享函式庫標頭檔 hello.h


// hello.h

#ifndef HELLO
#define HELLO

void sayHello(void);

#endif

● 共享函式庫源碼 hello.c


// hello.c

#include <stdio.h>

void sayHello(void) {
    printf("Hello world, ln~ \n");
}

● 編譯 -c 與位置無關代碼 -fPIC


gcc -c hello.c -o hello.o -fPIC

● 編譯共享庫 libhello.so


gcc -shared hello.o libhello.so

B. 透過標頭檔使用 libhello.so 共享庫


#include "./hello.h"

int main(void) {

    sayHello();

    return 0;
}

C. 連接 libhello.so 共享庫:使用 -Wl,-rpath 選項對連接器傳入設定,設定共享庫的路徑為當位置(以這個例子來說是這樣的)


cc main.c -o main.o -Wl,-rpath="$PWD" -L . -lhello

./main.o

最後使用 ldd 命令進行驗證,確定連接的位置就是我們指定的目錄

更多的 C 語言相關文章

關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言

編譯器、系統開念

編譯器、系統開念:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節

C 語言與系統開發

C 語言與系統開發:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響