前面两篇我们对性能做了一个优化,接下来继续来丰富调试器的特性。

我们前面提到过,函数内并不是所有行都是有效行,空行和注释行就不是有效行。我们之前在添加断点的时候,并没有对行号进行检查,任何行号都能成功添加断点。所以如果添加的断点行号是无效的,那么永远也不会断到那里。但是钩子里并不知道它是无效的,call事件仍然会以为函数有断点从而启动line事件,造成CPU的浪费。

所以本篇,我们将对断点的行号进行检查,对于不在函数范围内的行号直接添加断点失败;在函数范围内的行号则自动修正为下一个有效的行号;另外支持不指定行号,默认为函数的第一个有效行。

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

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

添加断点

因为是断点行号相关的检查,所以修改主要集中在添加断点的函数中。首先因为支持了不指定行号,所以修改了参数检查的地方允许为空。其次,因为要检查行号是否有效,我们就需要先获取到函数的信息。考虑到在钩子函数中也需要获取函数信息,我们就把相关的操作封装成了一个单独的函数getfuncinfo()。获取到函数信息之后,就可以验证行号是否有效了,同样我们将这个验证行号的操作也封装成了一个单独的函数verifyfuncline

local function setbreakpoint(func, line)
local s = status
if type(func) ~= "function" or ( line and type(line) ~= "number") then
io.write("invalid parameter\n")
return nil
end -- get func info
local info = getfuncinfo(func)
if not info then
io.write("unable to get func info\n")
return nil
end -- verify the line
line = verifyfuncline(info, line)
if not line then
io.write("invalid line\n")
return nil
end -- 省略
end

获取函数信息

getfuncinfo函数的代码如下:

local function getfuncinfo (func, level)
local s = status
local info = s.funcinfos[func]
if not info then
if level then
s.funcinfos[func] = debug.getinfo(level + 1, "nSL")
else
s.funcinfos[func] = debug.getinfo(func, "SL")
end
info = s.funcinfos[func]
info.sortedlines = {}
for k, _ in pairs(info.activelines) do
table.insert(info.sortedlines, k)
end
table.sort(info.sortedlines)
elseif level then -- name和namewhat需要实时获取
local nameinfo = debug.getinfo(level + 1, "n")
info.name = nameinfo.name
info.namewhat = nameinfo.namewhat
end
return info
end

该函数有两个参数,第一个参数就是函数,第二个可选的参数level用于指定在调用栈中的层数,第二个参数只有在钩子函数中时才会指定,返回值就是函数信息。如果在调用debug.getinfo的时候传递函数作为参数,那么是获取不到函数的名字信息的,namenamewhat字段都为空。因为函数可能是任意名字,Lua需要通过查找调用该函数的代码,知道它是怎么被调用的,从而确定函数的名字。所以只有当指定调用栈的层数时才能获取到名字信息。

我们接着看代码的主体部分:

首先尝试去s.funcinfos表中查找是否有缓存的函数信息。如果没有那就只能调用debug.getinfo去获取了,这里分为两种情况,如果指定了level参数,那么就以层数(这里+1同样是为了修正层数,我们在前面多次提到过)作为参数调用,此时第二个参数设置为了"nSL",比之前多了"L"用于获取有效行号;如果没有指定level参数,则以函数作为参数调用。获取到函数信息之后,为了方便我们后面的行号检查,我们对有效的行号进行了排序,info.sortedlines数组就是排序后的有效行号,然后就返回函数信息info了。

如果缓存中已经有函数信息了,如果本次调用又指定了level参数,那么我们就更新下name信息。调用debug.getinfo获取到信息之后设置到原有的info表中。完成之后同样是返回函数信息info

检查及修正函数行号

verifyfuncline函数的代码如下:

local function verifyfuncline (info, line)
if not line then
return info.sortedlines[1]
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

该函数有两个参数,其中第二个行号是可选的。如果没有指定行号,那么直接返回函数的第一个有效行号。如果指定了行号,但是范围超出了函数定义的范围,那么返回nil。如果行号落在函数范围内,那么就遍历已经排好序的有效行号数组,返回碰到的第一个大于等于指定行号的值。

钩子函数

接下来看下钩子函数的修改,因为我们已经封装了getfuncinfo函数,所以钩子函数中也改成用它来获取函数信息。不过这里在调用的时候指定了level从而可以获取到函数名字信息。

local function hook (event, line)
-- 省略
elseif event == "line" then
local curfunc = s.stackinfos[s.stackdepth].func
local funcbp = s.funcbpt[curfunc]
assert(funcbp)
if funcbp[line] then
local info = getfuncinfo(curfunc, 2)
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

OK,代码修改完了,我们进行测试。

测试有效行排序

首先测试一下,有效行号排序那块的逻辑。我们编写了一个如下的测试脚本:

local debug = require "debug"

local function foo()
local a = 0 a = a + 1 a = a + 1
end local function bar() end local function sortlines(func)
local info = debug.getinfo(func, "nSL")
info.sortedlines = {}
for k, v in pairs(info.activelines) do
print(k, v)
table.insert(info.sortedlines, k)
end table.sort(info.sortedlines) for k, v in ipairs(info.sortedlines) do
print(k, v)
end
end print("foo")
sortlines(foo)
print("bar")
sortlines(bar)

我们定义了两个函数foo和bar,其中foo函数的范围为第3行到第9行,有4个有效行4、6、8、9。而bar函数则为特殊的单行函数。

运行脚本,输出如下

$ lua sortlines.lua
foo
4 true
9 true
6 true
8 true
1 4
2 6
3 8
4 9
bar
11 true
1 11

foo函数4个有效行没排之前是4、9、6、8,排序之后变成4、6、8、9。bar函数唯一的有效行就是它开始定义的那行。

测试行号检查和自动修正

编写测试脚本如下:

local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint local function foo()
local a = 0 a = a + 1 a = a + 1
end local id1 = setbp(foo)
assert(id1 == 1)
local id2 = setbp(foo, 5)
assert(id2 == id1)
local id3 = setbp(foo, 6)
assert(id3 == id1)
local id4 = setbp(foo, 7)
assert(id4 == 2)
local id5 = setbp(foo, 8)
assert(id5 == id4)
local id6 = setbp(foo, 9)
assert(id6 == 3)
local id7 = setbp(foo, 100)
assert(not id7) foo() rmbp(id1)
rmbp(id4) foo() rmbp(id6) foo()

我们在foo函数上添加了好几个断点,第一个断点行号省略,第二个断点加在了第5行,也就是函数开始定义的行,第三个断点加在了第6行,这是函数第一个有效行。预期前三次添加断点应该都返回同一个断点id,断在第6行。接下来添加的两个断点,第7行不是有效行,第8行是有效行,预期返回同一个断点id,断在第8行。然后在第9行添加了一个断点,因为不是有效行,预期断在第10行。最后一个在第100行设置了一个断点,因为超出了函数的范围,预期设置断点失败返回nil

设置好断点,先调用一次foo函数,然后删除两个断点,在调用一次foo函数,最后将剩余那个断点删除,再调用一次foo函数。

我们了运行下测试脚本

$ lua test.lua
invalid line
Lua (local)foo test.lua:6
lua_debug>

断点的设置都符合预期,最后一个因为行号超出了范围,打了一行错误日志invalid line,程序停在了第6行处。然后我们输入两个cont,程序停在了最后一个断点处。

Lua (local)foo test.lua:6
lua_debug> cont
Lua (local)foo test.lua:8
lua_debug> cont
Lua (local)foo test.lua:10
lua_debug>

我们再次输入cont,foo函数运行结束,此时因为前两个断点已经被删除,第二次调用foo函数应该直接停在断点3处,也就是第10行

Lua (local)foo test.lua:6
lua_debug> cont
Lua (local)foo test.lua:8
lua_debug> cont
Lua (local)foo test.lua:10
lua_debug> cont
Lua (local)foo test.lua:10
lua_debug>

我们再次输入cont,因为最后一个断点也被删除了,所以最后一个执行foo函数没有再碰到断点。

$ lua test.lua
invalid line
Lua (local)foo test.lua:6
lua_debug> cont
Lua (local)foo test.lua:8
lua_debug> cont
Lua (local)foo test.lua:10
lua_debug> cont
Lua (local)foo test.lua:10
lua_debug> cont
$

Lua中如何实现类似gdb的断点调试—06断点行号检查与自动修正的更多相关文章

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  9. phpstorm开启xdebug断点调试,断点调试不成功来这里

    感谢一下两篇博主的文章 其他的就... https://paper.seebug.org/308/ https://www.cnblogs.com/jice/p/5064838.html 首先安装xd ...

随机推荐

  1. 分布式系统及CAP理论

    一.集中式系统 在学习分布式之前,先了解一下与之相对应的集中式系统是什么样的. 集中式系统用一句话概括就是:一个主机带多个终端.终端没有数据处理能力,仅负责数据的录入和输出.而运算.存储等全部在主机上 ...

  2. CSS样式表的书写位置

    行内式(内联样式) 是通过标签的style属性来设置元素的样式,其基本语法格式如下: <标签名 style="属性1:属性值1; 属性2:属性值2; 属性3:属性值3;"&g ...

  3. web容器、sevlet容器、spring容器、springmvc容器之间的关系

    原文链接:http://www.cnblogs.com/jieerma666/p/10805966.html https://blog.csdn.net/zhanglf02/article/detai ...

  4. MySQL 数据库的tab 补全功能 (懒人必备)

    MySQL 数据库的tab补全功能                      跟着步骤走~~ 懒人养成第一步 不仅帮你补全 甚至预判你的预判,就问你可怕不可怕 1.安装相关依赖软件(需要配置yum官方 ...

  5. LAMP以及各组件的编译安装

    LAMP以及各组件的编译安装 目录 LAMP以及各组件的编译安装 一.LAMP 1. LAMP概述 2. 各组件的主要作用 3. 平台环境的安装顺序 二.编译安装apache httpd 1. 关闭防 ...

  6. Shell循环练习题

    Shell循环练习题 目录 Shell循环练习题 1.计算从1到100所有整数的和 2.提示用户输入一个小于100的整数,并计算从1到该数之间所有整数的和 3.求从1到100所有整数的偶数和.奇数和 ...

  7. 最全Java架构师130面试题:微服务、高并发、大数据、缓存等中间件

    一.数据结构与算法基础 · 说一下几种常见的排序算法和分别的复杂度. · 用Java写一个冒泡排序算法 · 描述一下链式存储结构. · 如何遍历一棵二叉树? · 倒排一个LinkedList. · 用 ...

  8. Latex公式导出word,Latex转换MathML使用POI导出公式可编辑的Word文件

    背景 之前在 使用spire.doc导出支持编辑Latex公式的标准格式word 博客中写过,使用spire.doc来生成word,不得不说spire.doc的api操作起来还是比较方便,但是使用的过 ...

  9. 6.Flink实时项目之业务数据分流

    在上一篇文章中,我们已经获取到了业务数据的输出流,分别是dim层维度数据的输出流,及dwd层事实数据的输出流,接下来我们要做的就是把这些输出流分别再流向对应的数据介质中,dim层流向hbase中,dw ...

  10. python3发邮件脚本

    官方文档中建议保存token,且token是每2小时更新一次. 所以token先保存在本地token.txt文件夹中,设定计划任务每1小时删除一下token.txt.虽然造成了浪费,对于发消息不多的人 ...