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

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]