Chapter 1. x86 and x64
x86 is a little-endian architecture derived from the Intel 8086. Here it denotes the 32-bit implementation of the Intel architecture (IA-32) defined in the Intel Software Developer's Manual. The processor operates in two principal modes. Real mode is the power-on state and supports only a 16-bit instruction set. Protected mode supports virtual memory, paging, and the other features on which modern operating systems depend, and is the mode discussed throughout this chapter. The 64-bit extension is called x64 or x86-64.
x86 enforces privilege separation through ring levels, numbered 0 to 3. Ring 0 is the most privileged and can modify all system settings; ring 3 is the least privileged. Modern operating systems use only two: user-mode code runs in ring 3, the kernel in ring 0. The current ring level is encoded in the CS register and is referred to as the current privilege level (CPL).
Register Set and Data Types
In protected mode there are eight 32-bit general-purpose registers (GPRs): EAX, EBX, ECX, EDX, EDI, ESI, EBP, and ESP. Several can be accessed at narrower widths through aliased names. The instruction pointer is EIP. Status of arithmetic operations and execution state is held in the 32-bit EFLAGS register.
| REGISTER | PURPOSE |
|---|---|
ECX |
Counter in loops |
ESI |
Source in string/memory operations |
EDI |
Destination in string/memory operations |
EBP |
Base frame pointer |
ESP |
Stack pointer |
Sub-register aliasing follows the legacy 8086 layout. EAX (32-bit) contains AX (low 16 bits), which splits into AH (high byte) and AL (low byte). The same pattern applies to EBX, ECX, EDX. ESI, EDI, EBP, ESP expose only 16-bit aliases (SI, DI, BP, SP).
| DATA TYPE | WIDTH | EXAMPLES |
|---|---|---|
| Byte | 8-bit | AL, BL, CL |
| Word | 16-bit | AX, BX, CX |
| Doubleword | 32-bit | EAX, EBX, ECX |
| Quadword | 64-bit | Register pair, usually EDX:EAX |
x86 has no 64-bit GPRs, but pairs two 32-bit registers (typically EDX:EAX) to hold quadword values in specific instructions. RDTSC, for example, writes a 64-bit result to EDX:EAX.
EFLAGS and conditional branching
Flags in EFLAGS record the outcome of arithmetic and are the basis for conditional branching. If an add yields zero, ZF is set to 1. See the Control Flow section for the full flag set.
Beyond the GPRs, EIP, and EFLAGS, control registers govern low-level mechanisms: CR0 toggles paging, CR2 holds the linear address that caused a page fault, CR3 is the base of the paging structure, and CR4 controls hardware virtualization settings. DR0–DR7 are the debug registers.
Debug registers
Although there are seven usable debug registers, only four (DR0–DR3) set memory breakpoints. The remainder hold status.
Model-specific registers (MSRs) vary between processors and vendors. Each is identified by a name and a 32-bit number and is accessed with RDMSR/WRMSR from ring 0 only. SYSENTER, for instance, transfers execution to the address in the IA32_SYSENTER_EIP MSR (0x176), normally the OS system call handler.
Instruction Set
x86 permits flexible data movement between registers and memory, classifiable into five methods:
| METHOD | SUPPORT |
|---|---|
| Immediate to register | All architectures |
| Register to register | All architectures |
| Immediate to memory | All architectures |
| Register to/from memory | All architectures |
| Memory to memory | x86-specific |
A classical RISC architecture such as ARM is load/store: memory is touched only by LDR and STR, so incrementing a value in memory takes three instructions (load, add, store). x86 can operate on memory directly, so the same operation is a single INC or ADD; MOVS even reads and writes memory in one instruction.
x86 uses variable-length instructions, from 1 to 15 bytes. ARM instructions are fixed at 2 or 4 bytes.
Syntax
Two notations exist for x86 assembly: Intel and AT&T. They encode identical instructions.
| DIFFERENCE | INTEL | AT&T |
|---|---|---|
| Registers/immediates | plain | % before registers, $ before immediates |
| Operation width | implicit | mnemonic suffix (MOVL, MOVB) |
| Operand order | dst, src |
src, dst |
Intel AT&T
mov ecx, AABBCCDDh movl $0xAABBCCDD, %ecx
mov ecx, [eax] movl (%eax), %ecx
mov ecx, eax movl %eax, %ecx
Windows tools (IDA Pro, OllyDbg, MASM) use Intel notation; UNIX tools (GCC) frequently use AT&T. Intel notation is used throughout the book.
Data Movement
MOV is the most common instruction. Its simplest forms move an immediate or register into a register. Memory access is written with square brackets.
01: C7 00 01 00 00 00 mov dword ptr [eax], 1 ; *eax = 1
02: 8B 08 mov ecx, [eax] ; ecx = *eax
03: 89 18 mov [eax], ebx ; *eax = ebx
04: 89 46 34 mov [esi+34h], eax ; *(esi+34) = eax
05: 8B 46 34 mov eax, [esi+34h] ; eax = *(esi+34)
06: 8B 14 01 mov edx, [ecx+eax] ; edx = *(ecx+eax)
LEA is not a memory access
LEA uses bracket syntax but does not reference memory. It evaluates the address expression and stores the result in the destination register. If EBP = 0x1000, then lea edi, [ebp-0C0h] sets EDI = 0xF40.
The base-plus-offset form [base + offset] accesses structure members and buffers computed at runtime. Writing a wider value than a field's declared size intentionally initializes adjacent fields. Given a KDPC whose first three fields are Type (1 byte), Importance (1 byte), and Number (2 bytes), mov dword ptr [ecx], 113h sets all three at once: Type = 0x13, Importance = 0x1, Number = 0x0. The compiler folds three stores into one because the constants are known at compile time; the doubleword form is 6 bytes versus 13 for the byte/word/word sequence.
Memory access granularity is byte, word, or doubleword. The default is 4 bytes, overridden to 1 or 2 with a prefix (e.g., C6 for byte, 66 for word).
The array form is [base + index * scale], where scale matches element size:
01: 8B 34 B5 40 05 ... mov esi, [_KdLogBuffer + esi*4] ; 4-byte elements
02: 89 04 F7 mov [edi+esi*8], eax ; 8-byte elements
String Instructions
MOVSB/MOVSW/MOVSD copy 1, 2, or 4 bytes between memory addresses, implicitly using ESI (source) and EDI (destination) and auto-adjusting both per the direction flag (DF) in EFLAGS. When DF is 0 the addresses decrement; otherwise they increment. The REP prefix repeats the instruction up to ECX times.
01: BE 28 B5 41 00 mov esi, offset _RamdiskBootDiskGuid
02: 8D BD 40 FF FF lea edi, [ebp-0C0h]
03: A5 movsd ; four movsd = memcpy of a 16-byte GUID
| INSTRUCTION | ACTION |
|---|---|
MOVS |
Copy from [ESI] to [EDI] |
SCAS |
Compare AL/AX/EAX with [EDI]; used with REP to scan a buffer |
STOS |
Write AL/AX/EAX to [EDI]; used to initialize a buffer (like memset) |
LODS |
Read from [ESI] into AL/AX/EAX |
SCAS underlies strlen: zero AL, save the pointer, repne scasb to the NUL byte, then subtract. STOS with REP implements memset: rep stosd with ECX = 9 writes 36 zero bytes.
Arithmetic Operations
Addition, subtraction, and the bit operations (AND, OR, XOR, NOT, shifts) map directly to instructions.
01: 83 C4 14 add esp, 14h ; esp = esp + 0x14
02: 2B C8 sub ecx, eax ; ecx = ecx - eax
04: 41 inc ecx ; ecx = ecx + 1
06: 83 C8 FF or eax, 0FFFFFFFFh ; eax = eax | 0xFFFFFFFF
08: 33 C0 xor eax, eax ; eax = 0
09: F7 D7 not edi ; edi = ~edi
10: C0 E1 04 shl cl, 4 ; cl = cl << 4
11: D1 E9 shr ecx, 1 ; ecx = ecx >> 1
12: C0 C0 03 rol al, 3 ; rotate AL left 3
13: D0 C8 ror al, 1 ; rotate AL right 1
Strength reduction
Shifts frequently replace multiplication and division by powers of two. 100 >> 1 is 100 / 2; 100 << 1 is 100 * 2. This substitution of a cheap operation for an expensive one is called strength reduction.
Multiplication and Division
Unsigned uses MUL, signed uses IMUL. MUL reg/mem multiplies the operand by AL/AX/EAX and stores the result in AX, DX:AX, or EDX:EAX, depending on operand width. The double-width result accommodates products that overflow one register: 3 * 0x80000000 yields EDX=1, EAX=0x80000000.
IMUL has three forms:
| FORM | SEMANTIC |
|---|---|
IMUL reg/mem |
Same as MUL |
IMUL reg1, reg2/mem |
reg1 = reg1 * reg2/mem |
IMUL reg1, reg2/mem, imm |
reg1 = reg2 * imm |
DIV/IDIV reg/mem take a single divisor. The dividend is AX, DX:AX, or EDX:EAX by size; quotient and remainder land in AL/AH, AX/DX, or EAX/EDX. 0xA / 2 gives quotient 5 in AL, remainder 0 in AH.
Stack Operations and Function Invocation
The stack is a contiguous region pointed to by ESP that grows toward lower addresses. PUSH decrements ESP then writes at [ESP]; POP reads [ESP] then increments ESP. The default adjustment is 4 bytes because the OS requires doubleword alignment. ESP can also be modified directly by ADD/SUB.
The processor has no native function abstraction; functions are built on the stack. CALL performs two steps: it pushes the return address (the address after CALL) and sets EIP to the target. RET pops the top of stack into EIP. The pair push 0x12345678 / ret transfers control to 0x12345678.
A calling convention is defined by the platform ABI: how parameters are passed, in what order, who cleans the stack, and where the return value goes.
| ASPECT | CDECL | STDCALL | FASTCALL |
|---|---|---|---|
| Parameters | Stack, right-to-left | Stack, right-to-left | First two in ECX/EDX, rest on stack |
| Stack cleanup | Caller | Callee | Callee |
| Return value | EAX |
EAX |
EAX |
| Non-volatile regs | EBP, ESP, EBX, ESI, EDI |
Same | Same |
The function prologue establishes a new frame; the epilogue restores the previous one.
addme:
01: 55 push ebp ; prologue: save caller's frame
02: 8B EC mov ebp, esp ; prologue: new frame base
04: 0F BF 45 08 movsx eax, word ptr [ebp+8] ; first parameter
05: 0F BF 4D 0C movsx ecx, word ptr [ebp+0Ch]; second parameter
06: 03 C1 add eax, ecx
08: 8B E5 mov esp, ebp ; epilogue: restore stack pointer
09: 5D pop ebp ; epilogue: restore frame
10: C3 retn
When used as the frame base, EBP lets parameters and locals be addressed by fixed offsets: parameters at positive offsets ([ebp+8], [ebp+0Ch]), locals at negative offsets after the stack is grown. Frame pointer omission is a compiler optimization that addresses everything relative to ESP, freeing EBP as a general register.
Under CDECL the caller cleans the stack after the call (add esp, 8).
Control Flow
Higher-level constructs (if/else, switch, while/for) are built from CMP, TEST, JMP, Jcc, and EFLAGS.
| FLAG | NAME | SET WHEN |
|---|---|---|
ZF |
Zero | Result is zero |
SF |
Sign | Most significant bit of the result is 1 |
CF |
Carry | Result requires a carry (applies to unsigned) |
OF |
Overflow | Result overflows the max size (applies to signed) |
CMP subtracts one operand from the other, sets flags, and discards the result. TEST performs a logical AND instead. Jcc branches on the resulting flags.
| CODE | MEANING | FLAGS |
|---|---|---|
B/NAE |
Below (unsigned) | CF=1 |
NB/AE |
Above or equal (unsigned) | CF=0 |
E/Z |
Equal / zero | ZF=1 |
NE/NZ |
Not equal / not zero | ZF=0 |
L |
Less than (signed) | (SF ^ OF) = 1 |
GE/NL |
Greater or equal (signed) | (SF ^ OF) = 0 |
G/NLE |
Greater than (signed) | ((SF ^ OF) \| ZF) = 0 |
Signedness from conditional codes
Assembly has no type system. The choice of conditional code reveals signedness: "above/below" implies unsigned operands, "greater/less" implies signed.
If-Else
An if-else is a compare or test followed by a Jcc. test edx, edx followed by jz is the standard idiom for testing whether a register is zero.
Switch-Case
A switch is a sequence of if-else comparisons. When cases are dense and consecutive, the compiler builds a jump table: an array of handler addresses indexed directly by the case value, eliminating per-case comparisons.
01: cmp edi, 5 ; range check
02: ja short loc_10001141 ; default
03: jmp ds:off_100011A4[edi*4] ; index into jump table
...
19: off_100011A4 dd offset loc_10001125 ; the table
Loops
Loops are Jcc and JMP combinations — equivalent to if-else plus goto. A for loop compiles to an initialization, a body, an increment, a compare, and a conditional jump back. The compiler may omit the initial comparison when the counter's starting value makes the first iteration certain. The conditional code again reveals signedness: JL means the counter is signed.
The LOOP instruction executes a block up to ECX times, decrementing ECX each iteration:
01: 8B CA mov ecx, edx
02: loc:
03: AD lodsd
04: F7 D0 not eax
05: AB stosd
06: E2 FA loop loc ; ecx--, jump if ecx != 0
System Mechanism
Address Translation
Physical memory is divided into 4KB pages. Virtual addresses are used by instructions when paging is enabled; the memory management unit (MMU) translates each to a physical address before access.
On x86 with physical address extension (PAE), a virtual address decomposes into indices into three tables plus an offset:
| STRUCTURE | DESCRIPTION |
|---|---|
| PDPT | Page directory pointer table: 4 eight-byte entries, each pointing to a PD |
| PD | Page directory: 512 eight-byte entries, each pointing to a PT |
| PT | Page table: 512 eight-byte entries, each a PTE |
| PTE | Page table entry: describes the page and its permissions |
The bit layout of a virtual address under PAE:
| FIELD | BITS |
|---|---|
| Index into PDPT | 2 |
| Index into PD | 9 |
| Index into PT | 9 |
| Page offset | 12 |
CR3 holds the physical base of the PDPT. Translation walks PDPT → PD → PT → page, clearing the low 12 bits (flags/reserved, including the NX bit at bit 63) of each entry to obtain the next base. Each process has its own CR3, giving process-specific address translation and the illusion of a private address space.
Interrupts and Exceptions
A hardware interrupt is raised by a device requiring attention; it is associated with a number that indexes an array of function pointers. The processor calls the handler and resumes where it left off. Hardware interrupts are asynchronous.
Exceptions arise during instruction execution and fall into two categories:
| CATEGORY | DESCRIPTION | RESUMES |
|---|---|---|
| Fault | Correctable (e.g., page fault); state saved, handler corrects, instruction re-executed | Same instruction |
| Trap | Caused by a special instruction (e.g., SYSENTER) |
Next instruction |
Operating systems commonly implement system calls through this mechanism.
Walk-Through: DllMain
The chapter closes with a full analysis of Sample J's DllMain. Key techniques demonstrated:
The SIDT instruction writes the 6-byte IDT register to memory: the top 4 bytes are the IDT base, the bottom 2 the table limit. The code reads the IDT base and checks whether it lies in (0x8003F400, 0x80047400), returning early if so. 0x8003F400 is the known IDT base on Windows XP x86 core 0; the check is a crude anti-virtualization or environment heuristic.
Hardcoded IDT bases are fragile
Each core has its own IDT and IDTR, so the base differs per core. On later Windows versions the IDT base changes between reboots. Hardcoding a base address does not generalize.
The function then calls CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0), iterates processes with Process32First/Process32Next, and compares each szExeFile against "explorer.exe" using stricmp. Win32 API functions follow STDCALL. The local PROCESSENTRY32 structure is identified by its 0x128 size and the required dwSize initialization. Finally, based on fdwReason, the code calls CreateThread with start address 0x100032D0.
Recovered intent: verify a sane IDT, confirm explorer.exe is running (a user is logged on), then spawn a thread that infects the machine.
x64
x64 extends x86; most properties carry over, with differences in register size and some removed instructions (e.g., PUSHAD).
Register Set
There are 18 64-bit GPRs. 64-bit registers take the R prefix (RAX, RBP). Each aliases narrower widths: RAX → EAX → AX → AH/AL.
RBP as a general register
RBP can still serve as the frame pointer but rarely does in compiler-generated x64 code. Most x64 compilers treat RBP as an ordinary GPR and reference locals relative to RSP.
Data Movement
x64 adds RIP-relative addressing: instructions reference data at an offset from RIP, primarily to support position-independent code.
Most arithmetic is promoted to 64 bits even with 32-bit operands. Writing to a 32-bit register clears the upper 32 bits of its 64-bit parent: xor eax, eax zeroes all of RAX.
Canonical Address
Virtual addresses are 64 bits wide, but current Intel/AMD processors implement only 48. An address is canonical if bits 63 down to the most significant implemented bit are all identical — in practice, bits 48–63 must match bit 47. Dereferencing a non-canonical address raises an exception.
0xfffff801`c9c11000 canonical
0x000007f7`bdb67000 canonical
0xffff0800`00000000 non-canonical (bits 48-63 do not match bit 47)
0xffff8000`00000000 canonical
Function Invocation
Where x86 conventions often pass parameters on the stack, x64 conventions favor registers.
| PLATFORM | PARAMETER REGISTERS | OVERFLOW |
|---|---|---|
| Windows x64 | RCX, RDX, R8, R9 |
Stack, right-to-left |
| Linux x64 | RDI, RSI, RDX, RCX, R8, R9 |
Stack |
Windows x64 defines a single calling convention.