前言:由于逆向需要知識的全面性,這次我們來剖析一下so的加載流程惠奸;從java->c梅誓;廢話不多說,開始佛南。
一梗掰、JAVA層(這里我的target是28為例)
1、調(diào)用方式:System.loadLibrary();
System.loadLibrary("xxxx");
2嗅回、點進去
//類加載器 和 so名字
Runtime.getRuntime().loadLibrary0(classLoader,libName);
這里發(fā)現(xiàn)哈及穗,他是在runtime的時候才會獲取,要理解Runtime绵载,運行時埂陆。
3苛白、runtime層代碼:
代碼不多我直接全部貼上了,注意一下里面帶有中文注釋的地方焚虱,為個人理解部分和重點部分丸氛。
//同步方法。加載肯定是有先后順序著摔,類似隊列形式
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
//尋找這個文件是不是存在。 不存在的話返回null定续,直接拋異常
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
//文件存在谍咆,就交給下一層處理,這里如果返回的內(nèi)容不是null就說明加載失敗了私股,且含有錯誤信息摹察。
//所以這里是重點!3ā9┖俊! 下一步我們就看這里
String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : getLibPaths()) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = nativeLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
4峭状、nativeLoad();
這里就直接native層了克滴,
private static native String nativeLoad(String filename, ClassLoader loader);
二、Native層(這里我使用的是:http://androidxref.com/)
1优床、sdk28對應(yīng)的是 android9.0劝赔,所以這里要選擇一樣的版本,因為版本之間可能會有差異胆敞。
2着帽、源碼中,so層也會對應(yīng)有一個Runtime移层,注意關(guān)鍵詞
3仍翰、溯源1
這里注意下,動態(tài)加載观话。
4予借、溯源2
因為是native層,我們選這個
其實有人可能已經(jīng)發(fā)現(xiàn)了匪燕,這個.h不是頭文件嗎檐束?為什么要看頭文件呢? 這里特別提醒一下畜疾,我們要看非頭文件囊榜,因為頭文件是一種約束存在。
所以我們選擇:OpenjdkJvm.cc
5尼变、溯源3
6利凑、溯源4
這個方法比較長浆劲,這里分幾塊來看。(純個人理解哀澈,有誤歡迎指出)
6.1 -溯源4.1
我們的目的就是把這個so文件加載到內(nèi)存中牌借,那么肯定要圍繞這個文件做事情。還是老方法割按,捕捉關(guān)鍵字膨报。
6.2 -溯源4.2
看看下面圍繞這個 library做了什么事情。
這里又圍繞著他們得到了一個handle
6.3 -溯源4.3
這里我有個小疑問适荣,就是當(dāng)sym==nullptr 的時候他直接返回success现柠,這里是因為首次加載的時候,JNI_OnLoad 函數(shù)是首次執(zhí)行弛矛,所以他首次加載够吩。如果加載過了,他就要找到加載過他的classloader丈氓,并且拿到相關(guān)信息驗證周循,信息無誤之后才返回success。
7万俗、溯源5
這里要說的湾笛,在6部分,大致流程梳理完了闰歪。需要回頭來看一個比較重要的函數(shù)迄本,就是在 6.2里面提到的handle,handle是通過調(diào)用了一個 OpenNativeLibrary课竣,接下來在7部分嘉赎,來著重分析這個模塊的流程。
搜索他于樟,只有一個公条。
7.1 -溯源5.1
這里,只簡單看一下兩部分迂曲,一部分是classloader為空的靶橱,就是說首次加載,否則需要通過之前加載過的classLoader拿到相關(guān)信息返回路捧。
我們還是著重分析1关霸,dlopen() 函數(shù)
7.2 -溯源5.2
這里我們選擇的是bionic目錄下的 dlopen,是因為在 bionic目錄下都屬于android的內(nèi)核源碼杰扫,是比較關(guān)鍵的地方队寇。
7.3 -溯源5.3
又跳到__loader_dlopen()
繼續(xù)追,注意關(guān)鍵函數(shù)章姓,do_dlopen()
7.4 -溯源5.4
這里要注意特征佳遣,四個參數(shù)识埋。
繼續(xù)
劃重點,這里有個結(jié)構(gòu)體零渐,soinfo窒舟,這樣看沒有感覺,如果換成java的駝峰表示就一目了然了:soInfo诵盼,顧名思義惠豺,so的信息。
中間代碼都是判斷一些信息是不是null的環(huán)節(jié)风宁,這里代碼就不貼了耕腾,我們直接來看一下結(jié)尾返回的什么,然后追溯返回的東西的來歷杀糯。
相同點部分,標(biāo)記出來苍苞。
劃重點->
1固翰、 si是 find_library 返回的結(jié)構(gòu)體,也就是這個 library的一系列信息羹呵。
2骂际、handle = si->to_handle(); 將si的句柄返回。
什么是句柄? 它實際上是作為一個索引在一個表中查找對應(yīng)的內(nèi)核對象的實際地址冈欢。你可以理解為一個對象在內(nèi)存中的位置歉铝。 這句話出自:https://blog.csdn.net/qq_31511955/article/details/90763661
3、si->call_constructors(); 本人英語不好凑耻,翻譯一下:調(diào)用構(gòu)造函數(shù)太示。
8、溯源6
在上面的一系列函數(shù)中香浩,這一條調(diào)用鏈似乎是完成了类缤。這里先來小小的總結(jié)一下。
1邻吭、調(diào)用java層loadLirbrary
2餐弱、然后交給so層調(diào)用 nativeLoadLibrary
3、然后給vm(虛擬機)調(diào)用加載
4囱晴、然后我們需要根據(jù)路徑打開這個library膏蚓,并且返回這個library的句柄。
只要下層返回的信息無誤畸写,上面一層函數(shù)都會正常執(zhí)行驮瞧。so加載到內(nèi)存之后的地址不會變,所以只要圍繞著他的地址做操作就可以了枯芬。
這里還要深究一下si->call_constructors()剧董, 看看他的構(gòu)造函數(shù)里做了什么幢尚,這一步也比較關(guān)鍵,這樣一來整個流程就貫穿下來了翅楼。
9尉剩、溯源7
si->call_constructors()
1、代碼結(jié)合注釋內(nèi)容翻譯結(jié)果毅臊,這一段的含義是說:全局不允許頻繁調(diào)用這個構(gòu)造方法理茎,也就是這個so只能被初始化一次,且它在內(nèi)存中的地址是不會變的管嬉。 類似于java中的單例皂林。
2、如果這個so沒有被加載過蚯撩,那就調(diào)用DT_INIT 和 DT_INIT_ARRAY础倍, 類似于初始化方法。具體操作要看開發(fā)者是怎么寫的胎挎。 可以類似理解為:Application 的 attachBaseContext() 方法沟启,只有程序第一次進入的時候才會走。
10犹菇、溯源總結(jié)
整個流程走完之后德迹,腦瓜里清晰不少,最后用自己的理解來總結(jié)出一張大致流程圖奉上揭芍,有誤歡迎指出胳搞。