安卓 APP 执行二进制程序实践
由于安卓沙盒模型不断更新,在 APP 中执行二进制变得越发困难和受限制。而受限于实际业务需求(或应用生态等),有时不得不需要在安卓平台上执行一些 Linux 二进制程序。鉴于相关领域的资料较少,因此将自己的相关经验做一下整理。
截至目前(API 版本 35,安卓 15),应用在 BinaryDir
中(一般位于 /data/app/<随机字符串>/<你的应用包名>-<随机字符串>/lib/
)的二进制程序具有执行权限,可以由 Runtime.exec
等直接调用执行。
此目录中的二进制文件为只读,只能由开发者预先放置在 APK 安装包中,并在应用安装时由安装器释放到 APP 安装目录中,无法直接写入或热更新。这样的限制符合高版本安卓对应用的 W^X 限制要求,可以有效防止恶意代码的动态注入和执行。
你可以按照如下流程为安卓 APP 打包二进制程序,并在 APP 运行时对其进行调用:
1. 获取安卓 NDK 编译的二进制程序及其依赖
安卓使用非标准的 Bionic C 库,因此普通 Linux Arm64 二进制无法在安卓平台上直接运行,需要使用安卓 NDK 进行交叉编译
- 如果你需要执行的东西比较热门,可以尝试在 Termux 中安装,然后将对应的二进制程序(通常在
/data/data/com.termux/files/usr/bin
)和 so 依赖库(通常在/data/data/com.termux/files/usr/lib
)从 Termux 中复制出来备用,省去自己编译的麻烦 - 建议寻找对应二进制程序的 NDK 静态编译版本,静态编译的二进制嵌入起来最为方便,不存在任何依赖库的问题
2. 放置二进制程序到应用中
将二进制程序(及其依赖库)放到安卓项目的 jniLibs/对应ABI/
目录下,并且文件名格式必须遵循 libxxx.so
的格式。如图所示:
如果你的二进制文件没有遵循
libxxx.so
的命名格式,在最终安装 Release 版 APP 时这些文件将不会被安装,进而将导致程序无法正常工作。这个特性源于安卓一个长久未修复的 BUG,参见:only-files-named-are-copied-by-enforced-for-api-level-35
3. 修改配置文件
放置完二进制文件后,开始修改配置文件:
在
AndroidManifest.xml
中为<application>
标签增加android:extractNativeLibs="true"
属性在
build.gradle
中设置 NDK 支持的二进制 ABI,一般按如下设置即可:
android {
...
defaultConfig {
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
...
}
...
}
4. 运行二进制程序
现在,就可以使用 Runtime.exec
/ ProcessBuilder
等 API 执行 BinaryDir
中的二进制程序了。
对于拥有动态链接依赖库的二进制程序,需要手动配置 LD_LIBRARY_PATH
环境变量,让链接器在运行时可以找到对应的依赖库。示例代码如下:
String binaryDir = getApplicationInfo().nativeLibraryDir
String executable = binaryDir + File.separator + "xxxx";
List<String> cmd = Arrays.asList(executable, "--parameter1", "--parameter2");
try {
ProcessBuilder pb = new ProcessBuilder(cmd);
Map<String, String> env = pb.environment();
env.put("LD_LIBRARY_PATH", binaryDir); // 设置 LD_LIBRARY_PATH
Process process = pb.start();
// ....
} catch (IOException | InterruptedException e) {
Log.e("MyTag", "发生异常: " + e.getMessage());
}
5. 报错了?
5.1. 依赖库缺失
如果依赖库缺失,pb.start()
会抛出异常,在异常信息中可以看到缺失的依赖库名字。按照前面的方法在 jniLibs
中补全缺失的依赖库,然后重新编译安装 APP 运行即可。
5.2. 二进制文件命名问题
前面说了,jniLibs
中二进制文件名必须遵循 libxxx.so
的格式,否则会出现问题。
这个特性十分烦人,因为你所使用的程序和库往往并不能完全符合这种要求(比如会有程序依赖类似 libzstd.so.1
这种名字的动态库)
此时可以用符号链接来解决问题。具体而言,在每次 APP 开始运行前,到私有目录中(FilesDir
)为 BinaryDir
下的所有二进制文件创建符号链接。由于符号链接可以自定义名字,因此你可以提前在 APP 私有目录中用符号链接还原原来的二进制依赖的文件名,随后直接在私有目录下用符号链接执行此二进制文件。
解包二进制到私有目录的辅助函数如下:
// 当前平台下对应架构的 libxxx.so 将会被链接到 {linkingDir}/libxxx.so,注意linkingDir必须在应用私有目录下!
// 如果fileNameMapping中有自定义名字映射,符号链接将创建为对应的设定好的文件名
private void linkBinariesToDir(File linkingDir, Map<String, String> fileNameMapping) {
if(linkingDir.exists())
deleteDir(linkingDir);
linkingDir.mkdirs();
File nativeLibsFolder = new File(getApplication().getApplicationInfo().nativeLibraryDir);
File[] files = nativeLibsFolder.listFiles();
if (files == null)
return;
for (File srcFile : files) {
if (srcFile.isFile()) {
String destName = srcFile.getName();
if(fileNameMapping.containsKey(destName))
destName = fileNameMapping.get(destName);
File linkFile = new File(linkingDir, destName);
try {
Os.symlink(srcFile.getAbsolutePath(), linkFile.getAbsolutePath());
} catch (ErrnoException e) {
print("Fail to create symbolic link: " + e.getMessage());
}
}
}
}
// 辅助函数,删除目录
public static boolean deleteDir(File f) {
if(f == null)
return true;
if (f.isDirectory()) {
String[] children = f.list();
if (children != null) {
for (String child : children) {
boolean success = deleteDir(new File(f, child));
if (!success) {
return false;
}
}
}
}
return f.delete();
}
调用示例:
File linkingDir = new File(getApplication().getFilesDir(), "adb_dir")
linkBinariesToDir(linkingDir, Map.of(
"libz.so.1.3.1.so", "libz.so.1",
"libzstd.so.1.5.7.so", "libzstd.so.1",
"libadb.so", "adb"
));
调用后查看目录 /data/user/0/<包名>/files/adb_dir
,就可以看到里面的二进制文件和依赖库
并且二进制文件此时应该可以直接被执行。示例代码如下:
String executable = new File(linkingDir, "adb")
List<String> cmd = Arrays.asList(executable.getAbsolutePath(), "--version");
try {
ProcessBuilder pb = new ProcessBuilder(cmd);
Map<String, String> env = pb.environment();
env.put("LD_LIBRARY_PATH", linkingDir); // 设置 LD_LIBRARY_PATH 为 linkingDir
Process process = pb.start();
// ....
} catch (IOException | InterruptedException e) {
Log.e("MyTag", "发生异常: " + e.getMessage());
}
6. Reference
What path to put executable to run on Android 29? - Stack Overflow