• Lua调试工具使用及原理


    前言

    当我们在linux下使用c/c++开发时,可以通过gdb来调试我们编译后的elf文件。gdb支持了attch、单步运行(单行、单指令)、设置断点等非常实用的功能来辅助我们调试。当使用lua开发的时候,一般可能会使用print(打印到屏幕)或是输出日志等稍微简陋的调试方式,但如果日志输出不能满足我们需求时,比如我们需要类似断点、单步执行等更高级的调试功能,此时就必须借助第三方工具。
    本文介绍了lua调试工具LuaPanda的使用,以及lua调试工具和gdb在实现上的一些区别。

    gdb调试原理

    先简单介绍一下gdb的原理。一般的我们将gdb这种调试进程称为tracer,被调试进程称为tracee。当进程被调试时(处于traced状态)时,每次收到任何除了SIGKILL以外的任何信号,都会暂停当前的执行,并且tracer进程可以通过waitpid来获取tracee的暂停原因。gdb使用ptrace系统调用来实现操作tracee进程

    1. gdb附加到进程

    当使用gdb附加到一个正在运行的进程(tracee)上时,gdb会执行类似下面的代码:

    ptrace(PTRACE_ATTACH, pid, ...)
    

    这里的pid是tracee的pid。系统调用执行后,os会给tracee进程发送一个SIGTRAP信号,然后tracee的执行将会暂停。最后gdb(tracer)可以通过系统调用waitpid来获取tracee的暂停原因,并且开始调试。

    2. gdb单步执行

    单步调试与上述attch类似,gdb通过下面的代码告诉tracee进程需要在运行完一个指令后暂停:

    ptrace(PTRACE_SINGLESTEP, pid, ...)
    

    当tracee执行完一个指令后,tracee也会因为收到os的SIGTRAP信号从而暂停执行。

    3. gdb设置断点

    设置断点稍微有点不同,首先gdb需要从调试程序的调试信息中根据行号(函数名)找到代码的内存地址,然后通过ptrace将tracee进程的代码替换成一个软中断指令:int 3。由于这个指令实际上会被编码为一个字节0xcc,因此可以很安全的与任何指令替换。

    /* Look at the word at the address we're interested in */
    unsigned addr = 0x8048096;
    unsigned data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
    
    /* Write the trap instruction 'int 3' into the address */
    unsigned data_with_trap = (data & 0xFFFFFF00) | 0xCC;
    ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data_with_trap);
    

    通过给ptrace指定PTRACE_PEEKTEXT、PTRACE_POKETEXT可以读写tracee进程的代码段的内存。最终当程序执行到int 3时,会触发一个软中断,os会给tracee进程发送SIGTRAP信号。当断点成功后,gdb会用相同的方法用原来的指令替换掉int 3,在这之后tracee就可以正常执行了。

    LuaPanda使用介绍

    LuaPanda是腾讯开源的一个lua调试工具,配合vscode可以做到类似gdb的调试功能。当开始调试时,vscode会监听本机(127.0.0.1)的8818端口。tracce进程(包含被调试的lua代码的进程)通过LuaPanda连上vscode:

    require("LuaPanda").start("127.0.0.1",8818);
    

    LuaPanda通过使用LuaSocket模块创建tcp连接以此来实现通信(对比gdb使用信号机制通信)。vscode会把用户的调式命令(如设置断点、continue、单步运行等)通过tcp发送给tracee进程。

    Lua调试原理

    lua调试与gdb不同,我们可以通过debug.sethook来为一个lua线程设置hook函数,在调用函数、离开函数、进入新行的时候lua会先执行这个hook函数。LuaPanda在连接上vscode后会注册一个hook函数:

    debug.sethook(this.debug_hook, "lrc");
    

    "lrc"字符串掩码决定了hook函数会在什么时候被调用:

    • 'c': 每当 Lua 调用一个函数时,调用钩子;
    • 'r': 每当 Lua 从一个函数内返回时,调用钩子;
    • 'l': 每当 Lua 进入新的一行时,调用钩子。

    1. LuaPanda设置断点

    当我们设置一个断点时,vscode会把断点的信息,包括文件路径、行号发给tracee进程。tracee收到setBreakPoint命令时,表示需要注册一个断点。此时LuaPanda会将断点的信息存储在一个全局的lua Table中:breaks。

    -- 处理 收到的消息
    -- @dataStr 接收的消息json
    function this.dataProcess( dataStr )
        ...
        elseif dataTable.cmd == "setBreakPoint" then
            this.printToVSCode("dataTable.cmd == setBreakPoint");
            local bkPath = dataTable.info.path;
            bkPath = this.genUnifiedPath(bkPath);
            if autoPathMode then 
                -- 自动路径模式下,仅保留文件名
                bkPath = this.getFilenameFromPath(bkPath);
            end
            this.printToVSCode("setBreakPoint path:"..tostring(bkPath));
            breaks[bkPath] = dataTable.info.bks;
    

    当lua虚拟机调用hook函数的时候,hook会遍历breaks,看一下当前行是否命中断点:

    ------------------------断点处理-------------------------
    -- 参数info是当前堆栈信息
    -- @info getInfo获取的当前调用信息
    function this.isHitBreakpoint( info )
        local curLine = tostring(info.currentline);
        local breakpointPath = info.source;
        local isPathHit = false;
        
        if breaks[breakpointPath] then
            isPathHit = true;
        end
    
        if isPathHit then
            for k,v in ipairs(breaks[breakpointPath]) do
                if tostring(v["line"]) == tostring(curLine) then
                    ...
    

    如果断点被命中则会发一个消息给vscode,并原地等待消息回包,以此来实现暂停执行tracee进程:

    function this.real_hook_process(info)
        ...
        local isHit = false;
        if tostring(event) == "line" and jumpFlag == false then
            if currentRunState == runState.RUN or currentRunState == runState.STEPOVER or currentRunState == runState.STEPIN or currentRunState == runState.STEPOUT then
                --断点判断
                isHit = this.isHitBreakpoint(info) or hitBP;
                if isHit == true then
                    this.printToVSCode(" + HitBreakpoint true");
                    hitBP = false; --hitBP是断点硬性命中标记
                    --计数器清0
                    stepOverCounter = 0;
                    stepOutCounter = 0;
                    this.changeRunState(runState.HIT_BREAKPOINT);
                    --发消息并等待
                    this.SendMsgWithStack("stopOnBreakpoint");
    

    2. LuaPanda单步执行

    单步执行实现比较简单,当tracee收到stopOnStep命令时,表示vscode需要单步执行代码:执行到新的一行需要暂停,并且当有函数调用时应该跳过函数。LuaPanda在处理setBreakPoint命令时操作非常简单:将运行状态改为runState.STEPOVER然后结束:

    -- 处理 收到的消息
    -- @dataStr 接收的消息json
    function this.dataProcess( dataStr )
        ...
        elseif dataTable.cmd == "stopOnStep" then
            this.changeRunState(runState.STEPOVER);
            local msgTab = this.getMsgTable("stopOnStep", this.getCallbackId());
            this.sendMsg(msgTab);
            this.changeHookState(hookState.ALL_HOOK);
    

    当lua虚拟机由于进入新行(event为"line")时执行hook函数时,会根据stepOverCounter计数器来决定这次是否要暂停执行。而stepOverCounter计数器会在调用函数的时候+1,离开函数的时候-1。因此当处于内部函数调用的时候,计数器的值会大于零,执行不会被暂停,从而实现跳过函数执行。

    function this.real_hook_process(info)
        ...
        if currentRunState == runState.STEPOVER then
            -- line stepOverCounter!= 0 不作操作
            -- line stepOverCounter == 0 停止
            if event == "line" and stepOverCounter <= 0 and jumpFlag == false then
                stepOverCounter = 0;
                this.changeRunState(runState.STEPOVER_STOP)
                this.SendMsgWithStack("stopOnStep");
            elseif event == "return" or event == "tail return" then
                --5.1中是tail return
                if stepOverCounter ~= 0 then
                    stepOverCounter = stepOverCounter - 1;
                end
            elseif event == "call" then
                stepOverCounter = stepOverCounter + 1;
            end
    

    lua hook实现

    下面是LuaState结构中的与hook函数有关的字段:

    /*
    ** 'per thread' state
    */
    struct lua_State {
      ...
      volatile lua_Hook hook;
      l_signalT hookmask;
      ...
    };
    

    其中,hook字段表示对应的函数地址,hookmask是一个掩码,表示需要调用hook函数的事件。

    lua虚拟机会在每次执行每一个字节码之前判断是否需要调用hook函数。lua虚拟机执行的主循环(luaV_execute函数中),每次通过vmfetch获取一个字节码指令时,都会先检查LuaState的hookmask字段,看是否有LUA_MASKLINE标记,若有则继续判断是否进入新行。

    void luaV_execute (lua_State *L) {
        ...
        /* main loop of interpreter */
      for (;;) {
        Instruction i;
        StkId ra;
        vmfetch();
        ...
    

    vmfetch是一个宏,定义为:

    /* fetch an instruction and prepare its execution */
    #define vmfetch()	{ 
      i = *(ci->u.l.savedpc++); 
      if (L->hookmask & (LUA_MASKLINE | LUA_MASKCOUNT)) 
        Protect(luaG_traceexec(L)); 
      ra = RA(i); /* WARNING: any stack reallocation invalidates 'ra' */ 
      lua_assert(base == ci->u.l.base); 
      lua_assert(base <= L->top && L->top < L->stack + L->stacksize); 
    }
    

    最后在函数luaG_traceexec中判断是否执行新行:

    void luaG_traceexec (lua_State *L) {
        ...
        if (mask & LUA_MASKLINE) {
        Proto *p = ci_func(ci)->p;
        int npc = pcRel(ci->u.l.savedpc, p);
        int newline = getfuncline(p, npc);
        if (npc == 0 ||  /* call linehook when enter a new function, */
            ci->u.l.savedpc <= L->oldpc ||  /* when jump back (loop), or when */
            newline != getfuncline(p, pcRel(L->oldpc, p)))  /* enter a new line */
          luaD_hook(L, LUA_HOOKLINE, newline);  /* call line hook */
      }
    

    从代码可以看到在lua中,通过debug.sethook注册hook函数是有性能损耗的:

    1. 每次执行字节码前都需要判断是否是新行;
    2. 每次执行新行前都需要调用一个lua的函数(hook函数)

    而且LuaPanda的实现上看,断点命中判断是遍历breaks做字符串匹配,所以效率较低,不推荐在生产环境下使用LuaPanda调试(即使没有设置断点)。也不推荐在生产环境注册hook函数。

    LuaPanda使用限制

    由于LuaPanda是使用debug.sethook来实现调试功能的,并且由于每个luaState只能注册一个hook函数。因此如果在代码的其它地方中注册hook函数就会把LuaPanda的hook给覆盖。
    因此在调试时不能运行luacov这类的工具,因为luacov内部也会通过debug.sethook来注册钩子函数。

  • 相关阅读:
    【Java】Java创建String时,什么情况放进String Pool?
    【Java】代理模式,静态代理和动态代理(基于JDK或CGLib)
    【Java】Float计算不准确
    【Spring】初始化Spring IoC容器(非Web应用),并获取Bean
    【Eclipse】安装subclipse的Eclipse插件
    【多线程】如何通过线程返回值?如何使用多线程并发查询数据
    【多线程】并发执行指定数量的线程
    【ActiveMQ】ActiveMQ在Windows的安装,以及点对点的消息发送案例
    DBCP连接Oracle,数据库重启后现OALL8 is in an inconsistent state异常
    jQuery Validation remote的缓存请求
  • 原文地址:https://www.cnblogs.com/adinosaur/p/13289019.html
Copyright © 2020-2023  润新知