- 前言
- 實驗環境
- 目錄說明
- 準備工作
- 單獨測試
- 不配置路徑
- 預設路徑
- 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.cpp
和libhello_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.0
與libhello.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.o
和hello
的共享庫檔案連結成功。
直接使用makefile中預設好的命令即可完成上述操作:
make main_none
輸出:
可以看到由於我們沒有配置任何額外的搜尋路徑,並且沒有在預設搜尋路徑下放置libhello.so
檔案,連結器就找不到相應的共享庫檔案,就會連結失敗。
單次實驗結束後,使用make clean
命令清除本次實驗生成的檔案,然後再次使用make
命令重新生成共享庫檔案和可重定位目標檔案。(每次做完一個小實驗,都要重複此步驟,後不贅述)
預設路徑
將libhello.so.1.1.0
複製至預設搜尋路徑/usr/lib
,並在/usr/lib
下建立一個軟連結(libhello.so
)指向它,然後進行連結操作,檢視是否可以將main.o
和hello
的共享庫檔案連結成功。
直接使用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.o
和hello
的共享庫檔案的連結操作,檢視是否可以連結成功。
直接使用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.o
和hello
的共享庫檔案連結成功。
直接使用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.o
和hello
的共享庫檔案的連結操作。
直接使用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.o
和hello
的共享庫檔案的連結操作。
直接使用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