在第一篇的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优化钩子事件处理的更多相关文章

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

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

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

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

  3. Lua中如何实现类似gdb的断点调试—09支持动态添加和删除断点

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

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

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

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

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

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

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

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

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

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

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

  9. linux下的gdb调试工具--断点调试

    到目前为止我们的调试手段只有一种: 根据程序执行时的出错现象假设错误原因,然后在代码中适当的位置插入printf,执行程序并分析打印结果,如果结果和预期的一样,就基本上证明了自己假设的错误原因,就可以 ...

随机推荐

  1. ApacheCN Linux 译文集(二) 20211206 更新

    CentOS7 Linux 服务器秘籍 零.前言 一.安装 CentOS 二.配置系统 三.管理系统 四.用 YUM 管理包 五.管理文件系统 六.提供安全性 七.构建网络 八.使用文件传输协议 九. ...

  2. ApacheCN jQuery 译文集 20211121 更新

    创建 jQueryMobile 移动应用 零.序言 一.jQueryMobile 原型制作 二.jQuery Mobile 网站 三.分析.长表单和前端验证 四.QR 码.地理位置.谷歌地图 API ...

  3. ByteArrayOutputStream内存流

    简介 ByteArrayOutputStream 对byte类型数据进行写入的类 相当于一个中间缓冲层,创建ByteArrayOutputStream类实例时,内存中会创建一个byte数组类型的缓冲区 ...

  4. JAVA面向对象复习

    对象:真实存在的唯一的事物. 类: 同一种类型的事物公共属性与公共行为的抽取. java面向对象语言: 核心思想: 找适合的对象做适合的事情. 找对象的方式: 方式一: sun已经定义好了很多了类,我 ...

  5. node.js中的fs.appendFile方法使用说明

    方法说明: 该方法以异步的方式将 data 插入到文件里,如果文件不存在会自动创建.data可以是任意字符串或者缓存. 语法: 代码如下: fs.appendFile(filename, data, ...

  6. 循环retian

    1.循环retian基本概念 循环retain的场景 比如A对象retain了B对象,B对象retain了A对象 循环retain的弊端 这样会导致A对象和B对象永远无法释放 循环retain的解决方 ...

  7. 高德地图定位api以及导航和定位 位置的偏差

    <script type="text/javascript" src="http://webapi.amap.com/maps?v=1.4.2&key=37 ...

  8. Docker镜像实战(ssh、systemctl、nginx、tomcat、mysql)

    Docker镜像实战 1.构建ssh镜像 2.构建systemctl 镜像 3.构建nginx镜像 4.构建tomcat镜像 5.构建mysql镜像 1.构建ssh镜像: 创建镜像目录 mkdir / ...

  9. VUE动态生成table表格(element-ui)(新增/删除)

    (直接复制即可测试) 结构(红色部分 data/prop/v-model 数据绑定): <template> <el-table size="small" :da ...

  10. 10、架构--keepalive、四层负载均衡

    笔记 1.晨考 1.HTTPS的作用,怎么实现的呢? 2.全栈部署HTTPS 只需在代理中部署HTTPS 3.反向代理 BBS 步骤 1.部署WEB机器 2.部署代理 4.如果 LB01 宕机了,怎么 ...