• Lua中如何实现类似gdb的断点调试—07支持通过函数名称添加断点


    我们之前已经支持了通过函数来添加断点,并且已经支持了行号的检查和自动修正。但是通过函数来添加断点有一些限制,如果在当前的位置无法访问目标函数,那我们就无法对其添加断点。

    于是,本篇我们将扩展断点设置的接口,支持通过函数名称添加断点,以突破这个限制。

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

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

    实现分析

    由于Lua是动态类型语言,变量可以是任何值。而函数在Lua语言中又是第一类值,与其他值一样使用,可以被存放在变量中、作为参数或返回值传递。所以一个函数的名字是不确定的,它可能是任意名字,取决于函数调用时候的变量的名称。

    通过下面这个简单的例子,就可以看出来

    local ldb = require "luadebug"
    local setbp = ldb.setbreakpoint
    local rmbp = ldb.removebreakpoint
    
    local function foo()
    end
    
    setbp(foo, 6)
    
    local bar = foo
    
    foo()
    bar()
    

    我们在foo函数中添加了一个断点,将foo函数赋值给局部变量bar,然后分别用foo和bar调用函数。运行这个脚本结果如下:

    $ lua namenotstable.lua
    Lua (local)foo namenotstable.lua:6
    lua_debug> cont
    Lua (local)bar namenotstable.lua:6
    lua_debug> cont
    

    调用foo()bar()都会碰到断点,函数名称分别为foobar

    所以通过函数名称添加的断点并不是确定的,函数名称和函数之间并不是一一映射的关系,而可能是m对n的关系。就算已经匹配到了一个与断点设置的函数名称一致的函数,我们也不能简单地将函数名称断点转换成相应的函数断点,而是仍然需要维护函数名称断点。

    因此,我们需要增加一个维护函数名称断点的数据结构----新的断点表status.namebpt。类似之前在05优化断点信息数据结构中添加的status.funcbpt表,只是表的键由之前的函数变成了函数名称。status.namebpt表的值同样是一个表,该表的键是断点行号,值为断点id。同样地,为了快速获取断点个数,我们在表中额外加了一个特殊的num字段保存该函数名称中的断点个数。

    通过下面的例子来直观地看一下,假设我们的bptable表中添加了两个断点如下(name字段用来保存函数名称):

    bptable[1] = {name = "foo", line = 10}
    bptable[2] = {name = "foo", line = 20}
    

    对应的在namebpt表中的操作如下:

    namebpt["foo"] = {}          -- 构造表
    namebpt["foo"][10] = 1	     -- 函数名foo,行号10,断点id为1
    namebpt["foo"].num = 1       -- 该函数第一个断点
    namebpt["foo"][20] = 2       -- 函数名foo,行号20,断点id为2
    namebpt["foo"].num = namebpt["foo"].num + 1	-- 断点个数+1
    

    OK,分析完了,接下来开始修改相应的代码实现。

    添加断点

    按照惯例,我们先修改设置断点函数。因为支持了通过函数名称设置断点,第一个参数需要支持string类型。为了简洁及代码重用,我们将之前通过函数设置断点的操作封装成了setfuncbp函数,另外将通过函数名称设置断点的操作封装成了setnamebp函数。

    local function setbreakpoint(where, line)
        if (type(where) ~= "function" and type(where) ~= "string")
            or ( line and type(line) ~= "number") then
            io.write("invalid parameter\n")
            return nil
        end
    
        if type(where) == "function" then
            return setfuncbp(where, line)
        else            -- "string"
            return setnamebp(where, line)
        end
    end
    

    接下来,来看下setnamebp函数的实现:

    local function setnamebp(name, line)
        local s = status
        local namebp = s.namebpt[name]
        if not line then                    -- 如果没有指定行号
            line = 0                        -- 用一个特殊值0来表示第一个有效行
        end
        -- 是否已经添加了相同的断点
        if namebp and namebp[line] then
            return namebp[line]
        end
    
        s.bpid = s.bpid + 1
        s.bpnum = s.bpnum + 1
        s.bptable[s.bpid] = {name = name, line = line}
    
        if not namebp then                  -- 该函数名称的第一个断点
            s.namebpt[name] = {}
            namebp = s.namebpt[name]
            namebp.num = 0
        end
        namebp.num = namebp.num + 1
        namebp[line] = s.bpid
    
        if s.bpnum == 1 then                -- 第一个全局断点
            debug.sethook(hook, "c")        -- 设置钩子函数的"call"事件
        end
        return s.bpid                       --> 返回断点id
    end
    

    因为我们支持不指定行号,但我们并不确定函数的第一个有效行是什么。为了方便地记录断点,又不至于与实际的断点行冲突,我们用了一个特殊值0来表示这种情况。

    后续的逻辑与setfuncbp函数基本一致,如果已经添加了相同的断点,则返回之前的断点id。然后分别在bptable表和namebp表中添加断点。这里不再赘述。

    删除断点

    删除断点函数的改动不大。主要是要区分删除的是哪类断点,这个可以通过s.bptable表中id所对应的断点信息来判断。如果有func则说明是通过函数添加的断点,否则则是通过函数名称添加的断点。根据情况删除s.funcbpt或者s.namebpt表中的断点,最后删除s.bptable表中的断点。

    local function removebreakpoint(id)
        local s = status
        if s.bptable[id] == nil then
            return
        end
        local func = s.bptable[id].func
        local name = s.bptable[id].name
        local line = s.bptable[id].line
    
        local dstbp = nil
        if func then
            dstbp = s.funcbpt[func]
        else
            dstbp = s.namebpt[name]
        end
        if dstbp and dstbp[line] then
            dstbp.num = dstbp.num - 1
            dstbp[line] = nil
            if dstbp.num == 0 then
                dstbp = nil
            end
        end
    
        s.bptable[id] = nil
        s.bpnum = s.bpnum - 1
        if s.bpnum == 0 then
            debug.sethook()                 -- 移除钩子
        end
    end
    

    获取函数信息

    正如前面提到过的,因为函数名称信息是不确定的,所以我们修改了getfuncinfo函数实现,不再缓存函数名称信息,而只缓存确定的函数信息。

    local function getfuncinfo (func)
        local s = status
        local info = s.funcinfos[func]
        if not info then
            info = debug.getinfo(func, "SL")
            if (info.activelines) then
                info.sortedlines = {}
                for k, _ in pairs(info.activelines) do
                   table.insert(info.sortedlines, k)
                end
                table.sort(info.sortedlines)
            end
            s.funcinfos[func] = info
        end
        return info
    end
    

    钩子函数

    钩子函数的改动主要是在call事件。函数名称每次都根据调用栈实时获取。首先在函数断点表s.funcbpt中查找当前函数是否有断点,如果没有则再去函数名称断点表s.namebpt中查找。需要检查断点行号是否在当前函数的定义范围之内,只有当行号在范围之内才认为匹配。如果没有指定行号的话(默认为第一个有效行),则总是认为匹配。另外,在调用栈信息表中,分别将确定的函数信息funcinfo和调用栈相关信息stackinfo分别保存,以供return事件和line事件时使用。

    local function hook (event, line)
        local s = status
        if event == "call" or event == "tail call" then
            local stackinfo = debug.getinfo(2, "nf")
            local func = stackinfo.func
            local name = stackinfo.name
            local funcinfo = getfuncinfo(func)
            local hasbreak = false
            if s.funcbpt[func] then
                hasbreak = true
            end
            if not hasbreak and s.namebpt[name] then
                local min = funcinfo.linedefined
                local max = funcinfo.lastlinedefined
                for k, _ in pairs(s.namebpt[name]) do
                    if k ~= "num" and ((k >= min and k <= max) or k == 0) then
                        hasbreak = true
                        break
                    end
                end
            end
            if event == "call" then     -- for tail call, just overwrite
                s.stackdepth = s.stackdepth + 1
            end
            s.stackinfos[s.stackdepth] =
                {stackinfo = stackinfo, funcinfo = funcinfo, hasbreak = hasbreak}
            -- found breakpoint in current function
            if hasbreak then
                debug.sethook(hook, "crl")	-- add "line" event
            else        -- no breakpoints found
                debug.sethook(hook, "cr")   -- remove "line" event temporarily
            end
        elseif event == "return" or event == "tail return" then
                -- 省略
    end
    

    line事件也需要做相应的修改

    local function hook (event, line)
        -- 省略
        elseif event == "line" then
            local sinfo = s.stackinfos[s.stackdepth].stackinfo
            local finfo = s.stackinfos[s.stackdepth].funcinfo
            local func = sinfo.func
            local name = sinfo.name
            local funcbp = s.funcbpt[func]
            local namebp = s.namebpt[name]
            if (funcbp and funcbp[line]) or (namebp and namebp[line])
                or (namebp and namebp[0] and line == finfo.sortedlines[1]) then
                local prompt = string.format("%s (%s)%s %s:%d\n",
                    finfo.what, sinfo.namewhat, name, finfo.short_src, line)
                io.write(prompt)
                debug.debug()
            end
        end
    end
    

    在判断当前行是否有断点时,除了查看funcbpt表,还需要查看namebpt表,对于函数名称断点没有指定行号的情况,判断当前行是不是第一个有效行。打印提示信息时,则从stackinfos表中保存的信息中获取。

    测试

    代码修改好了,我们来测试下通过函数名称添加断点的功能。编写如下测试脚本:

    local ldb = require "luadebug"
    local setbp = ldb.setbreakpoint
    local rmbp = ldb.removebreakpoint
    
    local function foo()
        local a = 0
    end
    
    local function bar()
        local a = 0
    end
    
    local function pee()
        local a = 0
    end
    
    local id1 = setbp(foo)
    local id2 = setbp(foo, 7)
    
    local id3 = setbp("bar")
    local id4 = setbp("bar", 11)
    local id5 = setbp("bar", 100)
    
    local id6 = setbp(pee)
    local id7 = setbp("pee", 15)
    
    foo()
    bar()
    pee()
    
    rmbp(id1)
    rmbp(id3)
    rmbp(id6)
    
    foo()
    bar()
    pee()
    
    rmbp(id2)
    rmbp(id4)
    rmbp(id7)
    
    foo()
    bar()
    pee()
    

    我们添加了三个函数,其中foo函数以函数作为参数添加断点,bar函数以函数名称作为参数添加断点,pee函数分别用函数和函数名添加了一个断点。添加完断点,先分别调用一次,预期每个函数都会碰到两个断点。接着三个函数各删除一个断点,再各调用一次,预期每个函数都会碰到一个断点。最后三个函数再各删除一个断点,再各调用一次,预期不碰到断点。

    运行测试脚本,结果符合预期。

    $ lua test.lua
    Lua (local)foo test.lua:6
    lua_debug> cont
    Lua (local)foo test.lua:7
    lua_debug> cont
    Lua (local)bar test.lua:10
    lua_debug> cont
    Lua (local)bar test.lua:11
    lua_debug> cont
    Lua (local)pee test.lua:14
    lua_debug> cont
    Lua (local)pee test.lua:15		# 第一次调用,每个函数碰到两个断点
    lua_debug> cont					
    Lua (local)foo test.lua:7
    lua_debug> cont
    Lua (local)bar test.lua:11
    lua_debug> cont
    Lua (local)pee test.lua:15		# 第二次调用,每个函数碰到一个断点
    lua_debug> cont
    $						# 第三次调用,不再碰到断点
    
  • 相关阅读:
    pureftp 超时 mlsd
    安卓相机调用适配
    解决多个界面重复共用同一组数据导致数据同步改变(实现数据的完全深拷贝)
    PHP反射API的使用、体会、说明
    c冒泡排序
    iOS开发常用的第三方类库
    修改按钮上图片的大小-iOS
    获取手机通讯录--ios
    根据搜素的字符串改变label包含该字符串的文字
    cell点击按钮崩的一种情况
  • 原文地址:https://www.cnblogs.com/logchen/p/16005468.html
Copyright © 2020-2023  润新知