| name | make-doom-for-mips |
| description | Guidance for cross-compiling complex C programs (like Doom) to run on custom MIPS virtual machines or emulators. This skill should be used when tasked with building software for non-standard MIPS targets that use custom syscall interfaces, require freestanding compilation (-nostdlib), or involve running binaries in JavaScript/custom VM environments. |
Cross-Compiling Complex C Programs for Custom MIPS VMs
Overview
This skill provides guidance for cross-compiling complex C codebases (such as Doom) to run on custom MIPS virtual machines or emulators. The primary challenge is bridging the gap between standard C library expectations and a custom runtime environment with its own syscall interface, memory layout, and instruction support.
Critical First Steps: Understanding the Target Environment
Before writing any code, thoroughly analyze the target VM to understand its requirements:
1. Analyze the VM's Syscall Interface
Read the VM source code to extract the exact syscall interface:
- Identify syscall numbers (e.g., SYS_read=0, SYS_write=1, etc.)
- Understand the calling convention (which registers hold arguments and return values)
- Document all supported syscalls and their signatures
- Note any custom syscalls specific to the VM
2. Determine Endianness and ABI
Verify the target architecture details by examining the VM source:
- Endianness (little-endian vs big-endian MIPS)
- Floating-point support (hardware vs soft-float)
- Word size (32-bit vs 64-bit)
- ABI variant (o32, n32, n64)
3. Understand Memory Layout
From the VM source, extract:
- Expected entry point address
- Stack location and size
- Heap boundaries
- Text/data segment placement
- Any memory-mapped I/O regions
4. Identify Supported Instructions
Check for instruction limitations:
- Floating-point instruction support
- Privileged instructions
- Branch delay slot handling
- Any unimplemented instructions that will cause VM termination
Systematic Approach to Freestanding Compilation
Phase 1: Comprehensive Header Analysis
Before compiling, scan all source files to identify required headers:
# Find all included headers
grep -rh '#include' src/ | sort -u
Create stub headers for ALL identified system headers upfront rather than reactively fixing one error at a time. Common headers needed for complex C programs:
<stdio.h>,<stdlib.h>,<string.h>,<stdint.h><limits.h>,<stddef.h>,<stdarg.h><ctype.h>,<math.h>,<time.h><errno.h>,<assert.h>,<fcntl.h><unistd.h>,<sys/types.h>,<sys/stat.h>
Phase 2: Standard Library Implementation
Create a comprehensive stdlib replacement with COMPLETE implementations:
Critical functions that must work correctly (not return 0 or stub):
printf,fprintf,sprintf,snprintf- Used for debug output and parsingsscanf,fscanf- Used for configuration parsingmalloc,free,realloc- Memory managementmemcpy,memset,memmove,strlen,strcmp,strcpy- String/memory operationsatoi,atol,strtol- Number parsingfopen,fread,fwrite,fclose,fseek,ftell- File I/O
Warning signs of incomplete implementations:
- Functions returning 0 unconditionally
- Empty function bodies
- "TODO" or stub comments
- Missing format specifier handling in printf-family functions
Phase 3: Linker Script Configuration
Create a linker script that matches the VM's expectations:
- Entry point symbol must match what the VM looks for
- Section addresses must align with VM memory layout
- Stack initialization must be included in startup code
- Verify
.text,.data,.bss,.rodataplacement
Verification Strategies
1. Syscall Verification
After implementing syscalls, verify each one works:
// Test write syscall
const char *msg = "Syscall test\n";
write(1, msg, strlen(msg));
// Test read syscall
char buf[100];
int n = read(0, buf, sizeof(buf));
// Test file operations
int fd = open("/path/to/file", O_RDONLY);
// etc.
2. Entry Point Verification
Confirm the entry point matches:
# Check the ELF entry point
mips-*-readelf -h output.elf | grep Entry
# Verify symbol is at expected address
mips-*-nm output.elf | grep __start
3. Map File Analysis for Debugging
Generate and use map files to debug crashes:
# Generate map file during linking
-Wl,-Map=output.map
# When program terminates at specific PC, find the function:
grep "0x40b7b0" output.map
4. Incremental Testing
Test components in isolation before full integration:
- Minimal "hello world" to verify basic execution
- Memory allocation test program
- File I/O test program
- Individual library function tests
- Finally, the full application
Common Pitfalls and Solutions
Pitfall 1: ABI Mismatch Warnings
Problem: Linking soft-float compiled code with hard-float libgcc produces warnings:
warning: linking soft-float module with hard-float module
Impact: Can cause runtime crashes when floating-point values are passed/returned.
Solution: Ensure consistent ABI across all compilation units:
-msoft-float -mfloat-abi=soft
Or provide soft-float libgcc, or implement soft-float routines.
Pitfall 2: Incomplete printf/scanf Implementation
Problem: Returning 0 or doing nothing in printf-family functions.
Impact: Configuration parsing fails silently; debug output disappears.
Solution: Implement at least these format specifiers:
%d,%i,%u,%x,%X- integers%s- strings%c- characters%p- pointers- Width and precision modifiers
Pitfall 3: Missing Function Implementations
Problem: Compiler warnings about implicit function declarations.
Impact: Undefined behavior at runtime; usually crashes.
Solution: Treat ALL implicit declaration warnings as errors:
-Werror=implicit-function-declaration
Pitfall 4: Incorrect Syscall Numbers
Problem: Using standard Linux syscall numbers when VM uses different numbers.
Impact: Wrong operations executed; silent corruption or crashes.
Solution: Extract exact syscall numbers from VM source before implementation.
Pitfall 5: Premature Termination Without Investigation
Problem: Program terminates early; assuming it's due to missing data files.
Impact: Real bugs (unimplemented instructions, invalid memory access, failed syscalls) go undiagnosed.
Solution: Always investigate termination:
- Check the PC address in the map file
- Examine the VM's termination handling code
- Add instrumentation to log syscalls and critical operations
- Verify data files are accessible at expected paths
Pitfall 6: Reactive Header Creation
Problem: Creating stub headers one-by-one as compilation errors appear.
Impact: Slow iteration; easy to miss dependencies.
Solution: Analyze all includes upfront and create comprehensive stubs before first compile attempt.
Debugging Strategies
When the Binary Terminates Early
- Get the termination address from VM output
- Look up address in map file to identify the function
- Check for unimplemented instructions at that location
- Verify syscall was valid if termination is in syscall handler
- Check memory access if termination is in memory operation
When Output is Missing/Incorrect
- Verify write syscall reaches the correct output
- Check file descriptors are mapped correctly
- Trace printf calls to ensure they reach write syscall
- Verify buffer handling in I/O functions
When the Program Crashes Silently
- Add diagnostic output at key checkpoints
- Implement signal handlers if VM supports them
- Check stack overflow - increase stack size
- Verify heap operations - add malloc/free tracing
Compilation Flags Reference
Essential flags for freestanding MIPS compilation:
-nostdlib # No standard library
-ffreestanding # Freestanding environment
-mips32 # MIPS32 instruction set (or appropriate variant)
-EL # Little-endian (or -EB for big-endian)
-msoft-float # Software floating-point
-mno-abicalls # No PIC/GOT (if VM doesn't support)
-fno-pic # No position-independent code
-static # Static linking
-Wl,-Map=output.map # Generate map file for debugging