Linux中gcc的編譯、靜態庫、動態庫
文章目錄:
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
上面的四個過程也可以用一個命令
執行,直接生成可執行
的文件:
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
文件,內容如下:
#include <stdio.h>
// 雙引號導入的頭文件是自己寫的
#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
頭文件,內容如下:
#ifndef __HEAD_H_
#define __HEAD_H_
#define NUM1 10
#define NUM2 20
#endif
兩個文件的層級結構,同級目錄:
├── 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
差不多。
二、執行文件和頭文件同級目錄
目錄層級結構:
├── 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
的宏定義註釋掉
#include <stdio.h>
// 雙引號導入的頭文件是自己寫的
#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;
}
然後再執行:
>>>gcc sum.c -o sum
>>>./sum
結果:
並不會輸出print打印的信息了,如果再次打印出信息呢,此時可以通過參數
-D
,在執行命令的時候給程序指定一個宏
,如下:
>>>gcc sum.c -o sum -D DEBUG
>>>./sum
此時就可以打印出printf()
信息了。
總結:
-D
參數的作用:不在程序中定義宏,在程序編譯的時候定義。不指定,在程序預處理的時候,printf()就會被刪掉了。
2、-O參數:程序預處理的時候對代碼優化
在程序預處理的時候對代碼進行優化,把冗餘的代碼去掉,有三個優化等級:
- -O1:優化等級低
- -O2:優化等級中
- -O3:優化等級高
舉個例子:
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
是在頭文件
中體現出來的。
實例:
目錄結構:
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
實現的是加法運算:
#include "head.h"
int add(int a, int b)
{
int result = a + b;
return result;
}
頭文件 include/head.h
實現是對源代碼調用的接口API
:
#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是對頭文件調用,然後調用靜態文件,對算法的使用,但是並不知道算法的具體實現源代碼
#include <stdio.h>
#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
文件夾下找靜態文件(打包的二進制文件——即:加法算法的具體實現)
下面是具體的製作流程:
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.o
和sub.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文件
打包到可執行文件
中,只是做了一個記錄
,當程序運行之後
纔去把動態庫加載到程序中,也就是加載到上圖中的共享庫
空間,但是每次加載到共享庫
空間的位置可能不同
。
還是和上面靜態庫製作同樣的目錄結構:
動態庫製作實例
Calc
├── include
│ └── head.h
├── lib
├── main.c
└── src
├── add.c
├── div.c
├── mul.c
└── sub.c
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 可執行文件名
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
變量中查找有沒有所需動態庫。
# export
export LD_LIBARAY_PATH=./lib
注意:
但是,這種方法只是
臨時的
,當我們關閉終端,下次再執行程序又會提示找不到動態庫。因此,這鐘方法一般是再開發動態庫
的過程中,用於臨時的測試
。
三、動態庫查找不到解決方法三(永久設置——不常用
)
在當前用戶
的家目錄
(home
)下的.bashrc
文件中配置LD_LIBRARY_PAHT
環境變量。
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、動態庫的缺點
- 程序發佈的時候,需要把動態庫提供給用戶
- 動態庫沒有被打包到應用程序中,加載速度相對較慢
♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠