优雅漂亮的调试——GDB Pretty Printer技术实战分享

Sommaire :

(C/C++调试》一书阅读)

当你自信满满地写好一段代码,跑起来时却发现——呃,它似乎并没有按照你预想的流程在工作,或是崩溃了,或是得到了一个莫名其妙的结果。时候复很多我们在编写代码时疏忽大意了,很多时候就是一些非常低级的错误。但是,把这些恼人的错误揪出来却需要花费很大的精力。

调试的技巧千千万,在程序中打断点是最常用的一个办法。让进程暂停下来,观察进程执行到此刻时内部维护的数据是怎样的,我们便能直观方便地理解程序的实际行为了。

如下是一段简化的例子:

#include <iostream>
#include <vector>


class SimpleVec
{
	private:
		int * k_data;
		std::size_t k_capacity;
		std::size_t k_size;

	public:
		explicit
		SimpleVec(std::size_t n)
		{
			this->k_capacity = 2 * n;
			this->k_data = new int[this->k_capacity];
			this->k_size = n;
		}

		~SimpleVec()
		{
			delete[] this->k_data;
		}

		auto & operator[](std::size_t i)
		{
			return this->k_data[i];
		}
};

int main()
{
	SimpleVec vec(6);
	vec[0] = 114;
	vec[1] = 514;
	vec[2] = 1919;
	vec[3] = 810;

	std::vector<int> std_vec = {114, 514, 1919, 810};

	std::cout << vec[0] << std::endl;
	return 0;
}

(注:本文的重点并不在探讨 C++ 的最佳实践,因此,本文中所出现的 C++代码仅供展示一些最基本的思想使用,不会遵循良好的面向对象以及RAII、模板类型安全、异常安全等准则)

L’IDE est actuellement en cours de développement :

其中,有一个小窗口会帮你列举出当前上下文中的变量,包括他们的名字、类型与所存储的值等丰富的信息:

这里不难发现,对于标准库中的类型 std::vector<int>,IDE 展现出了其作为 »动态数组 »的逻辑结构,而对于我们自己定义的类型 SimpleVec,仅仅展现出了其最原始的结构。

Utilisation de C/C++中,一个指针变量指向的 »某一个 »单独的元素,还是 »一系列 »连续存储的数据,这其中的语义信息是由代码编写者去灵活维护的。这样的信息对于机械的计算机而言其实是并不能感知到的。因此,调试器在拿到这个指针时,并不能武断地决定它引用了一个数组,只能保守地认为它只引用了单个的元素。这也是为什么我们在上图中看到,变量树里只有一项 *k_data = 114

显然,我们自定义的 SimpleVec 内部的数据存储与 std::vector 并无本质区别——无非是一个指向缓冲区的指针 data,一个指示缓冲区最大承载容量的整数capacité, et taille de l’IDE. std::vector L’IDE est basé sur gdb. Il s’agit d’une jolie imprimante gdb.

这个技术顾名思义,就是用漂亮、优雅、直观的方式向我们去展示程序的内部数据,从而帮助我们更好地去理解程序的行为、更高效快速的debug。这个理念与《高效 C/C++本书会带你深入挖掘程序执行的底层原理,也会带你了解如gdb Pretty Printer est une imprimante C/C++.

C++调试的书以外,还额外赠送了我鸢尾花系列丛书中的两本,分别是《矩阵力量》和《统计至简》。我在C++领域花过不少时间钻研,但是数学基础仅仅只是掌握了考研数学二考纲里要求的那些板块的程度。加入量化行业以后,面对量化研究员同事所提的一些数学公式往往感到力不从心。这两本书结合生动有趣的实例讲解了线代和统计的知识,帮我快速弥补了短板,这里也一并推荐给大家。

非常大的开本,而且非常厚重的大部头书,而且是所有页都是全彩色印刷的。原先我觉得这个定价有点小贵,但是拿在手上翻看的时候觉得这个定价还是物有所值的。


一、初探究竟

Description de Python 包 (文件夹中有) __init__.py 文件的称为包,et importer des produits importés __init__.py par ici)

项目结构如下:

__init__.py 内容如下:

#
# @file       __init__.py
# @author     WentsingNee
#

import gdb

def printer_lookup(cxx_value: gdb.Value):
    print(f"received a cxx value of type: {cxx_value.type.name}")

    return None

gdb.pretty_printers.append(printer_lookup)

一会我们会结合运行的效果回过头来讲解这个代码。

然后,在你的 HOME 目录下,创建 .gdbinit 文件 ,内容如下:

python
import sys

sys.path.append('${这里请填项目的根目录}')
import pretty_printer

end

gdb s’occupe de la création de Pretty_Printer et de Pretty_printer.

En ce qui concerne le C++, il s’agit d’une application C++ :

如果我们在变量树中展开树,还会继续输出几行内容 :

received a cxx value of type: None
received a cxx value of type: SimpleVec
received a cxx value of type: SimpleVec
received a cxx value of type: SimpleVec
received a cxx value of type: None
received a cxx value of type: None
received a cxx value of type: None
received a cxx value of type: std::size_t
received a cxx value of type: unsigned long
received a cxx value of type: std::size_t
received a cxx value of type: unsigned long
received a cxx value of type: None
received a cxx value of type: int
received a cxx value of type: int
...

好,回过头来讲 __init__.py 文件中的内容。

À LIRE  La pomme de terre cuite au four parfaite!

导入了 gdb jolie imprimante Il s’agit d’une application C++ basée sur la technologie C++.

gdb.pretty_printers.append(printer_lookup)

向 gdb 中注册了一个我们自己的回调函数 printer_lookup

def printer_lookup(cxx_value: gdb.Value):
    print(f"received a cxx value of type: {cxx_value.type.name}")

    return None

gdb 会向回调函数传入一个 gdb.Value Python 对象。这里我们给这个参数起名叫 cxx_value, 它对应着 gdb 要打印的 C++ 变量。们这里输出了这个变量的类型名。由于暂时还没有写 Pretty imprimante, il s’agit d’une imprimante None

二、小试牛刀,为我们自定义的类型的成员换一个名字

在面向对象的开发中,有的时候可能会依据变量的可见性、类型等因素,给变量的起名中带上修饰。比如有些项目的命名规范中要求成员变量的命名必须前加小m:

struct HOMO
{
	int mHo;
	double mMo;
};

原始的输出结构可能是这样的:

Il s’agit d’une jolie imprimante.

pretty_printer 包中创建一个 ./printer/HOMOPrinter.py 文件,项目结构和文件内容如下:

#
# @file       HOMOPrinter.py
# @author     WentsingNee
#

import gdb

class HOMOPrinter:

    def __init__(self, val: gdb.Value):
        self.val = val

    def children(self):
        members = [
            ("ho", self.val["mHo"]),
            ("mo", self.val["mMo"]),
        ]
        return members

然后,在我们的 printer_lookup 函数中,加入这个 Imprimante:

#
# @file       __init__.py
# @author     WentsingNee
#

import gdb

from .printer.HOMOPrinter import HOMOPrinter

def printer_lookup(cxx_value: gdb.Value):

    if cxx_value.type.name == "HOMO":
        return HOMOPrinter(cxx_value)

    return None

gdb.pretty_printers.append(printer_lookup) 

Le sujet de la situation est :

gdb jolie imprimante printer_lookup 函数)一个 gdb.Value 类型的 Python 对象,printer_lookup Imprimante gdb, imprimante gdb对象的 children 方法得到结果。

class HOMOPrinter:

    def __init__(self, val: gdb.Value):
        self.val = val

这里,在 Imprimante HOMO类型的不同的对象 homo1, homo2, …)

    def children(self):
        members = [
            ("ho", self.val["mHo"]),
            ("mo", self.val["mMo"]),
        ]
        return members

children 方法返回一个列表,里面有该结构体中要打印的成员的 名字 - 值 对。对于 gdb.Value Applications Python et applications C++ pour les applications C++ val["数据成员名"] 不过要注意的是,这个方法拿到的还是一个 gdb.Value 的对象,而并不是 Python 的原生类型(int 类型、float 类型)的对象,这个 gdb.Value 还是要返回给 gdb,交由 gdb 去处理、去打印。

三、工欲善其事,必先利其器

你刚刚在写 HOMOPrinter IDE 的智能提示器可能无法识别 import gdb 这一句导入指令,也就随之无法处理与 gdb 包相关的任何智能提示。

Il s’agit de Python et de gdb, il s’agit de Python. Il s’agit d’une application C/C++ et LD_LIBRARY_PATH d’une application, par exemple pip L’installation de pip est terminée.

如果你一定要在系统目录中找到这个包,这里可以给你提供一个参考的位置:

如果你将这个位置加入到环境变量中,强行导入它,也是无法完全成功导入的:

这个 _gdb utilise gdb pour créer un fichier Python pour importer gdb 的.

À LIRE  Les Restaurants de Meilleurs de Paris, Oùre Sûrs de Bien Maiger!

Il s’agit de gdb 中执行 python 指令, 倒是可以 import gdb , 我们也借此机会查看一下这个包的信息:

不报错,能成功导入,位置也与我们刚刚系统中找到的那个目录一样。

Il s’agit d’un exemple de Python :

(gdb) python import sys
(gdb) python print(sys.builtin_module_names)

L’application Python s’appelle Python :

Il s’agit d’une jolie imprimante pour l’IDE IDE.

这需要在项目的根目录下放一个 gdb 包对应的 .pyi 存根文件。

.pyi utilise C/C++中头文件的作用类似,是在不给出函数实现的情况下向外展现函数的接口信息用的,方便在不对外展示内部细节的情况下又能方便对外协作。比如这一段.pyi signifie :

C/C++ est une application C/C++, et C/C++ est une application C/C++.

所以,谁说其他语言没有头文件的?

Il s’agit d’un IDE pour gdb et d’un gdb.pyi. Il s’agit de :

github.com/metal-ci/tes

将其放在项目的根目录下。这样,我们在编写调试接下来的例子时,有了智能提示,效率就更高了。

四、再上一层楼,SimpleVec Imprimante

添加 SimpleVecPrinter.py 文件

#
# @file       SimpleVecPrinter.py
# @author     WentsingNee
#

import gdb

class SimpleVecPrinter:

    def __init__(self, val: gdb.Value):
        self.__val = val

    def size(self):
        return self.__val["k_size"]

    def capacity(self):
        return self.__val["k_capacity"]

    def data(self):
        return self.__val["k_data"]

    def __getitem__(self, idx):
        return self.data()[idx]

    def children(self):
        member = [
            ("size", self.size()),
            ("capacity", self.capacity()),
            ("data", self.data()),
        ]

        for i in range(self.size()):
            member.append((f"[{i}]", self[i]))

        return member

这里,我们给 Imprimante 封装出 sizecapactitydata 以及 __getitem__ (opearator[]) Versions C++ et Python Il s’agit d’une imprimante Python jolie imprimante.

然后,在 printer_lookup Imprimante 中注册我们的新 :

#
# @file       __init__.py
# @author     WentsingNee
#

import gdb

from .printer.HOMOPrinter import HOMOPrinter
from .printer.SimpleVecPrinter import SimpleVecPrinter

def printer_lookup(cxx_value: gdb.Value):

    if cxx_value.type.name == "HOMO":
        return HOMOPrinter(cxx_value)

    if cxx_value.type.name == "SimpleVec":
        return SimpleVecPrinter(cxx_value)

    return None

gdb.pretty_printers.append(printer_lookup) 

Description du produit :

不过先别急着激动,因为我们的代码里有一处重大的改进 :

    def children(self):
        member = [
            ("size", self.size()),
            ("capacity", self.capacity()),
            ("data", self.data()),
        ]

        for i in range(self.size()):
            member.append((f"[{i}]", self[i]))

        return member

这一版 children Liste Python里。这里所演示的列表很短还好,但如果是实际开发中,长度几万个、几百万个元素的列表呢?这个运行效率和内存占用就无法接受了。

所幸的是,children 方法的返回值只要是支持 itearable Il s’agit d’un concept de gamme en C++.

    def children(self):
        yield "size", self.size()
        yield "capacity", self.capacity()
        yield "data", self.data()

        for i in range(self.size()):
            yield f"[{i}]", self[i]

Imprimante GDB Pretty Printer个,下一批次的等你需要展开的时候再加载。

五、精益求精,有哪些可以优化的地方呢?

from .printer.HOMOPrinter import HOMOPrinter
from .printer.SimpleVecPrinter import SimpleVecPrinter

def printer_lookup(cxx_value: gdb.Value):

    if cxx_value.type.name == "HOMO":
        return HOMOPrinter(cxx_value)

    if cxx_value.type.name == "SimpleVec":
        return SimpleVecPrinter(cxx_value)

    return None

我们注意到,随着我们提供的 Imprimante 越来越多,printer_lookup 中的 if 也开始变的越来越长,这显然不利于我们代码的维护。

À LIRE  15 meilleurs cafés à Newport Beach, Californie près de moi

Description de PrinterLookup :

#
# @file       PrinterLookup.py
# @author     WentsingNee
#

import gdb


class _PrinterLookup:

    def __init__(self):
        self.printer_lookup_table = {}

    def register_printer(self, cxx_type_name):
        def register_helper(printer_type):
            print(f"binding type: {cxx_type_name} with printer: {printer_type}")
            self.printer_lookup_table[cxx_type_name] = printer_type
            return printer_type

        return register_helper

    def __call__(self, cxx_value: gdb.Value):
        cxx_value_type_name = cxx_value.type.strip_typedefs().unqualified()

        if cxx_value_type_name in self.printer_lookup_table:
            return self.printer_lookup_table[cxx_value_type_name](cxx_value)

        return None


printer_lookup = _PrinterLookup()

在这个类中,维护一个叫 printer_lookup_table Utilisation de C++ (Python) str 类型)到对应的 Imprimante 类型(Python type 类型)的映射信息。

register_printer 提供了一个可以注册 Printer 的装饰器,它是这么使用的:

#
# @file       SimpleVecPrinter.py
# @author     WentsingNee
#

import gdb

from pretty_printer.PrinterLookup import printer_lookup

@printer_lookup.register_printer("SimpleVec")
class SimpleVecPrinter:

    def __init__(self, val: gdb.Value):
        self.__val = val
 

这样,我们就能很方便地在 printer_lookupImprimante C++ pour imprimante C++ Imprimante pour imprimante C++类的文件中所描述,printer_lookup Il s’agit d’une imprimante et d’une imprimante C++. printer_lookup 中修改关联,这个耦合程度就太高了。

而且将来,如果要调试时,我们暂时不需要 jolie impression 这个类型,désactiver掉它也很简单,只要把这行注解注释掉就可以了。

class _PrinterLookup:

    ...

    def __call__(self, cxx_value: gdb.Value):
        cxx_value_type_name = cxx_value.type.strip_typedefs().unqualified().name

        if cxx_value_type_name in self.printer_lookup_table:
            return self.printer_lookup_table[cxx_value_type_name](cxx_value)

        return None

原来的 printer_lookup 函数则转变为了 _PrinterLookup 类的 __call__ (opearator()),printer_lookup_table 那个字典就必须成全局变量了。闭包可以起到访问权限控制、不同实例的隔离等效果,Utilisation du langage C++, langage lambda,C语言一些项目里的上下文结构体(xxx_context),本质上都是一样的设计思想

代码写的越多,越能体会到它们底层的设计思想是想通的

另外,这一行也有变化:

cxx_value_type_name = cxx_value.type.strip_typedefs().unqualified().name

Typedef Typedef的修饰 (strip_typedefs()),去除掉 const/volatile 修饰 (unqualified()), 得到其原始的类型名,交给去除修饰后都相同的 Printer 处理。

__init__.py est également utilisé printer_lookup

#
# @file       __init__.py
# @author     WentsingNee
#

import gdb

from . import printer
from .PrinterLookup import printer_lookup

gdb.pretty_printers.append(printer_lookup)

也把 printer 文件夹转成一个包,在其中自动导入本文件夹下的所有 Printer.py :

#
# @file       printer/__init__.py
# @author     WentsingNee
#

import os
import pathlib
import importlib

for module_file in pathlib.Path(__path__[0]).glob("*Printer.py"):
    if str(module_file) == __file__:
        continue

    module_name = os.path.basename(module_file)[:-3]
    importlib.import_module(f".{module_name}", package=__name__)

文件结构:

小结

在本文中,我们介绍了一点 gdb jolie imprimante为我们提供了一个更直观地展示 C++ Il s’agit d’une jolie imprimante.能为使用者提供一个更加便捷的调试界面。比如,除了我自己写的模板库外,我们业务代码的基础工具中就有一个稀疏 vector 类,如果用 gdb 原始为 std::vector Il s’agit d’une jolie imprimante.

期待读者能创作出更多,更具有创意的imprimante.

github.com/WentsingNee/

Vous voulez suivre notre blog ?

Recevez nos conseils les plus précieux dans votre boîte de réception, une fois par mois !

Articles associés