Setting Up Your Environment for x64 Windows Assembly
The first thing people get wrong is trying to write code without a debugger attached. I spent about three weeks writing assembly in my head before I realized I should have just set up WinDbg and watched what the CPU was actually doing. Install the Windows SDK, grab WinDbg from the Microsoft Store if you can't be bothered with the full download, and set your debugger to use the source server symbols. It saves you from pulling your hair out over missing symbols later. Microsoft's toolchain for 64-bit assembly uses ML64 or MASM with the /Fl flag if you want listings. The syntax is closer to Intel than AT&T, which means register names and operand order that people coming from older tutorials will find slightly less confusing. You can write your .asm files with either .code64 or rely on the /c64 switch. I prefer the explicit directive because it stops the assembler from making assumptions about your intent.
Introduction To 64 Bit Windows Assembly Programming By Ray
Raymond Chen has written extensively on Windows internals and his older articles on assembly programming are still referenced more than most current textbooks. His writing style is practical rather than academic, which matters when you are trying to understand why a particular instruction behaves differently between x86 and x64. The principles he describes around the Windows ABI are still the baseline you should work from, even though some of his examples target 32-bit code. I keep his blog open in a browser tab while I work. About forty minutes into reading through Introduction To 64 Bit Windows Assembly Programming By Ray, I found myself Googling the x64 calling convention after he mentioned it without giving the full register breakdown. The detail he provides on the Windows API layer is solid. Here is the first technical hurdle and it is not obvious from most beginner guides. In x64 Windows, the first four integer arguments go into RCX, RDX, R8, and R9. The stack has to be aligned to a sixteen-byte boundary before any call instruction executes. This sounds simple until you write a function that calls another function and your stack alignment shifts by eight bytes because you pushed a register. When I was translating a small utility from x86 to x64, I wrote a function that saved RBX, called a library routine, and then restored RBX. The library call modified the stack pointer internally. After it returned, my alignment was off by eight bytes. The next call instruction in my code landed on an unaligned stack and the function crashed with no obvious error message. The fix was straightforward but not intuitive for someone used to x86 behavior. I adjusted my prologue to reserve the shadow space first. That is the thirty-two bytes below RSP that must exist even if your function uses fewer registers. Reserve those bytes, keep your alignment, and then do your pushes.
Another detail that does not get enough attention is the red zone. On x64 macOS and Linux, the eighteen bytes below RSP are unallocated space that the operating system promises not to overwrite during signal handling. Windows does not enforce the red zone in the same way, but compiler generated code sometimes assumes its presence. When writing raw assembly, you should treat the space below RSP as potentially volatile and adjust RSP before any operation that touches memory directly below it.
Get the Full Details

Debugging Assembly in Practice
WinDbg is better than Visual Studio for this work. The disassembly window in Visual Studio works for quick checks, but it gets in the way when you need to inspect registers at exact instruction boundaries. The keyboard shortcut I use most is F11 for step into and Shift+F11 for step out. These behave consistently across breakpoints and exceptions. One thing that catches people frequently is the difference between LEA and MOV. LEA computes addresses without dereferencing memory. MOV reads from the address. When you are writing code that manipulates pointers, using LEA instead of MOV can save you a whole class of segmentation faults. I remember spending about two hours debugging a function that kept returning garbage values. The cause was a MOV where I should have used LEA. The compiler would have caught this if I had been generating intermediate C code, but in raw assembly, the assembler accepts both instructions without complaint. For structure access, the offset calculation is different from x86. A structure member at offset 0x10 is accessed with [RCX + 0x10] on x64, which looks the same syntactically, but the addressing modes and register availability change the optimization path. I stopped trying to reason about this purely from the spec and started writing small test programs. Each program exercises one addressing mode. You learn faster when you watch the actual memory operations in the debugger.
What This Approach Cannot Do For You
Raymond Chen's material focuses on understanding the platform rather than teaching you to write production code from scratch. If you are looking for a complete curriculum, you will need to supplement it with documentation on the ABI, the calling conventions, and the kernel mode interface. His articles are reference material, not a course. I found myself cross-referencing his explanations with the Microsoft Developer Network documentation about three times per session. The main limitation is that his examples are often tied to specific Windows versions. Code that works on Windows 10 might encounter different behavior on Windows 11 or older servers due to changes in the API or security model. This is not a flaw in the writing, it is a feature of how Windows evolves. The underlying assembly instructions do not change, but the system calls and exception handling paths can. For learning purposes, I would recommend pairing the reading with a small project that exercises multiple calling conventions. Write a function that calls into user32.dll. Then write a function that passes structures by reference. Then write one that handles exceptions. The exercises in my own workflow covered about three days each. The return on investment is higher than reading passively because you encounter the edge cases yourself instead of reading about them abstractly.
Practical Starting Points
Start with a blank file. Use the directive .code64 at the top. Define a main procedure with the standard prologue and epilogue pattern. Push RBP, mov RBP, RSP, allocate stack space with sub RSP, 40. Call a simple function that takes one argument. Inspect RCX in the debugger. Verify that the argument landed where it should. If it did, you have confirmed the basic calling convention. From there, add a second argument and watch RDX. Add a third and watch R8. This process usually takes me about twenty minutes and it is faster than trying to read every detail before writing a single line of code. Keep the debugger log open. WinDbg has a .traplog command that records trap events. It is useful for reviewing the sequence of events after a crash. The log shows you exactly where the execution diverged from expectations. I find this more helpful than setting breakpoints everywhere, which tends to slow down execution and change timing in ways that make some bugs disappear entirely. The combination of reading Introduction To 64 Bit Windows Assembly Programming By Ray alongside hands-on debugging produces results that neither approach does alone. The writing gives you the conceptual framework. The debugger gives you the reality check. You need both.
