The twenty most common x86 instructions, operand types, the function prologue and epilogue, and the stack frame layout that every malware analyst reads daily in a debugger.
⚡ Quick Bite · 30s
Intel syntax and destination-source ordering, function prologue and stack frame sandboxing, and EBP offsets to locate arguments and local variables.
Watch this 30-second summary before diving into the full article. Sign in to earn 5 leaderboard points.
Assembly language is the human-readable representation of machine code. Each assembly instruction corresponds to one or a few bytes of machine code that the CPU executes directly. A disassembler converts raw bytes back into assembly; the analyst reads the assembly to reconstruct the program logic.
The x86 Intel syntax (used by IDA Pro and x64dbg) places the destination first:
MNEMONIC DESTINATION, SOURCE
For example, MOV EAX, 5 copies the value 5 into EAX. The destination always receives; the source always provides.
Note: AT&T syntax (used by GCC and GDB) reverses this order. When switching between tools, this is a common source of confusion.
Every x86 instruction operates on zero, one, or two operands in three possible forms:
| Type | Description | Example |
|---|---|---|
| Immediate | Constant value encoded in the instruction | MOV EAX, 5 |
| Register | CPU register (fastest access) | ADD EAX, EBX |
| Memory | Value at a memory address — [ ] = dereference | MOV EAX, [EBP-8] |
Constraint: at most one memory operand per instruction. MOV [EAX], [EBX] is invalid x86. Copy through a register instead.
You do not need to memorise every x86 instruction. Roughly 20 instructions account for 90% of what appears in malware.
| Instruction | Category | What It Does |
|---|---|---|
MOV dst, src | Data | Copy src into dst. Most common instruction in any binary. |
LEA dst, [addr] | Data | Load the address (not the value). Compilers use it for arithmetic shortcuts. |
PUSH / POP | Stack | Push: ESP−4, write value. Pop: read value, ESP+4. |
ADD / SUB | Arithmetic | Addition / subtraction. Updates flags. |
INC / DEC | Arithmetic | Add or subtract 1. |
MUL src | Arithmetic | Unsigned multiply: EDX:EAX = EAX × src. |
XOR dst, src | Bitwise | Exclusive OR. XOR EAX, EAX = fastest way to zero a register. |
AND / OR / NOT | Bitwise | Standard bitwise operations. |
SHL / SHR | Bitwise | Shift left / right. |
CMP a, b | Compare | Computes a−b, discards result, sets flags. |
TEST a, b | Compare | Computes a AND b, discards result, sets flags. |
JMP | Control flow | Unconditional jump. |
JE / JNE / JZ / JNZ | Control flow | Conditional jumps based on flags. |
JA / JB / JG / JL | Control flow | Unsigned (A/B) and signed (G/L) comparisons. |
CALL / RET | Control flow | Call function / return from function. |
NOP | Misc | No operation. Padding or breakpoint placeholder. |
PUSHAD / POPAD | Misc | Push/pop all general-purpose registers at once. Used by packers. |
REP MOVSB | String | Copy ECX bytes from [ESI] to [EDI]. Fast memory copy. |
LEAVE | Stack | Equivalent to MOV ESP, EBP then POP EBP. Tears down stack frame. |
INT 3 | Debug | Software breakpoint — opcode 0xCC. Triggers debugger exception. |
XOR EAX, EAX appears in almost every function. Beginners wonder why the compiler does not use MOV EAX, 0. The reason is size: XOR EAX, EAX encodes as two bytes (31 C0), while MOV EAX, 0 takes five (B8 00 00 00 00). Compilers optimise for code density.
Compilers generate a standard instruction sequence at the start and end of every function. Spotting this pattern is the fastest way to locate function boundaries in a raw binary.
; --- PROLOGUE ---
PUSH EBP ; Save caller's base pointer
MOV EBP, ESP ; New base pointer for this frame
SUB ESP, 0x20 ; Reserve 32 bytes for local variables
; ... function body ...
; --- EPILOGUE ---
MOV ESP, EBP ; Discard locals (restore stack pointer)
POP EBP ; Restore caller's base pointer
RET ; Pop return address into EIP
The SUB ESP value reveals local variable space. SUB ESP, 0x400 (1024 bytes) suggests a large buffer — and a possible overflow target.
After the prologue completes, the stack looks like this:
High address
┌─────────────────────────┐
│ Arg 2 (EBP+0x0C) │ ← Caller's data
│ Arg 1 (EBP+0x08) │
│ Return Address (EBP+04)│
├─────────────────────────┤ ← EBP points here
│ Saved EBP (EBP+0x00) │
├─────────────────────────┤
│ Local var 1 (EBP-0x04) │ ← Current frame
│ Local var 2 (EBP-0x08) │
│ ...remaining locals... │
└─────────────────────────┘
← ESP points here
Low address
Reading the stack frame:
[EBP+8] = first argument passed by the caller[EBP+C] = second argument[EBP-4] = first local variable[EBP-8] = second local variableThis layout is consistent across virtually all 32-bit Windows code compiled with MSVC or GCC. Once you internalise it, reading function call arguments in a debugger becomes immediate.
xor ecx, ecx ; counter = 0
decrypt_loop:
mov al, [esi+ecx] ; load encrypted byte
xor al, 0x55 ; XOR with key 0x55
mov [esi+ecx], al ; store decrypted byte
inc ecx
cmp ecx, edx ; compare to buffer length
jl decrypt_loop
When you see a tight loop with XOR operating byte-by-byte over a buffer, you are almost certainly looking at a decryption routine. Extract the key value and the buffer address to decrypt the hidden payload statically.
PUSH offset szFilePath ; argument pushed last (cdecl: right to left)
PUSH GENERIC_READ
CALL CreateFileA
TEST EAX, EAX ; check return value
JZ error_handler ; EAX=0 means failure
Reading the arguments pushed before each CALL reveals exactly what the function is doing: file paths, registry keys, URLs, process names.
mov ecx, func_length
lea esi, [func_start]
scan_loop:
cmp byte ptr [esi], 0xCC ; look for INT 3 breakpoint
je debugger_detected
inc esi
loop scan_loop
Malware scans its own code for 0xCC bytes (software breakpoint opcode). Bypass: use hardware breakpoints instead — they do not modify code bytes.
Sign in to mark this article as read and track your progress.