Sergey Bilovytskyy Research Labs

Environment Setup #

Linux/ WSL - Linux Netwide Assembler

I write in assembly on a Linux machine since I am most comfortable with the Netwide Assembler as well as the GNU Assembler. The following steps will be for installing the Assembler in a Debian-based Linux environment[1] but may also be used in the WSL for Windows 10 (I am not entirely sure about compatibility but last time I tried it was working).
sudo apt install nasm
For WSL you may have to manually install the binutils (for ld the linker) and make:
sudo apt install binutils make
[1] If you are not using a Debian-based distro, you likely know what you are doing and can look around for the appropriate package.

Linux/ WSL - Baremetal (x86) + Emulator

I have used both NASM and GNU Assembler for building x86 baremetal. The packages to download for NASM are:
sudo apt install nasm binutils make
The packages to download for GNU Assembler are:
sudo apt install gcc binutils make gdb
For WSL, you will need to install QEMU for Windows (QEMU). For Native linux, you can install the QEMU package.
sudo apt install qemu

Linux/ WSL - Baremetal (ARM) + Emulator

I have used GNU Assembler for building ARM baremetal.
sudo apt install binutils-arm-none-eabi \
	make gdb-multiarch
For WSL, you will need to install QEMU for Windows (QEMU). For Native linux, you can install the QEMU package.
sudo apt install qemu-system-arm

Linux/ WSL - Baremetal (AARCH64) + Emulator

I have used GNU Assembler for building Aarch64 baremetal.
sudo apt install gcc-aarch64-linux-gnu \
	make gdb-multiarch
For WSL, you will need to install QEMU for Windows (QEMU). For Native linux, you can install the QEMU package.
sudo apt install qemu-system-arm

Windows - Microsoft Macro Assembler

Windows Assembly is planned for a future date after I get more familiarity with it and the APIs and get an environment setup.

Hello World #

A basic 'Hello World' Program in 64-bit Linux Assembly. For syscalls, RAX denotes the syscall number. The arguments to the syscall are in the order as mentioned in the assembly conventions.
hello.asm
section .data
	msg db "Hello, World!",0x0A

section .text
	global _start

; Write Hello World - Syscall 1
_start:	mov rax, 1
	mov rdi, 1
	mov rsi, msg
	mov rdx, 14
	syscall
	; Syscall 60 - Exit Program
	mov rax, 60
	mov rdi, 0
	syscall
Compiling:
$ nasm -f elf64 -o hello.o hello.asm
$ ld -o build hello.o

Assembly Conventions #

When writing Assembly programs, it is useful to be mindful of the different conventions - especially if you are interfacing with external libraries or writing a library. One of the most notable conventions is what registers to preserve before or in a function call - this can ensure your program flow doesn't create undefined behavior or lose data from a function call.

Caller Saved (Volatile)

ARM

r0, r1, r2, r3, and lr

x86

rax, rcx, rdx, r8, r9, r10, and r11

Callee Saved (Non-volatile)

ARM

r4, r5, r6, r7, r8, r9, r10, r11, and r13

x86

rbx, rbp, rdi, rsi, rsp, r12, r13, r14, and r15

Function Arguments

x86

(In order from first to last argument): rdi, rsi, rdx, rcx, r8, r9. All remaining arguments are placed on the stack.
An example function call:
label:	push rbp
	mov rbp, rsp
	push %NonVolatileRegister1%
	push %NonVolatileRegisterN%
	...
	pop %NonVolatileRegisterN%
	pop %NonVolatileRegister1%
	mov rsp, rbp
	pop rbp
	ret

Dumping Hex Values to Buffers #

When writing Assembly Programs, I find it helpful if I can dump register contents at different points in the program. It can be especially useful to see the values in different bases (Octal, Decimal, Hexadecimal). In more advanced setups, debuggers can be useful to dump register contents for each line of execution. Even when using a debugger, it can still be nice to see the values in a different base, which is where the wbuf command can be useful.
main.asm
section .bss
	charactersUsed resb 1

section .data
	characterBuffer times 63 db 0
	firstDigit db 0, 0x0A
	characterLength equ 64
	numerals db "0123456789ABCDEF"

section .text
	global _start

; Program Entrypoint
_start:	mov rax, 0xDEADBEEF
	mov rcx, 8 ; Octal
	call wbuf
	call pbuf
	mov rax, 0xDEADBEEF
	mov rcx, 10 ; Decimal
	call wbuf
	call pbuf
	mov rax, 0xDEADBEEF
	mov rcx, 16 ; Hexadecimal
	call wbuf
	call pbuf
.exit:	mov rax, 60
	mov rdi, 0
	syscall

; RAX: Value to write to buffer
; RCX: Base for representation
;  e.g. 2, 8, 10, 12, 16
;  binary, octal, decimal,
;   duodecimal, hexadecimal
wbuf:	cmp rcx, 16
	; Currently only up to hex
	;  is supported
	; Extended bases can be 
	;  implemented by extending
	;  the numerals table
	; Jump above
	;  Don't consider signed 
	;  (negative rcx) result
	ja .exit
	cmp rcx, 2
	jb .exit
	; Fill digits in the buffer
	;  from the lowest power up
	;  123456
	;   <---
	std
	push rbx
	push rsi
	push rdi
	; Initially include newline 
	;  character
	mov rbx, 1
	mov rdi, firstDigit

.loop:	inc rbx
	xor rdx, rdx
	; Divide out the lowest digit
	;       rax  r rdx
	; rcx │ rax
	div rcx
	; Lookup the digit and insert
	;  it in the character buffer
	lea rsi, [numerals + rdx]
	movsb
	cmp rax, 0
	jne .loop
	; charactersUsed is only a byte 
	;  since the max buffer size is 
	;  64 < 255
	mov byte [charactersUsed], bl
	pop rdi
	pop rsi
	pop rbx
.exit:	ret

; Write buffer to console
pbuf:	push rsi
	push rdi
	mov rax, 1
	mov rdi, 1
	lea rsi, [firstDigit+2]
	xor rdx, rdx
	mov dl, byte [charactersUsed]
	sub rsi, rdx
	syscall
	pop rdi
	pop rsi
	ret

Assembly Sections #

There are 3 main sections: Text, Data, BSS. The Text segment is for assembly instructions. The Data segment is for initialized data. The BSS segment is for uninitialized data.

Makefiles #

It can be quite cumbersome to write out the build commands to reassemble the binary after each change. To simplify the build process, Makefiles can be used to run the commands automatically. An example:
Makefile
OBJ=obj/main.o
NAME=projectname

default: clean build/$(NAME)

.PHONY: clean

build/$(NAME): $(OBJ)
	ld -o $@ $^

obj/%.o: src/%.asm
	nasm -f elf64 -o $@ $<

clean:
	rm -f obj/*.o build/$(NAME)
After setting up your Makefile, assembling and linking your project is as simple as:
$ make
You can also add run to your .PHONY line after clean, replace build/$(NAME) with run on the default line, and add to the end of the Makefile:
run: build/$(NAME)
	./$<
With this addition, when you run $ make it will assemble the project and run it immediately.
My typical project structure looks like:
Project Root
├─ Makefile
├─ linker.ld (If used)
├─ gdbinit (If used)
├─src
│  ├─ main.asm
│  └─ ....asm
├─obj
│  ├─ main.o
│  └─ ....o
└─build
   └─ projectname

Ascii Character to Integer #

In the previous code segment, we wrote out the logic for transforming a number into an ASCII string representing the number for an arbitrary base. Now, we will take an ascii character (1 byte/ 8 bits) and convert it to a number.
main.asm
section .bss
	number resb 1

section .text
	global _start

_start:	mov rax, 0x42 ; B
	call atoi
.exit:	mov rax, 60
	mov rdi, 0
	syscall

atoi:	sub ax, 0x30
	cmp ax, 10
	; Check if ASCII 0-9
	jb .exit
	sub ax, 7
	; ASCII Uppercase A-F
	cmp ax, 16
	jb .exit
	sub ax, 0x20
	; ASCII Lowercase a-f
	cmp ax, 16
	jb .exit
	; Invalid ASCII character 
	;  detected - return 1
	mov rax, 1
	ret
.exit:	mov byte [number], al
	mov rax, 0
	ret