Every program DOS runs starts with a 256-byte program segment prefix (PSP), which DOS builds just before the program's code. A .COM's code starts at offset 100h because the PSP fills offsets 0 to FFh of the same segment. INT 21h AH=62h (DOS 3 and later) returns the PSP's segment in BX; the older 51h does the same.

In a .COM, CS, DS, ES and SS all hold the PSP segment at start, so 62h is not needed. In an .EXE, DS and ES point at the PSP only at start: once DS is loaded with @DATA, the PSP is reached by having saved ES first, or by asking DOS.

Layout

OffsetSizeContents
00h2 bytesCD 20: INT 20h, so a jump to PSP:0000 ends the program
02hwordthe segment just past the program's memory block
05h5 bytesa far call into DOS, for CP/M-style programs
0AhdwordINT 22h: where DOS goes when the program ends (the parent)
0EhdwordINT 23h: the Ctrl+C handler to restore
12hdwordINT 24h: the critical error handler to restore
16hwordthe parent's PSP segment
18h20 bytesthe handle table: one byte per handle, FFh = unused
2Chwordthe environment's segment
2EhdwordSS:SP saved by DOS on the last INT 21h
32hwordthe number of handles
34hdwordthe address of the handle table (normally PSP:18h)
50h3 bytesINT 21h, RETF: another way to call DOS
5Ch16 bytesFCB 1: the first argument, parsed as a file name
6Ch20 bytesFCB 2: the second argument
80hbytethe length of the command tail
81h127 bytesthe command tail, ending with 0Dh; the default DTA
prints: PSP segment = CS / PSP:0000 = CD20 (INT 20h) / command tail: 0 bytes (more when the program is given arguments)
MOV AH, 62h ; DOS: the PSP segment in BX
INT 21h
MOV ES, BX ; ES:0000 = the PSP
MOV DX, OFFSET SAMEM1
MOV AX, CS
CMP AX, BX ; in a .COM they are the same
JE SAY1
MOV DX, OFFSET OTHERM1
SAY1:
MOV AH, 09h
INT 21h
MOV DX, OFFSET FIRSTM1
MOV AH, 09h
INT 21h
XOR SI, SI
MOV AX, ES:[SI] ; the first two bytes: CDh, 20h
XCHG AL, AH ; in reading order
CALL PHEX
MOV DX, OFFSET INT20M1
MOV AH, 09h
INT 21h
MOV DX, OFFSET TAILM1
MOV AH, 09h
INT 21h
MOV AL, ES:[SI+80h] ; the length of the command tail
MOV AH, 0
CALL PDEC
MOV DX, OFFSET BYTESM1
MOV AH, 09h
INT 21h
PUSH CS
POP ES
SAMEM1 DB "PSP segment = CS", 13, 10, "$"
OTHERM1 DB "PSP segment is not CS", 13, 10, "$"
FIRSTM1 DB "PSP:0000 = $"
INT20M1 DB " (INT 20h)", 13, 10, "$"
TAILM1 DB "command tail: $"
BYTESM1 DB " bytes", 13, 10, "$"
PHEX PROC NEAR ; print AX as four hex digits (changes AX BX CX DX)
MOV BX, AX
MOV CX, 4
PHEX1:
PUSH CX
MOV CL, 4
ROL BX, CL ; the top digit to the bottom
POP CX
MOV DL, BL
AND DL, 0Fh
ADD DL, "0"
CMP DL, "9"
JBE PHEX2
ADD DL, 7 ; "A" to "F"
PHEX2:
MOV AH, 02h
INT 21h
LOOP PHEX1
RET
PHEX ENDP
PDEC PROC NEAR ; print AX in decimal (changes AX BX CX DX)
MOV BX, 10
XOR CX, CX ; digits on the stack
PDEC1:
XOR DX, DX
DIV BX ; AX = AX / 10, DX = the last digit
PUSH DX
INC CX
OR AX, AX
JNZ PDEC1
PDEC2:
POP DX ; the digits come back first to last
ADD DL, "0"
MOV AH, 02h
INT 21h
LOOP PDEC2
RET
PDEC ENDP

The command tail

PSP:80h holds the length and PSP:81h the text typed after the program's name, starting with the space that separated them and ending with a carriage return that the length does not count. The shell takes out redirections (>OUT.TXT, <IN.TXT, pipes) before the program sees the line, but leaves wildcards and capitals as typed. The default DTA lies over the same bytes (see INT 21h AH=1Ah), so read the tail before any find first.

The environment at PSP:2Ch is a list of NAME=value strings, each ending with a 0 byte, and an empty string. From DOS 3 on, a word (usually 1) and the program's own full path follow it: how a program finds the directory it was started from.