Assembler is a low-level programming language. Below it there is only machine code (zeros and ones).
Why learn assembler?
- To understand how a computer works. Any programmer must know C. Any programmer who knows C must know Assembler.
- The ability to reverse engineer software (we live in a copyist world). For me this is first of all the vintage games of the 90s, the sources of which have not survived.
- ok. commercial development. If your device doesn’t have enough memory. – You write in asm. Microcontrollers, loaders, embedded software. If you need everything to run faster than lightning for a particular system – you also write directly to the processor, this is more efficient.
- WebAssembly (wasm) 🙂 This is not a programming language, but a technology that is now taking over the web. You don’t have to write in assembler there, but it is useful to understand assembler for code optimization.
- And analysis of all sorts of viral crap. Computer security and anti-virus labs are where assembler comes in handy.
Assembler is a family of languages. It is different for different architectures. Don’t get your head screwed on and focus on x86 for starters – there is the most information on it.
What do you need to know to get into asm? The ability to program in C and an understanding of binary and hexadecimal notation. You don’t need to know C. You can learn it in the process.
So. How does a PC work? Check:
CPU (central processing unit), aka processor, has inside:
Control unit. It works with RAM. It takes the addresses of the instructions from the memory and then stores them in registers.
Registers. The “built-in” memory of the processor used to execute instructions. In fact, they are small memory cells that are located on the processor; from a programmer’s point of view, you could say that a register is a temporary variable with no data type, but a certain size.
Don’t confuse “registers” with “processor cache”,
which is essentially a branch of the RAM,
closer to the “decision center”
and where data frequently used by the processor is stored,
so as not to jerk the main RAM all the time.
ALU (arithmetic logic unit). It processes the data stored in registers according to instructions. Then it saves the received information into registers or RAM.
The point of coding in Asm is to interact with registers of the processor, which in turn interacts with the stack. There are different architectures: x86 (desktop), ARM (mobile), AVR (microcontrollers), but for now we will study only x86.
It is also worth mentioning that there are many assemblers for x86. The most popular one for reversing toys is MASM (Microsoft Macro Assembler) although there is also e.g. NASM (Netwide Assembler). We will study the native MASM.
So we know that the registers are special memory cells located directly in the processor.
The processor reads an instruction (aka opcode) from memory to itself into a register and starts its execution. Example of an instruction:
MOV EAX, 15
Руку MOV – opcode; EAX – register; 15 – value which putted to register.
Most important registers are:
1) general-purpose registers (GPR)
2) special purpose registers
— instruction register
— flag register
— segment registers
Even x86 has a lot of registers, but you don’t need to learn them all by heart, you only need to understand a few (I marked them in red in the picture).
Instruction pointer is a register that stores a pointer to the instruction on the stack to be executed next; it indicates its offset (address) in the code segment. After execution, it changes to the address of the next instruction.
So… General Purpose Registers aka Data Registers. There are 8 of them; on the right is their usual name:
AX (accumulator register)
BX (base register)
CX (counter register)
DX (data register)
SI (source index register)
DI (destination index register) // receiver (recipient) index
SP (stack pointer register) // the address of the most recently added element to the stack
BP (base pointer register) // the address of the beginning of the frame from which values are added or taken to the stack; its address is always static
Some commands only work with certain registers. For example, multiply and divide commands use the EAX and EDX registers to store the raw data and the result of the operation. Loop control instructions use the ECX register as the loop counter.
The registers above are 32-bit, as indicated by the letter E at the beginning. The 64-bit ones will start with R. If there are two letters in the name, the register is 16-bit.
EFLAGS – processor state (zero flag, carry flag, etc.)
The status flags (bits 0, 2, 4, 6, 7 and 11) are the result of arithmetic instructions (ADD, SUB, MUL, DIV).
The carry flag CF (0) marks the carry/borrow from the most significant bit and marks the overflow of unsigned numbers. See binary number arithmetic. // This is the only flag we can change directly (STC, CLC and CMC instructions).
The PF parity flag is set when the lowest significant byte is even // the lowest significant byte is the one on the right.
The auxiliary carry flag AF is set when carry/receive from bit 3 of the result.
The ZF zero flag becomes 1 when the result of the operation is zero.
The sign flag SF is equal to the value of the high sign bit.
The overflow flag OF (11) is raised when the result is too long to fit in a register or memory location.
Direction Flag DF (10) to decrease/increase addresses by controlling line instructions (MOVS, CMPS, SCAS, LODS, STOS). To set/reset this flag – STD and CLD.
A comment in the margin:
Since I’m primarily learning asm to “parse” the code of vintage games, here I need to know the Windows API in order to disassemble it. Unlike C where we have int, short, long, singed/unsigned and so on – Windows has its own types, for example WORD – unsigned 16-bit value, DWORD – 32-bit (Linuxoids hate this). The “Hungarian notation” is mandatory in the name of variables: a DWORD variable name will always start with dw. Interaction with the OS is through “descriptors” which are references to a memory location or an object (a file, process, window, menu etc); for example, when you create a window you get a descriptor HWND which you will refer to later in order to interact with this window. Descriptors differ from pointers in that they do not always give the address of an object and are not used in arithmetic.
The registries are clear. Now let’s talk about memory, i.e. the stack. Computer memory (RAM) is divided into 4 main segments (in any order):
- Static data – initialized at program startup and not changed at runtime. They are also called global, because they are accessible from any part of the code.
- Program code. Instructions executed by the processor.
- Heap – a dynamic memory where objects are constantly being created and removed from the heap while the program is running (from the heap in C we allocate memory with malloc() and release it with free().
- The stack is a temporary storage for values of procedure registers, local variables and arguments.
Simply put, a Stack is an area of program memory to temporarily store some data so that it can be used quickly and control the flow of program execution.
When a subprogram is called or an interrupt occurs, the return address – the address in memory of the next instruction of the paused program – is put on the stack and control is transferred to the subprogram or the handler subprogram. Accordingly, the stack keeps track of the place where each called procedure should return control after it terminates.
The ESP (and sometimes EBP) register works with the stack. There are three stack operations:
push – adding an item (the processor decrements the value in the ESP register and writes the value to be placed to the top of the stack).
pop – deleting an element (processor firstly copies the value to be deleted from the top of stack, and then increments the value in ESP register)
peek – reading of the head item.
We will speak about them below.
Stack Pointer (SP) is a stack pointer; it is a register of the processor which points to the address of the stack head.
Important: Do not confuse Stack pointer with Instruction pointer (see above).
There are 3 types of pointers in total: Instruction pointer, Stack pointer, Base Pointer.
They are different 🙂
Suppose, for example, that the head of the stack is located at a lower address, the next elements are located at ascending addresses. Each time a word is pushed onto the stack, SP is first incremented by 1 and then the address from SP is written to memory. Each time a word is taken off the stack (pushed out), it is first read at the current address from SP and then SP is decremented by 1. In short… There is a rule of working with the stack: the procedure that entered last – must exit it first.
Once again we remember the basic algorithm: the processor reads an instruction (aka opcode) from memory into its register and starts its execution.
Let’s walk through the popular instructions:
Data Movement Instructions
MOV – place the value in a register. Example command:
MOV EAX, 15,
MOV – opcode; EAX – register; 15 – value which putted to register
PUSH place any data on top of the stack. one “argument” is the source.
PUSH 21 the value in the SP register will be decreased by a machine word; the address will be 21
POP takes the top element from the top of the stack; this removes an element from the stack and returns it to us. The “argument” cannot be a number. After POP you have to write to a register/variable (where the value from the stack will be placed).
POP EAX take data from the top of the stack and put it in the EAX register
Control Flow Instructions
JMP (jump) – jumps from one section of the code to another at the specified address.
CALL – writes the return address on the stack and then moves to the specified address (the return address is the address of the instruction on the stack that follows the CALL command).
RET (return) – Exits the program or procedure at the return address previously written by the CALL instruction.
ADD EAX, 15 (result placed to EAX)
SUB subtraction. SUB is essentially an inverted ADD command (with a negative source).
SUB EDX, ECX (the result is put into EDX)
NEG reverses the sign of the number and reverses the flags CF, ZF, SF, OF, AF, PF.
Multiplication and division in Asm takes a long time to explain; so I’ll just give you a list, and you can google the manuals:
MUL multiplication of unsigned numbers.
IMUL multiplication of signed numbers
DIV divides unsigned numbers
IDIV division of numbers with a sign
AND bitwise logical multiplication
OR bitwise logical addition
XOR bitwise addition modulo two
CMP compare two numbers
TEST compare through logical multiplication
NOT inverts each bit
By the way, there is a cool trick called XOR swap (XOR swap algorithm). It is used to swap registers and works faster than xchg:
XOR AX, BX
XOR BX, AX
XOR AX, BX
a = 0101; b = 1010.
a = a ^ b // 1111
b = a ^ b // 1111 ^ 1010 = 0101
a = a ^ b // 1111 ^ 0101 = 1010
XOR also allows you to zero the registers, for example
XOR EAX, EAX. This is essentially analogous to the
MOV EAX, 0 command, but it works faster.
Data types in asm
There are no data types as such. In fact, there is only data written in binary. That is, there are no decimal numbers, symbols, or strings. But there are 5 directives for defining data:
Byte (DB – define byte) – defines a variable with a size of 8 bits (1 byte). Byte always occupies 8 bits; if a smaller number is stored there, the remaining bits are replaced by zeros. The maximum value is 255.
Word (DW – define word) – word. It consists of 16 bits (2 bytes). Maximum value: 65 535
Double Word (DD – define double word) – double word; 4 bytes; 32 bits.
Quad Word (DQ – define quad word) – quadruple word; 8 bytes; 64 bits.
Ten Byte (DT – define ten bytes) – 10 bytes.
<name> <type> <operand>, ... <operand>, example:
foo DB 1010011 – means to define a variable foo of size 1 byte with value 1010011, given in binary notation.
The same in hexadecimal:
foo DB 53h
foo DB 83d
The same as a string (83 in ASCII corresponds to S characters):
foo db 'S' ; Note that you can specify the type in both upper case (DB) and lower case (db).
For strings, the Byte (DB) data type is used. Essentially, strings are just a set of consecutive bytes, i.e. an array, for example:
foo db 115d, 107d, 111d, 98d, 107d, 105d
is the same as:
foo db 'skobki'
? gives you to set an uninitialized variable
foo dd ?
DUP allows you to fill the elements of the sequence with the same values
foo dd 9 dup (5) – define an array of 10 double words and initialize all elements with value 5. Thus:
foo dd 9 dup (5) ==
foo dd 5,5,5,5,5,5,5,5,5
We can do this trick:
foo dd 9 dup (?) – define an array with 9 uninitialized double words.