Wine's PE -> UNIX Interface
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 stores context in
amd64_thread_data()->syscall_frame
(amd64_thread_data()
isTEB
->GdiTebBatch
). -
It switches the thread’s stack from the “user space” one (Windows) to the “kernel” one (UNIX-side, allocated in
init_thread_stack()
). -
It figures out what to call on the UNIX side using the syscall number and System Service Descriptor Table, see
syscalls[]
andntdll_init_syscalls()
. -
It calls the UNIX function it has found and takes the result.
-
It restores the context / Windows stack.
-
It jumps back to the syscall thunk.
__wine_unix_call()
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.