Before you start#
Review PE loading, the ntdll Unix boundary, Wine handles, and virtual allocation. We study Wine wine-10.0, commit b073859675060c9211fcbccfd90e4e87520dc2c2; the numerical examples are synthetic and no Wine process is run.
Learning goals#
Explain pointer-width and calling-convention boundaries, translate one actual Wine WoW64 allocation request, identify output and failure ownership, and distinguish Windows API compatibility from executing a different processor instruction set.
Source walkthrough#
- 32-bit caller
Arguments and pointers use its ABI.
- WoW64 thunk
Read and translate each argument with the correct layout.
- 64-bit implementation
Perform the corresponding operation with validated values.
- Return status
Convert output and retain ownership rules.
A 32-bit executable and a 64-bit implementation can disagree about the width and alignment of a pointer, a handle, or a structure containing either. WoW64 provides a Windows-facing compatibility environment for selected 32-bit code on a 64-bit system. A thunk is boundary code that reads one representation, constructs the other and converts outputs back. It is not enough to reinterpret a pointer to an array of 32-bit fields as a pointer to a native 64-bit structure: fields can move, widths differ and padding can appear between them. A single wrong offset can turn a size into an address or a flag.
Wine's WoW64 virtual-memory implementation, wow64_NtAllocateVirtualMemory, starts by consuming an argument vector with helper functions. This exact short excerpt is from Wine 10.0 and the file notice grants LGPL-2.1-or-later; surrounding arguments are omitted:
HANDLE process = get_handle( &args );
ULONG *addr32 = get_ptr( &args );
ULONG_PTR zero_bits = get_ulong( &args );
ULONG *size32 = get_ptr( &args );
Each helper advances the argument cursor and interprets a slot according to its declared role. The process handle uses handle conversion; the address and size arguments are pointers to 32-bit output variables; zero-bits is a scalar. Confusing the value in addr32 with the address of that variable would change the call's meaning. The following code constructs native local addr and size from those 32-bit values. It calls the native-facing allocation routine with pointers to those local variables, and performs applicable pre/post allocation notifications. The source writes the converted address and size back only if the status is zero. A failure therefore cannot be treated as a successful new range simply because the caller's old output variables still contain numbers.
The second excerpt is from the same pinned file, mem_extended_parameters_32to64, also LGPL-2.1-or-later. It omits the later conversions and allocations:
if (*count && !params32) return STATUS_INVALID_PARAMETER;
The condition tests the relationship between a nonzero element count and the input array pointer. A nonzero count with no array is rejected before the conversion loop dereferences it. A zero count does not by itself establish that every other allocation parameter is valid. The subsequent code allocates temporary conversion storage, switches on each parameter type and builds native representations. For address requirements it checks the highest ending address against the supported user-address limit before converting it. These are specific checks in this function, not a claim that arbitrary invalid application pointers are safe to dereference. The temporary data and caller output have different lifetimes; the converted array must remain valid through the native operation that uses it.
Two ABIs in one process#
PE32 and PE32+ identify different optional-header layouts in PE images; the machine field also matters. PE32 is normally used for 32-bit images and PE32+ for 64-bit images, but the header form alone does not prove an image's instruction set or that the host can execute it. Code must inspect machine type and loadable format as well as bitness. A 32-bit pointer has a four-byte representation in the illustrated ABI and a 64-bit pointer has eight bytes. Fixed-width integers do not automatically change width just because a pointer changes width. Signedness and extension rules matter: a signed value extended from 32 to 64 bits can produce a different bit pattern from an unsigned value.
Consider a synthetic structure containing a 32-bit length followed by a pointer. In one layout the fields might occupy offsets zero and four, with total size eight; a typical naturally aligned 64-bit layout places the pointer at offset eight and has total size sixteen. These are stated example ABI assumptions, not a universal promise for every compiler, packing option or structure. If a thunk passed the eight-byte object as the sixteen-byte layout, the native code would read beyond the supplied structure and would not even locate the original pointer at the expected offset. Correct translation creates a new native structure and assigns each field; any nested strings, arrays and unions require their own validation and conversion.
Calling convention describes more than field size. It controls where arguments and return values reside, stack alignment, registers preserved across a call and responsibility for cleanup. On x86, different Windows calling conventions can disagree about argument passing and stack cleanup; on x86-64, register assignments and stack requirements differ from 32-bit x86. Wine's assembly and dispatch code must preserve the context required at the transition. A C cast cannot create the correct calling convention, preserve nonvolatile registers or arrange an exception frame. Architecture-specific source and generated tables must match the executing build.
A handle is an opaque process-scoped value with object and access semantics. A 32-bit handle representation is not automatically a 32-bit host fd, and widening it is not the same as extending an arbitrary pointer. Wine's conversion helpers implement the convention used by the target API, including special values; applications should not invent their own universal conversion rule. When a native function returns a handle, a thunk must produce a value representable in the caller's interface while preserving ownership. Closing the correct handle and retaining any in-flight operation remain caller obligations. A failed native operation must not leak a newly obtained reference or publish an invalid value as a successful result.
A concrete allocation request#
Imagine a 32-bit caller has an address variable containing zero and a size variable containing 8,192. Its packed argument slots also contain the current-process handle, zero-bits zero, a commit flag and a protection value. The address variable's address is distinct from its zero contents. The thunk reads the handle and pointers using the helpers shown, converts the contents to native local variables and applies the source's conditional rule that adds reservation when the address is null and commitment was requested. It then invokes the native-facing allocator. The chosen address, returned size and status depend on the actual build, virtual address space and host; this example does not assert that an allocation occurred.
The conversion and writeback lines of that same function, in dlls/wow64/virtual.c, Wine 10.0, LGPL-2.1-or-later, are shown below. The notification and native-call lines are omitted:
void *addr = ULongToPtr( *addr32 );
SIZE_T size = *size32;
BOOL is_current = RtlIsCurrentProcess( process );
NTSTATUS status;
if (!addr && (type & MEM_COMMIT)) type |= MEM_RESERVE;
/* ... */
if (!status)
{
put_addr( addr32, addr );
put_size( size32, size );
}
ULongToPtr( *addr32 ) reads the value stored in the caller's 32-bit variable. The variable's own address is used only for the writeback. The reservation rule applies only when the address is null and commit is requested, and it does not by itself make the call succeed. Both output writes sit inside the status-zero branch, so a failed call leaves the caller's variables unchanged.
- Read arguments
Helpers advance the packed slots: process handle, 32-bit pointers and scalars.
- Widen values
Copy 32-bit contents into native locals; pointers to the outputs stay separate.
- Call native
NtAllocateVirtualMemory runs with native local pointers, with notifications before and after.
- Write back on success
put_addr and put_size run only when the native status is zero.
If the native call reports success and returns a representable address such as the synthetic value 0x10000000, the thunk writes the converted address and size back to the 32-bit variables. A subsequent caller can use that result only within the allocation and protection contract. If a preceding parameter conversion finds a nonzero count with a null parameter array, it returns STATUS_INVALID_PARAMETER; there is no successful allocation output to consume. A separate zero-size request can fail in the native allocation implementation described in the previous chapter. Those are different checks at different layers, and the status should be recorded at the layer actually reached.
Now take a hypothetical native address 0x0000000110000000. Removing its high 32 bits yields 0x10000000, a different address. Blind truncation would silently redirect the caller to an unrelated range. Wine's actual helpers and its address-space restrictions are part of preventing invalid conversion in the supported path; the arithmetic alone does not prove every possible address or output conversion is handled by a particular helper. Record the original width and representability before reasoning about a conversion. A pointer is not made safe by the numerical fact that its low bits happen to match a mapped range.
Read the text version
- Native 0x0000000110000000: Needs more than 32 bits to represent → Blind low-32-bit mask: Silently yields 0x10000000, a different address (mask without a check)
- Value 0x10000000: Fits in 32 bits → Checked conversion: Must reject or keep the full value; never truncate silently (representable value passes unchanged)
Structures, objects, and error cleanup#
In Wine's WoW64 file thunk, wow64_NtCreateFile consumes a 32-bit output handle pointer, access mask, OBJECT_ATTRIBUTES32, 32-bit I/O status block, sharing flags and other arguments. It constructs native local object attributes and an I/O status block and invokes NtCreateFile, then converts the resulting handle and status block back. This is a concrete example of a compound conversion rather than a generic byte-for-byte copy. The path, object attributes, root directory handle, sharing and access still have the semantics covered in the file chapter. The translation layer does not grant host privileges.
The output handle is initialized before the underlying call. If the operation fails, the caller must use the returned status rather than treating the output as a valid open merely because the field is writable. If it succeeds, the handle belongs to the process's Windows-facing object table, and a corresponding close is needed at the appropriate point. The native temporary structures are stack-local in this source path; this synchronous thunk keeps them alive while NtCreateFile runs, but they cannot be retained by work that outlives the call. An I/O status block and application buffers can have API-specific asynchronous lifetime requirements beyond the lifetime of this conversion frame.
System call tables connect operation numbers to the appropriate Wine handlers. Wine's WoW64 syscall source is a useful entry point for the version-specific dispatch machinery. A number and its argument layout must agree with the selected table and architecture; a number copied from another Windows build or architecture is not a stable universal ABI. Some APIs perform work in the client and some use wineserver or Unix operations. A WoW64 call does not imply that every request travels through the same backend.
Thread state, redirection, and namespaces#
A Windows thread has thread-specific environment and execution context. A mixed-bitness environment must preserve the appropriate 32-bit and 64-bit views of thread state, including register context and stack, across transitions and exceptions. The thread environment block, or TEB, is not a Linux task structure; its fields and layout belong to the Windows-facing ABI. The process environment block, or PEB, describes process-facing state. A pointer into one layout cannot safely be interpreted as an equivalent pointer in the other. Wine's actual setup and architecture support depend on its build; avoid claiming that every platform uses the same pair of native structures or transition instructions.
Windows-facing directory names are counterintuitive: System32 conventionally denotes the native system directory, while SysWOW64 commonly contains 32-bit components on a 64-bit installation. Filesystem redirection can make a 32-bit caller's view differ from the literal name it requested. The registry similarly provides selected 32-bit and 64-bit views; some keys are shared and others redirected. The exact behavior must be checked for the key, process and Wine version. A string containing System32 alone is insufficient evidence of the file actually opened. The preceding file chapter explains why Wine's drive mapping and host permissions are separate concerns.
Prefix architecture and build capabilities are setup constraints, not a switch that safely converts an existing prefix in place. WINEARCH influences prefix creation for supported Wine configurations; changing its value later does not migrate every registry value, DLL, installed program and object layout. Use a separate disposable prefix for an authorized test, never a personal default prefix. A prefix is configuration and storage, not a security sandbox: code retains the host user's authority and is subject to host policy.
Old and new WoW64; instruction sets#
Traditional mixed-bitness Wine deployments can require 32-bit Unix-side components and their dependencies alongside 64-bit ones. Wine's newer WoW64 approach places more of the 32-to-64 transition inside Wine so that selected 32-bit Windows applications can use a 64-bit Unix-side environment. The existence of the source files does not prove a particular distribution enabled that mode, shipped all required components, or supports every application. Wine 10.0 supports a range of build and architecture choices; investigate the actual package, configuration and release notes before claiming a specific mode is active. The Wine README for this commit discusses platform dependencies; it is not a tested configuration matrix.
Bitness conversion is not the same task as executing an unrelated processor instruction set. An x86 instruction cannot become an arm64 instruction merely because both processes use 32-bit pointers. Cross-architecture execution needs an appropriate processor capability, translation mechanism or emulator as well as the API and ABI implementation. ARM64EC and ARM64X describe Windows-facing mixed-architecture arrangements with their own ABI rules; FEX and Box64 are separate projects with separate versions and support boundaries. No such project is bundled into this source example or claimed as tested. A successful thunk for one API cannot establish compatibility of the loader, graphics driver, exception machinery or all binary instructions.
Practice#
Run this small private arithmetic exercise with Python 3. It allocates no memory at the example address, opens no prefix and invokes no Wine process:
python3 - <<'PY'
value = 0x0000000110000000
low = value & 0xffffffff
print('native value:', hex(value), 'low 32 bits:', hex(low))
print('round trip preserves value:', low == value)
fields32 = {'length': 0, 'pointer': 4, 'size': 8}
fields64 = {'length': 0, 'pointer': 8, 'size': 16}
print('example layouts:', fields32, fields64)
PY
Expected output shows different values and False, then the two explicitly assumed layouts. The counterexample is a value below 2^32: it can survive this numeric mask, but that does not prove it is a valid pointer or that the ABI uses the assumed alignment. If Python is unavailable, subtract 2^32 from the high address and compare the result by hand. For a source-only alternative, trace the supplied allocation inputs through the quoted argument reads and write down why the null extended-parameter array fails before the native call. Deliver the argument table, output ownership and status interpretation; no compiler or Windows program is required.
Knowledge check#
- Why is an eight-byte example 32-bit structure not safely reinterpreted as the sixteen-byte 64-bit example?
- What happens to allocation output variables when the quoted thunk's native call reports failure?
- Does a 32-bit Windows handle equal a Linux file descriptor with the same numeric value?
- Why does a valid low 32-bit address not prove that arbitrary x86 instructions can execute on arm64?
- What does
System32in a requested path fail to prove for a 32-bit caller?
Answers#
- The native pointer has a different width and alignment in the stated example. Its offset is eight rather than four, so a cast reads the wrong bytes and can exceed the original object. Translate fields and nested representations under the actual ABI.
- The source writes converted address and size back inside its success condition. A failure is not a newly allocated region; inspect the returned status and do not infer success from old output contents.
- No. A Windows-facing handle names an object through the relevant handle machinery and carries access and lifetime semantics. A host fd is a different resource that may or may not exist behind an operation.
- Address width and instruction-set execution are independent requirements. Another ISA requires appropriate hardware or translation support in addition to Wine's API and ABI handling; this chapter does not establish that support.
- It does not establish the actual file opened because applicable redirection, prefix mapping and host namespace can change the resolved path. Determine the caller bitness and actual operation under the selected configuration.
Checkpoint#
Submit the argument-width table, failure-state explanation and pointer truncation calculation. Pass when the table separates a pointer to an output variable from the output value, records the success-only writeback, and distinguishes ISA execution from thunking. If a handle is treated as a host fd, revisit the file thunk and the handle chapter and retry. This is a readiness check, not certification.
Sources#
- Wine 10.0 WoW64 memory thunks
- Wine 10.0 WoW64 file thunks
- Wine 10.0 WoW64 syscall machinery
- Wine 10.0 WoW64 process code
- Wine 10.0 Unix memory implementation
- Microsoft PE format specification
- Microsoft file system redirector documentation
- Microsoft registry redirector documentation
- Microsoft x64 calling convention