如何優雅的實現一個 lua 調試器
最近一段時間在幫公司一個項目組的客戶端 review 代碼。
我們的所有項目,無論渲染底層是用的 ejoy2d 還是 Unity3d ,實際開發的時候都基本是使用 lua 。所以開發人員日常工作基本是在和 Lua 打交道。
雖然我個人挺反感圍繞著調試的開發方式,也就是不斷的在測試、試錯,糾正的循環中奔波。我認為好的程序應該努力在編寫的過程中,在頭腦中排錯;在預感到壞味道時,就趕快重寫。而壞味道通常指代碼陷入了復雜度太高的境地,無法一眼看出潛在的問題。對付復雜度最好的武器是簡化代碼,而非調試器。
在真正遇到 bug 時,應該仔細瀏覽代碼,設想各種出錯的可能。而不是將錯誤的代碼運行起來,查看運行中的狀態變化。
話說回來,看到項目組的同學真的碰到 bug 時,不斷的啟動 Unity 客戶端,把時間浪費在等待那幾行 debug log 上,我覺得效率還是很低。必要的調試工具應該能提升一些開發效率的。
lua 官方提供了完善的 debug api 可以查詢所有的信息;但并沒有一套官方的調試工具。我都不記得是第幾次寫調試工具了。至少在這個 blog 上就記錄了好幾次。最近的一次是 3 年前 。
每次做完送給人用了兩天就扔掉了。這次一時興起,周末又做了一個。當然每次都會有一些不一樣的想法。
這次的版本只開了個頭,把構想中的基礎架構搭好了。那就是,我認為一個優雅的調試器不應該過多的干涉被調試的實體(比方說你想監控被調試的程序內存使用的情況,和 gc 的工作)。
過去的一些版本都是把調試器代碼直接嵌入被調試的虛擬機的,調試器本身和被調試的代碼并沒有明顯的界限。調試過程也會在同一個虛擬機中運行。
我這次想玩點不一樣的,讓調試器運行在一個獨立的虛擬機內,它通過一組接口來觀察被調試程序。這樣,在這個基礎上制作的調試器,可以更放心的添加一些花哨的功能了。比如啟動一個圖形界面、或是提供一個 web server ,調試者可以通過瀏覽器來監控內部狀態,發送調試指令。
當然最簡單的用法是非侵入式的輸出 log 。不必在被調試代碼中硬加上幾句 print 輸出 log (這是沒有調試工具時,大家最常用的調試方法),而可以把 print 查看內部狀態的代碼寫在獨立的調試器模塊中。我們可以用編程的方式來編寫調試過程,而不局限于一個交互式調試工具提供的有限手段。
我這次設計的調試模塊,只提供一個概念:探測點。
你可以在被調試代碼中設置探測點,探測點并不是斷點,而更像一個觀測點。在這個點上,調試器并不會停下來等待調試者的指令,而是運行調試器里的一個函數。(這個函數是運行在獨立的虛擬機里的,完全不用代碼有什么副作用)
你可以在探測點函數中,訪問被調試代碼在該處的狀態,做一些合適的事情。比如把狀態輸出到 log 文件中,比如根據狀態條件來選擇做些事情;當然也可以暫停下來,交互式等待調試命令。
探測點可以分為兩種,一種是在調試前,硬編碼在代碼中的,只要運行到那里,就會觸發一下探測行為;還有一種是利用 lua 的 Hook 透明添加的。
兩種探測點可以混合使用。比如在游戲主循環中預先硬編碼進一個探測點,平時不啟動調試器,運行到探測點時就自動忽略;當需要的時候,讓這個探測點起作用,然后在探測函數中,設置 hook 點,進一步的調試。
不過期待它短期內發展成為一個圖形式的漂亮交互調試器可能有點不現實,除非做前端的朋友有興趣來完善它。(比如增加一個 web server ,直接可以通過瀏覽器連接到程序里交互調試)
最后說點這個東西的實現中一個有趣的部分:
由于調試器和被調試程序處于兩個不同的 VM 中,所以調試器代碼并不能直接引用被調試代碼環境中的 table 。這里是怎么做到的呢?
我設計了一個 C 結構(封裝成 userdata),里面保存了一個無法被直接引用的 lua 對象的引用路徑。
比如,從探測點出發,你想獲得某個對象的狀態,無非只有幾個途徑。獲取某個棧幀的 local 變量、upvalue 、或是從全局表中檢索到一個對象等等,如果這個對象是一個 table ,可以進一步的去取 table 里的子域。總之,你總是通過一層層的簡潔途徑獲得最終想觀察的變量的。
那么,在調試器中,只需要把這個過程記錄下來、而不需要鉚定一個特定的對象。這個過程封裝成一個 userdata ,它的實際含義和最終對應的對象是一致的。
來自:http://blog.codingnow.com/2016/11/lua_debugger.html