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 mainchunkdebug.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函數斷點處,ug都已經加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,大功告成!