Linux c 運行時獲取動態庫所在路徑
記錄一下如何在Linux環境下運行時獲取動態庫路徑。
只討論Linux amd64和arm64環境,因為使用的辦法都是平臺相關的不具備可移植性。
準備
一般來說動態庫并不需要關心自己所在的文件系統上的路徑,但業務有那么多總有一兩個會有特殊需求。
現在給定一個動態庫里的函數A,需求是要知道這個函數A是哪個動態庫里的以及這個庫的存放路徑。
測試對象有兩個,第一個是標準庫的函數printf,另一個是我們自己寫的動態鏈接庫里的PrintRandomText函數。
自定義動態庫的名字叫libmycustom1.so,代碼和編譯生成的庫都存放在libmycustom1目錄下。代碼如下:
// lib.h
#pragma once
#include <unistd.h>
#include <sys/random.h>
void PrintRandomText(ssize_t length);
// lib.c
#include <stdio.h>
#include "lib.h"
void PrintRandomText(ssize_t length)
{
unsigned char buff[64] = {0};
length = (length + 1) / 2;
if (length == 0) {
return;
}
while (1) {
ssize_t count = getrandom(buff, 64, 0);
count = length > count ? count : length;
for (ssize_t i = 0; i < count; ++i) {
printf("%02X", buff[i]&0xff);
}
if (length <= count) {
break;
}
length -= count;
}
printf("\n");
}
函數很簡單,從Linux的/dev/urandom隨機設備中讀取指定大小的數據然后打印輸出,編譯使用如下命令:
gcc -Wall -O2 -fPIC -shared lib.c -o libmycustom1.so
這樣我們就得到了libmycustom1/libmycustom1.so。下面可以介紹如何在運行時獲取動態庫的路徑了。
使用dladdr獲取動態庫路徑
第一種方法是使用dladdr這個函數。dladdr是libdl.so中的一個函數,用來獲取某個地址對應的動態庫信息,而libdl是Linux上專門用來處理動態鏈接庫的函數庫。
dladdr獲取的信息中恰巧有動態庫的實際存放路徑這一信息,我們可以加以利用:
#define _GNU_SOURCE // 這行不能少
#include <dlfcn.h> // for dladdr
#include <stdio.h>
#include "libmycustom1/lib.h"
int main()
{
Dl_info info1, info2;
if (dladdr((void*)&printf, &info1) == 0) {
fprintf(stderr, "cannot get printf's info\n");
return 1;
}
if (dladdr((void*)&PrintRandomText, &info2) == 0) {
fprintf(stderr, "cannot get PrintRandomText's info\n");
return 1;
}
// 還需要檢查dli_fname字段是否是NULL,這里就省略了
printf("lib contains printf: %s\n", info1.dli_fname);
printf("lib contains PrintRandomText: %s\n", info2.dli_fname);
}
dladdr在出錯的時候會返回0,這時可以用dlerror來獲取具體的報錯,不過這里我為了簡單起見就省略了。
編譯運行需要下面的命令:
$ gcc a.c -L./libmycustom1 -lmycustom1 -ldl
$ export LD_LIBRARY_PATH=./libmycustom1
$ ./a.out
lib contains printf: /lib/x86_64-linux-gnu/libc.so.6
lib contains PrintRandomText: ./libmycustom1/libmycustom1.so
編譯時還需要鏈接libdl。
因為庫沒有放在默認的系統搜索路徑里,也沒有單獨設置ld.cache,因此我們需要設置環境變量LD_LIBRARY_PATH來告訴加載器我們的動態庫在哪里。
可以看到對于存放在標準路徑里的libc,dladdr給出了絕對路徑,對于我們自定義的庫,因為LD_LIBRARY_PATH設置成了相對路徑,所以給我們的結果也是相對路徑的。因此dladdr拿到的結果最好得先做一次相對路徑到絕對路徑的轉換再使用。
dladdr受到廣泛的支持,基本主要的Linux發行版上都能使用,因此實際中大家也都在用它,但它還是有幾個缺點:
- 函數指針轉
void*在c/c++標準中都是不允許的,而且實際也有函數指針是胖指針的平臺存在,但至少這一行為在x86_64和arm的gcc/clang上都沒啥問題 dladdr只能正常獲取使用-fPIC編譯成位置不相關代碼的動態庫信息,這個信息也不一定準確。
綜上dladdr雖然能用,但不通用,而且可靠性也一般。
正如我在文章開頭就說了,這次討論的方案沒有可移植性,需要限定在具體的系統和硬件平臺上使用。
使用proc maps文件獲取動態庫路徑
如果我不想再額外鏈接一個庫,尤其是還得在文件開頭定義#define _GNU_SOURCE,那么就需要使用方案二了。
方案二很簡單也很直接,讀取進程的/proc/<pid>/maps,對比地址范圍就能找到函數所在的動態庫以及庫的路徑。
Linux加載動態鏈接庫是用的類似mmap的形式,庫實際只會被加載一次,然后被映射到每個需要這個庫的進程的地址空間里。
而/proc/<pid>/maps記載了進程的內存地址空間里所有的mmap映射的文件,包括普通文件、共享庫和匿名映射。當然這個文件里還包含了vdso和代碼段等的內存地址,總體上來說可以算作進程的內存空間分布概覽。一個例子是:
55bce8e1c000-55bce8e1d000 r--p 00000000 08:20 3337 /home/apocelipes/dladdrtest/a.out
55bce8e1d000-55bce8e1e000 r-xp 00001000 08:20 3337 /home/apocelipes/dladdrtest/a.out
55bce8e1e000-55bce8e1f000 r--p 00002000 08:20 3337 /home/apocelipes/dladdrtest/a.out
55bce8e1f000-55bce8e20000 r--p 00002000 08:20 3337 /home/apocelipes/dladdrtest/a.out
55bce8e20000-55bce8e21000 rw-p 00003000 08:20 3337 /home/apocelipes/dladdrtest/a.out
55bd039bf000-55bd039e0000 rw-p 00000000 00:00 0 [heap]
7f7bffb36000-7f7bffb39000 rw-p 00000000 00:00 0
7f7bffb39000-7f7bffb61000 r--p 00000000 08:20 49817 /usr/lib/x86_64-linux-gnu/libc.so.6
7f7bffb61000-7f7bffce9000 r-xp 00028000 08:20 49817 /usr/lib/x86_64-linux-gnu/libc.so.6
7f7bffce9000-7f7bffd38000 r--p 001b0000 08:20 49817 /usr/lib/x86_64-linux-gnu/libc.so.6
7f7bffd38000-7f7bffd3c000 r--p 001fe000 08:20 49817 /usr/lib/x86_64-linux-gnu/libc.so.6
7f7bffd3c000-7f7bffd3e000 rw-p 00202000 08:20 49817 /usr/lib/x86_64-linux-gnu/libc.so.6
7f7bffd3e000-7f7bffd4b000 rw-p 00000000 00:00 0
7f7bffd53000-7f7bffd54000 r--p 00000000 08:20 3397 /home/apocelipes/dladdrtest/libmycustom1/libmycustom1.so
7f7bffd54000-7f7bffd55000 r-xp 00001000 08:20 3397 /home/apocelipes/dladdrtest/libmycustom1/libmycustom1.so
7f7bffd55000-7f7bffd56000 r--p 00002000 08:20 3397 /home/apocelipes/dladdrtest/libmycustom1/libmycustom1.so
7f7bffd56000-7f7bffd57000 r--p 00002000 08:20 3397 /home/apocelipes/dladdrtest/libmycustom1/libmycustom1.so
7f7bffd57000-7f7bffd58000 rw-p 00003000 08:20 3397 /home/apocelipes/dladdrtest/libmycustom1/libmycustom1.so
7f7bffd58000-7f7bffd5a000 rw-p 00000000 00:00 0
7f7bffd5a000-7f7bffd5b000 r--p 00000000 08:20 49814 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7bffd5b000-7f7bffd86000 r-xp 00001000 08:20 49814 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7bffd86000-7f7bffd90000 r--p 0002c000 08:20 49814 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7bffd90000-7f7bffd92000 r--p 00036000 08:20 49814 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7f7bffd92000-7f7bffd94000 rw-p 00038000 08:20 49814 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fff6ded9000-7fff6defb000 rw-p 00000000 00:00 0 [stack]
7fff6dfaa000-7fff6dfae000 r--p 00000000 00:00 0 [vvar]
7fff6dfae000-7fff6dfb0000 r-xp 00000000 00:00 0 [vdso]
可以看到libc和我們自己的庫都被記載進文件里了。每行內容是空格分開的,對于匿名映射不會有最后的路徑。第一列的就是內存地址,以“-”連字符分隔,第一部分是內存映射區域開始地址,第二部分是結束地址。
這和獲取函數對應的動態庫有什么關系呢?關系肯定是有的,在Linux上動態庫里的“函數”其實就是一段編譯好的代碼,加載進內存后它也會占用一段內存空間,調用動態庫函數的時候實際上是下面這樣的流程:
- 根據函數名稱跳轉到對應的符號表項目上
- 檢查函數是否被加載,有加載就跳過下面步驟直接到4
- 未加載時loader會去動態庫文件里讀取對應函數的代碼,存入內存,然后把項目內容用代碼在內存里的起始地址覆蓋
- 程序跳轉到函數代碼所在的內存地址上,開始一條條加載執行這些代碼
加載進內存的代碼權限是r-xp,代表內存里的內容可以被執行。
現在出于安全考慮有些程序會使用編譯選項把這些工作提前到程序加載運行時就完成,但大致上是一樣的。被加載的函數的內存會被記載進maps文件,所以我們只要讀取maps文件然后對比內存地址范圍,就能知道函數對應的庫和路徑了。
因為我們只看函數地址,因此不用查的太細,只要地址在范圍內就可以,無需查看權限。知道原理后就可以寫個腳本去解析了:
local function searchAddr(pid, addr)
local file = io.open("/proc/" .. pid .. "/maps", "r")
if not file then
print("進程不存在: " .. pid)
return
end
for line in file:lines() do
local parts = {}
for word in line:gmatch("%S+") do
table.insert(parts, word)
end
if #parts > 5 then
local addrParts = {}
for addr in parts[1]:gmatch("[^%-]+") do
table.insert(addrParts, addr)
end
if #addrParts == 2 then
local startAddr = tonumber(addrParts[1], 16) or 0
local endAddr = tonumber(addrParts[2], 16) or 0
if startAddr <= addr and addr < endAddr then
print(parts[#parts])
break;
end
end
end
end
file:close()
end
if #arg ~= 2 then
print("no enough args")
os.exit(1)
end
local addr = tonumber(arg[2]) or 0
if addr == 0 then
print("addr can not be 0")
os.exit(1)
end
searchAddr(arg[1], addr)
c語言處理字符串太折磨了,所以我用lua偷個懶,代碼就不解釋了因為很簡單,你可以讓ai代勞解讀一下。
進程退出后proc文件也就沒了,所以測試代碼也得改一下不要讓進程那么快退出:
#include <stdio.h>
#include "libmycustom1/lib.h"
int main()
{
printf("pid %d\n", getpid());
printf("printf address: %p\n", (void*)&printf);
printf("PrintRandomText address: %p\n", (void*)&PrintRandomText);
pause(); // 阻塞進程直到收到信號
}
運行結果:


可以看到我們順利找到了函數對應的庫以及庫的存放路徑。
使用proc maps的優點是不需要額外的依賴,而且得到的路徑都是絕對路徑。缺點則是需要函數指針轉換成地址值,以及proc是Linux等少數系統獨有的,不通用,而且讀取maps文件需要有專門的權限,這個權限默認打開但是可以選擇關閉。
總結
運行時獲取動態庫地址除了dladdr和解析/proc/<pid>/maps還可以有一些別的做法。比如可以用nm獲取庫文件的符號表進行對比,但如果庫文件被strip處理過就不能這么用了。本文介紹的兩種方案是泛用性最高的。
另外也別太依賴這些結果,因為隱藏或者篡改這些信息太過簡單。如果你的想要動態庫的路徑,應該使用構建系統注入信息或者干脆做出輸入選項,而不是依靠這些可靠性和可移植性都欠佳的方案。


浙公網安備 33010602011771號