Something COMmon About MS-DOS and CP/M

Bill Wilkinson

This document was updated on Saturday, 2 April 2005.
Internal changes only; no changes to the content.


(From Remark magazine, 1987. See the Notes at the end for details.)

Here's an interesting programming technique I've discovered. I doubt that I was the first one to discover it, but I haven't seen it on any of the bulletin boards nor in any of the computer magazines. Basically, the technique lets you write COM-type programs that will run unmodified under both MS-DOS and CP/M.

The program can do this because of two facts: First, COM files in both CP/M and MS-DOS start at address 100 hex. Second, there exists certain bit patterns common to both the 8080 and 8088 instruction set that cause both CPUs to behave predictably. All that's necessary is that this bit pattern be placed at the start of the program. As a specific example, consider the following binary sequence.

	11001101 00010001 00100100 11111111
For the 8088, this pattern translates to the following opcodes:
        INT     11H
        AND     AL,0FFH
For the 8080, the first three bytes translate to:
        CALL    2411H
and the fourth byte is ignored.

See where this is heading? The 8088 instructions tell MS-DOS to return the I/O configuration and AND it with 0FFH, which, of course, does nothing to the returned data. Your MS-DOS program can immediately follow. Depending on what it does, you may or may not want to use the data returned by the interrupt instruction. The 8080 sees the first three bytes as an unconditional call to address 2411 hex, so that's where the CP/M code should start. This gives about 8K of memory for MS-DOS and whatever length is necessary for CP/M. Of course, if you need more memory for your MS-DOS code, you can always jump around the CP/M portion.

To use this technique requires two computers, one that runs CP/M and a PC machine that runs MS-DOS. (Note that this program may not work on the 8088-side of the original H/Z-100 series. Unless I overlooked it while reading version 2 of the Programmer's Utility Pack, the H/Z-100 does not recognize INT 11H.) The machines I developed and tested this program on were an H8/H19 and an HF-241. I transferred data between the two machines by hooking up a cable between the two serial ports and using XMODEM. I used CPS on the HF-241 side and a public domain version of Christensen's MODEM7 program on the H-8.

I was tempted to follow up on that INT 11H instruction and make the program display a large number of facts about the operating system it was running under, but decided to keep it simple. So the example program, DOSINFO.COM, will only print a sign-on message on the screen and then state whether it's running under MSDOS or CP/M.

DOSINFO.COM must be built out of two binary files called DCPM.BIN and DMSDOS.BIN. These in turn are assembled from their associated ASM files shown in listings one and two. In the CP/M code (Listing 1), the call start instruction is there for testing the program before transferring it to MS-DOS. However, it doesn't make any difference whether or not that command is there when you merge the program with DMSDOS.BIN. The remainder of the code begins at 2411 hex as defined by the org 2411h statement. When start: is called, the stack pointer puts the return address in the stack. Since the CP/M program will never return to main:, the pop h instruction removes this address from the stack. The rest of the program is the standard CP/M procedure for printing text to the display. The DE register points to the start of the text while the C register has the "print text" command. Calling BDOS performs the action, then jmp 0 causes the program to exit to CP/M.

The source code for DMSDOS (Listing 2) is similar to that in DCPM. After performing the int 11h and the and al,11h instructions, the program points the DX register to the text, loads the "print text" command into the AH register, then prints it by performing an interrupt 21H. The program exits to MS-DOS when it encounters int 20h.

To write the program, use your favorite text editor and enter Listing 1 on the CP/M machine. Assuming that A: is your default drive and all your programs are there, assemble DCPM by typing:

ASM DCPM.AAZ
LOAD CPM
Transfer the resulting COM file to MS-DOS with XMODEM (or other error-checking protocol) and name it DCPM.BIN.

Then, on the MS-DOS side, type in the code in Listing 2 and assemble it as follows:

MASM DMSDOS;
LINK DMSDOS;
EXE2BIN DMSDOS
MASM is part of the Programmer's Utility Pack, while LINK and EXE2BIN come with MS-DOS. Note that without the semicolons at the end of the first two commands, MASM and LINK will stop to ask you a bunch of unnecessary questions. EXE2BIN will automatically name the assembled program to DMSDOS.BIN.

Use DEBUG to merge DCPM.BIN and DMSDOS.BIN. The following steps show how. (Do not enter the comments in parenthesis. That's just for your information.)

DEBUG DMSDOS.BIN (Load the MS-DOS code first.)

R               (Note the value in the CX register pair. It should
                be 93 for this program.)

M100 L 93 8000  (Move the program to 8000H. If CX was a different 
                number in the previous step, use it instead of 93.)

NDCPM.BIN       (Load the CP/M file.)

L

M8000 L 93 100  (Move the MS-DOS program back to the beginning.)

NDOSINFO.COM    (Write the merged programs to DOSINFO.COM. 
                It's not necessary to calculate the
W               length, since the CX register was set up
                properly when DCPM.BIN was loaded.)

Q               (Exit to MS-DOS.)
You should now have a runable copy of DOSINFO.COM on your disk.

Instead of DEBUG, you can use the C program shown in Listing 3. I wrote this to avoid continually repeating the above procedure while developing DOSINFO. The code is heavily commented, so if you don't have a C compiler, you shouldn't have too much of a problem translating it to your preferred language. Basically, DMERGE will copy DMSDOS.BIN to DOSINFO.COM and write zeros in the area between the end of the MS-DOS code and the start of the CP/M code. Next, it opens DCPM.BIN, jumps to the start of the code at 2411 hex, and copies it to DOSINFO.COM. Then DMERGE closes all open files, exits to MS-DOS, and DOSINFO is ready to run.

Once you're satisfied that it runs properly on MS-DOS, transfer it to your CP/M system and test it there. This time, it will tell you that it's running under CP/M.

As you see the program run, you may be thinking, "it's clever, but what use is it?"

Beats me.

I got a kick out of the concept, but can't think of any way to get rich off it. It lets you write programs that are transportable between the two disk operating systems, but you have to write twice as much code. In such a case, writing your program in a high-level language and compiling it twice is more practical. The only useful thing I can think of is as a protection device for public domain and shareware programs. For example, somebody downloads CPMCHESS.COM from the local bulletin board and tries to run it on an MS-DOS machine. The program states "Sorry, I'm not an MS-DOS program" then performs an orderly exit.

Other than that, the DOSINFO technique may either be totally useless or is a solution looking for a problem. Any ideas?

(Source code follows.)


Listing 1


;
;       DCPM.ASM
;
;
;       Disk Operating System Informatinn Version 1.0
;       William A. Wilkinson 10-Nov-86
;
;       Assemble under CP/M then transfer the binary code
;       to MS-DOS.
;
cr      equ     0dh     ; Carriage return.
if      equ     0ah     ; Line feed.
tab     equ     9       ; Tab.
bdos    equ     5       ; Function entry point.

        org     l00h
;
;       MS-DOS thinks that the following instruction is
;       a "get i/o configuration" command (INT 11H).
main:
        call    start

        org     2411h   ; CP/M program starts here.


start:
        pop     h       ; Clean up.  We're not returning.
        lxi     d,cptxt ; Point to the CP/M message.
        mvi     c,9     ; Print string function.
        call    bdos
        jmp     0       ; Exit.

cptxt:  db      cr,lf,lf
        db      tab,tab,tab
        db      'DOS Information Version 1.0',cr,lf
        db      tab,tab,tab
        db      '   William A. Wilkinson',cr,lf
        db      tab,tab,tab
        db      '         10-Nov-86',cr,lf,lf
        db      tab,tab
        db      '    This program is running under CF/M.'
        db      cr,lf,lf
        db      '$'

        end


Listing 2


;
;       DMSDOS.ASM
;
;       Disk Operating System Information Version 1.0
;       William A. Wilkinson 10-Nov-86
;
;       MS-DOS code.
;
cr      equ     0dh     ; Carriage return.
lf      equ     0ah     ; Line feed.
tab     equ     9       ; Tab.

dosinfo segment

        org     l00h

        assume  cs:dosinfo,ds:dosinfo

main    proc    near


;       CP/M thinks that the following 1-1/2 instructions
;       form an unconditional call to 2411H.

start:
        int     llh             ; Get the I/O configuration.
        and     al,0ffh         ; Do nothing with it.
        mov     dx,offset mstxt ; Tell 'em MS-DOS.
        mov     ah,9
        int     21h
        int     20h              ; Goodbye.

mstxt   db      cr,lf,lf
        db      tab,tab,tab
        db      'DOS Information Version 1.0',cr,lf
        db      tab,tab,tab
        db      '       William A. Wilkinson',cr,lf
        db      tab,tab,tab
        db      '         10-Nov-86',cr,lf,lf
        db      tab,tab
        db      '    This program is running under MS-DOS.'
        db      cr,lf,lf
        db      '$'

main    endp                    ; End of main procedure

dosinfo ends                    ; Close the segment.

        end     main            ; Program starts at main.


Listing 3


/*
        DMERGE.C Ver.l.0
        Written in Toolworks C by William A. Wilkinson,
        15 November 1986.

        This program merges the two binary files used in the
        DOSINFO MSDOS-CP/M demonstration program.  The MS-DOS
        file should be named DMSDOS.BIN and the CP/M filename
        should be DCPM.BIN.  DMERGE will first transfer
        DMSDOS.BIN to DOSINFO.COM.  Next it will pad the
        remainder of DOSINFO.COM with nulls up to address
        0x2310.  DMERGE will then read DCPM.BIN, skipping the
        first 0x2310 bytes.  At byte number 0x2311 (0x2411 with
        the 0x100 offset), it will transfer DCPM.BIN to
        DOSINFO.COM.
*/

#include <stdio.h>
#include <math.h>       /* Comment out if not using Toolworks C or */
                        /*  don't have the Mathpak option. */

main()
{
  FILE  *fp0, *fp1, *o_file();
  int   c, i;
  long  cpmstart;       /* Change to int if using Toolworks C */
                        /*  without the Mathpak. */

  cpmstart = 0x2411 - 0x100;            /* Start of CP/M code. */

  fputs("DMERGE Ver.l.0 by Bill Wilkinson 15 November 1986\n", stdout);

  fp0 = o_file("dosinfo.com", "wb");    /* Create DOSINFO.COM. */
  fp1 = o_file("dmsdos.bin", "rb");     /* Open the MS-DOS binary file. */

  fputs("\nTransferring DMSDOS.BIN to DOSINFO.COM...", stdout);

  i=0x100;  /* Initialize counter to standard *.COM file offset. */

  while ((c = fgetc(fp1)) != EOF) {     /* Transfer DMSDOS.BIN */
    fputc(c, fp0);                      /*  to DOSINFO.COM. */
    i++;                                /* Point to the next empty byte.*/
  }

  fputs("\nPadding with nulls...", stdout);
  while (i < 0x2411) {                  /* Pad with nulls. */
      fputs(NULL, fp0);
      i++;
  }

  o_file(fp1);                          /* Close DMSDOS.BIN. */
  fp1 = o_file("dcpm.bin", "rb");       /* Now open CP/M binary file. */

  fputs("\nSkipping 0x2310 bytes in DCPM.BIN...", stdout);

  fseek(fp1, cpmstart, SEEK_SET);

  fputs("\nTransferring DCPM.BIN to DOSINFO.COM...", stdout);

  while ((c = fgetc(fp1)) != EOF)       /* Transfer rest to DOSINFO.COM.*/
    fputc(c, fp0);

  c_file(fp1);                          /* Done!  Close the files. */
  c_file(fp0);

  fputs("\n\nDone!\n\n", stdout);
  exit(0);                              /* Return to MS-DOS. */
}


/*
        Open the file (fn) for read or write (type).  Return
        the file pointer (fp).  Exit to MS-DOS if can't open.
*/

FILE *o_file(fn, type)
char *fn, *type;
{
  FILE  *fp, *fopen();

  if ((fp = fopen(fn, type)) == NULL) {
    fputs("\nCannot open file!\n", stderr);
    exit(1);
  }
  return(fp);
}


/*
        Close the file pointed to by the file pointer (fp).
        Do not return anything.  Exit to MS-DOS if can't
        close.
*/

c_file(fp)
FILE *fp;
{
  int c;

  if ((c = fclose(fp)) != NULL) {
    fputs("\nCannot close file!\n", stderr);
    exit(l);
  }
}


Notes About the 1999 HTML Version of This Article

I wrote this article in late 1986 and it was published in the February, 1987, issue of Remark magazine. (Remark was the official publication of Heath User's Group--a group devoted to Heath/Zenith computers.)

I've lost the original electronic copy of the article, so I've scanned it in from the magazine. The OCR software introduced some typos, but I think I've found and corrected them all. I've also made minor corrections to the orginal text (but not the source code). If you discover an error, however, please email me.

Bill Wilkinson
16 February 1999


The original copy of my article appeared in Remark magazine, which is Copyright (C) 1987, Heath/Zenith User's Group.
This HTML version of my article is Copyright © 1999, 2003, William Albert Wilkinson. All rights reserved.


Go to Bill Wilkinson's Heath Company Page.