<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stefan Hofmann</title>
    <description>The latest articles on DEV Community by Stefan Hofmann (@hofmann-dev).</description>
    <link>https://dev.to/hofmann-dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4229128%2Fcd93c3f1-cbbb-4115-8e5f-ac1a67ee5c29.png</url>
      <title>DEV Community: Stefan Hofmann</title>
      <link>https://dev.to/hofmann-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hofmann-dev"/>
    <language>en</language>
    <item>
      <title>Writing Windows Applications in x64 Assembly in 2026</title>
      <dc:creator>Stefan Hofmann</dc:creator>
      <pubDate>Fri, 06 Nov 2026 08:45:18 +0000</pubDate>
      <link>https://dev.to/hofmann-dev/writing-windows-applications-in-x64-assembly-in-2026-3n76</link>
      <guid>https://dev.to/hofmann-dev/writing-windows-applications-in-x64-assembly-in-2026-3n76</guid>
      <description>&lt;p&gt;&lt;em&gt;The compiler's tax, the practical Win32-to-Win64 migration traps — with a 55 KB tray application as the running example.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hofmann-development/clipflow" rel="noopener noreferrer"&gt;ClipFlow&lt;/a&gt;, the running example for this article, is a Windows tray application: press a hotkey and it loads a text template onto the clipboard; templates can carry &lt;code&gt;$$Name$$&lt;/code&gt; placeholders, a small dialog asks for the values, and the personalized text is ready to paste anywhere. The binary is 55 KB, icon included — one executable, no installer, no runtime, no framework, imports from four system DLLs, and its source contains no networking code.&lt;/p&gt;

&lt;p&gt;But the app is not the subject. The subject is the layer underneath, the one every C compiler handles for you. If you wrote 32-bit Windows assembly back in the MASM32 days, this article is your migration path: the x64 delta is smaller than rumored and sharper than expected. The calling convention is the famous half of it. The quiet half — instructions that vanished, addressing that changed meaning, a segment register that moved, exceptions that became metadata, and a type system where half the types widened and the other half did not — is where 32-bit habits break silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The toolchain: three commands, no C runtime
&lt;/h2&gt;

&lt;p&gt;The assembler is JWasm, the headers are the WinInc set, and the linker comes from the Windows SDK. A complete build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;jwasm&lt;/span&gt; &lt;span class="na"&gt;-nologo -I&lt;/span&gt;&lt;span class="nv"&gt;%USERPROFILE%&lt;/span&gt;\WinInc\Include &lt;span class="na"&gt;-win&lt;/span&gt;&lt;span class="m"&gt;64&lt;/span&gt; &lt;span class="na"&gt;-Zp&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt; &lt;span class="kd"&gt;clipflow&lt;/span&gt;.asm
&lt;span class="kd"&gt;rc&lt;/span&gt; &lt;span class="na"&gt;/nologo &lt;/span&gt;&lt;span class="kd"&gt;clipflow&lt;/span&gt;.rc
&lt;span class="kd"&gt;link&lt;/span&gt; &lt;span class="na"&gt;/nologo /subsystem&lt;/span&gt;&lt;span class="nl"&gt;:windows&lt;/span&gt; &lt;span class="na"&gt;/entry&lt;/span&gt;&lt;span class="nl"&gt;:start&lt;/span&gt; &lt;span class="kd"&gt;clipflow&lt;/span&gt;.obj &lt;span class="kd"&gt;clipflow&lt;/span&gt;.res
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-Zp8&lt;/code&gt; matters more than it looks: the SDK structures on x64 assume natural alignment capped at 8 bytes, and a mismatch silently changes every offset in every struct you touch.&lt;/p&gt;

&lt;p&gt;There is no C runtime in the binary — the entry point is genuinely yours. For clarity, here is an explicitly stack-managed entry-point sketch (the real build should follow one consistent assembler stack-management strategy):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start:
    and rsp, -16                  ; align for calls; entry is not a normal proc
    sub rsp, 20h                  ; reserve 32 bytes of outgoing shadow space
    call RunApplication
    mov ecx, eax                  ; ExitProcess takes a UINT in ECX
    call ExitProcess              ; does not return
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here the linker explicitly selects &lt;code&gt;start&lt;/code&gt; with &lt;code&gt;/entry:start&lt;/code&gt;. The manual &lt;code&gt;sub rsp, 20h&lt;/code&gt; is essential: alignment alone does not provide the callee's shadow space. In normal framed procedures, let your chosen JWasm &lt;code&gt;invoke&lt;/code&gt; strategy manage outgoing arguments rather than mixing it with ad hoc adjustments.&lt;/p&gt;

&lt;p&gt;No CRT startup objects, no hidden constructors, no &lt;code&gt;atexit&lt;/code&gt; machinery. The first instruction that runs is one you wrote.&lt;/p&gt;

&lt;p&gt;The following options illustrate the relevant assembler settings (choose &lt;strong&gt;one&lt;/strong&gt; &lt;code&gt;win64&lt;/code&gt; mode, not both):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;option casemap:none&lt;/code&gt; — case-sensitive identifiers, as the Windows headers require.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;option scoped&lt;/code&gt; — procedure-local labels stay local.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;option frame:auto&lt;/code&gt; — automatically generates prologues/epilogues and unwind metadata for &lt;code&gt;proc frame&lt;/code&gt; procedures; the &lt;code&gt;frame&lt;/code&gt; designation still matters.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;option win64:1&lt;/code&gt; — enables automatic register-parameter homing, but uses the default per-&lt;code&gt;invoke&lt;/code&gt; stack adjustment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. The Win64 ABI survival kit
&lt;/h2&gt;

&lt;p&gt;In C, the compiler pays a tax on your behalf — on every call, in every function. In assembly you are the compiler, and the tax collector still comes. These are the rules that actually bite.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 One default calling convention instead of a zoo
&lt;/h3&gt;

&lt;p&gt;The first four integer or pointer arguments travel in RCX, RDX, R8, R9 (floats use XMM0–XMM3). Everything from the fifth parameter onward goes on the stack, one argument per 8-byte slot — a DWORD argument still occupies a full slot. And the callee never cleans up: every procedure ends with a plain &lt;code&gt;ret&lt;/code&gt;, never &lt;code&gt;ret 8&lt;/code&gt;. In 32-bit Windows you had stdcall, cdecl, fastcall and thiscall coexisting; ordinary Windows x64 API calls use a unified default convention (specialized conventions such as &lt;code&gt;__vectorcall&lt;/code&gt; also exist). The &lt;code&gt;ret n&lt;/code&gt; habit from stdcall is the one that bites first — nothing crashes immediately; the caller's stack is just quietly wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 Shadow space: 32 bytes you owe every callee
&lt;/h3&gt;

&lt;p&gt;Before every call, the caller reserves 32 bytes of stack directly above the return address. The callee is entitled to spill RCX, RDX, R8 and R9 there — its "home slots". From the callee's perspective at entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[rsp+0x28]   5th parameter (first stack argument)
[rsp+0x20]   home slot for R9
[rsp+0x18]   home slot for R8
[rsp+0x10]   home slot for RDX
[rsp+0x08]   home slot for RCX
[rsp+0x00]   return address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip the shadow space and the crash does not happen in your code — it happens inside the callee, when it writes its parameters home. A crash far from the bug is the signature of this mistake.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 16-byte alignment, or the movaps crash
&lt;/h3&gt;

&lt;p&gt;RSP must be 16-byte aligned immediately before the &lt;code&gt;call&lt;/code&gt;; the callee therefore enters with RSP 8 mod 16, and the prologue's pushes restore alignment as they go. The reason is mundane and brutal: SSE. &lt;code&gt;movaps&lt;/code&gt; and friends fault on unaligned operands, and plenty of system code uses them. Get this wrong and your process dies inside a DLL you did not write, with a stack trace that is a lie. (It is also why the entry point above aligns RSP before reserving outgoing shadow space.)&lt;/p&gt;

&lt;h3&gt;
  
  
  2.4 Homing: parameters that survive a call
&lt;/h3&gt;

&lt;p&gt;RCX, RDX, R8 and R9 are volatile. A procedure that needs its parameters after making calls of its own must put them in memory. With the appropriate JWasm Win64 homing option and a framed procedure, the generated prologue can home them into the incoming shadow space (schematically):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    push rbp
    mov rbp, rsp
    push rsi
    push rdi
    sub rsp, N                    ; locals + outgoing shadow space, 16-aligned
    mov [rbp+0x10], rcx           ; parameter 1 → its home slot
    mov [rbp+0x18], rdx
    mov [rbp+0x20], r8
    mov [rbp+0x28], r9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parameters are stable memory references from then on — and the fifth parameter, if there is one, sits at &lt;code&gt;[rbp+0x30]&lt;/code&gt;. This is why a window procedure can still read its LPARAM deep inside a message handler.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.5 Unwind information is not optional
&lt;/h3&gt;

&lt;p&gt;x64 structured exception handling is table-based. Every non-leaf function should carry unwind data (&lt;code&gt;.pdata&lt;/code&gt;/&lt;code&gt;.xdata&lt;/code&gt;) so that debuggers walk honest stacks and exceptions can pass through your frames. &lt;code&gt;option frame:auto&lt;/code&gt; together with &lt;code&gt;proc frame&lt;/code&gt; can generate it; a true leaf function that needs no unwind metadata is exempt. Without it, everything runs fine — until the day it doesn't, and the crash dump blames the wrong function.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.6 The fine print
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Volatile:&lt;/strong&gt; RAX, RCX, RDX, R8–R11, XMM0–XMM5. Nonvolatile general-purpose registers (RBX, RBP, RSI, RDI, R12–R15) and XMM6–XMM15 must be preserved if modified. In the right framed-procedure mode, JWasm &lt;code&gt;USES&lt;/code&gt; can help save and restore these; it is not a blanket guarantee for every register or context.&lt;/p&gt;

&lt;h4&gt;
  
  
  Variadic calls
&lt;/h4&gt;

&lt;p&gt;Functions such as &lt;code&gt;printf(format, ...)&lt;/code&gt; accept a variable number of arguments. Windows x64 uses the normal calling convention: the first four arguments occupy RCX, RDX, R8 and R9 for integer or pointer values, or XMM0–XMM3 for floating-point values. Arguments beyond the fourth go on the stack in 8-byte slots.&lt;/p&gt;

&lt;p&gt;The important difference is how floating-point arguments are passed. For variadic calls, a floating-point value in one of the first four positions must appear in both its XMM register and the corresponding general-purpose register, using the same bit representation.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;printf("%f %d", 1.5, 7)&lt;/code&gt; passes the format pointer in RCX, the double &lt;code&gt;1.5&lt;/code&gt; in both XMM1 and RDX, and the integer &lt;code&gt;7&lt;/code&gt; in R8. The register numbers follow argument positions, not separate integer and floating-point sequences.&lt;/p&gt;

&lt;p&gt;The caller reserves the usual 32-byte shadow space. The callee can save register arguments into their home slots, allowing a &lt;code&gt;va_list&lt;/code&gt; to traverse them together with stack arguments. Standard C default argument promotions still apply: &lt;code&gt;float&lt;/code&gt; becomes &lt;code&gt;double&lt;/code&gt;, and narrow integer types are promoted to &lt;code&gt;int&lt;/code&gt; or &lt;code&gt;unsigned int&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Width discipline
&lt;/h4&gt;

&lt;p&gt;32-bit register writes zero the upper half of the 64-bit register; 8- and 16-bit writes do not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    mov eax, byteCount            ; upper half of RAX is now guaranteed zero
    sete al                       ; AL = 0/1 — the rest of EAX is stale
    movzx eax, al                 ; so widen explicitly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the upper halves of return values are only defined where the type is 64 bits wide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    invoke GetMessageW, addr msg, 0, 0, 0
    .if eax == -1                 ; BOOL: 32-bit — test EAX, never RAX
        ...

    invoke DialogBoxParamW, ...
    .if rax == -1                 ; INT_PTR: 64-bit
        ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testing the wrong half of RAX is a bug that assembles, runs, and simply never takes its error branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. From 32-bit to 64-bit: the migration delta
&lt;/h2&gt;

&lt;p&gt;The calling convention above is the famous half of the migration. The quiet half is below — the things that don't announce themselves, because half of them still assemble and then mean something different.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Instructions that vanished — and what replaces them
&lt;/h3&gt;

&lt;p&gt;Long mode removed more than most people notice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    pushad                        ; gone entirely — save registers individually
    push ecx                      ; gone — push always moves 8 bytes now
    push 80000000h                ; still assembles — but sign-extends!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one is the quiet trap: &lt;code&gt;push&lt;/code&gt; with a 32-bit immediate pushes the value &lt;strong&gt;sign-extended to 64 bits&lt;/strong&gt;. &lt;code&gt;push 80000000h&lt;/code&gt; puts &lt;code&gt;0xFFFFFFFF80000000&lt;/code&gt; on the stack, not &lt;code&gt;0x0000000080000000&lt;/code&gt;. If you meant the unsigned value, load through a 32-bit register first — &lt;code&gt;mov eax, 80000000h&lt;/code&gt; zero-extends RAX — then &lt;code&gt;push rax&lt;/code&gt;. For signed widening from a memory DWORD there is &lt;code&gt;movsxd&lt;/code&gt;; for unsigned, &lt;code&gt;mov&lt;/code&gt; into the 32-bit register and rely on the zero-extension.&lt;/p&gt;

&lt;p&gt;Two more removals worth knowing: the single-byte INC/DEC encodings (0x40–0x4F) were reassigned as REX prefixes, so INC/DEC still exist but with different encodings — invisible unless you hand-assembled opcode tables. And there is no &lt;code&gt;push imm64&lt;/code&gt; at all: the only instruction that holds a full 64-bit constant is &lt;code&gt;mov reg, imm64&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mov&lt;/code&gt; has a related encoding trap. &lt;code&gt;mov r64, imm64&lt;/code&gt; can load a full 64-bit constant into a register, but there is no &lt;code&gt;mov m64, imm64&lt;/code&gt;: a direct immediate store to a 64-bit memory operand uses a &lt;strong&gt;sign-extended 32-bit immediate&lt;/strong&gt;. For example, &lt;code&gt;mov qword ptr [rbx], -1&lt;/code&gt; writes &lt;code&gt;0xFFFFFFFFFFFFFFFF&lt;/code&gt;, which is intentional. For an unsigned value such as &lt;code&gt;0x0000000080000000&lt;/code&gt;, make the width explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    mov eax, 80000000h            ; zero-extends into RAX
    mov qword ptr [rbx], rax
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids relying on an assembler's handling of an out-of-range or ambiguous immediate.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Addressing went RIP-relative
&lt;/h3&gt;

&lt;p&gt;In 32-bit code, &lt;code&gt;mov eax, offset MyString&lt;/code&gt; puts an absolute address in a register. In 64-bit code, memory references with a plain displacement assemble to &lt;strong&gt;RIP+disp32&lt;/strong&gt; instead — relative to the instruction pointer. &lt;code&gt;lea rax, [MyString]&lt;/code&gt; and &lt;code&gt;mov ecx, [counter]&lt;/code&gt; are position-independent by construction.&lt;/p&gt;

&lt;p&gt;The consequences are pleasant: almost no &lt;code&gt;.reloc&lt;/code&gt; fixups for your code, ASLR for free, and your source syntax barely changes. The rule to internalize: ordinary static data references are generally RIP-relative. Restricted absolute forms still exist (including certain &lt;code&gt;mov&lt;/code&gt; accumulator forms); a general 64-bit address can be loaded with &lt;code&gt;mov rax, imm64&lt;/code&gt; and then dereferenced.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 The TEB moved from FS to GS
&lt;/h3&gt;

&lt;p&gt;If your 32-bit code read the Thread Environment Block — the self-pointer, stack limits, TLS array, last error — it did so through FS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    mov eax, fs:[18h]             ; 32-bit: linear address of the TEB
    mov eax, fs:[30h]             ; 32-bit: the PEB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On x64, all of that lives under GS instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    mov rax, gs:[30h]             ; 64-bit: TEB self-pointer
    mov rax, gs:[60h]             ; 64-bit: the PEB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap: FS-relative addressing &lt;strong&gt;still assembles&lt;/strong&gt; in 64-bit mode. It just no longer points at anything useful in a 64-bit process. Code that reads &lt;code&gt;fs:[18h]&lt;/code&gt; compiles, runs, and returns garbage — another member of the "looks fine, means something else" family. In practice you will rarely need the TEB directly; the API covers almost all of it. But if you port code that used it, this is the line to fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 Exceptions: runtime chains became static tables
&lt;/h3&gt;

&lt;p&gt;The classic 32-bit SEH prolog — installing a handler at run time by pushing it and re-pointing the FS chain — simply does not exist on x64:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ; the MASM32-era pattern — impossible on x64
    push handler
    push dword ptr fs:[0]
    mov dword ptr fs:[0], esp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 64-bit dispatcher does not use the old FS-linked handler chain. It looks up function-table entries in &lt;code&gt;.pdata&lt;/code&gt; for the function containing the faulting RIP and reads unwind codes from &lt;code&gt;.xdata&lt;/code&gt; that describe the prolog: which registers were pushed, where the frame is, how far to step. You don't install a handler — you &lt;em&gt;declare&lt;/em&gt; your prolog to the assembler, before execution ever starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MyProc proc frame
    push rbp
    .pushreg rbp
    mov rbp, rsp
    .setframe rbp, 0
    sub rsp, 28h
    .allocstack 28h
    .endprolog                    ; after this, the prolog must not change
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JWasm's &lt;code&gt;option frame:auto&lt;/code&gt; emits all of this for you — the directives above are what it produces. The mental shift: in 32-bit, SEH was code; in 64-bit, SEH is metadata. A prolog that lies about itself breaks unwinding even when the code "works".&lt;/p&gt;

&lt;h3&gt;
  
  
  3.5 Types: what widened, and what did not
&lt;/h3&gt;

&lt;p&gt;The dangerous intuition is "everything is 64-bit now". It is not. The type table has two halves, and confusing them is the classic porting bug:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;32-bit&lt;/th&gt;
&lt;th&gt;64-bit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LONG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32 bit&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32 bit — unchanged&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DWORD&lt;/code&gt;, &lt;code&gt;BOOL&lt;/code&gt;, &lt;code&gt;HRESULT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;32 bit&lt;/td&gt;
&lt;td&gt;32 bit — unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;size_t&lt;/code&gt;, &lt;code&gt;ULONG_PTR&lt;/code&gt;, &lt;code&gt;LONG_PTR&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;32 bit&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;64 bit&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;WPARAM&lt;/code&gt;, &lt;code&gt;LPARAM&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;32 bit&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;64 bit&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pointers&lt;/td&gt;
&lt;td&gt;32 bit&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;64 bit&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;HWND&lt;/code&gt;, &lt;code&gt;HANDLE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pointer-sized (32-bit)&lt;/td&gt;
&lt;td&gt;pointer-sized (64-bit); Windows handles are typically 32-bit-significant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three practical consequences:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message parameters.&lt;/strong&gt; WPARAM and LPARAM are 64-bit now. Reading only the low 32 bits is legitimate when you &lt;em&gt;know&lt;/em&gt; the payload is small — ClipFlow's tray handler reads &lt;code&gt;dword ptr parameter2&lt;/code&gt; because a tray message's mouse code lives in the low word — but a pointer passed through LPARAM (a &lt;code&gt;CREATESTRUCT*&lt;/code&gt;, a dialog's user data) must be read at full width. Know which messages carry pointers, and never let a truncation habit survive those cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SetWindowLong&lt;/code&gt; became &lt;code&gt;SetWindowLongPtr&lt;/code&gt;.&lt;/strong&gt; The canonical pattern — storing a pointer in &lt;code&gt;GWLP_USERDATA&lt;/code&gt; — needs the &lt;code&gt;Ptr&lt;/code&gt; variant on x64. ClipFlow's dialog procedure uses it to carry its &lt;code&gt;TemplateField*&lt;/code&gt; from &lt;code&gt;WM_INITDIALOG&lt;/code&gt; to the &lt;code&gt;IDOK&lt;/code&gt; handler. On 32-bit Windows the plain function took a LONG and macros papered over the difference; on x64, pushing a pointer through the wrong API is &lt;em&gt;the&lt;/em&gt; porting bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structures changed shape.&lt;/strong&gt; Fields holding pointers or &lt;code&gt;LONG_PTR&lt;/code&gt; doubled in size, padding moved, and every &lt;code&gt;sizeof&lt;/code&gt; you memorized in 32-bit code is wrong. &lt;code&gt;GetTickCount&lt;/code&gt; needed a 64-bit sibling (&lt;code&gt;GetTickCount64&lt;/code&gt;); window handles remain handle types rather than raw application pointers. Windows currently supports 32-bit-significant handles for 32/64-bit interoperability, but store them in &lt;code&gt;HANDLE&lt;/code&gt;/&lt;code&gt;HWND&lt;/code&gt;-sized fields rather than relying on truncation. When in doubt, let the assembler compute offsets: &lt;code&gt;[rbp].SomeStruct.SomeField&lt;/code&gt; and the header files exist precisely so you don't memorize layout.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.6 The cheat sheet
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;32-bit (Win32)&lt;/th&gt;
&lt;th&gt;64-bit (Win64)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Calling conventions&lt;/td&gt;
&lt;td&gt;stdcall, cdecl, fastcall, thiscall&lt;/td&gt;
&lt;td&gt;unified default convention; specialized alternatives exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argument registers&lt;/td&gt;
&lt;td&gt;ECX, EDX (fastcall only)&lt;/td&gt;
&lt;td&gt;RCX, RDX, R8, R9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5th+ arguments&lt;/td&gt;
&lt;td&gt;stack, 4-byte slots&lt;/td&gt;
&lt;td&gt;stack, 8-byte slots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stack cleanup&lt;/td&gt;
&lt;td&gt;convention-dependent (&lt;code&gt;stdcall&lt;/code&gt;: callee; &lt;code&gt;cdecl&lt;/code&gt;: caller)&lt;/td&gt;
&lt;td&gt;caller (plain &lt;code&gt;ret&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shadow space&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;32 bytes, mandatory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stack alignment&lt;/td&gt;
&lt;td&gt;less stringent ABI requirements&lt;/td&gt;
&lt;td&gt;16-byte before the call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parameter saving&lt;/td&gt;
&lt;td&gt;convention-dependent&lt;/td&gt;
&lt;td&gt;homing into shadow space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEH&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fs:[0]&lt;/code&gt; runtime chain&lt;/td&gt;
&lt;td&gt;static &lt;code&gt;.pdata&lt;/code&gt;/&lt;code&gt;.xdata&lt;/code&gt; tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TEB / PEB&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fs:[18h]&lt;/code&gt; / &lt;code&gt;fs:[30h]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gs:[30h]&lt;/code&gt; / &lt;code&gt;gs:[60h]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pushad&lt;/code&gt; / &lt;code&gt;push r32&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;available&lt;/td&gt;
&lt;td&gt;removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;push imm32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;as-is&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;sign-extended&lt;/strong&gt; to 64 bits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mov reg, imm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;up to 32-bit immediate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mov r64, imm64&lt;/code&gt; supports full 64-bit immediates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mov mem, imm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;up to 32-bit immediate&lt;/td&gt;
&lt;td&gt;64-bit memory destination: sign-extended &lt;code&gt;imm32&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data addressing&lt;/td&gt;
&lt;td&gt;often absolute&lt;/td&gt;
&lt;td&gt;generally RIP-relative for static memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EBP&lt;/td&gt;
&lt;td&gt;frame pointer by convention&lt;/td&gt;
&lt;td&gt;optional, general register&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LONG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32 bit&lt;/td&gt;
&lt;td&gt;32 bit — unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;WPARAM&lt;/code&gt;, &lt;code&gt;LPARAM&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;32 bit&lt;/td&gt;
&lt;td&gt;64 bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handles&lt;/td&gt;
&lt;td&gt;32-bit value&lt;/td&gt;
&lt;td&gt;32-bit significant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User address space&lt;/td&gt;
&lt;td&gt;typically 2 GB per process (configuration-dependent)&lt;/td&gt;
&lt;td&gt;much larger; OS- and configuration-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  4. What you actually get
&lt;/h2&gt;

&lt;p&gt;The honest answer to "why not C?" is: you could, and it would be fine.&lt;/p&gt;

&lt;p&gt;Assembly does not automatically make a tray application faster. Most of its lifetime is spent waiting for user input. The benefits lie elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimal dependencies.&lt;/strong&gt; No C runtime, no application framework, and a small set of Windows API imports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal startup overhead.&lt;/strong&gt; No framework initialization; the hotkey becomes available as soon as the application is ready.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small audit surface.&lt;/strong&gt; One main assembly source file and a resource script make it practical to examine what the application actually does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ClipFlow is one example: a 55 KB Windows tray application, written entirely in x64 assembly.&lt;/p&gt;

&lt;p&gt;For small, focused Windows utilities, x64 assembly remains a viable choice in 2026. The transition from 32-bit assembly is not a complete reinvention — but the details matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source code and builds:&lt;/strong&gt; &lt;a href="https://github.com/hofmann-development/clipflow" rel="noopener noreferrer"&gt;github.com/hofmann-development/clipflow&lt;/a&gt;&lt;/p&gt;

</description>
      <category>assembly</category>
      <category>programming</category>
      <category>systems</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
