1. 程式人生 > >連結與自定義函式名同名的庫函式

連結與自定義函式名同名的庫函式

遇到一個問題: 封裝SQLite3成靜態庫,過程中發現SQLite3的原始碼的shell.c中有main函式:

int SQLITE_CDECL main(int argc, char **argv){
  char *zErrMsg = 0;
  ShellState data;
  const char *zInitFile = 0;
  int i;
  int rc = 0;
  int warnInmemoryDb = 0;
  int readStdin = 1;
  int nCmd = 0;
  char **azCmd = 0;
  ...

將其封裝成靜態庫.a檔案自然是被使用者呼叫的,也就是說使用者也要有自己的main函式才行。如此說來,在使用該.a的專案中就有了兩個main函式,那應該是一定編譯不過的,然而事實並非如此,程式能正常編譯且符合設計邏輯執行,這涉及gcc中連結器ld的連結過程,於是通過自行編寫測試程式試驗一番。

新建如下檔案:
這裡寫圖片描述

addLib.和subLib.將編譯為庫函式使用,其實現為:

//addLib.h
#ifndef __ADDLIB_H__
#define __ADDLIB_H__

int add(int a, int b);

#endif /* __ADDLIB_H__ */

//addLib.c
#include <stdio.h>
#include "addLib.h"

int add(int a, int b)
{
    printf("%d + %d = %d\n", a, b, a + b);
    return 0;
}
//subLib.h
#ifndef __SUBLIB_H__
#define __SUBLIB_H__ #include <stdio.h> void sub(int a, int b); #endif /* __SUBLIB_H__ */ //subLib.c #include "subLib.h" void sub(int a, int b) { printf("%d - %d = %d\n", a, b, a - b); }

而main.c是對該庫函式的呼叫:

#include <stdio.h>
#include "addLib.h"
#include "subLib.h"

int main(void)
{
    add(4
, 6); return 0; }

簡單寫個makefile:

all : addLib.o subLib.o
    ar -r libcal.a addLib.o subLib.o 
    gcc main.c -L./ -lcal

addLib.o : addLib.c
    gcc -c $<

subLib.o : subLib.c
    gcc -c $<

clean:
    rm *.o *.a -rf

編譯通過:
這裡寫圖片描述

執行正確:
這裡寫圖片描述

Linux的nm可列出目標檔案的符號清單,通過它檢視libcal.a:
這裡寫圖片描述

圖中的T表示該符號位於程式碼段,U表示在當前檔案是未定義的,即該符號是定義在別的檔案。

在這個例子中,連結的命令為

gcc main.c -L./ -lcal

其中gcc main.c其實是做了編譯和連結,所以最後一步的連結操作是

gcc main.o -L./ -lcal

連結器從左向右掃描連結命令列引數中的.o和.a,最終目的是確定“最終.o檔案集合”和各個.o檔案中的外部符號的定義位置。
以本程式為例,
(1)首先是掃描main.o,main.o會無條件被加入到“最終.o檔案集合”中,該檔案引用了main符號,它是程式開始執行的符號,會將main放入“已定義符號表”中,接著又引用了外部符號符號add,因此會將add放入“未定義符號表”中。
(2)掃描到libcal.a中addLib.o,“未定義符號表”中存放的add在addLib.o中找到了定義,於是將addLib.o檔案加入到“最終.o檔案集合”中,且將add符號從“未定義符號表”中轉換到“已定義符號表”中。但是在掃描addLib.o中,發現了外部符號printf,於是printf符號被放入“未定義符號表”。
(3)掃描到libcal.a中的subLib.o,此時“未定義符號表”中存放的是printf,並不能在subLib.o中找到定義,直接略過該檔案,所以subLib.o並不加入到“最終.o檔案集合”中,其中的符號資訊也沒有被載入“未定義符號表”和“已定義符號表”。
(4)“未定義符號表”裡仍然存在printf符號,所以連結器會繼續掃描,它往哪裡掃描?連結命令上寫到-lcal就截止了,其實c程式預設會去連結標準c庫的,找到標準c庫的定義printf符號的.o檔案,並把該檔案加入“最終.o檔案集合”中,連結操作至此完成。注意連結完成的標誌是“未定義符號表”中為空,也就是不能出現未定義的符號。

如上分析,因為main.c程式中並沒有呼叫sub函式,subLib.o並不會被加入到“最終.o檔案集合,那麼在subLib.c中加上如下程式碼且不呼叫,同樣是能編譯通過咯:

void sub(int a, int b)
{
    printf("%d - %d = %d\n", a, b, a - b);
}

int main(void)
{
    printf("in lib mian!\n");
    return 0;
}

果然如此:
這裡寫圖片描述
main.c的main符號在庫函式外部,連結器已經認識這個符號了,會將其放入“已定義的符號表”中,所以不會去掃描cal庫內的subLib.o裡的main符號。
再做改動,在main.c的main函式中呼叫subLib.c的sub函式:

int main(void)
{
    add(4, 6);
    sub(9, 2);

    return 0;
}

再編譯就出現重定義錯誤了:
這裡寫圖片描述
原因也很簡單,因為main函式呼叫了sub函式導致subLib.c中的sub符號一開始放入到“未定義符號集”,再找到subLib.o後,該符號會被放入到“已定義符號集”,subLib.o也會隨之加入“最終.o檔案集合”中,問題就出現了:該集合中main.o和subLib.o均包含了main符號,自然就報錯了!

綜上所述,我們可以推論,連結器對目標檔案(.o)和庫檔案(.a)是區別對待的。我們知道,可執行程式是由一系列的.o檔案“合併”而成,以靜態連結為例,“最終.o檔案集合”中除了包含我們顯示提供的由.c編譯而來.o檔案外,還有從.a庫檔案提取出來的.o檔案,可執行程式對由.c編譯而成的.o檔案無條件的包含到“最終.o檔案集合”中,而對從.a庫提取的.o並非全盤提取,而是“按需”提取,“按需”是根據“未定義符號表”中的符號去提取的。這也符合軟體設計的思想,儘可能使得可執行檔案的size小。

實際開發中,我還遇到這樣一個問題: 可執行程式連結了n個靜態庫和一個動態庫,動態庫想要呼叫靜態庫裡的某個函式,執行時報錯找不到該符號,通過前面的學習,可以分析原因就是在於,可執行程式雖然連結了這個靜態庫但並沒有使用靜態庫的某個.o檔案,導致.o沒有真正被連結到可執行程式,而該.o檔案的某個符號又要被動態庫使用,這就出現了上面的報錯了。解決辦法無非就是讓可執行程式去呼叫一下該符號了。