amiga-bootcamp/05_reversing/case_studies/ramdrive_device.md

5.5 KiB
Raw Permalink Blame History

← Home · Reverse Engineering

Case Study — ramdrive.device Structure Analysis

Overview

ramdrive.device is the Amiga's built-in RAM disk device. It provides a RAM-based disk drive (RAD:) that can survive a warm reboot (Ctrl-Amiga-Amiga). This makes it an excellent target for reverse engineering to understand Resident Modules, Exec Device Architecture, and Memory Survival techniques.

Analysing it teaches:

  • Exec device initialization and the Resident structure.
  • BeginIO dispatch logic.
  • Persistence mechanisms across system resets.

Resident Structure (ROMTag)

Like all Amiga libraries and devices, ramdrive.device starts with a struct Resident (defined in exec/resident.h):

struct Resident {
    UWORD rt_MatchWord;    /* $4AFC (RTC_MATCHWORD) */
    struct Resident *rt_MatchTag; /* Pointer to self */
    APTR  rt_EndSkip;      /* Pointer to end of module */
    UBYTE rt_Flags;        /* RTF_AFTERDOS | RTF_COLDBOOT */
    UBYTE rt_Version;      /* Version of module */
    UBYTE rt_Type;         /* NT_DEVICE */
    BYTE  rt_Pri;          /* Priority */
    char *rt_Name;         /* "ramdrive.device" */
    char *rt_IdString;     /* ID string */
    APTR  rt_Init;         /* Pointer to Init routine */
};

Reset Survival Mechanism

The primary challenge for ramdrive.device is ensuring its memory is not reclaimed by the system after a reset.

  1. Memory Allocation: When first initialized, it allocates a large block for disk data.
  2. Validation: It writes a "magic cookie" and a checksum at the start of this block.
  3. Resident List: It adds its own ROMTag to the ExecBase->ResModules list.
  4. Warm Reboot: On a reset, the Exec loader scans memory for RTC_MATCHWORD ($4AFC). When it finds the ramdrive.device tag, it checks the block's checksum.
  5. Re-binding: If valid, the device re-binds the existing data block instead of allocating a new one.

Locating ramdrive.device in ROM

# Find the resident tag in Kickstart ROM dump:
python3 - <<'EOF'
import struct, sys

rom = open("kick31.rom", "rb").read()
for i in range(0, len(rom)-4, 2):
    tag = struct.unpack_from(">H", rom, i)[0]
    if tag == 0x4AFC:   # RomTag magic
        rt_matchword = struct.unpack_from(">H", rom, i)[0]
        rt_matchtag  = struct.unpack_from(">I", rom, i+2)[0]
        rt_name      = struct.unpack_from(">I", rom, i+14)[0]
        # Offset lookup for "ramdrive.device" string
        print(f"RomTag @ ROM+{i:#x}")
EOF

Device Structure Layout

ramdrive.device extends struct Device (which extends struct Library):

struct RAMDriveBase {
    struct Device  rd_Device;    /* standard device base */
    /* private fields follow */
    APTR           rd_RAMStart;  /* pointer to allocated RAM block */
    ULONG          rd_RAMSize;   /* total size */
    ULONG          rd_BlockSize; /* always 512 */
    ULONG          rd_NumBlocks; /* RAMSize / BlockSize */
    struct MinList rd_Units;     /* list of open units */
};

Standard Device Vectors (LVO)

Offset Vector Description
6 Open Open a unit (unit number in io_Unit)
12 Close Close unit, decrement open count
18 Expunge Unload if no users
24 Reserved NULL
30 BeginIO Queue or execute an IORequest
36 AbortIO Cancel pending IORequest

IORequest Command Handling

BeginIO is the heart of the driver. It dispatches on io_Command:

void BeginIO(struct IORequest *ior) {
    struct IOStdReq *io = (struct IOStdReq *)ior;
    switch (io->io_Command) {
        case CMD_READ:    rd_Read(io);   break;
        case CMD_WRITE:   rd_Write(io);  break;
        case CMD_CLEAR:   rd_Clear(io);  break;
        case TD_FORMAT:   rd_Format(io); break;
        case TD_GETGEOMETRY: rd_Geometry(io); break;
        default:
            io->io_Error = IOERR_NOCMD;
            ReplyMsg(&io->io_Message);
    }
}

CMD_READ Implementation

void rd_Read(struct IOStdReq *io) {
    UBYTE *src = rdbase->rd_RAMStart + io->io_Offset;
    CopyMem(src, io->io_Data, io->io_Length);
    io->io_Actual = io->io_Length;
    io->io_Error  = 0;
    ReplyMsg(&io->io_Message);
}

Deep Analysis: Checksum Verification

When disassembling the initialization routine, look for the verification pattern that identifies a valid "surviving" RAM disk:

; Typical checksum verification pattern
CheckSum:
    move.l  (a0)+, d1     ; Get magic cookie
    cmpi.l  #$ABCDEF01, d1 ; Verify magic
    bne.s   Invalid
    move.l  #Length, d0
Loop:
    add.l   (a0)+, d2     ; Sum up the block
    dbf     d0, Loop
    cmp.l   Expected, d2

Disassembly Landmarks in IDA

  1. Search for string "ramdrive.device" → finds the ROMTag.
  2. RT_INIT pointer → points to the initialization function.
  3. RT_INIT logic → calls MakeLibrary then AddDevice.
  4. Library Base → Follow the rd_Device base to find the BeginIO entry point at offset -30.
  5. Switch TableBeginIO typically uses a jump table (JMP) or a series of CMPI / BEQ to dispatch commands.

References

  • NDK39: exec/devices.h, exec/io.h, devices/trackdisk.h, exec/resident.h
  • io_requests.md — IORequest structure and dispatch
  • 10_devices/trackdisk_device.md — TD_* command codes
  • IRA Disassembly of ramdrive.device — Reference for instruction patterns.