Scite Debug

lua-users home
wiki

scite-debug Introduction

scite-debug is written entirely in SciTE Lua, which is standard 5.1 compatible. The only bit of C which is absolutely essential is the spawner, which captures an interactive command-line debugger like GDB, and allows scite-debug to write commands to it. Output is directed to a specified global function, using an appropriate main thread technique; on Windows, I'm passing messages to a wndproc from the debugger thread, and on GTK using the asynchronous output mechanism. On both platforms, we grab debugger output by full line, which means that there are restrictions in debugging interactive programs which issue prompts. However, on the Windows side we ask for a separate console window for the debugged program, which separates program input and output from debugger output and allows debugging of Windows console applications.

It is entirely possible for the debug target not to have any debugging symbols. This would be the case when we are only interested in debugging shared libraries loaded by the program. So typically we would be setting breakpoints in these libraries, but GDB has to be cajoled into accepting this. If debug.target starts with '[n]', then GDB will be fed an arbitrary little symbol file (provided by stubby.so|.dll). At this point, it is happy to accept any unresolved breakpoint requests as pending. (Do note that you need vs 6 of GDB for this; if you are using MinGW, download a fresher GDB if you are still running vs 5.)

Environment variables can be set for the debug target using the debug.environment property. This is a semi-colon separated list of VAR=VAL pairs.

A typical captive debugger session needs to set breakpoints, and start the program running with the parameters specified using View|Parameters. Any breakpoints set before the program is run are written out to a configuration file; in the case of GDB, it is called 'prompt.cmd'. This file is also an opportunity to configure the debugger more to our taste. For instance, for GDB we set a prompt with a linefeed, switch off output paging, etc.

Stopping on a breakpoint requires a distinct pattern that matches debugger output and can extract the file and line. GDB has a special mode (-f) which is used by Emacs, which prints out the full path at any break in this format: (26)(26)<path>:<line> where (26) is the ASCII character represented by decimal 26. This gives a very distinct target pattern. Other debuggers are not so amenable to being driven automatically, and we have to make do.

Once we are in 'break' mode, it is of course possible to set and unset breakpoints directly.

Any debugger command can be entered from the Output window. For instance, you can set a variable watch in GDB using 'watch var'.

A common task is to evaluate expressions. Alt-I (Inspect) will report the value of the expression at the cursor. scite-debug is intelligent enough to know that a.b and p->c->x are complete expressions, but if an expression is selected, it will use that. Tooltip evaluation uses the same heuristics and the same debugger command, but redirects the output to a tooltip rather than to the SciTE output window. The output of any command can be redirected to an arbitrary function, which opens up interesting opportunities for a more graphical interface. (This, however, will have to wait a while.)

For C/C++, scite-debug will notice that an expression evaluated as a pointer, and will attempt to deference that pointer. That is, if GDB reports the value as $11 = (A *)0xFFF23EE then scite-debug will attempt to evaluate *$11 and collect the results.

Another useful feature for C++ debugging is the automatic simplification of certain standard patterns, particularly std::string where it will extract the char pointer value. These rules are applied recursively, so that structures containing std::string values will be presented in a much more human-friendly form. Do note that these simplifications depend heavily on the precise implementation used! Currently only g++ is supported so the pattern is fairly stable. This is a good place for user customizations.

scite-debug generally knows how to interpret a stack trace; double-clicking on the required level will put you into that frame and place you at the corresponding source line. The stack trace can be explicitly shown using Ctrl+Alt+S and will automatically presented if possible when an error occurs. Alt+U and Alt+D correspond to 'up' and 'down' levels.

Integrated Lua and C/C++ debugging

This useful (and possibly unique) feature is implemented by running the clidebug Lua debugger in a process within GDB. clidebug has now got a 'GDB mode', where it mimics the GDB command set and output. So whether we break in GDB or clidebug, luagdb will take us to that place in the source.

There are two scenarios possible; the first is that the host program is Lua itself (or a close relative), and understands the -e and -l command line options; this is the default. The second is that the host is a program which has embedded Lua. The debug.target variable must indicate both the host program and the Lua script name. Some examples may clarify this:

# the host is Lua on the path, and has no debugging symbols.
debug.target=[n]:gdb;lua;mytest.lua

# the host is SciTE - the [h] indicates that it isn't Lua
debug.target=[n]:gdb;SciTE[h];/home/steve/{{scite-debug}}/extman.lua

# a debug version of Lua.
debug.target=:gdb;/home/steve/lua-5.1.3/src/lua;/home/steve/tests/testlfs.lua

If the host isn't Lua, then you must initialize clidebug yourself in the script to be debugged. In this snippet, replace the path with the appropriate location.

local path = ';/home/steve/{{scite-debug}}/lua_clidebugger/?.'
package.path = package.path .. path .. 'lua'
package.cpath = package.cpath .. path .. 'so'
WIN=false
GDB=true
require "debugger"
io.stdout:setvbuf("no")
pause('debug')

Debugging a GUI program like SciTE on Windows requires a special build, at least if you want to debug Lua scripts. Normally GUI programs ('subsystem:windows') do not have standard in and out, but they can be rebuilt. In the case of SciTE, I replaced '-mwindows' with '-lgdi32 -lcommctl32'; the resulting program comes with an ugly black console, but this will be hidden by the debugger.

(Another option for debugging Lua-only for GUI applications is remDebug, which is also supported by scite-debug.)

Single-stepping into C/C++

luagdb makes it possible to single-step into C extensions from Lua code. To do this, it is necessary to find the address of the particular C function we are entering on the 'call' debugger event. A little extension, dbgl, queries the Lua internals and returns this address. It also provides a convenient function to put breakpoints called debug_break() (this trick requires a GDB version that is fine with pending breakpoints.) clidebug prints out this address with a distinct prefix, and calls dbgl.debug_break(), which puts us into GDB. luagdb picks up the address and issues a GDB 'info line' query on the address to see if any debug information is available for that address. If so, it makes a temporary breakpoint at the start of this function. Either way, it then issues a 'continue' command so that we keep going. We cache the result of this so, so we don't have to continuously call 'info line', but we still set temporary breakpoints each time the program is single-stepping from Lua to C/C++. This ensures that it is always possible to step over a C function.

Once you are on the C/C++ side, you can continue single-stepping in Lua by simply continuing (Alt+R.)

These are still two separate debug sessions, so there are some limitations. Setting breakpoints in a running program can cause interesting problems, at least on Windows. There is no integrated call stack.

dbgl.c has to find out what the corresponding address of a Lua C function:

static int c_addr (lua_State *L)
{
    char buff[40];
    CallInfo *ci;
    Closure* cl = NULL;
    for (ci = L->ci - 1; ci > L->base_ci; ci--) {
      if (! f_isLua(ci)) {  // C function!
        cl = clvalue(ci->func);	
      	break;
      }
    }
    if (cl == NULL) {
      lua_pushnil(L);
    } else {
      void *fun = cl->c.f;    
      sprintf(buff,"0x%X",fun);
      lua_pushstring(L,buff);
    }
    return 1;
}

This requires some knowledge of the internals of Lua, and requires lstate.h, which is not part of the public API. (So when rebuilding dbgl.c, you need to give it an include path that points to the full set of Lua headers.) We look down through the stack of closures to find the C function we are calling.


See also: [Project page]
RecentChanges · preferences
edit · history
Last edited March 11, 2008 12:40 pm GMT (diff)