8

Lua中如何实现类似gdb的断点调试--03通用变量修改及调用栈回溯 - 猫猫哥

 2 years ago
source link: https://www.cnblogs.com/logchen/p/15990908.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在前面两篇01最小实现02通用变量打印中,我们已经实现了设置断点、删除断点及通用变量打印接口。

本篇将继续新增两个辅助的调试接口:调用栈回溯打印接口、通用变量设置接口。前者打印调用栈的回溯信息,后者可以方便地修改变量的值,支持局部变量、upvalue以及全局的_ENV中的变量。

本文代码已开源至Github,欢迎watch/star😘。

本博客已迁移至CatBro's Blog,那里是我自己搭建的个人博客,欢迎关注。

调用栈打印函数

我们首先来实现调用栈回溯打印接口printtraceback(),这个接口比较简单,只是简单地包装了一下debug.traceback(),对层级进行了一个修正,就不多介绍了。

-- 打印调用栈的一个回溯
local function printtraceback(level)
    -- 层次的默认值为1
    -- 加上4是为了修正层次数以包含printtraceback, debug mainchunk, debug.debug, hook
    level = (level or 1) + 4
    print(debug.traceback(nil, level))
end

通用变量值修改函数

接着来实现通用的变量值修改函数_setvarvalue(),这个函数的结构跟_getvarvalue()类似,依次在局部变量、上值和_ENV表中查找,只不过找到之后会修改相应的值。函数有三个参数,name为要修改的变量的名字,value是修改的目标值,level指示在哪个层级的函数中查找,我们同样分成几部分来看。

local function _setvarvalue (name, value, level)
    -- 省略
end

局部变量中查找

同样地,先处理层级,层级的默认值为1, 将层级加上1是为了将层次修正为包含_setvavalue函数自己。

然后遍历局部变量表,查找是否有名字为name的变量,如果找到的话记录其索引。注意 我们找到之后并没有立马跳出循环,因为可能具有多个同名的局部变量,我们应该获取索引最大的那个。

循环结束之后,如果已经在局部变量中找到了name,就修改该变量的值为value,然后返回"local",指示修改的是局部变量。

local function _setvarvalue (name, value, level)
    local index

    -- 加1是为了将层次修正了包含_setvarvalue自身
    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
            index = i
            -- 只更新索引值,并不退出循环
        end
    end
    if index then
        debug.setlocal(level, index, value)
        return "local"
    end
    -- 省略
end

上值中查找

如果在局部变量中没有找到,我们再尝试到upvalue中进行查找。首先通过getbug.getinfo获取到第level层的函数,然后遍历其上值,如果找到匹配的变量就修改其值为value,然后返回"upvalue"以指示修改的是上值。

local function _setvarvalue (name, value, level)
    -- 省略
    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
            debug.setupvalue(func, i, value)
            return "upvalue"
        end
    end
    -- 省略
end

_ENV表中查找

如果在普通的上值中还是没有找到,我们就去_ENV表中查找。首先调用_getvarvalue获取到_ENV表,注意这里的isenv标志为true。如果如果找到了_ENV表且表中存在名为name的变量,就修改其值为value,然后返回"global"以指示是修改的_ENV表中的值。如果没有_ENV表或表中不存在要找的变量,就返回nil

local function _setvarvalue (name, value, level)
    -- 省略
    local _, env = _getvarvalue("_ENV", level, true)
    if env and env[name] then
        env[name] = value
        return "global"
    else
        return nil
    end
end

接下来我们同样再定义一个包装函数,对层次数level进行修正以包含setvarvalue函数自身以及其上层的debug mainchunkdebug.debug以及钩子函数。

然后对返回值进行检查,如果返回值为真,说明修改变量值成功,就打印变量类型,否则提示未找到。

local function setvarvalue (name, value, level)
    -- level 默认值为1
    -- 加4是为了将层次纠正为包含 settvarvalue, debug mainchunk, debug.debug和hook
    level = (level or 1) + 4
    local where = _setvarvalue(name, value, level)
    if where then
        print(where, name)
    else
        print(name, "not found")
    end
end

接口定义好了,让我们把这两个接口也添加到返回到表中。

return {
    -- 省略
    printtraceback = printtraceback,
    setvarvalue = setvarvalue,
}

接下来编写一个测试脚本test.lua以对我们新添加的接口进行测试。脚本很简单,就不多做解释了。

local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
pv = ldb.printvarvalue
sv = ldb.setvarvalue
ptb = ldb.printtraceback

g = 1

local u = 2
local function foo (n)
    local a = 3
    u = u
    g = g
end

local id1 = setbp(foo, 14)

foo(10)

rmbp(id1)

我们运行测试脚本,首先调用堆栈打印函数,默认是打印从断点所在函数开始的堆栈。

$ lua test.lua
(local)foo test.lua:14
lua_debug> ptb()
stack traceback:
	test.lua:14: in local 'foo'
	test.lua:21: in main chunk
	[C]: in ?
lua_debug>

我们显式指定层数试一下。

lua_debug> ptb(2)
stack traceback:
	test.lua:21: in main chunk
	[C]: in ?
lua_debug> ptb(0)
stack traceback:
	./luadebug.lua:20: in hook '?'
	test.lua:14: in local 'foo'
	test.lua:21: in main chunk
	[C]: in ?
lua_debug>

没有问题,层数为2的时候少打印了一层,为0的时候则多打了一层。

我们再来测试下变量值修改函数,先看来变量原来的值

lua_debug> pv("a")
local	3
lua_debug> pv("u")
upvalue	2
lua_debug> pv("g")
global	1
lua_debug>

然后修改变量的值,我们把这三个变量值都改成了6

lua_debug> sv("a", 6)
local	a
lua_debug> sv("u", 6)
upvalue	u
lua_debug> sv("g", 6)
global	g
lua_debug>

然后再打印下值检查下结果,可以看到都修改成功了。

lua_debug> pv("a")
local	6
lua_debug> pv("u")
upvalue	6
lua_debug> pv("g")
global	6
lua_debug>

我们再试试显式指定层级为2,将变量值再改为8

lua_debug> sv("a", 8, 2)
a	not found
lua_debug> sv("u", 8, 2)
local	u
lua_debug> sv("g", 8, 2)
global	g
lua_debug> pv("a")
local	6
lua_debug> pv("u")
upvalue	8
lua_debug> pv("g")
global	8
lua_debug>

变量a因为是函数foo的局部变量,所以外层看不到。变量u在main chunk中是属于局部变量,而变量g则还是全局变量。修改之后变量a的结果没有变,其他两个都改成了8。

Well done!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK