| Author: | John Tsiombikas (Nuclear of TheLab and Mindlapse). |
|---|---|
| Contact: | nuclear@member.fsf.org |
Contents
Welcome to the first part of my series of tutorials on DOS retrocoding. In this series I will guide you through the basics of writing games or demos, on 90s era MS-DOS computers.
I will, necessarilly, have to touch onto some graphics programming fundamendals along the way, but I will not focus on high level graphics algorithms. Such concepts are mostly platform-independent, and there are many texts available on the subject (see Real-Time Rendering by Möller, Haines and Hoffman).
The focus of this series is going to be on the aspects of programming for games/demos, which modern systems handle for us. We will have to transcend the boundaries of typical modern user-level programs, into the field of what today would be called kernel programming, but was once the bread and butter of any game or demo hacker. This holistic systems programming approach which was required by 90s game development, is in a large extend the lure of such kind of retrocoding. It puts the programmer in the driver seat, requiring deep understanding of the system on all levels, and minimizing 3rd party black boxes which, today, impede total knowledge and control.
If any of the above sound enticing to you, feel free to tag along.
All x86 processors start running in 16bit mode after a reset, in order to maintain backwards compatibility with programs written for the 8086. This 16bit mode is called "real mode", and it's the mode in which MS-DOS and most DOS programs run.
In real mode the processor uses 20 bit addresses like the original 8088/8086, and can thus access up to 1MB of memory (2^20 bytes). The 20 bit addresses are formed by combining the value of a segment register with a 16bit offset. The offset is added to the value of the segment register shifted 4 bits to the left like so:
20 16 12 8 4 0
+------+------+------+------+ .
|15 12|11 8|7 4|3 0| . segment
+------+------+------+------+ .
+------+------+------+------+
+ |15 12|11 8|7 4|3 0| offset
+------+------+------+------+
When talking about real-mode addresses, we often use the segment:offset notation. So for instance the linear 20bit memory address abcde can be accessed by loading a segment register with the value a000 and using it with the offset value bcde, in which case we refer to that address as a000:bcde. Note that segments can overlap, and we could also access the exact same linear address as ab00:0cde, a00d:bc0e etc.
Intel processors during our focus period of the 90s, had progressed far from their lowly 16bit ancestors used in the original IBM PC and AT. Starting from the 386 and onwards, x86 processors where fully 32bit, featuring virtual memory, priviledge levels, and memory protection.
In 32bit protected mode, memory accesses use 32bit addresses, making it possible to access up to 4GB, which might as well be infinity for how ludicrously large it was back then. Addressing in protected mode then becomes much easier, as a single 32bit value can be used as a linear address; no more of all that segment/offset nonsense.
As I mentioned previously, all x86 processors start in 16bit real mode, and it takes explicit action by the program to switch into the 32bit protected mode of operation. The process of switching to protected mode and setting up a virtual memory system is very interesting, and instructive on the inner workings of the x86 processor and the fundamendals of kernel programming. It is however somewhat complicated, and was almost always delegated to specialized middleware like DOS4G and Tran's PMODE. If you're interested in this subject, see my "Kernel Development from Scratch" series of articles: http://nuclear.mutantstargoat.com/articles/kerneldev/ (unfortunately currently only available in Greek).
Let's write a hello world program to familiarize ourselves with our tools. To keep it from being completely trivial in terms of build process, we'll break it into two source files: main.c, and hello.c. Create a directory named hello (the DOS command for that is md hello), and create the these two source files in there:
void hello(void); int main(void) { hello(); return 0; }
void hello(void) { printf("hello world!\n"); }
Let's compile them manually first, to observe Watcom build process more vividly. First we'll call the watcom 16bit C compiler to compile our source files into object code, then use the linker to combine the object files into an executable:
C:\HELLO>wcc main.c
C:\HELLO>wcc hello.c
C:\HELLO>wlink name hello.exe file { main.obj hello.obj }
The first two steps produce intermediate object files main.obj and hello.obj respectively, which are then fed to the linker to produce the final executable: hello.exe:
C:\HELLO>hello hello world!
If you try these commands yourself, you will notice that wcc prints a lot of messages while compiling our source code, which tends to become annoying very fast. From now on we'll pass the -zq flag to the compiler to instruct it to shut up.