站長阿川

站長阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

站長精心設計,帶你實作 63 個小專案,得到作品集!

立即開始免費試讀!

iOS 26 正式版即將發布,Flutter 完成全新 devicectl + lldb 的 Debug JIT 運行支持

image

在之前的 《Flutter 又雙叒叕可以在 iOS 26 的真機上 hotload》《Flutter 在 iOS 真機 Debug 運行出現 Timed out *** to update》 我們聊過,由於 iOS 26 開始,Apple 正式禁止了 Debug 時 mprotect 的 RX 權限,導致了 Flutter 在 Debug 運行到 iOS 26 真機時會有 mprotect failed: Permission denied 的問題。

在 iOS 上 Dart 不管是 JIT 運行還是進行 hotload 的時候,都需要涉及代碼在內存從 RW 變成 RWX 的調整,

為了快速解決這一問題,Flutter 官方之前臨時實現了一個過度方案:

  • 讓 Flutter 應用在需要執行 JIT 新代碼時,“暫停下來”(斷點),主動通知旁邊的調試器,讓調試器利用它的特權來幫忙把代碼設置為“可執行”,然後再繼續運行
  • 通過「雙地址映射」讓兩個地址指向一個內存,一個寫入,一個執行,然後利用 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 的斷點,讓 lldb 執行授權賦予 RX,做到在用一塊內存上實現 Debug 時具備 RWX 的效果

對詳細實現感興趣的可以看之前的 《Flutter 又雙叒叕可以在 iOS 26 的真機上 hotload》,而從臨時實現方案就可以看出,這一個非常 hack 補丁,並且這個方案預計會為每個代碼空間頁的分配增加約 500 毫秒的延遲,在加上實際工作中和 debugserver 還有等待 Xcode 建立調試會話的時間,讓 iOS 在 Debug 開發中十分容易出現 Timed out *** to update 等問題。

事實上針對這類問題蘋果也發現了“盲點”,特別還需要 Xcode 啟動配合等繁瑣操作,所以在 Xcode 16 增加了 devicectl 和 Xcode 的命令行調試器 lldb 協同工作的支持:

image

而針對這個問題,Flutter 在 Xcode 16 也終於實現了新的調整#173443,通過新的 devicectl + lldb 集成到 flutter run 命令來回歸 Apple 官方的 debug 體系:

  • 透過 devicectl 實現安裝啟動: devicectl 作為在 Xcode 15 中引入的控制工具,它主要負責將編譯好的應用包(.app)安裝到物理設備上,並負責啟動應用進程
    image
  • 透過 lldb 實現 JIT 和調試運行:作為 LLVM 項目的一部分,lldb 是 Apple 標準的底層調試器,在新架構中它將作為核心的調試傳輸層,負責附加到由 devicectl 啟動的應用進程,並建立起和 Dart VM 進行通信的橋樑

具體可以在 flutter_toolslldb.dart 看到,launchAppWithLLDBDebugger 啟動之後,就會執行 lldb 的 attachAndStart

image

而對於 attachAndStart,主要核心就有:

  • 啟動一個定時器,如果一分鐘內沒有成功,提示超時
  • 設置一個斷點 _setBreakpoint
  • 依附進程 _attachToAppProcess

那為什麼需要在執行 lldb 的時候通過 _setBreakpoint 添加一個斷點呢?實際上這就是在前面臨時方案基礎上的完善, _setBreakpoint 的主要目的就是:

  • 設置 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 作為 lldb 的斷點
  • 寫入一個 _pythonScript 腳本,當斷點觸發時,利用 lldb 的權限執行腳本,創建一個新的 rx 內存

image

關於 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 作為斷點我們在之前講過,它是 Dart VM 在 VirtualMemory::AllocateAligned 時,會通過 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 觸發,去讓 lldb 用它的權限申請執行:

image

而對於在 lldb 裡執行的 py 腳本,它主要是:

  • 當 Flutter 應用的 Dart VM 需要一塊新的內存用於 JIT 編譯時,調用這個名為 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 的函數,這個調用會觸發預設的斷點
  • 斷點觸發後,_pythonScript 的代碼立即被執行
    • 從寄存器 (x0, x1) 讀取 Dart VM 請求的內存地址和長度
    • 利用 lldb 的 WriteMemory 向該內存地址寫入數據,這個“寫入”動作是關鍵,它會強制 iOS 系統為這塊內存做好準備
  • 寫入一個 b'IHELPED!' 的“回執”信號,以便 Dart VM 確認操作已成功
  • 執行完畢後,它返回 False,告訴 lldb “任務完成,請立即讓應用繼續運行”

image

之後,通過 lldb device process attach --pid 的方式,讓進程被納入“開發者調試上下文”,從而支持 JIT 權限:

image

前面說起來比較抽象,具體可以理解為:

_attachToAppProcess 獲取“權限”:

因為系統的 W^X 安全策略,_attachToAppProcess 的核心作用就是利用 lldb 附加的特權,為整個應用進程解鎖了這個限制。

在這一步完成之後,應用進程的狀態從“不允許 JIT”變成了“理論上可以 JIT”,它獲得了讓內存頁變為可讀、可寫、可執行 (RWX) 的可能性

但是,僅僅有可能性是不夠的,因為 Dart VM 在運行時和 hotload 是動態地、按需地需要新的可執行內頁 page,它需要一個“機制”來實現,在需要的時候真正地去執行這個“將內存頁變為 RWX”的操作,而 App 本身的 Dart VM 本身沒有這個權限,所以它無法自己完成這個操作。

這時,它就像一個身處大樓內、知道自己需要打開一扇門,但自己手上沒有鑰匙的住戶。

_setBreakpoint 建立“通信與執行機制”

_setBreakpoint 的作用就是建立這個缺失的機制,類似於:

  • 建立通信渠道:Dart VM 被設計成在需要新內存頁時,會去調用 NOTIFY_DEBUGGER_ABOUT_RX_PAGES 函數,這就像住戶去按下一個特定的“求助”門鈴
  • 部署執行者_setBreakpoint 告訴 lldb “請一直監聽這個‘求助’門鈴 (NOTIFY_DEBUGGER_ABOUT_RX_PAGES)”,這就相當於雇用了一位管家,讓他守在門鈴旁邊
  • 下達具體指令_setBreakpoint 還通過 _pythonScript 告訴管家:“一旦門鈴響起,你就用你手上的萬能鑰匙 (WriteMemory特權),去幫住戶打開他指定的那扇門 (在指定地址和長度的內存上執行操作)。”

所以,完整的流程是這樣的:

  • _attachToAppProcess:授予 lldb ,大樓的安全限制被解除了,你可以走進去,但是你沒有鑰匙,這是前提條件。
  • _setBreakpoint:管家 (_pythonScript) 被部署到位,並且明確了工作指令(監聽門鈴並開門),這是執行機制,當你需要 JIT 的時候,就去按下門鈴

所以 _pythonScript 是 Flutter lldb 架構的連接點,它作為一個即時協議適配器,在 lldb 的原生世界和 Dart VM 服務的托管世界之間進行翻譯。

自此, lldb attach 成功後,Dart VM 在啟動時會嘗試打開 JIT Compiler,當然,如果 lldb 失敗,它將回退到使用過去的 Xcode 自動化支持:

image

image

所以,隨著全新的 iOS 26 穩定版即將發布,Flutter 也完成了它全新 LLDB 調試的適配遷移,不過也可以看出,iOS 上的 JIT 持續支持,確實不是一件容易的事情。


原文出處:https://juejin.cn/post/7542461507402924075


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
站長阿川

站長阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

站長精心設計,帶你實作 63 個小專案,得到作品集!

立即開始免費試讀!