Debugging Reason 3

From WineHQ Wiki
Jump to: navigation, search

As it starts, the program shows an "Unknown exception" error dialog. Let's find out why.

The first stop when greeted with a problem that isn't obviously related to any one component like this is a relay trace. This means dumping every call into and out of Wines built in DLLs (but not any native DLLs).

Exceptions in Windows programs have kernel level support, unlike in Linux. When a program crashes, it's the same as when a program throws its own exception to indicate some internal problem. To tell them apart we need to get some info on the exception, which we can do using the +seh debug channel. So we need a +relay,+seh trace. Finally it's often useful to enable +tid, which lets you tell separate threads apart. Not all programs are multi-threaded but when you start, you don't know if it is or isn't.

So we grab a +relay,+seh,+tid trace. It's huge. Where do we start?

Well, load it up in your favourite text editor or less. Now look for the first "trace:seh" line, which indicates the source of the exception. It may be that there is no exception thrown and the program simply generates the message directly, so if you get nowhere with that look for a !MessageBox call. Here it is:

000b:Call shell32.SHGetMalloc(778fe0bc) ret=7721a8f8
000b:Ret  shell32.SHGetMalloc() retval=00000000 ret=7721a8f8
000b:Call shell32.SHGetSpecialFolderLocation(00000000,00000000,778fe0b8) ret=7721a94b
000b:Ret  shell32.SHGetSpecialFolderLocation() retval=00000000 ret=7721a94b
000b:Call shell32.SHGetPathFromIDListW(77c6b4b8,778fe108) ret=7721a963
000b:Ret  shell32.SHGetPathFromIDListW() retval=00000001 ret=7721a963
trace:seh:EXC_RtlRaiseException code=e06d7363 flags=1 addr=0x77b6b9c0
  ...

Here code e06d7363 is a MSVC++ exception. So it hasn't crashed, instead the program has thrown an internal exception using "throw foo::bar" syntax.

The return values of most functions normally have to be looked up, as there isn't any consistency as to whether zero or non-zero means success. In this case all three calls succeed. So what is going on here? These calls are to the "shell" or Explorer subsystem, and it seems to be wanting the path of a special folder. In Explorer parlance, a special folder is something like the desktop, the Recycle Bin or Network Neighborhood. As the calls succeed, maybe we're giving Reason the wrong answer?

To gain more information, now we've located the problematic subsystem, we use a +shell,+pidl,+seh trace. These names came from the top of the files where SHGetPathFromIDListW and friends are defined. If you don't know what C file a function is in, run {{{ make tags }}} in the root of the Wine source tree then use the tags facilities in emacs or vim to find the right file.

Here's the result of that trace:

trace:shell:SHAlloc 20 bytes at 0x77c5a568
trace:shell:SIC_IconAppend L"c:\\windows\\system\\shell32.dll" 38 0x112e 0x1136
trace:shell:SHAlloc 20 bytes at 0x77c5ad58
trace:shell:SIC_Initialize hIconSmall=0x77c59de0 hIconBig=0x77c5a6c0
trace:shell:SHGetFolderPathW (nil),0x778fda9c,nFolder=0x0025
trace:shell:PathFileExistsW (L"c:\\windows\\system")
trace:shell:SHGetFolderPathW returning 0x00000000 (final path is L"c:\\windows\\system")
fixme:win:WIN_CreateWindowEx Parent is HWND_MESSAGE
fixme:win:WIN_CreateWindowEx Parent is HWND_MESSAGE
trace:shell:SHGetMalloc (0x778fe0bc)
trace:shell:SHGetSpecialFolderLocation ((nil),0x0,0x778fe0b8)
trace:shell:SHGetFolderLocation (nil) 0x00000000 (nil) 0x00000000 0x778fe0b8
trace:pidl:_ILCreateDesktop ()
trace:shell:SHAlloc 2 bytes at 0x77c6b208
trace:shell:SHGetFolderLocation -- (new pidl 0x77c6b208)
trace:shell:SHGetPathFromIDListW (pidl=0x77c6b208,0x77efedea)
-------- pidl=0x77c6b208
empty pidl (Desktop)
trace:pidl:_ILIsValue (0x77c6b208)
trace:pidl:_ILIsFolder (0x77c6b208)
trace:pidl:_ILGetGUIDPointer 0x77c6b208
trace:pidl:_ILIsMyComputer (0x77c6b208)
trace:shell:SHELL_GetPathFromIDListW -- L"", 0x00000000
trace:shell:SHGetPathFromIDListW -- L"", 0x00000000
trace:seh:EXC_RtlRaiseException code=e06d7363 flags=1 addr=0x77b6b9c0

From this we see that the call to SH!GetSpecialFolderLocation is being used to get the desktop PIDL. A short word on PIDLs. Windows has several namespaces for files. The most common is the one you're familiar with, where paths look like C:\Program Files\Foobar 2000. There's another namespace, implemented at the Explorer/Shell level. This is why in Explorer the system is rooted at the Desktop. If you want to access special folders like the control panel, dialup connections or the Recycle Bin you need to use the shell API. Paths in the shell aren't human-readable, instead they're a binary string called an "ID list". A PIDL is a "pointer to an ID list", in other words, the equivalent of a string pointer.

Once it's retrieved the desktop PIDL (which is in fact special: it's empty) Reason calls SHGetPathFromIDListW using it. From MSDN we can see that this function converts an Explorer/shell path into a real Windows file path. The line above the last SEH line indicates that we're returning an empty string here - this seems wrong, the desktop is normally stored in c:\Windows\Desktop! Maybe we've found the bug.

Looking at the source of SHGetPathFromIDListW shows that it calls several other functions before finally arriving at SHELL_GetPathFromIDListW, which contains this code at the top:

    /* One case is a PIDL rooted at desktop level */
    if (_ILIsValue(pidl) || _ILIsFolder(pidl))
    {
        hr = SHGetSpecialFolderPathW(0, pszPath, CSIDL_DESKTOP, FALSE);

As we can see it's got a special-case for the desktop PIDL. For some reason this branch isn't being taken when it should be. Looking at the _ILIs* functions shows that they're Wine internal, and that there's also a _IL!IsDesktop function. We've found our bug: changing the if statement to include a check against _IL!IsDesktop fixes the problem (and lets us move onto the next bug!).