在之前的 《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 官方之前臨時實現了一個過度方案:
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
協同工作的支持:
而針對這個問題,Flutter 在 Xcode 16 也終於實現了新的調整#173443,通過新的 devicectl
+ lldb
集成到 flutter run
命令來回歸 Apple 官方的 debug 體系:
devicectl
實現安裝啟動: devicectl
作為在 Xcode 15 中引入的控制工具,它主要負責將編譯好的應用包(.app
)安裝到物理設備上,並負責啟動應用進程lldb
實現 JIT 和調試運行:作為 LLVM 項目的一部分,lldb
是 Apple 標準的底層調試器,在新架構中它將作為核心的調試傳輸層,負責附加到由 devicectl
啟動的應用進程,並建立起和 Dart VM 進行通信的橋樑具體可以在 flutter_tools
的 lldb.dart
看到,launchAppWithLLDBDebugger
啟動之後,就會執行 lldb 的 attachAndStart
:
而對於 attachAndStart
,主要核心就有:
_setBreakpoint
_attachToAppProcess
那為什麼需要在執行 lldb 的時候通過 _setBreakpoint
添加一個斷點呢?實際上這就是在前面臨時方案基礎上的完善, _setBreakpoint
的主要目的就是:
NOTIFY_DEBUGGER_ABOUT_RX_PAGES
作為 lldb 的斷點_pythonScript
腳本,當斷點觸發時,利用 lldb 的權限執行腳本,創建一個新的 rx 內存關於 NOTIFY_DEBUGGER_ABOUT_RX_PAGES
作為斷點我們在之前講過,它是 Dart VM 在 VirtualMemory::AllocateAligned
時,會通過 NOTIFY_DEBUGGER_ABOUT_RX_PAGES
觸發,去讓 lldb 用它的權限申請執行:
而對於在 lldb 裡執行的 py 腳本,它主要是:
NOTIFY_DEBUGGER_ABOUT_RX_PAGES
的函數,這個調用會觸發預設的斷點_pythonScript
的代碼立即被執行
x0
, x1
) 讀取 Dart VM 請求的內存地址和長度WriteMemory
向該內存地址寫入數據,這個“寫入”動作是關鍵,它會強制 iOS 系統為這塊內存做好準備b'IHELPED!'
的“回執”信號,以便 Dart VM 確認操作已成功False
,告訴 lldb “任務完成,請立即讓應用繼續運行”之後,通過 lldb device process attach --pid
的方式,讓進程被納入“開發者調試上下文”,從而支持 JIT 權限:
前面說起來比較抽象,具體可以理解為:
因為系統的 W^X 安全策略,_attachToAppProcess
的核心作用就是利用 lldb 附加的特權,為整個應用進程解鎖了這個限制。
在這一步完成之後,應用進程的狀態從“不允許 JIT”變成了“理論上可以 JIT”,它獲得了讓內存頁變為可讀、可寫、可執行 (RWX) 的可能性。
但是,僅僅有可能性是不夠的,因為 Dart VM 在運行時和 hotload 是動態地、按需地需要新的可執行內頁 page,它需要一個“機制”來實現,在需要的時候真正地去執行這個“將內存頁變為 RWX”的操作,而 App 本身的 Dart VM 本身沒有這個權限,所以它無法自己完成這個操作。
這時,它就像一個身處大樓內、知道自己需要打開一扇門,但自己手上沒有鑰匙的住戶。
_setBreakpoint
的作用就是建立這個缺失的機制,類似於:
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 自動化支持:
所以,隨著全新的 iOS 26 穩定版即將發布,Flutter 也完成了它全新 LLDB 調試的適配遷移,不過也可以看出,iOS 上的 JIT 持續支持,確實不是一件容易的事情。