Linux 实时远程进程调试

本文介绍如何建立与 Linux 的实时 WinDbg 连接。 在 Linux 上进行实时远程进程调试需要使用 WinDbg 1.2402.24001.0 或更高版本。

GNU 调试器 GDBServer 用于在 Linux 上支持 WinDbg 连接。 有关 GDBServer 的详细信息,请参阅 https://en.wikipedia.org/wiki/Gdbserver。 提供查看远程 gdb 调试文档的一站式位置是 - https://sourceware.org/gdb/current/onlinedocs/gdb#Remote-Debugging

以下示例使用的适用于 Linux 的 Windows 子系统 (WSL),但也可以使用其他 Linux 实现。

WinDbg 远程进程调试类型

使用 WinDbg 进行远程调试有两种主要方法 - 进程服务器KD 连接服务器。 进程服务器用于用户模式调试;KD 连接服务器用于内核模式调试。 有关这些 WinDbg 连接类型的一般信息,请参阅进程服务器(用户模式)KD 连接服务器(内核模式)

有两种方法可以开始调试 Linux 用户模式进程。 可以在特定进程上启动 gdbserver,也可以将 gdbserver 作为进程服务器启动,后者可以列出并附加到现有进程。 这与 Windows 上的 DbgSrv (dbgsrv.exe) 进程服务器非常类似。 有关详细信息,请参阅激活进程服务器

用户模式 Linux 进程调试

可以连接到特定的单用户模式进程,或者在多模式下,在列表中查看所有进程并选择一个进行连接。 本主题介绍这两种方法。 下面介绍了这两种方法共享相同的连接字符串语法。

gdbserver 连接字符串的格式

用于连接 gdbserver 的格式是“protocol:arguments”,其中 arguments 是以逗号分隔的“argument=value”列表。 对于用户模式的 gdbserver 连接,协议是 gdb,参数集如下所示。

server=<address> - 必填:指明要连接的 gdbserver 的 IP 地址。

port=<port> - 必填:指明要连接到的 gdbserver 的端口号。

threadEvents=<true|false> - 可选:指明此版本的 gdbserver 的线程事件在停止模式下是否正常工作。

当前的 gdbserver 版本中存在一个问题,即在服务器处于停止模式(WinDbg 使用的模式)时启用线程事件会导致 gdbserver 崩溃。 如果此值为 false(默认值),则线程启动和停止事件将被合成,但可能会比线程创建/销毁的实际时间晚很多。 当 gdbserver 中的修复程序可用时,可通过此选项启用实际事件。

连接到单个用户模式进程

本部分将介绍如何使用 WinDbg 在 Linux 中识别和连接单个用户模式进程。

WSL(适用于 Linux 的 Windows 子系统)

以下示例使用的 WSL(适用于 Linux 的 Windows 子系统),但也可以使用其他 Linux 实现。 有关设置环境和使用 WSL 的信息,请参阅:

选择所需进程

在 Linux 中使用 ps -A 命令列出进程,以确定要连接的运行中进程。

user1@USER1:/mnt/c/Users/USER1$ ps -A
    PID TTY          TIME CMD
    458 pts/1    00:00:00 bash
    460 ?        00:00:00 rtkit-daemon
    470 ?        00:00:00 dbus-daemon
    482 ?        00:00:19 python3
   1076 ?        00:00:00 packagekitd
   1196 pts/0    00:00:00 ps

在本示例演练中,我们将连接到 python3。

找到目标系统 IP 地址

如果连接到远程 linux 目标计算机,请使用 ip route show 等命令确定外部 IP 地址。

user1@USER1:/mnt/c/Users/USER1$ ip route show
default via 192.168.1.1 dev enp3s0 proto dhcp metric 100
172.25.144.0/24 dev enp3s0 proto kernel scope link src 192.168.1.107 metric 100

在本演练中,我们将连接到在同一台电脑上运行的 WSL,并使用 localhost 的 IP 地址。

将 GDBServer 附加到所选进程

在 WSL linux 控制台上输入 gdbserver localhost:1234 python3,以便在 1234 端口上启动 gdbserver,并将其附加到 python3 进程。

USER1@USER1:/mnt/c/Users/USER1$ gdbserver localhost:1234 python3
Process python3 created; pid = 1211
Listening on port 1234

对于某些 Linux 环境,可能需要以管理员身份运行该命令,例如使用 sudo - sudo gdbserver localhost:1234 python3。 在启用调试器管理员根级别访问权限时要谨慎,仅在需要时方可使用。

在 WinDbg 中创建进程服务器连接

打开 WinDbg,然后选择“文件/连接到远程调试器”,然后输入用于连接的协议字符串。 此处使用 gdb:server=localhost,port=1234 为例。

WinDbg 的”开始调试”屏幕的屏幕截图,其中显示了连接字符串。

单击“确定”按钮后,调试器应连接到 gdbserver,并且应在初始进程开始中断时。

达到初始断点后,可以按几次“g”。 将收到模块加载消息(sxe 样式的“模块加载时中断”事件应能正常工作)。

请注意,由于调试符号会加载到缓存中,因此可能需要一些时间才能达到该点。 除了通过符号服务器或本地搜索路径查找符号和二进制文件外,如果无法通过 symsrv 或本地找到这些文件,GDBServer 集成还能从远程文件系统中提取这些文件。 这通常比从 symsrv 或本地搜索路径获取符号要慢得多,但通过查找适当的符号可以让整体体验变得更好。

使用 k stacks 命令列出堆栈。 它会显示 python3 模块,因此可以确认正在使用 WinDbg 在 Linux 上调试 python3。

0:000> k
 # Child-SP          RetAddr               Call Site
00 00007fff`ffffce10 00007fff`f786d515     libc_so!_select+0xbd
01 00007fff`ffffce80 00005555`55601ce8     readline_cpython_310_x86_64_linux_gnu!PyInit_readline+0xac5
02 00007fff`ffffcf60 00005555`556f06a1     python3!PyOS_Readline+0x109
03 00007fff`ffffcfa0 00005555`556eee7e     python3!PyFrame_LocalsToFast+0x62a1
04 00007fff`ffffd000 00005555`556edcf0     python3!PyFrame_LocalsToFast+0x4a7e
05 00007fff`ffffdb80 00005555`557a18e9     python3!PyFrame_LocalsToFast+0x38f0
06 00007fff`ffffdc00 00005555`557a1470     python3!PyCodec_LookupError+0xb09
07 00007fff`ffffdc50 00005555`557b89dc     python3!PyCodec_LookupError+0x690
08 00007fff`ffffdc70 00005555`5560b42f     python3!PyUnicode_Tailmatch+0xc6c
09 00007fff`ffffdcb0 00005555`5560b012     python3!PyRun_InteractiveLoopObject+0x4e0
0a 00007fff`ffffdd50 00005555`557b7678     python3!PyRun_InteractiveLoopObject+0xc3
0b 00007fff`ffffdda0 00005555`555f55c8     python3!PyRun_AnyFileObject+0x68
0c 00007fff`ffffddd0 00005555`555ea6e8     python3!PyRun_AnyFileExFlags+0x4f
0d 00007fff`ffffde00 00005555`55780cad     python3!Py_str_to_int+0x2342a
0e 00007fff`ffffdef0 00007fff`f7c7cd90     python3!Py_BytesMain+0x2d
0f 00007fff`ffffdf20 00007fff`f7c7ce40     libc_so!_libc_init_first+0x90
10 00007fff`ffffdfc0 00005555`55780ba5     libc_so!_libc_start_main+0x80
11 00007fff`ffffe010 ffffffff`ffffffff     python3!start+0x25
12 00007fff`ffffe018 00000000`00000000     0xffffffff`ffffffff

此时,通过远程进程服务器连接到远程 Windows 调试器的 WinDbg 几乎可以完成所有操作。 可以单步执行、源级别调试、设置断点、检查局部变量等。

调试完成后,使用 CTRL+D 退出 WSL 中的 gbdserver 窗口。

连接到进程服务器

除了通过用户模式 GDBServer 连接到单个进程外,还可以将其设置为进程服务器,并列出和附加到系统上的现有进程。 为此,gdbserver 以“--multi”命令行参数开头 - gdbserver --multi localhost:1234

user1@USER1:/mnt/c/Users/USER1$ sudo gdbserver --multi localhost:1234
Listening on port 1234

要连接进程服务器,请在 WinDbg 中选择“文件/连接进程服务器”,然后输入与上述单进程 gdbserver 示例相同的协议字符串:

gdb:server=localhost,port=1234

单击“确定”按钮后,应作为进程服务器连接到 gdbserver。 与 dbgsrv 一样,可以启动一个新进程,也可以列出现有进程并附加到其中之一。

在本例中,使用“附加到进程”选项。

WinDbg 的“开始调试”屏幕的屏幕截图,其中显示了附加到 20 个左右列出的进程。

请注意,将看到许多对 Windows 进程可见的相同内容(包括 PID、用户和命令行)。 “附加到进程”对话框中的某些列与 Linux 无关,也不包含数据。

结束会议

使用 CTRL+D 退出 WSL 中的 gbdserver 窗口,并在 WinDbg 中选择停止调试。 要结束会话,在某些情况下可能需要退出调试器。

重新连接进程服务器

WinDbg 通过 gdbserver 是否连接到进程来识别“进程服务器”和“单个目标”。 如果连接到某个进程,让它处于冻结状态,关闭调试器,然后尝试重新连接到进程服务器,我们很可能无法将其识别为进程服务器。 在这种情况下,请重新启动目标 gdbserver 并重新连接调试器。

Linux WinDbg 功能

虽然调试器的大部分功能在调试内核转储(例如:堆栈行走、符号、类型信息、局部变量、反汇编等)时都能按预期运行,但必须注意的是,整个调试工具链尚未意识到 ELF、DWARF 以及由此产生的与 Windows 语义的差异。 调试器中的某些命令当前可能会导致意外输出。 例如,lm 仍然会显示 ELF 模块的错误消息,因为它需要手动分析 PE 标头。

通过 EXDI 的 Linux 内核模式

Windows 调试器支持使用 EXDI 进行内核调试。 这样就可以调试各种硬件和操作系统。 有关设置配置 EXDI 连接和故障排除的一般信息,请参阅配置 EXDI 调试器传输

有关如何使用 EXDI 设置 QEMU 内核模式调试的信息,请参阅使用 EXDI 设置 QEMU 内核模式调试

Linux 符号和源

本部分介绍 Linux 符号的基本用法和可用性。 有关详细信息,请参阅 Linux 符号和源以及源代码扩展访问

DebugInfoD 符号服务器

从 WinDbg 版本 1.2104 开始,源路径命令(.srcpath、.lsrcpath (设置源路径))支持通过 DebugInfoD* 标记从 DebugInfoD 服务器检索文件。

DebugInfoD* 标记可以指向一个或多个 DebugInfoD 服务器,其中每个服务器 URL 的格式设置为https://domain.com,并用 * 分隔。 将按源路径中列出的顺序搜索服务器,并从第一个匹配的 URL 检索文件。 有关详细信息,请参阅源代码扩展访问

例如,可以使用 .sympath(设置符号路径) 命令来设置如下所示的 DebugInfoD 路径。

.sympath+ DebugInfoD*https://debuginfod.elfutils.org

有关设置符号路径的一般信息,请参阅使用符号

要显示正在加载的符号信息,请使用 !sym noisy。 有关详细信息,请参阅 !sym

此外,还支持从 DebugInfoD 服务器自动下载源,这些源支持返回该项目类型。 实际上,可以执行以下操作:

.srcpath+ DebugInfoD*https://debuginfod.elfutils.org

有关使用 DWARF 符号和 Linux 符号实用工具(如 !sourcemap!diesym)的详细信息,请参阅 Linux 符号和源

C++ 应用演练

  1. 使用文本编辑器(如 nano 或 vi)创建 C++ 文件。 例如:

nano DisplayGreeting.cpp

  1. 在文本编辑器中编写 C++ 程序。 这是一个显示问候语的简单程序,需要对其进行调试:
#include <array>
#include <cwchar>
#include <cstdio>
#include <iostream>
using namespace std;

void GetCppConGreeting(wchar_t* buffer, size_t size)
{
    wchar_t const* const message = L"HELLO FROM THE WINDBG TEAM. GOOD LUCK IN ALL OF YOUR TIME TRAVEL DEBUGGING!";
    wcsncpy(buffer, message, size);
}

int main()
{
    std::array<wchar_t, 50> greeting{};
    GetCppConGreeting(greeting.data(), greeting.size());

    cin.get();
    wprintf(L"%ls\n", greeting.data());

    return 0;
}
  1. 保存 (CTRL-O) 并退出 (CTRL-X) nano 编辑器。

  2. 使用 g++ 编译 C++ 文件。 -o 选项用于指定输出文件名,而 -g 选项用于生成符号文件:

g++ DisplayGreeting.cpp -g -o DisplayGreeting

  1. 如果代码中没有错误,g++ 命令将在目录中创建一个名为 DisplayGreeting 的可执行文件。

  2. 可以使用以下命令来运行程序:

./DisplayGreeting

  1. 按返回键后,应用中将显示该消息。 从输出结果来看,“问候语”似乎被截断了,取而代之的是“????”。

HELLO FROM THE WINDBG TEAM. GOOD LUCK IN ALL OF YO????

调试 DisplayGreeting

  1. 代码准备好运行后,就可以使用 gdbserver 启动应用。

gdbserver localhost:1234 DisplayGreeting

  1. 打开 WinDbg,然后选择“文件/连接到远程调试器”,然后输入用于连接的协议字符串。 此处使用 gdb:server=localhost,port=1234 为例。

  2. 连接完成后,输出应显示它正在侦听 1234 端口,并已建立远程调试连接。

Bob@Bob6:/mnt/c/Users/bob$ gdbserver localhost:1234 DisplayGreeting
Process /mnt/c/Users/bob/DisplayGreeting created; pid = 725
Listening on port 1234
Remote debugging from host 127.0.0.1, port 47700

如前所述,对于某些 Linux 环境,可能需要以管理员身份运行该命令,通常使用 sudo。 在启用调试器管理员根级别访问权限时要谨慎,仅在需要时方可使用。

在调试器会话中添加源和符号路径

要设置断点并查看源代码和变量,请设置符号和源路径。 有关设置符号路径的一般信息,请参阅使用符号

使用 .sympath 将符号路径添加到调试器会话。 在本例中,代码运行在 WSL Linux Ubuntu 的这个位置,用户名为 Bob。

\\wsl$\Ubuntu\mnt\c\Users\Bob\

在 WSL 中,此目录映射到 Windows OS 的位置:C:\Users\Bob\

因此会使用这两个命令。

.sympath C:\Users\Bob\

.srcpath C:\Users\Bob\

有关 WSL 文件系统的详细信息,请参阅 WSL 的文件权限

  1. 要使用额外的 Linux OS 符号,请使用 .sympath 位置来添加 DebugInfoD 符号,如下所示。

.sympath+ DebugInfoD*https://debuginfod.elfutils.org

  1. 此外,还支持从 DebugInfoD 服务器自动下载源,这些源支持返回该项目类型。 要利用这一点,请使用 .srcpath 添加 elfutils 服务器。

.srcpath+ DebugInfoD*https://debuginfod.elfutils.org

设置断点

在 DisplayGreeting 应用的主位置中设置一个断点。

0:000> bp DisplayGreeting!main
0:000> bl
     0 e Disable Clear  00005555`55555225  [/mnt/c/Users/bob/DisplayGreeting.cpp @ 14]     0001 (0001)  0:**** DisplayGreeting!main

使用 Go 命令或菜单选项重新启动代码执行。

加载源代码

使用 .reload 命令重新加载符号。

使用 lm 命令确认正在运行 DisplayGreeting 应用。

0:000> lm
start             end                 module name
00005555`55554000 00005555`55558140   DisplayGreeting T (service symbols: DWARF Private Symbols)        c:\users\bob\DisplayGreeting
00007fff`f7a54000 00007fff`f7a732e8   libgcc_s_so   (deferred)             
00007fff`f7a74000 00007fff`f7b5a108   libm_so    (deferred)             
00007fff`f7b5b000 00007fff`f7d82e50   libc_so  T (service symbols: DWARF Private Symbols)        C:\ProgramData\Dbg\sym\_.debug\elf-buildid-sym-a43bfc8428df6623cd498c9c0caeb91aec9be4f9\_.debug
00007fff`f7d83000 00007fff`f7fae8c0   libstdc___so   (deferred)             
00007fff`f7fc1000 00007fff`f7fc1000   linux_vdso_so   (deferred)             
00007fff`f7fc3000 00007fff`f7ffe2d8   ld_linux_x86_64_so T (service symbols: DWARF Private Symbols)        C:\ProgramData\Dbg\sym\_.debug\elf-buildid-sym-9718d3757f00d2366056830aae09698dbd35e32c\_.debug

一旦命令触发了对显示问候语代码的访问,它就会显示在 WinDbg 中。

在 WinDbg 中显示 DisplayGreeting.cpp 代码的屏幕截图,其中在第 19 行设置了断点,wprint

使用“k”命令列出堆栈。

0:000> k
 # Child-SP          RetAddr               Call Site
00 00007fff`ffffde00 00007fff`f7b84d90     DisplayGreeting!main+0x1f [/mnt/c/Users/BOB/DisplayGreeting.cpp @ 15] 
01 00007fff`ffffdef0 00007fff`f7b84e40     libc_so!__libc_start_call_main+0x80 [./csu/../sysdeps/x86/libc-start.c @ 58] 
02 00007fff`ffffdf90 00005555`55555125     libc_so!__libc_start_main_impl+0x80 [./csu/../sysdeps/nptl/libc_start_call_main.h @ 379] 
03 00007fff`ffffdfe0 ffffffff`ffffffff     DisplayGreeting!start+0x25
04 00007fff`ffffdfe8 00000000`00000000     0xffffffff`ffffffff```

使用 dx 命令查看局部变量 greeting。 请注意,它的大小为 50。

0:000> dx greeting
greeting                 : { size=50 } [Type: std::array<wchar_t, 50>]
    [<Raw View>]     [Type: std::array<wchar_t, 50>]

仔细浏览代码,注意 50 的大小可能无法满足问候语消息的需要。

wchar_t const* const message = L"HELLO FROM THE WINDBG TEAM. GOOD LUCK IN ALL OF YOUR TIME TRAVEL

通过扩展问候语的局部变量并看到问候语被截断来确认这一点。

gdbserver 连接故障排除

使用 --debug 选项可在 gdbserver 控制台上显示更多信息,以收集更多有关连接状态的信息。 例如,使用此命令启动进程服务器。

gdbserver --debug --multi localhost:1234

另请参阅

Linux 符号和源

源代码扩展访问

ELFUTILS debuginfod

选择最佳的远程调试方法