用户自定义gdb命令的Python实现

使用gdb调试的时候,人们往往会希望有一些命令能实现自己特殊的需求,但实际上gdb本身并没有提供这样的命令。
对这些用户需求,其实常可以使用gdb的Python API(或其他语言的API)来实现。

首先,Python API的总帮助页在这里:
GDB Python API

其中常用的有关于Breakpoint和Command相关的API:
Break Point API
Command API

下面举几个用户自定义命令的例子。在文章的最后,把上篇博客中的gdb命令脚本翻译成Python脚本来做。

实例-1 如何实现 step-to-next-call 功能

  1. 想要一个gdb命令,运行后停在下一个函数调用之前

方法:
定义一个类,继承 gdb.Command 类; 然后重写 init 和 invoke 方法。

import gdb

# For Command "step-before-next-call"
class StepBeforeNextCall (gdb.Command):
    def __init__ (self):
        # "step-before-next-call" is user-defined command's name
        super (StepBeforeNextCall, self).__init__ ("step-before-next-call",
                                                   gdb.COMMAND_OBSCURE)

    def invoke (self, arg, from_tty):
        def addr2num(addr):
            try:
                return int(addr)  # Python 3
            except:
                return long(addr) # Python 2
    
        arch = gdb.selected_frame().architecture()

        while True:
            current_pc = addr2num(gdb.selected_frame().read_register("pc"))
            disa = arch.disassemble(current_pc)[0]
            if "call" in disa["asm"]: # or startswith ?
                break

            SILENT=True
            gdb.execute("stepi", to_string=SILENT)

        print("step-before-next-call: next instruction is a call.")
        print("{}: {}".format(hex(int(disa["addr"])), disa["asm"]))

# Declare user-defined command
StepBeforeNextCall()

  1. 想要一个gdb命令,运行后停在刚刚进入下一个函数调用的地方。

方法:
计算当前栈的深度,然后一直step,直到深度变深一层为止。

import gdb

# For Command "step-to-next-call"
class StepToNextCall (gdb.Command):
    def __init__ (self):
        # "step-to-next-call" is user-defined command's name
        super (StepToNextCall, self).__init__ ("step-to-next-call", 
                                               gdb.COMMAND_OBSCURE)

    def invoke (self, arg, from_tty):
        def callstack_depth():
            depth = 1
            frame = gdb.newest_frame()
            while frame is not None:
                frame = frame.older()
                depth += 1
            return depth
    
        start_depth = current_depth = callstack_depth()

        # step until we're one step deeper
        while current_depth == start_depth:
            SILENT=True
            gdb.execute("step", to_string=SILENT)
            current_depth = callstack_depth()

        # display information about the new frame
        gdb.execute("frame 0")

# Declare user-defined command
StepToNextCall()

实例-2 step调试,但不进入库函数调用

方法:
一直执行 step, 如果新的frame是以"/usr"开头的,那么执行 finish 命令以结束当前frame

import gdb

# For Command "step-no-library"
class StepNoLibrary (gdb.Command):
    def __init__ (self):
        # "step-no-library" is user-defined command's name
        super (StepNoLibrary, self).__init__ ("step-no-library",
                                              gdb.COMMAND_OBSCURE)

    def invoke (self, arg, from_tty):
        step_msg = gdb.execute("step", to_string=True)

        fname = gdb.newest_frame().function().symtab.objfile.filename

        if fname.startswith("/usr"):
            # inside a library
            SILENT=False
            gdb.execute("finish", to_string=SILENT)
        else:
            # inside the application
            print(step_msg[:-1])

# Declare user-defined command
StepNoLibrary()

用户自定义命令的测试

首先,将上述3个自定义命令集成到一个python脚本中,如pycmd.py

import gdb

# For Command "step-before-next-call"
class StepBeforeNextCall (gdb.Command):
    def __init__ (self):
        # "step-before-next-call" is user-defined command's name
        super (StepBeforeNextCall, self).__init__ ("step-before-next-call",
                                                   gdb.COMMAND_OBSCURE)

    def invoke (self, arg, from_tty):
        def addr2num(addr):
            try:
                return int(addr)  # Python 3
            except:
                return long(addr) # Python 2
    
        arch = gdb.selected_frame().architecture()

        while True:
            current_pc = addr2num(gdb.selected_frame().read_register("pc"))
            disa = arch.disassemble(current_pc)[0]
            if "call" in disa["asm"]: # or startswith ?
                break

            SILENT=True
            gdb.execute("stepi", to_string=SILENT)

        print("step-before-next-call: next instruction is a call.")
        print("{}: {}".format(hex(int(disa["addr"])), disa["asm"]))


# For Command "step-to-next-call"
class StepToNextCall (gdb.Command):
    def __init__ (self):
        # "step-to-next-call" is user-defined command's name
        super (StepToNextCall, self).__init__ ("step-to-next-call", 
                                               gdb.COMMAND_OBSCURE)

    def invoke (self, arg, from_tty):
        def callstack_depth():
            depth = 1
            frame = gdb.newest_frame()
            while frame is not None:
                frame = frame.older()
                depth += 1
            return depth
    
        start_depth = current_depth =callstack_depth()

        # step until we're one step deeper
        while current_depth == start_depth:
            SILENT=True
            gdb.execute("step", to_string=SILENT)
            current_depth = callstack_depth()

        # display information about the new frame
        gdb.execute("frame 0")

# For Command "step-no-library"
class StepNoLibrary (gdb.Command):
    def __init__ (self):
        # "step-no-library" is user-defined command's name
        super (StepNoLibrary, self).__init__ ("step-no-library",
                                              gdb.COMMAND_OBSCURE)

    def invoke (self, arg, from_tty):
        step_msg = gdb.execute("step", to_string=True)

        fname = gdb.newest_frame().function().symtab.objfile.filename

        if fname.startswith("/usr"):
            # inside a library
            SILENT=False
            gdb.execute("finish", to_string=SILENT)
        else:
            # inside the application
            print(step_msg[:-1])

# The following 3 lines are necessary for declaring user-defined commands. 
StepBeforeNextCall()
StepToNextCall()
StepNoLibrary()

在gdb中source这个 pycmd.py 之后,可以使用 help user-defined 命令查看用户自定义的命令:

(gdb) source pycmd.py
(gdb) help user-defined
User-defined commands.
The commands in this class are those defined by the user.
Use the "define" command to define a command.

List of commands:

step-before-next-call -- This command is not documented
step-no-library -- This command is not documented
step-to-next-call -- This command is not documented

Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.

接下来,看C语言源文件test2.c:

#include 

unsigned int myadd(unsigned int i)
{
    int result = 0;
    if (i > 0) {
        result = i + myadd(i-1);
    }
    return result;
}

int main()
{
    int a = 5;
    int sum = 0;
    
    sum = myadd(a);
    printf("sum = %d\n", sum);
    
    return 0;
}

build一下:

gcc -g test2.c -o test2.exe

调试过程示例(省略了一些冗余的打印):

(gdb) source pycmd.py

(gdb) file test2.exe

(gdb) b main
Breakpoint 8 at 0x6ae: file test2.c, line 13.

# stop before "printf" library call 
(gdb) b 18
Breakpoint 9 at 0x6d1: file test2.c, line 18.

(gdb) r
Breakpoint 8, main () at test2.c:14
14          int a = 5;

(gdb) step-to-next-call
15          int sum = 0;
17          sum = myadd(a);
myadd (i=5) at test2.c:5
5           int result = 0;
#0  myadd (i=5) at test2.c:5
5           int result = 0;

(gdb) step-to-next-call
6           if (i > 0) {
7               result = i + myadd(i-1);
myadd (i=4) at test2.c:5
5           int result = 0;
#0  myadd (i=4) at test2.c:5
5           int result = 0;

(gdb) step-to-next-call
6           if (i > 0) {
7               result = i + myadd(i-1);
myadd (i=3) at test2.c:5
5           int result = 0;
#0  myadd (i=3) at test2.c:5
5           int result = 0;

(gdb) c
Continuing.

Breakpoint 9, main () at test2.c:18
18          printf("sum = %d\n", sum);

(gdb) step-no-library       # Don't step into "printf" library call. 
sum = 15
20          return 0;

(gdb)

实例-3 当指定函数返回false的时候停住

import gdb

class FunctionFinishBreakpoint (gdb.FinishBreakpoint):
    def __init__ (self):
        gdb.FinishBreakpoint.__init__(self, gdb.newest_frame(), 
                                      internal=True)
        self.silent = True 

    def stop(self):
        #print("after: {}".format(int(self.return_value)))
        return not int(self.return_value)

class FunctionBreakpoint(gdb.Breakpoint):
    def __init__ (self, spec):
        gdb.Breakpoint.__init__(self, spec)
        self.silent = True

    def stop (self):
        #print("before")
        FunctionFinishBreakpoint() # set breakpoint on function return

        return False # do not stop at function entry

FunctionBreakpoint("function_name")

关于"指定函数返回false时停住"的测试

首先,C++代码如下:

// test_ret.cpp 
// g++ -g test_ret.cpp -o test3.exe 

#include 
using namespace std;

bool is_even_num(int num)
{
    return num % 2 == 0; 
}

int main()
{
    int data = 6;
    int i;
    bool result;
    
    for (i=0; i<data; i++) {
        result = is_even_num(i);
        if (!result) {
            cout << i << " ";
        }
    }
   cout << endl;
    
    return 0;
}

该程序将打印 1 3 5

调试的时候,每次 is_even_num() 函数返回false的时候,gdb都会stop. 详情见以下的调试过程,其中的 pycmd2.py 就是上一节中的python代码。

(gdb) file test3.exe

(gdb) source pycmd2.py
Breakpoint 1 at 0x961: file test_ret.cpp, line 6.

(gdb) b main
Breakpoint 2 at 0x976: file test_ret.cpp, line 11.

(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000000961 in is_even_num(int) at test_ret.cpp:6
2       breakpoint     keep y   0x0000000000000976 in main() at test_ret.cpp:11

(gdb) r
Breakpoint 2, main () at test_ret.cpp:11
11          int data = 6;

(gdb) c
Continuing.

(gdb) p i
$1 = 1

(gdb) c
Continuing.

(gdb) p i
$2 = 3

(gdb) c
Continuing.

(gdb) p i
$3 = 5

(gdb) c
Continuing.
1 3 5
[Inferior 1 (process 11476) exited normally]

实例-4 Python脚本实现自动化调试

在前一篇博客中,我们谈到了使用gdb脚本来实现调试的自动化。实际上,也可以通过Python脚本来实现。
下面的Python脚本就是用来调试前一篇博客中的test.c编译出的test.exe的。它的功能与前一篇博客中的gdb脚本基本相同。
为维护文章的完整性,下面先将C代码再贴一遍,然后再贴Python的gdb脚本。

C代码:

// test.c 
// gcc -g test.c -o test.exe 

#include 
#include 

int icount = 1; // default value

int main(int argc, char *argv[])
{
  int i, b;

  if (argc == 2) {
    icount = atoi(argv[1]);
  }

  i = icount;
  while (i > -1) {
    b = 5 / i;
    printf(" 5 / %d = %d \n", i, b );
    i--;
  }

  printf("Finished\n");
  return 0;
}

Python的gdb脚本如下:

import os
import sys
LOG_FILE='gdb.log'


class MyBreakPoint1(gdb.Breakpoint):
    def __init__(self, *args, **kwargs):
        super(MyBreakPoint1, self).__init__(*args, **kwargs)
        
    def stop(self):
        gdb.execute("p i")
        gdb.execute("p b")

class MyBreakPoint2(gdb.Breakpoint):
    def __init__(self, *args, **kwargs):
        super(MyBreakPoint2, self).__init__(*args, **kwargs)
        
    def stop(self):
        gdb.execute("p i")
        gdb.execute("p b")

def setup():
    print('Running GDB from: %s\n'%(gdb.PYTHONDIR))
    gdb.execute("set pagination off")
    gdb.execute("set print pretty")
    
    # if connecting to remote target
    # gdb.execute('target remote :')
    
    gdb.execute('set logging file %s'%(LOG_FILE))
    gdb.execute('set logging on')
    
    print('\nReading gdb env...\n')
    gdb.execute('show script-extension')
    gdb.execute('show sysroot')
    gdb.execute('show solib-search-path')
    print('\nSetup complete !!\n')

def main():
    setup()
    
    MyBreakPoint1('test.c:20')
    MyBreakPoint2('test.c:19')
    
    gdb.execute("r 3")

main()

关于使用方法,与前类似,只要在gdb中source一下这个Python脚本即可。具体调试trace从略。

参考文献

  • Simple GDB extensions with Python
  • Manipulating breakpoints using Python
  • Commands In Python
  • Python API
  • User Defined Commands

(完)

你可能感兴趣的:(调试,gdb)