In /emu_8086.c, implement enough of MSDOS for MSBASIC load and save commands
authorNick Downing <nick@ndcode.org>
Thu, 11 Dec 2025 13:02:51 +0000 (00:02 +1100)
committerNick Downing <nick@ndcode.org>
Thu, 11 Dec 2025 13:03:55 +0000 (00:03 +1100)
emu_8086.c

index c130fe5..37b3843 100644 (file)
@@ -1,8 +1,12 @@
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <unistd.h>
 #if ALT_BACKEND
 #include "virtualxt/lib/vxt/cpu.h"
 #else
@@ -21,6 +25,23 @@ uint8_t mem[MEM_SIZE];
 uint8_t io_read[IO_SIZE];
 uint8_t io_write[IO_SIZE];
 
+// MSDOS emulation
+uint16_t msdos_dta_offset, msdos_dta_segment;
+
+// drive to directory mapping
+// string from msdos_drives[] is prepended to filename from FCB
+// string must be less than ~0xf0 bytes long so eventual path doesn't overflow
+#define N_MSDOS_DRIVES 1
+char *msdos_drives[N_MSDOS_DRIVES] = {""};
+int msdos_default_drive;
+
+#define N_MSDOS_FILES 10
+struct msdos_file {
+  int fd; // -1 if not in use
+  uint16_t fcb_offset;
+  uint16_t fcb_segment;
+} msdos_files[N_MSDOS_FILES];
+
 int load_ihx(char *name) {
   FILE *fp = fopen(name, "r");
   if (fp == NULL) {
@@ -145,10 +166,298 @@ int load_ihx(char *name) {
   return entry_point;
 }
 
-int msdos(int ax, int dx, int ds, bool *zf) {
-  switch (ax >> 8) {
-  case 2:
-    // Character output
+// extract path from FCB
+bool fcb_path(uint16_t fcb_offset, uint16_t fcb_segment, char *path) {
+  int drive = mem[fcb_offset + (fcb_segment << 4)];
+  if (drive == 0)
+    drive = msdos_default_drive;
+  else
+    --drive;
+  if (drive >= N_MSDOS_DRIVES) {
+    errno = ENOTDIR;
+    return false;
+  }
+  char *q = path;
+  for (char *p = msdos_drives[drive]; *p; ++p)
+    *q++ = *p;
+  char *p = q;
+  for (int i = 0; i < 8; ++i)
+    *q++ = tolower(
+      mem[((fcb_offset + 1 + i) & 0xffff) + (fcb_segment << 4)] & 0x7f
+    );
+  while (q > p && q[-1] == ' ')
+    --q;
+  *q++ = '.';
+  p = q;
+  for (int i = 0; i < 3; ++i)
+    *q++ = tolower(
+      mem[((fcb_offset + 9 + i) & 0xffff) + (fcb_segment << 4)] & 0x7f
+    );
+  while (q > p && q[-1] == ' ')
+    --q;
+  if (q == p)
+    --q;
+  *q = 0;
+  return true;
+}
+
+int fcb_get_logical_record_size(uint16_t fcb_offset, uint16_t fcb_segment) {
+  return
+    mem[((fcb_offset + 0xe) & 0xffff) + (fcb_segment << 4)] |
+    (mem[((fcb_offset + 0xf) & 0xffff) + (fcb_segment << 4)] << 8);
+}
+
+long fcb_get_random_record_number(
+  uint16_t fcb_offset,
+  uint16_t fcb_segment,
+  int logical_record_size
+) {
+  long random_record_number =
+    mem[((fcb_offset + 0x21) & 0xffff) + (fcb_segment << 4)] |
+    (mem[((fcb_offset + 0x22) & 0xffff) + (fcb_segment << 4)] << 8) |
+    (mem[((fcb_offset + 0x23) & 0xffff) + (fcb_segment << 4)] << 16);
+  if (logical_record_size <= 0x40)
+    random_record_number |=
+      mem[((fcb_offset + 0x24) & 0xffff) + (fcb_segment << 4)] << 24;
+  return random_record_number;
+}
+
+void fcb_set_random_record_number(
+  uint16_t fcb_offset,
+  uint16_t fcb_segment,
+  long random_record_number,
+  int logical_record_size
+) {
+  mem[((fcb_offset + 0x21) & 0xffff) + (fcb_segment << 4)] =
+    (uint8_t)random_record_number;
+  mem[((fcb_offset + 0x22) & 0xffff) + (fcb_segment << 4)] =
+    (uint8_t)(random_record_number >> 8);
+  mem[((fcb_offset + 0x23) & 0xffff) + (fcb_segment << 4)] =
+    (uint8_t)(random_record_number >> 16);
+  if (logical_record_size <= 0x40)
+    mem[((fcb_offset + 0x24) & 0xffff) + (fcb_segment << 4)] =
+      (uint8_t)(random_record_number >> 24);
+}
+
+bool fcb_open(
+  uint16_t fcb_offset,
+  uint16_t fcb_segment,
+  char *path,
+  int flags,
+  int mode
+) {
+  for (int i = 0; i < N_MSDOS_FILES; ++i)
+    if (msdos_files[i].fd < 0) {
+      int fd = open(path, flags, mode);
+      if (fd == -1)
+        return false;
+      msdos_files[i] = (struct msdos_file){
+        .fd = fd,
+        .fcb_offset = fcb_offset,
+        .fcb_segment = fcb_segment,
+      };
+      return true;
+    }
+  errno = EMFILE;
+  return false;
+}
+
+#if 0 // for some reason msbasic.ihx uses random read/write
+int fcb_read(
+  uint16_t fcb_offset,
+  uint16_t fcb_segment,
+  uint16_t dma_off,
+  uint16_t dma_seg,
+  int logical_record_size
+) {
+  for (int i = 0; i < N_MSDOS_FILES; ++i)
+    if (
+      msdos_files[i].fd >= 0 &&
+      msdos_files[i].fcb_offset == fcb_offset &&
+      msdos_files[i].fcb_segment == fcb_segment
+    ) {
+#if 0
+ printf("read %04x:%04x", fcb_offset, fcb_segment);
+ for (int i = 0xc; i < 0x25; ++i)
+  printf(" %02x", mem[((fcb_offset + i) & 0xffff) + (fcb_segment << 4)]);
+ printf("\n");
+#endif
+      uint8_t *buf = malloc(logical_record_size);
+      if (buf == NULL) {
+        perror("malloc()");
+        exit(EXIT_FAILURE);
+      }
+      int count = (int)read(msdos_files[i].fd, buf, logical_record_size);
+      if (count == -1) {
+        free(buf);
+        return -1;
+      }
+      if (count == 0) {
+        free(buf);
+        return 0;
+      }
+      int j;
+      for (j = 0; j < count; ++j)
+        mem[((dma_off + j) & 0xffff) + (dma_seg << 4)] = buf[j];
+      for (; j < 0x80; ++j)
+        mem[((dma_off + j) & 0xffff) + (dma_seg << 4)] = 0;
+      free(buf);
+      return 1;
+    }
+  errno = EBADFD;
+  return -1;
+}
+
+int fcb_write(
+  uint16_t fcb_offset,
+  uint16_t fcb_segment,
+  uint16_t dma_off,
+  uint16_t dma_seg,
+  int logical_record_size
+) {
+  for (int i = 0; i < N_MSDOS_FILES; ++i)
+    if (
+      msdos_files[i].fd >= 0 &&
+      msdos_files[i].fcb_offset == fcb_offset &&
+      msdos_files[i].fcb_segment == fcb_segment
+    ) {
+#if 0
+ printf("write %04x:%04x", fcb_offset, fcb_segment);
+ for (int i = 0xc; i < 0x25; ++i)
+  printf(" %02x", mem[((fcb_offset + i) & 0xffff) + (fcb_segment << 4)]);
+ printf("\n");
+#endif
+      uint8_t *buf = malloc(logical_record_size);
+      if (buf == NULL) {
+        perror("malloc()");
+        exit(EXIT_FAILURE);
+      }
+      for (int j = 0; j < logical_record_size; ++j)
+        buf[j] = mem[((dma_off + j) & 0xffff) + (dma_seg << 4)];
+      int count = (int)write(msdos_files[i].fd, buf, logical_record_size);
+      free(buf);
+      if (count == -1)
+        return -1;
+      if (count < logical_record_size)
+        return 0;
+      return 1;
+    }
+  errno = EBADFD;
+  return false;
+}
+#endif
+
+int fcb_read_random(
+  uint16_t fcb_offset,
+  uint16_t fcb_segment,
+  uint16_t dma_off,
+  uint16_t dma_seg,
+  long random_record_number,
+  int logical_record_size
+) {
+  for (int i = 0; i < N_MSDOS_FILES; ++i)
+    if (
+      msdos_files[i].fd >= 0 &&
+      msdos_files[i].fcb_offset == fcb_offset &&
+      msdos_files[i].fcb_segment == fcb_segment
+    ) {
+      if (
+        lseek(
+          msdos_files[i].fd,
+          random_record_number * logical_record_size,
+          SEEK_SET
+        ) == (off_t)-1
+      ) {
+        perror("fseek()");
+        exit(EXIT_FAILURE);
+      }
+      uint8_t *buf = malloc(logical_record_size);
+      if (buf == NULL) {
+        perror("malloc()");
+        exit(EXIT_FAILURE);
+      }
+      int count = (int)read(msdos_files[i].fd, buf, logical_record_size);
+      if (count == -1) {
+        free(buf);
+        return -1;
+      }
+      if (count == 0) {
+        free(buf);
+        return 0;
+      }
+      int j;
+      for (j = 0; j < count; ++j)
+        mem[((dma_off + j) & 0xffff) + (dma_seg << 4)] = buf[j];
+      for (; j < 0x80; ++j)
+        mem[((dma_off + j) & 0xffff) + (dma_seg << 4)] = 0;
+      free(buf);
+      return 1;
+    }
+  errno = EBADFD;
+  return -1;
+}
+
+int fcb_write_random(
+  uint16_t fcb_offset,
+  uint16_t fcb_segment,
+  uint16_t dma_off,
+  uint16_t dma_seg,
+  long random_record_number,
+  int logical_record_size
+) {
+  for (int i = 0; i < N_MSDOS_FILES; ++i)
+    if (
+      msdos_files[i].fd >= 0 &&
+      msdos_files[i].fcb_offset == fcb_offset &&
+      msdos_files[i].fcb_segment == fcb_segment
+    ) {
+      if (
+        lseek(
+          msdos_files[i].fd,
+          random_record_number * logical_record_size,
+          SEEK_SET
+        ) == (off_t)-1
+      ) {
+        perror("fseek()");
+        exit(EXIT_FAILURE);
+      }
+      uint8_t *buf = malloc(logical_record_size);
+      if (buf == NULL) {
+        perror("malloc()");
+        exit(EXIT_FAILURE);
+      }
+      for (int j = 0; j < logical_record_size; ++j)
+        buf[j] = mem[((dma_off + j) & 0xffff) + (dma_seg << 4)];
+      int count = (int)write(msdos_files[i].fd, buf, logical_record_size);
+      free(buf);
+      if (count == -1)
+        return -1;
+      if (count < logical_record_size)
+        return 0;
+      return 1;
+    }
+  errno = EBADFD;
+  return false;
+}
+#
+bool fcb_close(uint16_t fcb_offset, uint16_t fcb_segment) {
+  for (int i = 0; i < N_MSDOS_FILES; ++i)
+    if (
+      msdos_files[i].fd >= 0 &&
+      msdos_files[i].fcb_offset == fcb_offset &&
+      msdos_files[i].fcb_segment == fcb_segment
+    ) {
+      close(msdos_files[i].fd);
+      msdos_files[i].fd = -1;
+      return true;
+    }
+  errno = EBADFD;
+  return false;
+}
+
+int msdos(int func, int ax, int dx, int ds, bool *zf, uint16_t *cx) {
+  switch (func) {
+  case 2: // DOS 1+ - WRITE CHARACTER TO STANDARD OUTPUT
     dx &= 0xff;
     switch (dx) {
     case '\n':
@@ -160,8 +469,7 @@ int msdos(int ax, int dx, int ds, bool *zf) {
       break;
     }
     break;
-  case 6:
-    // Direct console I/O
+  case 6: // DOS 1+ - DIRECT CONSOLE INPUT/OUTPUT
     dx &= 0xff;
     if (dx == 0xff) {
       int data = getchar();
@@ -193,8 +501,7 @@ int msdos(int ax, int dx, int ds, bool *zf) {
       break;
     }
     break;
-  case 9:
-    // Display string
+  case 9: // DOS 1+ - WRITE STRING TO STANDARD OUTPUT
     for (
       int data;
       (data = mem[dx + (ds << 4)]) != '$';
@@ -210,24 +517,301 @@ int msdos(int ax, int dx, int ds, bool *zf) {
         break;
       }
     break;
-  case 0x26:
-    // Create PSP -- ignore
+  case 0xe: // DOS 1+ - SELECT DEFAULT DRIVE
+    msdos_default_drive = dx & 0xff;
+    ax = N_MSDOS_DRIVES | (ax & 0xff00);
     break;
-  case 0x30:
-    // Get DOS version
-    ax = 0x211;
+  case 0xf: // DOS 1+ - OPEN FILE USING FCB
+    {
+      char path[0x100];
+      if (!fcb_path(dx, ds, path)) {
+        perror(path);
+        ax |= 0xff;
+        break;
+      }
+      fcb_close(dx, ds);
+      if (!fcb_open(dx, ds, path, O_RDWR, 0)) {
+        perror(path);
+        ax |= 0xff;
+        break;
+      }
+      ax &= 0xff00;
+    }
     break;
-  case 0x33:
-    // Get or set Ctrl-Break -- ignore
+  case 0x10: // DOS 1+ - CLOSE FILE USING FCB
+    if (!fcb_close(dx, ds)) {
+      ax |= 0xff;
+      break;
+    }
+    ax &= 0xff00;
     break;
-  case 0x25:
-    // Set interrupt vector -- ignore
+  case 0x13: // DOS 1+ - DELETE FILE USING FCB
+    // note: for now, we don't treat the ? character as a wildcard
+    {
+      char path[0x100];
+      if (!fcb_path(dx, ds, path)) {
+        perror(path);
+        ax |= 0xff;
+        break;
+      }
+      if (unlink(path) == -1) {
+        perror(path);
+        ax |= 0xff;
+        break;
+      }
+      fcb_close(dx, ds);
+      ax &= 0xff00;
+    }
     break;
-  case 0x35:
-    // Get interrupt vector -- ignore
+#if 0 // for some reason msbasic.ihx uses random read/write
+  case 0x14: // DOS 1+ - SEQUENTIAL READ FROM FCB FILE
+    // we don't bother with the sequential position fields for
+    // now, so this can't be mixed with random access to file
+    switch (
+      fcb_read(
+        dx,
+        ds,
+        msdos_dta_offset,
+        msdos_dta_segment,
+        fcb_get_logical_record_size(dx, ds)
+      )
+    ) {
+    case -1:
+      perror("fcb_read()");
+      ax |= 0xff;
+      break;
+    case 0:
+      ax = 1 | (ax & 0xff00); // end of file
+      break;
+    case 1:
+      ax &= 0xff00;
+      break;
+    default:
+      abort();
+    }
+    break;
+  case 0x15: // DOS 1+ - SEQUENTIAL WRITE TO FCB FILE
+    // we don't bother with the sequential position fields for
+    // now, so this can't be mixed with random access to file
+    switch (
+      fcb_write(
+        dx,
+        ds,
+        msdos_dta_offset,
+        msdos_dta_segment,
+        fcb_get_logical_record_size(dx, ds)
+      )
+    ) {
+    case -1:
+      perror("fcb_write()");
+      ax |= 0xff;
+      break;
+    case 0:
+      ax = 2 | (ax & 0xff00); // disc full
+      break;
+    case 1:
+      ax &= 0xff00;
+      break;
+    default:
+      abort();
+    }
+    break;
+#endif
+  case 0x16: // DOS 1+ - CREATE OR TRUNCATE FILE USING FCB
+    {
+      char path[0x100];
+      if (!fcb_path(dx, ds, path)) {
+        perror(path);
+        ax |= 0xff;
+        break;
+      }
+      fcb_close(dx, ds);
+      if (!fcb_open(dx, ds, path, O_RDWR | O_CREAT, 0666)) {
+        perror(path);
+        ax |= 0xff;
+        break;
+      }
+      ax &= 0xff00;
+    }
+    break;
+  case 0x19: // DOS 1+ - GET CURRENT DEFAULT DRIVE
+    ax &= 0xff00; // always a:
+    break;
+  case 0x1a: // DOS 1+ - SET DISK TRANSFER AREA ADDRESS
+    msdos_dta_offset = dx;
+    msdos_dta_segment = ds;
+    break;
+  case 0x21: // DOS 1+ - READ RANDOM RECORD FROM FCB FILE
+    {
+      int logical_record_size = fcb_get_logical_record_size(dx, ds);
+      long random_record_number = fcb_get_random_record_number(
+        dx,
+        ds,
+        logical_record_size
+      );
+      switch (
+        fcb_read_random(
+          dx,
+          ds,
+          msdos_dta_offset,
+          msdos_dta_segment,
+          random_record_number,
+          logical_record_size
+        )
+      ) {
+      case -1:
+        perror("fcb_read_random()");
+        ax |= 0xff;
+        break;
+      case 0:
+        ax = 1 | (ax & 0xff00); // end of file
+        break;
+      case 1:
+        ax &= 0xff00;
+        // random record number is not updated after reading
+        break;
+      default:
+        abort();
+      }
+    }
+    break;
+  case 0x22: // DOS 1+ - WRITE RANDOM RECORD TO FCB FILE
+    {
+      int logical_record_size = fcb_get_logical_record_size(dx, ds);
+      long random_record_number = fcb_get_random_record_number(
+        dx,
+        ds,
+        logical_record_size
+      );
+      switch (
+        fcb_write_random(
+          dx,
+          ds,
+          msdos_dta_offset,
+          msdos_dta_segment,
+          random_record_number,
+          logical_record_size
+        )
+      ) {
+      case -1:
+        perror("fcb_write_random()");
+        ax |= 0xff;
+        break;
+      case 0:
+        ax = 2 | (ax & 0xff00); // disc full
+        break;
+      case 1:
+        ax &= 0xff00;
+        // random record number is not updated after writing
+        break;
+      default:
+        abort();
+      }
+    }
+    break;
+  case 0x25: // DOS 1+ - SET INTERRUPT VECTOR -- ignore
+    break;
+  case 0x26: // DOS 1+ - CREATE NEW PROGRAM SEGMENT PREFIX -- ignore
+    break;
+  case 0x27: // DOS 1+ - RANDOM BLOCK READ FROM FCB FILE
+    {
+      int logical_record_size = fcb_get_logical_record_size(dx, ds);
+      long random_record_number = fcb_get_random_record_number(
+        dx,
+        ds,
+        logical_record_size
+      );
+      for (int i = 0; i < *cx; ++i) {
+        // for now we ignore the record number and just do it sequentially
+        switch (
+          fcb_read_random(
+            dx,
+            ds,
+            msdos_dta_offset + i * logical_record_size,
+            msdos_dta_segment,
+            random_record_number + i,
+            logical_record_size
+          )
+        ) {
+        case -1:
+          perror("fcb_read_random()");
+          ax |= 0xff;
+          break;
+        case 0:
+          ax = 1 | (ax & 0xff00); // end of file
+          break;
+        case 1:
+          continue;
+        default:
+          abort();
+        }
+        *cx = (uint16_t)i;
+        goto random_block_read_error;
+      }
+      ax &= 0xff00; // cx unchanged
+    random_block_read_error:
+      fcb_set_random_record_number(
+        dx,
+        ds,
+        random_record_number + *cx,
+        logical_record_size
+      );
+    }
+    break;
+  case 0x28: // DOS 1+ - RANDOM BLOCK WRITE TO FCB FILE
+    {
+      int logical_record_size = fcb_get_logical_record_size(dx, ds);
+      long random_record_number = fcb_get_random_record_number(
+        dx,
+        ds,
+        logical_record_size
+      );
+      for (int i = 0; i < *cx; ++i) {
+        // for now we ignore the record number and just do it sequentially
+        switch (
+          fcb_write_random(
+            dx,
+            ds,
+            msdos_dta_offset + i * logical_record_size,
+            msdos_dta_segment,
+            random_record_number + i,
+            logical_record_size
+          )
+        ) {
+        case -1:
+          perror("fcb_write_random()");
+          ax |= 0xff;
+          break;
+        case 0:
+          ax = 2 | (ax & 0xff00); // disc full
+          break;
+        case 1:
+          continue;
+        default:
+          abort();
+        }
+        *cx = (uint16_t)i;
+        goto random_block_write_error;
+      } 
+      ax &= 0xff00; // cx unchanged
+  random_block_write_error:
+      fcb_set_random_record_number(
+        dx,
+        ds,
+        random_record_number + *cx,
+        logical_record_size
+      );
+    }
+    break;
+  case 0x30: // DOS 2+ - GET DOS VERSION
+    ax = 0x211;
+    break;
+  case 0x33: // DOS 2+ - EXTENDED BREAK CHECKING -- ignore
+    break;
+  case 0x35: // DOS 2+ - GET INTERRUPT VECTOR -- ignore
     break;
   default:
-    fprintf(stderr, "unimplemented MSDOS call 0x%02x\n", ax >> 8);
+    fprintf(stderr, "unimplemented MSDOS call 0x%02x\n", func);
     exit(EXIT_FAILURE);
   }
   return ax;
@@ -343,12 +927,15 @@ int main(int argc, char **argv) {
 
   // psp
   int code_segment = (entry_point >> 16) & 0xffff;
-  mem[5 + (code_segment << 4)] = 0xc3; // ret, for bdos emulation
+  mem[5 + (code_segment << 4)] = 0xc3; // ret, for msdos emulation
   mem[6 + (code_segment << 4)] = 0xfe; // memory top, lo byte
   mem[7 + (code_segment << 4)] = 0xff; // memory top, hi byte
   mem[0x80 + (code_segment << 4)] = 0; // command line length
   mem[0x81 + (code_segment << 4)] = '\r'; // command line sentinel
 
+  for (int i = 0; i < N_MSDOS_FILES; ++i)
+    msdos_files[i].fd = -1;
+
 #if ALT_BACKEND
   struct cpu cpu;
   memset(&cpu, 0, sizeof(struct cpu));
@@ -400,13 +987,31 @@ int main(int argc, char **argv) {
       break;
     if (cpu.regs.ip == 5 && cpu.regs.cs == code_segment) {
       bool zf = (cpu.regs.flags >> 6) & 1;
-      cpu.regs.ax = msdos(cpu.regs.cl << 8, cpu.regs.dx, cpu.regs.ds, &zf);
+      uint16_t cx = cpu.regs.cx;
+      cpu.regs.ax = msdos(
+        cpu.regs.cl,
+        cpu.regs.ax,
+        cpu.regs.dx,
+        cpu.regs.ds,
+        &zf,
+        &cx
+      );
       cpu.regs.flags = (zf << 6) | (cpu.regs.flags & ~0x40);
+      cpu.regs.cx = cx;
     }
     else if (cpu.regs.ip == 1 && cpu.regs.cs == 0x40) {
       bool zf = (cpu.regs.flags >> 6) & 1;
-      cpu.regs.ax = msdos(cpu.regs.ax, cpu.regs.dx, cpu.regs.ds, &zf);
+      uint16_t cx = cpu.regs.cx;
+      cpu.regs.ax = msdos(
+        cpu.regs.ax >> 8,
+        cpu.regs.ax,
+        cpu.regs.dx,
+        cpu.regs.ds,
+        &zf,
+        &cx
+      );
       cpu.regs.flags = (zf << 6) | (cpu.regs.flags & ~0x40);
+      cpu.regs.cx = cx;
     }
     cpu_step(&cpu);
   }
@@ -453,7 +1058,7 @@ int main(int argc, char **argv) {
       break;
     }
     if (cpu.regs.word.pc == 5) {
-      cpu.regs.word.de = bdos(
+      cpu.regs.word.de = msdos(
         cpu.regs.byte.c,
         cpu.regs.word.de
       );