How to Read Assembly Code for Reverse Engineering

:high_voltage: OllyDbg Reverse Engineering Series — Part 5 of 60

This chapter introduces your first practical exposure to reading assembly code for reverse engineering.

# Title Link
Part 1 What is Reverse Engineering? Why OllyDbg? What is Reverse Engineering? And Why Am I Using OllyDbg?
Part 2 OllyDbg Interface Tour Every Window Explained How to Read Disassembly in OllyDbg: A Beginner’s Guide
Part 3 Number Systems – Hex, Decimal, Binary with Real Examples Hex vs Binary vs Decimal: Explained for Programmers and Reverse Engineers
Part 4 CPU Registers What They Are and Why They Matter CPU Registers Explained: What They Are and Why They Matter in Reverse Engineering
Part 5 - You are here Your First Look at Assembly Reading Code Like a Reverser How to Read Assembly Code for Reverse Engineering

Before We Start - What Parts 1–4 Already Covered

If you are landing here for the first time, I recommend going through the earlier parts first. We already covered what reverse engineering actually is, how to navigate OllyDbg’s four main windows, how numbers work in hex and binary, and what CPU registers are. All of that is foundation.

This part is where things get real. We are going to open OllyDbg’s disassembler window and actually read the code sitting inside it line by line, instruction by instruction without panicking.


What Confused Me Before I Understood This

When I first opened OllyDbg and saw the disassembler, I saw something like this:

00401000   6A 00        PUSH 0                   Title = NULL
00401002   E8 FF040000  CALL kernel32            GetModuleHandleA
0040100C   6A 00        PUSH 0
00401013   E8 A6040000  CALL FindWindowA         FindWindowA

I had no idea what any of it meant. Four columns of stuff. Random hex numbers. Words like PUSH and CALL. I did not know where to look first.

By the end of this part you will read that same output and actually understand what every column is telling you.


What Is Assembly Language?

When you write code in C or any high level language and compile it, the compiler translates your code into raw bytes the CPU can execute. Those bytes are called machine code. Something like 6A 00 E8 FF04.

The CPU does not understand C. It only understands those raw bytes.

Assembly language is the human-readable version of machine code. Each assembly instruction maps to exactly one CPU operation. Think of it like morse code versus plain English — ... --- ... and “SOS” are the same message, just one is readable by humans.

So when OllyDbg shows you PUSH 0 that is not the original source code. That is the debugger translating raw bytes 6A 00 into something you can read. The developer never wrote “PUSH 0” the compiler produced those bytes, and OllyDbg is decoding them back for you.


How to Read One Line in the Disassembler

Every single line in OllyDbg’s disassembler window has four columns:

ADDRESS  | HEX BYTES   | INSTRUCTION      | COMMENT
00401000 | 6A 00       | PUSH 0           | Title = NULL

debugger translating raw bytes

Address - where in memory this instruction lives. When EIP (the instruction pointer from Part 4) points to 00401000, the CPU is about to execute this line.

Hex bytes - the actual raw machine code. 6A 00 is what the CPU physically reads. The assembly instruction is just our translation of those bytes.

Instruction - the human-readable assembly. This is what we spend most of our time reading. Format is almost always OPERATION DESTINATION, SOURCE.

Comment - this is OllyDbg being helpful. It is NOT the developer’s comment. OllyDbg looks at the instruction and tries to guess what it means. “Title = NULL” is OllyDbg telling you that this PUSH 0 is probably passing a NULL title to a Windows API. Sometimes it is right, sometimes wrong but it is a huge help.

So when you see this line:

76FB1021   00 1E   ADD BYTE PTR DS:[ESI], BL

You now know 76FB1021 is the address, 00 1E is the raw machine code, ADD BYTE PTR DS:[ESI], BL is the instruction, and there is no comment because OllyDbg had nothing helpful to add.


NOP The Do-Nothing Instruction

NOP stands for No Operation. The CPU reads it, does absolutely nothing, and moves to the next instruction. That is its entire job.

It is one single byte in memory 0x90.

Why would anyone want an instruction that does nothing?

Think of it like whiteout on paper. You want to erase a word but you cannot shrink the page so you paint over it. The space is still there but it is now blank. NOP is whiteout for machine code.

Here is a real scenario. Imagine a program has a license check:

BEFORE PATCHING:
00401000   74 05   JE 00401007    ; jump to "invalid license" if check fails

AFTER NOP PATCH:
00401000   90      NOP
00401001   90      NOP

The check is still there in memory. But we replaced it with two NOPs. The CPU hits them, does nothing both times, and execution continues normally. The license check is neutralized. The program runs as if the check passed.

One important thing the CPU does not skip a NOP. It actually executes it. But executing a NOP means doing nothing. So the effect feels like skipping, but technically the CPU processed it. This matters when you are tracing execution step by step later.


MOV The Most Common Instruction You Will Ever See

MOV copies a value from a source to a destination. That is it.

Think of it like a photocopier. The original stays where it is. A copy lands in the destination. Nothing gets deleted.

The format is always:

MOV  DESTINATION, SOURCE

Simple examples:

MOV EAX, 5      ; put the number 5 into EAX
MOV EAX, EBX    ; copy EBX's value into EAX. EBX is unchanged.

The Square Bracket Rule This Is Important

Square brackets mean “go to this memory address and fetch what is stored there.”

Think of it like a locker system at a train station.

MOV EAX, EBX      ; EBX is holding a value. Give that value to EAX directly.

MOV EAX, [EBX]    ; EBX is holding a locker number.
                  ; Go to that locker. Open it. Take what is inside. Put it in EAX.

Without brackets use the value directly.
With brackets treat the value as an address, go there, fetch what is stored.

More examples:

MOV EAX, [EBX]       ; go to address stored in EBX, fetch 4 bytes, put in EAX
MOV EAX, [405000]    ; go to memory address 405000, fetch value, put in EAX
MOV EAX, [ECX+38]    ; go to address ECX+38, fetch value, put in EAX

That +38 is an offset. It means start at ECX, go 38 bytes forward, that is the address we care about. You will see this constantly when code is reading fields from a structure or object.


PUSH and POP Reading Stack Operations

Remember the stack from Part 2? Think of it like a stack of plates in a cafeteria. You can only add a plate on top. You can only take a plate from the top. You cannot pull one from the middle.

PUSH puts a value on top of the stack. ESP (the stack pointer) moves down.
POP takes the top value off the stack and puts it into a register. ESP moves up.

PUSH EAX    ; copy EAX's value onto top of stack. ESP decreases by 4.
POP EAX     ; take top of stack, put it into EAX. ESP increases by 4.

Visual example:

BEFORE PUSH EAX  (EAX = 00000005):

Address   Value
0012FFC8  7C816D4F   ← ESP (top of stack)
0012FFCC  FFFFFFFF
0012FFD0  7FFD8000

AFTER PUSH EAX:

Address   Value
0012FFC4  00000005   ← ESP moves here (new top)
0012FFC8  7C816D4F   ← old values untouched
0012FFCC  FFFFFFFF
0012FFD0  7FFD8000

Nothing gets destroyed. The old values are still sitting there. PUSH just adds a new plate on top and ESP follows it.

POP does the reverse takes the top value, puts it into a register, ESP moves back up.

POP EBX:

Address   Value
0012FFC8  7C816D4F   ← ESP moves back here
0012FFCC  FFFFFFFF

EBX now holds 00000005   ← the value we just popped off

You will see PUSH and POP constantly around function calls. Parameters get PUSHed before a CALL, and results get POPped after.


PUSHAD and POPAD - Saving All Registers At Once

PUSHAD is just PUSH for all general purpose registers at once. One instruction and EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI all land on the stack.

POPAD is the exact opposite — it takes everything PUSHAD saved and puts each value back into its original register in reverse order.

Think of it like pressing Save Game and Load Game.

PUSHAD   →   Save Game. All registers go to stack.
POPAD    →   Load Game. All registers restored exactly as they were.

Why reversers care about this:

When you open a packed executable in OllyDbg, almost always the very first instruction you will see is PUSHAD. That is your signal. The packer is saving the register state before it starts its unpacking work. When you see PUSHAD near the top of a program — you are looking at a packed executable. We will use this heavily in later parts when we get into unpacking.


LEA vs MOV - The One That Confuses Everyone

This one trips up almost every beginner. Read carefully.

Both MOV EAX, [ECX+38] and LEA EAX, [ECX+38] have square brackets. They look almost identical. But they do completely different things.

Think of a house address.

MOV EAX, [ECX+38]   ; go to that house. Walk inside. Bring me what is IN the house.

LEA EAX, [ECX+38]   ; just give me the house ADDRESS. Do not go inside.

Concrete example with real values:

ECX = 0012FFB0

MOV EAX, [ECX+38]   ; goes to address 0012FFE8, fetches the value stored there
                    ; EAX = whatever is sitting at that memory location

LEA EAX, [ECX+38]   ; calculates 0012FFB0 + 38 = 0012FFE8
                    ; EAX = 0012FFE8 (the address itself, not what is there)

LEA never touches memory. It just does the arithmetic and stores the result. MOV with brackets actually goes into memory and fetches the data.

LEA is often used for quick address arithmetic when you need to calculate a pointer without actually reading from it.


XCHG - Simple Swap

XCHG swaps the values of two registers. That is all it does.

Before XCHG EAX, ECX:
EAX = 00000005
ECX = 0012FFB0

After XCHG EAX, ECX:
EAX = 0012FFB0
ECX = 00000005

No temporary variable needed. The CPU handles the swap internally in one shot.

You will see XCHG frequently in shellcode. Attackers use it as a way to move values between registers without using MOV sometimes to avoid certain byte patterns that security tools flag.


What I Found Confusing (And Now Do Not)

The comment column is not the developer’s comment. I thought it was. It is OllyDbg’s own analysis. The developer never wrote “Title = NULL” OllyDbg figured that out by recognizing the Windows API being called.

LEA and MOV with brackets look identical. Same square brackets, completely different behaviour. LEA gives you the address. MOV gives you the value at that address. I had to repeat this to myself many times before it stuck.

PUSH does not destroy what was already on the stack. Old values stay exactly where they are. PUSH just adds a new item on top and ESP moves to track it.

NOP actually executes it just does nothing. The CPU does not skip it. It processes it, finds nothing to do, and moves on. Small distinction but it matters when you are stepping through code.


What We Learned — Glossary

Instruction What It Does
NOP Does nothing. Single byte 0x90. Used to patch out instructions.
MOV DST, SRC Copies value from source to destination.
MOV EAX, [addr] Goes to memory address, fetches value, puts in EAX.
PUSH EAX Copies EAX onto top of stack. ESP decreases.
POP EAX Takes top of stack, puts in EAX. ESP increases.
PUSHAD Pushes all general registers onto stack at once.
POPAD Restores all registers from stack back to originals.
LEA EAX, [ECX+38] Calculates address ECX+38 and stores it in EAX. Does NOT read memory.
XCHG EAX, ECX Swaps values between EAX and ECX.
[ ] Square brackets = go to this memory address and fetch what is there.
+38 Offset how many bytes forward from base address.

Coming Up Next → Part 6

Now that you can read individual assembly instructions, the next part covers the instructions that actually control program flow JMP, CALL, CMP, JE, JNE. These are the ones that make decisions, create loops, and call functions. Understanding them is how you start tracing what a program actually does instead of just reading individual lines.

[Part 6 — Coming Soon]