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

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

如果您刚开始接触 C/C++ 调试和内存检查器,请参阅以下几篇博文:

简介

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

在周围的 WebAssembly 内存中识别对象的字节是一项难题。从对象开始,您必须知道对象的大小和计数字节数。在下面的屏幕截图中,我们选择了一个包含 10 个元素的 int32 数组的第一个字节,但是您无法马上清楚还有哪些其他字节属于该数组。如果您可以立即识别属于对象的所有字节,不是很不错吗?

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

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

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

更新后的内存检查器的屏幕截图,其中突出显示了鲜艳的数组

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

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

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

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

  • 使用 Chrome 107 或更高版本。
  • 安装 C/C++ DWARF 扩展程序。
  • DevTools 启用 DWARF 调试 >设置。 设置 >实验 >WebAssemble 调试:启用 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++ 应用内存中发生的情况。

DevTools 如何确定要突出显示的内容

在本部分,我们将介绍支持 C/C++ 调试的工具生态系统。具体而言,您将了解 DevTools、V8、C/C++ DWARF 扩展程序和 Emscripten 如何让您能够在 Chrome 中调试 C/C++ 代码。

如需在开发者工具中充分发挥 C/C++ 调试的强大作用,您需要做好两件事:

  • 在 Chrome 中安装的 C/C++ DWARF 扩展程序
  • 按照这篇博文中的说明,使用最新的 Emscripten 编译器将 C/C++ 源文件编译为 WebAssembly

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

在编译期间,emscripten 会将 DWARF 调试数据嵌入到二进制文件中。概括来讲,这些数据可帮助扩展程序确定哪些 WebAssembly 变量与您的 C/C++ 变量相对应,以及其他信息。这样,即使 V8 实际运行了 WebAssembly,DevTools 也可以向您显示 C++ 变量。如果您有兴趣,请参阅这篇博文,查看 DWARF 调试数据示例。

那么,当您显示 lastNumber 时,实际上会发生什么?在您点击内存图标后,DevTools 就会立即检查您想要检查的变量。然后,它会针对 lastNumber 的数据类型和位置查询该扩展程序。一旦扩展程序用该信息进行响应,内存检查器就可以显示相关的内存切片并知道其类型,还可以向您显示对象的大小。

如果查看前面示例中的 lastNumber,您可能会注意到我们检查了 lastNumber: int *,但内存检查器中的芯片显示 *lastNumber: int,是什么原因呢?检查器使用 C++ 样式的指针解引用来指明向您展示的对象类型!如果您检查指针,检查器会显示它指向的内容。

对调试程序步骤保留突出显示效果

内存检查器中显示某个对象并使用调试程序单步调试时,如果检查器认为它仍然适用,则会保留突出显示的内容。最初,我们并未在路线图中添加此功能,但我们很快意识到,这会对您的调试体验产生负面影响。想象一下,如果您必须在完成每一步后重新检查数组,就像下面的视频中所示!

当调试程序命中新断点时,Memory Inspector 会再次查询 V8 和扩展程序,以查找与上次突出显示的变量关联的变量。然后比较这些对象的位置和类型。如果匹配,突出显示的内容会继续显示。在上面的视频中,有一个 for 循环会写入数组 x。这些操作不会更改数组的类型或位置,因此会保持突出显示状态。

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

总结

本文介绍了我们对 Memory Inspector 针对 C/C++ 调试功能所做的改进。我们希望这些新功能能够简化 C/C++ 应用的内存调试!如果您有进一步改进建议,请提交 bug 告诉我们!

后续步骤

如需了解详情,请参阅以下资源: