淺談C/C++連接庫

做者:施洪寶html

一. 說明

  • 一、本文後續代碼的編譯以及執行環境爲Centos 7.6 x86_64, g++ 4.8.5
  • 二、本文後續會用到linux下nm, ldd命令。nm用於查看文件中的符號, 例如變量, 函數名稱。ldd用於查看動態連接庫或者可執行文件的依賴庫(動態連接庫)。

二. 編譯連接

  • 一、程序員寫出的代碼爲.c或者.cpp, 這些文件須要通過: 預處理(處理代碼中的include, 宏等)、編譯(生成彙編代碼)、彙編(將彙編代碼生成二進制文件)、連接才能生成可執行程序。本文將預處理、編譯、彙編的過程都看作是編譯, 簡化讀者理解。更多細節能夠參考相關資料。
  • 二、生成可執行文件後, 經過終端進行執行
  • 三、g++參數說明,java

    • -std=c++11: 使用c++11標準
    • -o: 指定輸出文件名稱
  • 四、連接器ld參數:linux

    • -L: 指定連接時搜索的動態連接庫路徑
    • -l: 連接某個庫, 例如連接libmath.so, 寫爲-lmath

2.1 編譯

  • 一、對於c或者c++項目而言, 咱們認爲單個c或者cpp文件是一個編譯單元, 經過編譯器(gcc, g++, clang, clang++)能夠生成編譯後的二進制文件。例如: 編譯file1.cpp, 能夠生成file1.o。對於單個編譯單元而言, 裏面會有一些符號, 例如函數名稱, 變量名稱, 類名。這些符號能夠分爲三類:ios

    • 對外提供的, 也就是說其餘的編譯單元可使用的
    • 對外依賴的, 也就是說本單元須要外部的其餘編譯單元提供的符號
    • 本身內部使用的, 這種符號只有本編譯單元自身須要使用, 外部不可見
  • 二、經過nm, 咱們能夠查看某個編譯單元存在哪些符號

2.2 連接

  • 一、C/C++項目中含有不少個c文件或者cpp文件, 這些文件通過編譯生成了對應的二進制文件。須要經過連接器將這些文件連接, 進而生成可執行程序。
  • 二、linux下連接器爲ld, 利用該工具咱們能夠將這些文件連接, 進而生成可執行程序。
  • 三、在進行連接時, 每一個編譯單元須要的符號, 都須要可以找到對應的定義。例如: 某個編譯單元須要其餘編譯單元提供符號fun1, 這是一個函數, 若是連接器沒能從其餘編譯單元找到這個符號, 就會報咱們常常看到的未定義錯誤。若果出現屢次, 則會報出重複定義的錯誤。

2.3 示例

  • 一、math.h
#ifndef _MATH_H_
#define _MATH_H_

int add(int a, int b);

#endif
  • 二、math.cpp
#include "math.h"

int add(int a, int b){
    return a + b;
}
  • 三、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;
}
  • 四、生成可執行文件c++

    • 編譯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結尾)。首先咱們來看幾個問題:程序員

  • 一、什麼是靜態連接庫?什麼是動態連接庫?
  • 二、靜態連接庫如何生成?動態連接庫如何生成?
  • 三、靜態連接庫是否能夠依賴其餘的靜態連接庫? 是否能夠依賴其餘動態連接庫?
  • 四、動態連接庫是否能夠依賴其餘的靜態連接庫? 是否能夠依賴其餘的動態連接庫?
  • 五、連接靜態庫時?其依賴的庫該如何連接?
  • 六、連接動態庫時?其依賴的庫該如何連接?
  • 七、使用第三方庫時, 使用靜態連接庫仍是動態連接庫?

四. Hello World

本節以hello world爲例,shell

#include <iostream>
using namespace std;

int main(int argc, char **argv){
    cout << "hello world" << endl;
}
  • 一、編譯程序: g++ -std=c++11 -o main main.cpp
  • 二、使用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 靜態連接庫

  • 一、本節以2.3中的示例代碼爲例, 將math.h, math.cpp打包爲靜態連接庫以及動態連接庫, 在main.cpp中引用

5.1 靜態連接庫

  • 一、編譯: g++ -std=c++11 -fPIC -c math.cppbootstrap

    • fPIC用於生成位置無關的代碼, 更多細節能夠查找相關資料
  • 二、生成靜態連接庫: ar -crv libmath.a math.o
  • 三、使用這個靜態連接庫:windows

    • 使用靜態庫時, 咱們須要math.h文件, 這個文件中定義了這個庫對外提供的功能
    • 除了math.h文件, 咱們須要在連接階段連接libmath.a
  • 四、示例: main.cpp中已經導入了math.h文件, 編譯main.c並連接libmath.a, g++ -std=c++11 -o main main.cpp -L. -lmath
  • 五、ldd main能夠看出, main文件再也不依賴libmath.a文件

5.2 動態連接庫

  • 一、生成動態連接庫: g++ -std=c++11 -shared -fPIC math.cpp -o libmath.so
  • 二、使用動態連接庫:app

    • 須要使用math.h頭文件, 該文件定義了庫對外提供的功能
    • 連接階段須要連接libmath.so
  • 三、示例: g++ -std=c++11 -o main main.cpp -L. -lmath
  • 四、執行main, 會發現沒法執行
./main: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
  • 五、咱們先用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 對比

  • 一、靜態連接庫, 動態連接庫都是二進制文件(ELF格式, 詳細信息能夠查找相關資料)

從靜態連接庫生成的過程來看, 其本質就是將多個編譯單元(.o文件), 打包爲一個新的文件。連接靜態連接庫時, 會將靜態連接庫的代碼合併進程序中。

  • 二、連接動態連接庫時, 並不會將動態連接庫的內容合併進代碼中, 而是在程序執行時, 搜索動態連接庫, 再進行連接。

六. 庫之間的依賴

6.1 源代碼

  • 一、first.h
#ifndef __FIRST_H_
#define __FIRST_H_

#include <cstdio>

void first();

#endif
  • 二、first.cpp
#include"first.h"

void first()
{
    printf("This is first!\n");
}
  • 三、second.h
#ifndef __SECOND_H_
#define __SECOND_H_
 
#include <cstdio>
void second();

#endif
  • 四、second.cpp
#include"first.h"
#include"second.h"

void second()
{
    printf("This is second!\n");
    first();
}
  • 五、main.cpp
#include"second.h"
int main()
{
    second();
    return 0;
}

6.2 靜態庫依賴靜態庫

  • 一、生成libfirst.a靜態連接庫
g++ -std=c++11 -fPIC -c first.cpp
ar -crv libfirst.a first.o
  • 二、生成libsecond.a並連接libfirst.a
g++ -std=c++11 -c second.cpp -L. -lfirst
ar -crv libsecond.a second.o
  • 三、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
  • 四、解釋說明

    • 經過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)將多個靜態連接庫打包爲一個靜態連接庫
  • 五、解決方案

    • 將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 動態庫依賴靜態庫

  • 一、生成libfirst.a靜態連接庫, 這一步與5.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中了
    • 三、編譯main.cpp
    g++ -std=c++11 main.cpp -L. -lsecond -o main

    6.4 靜態庫依賴動態庫

    • 一、生成libfirst.so
    g++ -std=c++11 first.cpp -shared -fPIC -o libfirst.so
    • 二、生成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

    • 三、編譯main.cpp
    g++ -std=c++11 main.cpp -L. -lsecond -lfirst -o main

    若是沒有連接first, 會發現連接錯誤, 找不到first函數的定義

    6.5 動態庫依賴動態庫

    • 一、生成libfirst.so
    g++ -std=c++11 first.cpp -shared -fPIC -o libfirst.so
    • 二、生成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

    • 三、編譯main.cpp
    g++ -std=c++11 main.cpp -L. -lsecond -o main

    能夠看出, 可以成功編譯。
    以前講過libsecond.so須要依賴libfirst.so, 此處爲什麼咱們只連接libsecond.so也能成功呢?這裏是由於連接器會自動搜索動態連接庫的依賴庫

    七. 總結

    • 一、c或者cpp文件通過編譯、連接生成可執行文件
    • 二、單個c文件或者cpp文件是一個編譯單元。每一個編譯單元存在3種符號: 本身使用的, 依賴於外部的以及對外提供的。
    • 三、連接器是將多個編譯單元的符號相互連接以造成可執行文件。
    • 四、庫能夠分爲靜態連接庫(.a)以及動態連接庫(.so)。
    • 五、使用庫時, 除了庫文件, 還須要對應的頭文件。
    • 六、單個c文件或者cpp文件, 可能依賴其餘的庫文件, 可是在編譯時, 只須要有聲明, 並不須要有具體的定義。
    • 七、靜態庫沒有連接操做, 靜態庫只是將多個.o文件打包, 並無其餘操做。靜態庫可能依賴其餘的靜態庫或者其餘的動態庫, 用戶在使用靜態庫時, 須要手動連接這些依賴。
    • 八、動態庫有連接操做, 建立動態庫時能夠連接其餘的庫, 也能夠不連接, 若是連接靜態庫, 則會將靜態庫的內容所有放入動態庫, 若是連接動態庫, 只是放入符號, 在程序初始化時, 將依賴的這些動態庫也加載。若是這個動態庫依賴了其餘庫, 可是沒有連接, 也能夠生成動態庫, 但用戶在使用這個動態連接庫時, 須要手動連接這些依賴, 因爲使用者很難知道這些依賴, 因此一般不使用這種方式。
    • 九、整體而言, 動態庫在程序執行階段纔會裝進程序, 靜態庫則在連接階段直接放進程序。動態庫能夠由多個程序共享, 節省內存,易於升級。靜態庫外部依賴少, 更易於部署。

    八. 擴展

    • 一、動態庫升級問題?假設如今有2個程序: p1, p2, 一個動態連接庫libmath.so.1。若是如今math庫提供了新版本libmath.so.2, 程序p1須要使用libmath.so.2的新功能, p2則不想使用, 此時該如何升級math庫?

      • 若是math不兼容前一版, 則系統中須要同時存在兩個版本的math庫, p1, p2分別連接不一樣的版本
      • 若是math兼容前一版, 系統中是否能夠只保留新版的math庫呢?此時p1, p2又是否須要從新編譯呢?這個問題留給讀者自行思考。
    • 二、某個動態連接庫lib1動態連接了庫libbase, 如今應用程序中使用了lib1以及libbase, 編譯應用程序時, 是否須要連接libbase?

      • 應用程序不只須要連接lib1, 也須要連接libbase
      • 連接lib1只能保證應用程序依賴lib1的部分可以正確解析
      • 雖然lib1動態連接了libbase, 可是動態連接真正進行符號解析是在程序執行階段, 編譯階段沒法獲取libbase的相關信息, 應用程序中若是也使用了libbase中的函數, 則必須連接libbase, 不然會出現符號未定義
      • 若是lib1靜態連接了libbase, 也就是說包含了libbase中的函數, 則應用程序不須要在連接libbase
    • 三、菱形依賴問題, A依賴於B以及C, B、C都依賴於D, 可是是不一樣版本, 例如B依賴於D1, C依賴於D2, 這種狀況下如何連接?

      • D2兼容於D1(ABI層面兼容), 程序直接連接D2
      • D2不兼容於D1, 查看B是否能夠依賴D2從新編譯

    連接器的參數, 直接連接兩個版本。ld的參數–default-symver或者–version-script

    • 四、討論

      • 動態連接會有大量的依賴問題(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...

    相關文章
    相關標籤/搜索