- 若不修改android内核,要想加载成功,*.so必须放在系统要求的指定目录。而那几个指定目录,app没权限写入。也就是说,要想动态加载成功,必须在打包apk时就包含这个*.so。只是在app运行时,可按自个要求选个加载时机而已。
- 对android 7.1.2内核,要达到加载自定义路径下的libroseaplt.so,修改分在两处。一是linker.cpp,让libroseaplt.so进入灰名单。二是mmap.c,(vm_flags & VM_EXEC) == true时,不返回-EPERM。
一、android
假设要加载的动态链接库文件名是libroseaplt.so。涉及到的api中,dlopen打开*.so,dlsym得到库中函数,dlclose关闭*.so。
首先看android能不加载指定目录下的*.so。假设要加设外部存储中的libroseaplt.so,全路径是/sdcard/android/data/com.kos.launcher/files/libroseaplt.so。
dlopen("/sdcard/android/data/com.kos.launcher/files/libroseaplt.so", RTLD_NOW|RTLD_LOCAL);
执行它后会报以下错误:
E/linker: library "/sdcard/android/data/com.kos.launcher/files/libroseaplt.so" ("/storage/emulated/0/Android/data/com.kos.launcher/files/libroseaplt.so") needed or dlopened by "/data/app/com.kos.launcher-1/lib/arm/libSDL2.so" is not accessible for the namespace: [name="classloader-namespace", ld_library_paths="", default_library_paths="/data/app/com.kos.launcher-1/lib/arm:/data/app/com.kos.launcher-1/base.apk!/lib/armeabi-v7a", permitted_paths="/data:/mnt/expand:/data/data/com.kos.launcher"]
报这个错误的原因是dlopen只能加载指定目录下的*.so,而参数要求的路径不在指定目录下。错误提示给出了哪些是指定目录。
- /data/app/com.kos.launcher-1/lib/arm。解压缩apk后、存放*.so的目录。
- /data/app/com.kos.launcher-1/base.apk!/lib/armeabi-v7a。/data/app/com.kos.launcher-1/base.apk就是app的apk包。后面加“!”,是因为它存在文件系统中的是文件,不是目录,但它是可以解压的,解压后是目录。
错误提示中出现“libSDL2.so”,不是说libroseaplt.so依赖libSDL2.so,而是libSDL2.so调用了dlopen。如果调用dlopen的是libmain.so,那“needed or dlopened by”后面就会变成libmain.so。
综上所述,出错原因是/sdcard/android/data/com.kos.launcher/files不是“default_library_paths”,dlopen只能打开在default_library_paths中的*.so。可用以下方法解决问题。
- 打包apk时就包含libroseaplt.so。
- (如果能使用adb)运行过程中把libroseaplt.so复制到/data/app/com.kos.launcher-1/lib/arm。这个须要注意文件权限问题,复制到arm目录下后system用户须有读取该文件的权限。
但对于打包apk不能得到libroseaplt.so,并且没有访问/data/app/com.kos.launcher-1/lib/arm权限的app来说,要想达到目的,只能修改android内核。
二、修改android内核
android内核是firefly rk3399 android-7.1.2
/sdcard/Android/data/com.kos.launcher/files/libroseaplt.so ls -l libroseaplt.so时记录显示 -rw-rw---- 1 u0_a63 sdcard_rw 3020 2013-01-18 08:56 libroseaplt.so
要加载的libroseaplt.so放在/sdcard/Android/data/com.kos.launcher/files,所有人都没有“x”权限。
2.1 修改linker.cpp,让libroseaplt.so进入灰名单
<aosp>/bionic/linker/linker.cpp ------ static bool load_library(android_namespace_t* ns, LoadTask* task, LoadTaskList* load_tasks, int rtld_flags, const std::string& realpath) { ... // name: /sdcard/Android/data/com.kos.launcher/files/libroseaplt.so // realpath: /storage/emulated/0/Android/data/com.kos.launcher/files/libroseaplt.so // /sdcard是个符号连接,它真实指向的是/storage/emulated/0 if (!ns->is_accessible(realpath)) { // *.so所在的路径不是可访问的,会进入这里。 if (is_greylisted(name, needed_by)) { // 这里有个灰名单判断,做的就是让libroseaplt.so进入灰名单。 } else { // 在这里就会logcat出那条错误语句。 return false; } } ... }
报错的根源是“is_accessible”返回false,为不报这错误有两种方法。一是让"/storage/emulated/0/Android/data/com.kos.launcher/files"成为andorid承认的*.so的可加载路径,举个例子,让它进入“default_library_paths”数组。二是回看处理逻辑,即使is_accessible失败了,只要后面的is_greylisted返回true,那也是不会报错。为简单,这里投机取巧用第二方法。
<aosp>/bionic/linker/linker.cpp ------ static bool is_greylisted(const char* name, const soinfo* needed_by) { static const char* const kLibraryGreyList[] = { "libandroid_runtime.so", "libbinder.so", "libcrypto.so", ... nullptr }; // 增加以下这个if if (name[0] == '/') { // and reduce the path to basename const char* name2 = basename(name); if (strcmp(name2, "libroseaplt.so") == 0) { return true; } } // limit greylisting to apps targeting sdk version 23 and below if (get_application_target_sdk_version() > 23) { return false; } ... }
修改is_greylisted,不管路径,只要加载的文件名是libroseaplt.so,就返回true。
经过以上修改后,还是会报一个错误,只是这错误变成:
E/linker: couldn't map "/storage/emulated/0/Android/data/com.kos.launcher/files/libroseaplt.so" segment 1: Operation not permitted
这个错误的原因是后绪执行的ElfReader::LoadSegments会调用mmap64,把一块文件数据映射到内存,但失败了。
2.2 修改mmap.c,(vm_flags & VM_EXEC) == true时,不返回-EPERM
要追查错误原因需溯源mmap64,mmap64流程可参考“Android中mmap原理及应用简析”,这里直接指出结果,问题出在linux层的do_mmap。
<aosp>/kernel/mm/mmap.c ------ unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, vm_flags_t vm_flags, unsigned long pgoff, unsigned long *populate) { ... if (file) { struct inode *inode = file_inode(file); switch (flags & MAP_TYPE) { case MAP_SHARED: ... case MAP_PRIVATE: if (!(file->f_mode & FMODE_READ)) return -EACCES; if (path_noexec(&file->f_path)) { // 注释掉以下两条语句 // if (vm_flags & VM_EXEC) // return -EPERM; vm_flags &= ~VM_MAYEXEC; } ... default: return -EINVAL } } ... }
不清楚path_noexec功能是什么,但至少1)不是判断file->f_path这个路径(文件)是否可执行(ls命令显示'x'标记)。因为libroseaplt.so位在/data/app/com.kos.launcher下时,没有“x”,path_noexec返回false。2)libroseaplt.so放在/sdcard时,path_noexec返回true。
不能强制让vm_flags没有VM_EXEC标记,那只好注释掉if (vm_flags & VM_EXEC) == true时,不返回-EPERM。
三、windows
假设要加载的动态链接库文件名是liblaser.dll。涉及到的api中,LoadLibrary打开*.dll,GetProcAddress得到库中函数,FreeLibrary关闭*.dll。
要避免使用*.def文件。以下是dll中的cpp文件示例。
#include <SDL.h> #include <string> // 声明中必须包含两点, // 1)extern "C"; // 2)__declspec(dllexport),即DECLSPEC。用DECLSPEC是因为这代码要跨平台。 std::string test(const std::string& a) { return "1"; } DECLSPEC std::string test_dllexport(const std::string& a) { return "2"; } extern "C" std::string test_externC(const std::string& a) { return "3"; } extern "C" DECLSPEC std::string test_externC_dllexport(const std::string& a) { return "4"; }
接下用dumpbin查看liblaser.dll输出函数。

虽然该dll定义了4个函数,但只输出两个:test_dllexport、test_externC_dllexport。输出函数必须带__declspec(dllexport)修饰。
在这两个输出函数中,GetProcAddress只能得到test_externC_dllexport的函数地址。去获取test_dllexport地址时,返回NULL。也就是说,要能被GetProcAddress,必须同时有extern "C"、__declspec(dllexport)。
GetProcAddress要求输出函数是stdcall/C风格的调用约定。1)函数名以一个下划线字符为前缀。2)函数名后面就没字符了,不像__cdecl还有“@”以及后面的字符。
由于GetProcAddress要求输出函数是C调用风格,而参数和返回值都用了C++类std::string,编译时会出警告。
2>C:\ddksample\apps-src\apps\main1\dllmain.cpp(15,78): warning C4190: 'test_externC_dllexport' has C-linkage specified, but returns UDT 'std::basic_string<char,std::char_traits<char>,std::allocator<char>>' which is incompatible with C 2>C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.30.30705\include\xstring(4920): message : see declaration of 'std::basic_string<char,std::char_traits<char>,std::allocator<char>>'