android linux 工具 android on linux
概述
本文主要介绍如何在Linux(x86)主机上简单高效地运行一个Android可执行程序,主要使用类似LXC的技术,将Android可执行程序在容器中运行。首先会介绍如何运行Android x86程序,然后会介绍如何使用libhoudini运行Android ARM程序。文章中使用的程序可到https://gitee.com/cqupt/android_on_linux查看。
直接运行Android x86程序
运行静态编译的程序
我们先写一个简单的Android可执行程序,使用NDK编译,可以参考ndk-build的使用介绍。
// main.c #include <stdio.h> int main(int argc, char const *argv[]) { printf("hello world!\n"); return 0; }// Android.mk LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_SRC_FILES:= main.c LOCAL_MODULE := main include $(BUILD_EXECUTABLE)// Application.mk ## APP_ABI := arm64-v8a APP_ABI := x86_64 APP_PLATFORM := android-19关于Android ABI的介绍:https://developer.android.com/ndk/guides/abis?hl=zh-cn
此时因为我们想直接在Linux x86上执行此程序,所以使用的ABI是x86_64,开始编译并运行:
chenls@chenls-PC:jni$ ndk-build [x86_64] Compile : main <= main.c [x86_64] Executable : main [x86_64] Install : main => libs/x86_64/main chenls@chenls-PC:jni$ ../libs/x86_64/main bash: ../libs/x86_64/main: 没有那个文件或目录此时运行报错,我们来查看一下原因:
chenls@chenls-PC:jni$ file ../libs/x86_64/main ../libs/x86_64/main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /system/bin/linker64, BuildID[sha1]=c253c19cb84ea278fa41e8360fdf4f13a60d9d63, stripped chenls@chenls-PC:jni$ chenls@chenls-PC:jni$ gcc main.c chenls@chenls-PC:jni$ file a.out a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/.2, for GNU/Linux 3.2.0, BuildID[sha1]=e53d68617f04135f77102f0e87226709ef80cb50, not stripped我们使用file对比了ndk-build和主机gcc分别编译的文件发现,Android是使用/system/bin/linker64作为链接器(关于它的介绍:Android Linker),而在linux主机上是使用的是/lib64/.2(关于它的介绍:ld-linux.so)。它们的作用都是来加载动态库的。
查看../libs/x86_64/main文件的依赖:
chenls@chenls-PC:jni$ readelf -a ../libs/x86_64/main | grep NEED [ 9] .gnu.version_r VERNEED 000000000000040c 0000040c 0x0000000000000001 (NEEDED) 共享库:[libc.so] 0x0000000000000001 (NEEDED) 共享库:[libm.so] 0x0000000000000001 (NEEDED) 共享库:[libstdc++.so] 0x0000000000000001 (NEEDED) 共享库:[libdl.so] 0x000000006ffffffe (VERNEED) 0x40c 0x000000006fffffff (VERNEEDNUM) 1 chenls@chenls-PC:jni$../libs/x86_64/main依赖了libc.so等其它动态库。因为Android与Linux使用了不能的链接器,导致Android程序在Linux无法正常加载动态库。此时我们可以尝试将此程序静态编译。
修改Android.mk文件,使其静态编译。
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) // 新增 LOCAL_LDFLAGS := -static LOCAL_SRC_FILES:= main.c LOCAL_MODULE := main include $(BUILD_EXECUTABLE)重新编译,检查依赖,然后运行。
chenls@chenls-PC:jni$ ndk-build [x86_64] Install : main => libs/x86_64/main chenls@chenls-PC:jni$ readelf -a ../libs/x86_64/main | grep NEED chenls@chenls-PC:jni$ ../libs/x86_64/main hello world!此时一个简单的Android可执行程序能直接在Linux主机上运行了,但是实际项目依赖的库较多,并不能全部都能静态编译,而且我们会更青睐使用动态库的方式。接下来试试如何运行包含动态库的可执行程序。
运行带动态库的程序
可以在Android.mk中增加链接参数,指定链接器linker的位置:
ifeq ($(APP_ABI),x86_64) LOCAL_LDFLAGS += -Wl,--dynamic-linker=linker64 -Wl,-rpath=./ endif重新编译后,准备好main所依赖的x86_64的动态库和linker64:
chenls@chenls-PC:android_on_linux$ tree linker_path/ tree linker_path/ ├── libc.so ├── libdl.so ├── libm.so ├── libstdc++.so ├── linker64 └── main 0 directories, 6 files这样就可以直接运行x86_64的包含动态库的可执行程序了
使用clone和chroot运行Android X86程序
技术要点
接下来这一步走得比较艰难,刚开始一直没有找到头绪,最后发现了xDroid ,它是一款让android应用运行在PC上的服务平台(一个“Android模拟器”,之所以是加引号的模拟器,因为它使用不是模拟器,而是使用的LXC容器技术,从而能获得更加的性能)。之后又发现与另一个“Android模拟器”–Anbox,同样也是使用了LXC,我们有理由相信,它将是一个突破口。
什么是LXC?
LXC是Linux内核包含功能的用户空间接口。
当前的LXC使用以下内核功能来包含进程:
- 内核名称空间(ipc,uts,mount,pid,网络和用户)
- Apparmor和SELinux配置文件
- Seccomp政策
- chroots(使用pivot_root)
- 内核功能
- CGroups(对照组)
- LXC容器通常被视为chroot和成熟的虚拟机之间的中间对象。LXC的目标是创建一个尽可能接近标准Linux安装环境的环境,而不需要单独的内核。
在LXC Chroot Cgroup Namespace文章中总结到:
LXC, LinuX Containers,它是一个加强版的Chroot。简单的说,LXC就是将不同的应用隔离开来,这有点类似于chroot,chroot是将应用隔离到一个虚拟的私有root下,而LXC在这之上更进了一步。LXC内部依赖Linux内核的3种隔离机制(isolation infrastructure):
- Chroot
- Cgroups
- Namespaces
在DOCKER基础技术:LINUX NAMESPACE(上)文章中详细说明了Linux Namespace的使用。接下来跟着前人的步伐实践一下吧!
实践一下
参考DOCKER基础技术:LINUX NAMESPACE(上),我们需要准备好Android需要的rootfs文件夹。
chenls@chenls-PC:android_on_linux$ tree rootfs/ rootfs/ ├── proc └── system ├── bin │ ├── linker64 │ └── main └── lib64 ├── libc.so ├── libdl.so ├── libm.so └── libstdc++.so 4 directories, 8 files上述文件就是main程序(前面的示例代码,使用ndk-build非静态编译)必须所依赖的动态库和linker64,如果实际项目中依赖其它的库,需要再手动添加它们。另外这些库必须是Android X86平台中的,可以到android-x86下载。
下面就开始写代码:
// android_on_linux.c #define _GNU_SOURCE #include <sched.h> #include <signal.h> #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> #include <sys/mount.h> /* 定义一个给 clone 用的栈,栈大小10M */ #define STACK_SIZE (10 * 1024 * 1024) static char container_stack[STACK_SIZE]; char *container_args[] = {"/system/bin/main", NULL}; int container_main(void *arg) { printf("Container [%5d] - inside the container!\n", getpid()); if (mount("proc", "rootfs/proc", "proc", 0, NULL) != 0) { perror("proc"); } if (chdir("./rootfs") != 0 || chroot("./") != 0) { perror("chdir/chroot"); } printf("execv %s\n", container_args[0]); execv(container_args[0], container_args); perror("exec"); printf("Something's wrong! %s\n", container_args[0]); return 1; } int main(int argc, char const *argv[]) { printf("Parent [%5d] - start a container!\n", getpid()); int container_pid = clone(container_main, container_stack + STACK_SIZE, CLONE_NEWPID | SIGCHLD, NULL); waitpid(container_pid, NULL, 0); printf("Parent - container stopped!\n"); umount("rootfs/proc"); return 0; }编译android_on_linux.c,并使用sudo ./a.out执行,(这里需要sudo执行,可以参考一种在Linux上运行时免root的方法免去sudo):
chenls@chenls-PC:android_on_linux$ gcc android_on_linux.c chenls@chenls-PC:android_on_linux$ sudo ./a.out 请输入密码 [sudo] chenls 的密码: 验证成功 Parent [ 6571] - start a container! Container [ 1] - inside the container! execv /system/bin/main hello world! Parent - container stopped!可以看到一切OK,上述代码中主要做了以下几件事:
1、使用了clone()函数开启新的进程,系统调用clone()函数的介绍:
类似于fork()和vfork(),Linux特有的系统调用clone()也能创建一个新线程。与前两者不同的是,后者在进程创建期间对步骤的控制更为准确。
2、利用PID Namespace,使用了CLONE_NEWPID标志,进行PID隔离,还可以使用Mount namespaces、Network namespaces等,更多信息请参考:DOCKER基础技术:LINUX NAMESPACE(下)。
3、mount主机的proc文件系统到rootfs的proc下。
4、使用了chroot()函数把rootfs目录作为根目录。
5、调用/system/bin/main开始执行。
至此我们主要使用了clone和chroot函数,运行了带动态库的Android x86程序,接下我们再探索一下如何运行Android ARM程序。
使用libhoudini运行Android ARM程序
技术要点
houdini的介绍:
houdini技术 是intel 研发的ARM binary translator,用于解决当前android部分native应用库兼容跑在x86架构上的技术,它的原理在于把ARM的二进制代码转译为X86指令集,使得可以在X86的CPU上执行。
更多信息请查看关于houdini技术和android x86平台兼容性的问题,github下载仓库libhoudini。
在如何打开Android X86对houdini的支持和Anbox手动安装ARM兼容库文章中都写了如何开启houdini的支持。
在此总结成以下两点:
1、下载libhoudini兼容库并挂载到/system/lib/arm(arm64)目录下。
2、通过binfmt_misc设置将ARM的程序通过houdini来运行。
实践一下
1、我们这里使用Android 7 64bit的兼容库,下载地址:http://dl.android-x86.org/houdini/7_z/houdini.sfs">http://dl.android-x86.org/houdini/7_z/houdini.sfs,将其直接解压到上述rootfs文件夹的/system/lib64/arm64中。
2、可以通过binfmt_misc在其中设置使用houdini运行
## 通过文件开始位置的特殊的字节来判断是否是ARM程序,是的话将其使用houdini来运行 sudo echo ':arm64_exe:M::\x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7::/system/lib64/arm64/houdini64:P' | tee -a /proc/sys/fs/binfmt_misc/register sudo echo ':arm64_dyn:M::\x7f\x45\x4c\x46\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\xb7::/system/lib64/arm64/houdini64:P' | tee -a /proc/sys/fs/binfmt_misc/register但此处我们可以直接使用类似/system/lib64/arm64/houdini64 /system/bin/main_arm64命令来执行,因此不需要改动binfmt_misc。
修改Application.mk文件,编译arm64可执行程序。
// Application.mk APP_ABI := arm64-v8a ## APP_ABI := x86_64 APP_PLATFORM := android-19重新编译,并拷贝文件到rootfs/system/bin/main_arm64中
chenls@chenls-PC:android_on_linux$ ndk-build [arm64-v8a] Compile : main <= main.c [arm64-v8a] Executable : main [arm64-v8a] Install : main => libs/arm64-v8a/main chenls@chenls-PC:android_on_linux$ cp libs/arm64-v8a/main rootfs/system/bin/main_arm64修改android_on_linux.c文件,使用houdini64执行main_arm64。
-char *container_args[] = {"/system/bin/main", NULL}; +char *container_args[] = {"/system/lib64/arm64/houdini64", "/system/bin/main_arm64"};编译android_on_linux.c,并使用sudo ./a.out执行:
chenls@chenls-PC:android_on_linux$ gcc android_on_linux.c chenls@chenls-PC:android_on_linux$ sudo ./a.out Parent [23725] - start a container! Container [ 1] - inside the container! execv /system/lib64/arm64/houdini64 hello world! Parent - container stopped!至此我们使用了clone和chroot函数加上houdini64相关库,运行了带动态库的Android ARM程序。