概述
本文主要介绍如何在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/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e53d68617f04135f77102f0e87226709ef80cb50, not stripped
我们使用file
对比了ndk-build
和主机gcc
分别编译的文件发现,Android是使用/system/bin/linker64
作为链接器(关于它的介绍:Android Linker),而在linux主机上是使用的是/lib64/ld-linux-x86-64.so.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程序。