Linux從源碼分析ldconfig命令對可執行文件緩存信息的讀取原理(緩存文件的讀)
今日問題:Linux的ldconfig -p命令可打印出系統緩存已記錄的所有動態庫的信息。那么這個功能是如何實現的?
本文主要通過解讀Linux的ldconfig命令的關鍵代碼,分析了ldconfig命令是如何實現讀取緩存文件 /etc/ld.so.cache 的內容的。本文涉及到的ldconfig的cache.c 代碼文件網址[1],在參考資料里。
ldconfig 使用的 /etc/ld.so.cache 文件,曾出現過兩個版本:
1.老版本的緩存文件格式 老版本指libc5 格式的動態庫,在glibc 2.0/2.1版本時采用的格式。緩存文件內容由cache_file類型的數據結構填充,其定義為
struct cache_file
{
char magic[sizeof CACHEMAGIC - 1];
unsigned int nlibs; /* 記錄的條數*/
struct file_entry libs[0];
};
2.新版本的的緩存文件格式 新版本指glibc 2.2及之后版本的。緩存文件內容由cache_file_new數據結構填充。定義為:
struct cache_file_new
{
char magic[sizeof CACHEMAGIC_NEW - 1];
char version[sizeof CACHE_VERSION - 1];
uint32_t nlibs; /* 記錄的條數 */
uint32_t len_strings; /* Size of string table. */
/* flags & cache_file_new_flags_endian_mask is one of the values
cache_file_new_flags_endian_unset, cache_file_new_flags_endian_invalid,
cache_file_new_flags_endian_little, cache_file_new_flags_endian_big.
The remaining bits are unused and should be generated as zero and
ignored by readers. */
uint8_t flags;
uint8_t padding_unsed[3]; /* Not used, for future extensions. */
/* File offset of the extension directory. See struct
cache_extension below. Must be a multiple of four. */
uint32_t extension_offset;
uint32_t unused[3]; /* Leave space for future extensions
and align to 8 byte boundary. */
struct file_entry_new libs[0]; /* Entries describing libraries. */
/* After this the string table of size len_strings is found. */
};
glibc-ld.so.cache1.1??? 以上輸出信息確實以glibc-ld.so.cache開始,所以我用的Ubuntu22.04系統的ldconfig的緩存文件內容是新格式的。
ldconfig代碼的cache.c 文件里是這樣根據magic的不同用if(){} else{}處理的:
if (memcmp (cache->magic, CACHEMAGIC, sizeof CACHEMAGIC - 1)) {///當屬于老版本時,按這里的方式處理 /* This can only be the new format without the old one. */ cache_new = (struct cache_file_new *) cache;
if (memcmp (cache_new->magic, CACHEMAGIC_NEW, sizeof CACHEMAGIC_NEW - 1)
在glibc-2.35的代碼中已用英文說明了,glibc2.2格式的,能兼容glibc2.2之前的緩存文件內容。這里說的兼容,是依賴于代碼檢測實現的:由于兩種結構體都以magic作為第一個項目,來識別緩存文件類型。再根據magic值的不同,對后續數據段采用不同的處理方式。老magic的定義為#define CACHEMAGIC "ld.so-1.7.0",新magic的定義為#define CACHEMAGIC_NEW "glibc-ld.so.cache"。也就是老版本 cache_file 的文件頭部以字符串ld.so-1.7.0開始,新版本cache_file_new 的文件頭部以字符串glibc-ld.so.cache開始。這點我們可以用head -c 命令查看下/etc/ld.so.cache文件的頭部30個字符串舊可以驗證了:
# head -c 30 /etc/ld.so.cache
glibc-ld.so.cache1.1???
以上輸出信息確實以glibc-ld.so.cache開始,所以我用的Ubuntu22.04系統的ldconfig的緩存文件內容是新格式的。
ldconfig代碼的cache.c 文件里是這樣根據magic的不同用if(){} else{}處理的:
if (memcmp (cache->magic, CACHEMAGIC, sizeof CACHEMAGIC - 1))
{///當屬于老版本時,按這里的方式處理
/* This can only be the new format without the old one. */
cache_new = (struct cache_file_new *) cache;
if (memcmp (cache_new->magic, CACHEMAGIC_NEW, sizeof CACHEMAGIC_NEW - 1)
|| memcmp (cache_new->version, CACHE_VERSION,
sizeof CACHE_VERSION - 1))
error (EXIT_FAILURE, 0, _("File is not a cache file.\n"));
check_new_cache (cache_new);
format = 1;
/* This is where the strings start. */
cache_data = (const char *) cache_new;
}
else
{//當屬于新版本緩存文件的時候,按下面內容處理
……省略
}
在知道了 緩存文件類型(magic標記)后,就可以開始根據格式標準,逐條讀/寫每條記錄了,這是ldconfig的重頭戲。
先看對cache文件的讀取效果,以 ldconfig -p命令打印出緩存文件的所有記錄的結果為例:
# ldconfig -p
1525 libs found in cache `/etc/ld.so.cache
……
libGLESv1_CM.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libGLESv1_CM.so
libGL.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libGL.so.1
libGL.so (libc6,x86-64) => /lib/x86_64-linux-gnu/libGL.so
……
這里每條都是一個動態庫的名稱、格式(libc6等格式)、CPU架構、所在路徑的記錄。
緩存文件中的這么一條記錄,對應的結構體,舊版本的為file_entry,新版本的為file_entry_new。它們的定義分別為:
struct file_entry
{
int32_t flags; /* This is 1 for an ELF library. */
uint32_t key, value; /* String table indices. */
};
以及新版本的 file_entry格式:
struct file_entry_new ///文件記錄的新格式,增加了OS版本、硬件信息
{
union
{
/* Fields shared with struct file_entry. */
struct file_entry entry;
/* Also expose these fields directly. */
struct
{
int32_t flags; /* This is 1 for an ELF library. */
uint32_t key, value; /* String table indices. */
};
};
uint32_t osversion; /* Required OS version. */
uint64_t hwcap; /* Hwcap entry. */
};
繼續分析【讀緩存文件】的簡要流程:
使用了 mmap() 函數,將 /etc/ld.so.cache 緩存文件整體讀入內存:
struct cache_file *cache
= mmap (NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
這是通過mmap()函數,將打開的緩存文件(open(/etc/ld.so.cache)的句柄fd)的數據映射到內存,由于文件數據就是按struct cache_file_new結構體格式填充的,所以mmap()后,就可以按這個結構體去解析各個條目。2. 判斷magic,新老magic分流處理。3. 如果是新的magic,則按struct cache_file_new數據結構解析。4. 對于新格式,遍歷讀取數據、打印:
……
else{
struct cache_extension_all_loaded ext;
if (……)錯誤處理;
/* Print everything. */
for (unsigned int i = 0; i < cache_new->nlibs; i++)
{
const char *hwcaps_string
= glibc_hwcaps_string (&ext, cache, cache_size,
&cache_new->libs[i]);
print_entry (cache_data + cache_new->libs[i].key,
cache_new->libs[i].flags,
cache_new->libs[i].osversion,
cache_new->libs[i].hwcap, hwcaps_string,
cache_data + cache_new->libs[i].value);
}
print_extensions (&ext);
}
這里關鍵內容是:
- cache_data,代表了mmap()讀取到的緩存文件內容;以cache_data的地址為初始地址,按偏移量cache_new->libs[i].key 相加后,可得到每條file_entry_new的入口,然后分別打印出記錄內容,就實現了 ldconfig -p 的代碼功能。
- 動態庫的條數,等于 cache_new->nlibs 這個變量的值。作為for循環遍歷時的條件。
- cache_new->libs[i].key 這里的key,在struct file_entry_new中的定義是:
uint32_t key, value; /* String table indices. */
key相當于第i條動態庫記錄的目錄索引。通過索引可以查到value。在實現時,key和value都是數字,這個數字代表字符串相對于cache_data這個首地址的字節偏移量,例如key->value 即 cache_new->libs[i].key, cache_new->libs[i].value 43256 -> 43234
總之,通過對結構體的合理使用,將緩存文件內容解析后,可打印出緩存文件中記錄的所有已知動態庫文件的信息。
void print_cache (const char *cache_name) 的函數代碼結束之前,還做了一下內存回收工作:
/* Cleanup. */
munmap (cache, cache_size);
close (fd);
首先使用munmap()函數,將之前已映射內存數據做一下清除;然后關閉打開的cache緩存文件描述符。
本文主要通過解讀Linux的ldconfig命令的關鍵代碼,分析了ldconfig命令是如何實現讀取緩存文件 /etc/ld.so.cache 的內容的。本文涉及到的ldconfig的cache.c 代碼文件網址[1],在參考資料里。
參考資料
[1]ldconfig的cache.c 代碼文件網址: https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/cache.c;h=8149f889bab9f9cb32a50e349991ba821e4db0dd;hb=HEAD