一、What is Prelink?
1.1 Prelink 簡(jiǎn)介
Prelink 是 Red Hat 開(kāi)發(fā)者 Jakub Jelinek 所設(shè)計(jì)的工具。正如其名字所示,Prelink 利用事先鏈接代替運(yùn)行時(shí)鏈接的方法來(lái)加速共享庫(kù)的加載。它不僅可以加快起動(dòng)速度,還可以減少部分內(nèi)存開(kāi)銷,是各種 Linux 架構(gòu)上用于減少程序加載時(shí)間、縮短系統(tǒng)啟動(dòng)時(shí)間和加快應(yīng)用程序啟動(dòng)的很受歡迎的一個(gè)工具。
Linux 系統(tǒng)運(yùn)行時(shí)的動(dòng)態(tài)鏈接尤其是重定位 (Relocation) 的開(kāi)銷,對(duì)于大型系統(tǒng)來(lái)說(shuō)是很大的。相比之下,早期 UNIX 下的 a.out 格式的老式鏈接方法在速度和占用內(nèi)存方面有明顯的優(yōu)勢(shì)(但不如ELF格式更靈活,能方便的構(gòu)建動(dòng)態(tài)共享庫(kù))。Prelink 工具是試圖在保持一部分靈活性的基礎(chǔ)上,借鑒 a.out 格式在速度和占用內(nèi)存方面的優(yōu)點(diǎn),對(duì) ELF 文件進(jìn)行一些改進(jìn)。
Prelink 工具的原理主要基于這樣一個(gè)事實(shí):動(dòng)態(tài)鏈接和加載的過(guò)程開(kāi)銷很大,并且在大多數(shù)的系統(tǒng)上,函數(shù)庫(kù)并不會(huì)常常被更動(dòng),每次程序被執(zhí)行時(shí)所進(jìn)行的鏈接動(dòng)作都是完全相同的,對(duì)于嵌入式系統(tǒng)來(lái)說(shuō)尤其如此。因此,這一過(guò)程可以改在運(yùn)行時(shí)之前就可以預(yù)先處理好,即花一些時(shí)間利用 Prelink 工具對(duì)動(dòng)態(tài)共享庫(kù)和可執(zhí)行文件進(jìn)行處理,修改這些二進(jìn)制文件并加入相應(yīng)的重定位等信息,節(jié)約了本來(lái)在程序啟動(dòng)時(shí)的比較耗時(shí)的查詢函數(shù)地址等工作,這樣可以減少程序啟動(dòng)的時(shí)間,同時(shí)也減少了內(nèi)存的耗用。
Prelink 的這種做法當(dāng)然也有代價(jià):每次更新動(dòng)態(tài)共享庫(kù)時(shí),相關(guān)的可執(zhí)行文件都需要重新執(zhí)行一遍 Prelink 才能保證有效,因?yàn)樾碌墓蚕韼?kù)中的符號(hào)信息、地址等很可能與原來(lái)的已經(jīng)不同了。這種代價(jià)對(duì)于嵌入式系統(tǒng)的開(kāi)發(fā)者來(lái)說(shuō)可能稍微帶來(lái)一些復(fù)雜度,不過(guò)好在對(duì)用戶來(lái)說(shuō)幾乎是可以忽略的。
很多 Linux 發(fā)行版上已經(jīng)預(yù)裝了或者已經(jīng)使用了 Prelink 工具,不過(guò)我們需要適用于嵌入式平臺(tái),比如 ARM 的版本,這樣我們需要到下載 Prelink 的源代碼并重新編譯。
1.2 Prelink 機(jī)理
從我們最熟悉的 hello world 程序開(kāi)始分析:
#include 《stdio.h》
int main(int argc, const char* argv[]) {
printf(“Hello, World!\n”);
return 0;
}
我們知道,printf 是在 c語(yǔ)言運(yùn)行庫(kù) libc 中定義的。如果不使用動(dòng)態(tài)庫(kù),也就是使用glibc 的靜態(tài)庫(kù)版本,鏈接到 a.out 中的話,那么 printf 函數(shù)的地址在運(yùn)行之前就是已知的,很簡(jiǎn)單的一句地址轉(zhuǎn)移就可以完成了。
可是使用動(dòng)態(tài)庫(kù)的話,在程序編譯階段,我們是無(wú)法得知 printf 的函數(shù)地址,因?yàn)閯?dòng)態(tài)庫(kù)的加載的內(nèi)存地址是隨機(jī)的。那么對(duì)于動(dòng)態(tài)庫(kù)的情況,針對(duì) printf 是如何尋址的呢?
在程序啟動(dòng)時(shí),當(dāng)調(diào)用 printf 的時(shí)候,程序會(huì)將處理權(quán)交給 loader,由其負(fù)責(zé)在進(jìn)程以及其鏈接的動(dòng)態(tài)庫(kù)中查找 printf 的函數(shù)地址。由于 loader 不知道 printf 是在哪個(gè)動(dòng)態(tài)庫(kù),所以它將在整個(gè)進(jìn)程和動(dòng)態(tài)庫(kù)的范圍內(nèi)查找。更糟糕的是在 C++ 程序中,符號(hào)的命名是類名+函數(shù)名,這導(dǎo)致在做字符串比較時(shí),往往直到字符串的結(jié)尾才能獲得結(jié)果。
這就導(dǎo)致了,在進(jìn)程啟動(dòng)過(guò)程中,符號(hào)查找往往占據(jù)了大部分時(shí)間。據(jù)統(tǒng)計(jì),在 Linux 的 KDE 進(jìn)程中啟動(dòng)過(guò)程中,符號(hào)查找表竟占據(jù)了進(jìn)程啟動(dòng) 80% 的時(shí)間。有沒(méi)有辦法來(lái)改進(jìn)呢?
如果進(jìn)程在運(yùn)行前,就能獲知?jiǎng)討B(tài)庫(kù)的加載地址,那么函數(shù)調(diào)用的地址就應(yīng)該是已知的,我們就可以通過(guò)修改執(zhí)行程序,來(lái)避免符號(hào)的查找。從而節(jié)省進(jìn)程啟動(dòng)的時(shí)間。
實(shí)際上 Prelink 正是這么做的。Prelink 最早是在 Redhat 中引用的,用來(lái)加速 KDE 的啟動(dòng)速度。那時(shí)侯 Prelink 作為系統(tǒng)的一個(gè)進(jìn)程,不定期的啟動(dòng),對(duì)系統(tǒng)中的進(jìn)程和動(dòng)態(tài)庫(kù)進(jìn)行優(yōu)化,這在系統(tǒng)中進(jìn)程和動(dòng)態(tài)庫(kù)不怎么變化的情況下非常有用。
在做 Prelink 時(shí),需要為其指定需要做 Prelink 的進(jìn)程和動(dòng)態(tài)庫(kù)的目錄。Prelink 需要做以下幾件事情:
分析所有的進(jìn)程和動(dòng)態(tài)庫(kù),為每個(gè)動(dòng)態(tài)庫(kù)指定一塊唯一的(虛擬)內(nèi)存地址;
分析進(jìn)程和動(dòng)態(tài)庫(kù)中,所有需要重定位的函數(shù)、全局變量等,用 loader 進(jìn)行符號(hào)查找,對(duì)齊地址進(jìn)行解析;
修改進(jìn)程中和動(dòng)態(tài)庫(kù)的二進(jìn)制文件;
眾所周知,在 32 位 Linux 操作系統(tǒng)上有 4G 的地址空間,3G 以上為操作系統(tǒng)使用,0000000~4000000 歸進(jìn)程的代碼段、數(shù)據(jù)段和堆段使用,從 3G 往下歸棧段使用。基本上我們可以認(rèn)為從 1G~3G 的地址空間可以用來(lái)指定動(dòng)態(tài)庫(kù)的加載地址,地址空間還是很豐富的。
凡事總有萬(wàn)一,如果地址空間不夠怎么辦呢?Prelink 關(guān)于這個(gè)問(wèn)題,做了兩個(gè)約定:
總是一同出現(xiàn)的動(dòng)態(tài)庫(kù),其動(dòng)態(tài)庫(kù)的加載地址一定不能重疊;
總是不同時(shí)間段出現(xiàn)的動(dòng)態(tài)庫(kù),其動(dòng)態(tài)庫(kù)的加載地址可以重疊;
有了這兩個(gè)約定之后,基本上就可以保證,為每個(gè)動(dòng)態(tài)庫(kù)指定加載地址,從而在運(yùn)行前就能獲知函數(shù)和全局變量等符號(hào)的地址。
-
Linux
+關(guān)注
關(guān)注
87文章
11326瀏覽量
209959 -
Ha-VIS preLink
+關(guān)注
關(guān)注
0文章
2瀏覽量
1592 -
權(quán)重定位
+關(guān)注
關(guān)注
0文章
2瀏覽量
1297
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論