Win Asm 101 - Intel 8x86-32
by karmabis, 2015
Displacement (disp) - Address is encoded into the instruction
Immediate (imm) - Value is encoded into the instruction
Instruction - Assembly mnemonic (source file, e.g. nop)
Opcode - Encoded instruction (executable, e.g. $90)
Register (reg) - Storage in cpu
Virtual memory - logical addressing where each process has its own range
First, let's start off with the syntax of 80x86-32 assembly language, or as
most people refer to it, x86. The x signifies which version of the language you
are using; different processors released by Intel have added additional
instructions to the set, and so while the 80486 processor family may support
the bswap (byte swap) instruction, which is handy for network programming, an
80386 processor would not. Nowadays, however, you're unlikely to run into an
x86 computer that doesn't support 486+ instructions, so don't worry about
version-specific instructions right now.
Unlike higher level languages that you may be used to, assembly does not
encourage or allow compound statements; e.g. instead of saying
var i *= 5 + x / y;
You would instead have to break that down into each seperate statement.
var i = i * (5 + (x / y));
And then write out each operation in order, in mnemonics. (pseudo used):
div x, y
add x, 5
mul i, x
This syntax probably looks a little wierd, but it's a reminder that
assembly is a low-level language; there is no magic happening here, and each
segment of code can be examined and it will literally do what it was written.
Each instruction takes up it's own line, and each operand is seperated by a
comma. ie, xx 11, 22
Instructions generally follow the same general formats. Single operand
instructions (e.g. bswap) are written as xx 11, where the mnemonic and then
the operand follow, with no comma. Most of these operate on a register (more
about that later). With a few exceptions such as mul/div, most single operand
instructions use and modify the operand.
Double operand instructions are written as xx 11, 22. Generally, the first
operand is the destination operand, and the second is the source. E.g., the mov
instruction (which loads a value from source into dest) is mov dest, src.
Since assembly is so verbose, it will often require comments and labels.
The semicolon is used to denote comments, and is the same as // comments in C
based languages. There is no standard block comment, however many assemblers
support some kind (see your assembler's documentation, I am not aware of fasm's
block comment syntax).
mov xx, xx ; this is a valid comment
; h mov xx, xx <-- this line is commented out
; valid empty line comment
As a part of documentation, preprocessor defined constants should be used
instead of magic numbers. The standard assembler directive is "equ", used as
"LABEL equ VALUE".
DEFINED_SIZE equ $10
ANOTHER_CONST equ 'a'
Fasm (and many other assemblers) support a more human-readable syntax by
using the same form except "=" instead of "equ".
DEFINED_SIZE = $10
ANOTHER_CONST = 'a'
Symbolic labels can be used to address and call in memory. Labels are
identical to C based languages, with a name then a colon. Most assemblers
support the ability to have instructions on the line after the colon, however
it is invalid to have an instruction before the label (which only makes sense,
seeing as you would be addressing the next instruction anyways).
mov xx, xx
AnotherValidLabel: mov xx, xx
mov xx, xx InvalidLabel: # this is wrong
Fasm supports anonymous labels, useful for loops and operations where you don't want to clog the namespace. They are '@@:', and are referred to by @f for next anonymous label, and @b for last anonymous label.
sub xx, 1 # decrement loop counter, also sets zero flag if the destination is 0 afterwards
jg @b # if it's > 0 (signed comparison), loop
Hexidecimal numbers are specified by prepending with a $ or 0x, or appended
with h. The most commonly supported is $, but Fasm allows all three.
HexNumber = 0x10
AnotherHex = $10
LastMethod = 10h
Binary is specified by appending a b. Fasm allows a ` between nibbles for
Bitmask = 0001b
AnotherMask = 0101`1010b
Declaring variables is a similar process, except following the label is an
identifier rather than a colon. Variables are allocated at the same physical
location as you declare them, so remember there is no optimizing, so make sure
variables are placed in a section with the same protection as necessary.
Variables can be initialized with either a value or a '?' for uninitialized
data. If all of the data in a section (or data at the end of a section) is
uninitialized, then it won't need to physically exist on disk and can be a
purely virtual section (memory will be allocated for it upon runtime).
A. Variable size identifiers
ID | size
| db | 1 |
| dw | 2 |
| dd | 4 |
| dq | 8 |
Hint: at least in the PE format, virtual sections are initialized to 0, so
any uninitialized data will be guaranteed 0 (proving the usefulness of .bss
SomeVar dd $ff ; dword initialized to $000000ff
AnotherVar db ? ; uninitialized byte
Arrays are formed by the familiar label, the number of variables in that
array, and then "dup(INITIALIZER)".
SomeArray db 3 dup(0) ; array of 3 bytes initialized to 0
AnotherArray dd DEFINED_SIZE dup(?) ; array of DEFINED_SIZE uninitialized
There is no address-of (&) operator in asm, instead labels are treated as
pointers, and the bracket operators () are used to access data at pointers.
It is safer to include a size modifier in front of accessed memory,
however ideal mode will use the appropriate size (ie a 32 bit mov if the dest
is 32 bits, 16 bit mov if the dest is 16 bits.
mov xxx, AnotherVar ; loads pointer to SomeVar into xxx
mov xxx, dword [SomeVar]; loads dword value of SomeVar into xxx
mov xxx, [SomeArray] ; unsafe practice, loads dword value at SomeArray
; into xxx. Notice it is an array or bytes, however
; there is no error because we did not specify the
mov xxx, byte [SomeArray] ; better because it will throw an error if we
; don't use an 8 bit register (or movXx).
Memory access isn't fast enough, however, and requires a pointer to address it.
So the CPU has a small amount of memory set aside, that is much faster and
produces shorter opcodes. Due to backwards compatibility, most instructions
affect specific registers. For example, in logical math operations, ones that
use the eax register are one byte shorter than their counterparts that use two
The prepended 'e' stands for extended, or 32 bit size.
B. General purpose registers
| | Xh | Xl |
| | Xx |
| eXx |
General purpose registers eax, ebx, ecx, and edx follow the same pattern.
Each can be accessed as 32, 16, or 8 bytes, using the diagram above and
substituting X for the register letter. I.e., eax can be accessed as 32 bits
eax, 16 bits ax, high 8 bits ah, and low 8 bits al.
The other registers are esp, ebp, esi, and edi. These all have unique
functions, and are only accessible as 32 or 16 bits. For example, esi can be
accessed as 32 bits esi, and 16 bits si. Ebp can be 32 bits ebp, and 16 bits
C. Special registers
| | Xx |
| eXx |
For now all you'll need to know is the names of the registers, and which
ones are general purpose. Also, registers are much faster than accessing
memory, and generate shorter instructions when used.
There is one more special register, FLAGS. It describes properties of the destination operand's value, and is updated by arithmetic and most other instructions. Each bit is a boolean, and the abbreviations that correlate can be appended to jmp and other instructions, such as movc for mov with carry.
The FLAGS register cannot be directly accessed, at most the "load/store flags into accumulator" instructions, lahf & sahf, however you should not need to directly access it. Using conditional instructions after arithmetic or test instructions, and setting/clearing the direction flag to specify which way to copy data is sufficient.
There are more flags, but these are the relevant ones and their abbreviations:
C: Carry bit tells you if the last operation overflowed or shifted bits right (up). For instance, adding 0xFF and 0xFF would equal 0x1FE, which is not an 8 bit result. The carry flag is the highest "1" bit.
S: Sign bit specifies if a signed number is negative. In x86, the highest bit is the sign bit. 0xFF is (char)-1. This can be ignored for unsigned numbers, which is mostly what you will be using.
O: Overflow, like carry, specifies if a number went past its max value, but it is for signed numbers. For example, the largest signed byte value is 0x7F = 127. 0x7F + 1 = 0x80, which is treated as signed -128. Many exploits and game cheats are based on over/underflow errors.
D: The direction flag controls which way esi/edi move when looping over input/output. Set to 0, they are incremented and would read memory like a string. E.g. [ '\0', 1, 2, 3, esi -> a, b, c, d, e, f, '\0' ] would be read as abcdef. Set to 1, they are decremented at each operation and would read 321.
The Intel processors offer two ways to address more than 232 bits memory:
segmentation, and virtual memory. Virtual memory is only available in 32 bit
mode, and allows the separation between logical addresses and physical
addresses. Windows will automatically set up virtual memory, and each process
will have its own address space (from 0x0000000 - 0xffffffff, although
0x80000000+ is reserved for the kernel). If you're observant, you'll notice the
sign (high) bit is set on all kernel-owned addresses. These are also ring 0
permission pages, meaning you cannot read/write to them.
A. Example Windows address space
| Reserved | 0x00000000
| Process | 0x00400000
| Dlls | 0x10000000
| Kernel space | 0x80000000
Virtual memory is kept track of in the page directory and page table. This
means not all addresses are not necessarily able to be referenced - if there is
no entry for them, it will result in an access violation exception. However,
pages that have entries can be mapped to physical addresses (ie, they are backed
by real memory and are pointing towards a specific physical page). When an
address is accessed, the MMU translates virtual addresses to physical addresses