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

作者:小玩童
鏈接:juejin.im/post/5e820b61e51d45470652e7b8

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

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

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

黑科技進程保活原理

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

後來,爲數不多的維術大佬在Gityuan大佬的基礎上,發佈了博客Android 黑科技保活實現原理揭祕又進行了系統進程查殺相關的源碼分析。爲我們帶來的結論是,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…,這個方案是當檢測到進程被殺時,會通過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的源碼可以看到,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類的地址,因此可以強轉得到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 網頁裏找到適合的版本,下載相應的固件,然後解包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系列文章。

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)一節,在binder模塊下IPCThreadState.cpp中有這樣的實現(源碼目錄: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(源碼目錄: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

打開設備節點成功了,耶✌️!但是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/native/libs/binder/Parcel.cpp

jni Parcel類路徑:frameworks/base/core/jni/android_os_Parcel.cpp

如何獲取到BpBinder的mHandle句柄

具體流程參考Binder系列4—獲取ServiceManager。

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/KeepAlive),master分支是利用 libbinder.so 與 ActivityManagerService 通信的版本,ioctl分支是使用 ioctl 與 binder 驅動通信的版本。

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

說一下我的方法論。

1、確定問題和目標。

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

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

2、時序圖很重要。

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

3、實踐出真知。

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

鳴謝

Gityuan大佬的[Binder系列
http://gityuan.com/2015/10/31/binder-prepare/

Android 黑科技保活實現原理揭祕
http://weishu.me/2020/01/16/a-keep-alive-method-on-android/

binder Driver (binder IPC) 功能介紹與分析
https://blog.csdn.net/vshuang/article/details/88823044

Binder驅動之設備控制binder_ioctl
https://www.jianshu.com/p/49830c3473b7

Google官方Android源碼瀏覽網站
https://cs.android.com/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章