Android最強保活黑科技的最強技術實現!

語言: CN / TW / HK

 BATcoder技術 群,讓一部分人先進大廠

大家 ,我是劉望舒,騰訊TVP,著有三本業內知名暢銷書,連續四年蟬聯電子工業出版社年度優秀作者,百度百科收錄的資深技術專家。

前華為架構師,現大廠技術總監。

想要 加入  BATcoder技術群,公號回覆 BAT  即可。

作者:小頑童

https://juejin.cn/post/6844904110219608078

大家好,我是老玩童。今天來跟大家分享TIM最強保活思路的幾種實現方法。這篇文章我將通過ioctl跟binder驅動互動,實現以最快的方式喚醒新的保活服務,最大程度防止保活失敗。同時,我也將跟您分享,我是怎麼做到在不甚瞭解binder的情況下,快速實現ioctl binder這種高階操作。

宣告:現在這個保活方式在MIUI等定製Android系統中已經不能保活,大部分時候 只能活在模擬器中 了。但對與我們的輕量定製的Android系統,一些系統級應用的保活,這個方案還是有用的。

隨著Android陣營的各大手機廠商對於續航的高度重視,兩三年前的手機發佈會更是把反保活作為一個系統的賣點,不斷提出了各種反保活的方案,導致現在想實現應用保活簡直難於上青天,甚至都需要一個團隊來專門研究這個事情。連微信這種超級APP,也要拜倒在反保活的石榴裙下,允許後臺啟動太費電,不允許後臺啟動就收不到訊息。。Android發現了一個保活野路子就堵一條,然而很多場景是有保活的強需求的,有木有考慮過我們開發者的感受,自己人何必為難自己人:sob:。

我覺得這是一個Android設計的不合理的地方,路子可以堵,但還是有必要留一個統一的保活介面的。這個介面由Google實現也好,廠商來實現也好,總好過現在很笨拙的系統自啟動管理或者是JobScheduler。我覺得本質上來說,讓應用開發者想盡各種辦法去做保活,這個事情是沒有意義的,保活的路子被封了,但保活還是需要做,保活的成本也提高了,簡直浪費生命。 Android的鍋 。(僅代表個人觀點)

黑科技程序保活原理

大概2個月前,Gityuan大佬放出了一份分析TIM的黑科技保活的部落格 史上最強Android保活思路:深入剖析騰訊TIM的程序永生技術 [1] (後來不知道什麼原因又刪除了),頓時間掀起了一陣波瀾,彷彿讓開發者們又看到了應用保活的一絲希望。Gityuan大佬通過超強的專業技術分析,為我們解開了TIM保活方案的終極奧義。

後來,為數不多的維術大佬在Gityuan大佬的基礎上,釋出了部落格 Android 黑科技保活實現原理揭祕 [2] 又進行了系統程序查殺相關的原始碼分析。為我們帶來的結論是,Android系統殺應用的時候,會去殺程序組, 迴圈 40 遍不停地殺程序,每次殺完之後等 5ms

總之,引用維術的話語,原理如下:

  1. 利用Linux檔案鎖的原理,使用2個程序互相監聽各自的檔案鎖,來感知彼此的死亡。

  2. 通過 fork 產生子程序,fork 的程序同屬一個程序組,一個被殺之後會觸發另外一個程序被殺,從而被檔案鎖感知。

具體來說,建立 2 個程序 p1, p2,這兩個程序通過檔案鎖互相關聯,一個被殺之後拉起另外一個;同時 p1 經過 2 次 fork 產生孤兒程序 c1,p2 經過 2 次 fork 產生孤兒程序 c2,c1 和 c2 之間建立檔案鎖關聯。這樣假設 p1 被殺,那麼 p2 會立馬感知到,然後 p1 和 c1 同屬一個程序組,p1 被殺會觸發 c1 被殺,c1 死後 c2 立馬感受到從而拉起 p1,因此這四個程序三三之間形成了鐵三角,從而保證了存活率。

按照維術大佬的理論, 只要程序我復活的足夠快,系統它就殺不死我 ,嘿嘿。

維術大佬寫了一個簡單的實現,程式碼在這裡: github.com/tiann/Leori… [3] ,這個方案是當檢測到程序被殺時,會通過JNI的方式,呼叫Java層的方法來複活程序。為了實現穩定的保活,尤其是系統殺程序只給了5ms復活的機會,使用JNI這種方式復活程序現在達不到最優的效果。

Java 層復活程序

復活程序,其實就是啟動指定的Service。當native層檢測到有程序被殺時,為了能夠快速啟動新Service。我們可以通過反射,拿到ActivityManager的remote binder,直接通過這個binder傳送資料,即可實現快速啟動Service。

Class<?> amnCls = Class.forName("android.app.ActivityManagerNative");
amn = activityManagerNative.getMethod("getDefault").invoke(amnCls);
Field mRemoteField = amn.getClass().getDeclaredField("mRemote");
mRemoteField.setAccessible(true);
mRemote = (IBinder) mRemoteField.get(amn);

啟動Service的Intent:

Intent intent = new Intent();
ComponentName component = new ComponentName(context.getPackageName(), serviceName);
intent.setComponent(component);

封裝啟動Service的Parcel:

Parcel mServiceData = Parcel.obtain();
mServiceData.writeInterfaceToken("android.app.IActivityManager");
mServiceData.writeStrongBinder(null);
mServiceData.writeInt(1);
intent.writeToParcel(mServiceData, 0);
mServiceData.writeString(null); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.writeInt(0);

啟動Service:

mRemote.transact(transactCode, mServiceData, null, 1);

在 native 層進行 binder 通訊

在Java層做程序復活的工作,這個方式是比較低效的,最好的方式是在 native 層使用純 C/C++來複活程序。方案有兩個。

其一,維術大佬給出的方案是利用libbinder.so, 利用Android提供的C++介面,跟ActivityManagerService通訊,以喚醒新程序。

  1. Java 層建立 Parcel (含 Intent),拿到 Parcel 物件的 mNativePtr(native peer),傳到 Native 層。

  2. native 層直接把 mNativePtr 強轉為結構體指標。

  3. fork 子程序,建立管道,準備傳輸 parcel 資料。

  4. 子程序讀管道,拿到二進位制流,重組為 parcel。

其二,Gityuan大佬則認為使用 ioctl 直接給 binder 驅動傳送資料以喚醒程序,才是更高效的做法。然而,這個方法,大佬們並沒有提供思路。

那麼今天,我們就來實現這兩種在 native 層進行 Binder 呼叫的騷操作。

方式一 利用 libbinder.so 與 ActivityManagerService 通訊

上面在Java層復活程序一節中,是向ActivityManagerService傳送特定的封裝了Intent的Parcel包來實現喚醒程序。而在native層,沒有Intent這個類。所以就需要在Java層建立好Intent,然後寫到Parcel裡,再傳到Native層。

Parcel mServiceData = Parcel.obtain();
mServiceData.writeInterfaceToken("android.app.IActivityManager");
mServiceData.writeStrongBinder(null);
mServiceData.writeInt(1);
intent.writeToParcel(mServiceData, 0);
mServiceData.writeString(null); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.writeInt(0);

檢視 Parcel的原始碼 [4] 可以看到,Parcel類有一個mNativePtr變數:

private long mNativePtr; // used by native code
// android4.4 mNativePtr是int型別

可以通過反射得到這個變數:

private static long getNativePtr(Parcel parcel) {
try {
Field ptrField = parcel.getClass().getDeclaredField("mNativePtr");
ptrField.setAccessible(true);
return (long) ptrField.get(parcel);
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}

這個變數對應了C++中 Parcel類 [5] 的地址,因此可以強轉得到Parcel指標:

 Parcel *parcel = (Parcel *) parcel_ptr;

然而,NDK中並沒有提供binder這個模組,我們只能從Android原始碼中扒到binder相關的原始碼,再編譯出libbinder.so。騰訊TIM應該就是魔改了binder相關的原始碼。

提取libbinder.so

為了避免libbinder的版本相容問題,這裡我們可以採用一個更簡單的方式,拿到binder相關的標頭檔案,再從系統中拿到libbinder.so,當然binder模組還依賴了其它的幾個so,要一起拿到,不然編譯的時候會報連結錯誤。

adb pull /system/lib/libbinder.so ./
adb pull /system/lib/libcutils.so ./
adb pull /system/lib/libc.so ./
adb pull /system/lib/libutils.so ./

如果需要不同SDK版本,不同架構的系統so庫,可以在 Google Factory Images [6] 網頁裡找到適合的版本,下載相應的韌體,然後解包system.img(需要在windows或linux中操作),提取出目標so。

binder_libs
├── arm64-v8a
│ ├── libbinder.so
│ ├── libc.so
│ ├── libcutils.so
│ └── libutils.so
├── armeabi-v7a
│ ├── ...
├── x86
│ ├── ...
└── x86_64
├── ...

為了避免相容問題,我這裡只讓這些so參與了binder相關的標頭檔案的連結,而沒有實際使用這些so。這是利用了so的載入機制,如果應用lib目錄沒有相應的so,則會到system/lib目錄下查詢。

SDK24以上,系統禁止了從system中載入so的方式,所以使用這個方法務必保證targetApi <24。

否則,將會報找不到so的錯誤。可以把上面的so放到jniLibs目錄解決這個問題,但這樣就會有相容問題了。

CMake修改:

# 連結binder_libs目錄下的所有so庫
link_directories(binder_libs/${CMAKE_ANDROID_ARCH_ABI})
# 引入binder相關的標頭檔案
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/)
# libbinder.so libcutils.so libutils.so libc.so等庫連結到libkeep_alive.so
target_link_libraries(
keep_alive
${log-lib} binder cutils utils c)

程序間傳輸Parcel物件

C++裡面還能傳輸物件?不存在的。好在Parcel能直接拿到資料地址,並提供了構造方法。所以我們可以通過管道把Parcel資料傳輸到其它程序。

Parcel *parcel = (Parcel *) parcel_ptr;
size_t data_size = parcel->dataSize();
int fd[2];
// 建立管道
if (pipe(fd) < 0) {return;}

pid_t pid;
// 建立子程序
if ((pid = fork()) < 0) {
exit(-1);
} else if (pid == 0) {//第一個子程序
if ((pid = fork()) < 0) {
exit(-1);
} else if (pid > 0) {
// 託孤
exit(0);
}

uint8_t data[data_size];
// 託孤的子程序,讀取管道中的資料
int result = read(fd[0], data, data_size);
}

// 父程序向管道中寫資料
int result = write(fd[1], parcel->data(), data_size);


重新建立Parcel:

Parcel parcel;
parcel.setData(data, data_size);

傳輸Parcel資料

// 獲取ServiceManager
sp<IServiceManager> sm = defaultServiceManager();
// 獲取ActivityManager binder
sp<IBinder> binder = sm->getService(String16("activity"));
// 傳輸parcel
int result = binder.get()->transact(code, parcel, NULL, 0);

方式二 使用 ioctl 與 binder 驅動通訊

方式一讓我嚐到了一點甜頭,實現了大佬的思路,不禁讓鄙人浮想聯翩,感慨萬千,鄙人的造詣已經如此之深,不久就會人在美國,剛下飛機,迎娶白富美,走向人生巔峰矣......

咳咳。不禁想到ioctl的方式我也可以嘗試著實現一下。ioctl是一個linux標準方法,那麼我們就直奔主題看看,binder是什麼,ioctl怎麼跟binder driver通訊。

Binder介紹

Binder是Android系統提供的一種IPC機制。每個Android的程序,都可以有一塊使用者空間和核心空間。使用者空間在不同程序間不能共享,核心空間可以共享。Binder就是一個利用可以共享的核心空間,完成高效能的程序間通訊的方案。

Binder通訊採用C/S架構,從元件視角來說,包含Client、Server、ServiceManager以及binder驅動,其中ServiceManager用於管理系統中的各種服務。如圖:

可以看到,註冊服務、獲取服務、使用服務,都是需要經過binder通訊的。

  • Server通過註冊服務的Binder通訊把自己託管到ServiceManager

  • Client端可以通過ServiceManager獲取到Server

  • Client端獲取到Server後就可以使用Server的介面了

Binder通訊的代表類是BpBinder(客戶端)和BBinder(服務端)。

ps:有關binder的詳細知識,大家可以檢視Gityuan大佬的 Binder系列 [7] 文章。

ioctl函式

ioctl(input/output control)是一個專用於裝置輸入輸出操作的系統呼叫,它誕生在這樣一個背景下:

操作一個裝置的IO的傳統做法,是在裝置驅動程式中實現write的時候檢查一下是否有特殊約定的資料流通過,如果有的話,後面就跟著控制命令(socket程式設計中常常這樣做)。但是這樣做的話,會導致程式碼分工不明,程式結構混亂。所以就有了ioctl函式,專門向驅動層傳送或接收指令。

Linux作業系統分為了兩層,使用者層和核心層。我們的普通應用程式處於使用者層,系統底層程式,比如網路棧、裝置驅動程式,處於核心層。為了保證安全,作業系統要阻止使用者態的程式直接訪問核心資源。一個 Ioctl 介面是一個獨立的系統呼叫,通過它使用者空間可以跟裝置驅動溝通了。函式原型:

int ioctl(int fd, int request, …);

作用:通過IOCTL函式實現指令的傳遞

  • fd 是使用者程式開啟裝置時使用open函式返回的檔案描述符

  • request是使用者程式對裝置的控制命令

  • 後面的省略號是一些補充引數,和cmd的意義相關

應用程式在呼叫 ioctl 進行裝置控制時,最後會呼叫到設備註冊 struct file_operations 結構體物件時的 unlocked_ioctl 或者 compat_ioctl 兩個鉤子上,例如Binder驅動的這兩個鉤子是掛到了binder_ioctl方法上:

static const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.compat_ioctl = binder_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};

它的實現如下:

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
/*根據不同的命令,呼叫不同的處理函式進行處理*/
switch (cmd) {
case BINDER_WRITE_READ:
/*讀寫命令,資料傳輸,binder IPC通訊的核心邏輯*/
ret = **binder_ioctl_write_read**(filp, cmd, arg, thread);
break;
case BINDER_SET_MAX_THREADS:
/*設定最大執行緒數,直接將值設定到proc結構的max_threads域中。*/
break;
case BINDER_SET_CONTEXT_MGR:
/*設定Context manager,即將自己設定為ServiceManager,詳見3.3*/
break;
case BINDER_THREAD_EXIT:
/*binder執行緒退出命令,釋放相關資源*/
break;
case BINDER_VERSION: {
/*獲取binder驅動版本號,在kernel4.4版本中,32位該值為7,64位版本該值為8*/
break;
}
return ret;
}

具體核心層的實現,我們就不關心了。到這裡我們瞭解到,Binder在Android系統中會有一個裝置節點,呼叫ioctl控制這個節點時,實際上會呼叫到核心態的binder_ioctl方法。

為了利用ioctl啟動Android Service,必然是需要用ioctl向binder驅動寫資料,而這個控制命令就是 BINDER_WRITE_READ 。binder驅動層的一些細節我們在這裡就不關心了。那麼在什麼地方會用ioctl 向binder寫資料呢?

IPCThreadState.talkWithDriver

閱讀Gityuan的 Binder系列6—獲取服務(getService) [8] 一節,在binder模組下 IPCThreadState.cpp [9] 中有這樣的實現(原始碼目錄:frameworks/native/libs/binder/IPCThreadState.cpp):

status_t IPCThreadState::talkWithDriver(bool doReceive) {
...
binder_write_read bwr;
bwr.write_buffer = (uintptr_t)mOut.data();
status_t err;
do {
//通過ioctl不停的讀寫操作,跟Binder Driver進行通訊
if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
err = NO_ERROR;
...
} while (err == -EINTR); //當被中斷,則繼續執行
...
return err;
}

可以看到ioctl跟binder driver互動很簡單,一個引數是mProcess->mDriverFD,一個引數是BINDER_WRITE_READ,另一個引數是binder_write_read結構體,很幸運的是,NDK中提供了 linux/android/binder.h 這個標頭檔案,裡面就有binder_write_read這個結構體,以及BINDER_WRITE_READ常量的定義。

[驚不驚喜意不意外]

#include<linux/android/binder.h>
struct binder_write_read {
binder_size_t write_size;
binder_size_t write_consumed;
binder_uintptr_t write_buffer;
binder_size_t read_size;
binder_size_t read_consumed;
binder_uintptr_t read_buffer;
};
#define BINDER_WRITE_READ _IOWR('b', 1, struct binder_write_read)

這意味著,這些結構體和巨集定義很可能是版本相容的。

那我們只需要到時候把資料揌到binder_write_read結構體裡面,就可以進行ioctl系統呼叫了!

/dev/binder

再來看看mProcess->mDriverFD是什麼東西。mProcess也就是 ProcessState.cpp [10] (原始碼目錄:frameworks/native/libs/binder/ProcessState.cpp):

ProcessState::ProcessState(const char *driver)
: mDriverName(String8(driver))
, mDriverFD(open_driver(driver))
, ...
{}

從ProcessState的建構函式中得知,mDriverFD由open_driver方法初始化。

static int open_driver(const char *driver) {
int fd = open(driver, O_RDWR | O_CLOEXEC);
if (fd >= 0) {
int vers = 0;
status_t result = ioctl(fd, BINDER_VERSION, &vers);
}
return fd;
}

ProcessState在哪裡例項化呢?

sp<ProcessState> ProcessState::self() {
if (gProcess != nullptr) {
return gProcess;
}
gProcess = new ProcessState(kDefaultDriver);
return gProcess;
}

可以看到,ProcessState的gProcess是一個全域性單例物件,這意味著,在當前程序中,open_driver只會執行一次,得到的 mDriverFD 會一直被使用。

const char* kDefaultDriver = "/dev/binder";

而open函式操作的這個裝置節點就是/dev/binder。

納尼?在應用層直接操作裝置節點?Gityuan大佬不會騙我吧?一般來說,Android系統在整合SELinux的安全機制之後,普通應用甚至是系統應用,都不能直接操作一些裝置節點,除非有SELinux規則,給應用所屬的域或者角色賦予了那樣的許可權。

看看檔案許可權:

➜  ~ adb shell
chiron:/ $ ls -l /dev/binder
crw-rw-rw- 1 root root 10, 49 1972-07-03 18:46 /dev/binder

可以看到,/dev/binder裝置對所有使用者可讀可寫。

再看看,SELinux許可權:

chiron:/ $ ls -Z /dev/binder
u:object_r:binder_device:s0 /dev/binder

檢視原始碼中對binder_device角色的SELinux規則描述:

allow domain binder_device:chr_file rw_file_perms;

也就是所有domain對binder的字元裝置有讀寫許可權,而普通應用屬於domain。

既然這樣, 肝它!

寫個Demo試一下

驗證一下上面的想法,看看ioctl給binder driver發資料好不好使。

1、開啟裝置

int fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
if (fd < 0) {
LOGE("Opening '%s' failed: %s\n", "/dev/binder", strerror(errno));
} else {
LOGD("Opening '%s' success %d: %s\n", "/dev/binder", fd, strerror(errno));
}

2、ioctl

Parcel *parcel = new Parcel;
parcel->writeString16(String16("test"));
binder_write_read bwr;
bwr.write_size = parcel->dataSize();
bwr.write_buffer = (binder_uintptr_t) parcel->data();
int ret = ioctl(fd, BINDER_WRITE_READ, bwr);
LOGD("ioctl result is %d: %s\n", ret, strerror(errno));

3、檢視日誌

D/KeepAlive: Opening '/dev/binder' success, fd is 35
D/KeepAlive: ioctl result is -1: Invalid argument

開啟裝置節點成功了,耶:v:!但是ioctl失敗了 ,失敗原因是 Invalid argument ,也就是說可以通訊,但是Parcel資料有問題。來看看資料應該是什麼樣的。

binder_write_read結構體資料封裝

IPCThreadState.talkWithDriver方法中,bwr.write_buffer指標指向了mOut.data(),顯然mOut是一個Parcel物件。

binder_write_read bwr;
bwr.write_buffer = (uintptr_t)mOut.data();

再來看看什麼時候會向mOut中寫資料:

status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)
{
binder_transaction_data tr;
tr.data.ptr.buffer = data.ipcData();
...
mOut.writeInt32(cmd);
mOut.write(&tr, sizeof(tr));
return NO_ERROR;
}

writeTransactionData方法中,會往mOut中寫入一個binder_transaction_data結構體資料,binder_transaction_data結構體中又包含了作為引數傳進來的data Parcel物件。

writeTransactionData方法會被transact方法呼叫:

status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data,
Parcel* reply, uint32_t flags) {
status_t err = data.errorCheck(); // 資料錯誤檢查
flags |= TF_ACCEPT_FDS;
if (err == NO_ERROR) {
// 傳輸資料
err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL);
}
...

// 預設情況下,都是採用非oneway的方式, 也就是需要等待服務端的返回結果
if ((flags & TF_ONE_WAY) == 0) {
if (reply) {
//等待迴應事件
err = waitForResponse(reply);
}else {
Parcel fakeReply;
err = waitForResponse(&fakeReply);
}
} else {
err = waitForResponse(NULL, NULL);
}
return err;
}

IPCThreadState是跟binder driver真正進行互動的類。每個執行緒都有一個 IPCThreadState ,每個 IPCThreadState 中都有一個mIn、一個mOut。成員變數mProcess儲存了ProcessState變數(每個程序只有一個)。

接著看一下一次Binder呼叫的時序圖:

Binder介紹一節中說過,BpBinder是Binder Client,上層想進行程序間Binder通訊時,會呼叫到BpBinder的transact方法,進而呼叫到IPCThreadState的transact方法。來看看BpBinder的transact方法的定義:

status_t BpBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {
if (mAlive) {
status_t status = IPCThreadState::self()->transact(mHandle, code, data, reply, flags);
if (status == DEAD_OBJECT) mAlive = 0;
return status;
}
return DEAD_OBJECT;
}

BpBinder::transact方法的code/data/reply/flags這幾個引數都是呼叫的地方傳過來的,現在唯一不知道的就是mHandle是什麼東西。mHandle是BpBinder(也就是Binder Client)的一個int型別的區域性變數(控制代碼),只要拿到了這個handle就相當於拿到了BpBinder。

ioctl啟動Service分幾步?

下面是在依賴libbinder.so時,啟動Service的步驟:

// 獲取ServiceManager
sp<IServiceManager> sm = defaultServiceManager();
// 獲取ActivityManager binder
sp<IBinder> binder = sm->getService(String16("activity"));
// 傳輸parcel
int result = binder.get()->transact(code, parcel, NULL, 0);

1、獲取到IServiceManager Binder Client;

2、從ServiceManager中獲取到ActivityManager Binder Client;

3、呼叫ActivityManager binder的transact方法傳輸Service的Parcel資料。

通過ioctl啟動Service也應該是類似的步驟:

1、獲取到ServiceManager的mHandle控制代碼;

2、進行binder呼叫獲取到ActivityManager的mHandle控制代碼;

3、進行binder呼叫傳輸啟動Service的指令資料。

這裡有幾個問題:

1、不依賴libbinder.so時,ndk中沒有Parcel類的定義,parcel資料哪裡來,怎麼封裝?

2、如何獲取到BpBinder的mHandle控制代碼?

如何封裝Parcel資料

Parcel類是Binder程序間通訊的一個基礎的、必不可少的資料結構,往Parcel中寫入的資料實際上是寫入到了一塊內部分配的記憶體上,最後把這個記憶體地址封裝到binder_write_read結構體中。Parcel作為一個基礎的資料結構,和Binder相關類是可以解耦的,可以直接拿過來使用,我們可以根據需要對有耦合性的一些方法進行裁剪。

c++ Parcel類路徑: frameworks [11] / native [12] / libs [13] / binder [14] / Parcel.cpp [15]

jni Parcel類路徑: frameworks [16] / base [17] / core [18] / jni [19] / android\_os\_Parcel.cpp [20]

如何獲取到BpBinder的mHandle控制代碼

具體流程參考 Binder系列4—獲取ServiceManager [21]

1、獲取ServiceManager的mHandle控制代碼

defaultServiceManager()方法用來獲取 gDefaultServiceManager 物件,gDefaultServiceManager是ServiceManager的單例。

sp<IServiceManager> defaultServiceManager() {
if (gDefaultServiceManager != NULL) return gDefaultServiceManager;
while (gDefaultServiceManager == NULL) {
gDefaultServiceManager = interface_cast<IServiceManager>(
ProcessState::self()->getContextObject(NULL));
}
}
return gDefaultServiceManager;
}

getContextObject方法用來獲取BpServiceManager物件(BpBinder),檢視其定義:

sp<IBinder> ProcessState::getContextObject(const sp<IBinder>& /*caller*/) {
sp<IBinder> context = getStrongProxyForHandle(0);
return context;
}

可以發現,getStrongProxyForHandle是一個根據handle獲取IBinder物件的方法,而這裡handle的值為0,可以得知, ServiceManager的mHandle恆為0

2、獲取ActivityManager的mHandle控制代碼

獲取ActivityManager的c++方法是:

sp<IBinder> binder = serviceManager->getService(String16("activity"));

BpServiceManager.getService:

virtual sp<IBinder> getService(const String16& name) const {
sp<IBinder> svc = checkService(name);
if (svc != NULL) return svc;
return NULL;
}

BpServiceManager.checkService:

virtual sp<IBinder> checkService( const String16& name) const {
Parcel data, reply;
//寫入RPC頭
data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
//寫入服務名
data.writeString16(name);
remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply);
return reply.readStrongBinder();
}

可以看到,CHECK_SERVICE_TRANSACTION這個binder呼叫是有返回值的,返回值會寫到reply中,通過reply.readStrongBinder()方法,即可從reply這個Parcel物件中讀取到ActivityManager的IBinder。每個Binder物件必須要有它自己的mHandle控制代碼,不然,transact操作是沒辦法進行的。所以,很有可能,Binder的mHandle的值是寫到reply這個Parcel裡面的。

看看reply.readStrongBinder()方法搞了什麼鬼:

sp<IBinder> Parcel::readStrongBinder() const {
sp<IBinder> val;
readNullableStrongBinder(&val);
return val;
}
status_t Parcel::readNullableStrongBinder(sp<IBinder>* val) const {
return unflattenBinder(val);
}

呼叫到了Parcel::unflattenBinder方法,顧名思義,函式最終想要得到的是一個Binder物件,而Parcel中存放的是二進位制的資料,unflattenBinder很可能是把Parcel中的一個結構體資料給轉成Binder物件。

看看Parcel::unflattenBinder方法的定義:

status_t Parcel::unflattenBinder(sp<IBinder>* out) const {
const flat_binder_object* flat = readObject(false);
if (flat) {
...
sp<IBinder> binder =
ProcessState::self()->getStrongProxyForHandle(flat->handle);
}
return BAD_TYPE;
}

果然如此,從Parcel中可以得到一個flat_binder_object結構體,這個結構體重有一個handle變數,這個變數就是BpBinder中的mHandle控制代碼。

因此,在不依賴libbinder.so的情況下,我們可以自己組裝資料傳送給ServiceManager,進而獲取到ActivityManager的mHandle控制代碼。

IPCThreadState是一個被Binder依賴的類,它是可以從原始碼中抽離出來為我們所用的。上一節中說到,Parcel類也是可以從原始碼中抽離出來的。

通過如下的操作,我們就可以實現ioctl獲取到ActivityManager對應的Parcel物件reply:

Parcel data, reply;
// data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
// IServiceManager::getInterfaceDescriptor()的值是android.app.IActivityManager
data.writeInterfaceToken(String16("android.app.IActivityManager"));
data.writeString16(String16("activity"));
IPCThreadState::self()->transact(
0/*ServiceManger的mHandle控制代碼恆為0*/,
CHECK_SERVICE_TRANSACTION, data, reply, 0);

reply變數也就是我們想要的包含了flat_binder_object結構體的Parcel物件,再經過如下的操作就可以得到ActivityManager的mHandle控制代碼:

const flat_binder_object* flat = reply->readObject(false);
return flat->handle;

3、傳輸啟動指定Service的Parcel資料

上一步已經拿到ActivityManger的mHandle控制代碼,比如值為1。這一步的過程和上一步類似,自己封裝Parcel,然後呼叫IPCThreadState::transact方法傳輸資料,虛擬碼如下:

Parcel data;
// 把Service相關資訊寫到parcel中
writeService(data, packageName, serviceName, sdk_version);
IPCThreadState::self()->transact(
1/*上一步獲取的ActivityManger的mHandle控制代碼值是1*/,
CHECK_SERVICE_TRANSACTION, data, reply,
1/*TF_ONE_WAY*/);

4、writeService方法需要做什麼事情?

下面這段程式碼是Java中封裝Parcel物件的方法:

Intent intent = new Intent();
ComponentName component = new ComponentName(context.getPackageName(), serviceName);
intent.setComponent(component);

Parcel mServiceData = Parcel.obtain();
mServiceData.writeInterfaceToken("android.app.IActivityManager");
mServiceData.writeStrongBinder(null);
mServiceData.writeInt(1);
intent.writeToParcel(mServiceData, 0);
mServiceData.writeString(null); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.writeInt(0);

可以看到,有Intent類轉Parcel,ComponentName類轉Parcel,這些類在c++中是沒有對應的類的。所以需要我們參考 intent.writeToParcel / ComponentName.writeToParcel 等方法的原始碼的實現,自行封裝資料。下面這段程式碼就是把啟動Service的Intent寫到Parcel中的方法:

void writeIntent(Parcel &out, const char *mPackage, const char *mClass) {
// mAction
out.writeString16(NULL, 0);
// uri mData
out.writeInt32(0);
// mType
out.writeString16(NULL, 0);
// // mIdentifier
out.writeString16(NULL, 0);
// mFlags
out.writeInt32(0);
// mPackage
out.writeString16(NULL, 0);
// mComponent
out.writeString16(String16(mPackage));
out.writeString16(String16(mClass));
// mSourceBounds
out.writeInt32(0);
// mCategories
out.writeInt32(0);
// mSelector
out.writeInt32(0);
// mClipData
out.writeInt32(0);
// mContentUserHint
out.writeInt32(-2);
// mExtras
out.writeInt32(-1);
}


繼續寫Demo試一下

上面已經知道了怎麼通過ioctl獲取到ActivityManager,可以寫demo試一下:

// 開啟binder裝置
int fd = open("/dev/binder", O_RDWR | O_CLOEXEC);
Parcel data, reply;
// data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());
// IServiceManager::getInterfaceDescriptor()的值是android.app.IActivityManager
data.writeInterfaceToken(String16("android.app.IActivityManager"));
data.writeString16(String16("activity"));
IPCThreadState::self()->transact(
0/*ServiceManger的mHandle控制代碼恆為0*/,
CHECK_SERVICE_TRANSACTION, data, reply, 0);

const flat_binder_object *flat = reply->readObject(false);
if (flat) {
LOGD("write_transact handle is:%llu", flat->handle);
}else {
LOGD("write_transact failed, error=%d", status);
}

給IPCThreadState::transact加上一些日誌,列印結果如下:

D/KeepAlive: BR_DEAD_REPLY
D/KeepAlive: write_transact failed, error=-32

reply中始終讀不到資料。這是為什麼?現在已經不報 Invalid argument 的錯誤了,說明Parcel資料格式可能沒問題了。但是不能成功把資料寫給ServiceManager,或者ServiceManager返回的資料不能成功寫回來。

想到Binder是基於記憶體的一種IPC機制,資料都是對的,那問題就出在記憶體上了。這就要說到Binder基本原理以及Binder記憶體轉移關係。

Binder基本原理:

Binder的Client端和Server端位於不同的程序,它們的使用者空間是相互隔離。而核心空間由Linux核心程序來維護,在安全性上是有保障的。所以,Binder的精髓就是在核心態開闢了一塊共享記憶體。

資料傳送方寫資料時,核心態通過copy_from_user()方法把它的資料拷貝到資料接收方對映(mmap)到核心空間的地址上。這樣,只需要一次資料拷貝過程,就可以完成程序間通訊。

由此可知,沒有這塊核心空間是沒辦法完成IPC通訊的。Demo失敗的原因就是缺少了一個mmap過程,以對映一塊記憶體到核心空間。修改如下:

#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
int mDriverFD = open("/dev/binder", O_RDWR | O_CLOEXEC);
mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);

日誌:

D/KeepAlive: BR_REPLY
D/KeepAlive: write_transact handle is:1

搞定!

最後

相關的程式碼我已經發布到Github( lcodecorex [22] / KeepAlive [23] ), master 分支是 利用 libbinder.so 與 ActivityManagerService 通訊 的版本, ioctl 分支是 使用 ioctl 與 binder 驅動通訊 的版本。

當然,這個保活的辦法雖然很強,但現在也 只能活在模擬器裡 了。

說一下我的方法論。

1、確定問題和目標。

研究一個比較複雜的東西的時候,我們比較難有一個大局觀。這個時候,就需要明確自己需要什麼?有問題,才能推動自己學習,然後順騰摸瓜,最後弄清自己的模組在系統中的位置。

這篇文章,我們確定了目標是直接通過ioctl進行Binder通訊,進而確定Binder通訊的關鍵是拿到mHandle控制代碼。同時也理清了Binder通訊的一個基本流程。

2、時序圖很重要。

大佬們畫的時序圖,可快幫助我們快速理清框架的思路。

3、實踐出真知。

紙上得來終覺淺,絕知此事要躬行。我一直踐行的一個學習方式是學以致用,可以及時寫Demo幫助我們鞏固知識以及分析問題。

參考資料:

參考資料比較多,具體見原文:

https://juejin.cn/post/6844904110219608078

   微信改了推送機制,真愛請星標本公號 :point_down: