• Lua中如何实现类似gdb的断点调试—06断点行号检查与自动修正


    前面两篇我们对性能做了一个优化,接下来继续来丰富调试器的特性。

    我们前面提到过,函数内并不是所有行都是有效行,空行和注释行就不是有效行。我们之前在添加断点的时候,并没有对行号进行检查,任何行号都能成功添加断点。所以如果添加的断点行号是无效的,那么永远也不会断到那里。但是钩子里并不知道它是无效的,call事件仍然会以为函数有断点从而启动line事件,造成CPU的浪费。

    所以本篇,我们将对断点的行号进行检查,对于不在函数范围内的行号直接添加断点失败;在函数范围内的行号则自动修正为下一个有效的行号;另外支持不指定行号,默认为函数的第一个有效行。

    源码已经上传Github,欢迎watch/star。

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

    添加断点

    因为是断点行号相关的检查,所以修改主要集中在添加断点的函数中。首先因为支持了不指定行号,所以修改了参数检查的地方允许为空。其次,因为要检查行号是否有效,我们就需要先获取到函数的信息。考虑到在钩子函数中也需要获取函数信息,我们就把相关的操作封装成了一个单独的函数getfuncinfo()。获取到函数信息之后,就可以验证行号是否有效了,同样我们将这个验证行号的操作也封装成了一个单独的函数verifyfuncline

    local function setbreakpoint(func, line)
        local s = status
        if type(func) ~= "function" or ( line and type(line) ~= "number") then
            io.write("invalid parameter\n")
            return nil
        end
    
        -- get func info
        local info = getfuncinfo(func)
        if not info then
            io.write("unable to get func info\n")
            return nil
        end
    
        -- verify the line
        line = verifyfuncline(info, line)
        if not line then
            io.write("invalid line\n")
            return nil
        end
        
        -- 省略
    end
    

    获取函数信息

    getfuncinfo函数的代码如下:

    local function getfuncinfo (func, level)
        local s = status
        local info = s.funcinfos[func]
        if not info then
            if level then
                s.funcinfos[func] = debug.getinfo(level + 1, "nSL")
            else
                s.funcinfos[func] = debug.getinfo(func, "SL")
            end
            info = s.funcinfos[func]
            info.sortedlines = {}
            for k, _ in pairs(info.activelines) do
               table.insert(info.sortedlines, k)
            end
            table.sort(info.sortedlines)
        elseif level then	-- name和namewhat需要实时获取
             local nameinfo = debug.getinfo(level + 1, "n")
             info.name = nameinfo.name
             info.namewhat = nameinfo.namewhat
        end
        return info
    end
    

    该函数有两个参数,第一个参数就是函数,第二个可选的参数level用于指定在调用栈中的层数,第二个参数只有在钩子函数中时才会指定,返回值就是函数信息。如果在调用debug.getinfo的时候传递函数作为参数,那么是获取不到函数的名字信息的,namenamewhat字段都为空。因为函数可能是任意名字,Lua需要通过查找调用该函数的代码,知道它是怎么被调用的,从而确定函数的名字。所以只有当指定调用栈的层数时才能获取到名字信息。

    我们接着看代码的主体部分:

    首先尝试去s.funcinfos表中查找是否有缓存的函数信息。如果没有那就只能调用debug.getinfo去获取了,这里分为两种情况,如果指定了level参数,那么就以层数(这里+1同样是为了修正层数,我们在前面多次提到过)作为参数调用,此时第二个参数设置为了"nSL",比之前多了"L"用于获取有效行号;如果没有指定level参数,则以函数作为参数调用。获取到函数信息之后,为了方便我们后面的行号检查,我们对有效的行号进行了排序,info.sortedlines数组就是排序后的有效行号,然后就返回函数信息info了。

    如果缓存中已经有函数信息了,如果本次调用又指定了level参数,那么我们就更新下name信息。调用debug.getinfo获取到信息之后设置到原有的info表中。完成之后同样是返回函数信息info

    检查及修正函数行号

    verifyfuncline函数的代码如下:

    local function verifyfuncline (info, line)
        if not line then
            return info.sortedlines[1]
        end
        if line < info.linedefined or line > info.lastlinedefined then
            return nil
        end
        for _, v in ipairs(info.sortedlines) do
            if v >= line then
                return v
            end
        end
        assert(false)   -- impossible to reach here
    end
    

    该函数有两个参数,其中第二个行号是可选的。如果没有指定行号,那么直接返回函数的第一个有效行号。如果指定了行号,但是范围超出了函数定义的范围,那么返回nil。如果行号落在函数范围内,那么就遍历已经排好序的有效行号数组,返回碰到的第一个大于等于指定行号的值。

    钩子函数

    接下来看下钩子函数的修改,因为我们已经封装了getfuncinfo函数,所以钩子函数中也改成用它来获取函数信息。不过这里在调用的时候指定了level从而可以获取到函数名字信息。

    local function hook (event, line)
        -- 省略
        elseif event == "line" then
            local curfunc = s.stackinfos[s.stackdepth].func
            local funcbp = s.funcbpt[curfunc]
            assert(funcbp)
            if funcbp[line] then
                local info = getfuncinfo(curfunc, 2)
                local prompt = string.format("%s (%s)%s %s:%d\n",
                    info.what, info.namewhat, info.name, info.short_src, line)
                io.write(prompt)
                debug.debug()
            end
        end    
    end
    

    OK,代码修改完了,我们进行测试。

    测试有效行排序

    首先测试一下,有效行号排序那块的逻辑。我们编写了一个如下的测试脚本:

    local debug = require "debug"
    
    local function foo()
        local a = 0
    
        a = a + 1
    
        a = a + 1
    end
    
    local function bar() end
    
    local function sortlines(func)
        local info = debug.getinfo(func, "nSL")
        info.sortedlines = {}
        for k, v in pairs(info.activelines) do
            print(k, v)
            table.insert(info.sortedlines, k)
        end
    
        table.sort(info.sortedlines)
    
        for k, v in ipairs(info.sortedlines) do
            print(k, v)
        end
    end
    
    print("foo")
    sortlines(foo)
    print("bar")
    sortlines(bar)
    

    我们定义了两个函数foo和bar,其中foo函数的范围为第3行到第9行,有4个有效行4、6、8、9。而bar函数则为特殊的单行函数。

    运行脚本,输出如下

    $ lua sortlines.lua
    foo
    4	true
    9	true
    6	true
    8	true
    1	4
    2	6
    3	8
    4	9
    bar
    11	true
    1	11
    

    foo函数4个有效行没排之前是4、9、6、8,排序之后变成4、6、8、9。bar函数唯一的有效行就是它开始定义的那行。

    测试行号检查和自动修正

    编写测试脚本如下:

    local ldb = require "luadebug"
    local setbp = ldb.setbreakpoint
    local rmbp = ldb.removebreakpoint
    
    local function foo()
        local a = 0
    
        a = a + 1
    
        a = a + 1
    end
    
    local id1 = setbp(foo)
    assert(id1 == 1)
    local id2 = setbp(foo, 5)
    assert(id2 == id1)
    local id3 = setbp(foo, 6)
    assert(id3 == id1)
    local id4 = setbp(foo, 7)
    assert(id4 == 2)
    local id5 = setbp(foo, 8)
    assert(id5 == id4)
    local id6 = setbp(foo, 9)
    assert(id6 == 3)
    local id7 = setbp(foo, 100)
    assert(not id7)
    
    foo()
    
    rmbp(id1)
    rmbp(id4)
    
    foo()
    
    rmbp(id6)
    
    foo()
    

    我们在foo函数上添加了好几个断点,第一个断点行号省略,第二个断点加在了第5行,也就是函数开始定义的行,第三个断点加在了第6行,这是函数第一个有效行。预期前三次添加断点应该都返回同一个断点id,断在第6行。接下来添加的两个断点,第7行不是有效行,第8行是有效行,预期返回同一个断点id,断在第8行。然后在第9行添加了一个断点,因为不是有效行,预期断在第10行。最后一个在第100行设置了一个断点,因为超出了函数的范围,预期设置断点失败返回nil

    设置好断点,先调用一次foo函数,然后删除两个断点,在调用一次foo函数,最后将剩余那个断点删除,再调用一次foo函数。

    我们了运行下测试脚本

    $ lua test.lua
    invalid line
    Lua (local)foo test.lua:6
    lua_debug> 
    

    断点的设置都符合预期,最后一个因为行号超出了范围,打了一行错误日志invalid line,程序停在了第6行处。然后我们输入两个cont,程序停在了最后一个断点处。

    Lua (local)foo test.lua:6
    lua_debug> cont
    Lua (local)foo test.lua:8
    lua_debug> cont
    Lua (local)foo test.lua:10
    lua_debug> 
    

    我们再次输入cont,foo函数运行结束,此时因为前两个断点已经被删除,第二次调用foo函数应该直接停在断点3处,也就是第10行

    Lua (local)foo test.lua:6
    lua_debug> cont
    Lua (local)foo test.lua:8
    lua_debug> cont
    Lua (local)foo test.lua:10
    lua_debug> cont
    Lua (local)foo test.lua:10
    lua_debug>
    

    我们再次输入cont,因为最后一个断点也被删除了,所以最后一个执行foo函数没有再碰到断点。

    $ lua test.lua
    invalid line
    Lua (local)foo test.lua:6
    lua_debug> cont
    Lua (local)foo test.lua:8
    lua_debug> cont
    Lua (local)foo test.lua:10
    lua_debug> cont
    Lua (local)foo test.lua:10
    lua_debug> cont
    $
    
  • 相关阅读:
    smtp实验(生成树协议)
    结合以太通道的vlan配置
    路由器基础配置之浮动静态路由
    路由器基础配置之rip
    路由器基础配置之静态路由
    路由器基础配置之单臂路由实现vlan间通信
    交换机基础配置之三层交换机实现vlan间通信
    交换机基础配置之stp生成树实验
    交换机基础配置之结合以太通道的vlan设置
    交换机基础设置之vtp管理vlan设置
  • 原文地址:https://www.cnblogs.com/logchen/p/16000135.html
Copyright © 2020-2023  润新知