Archive for May, 2009

Apple Lisa Operating System Reference Manual (PDF, 1983)

Tuesday, May 26th, 2009

The Apple Lisa from 1983 was the first consumer-class computer with a graphical user interface and significantly more advanced than the 1984 Macintosh, which had a similar UI, but a comparatively primitive underlying OS. Here, I present a searchable PDF of the rare “Operating System Reference Manual for the Lisa” (1983), as well as a quick overview of the OS and how it compares to UNIX.

“Operating System Reference Manual for the Lisa” (1983)

(PDF, 188 pages, 6.2 MB)

The OS Reference Manual is actually volume 3 of 3 of the Lisa Pascal documentation. As the last page states, the book was typeset on a Lisa and printed on a dot matrix printer.

I also converted the typewriter-written draft version from into a searchable PDF:

“Lisa Operating System Reference Manual” (Draft March 1982)

(PDF, 113 pages, 1.2 MB)

In its spirit, the Lisa Operating System resembles UNIX a lot, and its features and details were pretty much on par with UNIX systems from that time.

Scheduling: Executable files are statically linked with the Pascal runtime, and they make syscalls into the kernel. The system manages processes (with a single thread of execution) with their own address spaces that are cooperatively scheduled (255 levels of priorities). A syscall or a code segment switch can yield the CPU. Processes are managed in a tree, and the death of a parent will kill its children.

Memory Management: There is no paging, but segmented memory (up to 106 code segments and 16 data segments), and explicit swapping of code and data pages. It is possible to write protect data segments.

Inter-Process Communication: Two processes can communicate through shared files on the filesystem, named pipes, event channels (blocking or callback messaging with typed data) and shared data segments. Exceptions are delivered as events.

Filesystem: The filesystem supports 32 character filenames and allows all ASCII characters except for “-”, which is the path separator. A path can be up to 255 characters; absolute paths start with “-” (processes have a working directory). There are no enforced extensions, but the convention is to use extensions for file types. Volumes can be mounted and unmounted, and accessed at the top level by using their device name or their volume name, if they are mounted. The serial (RS232A/RS323B) and parallel (PARAPORT) ports, stdio (MAINCONSOLE) and /dev/null (BITBKT) are
like character-devices and also accessible at the top level that also provide an ioctl-like interface (DEVICE_CONTROL).

File access goes through open/read/write/close. A file is associated with cdate, mdate, adate, a delete protection flag and an up to 128 character label. Apart from regular files, there are pipes, disk-mapped data segments and event channel files.

For safety, the on-disk data structures are very redundant. Every block contains context data like a size, name, filesize, forward/backward link, inode and position in file. Directories (catalogs) and the “medium descriptor data file” are managed just like regular files.

Tripos, the Roots of AmigaDOS

Tuesday, May 19th, 2009

The core of the Amiga Operating systems consists of the three major components Exec (scheduling, memory management, IPC), Intuition (GUI library) and AmigaDOS (process and file management). AmigaDOS is based on the Tripos operating system which Commodore bought because development of their own DOS subsystem failed to meet deadlines. In this article, I am presenting searchable PDFs of the very rare Tripos manuals (638 pages) as well as the AmigaDOS manual (304 pages). Comparing the two documents will share some insight in the relationship between Tripos and Amiga OS.

Tripos Manuals
(PDF, 638 p., 20MB)

The AmigaDOS Manual
(PDF, 304 p., 14MB)

These are the tables of contents of the two documents:

Introduction to Tripos AmigaDOS User’s Manual
1. Simple Use of Tripos 1. Introducing AmigaDOS
2. Editing Files
3. Further Use of Tripos
Tripos User’s Reference Manual
1. Tripos Commands 2. AmigaDOS Commands
2. ED – The Screen Editor 3. ED – The Screen Editor
3. EDIT – The Line Editor 4. EDIT – The Line Editor
Appendix A: Error Codes and Messages Appendix: Error Codes and Messages
Tripos Programmer’s Reference Manual AmigaDOS Developer’s Manual
1. Introduction to Programming 1. Programming on the Amiga
2. Calling the Kernel
3. Calling the DOS 2. Calling AmigaDOS
4. The Macro Assembler 3. The Macro Assembler
5. The Linker 4. The Linker
6. The System Debugger – DEBUG
7. Full Screen Support
8. Floating Point
Appendix: Console Input and Output on the Amiga
Tripos Technical Reference Manual AmigaDOS Technical Reference Manual
1. The Filing System 1. The Filing System
2. Binary File Structure 2. Amiga Binary File Structure
3. Tripos Data Structures 3. AmigaDOS Data Structures
4. Installation
4. AmigaDOS Additional Information for the Advanced Developer

The Original DOS Module of AmigaOS

AmigaOS was architected in a very modular way. “Exec”, the operating system kernel, was responsible for tasks (and therefore scheduling), memory management and the messaging infrastructure. It knew nothing about filesystems or user I/O. The graphics library “Intuition” built on Exec, just like the DOS module, but they were independent of each other. But the DOS module fell behind schedule, so Commodore decided to licenseTripos“.


Tripos is, according to “Introduction to Tripos”, “a multi-processing operating system designed for 68000 computers. Although you can use it as a multi-user system, you normally run Tripos for a single user. The multi-processing facility lets many jobs take place simultaneously. You can also use the multi-processing facility to suspend one job while you run another.

Tripos was very similar to the Amiga OS design and therefore fit well. Tripos consisted of a kernel, a DOS library and a collection of user mode tools. Since the “Exec” was already working well, and Intuition depended on it, there was no sense in replacing it with the Tripos kernel. Instead, only the DOS part of Tripos (including the tools) was integrated into AmigaOS.

So the DOS API, the command line interface with its commands, the executable format (“hunk”) as well as the original Amiga filesystem were all basically identical to Tripos.

Comparing the Manuals

Commodore published a number of reference manuals about the Amiga operating system:

  • AMIGA ROM Kernel Reference Manual: Exec
  • Amiga ROM Kernel Reference Manual: Libraries and Devices
  • Amiga ROM Kernel Reference Manual: Includes and Autodocs
  • Amiga Intuition Reference Manual
  • The AmigaDOS Manual

While the former four books were all written from stratch describing new Amiga technology, the AmigaDOS Manual was basically just the Tripos manual set with “Tripos” replaced with “AmigaDOS” and all references to technologies that didn’t make it into the Amiga removed.

You can see from the tables of contents of the two books that The AmigaDOS User’s Manual omitted the chapters about the Tripos kernel (AmigaOS used “Exec”), the debugger (AmigaOS had “ROMWack”), full screen support (everything above simple character I/O was done by Intuition), floating point (Amiga OS contained Motorola’s “MC68343 FLOATING POINT FIRMWARE” instead) and Installation.

What is interesting about the AmigaDOS Manual is that very limited adaption to the Amiga has been performed. The previously quoted definition of Tripos can be found in the AmigaDOS manual as well: “AmigaDOS a multi-processing operating system designed for 68000 computers. [...]” While this would be true for AmigaOS, AmigaDOS was only a library and in no way a “multi-processing operating system”.

There are not many real differences between the two documents: The AmigaDOS Manual replaced references to “screen” with “current window” and mentions the new logical devices “LIBS:”, “DEVS:” and “FONTS:” when talking about the filesystem layout. There are also some smaller differences on the command line: There is no MOUNT command (AmigaOS detects disks and automounts), the “C” command has been renamed to “EXECUTE” as well as the “PAR:” device to “PRT:”. The AmigaDOS Manual also contains a section about cross-development on a Sun or MS-DOS machine.

Now I am looking forward to your comments: Did you any other interesting differences? In what way were Exec and the Tripos kernel different? Any other input? Thanks!

Reverse-Engineering DOS 1.0 – Part 2: IBMBIO.COM

Tuesday, May 12th, 2009

My last post was about the internals of the DOS 1.0 bootsector. This time, let’s look at the next stage of the DOS 1.0 boot process, the hardware abstraction library IBMBIO.COM.

CP/M and DOS History

Let us first look at the historical background: CP/M was an 8 bit operating system that existed for virtually every computer with an 8080/Z80 CPU. It consisted of the three core components: BIOS, BDOS and CCP. BIOS was the machine abstraction layer that allowed CP/M work on different platforms. BDOS was the platform agnostic core library code, and CCP the command line interpreter.

86-DOS by Seattle Computer Products was a clone of CP/M intended for 8086 computers. It shared the architecture of CP/M, having a separate machine abstraction layer (“DOSIO”). When Microsoft bought 86-DOS and ported it to the upcoming IBM PC (model 5150), they kept this architecture, although there was no need to implement custom drivers, since the IBM PC had all its drivers in its “BIOS” firmware. But IBM’s BIOS did not have the same interface as 86-DOS DOSIO, so PC-DOS 1.0 included a very small DOSIO which would just sit on top of BIOS and using its driver library (and work around some bugs).

So DOS 1.0 for the IBM PC consisted of the three parts IBMBIO.COM (machine abstraction), IBMDOS.COM (DOS library) and COMMAND.COM (command line).

Microsoft soon started licensing MS-DOS to other computer manufacturers that wanted to make IBM PC compatibles. Back then, IBM PC compatible meant being able to run DOS applications and not necessarily sharing the whole system design with the IBM PC, so for MS-DOS to run on other 8086-based systems, it was enough to adapt the hardware abstraction layer to these machines, and therefore Microsoft provided the source code of this part of MS-DOS to hardware vendors. In MS-DOS, the system files are called IO.SYS and MSDOS.SYS.

But since MS-DOS only provided a rather narrow API that did not include, for example, access to bitmap graphics, soon many DOS programs accesses hardware directly, forcing clone makers to make their PC compatibles more and more similar to the IBM PC, eventually using the same support chips and a binary compatible firmware interface. The separation of the kernel in two parts was less and less necessary, so that starting with MS-DOS 5.0, Microsoft only provided a single version of IO.SYS, and in MS-DOS 7.0 (Windows 95), IO.SYS and MSDOS.SYS were merged into IO.SYS.


On DOS 1.0, IBMBIO.COM provides the following library functions to DOS (names taken from 86-DOS DOSIO source):

STATUS check for keypress
INP get key from keyboard
OUTP send character to screen
PRINT send character to printer
AUXIN get character from serial
AUXOUT send character to serial
READ read sector(s) from disk
WRITE write sector(s) to disk
DSKCHG check for disk change

(READ and WRITE will be directly hooked up by DOS into the INT 0×25/0×26 direct disk I/O API later.)

In addition to this, IBMBIO is the next step in the boot process after the bootloader and responsible for

  • initializing the serial and printer ports
  • building a list of floppy drives and its capabilities
  • setting up exception vectors (division by zero etc.)
  • call the IBMDOS init code (already in memory)
  • load and run COMMAND.COM

Let us now look at the library calls it provides:

Serial and Printer

The code to talk to the serial and printer ports is rather straightforward. There is support for a single serial port and a single printer port. IBMBIO sets up the port to 2400 8N1 and has no function to change this setting. I/O will just be passed to the respective BIOS functions, but errors will be evaluated and error messages will be printed in the error case.

Keyboard and Screen

While printing a character just passes the character to BIOS, character input is quite interesting: When reading a character, BIOS returns the ASCII code as well as the raw keyboard scancode. For keys that have no ASCII equivalent like the function or cursor keys, this returns zero as the ASCII code. IBMBIO always returns the ASCII code, but for special keys, it returns two bytes: A zero, indicating that it is a special key, and the BIOS scancode. Therefore in case of a special key, it caches the scancode, returns the zero, and will return the scancode the next time a character is read.

The Control+C/Control+Break handler uses this infrastructure to inject the code “3″ into the input stream.

Disk I/O: Virtual Disks

The library code for Disk I/O is the most interesting part, since it can simulate a virtual disk drive, and it works around two issues of the BIOS function.

DOS supports up to four disk drives, A:, B:, C: and D:, but in case there is only a single drive, it will present two drives to DOS. Since all disk access goes through the READ and WRITE functions of IBMBIO, it can compare the requested disk drive with the disk drive last used, and if it’s the other drive, it will print:

Insert diskette for drive A: and strike
any key when ready

After pressing a key, the actual I/O access is performed. This way, DOS can be completely agnostic about whether there are two physical drives, or a physical and a virtual drive, and “COPY A:FOO B:BAR” will just work. Note that without this feature, it would be impossible to get data from one disk onto another with standard DOS tools.

Disk I/O: Multiple Tracks

The first IBM PC BIOS issue IBMBIO works around is the fact that the original version of BIOS did not support a read or write of sectors across multiple heads. A track always had 8 sectors, and if your read starting at sector 1 of a track, it is possible to read up to 8 sectors, but starting at sector 8, only one sector can be read – if you want to continue reading sectors from the next track, you have to call BIOS again explicitly. The IBMBIO driver therefore breaks up longer reads if they span tracks.

Disk I/O: 8237 DMA Controller Bug

The second problem is actually a design issue with the Intel 8237 DMA controller in the PC. Although the Intel 8088 CPU was internally 16 bits, had 16 bit registers and could support up to 1 MB of RAM, it was the low cost version that was meant to interface to 8 bit support chips. The 8237 is such a support chip intended for 8 bit systems, and therefore only supports 64 KB of memory. Since this would have meant that data from the disk drive can only be read into the lower 64 KB of the PC, IBM extended the DMA controller by adding an external latch per channel to it: You set up the lower 16 bits of the DMA address in the DMA controller, and the upper 4 bits (20 bits correspond to 1 MB) in an external latch, and the upper four address lines will be provided by the latch when the DMA controller accesses memory.

Unfortunately, the 8237 had no “carry out”, so if you set up a DMA to 0x0FFFF (latch: 0×0, DMA controller: 0xFFFF), the address inside the DMA controller will wrap around to 0×0000, but it will not update the upper four bits of the address in the latch. So DMA that spans a 64 KB boundary will end up at the wrong location.

The idea of a device driver is to abstract away details of a device and work around device bugs, but the BIOS in the first PC failed to work around this quirk in the DMA hardware. Therefore IBMBIO works around it by detecting I/O that spans a 64 KB boundary and performing it in a temporary buffer inside IBMBIO.

What is interesting about these workarounds is that DOS 1.0 was the default operating system for the disk-equipped version of the IBM PC, and IBM shipped it with its first machines, so it should have been possible to include these workarounds in BIOS already. In fact, later versions of BIOS did not have these issues any more, but DOS kept supporting the workarounds for a long time.


Here is the assembly source of IBMBIO 1.0. It can be compiled with NASM and produces a binary which is not 100% identical, because of variations in the instruction encoding of different assemblers. The original assembler wasted a few bytes in the encoding, so NOPs have been added to keep the layout identical. The binary is only 1920 bytes. IBMDOS.COM is 6400 bytes, and I might be looking into that one in the future as well.

; DOS 1.0 IBMBIO.COM (disk image MD5 73c919cecadf002a7124b7e8bfe3b5ba)

SECTOR_SIZE     equ     0x0200          ; size of a sector
DOS_SIZE        equ     10000           ; max size of IBMDOS.COM in bytes
PAUSE_KEY       equ     0x7200          ; scancode + charcode of PAUSE key
KEYBUF_NEXT     equ     0x041A          ; next character in keyboard buffer
KEYBUF_FREE     equ     0x041C          ; next free slot in keyboard buffer
KEYBUF          equ     0x041E          ; keyboard buffer data
LOGICAL_DRIVE   equ     0x0504          ; linear address of logical drive byte
SEG_DOS_TEMP    equ     0xE0            ; segment in which DOS was loaded
SEG_DOS         equ     0xB1            ; segment in which DOS will run
SEG_BIO         equ     0x60            ; segment in which BIO is running


                org 0x0000              ; segment 0x0060

                jmp     INIT            ; 0x0060:0x0000 entry point
                jmp     STATUS          ; 0x0060:0x0003 check for keypress
                jmp     INP             ; 0x0060:0x0006 get key from keyboard
                jmp     OUTP            ; 0x0060:0x0009 send character to screen
                jmp     PRINT           ; 0x0060:0x000C send character to printer
                jmp     AUXIN           ; 0x0060:0x000F get character from serial
                jmp     AUXOUT          ; 0x0060:0x0012 send character to serial
                jmp     READ            ; 0x0060:0x0015 read sector(s) from disk (INT 0x25)
                jmp     WRITE           ; 0x0060:0x0018 write sector(s) to disk  (INT 0x26)
                jmp     DSKCHG          ; 0x0060:0x001B check for disk change


                dw SEG_DOS              ; ???
                dw TXT_VERSION          ; ???
TXT_VERSION     db 'BIOS Version 1.00'
                db ' '+0x80
                db '22-Jul-81',0


ERR_PAPER       db 13,10,'Out of pape','r'+0x80,13,10,0
ERR_PRINTER     db 13,10,'Printer faul','t'+0x80,13,10,0
ERR_AUX         db 13,10,'Aux I/O erro','r'+0x80,13,10,0

; check for keypress
;  AL = character
;  Z  = set if no character
;  all other registers preserved
STATUS          mov     al, [cs:next_char]; check for waiting character
                or      al, al
                jnz     char_avail      ; yes, return it
                push    dx
                xchg    ax, dx
                mov     ah, 1
                int     0x16            ; otherwise get key (don't clear)
                jz      status_exit     ; no key
                cmp     ax, PAUSE_KEY   ; PAUSE key?
                jnz     status_exit
                mov     al, 0x10        ; convert into Ctrl+P
                or      al, al

status_exit     mov     ah, dh          ; restore original AH
                pop     dx
char_avail      retf

; Interrupt 0x1B handler: Control+Break handler
int_1B          mov     byte [cs:next_char], 3; put code for Ctrl+C
                iret                    ; into keyboard queue

; Interrupt 0x00 handler: Division by Zero
int_00          sti
                push    ax
                push    dx
                mov     dx, ERR_DIVIDE
                call    print_string
                pop     dx
                pop     ax
                int     0x23            ; exit program through Ctrl+C path

; Interrupt 0x00 handler: Single Step
; Interrupt 0x03 handler: Breakpoint
; Interrupt 0x04 handler: Overflow
iret1           iret                    ; empty interrupt handler


ERR_DIVIDE      db 13,10,'Divide overflo','w'+0x80,13,10,0

; get key from keyboard
;  AL = character
;  all other registers preserved
again           xchg    ax, dx
                pop     dx
INP             mov     al, 0
                xchg    al, [cs:next_char]; get and clear waiting character
                or      al, al
                jnz     inp_exit        ; there is no character waiting
                push    dx
                xchg    ax, dx
                mov     ah, 0
                int     0x16            ; then read character from keyboard
                or      ax, ax
                jz      again
                cmp     ax, PAUSE_KEY
                jnz     not_pause2
                mov     al, 0x10        ; Ctrl+P
not_pause2      cmp     al, 0
                jnz     skip1           ; key with ASCII representation
                mov     [cs:next_char], ah; return scancode next time
skip1           mov     ah, dh          ; restore AH
                pop     dx
inp_exit        retf

; send character to screen
;  AL = character
;  all registers preserved
OUTP            push    bp
                push    ax
                push    bx
                push    si
                push    di
                mov     ah, 0x0E
                cs                      ; XXX makes no sense
                mov     bx, 7
                int     0x10            ; print character
                pop     di
                pop     si
                pop     bx
                pop     ax
                pop     bp

; send character to printer
;  AL = character
;  all registers preserved
PRINT           push    ax
                push    dx
                mov     byte [cs:printer_retry], 0
printer_again   mov     dx, 0           ; printer port #0
                mov     ah, 0
                int     0x17            ; send character to printer
                mov     dx, ERR_PAPER
                test    ah, 0x20
                jnz     printer_error   ; out of paper error
                mov     dx, ERR_PRINTER
                test    ah, 5
                jz      pop_dx_ax_retf  ; no timeout error, return
                xor     byte [cs:printer_retry], 1
                jnz     printer_again   ; on a timeout, try twice
printer_error   call    print_string
pop_dx_ax_retf  pop     dx
                pop     ax

; print zero-terminated string at DS:DX
print_string    xchg    si, dx
prints1         cs lodsb
                and     al, 0x7F        ; clear bit 7 (XXX why?)
                jz      prints2         ; zero-terminated
                call    SEG_BIO:OUTP    ; print character
                jmp     prints1         ; loop
prints2         xchg    si, dx

; get character from serial
;  AL = character
;  all other registers preserved
AUXIN           push    dx
                push    ax
                mov     dx, 0           ; serial port #0
                mov     ah, 2
                int     0x14            ; get character from serial port
                mov     dx, ERR_AUX
                test    ah, 0x0E        ; framing, parity or overrun?
                jz      aux_noerr       ; no error
                call    print_string
aux_noerr       pop     dx
                mov     ah, dh          ; restore AH
                pop     dx

; send character to serial
;  AL = character
;  all registers preserved
AUXOUT          push    ax
                push    dx
                mov     ah, 1
                mov     dx, 0
                int     0x14            ; send character to serial port
                test    ah, 0x80        ; timeout error?
                jz      pop_dx_ax_retf  ; no all fine
                mov     dx, ERR_AUX
                jmp     printer_error

; check for disk change
;  AH = flag (1=changed)
DSKCHG          mov     ah, 0           ; the IBM PC can't detect disk change

; entry point from boot sector
;  assumes DX = 0
INIT            cli
                mov     ax, cs
                mov     ds, ax
                mov     ss, ax
                mov     sp, temp_sector_end; set stack used during init
                xor     ah, ah
                int     0x13            ; reset disk 0 (DX = 0)
                mov     al, 0xA3        ; 2400 8N1
                int     0x14            ; initialize serial port
                mov     ah, 1
                int     0x17            ; initialize printer
                int     0x11            ; get system info
                and     ax, 0xC0        ; number of floppies in bits 6 and 7
                mov     cx, 5
                shr     ax, cl          ; (floppies-1) * 2
                add     ax, 2           ; floppies * 2
                and     ax, 6           ; will become 0 for 4 floppies
                jz      four_floppies   ; 4 floppies (num_floppies pre-assigned with 4)
                cmp     al, 2           ; one floppy?
                jnz     multi_floppy    ; no
                shl     ax, 1           ; pretend we have two, we'll emulate one
                mov     byte [single_floppy], 1
multi_floppy    mov     bx, floppy_list
                add     bx, ax          ; + floppies * 2
                mov     word [bx], 0    ; terminate list with 2 zero words
                mov     word [bx+2], 0
                nop                     ; XXX original assembler wasted a byte
                shr     ax, 1           ; =floppies
                mov     [num_floppies], al
four_floppies   push    ds
                mov     ax, 0
                mov     ds, ax          ; DS := 0x0000
                mov     ax, SEG_BIO     ; target segment for interrupt vectors
                mov     [0x6E], ax      ; set INT 1Bh segment
                mov     word [0x6C], int_1B; set INT 1Bh offset
                mov     word [0x00], int_00; set INT 00h offset
                mov     [0x02], ax      ; set INT 00h segment
                mov     bx, iret1       ; set INT 00h offset
                mov     [0x04], bx      ; set INT 01h offset (empty)
                mov     [0x06], ax      ; set INT 01h segment
                mov     [0x0C], bx      ; set INT 03h offset (empty)
                mov     [0x0E], ax      ; set INT 03h segment
                mov     [0x10], bx      ; set INT 04h offset (empty)
                mov     [0x12], ax      ; set INT 04h segment
                mov     ax, 0x50
                mov     ds, ax          ; DS := 0x0050
                mov     word [0x0], 0   ; clear 0x0500 in DOS Comm. Area (???)
                push    es
                mov     ax, SEG_DOS     ; target segment for IBMDOS.COM
                mov     es, ax
                mov     cx, DOS_SIZE/2  ; size/2 of IBMDOS.COM
                mov     ax, SEG_DOS_TEMP; source segment of IBMDOS.COM
                mov     ds, ax          ; the booloader read whole sectors and puts
                xor     di, di          ; the IBMDOS.COM image right after this;
                mov     si, di          ; so move it down a little
                rep movsw               ; copy 10 000 bytes from 0xE00 to 0xB10
                pop     es
                pop     ds
                mov     si, num_floppies; pass in pointer to structure
                call    SEG_DOS:0       ; init DOS (returns DS = memory for COMMAND.COM)
                mov     dx, 0x0100      ; 0x0100 in COMMAND.COM segment
                mov     ah, 0x1A
                int     0x21            ; set disk transfer area address
                mov     cx, [0x06]      ; remaining memory size
                sub     cx, 0x0100      ; - Program Segment Prefix = bytes to read
                mov     bx, ds
                mov     ax, cs
                mov     ds, ax
                mov     dx, FCB_command_com; File Control Block
                mov     ah, 0x0F
                int     0x21            ; DOS: open COMMAND.COM
                or      al, al
                jnz     error_command   ; error opening COMMAND.COM
                mov     word [FCB_command_com+0x21], 0; random record field
                mov     word [FCB_command_com+0x23], 0;  := 0x00000000
                mov     word [FCB_command_com+0x0E], 1; record length = 1 byte
                mov     ah, 0x27
                int     0x21            ; DOS: read
                jcxz    error_command   ; read 0 bytes -> error
                cmp     al, 1
                jnz     error_command   ; end of file not reached -> error
                mov     ds, bx
                mov     es, bx          ; DS := ES := SS := COMMAND.COM
                mov     ss, bx
                mov     sp, 0x40        ; 64 byte stack in PSP (XXX interrupts are on!)
                xor     ax, ax
                push    ax              ; push return address 0x0000 (int 0x20)
                mov     dx, [0x80]      ; get new DTA address
                mov     ah, 0x1A
                int     0x21            ; set disk transfer area address
                push    bx              ; segment of COMMAND.COM
                mov     ax, 0x0100      ; offset of COMMAND.COM entry
                push    ax
                retf                    ; run COMMAND.COM

error_command:  mov     dx, ERR_COMMANDCOM ; "\r\nBad or missing Command Interprete"
                call    print_string
halt            jp      halt    ; XXX jp instead of jmp


FCB_command_com db 1, 'COMMAND CO','M'+0x80
                times 19h db 0


ERR_COMMANDCOM  db 13,10,'Bad or missing Command Interprete','r'+0x80,13,10,0


; this is passed to IBMDOS.COM
num_floppies    db 4                    ; if there's 1 physical drive, this says 2
floppy_list     dw parameters           ; point to params for every floppy installed; 0-terminated
                dw parameters
                dw parameters
                dw parameters
                dw 0,0

parameters      dw SECTOR_SIZE
                db 1                    ; will be decremented by 1, then used
                dw 1
                db 2
                dw 0x0040
                dw 320                  ; number of total sectors


                times 512-($-temp_sector) db 0


printer_retry   db 0                    ; count for printer retries
next_char       db 0                    ; extra character in keyboard queue
                db 0                    ; XXX unused
single_floppy   db 0                    ; true if we emulate a second logical floppy

; READ  - read sector(s) from disk
; WRITE - write sector(s) to disk
;  al     drive number (0-3)
;  ds:bx  buffer
;  cx     count
;  dx     logical block number
READ            mov     ah, 2           ; BIOS code "read"
                jmp     short read_write
WRITE           mov     ah, 3           ; BIOS code "write"
read_write      push    es
                push    ds
                push    ds
                pop     es              ; ES := DS
                push    cs
                pop     ds              ; DS := CS
                mov     [temp_sp], sp   ; save sp for function abort
                mov     [int_13_cmd], ah; save whether it was read or write
; logic to emulate a "logical" drive B: by prompting the user to change disk
; when the currently used drive is changed
                cmp     byte [single_floppy], 1
                jnz     multi_drive     ; more than one drive
                push    ds
                xor     si, si
                mov     ds, si          ; DS := 0x0000
                mov     ah, al
                xchg    ah, [LOGICAL_DRIVE]; current logical drive
                pop     ds
                cmp     al, ah
                jz      drive_unchanged
                push    dx              ; save block number
                add     al, 'A'
                mov     [TXT_DRIVE], al
                mov     dx, TXT_INSERTDISK
                call    print_string    ; prompt for disk change
                push    ds
                xor     bp, bp
                mov     ds, bp
                mov     byte [KEYBUF_NEXT], KEYBUF & 0xFF
                mov     byte [KEYBUF_FREE], KEYBUF & 0xFF; clear keyboard buffer
                pop     ds
                mov     ah, 0
                int     0x16            ; wait for any key
                pop     dx              ; block number
drive_unchanged mov     al, 0           ; for both logical A: or B: use drive A:
multi_drive     xchg    ax, dx
                mov     dh, 8           ; convert LBA to CHS
                div     dh              ; al = track (starts at 0)
                inc     ah              ; ah = sector (starts at 1)
                xchg    al, ah          ; track and sector
                xchg    ax, cx          ; cx = t/s, ax = count
                mov     [num_sectors], ax; count
                mov     dh, 0
; work around DMA hardware bug in case I/O spans a 64 KB boundary
; by using a temporary buffer
                mov     di, es          ; destination segment
                shl     di, 1
                shl     di, 1           ; make es:bx a linear address
                shl     di, 1           ; (discard upper bits)
                shl     di, 1
                add     di, bx
                add     di, SECTOR_SIZE-1; last byte of sector (linear)
                jc      across_64k      ; sector overflows it
                xchg    bx, di          ; bx = last byte, di = buffer
                shr     bh, 1           ; sector index in memory
                mov     ah, 0x80        ; 0x80 sectors fit into 64 KB
                sub     ah, bh          ; sectors until 64 KB boundary
                mov     bx, di          ; bx = buffer
                cmp     ah, al          ; compare to number of sectors
                jbe     skip2           ; they fit into 64 KB, cap num
                mov     ah, al          ; don't cap number of sectors
skip2           push    ax
                mov     al, ah          ; al = count
                call    rw_tracks
                pop     ax
                sub     al, ah          ; requested = done?
                jz      rw_done         ; yes, exit
across_64k      dec     al              ; one sector less
                push    ax
                push    bx
                push    es              ; save data pointer
                cmp     byte [int_13_cmd], 2
                jz      across_64k_read ; write case follows
                mov     si, bx
                push    cx
                mov     cx, SECTOR_SIZE/2; copy first sector
                push    es
                pop     ds
                push    cs
                pop     es
                mov     di, temp_sector
                mov     bx, di
                rep movsw               ; copy into IBMBIO local data
                pop     cx
                push    cs
                pop     ds
                call    rw_one_sector   ; write last sector
                pop     es
                pop     bx
                jmp     short across_64k_end
across_64k_read mov     bx, temp_sector
                push    cs
                pop     es
                call    rw_one_sector   ; read last sector into temp buffer
                mov     si, bx
                pop     es
                pop     bx
                mov     di, bx
                push    cx
                mov     cx, SECTOR_SIZE/2
                rep movsw               ; copy out
                pop     cx
across_64k_end  add     bh, 2           ; continue 0x0200 after that
                pop     ax
                call    rw_tracks
rw_done         pop     ds
                pop     es
                clc                     ; success

; read/write an arbirtary number of sectors
rw_tracks       or      al, al
                jz      ret2            ; nothing to read
                mov     ah, 9
                sub     ah, cl
                cmp     ah, al          ; more sectors than left in track?
                jbe     skip3           ; no
                mov     ah, al          ; otherwise, read up to end of track
skip3           push    ax
                mov     al, ah
                call    near rw_sectors ; reads/writes up to 8 sectors
                pop     ax
                sub     al, ah          ; decrease sectors to read
                shl     ah, 1
                add     bh, ah          ; advance pointer by sectors * 0x0200
                jmp     rw_tracks       ; continue


int_13_err      xchg    ax, di
                mov     ah, 0
                int     0x13            ; disk reset
                dec     si
                jz      translate       ; retries exhausted
                mov     ax, di
                cmp     ah, 0x80        ; in the "timeout (not ready)" case,
                jz      translate       ; we don't retry (this would take forever)
                pop     ax
                jmp     short retry
translate       push    cs
                pop     es
                mov     ax, di
                mov     al, ah          ; status
                mov     cx, 0x0A
                mov     di, conv_status
                repne scasb
                mov     al, [di+9]
                nop                     ; XXX original assembler wasted a byte
                mov     cx, [num_sectors]
                mov     sp, [temp_sp]   ; clean up stack
                pop     ds
                pop     es
                stc                     ; error

rw_one_sector   mov     al, 1

; reads/writes one or more sectors that are on the same track
rw_sectors      mov     si, 5           ; number of retries
                mov     ah, [int_13_cmd]
retry           push    ax
                int     0x13            ; perform the read/write
                jc      int_13_err
                pop     ax
                sub     [num_sectors], al
                add     cl, al          ; calculate next sector number
                cmp     cl, 8           ; exceeds track?
                jbe     ret2            ; no
                inc     ch              ; next track
                mov     cl, 1           ; sector 1
ret2            retn


TXT_INSERTDISK  db 13,10,'Insert diskette for drive',' '+0x80
TXT_DRIVE       db 'A: and strik','e'+0x80,13,10
                db 'any key when read','y'+0x80,13,10,10,0


conv_status     db 0x80,0x40,0x20,0x10,9,8,4,3,2; BIOS error codes
                db 1,2,6,0x0C,4,0x0C,4,8,0,0x0C,0x0C; IBMBIO error codes


int_13_cmd      db 2    
temp_sp         dw 0    
num_sectors     db 0    


                times 513 db 0
                db 0xC9
                times 126 db 0


Reverse-Engineering DOS 1.0 – Part 1: The Boot Sector

Thursday, May 7th, 2009

The bootsector of DOS 1.0 is celebtaring its 28th birthday today (it contains the timestamp “7-May-81″), so let’s look at it more closely.

Here it is:

00000000  eb 2f 14 00 00 00 60 00  20 37 2d 4d 61 79 2d 38  |........ 7-May-8|
00000010  31 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |1...............|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 fa 8c c8 8e d8 ba 00  00 8e d2 bc 00 7c fb a1  |................|
00000040  06 7c 8e d8 8e c0 ba 00  00 8b c2 cd 13 72 41 e8  |................|
00000050  58 00 72 fb 2e 8b 0e 02  7c 51 bb 00 00 33 d2 b9  |................|
00000060  08 00 be 01 00 56 b0 01  b4 02 cd 13 72 22 5e 58  |................|
00000070  e8 e7 00 2b c6 74 14 fe  c5 b1 01 be 08 00 3b c6  |................|
00000080  73 04 8b f0 eb 01 96 56  50 eb dd 2e ff 2e 04 7c  |................|
00000090  be 44 7d b8 42 7d 50 32  ff ac 24 7f 74 0b 56 b4  |................|
000000a0  0e bb 07 00 cd 10 5e eb  f0 c3 bb 00 00 b9 04 00  |................|
000000b0  b8 01 02 cd 13 1e 72 34  8c c8 8e d8 bf 00 00 b9  |................|
000000c0  0b 00 26 80 0d 20 26 80  8d 20 00 20 47 e2 f3 bf  |................|
000000d0  00 00 be 76 7d b9 0b 00  fc f3 a6 75 0f bf 20 00  |................|
000000e0  be 82 7d b9 0b 00 f3 a6  75 02 1f c3 be f9 7c e8  |................|
000000f0  a5 ff b4 00 cd 16 1f f9  c3 0d 0a 4e 6f 6e 2d 53  |...........Non-S|
00000100  79 73 74 65 6d 20 64 69  73 6b 20 6f 72 20 64 69  |ystem disk or di|
00000110  73 6b 20 65 72 72 6f f2  0d 0a 52 65 70 6c 61 63  |sk erro?..Replac|
00000120  65 20 61 6e 64 20 73 74  72 69 6b 65 20 61 6e 79  |e and strike any|
00000130  20 6b 65 79 20 77 68 65  6e 20 72 65 61 64 f9 0d  | key when read?.|
00000140  0a 00 cd 18 0d 0a 44 69  73 6b 20 42 6f 6f 74 20  |......Disk Boot |
00000150  66 61 69 6c 75 72 e5 0d  0a 00 50 52 8b c6 bf 00  |failur?.........|
00000160  02 f7 e7 03 d8 5a 58 c3  52 6f 62 65 72 74 20 4f  |........Robert O|
00000170  27 52 65 61 72 20 69 62  6d 62 69 6f 20 20 63 6f  |'Rear ibmbio  co|
00000180  6d b0 69 62 6d 64 6f 73  20 20 63 6f 6d b0 c9 00  |m.ibmdos  com...|
00000190  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

DOS 1.0 shipped on a 160 KB single sided disk. The boot code in the IBM PC’s BIOS loaded the first sector into RAM at segment 0×0000, offset 0x7C00 and ran it. Later versions of BIOS checked for 0xAA55 in the last word of the bootsector, but the first version did not. Note that DOS 1.0 is also pre-BIOS Parameter Block, i.e. the bootsector does not contain any information about the physical layout of the disk, since there was only a single disk size.

What the boot sector is supposed to do is read “IBMBIO.COM” and “IBMDOS.COM” into RAM and run them – these are the DOS system files for machine abstraction and DOS API, respectively. In MS-DOS, they would be called “IO.SYS” and “MSDOS.SYS”.

But the DOS 1.0 bootsector takes quite a lot of shortcuts. It assumes it’s always a 40 track, 8 sectors single-sided disk and the two files occupy the first sectors of the data area contiguously – something that SYS.COM could guarantee when making a disk bootable.

So the bootsector first loads the first sector of the root directory (hardcoded to track 0, sector 4) and compares the first two entries with “IBMBIO.COM” and “IBMDOS.COM”. For some reason, the comparison is case-insensitive, although DOS only allows uppercase filenames.

00000600  49 42 4d 42 49 4f 20 20  43 4f 4d 06 00 00 00 00  |IBMBIO  COM.....|
00000610  00 00 00 00 00 00 00 00  f7 02 02 00 80 07 00 00  |................|
00000620  49 42 4d 44 4f 53 20 20  43 4f 4d 06 00 00 00 00  |IBMDOS  COM.....|
00000630  00 00 00 00 00 00 00 00  0d 03 06 00 00 19 00 00  |................|

If they are not there, it prompts the user to replace the disk and tries again. Otherwise, it loads 20 sectors starting from track 0, sector 8 to segment 0×0060, offset 0×0000 into memory and jumps there.

0×60:0×0000 is the same as the linear address 0×0600. On the IBM PC, 0×0000 to 0x03FF are occupied by the interrupts vectors, 0×0400 to 0x4FF is used by BIOS for its variables, and DOS 1.0 uses 0×500 to 0x5FF as the “DOS Communication Area”, so code can start at 0×0600.

00000e00  e9 62 01 e9 6d 00 e9 b2  00 e9 d8 00 e9 e8 00 e9  |................|
00000e10  24 01 e9 3a 01 e9 51 03  e9 52 03 e9 44 01 b1 00  |................|
00000e20  22 00 42 49 4f 53 20 56  65 72 73 69 6f 6e 20 31  |..BIOS Version 1|
00000e30  2e 30 30 a0 32 32 2d 4a  75 6c 2d 38 31 00 0d 0a  |.00.22-Jul-81...|

These are the first few bytes of IBMBIO.COM. Its birthday is about six weeks from now, but I am going to post about its internals next week.

Let us finally look at the complete commented disassembly of the boot sector. It compiles with NASM, but will emit a few bytes differently because of variations in the assembly encoding – but variations in size have been compensated wit NOPs. It is actually quite easy to read.

; DOS 1.0 Boot Sector (disk image MD5 73c919cecadf002a7124b7e8bfe3b5ba)

                org 0x7C00

                jmp     short start


os_numsectors   dw 20                   ; how many sectors to read
os_offset       dw 0                    ; segment to load code into
os_segment      dw 0x60                 ; offset to load code into

                db " 7-May-81",0        ; timestamp
                times 31 db 0           ; padding


start           cli
                mov     ax, cs
                mov     ds, ax          ; DS := CS
                mov     dx, 0
                mov     ss, dx          ; SS := 0000
                mov     sp, 0x7C00      ; stack below code
                mov     ax, [os_segment]
                mov     ds, ax
                mov     es, ax          ; ES := DS := where to load DOS
                mov     dx, 0
                mov     ax, dx
                int     0x13            ; reset drive 0
                jc      disk_error
again           call    check_sys_files ; check for presence of IBMDOS/IBMBIO
                jc      again           ; not found, try another disk
                mov     cx, [cs:os_numsectors]
                push    cx              ; remaining sectors
                mov     bx, 0
                xor     dx, dx          ; drive 0, head 0
                mov     cx, 8           ; track 0, sector 8
                mov     si, 1           ; read 1 sector in first found
                push    si
                mov     al, 1           ; 1 sector
read_loop       mov     ah, 2
                int     0x13            ; read sector(s)
                jc      disk_error
                pop     si              ; sectors read
                pop     ax              ; remaining sectors
                call    add_si_sectors  ; bx += si*512
                sub     ax, si          ; remaining -= read
                jz      done                ; none left
                inc     ch              ; next track
                mov     cl, 1           ; start at sector 1
                mov     si, 8           ; read up to 8 sectors
                cmp     ax, si          ; how many are left to read?
                jae     at_least_8_left ; at least 8
                mov     si, ax          ; only read remaining amount
                jmp     short skip
at_least_8_left xchg    ax, si          ; read 8 sectors this time
skip            push    si              ; number of remaining sectors
                push    ax              ; number of sectors to read this time
                jmp     read_loop       ; next read
done            jmp     far [cs:os_offset]; jump to IBMBIO.COM

disk_error      mov     si, FAILURE     ; string to print
                mov     ax, rom_basic   ; put return address of "int 18" code
                push    ax              ; onto stack

; print zero-terminated string pointed to by DS:SI

print           xor     bh, bh          ; XXX unnecessary
print_loop      lodsb
                and     al, 0x7F        ; clear bit 7 XXX why is it set?
                jz      ret0            ; zero-termination
                push    si
                mov     ah, 0x0E
                mov     bx, 7           ; light grey, text page 0
                int     0x10            ; write character
                pop     si
                jmp     print_loop
ret0            retn

; test for IBMBIO.COM and IBMDOS.COM in the first two directory entries

check_sys_files mov     bx, 0           ; read to address 0 in the DOS segment
                mov     cx, 4           ; track 0, sector 4
                mov     ax, 0x0201
                int     0x13            ; read 1 sector
                push    ds
                jc      non_system_disk ; error case
                mov     ax, cs
                mov     ds, ax          ; DS := CS
                mov     di, 0
                mov     cx, 11          ; convert 11 bytes of first two
to_lower        or      byte [es:di], 0x20; directory entries to lowercase
                or      byte [es:di+0x20], 0x20
                nop                     ; XXX original assembler wasted a byte
                inc     di
                loop    to_lower
                mov     di, 0           ; first entry
                mov     si, IBMBIO_COM
                mov     cx, 11
                rep cmpsb               ; compare first entry with IBMBIO.COM
                jnz     non_system_disk
                mov     di, 0x20        ; second entry
                mov     si, IBMDOS_COM
                mov     cx, 11
                rep cmpsb               ; compare second entry with IBMDOS.COM
                jnz     non_system_disk
                pop     ds
                retn                    ; return with carry clear
non_system_disk mov     si, NON_SYSTEM_DISK
                call    print
                mov     ah, 0
                int     0x16            ; wait for key
                pop     ds
                retn                    ; return with carry set


                db "Non-System disk or disk erro",'r'+0x80
                db 13,10
                db "Replace and strike any key when read",'y'+0x80
                db  13,10,0


rom_basic       int     0x18                ; ROM BASIC


FAILURE         db 13,10
                db "Disk Boot failur",'e'+0x80
                db 13,10,0


add_si_sectors  push    ax              ; bx += si*512
                push    dx
                mov     ax, si
                mov     di, 512
                mul     di
                add     bx, ax
                pop     dx
                pop     ax


                db "Robert O'Rear "

IBMBIO_COM      db "ibmbio  com"
                db 0xB0                 ; XXX unused
IBMDOS_COM      db "ibmdos  com"
                db 0xB0, 0xC9           ; XXX unused


                times 512-($-$$) db 0


The 6502 in “The Terminator”

Tuesday, May 5th, 2009

In the first Terminator movie, the audience sees the world from the T-800′s view several times. It is well-known that in two instances, there is 6502 assembly code on the T-800′s HUD, and many sites have analyzed the contents: It’s Apple-II code taken from Nibble Magazine. Here are HD versions of the shots, thanks to Dominik Wagner:

This is the first assembly snippet:

This is the second assembly snippet:

There are some assembly equates:

On the left, these are the assembled opcodes of the second assembly listing, reaching from “LDY#10″ to “SEC”. On the right, there is output of a run of the checksum application Key Perfect on a file names “OVLY.OBJ”, which prints a 16 bit checksum for every 0×50 bytes: