Lua中如何实现类似gdb的断点调试—07支持通过函数名称添加断点
我们之前已经支持了通过函数来添加断点,并且已经支持了行号的检查和自动修正。但是通过函数来添加断点有一些限制,如果在当前的位置无法访问目标函数,那我们就无法对其添加断点。
于是,本篇我们将扩展断点设置的接口,支持通过函数名称添加断点,以突破这个限制。
源码已经上传Github,欢迎watch/star。
本博客已迁移至CatBro's Blog,那是我自己搭建的个人博客,欢迎关注。本文链接
实现分析
由于Lua是动态类型语言,变量可以是任何值。而函数在Lua语言中又是第一类值,与其他值一样使用,可以被存放在变量中、作为参数或返回值传递。所以一个函数的名字是不确定的,它可能是任意名字,取决于函数调用时候的变量的名称。
通过下面这个简单的例子,就可以看出来
local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
local function foo()
end
setbp(foo, 6)
local bar = foo
foo()
bar()
我们在foo函数中添加了一个断点,将foo函数赋值给局部变量bar,然后分别用foo和bar调用函数。运行这个脚本结果如下:
$ lua namenotstable.lua
Lua (local)foo namenotstable.lua:6
lua_debug> cont
Lua (local)bar namenotstable.lua:6
lua_debug> cont
调用foo()
和bar()
都会碰到断点,函数名称分别为foo
和bar
。
所以通过函数名称添加的断点并不是确定的,函数名称和函数之间并不是一一映射的关系,而可能是m对n的关系。就算已经匹配到了一个与断点设置的函数名称一致的函数,我们也不能简单地将函数名称断点转换成相应的函数断点,而是仍然需要维护函数名称断点。
因此,我们需要增加一个维护函数名称断点的数据结构----新的断点表status.namebpt
。类似之前在05优化断点信息数据结构中添加的status.funcbpt
表,只是表的键由之前的函数变成了函数名称。status.namebpt
表的值同样是一个表,该表的键是断点行号,值为断点id。同样地,为了快速获取断点个数,我们在表中额外加了一个特殊的num
字段保存该函数名称中的断点个数。
通过下面的例子来直观地看一下,假设我们的bptable
表中添加了两个断点如下(name
字段用来保存函数名称):
bptable[1] = {name = "foo", line = 10}
bptable[2] = {name = "foo", line = 20}
对应的在namebpt
表中的操作如下:
namebpt["foo"] = {} -- 构造表
namebpt["foo"][10] = 1 -- 函数名foo,行号10,断点id为1
namebpt["foo"].num = 1 -- 该函数第一个断点
namebpt["foo"][20] = 2 -- 函数名foo,行号20,断点id为2
namebpt["foo"].num = namebpt["foo"].num + 1 -- 断点个数+1
OK,分析完了,接下来开始修改相应的代码实现。
添加断点
按照惯例,我们先修改设置断点函数。因为支持了通过函数名称设置断点,第一个参数需要支持string类型。为了简洁及代码重用,我们将之前通过函数设置断点的操作封装成了setfuncbp
函数,另外将通过函数名称设置断点的操作封装成了setnamebp
函数。
local function setbreakpoint(where, line)
if (type(where) ~= "function" and type(where) ~= "string")
or ( line and type(line) ~= "number") then
io.write("invalid parameter\n")
return nil
end
if type(where) == "function" then
return setfuncbp(where, line)
else -- "string"
return setnamebp(where, line)
end
end
接下来,来看下setnamebp
函数的实现:
local function setnamebp(name, line)
local s = status
local namebp = s.namebpt[name]
if not line then -- 如果没有指定行号
line = 0 -- 用一个特殊值0来表示第一个有效行
end
-- 是否已经添加了相同的断点
if namebp and namebp[line] then
return namebp[line]
end
s.bpid = s.bpid + 1
s.bpnum = s.bpnum + 1
s.bptable[s.bpid] = {name = name, line = line}
if not namebp then -- 该函数名称的第一个断点
s.namebpt[name] = {}
namebp = s.namebpt[name]
namebp.num = 0
end
namebp.num = namebp.num + 1
namebp[line] = s.bpid
if s.bpnum == 1 then -- 第一个全局断点
debug.sethook(hook, "c") -- 设置钩子函数的"call"事件
end
return s.bpid --> 返回断点id
end
因为我们支持不指定行号,但我们并不确定函数的第一个有效行是什么。为了方便地记录断点,又不至于与实际的断点行冲突,我们用了一个特殊值0来表示这种情况。
后续的逻辑与setfuncbp
函数基本一致,如果已经添加了相同的断点,则返回之前的断点id。然后分别在bptable
表和namebp
表中添加断点。这里不再赘述。
删除断点
删除断点函数的改动不大。主要是要区分删除的是哪类断点,这个可以通过s.bptable
表中id
所对应的断点信息来判断。如果有func则说明是通过函数添加的断点,否则则是通过函数名称添加的断点。根据情况删除s.funcbpt
或者s.namebpt
表中的断点,最后删除s.bptable
表中的断点。
local function removebreakpoint(id)
local s = status
if s.bptable[id] == nil then
return
end
local func = s.bptable[id].func
local name = s.bptable[id].name
local line = s.bptable[id].line
local dstbp = nil
if func then
dstbp = s.funcbpt[func]
else
dstbp = s.namebpt[name]
end
if dstbp and dstbp[line] then
dstbp.num = dstbp.num - 1
dstbp[line] = nil
if dstbp.num == 0 then
dstbp = nil
end
end
s.bptable[id] = nil
s.bpnum = s.bpnum - 1
if s.bpnum == 0 then
debug.sethook() -- 移除钩子
end
end
获取函数信息
正如前面提到过的,因为函数名称信息是不确定的,所以我们修改了getfuncinfo
函数实现,不再缓存函数名称信息,而只缓存确定的函数信息。
local function getfuncinfo (func)
local s = status
local info = s.funcinfos[func]
if not info then
info = debug.getinfo(func, "SL")
if (info.activelines) then
info.sortedlines = {}
for k, _ in pairs(info.activelines) do
table.insert(info.sortedlines, k)
end
table.sort(info.sortedlines)
end
s.funcinfos[func] = info
end
return info
end
钩子函数
钩子函数的改动主要是在call事件。函数名称每次都根据调用栈实时获取。首先在函数断点表s.funcbpt
中查找当前函数是否有断点,如果没有则再去函数名称断点表s.namebpt
中查找。需要检查断点行号是否在当前函数的定义范围之内,只有当行号在范围之内才认为匹配。如果没有指定行号的话(默认为第一个有效行),则总是认为匹配。另外,在调用栈信息表中,分别将确定的函数信息funcinfo
和调用栈相关信息stackinfo
分别保存,以供return事件和line事件时使用。
local function hook (event, line)
local s = status
if event == "call" or event == "tail call" then
local stackinfo = debug.getinfo(2, "nf")
local func = stackinfo.func
local name = stackinfo.name
local funcinfo = getfuncinfo(func)
local hasbreak = false
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 k ~= "num" and ((k >= min and k <= max) or k == 0) then
hasbreak = true
break
end
end
end
if event == "call" then -- for tail call, just overwrite
s.stackdepth = s.stackdepth + 1
end
s.stackinfos[s.stackdepth] =
{stackinfo = stackinfo, funcinfo = funcinfo, hasbreak = hasbreak}
-- 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 temporarily
end
elseif event == "return" or event == "tail return" then
-- 省略
end
line事件也需要做相应的修改
local function hook (event, line)
-- 省略
elseif event == "line" then
local sinfo = s.stackinfos[s.stackdepth].stackinfo
local finfo = s.stackinfos[s.stackdepth].funcinfo
local func = sinfo.func
local name = sinfo.name
local funcbp = s.funcbpt[func]
local namebp = s.namebpt[name]
if (funcbp and funcbp[line]) or (namebp and namebp[line])
or (namebp and namebp[0] and line == finfo.sortedlines[1]) then
local prompt = string.format("%s (%s)%s %s:%d\n",
finfo.what, sinfo.namewhat, name, finfo.short_src, line)
io.write(prompt)
debug.debug()
end
end
end
在判断当前行是否有断点时,除了查看funcbpt
表,还需要查看namebpt
表,对于函数名称断点没有指定行号的情况,判断当前行是不是第一个有效行。打印提示信息时,则从stackinfos
表中保存的信息中获取。
测试
代码修改好了,我们来测试下通过函数名称添加断点的功能。编写如下测试脚本:
local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
local function foo()
local a = 0
end
local function bar()
local a = 0
end
local function pee()
local a = 0
end
local id1 = setbp(foo)
local id2 = setbp(foo, 7)
local id3 = setbp("bar")
local id4 = setbp("bar", 11)
local id5 = setbp("bar", 100)
local id6 = setbp(pee)
local id7 = setbp("pee", 15)
foo()
bar()
pee()
rmbp(id1)
rmbp(id3)
rmbp(id6)
foo()
bar()
pee()
rmbp(id2)
rmbp(id4)
rmbp(id7)
foo()
bar()
pee()
我们添加了三个函数,其中foo函数以函数作为参数添加断点,bar函数以函数名称作为参数添加断点,pee函数分别用函数和函数名添加了一个断点。添加完断点,先分别调用一次,预期每个函数都会碰到两个断点。接着三个函数各删除一个断点,再各调用一次,预期每个函数都会碰到一个断点。最后三个函数再各删除一个断点,再各调用一次,预期不碰到断点。
运行测试脚本,结果符合预期。
$ lua test.lua
Lua (local)foo test.lua:6
lua_debug> cont
Lua (local)foo test.lua:7
lua_debug> cont
Lua (local)bar test.lua:10
lua_debug> cont
Lua (local)bar test.lua:11
lua_debug> cont
Lua (local)pee test.lua:14
lua_debug> cont
Lua (local)pee test.lua:15 # 第一次调用,每个函数碰到两个断点
lua_debug> cont
Lua (local)foo test.lua:7
lua_debug> cont
Lua (local)bar test.lua:11
lua_debug> cont
Lua (local)pee test.lua:15 # 第二次调用,每个函数碰到一个断点
lua_debug> cont
$ # 第三次调用,不再碰到断点
Lua中如何实现类似gdb的断点调试—07支持通过函数名称添加断点的更多相关文章
- Lua中如何实现类似gdb的断点调试—08支持通过包名称添加断点
在前一篇中我们支持了通过函数名称来添加断点,我们同时也提到了在Lua中一个函数的名称的并不是确定的.准确的说,Lua中的函数并没有名称,所谓名称其实是保存这个函数值的变量的名称. 于是通过函数名称添加 ...
- Lua中如何实现类似gdb的断点调试—09支持动态添加和删除断点
前面已经支持了几种不同的方式添加断点,但是必须事先在代码中添加断点,在使用上不是那么灵活方便.本文将支持动态增删断点,只需要开一开始引入调试库即可,后续可以在调试过程中动态的添加和删除断点.事不宜迟, ...
- Lua中如何实现类似gdb的断点调试--05优化断点信息数据结构
在上一篇04优化钩子事件处理中,我们在钩子函数中引入了call和return事件的处理,对性能进行了优化. 细心的同学可能已经发现了,我们的hook函数中call事件和line都需要对整个断点表进行遍 ...
- Lua中如何实现类似gdb的断点调试--01最小实现
说到Lua代码调试,最常用的方法应该就是加一堆print进行打印.print大法虽好,但其缺点也是显而易见的.比如效率低下,需要修改原有函数内部代码,在每个需要的地方添加print语句,运行一次只能获 ...
- Lua中如何实现类似gdb的断点调试--02通用变量打印
在前一篇01最小实现中,我们实现了Lua断点调试的的一个最小实现.我们编写了一个模块,提供了两个基本的接口:设置断点和删除断点. 虽然我们已经支持在断点进行变量的打印,但是需要自己指定层数以及变量索引 ...
- Lua中如何实现类似gdb的断点调试--04优化钩子事件处理
在第一篇的01最小实现中,我们实现了一个断点调试的最小实现,在设置钩子函数时只加了line事件,显然这会对性能有很大的影响.而后来两篇02通用变量打印和03通用变量修改及调用栈回溯则是提供了一些辅助的 ...
- Lua中如何实现类似gdb的断点调试—06断点行号检查与自动修正
前面两篇我们对性能做了一个优化,接下来继续来丰富调试器的特性. 我们前面提到过,函数内并不是所有行都是有效行,空行和注释行就不是有效行.我们之前在添加断点的时候,并没有对行号进行检查,任何行号都能成功 ...
- Lua中如何实现类似gdb的断点调试--03通用变量修改及调用栈回溯
在前面两篇01最小实现及02通用变量打印中,我们已经实现了设置断点.删除断点及通用变量打印接口. 本篇将继续新增两个辅助的调试接口:调用栈回溯打印接口.通用变量设置接口.前者打印调用栈的回溯信息,后者 ...
- Paip.断点调试MYSQL存储过程跟函数的解决方案大法
Paip.断点调试MYSQL存储过程跟函数的解决方案大法 作者Attilax , EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http://blog.csdn ...
随机推荐
- 10分钟了解代码命名规范(Java、Python)
前言 关于代码命名,我相信是经常困扰很多小伙伴的一个问题,尤其是对于强迫症晚期患者.怎么说呢,每次小编在写代码之前,总会在想啊想啊,用什么命名法好呢?对于经常在C++.Java.Python等主流语言 ...
- 「BalkanOI 2018 Day2」Parentrises
「BalkanOI 2018 Day2」Parentrises part1 显然可以直接贪心. 右括号记-1,左括号记1. 默认起始全部绿色,不染色. 策略如下: 从左往右扫,如果右括号个数大于左括号 ...
- 常见消息处理api
面试:子线程一定不能更新UI? SurfaceView :多媒体视频播放 ,可以在子线程中更新UI: Progress(进度)相关的控件:也是可以在子线程中更新Ui;审计机制:activity完全显示 ...
- Posix 信号
转载请注明来源:https://www.cnblogs.com/hookjc/ 函数sem_open创建一个新的有名信号灯或打开一个已存在的有名信号灯.有名信号灯总是既可用于线程间的同步,又可以用于进 ...
- NSDictionary基本概念
1.NSDictionar基本概念 什么是NSDictionary NSDictionary翻译过来叫做"字典" 日常生活中,"字典"的作用:通过一个拼音或者汉 ...
- java中Statement 对象
1.创建Statement对象建立了到特定数据库的连接之后,就可用该连接发送 SQL 语句.Statement 对象用 Connection 的方法 createStatement 创建,如下列代码段 ...
- Centos 系统目录概述
Linux目录一切从根目录开始,即"/",根下面的目录是一个有层次的树状结构.并且分区或磁盘是必须挂载在根目录才可以正常访问.做一个形象的比喻:目录类似一个一个的入口,而根目录则是 ...
- go基础——goto语法
package main import "fmt" func main() { a := 10 LOOP: for a < 20 { if a == 15 { a += 1 ...
- 有手就行8——项目构建细节3-Jenkins的参数化构建
有手就行8--项目构建细节3-Jenkins的参数化构建 有时在项目构建的过程中,我们需要根据用户的输入动态传入一些参数,从而影响整个构建结果,这时 我们可以使用参数化构建. Jenkins支持非常 ...
- aspnetcore 使用serilog日志
而在实际项目开发中,使用第三方日志框架来记录日志也是非常多的,首先一般基础的内置日志记录器在第三方日志框架中都有实现,然后很多第三方日志框架在功能上更强大和丰富,能满足我们更多的项目分析和诊断的需求. ...