Lua中如何實現類似gdb的斷點調試–02通用變數列印
在前一篇01最小實現中,我們實現了Lua斷點調試的的一個最小實現。我們編寫了一個模組,提供了兩個基本的介面:設置斷點和刪除斷點。
雖然我們已經支援在斷點進行變數的列印,但是需要自己指定層數以及變數索引,使用起來不是很方便。要進行upvalue列印的話,操作會更加麻煩。為了提升調試的方便性,我們決定封裝一個通用的變數列印函數,可以通過變數名查找到對應變數的值進行列印。支援局部變數、upvalue以及全局的_ENV
中的變數。
本文程式碼已開源至Github,歡迎watch/star😘。
本部落格已遷移至CatBro’s Blog,那裡是我自己搭建的個人部落格,歡迎關注。
因為函數比較長,我們分幾部分進行說明,該函數有三個參數:name
為要查找的變數的名字,level
指示在哪個層級的函數中查找,isenv
是個標記我們稍後再提。層級的默認值為1, 注意將層級加上1,是為了將層次修正為包含_getvavalue
函數自己。
然後遍歷局部變數表,比較變數名字是不是等於name
,如果匹配的話記錄其值,並且標記一下我們已經找到。注意我們找到之後並沒有立馬跳出循環,因為可能具有多個同名的局部變數,我們應該獲取索引最大的那個。
循環結束之後,如果已經在局部變數中找到了name
,就返回"local"
和變數的值。
local function _getvarvalue (name, level, isenv)
local value
local found = false
-- 加1將層次糾正為包括_getvarvalue自己
level = (level or 1) + 1
-- 嘗試局部變數
for i = 1, math.huge do
local n, v = debug.getlocal(level, i)
if not n then break end
if n == name then
value = v
found = true
-- 這裡不跳出,獲取具有最大索引的那個局部變數
end
end
if found then return "local", value end
-- 省略
end
上值中查找
如果在局部變數中沒有找到,我們再嘗試到upvalue中進行查找。首先通過getbug.getinfo
獲取到第level層的函數,然後遍歷其上值,如果找到匹配的變數就返回"upvalue"
和變數值。
local function _getvarvalue (name, level, isenv)
-- 省略
-- 嘗試非局部變數
local func = debug.getinfo(level, "f").func
for i = 1, math.huge do
local n, v = debug.getupvalue(func, i)
if not n then break end
if n == name then return "upvalue", v end
end
-- 省略
end
_ENV表中查找
如果在普通的上值中還是沒有找到,我們就去_ENV
表中查找。isenv
標誌表示當前name
是否就是"_ENV"
,是用來防止無限循環調用的,第一次調用的時候肯定不是。然後將"_ENV"
作為name
遞歸調用_getvarvalue
。因為多了一次函數調用,第二次調用的時候level又會自動加1。接下來還是先後在局部變數和上值中查找,找到了就返回類型和變數值。沒有找到的話,返回"noenv"
。
然後返回到外層的_getvarvalue
,判斷第二個返回值是否為真,如果是說明找到了_ENV
表,就從_ENV
表中獲取名為name的值,否則直接返回"noenv"
。
local function _getvarvalue (name, level, isenv)
-- 省略
if isenv then return "noenv" end -- 避免無限循環
-- 沒找到,從環境中獲取
local _, env = _getvarvalue("_ENV", level, true)
if env then
return "global", env[name]
else
return "noenv"
end
end
包裝函數
_getvarvalue
函數已經定義好了,我們再定義一個包裝函數printvarvalue
。如果第二個返回值為真,表示找到了變數,就列印變數類型及結果,否則提示未找到。
{% label warning@注意到 %}我們這裡將level
層次數加了4,目的是跟_getvarvalue
函數中類似,也是為了修正層次數以包含printvarvalue
函數自身以及其上層的debug mainchunk
、debug.debug
以及鉤子函數。這樣當level
參數為1時就表示斷點所在的函數。同樣地,如果level不指定默認為1,即斷點所在函數。2表示斷點所在函數上一層,以此類推。當然如果你有特殊需求,你也可以指定層次為0,查看hook函數的情況。
-- 包裝 _getvarvalue, 列印結果
local function printvarvalue (name, level)
-- level默認值1
-- 加4,將層次糾正為包含 printvarvalue, debug mainchunk, debug.debug和hook
level = (level or 1) + 4
local where, value = _getvarvalue(name, level)
if value then
print(where, value)
else
print(name, "not found")
end
end
最後將printvarvalue
函數作為模組的函數輸出。
return {
setbreakpoint = setbreakpoint,
removebreakpoint = removebreakpoint,
printvarvalue = printvarvalue,
}
測試腳本
OK,調試庫我們已經改好了,接下來將我們之前的測試腳本test.lua
稍做修改。在開頭添加如下一行:
pv = luadebug.printvarvalue
為了方便在斷點內部調用,我們將其寫到全局變數里了。然後調整下斷點的行號,換一下斷點刪除的順序,其他內容保持不變。
local luadebug = require "luadebug"
local setbp = luadebug.setbreakpoint
local rmbp = luadebug.removebreakpoint
pv = luadebug.printvarvalue -- 增加這一行
g = 1
local u = 2
local function foo (n)
local a = 3
a = a + 1
u = u + 1
g = g + 1
end
local function bar (n)
n = n + 1
end
local id1 = setbp(foo, 12) -- 行號調整
local id2 = setbp(bar, 17) -- 行號調整
foo(10)
bar(10)
rmbp(id2) -- 先刪除斷點2
foo(20)
bar(20)
rmbp(id1) -- 再刪除斷點1
foo(30)
bar(30)
測試驗證
接下來,讓我們來測試一把。可以看到無論是局部變數、upvalue、還是全局_ENV
表中的變數,都可以很方便地獲取值。
$ lua test.lua
(local)foo test.lua:12
lua_debug> pv("a")
local 4
lua_debug> pv("u")
upvalue 2
lua_debug> pv("g")
global 1
lua_debug> pv("x")
x not found
lua_debug>
第二次停到foo函數斷點處,u
和g
都已經加1。
lua_debug> cont
(local)bar test.lua:17
lua_debug> cont
(local)foo test.lua:12
lua_debug> pv("a")
local 4
lua_debug> pv("u")
upvalue 3
lua_debug> pv("g")
global 2
lua_debug>
我們嘗試顯式指定層數,結果一樣
lua_debug> pv("a", 1)
local 4
lua_debug> pv("u", 1)
upvalue 3
lua_debug> pv("g", 1)
global 2
lua_debug>
我們再嘗試列印上一層的變數(即main chunk),結果都符合預期。變數a是foo里的局部變數應該找不到,變數u在main chunk中是局部變數,全局變數g則沒啥區別。
lua_debug> pv("a", 2)
a not found
lua_debug> pv("u", 2)
local 3
lua_debug> pv("g", 2)
global 2
OK,大功告成!