Intel syntax like NASM – with the bytes it becomes.
Asmonite builds a program byte by byte from the source. The syntax is the familiar one: ; for comments, mov eax, [ebx+ecx*4+8], labels, db and dd. This page shows what it can do, and next to every example the bytes the assembler writes (taken from the real assembler). Where it differs from NASM, it says so.
The examples are x86 (32 bit, .arch i386). For x64, ARM Thumb-2 and ARM64 the line layout and the calculation are the same; their instructions are explained by F1 in the program.
At most one instruction per line. Everything is optional.
start:label (the colon is optional)movinstructioneax, [ebx+8]operands, separated by commas; copycomment (; only)start: mov eax, [ebx+8] ; copy
add eax, 1
jmp start
8B 43 08 83 C0 01 EB F8
A label is the address of the place where it stands. The case of a label does not matter (L1 and l1 are the same label). Write instructions in lower or upper case, but an instruction with a capital first letter chooses the second encoding (see below).
Wherever a number stands you may calculate: in operands, with db, dw, dd, dq, resb, align, org.
Operators in this order of precedence, strongest first: sign and ~; * / %; + -; << >>; &; ^; |. Brackets always work. Numbers: decimal, 0x1F, 1Fh, 101b, 0b101 and a character in single quotes ('A').
| Expression | Value | Note |
|---|---|---|
4*16+1 | 65 (0x41) | multiplication before addition |
(2+3)*4 | 20 (0x14) | brackets |
100/7 | 14 (0xE) | integer, signed |
100%7 | 2 | the remainder |
1<<5|3 | 35 (0x23) | << binds tighter than | |
0x10^0x11 | 1 | exclusive or |
~0xF & 0xFF | 240 (0xF0) | bitwise not, then and |
-5+8 | 3 | sign |
'A'+1 | 66 (0x42) | a character in quotes is a number |
1Fh | 31 (0x1F) | hexadecimal with h |
101b | 5 | binary with b |
0b101 | 5 | binary with 0b |
name equ expression gives a number a name (the line writes no bytes; the constant may stand behind its use). $ is the address of the line, $$ the start of the section. The distance of two labels is a number, a size: end-start, $-msg.
size equ 4*16
mov eax, size+1
msg: db 'hi', 0
len equ $-msg
mov al, len
start: mov ecx, end-start
nop
end:
B8 41 00 00 00 68 69 00 B0 03 B9 06
00 00 00 90
A distance needs the addresses; the assembler therefore runs more than once until they stay the same (you do not notice). Not possible are a number behind a label in a jump or call (jmp start+4) and division by zero – both are an error with the line number.
Registers, numbers, labels and memory in square brackets: [base + index*scale + number].
| You write | Bytes | Note |
|---|---|---|
mov eax, [ebx] | 8B 03 | a register |
mov eax, [ebx+8] | 8B 43 08 | register plus number |
mov eax, [ebx+ecx*4+8] | 8B 44 8B 08 | base, index times scale (1, 2, 4, 8), number |
mov eax, [ebx+2*4] | 8B 43 08 | the constant part is calculated: like [ebx+8] |
mov eax, [ebp-4] | 8B 45 FC | a minus continues the address |
mov byte [ebx], 1 | C6 03 01 | size of the access: byte, word, dword |
mov word [ebx], 1 | 66 C7 03 01 00 | as above, 16 bit: prefix 66 |
mov eax, [es:ebx] | 26 8B 03 | segment in front of the address |
A label alone (mov eax, label) is its address as a number, in brackets (mov eax, [label]) the memory there. label+4 and label-4 are addresses, also as a value and in dd. The address is the virtual address in the loaded program (image base plus position), not the distance from the start of the file.
A label is the target; the assembler finds the length of the distance itself.
| You write | Bytes | Note |
|---|---|---|
l: jmp l | EB FE | without a size the assembler takes the shortest form |
l: jmp near l | E9 FB FF FF FF | near: the long form (32 bit distance) |
l: jz short l | 74 FE | short: one byte distance |
l: nop jz l | 90 74 FD | a conditional jump backwards |
Different from NASM: a number behind jmp is the distance, not the target address: jmp 5 is EB 05. With a sign it works too: jmp short -2, call near -5. loop and jecxz need short and a label or a distance.
What is to stand in the program as bytes.
| You write | Bytes | Note |
|---|---|---|
db 'AB', 0, 10 | 41 42 00 0A | bytes; text in single quotes |
dw 0x1234, 1+1 | 34 12 02 00 | words (2 bytes, low byte first) |
dd 1, -1 | 01 00 00 00 FF FF FF FF | double words (4 bytes) |
dq 1 | 01 00 00 00 00 00 00 00 | quad words: 8 bytes |
du 'Hi' | 48 00 69 00 | text as UTF-16: 2 bytes per character |
nop align 4 nop | 90 00 00 00 90 | align pads with 00 to a multiple |
org 4 nop | 00 00 00 00 90 | org pads with 00 up to the position in the file |
resb 4 nop | 00 00 00 00 90 | resb: room for four bytes |
A label in front of a data line (msg db 'hi', 0, with or without a colon) is the address of the data. With .format pe, resb may stand only at the end of a section: the room is not in the file but is made when the program is loaded.
Directives at the start say what the assembler writes. They stand before the first instruction.
The assembler writes the header, the sections and the import table; you write code, data and imports.
.format pe console
.arch i386
.import msvcrt.dll cdecl printf
.import kernel32.dll ExitProcess
.code
start:
call [printf](text)
call [ExitProcess](0)
.data
text: db 'Hello', 10, 0
With .format binary the bytes stand as you write them: a .COM file, a boot sector or a header you write yourself.
.format binary
.output hello.com
.arch x86_real
section 0x100
start:
mov ah, 0x09
mov dx, msg
int 0x21
mov ax, 0x4C00
int 0x21
msg: db 'Hello!', 13, 10, '$'
| Directive | Meaning |
|---|---|
.arch i386 | processor: i386 (32 bit), x86_real (16 bit, DOS), x64, thumb (ARM Thumb-2), arm64 |
.format pe [console|gui] | writes a Windows program; .format binary the bytes as they stand |
.output name | the name of the file that is written (next to the source) |
.code, .data | with pe: the following lines are program or data |
.import dll name, name | imported functions; the name is a label of the import table: call [name]. For C functions add cdecl (see func) |
.entry label, .imagebase n | entry point, load address |
section n, segment n, org n | with binary: a section with this virtual address, a DOS segment, padding up to the file position |
The source looks like NASM, but the assembler is not NASM compatible. The main points:
| You write | Bytes | Note |
|---|---|---|
mov eax, ebx | 8B C3 | lower case: the first encoding |
MOV eax, ebx | 89 D8 | an instruction that starts with an upper-case letter: the second encoding where there are two |
mul eax, ebx | F7 E3 | the accumulator is written (mul ebx does not work) |
push byte 5 | 6A 05 | byte before the number chooses the short form |
mov ax, 5 | 66 B8 05 00 | without .arch it is 32 bit: a 16-bit register needs the prefix 66 |
| NASM | Asmonite |
|---|---|
bits 16, section .text | .arch x86_real; section <number> (the name is not evaluated) |
org 0x100 sets the load address | org pads the file with 00 up to this position |
align 4 pads code with nop | align always pads with 00 |
times, incbin, macros, %define | do not exist; there is no preprocessor |
global, extern | do not exist; imports: .import |
| labels are case sensitive | labels are not |
.local is valid until the next label | .l is an ordinary label, the same one twice is an error |
db "AB" | single quotes only: db 'AB' |
dd label is the distance from the start | dd label is the virtual address; rva label the distance |
db 256: a warning only | db 256 is an error |
Not included yet are the x87 FPU, MMX/SSE and some system instructions. The help (F1 on a line) in both programs explains every instruction the assembler knows and every directive.
func writes the stack frame, call Name(…) pushes the arguments, with stdcall and cdecl.
F1 on a line explains the instruction, the Windows function, the directive and the error message.
The ZIP has examples for Windows, DOS and ARM. From the download to the first breakpoint in 60 seconds.