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

我们前面提到过,函数内并不是所有行都是有效行,空行和注释行就不是有效行。我们之前在添加断点的时候,并没有对行号进行检查,任何行号都能成功添加断点。所以如果添加的断点行号是无效的,那么永远也不会断到那里。但是钩子里并不知道它是无效的,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. Linux性能优化实战(一)

    一.优化方向 1,性能指标 从应用负载的视角出发,考虑"吞吐"和"延时" 从系统资源的视角出发,考虑资源使用率.饱和度等 2,性能优化步骤 选择指标评估应用程序 ...

  2. Net6 DI源码分析Part2 Engine,ServiceProvider

    ServiceProvider ServiceProvider是对IServiceProvider实现,它有一个internal的访问修饰符描述的构造,并需要两个参数IServiceCollectio ...

  3. ExecutorService线程池简单使用

    简介 ExecutorService是Java中对线程池定义的一个接口,它位于java.util.concurrent包中,在这个接口中定义了和后台任务执行相关的方法. 常用方法 public < ...

  4. Swift中数据类型

    Swift类型的介绍 Swift中的数据类型也有:整型/浮点型/对象类型/结构体类型等等 先了解整型和浮点型 整型 有符号 Int8 : 有符号8位整型 Int16 : 有符号16位整型 Int32 ...

  5. php include,require,include_once,require_once 的区别

    include(),require(),include_once(),require_once()作用都是包含并运行指定文件,但是使用场景又有很大区别. 1.include()和require()的区 ...

  6. python办公自动化系列之金蝶K3自动登录(二)

    接上一篇博文python办公自动化系列之金蝶K3自动登录(一),我们接着聊聊利用python脚本实现金蝶K3 Wise客户端自动登录这一需求. 如上图所示,自动选择[组织机构]后,我们还需要驱动[当前 ...

  7. 在Excel VBA中使用SQL到底优势在哪儿

    小爬在之前的博文中多次提到,可以在VBA中写SQL来操作Excel文件,实现各类数据处理和分析需求.那么,你可能有这样的疑问:Excel原生的VBA,数据透视表,数据分析功能不够吗,为啥一定要用SQL ...

  8. vue中的钩子函数

    什么是vue的钩子函数? Vue 实例在被创建时,会经过一系列的初始化过程,初始化过程中会运行一些函数,叫做生命周期钩子函数,通过运用钩子函数,用户在可以在Vue实例初始化的不同阶段添加自己的代码,以 ...

  9. 简述CGI与FASTCGI区别

    CGI和FASTCGI都是服务器端与客户端进行交互的常见方式. CGI处理客户端请求,会生成一个子进程来专门调用外部程序来处理客户端请求,处理完成,子进程会随之关闭 FAST处理客户端请求时.服务器端 ...

  10. springboot学习第一步

    关于springboot的介绍就不多说了,可以去百度. 默认的情况下,springboot1.4.0版本要求Java7以上和spring4.3.2以上,当然你也可以使用java1.6,只不过你需要额外 ...