Linux中gcc的編譯、靜態庫和動態庫的製作

語言: CN / TW / HK

本文已參與「新人創作禮」活動,一起開啟掘金創作之路。

gcc是文字編譯器,就是編譯程式碼的工具,下面介紹gcc編譯C語言(.c檔案)的流程。

1 gcc的編譯過程

1.1 gcc的編譯過程

在這裡插入圖片描述

gcc的編譯分為以下四個階段:

  • gcc前處理器:把.c檔案編譯成預處理.i檔案
  • gcc編譯器:把預處理.i檔案編譯成.s的彙編檔案
  • gcc彙編器:把.s彙編檔案編譯成.o二進位制檔案
  • gcc連結器:把.o二進位制檔案連結成一個可執行檔案

四個階段的編譯命令: * 預處理:gcc -E hello.c -o hello.i * 編譯:gcc -S hello.i -o hello.s * 彙編:gcc -c hello.s -o hello.o * 連結:gcc hello.o -o hello

上面的四個過程也可以用一個命令執行,直接生成可執行的檔案: ```python gcc hello.c -o hello

gcc hello.c # 沒有指定輸出問價名,預設是生成一個a.out可執行檔案。 ``` 注意:

1、記憶引數可以用ESc-o引數是指定輸出檔案的名字 2、在windows下,如果gcc hello.c,預設生成的可執行檔案為a.exe;如果gcc hello.c -o myapp,會直接生成可執行檔案myapp.exe,自動新增字尾。 3、在第二階段把預處理.i檔案編譯成.s彙編檔案浪費時間。 4、即使是直接生成可執行檔案,但是也是經過了預處理編譯彙編連結這些過程,只是沒有生成中間的這些檔案。


四個階段的具體功能: * 預處理:1)把.c檔案中的標頭檔案展開新增到.i預處理檔案的開頭;2)然後把.c檔案程式碼新增到.i的標頭檔案內容之後;3)把巨集定義的量值替換為具體的值,去掉原始碼中的註釋

  • 編譯:把c檔案翻譯彙編檔案,就是兩種程式語法的轉化。

  • 彙編:把彙編檔案程式設計二進位制檔案,此時的檔案已經看不出具體的內容。

  • 連結:將函式庫中相應的程式碼組合到目標檔案中。

1.2 gcc的常用引數

下面具體例項:

一、執行檔案和標頭檔案同級目錄

1、建立一個sum.c檔案,內容如下: ```python

include

// 雙引號匯入的標頭檔案是自己寫的

include "head.h"

define DEBUG

// main是入口函式 int main(void) { int a = NUM1; int aa; int b = NUM2; int sum = a + b; // 這是一個加法運算

ifdef DEBUG

printf("The sum value is : %d + %d = %d\n", a, b, sum);

endif

return 0;

} ```

2、在sum.c的同級建立head.h標頭檔案,內容如下: ```python

ifndef __HEAD_H_

define __HEAD_H_

define NUM1 10

define NUM2 20

endif

```

兩個檔案的層級結構,同級目錄: python ├── head.h ├── sum.c

3、預處理:gcc -E sum.c -o sum.i

執行完之後用vi sum.i檢視預處理之後sum.i內容,如下:

在這裡插入圖片描述

從檔案中可以看到,檔案內容很長,之前的匯入的標頭檔案,被替換為具體的標頭檔案程式碼內容,程式碼中的巨集定義量被替換為具體的值,程式碼中的註釋去掉。(相當於做菜食材的準備階段)

4、編譯:gcc -S sum.i -o sum.s

編譯就是把預處理的.i檔案編譯成.s的組合語言,編譯之後的sum.s內容,如下:

在這裡插入圖片描述

從檔案中可以看出,這個檔案顯示的已經不是C語言編寫的程式碼,已經被轉換為組合語言的程式碼,如果你對微控制器瞭解,你可能也對組合語言的語法有所瞭解。(編譯:就是把C語言翻譯成組合語言

5、彙編:gcc -c sum.s -o sum.o

彙編就是把彙編檔案變成二進位制檔案,彙編之後的sum.o內容,如下:

在這裡插入圖片描述

從檔案中可以看出,彙編成二進位制檔案之後,裡面的內容已經看不出來了。

6、連結:gcc sum.o -o sum

使用gcc連結器二進位制檔案連結成一個可執行檔案,將函式庫中相應的程式碼組合到目標檔案中。通過./sum即可執行該可執行檔案,執行結果如下:

在這裡插入圖片描述

如果你開啟可執行檔案sum,顯示的內容和sum.o差不多。

二、執行檔案和標頭檔案同級目錄

目錄層級結構: python ├── include │ └── head.h ├── sum.c

如果直接編譯(gcc sum.c -o sum),會提示找不到標頭檔案,如下: 在這裡插入圖片描述

找不到標頭檔案有兩種解決方法: * 直接在程式編寫的時候指定標頭檔案的位置 * 在編譯的時候用-I引數,指定標頭檔案所在的資料夾位置

gcc sum.c -I ./include -o sum

三、gcc的其他引數使用

1、引數-D:指定一個巨集定義

上面的程式中有printf()列印程式除錯的log資訊,但是程式釋出的時候,我們是不需要這些log資訊的,當然我們可以通過加除錯的#define DEBUG巨集的宣告,但是,程式中需要除錯輸出的log資訊比較多的時候,這種方法顯然不合適。

現在我們把DEBUG的巨集定義註釋掉 ```python

include

// 雙引號匯入的標頭檔案是自己寫的

include "head.h"

//#define DEBUG

// main是入口函式 int main(void) { int a = NUM1; int aa; int b = NUM2; int sum = a + b; // 這是一個加法運算

// 程式有 DEBUG巨集定義,程式才會執行prinf()

ifdef DEBUG

printf("The sum value is : %d + %d = %d\n", a, b, sum);

endif

return 0;

} ```

然後再執行: ```python

gcc sum.c -o sum ./sum `` 結果: 並不會輸出print列印的資訊了,如果再次打印出資訊呢,此時可以通過引數-D,在執行命令的時候給程式指定一個巨集`,如下:

```python

gcc sum.c -o sum -D DEBUG ./sum `` 此時就可以打印出printf()`資訊了。

總結:

-D引數的作用:不在程式中定義巨集,在程式編譯的時候定義。不指定,在程式預處理的時候,printf()就會被刪掉了。

2、-O引數:程式預處理的時候對程式碼優化

在程式預處理的時候對程式碼進行優化,把冗餘的程式碼去掉,有三個優化等級: * -O1:優化等級低 * -O2:優化等級中 * -O3:優化等級高

舉個例子: ```python int a = 10 int b = a int c = b int d = c

優化完之後就是

int d = 10 // 就是對d的一個賦值操作 ```

3、-Wall引數:輸出程式中的警告資訊

例如我們在程式中定義一個變數int aa;,但是沒有使用,此時就會輸出警告資訊。

4、-g引數:在程式中新增一些除錯資訊

gcc sum.c -o sum -g

  • -g引數之後,輸出的可執行檔案會比不加的大(因為包含除錯資訊)
  • 程式釋出是不需要加-g引數
  • 除錯需要加-g引數,否則沒有除錯資訊不可以除錯。(gdb除錯的時候必須加此引數

總結: 在這裡插入圖片描述 引數:-E-S,不是很重要,-c比較重要,後面我們在製作靜態庫和動態庫的時候需要用到生成的.o二進位制值檔案

2 gcc 靜態庫的製作

比如你和別人做專案合作,你不可能直接把原始碼給別人,那樣別人就可以自己開發,因為原始碼就是你的核心技術。你不應該賣給他原始碼,而是應該是程式,這樣你就可以根據他有什麼需求進行改或新增什麼功能模組等,就可以改一次就可以收費一次,這樣就可以有一個長期合作。

那應該給到客戶的是什麼呢? * 生成的庫 * 標頭檔案

這樣把生成的庫標頭檔案給客戶也能夠使用,只是他不知道里面具體怎麼實現的。這樣二者才能維持一個長期的合作

標頭檔案對應的.c檔案都被打包到了靜態庫動態庫裡面了。

2.1 靜態庫的製作流程

一、靜態庫的製作

1、命名規則 * 1)lib + 庫的名字 + .a * 2)例如:libmytest.a

2、製作步驟: * 1)生成對應的.o二進位制檔案 .c --> .o eg:gcc sum.c -c sum.o * 2)將生成的.o檔案打包,使用ar rcs + 靜態庫的名字(libMytest.a) + 生成的所有的.o * 3)釋出和使用靜態庫: * 釋出靜態庫 * 標頭檔案

說明: * 把.c檔案,也就是原始碼轉化成.o二進位制檔案之後,客戶就不知道到你的核心技術具體是怎麼實現的了。 * ar是對.o的二進位制檔案進行打包rcs是打包引數,把所有.o二進位制檔案打包成一個.a檔案,即:靜態庫。因此:靜態庫是一個打包了二進位制檔案的集合。 * 介面API是在標頭檔案中體現出來的。

例項:

目錄結構: ```python Calc ├── include │ └── head.h ├── lib ├── main.c └── src ├── add.c ├── div.c ├── mul.c └── sub.c

`` **說明:** * include資料夾:存放標頭檔案,提供給使用者呼叫的介面API* lib資料夾:存放庫檔案,即:生成的靜態庫、動態庫 * src資料夾:存放原始檔 * main.c程式:是使用者呼叫head.h標頭檔案`裡面的介面,然後在呼叫靜態庫裡面我們實現的演算法(只不過已經不是原始碼,而是被編譯成二進位制檔案)

下面開始吧:

原始碼 src/add.c實現的是加法運算: ```python

include "head.h"

int add(int a, int b) { int result = a + b; return result; } ```

標頭檔案 include/head.h實現是對原始碼呼叫的介面API: ```python

ifndef __HEAD_H_

define __HEAD_H_

int add(int a, int b); int sub(int a, int b); int mul(int a, int b); int div(int a, int b);

endif

```

main.c是對標頭檔案呼叫,然後呼叫靜態檔案,對演算法的使用,但是並不知道演算法的具體實現原始碼 ```python

include

include "head.h"

int main(void) { int sum = add(2, 24); printf("sum = %d\n", sum); return 0; } ```

使用者在main.c中引入標頭檔案#include "head.h",即在./include/head.h,就可以使用./include/head.h中定義的介面int add(int a, int b);,當main.c程式執行到add(int a, int b);介面時,就會到./src資料夾下找靜態檔案(打包的二進位制檔案——即:加法演算法的具體實現)


下面是具體的製作流程: 在這裡插入圖片描述

```python shliang@shliang-vm:~/shliang/gcc_learn/Calc$ tree . ├── include │ └── head.h ├── lib ├── main.c └── src ├── add.c ├── div.c ├── mul.c └── sub.c

3 directories, 6 files shliang@shliang-vm:~/shliang/gcc_learn/Calc$ cd src shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls add.c div.c mul.c sub.c

1、原始碼生成二進位制檔案(.o檔案) shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ gcc *.c -c -I ../include shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls add.c add.o div.c div.o mul.c mul.o sub.c sub.o

2、對生成的二進位制檔案(.o檔案),打包成靜態檔案(.a檔案),並移動到lib目錄下 shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ar rcs libMyCalc.a *.o shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls add.c add.o div.c div.o libMyCalc.a mul.c mul.o sub.c sub.o shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ mv libMyCalc.a ../lib shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ cd .. shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ls

3、呼叫include目錄下的標頭檔案(即:封裝的API介面) shliang@shliang-vm:~/shliang/gcc_learn/Calc$ gcc main.c lib/libMyCalc.a -I ./include -o sum shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ls include lib main.c src sum shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ./sum sum = 26 shliang@shliang-vm:~/shliang/gcc_learn/Calc$ `` 主要: * 製作好的靜態檔案要放到lib目錄下* 呼叫標頭檔案中的介面API,然後用gcc編譯的自己呼叫的main.c檔案,需要加上靜態檔案(.a檔案)`。 * 程式釋出的時候只需要給使用者的檔案: * 1)include目錄下的標頭檔案(head.h):封裝的是具體演算法實現的介面API * 2)lib目錄下的靜態檔案(.a檔案):是原始碼編譯的之後的二進位制檔案(.o檔案),然後被打包成靜態檔案(.a檔案)

用於另外一種呼叫靜態庫的方法為:

gcc main.c -Iinclude -L lib -l MyCalc -o myapp

在這裡插入圖片描述

引數說明: * -I引數:指定頭文所在的資料夾名,資料夾名可以和引數貼著寫在一起 * -L引數:指定靜態庫的資料夾名 * -l引數:指定靜態庫的名字,但名字要掐頭去尾,eg:原靜態庫名字為libMyCalc.a,在指定-l引數值的時候為:-l MyCalc * -o引數:輸出編譯之後可執行檔案的名字

注意:

之所以用-l指定靜態庫的名字,是因為lib目錄下可能有多個靜態庫檔案,但是我們只需要使用其中的某一個,此時可以用這種方法指定相應的靜態庫檔案。

二、靜態庫相關檔案檢視

1、nm命令檢視靜態庫

可以使用nm命令檢視靜態庫檔案中具體打包了哪些二進位制檔案.o檔案

在這裡插入圖片描述

2、nm命令檢視生成的可執行檔案

在這裡插入圖片描述

T:代表的含義是把add程式碼會被放到程式碼區

2.2 靜態庫的優缺點

1、通過靜態庫生成可執行檔案

在這裡插入圖片描述

  • 靜態庫中封裝了多個.o檔案
  • main.c 中呼叫靜態庫中相應可執行檔案(二進位制檔案)中的函式
  • 圖中只調用了add.o和sub.o中的函式,因此main.c在生成可執行檔案的時候只會把靜態檔案中的add.osub.o兩個檔案打包到可執行檔案中,靜態檔案中的其他沒有用到的.o檔案不會被打包進可執行檔案中。
  • 在生成可執行檔案的時候也是以.o可執行檔案單位打包的,並不會把整個靜態檔案.a都打包到可執行檔案中。

靜態庫的優點: * 1)釋出程式的時候,不需要提供對應的庫了,因為庫已經被打包到了可執行檔案中去了。 * 2)庫的載入速度比較快,因為庫已經被打包到可執行檔案中去了。

靜態庫的缺點: * 1) 庫被打包到應用程式(最後生成的可執行檔案)中,如果庫很多的話就會導致應用程式的體積很大。 * 2)庫發生了改變,需要重新編譯程式,如果原始碼比較多,可能編譯一遍一天就過去了。

3 gcc 動態庫 / 共享庫 的製作

動態庫也叫共享庫,在windows中對用.dll檔案

3.1 動態庫 / 共享庫的製作流程

一、動態庫相關說明

1、命名規則: * 1)lib + 名字 + .so * 2)例如:libMyCalc.so

2、製作步驟: * 1)生成與位置無關的程式碼 (生成與位置無關的.o) * 2)將.o打包成共享庫(動態庫) * 3)釋出和使用共享庫:

注意: * 靜態庫生成的.o檔案是和位置有關的 * 用gcc生成和位置無關的.o檔案,需要使用引數-fPIC(常用) 或 -fpic

二、動態庫製作相關例項

在瞭解什麼叫生成和位置無關的.o檔案,我們來先了解一下虛擬地址空間在這裡插入圖片描述

linux上開啟一個執行的程式程序),作業系統就會為其分配一個(針對32位作業系統)0-4G的地址空間虛擬地址空間),虛擬地址空間不是在記憶體中,而是來自硬碟的儲存空間。

從下到上:

0-3G:是使用者區 * .text 程式碼段:存放的是程式碼 * .data :存放的是已初始化的變數 * .bss:存放的是未初始化的變數 * 堆空間: * 共享庫:動態庫的空間,每次程式執行的時候把動態庫載入到這個空間 * 棧空間:我們定義的區域性變數都是在棧空間分配的記憶體 * 命令列引數 * 環境變數

在往上 3-4G是核心區

  • 靜態庫生成與位置有關二進位制檔案(.o檔案)

    虛擬地址空間是從0開始的,生成的二進位制檔案(.o檔案)會被放到程式碼段,即.text程式碼區。生成的.o程式碼每次都被放到同一個位置,是因為使用的是絕對地址

  • 動態庫生成與位置無關二進位制檔案(.o檔案)

    動態庫 / 共享庫 在程式打包的時候並不會把.o檔案打包到可執行檔案中,只是做了一個記錄,當程式執行之後才去把動態庫載入到程式中,也就是載入到上圖中的共享庫空間,但是每次載入到共享庫空間的位置可能不同。 還是和上面靜態庫製作同樣的目錄結構:


動態庫製作例項

python Calc ├── include │ └── head.h ├── lib ├── main.c └── src ├── add.c ├── div.c ├── mul.c └── sub.c

在這裡插入圖片描述

```python shliang@shliang-vm:~/shliang/gcc_learn/Calc$ cd src/ shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls add.c div.c mul.c sub.c

1、把原始碼生成和位置無關的二進位制檔案 shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ gcc -fPIC -c *c -I ../include shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls add.c add.o div.c div.o mul.c mul.o sub.c sub.o

2、使用gcc把生成的二進位制檔案(.o檔案),打包成動態庫(.so檔案) shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ gcc -shared -o libMyCalc.so *o -Iinclude shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls add.c add.o div.c div.o libMyCalc.so mul.c mul.o sub.c sub.o

3、把生成的動態庫檔案移動到lib目錄下 shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ mv libMyCalc.so ../lib shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ cd .. shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ls include lib main.c src shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ```

引數說明: * -PIC:生成和位置無關的.o檔案 * -shared:共享,就是把.o檔案,打包成動態庫 / 共享庫

上面就已經完成動態庫的製作,然後把下面的兩個檔案釋出使用者即可呼叫 * include/head.h: 標頭檔案,定義介面API * lib/libMyCalc.so:動態庫,封裝了編譯之後的原始碼二進位制檔案

使用者使用動態庫

使用者使用動態庫和靜態庫一樣有兩種方法:

  • 使用者使用動態庫方法一:

    gcc main.c lib/libMyCalc.so -o app -Iinclude

在這裡插入圖片描述

  • 使用者使用動態庫方法二:

    gcc main.c -Iinclude -L lib -l MyCalc -o myapp

在這裡插入圖片描述

3.2 動態庫查詢不到解決方法

我們可以看到,第二種方法,至執行可執行程式的時候,提示找不到動態庫,這並不一定是動態庫檔案不存在,可能是由於連結不到

是不是真的連結不到,我們可以通過一個命令ldd:檢視可執行檔案執行的時候,依賴的所有共享庫/動態庫(.so檔案)

ldd命令使用:

ldd 可執行檔名

在這裡插入圖片描述 python shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ldd myapp linux-vdso.so.1 => (0x00007fff59d26000) # 後面的數字是庫的地址 # 提示我們自己的動態庫 / 共享庫 libMyCalc.so沒有找到 libMyCalc.so => not found # libc.so.6 是linux下的標準C庫 (寫C程式都會呼叫標準C庫裡的一些函式) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1e27462000) # 動態連結器,動態連結器的本質就是一個動態庫 /lib64/ld-linux-x86-64.so.2 (0x00007f1e2782c000) shliang@shliang-vm:~/shliang/gcc_learn/Calc$

在這裡插入圖片描述

如上圖:可執行程式./a.out在執行的時候,呼叫需要呼叫動態庫libmytest.so,但是實際上這個呼叫是通過動態連結器的來呼叫的。動態庫就是通過動態連結器--/lib64/ld-linux-x86-64.so.2載入到我們的可執行程式(應用程式)中的。

那麼動態連結器是-- /lib64/ld-linux-x86-64.so.2是通過什麼規則查詢可執行檔案在執行時,需要的動態檔案的呢?

其實就是通過環境變數

在linux下檢視環境變數:

echo $PATH

在這裡插入圖片描述

當然PATH下並不是存放動態庫的路徑,這裡只是做一個演示,如何檢視環境變數。

一、動態庫查詢不到解決方法一(不推薦——不允許使用

把自己製作的動態庫放到根目錄下的系統動態庫中,即/lib目錄下

sudo cp ./lib/libMyCalc.so /lib

在這裡插入圖片描述 從上面的結果可以看到,把自己製作的動態庫拷貝到系統動態庫中之後,動態連結器根據環境變數就可以找到這個動態庫,然後正確載入到可執行程式中。

注意:

這種方法一般不會使用的,因為如果你的動態庫的名字和系統中某個動態庫的名字一樣,就可能會導致系統奔潰的!!!這裡只是做一個演示,證明動態連結器是根據環境變數去查詢要載入的動態庫。

二、動態庫查詢不到解決方法二(臨時測試設定

通過把動態庫新增到動態庫環境變數中,即:LD_LIBRARY_PATH

在這裡插入圖片描述

使用export新增環境變數,把當前動態庫所在的位置(資料夾位置)新增到LD_LIBRARY_PATH變數中,可執行程式在執行的時候會在預設的動態庫之前從LD_LIBARAY_PATH變數中查詢有沒有所需動態庫。 ```python

export

export LD_LIBARAY_PATH=./lib ```

注意:

但是,這種方法只是臨時的,當我們關閉終端,下次再執行程式又會提示找不到動態庫。因此,這鐘方法一般是再開發動態庫的過程中,用於臨時的測試

三、動態庫查詢不到解決方法三(永久設定——不常用

當前使用者家目錄home)下的.bashrc檔案中配置LD_LIBRARY_PAHT環境變數。

```python cd ~ vi .bashrc

然後再最後一行新增一個環境變數,如果沒有就建立(Shift+G跳到最後一行)

然後把動態庫的絕對路徑賦值給該變數

export LD_LIBARAY_PATH=/home/shliang/shliang/gcc_learn/Calc/lib

儲存退出,用source啟用配置,如果不啟用需要重啟終端,因為終端每次重啟都會從.bashrc中載入一次配置

source .bashrc ``` 在這裡插入圖片描述

上面新增完環境變數之後就可以找到動態庫了。

在這裡插入圖片描述

四、動態庫查詢不到解決方法四(永久設定

這種方法,相對與前三種複雜一些,一定要掌握,可能以後用作中用到的就是這種。做法如下:

1、需要找到動態聯結器配置檔案/etc/ld.so.conf 2、把我們自己製作的動態庫目錄的絕對路徑寫到配置檔案中 3、更新配置檔案:sudo ldconfig -v

  • ld:dynamic library 動態庫的縮寫
  • -v :是更細配置檔案的時候輸出更新資訊。

修改配置檔案的路徑位置:/etc/ld.so.conf

/home/shliang/shliang/gcc_learn/Calc/lib新增到/etc/ld.so.conf配置檔案中

之後就可以找到動態庫了,如下:

在這裡插入圖片描述

3.3 動態庫的優缺點

1、動態庫的優點 * 執行程式的體積小:程式在執行的時候採取載入動態庫,並沒有和可執行程式打包在一起 * 動態庫更新了,不需要重新編譯程式(不是絕對的,前提是函式的介面不變,內容便裡沒事)

2、動態庫的缺點 * 程式釋出的時候,需要把動態庫提供給使用者 * 動態庫沒有被打包到應用程式中,載入速度相對較慢