kyle shanks
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    --- title: 'Binder - Kernel 驅動分析(v2)' disqus: kyleAlien --- Binder - Kernel 驅動分析(v2) === ## OverView of Content 請配合 [**Binder 驅動結構**](https://hackmd.io/TtT34BAwRP2sZ3vALt4Drg?view) 一起看 這裡著重介紹分析 Kernal 層… BinderKernal 比較複雜,但主要就是從 KernalSpace 角度看 Binder IPC,同時也會接觸到 UserSpace ```shell! APP / Framework │ │ Java Binder API ▼ Binder Framework (Java) │ │ JNI ▼ Binder Native (C++) │ │ ioctl() ▼ Binder Driver (Kernel) // 目前的位置 ``` [TOC] ## Binder 概述 - Misc device * 我們需要先知道 Binder 是一個 **雜項(miscellaneous)驅動**,簡單來說 Binder 是一個虛擬文件,它會直接存在內存(`dev` 中 ) :::info * `/dev` 目錄下就是一個真實的設備 ? Binder 並不是一個真實的硬體設備,Binder 驅動運行於內核,**屬於虛擬驅動設備** ::: * **Misc (`Miscellaneous`) 雜項驅動的 ++主設設備號統一為 10++,次設備號是每個設備獨有的**,驅動程式可以透過 MISC_DYNAMIC_MINOR 來由系統動態分配次設備號 ### Binder 初始化 - binder_init * Binder 的進入點是 [**binder**](https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/drivers/android/binder.c)#**device_initcall** 函數,在系統初始化時會呼叫 `binder_init` 函數 ```c= // binder.c device_initcall(binder_init); ``` * **binder_init** 分析:根據檔案設定創建相關的 binder 檔案 ```c= // common/drivers/android/binder.c static struct dentry *binder_debugfs_dir_entry_root; static int __init binder_init(void) { int ret; char *device_name, *device_tmp struct binder_device *device; struct hlist_node *tmp; char *device_names = NULL; ... 省略部分 // 創建 debug 目錄 binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL); if (binder_debugfs_dir_entry_root) { const struct binder_debugfs_entry *db_entry; binder_for_each_debugfs_entry(db_entry) debugfs_create_file(db_entry->name, db_entry->mode, binder_debugfs_dir_entry_root, db_entry->data, db_entry->fops); // 在 proc 目錄下創建一個 binder 資料夾 binder_debugfs_dir_entry_proc = debugfs_create_dir("proc", binder_debugfs_dir_entry_root); } // 1. 判斷 Kconfig 檔案 if (!IS_ENABLED(CONFIG_ANDROID_BINDERFS) && strcmp(binder_devices_param, "") != 0) { // 讀取 Kconfig 的參數 // kstrdup 為現有字符串分配空間並複制 device_names = kstrdup(binder_devices_param, GFP_KERNEL); if (!device_names) { ret = -ENOMEM; goto err_alloc_device_names_failed; } device_tmp = device_names; while ((device_name = strsep(&device_tmp, ","))) { // @ 追蹤 init_binder_device 函數 ret = init_binder_device(device_name); // 初始化設備 if (ret) goto err_init_binder_device_failed; } } ret = init_binderfs(); if (ret) goto err_init_binder_device_failed; return ret; ... 省略錯誤 } ``` 1. [**KConfig 檔案**](https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/drivers/android/Kconfig):如果有 `ANDROID_BINDER_DEVICES` 標示,按照 `ANDROID_BINDER_DEVICES` 設定的參數在 `dev` 下創建對應檔案 ```shell= ## KConfig config ANDROID_BINDER_DEVICES string "Android Binder devices" depends on ANDROID_BINDER_IPC default "binder,hwbinder,vndbinder" ### 預設創建 3 個檔案 ``` > ![](https://i.imgur.com/LfypTuM.png) 2. **init_binder_device 函數**:動態創建 `binder_device` 結構,並且設定 `binder_device` 的 name、minor、fos ... 等等資料 :::info * **file_operations 結構** 該結構定義了多個 **函數指標**,讓 File 操作行為 (`mmap`、`open`、`release`... 等等) 與真正執行的函數區分開來 ::: ```c= // binder.c // 真正執行的 filesystem 操作函數 const struct file_operations binder_fops = { .owner = THIS_MODULE, .poll = binder_poll, .unlocked_ioctl = binder_ioctl, .compat_ioctl = compat_ptr_ioctl, .mmap = binder_mmap, .open = binder_open, .flush = binder_flush, .release = binder_release, }; /** * name 目前傳入 3 個 name - binder - hwbinder - vndbinder */ static int __init init_binder_device(const char *name) { int ret; struct binder_device *binder_device; // 動態分配 `binder_device` 結構 binder_device = kzalloc(sizeof(*binder_device), GFP_KERNEL); if (!binder_device) return -ENOMEM; // 註冊 binder 的操作函數指針 binder_device->miscdev.fops = &binder_fops; binder_device->miscdev.minor = MISC_DYNAMIC_MINOR; // 次設備號 255 // 設定資料夾 /dev/<name> 名稱 binder_device->miscdev.name = name; refcount_set(&binder_device->ref, 1); // 先設定當前 binder 驅動的 manager 為 INVALID_UID // 之後有設定 Binder Manager 就會將其設定進去 binder_device->context.binder_context_mgr_uid = INVALID_UID; binder_device->context.name = name; // 互斥鎖初始化 mutex_init(&binder_device->context.context_mgr_node_lock); // 註冊 misc 設備 // @ 追蹤 misc_register 函數 ret = misc_register(&binder_device->miscdev); if (ret < 0) { kfree(binder_device); return ret; } // 將當前 binder_device 串接到 binder_devices 列表前面 hlist_add_head(&binder_device->hlist, &binder_devices); return ret; } ``` > ![](https://i.imgur.com/Go1v9lz.png) :::info * **miscdevice 函數**:實作在 [**misc.c**](https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/drivers/char/misc.c;l=173?q=misc.c&ss=android%2Fkernel%2Fsuperproject):該函數的主要功能是 **動態註冊 misc device 號,在 `/dev` 目錄下創建檔案 !** ```c= // miscdevice.h #define MISC_DYNAMIC_MINOR 255 extern int misc_register(struct miscdevice *misc); ``` ::: * Binder File (System Call) 對應處理的函數 | File 函數指針 | Binder 實作函數 | 簡單說明 | | ---------------- | ------------------ | ----------------------------------------------------------- | | `poll`(等待與排程) | `binder_poll` | 用於 `epoll/poll` 等待事件;當有 `BR_*` 回傳或可讀資料時喚醒 thread | | `unlocked_ioctl`(IPC 核心通道) | `binder_ioctl` | Binder IPC 核心入口;處理 `BC_*` 命令(如 `BC_TRANSACTION`、`BC_REPLY`) | | `compat_ioctl`(IPC 核心通道) | `compat_ptr_ioctl` | 32-bit process 在 64-bit kernel 上的 ioctl 相容層轉換 | | `mmap`(建立生命週期) | `binder_mmap` | 建立該 process 的 Binder transaction buffer pool(mmap 區) | | `open`(建立生命週期) | `binder_open` | 建立 `binder_proc`,初始化 `binder_alloc`、thread pool 結構 | | `flush`(清理) | `binder_flush` | 清除 thread 尚未處理的待辦工作(通常在 close 前) | | `release`(建立生命週期) | `binder_release` | 關閉 `/dev/binder` 時釋放 `binder_proc` 與相關資源 | ```c= // binder.c // 真正執行的 filesystem 操作函數 const struct file_operations binder_fops = { .owner = THIS_MODULE, .poll = binder_poll, .unlocked_ioctl = binder_ioctl, .compat_ioctl = compat_ptr_ioctl, .mmap = binder_mmap, .open = binder_open, .flush = binder_flush, .release = binder_release, }; ``` :::success * **Binder 較為重要的函數** 1. **`binder_open`**:打開 `/dev/binder` 設備,再 Binder 驅動中初始化 `binder_proc` 2. **`binder_mmap`**:讓當前 proc 取得 `/dev/binder` 的 **內存映射區**,**映射 (Mapping) 到當前 proc 的記憶體中** 3. **`binder_ioctl`**:Binder IPC 核心入口;處理 `BC_*` 命令(如 `BC_TRANSACTION`、`BC_REPLY`) ::: ### [misc](https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/drivers/char/misc.c) 雜項註冊 * 在上面我們有看到在 `init_binder_device` 函數中,使用 `misc_register` 做雜項硬體註冊到 Android 系統中 :::success * 目前是打算在 `/dev` 目錄下創建三個 binder 相關檔案:`binder`、`hwbinder`、`vndbinder` ::: 1. 可以動態註冊的驅動裝置數量為 `DYNAMIC_MINORS`,也就是 128 個 (`0 ~ 127`),每個標號是否已經被註冊過,都記錄在 `misc_minors` 陣列中 :::info * `misc_minors` 列表中,已分配設定為 1,未分配則為 0 ::: 2. 透過 `find_first_zero_bit` 函數找到第一個未分配的空間 3. **`MKDEV` 創建對應的 dev 檔案**,並註冊到檔案系統中 4. misc 註冊成功後串接到 misc_list 列表 ```c= // common/drivers/char/misc.c static DEFINE_MUTEX(misc_mtx); #define DYNAMIC_MINORS 128 /* like dynamic majors */ int misc_register(struct miscdevice *misc) { dev_t dev; int err = 0; // 是否是動態註冊 bool is_dynamic = (misc->minor == MISC_DYNAMIC_MINOR); // 動態註冊就是 255 // 雜項設備列表頭 INIT_LIST_HEAD(&misc->list); // 取得互斥鎖 mutex_lock(&misc_mtx); if (is_dynamic) { // 找到第一個尚未使用的編號 int i = find_first_zero_bit(misc_minors, DYNAMIC_MINORS); if (i >= DYNAMIC_MINORS) { // 無法動態註冊 (滿) err = -EBUSY; goto out; } misc->minor = DYNAMIC_MINORS - i - 1; // 設定為以使用 set_bit(i, misc_minors); } else { struct miscdevice *c; // 判斷所以雜項設備 list_for_each_entry(c, &misc_list, list) { // 是否已經註冊將要註冊的 misc 號碼 if (c->minor == misc->minor) { err = -EBUSY; goto out; } } } // 創建 dev dev = MKDEV(MISC_MAJOR, misc->minor); // MISC_MAJOR 是 10 // 將記錄檔案裝置 misc 註冊到系統中 misc->this_device = device_create_with_groups(misc_class, misc->parent, dev, misc, misc->groups, "%s", misc->name); // 失敗處理 if (IS_ERR(misc->this_device)) { if (is_dynamic) { int i = DYNAMIC_MINORS - misc->minor - 1; if (i < DYNAMIC_MINORS && i >= 0) clear_bit(i, misc_minors); misc->minor = MISC_DYNAMIC_MINOR; } err = PTR_ERR(misc->this_device); goto out; } // 註冊成功後串接到 misc_list 列表 list_add(&misc->list, &misc_list); out: // 釋放互斥鎖 mutex_unlock(&misc_mtx); return err; } ``` ## binder_open 概述 * **binder_open 這個函數主要是在做以下幾件事** 1. **創建 `binder_proc` 實體**(管理數據的記錄器):可以 adb shell 在 `/proc` 看到相對應的數據,通常都是由進程號為名 ```c= // /drivers/staging/android/binder.h // 管理數據的紀錄體 (proc 進程空間) struct binder_proc { struct hlist_node proc_node; struct rb_root threads; struct rb_root nodes; struct rb_root refs_by_desc; struct rb_root refs_by_node; int pid; ... wait_queue_head_t freeze_wait; struct list_head todo; ... }; ``` > ![image](https://hackmd.io/_uploads/B1JMNCmtWe.png) 2. 初始化 `binder_proc` 結構 (放進當前進程訊息;也就是 Caller 再 Binder 驅動中的訊息) 3. **添加到 `binder_procs` 鏈表中**(`binder_procs` 是 Kernel space 的全局變量) 4. 將當前進程 (呼叫者的進程) **`proc` 保存到 Binder 的 `filp->private_data`** (將創建該文件的 proc 存到 `/dev/binder` 中) > ![](https://i.imgur.com/E1QxJc2.png) :::success * **不同 App 應用都會透過 `binder_open` 會開起相同的 `/dev/binder` 文件**,打開 Binder 設備後取得偏移值,這個偏移值會存在 `mProcess->mDriverFD` * `binder_open` 返回的 FD,也就是 Caller 進程內部的檔案編號,協助 Caller 進程之後可以訪問 Binder 驅動 > 正確點的描述是:讓這個進程能透過 VFS 找到對應的 file 物件,間接找到 Binder 驅動 ```mermaid flowchart LR subgraph Process FD["binder_open<br>fd = 5"] end subgraph Kernel FILE["struct file"] PROC["binder_proc"] DRIVER["binder_ioctl()"] end FD --> FILE FILE -->|private_data| PROC FILE -->|f_op| DRIVER ``` ::: ### binder_open 函數分析 1. **`binder_open` 開啟 `/dev/binder` 節點(驅動層在 binder_open 函數實現)**:並在系統中的 [**proc 目錄**](https://hackmd.io/MtOap5UaR7KcJpdTisc75g?view#%E8%B3%87%E6%96%99%E5%A4%BE%E4%BB%8B%E7%B4%B9) (虛擬目錄,用於系統內存的映射) 下產生各種管理訊息 ```c= // /drivers/staging/android/binder.c static int binder_open(struct inode *nodp, struct file *filp) { // binder_proc 就是管理數據的紀錄體(每個進程都有獨立一個) struct binder_proc *proc; ... // 分配 binder_proc 結構一個記憶體空間 proc = kzalloc(sizeof(*proc), GFP_KERNEL); if (proc == NULL) return -ENOMEM; ... return 0; } ``` 2. **初始化 proc 操作** : 對 `binder_proc` 結構賦予值,並初始化 proc 列表 ```c= // /drivers/staging/android/binder.c static int binder_open(struct inode *nodp, struct file *filp) { // binder_proc 就是管理數據的紀錄體(每個進程都有獨立一個) struct binder_proc *proc; ... // 初始化操作 get_task_struct(current); proc->tsk = current->group_leader; // 工作控制區塊 ... // 初始化 Linked 列表 INIT_LIST_HEAD(&proc->todo); // todo Link init_waitqueue_head(&proc->wait); // wait Link proc->default_priority = task_nice(current); ... return 0; } ``` 3. 創建同步鎖,將 `proc` 訊息加入 Binder 全局管理鏈表 (**binder_procs**) binder 驅動會為每個 `open("/dev/binder")` 的進程建立一個 binder_proc,並把它加入全域 `binder_procs` binder_procs 的初始化如下 ```c= // /drivers/staging/android/binder.c static HLIST_HEAD(binder_procs); ``` > ![](https://i.imgur.com/tBkZIVu.png) * **指定 `proc` 給 `filp->private_data`**(filp 就是 `/dev/binder` 文件映射到應用進程的代表),**未來 Binder 驅動就可以透過 `filp->private_data` 找到對應的應用** > ![](https://i.imgur.com/kW5qxSo.png) ```c= // /drivers/staging/android/binder.c static int binder_open(struct inode *nodp, struct file *filp) { // binder_proc 就是管理數據的紀錄體(每個進程都有獨立一個) struct binder_proc *proc; ... // 獲取鎖 binder_lock(__func__); binder_stats_created(BINDER_STAT_PROC); // 計數 + 1 // 將創建出的 proc 結構 加入到 binder_procs 的 header hlist_add_head(&proc->proc_node, &binder_procs); // 進程 pid proc->pid = current->group_leader->pid; INIT_LIST_HEAD(&proc->delivered_death); // 將這個 proc 與 filp 串起來,這樣下次就可以通過 filp 找到該 proc // 最終會透過 private_data 取得 proc 資料 filp->private_data = proc; // 重點 !! // 解開鎖 binder_unlock(__func__); return 0; } ``` > ![](https://i.imgur.com/RM4seb6.png) ### binder_proc 結構關係 * **`binder_proc` 是一個進程在 Kernel space 的代表**:它還會與 `binder_node`、`binder_thread`、`binder_ref`、`binder_buffer` 結構會有關係 :::info binder_proc 會透過 **紅黑數** 儲存多個結構 ::: ```c= // binder_internal.h struct binder_proc { ... // binder_thread struct rb_root threads; // binder_node struct rb_root nodes; // binder_ref struct rb_root refs_by_desc; struct rb_root refs_by_node; ... 省略部份 struct binder_alloc alloc; ... 省略部份 }; ``` > ![](https://i.imgur.com/JgxveLN.png) ## binder_mmap 概述 `binder_mmap` 會為「Caller 進程」建立一塊由 Binder 驅動管理的 transaction buffer pool,這塊物理頁會同時映射(Mapping)到該進程的 UserSpace 與 KernelSpace 當其他進程呼叫它時,驅動會將資料複製到這塊 mmap 區域中,讓接收端可以直接從自己的 mmap 區讀取資料 :::warning 但這段過程仍是兩次複製,因為這是一種 Mapping 的過程,而不像是 DMA 這種機制 ::: ### binder_mmap 分析 * 在說明 Binder 驅動層的 `binder_mmap` 函數之前,先介紹幾個重要的數據結構,並且以下分析會將函數拆分說明(以下是站在 **Binder 驅動的角度對 Caller 進程的描述**) | Struct 名 | 說明 | | -------- | -------- | | **vm_area_struct** \*vma | 使用者空間的描述;該應用程序使用的 **虛擬內存**,其中 `vma->vm_start`、`vma->vm_end` 代表了 Caller 內存的起點 & 終點<br>**`vm_area_struct` 是 Binder 驅動中對使用者虛擬內存的描述** | | **vm_struct** \*area | Kernel 自己的「非連續實體頁面,但連續虛擬地址」記憶體區段(站在使用者的角度紀錄自身) | | **binder_proc** \*proc | binder 驅動分配的數據結構(`binder_open` 中有用到),儲存 **當前進程相關訊息(內存分配、線程管理)** | :::info * **為何要分 `vm_area_struct`、`vm_struct`** ? 主要是因為 PC 對硬體的設計,讓其有 MMU 機制,產生了虛擬記憶體的概念 > ![](https://i.imgur.com/hUXTsaC.png) ::: binder_mmap 流程圖 > ![](https://i.imgur.com/PbEcX82.png) * 分析 **binder_mmap 函數** 1. 判斷 `vm_area_struct` (APP 進程) 標誌是否禁止了 mmap (**`FORBIDDEN_MMAP_FLAGS`**) ```java= // binder.c // filp 就是 binder 文件 // vm_area_struct 是 Binder 空間對 APP 端的虛擬空間描述 (使用者空間) static int binder_mmap(struct file *filp, struct vm_area_struct *vma) { // private_data 是在 open 時設定的 ! // (biner_open 創建虛擬設備~ Do u remember~ 你是否記得~ ) struct binder_proc *proc = filp->private_data; ... 省略部分 // 判斷是否有禁止 Flag ! if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) { ... 錯誤訊息 return -EPERM; } vma->vm_ops = &binder_vm_ops; // 操作函數指針 vma->vm_private_data = proc; // @ 主要 binder_alloc_mmap_handler 函數處理,接下來主要分析該函數 return binder_alloc_mmap_handler(&proc->alloc, vma); } ``` > ![](https://i.imgur.com/0mfTCrR.png) 2. 判斷 mmap 是否已經有分配 (判斷 `buffer_size` 指針是否為空),若已分配則不繼續往下執行 :::info * 從這裡可以看出,**每個 APP 進程只能 mmap 一次** 之所以只能映射一次是因為 binder 的這塊 mmap 區被多執行緒共用,多次映射會導致管理的複雜度變高 ::: ```java= // drivers/android/binder_alloc.c // alloc: Kernel 管理 proc 的紀錄體 // vma: Binder 空間對 APP 端的虛擬空間描述 (使用者空間) int binder_alloc_mmap_handler(struct binder_alloc *alloc, struct vm_area_struct *vma) { int ret; const char *failure_string; // 錯誤訊息 struct binder_buffer *buffer; // 線程鎖 mutex_lock(&binder_alloc_mmap_lock); // 判斷是否有 mmap 分配 if (alloc->buffer_size) { ret = -EBUSY; failure_string = "already mapped"; // 若該進程已經分配 mmap 過則不可再分配 goto err_already_mapped; } ... mutex_unlock(&binder_alloc_mmap_lock); return 0; ...省略部分 (錯誤處理) } ``` > ![](https://i.imgur.com/EDi8CQj.png) 3. Binder 驅動會判斷 `App 進程` 申請的內存大小 - **==最大 4M==** - 若超出就修正為 4M (**這 4M 是分配在核心的緩衝區**) > 檢查呼叫 mmap 的 app 進程申請的空間 ```java= // drivers/android/binder_alloc.c // alloc: Kernel 管理 proc 的紀錄體 // vma: Binder 空間對 APP 端的虛擬空間描述 (使用者空間) int binder_alloc_mmap_handler(struct binder_alloc *alloc, struct vm_area_struct *vma) { int ret; const char *failure_string; // 錯誤訊息 struct binder_buffer *buffer; ... // 修正 Binder 內存大小,最大 4M alloc->buffer_size = min_t(unsigned long, vma->vm_end - vma->vm_start, SZ_4M); // 解開線程鎖 mutex_unlock(&binder_alloc_mmap_lock); ... return 0; ...省略部分 (錯誤處理) } ``` :::info * 應用進程對 MMAP 真正能申請的內存大小是多少 ? 4M 是 Binder 驅動定義的;APP 應用層定義的是 `1M - 8K = (0.992M)` > eg. Intent 就不能超過 1M 大小 ::: 4. 初始化進程 `proc` 的參數 (buffer、pages...),用 `kzalloc` 分配了 pages 數組空間 (申請物理頁面),**先幫進程申請一頁 (4KB) 的內存,當需要更多空間時才會繼續申請更大的空間** * 將在進程的 Binder 空間儲存的 App 虛擬位址 (`vma->vm_start`),指向 Kernel (`alloc->buffer`) 位置 * 在 Kernel 分配 `alloc->pages` 空間 ```java= // drivers/android/binder_alloc.c // alloc: Kernel 管理 proc 的紀錄體 // vma: Binder 空間對 APP 端的虛擬空間描述 (使用者空間) int binder_alloc_mmap_handler(struct binder_alloc *alloc, struct vm_area_struct *vma) { int ret; const char *failure_string; // 錯誤訊息 struct binder_buffer *buffer; ... // 初始化 proc 中 alloc->buffer 的參數 // 將應用進程 (vma) 開始位置 (vm_start) 放置到 buffer alloc->buffer = (void __user *)vma->vm_start; // 分配 page 空間 alloc->pages = kcalloc(alloc->buffer_size / PAGE_SIZE, sizeof(alloc->pages[0]), // 一頁空間 GFP_KERNEL); // 判斷是否申請成功 if (alloc->pages == NULL) { ret = -ENOMEM; failure_string = "alloc page array"; goto err_alloc_pages_failed; } return 0; ...省略部分 (錯誤處理) } ``` :::info * 開一頁 4KB 物理內存夠用嗎 ? 這是 Linxu 的核心分頁機制,先開一頁 Page,當有需要時會 **動態拓展**,以免浪費物理記憶體空間 ::: > ![](https://i.imgur.com/hmruuL2.png) 5. 使用 `kcalloc` 申請 `binder_buffer` 結構空間 (這個 buffer 在內核空間) ```c= // drivers/android/binder_alloc.h struct binder_buffer { struct list_head entry; /* free and allocated entries by address */ struct rb_node rb_node; /* free entry by size or allocated entry */ /* by address */ unsigned free:1; unsigned clear_on_free:1; unsigned allow_user_free:1; unsigned async_transaction:1; unsigned oneway_spam_suspect:1; unsigned debug_id:27; struct binder_transaction *transaction; struct binder_node *target_node; size_t data_size; size_t offsets_size; size_t extra_buffers_size; void __user *user_data; int pid; }; // -------------------------------------------------------- // drivers/android/binder_alloc.c // alloc: Kernel 管理 proc 的紀錄體 // vma: Binder 空間對 APP 端的虛擬空間描述 (使用者空間) int binder_alloc_mmap_handler(struct binder_alloc *alloc, struct vm_area_struct *vma) { int ret; const char *failure_string; // 錯誤訊息 struct binder_buffer *buffer; ... // 申請 binder_buffer 空間 buffer = kzalloc(sizeof(*buffer), GFP_KERNEL); if (!buffer) { ret = -ENOMEM; failure_string = "alloc buffer struct"; goto err_alloc_buf_struct_failed; } ... return 0; ...省略部分 (錯誤處理) } ``` > ![](https://i.imgur.com/4z9Y22d.png) 6. 在分配結束 `binder_buffer` 空間完成後,映射到 `binder_alloc` (Kernel Space) 中,並將創建好的 buffer 添加到 proc 中 (未來就可以找到這個空間做使用) > 這樣將來在寫入資料時就會映射 Mapping 到 User 空間 ```c= // drivers/android/binder_alloc.c // alloc: Kernel 管理 proc 的紀錄體 // vma: Binder 空間對 APP 端的虛擬空間描述 int binder_alloc_mmap_handler(struct binder_alloc *alloc, struct vm_area_struct *vma) { int ret; const char *failure_string; // 錯誤訊息 struct binder_buffer *buffer; ... 省略部分 // 用戶空間的 Buffer 指定給 Binder->user_date,該空間就可以給用戶寫資料 buffer->user_data = alloc->buffer; // 把 buffer->entry 存到 alloc->buffers 中 list_add(&buffer->entry, &alloc->buffers); // 1 => 代表該內存可用 buffer->free = 1; // 計算得出空間內存的大小 binder_insert_free_buffer(alloc, buffer); // 進行 Buffer 分配 & 排序 // 2 => 代表異步空間 alloc->free_async_space = alloc->buffer_size / 2; // 用戶空間設定為分配好的 alloc 空間 binder_alloc_set_vma(alloc, vma); // 添加原子計數 mmgrab(alloc->vma_vm_mm); return 0; ...省略部分 (錯誤處理) } ``` :::info * 在這邊也可以看到異步空間只占一半,避免過度佔據同步處理的優先度 ::: > ![](https://i.imgur.com/jqQQ1Ay.png) * 將 binder_buffer 添加到 Caller 進程的列表 `alloc->buffers` 中 ```c= list_add(&buffer->entry, &alloc->buffers); ``` * 將 binder_buffer 添加到該進程的 free 列表 (`binder_insert_free_buffer`) ```c= static void binder_insert_free_buffer(struct binder_alloc *alloc, struct binder_buffer *new_buffer) { struct rb_node **p = &alloc->free_buffers.rb_node; struct rb_node *parent = NULL; struct binder_buffer *buffer; size_t buffer_size; size_t new_buffer_size; BUG_ON(!new_buffer->free); // 計算新 buffer 空間大小 new_buffer_size = binder_alloc_buffer_size(alloc, new_buffer); ... // 計算紅黑數插入位置 while (*p) { parent = *p; buffer = rb_entry(parent, struct binder_buffer, rb_node); BUG_ON(!buffer->free); buffer_size = binder_alloc_buffer_size(alloc, buffer); if (new_buffer_size < buffer_size) p = &parent->rb_left; else p = &parent->rb_right; } // 插入紅黑樹 rb_link_node(&new_buffer->rb_node, parent, p); rb_insert_color(&new_buffer->rb_node, &alloc->free_buffers); } ``` :::success * 到這一步就完成了 `Binder 文件` 被讀取到 Kernel space,並在內核建立 `binder buffer` 再設定映射關係,最終把 `binder buffer` 存到 proc 的 `alloc->buffers` 中 ::: ### adb - 驗證 binder mmap * 首先用 adb root 進入手機裝置,查看 `system_server` 服務的 pid(system_server 同樣也有開啟 `/dev/binder` 文件,並用 mmap 將該文件映射到自己的進程中) ```shell= adb shell ps -A | grep system_server ``` system server 目前 pid 就是 `1920` > ![](https://i.imgur.com/lXNw5u1.png) * 在 `proc` 中查看 pid 的 maps 映射位置 ```shell= cat /proc/1920/maps | grep /dev/binder ``` 下圖的虛擬地址資訊對應到 vm_area_struct 裡的 vm_start(`7411da99d000`) / vm_end(`7411da9b000`) > ![](https://i.imgur.com/KMtrIS4.png) ## Binder 核心 Buffer ### 分配核心 Buffer * 當一個應用對 Binder 發起 `BC_TRANSACTION`、`BC_REPLY` 命令時,Binder 驅動就會將使用者資料 **複製** 到 Binder 驅動中,之後再傳遞給目標進程; 這時就需要 Binder 驅動分配一塊核心空間給儲存複製進來的資料 分配的是「目標進程的 binder mmap pool 裡的一段 buffer」(user VA 區段),並由驅動確保對應頁面(backing pages)已建立/映射,讓資料可以被寫入 > 因為 `binder_buffer->user_data` 是 `void __user *`,它指向的是 目標進程 UserSpace 的 mmap 區域 :::info * Buffer 是分配在目標 proc 的核心空間 Buffer 是從「目標進程的 `binder_alloc` 所管理的 mmap pool」切出來的,其地址是目標 proc 的 UserSpace VA(user_data),但由 kernel 控制其 backing pages ::: > ![](https://i.imgur.com/agkV8lG.png) * 在 Binder 驅動中就是 [**binder_alloc**](https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/drivers/android/binder_alloc.c)#`binder_alloc_new_buf` 函數負責 ```c= // binder_alloc.c struct binder_buffer *binder_alloc_new_buf( struct binder_alloc *alloc, size_t data_size, size_t offsets_size, size_t extra_buffers_size, int is_async, int pid) { struct binder_buffer *buffer; // 分配時必須使用互斥鎖鎖住 mutex_lock(&alloc->mutex); // @ 查看 binder_alloc_new_buf_locked buffer = binder_alloc_new_buf_locked(alloc, data_size, offsets_size, extra_buffers_size, is_async, pid); // 解開互斥鎖 mutex_unlock(&alloc->mutex); return buffer; } ``` * 分析 `binder_alloc_new_buf_locked` 函數 1. 取得目標 alloc 並判斷大小是否溢位,最終決定這次要分配的緩衝區大小賦予 size 變數 :::info 在分配核心核心空間時,要多分配一個 `binder_buffer` 結構來描述個核心緩衝區 ::: ```c= static struct binder_buffer *binder_alloc_new_buf_locked( struct binder_alloc *alloc, // 可看作 "目標" 進程 proc size_t data_size, // 資料緩衝的大小 size_t offsets_size, // 偏移陣列緩衝區大小 size_t extra_buffers_size, // 額外資料大小 int is_async, // 是否是異步交易 int pid) { struct rb_node *n = alloc->free_buffers.rb_node; struct binder_buffer *buffer; size_t buffer_size; struct rb_node *best_fit = NULL; void __user *has_page_addr; void __user *end_page_addr; size_t size, data_offsets_size; int ret; mmap_read_lock(alloc->vma_vm_mm); // 查找使用者空間分配的 alloc if (!binder_alloc_get_vma(alloc)) { ... 省略錯誤 return ERR_PTR(-ESRCH); } mmap_read_unlock(alloc->vma_vm_mm); // 計算要分配的核心大小空間 data_offsets_size = ALIGN(data_size, sizeof(void *)) + ALIGN(offsets_size, sizeof(void *)); // 判斷是否溢位 if (data_offsets_size < data_size || data_offsets_size < offsets_size) { ... 省略錯誤 return ERR_PTR(-EINVAL); } // 再計算額外資料大小,並判斷是否溢位 size = data_offsets_size + ALIGN(extra_buffers_size, sizeof(void *)); if (size < data_offsets_size || size < extra_buffers_size) { ... 省略錯誤 return ERR_PTR(-EINVAL); } // 判斷目標進程可接受的非同步的大小 // 並加上 `binder_buffer`(用於描述) 大小判斷溢位 if (is_async && alloc->free_async_space < size + sizeof(struct binder_buffer)) { ... 省略錯誤 return ERR_PTR(-ENOSPC); } /* Pad 0-size buffers so they get assigned unique addresses */ size = max(size, sizeof(void *)); ... ``` 2. 找尋目標進程的紅黑數的核心空間有沒有適當的位置,並有以下幾種狀況 * 找不到合適位置,返回錯誤 * 找不到合適位置,但有一塊更大個區塊可以使用 (待分配) ```c= static struct binder_buffer *binder_alloc_new_buf_locked( struct binder_alloc *alloc, // 可看作 "目標" 進程 proc size_t data_size, // 資料緩衝的大小 size_t offsets_size, // 偏移陣列緩衝區大小 size_t extra_buffers_size, // 額外資料大小 int is_async, // 是否是異步交易 int pid) { struct rb_node *n = alloc->free_buffers.rb_node; struct binder_buffer *buffer; size_t buffer_size; struct rb_node *best_fit = NULL; void __user *has_page_addr; void __user *end_page_addr; size_t size, data_offsets_size; int ret; ... 決定 Size // 紅黑樹中找合適的位置 while (n) { buffer = rb_entry(n, struct binder_buffer, rb_node); BUG_ON(!buffer->free); buffer_size = binder_alloc_buffer_size(alloc, buffer); if (size < buffer_size) { best_fit = n; n = n->rb_left; } else if (size > buffer_size) n = n->rb_right; else { best_fit = n; break; } } // 判斷是否有找到合適位置 if (best_fit == NULL) { ... 省略錯誤處理 return ERR_PTR(-ENOSPC); } if (n == NULL) { // 判沒有合適位置,但 ! 找到一塊較大的核心空間 buffer = rb_entry(best_fit, struct binder_buffer, rb_node); buffer_size = binder_alloc_buffer_size(alloc, buffer); } ... } ``` 3. 計算 Buffer 結束位置 & 使用者要求的結束位置,再決定最後要分配的記憶體位置 (`end_page_addr`),之後呼叫 `binder_update_page_range` 分配實體位置 ```c= static struct binder_buffer *binder_alloc_new_buf_locked( struct binder_alloc *alloc, // 可看作 "目標" 進程 proc size_t data_size, // 資料緩衝的大小 size_t offsets_size, // 偏移陣列緩衝區大小 size_t extra_buffers_size, // 額外資料大小 int is_async, // 是否是異步交易 int pid) { struct rb_node *n = alloc->free_buffers.rb_node; struct binder_buffer *buffer; size_t buffer_size; struct rb_node *best_fit = NULL; void __user *has_page_addr; void __user *end_page_addr; size_t size, data_offsets_size; int ret; ... 決定 Size ... 在目標核心空間找到相對適合的位置 // 計算 Page 的結束位置 has_page_addr = (void __user *) (((uintptr_t)buffer->user_data + buffer_size) & PAGE_MASK); WARN_ON(n && buffer_size != size); // 計算使用者要求的核心大小 end_page_addr = (void __user *)PAGE_ALIGN((uintptr_t)buffer->user_data + size); // 如果使用者要求過大 if (end_page_addr > has_page_addr) // 修正為使用者要求的大小 end_page_addr = has_page_addr; // 分配實體空間 // @ 之後分析 binder_update_page_range ret = binder_update_page_range(alloc, 1, (void __user *) PAGE_ALIGN((uintptr_t)buffer->user_data), end_page_addr); if (ret) return ERR_PTR(ret); ... return buffer; ... } ``` 4. 最後再做一些準備就完成分配:判斷是否需要將多出的空間加入目標進城的 free buffer、將 buffer 加入目標進程使用中的紅黑樹... 等等 ```c= static struct binder_buffer *binder_alloc_new_buf_locked( struct binder_alloc *alloc, // 可看作 "目標" 進程 proc size_t data_size, // 資料緩衝的大小 size_t offsets_size, // 偏移陣列緩衝區大小 size_t extra_buffers_size, // 額外資料大小 int is_async, // 是否是異步交易 int pid) { struct rb_node *n = alloc->free_buffers.rb_node; struct binder_buffer *buffer; size_t buffer_size; struct rb_node *best_fit = NULL; void __user *has_page_addr; void __user *end_page_addr; size_t size, data_offsets_size; int ret; ... 決定 Size ... 在目標核心空間找到相對適合的位置 ... 分配地址 // 判斷是否有多餘的空間 if (buffer_size != size) { struct binder_buffer *new_buffer; // 分配空間 new_buffer = kzalloc(sizeof(*buffer), GFP_KERNEL); if (!new_buffer) { pr_err("%s: %d failed to alloc new buffer struct\n", __func__, alloc->pid); goto err_alloc_buf_struct_failed; } // 賦予使用者空間位置 new_buffer->user_data = (u8 __user *)buffer->user_data + size; // 添加進紅黑數 list_add(&new_buffer->entry, &buffer->entry); // 標註可用 new_buffer->free = 1; // 添加進 alloc 中的 free 紅黑樹 binder_insert_free_buffer(alloc, new_buffer); } // 從 free buffer 中移除 rb_erase(best_fit, &alloc->free_buffers); // 標記已在使用 buffer->free = 0; buffer->allow_user_free = 0; // 添加進使用中的樹 binder_insert_allocated_buffer_locked(alloc, buffer); ... buffer->data_size = data_size; buffer->offsets_size = offsets_size; buffer->async_transaction = is_async; buffer->extra_buffers_size = extra_buffers_size; buffer->pid = pid; buffer->oneway_spam_suspect = false; if (is_async) { // 異步則縮減空間 alloc->free_async_space -= size + sizeof(struct binder_buffer); ... } return buffer; ... } ``` ### 釋放核心 Buffer * 當 Binder 處理完一個命令時,Binder 驅動發出 `BR_TRANSACTION`、`BR_REPLY` 命令之後,**應用方就會使用 ++`BC_FREE_BUFFER` 命令++,讓 Binder 驅動釋放對應的核心緩衝**; :::info * 要釋放的 Buffer 在目標 proc 的核心空間 ::: > ![](https://i.imgur.com/9Rs117P.png) * 在 Binder 驅動中就是 binder_alloc#`binder_alloc_new_buf` 函數負責 ```c= void binder_alloc_free_buf(struct binder_alloc *alloc, struct binder_buffer *buffer) { if (buffer->clear_on_free) { // 如果有標記 clear_on_free,則呼叫 binder_alloc_clear_buf binder_alloc_clear_buf(alloc, buffer); buffer->clear_on_free = false; } mutex_lock(&alloc->mutex); // @ 分析 binder_free_buf_locked binder_free_buf_locked(alloc, buffer); mutex_unlock(&alloc->mutex); } ``` * 分析 `binder_free_buf_locked` 函數 ```c= // binder_alloc.c static void binder_free_buf_locked(struct binder_alloc *alloc, struct binder_buffer *buffer) { size_t size, buffer_size; // 計算緩衝區大小 buffer_size = binder_alloc_buffer_size(alloc, buffer); size = ALIGN(buffer->data_size, sizeof(void *)) + // 計算 資料大小 ALIGN(buffer->offsets_size, sizeof(void *)) + // 計算 資料偏移 ALIGN(buffer->extra_buffers_size, sizeof(void *));// 計算 額外資料 ... if (buffer->async_transaction) { alloc->free_async_space += buffer_size + sizeof(struct binder_buffer); ... } // 更新 Page 資料 binder_update_page_range(alloc, 0, // 0 代表釋放 (void __user *)PAGE_ALIGN((uintptr_t)buffer->user_data), (void __user *)(((uintptr_t) buffer->user_data + buffer_size) & PAGE_MASK)); rb_erase(&buffer->rb_node, &alloc->allocated_buffers); // 設定為可用 buffer->free = 1; // 進行合併 // 判斷是否是最後一個 if (!list_is_last(&buffer->entry, &alloc->buffers)) { struct binder_buffer *next = binder_buffer_next(buffer); if (next->free) { rb_erase(&next->rb_node, &alloc->free_buffers); binder_delete_free_buffer(alloc, next); } } if (alloc->buffers.next != &buffer->entry) { struct binder_buffer *prev = binder_buffer_prev(buffer); if (prev->free) { binder_delete_free_buffer(alloc, buffer); rb_erase(&prev->rb_node, &alloc->free_buffers); buffer = prev; } } // 將合併完的緩衝區,放回 Proc 中的 alloc 中的紅黑樹中 binder_insert_free_buffer(alloc, buffer); } ``` ### 查詢核心 Buffer * 雖說使用者可以傳送 `BC_FREE_BUFFER` 指令就可以讓 Binder 驅動釋放目標 Proc 中的 Buffer 空間,但是要釋放哪一個空間 ? 這是應用程式指定,才能找到對應的 `binder_buffer` 結構 * Binder 驅動 `binder_alloc_prepare_to_free` 函數來負責取得指定 `binder_buffer` ```c= // binder_alloc.c struct binder_buffer *binder_alloc_prepare_to_free(struct binder_alloc *alloc, uintptr_t user_ptr) { struct binder_buffer *buffer; // 互斥鎖保護 mutex_lock(&alloc->mutex); // @ 分析 binder_alloc_prepare_to_free_locked buffer = binder_alloc_prepare_to_free_locked(alloc, user_ptr); mutex_unlock(&alloc->mutex); return buffer; } ``` * 分析 `binder_alloc_prepare_to_free_locked` 函數: :::info * Binder 驅動將資料傳給目標 Proc 時,它只是把資料緩衝區的 **使用者空間地址** 船給目標 Proc ::: ```c= // binder_alloc.c static struct binder_buffer *binder_alloc_prepare_to_free_locked( struct binder_alloc *alloc, uintptr_t user_ptr) // 使用者空間,指向了 binder_buffer#data 成員 { struct rb_node *n = alloc->allocated_buffers.rb_node; struct binder_buffer *buffer; void __user *uptr; uptr = (void __user *)user_ptr; // 搜尋目標 Proc 的黑樹 while (n) { buffer = rb_entry(n, struct binder_buffer, rb_node); BUG_ON(buffer->free); if (uptr < buffer->user_data) n = n->rb_left; else if (uptr > buffer->user_data) n = n->rb_right; else { // 判斷該 Buffer 是否已經被釋放 if (!buffer->allow_user_free) return ERR_PTR(-EPERM); buffer->allow_user_free = 0; return buffer; } } return NULL; } ``` ## binder_ioctl 概述 如果要對 Binder 讀寫的話都要執行 binder_ioctl 函數,Framework(Native) 層會以 IPCThreadState 類來呼叫 binder 驅動 ```c= // IPCThreadState.cpp status_t IPCThreadState::talkWithDriver(bool doReceive) // doReceive = true { ... 省略部份 status_t err; do { ... log 訊息 #if defined(__ANDROID__) // 呼叫 Kernel#binder_ioctl 函數 if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0) err = NO_ERROR; else err = -errno; ... } while (err == -EINTR); return err; } ``` * IPCThreadState 透過 ioctl 呼叫 Kernel#binder_ioctl 函數,這裡接收的參數就是呼叫者進程的訊息,參數是相互對應 | User space 參數 | BinderDriver 參數 | 說明 | | -------- | -------- | -------- | | `mProcess->mDriverFD` | \*filp | 呼叫者進程在 binder 文件的 FD | | `BINDER_WRITE_READ` | cmd | 對 BinderDriver 的控制協議命令 | | `&bwr` | arg | 其結構是 `binder_write_read` | ```cpp= // binder.c static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int ret; // 從 Binder 文件中取出 proc 訊息 struct binder_proc *proc = filp->private_data; struct binder_thread *thread; unsigned int size = _IOC_SIZE(cmd); void __user *ubuf = (void __user *)arg; ... 省略部份 // 取得進入 Kernel space 的 proc thread thread = binder_get_thread(proc); if (thread == NULL) { ret = -ENOMEM; goto err; }; switch (cmd) { case BINDER_WRITE_READ: // @ 分析 binder_ioctl_write_read ret = binder_ioctl_write_read(filp, cmd, arg, thread); if (ret) goto err; break; ... 省略其他 case } ... 省略錯誤處理 return ret; } ``` 目前進入 Kernel Space 後,帶入的命令如下圖,首先處理第一個命令 `BINDER_WRITE_READ` (對 binder 的控制協議命令);另外這裡的命令是層層疊加的,如下圖所示 > ![](https://i.imgur.com/bvDIn4V.png) ### 讀、寫 Binder 驅動 - binder_ioctl_write_read * 讀取、寫入 Binder 驅動主要的指令是 `BINDER_WRITE_READ`,在這之中會在針對 Read、Write 處理做不同的處理 | ioctl 行為 | BinderDriver 處理函數 | 含意 | | -------- | -------- | - | | read | binder_thread_read | User 有資料要傳給 BinderDriver | | write | binder_thread_write | BinderDriver 有資料要傳給 User | ```cpp= // binder.c static int binder_ioctl_write_read(struct file *filp, unsigned int cmd, unsigned long arg, struct binder_thread *thread) { int ret = 0; struct binder_proc *proc = filp->private_data; unsigned int size = _IOC_SIZE(cmd); void __user *ubuf = (void __user *)arg; struct binder_write_read bwr; ... 省略部份 // 將使用者空間數據 (ubuf) 複製到 bwr 結構中 if (copy_from_user(&bwr, ubuf, sizeof(bwr))) { ret = -EFAULT; goto out; } // User 有資料要傳給 BinderDriver if (bwr.write_size > 0) { ret = binder_thread_write(proc, thread, bwr.write_buffer, // 目前內部指令是 BC_ENTER_LOOPER bwr.write_size, &bwr.write_consumed); ... 省略錯誤處理 } // BinderDriver 有資料要傳給 User if (bwr.read_size > 0) { ret = binder_thread_read(proc, thread, bwr.read_buffer, bwr.read_size, &bwr.read_consumed, filp->f_flags & O_NONBLOCK); ... 省略錯誤處理 } ... debug 訊息 // 將內核空間數據 (bwr) 複製到 Caller 使用者空間(ubuf) if (copy_to_user(ubuf, &bwr, sizeof(bwr))) { ret = -EFAULT; goto out; } out: return ret; } ``` 流程圖 > ![](https://i.imgur.com/dLjM9cM.png) ### Binder 處理 User 寫入 - binder_thread_write * 前面已經介紹過如何進入 Kernel space 與 [**binder**](https://elixir.bootlin.com/linux/latest/source/drivers/android/binder.c) 通訊,這裡重點介紹 `binder_thread_write` 函數在 Kernel space 的運作流程、重點 :::success - 當然前提是 User 有在 `binder_write_read` 結構中 寫入資料; **當前寫入的是 `BC_TRANSACTION` 命令** > ![](https://i.imgur.com/zsx4s88.png) ::: ```c= // binder.c static int binder_thread_write( struct binder_proc *proc, // 調用者 - 進程 struct binder_thread *thread, // 調用者 - 線程 binder_uintptr_t binder_buffer, // bwr.write_buffer size_t size, // bwr.write_size binder_size_t *consumed) // bwr.write_consumed { uint32_t cmd; // buffer = cmd + 對應數據 // 取得調用者的 buffer address void __user *buffer = (void __user *)(uintptr_t)binder_buffer; // 1. 設定開頭、結尾的指針 void __user *ptr = buffer + *consumed; // 跳過已處理過得部份,取得開始部份 void __user *end = buffer + size; // 指向 buffer 結尾 while (ptr < end && thread->return_error == BR_OK) { // 2. 獲取 cmd if (get_user_preempt_disabled(cmd, (uint32_t __user *)ptr)) return -EFAULT; // 取出指令後移動指針,讓指針移動到數據開頭 ptr += sizeof(uint32_t); // 3. 統計數據 if (_IOC_NR(cmd) < ARRAY_SIZE(binder_stats.bc)) { binder_stats.bc[_IOC_NR(cmd)]++; proc->stats.bc[_IOC_NR(cmd)]++; thread->stats.bc[_IOC_NR(cmd)]++; } // 判斷 cmd switch (cmd) { ... case BC_TRANSACTION: case BC_REPLY: { struct binder_transaction_data tr; // 4. 從 UserSpace 獲取數據存到 tr 結構 (User 預計給 BinderDriver 的資料) if (copy_from_user(&tr, ptr, sizeof(tr))) return -EFAULT; // 移動指針到下一個指令 ptr += sizeof(tr); // 5. 接著繼續分析 binder_transaction() 執行具體命令 // @ 追蹤 binder_transaction binder_transaction(proc, thread, &tr, cmd == BC_REPLY); break; } } } } ``` 1. 這裡重點是處理 User 傳送的 `BC_TRANSACTION` 命令 2. 將 Caller 的 `binder_write_read` 結構複製到 Kernel space (BinderDriver) 中的 `binder_transaction_data` 結構中 > ![](https://i.imgur.com/Yzs1Vu4.png) ## Binder 內部傳送資料 - binder_transaction :::danger 目前是以 **Caller 的角度** 來看 BinderDriver ::: 這裡會分為兩個階段分析 | 函數 | 功能 | 補充 | | -------- | -------- | - | | binder_transaction | 處要要傳送的資料 (`binder_transaction`) | 同時負責 `reply` & `transact` 功能 | | binder_proc_transaction | 將處理好的資料傳給目標進程 | | [**binder**](https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/drivers/android/binder.c)#binder_transaction 函數:該函數同時負責 `reply` & `transact` 功能,而目前是 `BC_TRANSACTION`,所以關注 transact 功能,由於該函數較長以下會分階段說明 ### Binder 傳送 - 找到目標 binder_node 1. **判斷 ==handle 標示==,獲取 `binder_node`**:透過 handle 標示,**取得對應的目標 `binder_node`、`target_proc` 數據結構**,如果找不到目標 `binder_node` 則會錯誤 > `binder_node` 簡單來說就是 Kernel 中「代表一個 Binder 實體物件(通常是 Server 端 BBinder)」的節點 :::info * ServiceManager 的 handle 數值就固定是 0(就像是 DNS 的概念) * 這邊取的 `binder_node` 在 `/dev/binder` 文件中的 Server 節點,要傳遞給 Client 的也是這個節點訊息 ::: ```c= // binder.c static void binder_transaction(struct binder_proc *proc, // 呼叫進程 proc struct binder_thread *thread, // 呼叫進程 thread struct binder_transaction_data *tr, // 數據 int reply, // 目前為 0 binder_size_t extra_buffers_size) { int ret; // 要傳輸的資料 struct binder_transaction *t; struct binder_work *w; struct binder_work *tcomplete; ... 省略部份 // 目標 proc, thread struct binder_proc *target_proc = NULL; struct binder_thread *target_thread = NULL; // 目標 binder 節點 struct binder_node *target_node = NULL; // 回覆的數據 struct binder_transaction *in_reply_to = NULL; struct binder_transaction_log_entry *e; // log 訊息 ... 省略部份 // 當前進程的上下文 struct binder_context *context = proc->context; ... 省略部份 // 使用者空間 const void __user *user_buffer = (const void __user *) (uintptr_t)tr->data.ptr.buffer ... 省略 log 訊息 if (reply) { ... 先關注 transact 功能,BC_REPLY 才會進來這裡 } else { // handle 值代表 "控制碼" // ++只有 ServiceManager handle 為 0++ if (tr->target.handle) { // 進入到這裡代表並非 ServiceManager 服務 struct binder_ref *ref; // 鎖住當前 proc binder_proc_lock(proc); // 查看是否有符合 target.handle 的 binder_ref ref = binder_get_ref_olocked( proc, tr->target.handle, true ); if (ref) { // 透過 binder_ref 有辦法取得 binder_node target_node = binder_get_node_refs_for_txn( ref->node, &target_proc, &return_error); } else { ... 省略錯誤資訊 } // 解鎖 binder_proc_unlock(proc); } else { // 只有 ServiceManager 進入這 mutex_lock(&context->context_mgr_node_lock); // 不用尋找 Node // 直接指定 ServiceManager 進程的 node target_node = context->binder_context_mgr_node; // 取得對於 node 的 ref 引用 if (target_node) target_node = binder_get_node_refs_for_txn( target_node, &target_proc, &return_error); else return_error = BR_DEAD_REPLY; mutex_unlock(&context->context_mgr_node_lock); if (target_node && target_proc->pid == proc->pid) { // ServiceManager 呼叫自己 ... 省略 } } if (!target_node) { ... 找不到 binder_node goto err_dead_binder; } ... 後面接續分析 } } ``` :::info * **`binder_get_ref_olocked` 函數**:透過 Handle 搜尋 binder_ref ```c= // binder.c static struct binder_ref *binder_get_ref_olocked( struct binder_proc *proc // 當前 proc u32 desc, // tr->target.handle (控制碼) bool need_strong_ref) { struct rb_node *n = proc->refs_by_desc.rb_node; struct binder_ref *ref; while (n) { // 當前進程在 紅黑數的節點 ref = rb_entry(n, struct binder_ref, rb_node_desc); if (desc < ref->data.desc) { n = n->rb_left; } else if (desc > ref->data.desc) { n = n->rb_right; } else if (need_strong_ref && !ref->data.strong) { binder_user_error("tried to use weak ref as strong ref\n"); return NULL; } else { return ref; } } return NULL; } ``` ::: > ![](https://i.imgur.com/0cuSjTl.png) 2. **分配新的 `binder_transaction` 結構空間**:取得空的 `binder_work`、分配 `binder_transaction` 結構空間 ```c= // binder.c static void binder_transaction(struct binder_proc *proc, // 呼叫進程 proc struct binder_thread *thread, // 呼叫進程 thread struct binder_transaction_data *tr, // 數據 int reply, binder_size_t extra_buffers_size) { int ret; // 要傳輸的資料 struct binder_transaction *t; struct binder_work *w; struct binder_work *tcomplete; ... 省略部份 // 目標 proc, thread struct binder_proc *target_proc = NULL; struct binder_thread *target_thread = NULL; // 目標 binder 節點 struct binder_node *target_node = NULL; // 回覆的數據 struct binder_transaction *in_reply_to = NULL; struct binder_transaction_log_entry *e; // log 訊息 ... 省略部份 // 當前進程的上下文 struct binder_context *context = proc->context; ... 省略部份 // 使用者空間 const void __user *user_buffer = (const void __user *) (uintptr_t)tr->data.ptr.buffer ... 省略 log 訊息 // 取得一個空的 binder_work w = list_first_entry_or_null(&thread->todo, struct binder_work, entry); // 分配 binder_transaction 空間 t = kzalloc(sizeof(*t), GFP_KERNEL); if (t == NULL) { ...省略 goto err_alloc_t_failed; } ... } ``` > ![](https://i.imgur.com/B6LQeHh.png) ### Binder 傳送 - 找到目標 binder_thread 1. **嘗試鎖定 target thread**:主要在做 reply routing / call-chain 的對齊: 也就是「如果這個呼叫是嵌套的同步呼叫,盡量把新的交易送到同一條關聯鏈上的 thread」,避免亂跳 thread 造成死鎖/違反同步語意 ```c= // binder.c static void binder_transaction( struct binder_proc *proc, // 呼叫進程 proc struct binder_thread *thread, // 呼叫進程 thread struct binder_transaction_data *tr, // 數據 int reply, binder_size_t extra_buffers_size) { int ret; // 要傳輸的資料 struct binder_transaction *t; struct binder_work *w; struct binder_work *tcomplete; ... 省略部份 // 目標 proc, thread struct binder_proc *target_proc = NULL; struct binder_thread *target_thread = NULL; // 目標 binder 節點 struct binder_node *target_node = NULL; // 回覆的數據 struct binder_transaction *in_reply_to = NULL; struct binder_transaction_log_entry *e; // log 訊息 ... 省略部份 // 當前進程的上下文 struct binder_context *context = proc->context; ... 省略部份 // 使用者空間 const void __user *user_buffer = (const void __user *) (uintptr_t)tr->data.ptr.buffer ... 省略 log 訊息 // 查看呼叫者 thread 是否有 stack (任務相互依賴) // 在 target_proc 裡找「曾經和這個 thread 有關聯的對端 thread」 if (!(tr->flags & TF_ONE_WAY) && thread->transaction_stack) { struct binder_transaction *tmp; tmp = thread->transaction_stack; // 來源 thread stack if (tmp->to_thread != thread) { // 自己呼叫自己 goto err_bad_call_stack; } while (tmp) { struct binder_thread *from; spin_lock(&tmp->lock); from = tmp->from; if (from && from->proc == target_proc) { // 判斷來源 thread atomic_inc(&from->tmp_ref); target_thread = from; // 找到目標 thread spin_unlock(&tmp->lock); break; } spin_unlock(&tmp->lock); tmp = tmp->from_parent; } } ... } ``` :::info * 什麼時候會產生 `transaction_stack`? 通常發生在 Thread 正在處理一個同步來電(`BR_TRANSACTION`),並且還沒有回覆(`BC_REPLY`),或是巢狀同步呼叫 ```mermaid sequenceDiagram participant A as Client A (thread) participant K as Binder Driver participant B as Server B (binder thread) A->>K: BC_TRANSACTION (sync) K->>B: BR_TRANSACTION Note over B: push transaction to B.transaction_stack B->>K: BC_REPLY K->>A: BR_REPLY Note over B: pop transaction from B.transaction_stack ``` * 這裡可以看到單向傳遞 `TF_ONE_WAY` 是不用找到 Server 的最佳 thread,因為 thread 間的任務不會相互依賴 oneway 不需要維持 reply chain(因為沒有 reply),所以通常不需要沿 transaction_stack 找對應 thread > 但它仍然需要把 work 放進 target_proc(或 node 的 async queue)讓某個 thread 來處理 ::: > ![](https://i.imgur.com/Pey6DvA.png) 2. **分配 `binder_work` 空間**:分配一個 `tcomplete`,之後會封裝成一個 `BINDER_WORK_TRANSACTION_COMPLETE` 並添加到上一步找到的 User thread 的 todo 列表中(也就是 `thread->todo`) > 這樣使用者就可以知道該命令已傳送 > > 它的作用是讓 UserSpace 的 libbinder 在 `binder_thread_read()` 收到 `BR_TRANSACTION_COMPLETE` 之類事件,知道「driver 已經接手/排隊完成」,但不代表對方已處理 ```c= // binder.c static void binder_transaction( struct binder_proc *proc, // 呼叫進程 proc struct binder_thread *thread, // 呼叫進程 thread struct binder_transaction_data *tr, // 數據 int reply, binder_size_t extra_buffers_size) { int ret; // 要傳輸的資料 struct binder_transaction *t; struct binder_work *w; struct binder_work *tcomplete; ... 省略部份 // 目標 proc, thread struct binder_proc *target_proc = NULL; struct binder_thread *target_thread = NULL; // 目標 binder 節點 struct binder_node *target_node = NULL; // 回覆的數據 struct binder_transaction *in_reply_to = NULL; struct binder_transaction_log_entry *e; // log 訊息 ... 省略部份 // 當前進程的上下文 struct binder_context *context = proc->context; ... 省略部份 // 使用者空間 const void __user *user_buffer = (const void __user *) (uintptr_t)tr->data.ptr.buffer ... 省略 log 訊息 tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL); if (tcomplete == NULL) { ...省略 goto err_alloc_tcomplete_failed; } } ``` > ![](https://i.imgur.com/MHEd0cd.png) ### Binder 傳送 - 填充要傳遞的數據 1. **填充 `binder_transaction` 數據**:在上面步驟有創建出一個 binder_transaction,現在就在對其填充數據,其中包括 code(給目標 Service 的指令)、thread & proc 的來源紀錄、priority 優先性... 等等 :::info * `binder_transaction` 結構是 Binder 驅動獨用 ::: 後面也會將它 (t) 封裝成一個 `BINDER_WORK_TRANSACTION` 命令,並添加到目標 todo 列表 :::warning * 其中會對 `binder_transaction#buffer` 成員創建一個 `binder_buffer` 緩衝結構 > `binder_alloc_new_buf` 函數 * 並先將使用者 User space 的資料複製到 `binder_transaction#buffer` 中 > `binder_alloc_copy_user_to_buffer` 函數 注意:分配的空間是在 ServiceManager(或真正服務的進程)中分配 ! ::: ```c= // binder.c static void binder_transaction( struct binder_proc *proc, // 呼叫進程 proc struct binder_thread *thread, // 呼叫進程 thread struct binder_transaction_data *tr, // 數據 int reply, binder_size_t extra_buffers_size) { int ret; // 要傳輸的資料 struct binder_transaction *t; struct binder_work *w; struct binder_work *tcomplete; ... 省略部份 // 目標 proc, thread struct binder_proc *target_proc = NULL; struct binder_thread *target_thread = NULL; // 目標 binder 節點 struct binder_node *target_node = NULL; // 回覆的數據 struct binder_transaction *in_reply_to = NULL; struct binder_transaction_log_entry *e; // log 訊息 ... 省略部份 // 當前進程的上下文 struct binder_context *context = proc->context; ... 省略部份 // 使用者空間 const void __user *user_buffer = (const void __user *) (uintptr_t)tr->data.ptr.buffer ... 省略 debug, log 訊息 if (!reply && !(tr->flags & TF_ONE_WAY)) t->from = thread; // 紀錄發送者 thread 到 binder_transaction#from else t->from = NULL; // 紀錄各種訊息到 binder_transaction t->sender_euid = task_euid(proc->tsk); t->to_proc = target_proc; t->to_thread = target_thread; t->code = tr->code; // 給目標進的的命令 (code) t->flags = tr->flags; t->priority = task_nice(current); ... 省略 security // buffer: 為了完成這次 transaction 所申請的內存, // 創建一塊 buffer 指向 binder_transaction#buffer // 注意:分配的空間是在 ServiceManager(或真正服務的進程)中分配 ! t->buffer = binder_alloc_new_buf(&target_proc->alloc, tr->data_size, tr->offsets_size, extra_buffers_size, !reply && (t->flags & TF_ONE_WAY), current->tgid); ... 省略 buffer 錯誤、security // 添加 buffer 訊息 t->buffer->debug_id = t->debug_id; t->buffer->transaction = t; t->buffer->target_node = target_node; t->buffer->clear_on_free = !!(t->flags & TF_CLEAR_BUF); // 將 User space 資料複製到 buffer if (binder_alloc_copy_user_to_buffer( &target_proc->alloc, t->buffer, ALIGN(tr->data_size, sizeof(void *)), (const void __user *) (uintptr_t)tr->data.ptr.offsets, tr->offsets_size)) { ... 省略錯誤 goto err_copy_data_failed; } ... 省略對齊檢查 IS_ALIGNED ... } ``` > ![](https://i.imgur.com/fYoIUPr.png) 對應 Binder 通訊模型:就是先在 KernelSpace 分配 binder_buffer 空間,再將使用者資料複製進 binder_buffer 空間 (下圖的內核緩衝區) 開闢的 `binder_buffer` 是屬於 ServiceManager 空間 > ![](https://i.imgur.com/cl0yGOs.png) ### Binder 傳送 - 處理 User 傳入的 BinderServer 1. **複製資料、物件**:for 迴圈處理 User 傳入的 Binder 物件資料;這邊假設是 ServiceManager#addService 的話,就是 **Binder 驅動第一次接觸到該服務,那它就會創建一個 `binder_node` 與之對應** 1. 將 binder_transaction#buf 資料複製到目標 proc (目前是 ServiceManager) 的 alloc 區塊 2. 取得 User 傳入的資料 (目前是 Service 的相關數據) 並存入 `binder_object` 3. 針對 `binder_object` 類型轉換 binder 資料 ```c= // binder.c static void binder_transaction( struct binder_proc *proc, // 呼叫進程 proc struct binder_thread *thread, // 呼叫進程 thread struct binder_transaction_data *tr, // 數據 int reply, binder_size_t extra_buffers_size) { int ret; // 要傳輸的資料 struct binder_transaction *t; struct binder_work *w; struct binder_work *tcomplete; ... 省略部份 // 目標 proc, thread struct binder_proc *target_proc = NULL; struct binder_thread *target_thread = NULL; // 目標 binder 節點 struct binder_node *target_node = NULL; // 回覆的數據 struct binder_transaction *in_reply_to = NULL; struct binder_transaction_log_entry *e; // log 訊息 ... 省略部份 // 當前進程的上下文 struct binder_context *context = proc->context; ... 省略部份 // 使用者空間 const void __user *user_buffer = (const void __user *) (uintptr_t)tr->data.ptr.buffer ... 省略 log 訊息 // 簡單來說這個 for 迴圈的功能是 // 將使用者傳入的 BBinder (存在 Parcel 中的 cookie 服務) 存下來 for (buffer_offset = off_start_offset; buffer_offset < off_end_offset; buffer_offset += sizeof(binder_size_t)) { struct binder_object_header *hdr; size_t object_size; // 創建 binder_object 結構,可以先看成 flat_binder_object struct binder_object object; binder_size_t object_offset; binder_size_t copy_size; // 1. 從 buffer 緩存空間複製數據到目標 alloc 空間 if (binder_alloc_copy_from_buffer(&target_proc->alloc, &object_offset, t->buffer, buffer_offset, sizeof(object_offset))) { ... 錯誤處理 goto err_bad_offset; } ... copy size 檢查 // 2. 取得 User 傳入的資料並存入 `binder_object` object_size = binder_get_object(target_proc, user_buffer, t->buffer, object_offset, &object); if (object_size == 0 || object_offset < off_min) { ... 錯誤處理 goto err_bad_offset; } // 為下一個物件設定篇一量(因為當前 Service 已佔有一定的空間) user_offset = object_offset + object_size; hdr = &object.hdr; off_min = object_offset + object_size; // 3. 查看 Binder 物件類型,依照物件類型處理 switch (hdr->type) { // 使用 ServiceManager 添加服務時,大多是 BINDER_TYPE_BINDER 類型 case BINDER_TYPE_BINDER: case BINDER_TYPE_WEAK_BINDER: { struct flat_binder_object *fp; // 找到目標 Service 在資料中的偏移 fp = to_flat_binder_object(hdr); // BINDER_TYPE_BINDER 轉 HANDLE // @ 追蹤 binder_translate_binder 函數 ret = binder_translate_binder(fp, t, thread); if (ret < 0 || // 將 fp 資料複製進 buffer binder_alloc_copy_to_buffer(&target_proc->alloc, t->buffer, // 複製進這 object_offset, fp, sizeof(*fp))) { ... 錯誤處理 goto err_translate_failed; } } break; case BINDER_TYPE_HANDLE: case BINDER_TYPE_WEAK_HANDLE: { struct flat_binder_object *fp; fp = to_flat_binder_object(hdr); ret = binder_translate_handle(fp, t, thread); if (ret < 0 || binder_alloc_copy_to_buffer(&target_proc->alloc, t->buffer, object_offset, fp, sizeof(*fp))) { ... 錯誤處理 goto err_translate_failed; } } break; ... 省略部份 case default: ... 錯誤訊息 goto err_bad_object_type; } } // 完成 obj 對象處理,複製緩衝區的其餘部分複製到 buffer // 前面處理 object 時,驅動會「只 copy 物件 header/內容並改寫」,剩下沒碰的 payload 才一次 copy 補齊 if (binder_alloc_copy_user_to_buffer( &target_proc->alloc, t->buffer, user_offset, user_buffer + user_offset, tr->data_size - user_offset)) { ... 複製資料失敗 goto err_copy_data_failed; } ... } ``` :::info * **傳遞 IBinder 對象是真的物件 ? NO !** 1. 當 Server 把 Binder 實體傳給 Client 時,`flat_binder_object` 的 type 是 `BINDER_TYPE_BINDER`(代表該服務在自身進程中,**在自身進程中的地址是虛擬地址,對 目標進程 (ServiceManager) 是完全無效的!**) > handler 是目標進程的 `binder_ref` 紅黑樹上的數值,而不是具體服務 `binder_node` 上的數值 > 舉例:A 進程要取得 B 服務,所以訪問 ServiceManager 後,A 進程取得的 Handler 數值就是 ServiceManager 中的 `binder_ref` 的數值 2. 必須透過 kernel 把 type 轉為 `BINDER_TYPE_HANDLE`,為 目標進程 (ServiceManager) 創建內核引用 `binder_ref`,並將引用存入 handle 中 (控制碼) > 發起方 Caller 呼叫時會判斷並做出這個轉換 3. 最終將 handle (控制碼) 填入 `binder_transaction_data` 中 ```c= // binder.c static int binder_translate_binder( struct flat_binder_object *fp, struct binder_transaction *t, struct binder_thread *thread) { struct binder_node *node; struct binder_proc *proc = thread->proc; struct binder_proc *target_proc = t->to_proc; struct binder_ref_data rdata; int ret = 0; // 取得進程中的 node 節點 node = binder_get_node(proc, fp->binder); if (!node) { // 如果沒有則創建 node 節點 // 以照目前的狀況來說就是找不到 node,就會創建一個 node node = binder_new_node(proc, fp); if (!node) return -ENOMEM; } ... 省略部份 // 將對應的 binder_ref 加入 binder_proc // 並增加 ref 計數 ret = binder_inc_ref_for_node(target_proc, node, fp->hdr.type == BINDER_TYPE_BINDER, &thread->todo, &rdata); if (ret) goto done; // BINDER_TYPE_BINDER 轉 HANDLE 並存入 type if (fp->hdr.type == BINDER_TYPE_BINDER) // 描述改為遠端 Service(轉換處) fp->hdr.type = BINDER_TYPE_HANDLE; else fp->hdr.type = BINDER_TYPE_WEAK_HANDLE; fp->binder = 0; fp->handle = rdata.desc; fp->cookie = 0; ... done: binder_put_node(node); return ret; } ``` ::: 2. 調整 `binder_transaction`#work#type 為 `BINDER_WORK_TRANSACTION` ```c= // binder.c static void binder_transaction( struct binder_proc *proc, // 呼叫進程 proc struct binder_thread *thread, // 呼叫進程 thread struct binder_transaction_data *tr, // 數據 int reply, binder_size_t extra_buffers_size) { int ret; // 要傳輸的資料 struct binder_transaction *t; struct binder_work *w; struct binder_work *tcomplete; ... // 設定 tcomplete 的 work.type (要給 User 的 BinderThread 處理) if (t->buffer->oneway_spam_suspect) tcomplete->type = BINDER_WORK_TRANSACTION_ONEWAY_SPAM_SUSPECT; else tcomplete->type = BINDER_WORK_TRANSACTION_COMPLETE; // 在 ServiceManager 接收後,binder_thread_read 時判斷 t->work.type = BINDER_WORK_TRANSACTION; } ``` > ![](https://i.imgur.com/HrNPCvE.png) ### Binder 傳送 - 添加任務到目標 & 喚醒 1. **傳送 binder 資料到目標 proc**:**重點是 `binder_proc_transaction` 函數**(之後分析) ```c= // binder.c static void binder_transaction( struct binder_proc *proc, // 呼叫進程 proc struct binder_thread *thread, // 呼叫進程 thread struct binder_transaction_data *tr, // 數據 int reply, binder_size_t extra_buffers_size) { int ret; // 要傳輸的資料 struct binder_transaction *t; struct binder_work *w; struct binder_work *tcomplete; ... 省略部份 // 目標 proc, thread struct binder_proc *target_proc = NULL; struct binder_thread *target_thread = NULL; // 目標 binder 節點 struct binder_node *target_node = NULL; // 回覆的數據 struct binder_transaction *in_reply_to = NULL; struct binder_transaction_log_entry *e; // log 訊息 ... 省略部份 // 當前進程的上下文 struct binder_context *context = proc->context; ... 省略部份 // 使用者空間 const void __user *user_buffer = (const void __user *) (uintptr_t)tr->data.ptr.buffer ... 省略 log 訊息 if (reply) { ... 先關注 transact 功能 } else if (!(t->flags & TF_ONE_WAY)) { // 並非單向寫入 ... // 設定必須 reply 為 1 t->need_reply = 1; t->from_parent = thread->transaction_stack; // 設定定來 thread stack // 將交易 t 存到來源 thread (就是 User 的 thread) thread->transaction_stack = t; binder_inner_proc_unlock(proc); // @ 追蹤 binder_proc_transaction 函數 return_error = binder_proc_transaction(t, target_proc, target_thread); if (return_error) { ... 錯誤處理 goto err_dead_proc_or_thread; } } else { // 單向 binder_enqueue_thread_work(thread, tcomplete); // @ 追蹤 binder_proc_transaction 函數 return_error = binder_proc_transaction(t, target_proc, NULL); if (return_error) goto err_dead_proc_or_thread; } ... } ``` > ![](https://i.imgur.com/tsh6VRK.png) * 分析 [**binder**](https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/drivers/android/binder.c)#**binder_proc_transaction** 函數:前面的過程可以算是整理要傳輸的資料,**到 `binder_proc_transaction` 才是真正傳輸資料的部份** ```c= // binder.c static int binder_proc_transaction( struct binder_transaction *t, // 要傳輸的數據 struct binder_proc *proc, // 目標 proc struct binder_thread *thread) // 目標 thread { struct binder_node *node = t->buffer->target_node; bool oneway = !!(t->flags & TF_ONE_WAY); bool pending_async = false; binder_node_lock(node); // 鎖住目標 binder_node if (oneway) { if (node->has_async_transaction) pending_async = true; else node->has_async_transaction = true; } binder_inner_proc_lock(proc); // 鎖住目標 proc // 將任務串接到目標 if (thread) { binder_transaction_priority(thread, t, node); // 寶任務塞到目標執行緒的 todo 列表 binder_enqueue_thread_work_ilocked(thread, &t->work); } else if (!pending_async) { binder_enqueue_work_ilocked(&t->work, &proc->todo); } else { ... binder_enqueue_work_ilocked(&t->work, &node->async_todo); } ... 省略部份 if (!pending_async) // @ 追蹤 binder_wakeup_thread_ilocked 函數 binder_wakeup_thread_ilocked(proc, thread, !oneway /* sync */); proc->outstanding_txns++; // 解除 node, proc 的鎖 binder_inner_proc_unlock(proc); binder_node_unlock(node); return 0; } // onway = sync // onway true 的話 sync true static void binder_wakeup_thread_ilocked( struct binder_proc *proc, struct binder_thread *thread, bool sync) { assert_spin_locked(&proc->inner_lock); // 喚醒目標 thread if (thread) { trace_android_vh_binder_wakeup_ilocked(thread->task, sync, proc); // 喚醒 thread->wait,把正在等待事做的 Thread 叫醒 if (sync) // 同步交易有立刻喚醒、減少延遲的特性 wake_up_interruptible_sync(&thread->wait); else // 異步交易,屬於ㄧ般喚醒 wake_up_interruptible(&thread->wait); return; } ... } ``` 2. **移除臨時結構**:這段「移除暫時 `binder_*`」其實是在… * 釋放臨時引用(`tmp_ref`),避免 leak * 也確保在這段 transaction 建立/排隊/喚醒過程中,目標物件不會被釋放造成 UAF(`Use-After-Free`,也就是以釋放的目標仍被使用) * 最後用 memory barrier 確保 log 記錄一致 ```c= // binder.c static void binder_transaction( struct binder_proc *proc, // 呼叫進程 proc struct binder_thread *thread, // 呼叫進程 thread struct binder_transaction_data *tr, // 數據 int reply, binder_size_t extra_buffers_size) { ... 省略部份 // 移除暫時 binder_thread if (target_thread) binder_thread_dec_tmpref(target_thread); // 移除暫時 binder_proc binder_proc_dec_tmpref(target_proc); // 移除暫時 binder_node if (target_node) binder_dec_node_tmpref(target_node); /* * write barrier to synchronize with initialization * of log entry */ smp_wmb(); WRITE_ONCE(e->debug_id_done, t_debug_id); return; ... 省略各種錯誤處理 tag } ``` > ![](https://i.imgur.com/yifFu0X.png) ## Binder 讀取資料 - binder_thread_read :::info 目前我們假設是 **ServiceManager 進程被 Caller 在 Binder 驅動中喚醒,然後以 ServiceManager 的角度處理 Binder 訊息** ::: 前面已經介紹過如何進入 KernelSpace 與 Binder 通訊,這裡重點介紹 `binder_thread_read 函數` 在 Kernel space 的運作流程、重點 :::success - 當然前提是 User 有在 `binder_write_read` 結構中 寫入資料; 當前寫入的是 `BC_TRANSACTION` 命令 > 下圖是 Caller 原先對於 Binder 驅動寫入的命令 > ![](https://i.imgur.com/zsx4s88.png) ::: ### Binder 讀取 - 判斷是否有任務 1. **判斷 `binder_thread` 是否有任務**: 如果 `binder_thread` 沒有任務則會進行休眠(**把 BinderThread 切換成 Waiting 狀態**),直到有任務才會被喚醒,這也就是為什麼我們上面說 Caller 在 binder write 到最後要把目標喚醒的原因 :::info * 像是 `ServiceManager` 剛被創建出來時就沒有任務,所以會進行休眠 ::: ```c= // binder.c static int binder_thread_read( struct binder_proc *proc, // Server 的 Proc struct binder_thread *thread, // Server 的 Thread binder_uintptr_t binder_buffer, // IPCThreadState 的 mIn 數據空間 size_t size, binder_size_t *consumed, int non_block) { // 使用者空間的指針 void __user *buffer = (void __user *)(uintptr_t)binder_buffer; // 取得開頭結尾指針 void __user *ptr = buffer + *consumed; // 數據的起點 void __user *end = buffer + size; // 數據的終點 int ret = 0; int wait_for_proc_work; ... retry: // 判斷是否有任務,若沒有任務則需要休眠 // eg. 像是 service_manager 剛開始就沒有寫,因為沒有任務 wait_for_proc_work = binder_available_for_proc_work_ilocked(thread); // 加上 thread Waiting 狀態 thread->looper |= BINDER_LOOPER_STATE_WAITING; ... trace if (wait_for_proc_work) { // 判斷 Binder thread 狀態... 是否已註冊、是否開始 if (!(thread->looper & (BINDER_LOOPER_STATE_REGISTERED | BINDER_LOOPER_STATE_ENTERED))) { ... 省略錯誤訊息 } ... } // 看看服務是否是堵塞;像是 ServiceManager 就是堵塞(休眠等待) if (non_block) { if (!binder_has_work(thread, wait_for_proc_work)) ret = -EAGAIN; } else { ret = binder_wait_for_work(thread, wait_for_proc_work); } // 移除 Thread Waiting 狀態 thread->looper &= ~BINDER_LOOPER_STATE_WAITING; if (ret) return ret; ... } ``` > ![](https://i.imgur.com/zRnULNw.png) ### Binder 讀取 - 取出任務 1. **取得任務 header**:進入 while 循環處理資料,判斷 `binder_thread`、`binder_proc` 的 todo 列表是否有任務 (先 thread 再 proc) :::info Binder 驅動會將 Client 的任務加入目標進程的 `todo` 列表;列表優先處理順序是 thread,之後才處理 proc ::: ```c= // binder.c static int binder_thread_read( struct binder_proc *proc, struct binder_thread *thread, binder_uintptr_t binder_buffer, // IPCThreadState 的 mIn 數據空間 size_t size, binder_size_t *consumed, int non_block) { // 使用者空間的指針 void __user *buffer = (void __user *)(uintptr_t)binder_buffer; // 取得開頭結尾指針 void __user *ptr = buffer + *consumed; // 數據的起點 void __user *end = buffer + size; // 數據的終點 int ret = 0; int wait_for_proc_work; ... while (1) { uint32_t cmd; // 創建 binder_transaction_data_secctx struct binder_transaction_data_secctx tr; // 取 tr#transaction_data struct binder_transaction_data *trd = &tr.transaction_data; // 當前需要執行的 binder_work struct binder_work *w = NULL; // 任務 header struct list_head *list = NULL; // 要對外傳輸的訊息 struct binder_transaction *t = NULL; struct binder_thread *t_from; size_t trsize = sizeof(*trd); // 判斷 thread#todo 列表是否為空 if (!binder_worklist_empty_ilocked(&thread->todo)) // 在 binder_thread_write 中,有把 binder_work 添加到目標 Server 的 thread->todo 列表中 // 所以這裡會讀取到數據,並不為空 list = &thread->todo; // 判斷 proc#todo 列表 else if (!binder_worklist_empty_ilocked(&proc->todo) && wait_for_proc_work) list = &proc->todo; else { ... } ... } } ``` > ![](https://i.imgur.com/cs2bgMo.png) ### Binder 讀取 - 判斷任務類型 1. **判斷 binder_work#type**:根據判斷 `binder_work#type` 來決定要 Binder 驅動要回覆的 `BR_*` 命令 > * `BINDER_WORK_TRANSACTION` (Binder 驅動傳送數據,給目標進程 transaction) > * `BINDER_WORK_TRANSACTION_COMPLETE`(Binder 驅動處理完成的數據,回應給 Caller Thread,用來表示驅動已經處理完這次 Write) 這邊假設我們的 type 是 `BINDER_WORK_TRANSACTION`,也就是 Binder 驅動要求交易 :::warning * `BINDER_WORK_TRANSACTION` 並不是 Caller 發送給 Binder 驅動的命令,他是 Binder 驅動內部的工作類型,表示的是「**有一筆 transaction 要交給目標進程處理**」 ✅ 一筆 transaction ⇒ 通常一個 BINDER_WORK_TRANSACTION(入隊一次) ❌ 不是「找到 proc/thread 就各發一次」 ::: ```c= // binder.c static int binder_thread_read( struct binder_proc *proc, struct binder_thread *thread, binder_uintptr_t binder_buffer, // IPCThreadState 的 mIn 數據空間 size_t size, binder_size_t *consumed, int non_block) { // 使用者空間的指針 void __user *buffer = (void __user *)(uintptr_t)binder_buffer; // 取得開頭結尾指針 void __user *ptr = buffer + *consumed; // 數據的起點 void __user *end = buffer + size; // 數據的終點 int ret = 0; int wait_for_proc_work; ... while (1) { uint32_t cmd; // 創建 binder_transaction_data_secctx struct binder_transaction_data_secctx tr; // 取 tr#transaction_data struct binder_transaction_data *trd = &tr.transaction_data; // 當前需要執行的 binder_work struct binder_work *w = NULL; // 任務 header struct list_head *list = NULL; // 要對外傳輸的訊息 struct binder_transaction *t = NULL; struct binder_thread *t_from; size_t trsize = sizeof(*trd); ... // 判斷 binder_work#type switch (w->type) { case BINDER_WORK_TRANSACTION: { binder_inner_proc_unlock(proc); // 從 binder_work 中取得 binder_transaction // 會組出 BR_TRANSACTION(或 BR_REPLY)+ binder_transaction_data 寫回 userspace // 讓目標 thread 回到 userspace 去執行 onTransact() t = container_of(w, struct binder_transaction, work); } break; // Binder 驅動在完成 BINDER_WORK_TRANSACTION 後,會回應給 Caller BINDER_WORK_TRANSACTION_COMPLETE case BINDER_WORK_TRANSACTION_COMPLETE: case BINDER_WORK_TRANSACTION_ONEWAY_SPAM_SUSPECT: { // 設定 將要回覆的指令 cmd if (proc->oneway_spam_detection_enabled && w->type == BINDER_WORK_TRANSACTION_ONEWAY_SPAM_SUSPECT) cmd = BR_ONEWAY_SPAM_SUSPECT; else cmd = BR_TRANSACTION_COMPLETE; // 釋放 binder_work kfree(w); ... if (put_user(cmd, (uint32_t __user *)ptr)) return -EFAULT; // 移動該指標,跳過指令 ptr += sizeof(uint32_t); ... } break; // 處理 node 強/弱引用狀態變化 // 像是透過 ServiceManager 註冊 Server 服務時會調用 case BINDER_WORK_NODE: { ... int has_weak_ref; int has_strong_ref; void __user *orig_ptr = ptr; // Server 的計數 // 強計數 strong = node->internal_strong_refs || node->local_strong_refs; // 弱計數 weak = !hlist_empty(&node->refs) || node->local_weak_refs || node->tmp_refs || strong; // 是否有強引用 has_strong_ref = node->has_strong_ref; // 是否有弱引用 has_weak_ref = node->has_weak_ref; // 有弱引用,但沒有 ref if (weak && !has_weak_ref) { node->has_weak_ref = 1; // 設為 1 node->pending_weak_ref = 1; node->local_weak_refs++; } // 有強引用,但沒有 ref if (strong && !has_strong_ref) { node->has_strong_ref = 1; // 設為 1 node->pending_strong_ref = 1; node->local_strong_refs++; } if (!strong && has_strong_ref) node->has_strong_ref = 0; // 設為 0 (清除) if (!weak && has_weak_ref) node->has_weak_ref = 0; // 設為 0 (清除) if (!weak && !strong) { ... 釋放 server node } else { ... } if (weak && !has_weak_ref) // @ 查看 binder_put_node_cmd ret = binder_put_node_cmd( proc, thread, &ptr, node_ptr, node_cookie, node_debug_id, BR_INCREFS, "BR_INCREFS"); ... 省略部分 if (ret) return ret; } break; } } } ``` > ![](https://i.imgur.com/KeojQBJ.png) ### Binder 讀取 - 接收 Node > 以下的解釋語境是在「對 service manager 添加服務」 1. **判斷 binder_node**:從接收到的資料 t#buffer 中取得 `target_node` 此處 `target_node` 是「這筆 transaction 的目標本地 binder 實體」;若交易是呼叫 ServiceManager 的 context manager 介面,`target_node` 會是 `binder_context_mgr_node` ```c= // binder.c static int binder_thread_read( struct binder_proc *proc, struct binder_thread *thread, binder_uintptr_t binder_buffer, // IPCThreadState 的 mIn 數據空間 size_t size, binder_size_t *consumed, int non_block) { // 使用者空間的指針 void __user *buffer = (void __user *)(uintptr_t)binder_buffer; // 取得開頭結尾指針 void __user *ptr = buffer + *consumed; // 數據的起點 void __user *end = buffer + size; // 數據的終點 int ret = 0; int wait_for_proc_work; ... while (1) { uint32_t cmd; // 創建 binder_transaction_data_secctx struct binder_transaction_data_secctx tr; // 取 tr#transaction_data // Binder 驅動準備寫回 ServiceManager UserSpace Buffer 的數據 struct binder_transaction_data *trd = &tr.transaction_data; // 當前需要執行的 binder_work struct binder_work *w = NULL; // 任務 header struct list_head *list = NULL; // 別人要傳給 ServiceManager 的訊息 struct binder_transaction *t = NULL; struct binder_thread *t_from; size_t trsize = sizeof(*trd); ... // 從接收到的資料 t#buffer 中取得 target_node if (t->buffer->target_node) { struct binder_node *target_node = t->buffer->target_node; // 把目標 target_node 放到 service manager buffer trd->target.ptr = target_node->ptr; trd->cookie = target_node->cookie; binder_transaction_priority(thread, t, target_node); // 切換 cmd 改為 BR_TRANSACTION (Binder 驅動之後要回傳) cmd = BR_TRANSACTION; } else { ...無法取得 binder_node } ... } } ``` > ![](https://i.imgur.com/MZgg1s5.png) ### Binder 讀取 - 將資料複製給目標 Thread 1. **填充剩餘的 `binder_transaction_data` 數據**:其中就包括要傳遞給 Server 的命令(code) > 當前的目標是 ServiceManager > code 是 `ADD_SERVICE_TRANSACTION` ```c= // binder.c static int binder_thread_read( struct binder_proc *proc, struct binder_thread *thread, binder_uintptr_t binder_buffer, // IPCThreadState 的 mIn 數據空間 size_t size, binder_size_t *consumed, int non_block) { // 使用者空間的指針 void __user *buffer = (void __user *)(uintptr_t)binder_buffer; // 取得開頭結尾指針 void __user *ptr = buffer + *consumed; // 數據的起點 void __user *end = buffer + size; // 數據的終點 int ret = 0; int wait_for_proc_work; ... while (1) { uint32_t cmd; struct binder_transaction_data_secctx tr; struct binder_transaction_data *trd = &tr.transaction_data; struct binder_work *w = NULL; struct list_head *list = NULL; struct binder_transaction *t = NULL; struct binder_thread *t_from; size_t trsize = sizeof(*trd); ... // Caller 給 Server 的指令(Caller 就是 t) trd->code = t->code; trd->flags = t->flags; // 來源 ID,可做安全性檢查 trd->sender_euid = from_kuid(current_user_ns(), t->sender_euid); t_from = binder_get_txn_from(t); if (t_from) { struct task_struct *sender = t_from->proc->tsk; trd->sender_pid = task_tgid_nr_ns(sender, task_active_pid_ns(current)); ... trace } else { trd->sender_pid = 0; } ... 省略部份 // 將要傳輸的大小指向 t 數據區域大小 trd->data_size = t->buffer->data_size; // Offset 區域大小 trd->offsets_size = t->buffer->offsets_size; // 將要傳輸的數據指向 t 數據區域 trd->data.ptr.buffer = (uintptr_t)t->buffer->user_data; // 將要傳輸的偏移指向 t Offset 區域 trd->data.ptr.offsets = trd->data.ptr.buffer + ALIGN(t->buffer->data_size, sizeof(void *)); tr.secctx = t->security_ctx; if (t->security_ctx) { cmd = BR_TRANSACTION_SEC_CTX; trsize = sizeof(tr); } // 將指令 cmd 複製到 SM 的 UserSpace 中 // cmd 為 BR_TRANSACTION if (put_user(cmd, (uint32_t __user *)ptr)) { ... 失敗處理 return -EFAULT; } ptr += sizeof(uint32_t); // 將指令 `binder_transaction_data`(tr) 複製到 SM 的 UserSpace // (ServiceManager 的 BinderThread) 中 if (copy_to_user(ptr, &tr, trsize)) { ... 複製失敗 return -EFAULT; } ptr += trsize; ... 省略部份 // 設定 buffer 為可以用 t->buffer->allow_user_free = 1; if (cmd != BR_REPLY && !(t->flags & TF_ONE_WAY)) { binder_inner_proc_lock(thread->proc); // 挑選最佳處理的 thread t->to_parent = thread->transaction_stack; t->to_thread = thread; thread->transaction_stack = t; binder_inner_proc_unlock(thread->proc); } else { // @ 追蹤 binder_free_transaction 函數 binder_free_transaction(t); } break; } done: ... 省略部份 return 0; } ``` ```mermaid sequenceDiagram participant Caller participant Driver as Binder Driver participant SM as ServiceManager Caller->>Driver: BC_TRANSACTION Note over Driver: 建立 binder_transaction<br>translate objects<br>分配 buffer Driver-->>Caller: BR_TRANSACTION_COMPLETE Note over Caller: 表示 driver 已接受這筆 transaction Driver->>SM: BINDER_WORK_TRANSACTION Note over SM: enqueue 到 thread->todo Driver-->>SM: BR_TRANSACTION Note over SM: ServiceManager binder thread 被喚醒<br>onTransact() SM->>Driver: BC_REPLY Note over SM: 回傳結果 Driver-->>Caller: BR_REPLY Note over Caller: 收到 reply ``` > ![](https://i.imgur.com/vXdz3ME.png) * **binder_free_transaction 函數**:處理 `binder_transaction` 資料(處理完畢後也會用 free 釋放),如果目標進程在休眠也會在這個時候喚醒 > `binder_transaction` 結構是 KernalSpace 獨用 ```c= static void binder_free_transaction(struct binder_transaction *t) { struct binder_proc *target_proc = t->to_proc; if (target_proc) { ... 省略部份 // 如果 target_proc 在休眠則喚醒 target_proc if (!target_proc->outstanding_txns && target_proc->is_frozen) wake_up_interruptible_all(&target_proc->freeze_wait); if (t->buffer) t->buffer->transaction = NULL; binder_inner_proc_unlock(target_proc); } ... kfree(t); binder_stats_deleted(BINDER_STAT_TRANSACTION); } ``` > ![](https://i.imgur.com/L3tv3DD.png) :::info * 到這一步後 Binder 驅動的 Read 就處理完了 ::: ## Appendix & FAQ :::info ::: ###### tags: `Binder`

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully