Skip to content

Instantly share code, notes, and snippets.

@pekkavaa
Last active March 19, 2025 15:12
Show Gist options
  • Save pekkavaa/c97cd39a9690cd4f6b64aa33a49c9859 to your computer and use it in GitHub Desktop.
Save pekkavaa/c97cd39a9690cd4f6b64aa33a49c9859 to your computer and use it in GitHub Desktop.
Ares N64 GDB filesystem interop

This is an example on how to read & write files from N64 roms running in the ares emulator. It uses a GDB script that hooks into native functions that do nothing otherwise.

There's a helper script that starts GDB and passes the given root dir for the Python script.

./connect.sh /tmp/tmp.mCtBkj7vaS

Now you can do file access from your ROM.

License of all code in this gist: CC0.

#!/bin/bash
# Starts the file hook server in the given root directory.
# Usage: connect.sh ROOT_DIR.
set -o nounset
set -o errexit
GDB_FILE_HOOK_DIR=$1 \
gdb-multiarch --batch -ex "dir ../src/viewer" -ex "symbol-file ../src/viewer/build/viewer.elf" -ex "target remote localhost:9123" -x file_hook.py -ex "continue"
uint8_t data[200]{};
uint64_t lm = GDB::fileLastModified("output.txt");
debugf("last modified: %llu\n", lm);
int size = GDB::readFile("output.txt", (int)sizeof(data)-1, data);
if (size < 0) {
debugf("Couldn't read file\n");
} else {
debugf("read %d bytes to %p\n", size, data);
debug_hexdump(data, size);
}
int wrote = GDB::writeFileString("test.txt", "build: " __TIMESTAMP__);
debugf("Wrote %d bytes\n", wrote);
"""
Allows the inferior to access host's file system.
Reads and writes files in the given GDB_FILE_HOOK_DIR env var.
The paths are sanitized against parent traversal to prevent accidents,
but shouldn't be considered secure.
This requres support on the guest side, see gdb.cpp.
"""
import sys
import os
import traceback
import ctypes
import struct
import pathlib
import gdb
tmp_dir = os.environ.get('GDB_FILE_HOOK_DIR', None)
print(f"{tmp_dir=}")
def sanitize_path(path):
"""
Sanitize a path against directory traversals
"""
# From: https://stackoverflow.com/a/66950540
# - pretending to chroot to the current directory
# - cancelling all redundant paths (/.. = /)
# - making the path relative
return os.path.relpath(os.path.normpath(os.path.join("/", path)), "/")
def map_path(path: str):
return os.path.join(tmp_dir, sanitize_path(path))
class BPFileLastModified(gdb.Breakpoint):
def stop (self):
frame = gdb.selected_frame()
path = frame.read_var("path").string()
real_path = map_path(path)
time = gdb.lookup_static_symbol("gdbLastModified").value()
time.assign(0)
try:
last_modified = pathlib.Path(real_path).stat().st_mtime
except OSError as e:
print(e)
return False
print(f"{last_modified=}")
print(f"{time=}")
time.assign(last_modified)
class BPSaveFile(gdb.Breakpoint):
def stop (self):
frame = gdb.selected_frame()
path = frame.read_var("path").string()
data_addr = str(ctypes.c_uint32(frame.read_var("data")).value)
size = int(frame.read_var("dataSizeBytes"))
wrote_bytes = gdb.lookup_static_symbol("gdbWroteBytes").value()
wrote_bytes.assign(-1) # Set a return value of -1 in case open() throws
data = gdb.selected_inferior().read_memory(data_addr, size)
real_path = map_path(path)
print(f"{path=} {data=}")
print(f"{real_path=}")
try:
with open(real_path, "wb") as f:
wrote = f.write(data)
except OSError as e:
print(e)
return False
wrote_bytes.assign(wrote)
print(f"Wrote {len(data)} bytes to {real_path}")
return False
class BPReadFile(gdb.Breakpoint):
def stop_impl(self):
remote = gdb.selected_inferior()
frame = gdb.selected_frame()
path = frame.read_var("path").string()
max_size = int(frame.read_var("maxSize"))
dest = frame.read_var("dest")
dest_addr: str = str(ctypes.c_uint32(dest).value)
read_bytes = gdb.lookup_static_symbol("gdbReadBytes").value()
read_bytes.assign(-1) # Set a return value of -1 in case open() throws
real_path = map_path(path)
try:
with open(real_path, "rb") as f:
raw_data = f.read()
except FileNotFoundError as e:
print(e)
return False
data = raw_data[:max_size]
remote.write_memory(dest_addr, data)
read_bytes.assign(len(data))
print(f"Read {len(data)} bytes from '{path}' to {hex(ctypes.c_uint32(dest).value)}")
return False
def stop(self):
try:
self.stop_impl()
except Exception:
print(traceback.print_exc())
bp_save = BPSaveFile("GDB::writeFile")
bp_save.silent = True
bp_read = BPReadFile("GDB::readFile")
bp_read.silent = True
bp_lm = BPFileLastModified("GDB::fileLastModified")
bp_lm.silent = True
// Emulator host communication.
// The calls here are intercepted by editor/file_hooks.py and values
// are filled in by the debugger.
//
// I've tried to disable optimizations for these functions because they
// look like NOPs to the compiler otherwise.
#include "gdb.hpp"
#include <debug.h>
#include <cstdint>
#include <cstring>
#pragma GCC push_options
#pragma GCC optimize("O0")
static int gdbReadBytes = -1;
static int gdbWroteBytes = -1;
static uint64_t gdbLastModified = 0;
int GDB::readFile(const char* path, int maxSize, uint8_t* dest)
{
debugf("readFile('%s', maxSize=%d, dest=%p)\n", path, maxSize, dest);
return gdbReadBytes;
}
int GDB::writeFile(const char* path, const uint8_t* data, int dataSizeBytes)
{
debugf("writeFileBytes('%s', data=%p, dataSizeBytes=%d)\n", path, data, dataSizeBytes);
return gdbWroteBytes;
}
int GDB::writeFileString(const char* path, const char* data)
{
// Write the string without the NUL terminator
return writeFile(path, (uint8_t*)data, strlen(data));
}
uint64_t GDB::fileLastModified(const char* path)
{
return gdbLastModified;
}
#pragma GCC pop_options
#pragma once
#include <cstdint>
namespace GDB {
// Reads file from host to buffer 'dest'.
// Returns -1 on failure, and the number of read bytes on success.
int readFile(const char* path, int maxSize, uint8_t* dest);
// Writes 'data' to the given file on the host.
// Returns -1 on failure, the number of bytes written otherwise.
int writeFile(const char* path, const uint8_t* data, int dataSizeBytes);
// Writes a string to the given file.
// Returns -1 on failure, the number of bytes written otherwise.
int writeFileString(const char* path, const char* data);
// Returns the last modification time of 'path' in seconds since the Unix epoch.
// Also known as "stat.st_mtime".
// Returns 0 on failure.
uint64_t fileLastModified(const char* path);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment