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


    说到Lua代码调试,最常用的方法应该就是加一堆print进行打印。print大法虽好,但其缺点也是显而易见的。比如效率低下,需要修改原有函数内部代码,在每个需要的地方添加print语句,运行一次只能获取一次信息,下次换个地方又得重新添加print语句。而且有时候,事先并不知道该去哪打印、或者打印什么内容,需要通过运行中获取的信息才能确定。

    当print大法无法满足我们的需求时,就需要类似断点调试这样更高级的调试功能。本文将从零开始编写一个Lua调试器,实现类似gdb的断点调试功能。

    本文代码已开源至Github,欢迎watch/star😘。

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

    定义模块及接口

    首先,我们来定义模块及接口,创建一个名为luadebug.lua的模块,该模块是基于标准库中的debug库。为了实现最基本的断点调试功能,我们的模块提供了两个接口setbreakpointremovebreakpoint,分别用于设置断点和删除断点。断点信息通过一个函数和一个行号指定,返回断点的id。后续可以通过这个id来删除相应断点。

    #!/usr/bin/env lua
    local debug = require "debug"
    -- 省略...
    local function setbreakpoint(func, line)
    -- 省略...
    end
    local function removebreakpoint(id)
    -- 省略...
    end
    return {
    setbreakpoint = setbreakpoint,
    removebreakpoint = removebreakpoint,
    }

    维护状态的数据结构

    接着,来定义维护状态的数据结构,表status维护了所有断点相关信息,其中的bpnum元素表示当前总共有多少断点,bpid表示当前的断点id,这个值是不断递增的,bptable则是保存所有断点信息的表。表bptable的键是断点的id,值也是一个表,保存了断点的所在的函数和行号。

    -- 省略...
    -- 记录断点状态
    local status = {}
    status.bpnum = 0 -- 当前总断点数
    status.bpid = 0 -- 当前断点id
    status.bptable = {} -- 保存断点信息的表
    -- 省略...

    设置断点接口

    接下来来定义我们的setbreakpoint接口。设置断点时,首先检查参数有效性,再更新断点id和断点数,然后将参数中传入的函数func和行号line保存到表bptable中下一个断点id的位置。如果只有一个断点(从无到有),那么还需要调用debug.sethook设置钩子。这是实现断点调试的核心函数之一,它使得我们有机会停在断点处。因为是最小实现,简单起见这里只设置了line事件。

    -- 设置断点
    local function setbreakpoint(func, line)
    if type(func) ~= "function" or type(line) ~= "number" then
    return nil --> nil表示无效断点
    end
    status.bpid = status.bpid + 1
    status.bpnum = status.bpnum + 1
    status.bptable[status.bpid] = {func = func, line = line}
    if status.bpnum == 1 then -- 第一个断点
    debug.sethook(linehook, "l") -- 设置钩子
    end
    return status.bpid --> 返回断点id
    end

    钩子函数

    在钩子函数中,通过debug.getinfo获取到闭包信息,注意这里的层次为2,因为debug.getinfo()函数本身的层次是0,钩子函数层次是1,断点所在的函数层次即为2。然后遍历断点表,与获取的闭包信息进行比较,如果函数和行号都匹配,说明命中断点。我们打印一行提示信息,然后调用debug.debug()进入交互调试模式,debug.debug是实现断点调试的另一个核心函数,它使得我们可以在断点处输入任意代码执行。交互调试模式一直持续,直到用户输入cont为止。

    -- 钩子函数
    local function linehook (event, line)
    local info = debug.getinfo(2, "nfS")
    for _, v in pairs(status.bptable) do
    if v.func == info.func and v.line == line then
    local prompt = string.format("(%s)%s %s:%d\n",
    info.namewhat, info.name, info.short_src, line)
    io.write(prompt)
    debug.debug()
    end
    end
    end

    删除断点接口

    删除断点比较简单,首先检查id参数是否有效,如果无效直接返回,如果有效则将断点表中相应id位置的值置为nil即可,然后更新断点数,如果已经没有断点了,则清除钩子。

    -- 删除断点
    local function removebreakpoint(id)
    if status.bptable[id] == nil then
    return
    end
    status.bptable[id] = nil
    status.bpnum = status.bpnum - 1
    if status.bpnum == 0 then
    debug.sethook() -- 清除钩子
    end
    end

    至此我们的模块就编写好了,下面对这个模块进行测试。

    测试脚本

    我们编写一个如下的测试脚本test.lua,定义了两个函数foo和bar,然后分别在两个函数中设置了一个断点(注意:注释和空行不是有效的断点行),然后多次调用函数并先后删除断点:

    local ldb = require "luadebug"
    local setbp = ldb.setbreakpoint
    local rmbp = ldb.removebreakpoint
    g = 1
    local u = 2
    local function foo (n)
    local a = 3
    a = a + 1
    u = u + 1
    g = g + 1
    end
    local function bar (n)
    n = n + 1
    end
    local id1 = setbp(foo, 11) -- 设置断点1
    local id2 = setbp(bar, 16) -- 设置断点2
    foo(10)
    bar(10)
    rmbp(id1) -- 删除断点1
    foo(20)
    bar(20)
    rmbp(id2) -- 删除断点2
    foo(30)
    bar(30)

    测试验证

    然后我们运行测试脚本,可以看到程序停在了foo函数的断点1处。

    $ lua test.lua
    (local)foo test.lua:11
    lua_debug>

    我们可以在这里打印调用栈信息

    $ lua test.lua
    (local)foo test.lua:11
    lua_debug> print(debug.traceback())
    stack traceback:
    (debug command):1: in main chunk
    [C]: in function 'debug.debug'
    ./luadebug.lua:20: in hook '?'
    test.lua:11: in local 'foo'
    test.lua:22: in main chunk
    [C]: in ?
    lua_debug>

    可以看到foo函数在第4层(第1层是执行我们调试命令的main chunk,第2层是debug.debug函数,第3层是hook函数)。我们打印foo函数中第一个局部变量(即固定参数n)的值

    lua_debug> print(debug.getlocal(4, 1))
    n 10
    lua_debug>

    然后打印第二个局部变量(即a)的值

    lua_debug> print(debug.getlocal(4, 2))
    a 4
    lua_debug>

    然后我们输入cont继续代码的执行,碰到了bar函数的断点2

    lua_debug> cont
    (local)bar test.lua:16
    lua_debug>

    我们打印bar函数的参数n的值,可以看到也是10

    lua_debug> print(debug.getlocal(4, 1))
    n 10
    lua_debug>

    然后我们输入cont继续执行代码,因为断点1已经被移除,所以再次停在了bar函数的断点2处

    lua_debug> cont
    (local)bar test.lua:16
    lua_debug>

    我们再来打印下参数n的值,此时参数n的值是20

    lua_debug> print(debug.getlocal(4, 1))
    n 20
    lua_debug>

    我们再次输入cont,因为断点2也被移除了,所以第三次调用foo函数和bar函数就没有再碰到断点,程序运行结束

    lua_debug> cont
    $

    这样一个最简单的Lua断点调试器就完成了。虽然还比较简陋,但是已经能够应付一些简单的调试了。🎉

  • 相关阅读:
    Java容器知识体系
    实战:如何优雅地扩展Log4j配置?
    mysql 锁解决的办法
    《Head First HTML5 javascript》第3章 探索浏览器 知识点总结
    8K视频来了,8K 视频编辑的最低系统要求
    Ubuntu磁盘满了,导致黑屏
    Unity地面交互效果——5、角色足迹的制作
    数据结构--线性表
    Linux 6.7 正式移除对英特尔 IA-64 架构安腾处理器的支持
    (39.3)【XML漏洞专题】XML注入——查找、注入、防止SOAP
  • 原文地址:https://www.cnblogs.com/logchen/p/15969943.html