armvm is an ARMv7 assembler and virtual machine that allows you to run 32-bit ARM assembly code on any host architecture. The project consists of two main components:
- ARM Assembler: Compiles ARM assembly source code into bytecode
- ARM VM: Executes ARM32 bytecode in a virtual environment with syscall support
- ARMv7 32-bit ARM Support: Full implementation of ARMv7 instruction set
- Cross-Architecture: Run ARM32 code on x86, x86_64, ARM64, or any other architecture
- Assembly-to-Bytecode Compiler: Converts ARM assembly directly to executable bytecode
- Syscall Interface: Extensible system call mechanism for host function integration
- Memory Management: Built-in stack and heap with configurable sizes
- Zero Dependencies: Self-contained implementation in C
The assembler supports ARM UAL (Unified Assembly Language) syntax with Apple/Xcode conventions, specifically targeting ARMv7 32-bit ARM architecture.
This assembler was designed to parse assembly code generated by Xcode's ARM32 compiler (using the -arch armv7 flag). The syntax follows ARM UAL with Apple Mach-O object file conventions.
Key Characteristics:
- Architecture: ARMv7 (32-bit ARM)
- Syntax Style: ARM UAL (Unified Assembly Language)
- Object Format: Apple Mach-O conventions
- Registers: r0-r15 (with sp=r13, lr=r14, pc=r15)
- Instruction Width: 32-bit instructions
- Endianness: Little-endian
@ Data processing
mov r0, #10 @ Move immediate
add r0, r0, r1 @ Add registers
sub r2, r0, r1 @ Subtract
@ Conditional execution
cmp r0, #9 @ Compare
addgt r0, #10 @ Add if greater than
movls r0, #10 @ Move if lower or same
@ Memory operations
ldr r0, [sp, #4] @ Load from stack
str r1, [r0, #8] @ Store to memory
push {r0-r3, lr} @ Push multiple registers
pop {r0-r3, pc} @ Pop and return
@ Shifts and rotates
mov r1, r1, lsl #2 @ Logical shift left
add r0, r0, r1, lsr #1 @ Add with shifted operand
@ Branches
b label @ Unconditional branch
bl function @ Branch with link (call)
bx lr @ Branch and exchange (return)
@ PC-relative addressing
ldr r0, =constant @ Load constant (pseudo-instruction)
adr r0, label @ Load address of label.ascii "text" @ String data (no null terminator)
.asciz "text" @ Null-terminated string
.byte 0x42 @ 8-bit data
.short 0x1234 @ 16-bit data
.long 0x12345678 @ 32-bit data
.space 100 @ Reserve bytes
.globl symbol @ Global symbol export
.set name, value @ Define constant
.p2align 2 @ Align to power of 2
.section __TEXT,__text @ Section directive
@ Apple/Mach-O specific
.subsections_via_symbols
.build_version
.indirect_symbolData Processing:
and, eor, sub, rsb, add, adc, sbc, rsc, tst, teq, cmp, cmn, orr, mov, bic, mvn
Multiply:
mul, mla, umull, umlal, smull, smlal
Memory Access:
ldr, ldrb, ldrh, ldrsb, ldrsh, str, strb, strh, ldm, stm
(Note: Byte/halfword/signed variants are parsed as suffixes to LDR/STR base instructions)
Branch:
b, bl, bx
Shift Instructions:
lsl, lsr, asr, ror (converted internally to MOV with shift operands)
Condition Codes:
eq, ne, cs/hs, cc/lo, mi, pl, vs, vc, hi, ls, ge, lt, gt, le, al
To generate ARMv7 assembly compatible with this assembler using Clang:
# Compile C to ARMv7 assembly
clang -S -arch armv7 -target armv7-apple-darwin -O2 input.c -o output.s
# For iOS devices (32-bit)
clang -S -arch armv7 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) \
-miphoneos-version-min=10.0 -O2 input.c -o output.s# Using arm-none-eabi-gcc
arm-none-eabi-gcc -S -march=armv7-a -marm -O2 input.c -o output.s
# Using arm-linux-gnueabi-gcc
arm-linux-gnueabi-gcc -S -march=armv7-a -marm -O2 input.c -o output.sNote: Assembly generated by GCC may need minor syntax adjustments:
- GCC uses
@for comments; this assembler supports both@and; - Some GCC-specific directives may need to be removed or adapted
- Apple/Mach-O specific directives (like
.subsections_via_symbols) are optional
In Xcode, configure your target with:
Build Settings:
- Architecture: armv7
- Valid Architectures: armv7
- Deployment Target: iOS 10.0 or earlier (32-bit support)
To generate assembly:
1. Select Product > Perform Action > Assemble [filename].c
2. Or use: clang -S -arch armv7 [source.c] -o [output.s]
The project includes a Makefile for easy compilation on Linux and Mac:
# Build the compiler and VM (creates armvm-compiler executable)
make
# Clean build artifacts
make clean
# Build and run tests
make testxcodebuild -project armvm.xcodeproj -scheme armvmYou can also build directly with GCC or Clang:
# Using GCC
cd armvm
gcc -o armvm-compiler armvm.c compiler.c armcomp.c expr.c memory.c libpvm.c -lm
# Or with Clang
clang -o armvm-compiler armvm.c compiler.c armcomp.c expr.c memory.c libpvm.c -lmThe armvm-compiler executable is an assembler that compiles ARM assembly files to bytecode:
# Compile assembly to bytecode
./armvm-compiler -o output.bin input.s
# Compile multiple files
./armvm-compiler -o program.bin file1.s file2.s file3.sOutput Format:
.binfile: Contains compiled bytecode with header_dfile: Debug symbols (e.g.,output.bin_d)
The compiled bytecode includes:
- Program header with magic number (0x4143524F / "ORCA" in little-endian)
- Executable ARM32 instructions
- Symbol table with global labels and their positions
The preferred way to embed the ARM VM is through avm.h, whose interface is
modelled after the Lua C API.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "avm.h"
/* ---- Host functions (avm_CFunction signature) ---- */
/* strlen(const char *s) -> int β r0 = pointer to string in VM memory */
static int host_strlen(avm_State *S) {
avm_pushinteger(S, (int)strlen(avm_tostring(S, 1)));
return 1; /* one return value placed in r0 */
}
/* puts(const char *s) β prints and returns void */
static int host_puts(avm_State *S) {
puts(avm_tostring(S, 1));
return 0;
}
int main(void) {
/* 1. Create state with 64 KB stack and 64 KB heap */
avm_State *S = avm_newstate(64 * 1024, 64 * 1024);
/* 2. Register host functions β must happen BEFORE avm_loadbuffer()
* so the assembler can resolve "bl _strlen" / "bl _puts" */
avm_register(S, "strlen", host_strlen);
avm_register(S, "puts", host_puts);
/* 3. Compile and load ARM assembly source */
const char *src =
".globl _main\n"
"_main:\n"
" bl _puts\n" /* prints the string whose address is in r0 */
" bx lr\n";
if (avm_loadbuffer(S, src, strlen(src)) != 0) {
fprintf(stderr, "compilation failed\n");
avm_close(S);
return 1;
}
/* 4. Set up arguments: r0 = VM-relative offset of the string "Hello!\n" */
/* (normally you'd embed the string in the assembly; this just shows */
/* that r0 is read with avm_tointeger and written with avm_pushinteger) */
/* 5. Execute from the _main entry point */
avm_call(S, S->entry_point);
/* 6. Read return value from r0 (register index 1) */
int result = avm_tointeger(S, 1);
/* 7. Destroy state */
avm_close(S);
return result;
}| Function | Description |
|---|---|
avm_newstate(stack, heap) |
Allocate a new VM state |
avm_close(S) |
Destroy state and free memory |
avm_register(S, name, fn) |
Bind a C function to an assembly symbol |
avm_loadbuffer(S, src, len) |
Compile & load ARM assembly source |
avm_call(S, pc) |
Execute loaded code from given PC |
avm_tointeger(S, idx) |
Read register idx (1=r0) as int |
avm_touinteger(S, idx) |
β |
avm_tonumber(S, idx) |
Read register idx as float |
avm_tostring(S, idx) |
Register value β pointer into VM memory |
avm_toboolean(S, idx) |
Non-zero register β true |
avm_pushinteger(S, n) |
Write int return value to r0 |
avm_pushnumber(S, n) |
Write float return value to r0 |
avm_pushboolean(S, b) |
Write boolean (0/1) to r0 |
Register indices in avm_to* / avm_push* are 1-indexed: index 1 maps to
r0, index 2 maps to r1, etc., matching the ARM calling convention where r0
holds the first argument and the return value.
After avm_loadbuffer() succeeds, S->entry_point contains the byte offset
of the _main label, ready to pass to avm_call().
The lower-level vm_create / vm_shutdown / execute interface is still
available for applications that need more control.
#include "vm.h"
// Implement syscall handler
static DWORD my_syscall(LPVM vm, DWORD call_id) {
switch (call_id) {
case 1: return (DWORD)strlen((char *)(vm->memory + vm->r[0]));
case 2: return (DWORD)puts((char *)(vm->memory + vm->r[0]));
default: return 0;
}
}
int main(void) {
// Register external function names (index must match case number above)
strcpy(symbols[1], "strlen");
strcpy(symbols[2], "puts");
// Compile assembly source into a temporary file
FILE *fp = tmpfile();
compile_buffer(fp, NULL, NULL, source, &apple_asm_syntax);
// Read compiled bytecode
fseek(fp, 0, SEEK_END);
DWORD size = (DWORD)ftell(fp);
BYTE *program = malloc(size);
fseek(fp, 0, SEEK_SET);
fread(program, size, 1, fp);
fclose(fp);
// Create VM and execute
LPVM vm = vm_create(my_syscall, 64*1024, 64*1024, program, size);
execute(vm, (DWORD)main_label);
DWORD result = vm->r[0];
vm_shutdown(vm);
free(program);
return (int)result;
}- Registers: 16 general-purpose registers (r0-r15)
- r13: Stack Pointer (SP)
- r14: Link Register (LR)
- r15: Program Counter (PC)
- CPSR Flags: N (Negative), Z (Zero), C (Carry), V (Overflow)
- Memory Model: Separate stack and heap with configurable sizes
- Instruction Format: 32-bit ARM instructions (not Thumb)
- Default Sizes: 64KB stack, 64KB heap
The project includes a C-based test suite in the test/ directory:
# Run tests using Make (recommended)
make test
# Run tests with Xcode (original Objective-C tests in armtest/)
xcodebuild test -project armvm.xcodeproj -scheme armtest
# Tests include:
# - Basic instructions (MOV, ADD, SUB, etc.)
# - Memory operations (LDR, STR, PUSH, POP)
# - Conditional execution
# - Shifts and rotates
# - Branch instructions
# - Complex programs (linked lists, string operations, etc.)The C test suite runs 11 core tests covering essential ARM VM functionality.
Run untrusted user-supplied code β game mods, content scripts, user plugins β in a hard-isolated environment, similar to the Quake III Arena VM. The guest has no direct access to host memory or system calls beyond the narrow set of host functions you register, so a buggy or malicious plugin cannot corrupt or escape the host process.
Execute 32-bit ARMv7 logic β old iOS app libraries, embedded firmware, legacy mobile SDKs β on any modern host (x86-64 servers, Apple Silicon Macs, RISC-V boards) without a full OS emulator. Think of it like a lightweight container for ARM32 binaries: bring the code, not the hardware.
Embed armvm as a scripting layer inside a C/C++ application. Authors write behaviour in ARM assembly (or compile C to ARM32 with Clang), register only the host functions they want to expose, and call into the VM at runtime β a fully deterministic, zero-dependency alternative to Lua or Wren for latency-sensitive applications.
Provide a safe, self-contained environment for learning ARMv7 assembly. Students can write, compile, and step through real ARM instructions on any laptop without needing physical hardware or a full QEMU image.
Compile embedded firmware written in C to ARM32 assembly, load it into armvm, stub out peripheral registers as host functions, and run automated tests on a CI server β no evaluation board required.
Ship an .bin bytecode blob that runs identically on every supported host. The compact "ORCA" binary format is self-describing and deterministic, making it straightforward to checksum, cache, or version alongside application assets.
Fuzz ARM binary code paths in a fully-isolated, in-process sandbox. The VM's configurable memory sizes and register-level introspection make it easy to inject inputs, catch crashes, and inspect state without spinning up a separate OS process.
UI-Managed VM Environments (paired with orion-ui)
Connect armvm instances to orion-ui to get a dedicated graphical interface for creating, running, inspecting, and debugging VMs. A visual register/memory inspector, step-by-step execution, and multi-VM management panel would make armvm suitable for teaching environments, game modding toolchains, and embedded development workflows.
The items below would expand armvm's utility across the use cases above.
- Thumb / Thumb-2 instruction set β decode and execute the 16/32-bit Thumb encoding so that GCC and modern Clang outputs work without
-marmflags - VFP floating-point β basic single/double-precision arithmetic to support numerical code and C
float/doublevalues - Snapshot & restore β serialize full VM state (registers + memory) to a buffer so execution can be paused, migrated, or replayed
- Step / breakpoint API β expose
avm_step()andavm_breakpoint()in the high-level API to support debuggers and orion-ui integration
- Multiple concurrent VM instances β thread-safe state so several VMs can run in parallel within the same host process
- Inter-VM messaging β lightweight channels so cooperating VMs can exchange data without going through the host
- Filesystem & network sandboxing helpers β optional host-function packs that give the VM controlled access to files and sockets, similar to WASI
- orion-ui integration β a first-party adapter that exposes armvm's API to orion-ui for graphical VM management, register inspection, and live disassembly
- AArch64 (ARM64) guest support β execute 64-bit ARM code in the same embeddable VM model
- JIT compilation back-end β translate hot bytecode paths to native host instructions for performance-critical workloads
- WebAssembly target β compile the VM itself to WASM so ARM32 code can run in a browser with no native install
- Profiling & tracing API β instruction-level counters and call-graph tracing for performance analysis of guest code
This project is licensed under the GNU General Public License v3.0. See the LICENSE file for complete details.