so加載Linker跟NameSpace機制詳解
前言
so庫的加載可是我們?nèi)粘i_發(fā)都會用到的,因此系統(tǒng)也提供了非常方便的api給我們進行調(diào)用
System.loadLibrary(xxxso);
當然,隨著版本的變化,loadLibrary也是出現(xiàn)了非常大的變化,最重要的是分水嶺是androidN加入了namespace機制,可能很多人都是一頭霧水噢!這是個啥?我們在動態(tài)so加載方案中,會頻繁出現(xiàn)這個名詞,同時還有一個高頻的詞就是Linker,本期不涉及復雜的技術(shù)方案,我們就來深入聊聊,Linker的概念,與namespace機制的加入,希望能幫助更多開發(fā)者去了解so的加載過程。
Linker
我們都知道,Linux平臺下有動態(tài)鏈接文件(.so)與靜態(tài)鏈接文件(.a),兩者其實都是一種ELF文件(相關(guān)的文件格式我們不贅述)。為什么會有這么兩種文件呢?我們就從簡單的角度來想一次,其實就是為了更多的代碼復用,比如程序1,程序2都用到了同一個東西,比如xx.so
此時就會出現(xiàn),程序1與程序2中調(diào)用fun common的地方,在沒有鏈接之前,調(diào)用處的地址,我們以“stub”,表示這其實是一個未確定的東西,而后續(xù)的這個地址填充(寫入正確的common地址),其實就是Linker的職責。
我們通過上面的例子,其實就可以明白,Linker,主要的職責,就是幫助查找當前程序所依賴的動態(tài)庫文件(ELF文件)。那么Linker本身是個什么呢,其實他跟.so文件都是同一種格式,也是ELF文件,那么Linker又由誰幫助加載啟動呢,這里就會出現(xiàn)存在一個(雞生蛋,蛋生雞)的問題,而ELF文件給出的答案就是,設立一個:interp 的段,當一個進程啟動的時候(linux中通過execv啟動),此時就會通過load_elf_binary函數(shù),先加載ELF文件,然后再調(diào)用load_elf_interp方法,直接加載了:interp 段地址的起點,從而能夠構(gòu)建我們的大管家Linker,當然,Linker本身就不能像普通的so文件一樣,去依賴另一個so,其實原因也很簡單,沒人幫他初始化呀!因此Linker是采用配置的方式先啟動起來了!
當然,我們主要的目標是建立概念,Linker本身涉及的復雜加載,我們也不繼續(xù)貼出來了
NameSpace
在以往的anroidN以下版本中,加載so庫通常是直接采用dlopen的方式去直接加載的,對于非公開的符號,如果被使用,就容易在之后迭代出現(xiàn)問題,(類似java,使用了一個三方庫的private方法,如果后續(xù)變更方法含義,就會出現(xiàn)問題),因此引入了NameSpace機制
Android 7.0 為原生庫引入了命名空間,以限制內(nèi)部 API 可見性并解決應用意外使用平臺庫而不是自己的平臺庫的情況。
我們說的NameSpace,主要對應著一個數(shù)據(jù)結(jié)構(gòu)android_namespace_link_t
linker_namespaces.h struct android_namespace_link_t private: std::string name_; namespace名稱 bool is_isolated_; 是否隔離(大部分是true) std::vector<std::string> ld_library_paths_; 鏈接路徑 std::vector<std::string> default_library_paths_;默認可訪問路徑 std::vector<std::string> permitted_paths_;已允許訪問路徑 ....
我們來看一看,這個數(shù)據(jù)結(jié)構(gòu)在哪里會被使用到,其實就是so庫加載過程。當我們調(diào)用System.loadLibrary的時候,其實最終調(diào)用的是
private synchronized void loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) { 文件名校驗 if (libname.indexOf((int)File.separatorChar) != -1) { throw new UnsatisfiedLinkError( "Directory separator should not appear in library name: " + libname); } String libraryName = libname; // Android-note: BootClassLoader doesn't implement findLibrary(). http://b/111850480 // Android's class.getClassLoader() can return BootClassLoader where the RI would // have returned null; therefore we treat BootClassLoader the same as null here. if (loader != null && !(loader instanceof BootClassLoader)) { String filename = loader.findLibrary(libraryName); if (filename == null && (loader.getClass() == PathClassLoader.class || loader.getClass() == DelegateLastClassLoader.class)) { // Don't give up even if we failed to find the library in the native lib paths. // The underlying dynamic linker might be able to find the lib in one of the linker // namespaces associated with the current linker namespace. In order to give the // dynamic linker a chance, proceed to load the library with its soname, which // is the fileName. // Note that we do this only for PathClassLoader and DelegateLastClassLoader to // minimize the scope of this behavioral change as much as possible, which might // cause problem like b/143649498. These two class loaders are the only // platform-provided class loaders that can load apps. See the classLoader attribute // of the application tag in app manifest. filename = System.mapLibraryName(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) + """); } String error = nativeLoad(filename, loader); if (error != null) { throw new UnsatisfiedLinkError(error); } return; } // We know some apps use mLibPaths directly, potentially assuming it's not null. // Initialize it here to make sure apps see a non-null value. getLibPaths(); String filename = System.mapLibraryName(libraryName); //最終調(diào)用nativeLoad String error = nativeLoad(filename, loader, callerClass); if (error != null) { throw new UnsatisfiedLinkError(error); } }
這里我們注意到,拋出UnsatisfiedLinkError的時機,要么so文件名加載不合法,要么就是nativeLoad方法返回了錯誤信息,這里是需要我們注意的,我們?nèi)绻霈F(xiàn)這個異常,可以從這里排查,nativeLoad方法最終通過LoadNativeLibrary,在native層真正進入so的加載過程
LoadNativeLibrary 非常長,我們截取部分 bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, const std::string& path, jobject class_loader, jclass caller_class, std::string* error_msg) { 會判斷是否已經(jīng)加載過當前so,同時也要加鎖,因為存在多線程加載的情況 SharedLibrary* library; Thread* self = Thread::Current(); { // TODO: move the locking (and more of this logic) into Libraries. MutexLock mu(self, *Locks::jni_libraries_lock_); library = libraries_->Get(path); } 調(diào)用OpenNativeLibrary加載 void* handle = android::OpenNativeLibrary( env, runtime_->GetTargetSdkVersion(), path_str, class_loader, (caller_location.empty() ? nullptr : caller_location.c_str()), library_path.get(), &needs_native_bridge, &nativeloader_error_msg);
這里又是漫長的native方法,OpenNativeLibrary,在這里我們終于見到namespace了
void* OpenNativeLibrary(JNIEnv* env, int32_t target_sdk_version, const char* path, jobject class_loader, const char* caller_location, jstring library_path, bool* needs_native_bridge, char** error_msg) { #if defined(ART_TARGET_ANDROID) UNUSED(target_sdk_version); if (class_loader == nullptr) { *needs_native_bridge = false; if (caller_location != nullptr) { android_namespace_t* boot_namespace = FindExportedNamespace(caller_location); if (boot_namespace != nullptr) { const android_dlextinfo dlextinfo = { .flags = ANDROID_DLEXT_USE_NAMESPACE, .library_namespace = boot_namespace, }; //最終調(diào)用android_dlopen_ext打開 void* handle = android_dlopen_ext(path, RTLD_NOW, &dlextinfo); if (handle == nullptr) { *error_msg = strdup(dlerror()); } return handle; } } // Check if the library is in NATIVELOADER_DEFAULT_NAMESPACE_LIBS and should // be loaded from the kNativeloaderExtraLibs namespace. { Result<void*> handle = TryLoadNativeloaderExtraLib(path); if (!handle.ok()) { *error_msg = strdup(handle.error().message().c_str()); return nullptr; } if (handle.value() != nullptr) { return handle.value(); } } // Fall back to the system namespace. This happens for preloaded JNI // libraries in the zygote. // TODO(b/185833744): Investigate if this should fall back to the app main // namespace (aka anonymous namespace) instead. void* handle = OpenSystemLibrary(path, RTLD_NOW); if (handle == nullptr) { *error_msg = strdup(dlerror()); } return handle; } std::lock_guard<std::mutex> guard(g_namespaces_mutex); NativeLoaderNamespace* ns; //涉及到了namespace,如果當前classloader沒有,則創(chuàng)建,但是這屬于異常情況 if ((ns = g_namespaces->FindNamespaceByClassLoader(env, class_loader)) == nullptr) { // This is the case where the classloader was not created by ApplicationLoaders // In this case we create an isolated not-shared namespace for it. Result<NativeLoaderNamespace*> isolated_ns = CreateClassLoaderNamespaceLocked(env, target_sdk_version, class_loader, /*is_shared=*/false, /*dex_path=*/nullptr, library_path, /*permitted_path=*/nullptr, /*uses_library_list=*/nullptr); if (!isolated_ns.ok()) { *error_msg = strdup(isolated_ns.error().message().c_str()); return nullptr; } else { ns = *isolated_ns; } } return OpenNativeLibraryInNamespace(ns, path, needs_native_bridge, error_msg);
這里我們打斷一下,我們看到上面代碼分析,如果當前classloader的namespace如果為null,則創(chuàng)建,這里我們也知道一個信息,namespace是跟classloader綁定的。同時我們也知道,classloader在創(chuàng)建的時候,其實就會綁定一個namespace。我們在app加載的時候,就會通過LoadedApk這個class去加載一個pathclassloader
frameworks/base/core/java/android/app/LoadedApk.java if (!mIncludeCode) { if (mDefaultClassLoader == null) { StrictMode.ThreadPolicy oldPolicy = allowThreadDiskReads(); mDefaultClassLoader = ApplicationLoaders.getDefault().getClassLoader( "" /* codePath */, mApplicationInfo.targetSdkVersion, isBundledApp, librarySearchPath, libraryPermittedPath, mBaseClassLoader, null /* classLoaderName */); setThreadPolicy(oldPolicy); mAppComponentFactory = AppComponentFactory.DEFAULT; } if (mClassLoader == null) { mClassLoader = mAppComponentFactory.instantiateClassLoader(mDefaultClassLoader, new ApplicationInfo(mApplicationInfo)); } return; }
之后ApplicationLoaders.getDefault().getClassLoader會調(diào)用createClassLoader
public static ClassLoader createClassLoader(String dexPath, String librarySearchPath, String libraryPermittedPath, ClassLoader parent, int targetSdkVersion, boolean isNamespaceShared, String classLoaderName, List<ClassLoader> sharedLibraries, List<String> nativeSharedLibraries, List<ClassLoader> sharedLibrariesAfter) { final ClassLoader classLoader = createClassLoader(dexPath, librarySearchPath, parent, classLoaderName, sharedLibraries, sharedLibrariesAfter); String sonameList = ""; if (nativeSharedLibraries != null) { sonameList = String.join(":", nativeSharedLibraries); } Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "createClassloaderNamespace"); //這里就講上述的屬性傳入,創(chuàng)建了一個屬于該classloader的namespace String errorMessage = createClassloaderNamespace(classLoader, targetSdkVersion, librarySearchPath, libraryPermittedPath, isNamespaceShared, dexPath, sonameList); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); if (errorMessage != null) { throw new UnsatisfiedLinkError("Unable to create namespace for the classloader " + classLoader + ": " + errorMessage); } return classLoader; }
這里我們得到的主要消息是,我們的classloader的namespace,里面的so檢索路徑,其實都在創(chuàng)建的時候就被定下來了(這個也是,為什么想要實現(xiàn)so動態(tài)加載,其中的一個方案就是替換classloader的原因,因為我們當前使用的classloader的namespace檢索路徑,已經(jīng)是固定了,后續(xù)對classloader本身的檢索路徑添加,是不會同步給namespace的,只有創(chuàng)建的時候才會同步)
好了,我們繼續(xù)回到OpenNativeLibrary,內(nèi)部其實調(diào)用android_dlopen_ext打開
void* android_dlopen_ext(const char* filename, int flag, const android_dlextinfo* extinfo) { const void* caller_addr = __builtin_return_address(0); return __loader_android_dlopen_ext(filename, flag, extinfo, caller_addr); }
這里不知道大家有沒有覺得眼熟,這里肯定最終調(diào)用就是dlopen,只不過谷歌為了限制dlopen的調(diào)起方,采用了__builtin_return_address 內(nèi)建函數(shù)作為卡口,限制了普通app調(diào)喲dlopen(這里也是有破解方法的)
之后的經(jīng)歷android_dlopen_ext -> dlopen_ext ->do_dlopen,最終到了最后加載的方法了
void* do_dlopen(const char* name, int flags, const android_dlextinfo* extinfo, const void* caller_addr) { std::string trace_prefix = std::string("dlopen: ") + (name == nullptr ? "(nullptr)" : name); ScopedTrace trace(trace_prefix.c_str()); ScopedTrace loading_trace((trace_prefix + " - loading and linking").c_str()); soinfo* const caller = find_containing_library(caller_addr); // 找到調(diào)用者,屬于哪個namespace android_namespace_t* ns = get_caller_namespace(caller); ... ProtectedDataGuard guard; 之后就是在namespace的加載列表找library的過程了 soinfo* si = find_library(ns, translated_name, flags, extinfo, caller); .... return nullptr; }
總結(jié)
最后我們先總結(jié)一下,Linker作用跟NameSpace的調(diào)用流程,可以發(fā)現(xiàn)其實內(nèi)部非常復雜,但是我們抓住主干去看,NameSpace其實作用的功能,也就是規(guī)范了查找so的過程,需要在指定列表查找。
以上就是so加載Linker跟NameSpace機制詳解的詳細內(nèi)容,更多關(guān)于so加載Linker NameSpace機制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中使用findViewByMe提升組件查找效率
本文主要介紹了Android中使用findViewByMe提升組件查找效率的相關(guān)知識。具有很好的參考價值。下面跟著小編一起來看下吧2017-03-03Android設計模式之代理模式Proxy淺顯易懂的詳細說明
Android設計模式之代理模式也是平時比較常用的設計模式之一,代理模式其實就是提供了一個新的對象,實現(xiàn)了對真實對象的操作,或成為真實對象的替身2018-03-03Android自定義recyclerView實現(xiàn)時光軸效果
這篇文章主要介紹了Android自定義recyclerView實現(xiàn)時光軸效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01Android使用DrawerLayout實現(xiàn)仿QQ雙向側(cè)滑菜單
這篇文章主要介紹了Android使用DrawerLayout實現(xiàn)仿QQ雙向側(cè)滑菜單的方法和詳細代碼,有需要的小伙伴可以認真參考下。2016-01-01Android使用Javamail發(fā)送Email群發(fā)加附件
這篇文章主要為大家詳細介紹了Android使用Javamail發(fā)送Email群發(fā)加附件,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01Android控件系列之相冊Gallery&Adapter適配器入門&控件縮放動畫入門
本文介紹了如何使用Gallery打造簡單的相冊,并實現(xiàn)了與用戶點擊的互動動畫,并介紹了適配器的原理。您可以在此基礎(chǔ)上修改,實現(xiàn)自己的相冊,嵌入到任何程序中都會增色不少2012-11-11