โ C17 MOC ยท ๐ฏ Mission ยท ๐ Glossary ยท ๐ Resources
Lesson 0002: Safe Binary File I/O & ROM Loading into Memory at 0x200
[!NOTE] Challenge Goal By the end of this challenge, you will build and verify: A robust, memory-safe binary ROM file loader for your CHIP-8 emulator that validates file presence, ensures ROM size fits within RAM boundaries (max 3,584 bytes), and loads bytecode starting at address
0x200using standard C17 file streams.
1. Challenge & Background
In CHIP-8 Architecture, the virtual machine addresses 4,096 bytes (4 KB) of RAM (0x000 to 0xFFF). Memory from 0x000 to 0x1FF (512 bytes) is reserved for the built-in font set (0x000-0x050) and legacy interpreter memory. All CHIP-8 executable ROMs must be loaded starting at address 0x200 (512 decimal), and the Program Counter (PC) begins execution at 0x200.
Reading binary executables in C differs fundamentally from high-level environments like Node.js (fs.readFileSync). In C17, you operate raw file stream handles (FILE*) directly against physical memory buffers. An oversized ROM or unverified file read can cause buffer overflow and trigger Undefined Behavior (UB). Following Defensive Programming and Standard I/O Conventions, we must validate all boundaries before reading.
[!TIP] Mental Model Treat the ROM file as an untrusted stream of raw bytes. Verify stream presence with
fopen("rb"), inspect byte size viafseek(SEEK_END)+ftell(), enforce the strict ceilingfile_size <= 3584,rewind(), and transfer intomemory[0x200]withfread().
2. Architecture & Memory Layout
[!WARNING] Critical Invariants
- Binary Mode Only (
"rb"): Never open in text mode ("r"), which converts newline bytes and silently corrupts opcodes.- Max ROM Limit (3,584 Bytes): Total RAM is 4,096 bytes.
4096 - 0x200 (512) = 3584bytes. Any byte beyond 3,584 overflows RAM.- Stream Cleanup: Every opened
FILE*must be closed withfclose()across both success and early-return error paths.
3. Step Zero: Environment & Test Fixture Setup
Generate test binary fixtures with varying sizes (valid, empty, and oversized) in your repo to verify each validation gate:
cd /Users/morfes/projects/chip-8-emu/c
mkdir -p tests/fixtures
# 1. Create a valid dummy ROM (16 bytes)
printf '\x12\x00\x60\x00\x61\x00\x70\x01\xA2\x50\xD0\x15\x12\x00\x00\x00' > tests/fixtures/valid.ch8
# 2. Create an empty ROM (0 bytes)
touch tests/fixtures/empty.ch8
# 3. Create an oversized ROM (4000 bytes - exceeds 3584 bytes limit)
dd if=/dev/zero of=tests/fixtures/oversized.ch8 bs=1 count=4000 2>/dev/null
4. Challenge Steps
Step One: Define Memory & ROM Boundaries in Configuration
Goal: Ensure your configuration defines the memory architecture constants:
CHIP8_MEMORY_SIZE= 4096CHIP8_PROGRAM_LOAD_ADDRESS=0x200(512)CHIP8_MAX_ROM_SIZE= (CHIP8_MEMORY_SIZE-CHIP8_PROGRAM_LOAD_ADDRESS) = 3584
Declare the public loader function prototype in include/chip8.h:
bool chip8_load_rom(Chip8 *chip8, const char *filepath);
Step Two: Implement Stream Validation & Size Invariant Checks
Goal: In src/chip8.c, implement the opening and size validation logic:
- Return
falseimmediately ifchip8orfilepathisNULL. - Open the file in
"rb"binary mode. HandleNULLstream errors defensively. - Seek to
SEEK_ENDand useftellto calculate file size. - Enforce that
file_size > 0(reject empty files) andfile_size <= CHIP8_MAX_ROM_SIZE(reject oversized ROMs). Remember tofclose()before returningfalseon rejection. - Rewind the stream to the beginning using
rewind(file).
Step Three: Stream Bytecode into RAM at 0x200
Goal: Read the bytes directly into the memory buffer starting at index 0x200:
- Use
freadwith element sizesizeof(uint8_t)and member countfile_size(consult fread Reference). - Target buffer:
&chip8->memory.memory[CHIP8_PROGRAM_LOAD_ADDRESS]. - Verify that
freadreturns exactlyfile_size. - Close the file with
fclose(). - Log a success message reporting the file path, byte count, and load address
0x200.
Step Four: Connect CLI Entry Point in src/main.c
Goal: Update src/main.c to accept the ROM path from argv[1]:
- If
argc < 2, print usage instructions tostderrand exit with status1. - Call
chip8_load_rom(&chip8, argv[1]). If loading fails, exit with status1.
How to Test:
cmake --build build
./build/chip8
Expected Output:
Usage: ./build/chip8 <path-to-rom>
The Final Step: Run Against Fixtures & AddressSanitizer Verification
Goal: Verify all defensive checks against valid, empty, missing, and oversized files.
How to Test - Valid ROM:
./build/chip8 tests/fixtures/valid.ch8
Expected Output:
Successfully loaded ROM 'tests/fixtures/valid.ch8' (16 bytes) at 0x200
How to Test - Missing File:
./build/chip8 nonexistent.ch8
Expected Output:
Error: could not open ROM file 'nonexistent.ch8'
How to Test - Oversized File:
./build/chip8 tests/fixtures/oversized.ch8
Expected Output:
Error: ROM size (4000 bytes) exceeds maximum allowable RAM (3584 bytes).
5. Verification Checklist
- Step Zero: Test fixtures (
valid.ch8,empty.ch8,oversized.ch8) created - Step One: Memory constants and
chip8_load_romprototype declared - Step Two: Stream validation,
SEEK_ENDsize checks, andfclosesafety paths implemented - Step Three:
freadinto&memory[0x200]with complete element verification implemented - Step Four:
main.cCLI argument parsing verified - Final Step: All 4 test cases passed with 0 AddressSanitizer warnings
6. Primary Sources & Reference Material
- SEI CERT C Coding Standard: FIO19-C โ Secure file size validation and buffer limits.
- Vault Reference: Standard I/O & Binary File Streams โ C17 return conventions and
freadmechanics. - Vault Reference: CHIP-8 Memory Architecture โ Memory layout and fontset space.
๐ฌ Next Steps
Once your ROM loader is verified against your test fixtures, declare completion! Next, we will tackle Lesson 0003: 16-Bit Big-Endian Opcode Fetching & Bitwise Masking!
