Wine's PE -> UNIX Interface

2022-11-30, by ivyl

I’ve been recently debugging an issue where returning from the UNIX side resulted in a jump to 0x0. The whole PE -> UNIX interface was completely new to me so I’ve taken notes along the way. Here they are in a form of a blog post. Enjoy!

(analysis of the state as of wine 7.22)

The Syscall Thunk

If you look at dlls/ntdll/ntdll.spec, a file that defines the DLL’s exports, you can see a bunch of entries for syscalls, e.g.:

@ stdcall -syscall NtClose(long)`.

On Windows those functions are doing real syscalls.

On Wine it is a bit more complicated. It’s a Windows-style thunk created by winebuild:

0x17000d2f0 mov r10, rcx              | ulong sym.ntdll.dll_NtClose(ulong Handle)
0x17000d2f3 mov eax, 0x15             |
                                      | {
0x17000d2f8 test byte [0x7ffe0308], 1 |     if ((*0x7ffe0308 & 1) == 0) {
0x17000d300 jne 0x17000d305           |
0x17000d302 syscall                   |         return syscall(0x15);
0x17000d304 ret                       |
                                      |     }
0x17000d308 call qword [0x7ffe1000]   |     return (**0x7ffe1000)(0x15);
                                      | }

0x15 is the syscall number.

0x7ffe0308 is KUSER_SHARED_DATA’s SystemCall field.

Wine doesn’t execute the block with the syscall() instruction. It’s here to make software (e.g. Chromium) looking at the syscall thunks happy.

Instead, Wine executes 0x7ffe1000 aka __wine_syscall_dispatcher. The address is exactly the page after user_shared_data, see signal_init_process().

Userspace software should use the syscall thunks from the ntdll. Issuing syscalls directly can be problematic, as the syscall numbers can change between Windows versions.

The Dispatcher

The dispatcher itself is written in assembly and does a lot of heavy lifting:


It’s an extra system call that’s implemented in ntdll.dll. It’s meant for DLLs that have a UNIX part (aka unixlib) that is not exposed through syscalls.

It allows the .dll to call a UNIX function from an accompanying .so

It’s quite simple - once we are on the UNIX side it calls the UNIX function using the provided arguments.

The userspace side of things wraps all the arguments into a single struct.

Accessing The UNIX Part

__wine_init_unix_call() gets __wine_unix_call() (the syscall thunk) from ntdll.dll and puts it in __wine_unix_call_ptr.

It also gets the pointer to the array with the unixlib functions via NtQueryVirtualMemory() with a special memory information class MemoryWineUnixFuncs.

This can be included via include/wine/unixlib.h and called in the DLL’s DllMain(). The header also defines WINE_UNIX_CALL() for calling the functions.

Example: __wine_unix_call_funcs array, the accompanying enum (the file also contains struct definitions for passing the parameters), and a call.

The UNIX part is built into a .so and NtQueryVirtualMemory() just calls a helper function that makes sure the .so is loaded and dlsym()’s the function array from it.

The loader handles finding the dll / so pairing. There’s also a bit more going on for the DLLs that work through syscalls. I won’t dive into this here though.

Proton and “Real” Syscalls

Proton uses seccomp-bpf and installs a filter that captures any syscalls within the non-native address range (i.e. where all the Windows stuff lives). Whenever such a syscall happens it results in a sigsys. Proton has a signal handler to correctly dispatch the call. This will work with anything that directly syscalls using the numbers extracted from the thunk (the mov eax, $SYSCALL_NR).

There’s a special syscall number translation for a few games that assume certain syscall numbers.