前面已经支持了几种不同的方式添加断点,但是必须事先在代码中添加断点,在使用上不是那么灵活方便。本文将支持动态增删断点,只需要开一开始引入调试库即可,后续可以在调试过程中动态的添加和删除断点。事不宜迟,我们直接进入正题。

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

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

实现分析

入口断点

尽管我们目标是支持动态添加断点,但还是需要一个入口,提供用户添加初始的断点。仍然像之前一样,在用户代码中显式添加的确可以,但显然不是我们想要效果。理想的效果就是用户开始调试程序,就自动停在入口处,等待用户输入交互信息,就像gdb那样。

因为引入调试库这个动作是肯定要做的,所以最方便的方式就是在引入这个库的时候就直接停到入口断点。我们可以在调试库中实现一个init方法,在require这个调试库之后调用init进入调试入口,类似下面这样

require("luadebug").init()

用户代码中只需要添加这样一行,无需其他任何改动,后续就可以交互模式中动态添加断点了。

支持动态添加断点

要在交互模式中动态添加断点,我们的接口函数如添加断点函数、删除断点函数就需要在交互模式的作用域中可见,所以需要将公共接口函数放到_G_ENV中。但是放到这样的全局表中,可能出现名字冲突的情况,需要支持通过参数自定义接口函数的名称。

支持了动态断点之后,原本在call事件中判断函数是否有断点并记录在status.stackinfos中,然后在return事件中查询该值的机制就失效了。因为随时可以动态增删断点,所以在call和return事件都需要实时进行判断,然后根据结果决定是否添加或删除line事件。

另外为了方便添加断点,扩展断点添加函数以支持用"."表示当前函数或当前包。

支持动态删除断点

要支持动态删除断点,需要添加一个断点打印函数以查看当前的断点情况。

钩子函数

首先来看钩子函数,因为需要支持动态增删断点,所以call和return事件需要相应修改。先看call事件改动,updatehookevent函数把之前根据函数信息判断是否有断点,并调整line事件的逻辑给封装起来了,因为现在在return事件中也需要进行这些操作。而status.stackinfos中则不再缓存hasbreak,因为支持动态添加断点后,需要实时判断了。

local function hook (event, line)
local s = status
if event == "call" or event == "tail call" then
-- level 2: hook, target func
local sinfo = debug.getinfo(2, "nf")
local finfo = updatehookevent(sinfo)
if event == "call" then -- for tail call, just overwrite
s.stackdepth = s.stackdepth + 1
end
s.stackinfos[s.stackdepth] =
{stackinfo = sinfo, funcinfo = finfo}
-- 省略...
end
end

然后来看return事件的改动。s.stackinfos中把当前函数出栈,这还是跟之前一样。然后如果已经删除了所有断点,那么将钩子函数移除,并清空s.stackinfos缓存。如果栈中还有函数,则调用updatehookevent函数,这里的参数是即将返回的函数的信息。

local function hook (event, line)
-- 省略...
elseif event == "return" or event == "tail return" then
if s.stackdepth > 0 then
s.stackinfos[s.stackdepth] = nil
s.stackdepth = s.stackdepth - 1
end
if s.bpnum == 0 then
debug.sethook()
s.stackinfos = {}
s.stackdepth = 0
end
if s.stackdepth > 0 then
updatehookevent(s.stackinfos[s.stackdepth].stackinfo)
end
-- 省略...
end

我们来看下updatehookevent的实现:

function updatehookevent(stackinfo)
local s = status
local func = stackinfo.func
local name = stackinfo.name
local funcinfo = getfuncinfo(func)
local hasbreak = false
-- check unsolved srcbp
solvesrcbp(funcinfo, func) if funcinfo.what ~= "C" then
setsrcfunc(funcinfo, func)
end 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 type(k) == "number" and ((k >= min and k <= max) or k == 0) then
hasbreak = true
break
end
end
end
-- 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
end return funcinfo
end

大部分都是之前的call事件中干的事情,首先检查srcbpt中是否有当前包中未解析的断点,然后判断当前函数时候有断点,有断点打开line事件,没有断点移除line事件。

初始化函数

我们分成三个部分来看初始化函数,首先第一部分是将函数注册到全局表_G

local function init(name_table)
local s = status
if not _G.luadebug_inited then
_G.luadebug_inited = true
if name_table and type(name_table) == "table" then
if hasdupname(name_table) then
return
end
_G[name_table[1]] = setbreakpoint
_G[name_table[2]] = removebreakpoint
_G[name_table[3]] = printvarvalue
_G[name_table[4]] = setvarvalue
_G[name_table[5]] = printtraceback
_G[name_table[6]] = printbreakinfo
_G[name_table[7]] = help
hascustomnames = true
customnames = name_table
else
if hasdupname(longnames) then
return
end
if hasdupname(shortnames) then
return
end
_G.setbreakpoint = setbreakpoint
_G.removebreakpoint = removebreakpoint
_G.printvarvalue = printvarvalue
_G.setvarvalue = setvarvalue
_G.printtraceback = printtraceback
_G.printbreakinfo = printbreakinfo
_G.help = help
-- short names
_G.b = setbreakpoint
_G.d = removebreakpoint
_G.p = printvarvalue
_G.bt = printtraceback
_G.s = setvarvalue
_G.i = printbreakinfo
_G.h = help
end
end
-- 省略...
end

可选参数name_table用于指定自定义的函数名称。我们添加了一个标记luadebug_inited表示是否已经初始化了全局表。如果还没有则进行注册,如果提供了自定义的函数名,则注册自定义的,否则注册默认的函数名。注册前使用hasdupname函数检查_G表中是否已经有了同名的成员,如果有则终止注册,函数返回。

接着看函数第二部分,这部分在输出一些提示信息后debug.debug进入交互模式,这就是我们第一个入口断点,可以在这里添加一些初始的断点。

local function init(name_table)
-- 省略...
io.write(string.format("luadebug %s start ...\n", version))
if hascustomnames then
io.write("input '" .. customnames[7] .. "()' for help info or '"
.. customnames[7] .. "(1)' for verbose info\n")
else
io.write("input 'help()' for help info or 'help(1)' for verbose info\n")
end local sinfo = debug.getinfo(2, "nfl")
local func = sinfo.func
local name = sinfo.name
local finfo = getfuncinfo(func)
local prompt = string.format("%s (%s)%s %s:%d\n",
finfo.what, sinfo.namewhat, name, finfo.short_src, sinfo.currentline)
io.write(prompt)
debug.debug()
-- 省略...
end

接下来看函数的第三部分,这部分可能不太好理解。我们的status.stackinfos是用于缓存调用栈的函数信息的,call事件时入栈,return事件时出栈,我们依赖这个缓存的函数信息来决定是否添加line事件。但是在sethook函数启动钩子之前已经在调用栈中的函数,我们是没有缓存的函数信息的,也就造成即使我们在这些函数上添加了断点,也没有办法真正断到那里。

解决办法有两个:一个是不使用缓存,每次都debug.getinfo实时获取调用栈中的函数信息。这样虽然简单,但是性能有一定损失。第二个办法就是我们在第一次调用sethook函数前,把缺失的调用栈函数信息手动补上去。

原先我们是在添加第一个断点时,debug.sethook启动钩子函数,因为我们有多个断点添加函数,且存在潜嵌套调用的情况,所以如果在断点设置函数中处理代码上会有重复,而且debug.getinfo在层数上时不确定的,所以我们决定在init函数中干这个事情。

local function init(name_table)
-- 省略...
if s.bpnum > 0 then
if s.stackdepth == 0 then -- set hook
local max_depth = 2
while ( true ) do
if not debug.getinfo(max_depth, "f") then
max_depth = max_depth - 1
break
end
max_depth = max_depth + 1
end
-- init stackinfos
for i=max_depth, 1, -1 do
s.stackdepth = s.stackdepth + 1
local sinfo = debug.getinfo(i, "nf")
local func = sinfo.func
local finfo = getfuncinfo(func)
s.stackinfos[s.stackdepth] =
{stackinfo = sinfo, funcinfo = finfo}
end
-- add sethook
s.stackdepth = s.stackdepth + 1
s.stackinfos[s.stackdepth] =
{stackinfo = {name = "sethook", func = debug.sethook},
funcinfo = getfuncinfo(debug.sethook)}
debug.sethook(hook, "cr")
end
end
-- 省略...
end

首先检查是否添加了断点,如果没有断点不需要添加钩子函数。然后检查当前s.stackdepth是否为0,这是考虑到init函数可能被多次调用的情况,只有第一次才需要手动补调用栈信息。接下来的while循环是为了探测调用栈的深度,之所以不使用固定值,是考虑到调用init函数的也不一定就是最外层。然后从栈的最深处开始一层一层添加,最后再补上sethook函数本身。补充完status.stackinfos信息后就可以调用debug.sethook设置钩子函数了。

既然我们在init函数中sethook了,那么之前设置断点函数中的sethook就都可以去掉了。

断点打印函数

断点打印函数非常简单,只是遍历status.bptable表,打印断点信息,对应通过函数名字添加的断点打印名字及行数,其余断点打印包名及行数。

local function printbreakinfo()
local s = status
for i=1,s.bpid do
local bp = s.bptable[i]
local prompt
if bp then
if bp.name then
prompt = string.format("id: %d, name: %s, line: %d\n",
i, bp.name, bp.line)
else
prompt = string.format("id: %d, src: %s, line: %d\n",
i, bp.src, bp.line)
end
io.write(prompt)
end
end
end

其他

help帮助函数,以及扩展断点添加函数支持用"."表示当前函数或当前包,我就不专门讲了。另外,既然我们的接口函数已经支持在交互模式中动态调用了,那么也就不需要再导出了,模块只需要导出init函数即可。

return {
init = init,
}

测试

我们编写一个如下的Lua测试脚本

require("luadebug").init()
local lib = require "testlib" local g = 1
local function faa ()
g = 2
end faa()
lib.foo()
lib.bar()
faa()

测试包还是跟之前一样

local function foo ()
local a = 1
end local function bar()
local a = 1
end local a = 1 return {
foo = foo,
bar = bar,
}

入口脚本中断点测试

首先测试仅在入口脚本中添加断点

$ lua dynamictest.lua
luadebug 0.0.1 start ...
input 'help()' for help info or 'help(1)' for verbose info
main ()nil dynamictest.lua:1
lua_debug>

我们添加两个断点,一个是当前包的第7行,及faa函数的最后一行,一个是当前函数即mainchunk的第9行

lua_debug> b(".:7")
lua_debug> b(".@9")
lua_debug> i()
id: 1, src: dynamictest.lua, line: 7, refname: nil
id: 2, src: dynamictest.lua, line: 9, refname: main
lua_debug>

我们继续执行,首先停在了mainchunk的第9行,此时g的值为1,继续执行,又停在了faa的第7行,此时g已经改为2

lua_debug> cont
main ()nil dynamictest.lua:9
lua_debug> p("g")
local 1
lua_debug> cont
Lua (local)faa dynamictest.lua:7
lua_debug> p("g")
upvalue 2
lua_debug> i()
id: 1, src: dynamictest.lua, line: 7, refname: faa
id: 2, src: dynamictest.lua, line: 9, refname: main
lua_debug>

此时我们删除两个断点,再次继续执行,程序不再停到faa上

lua_debug> d(1)
lua_debug> d(2)
lua_debug> i()
lua_debug> cont
$

其他包中断点测试

接着测试下在testlib包中添加断点,首先启动调试,添加两个断点

$ lua dynamictest.lua
luadebug 0.0.1 start ...
input 'help()' for help info or 'help(1)' for verbose info
main ()nil dynamictest.lua:1
lua_debug> b("testlib:-9")
lua_debug> b("foo@")
lua_debug> i()
id: 1, src: /usr/local/share/lua/5.3/testlib.lua, line: -9, refname: nil
id: 2, name: foo, line: 0
lua_debug>

继续执行,程序首先停在了testlib的mainchunk第9行,我们在这里添加faa的断点

lua_debug> cont
main ()nil /usr/local/share/lua/5.3/testlib.lua:9
lua_debug> i()
id: 1, src: /usr/local/share/lua/5.3/testlib.lua, line: 9, refname: main
id: 2, name: foo, line: 0
lua_debug> b("faa@")
lua_debug>

继续执行,程序先停在faa函数,然后停在foo函数,最后听到faa函数。

lua_debug> cont
Lua (local)faa dynamictest.lua:6
lua_debug> cont
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:2
lua_debug> cont
Lua (local)faa dynamictest.lua:6
lua_debug> cont

多次初始化测试

我们在testlib包开头添加一行require("luadebug").init()。首先一样停在了dynamictest.lua中的入口断点处,我们添加两个断点。

$ lua dynamictest.lua
luadebug 0.0.1 start ...
input 'help()' for help info or 'help(1)' for verbose info
main ()nil dynamictest.lua:1
lua_debug> b("faa@")
lua_debug> b("foo@")
lua_debug> i()
id: 1, name: faa, line: 0
id: 2, name: foo, line: 0
lua_debug>

然后继续执行,发现程序停到了testlib的入口断点处,断点情况正常

lua_debug> cont
luadebug 0.0.1 start ...
input 'help()' for help info or 'help(1)' for verbose info
main ()nil /usr/local/share/lua/5.3/testlib.lua:1
lua_debug> bt()
stack traceback:
/usr/local/share/lua/5.3/testlib.lua:1: in main chunk
[C]: in function 'require'
dynamictest.lua:2: in main chunk
[C]: in ?
lua_debug> i()
id: 1, name: faa, line: 0
id: 2, name: foo, line: 0
lua_debug>

我们继续执行,停在了faa函数处,我们删除断点1,然后继续执行

lua_debug> cont
Lua (local)faa dynamictest.lua:6
lua_debug> d(1)
lua_debug> i()
id: 2, name: foo, line: 0
lua_debug>

程序停在了foo函数处,再继续因为faa函数处的断点1已经删除,所以程序直接结束。

lua_debug> cont
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:3
lua_debug> cont
$

仅在testlib包中初始化

我们删除dynamictest.lua中的第一行,继续测试,程序直接停在了testlib包的入口断点,我们同样添加两个断点。

lua dynamictest.lua
luadebug 0.0.1 start ...
input 'help()' for help info or 'help(1)' for verbose info
main ()nil /usr/local/share/lua/5.3/testlib.lua:1
lua_debug> b("faa@")
lua_debug> b("foo@")
lua_debug> i()
id: 1, name: faa, line: 0
id: 2, name: foo, line: 0
lua_debug>

继续执行,程序停在了faa函数处,我们删除断点1,然后继续执行

lua_debug> cont
Lua (local)faa dynamictest.lua:5
lua_debug> d(1)
lua_debug> i()
id: 2, name: foo, line: 0
lua_debug>

程序停在了foo函数处,再继续因为断点1已经删除,所以不再停在faa函数处,程序直接结束。

lua_debug> cont
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:3
lua_debug> cont
$

自定义函数名称及重名测试

我们在dynamictest.lua最前面添加一行:

d = 1

测试输出如下,提示错误之后没有进入交互模式。

$ lua dynamictest.lua
table `_G` already has element called "d" please specify custom names as the following example:
require("luadebug").init({"bb", "dd", "pp", "ss", "tt", "ii", "hh"})

我们再将第二行改为如下

require("luadebug").init({"bb", "dd", "pp", "ss", "tt", "ii", "hh"})

然后重新测试,可以看到函数名已经顺利修改

lua dynamictest.lua
luadebug 0.0.1 start ...
input 'hh()' for help info or 'hh(1)' for verbose info
main ()nil dynamictest.lua:2
lua_debug> bb("faa@")
lua_debug> bb("foo@")
lua_debug> ii()
id: 1, name: faa, line: 0
id: 2, name: foo, line: 0
lua_debug>

继续执行,一切正常。

lua_debug> cont
luadebug 0.0.1 start ...
input 'hh()' for help info or 'hh(1)' for verbose info
main ()nil /usr/local/share/lua/5.3/testlib.lua:1
lua_debug> cont
Lua (local)faa dynamictest.lua:7
lua_debug> dd(1)
lua_debug> cont
Lua (field)foo /usr/local/share/lua/5.3/testlib.lua:3
lua_debug> tt()
stack traceback:
/usr/local/share/lua/5.3/testlib.lua:3: in function 'testlib.foo'
dynamictest.lua:11: in main chunk
[C]: in ?
lua_debug> cont
$

Lua中如何实现类似gdb的断点调试—09支持动态添加和删除断点的更多相关文章

  1. Lua中如何实现类似gdb的断点调试--01最小实现

    说到Lua代码调试,最常用的方法应该就是加一堆print进行打印.print大法虽好,但其缺点也是显而易见的.比如效率低下,需要修改原有函数内部代码,在每个需要的地方添加print语句,运行一次只能获 ...

  2. Lua中如何实现类似gdb的断点调试--02通用变量打印

    在前一篇01最小实现中,我们实现了Lua断点调试的的一个最小实现.我们编写了一个模块,提供了两个基本的接口:设置断点和删除断点. 虽然我们已经支持在断点进行变量的打印,但是需要自己指定层数以及变量索引 ...

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

    在前面两篇01最小实现及02通用变量打印中,我们已经实现了设置断点.删除断点及通用变量打印接口. 本篇将继续新增两个辅助的调试接口:调用栈回溯打印接口.通用变量设置接口.前者打印调用栈的回溯信息,后者 ...

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

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

  5. Lua中如何实现类似gdb的断点调试—08支持通过包名称添加断点

    在前一篇中我们支持了通过函数名称来添加断点,我们同时也提到了在Lua中一个函数的名称的并不是确定的.准确的说,Lua中的函数并没有名称,所谓名称其实是保存这个函数值的变量的名称. 于是通过函数名称添加 ...

  6. Lua中如何实现类似gdb的断点调试--04优化钩子事件处理

    在第一篇的01最小实现中,我们实现了一个断点调试的最小实现,在设置钩子函数时只加了line事件,显然这会对性能有很大的影响.而后来两篇02通用变量打印和03通用变量修改及调用栈回溯则是提供了一些辅助的 ...

  7. Lua中如何实现类似gdb的断点调试--05优化断点信息数据结构

    在上一篇04优化钩子事件处理中,我们在钩子函数中引入了call和return事件的处理,对性能进行了优化. 细心的同学可能已经发现了,我们的hook函数中call事件和line都需要对整个断点表进行遍 ...

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

    前面两篇我们对性能做了一个优化,接下来继续来丰富调试器的特性. 我们前面提到过,函数内并不是所有行都是有效行,空行和注释行就不是有效行.我们之前在添加断点的时候,并没有对行号进行检查,任何行号都能成功 ...

  9. [Flex] Accordion系列-动态添加或删除Accordion容器中项目

    <?xml version="1.0" encoding="utf-8"?> <!--Flex中如何使用addChild()和removeCh ...

随机推荐

  1. JVM学习一:常用JVM配置参数

    原文链接:https://www.cnblogs.com/pony1223/p/8661219.html 在IDE的后台打印GC日志: 既然学习JVM,阅读GC日志是处理Java虚拟机内存问题的基础技 ...

  2. java多线程编程(一)

    一.概念基础 进程:正在进行中的程序(直译). 线程:就是进程中一个负责程序执行的控制单元(执行路径) 一个进程中可以多执行路径,称之为多线程. 一个进程中至少要有一个线程. 开启多个线程是为了同时运 ...

  3. Lvs+Keepalived+MySQL Cluster架设高可用负载均衡Mysql集群

    ------------------------------------- 一.前言 二.MySQL Cluster基本概念 三.环境 四.配置 1.LB-Master及LB-Backup配置 2.M ...

  4. Java判断是否是回文字符串

    public static boolean isPalindrome(String str) { int start = 0, end = str.length() - 1; while (start ...

  5. atc工具模拟网络

    通过Facebook开源的atc工具,进行模拟不同的网络情况,如图: 目前不支持python3 相关网址: ATC http://facebook.github.io/augmented-traffi ...

  6. python的namespace的理解

    Python命名空间的本质   python中的名称空间是名称(标识符)到对象的映射. 具体来说,python为模块.函数.类.对象保存一个字典(__dict__),里面就是重名称到对象的映射. -- ...

  7. 编译安装&打包压缩&定时任务

    内容概要 编译安装 打包压缩 定时任务 内容详细 一.编译安装 1.特点 使用源代码,编译打包软件. ​ 1.可以自定制软件 ​ 2.按需构建软件啊 2.步骤 下载安装包 wget 下载网址 如果没有 ...

  8. 聊聊几个阿里 P8、P9 程序员的故事

    大家好,我是对白. 阿里 P8 程序员年薪百万已经是公开的秘密了,有人关心他们年薪百万,而我更加关注阿里这些 P8.P9 程序员的成长故事,在聊这些大牛的故事之前,跟大家稍微简单聊下阿里技术人等级制度 ...

  9. 【一天一个小知识10/20】Unity通过www获取json文本信息。

    前提:领导要我在unity获取局域网服务器的文本信息.给了一个json的网络文本让我测试.我对于json以及服务器比较陌生.就直接去网上找相关的资料. 以下是自己测试的代码,没问题. 测试的网络jso ...

  10. 【C# 线程】并发编程的基石——CAS机制

    其实Java并发框架的基石一共有两块,一块是本文介绍的CAS,另一块就是AQS,后续也会写博客介绍. 什么是CAS机制 CAS机制是一种数据更新的方式.在具体讲什么是CAS机制之前,我们先来聊下在多线 ...