本文主要以 Wine 官网的这篇文章 《 Debugging Wine 》 来讲解。大部分内容是对该文的翻译,修正了原文的一些书写错误,删除了原文跟最新的 Wine 不适应的内容。
介绍
常用调试方法
Wine 为调试问题提供了多种方法。大多数 Wine 开发人员更喜欢使用 Wine 的调试通道收集日志来解决问题。您可以在开发人员调试日志使用指南中了解如何使用调试通道来记录日志的更多内容。(https://wiki.winehg.org/Wine Developer%27s Guide/Debug Logging)
本文的剩余部分将详细介绍 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
)。必须清楚,upid
和wpid
是不同的,不能相互替代。wpid
和tid
是 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 的人。旨在演示当应用程序不工作时怎样调试问题。
-
调试 Reason 3 - 一个简单的"未处理异常"错误消息。介绍了调试跟踪、shell32.dll 和 SEH/异常跟踪。 https://wiki.winehq.org/Debugging_Reason_3 -
调试 PE Explorer - 修复文件打开对话框中的简单挂起(另一个 shell32 错误)。介绍了 winedbg 的堆栈回溯用法和不同类型的错误码。 https://wiki.winehq.org/Debugging_PE_Explorer -
调试 Wild Metal Country - 查找游戏崩溃的原因以及如何确认错误。 https://wiki.winehq.org/Debugging_Wild_Metal_Country -
Anastasius Focht 提交的 bug 报告里详细描述了他如何发现问题,以下网址可查看他的 bug 报告: http://bugs.winehq.org/buglist.cgi?query_format=advanced&emailreporter1=1&emaillongdesc1=1&email1=focht&emailtype1=substring
Winedbg 启动方法
启动一个进程
任何程序(原生的 Windows 程序或链接 Winelib 的程序)都可以用 winedbg 来运行,命令行选项跟 Wine 一样的:
winedbg telnet.exe
winedbg hl.exe -windowed
附加一个进程
Winedbg 也可以不加任何命令行参数来启动: 此时 winedbg 以没有附加任何进程方式启动。您可以使用info proc
命令获取正在运行的W-process
(及其wpid
)的列表,然后使用attach
命令跟一个要调试的W-process
的wpid
参数。这功能允许您调试已经启动的应用程序。
在发生异常的时候
当出现问题时,Windows 会将它作为异常进行跟踪。比如有分段违例、堆栈溢出、除零等异常。
发生异常时,Wine 会检查W-process
是否被调试。如果是,异常事件将发送到调试器,调试器负责是否传递该异常。此机制是标准 Windows 调试 API 的一部分。
如果W-process
没有被调试,Wine 会尝试启动调试器。此调试器(通常是 winedbg,请参阅下一节的配置以了解更多详细信息),在启动时附加到生成异常事件的W-process
。在这种情况下,您可以查看异常的原因,并修复原因(和继续执行)或深入挖掘以了解出错的原因。
如果 winedbg 是标准调试器,则pass
和cont
命令是让进程进一步处理异常事件的两种方法。
更精确地说:当发生故障时(分段违例、堆栈溢出……),该事件首先发送到调试器(这称为第一次异常处理机会)。调试器可以给出两个答案:
-
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 报告中提供收集到的尽可能多的信息。
-
了解崩溃的原因。通常是页面错误、调用了Wine 中未实现的函数,或类似的原因。报告崩溃时,报告整个崩溃转储,即使它对您没有意义。(在这个例子里面,在写入 0x0000000 时出现页面错误。最有可能的是 Wine 将 NULL 传递给应用程序或类似问题。) -
确定崩溃的原因。由于通常是 Wine 实现的函数执行失败或者行为不正确导致的主要/次要反应,因此使用 WINEDEBUG=+relay
环境变量重新运行 Wine。这将生成相当多的日志输出,但通常原因是位于最后一个函数调用中。这些日志通常如下所示:
-
如果你已经发现了一个行为不正常的 Wine 函数,尝试找出它行为不正常的原因。在源代码中查找函数。试着理解传递的函数参数。通常有一个 WINE_DEFAULT_DEBUG_CHANNEL(channel);
在源文件的开头。使用WINEDEBUG=+xyz,+relay
环境变量重新运行 Wine。有时,在源文件的开头以WINE_DECLARE_DEBUG_CHANNEL(channel)
的形式定义了其他调试通道;如果是这样,有问题的函数也可能使用了这些备用通道之一。在该函数中搜索TRACE_(channel)(".../n");
并将找到的这些额外的通道添加到 WINEDEBUG 环境变量里面。 -
有关如何使用 winedbg 进行调试的其他信息,请参阅源码 programs/winedbg/README
。 -
如果这些信息不够清晰,或者您想知道该函数发生的更多信息,请尝试使用 WINEDEBUG=+all
重新运行 Wine ,这将转储 Wine 里面包含调试信息在内的所有日志。通常需要限制生成的调试输出。这可以通过管道把输出日志发给grep
过滤,或者使用注册表项来完成。有关详细信息,请参阅下文的配置 +relay 行为
一节。 -
如果这还不够,可以在您认为相关的函数中手动添加更多调试日志。有关详细信息,请参阅开发人员调试日志使用指南。您也可以尝试在 gdb 中运行该程序,代替使用 Wine 调试器。如果这样做,请在 ~/.gdbinit
文件里面增加这句handle SIGSEGV nostop noprint
来禁用 gdb 对seg fault
错误的处理(Win16 需要)。 -
您还可以为该函数设置断点。用 winedbg 启动调试程序而不是 Wine。一旦调试器运行起来,在命令行提示符输入命令: break RegOpenKeyExW
(将 RegOpenKeyExW 替换成您要调试的函数,区分大小写)以设置断点。然后,使用continue
命令启动程序正常执行。程序运行到断点位置,程序将停止;如果程序还没有运行到该函数崩溃的那次调用,再次使用continue
命令继续运行程序直到达到该函数即将崩溃的那次调用。现在,您可以用单步执行命令来继续运行程序,直到达到崩溃点,然后使用其他调试器命令来查看寄存器值和相关变量值等等。
程序挂起,没有反应
用 winedbg 启动程序而不是 Wine 。当程序没有反应的时候,切换到 winedbg 窗口,并按 Ctrl+C 。这将停止程序,并允许您调试该程序,就像崩溃时候一样。
程序弹出错误消息框
有时候程序会使用或多或少的非描述性消息框报告失败。我们可以使用与崩溃相同的方法进行调试,但有一个问题,为了设置消息框,程序会多出大量的调试日志。
由于故障通常发生在设置消息框之前,您可以启动 winedbg 并在 MessageBoxA (由 Win16 和 Win32 程序调用)处设置断点,然后继续运行。程序将在设置消息框之前停止。
您也可以使用这个命令来运行程序:WINEDEBUG=+relay wine program.exe 2>&1 | less -i
然后在 less 里面搜索 MessageBox 。
反汇编程序
您也可以尝试反汇编有问题的程序,以检查没有公开的功能或使用它们。
理解汇编代码主要是一个练习问题。Win16 函数入口通常如下所示:
push bp
mov bp, sp
... 函数代码 ..
retf XXXX <--------- XXXX 是函数参数的总字节数
这是一个没有局部变量的 FAR 函数。参数通常从[bp+6]
开始,偏移量增加。请注意,对于使用 PASCAL 调用约定导出的 Win16 函数,[bp+6]
属于最右侧的参数。因此,如果我们使用带a
和b
的strcmp(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>break MessageBoxA c <---- 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)
|... <----- 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"doc",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"C:~doc9281.tmp",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"You must close Windows and load SHARE.EXE before you start Word.",0x00000000,0x1030) ret=060f:084f ds=0927
并且在 MessageBoxA 的入口停下来了:
|Stopped on breakpoint 2 at 0x40189100 (MessageBoxA [msgbox.c:190])
|190 {
代码看起来要找一个可写磁盘,并试图在该磁盘创建一个文件。要解决此 Bug,可以将 C 盘定义为网络驱动器,上述代码将忽略该驱动器。
调试技巧
以下是一些其他调试技巧:
WINEDEBUG=+relay wine program
获取程序在启动函数中调用的所有函数清单。然后执行:winedbg win
file.exe
这样,您就进入 winedbg。现在,您可以在 start 函数中调用的任何函数上设置断点,然后不断按 c 以跳过 Winfile 对此函数的正常调用,直到您最终到达此函数调用崩溃的位置。您就可以像平常一样继续调试该程序。2. 如果尝试运行程序,程序在弹出错误消息框后就退出,则问题的原因通常可以检查在 MessageBox 之前调用的一些函数的返回值发现。您应该用下面的方式重新运行程序:WINEDEBUG=+relay wine program_name &>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"
RelayInclude
和RelayExclude
类似,只不过列出的函数将是输出中仅包含的函数。
如果应用程序使用+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 函数。 -
在强制转换操作中,在指定结构或联合时,必须使用 struct
或union
关键字(即使程序使用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 继续执行,直到下一行"C"代码(进入函数内部)
next,n 继续执行,直到下一行"C"代码(不进入函数内部)
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
类日志
bt 命令列出的调用堆栈说明
一般情况下,bt 命令输出如下的信息:
Wine-dbg>bt
Backtrace:
=>0 0x7b83c640 UnhandledExceptionFilter(epointers=0x65f948) [/home/deepin/deepin-wine/dlls/kernel32/except.c:426] in kernel32 (0x0065f958)
1 0x7bc7ef39 call_exception_handler+0x28() in ntdll (0x0065f988)
2 0x7bc7ef0b EXC_CallHandler+0x1a() in ntdll (0x0065f9a8)
3 0x7bc7f851 raise_exception+0x3a0(rec=0x65fd58, context=0x65fa8c, first_chance=<is not available>) [/home/deepin/deepin-wine/dlls/ntdll/signal_i386.c:698] in ntdll (0x0065fa18)
4 0x7bc8172e NtRaiseException+0x2d(rec=<couldn't compute location>, context=<couldn't compute location>, first_chance=<couldn't compute location>) [/home/deepin/deepin-wine/dlls/ntdll/signal_i386.c:2840] in ntdll (0x0065fa38)
5 0x7bc81e3b raise_generic_exception+0x2a(rec=<couldn't compute location>, context=<couldn't compute location>) [/home/deepin/deepin-wine/dlls/ntdll/signal_i386.c:2167] in ntdll (0x0065fa78)
6 0xdeadbabe (0x0065fdd8)
7 0x0040138b in a (+0x138a) (0x0065fe68)
8 0x7b85f7ec call_process_entry+0xb() in kernel32 (0x0065fe88)
9 0x7b860769 start_process+0x68(entry=<couldn't compute location>) [/home/deepin/deepin-wine/dlls/kernel32/process.c:1124] in kernel32 (0x0065fec8)
10 0x7bc7eebc call_thread_func_wrapper+0xb() in ntdll (0x0065fedc)
11 0x7bc82069 call_thread_func+0xa8(entry=0x7b860700, arg=0x4014a0, frame=0x65ffec) [/home/deepin/deepin-wine/dlls/ntdll/signal_i386.c:2962] in ntdll (0x0065ffcc)
12 0x7bc7ee9a call_thread_entry_point+0x11() in ntdll (0x0065ffec)
其中
-
第1列的数字是函数调用栈的帧号,比如上面的输出有13层调用,当前栈帧号是0,也就是调用栈的最底层的函数。这个编号的用途是用来切换栈帧的,执行命令 frame N
N是要查看的栈帧编号就可以切换到指定的栈帧。 -
第2列是每层调用栈帧的上一层函数返回地址的16进制表示,,比如上面的输出里面编号是12的栈帧的第2列数字是 0x7bc7ee9a
就是表示在函数call_thread_entry_point
内部调用函数call_thread_func
之后的返回地址。 -
第3列是第2列函数返回地址的符号名称,目的是增加可读性,有2种情况: -
函数名称+偏移地址来表示,比如上面的栈帧12的 call_thread_entry_point+0x11
; -
如果 winedbg 就找不到地址对应的函数名称,就用所在函数所在的模块名加一个偏移地址表示, 比如上面的栈帧7的 in a (+0x138a)
表示函数调用地址是在 a 模块的首地址+0x138a字节处。
-
-
第4列是函数参数,如果没有调试符号或者函数本身没有参数,就不显示;对于显示参数的,参数值可能读取不到的会以 <couldn't compute location>
和<is not available>
来表示。 -
第5列是对应的源码,如果找不到源码,就不显示。 -
in
后面的单词名称是所在的模块,比如栈帧12所在模块是 ntdll.dll 。 -
最后一列括号括起来的数字是当前栈帧的 ESP 寄存器值,即局部变量的起始内存区域。
从上面这个堆栈来看,我们可以得到如下信息:
-
程序是在执行到 0xdeadbabe 触发了异常,异常信息保存在 raise_exception的 第1个参数 rec 结构体里面。 -
调用 0xdeadbabe 的函数返回地址是 0x0040138b
。该异常没有对应的处理函数,最终交由 UnhandledExceptionFilter 函数处理。