DOS for modern developers
Yasser wrote his programs for a PC with no browser, no app store and no internet. If you build for the web today, you already know most of the ideas. In 1993 they had other names.
Seven ideas. Each one starts from something you use every day, and each one is a toy you can drive.
Memory is one big array
Picture the whole PC as one JavaScript array of 1,048,576 bytes. Your program, DOS, the screen and the BIOS all live in it, and any of them can write anywhere.
There are no tabs and no sandbox. A browser keeps every tab in its own box. A DOS PC runs one program at a time, in real mode, and lets it touch everything. Drag the white pointer from 0 to 1 MB, then press Write. No error, no permission prompt: one changed byte.
That freedom is what made his programs fast. ARABART paints letters by writing straight into the screen’s memory, the video memory, which starts 640 KB into that array. KVC rewrites the table DOS uses to route its calls, to cut a virus off. The rule was simple: know exactly what lives where.
memory[0xA1F4C]byte 663,372 of 1,048,576
The graphics screen
Not RAM at all: the graphics card answers here. Write a byte and pixels change on the monitor, at once.
In the 640 × 480 mode: pixels 96 to 103 of row 100.
ARABART, PESM, The Brave Sun and both editors draw here.
| 1991 to 1993 | 2026 | Why it is the same idea |
|---|---|---|
| One array of 1 MB, shared by everyone | Each app gets its own private memory | Today the operating system keeps programs apart. |
| Write to A0000h and the screen changes | Write into a canvas’s ImageData | Pixels are numbers in an array that becomes the picture. |
| 640 KB for DOS and your program | The memory one browser tab may use | IBM kept the top 384 KB for the screen and the chips. |
Two numbers make one address
The processor’s numbers stop at 65,535. Memory goes past a million. So every address is two numbers: a base, and an offset from that base.
Think of buffer.subarray(base * 16), then an index into that slice. The base is the segment. The processor multiplies it by 16, which in hex just adds a zero at the end, and adds the offset.
So A000h becomes A0000h, plus 1F4Ch is A1F4Ch: the byte that holds pixel (100, 100) on his 640 × 480 screen. Many pairs reach the same byte, the way two relative paths can name one file. Drag both sliders and watch the address form.
A000:1F4C is address A1F4Ch, in The graphics screen.
memory[(0xA000 << 4) + 0x1F4C] // = memory[0xA1F4C] The graphics screen
The byte for pixel (100, 100) in the 640 × 480 mode: row 100 × 80 bytes, plus 100 ÷ 8.
For engineers: near and far pointers in his programs
One segment is a 64 KB window. A near pointer is 2 bytes, only the offset: an index inside one array. A far pointer is 4 bytes, segment and offset: a reference to any array plus an index into it. Borland C chose the defaults for a whole program with its memory model.
- Tiny, one segment for everything: the Port Said Computer Systems boot screen.
- Small, one segment of code and one of data: WINKEY.
- Compact, near code and far data: KVC.
- Large, far code and far data: The Brave Sun, Digi-Test and both editors. EDITOR.EXE keeps up to 601 lines of 204 bytes, 122,604 bytes of text, more than one window can hold.
The step from segment to segment is 16 bytes, a paragraph. So B800:0000 and B000:8000 are the same byte, B8000h: the first letter of the color text screen.
Event handlers, built into the chip
You know addEventListener. The PC has the same idea in hardware. A table of 256 handlers sits at the very start of memory, with a number for every kind of event.
When a key goes down, the keyboard chip fires interrupt 9. The processor drops what it is doing, looks up entry 9 in the interrupt table, runs the code it finds there, and comes back. Your program never notices.
Programs use the same table to ask for help. Put a function number in a register and run int 21h: a system call into DOS, like a fetch() to the operating system. INT 10h asks the video BIOS, INT 16h the keyboard. Press a button and follow the yellow chip.
Something happens
Your program asks
Or type on your keyboard while a button here has focus.
| Entry | Where | For |
|---|---|---|
| 08h | 0000:0020 | clock tick |
| 09h | 0000:0024 | key press |
| 10h | 0000:0040 | video service |
| 16h | 0000:0058 | keyboard service |
| 21h | 0000:0084 | DOS |
Entry n sits at n × 4. Five of the 256 entries.
- BIOS clock codeon the BIOS chip
- BIOS keyboard codeon the BIOS chip
- Video BIOSon the graphics card
- DOSin memory, loaded at boot
- Keyboard buffer
- empty
- Clock ticks
- 0
- Screen
- Pixels drawn
- 0
- You press Y. The keyboard chip raises its hand: interrupt 9.
- The processor pauses your program and notes where it was, like pushing a frame on the call stack.
- It reads entry 09h of the table, at 0000:0024, and jumps to the address it finds there.
- The BIOS keyboard code reads the key and keeps Y in the keyboard buffer.
- It returns, and your program carries on exactly where it stopped. It never noticed.
const table = new Array(256); // at address 0table[0x08] = biosClock; // filled in at power-ontable[0x09] = biosKeyboard;table[0x10] = videoBios;table[0x21] = dos; // filled in when DOS starts// Events: the hardware calls the table for youclock.on('tick', () => table[0x08]());keyboard.on('key', k => table[0x09](k));// Requests: your program calls it, a system calltable[0x21]({ ah: 0x09, text: 'Hello$' });table[0x10]({ ah: 0x0C, x, y, color: 14 });
| 1991 to 1993 | 2026 | Why it is the same idea |
|---|---|---|
| Interrupt 9, a key press | A keydown event | The hardware calls your handler. Nobody has to keep checking. |
int 21h with a function number | A system call | One numbered door into the operating system. |
| The interrupt table | A map from event names to handlers | Change an entry, and a different function answers. |
For engineers: what int n does, step by step
int n pushes three values on the stack: the flags, CS and IP, which together say where your program was. Then it loads CS:IP from the 4 bytes at 0000:(n × 4), offset first, then segment. iret pops all three back. It is a function call through a pointer table, with the return address saved for you.
DOS function 35h reads an entry and function 25h writes one, so a program can hook a service politely. To stay in memory after it ends, a program calls DOS function 31h or the older INT 27h. ARABART uses INT 27h.
A program that ends, and stays
DOS runs one program at a time. So how do you run a background service? You write a TSR: a program that ends, but stays in memory.
A TSR starts, writes its own address into the interrupt table, then ends. DOS keeps its code in memory. From then on, every program that calls that interrupt reaches the TSR first. Today we would call it a daemon, or middleware in front of an API: it can answer, or call next().
His ARABART is one. Run it once and 30,110 bytes stay in memory, five fonts included, in front of the video service. A call for its own function, number 36h, gets Arabic. Any other call passes on, untouched, in three instructions. Run it below, run DEMO.EXE, then try to run ARABART twice.
ARABART stays in memory after it ends, so every program after it can call it.
C:\ARABART>ARABART
ARABIC-ART Ver 4.15 Copyright (c)1993 YASSER MOHAMED ALI.
C:\ARABART>DEMO
DEMO.EXE writes Arabic through ARABART, so run ARABART first.
- A callint 10h
- ARABARTis it 36h?
- Video BIOSthe card’s own code
// ARABART, written as middleware in front of the video servicevideoService.use((call, next) => {if (call.ah !== 0x36) return next(call); // not mine: pass it ondrawArabic(call); // 36h: join and draw});// It runs once. The program ends; the handler stays.
Before it loads, ARABART reads the video service’s entry and looks for its own name tag, DALYASSER. If the tag is already there, it prints “This utility is already active in your RAM.” and stops. One copy is enough. In modern terms, that is a single-instance lock.
Sixteen colors, four layers
On a canvas, a pixel is four bytes: red, green, blue and alpha. On his VGA screen, a pixel is four bits, and each bit lives in a different layer.
The 640 × 480 mode shows 307,200 pixels in 16 colors. The card keeps them as four bit planes of 38,400 bytes: blue, green, red, and one that makes a color bright. All four answer at the same address, A0000h, like four layers of one image in a design tool. Stack the four bits of a pixel and you get its color number, from 0 to 15.
Tap a pixel of the word to pick its color.
Decimal 14 Hex Eh
14 yellow
- 00000, black
- 00011, blue
- 00102, green
- 00113, cyan
- 01004, red
- 01015, magenta
- 01106, brown
- 01117, light gray
- 10008, dark gray
- 10019, light blue
- 101010, light green
- 101111, light cyan
- 110012, light red
- 110113, pink
- 111014, yellow
- 111115, white
The word is drawn by his own ARABART engine, ported pixel for pixel. Flip a color’s bits and watch which planes carry it. Switch a plane off and every color on the screen loses that bit at once.
Planes are also why his drawing code is fast. A program can tell the card one color for all four planes, then paint up to eight pixels with a single write. ARABART does exactly that, with the card’s Set/Reset register. See it on the ARABART page.
An EXE is a manifest, the code, and a list of fixes
A .COM file is raw code that DOS copies into memory and runs, like a plain script. An .EXE starts with a header: a small manifest, much like package.json.
The header says where the program starts, where its stack goes and how much memory it wants. Then it lists fix-ups. A program cannot know where DOS will put it, yet some instructions need real addresses. So the linker writes down every spot that holds one, and DOS adds the real load address to each. A browser does the same when it resolves a relative URL against the page.
Here is his antivirus, KVC.EXE, unpacked. Seven of its instructions hold the same number, 0378h: where its data starts, counted from its own first byte. Step through the load, then drag the load address and watch all seven follow. Bigger programs have more to fix: The Brave Sun lists 1,746. And every Windows .exe you download today still begins with the same two letters, MZ.
{
"magic": "MZ", // I am an EXE
"fixups": 7, // listed after this header
"start": "0000:0000", // counted from my load address
"stack": "0406:0100", // counted from it too
"memory": "all of it" // as much as DOS can give
} - 0000
mov dx,0378h1378h - 005A
mov di,0378h1378h - 009F
mov ax,0378h1378h - 00C9
mov ax,0378h1378h - 0103
mov ax,0378h1378h - 0D31
mov ax,0378h1378h - 0FD3
mov ax,0378h1378h
Starts at 1000:0000, stack at 1406:0100
Last, DOS points the stack where the header said and jumps to the start, counted from the same load address. KVC runs, sure of where its data is.
// Only the loader knows the start, so it patches each listed spot:
for (const at of header.fixups)
image.setUint16(at, image.getUint16(at, true) + loadSegment, true);
// Today the same job: a URL resolved against where the code loaded
new URL('./data.json', import.meta.url); Zip for programs, unzipped in memory
Every one of his program files is packed with DIET 1.00: compressed, with a small unpacker at the front. Run it, and the unpacker rebuilds the real program in memory, then jumps to its first instruction.
Think of a self-extracting zip that never touches the disk, or gzip on a JavaScript bundle where the code unzips itself. The method is the classic LZ77 kind: store a byte as it is, or say “go back this far and copy that many”. Every repeat becomes one small instruction.
Step through it on the Basmallah, stored the way his program PESM stores it: in the keys of an Arabic keyboard, so fsl is بسم. His engine draws the Arabic as each key comes back. On floppy disks of 360 KB to 1.44 MB, every byte saved was room for more.
-
fcopy as is -
scopy as is -
lcopy as is -
␣copy as is -
hcopy as is -
gcopy as is -
gcopy as is -
icopy as is -
←5 ×3go back 5, copy 3 -
vcopy as is -
pcopy as is -
lcopy as is -
kcopy as is -
←7 ×5go back 7, copy 5 -
dcopy as is -
lcopy as is
16 instructions: 14 letters stored as they are, and 2 copies of letters already rebuilt.
- f
- s
- l
- ␣
- h
- g
- g
- i
- ␣
- h
- g
- v
- p
- l
- k
- ␣
- h
- g
- v
- p
- d
- l
All 22 keys are back: بسم الله الرحمن الرحيم. Notice the copy that rebuilt hgvp: الرح, shared by الرحمن and الرحيم.
for (const t of packed) {
if (t.literal) out.push(t.byte);
else for (let i = 0; i < t.len; i++)
out.push(out[out.length - t.dist]);
}
jumpTo(out); // then run the program it rebuilt - MAIN.EXEDigi-Test 79,358→ 202,720
- SUN.EXEThe Brave Sun 47,709→ 93,792
- EDIT.EXEText Editor: Sample GUI 36,526→ 79,200
- NAMES.COMKemyam: Group Names 13,090→ 59,402
- PORTSAID.EXEPort Said Computer Systems 18,715→ 47,232
- EDITOR.EXEText Editor: Demo 20,565→ 37,392
- ARABART.EXEARABART 11,183→ 30,144
- DEMO.EXEARABART: Demo 18,129→ 28,128
- EDITOR.EXEARABART: Editor 17,492→ 27,088
- DEMO.COMArabic Commercial 4,864→ 25,899
- ARAB-ART.EXEThe Brave Sun 2,061→ 22,240
- KVC.EXEKVC: Kemyam Virus Capture 11,000→ 16,928
- FORM_X.EXEFormula Solver 3,546→ 9,744
- WINKEY.EXEWINKEY 4,220→ 7,056
- PESM.EXEArabic Basmallah 3,739→ 6,144
- SCREEN.COMPort Said IT Department 1,672→ 3,504
- TAHER.COMTAHER 2,050→ 2,346
All together: 295,919 bytes on disk become 698,959 in memory.
This is also why the site had to unpack each program before it could read it. Every Byte shows both layers of every file: the packed bytes on the disk, and the program DIET rebuilds in memory.
See every idea at work
Each idea on this page runs inside one of his programs, and every program runs in your browser.
- KVCThe interrupt table at work: a virus caught by asking it a question only it answers.
- ARABARTA background service that every program calls to write Arabic.
- Arabic BasmallahThe 640 × 480 screen and its four color layers, written by hand in 1991.
- Port Said IT DepartmentA COM file that draws every pixel through the video BIOS.
- Formula SolverPure assembly and the text screen, in 3,546 bytes on disk.
- The Brave SunBorland C++, and 1,746 fix-ups for DOS to apply.
- Every ByteAll of his files, packed and unpacked, down to the single byte.