編譯期連結時共享庫搜尋路徑優先順序實驗

paw5zx發表於2024-07-29

目錄
  • 前言
  • 實驗環境
  • 目錄說明
  • 準備工作
  • 單獨測試
    • 不配置路徑
    • 預設路徑
    • LIBRARY_PATH
    • -L
  • 優先順序測試
    • 預設路徑和LIBRARY_PATH
    • -L和預設路徑
  • DEBUG模式
    • 編譯器配置詳細資訊
    • 連結器詳細資訊
    • DEBUG總結
    • 驗證
  • 預設路徑>LIBRARY_PATH原因
  • 小結
  • 附錄
    • 庫檔案原始碼
    • 主程式原始碼
    • makefile

前言

《共享庫連結和載入時的路徑搜尋優先順序》中提到,使用g++時,共享庫在編譯期連結時的庫路徑搜尋優先順序為:-L指定的路徑>LIBRARY_PATH記錄的路徑>預設路徑

本實驗分三步驗證上述結論
①單獨測試每種方法指定的路徑的可行性
②對比測試三種方法間的優先順序
③使用DEBUG模式,檢視連結器輸出的詳細資訊,二次驗證上述結論

值得注意的是,我看網上都說LIBRARY_PATH指定的路徑優先順序要大於預設路徑的優先順序,但是就我的測試結果來看,結論是相反的(可能是我使用了g++而不是直接使用底層的ld?)。

實驗環境

作業系統:Ubuntu 20.04
編譯器:g++-11.4.0
make:GNU Make 4.2.1

目錄說明

專案的目錄結構如下:

.
├── lib
├── obj
├── libhello.cpp
├── libhello_alt.cpp
├── main.cpp
└── makefile

其中:

  • lib為存放共享庫的資料夾
  • obj為存放可重定位目標檔案的資料夾
  • libhello.cpplibhello_alt.cpp為共享庫原始碼(用於模擬不同版本的共享庫),他們中都只有一個hello函式,兩個hello函式的函式簽名完全相同。其中libhello.cpp將被編譯為libhello.so.1.1.0(soname為libhello.so.1),libhello_alt.cpp將被編譯為libhello.so.2.1.0(soname為libhello.so.2
  • mian.cpp為主函式,其中呼叫了hello函式
  • makefile為自動化構建指令碼

在附錄中,我將提供本次實驗涉及到的程式碼。

準備工作

在終端中進入專案路徑,並輸入make,會在./lib下生成libhello.so.1.1.0libhello.so.2.1.0,在./obj下生成main.o
生成後專案的目錄結構如下:

.
├── lib
│   ├── libhello.so.1.1.0	
│   └── libhello.so.2.1.0
├── obj
│   └── main.o
├── libhello.cpp
├── libhello_alt.cpp
├── main.cpp
└── makefile

單獨測試

不配置路徑

不做任何路徑的配置並且不在預設路徑下放置libhello.so檔案,檢視是否可以將main.ohello的共享庫檔案連結成功。

直接使用makefile中預設好的命令即可完成上述操作:

make main_none

輸出:

可以看到由於我們沒有配置任何額外的搜尋路徑,並且沒有在預設搜尋路徑下放置libhello.so檔案,連結器就找不到相應的共享庫檔案,就會連結失敗。

單次實驗結束後,使用make clean命令清除本次實驗生成的檔案,然後再次使用make命令重新生成共享庫檔案和可重定位目標檔案。(每次做完一個小實驗,都要重複此步驟,後不贅述)

預設路徑

libhello.so.1.1.0複製至預設搜尋路徑/usr/lib,並在/usr/lib下建立一個軟連結(libhello.so)指向它,然後進行連結操作,檢視是否可以將main.ohello的共享庫檔案連結成功。

直接使用makefile中預設好的命令即可完成上述操作:

make main_default

輸出:

沒有報錯。

然後使用readelf -d檢視可執行檔案的動態段資訊,可見連結成功,共享庫的soname已經被寫入到可執行檔案的動態段資訊中了。

LIBRARY_PATH

建立路徑/opt/hellolib,將libhello.so.1.1.0複製至/opt/hellolib,並在/opt/hellolib下建立一個軟連結(libhello.so)指向它。然後將/opt/hellolib新增至LIBRARY_PATH並進行main.ohello的共享庫檔案的連結操作,檢視是否可以連結成功。

直接使用makefile中預設好的命令即可完成上述操作:

make main_library_path

輸出:

沒有報錯。

然後使用readelf -d檢視可執行檔案的動態段資訊,可見連結成功,共享庫的soname已經被寫入到可執行檔案的動態段資訊中了。

-L

建立路徑/opt/hellolib,將libhello.so.1.1.0複製至/opt/hellolib,並在/opt/hellolib下建立一個軟連結(libhello.so)指向它,然後新增連結選項-L/opt/hellolib並進行連結操作,檢視是否可以將main.ohello的共享庫檔案連結成功。

直接使用makefile中預設好的命令即可完成上述操作:

make main_l

輸出:

沒有報錯。

然後使用readelf -d檢視可執行檔案的動態段資訊,可見連結成功,共享庫的soname已經被寫入到可執行檔案的動態段資訊中了。

優先順序測試

預設路徑和LIBRARY_PATH

  • ①將libhello.so.2.1.0複製至預設搜尋路徑/usr/lib,並在/usr/lib下建立一個軟連結(libhello.so)指向它。
  • ②建立路徑/opt/hellolib,將libhello.so.1.1.0複製至/opt/hellolib,並在/opt/hellolib下建立一個軟連結(libhello.so)指向它。
  • ③將/opt/hellolib新增至LIBRARY_PATH並進行main.ohello的共享庫檔案的連結操作。

直接使用makefile中預設好的命令即可完成上述操作:

make cmp_default_libpath

輸出:

然後使用readelf -d檢視可執行檔案的動態段資訊,可見連結成功,並且連結的是預設路徑下的共享庫檔案libhello.so.2.1.0(其soname為libhello.so.2)。因此可以得出結論:預設路徑搜尋優先順序要高於LIBRARY_PATH指定的路徑的搜尋優先順序。

對於上述結論,將會在後文的DEBUG模式中給出更詳細的驗證。

-L和預設路徑

  • ①建立路徑/opt/hellolib,將libhello.so.2.1.0複製至/opt/hellolib,並在/opt/hellolib下建立一個軟連結(libhello.so)指向它。
  • ②將libhello.so.1.1.0複製至預設搜尋路徑/usr/lib,並在/usr/lib下建立一個軟連結(libhello.so)指向它。
  • ③新增連結選項-L/opt/hellolib並進行main.ohello的共享庫檔案的連結操作。

直接使用makefile中預設好的命令即可完成上述操作:

make cmp_l_default

輸出:

然後使用readelf -d檢視可執行檔案的動態段資訊,可見連結成功,並且連結的是-L指定路徑下的共享庫檔案libhello.so.2.1.0(其soname為libhello.so.2)。因此可以得出結論:-L指定路徑搜尋優先順序要高於預設搜尋路徑的搜尋優先順序。

對於上述結論,將會在後文的DEBUG模式中給出更詳細的驗證。

DEBUG模式

在makefile中我新增了一個用於對比三種路徑優先順序的目標cmp_all,其中

  • -L指定路徑為/opt/hellolib_L
  • 預設路徑為/usr/lib
  • LIBRARY_PATH指定路徑為/opt/hellolib.so檔案(libhello.so.1.1.0的軟連結)僅放置於此路徑下。

此外我還預設了一個DEBUG模式,開啟DEBUG模式可以檢視編譯過程的詳細資訊,開啟的方法就是在命令後面新增DEBUG_MODE=1,例如:

make cmp_all DEBUG_MODE=1

下面我們就使用DEBUG模式執行cmp_all檢視其輸出(輸出資訊很多,我擷取關鍵部分講解):

編譯器配置詳細資訊

我們先看一下gcc在編譯過程中輸出的編譯器配置詳細資訊:

圖片中的文字內容如下:

LIBRARY_PATH=
	/usr/lib/gcc/x86_64-linux-gnu/11/:
	/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/:
	/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib/:
	/lib/x86_64-linux-gnu/:
	/lib/../lib/:
	/usr/lib/x86_64-linux-gnu/:
	/usr/lib/../lib/:
	/opt/hellolib/:
	/usr/lib/gcc/x86_64-linux-gnu/11/../../../:
	/lib/:
	/usr/lib/

我們可以發現編譯器列出了系統環境變數LIBRARY_PATH的內容,包含:

  • ①我們向環境變數新增的/opt/hellolib/,其所處位置應該是由編譯器規定的
  • ②系統預設的庫路徑(/usr/lib/lib),位於最後
  • ③根據編譯器配置自動新增的路徑,如/usr/lib/gcc/x86_64-linux-gnu/11/

然後再往下看,COLLECT_GCC_OPTIONS列出了傳遞給g++的一些選項:

圖片中的文字內容如下(省略了一部分不需要關注的):

COLLECT_GCC_OPTIONS=...
	-L/opt/hellolib_L
	-L/usr/lib/gcc/x86_64-linux-gnu/11 
	-L/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu 
	-L/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib 
	-L/lib/x86_64-linux-gnu 
	-L/lib/../lib 
	-L/usr/lib/x86_64-linux-gnu 
	-L/usr/lib/../lib 
	-L/opt/hellolib 
	-L/usr/lib/gcc/x86_64-linux-gnu/11/../../.. 
	...

可以發現:

  • ①我們透過-L顯式新增的路徑/opt/hellolib_L被排在了最前面
  • LIBRARY_PATH中的路徑(除了/usr/lib//lib/,原因暫時未知),都被加上-L並傳給了COLLECT_GCC_OPTIONS,並排在/opt/hellolib_L之後。

連結器詳細資訊

然後我們再看連結器輸出的詳細資訊:

圖片中的文字內容如下:

SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); 
SEARCH_DIR("=/lib/x86_64-linux-gnu"); 
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); 
SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64"); 
SEARCH_DIR("=/usr/local/lib64"); 
SEARCH_DIR("=/lib64"); 
SEARCH_DIR("=/usr/lib64"); 
SEARCH_DIR("=/usr/local/lib"); 
SEARCH_DIR("=/lib"); 
SEARCH_DIR("=/usr/lib"); 
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); 
SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");

SEARCH_DIR指令是用來指定連結器在搜尋動態和靜態庫檔案時應當考慮的目錄,這些路徑通常包括系統的標準庫目錄,如/usr/lib/lib等。但是注意,透過-L指定的路徑會在執行時臨時新增到SEARCH_DIR列表的前面,即-L指定的路徑搜尋優先順序更高。

DEBUG總結

至此,我們可以簡單總結一下上述資訊:

  • 我們設定的LIBRARY_PATH的值會傳給編譯器
  • 編譯器根據自己的配置以及我們手動賦予的LIBRARY_PATH變數的值,生成一個新的LIBRARY_PATH(我們手動賦予的LIBRARY_PATH變數的值處於一個特定的位置),並將這個新的LIBRARY_PATH的值(除了/usr/lib/lib)加上-L傳遞給編譯器
  • 我們顯式使用-L指定的路徑也被傳遞給編譯器,並位於所有-L選項的最前面

而且對於編譯器配置的路徑,如/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib/,其本質就是/usr/lib/(這也是預設路徑優先順序大於LIBRARY_PATH指定路徑優先順序的原因)。

因此對於-L指定路徑LIBRARY_PATH指定路徑預設路徑,最終都被轉化為-L的形式傳遞給編譯器,且他們排列優先順序為:

-L指定路徑>預設路徑>LIBRARY_PATH指定路徑

因此他們的搜尋優先順序也是符合上述排列。

驗證

最後我們可以透過連結器在連結特定庫(比如我們的libhello)時的搜尋過程驗證上述結論:

可見連結器先是搜尋我們使用-L指定的路徑/opt/hellolib_L,然後搜尋編譯器配置的路徑/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib/(其本質就是預設路徑/usr/lib/),最後搜尋LIBRARY_PATH指定的路徑/opt/hellolib。證明了編譯過程中連結時庫搜尋路徑的優先順序為

-L指定路徑>預設路徑>LIBRARY_PATH指定路徑

預設路徑>LIBRARY_PATH原因

如上文所述,g++根據自己的配置將例如:

/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib/

的路徑新增到了LIBRARY_PATH中,而且位於使用者設定的LIBRARY_PATH之前。這個路徑的本質就是/usr/lib/。這就導致最終出現預設路徑搜尋優先順序大於LIBRARY_PATH指定路徑的搜尋優先順序的現象。

至於手動使用ld去連結.o.so檔案,後面有機會再做測試。

小結

死記優先順序沒必要,因為實際情況中還會遇到很多其他的規則。知道如何去看連結器的連結過程(如路徑搜尋順序)才是關鍵。

附錄

庫檔案原始碼

//file: libhello.cpp
#include <iostream>
void hello()
{
    std::cout << "Hello from the 1.1.0 library!" << std::endl;
}
//file: libhello_alt.cpp
#include <iostream>
void hello()
{
    std::cout << "Hello from the 2.1.0 library!" << std::endl;
}

主程式原始碼

//file: main.cpp
extern void hello();
int main()
{
    hello();
    return 0;
}

makefile

//file: makefile
CXX = g++
CXXFLAGS = -fPIC
LDFLAGS = -shared
DEBUG_MODE ?= 0

ifeq ($(DEBUG_MODE),1)
    DEBUG_OPTS = -v -Wl,--verbose
endif

all: lib/libhello.so.1.1.0 lib/libhello.so.2.1.0 obj/main.o

lib/libhello.so.1.1.0: libhello.cpp
	$(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -Wl,-soname,libhello.so.1

lib/libhello.so.2.1.0: libhello_alt.cpp
	$(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -Wl,-soname,libhello.so.2

obj/main.o: main.cpp
	$(CXX) -c -o $@ $^

# 在任何路徑下都無法搜尋到libhello.so
main_none: obj/main.o
	$(CXX) $(DEBUG_OPTS) -o $@ $^ -lhello

# 測試預設路徑/usr/lib 
main_default: obj/main.o
	cp ./lib/libhello.so.1.1.0 /usr/lib
	ln -sf /usr/lib/libhello.so.1.1.0 /usr/lib/libhello.so
	$(CXX) $(DEBUG_OPTS) -o $@ $^ -lhello

# 測試僅使用LIBRARY_PATH
main_library_path: obj/main.o
	mkdir -p /opt/hellolib
	cp ./lib/libhello.so.1.1.0 /opt/hellolib
	ln -sf /opt/hellolib/libhello.so.1.1.0 /opt/hellolib/libhello.so
	LIBRARY_PATH=/opt/hellolib $(CXX) $(DEBUG_OPTS) -o $@ $^ -lhello

# 測試僅使用-L
main_l: obj/main.o
	mkdir -p /opt/hellolib
	cp ./lib/libhello.so.1.1.0 /opt/hellolib
	ln -sf /opt/hellolib/libhello.so.1.1.0 /opt/hellolib/libhello.so
	$(CXX) $(DEBUG_OPTS) -o $@ $^ -L/opt/hellolib -lhello

# 比較預設路徑和LIBRARY_PATH的搜尋優先順序
cmp_default_libpath: obj/main.o
	cp ./lib/libhello.so.2.1.0 /usr/lib
	ln -sf /usr/lib/libhello.so.2.1.0 /usr/lib/libhello.so
	mkdir -p /opt/hellolib
	cp ./lib/libhello.so.1.1.0 /opt/hellolib
	ln -sf /opt/hellolib/libhello.so.1.1.0 /opt/hellolib/libhello.so
	LIBRARY_PATH=/opt/hellolib $(CXX) $(DEBUG_OPTS) -o $@ $^ -lhello

# 比較-L和預設路徑的優先順序
cmp_l_default: obj/main.o
	mkdir -p /opt/hellolib
	cp ./lib/libhello.so.2.1.0 /opt/hellolib
	ln -sf /opt/hellolib/libhello.so.2.1.0 /opt/hellolib/libhello.so
	cp ./lib/libhello.so.1.1.0 /usr/lib
	ln -sf /usr/lib/libhello.so.1.1.0 /usr/lib/libhello.so
	$(CXX) $(DEBUG_OPTS) -o $@ $^ -L/opt/hellolib -lhello

# 總體比較測試,集合了-L,LIBRARY_PATH和預設路徑
cmp_all: main.cpp
	mkdir -p /opt/hellolib
	mkdir -p /opt/hellolib_L
	cp ./lib/libhello.so.1.1.0 /opt/hellolib
	ln -sf /opt/hellolib/libhello.so.1.1.0 /opt/hellolib/libhello.so
	LIBRARY_PATH=/opt/hellolib $(CXX) $(DEBUG_OPTS) -o $@ $^ -L/opt/hellolib_L -lhello

clean:
	rm -f ./lib/* ./obj/* main_* cmp_*
	rm -f /usr/lib/libhello.so*
	rm -rf /opt/hellolib*
	ldconfig
	
.PHONY: clean main_none main_default main_library_path  main_l cmp_default_libpath cmp_l_default cmp_all

相關文章