概述
本文主要介紹如何在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,開始編譯并運作:
[email protected]:jni$ ndk-build
[x86_64] Compile : main <= main.c
[x86_64] Executable : main
[x86_64] Install : main => libs/x86_64/main
[email protected]:jni$ ../libs/x86_64/main
bash: ../libs/x86_64/main: 沒有那個檔案或目錄
此時運作報錯,我們來檢視一下原因:
[email protected]: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
[email protected]:jni$
[email protected]:jni$ gcc main.c
[email protected]: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
檔案的依賴:
[email protected]: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
[email protected]: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)
重新編譯,檢查依賴,然後運作。
[email protected]:jni$ ndk-build
[x86_64] Install : main => libs/x86_64/main
[email protected]:jni$ readelf -a ../libs/x86_64/main | grep NEED
[email protected]:jni$ ../libs/x86_64/main
hello world!
此時一個簡單的Android可執行程式能直接在Linux主機上運作了,但是實際項目依賴的庫較多,并不能全部都能靜态編譯,而且我們會更青睐使用動态庫的方式。接下來試試如何運作包含動态庫的可執行程式。
使用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
檔案夾。
[email protected]: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):
[email protected]:android_on_linux$ gcc android_on_linux.c
[email protected]: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,将其直接解壓到上述
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
中
[email protected]:android_on_linux$ ndk-build
[arm64-v8a] Compile : main <= main.c
[arm64-v8a] Executable : main
[arm64-v8a] Install : main => libs/arm64-v8a/main
[email protected]: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
執行:
[email protected]:android_on_linux$ gcc android_on_linux.c
[email protected]: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程式。