0

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

 2 years ago
source link: https://www.cnblogs.com/logchen/p/16005468.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.

  博客园 ::

首页 :: 博问 :: 闪存 ::

新随笔 ::

联系 ::

订阅

::

管理 ::

  116 随笔 :: 0 文章 :: 3 评论 ::

77053 阅读

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

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

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

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

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

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

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

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

$ lua namenotstable.luaLua (local)foo namenotstable.lua:6lua_debug> contLua (local)bar namenotstable.lua:6lua_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为1namebpt["foo"].num = 1       -- 该函数第一个断点namebpt["foo"][20] = 2       -- 函数名foo,行号20,断点id为2namebpt["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)    endend

接下来,来看下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                       --> 返回断点idend

因为我们支持不指定行号,但我们并不确定函数的第一个有效行是什么。为了方便地记录断点,又不至于与实际的断点行冲突,我们用了一个特殊值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()                 -- 移除钩子    endend

获取函数信息

正如前面提到过的,因为函数名称信息是不确定的,所以我们修改了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 infoend

钩子函数的改动主要是在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    endend

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

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

local ldb = require "luadebug"local setbp = ldb.setbreakpointlocal rmbp = ldb.removebreakpoint local function foo()    local a = 0end local function bar()    local a = 0end local function pee()    local a = 0end 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.luaLua (local)foo test.lua:6lua_debug> contLua (local)foo test.lua:7lua_debug> contLua (local)bar test.lua:10lua_debug> contLua (local)bar test.lua:11lua_debug> contLua (local)pee test.lua:14lua_debug> contLua (local)pee test.lua:15		# 第一次调用,每个函数碰到两个断点lua_debug> cont					Lua (local)foo test.lua:7lua_debug> contLua (local)bar test.lua:11lua_debug> contLua (local)pee test.lua:15		# 第二次调用,每个函数碰到一个断点lua_debug> cont$						# 第三次调用,不再碰到断点

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK