Up to this point, all code running on my 286 system has been x86 assembly (real mode). I am hoping to learn how to compile C or C++ code that can run on my system. As I learn more about how to do this, I will capture my learning in this blog post. This post will be updated periodically.
Installing Open Watcom 2.0
I am starting with Open Watcom 2.0. From here I downloaded the Current Release Build (GitHub Release), open-watcom-2_0-c-win-x64.exe (newest as of this writing). I installed Open Watcom, with the following selections, on a Windows 11 host PC.
Path of d:\watcom
Selective installation
Include 16-bit compilers: Small, Medium, Compact, Large
Target operation systems: DOS
Host operating systems: Windows NT 64-bit (experimental)
Toolkits and other components: Include C++ compiler components, Sample programs, Help files for Win32 & PDF.
Modify local machine environment variables
Modify associations
I updated my Windows environment variables based on d:\watcom\changes.env. I then restarted the system and continued to setup a quick test of the compiler.
To perform a basic test of the compiler, I created a file d:\watcom_projects\helloworld\hello.cpp, as follows.
#include <iostream.h>
void main()
{
cout << "Hello world" << endl;
}
I then ran the following to build it.
wcl -l=dos hello.cpp
As this stage, I appear to have a compiled executable.
Configuring Visual Studio 2022
In my existing Visual Studio project with my x86 assembly code, I added a file "buildcpp.cmd" with the following.
@echo off
echo Open Watcom Build Environment
PATH D:\WATCOM\BINNT64;D:\WATCOM\BINNT;%PATH%
SET INCLUDE=D:\WATCOM\H;D:\WATCOM\H\NT;D:\WATCOM\H\NT\DIRECTX;D:\WATCOM\H\NT\DDK
SET WATCOM=D:\WATCOM
SET EDPATH=D:\WATCOM\EDDAT
SET WHTMLHELP=D:\WATCOM\BINNT\HELP
SET WIPFC=D:\WATCOM\WIPFC
@echo on
wcl -l=dos %1
Now, I can right click on my project and choose Open in Terminal. In Terminal, I run the following to build a file.
cmd /c buildcpp.cmd .\hello.cpp
So, at this point I can edit the .cpp files in Visual Studio, where I am using Visual Studio as a simple text editor. This will let me manage versions with Git.
DOSBox
Ultimately, I would like to run DOS on my system. With this in mind, I am going to try and develop DOS-compatible .com (or maybe .exe) files. To test my simple helloworld program from above, I installed DOSBox and ran my hello.exe. Success!
Building a COM File
Above, I was able to compile a .exe file. I am using the following wcl syntax to get a .com output.
wcl -2 -mt -bcl=com -lr hello.cpp
The resulting file is 9,152 bytes in size. The file tests fine in DOSBox.
Interrupts
Using Ndisasm, I disassembled the .com file to get a list of interrupts that the application is using. These would be DOS interrupts.
It appears that interrupts 21 and 3 are used. Interrupt 3 appears to be related to calling a debug exception handler. For each 21 interrupt I can look at the assembly code preceding the int call for the value of ah. This gives me a list of interrupts that I *should* try to support in my operating system, listed below (data from Interrupt Services DOS/BIOS/EMS/Mouse (stanislavs.org)). An additional helpful resource: 8086 bios and dos interrupts (IBM PC) (ablmcc.edu.hk).
21, 1 Keyboard Input with Echo
21, 4a Modify Allocated Memory Block (SETBLOCK)
21, 4c Terminate Process with Return Code
21, 30 Get DOS Version Number
21, 3d01 Open File Using Handle (write only)
21, 3e Close File Using Handle
21, 3f Read from File or Device Using Handle
21, 40 Write to File or Device Using Handle
21, 42 Move File Pointer Using Handle
21, 4400 I/O Control - Get Device Information
21, 68 Flush Buffer Using Handle
Next Steps
Additional steps needed (that I know of at this point):
Figure out how I would load a .com file into my running 286 system.
Implement required support in my "operating system," such as INT21.
Identify and implement required support in my "BIOS" (e.g., interrupts). So far, the line between BIOS and operating system has been very blurry in my build. I hope to better delineate the two as I move forward.
...
So Many Questions
The more I learn, the more daunting the task seems to be to implement an IBM compatible BIOS and MS-DOS compatible operating system; both will be required before I can realistically run a compiled C/C++ application. Below, I will add questions/unknowns that come to mind as I work through things. This may not be well organized, but I at least wanted to capture my thoughts. The list will likely grow significantly over time. :)
The first BIOS interrupt that I would like to add to my BIOS is INT 10H (video services). Functions (set in AH) that are of initial interest to me are 02h (set cursor position), 03h (get cursor position), 09h (write character with color value), 0Ah (write character only), 0Ch (write pixel), and 13h (write string). 09h uses BL for color, but I am using 16-bit color, not 8-bit color. How would I support more than 8-bit color with 09h? I also need to better understand how pages are used in the context of video interrupts. For example, 0Ch uses BH for page number, CX for x, and DX for y. I will need to determine how this maps to my screen resolution. INT 10H - Wikipedia
...
Additional Resources
The following might be helpful to me as I progress, so I am tracking them here.
Update...
In the following video, I get a C program working (with a little cheating).
Following the above video, I tried a C++ file, too. Here's the source code:
#include <iostream.h>
int main()
{
cout << "Hello world 3/20" << endl;
return 0;
}
The result is that it ran fine. I increased the debug logging details. Below is what the C++ code generated for interrupts. The additional columns of logged data are the values of registers AX, BX, CX, DX, CS, DS, and ES (in that order) coming into the interrupt.
The difference with the C++ from the C program is the lack of INT 0x21 Function 0x63 and the addition of Function 0x68 (flush buffer).
Update...
In an earlier video, I had commented that I cheated a bit in the Write function 0x40. The address I was getting in the interrupt (e.g. DX=0x2F94 in the C++ version) for the location of the string to print didn't seem to be working, and I was manually pointing to the location of the string data in loaded COM file. I appear to have resolved the issue. When loading the COM file into memory and far jumping to it, I wasn't setting all of my registers for the COM application. I added that code prior to making the jump, as seen below. Now, the location of the write buffer specified in DS:DX is working.
; CS=DS=ES=SS=PDB pointer, IP=100, SP=max size of block.
mov ax, 0
mov ds, ax
mov es, ax
mov ss, ax
mov ax, 0xfffe
mov sp, ax
; cs, ip will change with following jmp
jmp 0x2000:0x0100 ; Jump to embedded DOS .com at 0x20100
Credit to SLYNYRD for the graphic content for the software sprite.
Code used in the above video: x86/286-related/WorkingCode/20230326 at main · rehsd/x86 (github.com).
I hope you see that all the int21 are not BIOS but MSDOS interrupts. This will not help you with writing a proper BIOS. Best is to look to a MSDOS source to see how MSDOS is using the BIOS. A good starting point are the MSDOS 1.25 sources. Advice to compile it (it works, even in virtual box) can be found here: https://www.betaarchive.com/forum/viewtopic.php?t=40792
But beware all these MSDOS sources are in MASM assembly code.
I love to watch your series about this 80826 CPU whom I have good memories from.