扩展内存检查器以进行 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 > 设置。 Settings(设置)> Experiments(实验)> WebAssemble Debugging: Enable DWARF support(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++ 应用内存中发生的情况。

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

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

如需在 DevTools 中充分发挥 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++ 风格的指针解引用来指明向您显示的对象的类型!如果您检查指针,检查器会显示它指向的内容。

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

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

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

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

总结

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

后续步骤

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