comment |

	This program unravels DOS's (on most machines, Versions 2.0 up to 3.3)
	memory control blocks (MCBs) and prints the information it finds.
	If run under DOS 3.0 or later, it will also print the name it can
	find for each program in memory. The commenting and wording of
	printing messages have been changed in a number of places.

	DOS allocates blocks in 16-byte paragraphs, so shift the Block size
	reported by the program 4 bits left (multiply by 16!) to determine
	the actual block length in bytes.

	Microsoft has never officially documented how MSDOS allocates memory,
	so much of this program is based on techniques others have discovered.

	MCB format:

	  Byte Offset			Description
	-----------------------------------------------------------------------

	      0				contains byte 'M' if other MCBs higher
					in memory, 'Z' if highest current MCB.

	    1 - 2			PID of block owner (identical to seg
					addr of the program's program segment
					prefix [PSP]).

	    3 - 4			length of block in 16-byte paragraphs.


	Written for MASM 5.0 by Hardin Brothers for PCResource. The original
	appeared in the July 1988 issue of their magazine. Entire contents of
	the July 1988 issue (C) Copyright 1988 by
				IDG Communications/Peterborough, Inc.

	Hardin Brothers is a freelance programmer and technical writer. Write
	to him at 280 N. Campus Ave., Upland, CA  91786. Enclose a self-
	addressed, stamped envelope for a reply.

	|

LF	equ	0Ah	; linefeed char
CR	equ	0Dh	; carriage return char
STDOUT	equ	1	; standard output device

PRINT	macro	text
	mov	AH, 40h		; INT 21h service 40h: write to device/file
	mov	BX, STDOUT	; send message to std output
	lea	DX, &text&msg	; DS:DX gets address of message string Xmsg
	mov	CX, &text&len	; get length of message
	int	21h		; call DOS service
	endm

EXIT	macro	val
	mov	AH, 4Ch		; INT 21h service 4Ch: exit from program
				; also returns a value. replaces old INT 20h.
	mov	AL, val		; return value byte
	int	21h
	endm

	.MODEL	SMALL
	.STACK
	.DATA
ourpid		dw	?
maxmem		dw	?
basepsp		dw	?
DosVer		dw	?
DosV3		db	0

NoV1msg		db	'This program cannot be used with DOS Ver. 1.x', CR, LF
NoV1len		equ	$-NoV1msg

VERmsg		db	'Unknown DOS version -- cannot find head of memory'
		db	' chain.', CR, LF
VERlen		equ	$-VERmsg

LERRmsg		db	"There apparently in an error with this program's"
		db	' MCB.', CR, LF
LERRlen		equ	$-LERRmsg

ERRmsg		db	'Memory allocation error -- Illegal MCB found.', CR, LF
ERRlen		equ	$-ERRmsg

HDRmsg		db	CR, LF, CR, LF
		db	'  MCB     Block     Block     Block     Block'
		db	'     Process', CR, LF
		db	'  Seg     Seg       Owner     Size      Type '
		db	'     Name   ', CR, LF
		db	' --------------------------------------------'
		db	'------------', CR, LF
HDRlen		equ	$-HDRmsg

mcbmsg		db	'  '
mcbadr		db	'0000    '
blkadr		db	'0000      '
blkown		db	'0000      '
blksiz		db	'0000      '
mcblen		equ	$-mcbmsg

CRLFmsg		db	CR, LF
CRLFlen		equ	$-CRLFmsg

PRGmsg		db	'Prog    '
PRGlen		equ	$-PRGmsg

ENVmsg		db	'Envr    '
ENVlen		equ	$-ENVmsg

OTHmsg		db	'Data?   '
OTHlen		equ	$-OTHmsg

hexlist		db	'0123456789ABCDEF'

memMSG		db	CR, LF, CR, LF
		db	'Top of memory: '
memSIZ		db	'0000h.', CR, LF
		db	'Next program will load at: '
memLOC		db	'0000h.', CR, LF, CR, LF
		db	'Memory available: '
memAVL		db	'00000h bytes.', CR, LF, CR, LF
memlen		equ	$-memMSG

	.CODE
start:	cld
	mov	AX, @data
	mov	DS, AX		; set DS to "our" data seg
	mov	AH, 30h		; INT 21h service 30h: Get DOS Version
	int	21h
	mov	DosVer, AX	; save entire version number
	cmp	al, 2		; at least DOS version 2.x ?
	jae	verOK		; if so, continue...
	PRINT	NoV1		; else complain about it
	EXIT	-1		; and quit

verOK:	sub	AL, 2		; test for version 3.x
	mov	DosV3, AL	; save result (0 or >0) as a flag

	mov	BX, 2		; addr PSP:2 contains maxmem
	mov	AX, ES:[BX]
	mov	maxmem, AX
	push	ES
	mov	AX, ES		; get addr of "our" PSP
	mov	ourpid, AX	; save PSP
	dec	AX		; point to "our" MCB
	mov	ES, AX		; ES now points to our MCB
	mov	BX, 1		; offset of PID in our MCB
	inc	AX		; AX contains our PID again

	cmp	AX, word ptr ES:[BX]
	jz	okay		; if not both the same, something's wrong!
	PRINT	LERR
	EXIT	-1

okay:	pop	ES		; ES points to our PSP
	call	findStart	; get head of memory chain

;----------------
;	ES now points to the PSP in lowest memory, which ordinarily belongs
;	to the first and only copy of COMMAND.COM. This PSP will be used as
;	the place to begin the trek through memory.
;----------------

	PRINT	HDR		; output table column headers
rB1:	push	ES		; save ES
	call	printMCB	; print memory block info
	mov	AX, ES		; put MCB seg in AX
	inc	AX		; AX contains block seg
	mov	ES, AX		; ES points to memory block
	call	printTYPE	; print block type
	jc	rB2		; skip to rB2 if type not ENVR
	test	DosV3, -1	; using Version 3.x or later?
	jz	rB2		; skip to rB2 if not
	call	printCMD	; print program name if found

rB2:	PRINT	CRLF		; terminate output line
	pop	ES		; ES points to current MCB
	mov	BX, 0		; addr ES:BX points to block type (M or Z)

	cmp	byte ptr ES:[BX], 'Z'	; at tail of memory chain?
	jz	rB3			; skip to rB3 if at tail
	mov	AX, ES		; get current MCB seg
	inc	AX		; AX contians block seg
	mov	BX, 3		; addr ES:BX points to block size
	add	AX, ES:[BX]	; addr block size
	mov	ES, AX		; ES points to next MCB
	jmp	rB1		; traverse chain. loop back.

rB3:	call	printMEM	; print memory info
	EXIT	0		; and terminate program

;----------------
;	Find the head of the MCB list.
;	This is the trickiest part of the program, as it is rarely
;	documented. Method used was created by Ted Mirecki, and was published
;	in the October 1987 issue of PC Tech Journal.
;----------------

findStart	proc	near
	mov	AX, 0		; start with seg 0
	mov	ES, AX		;  in ES
	mov	BX, 0C3h	; find seg of resident DOS
	mov	AX, ES:[BX]	; put the seg in AX
	mov	ES, AX		; ES points to resident DOS seg
	mov	AX, DosVer	; get DOS Version
	xchg	AH, AL		; Ver major in AH, minor in AL
	mov	BX, 10Ah	; offset for Ver 2.0
	cmp	AX, 0209h	; is this Ver 2.00 - 2.09 ?
	jbe	fS2		; skip to fS2 if so
	mov	BX, 0F6h	; offset for Ver 2.1
	cmp	AX, 0213h	; is this Ver 2.10 - 2.19 ?
	jbe	fS2		; skip to fS2 if so
	cmp	AL, 2		; is this some unknown Ver 2.x ?
	jne	fS1		; skip to fS1 if not
	PRINT	VER
	EXIT	-1

fS1:	mov	BX, 128h	; offset for Ver 3.0
	cmp	AX, 0309h	; is this Ver 3.00 - 3.09 ?
	jbe	fS2		; skip to fS2 if so
	mov	BX, 22h		; offset for Ver 3.10 - 3.30

fS2:	les	BX, ES:[BX]	; get head of memory chain
	ret
findStart	endp

comment	|
;----------------
;	This is an alternate method of finding the head of the MCB list.
;	This method will trace backwards to the last copy of COMMAND.COM
;	installed in memory, and should work if the preceding method does
;	not with your version of MSDOS.
;----------------

findStart	proc	near
	mov	BX, 16h		; addr PSP:16h points to parental PSP
fS1:	mov	AX, ES:[BX]	; get addr of parental PSP
	cmp	AX, 0		; at tail of chain ? (1st test)
	jz	fS2		; skip to fS2 if so
	cmp	AX, basepsp	; at tail of chain ? (2nd test)
	jz	fS2		; skip to fS2 if so
	mov	basepsp, AX	; if not, save this value
	mov	ES, AX		; ES points to parental PSP
	jmp	fS1		; loop back. traverse chain.

fS2:	mov	AX, ES		; get current PSP seg
	dec	AX		; AX is seg of MCB
	mov	ES, AX		; ES points to head of memory chain (1st MCB)
	ret
findStart	endp
	|

;----------------
;	ES points to a MCB. Print the information contained in the MCB.
;----------------

printMCB	proc	near
	mov	AX, ES		; AX is seg of MCB
	lea	DI, mcbadr	; DI points to message area
	call	hextoasc	; convert AX to ASCII
	inc	AX		; AX is block addr
	lea	DI, blkadr
	call	hextoasc
	mov	BX, 0		; is this really a block ?
	mov	AL, ES:[BX]	; get first byte
	cmp	AL, 'M'		; part of chain ?
	je	pM1		; skip to pM1 if so
	cmp	AL, 'Z'		; tail of chain ?
	je	pM1		; skip to pM1 if so
	PRINT	ERR		; else notify error
	EXIT	-1		; and terminate program

pM1:	mov	BX, 1		; addr ES:BX points to block PID
	mov	AX, ES:[BX]	; get block PID
	lea	DI, blkown
	call	hextoasc
	mov	BX, 3		; addr ES:BX points to block size
	mov	AX, ES:[BX]	; get block size
	lea	DI, blksiz
	call	hextoasc
	PRINT	mcb
	ret
printMCB	endp

;----------------
;	ES points to a memory block. Determine if it is a PSP, ENVR, or
;	unknown block. Note that the test for a PSP block checks if the
;	first 4 bytes of the block are ASCII, so a data block could con-
;	ceivably show up as a PSP.
;----------------

printTYPE	proc	near
	mov	BX, 0		; addr ES:BX points to head of block

	cmp	byte ptr ES:[BX], 0CDh	; a PSP always starts at CDh
	jnz	typ1		; no PSP here, skip to typ1
	PRINT	PRG
	stc			; set carry flag
	ret

typ1:	mov	CX, 4		; test count of 4 chars
typ2:	mov	AL, ES:[BX]	; get a byte
	cmp	AL, ' '		; check for CTRL code
	jb	typ3		; skip to typ3 if so
	cmp	AL, 'a'		; check lowercase
	jae	typ3		; skip to typ3 if so
	inc	BX		; otherwise point to next byte
	loop	typ2		; and continue
	PRINT	ENV		; must be ENVR block type
	clc			; clear carry flag
	ret

typ3:	PRINT	OTH		; unknown block type
	stc			; set carry flag
	ret
printTYPE	endp

;----------------
;	ES points to an environment segment. Scan the segment and print the
;	name of the program reported to own this environment. The procedure
;	will only work with DOS Ver 3.0 and later.
;----------------

printCMD	proc	near
	push	DS		; save data seg
	push	ES		; copy ES
	pop	DS		;   to DS
	mov	SI, 0		; addr DS:SI points to environ text
	mov	CX, 8000h	; max. size of environ = 32768 bytes
	mov	AX, 0		; init AX for search

pC1:	lodsb			; get byte in AL, incr SI
	or	AX, AX		; set flags. test AX.
	jz	pC2		; AX empty, found start of CMD
	mov	AH, AL		; AX not empty, save byte
	loop	pC1		; and get next byte

pC1a:	pop	DS		; nothing found. restore DS
	ret

pC2:	lodsw			; get next word in AX, incr SI
	cmp	AX, 1		; ought to be 0001h
	jne	pC1a		; skip to pC1a if not
	mov	DX, SI		; save start addr
	mov	CX, 0		; ready byte count

pC3:	lodsb			; get next byte, incr SI
	inc	CX		; incr byte count
	cmp	AL, ' '		; end of string?
	ja	pC3		; loop if not
	mov	BX, STDOUT	; get ready to print
	mov	AH, 40h		; INT 21h service 40h: write to file/device
	int	21h
	pop	DS		; restore DS
	ret
printCMD	endp

;----------------
;	Print memory block information.
;----------------

printMEM	proc	near
	mov	AX, maxmem
	lea	DI, memSIZ
	call	hextoasc
	mov	AX, ourpid
	lea	DI, memLOC
	call	hextoasc
	mov	AX, maxmem
	sub	AX, ourpid
	lea	DI, memAVL
	call	hextoasc
	PRINT	mem
	ret
printMEM	endp

;----------------
;	Put value in AX as ASCII-Hex into location at addr DS:DI
;	Note: procedure can only alter DI
;----------------

hextoasc	proc	near
	push	AX
	push	BX
	push	CX		; Save registers
	push	ES		; Save ES
	push	DS		; Copy DS
	pop	ES		;   to ES
	lea	BX, hexlist	; addr DS:BX points to list of hex chars
	xchg	AH, AL		; swap bytes
	call	hexbyte		; process one byte
	xchg	AH, AL		; swap bytes back
	call	hexbyte		; process other byte
	pop	ES		; Restore ES
	pop	CX
	pop	BX
	pop	AX		; Restore registers
	ret

hexbyte		proc	near
	push	AX		; save AL
	and	AL, 0F0h	; mask low nibble
	mov	CL, 4		; bit shift count
	shr	AL, CL		; move high nibble down
	xlat			; translate nibble to ASCII char
	stosb			; store char at addr ES:DI, incr DI
	pop	AX		; restore AL
	push	AX		; save AX once more
	and	AL, 00Fh	; mask high nibble
	xlat			; translate to ASCII
	stosb			; store char in string
	pop	AX		; restore AX
	ret
hexbyte		endp
hextoasc	endp

	end	start

