peachykeen32: A Toy ARM32 Shell in Assembly
peachykeen32 is a toy shell for ARM32 Linux written entirely in hand-written ARM assembly. No libc, no CRT, no linker scripts. Everything — input parsing, string comparison, I/O — goes through direct Linux syscalls via svc #0. It’s about 400 lines of .s and runs under QEMU or on bare-metal ARM hardware.
The Entry Point
There is no _start from a C runtime. The binary’s entry point is a minimal trampoline that sets up the stack pointer and jumps to the main loop:
.globl _start
_start:
ldr sp, =stack_top
bl main_loop
mov r7, #1
svc #0
The stack is allocated in .bss — 4kB, zeroed at load time by the kernel. The svc #0 after main_loop returns is the exit syscall; if the loop ever falls through, the process terminates cleanly.
The REPL Loop
The shell presents a peachykeen32> prompt, reads a line from stdin, and parses it into up to three null-terminated argument buffers. Command dispatch is a series of better_strcmp calls against known command strings.
check_cmd:
ldr r0, =cmd_buf
ldr r1, =str_exit
bl better_strcmp
cmp r0, #1
beq exit_handle
ldr r0, =cmd_buf
ldr r1, =str_help
bl better_strcmp
cmp r0, #1
beq help_handle
...
better_strcmp compares two null-terminated strings byte by byte and returns 1 on match, 0 otherwise. It’s not optimised — it doesn’t need to be. The input is at most a few dozen bytes per line.
Commands
| Command | Implementation |
|---|---|
help |
Writes a help string to stdout via sys_write. The string is in .rodata. |
exit |
Calls sys_exit(0) — mov r7, #1; svc #0. |
rand |
Opens /dev/urandom via sys_openat (AT_FDCWD, 322), reads 16 bytes, prints them as hex digits. |
fwrite |
Placeholder — parses args, prints the command name. File write is WIP. |
ndir |
Placeholder — parses args, mkdir syscall not yet wired up. |
The rand command is the most interesting because it touches three different syscall interfaces: openat (to get a file descriptor), read (to get entropy), and write (to display it). The syscall numbers are defined as equates at the top of the file.
.equ sys_read, 3
.equ sys_write, 4
.equ sys_openat, 322
Why Assembly
There is no practical reason to write a shell in assembly. The point is to understand exactly what happens between svc #0 and the kernel’s return. Every instruction is visible, every syscall is explicit, and there is no magic — no printf buffering, no errno, no dynamic linker. The entire binary is a static ELF with no PT_INTERP segment.
Building and Running
The project includes scripts for cross-compilation from x86:
arm-linux-gnueabihf-as src/m_peachykeen32.s -o build/peachykeen32.o
arm-linux-gnueabihf-ld -Ttext=0x10000 --no-dynamic-linker \
-nostdlib build/peachykeen32.o -o build/peachykeen32
Run under QEMU or copy to an ARM board:
qemu-arm ./build/peachykeen32
There are also dependency installers for Arch, Debian, and Gentoo, plus GDB scripts for single-stepping through the syscall interface.
What I Learned
The Linux syscall ABI on ARM32 is straightforward — arguments in r0–r5, syscall number in r7, trap with svc #0. The return value is in r0. Negative values indicate errors, but without errno you either check the raw value or ignore it. String parsing in assembly is tedious but mechanical. The hardest part was managing the fixed-size argument buffers — a line longer than 256 bytes silently truncates, which is fine for a toy but would be exploitable in anything real.
Have a comment on this article? Send me an email.