Skip to content

corepunch/armvm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

50 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

armvm

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:

  1. ARM Assembler: Compiles ARM assembly source code into bytecode
  2. ARM VM: Executes ARM32 bytecode in a virtual environment with syscall support

Features

  • 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

Assembly Syntax

The assembler supports ARM UAL (Unified Assembly Language) syntax with Apple/Xcode conventions, specifically targeting ARMv7 32-bit ARM architecture.

Supported Assembly Syntax

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

Instruction Examples

@ 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

Supported Directives

.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_symbol

Supported Instructions

Data 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

Generating Compatible Assembly

Using Clang/LLVM

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 GCC ARM Cross-Compiler

# 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.s

Note: 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

Using Xcode (Original Target)

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]

Building

Using Make (Recommended)

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 test

Using Xcode

xcodebuild -project armvm.xcodeproj -scheme armvm

Manual Compilation

You 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 -lm

Command-Line Usage

The 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.s

Output Format:

  • .bin file: Contains compiled bytecode with header
  • _d file: 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

Programmatic Usage

Lua-like API (recommended)

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;
}

API Reference

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().

Low-level API (backward-compatible)

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;
}

Architecture Details

  • 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

Running Tests

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.

Use Cases

Sandboxed Plugin / Mod Execution

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.

Cross-Architecture Legacy Code

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.

Embedded Scripting Engine

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.

Education & ARM Assembly Learning

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.

Embedded / IoT Firmware Testing

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.

Reproducible Portable Bytecode Distribution

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.

Security Research & Fuzzing

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.


Roadmap

The items below would expand armvm's utility across the use cases above.

Near-term

  • Thumb / Thumb-2 instruction set β€” decode and execute the 16/32-bit Thumb encoding so that GCC and modern Clang outputs work without -marm flags
  • VFP floating-point β€” basic single/double-precision arithmetic to support numerical code and C float/double values
  • 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() and avm_breakpoint() in the high-level API to support debuggers and orion-ui integration

Medium-term

  • 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

Long-term

  • 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

License

This project is licensed under the GNU General Public License v3.0. See the LICENSE file for complete details.

About

armv7 compiler (from asm) and virtual machine project

Resources

License

Stars

6 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors