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