Testing#

There is no dedicated test framework for assembly. The operator exercises hand-written assembly by wrapping it as a C function and driving it from a C test harness, by running it through an emulator (unicorn), or by scripting gdb to verify register and memory state at known breakpoints.

For ud2 / int3 as in-binary checkpoints, see Errors.

Test via a C wrapper#

The simplest path: assemble the function with a C-callable signature, link with a C harness, and use the C-side test framework.

; sum.asm
global  asm_sum
asm_sum:
    lea     rax, [rdi + rsi]    ; rax = rdi + rsi
    ret
/* test_sum.c */
#include <assert.h>
extern long asm_sum(long a, long b);

int main(void) {
    assert(asm_sum(0, 0) == 0);
    assert(asm_sum(1, 2) == 3);
    assert(asm_sum(-1, 1) == 0);
    return 0;
}
$ nasm -felf64 sum.asm -o sum.o
$ gcc -o test test_sum.c sum.o
$ ./test && echo OK

For richer assertions, drop the asm function into a Unity / cmocka / Criterion harness; the assembly side is invisible.

GDB scripting#

gdb runs commands from a script and exits with the right code; useful for verifying state at named labels.

# test.gdb
file ./hello
break _start
run
info registers rax rdi rsi
continue
quit
$ gdb -batch -x test.gdb

gef and pwndbg are GDB plugins use for exploit work; both expose better disassembly views, register inspection, and exploit-aware commands.

Unicorn (CPU emulator)#

Unicorn emulates the CPU without an OS; the operator sets up registers and memory, runs arbitrary code, and inspects the post-state. Bindings exist for Python, C, Rust, Go.

from unicorn import Uc, UC_ARCH_X86, UC_MODE_64
from unicorn.x86_const import UC_X86_REG_RDI, UC_X86_REG_RSI, UC_X86_REG_RAX

ADDR = 0x1000
CODE = bytes.fromhex("488d0437c3")     # lea rax, [rdi + rsi]; ret

mu = Uc(UC_ARCH_X86, UC_MODE_64)
mu.mem_map(ADDR, 0x1000)
mu.mem_write(ADDR, CODE)

mu.reg_write(UC_X86_REG_RDI, 7)
mu.reg_write(UC_X86_REG_RSI, 35)
mu.emu_start(ADDR, ADDR + len(CODE))
assert mu.reg_read(UC_X86_REG_RAX) == 42

Use Unicorn for unit-testing payloads, fuzzing gadgets, and proving behaviour without booting a full process.

pwntools (for payload testing)#

The operator’s Swiss-army knife for exploit work. pwntools assembles, disassembles, runs binaries, attaches GDB, scripts ROP chains, and talks to network sockets, all from Python.

from pwn import *

context.arch = "amd64"
sc = asm(shellcraft.amd64.linux.sh())          # /bin/sh shellcode

p = process("./vuln")
p.sendline(cyclic(64) + p64(0xdeadbeef))
p.interactive()

For automated payload tests, run the target under process() (or remote via remote()), sends input, and asserts on the output.

Differential testing#

When the operator hand-writes assembly that should match the output of a compiler, the test is: compile the C reference, run both implementations on the same inputs, compare results.

long c_sum(long a, long b) { return a + b; }
extern long asm_sum(long a, long b);

/* test thousands of inputs */
for (int i = 0; i < 10000; i++) {
    long a = rand(), b = rand();
    assert(c_sum(a, b) == asm_sum(a, b));
}

This catches off-by-one bugs in arithmetic, sign extension mistakes, and ABI violations.

Fuzzing#

Wrap the asm function as a C-callable, then feed it through AFL++ or libFuzzer. The fuzzer drives random inputs; sanitizers (ASan) catch out-of-bounds and use-after-free.

#include <stdint.h>
#include <stddef.h>

extern uint32_t asm_parse(const uint8_t *data, size_t len);

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
    (void)asm_parse(data, size);                /* must not crash */
    return 0;
}
$ clang -fsanitize=fuzzer,address -o fuzz fuzz.c parse.s
$ ./fuzz -max_total_time=300

Side-channel timing#

For cryptographic assembly, the operator measures cycle counts with rdtsc and confirms the runtime is independent of the secret data.

uint64_t before = rdtsc();
cmp_constant_time(secret, guess, N);
uint64_t after = rdtsc();

The test passes when the cycle count is uniform across different guess values, given the same secret.

Sanitizers#

The C-side wrappers can run under ASan / UBSan, which catches out-of-bounds reads, use-after-free, and undefined behaviour at the C/asm boundary. The asm code itself is opaque to sanitizers; the boundary is where the operator surfaces bugs.

$ gcc -fsanitize=address,undefined -o test test_sum.c sum.o
$ ./test

Disassembly diff#

When the operator rewrites a function in assembly to match an existing C compiler’s output, diffing the disassemblies is the straightest verification.

$ gcc -c -O2 ref.c -o ref.o
$ objdump -d -M intel ref.o > ref.txt
$ objdump -d -M intel mine.o > mine.txt
$ diff ref.txt mine.txt

References#