Trust is earned, not given

A different perspective

2019-10-12 · Projects

Classic PC Assembly Language: dissecting the DOS copy command

Every DOS user knows COPY LETTER.TXT BACKUP.TXT — one line, file duplicated. But that innocent command touches nearly every important idea in classic PC assembly language: CPU registers, interrupts, the filesystem, buffers, and error handling with processor flags. In this article we open it up and rebuild it ourselves, with every line commented. No prior assembly experience needed — if you can read any programming language, you can read this.

The ground rules: registers, interrupts, and INT 21h

An 8086-class CPU has a handful of 16-bit registers — think of them as named variables that live inside the chip itself:

RegisterConventional job
AX (split into AH/AL)the accumulator — function numbers and arithmetic results
BXbase — in DOS, the file handle goes here
CXcounter — byte counts for reads and writes
DXdata — pointers to strings and buffers

DOS offers its services through software interrupts. The granddaddy is INT 21h: you put a function number in AH, parameters in the other registers, execute int 21h, and DOS does the work, returning results in registers and the carry flag (CF) — which is set when something failed. That "number in AH, call INT 21h" rhythm is 90% of DOS programming.

The four DOS calls behind copy

Function (AH)What it doesInputsReturns
3Dhopen an existing fileAL=0 (read), DS:DX→filenameAX=handle, or CF set + error code
3Chcreate/truncate a fileCX=attributes, DS:DX→filenameAX=handle, or CF set
3Fhread from a handleBX=handle, CX=max bytes, DS:DX→bufferAX=bytes actually read (0 = end of file)
40hwrite to a handleBX=handle, CX=bytes, DS:DX→dataAX=bytes written, or CF set

Filenames are ASCIIZ strings: the characters followed by a zero byte, e.g. 'LETTER.TXT',0. The zero marks the end, because DOS has no idea how long your string is otherwise.

A minimal file copier, in full (MASM syntax)

Here is the whole program — open, loop read/write, close, exit. Comments explain each line.

; COPY.ASM — a minimal file copier for MS-DOS (real-mode x86)
; Assemble: MASM COPY; LINK COPY;   Run: COPY

        .model small            ; one code segment + one data segment
        .stack 100h             ; 256 bytes of stack

        .data
srcName  db 'LETTER.TXT',0      ; ASCIIZ source filename
dstName  db 'BACKUP.TXT',0      ; ASCIIZ destination filename
srcHandle dw ?                  ; 16-bit handle DOS gives back on open
dstHandle dw ?
buffer   db 32768 dup(?)        ; 32 KB staging area — the secret of every copier
errMsg   db 'Copy failed.$'     ; '$' terminates strings for DOS print
okMsg    db '1 file(s) copied.$'

        .code
start:
        mov ax, @data           ; the data segment doesn't load itself —
        mov ds, ax              ; point DS at our variables first

; ---------- open the source file ----------
        mov ah, 3Dh             ; DOS function 3Dh: open file
        mov al, 0               ; access mode 0 = read only
        mov dx, offset srcName  ; DX:offset of the ASCIIZ name (DS already set)
        int 21h                 ; call DOS
        jc  failed              ; carry flag set? the open failed — bail out
        mov srcHandle, ax       ; success: AX holds the handle; save it

; ---------- create the destination file ----------
        mov ah, 3Ch             ; DOS function 3Ch: create/truncate
        mov cx, 0               ; normal file attributes
        mov dx, offset dstName
        int 21h
        jc  failed
        mov dstHandle, ax       ; save the destination handle too

; ---------- the copy loop ----------
copyLoop:
        mov ah, 3Fh             ; DOS function 3Fh: read from handle
        mov bx, srcHandle       ; BX = handle to read from
        mov cx, 32768           ; ask for a full 32 KB
        mov dx, offset buffer   ; where to put the bytes
        int 21h
        jc  failed
        mov cx, ax              ; AX = bytes ACTUALLY read (may be less!)
        jcxz done               ; CX = 0 means end of file — we're finished

        mov ah, 40h             ; DOS function 40h: write to handle
        mov bx, dstHandle       ; BX = handle to write to
        mov dx, offset buffer   ; write exactly what we just read
        int 21h                 ; CX still holds the byte count
        jc  failed
        jmp copyLoop            ; go back for the next chunk

; ---------- cleanup ----------
done:
        mov ah, 3Eh             ; DOS function 3Eh: close handle
        mov bx, srcHandle
        int 21h
        mov ah, 3Eh
        mov bx, dstHandle
        int 21h

        mov ah, 09h             ; print the success message ('$'-terminated)
        mov dx, offset okMsg
        int 21h
        mov ax, 4C00h           ; DOS function 4Ch: exit, return code 0
        int 21h

failed:
        mov ah, 09h             ; print the error message
        mov dx, offset errMsg
        int 21h
        mov ax, 4C01h           ; exit with return code 1 (signals error)
        int 21h

        end start

The ideas hiding in those thirty lines

Typing it live with DEBUG

You didn't need a compiler to try assembly in 1990 — every DOS disk shipped with DEBUG.EXE, which assembles instructions straight into memory. Here is a real session that prints the letter A:

C:\> debug
-a 100                      ; start assembling at offset 100h (COM programs live here)
1373:0100 mov ah,02         ; DOS function 02h: print one character
1373:0102 mov dl,41         ; DL = 41h = ASCII 'A'
1373:0104 int 21            ; call DOS
1373:0106 int 20            ; classic "exit to DOS" for COM programs
1373:0108                   ; empty line = stop assembling
-g                          ; GO — run what we just typed
A                           ; the program's entire output
Program terminated normally
-                           ; back at DEBUG's prompt; 'q' quits

Numbers are hex by default (41 means 41h), addresses show as segment:offset, and the program runs from the very memory you typed it into. The same -u (unassemble) command would let you watch COMMAND.COM's own code the other direction.

What the real COPY does that ours doesn't

COMMAND.COM's version is our loop plus patience: it parses wildcards and walks directory matches with FindFirst/FindNext (functions 4Eh/4Fh), treats CON and PRN as file names (the OS makes devices look like files — the same handles, the same read/write calls), concatenates multiple sources into one destination, honours the /V verify flag (read-back after write), and streams through a buffer sized from free memory. The bones are identical to our 30 lines; everything else is convenience layered on top.

Where to go next

Read the listing until jcxz done feels inevitable, then find an emulator (DOSBox runs DEBUG and MASM happily on any modern machine) and type the DEBUG program yourself. After that, natural next stops are the directory-search calls behind wildcards, and INT 13h — the BIOS disk services one layer below DOS, where "files" stop existing and only sectors remain. Every idea in modern I/O — buffers, handles, partial reads, error codes — started right here.