Friday, June 7, 2024

Coding in mixed Assembly and C for the PC-9801 in 2024

(Scroll down to the bottom to download the project)
Project contents:
     _LCC             : LSI-C makefile
    98header.s       : FDI header source      : Examine DOS-EXE and optionally relocate : Inserts a file to resolved cylinder/head/sector address of FDI                        file
    main.s           : ASM source for IPL/initial program loader
    Makefile         : GNU Make for this project
    test.c           : LSI-C source for TEST.EXE hello world

There isn’t a whole lot of options out there when it comes to compiling code for 16-bit real mode apps - what you need to make DOS mode games on the PC98!
It started with curiosity of how I would get started hacking Record of Lodoss War.
It started from this assembly hello world example I found online here: , and became a rabbit hole on every single edge.

1. Researching Disk Formats and Boot Loading
2. Finding a C Compiler that works is hard
3. Research into Disk Coding
4. DOS-EXE Disassembly 

5. …And Trimming Everything Into Place

1. Researching Disk Formats and Boot Loading

Based on the assembly example, there was a functional disk header defined in data bytes included. This isn’t very helpful however, since the format it uses (TFD) is not used by any emulator I have. I COULD, though, compare it to the FDI header generated from a blank FDI disk thanks to barbecues tools . After some stumbling around NASM syntax, I realized that generating a FDI header was pretty easy:

                   DD 0
        DD 144
        DD 4096
        DD 1261568
        DD 1024
        DD 8
        DD 2
        DD 77

        times 0x1000 - ($-$$) db 0

This file I compiled and saved as header.bin and put aside.

Despite a few small syntax errors in the example, I mangled to compile and run it as an FDI.
At the end of the assembly file, simply adding

        times 0x134000-($-$$) db 0xbe

will fill out the rest of the bytes. Then, running

        cat header.bin prog.bin > out.fdi

will create a bootable fdi image.

With a fair bit of ease, I was able to determine vis-a-vis DOSBox-X debugger (command: “ev cs”) that the code segment after booting from a disk with anything on it (e.g. jp $) was at 1fc00h. With the “memdump 1fc0:0000 1000” command, I was able to determine that 1024 bytes were copied from the disk into segment 1fc0 and ran there. The rest of the bytes after 1024 were not 0xbe, telling me no more were loaded from disk.

This is as expected. The PC8801 copies the first sector from disk into memory and runs it as well - but on the 88, it’s only 256 bytes.

Great! On to the next thing - can I do C? And if so, can I interoperate with arbitrary asm?

2. Finding a C Compiler that works is hard

I tried to install gcc-ia16 (various platform failures), Borland for DOS (crashes on PC98), build OpenWatcom (failed for lack of lib support) AND run its DOS installer (hangs the PC98), all to no avail.

Feeling frustrated, I googled Cコンパイラ PC98 and found this Qiita page: where they linked to an archived version of the LSI-C86 compiler toolchain!

This is the business. I quickly downloaded it, tested it, and uploaded it to It works well, EXCEPT, after much and much fumbling I was unable to get the linker (ldd) to play nice with its own assembler created files (r86) exporting to COM. This was to simplify the interaction between ASM and C. Unfortunately, perhaps due to a bug, COM export failed.

I was getting EXEs though, and that’s great news! So maybe there is a way to run the executable file from assembly? Perhaps stripping the header and calling the code start point? How complicated could exe files be anyway? Famous last words.

I made a quick Hello World that writes the text VRAM directly and left the EXE for now.

Before diving into exes, I wanted to make sure that I would be able to load a file that existed in a random location on disk into general RAM. For that, I would need some documentation on the floppies…

3. Research into Disk Coding

Lucky for me, in my ramblings in search of info (memory maps, etc) I stumbled upon mention of the PC-98 Programmer’s Bible: I decided to check it out, and wow! Glad I did.
This contains information on every BIOS call, including disk I/o, so despite some trouble in converting the example C program back to ASM and way more time than I should have spent narrowing down  the formula for the location on disk, I got a disk writing and reading routine done in just a couple hours.

%macro DiskLoad 6
; Src C, Src H, Src S, Dst Seg, Dst Ofs, Byte Ct
    mov cx,3<<8 | %1 ; sector len, cylinder
    mov dx,%2<<8 | %3 ; head, start sector
    mov ax,%4 ; segment
    mov es,ax
    mov bp,%5 ; offs
    mov bx,%6 ; bytes
    mov ax,0x76<<8 | 0x90 ; load cmd
    int 1bh
%macro DiskWrite 6
; Src Seg, Src Ofs, Dst C, Dst H, Dst S, Byte Ct
    mov ax,%4 ; segment
    mov es,ax
    mov bp,%5 ; offs
    mov cx,3<<8 | %1 ; sector len, cylinder
    mov dx,%2<<8 | %3 ; head, start sector
    mov bx,%6 ; bytes
    mov ax,0x75<<8 | 0x90 ; write cmd
    int 1bh

*Of note, even if the byte count in bx is < len(sector), the remainder will be filled with 00 if writing to disk - precision reads may be possible but precision writes are not.

While I was doing it, I wrote a python script (and sent it over to barbeque) that will add a file to a given cylinder, head and sector of a FDI image.

The formula, given a header of 0x1000 size and variables cy, he, se, is:

    loc = (int(cy) * 0x4000) + (int(he) * 0x2000) + ((int(se)-1) * 0x400) + 0x1000

The se (sector) variable is subbed by 1 because it's range is 1 to 8 instead of 0 to 7.

Almost there: I needed to figure out how to load up these pesky DOS-EXE files.

4. DOS-EXE Disassembly 

It took me about 20 tries reading over , , and (all which contain slightly varying and slightly confusing explanations of the same thing) before I understood what *precisely* the EXE file was doing.

It may help to restate:
The EXE file can be loaded to an arbitrary memory address. Whatever that address segment is, must be added to all of the locations that are pointed to by the values in its relocation table. Every (code_start + table_val) location contains a relative offset / segment value that must be added into the load address.

SO. A relocation table of: 

01 00 00 00, 

34 00 00 00, 

17 00 00 00
Means that at (code_start + 01h), (code_start + 34h), and (code_start + 17h) there are values which must be incremented by LOAD_ADDRESS. Then, you can safely JP LOAD_ADDRESS.

That’s it! This also means if you know where you are going to execute the code from, given a static memory map, you can strip the header completely and adjust the relocation segment variables beforehand.

5. …And Trimming Everything Into Place

Which is exactly what I did. Another hour or so and I had a “quick” python script to do the following:
- scan the header,
- print the data on the table,
- trim the header and
- adjust the relocation values in the file.

After that, everything was ready… I just had to perform the magic ASM:

          DiskLoad 0,0,2,0x2000,0,1024*6
    call 2000h:0000h

and use my trusty Makefile to put it all together:

      nasm 98header.s -o header.bin
      nasm main.s -o prog.bin
      cat header.bin prog.bin > app.fdi
      python3 TEST.EXE -r 0x2000
      python3 app.fdi TEST.EXE_ 0 0 2


Something is actually as easy at it looks for once! What a surprise.

You can download the entire project here!

But you must have all the tools (LSI-C, NASM, GNU Make, Python3) yourself.