diff --git a/examples/complex-app/.gitignore b/examples/complex-app/.gitignore new file mode 100644 index 0000000..abcfcd9 --- /dev/null +++ b/examples/complex-app/.gitignore @@ -0,0 +1,2 @@ +complex-app +build/ diff --git a/examples/complex-app/Makefile b/examples/complex-app/Makefile new file mode 100644 index 0000000..a565064 --- /dev/null +++ b/examples/complex-app/Makefile @@ -0,0 +1,63 @@ +# Makefile for the complex-app example +# +# On macOS with Xcode command-line tools, 'make all' also regenerates +# complex.s from input.c using clang's ARMv7 cross-compiler. On Linux +# (or any host without clang -arch armv7), 'make app' builds and runs +# the pre-generated complex.s that is already checked into the repository. + +CC = gcc +CFLAGS = -Wall -Wextra -O2 +LDFLAGS = -lm + +ARMVM_DIR = ../../armvm +OBJDIR = build + +# armvm library sources (everything except compiler.c, which has its own main) +ARMVM_SRCS = \ + $(ARMVM_DIR)/armvm.c \ + $(ARMVM_DIR)/armcomp.c \ + $(ARMVM_DIR)/expr.c \ + $(ARMVM_DIR)/memory.c \ + $(ARMVM_DIR)/libpvm.c + +# compiler.c is compiled in isolation with -Dmain=_unused_main so that +# compile_buffer() and avm_loadbuffer() are available to link against +# without conflicting with our own main(). +COMPILER_SRC = $(ARMVM_DIR)/compiler.c +COMPILER_OBJ = $(OBJDIR)/compiler.o + +TARGET = complex-app +ASM_OUTPUT = complex.s + +.PHONY: all app run clean + +# 'all' attempts to regenerate the assembly from input.c (macOS only). +all: $(TARGET) $(ASM_OUTPUT) + +# 'app' builds the host runner against the pre-generated assembly. +app: $(TARGET) + +$(OBJDIR): + mkdir -p $(OBJDIR) + +# Regenerate ARM assembly from input.c using the Apple clang cross-compiler. +# This target only works on macOS with Xcode command-line tools installed. +$(ASM_OUTPUT): input.c + clang -S -arch armv7 -target armv7-apple-darwin -marm -O1 $< -o $@ + +# Compile compiler.c in isolation so its main() is renamed. +$(COMPILER_OBJ): $(COMPILER_SRC) | $(OBJDIR) + $(CC) $(CFLAGS) -I$(ARMVM_DIR) -Dmain=_unused_main -c $< -o $@ + +# Build the host runner. +$(TARGET): main.c $(COMPILER_OBJ) $(ARMVM_SRCS) + $(CC) $(CFLAGS) -I$(ARMVM_DIR) \ + main.c $(ARMVM_SRCS) $(COMPILER_OBJ) \ + $(LDFLAGS) -o $(TARGET) + +# Build and run using the pre-generated complex.s. +run: app + ./$(TARGET) $(ASM_OUTPUT) + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/examples/complex-app/README.md b/examples/complex-app/README.md new file mode 100644 index 0000000..62beff7 --- /dev/null +++ b/examples/complex-app/README.md @@ -0,0 +1,97 @@ +# complex-app example + +A more complex example showing how to embed **armvm** in a host application +with multiple exported functions, loops, pointer arithmetic, struct-like +field access, bit shifts, and bit masks. + +## What it demonstrates + +### `input.c` / `complex.s` + +Three internal ARM functions plus `main()`, all demonstrating different +language features: + +| Function | Features | +|---|---| +| `compute_flags(value, shift, mask)` | register-controlled bit shift (`lsl r1`), bit-AND mask | +| `sum_array(arr*, count)` | counted loop (`cmp`/`bge`/`b`), pointer walk via scaled register offset (`[r0, r4, lsl #2]`), callee-saved registers | +| `process_record(record*)` | struct-like field access via immediate offsets (`[r0, #4]`, `[r0, #8]`), immediate bit shift (`lsl #8`), bitwise OR | +| `main` | stack-allocated arrays, calling all helpers, PC-relative string addressing | + +### `main.c` + +The host application that: +1. Registers `print_int` and `print_string` as host functions accessible to ARM code +2. Loads and compiles `complex.s` via `avm_loadbuffer()` +3. Executes the program with `avm_call()` +4. Reads and prints the return value + +## Expected output + +``` +Running complex ARM32 program... +compute_flags result: 3840 +sum_array result: 150 +process_record result: 1834 +Program returned: 150 +``` + +### Result breakdown + +| Call | Computation | Result | +|---|---|---| +| `compute_flags(0xFF, 4, 0xF00)` | `(255 << 4) & 3840 = 4080 & 3840` | **3840** | +| `sum_array([10,20,30,40,50], 5)` | `10+20+30+40+50` | **150** | +| `process_record([1, 42, 7])` | `42 \| (7 << 8) = 42 \| 1792` | **1834** | + +## Build and run + +```bash +# from the repository root — build armvm first (if not already done) +make + +# build the host runner and run with the pre-generated assembly +make -C examples/complex-app run +``` + +On macOS with Xcode command-line tools you can also regenerate `complex.s` +from `input.c`: + +```bash +# regenerate assembly and run +make -C examples/complex-app all run +``` + +## Assembly highlights + +### Bit shift and mask (`_compute_flags`) + +```asm +_compute_flags: + mov r0, r0, lsl r1 @ r0 = value << shift (register-controlled) + and r0, r0, r2 @ r0 = (value << shift) & mask + bx lr +``` + +### Loop with pointer walk (`_sum_array`) + +```asm +_sum_loop: + cmp r4, r1 @ if i >= count, exit + bge _sum_done + ldr r2, [r0, r4, lsl #2] @ r2 = arr[i] (base + i*4) + add r5, r5, r2 + add r4, r4, #1 @ i++ + b _sum_loop +``` + +### Struct-like field access (`_process_record`) + +```asm +_process_record: + ldr r1, [r0, #4] @ value = record[1] + ldr r2, [r0, #8] @ flags = record[2] + mov r2, r2, lsl #8 @ flags << 8 + orr r0, r1, r2 @ value | (flags << 8) + bx lr +``` diff --git a/examples/complex-app/complex.s b/examples/complex-app/complex.s new file mode 100644 index 0000000..5653531 --- /dev/null +++ b/examples/complex-app/complex.s @@ -0,0 +1,194 @@ +@ complex.s — Hand-written ARMv7 assembly for the complex-app example. +@ +@ Corresponds to input.c; generated manually because cross-compilation to +@ ARMv7 is only available on macOS. The syntax follows Apple/ARM UAL as +@ consumed by armvm's assembler (armcomp.c / compiler.c). +@ +@ Functions exported to the host via avm_register(): +@ print_int(int n) — prints an integer followed by a newline +@ print_string(const char*) — prints a NUL-terminated string +@ +@ Internal functions (resolved by the assembler's linker pass): +@ _compute_flags(value, shift, mask) -> (value << shift) & mask +@ _sum_array(arr*, count) -> sum of arr[0..count-1] +@ _process_record(record*) -> record[1] | (record[2] << 8) +@ +@ ARM calling convention (AAPCS): +@ Arguments : r0-r3 (first four integer args) +@ Return value: r0 +@ Callee-saved: r4-r11, sp, lr (push/pop in prologue/epilogue) +@ Caller-saved: r0-r3, r12 + + .section __TEXT,__text,regular,pure_instructions + .globl _main + .globl _compute_flags + .globl _sum_array + .globl _process_record + +@ ----------------------------------------------------------------------- +@ compute_flags(int value, int shift, int mask) -> int +@ +@ r0 = value, r1 = shift, r2 = mask +@ Returns (value << shift) & mask. +@ +@ Demonstrates: register-controlled bit shift (lsl r1), bit-AND mask. +@ ----------------------------------------------------------------------- +_compute_flags: + mov r0, r0, lsl r1 @ r0 = value << shift + and r0, r0, r2 @ r0 = (value << shift) & mask + bx lr + +@ ----------------------------------------------------------------------- +@ sum_array(int *arr, int count) -> int +@ +@ r0 = pointer to int array (in VM memory) +@ r1 = number of elements +@ Returns sum of arr[0] + arr[1] + ... + arr[count-1]. +@ +@ Demonstrates: loop (cmp/bge/b), pointer arithmetic via scaled register +@ offset ([r0, r4, lsl #2] = arr + i*4), callee-saved registers. +@ ----------------------------------------------------------------------- +_sum_array: + push { r4, r5, lr } + mov r4, #0 @ i = 0 + mov r5, #0 @ total = 0 +_sum_loop: + cmp r4, r1 @ if i >= count, exit + bge _sum_done + ldr r2, [r0, r4, lsl #2] @ r2 = arr[i] (arr + i*4) + add r5, r5, r2 @ total += arr[i] + add r4, r4, #1 @ i++ + b _sum_loop +_sum_done: + mov r0, r5 @ return total + pop { r4, r5, pc } + +@ ----------------------------------------------------------------------- +@ process_record(int *record) -> int +@ +@ r0 = pointer to int[3]: { id, value, flags } +@ Returns record[1] | (record[2] << 8). +@ +@ Demonstrates: struct-like field access via immediate offsets, +@ immediate bit shift (lsl #8), bitwise OR. +@ ----------------------------------------------------------------------- +_process_record: + ldr r1, [r0, #4] @ r1 = record[1] (value) + ldr r2, [r0, #8] @ r2 = record[2] (flags) + mov r2, r2, lsl #8 @ r2 = flags << 8 + orr r0, r1, r2 @ r0 = value | (flags << 8) + bx lr + +@ ----------------------------------------------------------------------- +@ main() — entry point +@ +@ Stack frame layout (after push + sub): +@ [sp + 0 .. +16] arr[5] (5 × int = 20 bytes) +@ [sp + 20 .. +28] record[3] (3 × int = 12 bytes) +@ — 32 bytes total — +@ +@ Registers used: +@ r4 = compute_flags result +@ r5 = sum_array result (also the return value) +@ r6 = process_record result +@ +@ Returns: sum_array result (150). +@ ----------------------------------------------------------------------- +_main: + push { r4, r5, r6, lr } + sub sp, sp, #32 @ allocate 32 bytes for locals + + @ ---- 1. compute_flags(0xFF, 4, 0xF00) ---- + @ (0xFF << 4) & 0xF00 = 0xFF0 & 0xF00 = 0xF00 = 3840 + mov r0, #255 @ value = 0xFF + mov r1, #4 @ shift = 4 + mov r2, #3840 @ mask = 0xF00 + bl _compute_flags + mov r4, r0 @ save flags result (3840) + + ldr r0, LCPI0_0 +LPC0_0: + add r0, pc, r0 @ r0 = address of string "compute_flags result: " + bl _print_string + + mov r0, r4 + bl _print_int + + @ ---- 2. sum_array({10,20,30,40,50}, 5) ---- + @ Build arr[5] on the stack at [sp, #0..#16] + mov r0, #10 + str r0, [sp, #0] + mov r0, #20 + str r0, [sp, #4] + mov r0, #30 + str r0, [sp, #8] + mov r0, #40 + str r0, [sp, #12] + mov r0, #50 + str r0, [sp, #16] + + mov r0, sp @ r0 = &arr[0] + mov r1, #5 @ count = 5 + bl _sum_array + mov r5, r0 @ save total (150) + + ldr r0, LCPI0_1 +LPC0_1: + add r0, pc, r0 @ r0 = address of string "sum_array result: " + bl _print_string + + mov r0, r5 + bl _print_int + + @ ---- 3. process_record({1, 42, 7}) ---- + @ Build record[3] on the stack at [sp, #20..#28] + @ record = { id=1, value=42, flags=7 } + @ result = 42 | (7 << 8) = 42 | 1792 = 1834 + mov r0, #1 + str r0, [sp, #20] + mov r0, #42 + str r0, [sp, #24] + mov r0, #7 + str r0, [sp, #28] + + add r0, sp, #20 @ r0 = &record[0] + bl _process_record + mov r6, r0 @ save processed (1834) + + ldr r0, LCPI0_2 +LPC0_2: + add r0, pc, r0 @ r0 = address of string "process_record result: " + bl _print_string + + mov r0, r6 + bl _print_int + + @ ---- return total (150) ---- + mov r0, r5 + add sp, sp, #32 + pop { r4, r5, r6, pc } + +@ ----------------------------------------------------------------------- +@ PC-relative string address constants +@ +@ Each LCPI0_x holds (label_address - (LPC0_x + 8)), so that at runtime: +@ ldr r0, LCPI0_x -> r0 = offset +@ add r0, pc, r0 -> r0 = absolute address in VM memory +@ +@ The PC value used in the add is the address of LPC0_x + 8 because the +@ ARM PC is always 2 instructions (8 bytes) ahead. +@ ----------------------------------------------------------------------- + .p2align 2 +LCPI0_0: + .long L_str0-(LPC0_0+8) +LCPI0_1: + .long L_str1-(LPC0_1+8) +LCPI0_2: + .long L_str2-(LPC0_2+8) + +L_str0: + .asciz "compute_flags result: " +L_str1: + .asciz "sum_array result: " +L_str2: + .asciz "process_record result: " diff --git a/examples/complex-app/input.c b/examples/complex-app/input.c new file mode 100644 index 0000000..54d0dc6 --- /dev/null +++ b/examples/complex-app/input.c @@ -0,0 +1,87 @@ +/* + * input.c - Complex example C program compiled to ARMv7 assembly for use + * with armvm. + * + * This file demonstrates several language features: + * - Multiple exported functions + * - Loops + * - Function calls + * - Pointer operations + * - Struct-like field access (int array acting as a struct) + * - Bit shifts + * - Bit masks + * + * To regenerate the assembly on macOS: + * clang -S -arch armv7 -target armv7-apple-darwin -O1 input.c -o complex.s + * + * Expected output when run via complex-app: + * compute_flags result: 3840 + * sum_array result: 150 + * process_record result: 1834 + * Program returned: 150 + */ + +/* Host functions provided by the host application via the syscall interface */ +extern int print_int(int n); +extern int print_string(const char *s); + +/* + * compute_flags — bit-shift and bit-mask demonstration. + * + * Shifts 'value' left by 'shift' bits, then masks with 'mask'. + * Example: compute_flags(0xFF, 4, 0xF00) = (0xFF << 4) & 0xF00 = 0xF00 = 3840 + */ +static int compute_flags(int value, int shift, int mask) { + return (value << shift) & mask; +} + +/* + * sum_array — loop and pointer demonstration. + * + * Iterates over an integer array and returns the sum of all elements. + */ +static int sum_array(int *arr, int count) { + int total = 0; + for (int i = 0; i < count; i++) { + total += arr[i]; + } + return total; +} + +/* + * process_record — struct-like pointer access demonstration. + * + * Treats a three-element int array as a record: + * record[0] = id (not used in result) + * record[1] = value + * record[2] = flags + * + * Returns value | (flags << 8), combining value and flags via bit ops. + * Example: process_record({1, 42, 7}) = 42 | (7 << 8) = 42 | 1792 = 1834 + */ +static int process_record(int *record) { + int value = record[1]; /* +4 bytes from base */ + int flags = record[2]; /* +8 bytes from base */ + return value | (flags << 8); +} + +int main(void) { + /* 1. Bit shifts and masks */ + int flags = compute_flags(0xFF, 4, 0xF00); /* = 3840 */ + print_string("compute_flags result: "); + print_int(flags); + + /* 2. Loop over a pointer/array */ + int arr[5] = {10, 20, 30, 40, 50}; + int total = sum_array(arr, 5); /* = 150 */ + print_string("sum_array result: "); + print_int(total); + + /* 3. Struct-like pointer access + bit ops */ + int record[3] = {1, 42, 7}; + int processed = process_record(record); /* = 1834 */ + print_string("process_record result: "); + print_int(processed); + + return total; /* 150 */ +} diff --git a/examples/complex-app/main.c b/examples/complex-app/main.c new file mode 100644 index 0000000..6c19b56 --- /dev/null +++ b/examples/complex-app/main.c @@ -0,0 +1,136 @@ +/* + * main.c - Host application for the complex-app example. + * + * Demonstrates embedding the ARM VM in a host application with multiple + * host functions exposed to the ARM guest code. + * + * Host functions registered: + * print_int(int n) — prints an integer followed by a newline + * print_string(const char*) — prints a NUL-terminated string + * + * Expected output: + * Running complex ARM32 program... + * compute_flags result: 3840 + * sum_array result: 150 + * process_record result: 1834 + * Program returned: 150 + * + * Usage: + * ./complex-app complex.s + */ + +#include +#include +#include + +#include "avm.h" + +/* ---------------------------------------------------------------------- */ +/* Host functions exposed to the ARM VM via avm_register() */ +/* ---------------------------------------------------------------------- */ + +/* + * print_int(int n) + * + * Reads the integer from r0 (register index 1) and prints it with a + * trailing newline. + */ +static int host_print_int(avm_State *S) { + int n = avm_tointeger(S, 1); + printf("%d\n", n); + return 0; /* void — no return value */ +} + +/* + * print_string(const char *s) + * + * ARM calling convention: s is passed as a VM-memory offset in r0. + * We validate the offset and verify NUL-termination before printing. + */ +static int host_print_string(avm_State *S) { + DWORD offset = avm_touinteger(S, 1); + DWORD mem_size = S->progsize + S->stacksize + S->heapsize; + if (offset >= mem_size) return 0; /* offset out of range */ + const char *base = (const char *)S->memory + offset; + DWORD max_len = mem_size - offset; + /* Verify the string is NUL-terminated within the addressable range */ + DWORD len = 0; + while (len < max_len && base[len] != '\0') len++; + if (len == max_len) return 0; /* not NUL-terminated */ + if (fwrite(base, 1, len, stdout) != len) return 0; + return 0; /* void — no return value */ +} + +/* ---------------------------------------------------------------------- */ +/* Utility */ +/* ---------------------------------------------------------------------- */ + +static char *read_file(const char *path) { + FILE *f = fopen(path, "r"); + if (!f) { + fprintf(stderr, "Cannot open '%s'\n", path); + return NULL; + } + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + char *buf = malloc((size_t)size + 1); + if (!buf) { fclose(f); return NULL; } + fread(buf, (size_t)size, 1, f); + buf[size] = '\0'; + fclose(f); + return buf; +} + +/* ---------------------------------------------------------------------- */ +/* Main */ +/* ---------------------------------------------------------------------- */ + +int main(int argc, char *argv[]) { + if (argc < 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + char *src = read_file(argv[1]); + if (!src) + return 1; + + if (!strstr(src, "_main:")) { + fprintf(stderr, "error: assembly source does not define a '_main:' label\n"); + free(src); + return 1; + } + + /* 1. Create the VM state (64 KB stack, 64 KB heap). */ + avm_State *S = avm_newstate(VM_STACK_SIZE, VM_HEAP_SIZE); + + /* 2. Register host functions before loading code. + * + * ARM assembly calls these with "bl _print_int" / "bl _print_string". + * The leading underscore is stripped; we register the bare name here. + */ + avm_register(S, "print_int", host_print_int); + avm_register(S, "print_string", host_print_string); + + /* 3. Compile and load the assembly source. */ + if (avm_loadbuffer(S, src, strlen(src)) != 0) { + fprintf(stderr, "Compilation failed\n"); + free(src); + avm_close(S); + return 1; + } + free(src); + + /* 4. Execute from the _main entry point. */ + printf("Running complex ARM32 program...\n"); + avm_call(S, S->entry_point); + + /* 5. Read the integer return value from r0 (register index 1). */ + int result = avm_tointeger(S, 1); + printf("Program returned: %d\n", result); + + /* 6. Clean up. */ + avm_close(S); + return 0; +}