活頁夾是Android中主要的IPC / RPC(進程間通信)系統。 它允許應用程序彼此通信,並且它是Android環境中幾種重要機制的基礎。 例如,Android服務是建立在Binder之上的。 與Binder交換的消息稱爲活頁夾事務 ,它們可以傳輸簡單數據(例如整數),但也可以處理更復雜的結構,例如文件描述符,內存緩衝區或對象上的弱/強引用。 Internet上有很多有趣的Binder文檔,但是關於如何將消息從一個進程轉換到另一個進程的細節很少。 本文試圖描述Binder如何處理消息以及如何在不同進程之間執行復雜對象(文件描述符,指針)的轉換。 爲此,將從用戶區到綁定程序內核進行綁定程序事務。
USERLAND中的活頁夾
在探索Binder內核模塊如何工作之前,讓我們看一下在調用Android Service的情況下如何在用戶區中準備事務。
Android服務概述
服務是在後臺運行併爲其他應用程序提供功能的Android組件。 其中一些是Android框架的一部分,但已安裝的應用程序也可以公開其自身的功能。 當應用程序要公開新服務時,它首先註冊到“服務管理器” (1) ,其中包含並更新所有正在運行的服務的列表。 稍後,客戶端向處理程序請求ServiceManager (2)與該服務進行通信,並能夠調用公開的函數(3) 。
服務互動
從Android 8.0開始,存在三個不同的Binder域。 每個域都有其自己的服務管理器,並且可以通過/dev/
的相應設備進行訪問。 下表描述了每個Binder域有一個設備:
綁定器域,摘自Android文檔網站
爲了使用活頁夾系統,一個過程需要打開這些設備之一併執行一些初始化步驟,然後再發送或接收活頁夾交易。
準備活頁夾交易
Android框架在活頁夾設備頂部包含多個抽象層。 通常,當開發人員實施新服務時,他們會描述以高級語言公開的接口。 在框架應用程序的情況下,描述是使用AIDL語言編寫的,而由供應商開發的硬件服務則具有以HIDL語言編寫的接口描述。 這些描述被編譯到Java / C ++文件中,在其中使用Parcel組件對參數進行反序列化。 生成的代碼包含兩個類,一個Binder Proxy和一個Binder Stub 。 代理類用於請求遠程服務,而存根則用於接收傳入呼叫,如下圖所示。
黏合劑層
在最低級別,使用域對應的設備將應用程序連接到Binder內核模塊。 他們使用ioctl
syscall來發送和接收綁定程序消息。
序列化步驟使用Parcel類完成,該類提供了在Binder消息中讀取和寫入數據的功能。 有兩種不同的宗地類:
/dev/binder
和/dev/vndbinder
域基於AIDL描述語言,並使用在frameworks/native/include/binder/Parcel.h
定義的Parcel。 這個包裹允許發送基本類型和文件描述符 。 例如,以下代碼摘自命令SHELL_COMMAND_TRANSACTION
的默認代理實現。 該命令準備並寫入遠程服務使用的標準輸入,輸出和錯誤流的文件描述符。
//從frameworks / base / core / java / Android / os / Binder.java中提取 public void shellCommand (輸入 FileDescriptor ,輸出 FileDescriptor , FileDescriptor err , 字符串 [] args , ShellCallback 回調 , ResultReceiver resultReceiver ) 引發 RemoteException { 宗地 數據 =宗地 。 獲得 (); 包裹 回覆 = 包裹 。 獲得 (); 數據 。 writeFileDescriptor ( in ); 數據 。 writeFileDescriptor ( out ); 數據 。 writeFileDescriptor ( err ); 數據 。 writeStringArray ( args ); ShellCallback 。 writeToParcel ( callback , data ); resultReceiver 。 writeToParcel ( data , 0 ); 嘗試 { 交易 ( SHELL_COMMAND_TRANSACTION , data , 回覆 , 0 ); 回覆 。 readException (); } 最後 { 數據 。 回收 (); 回覆 。 回收 (); } }
/dev/hwbinder
域使用在先前的基礎上在system/libhwbinder/include/hwbinder/Parcel.h
實現的另一個Parcel。 這種Parcel實現允許發送數據緩衝區,例如C結構。 數據緩衝區可以嵌套,幷包含指向其他結構的指針。 在以下示例中,結構hild_memory
結構包含一個嵌入式結構(hild_string
)和一個內存指針(mHandle
):
//從system / libhidl / transport / include / hidl / HidlBinderSupport.h中提取 // ---------------------- hidl_memory status_t readEmbeddedFromParcel ( const hidl_memory & memory, const Parcel & parcel, size_t parentHandle, size_t parentOffset); status_t writeEmbeddedToParcel ( const hidl_memory & memory, Parcel * parcel, size_t parentHandle, size_t parentOffset); // [...] //從system / libhidl / base / include / hidl / HidlSupport.h中提取 struct hidl_memory { // ... 私人的 : hidl_handle mHandle __attribute__ ((aligned( 8 ))); uint64_t mSize __attribute__ ((aligned( 8 ))); hidl_string mName __attribute__ ((aligned( 8 ))); };
這兩種Parcel能夠發送文件描述符和帶有內存地址的複雜數據結構。 因爲這些元素包含特定於調用者進程的數據,所以Parcel組件將綁定對象寫入事務消息中。
活頁夾對象
除了簡單類型(字符串,整數等)之外,還可以發送綁定對象。 活頁夾對象是一種類型值爲以下之一的結構:
//摘自:drivers / staging / Android / uapi / binder.h 枚舉 { BINDER_TYPE_BINDER = B_PACK_CHARS( 's' , 'b' , '*' , B_TYPE_LARGE), BINDER_TYPE_WEAK_BINDER = B_PACK_CHARS( 'w' , 'b' , '*' , B_TYPE_LARGE), BINDER_TYPE_HANDLE = B_PACK_CHARS( 's' , 'h' , '*' , B_TYPE_LARGE), BINDER_TYPE_WEAK_HANDLE = B_PACK_CHARS( 'w' , 'h' , '*' , B_TYPE_LARGE), BINDER_TYPE_FD = B_PACK_CHARS( 'f' , 'd' , '*' , B_TYPE_LARGE), BINDER_TYPE_FDA = B_PACK_CHARS( 'f' , 'd' , 'a' , B_TYPE_LARGE), BINDER_TYPE_PTR = B_PACK_CHARS( 'p' , 't' , '*' , B_TYPE_LARGE), };
下面是一個類型爲BINDER_TYPE_PTR
的活頁夾對象的BINDER_TYPE_PTR
:
structinder_object_header { __u32 類型; }; structinder_buffer_object { 結構 binder_object_header hdr; __u32 標誌; binding_uintptr_t 緩衝區; binding_size_t 長度; binding_size_t 父對象; 活頁夾大小 };
hdr
下方的屬性是特定於類型的。
不同的活頁夾對象可以描述如下:
- BINDER_TYPE_BINDER和BINDER_TYPE_WEAK_BINDER :這些類型是對本地對象的強引用和弱引用。
- BINDER_TYPE_HANDLER和BINDER_TYPE_WEAK_HANDLE :這些類型是對遠程對象的強引用和弱引用。
- BINDER_TYPE_FD :此類型用於發送文件描述符號。 這通常用於發送ashmem共享內存以傳輸大量數據。 實際上,活頁夾交易消息被限制爲1 MB。 但是,可以使用任何文件描述符類型(文件,套接字,標準輸入等)。
- BINDER_TYPE_FDA :描述文件描述符數組的對象。
- BINDER_TYPE_PTR :用於使用內存地址及其大小發送緩衝區的對象。
當Parcel類編寫緩衝區或文件描述符時,它將在數據緩衝區中添加活頁夾對象(圖上爲藍色)。 活頁夾對象和簡單類型混合在數據緩衝區中。 每次寫入對象時,其相對位置都會插入到偏移緩衝區中(紫色)。
活頁夾消息緩衝區和偏移量
一旦data
和offets
緩衝區已滿,就準備將binder_transaction_data
傳遞給內核。 我們可以注意到它包含上述指針,數據緩衝區和偏移量數組的大小。 字段handler
用於設置目標過程,該過程是先前由服務管理器檢索的。 另一個有趣的屬性是code
,其中包含要執行的遠程服務的方法ID。
//文件:development / ndk / platforms / android-9 / include / linux / binder.h structinder_transaction_data { 工會 { size_t 句柄; 無效 * ptr; } 目標; 無效 * cookie; 未簽名的 int 代碼; unsigned int 標誌; pid_t sender_pid; uid_t sender_euid; size_t data_size; size_t offsets_size; 工會 { 結構 { const void * buffer; const void * 偏移量; } ptr; uint8_t buf [ 8 ]; } 數據; };
在調用ioctl之前,必須填充最後一個結構( binder_write_read
)。 它包含讀寫命令緩衝區,並指向上一個緩衝區:
//文件:development / ndk / platforms / android-9 / include / linux / binder.h structinder_write_read { 簽名 長 write_size; 簽名 長 write_consumed; 無符號 長 write_buffer; 簽名 長 read_size; 簽名 長 read_consumed; 無符號 長 read_buffer; };
發送活頁夾事務所需的數據結構可以用下面的圖總結:
binding_write_read結構
我們可以注意到, write_buffer
並不直接指向binder_transaction_data
結構。 它以命令標識符爲前綴。 如果是交易,則值爲BC_TRANSACTION_SG
。
請注意,除了BC_TRANSACTION_SG
以外, BC_TRANSACTION_SG
存在許多命令,例如BC_ACQUIRE
和BC_RELEASE
以增加或減少強處理程序,或者在停止遠程服務時會注意到BC_REQUEST_DEATH_NOTIFICATION
。
現在所有人都準備好執行綁定程序事務,調用者需要使用命令BINDER_WRITE_READ
調用ioctl
,內核模塊將處理該消息並轉換目標進程的所有綁定程序對象:強/弱處理程序,文件描述符和緩衝區。
在下一部分中,讓我們繼續在內核方面進行分析!
活頁夾內核模塊
現在,調用者進程已準備好其數據並執行了一個ioctl來發送事務。 所有活頁夾對象都將被翻譯,並且消息將被複制到目標內存中。
用於ioctl
的命令由binder_ioctl_write_read
函數處理,該函數執行數據參數的安全複製。
//文件:drivers / android / binder.c 靜態 長 binder_ioctl ( 結構 文件 * filp, 無符號 int cmd, 無符號 長 arg) { // [...] 開關 (cmd) { 情況 BINDER_WRITE_READ: ret = 活頁夾_ioctl_write_read(filp, cmd, arg, thread ); 如果 (ret) 轉到 錯誤; 休息 ;
//文件:drivers / android / binder.c 靜態 整數 binder_ioctl_write_read ( 結構 文件 * filp, unsigned int cmd, unsigned long arg, struct binder_thread * 線程 ) { // [...] 如果 (copy_from_user( & bwr, ubuf, sizeof (bwr))) { ret = -EFAULT; 出去 } // [...] 如果 (bwr.write_size > 0 ) { ret = binder_thread_write(proc, thread , bwr.write_buffer, bwr.write_size, & bwr.write_consumed);
在寫事務的情況下,將調用函數binder_thread_write
,然後將與事務關聯的命令調度到相應的處理程序。
//文件:drivers / android / binder.c 開關 (cmd) { 情況 BC_INCREFS: 情況 BC_ACQUIRE: 情況 BC_RELEASE: 情況 BC_DECREFS: // [...] 情況 BC_TRANSACTION_SG: 案例 BC_REPLY_SG: { struct binder_transaction_data_sg tr; 如果 (copy_from_user( & tr, ptr, sizeof (tr))) 返回 -EFAULT; ptr + = sizeof (tr); binding_transaction(proc, thread 和 tr.transaction_data, cmd == BC_REPLY_SG, tr.buffers_size); 休息 ; } // [...]
對於命令BC_TRANSACTION_SG
,在userland中準備的binder_transaction_data緩衝區由binder_transaction
函數處理。
活頁夾交易
binder_transaction
函數位於文件drivers/staging/Android/binder.c
。
這個重要的功能執行以下任務:在目標進程中(在活頁夾保留的內存中)分配一個緩衝區,驗證所有數據對象並執行轉換,在目標內存進程中複製數據和偏移緩衝區。
爲了驗證活頁夾對象,內核查看包含所有對象相對位置的offsets
緩衝區。 取決於對象類型,內核執行不同的轉換。
//文件:drivers / android / binder.c 靜態 void binder_transaction ( struct binder_proc * proc, struct binder_thread * 線程 , struct binder_transaction_data * tr, int 回覆, binding_size_t extra_buffers_size){ // [...] // bind_transaction函數中的對象驗證。 // offp是指向偏移量緩衝區的指針 for (; offp < off_end; offp ++ ) { struct binder_object_header * hdr; size_t object_size = 活頁夾驗證對象(t- > buffer, * offp); 如果 (object_size == 0 || * offp < off_min) { bindingr_user_error( “%d:%d獲得了具有無效偏移量(%lld,最小%lld最大%lld)或對象的事務。 \ n ” , proc- > pid, 線程 -> pid, (u64) * offp, (u64)off_min, (u64)t- > 緩衝區 -> data_size); return_error = BR_FAILED_REPLY; return_error_param = -EINVAL; return_error_line = __LINE__; 轉到 err_bad_offset; } hdr = ( struct binder_object_header * )(t- > 緩衝區 -> 數據 + * offp); off_min = * offp + object_size; 開關 (hdr- > type) { 情況 BINDER_TYPE_BINDER: 案例 BINDER_TYPE_WEAK_BINDER: { // [..]驗證和翻譯 案例 BINDER_TYPE_HANDLE: 案例 BINDER_TYPE_WEAK_HANDLE: { // [..]驗證和翻譯 } 案例 BINDER_TYPE_FD:{ // [..]驗證和翻譯 } 案例 BINDER_TYPE_FDA:{ // [..]驗證和翻譯 } 案例 BINDER_TYPE_PTR: { // [..]驗證和翻譯 }
弱/強粘結劑/處理機
活頁夾對象引用可以是指向本地對象的虛擬內存地址(活頁夾引用),也可以是標識另一個進程的遠程對象的處理程序(處理程序引用)。
當內核獲取對象引用(本地或遠程)時,它將更新內部表,該表包含每個進程的真實虛擬內存地址和處理程序(binder <=>處理程序)之間的映射。
有兩種翻譯:
- 將虛擬內存地址轉換爲處理程序:
binder_translate_binder
- 將處理程序轉換爲虛擬內存地址:
binder_translate_handle
Binder內核模塊保留共享對象的引用計數。 與新進程共享引用時,其計數器值將增加。 當不再使用參考時,將通知所有者並可以釋放它。
活頁夾->處理程序翻譯
//文件:drivers / android / binder.c 靜態 int binding_translate_binder ( struct flat_binder_object * fp, struct binder_transaction * t, struct binder_thread * 線程 ) { // [...] 節點 = binder_get_node(proc,fp- > 粘合劑); if ( ! 節點) { 節點 = binder_new_node(proc, fp); 如果 ( ! 節點) 返回 -ENOMEM; } 如果 (fp- > cookie != 節點 -> cookie) { // [...]錯誤 } // SELinux檢查 如果 (security_binder_transfer_binder(proc- > tsk, target_proc- > tsk)) { // [...]錯誤 } ret = binder_inc_ref_for_node(target_proc, 節點, fp- > hdr.type == BINDER_TYPE_BINDER, & 線程 -> todo 和& rdata); 如果 (ret) 完成 如果 ( fp- > hdr.type == BINDER_TYPE_BINDER) fp- > hdr.type = BINDER_TYPE_HANDLE; 其他 fp- > hdr.type = BINDER_TYPE_WEAK_HANDLE; fp- > 粘結劑 = 0 ; fp- > handle = rdata.desc; fp- > cookie = 0 ; // [..] }
該函數獲取與綁定器值(虛擬地址)對應的節點,或者如果不存在該節點,則創建一個新節點。 此節點在本地對象和遠程對象( rdata.desc
)之間具有關聯。 在SELinux安全檢查之後,引用計數器將增加,並且綁定程序對象中的引用值將更改,並由引用處理程序替換。
處理程序->活頁夾翻譯
//文件:drivers / android / binder.c 靜態 整數 binder_translate_handle ( 結構 flat_binder_object * fp, struct binder_transaction * t, struct binder_thread * 線程 ) { // [...] 節點 = binder_get_node_from_ref(proc,fp- > 句柄, fp- > hdr.type == BINDER_TYPE_HANDLE 和 src_rdata); if ( ! 節點) { // [...]錯誤 } // SELinux安全檢查 如果 (security_binder_transfer_binder(proc- > tsk, target_proc- > tsk)) { ret = -EPERM; 完成 } binding_node_lock(node); if ( node- > proc == target_proc) { 如果 ( fp- > hdr.type == BINDER_TYPE_HANDLE) fp- > hdr.type = BINDER_TYPE_BINDER; 其他 fp- > hdr.type = BINDER_TYPE_WEAK_BINDER; fp- > 粘合劑 = 節點 -> ptr; fp- > cookie = 節點 -> cookie; // [...] binding_inc_node_nilocked(node, fp- > hdr.type == BINDER_TYPE_BINDER, 0 , NULL); // [...] } 其他 { struct binder_ref_data dest_rdata; ret = binder_inc_ref_for_node(target_proc, 節點, fp- > hdr.type == BINDER_TYPE_HANDLE, NULL & dest_rdata); // [...] fp- > 粘結劑 = 0 ; fp- > handle = dest_rdata.desc; fp- > cookie = 0 ; } 完成: binding_put_node(node); 返回 ret }
此翻譯功能與上一個功能非常相似。 但是,我們可以注意到,處理程序引用可以在不同的過程之間共享。 如果目標進程與節點匹配,則僅在綁定程序引用中轉換處理程序引用。
文件描述符
當聯編程序對象類型爲BINDER_TYPE_FD或BINDER_TYPE_FDA時,內核需要檢查文件描述符是否正確(與打開的struct文件相關聯)並在目標進程中將其複製。 翻譯是由binder_translate_fd
函數完成的。 詳情如下:
//文件:drivers / android / binder.c 靜態 整數 bind_translate_fd ( 整數 fd, struct binder_transaction * t, struct binder_thread * 線程 , struct binder_transaction * in_reply_to) { // [...] // 1:檢查目標是否允許文件描述符 如果 (in_reply_to) target_allows_fd = !! (in_reply_to- > 標誌 和 TF_ACCEPT_FDS); 其他 target_allows_fd = t- > 緩衝區 -> target_node- > accept_fds; 如果 ( ! target_allows_fd) { binder_user_error( “%d:%d使用fd獲得了%s,%d,但是目標不允許fds \ n ” , proc- > pid, 線程 -> pid, in_reply_to ? “ reply” : “交易” , fd); ret = -EPERM; 轉到 err_fd_not_accepted; } // 2:獲取與文件描述符號相對應的文件結構 文件 = fget(fd); 如果 ( ! 文件) { binding_user_error( “%d:%d獲得了無效fd的交易,%d \ n ” , proc- > pid, 線程 -> pid, fd); ret = -EBADF; 轉到 err_fget; } // 3:SELinux檢查 ret = security_binder_transfer_file(proc- > tsk, target_proc- > tsk, file); 如果 (ret < 0 ) { ret = -EPERM; 轉到 err_security; } // 4:在目標進程中獲取一個“免費”文件描述符號。 target_fd = task_get_unused_fd_flags(target_proc, O_CLOEXEC); 如果 (target_fd < 0 ) { ret = -ENOMEM; 轉到 err_get_unused_fd; } // 5:將“ file”插入到具有target_fd文件描述符編號的目標進程中。 task_fd_install(target_proc, target_fd, 文件); 返回 target_fd; // [...] }
經過一些驗證之後,對task_fd_install
的最後一次調用將在目標進程中添加與調用方文件描述符關聯的文件。 在內部,它使用內核API函數__fd_install
在進程fd數組中安裝文件指針。
緩衝對象
緩衝對象是最有趣的。 它們由硬件服務的Parcel類使用,並允許傳輸內存緩衝區。 緩衝區對象具有一種層次結構機制,可用於修補父對象的偏移量。 這對於發送包含指針的結構非常有用。 活頁夾緩衝區對象由以下結構定義:
//文件:include / uapi / linux / android / binder.h structinder_buffer_object { 結構 binder_object_header hdr; __u32 標誌; binding_uintptr_t 緩衝區; binding_size_t 長度; binding_size_t 父對象; 活頁夾大小 };
讓我們看一個例子:我們有以下代碼,我們想使用Binder發送hidl_string
結構的實例。
struct hidl_string { //從C樣式的字符串複製。 nullptr將創建一個空字符串 hidl_string( const char * ); // ... 私人的 : 詳細信息 :: hidl_pointer < const char > mBuffer; //指向真正的char字符串的指針 uint32_t mSize; //不包括結尾的'\ 0'。 bool mOwnsBuffer; //如果爲true,則mBuffer爲可變字符* }; hidl_string my_obj ( “我的演示字符串” );
創建my_obj時,將執行堆分配以存儲給定的字符串,並設置屬性mBuffer
。 要將這個對象發送到另一個進程,需要兩個BINDER_TYPE_PTR
對象:
- 第一個
binder_buffer_offset
,其緩衝區字段指向my_obj
結構 - 第二個指向堆中的字符串。 該對象必須是先前對象的子對象,並將parent_offset屬性設置爲
char * str
在結構中的位置
下圖詳細說明了所需的兩個綁定程序對象的配置:
活頁夾消息緩衝區
當內核轉換這些對象時,它將修補子緩衝區中描述的偏移量,並將不同的緩衝區([object.buffer,object.buffer + object.length])複製到目標內存進程中。 在我們的例子中,對應於屬性mBuffer
的偏移量是用指針修補的,該指針將字符串存儲在目標存儲過程中。
活頁夾消息緩衝區
爲了解析my_obj
數據,目標進程讀取第一個緩衝區以獲取hidl_struct
(3),而下一個緩衝區的預期大小爲mSize
以確保結構( mSize
)中描述的大小與包含該大小的緩衝區的大小相同。字符串(4) 。
結論
Binder是一個複雜而強大的IPC / RPC系統,它可以使整個Android生態系統正常工作。 即使內核組件很舊,也很少有有關其工作原理的文檔。 此外,最近在Android內核( https://lore.kernel.org/patchwork/patch/757477/ )中添加了有趣的對象類型BINDER_TYPE_FDA
和BINDER_TYPE_PTR
。 這些新類型是Android 8.0中通過Treble
項目引入的新HAL架構中的通信基礎(HIDL)。