so加載Linker跟NameSpace機(jī)制詳解
前言
so庫的加載可是我們?nèi)粘i_發(fā)都會(huì)用到的,因此系統(tǒng)也提供了非常方便的api給我們進(jìn)行調(diào)用
System.loadLibrary(xxxso);
當(dāng)然,隨著版本的變化,loadLibrary也是出現(xiàn)了非常大的變化,最重要的是分水嶺是androidN加入了namespace機(jī)制,可能很多人都是一頭霧水噢!這是個(gè)啥?我們在動(dòng)態(tài)so加載方案中,會(huì)頻繁出現(xiàn)這個(gè)名詞,同時(shí)還有一個(gè)高頻的詞就是Linker,本期不涉及復(fù)雜的技術(shù)方案,我們就來深入聊聊,Linker的概念,與namespace機(jī)制的加入,希望能幫助更多開發(fā)者去了解so的加載過程。
Linker
我們都知道,Linux平臺(tái)下有動(dòng)態(tài)鏈接文件(.so)與靜態(tài)鏈接文件(.a),兩者其實(shí)都是一種ELF文件(相關(guān)的文件格式我們不贅述)。為什么會(huì)有這么兩種文件呢?我們就從簡單的角度來想一次,其實(shí)就是為了更多的代碼復(fù)用,比如程序1,程序2都用到了同一個(gè)東西,比如xx.so

此時(shí)就會(huì)出現(xiàn),程序1與程序2中調(diào)用fun common的地方,在沒有鏈接之前,調(diào)用處的地址,我們以“stub”,表示這其實(shí)是一個(gè)未確定的東西,而后續(xù)的這個(gè)地址填充(寫入正確的common地址),其實(shí)就是Linker的職責(zé)。
我們通過上面的例子,其實(shí)就可以明白,Linker,主要的職責(zé),就是幫助查找當(dāng)前程序所依賴的動(dòng)態(tài)庫文件(ELF文件)。那么Linker本身是個(gè)什么呢,其實(shí)他跟.so文件都是同一種格式,也是ELF文件,那么Linker又由誰幫助加載啟動(dòng)呢,這里就會(huì)出現(xiàn)存在一個(gè)(雞生蛋,蛋生雞)的問題,而ELF文件給出的答案就是,設(shè)立一個(gè):interp 的段,當(dāng)一個(gè)進(jìn)程啟動(dòng)的時(shí)候(linux中通過execv啟動(dòng)),此時(shí)就會(huì)通過load_elf_binary函數(shù),先加載ELF文件,然后再調(diào)用load_elf_interp方法,直接加載了:interp 段地址的起點(diǎn),從而能夠構(gòu)建我們的大管家Linker,當(dāng)然,Linker本身就不能像普通的so文件一樣,去依賴另一個(gè)so,其實(shí)原因也很簡單,沒人幫他初始化呀!因此Linker是采用配置的方式先啟動(dòng)起來了!
當(dāng)然,我們主要的目標(biāo)是建立概念,Linker本身涉及的復(fù)雜加載,我們也不繼續(xù)貼出來了
NameSpace
在以往的anroidN以下版本中,加載so庫通常是直接采用dlopen的方式去直接加載的,對(duì)于非公開的符號(hào),如果被使用,就容易在之后迭代出現(xiàn)問題,(類似java,使用了一個(gè)三方庫的private方法,如果后續(xù)變更方法含義,就會(huì)出現(xiàn)問題),因此引入了NameSpace機(jī)制
Android 7.0 為原生庫引入了命名空間,以限制內(nèi)部 API 可見性并解決應(yīng)用意外使用平臺(tái)庫而不是自己的平臺(tái)庫的情況。
我們說的NameSpace,主要對(duì)應(yīng)著一個(gè)數(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_;默認(rèn)可訪問路徑
std::vector<std::string> permitted_paths_;已允許訪問路徑
....
我們來看一看,這個(gè)數(shù)據(jù)結(jié)構(gòu)在哪里會(huì)被使用到,其實(shí)就是so庫加載過程。當(dāng)我們調(diào)用System.loadLibrary的時(shí)候,其實(shí)最終調(diào)用的是
private synchronized void loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) {
文件名校驗(yàn)
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的時(shí)機(jī),要么so文件名加載不合法,要么就是nativeLoad方法返回了錯(cuò)誤信息,這里是需要我們注意的,我們?nèi)绻霈F(xiàn)這個(gè)異常,可以從這里排查,nativeLoad方法最終通過LoadNativeLibrary,在native層真正進(jìn)入so的加載過程
LoadNativeLibrary 非常長,我們截取部分
bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
const std::string& path,
jobject class_loader,
jclass caller_class,
std::string* error_msg) {
會(huì)判斷是否已經(jīng)加載過當(dāng)前so,同時(shí)也要加鎖,因?yàn)榇嬖诙嗑€程加載的情況
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,如果當(dāng)前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);
這里我們打斷一下,我們看到上面代碼分析,如果當(dāng)前classloader的namespace如果為null,則創(chuàng)建,這里我們也知道一個(gè)信息,namespace是跟classloader綁定的。同時(shí)我們也知道,classloader在創(chuàng)建的時(shí)候,其實(shí)就會(huì)綁定一個(gè)namespace。我們在app加載的時(shí)候,就會(huì)通過LoadedApk這個(gè)class去加載一個(gè)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會(huì)調(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)建了一個(gè)屬于該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檢索路徑,其實(shí)都在創(chuàng)建的時(shí)候就被定下來了(這個(gè)也是,為什么想要實(shí)現(xiàn)so動(dòng)態(tài)加載,其中的一個(gè)方案就是替換classloader的原因,因?yàn)槲覀儺?dāng)前使用的classloader的namespace檢索路徑,已經(jīng)是固定了,后續(xù)對(duì)classloader本身的檢索路徑添加,是不會(huì)同步給namespace的,只有創(chuàng)建的時(shí)候才會(huì)同步)
好了,我們繼續(xù)回到OpenNativeLibrary,內(nèi)部其實(shí)調(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)用者,屬于哪個(gè)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)其實(shí)內(nèi)部非常復(fù)雜,但是我們抓住主干去看,NameSpace其實(shí)作用的功能,也就是規(guī)范了查找so的過程,需要在指定列表查找。
以上就是so加載Linker跟NameSpace機(jī)制詳解的詳細(xì)內(nèi)容,更多關(guān)于so加載Linker NameSpace機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
dialog dismiss時(shí)鍵盤不消失的問題淺析及解決辦法
這篇文章主要介紹了dialog dismiss時(shí)鍵盤不消失的問題淺析及兩種解決方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下2017-01-01
Android系統(tǒng)檢測程序內(nèi)存占用各種方法
這篇文章主要介紹了Android系統(tǒng)檢測程序內(nèi)存占用各種方法,本文講解了檢查系統(tǒng)總內(nèi)存、檢查某個(gè)程序的各類型內(nèi)存占用、檢查程序狀態(tài)、檢查程序各部分的內(nèi)存占用等內(nèi)容,需要的朋友可以參考下2015-03-03
自定義Toast工具類ToastUtil防止多次點(diǎn)擊時(shí)Toast不消失的方法
下面小編就為大家?guī)硪黄远xToast工具類ToastUtil防止多次點(diǎn)擊時(shí)Toast不消失的方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-04-04
Android簡單實(shí)現(xiàn)屏幕下方Tab菜單的方法
這篇文章主要介紹了Android簡單實(shí)現(xiàn)屏幕下方Tab菜單的方法,簡單分析了Android實(shí)現(xiàn)tab菜單所涉及的界面布局及功能相關(guān)操作技巧,需要的朋友可以參考下2016-08-08
Android自定義ViewGroup實(shí)現(xiàn)右滑進(jìn)入詳情
這篇文章主要為大家詳細(xì)介紹了Android如何通過自定義ViewGroup實(shí)現(xiàn)右滑進(jìn)入詳情效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-01-01
Android編程開發(fā)之多點(diǎn)觸摸(Multitouch)實(shí)現(xiàn)方法
這篇文章主要介紹了Android編程開發(fā)之多點(diǎn)觸摸(Multitouch)實(shí)現(xiàn)方法,結(jié)合實(shí)例形式詳細(xì)分析了Android多點(diǎn)觸摸的相關(guān)實(shí)現(xiàn)步驟與操作技巧,需要的朋友可以參考下2016-08-08
Android實(shí)現(xiàn)自定義Crash handler記錄崩潰信息實(shí)例代碼
這篇文章主要給大家介紹了Android實(shí)現(xiàn)自定義Crash handler記錄崩潰信息的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-02-02
Android實(shí)現(xiàn)WIFI和GPRS網(wǎng)絡(luò)的切換
這篇文章主要介紹了Android實(shí)現(xiàn)WIFI和GPRS網(wǎng)絡(luò)的切換,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11
Flutter實(shí)現(xiàn)抖音點(diǎn)贊效果
抖音的點(diǎn)贊效果在第一次看到的時(shí)候,總有一種眼前一亮的感覺。一邊看視頻,還能在視頻上點(diǎn)贊,而且整個(gè)屏幕都能夠點(diǎn)贊,并伴隨動(dòng)畫,還是很炫酷的。今天我們用Flutter來實(shí)現(xiàn)一下這個(gè)效果2021-05-05

