扩展内存检查器以进行 C/C++ 调试

在 Chrome 92 中,我们推出了内存检查器,用于检查线性内存缓冲区。在本文中,我们将讨论我们如何改进用于 C/C++ 调试的检查器,以及在此过程中遇到的技术挑战。

如果您不熟悉 C/C++ 调试和内存检查器,可以阅读下面这些相关的博文:

简介

内存检查器为线性内存缓冲区提供了更强大的调试选项。对于 C/C++,您可以检查 WebAssembly Memory 中的 C/C++ 内存对象。

在周围的 WebAssembly 内存中识别对象的字节是一个痛点。您必须知道对象的大小,并从对象开始计算字节数。在下面的屏幕截图中,虽然选择了 10 个元素 int32 数组的第一个字节,但尚不清楚哪些其他字节属于该数组。如果您可以立即识别属于该对象的所有字节,那不是很棒吗?

原始内存检查器的屏幕截图,其中突出显示了一个字节

内存检查器中的对象突出显示

从 Chrome 107 开始,内存检查器会突出显示 C/C++ 内存对象的所有字节。这有助于您将它们与周围的记忆区分开来。

更新后的内存检查器的屏幕截图,其中以亮丽突出显示的数组

请观看下面的视频,了解内存检查器的实际应用。当您在内存检查器中显示数组 x 时,突出显示的内存会显示在内存查看器中,并且其上方会显示一个新芯片。此条状标签会提醒您突出显示内存的名称和类型。点击条状标签即可跳转到该对象的内存。如果将光标悬停在条状标签上,系统会显示一个十字图标,点击该图标即可取消突出显示。

当您选择所检查对象之外的某个字节时,突出显示效果会分散焦点,以免分散您的注意力。如需重新聚焦该对象,请再次点击该对象的任意字节或条状标签。

支持对象突出显示的功能不限于数组。您还可以检查结构体、对象和指针。这些变更可让您比以往更轻松地探索 C/C++ 应用的内存!

想试试吗?您需要执行以下操作:

  • 使用的是 Chrome 107 或更高版本。
  • 安装 C/C++ DWARF 扩展。
  • DevTools > 设置。 设置 > 实验 > WebAssemble 调试:启用 DWARF 支持中启用 DWARF 调试。
  • 打开此演示页面
  • 按照页面上的说明操作。

调试示例

在本部分中,我们将来看一个玩具 bug,以说明如何使用内存检查器进行 C/C++ 调试。在下面的代码示例中,程序员创建了一个整数数组,并决定使用指针算法选择最后一个元素。遗憾的是,程序员在指针计算中出错了,现在程序不会输出最后一个元素,而是输出无意义值。

#include <iostream>

int main()
{
    int numbers[] = {1, 2, 3, 4};
    int *ptr = numbers;
    int arraySize = sizeof(numbers)/sizeof(int);
    int* lastNumber = ptr + arraySize;  // Can you notice the bug here?
    std::cout <<../ *lastNumber <<../ '\n';
    return 0;
}

程序员使用内存检查器来调试问题。您可以跟着这个演示操作!他们首先在内存检查器中检查了该数组,发现 numbers 数组按预期仅包含整数 1234

已打开内存检查器的屏幕截图,其中包含已检查的 int32 数组。系统会突出显示所有数组元素。

接下来,它们在 Scope 窗格中显示 lastNumber 变量,并注意到指针指向数组外部的某个整数!掌握这些知识后,程序员就会意识到,他们在第 8 行错误地计算了指针偏移量。本应是 ptr + arraySize - 1

打开的内存检查器的屏幕截图,其中显示了由名为“lastNumber”的指针所指向的内存。突出显示的内存位于之前突出显示的数组的最后一个字节之后。

虽然这是一个玩具示例,但它说明了对象突出显示如何有效地传达内存对象的大小和位置,从而帮助您更好地了解 C/C++ 应用内存中发生的情况。

开发者工具如何确定要突出显示的内容

在本部分中,我们将探讨支持 C/C++ 调试的工具生态系统。具体来说,您将了解开发者工具、V8、C/C++ DWARF 扩展程序和 Emscripten 如何在 Chrome 中进行 C/C++ 调试。

如需在开发者工具中释放 C/C++ 调试的全部功能,您需要做两件事:

  • Chrome 中安装的 C/C++ DWARF 扩展程序
  • 此博文中所述,使用最新的 Emscripten 编译器编译到 WebAssembly 的 C/C++ 源文件。

但为什么呢?V8 是 Chrome 的 JavaScript 和 WebAssembly 引擎,无法执行 C 或 C++。借助 Emscripten(C/C++ 到 WebAssembly 的编译器),您可以将使用 C 或 C++ 构建的应用编译为 WebAssembly,并在浏览器中执行它们!

在编译期间,emscripten 会将 DWARF 调试数据嵌入到您的二进制文件中。大体上讲,这些数据有助于扩展程序弄清楚哪些 WebAssembly 变量对应于您的 C/C++ 变量等等。这样,即使 V8 实际运行了 WebAssembly,开发者工具也能向您显示 C++ 变量。如果您想了解 DWARF 调试数据,可以查看此博文

那么,当您显示 lastNumber 时实际上会发生什么?一旦您点击内存图标,开发者工具就会检查您要检查的变量。然后,它会根据 lastNumber 的数据类型和位置查询该扩展程序。一旦此扩展程序做出响应,内存检查器便可显示相关的内存切片并了解其类型,因此还可以向您显示对象的大小。

如果您查看上一个示例中的 lastNumber,可能会注意到我们检查了 lastNumber: int *,但内存检查器中的芯片显示 *lastNumber: int,这意味着什么?检查器使用 C++ 样式的指针解引用来指示向您显示的对象类型!如果您检查某个指针,检查器会向您显示它所指向的内容。

保留调试程序步骤的亮点

当您在 Memory Inspector 中显示某个对象并借助调试程序单步调试时,如果检查器认为突出显示对象仍然适用,则会持续突出显示相应对象。最初,我们的路线图上没有这项功能,但后来我们很快意识到,这影响到了您的调试体验。想象一下,在每个步骤后都必须重新检查数组,如下面的视频所示!

当调试程序遇到新的断点时,Memory Inspector 会再次查询 V8 以及与前一突出显示内容相关联的变量的扩展。然后比较这些对象的位置和类型。如果两者一致,突出显示就会持续显示。在上面的视频中,出现了向数组 x 写入数据的 for 循环。这些操作不会更改数组的类型或位置,因此数组会保持突出显示状态。

您可能想知道这对指针有何影响。如果您有一个突出显示的指针并将其重新分配给其他对象,那么突出显示的对象的旧位置和新位置会有所不同,并且突出显示效果会消失。由于新指向的对象可以位于 WebAssembly Memory 中的任何位置,并且与之前的内存位置可能几乎没有关系,因此取消突出显示会比跳到新的内存位置更清晰。您可以在 Scope 窗格中点击指针的内存图标,再次突出显示指针。

总结

本文介绍了我们对用于 C/C++ 调试的内存检查器进行的改进。我们希望这些新功能可以简化 C/C++ 应用内存的调试!如果您有进一步改进建议,请提交 bug 告诉我们!

后续步骤

如需了解详情,请参阅: