Lua中如何实现类似gdb的断点调试—08支持通过包名称添加断点
在前一篇中我们支持了通过函数名称来添加断点,我们同时也提到了在Lua中一个函数的名称的并不是确定的。准确的说,Lua中的函数并没有名称,所谓名称其实是保存这个函数值的变量的名称。
于是通过函数名称添加断点就造成了一定的不确定性,因为函数被调用时并不一定是以这个名字被调用的。另外,多个不同的函数也可能以相同的名字进行调用。
所以为了解决这个问题,本篇我们将继续扩展断点的设置接口,支持通过包名来添加断点。因为包名相对更具确定性,配合行号可以进行精确定位。
源码已经上传Github,欢迎watch/star。
本博客已迁移至CatBro's Blog,那是我自己搭建的个人博客,欢迎关注。本文链接
实现分析
为何选用包名的方式
其实一开始的想法并不是通过包名来添加断点,而是通过源文件名。但是源文件名同样存在重复的可能,我们只能考虑使用类似后缀匹配这种模糊的方式进行查找。这样的话,每个call事件中都需要对通过源文件名设置的断点表进行遍历。
使用绝对路径虽然可以解决重复的问题,但是使用上不方便,在添加断点的时候也不一定知道源文件的具体位置,况且short_src
也不一定是绝对路径。
所以出于性能及易用性的考虑,采用了通过包名来设置断点的方式,这样就可以利用Lua本身的包名搜索机制,使用搜索包路径的方法package.searchpackage()
将包名转成路径。而这个路径就是debug.getinfo
获取到的调试信息中的short_src
。
下面是一个例子:
package.searchpath( "foo.bar.baz", package.path )
--> (e.g.) "/usr/share/lua/5.3/foo/bar/baz.lua"
断点的保存解析和查找
跟上一篇中通过函数名称添加的断点不同的是,通过包名添加的断点都是确定的,所以最终都可以将其转换为函数的断点。但是只有当执行过该函数之后,我们才能获取到该函数的相关信息,进行断点的转换,所以在此之前我们需要将它们临时存放在一个表中,跟之前的status.funcbpt
、status.namebpt
类似,我们把这个表定义为status.srcbpt
。
当执行到该函数之后,我们就可以将status.srcbpt
中对应的断点转移到status.funcbpt
表中,后续就可以通过普通函数断点的方式进行处理了。
为了提升性能,我们可以将之前执行过的函数的信息保存下来,后续再(通过包名方式)在相同的函数中添加断点时,就可以快速地确定其所在函数,直接在添加断点的时候就转换为普通函数断点了,而不需要推迟到后面进行查找比对转换了。之前我们已经有了一个保存函数信息的表status.funcinfos
,但是那个表是以函数作为键,而这里我们需要通过源文件路径进行查找,所以需要新的数据结构。
解决断点歧义
通过包名添加断点存在着一种歧义的情况:当断点落在函数定义的范围之内时,它是表示对mainchunk中的函数声明添加断点,还是表示在该子函数执行时添加断点。(本质上是因为mainchunk的有效行和子函数有效行存在重叠)
所以为了优雅地解决这个歧义问题,我们使用行号的正负表示是在mainchunk中还是在子函数中添加断点。如果行号是正数,表示子函数,跟前几篇中的情况保持一致;如果行号是负数,则表示在mainchunk中添加断点。
好了,解决了最关键的几个问题,我们就可以开始着手写代码了
添加断点
同样依照惯例,我们先修改设置断点函数。它的改动较大,因为函数名称和包名称都是通过字符串参数指定,所以需要一个区分手段,我们这里采用了一个跟在名称后面的特殊字符来进行区分。如果后面跟的是@
,表示是函数名称,如果后面跟的是:
,表示是包名。
先来看通过包名添加断点的情况,首先查找标记符号:
,如果找到则前面部分表示包名,后面部分表示行号。切分之后,检查包名是否为空,再检查行号是否合法。如果没有指定行号,那么默认设置为-1,也就是mainchunk的第一行。接下来调用package.searchpath()
将包名转化为路径,如果找到了指定的包,再通过setsrcbp()
函数来设置断点。setsrcbp()
我们稍后介绍。
local function setbreakpoint(where, line)
-- 省略
if type(where) == "function" then
return setfuncbp(where, line)
else -- "string"
local i = string.find(where, ":")
if i then -- package name
local packname = string.sub(where, 1, i-1)
local line = string.sub(where, i+1)
if packname == "" then
io.write("no package name specified!\n")
return nil
end
if line ~= "" then
line = tonumber(line)
if not line then
io.write("no valid line number specified!\n")
return nil
end
else
line = -1
end
local path, err = package.searchpath(packname, package.path)
if not path then
io.write(err)
return nil
end
return setsrcbp(path, line)
else
end
通过函数名称添加断点的情况也是类似,不过省了路径转换的步骤。首先查找标记符号@
,然后切分函数名和行号,检查函数名是否为空,检查行号是否合法,都ok之后再交给setnamebp()
进行后面的工作。setnamebp()
函数我们在上一篇已经介绍过了。
local function setbreakpoint(where, line)
-- 省略
else
local i = string.find(where, "@")
if i then -- function name
local funcname = string.sub(where, 1, i-1)
local line = string.sub(where, i+1)
if funcname == "" then
io.write("no function name specified!\n")
return nil
end
if line ~= "" then
line = tonumber(line)
if not line then
io.write("no valid line number specified!\n")
return nil
end
else
line = nil
end
return setnamebp(funcname, line)
end
end
end
end
下面我们来看setsrcbp()
函数的实现,这个函数跟setnamebp()
大体上类似,开头部分稍有不同。首先会通过lookforfunc()
查看断点是否位于已知函数中,如果是的话返回该函数,然后直接调用setfuncbp()
函数作为函数断点处理。lookforfunc()
我们稍晚一点再介绍。后面的流程跟setnamebp()
并无二致就不再赘述。
local function setsrcbp(src, line)
local s = status
-- 检查断点是否位于已知函数中
local func = lookforfunc(src, line)
if func then
return setfuncbp(func, line)
end
local srcbp = s.srcbpt[src]
-- 检查相同的断点是否已经设置
if srcbp and srcbp[line] then
return srcbp[line]
end
-- 省略
end
钩子函数
钩子函数的改动都是在call事件中。首先,在获取到当前函数及其信息之后,调用solvesrcbp()
处理status.srcbpt
表中还未转换的断点,如果发现有位于当前函数中的断点,那么就进行相应的断点转换。接下来,如果当前函数不是C函数,就调用setsrcfunc()
函数保存函数信息。
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
-- 处理通过包名添加的还未转换的断点
solvesrcbp(funcinfo, func)
if funcinfo.what ~= "C" then
setsrcfunc(funcinfo, func)
end
if s.funcbpt[func] then
local id = s.funcbpt[func]
if s.bptable[id] and not s.bptable[id].src then
s.bptable[id].src = funcinfo.short_src
end
hasbreak = true
end
-- 省略
end
断点转换
接下来看下solvesrcbp()
函数的实现,如果当前源文件中存在未转换的断点,那么遍历这些断点,调用verifyfuncline()
判断断点是否在当前函数中,如果是的话就调用modsrcbp()
函数进行实际的转换操作。
local function solvesrcbp (info, func)
local s = status
local srcbp = s.srcbpt[info.short_src]
if srcbp then
for k, v in pairs(srcbp) do
if k ~= "num" then
line = verifyfuncline(info, k)
if line then
modsrcbp(info.short_src, func, k, line)
end
end
end
end
end
modsrcbp()
函数的实现如下,它有4个参数第一个src
是源文件路径、第二个func
是函数、第三个oline
是设置断点时的行号、第四个nline
是verifyfuncline()
进行修正后的行号。
该函数首先以src
和oline
为索引将断点从status.srcbpt
表中移除,然后设置到status.funcbpt
表中。如果同一个断点已经设置过了,那么将新添加的断点删除,然后返回旧的断点id。
local function modsrcbp(src, func, oline, nline)
local s = status
local srcbp = s.srcbpt[src]
local id = srcbp[oline]
-- 从srcbpt中移除
srcbp.num = srcbp.num - 1
srcbp[oline] = nil
if srcbp.num == 0 then
srcbp = nil
end
-- 设置funcbpt
local funcbp = s.funcbpt[func]
-- 检查是否已经设置了相同的断点
if funcbp and funcbp[nline] then
s.bptable[id] = nil -- 如果已经设置了,删除新加的断点
s.bpnum = s.bpnum - 1
assert(s.bpnum > 0)
return funcbp[nline] -- 返回旧的断点id
end
-- 省略
end
如果还未添加过这个断点,那么就在status.funcbpt
中添加该断点,然后将断点所在的函数和修正后的行号更新到s.bptable
表中。
local function modsrcbp(src, func, oline, nline)
-- 省略
if not funcbp then -- 该函数的第一个断点
s.funcbpt[func] = {}
funcbp = s.funcbpt[func]
funcbp.num = 0
end
funcbp.num = funcbp.num + 1
funcbp[nline] = id
-- 更新bptable中字段
s.bptable[id].func = func
s.bptable[id].line = nline
return id
end
行号扩展
因为我们对行号进行了扩展了,使用负数来表示mainchunk中的断点,所以verifyfuncline()
也需要进行相应的扩展。对于行号是负数的情况,如果不是mainchunk函数,直接返回nil,否则对行号取反还原为正常的行号。
local function verifyfuncline (info, line)
if not line then
return info.sortedlines[1]
end
if line < 0 then
if info.what ~= "main" then
return nil
end
line = -line
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
这里还有一个问题,通过debug.getinfo()
函数获取到的函数信息中,对于mainchunk的情况,linedefined
和lastlinedefined
字段的值都是0,于是我们的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)
-- mainchunk需要特殊处理以使`verifyfuncline`能够正常工作
if info.what == "main" then
info.linedefined = 1
info.lastlinedefined = info.sortedlines[#info.sortedlines]
end
end
s.funcinfos[func] = info
end
return info
end
对于mainchunk函数进行特殊处理,将linedefined
设置为1,将lastlinedefined
设置为最后一个有效行的行号。
缓存及查找源文件的函数
我们在钩子函数中调用setsrcfunc()
保存函数信息,将函数与源文件关联。在setsrcbp()
中调用lookforfunc()
通过源文件和行号查找对应的函数。先来看setsrcfunc()
函数实现:
local function setsrcfunc (info, func)
local s = status
local srcfunc = s.srcfuncmap[info.short_src]
if not srcfunc then
srcfunc = {}
s.srcfuncmap[info.short_src] = srcfunc
end
if not srcfunc[func] then
srcfunc[func] = info
end
end
其中status.srcfuncmap
就是我们新增的数据结构,它是一个以源文件路径为键的表,其值也是一个表,保存位于该源文件中的函数信息,以函数为键,以函数信息为值。
再来看lookforfunc()
函数的实现:
local function lookforfunc (src, line)
assert(line)
local srcfunc = status.srcfuncmap[src]
if srcfunc then
for func, info in pairs(srcfunc) do
if info.what == "main" then
if line < 0 then
return func
end
elseif line >= info.linedefined
and line <= info.lastlinedefined then
return func
end
end
end
return nil
end
lookforfunc()
函数中首先判断该源文件是否有缓存的函数信息,如果有则进行遍历。如果行号是负数,则只要找到mainchunk就可以返回了。否则,需要判断断点的行号是否在函数定义的范围内。找到了,就返回断点所在函数;没有找到返回nil
。
测试
首先,编写一个用于测试的包testlib.lua
,实现了两个简单的函数foo和bar。
local function foo ()
local a = 1
end
local function bar()
local a = 1
end
local a = 1
return {
foo = foo,
bar = bar,
}
测试通过函数名和包名添加断点
我们通过函数名添加了两个断点,其中一个省略行号,默认为函数第一个有效行。又通过包名添加了两个有效断点id3和id4,id5虽然能添加成功,但是并不落在有效函数范围内。id6和id7都是参数错误的情况,添加断点失败。
设置完断点,先分别调用foo和bar函数一次,预期都在foo函数第2行第3行会碰到断点,然后在bar函数的第6行第7行碰到断点。接着分别删除foo和bar函数中的1个断点,再分别调用foo和bar函数一次,预期在foo函数的第3行和bar函数的第7行碰到断点。最后删除foo和bar函数中的另一个断点,再分别调用foo和bar函数一次,预期不再碰到断点。
local ldb = require "luadebug"
local lib = require "testlib"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
local id1 = setbp("foo@") -- foo 2
local id2 = setbp("foo@3") -- foo 3
local id3 = setbp("testlib:5") -- bar 6
local id4 = setbp("testlib:7") -- bar 7
local id5 = setbp("testlib:100") -- invalid line
local id6 = setbp(":5")
assert(not id6)
local id7 = setbp("testlib:aa")
assert(not id7)
lib.foo(1) -- break twice
lib.bar(1) -- break twice
rmbp(id1)
rmbp(id3)
lib.foo(2) -- break once
lib.bar(2) -- break once
rmbp(id2)
rmbp(id4)
lib.foo(3) -- not break
lib.bar(3) -- not break
运行测试脚本,分别在foo函数和bar函数中碰到两个断点。
$ lua setbpbysrc.lua
no package name specified!
no valid line number specified!
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:2
lua_debug> cont
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:3
lua_debug> cont
Lua (field)bar /usr/local/share/lua/5.3/testlib.lua:6
lua_debug> cont
Lua (field)bar /usr/local/share/lua/5.3/testlib.lua:7
lua_debug>
继续执行,分别在foo函数和bar函数中碰到一个断点。
lua_debug> cont
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:3
lua_debug> cont
Lua (field)bar /usr/local/share/lua/5.3/testlib.lua:7
lua_debug>
继续执行,没有再碰到断点。
lua_debug> cont
$
测试行号扩展
编写如下测试脚本,在mainchunk中添加了4个断点,其中不指定行号时默认是mainchunk的第一个有效行。当require该包时,预期在mainchunk的第3行、第7行、第9行和第13行分别碰到断点。
接着在子函数foo中添加两个断点,调用foo函数,预期在foo函数第2行和第3行碰到断点。然后其中一个断点,再调用foo函数,预期在foo函数第3行碰到断点。最后删除剩余一个断点,再调用foo函数,预期不再碰到断点。
local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
local id1 = setbp("testlib:") -- main 3
local id2 = setbp("testlib:-5") -- main 7
local id3 = setbp("testlib:-9") -- main 9
local id4 = setbp("testlib:-13") -- main 13
local lib = require "testlib" -- break 4 times
local id5 = setbp("testlib:2") -- foo 2
local id6 = setbp("testlib:3") -- foo 3
lib.foo() -- break 2 times
rmbp(id5)
lib.foo() -- break 1 time
rmbp(id6)
lib.foo() -- not break
运行测试脚本,首先碰到了mainchunk中的4个断点
lua mainchunk.lua
main ()nil /usr/local/share/lua/5.3/testlib.lua:3
lua_debug> cont
main ()nil /usr/local/share/lua/5.3/testlib.lua:7
lua_debug> cont
main ()nil /usr/local/share/lua/5.3/testlib.lua:9
lua_debug> cont
main ()nil /usr/local/share/lua/5.3/testlib.lua:13
lua_debug>
继续执行,碰到foo函数中两个断点
lua_debug> cont
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:2
lua_debug> cont
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:3
lua_debug>
再继续执行,碰到foo函数中一个断点
lua_debug> cont
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:3
lua_debug>
再cont就结束了。
lua_debug> cont
$
测试断点重复及缓存源文件的函数
编写如下测试脚本,首先我们通过函数添加一个断点,然后再通过包名添加一个断点。虽然这两个实际是同一个断点,但是刚开始的时候因为还没有缓存信息,所以第二个断点也会添加成功。接着调用 foo函数,预期在foo函数第2行碰到断点。在钩子函数call事件中会处理未转换的断点2,因为已经存在同样的断点了,所以会将断点2删除。
接下来我们删除断点1之后,就没有断点了,再次调用foo函数,预期不会碰到断点。
然后我们再通过包名添加一个断点,这里因为已经有了对应的函数信息,所以预期会直接转换成对应的函数断点。当我们再对函数添加同样的断点的时候预期返回之前的断点号。
调用foo函数,预期在foo函数第3行碰到断点。删除断点3,预期不再碰到断点。
local ldb = require "luadebug"
local lib = require "testlib"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
local id1 = setbp(lib.foo)
local id2 = setbp("testlib:1") -- foo 2
lib.foo() -- break once
rmbp(id1)
lib.foo() -- not break
print("not break")
local id3 = setbp("testlib:3") -- foo 3
assert(id3 == 3)
local id4 = setbp(lib.foo, 3)
assert(id3 == id4)
lib.foo() -- break once
rmbp(id3)
lib.foo() -- not break
运行测试脚本,结果符合预期。
$ lua srcfuncmap.lua
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:2
lua_debug> cont
not break
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:3
lua_debug> cont
Lua中如何实现类似gdb的断点调试—08支持通过包名称添加断点的更多相关文章
- Lua中如何实现类似gdb的断点调试—07支持通过函数名称添加断点
我们之前已经支持了通过函数来添加断点,并且已经支持了行号的检查和自动修正.但是通过函数来添加断点有一些限制,如果在当前的位置无法访问目标函数,那我们就无法对其添加断点. 于是,本篇我们将扩展断点设置的 ...
- Lua中如何实现类似gdb的断点调试—09支持动态添加和删除断点
前面已经支持了几种不同的方式添加断点,但是必须事先在代码中添加断点,在使用上不是那么灵活方便.本文将支持动态增删断点,只需要开一开始引入调试库即可,后续可以在调试过程中动态的添加和删除断点.事不宜迟, ...
- Lua中如何实现类似gdb的断点调试--01最小实现
说到Lua代码调试,最常用的方法应该就是加一堆print进行打印.print大法虽好,但其缺点也是显而易见的.比如效率低下,需要修改原有函数内部代码,在每个需要的地方添加print语句,运行一次只能获 ...
- Lua中如何实现类似gdb的断点调试--02通用变量打印
在前一篇01最小实现中,我们实现了Lua断点调试的的一个最小实现.我们编写了一个模块,提供了两个基本的接口:设置断点和删除断点. 虽然我们已经支持在断点进行变量的打印,但是需要自己指定层数以及变量索引 ...
- Lua中如何实现类似gdb的断点调试--04优化钩子事件处理
在第一篇的01最小实现中,我们实现了一个断点调试的最小实现,在设置钩子函数时只加了line事件,显然这会对性能有很大的影响.而后来两篇02通用变量打印和03通用变量修改及调用栈回溯则是提供了一些辅助的 ...
- Lua中如何实现类似gdb的断点调试--05优化断点信息数据结构
在上一篇04优化钩子事件处理中,我们在钩子函数中引入了call和return事件的处理,对性能进行了优化. 细心的同学可能已经发现了,我们的hook函数中call事件和line都需要对整个断点表进行遍 ...
- Lua中如何实现类似gdb的断点调试—06断点行号检查与自动修正
前面两篇我们对性能做了一个优化,接下来继续来丰富调试器的特性. 我们前面提到过,函数内并不是所有行都是有效行,空行和注释行就不是有效行.我们之前在添加断点的时候,并没有对行号进行检查,任何行号都能成功 ...
- Lua中如何实现类似gdb的断点调试--03通用变量修改及调用栈回溯
在前面两篇01最小实现及02通用变量打印中,我们已经实现了设置断点.删除断点及通用变量打印接口. 本篇将继续新增两个辅助的调试接口:调用栈回溯打印接口.通用变量设置接口.前者打印调用栈的回溯信息,后者 ...
- chrome developer tool—— 断点调试篇
断点,调试器的功能之一,可以让程序中断在需要的地方,从而方便其分析.也可以在一次调试中设置断点,下一次只需让程序自动运行到设置断点位置,便可在上次设置断点的位置中断下来,极大的方便了操作,同时节省了时 ...
随机推荐
- asp.net core 中的各种路径
1.获取完整网址URL 方法一:先引用"using Microsoft.AspNetCore.Http.Extensions;",然后直接用"Request.GetDis ...
- vc获取进程版本号
#param comment(lib, "version.lib") CString &CMonitorManagerDlg::GetApplicationVersion( ...
- webpack引入css文件
需要配置 postcss 详见 官网 https://www.postcss.com.cn/
- js trim()方法
从字符串中移除前导空格.尾随空格和行终止符. 语法 stringObj.trim() 参数 stringObj 必选.String 对象或字符串.trim 方法不修改该字符串. 返回值 已移除前导空格 ...
- sublime中的emmet插件的使用技巧
1.我要生成一个2行3列,宽300px,高500px的表. table[width=300 height=500]>(tr>td{$}*3)*2
- MySQL高质量博文链接集合
1. 『浅入浅出』MySQL 和 InnoDB https://draveness.me/mysql-innodb.html
- LeetCode随缘刷题之回文数
package leetcode.day_01_30; /** * 给你一个整数 x ,如果 x 是一个回文整数,返回 true :否则,返回 false . * <p> * 回文数是指正 ...
- [USACO4.2]工序安排Job Processing
两种想法: (样例是真的良心,卡掉了两种错误做法)洗完一件马上塞一件到最快的空闲烘干机去?X,因为最后一件洗完的衣服决定了第二问的答案,但它并不一定得到最优待遇--最快的烘干机. 给最后一件洗完的 ...
- Dubbo源码剖析六之SPI扩展点的实现之getExtension
上文Dubbo源码剖析六之SPI扩展点的实现之getExtensionLoader - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)中分析了getExtensionLoader,本文继续分 ...
- python-利用faker模块生成测试数据
Python-利用faker模块生成测试数据 1.前言: Faker模块是一个生成伪数据的第三方模块,他提供了一系列方法,使用非常方便,在做自动化测试时,注册信息,用这个模块生成测试数据就体现了它的好 ...