Setup#

Assembly has no single toolchain. Pick an architecture (x86_64, ARM64, RISC-V), a syntax (Intel vs AT&T, NASM vs GAS), an assembler, and a linker. The choice is driven by the target the operator is reading or writing for.

Install#

1. Pick an assembler.

Assembler

Syntax

Architectures

Notes

nasm

Intel

x86, x86_64

The default for hand-written x86 / x64.

yasm

Intel / GAS

x86, x86_64

NASM-compatible, alternative implementation.

gas

AT&T (default), Intel via .intel_syntax

everything GCC supports

Ships with binutils; what GCC drives.

as (clang)

integrated

everything LLVM supports

Bundled with clang / LLVM.

2. Install on Linux.

$ sudo apt install nasm yasm binutils gdb       # Debian / Ubuntu
$ sudo dnf install nasm yasm binutils gdb       # Fedora / RHEL
$ brew install nasm yasm binutils gdb           # macOS

3. Install a disassembler and reverse-engineering tool.

$ sudo apt install radare2 binutils             # objdump, nm, strings
# Ghidra, IDA Free, Cutter are GUI installs

4. Verify.

$ nasm -v
$ as --version
$ ld --version
$ objdump -v
$ gdb --version

Setup project#

1. Lay out the source.

my-asm/
├── Makefile
├── src/
│   ├── hello.s          # GAS / AT&T
│   └── exit.asm         # NASM / Intel
└── README.md

2. NASM “hello, world” on Linux x86_64.

; src/hello.asm
section .data
    msg: db "hello", 0x0A
    len: equ $ - msg

section .text
    global _start

_start:
    mov rax, 1          ; sys_write
    mov rdi, 1          ; stdout
    mov rsi, msg
    mov rdx, len
    syscall

    mov rax, 60         ; sys_exit
    xor rdi, rdi
    syscall

3. Build.

$ nasm -f elf64 src/hello.asm -o hello.o
$ ld hello.o -o hello
$ ./hello

4. Minimal Makefile.

AS    = nasm
LD    = ld
AFLAGS = -f elf64

hello: hello.o
^I$(LD) -o $@ $<

%.o: src/%.asm
^I$(AS) $(AFLAGS) -o $@ $<

clean:
^Irm -f hello *.o

.PHONY: clean

Inline assembly in C#

The common case where write a few lines of assembly. GCC / clang use __asm__ blocks; the syntax follows AT&T by default.

#include <stdint.h>

static inline uint64_t rdtsc(void) {
    uint32_t lo, hi;
    __asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
    return ((uint64_t)hi << 32) | lo;
}

Cross-architecture#

For ARM, RISC-V, or non-Linux targets.

$ sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
$ sudo apt install gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu

$ aarch64-linux-gnu-as src/arm.s -o arm.o
$ aarch64-linux-gnu-ld arm.o -o arm

# Run cross-arch binaries with qemu-user
$ sudo apt install qemu-user
$ qemu-aarch64 ./arm

Common Tasks#

Disassemble a binary.

$ objdump -d -M intel hello
$ r2 -A hello                       # interactive (radare2)

See what GCC produces for a C function.

$ gcc -S -O2 -masm=intel example.c -o example.s

Step through a binary in gdb.

$ gdb hello
(gdb) break _start
(gdb) run
(gdb) layout asm
(gdb) si                            # step one instruction

Convert bytes into NASM-style data.

$ xxd -i shellcode.bin | sed 's/.*= */shellcode db */'

Compute the size of a function.

$ nm --size-sort hello

References#