Lua中如何实现类似gdb的断点调试--04优化钩子事件处理
在第一篇的01最小实现中,我们实现了一个断点调试的最小实现,在设置钩子函数时只加了line事件,显然这会对性能有很大的影响。而后来两篇02通用变量打印和03通用变量修改及调用栈回溯则是提供了一些辅助的调试接口,并没有对钩子函数进行修改。
我们本篇将在钩子中引入call和return事件的处理,尝试对性能进行优化。
源码已经上传Github,欢迎watch/star。
本博客已迁移至CatBro's Blog,那里是我自己搭建的个人博客,欢迎关注。
实现分析
当前的实现因为只加了line事件,执行每一行代码都会执行钩子函数去查看是否有断点,这是没有必要的。我们可以在call事件时检查当前函数是否有断点,只有当有断点的时候才加入line事件。那我们什么时候去掉line事件呢?是不是遇到return事件就去掉呢?
考虑如下场景
考虑如下场景:假设f1调用f2,f2又调用f3。f1中有断点,f2没有断点,f3有断点。如果遇到return就去掉line事件,那么从f2返回到f1之后,就无法再停到f1后面的断点上了。
b b
f1 --> f2 --> f3
<-- <--
正确的做法
所以正确的做法应该是:call事件时,根据被调函数是否有断点,决定是否加line事件;return事件时,则根据主调函数是否有断点,决定是否加line事件。那么return时如何获取到主调函数的信息呢?我们就需要在call的时候保存函数的相关信息,组成一个链表。call的时候在尾部增加一个节点,return的时候则去掉一个节点。
数据结构
首先,在status数据结构中增加3个成员,stackinfos相当于我们前面提到的链表(只不过这里以数组的形式实现),维护了调用栈中每个函数的信息,记录其中是否有断点,stackdepth记录了链表的长度,或者说栈的深度。funcinfos用于缓存一些函数调试信息,不用每次都调用debug.getinfo去获取。
status.stackinfos = {} -- table for saving stack infos
status.stackdepth = 0 -- the depth of stack
status.funcinfos = {} -- table for caching func infos
钩子函数
我们本篇最主要的改动是钩子函数,除了line事件,我们还增加了call(或tail call)事件和return(或tail return)事件的处理。为了代码的简洁,用局部变量s来表示status。接下来我们分别来看这三个部分。
local function hook (event, line)
local s = status
if event == "call" or event == "tail call" then
-- 省略
elseif event == "return" or event == "tail return" then
-- 省略
elseif event == "line" then
-- 省略
end
end
call事件
如果是call事件(或tail call事件),那么先获取当前函数,查看是否在断点表中有断点。
如果有则在s.stackinfos表尾部插入一个元素,其中hasbreak字段为true指示该函数有断点。注意,这里我们对tail call进行了一个优化,直接覆盖上一层的节点,在递归尾调用时可以防止空间无限膨胀。(Lua5.1上因为没有tail call就无能为力了:)。然后重新设置钩子函数的事件,将call、return和line事件全加上了。
如果当前函数没有断点,同样在s.stackinfos表尾部插入一个节点,不过其中hasbreak字段为false指示该层没有断点。在设置的钩子事件中,则只保留了cr,将line事件移除了。这样就只有断点所在的函数内才会触发line事件,可以大幅提升性能。
local function hook (event, line)
local s = status
if event == "call" or event == "tail call" then
local func = debug.getinfo(2, "f").func
for _, v in pairs(s.bptable) do
-- 当前函数中有断点
if v.func == func then
if event == "call" then
s.stackdepth = s.stackdepth + 1
end
s.stackinfos[s.stackdepth] =
{func = func, hasbreak = true}
debug.sethook(hook, "crl") -- 添加"line"事件
return
end
end
-- 当前函数中没有断点
if event == "call" then
s.stackdepth = s.stackdepth + 1
end
s.stackinfos[s.stackdepth] = {func = func, hasbreak = false}
debug.sethook(hook, "cr") -- 移除"line"事件
elseif event == "return" or event == "tail return" then
-- 省略
end
return事件
接下来,我们来看return事件的处理。它首先删除s.stackinfos表尾部的节点,然后检查前一个节点的函数是否有断点,如果有则恢复line事件,否则移除line事件。
local function hook (event, line)
local s = status
if event == "call" or event == "tail call" then
-- 省略
elseif event == "return" or event == "tail return" then
s.stackinfos[s.stackdepth] = nil
s.stackdepth = s.stackdepth - 1
-- 如果上一层的函数有断点
if s.stackdepth > 0 and s.stackinfos[s.stackdepth].hasbreak then
debug.sethook(hook, "crl") -- 恢复"line"事件
else
debug.sethook(hook, "cr") -- 移除"line"事件
end
elseif event == "line" then
-- 省略
end
end
line事件
最后一部分是line事件的处理,跟之前没有太大的变化。它遍历断点表,如果匹配到断点则打印提示信息,然后进入用户交互模式。不过这里也做了一个小优化,将debug.getinfo获取的函数信息缓存到了status.funcinfos中,下一次就可以直接从缓存中获取到该函数的信息。
local function hook (event, line)
local s = status
if event == "call" or event == "tail call" then
-- 省略
elseif event == "line" then
for _, v in pairs(s.bptable) do
if v.func == s.stackinfos[s.stackdepth].func
and v.line == line then
if not s.funcinfos[v.func] then
s.funcinfos[v.func] = debug.getinfo(2, "nS")
end
local info = s.funcinfos[v.func]
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
end
初始事件设置
hook函数已经修改好了,我们再调整一下setbreakpoint函数中第一次设置钩子时的行为。初始只设置call事件。
local function setbreakpoint(func, line)
-- 省略
if s.bpnum == 1 then -- 只有一个断点
debug.sethook(hook, "c") -- 设置call事件
end
return s.bpid
end
复杂度分析
我们假设代码执行的总行数为L,断点数N=n*b,其中n为有断点的函数个数,b为平均每个函数的断点数,断点所在函数平均行数为l,断点所在函数平均调用次数为c,总的函数调用次数C。
那么优化前复杂度为O(L*N),优化后的复杂度为O(C*N+c*l*N)
一般情况下(C+c*I) << L,因为右边L代码执行总行数可以分成有断点的函数执行总行数+没有断点的函数执行总行数,而左边的c*I就是有断点的函数执行总行数,C为函数调用总次数。正常情况下函数调用总次数肯定是远远小于没有断点的函数执行的总行数的,有断点的函数执行总行数也是远远小于没有断点的函数执行总行数的。
测试断点是否正常
我们编写如下测试脚本,来测试下之前提到的那种场景:f1调用f2,f2又调用f3,f1中加了两个断点,在调用f2前后各有一个,f2没有断点,f3有断点。
local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
pv = ldb.printvarvalue
sv = ldb.setvarvalue
ptb = ldb.printtraceback
local function f3()
end
local function f2()
f3()
end
local function f1()
f2()
end
-- f3中加断点
local id1 = setbp(f3, 9)
-- f2不加断点
-- f1中在调用f2前后各加一个断点
local id2 = setbp(f1, 16)
local id3 = setbp(f1, 17)
f1()
rmbp(id1)
rmbp(id2)
rmbp(id3)
然后来运行测试脚本验证一下。首先停在了f1函数第16行(调用f2之前),然后cont继续执行,停在了函数f3的断点处,再次cont继续,函数停在了f1函数第17行(调用f2之后)。可见断点能正常工作
$ lua test.lua
Lua (local)f1 test.lua:16
lua_debug> cont
Lua (upvalue)f3 test.lua:9
lua_debug> cont
Lua (local)f1 test.lua:17
lua_debug> cont
测试tail call优化
我们再来测试下tail call的优化,编写如下测试脚本。我们定义了一个尾调用递归的函数foo,然后再其他函数上随便加了一个断点(为了设置hook)。然后我们foo函数一直递归调用。
local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
pv = ldb.printvarvalue
sv = ldb.setvarvalue
ptb = ldb.printtraceback
local function foo(n)
if n == 0 then
return 0
end
return foo(n-1)
end
local function bar()
end
-- add a break in bar
local id1 = setbp(bar, 16)
foo(100000000000)
rmbp(id1)
Lua5.1测试
$ lua5.1 test2.lua
用Lua5.1运行上面的测试脚本,内存占用一直在飙升,我只测试了一小会,就已经飙到8G了。

Lua5.3测试
$ lua5.3 test2.lua
用Lua5.3运行上面的测试脚本,因为有尾调用的优化,内存占用一直保持在720KB。

细心的同学可能已经发现了,我们的hook函数中call事件和line都需要对整个断点表进行遍历,这其中其实是存在着一些冗余的。因为篇幅原因,我们放到下回分解。
Lua中如何实现类似gdb的断点调试--04优化钩子事件处理的更多相关文章
- Lua中如何实现类似gdb的断点调试--05优化断点信息数据结构
在上一篇04优化钩子事件处理中,我们在钩子函数中引入了call和return事件的处理,对性能进行了优化. 细心的同学可能已经发现了,我们的hook函数中call事件和line都需要对整个断点表进行遍 ...
- Lua中如何实现类似gdb的断点调试--01最小实现
说到Lua代码调试,最常用的方法应该就是加一堆print进行打印.print大法虽好,但其缺点也是显而易见的.比如效率低下,需要修改原有函数内部代码,在每个需要的地方添加print语句,运行一次只能获 ...
- Lua中如何实现类似gdb的断点调试—09支持动态添加和删除断点
前面已经支持了几种不同的方式添加断点,但是必须事先在代码中添加断点,在使用上不是那么灵活方便.本文将支持动态增删断点,只需要开一开始引入调试库即可,后续可以在调试过程中动态的添加和删除断点.事不宜迟, ...
- Lua中如何实现类似gdb的断点调试--02通用变量打印
在前一篇01最小实现中,我们实现了Lua断点调试的的一个最小实现.我们编写了一个模块,提供了两个基本的接口:设置断点和删除断点. 虽然我们已经支持在断点进行变量的打印,但是需要自己指定层数以及变量索引 ...
- Lua中如何实现类似gdb的断点调试—07支持通过函数名称添加断点
我们之前已经支持了通过函数来添加断点,并且已经支持了行号的检查和自动修正.但是通过函数来添加断点有一些限制,如果在当前的位置无法访问目标函数,那我们就无法对其添加断点. 于是,本篇我们将扩展断点设置的 ...
- Lua中如何实现类似gdb的断点调试—08支持通过包名称添加断点
在前一篇中我们支持了通过函数名称来添加断点,我们同时也提到了在Lua中一个函数的名称的并不是确定的.准确的说,Lua中的函数并没有名称,所谓名称其实是保存这个函数值的变量的名称. 于是通过函数名称添加 ...
- Lua中如何实现类似gdb的断点调试--03通用变量修改及调用栈回溯
在前面两篇01最小实现及02通用变量打印中,我们已经实现了设置断点.删除断点及通用变量打印接口. 本篇将继续新增两个辅助的调试接口:调用栈回溯打印接口.通用变量设置接口.前者打印调用栈的回溯信息,后者 ...
- Lua中如何实现类似gdb的断点调试—06断点行号检查与自动修正
前面两篇我们对性能做了一个优化,接下来继续来丰富调试器的特性. 我们前面提到过,函数内并不是所有行都是有效行,空行和注释行就不是有效行.我们之前在添加断点的时候,并没有对行号进行检查,任何行号都能成功 ...
- linux下的gdb调试工具--断点调试
到目前为止我们的调试手段只有一种: 根据程序执行时的出错现象假设错误原因,然后在代码中适当的位置插入printf,执行程序并分析打印结果,如果结果和预期的一样,就基本上证明了自己假设的错误原因,就可以 ...
随机推荐
- 计算机电子书 2020 CDNDrive 备份(预览版 II)
下载方式 pip install CDNDrive # 或 # pip install git+https://github.com/apachecn/CDNDrive cdrive download ...
- 布客·ApacheCN 翻译/校对/笔记整理活动进度公告 2020.1
注意 请贡献者查看参与方式,然后直接在 ISSUE 中认领. 翻译/校对三个文档就可以申请当负责人,我们会把你拉进合伙人群.翻译/校对五个文档的贡献者,可以申请实习证明. 请私聊片刻(52981514 ...
- SpringBoot Log4j 安全漏洞分析及解决方案
一.序言 SpringBoot作为Java基础框架大行其道,前不久爆发出Log4j安全漏洞,大众更多关心Log4j的危害是多么严重,然而鲜有关心SpringBoot这一底层框架的安全性问题,换而言之, ...
- AT2402 [ARC072D] Dam
首先我们可以将 \(t_i \times v_i\) 看作一个整体,不妨令 \(x_i = v_i, y_i = t_i \times v_i\) 这样两堆水混合后相当于将两个维度相加,方便了计算. ...
- foreEach 跳出循环
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; const _ = require('lodash'); let outArr = []; try { arr. ...
- Android的基本资源引用(字符串、颜色、尺寸、数组)【转】
感谢大佬:https://blog.csdn.net/wenge1477/article/details/81295763 Android的基本资源引用(字符串.颜色.尺寸.数组)[转] 一.Andr ...
- Android中的多线程【转】
感谢大佬:https://www.cnblogs.com/zoe-mine/p/7954605.html 感谢大佬:https://blog.csdn.net/u014555121/article/d ...
- SlidingMenu addIgnoreView() 无效的bug解决方法
## 感谢大佬:https://blog.csdn.net/fuchaosz/article/details/51513288 1 简介 最近在做侧滑的时候用到了SlidingMenu,在MainAc ...
- axios 之cancelToken原理以及使用
看axios文档的时候发现cancelToken这个东东,这个是用来取消ajax请求的,一般原生的话用的是abort()这个方法.看到这玩意的第一感觉是用起来有点麻烦,但是看了内部实现,发现还是比较有 ...
- Java程序性能监控工具
系统性能监控: 确定系统运行的整体状态,基本定位问题所在 uptime命令 [root@localhost ~]# uptime23:19:38 up 244 days, 3:39, 34 users ...