A Crash Course in Assembly Language

The Basics of x86 Assembly for Reverse Engineering

Madeline Farina
Reverse Engineering for Dummies
8 min readMar 27, 2021

--

If you are doing any kind of reverse engineering — and by this I mean disassembling a compiled executable with tools like Ghidra to analyze the machine code — then you must become knowledgeable of the assembly language. If you have an interest in this field, you’ve probably already heard of assembly and know it has a less-than-beloved (perhaps even despised) reputation, but this does not necessarily mean it’s difficult to learn.

It’s not a standard programming language like Python or Java. These are both high-level languages, while x86 and ARM Assembly are low-level. This means they can be a lot trickier to understand and involve working with registers in memory to perform tasks. High-level languages are more easily understood by the programmer, while the computer can more readily interpret low-level language, also known as machine language. There are of course more discrepancies (you can read about them here), but this is the most basic difference between them.

When I first learned assembly for one of my required CS courses, I found it… challenging, to say the least. It was annoying as hell to code simple projects like a four-function calculator or GCD algorithm in ARM v8. Not only was the syntax nebulous and mind-boggling, I had to do all coding and testing either with an emulator or by working in the terminal, connected to a Firefly ROC-RK3328-CC computer board plugged into my laptop. I’m no stranger to working at the command line, but when it comes to coding projects, I find it’s easier to work in an IDE just because of the quantity of code I have to work with.

(in hindsight, I would have used the ssh feature in Visual Studio Code to connect to my Firefly, but notwithstanding)

All that being said, the takeaway is that it’s probably going to be annoying to learn, and that’s okay. As long as you keep practicing and studying educational materials like this post, you’ll become comfortable enough with it to start reverse engineering.

Program Build Flow

To get a better understanding of how everything works, let’s take a look at the diagram below.

Source: http://faculty.cs.niu.edu/~mcmahon/CS241/Images/compile.png

If you’ve done any kind of C++ development, you know that the .cpp file is the one which contains the programmer’s code and you have to compile it to get the output (.o) file. These files have object code, which are the machine-language programs containing operation code (op-code) that are the instructions for the computer. Interestingly, malware is often presented as object code, which is why reverse engineering is necessary to understand the op-code. One of RE’s goals is in fact to convert object code into assembly language by the process of disassembly.

So now that you have a general understanding of program flow, let’s take a look at assembly.

ARM vs x86

If you may have noticed, I mentioned both x86 and ARM v8 assembly. There is in fact a difference, since there actually multiple different architectures (and subsequent “flavors”) of assembly. x86 is one of the most common assembly languages, with its architecture having both 32-bit and 64-bit versions and its syntax either being AT&T or Intel-based. In other words, most Intel computers use x86 assembly. The differences of syntax include operand ordering, register and immediate prefixes, suffixes indicating operand size, and so on.

ARM versions are similar (it’s not like comparing apples and oranges), but they differ in that ARM is RISC architecture while x86 is CISC (see here for more). ARM instructions have conditional flags built in, allowing this can be used to avoid all those jumps around one or two instructions that you often see in Intel assembly, and ARM can’t do much with memory directly except load from and store to it. Intel assembly can perform more operations directly on memory.

For the rest of this explanation, we will be referring to x86 assembly.

Big Endian vs. Little Endian

Source: http://users.cis.fiu.edu/~prabakar/cda4101/Common/notes/lecture04.html

Processors can either be “big-endian” or “little-endian”, which refer to the order of bytes in a word of memory. Big endian systems store the most significant byte (MSB) at the smallest memory address and the least significant byte (LSB) at the largest. Conversely, little endian systems store the LSB at the smallest memory address and vice versa. Most processors are, in fact, little endian because it means using less circuits, whereas most network protocols are big endian because it’s better for transfers of information one bit at a time.

An understanding of big-endian and little-endian is needed because it helps one understand how data is stored on the stack in memory. Know that bytes are stored as little-endian on Intel computers.

Also, fun fact: a half-byte (4-bit) value is called a nibble!

x86 Registers

x86 architecture has 8 General-Purpose Registers (GPR), 6 Segment Registers, 1 Flags Register, and an Instruction Pointer for 32-bit x86.

Source: https://www.cs.virginia.edu/~evans/cs216/guides/x86.html

EAX — Stores function return values

EBX — Base pointer to the data section

ECX — Counter for string and loop operations

EDX — I/O (input/output) pointer

ESI — Source pointer for string operations

EDI — Destination pointer for string operations

ESP — Stack pointer

EBP — Stack frame base pointer

EIP — Pointer to next instruction to execute (“instruction pointer”), cannot be directly modified with mov but can indirectly be modified by referencing with operations

General Purpose registers are used for basic arithmetic and typical operations. Pointer registers are used for pointing at memory for program control.

Flags and Segment Registers

CS — Pointer to Code segment in which your program runs

DS — Pointer to Data segment that your program accesses

ES,FS,GS — Extra segment registers available for far pointer addressing like video memory

SS — Pointer to Stack segment your program uses.

OF — Overflow flag, used if destination could not store the entire result

SF — Sign flag, used if last operation yielded a value with MSB set

ZF — Set if the result of an arithmetic operation is 0

Simple Instructions

mov — moves date from one location to another without modification, in the form:

mov destination, source

lea — load effective address, calculates indirect address and stores the address (not the memory contents) in the destination

lea destination, value

jmp — tells the CPU to jump to a new location, transfers the flow of execution by changing the instruction pointer register

jmp destination

add — arithmetic adding, adds the value specified to the value stored in the destination and replaces the destination with the result

add destination, value

sub — arithmetic subtraction, similar to addition

sub destination, value

inc — increments destination by 1

inc destination

dec — decrements destination by 1

dec destination

There are also logic operations like or, and, and xor which have the same addressing modes as add and sub.

Addressing

While some instructions in assembly language do not need an operand, others need one or more, and when an instruction requires two, the second operand is the source. The source contains either the data to be delivered or the address (in register or memory) of the data. Addressing occurs with the Segment registers and has three different modes: register addressing, immediate addressing (when the data to be delivered is in the source of the instruction), register and memory addressing (when the source contains the address of the data).

And speaking of addressing, it’s important to know about a Return Address, which is a parameter which tells the function where to resume execution after the function is complete. This is necessary because functions can be called to do processing from many different parts of a program, and the function needs to be able to get back to wherever it was called from.

The Stack

Each active function call has a frame that stores the values of all local variables, and the frames of all active functions are maintained on the Stack. The Stack is a very important data structure in memory. In general, it is used to store temporary data needed during the execution of a program, like local variables and parameters, function return addresses, and more. It is static memory, meaning it cannot be altered during runtime. Dynamic memory like that allocated with the malloc() or new() functions is stored on the Heap. I will be covering the Stack vs. the Heap in my next post, but for now, we will just focus on the Stack.

Just like a stack of pancakes, the Stack in memory is Last In First Out (LIFO) ordering, which means entities on the top of the stack are popped first. And although this is weird, in x86 the stack grows downwards from high addresses to low addresses. In relation to assembly, you can find some of the most common Stack instructions below.

Stack Instructions

push value

This decrements ESP by the number of bytes occupied by value, and copies value to the address not referred to ESP.

pop destination

This copies bytes in memory from the address of ESP to the destination, then increments ESP by the size of the destination.

Now onto some uncommon stack instructions that are often used by malware (important if you are going to reverse any suspicious programs):

pusha — Pushes the 16-bit register values AX, CX, DX, BX, SP, BP, SI, DI to the stack

pushad — pushes the 32-bit register values EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI to the stack

popa — Inverse operation of pusha

popad — Inverse operation of pushad

Conclusion

I hope you know have a general understanding of Assembly and are at a good starting point to actually learn the intricacies of the language. Like anything, it gets easier with practice. I’ve included some references below that will help you in your journey.

References:

https://en.wikibooks.org/wiki/X86_Assembly/X86_Architecture

Dennis Yurichev, Reverse Engineering for Beginners, https://beginners.re/RE4B-EN.pdf

Michael Sikorski, Andrew Honig, Practical Malware Analysis: The Hands-On Guide to Dissecting Malicious Software, (2012)

--

--