Android on Linux(在Linux主机上运行Android可执行程序)

概述

本文主要介绍如何在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的包含动态库的可执行程序了,测试脚本可查看https://gitee.com/cqupt/android_on_linux/blob/master/run_x86_64_by_modify_linker_path.sh

使用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的介绍,也可以参考:https://www.redhat.com/zh/topics/containers/whats-a-linux-container

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文件系统到rootfsproc下。

4、使用了chroot()函数把rootfs目录作为根目录。

5、调用/system/bin/main开始执行。

至此我们主要使用了clonechroot函数,运行了带动态库的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,将其直接解压到上述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!

至此我们使用了clonechroot函数加上houdini64相关库,运行了带动态库的Android ARM程序。测试脚本可查看https://gitee.com/cqupt/android_on_linux/blob/master/run_arm64-v8a_by_houdini.sh

  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值