wine开发系列之——调试Wine

本文主要以 wine 官网的这篇文章 Debugging Wine,来讲解。大部分内容是对该文的翻译,修正了原文的一些书写错误,删除了原文跟最新的 Wine 不适应的内容。

介绍

常用调试方法

Wine 为调试问题提供了多种方法。大多数 wine 开发人员更喜欢使用Wine的调试通道收集日志来解决问题。您可以在开发人员调试日志使用指南中了解如何使用调试通道来记录日志的更多内容。

本文的剩余部分详细介绍了 Wine 的内部调试器 winedbg 的使用。

在底层操作系统和 Windows 中的进程和线程

在深入讲解 Wine 的调试之前,下面是 Wine 中对进程和线程处理的小概述。必须清楚的是,我们有两种不同的模型:从 Unix 角度看到的进程/线程,从 Windows 角度看到的进程/线程。

每个 Windows 线程都用一个 Unix 线程来实现,这意味着同一个 Windows 进程的所有线程共享相同的 Unix 进程地址空间。以下中:

  • W-process 表示 Windows 中的进程
  • U-process 表示 Unix 中的进程
  • W-thread 表示 Windows 中的线程

一个 W-process 由一个或多个 W-thread 组成。每个 W-thread 映射到一个且只有一个 U-process。同一个 W-process 的所有 U-process 共享相同的地址空间。

所以每个 Unix 进程都可以用两个值来标识:

  • Unix 进程 ID( 简称 upid)
  • Windows 线程 ID(简称 tID)

每个 Windows 进程还具有 Windows 进程 ID(简称 wpid)。必须清楚,upidwpid 是不同的,不能相互替代。wpidtid 是 Windows 系统层面定义的,它们不能与进程或线程句柄混淆,因为任何句柄都指向系统对象(在本例中为进程或线程)。同一个进程可以对同一个内核对象有多个不同的句柄。句柄可以定义为局部(值仅在同一个进程中有效),也可以定义为系统范围的(任何 W-process 都可以使用相同的句柄)。

Wine、调试和 Winedbg

在 Wine 中谈到调试时,至少需要考虑两个层次:

  • Windows 调试 API。
  • Wine 集成调试器,被称为 winedbg

Wine 实现了大多数 Windows 调试 API。调试 API 的第一部分在 KERNEL32.DLL 中实现,允许称为调试器的 W-process 控制另一个被调试的 W-process 的执行。控制意味着停止/恢复执行、启用/禁用单步、设置断点、读写内存… 等等。调试 API 的另一部分在 DBGHELP.DLL (依赖IMGHLP.DLL) 中实现,允许调试器查看任何模块中的符号和符号类型(如果模块已使用调试选项编译)。

winedbg 就是一个使用这些 API 的 Winelib 应用程序,允许调试任何 Wine 或 Winelib 应用程序以及 Wine 本身。

调试教程

这些教程针对的是了解 C 语言编程,但刚刚开始参与开发 wine 的人。它们旨在向您演示在应用程序不工作时怎样调试问题。

winedbg 启动方法

启动一个进程

任何程序(原生的 Windows 程序或链接 Winelib 的程序)都可以用 winedbg 来运行,命令行选项跟wine 一样的:

winedbg telnet.exe
winedbg hl.exe -windowed

附加一个进程

winedbg 也可以不加任何命令行参数来启动: 此时 winedbg 以没有附加任何进程方式启动。您可以使用 info proc 命令获取正在运行的 W-process (及其 wpid)的列表,然后使用 attach 命令跟一个要调试的 W-processwpid 参数。这功能允许您调试已经启动的应用程序。

在发生异常的时候

当出现问题时,Windows 会将它作为异常进行跟踪。比如有分段违例、堆栈溢出、除零等异常。

发生异常时,Wine 会检查 W-process 是否被调试。如果是,异常事件将发送到调试器,调试器负责是否传递该异常。此机制是标准 Windows 调试 API 的一部分。

如果 W-process 没有被调试,Wine 会尝试启动调试器。此调试器(通常是 winedbg,请参阅下一节的配置以了解更多详细信息),在启动时附加到生成异常事件的 W-process 。在这种情况下,您可以查看异常的原因,并修复原因(和继续执行)或深入挖掘以了解出错的原因。

如果 winedbg 是标准调试器,则 passcont 命令是让进程进一步处理异常事件的两种方法。

更精确地说: 当发生故障时(分段违例、堆栈溢出…),该事件首先发送到调试器(这称为第一次异常处理机会)。调试器可以给出两个答案:

  • continue
    调试器能够修复这个异常,并且能够让程序继续执行。
  • pass
    调试器在第一次异常处理机会时不能修复这个异常。Wine 将尝试遍历异常处理程序列表,查看其中一个处理程序是否可以处理该异常。如果未找到异常处理程序,则再次将这个异常发送到调试器,以指示异常处理失败。

注意:由于某些 Wine 代码使用异常和 try/catch 块来实现某些功能,因此在这种情况下winedbg 收到 segv 异常而停下来。例如,使用 IsBadReadPtr 函数时会发生这种情况。在这种情况下,应使用pass 命令,以便由 IsBadReadPtr 中的 catch 块处理异常。

中断

您可以在 winedbg 窗口同时按下 Ctrl+C 来停止正在运行的被调试程序,并允许您在 winedbg 里面操作被调试程序的进程上下文。

退出

Wine 支持新的 XP API,允许调试器从被调试程序上分离(见下文的 detach 命令)。

使用 Wine 调试器

这一节介绍从何处开始调试 wine。如果您在任何时候卡住了并且需要帮助,请阅读Wine 用户指南之如何报告bug一节

崩溃

崩溃时候我们通常看到类似这样的对话框:

Unhandled exception: page fault on write access to 0x00000000 in 32-bit code (0x0043369e).
Register dump:
 CS:0023 SS:002b DS:002b ES:002b FS:0063 GS:006b
 EIP:0043369e ESP:0b3ee90c EBP:0b3ee938 EFLAGS:00010246(  R- --  I  Z- -P- )
 EAX:00000072 EBX:7b8acff4 ECX:00000000 EDX:6f727265
 ESI:7ba3b37c EDI:7ffa0000
Stack dump:
0x0b3ee90c:  7b82ced8 00000000 7ba3b348 7b884401
0x0b3ee91c:  7b883cdc 00000008 00000000 7bc36e7b
0x0b3ee92c:  7b8acff4 7b82ceb9 7b8acff4 0b3eea18
0x0b3ee93c:  7b82ce82 00000000 00000000 00000000
0x0b3ee94c:  00000000 0b3ee968 70d7ed7b 70c50000
0x0b3ee95c:  00000000 0b3eea40 7b87fd40 7b82d0d0
Backtrace:
=>0 0x0043369e in elementclient (+0x3369e) (0x0b3ee938)
  1 0x7b82ce82 CONSOLE_SendEventThread+0xe1(pmt=0x0(nil)) [/usr/src/debug/wine-1.5.14/dlls/kernel32/console.c:1989] in kernel32 (0x0b3eea18)
  2 0x7bc76320 call_thread_func_wrapper+0xb() in ntdll (0x0b3eea28)
  3 0x7bc7916e call_thread_func+0x7d(entry=0x7b82cda0, arg=0x0(nil), frame=0xb3eeb18) [/usr/src/debug/wine-1.5.14/dlls/ntdll/signal_i386.c:2522] in ntdll (0x0b3eeaf8)
  4 0x7bc762fe RtlRaiseException+0x21() in ntdll (0x0b3eeb18)
  5 0x7bc7f3da start_thread+0xe9(info=0x7ffa0fb8) [/usr/src/debug/wine-1.5.14/dlls/ntdll/thread.c:408] in ntdll (0x0b3ef368)
  6 0xf7597adf start_thread+0xce() in libpthread.so.0 (0x0b3ef468)
0x0043369e: movl    %edx,0x0(%ecx)
Modules:
Module  Address         Debug info  Name (143 modules)
PE    340000-  3af000   Deferred        speedtreert
PE    3b0000-  3d6000   Deferred        ftdriver
PE    3e0000-  3e6000   Deferred        immwrapper
PE    400000-  b87000   Export          elementclient
PE    b90000-  e04000   Deferred        elementskill
PE    e10000-  e42000   Deferred        ifc22
PE  10000000-10016000   Deferred        zlibwapi
ELF 41f75000-41f7e000   Deferred        librt.so.1
ELF 41ff9000-42012000   Deferred        libresolv.so.2
PE  48080000-480a8000   Deferred        msls31
PE  65340000-653d2000   Deferred        oleaut32
PE  70200000-70294000   Deferred        wininet
PE  702b0000-70328000   Deferred        urlmon
PE  70440000-704cf000   Deferred        mlang
PE  70bd0000-70c34000   Deferred        shlwapi
PE  70c50000-70ef3000   Deferred        mshtml
PE  71930000-719b8000   Deferred        shdoclc
PE  78130000-781cb000   Deferred        msvcr80
ELF 79afb000-7b800000   Deferred        libnvidia-glcore.so.304.51
ELF 7b800000-7ba3d000   Dwarf           kernel32<elf>
  \-PE  7b810000-7ba3d000   \               kernel32
ELF 7bc00000-7bcd5000   Dwarf           ntdll<elf>
  \-PE  7bc10000-7bcd5000   \               ntdll
ELF 7bf00000-7bf04000   Deferred        <wine-loader>
ELF 7c288000-7c400000   Deferred        libvorbisenc.so.2
PE  7c420000-7c4a7000   Deferred        msvcp80
ELF 7c56d000-7c5b6000   Deferred        dinput<elf>
  \-PE  7c570000-7c5b6000   \               dinput
ELF 7c5b6000-7c600000   Deferred        libdbus-1.so.3
ELF 7c70e000-7c715000   Deferred        libasyncns.so.0
ELF 7c715000-7c77e000   Deferred        libsndfile.so.1
ELF 7c77e000-7c7e5000   Deferred        libpulsecommon-1.1.so
ELF 7c7e5000-7c890000   Deferred        krnl386.exe16.so
PE  7c7f0000-7c890000   Deferred        krnl386.exe16
ELF 7c890000-7c900000   Deferred        ieframe<elf>
  \-PE  7c8a0000-7c900000   \               ieframe
ELF 7ca00000-7ca1a000   Deferred        rasapi32<elf>
  \-PE  7ca10000-7ca1a000   \               rasapi32
ELF 7ca1a000-7ca21000   Deferred        libnss_dns.so.2
ELF 7ca21000-7ca25000   Deferred        libnss_mdns4_minimal.so.2
ELF 7ca25000-7ca2d000   Deferred        libogg.so.0
ELF 7ca2d000-7ca5a000   Deferred        libvorbis.so.0
ELF 7cd5d000-7cd9c000   Deferred        libflac.so.8
ELF 7cd9c000-7cdea000   Deferred        libpulse.so.0
ELF 7cdfe000-7ce23000   Deferred        iphlpapi<elf>
  \-PE  7ce00000-7ce23000   \               iphlpapi
ELF 7cff1000-7cffd000   Deferred        libnss_nis.so.2
ELF 7d60d000-7d629000   Deferred        wsock32<elf>
  \-PE  7d610000-7d629000   \               wsock32
ELF 7d80d000-7d828000   Deferred        libnsl.so.1
ELF 7d8cf000-7d8db000   Deferred        libgsm.so.1
ELF 7d8db000-7d903000   Deferred        winepulse<elf>
  \-PE  7d8e0000-7d903000   \               winepulse
ELF 7d95c000-7d966000   Deferred        libwrap.so.0
ELF 7d966000-7d96d000   Deferred        libxtst.so.6
ELF 7d96d000-7d992000   Deferred        mmdevapi<elf>
  \-PE  7d970000-7d992000   \               mmdevapi
ELF 7d9b3000-7d9d0000   Deferred        msimtf<elf>
  \-PE  7d9c0000-7d9d0000   \               msimtf
ELF 7d9d0000-7d9e5000   Deferred        comm.drv16.so
PE  7d9e0000-7d9e5000   Deferred        comm.drv16
ELF 7da83000-7db5f000   Deferred        libgl.so.1
ELF 7db60000-7db63000   Deferred        libx11-xcb.so.1
ELF 7db63000-7db78000   Deferred        system.drv16.so
PE  7db70000-7db78000   Deferred        system.drv16
ELF 7db98000-7dca1000   Deferred        opengl32<elf>
  \-PE  7dbb0000-7dca1000   \               opengl32
ELF 7dca1000-7dcb6000   Deferred        vdmdbg<elf>
  \-PE  7dcb0000-7dcb6000   \               vdmdbg
ELF 7dcce000-7dd04000   Deferred        uxtheme<elf>
  \-PE  7dcd0000-7dd04000   \               uxtheme
ELF 7dd04000-7dd0a000   Deferred        libxfixes.so.3
ELF 7dd0a000-7dd15000   Deferred        libxcursor.so.1
ELF 7dd16000-7dd1f000   Deferred        libjson.so.0
ELF 7dd24000-7dd38000   Deferred        psapi<elf>
  \-PE  7dd30000-7dd38000   \               psapi
ELF 7dd78000-7dda1000   Deferred        libexpat.so.1
ELF 7dda1000-7ddd6000   Deferred        libfontconfig.so.1
ELF 7ddd6000-7dde6000   Deferred        libxi.so.6
ELF 7dde6000-7ddef000   Deferred        libxrandr.so.2
ELF 7ddef000-7de11000   Deferred        libxcb.so.1
ELF 7de11000-7df49000   Deferred        libx11.so.6
ELF 7df49000-7df5b000   Deferred        libxext.so.6
ELF 7df5b000-7df75000   Deferred        libice.so.6
ELF 7df75000-7e005000   Deferred        winex11<elf>
  \-PE  7df80000-7e005000   \               winex11
ELF 7e005000-7e0a5000   Deferred        libfreetype.so.6
ELF 7e0a5000-7e0c5000   Deferred        libtinfo.so.5
ELF 7e0c5000-7e0ea000   Deferred        libncurses.so.5
ELF 7e123000-7e1eb000   Deferred        crypt32<elf>
  \-PE  7e130000-7e1eb000   \               crypt32
ELF 7e1eb000-7e235000   Deferred        dsound<elf>
  \-PE  7e1f0000-7e235000   \               dsound
ELF 7e235000-7e2a7000   Deferred        ddraw<elf>
  \-PE  7e240000-7e2a7000   \               ddraw
ELF 7e2a7000-7e3e3000   Deferred        wined3d<elf>
  \-PE  7e2b0000-7e3e3000   \               wined3d
ELF 7e3e3000-7e417000   Deferred        d3d8<elf>
  \-PE  7e3f0000-7e417000   \               d3d8
ELF 7e417000-7e43b000   Deferred        imm32<elf>
  \-PE  7e420000-7e43b000   \               imm32
ELF 7e43b000-7e46f000   Deferred        ws2_32<elf>
  \-PE  7e440000-7e46f000   \               ws2_32
ELF 7e46f000-7e49a000   Deferred        msacm32<elf>
  \-PE  7e470000-7e49a000   \               msacm32
ELF 7e49a000-7e519000   Deferred        rpcrt4<elf>
  \-PE  7e4b0000-7e519000   \               rpcrt4
ELF 7e519000-7e644000   Deferred        ole32<elf>
  \-PE  7e530000-7e644000   \               ole32
ELF 7e644000-7e6f7000   Deferred        winmm<elf>
  \-PE  7e650000-7e6f7000   \               winmm
ELF 7e6f7000-7e7fa000   Deferred        comctl32<elf>
  \-PE  7e700000-7e7fa000   \               comctl32
ELF 7e7fa000-7ea23000   Deferred        shell32<elf>
  \-PE  7e810000-7ea23000   \               shell32
ELF 7ea23000-7eaf9000   Deferred        gdi32<elf>
  \-PE  7ea30000-7eaf9000   \               gdi32
ELF 7eafb000-7eaff000   Deferred        libnvidia-tls.so.304.51
ELF 7eaff000-7eb09000   Deferred        libxrender.so.1
ELF 7eb09000-7eb0f000   Deferred        libxxf86vm.so.1
ELF 7eb0f000-7eb18000   Deferred        libsm.so.6
ELF 7eb18000-7eb32000   Deferred        version<elf>
  \-PE  7eb20000-7eb32000   \               version
ELF 7eb32000-7ec87000   Deferred        user32<elf>
  \-PE  7eb40000-7ec87000   \               user32
ELF 7ec87000-7ecf1000   Deferred        advapi32<elf>
  \-PE  7ec90000-7ecf1000   \               advapi32
ELF 7ecf1000-7ed8f000   Deferred        msvcrt<elf>
  \-PE  7ed00000-7ed8f000   \               msvcrt
ELF 7ef8f000-7ef9c000   Deferred        libnss_files.so.2
ELF 7ef9c000-7efc7000   Deferred        libm.so.6
ELF 7efc8000-7efe5000   Deferred        libgcc_s.so.1
ELF 7efe5000-7f000000   Deferred        crtdll<elf>
  \-PE  7eff0000-7f000000   \               crtdll
ELF f73d0000-f73d4000   Deferred        libxinerama.so.1
ELF f73d4000-f73d8000   Deferred        libxau.so.6
ELF f73da000-f73df000   Deferred        libdl.so.2
ELF f73df000-f7591000   Dwarf           libc.so.6
ELF f7591000-f75ab000   Dwarf           libpthread.so.0
ELF f75ab000-f76ef000   Dwarf           libwine.so.1
ELF f7722000-f7728000   Deferred        libuuid.so.1
ELF f7729000-f774a000   Deferred        ld-linux.so.2
ELF f774a000-f774b000   Deferred        [vdso].so
Threads:
process  tid      prio (all id:s are in hex)
00000008 (D) C:\Perfect World Entertainment\Perfect World International\element\elementclient.exe
    00000031    0 <==
    00000035   15
    00000012    0
    00000021    0
    00000045    0
    00000044    0
    00000043    0
    00000038   15
    00000037    0
    00000036   15
    00000034    0
    00000033    0
    00000032    0
    00000027    0
    00000009    0
0000000e services.exe
    0000000b    0
    00000020    0
    00000017    0
    00000010    0
    0000000f    0
00000014 winedevice.exe
    0000001e    0
    0000001b    0
    00000016    0
    00000015    0
0000001c plugplay.exe
    00000022    0
    0000001f    0
    0000001d    0
00000023 explorer.exe
    00000024    0

调试崩溃的步骤。您可能在任何步骤中崩溃,但请报告 Bug,并在 Bug 报告中提供收集到的尽可能多的信息。

  1. 了解崩溃的原因。通常是页面错误、调用了Wine 中未实现的函数,或类似的原因。报告崩溃时,报告整个崩溃转储,即使它对您没有意义。 (在这个例子里面,在写入 0x0000000 时出现页面错误。最有可能的 Wine 将 NULL 传递给应用程序或类似。)
  2. 确定崩溃的原因。由于通常是 Wine 实现的函数执行失败或者行为不正确导致的主要/次要反应,因此使用WINEDEBUG=+relay 环境变量重新运行 Wine。这将生成相当多的日志输出,但通常原因是位于最后一个函数调用中。这些日志通常如下所示:
  3. 如果你已经发现了一个行为不正常的 Wine 函数,尝试找出它行为不正常的原因。在源代码中查找函数。试着理解传递的函数参数。通常有一个 WINE_DEFAULT_DEBUG_CHANNEL(channel); 在源文件的开头。使用 WINEDEBUG=+xyz,+relay 环境变量重新运行 wine。有时,在源文件的开头以WINE_DECLARE_DEBUG_CHANNEL(channel)的形式定义了其他调试通道;如果是这样,有问题的函数也可能使用了这些备用通道之一。在该函数中搜索 TRACE_(channel)(".../n"); 并将找到的这些额外的通道添加到 WINEDEBUG 环境变量里面。
  4. 有关如何使用 winedbg 进行调试的其他信息,请参阅源码 programs/winedbg/README
  5. 如果这些信息不够清晰,或者您想知道该函数发生的更多信息,请尝试使用 WINEDEBUG=+all重新运行 wine ,这将转储 wine 里面包含调试信息在内的所有日志。通常需要限制生成的调试输出。这可以通过管道把输出日志发给 grep 过滤,或者使用注册表项来完成。有关详细信息,请参阅下文的 配置 +relay 行为 一节。
  6. 即使这还不够,在您认为相关的函数中手动添加更多调试日志。有关详细信息,请参阅开发人员调试日志使用指南。您也可以尝试在 gdb 中运行该程序,代替使用 Wine 调试器。如果这样做,请在 ~/.gdbinit 文件里面增加这句 handle SIGSEGV nostop noprint 来禁用 gdb 对 seg fault 错误的处理(Win16 需要)。
  7. 您还可以为该函数设置断点。用 winedbg 启动调试程序而不是 wine。一旦调试器运行起来,在命令行提示符输入命令: break RegOpenKeyExW (将 RegOpenKeyExW 替换成你要调试的函数,区分大小写)以设置断点。然后,使用 continue 命令启动程序正常执行。程序运行到断点位置,程序将停止;如果程序还没有运行到该函数崩溃的那次调用,再次使用 continue 命令继续运行程序直到达到该函数即将崩溃的那次调用。现在,您可以用单步执行命令来继续运行程序,直到达到崩溃点,然后使用其他调试器命令来查看寄存器值和相关变量值等等。

程序挂起,没有反应

用 winedbg 启动程序而不是 wine 。当程序没有反应时候,切换到 winedbg 窗口,并按 Ctrl+C 。这将停止程序,并允许您调试该程序,就像崩溃时候一样。

程序弹出错误消息框

有时候程序使用或多或少的非描述性消息框报告失败。我们可以使用与崩溃相同的方法进行调试,但有一个问题…为了设置消息框,程序会多出大量的调试日志。

由于故障通常发生在设置消息框之前,您可以启动 winedbg 并在 MessageBoxA (由 win16 和 win32程序调用)处设置断点,然后继续运行。程序将在设置消息框之前停止。

您也可以使用这个命令来运行程序:
WINEDEBUG=+relay wine program.exe 2&gt;&amp;1 | less -i
然后在 less 里面搜索 MessageBox .

反汇编程序

您也可以尝试反汇编有问题的程序,以检查没有公开的功能或使用它们。

理解汇编代码主要是一个练习问题。Win16 函数入口通常如下所示:

push bp
mov bp, sp
... 函数代码 ..
retf XXXX   <--------- XXXX 是函数参数的总字节数

这是一个没有局部变量的 FAR 函数。参数通常从 [bp+6] 开始,偏移量增加。请注意,对于使用 PASCAL 调用约定导出的 win16 函数, [bp+6] 属于最右侧的参数。因此,如果我们使用带 abstrcmp(a,b) 来说,则参数b 的存储位置在 [bp+6],参数 a 的存储位置在 [bp+10]

大多数函数用栈存储局部变量:

enter 0086, 00
... 函数代码 ...
leave
retf XXXX

这与上述内容基本相同,但还添加了 0x86 字节的栈存储,使用 [bp-xx] 进行访问。在调用该函数之前,使用如下所示的代码把参数压到栈上:

push word ptr [bp-02]   <- 压到 [bp+8] 处
push di                 <- 压到 [bp+6] 处
call KERNEL.LSTRLEN

在这里,首先压人选择器地址,然后压入传递的字符串的偏移量。

调试示例

让我们调试臭名昭著的 WORD SHARE.EXE 消息框:

|marcus@jet $ wine winword.exe
|            +---------------------------------------------+
|            | !  You must leave Windows and load SHARE.EXE|
|            |    before starting Word.                    |
|            +---------------------------------------------+
|marcus@jet $ WINEDEBUG=+relay,-debug wine winword.exe
|CallTo32(wndproc=0x40065bc0,hwnd=000001ac,msg=00000081,wp=00000000,lp=00000000)
|Win16 task 'winword': Breakpoint 1 at 0x01d7:0x001a
|CallTo16(func=0127:0070,ds=0927)
|Call WPROCS.24: TASK_RESCHEDULE() ret=00b7:1456 ds=0927
|Ret  WPROCS.24: TASK_RESCHEDULE() retval=0x8672 ret=00b7:1456 ds=0927
|CallTo16(func=01d7:001a,ds=0927)
|     AX=0000 BX=3cb4 CX=1f40 DX=0000 SI=0000 DI=0927 BP=0000 ES=11f7
|Loading symbols: /home/marcus/wine/wine...
|Stopped on breakpoint 1 at 0x01d7:0x001a
|In 16 bit mode.
|Wine-dbg&gt;break MessageBoxA                          c                                            &lt;---- Continue
|Call KERNEL.91: INITTASK() ret=0157:0022 ds=08a7
|     AX=0000 BX=3cb4 CX=1f40 DX=0000 SI=0000 DI=08a7 ES=11d7 EFL=00000286
|CallTo16(func=090f:085c,ds=0dcf,0x0000,0x0000,0x0000,0x0000,0x0800,0x0000,0x0000,0x0dcf)
|...                                                   &lt;----- Much debug output
|Call KERNEL.136: GETDRIVETYPE(0x0000) ret=060f:097b ds=0927
                               ^^^^^^ Drive 0 (A:)
|Ret  KERNEL.136: GETDRIVETYPE() retval=0x0002 ret=060f:097b ds=0927
                                        ^^^^^^  DRIVE_REMOVEABLE
                        (It is a floppy diskdrive.)

|Call KERNEL.136: GETDRIVETYPE(0x0001) ret=060f:097b ds=0927
                               ^^^^^^ Drive 1 (B:)
|Ret  KERNEL.136: GETDRIVETYPE() retval=0x0000 ret=060f:097b ds=0927
                                        ^^^^^^  DRIVE_CANNOTDETERMINE
                        (I don't have drive B: assigned)

|Call KERNEL.136: GETDRIVETYPE(0x0002) ret=060f:097b ds=0927
                               ^^^^^^^ Drive 2 (C:)
|Ret  KERNEL.136: GETDRIVETYPE() retval=0x0003 ret=060f:097b ds=0927
                                        ^^^^^^ DRIVE_FIXED
                                               (specified as a hard disk)

|Call KERNEL.97: GETTEMPFILENAME(0x00c3,0x09278364&quot;doc&quot;,0x0000,0927:8248) ret=060f:09b1 ds=0927
                                 ^^^^^^           ^^^^^        ^^^^^^^^^
                                 |                |            |buffer for fname
                                 |                |temporary name ~docXXXX.tmp
                                 |Force use of Drive C:.

|Warning: GetTempFileName returns 'C:~doc9281.tmp', which doesn't seem to be writable.
|Please check your configuration file if this generates a failure.

哎呀,日志中发现了问题 (OPENFILE 失败):

|Ret  KERNEL.97: GETTEMPFILENAME() retval=0x9281 ret=060f:09b1 ds=0927
                                          ^^^^^^ Temporary storage ID

|Call KERNEL.74: OPENFILE(0x09278248&quot;C:~doc9281.tmp&quot;,0927:82da,0x1012) ret=060f:09d8 ds=0927
                                    ^^^^^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^
                                    |filename        |OFSTRUCT |open mode:

                                       OF_CREATE|OF_SHARE_EXCLUSIVE|OF_READWRITE

这里失败的原因是我的C盘是只读的

|Ret  KERNEL.74: OPENFILE() retval=0xffff ret=060f:09d8 ds=0927
                                   ^^^^^^ HFILE_ERROR16, yes, it failed.

|Call USER.1: MESSAGEBOX(0x0000,0x09278376&quot;You must close Windows and load SHARE.EXE before you start Word.&quot;,0x00000000,0x1030) ret=060f:084f ds=0927

并且在 MessageBoxA 的入口停下来了:

|Stopped on breakpoint 2 at 0x40189100 (MessageBoxA [msgbox.c:190])
|190     {      

代码看起来要找一个可写磁盘,并试图在该磁盘创建一个文件。要解决此 Bug,可以将C盘定义为网络驱动器,上述代码将忽略该驱动器。

调试技巧

以下是一些其他调试技巧:

  1. 如果您有一个程序在加载前期崩溃,以至于您无法正常使用 Wine 调试器来调试,但 Wine 已执行该程序的启动代码,则可以使用特殊的技巧。您应该执行WINEDEBUG=+relay wine program获取程序在启动函数中调用的所有函数清单。现在,你执行:winedbg winfile.exe这样,你就进入 winedbg。现在,您可以在 start 函数中调用的任何函数上设置断点,然后不断按 c 以跳过 Winfile 对此函数的正常调用,直到您最终到达此函数调用崩溃的位置。现在,您可以像平常一样继续调试该程序。
  2. 如果尝试运行程序,程序在弹出错误消息框后就退出,则问题的原因通常可以检查在 MessageBox 之前调用的一些函数的返回值发现。你应该用下面的方式重新运行程序:
    WINEDEBUG=+relay wine program_name &amp;&gt;relmsg
    接着执行 more relmsg 然后搜索最后一个出现的 MESSAGEBOX,类似这样的:
    Call USER.1: MESSAGEBOX(0x0000,0x01ff1246 "Runtime error 219 at 0004:1056.",0x00000000,0x1010) ret=01f7:2160 ds=01ff
    在我的例子里面,在调用 MessageBox 函数之前的代码类似这样:
Call KERNEL.96: FREELIBRARY(0x0347) ret=01cf:1033 ds=01ff
CallTo16(func=033f:0072,ds=01ff,0x0000)
Ret  KERNEL.96: FREELIBRARY() retval=0x0001 ret=01cf:1033 ds=01ff
Call KERNEL.96: FREELIBRARY(0x036f) ret=01cf:1043 ds=01ff
CallTo16(func=0367:0072,ds=01ff,0x0000)
Ret  KERNEL.96: FREELIBRARY() retval=0x0001 ret=01cf:1043 ds=01ff
Call KERNEL.96: FREELIBRARY(0x031f) ret=01cf:105c ds=01ff
CallTo16(func=0317:0072,ds=01ff,0x0000)
Ret  KERNEL.96: FREELIBRARY() retval=0x0001 ret=01cf:105c ds=01ff
Call USER.171: WINHELP(0x02ac,0x01ff05b4 "COMET.HLP",0x0002,0x00000000) ret=01cf:1070 ds=01ff
CallTo16(func=0117:0080,ds=01ff)
Call WPROCS.24: TASK_RESCHEDULE() ret=00a7:0a2d ds=002b
Ret  WPROCS.24: TASK_RESCHEDULE() retval=0x0000 ret=00a7:0a2d ds=002b
Ret  USER.171: WINHELP() retval=0x0001 ret=01cf:1070 ds=01ff
Call KERNEL.96: FREELIBRARY(0x01be) ret=01df:3e29 ds=01ff
Ret  KERNEL.96: FREELIBRARY() retval=0x0000 ret=01df:3e29 ds=01ff
Call KERNEL.52: FREEPROCINSTANCE(0x02cf00ba) ret=01f7:1460 ds=01ff
Ret  KERNEL.52: FREEPROCINSTANCE() retval=0x0001 ret=01f7:1460 ds=01ff
Call USER.1: MESSAGEBOX(0x0000,0x01ff1246 "Runtime error 219 at 0004:1056.",0x00000000,0x1010) ret=01f7:2160 ds=01ff

我认为本示例中对 MessageBox 的调用不是由以前调用的函数返回错误值引起的(经常发生这样的情况),而是消息框里面提到的: 0x0004:0x1056 处出现运行时错误。由于地址的段值仅为 4,因此我认为这只是一个内部值。但偏移地址揭示了一些非常有趣的内容:偏移 0x1056 非常接近 FREELIBRARY 的返回地址():

Call KERNEL.96: FREELIBRARY(0x031f) ret=01cf:105c ds=01ff
                                             ^^^^

如果段 0x0004 确实是段 0x1cf,我们可以反汇编调用FreeLibrary的地址,分析发生运行时错误之前的某些行。
3. 如果希望设置某个位置的断点,但该断点所在的模块还没有映射到内存里面,则可以将断点设置为GetVersion16/32 函数,因为这些函数被调用很频繁,断点停下来的时候执行 continue 命令直到您能够设置此断点而不再显示错误消息。

调试器的基本用法

使用 winebg myprog.exe 启动程序后,程序加载并在起点处停止,终端显示 winedbg 命令行提示符。然后,您可以这样设置断点:

b RoutineName (按函数名称加断点)或
b *0x812575   (按地址加断点)

然后,您输入 c(continue命令简写)来运行程序。当它停在断点处后,您可以键入:

step            (一次步进一行)或
stepi           (一次步进一个机器指令;它有助于了解386基本指令集)
info reg        (查看寄存器)
info stack      (查看堆栈中的十六进制值)
info local      (查看局部变量)
list 行号       (列出源代码)
x 变量名称      (检查变量;仅当代码关闭优化编译时候有效)
x 0x4269978     (检查内存位置的内容)
?               (帮助)
q               (退出)

直接按 Enter,您可以重复最后一个命令。

有用的程序

一些有用的程序:

  • IDA:IDA Pro 是强烈推荐的,但不是免费的。

  • pedump:http://pedump.me/, 转储 PE格式的DLL 的导入和导出。

  • winedump: (包括在 wine 中), 转储 PE格式的DLL 的导入和导出。

配置

Windows 调试配置

Windows 调试 API 使用这个注册表项来指明发生未处理异常时要调用哪个调试器。

[MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug]

有两个值来决定行为:

  • Debugger
    指定用于启动调试器的命令行(它使用两个 printf 格式占位符 (%ld) 将上下文相关信息传递给调试器)。您应该在这里放置一个您的调试器的完整路径 ( winedbg 当然可以使用,但任何其他使用 Windows 调试 API 的调试器也可以 )。您选择使用的调试器的路径必须通过 wine 容器根目录的 dosdevices 子目录里面配置的DOS驱动器之一进行访问。
  • Auto
    如果此值为零,在发生未处理异常时将弹出对话框询问用户是否希望启动调试器。否则,调试器将自动启动。

默认的 wine 注册表如下所示:

[MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug] 957636538
"Auto"=dword:00000001
"Debugger"="winedbg %ld %ld"

注意 1: 创建这个注册表项是必需的。如果不这样做,在发生异常时不会触发调试器。

注意 2: wineinstall (wine 附带的)创建这个注册表项。但是由于安装的注册表存在一些限制,如果存在以前的 Wine 安装,则先删除整个 [MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug] ,再运行 wineinstall 以重新创建是安全的。

WineDbg 配置

winedbg 可通过多种选项进行配置。这些选项按用户存储在注册表中:[HKCU\\Software\\Wine\\WineDbg]

这些选项可以在 winedbg 里面读取/写入,作为调试器表达式的一部分。要引用这些选项之一,其名称必须以 $ 符号为前缀。例如,

set $BreakAllThreadsStartup = 1

BreakAllThreadsStartup 选项设置为 TRUE。

所有选项在 winedbg 启动时从注册表中读取(如果未找到相应的值,则使用默认值),并在 winedbg 退出时写回注册表。

下面是所有选项的列表:

  • BreakAllThreadsStartup
    如果为 TRUE 在所有线程启动时调试器停止;如果为 FALSE 仅在给定进程的第一个线程启动时调试器停止。默认情况下为 FALSE。
  • BreakOnCritSectTimeOut
    如果为 TRUE 当临界区超时(5 分钟)时调试器停止;默认情况下为 TRUE。
  • BreakOnAttach
    如果为 TRUE 在未处理异常发生后 winedbg 附加到进程时,在第一个附加事件中停下来。由于附加事件在异常事件的上下文中没有意义,因此该选项最好是 FALSE。
  • BreakOnFirstChance
    异常生成两个调试事件。第一个是在异常发生之后传递到调试器(称为第一次机会)。调试器可 以决定恢复执行(通过 winedbg cont命令)或将异常传递给程序中的异常处理程序链(如果存在)(winedbg 通过 pass 命令)。如果异常处理程序没有处理异常,则异常事件将再次发送到调试器(称为最后一次机会)。调试器不能传递最后一次机会的异常。如果 BreakOnFirstChance 为TRUE ,则第一次机会异常和最后一次机会异常发生时 winedbg 都停止;如果为 FALSE,仅在最后一次机会异常是停止。

配置 +relay 行为

WINEDEBUG 设置为 +relay 调试时,可能会得到大量输出日志。您可以通过把注册表中
[HKCU\\Software\\Wine\\Debug] 下的 RelayExclude 键值设置为用分号分隔的要排除的函数列表,例如:

"RtlEnterCriticalSection;RtlLeaveCriticalSection;kernel32.97;kernel32.98"

RelayIncludeRelayExclude 类似,只不过列出的函数将是输出中仅包含的函数。

如果应用程序使用 +relay 运行速度太慢无法获得有意义的输出,并且对生成的几 GB 的日志文件束手无策,不确定要排除哪些函数,下面是一个技巧。首先,运行应用程序一分钟左右,将其输出重定向到磁盘上的文件:

WINEDEBUG=+relay wine appname.exe &>relay.log

然后运行此命令以查看调用最多的函数:

awk -F'(' '{print $1}' < relay.log | awk '{print $2}' | sort | uniq -c | sort

在确保这些函数不相关后使用 RelayExclude 排除调用最多的函数,然后再次运行应用程序。

WineDbg 表达式和变量

表达式

winedbg 中的表达式大多以 C 形式编写。但是有一些差异:

  • 标识符的名称中可以加一个 !,这主要区分不同 DLL 的符号,如 USER32!CreateWindowExA 表示 USER32.DLL里面的 CreateWindowExA 函数。
  • 在强制转换操作中,在指定结构或联合时,必须使用 structunion 关键字(即使程序使用 typedef)。

当按名称指定标识符时,如果存在多个相同名称的符号,调试器将提示用户要选择哪个符号,输入你想要的那个符号前面的数字序号即可。

变量

winedbg 定义自己的变量集。上面的配置变量是其中的一部分。其他包括:

  • $ThreadId 当前调试的 W-thread 的 ID
  • $ProcessId 当前调试的 W-process 的 ID
  • 寄存器变量 所有 CPU 寄存器用"$"前缀加寄存器名来访问。您可以使用 info regs 来获取 CPU 寄存器的名称列表。
  • $ThreadId$ProcessId 变量可以很方便地在指定的线程或进程上设置条件断点。

WineDbg 命令参考

杂项

abort           中止调试器
quit            退出调试器
attach N        附加到 `W-process` 进程(N 是其 ID,10进制数字或十六进制 (0xN))。
                ID 可以使用 `info process` 命令获取。请注意,`info process` 命令返回的是十六进制值。
detach          从 `W-process` 进程分离。
help            打印一些帮助
help info       打印一些 info 命令的帮助

流程控制

cont,c          继续运行直到下一个断点或异常。
pass            将异常事件传递给异常处理链。
step,s          继续执行,直到下一行&quot;C&quot;代码(进入函数内部)
next,n          继续执行,直到下一行&quot;C&quot;代码(不进入函数内部)
stepi,si        执行下一个指令(进入函数内部)
nexti,ni        执行下一个指令(不进入函数内部)
finish,f        执行,直到当前函数返回

cont、step、next、stepi、nexti 命令后面可以加一个数字 (N) 参数,表示命令执行 N 次。

断点,监视点

enable N        启用编号为 N 的断点或监视点
disable N       禁用编号为 N 的断点或监视点
delete N        删除编号为 N 的断点或监视点
cond N          删除编号为 N 的断点或监视点的条件
cond N expr     设置编号为 N 的断点或监视点的条件;每次断点命中时,都会计算表达式 expr ,如果结果为零值,则不触发断点。
break *N        在地址 N 处添加断点
break ID        在符号 ID 的地址添加断点
break N         在当前源文件的第 N 行添加断点
watch *N        在地址 N 处添加写监视点
watch id        在符号 ID 的地址添加写监视点
info break      列出所有断点或监视点的状态

您可以使用符号 EntryPoint 代表 DLL 的入口点。

在按符号名称设置断点或监视点时,如果找不到符号(例如,符号所在模块还没有加载),winedbg 将记住符号的名称,并在每次加载新模块时尝试设置该断点(直到成功)。

栈帧操作

bt              打印当前线程的调用栈
bt N            打印线程ID为 N 的线程的调用堆栈(注意:这不会更改当前帧的位置,因为它们由down和up命令操纵)
up              当前线程栈中向上移动一帧
up  N           当前线程栈中向上移动 N 帧
down            当前线程栈中向下移动一帧
down N          当前线程栈中向下移动 N 帧
frame N         设置 N 为当前线程栈的当前帧
info local      列出当前帧的局部变量信息

目录和源文件操作

show dir        打印查找源文件的目录列表
dir pathname    将 pathname 指定的目录添加到查找源文件的目录列表里面
dir             清空查找源文件的目录列表
list            列出当前位置开始的10行源码
list -          列出当前位置往后的10行源码
list N          列出当前文件中从 N 行开始的10行源码
list path:N     列出 path 指定的文件的第N行开始的10行源码
list id         列出函数 ID 的10行源码
list *N         列出地址 N 开始的10行源码

您还可以使用逗号分隔来指定一段范围。例如:

list 123,234        列出当前文件的第 123 行到 234 行
list foo.c:1,56     列出foo.c文件的第 1 行到 56 行

显示

显示是在执行任何 winedbg 命令后计算并打印的表达式。

winedbg 将自动检测您输入的表达式是否包含局部变量。如果包含,则仅当上下文所在函数与设置显示表达式时所在的函数一样时,才会显示该局部变量的值。

info display        列出所有的活动显示
display             查看所有活动显示的值(在每次调试器停止时都执行)
display expr        添加表达式 expr 的显示
display /fmt expr   添加给定格式打印 expr 的值的显示(有关格式的更多信息,请参阅下文的打印命令用法)
undisplay N         删除显示编号为 N 的显示

反汇编

disas               从当前位置反汇编
disas expr          从 expr 指定的地址反汇编
disas expr,expr     在两个 expr 指定的地址之间反汇编

内存(读取、写入、查看)

x expr                          查看 expr 指定的地址处的内存
x /fmt expr                     使用格式 fmt 查看 expr 指定的地址处的内存
print expr                      打印 expr 的值(可能使用其类型)
print /fmt expr                 使用格式 fmt 打印 expr 的值
set lval=expr                   在 lval 中写入 expr 的值
whatis expr                     打印表达式 expr 的 C 类型
set !symbol_picker interactive  在打印值时,如果找到多个符号,询问用户要选取哪个符号(默认)
set !symbol_picker scoped       在打印值时,局部符号优先于全局符号

fmt 是字母或个数加字母(个数和字母之间没有空格),其中字母可以是以下字符:

  • s 表示 ASCII 字符串
  • u 表示 Unicode UTF16 字符串
  • i 表示一个指令 (反汇编)
  • d 表示十进制显示32位符号整数
  • x 表示十六进制显示32位无符号整数
  • w 表示十六进制显示16位无符号整数
  • b 表示十六进制显示8位无符号整数
  • c 表示 ASCII 字符(仅打印可打印的 0x20-0x7f 之间的字符)
  • g 表示 GUID

查看 Wine 内部信息

info class      列出在 Wine 中注册的所有 Windows 类
info class id   打印 Windows 类 ID 上的信息
info share      列出调试程序加载的所有模块信息(包括 .so 文件、NE 和 PE DLL)
info share N    打印地址 N 对应的模块的信息
info regs       打印 CPU 寄存器的值
info all-regs   打印的CPU和浮点寄存器的值
info stack      打印栈顶部96个字节
info map        列出调试程序使用的所有虚拟映射
info map N      列出 wpid 为 N 程序使用的所有虚拟映射
info wnd        列出从桌面窗口开始的所有窗口层次结构
info wnd N      打印句柄为 N 的窗口的信息
info process    列出当前容器里面的所有 W-process 进程信息
info thread     列出当前容器里面的所有 W-thread 线程信息
info exception  列出异常帧(从当前栈帧开始)

调试通道

在进行调试时,可以使用 set 命令打开和关闭调试通道(仅适用于 WINEDEBUG 环境变量中指定的调试通道)。有关调试通道的更多详细信息,请参阅 Wine 开发者指南 第 2 章

set + warn channel      打开指定通道的 warn 类日志
set + channel           打开指定通道的 warn/fixme/err/trace 类日志
set - channel           关闭指定通道的 warn/fixme/err/trace 类日志
set - fixme             关闭`fixme`类日志

发表评论

电子邮件地址不会被公开。 必填项已用*标注