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.
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.
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:
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.asmsection .bsscharactersUsedresb1section .datacharacterBuffertimes63db0firstDigitdb0, 0x0AcharacterLengthequ64numeralsdb"0123456789ABCDEF"section .textglobal_start
; Program Entrypoint
_start: movrax, 0xDEADBEEFmovrcx, 8 ; Octal
callwbufcallpbufmovrax, 0xDEADBEEFmovrcx, 10 ; Decimal
callwbufcallpbufmovrax, 0xDEADBEEFmovrcx, 16 ; Hexadecimal
callwbufcallpbuf.exit: movrax, 60movrdi, 0syscall
; RAX: Value to write to buffer
; RCX: Base for representation
; e.g. 2, 8, 10, 12, 16
; binary, octal, decimal,
; duodecimal, hexadecimal
wbuf: cmprcx, 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.exitcmprcx, 2jb.exit
; Fill digits in the buffer
; from the lowest power up
; 123456
; <---
stdpushrbxpushrsipushrdi
; Initially include newline
; character
movrbx, 1movrdi, firstDigit.loop: incrbxxorrdx, rdx
; Divide out the lowest digit
; rax r rdx
; rcx │ rax
divrcx
; Lookup the digit and insert
; it in the character buffer
learsi, [numerals + rdx]
movsbcmprax, 0jne.loop
; charactersUsed is only a byte
; since the max buffer size is
; 64 < 255
mov byte [charactersUsed], blpoprdipoprsipoprbx.exit: ret
; Write buffer to console
pbuf: pushrsipushrdimovrax, 1movrdi, 1learsi, [firstDigit+2]
xorrdx, rdxmovdl, byte [charactersUsed]
subrsi, rdxsyscallpoprdipoprsiret
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.
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:
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.