运行时加载动态链接库(Android、Windows)

  • 若不修改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。可用以下方法解决问题。

  1. 打包apk时就包含libroseaplt.so。
  2. (如果能使用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>>'

 

全部评论: 0

    写评论: