作者:施洪寶
一. 說明
- 1、本文後續代碼的編譯以及執行環境爲Centos 7.6 x86_64, g++ 4.8.5
- 2、本文後續會用到linux下nm, ldd命令。nm用於查看文件中的符號, 例如變量, 函數名稱。ldd用於查看動態鏈接庫或者可執行文件的依賴庫(動態鏈接庫)。
二. 編譯鏈接
- 1、程序員寫出的代碼爲.c或者.cpp, 這些文件需要經過: 預處理(處理代碼中的include, 宏等)、編譯(生成彙編代碼)、彙編(將彙編代碼生成二進制文件)、鏈接才能生成可執行程序。本文將預處理、編譯、彙編的過程都看做是編譯, 簡化讀者理解。更多細節可以參考相關資料。
- 2、生成可執行文件後, 通過終端進行執行
-
3、g++參數說明,
- -std=c++11: 使用c++11標準
- -o: 指定輸出文件名稱
-
4、鏈接器ld參數:
- -L: 指定鏈接時搜索的動態鏈接庫路徑
- -l: 鏈接某個庫, 例如鏈接libmath.so, 寫爲-lmath
2.1 編譯
-
1、對於c或者c++項目而言, 我們認爲單個c或者cpp文件是一個編譯單元, 通過編譯器(gcc, g++, clang, clang++)可以生成編譯後的二進制文件。例如: 編譯file1.cpp, 可以生成file1.o。對於單個編譯單元而言, 裏面會有一些符號, 例如函數名稱, 變量名稱, 類名。這些符號可以分爲三類:
- 對外提供的, 也就是說其他的編譯單元可以使用的
- 對外依賴的, 也就是說本單元需要外部的其他編譯單元提供的符號
- 自己內部使用的, 這種符號只有本編譯單元自身需要使用, 外部不可見
- 2、通過nm, 我們可以查看某個編譯單元存在哪些符號
2.2 鏈接
- 1、C/C++項目中含有很多個c文件或者cpp文件, 這些文件經過編譯生成了對應的二進制文件。需要通過鏈接器將這些文件鏈接, 進而生成可執行程序。
- 2、linux下鏈接器爲ld, 利用該工具我們可以將這些文件鏈接, 進而生成可執行程序。
- 3、在進行鏈接時, 每個編譯單元需要的符號, 都需要能夠找到對應的定義。例如: 某個編譯單元需要其他編譯單元提供符號fun1, 這是一個函數, 如果鏈接器沒能從其他編譯單元找到這個符號, 就會報我們經常看到的未定義錯誤。若果出現多次, 則會報出重複定義的錯誤。
2.3 示例
- 1、math.h
#ifndef _MATH_H_
#define _MATH_H_
int add(int a, int b);
#endif
- 2、math.cpp
#include "math.h"
int add(int a, int b){
return a + b;
}
- 3、main.cpp
#include <iostream>
#include "math.h"
using namespace std;
int main(int argc, char **argv){
int a = 100, b = 200;
int result = add(a, b);
cout << result << endl;
}
-
4、生成可執行文件
- 編譯math.cpp: g++ -std=c++11 -c math.cpp, 生成math.o
- 編譯main.cpp: g++ -std=c++11 -c main.cpp, 生成main.o
- 生成可以執行的文件: g++ -v math.o main.o -o main, 可以看到g++的編譯鏈接過程
Using built-in specs.
COLLECT_GCC=g++
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.5/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'main' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/collect2 --build-id --no-add-needed --eh-frame-hdr --hash-style=gnu -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o main /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../.. main.o math.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crtn.o
- 其中最後一行調用collect2(對ld進行了包裝)會執行真正的鏈接操作, 我們直接調用這一句也可以生成main可執行文件
- 可以看出linux下的鏈接操作比較複雜, 不是簡單的ld main.o math.o即可成功的。
三. 問題
通過上面的介紹, 我們知道一個c/cpp文件通過編譯鏈接, 最終生成可執行文件。無論任何語言, 程序員在寫代碼時, 都不可避免需要使用到庫, 本文主要介紹C/C++中的庫, 總體而言, 我們將這些庫分爲靜態鏈接庫(通常以.a結尾),動態鏈接庫(通常以.so結尾)。首先我們來看幾個問題:
- 1、什麼是靜態鏈接庫?什麼是動態鏈接庫?
- 2、靜態鏈接庫如何生成?動態鏈接庫如何生成?
- 3、靜態鏈接庫是否可以依賴其他的靜態鏈接庫? 是否可以依賴其他動態鏈接庫?
- 4、動態鏈接庫是否可以依賴其他的靜態鏈接庫? 是否可以依賴其他的動態鏈接庫?
- 5、鏈接靜態庫時?其依賴的庫該如何鏈接?
- 6、鏈接動態庫時?其依賴的庫該如何鏈接?
- 7、使用第三方庫時, 使用靜態鏈接庫還是動態鏈接庫?
四. Hello World
本節以hello world爲例,
#include <iostream>
using namespace std;
int main(int argc, char **argv){
cout << "hello world" << endl;
}
- 1、編譯程序: g++ -std=c++11 -o main main.cpp
- 2、使用ldd查看main的依賴: ldd main
linux-vdso.so.1 => (0x00007ffcf53fa000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f7828b3b000)
libm.so.6 => /lib64/libm.so.6 (0x00007f7828839000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f7828623000)
libc.so.6 => /lib64/libc.so.6 (0x00007f7828256000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7828e42000)
- 可以看出, 最簡單的hello world程序也需要鏈接一些庫
- 上述的幾種鏈接庫, 感興趣的可以逐個研究
五. 動態鏈接庫 vs 靜態鏈接庫
- 1、本節以2.3中的示例代碼爲例, 將math.h, math.cpp打包爲靜態鏈接庫以及動態鏈接庫, 在main.cpp中引用
5.1 靜態鏈接庫
-
1、編譯: g++ -std=c++11 -fPIC -c math.cpp
- fPIC用於生成位置無關的代碼, 更多細節可以查找相關資料
- 2、生成靜態鏈接庫: ar -crv libmath.a math.o
-
3、使用這個靜態鏈接庫:
- 使用靜態庫時, 我們需要math.h文件, 這個文件中定義了這個庫對外提供的功能
- 除了math.h文件, 我們需要在鏈接階段鏈接libmath.a
- 4、示例: main.cpp中已經導入了math.h文件, 編譯main.c並鏈接libmath.a, g++ -std=c++11 -o main main.cpp -L. -lmath
- 5、ldd main可以看出, main文件不再依賴libmath.a文件
5.2 動態鏈接庫
- 1、生成動態鏈接庫: g++ -std=c++11 -shared -fPIC math.cpp -o libmath.so
-
2、使用動態鏈接庫:
- 需要使用math.h頭文件, 該文件定義了庫對外提供的功能
- 鏈接階段需要鏈接libmath.so
- 3、示例: g++ -std=c++11 -o main main.cpp -L. -lmath
- 4、執行main, 會發現無法執行
./main: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
- 5、我們先用ldd 查看main的依賴庫:
linux-vdso.so.1 => (0x00007ffd2adde000)
libmath.so => not found
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fd3b7ee6000)
libm.so.6 => /lib64/libm.so.6 (0x00007fd3b7be4000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fd3b79ce000)
libc.so.6 => /lib64/libc.so.6 (0x00007fd3b7601000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd3b81ed000)
很奇怪, libmath.so沒有找到, 我們在第三步編譯時明明將這個庫加入進去了。這個是由於, 在鏈接階段, 鏈接器可以在當前目錄找到libmath.so。執行階段, 搜索動態鏈接庫時, 並沒有包含當前目錄, 所以報錯。我們可以通過export LD_LIBRARY_PATH=/libpath將libmath.so所在路徑放入動態鏈接庫的搜索路徑中。此時即可成功執行。
5.3 對比
- 1、靜態鏈接庫, 動態鏈接庫都是二進制文件(ELF格式, 詳細信息可以查找相關資料)
從靜態鏈接庫生成的過程來看, 其本質就是將多個編譯單元(.o文件), 打包爲一個新的文件。鏈接靜態鏈接庫時, 會將靜態鏈接庫的代碼合併進程序中。
- 2、鏈接動態鏈接庫時, 並不會將動態鏈接庫的內容合併進代碼中, 而是在程序執行時, 搜索動態鏈接庫, 再進行鏈接。
六. 庫之間的依賴
6.1 源代碼
- 1、first.h
#ifndef __FIRST_H_
#define __FIRST_H_
#include <cstdio>
void first();
#endif
- 2、first.cpp
#include"first.h"
void first()
{
printf("This is first!\n");
}
- 3、second.h
#ifndef __SECOND_H_
#define __SECOND_H_
#include <cstdio>
void second();
#endif
- 4、second.cpp
#include"first.h"
#include"second.h"
void second()
{
printf("This is second!\n");
first();
}
- 5、main.cpp
#include"second.h"
int main()
{
second();
return 0;
}
6.2 靜態庫依賴靜態庫
- 1、生成libfirst.a靜態鏈接庫
g++ -std=c++11 -fPIC -c first.cpp
ar -crv libfirst.a first.o
- 2、生成libsecond.a並鏈接libfirst.a
g++ -std=c++11 -c second.cpp -L. -lfirst
ar -crv libsecond.a second.o
- 3、main.cpp中使用libsecond.a
執行: g++ -std=c++11 main.cpp -L. -lsecond -o main
會出現以下錯誤:
./libsecond.a(second.o): In function second()': second.cpp:(.text+0xf): undefined reference tofirst()’
collect2: error: ld returned 1 exit status
-
4、解釋說明
- 通過nm, 我們查看libsecond.a中的符號, 找出未定義的符號, 執行nm -u libsecond.a, 即可發現first並沒有定義(編譯器編譯後的符號並不是first, 我這裏是_Z5firstv)。我們明明在生成libsecond.a時鏈接了libfirst.a?
- 主要的原因是: 生成靜態鏈接庫時, 只是將second.cpp生成的second.o打包, 並沒有真正的將libfirst.a中的內容鏈接進libsecond.a
- 靜態庫不與其他靜態庫鏈接。我們使用archiver工具(例如Linux上的ar)將多個靜態鏈接庫打包爲一個靜態鏈接庫
-
5、解決方案
- 將first.cpp, second.cpp打包爲一個靜態鏈接庫: g++ -std=c++11 -fPIC -c first.cpp second.cpp, ar -crv libsecond.a first.o second.o。main中可以直接鏈接libsecond.a即可
同時鏈接libsecond.a, libfirst.a
6.3 動態庫依賴靜態庫
- 1、生成libfirst.a靜態鏈接庫, 這一步與5.2節相同
- 2、生成libsecond.so靜態鏈接libfirst.a
g++ -std=c++11 second.cpp -fPIC -shared -o libsecond.so -L. -lfirst
- nm -u libseond.so, 我們可以看出, 並沒有出現first, 也就是說, libfirst.a已經被鏈接進libsecond.so中了
- 3、編譯main.cpp
g++ -std=c++11 main.cpp -L. -lsecond -o main
6.4 靜態庫依賴動態庫
- 1、生成libfirst.so
g++ -std=c++11 first.cpp -shared -fPIC -o libfirst.so
- 2、生成libsecond.a鏈接libfirst.so
g++ -std=c++11 -c second.cpp -fPIC -L. -lfirst
ar crv libsecond.a second.o
nm -u libsecond.a, 可以看到_Z5firstv, 說明並沒有將libfirst.so中包含進libsecond.a
- 3、編譯main.cpp
g++ -std=c++11 main.cpp -L. -lsecond -lfirst -o main
如果沒有鏈接first, 會發現鏈接錯誤, 找不到first函數的定義
6.5 動態庫依賴動態庫
- 1、生成libfirst.so
g++ -std=c++11 first.cpp -shared -fPIC -o libfirst.so
- 2、生成libsecond.so鏈接libfirst.so
g++ -std=c++11 second.cpp -shared -fPIC -o libsecond.so -L. -lfirst
nm -u libsecond.so, 可以看到_Z5firstv, 這個就是first函數
ldd libsecond.so, 也可以看到libfirst.so
可以看出, 使用libsecond.so時, 仍然需要libfirst.so
- 3、編譯main.cpp
g++ -std=c++11 main.cpp -L. -lsecond -o main
可以看出, 能夠成功編譯。
之前講過libsecond.so需要依賴libfirst.so, 此處爲何我們只鏈接libsecond.so也能成功呢?這裏是因爲鏈接器會自動搜索動態鏈接庫的依賴庫
七. 總結
- 1、c或者cpp文件經過編譯、鏈接生成可執行文件
- 2、單個c文件或者cpp文件是一個編譯單元。每個編譯單元存在3種符號: 自己使用的, 依賴於外部的以及對外提供的。
- 3、鏈接器是將多個編譯單元的符號相互鏈接以形成可執行文件。
- 4、庫可以分爲靜態鏈接庫(.a)以及動態鏈接庫(.so)。
- 5、使用庫時, 除了庫文件, 還需要對應的頭文件。
- 6、單個c文件或者cpp文件, 可能依賴其他的庫文件, 但是在編譯時, 只需要有聲明, 並不需要有具體的定義。
- 7、靜態庫沒有鏈接操作, 靜態庫只是將多個.o文件打包, 並沒有其他操作。靜態庫可能依賴其他的靜態庫或者其他的動態庫, 用戶在使用靜態庫時, 需要手動鏈接這些依賴。
- 8、動態庫有鏈接操作, 創建動態庫時可以鏈接其他的庫, 也可以不鏈接, 如果鏈接靜態庫, 則會將靜態庫的內容全部放入動態庫, 如果鏈接動態庫, 只是放入符號, 在程序初始化時, 將依賴的這些動態庫也加載。如果這個動態庫依賴了其他庫, 但是沒有鏈接, 也可以生成動態庫, 但用戶在使用這個動態鏈接庫時, 需要手動鏈接這些依賴, 由於使用者很難知道這些依賴, 所以通常不使用這種方式。
- 9、總體而言, 動態庫在程序執行階段纔會裝進程序, 靜態庫則在鏈接階段直接放進程序。動態庫可以由多個程序共享, 節省內存,易於升級。靜態庫外部依賴少, 更易於部署。
八. 擴展
-
1、動態庫升級問題?假設現在有2個程序: p1, p2, 一個動態鏈接庫libmath.so.1。如果現在math庫提供了新版本libmath.so.2, 程序p1需要使用libmath.so.2的新功能, p2則不想使用, 此時該如何升級math庫?
- 如果math不兼容前一版, 則系統中需要同時存在兩個版本的math庫, p1, p2分別鏈接不同的版本
- 如果math兼容前一版, 系統中是否可以只保留新版的math庫呢?此時p1, p2又是否需要重新編譯呢?這個問題留給讀者自行思考。
-
2、某個動態鏈接庫lib1動態鏈接了庫libbase, 現在應用程序中使用了lib1以及libbase, 編譯應用程序時, 是否需要鏈接libbase?
- 應用程序不僅需要鏈接lib1, 也需要鏈接libbase
- 鏈接lib1只能保證應用程序依賴lib1的部分能夠正確解析
- 雖然lib1動態鏈接了libbase, 但是動態鏈接真正進行符號解析是在程序執行階段, 編譯階段無法獲取libbase的相關信息, 應用程序中如果也使用了libbase中的函數, 則必須鏈接libbase, 否則會出現符號未定義
- 如果lib1靜態鏈接了libbase, 也就是說包含了libbase中的函數, 則應用程序不需要在鏈接libbase
-
3、菱形依賴問題, A依賴於B以及C, B、C都依賴於D, 但是是不同版本, 例如B依賴於D1, C依賴於D2, 這種情況下如何鏈接?
- D2兼容於D1(ABI層面兼容), 程序直接鏈接D2
- D2不兼容於D1, 查看B是否可以依賴D2重新編譯
鏈接器的參數, 直接鏈接兩個版本。ld的參數–default-symver或者–version-script
-
4、討論
- 動態鏈接會有大量的依賴問題(windows dll hell)
- 由於採用模塊化, 又允許升級單個模塊, 菱形依賴問題對於很多語言都是存在的
- rust, go等語言都開始採用源碼編譯的方式, 解決依賴問題
九. 參考
http://blog.chinaunix.net/uid...
https://www.cnblogs.com/fnlin...
https://blog.csdn.net/coolwat...
https://blog.habets.se/2012/0...