The MS-DOS Encyclopedia
Section II: Programming in the MS-DOS Environment
Part A Structure of MS-DOS
Article 1: An Introduction to MS-DOS
An operating system is a set of interrelated supervisory programs that
manage and control computer processing. In general, an operating
system provides
■ Storage management
■ Processing management
■ Security
■ Human interface
Existing operating systems for microcomputers fall into three major
categories: ROM monitors, traditional operating systems, and operating
environments. The general characteristics of the three categories are
listed in Table 1-1.
Table 1-1. Characteristics of the Three Major Types of
Operating Systems.
╓┌──────────────────────────┌─────────────┌─────────────┌────────────────────╖
Traditional
ROM Operating Operating
Monitor System Environment
──────────────────────────────────────────────────────────────────
Complexity Low Medium High
Built on Hardware BIOS Operating system
Delivered on ROM Disk Disk
Programs on ROM Disk Disk
Peripheral support Physical Logical Logical
Disk access Sector File system File system
Example PC ROM BIOS MS-DOS Microsoft Windows
A ROM monitor is the simplest type of operating system. It is designed
for a particular hardware configuration and provides a program with
basic--and often direct--access to peripherals attached to the
computer. Programs coupled with a ROM monitor are often used for
dedicated applications such as controlling a microwave oven or
controlling the engine of a car.
A traditional microcomputer operating system is built on top of a ROM
monitor, or BIOS (basic input/output system), and provides additional
features such as a file system and logical access to peripherals.
(Logical access to peripherals allows applications to run in a
hardware-independent manner.) A traditional operating system also
stores programs in files on peripheral storage devices and, on
request, loads them into memory for execution. MS-DOS is a traditional
operating system.
An operating environment is built on top of a traditional operating
system. The operating environment provides additional services, such
as common menu and forms support, that simplify program operation and
make the user interface more consistent. Microsoft Windows is an
operating environment.
MS-DOS System Components
The Microsoft Disk Operating System, MS-DOS, is a traditional
microcomputer operating system that consists of five major components:
■ The operating-system loader
■ The MS-DOS BIOS
■ The MS-DOS kernel
■ The user interface (shell)
■ Support programs
Each of these is introduced briefly in the following pages. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: STRUCTURE OF MS-DOS: The
Components of MS-DOS.
The operating-system loader
The operating-system loader brings the operating system from the
startup disk into RAM.
The complete loading process, called bootstrapping, is often complex,
and multiple loaders may be involved. (The term bootstrapping came
about because each level pulls up the next part of the system, like
pulling up on a pair of bootstraps.) For example, in most standard
MS-DOS-based microcomputer implementations, the ROM loader, which is
the first program the microcomputer executes when it is turned on or
restarted, reads the disk bootstrap loader from the first (boot)
sector of the startup disk and executes it. The disk bootstrap loader,
in turn, reads the main portions of MS-DOS--MSDOS.SYS and IO.SYS
(IBMDOS.COM and IBMBIO.COM with PC-DOS)--from conventional disk files
into memory. The special module SYSINIT within MSDOS.SYS then
initializes MS-DOS's tables and buffers and discards itself. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: STRUCTURE OF MS-DOS: MS-DOS
Storage Devices.
(The term loader is also used to refer to the portion of the operating
system that brings application programs into memory for execution.
This loader is different from the ROM loader and the operating-system
loader.)
The MS-DOS BIOS
The MS-DOS BIOS, loaded from the file IO.SYS during system
initialization, is the layer of the operating system that sits between
the operating-system kernel and the hardware. An application performs
input and output by making requests to the operating-system kernel,
which, in turn, calls the MS-DOS BIOS routines that access the
hardware directly. See SYSTEM CALLS. This division of function allows
application programs to be written in a hardware-independent manner.
The MS-DOS BIOS consists of some initialization code and a collection
of device drivers. (A device driver is a specialized program that
provides support for a specific device such as a display or serial
port.) The device drivers are responsible for hardware access and for
the interrupt support that allows the associated devices to signal the
microprocessor that they need service.
The device drivers contained in the file IO.SYS, which are always
loaded during system initialization, are sometimes referred to as the
resident drivers. With MS-DOS versions 2.0 and later, additional
device drivers, called installable drivers, can optionally be loaded
during system initialization as a result of DEVICE directives in the
system's configuration file. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: CUSTOMIZING MS-DOS: Installable Device Drivers; USER
COMMANDS: CONFIG.SYS:DEVICE.
The MS-DOS kernel
The services provided to application programs by the MS-DOS kernel
include
■ Process control
■ Memory management
■ Peripheral support
■ A file system
The MS-DOS kernel is loaded from the file MSDOS.SYS during system
initialization.
Process control
Process, or task, control includes program loading, task execution,
task termination, task scheduling, and intertask communication.
Although MS-DOS is not a multitasking operating system, it can have
multiple programs residing in memory at the same time. One program can
invoke another, which then becomes the active (foreground) task. When
the invoked task terminates, the invoking program again becomes the
foreground task. Because these tasks never execute simultaneously,
this stack-like operation is still considered to be a single-tasking
operating system.
MS-DOS does have a few "hooks" that allow certain programs to do some
multitasking on their own. For example, terminate-and-stay-resident
(TSR) programs such as PRINT use these hooks to perform limited
concurrent processing by taking control of system resources while
MS-DOS is "idle," and the Microsoft Windows operating environment adds
support for nonpreemptive task switching.
The traditional intertask communication methods include semaphores,
queues, shared memory, and pipes. Of these, MS-DOS formally supports
only pipes. (A pipe is a logical, unidirectional, sequential stream of
data that is written by one program and read by another.) The data in
a pipe resides in memory or in a disk file, depending on the
implementation; MS-DOS uses disk files for intermediate storage of
data in pipes because it is a single-tasking operating system.
Memory management
Because the amount of memory a program needs varies from program to
program, the traditional operating system ordinarily provides memory-
management functions. Memory requirements can also vary during program
execution, and memory management is especially necessary when two or
more programs are present in memory at the same time.
MS-DOS memory management is based on a pool of variable-size memory
blocks. The two basic memory-management actions are to allocate a
block from the pool and to return an allocated block to the pool.
MS-DOS allocates program space from the pool when the program is
loaded; programs themselves can allocate additional memory from the
pool. Many programs perform their own memory management by using a
local memory pool, or heap--an additional memory block allocated from
the operating system that the application program itself divides into
blocks for use by its various routines. See PROGRAMMING IN
THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: Memory Management.
Peripheral support
The operating system provides peripheral support to programs through a
set of operating-system calls that are translated by the operating
system into calls to the appropriate device driver.
Peripheral support can be a direct logical-to-physical-device
translation or the operating system can interject additional features
or translations. Keyboards, displays, and printers usually require
only logical-to-physical-device translations; that is, the data is
transferred between the application program and the physical device
with minimal alterations, if any, by the operating system. The data
provided by clock devices, on the other hand, must be transformed to
operating-system-dependent time and date formats. Disk devices--and
block devices in general--have the greatest number of features added
by the operating system. See The File System, below.
As stated earlier, an application need not be concerned with the
details of peripheral devices or with any special features the devices
might have. Because the operating system takes care of all the
logical-to-physical-device translations, the application program need
only make requests of the operating system.
The file system
The file system is one of the largest portions of an operating system.
A file system is built on the storage medium of a block device
(usually a floppy disk or a fixed disk) by mapping a directory
structure and files onto the physical unit of storage. A file system
on a disk contains, at a minimum, allocation information, a directory,
and space for files. See PROGRAMMING IN THE MS-DOS ENVIRONMENT:
STRUCTURE OF MS-DOS: MS-DOS Storage Devices.
The file allocation information can take various forms, depending on
the operating system, but all forms basically track the space used by
files and the space available for new data. The directory contains a
list of the files stored on the device, their sizes, and information
about where the data for each file is located.
Several different approaches to file allocation and directory entries
exist. MS-DOS uses a particular allocation method called a file
allocation table (FAT) and a hierarchical directory structure. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: STRUCTURE OF MS-DOS: MS-DOS
Storage Devices; PROGRAMMING FOR MS-DOS: Disk Directories and Volume
Labels.
The file granularity available through the operating system also
varies depending on the implementation. Some systems, such as MS-DOS,
have files that are accessible to the byte level; others are
restricted to a fixed record size.
File systems are sometimes extended to map character devices as if
they were files. These device "files" can be opened, closed, read
from, and written to like normal disk files, but all transactions
occur directly with the specified character device. Device files
provide a useful consistency to the environment for application
programs; MS-DOS supports such files by assigning a reserved logical
name (such as CON or PRN) to each character device.
The user interface
The user interface for an operating system, also called a shell or
command processor, is generally a conventional program that allows the
user to interact with the operating system itself. The default MS-DOS
user interface is a replaceable shell program called COMMAND.COM.
One of the fundamental tasks of a shell is to load a program into
memory on request and pass control of the system to the program so
that the program can execute. When the program terminates, control
returns to the shell, which prompts the user for another command. In
addition, the shell usually includes functions for file and directory
maintenance and display. In theory, most of these functions could be
provided as programs, but making them resident in the shell allows
them to be accessed more quickly. The tradeoff is memory space versus
speed and flexibility. Early microcomputer-based operating systems
provided a minimal number of resident shell commands because of
limited memory space; modern operating systems such as MS-DOS include
a wide variety of these functions as internal commands.
Support programs
The MS-DOS software includes support programs that provide access to
operating-system facilities not supplied as resident shell commands
built into COMMAND.COM. Because these programs are stored as
executable files on disk, they are essentially the same as application
programs and MS-DOS loads and executes them as it would any other
program.
The support programs provided with MS-DOS, often referred to as
external commands, include disk utilities such as FORMAT and CHKDSK
and more general support programs such as EDLIN (a line-oriented text
editor) and PRINT (a TSR utility that allows files to be printed while
another program is running). See USER COMMANDS.
MS-DOS releases
MS-DOS and PC-DOS have been released in a number of forms, starting in
1981. See THE DEVELOPMENT OF MS-DOS. The major MS-DOS and PC-DOS
implementations are summarized in the following table.
╓┌────────────────────────┌─────────┌────────────────────────────────────────╖
Version Date Special Characteristics
──────────────────────────────────────────────────────────────────
PC-DOS 1.0 1981 First operating system for the IBM
PC Record-oriented files
PC-DOS 1.1 1982 Double-sided-disk support
MS-DOS 1.25 1982 First OEM release of MS-DOS
MS-DOS/PC-DOS 2.0 1983 Operating system for the IBM
PC/XT UNIX/XENIX-like file system
Installable device drivers
Byte-oriented files
Support for fixed disks
PC-DOS 2.1 Operating system for the IBM PCjr
MS-DOS 2.11 Internationalization support 2.0x bug
fixes
MS-DOS/PC-DOS 3.0 1984 Operating system for the IBM PC/AT
Support for 1.2 MB floppy disks
Support for large fixed disks
Support for file and record locking
Application control of print spooler
MS-DOS/PC-DOS 3.1 1984 Support for MS Networks
MS-DOS/PC-DOS 3.2 1986 3.5-inch floppy-disk support
Disk track formatting support added
to device drivers
MS-DOS/PC-DOS 3.3 1987 Support for the IBM PS/2
Enhancedinternationalization support
Improved file-system performance
Partitioning support for disks with
capacity above 32 MB
PC-DOS version 1.0 was the first commercial version of MS-DOS. It was
developed for the original IBM PC, which was typically shipped with 64
KB of memory or less. MS-DOS and PC-DOS versions 1.x were similar in
many ways to CP/M, the popular operating system for 8-bit
microcomputers based on the Intel 8080 (the predecessor of the 8086).
These versions of MS-DOS used a single-level file system with no
subdirectory support and did not support installable device drivers or
networks. Programs accessed files using file control blocks (FCBs)
similar to those found in CP/M programs. File operations were record
oriented, again like CP/M, although record sizes could be varied in
MS-DOS.
Although they retained compatibility with versions 1.x, MS-DOS and
PC-DOS versions 2.x represented a major change. In addition to
providing support for fixed disks, the new versions switched to a
hierarchical file system like that found in UNIX/XENIX and to file-
handle access instead of FCBs. (A file handle is a 16-bit number used
to reference an internal table that MS-DOS uses to keep track of
currently open files; an application program has no access to this
internal table.) The UNIX/XENIX-style file functions allow files to be
treated as a byte stream instead of as a collection of records.
Applications can read or write 1 to 65535 bytes in a single operation,
starting at any byte offset within the file. Filenames used for
opening a files are passed as text strings instead of being parsed
into an FCB. Installable device drivers were another major
enhancement.
MS-DOS and PC-DOS versions 3.x added a number of valuable features,
including support for the added capabilities of the IBM PC/AT, for
larger-capacity disks, and for file-locking and record-locking
functions. Network support was added by providing hooks for a
redirector (an additional operating-system module that has the ability
to redirect local system service requests to a remote system by means
of a local area network).
With all these changes, MS-DOS remains a traditional single-tasking
operating system. It provides a large number of system services in a
transparent fashion so that, as long as they use only the MS-DOS-
supplied services and refrain from using hardware-specific operations,
applications developed for one MS-DOS machine can usually run on
another.
Basic MS-DOS Requirements
Foremost among the requirements for MS-DOS is an Intel 8086-compatible
microprocessor. See Specific Hardware Requirements below.
The next requirement is the ROM bootstrap loader and enough RAM to
contain the MS-DOS BIOS, kernel, and shell and an application program.
The RAM must start at address 0000:0000H and, to be managed by MS-DOS,
must be contiguous. The upper limit for RAM is the limit placed upon
the system by the 8086 family--1 MB.
The final requirement for MS-DOS is a set of devices supported by
device drivers, including at least one block device, one character
device, and a clock device. The block device is usually the boot disk
device (the disk device from which MS-DOS is loaded); the character
device is usually a keyboard/display combination for interaction with
the user; the clock device, required for time-of-day and date support,
is a hardware counter driven in a submultiple of one second.
Specific hardware requirements
MS-DOS uses several hardware components and has specific requirements
for each. These components include
■ An 8086-family microprocessor
■ Memory
■ Peripheral devices
■ A ROM BIOS (PC-DOS only)
The microprocessor
MS-DOS runs on any machine that uses a microprocessor that executes
the 8086/8088 instruction set, including the Intel 8086, 80C86, 8088,
80186, 80188, 80286, and 80386 and the NEC V20, V30, and V40.
The 80186 and 80188 are versions of the 8086 and 8088, integrated in a
single chip with direct memory access, timer, and interrupt support
functions. PC-DOS cannot usually run on the 80186 or 80188 because
these chips have internal interrupt and interface register addresses
that conflict with addresses used by the PC ROM BIOS. See PROGRAMMING
IN THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Hardware Interrupt
Handlers. MS-DOS, however, does not have address requirements that
conflict with those interrupt and interface areas.
The 80286 has an extended instruction set and two operating modes:
real and protected. Real mode is compatible with the 8086/8088 and
runs MS-DOS. Protected mode, used by operating systems like UNIX/XENIX
and MS OS/2, is partially compatible with real mode in terms of
instructions but provides access to 16 MB of memory versus only 1 MB
in real mode (the limit of the 8086/8088).
The 80386 adds further instructions and a third mode called virtual 86
mode. The 80386 instructions operate in either a 16-bit or a 32-bit
environment. MS-DOS can run on the 80386 in real or virtual 86 mode,
although the latter requires additional support in the form of a
virtual machine monitor such as Windows /386.
Memory requirements
At a minimum, MS-DOS versions 1.x require 64 KB of contiguous RAM from
the base of memory to do useful work; versions 2.x and 3.x need at
least 128 KB. The maximum is 1 MB, although most MS-DOS machines have
a 640 KB limit for IBM PC compatibility. MS-DOS can use additional
noncontiguous RAM for a RAMdisk if the proper device driver is
included. (Other uses for noncontiguous RAM include buffers for video
displays, fixed disks, and network adapters.)
PC-DOS has the same minimum memory requirements but has an upper limit
of 640 KB on the initial contiguous RAM, which is generally referred
to as conventional memory. This limit was imposed by the architecture
of the original IBM PC, with the remaining area above 640 KB reserved
for video display buffers, fixed disk adapters, and the ROM BIOS. Some
of the reserved areas include
╓┌──────────────────────┌─────────────────────┌──────────────────────────────╖
Base Address Size (bytes) Description
──────────────────────────────────────────────────────────────────
A000:0000H 10000H (64 KB) EGA video buffer
B000:0000H 1000H (4 KB) Monochrome video buffer
B800:0000H 4000H (16 KB) Color/graphics video buffer
C800:0000H 4000H (16 KB) Fixed-disk ROM
F000:0000H 10000H (64 KB) PC ROM BIOS and ROM BASIC
The bottom 1024 bytes of system RAM (locations 00000-003FFH) are used
by the microprocessor for an interrupt vector table--that is, a list
of addresses for interrupt handler routines. MS-DOS uses some of the
entries in this table, such as the vectors for interrupts 20H through
2FH, to store addresses of its own tables and routines and to provide
linkage to its services for application programs. The IBM PC ROM BIOS
and IBM PC BASIC use many additional vectors for the same purposes.
Peripheral devices
MS-DOS can support a wide variety of devices, including floppy disks,
fixed disks, CD ROMs, RAMdisks, and digital tape drives. The required
peripheral support for MS-DOS is provided by the MS-DOS BIOS or by
installable device drivers.
Five logical devices are provided in a basic MS-DOS system:
╓┌────────────────────────────┌──────────────────────────────────────────────╖
Device Name Description
──────────────────────────────────────────────────────────────────
CON Console input and output
PRN Printer output
AUX Auxiliary input and output
CLOCK$ Date and time support
Varies (A-E) One block device
These five logical devices can be implemented with a BIOS supporting a
minimum of three physical devices: a keyboard and display, a timer or
clock/calendar chip that can provide a hardware interrupt at regular
intervals, and a block storage device. In such a minimum case, the
printer and auxiliary device are simply aliases for the console
device. However, most MS-DOS systems support several additional
logical and physical devices. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: Character Device Input and
Output.
The MS-DOS kernel provides one additional device: the NUL device. NUL
is a "bit bucket"--that is, anything written to NUL is simply
discarded. Reading from NUL always returns an end-of-file marker. One
common use for the NUL device is as the redirected output device of a
command or application that is being run in a batch file; this
redirection prevents screen clutter and disruption of the batch file's
menus and displays.
The ROM BIOS
MS-DOS requires no ROM support (except that most bootstrap loaders
reside in ROM) and does not care whether device-driver support resides
in ROM or is part of the MS-DOS IO.SYS file loaded at initialization.
PC-DOS, on the other hand, uses a very specific ROM BIOS. The PC ROM
BIOS does not provide device drivers; rather, it provides support
routines used by the device drivers found in IBMBIO.COM (the PC-DOS
version of IO.SYS). The support provided by a PC ROM BIOS includes
■ Power-on self test (POST)
■ Bootstrap loader
■ Keyboard
■ Displays (monochrome and color/graphics adapters)
■ Serial ports 1 and 2
■ Parallel printer ports 1, 2, and 3
■ Clock
■ Print screen
The PC ROM BIOS loader routine searches the ROM space above the PC-DOS
640 KB limit for additional ROMs. The IBM fixed-disk adapter and
enhanced graphics adapter (EGA) contain such ROMs. (The fixed-disk ROM
also includes an additional loader routine that allows the system to
start from the fixed disk.)
Summary
MS-DOS is a widely accepted traditional operating system. Its
consistent and well-defined interface makes it one of the easier
operating systems to adapt and program.
MS-DOS is also a growing operating system--each version has added more
features yet made the system easier to use for both end-users and
programmers. In addition, each version has included more support for
different devices, from 5.25-inch floppy disks to high-density 3.5-
inch floppy disks. As the hardware continues to evolve and user needs
become more sophisticated, MS-DOS too will continue to evolve.
William Wong
Article 2: The Components of MS-DOS
MS-DOS is a modular operating system consisting of multiple components
with specialized functions. When MS-DOS is copied into memory during
the loading process, many of its components are moved, adjusted, or
discarded. However, when it is running, MS-DOS is a relatively static
entity and its components are predictable and easy to study.
Therefore, this article deals first with MS-DOS in its running state
and later with its loading behavior.
The Major Elements
MS-DOS consists of three major modules:
╓┌────────────────────────────┌───────────────────────┌──────────────────────╖
Module MS-DOS Filename PC-DOS Filename
──────────────────────────────────────────────────────────────────
MS-DOS BIOS IO.SYS IBMBIO.COM
MS-DOS kernel MSDOS.SYS IBMDOS.COM
MS-DOS shell COMMAND.COM COMMAND.COM
During system initialization, these modules are loaded into memory, in
the order given, just above the interrupt vector table located at the
beginning of memory. All three modules remain in memory until the
computer is reset or turned off. (The loader and system initialization
modules are omitted from this list because they are discarded as soon
as MS-DOS is running. See Loading MS-DOS, below.)
The MS-DOS BIOS is supplied by the original equipment manufacturer
(OEM) that distributes MS-DOS, usually for a particular computer. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: STRUCTURE OF MS-DOS: An
Introduction to MS-DOS. The kernel is supplied by Microsoft and is the
same across all OEMs for a particular version of MS-DOS--that is, no
modifications are made by the OEM. The shell is a replaceable module
that can be supplied by the OEM or replaced by the user; the default
shell, COMMAND.COM, is supplied by Microsoft.
The MS-DOS BIOS
The file IO.SYS contains the MS-DOS BIOS and the MS-DOS initialization
module, SYSINIT. The MS-DOS BIOS is customized for a particular
machine by an OEM. SYSINIT is supplied by Microsoft and is put into
IO.SYS by the OEM when the file is created. See Loading MS-DOS, below.
The MS-DOS BIOS consists of a list of resident device drivers and an
additional initialization module created by the OEM. The device
drivers appear first in IO.SYS because they remain resident after
IO.SYS is initialized; the MS-DOS BIOS initialization routine and
SYSINIT are usually discarded after initialization.
The minimum set of resident device drivers is CON, PRN, AUX, CLOCK$,
and the driver for one block device. The resident character-device
drivers appear in the driver list before the resident block-device
drivers; installable character-device drivers are placed ahead of the
resident device drivers in the list; installable block-device drivers
are placed after the resident device drivers in the list. This
sequence allows installable character-device drivers to supersede
resident drivers. The NUL device driver, which must be the first
driver in the chain, is contained in the MS-DOS kernel.
Device driver code can be split between IO.SYS and ROM. For example,
most MS-DOS systems and all PC-DOS-compatible systems have a ROM BIOS
that contains primitive device support routines. These routines are
generally used by resident and installable device drivers to augment
routines contained in RAM. (Placing the entire driver in RAM makes the
driver dependent on a particular hardware configuration; placing part
of the driver in ROM allows the MS-DOS BIOS to be paired with a
particular ROM interface that remains constant for many different
hardware configurations.)
The IO.SYS file is an absolute program image and does not contain
relocation information. The routines in IO.SYS assume that the CS
register contains the segment at which the file is loaded. Thus,
IO.SYS has the same 64 KB restriction as a .COM file. See PROGRAMMING
IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: Structure of an
Application Program. Larger IO.SYS files are possible, but all device
driver headers must lie in the first 64 KB and the code must rely on
its own segment arithmetic to access routines outside the first 64 KB.
The MS-DOS kernel
The MS-DOS kernel is the heart of MS-DOS and provides the functions
found in a traditional operating system. It is contained in a single
proprietary file, MSDOS.SYS, supplied by Microsoft Corporation. The
kernel provides its support functions (referred to as system
functions) to application programs in a hardware-independent manner
and, in turn, is isolated from hardware characteristics by relying on
the driver routines in the MS-DOS BIOS to perform physical input and
output operations.
The MS-DOS kernel provides the following services through the use of
device drivers:
■ File and directory management
■ Character device input and output
■ Time and date support
It also provides the following non-device-related functions:
■ Memory management
■ Task and environment management
■ Country-specific configuration
Programs access system functions using software interrupt (INT)
instructions. MS-DOS reserves Interrupts 20H through 3FH for this
purpose. The MS-DOS interrupts are
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Interrupt Name
──────────────────────────────────────────────────────────────────
20H Terminate Program
21H MS-DOS Function Calls
22H Terminate Routine Address
23H Control-C Handler Address
24H Critical Error Handler Address
25H Absolute Disk Read
26H Absolute Disk Write
27H Terminate and Stay Resident
28H-2EH Reserved
2FH Multiplex
30H-3FH Reserved
Interrupt 21H is the main source of MS-DOS services. The Interrupt 21H
functions are implemented by placing a function number in the AH
register, placing any necessary parameters in other registers, and
issuing an INT 21H instruction. (MS-DOS also supports a call
instruction interface for CP/M compatibility. The function and
parameter registers differ from the interrupt interface. The CP/M
interface was provided in MS-DOS version 1.0 solely to assist in
movement of CP/M-based applications to MS-DOS. New applications should
use Interrupt 21H functions exclusively.)
MS-DOS version 2.0 introduced a mechanism to modify the operation of
the MS-DOS BIOS and kernel: the CONFIG.SYS file. CONFIG.SYS is a text
file containing command options that modify the size or configuration
of internal MS-DOS tables and cause additional device drivers to be
loaded. The file is read when MS-DOS is first loaded into memory. See
USER COMMANDS: CONFIG.SYS.
The MS-DOS shell
The shell, or command interpreter, is the first program started by
MS-DOS after the MS-DOS BIOS and kernel have been loaded and
initialized. It provides the interface between the kernel and the
user. The default MS-DOS shell, COMMAND.COM, is a command-oriented
interface; other shells may be menu-driven or screen-oriented.
COMMAND.COM is a replaceable shell. A number of commercial products
can be used as COMMAND.COM replacements, or a programmer can develop a
customized shell. The new shell program is installed by renaming the
program to COMMAND.COM or by using the SHELL command in CONFIG.SYS.
The latter method is preferred because it allows initialization
parameters to be passed to the shell program.
COMMAND.COM can execute a set of internal (built-in) commands, load
and execute programs, or interpret batch files. Most of the internal
commands support file and directory operations and manipulate the
program environment segment maintained by COMMAND.COM. The programs
executed by COMMAND.COM are .COM or .EXE files loaded from a block
device. The batch (.BAT) files supported by COMMAND.COM provide a
limited programming language and are therefore useful for performing
small, frequently used series of MS-DOS commands. In particular, when
it is first loaded by MS-DOS, COMMAND.COM searches for the batch file
AUTOEXEC.BAT and interprets it, if found, before taking any other
action. COMMAND.COM also provides default terminate, Control-C and
critical error handlers whose addresses are stored in the vectors for
Interrupts 22H, 23H, and 24H. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: CUSTOMIZING MS-DOS: Exception Handlers.
COMMAND.COM's split personality
COMMAND.COM is a conventional .COM application with a slight twist.
Ordinarily, a .COM program is loaded into a single memory segment.
COMMAND.COM starts this way but then copies the nonresident portion of
itself into high memory and keeps the resident portion in low memory.
The memory above the resident portion is released to MS-DOS.
The effect of this split is not apparent until after an executed
program has terminated and the resident portion of COMMAND.COM regains
control of the system. The resident portion then computes a checksum
on the area in high memory where the nonresident portion should be, to
determine whether it has been overwritten. If the checksum matches a
stored value, the nonresident portion is assumed to be intact;
otherwise, a copy of the nonresident portion is reloaded from disk and
COMMAND.COM continues its normal operation.
This "split personality" exists because MS-DOS was originally designed
for systems with a limited amount of RAM. The nonresident portion of
COMMAND.COM, which contains the built-in commands and batch-file-
processing routines that are not essential to regaining control and
reloading itself, is much larger than the resident portion, which is
responsible for these tasks. Thus, permitting the nonresident portion
to be overwritten frees additional RAM and allows larger application
programs to be run.
Command execution
COMMAND.COM interprets commands by first checking to see if the
specified command matches the name of an internal command. If so, it
executes the command; otherwise, it searches for a .COM, .EXE, or .BAT
file (in that order) with the specified name. If a .COM or .EXE
program is found, COMMAND.COM uses the MS-DOS EXEC function (Interrupt
21H Function 4BH) to load and execute it; COMMAND.COM itself
interprets .BAT files. If no file is found, the message Bad command or
file name is displayed.
Although a command is usually simply a filename without the extension,
MS-DOS versions 3.0 and later allow a command name to be preceded by a
full pathname. If a path is not explicitly specified, the COMMAND.COM
search mechanism uses the contents of the PATH environment variable,
which can contain a list of paths to be searched for commands. The
search starts with the current directory and proceeds through the
directories specified by PATH until a file is found or the list is
exhausted. For example, the PATH specification
PATH C:\BIN;D:\BIN;E:\
causes COMMAND.COM to search the current directory, then C:\BIN, then
D:\BIN, and finally the root directory of drive E. COMMAND.COM
searches each directory for a matching .COM, .EXE, or .BAT file, in
that order, before moving to the next directory.
MS-DOS environments
Version 2.0 introduced the concept of environments to MS-DOS. An
environment is a paragraph-aligned memory segment containing a
concatenated set of zero-terminated (ASCIIZ) variable-length strings
of the form
variable=value
that provide such information as the current search path used by
COMMAND.COM to find executable files, the location of COMMAND.COM
itself, and the format of the user prompt. The end of the set of
strings is marked by a null string--that is, a single zero byte. A
specific environment is associated with each program in memory through
a pointer contained at offset 2CH in the 256-byte program segment
prefix (PSP). The maximum size of an environment is 32 KB; the default
size is 160 bytes.
If a program uses the EXEC function to load and execute another
program, the contents of the new program's environment are provided to
MS-DOS by the initiating program--one of the parameters passed to the
MS-DOS EXEC function is a pointer to the new program's environment.
The default environment provided to the new program is a copy of the
initiating program's environment.
A program that uses the EXEC function to load and execute another
program will not itself have access to the new program's environment,
because MS-DOS provides a pointer to this environment only to the new
program. Any changes made to the new program's environment during
program execution are invisible to the initiating program because a
child program's environment is always discarded when the child program
terminates.
The system's master environment is normally associated with the shell
COMMAND.COM. COMMAND.COM creates this set of environment strings
within itself from the contents of the CONFIG.SYS and AUTOEXEC.BAT
files, using the SET, PATH, and PROMPT commands. See USER COMMANDS:
AUTOEXEC.BAT; CONFIG.SYS. In MS-DOS version 3.2, the initial size of
COMMAND.COM's environment can be controlled by loading COMMAND.COM
with the /E parameter, using the SHELL directive in CONFIG.SYS. For
example, placing the line
SHELL=COMMAND.COM /E:2048 /P
in CONFIG.SYS sets the initial size of COMMAND.COM's environment to 2
KB. (The /P option prevents COMMAND.COM from terminating, thus causing
it to remain in memory until the system is turned off or restarted.)
The SET command is used to display or change the COMMAND.COM
environment contents. SET with no parameters displays the list of all
the environment strings in the environment. A typical listing might
show the following settings:
COMSPEC=A:\COMMAND.COM
PATH=C:\;A:\;B:\
PROMPT=$p $d $t$_$n$g
TMP=C:\TEMP
The following is a dump of the environment segment containing the
previous environment example:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 43 4F 4D 53 50 45 43 3D-41 3A 5C 43 4F 4D 4D 41 COMSPEC=A:\COMMA
0010 4E 44 2E 43 4F 4D 00 50-41 54 48 3D 43 3A 5C 3B ND.COM.PATH=C:\;
0020 41 3A 5C 3B 42 3A 5C 00-50 52 4F 4D 50 54 3D 24 A:\;B:\.PROMPT=$
0030 70 20 20 24 64 20 20 24-74 24 5F 24 6E 24 67 00 p $d $t$_$n$g.
0040 54 4D 50 3D 43 3A 5C 54-45 4D 50 00 00 00 00 00 TMP=C:\TEMP.....
A SET command that specifies a variable but does not specify a value
for it deletes the variable from the environment.
A program can ignore the contents of its environment; however, use of
the environment can add a great deal to the flexibility and
configurability of batch files and application programs.
Batch files
Batch files are text files with a .BAT extension that contain MS-DOS
user and batch commands. Each line in the file is limited to 128
bytes. See USER COMMANDS: BATCH. Batch files can be created using most
text editors, including EDLIN, and short batch files can even be
created using the COPY command:
C>COPY CON SAMPLE.BAT <ENTER>
The CON device is the system console; text entered from the keyboard
is echoed on the screen as it is typed. The copy operation is
terminated by pressing Ctrl-Z (or the F6 key on IBM-compatible
machines), followed by the Enter key.
Batch files are interpreted by COMMAND.COM one line at a time. In
addition to the standard MS-DOS commands, COMMAND.COM's batch-file
interpreter supports a number of special batch commands:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Command Meaning
──────────────────────────────────────────────────────────────────
ECHO Display a message.
FOR Execute a command for a list of files.
GOTO Transfer control to another point.
IF Conditionally execute a command.
PAUSE Wait for any key to be pressed.
REM Insert comment line.
SHIFT Access more than 10 parameters.
Execution of a batch file can be terminated before completion by
pressing Ctrl-C or Ctrl-Break, causing COMMAND.COM to display the
prompt
Terminate batch job? (Y/N)
I/O redirection
I/O redirection was introduced with MS-DOS version 2.0. The
redirection facility is implemented within COMMAND.COM using the
Interrupt 21H system functions Duplicate File Handle (45H) and Force
Duplicate File Handle (46H). COMMAND.COM uses these functions to
provide both redirection at the command level and a UNIX/XENIX-like
pipe facility.
Redirection is transparent to application programs, but to take
advantage of redirection, an application program must make use of the
standard input and output file handles. The input and output of
application programs that directly access the screen or keyboard or
use ROM BIOS functions cannot be redirected.
Redirection is specified in the command line by prefixing file or
device names with the special characters >, >>, and <. Standard output
(default = CON) is redirected using > and >> followed by the name of a
file or character device. The former character creates a new file (or
overwrites an existing file with the same name); the latter appends
text to an existing file (or creates the file if it does not exist).
Standard input (default = CON) is redirected with the < character
followed by the name of a file or character device. See also
PROGRAMMING IN THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Writing MS-
DOS Filters.
The redirection facility can also be used to pass information from one
program to another through a "pipe." A pipe in MS-DOS is a special
file created by COMMAND.COM. COMMAND.COM redirects the output of one
program into this file and then redirects this file as the input to
the next program. The pipe symbol, a vertical bar (), separates the
program names. Multiple program names can be piped together in the
same command line:
C>DIR *.* | SORT | MORE
This command is equivalent to
C>DIR *.* > PIPE0 <ENTER>
C>SORT < PIPE0 > PIPE1 <ENTER>
C>MORE < PIPE1 <ENTER>
The concept of pipes came from UNIX/XENIX, but UNIX/XENIX is a
multitasking operating system that actually runs the programs
simultaneously. UNIX/XENIX uses memory buffers to connect the
programs, whereas MS-DOS loads one program at a time and passes
information through a disk file.
Loading MS-DOS
Getting MS-DOS up to the standard A> prompt is a complex process with
a number of variations. This section discusses the complete process
normally associated with MS-DOS versions 2.0 and later. (MS-DOS
versions 1.x use the same general steps but lack support for various
system tables and installable device drivers.)
MS-DOS is loaded as a result of either a "cold boot" or a "warm boot."
On IBM-compatible machines, a cold boot is performed when the computer
is first turned on or when a hardware reset occurs. A cold boot
usually performs a power-on self test (POST) and determines the amount
of memory available, as well as which peripheral adapters are
installed. The POST is ordinarily reserved for a cold boot because it
takes a noticeable amount of time. For example, an IBM-compatible ROM
BIOS tests all conventional and extended RAM (RAM above 1 MB on an
80286-based or 80386-based machine), a procedure that can take tens
of seconds. A warm boot, initiated by simultaneously pressing the
Ctrl, Alt, and Del keys, bypasses these hardware checks and begins by
checking for a bootable disk.
A bootable disk normally contains a small loader program that loads
MS-DOS from the same disk. See PROGRAMMING IN THE MS-DOS ENVIRONMENT:
STRUCTURE OF MS-DOS: MS-DOS Storage Devices. The body of MS-DOS is
contained in two files: IO.SYS and MSDOS.SYS (IBMBIO.COM and
IBMDOS.COM with PC-DOS). IO.SYS contains the Microsoft system
initialization module, SYSINIT, which configures MS-DOS using either
default values or the specifications in the CONFIG.SYS file, if one
exists, and then starts up the shell program (usually COMMAND.COM, the
default). COMMAND.COM checks for an AUTOEXEC.BAT file and interprets
the file if found. (Other shells might not support such batch files.)
Finally, COMMAND.COM prompts the user for a command. (The standard
MS-DOS prompt is A> if the system was booted from a floppy disk and C>
if the system was booted from a fixed disk.) Each of these steps is
discussed in detail below.
The ROM BIOS, POST, and bootstrapping
All 8086/8088-compatible microprocessors begin execution with the
CS:IP set to FFFF:0000H, which typically contains a jump instruction
to a destination in the ROM BIOS that contains the initialization code
for the machine. (This has nothing to do with MS-DOS; it is a feature
of the Intel microprocessors.) On IBM-compatible machines, the ROM
BIOS occupies the address space from F000:0000H to this jump
instruction. Figure 2-1 shows the location of the ROM BIOS within the
1 MB address space. Supplementary ROM support can be placed before (at
lower addresses than) the ROM BIOS.
All interrupts are disabled when the microprocessor starts execution
and it is up to the initialization routine to set up the interrupt
vectors at the base of memory.
┌───────────────────┐──FFFF:000FH (1 MB)
│ ROM BIOS │──FFFF:0000H
├───────────────────┤──F000:0000H
│ │
│ Other ROM and RAM │
│ │
├───────────────────┤──Top of RAM
│ │ (A000:0000H for IBM PC)
│ │
│ │
│ Free RAM │
│ │
│ │
│ │
└───────────────────┘──0000:0000H
Figure 2-1. Memory layout at startup.
The initialization routine in the ROM BIOS--the POST procedure--
typically determines what devices are installed and operational and
checks conventional memory (the first 1 MB) and, for 80286-based or
80386-based machines, extended memory (above 1 MB). The devices are
tested, where possible, and any problems are reported using a series
of beeps and display messages on the screen.
When the machine is found to be operational, the ROM BIOS sets it up
for normal operation. First, it initializes the interrupt vector table
at the beginning of memory and any interrupt controllers that
reference the table. The interrupt vector table area is located from
0000:0000H to 0000:03FFH. On IBM-compatible machines, some of the
subsequent memory (starting at address 0000:0400H) is used for table
storage by various ROM BIOS routines (Figure 2-2). The beginning load
address for the MS-DOS system files is usually in the range 0000:0600H
to 0000:0800H.
┌───────────────────┐──FFFF:000FH (1 MB)
│ ROM BIOS │──FFFF:0000H
├───────────────────┤──F000:0000H
│ │
│ Other ROM and RAM │
│ │
├───────────────────┤──Top of RAM
│ │ (A000:0000H for IBM PC)
│ │
│ │
│ Free RAM │
│ │
│ │
│ │
├───────────────────┤──0000:0600H
│ ROM BIOS tables │
├───────────────────┤──0000:0400H
│ Interrupt vectors │
│ │
└───────────────────┘──0000:0000H
Figure 2-2. The interrupt vector table and the ROM BIOS table.
Next, the ROM BIOS sets up any necessary hardware interfaces, such as
direct memory access (DMA) controllers, serial ports, and the like.
Some hardware setup may be done before the interrupt vector table area
is set up. For example, the IBM PC DMA controller also provides
refresh for the dynamic RAM chips and RAM cannot be used until the
refresh DMA is running; therefore, the DMA must be set up first.
Some ROM BIOS implementations also check to see if additional ROM
BIOSs are installed by scanning the memory from A000:0000H to
F000:0000H for a particular sequence of signature bytes. If additional
ROM BIOSs are found, their initialization routines are called to
initialize the associated devices. Examples of additional ROMs for the
IBM PC family are the PC/XT's fixed-disk ROM BIOS and the EGA ROM
BIOS.
The ROM BIOS now starts the bootstrap procedure by executing the ROM
loader routine. On the IBM PC, this routine checks the first floppy-
disk drive to see if there is a bootable disk in it. If there
is not, the routine then invokes the ROM associated with another
bootable device to see if that device contains a bootable disk. This
procedure is repeated until a bootable disk is found or until all
bootable devices have been checked without success, in which case ROM
BASIC is enabled.
Bootable devices can be detected by a number of proprietary means. The
IBM PC ROM BIOS reads the first sector on the disk into RAM (Figure 2-
3) and checks for an 8086-family short or long jump at the beginning
of the sector and for AA55H in the last word of the sector. This
signature indicates that the sector contains the operating-system
loader. Data disks--those disks not set up with the MS-DOS system
files--usually cause the ROM loader routine to display a message
indicating that the disk is not a bootable system disk. The customary
recovery procedure is to display a message asking the user to insert
another disk (with the operating system files on it) and press a key
to try the load operation again. The ROM loader routine is then
typically reexecuted from the beginning so that it can repeat its
normal search procedure.
┌───────────────────┐──FFFF:000FH (1 MB)
│ ROM BIOS │──FFFF:0000H
├───────────────────┤──F000:0000H
│ │
│ Other ROM and RAM │
│ │
├───────────────────┤──Top of RAM
│ │ (A000:0000H for IBM PC)
│ │
│ │
│ Free RAM │
│ │
│ │
│ │
├───────────────────┤──0000:0600H
│ ROM BIOS tables │
├───────────────────┤──0000:0400H
│ Interrupt vectors │
│ │
└───────────────────┘──0000:0000H
Figure 2-3. A loaded boot sector.
When it finds a bootable device, the ROM loader routine loads the
operating-system loader and transfers control to it. The operating-
system loader then uses the ROM BIOS services through the interrupt
table to load the next part of the operating system into low memory.
Before it can proceed, the operating-system loader must know something
about the configuration of the system boot disk (Figure 2-4). MS-DOS-
compatible disks contain a data structure that contains this
information. This structure, known as the BIOS parameter block (BPB),
is located in the same sector as the operating-system loader. From the
contents of the BPB, the operating-system loader calculates the
location of the root directory for the boot disk so that it can verify
that the first two entries in the root directory are IO.SYS and
MSDOS.SYS. For versions of MS-DOS through 3.2, these files must also
be the first two files in the file data area, and they must be
contiguous. (The operating-system loader usually does not check the
file allocation table [FAT] to see if IO.SYS and MSDOS.SYS are
actually stored in contiguous sectors.) See PROGRAMMING IN
THE MS-DOS ENVIRONMENT: STRUCTURE OF MS-DOS: MS-DOS Storage Devices.
┌───────────────────┐
│ Boot sector │──First sector on the disk
├───────────────────┤
│Reserved (optional)│
├───────────────────┤
│ FAT#1 │
├───────────────────┤
│ FAT#2 │
├───────────────────┤
│ Root directory │
├───────────────────┤
│ IO.SYS │
├───────────────────┤
│ MSDOS.SYS │
├───────────────────┤
│ │
│ │
│ File data area │
│ │
└─────┐ │
┌────┐└───────┐ │
│ └───────┐└─────┘
│ └──────┐
└───────────────────┘
Figure 2-4. Boot-disk configuration.
Next, the operating-system loader reads the sectors containing IO.SYS
and MSDOS.SYS into contiguous areas of memory just above the ROM BIOS
tables (Figure 2-5). (An alternative method is to take advantage
of the operating-system loader's final jump to the entry point in
IO.SYS and include routines in IO.SYS that allow it to load
MSDOS.SYS.)
Finally, assuming the file was loaded without any errors, the
operating-system loader transfers control to IO.SYS, passing the
identity of the boot device. The operating-system loader is no longer
needed and its RAM is made available for other purposes.
┌───────────────────┐──FFFF:000FH (1 MB)
│ ROM BIOS │
├───────────────────┤──F000:0000H
│ │
│ Other ROM and RAM │
│ │
├───────────────────┤──Top of RAM
│ │ (A000:0000H for IBM PC)
│ Possible free RAM │
│ │
├───────────────────┤
│ Boot sector │──Arbitrary location
├───────────────────┤
│ │
│ │
│ │
│ Free RAM │
│ │
│ │
│ │
├───────────────────┤
│ MSDOS.SYS │
├───────────────────┤
│ │
│ IO.SYS │──SYSINIT
│ │──MS-DOS BIOS (resident device drivers)
├───────────────────┤──0000:0600H
│ ROM BIOS tables │
├───────────────────┤──0000:0400H
│ Interrupt vectors │
│ │
└───────────────────┘──0000:0000H
Figure 2-5. IO.SYS and MSDOS.SYS loaded.
MS-DOS system initialization (SYSINIT)
MS-DOS system initialization begins after the operating-system loader
has loaded IO.SYS and MSDOS.SYS and transferred control to the
beginning of IO.SYS. To this point, there has been no standard loading
procedure imposed by MS-DOS, although the IBM PC loading procedure
outlined here has become the de facto standard for most MS-DOS
machines. When control is transferred to IO.SYS, however, MS-DOS
imposes its standards.
The IO.SYS file is divided into three modules:
■ The resident device drivers
■ The basic MS-DOS BIOS initialization module
■ The MS-DOS system initialization module, SYSINIT
The two initialization modules are usually discarded as soon as MS-DOS
is completely initialized and the shell program is running; the
resident device drivers remain in memory while MS-DOS is running and
are therefore placed in the first part of the IO.SYS file, before the
initialization modules.
The MS-DOS BIOS initialization module ordinarily displays a sign-on
message and the copyright notice for the OEM that created IO.SYS. On
IBM-compatible machines, it then examines entries in the interrupt
table to determine what devices were found by the ROM BIOS at POST
time and adjusts the list of resident device drivers accordingly. This
adjustment usually entails removing those drivers that have no
corresponding installed hardware. The initialization routine may also
modify internal tables within the device drivers. The device driver
initialization routines will be called later by SYSINIT, so the MS-DOS
BIOS initialization routine is now essentially finished and control is
transferred to the SYSINIT module.
SYSINIT locates the top of RAM and copies itself there. It then
transfers control to the copy and the copy proceeds with system
initialization. The first step is to move MSDOS.SYS, which contains
the MS-DOS kernel, to a position immediately following the end of the
resident portion of IO.SYS, which contains the resident device
drivers. This move overwrites the original copy of SYSINIT and usually
all of the MS-DOS BIOS initialization routine, which are no longer
needed. The resulting memory layout is shown in Figure 2-6.
┌───────────────────┐──FFFF:000FH (1 MB)
│ ROM BIOS │
├───────────────────┤──F000:0000H
│ │
│ Other ROM and RAM │
│ │
├───────────────────┤──Top of RAM
│ │ (A000:0000H for IBM PC)
│ SYSINIT │
│ │
├───────────────────┤
│ │
│ Free RAM │
│ │
├───────────────────┤
│ MS-DOS kernel │
│ (MSDOS.SYS) │
├───────────────────┤
│ MS-DOS BIOS │──Resident device drivers
│ (IO.SYS) │
├───────────────────┤──0000:0600H
│ ROM BIOS tables │
├───────────────────┤──0000:0400H
│ Interrupt vectors │
└───────────────────┘──0000:0000H
Figure 2-6. SYSINIT and MSDOS.SYS relocated.
SYSINIT then calls the initialization routine in the newly relocated
MS-DOS kernel. This routine performs the internal setup for the
kernel, including putting the appropriate values into the vectors for
Interrupts 20H through 3FH.
The MS-DOS kernel initialization routine then calls the initialization
function of each resident device driver to set up vectors for any
external hardware interrupts used by the device. Each block-device
driver returns a pointer to a BPB for each drive that it supports;
these BPBs are inspected by SYSINIT to find the largest sector size
used by any of the drivers. See PROGRAMMING IN THE MS-DOS ENVIRONMENT:
STRUCTURE OF MS-DOS: MS-DOS Storage Devices. The kernel initialization
routine then allocates a sector buffer the size of the largest sector
found and places the NUL device driver at the head of the device
driver list.
The kernel initialization routine's final operation before returning
to SYSINIT is to display the MS-DOS copyright message. The loading of
the system portion of MS-DOS is now complete and SYSINIT can use any
MS-DOS function in conjunction with the resident set of device
drivers.
SYSINIT next attempts to open the CONFIG.SYS file in the root
directory of the boot drive. If the file does not exist, SYSINIT uses
the default system parameters; if the file is opened, SYSINIT reads
the entire file into high memory and converts all characters to
uppercase. The file contents are then processed to determine such
settings as the number of disk buffers, the number of entries in the
file tables, and the number of entries in the drive translation table
(depending on the specific commands in the file), and these structures
are allocated following the MS-DOS kernel (Figure 2-7).
Then SYSINIT processes the CONFIG.SYS text sequentially to determine
what installable device drivers are to be implemented and loads the
installable device driver files into memory after the system disk
buffers and the file and drive tables. Installable device driver files
can be located in any directory on any drive whose driver has already
been loaded. Each installable device driver initialization function is
called after the device driver file is loaded into memory. The
initialization procedure is the same as for resident device drivers,
except that SYSINIT uses an address returned by the device driver
itself to determine where the next device driver is to be placed. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Installable
Device Drivers.
┌───────────────────┐──FFFF:000FH (1 MB)
│ ROM BIOS │
├───────────────────┤──F000:0000H
│ │
│ Other ROM and RAM │
│ │
├───────────────────┤──Top of RAM
│ │ (A000:0000H for IBM PC)
│ SYSINIT │
│ │
├───────────────────┤
│ │
│ │
│ Free RAM │
│ │
│ │
├───────────────────┤
│ Installable │
│ device drivers │
├───────────────────┤
│File control blocks│
├───────────────────┤
│ Disk buffers │
├───────────────────┤
│ MS-DOS tables │
├───────────────────┤
│ MS-DOS kernel │
│ (MSDOS.SYS) │
├───────────────────┤
│ MS-DOS BIOS │──Resident device drivers
│ (IO.SYS) │
├───────────────────┤──0000:0600H
│ ROM BIOS tables │
├───────────────────┤──0000:0400H
│ Interrupt vectors │
│ │
└───────────────────┘──0000:0000H
Figure 2-7. Tables allocated and installable device drivers loaded.
Like resident device drivers, installable device drivers can be
discarded by SYSINIT if the device driver initialization routine
determines that a device is inoperative or nonexistent. A discarded
device driver is not included in the list of device drivers.
Installable character-device drivers supersede resident character-
device drivers with the same name; installable block-device drivers
cannot supersede resident block-drivers and are assigned drive letters
following those of the resident block-device drivers.
SYSINIT now closes all open files and then opens the three character
devices CON, PRN, and AUX. The console (CON) is used as standard
input, standard output, and standard error; the standard printer port
is PRN (which defaults to LPT1); the standard auxiliary port is AUX
(which defaults to COM1). Installable device drivers with these names
will replace any resident versions.
Starting the shell
SYSINIT's last function is to load and execute the shell program by
using the MS-DOS EXEC function. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: The MS-DOS EXEC Function. The
SHELL statement in CONFIG.SYS specifies both the name of the shell
program and its initial parameters; the default MS-DOS shell is
COMMAND.COM. The shell program is loaded at the start of free memory
after the installable device drivers or after the last internal MS-DOS
file control block if there are no installable device drivers (Figure
2-8).
COMMAND.COM
COMMAND.COM consists of three parts:
■ A resident portion
■ An initialization module
■ A transient portion
The resident portion contains support for termination of programs
started by COMMAND.COM and presents critical-error messages. It is
also responsible for reloading the transient portion when necessary.
The initialization module is called once by the resident portion.
First, it moves the transient portion to high memory. (Compare Figures
2-8 and 2-9.) Then it processes the parameters specified in the SHELL
command in the CONFIG.SYS file, if any. See USER COMMANDS: COMMAND.
Next, it processes the AUTOEXEC.BAT file, if one exists, and finally,
it transfers control back to the resident portion, which frees the
space used by the initialization module and transient portion. The
relocated transient portion then displays the MS-DOS user prompt and
is ready to accept commands.
The transient portion gets a command from either the console or a
batch file and executes it. Commands are divided into three
categories:
■ Internal commands
■ Batch files
■ External commands
Internal commands are routines contained within COMMAND.COM and
include operations like COPY or ERASE. Execution of an internal
command does not overwrite the transient portion. Internal commands
consist of a keyword, sometimes followed by a list of command-specific
parameters.
┌───────────────────┐──FFFF:000FH (1 MB)
│ ROM BIOS │
├───────────────────┤──F000:0000H
│ │
│ Other ROM and RAM │
│ │
├───────────────────┤──Top of RAM
│ │ (A000:0000H for IBM PC)
│ SYSINIT │
│ │
├───────────────────┤
│ │
│ │
│ Free RAM │
│ │
│ │
├───────────────────┤
│ COMMAND.COM │
│ (transient) │
├───────────────────┤
│ COMMAND.COM │
│ (initialization) │
├───────────────────┤
│ COMMAND.COM │
│ (resident) │
├───────────────────┤
│ Installable │
│ device drivers │
├───────────────────┤
│File control blocks│
├───────────────────┤
│ Disk buffers │
├───────────────────┤
│ MS-DOS tables │
├───────────────────┤
│ MS-DOS kernel │
│ (MSDOS.SYS) │
├───────────────────┤
│ MS-DOS BIOS │──Resident device drivers
│ (IO.SYS) │
├───────────────────┤──0000:0600H
│ ROM BIOS tables │
├───────────────────┤──0000:0400H
│ Interrupt vectors │
└───────────────────┘──0000:0000H
Figure 2-8. COMMAND.COM loaded.
┌───────────────────┐──FFFF:000FH (1 MB)
│ ROM BIOS │
├───────────────────┤──F000:0000H
│ │
│ Other ROM and RAM │
│ │
├───────────────────┤──Top of RAM
│ COMMAND.COM │ (A000:0000H for IBM PC)
│ (transient) │
├───────────────────┤
│ │
│ │
│ Free RAM │
│ │
│ │
├───────────────────┤
│ COMMAND.COM │
│ (resident) │
├───────────────────┤
│ Installable │
│ device drivers │
├───────────────────┤
│File control blocks│
├───────────────────┤
│ Disk buffers │
├───────────────────┤
│ MS-DOS tables │
├───────────────────┤
│ MS-DOS kernel │
│ (MSDOS.SYS) │
├───────────────────┤
│ MS-DOS BIOS │──Resident device drivers
│ (IO.SYS) │
├───────────────────┤──0000:0600H
│ ROM BIOS tables │
├───────────────────┤──0000:0400H
│ Interrupt vectors │
│ │
└───────────────────┘──0000:0000H
Figure 2-9. COMMAND.COM after relocation.
Batch files are text files that contain internal commands, external
commands, batch-file directives, and nonexecutable comments. See USER
COMMANDS: BATCH.
External commands, which are actually executable programs, are stored
in separate files with .COM and .EXE extensions and are included on
the MS-DOS distribution disks. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: Structure of an Application
Program. These programs are invoked with the name of the file without
the extension. (MS-DOS versions 3.x allow the complete pathname of the
external command to be specified.)
External commands are loaded by COMMAND.COM by means of the MS-DOS
EXEC function. The EXEC function loads a program into the free memory
area, also called the transient program area (TPA), and then passes it
control. Control returns to COMMAND.COM when the new program
terminates. Memory used by the program is released unless it is a
terminate-and-stay-resident (TSR) program, in which case some of the
memory is retained for the resident portion of the program. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Terminate-
and-Stay-Resident Utilities.
After a program terminates, the resident portion of COMMAND.COM checks
to see if the transient portion is still valid, because if the program
was large, it may have overwritten the transient portion's memory
space. The validity check is done by computing a checksum on the
transient portion and comparing it with a stored value. If the
checksums do not match, the resident portion loads a new copy of the
transient portion from the COMMAND.COM file.
Just as COMMAND.COM uses the EXEC function to load and execute a
program, programs can load and execute other programs until the system
runs out of memory. Figure 2-10 shows a typical memory configuration
for multiple applications loaded at the same time. The active task--
the last one executed--ordinarily has complete control over the
system, with the exception of the hardware interrupt handlers, which
gain control whenever a hardware interrupt needs to be serviced.
MS-DOS is not a multitasking operating system, so although several
programs can be resident in memory, only one program can be active at
a time. The stack-like nature of the system is apparent in Figure
2-10. The top program is the active one; the next program down will
continue to run when the top program exits, and so on until control
returns to COMMAND.COM. RAM-resident programs that remain in memory
after they have terminated are the exception. In this case, a program
lower in memory than another program can become the active program,
although the one-active-process limit is still in effect.
┌───────────────────┐──FFFF:000FH (1 MB)
│ ROM BIOS │
├───────────────────┤──F000:0000H
│ │
│ Other ROM and RAM │
│ │
├───────────────────┤──Top of RAM
│ COMMAND.COM │ (A000:0000H for IBM PC)
│ (transient) │
├───────────────────┤
│ │
│ Free RAM │
│ │
├───────────────────┤
│ Program #3 │
│ (active) │
├───────────────────┤
│ Program #2 │
├───────────────────┤
│ Program #1 │
├───────────────────┤
│ COMMAND.COM │
│ (resident) │
├───────────────────┤
│ Installable │
│ device drivers │
├───────────────────┤
│File control blocks│
├───────────────────┤
│ Disk buffers │
├───────────────────┤
│ MS-DOS tables │
├───────────────────┤
│ MS-DOS kernel │
│ (MSDOS.SYS) │
├───────────────────┤
│ MS-DOS BIOS │──Resident device drivers
│ (IO.SYS) │
├───────────────────┤──0000:0600H
│ ROM BIOS tables │
├───────────────────┤──0000:0400H
│ Interrupt vectors │
│ │
└───────────────────┘──0000:0000H
Figure 2-10. Multiple programs loaded.
A custom shell program
The SHELL directive in the CONFIG.SYS file can be used to replace the
system's default shell, COMMAND.COM, with a custom shell. Nearly any
program can be used as a system shell as long as it supplies default
handlers for the Control-C and critical error exceptions. For example,
the program in Figure 2-11 can be used to make any application program
appear to be a shell program--if the application program terminates,
SHELL.COM restarts it, giving the appearance that the application
program is the shell program.
SHELL.COM sets up the segment registers for operation as a .COM file
and reduces the program segment size to less than 1 KB. It then
initializes the segment values in the parameter table for the EXEC
function, because .COM files cannot set up segment values within a
program. The Control-C and critical error interrupt handler vectors
are set to the address of the main program loop, which tries to load
the new shell program. SHELL.COM prints a message if the EXEC
operation fails. The loop continues forever and SHELL.COM will never
return to the now-discarded SYSINIT that started it.
──────────────────────────────────────────────────────────────────────
Figure 2-11. A simple program to run an application as an MS-DOS
shell.
──────────────────────────────────────────────────────────────────────
SHELL.COM is very short and not too smart. It needs to be changed and
rebuilt if the name of the application program changes. A simple
extension to SHELL--call it XSHELL-would be to place the name of the
application program and any parameters in the command line. XSHELL
would then have to parse the program name and the contents of the two
FCBs needed for the EXEC function. The CONFIG.SYS line for starting
this shell would be
SHELL=XSHELL \SHELL\DEMO.EXE PARAM1 PARAM2 PARAM3
SHELL.COM does not set up a new environment but simply uses the one
passed to it.
William Wong
Article 3: MS-DOS Storage Devices
Application programs access data on MS-DOS storage devices through the
MS-DOS file-system support that is part of the MS-DOS kernel. The
MS-DOS kernel accesses these storage devices, also called block
devices, through two types of device drivers: resident block-device
drivers contained in IO.SYS and installable block-device drivers
loaded from individual files when MS-DOS is loaded. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: STRUCTURE OF MS-DOS: The
Components of MS-DOS; CUSTOMIZING MS-DOS: Installable
Device Drivers.
MS-DOS can handle almost any medium, recording method, or other
variation for a storage device as long as there is a device driver for
it. MS-DOS needs to know only the sector size and the maximum number
of sectors for the device; the appropriate translation between logical
sector number and physical location is made by the device driver.
Information about the number of heads, tracks, and so on is required
only for those partitioning programs that allocate logical devices
along these boundaries. See Layout of a Partition, below.
The floppy-disk drive is perhaps the best-known block device, followed
by its faster cousin, the fixed-disk drive. Other MS-DOS media include
RAMdisks, nonvolatile RAMdisks, removable hard disks, tape drives, and
CD ROM drives. With the proper device driver, MS-DOS can place a file
system on any of these devices (except read-only media such as CD
ROM).
This article discusses the structure of the file system on floppy and
fixed disks, starting with the physical layout of a disk and then
moving on to the logical layout of the file system. The scheme
examined is for the IBM PC fixed disk.
Structure of an MS-DOS Disk
The structure of an MS-DOS disk can be viewed in a number of ways:
■ Physical device layout
■ Logical device layout
■ Logical block layout
■ MS-DOS file system
The physical layout of a disk is expressed in terms of sectors,
tracks, and heads. The logical device layout, also expressed in terms
of sectors, tracks, and heads, indicates how a logical device maps
onto a physical device. A partitioned physical device contains
multiple logical devices; a physical device that cannot be partitioned
contains only one. Each logical device has a logical block layout used
by MS-DOS to implement a file system. These various views of an MS-DOS
disk are discussed below. See also PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: File and Record Management; Disk
Directories and Volume Labels.
Layout of a physical block device
The two major block-device implementations are solid-state RAMdisks
and rotating magnetic media such as floppy or fixed disks. Both
implementations provide a fixed amount of storage in a fixed number of
randomly accessible same-size sectors.
RAMdisks
A RAMdisk is a block device that has sectors mapped sequentially into
RAM. Thus, the RAMdisk is viewed as a large set of sequentially
numbered sectors whose addresses are computed by simply multiplying
the sector number by the sector size and adding the base address of
the RAMdisk sector buffer. Access is fast and efficient and the access
time to any sector is fixed, making the RAMdisk the fastest block
device available. However, there are significant drawbacks to
RAMdisks. First, they are volatile; their contents are irretrievably
lost when the computer's power is turned off (although a special
implementation of the RAMdisk known as a nonvolatile RAMdisk includes
a battery backup system that ensures that its contents are not lost
when the computer's power is turned off). Second, they are usually not
portable.
Physical disks
Floppy-disk and fixed-disk systems, on the other hand, store
information on revolving platters coated with a special magnetic
material. The disk is rotated in the drive at high speeds--
approximately 300 revolutions per minute (rpm) for floppy disks and
3600 rpm for fixed disks. (The term "fixed" refers to the fact that
the medium is built permanently into the drive, not to the motion of
the medium.) Fixed disks are also referred to as "hard" disks, because
the disk itself is usually made from a rigid material such as metal or
glass; floppy disks are usually made from a flexible material such as
plastic.
A transducer element called the read/write head is used to read and
write tiny magnetic regions on the rotating magnetic medium. The
regions act like small bar magnets with north and south poles. The
magnetic regions of the medium can be logically oriented toward one or
the other of these poles--orientation toward one pole is interpreted
as a specific binary state (1 or 0) and orientation toward the other
pole is interpreted as the opposite binary state. A change in the
direction of orientation (and hence a change in the binary value)
between two adjacent regions is called a flux reversal, and the
density of a particular disk implementation can be measured by the
number of regions per inch reliably capable of flux reversal. Higher
densities of these regions yield higher-capacity disks. The flux
density of a particular system depends on the drive mechanics, the
characteristics of the read/write head, and the magnetic properties of
the medium.
The read/write head can encode digital information on a disk using a
number of recording techniques, including frequency modulation (FM),
modified frequency modulation (MFM), run length limited (RLL)
encoding, and advanced run length limited (ARLL) encoding. Each
technique offers double the data encoding density of the previous one.
The associated control logic is more complex for the denser
techniques.
Tracks
A read/write head reads data from or writes data to a thin section of
the disk called a track, which is laid out in a circular fashion
around the disk (Figure 3-1). Standard 5.25-inch floppy disks contain
either 40 (0-39) or 80 (0-79) tracks per side. Like-numbered tracks on
either side of a double-sided disk are distinguished by the number of
the read/write head used to access the track. For example, track 1 on
the top of the disk is identified as head 0, track 1; track 1 on the
bottom of the disk is identified as head 1, track 1.
Tracks can be either spirals, as on a phonograph record, or concentric
rings. Computer media usually use one of two types of concentric
rings. The first type keeps the same number of sectors on each track
(see Sectors, below) and is rotated at a constant angular velocity
(CAV). The second type maintains the same recording density across the
entire surface of the disk, so a track near the center of a disk
contains fewer sectors than a track near the perimeter. This latter
type of disk is rotated at different speeds to keep the medium under
the magnetic head moving at a constant linear velocity (CLV).
╔══════════════════════════════════════════╗
║ ║
║ Figure 3-1 is found on page 87 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 3-1. The physical layout of a CAV 9-sector, 5.25-inch floppy
disk.
Most MS-DOS computers use CAV disks, although a CLV disk can store
more sectors using the same type of medium. This difference in storage
capacity occurs because the limiting factor is the flux density of the
medium and a CAV disk must maintain the same number of magnetic flux
regions per sector on the interior of the disk as at the perimeter.
Thus, the sectors on or near the perimeter do not use the full
capability of the medium and the heads, because the space reserved for
each magnetic flux region on the perimeter is larger than that
available near the center of the disk. In spite of their greater
storage capacity, however, CLV disks (such as CD ROMs) usually have
slower access times than CAV disks because of the constant need to
fine-tune the motor speed as the head moves from track to track. Thus,
CAV disks are preferred for MS-DOS systems.
Heads
Simple disk systems use a single disk, or platter, and use one or two
sides of the platter; more complex systems, such as fixed disks, use
multiple platters. Disk systems that use both sides of a disk have one
read/write head per side; the heads are positioned over the track to
be read from or written to by means of a positioning mechanism such as
a solenoid or servomotor. The heads are ordinarily moved in unison,
using a single head-movement mechanism; thus, heads on opposite sides
of a platter in a double-sided disk system typically access the same
logical track on their associated sides of the platter. (Performance
can be increased by increasing the number of heads to as many as one
head per track, eliminating the positioning mechanism. However,
because they are quite expensive, such multiple-head systems are
generally found only on high-performance minicomputers and
mainframes.)
The set of like-numbered tracks on the two sides of a platter (or on
all sides of all platters in a multiplatter system) is called a
cylinder. Disks are usually partitioned along cylinders. Tracks and
cylinders may appear to have the same meaning; however, the term track
is used to define a concentric ring containing a specific number of
sectors on a single side of a single platter, whereas the term
cylinder refers to the number of like-numbered tracks on a device
(Figure 3-2).
╔══════════════════════════════════════════╗
║ ║
║ Figure 3-2 is found on page 88 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 3-2. Tracks and cylinders on a fixed-disk system.
Sectors
Each track is divided into equal-size portions called sectors. The
size of a sector is a power of 2 and is usually greater than 128
bytes--typically, 512 bytes.
Floppy disks are either hard-sectored or soft-sectored, depending on
the disk drive and the medium. Hard-sectored disks are implemented
using a series of small holes near the center of the disk that
indicate the beginning of each sector; these holes are read by a
photosensor/LED pair built into the disk drive. Soft-sectored disks
are implemented by magnetically marking the beginning of each sector
when the disk is formatted. A soft-sectored disk has a single hole
near the center of the disk (see Figure 3-1) that marks the location
of sector 0 for reference when the disk is formatted or when error
detection is performed; this hole is also read by a photosensor/LED
pair. Fixed disks use a special implementation of soft sectors (see
below). A hard-sectored floppy disk cannot be used in a disk drive
built for use with soft-sectored floppy disks (and vice versa).
In addition to a fixed number of data bytes, both sector types include
a certain amount of overhead information, such as error correction and
sector identification, in each sector. The structure of each sector is
implemented during the formatting process.
Standard fixed disks and 5.25-inch floppy disks generally have from 8
to 17 physical sectors per track. Sectors are numbered beginning at 1.
Each sector is uniquely identified by a complete specification of the
read/write head, cylinder number, and sector number. To access a
particular sector, the disk drive controller hardware moves all heads
to the specified cylinder and then activates the appropriate head for
the read or write operation.
The read/write heads are mechanically positioned using one of two
hardware implementations. The first method, used with floppy disks,
employs an "open-loop" servomechanism in which the software computes
where the heads should be and the hardware moves them there. (A
servomechanism is a device that can move a solenoid or hold it in a
fixed position.) An open-loop system employs no feedback mechanism to
determine whether the heads were positioned correctly--the hardware
simply moves the heads to the requested position and returns an error
if the information read there is not what was expected. The
positioning mechanism in floppy-disk drives is made with close
tolerances because if the positioning of the heads on two drives
differs, disks written on one might not be usable on the other.
Most fixed disk systems use the second method--a "closed-loop"
servomechanism that reserves one side of one platter for positioning
information. This information, which indicates where the tracks and
sectors are located, is written on the disk at the factory when the
drive is assembled. Positioning the read/write heads in a closed-loop
system is actually a two-step process: First, the head assembly is
moved to the approximate location of the read or write operation; then
the disk controller reads the closed-loop servo information, compares
it to the desired location, and fine-tunes the head position
accordingly. This fine-tuning approach yields faster access times and
also allows for higher-capacity disks because the positioning can be
more accurate and the distances between tracks can therefore be
smaller. Because the "servo platter" usually has positioning
information on one side and data on the other, many systems have an
odd number of read/write heads for data.
Interleaving
CAV MS-DOS disks are described in terms of bytes per sector, sectors
per track, number of cylinders, and number of read/write heads.
Overall access time is based on how fast the disk rotates (rotational
latency) and how fast the heads can move from track to track (track-
to-track latency).
On most fixed disks, the sectors on the disk are logically or
physically numbered so that logically sequential sectors are not
physically adjacent (Figure 3-3). The underlying principle is that,
because the controller cannot finish processing one sector before the
next sequential sector arrives under the read/write head, the
logically numbered sectors must be staggered around the track. This
staggering of sectors is called skewing or, more commonly,
interleaving. A 2-to-1 (2:1) interleave places sequentially accessed
sectors so that there is one additional sector between them; a 3:1
interleave places two additional sectors between them. A slower disk
controller needs a larger interleave factor. A 3:1 interleave means
that three revolutions are required to read all sectors on a track in
numeric order.
╔══════════════════════════════════════════╗
║ ║
║ Figure 3-3 is found on page 90 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 3-3. A 3:1 interleave.
One approach to improving fixed-disk performance is to decrease the
interleave ratio. This generally requires a specialized utility
program and also requires that the disk be reformatted to adjust to
the new layout. Obviously, a 1:1 interleave is the most efficient,
provided the disk controller can process at that speed. The normal
interleave for an IBM PC/AT and its standard fixed disk and disk
controller is 3:1, but disk controllers are available for the PC/AT
that are capable of handling a 1:1 interleave. Floppy disks on MS-DOS-
based computers all have a 1:1 interleave ratio.
Layout of a partition
For several reasons, large physical block devices such as fixed disks
are often logically partitioned into smaller logical block devices
(Figure 3-4). For instance, such partitions allow a device to be
shared among different operating systems. Partitions can also be used
to keep the size of each logical device within the PC-DOS 32 MB
restriction (important for large fixed disks). MS-DOS permits a
maximum of four partitions.
╔══════════════════════════════════════════╗
║ ║
║ Figure 3-4 is found on page 91 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 3-4. A partitioned disk.
A partitioned block device has a partition table located in one sector
at the beginning of the disk. This table indicates where the logical
block devices are physically located. (Even a partitioned device with
only one partition usually has such a table.)
Under the MS-DOS partitioning standard, the first physical sector on
the fixed disk contains the partition table and a bootstrap program
capable of checking the partition table for a bootable partition,
loading the bootable partition's boot sector, and transferring control
to it. The partition table, located at the end of the first physical
sector of the disk, can contain a maximum of four entries:
╓┌────────────────────────┌───────────────────┌──────────────────────────────╖
Offset From
Start of Sector Size (bytes) Description
──────────────────────────────────────────────────────────────────
01BEH 16 Partition #4
01CEH 16 Partition #3
01DEH 16 Partition #2
01EEH 16 Partition #1
01FEH 2 Signature: AA55H
The partitions are allocated in reverse order. Each 16-byte entry
contains the following information:
╓┌─────────────────────┌─────────────┌───────────────────────────────────────╖
Offset From
Start of Entry Size (bytes) Description
──────────────────────────────────────────────────────────────────
00H 1 Boot indicator
01H 1 Beginning head
02H 1 Beginning sector
03H 1 Beginning cylinder
04H 1 System indicator
05H 1 Ending head
06H 1 Ending sector
07H 1 Ending cylinder
08H 4 Starting sector (relative to
beginning of disk)
0CH 4 Number of sectors in partition
The boot indicator is zero for a nonbootable partition and 80H for a
bootable (active) partition. A fixed disk can have only one bootable
partition. (When setting a bootable partition, partition programs such
as FDISK reset the boot indicators for all other partitions to zero.)
See USER COMMANDS: FDISK.
The system indicators are
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Code Meaning
──────────────────────────────────────────────────────────────────
00H Unknown
01H MS-DOS, 12-bit FAT
04H MS-DOS, 16-bit FAT
Each partition's boot sector is located at the start of the partition,
which is specified in terms of beginning head, beginning sector, and
beginning cylinder numbers. This information, stored in the partition
table in this order, is loaded into the DX and CX registers by the PC
ROM BIOS loader routine when the machine is turned on or restarted.
The starting sector of the partition relative to the beginning of the
disk is also indicated. The ending head, sector, and cylinder numbers,
also included in the partition table, specify the last accessible
sector for the partition. The total number of sectors in a partition
is the difference between the starting and ending head and cylinder
numbers times the number of sectors per cylinder.
MS-DOS versions 2.0 through 3.2 allow only one MS-DOS partition per
partitioned device. Various device drivers have been implemented that
use a different partition table that allows more than one MS-DOS
partition to be installed, but the secondary MS-DOS partitions are
usually accessible only by means of an installable device driver that
knows about this change. (Even with additional MS-DOS partitions, a
fixed disk can have only one bootable partition.)
Layout of a file system
Block devices are accessed on a sector basis. The MS-DOS kernel,
through the device driver, sees a block device as a logical fixed-size
array of sectors and assumes that the array contains a valid MS-DOS
file system. The device driver, in turn, translates the logical sector
requests from MS-DOS into physical locations on the block device.
The initial MS-DOS file system is written to the storage medium by the
MS-DOS FORMAT program. See USER COMMANDS: FORMAT. The general layout
for the file system is shown in Figure 3-5.
┌────────────────────────────────────────────────────────┐
│OEM identification, BIOS parameter block, Loader routine│
│ Reserved area │
├────────────────────────────────────────────────────────┤
│ File allocation table (FAT) #1 │
├────────────────────────────────────────────────────────┤
│ Possible additional copies of FAT │
├────────────────────────────────────────────────────────┤
│ │
│ Root disk directory │
│ │
├────────────────────────────────────────────────────────┤
│ │
│ │
│ │
│ Files area │
│ │
└───────────────────┐ │
┌──────────────────┐└──────────────────┐ │
│ └──────────────────┐└─────────────────┘
│ └──────────────────┐
│ │
└────────────────────────────────────────────────────────┘
Figure 3-5. The MS-DOS file system.
The boot sector is always at the beginning of a partition. It contains
the OEM identification, a loader routine, and a BIOS parameter block
(BPB) with information about the device, and it is followed by an
optional area of reserved sectors. See The Boot Sector, below. The
reserved area has no specific use, but an OEM might require a more
complex loader routine and place it in this area. The file allocation
tables (FATs) indicate how the file data area is allocated; the root
directory contains a fixed number of directory entries; and the file
data area contains data files, subdirectory files, and free data
sectors.
All the areas just described--the boot sector, the FAT, the root
directory, and the file data area--are of fixed size; that is, they do
not change after FORMAT sets up the medium. The size of each of these
areas depends on various factors. For instance, the size of the FAT is
proportional to the file data area. The root directory size ordinarily
depends on the type of device; a single-sided floppy disk can hold 64
entries, a double-sided floppy disk can hold 112, and a fixed disk can
hold 256. (RAMdisk drivers such as RAMDRIVE.SYS and some
implementations of FORMAT allow the number of directory entries to be
specified.)
The file data area is allocated in terms of clusters. A cluster is a
fixed number of contiguous sectors. Sector size and cluster size must
be a power of 2. The sector size is usually 512 bytes and the cluster
size is usually 1, 2, or 4 KB, but larger sector and cluster sizes are
possible. Commonly used MS-DOS cluster sizes are
╓┌────────────────────────────────┌───────────────────┌──────────────────────╖
Disk Type Sectors/Cluster Bytes/Cluster
──────────────────────────────────────────────────────────────────
Single-sided floppy disk 1 512
Double-sided floppy disk 2 1024
PC/AT fixed disk 4 2048
PC/XT fixed disk 8 4096
Other fixed disks 16 8192
Other fixed disks 32 16384
In general, larger cluster sizes are used to support larger fixed
disks. Although smaller cluster sizes make allocation more space
-efficient, larger clusters are usually more efficient for random and
sequential access, especially if the clusters for a single file are
not sequentially allocated.
The file allocation table contains one entry per cluster in the file
data area. Doubling the sectors per cluster will also halve the number
of FAT entries for a given partition. See The File Allocation Table,
below.
The boot sector
The boot sector (Figure 3-6) contains a BIOS parameter block, a loader
routine, and some other fields useful to device drivers. The BPB
describes a number of physical parameters of the device, as well as
the location and size of the other areas on the device. The device
driver returns the BPB information to MS-DOS when requested, so that
MS-DOS can determine how the disk is configured.
00H ┌───────────────────────────────────────────┐
│ E9 XX XX or EB XX 90 │
03H ├───────────────────────────────────────────┤
│ │
│ OEM name and version (8 bytes) │
│ │
0BH ├───────────────────────────────────────────┤
│ Bytes per sector (2 bytes) │▒
0DH ├───────────────────────────────────────────┤▒
│ Sectors per allocation unit (1 byte) │▒
0EH ├───────────────────────────────────────────┤▒
│ Reserved sectors, starting at 0 (2 bytes) │▒
10H ├───────────────────────────────────────────┤▒
│ Number of FATs (1 byte) │▒
11H ├───────────────────────────────────────────┤▒ BPB
│Number of root-directory entries (2 bytes) │▒
13H ├───────────────────────────────────────────┤▒
│ Total sectors in logical volume (2 bytes) │▒
15H ├───────────────────────────────────────────┤▒
│ Media descriptor byte │▒
16H ├───────────────────────────────────────────┤▒
│ Number of sectors per FAT (2 bytes) │▒
18H ├───────────────────────────────────────────┤
│ Sectors per track (2 bytes) │
1AH ├───────────────────────────────────────────┤
│ Number of heads (2 bytes) │
1CH ├───────────────────────────────────────────┤
│ Number of hidden sectors (2 bytes) │
1EH ├───────────────────────────────────────────┤
│ │
│ │
│ │
│ Loader routine │
│ │
│ │
│ │
│ │
│ │
└───────────────────────────────────────────┘
Figure 3-6. Map of the boot sector of an MS-DOS disk. Bytes 0BH
through 17H are the BIOS parameter block (BPB).
Figure 3-7 is a hexadecimal dump of an actual boot sector. The first 3
bytes of the boot sector shown in Figure 3-7 would be E9H 2CH 00H if a
long jump were used instead of a short one (as in early versions of
MS-DOS). The last 2 bytes in the sector, 55H and AAH, are a fixed
signature used by the loader routine to verify that the sector is a
valid boot sector.
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 EB 2D 90 20 20 20 20 20-20 20 20 00 02 02 01 00 k-. .....
0010 02 70 00 A0 05 F9 03 00-09 00 02 00 00 00 00 00 .p. .y..........
0020 00 0A 00 00 DF 02 25 02-09 2A FF 50 F6 0A 02 FA ...._.%..*.Pv..z
0030 B8 C0 07 8E D8 BC 00 7C-33 C0 8E D0 8E C0 FB FC 8@..X<.|3@.P.@{|
.
.
.
0180 0A 44 69 73 6B 20 42 6F-6F 74 20 46 61 69 6C 75 .Disk Boot Failu
0190 72 65 0D 0A 0D 0A 4E 6F-6E 2D 53 79 73 74 65 6D re....Non-System
01A0 20 64 69 73 6B 20 6F 72-20 64 69 73 6B 20 65 72 disk or disk er
01B0 72 6F 72 0D 0A 52 65 70-6C 61 63 65 20 61 6E 64 ror..Replace and
01C0 20 70 72 65 73 73 20 61-6E 79 20 6B 65 79 20 77 press any key w
01D0 68 65 6E 20 72 65 61 64-79 0D 0A 00 00 00 00 00 hen ready.......
01E0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
01F0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 55 AA ...............*
Figure 3-7. Hexadecimal dump of an MS-DOS boot sector. The BPB is
highlighted.
The BPB information contained in bytes 0BH through 17H indicates that
there are
512 bytes per sector
2 sectors per cluster
1 reserved sector (for the boot sector)
2 FATs
112 root directory entries
1440 sectors on the disk
F9H media descriptor
3 sectors per FAT
Additional information immediately after the BPB indicates that there
are 9 sectors per track, 2 read/write heads, and 0 hidden sectors.
The media descriptor, which appears in the BPB and in the first byte
of each FAT, is used to indicate the type of medium currently in a
drive. IBM-compatible media have the following descriptors:
╓┌───────────────────┌────────────────────────────────┌──────────────────────╖
Descriptor Media Type MS-DOS Versions
──────────────────────────────────────────────────────────────────
0F8H Fixed disk 2, 3
0F0H 3.5-inch, 2-sided, 18 sector 3.2
0F9H 3.5-inch, 2-sided, 9 sector 3.2
0F9H 5.25-inch, 2-sided, 15 sector 3.x
0FCH 5.25-inch, 1-sided, 9 sector 2.x, 3.x
0FDH 5.25-inch, 2-sided, 9 sector 2.x, 3.x
0FEH 5.25-inch, 1-sided, 8 sector 1.x, 2.x, 3.x
0FFH 5.25-inch, 2-sided, 8 sector 1.x (except 1.0), 2, 3
0FEH 8-inch, 1-sided, single-density
0FDH 8-inch, 2-sided, single-density
0FEH 8-inch, 1-sided, double-density
0FDH 8-inch, 2-sided, double-density
The file allocation table
The file allocation table provides a map to the storage locations of
files on a disk by indicating which clusters are allocated to each
file and in what order. To enable MS-DOS to locate a file, the file's
directory entry contains its beginning FAT entry number. This FAT
entry, in turn, contains the entry number of the next cluster if the
file is larger than one cluster or a last-cluster number if there is
only one cluster associated with the file. A file whose size implies
that it occupies 10 clusters will have 10 FAT entries and 9 FAT links.
(The set of links for a particular file is called a chain.)
Additional copies of the FAT are used to provide backup in case of
damage to the first, or primary, FAT; the typical floppy disk or fixed
disk contains two FATs. The FATs are arranged sequentially after the
boot sector, with some possible intervening reserved area. MS-DOS
ordinarily uses the primary FAT but updates all FATs when a change
occurs. It also compares all FATs when a disk is first accessed, to
make sure they match.
MS-DOS supports two types of FAT: One uses 12-bit links; the other,
introduced with version 3.0 to accommodate large fixed disks with more
than 4087 clusters, uses 16-bit links.
The first two entries of a FAT are always reserved and are filled with
a copy of the media descriptor byte and two (for a 12-bit FAT) or
three (for a 16-bit FAT) 0FFH bytes, as shown in the following dumps
of the first 16 bytes of the FAT:
12-bit FAT:
F9 FF FF 03 40 00 FF 6F-00 07 F0 FF 00 00 00 00
16-bit FAT:
F8 FF FF FF 03 00 04 00-FF FF 06 00 07 00 FF FF
The remaining FAT entries have a one-to-one relationship with the
clusters in the file data area. Each cluster's use status is indicated
by its corresponding FAT value. (FORMAT initially marks the FAT entry
for each cluster as free.) The use status is one of the following:
╓┌──────────────────────────┌──────────────────┌─────────────────────────────╖
12-bit 16-bit Meaning
──────────────────────────────────────────────────────────────────
000H 0000H Free cluster
001H 0001H Unused code
FF0-FF6H FFF0-FFF6H Reserved
FF7H FFF7H Bad cluster; cannot be used
FF8-FFFH FFF8-FFFFH Last cluster of file
All other values All other values Link to next cluster in file
If a FAT entry is nonzero, the corresponding cluster has been
allocated. A free cluster is found by scanning the FAT from the
beginning to find the first zero value. Bad clusters are ordinarily
identified during formatting. Figure 3-8 shows a typical FAT chain.
FAT: 0 1 2 3 4 5 6 7 8 9
entry
┌─────┐ ┌───────────┐ ┌─────┐
┌──────┬──────┬───┴──┬───┴─┬──────┬───┴─┬─────┬──────┬──────┬──────┬─
│ FFDH │ FFFH │ 003H │ 005H │ FF7H │ 006H │ FFFH │ 000H │ 000H │ 000H │
│(4093)│(4095)│ (3) │ (5) │(4087)│ (6) │(4095)│ (0) │ (0) │ (0) │
└──┬───┴──┬───┴──────┴──────┴──┬───┴──────┴──────┴──┬───┴──────┴──────┴─
│ │ │ │ continues...
│ │ │ │
│ │ │ │
│ │ │ └─Unused;
│ │ │ available cluster
│ │ │
│ │ └─Unusable
│ │
│ └─Unused; not available
│
└─Disk is double-sided, double-density
Figure 3-8. Space allocation in the FAT for a typical MS-DOS disk.
Free FAT entries contain a link value of zero; a link value of 1 is
never used. Thus, the first allocatable link number, associated with
the first available cluster in the file data area, is 2, which is the
number assigned to the first physical cluster in the file data area.
Figure 3-9 shows the relationship of files, FAT entries, and clusters
in the file data area.
12-bit FAT:
Reserved 003H FFFH 007H 000H
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐
┌────┴─────┐┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │
│F9 FF FF││03│ 40 │00││FF│ 6F │00││07│ F0 │FF││00│ 00
│ └─┬┘ │ └─┬┘ │ └─┬┘
└────┘ └────┘ └────┘
004H 006H FFFH
16-bit FAT:
Reserved
│ 0003H 0004H FFFFH 0006H 0007H FFFFH 0000H
┌──────┴───────┐┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐
│F8 FF FF FF││03 00││04 00││FF FF││06 00││07 00││FF FF││00 00│
FAT entry: 0 1 2 3 4 5 6 7 8
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─
12-bit FAT: │ │ │ 003H│ 004H│ FFFH│ 006H│ 007H│ FFFH│ 000H│
│ Reserved │ │ │ │ │ │ │ │
16-bit FAT: │ │ │0003H│0004H│FFFFH│0006H│0007H│FFFFH│0000H│
└─────┴─────┴──┬─┴──┬─┴────┴──┬─┴──┬─┴────┴─────┴─
│ └───┘ └───┘ │ └───┘ └───┘continues...
Directory entry │ │
┌───────────────────────┐ │ │
│ FILE1.TXT ├─┘ │
│(points to FAT entry 2)│ │
└───────────────────────┘ │
│
┌───────────────────────┐ │
│ FILE2.TXT ├───────────────────┘
│(points to FAT entry 5)│
└───────────────────────┘
File data area Corresponding FAT entry
┌───────────────────────┐
│ FILE1.TXT │ 2
├───────────────────────┤
│ FILE1.TXT │ 3
├───────────────────────┤
│ FILE1.TXT │ 4
├───────────────────────┤
│ FILE2.TXT │ 5
├───────────────────────┤
│ FILE2.TXT │ 6
├───────────────────────┤
│ FILE2.TXT │ 7
├───────────────────────┤
│ Unused (available) │ 8
├───────────────────────┤
Figure 3-9. Correspondence between the FAT and the file data area.
There is no logical difference between the operation of the 12-bit and
16-bit FAT entries; the difference is simply in the storage and access
methods. Because the 8086 is specifically designed to manipulate 8- or
16-bit values efficiently, the access procedure for the 12-bit FAT is
more complex than that for the 16-bit FAT (see Figures 3-10
and 3-11).
Special considerations
The FAT is a highly efficient bookkeeping system, but various
tradeoffs and problems can occur. One tradeoff is having a partially
filled cluster at the end of a file. This situation leads to an
efficiency problem when a large cluster size is used, because an
entire cluster is allocated, regardless of the number of bytes it
contains. For example, ten 100-byte files on a disk with 16 KB
clusters use 160 KB of disk space; the same files on a disk with 1 KB
clusters use only 10 KB--a difference of 150 KB, or 15 times less
storage used by the smaller cluster size. On the other hand, the 12-
bit FAT routine in Figure 3-10 shows the difficulty (and therefore
slowness) of moving through a large file that has a long linked list
of many small clusters. Therefore, the nature of the data must be
considered: Large database applications work best with a larger
cluster size; a smaller cluster size allows many small text files to
fit on a disk. (The programmer writing the device driver for a disk
device ordinarily sets the cluster size.)
──────────────────────────────────────────────────────────────────────
Figure 3-10. Assembly-language routine to access a 12-bit FAT.
──────────────────────────────────────────────────────────────────────
Figure 3-11. Assembly-language routine to access a 16-bit FAT.
──────────────────────────────────────────────────────────────────────
Problems with corrupted directories or FATs, induced by such events as
power failures and programs running wild, can lead to greater problems
if not corrected. The MS-DOS CHKDSK program can detect and fix some of
these problems. See USER COMMANDS: CHKDISK. For example, one common
problem is dangling allocation lists caused by the absence of a
directory entry pointing to the start of the list. This situation
often results when the directory entry was not updated because a file
was not closed before the computer was turned off or restarted. The
effect is relatively benign: The data is inaccessible, but this
limitation does not affect other file allocation operations. CHKDSK
can fix this problem by making a new directory entry and linking it to
the list.
Another difficulty occurs when the file size in a directory entry does
not match the file length as computed by traversing the linked list in
the FAT. This problem can result in improper operation of a program
and in error responses from MS-DOS.
A more complex (and rarer) problem occurs when the directory entry is
properly set up but all or some portion of the linked list is also
referenced by another directory entry. The problem is grave, because
writing or appending to one file changes the contents of the other
file. This error usually causes severe data and/or directory
corruption or causes the system to crash.
A similar difficulty occurs when a linked list terminates with a free
cluster instead of a last-cluster number. If the free cluster is
allocated before the error is corrected, the problem eventually
reverts to the preceding problem. An associated difficulty occurs if a
link value of 1 or a link value that exceeds the size of the FAT is
encountered.
In addition to CHKDSK, a number of commercially available utility
programs can be used to assist in FAT maintenance. For instance, disk
reorganizers can be used to essentially rearrange the FAT and adjust
the directory so that all files on a disk are laid out sequentially in
the file data area and, of course, in the FAT.
The root directory
Directory entries, which are 32 bytes long, are found in both the root
directory and the subdirectories. Each entry includes a filename and
extension, the file's size, the starting FAT entry, the time and date
the file was created or last revised, and the file's attributes. This
structure resembles the format of the CP/M-style file control blocks
(FCBs) used by the MS-DOS version 1.x file functions. See PROGRAMMING
IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: Disk Directories
and Volume Labels.
The MS-DOS file-naming convention is also derived from CP/M: an eight-
character filename followed by a three-character file type, each left
aligned and padded with spaces if necessary. Within the limitations of
the character set, the name and type are completely arbitrary. The
time and date stamps are in the same format used by other MS-DOS
functions and reflect the time the file was last written to.
Figure 3-12 shows a dump of a 512-byte directory sector containing 16
directory entries. (Each entry occupies two lines in this example.)
The byte at offset 0ABH, containing a 10H, signifies that the entry
starting at 0A0H is for a subdirectory. The byte at offset 160H,
containing 0E5H, means that the file has been deleted. The byte at
offset 8BH, containing the value 08H, indicates that the directory
entry beginning at offset 80H is a volume label. Finally the zero byte
at offset 1E0H marks the end of the directory, indicating that the
subsequent entries in the directory have never been used and therefore
need not be searched (versions 2.0 and later).
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 49 4F 20 20 20 20 20 20-53 59 53 27 00 00 00 00 IO SYS'....
0010 00 00 00 00 00 00 59 53-89 0B 02 00 D1 12 00 00 ......YS....Q...
0020 4F 53 44 4F 53 20 20 20-53 59 53 27 00 00 00 00 MSDOS SYS'....
0030 00 00 00 00 00 00 41 49-52 0A 07 00 C9 43 00 00 ......AIR...IC..
0040 41 4E 53 49 20 20 20 20-53 59 53 20 00 00 00 00 ANSI SYS ....
0050 00 00 00 00 00 00 41 49-52 0A 18 00 76 07 00 00 ......AIR...v...
0060 58 54 41 4C 4B 20 20 20-45 58 45 20 00 00 00 00 XTALK EXE ....
0070 00 00 00 00 00 00 F7 7D-38 09 23 02 84 0B 01 00 ......w}8.#.....
0080 4C 41 42 45 4C 20 20 20-20 20 20 08 00 00 00 00 LABEL ....
0090 00 00 00 00 00 00 8C 20-2A 09 00 00 00 00 00 00 ....... *.D..R..
00A0 4C 4F 54 55 53 20 20 20-20 20 20 10 00 00 00 00 LOTUS ....
00B0 00 00 00 00 00 00 E0 0A-E1 06 A6 01 00 00 00 00 ......'.a.&.a...
00C0 4C 54 53 4C 4F 41 44 20-43 4F 4D 20 00 00 00 00 LTSLOAD COM ....
00D0 00 00 00 00 00 00 E0 0A-E1 06 A7 01 A0 27 00 00 ......'.a.'. '..
00E0 4D 43 49 2D 53 46 20 20-58 54 4B 20 00 00 00 00 MCI-SF XTK ....
00F0 00 00 00 00 00 00 46 19-32 0D B1 01 79 04 00 00 ......F.2.1.y...
0100 58 54 41 4C 4B 20 20 20-48 4C 50 20 00 00 00 00 XTALK HLP ....
0110 00 00 00 00 00 00 C5 6D-73 07 A3 02 AF 88 00 00 ......Ems.#./...
0120 54 58 20 20 20 20 20 20-43 4F 4D 20 00 00 00 00 TX COM ....
0130 00 00 00 00 00 00 05 61-65 0C 39 01 E8 20 00 00 .......ae.9.h ..
0140 43 4F 4D 4D 41 4E 44 20-43 4F 4D 20 00 00 00 00 COMMAND COM ....
0150 00 00 00 00 00 00 41 49-52 0A 27 00 55 3F 00 00 ......AIR.'.U?..
0160 E5 32 33 20 20 20 20 20-45 58 45 20 00 00 00 00 e23 EXE ....
0170 00 00 00 00 00 00 9C B2-85 0B 42 01 80 5F 01 00 .......2..B.._..
0180 47 44 20 20 20 20 20 20-44 52 56 20 00 00 00 00 GD DRV ....
0190 00 00 00 00 00 00 E0 0A-E1 06 9A 01 5B 08 00 00 ......'.a...[...
01A0 4B 42 20 20 20 20 20 20-44 52 56 20 00 00 00 00 KB DRV ....
01B0 00 00 00 00 00 00 E0 0A-E1 06 9D 01 60 01 00 00 ......'.a...'...
01C0 50 52 20 20 20 20 20 20-44 52 56 20 00 00 00 00 PR DRV ....
01D0 00 00 00 00 00 00 E0 0A-E1 06 9E 01 49 01 00 00 ......'.a...I...
01E0 00 F6 F6 F6 F6 F6 F6 F6-F6 F6 F6 F6 F6 F6 F6 F6 ................
01F0 F6 F6 F6 F6 F6 F6 F6 F6-F6 F6 F6 F6 F6 F6 F6 F6 ................
Figure 3-12. Hexadecimal dump of a 512-byte directory sector.
The sector shown in Figure 3-12 is actually an example of the first
directory sector in the root directory of a bootable disk. Notice that
IO.SYS and MSDOS.SYS are the first two files in the directory and that
the file attribute byte (offset 0BH in a directory entry) has a binary
value of 00100111, indicating that both files have hidden (bit 1 = 1),
system (bit 0 = 1), and read-only (bit 2 = 1) attributes. The archive
bit (bit 5) is also set, marking the files for possible backup.
The root directory can optionally have a special type of entry called
a volume label, identified by an attribute type of 08H, that is used
to identify disks by name. A root directory can contain only one
volume label. The root directory can also contain entries that point
to subdirectories; such entries are identified by an attribute type of
10H and a file size of zero. Programs that manipulate subdirectories
must do so by tracing through their chains of clusters in the FAT.
Two other special types of directory entries are found only within
subdirectories. These entries have the filenames . and .. and
correspond to the current directory and the parent directory of the
current directory. These special entries, sometimes called directory
aliases, can be used to move quickly through the directory structure.
The maximum pathname length supported by MS-DOS, excluding a drive
specifier but including any filename and extension and subdirectory
name separators, is 64 characters. The size of the directory structure
itself is limited only by the number of root directory entries and the
available disk space.
The file area
The file area contains subdirectories, file data, and unallocated
clusters. The area is divided into fixed-size clusters and the use for
a particular cluster is specified by the corresponding FAT entry.
Other MS-DOS Storage Devices
As mentioned earlier, MS-DOS supports other types of storage devices,
such as magnetic-tape drives and CD ROM drives. Tape drives are most
often used for archiving and for sequential transaction processing and
therefore are not discussed here.
CD ROMs are compact laser discs that hold a massive amount of
information--a single side of a CD ROM can hold almost 500 MB of data.
However, there are some drawbacks to current CD ROM technology. For
instance, data cannot be written to them--the information is placed on
the compact disk at the factory when the disk is made and is available
on a read-only basis. In addition, the access time for a CD ROM is
much slower than for most magnetic-disk systems. Even with these
limitations, however, the ability to hold so much information makes
CD ROM a good method for storing large amounts of static information.
William Wong
───────────────────────────────────────────────────────────────────────────
Part B Programming for MS-DOS
Article 4: Structure of an Application Program
Planning an MS-DOS application program requires serious analysis of
the program's size. This analysis can help the programmer determine
which of the two program styles supported by MS-DOS best suits the
application. The .EXE program structure provides a large program with
benefits resulting from the extra 512 bytes (or more) of header that
preface all .EXE files. On the other hand, at the cost of losing the
extra benefits, the .COM program structure does not burden a small
program with the overhead of these extra header bytes.
Because .COM programs start their lives as .EXE programs (before being
converted by EXE2BIN) and because several aspects of application
programming under MS-DOS remain similar regardless of the program
structure used, a solid understanding of .EXE structures is beneficial
even to the programmer who plans on writing only .COM programs.
Therefore, we'll begin our discussion with the structure and behavior
of .EXE programs and then look at differences between .COM programs
and .EXE programs, including restrictions on the structure and content
of .COM programs.
The .EXE Program
The .EXE program has several advantages over the .COM program for
application design. Considerations that could lead to the choice of
the .EXE format include
■ Extremely large programs
■ Multiple segments
■ Overlays
■ Segment and far address constants
■ Long calls
■ Possibility of upgrading programs to MS OS/2 protected mode
The principal advantages of the .EXE format are provided by the file
header. Most important, the header contains information that permits a
program to make direct segment address references--a requirement if
the program is to grow beyond 64 KB.
The file header also tells MS-DOS how much memory the program
requires. This information keeps memory not required by the program
from being allocated to the program-an important consideration if the
program is to be upgraded in the future to run efficiently under MS
OS/2 protected mode.
Before discussing the .EXE program structure in detail, we'll look at
how .EXE programs behave.
Giving control to the .EXE program
Figure 4-1 gives an example of how a .EXE program might appear in
memory when MS-DOS first gives the program control. The diagram shows
Microsoft's preferred program segment arrangement.
┌─────────────────────┬───────────────────────┐ SP
│ ▒│Any segments with class│
│ ▒│ STACK │
│ ▒├───────────────────────┤ SS
│ All segments ▒│Any segments with class│
│ declared as ▒│ BSS │
│ part of group ▒├───────────────────────┤
│ DGROUP ▒│ Any DGROUP segments │
│ ▒│ not shown elsewhere │
│ ▒├───────────────────────┤
│ ▒│Any segments with class│
│ ▒│ BEGDATA │
├─────────────────────┴───────────────────────┤
│ Any segments with class names │ IP
Start segment │ ending with CODE │
and start of ├─────────────────────────────────────────────┤ CS
program image Program segment prefix (PSP)
(load module) └─ ── ── ── ── ── ── ── ── ── ── ──┘ DS,ES
Figure 4-1. The .EXE program: memory map diagram with register
pointers.
Before transferring control to the .EXE program, MS-DOS initializes
various areas of memory and several of the microprocessor's registers.
The following discussion explains what to expect from MS-DOS before it
gives the .EXE program control.
The program segment prefix
The program segment prefix (PSP) is not a direct result of any program
code. Rather, this special 256-byte (16-paragraph) page of memory is
built by MS-DOS in front of all .EXE and .COM programs when they are
loaded into memory. Although the PSP does contain several fields of
use to newer programs, it exists primarily as a remnant of CP/M--
Microsoft adopted the PSP for ease in porting the vast number of
programs available under CP/M to the MS-DOS environment. Figure 4-2
shows the fields that make up the PSP.
x0H x1H x2H x3H x4H x5H x6H x7H x8H x9H xAH xBH xCH xDH xEH xFH
┌─────────────┬─────────────┬──────┬───────────────────────────────────┬───────────────────────────┬─────────────┐
│ │ │ │ │ │ │
│ INT 20H │ End alloc │ Resv.│ Far call to MS-DOS fn handler... │Prev terminate address │Prev Ctrl C...
0xH │ │ │ │ │ │ │
│ 0CDH │ 20H │seg lo│seg hi│ │ 9AH │ofs lo│ofs hi│ seg lo│seg hi│ofs lo│ofs hi│seg lo│seg hi│ofs lo│ofs hi│
└──────┴──────┼──────┴──────┴──────┴──────┼──────┴──────┴───────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
│ │
│ ...address │Prev critical error address│ ...Reserved...
1xH │ │ │
│seg lo│seg hi│ofs lo│ofs hi│seg lo│seg hi│ │ │ │ │ │ │ │ │ │ │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───────┴──────┴──────┴──────┼──────┴──────┼──────┴──────┘
│ │ │
│ ...Reserved... │ Environ seg │ Reserved...
2xH │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │seg lo│seg hi│ │ │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
░░░░░░░░░░░░░░░──────────────────────┐
│ ...Reserved...
3xH │ (MS-DOS 2.0
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ and later only)
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┤
│
│ │
│ ...Reserved... │
4xH │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
├──────┴──────┴──────┼──────┴──────┴──────┴──────┴──────┴───────┴──────┴──────┴──────┼──────┴──────┴──────┴──────┘
│ │ │
│ INT 21H and RETF │ Reserved │ Primary FCB...
5xH │ │ │
│ OCDH │ 21H │ OCBH │ │ │ │ │ │ │ │ │ │ d │ F │ i │ l │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───────┴──────┴──────┴──────┼──────┴──────┴──────┴──────┘
│
│ ...Primary file control block (FCB)... │ Secondary FCB...
6xH │ │
│ e │ n │ a │ m │ e │ E │ x │ t │ 00H │ 00H │ 00H │ 00H │ d │ F │ i │ l │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───────┴──────┴──────┴──────┼──────┴──────┴──────┴──────┤
│ │
│ ...Secondary file control block (FCB)... │ Reserved │
7xH │ │ │
│ e │ n │ a │ m │ e │ E │ x │ t │ 00H │ 00H │ 00H │ 00H │ │ │ │ │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
│
│ Command trail and default disk transfer area (DTA)
8xH │ (continues through OFFH)...
│ Len │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
Figure 4-2. The program segment prefix (PSP).
PSP:0000H (Terminate [old Warm Boot] Vector) The PSP begins with an
8086-family INT 20H instruction, which the program can use to transfer
control back to MS-DOS. The PSP includes this instruction at offset
00H because this address was the WBOOT (Warm Boot/Terminate) vector
under CP/M and CP/M programs usually terminated by jumping to this
vector. This method of termination should not be used in newer
programs. See Terminating the .EXE Program, below.
PSP:0002H (Address of Last Segment Allocated to Program) MS-DOS
introduced the word at offset 02H into the PSP. It contains the
segment address of the paragraph following the block of memory
allocated to the program. This address should be used only to
determine the size or the end of the memory block allocated to the
program; it must not be considered a pointer to free memory that the
program can appropriate. In most cases this address will not point to
free memory, because any free memory will already have been
allocated to the program unless the program was linked using the
/CPARMAXALLOC switch. Even when /CPARMAXALLOC is used, MS-DOS may fit
the program into a block of memory only as big as the program
requires. Well-behaved programs should acquire additional memory only
through the MS-DOS function calls provided for that purpose.
PSP:0005H (MS-DOS Function Call [old BDOS] Vector) Offset 05H is also
a hand-me-down from CP/M. This location contains an 8086-family far
(intersegment) call instruction to MS-DOS's function request handler.
(Under CP/M, this address was the Basic Disk Operating System [BDOS]
vector, which served a similar purpose.) This vector should not be
used to call MS-DOS in newer programs. The System Calls section of
this book explains the newer, approved method for calling MS-DOS.
MS-DOS provides this vector only to support CP/M-style programs and
therefore honors only the CP/M-style functions (00-24H) through it.
PSP:000AH-0015H (Parent's 22H, 23H, and 24H Interrupt Vector Save)
MS-DOS uses offsets 0AH through 15H to save the contents of three
program-specific interrupt vectors. MS-DOS must save these vectors
because it permits any program to execute another program (called a
child process) through an MS-DOS function call that returns control to
the original program when the called program terminates. Because the
original program resumes executing when the child program terminates,
MS-DOS must restore these three interrupt vectors for the original
program in case the called program changed them. The three vectors
involved include the program termination handler vector (Interrupt
22H), the Control-C/Control-Break handler vector (Interrupt 23H), and
the critical error handler vector (Interrupt 24H). MS-DOS saves the
original preexecution contents of these vectors in the child program's
PSP as doubleword fields beginning at offsets 0AH for the program
termination handler vector, 0EH for the Control-C/Control-Break
handler vector, and 12H for the critical error handler vector.
PSP:002CH (Segment Address of Environment) Under MS-DOS versions 2.0
and later, the word at offset 2CH contains one of the most useful
pieces of information a program can find in the PSP--the segment
address of the first paragraph of the MS-DOS environment. This pointer
enables the program to search through the environment for any
configuration or directory search path strings placed there by users
with the SET command.
PSP:0050H (New MS-DOS Call Vector) Many programmers disregard the
contents of offset 50H. The location consists simply of an INT 21H
instruction followed by a RETF. A .EXE program can call this location
using a far call as a means of accessing the MS-DOS function handler.
Of course, the program can also simply do an INT 21H directly, which
is smaller and faster than calling 50H. Unlike calls to offset 05H,
calls to offset 50H can request the full range of MS-DOS functions.
PSP:005CH (Default File Control Block 1) and PSP:006CH (Default File
Control Block 2) MS-DOS parses the first two parameters the user
enters in the command line following the program's name. If the first
parameter qualifies as a valid (limited) MS-DOS filename (the name can
be preceded by a drive letter but not a directory path), MS-DOS
initializes offsets 5CH through 6BH with the first 16 bytes of an
unopened file control block (FCB) for the specified file. If the
second parameter also qualifies as a valid MS-DOS filename, MS-DOS
initializes offsets 6CH through 7BH with the first 16 bytes of an
unopened FCB for the second specified file. If the user specifies a
directory path as part of either filename, MS-DOS initializes only the
drive code in the associated FCB. Many programmers no longer use this
feature, because file access using FCBs does not support directory
paths and other newer MS-DOS features.
Because FCBs expand to 37 bytes when the file is opened, opening the
first FCB at offset 5CH causes it to grow from 16 bytes to 37 bytes
and to overwrite the second FCB. Similarly, opening the second FCB at
offset 6CH causes it to expand and to overwrite the first part of the
command tail and default disk transfer area (DTA). (The command tail
and default DTA are described below.) To use the contents of both
default FCBs, the program should copy the FCBs to a pair of 37-byte
fields located in the program's data area. The program can use the
first FCB without moving it only after relocating the second FCB (if
necessary) and only by performing sequential reads or writes when
using the first FCB. To perform random reads and writes using the
first FCB, the programmer must either move the first FCB or change the
default DTA address. Otherwise, the first FCB's random record field
will overlap the start of the default DTA. See PROGRAMMING IN THE MS-
DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: File and Record Management.
PSP:0080H (Command Tail and Default DTA) The default DTA resides in
the entire second half (128 bytes) of the PSP. MS-DOS uses this area
of memory as the default record buffer if the program uses the FCB-
style file access functions. Again, MS-DOS inherited this location
from CP/M. (MS-DOS provides a function the program can call to change
the address MS-DOS will use as the current DTA. See SYSTEM CALLS:
INTERRUPT 21H: Function 1AH.) Because the default DTA serves no
purpose until the program performs some file activity that requires
it, MS-DOS places the command tail in this area for the program to
examine. The command tail consists of any text the user types
following the program name when executing the program. Normally, an
ASCII space (20H) is the first character in the command tail, but any
character MS-DOS recognizes as a separator can occupy this position.
MS-DOS stores the command-tail text starting at offset 81H and always
places an ASCII carriage return (0DH) at the end of the text. As an
additional aid, it places the length of the command tail at offset
80H. This length includes all characters except the final 0DH. For
example, the command line
C>DOIT WITH CLASS <ENTER>
will result in the program DOIT being executed with PSP:0080H
containing
0B 20 57 49 54 48 20 43 4C 41 53 53 0D
len sp W I T H sp C L A S S cr
The stack
Because .EXE-style programs did not exist under CP/M, MS-DOS expects
.EXE programs to operate in strictly MS-DOS fashion. For example,
MS-DOS expects the .EXE program to supply its own stack. (Figure 4-1
shows the program's stack as the top box in the diagram.)
Microsoft's high-level-language compilers create a stack themselves,
but when writing in assembly language the programmer must specifically
declare one or more segments with the STACK combine type. If the
programmer declares multiple stack segments, possibly in different
source modules, the linker combines them into one large segment. See
Controlling the .EXE Program's Structure, below.
Many programmers declare their stack segments as preinitialized with
some recognizable repeating string such as *STACK. This makes it
possible to examine the program's stack in memory (using a debugger
such as DEBUG) to determine how much stack space the program actually
used. On the other hand, if the stack is left as uninitialized memory
and linked at the end of the .EXE program, it will not require space
within the .EXE file. (The reason for this will become more apparent
when we examine the structure of a .EXE file.)
Note: When multiple stack segments have been declared in different
.ASM files, the Microsoft Object Linker (LINK) correctly allocates the
total amount of stack space specified in all the source modules, but
the initialization data from all modules is overlapped module by
module at the high end of the combined segment.
An important difference between .COM and .EXE programs is that MS-DOS
preinitializes a .COM program's stack with a termination address
before transferring control to the program. MS-DOS does not do this
for .EXE programs, so a .EXE program cannot simply execute an 8086-
family RET instruction as a means of terminating.
Note: In the assembly-language files generated for a Microsoft C
program or for programs in most other high-level-languages, the
compiler's placement of a RET instruction at the end of the main
function/subroutine/procedure might seem confusing. After all, MS-DOS
does not place any return address on the stack. The compiler places
the RET at the end of main because main does not receive control
directly from MS-DOS. A library initialization routine receives
control from MS-DOS; this routine then calls main. When main performs
the RET, it returns control to a library termination routine, which
then terminates back to MS-DOS in an approved manner.
Preallocated memory
While loading a .EXE program, MS-DOS performs several steps to
determine the initial amount of memory to be allocated to the program.
First, MS-DOS reads the two values the linker places near the start of
the .EXE header: The first value, MINALLOC, indicates the minimum
amount of extra memory the program requires to start executing; the
second value, MAXALLOC, indicates the maximum amount of extra memory
the program would like allocated before it starts executing. Next,
MS-DOS locates the largest free block of memory available. If the size
of the program's image within the .EXE file combined with the value
specified for MINALLOC exceeds the memory block it found, MS-DOS
returns an error to the process trying to load the program. If that
process is COMMAND.COM, COMMAND.COM then displays a Program too big to
fit in memory error message and terminates the user's execution
request. If the block exceeds the program's MINALLOC requirement,
MS-DOS then compares the memory block against the program's image
combined with the MAXALLOC request. If the free block exceeds the
maximum memory requested by the program, MS-DOS allocates only the
maximum request; otherwise, it allocates the entire block. MS-DOS then
builds a PSP at the start of this block and loads the program's image
from the .EXE file into memory following the PSP.
This process ensures that the extra memory allocated to the program
will immediately follow the program's image. The same will not
necessarily be true for any memory MS-DOS allocates to the program as
a result of MS-DOS function calls the program performs during its
execution. Only function calls requesting MS-DOS to increase the
initial allocation can guarantee additional contiguous memory. (Of
course, the granting of such increase requests depends on the
availability of free memory following the initial allocation.)
Programmers writing .EXE programs sometimes find the lack of keywords
or compiler/ assembler switches that deal with MINALLOC (and possibly
MAXALLOC) confusing. The programmer never explicitly specifies a
MINALLOC value because LINK sets MINALLOC to the total size of all
uninitialized data and/or stack segments linked at the very end of the
program. The MINALLOC field allows the compiler to indicate the size
of the initialized data fields in the load module without actually
including the fields themselves, resulting in a smaller .EXE program
file. For LINK to minimize the size of the .EXE file, the program must
be coded and linked in such a way as to place all uninitialized data
fields at the end of the program. Microsoft high-level-language
compilers handle this automatically; assembly-language programmers
must give LINK a little help.
Note: Beginning and even advanced assembly-language programmers can
easily fall into an argument with the assembler over field addressing
when attempting to place data fields after the code in the source
file. This argument can be avoided if programmers use the SEGMENT and
GROUP assembler directives. See Controlling the .EXE Program's
Structure, below.
No reliable method exists for the linker to determine the correct
MAXALLOC value required by the .EXE program. Therefore, LINK uses a
"safe" value of FFFFH, which causes MS-DOS to allocate all of the
largest block of free memory--which is usually all free memory--to the
program. Unless a program specifically releases the memory for which
it has no use, it denies multitasking supervisor programs, such as
IBM's TopView, any memory in which to execute additional programs--
hence the rule that a well-behaved program releases unneeded memory
during its initialization. Unfortunately, this memory conservation
approach provides no help if a multitasking supervisor supports the
ability to load several programs into memory without executing them.
Therefore, programs that have correctly established MAXALLOC values
actually are well-behaved programs.
To this end, newer versions of Microsoft LINK include the
/CPARMAXALLOC switch to permit specification of the maximum amount of
memory required by the program. The /CPARMAXALLOC switch can also be
used to set MAXALLOC to a value that is known to be less than
MINALLOC. For example, specifying a MAXALLOC value of 1 (/CP:1) forces
MS-DOS to allocate only MINALLOC extra paragraphs to the program. In
addition, Microsoft supplies a program called EXEMOD with most of its
languages. This program permits modification of the MAXALLOC field in
the headers of existing .EXE programs. See Modifying the .EXE File
Header, below.
The registers
Figure 4-1 gives a general indication of how MS-DOS sets the 8086-
family registers before transferring control to a .EXE program. MS-DOS
determines most of the original register values from information the
linker places in the .EXE file header at the start of the .EXE file.
MS-DOS sets the SS register to the segment (paragraph) address of the
start of any segments declared with the STACK combine type and sets
the SP register to the offset from SS of the byte immediately after
the combined stack segments. (If no stack segment is declared, MS-DOS
sets SS:SP to CS:0000.) Because in the 8086-family architecture a
stack grows from high to low memory addresses, this effectively sets
SS:SP to point to the base of the stack. Therefore, if the programmer
declares stack segments when writing an assembly-language program, the
program will not need to initialize the SS and SP registers.
Microsoft's high-level-language compilers handle the creation of stack
segments automatically. In both cases, the linker determines the
initial SS and SP values and places them in the header at the start of
the .EXE program file.
Unlike its handling of the SS and SP registers, MS-DOS does not
initialize the DS and ES registers to any data areas of the .EXE
program. Instead, it points DS and ES to the start of the PSP. It does
this for two primary reasons: First, MS-DOS uses the DS and ES
registers to tell the program the address of the PSP; second, most
programs start by examining the command tail within the PSP. Because
the program starts without DS pointing to the data segments, the
program must initialize DS and (optionally) ES to point to the data
segments before it starts trying to access any fields in those
segments. Unlike .COM programs, .EXE programs can do this easily
because they can make direct references to segments, as follows:
MOV AX,SEG DATA_SEGMENT_OR_GROUP_NAME
MOV DS,AX
MOV ES,AX
High-level-language programs need not initialize and maintain DS and
ES; the compiler and library support routines do this.
In addition to pointing DS and ES to the PSP, MS-DOS also sets AH and
AL to reflect the validity of the drive identifiers it placed in the
two FCBs contained in the PSP. MS-DOS sets AL to 0FFH if the first FCB
at PSP:005CH was initialized with a nonexistent drive identifier;
otherwise, it sets AL to zero. Similarly, MS-DOS sets AH to reflect
the drive identifier placed in the second FCB at PSP:006CH.
When MS-DOS analyzes the first two command-line parameters following
the program name in order to build the first and second FCBs, it
treats any character followed by a colon as a drive prefix. If the
drive prefix consists of a lowercase letter (ASCII a through z), MS-
DOS starts by converting the character to uppercase (ASCII A through
Z). Then it subtracts 40H from the character, regardless of its
original value. This converts the drive prefix letters A through Z to
the drive codes 01H through 1AH, as required by the two FCBs. Finally,
MS-DOS places the drive code in the appropriate FCB.
This process does not actually preclude invalid drive specifications
from being placed in the FCBs. For instance, MS-DOS will accept the
drive prefix !: and place a drive code of 0E1H in the FCB (! = 21H;
21H-40H = 0E1H). However, MS-DOS will then check the drive code to see
if it represents an existing drive attached to the computer and will
pass a value of 0FFH to the program in the appropriate register (AL or
AH) if it does not.
As a side effect of this process, MS-DOS accepts @: as a valid drive
prefix because the subtraction of 40H converts the @ character (40H)
to 00H. MS-DOS accepts the 00H value as valid because a 00H drive code
represents the current default drive. MS-DOS will leave the FCB's
drive code set to 00H rather than translating it to the code for the
default drive because the MS-DOS function calls that use FCBs accept
the 00H code.
Finally, MS-DOS initializes the CS and IP registers, transferring
control to the program's entry point. Programs developed using high-
level-language compilers usually receive control at a library
initialization routine. A programmer writing an assembly-language
program using the Microsoft Macro Assembler (MASM) can declare any
label within the program as the entry point by placing the label
after the END statement as the last line of the program:
END ENTRY_POINT_LABEL
With multiple source files, only one of the files should have a label
following the END statement. If more than one source file has such a
label, LINK uses the first one it encounters as the entry point.
The other processor registers (BX, CX, DX, BP, SI, and DI) contain
unknown values when the program receives control from MS-DOS. Once
again, high-level-language programmers can ignore this fact--the
compiler and library support routines deal with the situation. How-
ever, assembly-language programmers should keep this fact in mind.
It may give needed insight sometime in the future when a program
functions at certain times and not at others.
In many cases, debuggers such as DEBUG and SYMDEB initialize
uninitialized registers to some predictable but undocumented state.
For instance, some debuggers may predictably set BP to zero before
starting program execution. However, a program must not rely on such
debugger actions, because MS-DOS makes no such promises. Situations
like this could account for a program that fails when executed
directly under MS-DOS but works fine when executed using a debugger.
Terminating the .EXE program
After MS-DOS has given the .EXE program control and it has completed
whatever task it set out to perform, the program needs to give control
back to MS-DOS. Because of MS-DOS's evolution, five methods of program
termination have accumulated--not including the several ways MS-DOS
allows programs to terminate but remain resident in memory.
Before using any of the termination methods supported by MS-DOS, the
program should always close any files it had open, especially those
to which data has been written or whose lengths were changed. Under
versions 2.0 and later, MS-DOS closes any files opened using handles.
However, good programming practice dictates that the program not rely
on the operating system to close the program's files. In addition,
programs written to use shared files under MS-DOS versions 3.0 and
later should release any file locks before closing the files and
terminating.
The Terminate Process with Return Code function
Of the five ways a program can terminate, only the Interrupt 21H
Terminate Process with Return Code function (4CH) is recommended for
programs running under MS-DOS version 2.0 or later. This method is one
of the easiest approaches to terminating any program, regardless of
its structure or segment register settings. The Terminate Process with
Return Code function call simply consists of the following:
MOV AH,4CH ;load the MS-DOS function code
MOV AL,RETURN_CODE ;load the termination code
INT 21H ;call MS-DOS to terminate program
The example loads the AH register with the Terminate Process with
Return Code function code. Then it loads the AL register with a return
code. Normally, the return code represents the reason the program
terminated or the result of any operation the program performed.
A program that executes another program as a child process can recover
and analyze the child program's return code if the child process used
this termination method. Likewise, the child process can recover the
RETURN_CODE returned by any program it executes as a child process.
When a program is terminated using this method and control returns to
MS-DOS, a batch (.BAT) file can be used to test the terminated
program's return code using the IF ERRORLEVEL statement.
Only two general conventions have been adopted for the value of
RETURN_CODE: First, a RETURN_CODE value of 00H indicates a normal
no-error termination of the program; second, increasing RETURN_CODE
values indicate increasing severity of conditions under which the
program terminated. For instance, a compiler could use the RETURN_CODE
00H if it found no errors in the source file, 01H if it found only
warning errors, or 02H if it found severe errors.
If a program has no need to return any special RETURN_CODE values,
then the following instructions will suffice to terminate the program
with a RETURN_CODE of 00H:
MOV AX,4C00H
INT 21H
Apart from being the approved termination method, Terminate Process
with Return Code is easier to use with .EXE programs than any other
termination method because all other methods require that the CS
register point to the start of the PSP when the program terminates.
This restriction causes problems for .EXE programs because they have
code segments with segment addresses different from that of the PSP.
The only problem with Terminate Process with Return Code is that it is
not available under MS-DOS versions earlier than 2.0, so it cannot be
used if a program must be compatible with early MS-DOS versions.
However, Figure 4-3 shows how a program can use the approved
termination method when available but still remain pre-2.0 compatible.
See The Warm Boot/Terminate Vector, below.
──────────────────────────────────────────────────────────────────────
Figure 4-3. Terminating properly under any MS-DOS version.
──────────────────────────────────────────────────────────────────────
The Terminate Program interrupt
Before MS-DOS version 2.0, terminating with an approved method meant
executing an INT 20H instruction, the Terminate Program interrupt. The
INT 20H instruction was replaced as the approved termination method
for two primary reasons: First, it did not provide a means whereby
programs could return a termination code; second, CS had to point to
the PSP before the INT 20H instruction was executed.
The restriction placed on the value of CS at termination did not pose
a problem for .COM programs because they execute with CS pointing to
the beginning of the PSP. A .EXE program, on the other hand, executes
with CS pointing to various code segments of the program, and the
value of CS cannot be changed arbitrarily when the program is ready to
terminate. Because of this, few .EXE programs attempt simply to
execute a Terminate Program interrupt from directly within their own
code segments. Instead, they usually use the termination method
discussed next.
The Warm Boot/Terminate vector
The earlier discussion of the structure of the PSP briefly covered one
older method a .EXE program can use to terminate: Offset 00H within
the PSP contains an INT 20H instruction to which the program can jump
in order to terminate. MS-DOS adopted this technique to support the
many CP/M programs ported to MS-DOS. Under CP/M, this PSP location was
referred to as the Warm Boot vector because the CP/M operating system
was always reloaded from disk (rebooted) whenever a program
terminated.
Because offset 00H in the PSP contains an INT 20H instruction, jumping
to that location terminates a program in the same manner as an INT 20H
included directly within the program, but with one important
difference: By jumping to PSP:0000H, the program sets the CS register
to point to the beginning of the PSP, thereby satisfying the only
restriction imposed on executing the Terminate Program interrupt. The
discussion of MS-DOS Function 4CH gave an example of how a .EXE
program can terminate via PSP:0000H. The example first asks MS-DOS for
its version number and then terminates via PSP:0000H only under
versions of MS-DOS earlier than 2.0. Programs can also use PSP:0000H
under MS-DOS versions 2.0 and later; the example uses Function 4CH
simply because it is preferred under the later MS-DOS versions.
The RET instruction
The other popular method used by CP/M programs to terminate involved
simply executing a RET instruction. This worked because CP/M pushed
the address of the Warm Boot vector onto the stack before giving the
program control. MS-DOS provides this support only for .COM-style
programs; it does not push a termination address onto the stack before
giving .EXE programs control.
The programmer who wants to use the RET instruction to return to
MS-DOS can use the variation of the Figure 4-3 listing shown in
Figure 4-4.
──────────────────────────────────────────────────────────────────────
Figure 4-4. Using RET to return control to MS-DOS.
──────────────────────────────────────────────────────────────────────
The Terminate Process function
The final method for terminating a .EXE program is Interrupt 21H
Function 00H (Terminate Process). This method maintains the same
restriction as all other older termination methods: CS must point to
the PSP. Because of this restriction, .EXE programs typically avoid
this method in favor of terminating via PSP:0000H, as discussed above
for programs executing under versions of MS-DOS earlier than 2.0.
Terminating and staying resident
A .EXE program can use any of several additional termination methods
to return control to MS-DOS but still remain resident within memory to
service a special event. See PROGRAMMING IN THE MS-DOS ENVIRONMENT:
CUSTOMIZING MS-DOS: Terminate-and-Stay-Resident Utilities.
Structure of the .EXE files
So far we've examined how the .EXE program looks in memory, how MS-DOS
gives the program control of the computer, and how the program should
return control to MS-DOS. Next we'll investigate what the program
looks like as a disk file, before MS-DOS loads it into memory. Figure
4-5 shows the general structure of a .EXE file.
x0H x1H x2H x3H x4H x5H x6H x7H x8H x9H xAH xBH xCH xDH xEH xFH
┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐
│ │ │ │ │ │ │ │ │
│ Signature │ Last page │ File Pages │ Reloc Items │Header Paras │ MINALLOC │ MAXALLOC │PreReloc SS │
0xH │ │ Size │ │ │ │ │ │ │
│ 4DH │ 5AH │lo byt│hi byt│lo byt│hi byt│lo byt│hi byt│lo byt│hi byt│lo byt│hi byt│lo byt│hi byt│lo byt│hi byt│
├──────┴──────┼──────┴──────┼──────┴──────┼──────┴──────┼──────┴──────┼──────┴──────┼──────┴──────┴──────┴──────┤
│ │ │ │ │ │ │ │
│ Initial SP │ Neg Chksum │ Initial IP │Pre Reloc CS │Reloc Tbl Ofs│ Overlay Num │ Reserved │
1xH │ │ │ │ │ │ │ │
│ofs lo│ofs hi│lo byt│hi byt│ofs lo│ofs hi│seg lo│seg hi│lo byt│hi byt│lo byt│hi byt│ │
├──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ │
└────────────────────┐ ┌────────────────┘
┌───────────────────┐└────────────────────┐ ┌──────────────────┘┌───────────────┐
│ └────────────────────┐└─────────────────────────────────┘┌──────────────────┘ │
Use Reloc │ └───────────────────────────────────┘ │
Tbl Ofs at │ │
18H (offset ├───────────────────────────┬───────────────────────────┬───────────────────────────┬───────────────────────────┤
is from │ Seg relocation Ptr #1 │ Seg relocation Ptr #2 │ Seg Relocation Ptr #3 │ Seg Relocation Ptr #4 │
start of │ │ │ │ │
file) │ │ │ │ │
│ofs lo│ofs hi│seg lo│seg hi│ofs lo│ofs hi│seg lo│seg hi│ofs lo│ofs hi│seg lo│seg hi│ofs lo│ofs hi│seg lo│seg hi│
├──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┤
└────────────────────┐ ┌─────────────┘
┌───────────────────┐└────────────────────┐ ┌────────────────────┘┌────────────┐
│ └────────────────────┐└──────────────────────────────────┘┌────────────────────┘ │
│ └────────────────────────────────────┘ │
│ │
├───────────────────────────┬───────────────────────────┬───────────────────────────┬───────────────────────────┤
│ Seg relocation Ptr #n-3 │ Seg relocation Ptr #n-2 │ Seg relocation Ptr #n-1 │ Seg relocation Ptr #n │
│ │ │ │ │─┐
│ │ │ │ │ Use
│ofs lo│ofs hi│seg lo│seg hi│ofs lo│ofs hi│seg lo│seg hi│ofs lo│ofs hi│seg lo│seg hi│ofs lo│ofs hi│seg lo│seg hi│ Reloc
├──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┤ Items
└────────────────────┐ ┌─────────────┘ at
Use Header ┌───────────────────┐└────────────────────┐ ┌────────────────────┘┌────────────┐ 06H
Paras at 08H │ └────────────────────┐└──────────────────────────────────┘┌────────────────────┘ │
(load module │ └────────────────────────────────────┘ │
always starts │ │
on paragraph ├───────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
boundary) │ │
│ Program image │
│ - - - - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - │
│ (load module) Use last page size at 02H │
│ Final 512-byte page as │
End of file ├─────────────────────────────────────────────────────────────────────────────────────indicated by File Pages───┘
| at 04H |
| |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Figure 4-5. Structure of a .EXE file.
The file header
Unlike .COM program files, .EXE program files contain information that
permits the .EXE program and MS-DOS to use the full capabilities of
the 8086 family of microprocessors. The linker places all this extra
information in a header at the start of the .EXE file. Although the
.EXE file structure could easily accommodate a header as small as 32
bytes, the linker never creates a header smaller than 512 bytes. (This
minimum header size corresponds to the standard record size preferred
by MS-DOS.) The .EXE file header contains the following information,
which MS-DOS reads into a temporary work area in memory for use while
loading the .EXE program:
00-01H (.EXE Signature) MS-DOS does not rely on the extension (.EXE or
.COM) to determine whether a file contains a .COM or a .EXE program.
Instead, MS-DOS recognizes the file as a .EXE program if the first 2
bytes in the header contain the signature 4DH 5AH (ASCII characters M
and Z). If either or both of the signature bytes contain other values,
MS-DOS assumes the file contains a .COM program, regardless of the
extension. The reverse is not necessarily true--that is, MS-DOS does
not accept the file as a .EXE program simply because the file begins
with a .EXE signature. The file must also pass several other tests.
02-03H (Last Page Size) The word at this location indicates the actual
number of bytes in the final 512-byte page of the file. This word
combines with the following word to determine the actual size of the
file.
04-05H (File Pages) This word contains a count of the total number of
512-byte pages required to hold the file. If the file contains 1024
bytes, this word contains the value 0002H; if the file contains 1025
bytes, this word contains the value 0003H. The previous word (Last
Page Size, 02-03H) is used to determine the number of valid bytes in
the final 512-byte page. Thus, if the file contains 1024 bytes, the
Last Page Size word contains 0000H because no bytes overflow into a
final partly used page; if the file contains 1025 bytes, the Last Page
Size word contains 0001H because the final page contains only a single
valid byte (the 1025th byte).
06-07H (Relocation Items) This word gives the number of entries that
exist in the relocation pointer table. See Relocation Pointer Table,
below.
08-09H (Header Paragraphs) This word gives the size of the .EXE file
header in 16-byte paragraphs. It indicates the offset of the program's
compiled/assembled and linked image (the load module) within the .EXE
file. Subtracting this word from the two file-size words starting at
02H and 04H reveals the size of the program's image. The header always
spans an even multiple of 16-byte paragraphs. For example, if the file
consists of a 512-byte header and a 513-byte program image, then the
file's total size is 1025 bytes. As discussed before, the Last Page
Size word (02-03H) will contain 0001H and the File Pages word (04-05H)
will contain 0003H. Because the header is 512 bytes, the Header
Paragraphs word (08-09H) will contain 32 (0020H). (That is, 32
paragraphs times 16 bytes per paragraph totals 512 bytes.) By
subtracting the 512 bytes of the header from the 1025-byte total file
size, the size of the program's image can be determined--in this case,
513 bytes.
0A-0BH (MINALLOC) This word indicates the minimum number of 16-byte
paragraphs the program requires to begin execution in addition to the
memory required to hold the program's image. MINALLOC normally
represents the total size of any uninitialized data and/or stack
segments linked at the end of the program. LINK excludes the space
reserved by these fields from the end of the .EXE file to avoid
wasting disk space. If not enough memory remains to satisfy MINALLOC
when loading the program, MSDOS returns an error to the process trying
to load the program. If the process is COMMAND.COM, COMMAND.COM then
displays a Program too big to fit in memory error message. The EXEMOD
utility can alter this field if desired. See Modifying the .EXE File
Header, below.
0C-0DH (MAXALLOC) This word indicates the maximum number of 16-byte
paragraphs the program would like allocated to it before it begins
execution. MAXALLOC indicates additional memory desired beyond that
required to hold the program's image. MS-DOS uses this value to
allocate MAXALLOC extra paragraphs, if available. If MAXALLOC
paragraphs are not available, the program receives the largest memory
block available--at least MINALLOC additional paragraphs. The
programmer could use the MAXALLOC field to request that MS-DOS
allocate space for use as a print buffer or as a program-maintained
heap, for example.
Unless otherwise specified with the /CPARMAXALLOC switch at link time,
the linker sets MAXALLOC to FFFFH. This causes MS-DOS to allocate all
of the largest block of memory it has available to the program. To
make the program compatible with multitasking supervisor programs, the
programmer should use /CPARMAXALLOC to set the true maximum number of
extra paragraphs the program desires. The EXEMOD utility can also be
used to alter this field.
Note: If both MINALLOC and MAXALLOC have been set to 0000H, MS-DOS
loads the program as high in memory as possible. LINK sets these
fields to 0000H if the /HIGH switch was used; the EXEMOD utility can
also be used to modify these fields.
0E-0FH (Initial SS Value) This word contains the paragraph address of
the stack segment relative to the start of the load module. At load
time, MS-DOS relocates this value by adding the program's start
segment address to it, and the resulting value is placed in the SS
register before giving the program control. (The start segment
corresponds to the first segment boundary in memory following the
PSP.)
10-11H (Initial SP Value) This word contains the absolute value that
MS-DOS loads into the SP register before giving the program control.
Because MS-DOS always loads programs starting on a segment address
boundary, and because the linker knows the size of the stack segment,
the linker is able to determine the correct SP offset at link time;
therefore, MS-DOS does not need to adjust this value at load time. The
EXEMOD utility can be used to alter this field.
12-13H (Complemented Checksum) This word contains the one's complement
of the summation of all words in the .EXE file. Current versions of
MS-DOS basically ignore this word when they load a .EXE program;
however, future versions might not. When LINK generates a .EXE file,
it adds together all the contents of the .EXE file (including the .EXE
header) by treating the entire file as a long sequence of 16-bit
words. During this addition, LINK gives the Complemented Checksum word
(12-13H) a temporary value of 0000H. If the file consists of an odd
number of bytes, then the final byte is treated as a word with a high
byte of 00H. Once LINK has totaled all words in the .EXE file, it
performs a one's complement operation on the total and records the
answer in the .EXE file header at offsets 12-13H. The validity of a
.EXE file can then be checked by performing the same word-totaling
process as LINK performed. The total should be FFFFH, because the
total will include LINK's calculated complemented checksum, which is
designed to give the file the FFFFH total.
An example 7-byte .EXE file illustrates how .EXE file checksums are
calculated. (This is a totally fictitious file, because .EXE headers
are never smaller than 512 bytes.) If this fictitious file contained
the bytes 8CH C8H 8EH D8H BAH 10H B4H, then the file's total would be
calculated using C88CH+D88EH+10BAH+00B4H=1B288H. (Overflow past 16
bits is ignored, so the value is interpreted as B288H.) If this were a
valid .EXE file, then the B288H total would have been FFFFH instead.
14-15H (Initial IP Value) This word contains the absolute value that
MS-DOS loads into the IP register in order to transfer control to the
program. Because MS-DOS always loads programs starting on a segment
address boundary, the linker can calculate the correct IP offset from
the initial CS register value at link time; therefore, MS-DOS does not
need to adjust this value at load time.
16-17H (Pre-Relocated Initial CS Value) This word contains the initial
value, relative to the start of the load module, that MS-DOS places in
the CS register to give the .EXE program control. MS-DOS adjusts this
value in the same manner as the initial SS value before loading it
into the CS register.
18-19H (Relocation Table Offset) This word gives the offset from the
start of the file to the relocation pointer table. This word must be
used to locate the relocation pointer table, because variable-length
information pertaining to program overlays can occur before the table,
thus causing the position of the table to vary.
1A-1BH (Overlay Number) This word is normally set to 0000H, indicating
that the .EXE file consists of the resident, or primary, part of the
program. This number changes only in files containing programs that
use overlays, which are sections of a program that remain on disk
until the program actually requires them. These program sections are
loaded into memory by special overlay managing routines included in
the run-time libraries supplied with some Microsoft high-level-
language compilers.
The preceding section of the header (00-1BH) is known as the formatted
area. Optional information used by high-level-language overlay
managers can follow this formatted area. Unless the program in the
.EXE file incorporates such information, the relocation pointer table
immediately follows the formatted header area.
Relocation Pointer Table The relocation pointer table consists of a
list of pointers to words within the .EXE program image that MS-DOS
must adjust before giving the program control. These words consist of
references made by the program to the segments that make up the
program. MS-DOS must adjust these segment address references when it
loads the program, because it can load the program into memory
starting at any segment address boundary.
Each pointer in the table consists of a doubleword. The first word
contains an offset from the segment address given in the second word,
which in turn indicates a segment address relative to the start of the
load module. Together, these two words point to a third word within
the load module that must have the start segment address added to it.
(The start segment corresponds to the segment address at which MS-DOS
started loading the program's image, immediately following the PSP.)
Figure 4-6 shows the entire procedure MS-DOS performs for each
relocation table entry.
.EXE File
┌────────────────────┐ End of file
│ │
│ │
│ │
│┌──────────────────┐│
││Rel Seg Ref=003CH ││
││Abs Seg Ref=25D1H ││
│└──────────────────┘│
│ Load module │
├────────────────────┤
└────────┐ │ Memory
┌───────┐└────────┐ │ ┌──────────────────────┐
│ └────────┐└──┘ │ │
│ └───┐ 003CH──┐│ │
│┌──────────────────┐│ ┌─+2595H ││ ┌─────────────────┐ │
││Relocation pointer││ │ ───── └┼─┤Rel Seg Ref=003CH│ │
││ 0002H:0005H ───┼┼─┐ │ 25D1H────┼─Abs Seg Ref=25D1H│ │
│└──────────────────┘│ │ │┌───────────┼│ │ │
│ Relocation pointer │ │ ││ │ └─────────────────┘ │
│ table │ └─ 0002H:0005H││ │ │
├────────────────────┤ +2595H ────┴┼"Start seg"│ Load module │
│ Formatted header │ ─────────── │ 2595H ├──────────────────────┤
│ area │ 2597H:0005H─┘ │Program segment prefix│
└────────────────────┘Start of file └──────────────────────┘
Figure 4-6. The .EXE file relocation procedure.
The load module
The load module starts where the .EXE header ends and consists of the
fully linked image of the program. The load module appears within the
.EXE file exactly as it would appear in memory if MS-DOS were to load
it at segment address 0000H. The only changes MS-DOS makes to the load
module involve relocating any direct segment references.
Although the .EXE file contains distinct segment images within the
load module, it provides no information for separating those
individual segments from one another. Existing versions of MS-DOS
ignore how the program is segmented; they simply copy the load module
into memory, relocate any direct segment references, and give the
program control.
Loading the .EXE program
So far we've covered all the characteristics of the .EXE program as
it resides in memory and on disk. We've also touched on all the steps
MS-DOS performs while loading the .EXE program from disk and executing
it. The following list recaps the .EXE program loading process in the
order in which MS-DOS performs it:
1. MS-DOS reads the formatted area of the header (the first
1BH bytes) from the .EXE file into a work area.
2. MS-DOS determines the size of the largest available block of
memory.
3. MS-DOS determines the size of the load module using the Last Page
Size (offset 02H), File Pages (offset 04H), and Header Paragraphs
(offset 08H) fields from the header. An example of this process
is in the discussion of the Header Paragraphs field.
4. MS-DOS adds the MINALLOC field (offset 0AH) in the header to the
calculated load-module size and the size of the PSP (100H bytes).
If this total exceeds the size of the largest available block,
MS-DOS terminates the load process and returns an error to the
calling process. If the calling process was COMMAND.COM,
COMMAND.COM then displays a Program too big to fit in memory
error message.
5. MS-DOS adds the MAXALLOC field (offset 0CH) in the header to the
calculated load-module size and the size of the PSP. If the memory
block found earlier exceeds this calculated total, MS-DOS allo-
cates the calculated memory size to the program from the memory
block; if the calculated total exceeds the block's size, MS-DOS
allocates the entire block.
6. If the MINALLOC and MAXALLOC fields both contain 0000H, MS-DOS
uses the calculated load-module size to determine a start segment.
MS-DOS calculates the start segment so that the load module will
load into the high end of the allocated block. If either MINALLOC
or MAXALLOC contains nonzero values (the normal case), MS-DOS
establishes the start segment as the segment following the PSP.
7. MS-DOS loads the load module into memory starting at the start
segment.
8. MS-DOS reads the relocation pointers into a work area and re-
locates the load module's direct segment references, as shown in
Figure 4-6.
9. MS-DOS builds a PSP in the first 100H bytes of the allocated
memory block. While building the two FCBs within the PSP, MS-DOS
determines the initial values for the AL and AH registers.
10. MS-DOS sets the SS and SP registers to the values in the header
after the start segment is added to the SS value.
11. MS-DOS sets the DS and ES registers to point to the beginning of
the PSP.
12. MS-DOS transfers control to the .EXE program by setting CS and IP
to the values in the header after adding the start segment to the
CS value.
Controlling the .EXE program's structure
We've now covered almost every aspect of a completed .EXE program.
Next, we'll discuss how to control the structure of the final .EXE
program from the source level. We'll start by covering the statements
provided by MASM that permit the programmer to define the structure of
the program when programming in assembly language. Then we'll cover
the five standard memory models provided by Microsoft's C and FORTRAN
compilers (both version 4.0), which provide predefined structuring
over which the programmer has limited control.
The MASM SEGMENT directive
MASM's SEGMENT directive and its associated ENDS directive mark the
beginning and end of a program segment. Program segments contain
collections of code or data that have offset addresses relative to the
same common segment address.
In addition to the required segment name, the SEGMENT directive has
three optional parameters:
segname SEGMENT [align] [combine] ['class']
With MASM, the contents of a segment can be defined at one point in
the source file and the definition can be resumed as many times as
necessary throughout the remainder of the file. When MASM encounters a
SEGMENT directive with a segname it has previously encountered, it
simply resumes the segment definition where it left off. This occurs
regardless of the combine type specified in the SEGMENT directive--the
combine type influences only the actions of the linker. See The
combine Type Parameter, below.
The align type parameter
The optional align parameter lets the programmer send the linker an
instruction on how to align a segment within memory. In reality, the
linker can align the segment only in relation to the start of the
program's load module, but the result remains the same because MS-DOS
always loads the module aligned on a paragraph (16-byte) boundary.
(The PAGE align type creates a special exception, as discussed below.)
The following alignment types are permitted:
BYTE This align type instructs the linker to start the segment on the
byte immediately following the previous segment. BYTE alignment
prevents any wasted memory between the previous segment and the BYTE-
aligned segment.
A minor disadvantage to BYTE alignment is that the 8086-family segment
registers might not be able to directly address the start of the
segment in all cases. Because they can address only on paragraph
boundaries, the segment registers may have to point as many as 15
bytes behind the start of the segment. This means that the segment
size should not be more than 15 bytes short of 64 KB. The linker
adjusts offset and segment address references to compensate for
differences between the physical segment start and the paragraph
addressing boundary.
Another possible concern is execution speed on true 16-bit 8086-family
microprocessors. When using non-8088 microprocessors, a program can
actually run faster if the instructions and word data fields within
segments are aligned on word boundaries. This permits the 16-bit
processors to fetch full words in a single memory read, rather than
having to perform two single-byte reads. The EVEN directive tells MASM
to align instructions and data fields on word boundaries; however,
MASM can establish this alignment only in relation to the start of the
segment, so the entire segment must start aligned on a word or larger
boundary to guarantee alignment of the items within the segment.
WORD This align type instructs the linker to start the segment on the
next word boundary. Word boundaries occur every 2 bytes and consist of
all even addresses (addresses in which the least significant bit
contains a zero). WORD alignment permits alignment of data fields and
instructions within the segment on word boundaries, as discussed for
the BYTE alignment type. However, the linker may have to waste 1 byte
of memory between the previous segment and the word-aligned segment in
order to position the new segment on a word boundary.
Another minor disadvantage to WORD alignment is that the 8086-family
segment registers might not be able to directly address the start of
the segment in all cases. Because they can address only on paragraph
boundaries, the segment registers may have to point as many as 14
bytes behind the start of the segment. This means that the segment
size should not be more than 14 bytes short of 64 KB. The linker
adjusts offset and segment address references to compensate for
differences between the physical segment start and the paragraph
addressing boundary.
PARA This align type instructs the linker to start the segment on the
next paragraph boundary. The segments default to PARA if no alignment
type is specified. Paragraph boundaries occur every 16 bytes and
consist of all addresses with hexadecimal values ending in zero
(0000H, 0010H, 0020H, and so forth). Paragraph alignment ensures that
the segment begins on a segment register addressing boundary, thus
making it possible to address a full 64 KB segment. Also, because
paragraph addresses are even addresses, PARA alignment has the same
advantages as WORD alignment. The only real disadvantage to PARA
alignment is that the linker may have to waste as many as 15 bytes of
memory between the previous segment and the paragraph-aligned segment.
PAGE This align type instructs the linker to start the segment on the
next page boundary. Page boundaries occur every 256 bytes and consist
of all addresses in which the low address byte equals zero (0000H,
0100H, 0200H, and so forth). PAGE alignment ensures only that the
linker positions the segment on a page boundary relative to the start
of the load module. Unfortunately, this does not also ensure alignment
of the segment on an absolute page within memory, because MS-DOS only
guarantees alignment of the entire load module on a paragraph
boundary.
When a programmer declares pieces of a segment with the same name in
different source modules, the align type specified for each segment
piece influences the alignment of that specific piece of the segment.
For example, assume the following two segment declarations appear in
different source modules:
_DATA SEGMENT PARA PUBLIC 'DATA'
DB '123'
_DATA ENDS
_DATA SEGMENT PARA PUBLIC 'DATA'
DB '456'
_DATA ENDS
The linker starts by aligning the first segment piece located in the
first object module on a paragraph boundary, as requested. When the
linker encounters the second segment piece in the second object
module, it aligns that piece on the first paragraph boundary following
the first segment piece. This results in a 13-byte gap between the
first segment piece and the second. The segment pieces must exist in
separate source modules for this to occur. If the segment pieces exist
in the same source module, MASM assumes that the second segment
declaration is simply a resumption of the first and creates an object
module with segment declarations equivalent to the following:
_DATA SEGMENT PARA PUBLIC 'DATA'
DB '123'
DB '456'
_DATA ENDS
The combine type parameter
The optional combine parameter allows the programmer to send
directions to the linker on how to combine segments with the same
segname occurring in different object modules. If no combine type is
specified, the linker treats such segments as if each had a different
segname. The combine type has no effect on the relationship of
segments with different segnames. MASM and LINK both support the
following combine types:
PUBLIC This combine type instructs the linker to concatenate multiple
segments having the same segname into a single contiguous segment. The
linker adjusts any address references to labels within the
concatenated segments to reflect the new position of those labels
relative to the start of the combined segment. This combine type is
useful for accessing code or data in different source modules using a
common segment register value.
STACK This combine type operates similarly to the PUBLIC combine type,
except for two additional effects: The STACK type tells the linker
that this segment comprises part of the program's stack and
initialization data contained within STACK segments is handled
differently than in PUBLIC segments. Declaring segments with the STACK
combine type permits the linker to determine the initial SS and SP
register values it places in the .EXE file header. Normally, a
programmer would declare only one STACK segment in one of the source
modules. If pieces of the stack are declared in different source
modules, the linker will concatenate them in the same fashion as
PUBLIC segments. However, initialization data declared within any
STACK segment is placed at the high end of the combined STACK segments
on a module-by-module basis. Thus, each successive module's
initialization data overlays the previous module's data. At least one
segment must be declared with the STACK combine type; otherwise, the
linker will issue a warning message because it cannot determine the
program's initial SS and SP values. (The warning can be ignored if the
program itself initializes SS and SP.)
COMMON This combine type instructs the linker to overlap multiple
segments having the same segname. The length of the resulting segment
reflects the length of the longest segment declared. If any code or
data is declared in the overlapping segments, the data contained in
the final segments linked replaces any data in previously loaded
segments. This combine type is useful when a data area is to be shared
by code in different source modules.
MEMORY Microsoft's LINK treats this combine type the same as it treats
the PUBLIC type. MASM, however, supports the MEMORY type for
compatibility with other linkers that use Intel's definition of a
MEMORY combine type.
AT address This combine type instructs LINK to pretend that the
segment will reside at the absolute segment address. LINK then adjusts
all address references to the segment in accordance with the
masquerade. LINK will not create an image of the segment in the load
module, and it will ignore any data defined within the segment. This
behavior is consistent with the fact that MS-DOS does not support the
loading of program segments into absolute memory segments. All pro-
grams must be able to execute from any segment address at which MS-DOS
can find available memory. The SEGMENT AT address combine type is
useful for creating templates of various areas in memory outside the
program. For instance, SEGMENT AT 0000H could be used to create a
template of the 8086-family interrupt vectors. Because data contained
within SEGMENT AT address segments is suppressed by LINK and not by
MASM (which places the data in the object module), it is possible to
use .OBJ files generated by MASM with another linker that supports ROM
or other absolute code generation should the programmer require this
specialized capability.
The class type parameter
The class parameter provides the means to organize different segments
into classifications. For instance, here are three source modules,
each with its own separate code and data segments:
;Module "A"
A_DATA SEGMENT PARA PUBLIC 'DATA'
;Module "A" data fields
A_DATA ENDS
A_CODE SEGMENT PARA PUBLIC 'CODE'
;Module "A" code
A_CODE ENDS
END
;Module "B"
B_DATA SEGMENT PARA PUBLIC 'DATA'
;Module "B" data fields
B_DATA ENDS
B_CODE SEGMENT PARA PUBLIC 'CODE'
;Module "B" code
B_CODE ENDS
END
;Module "C"
C_DATA SEGMENT PARA PUBLIC 'DATA'
;Module "C" data fields
C_DATA ENDS
C_CODE SEGMENT PARA PUBLIC 'CODE'
;Module "C" code
C_CODE ENDS
END
If the 'CODE' and 'DATA' class types are removed from the SEGMENT
directives shown above, the linker organizes the segments as it en-
counters them. If the programmer specifies the modules to the linker
in alphabetic order, the linker produces the following segment
ordering:
A_DATA
A_CODE
B_DATA
B_CODE
C_DATA
C_CODE
However, if the programmer specifies the class types shown in the
sample source modules, the linker organizes the segments by
classification as follows:
'DATA' class: A_DATA
B_DATA
C_DATA
'CODE' class: A_CODE
B_CODE
C_CODE
Notice that the linker still organizes the classifications in the
order in which it encounters the segments belonging to the various
classifications. To completely control the order in which the linker
organizes the segments, the programmer must use one of three basic
approaches. The preferred method involves using the /DOSSEG switch
with the linker. This produces the segment ordering shown in Figure
4-1. The second method involves creating a special source module that
contains empty SEGMENT-ENDS blocks for all the segments declared in
the various other source modules. The programmer creates the list in
the order the segments are to be arranged in memory and then specifies
the .OBJ file for this module as the first file for the linker to
process. This procedure establishes the order of all the segments
before LINK begins processing the other program modules, so the
programmer can declare segments in these other modules in any con-
venient order. For instance, the following source module rearranges
the result of the previous example so that the linker places the
'CODE' class before the 'DATA' class:
A_CODE SEGMENT PARA PUBLIC 'CODE'
A_CODE ENDS
B_CODE SEGMENT PARA PUBLIC 'CODE'
B_CODE ENDS
C_CODE SEGMENT PARA PUBLIC 'CODE'
C_CODE ENDS
A_DATA SEGMENT PARA PUBLIC 'DATA'
A_DATA ENDS
B_DATA SEGMENT PARA PUBLIC 'DATA'
B_DATA ENDS
C_DATA SEGMENT PARA PUBLIC 'DATA'
C_DATA ENDS
END
Rather than creating a new module, the third method places the same
segment ordering list shown above at the start of the first module
containing actual code or data that the programmer will be specifying
for the linker. This duplicates the approach used by Microsoft's newer
compilers, such as C version 4.0.
The ordering of segments within the load module has no direct effect
on the linker's adjustment of address references to locations within
the various segments. Only the GROUP directive and the SEGMENT
directive's combine parameter affect address adjustments performed by
the linker. See The MASM GROUP Directive, below.
Note: Certain older versions of the IBM Macro Assembler wrote segments
to the object file in alphabetic order regardless of their order in
the source file. These older versions can limit efforts to control
segment ordering. Upgrading to a new version of the assembler is the
best solution to this problem.
Ordering segments to shrink the .EXE file
Correct segment ordering can significantly decrease the size of a .EXE
program as it resides on disk. This size-reduction ordering is
achieved by placing all uninitialized data fields in their own
segments and then controlling the linker's ordering of the program's
segments so that the uninitialized data field segments all reside at
the end of the program. When the program modules are assembled, MASM
places information in the object modules to tell the linker about
initialized and uninitialized areas of all segments. The linker then
uses this information to prevent the writing of uninitialized data
areas that occur at the end of the program image as part of the
resulting .EXE file. To account for the memory space required by these
fields, the linker also sets the MINALLOC field in the .EXE file
header to represent the data area not written to the file. MS-DOS then
uses the MINALLOC field to reallocate this missing space when loading
the program.
The MASM GROUP directive
The MASM GROUP directive can also have a strong impact on a .EXE
program. However, the GROUP directive has no effect on the arrangement
of program segments within memory. Rather, GROUP associates program
segments for addressing purposes.
The GROUP directive has the following syntax:
grpname GROUP segname,segname,segname,...
This directive causes the linker to adjust all address references to
labels within any specified segname to be relative to the start of the
declared group. The start of the group is determined at link time. The
group starts with whichever of the segments in the GROUP list the
linker places lowest in memory.
That the GROUP directive neither causes nor requires contiguous
arrangement of the grouped segments creates some interesting, although
not necessarily desirable, possibilities. For instance, it permits the
programmer to locate segments not belonging to the declared group
between segments that do belong to the group. The only restriction
imposed on the declared group is that the last byte of the last
segment in the group must occur within 64 KB of the start of the
group. Figure 4-7 illustrates this type of segment arrangement:
────────────────────────────────── ┌─────────────────────────────────┐
│ │
│ │ SEGMENT_C │
│ │ (listed with GROUP directive) │
│ ───────────────LABEL_C │ │
│ ├─────────────────────────────────┤
│ │ ─────LABEL_B │ │
64 KB │ │ SEGMENT_B │
maximum │ Offset to │(not listed with GROUP directive)│
│ Offset to LABEL_B │ │
│ LABEL_C ───────────── ├─────────────────────────────────┤
│ │ ─────LABEL_A │ │
│ │ │ SEGMENT_A │
│ │ Offset to │ (listed with GROUP directive) │
LABEL_A │ │
────────────────────────────────── └─────────────────────────────────┘
Figure 4-7. Noncontiguous segments in the same GROUP.
Warning: One of the most confusing aspects of the GROUP directive
relates to MASM's OFFSET operator. The GROUP directive affects only
the offset addresses generated by such direct addressing instructions
as
MOV AX,FIELD_LABEL
but it has no effect on immediate address values generated by such
instructions as
MOV AX,OFFSET FIELD_LABEL
Using the OFFSET operator on labels contained within grouped segments
requires the following approach:
MOV AX,OFFSET GROUP_NAME:FIELD_LABEL
The programmer must explicitly request the offset from the group base,
because MASM defines the result of the OFFSET operator to be the
offset of the label from the start of its segment, not its group.
Structuring a small program with SEGMENT and GROUP
Now that we have analyzed the functions performed by the SEGMENT and
GROUP directives, we'll put both directives to work structuring a
skeleton program. The program, shown in Figures 4-8, 4-9, and 4-10,
consists of three source modules (MODULE_A, MODULE_B, and MODULE_C),
each using the following four program segments:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Segment Definition
──────────────────────────────────────────────────────────────────
_TEXT The code or program text segment
_DATA The standard data segment containing preinitialized
data fields the program might change
CONST The constant data segment containing constant data
fields the program will not change
_BSS The "block storage segment/space" segment containing
uninitialized data fields
──────────────────────────────────────────────────────────────────────
Figure 4-8. Structuring a .EXE program: MODULE_A.
──────────────────────────────────────────────────────────────────────
Figure 4-9. Structuring a .EXE program: MODULE_B.
──────────────────────────────────────────────────────────────────────
Figure 4-10. Structuring a .EXE program: MODULE_C.
──────────────────────────────────────────────────────────────────────
This example creates a small memory model program image, so the linked
program can have only a single code segment and a single data
segment--the simplest standard form of a .EXE program. See Using
Microsoft's Contemporary Memory Models, below.
In addition to declaring the four segments already discussed, MODULE_A
declares a STACK segment in which to define a block of memory for use
as the program's stack and also defines the linking order of the five
segments. Defining the linking order leaves the programmer free to
declare the segments in any order when defining the segment contents--
a necessity because the assembler has difficulty assembling programs
that use forward references.
With Microsoft's MASM and LINK on the same disk with the .ASM files,
the following commands can be made into a batch file:
MASM STRUCA;
MASM STRUCB;
MASM STRUCC;
LINK STRUCA+STRUCB+STRUCC/M;
These commands will assemble and link all the .ASM files listed,
producing the memory map report file STRUCA.MAP shown in Figure 4-11.
Start Stop Length Name Class
00000H 0000CH 0000DH _TEXT CODE
0000EH 0001FH 00012H _DATA DATA
00020H 0003DH 0001EH CONST CONST
0003EH 0004EH 00011H _BSS BSS
00050H 0014FH 00100H STACK STACK
Origin Group
0000:0 DGROUP
Address Publics by Name
0000:000B PROC_B
0000:000C PROC_C
Address Publics by Value
0000:000B PROC_B
0000:000C PROC_C
Program entry point at 0000:0000
Figure 4-11. Structuring a .EXE program: memory map report.
The above memory map report represents the memory diagram shown in
Figure 4-12.
Absolute address Size in bytes
00150H ┌────────┬──────────┬──────────┬────── ─────
│ │ STACK │ STACK │ 256
│ │ Class │ (A) │ │
00050H ├ ─ ─ ─ ─┼──────────┴──────────┼────── │
│ │ PARA align gap │ 1 │
0004FH ├ ─ ─ ─ ─┼──────────┬──────────┼─────────── │
│ │ │BSS (C) │ 5 │
0004AH ├ ─ ─ ─ ─┼ ─ ─ ─ ─ ─┼──────────┼────── │ │
│ │ │WORD │ │ │
│ │ │align gap │ │ │
00049H ├ ─ ─ ─ ─├ BSS ─ ─┼──────────┤ │ │
│ │ │BSS (B) │ 5 15 │
00044H ├ ─ ─ ─ ─├ Class ─ ─┼──────────┼────── │ │
│ │ │WORD │ │ │
│ │ │align gap │ │ │
00043H ├ DGROUP─│ ─ ─ ─ ─ ─┼──────────┤ │ 321
│ │ │BSS (A) │ 5 │
0003EH ├ Group ─┼──────────┼──────────┼─────────── │
│ │ │CONST(C) │ 10 │
00034H ├ ─ ─ ─ ─┼ CONST ─ ─┼──────────┼────── │ │
│ │ │CONST(B) │ 10 30 │
0002AH ├ ─ ─ ─ ─┼ Class ─ ─┼──────────┼────── │ │
│ │ │CONST(A) │ 10 │
00020H ├ ─ ─ ─ ─┼──────────┬──────────┬─────────── │
│ │ │DATA (C) │ 6 │
0001AH ├ ─ ─ ─ ─┼─ DATA─ ─ ├──────────┼────── │ │
│ │ │DATA (B) │ 6 18 │
00014H ├ ─ ─ ─ ─┼─ Class ─ ├──────────┼────── │ │
│ │ │DATA (A) │ 6 │
0000EH ├ ─ ─ ─ ─┼──────────┴──────────┼─────────── │
│ │ WORD align gap │ 1
0000DH ├────────┼──────────┬──────────┼──────────── ─────
│ │ │TEXT (C) │ 1
0000CH │ ─ ─ ─CODE ─ ─ ─ ─ ├──────────┼────── │
│ │TEXT (B) │ 1 13
0000BH │ ─ ─ ─Class─ ─ ─ ─ ├──────────┼────── │
DGROUP │ │ │TEXT (A) │ 11
addressing 00000H └────────┴──────────┴──────────┴───────────
base
Figure 4-12. Structure of the sample .EXE program.
Using Microsoft's contemporary memory models
Now that we've analyzed the various aspects of designing assembly-
language .EXE programs, we can look at how Microsoft's high-level-
language compilers create .EXE programs from high-level-language
source files. Even assembly-language programmers will find this
discussion of interest and should seriously consider using the five
standard memory models outlined here.
This discussion is based on the Microsoft C Compiler version 4.0,
which, along with the Microsoft FORTRAN Compiler version 4.0,
incorporates the most contemporary code generator currently available.
These newer compilers generate code based on three to five
of the following standard programmer-selectable program structures,
referred to as memory models. The discussion of each of these memory
models will center on the model's use with the Microsoft C Compiler
and will close with comments regarding any differences for the
Microsoft FORTRAN Compiler.
Small (C compiler switch /AS) This model, the default, includes only a
single code segment and a single data segment. All code must fit
within 64 KB, and all data must fit within an additional 64 KB. Most C
program designs fall into this category. Data can exceed the 64 KB
limit only if the far and huge attributes are used, forcing the
compiler to use far addressing, and the linker to place far and huge
data items into separate segments. The data-size-threshold switch
described for the compact model is ignored by the Microsoft C Compiler
when used with a small model. The C compiler uses the default segment
name _TEXT for all code and the default segment name _DATA for all
non-far/huge data. Microsoft FORTRAN programs can generate a semblance
of this model only by using the /NM (name module) and /AM (medium
model) compiler switches in combination with the near attribute on all
subprogram declarations.
Medium (C and FORTRAN compiler switch /AM) This model includes only a
single data segment but breaks the code into multiple code segments.
All data must fit within 64 KB, but the 64 KB restriction on code size
applies only on a module-by-module basis. Data can exceed the 64 KB
limit only if the far and huge attributes are used, forcing the
compiler to use far addressing, and the linker to place far and huge
data items into separate segments. The data-size-threshold switch
described for the compact model is ignored by the Microsoft C Compiler
when used with a medium model. The compiler uses the default segment
name _DATA for all non-far/huge data and the template module_TEXT to
create names for all code segments. The module element of module_TEXT
indicates where the compiler is to substitute the name of the source
module. For example, if the source module HELPFUNC.C is compiled using
the medium model, the compiler creates the code segment HELPFUNC_TEXT.
The Microsoft FORTRAN Compiler version 4.0 directly supports the
medium model.
Compact (C compiler switch /AC) This model includes only a single code
segment but breaks the data into multiple data segments. All code must
fit within 64 KB, but the data is allowed to consume all the remaining
available memory. The Microsoft C Compiler's optional data-size-
threshold switch (/Gt) controls the placement of the larger data items
into additional data segments, leaving the smaller items in the
default segment for faster access. Individual data items within the
program cannot exceed 64 KB under the compact model without being
explicitly declared huge. The compiler uses the default segment name
_TEXT for all code segments and the template module#_DATA to create
names for all data segments. The module element indicates where the
compiler is to substitute the source module's name; the # element
represents a digit that the compiler changes for each additional data
segment required to hold the module's data. The compiler starts with
the digit 5 and counts up. For example, if the name of the source
module is HELPFUNC.C, the compiler names the first data segment
HELPFUNC5_DATA. FORTRAN programs can generate a semblance of this
model only by using the /NM (name module) and /AL (large model)
compiler switches in combination with the near attribute on all
subprogram declarations.
Large (C and FORTRAN compiler switch /AL) This model creates multiple
code and data segments. The compiler treats data in the same manner as
it does for the compact model and treats code in the same manner as it
does for the medium model. The Microsoft FORTRAN Compiler version 4.0
directly supports the large model.
Huge (C and FORTRAN compiler switch /AH) Allocation of segments under
the huge model follows the same rules as for the large model. The
difference is that individual data items can exceed 64 KB. Under the
huge model, the compiler generates the necessary code to index arrays
or adjust pointers across segment boundaries, effectively transforming
the microprocessor's segment-addressed memory into linear-addressed
memory. This makes the huge model especially useful for porting a
program originally written for a processor that used linear
addressing. The speed penalties the program pays in exchange for this
addressing freedom require serious consideration. If the program
actually contains any data structures exceeding 64 KB, it probably
contains only a few. In that case, it is best to avoid using the huge
model by explicitly declaring those few data items as huge using the
huge keyword within the source module. This prevents penalizing all
the non-huge items with extra addressing math. The Microsoft FORTRAN
Compiler version 4.0 directly supports the huge model.
Figure 4-13 shows an example of the segment arrangement created by a
large/huge model program. The example assumes two source modules:
MSCA.C and MSCB.C. Each source module specifies enough data to cause
the compiler to create two extra data segments for that module. The
diagram does not show all the various segments that occur as a result
of linking with the run-time library or as a result of compiling with
the intention of using the CodeView debugger.
Groups Classes Segments
┌───────────┬───────────┬───────────┐
│ │ │ │ SMCLH: Program stack
│ │ STACK │ STACK │
│ ├───────────┼───────────┤
│ │ │ │ SM: All unitialized global
│ │ │ c_common │ items, CLH: Empty
│ DGROUP │ BSS ├───────────┤
│ │ │ │ SMCLH: All uninitialized non-
│ │ │ _BSS │ far/huge items
│ ├───────────┼───────────┤
│ │ │ │ SMCLH: Constants (floating point
│ │ CONST │ CONST │ constraints, segment addresses, etc)
│ ├───────────┼───────────┤
│ │ │ │ SMCLH: All items that don't end up
│ │ DATA │ _DATA │ anywhere else
├───────────┼───────────┼───────────┤
│ │ │ │ SM: Nonexistent, CLH: All unini-
│ │ FAR_BSS │ FAR_BSS │ tialized global items
│ ├───────────┼───────────┤
│ │ │ │ From MSCB only: SM Far/huge items,
│ │ │MSCB6_DATA │ CLH: Items larger than threshold
│ │ ├───────────┤
│ │ │ │ From MSCB only: SM Far/huge items,
│ │ │MSCB5_DATA │ CLH: Items larger than threshold
│ │ FAR_DATA ├───────────┤
│ │ │ │ From MSCB only: SM Far/huge items,
│ │ │MSCA6_DATA │ CLH: Items larger than threshold
│ │ ├───────────┤
│ │ │ │ From MSCB only: SM Far/huge items,
│ │ │MSCA5_DATA │ CLH: Items larger than threshold
│ ├───────────┼───────────┤
│ │ │ │ SC: All code, MLH: Run-time
│ │ │ TEXT │ library code only
│ │ ├───────────┤
│ │ CODE │ │ SC: Nonexistent, MLH: MSCB.C Code
│ │ │ MSCB_TEXT │
│ │ ├───────────┤
│ │ │ │ SC: Nonexistent, MLH: MSCA.C Code
│ │ │ MSCA_TEXT │
└───────────┴───────────┴───────────┘ ───────────────────────
S = Small model L = Large model
M = Medium model H = Huge model
C = Compact model
Figure 4-13. General structure of a Microsoft C program.
Note that if the program declares an extremely large number of small
data items, it can exceed the 64 KB size limit on the default data
segment (_DATA) regardless of the memory model specified. This occurs
because the data items all fall below the data-size-threshold limit
(compiler /Gt switch), causing the compiler to place them in the _DATA
segment. Lowering the data size threshold or explicitly using the far
attribute within the source modules eliminates this problem.
Modifying the .EXE file header
With most of its language compilers, Microsoft supplies a utility
program called EXEMOD. See PROGRAMMING UTILITIES: EXEMOD. This utility
allows the programmer to display and modify certain fields contained
within the .EXE file header. Following are the header fields EXEMOD
can modify (based on EXEMOD version 4.0):
MAXALLOC This field can be modified by using EXEMOD's /MAX switch.
Because EXEMOD operates on .EXE files that have already been linked,
the /MAX switch can be used to modify the MAXALLOC field in existing
.EXE programs that contain the default MAXALLOC value of FFFFH,
provided the programs do not rely on MS-DOS's allocating all free
memory to them. EXEMOD's /MAX switch functions in an identical manner
to LINK's /CPARMAXALLOC switch.
MINALLOC This field can be modified by using EXEMOD's /MIN switch.
Unlike the case with the MAXALLOC field, most programs do not have an
arbitrary value for MINALLOC. MINALLOC normally represents
uninitialized memory and stack space the linker has compressed out of
the .EXE file, so a programmer should never reduce the MINALLOC value
within a .EXE program written by someone else. If a program requires
some minimum amount of extra dynamic memory in addition to any static
fields, MINALLOC can be increased to ensure that the program will have
this extra memory before receiving control. If this is done, the
program will not have to verify that MS-DOS allocated enough memory to
meet program needs. Of course, the same result can be achieved without
EXEMOD by declaring this minimum extra memory as an uninitialized
field at the end of the program.
Initial SP Value This field can be modified by using the /STACK switch
to increase or decrease the size of a program's stack. However,
modifying the initial SP value for programs developed using Microsoft
language compiler versions earlier than the following may cause the
programs to fail: C version 3.0, Pascal version 3.3, and FORTRAN
version 3.3. Other language compilers may have the same restriction.
The /STACK switch can also be used with programs developed using MASM,
provided the stack space is linked at the end of the program, but it
would probably be wise to change the size of the STACK segment
declaration within the program instead. The linker also provides a
/STACK switch that performs the same purpose.
Note: With the /H switch set, EXEMOD displays the current values of
the fields within the .EXE header. This switch should not be used with
the other switches. EXEMOD also displays field values if no switches
are used.
Warning: EXEMOD also functions correctly when used with packed .EXE
files created using EXEPACK or the /EXEPACK linker switch. However, it
is important to use the EXEMOD version shipped with the linker or
EXEPACK utility. Possible future changes in the packing method may
result in incompatibilities between EXEMOD and nonassociated
linker/EXEPACK versions.
Patching the .EXE program using DEBUG
Every experienced programmer knows that programs always seem to have
at least one unspotted error. If a program has been distributed to
other users, the programmer will probably need to provide those users
with corrections when such bugs come to light. One inexpensive
updating approach used by many large companies consists of mailing out
single-page instructions explaining how the user can patch the program
to correct the problem.
Program patching usually involves loading the program file into the
DEBUG utility supplied with MS-DOS, storing new bytes into the program
image, and then saving the program file back to disk. Unfortunately,
DEBUG cannot load a .EXE program into memory and then save it back to
disk in .EXE format. The programmer must trick DEBUG into patching
.EXE program files, using the procedure outlined below. See
PROGRAMMING UTILITIES: DEBUG.
Note: Users should be reminded to make backup copies of their program
before attempting the patching procedure.
1. Rename the .EXE file using a filename extension that does not have
special meaning for DEBUG. (Avoid .EXE, .COM, and .HEX.) For
instance, MYPROG.BIN serves well as a temporary new name for
MYPROG.EXE because DEBUG does not recognize a file with a .BIN
extension as anything special. DEBUG will load the entire image of
MYPROG.BIN, including the .EXE header and relocation table, into
memory starting at offset 100H within a .COM-style program segment
(as discussed previously).
2. Locate the area within the load module section of the .EXE file
image that requires patching. The previous discussion of the .EXE
file image, together with compiler/ assembler listings and linker
memory map reports, provides the information necessary to locate
the error within the .EXE file image. DEBUG loads the file image
starting at offset 100H within a .COM-style program segment, so the
programmer must compensate for this offset when calculating
addresses within the file image. Also, the compiler listings and
linker memory map reports provide addresses relative to the start
of the program image within the .EXE file, not relative to the
start of the file itself. Therefore, the programmer must first
check the information contained in the .EXE file header to
determine where the load module (the program's image) starts within
the file.
3. Use DEBUG's E (Enter Data) or A (Assemble Machine Instructions
command to insert the corrections. (Normally, patch instructions to
users would simply give an address at which the user should apply
the patch. The user need not know how to determine the address.)
4. After the patch has been applied, simply issue the DEBUG W (Write
File or Sectors) command to write the corrected image back to disk
under the same filename, provided the patch has not increased the
size of the program. If program size has increased, first
change the appropriate size fields in then .EXE header at the start
of the file and use the DEBUG R (Display or Modify Registers)
command to modify the BX and CX registers so that they contain the
file image's new size. Then use the W command to write the image
back to disk under the same name.
5. Use the DEBUG Q (Quit) command to return to MS-DOS command level,
and then rename the file to the original .EXE filename extension.
.EXE summary
To summarize, the .EXE program and file structures provide
considerable flexibility in the design of programs, providing the
programmer with the necessary freedom to produce large-scale
applications. Programs written using Microsoft's high-level-language
compilers have access to five standardized program structure models
(small, medium, compact, large, and huge). These standardized models
are excellent examples of ways to structure assembly-language
programs.
The .COM Program
The majority of differences between .COM and .EXE programs exist
because .COM program files are not prefaced by header information.
Therefore, .COM programs do not benefit from the features the .EXE
header provides.
The absence of a header leaves MS-DOS with no way of knowing how much
memory the .COM program requires in addition to the size of the
program's image. Therefore, MS-DOS must always allocate the largest
free block of memory to the .COM program, regardless of the program's
true memory requirements. As was discussed for .EXE programs, this
allocation of the largest block of free memory usually results in
MS-DOS's allocating all remaining free memory--an action that can
cause problems for multitasking supervisor programs.
The .EXE program header also includes the direct segment address
relocation pointer table. Because they lack this table, .COM programs
cannot make address references to the labels specified in SEGMENT
directives, with the exception of SEGMENT AT address directives. If a
.COM program did make these references, MS-DOS would have no way of
adjusting the addresses to correspond to the actual segment address
into which MS-DOS loaded the program. See Creating the .COM Program,
below.
The .COM program structure exists primarily to support the vast number
of CP/M programs ported to MS-DOS. Currently, .COM programs are most
often used to avoid adding the 512 bytes or more of .EXE header
information onto small, simple programs that often do not exceed 512
bytes by themselves.
The .COM program structure has another advantage: Its memory
organization places the PSP within the same address segment as the
rest of the program. Thus, it is easier to access fields within the
PSP in .COM programs.
Giving control to the .COM program
After allocating the largest block of free memory to the .COM program,
MS-DOS builds a PSP in the lowest 100H bytes of the block. No
difference exists between the PSP MS-DOS builds for .COM programs and
the PSP it builds for .EXE programs. Also with .EXE programs, MS-DOS
determines the initial values for the AL and AH registers at this time
and then loads the entire .COM-file image into memory immediately
following the PSP. Because .COM files have no file-size header fields,
MS-DOS relies on the size recorded in the disk directory to determine
the size of the program image. It loads the program exactly as it
appears in the file, without checking the file's contents.
MS-DOS then sets the DS, ES, and SS segment registers to point to the
start of the PSP. If able to allocate at least 64 KB to the program,
MS-DOS sets the SP register to offset FFFFH + 1 (0000H) to establish
an initial stack; if less than 64 KB are available for allocation to
the program, MS-DOS sets the SP to 1 byte past the highest offset
owned by the program. In either case, MS-DOS then pushes a single word
of 0000H onto the program's stack for use in terminating the program.
Finally, MS-DOS transfers control to the program by setting the CS
register to the PSP's segment address and the IP register to 0100H.
This means that the program's entry point must exist at the very start
of the program's image, as shown in later examples.
Figure 4-14 shows the overall structure of a .COM program as it
receives control from MS-DOS.
.COM program memory image
┌───────────┐
│ ┌───┬──┬───────────────┐ ─
SP=FFEH ────┘ │00H│00H│ │
│ Remaining free memory │ │
│ within first 64 KB │ │
│ allocated to .COM pro-│ │
│ gram (provided a full │ │
│ 64 KB was available) │ │
│ │ 64 KB
├───────────────────────┤ │
┌─│ .COM program image │ │
│ │ from file │ │
┌───────────────────────┐│ │ │ │
│ ││ ├───────────────────────┤ IP=0100H │
│ .COM program image ├┘ │Program segment prefix │
└───────────────────────┘ └───────────────────────┘ CS,DS,ES,SS ─
Figure 4-14. The .COM program: memory map diagram with register
pointers.
Terminating the .COM program
A .COM program can use all the termination methods described for .EXE
programs but should still use the MS-DOS Interrupt 21H Terminate
Process with Return Code function (4CH) as the preferred method. If
the .COM program must remain compatible with versions of MS-DOS
earlier than 2.0, it can easily use any of the older termination
methods, including those described as difficult to use from .EXE
programs, because .COM programs execute with the CS register pointing
to the PSP as required by these methods.
Creating the .COM program
A .COM program is created in the same manner as a .EXE program and
then converted using the MS-DOS EXE2BIN utility. See PROGRAMMING
UTILITIES: EXE2BIN.
Certain restrictions do apply to .COM programs, however. First, .COM
programs cannot exceed 64 KB minus 100H bytes for the PSP minus
2 bytes for the zero word initially pushed on the stack.
Next, only a single segment--or at least a single addressing group--
should exist within the program. The following two examples show ways
to structure a .COM program to satisfy both this restriction and
MASM's need to have data fields precede program code in the source
file.
COMPROG1.ASM (Figure 4-15) declares only a single segment (COMSEG), so
no special considerations apply when using the MASM OFFSET operator.
See The MASM GROUP Directive above. COMPROG2.ASM (Figure 4-16)
declares separate code (CSEG) and data (DSEG) segments, which the
GROUP directive ties into a common addressing block. Thus, the
programmer can declare data fields at the start of the source file and
have the linker place the data fields segment (DSEG) after the code
segment (CSEG) when it links the program, as discussed for the .EXE
program structure. This second example simulates the program
structuring provided under CP/M by Microsoft's old Macro-80 (M80)
macro assembler and Link-80 (L80) linker. The design also expands
easily to accommodate COMMON or other additional segments.
──────────────────────────────────────────────────────────────────────
Figure 4-15. .COM program with data at start.
──────────────────────────────────────────────────────────────────────
Figure 4-16. .COM program with data at end.
──────────────────────────────────────────────────────────────────────
These examples demonstrate other significant requirements for
producing a functioning .COM program. For instance, the ORG 0100H
statement in both examples tells MASM to start assembling the code at
offset 100H within the encompassing segment. This corresponds to
MS-DOS's transferring control to the program at IP = 0100H. In
addition, the entry-point label (BEGIN) immediately follows the ORG
statement and appears again as a parameter to the END statement.
Together, these factors satisfy the requirement that .COM programs
declare their entry point at offset 100H. If any factor is missing,
the MS-DOS EXE2BIN utility will not properly convert the .EXE file
produced by the linker into a .COM file. Specifically, if a .COM
program declares an entry point (as a parameter to the END statement)
that is at neither offset 0100H nor offset 0000H, EXE2BIN rejects the
.EXE file when the programmer attempts to convert it. If the program
fails to declare an entry point or declares an entry point at offset
0000H, EXE2BIN assumes that the .EXE file is to be converted to a
binary image rather than to a .COM image. When EXE2BIN converts a .EXE
file to a non-.COM binary file, it does not strip the extra 100H bytes
the linker places in front of the code as a result of the ORG
0100H instruction. Thus, the program actually begins at offset
200H when MS-DOS loads it into memory, but all the program's address
references will have been assembled and linked based on the 100H
offset. As a result, the program--and probably the rest of the system
as well--is likely to crash.
A .COM program also must not contain direct segment address references
to any segments that make up the program. Thus, the .COM program
cannot reference any segment labels or reference any labels as long
(FAR) pointers. (This rule does not prevent the program from
referencing segment labels declared using the SEGMENT AT address
directive.) Following are various examples of direct segment address
references that are not permitted as part of .COM programs:
PROC_A PROC FAR
PROC_A ENDP
CALL PROC_A ;intersegment call
JMP PROC_A ;intersegment jump
or
EXTRN PROC_A:FAR
CALL PROC_A ;intersegment call
JMP PROC_A ;intersegment jump
or
MOV AX,SEG SEG_A ;segment address
DD LABEL_A ;segment:offset pointer
Finally, .COM programs must not declare any segments with the STACK
combine type. If a program declares a segment with the STACK combine
type, the linker will insert initial SS and SP values into the .EXE
file header, causing EXE2BIN to reject the .EXE file. A .COM program
does not have explicitly declared stacks, although it can reserve
space in a non-STACK combine type segment to which it can initialize
the SP register after it receives control. The absence of a stack
segment will cause the linker to issue a harmless warning message.
When the program is assembled and linked into a .EXE file, it must be
converted into a binary file with a .COM extension by using the
EXE2BIN utility as shown in the following example for the file
YOURPROG.EXE:
C>EXE2BIN YOURPROG YOURPROG.COM <ENTER>
It is not necessary to delete or rename a .EXE file with the same
filename as the .COM file before trying to execute the .COM file as
long as both remain in the same directory, because MS-DOS's order of
execution is .COM files first, then .EXE files, and finally .BAT
files. However, the safest practice is to delete a .EXE file
immediately after converting it to a .COM file in case the .COM file
is later renamed or moved to a different directory. If a .EXE file
designed for conversion to a .COM file is executed by accident, it is
likely to crash the system.
Patching the .COM program using DEBUG
As discussed for .EXE files, a programmer who distributes software to
users will probably want to send instructions on how to patch in error
corrections. This approach to software updates lends itself even
better to .COM files than it does to .EXE files.
For example, because .COM files contain only the code image, they need
not be renamed in order to read and write them using DEBUG. The user
need only be instructed on how to load the .COM file into DEBUG, how
to patch the program, and how to write the patched image back to disk.
Calculating the addresses and patch values is even easier, because no
header exists in the .COM file image to cause complications. With the
preceding exceptions, the details for patching .COM programs remain
the same as previously outlined for .EXE programs.
.COM summary
To summarize, the .COM program and file structures are a simpler but
more restricted approach to writing programs than the .EXE structure
because the programmer has only a single memory model from which to
choose (the .COM program segment model). Also, .COM program files do
not contain the 512-byte (or more) header inherent to .EXE files, so
the .COM program structure is well suited to small programs for which
adding 512 bytes of header would probably at least double the file's
size.
Summary of Differences
The following table summarizes the differences between .COM and .EXE
programs.
╓┌─────────────────────┌────────────────────────────┌────────────────────────╖
.COM program .EXE program
─────────────────────────────────────────────────────────────────────
Maximum size 65536 bytes minus 256 bytes No limit
for PSP and 2 bytes for
stack
Entry point PSP:0100H Defined by END statement
CS at entry PSP Segment containing
program's entry point
IP at entry 0100H Offset of entry point
within its segment
DS at entry PSP PSP
ES at entry PSP PSP
SS at entry PSP Segment with STACK
attribute
SP at entry FFFEH or top word in End of segment
available memory, defined with
whichever is lower STACK attribute
Stack at entry Zero word Initialized or
uninitialized,
depending on source
Stack size 65536 bytes minus 256 bytes Defined in
for PSP and size of segment with
executable code and data STACK attribute
Subroutine calls NEAR NEAR or FAR
Exit method Interrupt 21H Function 4CH Interrupt 21H Function
preferred; NEAR RET if 4CHpreferred;
MS-DOS versions 1.x indirect jump to
PSP:0000H if MS-DOS
versions 1.x
Size of file Exact size of program Size of program plus
header (at least 512
extra bytes)
Which format the programmer uses for an application usually depends on
the program's intended size, but the decision can also be influenced
by a program's need to address multiple memory segments. Normally,
small utility programs (such as CHKDSK and FORMAT) are designed as
.COM programs; large programs (such as the Microsoft C Compiler) are
designed as .EXE programs. The ultimate decision is, of course, the
programmer's.
Keith Burgoyne
Article 5: Character Device Input and Output
All functional computer systems are composed of a central processing
unit (CPU), some memory, and peripheral devices that the CPU can use
to store data or communicate with the outside world. In MS-DOS
systems, the essential peripheral devices are the keyboard (for
input), the display (for output), and one or more disk drives (for
nonvolatile storage). Additional devices such as printers, modems, and
pointing devices extend the functionality of the computer or offer
alternative methods of using the system.
MS-DOS recognizes two types of devices: block devices, which are
usually floppy-disk or fixed-disk drives; and character devices, such
as the keyboard, display, printer, and communications ports.
The distinction between block and character devices is not always
readily apparent, but in general, block devices transfer information
in chunks, or blocks, and character devices move data one character
(usually 1 byte) at a time. MS-DOS identifies each block device by a
drive letter assigned when the device's controlling software, the
device driver, is loaded. A character device, on the other hand, is
identified by a logical name (similar to a filename and subject to
many of the same restrictions) built into its device driver. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Installable
Device Drivers.
Background Information
Versions 1.x of MS-DOS, first released for the IBM PC in 1981,
supported peripheral devices with a fixed set of device drivers
loaded during system initialization from the hidden file IO.SYS (or
IBMBIO.COM with PC-DOS). These versions of MS-DOS offered application
programs a high degree of input/output device independence by allowing
character devices to be treated like files, but they did not provide
an easy way to augment the built-in set of drivers if the user wished
to add a third-party peripheral device to the system.
With the release of MS-DOS version 2.0, the hardware flexibility of
the system was tremendously enhanced. Versions 2.0 and later support
installable device drivers that can reside in separate files on the
disk and can be linked into the operating system simply by adding a
DEVICE directive to the CONFIG.SYS file on the startup disk. See USER
COMMANDS: CONFIG.SYS: DEVICE. A well-defined interface between
installable drivers and the MS-DOS kernel allows such drivers to be
written for most types of peripheral devices without the need for
modification to the operating system itself.
The CONFIG.SYS file can contain a number of different DEVICE commands
to load separate drivers for pointing devices, magnetic-tape drives,
network interfaces, and so on. Each driver, in turn, is specialized
for the hardware characteristics of the device it supports. When the
system is turned on or restarted, the installable device drivers are
added to the chain, or linked list, of default device drivers loaded
from IO.SYS during MS-DOS initialization. Thus, the need for the
system's default set of device drivers to support a wide range of
optional device types and features at an excessive cost of system
memory is avoided.
One important distinction between block and character devices is that
MS-DOS always adds new block-device drivers to the tail of the driver
chain but adds new character-device drivers to the head of the chain.
Thus, because MS-DOS searches the chain sequentially and uses the
first driver it finds that satisfies its search conditions, any
existing character-device driver can be superseded by simply
installing another driver with an identical logical device name.
This article covers some of the details of working with MS-DOS
character devices: displaying text, keyboard input, and other basic
character I/O functions; the definition and use of standard input and
output; redirection of the default character devices; and the use of
the IOCTL function (Interrupt 21H Function 44H) to communicate
directly with a character-device driver. Much of the information
presented in this article is applicable only to MS-DOS versions 2.0
and later.
Accessing Character Devices
Application programs can use either of two basic techniques to access
character devices in a portable manner under MS-DOS. First, a program
can use the handle-type function calls that were added to MS-DOS in
version 2.0. Alternatively, a program can use the so-called
"traditional" character-device functions that were present in versions
1.x and have been retained in the operating system for compatibility.
Because the handle functions are more powerful and flexible, they are
discussed first.
A handle is a 16-bit number returned by the operating system whenever
a file or device is opened or created by passing a name to MS-DOS
Interrupt 21H Function 3CH (Create File with Handle), 3DH (Open File
with Handle), 5AH (Create Temporary File), or 5BH (Create New File).
After a handle is obtained, it can be used with Interrupt 21H Function
3FH (Read File or Device) or Function 40H (Write File or Device) to
transfer data between the computer's memory and the file or device.
During an open or create function call, MS-DOS searches the device-
driver chain sequentially for a character device with the specified
name (the extension is ignored) before searching the disk directory.
Thus, a file with the same name as any character device in the driver
chain--for example, the file NUL.TXT--cannot be created, nor can an
existing file be accessed if a device in the chain has the same name.
The second method for accessing character devices is through the
traditional MS-DOS character input and output functions, Interrupt 21H
Functions 01H through 0CH. These functions are designed to communicate
directly with the keyboard, display, printer, and serial port. Each of
these devices has its own function or group of functions, so neither
names nor handles need be used. However, in MS-DOS versions 2.0 and
later, these function calls are translated within MS-DOS to make use
of the same routines that are used by the handle functions, so the
traditional keyboard and display functions are affected by I/O
redirection and piping.
Use of either the traditional or the handle-based method for character
device I/O results in highly portable programs that can be used on any
computer that runs MS-DOS. A third, less portable access method is to
use the hardware-specific routines resident in the read-only memory
(ROM) of a specific computer (such as the IBM PC ROM BIOS driver
functions), and a fourth, definitely nonportable approach is to
manipulate the peripheral device's adapter directly, bypassing the
system software altogether. Although these latter hardware-dependent
methods cannot be recommended, they are admittedly sometimes necessary
for performance reasons.
The Basic MS-DOS Character Devices
Every MS-DOS system supports at least the following set of logical
character devices without the need for any additional installable
drivers:
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Device Meaning
──────────────────────────────────────────────────────────────────
CON Keyboard and display
PRN System list device, usually a parallel port
AUX Auxiliary device, usually a serial port
CLOCK$ System real-time clock
NUL "Bit-bucket" device
These devices can be opened by name or they can be addressed through
the "traditional" function calls; strings can be read from or written
to the devices according to their capabilities on any MS-DOS system.
Data written to the NUL device is discarded; reads from the NUL device
always return an end-of-file condition.
PC-DOS and compatible implementations of MS-DOS typically also support
the following logical character-device names:
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Device Meaning
──────────────────────────────────────────────────────────────────
COM1 First serial communications port
COM2 Second serial communications port
LPT1 First parallel printer port
LPT2 Second parallel printer port
LPT3 Third parallel printer port
In such systems, PRN is an alias for LPT1 and AUX is an alias for
COM1. The MODE command can be used to redirect an LPT device to
another device. See USER COMMANDS: MODE.
As previously mentioned, any of these default character-device drivers
can be superseded by a user-installed device driver--for example, one
that offers enhanced functionality or changes the device's apparent
characteristics. One frequently used alternative character-device
driver is ANSI.SYS, which replaces the standard MS-DOS CON device
driver and allows ANSI escape sequences to be used to perform tasks
such as clearing the screen, controlling the cursor position, and
selecting character attributes. See USER COMMANDS: ANSI.SYS.
The standard devices
Under MS-DOS versions 2.0 and later, each program owns five previously
opened handles for character devices (referred to as the standard
devices) when it begins executing. These handles can be used for input
and output operations without further preliminaries. The five standard
devices and their associated handles are
╓┌───────────────────────────────────────┌────────────┌──────────────────────╖
Standard Device Name Handle Default Assignment
──────────────────────────────────────────────────────────────────
Standard input (stdin) 0 CON
Standard output (stdout) 1 CON
Standard error (stderr) 2 CON
Standard auxiliary (stdaux) 3 AUX
Standard printer (stdprn) 4 PRN
The standard input and standard output handles are especially
important because they are subject to I/O redirection. Although these
handles are associated by default with the CON device so that read and
write operations are implemented using the keyboard and video display,
the user can associate the handles with other character devices or
with files by using redirection parameters in a program's command
line:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Redirection Result
──────────────────────────────────────────────────────────────────────
< file Causes read operations from standard input to obtain
data from file.
> file Causes data written to standard output to be placed
in file.
>> file Causes data written to standard output to be appended
to file.
p1 | p2 Causes data written to standard output by program p1
to appear as the standard input of program p2.
This ability to redirect I/O adds great flexibility and power to the
system. For example, programs ordinarily controlled by keyboard
entries can be run with "scripts" from files, the output of a program
can be captured in a file or on a printer for later inspection, and
general-purpose programs (filters) can be written that process text
streams without regard to the text's origin or destination. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Writing
MS-DOS Filters.
Ordinarily, an application program is not aware that its input or
output has been redirected, although a write operation to standard
output will fail unexpectedly if standard output was redirected to a
disk file and the disk is full. An application can check for the
existence of I/O redirection with an IOCTL (Interrupt 21H Function
44H) call, but it cannot obtain any information about the destination
of the redirected handle except whether it is associated with a
character device or with a file.
Raw versus cooked mode
MS-DOS associates each handle for a character device with a mode that
determines how I/O requests directed to that handle are treated. When
a handle is in raw mode, characters are passed between the application
program and the device driver without any filtering or buffering by
MS-DOS. When a handle is in cooked mode, MS-DOS buffers any data that
is read from or written to the device and takes special actions when
certain characters are detected.
During cooked mode input, MS-DOS obtains characters from the device
driver one at a time, checking each character for a Control-C. The
characters are assembled into a string within an internal MS-DOS
buffer. The input operation is terminated when a carriage return (0DH)
or an end-of-file mark (1AH) is received or when the number of
characters requested by the application have been accumulated. If the
source is standard input, lone linefeed characters are translated to
carriage-return/linefeed pairs. The string is then copied from the
internal MS-DOS buffer to the application program's buffer, and
control returns to the application program.
During cooked mode output, MS-DOS transfers the characters in the
application program's output buffer to the device driver one at a
time, checking after each character for a Control-C pending at the
keyboard. If the destination is standard output and standard output
has not been redirected, tabs are expanded to spaces using eight-
column tab stops. Output is terminated when the requested number of
characters have been written or when an end-of-file mark (1AH) is
encountered in the output string.
In contrast, during raw mode input or output, data is transferred
directly between the application program's buffer and the device
driver. Special characters such as carriage return and the end-of-file
mark are ignored, and the exact number of characters in the
application program's request are always read or written. MS-DOS does
not break the strings into single-character calls to the device driver
and does not check the keyboard buffer for Control-C entries during
the I/O operation. Finally, characters read from standard input in raw
mode are not echoed to standard output.
As might be expected from the preceding description, raw mode input or
output is usually much faster than cooked mode input or output,
because each character is not being individually processed by the
MS-DOS kernel. Raw mode also allows programs to read characters from
the keyboard buffer that would otherwise be trapped by MS-DOS (for
example, Control-C, Control-P, and Control-S). (If BREAK is on, MS-DOS
will still check for Control-C entries during other function calls,
such as disk operations, and transfer control to the Control-C
exception handler if a Control-C is detected.) A program can use the
MS-DOS IOCTL Get and Set Device Data services (Interrupt 21H Function
44H Subfunctions 00H and 01H) to set the mode for a character-device
handle. See IOCTL, below.
Ordinarily, raw or cooked mode is strictly an attribute of a specific
handle that was obtained from a previous open operation and affects
only the I/O operations requested by the program that owns the handle.
However, when a program uses IOCTL to select raw or cooked mode for
one of the standard device handles, the selection has a global effect
on the behavior of the system because those handles are never closed.
Thus, some of the "traditional" keyboard input functions might behave
in unexpected ways. Consequently, programs that change the mode on a
standard device handle should save the handle's mode at entry and
restore it before performing a final exit to MS-DOS, so that the
operation of COMMAND.COM and other applications will not be disturbed.
Such programs should also incorporate custom critical error and
Control-C exception handlers so that the programs cannot be terminated
unexpectedly. See PROGRAMMING IN THE MS-DOS ENVIRONMENT: CUSTOMIZING
MS-DOS: Exception Handlers.
The keyboard
Among the MS-DOS Interrupt 21H functions are two methods of checking
for and receiving input from the keyboard: the traditional method,
which uses MS-DOS character input Functions 01H, 06H, 07H, 08H, 0AH,
0BH, and 0CH (Table 5-1); and the handle method, which uses Function
3FH. Each of these methods has its own advantages and disadvantages.
See SYSTEM CALLS.
Table 5-1. Traditional MS-DOS Character Input Functions.
╓┌───────────────┌─────────────────────────────┌──────────────┌──────┌───────╖
Read Multiple Ctrl-C
Function Name Characters Echo Check
──────────────────────────────────────────────────────────────────────
01H Character Input with Echo No Yes Yes
06H Direct Console I/O No No No
07H Unfiltered Character Input
Without Echo No No No
08H Character Input Without Echo No No Yes
0AH Buffered Keyboard Input Yes Yes Yes
0BH Check Keyboard Status No No Yes
0CH Flush Buffer, Read Keyboard
The first four traditional keyboard input calls are really very
similar. They all return a character in the AL register; they differ
mainly in whether they echo that character to the display and whether
they are sensitive to interruption by the user's entry of a Control-C.
Both Functions 06H and 0BH can be used to test keyboard status (that
is, whether a key has been pressed and is waiting to be read by the
program); Function 0BH is simpler to use, but Function 06H is immune
to Control-C entries.
Function 0AH is used to read a "buffered line" from the user, meaning
that an entire line is accepted by MS-DOS before control returns to
the program. The line is terminated when the user presses the Enter
key or when the maximum number of characters (to 255) specified by the
program have been received. While entry of the line is in progress,
the usual editing keys (such as the left and right arrow keys and the
function keys on IBM PCs and compatibles) are active; only the final,
edited line is delivered to the requesting program.
Function 0CH allows a program to flush the type-ahead buffer before
accepting input. This capability is important for occasions when a
prompt must be displayed unexpectedly (such as when a critical error
occurs) and the user could not have typed ahead a valid response. This
function should also be used when the user is being prompted for a
critical decision (such as whether to erase a file), to prevent a
character that was previously pressed by accident from triggering an
irrecoverable operation. Function 0CH is unusual in that it is called
with the number of one of the other keyboard input functions in
register AL. After any pending input has been discarded, Function 0CH
simply transfers to the other specified input function; thus, its
other parameters (if any) depend on the function that ultimately will
be executed.
The primary disadvantage of the traditional function calls is that
they handle redirected input poorly. If standard input has been
redirected to a file, no way exists for a program calling the
traditional input functions to detect that the end of the file has
been reached-the input function will simply wait forever, and the
system will appear to hang.
A program that wishes to use handle-based I/O to get input from the
keyboard must use the MS-DOS Read File or Device service, Interrupt
21H Function 3FH. Ordinarily, the program can employ the predefined
handle for standard input (0), which does not need to be opened and
which allows the program's input to be redirected by the user to
another file or device. If the program needs to circumvent redirection
and ensure that its input is from the keyboard, it can open the CON
device with Interrupt 21H Function 3DH and use the handle obtained
from that open operation instead of the standard input handle.
A program using the handle functions to read the keyboard can control
the echoing of characters and sensitivity to Control-C entries by
selecting raw or cooked mode with the IOCTL Get and Set Device Data
services (default = cooked mode). To test the keyboard status, the
program can either issue an IOCTL Check Input Status call (Interrupt
21H Function 44H Subfunction 06H) or use the traditional Check
Keyboard Status call (Interrupt 21H Function 0BH).
The primary advantages of the handle method for keyboard input are its
symmetry with file operations and its graceful handling of redirected
input. The handle function also allows strings as long as 65535 bytes
to be requested; the traditional Buffered Keyboard Input function
allows a maximum of 255 characters to be read at a time. This
consideration is important for programs that are frequently used with
redirected input and output (such as filters), because reading and
writing larger blocks of data from files results in more efficient
operation. The only real disadvantage to the handle method is that it
is limited to MS-DOS versions 2.0 and later (although this is no
longer a significant restriction).
Role of the ROM BIOS
When a key is pressed on the keyboard of an IBM PC or compatible, it
generates a hardware interrupt (09H) that is serviced by a routine in
the ROM BIOS. The ROM BIOS interrupt handler reads I/O ports assigned
to the keyboard controller and translates the key's scan code into an
ASCII character code. The result of this translation depends on the
current state of the NumLock and CapsLock toggles, as well as on
whether the Shift, Control, or Alt key is being held down. (The ROM
BIOS maintains a keyboard flags byte at address 0000:0417H that gives
the current status of each of these modifier keys.)
After translation, both the scan code and the ASCII code are placed in
the ROM BIOS's 32-byte (16-character) keyboard input buffer. In the
case of "extended" keys such as the function keys or arrow keys, the
ASCII code is a zero byte and the scan code carries all the
information. The keyboard buffer is arranged as a circular, or ring,
buffer and is managed as a first-in/first-out queue. Because of the
method used to determine when the buffer is empty, one position in the
buffer is always wasted; the maximum number of characters that can be
held in the buffer is therefore 15. Keys pressed when the buffer is
full are discarded and a warning beep is sounded.
The ROM BIOS provides an additional module, invoked by software
Interrupt 16H, that allows programs to test keyboard status, determine
whether characters are waiting in the type-ahead buffer, and remove
characters from the buffer. See Appendix O: IBM PC BIOS Calls. Its use
by application programs should ordinarily be avoided, however, to
prevent introducing unnecessary hardware dependence.
On IBM PCs and compatibles, the keyboard input portion of the CON
driver in the BIOS is a simple sequence of code that calls ROM BIOS
Interrupt 16H to do the hardware-dependent work. Thus, calls to MS-DOS
for keyboard input by an application program are subject to two layers
of translation: The Interrupt 21H function call is converted by the
MS-DOS kernel to calls to the CON driver, which in turn remaps the
request onto a ROM BIOS call that obtains the character.
Keyboard programming examples
Example: Use the ROM BIOS keyboard driver to read a character from the
keyboard. The character is not echoed to the display.
mov ah,00h ; subfunction 00H = read character
int 16h ; transfer to ROM BIOS
; now AH = scan code, AL = character
Example: Use the MS-DOS traditional keyboard input function to read a
character from the keyboard. The character is not echoed to the
display. The input can be interrupted with a Ctrl-C keystroke.
mov ah,08h ; function 08H = character input
; without echo
int 21h ; transfer to MS-DOS
; now AL = character
Example: Use the MS-DOS traditional Buffered Keyboard Input function
to read an entire line from the keyboard, specifying a maximum line
length of 80 characters. All editing keys are active during entry, and
the input is echoed to the display.
kbuf db 80 ; maximum length of read
db 0 ; actual length of read
db 80 dup (0) ; keyboard input goes here
.
.
.
mov dx,seg kbuf ; set DS:DX = address of
mov ds,dx ; keyboard input buffer
mov dx,offset kbuf
mov ah,0ah ; function 0AH = read buffered line
int 21h ; transfer to MS-DOS
; terminated by a carriage return,
; and kbuf+1 = length of input,
; not including the carriage return
Example: Use the MS-DOS handle-based Read File or Device function and
the standard input handle to read an entire line from the keyboard,
specifying a maximum line length of 80 characters. All editing keys
are active during entry, and the input is echoed to the display. (The
input will not terminate on a carriage return as expected if standard
input is in raw mode.)
kbuf db 80 dup (0) ; buffer for keyboard input
.
.
.
mov dx,seg kbuf ; set DS:DX = address of
mov ds,dx ; keyboard input buffer
mov dx,offset kbuf
mov cx,80 ; CX = maximum length of input
mov bx,0 ; standard input handle = 0
mov ah,3fh ; function 3FH = read file/device
int 21h ; transfer to MS-DOS
jc error ; jump if function failed
; otherwise AX = actual
; length of keyboard input,
; including carriage-return and
; linefeed, and the data is
; in the buffer 'kbuf'
The display
The output half of the MS-DOS logical character device CON is the
video display. On IBM PCs and compatibles, the video display is an
"option" of sorts that comes in several forms. IBM has introduced five
video subsystems that support different types of displays: the
Monochrome Display Adapter (MDA), the Color/Graphics Adapter (CGA),
the Enhanced Graphics Adapter (EGA), the Video Graphics Array (VGA),
and the Multi-Color Graphics Array (MCGA). Other, non-IBM-compatible
video subsystems in common use include the Hercules Graphics Card and
its variants that support downloadable fonts.
Two portable techniques exist for writing text to the video display
with MS-DOS function calls. The traditional method is supported by
Interrupt 21H Functions 02H (Character Output), 06H (Direct Console
I/O), and 09H (Display String). The handle method is supported by
Function 40H (Write File or Device) and is available only in MS-DOS
versions 2.0 and later. See SYSTEM CALLS: INTERRUPT 21H: Functions
02H, 06H, 09H, 40H. All these calls treat the display essentially
as a "glass teletype" and do not support bit-mapped graphics.
Traditional Functions 02H and 06H are similar. Both are called with
the character to be displayed in the DL register; they differ in that
Function 02H is sensitive to interruption by the user's entry of a
Control-C, whereas Function 06H is immune to Control-C but cannot be
used to output the character 0FFH (ASCII rubout). Both calls check
specifically for carriage return (0DH), linefeed (0AH), and backspace
(08H) characters and take the appropriate action if these characters
are detected.
Because making individual calls to MS-DOS for each character to be
displayed is inefficient and slow, the traditional Display String
function (09H) is generally used in preference to Functions 02H and
06H. Function 09H is called with the address of a string that is
terminated with a dollar-sign character ($); it displays the entire
string in one operation, regardless of its length. The string can
contain embedded control characters such as carriage return and
linefeed.
To use the handle method for screen display, programs must call the
MS-DOS Write File or Device service, Interrupt 21H Function 40H.
Ordinarily, a program should use the predefined handle for standard
output (1) to send text to the screen, so that any redirection
requested by the user on the program's command line will be honored.
If the program needs to circumvent redirection and ensure that its
output goes to the screen, it can either use the predefined handle for
standard error (2) or explicitly open the CON device with Interrupt
21H Function 3DH and use the resulting handle for its write
operations.
The handle technique for displaying text has several advantages over
the traditional calls. First, the length of the string to be displayed
is passed as an explicit parameter, so the string need not contain a
special terminating character and the $ character can be displayed as
part of the string. Second, the traditional calls are translated to
handle calls inside MS-DOS, so the handle calls have less internal
overhead and are generally faster. Finally, use of the handle Write
File or Device function to display text is symmetric with the methods
the program must use to access its files. In short, the traditional
functions should be avoided unless the program must be capable of
running under MS-DOS versions 1.x.
Controlling the screen
One of the deficiencies of the standard MS-DOS CON device driver is
the lack of screen-control capabilities. The default CON driver has no
built-in routines to support cursor placement, screen clearing,
display mode selection, and so on.
In MS-DOS versions 2.0 and later, an optional replacement CON driver
is supplied in the file ANSI.SYS. This driver contains most of the
screen-control capabilities needed by text-oriented application
programs. The driver is installed by adding a DEVICE directive to the
CONFIG.SYS file and restarting the system. When ANSI.SYS is active, a
program can position the cursor, inquire about the current cursor
position, select foreground and background colors, and clear the
current line or the entire screen by sending an escape sequence
consisting of the ASCII Esc character (1BH) followed by various
function-specific parameters to the standard output device. See USER
COMMANDS: ANSI.SYS.
Programs that use the ANSI.SYS capabilities for screen control are
portable to any MS-DOS implementation that contains the ANSI.SYS
driver. Programs that seek improved performance by calling the ROM
BIOS video driver or by assuming direct control of the hardware are
necessarily less portable and usually require modification when new PC
models or video subsystems are released.
Role of the ROM BIOS
The video subsystems in IBM PCs and compatibles use a hybrid of
memory-mapped and port-addressed I/O. A range of the machine's memory
addresses is typically reserved for a video refresh buffer that holds
the character codes and attributes to be displayed on the screen; the
cursor position, display mode, palettes, and similar global display
characteristics are governed by writing control values to specific I/O
ports.
The ROM BIOS of IBM PCs and compatibles contains a primitive driver
for the MDA, CGA, EGA, VGA, and MCGA video subsystems. This driver
supports the following functions:
■ Read or write characters with attributes at any screen position.
■ Query or set the cursor position.
■ Clear or scroll an arbitrary portion of the screen.
■ Select palette, background, foreground, and border colors.
■ Query or set the display mode (40-column text, 80-column text, all-
points-addressable graphics, and so on).
■ Read or write a pixel at any screen coordinate.
These functions are invoked by a program through software Interrupt
10H. See Appendix O: IBM PC BIOS Calls. In PC-DOS-compatible
implementations of MS-DOS, the display portions of the MS-DOS CON and
ANSI.SYS drivers use these ROM BIOS routines. Video subsystems that
are not IBM compatible either must contain their own ROM BIOS or must
be used with an installable device driver that captures Interrupt 10H
and provides appropriate support functions.
Text-only application programs should avoid use of the ROM BIOS
functions or direct access to the hardware whenever possible, to
ensure maximum portability between MS-DOS systems. However, because
the MS-DOS CON driver contains no support for bit-mapped graphics,
graphically oriented applications usually must resort to direct
control of the video adapter and its refresh buffer for speed and
precision.
Display programming examples
Example: Use the ROM BIOS Interrupt 10H function to write an asterisk
character to the display in text mode. (In graphics mode, BL must also
be set to the desired foreground color.)
mov ah,0eh ; subfunction 0EH = write character
; in teletype mode
mov al,'*' ; AL = character to display
mov bh,0 ; select display page 0
int 10h ; transfer to ROM BIOS video driver
Example: Use the MS-DOS traditional function to write an asterisk
character to the display. If the user's entry of a Control-C is
detected during the output and standard output is in cooked mode,
MS-DOS calls the Control-C exception handler whose address is found in
the vector for Interrupt 23H.
mov ah,02h ; function 02H = display character
mov dl,'*' ; DL = character to display
int 21h ; transfer to MS-DOS
Example: Use the MS-DOS traditional function to write a string to the
display. The output is terminated by the $ character and can be
interrupted when the user enters a Control-C if standard output is in
cooked mode.
msg db 'This is a test message','$'
.
.
.
mov dx,seg msg ; DS:DX = address of text
mov ds,dx ; to display
mov dx,offset msg
mov ah,09h ; function 09H = display string
int 21h ; transfer to MS-DOS
Example: Use the MS-DOS handle-based Write File or Device function and
the predefined handle for standard output to write a string to the
display. Output can be interrupted by the user's entry of a Control-C
if standard output is in cooked mode.
msg db 'This is a test message'
msg_len equ $-msg
.
.
.
mov dx,seg msg ; DS:DX = address of text
mov ds,dx ; to display
mov dx,offset msg
mov cx,msg_len ; CX = length of text
mov bx,1 ; BX = handle for standard output
mov ah,40h ; function 40H = write file/device
int 21h ; transfer to MS-DOS
The serial communications ports
Through version 3.2, MS-DOS has built-in support for two serial
communications ports, identified as COM1 and COM2, by means of three
drivers named AUX, COM1, and COM2. (AUX is ordinarily an alias for
COM1.)
The traditional MS-DOS method of reading from and writing to the
serial ports is through Interrupt 21H Function 03H for AUX input and
Function 04H for AUX output. In MS-DOS versions 2.0 and later, the
handle-based Read File or Device and Write File or Device functions
(Interrupt 21H Functions 3FH and 40H) can be used to read from or
write to the auxiliary device. A program can use the predefined handle
for the standard auxiliary device (3) with Functions 3FH and 40H, or
it can explicitly open the COM1 or COM2 devices with Interrupt 21H
Function 3DH and use the handle obtained from that open operation to
perform read and write operations.
MS-DOS support for the serial communications port is inadequate in
several respects for high-performance serial I/O applications. First,
MS-DOS provides no portable way to test for the existence or the
status of a particular serial port in a system; if a program "opens"
COM2 and writes data to it and the physical COM2 adapter is not
present in the system, the program may simply hang. Similarly, if the
serial port exists but no character has been received and the program
attempts to read a character, the program will hang until one is
available; there is no traditional function call to check if a
character is waiting as there is for the keyboard.
MS-DOS also provides no portable method to initialize the
communications adapter to a particular baud rate, word length, and
parity. An application must resort to ROM BIOS calls, manipulate the
hardware directly, or rely on the user to configure the port properly
with the MODE command before running the application that uses it. The
default settings for the serial port on PC-DOS-compatible systems are
2400 baud, no parity, 1 stop bit, and 8 databits. See USER COMMANDS:
MODE.
A more serious problem with the default MS-DOS auxiliary device driver
in IBM PCs and compatibles, however, is that it is not interrupt
driven. Accordingly, when baud rates above 1200 are selected,
characters can be lost during time-consuming operations performed by
the drivers for other devices, such as clearing the screen or reading
or writing a floppy-disk sector. Because the MS-DOS AUX device driver
typically relies on the ROM BIOS serial port driver (accessed through
software Interrupt 14H) and because the ROM BIOS driver is not
interrupt driven either, bypassing MS-DOS and calling the ROM BIOS
functions does not usually improve matters.
Because of all the problems just described, telecommunications
application programs commonly take over complete control of the serial
port and supply their own interrupt handler and internal buffering for
character read and write operations. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: Interrupt-Driven Communications.
Serial port programming examples
Example: Use the ROM BIOS serial port driver to write a string to
COM1.
msg db 'This is a test message'
msg_len equ $-msg
.
.
.
mov bx,seg msg ; DS:BX = address of message
mov ds,bx
mov bx,offset msg
mov cx,msg_len ; CX = length of message
mov dx,0 ; DX = 0 for COM1
L1: mov al,[bx] ; get next character into AL
mov ah,01h ; subfunction 01H = output
int 14h ; transfer to ROM BIOS
inc bx ; bump pointer to output string
loop L1 ; and loop until all chars. sent
Example: Use the MS-DOS traditional function for auxiliary device
output to write a string to COM1.
msg db 'This is a test message'
msg_len equ $-msg
.
.
.
mov bx,seg msg ; set DS:BX = address of message
mov ds,bx
mov bx,offset msg
mov cx,msg_len ; set CX = length of message
L1: mov dl,[bx] ; get next character into DL
mov ah,04h ; function 04H = auxiliary output
int 21h ; transfer to MS-DOS
inc bx ; bump pointer to output string
loop L1 ; and loop until all chars. sent
Example: Use the MS-DOS handle-based Write File or Device function and
the predefined handle for the standard auxiliary device to write a
string to COM1.
msg db 'This is a test message'
msg_len equ $-msg
.
.
.
mov dx,seg msg ; DS:DX = address of message
mov ds,dx
mov dx,offset msg
mov cx,msg_len ; CX = length of message
mov bx,3 ; BX = handle for standard aux.
mov ah,40h ; function 40H = write file/device
int 21h ; transfer to MS-DOS
jc error ; jump if write operation failed
The parallel port and printer
Most MS-DOS implementations contain device drivers for four printer
devices: LPT1, LPT2, LPT3, and PRN. PRN is ordinarily an alias for
LPT1 and refers to the first parallel output port in the system. To
provide for list devices that do not have a parallel interface, the
LPT devices can be individually redirected with the MODE command to
one of the serial communications ports. See USER COMMANDS: MODE.
As with the keyboard, the display, and the serial port, MS-DOS allows
the printer to be accessed with either traditional or handle-based
function calls. The traditional function call is Interrupt 21H
Function 05H, which accepts a character in DL and sends it to the
physical device currently assigned to logical device name LPT1.
A program can perform handle-based output to the printer with
Interrupt 21H Function 40H (Write File or Device). The predefined
handle for the standard printer (4) can be used to send strings to
logical device LPT1. Alternatively, the program can issue an open
operation for a specific printer device with Interrupt 21H Function
3DH and use the handle obtained from that open operation with Function
40H. This latter method also allows more than one printer to be used
at a time from the same program.
Because the parallel ports are assumed to be output only, no
traditional call exists for input from the parallel port. In addition,
no portable method exists to test printer port status under MS-DOS;
programs that wish to avoid sending a character to the printer adapter
when it is not ready or not physically present in the system must test
the adapter's status by making a call to the ROM BIOS printer driver
(by means of software Interrupt 17H; see Appendix O: IBM PC BIOS
Calls) or by accessing the hardware directly.
Parallel port programming examples
Example: Use the ROM BIOS printer driver to send a string to the first
parallel printer port.
msg db 'This is a test message'
msg_len equ $-msg
.
.
.
mov bx,seg msg ; DS:BX = address of message
mov ds,bx
mov bx,offset msg
mov cx,msg_len ; CX = length of message
mov dx,0 ; DX = 0 for LPT1
L1: mov al,[bx] ; get next character into AL
mov ah,00h ; subfunction 00H = output
int 17h ; transfer to ROM BIOS
inc bx ; bump pointer to output string
loop L1 ; and loop until all chars. sent
Example: Use the traditional MS-DOS function call to send a string to
the first parallel printer port.
msg db 'This is a test message'
msg_len equ $-msg
.
.
.
mov bx,seg msg ; DS:BX = address of message
mov ds,bx
mov bx,offset msg
mov cx,msg_len ; CX = length of message
L1: mov dl,[bx] ; get next character into DL
mov ah,05h ; function 05H = printer output
int 21h ; transfer to MS-DOS
inc bx ; bump pointer to output string
loop L1 ; and loop until all chars. sent
Example: Use the handle-based MS-DOS Write File or Device call and the
predefined handle for the standard printer to send a string to the
system list device.
msg db 'This is a test message'
msg_len equ $-msg
.
.
.
mov dx,seg msg ; DS:DX = address of message
mov ds,dx
mov dx,offset msg
mov cx,msg_len ; CX = length of message
mov bx,4 ; BX = handle for standard printer
mov ah,40h ; function 40H = write file/device
int 21h ; transfer to MS-DOS
jc error ; jump if write operation failed
IOCTL
In versions 2.0 and later, MS-DOS has provided applications with the
ability to communicate directly with device drivers through a set of
subfunctions grouped under Interrupt 21H Function 44H (IOCTL). See
SYSTEM CALLS: INTERRUPT 21H: Function 44H. The IOCTL subfunctions that
are particularly applicable to the character I/O needs of application
programs are
╓┌────────────────────────────┌──────────────────────────────────────────────╖
Subfunction Name
──────────────────────────────────────────────────────────────────
00H Get Device Data
01H Set Device Data
02H Receive Control Data from Character Device
03H Send Control Data to Character Device
06H Check Input Status
07H Check Output Status
0AH Check if Handle is Remote (version 3.1 or
later)
0CH Generic I/O Control for Handles: Get/Set
Output Iteration Count
Various bits in the device information word returned by Subfunction
00H can be tested by an application to determine whether a specific
handle is associated with a character device or a file and whether the
driver for the device can process control strings passed by
Subfunctions 02H and 03H. The device information word also allows the
program to test whether a character device is the CLOCK$, standard
input, standard output, or NUL device and whether the device is in raw
or cooked mode. The program can then use Subfunction 01H to select raw
mode or cooked mode for subsequent I/O performed with the handle.
Subfunctions 02H and 03H allow control strings to be passed between
the device driver and an application; they do not usually result in
any physical I/O to the device. For example, a custom device driver
might allow an application program to configure the serial port by
writing a specific set of control parameters to the driver with
Subfunction 03H. Similarly, the custom driver might respond to
Subfunction 02H by passing the application a series of bytes that
defines the current configuration and status of the serial port.
Subfunctions 06H and 07H can be used by application programs to test
whether a device is ready to accept an output character or has a
character ready for input. These subfunctions are particularly
applicable to the serial communications ports and parallel printer
ports because MS-DOS does not supply traditional function calls to
test their status.
Subfunction 0AH can be used to determine whether the character device
associated with a handle is local or remote--that is, attached to the
computer the program is running on or attached to another computer on
a local area network. A program should not ordinarily attempt to
distinguish between local and remote devices during normal input and
output, but the information can be useful in attempts to recover from
error conditions. This subfunction is available only if Microsoft
Networks is running.
Finally, Subfunction 0CH allows a program to query or set the number
of times a device driver tries to send output to the printer before
assuming the device is not available.
IOCTL programming examples
Example: Use IOCTL Subfunction 00H to obtain the device information
word for the standard input handle and save it, and then use
Subfunction 01H to place standard input into raw mode.
info dw ? ; save device information word here
.
.
.
mov ax,4400h ; AH = function 44H, IOCTL
; AL = subfunction 00H, get device
; information word
mov bx,0 ; BX = handle for standard input
int 21h ; transfer to MS-DOS
mov info,dx ; save device information word
; (assumes DS = data segment)
or dl,20h ; set raw mode bit
mov dh,0 ; and clear DH as MS-DOS requires
mov ax,4401h ; AL = subfunction 01H, set device
; information word
; (BX still contains handle)
int 21h ; transfer to MS-DOS
Example: Use IOCTL Subfunction 06H to test whether a character is
ready for input on the first serial port. The function returns AL =
0FFH if a character is ready and AL = 00H if not.
mov ax,4406H ; AH = function 44H, IOCTL
; AL = subfunction 06H, get
; input status
mov bx,3 ; BX = handle for standard aux
int 21h ; transfer to MS-DOS
or al,al ; test status of AUX driver
jnz ready ; jump if input character ready
; else no character is waiting
Jim Kyle
Chip Rabinowitz
Article 6: Interrupt-Driven Communications
In the earliest days of personal-computer communications, when speeds
were no faster than 300 bits per second, primitive programs that moved
characters to and from the remote system were adequate. The PC had
time between characters to determine what it ought to do next and
could spend that time keeping track of the status of the remote
system.
Modern data-transfer rates, however, are four to eight times faster
and leave little or no time to spare between characters. At 1200 bits
per second, as many as three characters can be lost in the time
required to scroll the display up one line. At such speeds, a
technique to permit characters to be received and simultaneously
displayed becomes necessary.
Mainframe systems have long made use of hardware interrupts to
coordinate such activities. The processor goes about its normal
activity; when a peripheral device needs attention, it sends an
interrupt request to the processor. The processor interrupts its
activity, services the request, and then goes back to what it was
doing. Because the response is driven by the request, this type of
processing is known as interrupt-driven. It gives the effect of doing
two things at the same time without requiring two separate processors.
Successful telecommunication with PCs at modern data rates demands an
interrupt-driven routine for data reception. This article discusses in
detail the techniques for interrupt-driven communications and
culminates in two sample program packages.
The article begins by establishing the purpose of communications
programs and then discusses the capability of the simple functions
provided by MS-DOS to achieve this goal. To see what must be done to
supplement MS-DOS functions, the hardware (both the modem and the
serial port) is examined. This leads to a discussion of the method
MS-DOS has provided since version 2.0 for solving the problems of
special hardware interfacing: the installable device driver.
With the background established, alternate paths to interrupt-driven
communications are discussed--one following recommended MS-DOS
techniques, the other following standard industry practice--and
programs are developed for each.
Throughout this article, the discussion is restricted to the
architecture and BIOS of the IBM PC family. MS-DOS systems not totally
compatible with this architecture may require substantially different
approaches at the detailed level, but the same general principles
apply.
Purpose of Communications Programs
The primary purpose of any communications program is communicating--
that is, transmitting information entered as keystrokes (or bytes read
from a file) in a form suitable for transmission to a remote computer
via phone lines and, conversely, converting information received from
the remote computer into a display on the video screen (or data in a
file).
Some years ago, the most abstract form of all communications programs
was dubbed a modem engine, by analogy to Babbage's analytical engine
or the inference-engine model used in artificial-intelligence
development. The functions of the modem engine are common to all kinds
of communications programs, from the simplest to the most complex, and
can be described in a type of pseudo-C as follows:
The Modem Engine Pseudocode
DO { IF (input character is available)
send_it_to_remote;
IF (remote character is available)
use_it_locally;
} UNTIL (told_to_stop);
The essence of this modem-engine code is that the absence of an input
character, or of a character from the remote computer, does not hang
the loop in a wait state. Rather, the engine continues to cycle: If it
finds work to do, it does it; if not, the engine keeps looking.
Of course, at times it is desirable to halt the continuous action of
the modem engine. For example, when receiving a long message, it is
nice to be able to pause and read the message before the lines scroll
into oblivion. On the other hand, taking too long to study the screen
means that incoming characters are lost. The answer is a technique
called flow control, in which a special control character is sent to
shut down transmission and some other character is later sent to start
it up again.
Several conventions for flow control exist. One of the most widespread
is known as XON/XOFF, from the old Teletype-33 keycap legends for the
two control codes involved. In the original use, XOFF halted the paper
tape reader and XON started it going again. In mid-1967, the General
Electric Company began using these signals in its time-sharing
computer services to control the flow of data, and the practice
rapidly spread throughout the industry.
The sample program named ENGINE, shown later in this article, is an
almost literal implementation of the modem-engine approach. This
sample represents one extreme of simplicity in communications
programs. The other sample program, CTERM.C, is much more complex, but
the modem engine is still at its heart.
Using Simple MS-DOS Functions
Because MS-DOS provides, among its standard service functions, the
capability of sending output to or reading input from the device named
AUX (which defaults to COM1, the first serial port on most machines),
a first attempt at implementing the modem engine using MS-DOS
functions might look something like the following incomplete fragment
of Microsoft Macro Assembler (MASM) code:
;Incomplete (and Unworkable) Implementation
LOOP: MOV AH,08h ; read keyboard, no echo
INT 21h
MOV DL,AL ; set up to send
MOV AH,04h ; send to AUX device
INT 21h
MOV AH,03h ; read from AUX device
INT 21h
MOV DL,AL ; set up to send
MOV AH,02h ; send to screen
INT 21h
JMP LOOP ; keep doing it
The problem with this code is that it violates the keep-looking
principle both at the keyboard and at the AUX port: Interrupt 21H
Function 08H does not return until a keyboard character is available,
so no data from the AUX port can be read until a key is pressed
locally. Similarly, Function 03H waits for a character to become
available from AUX, so no more keys can be recognized locally until
the remote system sends a character. If nothing is received, the loop
waits forever.
To overcome the problem at the keyboard end, Function 0BH can be used
to determine if a key has been pressed before an attempt is made to
read one, as shown in the following modification of the fragment:
;Improved, (but Still Unworkable) Implementation
LOOP: MOV AH,0Bh ; test keyboard for char
INT 21h
OR AL,AL ; test for zero
JZ RMT ; no char avail, skip
MOV AH,08h ; have char, read it in
INT 21h
MOV DL,AL ; set up to send
MOV AH,04h ; send to AUX device
INT 21h
RMT:
MOV AH,03h ; read from AUX device
INT 21h
MOV DL,AL ; set up to send
MOV AH,02h ; send to screen
INT 21h
JMP LOOP ; keep doing it
This code permits any input from AUX to be received without waiting
for a local key to be pressed, but if AUX is slow about providing
input, the program waits indefinitely before checking the keyboard
again. Thus, the problem is only partially solved.
MS-DOS, however, simply does not provide any direct method of making
the required tests for AUX or, for that matter, any of the serial port
devices. That is why communications programs must be treated
differently from most other types of programs under MS-DOS and why
such programs must be intimately involved with machine details despite
all accepted principles of portable program design.
The Hardware Involved
Personal-computer communications require at least two distinct pieces
of hardware (separate devices, even though they are often combined on
a single board). These hardware items are the serial port, which
converts data from the computer's internal bus into a bit stream for
transmission over a single external line, and the modem, which
converts the bit stream into a form suitable for telephone-line (or,
sometimes, radio) transmission.
The modem
The modem (a word coined from MOdulator-DEModulator) is a device that
converts a stream of bits, represented as sequential changes of
voltage level, into audio frequency signals suitable for transmission
over voice-grade telephone circuits (modulation) and converts these
signals back into a stream of bits that duplicates the original input
(demodulation).
Specific characteristics of the audio signals involved were
established by AT&T when that company monopolized the modem industry,
and those characteristics then evolved into de facto standards when
the monopoly vanished. They take several forms, depending on the data
rate in use; these forms are normally identified by the original Bell
specification number, such as 103 (for 600 bps and below) or 212A (for
the 1200 bps standard).
The data rate is measured in bits per second (bps), often mistermed
baud or even "baud per second." A baud measures the number of signals
per second; as with knot (nautical miles per hour), the time reference
is built in. If one signal change marks one bit, as is true for the
Bell 103 standard, then baud and bps have equal values. However, they
are not equivalent for more complex signals. For example, the Bell
212A diphase standard for 1200 bps uses two tone streams, each
operating at 600 baud, to transmit data at 1200 bits per second.
For accuracy, this article uses bps, rather than baud, except where
widespread industry misuse of baud has become standardized (as in
"baud rate generator").
Originally, the modem itself was a box connected to the computer's
serial port via a cable. Characteristics of this cable, its
connectors, and its signals were standardized in the 1960s by the
Electronic Industries Association (EIA), in Standard RS232C. Like the
Bell standards for modems, RS232C has survived almost unchanged. Its
characteristics are listed in Table 6-1.
Table 6-1. RS232C Signals.
╓┌─────────────────────┌──────────┌───────────┌──────────────────────────────╖
DB25 Pin 232 Name Description
──────────────────────────────────────────────────────────────────
1 Safety Ground
2 BA TXD Transmit Data
3 BB RXD Receive Data
4 CA RTS Request To Send
5 CB CTS Clear To Send
6 CC DSR Data Set Ready
7 AB GND Signal Ground
8 CF DCD Data Carrier Detected
20 CD DTR Data Terminal Ready
22 CE RI Ring Indicator
With the increasing popularity of personal computers, internal modems
that plug into the PC's motherboard and combine the modem and a serial
port became available.
The first such units were manufactured by Hayes Corporation, and like
Bell and the EIA, they created a standard. Functionally, the internal
modem is identical to the combination of a serial port, a connecting
cable, and an external modem.
The serial port
Each serial port of a standard IBM PC connects the rest of the system
to a type INS8250 Universal Asynchronous Receiver Transmitter (UART)
integrated circuit (IC) chip developed by National Semiconductor
Corporation. This chip, along with associated circuits in the port,
1. Converts data supplied via the system data bus into a sequence of
voltage levels on the single TXD output line that represent binary
digits.
2. Converts data received as a sequence of binary levels on the single
RXD input line into bytes for the data bus.
3. Controls the modem's actions through the DTR and RTS output lines.
4. Provides status information to the processor; this information
comes from the modem, via the DSR, DCD, CTS, and RI input lines,
and from within the UART itself, which signals data available, data
needed, or error detected.
The word asynchronous in the name of the IC comes from the Bell
specifications. When computer data is transmitted, each bit's
relationship to its neighbors must be preserved; this can be done in
either of two ways. The most obvious method is to keep the bit stream
strictly synchronized with a clock signal of known frequency and count
the cycles to identify the bits. Such a transmission is known as
synchronous, often abbreviated to synch or sometimes bisync for binary
synchronous. The second method, first used with mechanical
teleprinters, marks the start of each bit group with a defined start
bit and the end with one or more defined stop bits, and it defines a
duration for each bit time. Detection of a start bit marks the
beginning of a received group; the signal is then sampled at each bit
time until the stop bit is encountered. This method is known as
asynchronous (or just asynch) and is the one used by the standard
IBM PC.
The start bit is, by definition, exactly the same as that used to
indicate binary zero, and the stop bit is the same as that indicating
binary one. A zero signal is often called SPACE, and a one signal is
called MARK, from terms used in the teleprinter industry.
During transmission, the least significant bit of the data is sent
first, after the start bit. A parity bit, if used, appears as the most
significant bit in the data group, before the stop bit or bits; it
cannot be distinguished from a databit except by its position. Once
the first stop bit is sent, the line remains in MARK (sometimes called
idling) condition until a new start bit indicates the beginning of
another group.
In most PC uses, the serial port transfers one 8-bit byte at a time,
and the term word specifies a 16-bit quantity. In the UART world,
however, a word is the unit of information sent by the chip in each
chunk. The word length is part of the control information set into the
chip during setup operations and can be 5, 6, 7, or 8 bits. This
discussion follows UART conventions and refers to words, rather than
to bytes.
One special type of signal, not often used in PC-to-PC communications
but sometimes necessary in communicating with mainframe systems, is a
BREAK. The BREAK is an all-SPACE condition that extends for more than
one word time, including the stop-bit time. (Many systems require the
BREAK to last at least 150 milliseconds regardless of data rate.)
Because it cannot be generated by any normal data character
transmission, the BREAK is used to interrupt, or break into, normal
operation. The IBM PC's 8250 UART can generate the BREAK signal, but
its duration must be determined by a program, rather than by the chip.
The 8250 UART architecture
The 8250 UART contains four major functional areas: receiver,
transmitter, control circuits, and status circuits. Because these
areas are closely related, some terms used in the following
descriptions are, of necessity, forward references to subsequent
paragraphs.
The major parts of the receiver are a shift register and a data
register called the Received Data Register. The shift register
assembles sequentially received data into word-parallel form by
shifting the level of the RXD line into its front end at each bit time
and, at the same time, shifting previous bits over. When the shift
register is full, all bits in it are moved over to the data register,
the shift register is cleared to all zeros, and the bit in the status
circuits that indicates data ready is set. If an error is detected
during receipt of that word, other bits in the status circuits are
also set.
Similarly, the major parts of the transmitter are a holding register
called the Transmit Holding Register and a shift register. Each word
to be transmitted is transferred from the data bus to the holding
register. If the holding register is not empty when this is done, the
previous contents are lost. The transmitter's shift register converts
word-parallel data into bit-serial form for transmission by shifting
the most significant bit out to the TXD line once each bit time, at
the same time shifting lower bits over and shifting in an idling bit
at the low end of the register. When the last databit has been shifted
out, any data in the holding register is moved to the shift register,
the holding register is filled with idling bits in case no more data
is forthcoming, and the bit in the status circuits that indicates the
Transmit Holding Register is empty is set to indicate that another
word can be transferred. The parity bit, if any, and stop bits are
added to the transmitted stream after the last databit of each word is
shifted out.
The control circuits establish three communications features: first,
line control values, such as word length, whether or not (and how)
parity is checked, and the number of stop bits; second, modem control
values, such as the state of the DTR and RTS output lines; and third,
the rate at which data is sent and received. These control values are
established by two 8-bit registers and one 16-bit register, which are
addressed as four 8-bit registers. They are the Line Control Register
(LCR), the Modem Control Register (MCR), and the 16-bit BRG Divisor
Latch, addressed as Baud0 and Baud1.
The BRG Divisor Latch sets the data rate by defining the bit time
produced by the Programmable Baud Rate Generator (PBRG), a major part
of the control circuits. The PBRG can provide any data speed from a
few bits per second to 38400 bps; in the BIOS of the IBM PC, PC/XT,
and PC/AT, though, only the range 110 through 9600 bps is supported.
How the LCR and the MCR establish their control values, how the PBRG
is programmed, and how interrupts are enabled are discussed later.
The fourth major area in the 8250 UART, the status circuits, records
(in a pair of status registers) the conditions in the receive and
transmit circuits, any errors that are detected, and any change in
state of the RS232C input lines from the modem. When any status
register's content changes, an interrupt request, if enabled, is
generated to notify the rest of the PC system. This approach lets the
PC attend to other matters without having to continually monitor the
status of the serial port, yet it assures immediate action when
something does occur.
The 8250 programming interface
Not all the registers mentioned in the preceding section are
accessible to programmers. The shift registers, for example, can be
read from or written to only by the 8250's internal circuits. There
are 10 registers available to the programmer, and they are accessed by
only seven distinct addresses (shown in Table 6-2). The Received Data
Register and the Transmit Holding Register share a single address (a
read gets the received data; a write goes to the holding register). In
addition, both this address and that of the Interrupt Enable Register
(IER) are shared with the PBRG Divisor Latch. A bit in the Line
Control Register called the Divisor Latch Access Bit (DLAB) determines
which register is addressed at any specific time.
In the IBM PC, the seven addresses used by the 8250 are selected by
the low 3 bits of the port number (the higher bits select the specific
port). Thus, each serial port occupies eight positions in the address
space. However, only the lowest address used--the one in which the low
3 bits are all 0--need be remembered in order to access all eight
addresses.
Because of this, any serial port in the PC is referred to by an
address that, in hexadecimal notation, ends with either 0 or 8: The
COM1 port normally uses address 03F8H, and COM2 uses 02F8H. This
lowest port address is usually called the base port address, and each
addressable register is then referenced as an offset from this base
value, as shown in Table 6-2.
Table 6-2. 8250 Port Offsets from Base Address.
╓┌────────────────┌──────────────────────┌───────────────────────────────────╖
Offset Name Description
──────────────────────────────────────────────────────────────────
If DLAB bit in LCR = 0:
00H DATA Received Data Register if
read from, Transmit Holding
Register if written to
01H IER Interrupt Enable Register
If DLAB bit in LCR = 1:
00H Baud0 BRG Divisor Latch, low byte
01H Baud1 BRG Divisor Latch, high byte
Not affected by DLAB bit:
02H IID Interrupt Identifier Register
03H LCR Line Control Register
04H MCR Modem Control Register
05H LSR Line Status Register
06H MSR Modem Status Register
The control circuits
The control circuits of the 8250 include the Programmable Baud Rate
Generator (PBRG), the Line Control Register (LCR), the Modem Control
Register (MCR), and the Interrupt Enable Register (IER).
The PBRG establishes the bit time used for both transmitting and
receiving data by dividing an external clock signal. To select a
desired bit rate, the appropriate divisor is loaded into the PBRG's
16-bit Divisor Latch by setting the Divisor Latch Access Bit (DLAB) in
the Line Control Register to 1 (which changes the functions of
addresses 0 and 1) and then writing the divisor into Baud0 and Baud1.
After the bit rate is selected, DLAB is changed back to 0, to permit
normal operation of the DATA registers and the IER.
With the 1.8432 MHz external UART clock frequency used in standard IBM
systems, divisor values (in decimal notation) for bit rates between
45.5 and 38400 bps are listed in Table 6-3. These speeds are
established by a crystal contained in the serial port (or internal
modem) and are totally unrelated to the speed of the processor's
clock.
Table 6-3. Bit Rate Divisor Table for 8250/IBM.
╓┌─────────────────┌─────────────────────────────────────────────────────────╖
BPS Divisor
──────────────────────────────────────────────────────────────────
45.5 2532
50 2304
75 1536
110 1047
134.5 857
150 768
300 384
600 192
1200 96
1800 64
2000 58
2400 48
4800 24
9600 12
19200 6
38400 3
The remaining control circuits are the Line Control Register, the
Modem Control Register, and the Interrupt Enable Register. Bits in the
LCR control the assignment of offsets 0 and 1, transmission of the
BREAK signal, parity generation, the number of stop bits, and the word
length sent and received, as shown in Table 6-4.
Table 6-4. 8250 Line Control Register Bit Values.
╓┌─────────────────┌─────────┌─────────────┌─────────────────────────────────╖
Bit Name Binary Meaning
──────────────────────────────────────────────────────────────────
Address Control:
7 DLAB 0xxxxxxx Offset 0 refers to DATA;
offset 1 refers to IER
1xxxxxxx Offsets 0 and 1 refer to BRG
Divisor Latch
BREAK Control:
6 SETBRK x0xxxxxx Normal UART operation
x1xxxxxx Send BREAK signal
Parity Checking:
5,4,3 GENPAR xxxx0xxx No parity bit
xx001xxx Parity bit is ODD
xx011xxx Parity bit is EVEN
xx101xxx Parity bit is 1
xx111xxx Parity bit is 0
Stop Bits:
2 XSTOP xxxxx0xx Only 1 stop bit
xxxxx1xx 2 stop bits(1.5 if WL = 5)
Word Length:
1,0 WD5 xxxxxx00 Word length = 5
WD6 xxxxxx01 Word length = 6
WD7 xxxxxx10 Word length = 7
WD8 xxxxxx11 Word length = 8
Two bits in the MCR (Table 6-5) control output lines DTR and RTS; two
other MCR bits (OUT1 and OUT2) are left free by the UART to be
assigned by the user; a fifth bit (TEST) puts the UART into a self-
test mode of operation. The upper 3 bits have no effect on the UART.
The MCR can be both read from and written to.
Both of the user-assignable bits are defined in the IBM PC. OUT1 is
used by Hayes internal modems to cause a power-on reset of their
circuits; OUT2 controls the passage of UART-generated interrupt
request signals to the rest of the PC. Unless OUT2 is set to 1,
interrupt signals from the UART cannot reach the rest of the PC, even
though all other controls are properly set. This feature is
documented, but obscurely, in the IBM Technical Reference manuals and
the asynchronous-adapter schematic; it is easy to overlook when
writing an interrupt-driven program for these machines.
Table 6-5. 8250 Modem Control Register Bit Values.
╓┌────────────┌───────────┌──────────────────────────────────────────────────╖
Name Binary Description
──────────────────────────────────────────────────────────────────
TEST xxx1xxxx Turns on UART self-test configuration.
OUT2 xxxx1xxx Controls 8250 interrupt signals (User2 Output).
OUT1 xxxxx1xx Resets Hayes 1200b internal modem (User1 Output).
RTS xxxxxx1x Sets RTS output to RS232C connector.
DTR xxxxxxx1 Sets DTR output to RS232C connector.
The 8250 can generate any or all of four classes of interrupts, each
individually enabled or disabled by setting the appropriate control
bit in the Interrupt Enable Register (Table 6-6). Thus, setting the
IER to 00H disables all the UART interrupts within the 8250 without
regard to any other settings, such as OUT2, system interrupt masking,
or the CLI/STI commands. The IER can be both read from and written to.
Only the low 4 bits have any effect on the UART.
Table 6-6. 8250 Interrupt Enable Register Constants.
╓┌─────────────────┌─────────────────────────────────────────────────────────╖
Binary Action
──────────────────────────────────────────────────────────────────
xxxx1xxx Enable Modem Status Interrupt.
xxxxx1xx Enable Line Status Interrupt.
xxxxxx1x Enable Transmit Register Interrupt.
xxxxxxx1 Enable Received Data Ready Interrupt.
The status circuits
The status circuits of the 8250 include the Line Status Register
(LSR), the Modem Status Register (MSR), the Interrupt Identifier (IID)
Register, and the interrupt-request generation system.
The 8250 includes circuitry that detects a received BREAK signal and
also detects three classes of data-reception errors. Separate bits in
the LSR (Table 6-7) are set to indicate that a BREAK has been received
and to indicate any of the following: a parity error (if lateral
parity is in use), a framing error (incoming bit = 0 at stop-bit
time), or an overrun error (word not yet read from receive buffer by
the time the next word must be moved into it).
The remaining bits of the LSR indicate the status of the Transmit
Shift Register, the Transmit Holding Register, and the Received Data
Register; the most significant bit of the LSR is not used and is
always 0. The LSR is a read-only register; writing to it has no
effect.
Table 6-7. 8250 Line Status Register Bit Values.
╓┌──────────────┌────────────────┌───────────────────────────────────────────╖
Bit Binary Meaning
──────────────────────────────────────────────────────────────────
7 0xxxxxxx Always zero
6 x1xxxxxx Transmit Shift Register empty
5 xx1xxxxx Transmit Holding Register empty
4 xxx1xxxx BREAK received
3 xxxx1xxx Framing error
2 xxxxx1xx Parity error
1 xxxxxx1x Overrun error
0 xxxxxxx1 Received data ready
The MSR (Table 6-8) monitors the four RS232C lines that report modem
status. The upper 4 bits of this register indicate the voltage level
of the associated RS232C line; the lower 4 bits indicate that the
voltage level has changed since the register was last read.
Table 6-8. 8250 Modem Status Register Bit Values.
╓┌──────────────┌──────────────────┌─────────────────────────────────────────╖
Bit Binary Meaning
──────────────────────────────────────────────────────────────────
7 1xxxxxxx Data Carrier Detected (DCD) level
6 x1xxxxxx Ring Indicator (RI) level
5 xx1xxxxx Data Set Ready (DSR) level
4 xxx1xxxx Clear To Send (CTS) level
3 xxxx1xxx DCD change
2 xxxxx1xx RI change
1 xxxxxx1x DSR change
0 xxxxxxx1 CTS change
As mentioned previously, four types of interrupts are generated. The
four types are identified by flag values in the IID Register (Table
6-9). These flags are set as follows:
■ Change of any bit value in the MSR sets the modem status flag.
■ Setting of the BREAK Received bit or any of the three error bits in
the LSR sets the line status flag.
■ Setting of the Transmit Holding Register Empty bit in the LSR sets
the transmit flag.
■ Setting of the Received Data Ready bit in the LSR sets the receive
flag.
The IID register indicates the interrupt type, even though the IER may
be disabling that type of interrupt from generating any request. The
IID is a read-only register; attempts to write to it have no effect.
Table 6-9. 8250 Interrupt Identification and Causes.
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
IID content Meaning
──────────────────────────────────────────────────────────────────
xxxxxxx1B No interrupt active
xxxxx000B Modem Status Interrupt; bit changed in MSR
xxxxx010B Transmit Register Interrupt; Transmit Holding
Register empty, bitset in LSR
xxxxx100B Received Data Ready Interrupt; Data Register full,
bit set in LSR
xxxxx110B Line Status Interrupt; BREAK or error bit set in LSR
As shown in Table 6-9, an all-zero value (which in most of the other
registers is a totally disabling condition) means that a Modem Status
Interrupt condition has not yet been serviced. A modem need not be
connected, however, for a Modem Status Interrupt condition to occur;
all that is required is for one of the RS232C non-data input lines to
change state, thus changing the MSR.
Whenever a flag is set in the IID, the UART interrupt-request
generator will, if enabled by the UART programming, generate an
interrupt request to the processor. Two or more interrupts can be
active at the same time; if so, more than one flag in the IID register
is set.
The IID flag for each interrupt type (and the LSR or MSR bits
associated with it) clears when the corresponding register is read
(or, in one case, written to). For example, reading the content of the
MSR clears the modem status flag; writing a byte to the DATA register
clears the transmit flag; reading the DATA register clears the receive
flag; reading the LSR clears the line status flag. The LSR or MSR bit
does not clear until it has been read; the IID flag clears with the
LSR or MSR bit.
Programming the UART
Each time power is applied, any serial-interface device must be
programmed before it is used. This programming can be done by the
computer's bootstrap sequence or as a part of the port initialization
routines performed when a port driver is installed. Often, both
techniques are used: The bootstrap provides default conditions, and
these can be modified during initialization to meet the needs of each
port driver used in a session.
When the 8250 chip is programmed, the BRG Divisor Latch should be set
for the proper baud rate, the LCR and MCR should be loaded, the IER
should be set, and all internal interrupt requests and the receive
buffer should be cleared. The sequence in which these are done is not
especially critical, but any pending interrupt requests should be
cleared before they are permitted to pass on to the rest of the PC.
The following sample code performs these startup actions, setting up
the chip in device COM1 (at port 03F8H) to operate at 1200 bps with a
word length of 8 bits, no parity checking, and all UART interrupts
enabled. (In practical code, all values for addresses and operating
conditions would not be built in; these values are included in the
example to clarify what is being done at each step.)
MOV DX,03FBh ; base port COM1 (03F8) + LCR (3)
MOV AL,080h ; enable Divisor Latch
OUT DX,AL
MOV DX,03F8h ; set for Baud0
MOV AX,96 ; set divisor to 1200 bps
OUT DX,AL
INC DX ; to offset 1 for Baud1
MOV AL,AH ; high byte of divisor
OUT DX,AL
MOV DX,03FBh ; back to the LCR offset
MOV AL,03 ; DLAB = 0, Parity = N, WL = 8
OUT DX,AL
MOV DX,03F9h ; offset 1 for IER
MOV AL,0Fh ; enable all ints in 8250
OUT DX,AL
MOV DX,03FCh ; COM1 + MCR (4)
MOV AL,0Bh ; OUT2 + RTS + DTR bits
OUT DX,AL
CLRGS:
MOV DX,03FDh ; clear LSR
IN AL,DX
MOV DX,03F8h ; clear RX reg
IN AL,DX
MOV DX,03FEh ; clear MSR
IN AL,DX
MOV DX,03FAh ; IID reg
IN AL,DX
IN AL,DX ; repeat to be sure
TEST AL,1 ; int pending?
JZ CLRGS ; yes, repeat
Note: This code does not completely set up the IBM serial port.
Although it fully programs the 8250 itself, additional work remains to
be done. The system interrupt vectors must be changed to provide
linkage to the interrupt service routine (ISR) code, and the 8259
Priority Interrupt Controller (PIC) chip must also be programmed to
respond to interrupt requests from the UART channels. See PROGRAMMING
IN THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Hardware Interrupt
Handlers.
Device Drivers
All versions of MS-DOS since 2.0 have permitted the installation of
user-provided device drivers. From the standpoint of operating-system
theory, using such drivers is the proper way to handle generic
communications interfacing. The following paragraphs are intended as a
refresher and to explain this article's departure from standard
device-driver terminology. See PROGRAMMING IN THE MS-DOS ENVIRONMENT:
CUSTOMIZING MS-DOS: Installable Device Drivers.
An installable device driver consists of (1) a driver header that
links the driver to others in the chain maintained by MS-DOS, tells
the system the characteristics of this specific driver, provides
pointers to the two major routines contained in the driver, and (for a
character-device driver) identifies the driver by name; (2) any data
and storage space the driver may require; and (3) the two major code
routines.
The code routines are called the Strategy routine and the Interrupt
routine in normal device-driver descriptions. Neither has any
connection with the hardware interrupts dealt with by the drivers
presented in this article. Because of this, the term Request routine
is used instead of Interrupt routine, so that hardware interrupt code
can be called an interrupt service routine (ISR) with minimal chances
for confusion.
MS-DOS communicates with a device driver by reserving space for a
command packet of as many as 22 bytes and by passing this packet's
address to the driver with a call to the Strategy routine. All data
transfer between MS-DOS and the driver, in both directions, occurs via
this command packet and the Request routine. The operating system
places a command code and, optionally, a byte count and a buffer
address into the packet at the specified locations, then calls the
Request routine. The driver performs the command and returns the
status (and sometimes a byte count) in the packet.
Two Alternative Approaches
Now that the factors involved in creating interrupt-driven
communications programs have been discussed, they can be put together
into practical program packages. Doing so brings out not only general
principles but also minor details that make the difference between
success and failure of program design in this hardware-dependent and
time-critical area.
The traditional way: Going it alone
Because MS-DOS provides no generic functions suitable for
communications use, virtually all popular communications programs
provide and install their own port driver code, and then remove it
before returning to MS-DOS. This approach entails the creation of a
communications handler for each program and requires the
"uninstallation" of the handler on exit from the program that uses it.
Despite the extra requirements, most communications programs use this
method.
The alternative: Creating a communications device driver
Instead of providing temporary interface code that must be removed
from the system before returning to the command level, an installable
device driver can be built as a replacement for COMx so that every
program can have all features. However, this approach is not
compatible with existing terminal programs because it has never been a
part of MS-DOS.
Comparison of the two methods
The traditional approach has several advantages, the most obvious
being that the driver code can be fully tailored to the needs of the
program. Because only one program will ever use the driver, no general
cases need be considered.
However, if a user wants to keep communications capability available
in a terminate-and-stay-resident (TSR) module for background use and
also wants a different type of communications program running in the
foreground (not, of course, while the background task is using the
port), the background program and the foreground job must each have
its own separate driver code. And, because such code usually includes
buffer areas, the duplicated drivers represent wasted resources.
A single communications device driver that is installed when the
system powers up and that remains active until shutdown avoids wasting
resources by allowing both the background and foreground tasks to
share the driver code. Until such drivers are common, however, it is
unlikely that commercial software will be able to make use of them. In
addition, such a driver must either provide totally general
capabilities or it must include control interfaces so each user
program can dynamically alter the driver to suit its needs.
At this time, the use of a single driver is an interesting exercise
rather than a practical application, although a possible exception is
a dedicated system in which all software is either custom designed or
specially modified. In such a system, the generalized driver can
provide significant improvement in the efficiency of resource
allocation.
A Device-Driver Program Package
Despite the limitations mentioned in the preceding section, the first
of the two complete packages in this article uses the concept of a
separate device driver. The driver handles all hardware-dependent
interfacing and thus permits extreme simplicity in all other modules
of the package. This approach is presented first because it is
especially well suited for introducing the concepts of communications
programs. However, the package is not merely a tutorial device: It
includes some features that are not available in most commercial
programs.
The package itself consists of three separate programs. First is the
device driver, which becomes a part of MS-DOS via the CONFIG.SYS file.
Second is the modem engine, which is the actual terminal program. (A
functionally similar component forms the heart of every communications
program, whether it is written in assembly language or a high-level
language and regardless of the machine or operating system in use.)
Third is a separately executed support program that permits changing
such driver characteristics as word length, parity, and baud rate.
In most programs that use the traditional approach, the driver and the
support program are combined with the modem engine in a single unit
and the resulting mass of detail obscures the essential simplicity of
each part. Here, the parts are presented as separate modules to
emphasize that simplicity.
The device driver: COMDVR.ASM
The device driver is written to augment the default COM1 and COM2
devices with other devices named ASY1 and ASY2 that use the same
physical hardware but are logically separate. The driver (COMDVR.ASM)
is implemented in MASM and is shown in the listing in Figure 6-1.
Although the driver is written basically as a skeleton, it is designed
to permit extensive expansion and can be used as a general-purpose
sample of device-driver source code.
The code
──────────────────────────────────────────────────────────────────────
Figure 6-1. COMDVR.ASM.
──────────────────────────────────────────────────────────────────────
The first part of the driver source code (after the necessary MASM
housekeeping details in lines 1 through 8) is a commented-out macro
definition (lines 10 through 32). This macro is used only during
debugging and is part of a debugging technique that requires no
sophisticated hardware and no more complex debugging program than the
venerable DEBUG.COM. (Debugging techniques are discussed after the
presentation of the driver program itself.)
Definitions
The actual driver source program consists of three sets of EQU
definitions (lines 34 through 194), followed by the modular code and
data areas (lines 197 through 900). The first set of definitions
(lines 34 through 82) gives symbolic names to the permissible values
for MS-DOS device-driver control bits and the device-driver
structures.
The second set of definitions (lines 84 through 145) assigns names to
the ports and bit values that are associated with the IBM hardware--
both the 8259 PIC and the 8250 UART. The third set of definitions
(lines 147 through 194) assigns names to the control values and
structures associated with this driver.
The definition method used here is recommended for all drivers. To
move this driver from the IBM architecture to some other hardware, the
major change required to the program would be reassignment of the port
addresses and bit values in lines 84 through 145.
The control values and structures for this specific driver (defined in
the third EQU set) provide the means by which the separate support
program can modify the actions of each of the two logical drivers.
They also permit the driver to return status information to both the
support program and the using program as necessary. Only a few
features are implemented, but adequate space for expansion is
provided. The addition of a few more definitions in this area and one
or two extra procedures in the code section would do all that is
necessary to extend the driver's capabilities to such features as
automatic expansion of tab characters, case conversion, and so forth,
should they be desired.
Headers and structure tables
The driver code itself starts with a linked pair of device-driver
header blocks, one for ASY1 (lines 201 through 207) and the other for
ASY2 (lines 208 through 213). Following the headers, in lines 215
through 236, are a commented-out space reservation used by the
debugging procedure (line 215), the pointer to the command packet
(line 219), and the baud-rate conversion table (lines 221 through
236).
The conversion table is followed by structure tables containing all
data unique to ASY1 (lines 239 through 242) and ASY2 (lines 244
through 247). After the structure tables, buffer areas are reserved in
lines 249 through 254. One input buffer and one output buffer are
reserved for each port. All buffers are the same size; for simplicity,
buffer size is given a name (at line 249) so that it can be changed by
editing a single line of the program.
The size is arbitrary in this case, but if file transfers are
anticipated, the buffer should be able to hold at least 2 seconds'
worth of data (240 bytes at 1200 bps) to avoid data loss during writes
to disk. Whatever size is chosen should be a power of 2 for simple
pointer arithmetic and, if video display is intended, should not be
less than 8 bytes, to prevent losing characters when the screen
scrolls.
If additional ports are desired, more headers can be added after line
213; corresponding structure tables for each driver, plus matching
pairs of buffers, would also be necessary. The final part of this area
is the dispatch table (lines 256 through 284), which lists offsets of
all request routines in the driver; its use is discussed below.
Strategy and Request routines
With all data taken care of, the program code begins at the Strategy
routine (lines 289 through 296), which is used by both ports. This
code saves the command packet address passed to it by MS-DOS for use
by the Request routine and returns to MS-DOS.
The Request routines (lines 298 through 567) are also shared by both
ports, but the two drivers are distinguished by the address placed
into the SI register. This address points to the structure table that
is unique to each port and contains such data as the port's base
address, the associated hardware interrupt vector, the interrupt
service routine offset within the driver's segment, the base offsets
of the input and output buffers for that port, two pointers for each
of the buffers, and the input and output status conditions (including
baud rate) for the port. The only difference between one port's driver
and the other's is the data pointed to by SI; all Request routine code
is shared by both ports.
Each driver's Request routine has a unique entry point (at line 298
for ASY1 and at line 303 for ASY2) that saves the original content of
the SI register and then loads it with the address of the structure
table for that driver. The routines then join as a common stream at
line 307 (Gen_request).
This common code preserves all other registers used (lines 309 through
318), sets DS equal to CS (lines 319 and 320), retrieves the command-
packet pointer saved by the Strategy routine (line 321), uses the
pointer to get the command code (line 323), uses the code to calculate
an offset into a table of addresses (lines 324 through 326), and
performs an indexed jump (lines 322 and 327) by way of the dispatch
table (lines 256 through 284) to the routine that executes the
requested command (at line 336, 360, 389, 404, 414, 421, 441, 453,
500, or 829).
Although the device-driver specifications for MS-DOS version 3.2 list
command request codes ranging from 0 to 24, not all are used. Earlier
versions of MS-DOS permitted only 0 to 12 (versions 2.x) or 0 to 16
(versions 3.0 and 3.1) codes. In this driver, all 24 codes are
accounted for; those not implemented in this driver return a DONE and
NO ERROR status to the caller. Because the Request routine is called
only by MS-DOS itself, there is no check for invalid codes. Actually,
because the header attribute bits are not set to specify that codes 13
through 24 are valid, the 24 bytes occupied by their table entries
(lines 273 through 284) could be saved by omitting the entries. They
are included only to show how nonexistent commands can be
accommodated.
Immediately following the dispatch indexed jump, at lines 329 through
353 within the same PROC declaration, is the common code used by all
Request routines to store status information in the command packet,
restore the registers, and return to the caller. The alternative entry
points for BUSY status (line 332), NO ERROR status (line 338), or an
error code (in the AX register at entry to Exit, line 339) not only
save several bytes of redundant code but also improve readability of
the code by providing unique single labels for BUSY, NO ERROR, and
ERROR return conditions.
All of the Request routines, except for the Init code at line 829,
immediately follow the dispatching shell in lines 358 through 568.
Each is simplified to perform just one task, such as read data in or
write data out. The Read routine (lines 360 through 385) is typical:
First, the requested byte count and user's buffer address are obtained
from the command packet. Next, the pointer to the command packet is
saved with a PUSH instruction, so that the ES and BX registers can be
used for a pointer to the port's input buffer.
Before the Get_in routine that actually accesses the input buffer is
called, the input status byte is checked (line 368). If an error
condition is flagged, lines 370 through 373 clear the status flag,
flush the saved pointers from the stack, and jump to the error-return
exit from the driver. If no error exists, line 375 calls Get_in to
access the input buffer and lines 376 and 377 determine whether a byte
was obtained. If a byte is found, it is stored in the user's buffer by
line 378, and line 379 loops back to get another byte until the
requested count has been obtained or until no more bytes are
available. In practice, the count is an upper limit and the loop is
normally broken when data runs out.
No matter how it happens, control eventually reaches the Got_all
routine and lines 381 and 382, where the saved pointers to the command
packet are restored from the stack. Lines 383 and 384 adjust the count
value in the packet to reflect the actual number of bytes obtained.
Finally, line 385 jumps to the normal, no-error exit from the driver.
Buffering
Both buffers for each driver are of the type known as circular, or
ring, buffers. Effectively, such a buffer is endless; it is accessed
via pointers, and when a pointer increments past the end of the
buffer, the pointer returns to the buffer's beginning. Two pointers
are used here for each buffer, one to put data into it and one to get
data out. The get pointer always points to the next byte to be read;
the put pointer points to where the next byte will be written, just
past the last byte written to the buffer.
If both pointers point to the same byte, the buffer is empty; the next
byte to be read has not yet been written. The full-buffer condition is
more difficult to test for: The put pointer is incremented and
compared with the get pointer; if they are equal, doing a write would
force a false buffer-empty condition, so the buffer must be full.
All buffer manipulation is done via four procedures (lines 569 through
674). Put_out (lines 572 through 596) writes a byte to the driver's
output buffer or returns a buffer-full indication by setting AH to
0FFH. Get_out (lines 598 through 622) gets a byte from the output
buffer or returns 0FFH in AH to indicate that no byte is available.
Put_in (lines 624 through 648) and Get_in (lines 650 through 674) do
exactly the same as Put_out and Get_out, but for the input buffer.
These procedures are used both by the Request routines and by the
hardware interrupt service routine (ISR).
Interrupt service routines
The most complex part of this driver is the ISR (lines 676 through
806), which decides which of the four possible services for a port is
to be performed and where. Like the Request routines, the ISR provides
unique entry points for each port (line 679 for ASY1 and line 685 for
ASY2); these entry points first preserve the SI register and then load
it with the address of the port's structure table. With SI indicating
where the actions are to be performed, the two entries then merge at
line 690 into common code that first preserves all registers to be
used by the ISR (lines 690 through 698) and then tests for each of the
four possible types of service and performs each requested action.
Much of the complexity of the ISR is in the decoding of modem-status
conditions. Because the resulting information is not used by this
driver (although it could be used to prevent attempts to transmit
while off line), these ISR options can be removed so that only the
Transmit and Receive interrupts are serviced. To do this, AllInt (at
line 145) should be changed from the OR of all four bits to include
only the transmit and receive bits (03H, or 00000011B).
The transmit and receive portions of the ISR incorporate XON/XOFF flow
control (for transmitted data only) by default. This control is done
at the ISR level, rather than in the using program, to minimize the
time required to respond to an incoming XOFF signal. Presence of the
flow-control decisions adds complexity to what would otherwise be
extremely simple actions.
Flow control is enabled or disabled by setting the OutSpec word in the
structure table with the Driver Status utility (presented later) via
the IOCTL function (Interrupt 21H Function 44H). When flow control is
enabled, any XOFF character (11H) that is received halts all outgoing
data until XON (13H) is received. No XOFF or XON is retained in the
input buffer to be sent on to any program, although all patterns other
than XOFF and XON are passed through by the driver. When flow
control is disabled, the driver passes all patterns in both
directions. For binary file transfer, flow control must be disabled.
The transmit action is simple: The code merely calls the Start_output
procedure at line 750. Start_output is described in detail below.
The receive action is almost as simple as transmit, except for the
flow-control testing. First, the ISR takes the received byte from the
UART (lines 758 and 759) to avoid any chance of an overrun error. The
ISR then tests the input specifier (at line 760) to determine whether
flow control is in effect. If it is not, processing jumps directly to
line 784 to store the received byte in the input buffer with Put_in
(line 785).
If flow control is active, however, the received byte is compared with
the XOFF character (lines 762 through 765). If the byte matches,
output is disabled and the byte is ignored. If the byte is not XOFF,
it is compared with XON (lines 766 through 768). If it is not XON
either, control jumps to line 784. If the byte is XON, output is re-
enabled if it was disabled. Regardless, the XON byte itself is
ignored.
When control reaches Stuff_in at line 784, Put_in is called to store
the received byte in the input buffer. If there is no room for it, a
lost-databit is set in the input status flags (line 788); otherwise,
the receive routine is finished.
If the interrupt was a line-status action, the LSR is read (lines 776
through 779). If the input specifier so directs, the content is
converted to an IBM PC extended graphics character by setting bit 7 to
1 and the character is stored in the input buffer as if it were a
received byte. Otherwise, the Line Status interrupt merely sets the
generic BadInp error bit in the input status flags, which can be read
with the IOCTL Read function of the driver.
When all ISR action is complete, lines 794 through 806 restore machine
conditions to those existing at the time of the interrupt and return
to the interrupted procedure.
The Start_output routine
Start_output (lines 808 through 824) is a routine that, like the four
buffer procedures, is used by both the Request routines and the ISR.
Its purpose is to initiate transmission of a byte, provided that
output is not blocked by flow control, the UART Transmit Holding
Register is empty, and a byte to be transmitted exists in the output
ring buffer. This routine uses the Get_out buffer routine to access
the buffer and determine whether a byte is available. If all
conditions are met, the byte is sent to the UART holding register by
lines 819 and 820.
The Initialization Request routine
The Initialization Request routine (lines 829 through 897) is critical
to successful operation of the driver. This routine is placed last in
the package so that it can be discarded as soon as it has served its
purpose by installing the driver. It is essential to clear each
register of the 8250 by reading its contents before enabling the
interrupts and to loop through this action until the 8250 finally
shows no requests pending. The strange Clc jnc $+2 sequence that
appears repeatedly in this routine is a time delay required by high-
speed machines (6 MHz and up) so that the 8250 has time to settle
before another access is attempted; the delay does no harm on slower
machines.
Using COMDVR
The first step in using this device driver is assembling it with the
Microsoft Macro Assembler (MASM). Next, use the Microsoft Object
Linker (LINK) to create a .EXE file. Convert the .EXE file into a
binary image file with the EXE2BIN utility. Finally, include the line
DEVICE=COMDVR.SYS in the CONFIG.SYS file so that COMDVR will be
installed when the system is restarted.
Note: The number and colon at the beginning of each line in the
program listings in this article are for reference only and should not
be included in the source file.
Figure 6-2 shows the sequence of actions required, assuming that EDLIN
is used for modifying (or creating) the CONFIG.SYS file and that all
commands are issued from the root directory of the boot drive.
C>Creating the driver:
C>MASM COMDVR; <Enter>
C>LINK COMDVR; <Enter>
C>EXE2BIN COMDVR.EXE COMDVR.SYS <Enter>
Modifying CONFIG.SYS (^Z = press Ctrl-Z):
C>EDLIN CONFIG.SYS <Enter>
*#I <Enter>
*DEVICE=COMDVR.SYS <Enter>
*^Z <Enter>
*E <Enter>
Figure 6-2. Assembling, linking, and installing COMDVR.
Because the devices installed by COMDVR do not use the standard MS-DOS
device names, no conflict occurs with any program that uses
conventional port references. Such a program will not use the driver,
and no problems should result if the program is well behaved and
restores all interrupt vectors before returning to MS-DOS.
Device-driver debugging techniques
The debugging of device drivers, like debugging for any part of MS-DOS
itself, is more difficult than normal program checking because the
debugging program, DEBUG.COM or DEBUG.EXE, itself uses MS-DOS
functions to display output. When these functions are being checked,
their use by DEBUG destroys the data being examined. And because
MS-DOS always saves its return address in the same location, any call
to a function from inside the operating system usually causes a system
lockup that can be cured only by shutting the system down and powering
up again.
One way to overcome this difficulty is to purchase costly debugging
tools. An easier way is to bypass the problem: Instead of using MS-DOS
functions to track program operation, write data directly to video
RAM, as in the macro DBG (lines 10 through 32 of COMDVR.ASM).
This macro is invoked with a three-character parameter string at each
point in the program a progress report is desired. Each invocation has
its own unique three-character string so that the sequence of actions
can be read from the screen. When invoked, DBG expands into code that
saves all registers and then writes the three-character string to
video RAM. Only the top 10 lines of the screen (800 characters, or
1600 bytes) are used: The macro uses a single far pointer to the area
and treats the video RAM like a ring buffer.
The pointer, Dbgptr (line 215), is set up for use with the monochrome
adapter and points to location B000:0000H; to use a CGA or EGA (in CGA
mode), the location should be changed to B800:0000H.
Most of the frequently used Request routines, such as Read and Write,
have calls to DBG as their first lines (for example, lines 361 and
422). As shown, these calls are commented out, but for debugging, the
source file should be edited so that all the calls and the macro
itself are enabled.
With DBG active, the top 10 lines of the display are overwritten with
a continual sequence of reports, such as RR Tx , put directly into
video RAM. Because MS-DOS functions are not used, no interference with
the driver itself can occur.
Although this technique prevents normal use of the system during
debugging, it greatly simplifies the problem of knowing what is
happening in time-critical areas, such as hardware interrupt service.
In addition, all invocations of DBG in the critical areas are in
conditional code that is executed only when the driver is working as
it should.
Failure to display the pi message, for instance, indicates that the
received-data hardware interrupt is not being serviced, and absence of
go after an Ix report shows that data is not being sent out as it
should.
Of course, once debugging is complete, the calls to DBG should be
deleted or commented out. Such calls are usually edited out of the
source code before release. In this case, they remain to demonstrate
the technique and, most particularly, to show placement of the calls
to provide maximum information with minimal clutter on the screen.
A simple modem engine
The second part of this package is the modem engine itself
(ENGINE.ASM), shown in the listing in Figure 6-3. The main loop of
this program consists of only a dozen lines of code (lines 9 through
20). Of these, five (lines 9 through 13) are devoted to establishing
initial contact between the program and the serial-port driver and two
(lines 19 and 20) are for returning to command level at the program's
end.
Thus, only five lines of code (lines 14 through 18) actually carry out
the bulk of the program as far as the main loop is concerned. Four of
these lines are calls to subroutines that get and put data from and to
the console and the serial port; the fifth is the JMP that closes the
loop. This structure underscores the fact that a basic modem engine is
simply a data-transfer loop.
──────────────────────────────────────────────────────────────────────
Figure 6-3. ENGINE.ASM.
──────────────────────────────────────────────────────────────────────
Because the details of timing and data conversion are handled by the
driver code, each of the four subroutines is--to show just how simple
the whole process is--essentially a buffered interface to the MS-DOS
Read File or Device or Write File or Device routine.
For example, the getmdm procedure (lines 22 through 31) asks MS-DOS to
read a maximum of 256 bytes from the serial device and then stores the
number actually read in a word named mdlen. The driver returns
immediately, without waiting for data, so the normal number of bytes
returned is either 0 or 1. If screen scrolling causes the loop to be
delayed, the count might be higher, but it should never exceed about a
dozen characters.
When called, the putcrt procedure (lines 63 through 72) checks the
value in mdlen. If the value is zero, putcrt does nothing; otherwise,
it asks MS-DOS to write that number of bytes from mbufr (where getmdm
put them) to the display, and then it returns.
Similarly, getkbd gets keystrokes from the keyboard, stores them in
kbufr, and posts a count in kblen; putmdm checks kblen and, if the
count is not zero, sends the required number of bytes from kbufr to
the serial device.
Note that getkbd does not use the Read File or Device function,
because that would wait for a keystroke and the loop must never wait
for reception. Instead, it uses the MS-DOS functions that test
keyboard status (0BH) and read a key without echo (07H). In addition,
special treatment is given to the Enter key (lines 45 through 48): A
linefeed is inserted in kbufr immediately behind Enter and kblen is
set to 2.
A Ctrl-C keystroke ends program operation; it is detected in getkbd
(line 41) and causes immediate transfer to the quit label (line 19) at
the end of the main loop. Because ENGINE uses only permanently
resident routines, there is no need for any uninstallation before
returning to the MS-DOS command prompt.
ENGINE.ASM is written to be used as a .COM file. Assemble and link it
the same as COMDVR.SYS (Figure 6-2) but use the extension COM instead
of SYS; no change to CONFIG.SYS is needed.
The driver-status utility: CDVUTL.C
The driver-status utility program CDVUTL.C, presented in Figure 6-4,
permits either of the two drivers (ASY1 and ASY2) to be reconfigured
after being installed, to suit different needs. After one of the
drivers has been specified (port 1 or port 2), the baud rate, word
length, parity, and number of stop bits can be changed; each change is
made independently, with no effect on any of the other
characteristics. Additionally, flow control can be switched between
two types of hardware handshaking--the software XON/XOFF control or
disabled--and error reporting can be switched between character-
oriented and message-oriented operation.
──────────────────────────────────────────────────────────────────────
Figure 6-4. CDVUTL.C
──────────────────────────────────────────────────────────────────────
Although CDVUTL appears complicated, most of the complexity is
concentrated in the routines that map driver bit settings into on-
screen display text. Each such mapping requires several lines of
source code to generate only a few words of the display report. Table
6-10 summarizes the functions found in this program.
Table 6-10. CDVUTL Program Functions.
╓┌─────────────────────┌──────────┌──────────────────────────────────────────╖
Lines Name Description
──────────────────────────────────────────────────────────────────
42-45 main() Conventional entry point.
47-150 disp() Main dispatching loop.
152-158 center() Centers text on CRT.
160-166 iocwr() Writes control string to driver with IOCTL
Write.
168-170 onoff() Returns pointer to ON or OFF.
172-233 report() Reads driver status and reports it on
display.
The long list of #define operations at the start of the listing (lines
11 through 33) helps make the bitmapping comprehensible by assigning a
symbolic name to each significant bit in the four UART registers.
The main() procedure of CDVUTL displays a banner line and then calls
the dispatcher routine, disp(), to start operation. CDVUTL makes no
use of either command-line parameters or the environment, so the usual
argument declarations are omitted.
Upon entry to disp(), the first action is to establish the default
driver as ASY1 by setting u = 1 and opening ASY1 (line 50); the
program then enters an apparent infinite loop (lines 51 through 149).
With each repetition, the loop first prompts for a command (line 52)
and then gets the next keystroke and uses it to control a huge
switch() statement (lines 53 through 145). If no case matches the key
pressed, the switch() statement does nothing; the program simply
displays a report of all current conditions at the selected driver
(lines 146 through 148) and then closes the loop back to issue a new
prompt and get another keystroke.
However, if the key pressed matches one of the cases in the switch()
statement, the corresponding command is executed. The digits 1 (line
55) and 2 (line 61) select the driver to be affected. The ? key (line
105) causes the list of valid command keys to be displayed. The q key
(line 142) causes the program to terminate by calling exit( 0 ) and is
the only exit from the infinite loop. The other valid keys all change
one or more bits in the IOCTL control string to modify corresponding
attributes of the driver and then send the string to the driver by
using the MS-DOS IOCTL Write function (Interrupt 21H Function 44H
Subfunction 03H) via function iocwr() (lines 160 through 166).
After the command is executed (except for the q command, which
terminates operation of CDVUTL and returns to MS-DOS command level,
and the ? command, which displays the command list), the report()
function (lines 172 through 233) is called (at line 148) to display
all of the driver's attributes, including those just changed. This
function issues an IOCTL Read command (Interrupt 21H Function 44H
Subfunction 02H, in lines 174 through 178) to get new status
information into the control string and then uses a sequence of bit
filtering (lines 179 through 232) to translate the obtained status
information into words for display.
The special console I/O routines provided in Microsoft C libraries
have been used extensively in this routine. Other compilers may
require changes in the names of such library routines as getch or
dosint as well as in the names of #include files (lines 6 through 9).
Each of the actual command sequences changes only a few bits in one of
the 10 bytes of the command string and then writes the string to the
driver. A full-featured communications program might make several
changes at one time--for example, switching from 7-bit, even parity,
XON/XOFF flow control to 8-bit, no parity, without flow control to
prevent losing any bytes with values of 11H or 13H while performing a
binary file transfer with error-correcting protocol. In such a case,
the program could make all required changes to the control string
before issuing a single IOCTL Write to put them into effect.
The Traditional Approach
Because the necessary device driver has never been a part of MS-DOS,
most communications programs are written to provide and install their
own port driver code and remove it before returning to MS-DOS. The
second sample program package in this article illustrates this
approach. Although the major part of the package is written in
Microsoft C, three assembly-language modules are required to provide
the hardware interrupt service routines, the exception handler, and
faster video display. They are discussed first.
The hardware ISR module
The first module is a handler to service UART interrupts. Code for
this handler, including routines to install it at entry and remove it
on exit, appears in CH1.ASM, shown in Figure 6-5.
──────────────────────────────────────────────────────────────────────
Figure 6-5. CH1.ASM
──────────────────────────────────────────────────────────────────────
The routines in CH1 are set up to work only with port COM2; to use
them with COM1, the three symbolic constants BPORT (base address),
GETIV, and PUTIV must be changed to match the COM1 values. Also, as
presented, this code is for use with the Microsoft C small memory
model only; for use with other memory models, the C compiler manuals
should be consulted for making the necessary changes. See also
PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS:
Structure of an Application Program.
The parts of CH1 are listed in Table 6-11, as they occur in the
listing. The leading underscore that is part of the name for each of
the six functions is supplied by the C compiler; within the C program
that calls the function, the underscore is omitted.
Table 6-11. CH1 Module Functions.
╓┌────────────────┌──────────────┌───────────────────────────────────────────╖
Lines Name Description
──────────────────────────────────────────────────────────────────
1-26 Administrative details.
27-46 Data areas.
48-84 _set_mdm Initializes UART as specified by parameter
passed from C.
86-114 _wrtmdm Outputs character to UART.
87 _Send_Byte Entry point for use if flow control is
added to system.
116-140 _rdmdm Gets character from buffer where ISR put
it, or signals that no character available.
142-155 w_tmr Wait timer; internal routine used to
prevent infinite wait in case of problems.
157-182 rts_m Hardware ISR; installed by _i_m and removed
by _u_m.
184-240 _i_m Installs ISR, saving old interrupt vector.
242-265 _u_m Uninstalls ISR, restoring saved interrupt
vector.
For simplest operation, the ISR used in this example (unlike the
device driver) services only the received-data interrupt; the other
three types of IRQ are disabled at the UART. Each time a byte is
received by the UART, the ISR puts it into the buffer. The _rdmdm
code, when called by the C program, gets a byte from the buffer if one
is available. If not, _rdmdm returns the C EOF code (-1) to indicate
that no byte can be obtained.
To send a byte, the C program can call either _Send_Byte or _wrtmdm;
in the package as shown, these are alternative names for the same
routine. In the more complex program from which this package was
adapted, _Send_Byte is called when flow control is desired and the
flow-control routine calls _wrtmdm. To implement flow control, line 87
should be deleted from CH1.ASM and a control function named
Send_Byte() should be added to the main C program. Flow-control tests
must occur in Send_Byte(); _wrtmdm performs the actual port
interfacing.
To set the modem baud rate, word length, and parity, _set_mdm is
called from the C program, with a setup parameter passed as an
argument. The format of this parameter is shown in Table 6-12 and is
identical to the IBM BIOS Interrupt 14H Function 00H (Initialization).
Table 6-12. set_mdm() Parameter Coding.
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Binary Meaning
──────────────────────────────────────────────────────────────────
000xxxxx Set to 110 bps
001xxxxx Set to 150 bps
010xxxxx Set to 300 bps
011xxxxx Set to 600 bps
100xxxxx Set to 1200 bps
101xxxxx Set to 2400 bps
110xxxxx Set to 4800 bps
111xxxxx Set to 9600 bps
xxxx0xxx No parity
xxx01xxx ODD Parity
xxx11xxx EVEN Parity
xxxxx0xx 1 stop bit
xxxxx1xx 2 stop bits (1.5 if WL = 5)
xxxxxx00 Word length = 5
xxxxxx01 Word length = 6
xxxxxx10 Word length = 7
xxxxxx11 Word length = 8
The CH1 code provides a 512-byte ring buffer for incoming data; the
buffer size should be adequate for reception at speeds up to 2400 bps
without loss of data during scrolling.
The exception-handler module
For the ISR handler of CH1 to be usable, an exception handler is
needed to prevent return of control to MS-DOS before _u_m restores the
ISR vector to its original value. If a program using this code returns
to MS-DOS without calling _u_m, the system is virtually certain to
crash when line noise causes a received-data interrupt and the ISR
code is no longer in memory.
A replacement exception handler (CH1A.ASM), including routines for
installation, access, and removal, is shown in Figure 6-6. Like the
ISR, this module is designed to work with Microsoft C (again, the
small memory model only).
Note: This module does not provide for fatal disk errors; if one
occurs, immediate restarting is necessary. See PROGRAMMING IN THE MS-
DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Exception Handlers.
──────────────────────────────────────────────────────────────────────
Figure 6-6. CH1A.ASM.
──────────────────────────────────────────────────────────────────────
The three functions in CH1A are _set_int, which saves the old vector
value for Interrupt 1BH (ROM BIOS Control-Break) and then resets both
that vector and the one for Interrupt 23H (Control-C Handler Address)
to internal ISR code; _rst_int, which restores the original value for
the Interrupt 1BH vector; and _broke, which returns the present value
of an internal flag (and always clears the flag, just in case it had
been set). The internal flag is set to a nonzero value in response to
either of the revectored interrupts and is tested from the main C
program via the _broke function.
The video display module
The final assembly-language module (CH2.ASM) used by the second
package is shown in Figure 6-7. This module provides convenient screen
clearing and cursor positioning via direct calls to the IBM BIOS, but
this can be eliminated with minor rewriting of the routines that call
its functions. In the original, more complex program (DT115.EXE,
available from DL6 in the CLMFORUM of CompuServe) from which CTERM was
derived, this module provided windowing capability in addition to
improved display speed.
──────────────────────────────────────────────────────────────────────
Figure 6-7. CH2.ASM.
──────────────────────────────────────────────────────────────────────
The sample smarter terminal emulator: CTERM.C
Given the interrupt handler (CH1), exception handler (CH1A), and video
handler (CH2), a simple terminal emulation program (CTERM.C) can be
presented. The major functions of the program are written in Microsoft
C; the listing is shown in Figure 6-8.
──────────────────────────────────────────────────────────────────────
Figure 6-8. CTERM.C.
──────────────────────────────────────────────────────────────────────
CTERM features file-capture capabilities, a simple yet effective
script language, and a number of stub (that is, incompletely
implemented) actions, such as emulation of the VT52 and VT100 series
terminals, indicating various directions in which it can be developed.
The names of a script file and a capture file can be passed to CTERM
in the command line. If no filename extensions are included, the
default for the script file is .SCR and that for the capture file is
.CAP. If extensions are given, they override the default values. The
capture feature can be invoked only if a filename is supplied in the
command line, but a script file can be called at any time via the Esc
command sequence, and one script file can call for another with the
same feature.
The functions included in CTERM.C are listed and summarized in Table
6-13.
Table 6-13. CTERM.C Functions.
╓┌─────────────────────┌──────────────────────┌──────────────────────────────╖
Lines Name Description
──────────────────────────────────────────────────────────────────
1-5 Program documentation.
7-11 Include files.
12-20 Definitions.
22-43 Global data areas.
45 External prototype
declaration.
47-49 Wants_To_Abort() Checks for Ctrl-Break or Ctrl-
C being pressed.
52-165 main() Main program loop; includes
modem engine and sequential
state machine to decode
remote commands.
167-297 docmd() Gets, interprets, and performs
local (console or script)
command.
299-304 kbd_wait() Waits for input from console
or script file.
306-334 kb_file() Gets keystroke from console or
script; returns EOF if no
character available.
336-362 esc() Translates script escape
sequence.
364-370 getfil() Gets name of script file and
opens the file.
372-382 getnam() Gets string from console or
script into designated
buffer.
384-393 addext() Checks buffer for extension;
adds one if none given.
395-398 put_cap() Writes character to capture
file if capture in effect.
400-406 cap_flush() Closes capture file and
terminates capture mode if
capture in effect.
408-411 Timer data locations.
413-425 getmr() Returns time since midnight,
in milliseconds.
427-432 Delay() Sleeps n milliseconds.
434-436 Start_Timer() Sets timer for n seconds.
438-440 Timer_Expired() Checks timer versus clock.
442-445 Set_Vid() Initializes video data.
447-452 locate() Positions cursor on display.
454-456 deol() Deletes to end of line.
458-468 deos() Deletes to end of screen.
470-472 cls() Clears screen.
474-478 cursor() Turns cursor on or off.
480-485 revvid() Toggles inverse/normal video
display attributes.
487-492 putchx() Writes char to display using
putch() (Microsoft C
library).
494-500 Read_Keyboard() Gets keystroke from keyboard.
502-504 Modem data areas.
506-512 Init_Comm() Installs ISR and so forth and
initializes modem.
514-515 Baud-rate definitions.
517-529 Set_Baud() Changes bps rate of UART.
531-537 Parity, WL definitions.
539-557 Set_Parity() Establishes UART parity mode.
559-562 Write_Modem() Sends character to UART.
564-566 Read_Modem() Gets character from ISR's
buffer.
568-570 Term_Comm() Uninstalls ISR and so forth
and restores original
vectors.
For communication with the console, CTERM uses the special Microsoft C
library functions defined by CONIO.H, augmented with the functions in
the CH2.ASM handler. Much of the code may require editing if used with
other compilers. CTERM also uses the function prototype file CTERM.H,
listed in Figure 6-9, to optimize function calling within the program.
──────────────────────────────────────────────────────────────────────
Figure 6-9. CTERM.H.
──────────────────────────────────────────────────────────────────────
Program execution begins at the entry to main(), line 52. CTERM first
checks (lines 56 through 59) whether any filenames were passed in the
command line; if they were, CTERM opens the corresponding files. Next,
the program installs the exception handler (line 60), initializes the
video handler (line 61), clears the display (line 62), and announces
its presence (lines 63 and 64). The serial driver is installed and
initialized to 1200 bps and no parity (lines 65 through 67), and the
program enters its main modem-engine loop (lines 68 through 159).
This loop is functionally the same as that used in ENGINE, but it has
been extended to detect an Esc from the keyboard as signalling the
start of a local command sequence (lines 70 through 73) and to include
a state-machine technique (lines 80 through 153) to recognize incoming
escape sequences, such as the VT52 or VT100 codes. To specify a local
command from the keyboard, press the Escape (Esc) key, then the first
letter of the local command desired. After the local command has been
selected, press any key (such as Enter or the spacebar) to continue.
To get a listing of all the commands available, press Esc-H.
The kb_file() routine of CTERM (called in the main loop at line 69)
can get its input from either a script file or the keyboard. If a
script file is open (lines 308 through 330), it is used until EOF is
reached or until the operator presses Ctrl-C to stop script-file
input. Otherwise, input is taken from the keyboard (lines 331 and
332). If a script file is in use, its input is echoed to the display
(lines 325 through 329) if the V command has been given.
To permit the Esc character itself to be placed in script files, the
backslash (\) character serves as a secondary escape signal. When a
backslash is detected (lines 323 and 324) in the input stream, the
next character input is translated according to the following rules:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Character Interpretation
──────────────────────────────────────────────────────────────────
E or e Translates to Esc.
N or n Translates to Linefeed.
R or r Translates to Enter (CR).
T or t Translates to Tab.
^ Causes the next character input to be converted into
a control character.
Any other character, including another \, is not translated at all.
When the Esc character is detected from either the console or a script
file, the docmd() function (lines 167 through 297) is called to prompt
for and decode the next input character as a command and to perform
appropriate actions. Valid command characters, and the actions they
invoke, are as follows:
╓┌────────────────────────────┌──────────────────────────────────────────────╖
Command Character Action
──────────────────────────────────────────────────────────────────
D Delay 0-9 seconds, then proceed. Must be
followed by a decimal digit that indicates
how long to delay.
E Set EVEN parity.
F Set (fast) 1200 baud.
H Display list of valid commands.
N Set no parity.
O Set ODD parity.
Q Quit; return to MS-DOS command prompt.
R Reset modem.
S Set (slow) 300 baud.
U Use script file (CTERM prompts for filename).
V Verify file input. Echoes each script-file
byte.
W Wait for character; the next input character
is the one that must be matched.
Any other character input after an Esc and the resulting Command
prompt generates the message Don't know X (where X stands for the
actual input character) followed by the prompt Use `H' command for
Help.
If input is taken from a script and the V flag is off, docmd()
performs its task quietly, with no output to the screen. If input is
received from the console, however, the command letter, followed by a
descriptive phrase, is echoed to the screen. Input, detection, and
execution of the local commands are accomplished much as in CDVUTL, by
way of a large switch() statement (lines 178 through 290).
Although the listed commands are only a subset of the features
available in CDVUTL for the device-driver program, they are more than
adequate for creating useful scripts. The predecessor of CTERM
(DT115.EXE), which included the CompuServe B-Protocol file-transfer
capability but had no additional commands, has been in use since early
1986 to handle automatic uploading and downloading of files from the
CompuServe Information Service by means of script files. In
conjunction with an auto-dialing modem, DT115.EXE handles the entire
transaction, from login through logout, without human intervention.
All the bits and pieces of CTERM are put together by assembling the
three handlers with MASM, compiling CTERM with Microsoft C, and
linking all four object modules into an executable file. Figure 6-10
shows the complete sequence and also the three ways of using the
finished program.
Compiling:
MASM CH1;
MASM CH1A;
MASM CH2;
MSC CTERM;
Linking:
LINK CTERM+CH1+CH1A+CH2;
Use:
(no files)
CTERM
or
(script only)
CTERMscriptfile
or
CTERMscriptfile capturefile
Figure 6-10. Putting CTERM together and using it.
Jim Kyle
Chip Rabinowitz
Article 7: File and Record Management
The core of most application programs is the reading, processing, and
writing of data stored on magnetic disks. This data is organized into
files, which are identified by name; the files, in turn, can be
organized by grouping them into directories. Operating systems provide
application programs with services that allow them to manipulate these
files and directories without regard to the hardware characteristics
of the disk device. Thus, applications can concern themselves solely
with the form and content of the data, leaving the details of the
data's location on the disk and of its retrieval to the operating
system.
The disk storage services provided by an operating system can be
categorized into file functions and record functions. The file
functions operate on entire files as named entities, whereas the
record functions provide access to the data contained within files.
(In some systems, an additional class of directory functions allows
applications to deal with collections of files as well.) This article
discusses the MS-DOS function calls that allow an application program
to create, open, close, rename, and delete disk files; read data from
and write data to disk files; and inspect or change the information
(such as attributes and date and time stamps) associated with disk
filenames in disk directories. See also PROGRAMMING IN THE MS-DOS
ENVIRONMENT: STRUCTURE OF MS-DOS: MS-DOS Storage Devices; PROGRAMMING
FOR MS-DOS: Disk Directories and Volume Labels.
Historical Perspective
Current versions of MS-DOS provide two overlapping sets of file and
record management services to support application programs: the handle
functions and the file control block (FCB) functions. Both sets are
available through Interrupt 21H (Table 7-1). See SYSTEM CALLS:
INTERRUPT 21H. The reasons for this surprising duplication are
strictly historical.
The earliest versions of MS-DOS used FCBs for all file and record
access because CP/M, which was the dominant operating system on 8-bit
microcomputers, used FCBs. Microsoft chose to maintain compatibility
with CP/M to aid programmers in converting the many existing CP/M
application programs to the 16-bit MS-DOS environment; consequently,
MS-DOS versions 1.x included a set of FCB functions that were a
functional superset of those present in CP/M. As personal computers
evolved, however, the FCB access method did not lend itself well to
the demands of larger, faster disk drives.
Accordingly, MS-DOS version 2.0 introduced the handle functions to
provide a file and record access method similar to that found in
UNIX/XENIX. These functions are easier to use and more flexible than
their FCB counterparts and fully support a hierarchical (tree-like)
directory structure. The handle functions also allow character
devices, such as the console or printer, to be treated for some
purposes as though they were files. MS-DOS version 3.0 introduced
additional handle functions, enhanced some of the existing handle
functions for use in network environments, and provided improved error
reporting for all functions.
The handle functions, which offer far more capability and performance
than the FCB functions, should be used for all new applications.
Therefore, they are discussed first in this article.
Table 7-1. Interrupt 21H Function Calls for File and Record
Management.
╓┌───────────────────────────────────────┌────────────┌──────────────────────╖
Handle FCB
Operation Function Function
──────────────────────────────────────────────────────────────────
Create file. 3CH 16H
Create new file. 5BH
Create temporary file. 5AH
Open file. 3DH 0FH
Close file. 3EH 10H
Delete file. 41H 13H
Rename file. 56H 17H
Perform sequential read. 3FH 14H
Perform sequential write. 40H 15H
Perform random record read. 3FH 21H
Perform random record write. 40H 22H
Perform random block read. 27H
Perform random block write. 28H
Set disk transfer area address. 1AH
Get disk transfer area address. 2FH
Parse filename. 29H
Position read/write pointer. 42H
Set random record number. 24H
Get file size. 42H 23H
Get/Set file attributes. 43H
Get/Set date and time stamp. 57H
Duplicate file handle. 45H
Redirect file handle. 46H
Using the Handle Functions
The initial link between an application program and the data stored on
disk is the name of a disk file in the form
drive:path\filename.ext
where drive designates the disk on which the file resides, path
specifies the directory on that disk in which the file is located, and
filename.ext identifies the file itself. If drive and/or path is
omitted, MS-DOS assumes the default disk drive and current directory.
Examples of acceptable pathnames include
C:\PAYROLL\TAXES.DAT
LETTERS\MEMO.TXT
BUDGET.DAT
Pathnames can be hard-coded into a program as part of its data. More
commonly, however, they are entered by the user at the keyboard,
either as a command-line parameter or in response to a prompt from the
program. If the pathname is provided as a commandline parameter, the
application program must extract it from the other information in the
command line. Therefore, to allow a program to distinguish between
pathnames and other parameters when the two are combined in a command
line, the other parameters, such as switches, usually begin with a
slash (/) or dash (-) character.
All handle functions that use a pathname require the name to be in the
form of an ASCIIZ string--that is, the name must be terminated by a
null (zero) byte. If the pathname is hard-coded into a program, the
null byte must be part of the ASCIIZ string. If the pathname is
obtained from keyboard input or from a command-line parameter, the
null byte must be appended by the program. See Opening an Existing
File, below.
To use a disk file, a program opens or creates the file by calling the
appropriate MS-DOS function with the ASCIIZ pathname. MS-DOS checks
the pathname for invalid characters and, if the open or create
operation is successful, returns a 16-bit handle, or identification
code, for the file. The program uses this handle for subsequent
operations on the file, such as record reads and writes.
The total number of handles for simultaneously open files is limited
in two ways. First, the per-process limit is 20 file handles. The
process's first five handles are always assigned to the standard
devices, which default to the CON, AUX, and PRN character devices:
╓┌──────────────┌───────────────────┌────────────────────────────────────────╖
Handle Service Default
──────────────────────────────────────────────────────────────────
0 Standard input Keyboard (CON)
1 Standard output Video display (CON)
2 Standard error Video display (CON)
3 Standard auxiliary First communications port (AUX)
4 Standard list First parallel printer port (PRN)
Ordinarily, then, a process has only 15 handles left from its initial
allotment of 20; however, when necessary, the 5 standard device
handles can be redirected to other files and devices or closed and
reused.
In addition to the per-process limit of 20 file handles, there is a
system-wide limit. MS-DOS maintains an internal table that keeps track
of all the files and devices opened with file handles for all
currently active processes. The table contains such information as the
current file pointer for read and write operations and the time and
date of the last write to the file. The size of this table, which is
set when MS-DOS is initially loaded into memory, determines the
system-wide limit on how many files and devices can be open
simultaneously. The default limit is 8 files and devices; thus, this
system-wide limit usually overrides the per-process limit.
To increase the size of MS-DOS's internal handle table, the statement
FILES=nnn can be included in the CONFIG.SYS file. (CONFIG.SYS settings
take effect the next time the system is turned on or restarted.) The
maximum value for FILES is 99 in MS-DOS versions 2.x and 255 in
versions 3.x. See USER COMMANDS: CONFIG.SYS: FILES.
Error handling and the handle functions
When a handle-based file function succeeds, MS-DOS returns to the
calling program with the carry flag clear. If a handle function fails,
MS-DOS sets the carry flag and returns an error code in the AX
register. The program should check the carry flag after each operation
and take whatever action is appropriate when an error is encountered.
Table 7-2 lists the most frequently encountered error codes for file
and record I/O (exclusive of network operations).
Table 7-2. Frequently Encountered Error Diagnostics for File and
Record Management.
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Code Error
──────────────────────────────────────────────────────────────────
02 File not found
03 Path not found
04 Too many open files (no handles left)
05 Access denied
06 Invalid handle
11 Invalid format
12 Invalid access code
13 Invalid data
15 Invalid disk drive letter
17 Not same device
18 No more files
The error codes used by MS-DOS in versions 3.0 and later are a
superset of the MS-DOS version 2.0 error codes. See APPENDIX B:
CRITICAL ERROR CODES; APPENDIX C: EXTENDED ERROR CODES. Most MS-DOS
version 3 error diagnostics relate to network operations, which
provide the program with a greater chance for error than does a
single-user system.
Programs that are to run in a network environment need to anticipate
network problems. For example, the server can go down while the
program is using shared files.
Under MS-DOS versions 3.x, a program can also use Interrupt 21H
Function 59H (Get Extended Error Information) to obtain more details
about the cause of an error after a failed handle function. The
information returned by Function 59H includes the type of device that
caused the error and a recommended recovery action.
Warning: Many file and record I/O operations discussed in this article
can result in or be affected by a hardware (critical) error. Such
errors can be intercepted by the program if it contains a custom
critical error exception handler (Interrupt 24H).See PROGRAMMING IN
THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Exception Handlers.
Creating a file
MS-DOS provides three Interrupt 21H handle functions for creating
files:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Function Name
──────────────────────────────────────────────────────────────────
3CH Create File with Handle (versions 2.0 and later)
5AH Create Temporary File (versions 3.0 and later)
5BH Create New File (versions 3.0 and later)
Each function is called with the segment and offset of an ASCIIZ
pathname in the DS:DX registers and the attribute to be assigned to
the new file in the CX register. The possible attribute values are
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Code Attribute
──────────────────────────────────────────────────────────────────
00H Normal file
01H Read-only file
02H Hidden file
04H System file
Files with more than one attribute can be created by combining the
values listed above. For example, to create a file that has both the
read-only and system attributes, the value 05H is placed in the CX
register.
If the file is successfully created, MS-DOS returns a file handle in
AX that must be used for subsequent access to the new file and sets
the file read/write pointer to the beginning of the file; if the file
is not created, MS-DOS sets the carry flag (CF) and returns an error
code in AX.
Function 3CH is the only file-creation function available under MS-DOS
versions 2.x. It must be used with caution, however, because if a file
with the specified name already exists, Function 3CH will open it and
truncate it to zero length, eradicating the previous contents of the
file. This complication can be avoided by testing for the previous
existence of the file with an open operation before issuing the create
call.
Under MS-DOS versions 3.0 and later, Function 5BH is the preferred
function in most cases because it will fail if a file with the same
name already exists. In networking environments, this function can be
used to implement semaphores, allowing the synchronization of programs
running in different network nodes.
Function 5AH is used to create a temporary work file that is
guaranteed to have a unique name. This capability is important in
networking environments, where several copies of the same program,
running in different nodes, may be accessing the same logical disk
volume on a server. The function is passed the address of a buffer
that can contain a drive and/or path specifying the location for the
created file. MS-DOS generates a name for the created file that is a
sequence of alphanumeric characters derived from the current time and
returns the entire ASCIIZ pathname to the program in the same buffer,
along with the file's handle in AX. The program must save the filename
so that it can delete the file later, if necessary; the file created
with Function 5AH is not destroyed when the program exits.
Example: Create a file named MEMO.TXT in the \LETTERS directory on
drive C using Function 3CH. Any existing file with the same name is
truncated to zero length and opened.
fname db 'C:\LETTERS\MEMO.TXT',0
fhandle dw ?
.
.
.
mov dx,seg fname ; DS:DX = address of
mov ds,dx ; pathname for file
mov dx,offset fname
xor cx,cx ; CX = normal attribute
mov ah,3ch ; Function 3CH = create
int 21h ; transfer to MS-DOS
jc error ; jump if create failed
mov fhandle,ax ; else save file handle
.
.
.
Example: Create a temporary file using Function 5AH and place it in
the \TEMP directory on drive C. MS-DOS appends the filename it
generates to the original path in the buffer named fname. The
resulting file specification can be used later to delete the file.
fname db 'C:\TEMP\' ; generated ASCIIZ filename
db 13 dup (0) ; is appended by MS-DOS
fhandle dw ?
.
.
.
mov dx,seg fname ; DS:DX = address of
mov ds,dx ; path for temporary file
mov dx,offset fname
xor cx,cx ; CX = normal attribute
mov ah,5ah ; Function 5AH = create
; temporary file
int 21h ; transfer to MS-DOS
jc error ; jump if create failed
mov fhandle,ax ; else save file handle
.
.
.
Opening an existing file
Function 3DH (Open File with Handle) opens an existing normal, system,
or hidden file in the current or specified directory. When calling
Function 3DH, the program supplies a pointer to the ASCIIZ pathname in
the DS:DX registers and a 1-byte access code in the AL register. This
access code includes the read/write permissions, the file-sharing
mode, and an inheritance flag. The bits of the access code are
assigned as follows:
╓┌──────────────┌────────────────────────────────────────────────────────────╖
Bit(s) Description
──────────────────────────────────────────────────────────────────
0-2 Read/write permissions (versions 2.0 and later)
3 Reserved
4-6 File-sharing mode (versions 3.0 and later)
7 Inheritance flag (versions 3.0 and later)
The read/write permissions field of the access code specifies how the
file will be used and can take the following values:
╓┌──────────────┌────────────────────────────────────────────────────────────╖
Bits 0-2 Description
──────────────────────────────────────────────────────────────────
000 Read permission desired
001 Write permission desired
010 Read and write permission desired
For the open to succeed, the permissions field must be compatible with
the file's attribute byte in the disk directory. For example, if the
program attempts to open an existing file that has the read-only
attribute when the permissions field of the access code byte is set to
write or read/write, the open function will fail and an error code
will be returned in AX.
The sharing-mode field of the access code byte is important in a
networking environment. It determines whether other programs will also
be allowed to open the file and, if so, what operations they will be
allowed to perform. Following are the possible values of the file-
sharing mode field:
╓┌──────────────┌────────────────────────────────────────────────────────────╖
Bits 4-6 Description
──────────────────────────────────────────────────────────────────
000 Compatibility mode. Other programs can open the file and
perform read or write operations as long as no process
specifies any sharing mode other than compatibility mode.
001 Deny all. Other programs cannot open the file.
010 Deny write. Other programs cannot open the file in
compatibility mode or with write permission.
011 Deny read. Other programs cannot open the file in
compatibility mode or with read permission.
100 Deny none. Other programs can open the file and perform both
read and write operations but cannot open the file in
compatibility mode.
When file-sharing support is active (that is, SHARE.EXE has previously
been loaded), the result of any open operation depends on both the
contents of the permissions and file-sharing fields of the access code
byte and the permissions and file-sharing requested by other processes
that have already successfully opened the file.
The inheritance bit of the access code byte controls whether a child
process will inherit that file handle. If the inheritance bit is
cleared, the child can use the inherited handle to access the file
without performing its own open operation. Subsequent operations
performed by the child process on inherited file handles also affect
the file pointer associated with the parent's file handle. If the
inheritance bit is set, the child process does not inherit the handle.
If the file is opened successfully, MS-DOS returns its handle in AX
and sets the file read/write pointer to the beginning of the file; if
the file is not opened, MS-DOS sets the carry flag and returns an
error code in AX.
Example: Copy the first parameter from the program's command tail in
the program segment prefix (PSP) into the array fname and append a
null character to form an ASCIIZ filename. Attempt to open the file
with compatibility sharing mode and read/write access. If the file
does not already exist, create it and assign it a normal attribute.
cmdtail equ 80h ; PSP offset of command tail
fname db 64 dup (?)
fhandle dw ?
.
.
.
; assume that DS already
; contains segment of PSP
; prepare to copy filename...
mov si,cmdtail ; DS:SI = command tail
mov di,seg fname ; ES:DI = buffer to receive
mov es,di ; filename from command tail
mov di,offset fname
cld ; safety first!
lodsb ; check length of command tail
or al,al
jz error ; jump, command tail empty
label1: ; scan off leading spaces
lodsb ; get next character
cmp al,20h ; is it a space?
jz label1 ; yes, skip it
label2:
cmp al,0dh ; look for terminator
jz label3 ; quit if return found
cmp al,20h
jz label3 ; quit if space found
stosb ; else copy this character
lodsb ; get next character
jmp label2
label3:
xor al,al ; store final NULL to
stosb ; create ASCIIZ string
; now open the file...
mov dx,seg fname ; DS:DX = address of
mov ds,dx ; pathname for file
mov dx,offset fname
mov ax,3d02h ; Function 3DH = open r/w
int 21h ; transfer to MS-DOS
jnc label4 ; jump if file found
cmp ax,2 ; error 2 = file not found
jnz error ; jump if other error
; else make the file...
xor cx,cx ; CX = normal attribute
mov ah,3ch ; Function 3CH = create
int 21h ; transfer to MS-DOS
jc error ; jump if create failed
label4:
mov fhandle,ax ; save handle for file
.
.
.
Closing a file
Function 3EH (Close File) closes a file created or opened with a file
handle function. The program must place the handle of the file to be
closed in BX. If a write operation was performed on the file, MS-DOS
updates the date, time, and size in the file's directory entry.
Closing the file also flushes the internal MS-DOS buffers associated
with the file to disk and causes the disk's file allocation table
(FAT) to be updated if necessary.
Good programming practice dictates that a program close files as soon
as it finishes using them. This practice is particularly important
when the file size has been changed, to ensure that data will not be
lost if the system crashes or is turned off unexpectedly by the user.
A method of updating the FAT without closing the file is outlined
below under Duplicating and Redirecting Handles.
Reading and writing with handles
Function 3FH (Read File or Device) enables a program to read data from
a file or device that has been opened with a handle. Before calling
Function 3FH, the program must set the DS:DX registers to point to the
beginning of a data buffer large enough to hold the requested
transfer, put the file handle in BX, and put the number of bytes to be
read in CX. The length requested can be a maximum of 65535 bytes. The
program requesting the read operation is responsible for providing the
data buffer.
If the read operation succeeds, the data is read, beginning at the
current position of the file read/write pointer, to the specified
location in memory. MS-DOS then increments its internal read/write
pointer for the file by the length of the data transferred and returns
the length to the calling program in AX with the carry flag cleared.
The only indication that the end of the file has been reached is that
the length returned is less than the length requested. In contrast,
when Function 3FH is used to read from a character device that is not
in raw mode, the read will terminate at the requested length or at the
receipt of a carriage return character, whichever comes first. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS:
Character Device Input and Output. If the read operation fails, MS-DOS
returns with the carry flag set and an error code in AX.
Function 40H (Write File or Device) writes from a buffer to a file (or
device) using a handle previously obtained from an open or create
operation. Before calling Function 40H, the program must set DS:DX to
point to the beginning of the buffer containing the source data, put
the file handle in BX, and put the number of bytes to write in CX. The
number of bytes to write can be a maximum of 65535.
If the write operation is successful, MS-DOS puts the number of bytes
written in AX and increments the read/write pointer by this value; if
the write operation fails, MS-DOS sets the carry flag and returns an
error code in AX.
Records smaller than one sector (512 bytes) are not written directly
to disk. Instead, MS-DOS stores the record in an internal buffer and
writes it to disk when the internal buffer is full, when the file is
closed, or when a call to Interrupt 21H Function 0DH (Disk Reset) is
issued.
Note: If the destination of the write operation is a disk file and the
disk is full, the only indication to the calling program is that the
length returned in AX is not the same as the length requested in CX.
Disk full is not returned as an error with the carry flag set.
A special use of the Write function is to truncate or extend a file.
If Function 40H is called with a record length of zero in CX, the file
size will be adjusted to the current location of the file read/write
pointer.
Example: Open the file MYFILE.DAT, create the file MYFILE.BAK, copy
the contents of the .DAT file into the .BAK file using 512-byte reads
and writes, and then close both files.
file1 db 'MYFILE.DAT',0
file2 db 'MYFILE.BAK',0
handle1 dw ? ; handle for MYFILE.DAT
handle2 dw ? ; handle for MYFILE.BAK
buff db 512 dup (?) ; buffer for file I/O
.
.
.
; open MYFILE.DAT...
mov dx,seg file1 ; DS:DX = address of filename
mov ds,dx
mov dx,offset file1
mov ax,3d00h ; Function 3DH = open (read-only)
int 21h ; transfer to MS-DOS
jc error ; jump if open failed
mov handle1,ax ; save handle for file
; create MYFILE.BAK...
mov dx,offset file2 ; DS:DX = address of filename
mov cx,0 ; CX = normal attribute
mov ah,3ch ; Function 3CH = create
int 21h ; transfer to MS-DOS
jc error ; jump if create failed
mov handle2,ax ; save handle for file
loop: ; read MYFILE.DAT
mov dx,offset buff ; DS:DX = buffer address
mov cx,512 ; CX = length to read
mov bx,handle1 ; BX = handle for MYFILE.DAT
mov ah,3fh ; Function 3FH = read
int 21h ; transfer to MS-DOS
jc error ; jump if read failed
or ax,ax ; were any bytes read?
jz done ; no, end of file reached
; write MYFILE.BAK
mov dx,offset buff ; DS:DX = buffer address
mov cx,ax ; CX = length to write
mov bx,handle2 ; BX = handle for MYFILE.BAK
mov ah,40h ; Function 40H = write
int 21h ; transfer to MS-DOS
jc error ; jump if write failed
cmp ax,cx ; was write complete?
jne error ; jump if disk full
jmp loop ; continue to end of file
done: ; now close files...
mov bx,handle1 ; handle for MYFILE.DAT
mov ah,3eh ; Function 3EH = close file
int 21h ; transfer to MS-DOS
jc error ; jump if close failed
mov bx,handle2 ; handle for MYFILE.BAK
mov ah,3eh ; Function 3EH = close file
int 21h ; transfer to MS-DOS
jc error ; jump if close failed
.
.
.
Positioning the read/write pointer
Function 42H (Move File Pointer) sets the position of the read/write
pointer associated with a given handle. The function is called with a
signed 32-bit offset in the CX and DX registers (the most significant
half in CX), the file handle in BX, and the positioning mode in AL:
╓┌──────────────┌────────────────────────────────────────────────────────────╖
Mode Significance
──────────────────────────────────────────────────────────────────
00 Supplied offset is relative to beginning of file.
01 Supplied offset is relative to current position of
read/write pointer.
02 Supplied offset is relative to end of file.
If Function 42H succeeds, MS-DOS returns the resulting absolute offset
(in bytes) of the file pointer relative to the beginning of the file
in the DX and AX registers, with the most significant half in DX; if
the function fails, MS-DOS sets the carry flag and returns an error
code in AX.
Thus, a program can obtain the size of a file by calling Function 42H
with an offset of zero and a positioning mode of 2. The function
returns a value in DX:AX that represents the offset of the end-of-file
position relative to the beginning of the file.
Example: Assume that the file MYFILE.DAT was previously opened and its
handle is saved in the variable fhandle. Position the file pointer
32768 bytes from the beginning of the file and then read 512 bytes of
data starting at that file position.
fhandle dw ? ; handle from previous open
buff db 512 dup (?) ; buffer for data from file
.
.
.
; position the file pointer...
mov cx,0 ; CX = high part of file offset
mov dx,32768 ; DX = low part of file offset
mov bx,fhandle ; BX = handle for file
mov al,0 ; AL = positioning mode
mov ah,42h ; Function 42H = position
int 21h ; transfer to MS-DOS
jc error ; jump if function call failed
; now read 512 bytes from file
mov dx,offset buff ; DS:DX = address of buffer
mov cx,512 ; CX = length of 512 bytes
mov bx,fhandle ; BX = handle for file
mov ah,3fh ; Function 3FH = read
int 21h ; transfer to MS-DOS
jc error ; jump if read failed
cmp ax,512 ; was 512 bytes read?
jne error ; jump if partial rec. or EOF
.
.
.
Example: Assume that the file MYFILE.DAT was previously opened and its
handle is saved in the variable fhandle. Find the size of the file in
bytes by positioning the file pointer to zero bytes relative to the
end of the file. The returned offset, which is relative to the
beginning of the file, is the file's size.
fhandle dw ? ; handle from previous open
.
.
.
; position the file pointer
; to the end of file...
mov cx,0 ; CX = high part of offset
mov dx,0 ; DX = low part of offset
mov bx,fhandle ; BX = handle for file
mov al,2 ; AL = positioning mode
mov ah,42h ; Function 42H = position
int 21h ; transfer to MS-DOS
jc error ; jump if function call failed
; if call succeeded, DX:AX
; now contains the file size
.
.
.
Other handle operations
MS-DOS provides other handle-oriented functions to rename (or move) a
file, delete a file, read or change a file's attributes, read or
change a file's date and time stamp, and duplicate or redirect a file
handle. The first three of these are "file-handle-like" because they
use an ASCIIZ string to specify the file; however, they do not return
a file handle.
Renaming a file
Function 56H (Rename File) renames an existing file and/or moves the
file from one location in the hierarchical file structure to another.
The file to be renamed cannot be a hidden or system file or a
subdirectory and must not be currently open by any process; attempting
to rename an open file can corrupt the disk. MS-DOS renames a file by
simply changing its directory entry; it moves a file by removing its
current directory entry and creating a new entry in the target
directory that refers to the same file. The location of the file's
actual data on the disk is not changed.
Both the current and the new filenames must be ASCIIZ strings and can
include a drive and path specification; wildcard characters (* and ?)
are not permitted in the filenames. The program calls Function 56H
with the address of the current pathname in the DS:DX registers and
the address of the new pathname in ES:DI. If the path elements of the
two strings are not the same and both paths are valid, the file
"moves" from the source directory to the target directory. If the
paths match but the filenames differ, MS-DOS simply modifies the
directory entry to reflect the new filename.
If the function succeeds, MS-DOS returns to the calling program with
the carry flag clear. The function fails if the new filename is
already in the target directory; in that case, MS-DOS sets the carry
flag and returns an error code in AX.
Example: Change the name of the file MYFILE.DAT to MYFILE.OLD. In the
same operation, move the file from the \WORK directory to the \BACKUP
directory.
file1 db '\WORK\MYFILE.DAT',0
file2 db '\BACKUP\MYFILE.OLD',0
.
.
.
mov dx,seg file1 ; DS:DX = old filename
mov ds,dx
mov es,dx
mov dx,offset file1
mov di,offset file2 ; ES:DI = new filename
mov ah,56h ; Function 56H = rename
int 21h ; transfer to MS-DOS
jc error ; jump if rename failed
.
.
.
Deleting a file
Function 41H (Delete File) effectively deletes a file from a disk.
Before calling the function, a program must set the DS:DX registers to
point to the ASCIIZ pathname of the file to be deleted. The supplied
pathname cannot specify a subdirectory or a read-only file, and the
file must not be currently open by any process.
If the function is successful, MS-DOS deletes the file by simply
marking the first byte of its directory entry with a special character
(0E5H), making the entry subsequently unrecognizable. MS-DOS then
updates the disk's FAT so that the clusters that previously belonged
to the file are "free" and returns to the program with the carry flag
clear. If the delete function fails, MS-DOS sets the carry flag and
returns an error code in AX.
The actual contents of the clusters assigned to the file are not
changed by a delete operation, so for security reasons sensitive
information should be overwritten with spaces or some other constant
character before the file is deleted with Function 41H.
Example: Delete the file MYFILE.DAT, located in the \WORK directory on
drive C.
fname db 'C:\WORK\MYFILE.DAT',0
.
.
.
mov dx,seg fname ; DS:DX = address of filename
mov ds,dx
mov dx,offset fname
mov ah,41h ; Function 41H = delete
int 21h ; transfer to MS-DOS
jc error ; jump if delete failed
.
.
.
Getting/setting file attributes
Function 43H (Get/Set File Attributes) obtains or modifies the
attributes of an existing file. Before calling Function 43H, the
program must set the DS:DX registers to point to the ASCIIZ pathname
for the file. To read the attributes, the program must set AL to zero;
to set the attributes, it must set AL to 1 and place an attribute code
in CX. See Creating a File, above.
If the function is successful, MS-DOS reads or sets the attribute byte
in the file's directory entry and returns with the carry flag clear
and the file's attribute in CX. If the function fails, MS-DOS sets the
carry flag and returns an error code in AX.
Function 43H cannot be used to set the volume-label bit (bit 3) or the
subdirectory bit (bit 4) of a file. It also should not be used on a
file that is currently open by any process.
Example: Change the attributes of the file MYFILE.DAT in the \BACKUP
directory on drive C to read-only. This prevents the file from being
accidentally deleted from the disk.
fname db 'C:\BACKUP\MYFILE.DAT',0
.
.
.
mov dx,seg fname ; DS:DX = address of filename
mov ds,dx
mov dx,offset fname
mov cx,1 ; CX = attribute (read-only)
mov al,1 ; AL = mode (0 = get, 1 = set)
mov ah,43h ; Function 43H = get/set attr
int 21h ; transfer to MS-DOS
jc error ; jump if set attrib. failed
.
.
.
Getting/setting file date and time
Function 57H (Get/Set Date/Time of File) reads or sets the directory
time and date stamp of an open file. To set the time and date to a
particular value, the program must call Function 57H with the desired
time in CX, the desired date in DX, the handle for the file (obtained
from a previous open or create operation) in BX, and the value 1 in
AL. To read the time and date, the function is called with AL
containing 0 and the file handle in BX; the time is returned in the CX
register and the date is returned in the DX register. As with other
handle-oriented file functions, if the function succeeds, the carry
flag is returned cleared; if the function fails, MS-DOS returns the
carry flag set and an error code in AX.
The formats used for the file time and date are the same as those used
in disk directory entries and FCBs. See Structure of the File Control
Block, below.
The main uses of Function 57H are to force the time and date entry for
a file to be updated when the file has not been changed and to
circumvent MS-DOS's modification of a file date and time when the file
has been changed. In the latter case, a program can use this function
with AL = 0 to obtain the file's previous date and time stamp, modify
the file, and then restore the original file date and time by re-
calling the function with AL = 1 before closing the file.
Duplicating and redirecting handles
Ordinarily, the disk FAT and directory are not updated until a file is
closed, even when the file has been modified. Thus, until the file is
closed, any new data added to the file can be lost if the system
crashes or is turned off unexpectedly. The obvious defense against
such loss is simply to close and reopen the file every time the file
is changed. However, this is a relatively slow procedure and in a
network environment can cause the program to lose control of the file
to another process.
Use of a second file handle, created by using Function 45H (Duplicate
File Handle) to duplicate the original handle of the file to be
updated, can protect data added to a disk file before the file is
closed. To use Function 45H, the program must put the handle to be
duplicated in BX. If the operation is successful, MS-DOS clears the
carry flag and returns the new handle in AX; if the operation fails,
MS-DOS sets the carry flag and returns an error code in AX.
If the function succeeds, the duplicate handle can simply be closed in
the usual manner with Function 3EH. This forces the desired update of
the disk directory and FAT. The original handle remains open and the
program can continue to use it for file read and write operations.
Note: While the second handle is open, moving the read/write pointer
associated with either handle moves the pointer associated with the
other.
Example: Assume that the file MYFILE.DAT was previously opened and the
handle for that file has been saved in the variable fhandle. Duplicate
the handle and then close the duplicate to ensure that any data
recently written to the file is saved on the disk and that the
directory entry for the file is updated accordingly.
fhandle dw ? ; handle from previous open
.
.
.
; duplicate the handle...
mov bx,fhandle ; BX = handle for file
mov ah,45h ; Function 45H = dup handle
int 21h ; transfer to MS-DOS
jc error ; jump if function call failed
; now close the new handle...
mov bx,ax ; BX = duplicated handle
mov ah,3eh ; Function 3EH = close
int 21h ; transfer to MS-DOS
jc error ; jump if close failed
mov bx,fhandle ; replace closed handle with
; active handle
.
.
.
Function 45H is sometimes also used in conjunction with Function 46H
(Force Duplicate File Handle). Function 46H forces a handle to be a
duplicate for another open handle--in other words, to refer to the
same file or device at the same file read/write pointer location. The
handle is then said to be redirected.
The most common use of Function 46H is to change the meaning of the
standard input and standard output handles before loading a child
process with the EXEC function. In this manner, the input for the
child program can be redirected to come from a file or its output can
be redirected into a file, without any special knowledge on the part
of the child program. In such cases, Function 45H is used to also
create duplicates of the standard input and standard output handles
before they are redirected, so that their original meanings can be
restored after the child exits. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: CUSTOMIZING MS-DOS: Writing MS-DOS Filters.
Using the FCB Functions
A file control block is a data structure, located in the application
program's memory space, that contains relevant information about an
open disk file: the disk drive, the filename and extension, a pointer
to a position within the file, and so on. Each open file must have its
own FCB. The information in an FCB is maintained cooperatively by both
MS-DOS and the application program.
MS-DOS moves data to and from a disk file associated with an FCB by
means of a data buffer called the disk transfer area (DTA). The
current address of the DTA is under the control of the application
program, although each program has a 128-byte default DTA at offset
80H in its program segment prefix (PSP). See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: Structure of an Application
Program.
Under early versions of MS-DOS, the only limit on the number of files
that can be open simultaneously with FCBs is the amount of memory
available to the application to hold the FCBs and their associated
disk buffers. However, under MS-DOS versions 3.0 and later, when file-
sharing support (SHARE.EXE) is loaded, MS-DOS places some restrictions
on the use of FCBs to simplify the job of maintaining network
connections for files. If the application attempts to open too many
FCBs, MS-DOS simply closes the least recently used FCBs to keep the
total number within a limit.
The CONFIG.SYS file directive FCBS allows the user to control the
allowed maximum number of FCBs and to specify a certain number of FCBs
to be protected against automatic closure by the system. The default
values are a maximum of four files open simultaneously using FCBs and
zero FCBs protected from automatic closure by the system. See USER
COMMANDS: CONFIG.SYS: FCBS.
Because the FCB operations predate MS-DOS version 2.0 and because FCBs
have a fixed structure with no room to contain a path, the FCB file
and record services do not support the hierarchical directory
structure. Many FCB operations can be performed only on files in the
current directory of a disk. For this reason, the use of FCB file and
record operations should be avoided in new programs.
Structure of the file control block
Each FCB is a 37-byte array allocated from its own memory space by the
application program that will use it. The FCB contains all the
information needed to identify a disk file and access the data within
it: drive identifier, filename, extension, file size, record size,
various file pointers, and date and time stamps. The FCB structure is
shown in Table 7-3.
Table 7-3. Structure of a Normal File Control Block.
╓┌──────────────────────┌───────────┌──────────────┌─────────────────────────╖
Offset Size
Maintained by (bytes) (bytes) Description
──────────────────────────────────────────────────────────────────
Program 00H 1 Drive identifier
Program 01H 8 Filename
Program 09H 3 File extension
MS-DOS 0CH 2 Current block number
Program 0EH 2 Record size (bytes)
MS-DOS 10H 4 File size (bytes)
MS-DOS 14H 2 Date stamp
MS-DOS 16H 2 Time stamp
MS-DOS 18H 8 Reserved
MS-DOS 20H 1 Current record number
Program 21H 4 Random record number
Drive identifier: Initialized by the application to designate the
drive on which the file to be opened or created resides. 0 = default
drive, 1 = drive A, 2 = drive B, and so on. If the application
supplies a zero in this byte (to use the default drive), MS-DOS alters
the byte during the open or create operation to reflect the actual
drive used; that is, after an open or create operation, this drive
will always contain a value of 1 or greater.
Filename: Standard eight-character filename; initialized by the
application; must be left justified and padded with blanks if the name
has fewer than eight characters. A device name (for example, PRN) can
be used; note that there is no colon after a device name.
File extension: Three-character file extension; initialized by the
application; must be left justified and padded with blanks if the
extension has fewer than three characters.
Current block number: Initialized to zero by MS-DOS when the file is
opened. The block number and the record number together make up the
record pointer during sequential file access.
Record size: The size of a record (in bytes) as used by the program.
MS-DOS sets this field to 128 when the file is opened or created; the
program can modify the field afterward to any desired record size. If
the record size is larger than 128 bytes, the default DTA in the PSP
cannot be used because it will collide with the program's own code or
data.
File size: The size of the file in bytes. MS-DOS initializes this
field from the file's directory entry when the file is opened. The
first 2 bytes of this 4-byte field are the least significant bytes of
the file size.
Date stamp: The date of the last write operation on the file. MS-DOS
initializes this field from the file's directory entry when the file
is opened. This field uses the same format used by file handle
Function 57H (Get/Set/Date/Time of File):
Date Format
Bit: 15 14 13 12 11 10 9 8 ║ 7 6 5 4 3 2 1 0
║
Content: ┌───╥───╥───╥───╥───╥───╥───╥───╫───╥───╥───╥───╥───╥───╥───╥───┐
│ Y ║ Y ║ Y ║ Y ║ Y ║ Y ║ Y ║ M ║ M ║ M ║ M ║ D ║ D ║ D ║ D ║ D │
└───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───┘
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Bits Contents
──────────────────────────────────────────────────────────────────
0-4 Day of month (1-31)
5-8 Month (1-12)
9-15 Year (relative to 1980)
Time stamp: The time of the last write operation on the file. MS-DOS
initializes this field from the file's directory entry when the file
is opened. This field uses the same format used by file handle
Function 57H (Get/Set/Date/Time of File):
Time Format
Bit: 15 14 13 12 11 10 9 8 ║ 7 6 5 4 3 2 1 0
║
Content: ┌───╥───╥───╥───╥───╥───╥───╥───╫───╥───╥───╥───╥───╥───╥───╥───┐
│ H ║ H ║ H ║ H ║ H ║ M ║ M ║ M ║ M ║ M ║ M ║ S ║ S ║ S ║ S ║ S │
└───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───╨───┘
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Bits Contents
──────────────────────────────────────────────────────────────────
0-4 Number of 2-second increments (0-29)
5-10 Minutes (0-59)
11-15 Hours (0-23)
Current record number: Together with the block number, constitutes the
record pointer used during sequential read and write operations. MS-
DOS does not initialize this field when a file is opened. The record
number is limited to the range 0 through 127; thus, there are 128
records per block. The beginning of a file is record 0 of block 0.
Random record pointer: A 4-byte field that identifies the record to be
transferred by the random record functions 21H, 22H, 27H, and 28H. If
the record size is 64 bytes or larger, only the first 3 bytes of this
field are used. MS-DOS updates this field after random block reads and
writes (Functions 27H and 28H) but not after random record reads and
writes (Functions 21H and 22H).
An extended FCB, which is 7 bytes longer than a normal FCB, can be
used to access files with special attributes such as hidden, system,
and read-only. The extra 7 bytes of an extended FCB are simply
prefixed to the normal FCB format (Table 7-4). The first byte of an
extended FCB always contains 0FFH, which could never be a legal drive
code and therefore serves as a signal to MS-DOS that the extended
format is being used. The next 5 bytes are reserved and must be zero,
and the last byte of the prefix specifies the attributes of the file
being manipulated. The remainder of an extended FCB has exactly the
same layout as a normal FCB. In general, an extended FCB can be used
with any MS-DOS function call that accepts a normal FCB.
Table 7-4. Structure of an Extended File Control Block.
╓┌──────────────────────┌─────────────┌───────────┌──────────────────────────╖
Offset Size
Maintained by (bytes) (bytes) Description
──────────────────────────────────────────────────────────────────
Program 00H 1 Extended FCB flag = 0FFH
MS-DOS 01H 5 Reserved
Program 06H 1 File attribute byte
Program 07H 1 Drive identifier
Program 08H 8 Filename
Program 10H 3 File extension
MS-DOS 13H 2 Current block number
Program 15H 2 Record size (bytes)
MS-DOS 17H 4 File size (bytes)
MS-DOS 1BH 2 Date stamp
MS-DOS 1DH 2 Time stamp
MS-DOS 1FH 8 Reserved
MS-DOS 27H 1 Current record number
Program 28H 4 Random record number
Extended FCB flag: When 0FFH is present in the first byte of an FCB,
it is a signal to MS-DOS that an extended FCB (44 bytes) is being used
instead of a normal FCB (37 bytes).
File attribute byte: Must be initialized by the application when an
extended FCB is used to open or create a file. The bits of this field
have the following significance:
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Bit Meaning
──────────────────────────────────────────────────────────────────
0 Read-only
1 Hidden
2 System
3 Volume label
4 Directory
5 Archive
6 Reserved
7 Reserved
FCB functions and the PSP
The PSP contains several items that are of interest when using the FCB
file and record operations: two FCBs called the default FCBs, the
default DTA, and the command tail for the program. The following table
shows the size and location of these elements:
╓┌──────────────────────────┌─────────────────┌──────────────────────────────╖
PSP Offset Size
PSP Offset Size
(bytes) (bytes) Description
──────────────────────────────────────────────────────────────────
5CH 16 Default FCB #1
6CH 20 Default FCB #2
80H 1 Length of command tail
81H 127 Command-tail text
80H 128 Default disk transfer area
(DTA)
When MS-DOS loads a program into memory for execution, it copies the
command tail into the PSP at offset 81H, places the length of the
command tail in the byte at offset 80H, and parses the first two
parameters in the command tail into the default FCBs at PSP offsets
5CH and 6CH. (The command tail consists of the command line used to
invoke the program minus the program name itself and any redirection
or piping characters and their associated filenames or device names.)
MS-DOS then sets the initial DTA address for the program to PSP:0080H.
For several reasons, the default FCBs and the DTA are often moved to
another location within the program's memory area. First, the default
DTA allows processing of only very small records. In addition, the
default FCBs overlap substantially, and the first byte of the default
DTA and the last byte of the first FCB conflict. Finally, unless
either the command tail or the DTA is moved beforehand, the first FCB-
related file or record operation will destroy the command tail.
Function 1AH (Set DTA Address) is used to alter the DTA address. It is
called with the segment and offset of the new buffer to be used as the
DTA in DS:DX. The DTA address remains the same until another call to
Function 1AH, regardless of other file and record management calls; it
does not need to be reset before each read or write.
Note: A program can use Function 2FH (Get DTA Address) to obtain the
current DTA address before changing it, so that the original address
can be restored later.
Parsing the filename
Before a file can be opened or created with the FCB function calls,
its drive, filename, and extension must be placed within the proper
fields of the FCB. The filename can be coded into the program itself,
or the program can obtain it from the command tail in the PSP or by
prompting the user and reading it in with one of the several function
calls for character device input.
MS-DOS automatically parses the first two parameters in the program's
command tail into the default FCBs at PSP:005CH and PSP:006CH. It does
not, however, attempt to differentiate between switches and filenames,
so the pre-parsed FCBs are not necessarily useful to the application
program. If the filenames were preceded by any switches, the program
itself has to extract the filenames directly from the command tail.
The program is then responsible for determining which parameters are
switches and which are filenames, as well as where each parameter
begins and ends.
After a filename has been located, Function 29H (Parse Filename) can
be used to test it for invalid characters and separators and to insert
its various components into the proper fields in an FCB. The filename
must be a string in the standard form drive:filename.ext. Wildcard
characters are permitted in the filename and/or extension; asterisk
(*) wildcards are expanded to question mark (?) wildcards.
To call Function 29H, the DS:SI registers must point to the candidate
filename, ES:DI must point to the 37-byte buffer that will become the
FCB for the file, and AL must hold the parsing control code. See
SYSTEM CALLS: INTERRUPT 21H: Function 29H.
If a drive code is not included in the filename, MS-DOS inserts the
drive number of the current drive into the FCB. Parsing stops at the
first terminator character encountered in the filename. Terminators
include the following:
; , = + / " [] | < > | space tab
If a colon character (:) is not in the proper position to delimit the
disk drive identifier or if a period (.) is not in the proper position
to delimit the extension, the character will also be treated as a
terminator. For example, the filename C:MEMO.TXT will be parsed
correctly; however, ABC:DEF.DAY will be parsed as ABC.
If an invalid drive is specified in the filename, Function 29H returns
0FFH in AL; if the filename contains any wildcard characters, it
returns 1. Otherwise, AL contains zero upon return, indicating a
valid, unambiguous filename.
Note that this function simply parses the filename into the FCB. It
does not initialize any other fields of the FCB (although it does zero
the current block and record size fields), and it does not test
whether the specified file actually exists.
Error handling and FCB functions
The FCB-related file and record functions do not return much in the
way of error information when a function fails. Typically, an FCB
function returns a zero in AL if the function succeeded and 0FFH if
the function failed. Under MS-DOS versions 2.x, the program is left to
its own devices to determine the cause of the error. Under MS-DOS
versions 3.x, however, a failed FCB function call can be followed by a
call to Interrupt 21H Function 59H (Get Extended Error Information).
Function 59H will return the same descriptive codes for the error,
including the error locus and a suggested recovery strategy, as would
be returned for the counterpart handle-oriented file or record
function.
Creating a file
Function 16H (Create File with FCB) creates a new file and opens it
for subsequent read/write operations. The function is called with
DS:DX pointing to a valid, unopened FCB. MS-DOS searches the current
directory for the specifed filename. If the filename is found, MS-DOS
sets the file length to zero and opens the file, effectively
truncating it to a zero-length file; if the filename is not found,
MS-DOS creates a new file and opens it. Other fields of the FCB are
filled in by MS-DOS as described below under Opening a File.
If the create operation succeeds, MS-DOS returns zero in AL; if the
operation fails, it returns 0FFH in AL. This function will not
ordinarily fail unless the file is being created in the root directory
and the directory is full.
Warning: To avoid loss of existing data, the FCB open function should
be used to test for file existence before creating a file.
Opening a file
Function 0FH opens an existing file. DS:DX must point to a valid,
unopened FCB containing the name of the file to be opened. If the
specified file is found in the current directory, MS-DOS opens the
file, fills in the FCB as shown in the list below, and returns with AL
set to 00H; if the file is not found, MS-DOS returns with AL set to
0FFH, indicating an error.
When the file is opened, MS-DOS
■ Sets the drive identifier (offset 00H) to the actual drive (01 = A,
02 = B, and so on).
■ Sets the current block number (offset 0CH) to zero.
■ Sets the file size (offset 10H) to the value found in the directory
entry for the file.
■ Sets the record size (offset 0EH) to 128.
■ Sets the date and time stamp (offsets 14H and 16H) to the values
found in the directory entry for the file.
The program may need to adjust the FCB--change the record size and the
random record pointer, for example--before proceeding with record
operations.
Example: Display a prompt and accept a filename from the user. Parse
the filename into an FCB, checking for an illegal drive identifier or
the presence of wildcards. If a valid, unambiguous filename has been
entered, attempt to open the file. Create the file if it does not
already exist.
kbuf db 64,0,64 dup (0)
prompt db 0dh,0ah,'Enter filename: $'
myfcb db 37 dup (0)
.
.
.
; display the prompt...
mov dx,seg prompt ; DS:DX = prompt address
mov ds,dx
mov es,dx
mov dx,offset prompt
mov ah,09h ; Function 09H = print string
int 21h ; transfer to MS-DOS
; now input filename...
mov dx,offset kbuf ; DS:DX = buffer address
mov ah,0ah ; Function 0AH = enter string
int 21h ; transfer to MS-DOS
; parse filename into FCB...
mov si,offset kbuf+2 ; DS:SI = address of filename
mov di,offset myfcb ; ES:DI = address of fcb
mov ax,2900h ; Function 29H = parse name
int 21h ; transfer to MS-DOS
or al,al ; jump if bad drive or
jnz error ; wildcard characters in name
; try to open file...
mov dx,offset myfcb ; DS:DX = FCB address
mov ah,0fh ; Function 0FH = open file
int 21h ; transfer to MS-DOS
or al,al ; check status
jz proceed ; jump if open successful
; else create file...
mov dx,offset myfcb ; DS:DX = FCB address
mov ah,16h ; Function 16H = create
int 21h ; transfer to MS-DOS
or al,al ; did create succeed?
jnz error ; jump if create failed
proceed:
. ; file has been opened or
. ; created, and FCB is valid
. ; for read/write operations...
Closing a file
Function 10H (Close File with FCB) closes a file previously opened
with an FCB. As usual, the function is called with DS:DX pointing to
the FCB of the file to be closed. MS-DOS updates the directory, if
necessary, to reflect any changes in the file's size and the date and
time last written.
If the operation succeeds, MS-DOS returns 00H in AL; if the operation
fails, MS-DOS returns 0FFH.
Reading and writing files with FCBs
MS-DOS offers a choice of three FCB access methods for data within
files: sequential, random record, and random block.
Sequential operations step through the file one record at a time.
MS-DOS increments the current record and current block numbers after
each file access so that they point to the beginning of the next
record. This method is particularly useful for copying or listing
files.
Random record access allows the program to read or write a record from
any location in the file, without sequentially reading all records up
to that point in the file. The program must set the random record
number field of the FCB appropriately before the read or write is
requested. This method is useful in database applications, in which a
program must manipulate fixed-length records.
Random block operations combine the features of sequential and random
record access methods. The program can set the record number to point
to any record within a file, and MS-DOS updates the record number
after a read or write operation. Thus, sequential operations can
easily be initiated at any file location. Random block operations with
a record length of 1 byte simulate file-handle access methods.
All three methods require that the FCB for the file be open, that
DS:DX point to the FCB, that the DTA be large enough for the specified
record size, and that the DTA address be previously set with Function
1AH if the default DTA in the program's PSP is not being used.
MS-DOS reports the success or failure of any FCB-related read
operation (sequential, random record, or random block) with one of
four return codes in register AL:
╓┌──────────────┌────────────────────────────────────────────────────────────╖
Code Meaning
──────────────────────────────────────────────────────────────────────
00H Successful read
01H End of file reached; no data read into DTA
02H Segment wrap (DTA too close to end of segment); no data read
into DTA
03H End of file reached; partial record read into DTA
MS-DOS reports the success or failure of an FCB-related write
operation as one of three return codes in register AL:
╓┌──────────────┌────────────────────────────────────────────────────────────╖
Code Meaning
──────────────────────────────────────────────────────────────────────
00H Successful write
01H Disk full; partial or no write
02H Segment wrap (DTA too close to end of segment); write failed
For FCB write operations, records smaller than one sector (512 bytes)
are not written directly to disk. Instead, MS-DOS stores the record in
an internal buffer and writes the data to disk only when the internal
buffer is full, when the file is closed, or when a call to Interrupt
21H Function 0DH (Disk Reset) is issued.
Sequential access: reading
Function 14H (Sequential Read) reads records sequentially from the
file to the current DTA address, which must point to an area at least
as large as the record size specified in the file's FCB. After each
read operation, MS-DOS updates the FCB block and record numbers
(offsets 0CH and 20H) to point to the next record.
Sequential access: writing
Function 15H (Sequential Write) writes records sequentially from
memory into the file. The length written is specified by the record
size field (offset 0EH) in the FCB; the memory address of the record
to be written is determined by the current DTA address. After each
sequential write operation, MS-DOS updates the FCB block and record
numbers (offsets 0CH and 20H) to point to the next record.
Random record access: reading
Function 21H (Random Read) reads a specific record from a file. Before
requesting the read operation, the program specifies the record to be
transferred by setting the record size and random record number fields
of the FCB (offsets 0EH and 21H). The current DTA address must also
have been previously set with Function 1AH to point to a buffer of
adequate size if the default DTA is not large enough.
After the read, MS-DOS sets the current block and current record
number fields (offsets 0CH and 20H) to point to the same record. Thus,
the program is set up to change to sequential reads or writes.
However, if the program wants to continue with random record access,
it must continue to update the random record field of the FCB before
each random record read or write operation.
Random record access: writing
Function 22H (Random Write) writes a specific record from memory to a
file. Before issuing the function call, the program must ensure that
the record size and random record pointer fields at FCB offsets 0EH
and 21H are set appropriately and that the current DTA address points
to the buffer containing the data to be written.
After the write, MS-DOS sets the current block and current record
number fields (offsets 0CH and 20H) to point to the same record. Thus,
the program is set up to change to sequential reads or writes. If the
program wants to continue with random record access, it must continue
to update the random record field of the FCB before each random record
read or write operation.
Random block access: reading
Function 27H (Random Block Read) reads a block of consecutive records.
Before issuing the read request, the program must specify the file
location of the first record by setting the record size and random
record number fields of the FCB (offsets 0EH and 21H) and must put the
number of records to be read in CX. The DTA address must have already
been set with Function 1AH to point to a buffer large enough to
contain the group of records to be read if the default DTA was not
large enough. The program can then issue the Function 27H call with
DS:DX pointing to the FCB for the file.
After the random block read operation, MS-DOS resets the FCB random
record pointer (offset 21H) and the current block and current record
number fields (offsets 0CH and 20H) to point to the beginning of the
next record not read and returns the number of records actually read
in CX.
If the record size is set to 1 byte, Function 27H reads the number of
bytes specified in CX, beginning with the byte position specified in
the random record pointer. This simulates (to some extent) the handle
type of read operation (Function 3FH).
Random block access: writing
Function 28H (Random Block Write) writes a block of consecutive
records from memory to disk. The program specifies the file location
of the first record to be written by setting the record size and
random record pointer fields in the FCB (offsets 0EH and 21H). If the
default DTA is not being used, the program must also ensure that the
current DTA address is set appropriately by a previous call to
Function 1AH. When Function 28H is called, DS:DX must point to the FCB
for the file and CX must contain the number of records to be written.
After the random block write operation, MS-DOS resets the FCB random
record pointer (offset 21H) and the current block and current record
number fields (offsets 0CH and 20H) to point to the beginning of the
next block of data and returns the number of records actually written
in CX.
If the record size is set to 1 byte, Function 28H writes the number of
bytes specified in CX, beginning with the byte position specified in
the random record pointer. This simulates (to some extent) the handle
type of write operation (Function 40H).
Calling Function 28H with a record count of zero in register CX causes
the file length to be extended or truncated to the current value in
the FCB random record pointer field (offset 21H) multiplied by the
contents of the record size field (offset 0EH).
Example: Open the file MYFILE.DAT and create the file MYFILE.BAK on
the current disk drive, copy the contents of the .DAT file into the
.BAK file using 512-byte reads and writes, and then close both files.
fcb1 db 0 ; drive = default
db 'MYFILE ' ; 8 character filename
db 'DAT' ; 3 character extension
db 25 dup (0) ; remainder of fcb1
fcb2 db 0 ; drive = default
db 'MYFILE ' ; 8 character filename
db 'BAK' ; 3 character extension
db 25 dup (0) ; remainder of fcb2
buff db 512 dup (?) ; buffer for file I/O
.
.
.
; open MYFILE.DAT...
mov dx,seg fcb1 ; DS:DX = address of FCB
mov ds,dx
mov dx,offset fcb1
mov ah,0fh ; Function 0FH = open
int 21h ; transfer to MS-DOS
or al,al ; did open succeed?
jnz error ; jump if open failed
; create MYFILE.BAK...
mov dx,offset fcb2 ; DS:DX = address of FCB
mov ah,16h ; Function 16H = create
int 21h ; transfer to MS-DOS
or al,al ; did create succeed?
jnz error ; jump if create failed
; set record length to 512
mov word ptr fcb1+0eh,512
mov word ptr fcb2+0eh,512
; set DTA to our buffer...
mov dx,offset buff ; DS:DX = buffer address
mov ah,1ah ; Function 1AH = set DTA
int 21h ; transfer to MS-DOS
loop: ; read MYFILE.DAT
mov dx,offset fcb1 ; DS:DX = FCB address
mov ah,14h ; Function 14H = seq. read
int 21h ; transfer to MS-DOS
or al,al ; was read successful?
jnz done ; no, quit
; write MYFILE.BAK...
mov dx,offset fcb2 ; DS:DX = FCB address
mov ah,15h ; Function 15H = seq. write
int 21h ; transfer to MS-DOS
or al,al ; was write successful?
jnz error ; jump if write failed
jmp loop ; continue to end of file
done: ; now close files...
mov dx,offset fcb1 ; DS:DX = FCB for MYFILE.DAT
mov ah,10h ; Function 10H = close file
int 21h ; transfer to MS-DOS
or al,al ; did close succeed?
jnz error ; jump if close failed
mov dx,offset fcb2 ; DS:DX = FCB for MYFILE.BAK
mov ah,10h ; Function 10H = close file
int 21h ; transfer to MS-DOS
or al,al ; did close succeed?
jnz error ; jump if close failed
.
.
.
Other FCB file operations
As it does with file handles, MS-DOS provides FCB-oriented functions
to rename or delete a file. Unlike the other FCB functions and their
handle counterparts, these two functions accept wildcard characters.
An additional FCB function allows the size or existence of a file to
be determined without actually opening the file.
Renaming a file
Function 17H (Rename File) renames a file (or files) in the current
directory. The file to be renamed cannot have the hidden or system
attribute. Before calling Function 17H, the program must create a
special FCB that contains the drive code at offset 00H, the old
filename at offset 01H, and the new filename at offset 11H. Both the
current and the new filenames can contain the ? wildcard character.
When the function call is made, DS:DX must point to the special FCB
structure. MS-DOS searches the current directory for the old filename.
If it finds the old filename, MS-DOS then searches for the new
filename and, if it finds no matching filename, changes the directory
entry for the old filename to reflect the new filename. If the old
filename field of the special FCB contains any wildcard characters,
MS-DOS renames every matching file. Duplicate filenames are not
permitted; the process will fail at the first duplicate name.
If the operation is successful, MS-DOS returns zero in AL; if the
operation fails, it returns 0FFH. The error condition may indicate
either that no files were renamed or that at least one file was
renamed but the operation was then terminated because of a duplicate
filename.
Example: Rename all the files with the extension .ASM in the current
directory of the default disk drive to have the extension .COD.
renfcb db 0 ; default drive
db '????????' ; wildcard filename
db 'ASM' ; old extension
db 5 dup (0) ; reserved area
db '????????' ; wildcard filename
db 'COD' ; new extension
db 15 dup (0) ; remainder of FCB
.
.
.
mov dx,seg renfcb ; DS:DX = address of
mov ds,dx ; "special" FCB
mov dx,offset renfcb
mov ah,17h ; Function 17H = rename
int 21h ; transfer to MS-DOS
or al,al ; did function succeed?
jnz error ; jump if rename failed
.
.
.
Deleting a file
Function 13H (Delete File) deletes a file from the current directory.
The file should not be currently open by any process. If the file to
be deleted has special attributes, such as read-only, the program must
use an extended FCB to remove the file. Directories cannot be deleted
with this function, even with an extended FCB.
Function 13H is called with DS:DX pointing to an unopened, valid FCB
containing the name of the file to be deleted. The filename can
contain the ? wildcard character; if it does, MS-DOS deletes all files
matching the specified name. If at least one file matches the FCB and
is deleted, MS-DOS returns 00H in AL; if no matching filename is
found, it returns 0FFH.
Note: This function, if it succeeds, does not return any information
about which and how many files were deleted. When multiple files must
be deleted, closer control can be exercised by using the Find File
functions (Functions 11H and 12H) to inspect candidate filenames. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: Disk
Directories and Volume Labels. The files can then be deleted
individually.
Example: Delete all the files in the current directory of the current
disk drive that have the extension .BAK and whose filenames have A as
the first character.
delfcb db 0 ; default drive
db 'A???????' ; wildcard filename
db 'BAK' ; extension
db 25 dup (0) ; remainder of FCB
.
.
.
mov dx,seg delfcb ; DS:DX = FCB address
mov ds,dx
mov dx,offset delfcb
mov ah,13h ; Function 13H = delete
int 21h ; transfer to MS-DOS
or al,al ; did function succeed?
jnz error ; jump if delete failed
.
.
.
Finding file size and testing for existence
Function 23H (Get File Size) is used primarily to find the size of a
disk file without opening it, but it may also be used instead of
Function 11H (Find First File) to simply test for the existence of a
file. Before calling Function 23H, the program must parse the filename
into an unopened FCB, initialize the record size field of the FCB
(offset 0EH), and set the DS:DX registers to point to the FCB.
When Function 23H returns, AL contains 00H if the file was found in
the current directory of the specified drive and 0FFH if the file was
not found.
If the file was found, the random record field at FCB offset 21H
contains the number of records (rounded upward) in the target file, in
terms of the value in the record size field (offset 0EH) of the FCB.
If the record size is at least 64 bytes, only the first 3 bytes of the
random record field are used; if the record size is less than 64
bytes, all 4 bytes are used. To obtain the size of the file in bytes,
the program must set the record size field to 1 before the call. This
method is not any faster than simply opening the file, but it does
avoid the overhead of closing the file afterward (which is necessary
in a networking environment).
Summary
MS-DOS supports two distinct but overlapping sets of file and record
management services. The handle-oriented functions operate in terms of
null-terminated (ASCIIZ) filenames and 16-bit file identifiers, called
handles, that are returned by MS-DOS after a file is opened or
created. The filenames can include a full path specifying the file's
location in the hierarchical directory structure. The information
associated with a file handle, such as the current read/write pointer
for the file, the date and time of the last write to the file, and the
file's read/write permissions, sharing mode, and attributes, is
maintained in a table internal to MS-DOS.
In contrast, the FCB-oriented functions use a 37-byte structure called
a file control block, located in the application program's memory
space, to specify the name and location of the file. After a file is
opened or created, the FCB is used by both MS-DOS and the application
to hold other information about the file, such as the current
read/write file pointer, while that file is in use. Because FCBs
predate the hierarchical directory structure that was introduced in
MS-DOS version 2.0 and do not have room to hold the path for a file,
the FCB functions cannot be used to access files that are not in the
current directory of the specified drive.
In addition to their lack of support for pathnames, the FCB functions
have much poorer error reporting capabilities than handle functions
and are nearly useless in networking environments because they do not
support file sharing and locking. Consequently, it is strongly
recommended that the handle-related file and record functions be used
exclusively in all new applications.
Robert Byers
Code by Ray Duncan
Article 8: Disk Directories and Volume Labels
MS-DOS, being a disk operating system, provides facilities for
cataloging disk files. The data structure used by MS-DOS for this
purpose is the directory, a linear list of names in which each name is
associated with a physical location on the disk. Directories are
accessed and updated implicitly whenever files are manipulated, but
both directories and their contents can also be manipulated explicitly
using several of the MS-DOS Interrupt 21H service functions.
MS-DOS versions 1.x support only one directory on each disk. Versions
2.0 and later, however, support multiple directories linked in a two-
way, hierarchical tree structure (Figure 8-1), and the complete
specification of the name of a file or directory thus must describe
the location in the directory hierarchy in which the name appears.
This specification, or path, is created by concatenating a disk drive
specifier (for example, A: or C:), the names of the directories in
hierarchical order starting with the root directory, and finally the
name of the file or directory. For example, in Figure 8-1, the
complete pathname for FILE5.COM is C:\ALPHA\GAMMA\FILE5.COM. The two
instances of FILE1.COM, in the root directory and in the directory
EPSILON, are distinguished by their pathnames: C:\FILE1.COM in the
first instance and C:\BETA\EPSILON\FILE1.COM in the second.
C:\ (root directory)
┌─────────────────────┐
│subdirect. ALPHA │
│subdirect. BETA │
│file FILE1.COM │
│file FILE2.COM │
│ │
└───────────┬─────────┘
├────────────────────────────────────────────────┐
C:\ALPHA C:\BETA
┌─────────────────────┐ ┌─────────────────────┐
│subdirect. ∙ │ │subdirect. ∙ │
│subdirect. ∙∙ │ │subdirect. ∙∙ │
│subdirect. GAMMA │ │file EPSILON │
│subdirect. DELTA │ │file FILE4.COM │
│file FILE3.COM │ │ │
└───────────┬─────────┘ └──────────┬──────────┘
│ │
├───────────────────────┐ │
│ │ │
C:\ALPHA\GAMMA C:\ALPHA\DELTA C:\BETA\EPSILON
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│subdirect. ∙ │ │subdirect. ∙ │ │subdirect. ∙ │
│subdirect. ∙∙ │ │subdirect. ∙∙ │ │subdirect. ∙∙ │
│file FILE5.COM │ │ │ │file FILE1.COM │
│ │ │ │ │ │
│ │ │ │ │ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
Figure 8-1. Typical hierarchical directory structure (MS-DOS versions
2.0 and later).
Note: If no drive is specified, the current drive is assumed. Also, if
the first name in the specification is not preceded by a backslash,
the specification is assumed to be relative to the current directory.
For example, if the current directory is C:\BETA\EPSILON, the
specification \FILE1.COM indicates the file FILE1.COM in the root
directory and the specification FILE1.COM indicates the file FILE1.COM
in the directory C:\BETA\EPSILON. See Figure 8-1.
Although the casual user of MS-DOS need not be concerned with how this
hierarchical directory structure is implemented, MS-DOS programmers
should be familiar with the internal structure of directories and with
the Interrupt 21H functions available for manipulating directory
contents and maintaining the links between directories. This article
provides that information.
Logical Structure of MS-DOS Directories
An MS-DOS directory consists of a list of 32-byte directory entries,
each of which contains a name and descriptive information. In MS-DOS
versions 1.x, each name must be a filename; in versions 2.0 and later,
volume labels and directory names can also appear in directory
entries.
Directory searches
Directory entries are not sorted, nor are they maintained as a linked
list. Thus, when MS-DOS searches a directory for a name, the search
must proceed linearly from the first name in the directory. In MS-DOS
versions 1.x, a directory search continues until the specified name is
found or until every entry in the directory has been examined. In
versions 2.0 and later, the search continues until the specified name
is found or until a null directory entry (that is, one whose first
byte is zero) is encountered. This null entry indicates the logical
end of the directory.
Adding and deleting directory entries
MS-DOS deletes a directory entry by marking it with 0E5H in the first
byte rather than by erasing it or excising it from the directory. New
names are added to the directory by reusing the first deleted entry in
the list. If no deleted entries are available, MS-DOS appends the new
entry to the list.
The current directory
When more than one directory exists on a disk, MS-DOS keeps track of a
default search directory known as the current directory. The current
directory is the directory used for all implicit directory searches,
such as those occasioned by a request to open a file, if no
alternative path is specified. At startup, MS-DOS makes the root
directory the current directory, but any other directory can be
designated later, either interactively by using the CHDIR command or
from within an application by using Interrupt 21H Function 3BH (Change
Current Directory).
Directory Format
The root directory is created by the MS-DOS FORMAT program. See USER
COMMANDS: FORMAT. The FORMAT program places the root directory
immediately after the disk's file allocation tables (FATs). FORMAT
also determines the size of the root directory. The size depends on
the capacity of the storage medium: FORMAT places larger root
directories on high-capacity fixed disks and smaller root directories
on floppy disks. In contrast, the size of subdirectories is limited
only by the storage capacity of the disk because disk space for
subdirectories is allocated dynamically, as it is for any MS-DOS file.
The size and physical location of the root directory can be derived
from data in the BIOS parameter block (BPB) in the disk boot sector.
See PROGRAMMING IN THE MS-DOS ENVIRONMENT: STRUCTURE OF MS-DOS: MS-DOS
Storage Devices.
Because space for the root directory is allocated only when the disk
is formatted, the root directory cannot be deleted or moved.
Subdirectories, whose disk space is allocated dynamically, can be
added or deleted as needed.
Directory entry format
Each 32-byte directory entry consists of seven fields, including a
name, an attribute byte, date and time stamps, and information that
describes the file's size and physical location on the disk (Figure
8-2). The fields are formatted as described in the following
paragraphs.
Byte 0 0BH 0CH 16H 18H 1AH 1CH 1FH
┌─────┬──────────┬───────────┬─────┬─────┬─────────────────┬─────────┐
│Name │Attribute │(Reserved) │Time │Date │Starting cluster │File size│
└─────┴──────────┴───────────┴─────┴─────┴─────────────────┴─────────┘
Figure 8-2. Format of a directory entry.
The name field (bytes 0-0AH) contains an 11-byte name unless the first
byte of the field indicates that the directory entry is deleted or
null. The name can be an 11-byte filename (8-byte name followed by a
3-byte extension), an 11-byte subdirectory name (8-byte name followed
by a 3-byte extension), or an 11-byte volume label. Names less than 8
bytes and extensions less than 3 bytes are padded to the right with
blanks so that the extension always appears in bytes 08-0AH of the
name field. The first byte of the name field can contain certain
reserved values that affect the way MS-DOS processes the directory
entry:
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Value Meaning
──────────────────────────────────────────────────────────────────────
0 Null directory entry (logical end of directory in MS-DOS
versions 2.0 and later)
5 First character of name to be displayed as the character
represented by 0E5H (MS-DOS version 3.2 )
0E5H Deleted directory entry
When MS-DOS creates a subdirectory, it always includes two aliases as
the first two entries in the newly created directory. The name . (an
ASCII period) is an alias for the name of the current directory; the
name .. (two ASCII periods) is an alias for the directory's parent
directory--that is, the directory in which the entry containing the
name of the current directory is found.
The attribute field (byte 0BH) is an 8-bit field that describes the
way MS-DOS processes the directory entry (Figure 8-3). Each bit in the
attribute field designates a particular attribute of that directory
entry; more than one of the bits can be set at a time.
Bit 7 6 5 4 3 2 1 0
┌───────────┬───────────┬────────┬───────┬─────┬───────┬───────┬─────┐
│(Reserved) │(Reserved) │Archive │Sub- │Vol- │System │Hidden │Read-│
│ │ │ │direc- │ume │file │file │only │
│ │ │ │tory │label│ │ │file │
└───────────┴───────────┴────────┴───────┴─────┴───────┴───────┴─────┘
Figure 8-3. Format of the attribute field in a directory entry.
The read-only bit (bit 0) is set to 1 to mark a file read-only.
Interrupt 21H Function 3DH (Open File with Handle) will fail if it is
used in an attempt to open this file for writing. The hidden bit (bit
1) is set to 1 to indicate that the entry is to be skipped in normal
directory searches--that is, in directory searches that do not
specifically request that hidden entries be included in the search.
The system bit (bit 2) is set to 1 to indicate that the entry refers
to a file used by the operating system. Like the hidden bit, the
system bit excludes a directory entry from normal directory searches.
The volume label bit (bit 3) is set to 1 to indicate that the
directory entry represents a volume label. The subdirectory bit (bit
4) is set to 1 when the directory entry contains the name and location
of another directory. This bit is always set for the directory entries
that correspond to the current directory (.) and the parent directory
(..). The archive bit (bit 5) is set to 1 by MS-DOS functions that
close a file that has been written to. Simply opening and closing a
file is not sufficient to update the archive bit in the file's
directory entry.
The time and date fields (bytes 16-17H and 18-19H) are initialized by
MS-DOS when the directory entry is created. These fields are updated
whenever a file is written to. The formats of these fields are shown
in Figures 8-4 and 8-5.
Bit 15 10 4 0
┌──────────────────────┬───────────────────────┬───────────────────┐
│ Hours (0-23) │ Minutes (0-59) │ 2-second │
│ │ │ increments (0-29) │
└──────────────────────┴───────────────────────┴───────────────────┘
Figure 8-4. Format of the time field in a directory entry.
Bit 15 8 4 0
┌───────────────────────────────┬──────────────┬───────────────────┐
│ Year (relative to 1980) │ Month (1-12) │ Day (1-31) │
│ │ │ │
└───────────────────────────────┴──────────────┴───────────────────┘
Figure 8-5. Format of the date field in a directory entry.
The starting cluster field (bytes 1A-1BH) indicates the disk location
of the first cluster assigned to the file. This cluster number can be
used as an entry point to the file allocation table (FAT) for the
disk. (Cluster numbers can be converted to logical sector numbers with
the aid of the information in the disk's BPB.)
For the . entry (the alias for the directory that contains the entry),
the starting cluster field contains the starting cluster number of the
directory itself. For the .. entry (the alias for the parent
directory), the value in the starting cluster field refers to the
parent directory unless the parent directory is the root directory, in
which case the starting cluster number is zero.
The file size field (bytes 1C-1FH) is a 32-bit integer that indicates
the file size in bytes.
Volume Labels
The generic term volume refers to a unit of auxiliary storage such as
a floppy disk, a fixed disk, or a reel of magnetic tape. In computer
environments where many different volumes might be used, the operating
system can uniquely identify each volume by initializing it with a
volume label.
Volume labels are implemented in MS-DOS versions 2.0 and later as a
specific type of directory entry specified by setting bit 3 in the
attribute field to 1. In a volume label directory entry, the name
field contains an 11-byte string specifying a name for the disk
volume. A volume label can appear only in the root directory of a
disk, and only one volume label can be present on any given disk.
In MS-DOS versions 2.0 and later, the FORMAT command can be used with
the /V switch to initialize a disk with a volume label. In versions
3.0 and later, the LABEL command can be used to create, update, or
delete a volume label. Several commands can display a disk's volume
label, including VOL, DIR, LABEL, TREE, and CHKDSK. See USER COMMANDS.
In MS-DOS versions 2.x, volume labels are simply a convenience for the
user; no MS-DOS routine uses a volume label for any other purpose. In
MS-DOS versions 3.x, however, the SHARE command examines a disk's
volume label when it attempts to verify whether a disk volume has been
inadvertently replaced in the midst of a file read or write operation.
Removable disk volumes should therefore be assigned unique volume
names if they are to contain shared files.
Functional Support for MS-DOS Directories
Several Interrupt 21H service routines can be useful to programmers
who need to manipulate directories and their contents (Table 8-1). The
routines can be broadly grouped into two categories: those that use a
modified file control block (FCB) to pass filenames to and from the
Interrupt 21H service routines (Functions 11H, 12H, 17H, and 23H) and
those that use hierarchical path specifications (Functions 39H, 3AH,
3BH, 43H, 47H, 4EH, 4FH, 56H, and 57H). See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: File and Record Management;
SYSTEM CALLS: INTERRUPT 21H.
The functions that use an FCB require that the calling program
reserve enough memory for an extended FCB before the Interrupt 21H
function is called. The calling program initializes the filename and
extension fields of the FCB and passes the address of the FCB to the
MS-DOS service routine in DS:DX. The functions that use pathnames
expect all pathnames to be in ASCIIZ format--that is, the last
character of the name must be followed by a zero byte.
Names in pathnames passed to Interrupt 21H functions can be separated
by either a backslash (\) or a forward slash (/). (The forward slash
is the separator character used in pathnames in UNIX/XENIX systems.)
For example, the pathnames C:/MSP/SOURCE/ROSE.PAS and
C:\MSP\SOURCE\ROSE.PAS are equivalent when passed to an Interrupt 21H
function. The forward slash can thus be used in a pathname in a
program that must run on both MSDOS and UNIX/XENIX. However, the MS-
DOS comand processor (COMMAND.COM) recognizes only the backslash as a
pathname separator character, so forward slashes cannot be used as
separators in the command line.
Table 8-1. MS-DOS Functions for Accessing Directories.
╓┌────────────────────────┌───────────────────┌──────────────────────┌───────────────╖
Function Call With Returns Comment
───────────────────────────────────────────────────────────────────────────
Find First File AH = 11H AL = 0 (directory If default not
DS:DX = pointer entry found) or satisfactory,
to unopened FCB 0FFH (not found) DTA must be
INT 21H DTA updated (if set before
directory entry using this
found) function.
Find Next File AH = 12H AL = 0 (directory Use the same
DS:DX = pointer entry found) or FCB for Func-
to unopened FCB 0FFH (not found) tion 11H and
INT 21H DTA updated (if Function 12H.
directory entry
found)
Rename File AH = 17H AL = 0 (file renamed)
DS:DX = pointer or 0FFH (no directory
to modified FCB entry or duplicate
INT 21H filename)
Get File Size AH = 23H AL = 0 (directory
DS:DX = pointer entry found) or
to unopened FCB 0FFH (not found)
INT 21H FCB updated with
number of records
in file
Create AH = 39H Carry flag set
Directory DS:DX = pointer (if error)
to ASCIIZ AX = error code
pathname (if error)
INT 21H
Remove AH = 3AH Carry flag set
Directory DS:DX = pointer (if error)
to ASCIIZ AX = error code
pathname (if error)
INT 21H
Change Current AH = 3BH Carry flag set
Directory DS:DX = pointer (if error)
to ASCIIZ AX = error code
pathname (if error)
INT 21H
Get/Set File AH = 43H Carry flag set Cannot be
Attributes AL = 0 (get (if error) used to
attributes) AX = error code modify the
1 (set (if error) volume label
attributes) CX = attribute or subdirec-
CX = attributes field from direc- tory bits.
(if AL = 1) tory entry (if
DS:DX = pointer called with AL = 0)
to ASCIIZ
pathname
INT 21H
Get Current AH - 47H Carry flag set
Directory DS:SI = pointer (if error)
to 64-byte AX = error code
buffer (if error)
DL = drive Buffer updated with
number pathname of current
INT 21H directory
Find First File AH = 4EH Carry flag set If default
DS:DX = pointer (if error) not satisfac-
to ASCIIZ AX = error code tory, DTA
pathname (if error) must be set
CX = file DTA updated before using
attributes to this
match function.
INT 21H
Find Next File AH = 4FH Carry flag set
INT 21H (if error)
AX = error code
(if error)
DTA updated
Rename File AH = 56H Carry flag set
DS:DX = pointer (if error)
to ASCIIZ AX = error code
pathname (if error)
ES:DI = pointer
to new ASCIIZ
pathname
INT 21H
Get/Set Date AH = 57H Carry flag set
/Time of File AL = 0 (get (if error)
date/time) AX = error code
1 (set (if error)
date/time) CX = time
BX = handle (if AL = 0)
CX = time DX = date
(if AL = 1) (if AL = 0)
DX = date
(if AL = 1)
INT 21H
Searching a directory
Two pairs of Interrupt 21H functions are available for directory
searches. Functions 11H and 12H use FCBs to transfer filenames to
MS-DOS; these functions are available in all versions of MS-DOS, but
they cannot be used with pathnames. Functions 4EH and 4FH support
pathnames, but these functions are unavailable in MS-DOS versions 1.x.
All four functions require the address of the disk transfer area (DTA)
to be initialized appropriately before the function is invoked. When
Function 12H or 4FH is used, the current DTA must be the same as the
DTA for the preceding call to Function 11H or 4EH.
The Interrupt 21H directory search functions are designed to be used
in pairs. The Find First File functions return the first matching
directory entry in the current directory (Function 11H) or in the
specified directory (Function 4EH). The Find Next File functions
(Functions 12H and 4FH) can be called repeatedly after a successful
call to the corresponding Find First File function. Each call to one
of the Find Next File functions returns the next directory entry that
matches the name originally specified to the Find First File function.
A directory search can thus be summarized as follows:
call "find first file" function
while ( matching directory entry returned )
call "find next file" function
Wildcard characters
This search strategy is used because name specifications can include
the wildcard characters ?, which matches any single character, and *
(see below). When one or more wildcard characters appear in the name
specified to one of the Find First File functions, only the
nonwildcard characters in the name participate in the directory
search. Thus, for example, the specification FOO? matches the
filenames FOO1, FOO2, and so on; the specification FOO?????.???
matches FOO4.COM, FOOBAR.EXE, and FOONEW.BAK, as well as FOO1 and
FOO2; the specification ????????.TXT matches all files whose extension
is .TXT; the specification ????????.??? matches all files in the
directory.
Function 4EH also recognizes the wildcard character *, which matches
any remaining characters in a filename or extension. MS-DOS expands
the * wildcard character internally to question marks. Thus, for
example, the specification FOO* is the same as FOO?????; the
specification FOO*.* is the same as FOO?????.???; and, of course, the
specification *.* is the same as ????????.???.
Examining a directory entry
All four Interrupt 21H directory search functions return the name,
attribute, file size, time, and date fields for each directory entry
found during a directory search. The current DTA is used to return
this data, although the format is different for the two pairs of
functions: Functions 11H and 12H return a copy of the 32-byte
directory entry--including the cluster number--in the DTA; Functions
4EH and 4FH return a 43-byte data structure that does not include the
starting cluster number. See SYSTEM CALLS: INTERRUPT 21H: Function
4EH.
The attribute field of a directory entry can be examined using
Function 43H (Get/Set File Attributes). Also, Function 57H (Get/Set
Date/Time of File) can be used to examine a file's time or date.
However, unlike the other functions discussed here, Function 57H is
intended only for files that are being actively used within an
application--that is, Function 57H can be called to examine the file's
time or date stamp only after the file has been opened or created
using an Interrupt 21H function that returns a handle (Function 3CH,
3DH, 5AH, or 5BH).
Modifying a directory entry
Four Interrupt 21H functions can modify the contents of a directory
entry. Function 17H (Rename File) can be used to change the name field
in any directory entry, including hidden or system files,
subdirectories, and the volume label. Related Function 56H (Rename
File) also changes the name field of a filename but cannot rename a
volume label or a hidden or system file. However, it can be used to
move a directory entry from one directory to another. (This capability
is restricted to filenames only; subdirectory entries cannot be moved
with Function 56H.)
Functions 43H (Get/Set File Attributes) and 57H (Get/Set Date/Time of
File) can be used to modify specific fields in a directory entry.
Function 43H can mark a directory entry as a hidden or system file,
although it cannot modify the volume label or subdirectory bits.
Function 57H, as noted above, can be used only with a previously
opened file; it provides a way to read or update a file's time and
date stamps without writing to the file itself.
Creating and deleting directories
Function 39H (Create Directory) exists only to create directories--
that is, directory entries with the subdirectory bit set to 1.
(Interrupt 21H functions that create files, such as Function 3CH,
cannot assign the subdirectory attribute to a directory entry.) The
converse function, 3AH (Remove Directory), deletes a subdirectory
entry from a directory. (The subdirectory must be completely empty.)
Again, Interrupt 21H functions that delete files from directories,
such as Function 41H, cannot be used to delete subdirectories.
Specifying the current directory
A call to Interrupt 21H Function 47H (Get Current Directory) returns
the pathname of the current directory in use by MS-DOS to a user-
supplied buffer. The converse operation, in which a new current
directory can be specified to MS-DOS, is performed by Function 3BH
(Change Current Directory).
Programming examples: Searching for files
The subroutines in Figure 8-6 below illustrate Functions 4EH and 4FH,
which use path specifications passed as ASCIIZ strings to search for
files. Figure 8-7 applies these assembly-language subroutines in a
simple C program that lists the attributes associated with each entry
in the current directory. Note how the directory search is performed
in the WHILE loop in Figure 8-7 by using a global wildcard file
specification (*.*) and by repeatedly executing FindNextFile() until
no further matching filenames are found. (See Programming Example:
Updating a Volume Label for examples of the FCB-related search
functions, 11H and 21H.)
──────────────────────────────────────────────────────────────────────
Figure 8-6. Subroutines illustrating Interrupt 21H Functions 4EH and
4FH.
──────────────────────────────────────────────────────────────────────
Figure 8-7. The complete DIRDUMP.C program.
──────────────────────────────────────────────────────────────────────
Programming example: Updating a volume label
To create, modify, or delete a volume-label directory entry, the
Interrupt 21H functions that work with FCBs should be used. Figure 8-8
contains four subroutines that show how to search for, rename, create,
or delete a volume label in MS-DOS versions 2.0 and later.
──────────────────────────────────────────────────────────────────────
Figure 8-8. Subroutines for manipulating volume labels.
──────────────────────────────────────────────────────────────────────
Richard Wilton
Article 9: Memory Management
Personal computers that are MS-DOS compatible can be outfitted with as
many as three kinds of random-access memory (RAM): conventional
memory, expanded memory, and extended memory.
All MS-DOS machines have at least some conventional memory, but the
presence of expanded or extended memory depends on the installed
hardware options and the model of microprocessor on which the computer
is based. Each storage class has its own capabilities,
characteristics, and limitations. Each also has its own management
techniques, which are the subject of this chapter.
Conventional Memory
Conventional memory is the term for the up to 1 MB of memory that is
directly addressable by an Intel 8086/8088 microprocessor or by an
80286 or 80386 microprocessor running in real mode (8086-emulation
mode). Physical addresses for references to conventional memory are
generated by a 16-bit segment register, which acts as a base register
and holds a paragraph address, combined with a 16-bit offset contained
in an index register or in the instruction being executed.
On IBM PCs and compatibles, MS-DOS and the programs that run under its
control occupy the bottom 640 KB or less of the conventional memory
space. The memory space above the 640 KB mark is partitioned among ROM
(read-only memory) chips on the system board that contain various
primitive device handlers and test programs and among RAM and ROM
chips on expansion boards that are used for input and output buffers
and for additional device-dependent routines.
The bottom 640 KB of memory administered by MS-DOS is divided into
three zones (Figure 9-1):
■ The interrupt vector table
■ The operating system area
■ The transient program area
┌────────────────────────┐ 100000H (1 MB)
│ ROM BIOS │
│ additional ROM code │
│ on expansion boards, │
│ memory-mapped I/O │
└───┐ buffers │
┌──┐└─────┐ │
│ └─────┐└──────┐ │
│ └──────┐└───────┘
│ └────────┐
├────────────────────────┤ A0000H (640 KB)
│ │
│ │
│ Transient │
│ program area │
│ │
│ │
│ │
├────────────────────────┤ Boundary varies
│ MS-DOS and │
│ its buffers, tables, │
│ and device drivers │
├────────────────────────┤ 00400H (1 KB)
│ Interrupt vector table │
└────────────────────────┘ 00000H
Figure 9-1. A diagram showing conventional memory in an IBM PC-
compatible MS-DOS system. The bottom 1024 bytes of memory are used for
the interrupt vector table. The memory above the vector table, up to
the 640 KB boundary, is available for use by MS-DOS and the programs
that run under its control. The top 384 KB are used for the ROM BIOS,
other device-control and diagnostic routines, and memory-mapped input
and output.
The interrupt vector table occupies the lowest 1024 bytes of memory
(locations 00000003FFH); its address and length are hard-wired into
the processor and cannot be changed. Each doubleword position in the
table is called an interrupt vector and contains the segment and
offset of an interrupt handler routine for the associated hardware or
software interrupt number. Interrupt handler routines are usually
built into the operating system, but in special cases application
programs can contain handler routines of their own. Vectors for
interrupt numbers that are not used for software linkages or by some
hardware device are usually initialized by the operating system to
point to a simple interrupt return (IRET) instruction or to a routine
that displays an error message.
The operating-system area begins immediately above the interrupt
vector table and holds the operating system proper, its tables and
buffers, any additional installable device drivers specified in the
CONFIG.SYS file, and the resident portion of the COMMAND.COM command
interpreter. The amount of memory occupied by the operating-system
area varies with the version of MS-DOS being used, the number of disk
buffers, and the number and size of installed device drivers.
The transient program area (TPA) is the remainder of RAM above the
operating-system area, extending to the 640 KB limit or to the end of
installed RAM (whichever is smaller). External MS-DOS commands (such
as CHKDSK) and other programs are loaded into the TPA for execution.
The transient portion of COMMAND.COM also runs in this area.
The TPA is organized into a structure called the memory arena, which
is divided into portions called arena entries (or memory blocks).
These entries are allocated in paragraph (16-byte) multiples and can
be as small as one paragraph or as large as the entire TPA. Each arena
entry is preceded by a control structure called an arena entry header,
which contains information indicating the size and status of the arena
entry.
MS-DOS inspects the arena entry headers whenever a function requesting
a memory-block allocation, modification, or release is issued; when a
program is loaded and executed with the EXEC function (Interrupt 21H
Function 4BH); or when a program is terminated. If any of the arena
entry headers appear to be damaged, MS-DOS returns an error to the
calling process. If that process is COMMAND.COM, COMMAND.COM then
displays the message Memory allocation error and halts the system.
MS-DOS support for conventional memory management
The MS-DOS kernel supports three memory-management functions, invoked
with Interrupt 21H, that operate on the TPA:
■ Function 48H (Allocate Memory Block)
■ Function 49H (Free Memory Block)
■ Function 4AH (Resize Memory Block)
These three functions (Table 9-1) can be called by application
programs, by the command processor, and by MS-DOS itself to
dynamically allocate, resize, and release arena entries as they are
needed. See SYSTEM CALLS: INTERRUPT 21H: Functions 48H; 49H; 4AH.
Table 9-1. MS-DOS Memory-Management Functions.
╓┌───────────────────────────┌───────────────────────┌───────────────────────╖
Function Name Call With Returns
──────────────────────────────────────────────────────────────────
Allocate Memory Block AH = 48H AX = segment of
BX = paragraphs allocated block
needed If failed:
BX = size of largest
available block in
paragraphs
Free Memory Block AH = 49H nothing
ES = segment of block
to release
Resize (Allocated) AH = 4AH If failed:
Memory Block BX = new size of BX = maximum size
block in paragraphs for block in para-
ES = segment of block graphs
to resize
Get/Set Allocation AH = 58H If getting:
Strategy AL = 00H (get strategy) AX = strategy code
= 01H (set strategy)
If setting:
BX = strategy:
00H = first fit
01H = best fit
02H = last fit
When the MS-DOS kernel receives a memory-allocation request, it
inspects the chain of arena entry headers to find a free arena entry
that can satisfy the request. The memory manager can use any of three
allocation strategies:
■ First fit-the arena entry at the lowest address that is large
enough to satisfy the request
■ Best fit-the smallest available arena entry that satisfies the
request, regardless of its position
■ Last fit-the arena entry at the highest address that is large
enough to satisfy the request
If the arena entry selected is larger than the size needed to fulfill
the request, the arena entry is divided and the program is given an
arena entry exactly the size it requires. A new arena entry header is
then created for the remaining portion of the original arena entry; it
is marked "unowned" and can be used to satisfy subsequent allocation
calls.
Research on allocation strategies has demonstrated that the first-fit
approach is most efficient, and this is the default strategy used by
MS-DOS. However, in MS-DOS versions 3.0 and later, an application
program can select a different strategy for the memory manager with
Interrupt 21H Function 58H (Get/Set Allocation Strategy). See SYSTEM
CALLS: INTERRUPT 21H: Function 58H.
Using the memory-management functions
When a program begins executing, it already owns two arena entries
allocated on its behalf by the MS-DOS EXEC function (Interrupt 21H
Function 4BH). The first entry holds the program's environment and is
just large enough to contain this information; the second entry
(called the program block in this article) contains the program's PSP,
code, data, and stack.
The amount of memory MS-DOS allocates to the program block for a newly
loaded transient program depends on its type (.COM or .EXE). Under
typical conditions, a .COM program is allocated all of the first arena
entry that is large enough to hold the contents of its file, plus 256
bytes for the PSP and at least 2 bytes for the stack. Because the TPA
is seldom fragmented into more than one arena entry before a program
is loaded, a .COM program usually ends up owning all the memory in the
system that does not belong to the operating system itself--memory
divided between a relatively small environment and a comparatively
immense program block.
The amount of memory allocated to a .EXE program, on the other hand,
is controlled by two fields called MINALLOC and MAXALLOC in the .EXE
program file header. The MINALLOC field tells the MS-DOS loader how
many paragraphs of memory, in addition to the memory required to hold
the initialized code and the data present in the file, must be
available for the program to execute at all. The MAXALLOC field
contains the maximum number of excess paragraphs, if available, to
allocate to the program.
The default value placed in MAXALLOC by the Microsoft Object Linker is
FFFFH paragraphs, corresponding to 1 MB. Consequently, a .EXE program
is typically allocated all of available memory when it is loaded, as
is a .COM file. Although it is possible to set the MAXALLOC field to
other, smaller values with the linker's /CPARMAXALLOC switch or with
the EXEMOD utility supplied with Microsoft language compilers, few
programmers bother to do so.
In short, when a program begins executing, it usually owns all of
available memory-frequently much more memory than it needs. If the
program wants to be well behaved in its use of memory and, possibly,
load child programs as well, it should immediately release any extra
memory. In assembly-language programs, the extra memory is released by
calling Interrupt 21H Function 4AH (Resize Memory Block) with the
segment of the program's PSP in the ES register and the number of
paragraphs of memory to retain for the program's use in the BX
register. (See Figures 9-2 and 9-3.) In most high-level languages,
such as Microsoft C, excess memory is released by the run-time
library's startup module.
──────────────────────────────────────────────────────────────────────
Figure 9-2. An example of a .COM program releasing excess memory after
it receives control from MS-DOS. Interrupt 21H Function 4AH is called
with the segment address of the program's PSP in register ES and the
number of paragraphs of memory to retain in register BX.
──────────────────────────────────────────────────────────────────────
Figure 9-3. An example of a .EXE program releasing excess memory after
it receives control from MS-DOS. This particular code sequence depends
on the segment order shown. When a .EXE program is linked from many
different object modules, other techniques may be needed to determine
the amount of memory occupied by the program at run time.
──────────────────────────────────────────────────────────────────────
Later, if the transient program needs additional memory for a buffer,
table, or other work area, it can call Interrupt 21H Function 48H
(Allocate Memory Block) with the desired number of paragraphs. If a
sufficiently large block of memory is available, MS-DOS creates a new
arena entry of the requested size and returns a pointer to its base in
the form of a segment address in the AX register. If an arena entry of
the requested size cannot be created, MS-DOS returns an error code in
the AX register and the size in paragraphs of the largest available
block of memory in the BX register. The application program can
inspect this value to determine whether it can continue in a degraded
fashion with a smaller amount of memory.
When a program finishes using an allocated arena entry, it should
promptly call Interrupt 21H Function 49H to release it. This allows
MS-DOS to collect small blocks of freed memory into contiguous arena
entries and reduces the chance that future allocation requests by the
same program will fail because of memory fragmentation. In any case,
all arena entries owned by a program are released when the program
terminates with Interrupt 20H or with Interrupt 21H Function 00H or
4CH.
A program skeleton demonstrating the use of dynamic memory allocation
services is shown in Figure 9-4.
──────────────────────────────────────────────────────────────────────
Figure 9-4. A skeleton example of dynamic memory allocation. The
program requests a 32 KB memory block, uses it to copy its working
file to a backup file, and then releases the memory block. Note the
use of ASSUME directives to force the assembler to generate proper
segment overrides on references to variables containing file handles.
──────────────────────────────────────────────────────────────────────
Expanded Memory
The original Expanded Memory Specification (EMS) version 3.0 was
developed as a joint effort of Lotus Development Corporation and Intel
Corporation and was announced at the Spring COMDEX in 1985. The EMS
was designed to provide a uniform means for applications running on
8086/8088-based personal computers, or on 80286/80386-based computers
in real mode, to circumvent the 1 MB limit on conventional memory,
thus providing such programs with much larger amounts of fast random-
access storage. The EMS version 3.2, modified from 3.0 to add support
for multitasking operating systems, was released shortly afterward as
a joint effort of Lotus, Intel, and Microsoft.
The EMS is a functional definition of a bank-switched memory
subsystem; it consists of user-installable boards that plug into the
IBM PC's expansion bus and a resident driver program called the
Expanded Memory Manager (EMM) that is provided by the board
manufacturer. As much as 8 MB of expanded memory can be installed in a
single machine. Expanded memory is made available to application
software in 16 KB pages, which are mapped by the EMM into a contiguous
64 KB area called the page frame somewhere above the conventional
memory area used by MS-DOS (0-640 KB). An application program can thus
access as many as four 16 KB expanded memory pages simultaneously. The
location of the page frame is user configurable so that it will not
conflict with other hardware options (Figure 9-5).
Expanded memory
┌────────────────┐ 8 MB
Conventional memory ┌──├────────────────┤
1 MB ┌────────────────┐ │ ├────────────────┤
│ │ │ ├───────────┬────┘
│ ROM BIOS etc. │ │ ├────┬──────┘┌───┐
├────────────────┤ │ └────┘┌──────┴───┤
│ │ │┌─┌─────┴──────────┤
├────────────────┤ ││ ├────────────────┤
▒├────────────────┤─┘│ ├────────────────┤
EMS page frame ─▒├────────────────┤──┘ ├────────────────┤
(four 16 KB pages) ▒├────────────────┤──┐ ├────────────────┤
├────────────────┤─┐│ ├────────────────┤
│ │ │└─├────────────────┤
640 KB ├────────────────┤ │ ├────────────────┤
│ Transient │ │ ├────────────────┤
│ program area │ │ ├────────────────┤
│ │ │ ├────────────────┤
├────────────────┤ │ ├────────────────┤
│ │ │ ├────────────────┤
│ MS-DOS │ │ ├────────────────┤
00400H ├────────────────┤ │ ├────────────────┤
│ Interrupt │ └──├────────────────┤
│ vector table │ ├────────────────┤
0 └────────────────┘ └────────────────┘ 0
Figure 9-5. A sketch of the relationship of expanded memory to
conventional memory; 16 KB pages of expanded memory are mapped into a
64 KB area, called the page frame, above the 640 KB boundary. The
location of the page frame can be configured by the user to eliminate
conflicts with ROMs or I/O buffers on expansion boards.
The Expanded Memory Manager
The Expanded Memory Manager provides a hardware-independent interface
between application programs and the expanded memory board(s). The EMM
is supplied by the board manufacturer in the form of an installable
character-device driver and is linked into MS-DOS by a DEVICE
directive added to the CONFIG.SYS file on the system startup disk.
Internally, the EMM is divided into two distinct components that can
be referred to as the driver and the manager. The driver portion
mimics some of the actions of a genuine installable device driver, in
that it includes Initialization and Output Status subfunctions and a
valid device header. See PROGRAMMING IN THE MS-DOS ENVIRONMENT:
CUSTOMIZING MS-DOS: Installable Device Drivers.
The second, and major, element of the EMM is the true interface
between application software and the expanded memory hardware. Several
classes of services provide
■ Status of the expanded memory subsystem
■ Allocation of expanded memory pages
■ Mapping of logical pages into physical memory
■ Deallocation of expanded memory pages
■ Support for multitasking operating systems
■ Diagnostic routines
Application programs communicate with the EMM directly by means of a
software interrupt (Interrupt 67H). The MS-DOS kernel does not take
part in expanded memory manipulations and does not use expanded memory
for its own purposes.
Checking for expanded memory
Before it attempts to use expanded memory for storage, an application
program must establish that the EMM is present and functional, and
then it must use the manager portion of the EMM to check the status of
the memory boards themselves. There are two methods a program can use
to test for the existence of the EMM.
The first method is to issue an Open File or Device request (Interrupt
21H Function 3DH) using the guaranteed device name of the EMM driver:
EMMXXXX0. If the open operation succeeds, one of two conditions is
indicated--either the driver is present or a file with the same name
exists in the current directory of the default disk drive. To rule out
the latter possibility, the application can issue IOCTL Get Device
Information (Interrupt 21H Function 44H Subfunction 00H) and Check
Output Status (Interrupt 21H Function 44H Subfunction 07H) requests to
determine whether the handle returned by the open operation is
associated with a file or with a device. In either case, the handle
that was obtained from the open function should then be closed
(Interrupt 21H Function 3EH) so that it can be reused for another file
or device.
The second method of testing for the driver is to use the address that
is found in the vector for Interrupt 67H to inspect the device header
of the presumed EMM. (The contents of the vector can be obtained
conveniently with Interrupt 21H Function 35H.) If the EMM is present,
the name field at offset 0AH of the device header contains the string
EMMXXXX0. This method is nearly foolproof, and it avoids the
relatively high overhead of an MS-DOS open function. However, it is
somewhat less well behaved because it involves inspection of memory
that does not belong to the application.
The two methods of testing for the existence of the EMM are
illustrated in Figures 9-6 and 9-7.
──────────────────────────────────────────────────────────────────────
Figure 9-6. Testing for the presence of the Expanded Memory
Manager with the MS-DOS Open File or Device (Interrupt 21H Function
3DH) and IOCTL (Interrupt 21H Function 44H) functions.
──────────────────────────────────────────────────────────────────────
Figure 9-7. Testing for the presence of the Expanded Memory
Manager by inspecting the name field in the device driver header.
──────────────────────────────────────────────────────────────────────
Using expanded memory
After establishing that the EMM is present, the application program
can bypass MS-DOS and communicate with the EMM directly by means of
software Interrupt 67H. The calling sequence is as follows:
mov ah,function ; AH selects EMM function
. ; Load other registers with
. ; values specific to the
. ; requested service
int 67h ; Transfer to EMM
In general, the ES:DI registers are used to pass the address of a
buffer or an array, and the DX register is used to hold an expanded
memory "handle." Some EMM functions also use other registers (chiefly
AL and BX) to pass such information as logical and physical page
numbers. Table 9-2 summarizes the services available from the EMM.
Upon return from an EMM function call, the AH register contains zero
if the function was successful; otherwise, AH contains an error code
with the most significant bit set (Table 9-3). Other values are
typically returned in the AL and BX registers or in a user-specified
buffer.
Table 9-2. MS-DOS Summary of the Software Interface to
Application Programs Provided by the EMM.
╓┌───────────────────────┌────────────────────────┌─────────────────────┌────────────────────────┌────────────────────────────────────────────────────────────────╖
Function Call
Name Action With Returns Comments
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Get Manager Test whether the AH - 40H AH = status This call is used after the program has established, with
Status expanded memory one of the techniques presented in Figures 9-6 and 9-7 ,
software and hardware that the EMM is present.
are functional.
Get Page Obtain the segment AH = 41H AH = status The page frame is divided into four 16 KB pages that are
Frame Segment address of the EMM page BX = segment of page used to map logical expanded memory pages into the
frame. frame, if AH = 00H physical memory space of the 8086/8088 processor.
Get Expanded Obtain an EMM handle AH = 43H AH = status The application need not have already acquired an EMM
Memory Pages of logical expanded BX = unallocated EMM handle to use this function.
memory pages present pages, if AH = 00H
in the system and the DX = total EMM pages in
number of pages that are system
not already allocated.
Allocate Obtain an EMM handle AH = 43H AH = status This function is equivalent to a file-open function for the
Expanded and allocate logical BX = logical pages DX = handle, if EMM. The handle returned is analogous to a file handle
Memory pages to be controlled to allocate AH = 00H and owns a certain number of EMM pages. The handle
by that handle. must be used with every subsequent request to map
memory and must be released by a close operation when
the application is finished.
This function can fail because either the available EMM
handles or the EMM pages have been exhausted.
Function 42H can be called by the application to
determine the actual number of pages available.
Map Memory Map one of the logical AH = 44H AH = status The logical page number must be in the range 0-n-1,
pages of expanded AL = physical page where n is the number of logical pages previously
memory assigned to a (0-3) allocated to the EMM handle with Function 43H.
handle onto one of the BX = logical page
four physical pages (0...n-1) To access the memory after it has been mapped to a
within the EMM's page DX = EMM handle physical page, the application also needs the segment of
frame. the EMM's page frame, which can be obtained with
Function 41H.
Release Handle Deallocate the logical AH = 45H AH = status This function is the equivalent of a close operation on
and Memory pages of expanded DX = EMM handle a file. It notifies the EMM that the application will not be
memory currently making further use of the data it may have stored within
assigned to a handle expanded memory pages.
and then release the
handle itself for reuse.
Get EMM Return the version AH = 46H AH = status The returned value is the version of the EMM with which
Version number of the EMM AL = EMM version, the driver complies. The version number is encoded as
software. if AH = 00H BCD, with the integer part in the upper 4 bits and the
fractional part in the lower 4 bits.
Save Mapping Save the contents of AH = 47H AH = status This function is designed for use by interrupt handlers
Context the expanded memory DX = EMM handle and resident drivers or utilities that must access expanded
page-mapping registers memory. The handle supplied to the function is the
on the expanded memory handle that was assigned to the interrupt handler during
boards, associating its initialization sequence, not to the program that was
those contents with a interrupted.
specific EMM handle.
Restore Restore the contents AH = 48H AH = status Use of this function must be balanced by a previous call
Mapping of all expanded memory DX = EMM handle to EMM Function 47H. It allows an interrupt handler or a
Context hardware page-mapping resident driver that used expanded memory to restore the
registers to the values mapping context to its state at the point of interruption.
associated with the
given handle.
Get Number of Return the number of AH = 4BH AH = status If the number of handles returned is zero, none of the
EMM Handles active EMM handles. BX = number of EMM expanded memory is in use. The number of active EMM
handles, if handles never exceeds 255.
AH =00H
A single program can make several allocation requests
and therefore own several EMM handles.
Get Pages Return the number AH = 4CH AH = status The number of pages returned if the function is success-
Owned by of logical expanded DX = EMM handle BX = logical pages, ful is always in the range 1-512. An EMM handle never
Handle memory pages allocated if AH = 00H has zero pages of memory allocated to it.
to a specific handle.
Get Pages for Return an array that AH = 4DH AH = status The array is filled in with doubleword entries. The first
All Handles contains all the active DI = offset of array BX = number of active word of each entry contains a handle; the second word
handles and the number to receive EMM handles contains the number of pages associated with that handle.
of logical expanded information The value returned in BX gives the number of valid
memory pages associated ES = array segment If AH = 00H, array is doubleword entries in the array.
with each handle. filled in as
described in Because 255 is the maximum number of EMM handles,
comments column. the array need not be larger than 1020 bytes.
Get/Set Save or set the AH = 4EH AH = status Subfunctions:
Page Map contents of the EMM AL = subfunction AL = bytes in page- 00H = get mapping registers into array
page-mapping registers number mapping array 01H = set mapping registers from array
on the expanded memory DS:SI = array (Subfunction 03H) 02H = get and set mapping registers in one operation
boards. holding mapping 03H = return needed size of page-mapping array
information Array pointed to by
(Subfunctions ES:DI receives map- This function was added in EMM version 3.2 and is
01H, 02H) ping information for designed to support multitasking. It should not ordinarily
ES:DI = array to Subfunctions 00H and be used by application programs.
receive information 02H
(Subfunctions 00H, The content of the array is hardware and EMM software
02H) dependent. In addition to the contents of the page-
mapping registers, it may contain other information that
is necessary to restore the expanded memory subsystem
to its previous state.
Table 9-3. The Expanded Memory Manager (EMM) Error Codes.
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Error Code Significance
──────────────────────────────────────────────────────────────────
00H Function was successful.
80H Internal error in the EMM software. Possible causes
include an error in the driver itself or damage to
its memory image.
81H Malfunction in the expanded memory hardware.
82H EMM is busy.
83H Invalid expanded memory handle.
84H Function requested by the application is not
supported by the EMM.
85H No more expanded memory handles available.
86H Error in save or restore of mapping context.
87H Allocation request specified more logical pages than
are available in the system; no pages were
allocated.
88H Allocation request specified more logical pages than
are currently available in the system (the request
does not exceed the physical pages that exist, but
some are already allocated to other handles); no
pages were allocated.
89H Zero pages cannot be allocated.
8AH Logical page requested for mapping is outside the
range of pages assigned to the handle.
8BH Illegal physical page number in mapping request (not
in the range 0-3).
8CH Save area for mapping contexts is full.
8DH Save of mapping context failed because save area
already contains a context associated with the
requested handle.
8EH Restore of mapping context failed because save area
does not contain a context for the requested handle.
8FH Subfunction parameter not defined.
An application program that uses expanded memory should regard that
memory as a system resource, such as a file or a device, and use only
the documented EMM services to allocate, access, and release expanded
memory pages. Here is the general strategy that can be used by such a
program:
1. Establish the presence of the EMM by one of the two methods
demonstrated in Figures 9-6 and 9-7.
2. After the driver is known to be present, check its operational
status with EMM Function 40H.
3. Check the version number of the EMM with EMM Function 46H to ensure
that all services the application will request are available.
4. Obtain the segment of the page frame used by the EMM with EMM
Function 41H.
5. Allocate the desired number of expanded memory pages with EMM
Function 43H. If the allocation is successful, the EMM returns a
handle in DX that is used by the application to refer to the
expanded memory pages it owns. This step is exactly analogous to
opening a file and using the handle obtained from the open function
for subsequent read/write operations on the file.
6. If the requested number of pages is not available, query the EMM
for the actual number of pages available (EMM Function 42H) and
determine whether the program can continue.
7. After successfully allocating the number of expanded memory pages
needed, use EMM Function 44H to map logical pages in and out of the
physical page frame, to store and retrieve data in expanded memory.
8. When finished using the expanded memory pages, release them by
calling EMM Function 45H. Otherwise, the pages will not be
available for use by other programs until the system is restarted.
A program skeleton that illustrates this general approach to the use
of expanded memory is shown in Figure 9-8.
──────────────────────────────────────────────────────────────────────
Figure 9-8. A program skeleton for the use of expanded memory. This
code assumes that the presence of the Expanded Memory Manager has
already been verified with one of the techniques shown in Figures 9-6
and 9-7.
──────────────────────────────────────────────────────────────────────
An interrupt handler or resident driver that uses the EMM follows the
same general procedure outlined in steps 1 through 8, with a few minor
variations. It may need to acquire an EMM handle and allocate pages
before the operating system is fully functional; in particular, the
MS-DOS services Open File or Device (Interrupt 21H Function 3DH),
IOCTL (Interrupt 21H Function 44H), and Get Interrupt Vector
(Interrupt 21H Function 35H) cannot be assumed to be available. Thus,
such a handler or driver must use a modified version of the "get
interrupt vector" technique to test for the existence of the EMM,
fetching the contents of the Interrupt 67H vector directly instead of
using MS-DOS Interrupt 21H Function 35H.
A device driver or interrupt handler typically owns its expanded
memory pages on a permanent basis (until the system is restarted) and
never deallocates them. Such a program must also take care to save
(EMM Function 47H) and restore (EMM Function 48H) the EMM's page-
mapping context (the EMM pages mapped into the page frame at the time
the device driver or interrupt handler takes control of the system) so
that use of the expanded memory by a foreground program will not be
disturbed.
The EMM relies heavily on the good behavior of application software to
avoid the corruption of expanded memory. If several applications that
use expanded memory are running under a multitasking manager, such as
Microsoft Windows, and one or more of those applications does not
abide strictly by the EMM's conventions, the data stored in expanded
memory can be corrupted.
Extended Memory
Extended memory is that storage at addresses above 1 MB (100000H) that
can be accessed by an 80286 or 80386 microprocessor running in
protected mode. IBM PC/AT-compatible machines can (theoretically) have
as much as 15 MB of extended memory installed, in addition to the
usual 1 MB of conventional memory address space. Unlike expanded
memory, extended memory is linearly addressable: The address of each
memory cell is fixed, so no special manager program is required.
Protected-mode operating systems, such as Microsoft XENIX and MS OS/2,
can use extended memory for execution of programs. MS-DOS, on the
other hand, runs in real mode on an 80286 or 80386, and programs
running under its control cannot ordinarily execute from extended
memory or even address that memory for storage of data.
To provide some access to extended memory for real-mode programs, IBM
PC/ATcompatible machines contain two routines in their ROM BIOS
(Tables 9-4 and 9-5) that allow the amount of extended memory present
to be determined (Interrupt 15H Function 88H) and that transfer blocks
of data between conventional memory and extended memory (Interrupt
15H Function 87H). These routines can be used by electronic disks
(RAMdisks) and by other programs that wish to use extended memory for
fast storage and retrieval of information that would otherwise have to
be written to a slower physical disk drive.
Table 9-4. IBM PC/AT ROM BIOS Interrupt 15H Functions for
Access to Extended Memory.
╓┌─────────────────────────────┌─────────────────────────┌───────────────────╖
Interrupt 15H Function Call With Returns
──────────────────────────────────────────────────────────────────────
Move Extended AH = 87H Carry flag =
Memory Block CX = length (words) 0 if successful
1 if error
ES:SI = address of block AH = status:
= move descriptor 00H no error
= table 01H RAM parity
error
02H exception
interrupt error
03H gate address
line 20 failed
Obtain Size of AH = 88H AX = kilobytes of
Extended Memory memory installed
above 1 MB
Table 9-5. Block Move Descriptor Table Format for IBM PC/AT ROM BIOS
Interrupt 15H Function 87H (Move Extended Memory Block).
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Bytes Contents
──────────────────────────────────────────────────────────────────
00-0FH Zero
10-11H Segment length in bytes (2*CX-1 or greater)
12-14H 24-bit source address
15H Access rights byte (93H)
16-17H Zero
18-19H Segment length in bytes (2*CX-1 or greater)
1A-1CH 24-bit destination address
1DH Access rights byte (93H)
1E-1FH Zero
20-2FH Zero
Note: This data structure actually constitutes a global descriptor
table (GDT) to be used by the CPU while it is running in protected
mode; the zero bytes at offsets 0-0FH and 20-2FH are filled in by the
ROM BIOS code before the mode transition. The supplied 24-bit address
is a linear address in the range 000000-FFFFFFH (not a segment and
offset), with the least significant byte first and the most
significant byte last.
Programmers should use these ROM BIOS routines with caution. Data
stored in extended memory is volatile; it is lost if the machine is
turned off. The transfer of data to or from extended memory involves a
switch from real mode to protected mode and back again. This is a
relatively slow process on 80286-based machines; in some cases it is
only marginally faster than actually reading the data from a fixed
disk. In addition, programs that use the ROM BIOS extended memory
functions are not compatible with the MS-DOS 3.x Compatibility Box of
MS OS/2, nor are they reliable if used for communications or
networking.
Finally, a major deficit in these ROM BIOS functions is that they do
not make any attempt to arbitrate between two or more programs or
device drivers that are using extended memory for temporary storage.
For example, if an application program and an installed RAMdisk driver
attempt to put data in the same area of extended memory, no error is
returned to either program, but the data belonging to one or both may
be destroyed.
Figure 9-9 demonstrates the use of the ROM BIOS routines to transfer a
block of data from extended memory to conventional memory.
──────────────────────────────────────────────────────────────────────
Figure 9-9. Demonstration of a block move from extended memory to
conventional memory using the ROM BIOS routine. The procedure
getblk accepts a source address in extended memory, a destination
address in conventional memory, a length in bytes, and the segment
and offset of a block move descriptor table. The extended-memory
address is a linear 32-bit address, of which only the lower 24 bits
are significant; the conventional-memory address is a segment and
offset. The getblk routine converts the destination segment and offset
to a linear address, builds the appropriate fields in the block move
descriptor table, invokes the ROM BIOS routine to perform the
transfer, and returns the status in the AH register.
──────────────────────────────────────────────────────────────────────
Summary
Personal computers that run MS-DOS can support as many as three
different types of fast, random-access memory (RAM). Each type has
specific characteristics and requires different techniques for its
management.
Conventional memory is the term used for the 1 MB of linear address
space that can be accessed by an 8086 or 8088 microprocessor or by an
80286 or 80386 microprocessor running in real mode. MS-DOS and the
programs that execute under its control run in this address space.
MS-DOS provides application programs with services to dynamically
allocate and release blocks of conventional memory.
As much as 8 MB of expanded memory can be installed in a PC and used
for electronic disks, disk caching, and storage of application program
data. The memory is made available in 16 KB pages and is administered
by a driver program called the Expanded Memory Manager, which provides
allocation, mapping, deallocation, and multitasking support.
Extended memory refers to the memory at addresses above 1 MB that can
be accessed by an 80286-based or 80386-based microprocessor running in
protected mode; it is not available in PCs based on the 8086 or 8088
microprocessors. As much as 15 MB of extended memory can be installed;
however, the ROM BIOS services to access the memory are primitive and
slow, and no manager is provided to arbitrate between multiple
programs that attempt to use the same extended memory addresses for
storage.
Ray Duncan
Article 10: The MS-DOS EXEC Function
The MS-DOS system loader, which brings .COM or .EXE files from disk
into memory and executes them, can be invoked by any program with the
MS-DOS EXEC function (Interrupt 21H Function 4BH). The default MS-DOS
command interpreter, COMMAND.COM, uses the EXEC function to load and
run its external commands, such as CHKDSK, as well as other
application programs. Many popular commercial programs, such as
databases and word processors, use EXEC to load and run subsidiary
programs (spelling checkers, for example) or to load and run a second
copy of COMMAND.COM. This allows a user to run subsidiary programs or
enter MS-DOS commands without losing his or her current working
context.
When EXEC is used by one program (called the parent) to load and run
another (called the child), the parent can pass certain information to
the child in the form of a set of strings called the environment, a
command line, and two file control blocks. The child program also
inherits the parent program's handles for the MS-DOS standard devices
and for any other files or character devices the parent has opened
(unless the open operation was performed with the "noninheritance"
option). Any operations performed by the child on inherited handles,
such as seeks or file I/O, also affect the file pointers associated
with the parent's handles. A child program can, in turn, load another
program, and the cycle can be repeated until the system's memory area
is exhausted.
Because MS-DOS is not a multitasking operating system, a child program
has complete control of the system until it has finished its work; the
parent program is suspended. This type of processing is sometimes
called synchronous execution. When the child terminates, the parent
regains control and can use another system function call (Interrupt
21H Function 4DH) to obtain the child's return code and determine
whether the program terminated normally, because of a critical
hardware error, or because the user entered a Control-C.
In addition to loading a child program, EXEC can also be used to load
subprograms and overlays for application programs written in assembly
language or in a high-level language that does not include an overlay
manager in its run-time library. Such overlays typically cannot be run
as self-contained programs; most require "helper" routines or data in
the application's root segment.
The EXEC function is available only with MS-DOS versions 2.0 and
later. With MS-DOS versions 1.x, a parent program can use Interrupt
21H Function 26H to create a program segment prefix for a child but
must carry out the loading, relocation, and execution of the child's
code and data itself, without any assistance from the operating
system.
How EXEC Works
When the EXEC function receives a request to execute a program, it
first attempts to locate and open the specified program file. If the
file cannot be found, EXEC fails immediately and returns an error code
to the caller.
If the file exists, EXEC opens the file, determines its size, and
stop inspects the first block of the file. If the first 2 bytes of the
block are the ASCII characters MZ, the file is assumed to contain a
.EXE load module, and the sizes of the program's code, data, and stack
segments are obtained from the .EXE file header. Otherwise, the entire
file is assumed to be an absolute load image (a .COM program). The
actual filename extension (.COM or .EXE) is ignored in this
determination.
At this point, the amount of memory needed to load the program is
known, so EXEC attempts to allocate two blocks of memory: one to hold
the new program's environment and one to contain the program's code,
data, and stack segments. Assuming that enough memory is available to
hold the program itself, the amount actually allocated to the program
varies with its type. Programs of the .COM type are usually given all
the free memory in the system (unless the memory area has previously
become fragmented), whereas the amount assigned to a .EXE program is
controlled by two fields in the file header, MINALLOC and MAXALLOC,
that are set by the Microsoft Object Linker (LINK). See PROGRAMMING IN
THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: Structure of an
Application Program; PROGRAMMING TOOLS: The Microsoft Object Linker;
PROGRAMMING UTILITIES: LINK.
EXEC then copies the environment from the parent into the memory
allocated for child's environment, builds a program segment prefix
(PSP) at the base of the child's program memory block, and copies into
the child's PSP the command tail and the two default file control
blocks passed by the parent. The previous contents of the terminate
(Interrupt 22H), Control-C (Interrupt 23H), and critical error
(Interrupt 24H) vectors are saved in the new PSP, and the terminate
vector is updated so that control will return to the parent program
when the child terminates or is aborted.
The actual code and data portions of the child program are then read
from the disk file into the program memory block above the newly
constructed PSP. If the child is a .EXE program, a relocation table in
the file header is used to fix up segment references within the
program to reflect its actual load address.
Finally, the EXEC function sets up the CPU registers and stack
according to the program type and transfers control to the program.
The entry point for a .COM file is always offset 100H within the
program memory block (the first byte following the PSP). The entry
point for a .EXE file is specified in the file header and can be
anywhere within the program. See also PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: Structure of an Application
Program.
When EXEC is used to load and execute an overlay rather than a child
program, its operation is much simpler than described above. For an
overlay, EXEC does not attempt to allocate memory or build a PSP or
environment. It simply loads the contents of the file at the address
specified by the calling program and performs any necessary
relocations (if the overlay file has a .EXE header), using a segment
value that is also supplied by the caller. EXEC then returns to the
program that invoked it, rather than transferring control to the code
in the newly loaded file. The requesting program is responsible for
calling the overlay at the appropriate location.
Using EXEC to Load a Program
When one program loads and executes another, it must follow these
steps:
1. Ensure that enough free memory is available to hold the code, data,
and stack of the child program.
2. Set up the information to be passed to EXEC and the child program.
3. Call the MS-DOS EXEC function to run the child program.
4. Recover and examine the child program's termination and return
codes.
Making memory available
MS-DOS typically allocates all available memory to a .COM or .EXE
program when it is loaded. (The infrequent exceptions to this rule
occur when the transient program area is fragmented by the presence of
resident data or programs or when a .EXE program is loaded that was
linked with the /CPARMAXALLOC switch or modified with EXEMOD.)
Therefore, before a program can load another program, it must free any
memory it does not need for its own code, data, and stack.
The extra memory is released with a call to the MS-DOS Resize Memory
Block function (Interrupt 21H Function 4AH). In this case, the segment
address of the parent's PSP is passed in the ES register, and the BX
register holds the number of paragraphs of memory the program must
retain for its own use. If the prospective parent is a .COM program,
it must be certain to move its stack to a safe area if it is reducing
its memory allocation to less than 64 KB.
Preparing parameters for EXEC
When used to load and execute a program, the EXEC function must be
supplied with two principal parameters:
■ The address of the child program's pathname
■ The address of a parameter block
The parameter block, in turn, contains the addresses of information to
be passed to the child program.
The program name
The pathname for the child program must be an unambiguous, null-
terminated (ASCIIZ) file specification (no wildcard characters). If
a path is not included, the current directory is searched for the
program; if a drive specifier is not present, the default drive is
used.
The parameter block
The parameter block contains the addresses of four data items
Figure 10-1):
■ The environment block
■ The command tail
■ The two default file control blocks (FCBs)
The position reserved in the parameter block for the pointer to an
environment is only 2 bytes and contains a segment address, because an
environment is always paragraph aligned (its address is always evenly
divisible by 16); a value of 0000H indicates the parent program's
environment should be inherited unchanged. The remaining three
addresses are all doubleword addresses in the standard Intel format,
with an offset value in the lower word and a segment value in the
upper word.
╔══════════════════════════════════════════╗
║ ║
║ Figure 10-1 is found on page 324 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 10-1. Synopsis of calling conventions for the MS-DOS EXEC
function (Interrupt 21H Function 4BH), which can be used to load and
execute child processes or overlays.
The environment
An environment always begins on a paragraph boundary and is composed
of a series of null-terminated (ASCIIZ) strings of the form:
name = variable
The end of the entire set of strings is indicated by an additional
null byte.
If the environment pointer in the parameter block supplied to an EXEC
call contains zero, the child simply acquires a copy of the parent's
environment. The parent can, however, provide a segment pointer to a
different or expanded set of strings. In either case, under MS-DOS
versions 3.0 and later, EXEC appends the child program's fully
qualified pathname to its environment block. The maximum size of an
environment is 32 KB, so very large amounts of information can be
passed between programs by this mechanism.
The original, or master, environment for the system is owned by the
command processor that is loaded when the system is turned on or
restarted (usually COMMAND.COM). Strings are placed in the system's
master environment by COMMAND.COM as a result of PATH, SHELL, PROMPT,
and SET commands, with default values always present for the first
two. For example, if an MS-DOS version 3.2 system is started from
drive C and a PATH command is not present in the AUTOEXEC.BAT file nor
a SHELL command in the CONFIG.SYS file, the master environment will
contain the two strings:
PATH =
COMSPEC = C:\COMMAND.COM
These specifications are used by COMMAND.COM to search for executable
"external" commands and to find its own executable file on the disk so
that it can reload its transient portion when necessary. When the
PROMPT string is present (as a result of a previous PROMPT or SET
PROMPT command), COMMAND.COM uses it to tailor the prompt displayed to
the user.
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0000 43 4F 4D 53 50 45 43 3D 43 3A 5C 43 4F 4D 4D 41 COMSPEC=C:\COMMA
0010 4E 44 2E 43 4F 4D 00 50 52 4F 4D 50 54 3D 24 70 ND.COM.PROMPT=$p
0020 24 5F 24 64 20 20 20 24 74 24 68 24 68 24 68 24 $$d $t$h$h$h$
0030 68 24 68 24 68 20 24 71 24 71 24 67 00 50 41 54 h$h$h $q$q$g.PAT
0040 48 3D 43 3A 5C 53 59 53 54 45 4D 3B 43 3A 5C 41 H=C:\SYSTEM;C:\A
0050 53 4D 3B 43 3A 5C 57 53 3B 43 3A 5C 45 54 48 45 SM;C:\WS;C:\ETHE
0060 52 4E 45 54 3B 43 3A 5C 46 4F 52 54 48 5C 50 43 RNET;C:\FORTH\PC
0070 33 31 3B 00 00 01 00 43 3A 5C 46 4F 52 54 48 5C 31;....C:\FORTH\
0080 50 43 33 31 5C 46 4F 52 54 48 2E 43 4F 4D 00 PC31\FORTH.COM.
Figure 10-2. Dump of a typical environment under MS-DOS version
3.2. This particular example contains the default COMSPEC parameter
and two relatively complex PATH and PROMPT control strings that were
set up by entries in the user's AUTOEXEC file. Note the two null bytes
at offset 73H, which indicate the end of the environment. These bytes
are followed by the pathname of the program that owns the environment.
Other strings in the environment are used only for informational
purposes by transient programs and do not affect the operation of the
operating system proper. For example, the Microsoft C Compiler and the
Microsoft Object Linker look in the environment for INCLUDE, LIB, and
TMP strings that specify the location of include files, library files,
and temporary working files. Figure 10-2 contains a hex dump of a
typical environment block.
The command tail
The command tail to be passed to the child program takes the form of a
byte indicating the length of the remainder of the command tail,
followed by a string of ASCII characters terminated with an ASCII
carriage return (0DH); the carriage return is not included in the
length byte. The command tail can include switches, filenames, and
other parameters that can be inspected by the child program and used
to influence its operation. It is copied into the child program's PSP
at offset 80H.
When COMMAND.COM uses EXEC to run a program, it passes a command tail
that includes everything the user typed in the command line except the
name of the program and any redirection parameters. I/O redirection is
processed within COMMAND.COM itself and is manifest in the behavior of
the standard device handles that are inherited by the child program.
Any other program that uses EXEC to run a child program must try to
perform any necessary redirection on its own and must supply an
appropriate command tail so that the child program will behave as
though it had been loaded by COMMAND.COM.
The default file control blocks
The two default FCBs pointed to by the EXEC parameter block are copied
into the child program's PSP at offsets 5CH and 6CH. See also
PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: File
and Record Management.
Few of the currently popular application programs use FCBs for file
and record I/O because FCBs do not support the hierarchical directory
structure. But some programs do inspect the default FCBs as a quick
way to isolate the first two switches or other parameters from the
command tail. Therefore, to make its own identity transparent to the
child program, the parent should emulate the action of COMMAND.COM by
parsing the first two parameters of the command tail into the default
FCBs. This can be conveniently accomplished with the MS-DOS function
Parse Filename (Interrupt 21H Function 29H).
If the child program does not require one or both of the default FCBs,
the corresponding address in the parameter block can be initialized to
point to two dummy FCBs in the application's memory space. These dummy
FCBs should consist of 1 zero byte followed by 11 bytes containing
ASCII blank characters (20H).
Running the child program
After the parent program has constructed the necessary parameters, it
can invoke the EXEC function by issuing Interrupt 21H with the
registers set as follows:
AH = 4BH
AL = 00H (EXEC subfunction to load and execute program)
DS:DX = segment:offset of program pathname
ES:BX = segment:offset of parameter block
Upon return from the software interrupt, the parent must test the
carry flag to determine whether the child program did, in fact, run.
If the carry flag is clear, the child program was successfully loaded
and given control. If the carry flag is set, the EXEC function failed,
and the error code returned in AX can be examined to determine why.
The usual reasons are
■ The specified file could not be found.
■ The file was found, but not enough memory was free to load it.
Other causes are uncommon and can be symptoms of more severe problems
in the system as a whole (such as damage to disk files or to the
memory image of MS-DOS). With MS-DOS versions 3.0 and later,
additional details about the cause of an EXEC failure can be obtained
by subsequently calling Interrupt 21H Function 59H (Get Extended Error
Information).
In general, supplying either an invalid address for an EXEC parameter
block or invalid addresses within the parameter block itself does not
cause a failure of the EXEC function, but may result in the child
program behaving in unexpected ways.
Special considerations
With MS-DOS versions 2.x, the previous contents of all the parent
registers except for CS:IP can be destroyed after an EXEC call,
including the stack pointer in SS:SP. Consequently, before issuing the
EXEC call, the parent must push onto the stack the contents of any
registers that it needs to preserve, and then it must save the stack
segment and offset in a location that is addressable with the CS
segment register. Upon return, the stack segment and offset can be
loaded into SS:SP with code segment overrides, and then the other
registers can be restored by popping them off the stack. With MS-DOS
versions 3.0 and later, registers are preserved across an EXEC call in
the usual fashion.
Note: The code segments of Windows applications that use this
technique should be given the IMPURE attribute.
In addition, a bug in MS-DOS version 2.0 and in PC-DOS versions 2.0
and 2.1 causes an arbitrary doubleword in the parent's stack segment
to be destroyed during an EXEC call. When the parent is a .COM program
and SS = PSP, the damaged location falls within the PSP and does no
harm; however, in the case of a .EXE parent where DS = SS, the
affected location may overlap the data segment and cause aberrant
behavior or even a crash after the return from EXEC. This bug was
fixed in MS-DOS versions 2.11 and later and in PC-DOS versions 3.0
and later.
Examining the child program's return codes
If the EXEC function succeeds, the parent program can call Interrupt
21H Function 4DH (Get Return Code of Child Process) to learn whether
the child executed normally to completion and passed back a return
code or was terminated by the operating system because of an external
event. Function 4DH returns
AH = termination type:
= 00H Child terminated normally (that is, exited via
Interrupt 20H or Interrupt 21H Function 00H or Function
4CH).
= 01H Child was terminated by user's entry of a Ctrl-C.
= 02H Child was terminated by critical error handler (either
the user responded with A to the Abort, Retry, Ignore
prompt from the system's default Interrupt 24H handler,
or a custom Interrupt 24H handler returned to MS-DOS with
action code = 02H in register AL).
= 03H Child terminated normally and stayed resident (that is,
exited via Interrupt 21H Function 31H or Interrupt 27H).
AL = return code:
= Value passed by the child program in register AL when it
terminated with Interrupt 21H Function 4CH or 31H.
= 00H if the child terminated using Interrupt 20H, Interrupt
27H, or Interrupt 21H Function 00H.
These values are only guaranteed to be returned once by Function 4DH.
Thus, a subsequent call to Function 4DH, without an intervening EXEC
call, does not necessarily return any useful information.
Additionally, if Function 4DH is called without a preceding successful
EXEC call, the returned values are meaningless.
Using COMMAND.COM with EXEC
An application program can "shell" to MS-DOS--that is, provide the
user with an MS-DOS prompt without terminating--by using EXEC to load
and execute a secondary copy of COMMAND.COM with an empty command
tail. The application can obtain the location of the COMMAND.COM disk
file by inspecting its own environment for the COMSPEC string. The
user returns to the application from the secondary command processor
by typing exit at the COMMAND.COM prompt.
Batch-file interpretation is carried out by COMMAND.COM, and a batch
(.BAT) file cannot be called using the EXEC function directly.
Similarly, the sequential search for .COM, .EXE, and .BAT files in all
the locations specified in the environment's PATH variable is a
function of COMMAND.COM, rather than of EXEC. To execute a batch file
or search the system path for a program, an application program can
use EXEC to load and execute a secondary copy of COMMAND.COM to use as
an intermediary. The application finds the location of COMMAND.COM as
described in the preceding paragraph, but it passes a command tail in
the form:
/C program parameter1 parameter2 ...
where program is the .EXE, .COM, or .BAT file to be executed. When
program terminates, the secondary copy of COMMAND.COM exits and
returns control to the parent.
A parent and child example
The source programs PARENT.ASM in Figure 10-3 and CHILD.ASM in Figure
10-4 illustrate how one program uses EXEC to load another.
──────────────────────────────────────────────────────────────────────
Figure 10-3. PARENT.ASM, source code for PARENT.EXE.
──────────────────────────────────────────────────────────────────────
Figure 10-4. CHILD.ASM, source code for CHILD.EXE.
──────────────────────────────────────────────────────────────────────
PARENT.ASM can be assembled and linked into the executable program
PARENT.EXE with the following commands:
C>MASM PARENT; <Enter>
C>LINK PARENT; <Enter>
Similarly, CHILD.ASM can be assembled and linked into the file
CHILD.EXE as follows:
C>MASM CHILD; <Enter>
C>LINK CHILD; <Enter>
When PARENT.EXE is executed with the command
C>PARENT <Enter>
PARENT reduces the size of its main memory block with a call to
Interrupt 21H Function 4AH, to maximize the amount of free memory in
the system, and then calls the EXEC function to load and execute
CHILD.EXE.
CHILD.EXE runs exactly as though it had been loaded directly by
COMMAND.COM. CHILD resets the DS segment register to point to its own
data segment, uses Interrupt 21H Function 40H to display a message on
standard output, and then terminates using Interrupt 21H Function 4CH,
passing a return code of zero.
When PARENT.EXE regains control, it first checks the carry flag to
determine whether the EXEC call succeeded. If the EXEC call failed,
PARENT displays an error message and terminates with Interrupt 21H
Function 4CH, itself passing a nonzero return code to COMMAND.COM to
indicate an error.
Otherwise, PARENT uses Interrupt 21H Function 4DH to obtain
CHILD.EXE's termination type and return code, which it converts
to ASCII and displays. PARENT then terminates using Interrupt 21H
Function 4CH and passes a return code of zero to COMMAND.COM to
indicate success. COMMAND.COM in turn receives control and displays
a new user prompt.
Using EXEC to Load Overlays
Loading overlays with the EXEC function is much less complex than
using EXEC to run another program. The main program, called the root
segment, must carry out the following steps to load and execute an
overlay:
1. Make a memory block available to receive the overlay.
2. Set up the overlay parameter block to be passed to the EXEC
function.
3. Call the EXEC function to load the overlay.
4. Execute the code within the overlay by transferring to it with a
far call.
The overlay itself can be constructed as either a memory image (.COM)
or a relocatable (.EXE) file and need not be the same type as the root
program. In either case, the overlay should be designed so that the
entry point (or a pointer to the entry point) is at the beginning of
the module after it is loaded. This allows the root and overlay mod-
ules to be maintained separately and avoids a need for the root
to have "magical" knowledge of addresses within the overlay.
To prevent users from inadvertently running an overlay directly from
the command line, overlay files should be assigned an extension other
than .COM or .EXE. The most convenient method relates overlays to
their root segment by assigning them the same filename but an
extension such as .OVL or .OV1, .OV2, and so on.
Making memory available
If EXEC is to load a child program successfully, the parent must
release memory. In contrast, EXEC loads an overlay into memory that
belongs to the calling program. If the root segment is a .COM program
and has not explicitly released extra memory, the root segment program
need only ensure that the system contains enough memory to load the
overlay and that the overlay load address does not conflict with its
own code, data, or stack areas.
If the root segment program was loaded from a .EXE file, no
straightforward way exists for it to determine unequivocally how much
memory it already owns. The simplest course is for the program to
release all extra memory, as discussed earlier in the section on
loading a child program, and then use the MS-DOS memory allocation
function (Interrupt 21H Function 48H) to obtain a new block of memory
that is large enough to hold the overlay.
Preparing overlay parameters
When it is used to load an overlay, the EXEC function requires two
major parameters:
■ The address of the pathname for the overlay file
■ The address of an overlay parameter block
As for a child program, the pathname for the overlay file must be an
unambiguous ASCIIZ file specification (again, no wildcard characters),
and it must include an explicit extension. As before, if a path and/or
drive are not included in the pathname, the current directory and
default drive are used.
The overlay parameter block contains the segment address at which the
overlay should be loaded and a fixup value to be applied to any
relocatable items within the overlay file. If the overlay file is in
.EXE format, the fixup value is typically the same as the load
address; if the overlay is in memory-image (.COM) format, the fixup
value should be zero. The EXEC function does not attempt to validate
the load address or the fixup value or to ensure that the load address
actually belongs to the calling program.
Loading and executing the overlay
After the root segment program has prepared the filename of the
overlay file and the overlay parameter block, it can invoke the EXEC
function to load the overlay by issuing an Interrupt 21H with the
registers set as follows:
AH = 4BH
AL = 03H (EXEC subfunction to load overlay)
DS:DX = segment:offset of overlay file pathname
ES:BX = segment:offset of overlay parameter block
Upon return from Interrupt 21H, the root segment must test the carry
flag to determine whether the overlay was loaded. If the carry flag is
clear, the overlay file was located and brought into memory at the
requested address. The overlay can then be entered by a far call and
should exit back to the root segment with a far return.
If the carry flag is set, the overlay file was not found or some other
(probably severe) system problem was encountered, and the AX register
contains an error code. With MS-DOS versions 3.0 and later, Interrupt
21H Function 59H can be used to get more information about the EXEC
failure. An invalid load address supplied in the overlay parameter
block does not (usually) cause the EXEC function itself to fail but
may result in the disconcerting message Memory Allocation Error,
System Halted when the root program terminates.
An overlay example
The source programs ROOT.ASM in Figure 10-5 and OVERLAY.ASM in Figure
10-6 demonstrate the use of EXEC to load a program overlay. The
program ROOT.EXE is executable from the MS-DOS prompt; it represents
the root segment of an application. OVERLAY is constructed as a .EXE
file (although it is named OVERLAY.OVL because it cannot be run alone)
and represents a subprogram that can be loaded by the root segment
when and if it is needed.
──────────────────────────────────────────────────────────────────────
Figure 10-5. ROOT.ASM, source code for ROOT.EXE.
──────────────────────────────────────────────────────────────────────
Figure 10-6. OVERLAY.ASM, source code for OVERLAY.OVL.
──────────────────────────────────────────────────────────────────────
ROOT.ASM can be assembled and linked into the executable program
ROOT.EXE with the following commands:
C>MASM ROOT; <Enter>
C>LINK ROOT; <Enter>
OVERLAY.ASM can be assembled and linked into the file OVERLAY.OVL by
typing
C>MASM OVERLAY; <Enter>
C>LINK OVERLAY,OVERLAY.OVL; <Enter>
The Microsoft Object Linker will display the message
Warning: no stack segment
but this message can be ignored.
When ROOT.EXE is executed with the command
C>ROOT <Enter>
it first shrinks its main memory block with a call to Interrupt 21H
Function 4AH and then allocates a separate block for the overlay with
Interrupt 21H Function 48H. Next, ROOT calls the EXEC function to load
the file OVERLAY.OVL into the newly allocated memory block. If the
EXEC function fails, ROOT displays an error message and terminates
with Interrupt 21H Function 4CH, passing a nonzero return code to
COMMAND.COM to indicate an error. If the EXEC function succeeds, ROOT
saves the contents of its DS segment register and then enters the
overlay through an indirect far call.
The overlay resets the DS segment register to point to its own data
segment, displays a message using Interrupt 21H Function 40H, and then
returns. Note that the main procedure of the overlay is declared with
the far attribute to force the assembler to generate the opcode for a
far return.
When ROOT regains control, it restores the DS segment register to
point to its own data segment again and displays an additional
message, also using Interrupt 21H Function 40H, to indicate that the
overlay executed successfully. ROOT then terminates using Interrupt
21H Function 4CH, passing a return code of zero to indicate success,
and control returns to COMMAND.COM.
Ray Duncan
───────────────────────────────────────────────────────────────────────────
Part C Customizing MS-DOS
Article 11: Terminate-and-Stay-Resident Utilities
The MS-DOS Terminate and Stay Resident system calls (Interrupt 21H
Function 31H and Interrupt 27H) allow the programmer to install
executable code or program data in a reserved block of RAM, where it
resides while other programs execute. Global data, interrupt handlers,
and entire applications can be made RAM-resident in this way. Programs
that use the MS-DOS terminate-and-stay-resident capability are
commonly known as TSR programs or TSRs.
This article describes how to install a TSR in RAM, how to communicate
with the resident program, and how the resident program can interact
with MS-DOS. The discussion proceeds from a general description of the
MS-DOS functions useful to TSR programmers to specific details about
certain MS-DOS structural elements necessary to proper functioning of
a TSR utility and concludes with two programming examples.
Note: Microsoft cannot guarantee that the information in this article
will be valid for future versions of MS-DOS.
Structure of a Terminate-and-Stay-Resident Utility
The executable code and data in TSRs can be separated into RAM-
resident and transient portions (Figure 11-1). The RAM-resident
portion of a TSR contains executable code and data for an application
that performs some useful function on demand. The transient portion
installs the TSR; that is, it initializes data and interrupt handlers
contained in the RAM-resident portion of the program and executes an
MS-DOS Terminate and Stay Resident function call that leaves the RAM-
resident portion in memory and frees the memory used by the transient
portion. The code in the transient portion of a TSR runs when the .EXE
or .COM file containing the program is executed; the code in the RAM-
resident portion runs only when it is explicitly invoked by a fore-
ground program or by execution of a hardware or software interrupt.
Higher addresses ┌───────────────────────────┐
│ │▒
│Initialization code & data │▒─ Transient portion
│ │▒ (executed when
├───────────────────────────┤ .EXE file runs)
│ │▒
│ Application code & data │▒
│ │▒
├───────────────────────────┤▒─ RAM-resident portion
│ │▒
│ Monitor routines │▒
│ │▒
├───────────────────────────┤
│ Program segment prefix │
Lower addresses └───────────────────────────┘
Figure 11-1. Organization of a TSR program in memory.
TSRs can be broadly classified as passive or active, depending on the
method by which control is transferred to the RAM-resident program. A
passive TSR executes only when another program explicitly transfers
control to it, either through a software interrupt or by means of a
long JMP or CALL. The calling program is not interrupted by the TSR,
so the status of MS-DOS, the system BIOS, and the hardware is well
defined when the TSR program starts to execute.
In contrast, an active TSR is invoked by the occurrence of some event
external to the currently running (foreground) program, such as a
sequence of user keystrokes or a predefined hardware interrupt.
Therefore, when it is invoked, an active TSR almost always
interrupts some other program and suspends its execution. To avoid
disrupting the interrupted program, an active TSR must monitor the
status of MS-DOS, the ROM BIOS, and the hardware and take control of
the system only when it is safe to do so.
Passive TSRs are generally simpler in their construction than active
TSRs because a passive TSR runs in the context of the calling program;
that is, when the TSR executes, it assumes that it can use the calling
program's program segment prefix (PSP), open files, current directory,
and so on. See PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR
MS-DOS: Structure of an Application Program. It is the calling
program's responsibility to ensure that the hardware and MS-DOS are in
a stable state before it transfers control to a passive TSR.
An active TSR, on the other hand, is invoked asynchronously; that is,
the status of the hardware, MS-DOS, and the executing foreground
program is indeterminate when the event that invokes the TSR occurs.
Therefore, active TSRs require more complex code. The RAM-resident
portion of an active TSR must contain modules that monitor the
operating system to determine when control can safely be transferred
to the application portion of the TSR. The monitor routines typically
test the status of keyboard input, ROM BIOS interrupt processing,
hardware interrupt processing, and MS-DOS function processing. The TSR
activates the application (the part of the RAM-resident portion that
performs the TSR's main task) only when it detects the appropriate
keyboard input and determines that the application will not interfere
with interrupt and MS-DOS function processing.
Keyboard input
An active TSR usually contains a RAM-resident module that examines
keyboard input for a predetermined keystroke sequence called a "hot-
key" sequence. A user executes the RAM-resident application by
entering this hot-key sequence at the keyboard.
The technique used in the TSR to monitor keyboard input depends on the
keyboard hardware implementation. On computers in the IBM PC and PS/2
families, the keyboard coprocessor generates an Interrupt 09H for each
keypress. Therefore, a TSR can monitor user keystrokes by installing
an interrupt handler (interrupt service routine, or ISR) for Interrupt
09H. This handler can thus detect a specified hot-key sequence.
ROM BIOS interrupt processing
The ROM BIOS routines in IBM PCs and PS/2s are not reentrant. An
active TSR that calls the ROM BIOS must ensure that its code does not
attempt to execute a ROM BIOS function that was already being executed
by the foreground process when the TSR program took control of the
system.
The IBM ROM BIOS routines are invoked through software interrupts, so
an active TSR can monitor the status of the ROM BIOS by replacing the
default interrupt handlers with custom interrupt handlers that
intercept the appropriate BIOS interrupts. Each of these interrupt
handlers can maintain a status flag, which it increments before
transferring control to the corresponding ROM BIOS routine and
decrements when the ROM BIOS routine has finished executing. Thus, the
TSR monitor routines can test these flags to determine when non-
reentrant BIOS routines are executing.
Hardware interrupt processing
The monitor routines of an active TSR, which may themselves be
executed as the result of a hardware interrupt, should not activate
the application portion of the TSR if any other hardware interrupt is
being processed. On IBM PCs, for example, hardware interrupts are
processed in a prioritized sequence determined by an Intel 8259A
Programmable Interrupt Controller. The 8259A does not allow a hardware
interrupt to execute if a previous interrupt with the same or higher
priority is being serviced. All hardware interrupt handlers include
code that signals the 8259A when interrupt processing is completed.
(The programming interface to the 8259A is described in IBM's
Technical Reference manuals and in Intel's technical literature.)
If a TSR were to interrupt the execution of another hardware interrupt
handler before the handler signaled the 8259A that it had completed
its interrupt servicing, subsequent hardware interrupts could be
inhibited indefinitely. Inhibition of high-priority hardware
interrupts such as the timer tick (Interrupt 08H) or keyboard
interrupt (Interrupt 09H) could cause a system crash. For this reason,
an active TSR must monitor the status of all hardware interrupt
processing by interrogating the 8259A to ensure that control is
transferred to the RAM-resident application only when no other
hardware interrupts are being serviced.
MS-DOS function processing
Unlike the IBM ROM BIOS routines, MS-DOS is reentrant to a limited
extent. That is, there are certain times when MS-DOS's servicing of an
Interrupt 21H function call invoked by a foreground process can be
suspended so that the RAM-resident application can make an Interrupt
21H function call of its own. For this reason, an active TSR must
monitor operating system activity to determine when it is safe for the
TSR application to make its calls to MS-DOS.
MS-DOS Support for Terminate-and-Stay-Resident Programs
Several MS-DOS system calls are useful for supporting terminate-and-
stay-resident utilities. These are listed in Table 11-1. See SYSTEM
CALLS.
Table 11-1. MS-DOS Functions Useful in TSR Programs.
╓┌─────────────────────┌─────────────────────┌───────────────┌───────────────╖
Function Name Call With Returns Comment
──────────────────────────────────────────────────────────────────
Terminate and AH = 31H Nothing Preferred over
Stay Resident AL = return code Interrupt 27H
DX = size of resident with MS-DOS
program (in 16-byte versions 2.x
paragraphs) and later
INT 21H
Terminate and CS = PSP Nothing Provided for
Stay Resident DX = size of resident compatibility
program (bytes) with MS-DOS
versions 1.x
INT 27H
Set Interrupt AH = 25H Nothing
Vector AL = interrupt number
DS:DX = address of
interrupt handler
INT 21H
Get Interrupt AH = 35H ES:BX = address
Vector AL = interrupt number of interrupt
INT 21H handler
Set PSP Address AH = 50H Nothing
BX = PSP segment
INT 21H
Get PSP Address AH = 51H BX = PSP segment
INT 21H
Set Extended AX = 5D0AH Nothing MS-DOS versions
Error DS:DX = address of 11- 3.1 and
Information word data structure: later
word 0: register AX
as returned by
Function 59H
word 1: register BX
word 2: register CX
word 3: register DX
word 4: register SI
word 5: register DI
word 6: register DS
word 7: register ES
words 8-0AH: reserved;
should be 0
INT 21H
Get Extended AH = 59H AX = extended
Error BX = 0 error code
Information INT 21H BH = error class
BL = suggested
action
CH = error locus
Set Disk AH = 1AH Nothing
Transfer Area DS:DX = address
Address of DTA
Get Disk AH = 2FH ES:BX = address
Transfer Area INT 21H of current DTA
Address
Get InDOS Flag AH = 34H ES:BX = address
Address INT 21H of InDOS flag
Terminate-and-stay-resident functions
MS-DOS provides two mechanisms for terminating the execution of a
program while leaving a portion of it resident in RAM. The preferred
method is to execute Interrupt 21H Function 31H.
Interrupt 21H Function 31H
When this Interrupt 21H function is called, the value in DX specifies
the amount of RAM (in paragraphs) that is to remain allocated after
the program terminates, starting at the program segment prefix (PSP).
The function is similar to Function 4CH (Terminate Process with Return
Code) in that it passes a return code in AL, but it differs in that
open files are not automatically closed by Function 31H.
Interrupt 27H
When Interrupt 27H is executed, the value passed in DX specifies the
number of bytes of memory required for the RAM-resident program. MS-
DOS converts the value passed in DX from bytes to paragraphs, sets AL
to zero, and jumps to the same code that would be executed for
Interrupt 21H Function 31H. Interrupt 27H is less flexible than
Interrupt 21H Function 31H because it limits the size of the program
that can remain resident in RAM to 64 KB, it requires that CS point to
the base of the PSP, and it does not pass a return code. Later
versions of MS-DOS support Interrupt 27H primarily for compatibility
with versions 1.x.
TSR RAM management
In addition to the RAM explicitly allocated to the TSR by means of the
value in DX, the RAM allocated to the TSR's environment remains
resident when the installation portion of the TSR program terminates.
(The paragraph address of the environment is found at offset 2CH in
the TSR's PSP.) Moreover, if the installation portion of a TSR program
has used Interrupt 21H Function 48H (Allocate Memory Block) to
allocate additional RAM, this memory also remains allocated when the
program terminates. If the RAM-resident program does not need this
additional RAM, the installation portion of the TSR program should
free it explicitly by using Interrupt 21H Function 49H (Free Memory
Block) before executing Interrupt 21H Function 31H.
Set and Get Interrupt Vector functions
Two Interrupt 21H function calls are available to inspect or update
the contents of a specified 8086-family interrupt vector. Function 25H
(Set Interrupt Vector) updates the vector of the interrupt number
specified in the AL register with the segment and offset values
specified in DS:DX. Function 35H (Get Interrupt Vector) performs the
inverse operation: It copies the current vector of the interrupt
number specified in AL into the ES:BX register pair.
Although it is possible to manipulate interrupt vectors directly, the
use of Interrupt 21H Functions 25H and 35H is generally more
convenient and allows for upward compatibility with future versions of
MS-DOS.
Set and Get PSP Address functions
MS-DOS uses a program's PSP to keep track of certain data unique to
the program, including command-line parameters and the segment address
of the program's environment. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: Structure of an Application
Program. To access this information, MS-DOS maintains an internal
variable that always contains the location of the PSP associated with
the foreground process. When a RAM-resident application is activated,
it should use Interrupt 21H Functions 50H (Set Program Segment Prefix
Address) and 51H (Get Program Segment Prefix Address) to preserve the
current contents of this variable and to update the variable with the
location of its own PSP. Function 50H (Set Program Segment Prefix
Address) updates an internal MS-DOS variable that locates the PSP
currently in use by the foreground process. Function 51H (Get Program
Segment Prefix Address) returns the contents of the internal MS-DOS
variable to the caller.
Set and Get Extended Error Information functions
In MS-DOS versions 3.1 and later, the RAM-resident program should
preserve the foreground process's extended error information so that,
if the RAM-resident application encounters an MS-DOS error, the
extended error information pertaining to the foreground process will
still be available and can be restored. Interrupt 21H Functions 59H
and 5D0AH provide a mechanism for the RAM-resident program to save and
restore this information during execution of a TSR application.
Function 59H (Get Extended Error Information), which became available
in version 3.0, returns detailed information on the most recently
detected MS-DOS error. The inverse operation is performed by Function
5D0AH (Set Extended Error Information), which can be used only in
MS-DOS versions 3.1 and later. This function copies extended error
information to MS-DOS from a data structure defined in the calling
program.
Set and Get Disk Transfer Area Address functions
Several MS-DOS data transfer functions, notably Interrupt 21H
Functions 21H, 22H, 27H, and 28H (the Random Read and Write functions)
and Interrupt 21H Functions 14H and 15H (the Sequential Read and Write
functions), require a program to specify a disk transfer area (DTA).
By default, a program's DTA is located at offset 80H in its program
segment prefix. If a RAM-resident application calls an MS-DOS function
that uses a DTA, the TSR should save the DTA address belonging to the
interrupted program by using Interrupt 21H Function 2FH (Get Disk
Transfer Area Address), supply its own DTA address to MS-DOS using
Interrupt 21H Function 1AH (Set Disk Transfer Area Address), and then,
before terminating, restore the interrupted program's DTA.
The MS-DOS idle interrupt (Interrupt 28H)
Several of the first 12 MS-DOS functions (01H through 0CH) must wait
for the occurrence of an expected event such as a user keypress. These
functions contain an "idle loop" in which looping continues until the
event occurs. To provide a mechanism for other system activity to take
place while the idle loop is executing, these MS-DOS functions execute
an Interrupt 28H from within the loop.
The default MS-DOS handler for Interrupt 28H is only an IRET
instruction. By supplying its own handler for Interrupt 28H, a TSR can
perform some useful action at times when MS-DOS is otherwise idle.
Specifically, a custom Interrupt 28H handler can be used to examine
the current status of the system to determine whether or not it is
safe to activate the RAM-resident application.
Determining MS-DOS Status
A TSR can infer the current status of MS-DOS from knowledge of its
internal use of stacks and from a pair of internal status flags. This
status information is essential to the proper execution of an active
TSR because a RAM-resident application can make calls to MS-DOS only
when those calls will not disrupt an earlier call made by the
foreground process.
MS-DOS internal stacks
MS-DOS versions 2.0 and later may use any of three internal stacks:
the I/O stack (IOStack), the disk stack (DiskStack), and the auxiliary
stack (AuxStack). In general, IOStack is used for Interrupt 21H
Functions 01H through 0CH and DiskStack is used for the remaining
Interrupt 21H functions; AuxStack is normally used only when MS-DOS
has detected a critical error and subsequently executed an Interrupt
24H. See PROGRAMMING IN THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS:
Exception Handlers. Specifically, MS-DOS's internal stack use depends
on which MS-DOS function is being executed and on the value of the
critical error flag.
The critical error flag
The critical error flag (ErrorMode) is a 1-byte flag that MS-DOS uses
to indicate whether or not a critical error has occurred. During
normal, errorless execution, the value of the critical error flag
is zero. Whenever MS-DOS detects a critical error, it sets this flag
to a nonzero value before it executes Interrupt 24H. If an Interrupt
24H handler subsequently invokes an MS-DOS function by using Interrupt
21H, the nonzero value of the critical error flag tells MS-DOS to use
its auxiliary stack for Interrupt 21H Functions 01H through 0CH
instead of using the I/O stack as it normally would.
In other words, when control is transferred to MS-DOS through
Interrupt 21H, the function number and the critical error flag
together determine MS-DOS stack use for the function. Figure 11-2
outlines the internal logic used on entry to an MS-DOS function to
select which stack is to be used during processing of the function.
AS stated above, for Functions 01H through 0CH, MS-DOS uses IOStack if
the critical error flag is zero and AuxStack if the flag is nonzero.
For function numbers greater than 0CH, MS-DOS usually uses DiskStack,
but Functions 50H, 51H, and 59H are important exceptions. Functions
50H and 51H use either IOStack (in versions 2.x) or the stack supplied
by the calling program (in versions 3.x). In version 3.0, Function 59H
uses either IOStack or AuxStack, depending on the value of the
critical error flag, but in versions 3.1 and later, Function 59H
always uses AuxStack.
MS-DOS versions 2.x
if (FunctionNumber >= 01H and FunctionNumber <= 0CH)
or
FunctionNumber = 50H
or
FunctionNumber = 51H
then if ErrorMode = 0
then use IOStack
else use AuxStack
else ErrorMode = 0
use DiskStack
MS-DOS version 3.0
if FunctionNumber = 50H
or
FunctionNumber = 51H
or
FunctionNumber = 62H
then use caller's stack
else if (FunctionNumber >= 01H and FunctionNumber <= 0CH)
or
Function Number = 59H
then if ErrorMode = 0
then use IOStack
else use AuxStack
else ErrorMode = 0
use DiskStack
MS-DOS versions 3.1 and later
if FunctionNumber = 33H
or
FunctionNumber = 50H
or
FunctionNumber = 51H
or
FunctionNumber = 62H
then use caller's stack
else if (FunctionNumber >= 01H and FunctionNumber <= 0CH)
then if ErrorMode = 0
then use IOStack
else use AuxStack
else if FunctionNumber = 59H
then use AuxStack
else ErrorMode = 0
use DiskStack
Figure 11-2. Strategy for use of MS-DOS internal stacks.
This scheme makes Functions 01H through 0CH reentrant in a limited
sense, in that a substitute critical error (Interrupt 24H) handler
invoked while the critical error flag is nonzero can still use these
Interrupt 21H functions. In this situation, because the flag is
nonzero, AuxStack is used for Functions 01H through 0CH instead of
IOStack. Thus, if IOStack is in use when the critical error is
detected, its contents are preserved during the handler's subsequent
calls to these functions.
The stack-selection logic differs slightly between MS-DOS versions 2
and 3. In versions 3.x, a few functions--notably 50H and 51H--avoid
using any of the MS-DOS stacks. These functions perform uncomplicated
tasks that make minimal demands for stack space, so the calling
program's stack is assumed to be adequate for them.
The InDOS flag
InDOS is a 1-byte flag that is incremented each time an Interrupt 21H
function is invoked and decremented when the function terminates. The
flag's value remains nonzero as long as code within MS-DOS is being
executed. The value of InDOS does not indicate which internal stack
MS-DOS is using.
Whenever MS-DOS detects a critical error, it zeros InDOS before it
executes Interrupt 24H. This action is taken to accommodate substitute
Interrupt 24H handlers that do not return control to MS-DOS. If InDOS
were not zeroed before such a handler gained control, its value would
never be decremented and would therefore be incorrect during
subsequent calls to MS-DOS.
The address of the 1-byte InDOS flag can be obtained from MS-DOS by
using Interrupt 21H Function 34H (Return Address of InDOS Flag). In
versions 3.1 and later, the 1-byte critical error flag is located in
the byte preceding InDOS, so, in effect, the address of both flags can
be found using Function 34H. Unfortunately, there is no easy way to
find the critical error flag in other versions. The recommended
technique is to scan the MS-DOS segment, which is returned in the ES
register by Function 34H, for one of the following sequences of
instructions:
test ss:[CriticalErrorFlag],0FFH ;(versions 3.1 and
;later)
jne NearLabel
push ss:[NearWord]
int 28H
or
cmp ss:[CriticalErrorFlag],00 ;(versions earlier
;than 3.1)
jne NearLabel
int 28H
When the TEST or CMP instruction has been identified, the offset of
the critical error flag can be obtained from the instruction's operand
field.
The Multiplex Interrupt
The MS-DOS multiplex interrupt (Interrupt 2FH) provides a general
mechanism for a program to verify the presence of a TSR and
communicate with it. A program communicates with a TSR by placing an
identification value in AH and a function number in AL and issuing an
Interrupt 2FH. The TSR's Interrupt 2FH handler compares the value in
AH to its own predetermined ID value. If they match, the TSR's handler
keeps control and performs the function specified in the AL register.
If they do not match, the TSR's handler relinquishes control to the
previously installed Interrupt 2FH handler. (Multiplex ID values 00H
through 7FH are reserved for use by MS-DOS; therefore, user multiplex
numbers should be in the range 80H through 0FFH.)
The handler in the following example recognizes only one function,
corresponding to AL = 00H. In this case, the handler returns the value
0FFH in AL, signifying that the handler is indeed resident in RAM.
Thus, a program can detect the presence of the handler by executing
Interrupt 2FH with the handler's ID value in H and 00H in AL.
mov ah,MultiplexID
mov al,00H
int 2FH
cmp al,0FFH
je AlreadyInstalled
To ensure that the identification byte is unique, its value should be
determined at the time the TSR is installed. One way to do this is to
pass the value to the TSR program as a command-line parameter when the
TSR program is installed. Another approach is to place the
identification value in an environment variable. In this way, the
value can be found in the environment of both the TSR and any other
program that calls Interrupt 2FH to verify the TSR's presence.
In practice, the multiplex interrupt can also be used to pass
information to and from a RAM-resident program in the CPU registers,
thus providing a mechanism for a program to share control or status
information with a TSR.
TSR Programming Examples
One effective way to become familiar with TSRs is to examine
functional programs. Therefore, the subsequent pages present two
examples: a simple passive TSR and a more complex active TSR.
HELLO.ASM
The "bare-bones" TSR in Figure 11-3 is a passive TSR. The RAM-resident
application, which simply displays the message Hello, World, is
invoked by executing a software interrupt. This example illustrates
the fundamental interactions among a RAM-resident program, MS-DOS, and
programs that execute after the installation of the RAM-resident
utility.
──────────────────────────────────────────────────────────────────────
Figure 11-3. HELLO.ASM, a passive TSR.
──────────────────────────────────────────────────────────────────────
The transient portion of the program (in the segments TRANSIENT_TEXT
and TRANSIENT_STACK) runs only when the file HELLO.EXE is executed.
This installation code updates an interrupt vector to point to the
resident application (the procedure TSRAction) and then calls
Interrupt 21H Function 31H to terminate execution, leaving the
segments RESIDENT_TEXT and RESIDENT_DATA in RAM.
The order in which the code and data segments appear in the listing is
important. It ensures that when the program is executed as a .EXE
file, the resident code and data are placed in memory at lower
addresses than the transient code and data. Thus, when Interrupt 21H
Function 31H is called, the memory occupied by the transient portion
of the program is freed without disrupting the code and data in the
resident portion.
The RAM containing the resident portion of the utility is left intact
by MS-DOS during subsequent execution of other programs. Thus, after
the TSR has been installed, any program that issues the software
interrupt recognized by the TSR (in this example, Interrupt 64H) will
transfer control to the routine TSRAction, which uses Interrupt 21H
Function 40H to display a simple message on standard output.
Part of the reason this example is so short is that it performs no
error checking. A truly reliable version of the program would check
the version of MS-DOS in use, verify that the program was not already
installed in memory, and chain to any previously installed interrupt
handlers that use the same interrupt vector. (The next program,
SNAP.ASM, illustrates these techniques.) However, the primary reason
the program is small is that it makes the basic assumption that MS-
DOS, the ROM BIOS, and the hardware interrupts are all stable at the
time the resident utility is executed.
SNAP.ASM
The preceding assumption is a reliable one in the case of the passive
TSR in Figure 11-3, which executes only when it is explicitly invoked
by a software interrupt. However, the situation is much more
complicated in the case of the active TSR in Figure 11-4. This program
is relatively long because it makes no assumptions about the stability
of the operating environment. Instead, it monitors the status of
MS-DOS, the ROM BIOS, and the hardware interrupts to decide when the
RAM-resident application can safely execute.
──────────────────────────────────────────────────────────────────────
Figure 11-4. SNAP.ASM, a video snapshot TSR.
──────────────────────────────────────────────────────────────────────
When installed, the SNAP program monitors keyboard input until the
user types the hot-key sequence Alt-Enter. When the hot-key sequence
is detected, the monitoring routine waits until the operating
environment is stable and then activates the RAM-resident application,
which dumps the current contents of the computer's video buffer into
the file SNAP.IMG. Figure 11-5 is a block diagram of the RAM-resident
and transient components of this TSR.
Higher addresses ┌─────────────────────────────┐
│ Transient data │ TRANSIENT _DATA segment
├─────────────────────────────┤
│ InstallSnapTSR │ TRANSIENT _TEXT segment
│ Initialization code & data │
├─────────────────────────────┤
│ RAM-resident stack │ RESIDENT _STACK segment
├─────────────────────────────┤
│ RAM-resident data │ RESIDENT _DATA segment
├─────────────────────────────┤
│ TSRapp │▒
│ RAM-resident application │▒
├─────────────────────────────┤▒
│ ISR2F$--INT 2FH │▒
│(multiplex interrupt) handler│▒
├─────────────────────────────┤▒
│ ISR28--INT 28H │▒
│(DOS idle interrupt) handler │▒
├─────────────────────────────┤▒
│ ISR24--INT 24H │▒
│ (critical error) handler │▒
├─────────────────────────────┤▒
│ ISR23--INT 23H │▒
Middle addresses │ (Control-C) handler │▒ RESIDENT _TEXT segment
├─────────────────────────────┤▒
│ ISR1B--INT 1BH │▒
│ (Control-Break) handler │▒
├─────────────────────────────┤▒
│ ISR13--INT 13H │▒
│(BIOS fixed-disk I/O) handler│▒
├─────────────────────────────┤▒
│ ISR10--INT 10H │▒
│ (BIOS video I/O) handler │▒
├─────────────────────────────┤▒
│ ISR9--INT 09H │▒
│(keyboard interrupt) handler │▒
├─────────────────────────────┤▒
│ ISR8--INT 08H │▒
│ (timer interrupt) handler │▒
├─────────────────────────────┤▒
│ ISR5--INT 05H │▒
│ (BIOS print screen) handler │▒
Lower addresses └─────────────────────────────┘
Figure 11-5. Block structure of the TSR program SNAP.EXE when loaded
into memory. (Compare with Figure 11-1.)
Installing the program
When SNAP.EXE is run, only the code in the transient portion of the
program is executed. The transient code performs several operations
before it finally executes Interrupt 21H Function 31H (Terminate and
Stay Resident). First it determines which MS-DOS version is in use.
Then it executes the multiplex interrupt (Interrupt 2FH) to discover
whether the resident portion has already been installed. If an MS-DOS
version earlier than 2.0 is in use or if the resident portion has
already been installed, the program aborts with an error message.
Otherwise, installation continues. The addresses of the InDOS and
critical error flags are saved in the resident data segment. The
interrupt service routines in the RAM-resident portion of the program
are installed by updating all relevant interrupt vectors. The
transient code then frees the RAM occupied by the program's
environment, because the resident portion of this program never uses
the information contained there. Finally, the transient portion of the
program, which includes the TRANSIENT_TEXT and TRANSIENT_DATA
segments, is discarded and the program is terminated using Interrupt
21H Function 31H.
Detecting a hot key
The SNAP program detects the hot-key sequence (Alt-Enter) by
monitoring each keypress. On IBM PCs and PS/2s, each keystroke
generates a hardware interrupt on IRQ1 (Interrupt 09H). The TSR's
Interrupt 09H handler compares the keyboard scan code corresponding to
each keypress with a predefined sequence. The TSR's handler also
inspects the shift-key status flags maintained by the ROM BIOS
Interrupt 09H handler. When the predetermined sequence of keypresses
is detected at the same time as the proper shift keys are pressed, the
handler sets a global status flag (HotFlag).
Note how the TSR's handler transfers control to the previous Interrupt
09H ISR before it performs its own work. If the TSR's Interrupt 09H
handler did not chain to the previous handler(s), essential system
processing of keystrokes (particularly in the ROM BIOS Interrupt 09H
handler) might not be performed.
Activating the application
The TSR monitors the status of HotFlag by regularly testing its value
within a timer-tick handler. On IBM PCs and PS/2s, the timer-tick
interrupt occurs on IRQ0 (Interrupt 08H) roughly 18.2 times per
second. This hardware interrupt occurs regardless of what else the
system is doing, so an Interrupt 08H ISR a convenient place to check
whether HotFlag has been set.
As in the case of the Interrupt 09H handler, the TSR's Interrupt 08H
handler passes control to previous Interrupt 08H handlers before it
proceeds with its own work. This procedure is particularly important
with Interrupt 08H because the ROM BIOS Interrupt 08H handler, which
maintains the system's time-of-day clock and resets the system's Intel
8259A Programmable Interrupt Controller, must execute before the next
timer tick can occur. The TSR's handler therefore defers its own work
until control has returned after previous Interrupt 08H handlers have
executed.
The only function of the TSR's Interrupt 08H handler is to attempt to
transfer control to the RAM-resident application. The routine
VerifyTSRState performs this task. It first examines the contents of
HotFlag to determine whether a hot-key sequence has been detected. If
so, it examines the state of the MS-DOS InDOS and critical error
flags, the current status of hardware interrupts, and the current
status of any non-reentrant ROM BIOS routines that might be executing.
If HotFlag is nonzero, the InDOS and critical error flags are both
zero, no hardware interrupts are currently being serviced, and no non-
reentrant ROM BIOS code has been interrupted, the Interrupt 08H
handler activates the RAM-resident utility. Otherwise, nothing happens
until the next timer tick, when the handler executes again.
While HotFlag is nonzero, the Interrupt 08H handler continues to
monitor system status until MS-DOS, the ROM BIOS, and the hardware
interrupts are all in a stable state. Often the system status is
stable at the time the hot-key sequence is detected, so the RAM-
resident application runs immediately. Sometimes, however, system
activities such as prolonged disk reads or writes can preclude the
activation of the RAM-resident utility for several seconds after the
hot-key sequence has been detected. The handler could be designed to
detect this situation (for example, by decrementing HotFlag on each
timer tick) and return an error status or display a message to the
user.
A more serious difficulty arises when the MS-DOS default command
processor (COMMAND.COM) is waiting for keyboard input. In this
situation, Interrupt 21H Function 01H (Character Input with Echo) is
executing, so InDOS is nonzero and the Interrupt 08H handler can never
detect a state in which it can activate the RAM-resident utility. This
problem is solved by providing a custom handler for Interrupt 28H (the
MS-DOS idle interrupt), which is executed by Interrupt 21H Function
01H each time it loops as it waits for a keypress. The only difference
between the Interrupt 28H handler and the Interrupt 08H handler is
that the Interrupt 28H handler can activate the RAM-resident
application when the value of InDOS is 1, which is reasonable because
InDOS must have been incremented when Interrupt 21H Function 01H
started to execute.
The interrupt service routines for ROM BIOS Interrupts 05H, 10H, and
13H do nothing more than increment and decrement flags that indicate
whether these interrupts are being processed by ROM BIOS routines.
These flags are inspected by the TSR's Interrupt 08H and 28H handlers.
Executing the RAM-resident application
When the RAM-resident application is first activated, it runs in the
context of the program that was interrupted; that is, the contents of
the registers, the video display mode, the current PSP, and the
current DTA all belong to the interrupted program. The resident
application is responsible for preserving the registers and updating
MS-DOS with its PSP and DTA values.
The RAM-resident application preserves the previous contents of the
CPU registers on its own stack to avoid overflowing the interrupted
program's stack. It then installs its own handlers for Control-Break
(Interrupt 1BH), Control-C (Interrupt 23H), and critical error
(Interrupt 24H). (Otherwise, the interrupted program's handlers would
take control if the user pressed Ctrl-Break or Ctrl-C or if an MS-DOS
critical error occurred.) These handlers perform no action other than
setting flags that can be inspected later by the RAM-resident
application, which could then take appropriate action.
The application uses Interrupt 21H Functions 50H and 51H to update MS-
DOS with the address of its PSP. If the application is running under
MS-DOS versions 2.x, the critical error flag is set before Functions
50H and 51H are executed so that AuxStack is used for the call instead
of IOStack, to avoid corrupting IOStack in the event that InDOS is 1.
The application preserves the current extended error information with
a call to Interrupt 21H Function 59H. Otherwise, the RAM-resident
application might be activated immediately after a critical error
occurred in the interrupted program but before the interrupted program
had executed Function 59H and, if a critical error occurred in the TSR
application, the interrupted program's extended error information
would inadvertently be destroyed.
This example also shows how to update the MS-DOS default DTA using
Interrupt 21H Functions 1AH and 2FH, although in this case this step
is not necessary because the DTA is never used within the application.
In practice, the DTA should be updated only if the RAM-resident
application includes calls to Interrupt 21H functions that use a DTA
(Functions 11H, 12H, 14H, 15H, 21H, 22H, 27H, 28H, 4EH, and 4FH).
After the resident interrupt handlers are installed and the PSP, DTA,
and extended error information have been set up, the RAM-resident
application can safely execute any Interrupt 21H function calls except
those that use IOStack (Functions 01H through 0CH). These functions
cannot be used within a RAM-resident application even if the
application sets the critical error flag to force the use of the
auxiliary stack, because they also use other non-reentrant data
structures such as input/output buffers. Thus, a RAM-resident utility
must rely either on user-written console input/output functions or, as
in the example, on ROM BIOS functions.
The application terminates by returning the interrupted program's
extended error information, DTA, and PSP to MS-DOS, restoring the
previous Interrupt 1BH, 23H, and 24H handlers, and restoring the
previous CPU registers and stack.
Richard Wilton
Article 12: Exception Handlers
Exceptions are system events directly related to the execution of an
application program; they ordinarily cause the operating system to
abort the program. Exceptions are thus different from errors, which
are minor unexpected events (such as failure to find a file on disk)
that the program can be expected to handle appropriately. Likewise,
they differ from external hardware interrupts, which are triggered by
events (such as a character arriving at the serial port) that are not
directly related to the program's execution.
The computer hardware assists MS-DOS in the detection of some
exceptions, such as an attempt to divide by zero, by generating an
internal hardware interrupt. Exceptions related to peripheral devices,
such as an attempt to read from a disk drive that is not ready or does
not exist, are called critical errors. Instead of causing a hardware
interrupt, these exceptions are typically reported to the operating
system by device drivers. MS-DOS also supports a third type of
exception, which is triggered by the entry of a Control-C or
ControlBreak at the keyboard and allows the user to signal that the
current program should be terminated immediately.
MS-DOS contains built-in handlers for each type of exception and so
guarantees a minimum level of system stability that requires no effort
on the part of the application programmer. For some applications,
however, these default handlers are inadequate. For example, if a
communications program that controls the serial port directly with
custom interrupt handlers is terminated by the operating system
without being given a chance to turn off serial-port interrupts, the
next character that arrives on the serial line will trigger an
interrupt for which a handler is no longer present in memory. The
result will be a system crash. Accordingly, MS-DOS allows application
programs to install custom exception handlers so that they can shut
down operations in an orderly way when an exception occurs.
This article examines the default exception handlers provided by
MS-DOS and discusses methods programmers can use to replace those
routines with handlers that are more closely matched to specific
application requirements.
Overview
Two major exception handlers of importance to application programmers
are supported under all versions of MS-DOS. The first, the Control-C
exception handler, terminates the program and is invoked when the user
enters a Ctrl-C or Ctrl-Break keystroke; the address of this handler
is found in the vector for Interrupt 23H. The second, the critical
error exception handler, is invoked if MS-DOS detects a critical error
while servicing an I/O request. (A critical error is a hardware error
that makes normal completion of the request impossible.) This
exception handler displays the familiar Abort, Retry, Ignore prompt;
its address is saved in the vector for Interrupt 24H.
When a program begins executing, the addresses in the Interrupt 23H
and 24H vectors usually point to the system's default Control-C and
critical error handlers. If the program is a child process, however,
the vectors might point to exception handlers that belong to the
parent process, if the immediate parent is not COMMAND.COM. In any
case, the application program can install its own custom handler for
Control-C or critical error exceptions simply by changing the address
in the vector for Interrupt 23H or Interrupt 24H so that the vector
points to the application's own routine. When the program performs a
final exit by means of Interrupt 21H Function 00H (Terminate Process),
Function 31H (Terminate and Stay Resident), Function 4CH (Terminate
Process with Return Code), Interrupt 20H (Terminate Process), or
Interrupt 27H (Terminate and Stay Resident), MS-DOS restores the
previous contents of the Interrupt 23H and 24H vectors.
Note that Interrupts 23H and 24H never occur as externally generated
hardware interrupts in an MS-DOS system. The vectors for these
interrupts are used simply as storage areas for the addresses of the
exception handlers.
MS-DOS also contains default handlers for the Control-Break event
detected by the ROM BIOS in IBM PCs and compatible computers and for
some of the Intel microprocessor exceptions that generate actual
hardware interrupts. These exception handlers are not replaced by
application programs as often as the Control-C and critical error
handlers. The interrupt vectors that contain the addresses of these
handlers are not restored by MS-DOS when a program exits.
The address of the Control-Break handler is saved in the vector for
Interrupt 1BH and is invoked by the ROM BIOS whenever the Ctrl-Break
key combination is detected. The default MS-DOS handler normally
flushes the keyboard input buffer and substitutes Control-C for
Control-Break, and the Control-C is later handled by the Control-C
exception handler. The default handlers for exceptions that generate
hardware interrupts either abort the current program (as happens with
Divide by Zero) or bring the entire system to a halt (as for a memory
parity error).
The Control-C Handler
The vector for Interrupt 23H points to code that is executed whenever
MS-DOS detects a Control-C character in the keyboard input buffer.
When the character is detected, MS-DOS executes a software Interrupt
23H.
In response to Interrupt 23H, the default Control-C exception handler
aborts the current process. Files that were opened with handles are
closed (FCB-based files are not), but no other cleanup is performed.
Thus, unsaved data can be left in buffers, some files might not be
processed, and critical addresses, such as the vectors for custom
interrupt handlers, might be left pointing into free RAM. If more
complete control over process termination is wanted, the application
should replace the default Control-C handler with custom code. See
Customizing Control-C Handling below.
The Control-Break exception handler, pointed to by the vector for
Interrupt 1BH, is closely related to the Control-C exception handler
in MS-DOS systems on the IBM PC and close compatibles but is called by
the ROM BIOS keyboard driver on detection of the Ctrl-Break keystroke
combination. Because the Control-Break exception is generated by the
ROM BIOS, it is present only on IBM PC-compatible machines and is not
a standard feature of MS-DOS. The default ROM BIOS handler for
Control-Break is a simple interrupt return--in other words, no action
is taken to handle the keystroke itself, other than converting the
Ctrl-Break scan code to an extended character and passing it through
to MS-DOS as normal keyboard input.
To account for as many hardware configurations as possible, MS-DOS
redirects the ROM BIOS Control-Break interrupt vector to its own
Control-Break handler during system initialization. The MS-DOS
Control-Break handler sets an internal flag that causes the Ctrl-Break
keystroke to be interpreted as a Ctrl-C keystroke and thus causes
Interrupt 23H to occur.
Customizing Control-C handling
The exception handlers most often neglected by application
programmers--and most often responsible for major program failures--
are the default exception handlers invoked by the Ctrl-C and Ctrl-
Break keystrokes. Although the user must be able to recover from a
runaway condition (the reason for Ctrl-C capability in the first
place), any exit from a complex program must also be orderly, with
file buffers flushed to disk, directories and indexes updated, and so
on. The default Control-C and Control-Break handlers do not provide
for such an orderly exit.
The simplest and most direct way to deal with Ctrl-C and Ctrl-Break
keystrokes is to install new exception handlers that do nothing more
than an IRET and thus take MS-DOS out of the processing loop entirely.
This move is not as drastic as it sounds: It allows an application to
check for and handle the Ctrl-C and Ctrl-Break keystrokes at its
convenience when they arrive through the normal keyboard input
functions and prevents MS-DOS from terminating the program
unexpectedly.
The following example sets the Interrupt 23H and Interrupt 1BH vectors
to point to an IRET instruction. When the user presses Ctrl-C or Ctrl-
Break, the keystroke combination is placed into the keyboard buffer
like any other keystroke. When it detects the Ctrl-C or Ctrl-Break
keystroke, the executing program should exit properly (if that is the
desired action) after an appropriate shutdown procedure.
To install the new exception handlers, the following procedure (set_
int) should be called while the main program is initializing:
_DATA segment para public 'DATA'
oldint1b dd 0 ; original INT 1BH vector
oldint23 dd 0 ; original INT 23H vector
_DATA ends
_TEXT segment byte public 'CODE'
assume cs:_TEXT,ds:_DATA,es:NOTHING
myint1b: ; handler for Ctrl-Break
myint23: ; handler for Ctrl-C
iret
set_int proc near
mov ax,351bh ; get current contents of
int 21h ; Int 1BH vector and save it
mov word ptr oldint1b,bx
mov word ptr oldint1b+2,es
mov ax,3523h ; get current contents of
int 21h ; Int 23H vector and save it
mov word ptr oldint23,bx
mov word ptr oldint23+2,es
push ds ; save our data segment
push cs ; let DS point to our
pop ds ; code segment
mov dx,offset myint1b
mov ax,251bh ; set interrupt vector 1BH
int 21h ; to point to new handler
mov dx,offset myint23
mov ax,2523h ; set interrupt vector 23H
int 21h ; to point to new handler
pop ds ; restore our data segment
ret ; back to caller
set_int endp
_TEXT ends
The application can use the following routine to restore the original
contents of the vectors pointing to the Control-C and Control-Break
exception handlers before making a final exit back to MS-DOS. Note
that, although MS-DOS restores the Interrupt 23H vector to its
previous contents, the application must restore the Interrupt 1BH
vector itself.
rest_int proc near
push ds ; save our data segment
mov dx,word ptr oldint23
mov ds,word ptr oldint23+2
mov ax,2523h ; restore original contents
int 21h ; of Int 23H vector
pop ds ; restore our data segment
push ds ; then save it again
mov dx,word ptr oldint1B
mov ds,word ptr oldint1B+2
mov ax,251Bh ; restore original contents
int 21h ; of Int 1BH vector
pop ds ; get back our data segment
ret ; return to caller
rest_int endp
The preceding example simply prevents MS-DOS from terminating an
application when a Ctrl-C or Ctrl-Break keystroke is detected. Program
termination is still often the ultimate goal, but after a more orderly
shutdown than is provided by the MS-DOS default Control-C handler. The
following exception handler allows the program to exit more
gracefully:
myint1b: ; Control-Break exception handler
iret ; do nothing
myint23: ; Control-C exception handler
call safe_shut_down ; release interrupt vectors,
; close files, etc.
jmp program_exit_point
Note that because the Control-Break handler is invoked by the ROM BIOS
keyboard driver and MS-DOS is not reentrant, MS-DOS services (such as
closing files and terminating with return code) cannot be invoked
during processing of a Control-Break exception. In contrast, any
MS-DOS Interrupt 21H function call can be used during the processing
of a Control-C exception. Thus, the Control-Break handler in the
preceding example does nothing, whereas the Control-C handler performs
orderly shutdown of the application.
Most often, however, neither a handler that does nothing nor a handler
that shuts down and terminates is sufficient for processing a Ctrl-C
(or Ctrl-Break) keystroke. Rather than simply prevent Control-C
processing, software developers usually prefer to have a Ctrl-C
keystroke signal some important action without terminating the
program. Using methods similar to those above, the programmer can
replace Interrupts 1BH and 23H with a routine like the following:
myint1b: ; Control-Break exception handler
myint23: ; Control-C exception handler
call control_c_happened
iret
Notes on processing Control-C
The preceding examples assume the programmer wants to treat Control-C
and Control-Break the same way, but this is not always desirable.
Control-C and Control-Break are not the same, and the difference
between the two should be kept in mind: The Control-Break handler is
invoked by a keyboard-input interrupt and can be called at any time;
the Control-C handler is called only at "safe" points during the
processing of MS-DOS Interrupt 21H functions. Also, even though MS-DOS
restores the Interrupt 23H vector on exit from a program, the
application must restore the previous contents of the Interrupt 1BH
vector before exiting. If this interrupt vector is not restored, the
next Ctrl-Break keystroke will cause the machine to attempt to execute
an undetermined piece of code or data and will probably crash the
system.
Although it is generally desirable to take control of the Control-C
and Control-Break interrupts, control should be retained only as long
as necessary. For example, a RAM-resident pop-up application should
take over Control-C and Control-Break handling only when it is
activated, and it should restore the previous contents of the
Interrupt 1BH and Interrupt 23H vectors before it returns control to
the foreground process.
The Critical Error Handler
When MS-DOS detects a critical error--an error that prevents
successful completion of an I/O operation--it calls the exception
handler whose address is stored in the vector for Interrupt 24H.
Information about the operation in progress and the nature of the
error is passed to the exception handler in the CPU registers. In
addition, the contents of all the registers at the point of the
original MS-DOS call are pushed onto the stack for inspection by the
exception handler.
The action of MS-DOS's default critical error handler is to present a
message such as
Error type error action device
Abort, Retry, Ignore?
This message signals a hardware error from which MS-DOS cannot recover
without user intervention. For example, if the user enters the command
C>DIR A: <ENTER>
but drive A either does not contain a disk or the disk drive door is
open, the MS-DOS critical error handler displays the message
Not ready error reading drive A
Abort, Retry, Ignore?
I (Ignore) simply tells MS-DOS to forget that an error occurred and
continue on its way. (Of course, if the error occurred during the
writing of a file to disk, the file is generally corrupted; if the
error occurred during reading, the data might be incorrect.)
R (Retry) gives the application a second chance to access the device.
The critical error handler returns information to MS-DOS that says, in
effect, "Try again; maybe it will work this time." Sometimes, the
attempt succeeds (as when the user closes an open drive door), but
more often the same or another critical error occurs.
A (Abort) is the problem child of Interrupt 24H. If the user responds
with A, the application is terminated immediately. The directory
structure is not updated for open files, interrupt vectors are left
pointing to inappropriate locations, and so on. In many cases,
restarting the system is the only safe thing to do at this point.
Note: Beginning with version 3.3, an F (Fail) option appears in the
message displayed by MS-DOS's default critical error handler. When
Fail is selected, the current MS-DOS function is terminated and an
error condition is returned to the calling program. For example, if a
program calls Interrupt 21H Function 3DH to open a file on drive A but
the drive door is open, choosing F in response to the error message
causes the function call to return with the carry flag set, indicating
that an error occurred but processing continues.
Like the Control-C exception handler, the default critical error
exception handler can and should be replaced by an application program
when complete control of the system is desired. The program installs
its own handler simply by placing the address of the new handler in
the vector for Interrupt 24H; MS-DOS restores the previous contents of
the Interrupt 24H vector when the program terminates.
Unlike the Control-C handler, however, the critical error handler must
be kept within carefully defined limits to preserve the stability of
the operating system. Programmers must rigidly adhere to the structure
described in the following pages for passing information to and from
an Interrupt 24H handler.
Mechanics of critical error handling
MS-DOS critical error handling has two components: the exception
handler, whose address is saved in the Interrupt 24H vector and which
can be replaced by an application program; and an internal routine
inside MS-DOS. The internal routine sets up the information to be
passed to the exception handler on the stack and in registers and, in
turn, calls the exception handler itself. The internal routine also
responds to the values returned by the critical error handler when
that handler executes an IRET to return to the MS-DOS kernel.
Before calling the exception handler, MS-DOS arranges the stack
(Figure 12-1) so the handler can inspect the location of the error
and register contents at the point in the original MS-DOS function
call that led to the critical error.
┌───────────────┐
│ Flags │▒
├───────────────┤▒
│ CS │▒ Flags and CS:IP pushed on stack
├───────────────┤▒ by original Interrupt 21H call
│ IP │▒
├───────────────┤──SP on entry to Interrupt 21H handler
│ ES │▒
├───────────────┤▒
│ DS │▒
├───────────────┤▒
│ BP │▒
├───────────────┤▒
│ DI │▒
├───────────────┤▒ Registers at point of
│ SI │▒ original Interrupt 21H call
├───────────────┤▒
│ DX │▒
├───────────────┤▒
│ CX │▒
├───────────────┤▒
│ BX │▒
├───────────────┤▒
│ AX │▒
├───────────────┤
│ Flags │▒
├───────────────┤▒
│ CS │▒ Return address from
├───────────────┤▒ Interrupt 24H handler
│ IP │▒
└───────────────┘──SP on entry to Interrupt 24H handler
Figure 12-1. The stack contents at entry to a critical error exception
handler.
When the critical error handler is called by the internal routine,
four registers may contain important information: AX, DI, BP, and SI.
(With MS-DOS versions 1.x, only the AX and DI registers contain
significant information.) The information passed to the handler in the
registers differs somewhat, depending on whether a character device or
a block device is causing the error.
Block-device (disk-based) errors
If the critical error handler is entered in response to a block-device
(disk-based) error, registers BP:SI contain the segment:offset of the
device driver header for the device causing the error and bit 7 (the
high-order bit) of the AH register is zero. The remaining bits of the
AH register contain the following information (bits 3 through 5 apply
only to MS-DOS versions 3.1 and later):
╓┌────────────────┌───────────┌──────────────────────────────────────────────╖
Bit Value Meaning
──────────────────────────────────────────────────────────────────
0 0 Read operation
1 Write operation
1-2 Indicate the affected disk area:
00 MS-DOS
01 File allocation table
10 Root directory
11 Files area
3 0 Fail response not allowed
1 Fail response allowed
4 0 Retry response not allowed
1 Retry response allowed
5 0 Ignore response not allowed
1 Ignore response allowed
6 0 Undefined
The AL register contains the designation of the drive where the error
occurred; for example, AL = 00H (drive A), AL = 01H (drive B), and so
on.
The lower half of the DI register contains the following error codes
(the upper half of this register is undefined):
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Error Code Meaning
──────────────────────────────────────────────────────────────────
00H Write-protected disk
01H Unknown unit
02H Drive not ready
03H Invalid command
04H Data error (CRC)
05H Length of request structure invalid
06H Seek error
07H Non-MS-DOS disk
08H Sector not found
09H Printer out of paper
0AH Write fault
0BH Read fault
0CH General failure
0FH Invalid disk change (version 3.0 or later)
Note: With versions 1.x, the only valid error codes are 00H, 02H, 04H,
06H, 08H, 0AH, and 0CH.
Before calling the critical error handler for a disk-based error, MS-
DOS tries from one to five times to perform the requested read or
write operation, depending on the type of operation. Critical disk
errors result only from Interrupt 21H operations, not from failed
sector-read and sector-write operations attempted with Interrupts 25H
and 26H.
Character-device errors
If the critical error handler is called from the MS-DOS kernel with
bit 7 of the AH register set to 1, either an error occurred on a
character device or the memory image of the file allocation table is
bad (a rare occurrence). Again, registers BP:SI contain the segment
and offset of the device driver header for the device causing the
critical error. The exception handler can inspect bit 15 of the device
attribute word at offset 04H in the device header to confirm that the
error was caused by a character device--this bit is 0 for block
devices and 1 for character devices. See also PROGRAMMING IN THE MS-
DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Installable Device Drivers.
If the error was caused by a character device, the lower half of the
DI register contains error codes as described above and the contents
of the AL register are undefined. The exception handler can inspect
the other fields of the device header to obtain the logical name of
the character device; to determine whether that device is the standard
input, standard output, or both; and so on.
Critical error processing
The critical error exception handler is entered from MS-DOS with
interrupts disabled. Because an MS-DOS system call is already in
progress and MS-DOS is not reentrant, the handler cannot request any
MS-DOS system services other than Interrupt 21H Functions 01 through
0CH (character I/O functions), Interrupt 21H Function 30H (Get MS-DOS
Version Number), and Interrupt 21H Function 59H (Get Extended Error
Information). These functions use a special stack so that they can be
called during error processing.
In general, the critical error handler must preserve all but the AL
register. It must not change the contents of the device header pointed
to by BP:SI. The handler must return to the MS-DOS kernel with an
IRET, passing an action code in register AL as follows:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Value in AL Meaning
──────────────────────────────────────────────────────────────────
00H Ignore
01H Retry
02H Terminate process
03H Fail current system call
These values correspond to the options presented by the MS-DOS default
critical error handler. The default handler prompts the user for
input, places the appropriate return information in the AL register,
and immediately issues an IRET instruction.
Note: Although the Fail option is displayed by the MS-DOS default
critical error handler in versions 3.3 and later, the Fail option
inside the handler was added in version 3.1.
With MS-DOS versions 3.1 and later, if the handler returns an action
code in AL that is not allowed for the error in question (bits 3
through 5 of the AH register at the point of call), MS-DOS reacts
according to the following rules:
If Ignore is specified by AL = 00H but is not allowed because bit 5 of
AH = 0, the response defaults to Fail (AL = 03H).
If Retry is specified by AL = 01H but is not allowed because bit 4 of
AH = 0, the response defaults to Fail (AL = 03H).
If Fail is specified by AL = 03H but is not allowed because bit 3 of
AH = 0, the response defaults to Abort.
Custom critical error handlers
Each time it receives control, COMMAND.COM restores the Interrupt 24H
vector to point to the system's default critical error handler and
displays a prompt to the user. Consequently, a single custom handler
cannot terminate and stay resident to provide critical error handling
services for subsequent application programs. Each program that needs
better critical error handling than MS-DOS provides must contain its
own critical error handler.
Figure 12-2 contains a simple critical error handler, INT24.ASM,
written in assembly language. In the form shown, INT24.ASM is no more
than a functional replacement for the MS-DOS default critical error
handler, but it can be used as the basis for more sophisticated
handlers that can be incorporated into application programs.
──────────────────────────────────────────────────────────────────────
Figure 12-2. INT24.ASM, a replacement Interrupt 24 handler.
──────────────────────────────────────────────────────────────────────
INT24.ASM contains three routines:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Routine Action
──────────────────────────────────────────────────────────────────
get24 Saves the previous contents of the Interrupt 24H
critical error handler vector and stores the address
of the new critical error handler into the vector.
res24 Restores the address of the previous critical error
handler, which was saved by a call to get24, into
the Interrupt 24 vector.
int24 Replaces the MS-DOS critical error handler.
A program wishing to substitute the new critical error handler for the
system's default handler should call the get24 routine during its
initialization sequence. If the program wishes to revert to the
system's default handler during execution, it can accomplish this with
a call to the res24 routine. Otherwise, a call to res24 (and the
presence of the routine itself in the program) is not necessary,
because MS-DOS automatically restores the Interrupt 24H vector to its
previous value when the program exits, from information stored in the
program segment prefix (PSP).
The replacement critical error handler, int24, is simple. First it
saves all registers; then it displays a message that a critical error
has occurred and prompts the user to enter a key selecting one of the
four possible options: Abort, Retry, Ignore, or Fail. If an illegal
key is entered, the prompt is displayed again; otherwise, the action
code corresponding to the key is extracted from a table and placed in
the AL register, the other registers are restored, and control is
returned to the MS-DOS kernel with an IRET instruction.
Note that the handle read and write functions (Interrupt 21H Functions
3FH and 40H), which would normally be preferred for interaction with
the display and keyboard, cannot be used in a critical error handler.
Hardware-generated Exception Interrupts
Intel reserved the vectors for Interrupts 00H through 1FH (Table 12-1)
for exceptions generated by the execution of various machine
instructions. Handling of these chip-dependent internal interrupts can
vary from one make of MS-DOS machine to another; some such differences
are mentioned in the discussion.
Table 12-1. Intel Reserved Exception Interrupts.
╓┌──────────────────────┌────────────────────────────────────────────────────╖
Interrupt Number Definition
──────────────────────────────────────────────────────────────────
00H Divide by Zero
01H Single-Step
02H Nonmaskable Interrupt (NMI)
03H Breakpoint Trap
04H Overflow Trap
05H BOUND Range Exceeded
06H Invalid Opcode
07H Coprocessor not Available
08H Double-Fault Exception
09H Coprocessor Segment Overrun
0AH Invalid Task State Segment (TSS)
0BH Segment not Present
0CH Stack Exception
0DH General Protection Exception
0EH Page Fault
0FH (Reserved)
10H Coprocessor Error
11-1FH (Reserved)
Note: A number of these reserved exception interrupts generally do not
occur in MS-DOS because they are generated only when the 80286 or
80386 microprocessor is operating in protected mode. The following
discussions do not cover these interrupts.
Divide by Zero (Interrupt 00H)
An Interrupt 00H occurs whenever a DIV or IDIV operation fails to
terminate within a reasonable period of time. The interrupt is
triggered by a mathematical anomaly: Division by zero is inherently
undefined. To handle such situations, Intel built special processing
into the DIV and IDIV instructions to ensure that the condition does
not cause the processor to lock up. Although the assumption underlying
Interrupt 00H is an attempt to divide by zero (a condition that will
never terminate), the interrupt can also be triggered by other error
conditions, such as a quotient that is too large to fit in the
designated register (AX or AL).
The ROM BIOS handler for Interrupt 00H in the IBM PC and close
compatibles is a simple IRET instruction. During the MS-DOS startup
process, however, MS-DOS modifies the interrupt vector to point to its
own handler--a routine that issues the warning message Divide by Zero
and aborts the current application. This abort procedure can leave the
computer and operating system in an extremely unstable state. If the
default handler is used, the system should be restarted immediately
and an attempt should be made to find and eliminate the cause of the
error. A better approach, however, is to provide a replacement handler
that treats Interrupt 00H much as MS-DOS treats Interrupt 24H.
Single-Step (Interrupt 01H)
If the trap flag (bit 8 of the microprocessor's 16-bit flags register)
is set, Interrupt 01H occurs at the end of every instruction executed
by the processor. By default, Interrupt 01H points to a simple IRET
instruction, so the net effect is as if nothing happened. However,
debugging programs, which are the only applications that use this
interrupt, modify the interrupt vector to point to their own handlers.
The interrupt can then be used to allow a debugger to single-step
through the machine instructions of the program being debugged, as
DEBUG does with its T (Trace) command.
Nonmaskable Interrupt, or NMI (Interrupt 02H)
In the hardware architecture of IBM PCs and close compatibles,
Interrupt 02H is invoked whenever a memory parity error is detected.
MS-DOS provides no handler, because this error, as a hardware-related
problem, is in the domain of the ROM BIOS.
In response to the Interrupt 02H, the default ROM BIOS handler
displays a message and locks the machine, on the assumption that bad
memory prevents reliable system operation. Many programmers, however,
prefer to include code that permits orderly shutdown of the system.
Replacing the ROM BIOS parity trap routine can be dangerous, though,
because a parity error detected in memory means the contents of RAM
are no longer reliable--even the memory locations containing the NMI
handler itself might be defective.
Breakpoint Trap (Interrupt 03H)
Interrupt 03H, which is used in conjunction with Interrupt 01H for
debugging, is invoked by a special 1-byte opcode (0CCH). During a
debugging session, a debugger modifies the vector for Interrupt 03H to
point to its own handler and then replaces 1 byte of program opcode
with the 0CCH opcode at any location where a breakpoint is needed.
When a breakpoint is reached, the 0CCH opcode triggers Interrupt 03H
and the debugger regains control. The debugger then restores the
original opcode in the program being debugged and issues a prompt so
that the user can display or alter the contents of memory or
registers. The use of Interrupt 03H is illustrated by DEBUG and
SYMDEB's breakpoint capabilities.
Overflow Trap (Interrupt 04H)
If the overflow bit (bit 11) in the microprocessor's flags register is
set, Interrupt 04H occurs when the INTO (Interrupt on Overflow)
instruction is executed. The overflow bit can be set during prior
execution of any arithmetic instruction (such as MUL or IMUL) that can
produce an overflow error.
The ROM BIOS of the IBM PC and close compatibles initializes the
Interrupt 04H vector to point to an IRET, so this interrupt becomes
invisible to the user if it is executed. MS-DOS does not have its own
handler for Interrupt 04H. However, because the Intel microprocessors
also include JO (Jump if Overflow) and JNO (Jump if No Overflow)
instructions, applications rarely need the INTO instruction and,
hence, seldom have to provide their own Interrupt 04H handlers.
BOUND Range Exceeded (Interrupt 05H)
Interrupt 05H is generated on 80186, 80286, and 80386 microprocessors
if a BOUND instruction is executed to test the value of an array index
and the index falls outside the limits specified by the instruction's
operand. The exception handler is expected to alter the index so that
it is correct--when the handler performs an interrupt return (IRET),
the CPU reexecutes the BOUND instruction that caused the interrupt.
On IBM PC/AT-compatible machines, the ROM BIOS assignment of the PrtSc
(print screen) routine to Interrupt 05H is in conflict with the CPU's
use of Interrupt 05H for BOUND exceptions.
Invalid opcode (Interrupt 06H)
Interrupt 06H is generated by the 80186, 80286, and 80386
microprocessors if the current instruction is not a valid opcode--for
example, if the machine tries to execute a data statement.
On IBM PC/ATs, Interrupt 06H simply points to an IRET instruction. The
ROM BIOS routines of some IBM PC/AT-compatibles, however, provide an
interrupt handler that reports an unexpected software Interrupt 06H
and asks if the user wants to continue. A Y response causes the
interrupt handler to skip over the invalid opcode. Unfortunately,
because the succeeding opcode is often invalid as well, the user may
have the feeling of being trapped in a loop.
Extended Error Information
Under MS-DOS versions 1.x, the operating system provided limited
information about errors that occurred during calls to the Interrupt
21H system functions. For example, if a program called Function 0FH to
open a file, there were only two possible results: On return, the AL
register either contained 00H for a successful open or 0FFH for
failure. No further detail was available from the operating system.
Although some of these early system calls (such as the read and write
functions) returned somewhat more information, the 1.x versions of MS-
DOS were essentially limited to success/failure return codes.
Beginning with version 2.0 and the introduction of the handle concept,
additional error information became available. See PROGRAMMING IN THE
MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: File and Record
Management. For example, if a program attempts to open a file with
Interrupt 21H Function 3DH (Open File with Handle), it can check the
status of the carry flag on return to detect whether an error
occurred. If the carry flag is not set, the call was successful and
the AX register contains the file handle. If the carry flag is set,
the AX register contains one of the following possible error codes:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Error Code Meaning
──────────────────────────────────────────────────────────────────
01H Invalid function code
02H File not found
03H Path not found
04H Too many open files (no more handles available)
05H Access denied
0CH Invalid access code
In some circumstances, however, even these error codes do not provide
enough information. Therefore, beginning with version 3.0, MS-DOS made
extended error information available through Interrupt 21H Function
59H (Get Extended Error Information). This function can be called
after any other Interrupt 21H function fails, or it can be called from
a critical error handler. The extended error codes, briefly described
below, maintain compatibility with the MS-DOS versions 2.x error
returns and are grouped as follows:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Error Code Error Group
──────────────────────────────────────────────────────────────────────
00H No error encountered.
01-12H MS-DOS versions 2.x and 3.x Interrupt 21H errors.
These error codes are identical to those returned in
the AX register by Functions 38H through 57H if the
carry flag is set on return from the function call.
13-1FH MS-DOS versions 2.x and 3.x Interrupt 24H errors.
These error codes are 13H (19) greater than the
codes passed to a critical error handler in the
lower half of the DI register; that is, if the
critical error handler receives error code 04H (CRC
error), Interrupt 21H Function 59H returns 17H.
20-58H Extended error codes, many related to networking and
file sharing, for MS-DOS versions 3.0 and later.
Note: The contents of the CPU registers (except CS:IP and SS:SP) are
destroyed by a call to Function 59H. Also, as mentioned earlier, this
function is available only with MS-DOS versions 3.x, even though it
maintains compatibility with error returns in versions 2.x.
On return, Function 59H provides the extended error code in the AX
register, the error class (type) in the BH register, a code for the
suggested corrective action in the BL register, and the locus of the
error in the CH register. These values are defined in the following
paragraphs. With MS-DOS or PC-DOS versions 3.x, if an error 22H
(invalid disk change) occurs and if the capability is supported by the
system's block-device drivers, ES:DI points to an ASCIIZ volume label
that designates the disk to be inserted in the drive before the
operation is retried.
Error Code (AX register). This value is defined as follows:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Value in AX Meaning
──────────────────────────────────────────────────────────────────
Interrupt 21H errors (MS-DOS versions 2.0 and later):
01H Invalid function number
02H File not found
03H Path not found
04H Too many open files (no handles available)
05H Access denied
06H Invalid handle
07H Memory control blocks destroyed
08H Insufficient memory
09H Invalid memory-block address
0AH Invalid environment
0BH Invalid format
0CH Invalid access code
0DH Invalid data
0EH Reserved
0FH Invalid disk drive specified
10H Attempt to remove the current directory
11H Not same device
12H No more files
──────────────────────────────────────────────────────────────────
Interrupt 24H errors (MS-DOS versions 2.0 and later):
13H Attempt to write on write-protected disk
14H Unknown unit
15H Drive not ready
16H Invalid command
17H Data error based on cyclic redundancy check (CRC)
18H Length of request structure invalid
19H Seek error
1AH Unknown media type (non-MS-DOS disk)
1BH Sector not found
1CH Printer out of paper
10H Write fault
1EH Read fault
1FH General failure
──────────────────────────────────────────────────────────────────
MS-DOS versions 3.x extended errors:
20H Sharing violation
21H Lock violation
22H Invalid disk change
23H FCB unavailable
24H Sharing buffer exceeded
25H-31H Reserved
32H Network request not supported
33H Remote computer not listening
34H Duplicate name on network
35H Network name not found
36H Network busy
37H Device no longer exists on network
38H Net BIOS command limit exceeded
39H Error in network adapter hardware
3AH Incorrect response from network
3BH Unexpected network error
3CH Incompatible remote adapter
3DH Print queue full
3EH Queue not full
3FH Not enough room for print file
40H Network name deleted
41H Access denied
42H Incorrect network device type
43H Network name not found
44H Network name limit exceeded
45H Net BIOS session limit exceeded
46H Temporary pause
47H Network request not accepted
48H Print or disk redirection paused
49H-4FH Reserved
50H File already exists
51H Reserved
52H Cannot make directory
53H Failure on Interrupt 24H
54H Out of structures
55H Already assigned
56H Invalid password
57H Invalid parameter
58H Network write fault
Locus (CH register). This value provides information on the location
of the error:
╓┌────────────────────────┌──────────────────────────────────────────────────╖
Value in CH Meaning
──────────────────────────────────────────────────────────────────
01H Location unknown
02H Block device; generally caused by a disk error
03H Network
04H Serial device; generally caused by a timeout from
a character device
05H Memory; caused by an error in RAM
Error Class (BH register). This value gives the general category of
the error:
╓┌────────────────────────┌──────────────────────────────────────────────────╖
Value in BH Meaning
──────────────────────────────────────────────────────────────────
01H Out of resource; out of storage space or I/O
channels.
02H Temporary situation; expected to clear, as in a
file or record lock--generally occurs only in a
network environment.
03H Authorization; a problem with permission to access
the requested device.
04H Internal error in system software; generally
reflects a system software bug rather than an
application or system failure.
05H Hardware failure; a serious hardware-related
problem not the fault of the user program.
06H System failure; a serious failure of the system
software, not directly the fault of the
application--generally occurs if configuration
files are missing or incorrect.
07H Application-program error; generally caused by
inconsistent function requests from the user
program.
08H File or item not found.
09H File or item of invalid format or type detected,
or an otherwise unsuitable or invalid item
requested.
0AH File or item interlocked by the system.
0BH Media failure; generally occurs with a bad disk in
a drive, a bad spot on the disk, or the like.
0CH Already exists; generally occurs when application
tries to declare a machine name or device that
already exists.
0DH Unknown.
Suggested Action (BL register). One of the most useful returns from
Function 59H, this value suggests a corrective action to try:
╓┌────────────────────────┌──────────────────────────────────────────────────╖
Value in BL Meaning
──────────────────────────────────────────────────────────────────
01H Retry a few times before prompting the user to
choose Ignore for the program to continue or
Abort to terminate.
02H Pause for a few seconds between retries and then
prompt user as above.
03H Ask user to reenter the input. In most cases, this
solution applies when an incorrect drive
specifier or filename was entered. Of course, if
the value was hard-coded into the program, the
user should not be prompted for input.
04H Clean up as well as possible, then abort the
application. This solution applies when the error
is destructive enough that the application cannot
safely proceed, but the system is healthy enough
to try an orderly shutdown of the application.
05H Exit from the application as soon as possible,
without trying to close files and clean up. This
means something is seriously wrong with either
the application or the system.
06H Ignore; error is informational.
07H Prompt user to perform some action, such as
changing floppy disks in a drive and then retry.
Function 59H and older system calls
The Interrupt 21H functions--primarily the FCB-related file and record
calls--that return 0FFH in the AL register to indicate that an error
has occurred but provide no further information about the type of
error include
╓┌─────────────────┌─────────────────────────────────────────────────────────╖
Function Name
──────────────────────────────────────────────────────────────────
0FH Open File with FCB
10H Close File with FCB
11H Find First File
12H Find Next File
13H Delete File
16H Create File with FCB
17H Rename File
23H Get File Size
These function calls now exist only to maintain compatibility with
MS-DOS versions 1.x. The preferred choices are the handle-style calls
available in MS-DOS versions 2.0 and later, which offer full path
support and much better error reporting. See also SYSTEM CALLS.
If the older calls must be used, the program can use Function 59H to
obtain more detailed information under MS-DOS version 3.0 or later.
For example:
myfcb db 0 ; drive = default
db 'MYFILE ' ; filename, 8 chars
db 'DAT' ; extension, 3 chars
db 25 dup (0) ; remainder of FCB
.
.
.
mov dx,seg myfcb ; DS:DX = FCB
mov ds,dx
mov dx,offset myfcb
mov ah,0fh ; function 0FH = Open FCB
int 21h ; transfer to MS-DOS
or al,al ; test status
jz success ; jump, open succeeded
; open failed, get
; extended error info
mov bx,0 ; BX = 00H for ver. 2.x-3.x
mov ah,59h ; function 59H = Get Info
int 21h ; transfer to MS-DOS
or ax,ax ; really an error?
jz success ; no error, jump
; test recommended actions
cmp bl,01h
jz retry ; if BL = 01H retry operation
cmp bl,04h
jz cleanup ; if BL = 04H clean up and exit
cmp bl,05h
jz panic ; if BL = 05H exit immediately
.
.
.
Function 59H and newer system calls
The function calls listed below were added in MS-DOS versions 2.0 and
later. These calls return with the carry flag set if an error occurs;
in addition, the AX register contains an error value corresponding to
error codes 01H through 12H of the extended error return codes:
╓┌─────────────────┌─────────────────────────────────────────────────────────╖
Function Name
──────────────────────────────────────────────────────────────────
MS-DOS versions 2.0 and later:
38H Get/Set Current Country
39H Create Directory
3AH Remove Directory
3BH Change Current Directory
3CH Create File with Handle
3DH Open File with Handle
3EH Close File
3FH Read File or Device
40H Write File or Device
41H Delete File
42H Move File Pointer
43H Get/Set File Attributes
44H IOCTL (I/O Control for Devices)
45H Duplicate File Handle
46H Force Duplicate File Handle
47H Get Current Directory
48H Allocate Memory Block
49H Free Memory Block
4AH Resize Memory Block
4BH Load and Execute Program (EXEC)
4EH Find First File
4FH Find Next File
56H Rename File
57H Get/Set Date/Time of File
──────────────────────────────────────────────────────────────────
MS-DOS versions 3.0 and later:
58H Get/Set Allocation Strategy
5AH Create Temporary File
5BH Create New File
5CH Lock/Unlock File Region
──────────────────────────────────────────────────────────────────
MS-DOS versions 3.1 and later:
5EH Network Machine Name/Printer Setup
5FH Get/Make Assign List Entry
Although these newer functions have much better error reporting than
the older FCB functions, Function 59H is still useful. Regardless of
the version of MS-DOS that is running, the error code returned in the
AX register from an Interrupt 21H function call is always in the range
0-12H. If a program is running under MS-DOS versions 3.x and wants to
obtain one or more of the more specific error codes in the range 20-
58H, the program must follow the failed Interrupt 21H call with a
subsequent call to Interrupt 21H Function 59H. The program can then
use the code returned by Function 59H in the BL register as a guide to
the action to take in response to the error. For example:
myfile db 'MYFILE.DAT',0 ; ASCIIZ filename
.
.
.
mov dx,seg myfile ; DS:DX = ASCIIZ filename
mov ds,dx
mov dx,offset myfile
mov ax,3d02h ; open, read/write
int 21h ; transfer to MS-DOS
jnc success ; jump, open succeeded
; open failed, get
; extended error info
mov bx,0 ; BX = 00H for ver. 2.x-3.x
mov ah,59h ; function 59H = Get Info
int 21h ; transfer to MS-DOS
or ax,ax ; really an error?
jz success ; no error, jump
; test recommended actions
cmp bl,01h
jz retry ; if BL = 01H retry operation
.
.
.
If the standard critical error handler is replaced with a customized
critical handler, Function 59H can also be used to obtain more
detailed information about an error inside the handler before either
returning control to the application or aborting. The value in the BL
register should be used to determine the appropriate action to take or
the message to display to the user.
Jim Kyle
Chip Rabinowitz
Article 13: Hardware Interrupt Handlers
Unlike software interrupts, which are service requests initiated by a
program, hardware interrupts occur in response to electrical signals
received from a peripheral device such as a serial port or a disk
controller, or they are generated internally by the microprocessor
itself. Hardware interrupts, whether external or internal to the
microprocessor, are given prioritized servicing by the Intel CPU
architecture.
The 8086 family of microprocessors (which includes the 8088, 8086,
80186, 80286, and 80386) reserves the first 1024 bytes of memory
(addresses 0000:0000H through 0000:03FFH) for a table of 256 interrupt
vectors, each a 4-byte far pointer to a specific interrupt service
routine (ISR) that is carried out when the corresponding interrupt is
processed. The design of the 8086 family requires certain of these
interrupt vectors to be used for specific functions (Table 13-1).
Although Intel actually reserves the first 32 interrupts, IBM, in the
original PC, redefined usage of Interrupts 05H to 1FH. Most, but not
all, of these reserved vectors are used by software, rather than
hardware, interrupts; the redefined IBM uses are listed in Table 13-2.
Table 13-1. Intel Reserved Exception Interrupts.
╓┌────────────────────────┌──────────────────────────────────────────────────╖
Interrupt Number Definition
──────────────────────────────────────────────────────────────────
00H Divide by zero
01H Single step
02H Nonmaskable interrupt (NMI)
03H Breakpoint trap
04H Overflow trap
05H BOUND range exceeded
06H Invalid opcode
07H Coprocessor not available
08H Double-fault exception
09H Coprocessor segment overrun
0AH Invalid task state segment (TSS)
0BH Segment not present
0CH Stack exception
0DH General protection exception
0EH Page fault
0FH (Reserved)
10H Coprocessor error
Table 13-2. IBM Interrupt Usage.
╓┌────────────────────────┌──────────────────────────────────────────────────╖
Interrupt Number Definition
──────────────────────────────────────────────────────────────────
05H Print screen
06H Unused
07H Unused
08H Hardware IRQ0 (timer-tick)
09H Hardware IRQ1 (keyboard)
0AH Hardware IRQ2 (reserved)
0BH Hardware IRQ3 (COM2)
0CH Hardware IRQ4 (COM1)
0DH Hardware IRQ5 (fixed disk)
0EH Hardware IRQ6 (floppy disk)
0FH Hardware IRQ7 (printer)
10H Video service
11H Equipment information
12H Memory size
13H Disk I/O service
14H Serial-port service
15H Cassette/network service
16H Keyboard service
17H Printer service
18H ROM BASIC
19H Restart system
1AH Get/Set time/date
1BH Control-Break (user defined)
1CH Timer tick (user defined)
1DH Video parameter pointer
1EH Disk parameter pointer
1FH Graphics character table
Nestled in the middle of Table 13-2 are the eight hardware interrupt
vectors (08-0FH) IBM implemented in the original PC design. These
eight vectors provide the maskable interrupts for the IBM PC-family
and close compatibles. Additional IRQ lines built into the IBM PC/AT
are discussed under The IRQ Levels below.
The conflicting uses of the interrupts listed in Tables 13-1 and 13-2
have created compatibility problems as the 8086 family of
microprocessors has developed. For complete compatibility with IBM
equipment, the IBM usage must be followed even when it conflicts with
the chip design. For example, a BOUND error occurs if an array index
exceeds the specified upper and lower limits (bounds) of the array,
causing an Interrupt 05H to be generated. But the 80286 processor used
in all AT-class computers will, if a BOUND error occurs, send the
contents of the display to the printer, because IBM uses Interrupt 05H
for the Print Screen function.
Hardware Interrupt Categories
The 8086 family of microprocessors can handle three types of hardware
interrupts. First are the internal, microprocessor-generated exception
interrupts (Table 13-1). Second is the nonmaskable interrupt, or NMI
(Interrupt 02H), which is generated when the NMI line (pin 17 on the
8088 and 8086, pin 59 on the 80286, pin B8 on the 80386) goes high
(active). In the IBM PC family (except the PCjr and the Convertible),
the nonmaskable interrupt is designated for memory parity errors.
Third are the maskable interrupts, which are usually generated by
external devices.
Maskable interrupts are routed to the main processor through a chip
called the 8259A Programmable Interrupt Controller (PIC). When it
receives an interrupt request, the PIC signals the microprocessor that
an interrupt needs service by driving the interrupt request (INTR)
line of the main processor to high voltage level. This article focuses
on the maskable interrupts and the 8259A because it is through the PIC
that external I/O devices (disk drives, serial communication ports,
and so forth) gain access to the interrupt system.
Interrupt priorities in the 8086 family
The Intel microprocessors have a built-in priority system for handling
interrupts that occur simultaneously. Priority goes to the internal
instruction exception interrupts, such as Divide by Zero and Invalid
Opcode, because priority is determined by the interrupt number:
Interrupt 00H takes priority over all others, whereas the last
possible interrupt, 0FFH, would, if present, never be allowed to break
in while another interrupt was being serviced. However, if interrupt
service is enabled (the microprocessor's interrupt flag is set), any
hardware interrupt takes priority over any software interrupt (INT
instruction).
The priority sequencing by interrupt number must not be confused with
the priority resolution performed by hardware external to the
microprocessor. The numeric priority discussed here applies only to
interrupts generated within the 8086 family of microprocessor chips
and is totally independent of system interrupt priorities established
for components external to the microprocessor itself.
Interrupt service routines
For the most part, programmers need not write hardware-specific
program routines to service the hardware interrupts. The IBM PC BIOS
routines, together with MS-DOS services, are usually sufficient. In
some cases, however, MS-DOS and the ROM BIOS do not provide enough
assistance to ensure adequate performance of a program. Most notable
in this category is communications software, for which programmers
usually must access the 8259A and the 8250 Universal Asynchronous
Receiver and Transmitter (UART) directly. See PROGRAMMING IN THE
MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: Interrupt-Driven
Communications.
Characteristics of Maskable Interrupts
Two major characteristics distinguish maskable interrupts from all
other events that can occur in the system: They are totally
unpredictable, and they are highly volatile. In general, a hardware
interrupt occurs when a peripheral device requires the full attention
of the system and data will be irretrievably lost unless the system
responds rapidly.
All things are relative, however, and this is especially true of the
speed required to service an interrupt request. For example, assume
that two interrupt requests occur at essentially the same time. One is
from a serial communications port receiving data at 300 bps; the other
is from a serial port receiving data at 9600 bps. Data from the first
serial port will not change for at least 30 milliseconds, but the
second serial port must be serviced within one millisecond to avoid
data loss.
Unpredictability
Because maskable interrupts generally originate in response to
external physical events, such as the receipt of a byte of data over a
communications line, the exact time at which such an interrupt will
occur cannot be predicted. Even the timer interrupt request, which by
default occurs approximately 18.2 times per second, cannot be
predicted by any program that happens to be executing when the
interrupt request occurs.
Because of this unpredictability, the system must, if it allows any
interrupts to be recognized, be prepared to service all maskable
interrupt requests. Conversely, if interrupts cannot be serviced, they
must all be disabled. The 8086 family of microprocessors provides the
Set Interrupt Flag (STI) instruction to enable maskable interrupt
response and the Clear Interrupt Flag (CLI) instruction to disable it.
The interrupt flag is also cleared automatically when a hardware
interrupt response begins; the interrupt handler should execute STI as
quickly as possible to allow higher priority interrupts to be
serviced.
Volatility
As noted earlier, a maskable interrupt request must normally be
serviced immediately to prevent loss of data, but the concept of
immediacy is relative to the data transfer rate of the device
requesting the interrupt. The rule is that the currently available
unit of data must be processed (at least to the point of being stored
in a buffer) before the next such item can arrive. Except for such
devices as disk drives, which always require immediate response,
interrupts for devices that receive data are normally much more
critical than interrupts for devices that transmit data.
The problems imposed by data volatility during hardware interrupt
service are solved by establishing service priorities for interrupts
generated outside the microprocessor chip itself. Devices with the
slowest transfer rates are assigned lower interrupt service
priorities, and the most time-critical devices are assigned the
highest priority of interrupt service.
Handling Maskable Interrupts
The microprocessor handles all interrupts (maskable, nonmaskable, and
software) by pushing the contents of the flags register onto the
stack, disabling the interrupt flag, and pushing the current contents
of the CS:IP registers onto the stack.
The microprocessor then takes the interrupt number from the data bus,
multiplies it by 4 (the size of each vector in bytes), and uses the
result as an offset into the interrupt vector table located in the
bottom 1 KB (segment 0000H) of system RAM. The 4-byte address at that
location is then used as the new CS:IP value (Figure 13-1).
┌────────────────────┐
│ Push flags │
└─────────┬──────────┘
┌───────────────────┐
│ Disable interrupts │
└─────────┬──────────┘
┌───────────────────┐
│ Push CS:IP │
└─────────┬──────────┘
┌───────────────────┐
│ Get address of ISR │
│ from table; │
│ place in CS:IP │
└─────────┬──────────┘
┌───────────────────┐
│ Process interrupt │
└─────────┬──────────┘
┌───────────────────┐
│ IRET │
└─────────┬──────────┘
┌───────────────────┐
│Restore CS:IP, flags│
└────────────────────┘
Figure 13-1. General interrupt sequence.
External devices are assigned dedicated interrupt request lines (IRQs)
associated with the 8259A. See The IRQ Levels below. When a device
requires attention, it sends a signal to the PIC via its IRQ line. The
PIC, which functions as an "executive secretary" for the external
devices, operates as shown in Figure 13-2. It evaluates the service
request and, if appropriate, causes the microprocessor's INTR line to
go high. The microprocessor then checks whether interrupts are enabled
(whether the interrupt flag is set). If they are, the flags are pushed
onto the stack, the interrupt flag is disabled, and CS:IP is pushed
onto the stack.
DEVICE 8259A MICROPROCESSOR
┌────────┐
│ ┌────────┐
│ │Process │────┐
/ \ │ └────┬───┘ │
┌───────────────┐IRQ / Any \ No │ │
│Signals request├─── IRQs ───┘ │
└───────────────┘ \active?/ / \ │
\ / │ / INTR \ No │
│ ┌─ high? ────┘
Yes │ │ \ /
│ │ \ / │
/ \ │ │ │
/Is this\ Yes│ │ Yes │
INT ───┘ │ │
\masked off? │ / \ │
\ / │ │ / INTs \ No │
│ │ enabled?────┘
No │ │ \ /
│ │ \ /
/ \ │ │
/ INT \ Yes│ │ Yes
being ───┘ │ ┌────────────┐
\serviced? │ │ Push flags │
\ / │ └────────────┘
No │
│ ┌─────────────┐
┌──────────────┐ │ │ Disable INTs│
│Signal request├───┘ └──────┬──────┘
└──────────────┘INTR
┌─────────────┐
│ Push CS:IP │
└──────┬──────┘
┌─────────────┐
──── │ Acknowledge │
INTA │ INT │
┌────────────────┐─────────────┴─────────────┘
│Place INT number│
│ on data bus ├─────────────┌─────────────┐
└────────────────┘ Data bus │ Get INT │
│ number │
└──────┬──────┘
┌─────────────┐
│ Calculate │
│ new CS:IP │
└─────────────┘
Figure 13-2. Maskable interrupt service.
The microprocessor acknowledges the interrupt request by signaling the
8259A via the interrupt acknowledge (INTA) line. The 8259A then places
the interrupt number on the data bus. The microprocessor gets the
interrupt number from the data bus and services the interrupt. Before
issuing the IRET instruction, the interrupt service routine must issue
an end-of-interrupt (EOI) sequence to the 8259A so that other
interrupts can be processed. This is done by sending 20H to port 20H.
(The similarity of numbers is pure coincidence.) The EOI sequence is
covered in greater detail elsewhere. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: Interrupt-Driven Communications.
The 8259A Programmable Interrupt Controller
The 8259A (Figure 13-3) has a number of internal components, many of
them under software control. Only the default settings for the IBM PC
family are covered here.
Three registers influence the servicing of maskable interrupts: the
interrupt request register (IRR), the in-service register (ISR), and
the interrupt mask register (IMR).
The IRR is used to keep track of the devices requesting attention.
When a device causes its IRQ line to go high to signal the 8259A that
it needs service, a bit is set in the IRR that corresponds to the
interrupt level of the device.
The ISR specifies which interrupt levels are currently being serviced;
an ISR bit is set when an interrupt has been acknowledged by the CPU
(via INTA) and the interrupt number has been placed on the data bus.
The ISR bit associated with a particular IRQ remains set until an EOI
sequence is received.
The IMR is a read/write register (at port 21H) that masks (disables)
specific interrupts. When a bit is set in this register, the
corresponding IRQ line is masked and no servicing for it is performed
until the bit is cleared. Thus, a particular IRQ can be disabled while
all others continue to be serviced.
The fourth major block in Figure 13-3, labeled Priority resolver, is a
complex logical circuit that forms the heart of the 8259A. This
component combines the statuses of the IMR, the ISR, and the IRR to
determine which, if any, pending interrupt request should be serviced
and then causes the microprocessor's INTR line to go high. The
priority resolver can be programmed in a number of modes, although
only the mode used in the IBM PC and close compatibles is described
here.
<────────────────────────────────────────────────────────>
< >
< DATA BUS >
< >
<───────────────────────────────────────────────────────>
║
║
<──────╨─────────────────────────────────────────────────>
< >
< CONTROL BUS >
< >
<──────╥──────────┬─────────────────────────────────────>
║ │ ──── │
║ │ INTA │ INT
- - - -║- - - - - │ - - - - - - - - - - - - - -│- - - - - -
| ║ ┌─────────────────────────────────┴───┐ |
| ║ │ Control logic │ |
| ║ │ │ |
| └┬───────────────────────────────────┘ |
| <────────────┴──────────────────┴─────────────────┴──────> |
| < INTERNAL BUS > |
| <──────────┬──────────────────┬─────────────────┬─────> |
| ║ ┌───╨────────┐ ┌────────────┐ ┌──────┴──╨───┐ |
| ║ │ │ │ │ │ │--IRQ0 ▒
| ║ │ │ │ │ │ │--IRQ1 ▒
| ║ │ In-service │ │ Priority │ │ Interrupt │--IRQ2 ▒
| ║ │ register │══│ resolver │══╡ request │--IRQ3 ▒IRQ
| ║ │ (ISR) │ │ │ │ register │--IRQ4 ▒ lines
| ║ │ │ │ │ │ (IRR) │--IRQ5 ▒
| ║ │ │ │ │ │ │--IRQ6 ▒
| ║ │ │ │ │ │ │--IRQ7 ▒
| ║ └─────────────┘ └─────────────┘ └─────────────┘ |
| ║ |
| ║ ┌───┴──────────────┴─────────────┴───┐ |
| ║ │ Interrupt mask register │ |
| ╚════════│ (IMR) │ |
| └────────────────────────────────────┘ |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Figure 13-3. Block diagram of the 8259A Programmable Interrupt
Controller.
The IRQ levels
When two or more unserviced hardware interrupts are pending, the 8259A
determines which should be serviced first. The standard mode of
operation for the PIC is the fully nested mode, in which IRQ lines are
prioritized in a fixed sequence. Only IRQ lines with higher priority
than the one currently being serviced are permitted to generate new
interrupts.
The highest priority is IRQ0, and the lowest is IRQ7. Thus, if an
Interrupt 09H (signaled by IRQ1) is being serviced, only an Interrupt
08H (signaled by IRQ0) can break in. All other interrupt requests are
delayed until the Interrupt 09H service routine is completed and has
issued an EOI sequence.
Eight-level designs
The IBM PC, PCjr, and PC/XT (and port-compatible computers) have eight
IRQ lines to the PIC chip--IRQ0 through IRQ7. These lines are mapped
into interrupt vectors for Interrupts 08H through 0FH (that is, 8 +
IRQ level). These eight IRQ lines and their associated interrupts are
listed in Table 13-3.
Table 13-3. Eight-Level Interrupt Map.
╓┌─────────────────────┌─────────────────┌───────────────────────────────────╖
IRQ Line Interrupt Description
──────────────────────────────────────────────────────────────────────
IRQ0 08H Timer tick, 18.2 times per second
IRQ1 09H Keyboard service required
IRQ2 0AH I/O channel (unused on IBM PC/XT)
IRQ3 0BH COM1 service required
IRQ4 0CH COM2 service required
IRQ5 0DH Fixed-disk service required
IRQ6 0EH Floppy-disk service required
IRQ7 0FH Data request from parallel printer
Sixteen-level designs
In the IBM PC/AT, 8 more IRQ levels have been added by using a second
8259A PIC (the "slave") and a cascade effect, which gives 16 priority
levels.
The cascade effect is accomplished by connecting the INT line of the
slave to the IRQ2 line of the first, or "master," 8259A instead of to
the microprocessor. When a device connected to one of the slave's IRQ
lines makes an interrupt request, the INT line of the slave goes high
and causes the IRQ2 line of the master 8259A to go high, which, in
turn, causes the INT line of the master to go high and thus interrupts
the microprocessor.
The microprocessor, ignorant of the second 8259A's presence, simply
generates an interrupt acknowledge signal on receipt of the interrupt
from the master 8259A. This signal initializes both 8259As and also
causes the master to turn control over to the slave. The slave then
completes the interrupt request.
On the IBM PC/AT, the eight additional IRQ lines are mapped to
Interrupts 70H through 77H (Table 13-4). Because the eight additional
lines are effectively connected to the master 8259A's IRQ2 line, they
take priority over the master's IRQ3 through IRQ7 events. The cascade
effect is graphically represented in Figure 13-4.
Table 13-4. Sixteen-Level Interrupt Map.
╓┌─────────────────────┌─────────────────┌───────────────────────────────────╖
IRQ Line Interrupt Description
──────────────────────────────────────────────────────────────────
IRQ0 08H Timer tick, 18.2 times per second
IRQ1 09H Keyboard service required
IRQ2 0AH INT from slave 8259A:
IRQ8 70H Real-time clock service
IRQ9 71H Software redirected to IRQ2
IRQ10 72H Reserved
IRQ11 73H Reserved
IRQ12 74H Reserved
IRQ13 75H Numeric coprocessor
IRQ14 76H Fixed-disk controller
IRQ15 77H Reserved
IRQ3 0BH COM2 service required
IRQ4 0CH COM1 service required
IRQ5 0DH Data request from LPT2
IRQ6 0EH Floppy-disk service required
IRQ7 0FH Data request from LPT1
<───────────────────────────────────────────────────────────────>
< >
< DATA BUS >
< >
<─────────────────────────────────────────────────────────────>
║ ║
║ ║
<──────────╨──────────────────────────────╨─────────────────────>
< >
< CONTROL BUS >
< >
<──────────╥──────┬───────────────────────╥────────┬───────────>
║ │ ║ │ │
║ │ ┌───────┐ ║ │ │
┌────────────────────┴───┐ │ ┌─────────────────────┴──┐
│ ──── │ │ │ ──── │
│ INTA INT ═══╪═══╡ INTA INT │
│ Slave 8259A │Control│ Master 8259A │
└──────────────────────────┘ lines └──────────────────────────┘
│
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
IRQ15│IRQ13│IRQ11│IRQ9 │ │ IRQ7 │IRQ5 │IRQ3 │IRQ1 │
│ │ │ │ │ │ │ │ │
IRQ14 IRQ12 IRQ10 IRQ8 │ IRQ6 IRQ4 │ IRQ0
└─────────────────────┘
Figure 13-4. A graphic representation of the cascade effect for IRQ
priorities.
Note: During the INTA sequence, the corresponding bit in the ISR
register of both 8259As is set, so two EOIs must be issued to complete
the interrupt service--one for the slave and one for the master.
Programming for the Hardware Interrupts
Any program that modifies an interrupt vector must restore the vector
to its original condition before returning control to MS-DOS (or to
its parent process). Any program that totally replaces an existing
hardware interrupt handler with one of its own must perform all the
handshaking and terminating actions of the original--reenable
interrupt service, signal EOI to the interrupt controller, and so
forth. Failure to follow these rules has led to many hours of
programmer frustration. See also PROGRAMMING IN THE MS-DOS
ENVIRONMENT: CUSTOMIZING MS-DOS: Exception Handlers.
When an existing interrupt handler is completely replaced with a new,
customized routine, the existing vector must be saved so it can be
restored later. Although it is possible to modify the 4-byte vector by
directly addressing the vector table in low RAM (and many published
programs have followed this practice), any program that does so runs
the risk of causing system failure when the program is used with
multitasking or multiuser enhancements or with future versions of MS-
DOS. The only technique that can be recommended for either obtaining
the existing vector values or changing them is to use the MS-DOS
functions provided for this purpose: Interrupt 21H Functions 25H (Set
Interrupt Vector) and 35H (Get Interrupt Vector).
After the existing vector has been saved, it can be replaced with a
far pointer to the replacement routine. The new routine must end with
an IRET instruction. It should also take care to preserve all
microprocessor registers and conditions at entry and restore them
before returning.
A sample replacement handler
Suppose a program performs many mathematical calculations of random
values. To prevent abnormal termination of the program by the default
MS-DOS Interrupt 00H handler when a DIV or IDIV instruction is
attempted and the divisor is zero, a programmer might want to replace
the Interrupt 00H (Divide by Zero) routine with one that informs the
user of what has happened and then continues operation without
abnormal termination. The .COM program DIVZERO.ASM (Figure 13-5) does
just that. (Another example is included in the article on interrupt-
driven communications. See PROGRAMMING IN THE MS-DOS ENVIRONMENT:
PROGRAMMING FOR MS-DOS: Interrupt-Driven Communications.)
──────────────────────────────────────────────────────────────────────
Figure 13-5. The Divide by Zero replacement handler, DIVZERO.ASM. This
code is specific to 80286 and 80386 microprocessors. (See Appendix M:
8086/8088 Software Compatibility Issues.)
──────────────────────────────────────────────────────────────────────
Supplementary handlers
In many cases, a custom interrupt handler augments, rather than
replaces, the existing routine. The added routine might process some
data before passing the data to the existing routine, or it might do
the processing afterward. These cases require slightly different
coding for the handler.
If the added routine is to process data before the existing handler
does, the routine need only jump to the original handler after
completing its processing. This jump can be done indirectly, with the
same pointer used to save the original content of the vector for
restoration at exit. For example, a replacement Interrupt 08H handler
that merely increments an internal flag at each timer tick can look
something like the following:
.
.
.
myflag dw ? ; variable to be incremented
; on each timer-tick interrupt
oldint8 dd ? ; contains address of previous
; timer-tick interrupt handler
.
. ; get the previous contents
. ; of the Interrupt 08H
; vector...
mov ax,3508h ; AH = 35H (Get Interrupt
; Vector)
int 21h ; AL = Interrupt number (08H)
mov word ptr oldint8,bx ; save the address of
mov word ptr oldint8+2,es ; the previous Int 08H Handler
mov dx,seg myint8 ; put address of the new
mov ds,dx ; interrupt handler into DS:DX
mov dx,offset myint8 ; and call MS-DOS to set
; vector
mov ax,2508h ; AH = 25H (Set Interrupt
; Vector)
int 21h ; AL = Interrupt number (08H)
.
.
.
myint8: ; this is the new handler
; for Interrupt 08H
inc cs:myflag ; increment variable on each
; timer-tick interrupt
jmp dword ptr cs:[oldint8] ; then chain to the
; previous interrupt handler
The added handler must preserve all registers and machine conditions,
except those machine conditions it will modify, such as the value of
myflag in the example (and the flags register, which is saved by the
interrupt action), and it must restore those registers and conditions
before performing the jump to the original handler.
A more complex situation arises when a replacement handler does some
processing after the original routine executes, especially if the
replacement handler is not reentrant. To allow for this processing,
the replacement handler must prevent nested interrupts, so that even
if the old handler (which is chained to the replacement handler by a
CALL instruction) issues an EOI, the replacement handler will not be
interrupted during postprocessing. For example, instead of using the
preceding Interrupt 08H example routine, the programmer could use the
following code to implement myflag as a semaphore and use the XCHG
instruction to test it:
myint8: ; this is the new handler
; for Interrupt 08H
mov ax,1 ; test and set interrupt-
xchg cs:myflag,ax ; handling-in-progress
; semaphore
push ax ; save the semaphore
pushf ; simulate interrupt, allowing
call dword ptr cs:oldint8 ; the previous handler for the
; Interrupt 08H vector to run
pop ax ; get the semaphore back
or ax,ax ; is our interrupt handler
; already running?
jnz myint8x ; yes, skip this one
. ; now perform our interrupt
. ; processing here...
.
mov cs:myflag,0 ; clear the interrupt-
; handling-
; in-progress flag
myint8x:
iret ; return from interrupt
Note that an interrupt handler of this type must simulate the original
call to the interrupt routine by first doing a PUSHF, followed by a
far CALL via the saved pointer to execute the original handler
routine. The flags register pushed onto the stack is restored by the
IRET of the original handler. Upon return from the original code, the
new routine can preserve the machine state and do its own processing,
finally returning to the caller by means of its own IRET.
The flags inside the new routine need not be preserved, as they are
automatically restored by the IRET instruction. Because of the nature
of interrupt servicing, the service routine should not depend on any
information in the flags register, nor can it return any information
in the flags register. Note also that the previous handler (invoked by
the indirect CALL) will almost certainly have dismissed the interrupt
by sending an EOI to the 8259A PIC. Thus, the machine state is not the
same as in the first myint8 example.
To remove the new vector and restore the original, the program simply
replaces the new vector (in the vector table) with the saved copy. If
the substituted routine is part of an application program, the
original vector must be restored for every possible method of exiting
from the program (including Control-Break, Control-C, and critical-
error Abort exits). Failure to observe this requirement invariably
results in system failure. Even though the system failure might be
delayed for some time after the exit from the offending program, when
some subsequent program overlays the interrupt handler code the crash
will be imminent.
Summary
Hardware interrupt handler routines, although not strictly a part of
MS-DOS, form an integral part of many MS-DOS programs and are tightly
constrained by MS-DOS requirements. Routines of this type play
important roles in the functioning of the IBM personal computers, and,
with proper design and programming, significantly enhance product
reliability and performance. In some instances, no other practical
method exists for meeting performance requirements.
Jim Kyle
Chip Rabinowitz
Article 14: Writing MS-DOS Filters
A filter is, essentially, a program that operates on a stream of
characters. The source and destination of the character stream can be
files, another program, or almost any character device. The
transformation applied by the filter to the character stream can range
from an operation as simple as substituting a character set to an
operation as elaborate as generating splines from sets of coordinates.
The standard MS-DOS package includes three simple filters: SORT, which
alphabetically sorts text on a line-by-line basis; FIND, which
searches a text stream to match a specified string; and MORE, which
displays text one screenful at a time. This article describes how
filters work and how new ones can be constructed. See also USER
COMMANDS: FIND; MORE; SORT.
System Support for Filters
The operation of a filter program relies on two features that appeared
in MS-DOS version 2.0: standard devices and redirectable I/O.
The standard devices are represented by five handles that are
originally established when the system is initialized. Each process
inherits these handles from its immediate parent. Thus, the standard
device handles are already opened when a process acquires control of
the system, and the process can use the handles with Interrupt 21H
Functions 3FH and 40H for read and write operations without further
preliminaries. The default assignments of the standard device handles
are
╓┌────────────────┌──────────────────────────────────┌───────────────────────╖
Handle Name Default Device
──────────────────────────────────────────────────────────────────
0 stdin (standard input) CON
1 stdout (standard output) CON
2 stderr (standard error) CON
3 stdaux (standard auxiliary) AUX
4 stdlst (standard list) PRN
The CON device is assigned by default to the system's keyboard and
video display. AUX is assigned by default to COM1 (the first physical
serial port), and PRN is assigned by default to LPT1 (the first
physical parallel printer port); in some systems these assignments can
be altered with the MODE command. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: Character Device Input and
Output; USER COMMANDS: MODE; CTTY.
When a program is executed by entering its name at the system
(COMMAND.COM) prompt, the user can redirect either or both of the
standard input and standard output handles from their default device
(CON) to another file, a character device, or a process. This
redirection is accomplished by including one of the special characters
<, >, >>, or in the command line, in the following form:
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Redirection Result
──────────────────────────────────────────────────────────────────
< file Contents of the specified file are used instead of
the keyboard as the program's standard input.
< device Program takes its standard input from the named
device instead of from the keyboard.
> device Program sends its standard output to the named device
instead of to the video display.
> file Program sends its standard output to the specified
file instead of to the video display.
>> file Program appends its standard output to the current
contents of the specified file instead of to the
video display.
p1 | p2 Standard output of program p1 is routed to become the
standard input of program p2 (output of p1 is said
to be piped to p2).
For example, the command
C>SORT < MYFILE.TXT > PRN <Enter>
causes the SORT filter to read its input from the file MYFILE.TXT,
sort the lines alphabetically, and write resulting text to the
character device PRN (the logical name for the system's list device).
The redirection requested by the <, >, >>, or characters takes place
at the level of COMMAND.COM and is invisible to the program it
affects. Such redirection can also be put into effect by another
process. See Using a Filter as a Child Process, below.
Note that if a program "goes around" MS-DOS to perform its input and
output, either by calling ROM BIOS functions or by manipulating the
keyboard or video controller directly, redirection commands placed in
the program's command line do not have the expected effect.
How Filters Work
By convention, a filter program reads its text from standard input and
writes the results of its operations to standard output. When the end
of the input stream is reached, the filter simply terminates,
optionally writing an end-of-file mark (1AH) to the output stream. As
a result, filters are both flexible and simple.
Filter programs are flexible because they do not know, and do not
care, about the source of the data they process or the destination of
their output. Any redirection that the user specifies in the command
line is invisible to the filter. Thus, any character device that has a
logical name within the system (CON, AUX, COM1, COM2, PRN, LPT1, LPT2,
LPT3, and so on), any file on any block device (local or network)
known to the system, or any other program can supply a filter's input
or accept its output. If necessary, several functionally simple
filters can be concatenated with pipes to perform very complex
operations.
Although flexible, filters are also simple because they rely on their
parent process to supply standard input and standard output handles
that have already been appropriately redirected. The parent is
responsible for opening or creating any necessary files, checking the
validity of logical character device names, and loading and executing
the preceding or following process in a pipe. The filter need only
concern itself with the transformation it will apply to the data; it
can leave the I/O details to the operating system and to its parent.
Building a Filter
Creating a new filter for MS-DOS is a straightforward process. In its
simplest form, a filter need only use the handle-oriented read
(Interrupt 21H Function 3FH) and write (Interrupt 21H Function 40H)
functions to get characters or lines from standard input and send them
to standard output, performing any desired alterations on the text
stream on a character-by-character or line-by-line basis.
Figures 14-1 through 14-4 contain template character-oriented and
line-oriented filters in both assembly language and C. The C version
of the character filter runs much faster than the assembly-language
version, because the C run-time library provides hidden blocking and
deblocking (buffering) of character reads and writes; the assembly-
language program actually makes two calls to MS-DOS for each character
processed. (Of course, if buffering is added to the assembly-language
version it will be both faster and smaller than the C filter.) The C
and assembly-language versions of the line-oriented filter run at
roughly the same speed.
──────────────────────────────────────────────────────────────────────
Figure 14-1. Assembly-language template for a character-oriented
filter (file PROTOC.ASM).
──────────────────────────────────────────────────────────────────────
Figure 14-2. C template for a character-oriented filter (file
PROTOC.C).
──────────────────────────────────────────────────────────────────────
Figure 14-3. Assembly-language template for a line-oriented filter
(file PROTOL.ASM).
──────────────────────────────────────────────────────────────────────
Figure 14-4. C template for a line-oriented filter (file PROTOL.C).
──────────────────────────────────────────────────────────────────────
Each of the four template filters can be assembled or compiled,
linked, and run exactly as they are shown in Figures 14-1 through
14-4. Of course, in this form they function like an incredibly slow
COPY command.
To obtain a filter that does something useful, a routine that performs
some modification of the text stream that is flowing by must be
inserted between the reads and writes. For example, Figures 14-5 and
14-6 contain the assembly-language and C source code for a character-
oriented filter named LC. This program converts all uppercase input
characters (A-Z) to lowercase (a-z) output, leaving other characters
unchanged. The only difference between LC and the template character
filter is the translation subroutine that operates on the text stream.
──────────────────────────────────────────────────────────────────────
Figure 14-5. Assembly-language source code for the LC filter (file
LC.ASM).
──────────────────────────────────────────────────────────────────────
Figure 14-6. C source code for the LC filter (file LC.C).
──────────────────────────────────────────────────────────────────────
As another example, Figure 14-7 contains the C source code for a line-
oriented filter called FIND. This simple filter is invoked with a
command line in the form
FIND "pattern" < source > destination
FIND searches the input stream for lines containing the pattern
specified in the command line. The line number and text of any line
containing a match is sent to standard output, with any tabs expanded
to eight-column tab stops.
──────────────────────────────────────────────────────────────────────
Figure 14-7. C source code for a new FIND filter (file FIND.C).
──────────────────────────────────────────────────────────────────────
This sample FIND filter differs from the FIND filter supplied by
Microsoft with MS-DOS in several respects. It is not case sensitive,
so the pattern "foobar" will match "FOOBAR", "FooBar", and so forth.
Second, this filter supports no switches; these are left as an
exercise for the reader. Third, unlike the Microsoft version of FIND,
this program always reads from standard input; it is not able to open
its own files.
Using a Filter as a Child Process
Instead of incorporating all the code necessary to do the job itself,
an application program can load and execute a filter as a child
process to carry out a specific task. Before the child filter is
loaded, the parent must arrange for the standard input and standard
output handles that will be inherited by the child to be attached to
the files or character devices that will supply the filter's input and
receive its output. This redirection is accomplished with the
following steps using Interrupt 21H functions:
1. The parent process uses Function 45H (Duplicate File Handle) to
create duplicates of its standard input and standard output handles
and then saves the duplicates.
2. The parent opens (with Function 3DH) or creates (with Function 3CH)
the files or devices that the child process will use for input and
output.
3. The parent uses Function 46H (Force Duplicate File Handle) to force
its own standard device handles to track the new file or device
handles acquired in step 2.
4. The parent uses Function 4B00H (Load and Execute Program [EXEC]) to
load and execute the child process. The child inherits the
redirected standard input and standard output handles and uses them
to do its work. The parent regains control after the child filter
terminates.
5. The parent uses the duplicate handles created in step 1, together
with Function 46H (Force Duplicate File Handle), to restore its own
standard input and standard output handles to their original
meanings.
6. The parent closes (with Function 3EH) the duplicate handles created
in step 1, because they are no longer needed.
It might seem as though the parent process could just as easily close
its own standard input and standard output (handles 0 and 1), open the
input and output files needed by the child, load and execute the
child, close the files upon regaining control, and then reopen the CON
device twice. Because the open operation always assigns the first free
handle, this approach would have the desired effect as far as the
child process is concerned. However, it would throw away any
redirection that had been established for the parent process by its
parent. Thus, the need to preserve any preexisting redirection of the
parent's standard input and standard output, along with the desire to
preserve the parent's usual output channel for informational messages
right up to the actual point of the EXEC call, is the reason for the
elaborate procedure outlined above in steps 1 through 6.
The program EXECSORT.ASM in Figure 14-8 demonstrates this redirection
of input and output for a filter run as a child process. The parent,
which is called EXECSORT, saves duplicates of its current standard
input and standard output handles and then redirects those handles
respectively to the files MYFILE.DAT (which it opens) and MYFILE.SRT
(which it creates). EXECSORT then uses Interrupt 21H Function 4BH
(EXEC) to run the SORT.EXE filter that is supplied with MS-DOS (this
file must be in the current drive and directory for the demonstration
to work correctly).
──────────────────────────────────────────────────────────────────────
Figure 14-8. Assembly-language source code demonstrating use of a
filter as a child process. This code redirects the standard input and
standard output handles to files, invokes the EXEC function (Interrupt
21H Function 4BH) to run the SORT.EXE program, and then restores the
original meaning of the standard input and standard output handles
(file EXECSORT.ASM).
──────────────────────────────────────────────────────────────────────
The MS-DOS SORT program reads the file MYFILE.DAT via its standard
input handle, sorts the file alphabetically, and writes the sorted
data to MYFILE.SRT via its standard output handle. When SORT
terminates, MS-DOS closes SORT's inherited handles for standard input
and standard output, which forces an update of the directory entries
for the associated files. The program EXECSORT then resumes execution,
restores its own standard input and standard output handles (which are
still open) to their original meanings, displays a success message on
standard output, and exits to MS-DOS.
Ray Duncan
Article 15: Installable Device Drivers
The software that runs on modern computer systems is, by convention,
organized into layers with varied degrees of independence from the
underlying computer hardware. The purpose of this layering is
threefold:
■ To minimize the impact on programs of differences between hardware
devices or changes in the hardware.
■ To allow the code for common operations to be centralized and
optimized.
■ To ease the task of moving programs and their data from one machine
to another.
The top and most hardware-independent layer is usually the transient,
or application, program, which performs a specific job and deals with
data in terms of files and records within those files. Such programs
are called transient because they are brought into RAM for execution
when needed and are discarded from memory when their job is finished.
Examples of such programs are Microsoft Word, various programming
tools such as the Microsoft Macro Assembler (MASM) and the Microsoft
Object Linker (LINK), and even some of the standard MS-DOS utility
programs such as CHKDSK and FORMAT.
The middle layer is the operating-system kernel, which manages the
allocation of system resources such as memory and disk storage,
provides a battery of services to application programs, and implements
disk directories and the other housekeeping details of disk storage.
The MS-DOS kernel is brought into memory from the file MSDOS.SYS (or
IBMDOS.COM with PC-DOS) when the system is turned on or restarted and
remains fixed in memory until the system is turned off. The system's
default command processor, COMMAND.COM, and system manager programs
such as Microsoft Windows bridge the categories of application program
and operating system: Parts of them remain resident in memory at all
times, but they rely on the MS-DOS kernel for services such as file
I/O. See PROGRAMMING IN THE MS-DOS ENVIRONMENT: STRUCTURE OF MS-DOS:
Components of MS-DOS.
The modules in the lowest layer are called device drivers. These
drivers are the components of the operating system that manage the
controller, or adapter, of a peripheral device--a piece of hardware
that the computer uses for such purposes as storage or communicating
with the outside world. Thus, device drivers are responsible for
transferring data between a peripheral device and the computer's RAM
memory, where other programs can work on it. Drivers shield the
operating-system kernel from the need to deal with hardware I/O port
addresses, operating characteristics, and the peculiarities of a
particular peripheral device, just as the kernel, in turn, shields
application programs from the details of file management.
In MS-DOS versions 1.x, device drivers were integrated into the
operating system and could be extended or replaced only by patching
the files that contained the operating system itself. Because every
third-party peripheral manufacturer evolved a different method of
modifying these files to get its product to work, conflicts between
products from different manufacturers were frequent and expansion of a
PC with new disk drives and other devices (especially fixed disks) was
often a chancy proposition.
In MS-DOS versions 2.0 and later, there is a clean separation between
device drivers and the MS-DOS kernel. Device drivers have a
straightforward structure and are interfaced to the kernel through a
simple and clearly defined scheme that consists of far calls, function
codes, and data packets. Given adequate information about the
hardware, a programmer can write a new device driver that follows this
structure and interface for almost any conceivable peripheral device;
such a driver can subsequently be installed and used without any
changes to the underlying operating system.
This article explains the anatomy, operation, and creation of drivers
for MS-DOS versions 2.0 and later. Device drivers for versions 1.x are
not discussed further here.
Resident and Installable Drivers
Every MS-DOS system contains built-in device drivers for the console
(keyboard and video display), the serial port, the parallel printer
port, the real-time clock, and at least one disk storage device (the
system boot device). These drivers, known as the resident drivers, are
loaded as a set from the file IO.SYS (or IBMBIO.COM with PC-DOS) when
the system is turned on or restarted.
Drivers for additional peripheral devices occupy individual files on
the disk. These drivers, called installable drivers, are loaded and
linked into the system during its initialization as a result of DEVICE
directives in the CONFIG.SYS file. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: STRUCTURE OF MS-DOS: Components of MS-DOS. Examples of
such drivers are the ANSI.SYS and RAMDISK.SYS files included with MS-
DOS version 3.2. In all other respects, installable drivers have the
same structure and relationship to the MS-DOS kernel as the resident
drivers. All drivers in the system are chained together so that MS-DOS
can rapidly search the entire set to find a specific block or
character device when an I/O operation is requested.
Device drivers as a whole are categorized into two groups: block-
device drivers and character-device drivers. A driver's membership in
one of these two groups determines how the associated device is viewed
by MS-DOS and what functions the driver itself must support.
Character-device drivers
Character-device drivers control peripheral devices, such as a
terminal or a printer, that perform input and output one character (or
byte) at a time. Each character-device driver ordinarily supports a
single hardware unit. The device has a one-character to eight-
character logical name that can be used by an application program to
"open" the device for input or output as though it were a file. The
logical name is strictly a means of identifying the driver to MS-DOS
and has no physical equivalent on the device (unlike a volume label
for block devices).
The three resident character-device drivers for the console, serial
port, and printer carry the logical device names CON, AUX, and PRN,
respectively. These three drivers receive special treatment by MS-DOS
that allows application programs to address the associated devices in
three different ways:
■ They can be opened by name for input and output (like any other
character device).
■ They are supported by special-purpose MS-DOS function calls
(Interrupt 21H Functions 01-0CH).
■ They are assigned to default handles (standard input, standard
output, standard error, standard auxiliary, and standard list) that
need not be opened to be used.
See PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS:
Character Device Input and Output.
Other character devices can be supported by simply installing
additional character-device drivers. The only significant restriction
on the total number of devices that can be supported, other than the
memory required to hold the drivers, is that each driver must have a
unique logical name. When MS-DOS receives an open request for a
character device, it searches the chain of device drivers in order
from the last driver loaded to the first. Thus, if more than one
driver uses the same logical name, the last driver to be loaded
supersedes any others and receives all I/O requests addressed to that
logical name. This behavior can be used to advantage in some
situations. For example, it allows the more powerful ANSI.SYS display
driver to supersede the system's default console driver, which does
not support cursor positioning and character attributes.
The MS-DOS kernel's buffering and filtering of the characters that
pass between it and a character-device driver are affected by whether
MS-DOS regards the device to be in cooked mode or raw mode. During
cooked mode input, MS-DOS requests characters one at a time from the
driver and places them in its own internal buffer, echoing each
character to the screen (if the input device is the keyboard) and
checking each character for a Control-C (03H) or a Return (0DH). When
either the number of characters requested by the application program
has been received or a Return is detected, the input is terminated and
the data is copied from MS-DOS's internal buffer into the requesting
program's buffer. When a Control-C is detected, MS-DOS aborts the
input operation and transfers to the routine whose address is stored
in the Interrupt 23H (Control-C Handler Address) vector. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Exception
Handlers. Similarly, during output in cooked mode, MS-DOS checks
between each character for a Control-C pending at the keyboard and
aborts the output operation if one is detected.
In raw mode, the exact number of bytes requested by the application
program is read or written, without regard to any control characters
such as Return or Control-C. MS-DOS passes the entire I/O request to
the driver in a single operation, instead of breaking the request into
single-character reads or writes, and the characters are transferred
directly to or from the requesting program's buffer.
The mode for a specific device can be queried by an application
program with the IOCTL Get Device Data function (Interrupt 21H
Function 44H Subfunction 00H); the mode can be selected with the Set
Device Data function (Interrupt 21H Function 44H Subfunction 01H). See
SYSTEM CALLS: INTERRUPT 21H: Function 44H. The driver itself is not
usually aware of its mode and the mode does not affect its operation.
Block-Device Drivers
Block-device drivers control peripheral devices that transfer data in
chunks rather than 1 byte at a time. Block devices are usually
randomly addressable devices such as floppy- or fixed-disk drives, but
they can also be sequential devices such as magnetic-tape drives. A
block driver can support more than one physical unit and can also map
two or more logical units onto a single physical unit, as with a
partitioned fixed disk.
MS-DOS assigns single-letter drive identifiers (A, B, and so forth) to
block devices, instead of logical names. The first letter assigned to
a block-device driver is determined solely by the driver's position in
the chain of all drivers--that is, by the number of units supported by
the block drivers loaded before it; the total number of letters
assigned to the driver is determined by the number of logical drive
units the driver supports.
MS-DOS does not associate a mode (cooked or raw) with block-device
drivers. A block-device driver always reads or writes exactly the
number of sectors requested (barring hardware or addressing errors)
and never filters or otherwise manipulates the contents of the blocks
being transferred.
Structure of an MS-DOS Device Driver
A device driver has three major components (Figure 15-1):
■ The device header
■ The Strategy routine (Strat)
■ The Interrupt routine (Intr)
┌─────────────────────────┬─────────────────────────────┐
│ │ Initialization │
│ ├─────────────────────────────┤
│ │ Media Check │
│ ├─────────────────────────────┤
│ │ Build BPB │
│ ├─────────────────────────────┤
│ │ IOCTL Read and Write │
│ ├─────────────────────────────┤
│ │ Status │
│ ├─────────────────────────────┤
│ Interrupt routine │ Read │
│ ├─────────────────────────────┤
│ │ Write, Write/Verify │
│ ├─────────────────────────────┤
│ │ Output Until Busy │
│ ├─────────────────────────────┤
│ │ Flush Buffers │
│ ├─────────────────────────────┤
│ │ Device Open │
│ ├─────────────────────────────┤
│ │ Device Close │
│ ├─────────────────────────────┤
│ │ Check if Removable │
│ ├─────────────────────────────┤
│ │ Generic IOCTL │
│ ├─────────────────────────────┤
│ │ Get/Set Logical Device │
│ └─────────────────────────────┤
│ │
├───────────────────────────────────────────────────────┤
│ │
│ Strategy routine │
│ │
├───────────────────────────────────────────────────────┤
│ Device-driver header │
└───────────────────────────────────────────────────────┘
Figure 15-1. General structure of an MS-DOS installable device driver.
The device header
The device header (Figure 15-2) always lies at the beginning of the
driver. It contains a link to the next driver in the chain, a word (16
bits) of device attribute flags, offsets to the executable Strategy
and Interrupt routines for the device, and the logical device name if
it is a character device such as PRN or COM1 or the number of logical
units if it is a block device.
Offset
00H ┌────────────────────────────────────────────┐
│ Link to next driver, offset │
02H ├────────────────────────────────────────────┤
│ Link to next driver, segment │
04H ├────────────────────────────────────────────┤
│ Device attribute word │
06H ├────────────────────────────────────────────┤
│ Offset, Strategy entry point │
08H ├────────────────────────────────────────────┤
│ Offset, Interrupt entry point │
0AH ├────────────────────────────────────────────┤
│ Logical name (8 bytes) if character device │
│ or │
│ Number of units (1 byte) followed by │
│ 7 bytes of reserved space if block device │
12H └────────────────────────────────────────────┘
Figure 15-2. Device header. The offsets to the Strat and Intr routines
are offsets from the same segment used to point to the device header.
The device attribute flags word (Table 15-1) defines whether a driver
controls a character or a block device, which of the optional
subfunctions added in MS-DOS versions 3.0 and 3.2 are supported by the
driver, and, in the case of block drivers, whether the driver supports
IBM-compatible disk media. The least significant 4 bits of the device
attribute flags word control whether MS-DOS should use the driver as
the standard input, standard output, clock, or NUL device; each of
these 4 bits should be set on only one driver in the system at a time.
Table 15-1. Device Attribute Word in Device Header.
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Bit Setting
──────────────────────────────────────────────────────────────────
15 1 if character device, 0 if block device
14 1 if IOCTL Read and Write supported
13 1 if non-IBM format (block device)
1 if Output Until Busy supported (character device)
12 0 (reserved)
11 1 if Open/Close/Removable Media supported (versions 3.0
and later)
10 0 (reserved)
9 0 (reserved)
8 0 (reserved)
7 0 (reserved)
6 1 if Generic IOCTL and Get/Set Logical Drive supported
(version 3.2)
5 0 (reserved)
4 1 if special fast output function for CON device supported
3 1 if current CLOCK device
2 1 if current NUL device
1 1 if current standard output (stdout)
0 1 if current standard input (stdin)
The information in the device header is ordinarily used only by the
MS-DOS kernel and is not available to application programs. However,
the IOCTL subfunctions Get and Set Device Data (Interrupt 21H Function
44H Subfunctions 00H and 01H) can be used to inspect or modify some of
the bits in the device attribute flags word. Note that there is not a
one-to-one correspondence between the bits defined for those functions
and the bits in the device header. For example, in the device
information word used by the IOCTL subfunctions, bit 7 indicates a
block or character device; in the device attribute word of the device
header, bit 15 indicates a block or character device.
The Strategy routine (Strat)
MS-DOS calls the driver's Strategy routine as the first step of any
operation, passing it the segment and offset of a data structure
called a request header in registers ES:BX. The Strategy routine saves
this pointer for subsequent processing by the Interrupt routine and
returns to MS-DOS.
A request header is essentially a small buffer used for private
communication between MS-DOS and the device driver. Both MS-DOS and
the device driver read and write information in the request header.
The first 13 bytes of a request header are the same for all device-
driver functions and are therefore referred to as the static portion
of the header. The number and contents of the subsequent bytes vary
according to the type of operation being requested by the MS-DOS
kernel (Figure 15-3). The request header's most important component is
the command code passed in its third byte; this code selects a driver
function such as Read or Write. Other information passed to the driver
in the request header includes unit numbers, transfer addresses, and
sector or byte counts.
00H ┌──────────────────────────────────────┐
│ Request header length │▒
01H ├──────────────────────────────────────┤▒
│ Block-device unit number │▒
02H ├──────────────────────────────────────┤▒
│ Command code (driver subfunction) │▒
03H ├──────────────────────────────────────┤▒
│ │▒
│ Returned status │▒ Static portion
│ │▒ of request header
05H ├──────────────────────────────────────┤▒
│ │▒
│ │▒
│ │▒
│ Reserved │▒
│ │▒
│ │▒
│ │▒
0DH ├──────────────────────────────────────┤
│ Media ID byte │▒
0EH ├──────────────────────────────────────┤▒
│ │▒
│ Offset │▒
│ │▒
10H ├──────────────────────────────────────┤▒
│ │▒
│ Segment of data to be transferred │▒ Variable portion
│ │▒ of request header
12H ├──────────────────────────────────────┤▒
│ │▒
│ Byte/sector count │▒
│ │▒
14H ├──────────────────────────────────────┤▒
│ │▒
│ Starting sector number │▒
│ │▒
└──────────────────────────────────────┘
Figure 15-3. A typical driver request header. The bytes following the
static portion are the format used for driver Read, Write, Write with
Verify, IOCTL Read, and IOCTL Write operations.
The Interrupt routine (Intr)
The last and most complex part of a device driver is the Interrupt
routine, which is called by MS-DOS immediately after the call to the
Strategy routine. The bulk of the Interrupt routine is a collection of
functions or subroutines, sometimes called command-code routines, that
carry out each of the various operations the MS-DOS kernel requires a
driver to support.
When the Interrupt routine receives control from MS-DOS, it saves any
affected registers, examines the request header whose address was
previously passed in the call to the Strategy routine, determines
which command-code routine is needed, and branches to the appropriate
function. When the operation is completed, the Interrupt routine
stores the status (Table 15-2), error (Table 15-3), and any other
applicable information into the request header, restores the previous
contents of the affected registers, and returns to the MS-DOS kernel.
Table 15-2. The Request Header Status Word.
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Bits Meaning
──────────────────────────────────────────────────────────────────
15 Error
12-14 Reserved
9 Busy
8 Done
0-7 Error code if bit 15 = 1
Table 15-3. Device-Driver Error Codes.
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Code Meaning
──────────────────────────────────────────────────────────────────
00H Write-protect violation
01H Unknown unit
02H Drive not ready
03H Unknown command
04H CRC error
05H Bad drive request structure length
06H Seek error
07H Unknown media
08H Sector not found
09H Printer out of paper
0AH Write fault
0BH Read fault
0CH General failure
0DH Reserved
0EH Reserved
0FH Invalid disk change (versions 3.x)
The Interrupt routine's name is misleading in that it is never entered
asynchronously as a hardware interrupt. The division of function
between the Strategy and Interrupt routines is present for symmetry
with UNIX/XENIX and MS OS/2 drivers but is essentially meaningless in
single-tasking MS-DOS because there is never more than one I/O request
in progress at a time.
The command-code functions
A total of twenty command codes are defined for MS-DOS device drivers.
The command codes and the names of their associated Interrupt routines
are shown in the following list:
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Code Routine
──────────────────────────────────────────────────────────────────
0 Init (initialization)
1 Media Check (block devices only)
2 Build BIOS Parameter Block (block devices only)
3 IOCTL Read
4 Read (Input)
5 Nondestructive Read (character devices only)
6 Input Status (character devices only)
7 Flush Input Buffers (character devices only)
8 Write (Output)
9 Write with Verify
10 Output Status (character devices only)
11 Flush Output Buffers (character devices only)
12 IOCTL Write
13 Device Open
14 Device Close
15 Removable Media (block devices only)
16 Output Until Busy (character devices only)
19 Generic IOCTL Request
23 Get Logical Device (block devices only)
24 Set Logical Device (block devices only)
Functions 0 through 12 must be supported by a driver's Interrupt
section under all versions of MS-DOS. Drivers tailored for versions
3.0 and 3.1 can optionally support an additional 4 functions defined
under those versions of the operating system and drivers designed for
version 3.2 can support 3 more, for a total of 20. MS-DOS inspects the
bits in the device attribute word of the device header to determine
which of the optional version 3.x functions a driver supports, if any.
As noted in the list above, some of the functions are relevant only
for character drivers, some only for block drivers, and some for both.
In any case, there must be an executable routine present for each
function, even if the routine does nothing but set the done flag in
the status word of the request header. The general requirements for
each function routine are described below.
The Init function
The Init (initialization) function (command code 0) for a driver is
called only once, when the driver is loaded (Figure 15-4). Init is
responsible for checking that the hardware device controlled by the
driver is present and functional, performing any necessary hardware
initialization (such as a reset on a printer or a seek to the home
track on a disk device), and capturing any interrupt vectors that the
driver will need later.
The Init function is passed a pointer in the request header to the
text of the DEVICE line in CONFIG.SYS that caused the driver to be
loaded--specifically, the address of the next byte after the equal
sign (=). The line is read-only and is terminated by a linefeed or
carriage-return character; it can be scanned by the driver for
switches or other parameters that might influence the driver's
operation. (Alphabetic characters in the line are folded to
uppercase.) With versions 3.0 and later, block drivers are also passed
the drive number that will be assigned to their first unit (0 = A, 1 =
B, and so on).
Driver called with Driver returns
00H ┌─────────────────────────┐ 00H ┌─────────────────────────┐
│ Request header length │ │ │
01H ├─────────────────────────┤ 01H ├─────────────────────────┤
│ │ │ │
02H ├─────────────────────────┤ 02H ├─────────────────────────┤
│ Command code │ │ │
03H ├─────────────────────────┤ 03H ├─────────────────────────┤
│ │ │ │
│ │ │ Status │
│ │ │ │
05H ├─────────────────────────┤ 05H ├─────────────────────────┤
│ │ │ │
│ │ │ │
│ Reserved │ │ Reserved │
│ │ │ │
│ │ │ │
0DH ├─────────────────────────┤ 0DH ├─────────────────────────┤
│ │ │ Units supported │
0EH ├─────────────────────────┤ 0EH ├─────────────────────────┤
│ │ │ │
│ │ │ Offset of free memory │
│ │ │ above driver │
10H ├─────────────────────────┤ 10H ├─────────────────────────┤
│ │ │ │
│ │ │ Segment of free menory │
│ │ │ above driver │
12H ├─────────────────────────┤ 12H ├─────────────────────────┤
│ │ │ │
│Offset of CONFIG.SYS │ │ Offset of │
│line loading driver │ │ BPB pointer array │
14H ├─────────────────────────┤ 14H ├─────────────────────────┤
│ │ │ │
│Segment of CONFIG.SYS │ │ Segment of │
│line loading driver │ │ BPB pointer array │
16H ├─────────────────────────┤ 16H ├─────────────────────────┤
│First unit number │ │ │
└─────────────────────────┘ └─────────────────────────┘
Figure 15-4. Initialization request header (command code 0).
When it returns to the kernel, the Init function must set the done
flag in the status word of the request header and return the address
of the start of free memory after the driver (sometimes called the
break address). This address tells the kernel where it can build
certain control structures of its own associated with the driver and
then load the next driver. The Init routine of a block-device driver
must also return the number of logical units supported by the driver
and the address of a BPB pointer array.
The number of units returned by a block driver is used to assign
device identifiers. For example, if at the time the driver is loaded
there are already drivers present for four block devices (drive codes
0-3, corresponding to drive identifiers A through D) and the driver
being initialized supports four units, it will be assigned the drive
numbers 4 through 7 (corresponding to the drive names E through H).
(Although there is also a field in the device header for the number of
units, it is not inspected by MS-DOS; rather, it is set by MS-DOS from
the information returned by the Init function.)
The BPB pointer array is an array of word offsets to BIOS parameter
blocks. See The Build BIOS Parameter Block Function, below;
PROGRAMMING IN THE MS-DOS ENVIRONMENT: STRUCTURE OF MS-DOS: MS-DOS
Storage Devices. The array must contain one entry for each unit
defined by the driver, although all entries can point to the same BPB
to conserve memory. During the operating-system boot sequence, MS-DOS
scans all the BPBs defined by all the units in all the resident block-
device drivers to determine the largest sector size that exists on any
device in the system; this information is used to set MS-DOS's cache
buffer size. Thus, the sector size in the BPB of any installable block
driver must be no larger than the largest sector size used by the
resident block drivers.
If the Init routine finds that its hardware device is missing or
defective, it can bypass the installation of the driver completely by
returning the following values in the request header:
╓┌────────────────────────────────┌──────────────────────────────────────────╖
Item Value
──────────────────────────────────────────────────────────────────
Number of units 0
Address of free memory Segment and offset of the driver's own
device header
A character-device driver must also clear bit 15 of the device
attribute word in the device header so that MS-DOS will load the next
driver in the same location as the one that just terminated itself.
The operating-system services that can be invoked by the Init routine
are very limited. Only MS-DOS Interrupt 21H Functions 01-0CH (various
character input and output services), 25H (Set Interrupt Vector), 30H
(Get MS-DOS Version Number), and 35H (Get Interrupt Vector) can be
called by the Init code. These functions assist the driver in
configuring itself for the version of the host operating system it is
to run under, capturing vectors for hardware interrupts, and
displaying informational or error messages.
The amount of RAM required by a device driver can be reduced by
positioning the Init routine at the end of the driver and returning
that routine's starting address as the location of the first free
memory.
The Media Check function
The Media Check function (command code 1) is used only in block-device
drivers. It is called by the MS-DOS kernel when there is a pending
drive access call other than a simple file read or write (for example,
a file open, close, rename, or delete), passing the media ID byte
(Figure 15-5) for the disk that MS-DOS assumes is in the drive:
╓┌────────────────────────────┌──────────────────────────────────────────────╖
Description Medium
──────────────────────────────────────────────────────────────────
0F9H 5.25-inch double-sided, 15 sectors
0FCH 5.25-inch single-sided, 9 sectors
0FDH 5.25-inch double-sided, 9 sectors
0FEH 5.25-inch single-sided, 8 sectors
0FFH 5.25-inch double-sided, 8 sectors
0F9H 3.5-inch double-sided, 9 sectors
0F0H 3.5-inch double-sided, 18 sectors
0F8H Fixed disk
The function returns a code indicating whether the medium has been
changed since the last transfer:
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Code Meaning
──────────────────────────────────────────────────────────────────
-1 Medium changed
-0 Don't know if medium changed
-1 Medium not changed
Driver called with Driver returns
00H ┌─────────────────────────┐ 00H ┌─────────────────────────┐
│ Request header length │ │ │
01H ├─────────────────────────┤ 01H ├─────────────────────────┤
│ Unit number │ │ │
02H ├─────────────────────────┤ 02H ├─────────────────────────┤
│ Command code │ │ │
03H ├─────────────────────────┤ 03H ├─────────────────────────┤
│ │ │ │
│ │ │ Status │
│ │ │ │
05H ├─────────────────────────┤ 05H ├─────────────────────────┤
│ │ │ │
│ │ │ │
│ Reserved │ │ Reserved │
│ │ │ │
│ │ │ │
0DH ├─────────────────────────┤ 0DH ├─────────────────────────┤
│ Media ID byte │ │ │
0EH ├─────────────────────────┤ 0EH ├─────────────────────────┤
│ │ │ Media change code │
0FH ├─────────────────────────┤ 10H ├─────────────────────────┤
│ │ │ │
│ │ │ Offset of volume │
│ │ │ (if error 0FH) │
11H ├─────────────────────────┤ 11H ├─────────────────────────┤
│ │ │ │
│ │ │ Segment of volume label │
│ │ │ (if error 0FH) │
└─────────────────────────┘ └─────────────────────────┘
Figure 15-5. Media Check request header (command code 1).
If the Media Check routine asserts that the disk has not been changed,
MS-DOS bypasses rereading the FAT and proceeds with the disk access.
If the returned code indicates that the disk has been changed, MS-DOS
invalidates all buffers associated with the drive, including buffers
containing data waiting to be written (this data is simply lost),
performs a Build BPB call, and then reads the disk's FAT and
directory.
The action taken by MS-DOS when Don't know is returned depends on the
state of its internal buffers. If data that needs to be written out is
present in the buffers associated with the drive, MS-DOS assumes that
no disk change has occurred. If the buffers are empty or have all been
previously flushed to the disk, MS-DOS assumes that the disk was
changed and proceeds as described above for the Medium changed return
code.
If bit 11 of the device attribute word is set (that is, the driver
supports the optional Open/Close/Removable Media functions), the host
system is MS-DOS version 3.0 or later, and the function returns the
Medium changed code (-1), the function must also return the segment
and offset of the ASCIIZ volume label for the previous disk in the
drive. (If the driver does not have the volume label, it can return a
pointer to the ASCIIZ string NO NAME.) If MS-DOS determines that the
disk was changed with unwritten data still present in the buffers, it
issues a critical error 0FH (Invalid Disk Change). Application
programs can trap this critical error and prompt the user to replace
the original disk.
In character-device drivers, the Media Change function should simply
set the done flag in the status word of the request header and return.
The Build BIOS Parameter Block function
The Build BPB function (command code 2) is supported only on block
devices. MS-DOS calls this function when the Medium changed code has
been returned by the Media Check routine or when the Don't know code
has been returned and there are no dirty buffers (buffers that have
not yet been written to disk). Thus, a call to this function indicates
that the disk has been legally changed.
The Build BPB call receives a pointer to a one-sector buffer in the
request header (Figure 15-6). If the non-IBM-format bit (bit 13) in
the device attribute word in the device header is zero, the buffer
contains the first sector of the disk's FAT, with the media ID byte in
the first byte of the buffer. In this case, the contents of the buffer
should not be modified by the driver. However, if the non-IBM-format
bit is set, the buffer can be used by the driver as scratch space.
The Build BPB function must return the segment and offset of a BIOS
parameter block (Table 15-4) for the disk format indicated by the
media ID byte and set the done flag in the status word of the request
header. The information in the BPB is used by the kernel to interpret
the disk structure and is also used by the driver itself to translate
logical sector addresses into physical track, sector, and head
addresses. If bit 11 of the device attribute word is set (that is, the
driver supports the optional Open/Close/Removable Media functions) and
the host system is MS-DOS version 3.0 or later, this routine should
also read the volume label from the disk and save it.
Driver called with Driver returns
00H ┌─────────────────────────┐ 00H ┌─────────────────────────┐
│ Request header length │ │ │
01H ├─────────────────────────┤ 01H ├─────────────────────────┤
│ Unit number │ │ │
02H ├─────────────────────────┤ 02H ├─────────────────────────┤
│ Command code │ │ │
03H ├─────────────────────────┤ 03H ├─────────────────────────┤
│ │ │ │
│ │ │ Status │
│ │ │ │
05H ├─────────────────────────┤ 05H ├─────────────────────────┤
│ │ │ │
│ │ │ │
│ Reserved │ │ Reserved │
│ │ │ │
│ │ │ │
0DH ├─────────────────────────┤ 0DH ├─────────────────────────┤
│ Media ID byte │ │ │
0EH ├─────────────────────────┤ 0EH ├─────────────────────────┤
│ │ │ │
│ Offset of FAT buffer │ │ │
│ or scratch area │ │ │
10H ├─────────────────────────┤ 10H ├─────────────────────────┤
│ │ │ │
│ Segment of FAT buffer │ │ │
│ or scratch area │ │ │
12H ├─────────────────────────┤ 12H ├─────────────────────────┤
│ │ │ │
│ │ │ Offset of BIOS │
│ │ │ parameter block │
14H ├─────────────────────────┤ 14H ├─────────────────────────┤
│ │ │ │
│ │ │ Segment of BIOs │
│ │ │ parameter block │
└─────────────────────────┘ └─────────────────────────┘
Figure 15-6. Build BPB request header (command code 2).
Table 15-4. Format of a BIOS Parameter Block (BPB).
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Bytes Contents
──────────────────────────────────────────────────────────────────
00-01H Bytes per sector
02H Sectors per allocation unit (must be power of 2)
03-04H Number of reserved sectors (starting at sector 0)
05H Number of file allocation tables (FATs)
06-07H Maximum number of root-directory entries
08-09H Total number of sectors in medium
0AH Media ID byte
0B-0CH Number of sectors occupied by a single FAT
0D-0EH Sectors per track (versions 3.0 and later)
0F-10H Number of heads (versions 3.0 and later)
11-12H Number of hidden sectors (versions 3.0 and later)
13-14H High-order word of number of hidden sectors (version 3.2)
15-18H If bytes 8-9 are zero, total number of sectors in medium
(version 3.2)
In character-device drivers, the Build BPB function should simply set
the done flag in the status word of the request header and return.
The Read, Write, and Write with Verify functions
The Read (Input) function (command code 4) transfers data from the
device into a specified memory buffer. The Write (Output) function
(command code 8) transfers data from a specified memory buffer to the
device. The Write with Verify function (command code 9) works like the
Write function but, if feasible, also performs a read-after-write
verification that the data was transferred correctly. The MS-DOS
kernel calls the Write with Verify function, instead of the Write
function, whenever the system's global verify flag has been turned on
with the VERIFY command or with Interrupt 21H Function 2EH (Set Verify
Flag).
All three of these driver functions are called by the MS-DOS kernel
with the address and length of the buffer for the data to be
transferred. In the case of block-device drivers, the kernel also
passes the drive unit code, the starting logical sector number, and
the media ID byte for the disk (Figure 15-7).
Driver called with Driver returns
00H ┌─────────────────────────┐ 00H ┌─────────────────────────┐
│ Request header length │ │ │
01H ├─────────────────────────┤ 01H ├─────────────────────────┤
│ Unit number │ │ │
02H ├─────────────────────────┤ 02H ├─────────────────────────┤
│ Command code │ │ │
03H ├─────────────────────────┤ 03H ├─────────────────────────┤
│ │ │ │
│ │ │ Status │
│ │ │ │
05H ├─────────────────────────┤ 05H ├─────────────────────────┤
│ │ │ │
│ │ │ │
│ Reserved │ │ Reserved │
│ │ │ │
│ │ │ │
0DH ├─────────────────────────┤ 0DH ├─────────────────────────┤
│ Media ID byte │ │ │
0EH ├─────────────────────────┤ 0EH ├─────────────────────────┤
│ │ │ │
│ Offset of data │ │ │
│ │ │ │
10H ├─────────────────────────┤ 10H ├─────────────────────────┤
│ │ │ │
│ Segment of data │ │ │
│ │ │ │
12H ├─────────────────────────┤ 12H ├─────────────────────────┤
│ │ │ │
│ Bytes/sectors requested │ │Bytes/sectors transferred│
│ │ │ │
14H ├─────────────────────────┤ 14H ├─────────────────────────┤
│ │ │ │
│Starting sector number │ │ │
│ │ │ │
16H ├─────────────────────────┤ 16H ├─────────────────────────┤
│ │ │ │
│ │ │ Offset of volume label │
│ │ │ (if error 0FH) │
18H ├─────────────────────────┤ 18H ├─────────────────────────┤
│ │ │ │
│ │ │ Segment of volume label │
│ │ │ (if error 0FH) │
└─────────────────────────┘ └─────────────────────────┘
Figure 15-7. The request header for IOCTL Read (command code 3), Read
(command code 4), Write (command code 8), Write with Verify (command
code 9), IOCTL Write (command code 12), and Output Until Busy (command
code 16).
The Read and Write functions must perform the requested I/O, first
translating each logical sector number for a block device into a
physical track, head, and sector with the aid of the BIOS parameter
block. Then the functions must return the number of bytes or sectors
actually transferred in the appropriate field of the request header
and also set the done flag in the request header status word. If an
error is encountered during an operation, the functions must set the
done flag, the error flag, and the error type in the status word and
also report the number of bytes or sectors successfully transferred
before the error; it is not sufficient to simply report the error.
Under MS-DOS versions 3.0 and later, the Read and Write functions can
optionally use the reference count of open files maintained by the
driver's Device Open and Device Close functions, together with the
media ID byte, to determine whether the medium has been illegally
changed. If the medium was changed with files open, the driver can
return the error code 0FH and the segment and offset of the volume
label for the correct disk so that the user can be prompted to replace
the disk.
The Nondestructive Read function
The Nondestructive Read function (command code 5) is supported only on
character devices. It allows MS-DOS to look ahead in the character
stream by one character and is used to check for Control-C characters
pending at the keyboard.
The function is called by the kernel with no parameters other than the
command code itself (Figure 15-8). It must set the done bit in the
status word of the request header and also set the busy bit in the
status word to reflect whether the device's input buffer is empty
(busy bit = 1) or contains at least one character (busy bit = 0). If
the latter, the function must also return the next character that
would be obtained by a kernel call to the Read function, without
removing that character from the buffer (hence the term
nondestructive).
In block-device drivers, the Nondestructive Read function should
simply set the done flag in the status word of the request header and
return.
Driver called with Driver returns
00H ┌─────────────────────────┐ 00H ┌─────────────────────────┐
│ Request header length │ │ │
01H ├─────────────────────────┤ 01H ├─────────────────────────┤
│ │ │ │
02H ├─────────────────────────┤ 02H ├─────────────────────────┤
│ Command code │ │ │
03H ├─────────────────────────┤ 03H ├─────────────────────────┤
│ │ │ │
│ │ │ Status │
│ │ │ │
05H ├─────────────────────────┤ 05H ├─────────────────────────┤
│ │ │ │
│ │ │ │
│ Reserved │ │ Reserved │
│ │ │ │
│ │ │ │
0DH ├─────────────────────────┤ 0DH ├─────────────────────────┤
│ │ │ Character │
└─────────────────────────┘ └─────────────────────────┘
Figure 15-8. The Nondestructive Read request header.
The Input Status and Output Status functions
The Input Status and Output Status functions (command codes 6 and 10)
are defined only for character devices. They are called with no
parameters in the request header other than the command code itself
and return their results in the busy bit of the request header status
word (Figure 15-9). These functions constitute the driver-level
support for the services the MS-DOS kernel provides to application
programs by means of Interrupt 21H Function 44H Subfunctions 06H and
07H (Check Input Status and Check Output Status).
MS-DOS calls the Input Status function to determine whether there are
characters waiting in a type-ahead buffer. The function sets the done
bit in the status word of the request header and sets the busy bit to
0 if at least one character is already in the input buffer or to 1 if
no characters are in the buffer and a read request would wait on a
character from the physical device. If the character device does not
have a type-ahead buffer, the Input Status routine should always
return the busy bit set to 0 so that MS-DOS will not wait for
something to arrive in the buffer before calling the Read function.
Driver called with Driver returns
00H ┌─────────────────────────┐ 00H ┌─────────────────────────┐
│ Request header length │ │ │
01H ├─────────────────────────┤ 01H ├─────────────────────────┤
│ │ │ │
02H ├─────────────────────────┤ 02H ├─────────────────────────┤
│ Command code │ │ │
03H ├─────────────────────────┤ 03H ├─────────────────────────┤
│ │ │ │
│ │ │ Status │
│ │ │ │
05H ├─────────────────────────┤ 05H ├─────────────────────────┤
│ │ │ │
│ │ │ │
│ Reserved │ │ Reserved │
│ │ │ │
│ │ │ │
0DH └─────────────────────────┘ 0DH └─────────────────────────┘
Figure 15-9. The request header for Input Status (command code 6),
Flush Input Buffers (command code 7), Output Status (command code 10),
and Flush Output Buffers (command code 11).
MS-DOS uses the Output Status function to determine whether a write
operation is already in progress for the device. The function must set
the done bit and the busy bit (0 if the device is idle and a write
request would start immediately; 1 if a write is already in progress
and a new write request would be delayed) in the status word of the
request header.
In block-device drivers, the Input Status and Output Status functions
should simply set the done flag in the status word of the request
header and return.
The Flush Input Buffer and Flush Output Buffer functions
The Flush Input Buffer and Flush Output Buffer functions (command
codes 7 and 11) are defined only for character devices. They simply
terminate any read (for Flush Input) or write (for Flush Output)
operations that are in progress and empty the associated buffer. The
Flush Input Buffer function is used by MS-DOS to discard characters
waiting in the type-ahead queue. This driver action corresponds to the
MS-DOS service provided to application programs by means of Interrupt
21H Function 0CH (Flush Buffer, Read Keyboard).
These functions are called with no parameters in the request header
other than the command code itself (see Figure 15-9) and return only
the status word.
In block-device drivers, the Flush Buffer functions have no meaning.
They should simply set the done flag in the status word of the request
header and return.
The IOCTL Read and IOCTL Write functions
The IOCTL (I/O Control) Read and IOCTL Write functions (command codes
3 and 12) allow control information to be passed directly between a
device driver and an application program. The IOCTL Read and Write
driver functions are called by the MS-DOS kernel only if the IOCTL
flag (bit 14) is set in the device attribute word of the device
header.
The MS-DOS kernel passes the address and length of the buffer that
contains or will receive the IOCTL information (see Figure 15-7). The
driver must return the actual count of bytes transferred and set the
done flag in the request header status word. Any error code returned
by the driver is ignored by the kernel.
IOCTL Read and IOCTL Write operations are typically used to configure
a driver or device or to report driver or device status and do not
usually result in the transfer of data to or from the physical device.
These functions constitute the driver support for the services
provided to application programs by the MS-DOS kernel through
Interrupt 21H Function 44H Subfunctions 02H, 03H, 04H, and 05H
(Receive Control Data from Character Device, Send Control Data to
Character Device, Receive Control Data from Block Device, and Send
Control Data to Block Device).
The Device Open and Device Close functions
The Device Open and Device Close functions (command codes 13 and 14)
are supported only in MS-DOS versions 3.0 and later and are called
only if the open/close/removable media flag (bit 11) is set in the
device attribute word of the device header. The Device Open and Device
Close functions have no parameters in the request header other than
the unit code for block devices and return nothing except the done
flag and, if applicable, the error flag and number in the request
header status word (Figure 15-10).
Driver called with Driver returns
00H ┌─────────────────────────┐ 00H ┌─────────────────────────┐
│ Request header length │ │ │
01H ├─────────────────────────┤ 01H ├─────────────────────────┤
│ Unit number │ │ │
02H ├─────────────────────────┤ 02H ├─────────────────────────┤
│ Command code │ │ │
03H ├─────────────────────────┤ 03H ├─────────────────────────┤
│ │ │ │
│ │ │ Status │
│ │ │ │
05H ├─────────────────────────┤ 05H ├─────────────────────────┤
│ │ │ │
│ │ │ │
│ Reserved │ │ Reserved │
│ │ │ │
│ │ │ │
0DH └─────────────────────────┘ 0DH └─────────────────────────┘
Figure 15-10. The request header for Device Open (command code 13),
Device Close (command code 14), and Removable Media (command code 15).
Each Interrupt 21H request by an application to open or create a file
or to open a character device for input or output results in a Device
Open call by the kernel to the corresponding device driver. Similarly,
each Interrupt 21H call by an application to close a file or device
results in a Device Close call by the kernel to the appropriate device
driver. These Device Open and Device Close calls are in addition to
any directory read or write calls that may be necessary.
On block devices, the Device Open and Device Close functions can be
used to manage local buffering and to maintain a reference count of
the number of open files on a device. Whenever this reference count is
decremented to zero, all files on the disk have been closed and the
driver should flush any internal buffers so that data is not lost, as
the user may be about to change disks. The reference count can also be
used together with the media ID byte by the Read and Write functions
to determine whether the disk has been changed while files are still
open.
The reference count should be forced to zero when a Media Check call
that returns the Medium changed code is followed by a Build BPB call,
to provide for those programs that use FCBs to open files and then
never close them. This problem does not arise with programs that use
the handle functions for file management, because all handles are
always closed automatically by MS-DOS on behalf of the program when it
terminates. See PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR
MS-DOS: File and Record Management.
On character devices, the Device Open and Device Close functions can
be used to send hardware-dependent initialization and post-I/O strings
to the associated device (for example, a reset sequence or formfeed
character to precede new output and a formfeed to follow it). Although
these strings can be written directly by an application using ordinary
write function calls, they can also be previously passed to the driver
by application programs with IOCTL Write calls (Interrupt 21H Function
44H Subfunction 05H), which in turn are translated by the MS-DOS
kernel into driver command code 12 (IOCTL Write) requests. The latter
method makes the driver responsible for sending the proper control
strings to the device each time a Device Open or Device Close is
executed, but this method can be used only with drivers specifically
written to support it.
The Removable Media function
The Removable Media function (command code 15) is defined only for
block devices. It is supported in MS-DOS versions 3.0 and later and is
called by MS-DOS only if the open/ close/removable media flag (bit 11)
is set in the device attribute word of the device header. This
function constitutes the driver-level support for the service provided
to application programs by MS-DOS by means of Interrupt 21H Function
44H Subfunction 08H (Check If Block Device Is Removable).
The only parameter for the Removable Media function is the unit code
(see Figure 15-10). The function sets the done bit in the request
header status word and sets the busy bit to 1 if the disk is not
removable or to 0 if the disk is removable. This information can be
used by MS-DOS to optimize its accesses to the disk and to eliminate
unnecessary FAT and directory reads.
In character-device drivers, the Removable Media function should
simply set the done flag in the status word of the request header and
return.
The Output Until Busy function
The Output Until Busy function (command code 16) is defined only for
character devices under MS-DOS versions 3.0 and later and is called by
the MS-DOS kernel only if the corresponding flag (bit 13) is set in
the device attribute word of the device header. This function is an
optional driver-optimization function included specifically for the
benefit of background print spoolers driving printers that have
internal memory buffers. Such printers can accept data at a rapid rate
until the buffer is full.
The Output Until Busy function is called with the address and length
of the data to be written to the device (see Figure 15-7). It
transfers data continuously to the device until the device indicates
that it is busy or until the data is exhausted. The function then must
set the done flag in the request header status word and return the
actual number of bytes transferred in the appropriate field of the
request header.
For this function to return a count of bytes transferred that is less
than the number of bytes requested is not an error. MS-DOS will adjust
the address and length of the data passed in the next Output Until
Busy function request so that all characters are sent.
In block-device drivers, the Output Until Busy function should simply
set the done flag in the status word of the request header and return.
The Generic IOCTL function
The Generic IOCTL function (command code 19) is defined under MS-DOS
version 3.2 and is called only if the 3.2-functions-supported flag
(bit 6) is set in the device attribute word of the device header. This
driver function corresponds to the MS-DOS generic IOCTL service
supplied to application programs by means of Interrupt 21H Function
44H Subfunctions 0CH (Generic I/O Control for Handles) and 0DH
(Generic I/O Control for Block Devices).
In addition to the usual information in the static portion of the
request header, the Generic IOCTL function is passed a category
(major) code, a function (minor) code, the contents of the SI and DI
registers at the point of the IOCTL call, and the segment and offset
of a data buffer (Figure 15-11). This buffer in turn contains other
information whose format depends on the major and minor IOCTL codes
passed in the request header. The driver must interpret the major and
minor codes in the request header and the contents of the additional
buffer to determine which operation it will carry out and then set the
done flag in the request header status word and return any other
applicable information in the request header or the data buffer.
Services that can be invoked by the Generic IOCTL function, if the
driver supports them, include configuring the driver for nonstandard
disk formats, reading and writing entire disk tracks of data, and
formatting and verifying tracks. The Generic IOCTL function has been
designed to be open-ended so that it can be used to easily extend the
device driver definition in future versions of MS-DOS.
Driver called with Driver returns
00H ┌─────────────────────────┐ 00H ┌─────────────────────────┐
│ Request header length │ │ │
01H ├─────────────────────────┤ 01H ├─────────────────────────┤
│ Unit number │ │ │
02H ├─────────────────────────┤ 02H ├─────────────────────────┤
│ Command code │ │ │
03H ├─────────────────────────┤ 03H ├─────────────────────────┤
│ │ │ │
│ │ │ Status │
│ │ │ │
05H ├─────────────────────────┤ 05H ├─────────────────────────┤
│ │ │ │
│ │ │ │
│ Reserved │ │ Reserved │
│ │ │ │
│ │ │ │
0DH ├─────────────────────────┤ 0DH ├─────────────────────────┤
│ Category (major) code │ │ │
0EH ├─────────────────────────┤ 0EH ├─────────────────────────┤
│ Function (minor) code │ │ │
0FH ├─────────────────────────┤ 0FH ├─────────────────────────┤
│ │ │ │
│ SI register contents │ │ │
│ │ │ │
11H ├─────────────────────────┤ 11H ├─────────────────────────┤
│ │ │ │
│ DI register contents │ │ │
│ │ │ │
13H ├─────────────────────────┤ 13H ├─────────────────────────┤
│ │ │ │
│ Offset of generic │ │ │
│ IOCTL data packet │ │ │
15H ├─────────────────────────┤ 15H ├─────────────────────────┤
│ │ │ │
│ Segment of generic │ │ │
│ IOCTL data packet │ │ │
└─────────────────────────┘ └─────────────────────────┘
Figure 15-11. Generic IOCTL request header.
The Get Logical Device and Set Logical Device functions
The Get and Set Logical Device functions (command codes 23 and 24) are
defined only for block devices under MS-DOS version 3.2 and are called
only if the 3.2-functions-supported flag (bit 6) is set in the device
attribute word of the device header. They correspond to the Get and
Set Logical Drive Map services supplied by MS-DOS to application
programs by means of Interrupt 21H Function 44H Subfunctions 0EH and
0FH.
The Get and Set Logical Device functions are called with a drive unit
number in the request header (Figure 15-12). Both functions return a
status word for the operation in the request header; the Get Logical
Device function also returns a unit number.
The Get Logical Device function is called to determine whether more
than one drive letter is assigned to the same physical device. It
returns a code for the last drive letter used to reference the device
(1 = A, 2 = B, and so on); if only one drive letter is assigned to the
device, the returned unit code should be 0.
The Set Logical Device function is called to inform the driver of the
next logical drive identifier that will be used to reference the
device. The unit code passed by the MS-DOS kernel in this case is zero
based relative to the logical drives supported by this particular
driver. For example, if the driver supports two logical floppy-disk-
drive units (A and B), only one physical disk drive exists in the
system, and Set Logical Device is called with a unit number of 1, the
driver is being informed that the next read or write request from the
MS-DOS kernel will be directed to drive B.
Driver called with Driver returns
00H ┌─────────────────────────┐ 00H ┌─────────────────────────┐
│ Request header length │ │ │
01H ├─────────────────────────┤ 01H ├─────────────────────────┤
│ Unit number │ │ Last device driver │
02H ├─────────────────────────┤ 02H ├─────────────────────────┤
│ Command code │ │ │
03H ├─────────────────────────┤ 03H ├─────────────────────────┤
│ │ │ │
│ │ │ Status │
│ │ │ │
05H ├─────────────────────────┤ 05H ├─────────────────────────┤
│ │ │ │
│ │ │ │
│ Reserved │ │ Reserved │
│ │ │ │
│ │ │ │
0DH └─────────────────────────┘ 0DH └─────────────────────────┘
Figure 15-12. Get Logical Device and Set Logical Device request
header.
In character-device drivers, the Get Logical Device and Set Logical
Device functions should simply set the done flag in the status word of
the request header and return.
The Processing of a Typical I/O Request
An application program requests an I/O operation from MS-DOS by
loading registers with the appropriate values and addresses and
executing a software Interrupt 21H. MS-DOS inspects its internal
tables, searches the chain of device headers if necessary, and
determines which device driver should receive the I/O request.
MS-DOS then creates a request header data packet in a reserved area of
memory. Disk I/O requests are transformed from file and record
information into logical sector requests by MS-DOS's interpretation of
the disk directory and file allocation table. (MS-DOS locates these
disk structures using the information returned by the driver from a
previous Build BPB call and issues additional driver read requests, if
necessary, to bring their sectors into memory.)
After the request header is prepared, MS-DOS calls the device driver's
Strategy entry point, passing the address of the request header in
registers ES:BX. The Strategy routine saves the address of the request
header and performs a far return to MS-DOS.
MS-DOS then immediately calls the device driver's Interrupt entry
point. The Interrupt routine saves all registers, retrieves the
address of the request header that was saved by the Strategy routine,
extracts the command code, and branches to the appropriate function to
perform the operation requested by MS-DOS. When the requested function
is complete, the Interrupt routine sets the done flag in the status
word and places any other required information into the request
header, restores all registers to their state at entry, and performs a
far return.
MS-DOS translates the driver's returned status into the appropriate
carry flag status, register values, and (possibly) error code for the
MS-DOS Interrupt 21H function that was requested and returns control
to the application program. Figure 15-13 sketches this entire flow of
control and data.
┌─────────────────────────────────────────┐
│ Application program │
└─────────────────┬──────────────────────┘
│ │
Interrupt 21H Function 3FH, │ │ Read status returned
Read File or Device │ │ in carry flag and AX register
│ │
┌────────────────────────┴───────────────┐
│ MS-DOS kernel │
└─────────────────┬──────────────────────┘
Calls to driver Strategy, then │ │ Status returned to MS-DOS
Interrupt routine, passing │ │ kernel in request header,
request header with command │ │ data placed in buffer
code 4, Read (Input) │ │ indicated by kernel
┌────────────────────────┴───────────────┐
│ Device driver │
└─────────────────┬──────────────────────┘
Device commands issued to │ │ Data transferred from
adapter I/O ports, requesting │ │ device to memory
read sector at physical track, │ │
head, and sector number │ │
┌────────────────────────┴───────────────┐
│ Physical device │
└─────────────────────────────────────────┘
Figure 15-13. The processing of a typical I/O request from an
application program.
Note that a single Interrupt 21H function request by an application
program can result in many operation requests by MS-DOS to the device
driver. For example, if the application invokes Interrupt 21H Function
3DH (Open File with Handle) to open a file, MS-DOS may have to issue
multiple sector read requests to the driver while searching the
directory for the filename. Similarly, an application program's
request to write a string to the screen in cooked mode with Interrupt
21H Function 40H (Write File or Device) will result in a write request
to the driver for each character in the string, because MS-DOS filters
the characters and polls the keyboard for a pending Control-C between
each character output.
Writing Device Drivers
Device drivers are traditionally coded in assembly language, both
because of the rigid structural requirements and because of the need
to keep driver execution speed high and memory overhead low. Although
MS-DOS versions 3.0 and later are capable of loading drivers in .EXE
format, versions 2.x can load only pure memory-image device drivers
that do not require relocation. Therefore, drivers are typically
written as though they were .COM programs with an "origin" of zero and
converted with EXE2BIN to .BIN or .SYS files so that they will be
compatible with any version of MS-DOS (2.0 or later). See PROGRAMMING
IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: Structure of an
Application Program.
The device header must be located at the beginning of the file (offset
0). Both words in the header's link field should be set to -1, thus
allowing MS-DOS to fix up the link field when the driver is loaded
during system initialization so that it points to the next driver in
the chain. When a single file contains more than one driver, the
offset portion of each header link field should point to the next
header in that file, all using the same segment base of zero, and only
the link field of the last header in the file should be set to -1, -1.
The device attribute word must reflect the device-driver type
(character or block) and the bits that indicate support for the
various optional command codes must have appropriate values. The
device header's offsets to the Strategy and Interrupt routines must be
relative to the same segment base as the device header itself. If the
driver is for a character device, the name field should be filled in
properly with the device's logical name, which can be any legal eight-
character uppercase filename padded with spaces and without a colon.
Duplication of existing character-device names or existing disk-file
names should be avoided (unless a resident character-device driver is
being intentionally superseded).
The Strategy and Interrupt routines for the device are called by MS-
DOS by means of an intersegment call (CALL FAR) and must return to MS-
DOS with a far return. Both routines must preserve all CPU registers
and flags. The MS-DOS kernel's stack has room for 40 to 50 bytes when
the driver is called; if the driver makes heavy use of the stack, it
should switch to an internal stack of adequate depth.
The Strategy routine is, of course, very simple. It need only save the
address of the request header that is passed to it in registers ES:BX
and exit back to the kernel.
The logic of the Interrupt routine is necessarily more complex. It
must save the CPU registers and flags, extract the command code from
the request header whose address was previously saved by the Strategy
routine, and dispatch the appropriate command-code function. When that
function is finished, the Interrupt routine must ensure that the
appropriate status and other information is placed in the request
header, restore the CPU registers and flags, and return control to the
kernel.
Although the interface between the MS-DOS kernel and the command-code
routines is fairly simple, it is also strict. The command-code
functions must behave exactly as they are defined or the system will
behave erratically. Even a very subtle discrepancy in the action of a
driver function can have unexpectedly large global effects. For
example, if a block driver Read function returns an error but does not
return a correct value for the number of sectors successfully
transferred, the MS-DOS kernel will be misled in its attempts to retry
the read for only the failing sectors and disk data might be
corrupted.
Example character driver: TEMPLATE
Figure 15-14 contains the source code for a skeleton character-device
driver called TEMPLATE.ASM. This driver does nothing except display a
sign-on message when it is loaded, but it demonstrates all the
essential driver components, including the device header, Strategy
routine, and Interrupt routine. The command-code functions take no
action other than to set the done flag in the request header status
word.
──────────────────────────────────────────────────────────────────────
Figure 15-14. TEMPLATE.ASM, the source file for the TEMPLATE.SYS
driver.
──────────────────────────────────────────────────────────────────────
TEMPLATE.ASM can be assembled, linked, and converted into a loadable
driver with the following commands:
C>MASM TEMPLATE; <Enter>
C>LINK TEMPLATE; <Enter>
C>EXE2BIN TEMPLATE.EXE TEMPLATE.SYS <Enter>
The Microsoft Object Linker (LINK) will display the warning message No
Stack Segment; this message can be ignored. The driver can then be
installed by adding the line
DEVICE=TEMPLATE.SYS
to the CONFIG.SYS file and restarting the system. The fact that the
TEMPLATE.SYS driver also has the logical character-device name
TEMPLATE allows the demonstration of an interesting MS-DOS effect:
After the driver is installed, the file that contains it can no longer
be copied, renamed, or deleted. The reason for this limitation is that
MS-DOS always searches its list of character-device names first when
an open request is issued, before it inspects the disk directory. The
only way to erase the TEMPLATE.SYS file is to modify the CONFIG.SYS
file to remove the associated DEVICE statement and then restart the
system.
For a complete example of a character-device driver for interrupt-
driven serial communications, See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: STRUCTURE OF MS-DOS: Interrupt-Driven Communications.
Example block driver: TINYDISK
Figure 15-15 contains the source code for a simple 64 KB virtual disk
(RAMdisk) called TINYDISK.ASM. This code provides a working example of
a simple block-device driver. When its Initialization routine is
called by the kernel, TINYDISK allocates itself 64 KB of RAM and maps
a disk structure onto the RAM in the form of a boot sector containing
a valid BPB, a FAT, a root directory, and a files area. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: STRUCTURE OF MS-DOS: MS-DOS
Storage Devices.
──────────────────────────────────────────────────────────────────────
Figure 15-15. TINYDISK.ASM, the source file for the TINYDISK.SYS
driver.
──────────────────────────────────────────────────────────────────────
Subsequent driver Read and Write calls by the kernel to TINYDISK
function as though they were transferring sectors to and from a
physical storage device but actually only copy data from one area in
memory to another. A programmer can learn a great deal about the
operation of block-device drivers and MS-DOS's relationship to those
drivers (such as the order and frequency of Media Change, Build BPB,
Read, Write, and Write With Verify calls) by inserting software probes
into TINYDISK at appropriate locations and monitoring its behavior.
TINYDISK.ASM can be assembled, linked, and converted into a loadable
driver with the following commands:
MASM TINYDISK;
LINK TINYDISK;
EXE2BIN TINYDISK.EXE TINYDISK.SYS
The linker will display the warning message No Stack Segment; this
message can be ignored. The driver can then be installed by adding the
line
DEVICE=TINYDISK.SYS
to the CONFIG.SYS file and restarting the system. When it is loaded,
TINYDISK displays a sign-on message and the drive letter that it was
assigned if it is running under MS-DOS version 3.0 or later. (If the
host system is MS-DOS version 2.x, this information is not provided to
the driver.) Files can then be copied to the RAMdisk as though it were
a small but extremely fast disk drive.
Ray Duncan
───────────────────────────────────────────────────────────────────────────
Part D Directions of MS-DOS
Article 16: Writing Applications for Upward Compatibility
One of the major concerns of the designers of Microsoft OS/2 was that
it be backwardly compatible--that is, that programs written to run
under MS-DOS versions 2 and 3 be able to run on MS OS/2. A major
concern for present application programmers is that their programs run
not only on current versions of MS-DOS (and MS OS/2) but also on
future versions of MS-DOS. Ensuring such upward compatibility involves
both hardware issues and operating-system issues.
Hardware Issues
A basic requirement for ensuring upward compatibility is hardware-
independent code. If you bypass system services and directly program
the hardware--such as the system interrupt controller, the system
clock, and the enhanced graphics adapter (EGA) registers--your
application will not run on future versions of MS-DOS.
Protected mode compatibility
The 80286 and the 80386 microprocessors can operate in two
incompatible modes: real mode and protected mode. When either chip is
operating in real mode, it is perceived by the operating system and
programs as a fast 8088 chip. Applications written for the 8086 and
8088 run the same on the 80286 and the 80386--only faster. They
cannot, however, take advantage of 80286 and 80386 features unless
they can run in protected mode.
Following the guidelines below will minimize the work necessary to
convert a real mode program to protected mode and will also allow a
program to use a special subset of the MS OS/2 Applications Program
Interface (API)--Family API. A binary program (.EXE) that uses the
family API can run in either protected mode or real mode under MS OS/2
and subsequent systems, but it can run only in real mode under MS-DOS
version 3.
Family API
The Family API requires that the application use a subset of the MS
OS/2 Dynamic Link System API. Special tools link the application with
a special library that implements the subset MS OS/2 system services
in the MS-DOS version 3 environment. Many of these services are
implemented by calling the appropriate Interrupt 21H subfunction; some
are implemented in the special library itself.
When a Family API application is loaded under MS OS/2 protected mode,
MS OS/2 ignores the special library code and loads only the
application itself. MS OS/2 then provides the requested services in
the normal fashion. However, MS-DOS version 3 loads the entire
package--the application and the special library--because the Family
API.EXE file is constructed to look like an MS-DOS 3.EXE file.
Linear vs segmented memory
The protected mode and the real mode of the 80286 and the 80386 are
compatible except in the area of segmentation. The 8086 has been
described as a segmented machine, but it is actually a linear memory
machine with offset registers. When a memory address is generated, the
value in one of the "segment" registers is multiplied by 16 and added
as a displacement to the offset value supplied by the instruction's
addressing mode. No length information is associated with each
"segment"; the "segment" register supplies only a 20-bit addressing
offset. Programs routinely use this by computing a 20-bit address and
then decomposing it into a 16-bit "segment" value and a 16-bit
displacement value so that the address can be referenced.
The protected mode of the 80286 and the 80386, however, is truly
segmented. A value placed in a segment register selects an entry from
a descriptor table; that entry contains the addressing offset, a
segment length, and permission bits. On the 8086, the so-called
segment component of an address is multiplied by 16 and added to the
offset component, producing a 20-bit physical address. Thus, if you
take an address in the segment:offset form, add 4 to the segment
value, and subtract 64 (that is, 4 * 16) from the offset value, the
new address references exactly the same location as the old address.
On the 80286 and the 80386 in protected mode, however, segment values,
called segment selectors, have no direct correspondence to physical
addresses. In other words, in 8086 mode, the two address forms
1000(sub 16):0345(sub 16)
and
1004(sub 16):0305(sub 16)
reference the same memory location, but in protected mode these two
forms reference totally different locations.
Creating segment values
This architectural difference gives rise to the most common cause of
incompatibility--the program performs addressing arithmetic to compute
"segment" values. Any program that uses the 20-bit addressing scheme
to create or to compute a value to be loaded in a segment register
cannot be converted to run in protected mode. To be protected mode
compatible, a program must treat the 8086's so-called segments as true
segments.
To create a program that does this, write according to the following
guidelines:
1. Do not generate any segment values. Use only the segment values
supplied by MS-DOS calls and those placed in the segment registers
when MS-DOS loaded your program. The exception is "huge objects"--
memory objects larger than 64 KB. In this case, MS OS/2 provides a
base segment number and a "segment offset value." The returned
segment number selects the first 64 KB of the object and the
segment number, plus the segment offset value address the second
64 KB of the object. Likewise, the returned segment value plus
N*(segment offset value) selects the N+1 64 KB piece of the huge
object. Write real mode code in this same fashion, using 4096 as
the segment offset value. When you convert your program, you can
substitute the value provided by MS OS/2.
2. Do not address beyond the allocated length of a segment.
3. Do not use segment registers as scratch registers by placing
general data in them. Place only valid segment values, supplied by
MS-DOS, in a segment register. The one exception is that you can
place a zero value in a segment register, perhaps to indicate "no
address." You can place the zero in the segment register, but you
cannot reference memory using that register; you can only
load/store or push/pop it.
4. Do not use CS: overrides on instructions that store into memory. It
is impossible to store into a code segment in protected mode.
CPU speed
Because various microprocessors and machine configurations execute at
different speeds, a program should not contain timing loops that
depend on CPU speed. Specifically, a program should not establish CPU
speed during initialization and then use that value for timing loops
because the preemptive scheduling of MS OS/2 and future operating
systems can "take away" the CPU at any time for arbitrary and
unpredictable lengths of time. (In any case, time should not be wasted
in a timing loop when other processes could be using system
resources.)
Program timing
Programs must measure the passage of time carefully. They can use the
system clock-tick interrupt while directly interfacing with the user,
but no clock ticks will be seen by real mode programs when the user
switches the screen interface to another program.
It is recommended that applications use the time-of-day system
interface to determine elapsed time. To facilitate conversion to MS
OS/2 protected mode, programs should encapsulate time-of-day or
elapsed-time functions into subroutines.
BIOS
Avoid BIOS interrupt interfaces except for Interrupt 10H (the screen
display functions) and Interrupt 16H (the keyboard functions).
Interrupt 10H functions are contained in the MS OS/2 VIO package, and
Interrupt 16H functions are in the MS OS/2 KBD package. Other BIOS
interrupts provide functions that are available under MS OS/2 only in
considerably modified forms.
Special operations
Uncommon, or special, operations and instructions can produce varied
results, depending on the microprocessor. For example, when a "divide
by 0" trap is taken on an 8086, the stack frame points to the
instruction after the fault; when such action is taken on the 80286
and 80386, the return address points to the instruction that caused
the fault. The effect of pushing the SP register is different between
the 80286 and the 80386 as well. See Appendix M: 8086/8088 Software
Compatibility Issues. Write your program to avoid these problem areas.
Operating-System Issues
Basic to writing programs that will run on future operating systems is
writing code that is not version specific. Incorporating special
version-specific features in a program will virtually ensure that the
program will be incompatible with future versions of MS-DOS and MS
OS/2.
Following the guidelines below will not necessarily ensure your
program's compatibility, but it will facilitate converting the program
or using the Family API to produce a dual-mode binary program.
Filenames
MS-DOS versions 2 and 3 silently truncate a filename that is longer
than eight characters or an extension that is longer than three
characters. MS-DOS generates no error message when performing this
task. In real mode, MS OS/2 also silently truncates a filename or
extension that exceeds the maximum length; in protected mode, however,
it does not. Therefore, a real mode application program needs to
perform this truncating function. The program should check the length
of the filenames that it generates or that it obtains from a user and
refuse names that are longer than the eight-character maximum. This
prevents improperly formatted names from becoming embedded in data and
control files--a situation that could cause a protected mode version
of the application to fail when it presents that invalid name to the
operating system.
When you convert your program to protected mode API, remove the
length-checking code; MS OS/2 will check the length and return an
error code as appropriate. Future file systems will support longer
filenames, so it's important that protected mode programs simply
present filenames to the operating system, which is then responsible
for judging their validity.
Other MS-DOS version 2 and 3 elements have fixed lengths, including
the current directory path. To be upwardly compatible, your program
should accept whatever length is provided by the user or returned from
a system call and rely on MS OS/2 to return an error message if a
length is inappropriate. The exception is filename length in real mode
non-Family API programs: These programs should enforce the eight-
character maximum because MS-DOS versions 2 and 3 fail to do so.
File truncation
Files are truncated by means of a zero-length write under MS-DOS
versions 2 and 3; under MS OS/2 in protected mode, files are truncated
with a special API. File truncation operations should be encapsulated
in a special routine to facilitate conversion to MS OS/2 protected
mode or the Family API.
File searches
MS-DOS versions 2 and 3 never close file-system searches (Find First
File/Find Next File). The returned search contains the information
necessary for MS-DOS to continue the search later, and if the search
is never continued, no harm is done.
MS OS/2, however, retains the necessary search continuation
information in an internal structure of limited size. For this reason,
your program should not depend on more than about 10 simultaneous
searches and it should be able to close searches when it is done. If
your program needs to perform more than about 10 searches
simultaneously, it should be able to close a search, restart it later,
and advance to the place where the program left off, rather than
depending on MS OS/2 to continue the search.
MS OS/2 further provides a Find Close function that releases the
internal search information. Protected mode programs should use this
call at the end of every search sequence. Because MS-DOS versions 2
and 3 have no such call, your program should call a dummy procedure by
this name at the appropriate locations. Then you can convert your
program to the protected mode API or to the Family API without
reexamining your algorithms.
Note: Receiving a "No more files" return code from a search does not
implicitly close the search; all search closes must be explicit.
The Family API allows only a single search at a time. To circumvent
this restriction, code two different Find Next File routines in your
program--one for MS OS/2 protected mode and one for MS-DOS real mode--
and use the Family API function that determines the program's current
environment to select the routine to execute.
MS-DOS calls
A program that uses only the Interrupt 21H functions listed below is
guaranteed to work in the Compatibility Box of MS OS/2 and will be
relatively easy to modify for MS OS/2 protected mode.
╓┌─────────────────────┌─────────────────────────────────────────────────────╖
Function Name
──────────────────────────────────────────────────────────────────
0DH Disk Reset
0EH Select Disk
19H Get Current Disk
1AH Set DTA Address
25H Set Interrupt Vector
2AH Get Date
2BH Set Date
2CH Get Time
2EH Set/Reset Verify Flag
2FH Get DTA Address
30H Get MS-DOS Version Number
33H Get/Set Control-C Check Flag
35H Get Interrupt Vector
36H Get Disk Free Space
38H Get/Set Current Country
39H Create Directory
3AH Remove Directory
3BH Change Current Directory
3CH Create File with Handle
3DH Open File with Handle
3EH Close File
3FH Read File or Device
40H Write File or Device
41H Delete File
42H Move File Pointer
43H Get/Set File Attributes
44H IOCTL (all subfunctions)
45H Duplicate File Handle
46H Force Duplicate File Handle
47H Get Current Directory
48H Allocate Memory Block
49H Free Memory Block
4AH Resize Memory Block
4BH Load and Execute Program (EXEC)
4CH Terminate Process with Return Code
4DH Get Return Code of Child Process
4EH Find First File
4FH Find Next File
54H Get Verify Flag
56H Rename File
57H Get/Set Date/Time of File
59H Get Extended Error Information
5AH Create Temporary File
5BH Create New File
5CH Lock/Unlock File Region
FCBs
FCBs are not supported in MS OS/2 protected mode. Use handle-based
calls instead.
Interrupt calls
MS-DOS versions 2 and 3 use an interrupt-based interface; MS OS/2
protected mode uses a procedure-call interface. Write your code to
accommodate this difference by encapsulating the interrupt-based
interfaces into individual subroutines that can then easily be
modified to use the MS OS/2 procedure-call interface.
System call register usage
The MS OS/2 procedure-call interface preserves all registers except AX
and FLAGS. Write your program to assume that the contents of AX and
the contents of any register modified by MS-DOS version 2 and 3
interrupt interfaces are destroyed at each system call, regardless of
the success or failure of that call.
Flush/Commit calls
Your program should issue Flush/Commit calls where necessary--for
example, after writing out the user's work file--but no more than
necessary. Because MS OS/2 is multitasking, the floppy disk that
contains the files to be flushed may not be in the drive. In such a
case, MS OS/2 prompts the user to insert the proper floppy disk. As a
result, too frequent flushes could generate a great many Insert disk
messages and degrade the system's usability.
Seeks
Seeks to negative offsets and to devices also create compatibility
issues.
To negative offsets
Your program should not attempt to seek to a negative file location. A
negative seek offset is permissible as long as the sum of the seek
offset and the current file position is positive. MS-DOS versions 2
and 3 allow seeking to a negative offset as long as you do not attempt
to read or write the file at that offset. MS OS/2 and subsequent
systems return an error code for negative net offsets.
On devices
Your program should not issue seeks to devices (such as AUX, COM, and
so on). Doing so produces an error under MS OS/2.
Error codes
Because future releases of the operating system may return new error
codes to system calls, you should write code that is open-ended about
error codes--that is, write your program to deal with error codes
beyond those currently defined. You can generally do this by including
special handling for any codes that require special treatment, such as
"File not found," and by taking a generic course of action for all
other errors. The MS OS/2 protected mode API and the Family API have
an interface that contains a message describing the error; this
message can be displayed to the user. The interface also returns error
classification information and a recommended action.
Multitasking concerns
Multitasking is a feature of MS OS/2 and will be a feature of all
future versions of MS-DOS. The following guidelines apply to all
programs, even to those written for MS-DOS version 3, because they may
run in compatibility mode under MS OS/2.
Disabling interrupts
Do not disable interrupts, typically with the CLI instruction. The
consequences of doing so depend on the environment.
In real mode programs under MS OS/2, disabling interrupts works
normally but has a negative impact on the system's ability to maintain
proper system throughput. Communications programs or networking
applications might lose data. In a future version of real mode MS
OS/2-80386, the operating system will disregard attempts to disable
interrupts.
Protected mode programs under MS OS/2 can disable interrupts only in
special Ring 2 segments. Disabling interrupts for longer than 100
microseconds might cause communications programs or networking
applications to lose data or break connection. A future 80386-specific
version of MS OS/2 will ignore attempts to disable interrupts in
protected mode programs.
Measuring system resources
Do not attempt to measure system resources by exhausting them, and do
not assume that because a resource is available at one time it will be
available later. Remember: System resources are being shared with
other programs.
For example, it is common for an MS-DOS version 3 application to
request 1 MB of memory. The system cannot fulfill this request, so it
returns the largest amount of memory available. The application then
requests that amount of memory. Typically, applications do not even
check for an error code from the second request. They routinely
request all available memory because their creators knew that no other
application could be in the system at the same time. This practice
will work in real mode MS OS/2, although it is inefficient because MS
OS/2 must allocate memory to a program that has no effective use for
it. However, this practice will not work under MS OS/2 protected mode
or under the Family API.
Another typical resource-exhaustion technique is opening files until
an open is refused and then closing unneeded file handles. All
applications, even those that run only in an MS OS/2 real mode
environment, must use only the resources they need and not waste
system resources; in a multitasking environment, other programs in the
system usually need those resources.
Sharing rules
Because multiple programs can run under MS OS/2 simultaneously and
because the system can be networked, conflicts can occur when two
programs try to access the same file. MS OS/2 handles this situation
with special file-sharing support. Although programs ignorant of file-
sharing rules can run in real mode, you should explicitly specify
file-sharing rules in your program. This will reduce the number of
file-access conflicts the user will encounter.
Miscellaneous guidelines
Do not use undocumented features of MS-DOS or undocumented fields such
as those in the Find First File buffer. Also, do not modify or store
your own values in such areas.
Maintain at least 2048 free bytes on the stack at all times. Future
releases of MS-DOS may require extra stack space at system call and at
interrupt time.
Print using conventional handle writes to the LPT device(s). For
example:
fd = open("LPT1");
write(fd, data, datalen);
Do not use Interrupt 17H (the IBM ROM BIOS printer services), writes
to the stdprn handle (handle 3), or special-purpose Interrupt 21H
functions such as 05H (Printer Output). These methods are not
supported under MS OS/2 protected mode or in the Family API.
Do not use the MS-DOS standard handles stdaux and stdprn (handles 3
and 4); these handles are not supported in MS OS/2 protected mode. Use
only stdin (handle 0), stdout (handle 1), and stderr (handle 2). Do
use these latter handles where appropriate and avoid opening the CON
device directly. Avoid Interrupt 21H Functions 03H (Auxiliary Input)
and 04H (Auxiliary Output), which are polling operations on stdaux.
Summary
A tenet of MS OS/2 design was flexibility: Each component was
constructed in anticipation of massive changes in a future release and
with an eye toward existing versions of MS-DOS. Writing applications
that are upwardly and backwardly compatible in such an environment is
essential--and challenging. Following the guidelines in this article
will ensure that your programs function appropriately in the
MS-DOS/OS/2 operating system family.
Gordon Letwin
Article 17: Windows
Microsoft Windows is an operating environment that runs under MS-DOS
versions 2.0 and later. The current version of Windows, version 2.0,
requires either a fixed disk or two double-sided floppy-disk drives,
at least 320 KB of memory, and a video display board and monitor
capable of graphics and a screen resolution of at least 640
(horizontal) by 200 (vertical) pixels. A fixed disk and 640 KB of
memory provide the best environment for running Windows; a mouse or
other pointing device is optional but recommended.
For the user, Windows provides a multitasking, graphics-based
windowing environment for running programs. In this environment, users
can easily switch among several programs and transfer data between
them. Because programs specially designed to run under Windows usually
have a consistent user interface, the time spent learning a new
program is greatly diminished. Furthermore, the user can carry out
command functions using only the keyboard, only the mouse, or some
combination of the two. In some cases, Windows (and Windows
applications) provides several different ways to execute the same
command.
For the program developer, Windows provides a wealth of high-level
routines that make it easy to incorporate menus, scroll bars, and
dialog boxes (which contain controls, such as push buttons and list
boxes) into programs. Windows' graphics interface is device
independent, so programs developed for Windows work with every video
display adapter and printer that has a Windows driver (usually
supplied by the hardware manufacturer). Windows also includes features
that facilitate the translation of programs into foreign languages for
international markets.
When Windows is running, it shares responsibility for managing system
resources with MS-DOS. Thus, programs that run under Windows continue
to use MS-DOS function calls for all file input and output and for
executing other programs, but they do not use MS-DOS for display or
printer output, keyboard or mouse input, or memory management.
Instead, they use functions provided by Windows.
Program Categories
Programs that run under Windows can be divided into three categories:
1. Programs specially designed for the Windows environment. Examples
of such programs include Clock and Calculator, which come with
Windows. Microsoft Excel is also specially designed for Windows.
Other programs of this type (such as Aldus's Pagemaker) are
available from software vendors other than Microsoft. Programs in
this category cannot run under MS-DOS without Windows.
2. Programs designed to run under MS-DOS but that can usually be run
in a window along with programs designed specially for Windows.
These programs do not require large amounts of memory, do
not write directly to the display, do not use graphics, and do not
alter the operation of the keyboard interrupt. They cannot use the
mouse, the Windows application-program interface (such as menus and
dialog boxes), or the graphics services that Windows provides.
MS-DOS utilities, such as EDLIN and CHKDSK, are examples of
programs in this category.
3. Programs designed to run under MS-DOS but that require large
amounts of memory, write directly to the display, use graphics, or
alter the operation of the keyboard interrupt. When Windows runs
such a program, it must suspend operation of all other programs
running in Windows and allow the program to use the full screen. In
some cases, Windows cannot switch back to its normal display until
the program terminates. Microsoft Word and Lotus 1-2-3 are examples
of programs in this category.
The programs in categories 2 and 3 are sometimes called standard
applications. To run one of these programs in Windows, the user must
create a PIF file (Program Information File) that describes how much
memory the program requires and how it uses the computer's hardware.
Although the ability to run existing MS-DOS programs under Windows
benefits the user, the primary purpose of Windows is to provide an
environment for specially designed programs that take full advantage
of the Windows interface. This discussion therefore concentrates
almost exclusively on programs written for the Windows 2.0
environment.
The Windows Display
Figure 17-1 shows a typical Windows display running several programs
that are included with the retail version of Windows 2.0.
The display is organized as a desktop, with each program occupying one
or more rectangular windows that, unlike the tiled (juxtaposed)
windows typical of earlier versions, can be overlapped. Only one
program is active at any time--usually the program that is currently
receiving keyboard input. Windows displays the currently active
program on top of (overlying) the others. Programs such as CLOCK and
TERMINAL that are not active continue to run normally, but do not
receive keyboard input.
The user can make another program active by pressing and releasing
(clicking) the mouse button when the mouse cursor is positioned in the
new program's window or by pressing either the Alt-Tab or Alt-Esc key
combination. Windows then brings the new active program to the top.
Most Windows programs allow their windows to be moved to another part
of the display or to be resized to occupy smaller or larger areas.
Most of these programs can also be maximized to fill the entire screen
or minimized--generally as a small icon displayed at the bottom of the
screen--to occupy a small amount of display space.
╔══════════════════════════════════════════╗
║ ║
║ Figure 17-1 is found on page 501 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 17-1. A typical Windows display.
Parts of the window
Figure 17-2 shows the Windows NOTEPAD program, with the different
parts of the window identified. NOTEPAD is a small ASCII text editor
limited to files of 16 KB. The various parts of the NOTEPAD window
(similar to all Windows programs) are described in this section.
╔══════════════════════════════════════════╗
║ ║
║ Figure 17-2 is found on page 502 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 17-2. The Windows NOTEPAD program, with different parts of the
display labeled.
Title bar (or caption bar). The title bar identifies the program and,
if applicable, the data file currently loaded into the program. For
example, the NOTEPAD window shown in Figure 17-2 has the file WIN.INI
loaded into memory. Windows uses different title-bar colors to distin-
guish the active window from inactive windows. The user can move a
window to another part of the display by pressing the mouse button
when the mouse pointer is positioned anywhere on the title bar and
dragging (moving) the mouse while the button is pressed.
System-menu icon. When the user clicks a system-menu icon with the
mouse (or presses Alt-Spacebar), Windows displays a system menu like
that shown in Figure 17-3. (Most Windows programs have identical
system menus.) The user selects a menu item in one of several ways:
clicking on the item; moving the highlight bar to the item with the
cursor-movement keys and then pressing Enter; or pressing the letter
that is underlined in the menu item (for example, n for
Minimimize).
The keyboard combinations (Alt plus function key) at the right of the
system menu are keyboard accelerators. Using a keyboard accelerator,
the user can select system-menu options without first displaying the
system menu.
The six options on the standard system menu are
■ Restore: Return the window to its previous position and size after
it has been minimized or maximized.
■ Move: Allow the window to be moved with the cursor-movement keys.
■ Size: Allow the window to be resized with the cursor-movement keys.
■ Minimize: Display the window in its iconic form.
■ Maximize: Allow the window to occupy the full screen.
■ Close: End the program.
Windows displays an option on the system menu in grayed text to
indicate that the option is not currently valid. In the system menu
shown in Figure 17-3, for example, the Restore option is grayed
because the window is not in a minimized or maximized form.
╔══════════════════════════════════════════╗
║ ║
║ Figure 17-3 is found on page 502 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 17-3. A system menu, displayed either when the user clicks the
system-menu icon (top left corner) or presses Alt-Spacebar.
Minimize icon. When the user clicks on the minimize icon with the
mouse, Windows displays the program in its iconic form.
Maximize icon. Clicking on the maximize icon expands the window to
fill the full screen. Windows then replaces the maximize icon with a
restore icon (shown in Figure 17-4). Clicking on the restore icon
restores the window to its previous size and position.
╔══════════════════════════════════════════╗
║ ║
║ Figure 17-4 is found on page 503 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 17-4. The restore icon, which replaces the maximize icon when a
window is expanded to fill the entire screen.
Programs that use a window of a fixed size (such as the CALC.EXE
calculator program included with Windows) do not have a maximize icon.
Menu bar. The menu bar, sometimes called the program's main or top-
level menu, displays keywords for several sets of commands that differ
from program to program.
When the user clicks on a main-menu item with the mouse or presses the
Alt key and the underlined letter in the menu text, Windows displays a
pop-up menu for that item. The pop-up menu for NOTEPAD's keyword File
is shown in Figure 17-5. Items are selected from a pop-up menu in the
same way they are selected from the system menu.
╔══════════════════════════════════════════╗
║ ║
║ Figure 17-5 is found on page 503 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 17-5. The NOTEPAD program's pop-up file menu.
A Windows program can display options on the menu in grayed text to
indicate that they are not currently valid. The program can also
display checkmarks to the left of pop-up menu items to indicate which
of several options have been selected by the user.
In addition, items on a pop-up menu can be followed by an ellipsis
(...) to indicate that selecting the item invokes a dialog box that
prompts the user for additional information-more than can be provided
by the menu.
Client area. The client area of the window is where the program
displays data. In the case of the NOTEPAD program shown in Figure
17-2, the client area displays the file currently being edited. A
program's handling of keyboard and mouse input within the client area
depends on the type of work it does.
Scroll bars. Programs that cannot display all the data in a file
within the client area of the window often have a horizontal scroll
bar across the bottom and a vertical scroll bar down the right edge.
Both types of scroll bars have a small, boxed arrow at each end to
indicate the direction in which to scroll. In the NOTEPAD window in
Figure 17-2, for example, clicking on the up arrow of the vertical
scroll bar moves the data within the window down one line. Clicking
on the shaded part of the vertical scroll bar above the thumb (the
box near the middle moves the data within the client area of the
window down one screen; clicking below the thumb moves the data up one
screen. The user can also drag the thumb with the mouse to move to
a relative position within the file.
Windows programs often include a keyboard interface (generally relying
on the cursor-movement keys) to duplicate the mouse-based scroll-bar
commands.
Window border. The window border is a thick frame surrounding the
entire window. It is segmented into eight sections that represent the
four sides and four corners of the window. The user can change the
size of a window by dragging the window border with the mouse.
Dragging a corner section moves two adjacent sides of the border.
When a program is maximized to fill the full screen, Windows does not
draw the window border. Programs that use a window of a fixed size do
not have a window border either.
Dialog boxes
When a pop-up menu is not adequate for all the command options a
program requires, the program can display a dialog box. A dialog box
is a pop-up window that contains various controls in the form of push
buttons, check boxes, radio buttons, list boxes, and text and edit
fields. Programmers can also design their own controls for use in
dialog boxes. A user fills in a dialog box and then clicks on a
button, such as OK, or presses Enter to indicate that the information
can be processed by the program.
Most Windows programs use a dialog box to open an existing data file
and load it into the program. The program displays the dialog box when
the user selects the Open option on the File pop-up menu. The sample
dialog box shown in Figure 17-6 is from the NOTEPAD program.
The list box displays a list of all valid disk drives, the
subdirectories of the current directory, and all the filenames in the
current directory, including the filename extension used by the
program. (NOTEPAD uses the extension .TXT for its data files.) The
user can scroll through this list box and change the current drive or
subdirectory or select a filename with the keyboard or the mouse. The
user can also perform these actions by typing the name directly into
the edit field.
╔══════════════════════════════════════════╗
║ ║
║ Figure 17-6 is found on page 504 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 17-6. A dialog box from the NOTEPAD program, with parts
labeled.
Clicking the Open button (or pressing Enter) indicates to NOTEPAD that
a file has been selected or that a new drive or subdirectory has been
chosen (in this case, the program displays the files on the new drive
or subdirectory). Clicking the Cancel button (or pressing Esc) tells
NOTEPAD to close the dialog box without loading a new file.
Figure 17-7 shows a different dialog box--this one from the Windows
TERMINAL communications program. The check boxes turn options on
(indicated by an X) and off. The circular radio buttons allow the user
to select from a set of mutually exclusive options.
╔══════════════════════════════════════════╗
║ ║
║ Figure 17-7 is found on page 505 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 17-7. A dialog box from the TERMINAL program, with parts
labeled.
Another, simple form of a dialog box is called a message box. This box
displays one or more lines of text, an optional icon such as an
exclamation point or an asterisk, and one or more buttons containing
the words OK, Yes, No, or Cancel. Programs sometimes use message boxes
for warnings or error messages.
The MS-DOS Executive
Within Windows, the MS-DOS Executive program (shown in Figure 17-8)
serves much the same function as the COMMAND.COM program in the MS-DOS
environment.
╔══════════════════════════════════════════╗
║ ║
║ Figure 17-8 is found on page 506 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 17-8. The MS-DOS Executive.
The top of the MS-DOS Executive client area displays all valid disk
drives. The current disk drive is highlighted. Below or to the right
of the disk drives is a display of the full path of the current
directory. Below this is an alphabetic listing of all subdirectories
in the current directory, followed by an alphabetic listing of all
files in the current directory. Subdirectory names are displayed in
boldface to distinguish them from filenames.
The user can change the current drive by clicking on the disk drive
with the mouse or by pressing Ctrl and the key corresponding to the
disk drive letter.
To change to one of the parent directories, the user double-clicks
(clicks the mouse button twice in succession) on the part of the text
string corresponding to the directory name. Pressing the Backspace key
moves up one directory level toward the root directory. The user can
also change the current directory to a child subdirectory by double-
clicking on the subdirectory name in the list or by pressing the Enter
key when the cursor highlight is on the subdirectory name. In
addition, the menu also contains an option for changing the current
directory.
The user can run a program by double-clicking on the program filename,
by pressing the Enter key when the highlight is on the program name,
or by selecting it from a menu.
Other menu options allow the user to display the file and subdirectory
lists in a variety of ways. A long format includes the same
information displayed by the MS-DOS DIR command, or the user can
choose to display a select group of files. Menu options also enable
the user to specify whether the files should be listed in alphabetic
order by filename, by filename extension, or by date or size.
The remaining options on the MS-DOS Executive menu allow the user to
run programs; copy, rename, and delete files; format a floppy disk;
change a volume name; make a system disk; create a subdirectory; and
print a text file.
Other Windows Programs
Windows 2.0 also includes a number of application and utility
programs. The application programs are CALC (a calculator), CALENDAR,
CARDFILE (a database arranged as a series of index cards), CLOCK,
NOTEPAD, PAINT (a drawing and painting program), REVERSI (a game),
TERMINAL, and WRITE (a word processor).
The utility programs include:
CLIPBRD. This program displays the current contents of the Clipboard,
which is a storage facility that allows users to transfer data from
one program to another.
CONTROL. The Control Panel utility allows the user to add or delete
font files and printer drivers and to change the following: current
printer, printer output port, communications parameters, date and
time, cursor blink rate, screen colors, border width, mouse double-
click time and options, and country-specific information, such
as time and date formats. The Control Panel stores much of this
information in the file named WIN.INI (Windows Initialization), so
the information is available to other Windows programs.
PIFEDIT. The PIF editor allows the user to create or modify the PIFs
that contain information about standard applications that have not
been specially designed to run under Windows. This information allows
Windows to adjust the environment in which the program runs.
SPOOLER. Windows uses the print-spooler utility to print files without
suspending the operation of other programs. Most printer-directed
output from Windows programs goes to the print spooler, which then
prints the files while other programs run. SPOOLER enables the user to
change the priority of print jobs or to cancel them.
The Structure of Windows
When programs run under MS-DOS, they make requests of the operating
system through MS-DOS software interrupts (such as Interrupt 21H),
through BIOS software interrupts, or by directly accessing the machine
hardware.
When programs run under Windows, they use MS-DOS function calls only
for file input and output and (more rarely) for executing other
programs. Windows programs do not use MS-DOS function calls for memory
management, keyboard input, display or printer output, or RS232
communications. Nor do Windows programs use BIOS routines or direct
access to the hardware.
Instead, Windows provides application programs with access to more
than 450 functions that allow programs to create and manipulate
windows on the display; use menus, dialog boxes, and scroll bars;
display text and graphics within the client area of a window; use the
printer and RS232 communications port; and allocate memory.
The Windows modules
The functions provided by Windows are largely handled by three main
modules named KERNEL, GDI, and USER. The KERNEL module is responsible
for scheduling and multitasking, and it provides functions for memory
management and some file I/O. The GDI module provides Windows'
Graphics Device Interface functions, and the USER module does
everything else.
The USER and GDI modules, in turn, call functions in various driver
modules that are also included with Windows. Drivers control the
display, printer, keyboard, mouse, sound, RS232 port, and timer. In
most cases, these driver modules access the hardware of the computer
directly. Windows includes different driver files for various hardware
configurations. Hardware manufacturers can also develop Windows
drivers specifically for their products.
A block diagram showing the relationships of an application program,
the KERNEL, USER, and GDI modules, and the driver modules is shown in
Figure 17-9. The figure shows each of these modules as a separate
file--KERNEL, USER, and GDI have the extension .EXE; the driver files
have the extension .DRV. Some program developers install Windows with
these modules in separate files, as in Figure 17-9, but most users
install Windows by running the SETUP program included with Windows.
SETUP combines most of these modules into two larger files called
WIN200.BIN and WIN200.OVL. Printer drivers are a little different from
the other driver files, however, because the Windows SETUP program
does not include them in WIN200.BIN and WIN200.OVL. The name of the
driver file identifies the printer. For example, IBMGRX.DRV is a
printer driver file for the IBM Personal Computer Graphics Printer.
┌────────────┐ ┌──────────────┐
┌──│ ├──│ DISPLAY.DRV ├──Display
│ │ │ └──────────────┘
│ │ GDI.EXE │ ┌──────────────┐
│ ┌│ ├──│Printer driver├──Printer
│ │ └────────────┘ └──────────────┘
│ └────────────────┐
│ ┌────────────┐ │
│ │ ├─┘
│ │ │ ┌──────────────┐
│ │ ├──│ KEYBOARD.DRV ├──Keyboard
│ │ │ └──────────────┘
│ │ │ ┌──────────────┐
│ │ ├──│ MOUSE.DRV ├──Mouse
│ │ │ └──────────────┘
│ │ │ ┌──────────────┐
│ │ ├──│ SOUND.DRV ├──Sound hardware
┌────────────┐ │ │ │ └──────────────┘
│ Windows ├─┘ │ │ ┌──────────────┐
│application ├────│ USER.EXE ├──│ COMM.DRV ├──RS-232 hardware
│ program ├─┐ │ │ └──────────────┘
└────────────┘ │ │ │ ┌──────────────┐
│ │ ├──│ SYSTEM.DRV ├──Timer hardware
│ │ │ └──────────────┘
│ │ │
│ │ │
│ │ │
│ │ ├─┐
│ └────────────┘ │
│┌─────────────────┘
││ ┌────────────┐
│└─│ ├──MS-DOS file I/O
│ │ KERNEL.EXE │
└──│ ├──Memory management
└────────────┘
Figure 17-9. A simplified block diagram showing the relationships of
an application program, Windows modules (GDI, USER, and KERNEL),
driver modules, and system hardware.
The diagram in Figure 17-9 is somewhat simplified. In reality, a
Windows application program can also make direct calls to the
KEYBOARD.DRV and SOUND.DRV modules, and USER.EXE calls the DISPLAY.DRV
and printer driver modules directly. The GDI.EXE module and driver
modules can also call routines in KERNEL.EXE, and drivers sometimes
call routines in SYSTEM.DRV.
Also, Figure 17-9 omits the various font files provided with Windows,
the WIN.INI file that contains Windows initialization information and
user preferences, and the files WINOLDAP.MOD and WINOLDAP.GRB, which
Windows uses to run standard MS-DOS applications.
Libraries and programs
The USER.EXE, GDI.EXE, and KERNEL.EXE files, all driver files with the
extension .DRV, and all font files with the extension .FON are called
Windows libraries or, sometimes, dynamic link libraries to distinguish
them from Windows programs. Programs and libraries both use a file
format called the New Executable format.
From the user's perspective, a Windows program and a Windows library
are very different. The user cannot run a Windows library directly:
Windows loads a part of a library into memory only when a program
needs to use a function that the library provides.
The user can also run multiple instances of the same Windows program.
Windows uses the same code segments for the different instances but
creates a unique data segment for each. Windows never runs multiple
instances of a Windows library.
From the programmer's perspective, a Windows program is a task that
creates and manages windows on the display. Libraries are modules that
assist the task. A programmer can write additional library modules,
which one or more programs can use. For the developer, one important
distinction between programs and libraries is that a Windows library
does not have its own stack; instead, the library uses the stack of
the program that calls the routine in the library.
The New Executable format used for both programs and libraries gives
Windows much more information about the module than is provided by the
current MS-DOS .EXE format. In particular, the module contains
information that allows Windows to make links between program modules
and library modules when a program is run.
When a module (such as a library) contains functions that can be
called from another module (such as a program), the functions are said
to be exported from the module that contains them. Each exported
function in a module is identified either by a name (generally the
name of the function) or by an ordinal (positive) number. A list of
all exported functions in a module is included in the New Executable
format header section of the module.
Conversely, when a module (such as a program) contains code that calls
a function in another module (such as a library), the function is said
to be imported to the module that makes the call. This call appears in
the .EXE file as an unresolved reference to an external function. The
New Executable format identifies the module and the function name or
ordinal number that the call references.
When Windows loads a program or a library into memory, it must resolve
all calls the module makes to functions in other modules. Windows does
this by inserting the addresses of the functions into the code--a
process called dynamic linking.
For example, many Windows programs use the function TextOut to display
text in the client area. In the code segment of the program's .EXE
file, a call to TextOut appears as an unresolved far (intersegment)
call. The code segment's relocation table shows that this call is to
an imported function in the GDI module identified by the ordinal
number 33. The header section of the GDI module lists TextOut as an
exported function with the ordinal number 33. When Windows loads the
program, it resolves all references to TextOut by inserting the
address of the function into the program's code segment in each place
where TextOut is called.
Although Windows programs reference many functions that are exported
from the standard Windows libraries, Windows programs also often
include at least one exported function, called a window function.
While the program is running, Windows calls this function to pass
messages to the program's window. See The Structure of a Windows
Program, below.
Memory Management
Windows' memory management is based on the segmented-memory
architecture of the Intel 8086 family of microprocessors. The memory
space controlled by Windows is divided into segments of various
lengths. Windows uses separate segments for nearly everything kept in
memory--such as the code and data segments of programs and libraries--
and for resources, such as fonts and bitmaps.
Windows programs and libraries contain one or more code segments,
which are usually both movable and discardable. Windows can move a
code segment in memory in order to consolidate free memory space. It
can also discard a code segment from memory and later reload the code
segment from the program's or library's .EXE file when it is needed
again. This capability is called demand loading.
Windows programs usually contain only one data segment; Windows
libraries are limited to one data segment. In most cases, Windows can
move data segments in memory. However, it cannot usually discard data
segments, because they can contain data that changes after the program
begins executing. When a user runs multiple copies of a program, the
different instances share the same code segments but have separate
data segments.
The use of movable and discardable segments allows Windows to run
several large programs in a memory space that might be inadequate for
even one of the programs if the entire program were kept in memory, as
is typical under MS-DOS without Windows. The ability of Windows to use
memory in this way is called memory overcommitment.
The moving and discarding of code segments requires Windows to make
special provisions so that intersegment calls continue to reference
the correct address when a code segment is moved. These provisions are
another part of dynamic linking. When Windows resolves a far call from
one code segment to a function in another code segment that is movable
and discardable, the call actually references a fixed area of memory.
This fixed area of memory contains a small section of code called a
thunk. If the code segment containing the function is currently in
memory, the thunk simply jumps to the function. If the code segment
with the function is not currently in memory, the thunk calls a loader
that loads the segment into memory. This process is called dynamic
loading. When Windows moves or discards a code segment, it must alter
the thunks appropriately.
Windows and Windows programs generally reference data structures
stored in Windows' memory space by using 16-bit unsigned integers
known as handles. The data structure that a handle references can be
movable and discardable, so when Windows or the Windows program needs
to access the data directly, it must lock the handle to cause the data
to become fixed in memory. The function that locks the segment returns
a pointer to the program.
During the time the handle is locked, Windows cannot move or discard
the data. The data can then be referenced directly with the pointer.
When Windows (or the Windows program) finishes using the data, it
unlocks the segment so that it can be moved (or in some cases
discarded) to free up memory space if necessary.
Programmers can choose to allocate nonmovable data segments, but the
practice is not recommended, because Windows cannot relocate the
segments to make room for segments required by other programs.
The Structure of a Windows Program
During development, a Windows program includes several components that
are combined later into a single executable file with the extension
.EXE for execution under Windows. Although the Windows executable file
has the same .EXE filename extension as MS-DOS executable files, the
format is different. Among other things, the New Executable format
includes Windows-specific information required for dynamic linking and
the discarding and reloading of the program's code segments.
Programmers generally use C, Pascal, or assembly language to create
applications specially designed to run under Windows. Also required
are several header files and development tools, which are included in
the Microsoft Windows Software Development Kit.
The Microsoft Windows Software Development Kit
The Windows Software Development Kit contains reference material, a
special linker (LINK4), the Windows Resource Compiler (RC), special
versions of the SYMDEB and CodeView debuggers, header files, and
several programs that aid development and testing. These programs
include
■ DIALOG: Used for creating dialog boxes.
■ ICONEDIT: Used for creating a program's icon, customized cursors,
and bitmap images.
■ FONTEDIT: Used for creating customized fonts derived from an
existing font file with the extension .FNT.
■ HEAPWALK: Used for displaying the organization of code and data
segments in Windows' memory space and for testing programs under
low memory conditions.
■ SHAKER: Used for randomly allocating memory to force segment
movement and discarding. SHAKER tests a program's response to
movement in memory and is useful for exposing program bugs
involving pointers to unlocked segments.
The Windows Software Development Kit also provides several include and
header files that contain declarations of all Windows functions,
definitions of many macro identifiers that the programmer can use, and
structure definitions. Import libraries included in the kit allow
LINK4 to resolve calls to Windows functions and to prepare the
program's .EXE file for dynamic linking.
Work with the Windows Software Development Kit requires one of the
following compilers or assemblers:
■ Microsoft C Compiler version 4.0 or later
■ Microsoft Pascal Compiler version 3.31 or later
■ Microsoft Macro Assembler version 4.0 or later
Other software manufacturers also provide compilers that are suitable
for compiling Windows programs.
Components of a Windows program
The discussion in this section is illustrated by a program called
SAMPLE, which displays the word Windows in its client area. In
response to a menu selection, the program displays this text in any of
the three vector fonts--Script, Modern, and Roman--that are included
with Windows. Sometimes also called stroke or graphics fonts, these
vector fonts are defined by a series of line segments, rather than by
the pixel patterns that make up the more common raster fonts. The
SAMPLE program picks a font size that fits the client area.
Figure 17-10 shows several instances of this program running under
Windows.
╔══════════════════════════════════════════╗
║ ║
║ Figure 17-10 is found on page 512 ║
║ in the printed version of the book. ║
║ ║
╚══════════════════════════════════════════╝
Figure 17-10. A display produced by the example program SAMPLE.
Five separate files go into the making of this program:
1. Source-code file: This is the main part of the program, generally
written in C, Pascal, or assembly language. The SAMPLE program was
written in C, which is the most popular language for Windows
programs because of its flexibility in using pointers and
structures. The SAMPLE.C source-code file is shown in Figure 17-11.
──────────────────────────────────────────────────────────────────────
Figure 17-11. The SAMPLE.C source code.
──────────────────────────────────────────────────────────────────────
2. Resource script: The resource script is an ASCII file that
generally has the extension .RC. This file contains definitions of
menus, dialog boxes, string tables, and keyboard accelerators used
by the program. The resource script can also reference other files
that contain icons, cursors, bitmaps, and fonts in binary form, as
well as other read-only data defined by the programmer. When a
program is running, Windows loads resources into memory only when
they are needed and in most cases can discard them if additional
memory space is required.
SAMPLE.RC, the resource script for the SAMPLE program, is shown in
Figure 17-12; it contains only the definition of the menu used in
the program.
#include "sample.h"
Sample MENU
BEGIN
POPUP "&Typeface"
BEGIN
MENUITEM "&Script", IDM_SCRIPT, CHECKED
MENUITEM "&Modern", IDM_MODERN
MENUITEM "&Roman", IDM_ROMAN
END
END
Figure 17-12. The resource script for the SAMPLE program.
3. Header (or include) file: This file, with the extension .H, can
contain definitions of constants or macros, as is customary in C
programming. For Windows programs, the header file also reconciles
constants used in both the resource script and the program source-
code file. For example, in the SAMPLE.RC resource script, each item
in the pop-up menu (Script, Modern, and Roman) also includes an
identifier--IDM_SCRIPT, IDM_MODERN, and IDM_ROMAN, respectively.
These identifiers are merely numbers that Windows uses to notify
the program of the user's selection of a menu item. The same names
are used to identify the menu selection in the C source-code file.
And, because both the resource compiler and the source-code
compiler must have access to these identifiers, the header file is
included in both the resource script and the source-code file.
The header file for the SAMPLE program, SAMPLE.H, is shown in
Figure 17-13.
#define IDM_SCRIPT 1
#define IDM_MODERN 2
#define IDM_ROMAN 3
Figure 17-13. The SAMPLE.H header file.
4. Module-definition file: The module-definition file generally has a
.DEF extension. The Windows linker uses this file in creating the
executable .EXE file. The module-definition file specifies various
attributes of the program's code and data segments, and it lists
all imported and exported functions in the source-code file. In
large programs that are divided into multiple code segments, the
module-definition file allows the programmer to specify different
attributes for each code segment.
The module-definition file for the SAMPLE program is named
SAMPLE.DEF and is shown in Figure 17-14.
NAME SAMPLE
DESCRIPTION 'Demonstration Windows Program'
STUB 'WINSTUB.EXE'
CODE MOVABLE
DATA MOVABLE MULTIPLE
HEAPSIZE 1024
STACKSIZE 4096
EXPORTS WndProc
Figure 17-14. The SAMPLE.DEF module-definition file.
5. Make file: To facilitate construction of the executable file from
these different components, Windows programmers often use the MAKE
program to compile only those files that have changed since the
last time the program was linked. To do this, the programmer first
creates an ASCII text file called a make file. By convention, the
make file has no extension.
The make file for the SAMPLE program is named SAMPLE and is shown
in Figure 17-15. The programmer can create the SAMPLE.EXE
executable file by executing
C>MAKE SAMPLE <Enter>
A make file often contains several sections, each beginning with a
target filename, followed by a colon and one or more dependent
filenames, such as
sample.obj : sample.c sample.h
If either or both the SAMPLE.C and SAMPLE.H files have a later
creation time than SAMPLE.OBJ, then MAKE runs the program or pro-
grams listed immediately below. In the case of the SAMPLE make
file, the program is the C compiler, and it compiles the SAMPLE.C
source code:
cl -c -Gsw -W2 -Zdp sample.c
Thus, if the programmer changes only one of the several files used
in the development of SAMPLE, then running MAKE ensures that the
executable file is brought up to date, while carrying out only the
required steps.
sample.obj : sample.c sample.h
cl -c -Gsw -W2 -Zdp sample.c
sample.res : sample.rc sample.h
rc -r sample.rc
sample.exe : sample.obj sample.def sample.res
link4 sample, /align:16, /map /line, slibw, sample
rc sample.res
mapsym sample
Figure 17-15. The make file for the SAMPLE program.
Construction of a Windows program
The make file shows the steps that create a program's .EXE file from
the various components:
1. Compiling the source-code file:
cl -c -Gsw -W2 -Zdp sample.c
This step uses the CL.EXE C compiler to create a .OBJ object-module
file. The command line switches are
- -c: Compiles the program but does not link it. Windows programs
must be linked with Windows' LINK4 linker, rather than with the
LINK program the C compiler would normally invoke.
- -Gsw: Includes two switches, -Gs and -Gw. The -Gs switch removes
stack checks from the program. The -Gw switch inserts special
prologue and epilogue code in all far functions defined in the
program. This special code is required for Windows' memory
management.
- -W2: Compiles with warning level 2. This is the highest warning
level, and it causes the compiler to display messages for
conditions that may be acceptable in normal C programs but that
can cause serious errors in a Windows program.
- -Zdp: Includes two switches, -Zd and -Zp. The -Zd switch includes
line numbers in the .OBJ file--helpful for debugging at the
source-code level. The -Zp switch packs structures on byte
boundaries. The -Zp switch is required, because data structures
used within Windows are in a packed format.
2. Compiling the resource script:
rc -r sample.rc
This step runs the resource compiler and converts the ASCII .RC
resource script into a binary .RES form. The -r switch indicates
that the resource script should be compiled but the resources
should not yet be added to the program's .EXE file.
3. Linking the program:
link4 sample, /align:16, /map /line, slibw, sample
This step uses the special Windows linker, LINK4. The first
parameter listed is the name of the .OBJ file. The /align:16 switch
instructs LINK4 to align segments in the .EXE file on 16-byte
boundaries. The /map and /line switches cause LINK4 to create a
.MAP file that contains program line numbers--again, useful for
debugging source code. Next, slibw is a reference to the SLIBW.LIB
file, which is an import library that contains module names and
ordinal numbers for all Windows functions. The last parameter,
sample, is the program's module-definition file, SAMPLE.DEF.
4. Adding the resources to the .EXE file:
rc sample.res
This step runs the resource compiler a second time, using the
compiled resource file, SAMPLE.RES. This time, the resource
compiler adds the resources to the .EXE file.
5. Creating a symbol (.SYM) file from the linker's map (.MAP) file:
mapsym sample
This step is required for symbolic debugging with SYMDEB.
Figure 17-16 shows how the various components of a Windows program fit
into the creation of a .EXE file.
┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Module │ │ Program │ │ Header or │ │ │
│definition file│ │ source code │ │ include files │ │Resource script│
│ (.DEF) │ │(.C,.PAS,.ASM) │ │ (.H or .INC) │ │ (.RC) │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ ┌─────────────┴────────────┐ │
│ ┌─────────────┐ ┌─────────────┐
│ │ C or Pascal │ │ RC.EXE │
│ │ Compiler or │ │ Resource │
│ │Macro Assembler│ │ compiler │
│ └───────┬───────┘ └───────┬───────┘
│ │ │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ │ │ │ │ │ Compiled │
│ │ Object module │ │ Libraries │ │ resources │
│ │ (.OBJ) │ │ (.LIB) │ │ (.RES) │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘
└─────────────┐ │ ┌─────────────┘ │
┌────────────┐ │
│ │ │
│ LINK4.EXE │ │
│ Window linker │ │
└───┬───┬───────┘ │
┌─────────────┘ │ │
┌──────────────┐ ┌──────────────┐ │
│ │ │ Executable │ │
│ Map file │ │ without re- │ │
│ (.MAP) │ │sources (.EXE) │ │
└───────┬───────┘ └───────┬───────┘ │
│ │ ┌──────────────────────────────┘
┌──────────────┐ ┌─────────────┐
│ MAPSYM.EXE │ │ RC.EXE │
│Makes map file │ │ Resource │
│ a symbol file │ │ compiler │
└───────┬───────┘ └───────┬───────┘
│ │
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ Symbol file │ │ Executable │
│ (.SYM) │ │ (.EXE) │
└───────────────┘ └───────────────┘
Figure 17-16. A block diagram showing the creation of a Windows
.EXE file.
Program initialization
The SAMPLE.C program shown in Figure 17-11 contains some code that
appears in almost every Windows program. The statement
#include <windows.h>
appears at the top of every Windows source-code file written in C. The
WINDOWS.H file, provided with the Microsoft Windows Software
Development Kit, contains templates for all Windows functions,
structure definitions, and #define statements for many mnemonic
identifiers.
Some of the variable names in SAMPLE.C may look unusual to C
programmers because they begin with a prefix notation that denotes the
data type of the variable. Windows programmers are encouraged to use
this type of notation. Some of the more common prefixes are
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Prefix Data Type
──────────────────────────────────────────────────────────────────
i or n Integer (16-bit signed integer)
w Word (16-bit unsigned integer)
l Long (32-bit signed integer)
dw Doubleword (32-bit unsigned integer)
h Handle (16-bit unsigned integer)
sz Null-terminated string
lpsz Long pointer to null-terminated string
lpfn Long pointer to a function
The program's entry point (following some startup code) is the WinMain
function, which is passed the following parameters: a handle to the
current instance of the program (hInstance), a handle to the most
recent previous instance of the program (hPrevInstance), a long
pointer to the program's command line (lpszCmdLine), and a number
(nCmdShow) that indicates whether the program should initially be
displayed as a normally sized window or as an icon.
The first job SAMPLE performs in the WinMain function is to register
a window class--a structure that describes characteristics of the
windows that will be created in the class. These characteristics
include background color, the type of cursor to be displayed in the
window, the window's initial menu and icon, and the window function
(the structure member called lpfnWndProc).
Multiple instances of a program can share the same window class, so
SAMPLE registers the window class only for the first instance of the
program:
if (!hPrevInstance)
{
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = NULL ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = szAppName ;
wndclass.lpszClassName = szAppName ;
RegisterClass (&wndclass) ;
}
The SAMPLE program then creates a window using the CreateWindow call,
displays it to the screen by calling ShowWindow, and updates the
client area by calling UpdateWindow:
hWnd = CreateWindow (szAppName, "Demonstration Windows Program",
WS_OVERLAPPEDWINDOW,
(int) CW_USEDEFAULT,0,
(int) CW_USEDEFAULT,0,
NULL, NULL, hInstance, NULL) ;
ShowWindow (hWnd, nCmdShow) ;
UpdateWindow (hWnd) ;
The first parameter to CreateWindow is the name of the window class.
The second parameter is the actual text that appears in the window's
title bar. The third parameter is the style of the window--in this
case, the WINDOWS.H identifier WS_OVERLAPPEDWINDOW. The
WS_OVERLAPPEDWINDOW is the most common window style. The fourth
through seventh parameters specify the initial position and size of
the window. The identifier CW_USEDEFAULT tells Windows to position and
size the window according to the default rules.
After creating and displaying a Window, the SAMPLE program enters a
piece of code called the message loop:
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
This loop continues to execute until the GetMessage call returns zero.
When that happens, the program instance terminates and the memory
required for the instance is freed.
The Windows messaging system
Interactive programs written for the normal MS-DOS environment
generally obtain user input only from the keyboard, using either an
MS-DOS or a ROM BIOS software interrupt to check for keystrokes. When
the user types something, such programs act on the keystroke and then
return to wait for the next keystroke.
Programs written for Windows, however, can receive user input from a
variety of sources, including the keyboard, the mouse, the Windows
timer, menus, scroll bars, and controls, such as buttons and edit
boxes.
Moreover, a Windows program must be informed of other events occurring
within the system. For instance, the user of a Windows program might
choose to make its window smaller or larger. Windows must make the
program aware of this change so that the program can adjust its screen
output to fit the new window size. Thus, for example, if a Windows
program is minimized as an icon and the user maximizes its window to
fill the full screen, Windows must inform the program that the size of
the client area has changed and needs to be re-created.
Windows carries out this job of keeping a program informed of other
events through the use of formatted messages. In effect, Windows sends
these messages to the program. The Windows program receives and acts
upon the messages.
This messaging makes the relationship between Windows and a Windows
program much different from the relationship between MS-DOS and an
MS-DOS program. Whereas MS-DOS does not provide information until a
program requests it through an MS-DOS function call, Windows must
continually notify a program of all the events that affect its window.
Window messages can be separated into two major categories: queued and
nonqueued.
Queued messages are similar to the keyboard information an MS-DOS
program obtains from MS-DOS. When the Windows user presses a key on
the keyboard, moves the mouse, or presses one of the mouse buttons,
Windows saves information about the event (in the form of a data
structure) in the system message queue. Each message is destined for a
particular window in a particular instance of a Windows program.
Windows therefore determines which window should get the information
and then places the message in the instance's own message queue.
A Windows program retrieves information from its queue in the message
loop:
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
The msg variable is a structure. During the GetMessage call, Windows
fills in the fields of this structure with information about the
message. The fields are as follows:
■ hwnd: The handle for the window that is to receive the message.
■ iMessage: A numeric code identifying the type of message (for
example, keyboard or mouse).
■ wParam: A 16-bit value containing information specific to the
message. See The Windows Messages, below.
■ lParam: A 32-bit value containing information specific to the
message.
■ time: The time, in milliseconds, that the message was placed in the
queue. The time is a 32-bit value relative to the time at which the
current Windows session began.
■ pt.x: The horizontal coordinate of the mouse cursor at the time the
event occurred.
■ pt.y: The vertical coordinate of the mouse cursor at the time the
event occurred.
GetMessage always returns a nonzero value except when it receives a
quit message. The quit message causes the message loop to end. The
program should then terminate and return control to Windows.
Within the message loop, the TranslateMessage function translates
physical keystrokes into character-code messages. Windows places these
translated messages into the program's message queue.
The DispatchMessage function essentially makes a call to the window
function of the window specified by the hwnd field. This window
function (WndProc in SAMPLE) is indicated in the lpfnWndProc field of
the window class structure.
When DispatchMessage passes the message to the window function,
Windows uses the first four fields of the message structure as
parameters to the function. The window function can then process the
message. In SAMPLE, for instance, the four fields passed to WndProc
are hwnd (the handle to the window), iMessage (the numeric message
identifier), wParam, and lParam. Although Windows does not pass the
time and mouse-position information fields as parameters to the window
function, this information is available through the Windows functions
GetMessageTime and GetMessagePos.
A Windows program obtains only a few specific types of messages
through its message queue. These are keyboard messages, mouse
messages, timer messages, the paint message that tells the program it
must re-create the client area of its window, and the quit message
that tells the program it is being terminated.
In addition to the queued messages, however, a program's window
function also receives many nonqueued messages. Windows sends these
nonqueued messages by bypassing the message loop and calling the
program's window function directly.
Many of these nonqueued messages are derived from queued messages. For
example, when the user clicks the mouse on the menu bar, a mouse-click
message is placed in the program's message queue. The GetMessage
function retrieves the message and the DispatchMessage function sends
it to the program's window function. However, because this mouse
message affects a nonclient area of the window (an area outside the
window's client area), the window function normally does not process
it. Instead, the function passes the message back to Windows. In this
example, the message tells Windows to invoke a pop-up menu. Windows
calls up the menu and then sends the window function several nonqueued
messages to inform the program of this action.
A Windows program is thus message driven. Once a program reaches the
message loop, it acts only when the window function receives a
message. And, although a program receives many messages that affect
the window, the program usually processes only some of them, sending
the rest to Windows for normal default processing.
The Windows messages
Windows can send a window function more than 100 different messages.
The WINDOWS.H header file includes identifiers for all these messages
so that C programmers do not have to remember the message numbers.
Some of the more common messages and the meanings of the wParam and
lParam parameters are discussed here:
WM_CREATE. Windows sends a window function this nonqueued message
while processing the CreateWindow call. The lParam parameter is a
pointer to a creation structure. A window function can perform some
program initialization during the WM_CREATE message.
WM_MOVE. Windows sends a window function the nonqueued WM_MOVE message
when the window has been moved to another part of the display. The
lParamparameter gives the new coordinates of the window relative to
the upper left corner of the screen.
WM_SIZE. This nonqueued message indicates that the size of the window
has been changed. The new size is encoded in the lParam parameter.
Programs often save this window size for later use.
WM_PAINT. This queued message indicates that a region in the window's
client area needs repainting. (The message queue can contain only one
WM_PAINT message.)
WM_COMMAND. This nonqueued message signals a program that a user has
selected a menu item or has triggered a keyboard accelerator. Child-
window controls also use WM_COMMAND to send messages to the parent
window.
WM_KEYDOWN. The wParam parameter of this queued message is a virtual
key code that identifies the key being pressed. The lParam parameter
includes flags that indicate the previous key state and the number of
keypresses the message represents.
WM_KEYUP. This queued message tells a window function that a key has
been released. The wParam parameter is a virtual key code.
WM_CHAR. This queued message is generated from WM_KEYDOWN messages
during the TranslateMessage call. The wParam parameter is the ASCII
code of a keyboard key.
WM_MOUSEMOVE. Windows uses this queued message to tell a program about
mouse movement. The lParam parameter contains the coordinates of the
mouse relative to the upper left corner of the client area of the
window. The wParam parameter contains flags that indicate whether any
mouse buttons or the Shift or Ctrl keys are currently pressed.
WM_xBUTTONDOWN. This queued message tells a program that a button on
the mouse was depressed while the mouse was in the window's client
area. The x can be either L, R, or M for the left, right, or middle
mouse button. The wParam and lParam parameters are the same as for
WM_MOUSEMOVE.
WM_xBUTTONUP. This queued message tells a program that the user has
released a mouse button.
WM_xBUTTONDBLCLK. When the user double-clicks a mouse button, Windows
generates a WM_xBUTTONDOWN message for the first click and a queued
WM_xBUTTONDBLCLK message for the second click.
WM_TIMER. When a Windows program sets a timer with the SetTimer
function, Windows places a WM_TIMER message in the message queue at
periodic intervals. The wParam parameter is a timer ID. (If the
message queue already contains a WM_TIMER message, Windows does not
add another one to the queue.)
WM_VSCROLL. A Windows program that includes a vertical scroll bar in
its window receives nonqueued WM_VSCROLL messages indicating various
types of scroll-bar manipulation.
WM_HSCROLL. This nonqueued message indicates a user is manipulating a
horizontal scroll bar.
WM_CLOSE. Windows sends a window function this nonqueued message when
the user has selected Close from the window's system menu. A program
can query the user to determine whether any action, such as saving a
file to disk, is needed before the program is terminated.
WM_QUERYENDSESSION. This nonqueued message indicates that the user is
shutting down Windows by selecting Close from the MS-DOS Executive
system menu. A program can request the user to verify that the program
should be ended. If the window function returns a zero value from the
message, Windows does not end the session.
WM_DESTROY. This nonqueued message is the last message a window
function receives before the program ends. A window function can
perform some last-minute cleanup while processing WM_DESTROY.
WM_QUIT. This is a queued message that never reaches the window
function because it causes GetMessage to return a zero value that
causes the program to exit the message loop.
Message processing
Programmers can choose to process some messages and ignore others in
the window function. Messages that are ignored are generally passed on
to the function DefWindowProc for default processing within Windows.
Because Windows eventually has access to messages that a window
function does not process, it can send a program messages that might
otherwise be regarded as pertaining to system functions--for example,
mouse messages that occur in a nonclient area of the window, or system
keyboard messages that affect the menu. Unless these messages are
passed on to DefWindowProc, the menu and other system functions do not
work properly.
A program can, however, trap some of these messages to override
Windows' default processing. For example, when Windows needs to
repaint the nonclient area of a window (the title bar, system-menu
box, and scroll bars), it sends the window function a WM_NCPAINT
(nonclient paint) message. The window function normally passes
this message to DefWindowProc, which then calls routines to update the
nonclient areas of the window. The program can, however, choose to
process the WM_NCPAINT message and paint the nonclient area itself. A
program that does this can, for example, draw its own scroll bars.
The Windows messaging system also notifies a program of important
events occurring outside its window. For example, if the MS-DOS
Executive were simply to end the Windows session when the user selects
the Close option from its system menu, then applications that were
still running would not have a chance to save changed files to disk.
Instead, when the user selects Close from the last instance of the
MS-DOS Executive's system menu, the MS-DOS Executive sends a
WM_QUERYENDSESSION message to each currently running application
If any application responds by returning a zero value, the MS-DOS
Executive does not end the Windows session.
Before responding, an application can process the WM_QUERYENDSESSION
message and display a message box asking the user if a file should be
saved. The message box should include three buttons labeled Yes, No,
and Cancel. If the user answers Yes, the program can save the file and
then return a nonzero value to the WM_QUERYENDSESSION message. If the
user answers No, the program can return a nonzero value without saving
the file. But if the user answers Cancel, the program should return a
zero value so that the Windows session will not be ended. If a program
does not process the WM_QUERYENDSESSION message, DefWindowProc returns
a nonzero value.
When a user selects Close from the system menu of a particular
instance of an application, rather than from the MS-DOS Executive's
menu, Windows sends the window function a WM_CLOSE message. If the
program has an unsaved file loaded, it can query the user with a
message box--possibly the same one displayed when WM_QUERYENDSESSION
is processed. If the user responds Yes to the query, the program can
save the file and then call DestroyWindow. If the user responds No,
the program can call DestroyWindow without saving the file. If the
user responds Cancel, the window function does not call DestroyWindow
and the program will not be terminated. If a program does not process
WM_CLOSE messages, DefWindowProc calls DestroyWindow.
Finally, a window function can send messages to other window
functions, either within the same program or in other programs, with
the Windows SendMessage function. This function returns control to the
calling program after the message has been processed. A program can
also place messages in a program's message queue with the PostMessage
function. This function returns control immediately after posting the
message.
For example, when a program makes changes to the WIN.INI file (a file
containing Windows initialization information), it can notify all
currently running instances of these changes by sending them a
WM_WININICHANGE message:
SendMessage (-1, WM_WININICHANGE, 0, 0L) ;
The -1 parameter indicates that the message is to be sent to all
window functions of all currently running instances. Windows calls the
window functions with the WM_WININICHANGE message and then returns
control to the program that sent the message.
SAMPLE's message processing
The SAMPLE program shown in Figure 17-11 processes only four messages:
WM_COMMAND, WM_SIZE, WM_PAINT, and WM_DESTROY. All other messages are
passed to DefWindowProc. As is typical with most Windows programs
written in C, SAMPLE uses a switch and case construction for
processing messages.
The WM_COMMAND message signals the program that the user has selected
a new font from the menu. SAMPLE first obtains a handle to the menu
and removes the checkmark from the previously selected font:
hMenu = GetMenu (hWnd) ;
CheckMenuItem (hMenu, nCurrentFont, MF_UNCHECKED) ;
The value of wParam in the WM_COMMAND message is the menu ID of the
newly selected font. SAMPLE saves that value in a static variable
(nCurrentFont) and then places a checkmark on the new menu choice:
nCurrentFont = wParam ;
CheckMenuItem (hMenu, nCurrentFont, MF_CHECKED) ;
Because the typeface has changed, SAMPLE must repaint its display. The
program does not repaint it immediately, however. Instead, it calls
the InvalidateRect function:
InvalidateRect (hWnd, NULL, TRUE) ;
This causes a WM_PAINT message to be placed in the program's message
queue. The NULL parameter indicates that the entire client area should
be repainted. The TRUE parameter indicates that the background should
be erased.
The WM_SIZE message indicates that the size of SAMPLE's client area
has changed. SAMPLE simply saves the new dimensions of the client area
in two static variables:
xClient = LOWORD (lParam) ;
yClient = HIWORD (lParam) ;
The LOWORD and HIWORD macros are defined in WINDOWS.H.
Windows also places a WM_PAINT message in SAMPLE's message queue when
the size of the client area has changed. As is the case with
WM_COMMAND, the program does not have to repaint the client area
immediately, because the WM_PAINT message is in the message queue.
SAMPLE can receive a WM_PAINT message for many reasons. The first
WM_PAINT message it receives results from calling UpdateWindow in the
WinMain function. Later, if the current font is changed from the menu,
the program itself causes a WM_PAINT message to be placed in the
message queue by calling InvalidateRect. Windows also sends a window
function a WM_PAINT message whenever the user changes the size of the
window or when part of the window previously covered by another window
is uncovered.
Programs begin processing WM_PAINT messages by calling BeginPaint:
BeginPaint (hWnd, &ps) ;
The SAMPLE program then creates a font based on the current size of
the client area and the current typeface selected from the menu:
hFont = CreateFont (yClient, xClient / 8,
0, 0, 400, 0, 0, 0, OEM_CHARSET,
OUT_STROKE_PRECIS, OUT_STROKE_PRECIS,
DRAFT_QUALITY, (BYTE) VARIABLE_PITCH |
cFamily [nCurrentFont - IDM_SCRIPT],
szFace [nCurrentFont - IDM_SCRIPT]) ;
The font is selected into the device context (a data structure
internal to Windows that describes the characteristics of the output
device); the program also saves the original device-context font:
hFont = SelectObject (ps.hdc, hFont) ;
And the word Windows is displayed:
TextOut (ps.hdc, 0, 0, "Windows", 7) ;
The original font in the device context is then selected, and the font
that was created is now deleted:
DeleteObject (SelectObject (ps.hdc, hFont)) ;
Finally, SAMPLE calls EndPaint to signal Windows that the client area
is now updated and valid:
EndPaint (hWnd, &ps) ;
Although the processing of the WM_PAINT message in this program is
simple, the method used is common to all Windows programs. The
BeginPaint and EndPaint functions always occur in pairs, first to get
information about the area that needs repainting and then to mark that
area as valid.
SAMPLE will display this text even when the program is minimized to be
displayed as an icon at the bottom of the screen. Although most
Windows programs use a customized icon for this purpose, the window-
class structure in SAMPLE indicates that the program's icon is NULL,
meaning that the program is responsible for drawing its own icon.
SAMPLE does not, however, make any special provisions for drawing the
icon. To it, the icon is simply a small client area. As a result,
SAMPLE displays the word Windows in its "icon," using a small font
size.
Windows sends the window function the WM_DESTROY message as a result
of the DestroyWindow function that DefWindowProc calls when processing
a WM_CLOSE message. The standard processing involves placing a WM_QUIT
message in the message queue:
PostQuitMessage (0) ;
When the GetMessage function retrieves WM_QUIT from the message queue,
GetMessage returns 0. This terminates the message loop and the
program.
For all other messages, SAMPLE calls DefWindowProc and exits the
window function by returning the value from the call:
return DefWindowProc (hWnd, iMessage, wParam, lParam) ;
This allows Windows to perform default processing on the messages
SAMPLE ignores.
Windows' multitasking
Most operating systems or operating environments that allow
multitasking use what is called a preemptive scheduler. Generally, the
procedure involves use of the computer's clock to switch rapidly
between programs and allow each a small time slice. When switching
between programs, the operating system must preserve the machine
state.
Windows is different. It is a nonpreemptive multitasking environment.
Although Windows allows several programs to run simultaneously, it
never switches from one program to allow another to run. It switches
between programs only when the currently running program calls the
GetMessage function or the related PeekMessage and WaitMessage
functions.
When a Windows program calls GetMessage and the program's message
queue contains a message other than WM_PAINT or WM_TIMER, Windows
returns control to the program with the next message. However, if the
program's message queue contains only a WM_PAINT or WM_TIMER message
and another program's queue contains a message other than WM_PAINT or
WM_TIMER, Windows returns control to the other program, which is also
waiting for its GetMessage call to return.
(Windows also switches between programs temporarily when a program
uses SendMessage to send a message to a window function in another
program, but control returns to the calling program after the window
function has processed the message sent to it.)
To cooperate with Windows' nonpreemptive multitasking, programmers
should try to perform message processing as quickly as possible.
Programs can, for example, split a large amount of processing into
several smaller pieces to allow other programs to run in the interval.
During long processing a program can also periodically call
PeekMessage to allow other programs to run.
Graphics Device Interface
Programs receive input through the Windows message system. For program
output, Windows provides a device-independent interface to graphics
output devices, such as the video display, printers, and plotters.
This interface is called the Graphics Device Interface, or GDI.
The device context (DC)
When a Windows program needs to send output to the video screen, the
printer, or another graphics output device, it must first obtain a
handle to the device's device context, or DC. Windows provides a
number of functions for obtaining this device-context handle:
BeginPaint. Used for obtaining a video device-context handle during
processing of a WM_PAINT message. This device context applies only to
the rectangular section of the client area that is invalid (needs
repainting). This region is also a clipping region, meaning that a
program cannot paint outside this rectangle. BeginPaint fills in the
fields of a PAINTSTRUCT structure. This structure contains the
coordinates of the invalid rectangle and a byte that indicates if the
background of the invalid rectangle has been erased.
GetDC. Generally used for obtaining a video device-context handle
during processing of messages other than WM_PAINT. The handle obtained
with this function references only the client area of the window.
GetWindowDC. Used for obtaining a video device-context handle that
encompasses the entire window, including the title bar, menu bar, and
scroll bars. A Windows program can use this function if it is
necessary to paint over areas of the window outside the client area.
CreateDC. Used for obtaining a device-context handle for the entire
display or for a printer, a plotter, or other graphics output device.
CreateIC. Used for obtaining an information-context handle, which is
similar to a device-context handle but can be used only for obtaining
information about the output device, not for drawing.
CreateCompatibleDC. Used for obtaining a device-context handle to a
memory device context compatible with a particular graphics output
device. This function is generally used for transferring bitmaps to a
graphics output device.
CreateMetaFile. Used for obtaining a metafile device-context handle. A
metafile is a collection of GDI calls encoded in binary form.
The Windows program uses the device-context handle when calling GDI
functions. In addition to drawing, the various GDI functions can
change the attributes of the device context, select different drawing
objects (such as pens and fonts) into the device context, and
determine the characteristics of the device context.
Device-independent programming
Windows supports such a wide variety of video displays, printers, and
plotters that programs cannot make assumptions about the size and
resolution of the device. Furthermore, because the user can generally
alter the size of a program's window, the program must be able to
adjust its output appropriately. The SAMPLE program, for example,
showed how the window function can use the WM_SIZE message to obtain
the current size of a window to create a font that fits text within
the window's client area.
Programs can also use other Windows functions to determine the
physical characteristics of a device. For instance, a program can use
the GetDeviceCaps function to obtain information about the device
context, including the resolution of the device, its physical
dimensions, and its relative pixel height and width.
Then, too, the GetTextMetrics function returns information about the
current font selected in the device context. In the default device
context, this is the system font. Many Windows programs base the size
of their display output on the size of a system-font character.
Device-context attributes
The device context includes attributes that define how the graphics
output functions work on the device. When a program first obtains a
handle to a device context, Windows sets these attributes to default
values, but the program can change them. Some of these device-context
attributes are as follows:
Pen. Windows uses the current pen for drawing lines. The default pen
produces a solid black line 1 pixel wide. A program can change the pen
color, change to a dotted or dashed line, or make the pen draw a solid
line wider than 1 pixel.
Brush. Windows uses the current brush (sometimes called a pattern) for
filling areas. A brush is an 8-pixel-by-8-pixel bitmap. The default
brush is solid white. Programs can create colored brushes, hatched
brushes, and customized brushes based on bitmaps.
Background color. Windows uses the background color to fill the spaces
in and between characters when drawing text and to color the open
areas in hatched brushstrokes and dotted or dashed pen lines. Windows
uses the background color only if the background mode (another
attribute of the display context) is opaque. If the background mode is
transparent, Windows leaves the background unaltered. The default
background color is white.
Text color. Windows uses this color for drawing text. The default is
black.
Font. Windows uses the font to determine the shape of text characters.
The default is called the system font, a fixed-pitch font that also
appears in menus, caption bars, and dialog boxes.
Additional device-context attributes (such as mapping modes) are
described in the following sections.
Mapping modes
Most GDI drawing functions in Windows have parameters that specify the
coordinates or size of an object. For instance, the Rectangle function
has five parameters:
Rectangle (hDC, x1, y1, x2, y2) ;
The first parameter is the handle to the device context. The others
are
■ x1: horizontal coordinate of the upper left corner of the
rectangle.
■ y1: vertical coordinate of the upper left corner of the rectangle.
■ x2: horizontal coordinate of the lower right corner of the
rectangle.
■ y2: vertical coordinate of the lower right corner of the rectangle.
In the Rectangle and most other GDI functions, coordinates are logical
coordinates, which are not necessarily the same as the physical
coordinates (pixels) of the device. To translate logical coordinates
into physical coordinates, Windows uses the current mapping mode.
In actuality, the mapping mode defines a transformation of coordinates
between a window, which is defined in terms of logical coordinates,
and a viewport, which is defined in terms of physical coordinates. For
any mapping mode, a program can define separate window and viewport
origins. The logical point defined as the window origin is then mapped
to the physical point defined as the viewport origin. For some mapping
modes, a program can also define window and viewport extents, which
determine how the logical coordinates are scaled to the physical
coordinates.
Windows programs can select one of eight mapping modes. The first six
are sometimes called fully constrained, because the ratio between the
window and viewport extents is fixed and cannot be changed.
In MM_TEXT, the default mapping mode, coordinates on the x axis
increase from left to right, and coordinates on the y axis increase
from the top downward. In the other five fully constrained mapping
modes, coordinates on the x axis also increase from left to right, but
coordinates on the y axis increase from the bottom upward. The six
fully constrained mapping modes are
■ MM_TEXT: Logical coordinates are the same as physical coordinates.
■ MM_LOMETRIC: Logical coordinates are in units of 0.1 millimeter.
■ MM_HIMETRIC: Logical coordinates are in units of 0.01 millimeter.
■ MM_LOENGLISH: Logical coordinates are in units of 0.01 inch.
■ MM_HIENGLISH: Logical coordinates are in units of 0.001 inch.
■ MM_TWIPS: Logical coordinates are in units of 1/1440 inch. (These
units are 1/20 of a typographic point, which is approximately
1/72 inch.)
The seventh mapping mode is called partially constrained, because a
program can change the window and viewport extents but Windows adjusts
the values to ensure that equal horizontal and vertical logical
coordinates translate to equal horizontal and vertical physical
dimensions:
■ MM_ISOTROPIC: Logical coordinates represent the same physical
distance on both the x and y axes.
The MM_ISOTROPIC mapping mode is useful for drawing circles and
squares. The MM_LOMETRIC, MM_HIMETRIC, MM_LOENGLISH, MM_HIENGLISH, and
MM_TWIPS mapping modes are also isotropic, because equal logical
coordinates map to the same physical dimensions on both axes.
The final mapping mode is sometimes called unconstrained because a
program is free to set different window and viewport extents on the x
and y axes.
■ MM_ANISOTROPIC: Logical coordinates are mapped to arbitrarily
scaled physical coordinates.
Functions for drawing
Windows includes several functions that programs can use to draw in
the client area of a window. The most common of these functions are
SetPixel. Sets a point to a particular color.
LineTo. Draws a line from the current position to a point specified in
the LineTo function. The current position is defined in the device
context and can be altered before the call to LineTo with the MoveTo
function, which changes the current position but does not draw
anything. Windows uses the current pen and the current drawing mode
(see below) for drawing the line.
Polyline. Draws multiple lines much like a series of LineTo calls but
does not alter the current position on completion.
Rectangle. Draws a filled rectangle with a border. Parameters to the
Rectangle function specify the coordinates of the upper left and lower
right corners of the rectangle. Windows draws the border of the
rectangle with the current pen and current drawing mode defined in the
device context, just as if it were using the Polyline function then
Windows fills the rectangle with the current brush defined in the
device context.
Ellipse. Uses the same parameters as Rectangle but draws an ellipse
within the rectangular area.
RoundRect. Draws a rectangle with rounded corners. Two parameters to
this function define the height and width of an ellipse that Windows
uses for drawing the rounded corners.
Polygon. Draws a polygon connecting a series of points and fills the
enclosed areas in either an alternate or winding mode. The winding
mode causes Windows to fill every area within the polygon. The
alternate mode fills every other area. For a polygon that defines a
five-pointed star, for instance, the center is filled if the mode is
winding but is not filled if the mode is alternate.
Arc. Draws a curved line that is part of the circumference of an
ellipse.
Chord. Similar to the Arc function, but Windows connects the beginning
and ending points of the arc with a straight line. The area is filled
with the current brush defined in the device context.
Pie. Similar to the Arc function, but Windows draws lines from the
beginning and ending points of the arc to the center of the ellipse.
The area is filled with the current brush defined in the device
context.
TextOut. Writes text with the current font, text color, background
color, and background mode (transparent or opaque).
Windows also includes other drawing functions for filling areas,
formatting text, and transferring bitmaps.
Raster operations for pens
When Windows uses a pen to write to a device context, it must first
determine which pixels of the destination are to be altered by the pen
(the foreground) and which pixels will not be affected (the
background). With dotted and dashed pens, the background--the space
between the dots or dashes--is left unaltered if the drawing mode is
transparent and is filled with the background color if the drawing
mode is opaque.
When Windows alters the pixels of the destination that correspond to
the foreground of the pen, the most obvious result is that the color
of the current pen defined in the display context is used to color the
destination. But this is not the only possible result. Windows also
generalizes the process by using a logical operation to combine the
pixels of the pen and the pixels of the destination.
This logical operation is defined by the drawing mode attribute of the
device context. This drawing mode can be set to one of 16 binary
raster operations (abbreviated ROP2).
The following table shows the 16 binary raster operation codes defined
in WINDOWS.H. The column headed "Resultant Destination" shows how the
destination changes, depending on the bit pattern of the pen and the
bit pattern of the destination before the line is drawn. The words OR,
AND, XOR, and NOT are the logical operations.
╓┌────────────────────────────────┌──────────────────────────────────────────╖
Binary Raster Resultant
Operation Destination
──────────────────────────────────────────────────────────────────
R2_BLACK 0
R2_COPYPEN pen
R2_MERGEPEN pen OR destination
R2_MASKPEN pen AND destination
R2_XORPEN pen XOR destination
R2_NOTCOPYPEN NOT pen
R2_NOTMERGEPEN NOT (pen OR destination)
R2_NOTMASKPEN NOT (pen AND destination)
R2_NOTXORPEN NOT (pen XOR destination)
R2_MERGEPENNOT pen OR (NOT destination)
R2_MASKPENNOT pen AND (NOT destination)
R2_MERGENOTPEN (NOT pen) OR destination
R2_MASKNOTPEN (NOT pen) AND destination
R2_NOP destination
R2_NOT NOT destination
R2_WHITE 1
The default drawing mode defined in a device context is R2_COPYPEN,
which simply copies the pen to the destination. However, if the pen
color is blue, the destination is red, and the drawing mode is
R2_MERGEPEN, then the drawn line appears as magenta, which results
from combining the pen and destination colors. If the pen color is
blue, the destination is red, and the drawing mode is R2_NOTMERGEPEN,
then the drawn line is green, because the blue pen and the red
destination are combined into magenta, which Windows then inverts to
make green.
Bit-block transfers
Windows also uses logical operations when transferring a rectangular
pixel pattern (a bit block) from one device context to another or from
one area of a device context to another area of the same device
context.
While line drawing involves a logical combination of two sets of
pixels (the pen and the destination), the bit-block transfer functions
perform a logical combination of three sets of pixels: a source
bitmap, a destination bitmap, and the brush currently selected in the
destination device context. As shown in the preceding section, there
are 16 different ROP2 drawing modes for all the possible combinations
of two sets of pixels. The tertiary raster operations (abbreviated
ROP3) for bit-block transfers require 256 different operations for all
possible combinations.
Windows defines three functions for transferring rectangular pixel
patterns: BitBlt (bit-block transfer), StretchBlt (stretch-block
transfer), and PatBlt (pattern-block transfer). Of these three
functions, StretchBlt is the most generalized. StretchBlt transfers a
bitmap from a source device context to a destination device context.
Function parameters specify the origin, width, and height of the
bitmap. If the source and destination widths and heights are
different, Windows stretches or compresses the bitmap appropriately.
Negative values of widths and heights cause Windows to draw a mirror
image of the bitmap.
The BitBlt function transfers a bitmap from a source device context to
a destination device context, but the width and height of the source
and destination must be the same. If the source and destination device
contexts have different mapping modes, Windows uses StretchBlt
instead.
In both BitBlt and StretchBlt, Windows performs a bit-by-bit logical
operation with the bit block in the source device context, the bit
block in the destination area of the destination device context, and
the brush currently selected in the destination device context.
Although Windows supports all 256 possible raster operations with
these three bitmaps, only a few have been given WINDOWS.H identifiers:
╓┌────────────────────────────┌──────────────────────────────────────────────╖
Raster Resultant
Operation Destination
──────────────────────────────────────────────────────────────────
BLACKNESS 0
SRCCOPY source
SRCAND source AND destination
SRCPAINT source OR destination
SRCINVERT source XOR destination
SRCERASE source AND (NOT destination)
MERGEPAINT source OR (NOT destination)
NOTSRCCOPY NOT source
NOTSRCERASE NOT (source OR destination)
DSTINVERT NOT destination
PATCOPY pattern
MERGECOPY source AND pattern
PATINVERT destination XOR pattern
PATPAINT source OR (NOT destination) OR pattern
WHITENESS 1
The PatBlt function is similar to BitBlt and StretchBlt but performs a
logical operation only between the currently selected brush and a
destination area of the device context. Thus, only 16 raster
operations can be used with PatBlt; these are equivalent to the binary
raster operations used with line drawing.
Text and fonts
Windows supports file-based text fonts in two different formats:
raster and vector. The raster fonts, such as Courier, Helvetica, and
Times Roman, are defined by digital representations of the bit
patterns of the characters. Font files usually contain several
different sizes for each typeface. The vector fonts, such as Modern,
Script, and Roman, are defined by points that are connected to form
the letters and can be scaled to different sizes.
When using a device such as a printer, which has built-in fonts,
Windows can also use these device-based fonts.
To specify a font, a Windows program uses the CreateFont function to
create a logical font--a detailed description of the desired font.
When this logical font is selected into a device context, Windows
finds the actual font that best fits this description. In many cases,
this match is not exact. The program can then call GetTextMetrics to
determine the characteristics of the actual font that the device will
use to display text.
Windows supports both fixed-width and variable-width fonts, as well as
such attributes as italics, underlining, and boldfacing. Programs can
also justify text with the GetTextExtent call, which obtains the width
of a particular text string. The program can then insert extra spaces
between words with SetTextJustification or it can insert extra spaces
between letters with SetTextCharacterExtra.
Metafiles
As explained earlier, a metafile is a collection of GDI function calls
stored in a binary coded form. A program can create a metafile by
calling CreateMetaFile and giving it either an MS-DOS filename or NULL
as a parameter. If CreateMetaFile is given an MS-DOS filename, Windows
creates a disk-based metafile; if the parameter is NULL, Windows
creates a metafile in memory. The CreateMetaFile call returns a handle
to a metafile device context. Any GDI calls that reference this
device-context handle become part of the metafile.
When the program calls CloseMetaFile, Windows closes the metafile
device context and returns a handle to the metafile. The program can
then "play" this metafile on another device context (such as the video
display) without calling the GDI functions directly.
Metafiles provide a useful way to transfer device-independent pictures
between programs.
Data Sharing and Data Exchange
Windows includes a variety of methods by which programs can share and
exchange data. These methods are discussed in the following sections.
Sharing local data among instances
Multiple instances of the same program can share data in the static
data area of the program's data segment. Later instances of a program
can thus call GetInstanceData and copy configuration options
established by the user in the first instance. Multiple instances of
programs can also share resources, such as dialog-box templates.
The Windows Clipboard
The Windows Clipboard is a general-purpose mechanism that allows a
user to transfer data from one program to another. Programs that
support the Clipboard generally include a top-level menu item called
Edit, which invokes a pop-up menu that offers at least these three
options:
■ Cut: Copies the current selection to the Clipboard and deletes the
selection from the current program file.
■ Copy: Copies the current selection to the Clipboard without
deleting the selection from the current program file.
■ Paste: Copies the contents of the Clipboard to the current program
file.
The Clipboard can hold only one item at a time. A program can transfer
data to the Clipboard through the function call SetClipboardData. With
this function, the program passes the Clipboard a handle to a global
memory block, which then becomes the property of the Clipboard. A
program can access Clipboard data through the complementary function
GetClipboardData.
The Clipboard supports several formats:
■ Text: ASCII text; each line ends with a carriage return and
linefeed, and the text is terminated with a NULL character.
■ Bitmap: A collection of bits in the GDI bitmap format.
■ Metafile Picture: A structure that contains a handle to a metafile
along with other information suggesting the mapping mode and aspect
ratio of the picture.
■ SYLK: Microsoft's Symbolic Link format.
■ DIF: Software Arts' Data Interchange Format.
Programs can also use the Clipboard for storing data in private
formats.
Some programs, such as the CLIPBRD program included with Windows, can
also become Clipboard viewers. Such programs receive a message
whenever the contents of the Clipboard change.
Dynamic Data Exchange (DDE)
Dynamic Data Exchange (DDE) is a protocol that cooperating programs
can use to exchange data without user intervention. DDE makes use of
the facilities in Windows that enable programs to send messages among
themselves.
In DDE, the program that needs data from another program is called the
client. The client sends a WM_DDE_INITIATE message either to a
dedicated server program or to all currently running programs.
Parameters to the WM_DDE_INITIATE message are atoms, which are numbers
referring to text strings. A server application that has the data the
client needs sends a WM_DDE_ACK message back to the client. The client
can then be more specific about the data it needs by sending the
server a WM_DDE_ADVISE message. The server can then pass global memory
handles to the client with the WM_DDE_DATA message.
Internationalization
Windows includes several features that ease the conversion and
translation of programs for international markets. Among these
features are keyboard drivers appropriate for many European languages
and use of the ANSI character set, which provides a richer set of
accented letters than does the character set resident in the IBM PC
and compatibles.
Windows also includes several functions that assist in language-
independent coding. The AnsiUpper and AnsiLower functions translate
characters or strings to uppercase or lowercase in the full ANSI
character set, rather than the more limited ASCII character set. In
addition, the AnsiNext and AnsiPrev functions allow scanning of text
strings that may contain 2 or more bytes per character.
Windows programmers can also help in program translation by defining
all text strings used within the program as resources contained in the
resource script file. Because the resource script file also contains
menu templates and dialog-box templates, it thus becomes the only file
that needs alteration when a foreign-language version of the program
is created.
Charles Petzold
───────────────────────────────────────────────────────────────────────────
Part E Programming Tools
Article 18: Debugging in the MS-DOS Environment
It is axiomatic that any program will need debugging at some time in
its development cycle, and programs written to run under MS-DOS are no
exception. This article provides an introduction to the debugging
tools and techniques available to the serious programmer developing
code in the MS-DOS environment. Space does not permit a thorough
investigation of the philosophy, psychology, and science of debugging
computer programs; instead, a brief and practical discussion of the
basic debugging approaches is presented, along with some rules-of-
thumb for choosing the best approach. Nor are the details of every
single utility and command included in this article; these are
described in detail in the reference sections of this volume. The
commands and utility programs that are most useful for debugging are
discussed and illustrated with examples and case histories that also
serve as models for the various debugging methods.
The reader of this article is assumed to be a programmer with
sufficient experience to understand an assembly-language program. The
reader is also assumed to be familiar with MS-DOS--terms like FCB and
PSP are not explained. A reader without this background in MS-DOS need
not be deterred, however; these terms are thoroughly explained
elsewhere in this book. Besides assembly language, examples in this
article are written in Microsoft QuickBASIC and Microsoft C. A
detailed knowledge of these languages is not required; the examples
are short and straightforward.
The reader should also keep in mind that the examples given here are
real but not necessarily realistic. To avoid the tedium that
accompanies debugging, the examples have been designed to reveal their
bugs fairly quickly. All the methods and techniques shown are accurate
in detail but not always in scale. Most of the debugging examples
presented here would require one-half to one hour of work. It is
possible for real debugging sessions to last for hours or days,
especially if the wrong approach or tool is chosen. One of the
purposes of this article is to help the programmer choose the correct
tool and, thus, to reduce the tedium.
The Programs
There are more than a dozen listings in this article. Some of them are
correct and others contain errors for use in illustrating debugging
techniques. Many of the programs serve as examples in multiple
sections of the article. The following summary of the programs (Table
18-1) is given to avoid confusion and to provide a common location to
consult for explanations of the programs.
Table 18-1. Summary of Example Programs.
Name: EXP.BAS
Figure: 18-1
Status: Incorrect--do not use.
Purpose: Computes EXP(x) (the exponential of x) to a specified
precision using an infinite series.
Compiling: QB EXP;
LINK EXP;
Parameters: Prompts for value for x and a convergence criterion.
Enter zero to quit.
──────────────────────────────────────────────────────────────────
Name: EXP.BAS
Figure: 18-3
Status: Correct version of Figure 18-1.
Purpose: Computes EXP(x) (the exponential of x) to a specified
precision using an infinite series.
Compiling: QB EXP;
LINK EXP;
Parameters: Prompts for value for x and a convergence criterion.
Enter zero to quit.
──────────────────────────────────────────────────────────────────
Name: COMMSCOP.ASM
Figure: 18-4
Status: Correct.
Purpose: Monitors the activity on a specified COM port and
places a copy of all transmitted and received data in
a RAM buffer. Each entry in the buffer is tagged to
indicate whether the byte was sent by or received by
the application program under test. Control is
provided to start, stop, and resume tracing by means
of a control interrupt. When tracing is stopped and
resumed, a marker is left in the buffer. COMMSCOP is a
terminate-and-stay-resident (TSR) program.
Compiling: MASM COMMSCOP;
LINK COMMSCOP;
EXE2BIN COMMSCOP.EXE COMMSCOP.COM
DEL COMMSCOP.EXE
Parameters: Installed by entering COMMSCOP; no parameters for
installation. The TSR is controlled by passing
parameter data in registers with an Interrupt 60H
call. The registers can have the following values:
AH: Command:
00H STOP
01H FLUSH AND START
02H RESUME TRACE
03H RETURN TRACE BUFFER ADDRESS
DX: COM port:
00H COM1
01H COM2
Interrupt 60H returns the following in response to
function 3:
CX Buffer count in bytes
DX Segment address of buffer
BX Offset address of buffer
──────────────────────────────────────────────────────────────────
Name: COMMSCMD.C
Figure: 18-5
Status: Correct.
Purpose: Controls the COMMSCOP program by issuing Interrupt 60H
calls.C version.
Compiling: MSC COMMSCMD;
LINK COMMSCMD;
Parameters: Commands are issued byCOMMSCMD [[cmd][ port]]
where: cmd is the command to be executed:
STOP Stop trace
START Flush buffer and start trace
RESUME Resume a stopped trace
port is the COM port (1 = COM1, 2 = COM2)
If cmd is omitted, STOP is assumed; if port is omitted,
1 is assumed.
──────────────────────────────────────────────────────────────────
Name: COMMSCMD.BAS
Figure: 18-6
Status: Correct.
Purpose: Controls the COMMSCOP program by issuing Interrupt 60H
calls. QuickBASIC version.
Compiling: QB COMMSCMD;
LINK COMMSCMD USERLIB;
Parameters: Commands are issued by COMMSCMD [[cmd][,port]]
where: cmd is the command to be executed:
STOP Stop trace
START Flush buffer and start trace
RESUME Resume a stopped trace
port is the COM port (1 = COM1, 2 = COM2)
If cmd is omitted, STOP is assumed; if port is omitted,
1 is assumed.
──────────────────────────────────────────────────────────────────
Name: COMMDUMP.BAS
Figure: 18-7
Status: Correct.
Purpose: Produces a formatted dump of the communications trace
buffer.
Compiling: QB COMMDUMP;
LINK COMMDUMP USERLIB;
Parameters: No parameters. When COMMDUMP is invoked, it formats and
dumps the entire buffer.
──────────────────────────────────────────────────────────────────
Name: TESTCOMM.ASM
Figure: 18-9
Status: Incorrect--do not use.
Purpose: Provides test data for the COMMSCOP routine.
Compiling: MASM TESTCOMM;
LINK TESTCOMM;
Parameters: No parameters. TESTCOMM reads data from the keyboard
and writes to COM1 and reads COM1 data and displays it
on the screen. Ctrl-C cancels.
──────────────────────────────────────────────────────────────────
Name: TESTCOMM.ASM
Figure: 18-10
Status: Correct version of Figure 18-9.
Purpose: Provides test data for the COMMSCOP routine.
Compiling: MASM TESTCOMM;
LINK TESTCOMM;
Parameters: No parameters. TESTCOMM reads data from the keyboard
and writes to COM1 and reads COM1 data and displays it
on the screen. Ctrl-C cancels.
──────────────────────────────────────────────────────────────────
Name: BADSCOP.ASM
Figure: 18-11
Status: Incorrect version of Figure 18-4--do not use.
Purpose: Monitors the activity on a specified COM port and
places a copy of all transmitted and received data in
a RAM buffer. Each entry in the buffer is tagged to
indicate whether the byte was sent by or received by
the application program under test. Control is
provided to start, stop, and resume tracing by means
of a control interrupt. When tracing is stopped and
resumed, a marker is left in the buffer. BADSCOP is a
terminate-and-stay-resident (TSR) program.
Compiling: MASM BADSCOP;
LINK BADSCOP;
EXE2BIN BADSCOP.EXE BADSCOP.COM
DEL BADSCOP.EXE
Parameters: Installed by entering BADSCOP; no parameters for
installation. The TSR is controlled by passing
parameter data in registers with an Interrupt 60H
call. The registers can have the following values:
AH: Command:
00H STOP
01H FLUSH AND START
02H RESUME TRACE
03H RETURN TRACE BUFFER ADDRESS
DX: COM port:
00H COM1
01H COM2
Interrupt 60H returns the following in response to
function 3:
CX Buffer count in bytes
DX Segment address of buffer
BX Offset address of buffer
──────────────────────────────────────────────────────────────────
Name: UPPERCAS.C
Figure: 18-13
Status: Incorrect--do not use.
Purpose: Converts a fixed string to uppercase and prints it.
Compiling: MSC /Zi UPPERCAS;
LINK UPPERCAS /CO;
Parameters: No parameters.
──────────────────────────────────────────────────────────────────
Name: UPPERCAS.C
Figure: 18-14
Status: Correct version of Figure 18-13.
Purpose: Converts a fixed string to uppercase and prints it.
Compiling: MSC /Zi UPPERCAS;
LINK UPPERCAS /CO;
Parameters: No parameters.
──────────────────────────────────────────────────────────────────
Name: ASCTBL.C
Figure: 18-16
Status: Incorrect--do not use.
Purpose: Displays a table of all displayable characters.
Compiling: MSC /Zi ASCTBL;
LINK ASCTBL /CO;
Parameters: No parameters.
──────────────────────────────────────────────────────────────────
Name: ASCTBL.C
Figure: 18-17
Status: Correct version of Figure 18-16.
Purpose: Displays a table of all displayable characters.
Compiling: MSC /Zi ASCTBL;
LINK ASCTBL /CO;
Parameters: No parameters.
Debugging Tools and Techniques
MS-DOS provides a wide variety of tools to aid in the debugging
process. Some are intended specifically for debugging. For example,
the DEBUG program is delivered with MS-DOS and provides basic
debugging aid; the more sophisticated SYMDEB is supplied with MASM,
Microsoft's macro assembler; CodeView, a debugger for high-order
languages, is supplied with Microsoft C, Microsoft Pascal, and
Microsoft FORTRAN. Others are general MS-DOS services and features
that are also useful in the debugging process.
Debugging, like programming, has aspects of both an art and a craft.
The craft--the mechanical details of using the tools--is discussed
both here and elsewhere in this volume, but the main subject of this
article is the art of debugging--the choice of the correct tool, the
best techniques to use in various situations, the methods of
extracting the clues to the problem from a recalcitrant program.
Debugging a program is a form of puzzle solving. As with most
intellectual detective work, the following rule applies:
Gather enough information and the solution will be obvious.
The craft of debugging involves gathering the data; the art lies in
deciding which data to gather and in noticing when the solution has
become obvious.
The methods of gathering data for debugging, listed in order of
increasing difficulty and tediousness, fall into four major
categories:
■ Inspection and observation
■ Instrumentation
■ Use of software debugging monitors (DEBUG, SYMDEB, and CodeView)
■ Use of hardware debugging aids
As mentioned above, part of the art of debugging is knowing which
method to use. This is one of the most difficult aspects of debugging-
so difficult, in fact, that even programmers with years of experience
make mistakes. Many programmers have spent hours single-stepping
through a program with DEBUG only to discover that the cause of the
problem would have been obvious if they had given the program's output
even a cursory check. The only universal rule for choosing the correct
debugging method is
Try them all, starting with the simplest.
Inspection and observation
Inspection and observation is the oldest and, usually, the best method
of program debugging. It is also the basis for all the other methods.
The first step with this method, as with the others, is to gather all
the pertinent materials. Program listings, file layouts, report
layouts, and program design materials (such as algorithm descriptions
and flowcharts) are all extremely valuable in the debugging process.
Desk-checking
Before a programmer can determine what a program is doing wrong, he or
she must know the correct operation of the program. There was a time,
when computers were rare and expensive resources, that programmers
were encouraged not to run their programs until the programs had been
thoroughly desk-checked. The desk-checking process involves sitting
down with a listing, a hand calculator, and some sample data. The
programmer then "plays computer," executing each line of the program
manually and writing down on paper the results of each program step.
This process is extremely slow and tedious. When the desk-checking is
completed, however, the programmer not only has found most of the bugs
in the program but also has become intimately familiar with the
execution of the program and the values of the program variables at
each step.
The advent of inexpensive yet powerful personal computers, combined
with the rising cost of programmer time, has made complete desk-
checking nearly obsolete. It is now cheaper to run the program and let
the computer find the errors. However, the usefulness of the desk-
checking technique remains. Many programmers find it helpful to
manually execute those sections of a program that they suspect are
causing trouble. Even if they don't find errors in the code, the
insight they gain into the workings of the code and the values of the
variables at each step can be invaluable when applying other debugging
techniques.
The inspection-and-observation methodology
The basic technique of the inspection-and-observation method is
simple: After gathering all the required materials, run the program
and observe. Observe very carefully; events that seem insignificant
may be the very clues needed to discover where the program is going
astray. As the program executes, note whether each section performs
correctly. Does the program clear the screen when it should? Does it
ask for input when it should? Does it produce the correct results?
Observable events are the debugger's milestones in the execution of
the program. If the program clears the screen but writes purple
asterisks instead of requesting input, then the problem lies somewhere
after the program issues the Clear Screen command but before it writes
the input prompt on the screen. At this point, the program listing and
design data become important. Inspect the listing and examine the area
after the last successful milestone and before the missing milestone.
Look for a logic error in the code that could explain the observed
data.
If the program produces printed reports, they may also be useful.
Watch the screen and listen to the printer. Clues can sometimes be
found in the order in which things happen. The light on the disk drive
can be another indication of activity. See how disk activity
coordinates with screen and printer events. Try to identify each
section of the program from these clues. Then use this information to
localize the inspection of the listing to isolate the erroneous code.
The values of data given in reports and on the screen can also give
clues to what's going wrong. Examining the data and reconstructing the
values used to compute it sometimes leads to inferences about data
problems. Perhaps a variable was misspelled in the code or perhaps a
data file is in the wrong format or has been corrupted. With this
information, the bug can often be isolated. However, a very thorough
knowledge of the program and its algorithms is required. See
Desk-checking, above.
MS-DOS provides four commands and filters that are useful in the
collection and examination of data for debugging: TYPE, PRINT, FIND,
and DEBUG. All these commands display the data in a file in some way.
If the data is ASCII (displayable) characters, TYPE and PRINT can be
used, with some help from FIND. Binary files can be examined and
modified with the DEBUG utility. See USER COMMANDS: FIND; PRINT;
TYPE; PROGRAMMING UTILITIES: DEBUG.
The TYPE command provides the simplest way to display ASCII data
files. This method can be used to examine both input and output files.
Checking the input files may uncover some bad (or unexpected) data
that causes the program to malfunction; examining the output files
will show whether calculations are being performed correctly and may
help pinpoint the erroneous calculations if they are not.
The FIND utility is useful in locating specific data in a file. Using
FIND is more accurate and definitely less tedious than examining the
file manually using the TYPE command. The /N switch causes FIND to
also display the relative line number of the matching line-information
that is most useful in debugging.
Sometimes the data is too complex to be examined on the screen and
printed copy is needed. The PRINT command will produce hard copy of an
ASCII file as will the TYPE command if its output is redirected to the
printer with the >PRN command-line parameter after the filename.
Not all data files contain pure ASCII data, and displaying such non-
ASCII files requires a different approach. The TYPE command can be
used, but nonprintable characters will produce garbage on the screen.
This technique can still prove useful if the file has a large amount
of ASCII data or if the records are regular and the ASCII information
always appears at the same location, but no information can be gained
about non-ASCII numeric data in such files. Note also that the entire
file might not be displayed using TYPE because if TYPE encounters a
byte containing 1AH (Control-Z), it assumes it has reached the end of
the file and stops.
Clearly, a more useful tool for examining non-ASCII files would be a
program that dumps the file in hexadecimal, with an appropriate
translation of all displayable characters. Such programs exist in the
public domain (through bulletin-board services, for instance) and, in
any event, are not difficult to write. MS-DOS does not include a
stand-alone file-dumping program among its standard commands and
utilities, but the DEBUG program can be used, with minor
inconvenience, to display files. This program is discussed in detail
later in this article; for now, simply follow these instructions to
use DEBUG as a file dumper. To load DEBUG and the program to be
debugged, use the form
DEBUG [drive:][path]filename.ext
DEBUG will display a hyphen as a prompt. To see the contents of the
file, enter D (the DEBUG Display Memory command) and press Enter.
DEBUG will display the first 128 (80H) bytes of the file in
hexadecimal and will also show any displayable characters. To see the
rest of the file, simply continue entering D until the desired area is
found. Hard copy of the contents of the display can be made by using
the PrtSc key (or Ctrl-PrtSc to print continuously). Note that the
offset addresses for the bytes in the file begin at the value in the
program's CS:IP registers, which can be viewed by using the Debug R
(Display or Modify Registers) command. To obtain the true offsets,
subtract CS:IP from the address shown.
The essence of the inspection-and-observation method is careful and
thoughtful observation. The computer and the operating system can
provide tools to aid in the collection of data, but the most important
tool is the programmer's mind. By applying the logical skills they
already possess to the observed data, programmers can usually avoid
the more complex forms of debugging.
Instrumentation
Debugging by instrumentation is a traditional method that has been
popular since programs were holes punched in cards. In general, this
method consists of adding something to the program, either internally
or externally, to report on the progress of program execution.
Programmers call this added mechanism instrumentation because of
its resemblance to the measuring instruments used in science and
engineering. Instrumentation can be software, hardware, or a combina-
tion of both; it can be internal to the program or external to it.
Internal instrumentation is always software, but external
instrumentation may be either hardware or software.
Internal instrumentation
Internal instrumentation usually consists of display or print
statements placed at strategic locations. Other signals to the user
can be used if they are available. For instance, the system beeper can
be sounded at key locations, perhaps in a coded sequence of beeps; if
the device being debugged has lights that can be accessed by the
program, these lights can be flashed at important locations in the
program. Beeping and flashing do not, however, possess the information
content usually required for debugging, so display or print statements
are preferred.
The use of display or print statements to display key data and
milestones on the screen or printer requires careful planning. First,
apply the techniques of inspection and observation described in the
previous section to determine the most probable points of failure.
Then, if there is some doubt about what path execution is taking
through the code, embed messages of the following types after key
decision points:
BEGINNING SORT PHASE
ENDING PRINCIPAL OF CALCULATION
PROCESSING RECORD XX
A second way to use display or print statement instrumentation is to
embed statements that display the data and interim values used for
calculations. This technique can be extremely useful in finding
problems related to the data being processed. Consider the QuickBASIC
program in Figure 18-1 as an example. The program has no syntax errors
and compiles cleanly, but it sometimes produces an incorrect answer.
──────────────────────────────────────────────────────────────────────
Figure 18-1. A routine to compute exponentials.
──────────────────────────────────────────────────────────────────────
The purpose of the EXP.BAS program is to compute the exponential of a
given number to a specified precision using an infinite series. The
program computes the value of each term in the infinite series and
adds it to a running total. To keep from executing forever, the
program checks the difference between the last two elements computed
and stops when this difference is less than the convergence criterion
entered by the user.
When the program is run for several values, the following results are
observed:
Enter number: ? 1
Enter convergence criterion (.0001 for 4 places): ? .0001
2.718282
10 elements required to converge
Enter number: ? 1.5
Enter convergence criterion (.0001 for 4 places): ? .0001
4.481686
11 elements required to converge
Enter number: ? 2
Enter convergence criterion (.0001 for 4 places): ? .0001
5
3 elements required to converge
Enter number: ? 2.5
Enter convergence criterion (.0001 for 4 places): ? .0001
12.18249
15 elements required to converge
Enter number: ? 3
Enter convergence criterion (.0001 for 4 places): ? .0001
13
4 elements required to converge
Enter number: ? 0
Some of these numbers are incorrect. Table 18-2 shows the computed
values and the correct values.
Table 18-2. The Computed Values Generated By EXP.BAS
and the Expected Values.
──────────────────────────────────────────────────────────────────
x Computed Correct
──────────────────────────────────────────────────────────────────
1.0 2.718282 2.718282
1.5 4.481686 4.481689
2.0 5 7.389056
2.5 12.18249 12.18249
3.0 13 20.08554
Applying the methods from the first section of this article and
observing the data quickly reveals a pattern. With the exception of 1,
all whole numbers give incorrect results, but all numbers with
fractions give results that are correct to the specified convergence
criterion. Examination of the listing shows no obvious reason for
this. The answer is there, but only an exceptionally intuitive numeric
analyst would see it. Because no answer is obvious, the next step is
to validate the only information available--that whole numbers produce
errors and fractional ones do not. Repeating the first experiment with
2 and a number very close to 2 yields the following results:
Enter number: ? 1.999
Enter convergence criterion (.0001 for 4 places): ? .0001
7.38167
13 elements required to converge
Enter number: ? 2
Enter convergence criterion (.0001 for 4 places): ? .0001
5
3 elements required to converge
Enter number: ? 0
The outcome is the same--repeating the experiment with a number as
near to 2 as the convergence criterion permits (1.9999) produces the
same result. The error is indeed caused by the fact that the number is
an integer.
Because no intuitive way can be found to solve the mystery by
inspection, the programmer must turn to the next method in the
hierarchy, instrumentation. The problem has something to do with the
calculation of the terms of the series. Therefore, the section of the
program that performs this calculation should be instrumented by
placing PRINT statements inside the WHILE loop (Figure 18-2) to
display all the intermediate values of the calculation.
WHILE ABS(LAST - DELTA) >= C
LAST = DELTA
FACT = FACT * TERMS
DELTA = X ^ TERMS / FACT
EX = EX + DELTA
PRINT "TERMS="; TERMS; "EX="; EX; "FACT="; FACT; "DELTA="; DELTA;
PRINT "LAST="; LAST
TERMS = TERMS + 1
WEND
Figure 18-2. Instrumenting the WHILE loop.
The print statements used in this WHILE loop are typical of the type
used for instrumentation. The program makes no attempt at fancy
formatting. The print statements simply identify each value with its
variable name, allowing easy correlation of the data and the code in
the listing. Repeating the experiment with 1.999 and 2 yields
Enter number: ? 1.999
Enter convergence criterion (.0001 for 4 places): ? .0001
TERMS= 1 EX= 2.999 FACT= 1 DELTA= 1.999 LAST= 1E+34
TERMS= 2 EX= 4.997001 FACT= 2 DELTA= 1.998 LAST= 1.999
TERMS= 3 EX= 6.328335 FACT= 6 DELTA= 1.331334 LAST= 1.998
TERMS= 4 EX= 6.993669 FACT= 24 DELTA= .6653343 LAST= 1.331334
TERMS= 5 EX= 7.25967 FACT= 120 DELTA= .2660006 LAST= .6653343
TERMS= 6 EX= 7.348292 FACT= 720 DELTA= 8.862254E-02 LAST= .2660006
TERMS= 7 EX= 7.373601 FACT= 5040 DELTA= 2.530806E-02 LAST= 8.862254E-02
TERMS= 8 EX= 7.379924 FACT= 40320 DELTA= 6.323853E-03 LAST= 2.530806E-02
TERMS= 9 EX= 7.381329 FACT= 362880 DELTA= 1.404598E-03 LAST= 6.323853E-03
TERMS= 10 EX= 7.38161 FACT= 3628800 DELTA= 2.807791E-04 LAST= 1.404598E-03
TERMS= 11 EX= 7.381661 FACT= 3.99168E+07 DELTA= 5.102522E-05 LAST= 2.807791E-04
TERMS= 12 EX= 7.38167 FACT= 4.790016E+08 DELTA= 8.499951E-06 LAST= 5.102522E-05
7.38167
13 elements required to converge
Enter number: ? 2
Enter convergence criterion (.0001 for 4 places): ? .0001
TERMS= 1 EX= 3 FACT= 1 DELTA= 2 LAST= 1E+34
TERMS= 2 EX= 5 FACT= 2 DELTA= 2 LAST= 2
5
3 elements required to converge
Examination of the instrumentation printout for the two cases shows a
drastically different pattern. The fractional number went through 13
iterations following the expected pattern; the whole number, however,
quit on the third step. The loop is terminating prematurely. Why? Look
at the values calculated for DELTA and LAST on the last complete step.
They are the same, giving a difference of zero. Because this
difference will always be less than the convergence criterion, the
loop will always terminate early. A moment's reflection shows why. The
numerator of the fraction for each term but the first in the infinite
series is a power of the number entered; the denominator is a
factorial, a product formed by multiplying successive integers.
Because n! = n * (n-1)!, when an integer is raised to a power equal to
itself and divided by the factorial of that integer the result will
always be the same as the preceding term. That is what has happened
here.
Now that the cause of the problem is found, it must be fixed. How can
this problem be prevented? In this case, the problem is caused by a
logic error. The programmer misread (or miswrote!) the algorithm and
assumed that the criterion for termination was that the difference
between the last two terms be less than the specified value. This is
incorrect. Actually, the termination criterion should be that the
difference between the forming EXP(x) and the last term be less than
the criterion. To simplify, the last term itself must be less than the
value specified. The correct program listing, including the new WHILE
loop, is shown in Figure 18-3.
──────────────────────────────────────────────────────────────────────
Figure 18-3. Corrected exponential calculation routine.
──────────────────────────────────────────────────────────────────────
The program now produces the correct results within the limits of the
specified accuracy:
Enter number: ? 1.999
Enter convergence criterion (.0001 for 4 places): ? .0001
7.381661
12 elements required to converge
Enter number: ? 2
Enter convergence criterion (.0001 for 4 places): ? .0001
7.389047
12 elements required to converge
Enter number: ? 0
This example illustrates how easy it is to use internal
instrumentation in high-order languages. Because these languages
usually have simple formatted output commands, they require very
little work to instrument. When these output commands are not
available, however, more work may be required. For instance, if the
program being debugged is in assembly language, it is possible that
the code required to format and print internal data will be longer
than the program being debugged. For this reason, internal
instrumentation is rarely used on small and moderate assembly
programs. However, large assembly programs and systems often already
have print formatting routines built into them; in these cases,
internal instrumentation may be as easy as with high-order languages.
External instrumentation
Sometimes it is difficult to use internal instrumentation with a
program. If, for instance, the problem is timing related, adding print
statements could cloud the problem or, worse yet, make it go away
completely. This leaves the programmer in the frustrating position of
having the problem only when the cause can't be seen and not having
the problem when it can. A solution to this type of problem can
sometimes be found by moving the instrumentation outside the program
itself. There are two types of external instrumentation: hardware and
software.
Hardware instrumentation consists of whatever logic analyzers,
oscilloscopes, meters, lights, bells, or gongs are appropriate to the
hardware and software under test. Hardware instrumentation is
difficult to set up and tedious to use. It is, therefore, usually
reserved for those problems directly associated with hardware. Such
problems often arise when new software is being run on new hardware
and no one is quite sure where the bugs are. Because most programmers
reading this book are developing software on tried-and-true personal
computer hardware and because most of those programmers are unlikely
to have a logic analyzer costing several thousand dollars, we will
skip over the use of hardware instrumentation for software debugging.
If a logic analyzer must be used, the programmer should remember that
the debugging philosophy and techniques discussed in this article can
still be applied effectively.
MS-DOS provides a feature that is very useful in building external
instrumentation software: the TSR, or terminate-and-stay-resident
routine. See PROGRAMMING IN THE MS-DOS ENVIRONMENT: CUSTOMIZING
MS-DOS: Terminate-and-Stay-Resident Utilities. This feature of the
operating system allows the programmer to build a monitoring routine
that is, in essence, a part of the operating system and outside the
application program. The TSR is loaded as a normal program, but
instead of leaving the system when it is done, it remains intact in
memory. The operating system provides no way to reexecute the program
after it terminates, so most TSRs are interrupt driven.
Because TSRs exist outside the application program, they can be used
to build external instrumentation devices. This independence allows
them to perform monitoring functions without disturbing the logic flow
of the application program. The only areas where interference is
likely are those where the TSR and the program must use common
resources. These conflicts typically involve timing but can also
involve other resources, such as I/O devices, disk files, and MS-DOS
resources, including environment space. Some of these problems are
addressed in the next example.
The TSR type of external instrumentation software can prove useful in
analyzing serial communications. Such an instrumentation program
monitors the serial communication line and records all data. To detect
protocol or timing problems, the program tags the recorded data so
that transmitted data can be differentiated from received data.
Hardware devices exist that plug into the serial port and perform both
the monitoring and tagging function, but they are expensive and not
always handy. Fortunately, this inexpensive piece of software
instrumentation will serve in many cases.
Software interrupt calls are made with the INT instruction. Although
their service routines must obey similar rules, these interrupts
should not be confused with hardware interrupts caused by external
hardware events. Software interrupts in MS-DOS are used by an
application program to communicate with the operating system and, by
extension in IBM systems, with the ROM BIOS. For example, on IBM PCs
and compatibles, application programs can use software Interrupt 14H
to communicate with the ROM BIOS serial port driver. The ROM BIOS
routine, in turn, manages the hardware interrupts from the actual
serial device. Thus, Interrupt 14H does not communicate directly with
the hardware. All the programs in this article deal with software
interrupts to the ROM BIOS and MS-DOS.
A program to trace the serial data flow must have access to the serial
data, so such a program must replace the vector for Interrupt 14H with
one that points to itself. The routine can then record all the serial
data and pass it along through the serial port. Because the goal is to
minimize the effect of this monitoring on the timing of the data, the
method used for recording the data should be fast. This requirement
rules out writing to a disk file, because unexpected delays can be
introduced (and because doing disk I/O from an interrupt service
routine under MS-DOS is difficult, if not impossible). Printing the
data on paper is clearly too slow, and data displayed on the screen is
too ephemeral. Thus, about the only thing that can be done with the
data is to write it to RAM. Luckily, memory has become cheap and most
personal computers have plenty.
Writing a routine that monitors and records serial data is not enough,
however. The data must still flow through the serial port to and from
the external serial device. Thus, the monitor program can have only
temporary custody of the data and must pass it on to the serial
interrupt service routine in the ROM BIOS. This is accomplished by
using MS-DOS function calls to extract the address of the serial
interrupt handler before the new vector is installed in its place. The
process of intercepting interrupts and then passing the data on is
known as "daisy-chaining" interrupt handlers. So long as such
intercepting programs are careful to maintain the data and conditions
upon entrance for subsequent routines (that is, so long as routines
are well behaved; see PROGRAMMING IN THE MS-DOS ENVIRONMENT:
PROGRAMMING FOR MS-DOS), several interrupt handlers can be daisy-
chained together with no detriment to processing. (Woe be unto the
person who breaks the daisy chain--the results are annoying at best
and unpredictable at worst.)
The serial monitoring program, as described so far, correctly collects
and stores serial data and then passes it on to the serial port. This
may be intellectually satisfying, but it is not of much use in the
real world. Some way must be provided to control the program--to start
collection, to stop collection, to pause and resume collection. Also,
once data is collected, a control function must be provided that
returns the number of bytes collected and their starting location, so
that the data can be examined.
From all this, it is clear that a serial communications monitoring
instrument must
1. Replace the Interrupt 14H vector with one pointing to itself.
2. Save the address of the old interrupt handler.
3. Collect the serial data, tag it as transmitted or received, and
store it in RAM.
4. Pass the data on, in a completely transparent manner, to the old
interrupt handler.
5. Provide some way to control data collection.
A program that meets all these criteria is shown in Figure 18-4. The
COMMSCOP program has three major parts:
╓┌───────────────────────┌───────────────────────────────────────────────────╖
Procedure Purpose
──────────────────────────────────────────────────────────────────
COMMSCOPE Monitoring and tagging
CONTROL External control
VECTOR_INIT Interrupt vector initialization
The COMMSCOPE procedure provides the new Interrupt 14H handler that
intercepts the serial I/O interrupts. The CONTROL procedure provides
the external control needed to make the system work. The VECTOR_INIT
procedure gets the old interrupt handler address, installs COMMSCOPE
as the new interrupt handler, and installs the interrupt handler for
the control interrupt.
──────────────────────────────────────────────────────────────────────
Figure 18-4. Communications trace utility.
──────────────────────────────────────────────────────────────────────
The first executable statement of the program is a jump to the
VECTOR_INIT procedure. The vector initialization code is needed only
during installation; after initialization of the vectors, the code can
be discarded. In this case, the area where this code resides will
become the start of the trace buffer; therefore, it makes sense to put
the initialization code at the end of the program where it can be
overlaid by the trace buffer. The jump at the start of the program is
required because the rules for making .COM files require that the
entry point be the first instruction of the program.
The vector initialization routine uses Interrupt 21H Function 35H (Get
Interrupt Vector) to get the address of the current Interrupt 14H
service routine. The segment and offset address (returned in the ES:BX
registers) is stored in the doubleword at OLD_COMM_INT. Interrupt 21H
Function 25H (Set Interrupt Vector) is then used to vector all
Interrupt 14H calls to COMMSCOPE. Another Function 25H call sets
Interrupt 60H to vector to the CONTROL routine. This interrupt, which
provides the means to control and interrogate the COMMSCOPE routine,
was chosen because it is unused by MS-DOS and because some IBM
technical materials list 60H through 66H as being available for user
interrupts. (If, for some reason, Interrupt 60H is not available,
simply change the equated symbol COMMSCOPE_INT to an available
interrupt.)
When the vector initialization process is complete, the routine exits
and stays resident by using Interrupt 21H Function 31H (Terminate and
Stay Resident). As part of the termination process, the routine
requests 1000H paragraphs, or 64 KB, of storage. A little over 500
bytes of this storage area is used for the code; the rest is available
for trace data. If the serial port is running at 2400 baud, a solid
stream of data will fill this buffer in about two minutes. However, a
solid 32 KB block of data is unusual in asynchronous communications
and, in reality, the buffer will usually contain many minutes worth of
data. Note that the buffer-handling routines in COMMSCOPE require that
the buffer be aligned on an even byte boundary, so VECTOR_INIT is
preceded by the EVEN directive.
The interrupt service routine, COMMSCOPE, receives all Interrupt 14H
calls. First COMMSCOPE checks its own status. If it has not been
activated, it immediately passes control to the real service routine.
If the tracer is active, COMMSCOPE examines the Interrupt 14H function
in AH. Setup and status requests (AH = 0 and AH = 3) do not affect
tracing, so they are passed on directly to the the real service
routine. If the Interrupt 14H call is a write-data request (AH = 1),
COMMSCOPE moves the byte marking the data as transmitted and the data
byte itself to the current buffer location and increments both the
byte count and the buffer pointer by 2. If the buffer pointer goes to
zero, the buffer has wrapped; data collection is turned off and cannot
be turned on again without clearing the trace buffer. Because the
buffer, which starts at VECTOR_INIT, is always on an even byte
boundary, there is no danger of the first byte of the data pair
forcing a wrap. After the transmitted data is added to the buffer,
COMMSCOPE passes control to the real service routine.
A read-data request (AH = 2) must be handled a little differently. In
this case, the data to be collected is not yet available. In order to
get it, COMMSCOPE must pass control to the real service routine and
then intercept the results on the way back. The code at GET_READ fakes
an interrupt to the service routine by pushing the flags onto the
stack so that the service routine's IRET will pop them off again.
COMMSCOPE then calls the service routine and, when it returns,
retrieves the incoming serial data character from AL. If the incoming
data byte is valid (bit 7 of AH is zero), the byte marking the data as
received and the data byte itself are placed in the trace buffer, and
both the byte count and the buffer pointer are incremented by 2. The
buffer-wrap condition is detected and handled in the same manner as
with transmitted data. Because the real service routine has already
been called, COMMSCOPE exits as if it were the service routine by
issuing an IRET.
The CONTROL procedure provides the mechanism for external control of
the trace procedure. The routine is entered whenever an Interrupt 60H
is executed. Commands are sent through the AH register and can cause
the routine to STOP (AH = 0), START/FLUSH (AH = 1), RESUME (AH = 2),
or RETURN STATUS (AH = 3). This routine also sets the communications
port to be traced. The required information is provided in DX using
the same format as the Interrupt 14H routine. The port information is
used only with START and RESUME requests. The RETURN STATUS command
returns data in registers: the byte count (CX), the segment address of
the buffer (DX), and the offset of the first byte in the buffer (BX).
The COMMSCOP program is assembled using the Microsoft Macro Assembler
(MASM), linked using the Microsoft Object Linker (LINK), and then
converted to a .COM file using EXE2BIN (see PROGRAMMING UTILITIES):
C>MASM COMMSCOP; <Enter>
C>LINK COMMSCOP; <Enter>
C>EXE2BIN COMMSCOP.EXE COMMSCOP.COM <Enter>
C>DEL COMMSCOP.EXE <Enter>
The linker will display the message Warning: no stack segment; this
message can be ignored because the rules for making a .COM file forbid
a separate stack segment.
The program is installed by simply typing COMMSCOP. Tracing can then
be started and stopped using Interrupt 60H. MS-DOS does not allow
resident routines to be removed, so COMMSCOP will be in the system
until the system is restarted. Also note that, because COMMSCOP is
well behaved, nothing disastrous will happen if multiple copies of it
are accidentally installed. As each new copy is installed, it chains
to the previous copy. When Interrupt 14H is intercepted, the new
routine dutifully passes the data on to the previous routine, which
repeats the process until the real service routine is reached. The
data is added to the trace buffer of each copy, giving multiple,
redundant copies of the same data. Because Interrupt 60H is not
chained, only the last copy's buffer can be accessed. Thus, the other
copies simply waste 64 KB each.
Two techniques can be used to start or stop a trace. The first is to
issue Interrupt 60H calls at strategic locations within the program
being debugged. With assembly-language programs, this is easy. The
appropriate registers are loaded and an INT 60H instruction is
executed. Issuing this INT instruction is not much more difficult with
higher-order Microsoft languages--both QuickBASIC and C provide a
library routine called INT86 that allows registers to be loaded and
INT instructions to be executed. (In QuickBASIC, the INT86 library
routine is included in the File USERLIB.OBJ; in Microsoft C, it is
included in the file DOS.H.) Embedded Interrupt 60H calls can be
convenient because they limit tracing to those areas where processing
is suspect. Because COMMSCOP marks the buffer each time the trace is
stopped and resumed, the separate pieces of a trace are easy to
differentiate.
The second technique is to write a simple routine to start or stop the
trace outside the program being debugged. The example in Figure 18-5,
COMMSCMD, is a Microsoft C program that can perform these functions
using the INT86 library function to issue Interrupt 60H calls.
──────────────────────────────────────────────────────────────────────
Figure 18-5. A serial-trace control routine written in C.
──────────────────────────────────────────────────────────────────────
COMMSCMD is passed arguments in the command line. The first argument
is the command to be performed: STOP, START, or RESUME. If no command
is specified, STOP is assumed. The second argument is the port number:
1 (for COM1) or 2 (for COM2). If no port number is specified, 1 is
assumed.
The COMMSCMD program uses a simple IF filter to determine the function
to be performed. The program tests the number of arguments in the
command line to see if a port has been specified. If the argument
count (argc) is 3 (one for the command name, one for the command, and
one for the port number), the port number argument is retrieved and
converted to an integer. The Interrupt 60H routine expects port
numbers to be specified in the same manner as for Interrupt 14H, so
the port number is decremented if it is not already zero. The AH
register is loaded with the command (cmd), the DX register is loaded
with the port number (port), and the INT86 library function is then
used to execute an Interrupt 60H call. When the interrupt returns,
COMMSCMD displays a message showing the function and port.
The same function can be performed by the QuickBASIC program in
Figure 18-6.
──────────────────────────────────────────────────────────────────────
Figure 18-6. A QuickBASIC version of COMMSCMD.
──────────────────────────────────────────────────────────────────────
Both versions of COMMSCMD accept their commands from the command tail;
both are invoked with a STOP, START, or RESUME command and a serial
port number (1 or 2). If the operands are omitted, STOP and COM1 are
assumed.
After data has been collected and safely placed in the trace buffer,
it must be read before it can be useful. Interrupt 60H provides a
function (AH = 3) that returns the buffer address and the number of
bytes in the buffer. The QuickBASIC routine in Figure 18-7 uses this
function to get the address of the data and then formats the data on
the screen.
──────────────────────────────────────────────────────────────────────
Figure 18-7. Formatted dump routine for serial-trace buffer.
──────────────────────────────────────────────────────────────────────
COMMDUMP is a simple routine. Like most debugging aids, it lacks
needless frills. When it is executed, COMMDUMP displays the data in
the trace buffer on the screen in the format shown in Figure 18-8.
.012832.132056780001006713205678000100671320567800010067132056780001006713205678
03333330333333333333333333333333333333333333333333333333333333333333333333333333
10128323132056780001006713205678000100671320567800010067132056780001006713205678
--------------------------------------------------------------------------------
00010067132056780001006713205678000100671320567800010067.#...012832.567813200001
33333333333333333333333333333333333333333333333333333333021003333330333333333333
00010067132056780001006713205678000100671320567800010067338610128323567813200001
--------------------------------------------------------------------------------
00675678132000010067567813200001006756781320000100675678132000010067567813200001
33333333333333333333333333333333333333333333333333333333333333333333333333333333
00675678132000010067567813200001006756781320000100675678132000010067567813200001
--------------------------------------------------------------------------------
006756781320000100675678132000010067.#...012832.00671320567800010067132056780001
33333333333333333333333333333333333302100333333033333333333333333333333333333333
00675678132000010067567813200001006733861012832300671320567800010067132056780001
--------------------------------------------------------------------------------
00671320567800010067132056780001006713205678000100671320567800010067132056780001
33333333333333333333333333333333333333333333333333333333333333333333333333333333
00671320567800010067132056780001006713205678000100671320567800010067132056780001
--------------------------------------------------------------------------------
0067132056780001.#...012832.1320567800010067132056780001006713205678000100671320
33333333333333330210033333303333333333333333333333333333333333333333333333333333
00671320567800013386101283231320567800010067132056780001006713205678000100671320
--------------------------------------------------------------------------------
NUM = 1122 BUFSEG = 1313 BUFOFF = 208 ENTER ANY KEY TO CONTINUE:
Figure 18-8. Formatted trace dump routine output.
Note that the data for each byte is presented in two forms. If the
byte is greater than 1FH, the ASCII character represented by that
number is shown; otherwise, a dot is shown. Directly below each
character is the hexadecimal representation of the data. The display
shows received data in reverse video and transmitted data in normal
video. The mark placed in the buffer when collection is stopped and
resumed is represented on the screen as a vertical bar one character
wide. The display pauses when the screen is full and waits for a key
to be pressed.
Data collected and displayed in this way can be invaluable to the
programmer trying to debug a program involving a communications
protocol. The example shown above is part of an ordered exchange of
sales data for a system using blocked transmissions and ACK/NAK
protocol. Like all debugging, finding bugs in such a system requires
the collection of large amounts of data. With no data, the causes of
problems can be almost impossible to find; with sufficiently large
amounts of data, the solutions are obvious.
Several things could be done to the COMMSCOP program to increase its
usefulness. For instance, there are six unused bits in the tag
accompanying each data byte in the trace buffer. These could be used
to record the status of the modem control bits, to place timer ticks
in the buffer, or to coordinate the data with some outside event.
(Such changes to COMMSCOP would require a more complicated COMMDUMP
routine to display them.)
Software debugging monitors
Debugging monitors provide the next level of sophistication in the
hierarchy of debugging methods. These monitors are coresident in
memory with the application being debugged and provide a controlled
testing environment--that is, they allow the programmer to control the
execution of the program and to monitor the results. They even allow
some problems to be fixed directly and the result reexecuted
immediately, without the need to reassemble or recompile.
These monitors are analogous to the TSR serial monitor from the
previous section. The debugging monitors, however, do not reside
permanently in memory and are controlled interactively from the
keyboard during the execution of the program under test. Although this
level of control is more flexible than instrumentation, it is also
more intrusive into program execution. While the debugging monitor
sits and waits for input from the keyboard, the application program is
also idle. For programs that must run in real time or must respond to
external stimuli, long delays can be fatal. Careful planning and a
thorough knowledge of the internal workings of the program are
required to debug in such an environment.
Other problems with debugging monitors arise from the nature of the
monitors themselves. They are programs, no different from the
application program being debugged and are therefore limited to those
things that can be done with software. For instance, they can break
(stop execution to allow investigation of program status) when a
specific instruction address is executed (because this can be done
with software), but they cannot break when a data address is
referenced (because this would require special hardware). Because
these monitors reside in RAM, as do the application program and
MS-DOS, they are susceptible to damage from a program running wild.
Some trial and error is usually involved in locating the problem
causing this kind of damage; breakpoints won't work here because the
problem kills the monitor (and usually MS-DOS also).
Microsoft provides three debugging monitors, each with greater
capabilities than its predecessor. In order of increasing
sophistication, these three monitors are
╓┌─────────────────┌─────────────────────────────────────────────────────────╖
Monitor Description
──────────────────────────────────────────────────────────────────────
DEBUG A basic debugging monitor with the ability to load files,
modify memory and registers, execute programs, set
simple breakpoints, trace execution, modify disk files,
and enter assembly-language statements into memory.
SYMDEB A more advanced debugging monitor incorporating all the
features of DEBUG plus more sophisticated data display,
support for graphics programs, support for the Intel
80186/80286 microprocessors and the Intel 80287 math
coprocessor, improved breakpoints, improved tracing,
recognition of symbols from the program being debugged,
and limitedsource-line display.
CodeView The most sophisticated debugging monitor, incorporating
the functionality of SYMDEB (with some differences in
the details) plus windows, full source-line support,
mouse support, and generally more sophistication on all
functions.
Although all these debugging monitors will be discussed here, this
section is not intended to be a tutorial on all the commands and
options of the monitors--those are presented elsewhere in this volume
and in the manuals accompanying the monitors. See PROGRAMMING
UTILITIES: DEBUG; SYMDEB; CODEVIEW. Rather, this section uses case
histories and sample programs to illustrate the techniques for solving
various types of common debugging problems. The case histories have
been chosen to show a wide range of problems, from simple to extremely
complex.
DEBUG
Although DEBUG is the least sophisticated of the software debugging
monitors, it is quite useful with moderately complex programs and is
an effective tool for learning basic techniques.
Basic techniques
The first sample program is written in assembly language. It is a test
program that performs serial input and output and was used to debug
COMMSCOP, the serial-trace TSR presented earlier. The routine reads
from the keyboard and writes to COM1 by means of Interrupt 14H. It
also accepts incoming serial data and displays it on the screen. This
process continues until Ctrl-C is pressed on the keyboard. A serial
terminal is attached to COM1 to serve as a data source. Figure 18-9
shows the erroneous program.
──────────────────────────────────────────────────────────────────────
Figure 18-9. Incorrect serial test routine.
──────────────────────────────────────────────────────────────────────
When executed, this program produces a constant stream of zeros from
the serial port. Incoming serial data is not echoed on the screen, but
the cursor moves as if it were. Further, the Ctrl-C keystroke is not
recognized, so the only way to stop the program is to restart the
system.
An examination of the listing should reveal the errors that cause
these problems, but things do not always happen that way. For the
purposes of this case study, assume that the listing was no help.
Instrumentation is more difficult for assembly-language programs than
for programs written in higher-order languages, so in this case it is
advantageous to go directly to a debugging monitor. The monitor for
this example is DEBUG.
The first step in using DEBUG is not to invoke the monitor; rather, it
is to gather all pertinent listings, link maps, and program design
documentation. In this case, the program is so short that a link map
will not be needed; all the design documentation that exists is in the
program comments.
Now begin DEBUG by typing
C>DEBUG TESTCOMM.EXE <Enter>
The filename must be fully qualified; DEBUG makes no assumptions about
the extension. Any type of file can be examined with DEBUG, but only
files with an extension of .COM, .EXE, or .HEX are actually loaded and
made ready for execution. Since TESTCOMM is a .EXE file, DEBUG loads
it and prepares it for execution in a manner compatible with the MS-
DOS loader. Type the Display or Modify Registers command, R.
-R <Enter>
AX=0000 BX=0000 CX=0131 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=1AAD ES=1AAD SS=1ABD CS=1ACD IP=0000 NV UP EI PL NZ NA PO NC
1ACD:0000 1E PUSH DS
Notice that the SS and CS registers have been loaded to their correct
values and that SP points to the bottom of the stack. DS and ES point
to an address 100H bytes (10H paragraphs) before the stack segment.
(This is because the system sets these registers to point to the
program segment prefix [PSP] when a .EXE program is loaded.) Normally,
the program code would be responsible for loading the correct value of
DS, but this example does not use the data segment, so the program
doesn't bother. The register display also shows the instruction at the
current value of CS:IP, 1ACD:0000H. The instruction pointer was set to
this address because the END statement in the source program specified
the procedure BEGIN as the entry point and that procedure begins at
CS:IP. Note that the instruction displayed below the register
information has not yet been executed. This condition is true for all
register displays in DEBUG--IP always points to the next instruction
to be executed, so the instruction at IP has not been executed.
From the symptoms observed during program execution, it is clear that
the keyboard data is not reaching the serial port. The failure could
be in the keyboard read routine or in the serial port write routine.
This code is compact and fairly linear, so the easiest way to find out
what is going on is to trace through the first few instructions of the
program. Executing five instructions with the Trace Program Execution
command, T, will do this.
-T5 <Enter>
AX=0000 BX=0000 CX=0131 DX=0000 SP=00FE BP=0000 SI=0000 DI=0000
DS=1AAD ES=1AAD SS=1ABD CS=1ACD IP=0001 NV UP EI PL NZ NA PO NC
1ACD:0001 33C0 XOR AX,AX
AX=0000 BX=0000 CX=0131 DX=0000 SP=00FE BP=0000 SI=0000 DI=0000
DS=1AAD ES=1AAD SS=1ABD CS=1ACD IP=0003 NV UP EI PL ZR NA PE NC
1ACD:0003 50 PUSH AX
AX=0000 BX=0000 CX=0131 DX=0000 SP=00FC BP=0000 SI=0000 DI=0000
DS=1AAD ES=1AAD SS=1ABD CS=1ACD IP=0004 NV UP EI PL ZR NA PE NC
1ACD:0004 B406 MOV AH,06
AX=0600 BX=0000 CX=0131 DX=0000 SP=00FC BP=0000 SI=0000 DI=0000
DS=1AAD ES=1AAD SS=1ABD CS=1ACD IP=0006 NV UP EI PL ZR NA PE NC
1ACD:0006 B2FF MOV DL,FF
AX=0600 BX=0000 CX=0131 DX=00FF SP=00FC BP=0000 SI=0000 DI=0000
DS=1AAD ES=1AAD SS=1ABD CS=1ACD IP=0008 NV UP EI PL ZR NA PE NC
1ACD:0008 CD15 INT 15
The Trace command shows the contents of the registers as each
instruction is executed. The register contents are after
the execution of the instruction listed above the registers and the
instruction shown with the registers is the next instruction to be
executed. The first register display in this example represents the
state of affairs after the execution of the PUSH DS instruction, as
indicated by SP. The first three instructions set up the stack so that
the far return issued at the end of the program will pass control to
the PSP for termination. The next two instructions set the registers
for a Direct Console I/O MS-DOS call (AH = 060, DL = HFFH for input).
After these registers are set up, the program should execute the MS-
DOS call INT 21H. However, the next instruction to be executed is INT
15H. This is the reason the keyboard data is not being read. The code
requests INT 21, not 21H. This mistake is a common one. The
assembler's default radix is decimal, so it converted 21 into 15H.
This error can be corrected in memory from within DEBUG and, because
the instruction hasn't executed yet, the fix can be tested
immediately. To make the correction, use the Assemble Machine
Instructions command, A.
-A 8 <Enter>
1ACD:0008 int 21 <Enter>
1ACD:000A
The A 8 code instructs DEBUG to begin assembling at CS:0008H. DEBUG
prompts with the address and waits for an instruction to be entered.
The letter H is not needed after the 21 this time because DEBUG
assumes all numbers entered with the Assemble command are in
hexadecimal form. In general, any valid 8086/8087/8088 assembly-
language statement can be entered this way and translated into
executable machine code. See PROGRAMMING UTILITIES: DEBUG: A. Within
its restrictions, the Assemble command is a handy way of making
changes. The Enter Data command, E, could also have been used to
change the 15H to a 21H, but the Assemble command is safer, especially
for complex instructions. After the new instruction has been entered,
press Enter again to stop the assembly process.
There is a danger associated with making changes in memory during
debugging: The memory copy of the program is temporary; the changes
exist only in memory and when DEBUG exits, they are lost. Changes made
to .EXE and .HEX files cannot be written back to disk. To avoid
forgetting the changes, write them down. When DEBUG exits, edit the
source file immediately. Changes made to other files can be written
back to disk with DEBUG's Write File or Sectors command, W.
To be sure that the change was made correctly, use the Disassemble
(Unassemble) Program command, U, to show the instructions starting at
CS:0004H.
-U 4 <Enter>
1ACD:0004 B406 MOV AH,06
1ACD:0006 B2FF MOV DL,FF
1ACD:0008 CD21 INT 21
1ACD:000A 740C JZ 0018
1ACD:000C 3C03 CMP AL,03
1ACD:000E 7501 JNZ 0011
1ACD:0010 CB RETF
1ACD:0011 B401 MOV AH,01
1ACD:0013 BA0000 MOV DX,0000
1ACD:0016 CD14 INT 14
1ACD:0018 B403 MOV AH,03
1ACD:001A BA0000 MOV DX0000
1ACD:001D CD14 INT 14
1ACD:001F 80E401 AND AH,01
1ACD:0022 74E0 JZ 0004
The change has been correctly made. Now, to test the change, start the
program to see if characters make it out the serial port. The problem
of data from the serial port not making it to the screen remains,
however, so instead of simply starting the program, set a breakpoint
at the location in the program that handles incoming serial data
(CS:0024H). This technique allows the output section of the code to be
tested separately. The breakpoint is set using the Go command, G.
-G 24 <Enter>
AX=0130 BX=0000 CX=0131 DX=0000 SP=00FC BP=0000 SI=0000 DI=0000
DS=1AAD ES=1AAD SS=1ABD CS=1ACD IP=0024 NV UP EI PL NZ NA PO NC
1ACD:0024 B402 MOV AH,02
-U <Enter>
1ACD:0024 B402 MOV AH,02
1ACD:0026 BA0000 MOV DX,0000
1ACD:0029 CD14 INT 14
1ACD:002B B406 MOV AH,06
1ACD:002D CD21 INT 21
1ACD:002F EBD3 JMP 0004
1ACD:0031 0000 ADD [BX+SI],AL
1ACD:0033 0000 ADD [BX+SI],AL
1ACD:0035 0000 ADD [BX+SI],AL
1ACD:0037 0000 ADD [BX+SI],AL
1ACD:0039 0000 ADD [BX+SI],AL
1ACD:003B 0000 ADD [BX+SI],AL
1ACD:003D 0000 ADD [BX+SI],AL
1ACD:003F 0000 ADD [BX+SI],AL
1ACD:0041 0000 ADD [BX+SI],AL
1ACD:0043 0000 ADD [BX+SI],AL
As stated earlier, the serial port is attached to a serial terminal.
After execution of the program is started with the Go command, all
keys typed on the keyboard are displayed correctly on the terminal,
thus confirming the fix made to the INT 21H instruction. To test
serial input, a key must be pressed on the terminal, causing the
breakpoint at CS:0024H to be executed.
The fact that location CS:0024H was reached indicates that Interrupt
14H is detecting the presence of an input character. To test if the
character is now making it to the screen, a breakpoint is needed after
the write to the screen. The Disassemble command shows the
instructions starting at the current IP value. The program ends at
CS:002FH; the instructions shown after that are whatever happened to
be in memory when the program was loaded. A good place to set the next
breakpoint is CS:002FH, just after the Interrupt 21H call.
-G 2f <Enter>
AX=0600 BX=0000 CX=0131 DX=0000 SP=00FC BP=0000 SI=0000 DI=0000
DS=1AAD ES=1AAD SS=1ABD CS=1ACD IP=002F NV UP EI PL NZ NA PO NC
1ACD:002F EBD3 JMP 0004
DEBUG shows that the breakpoint was reached and the character did not
print (it should have been on the line after -G 2f), so something must
be wrong with the Interrupt 21H call. A breakpoint just before the
MS-DOS call at CS:002DH should reveal the cause of the problem.
-G 2d <Enter>
AX=0662 BX=0000 CX=0131 DX=0000 SP=00FC BP=0000 SI=0000 DI=0000
DS=1AAD ES=1AAD SS=1ABD CS=1ACD IP=002D NV UP EI PL NZ NA PO NC
1ACD:002D CD21 INT 21
The key that was entered on the serial terminal (b) is in AL, where it
was returned by Interrupt 14H. Unfortunately, it is not in DL, where
it is expected by the Direct Console I/O function (06H) of the MS-DOS
command. The MS-DOS function was simply printing a null (00H) and then
moving the cursor. An instruction (MOV DL,AL) is missing.
Fixing this problem requires the insertion of a line of code, which is
usually difficult to do inside DEBUG. The Move (Copy) Data command, M,
can be used to move the code located below the point where the
insertion is to be made down 2 bytes, but this will probably throw any
subsequent addressing off. It is usually easier to exit DEBUG, edit
the source file, and then reassemble. In this case, however, because
the instruction to be added is near the last instruction, a patch can
easily be made by entering only three instructions: the new one and
the two it destroys.
-A 2d <Enter>
1ACD:002D mov dl,al <Enter>
1ACD:002F int 21 <Enter>
1ACD:0031 jmp 4 <Enter>
1ACD:0033
-U 2b <Enter>
1ACD:002B B406 MOV AH,06
1ACD:002D 88C2 MOV DL,AL
1ACD:002F CD21 INT 21
1ACD:0031 EBD1 JMP 0004
1ACD:0033 0000 ADD [BX+SI],AL
1ACD:0035 0000 ADD [BX+SI],AL
1ACD:0037 0000 ADD [BX+SI],AL
1ACD:0039 0000 ADD [BX+SI],AL
1ACD:003B 0000 ADD [BX+SI],AL
1ACD:003D 0000 ADD [BX+SI],AL
1ACD:003F 0000 ADD [BX+SI],AL
1ACD:0041 0000 ADD [BX+SI],AL
1ACD:0043 0000 ADD [BX+SI],AL
1ACD:0045 0000 ADD [BX+SI],AL
1ACD:0047 0000 ADD [BX+SI],AL
1ACD:0049 0000 ADD [BX+SI],AL
The new line of code has been inserted and verified with the Dis-
assemble command. The fix is ready to test. The Trace command could
be used to single-step through the program to verify execution. A word
of warning is in order, however: The DEBUG Trace command should never
be used to trace an Interrupt 21H call. Once the trace enters the MS-
DOS call, it will wander around for a while and then lock the machine,
requiring a restart. Avoid this problem either by setting a breakpoint
just beyond the Interrupt 21H call or by using the Proceed Through
Loop or Subroutine command, P. The Proceed command operates in a
similar manner to the Trace command but does not trace loops, calls,
and interrupts.
Because the fix is fairly certain, use the Go command in its simple
form with no breakpoints. The program will execute without further
intervention from DEBUG.
-G <Enter>
lasdfgh
Program terminated normally
-Q <Enter>
The lasdfgh text entered on the serial terminal is displayed
correctly. When a Ctrl-C is entered from the keyboard, the program
terminates properly and DEBUG displays the message Program terminated
normally. Now exit DEBUG with the Quit command, Q.
The source code of TESTCOMM should be edited immediately so that it
reflects the two changes made temporarily under DEBUG. Figure 18-10
shows the corrected listing.
──────────────────────────────────────────────────────────────────────
Figure 18-10. Correct serial test routine.
──────────────────────────────────────────────────────────────────────
DEBUG has a rich set of commands and features. The preceding case
study shows the more common ones in their most straightforward aspect.
Some of the other commands and some useful techniques are described
below. See PROGRAMMING UTILITIES: DEBUG.
Establishing initial conditions
When a program is loaded for testing, four areas may require
initialization:
■ Registers
■ Data areas
■ Default file-control blocks (FCBs)
■ Command tail
These areas may also require changes during testing, especially when
the programmer is working around bugs or establishing different test
conditions.
Registers. Registers are ordinarily set when the program is loaded.
The values in them depend on whether a .EXE, .COM, or .HEX file was
loaded. Generally, the segment registers, the IP register, and the SP
register are set to appropriate values; with the exception of AX, BX,
and CX, the rest of the registers are set to zero. BX and CX contain
the length of the loaded file. By MS-DOS convention, when a program is
loaded, the contents of AL and AH indicate the validity of the drive
specifiers in the first and second DEBUG command-line parameters,
respectively. Each register contains zero if the corresponding drive
was valid, 01H if the drive was valid and wildcards were used, or 0FFH
if the drive was invalid.
To change the value of any register, use an alternate form of the
Register command. Enter R followed by the two-letter register name.
Only 16-bit registers can be changed, so use the X form of the
general-purpose registers:
-R AX <Enter>
DEBUG will respond with the current contents of the register and
prompt for a new value. Either enter a new hexadecimal value or press
Enter to keep the current value:
AX 0000
:FFFF <Enter>
In this example, the new value of AX is FFFFH.
When changing registers, exercise caution modifying the segment
registers. These registers control the execution of the program and
should be changed only after careful and thoughtful consideration.
The Register command can also be used to modify the CPU flags.
Data areas. Initializing or changing data areas is easy, and several
methods are provided. The Fill Memory command, F, can be used to
initialize areas of RAM. For instance,
-F 0 L400 0 <Enter>
fills DS:0000H through DS:03FFH with zero. (The absence of a segment
override causes the Fill command to use its default segment, DS.)
Entering
-F CS:100 200 1B "[Hello" 0D <Enter>
fills CS:0100H through CS:0200H with many repetitions of the string 1B
5B 48 65 6C 6C 6F 0D. (Note that an address range was specified, not a
length.)
When the wholesale changing of memory is not appropriate, the Enter
command can be used to edit a small number of locations. The Enter
command has two forms: One enters a list of bytes into the specified
memory location; the other prompts with the contents of each location
and waits for input. Either form can be used as appropriate.
Default file-control blocks and the command tail. The setting of the
default FCBs and of the command tail are related functions. When DEBUG
is entered, the first parameter following the command DEBUG is the
name of the file to be loaded into memory for debugging. If the next
two parameters are filenames, FCBs for these files are formatted at
DS:005CH and DS:006CH in the PSP. See PROGRAMMING IN THE MS-DOS
ENVIRONMENT: PROGRAMMING FOR MS-DOS: File and Record Management. If
either parameter contains a pathname, the corresponding FCB will
contain only a valid drive number; the filename field will not be
valid. All filenames and switches following the name of the file to be
debugged are considered the command tail and are saved in memory
starting at DS:0081H. The length of the command tail is in DS:0080H.
For example, entering
C>DEBUG COMMDUMP.EXE FILE1.DAT FILE2.DAT <Enter>
results in the first FCB (5CH), the second FCB (6CH), and the command
tail (81H) being loaded as follows:
-D 50
42C9:0050 CD 21 CB 00 00 00 00 00-00 00 00 00 00 46 49 4C .!...........FIL
42C9:0060 45 31 20 20 20 44 41 54-00 00 00 00 00 46 49 4C E1 DAT.....FIL
42C9:0070 45 32 20 20 20 44 41 54-00 00 00 00 00 00 00 00 E2 DAT........
42C9:0080 15 20 66 69 6C 65 31 2E-64 61 74 20 66 69 6C 65 . file1.dat file
42C9:0090 32 2E 64 61 74 20 0D 74-20 66 69 6C 65 32 2E 64 2.dat .t file2.d
42C9:00A0 61 74 20 0D 00 00 00 00-00 00 00 00 00 00 00 00 at .............
42C9:00B0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
42C9:00C0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
In this example, location DS:005CH contains an unopened FCB for file
FILE1.DAT on the current drive. Location DS:006CH contains an unopened
FCB for FILE2.DAT on the current drive. (The second FCB cannot be used
where it is and must be moved to another location before the first FCB
is opened.) Location DS:0080H contains the length of the command tail,
15H (21) bytes. The next 21 bytes are the command tail prepared by
DEBUG; they correspond exactly to what the command tail would be if
the program had been loaded by COMMAND.COM instead of by DEBUG.
The default FCBs and the command tail can also be set after the
program has been loaded, by using the Name File or Command-Tail
Parameters command, N. DEBUG treats the string of characters that
follow the Name command as the command tail: If the first two
parameters are filenames, they become the first and second FCBs,
respectively. The Name command also places the string at DS:0081H,
with the length of the string at DS:0080H. Entering the DEBUG command
-N FILE1.DAT FILE2.DAT <Enter>
produces the same results as specifying the filenames in the command
line. When employed in this manner, the Name command is useful for
initializing command-tail data that was not in the command line or for
changing the command-tail data to test different aspects of a program.
(If files are named in this manner, they are not validated until the
Load File or Sectors command, L, is used.) Note that the data
following the Name command need not be filenames; it can be any
parameters, data, or switches that the application program expects to
see.
More on breakpoints
The case study at the beginning of this section used breakpoints in
their simplest form: Only a single breakpoint was specified at a time
and the execution address was considered to be the current IP. The Go
command is also capable of setting multiple breakpoints and of
beginning execution at any address in memory. The more general form of
the Go command is
G[=address] [address [address...]]
If Go is used with no operands, execution begins at the current value
of CS:IP and no breakpoints are set. If the =address operand is used,
DEBUG sets IP to the address specified and execution then begins at
the new CS:IP. The other optional addresses are breakpoints. When
execution reaches one of these breakpoints, DEBUG stops and displays
the system's registers. As many as 10 breakpoints can be set on one Go
command, and they can be in any order.
The breakpoint addresses must be on instruction boundaries because
DEBUG replaces the instruction at each breakpoint address with an
INT 03H instruction (0CCH). DEBUG saves the replaced instructions
internally. When any breakpoint is reached, DEBUG stops execution and
restores the instructions at all the breakpoints; if no breakpoint is
reached, the instructions are not restored and the Load command must
be used to reload the original program.
The multiple-breakpoint feature of the Go command allows the tracing
of program execution when branches exist in the code. When a program
contains, for instance, a conditional jump on the zero flag, a
breakpoint can be placed in each of the two possible branches. When
the branch is reached, one of the two breakpoints will be encountered
shortly thereafter. When DEBUG displays the breakpoint, the programmer
knows which branch was taken. Moving through a program with
breakpoints at key locations is faster than using the Trace command to
execute each and every instruction.
Multiple breakpoints can also be used to home in on a bad piece of
code. This technique is particularly useful in those nasty situations
when there are no symptoms except that the system locks up and must be
restarted. When debugging a problem such as this, set breakpoints at
each of the major sections of the program and then note those
breakpoints that are executed successfully, continuing until the
system locks up. The problem lies somewhere between the last
successful breakpoint and the next breakpoint set. Now repeat the
processes, setting breakpoints between the last breakpoint and the one
that was never reached. By progressively narrowing the gap between
breakpoints, the exact offending instruction can be isolated.
Some general comments about the Go command and breakpoints:
■ After a program has reached completion and returned to MS-DOS, it
must be reloaded with the Load command before it can be executed
again. (DEBUG intercepts this return and displays Program
terminated normally.)
■ Because DEBUG replaces program instructions with an INT 03H
instruction to form breakpoints, the break address must be on an
instruction boundary. If it is not, the INT 03H will be stuck in
the middle of an instruction, causing strange and sometimes
entertaining results.
■ Breakpoints cannot be set in data, because data is not executed.
■ The target program's SS:SP registers must point to a valid stack
that has at least 6 bytes of stack space available. When the Go
command is executed, it pushes the target program's flags and CS
and IP registers onto the stack and then transfers control to the
program with an IRET instruction. Thus, if the target program's
stack is not valid or is too small, the system may crash.
■ Finally, and obviously, breakpoints cannot be set in read-only
memory (the ROM BIOS, for instance).
Using the Write commands
After a program has been debugged, fixed, and tested with DEBUG, the
temptation exists to write the patched program directly back to the
disk as a .COM file. This action is sometimes legitimate, but only
rarely. The technique will be explained in a moment, but first a
sermon:
DON'T DO IT.
One of the greatest sadnesses in a programmer's life comes when, after
a program has been running wonderfully, enhancements are made to the
source code and the recompiled program suddenly has bugs in it that
haven't been seen for months. Always make any debugging patches
permanent in the source file immediately.
Unless, of course, the source code is not available. This is the only
time saving a patched program is permissible. For example, sometimes
commercial programs require patching because the program does not
quite fit the hardware it must run on or because bugs have been found
in the program. The source of these patches is sometimes word-of-
mouth, sometimes a bulletin-board service, and sometimes the program's
manufacturer.
Even when legitimate reasons exist to save patched code, precautions
should be taken. Be very careful, meticulous, and alert as the patches
are applied. Understand each step before undertaking it. Most
important of all, always have a backup of the original unpatched
program safely on a floppy disk.
Use the Write command to write the program image to disk. A starting
address can optionally be specified; otherwise the write starts at
CS:0100H. The name of the file will be either the name specified in
the last Name command or the name of the program from the DEBUG
command line if the Name command has not been used. The number of
bytes to be written is in BX and CX, with the most significant half in
BX. These registers will have been loaded correctly when the program
was loaded, but they should be checked if the program has executed
since it was loaded.
The .EXE and .HEX file types cannot be written to disk with the Write
command. The command performs no formatting and only writes the binary
image of memory to the disk file. Thus, all programs written with
Write must be .COM files. The image of a .EXE or .HEX file can still
be written as a .COM file provided no segment fixups are required and
provided the other rules for a .COM file are followed. See PROGRAMMING
IN THE MS-DOS ENVIRONMENT: PROGRAMMING FOR MS-DOS: Structure of an
Application Program. (A segment fixup is a segment address that must
be provided by the loader when the program is originally loaded.
See PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING TOOLS:
Object Modules.) If a .EXE file containing a segment fixup is written
as a .COM file, the new file will execute correctly only when loaded
at exactly the same address as the original file, and this is
difficult to ensure for programs running under MS-DOS.
If it is necessary to patch a .EXE or .HEX file and the exact
addresses relative to the start of the file are known, use the
following procedure:
1. Rename (or better yet, copy) the file to an extension other than
.EXE or .HEX.
2. Load the program image into memory by placing the new name on
DEBUG's command line. Note that the loaded file is an image of the
disk file and is not executable.
3. Modify the program image in memory, but never try to execute the
program. Results would be unpredictable and the program image could
be damaged.
4. Write the modified image back to disk using a simple w. No other
action is needed, because the original load will have set the
filename and the correct length in BX and CX.
5. Rename the file to a name with the correct .EXE or .HEX extension.
The new name need not be the same as the original, but it should
have the same extension.
The same technique can be used to load, modify, and save data files.
Simply make sure that the file does not have an extension of .COM,
.EXE, or .HEX. The data file will be loaded at address CS:0100H.
(DEBUG treats the file much the same as a .COM file.) After patching
the data (the Enter command works best), use the Write command to
write it back to the disk.
SYMDEB
SYMDEB is an extension of DEBUG; virtually all the DEBUG commands and
techniques still work as expected. The major new feature, and the
source of the name SYMDEB, is symbolic debugging: SYMDEB can use all
public labels in a program for reference, instead of using hexadecimal
offset addresses. In addition, SYMDEB allows the use of line numbers
for reference in compatible high-order languages; source-line display
within SYMDEB is also possible for these languages. Currently, the
languages supporting these options are Microsoft FORTRAN versions 3.0
and later, Microsoft Pascal versions 3.0 and later, and Microsoft C
versions 2.0 and later. Versions 4.0 and earlier of the Microsoft
Macro Assembler (MASM) do not generate the data needed for line-number
display and source-line debugging.
In addition to symbolic debugging, SYMDEB has added several other new
features and has expanded existing DEBUG features:
■ Breakpoints have been made more sophisticated with the addition of
"sticky" breakpoints. Unlike the breakpoints set with the Go
command, sticky breakpoints remain attached to the program
throughout a SYMDEB session until they are explicitly removed.
Specific commands are supplied for listing, removing, enabling, and
disabling sticky breakpoints.
■ DEBUG's Display Memory command, D, has been extended so that data
can be displayed in different formats.
■ Full redirection is supported.
■ A stack trace feature has been added.
■ Terminate-and-stay-resident programs are supported.
■ A shell escape command has been added to allow the execution of
MS-DOS commands and programs without leaving SYMDEB and the
debugging session.
These additions allow more sophisticated debugging techniques to be
used and, in some cases, also simplify locating problems. To see the
advantages of using symbols and sticky breakpoints in debugging,
consider a type of program that is one of the most difficult to de-
bug--the TSR.
Debugging TSRs with SYMDEB
Terminate-and-stay-resident routines can be difficult to debug. They
exist in two worlds and can have bugs associated with each. At the
outset, they are usually simple programs that perform some
initialization task and then exit. At this point, they are transformed
into another type of beast entirely--resident routines that are more a
part of the operating system than of any application program. Each
form of the program must be debugged separately, using different
techniques.
The TSR routine used for this case study is the same one created
previously to serve as external instrumentation to trace serial
communications. The program was called COMMSCOP, but to avoid
confusion of that working program with the broken one presented here,
the name has been changed to BADSCOP. BADSCOP was assembled and linked
in the usual manner and then converted to a .COM file using EXE2BIN.
When it was installed, it returned normally, but at the first attempt
to issue an Interrupt 14H, the system locked up completely. Warm
booting was not sufficient to restore it, and a power-on cold boot was
required to get the system working again.
Figure 18-11 is a listing of BADSCOP. The only difference from
COMMSCOP, aside from the errors, is the addition of two PUBLIC
statements to make all the procedure names and the important data
names available to SYMDEB.
──────────────────────────────────────────────────────────────────────
Figure 18-11. An incorrect version of the serial trace utility.
──────────────────────────────────────────────────────────────────────
In order to use the symbolic debugging features of SYMDEB, a symbol
file must be built in a specific format. The SYMDEB utility MAPSYM
performs this function, using the contents of the .MAP file built by
LINK. MAPSYM is easy to use because it has only two parameters: the
.MAP file and the /L switch (which triggers verbose mode). The symbol
table for BADSCOP is built as follows:
C>MAPSYM BADSCOP <Enter>
This operation produces a symbol file called BADSCOP.SYM.
Armed with the .SYM file and the usual collection of listing and
design notes, the programmer can begin the debugging process using
SYMDEB.
The first task is to discover if the BADSCOP TSR is installing
correctly. To test this, run the .COM file under SYMDEB by typing
C>SYMDEB BADSCOP.SYM BADSCOP.COM <Enter>
Note the order in which operands are passed to SYMDEB--it is not the
order that would be expected. All switches (none were used here) must
immediately follow the word SYMDEB. These switches must be followed in
turn by the fully qualified names of any symbol files (in this case,
BADSCOP.SYM). Only then is the name of the file to be debugged given.
If BADSCOP expected any parameters in the command tail, they would be
last. This potential need for command-tail data is the reason the name
of the file to be debugged follows the name of the symbol file. SYMDEB
knows that the first non-.SYM file it encounters is the file to be
loaded; the parameters that follow the filename may be of any form and
number.
When SYMDEB begins, it displays
Microsoft (R) Symbolic Debug Utility Version 4.00
Copyright (C) Microsoft Corp 1984, 1985. All rights reserved.
Processor is [80286]
The debugger identifies itself and then notes the type of CPU it is
running on--in this case, an Intel 80286. The Display or Modify
Registers command, R, gives the same display that DEBUG gives, with
one exception.
-R <Enter>
AX=0000 BX=0000 CX=0133 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1FD0 SS=1FD0 CS=1FD0 IP=0100 NV UP EI PL NZ NA PO NC
CSEG:INITIALIZE:
1FD0:0100 E90701 JMP VECTOR_INIT
The instruction at CS:IP, JMP, is now preceded by the information that
the instruction is at label INITIALIZE within segment CSEG. An exam-
ination of Figure 18-11 shows that this is indeed the case.
To check that all the symbols requested with the PUBLIC statement are
present, use the X?* form of the Examine Symbol Map command.
-X?* <Enter>
CSEG: (1FD0)
0100 INITIALIZE 0103 OLD_COMM_INT 0107 COUNT 0109 STATUS
010A PORT 010B BUFPNTR 010D COMMSCOPE 018F CONTROL
020A VECTOR_INIT
The display shows that the value of CSEG (1FD0H) matches the current
value of CS. The offset values shown for the procedure names and data
names match the numbers from an assembled listing. Because this is a
.COM file, there is only one segment. If there had been other
segments--a data segment, for instance--they would have been shown
with their values and associated labels and offsets.
The purpose of this test is to determine whether the problems this
program is having are caused by an incorrect installation. First, use
the Trace Program Execution command, T, to trace through the first few
steps.
-T7 <Enter>
AX=0000 BX=0000 CX=0133 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1FD0 SS=1FD0 CS=1FD0 IP=020A NV UP EI PL NZ NA PO NC
CSEG:VECTOR_INIT:
1FD0:020A B435 MOV AH,35 ;'5'
AX=3500 BX=0000 CX=0133 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1FD0 SS=1FD0 CS=1FD0 IP=020C NV UP EI PL NZ NA PO NC
1FD0:020C B014 MOV AL,14
AX=3514 BX=0000 CX=0133 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1FD0 SS=1FD0 CS=1FD0 IP=020E NV UP EI PL NZ NA PO NC
1FD0:020E CD21 INT 21 ;Get Interrupt Vector
AX=3514 BX=1375 CX=0133 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=0210 NV UP EI PL NZ NA PO NC
1FD0:0210 891E0301 MOV [OLD_COMM_INT],BX DS:0103=0000
AX=3514 BX=1375 CX=0133 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=0214 NV UP EI PL NZ NA PO NC
1FD0:0214 8CC0 MOV AX,ES
AX=1567 BX=1375 CX=0133 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=0216 NV UP EI PL NZ NA PO NC
1FD0:0216 A30501 MOV [OLD_COMM_INT+02 (0105)],AX DS:0105=0000
AX=1567 BX=1375 CX=0133 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=0219 NV UP EI PL NZ NA PO NC
1FD0:0219 BA0D01 MOV DX,010D
This part of the program uses Interrupt 21H Function 35H to obtain the
current vector for Interrupt 14H. Note that, unlike DEBUG, SYMDEB
coasts right through an Interrupt 21H call with no problems. It not
only knows enough not to make the call but also displays the type of
function call being made, based on the value in AH.
To make sure that the correct vector for the old Interrupt 14H handler
has been stored, use the Display Doublewords command, DD, in
conjunction with a symbol name.
-DD OLD_COMM_INT L1 <Enter>
1FD0:01030 1567:1375
This is the correct vector address (1567:1375H). Now trace through
the next part of the program, which establishes the new vectors for
interrupts.
-T8 <Enter>
AX=1567 BX=1375 CX=0133 DX=010D SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=021C NV UP EI PL NZ NA PO NC
1FD0:021C B425 MOV AH,25 ;'%'
AX=2567 BX=1375 CX=0133 DX=010D SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=021E NV UP EI PL NZ NA PO NC
1FD0:021E B014 MOV AL,14
AX=2514 BX=1375 CX=0133 DX=010D SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=0220 NV UP EI PL NZ NA PO NC
1FD0:0220 CD21 INT 21 ;Set Vector
AX=2514 BX=1375 CX=0133 DX=010D SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=0222 NV UP EI PL NZ NA PO NC
1FD0:0222 BA8F01 MOV DX,018F
AX=2514 BX=1375 CX=0133 DX=018F SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=0225 NV UP EI PL NZ NA PO NC
1FD0:0225 B425 MOV AH,25 ;'%'
AX=2514 BX=1375 CX=0133 DX=018F SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=0227 NV UP EI PL NZ NA PO NC
1FD0:0227 B060 MOV AL,60 ;'`'
AX=2560 BX=1375 CX=0133 DX=018F SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=0229 NV UP EI PL NZ NA PO NC
1FD0:0229 CD21 INT 21 ;Set Vector
AX=2560 BX=1375 CX=0133 DX=018F SP=FFFE BP=0000 SI=0000 DI=0000
DS=1FD0 ES=1567 SS=1FD0 CS=1FD0 IP=022B NV UP EI PL NZ NA PO NC
1FD0:022B B80031 MOV AX,3100
Examination of these trace steps shows that all went normally. The new
Interrupt 14H vector has been established at COMMSCOPE; the vector for
the new Interrupt 60H has also been correctly installed. Use the Go
command, G, to allow the program to continue to termination and then
use the Quit command, Q, to exit SYMDEB.
-G <Enter>
Program terminated and stayed resident (0)
-Q <Enter>
SYMDEB displays the information that the program terminated with a
completion code of zero and stayed resident. This is as it should be,
and the conclusion is that the installation portion of this TSR is
running properly. The problem must be in the real-time execution of
the program.
Debugging the resident portion of a TSR is complicated but not
especially difficult. A simple program is used to exercise the TSR,
and it is this program that is debugged. As this driver program
exercises the TSR, the tracing process continues into the resident
routine.
Because symbol tables exist for the TSR, symbolic debugging can be
used to follow its execution.
The driver program will be TESTCOMM, shown in Figure 18-10. To make
the program more easily usable by SYMDEB, one line has been added
before the first SEGMENT statement:
PUBLIC BEGIN,MAINLOOP,SENDCOMM,TESTCOMM
Using the .MAP file produced by LINK, the MAPSYM routine creates
TESTCOMM.SYM. TESTCOMM can now be invoked with two symbol files:
C>SYMDEB TESTCOMM.SYM BADSCOP.SYM TESTCOMM.EXE <Enter>
SYMDEB will load both symbol files and then load TESTCOMM.EXE. Be-
cause the name of the TESTCOMM.SYM file matches the name of the pro-
gram being loaded, SYMDEB makes TESTCOMM.SYM the active symbol file.
Use the Register command to show that the test program was properly
loaded.
-R <Enter>
AX=0000 BX=0000 CX=0133 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=38EE ES=38EE SS=38FE CS=390E IP=0000 NV UP EI PL NZ NA PO NC
CSEG:BEGIN:
390E:0000 1E PUSH DS
Then use the Examine Symbol Map command to determine whether the
symbol files were loaded correctly. The form X* lists all the symbol
maps and their segments; the form X?* lists all the symbols for the
current symbol map and segment.
-X* <Enter>
[38FE TESTCOMM]
[390E CSEG]
0000 BADSCOP
0000 CSEG
-X?* <Enter>
CSEG: (390E)
0000 BEGIN 0004 MAINLOOP 0011 SENDCOMM 0018 TESTCOMM
The current symbol map and segment are shown in square brackets. The
symbol map for BADSCOP is also present but not selected. Note that
there are no values associated with BADSCOP in the listing produced by
the X?* command, because all the symbols currently available to SYMDEB
are shown and only the symbols in TESTCOMM's CSEG are available (that
is, TESTCOMM.SYM is the only active symbol file).
Recall that the BADSCOP TSR loaded normally but locked the system up
at the first attempt to issue an Interrupt 14H. This behavior
indicates that the problem is associated with an Interrupt 14H call.
TESTCOMM repeatedly makes the system fail, but which of the Interrupt
14H calls within TESTCOMM is causing the trouble is not known. The
most straightforward approach would be to put a breakpoint just before
each Interrupt 14H instruction. Use the Disassemble (Unassemble)
command, U, to find the location of all Interrupt 14H calls.
-U MAINLOOP L19 <Enter>
CSEG:MAINLOOP:
390E:0004 B406 MOV AH,06
390E:0006 B2FF MOV DL,FF
390E:0008 CD21 INT 21
390E:000A 740C JZ TESTCOMM
390E:000C 3C03 CMP AL,03
390E:000E 7501 JNZ SENDCOMM
390E:0010 CB RETF
CSEG:SENDCOMM:
390E:0011 B401 MOV AH,01
390E:0013 BA0000 MOV DX,BADSCOP!CSEG
390E:0016 CD14 INT 14
CSEG:TESTCOMM:
390E:0018 B403 MOV AH,03
390E:001A BA0000 MOV DX,BADSCOP!CSEG
390E:001D CD14 INT 14
390E:001F 80E401 AND AH,01
390E:0022 74E0 JZ MAINLOOP
390E:0024 B402 MOV AH,02
390E:0026 BA0000 MOV DX,BADSCOP!CSEG
390E:0029 CD14 INT 14
390E:002B B406 MOV AH,06
390E:002D 8AD0 MOV DL,AL
390E:002F CD21 INT 21
390E:0031 EBD1 JMP MAINLOOP
The Disassemble request starts at MAINLOOP and acts on the next 25
(19H) instructions. SYMDEB displays symbol names instead of numbers
whenever it can. However, it does get confused from time to time, so a
grain of salt might be needed when reading the disassembly. Notice,
for instance, the MOV DX,0 instructions at offsets 13H, 1AH, and 26H.
SYMDEB has decided that what is being moved is not zero, but
BADSCOP!CSEG. (The ! identifies a mapname in the same way a : defines
a segment.) In this case, SYMDEB searched its map tables for an
address of zero and found one at CSEG in BADSCOP. This segment has the
address of zero because it has not been initialized.
Ignoring the name confusions, the disassembly clearly shows the three
INT 14H instructions at offsets 16H, 1DH, and 29H. Use the Set
Breakpoints command, BP, to set a sticky, or permanent, breakpoint at
each of these locations. In this way, any Interrupt 14H call issued by
TESTCOMM will be intercepted before it executes. Use the List
Breakpoints command, BL, to verify the breakpoints.
-BP 16 <Enter>
-BP 1D <Enter>
-BP 29 <Enter>
-BL <Enter>
0 e 390E:0016 [CSEG:SENDCOMM+05 (0016)]
1 e 390E:001D [CSEG:TESTCOMM+05 (001D)]
2 e 390E:0029 [CSEG:TESTCOMM+11 (0029)]
The List Breakpoints command shows that breakpoint 0 is enabled
and set to SENDCOMM+05, or CS:0016H. Likewise, breakpoint 1 is at
CS:001DH and breakpoint 2 is at CS:0029H. It is important to trap
on an Interrupt 14H so that the subsequent actions of the Interrupt
14H service routine can be traced. Now allow the program to execute
until it encounters a breakpoint.
-G <Enter>
AX=0300 BX=0000 CX=0133 DX=0000 SP=00FC BP=0000 SI=0000 DI=0000
DS=38EE ES=38EE SS=38FE CS=390E IP=001D NV UP EI PL ZR NA PE NC
390E:001D CD14 INT 14 ;BR1
The first Interrupt 14H encountered is the one at the second
breakpoint, breakpoint 1, as can be seen from the address at which
execution broke. Also, SYMDEB was kind enough to include the comment
;BR1 on the disassembled line, indicating that this is Break Request
1. The instruction at this location is a request for serial port
status (AH = 3) and the registers are loaded correctly. Execution can
now be passed to the TSR by simply executing the current instruction.
(Remember that the instruction displayed at a breakpoint has not yet
been executed.)
-T <Enter>
AX=0300 BX=0000 CX=0133 DX=0000 SP=00F6 BP=0000 SI=0000 DI=0000
DS=38EE ES=38EE SS=38FE CS=1FD0 IP=010D NV UP DI PL ZR NA PE NC
1FD0:010D 2EF606090101 TEST Byte Ptr CS:[0109],01 CS:0109=00
The single Trace command has moved execution into the TSR. Note that
the Interrupt 14H has changed the value of CS and jumped to location
10DH off the new CS. This location contains the first instruction of
the COMMSCOPE procedure in the TSR. SYMDEB does not know that a
different segment is being executed and must be instructed to use a
different map table. Use the Open Symbol Map command, XO, to do this,
instructing SYMDEB to set the active map table to BADSCOP!.
-XO BADSCOP! <Enter>
-X?* <Enter>
CSEG: (0000)
0100 INITIALIZE 0103 OLD_COMM_INT 0107 COUNT 0109 STATUS
010A PORT 010B BUFPNTR 010D COMMSCOPE 018F CONTROL
020A VECTOR_INIT
The X?* command shows that the BADSCOP symbols are now the current
map. They are not usable, however, because the value of CSEG--zero--
needs to be changed to the current CS register. To correct this, use
the SYMDEB Set Symbol Value command, Z. This command can set any
symbol in the current map table to any value; the value can be a
number, another symbol, or the contents of a register. In this case,
set the value of CSEG in BADSCOP! to the current contents of the CS
register.
-Z CSEG CS <Enter>
-X* <Enter>
38FE TESTCOMM
390E CSEG
[0000 BADSCOP]
[1FD0 CSEG]
The X* command confirms that BADSCOP! is now the selected symbol map
and that the CSEG within it has the value 1FD0H. The CSEG segment in
TESTCOMM is an entirely different entity and still has its correct
value, which will be valid when the TSR returns.
With the symbols set, the debugging can begin by tracing the first few
instructions. Because COMMSCOPE is not currently active, the routine
should quickly pass the processing on to the old interrupt handler.
-T5 <Enter>
AX=0300 BX=0000 CX=0133 DX=0000 SP=00F6 BP=0000 SI=0000 DI=0000
DS=38EE ES=38EE SS=38FE CS=1FD0 IP=0113 NV UP DI PL ZR NA PE NC
1FD0:0113 7476 JZ COMMSCOPE+7E (018B)
AX=0300 BX=0000 CX=0133 DX=0000 SP=00F6 BP=0000 SI=0000 DI=0000
DS=38EE ES=38EE SS=38FE CS=1FD0 IP=018B NV UP DI PL ZR NA PE NC
1FD0:018B FF2E0301 JMP FAR [0103] DS:0103=0000
AX=0300 BX=0000 CX=0133 DX=0000 SP=00F6 BP=0000 SI=0000 DI=0000
DS=38EE ES=38EE SS=38FE CS=0000 IP=0000 NV UP DI PL ZR NA PE NC
0000:0000 381E6715 CMP [1567],BL DS:1567=00
AX=0300 BX=0000 CX=0133 DX=0000 SP=00F6 BP=0000 SI=0000 DI=0000
DS=38EE ES=38EE SS=38FE CS=0000 IP=0004 NV UP DI PL ZR NA PE NC
0000:0004 BC2CE1 MOV SP,E12C
AX=0300 BX=0000 CX=0133 DX=0000 SP=E12C BP=0000 SI=0000 DI=0000
DS=38EE ES=38EE SS=38FE CS=0000 IP=0007 NV UP DI PL ZR NA PE NC
0000:0007 2F DAS
STATUS is tested with a mask of 01H at CS:010DH; the test sets the
zero flag, indicating that tracing is disabled. The JZ to
COMMSCOPE+7E (CS:018BH) is taken. At this address is a far jump to
the old Interrupt 14H handler at 1567:1375H. The jump is taken and
then disaster strikes. Instead of going to the correct address,
processing is suddenly at 0000:0000H. Any wild jump is dangerous, but
a far jump into low memory is exceptionally so. This explains the
system's locking up and requiring a cold boot to recover.
Now that the bug has been caught in the act, it should be a simple
matter to determine what went wrong. When the BADSCOP TSR installed
itself, it was seen to place the correct offset address at 0103H. Yet
whenever the resident portion of the TSR tries to use the value at
that address, it finds all zeros. The initialization routine placed
the address at the symbol OLD_COMM_INT (1FD0:0103H). If that location
is examined, the following is found:
-DD OLD_COMM_INT L1 <Enter>
1FD0:0103 1567:1375
This is the correct address. Why, then, did the programs find zero
there? Use the Display Doublewords command to look at the same memory
location again, this time using the specific address 0103H rather than
a program symbol.
-DD 103 L1 <Enter>
38EE:0103 0000:0000
The dump of OLD_COMM_INT looked at 1FD0:0103H, but the simple dump
looked at 38EE:0103H. The explanation is clear when the values of the
registers just before the far jump are examined. The CS register
contains 1FD0H and the DS register contains 38EEH.
This is the problem--there is a missing CS override on the indirect
jump command. When the TSR installed itself, CS and DS were the same
because it was a .COM file. When the TSR is entered as the result of
an interrupt call, only CS is set; DS remains what it was in the
calling program. Without an override, the CPU assumed that the address
of the destination of the far call was located at offset 103H from the
DS register. This offset, unfortunately, contained zeros, and the
program locked up the system.
The problem is now easily corrected. Exit SYMDEB with the Quit command
and edit the program source so that the offending line reads
OLD_JUMP:
JMP CS:OLD_COMM_INT
Debugging C programs with SYMDEB
One of SYMDEB's finest features is the ability to debug with source-
line data from programs written in Microsoft C, Pascal, and FORTRAN.
The actual lines of C or FORTRAN can be included in the debugging
display, and the addresses for breakpoints show which line of code the
breakpoints are in. Combined with symbolic debugging, these features
provide a powerful tool that can significantly reduce debugging time
for programs written in a supported language.
The following rather complicated case illustrates SYMDEB at its best.
The program BADSCOP from the previous example was not completely
debugged. Although the patch to the BADSCOP code at OLD_JUMP: did
correct the disastrous problem that caused the system to lock up,
running the program in a realistic test situation reveals that a
subtle problem still remains that might be in either BADSCOP or one of
the support programs.
Before we investigate the problem, a quick review of the programs in
the COMMSCOP system is in order. At the heart of the system is the
Interrupt 14H intercept program COMMSCOP. When executed, this program
installs itself as a TSR and intercepts all Interrupt 14H calls. (The
incorrect version of the COMMSCOP program is called BADSCOP.) The
installed COMMSCOP TSR passes all Interrupt 14H calls on to the real
service routine in the ROM BIOS until it is commanded to start
tracing. The COMMSCMD routine controls tracing. This control routine
can request that COMMSCOP start, stop, or resume tracing for a
specific serial port. These commands are facilitated through Interrupt
60H, which is recognized by the COMMSCOP TSR as a command request.
When tracing is started, the trace buffer is emptied by zeroing the
trace count and setting the buffer pointer to the first buffer
location. When tracing is stopped by COMMSCMD's STOP command, a marker
is placed in the buffer to indicate the end of a trace segment.
Tracing can be resumed with COMMSCMD's RESUME command. Resuming a
trace preserves collected data and places new trace data after the
marker in the trace buffer. The RESUME command differs from the START
command in that the buffer is not emptied.
Now the problem: When the serial data tracing is started with COMMSCMD
(see Figure 18-5), data is collected normally. When COMMSCMD issues a
STOP command and the data is displayed with COMMDUMP (see Figure
18-7), the data appears normal. The traced data ends with a stop mark
just as it should. However, the RESUME command of COMMSCMD causes the
stop mark to be overwritten with collected data. After this, whenever
COMMDUMP displays data an extra byte appears at the end of the data.
The problem could be with either BADSCOP or COMMSCMD. SYMDEB has the
facilities to debug both the routines at once.
The first step in the debugging process is, as usual, to gather all
the listings and design documentation. As a part of this process, the
symbol tables needed for SYMDEB must be prepared. The process of
preparing a symbol table for BADSCOP has already been explained;
however, preparing the SYMDEB input and supporting listings for a C
program is slightly more complicated.
First, when the C program is compiled, three switches must be
specified. (C switches are case sensitive and must be entered exactly
as shown.)
C>MSC /Fc /Zd /Od COMMSCMD; <Enter>
The /Zd switch produces an object file containing line-number
information that corresponds to the line numbers of the source file.
The /Od switch disables optimization that involves complex code
rearrangement; localized optimization, peephole optimization, and
other simple forms of optimization are still performed. The /Od switch
is not required, but code rearrangement can make the resulting object
code more difficult to debug.
The /Fc switch invokes a feature of C that is especially important for
debugging with SYMDEB: a listing that contains the C source lines and
the generated assembler code intermixed. The file is a .COD file; the
command line shown above would produce the file COMMSCMD.COD. Figure
18-12 shows the contents of COMMSCMD.COD.
──────────────────────────────────────────────────────────────────────
Figure 18-12. COMMSCMD.COD.
──────────────────────────────────────────────────────────────────────
After the C program is compiled, it must be linked using the /LI
switch to indicate that the line number information is to be
maintained:
C>LINK COMMSCMD /MAP /LI; <Enter>
The /MAP switch is still required to generate a map file of public
names for use in building the symbol file, which is created in the
usual manner:
C>MAPSYM COMMSCMD <Enter>
Everything needed to debug COMMSCMD and BADSCOP is now available. The
first test is an attempt to start tracing. To invoke SYMDEB, type
C>SYMDEB COMMSCMD.SYM BADSCOP.SYM COMMSCMD.EXE START 1 <Enter>
SYMDEB first loads the symbol files for COMMSCMD and BADSCOP and then
loads the .EXE file for COMMSCMD. BADSCOP is already in memory, having
been loaded by simply running it. (It then stays resident.) The last
two entries in the command line load the command tail for COMMSCMD
with a start request for COM1. SYMDEB responds with
Microsoft (R) Symbolic Debug Utility Version 4.00
Copyright (C) Microsoft Corp 1984, 1985. All rights reserved.
Processor is [80286]
Use the Register and Examine Symbol Map commands to display the
initial register values and symbol table information.
-R <Enter>
AX=0000 BX=0000 CX=1928 DX=0000 SP=0800 BP=0000 SI=0000 DI=0000
DS=2CA0 ES=2CA0 SS=2E85 CS=2CB0 IP=010F NV UP EI PL NZ NA PO NC
_TEXT:__astart:
2CB0:010F B430 MOV AH,30 ;'0'
-X* <Enter>
[2CB0 COMMSCMD]
[2CB0 _TEXT]
2E08 DGROUP
0000 BADSCOP
0000 CSEG
-X?* <Enter>
9876 __acrtused 9876 __acrtmsg
_TEXT: (2CB0)
0010 _main 00F6 _atoi
00F9 __chkstk 010F __astart 01AB __cintDIV 01AE __amsg_exit
01B9 _int86 023A _printf 0270 _strcmpi 0270 _stricmp
02C2 __stbuf 0361 __ftbuf 03E7 __catox 043C __nullcheck
0458 __cinit 0507 _exit 051E __exit 054A __ctermsub
0572 __dosret0 057A __dosretax 0586 __maperror 05BA __NMSG_TEXT
05EA __NMSG_WRITE 0613 __output 0E22 __setargv 0F07 __setenvp
0F6D __flsbuf 1098 __fassign 1098 __cropzeros 1098 __positive
1098 __forcdecpt 1098 __cfltcvt 109B _fflush 1103 _isatty
1125 __myalloc 1167 _strlen 1182 _ultoa 118C __fptrap
1192 _flushall 11C3 _free 11C3 __nfree 11D1 _malloc
11D1 __nmalloc 1217 _write 12F1 __cltoasub 12FD __cxtoa
1351 __amalloc 1432 __amexpand 146C __amlink 148E __amallocbrk
14AD _brkctl
DGROUP: (2E08)
0094 STKHQQ 0096 __asizds 0098 __atopsp
009A __abrktb 00EA __abrktbe 00EA __abrkp 00EC __iob
018C __iob2 0204 __lastiob 0212 __aintdiv 0216 __fac
021E _errno 0220 __umaskval 0222 __pspadr 0224 __psp
0226 __osmajor 0226 __dosvermajor 0227 __osminor 0227 __dosverminor
0228 __oserr 0228 __doserrno 022A __osfile 023E ___argc
0240 ___argv 0242 _environ 0244 __child 0246 __csigtab
0278 __cflush 027A __asegds 0286 __aseg1 0288 __asegn
028A __asegr 028C __amblksiz 0292 __fpinit 03A8 _edata
03D0 __bufout 05D0 __bufin 07D0 _end
The Register command shows that the first instruction to be executed
will be at symbol astart in the _TEXT segment. (Note that C puts a
single underscore in front of all public library and routine names; a
double underscore indicates routines for C's internal use.) The
Examine Symbol Map command reveals that the symbol map COMMSCMD! has
two segments, _TEXT and DGROUP, with _TEXT currently selected. The
segment in BADSCOP!, CSEG, has no value assigned to it because SYMDEB
doesn't know where it is; one of the debugging tasks is to determine
the location of CSEG.
C places initialization and preamble code at the front of its object
modules. This code can be skipped during debugging, so this example
begins at the label _main. Examination of the code at this label using
the Disassemble command reveals the following:
-U _main <Enter>
commscmd.C
29: int argc;
_TEXT:_main:
2CB0:0010 55 PUSH BP
2CB0:0011 8BEC MOV BP,SP
2CB0:0013 B82200 MOV AX,0022
2CB0:0016 E8E000 CALL __chkstk
2CB0:0019 57 PUSH DI
This disassembly shows the way source-line information is displayed.
These instructions are generated by line 29 of COMMSCMD.C. When the
disassembly is compared with the listing in Figure 18-12, the same
instructions are seen. However, their addresses are different. The
addresses in the disassembly are relative to the start of the segment
_TEXT, but the addresses in the listing are relative to the start of
_main. SYMDEB allows address references to be made relative to a
symbol, so breakpoints can be set as displacements from _main and the
addresses shown in the listing can be used.
Because the location of the problem being debugged is not known,
breakpoints must be placed strategically throughout COMMSCMD to trace
the execution of the program. Use the Set Breakpoints command to set
the breakpoints.
-BP _main+1e <Enter>
-BP _main+36 <Enter>
-BP _main+56 <Enter>
-BP _main+76 <Enter>
-BP _main+7b <Enter>
-BP _main+9c <Enter>
-BP _main+b7 <Enter>
-BP _main+e5 <Enter>
-BL <Enter>
0 e 2CB0:002E [_TEXT:_main+1E (002E)] commscmd.C:41
1 e 2CB0:0046 [_TEXT:_main+36 (0046)] commscmd.C:42
2 e 2CB0:0066 [_TEXT:_main+56 (0066)] commscmd.C:44
3 e 2CB0:0086 [_TEXT:_main+76 (0086)] commscmd.C:46
4 e 2CB0:008B [_TEXT:_main+7B (008B)] commscmd.C:49
5 e 2CB0:00AC [_TEXT:_main+9C (00AC)] commscmd.C:53
6 e 2CB0:00C7 [_TEXT:_main+B7 (00C7)] commscmd.C:58
7 e 2CB0:00F5 [_TEXT:_main+E5 (00F5)] commscmd.C:63
The List Breakpoints command shows the breakpoint addresses in three
ways: first the absolute segment:offset address, then the displacement
from the label _main, and finally the line number in COMMSCMD.C.
The first part of the COMMSCMD program decodes the arguments and sets
the appropriate values for cmd and port. If there are no arguments,
this decoding is skipped; if there are arguments, the decoding begins
at line 41, so the first breakpoint is set there. If the criterion of
line 41 is met (the first argument is STOP), then line 42 is executed.
The second breakpoint is set there. Reaching the second breakpoint
means that a STOP command was properly decoded. If the command was not
STOP, execution continues at line 43. If this test is passed, line 44
is executed. This is the location of the third breakpoint. If the test
at line 44 fails but the one at line 45 is passed, then the breakpoint
at line 46 is executed. Whether or not one of the tests passes,
execution ends up at line 49. At this point, the program tests for the
presence of a second operand. If there is a second operand, execution
traps at line 53, where the program decrements the port number to put
it in the proper form for the Interrupt 60H handler. Execution will
then always stop in line 58, just before the call to _int86. (_int86
is a library routine that loads registers and executes INT
instructions.)
When the program is run with START 1 in the command tail, it gives the
following results:
G <Enter>
AX=0022 BX=0F82 CX=0019 DX=0098 SP=0F7E BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=002E NV UP EI PL NZ NA PO NC
41: if (0 == stricmp(argv[1],"STOP"))
2CB0:002E B83600 MOV AX,0036 ;BR0
-G <Enter>
AX=0000 BX=415A CX=0000 DX=0098 SP=0F7E BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=0066 NV UP EI PL ZR NA PE NC
44: cmd = 1;
2CB0:0066 C746FC0100 MOV Word Ptr [BP-04],0001 ;BR2
SS:0FA0=0000
-G <Enter>
AX=0000 BX=415A CX=0000 DX=0098 SP=0F7E BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=008B NV UP EI PL ZR NA PE NC
49: if (argc == 3)
2CB0:008B 837E0403 CMP Word Ptr [BP+04],+03 ;BR4
SS:0FA8=0003
-G <Enter>
AX=0001 BX=00D0 CX=0000 DX=0000 SP=0F7E BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=00AC NV UP EI PL NZ NA PO NC
5 port = port-1;
2CB0:00AC FF4EFA DEC Word Ptr [BP-06] ;BR5
SS:0F9E=0001
-G <Enter>
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F78 BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=00C7 NV UP EI PL ZR NA PE NC
2CB0:00C7 E8EF00 CALL _int86 ;BR6
The first break occurs at line 41, indicating that one or more
arguments were present in the command line. The next break is at line
44, where the program sets the cmd code for Interrupt 60H to 1, the
correct value for a start request. The next break occurs at line 49,
where the program checks the number of arguments. If this number is 3,
then there is a second argument in the command line. (Remember that,
in C, the first argument is the name of the routine, so an argument
count of 3 actually means that there are 2 arguments present.) The
number of arguments is at BP+04, or SS:0FA8H, and it is indeed 3.
Therefore, the next break is at line 53. The program decrements the
current value of port, leaving a value of 0, which is what Interrupt
60H expects to see for COM1.
Continuing execution causes a break just before the call to _int86. To
validate that the Interrupt 60H call is being made correctly, set a
breakpoint just before the INT 60H instruction is issued.
Unfortunately, no listing of _int86 is available, so no alternative
exists but to trace the execution of the routine until the INT
instruction is issued. The details of the processing are of no
interest to this debugging session, so they can be ignored until an
INT 60H is seen. (The trace offers a great deal of information about
how C interfaces with subroutines. Studying the trace would be
educational but is beyond the scope of this example.)
-T 5 <Enter>
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F76 BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01B9 NV UP EI PL ZR NA PE NC
_TEXT:_int86:
2CB0:01B9 55 PUSH BP
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F74 BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01BA NV UP EI PL ZR NA PE NC
2CB0:01BA 8BEC MOV BP,SP
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F74 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01BC NV UP EI PL ZR NA PE NC
2CB0:01BC 56 PUSH SI
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F72 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01BD NV UP EI PL ZR NA PE NC
2CB0:01BD 57 PUSH DI
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F70 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01BE NV UP EI PL ZR NA PE NC
2CB0:01BE 83EC0A SUB SP,+0A
-T 5 <Enter>
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01C1 NV UP EI PL NZ AC PE NC
2CB0:01C1 C646F6CD MOV Byte Ptr [BP-0A],CD SS:0F6A=BE
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01C5 NV UP EI PL NZ AC PE NC
2CB0:01C5 8B4604 MOV AX,[BP+04] SS:0F78=0060
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01C8 NV UP EI PL NZ AC PE NC
2CB0:01C8 8846F7 MOV [BP-09],AL SS:0F6B=01
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01CB NV UP EI PL NZ AC PE NC
2CB0:01CB 3C25 CMP AL,25 ;'%'
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01CD NV UP EI PL NZ AC PO NC
2CB0:01CD 740A JZ _int86+20 (01D9)
-T 5 <Enter>
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01CF NV UP EI PL NZ AC PO NC
2CB0:01CF 3C26 CMP AL,26 ;'&'
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01D1 NV UP EI PL NZ AC PE NC
2CB0:01D1 7406 JZ _int86+20 (01D9)
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01D3 NV UP EI PL NZ AC PE NC
2CB0:01D3 C646F8CB MOV Byte Ptr [BP-08],CB SS:0F6C=B0
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01D7 NV UP EI PL NZ AC PE NC
2CB0:01D7 EB0C JMP _int86+2C (01E5)
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01E5 NV UP EI PL NZ AC PE NC
2CB0:01E5 8C56F4 MOV [BP-0C],SS SS:0F68=0F74
-T 5 <Enter>
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01E8 NV UP EI PL NZ AC PE NC
2CB0:01E8 8D46F6 LEA AX,[BP-0A] SS:0F6A=60CD
AX=0F6A BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01EB NV UP EI PL NZ AC PE NC
2CB0:01EB 8946F2 MOV [BP-0E],AX SS:0F66=0060
AX=0F6A BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01EE NV UP EI PL NZ AC PE NC
2CB0:01EE 8B7E06 MOV DI,[BP+06] SS:0F7A=0F82
AX=0F6A BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=0F82
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01F1 NV UP EI PL NZ AC PE NC
2CB0:01F1 8B05 MOV AX,[DI] DS:0F82=0100
AX=0100 BX=00D0 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=0F82
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01F3 NV UP EI PL NZ AC PE NC
2CB0:01F3 8B5D02 MOV BX,[DI+02] DS:0F84=0000
-T 5 <Enter>
AX=0100 BX=0000 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=0F82
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01F6 NV UP EI PL NZ AC PE NC
2CB0:01F6 8B4D04 MOV CX,[DI+04] DS:0F86=0000
AX=0100 BX=0000 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=0F82
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01F9 NV UP EI PL NZ AC PE NC
2CB0:01F9 8B5506 MOV DX,[DI+06] DS:0F88=0000
AX=0100 BX=0000 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0089 DI=0F82
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01FC NV UP EI PL NZ AC PE NC
2CB0:01FC 8B7508 MOV SI,[DI+08] DS:0F8A=0000
AX=0100 BX=0000 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0000 DI=0F82
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=01FF NV UP EI PL NZ AC PE NC
2CB0:01FF 8B7D0A MOV DI,[DI+0A] DS:0F8C=0000
AX=0100 BX=0000 CX=0000 DX=0000 SP=0F66 BP=0F74 SI=0000 DI=0000
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=0202 NV UP EI PL NZ AC PE NC
2CB0:0202 55 PUSH BP
-T 5 <Enter>
AX=0100 BX=0000 CX=0000 DX=0000 SP=0F64 BP=0F74 SI=0000 DI=0000
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=0203 NV UP EI PL NZ AC PE NC
2CB0:0203 83ED0E SUB BP,+0E
AX=0100 BX=0000 CX=0000 DX=0000 SP=0F64 BP=0F66 SI=0000 DI=0000
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=0206 NV UP EI PL NZ AC PE NC
2CB0:0206 FF5E00 CALL FAR [BP+00] SS:0F66=0F6A
AX=0100 BX=0000 CX=0000 DX=0000 SP=0F60 BP=0F66 SI=0000 DI=0000
DS=2E08 ES=2E08 SS=2E08 CS=2E08 IP=0F6A NV UP EI PL NZ AC PE NC
2E08:0F6A CD60 INT 60
AX=0100 BX=0000 CX=0000 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=0000
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0190 NV UP DI PL NZ AC PE NC
1313:0190 80FC00 CMP AH,00
AX=0100 BX=0000 CX=0000 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=0000
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0193 NV UP DI PL NZ NA PO NC
1313:0193 7521 JNZ 01B6
When the Interrupt 60H call is encountered at offset 0F6AH, the values
passed to it can be checked. AH contains 1 and DX contains 0--the
correct values for START COM1.
In order to use the symbols for BADSCOP, use the Open Symbol Map
command, XO, to switch to the correct symbol map. Then, because the
value of CSEG is not defined in the map, use the Set Symbol Value
command to set CSEG to the current value of CS. (CS was changed to the
correct value for BADSCOP when the program executed the INT 60H
instruction.)
-XO BADSCOP! <Enter>
-Z CSEG CS <Enter>
-X?* <Enter>
CSEG: (1313)
0100 INITIALIZE 0103 OLD_COMM_INT 0107 COUNT 0109 STATUS
010A PORT 010B BUFPNTR 010D COMSCOPE 0190 CONTROL
020A VECTOR_INIT
Because the BADSCOP symbols now have meaning, a great deal of trouble
can be avoided by setting a breakpoint at CONTROL, the entry point for
Interrupt 60H, so that it will no longer be necessary to trace the
_int86 routine to find the INT 60H command. Execution will
automatically stop when the Interrupt 60H handler is entered.
-BP CONTROL <Enter>
-BL <Enter>
0 e 2CB0:002E [COMMSCMD!_TEXT:_main+1E (002E)] commscmd.C:41
1 e 2CB0:0046 [COMMSCMD!_TEXT:_main+36 (0046)] commscmd.C:42
2 e 2CB0:0066 [COMMSCMD!_TEXT:_main+56 (0066)] commscmd.C:44
3 e 2CB0:0086 [COMMSCMD!_TEXT:_main+76 (0086)] commscmd.C:46
4 e 2CB0:008B [COMMSCMD!_TEXT:_main+7B (008B)] commscmd.C:49
5 e 2CB0:00AC [COMMSCMD!_TEXT:_main+9C (00AC)] commscmd.C:53
6 e 2CB0:00C7 [COMMSCMD!_TEXT:_main+B7 (00C7)] commscmd.C:58
7 e 2CB0:00F5 [COMMSCMD!_TEXT:_main+E5 (00F5)] commscmd.C:63
8 e 1313:0190 [CSEGS:CONTROL]
With the housekeeping tasks done, the business of debugging BADSCOP
can begin. The first thing CONTROL does is check for a stop request.
If no stop request is present, the routine jumps to the check for a
start request. (The first test and jump were already complete when the
trace ended above.) The test for a start request is passed. CONTROL
places the port number in a local variable, resets the buffer pointer
and the buffer count, and turns tracing status on. With all this
complete, CONTROL returns.
-T 5 <Enter>
AX=01BB BX=E81E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=1CE7 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01B6 NV UP DI PL NZ NA PO NC
1313:01B6 80FC01 CMP AH,01
AX=01BB BX=E81E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=1CE7 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01B9 NV UP DI PL ZR NA PE NC
1313:01B9 751C JNZ CONTROL+47 (01D7)
AX=01BB BX=E81E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=1CE7 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01BB NV UP DI PL ZR NA PE NC
1313:01BB 2E88160A01 MOV CS:[PORT],DL CS:010A=00
AX=01BB BX=E81E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=1CE7 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01C0 NV UP DI PL ZR NA PE NC
1313:01C0 2EC7060B010202 MOV Word Ptr CS:[BUFPNTR],VECTOR_INIT (0209) CS:010B
AX=01BB BX=E81E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=1CE7 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01C7 NV UP DI PL ZR NA PE NC
1313:01C7 2EC70607010000 MOV Word Ptr CS:[COUNT],0000 CS:0107=0002
-T 5 <Enter>
AX=01BB BX=E81E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=1CE7 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01CE NV UP DI PL ZR NA PE NC
1313:01CE 2EC606090101 MOV Byte Ptr CS:[STATUS],01 CS:0109=01
AX=01BB BX=E81E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=1CE7 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01D4 NV UP DI PL ZR NA PE NC
1313:01D4 EB2B JMP CONTROL+71 (0201)
AX=01BB BX=E81E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=1CE7 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0201 NV UP DI PL ZR NA PE NC
1313:0201 CF IRET
AX=01BB BX=E81E CX=3F48 DX=0000 SP=0F60 BP=0F66 SI=1CE7 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=2E08 IP=0F6C NV UP EI PL NZ AC PE NC
2E08:0F6C CB RETF
AX=01BB BX=E81E CX=3F48 DX=0000 SP=0F64 BP=0F66 SI=1CE7 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=0209 NV UP EI PL NZ AC PE NC
2CB0:0209 5D POP BP
As can be seen from the trace, CONTROL performed correctly, so
execution of the routine can continue.
-G <Enter>
Communications tracing STARTED for port COM1:
AX=002F BX=0001 CX=0C13 DX=0000 SP=0FA6 BP=0000 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=00F5 NV UP EI PL NZ NA PE NC
2CB0:00F5 C3 RET ;BR7
COMMSCMD has written the message to the user and trapped at the
breakpoint set at the end of _main. The Examine Symbol Map command now
shows that SYMDEB has automatically switched to the symbol map for
COMMSCMD.
-X* <Enter>
[2CB0 COMMSCMD]
[2CB0 _TEXT]
2E08 DGROUP
0000 BADSCOP
1313 CSEG
No problems have been encountered with the START command; now the same
process of checking COMMSCMD and BADSCOP must be repeated for the STOP
command. (Even if problems had been found with the START command, it
would be imprudent not to test the other commands--they could have
errors, too.) SYMDEB could be exited and restarted with new commands,
but this would mean the loss of the painfully created set of break-
points. Instead, a new copy of COMMSCMD is loaded without leaving
SYMDEB. One problem with this, however, is that when SYMDEB loads an
.EXE file, it adds the value of the initial CS register to the
addresses of the segments in the symbol map whose name matches the
.EXE file. This is fine the first time the program loads, but the
second time, all the values are doubled and therefore incorrect. To
avoid this error, the addresses must be adjusted before the load. Use
the Set Symbol Value command to subtract CS from each segment name in
COMMSCMD!. The Examine Symbol Map command shows the new values.
-Z _TEXT _TEXT-CS <Enter>
-Z DGROUP DGROUP-CS <Enter>
-X* <Enter>
[2CB0 COMMSCMD]
[0000 _TEXT]
0158 DGROUP
0000 BADSCOP
1313 CSEG
The Name File or Command-Tail Parameters command, N, and the Load File
or Sectors command, L, can now be used to load a new copy of
COMMSCMD.EXE.
-N COMMSCMD.EXE <Enter>
-L <Enter>
-X* <Enter>
[2CB0 COMMSCMD]
[2CB0 _TEXT]
2E08 DGROUP
0000 BADSCOP
1313 CSEG
Notice that the segment values inside COMMSCMD! are the same as they
were when the program was first loaded. Use the Name command again,
this time to set the command tail to contain a STOP command for COM1.
The breakpoint table from the first execution is still set, so the
program can now be traced in the same way.
-N STOP 1 <Enter>
-G <Enter>
AX=0022 BX=0F84 CX=0019 DX=0098 SP=0F80 BP=0FA6 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=002E NV UP EI PL NZ NA PO NC
41: if (0 == stricmp(argv[1],"STOP"))
2CB0:002E B83600 MOV AX,0036 ;BR0
-G <Enter>
AX=0000 BX=415A CX=0000 DX=0098 SP=0F80 BP=0FA6 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=0046 NV UP EI PL ZR NA PE NC
42: cmd = 0;
2CB0:0046 C746FC0000 MOV Word Ptr [BP-04],0000 ;BR1
SS:0FA2=0000
-G <Enter>
AX=0000 BX=415A CX=0000 DX=0098 SP=0F80 BP=0FA6 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=008B NV UP EI PL ZR NA PE NC
49: if (argc == 3)
2CB0:008B 837E0403 CMP Word Ptr [BP+04],+03 ;BR4
SS:0FAA=0003
-G <Enter>
AX=0001 BX=00D0 CX=0000 DX=0000 SP=0F80 BP=0FA6 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=00AC NV UP EI PL NZ NA PO NC
53: port = port-1;
;ET
2CB0:00AC FF4EFA DEC Word Ptr [BP-06] ;BR5
SS:0FA0=0001
-G <Enter>
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F7A BP=0FA6 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=00C7 NV UP EI PL ZR NA PE NC
2CB0:00C7 E8EF00 CALL _int86 ;BR6
COMMSCMD detected that this is a stop request for COM1 and set the
arguments for _int86 correctly. Because a breakpoint is now set at
CONTROL, tracing until the Interrupt 60H call is found is not
necessary. Simply executing the program will cause it to stop at
CONTROL.
-G <Enter>
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F5C BP=0F68 SI=7400 DI=E903
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0190 NV UP DI PL NZ AC PO NC
CSEG:CONTROL:
1313:0190 80FC00 CMP AH,00 ;BR8
The registers are set correctly for a stop request on COM1 (AH = 0, DX
= 0). The routine can now be traced to check for correct operation.
First, however, a quick look at the symbol maps shows that SYMDEB has
automatically switched to BADSCOP's symbols.
-X* <Enter>
2CB0 COMMSCMD
2CB0 _TEXT
2E08 DGROUP
[0000 BADSCOP]
[1313 CSEG]
-T 5 <Enter>
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F5C BP=0F68 SI=7400 DI=E903
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0193 NV UP DI PL ZR NA PE NC
1313:0193 7521 JNZ CONTROL+26 (01B6)
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F5C BP=0F68 SI=7400 DI=E903
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0195 NV UP DI PL ZR NA PE NC
1313:0195 1E PUSH DS
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F5A BP=0F68 SI=7400 DI=E903
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0196 NV UP DI PL ZR NA PE NC
1313:0196 53 PUSH BX
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F58 BP=0F68 SI=7400 DI=E903
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0197 NV UP DI PL ZR NA PE NC
1313:0197 0E PUSH CS
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F56 BP=0F68 SI=7400 DI=E903
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0198 NV UP DI PL ZR NA PE NC
1313:0198 1F POP DS
-T 5 <Enter>
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F58 BP=0F68 SI=7400 DI=E903
DS=1313 ES=2E08 SS=2E08 CS=1313 IP=0199 NV UP DI PL ZR NA PE NC
1313:0199 C606090100 MOV Byte Ptr [STATUS],00 DS:0109=01
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F58 BP=0F68 SI=7400 DI=E903
DS=1313 ES=2E08 SS=2E08 CS=1313 IP=019E NV UP DI PL ZR NA PE NC
1313:019E 8B1E0B01 MOV BX,[BUFPNTR] DS:010B=0202
AX=001E BX=0202 CX=0000 DX=0000 SP=0F58 BP=0F68 SI=7400 DI=E903
DS=1313 ES=2E08 SS=2E08 CS=1313 IP=01A2 NV UP DI PL ZR NA PE NC
1313:01A2 C60780 MOV Byte Ptr [BX],80 DS:0202=80
AX=001E BX=0202 CX=0000 DX=0000 SP=0F58 BP=0F68 SI=7400 DI=E903
DS=1313 ES=2E08 SS=2E08 CS=1313 IP=01A5 NV UP DI PL ZR NA PE NC
1313:01A5 C64701FF MOV Byte Ptr [BX+01],FF DS:0203=FF
AX=001E BX=0202 CX=0000 DX=0000 SP=0F58 BP=0F68 SI=7400 DI=E903
DS=1313 ES=2E08 SS=2E08 CS=1313 IP=01A9 NV UP DI PL ZR NA PE NC
1313:01A9 FF060701 INC Word Ptr [COUNT] DS:0107=0000
T 5 <Enter>
AX=001E BX=0202 CX=0000 DX=0000 SP=0F58 BP=0F68 SI=7400 DI=E903
DS=1313 ES=2E08 SS=2E08 CS=1313 IP=01AD NV UP DI PL NZ NA PO NC
1313:01AD FF060701 INC Word Ptr [COUNT] DS:0107=0001
AX=001E BX=0202 CX=0000 DX=0000 SP=0F58 BP=0F68 SI=7400 DI=E903
DS=1313 ES=2E08 SS=2E08 CS=1313 IP=01B1 NV UP DI PL NZ NA PO NC
1313:01B1 5B POP BX
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F5A BP=0F68 SI=7400 DI=E903
DS=1313 ES=2E08 SS=2E08 CS=1313 IP=01B2 NV UP DI PL NZ NA PO NC
1313:01B2 1F POP DS
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F5C BP=0F68 SI=7400 DI=E903
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01B3 NV UP DI PL NZ NA PO NC
1313:01B3 EB4C JMP CONTROL+71 (0201)
AX=001E BX=3F48 CX=0000 DX=0000 SP=0F5C BP=0F68 SI=7400 DI=E903
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0201 NV UP DI PL NZ NA PO NC
1313:0201 CF IRET
CONTROL correctly detected that this was a stop request. It then saved
the user's registers and established a DS equal to CS. (Remember that
BADSCOP is a .COM file and CS = DS = SS.) Having done this, the
routine moves a zero to STATUS, which turns the trace off. It then
moves 80H FFH to the buffer to indicate the end of a trace session,
increments COUNT to allow for the new entry, and restores the user's
registers. What it does not do is increment the buffer pointer to
allow for the stop marker. This behavior is entirely consistent with
the observed phenomena: When a trace is stopped and resumed, the stop
marker is missing and the count is one too high. The fix is to add
INC BX ;INCREMENT BUFFER POINTER
INC BX ; .
MOV BUFPNTR,BX ; .
to the CONTROL procedure before the registers are restored. (Insert
these lines later with your favorite editor.)
Even though the bug has been found, the rest of the routine should be
checked for other possible bugs.
-G <Enter>
Communications tracing STOPPED for port COM1:
AX=002F BX=0001 CX=0C13 DX=0000 SP=0FA8 BP=0000 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=00F5 NV UP EI PL NZ AC PO NC
2CB0:00F5 C3 RET ;BR7
Loading a new copy of COMMSCMD, setting the command tail to RESUME 1,
and monitoring program execution yields the following:
-N COMMSCMD.EXE <Enter>
-Z _TEXT _TEXT-CS <Enter>
-Z DGROUP DGROUP-CS <Enter>
-X* <Enter>
[2CB0 COMMSCMD]
[0000 _TEXT]
0158 DGROUP
0000 BADSCOP
1313 CSEG
-L <Enter>
-X* <Enter>
[2CB0 COMMSCMD]
[2CB0 _TEXT]
2E08 DGROUP
0000 BADSCOP
1313 CSEG
-N RESUME 1 <Enter>
-G <Enter>
AX=0022 BX=0F82 CX=0019 DX=0098 SP=0F7E BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=002E NV UP EI PL NZ NA PO NC
41: if (0 == stricmp(argv[1],"STOP"))
2CB0:002E B83600 MOV AX,0036 ;BR0
-G <Enter>
AX=0000 BX=415A CX=0000 DX=0098 SP=0F7E BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=0086 NV UP EI PL ZR NA PE NC
46: cmd = 2;
2CB0:0086 C746FC0200 MOV Word Ptr [BP-04],0002 ;BR3 SS:0FA0=0000
-G <Enter>
AX=0000 BX=415A CX=0000 DX=0098 SP=0F7E BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=008B NV UP EI PL ZR NA PE NC
49: if (argc == 3)
2CB0:008B 837E0403 CMP Word Ptr [BP+04],+03 ;BR4 SS:0FA8=0003
-G <Enter>
AX=0001 BX=00D0 CX=0000 DX=0000 SP=0F7E BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=00AC NV UP EI PL NZ NA PO NC
53: port = port-1;
2CB0:00AC FF4EFA DEC Word Ptr [BP-06] ;BR SS:0F9E=00015
-G <Enter>
AX=0060 BX=00D0 CX=0000 DX=0000 SP=0F78 BP=0FA4 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=00C7 NV UP EI PL ZR NA PE NC
2CB0:00C7 E8EF00 CALL _int86 ;BR6
-G <Enter>
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0190 NV UP DI PL NZ AC PE NC
CSEG:CONTROL:
1313:0190 80FC00 CMP AH,00 ;BR8
-T 5 <Enter>
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0193 NV UP DI PL NZ NA PO NC
1313:0193 7521 JNZ CONTROL+26 (01B6)
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01B6 NV UP DI PL NZ NA PO NC
1313:01B6 80FC01 CMP AH,01
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01B9 NV UP DI PL NZ NA PO NC
1313:01B9 751C JNZ CONTROL+47 (01D7)
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01D7 NV UP DI PL NZ NA PO NC
1313:01D7 80FC02 CMP AH,02
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01DA NV UP DI PL ZR NA PE NC
1313:01DA 7516 JNZ CONTROL+62 (01F2)
-T 5 <Enter>
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01DC NV UP DI PL ZR NA PE NC
1313:01DC 2E833E0B0100 CMP Word Ptr CS:[BUFPNTR],+00 CS:010B=0202
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01E2 NV UP DI PL NZ NA PO NC
1313:01E2 741D JZ CONTROL+71 (0201)
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01E4 NV UP DI PL NZ NA PO NC
1313:01E4 2E88160A01 MOV CS:[PORT],DL CS:010A=00
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01E9 NV UP DI PL NZ NA PO NC
1313:01E9 2EC606090101 MOV Byte Ptr CS:[STATUS],01 CS:0109=00
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=01EF NV UP DI PL NZ NA PO NC
1313:01EF EB10 JMP CONTROL+71 (0201)
-T 5 <Enter>
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F5A BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=1313 IP=0201 NV UP DI PL NZ NA PO NC
1313:0201 CF IRET
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F60 BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=2E08 IP=0F6C NV UP EI PL NZ AC PE NC
2E08:0F6C CB RETF
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F64 BP=0F66 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=0209 NV UP EI PL NZ AC PE NC
2CB0:0209 5D POP BP
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F66 BP=0F74 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=020A NV UP EI PL NZ AC PE NC
2CB0:020A 57 PUSH DI
AX=0265 BX=001E CX=3F48 DX=0000 SP=0F64 BP=0F74 SI=0000 DI=7400
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=020B NV UP EI PL NZ AC PE NC
2CB0:020B 8B7E08 MOV DI,[BP+08] SS:0F7C=0F90
-G <Enter>
Communications tracing RESUMED for port COM1:
AX=002F BX=0001 CX=0C13 DX=0000 SP=0FA6 BP=0000 SI=0089 DI=1065
DS=2E08 ES=2E08 SS=2E08 CS=2CB0 IP=00F5 NV UP EI PL NZ NA PE NC
2CB0:00F5 C3 RET ;BR7
-Q <Enter>
The processing of a resume request is correct. Thus, the problem with
stop processing in BADSCOP was the only problem. The corrected
BADSCOP, which is actually COMMSCOP, is shown in Figure 18-4.
CodeView
CodeView is the most sophisticated debugging monitor produced by
Microsoft. It combines the philosophy and many of the commands of its
predecessors, DEBUG and SYMDEB, with true source-code debugging. The
availability of source lines and symbols allows CodeView to rival the
convenience of program development and debugging previously available
only in interpreters such as Microsoft GW-BASIC. However, this high
level of interaction with the source program is also the root of its
problems for advanced debugging.
In order to provide the debugger with the tools to debug at the
source-line level and to interrogate program variables, CodeView is
required to have a detailed knowledge of how high-order languages work
and of their internal conventions. This is not a problem for languages
like C, Pascal, and FORTRAN, versions of which are produced by the
same company that created CodeView. The object code generated by these
compilers obeys a stringent set of rules and conventions. Assembly-
language programs, however, tend to follow their own rules and
traditions, making them quite different from C programs, with their
own separate debugging needs.
C, Pascal, and FORTRAN programmers will find CodeView a dream to use.
Assembly-language programmers using versions of MASM earlier than 5.0
will find CodeView cumbersome and will have to weigh its advantages
over its disadvantages. All users will, however, appreciate the good
design and programming that have gone into CodeView. It is pleasing to
know that someone understands the programmer's debugging needs and is
trying to ease the burden.
CodeView has added several welcome functions to the debugger's
repertoire, but one of these new features towers above the rest--
watchpoints. The debugger can watch the values of program variables or
expressions and set breakpoints on them, making it possible to stop
execution if an expression evaluates to zero or if a location changes.
Previous debugging monitors have been limited to tracing and breaking
on instructions. This new facet of debugging changes, somewhat, the
approach to resolving a bug.
In the previous discussion of debugging techniques, an orderly
application of techniques from inspection and observation through
instrumentation to debugging monitors was recommended. This sequence
is still recommended with CodeView, but now the instrumentation
features have been integrated into the debugging monitor.
A simple example
The following example shows how CodeView uses the instrumentation
approach to isolate a problem and then uses the debugging monitor
functions to solve it. The example is also an introduction to CodeView
commands and techniques. The commands are, for the most part, similar
to those used by SYMDEB. Those commands that differ greatly are
indicated. This example, like all the examples and demonstrations in
this article, is not intended to be a complete tutorial--CodeView
commands are summarized elsewhere in this book and explained in detail
in the manual accompanying the product. See PROGRAMMING UTILITIES:
CODEVIEW. The example simply shows some of the more common CodeView
commands and demonstrates debugging techniques using them.
UPPERCAS.C (Figure 18-13) is a simple program whose sole function is
to convert a canned string to uppercase. When executed, the program
prints a few of the characters from the string and some that aren't in
the string. Inspecting the listing doesn't reveal the cause of the
problem. (Some readers with experience writing C programs will see the
cause of the problem, because it is quite common; pretend, for now,
that the listing is of no help and enjoy the wonders of CodeView.)
──────────────────────────────────────────────────────────────────────
Figure 18-13. An erroneous C program to convert a string to uppercase.
──────────────────────────────────────────────────────────────────────
Like SYMDEB, CodeView requires some special preparation to produce a
suitable executable file. CodeView, however, makes the job much
simpler. Using the Microsoft C Compiler, compile the program with
C>MSC /Zi UPPERCAS; <Enter>
(Remember that C is case sensitive when interpreting switches, so the
/Zi switch should be entered exactly as shown.) The /Zi switch
instructs the compiler to generate the symbol tables and line-number
information needed by CodeView. Other options appropriate to the
program can also be included, but /Zi is required.
To form an executable file, use the Microsoft Object Linker (LINK) as
follows:
C>LINK /CO UPPERCAS; <Enter>
This command line instructs LINK to build an executable file with the
information needed for CodeView. Other options can be used as needed
or desired. The output of LINK, UPPERCAS.EXE, will be larger than a
.EXE file built without /CO (about 2600 bytes larger in this case),
but the program will run correctly when executed without CodeView.
Starting CodeView is straightforward. Simply type
C>CV UPPERCAS <Enter>
CodeView loads UPPERCAS.EXE. It locates UPPERCAS.C, the source file,
and loads that too. It then presents a full-screen display similar to
this:
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ uppercas.C ╞═══════════════════════════════════╕
1:
2: /**********************************************************************▓
3: * ▒
4: * UPPERCAS.C ▒
5: * This routine converts a fixed string to uppercase and prints it. ▒
6: * ▒
7: **********************************************************************▒
8: ▒
9: #include <ctype.h> ▒
10: #include <string.h> ▒
11: #include <stdio.h> ▒
12: ▒
13: main(argc,argv) ▒
14: ▒
15: int argc; ▒
16: char *argv[]; ▒
17: ▒
18: {
══════════════════════════════════════════════════════════════════════════════╡
Microsoft (R) CodeView (R) Version 2.0 ▓
(C) Copyright Microsoft Corp. 1986, 1987. All rights reserved. ▒
>
This display has two windows open: the display window, which shows the
program being debugged, and the the dialog window, which currently
contains only the copyright notice and a prompt (>) for input. The F6
function key moves the cursor back and forth between the two windows.
CodeView can be instructed from either window to go to a specific line
(that is, to execute until a specific line is reached). If the cursor
is in the display window, use the arrow keys to select a line and
press the F7 key. Execution will proceed until the selected line (or
the end of the program) is reached. To start execution without
specifying a stop line, press F5.
The same functions can be performed from the dialog window using typed
commands, which may seem more familiar. Enter the Go Execute Program
command, G, optionally followed by an address. Execution will continue
until the specified address is reached or until stopped by something
else, such as the end of the program. In this sense, the CodeView Go
command is the same as that of DEBUG and SYMDEB. Unlike those
routines, however, CodeView's Go command does not allow an equals
operator (=).
The address for the Go command can be specified in several ways.
Because the display window is currently showing only source lines, it
is appropriate to set the stop location in terms of line numbers. The
syntax of a line-number specification is the same as in SYMDEB--simply
enter the line number preceded by a period:
>G .27 <Enter>
Note that the line number is specified in decimal. This seemingly
innocent statement uncovers one of the problem areas in CodeView,
especially for assembly-language programmers. The default radix for
CodeView is decimal. This convention works well for things associated
with the C program, such as line numbers, but is very inconvenient for
addresses and other similar items, which are usually in hexadecimal.
Hexadecimal numbers must be specified using the cumbersome C notation.
Thus, the number FF3EH would be entered as 0xff3e. The radix can be
changed using the Change Current Radix command, N (different from the
DEBUG and SYMDEB N command). (The problems associated with hexadecimal
numbers in early versions of CodeView are no longer present in
versions 2.0 and later.)
The radix problem can be avoided, for the moment, by using labels.
Issue
>G _main <Enter>
to cause CodeView to execute until the main routine is reached.
CodeView then shows
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ uppercas.C ╞═══════════════════════════════════╕
9: #include <ctype.h>
10: #include <string.h> ▒
11: #include <stdio.h> ▒
12: ▒
13: main(argc,argv) ▒
14: ▓
15: int argc; ▒
16: char *argv[]; ▒
17: ▒
18: { ▒
19: char *cp,c; ▒
20: ▒
21: cp = "a string\n"; ▒
22: ▒
23: /* Convert *cp to uppercase and write to standard output */ ▒
24: ▒
25: while (*cp != '\0') ▒
26: {
══════════════════════════════════════════════════════════════════════════════╡
Microsoft (R) CodeView (R) Version 2.0
(C) Copyright Microsoft Corp. 1986, 1987. All rights reserved. ▓
>g _main ▒
>
The display shows line 15 in reverse video, indicating that CodeView
has stopped there. This is the first line of the main() module, but it
is not executable. Press the F10 key, which has the same effect as
entering the Step Through Program command, P, in the dialog window, to
cause line 19 to be executed. The reverse video line is then 21, which
is the next line to be executed.
To see the changes to cp, *cp, and c, establish a watch on these three
variables. To use the Watch Word command, WW, for the word cp, type
>WW cp <Enter>
When entered from the dialog window, this command opens the watch
window at the top of the screen and displays the current value of cp.
To display the expression at *cp, use the Watch Expression command,
W?, as follows:
>W? cp,s <Enter>
This expression will display the null-delimited string at *cp.
Finally, to see the ASCII character value of c, use the Watch ASCII
command, WA:
>WA c <Enter>
The results of these watch commands are shown in the following screen:
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ uppercas.C ╞═══════════════════════════════════╕
0) cp : 55C4:0FF0 5527 │
1) cp,s : "" │
2) c : 55C4:0FF2 . │
═══════════════════════════════════════════════════════════════════════════════╡
9: #include <ctype.h>
10: #include <string.h> ▒
11: #include <stdio.h> ▒
12: ▒
13: main(argc,argv) ▓
14: ▒
15: int argc; ▒
16: char *argv[]; ▒
17: ▒
18: { ▒
19: char *cp,c; ▒
20: ▒
21: cp = "a string\n"; ▒
22:
══════════════════════════════════════════════════════════════════════════════╡
>ww cp
>w? cp,s ▓
>wa c ▒
>
The values displayed in the watch window are not yet defined because
line 21, which initialized cp, has not been executed. Press F8 to
rectify this. Press it again to bring the execution of the program
into the main loop.
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ uppercas.C ╞═══════════════════════════════════╕
0) cp : 55C4:0FF0 0036 │
1) cp,s : "a string │
2) c : 55C4:0FF2 . │
═══════════════════════════════════════════════════════════════════════════════╡
18: {
19: char *cp,c; ▒
20: ▒
21: cp = "a string\n"; ▒
22: ▒
23: /* Convert *cp to uppercase and write to standard output */ ▒
24: ▒
25: while (*cp != '\0') ▓
26: { ▒
27: c = toupper(*cp++); ▒
28: putchar(c); ▒
29: } ▒
30: ▒
31: }
══════════════════════════════════════════════════════════════════════════════╡
>ww cp
>w? cp,s ▓
>wa c ▒
>
The pointer cp now contains the correct address. The Display Memory
command, D, could be used to display the contents of DS:0036H, just
as in DEBUG and SYMDEB. (This step is not necessary, however, because
there is a formatted display of memory in the watch window at 1). The
variable c has not yet been initialized.
Press the F8 key to execute line 27. A curious and unexpected thing
happens, as shown in the next screen:
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ uppercas.C ╞═══════════════════════════════════╕
0) cp : 55C4:0FF0 0038 │
1) cp,s : "string │
2) c : 55C4:0FF2 │
═══════════════════════════════════════════════════════════════════════════════╡
18: {
19: char *cp,c; ▒
20: ▒
21: cp = "a string\n"; ▒
22: ▒
23: /* Convert *cp to uppercase and write to standard output */ ▒
24: ▒
25: while (*cp != '\0') ▓
26: { ▒
27: c = toupper(*cp++); ▒
28: putchar(c); ▒
29: } ▒
30: ▒
31: }
══════════════════════════════════════════════════════════════════════════════╡
>ww cp
>w? cp,s ▓
>wa c ▒
>
Notice that the value of cp has changed from 0036H to 0038H. The line
of code, however, indicates that the pointer should have been
incremented by only one (*cp++). The second character of the string, a
blank, has been loaded into c. This could explain the apparent random
selection of characters being displayed (actually every other
character) and the garbage characters displayed (the zero at the end
of the string might be skipped, causing the routine to continue
converting until a zero is encountered somewhere in memory).
Source-line debugging does not reveal enough about what is happening
in this case. To look more closely at the mechanism of the program,
the program must be restarted. Before doing this, set a breakpoint at
line 27:
>BP .27 <Enter>
Then restart (actually, reload) the program with the Reload
Program command, L. Note that watch commands and breakpoints are
preserved when a program is restarted. Executing the restarted program
with G yields
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ uppercas.C ╞═══════════════════════════════════╕
0) cp : 55C4:0FF0 0036 │
1) cp,s : "a string │
2) c : 55C4:0FF2 . │
═══════════════════════════════════════════════════════════════════════════════╡
18: {
19: char *cp,c; ▒
20: ▒
21: cp = "a string\n"; ▒
22: ▒
23: /* Convert *cp to uppercase and write to standard output */ ▒
24: ▒
25: while (*cp != '\0') ▓
26: { ▒
27: c = toupper(*cp++); ▒
28: putchar(c); ▒
29: } ▒
30: ▒
31: }
══════════════════════════════════════════════════════════════════════════════╡
>bp .27
>l ▓
>g ▒
>
The display shows line 27 in reverse video, indicating that it is the
next line to be executed. The pointer cp has the correct value, as
shown in the watch window. Now Press the F2 key to turn on the
register display and press F3 to show the assembly code.
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ uppercas.C ╞═════════════════════════╤══════════
0) cp : 55C4:0FF0 0036 │ AX = 0004
1) cp,s : "a string │ BX = 0036
2) c : 55C4:0FF2 . │ CX = 0019
═════════════════════════════════════════════════════════════════════╡ DX = 00B8
27: c = toupper(*cp++); SP = 0FF0
5527:0026 FF46FC INC Word Ptr [cp] ;BR0 ▒ BP = 0FF4
5527:0029 8A07 MOV AL,Byte Ptr [BX] ▒ SI = 00A9
5527:002B 98 CBW ▒ DI = 10D5
5527:002C 8BD8 MOV BX,AX ▒ DS = 55C4
5527:002E F687B30102 TEST Byte Ptr [BX+01B3],02 ▓ ES = 55C4
5527:0033 740C JZ _main+31 (0041) ▒ SS = 55C4
5527:0035 8B5EFC MOV BX,Word Ptr [cp] ▒ CS = 5527
5527:0038 FF46FC INC Word Ptr [cp] ▒ IP = 0026
5527:003B 8A07 MOV AL,Byte Ptr [BX] ▒
5527:003D 2C20 SUB AL,20 ▒ NV UP
5527:003F EB08 JMP _main+39 (0049) ▒ EI PL
5527:0041 8B5EFC MOV BX,Word Ptr [cp] ▒ NZ NA
5527:0044 FF46FC INC Word Ptr [cp] PO NC
════════════════════════════════════════════════════════════════════╡
>bp .27 SS:0FF0
>l ▓ 0036
>g ▒
>
The display highlights line 27, indicating that a breakpoint exists at
this line. The line of code at CS:0026H is in reverse video,
indicating that it is the next line to be executed.
The previous instruction has loaded BX with [cp]. The first thing the
code for line 27 does is increment the word at memory location [cp].
The initial value of cp is in BX, so the *cp++ request can now be
executed. Use the F8 key to single-step through the lines of code.
Notice that when only source lines are on the screen, F8 steps one
source line at a time, but when assembly code is shown, F8 steps one
assembly line at a time. Single-stepping through the code, note how
the registers and watch window change. Everything appears normal until
CS:0038H is executed.
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ uppercas.C ╞═════════════════════════╤══════════
0) cp : 55C4:0FF0 0038 │ AX = 0061
1) cp,s : "string │ BX = 0037
2) c : 55C4:0FF2 . │ CX = 0019
═════════════════════════════════════════════════════════════════════╡ DX = 00B8
27: c = toupper(*cp++); SP = 0FF0
5527:0026 FF46FC INC Word Ptr [cp] ;BR0 ▒ BP = 0FF4
5527:0029 8A07 MOV AL,Byte Ptr [BX] ▒ SI = 00A9
5527:002B 98 CBW ▒ DI = 10D5
5527:002C 8BD8 MOV BX,AX ▒ DS = 55C4
5527:002E F687B30102 TEST Byte Ptr [BX+01B3],02 ▓ ES = 55C4
5527:0033 740C JZ _main+31 (0041) ▒ SS = 55C4
5527:0035 8B5EFC MOV BX,Word Ptr [cp] ▒ CS = 5527
5527:0038 FF46FC INC Word Ptr [cp] ▒ IP = 003B
5527:003B 8A07 MOV AL,Byte Ptr [BX] ▒
5527:003D 2C20 SUB AL,20 ▒ NV UP
5527:003F EB08 JMP _main+39 (0049) ▒ EI PL
5527:0041 8B5EFC MOV BX,Word Ptr [cp] ▒ NZ NA
5527:0044 FF46FC INC Word Ptr [cp] PO NC
════════════════════════════════════════════════════════════════════╡
>bp .27 DS:0037
>l ▓ 20
>g ▒
>
Notice that the value of cp in the watch window has incremented again.
The line of C code has two increments hidden in it, not the expected
single increment. Why is this?
To find the answer, examine the toupper() macro. The following
definition, extracted from CTYPE.H, explains what is happening:
#define _UPPER 0x1 /* uppercase letter */
#define _LOWER 0x2 /* lowercase letter */
#define isupper(c) ( (_ctype+1)[c] & _UPPER )
#define islower(c) ( (_ctype+1)[c] & _LOWER )
#define _tolower(c) ( (c)-'A'+'a' )
#define _toupper(c) ( (c)-'a'+'A' )
#define toupper(c) ( (islower(c)) ? _toupper(c) : (c) )
#define tolower(c) ( (isupper(c)) ? _tolower(c) : (c) )
The argument to toupper(), c, is used twice, once in the macro that
checks for lowercase, islower(), and once in _toupper(). The argument
is replaced in this case with *cp$QP$QP, which has the famous C
unexpected side effects. Because the unary post-increment is the
handiest way to perform the function desired in the program, fixing
the problem by changing the code in the main loop is undesirable.
Another solution to the problem is to use the function version of
toupper(). Because toupper() is defined as a function in STDIO.H,
simply deleting #include <ctype.h> would solve the problem.
Unfortunately, this would also deprive the program of the other useful
definitions in CTYPE.H. (Admittedly, the features are not currently
used by the program, but little programs sometimes grow into mighty
systems.) So to keep CTYPE.H but still remove the macro definition of
toupper(), use the #undef command. (Because tolower() has the same
problem, it should also be undefined.) The corrected listing is shown
in Figure 18-14.
──────────────────────────────────────────────────────────────────────
Figure 18-14. The corrected version of UPPERCAS.C.
──────────────────────────────────────────────────────────────────────
An example using screen output
A problem with DEBUG is that it writes to the same screen as the
program does. Both SYMDEB and CodeView, however, allow the debugger to
switch back and forth between the screen containing the program's
output and the screen containing the debugger's output. This feature
is a special option with SYMDEB and is sometimes clumsy to use, but
with CodeView, keeping a separate program output screen is automatic
and switching back and forth involves simply pressing a function key
(F4).
The following example program is intended to display an ASCII lookup
table with all the displayable characters available on an IBM PC. The
expected output is shown in Figure 18-15.
C>asctbl
ASCII LOOKUP TABLE
0 1 2 3 4 5 6 7 8 9 A B C D E F
0
1
2 ! " # $ % & ' ( ) * + , - . /
3 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
4 @ A B C D E F G H I J K L M N O
5 P Q R S T U V W X Y Z [ \ ] ^ _
6 ` a b c d e f g h i j k l m n o
7 p q r s t u v w x y z { | } ~
8 Ç ü é â ä à å ç ê ë è ï î ì Ä Å
9 É æ Æ ô ö ò û ù ÿ Ö Ü ¢ £ ¥ ₧ ƒ
A á í ó ú ñ Ñ ª º ¿ ⌐ ¬ ½ ¼ ¡ « »
B ░ ▒ ▓ │ ┤ ╡ ╢ ╖ ╕ ╣ ║ ╗ ╝ ╜ ╛ ┐
C └ ┴ ┬ ├ ─ ┼ ╞ ╟ ╚ ╔ ╩ ╦ ╠ ═ ╬ ╧
D ╨ ╤ ╥ ╙ ╘ ╒ ╓ ╫ ╪ ┘ ┌ █ ▄ ▌ ▐ ▀
E α ß Γ π Σ σ µ τ Φ Θ Ω δ ∞ φ ε ∩
F ≡ ± ≥ ≤ ⌠ ⌡ ÷ ≈ ° ∙ · √ ⁿ ² ■
Figure 18-15. The output expected from ASCTBL.C.
The program that should produce this display, ASCTBL.C, is shown in
Figure 18-16.
──────────────────────────────────────────────────────────────────────
Figure 18-16. An erroneous program to display ASCII characters.
──────────────────────────────────────────────────────────────────────
The problem to be debugged in this example is evident when the program
in Figure 18-16 is compiled, linked, and executed. Here is the
resulting display:
╓┌─────────┌──────────────────────────────────────────────────────────────────╖
C>asctbl
ASCII LOOKUP TABLE
0 1 2 3 4 5 6 7 8 9 A B C D E F h0
y1 y2
! " # $ % & ' ( ) * + , - . / y3 0 1 2 3 4 5 6 7 8
9 : ; < = > ? y4 @ A B C D E F G H I J K L M N O y5 P
Q R S T U V W X Y Z [ \ ] ^ _ y6 ` a b c d e f g h i
j k l m n o y7 p q r s t u v w x y z { | } ~ y8 Ç
ü é â ä à å ç ê ë è ï î ì Ä Å y9 É æ Æ ô ö ò û ù ÿ Ö
Ü ¢ £ ¥ ₧ ƒ yA á í ó ú ñ Ñ ª º ¿ ⌐ ¬ ½ ¼ ¡ « » yB ░
▒ ▓ │ ┤ ╡ ╢ ╖ ╕ ╣ ║ ╗ ╝ ╜ ╛ ┐ yC └ ┴ ┬ ├ ─ ┼ ╞ ╟ ╚ ╔
╩ ╦ ╠ ═ ╬ ╧ yD ╨ ╤ ╥ ╙ ╘ ╒ ╓ ╫ ╪ ┘ ┌ █ ▄ ▌ ▐ ▀ yE α ß
Γ π Σ σ µ τ Φ Θ Ω δ ∞ φ ε ∩ yF ≡ ± ≥ ≤ ⌠ ⌡ ÷ ≈ ° ∙ ·
√ ⁿ ² ■ y
C>
Something is clearly wrong. The output is jumbled and no pattern is
immediately obvious. To locate the problem, first prepare a .EXE file
and start CodeView as follows:
C>MSC /Zi ASCTBL; <Enter>
C>LINK /CO ASCTBL; <Enter>
C>CV ASCTBL <Enter>
CodeView starts and displays the following screen:
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ asctbl.C ╞═════════════════════════════════════╕
1:
2: /**********************************************************************▓
3: * ▒
4: * ASCTBL.C ▒
5: * This program generates an ASCII lookup table for all displayable ▒
6: * ASCII and extended IBMPC codes, leaving blanks for nondisplayable ▒
7: * codes. ▒
8: * ▒
9: **********************************************************************▒
10: ▒
11: #include <ctype.h> ▒
12: #include <stdio.h> ▒
13: ▒
14: main() ▒
15: { ▒
16: int i, j, k; ▒
17: /* Print table title. */ ▒
18: printf("\n\n\n ASCII LOOKUP TABLE\n\n");
══════════════════════════════════════════════════════════════════════════════╡
Microsoft (R) CodeView (R) Version 2.0 ▓
(C) Copyright Microsoft Corp. 1986, 1987. All rights reserved. ▒
>
The start of the source program is shown in the display window and the
dialog window contains an input prompt. Press the F10 key three times
to bring execution to line 21. (Remember that the line indicated in
reverse video has not yet been executed.)
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ asctbl.C ╞═════════════════════════════════════╕
9: **********************************************************************
10: ▒
11: #include <ctype.h> ▒
12: #include <stdio.h> ▒
13: ▓
14: main() ▒
15: { ▒
16: int i, j, k; ▒
17: /* Print table title. */ ▒
18: printf("\n\n\n ASCII LOOKUP TABLE\n\n"); ▒
19: ▒
20: /* Print column headers. */ ▒
21: printf(" "); ▒
22: for (i = 0; i < 16; i++) ▒
23: printf("%X ", i); ▒
24: fputchar("\n"); ▒
25: ▒
26: /* Print each line of the table. */
══════════════════════════════════════════════════════════════════════════════╡
Microsoft (R) CodeView (R) Version 2.0 ▓
(C) Copyright Microsoft Corp. 1986, 1987. All rights reserved. ▒
>
The display heading has been printed at line 18. Press the F4 key to
display what the program has written on the screen.
C>cv asctbl
ASCII LOOKUP TABLE
Note: Any information on the screen when you started CodeView will
remain on the virtual output screen until program execution clears it
or forces it to scroll off.
The table heading has been properly written to the screen. Press the
F4 key again to return to the CodeView display. Continue executing the
program with the F10 key to bring the program to line 24.
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ asctbl.C ╞═════════════════════════════════════╕
9: **********************************************************************
10: ▒
11: #include <ctype.h> ▒
12: #include <stdio.h> ▒
13: ▓
14: main() ▒
15: { ▒
16: int i, j, k; ▒
17: /* Print table title. */ ▒
18: printf("\n\n\n ASCII LOOKUP TABLE\n\n"); ▒
19: ▒
20: /* Print column headers. */ ▒
21: printf(" "); ▒
22: for (i = 0; i < 16; i++) ▒
23: printf("%X ", i); ▒
24: fputchar("\n"); ▒
25: ▒
26: /* Print each line of the table. */
══════════════════════════════════════════════════════════════════════════════╡
Microsoft (R) CodeView (R) Version 2.0 ▓
(C) Copyright Microsoft Corp. 1986, 1987. All rights reserved. ▒
>
At this point in program execution, the column headings have been
written on the screen. Press the F4 key again to see the results.
C>cv asctbl
ASCII LOOKUP TABLE
0 1 2 3 4 5 6 7 8 9 A B C D E F
The output of the program is still correct, so allow execution to
continue by pressing F4 to return to the CodeView screen and then
pressing the F10 key. This will execute the call to the fputchar()
function to write a newline character.
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ asctbl.C ╞═════════════════════════════════════╕
21: printf(" ");
22: for (i = 0; i < 16; i++) ▒
23: printf("%X ", i); ▒
24: fputchar("\n"); ▒
25: ▒
26: /* Print each line of the table. */ ▒
27: for ( i = 0, k = 0; i < 16; i++) ▒
28: { ▒
29: /* Print first hex digit of symbols on this line. */ ▓
30: printf("%X ", i); ▒
31: /* Print each of the 16 symbols for this line. */ ▒
32: for (j = 0; j < 16; j++) ▒
33: { ▒
34: /* Filter non-printable characters. */ ▒
35: if ((k >= 7 && k <= 13) || (k >= 28 && k <= 31)▒
36: printf(" "); ▒
37: else ▒
38: printf("%c ", k);
══════════════════════════════════════════════════════════════════════════════╡
Microsoft (R) CodeView (R) Version 2.0 ▓
(C) Copyright Microsoft Corp. 1986, 1987. All rights reserved. ▒
>
Examination of the output screen shows that the display is now
incorrect.
C>cv asctbl
ASCII LOOKUP TABLE
0 1 2 3 4 5 6 7 8 9 A B C D E F h
A lowercase h has been written to the screen instead of a newline
character. Further execution demonstrates that newline characters
written with fputchar() are not working. A closer inspection of the
fputchar() function is needed.
To see what is happening, use the Reload Program command to restart
execution at the top of the program. Change the cursor window with the
F6 key, use the arrow keys to place the cursor on line 24, and press
F7. This brings execution back to line 24, where fputchar() is called.
Press the F3 key to place the display in assembly mode and the F2 key
to show the CPU registers and flags. The first assembly instruction of
the fputchar() function call is about to be executed.
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ asctbl.C ╞═══════════════════════════╤══════════
24: fputchar("\n"); AX = 0003
5527:004E B86800 MOV AX,0068 ▒ BX = 0001
5527:0051 50 PUSH AX ▒ CX = 0001
5527:0052 E83F01 CALL _fputchar (0194) ▒ DX = 03C0
5527:0055 83C402 ADD SP,+02 ▒ SP = 0F90
27: for ( i = 0, k = 0; i < 16; i++) ▒ BP = 0F96
5527:0058 C746FE0000 MOV Word Ptr [i],0000 ▒ SI = 00A9
5527:005D C746FA0000 MOV Word Ptr [k],0000 ▓ DI = 1075
5527:0062 837EFE10 CMP Word Ptr [i],+10 ▒ DS = 566D
5527:0066 7D68 JGE _main+c0 (00D0) ▒ ES = 566D
5527:0068 EB05 JMP _main+5f (006F) ▒ SS = 566D
5527:006A FF46FE INC Word Ptr [i] ▒ CS = 5527
5527:006D EBF3 JMP _main+52 (0062) ▒ IP = 004E
30: printf("%X ", i); ▒
5527:006F FF76FE PUSH Word Ptr [i] ▒ NV UP
5527:0072 B86A00 MOV AX,006A ▒ EI PL
5527:0075 50 PUSH AX ▒ ZR NA
5527:0076 E84801 CALL _printf (01C1) PE NC
════════════════════════════════════════════════════════════════════╡
Microsoft (R) CodeView (R) Version 2.0
(C) Copyright Microsoft Corp. 1986, 1987. All rights reserved. ▓
>l ▒
>
Notice that the parameter being passed to the function by means of the
stack is 0068H. Use the Display Memory command to display DS:0068H.
(Note the hexadecimal notation.)
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ asctbl.C ╞═══════════════════════════╤══════════
24: fputchar("\n"); AX = 0003
5527:004E B86800 MOV AX,0068 ▒ BX = 0001
5527:0051 50 PUSH AX ▒ CX = 0001
5527:0052 E83F01 CALL _fputchar (0194) ▒ DX = 03C0
5527:0055 83C402 ADD SP,+02 ▒ SP = 0F90
27: for ( i = 0, k = 0; i < 16; i++) ▒ BP = 0F96
5527:0058 C746FE0000 MOV Word Ptr [i],0000 ▒ SI = 00A9
5527:005D C746FA0000 MOV Word Ptr [k],0000 ▓ DI = 1075
5527:0062 837EFE10 CMP Word Ptr [i],+10 ▒ DS = 566D
5527:0066 7D68 JGE _main+c0 (00D0) ▒ ES = 566D
5527:0068 EB05 JMP _main+5f (006F) ▒ SS = 566D
5527:006A FF46FE INC Word Ptr [i] ▒ CS = 5527
5527:006D EBF3 JMP _main+52 (0062) ▒ IP = 004E
30: printf("%X ", i); ▒
5527:006F FF76FE PUSH Word Ptr [i] ▒ NV UP
5527:0072 B86A00 MOV AX,006A ▒ EI PL
5527:0075 50 PUSH AX ▒ ZR NA
5527:0076 E84801 CALL _printf (01C1) PE NC
════════════════════════════════════════════════════════════════════╡
>l
>d 0x68 L8 ▓
566D:0060 -0A 00 25 58 20 20 20 00 .▒
>
The contents of memory at this address consist of a null-delimited
string containing a newline character. The representation of \n is
correct. To see how the string is handled, use the trace key, F8, to
single-step through fputchar() and subordinate functions. These
functions are complicated; nearly 100 steps are required to reach the
MS-DOS Interrupt 21H call that actually writes the screen.
File View Search Run Watch Options Language Calls Help │ F8=Trace F5=Go
══════════════════════════════╡ asctbl.C ╞═══════════════════════════╤══════════
5527:10E9 51 PUSH CX AX = 400A
5527:10EA 8BCF MOV CX,DI ▒ BX = 0001
5527:10EC 2BCA SUB CX,DX ▒ CX = 0001
5527:10EE CD21 INT 21 ▒ DX = 0F84
5527:10F0 9C PUSHF ▒ SP = 0F68
5527:10F1 03F0 ADD SI,AX ▒ BP = 0F6E
5527:10F3 9D POPF ▒ SI = 0000
5527:10F4 7304 JNB _write+82 (10FA) ▒ DI = 0F85
5527:10F6 B409 MOV AH,09 ▒ DS = 566D
5527:10F8 EB1A JMP _write+9c (1114) ▒ ES = 566D
5527:10FA 0BC0 OR AX,AX ▒ SS = 566D
5527:10FC 7516 JNZ _write+9c (1114) ▒ CS = 5527
5527:10FE F687120240 TEST Byte Ptr [BX+__osfile],40 ▒ IP = 10EE
5527:1103 740B JZ _write+98 (1110) ▒
5527:1105 8B5E06 MOV BX,Word Ptr [BP+06] ▒ NV UP
5527:1108 803F1A CMP Byte Ptr [BX],1A ▒ EI PL
5527:110B 7503 JNZ _write+98 (1110) ▓ NZ NA
5527:110D F8 CLC PO NC
════════════════════════════════════════════════════════════════════╡
566D:0060 -0A 00 25 58 20 20 20 00 .
>d 0xf84 L8 ▓
566D:0F80 68 00 DC 00-A9 00 96 0F h....▒
>
The AH register's contents, 40H, indicate that the Interrupt 21H call
is a request for a write to a device. The BX register has the handle
of the device, 1, which is the special file handle for standard output
(stdout). For this program as it was invoked, standard output is the
screen. The CX register indicates that 1 byte is to be written; DS:DX
points to the data to be written. The contents of memory at DS:0F84H
finally reveal the cause of the problem: This memory location contains
the address of the data to be written, not the data. The fputchar()
function was called with the wrong level of indirection.
Examination of the listing shows that all the newline requests were
made with
fputchar("\n");
Strings specified with double quotes are replaced in C functions with
the address of the string, but the function expected the actual
character and not its address. The problem can be corrected by
replacing the fputchar() calls with
fputchar('\n');
The newline character will now be passed directly to the function.
This kind of problem can be avoided. C provides the ability to check
the type of each parameter passed to a function against the expected
type. If the following definition is included at the top of the C
program, incorrect types will generate error messages:
#define LINT_ARGS
The corrected listing is shown in Figure 18-17. This new program
produces the correct output.
──────────────────────────────────────────────────────────────────────
Figure 18-17. The correct ASCII table generation program.
──────────────────────────────────────────────────────────────────────
CodeView is a good choice for debugging C, Pascal, BASIC, and FORTRAN
programs. The fact that versions of MASM earlier than 5.0 do not
generate data for CodeView makes CodeView a poorer choice for these
assembly-language programs. These disadvantages must be weighed
against the ability to set watchpoints and to trap nonmaskable
interrupts (NMIs). CodeView is also not as well suited as SYMDEB for
debugging programs that interact with TSRs and device drivers, because
CodeView does not provide any mechanism for including symbol tables
for routines not linked together.
Hardware debugging aids
Hardware debuggers are a combination of hardware and software designed
to be installed in a PC system. The software provides features much
like those available with SYMDEB and CodeView. The advantages of
hardware debuggers over purely software debuggers can be summarized in
three points:
■ Crash protection
■ Manual execution break
■ Hardware breakpoints
A hardware debugger can provide program crash protection because of
its independence from the PC software. If the program being debugged
goes wild and destroys the operating system of the PC, the hardware
debugger is protected by virtue of being a separate hardware system
and is capable of recovering enough control to allow the user to find
out what happened.
All hardware debuggers offer a means of breaking into the program
under test from some external source--usually a push button in the
hands of the programmer. The mechanism used to get the attention of
the PC's CPU is the nonmaskable interrupt (NMI). This interrupt
provides a more reliable means of interrupting program execution than
the Break key because its operation is independent of the state of
interrupts and other conditions.
Hardware debuggers usually have access to the address and data lines
on the PC bus, allowing them to set hardware breakpoints. Thus, these
debuggers can be set to break when specific addresses are referenced.
They execute the breakpoint code from a debugging monitor, which
generally runs from their own memory. This memory is usually protected
from the regular operating system and the application program.
Although hardware debuggers can be used to instrument a program, they
should not be confused with the external hardware instrumentation
discussed earlier in this article. The logic analyzers and in-circuit
emulators mentioned there are general-purpose test instruments; the
hardware debuggers are highly specific devices intended to do only one
thing on one type of hardware--provide debugging monitor functions at
a hardware level to IBM PC-type machines. It is this specialization
that makes hardware debuggers so much easier to use for programmers
trying to get a piece of code running.
Because this volume deals only with MS-DOS and associated Microsoft
software, a detailed discussion of hardware debuggers and debugging
would not be appropriate. Instead, a few popular hardware products
that work with MS-DOS utilities are mentioned and a general discussion
of debugging with hardware is presented.
Several manufacturers make hardware products that can be used for
debugging. These products vary in the features offered and in their
suitability for various kinds of debugging. Three of these products
that can be used with SYMDEB are
■ IBM Professional Debug Utility
■ PC Probe and AT Probe from Atron Corporation
■ Periscope from The Periscope Company, Inc.
These boards can be used with SYMDEB by specifying the /N switch when
the program is started. When used in this way, however, the hardware
provides little more than a source of NMIs to interrupt program
execution; otherwise, SYMDEB runs as usual. This restriction may not
be acceptable to a programmer who wants to use the sophisticated
debugging software that accompanies these products and makes use of
their hardware features. For this reason, these boards are rarely used
with SYMDEB.
The general techniques of debugging with hardware aids will already be
familiar to the reader--they are the same techniques discussed at
length earlier in this article. The techniques of inspection and
observation should still be applied; instrumentation is facilitated by
hardware; a debugging monitor accompanies all hardware debuggers and
the same techniques discussed for DEBUG, SYMDEB, and CodeView apply.
No new techniques are needed to use these devices. The changes in the
details of the techniques come with the added features available with
the hardware debuggers. (Remember that all these features are not
universally available on all hardware debuggers.)
The manual interrupt feature of hardware debuggers is useful in a
system crash. Every programmer, especially assembly-language
programmers, has had the situation where the program runs wild,
destroys the operating system, and locks up the system. The techniques
described in previous sections of this article show that about the
only way to solve these problems without hardware help is to set
breakpoints at strategic locations in the program and see how many are
passed before the system locks up. The breakpoints are placed at finer
and finer increments until the instruction causing the crash is
located.
This long and ugly procedure can sometimes be shortened with a
hardware debugger. When the system crashes, the programmer can push
the manual interrupt button, suspend program execution, and give
control to the debugger card. At this point, the programmer can use
the debugging monitor software supplied with the card to sniff around
memory looking for something suspicious. Clues can sometimes be found
by examining the program's stack and data areas--provided, of course,
that they are still in memory and haven't been destroyed, along with
the operating system, by the rampaging program. This approach is not
always an immediate solution to the problem, however; often, the
start-and-set-breakpoints process has to be repeated even with a
hardware debugger. The hardware will, however, possibly shed some
light on the causes of the problem and shorten the procedure.
Another feature offered by many of the debugging boards is the ability
to set breakpoints on events other than the execution of a line of
code. Often, these boards will allow the programmer to break on a
reference to a specific memory location, to a range of memory
locations, or to an I/O port. This feature allows a watch to be set on
data, analogous to the watchpoint feature of CodeView. This technique
is almost always useful, as it is with CodeView, but there is one
class of problems where it is essential to reaching a solution.
Consider the case of a program that seems to be running well. Every so
often, however, an ampersand appears in the middle of a payroll
amount, or occasionally the program makes an erroneous branch and
executes the wrong path. Suppose that, after painstaking investi-
gation, the programmer discovers that these problems are being
caused by a change in a specific location in memory sometime during
the execution of the program. In debugging, the discovery of the cause
of a problem usually leads almost instantly to a fix. Not so in this
case. That byte of memory could be changed by an error in the program,
by a glitch in the operating system or in a device driver, or by
cosmic rays from outer space. Discovering the culprit in a case like
this is almost impossible without the help of hardware breakpoints.
Setting a breakpoint on the affected memory location and running the
program will solve the problem. As soon as the memory location is
changed, the breakpoint will be executed and the state of the system
registers will point a clear finger at the instruction that caused the
problem.
Hardware debuggers can provide significant aid to the serious
programmer. They are especially helpful in debugging operating systems
and operating-system services such as device drivers. They are also
helpful in complicated situations where many programs may be running
at the same time. The consensus among programmers who have hardware
debuggers is that they are well worth the money.
Summary
Although Microsoft and others have provided an impressive array of
technology to aid in program debugging, the most important tool a
programmer has is his or her native wit and talent. As the examples in
this article have illustrated, the technology makes the task easier,
but never easy. In all cases, however, it is the programmer who debugs
the program and solves the problems.
Technology will never be able to replace the person for solving the
problem of a bug-ridden program. (This is an area where artificial
intelligence will undoubtedly fail.) Therefore, it is the skills
discussed in the first part of this article--debugging by inspection
and observation--that deserve the greatest attention and practice. All
the other techniques and technologies, with their ever-increasing
sophistication, are only extensions of these basic techniques. A
programmer who can debug effectively at the lowest level of technology
will always be ready to use whatever advanced technology is available.
Therefore, as a final word, remember the rule that opened this
article:
Gather enough information and the solution will be obvious.
All the rest of this article was merely a discussion of ways to gather
the information.
Steve Bostwick
Article 19: Object Modules
Object modules are used primarily by programmers. The end user of an
MS-DOS application need never be concerned with object code, object
modules, and object libraries because application programs are almost
always distributed as .EXE or .COM files that can be executed with a
simple startup command.
An application programmer writing in a high-level language can use
object modules and object libraries without knowing either the format
of object code or the details of what the utilities that process
object modules, such as the Microsoft Library Manager (LIB) and the
Microsoft Object Linker (LINK), are actually doing. Most application
programmers simply regard the contents of an object module as a "black
box" and trust their compilers and object module utility programs to
do the right thing.
A programmer using assembly language or an assembly-language debugger
such as DEBUG or SYMDEB, however, might want to know more about the
content and function of object modules. The use of assembly language
gives the programmer more control over the actual contents of object
modules, so knowing how the modules are constructed and examining
their contents can sometimes help with program debugging.
Finally, a programmer writing a compiler, an assembler, or a language
translator must know the details of object module format and
processing. To take advantage of LIB and LINK, a language translator
must construct object modules that conform to the format and usage
conventions specified by Microsoft.
Note: This article assumes some background knowledge of the process by
which source code is converted into an executable file in the MS-DOS
environment. See PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING
FOR MS-DOS: Structure of an Application Program; PROGRAMMING TOOLS:
The Microsoft Object Linker; PROGRAMMING UTILITIES.
The Use of Object Modules
Although some MS-DOS language translators generate executable 8086-
family machine code directly from source code, most produce object
code instead. Typically, a translator processes each file of source
code individually and leaves the resulting object module in a separate
file bearing a .OBJ extension. The source-code files themselves remain
unchanged. After all of a program's source-code modules have been
translated, the resulting object modules can be linked into a single
executable program. Because object modules frequently represent only a
portion of a complete program, each source-code module usually
contains instructions that indicate how its corresponding object code
is to be combined with the object code in other object modules when
they are linked.
The object code contained in each object module consists of a binary
image of the program plus program structure information. This object
code is not directly executable. The binary image corresponds to the
executable code that will ultimately be loaded into memory for
execution; it contains both machine code and program data. The program
structure information includes descriptions of logical groupings
defined in the source code (such as named subroutines or segments) and
symbolic references to addresses in other object modules.
The program structure information is used by a linkage editor, or
linker, such as Microsoft LINK to edit the binary image of the program
contained in the object module. The linker combines the binary images
from one or more object modules into a complete executable program.
The linker's output is a .EXE file--a file containing executable
machine code that can be loaded into RAM and executed (Figure 19-1).
The linker leaves intact all of the object modules it processes.
┌───────────────────┐
│ Source code │
└─────────┬─────────┘
│ Language translator or assembler
┌──────────────────┐ ┌───────────────────┐
│ │ │ │
│ Object module │── Object module ──│ Object library │
│ (.OBJ file) │ librarian (LIB) │ (.LIB file) │
└─────────┬─────────┘ └─────────┬─────────┘
└─────────────────────┬───────────────────┘
│ Linker (LINK)
┌──────────────────┐
│ │
│ Executable │
│ binary image │
│ (.EXE file) │
│ │
└─────────┬─────────┘
│ MS-DOS loader
(Program runs)
Figure 19-1. Generation of an executable (.EXE) file.
Object code thus serves as an intermediate form for compiled programs.
This form offers two major advantages:
■ Modular intermediate code. The use of object modules eliminates the
overhead of repeated compilation of an entire program whenever
changes are made to parts of its source code. Instead, only those
object modules affected by source-code revisions need be
recompiled.
■ Shareable format. Object module format is well defined, so object
modules can be linked even if they were produced by different
translators. Many high-level-language compilers take advantage of
this commonality of object-code format to support "interlanguage"
linkage.
Contents of an object module
Object modules contain five basic types of information. Some of this
information exists explicitly in the source code (and is subsequently
passed on to the object module), but much is inferred by the program
translator from the structure of the source code and the way memory is
accessed by the 8086.
Binary Image. As described earlier, the binary image comprises ex-
ecutable code (such as opcodes and addresses) and program data. When
object modules are linked, the linker builds an executable program
from the binary image in each object module it processes. The binary
image in each object module is always associated with program
structure information that tells the linker how to combine it with
related binary images in other object modules.
External References. Because an object module generally represents
only a small portion of a larger program that will be constructed from
several object modules, it usually contains symbols that allow it to
be linked to the other modules. Such references to corresponding
symbols in other object modules are resolved when the modules are
linked.
For example, consider the following short C program:
main()
{
puts("Hello, world\n");
}
This program calls the C function puts() to display a character
string, but puts() is not defined in the source code. Rather, the name
puts is a reference to a function that is external to the program's
main() routine. When the C compiler generates an object module for
this program, it will identify puts as an external reference. Later,
the linker will resolve the external reference by linking the object
module containing the puts() routine with the module containing the
main() routine.
Address References. When a program is built from a group of object
modules, the actual values of many addresses cannot be computed until
the linker combines the binary image of executable code and the
program data from each of the program's constituent object modules.
Object modules contain information that tells the linker how to
resolve the values of such addresses, either symbolically (as in the
case of external references) or relatively, in terms of some other
address (such as the beginning of a block of executable code or
program data).
Debugging Information. An object module can also contain information
that relates addresses in the executable program to the corresponding
source code. After the linker performs its address fixups, it can use
the object module's debugging information to relate a line of source
code in a program module to the executable code that corresponds to
it.
Miscellaneous Information. Finally, an object module can contain
comments, lists of symbols defined in or referenced by the module,
module identification information, and information for use by an
object library manager or a linker (for example, the names of object
libraries to be searched by default).
Object module terminology
When the linker generates an executable program, it organizes the
structural components of the program according to the information
contained in the object modules. The layout of the executable program
can be conceptually described as a run-time memory map after it has
been loaded into memory.
The basic structure of every executable program for the 8086 family of
microprocessors must conform to the segmented architecture of the
microprocessor. Thus, the run-time memory map of an executable program
is partitioned into segments, each of which can be addressed by using
one of the microprocessor's segment registers. This segmented
structure of 8086-based programs is the basis for most of the
following terminology.
Frames. The memory address space of the 8086 is conceptually divided
into a sequence of paragraph-aligned, overlapping 64 KB regions called
frames. Frame 0 in the 8086's address space is the 64 KB of memory
starting at physical address 00000H (0000:0000 in segment:offset
notation), frame 1 is the 64 KB of memory starting at 00010H
(0001:0000), and so on. A frame number thus denotes the beginning of
any paragraph-aligned 64 KB of memory. For example, the location of a
64 KB buffer that starts at address B800:0000 can be specified as
frame 0B800H.
Logical Segments. The run-time memory map for every 8086 program is
partitioned into one or more logical segments, which are groupings of
logically related portions of the program. Typically, an MS-DOS
program includes at least one code segment (that contains all of the
program's executable code), one or more data segments (that contain
program data), and one stack segment.
When a program is loaded into RAM to be executed, each logical segment
in the program can be addressed with a frame number--that is, a
physical 8086 segment address. Before the MS-DOS loader transfers
control to a program in memory, it initializes the CS and SS registers
with the segment addresses of the program's executable code and stack
segments. If an MS-DOS program has a separate logical segment for
program data, the program itself usually stores this segment's address
in the DS register.
Relocatable Segments. In MS-DOS programs, most logical segments are
relocatable. The loader determines the physical addresses of a
program's relocatable segments when it places the program into memory
to be executed. However, this address determination poses a problem
for the MS-DOS loader, because a program may contain references to the
address of a relocatable segment even though the address value is not
determined until the program is loaded. The problem is solved by
indicating where such references occur within the program's object
modules. The linker then extracts this information from the object
modules and uses it to build a list of such address references into a
segment relocation table in the header of executable files. After the
loader copies a program into memory for execution, it uses the segment
relocation table to update, or fix up, the segment address references
within the program.
Consider the following example, in which a program loads the starting
addresses of two data segments into the DS and ES segment registers:
mov ax,seg _DATA
mov ds,ax ; make _DATA segment addressable through DS
mov ax,seg FAR_DATA
mov es,ax ; make FAR_DATA segment addressable through ES
The actual addresses of the _DATA and FAR_DATA segments are unknown
when the source code is assembled and the corresponding object module
is constructed. The assembler indicates this by including segment
fixup information, instead of actual segment addresses, in the
program's object module. When the object module is linked, the linker
builds this segment fixup information into the segment relocation
table in the header of the program's .EXE file. Then, when the .EXE
file is loaded, the MS-DOS loader uses the information in the .EXE
file's header to patch the actual address values into the program.
Absolute Segments. Sometimes a program needs to address a
predetermined segment of memory. In this case, the program's source
code must declare an absolute segment so that a reference to the
corresponding frame number can be built into the program's object
module.
For example, a program might need to address a video display buffer
located at a specific physical address. The following assembler
directive declares the name of the segment and its frame number:
VideoBufferSeg SEGMENT at 0B800h
Segment Alignment. When a program is loaded, the physical address of
each logical segment is constrained by the segment's alignment. A
segment can be page aligned (aligned on a 256-byte boundary),
paragraph aligned (aligned on a 16-byte paragraph boundary), word
aligned (aligned on an even-byte boundary), or byte aligned (not
aligned on any particular boundary). A specification of each segment's
alignment is part of every object module's program structure
information.
High-level-language translators generally align segments according to
the type of data they contain. For example, executable code segments
are usually byte aligned; program data segments are usually word
aligned. With an assembler, segment alignment can be specified with
the SEGMENT directive and the assembler will build this information
into the program's object module.
Concatenated Segments. The linker can concatenate logical segments
from different object modules when it builds the executable program.
For example, several object modules may each contain part of a
program's executable code. When the linker processes these object
modules, it can concatenate the executable code from the different
object modules into one range of contiguous addresses.
The order in which the linker concatenates logical segments in the
executable program is determined by the order in which the linker
processes its input files and by the program structure information in
the object modules. With a high-level-language translator, the
translator infers which segments can be concatenated from the
structure of the source code and builds appropriate segment
concatenation information into the object modules it generates. With
an assembler, the segment class type can be used to indicate which
segments can be concatenated.
Groups of Segments. Segments with different names may also be grouped
together by the linker so that they can all be addressed within the
same 64 KB frame, even though they are not concatenated. For example,
it might be desirable to group program data segments and a stack
segment within the same 64 KB frame so that program data items and
data on the stack can be addressed with the same 8086 segment
register.
In high-level languages, it is up to the translator to incorporate
appropriate segment grouping information into the object modules it
generates. With an assembler, groups of segments can be declared with
the GROUP directive.
Fixups. Sometimes a compiler or an assembler encounters addresses
whose values cannot be determined from the source code. The addresses
of external symbols are an obvious example. The addresses of
relocatable segments and of labels within those segments are another
example.
A fixup is a language translator's way of passing the buck about such
addresses to the linker. Typically, a translator builds a zero value
in the binary image at locations where it cannot store an actual
address. Accompanying each such location is fixup information, which
allows the linker to determine the correct address. The linker then
completes the fixup by calculating the correct address value and
adding it to the value in the corresponding location in the binary
image. The only fixups the linker cannot fully resolve are those that
refer to the segment address of a relocatable segment. Such addresses
are not known until the program is actually loaded, so the linker, in
turn, passes the responsibility to the MS-DOS loader by creating a
segment relocation table in the header of the executable file.
To process fixups properly, the linker needs three pieces of
information: the LOCATION of the value in the object module, the
nature of the TARGET (the address whose value is not yet known), and
the FRAME in which the address calculations are to take place. Object
modules contain the LOCATION, TARGET, and FRAME information the linker
uses to calculate the appropriate address for any given fixup.
Consider the "program" in Figure 19-2. The statement:
start: call far ptr FarProc
contains a reference to an address in the logical segment FarSeg2.
Because the assembler does not know the address of FarSeg2, it places
fixup information about the address into the object module. The
LOCATION to be fixed up is 1 byte past the label start (the 4-byte
pointer following the call opcode 9AH). The TARGET is the address
referenced in the call instruction--that is, the label FarProc in the
segment FarSeg2. The FRAME to which the fixup relates is designated by
the group FarGroup and is inferred from the statement
ASSUME cs:FarGroup
in the FarSeg2 segment.
──────────────────────────────────────────────────────────────────────
Figure 19-2. A sample "program" containing statements from which the
assembler derives fixup information.
──────────────────────────────────────────────────────────────────────
There are several different ways for a language translator to identify
a fixup. For example, the LOCATION might be a single byte, a 16-bit
offset, or a 32-bit pointer, as in Figure 19-2. The TARGET might be a
label whose offset is relative either to the base (beginning) of a
particular segment or to the LOCATION itself. The FRAME might be a
relocatable segment, an absolute segment, or a group of segments.
Taken together, all the information in an object module that concerns
the alignment and grouping of segments can be regarded as a
specification of a program's run-time memory map. In effect, the
object module specifies what goes where in memory when a program is
loaded. The linker can then take the program structure information in
the object modules and generate a file containing an executable
program with the corresponding structure.
The Structure of an Object Module
Although object modules contain the information that ultimately
determines the structure of an executable program, they bear little
structural resemblance to the resulting executable program. Each
object module is made up of a sequence of variable-length object
records. Different types of object records contain different types of
program information.
Each object record begins with a 1-byte field that identifies its
type. This is followed by a 2-byte field containing the length (in
bytes) of the remainder of the record. Next comes the actual
structural or program information, represented in one or more fields
of varied lengths. Finally, each record ends with a 1-byte checksum.
The sequence in which object records appear in an object module is
important. Because the records vary in length, each object module must
be constructed linearly, from start to end. More important, however,
is the fact that some types of object records contain references to
preceding object records. Because the linker processes object records
sequentially, the position of each object record within an object
module depends primarily on the type of information each record
contains.
Types of object records
Microsoft LINK currently recognizes 14 types of object records, each
of which carries a specific type of information within the object
module. Each type of object record is assigned an identifying six-
letter abbreviation, but these abbreviations are used only in
documentation, not within an object module itself. As already
mentioned, the first byte of each object record contains a value that
indicates its type. In a hexadecimal dump of the contents of an object
module, these identifying bytes identify the start of each object
record.
Table 19-1 lists the types of object records supported by LINK. The
value of each record's identifying byte (in hexadecimal) is included,
along with the six-letter abbreviation and a brief functional
description. The functions of the 14 types of object records fall into
six general categories:
■ Binary data (executable code and program data) is contained in the
LEDATA and LIDATA records.
■ Address binding and relocation information is contained in FIXUPP
records.
■ The structure of the run-time memory map is indicated by SEGDEF,
GRPDEF, COMDEF, and TYPDEF records.
■ Symbol names are declared in LNAMES, EXTDEF, and PUBDEF records.
■ Debugging information is in the LINNUM record.
■ Finally, the structure of the object module itself is determined by
the THEADR, COMENT, and MODEND records.
Table 19-1. Types of 8086 Object Records Supported by
Microsoft LINK.
╓┌────────────────┌──────────────────────┌───────────────────────────────────╖
ID byte Abbreviation Description
──────────────────────────────────────────────────────────────────
80H THEADR Translator Header Record
88H COMENT Comment Record
8AH MODEND Module End Record
8CH EXTDEF External Names Definition Record
8EH TYPDEF Type Definition Record
90H PUBDEF Public Names Definition Record
94H LINNUM Line Number Record
96H LNAMES List of Names Record
98H SEGDEF Segment Definition Record
9AH GRPDEF Group Definition Record
9CH FIXUPP Fixup Record
0A0H LEDATA Logical Enumerated Data Record
0A2H LIDATA Logical Iterated Data Record
0B0H COMDEF Communal Names Definition Record
Object record order
The sequence in which the types of object records appear in an object
module is fairly flexible in some respects. Several record types are
optional, and if the type of information they carry is unnecessary,
they are omitted from an object module. In addition, most object
record types can occur more than once in the same object module. And,
because object records are variable in length, it is often possible to
choose, as a matter of convenience, between combining information into
one large record or breaking it down into several smaller records of
the same type.
As stated previously, an important constraint on the order in which
object records appear is the need for some types of object records to
refer to information contained in other records. Because the linker
processes the records sequentially, object records containing such
information must precede the records that refer to it. For example,
two types of object records, SEGDEF and GRPDEF, refer to the names
contained in an LNAMES record. Thus, an LNAMES record must appear be-
fore any SEGDEF or GRPDEF records that refer to it so that the names
in the LNAMES record are known to the linker by the time it processes
the SEGDEF or GRPDEF records.
A typical object module
Figure 19-3 contains the source code for HELLO.ASM, an assembly-
language program that displays a short message. Figure 19-4 is a
hexadecimal dump of HELLO.OBJ, the object module generated by
assembling HELLO.ASM with the Microsoft Macro Assembler. Figure 19-5
isolates the object records within the object module.
──────────────────────────────────────────────────────────────────────
Figure 19-3. The source code for HELLO.ASM.
──────────────────────────────────────────────────────────────────────
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 80 07 00 05 48 45 4C 4C 4F 00 96 25 00 00 04 43 ....HELLO..%...C
0010 4F 44 45 04 44 41 54 41 05 53 54 41 43 4B 05 5F ODE.DATA.STACK._
0020 44 41 54 41 06 5F 53 54 41 43 4B 05 5F 54 45 58 DATA._STACK._TEX
0030 54 8B 98 07 00 28 11 00 07 02 01 1E 98 07 00 48 T....(.........H
0040 0F 00 05 03 01 01 98 07 00 74 00 01 06 04 01 E1 .........t......
0050 A0 15 00 01 00 00 B8 00 00 8E D8 BA 00 00 B4 09 ................
0060 CD 21 B8 00 4C CD 21 D5 9C 0B 00 C8 01 04 02 02 .!..L.!.........
0070 C4 06 04 02 02 B6 A0 13 00 02 00 00 48 65 6C 6C ............Hell
0080 6F 2C 20 77 6F 72 6C 64 0D 0A 24 A8 8A 07 00 C1 o, world..$.....
0090 00 01 01 00 00 AC ......
Figure 19-4. A hexadecimal dump of HELLO.OBJ.
0 1 2 3 4 5 6 7 8 9 A B C D E F
THEADR
0000 80 07 00 05 48 45 4C 4C 4F 00 ....HELLO.
LNAMES
0000 96 25 00 00 04 43 .%...C
0010 4F 44 45 04 44 41 54 41 05 53 54 41 43 4B 05 5F ODE.DATA.STACK._
0020 44 41 54 41 06 5F 53 54 41 43 4B 05 5F 54 45 58 DATA._STACK._TEX
0030 54 8B T.
SEGDEF
0030 98 07 00 28 11 00 07 02 01 1E ...(......
SEGDEF
0030 98 07 00 48 ...H
0040 0F 00 05 03 01 01 ......
SEGDEF
0040 98 07 00 74 00 01 06 04 01 E1 ...t......
LEDATA
0050 A0 15 00 01 00 00 B8 00 00 8E D8 BA 00 00 B4 09 ................
0060 CD 21 B8 00 4C CD 21 D5 .!..L.!.
FIXUPP
0060 9C 0B 00 C8 01 04 02 02 ........
0070 C4 06 04 02 02 B6 ......
LEDATA
0070 A0 13 00 02 00 00 48 65 6C 6C ......Hell
0080 6F 2C 20 77 6F 72 6C 64 0D 0A 24 A8 o, world..$.
MODEND
0080 8A 07 00 C1 ....
0090 00 01 01 00 00 AC ......
Figure 19-5. The object records in HELLO.OBJ.
As shown most clearly in Figure 19-5, each of the object records
begins with the single byte value identifying the record's type. The
second and third bytes of each record contain a single 16-bit value,
stored with its low-order byte first, that represents the length (in
bytes) of the remainder of the object record.
The first record, THEADR, identifies the object module and the last
record, MODEND, terminates the object module. The second record,
LNAMES, contains a list of segment names and segment class names that
LINK will use to lay out the run-time memory map. The three succeeding
SEGDEF records describe the three corresponding segments defined in
the source code.
The order in which the object records appear reflects both the
structure of the source code and the record order constraints already
mentioned. The LNAMES record appears before the three SEGDEF records
because each SEGDEF record contains a reference to a name in the
LNAMES record.
The binary data representing each of the two segments in the source
code is contained in the two LEDATA records. The first LEDATA record
represents the _TEXT segment; the second specifies the data in the
_DATA segment. The FIXUPP record following the first LEDATA record
contains information about the address references in the _TEXT
segment. Again, the order in which the records appear is important:
the FIXUPP record refers to the LEDATA record preceding it.
References between object records
Object records can refer to information in other records either
indirectly, by means of implicit references, or directly, by means of
indexed references to names or other records.
Implicit References. Some types of object records implicitly reference
another record in the same object module. The most important example
of such implicit referencing is in the FIXUPP record, which always
contains fixup information for the preceding LEDATA or LIDATA record
in the object module. Whenever an LEDATA or LIDATA record contains a
value that needs to be fixed up, the next record in the object module
is always a FIXUPP record containing the actual fixup information.
Indexed References to Names. An object record that refers to a
symbolic name, such as the name of a segment or an external routine,
uses an index into a list of names contained in a previous object
record. (The LNAMES record in Figure 19-5 is an example.) The first
name in such a list has the index number 1, the second name has index
number 2, the third has index number 3, and so on. Altogether, a list
of as many as 32,767 (7FFFH) names can be incorporated into an object
module--generally adequate for even the most verbose programmer. (LINK
does, however, impose its own version-specific limits.)
Indexed References to Object Records. An object record can also refer
to a previous object record by using the same type of index. In this
case, the index number refers to one of a list of object records of a
particular type. For example, a FIXUPP record might refer to a segment
by referencing one of several preceding SEGDEF records in the object
module. In that case, a value of 1 would indicate the first SEGDEF
record in the object module, a value of 2 would indicate the second,
and so on.
The index-number field in an object record can be either 1 or 2 bytes
long. If the number is in the range 0-7FH, the high-order bit (bit 7)
is 0 and the low-order 7 bits contain the index number, so the field
is only 1 byte long:
bit 7 6 5 4 3 2 1 0
┌──────┬────────────────────────────────────────────────┐
│ │ │
│ 0 │ index number │
└──────┴────────────────────────────────────────────────┘
If the index number is in the range 80-7FFFH, the field is 2 bytes
long. The high-order bit of the first byte in the field is set to 1,
and the high-order byte of the index number (which must be in the
range 0-7FH) fits in the remaining 7 bits. The low-order byte of the
index number is specified in the second byte of the field:
bit 7 6 5 4 3 2 1 0 7 6
┌──────┬────────────────────────────────────────────────┬────────────────────────────────────────────────────┐
│ │ │ │
│ 1 │ high-order byte of index number │ low-order byte of index number │
└──────┴────────────────────────────────────────────────┴────────────────────────────────────────────────────┘
first byte
The same format is used whether an index refers to a list of names or
to a previous object record.
Microsoft 8086 Object Record Formats
Just as the design of the Intel 8086 microprocessor reflects the
design of its 8-bit predecessors, 8086 object record formats are
reminiscent of the 8-bit software tradition. In 8-bit systems, disk
space and RAM were often at a premium. To minimize the space consumed
by object records, information is packed into bit fields within bytes
and variable-length fields are frequently used.
Microsoft LINK recognizes a major subset of Intel's original 8086
object module specification (Intel Technical Specification 121748-
001). Intel also proposed a six-letter name for each type of object
record and symbolic names for fields. These names are documented in
the following descriptions, which appear in the order shown earlier in
Table 19-1.
The Intel record types that are not recognized by LINK provide
information about an executable program that MS-DOS obtains in other
ways. (For example, information about run-time overlays is supplied in
LINK's command line rather than being encoded in object records.)
Because they are ignored by LINK, they are not included here.
All 8086 object records conform to the following format:
┌──────┬──────┬──────┬───///──┬──────┐
│record│ record │ body │ chk │
│ type │ length │ │ sum │
└──────┴──────┴──────┴───///──┴──────┘
The record type field is a 1-byte field containing the hexadecimal
number that identifies the type of object record (see Table 19-1).
The record length is a 2-byte field that gives the length of the
remainder of the object record in bytes (excluding the bytes in the
record type and record length fields). The record length is stored
with the low-order byte first.
The body field of the record varies in size and content, depending on
the record type.
The checksum is a 1-byte field that contains the negative sum (modulo
256) of all other bytes in the record. In other words, the checksum
byte is calculated so that the low-order byte of the sum of all the
bytes in the record, including the checksum byte, equals zero.
Note: As shown in the preceding example, the boxes used to depict the
fields vary in size. The square boxes used for record type and chksum
indicate a single byte, the rectangular box used for record length
indicates 2 bytes, and the diagonal lines used for body indicate a
variable-length field.
80H THEADR Translator Header Record
The THEADR record contains the name of the object module. This name
identifies an object module within an object library or in messages
produced by the linker.
Record format
┌──────┬──────┬──────┬───///──┬──────┐
│ │ │ │T-module│ chk │
│ 80H │ length │ name │ sum │
└──────┴──────┴──────┴───///──┴──────┘
T-module name
The T-module name field is a variable-length field that contains the
name of the object module. The first byte of the field contains the
number of subsequent bytes that contain the name itself. The name can
be uppercase or lowercase and can be any string of characters.
The T-module name is used by LIB and LINK within error messages.
Language translators frequently derive the T-module name from the name
of the file that contains a program's source code. Assembly-language
programmers can specify the T-module name explicitly with the
assembler NAME directive.
Location in object module
As its name implies, the THEADR record must be the first record in
every object module generated by a language translator.
Example
The following THEADR record was generated by the Microsoft C Compiler:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 80 09 00 07 68 65 6C 6C 6F 2E 63 CB ....hello.c.
■ Byte 00H contains 80H, indicating a THEADR record.
■ Bytes 01-02H contain 0009H, the length of the remainder of the
record.
■ Bytes 03-0AH contain the T-module name. Byte 03H contains 07H, the
length of the name, and bytes 04H through 0AH contain the name
itself (hello.c). (In object modules generated by the Microsoft C
Compiler, the THEADR record indicates the filename that contained
the C source code for the module.)
■ Byte 0BH contains the checksum, 0CBH.
88H COMENT Comment Record
The COMENT record contains a character string that may represent a
plain text comment, a symbol meaningful to a program such as LIB or
LINK, or even binary-encoded identification data. An object module can
contain any number of COMENT records.
Record format
┌──────┬──────┬──────┬──────┬──────┬─────────///────────┬──────┐
│ │ │ │ │ │ │ chk │
│ 88H │ length │attrib│ │ comment │ sum │
└──────┴──────┴──────┴──────┴──┼───┴─────────///────────┴──────┘
└─ comment
class
Attrib
Attrib is a 1-byte field in which only the first 2 bits are
meaningful:
bit 7 6 5 4 3 2 1 0
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ no │ no │ │ │ │ │ │ │
│purge │ list │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
■ If bit 7 (no purge) is set to 1, utility programs that manipulate
object modules should not delete the comment record from the object
module. Bit 7 can thus protect an important comment, such as a
copyright message, from deletion.
■ If bit 6 (no list) is set to 1, utility programs that can list the
contents of object modules are directed not to list the comment.
Bit 6 can thus hide a comment.
■ Bits 5 through 0 are unused and should be set to 0.
Microsoft LIB ignores the attrib field.
Comment class
Comment class is a 1-byte field whose value provides information about
the type of comment. The original Intel specification provided for the
following possible comment class values:
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Value Use
─────────────────────────────────────────────────────────────────────
00H Language-translator comment (the name of the translator
that generated the object module).
01H Copyright comment.
02-9BH Reserved for Intel proprietary software.
Microsoft language translators can generate several other classes of
COMENT record that communicate specific information about the object
module to LINK:
╓┌────────────────┌──────────────────────────────────────────────────────────╖
Value Use
─────────────────────────────────────────────────────────────────────
81H Obsolete; replaced by comment class 9FH.
9CH MS-DOS version number. Some language translators create a
COMENT record with a 2-byte binary value in the comment
field indicating the MS-DOS version under which the
module was created. This record is ignored by LINK.
9DH Memory model. The comment field contains a string that
indicates the memory model used by the language
translator. The string contains one of the lowercase
letters s, c, m, l, and h to designate small, compact,
medium, large, and huge memory models. Microsoft language
translators generate COMENT records with this comment
class only for compatibility with the XENIX version of
LINK. The MS-DOS version of LINK ignores these COMENT
records.
9EH Sets Microsoft LINK's DOSSEG switch.
9FH Default library search name. LINK interprets the contents
of the comment field as the name of a library to be
searched in order to resolve external references within
the object module. The default library search can be
overridden with LINK's NODEFAULTLIBRARYSEARCH switch.
0A1H Indicates that Microsoft extensions to the Intel object
record specification are used in the object module. For
example, when COMDEF records are used within an object
module, a COMENT record with comment class 0A1H must
appear in the object module at some point before the
first COMDEF record. LINK ignores the comment string in
COMENT records with this comment class.
0C0H-0FFH Reserved for user-defined comment classes.
Comment
The comment field is a variable-length string of bytes that represent
the comment. The length of the string is inferred from the length of
the object record.
Location in object module
A COMENT record can appear almost anywhere in an object module. Only
two restrictions apply:
■ A COMENT record cannot be placed between a FIXUPP record and the
LEDATA or LIDATA record to which it refers.
■ A COMENT record cannot be the first or last record in an object
module. (The first record must always be a THEADR record and the
last must always be MODEND.)
Examples
The following three examples are typical COMENT records taken from an
object module generated by the Microsoft C Compiler.
This first example is a language-translator comment:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 88 07 00 00 00 4D 53 20 43 6E .....MS Cn
■ Byte 00H contains 88H, indicating that this is a COMENT record.
■ Bytes 01-02H contain 0007H, the length of the remainder of the
record.
■ Byte 03H (the attrib field) contains 00H. Bit 7 (no purge) is set
to 0, indicating that this COMENT record may be purged from the
object module by a utility program that manipulates object modules.
Bit 6 (no list) is set to 0, indicating that this comment need not
be excluded from any listing of the module's contents. The
remaining bits are all 0.
■ Byte 04H (the comment class field) contains 00H, indicating that
this COMENT record contains the name of the language translator
that generated the object module.
■ Bytes 05H through 08H contain the name of the language translator,
MS C.
■ Byte 09H contains the checksum, 6EH.
The second example contains the name of an object library to be
searched by default when LINK processes the object module containing
this COMENT record:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 88 09 00 00 9F 53 4C 49 42 46 50 10 .....SLIBFP.
■ Byte 04H (the comment class field) contains 9FH, indicating that
this record contains the name of a library for LINK to use to
resolve external references.
■ Bytes 05-0AH contain the library name, SLIBFP. In this example, the
name refers to the Microsoft C Compiler's floating-point function
library, SLIBFP.LIB.
The last example indicates that the object module contains Microsoft-
defined extensions to the Intel object module specification:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 88 06 00 00 A1 01 43 56 37 .....CV7
■ Byte 04H indicates the comment class, 0A1H.
■ Bytes 05-07H, which contain the comment string, are ignored by
LINK.
8AH MODEND Module End Record
The MODEND record denotes the end of an object module. It also
indicates whether the object module contains the main routine in a
program, and it can, optionally, contain a reference to a program's
entry point.
Record format
┌──────┬──────┬──────┬──────┬─────────///────────┬──────┐
│ │ │module│ │ chk │
│ 8AH │ length │ type │ start address │ sum │
└──────┴──────┴──────┴──────┴─────────///────────┴──────┘
Module type
The module type field is an 8-bit (1-byte) field:
bit 7 6 5 4 3 2 1 0
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ │ │ │ │ │ │ │ │
│main │start │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
■ Bit 7 (main) is set to 1 if the module is a main program module.
■ Bit 6 (start) is set to 1 if the MODEND record contains an entry
point (start address).
■ Bit 0 is set to 1 if the start address field contains a relocatable
address reference that LINK must fix up. If bit 6 is set to 1, bit
0 must also be set to 1. (The Intel specification allows bit 0 to
be set to 0, to indicate that start address is an absolute physical
address, but this capability is not supported by LINK.)
Start address
The start address field appears in the MODEND record only when bit 6
is set to 1:
┌──────┬─────────///────────┬─────────///────────┬─────────///────────┐
│ end │ │ │ target │
│ dat │ frame datum │ target datum │ displacement │
└──────┴────────///─────────┴─────────///────────┴─────────///────────┘
The format and interpretation of the start address field corresponds
to the fixup field of the FIXUPP record. The end dat field corresponds
to the fix dat field in the FIXUPP record. Bit 2 of the end dat field,
which corresponds to the P bit in a fix dat field, must be zero.
Location in object module
A MODEND record can appear only as the last record in an object
module.
Example
Consider the MODEND record of the HELLO.ASM example:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 8A 07 00 C1 00 01 01 00 00 AC ..........
■ Byte 00H contains 8AH, indicating a MODEND record.
■ Bytes 01-02H contain 0007H, the length of the remainder of the
record.
■ Byte 03H contains 0C1H (11000001B). Bit 7 is set to 1, indicating
that this module is the main module of the program. Bit 6 is set to
1, indicating that a start address field is present. Bit 0 is set
to 1, indicating that the address referenced in the start address
field must be fixed up by LINK.
■ Byte 04H (end dat in the start address field) contains 00H. As in a
FIXUPP record, bit 7 indicates that the frame for this fixup is
specified explicitly, and bits 6 through 4 indicate that a SEGDEF
index specifies the frame. Bit 3 indicates that the target
reference is also specified explicitly, and bits 2 through 0
indicate that a SEGDEF index also specifies the target. See also
9CH FIXUPP Fixup Record, below.
■ Byte 05H (frame datum in the start address field) contains 01H.
This is a reference to the first SEGDEF record in the module, which
in this example corresponds to the _TEXT segment. This reference
tells LINK that the start address lies in the _TEXT segment of the
module.
■ Byte 06H (target datum in the start address field) contains 01H.
This too is a reference to the first SEGDEF record in the object
module, which corresponds to the _TEXT segment. LINK uses the
following target displacement field to determine where in the _TEXT
segment the address lies.
■ Bytes 07-08H (target displacement in the start address field)
contain 0000H. This is the offset (in bytes) of the start address.
■ Byte 09H contains the checksum, 0ACH.
8CH EXTDEF External Names Definition Record
The EXTDEF record contains a list of symbolic external references--
that is, references to symbols defined in other object modules. The
linker resolves external references by matching the symbols declared
in EXTDEF records with symbols declared in PUBDEF records.
Record format
┌──────┬──────┬──────┬──────────────///────────────┬──────┐
│ │ │ │ │ chk │
│ 8CH │ length │ external reference list │ sum │
└──────┴──────┴──────┴──────────────///────────────┴──────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
can be
repeated
External reference list
The external reference list is a variable-length field containing a
list of names and name types, each formatted as follows:
┌──────┬─────────///────────┬──────┐
│ name │ │type │
│length│ name │index │
└──────┴─────────///────────┴──────┘
■ The name length is a 1-byte field containing the length of the name
field that follows it. (LINK restricts name length to a value
between 01H and 7FH.)
■ The type index is a 1-byte reference to the TYPDEF record in the
object module that describes the type of symbol the name
represents. A type index value of zero indicates that no TYPDEF
record is associated with the symbol. A nonzero value indicates
which TYPDEF record is associated with the external name. Microsoft
LINK recognizes TYPDEF records only for the purpose of declaring
communal variables. See 8EH TYPDEF Type Definition Record, below.
LINK imposes a limit of 1023 external names.
Location in object module
Any EXTDEF records in an object module must appear before the FIXUPP
records that reference them. Also, if an EXTDEF record contains a
nonzero type index, the indexed TYPDEF record must precede the EXTDEF
record.
Example
Consider this EXTDEF record generated by the Microsoft C Compiler:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 8C 25 00 0A 5F 5F 61 63 72 74 75 73 65 64 00 05 .%..__acrtused..
0010 5F 6D 61 69 6E 00 05 5F 70 75 74 73 00 08 5F 5F _main.._puts..__
0020 63 68 6B 73 74 6B 00 A5 chkstk..
■ Byte 00H contains 8CH, indicating that this is an EXTDEF record.
■ Bytes 01-02H contain 0025H, the length of the remainder of the
record.
■ Bytes 03-26H contain a list of external references. The first
reference starts in byte 03H, which contains 0AH, the length of the
name acrtused. The name itself follows in bytes 04-0DH. Byte 0EH
contains 00H, which indicates that the symbol's type is not defined
by any TYPDEF record in this object module. Bytes 0F-26H contain
similar references to the external symbols _main, _puts, and
chkstk.
■ Byte 27H contains the checksum, 0A5H.
8EH TYPDEF Type Definition Record
The TYPDEF record contains details about the type of data represented
by a name declared in a PUBDEF or an EXTDEF record. This information
may be used by the linker to validate references to names, or it may
be used by a debugger to display data according to type.
Starting with Microsoft LINK version 3.50, the COMDEF record should be
used for declaration of communal variables. For compatibility,
however, later versions of LINK recognize TYPDEF records as well as
COMDEF records.
Record format
┌──────┬──────┬──────┬──────┬─────────///────────┬──────┐
│ │ │ │ │ eight-leaf │ chk │
│ 8EH │ length │ name │ descriptor │ sum │
└──────┴──────┴──────┴──────┴─────────///────────┴──────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
can be
repeated
Although the original Intel specification allowed for many different
type specifications, such as scalar, pointer, and mixed data
structure, LINK uses TYPDEF records to declare only communal
variables. Communal variables represent globally shared memory
areas--for example, FORTRAN common blocks or uninitialized public
variables in C.
The size of a communal variable is declared explicitly in the TYPDEF
record. If a communal variable has different sizes in different object
modules, LINK uses the largest declared size when it generates an
executable module.
Name
The name field of a TYPDEF record is a 1-byte field that is always
null; that is, it contains a single zero byte.
Eight-leaf descriptor
The eight-leaf descriptor field, in the original Intel specification,
was a variable-length field that contained as many as eight "leaves"
that could be used to describe mixed data structures.
Microsoft uses a stripped-down version of the eight-leaf descriptor,
because the field's only function is to describe communal variables:
┌──────┬─────────///────────┐
│ │ │
│ 0 │ leaf descriptor │
└──────┴─────────///────────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
can be
repeated
■ The first field in the eight-leaf descriptor is a 1-byte field that
contains a zero byte.
■ The leaf descriptor field is a variable-length field that is itself
divided into four fields ("leaves") that describe the size and type
of a variable. The two possible variable types are NEAR and FAR.
If the field describes a NEAR variable (one that can be referenced
as an offset within a default data segment), the format is
┌──────┬──────┬────────///────────┐
│ │ │ │
│ 62H │ │ length in bits │
└──────┴──┼───┴────────///────────┘
└───── variable
type
- The 1-byte field containing 62H signifies a NEAR variable.
- The variable type field is a 1-byte field that specifies the
variable type:
77H Array
79H Structure
7BH Scalar
This field is ignored by LINK.
- The length in bits field is a variable-length field that
indicates the size of the communal variable. Its format depends
on the size it represents. If the size is less than 128 (80H)
bits, length in bits is a 1-byte field containing the actual size
of the field:
┌──────┐
│ │
│ size │
└──────┘
If the size is 128 bits or greater, it cannot be represented in a
single byte value, so the length in bits field is formatted with
an extra initial byte that indicates whether the size is
represented as a 2-, 3-, or 4-byte value:
┌──────┬──────┬──────┐
│ │ │
│ 81H │ 2-byte size │
└──────┴──────┴──────┘
┌──────┬──────┬──────┬──────┐
│ │ │
│ 84H │ 3-byte size │
└──────┴──────┴──────┴──────┘
┌──────┬──────┬──────┬──────┬──────┐
│ │ │
│ 88H │ 4-byte size │
└──────┴──────┴──────┴──────┴──────┘
If the leaf descriptor field describes a FAR variable (one that
must be referenced with an explicit segment and offset), the
format is
┌──────┬──────┬────────///─────────┬─────────///────────┐
│ │ │ number of │ element type │
│ 61H │ │ elements │ index │
└──────┴──┼───┴────────///─────────┴─────────///────────┘
└───── variable
type
- The 1-byte field containing 61H signifies a FAR variable.
- The 1-byte variable type for a FAR communal variable is
restricted to 77H (array). (As with the NEAR variable type field,
LINK ignores this field.)
- The number of elements is a variable-length field that contains
the number of elements in the array. It has the same format as
the length in bits field in the leaf descriptor for a NEAR
variable.
- The element type index is an index field that references a
previous TYPDEF record. A value of 1 indicates the first TYPDEF
record in the object module, a value of 2 indicates the second
TYPDEF record, and so on. The TYPDEF record referenced must
describe a NEAR variable. This way, the data type and size of the
elements in the array can be determined.
Location in object module
Any TYPDEF records in an object module must precede the EXTDEF or
PUBDEF records that reference them.
Examples
The following three examples of TYPDEF records were generated by the
Microsoft C Compiler version 3.0. (Later versions use COMDEF records.)
The first sample TYPDEF record corresponds to the public declaration
int foo; /* 16-bit integer */
The TYPEDEF record is
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 8E 06 00 00 00 62 7B 10 7F .....b{..
■ Byte 00H contains 8EH, indicating that this is a TYPDEF record.
■ Bytes 01-02H contain 0006H, the length of the remainder of the
record.
■ Byte 03H (the name field) contains 00H, a null name.
■ Bytes 04-07H represent the eight-leaf descriptor field. The first
byte of this field (byte 04H) contains 00H. The remaining bytes
(bytes 05-07H) represent the leaf descriptor field:
- Byte 05H contains 62H, indicating this TYPDEF record describes a
NEAR variable.
- Byte 06H (the variable type field) contains 7BH, which describes
this variable as a scalar.
- Byte 07H (the length in bits field) contains 10H, the size of the
variable in bits.
■ Byte 08H contains the checksum, 7FH.
The next example demonstrates how the variable size contained in the
length in bits field of the leaf descriptor is formatted:
char foo2[32768]; /* 32 KB array */
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 8E 09 00 00 00 62 7B 84 00 00 04 04 .....b{.....
■ The length in bits field (bytes 07-0AH) starts with a byte
containing 84H, which indicates that the actual size of the
variable is represented as a 3-byte value (the following 3 bytes).
Bytes 08-0AH contain the value 040000H, the size of the 32 KB array
in bits.
This third C statement, because it declares a FAR variable, causes two
TYPDEF records to be generated:
char far foo3[10][2][20]; /* 400-element FAR array*/
The two TYPDEF records are
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 8E 06 00 00 00 62 7B 08 87 8E 09 00 00 00 61 77 .....b{.......aw
0010 81 90 01 01 7E ....|
■ Bytes 00-08H contain the first TYPDEF record, which defines the
data type of the elements of the array (NEAR, scalar, 8 bits in
size).
■ Bytes 09-14H contain the second TYPDEF record. The leaf
descriptor field of this record declares that the variable is
FAR (byte 0EH contains 61H) and an array (byte 0FH, the variable
type, contains 77H).
- Because this TYPDEF record describes a FAR variable, bytes 10-12H
represent a number of elements field. The first byte of the field
is 81H, indicating a 2-byte value, so the next 2 bytes (bytes 11-
12H) contain the number of elements in the array, 0190H (400D).
■ Byte 13H (the element type index) contains 01H, which is a
reference to the first TYPDEF record in the object module--in this
example, the one in bytes 00-08H.
90H PUBDEF Public Names Definition Record
The PUBDEF record contains a list of public names. When object modules
are linked, the linker uses these names to resolve external references
in other object modules.
Record format
┌──────┬──────┬──────┬─────///─────┬─────///─────┬─────///─────┬─────///─────┬──────┐
│ │ │ │ │ │ │ │ chk │
│ 90H │ length │ public base │ public name │public offset│ type index │ sum │
└──────┴──────┴──────┴─────///─────┴─────///─────┴─────///─────┴─────///─────┴──────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
can be
repeated
Public base
Each name in the PUBDEF record refers to a location (a 16-bit offset)
in a particular segment or group. The public base, a variable-length
field that specifies the segment or group, is formatted as follows:
┌────────///─────────┬─────────///────────┬──────┬──────┐
│ │ │ frame │
│ group index │ segment index │ number │
└────────///─────────┴─────────///────────┴──────┴──────┘
■ Group index is an index field that references a previous GRPDEF
record in the object module. If the group index value is 0, no
group is associated with this PUBDEF record.
■ Segment index is also an index field. It associates a particular
segment with this PUBDEF record by referencing a previous SEGDEF
record. A value of 1 indicates the first SEGDEF record in the
object module, a value of 2 indicates the second, and so on. If the
segment index value is 0, the group index must also be 0--in this
case, the frame number appears in the public base field.
■ The 2-byte frame number appears in the public base field only when
the group index and segment index are both 0. In other words, the
frame number specifies the start of an absolute segment. If
present, the value in the frame number field indicates the number
of the frame containing the public name.
Public name
Public name is a variable-length field containing a public name. The
first byte specifies the length of the name; the remainder is the name
itself. (The Intel specification allows names of 1 to 255 bytes.
Microsoft LINK restricts the maximum length of a public name to 127
bytes.)
Public offset
Public offset is a 2-byte field containing the offset of the location
referred to by the public name. This offset is assumed to lie within
the segment, group, or frame specified in the public base field.
Type index
Type index is an index field that references a previous TYPDEF record
in the object module. A value of 1 indicates the first TYPDEF record
in the module, a value of 2 indicates the second, and so on. The type
index value can be 0 if no data type is associated with the public
name.
The public name, public offset, and type index fields can be repeated
within a single PUBDEF record. Thus, one PUBDEF record can declare a
list of public names.
Location in object module
Any PUBDEF records in an object module must appear after the GRPDEF
and SEGDEF records to which they refer. Because PUBDEF records are not
themselves referenced by any other type of object record, they are
generally placed near the end of an object module.
Examples
The following two examples show PUBDEF records created by the
Microsoft Macro Assembler.
The first example is the record for the statement
PUBLIC GAMMA
The PUBDEF record is
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 90 0C 00 00 01 05 47 41 4D 4D 41 02 00 00 F9 ......GAMMA....
■ Byte 00H contains 90H, indicating a PUBDEF record.
■ Bytes 01-02H contain 000CH, the length of the remainder of the
record.
■ Bytes 03-04H represent the public base field. Byte 03H (the group
index) contains 0, indicating that no group is associated with the
name in this PUBDEF record. Byte 04H (the segment index) contains
1, a reference to the first SEGDEF record in the object module.
This is the segment to which the name in this PUBDEF record refers.
■ Bytes 05-0AH represent the public name field. Byte 05H contains 05H
(the length of the name), and bytes 06-0AH contain the name itself,
GAMMA.
■ Bytes 0B-0CH contain 0002H, the public offset. The name GAMMA thus
refers to the location that is offset 2 bytes from the beginning of
the segment referenced by the public base.
■ Byte 0DH is the type index. The value of the type index is 0,
indicating that no data type is associated with the name GAMMA.
■ Byte 0EH contains the checksum, 0F9H.
The next example is the PUBDEF record for the following absolute
symbol declaration:
PUBLIC ALPHA
ALPHA EQU 1234h
The PUBDEF record is
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 90 0E 00 00 00 00 00 05 41 4C 50 48 41 34 12 00 ......ALPHA4....
0010 B1 .
■ Bytes 03-06H (the public base field) contain a group index of 0
(byte 03H) and a segment index of 0 (byte 04H). Since both the
group index and segment index are 0, a frame number also appears in
the public base field. In this instance, the frame number (bytes
05-06H) also happens to be 0.
■ Bytes 07-0CH (the public name field) contain the name ALPHA,
preceded by its length.
■ Bytes 0D-0EH (the public offset field) contain 1234H. This is the
value associated with the symbol ALPHA in the assembler EQU
directive. If ALPHA is declared in another object module with the
declaration
EXTRN ALPHA:ABS
any references to ALPHA in that object module are fixed up as absolute
references to offset 1234H in frame 0. In other words, ALPHA would
have the value 1234H.
■ Byte 0FH (the type index) contains 0.
94H LINNUM Line Number Record
The LINNUM record relates line numbers in source code to addresses in
object code.
Record format
┌──────┬──────┬──────┬─────────///────────┬──────┬──────┬──────┬──────┬──────┐
│ │ │ │ line number │ line │ line number │ chk │
│ 94H │ length │ base │ number │ offset │ sum │
└──────┴──────┴──────┴─────────///────────┴──────┴──────┴──────┴──────┴──────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
can be
repeated
Line number base
The line number base describes the segment to which the line number
refers. Although the complete Intel specification allows the line
number base to refer to a group or to an absolute segment as well as
to a relocatable segment, Microsoft restricts references in this field
to relocatable segments. The format of the line number base field is
┌──────┬────────///─────────┐
│group │ │
│index │ segment index │
└──────┴────────///─────────┘
■ The group index field always contains a single zero byte.
■ The segment index is an index field that references a previous
SEGDEF record. A value of 1 indicates the first SEGDEF record in
the object module, a value of 2 indicates the second, and so on.
Line number
Line number is a 2-byte field containing a line number between 0 and
32,767 (0-7FFFH).
Line number offset
The line number offset is a 2-byte field that specifies the offset of
the executable code (in the segment specified in the line number base
field) to which the line number in the line number field refers.
The line number and line number offset fields can be repeated, so a
single LINNUM record can specify multiple line numbers in the same
segment.
Location in object module
Any LINNUM records in an object module must appear after the SEGDEF
records to which they refer. Because LINNUM records are not themselves
referenced by any other type of object record, they are generally
placed near the end of an object module.
Example
The following LINNUM record was generated by the Microsoft C Compiler:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 94 0F 00 00 01 02 00 00 00 03 00 08 00 04 00 0F ................
0010 00 3C ..
■ Byte 00H contains 94H, indicating that this is a LINNUM record.
■ Bytes 01-02H contain 000FH, the length of the remainder of the
record.
■ Bytes 03-04H represent the line number base field. Byte 03H (the
group index field) contains 00H, as it must. Byte 04H (the segment
index field) contains 01H, indicating that the line numbers in this
LINNUM record refer to code in the segment defined in the first
SEGDEF record in this object module.
■ Bytes 05-06H (a line number field) contain 0002H, and bytes 07-08H
(a line number offset field) contain 0000H. Together, they indicate
that source-code line number 0002 corresponds to offset 0000H in
the segment indicated in the line number base field.
Similarly, the two pairs of line number and line number offset
fields in bytes 09-10H specify that line number 0003 corresponds to
offset 0008H and that line number 0004 corresponds to offset 000FH.
■ Byte 11H contains the checksum, 3CH.
96H LNAMES List of Names Record
The LNAMES record is a list of names that can be referenced by
subsequent SEGDEF and GRPDEF records in the object module.
Record format
┌──────┬──────┬──────┬─────────///────────┬──────┐
│ │ │ │ │ chk │
│ 96H │ length │ name list │ sum │
└──────┴──────┴──────┴─────────///────────┴──────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
can be
repeated
Name list
Name list is a variable-length field that contains the list of names.
Each name is preceded by 1 byte that defines its length, which can be
a value between 0 and 255 (0-0FFH).
The names in the list are indexed implicitly in the order they appear:
The first name in the list has an index of 1, the second name has an
index of 2, and so forth. References to the names contained in name
list by subsequent object records, such as SEGDEF, are accomplished by
using this index number. LINK imposes a limit of 255 logical names per
object module.
Location in object module
Any LNAMES records in an object module must appear before the GRPDEF
or SEGDEF records that refer to them. Because it does not refer to any
other type of object records, an LNAMES record usually appears near
the start of an object module.
Example
The following LNAMES record contains the segment and class names
specified in all three of the assembler statements:
_TEXT SEGMENT byte public 'CODE'
_DATA SEGMENT word public 'DATA'
_STACK SEGMENT para public 'STACK'
The LNAMES record is
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 96 25 00 00 04 43 4F 44 45 04 44 41 54 41 05 53 .%...CODE.DATA.S
0010 54 41 43 4B 05 5F 44 41 54 41 06 5F 53 54 41 43 TACK._DATA._STAC
0020 4B 05 5F 54 45 58 54 8B K._TEXT.
■ Byte 00H contains 96H, indicating that this is an LNAMES record.
■ Bytes 01-02H contain 0025H, the length of the remainder of the
record.
■ Byte 03H contains 00H, a zero-length name.
■ Byte 04H contains 04H, the length of the class name CODE, which is
found in bytes 05-08H. Bytes 09-26H contain the class names DATA
and STACK and the segment names _DATA, _STACK, and _TEXT, each
preceded by 1 byte giving its length.
■ Byte 27H contains the checksum, 8BH.
98H SEGDEF Segment Definition Record
The SEGDEF record describes a logical segment in an object module. It
defines the segment's name, length, and alignment, and the way the
segment can be combined with other logical segments. LINK imposes a
limit of 255 SEGDEF records per object module.
Object records that follow a SEGDEF record can refer to it to identify
a particular segment.
Record format
┌──────┬──────┬──────┬─────────///────────┬──────┬──────┬─────────///────────┬─────────///────────┬─────────///────────┬──────┐
│ │ │ │ │ segment │ segment name │ class name │ overlay name │ chk │
│ 98H │ length │ segment attributes │ length │ index │ index │ index │ sum │
└──────┴──────┴──────┴─────────///────────┴──────┴──────┴────────///─────────┴────────///─────────┴─────────///────────┴──────┘
Segment attributes
Segment attributes is a variable-length field:
┌──────┬──────┬──────┬──────┐
│ ACBP │ frame │ │
│ byte │ number │offset│
└──────┴──────┴──────┴──────┘
The ACBP byte
The contents and size of the segment attributes field depend on the
first byte of the field, the ACBP byte:
bit 7 6 5 4 3 2 1 0
┌────────────────────┬────────────────────┬──────┬──────┐
│ │ │ │ │
│ A │ C │ B │ P │
└────────────────────┴────────────────────┴──────┴──────┘
The bit fields in the ACBP byte describe the following characteristics
of the segment:
A Alignment in the run-time memory map
C Combination with other segments
B Big (a segment of exactly 64 KB)
P Page-resident (not used in MS-DOS)
The A field. Bits 7-5 of the ACBP byte, the A field, describe
the logical segment's alignment:
A = 0 (000B) Absolute (located at a specified frame address)
A = 1 (001B) Relocatable, byte aligned
A = 2 (010B) Relocatable, word aligned
A = 3 (011B) Relocatable, paragraph aligned
A = 4 (100B) Relocatable, page aligned
The original Intel specification includes two additional segment-
alignment values not supported in MS-DOS.
The following examples of Microsoft assembler SEGMENT directives show
the resulting values for the A field in the corresponding SEGDEF
object record:
aseg SEGMENT at 400h ; A = 0
bseg SEGMENT byte public 'CODE' ; A = 1
cseg SEGMENT para stack 'STACK' ; A = 3
The C field. Bits 4-2 of the ACBP byte, the C field, describe how the
linker can combine the segment with other segments. Under MS-DOS,
segments with the same name and class can be combined in two ways.
They can be concatenated to form one logical segment, or they can be
overlapped. In the latter case, they have either the same starting
address or the same end address and they describe a common area of
memory.
The value in the C field corresponds to one of these two methods of
combining segments. Meaningful values, however, also depend on whether
the segment is absolute (A = 0) or relocatable (A = 1, 2, 3, or 4). If
A = 0, then C must also be 0, because absolute segments cannot be
combined. Values for the C field are
C = 0 (000B) Cannot be combined; used for segments whose combine
type is not explicitly specified (private segments).
C = 1 (001B) Not used by Microsoft.
C = 2 (010B) Can be concatenated with another segment of the same
name; used for segments with the public combine type.
C = 3 (011B) Undefined.
C = 4 (100B) As defined by Microsoft, same as C = 2.
C = 5 (101B) Can be concatenated with another segment with the same
name; used for segments with the stack combine type.
C = 6 (110B) Can be overlapped with another segment with the same
name; used for segments with the common combine type.
C = 7 (111B) As defined by Microsoft, same as C = 2.
The following examples of assembler SEGMENT directives show the
resulting values for the C field in the corresponding SEGDEF object
record:
aseg SEGMENT at 400H ; C = 0
bseg SEGMENT public 'DATA' ; C = 2
cseg SEGMENT stack 'STACK' ; C = 5
dseg SEGMENT common 'COMMON' ; C = 6
See PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING TOOLS: The
Microsoft Object Linker.
The B and P fields. Bit 1 of the ACBP byte, the B field, is set to 1
(and the segment length field is set to 0) only if the segment is
exactly 64 KB long.
Bit 0 of the ACBP byte, the P field, is unused in MS-DOS. Its value
should always be 0.
Frame number and offset
The frame number and offset fields of the segment attributes field are
present only if the segment is an absolute segment (A = 0 in the ACBP
byte). Taken together, the frame number and offset indicate the
starting address of the segment.
■ Frame number is a 2-byte field that contains the frame number of
the start of the segment.
■ Offset is a 1-byte field that contains an offset between 00H and
0FH within the specified frame. LINK ignores the offset field.
Segment length
Segment length is a 2-byte field that specifies the length of the
segment in bytes. The length can be from 00H to FFFFH. If a segment is
exactly 64 KB (10000H) in size, segment length should be 0 and the B
field in the ACBP byte should be 1.
Segment name index, class name index, and overlay name index
Each of the segment name index, class name index, and overlay name
index fields contains an index into the list of names defined in
previous LNAMES records in the object module. An index value of 1
indicates the first name in the LNAMES record, a value of 2 the
second, and so on.
■ The segment name index identifies the segment with a unique name.
The name may have been assigned by the programmer, or it may have
been generated by a compiler.
■ The class name index identifies the segment with a class name (such
as CODE, FAR_DATA, and STACK). The linker places segments with the
same class name into a contiguous area of memory in the run-time
memory map.
■ The overlay name index identifies the segment with a run-time
overlay. Starting with version 2.40, however, LINK ignores the
overlay name index. In versions 2.40 and later, command-line
parameters to LINK, rather than information contained in object
modules, determine the creation of run-time overlays.
Location in object module
SEGDEF records must follow the LNAMES record to which they refer. In
addition, SEGDEF records must precede any PUBDEF, LINNUM, GRPDEF,
FIXUPP, LEDATA, or LIDATA records that refer to them.
Examples
In this first example, the segment is byte aligned:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 98 07 00 28 11 00 07 02 01 1E ...(......
■ Byte 00H contains 98H, indicating that this is a SEGDEF record.
■ Bytes 01-02H contain 0007H, the length of the remainder of the
record.
■ Byte 03H contains 28H (00101000B), the ACBP byte. Bits 7-5 (the A
field) contain 1 (001B), indicating that this segment is
relocatable and byte aligned. Bits 4-2 (the C field) contain 2
(010B), which represents a public combine type. (When this object
module is linked, this segment will be concatenated with all other
segments with the same name.) Bit 1 (the B field) is 0, indicating
that this segment is smaller than 64 KB. Bit 0 (the P field) is
ignored and should be zero, as it is here.
■ Bytes 04-05H contain 0011H, the size of the segment in bytes.
■ Bytes 06-08H index the list of names defined in the module's LNAMES
record. Byte 06H (the segment name index) contains 07H, so the name
of this segment is the seventh name in the LNAMES record. Byte 07H
(the class name index) contains 02H, so the segment's class name is
the second name in the LNAMES record. Byte 08H (the overlay name
index) contains 1, a reference to the first name in the LNAMES
record. (This name is usually null, as MS-DOS ignores it anyway.)
■ Byte 09H contains the checksum, 1EH.
The second SEGDEF record declares a word-aligned segment. It differs
only slightly from the first.
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 98 07 00 48 0F 00 05 03 01 01 ...H......
■ Bits 7-5 (the A field) of byte 03H (the ACBP byte) contain 2
(010B), indicating that this segment is relocatable and word
aligned.
■ Bytes 04-05H contain the size of the segment, 000FH.
■ Byte 06H (the segment name index) contains 05H, which refers to the
fifth name in the previous LNAMES record.
■ Byte 07H (the class name index) contains 03H, a reference to the
third name in the LNAMES record.
9AH GRPDEF Group Definition Record
The GRPDEF record defines a group of segments, all of which lie within
the same 64 KB frame in the run-time memory map. LINK imposes a limit
of 21 GRPDEF records per object module.
Record format
┌──────┬──────┬──────┬────────///─────────┬─────────///────────┬──────┐
│ │ │ │ group name │ group component │ chk │
│ 9AH │ length │ index │ descriptor │ sum │
└──────┴──────┴──────┴────────///─────────┴─────────///────────┴──────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
can be
repeated
Group name index
Group name index is an index field whose value refers to a name in the
name list field of a previous LNAMES record.
Group component descriptor
The group component descriptor consists of two fields:
┌──────┬─────────///────────┐
│ │ │
│type │ segment index │
└──────┴─────────///────────┘
■ Type is a 1-byte field whose value is always 0FFH, indicating that
the following field contains a segment index value. The original
Intel specification defines four other types of group component
descriptor with the values 0FEH, 0FDH, 0FBH, and 0FAH. LINK ignores
these other type values, however, and assumes that the group
component descriptor contains a segment index value.
■ The segment index field contains an index number that refers to a
previous SEGDEF record. A value of 1 indicates the first SEGDEF
record in the object module, a value of 2 indicates the second, and
so on.
The group component descriptor field is usually repeated within the
GRPDEF record, so all segments constituting the group can be included
in one GRPDEF record.
Location in object module
GRPDEF records must follow the LNAMES and SEGDEF records to which they
refer. They must also precede any PUBDEF, LINNUM, FIXUPP, LEDATA, or
LIDATA records that refer to them.
Example
The following example of a GRPDEF record corresponds to the assembler
directive:
tgroup GROUP seg1,seg2,seg3
The GRPDEF record is
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9A 08 00 06 FF 01 FF 02 FF 03 55 ..........U
■ Byte 00H contains 9AH, indicating that this is a GRPDEF record.
■ Bytes 01-02H contain 0008H, the length of the remainder of the
record.
■ Byte 03H contains 06H, the group name index. In this instance, the
index number refers to the sixth name in the previous LNAMES record
in the object module. That name is the name of the group of
segments defined in the remainder of the record.
■ Bytes 04-05H contain the first of three group component descriptor
fields. Byte 04H contains the required 0FFH, indicating that the
subsequent field is a segment index. Byte 05H contains 01H, a
segment index that refers to the first SEGDEF record in the object
module. This SEGDEF record declared the first of three segments in
the group.
■ Bytes 06-07H represent the second group component descriptor, this
one referring to the second SEGDEF record in the object module.
■ Similarly, bytes 08-09H are a group component descriptor field that
references the third SEGDEF record.
■ Byte 0AH contains the checksum, 55H.
9CH FIXUPP Fixup Record
The FIXUPP record contains information that allows the linker to
resolve (fix up) addresses whose values cannot be determined by the
language translator. FIXUPP records describe the LOCATION of each
address value to be fixed up, the TARGET address to which the fixup
refers, and the FRAME relative to which the address computation is
performed.
Record format
┌──────┬──────┬──────┬────────///─────────┬─────────///────────┬──────┐
│ │ │ │ │ │ chk │
│ 9CH │ length │ thread │ fixup │ sum │
└──────┴──────┴──────┴────────///─────────┴─────────///────────┴──────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
can be can be
repeated repeated
Thread and fixup fields
A FIXUPP record can contain zero or more thread fields and zero or
more fixup fields. Each fixup field describes the method to be used by
the linker to compute the TARGET address to be placed at a particular
location in the executable image, relative to a particular FRAME. The
information that determines the LOCATION, TARGET, and FRAME can be
specified explicitly in the fixup field. It can also be specified
within the fixup field by a reference to a previous thread field.
A thread field describes only the method to be used by the linker to
refer to a particular TARGET or FRAME. Because the same thread field
can be referenced in several subsequent fixup fields, a FIXUPP record
that uses thread fields may be smaller than one in which thread fields
are not used.
Thread and fixup fields are distinguished from one another by the
high-order bit of the first byte in the field. If the high-order bit
is 0, the field is a thread field. If the high-order bit is 1, the
field is a fixup field.
The thread field
A thread field contains information that can be referenced in
subsequent thread or fixup fields in the same or subsequent FIXUPP
records. It has the following format:
┌──────┬─────────///────────┐
│thread│ │
│ data │ index │
└──────┴─────────///────────┘
The thread data field is a single byte comprising five subfields:
bit 7 6 5 4 3 2 1 0
┌──────┬──────┬──────┬────────────────────┬─────────────┐
│ │ │ │ │ thread │
│ 0 │ D │ 0 │ method │ number │
└──────┴──────┴──────┴────────────────────┴─────────────┘
■ Bit 7 of the thread data byte is 0, indicating the start of a
thread field.
■ The D field (bit 6) indicates whether the thread field specifies a
FRAME or a TARGET. The D bit is set to 1 to indicate a FRAME or to
0 to indicate a TARGET.
■ Bit 5 of the thread data byte is not used. It should always be set
to 0.
■ Bits 4 through 2 represent the method field. If D = 1, the method
field contains 0, 1, 2, 4, or 5. Each of these numbers corresponds
to one method of specifying a FRAME (see Table 19-2). If D = 0, the
method field contains 0, 1, 2, 4, 5, or 6, each of which
corresponds to one of the methods of specifying a TARGET (see Table
19-3).
In the case of a TARGET address, only bits 3 and 2 of the method
field are used. When D = 0, the high-order bit of the value in the
method field is derived from the P bit in the fix dat field of any
subsequent fixup field that refers to this thread field. Thus, if D
= 0, bit 4 of the method field is also 0, and the only meaningful
values for the method field are 0, 1, and 2.
■ The thread number field (bits 1 and 0) contains a number between 0
and 3. This number is used in subsequent fixup or thread fields to
refer to this particular thread field.
The thread number is implicitly associated with the D field by the
linker, so as many as eight different thread fields (four FRAMEs
and four TARGETs) can be referenced at any time. A thread number
can be reused in an object module and, if it is, always refers to
the thread field in which it last appeared.
Table 19-2. FRAME Fixup Methods.
╓┌───────────┌───────────────────────────────────────────────────────────────╖
Method Description
──────────────────────────────────────────────────────────────────
0 The FRAME is specified by a segment index.
1 The FRAME is specified by a group index.
2 The FRAME is indicated by an external index. LINK determines
the FRAME from the external name's corresponding PUBDEF record
in another object module, which specifies either a logical
segment or a group.
3 The FRAME is identified by an explicit frame number. (Not
supported by LINK.)
4 The FRAME is determined by the segment in which the LOCATION is
defined. In this case, the largest possible frame number is
used.
5 The FRAME is determined by the TARGET's segment, group, or
external index.
Table 19-3. TARGET Fixup Methods.
╓┌─────────────┌─────────────────────────────────────────────────────────────╖
Method Description
──────────────────────────────────────────────────────────────────
0 The TARGET is specified by a segment index and a
displacement. The displacement is given in the target
displacement field of the FIXUPP record.
1 The TARGET is specified by a group index and a target
displacement.
2 The TARGET is specified by an external index and a target
displacement. LINK adds the displacement to the address it
determines from the external name's corresponding PUBDEF
record in another object module.
3 The TARGET is identified by an explicit frame number. (Not
supported by LINK.)
4 The TARGET is specified by a segment index only.
5 The TARGET is specified by a group index only.
6 The TARGET is specified by an external index. The TARGET is
the address associated with the external name.
7 The TARGET is identified by an explicit frame number. (Not
supported by LINK.)
The index field either contains an index value that refers to a
previous SEGDEF, GRPDEF, or EXTDEF record, or it contains an explicit
frame number. The interpretation of the index value depends on the
value of the method field of the thread data field:
method = 0 Segment index (reference to a previous SEGDEF record)
method = 1 Group index (reference to a previous GRPDEF record)
method = 2 External index (reference to a previous EXTDEF record)
method = 3 Frame number (not supported by LINK; ignored)
The fixup field
The fixup field provides the information needed by the linker to
resolve a reference to a relocatable or external address. The fixup
field has the following format:
┌──────┬──────┬──────┬────────///─────────┬─────────///────────┬──────┬──────┐
│ │ │ fix │ │ │ target │
│ locat │ dat │ frame datum │ target datum │displacement │
└──────┴──────┴──────┴────────///─────────┴─────────///────────┴──────┴──────┘
The 2-byte locat field has an unusual format. Contrary to
the usual byte order in Intel data structures, the most significant
bits of the locat field are found in the low-order, rather
than the high-order, byte:
low-order byte │
bit 15 14 13 12 11 10 9 8 │ 7 6
┌──────┬──────┬──────┬────────────────────┬─────────────┼───────────────────────────────────────────────────────┐
│ │ │ │ │ │
│ 1 │ M │ S │ loc │ data record offset │
└──────┴──────┴──────┴────────────────────┴─────────────┴───────────────────────────────────────────────────────┘
■ Bit 15 (the high-order bit of the locat field) contains 1,
indicating that this is a fixup field.
■ Bit 14 (the M bit) is 1 if the fixup is segment relative and 0 if
the fixup is self-relative.
■ Bit 13 (the S bit) is currently unused and should always be set to
0.
■ Bits 12 through 10 represent the loc field. This field contains a
number between 0 and 5 that indicates the type of LOCATION to be
fixed up:
loc = 0 Low-order byte
loc = 1 Offset
loc = 2 Segment
loc = 3 Pointer (segment:offset)
loc = 4 High-order byte (not recognized by LINK)
loc = 5 Loader-resolved offset (treated as loc = 1 by the linker)
■ Bits 9 through 0 (the data record offset) indicate the position of
the LOCATION to be fixed up in the LEDATA or LIDATA record
immediately preceding the FIXUPP record. This offset indicates
either a byte in the data field of an LEDATA record or a data byte
in the content field of an iterated data block in an LIDATA record.
The fix dat field is a single byte comprising five fields:
bit 7 6 5 4 3 2 1 0
┌──────┬────────────────────┬──────┬──────┬─────────────┐
│ │ │ │ │ │
│ F │ frame │ T │ P │ targt │
└──────┴────────────────────┴──────┴──────┴─────────────┘
■ Bit 7 (the F bit) is set to 1 if the FRAME for this fixup is
specified by a reference to a previous thread field. The F bit is 0
if the FRAME method is explicitly defined in this fixup field.
■ The interpretation of the frame field in bits 6 through 4 depends
on the value of the F bit. If F = 1, the frame field contains a
number between 0 and 3 that indicates the thread field containing
the FRAME method. If F = 0, the frame field contains 0, 1, 2, 4, or
5, corresponding to one of the methods of specifying a FRAME listed
in Table 19-2.
■ Bit 3 (the T bit) is set to 1 if the TARGET for the fixup is
specified by a reference to a previous thread field. If the T bit
is 0, the TARGET is explicitly defined in this fixup field.
■ Bit 2 (the P bit) and bits 1 and 0 (the targt field) can be
considered a 3-bit field analogous to the frame field.
■ If the T bit indicates that the TARGET is specified by a previous
thread reference (T = 1), the targt field contains a number between
0 and 3 that refers to a previous thread field containing the
TARGET method. In this case, the P bit, combined with the 2 low-
order bits of the method field in the thread field, determines the
TARGET method. If the T bit is 0, indicating that the target is
explicitly defined, the P and targt fields together contain 0, 1,
2, 4, 5, or 6. This number corresponds to one of the TARGET fixup
methods listed in Table 19-3. (In this case, the P bit can be
regarded as the high-order bit of the method number.)
Frame datum is an index field that refers to a previous SEGDEF,
GRPDEF, or EXTDEF record, depending on the FRAME method.
Similarly, the target datum field contains a segment index, a group
index, or an external index, depending on the TARGET method.
The target displacement field, a 2-byte field, is present only if the
P bit in the fixdat field is set to 0, in which case the target
displacement field contains the 16-bit offset used in methods 0, 1,
and 2 of specifying a TARGET.
Location in object module
FIXUPP records must appear after the SEGDEF, GRPDEF, or EXTDEF records
to which they refer. In addition, if a FIXUPP record contains any
fixup fields, it must immediately follow the LEDATA or LIDATA record
to which the fixups refer.
Examples
Although crucial to the proper linking of object modules, FIXUPP
records are terse: Almost every bit is meaningful. For these reasons,
the following three examples of FIXUPP records are particularly
detailed.
A good way to understand how a FIXUPP record is put together is to
compare it to the corresponding source code. The Microsoft Macro
Assembler is helpful in this regard, because it marks in its source
listing address references it cannot resolve. The "program" in Figure
19-6 is designed to show how some of the most frequently encountered
fixups are encoded in FIXUPP records.
──────────────────────────────────────────────────────────────────────
Figure 19-6. A sample "program" showing how some common fixups are
encoded in FIXUPP records.
──────────────────────────────────────────────────────────────────────
The assembler generates one LEDATA record for this program:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0010 A0 1A 00 01 00 00 E9 00 00 EB 00 EA 00 00 00 00 ................
0020 EA 00 00 00 00 BB 00 00 B8 00 00 C3 67 ............g
Bytes 06-2BH (the data field) of this LEDATA record contain 8086
opcodes for each of the instruction mnemonics in the source code. The
gaps (zero values) in the data field correspond to address values that
the assembler cannot resolve. The linker will fix up the address
values in the gaps by computing the correct values and adding them to
the zero values in the gaps. The FIXUPP record that tells the linker
how to do this immediately follows the LEDATA record in the object
module:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 21 00 84 01 06 01 02 80 04 06 01 02 CC 06 04 .!..............
0010 02 02 CC 0B 06 01 01 C4 10 00 01 01 15 00 C8 13 ................
0020 04 01 01 A3 ....
■ Byte 00H contains 9CH, indicating this is a FIXUPP record.
■ Bytes 01-02H contain 0021H, the length of the remainder of the
record.
■ Bytes 03-07H represent the first of the six fixup fields in this
record:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 21 00 84 01 06 01 02 80 04 06 01 02 CC 06 04 .!..............
0010 02 02 CC 0B 06 01 01 C4 10 00 01 01 15 00 C8 13 ................
0020 04 01 01 A3 ....
The information in this fixup field will allow the linkery to resolve
the address reference in the statement
jmp NearLabel
- Bytes 03-04H (the locat field) contain 8401H
(1000010000000001B). (Recall that this field does not conform to
the usual Intel byte order.) Bit 15 is 1, signifying that this is
a fixup field, not a thread field. Bit 14 (the M bit) is 0, so
this fixup is self-relative. Bit 13 is unused and should be set
to 0, as it is here.Bits 12-10 (the loc field) contain 1 (001B),
so the LOCATION to be fixed up is a 16-bit offset. Bits 9-0 (the
data record offset) contain 1 (0000000001B), which informs the
linker that the LOCATION to be fixed up is at offset 1 in the
data field of the LEDATA record immediately preceding this FIXUPP
record--in other words, the 2 bytes immediately following the
first opcode 0E9H.
- Byte 05H (the fix dat field) contains 06H (00000110B). Bit 7
(the F bit) is 0, meaning the FRAME for this fixup is explicitly
specified in this fixup field. Bits 6-4 (the frame field) contain
0 (000B), indicating that FRAME method 0 specifies the FRAME. Bit
3 (the T bit) is 0, so the TARGET for this fixup is also
explicitly specified. Bits 2-0 (the P bit) and the targt field
contain 6 (110B), so TARGET method 6 specifies the TARGET.
- Byte 06H is a frame datum field, because the FRAME is explicitly
specified (the F bit of the fix dat field = 0). And, because
method 0 is specified, the frame datum is an index field that
refers to a previous SEGDEF record. In this example, the frame
datum field contains 1, which indicates the first SEGDEF record
in the object module: the _TEXT segment.
- Similarly, byte 07H is a target datum, because the TARGET is also
explicitly specified (the T bit of the fix dat field = 0). The
fix dat field also indicates that TARGET method 6 is used, so the
target datum is an index field that refers to the external
reference list in a previous EXTDEF record. The value of this
index is 2, so the TARGET is the second external reference
declared in the EXTDEF record: NearLabel in this object module.
■ Bytes 08-0CH represent the second fixup field:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 21 00 84 01 06 01 02 80 04 06 01 02 CC 06 04 .!..............
0010 02 02 CC 0B 06 01 01 C4 10 00 01 01 15 00 C8 13 ................
0020 04 01 01 A3 ....
This fixup field corresponds to the statement
jmp short NearLabel
The only difference between this statement and the first is that the
jump uses an 8-bit, rather than a 16-bit, offset. Thus, the loc field
(bits 12-10 of byte 08H) contains 0 (000B) to indicate that the
LOCATION to be fixed up is a low-order byte.
■ Bytes 0D-11H represent the third fixup field in this FIXUPP record:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 21 00 84 01 06 01 02 80 04 06 01 02 CC 06 04 .!..............
0010 02 02 CC 0B 06 01 01 C4 10 00 01 01 15 00 C8 13 ................
0020 04 01 01 A3 ....
This fixup field corresponds to the statement
jmp far ptr FarProc
In this case, both the TARGET's frame (the segment FAR_TEXT) and
offset (the label FarProc) are known to the assembler. Both the
segment address and the label offset are relocatable, however, so
in the FIXUPP record the assembler passes the responsibility for
resolving the addresses to the linker.
- Bytes 0D-0EH (the locat field) indicate that the field is a fixup
field (bit 15 = 1) and that the fixup is segment relative (bit
14--the M bit = 1). The loc field (bits 12-10) contains 3 (011B),
so the LOCATION being fixed up is a 32-bit (FAR) pointer (segment
and offset). The data record offset (bits 9-0) is 6
(0000000110B); the LOCATION is the 4 bytes following the first
far jump opcode (EAH) in the preceding LEDATA record.
- In byte 0FH (the fix dat field), the F bit and the frame field
are 0, indicating that method 0 (a segment index) is used to
specify the FRAME. The T bit is 0 (meaning the target is
explicitly defined in the fixup field); therefore, the P bit and
targt fields together indicate method 4 (a segment index) to
specify the TARGET.
- Because the FRAME is specified with a segment index, byte 10H
(the frame datum field) is a reference to the second SEGDEF
record in the object module, which in this example declared the
FAR_TEXT segment. Similarly, byte 11H (the target datum field)
references the FAR_TEXT segment. In this case, the FRAME is the
same as the TARGET segment; had FAR_TEXT been one of a group of
segments, the FRAME could have referred to the group instead.
■ The fourth assembler statement is different from the third because
it references a segment not known to the assembler:
jmp FarLabel
Bytes 12-16H contain the corresponding fixup field:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 21 00 84 01 06 01 02 80 04 06 01 02 CC 06 04 .!..............
0010 02 02 CC 0B 06 01 01 C4 10 00 01 01 15 00 C8 13 ................
0020 04 01 01 A3 ....
The significant difference between this and the preceding fixup
field is that the P bit and targt field of the fix dat byte (byte
14H) specify TARGET method 6. In this fixup field, the target datum
(byte 16H) refers to the first EXTDEF record in the object module,
which declares FarLabel as an external reference.
■ The fifth fixup field (bytes 17-1DH) is
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 21 00 84 01 06 01 02 80 04 06 01 02 CC 06 04 .!..............
0010 02 02 CC 0B 06 01 01 C4 10 00 01 01 15 00 C8 13 ................
0020 04 01 01 A3 ....
This fixup field contains information that enables the linker to
calculate the value of the relocatable offset LocalLabel:
mov bx,offset LocalLabel
- Bytes 17-18H (the locat field) contain C410H (1100010000010000B).
Bit 15 is 1, denoting a fixup field. The M bit (bit 14) is 1,
indicating that this fixup is segment relative. The loc field
(bits 12-10) contains 1 (001B), so the LOCATION is a 16-bit
offset. The data record offset (bits 9-0) is 10H (0000010000B), a
reference to the 2 bytes in the LEDATA record following the
opcode 0BBH.
- Byte 19H (the fix dat byte) contains 00H. The F bit, frame field,
T bit, P bit, and targt field are all 0, so FRAME method 0 and
TARGET method 0 are explicitly specified in this fixup field.
- Because FRAME method 0 is used, byte 1AH (the frame datum field)
is an index field. It contains 01H, a reference to the first
SEGDEF record in the object module, which declares the segment
_TEXT.
Similarly, byte 1BH (the target datum field) references the _TEXT
segment.
- Because TARGET method 0 is specified, an offset, in addition to a
segment, is required to define the TARGET. This offset appears in
the target displacement field in bytes 1C-1DH. The value of this
offset is 0015H, corresponding to the offset of the TARGET
(LocalLabel) in its segment (_TEXT).
■ The sixth and final fixup field in this FIXUPP record (bytes 1E
-22H) is
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 21 00 84 01 06 01 02 80 04 06 01 02 CC 06 04 .!..............
0010 02 02 CC 0B 06 01 01 C4 10 00 01 01 15 00 C8 13 ................
0020 04 01 01 A3 ....
This corresponds to the segment of the relocatable address
LocalLabel:
mov ax,seg LocalLabel
- Bytes 1E-1FH (the locat field) contain C813H (1100100000010011B).
Bit 15 is 1, so this is a fixup field. The M bit (bit 14) is 1,
so the fixup is segment relative. The loc field (bits 12-10)
contains 2 (010B), so the LOCATION is a 16-bit segment value. The
data record offset (bits 9-0) indicates the 2 bytes in the LEDATA
record following the opcode 0B8H.
- Byte 20H (the fix dat byte) contains 04H, so FRAME method 0 and
TARGET method 4 are explicitly specified in this fixup field.
- Byte 21H (the frame datum field) contains 01H. Because FRAME
method 0 is specified, the frame datum is an index value that
refers to the first SEGDEF record in the object module
(corresponding to the _TEXT segment).
- Byte 22H (the target datum field) contains 01H. Because TARGET
method 4 is specified, the target datum also references the _TEXT
segment.
■ Finally, byte 23H contains this FIXUPP record's checksum, 0A3H.
The next two FIXUPP records show how thread fields are used. The first
of the two contains six thread fields that can be referenced by both
thread and fixup fields in subsequent FIXUPP records in the same
object module:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 0D 00 00 03 01 02 02 01 03 04 40 01 45 01 C0 ...........@....
Bytes 03-04H, 05-06H, 07-08H, 09-0AH, 0B-0CH, and 0D-0EH represent the
six thread fields in this FIXUPP record. The high-order bit of the
first byte of each of these fields is 0, indicating that they are,
indeed, thread fields and not fixup fields.
■ Byte 03H, which contains 00H, is the thread data byte of the first
thread field. Bit 7 of this byte is 0, indicating this is a thread
field. Bit 6 (the D bit) is 0, so this field specifies a TARGET.
Bit 5 is 0, as it must always be. Bits 4 through 2 (the method
field) contain 0 (000B), which specifies TARGET method 0. Finally,
bits 1 and 0 contain 0 (00B), the thread number that identifies
this thread field.
Byte 04H represents a segment index field, because method 0 of
specifying a TARGET references a segment. The value of the index,
3, is a reference to the third SEGDEF record defined in the object
module.
■ Bytes 05-06H, 07-08H, and 09-0AH contain similar thread fields. In
each, the method field specifies TARGET method 0. The three thread
fields also have thread numbers of 1, 2, and 3. Because TARGET
method 0 is specified for each thread field, bytes 06H, 08H, and
0AH represent segment index fields, which reference the second,
first, and fourth SEGDEF records, respectively.
■ Byte 0BH (the thread data byte of the fifth thread field in this
FIXUPP record) contains 40H (01000000B). The D bit (bit 6) is 1, so
this thread field specifies a FRAME. The method field (bits 4
through 2) contains 0 (000B), which specifies FRAME method 0. Byte
0CH (which contains 01H) is therefore interpreted as a segment
index reference to the first SEGDEF record in the object module.
■ Byte 0DH is the thread data byte of the sixth thread field. It
contains 45H (01000101B). Bit 6 is 1, which indicates that this
thread specifies a FRAME. The method field (bits 4 through 2)
contains 1 (001B), which specifies FRAME method 1. Byte 0EH (which
contains 01H) is therefore interpreted as a group index to the
first preceding GRPDEF record.
The thread number fields of the fifth and sixth thread fields
contain 0 and 1, respectively, but these thread numbers do not
conflict with the ones used in the first and second thread fields,
because the latter represent TARGET references, not FRAME
references.
The next FIXUPP example appears after the preceding record, in the
same object module. This FIXUPP record contains a fixup field in bytes
03-05H that refers to a thread in the previous FIXUPP record:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 04 00 C4 09 9D F6 .......
■ Bytes 03-04H represent the 16-bit locat field, which contains C409H
(1100010000001001B). Bit 15 of the locat field is 1, indicating a
fixup field. The M bit (bit 14) is 1, so this fixup is relative to
a particular segment, which is specified later in the fixup field.
Bit 13 is 0, as it should be. Bits 12-10 (the loc field) contain 1
(001B), so the LOCATION to be fixed up is a 16-bit offset. Bits 9-0
(the data record offset field) contain 9 (0000001001B), so the
LOCATION to be fixed up is represented at an offset of 9 bytes into
the data field of the preceding LEDATA or LIDATA record.
■ Byte 05H (the fix dat byte) contains 9DH (10011101B). The F bit
(bit 7) is 1, so this fixup field references a thread field that,
in turn, defines the method of specifying the FRAME for the fixup.
Bits 6-4 (the frame field) contain 1 (001B), the number of the
thread that contains the FRAME method. This thread contains a
method number of 1, which references the first GRPDEF record in the
object module, thus specifying the FRAME.
The T bit (bit 3 in the fix dat byte) is 1, so the TARGET method is
also defined in a preceding thread field. The targt field (bits 1
and 0 in the fix dat byte) contains 1 (01B), so the TARGET thread
field whose thread number is 1 specifies the TARGET. The P bit (bit
3 in the fix dat byte) contains 1, which is combined with the low
-order bits of the method field in the thread field that describes
the target to obtain TARGET method number 4 (100B). The TARGET
thread references the second SEGDEF record to specify the TARGET.
The last FIXUPP example illustrates that the linker performs a fixup
by adding the calculated address value to the value in the LOCATION
being fixed up. This function of the linker can be exploited to use
fixups to modify opcodes or program data, as well as to resolve
address references.
Consider how the following assembler instruction might be fixed up:
lea bx,alpha+10h ; alpha is an external symbol
Typically, this instruction is translated into an LEDATA record with
zero in the LOCATION (bytes 08-09H) to be fixed up:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 A0 08 00 01 00 00 8D 1E 00 00 AC ...........
The corresponding FIXUPP record contains a target displacement of 10H
bytes (bytes 08-09H):
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 08 00 C4 02 02 01 01 10 00 82 ...........
This FIXUPP record specifies TARGET method 2, which is indicated by
the targt field (bits 2-0) of the fixdat field (byte 05H). In this
case, the linker adds the target displacement to the address it has
determined for the TARGET (alpha) and then completes the fixup by
adding this calculated address value to the zero value in the
LOCATION.
The same result can be achieved by storing the displacement (10H)
directly in the LOCATION in the LEDATA record:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 A0 08 00 01 00 00 8D 1E 10 00 9C ...........
Then, the target displacement can be omitted from the FIXUPP record:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 9C 06 00 C4 02 06 01 01 90 .........
This FIXUPP record specifies TARGET method 6, which does not use a
target displacement. The linker performs this fixup by adding the
address of alpha to the value in the LOCATION, so the result is
identical to the preceding one.
The difference between the two techniques is that in the latter the
linker does not perform error checking when it adds the calculated
fixup value to the value in the LOCATION. If this second technique is
used, the linker will not flag arithmetic overflow or underflow errors
when it adds the displacement to the TARGET address. The first
technique, then, traps all errors; the second can be used when
overflow or underflow is irrelevant and an error message would be
undesirable.
0A0H LEDATA Logical Enumerated Data Record
The LEDATA record contains contiguous binary data--executable code or
program data--that is eventually copied into the program's executable
binary image.
The binary data in an LEDATA record can be modified by the linker if
the record is followed by a FIXUPP record.
Record format
┌──────┬──────┬──────┬─────────///────────┬──────┬──────┬──────────///────────┬──────┐
│ │ │ │ │ enumerated │ │ chk │
│ A0H │ length │ segment index │ data offset │ data │ sum │
└──────┴──────┴──────┴─────────///────────┴──────┴──────┴──────────///────────┴──────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
can be
repeated
Segment index
The segment index is a variable-length index field. The index number
in this field refers to a previous SEGDEF record in the object module.
A value of 1 indicates the first SEGDEF record, a value of 2 the
second, and so on. That SEGDEF record, in turn, indicates the segment
into which the data in this LEDATA record is to be placed.
Enumerated data offset
The enumerated data offset is a 2-byte offset into the segment
referenced by the segment index, relative to the base of the segment.
Taken together, the segment index and the enumerated data offset
fields indicate the location where the enumerated data will be placed
in the run-time memory map.
Data
The data field contains the actual data, which can be either
executable 8086 instructions or program data. The maximum size of the
data field is 1024 bytes.
Location in object module
Any LEDATA records in an object module must be preceded by the SEGDEF
records to which they refer. Also, if an LEDATA record requires a
fixup, a FIXUPP record must immediately follow the LEDATA record.
Example
The following LEDATA record contains a simple text string:
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 A0 13 00 02 00 00 48 65 6C 6C 6F 2C 20 77 6F 72 ......Hello, wor
0010 6C 64 0D 0A 24 A8 ld..$.
■ Byte 00H contains 0A0H, which identifies this as an LEDATA record.
■ Bytes 01-02H contain 0013H, the length of the remainder of the
record.
■ Byte 03H (the segment index field) contains 02H, a reference to the
second SEGDEF record in the object module.
■ Bytes 04-05H (the enumerated data offset field) contain 0000H. This
is the offset, from the base of the segment indicated by the
segment index field, at which the data in the data field will be
placed when the program is linked. Of course, this offset is
subject to relocation by the linker because the segment declared in
the specified SEGDEF record may be relocatable and may be combined
with other segments declared in other object modules.
■ Bytes 06-14H (the data field) contain the actual data.
■ Byte 15H contains the checksum, 0A8H.
0A2H LIDATA Logical Iterated Data Record
Like the LEDATA record, the LIDATA record contains binary data--
executable code or program data. The data in an LIDATA record,
however, is specified as a repeating pattern (iterated), rather than
by explicit enumeration.
The data in an LIDATA record may be modified by the linker if the
LIDATA record is followed by a FIXUPP record.
Record format
┌──────┬──────┬──────┬─────────///────────┬──────┬──────┬─────────///────────┬──────┐
│ │ │ │ │ iterated │ │ chk │
│ A2H │ length │ segment index │ data offset │iterated data block │ sum │
└──────┴──────┴──────┴─────────///────────┴──────┴──────┴─────────///────────┴──────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
can be
repeated
Segment index
The segment index is a variable-length index field. The index number
in this field refers to a previous SEGDEF record in the object module.
A value of 1 indicates the first SEGDEF record, 2 indicates the
second, and so on. That SEGDEF record, in turn, indicates the segment
into which the data in this LIDATA record is to be placed when the
program is executed.
Iterated data offset
The iterated data offset is a 2-byte offset into the segment
referenced by the segment index, relative to the base of the segment.
Taken together, the segment index and the iterated data offset fields
indicate the location where the iterated data will be placed in the
run-time memory map.
Iterated data block
The iterated data block is a variable-length field containing the
actual data--executable code and program data. Iterated data blocks
can be nested, so one iterated data block can contain one or more
other iterated data blocks. Microsoft LINK restricts the maximum size
of an iterated data block to 512 bytes.
The format of the iterated data block is
┌──────┬──────┬──────┬──────┬────────///─────────┐
│ repeat │ block │ │
│ count │ count │ content │
└──────┴──────┴──────┴──────┴────────///─────────┘
■ Repeat count is a 2-byte field indicating the number of times the
content field is to be repeated.
■ Block count is a 2-byte field indicating the number of iterated
data blocks in the content field. If the block count is 0, the
content field contains data only.
■ Content is a variable-length field that can contain either nested
iterated data blocks (if the block count is nonzero) or data (if
the block count is 0). If the content field contains data, the
field contains a 1-byte count of the number of data bytes in the
field, followed by the actual data.
Location in object module
Any LIDATA records in an object module must be preceded by the SEGDEF
records to which they refer. Also, if an LIDATA record requires a
fixup, a FIXUPP record must immediately follow the LIDATA record.
Example
This sample LIDATA record corresponds to the following assembler
statement, which declares a 10-element array containing the strings
ALPHA and BETA:
db 10 dup('ALPHA','BETA')
The LIDATA record is
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 A2 1B 00 01 00 00 0A 00 02 00 01 00 00 00 05 41 ...............A
0010 4C 50 48 41 01 00 00 00 04 42 45 54 41 A9 LPHA.....BETA.
■ Byte 00H contains 0A2H, identifying this as an LIDATA record.
■ Bytes 01-02H contain 1BH, the length of the remainder of the
record.
■ Byte 03H (the segment index) contains 01H, a reference to the first
SEGDEF record in this object module, indicating that the data
declared in this LIDATA record is to be placed into the segment
described by the first SEGDEF record.
■ Bytes 04-05H (the iterated data offset) contain 0000H, so the data
in this LIDATA record is to be located at offset 0000H in the
segment designated by the segment.
■ Bytes 06-1CH represent an iterated data block:
- Bytes 06-07H contain the repeat count, 000AH, which indicates
that the content field of this iterated data block is to be
repeated 10 times.
- Bytes 08-09H (the block count for this iterated data block)
contain 0002H, which indicates that the content field of this
iterated data block (bytes 0A-1CH) contains two nested iterated
data block fields (bytes 0A-13H and bytes 14-1CH).
- Bytes 0A-0BH contain 0001H, the repeat count for the first nested
iterated data block. Bytes 0C-0DH contain 0000H, indicating that
the content field of this nested iterated data block contains
data, rather than more nested iterated data blocks. The content
field (bytes 0E-13H) contains the data: Byte 0EH contains 05H,
the number of subsequent data bytes, and bytes 0F-13H contain the
actual data (the string ALPHA).
- Bytes 14-1CH represent the second nested iterated data block,
which has a format similar to that of the block in bytes 0A-13H.
This second nested iterated data block represents the 4-byte
string BETA.
■ Byte 1DH is the checksum, 0A9H.
0B0H COMDEF Communal Names Definition Record
The COMDEF record is a Microsoft extension to the basic set of 8086
object record types defined by Intel that declares a list of one or
more communal variables. The COMDEF record is recognized by versions
3.50 and later of LINK. Microsoft encourages the use of the COMDEF
record for declaration of communal variables.
Record format
┌──────┬──────┬──────┬─────────///────────┬──────┬──────┬─────────///────────┬──────┐
│ │ │ │ communal │type │ │ communal │ chk │
│ B0H │ length │ name │index │ │ length │ sum │
└──────┴──────┴──────┴─────────///────────┴──────┴──┼───┴─────────///────────┴──────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
└───── data
can be segment
repeated type
Communal name
The communal name field is a variable-length field that contains the
name of a communal variable. The first byte of this field indicates
the length of the name contained in the remainder of the field.
Type index
The type index field is an index field that references a previous
TYPDEF record in the object module. A value of 1 indicates the first
TYPDEF record in the module, a value of 2 indicates the second, and so
on. The type index value can be 0 if no data type is associated with
the public name.
Data segment type
The data segment type field is a single byte that indicates whether
the communal variable is FAR or NEAR. There are only two possible
values for data segment type:
61H FAR variable
62H NEAR variable
Communal length
The communal length is a variable-length field that indicates the
amount of memory to be allocated for the communal variable. The
contents of this field depend on the value in the data segment type
field. If the data segment type is NEAR (62H), the communal length
field contains the size (in bytes) of the communal variable:
┌─────────///────────┐
│ │
│ variable size │
└────────///─────────┘
If the data segment type is FAR (61H), the communal length field is
formatted as follows:
┌─────────///────────┬─────────///────────┐
│ number of │ │
│ elements │ element size │
└────────///─────────┴────────///─────────┘
A FAR communal variable is viewed as an array of elements of a
specified size. Thus, the number of elements field is a variable
-length field representing the number of elements in the array, and
the element size field is a variable-length field that indicates the
size (in bytes) of each element. The amount of memory required for a
FAR communal variable is thus the product of the number of elements
and the element size.
The format of the variable size, number of elements, and element size
fields depends upon the magnitude of the values they contain:
■ If the value is less than 128 (80H), the field is formatted as a
1-byte field containing the actual value:
┌──────┐
│ │
│value │
└──────┘
■ If the value is 128 (80H) or greater, the field is formatted with
an extra initial byte that indicates whether the value is
represented in the subsequent 2, 3, or 4 bytes:
┌──────┬──────┬──────┐
│ │ │
│ 81H │2-byte value │
└──────┴──────┴──────┘
┌──────┬──────┬──────┬──────┐
│ │ │
│ 84H │ 3-byte value │
└──────┴──────┴──────┴──────┘
┌──────┬──────┬──────┬──────┬──────┐
│ │ │
│ 88H │ 4-byte value │
└──────┴──────┴──────┴──────┴──────┘
Groups of communal name, type index, data segment type, and communal
length fields can be repeated so that more than one communal variable
can be declared in the same COMDEF record.
Location in object module
Any object module that contains COMDEF records must also contain one
COMENT record with the comment class 0A1H, indicating that Microsoft
extensions to the Intel object record specification are included in
the object module. This COMENT record must appear before any COMDEF
records in the object module.
Example
The following COMDEF record was generated by the Microsoft C Compiler
version 4.0 for these public variable declarations:
int foo; /* 2-byte integer */
char foo2[32768]; /* 32768-byte array */
char far foo3[10][2][20]; /* 400-byte array */
The COMDEF record is
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 B0 20 00 04 5F 66 6F 6F 00 62 02 05 5F 66 6F 6F . .._foo.b.._foo
0010 32 00 62 81 00 80 05 5F 66 6F 6F 33 00 61 81 90 2.b...._foo3.a..
0020 01 01 99 ...
■ Byte 00H contains 0B0H, indicating that this is a COMDEF record.
■ Bytes 01-02H contain 0020H, the length of the remainder of the
record.
■ Bytes 03-0AH, 0B-15H, and 16-21H represent three declarations for
the communal variables foo, foo2, and foo3. The C compiler prepends
an underscore to each of the names declared in the source code, so
the symbols represented in this COMDEF record are _foo, _foo2, and
_foo3.
- Byte 03H contains 04H, the length of the first communal name in
this record. Bytes 04-07H contain the name itself (_foo). Byte
08H (the type index field) contains 00H, as required. Byte 09H
(the data segment type field) contains 62H, indicating this is a
NEAR variable. Byte 0AH (the communal length field) contains 02H,
the size of the variable in bytes.
- Byte 0BH contains 05H, the length of the second communal name.
Bytes 0C-10H contain the name, _foo2. Byte 11H is the type index
field, which again contains 00H as required. Byte 12H (the data
segment type field) contains 62H, indicating that _foo2 is a NEAR
variable.
Bytes 13-15H (the communal length field) contain the size in bytes
of the variable. The first byte of the communal length field (byte
13H) is 81H, indicating that the size is represented in the
subsequent 2 bytes of data--bytes 14-15H, which contain the value
8000H.
- Bytes 16-1BH represent the communal name field for _foo3, the
third communal variable declared in this record. Byte 1CH (the
type index field) again contains 00H as required. Byte 1DH (the
data segment type field) contains 61H, indicating this is a FAR
variable. This means the communal length field is formatted as a
number of elements field (bytes 1E-20H, which contain the value
0190H) and an element size field (byte 21H, which contains 01H).
The total size of this communal variable is thus 190H times 1, or
400 bytes.
■ Byte 22H contains the checksum, 99H.
Richard Wilton
Article 20: The Microsoft Object Linker
MS-DOS object modules can be processed in two ways: They can be
grouped together in object libraries, or they can be linked into
executable files. All Microsoft language translators are distributed
with two utility programs that process object modules: The Microsoft
Library Manager (LIB) creates and modifies object libraries; the
Microsoft Object Linker (LINK) processes the individual object records
within object modules to create executable files.
The following discussion focuses on LINK because of its crucial role
in creating an executable file. Before delving into the complexities
of LINK, however, it is worthwhile reviewing how object modules are
managed.
Object Files, Object Libraries, and LIB
Compilers and assemblers translate source-code modules into object
modules (Figure 20-1). See PROGRAMMING IN THE MS-DOS ENVIRONMENT:
PROGRAMMING TOOLS: Object Modules. An object module consists of a
sequence of object records that describe the form and content of part
of an executable program. An MS-DOS object module always starts with a
THEADR record; subsequent object records in the module follow the
sequence discussed in the Object Modules article.
Object modules can be stored in either of two types of MS-DOS files:
object files and object libraries. By convention, object files have
the filename extension .OBJ and object libraries have the extension
.LIB. Although both object files and object libraries contain one or
more object modules, the files and the libraries have different
internal organization. Furthermore, LINK processes object files and
libraries differently.
┌──────────────────┐
│ Source code │
└────────┬─────────┘
│ Language translator or assembler
│
┌─────────────────┐ ┌──────────────────┐
│ Object module │── Object module ──│ Object library │
│ (.OBJ file) │ librarian (LIB) │ (.LIB file) │
└────────┬─────────┘ └─────────┬────────┘
└─────────────────────┬─────────────────────┘
│ Linker (LINK)
│
┌─────────────────┐
│ Executable │
│ binary image │
│ (.EXE file) │
└────────┬─────────┘
│ MS-DOS loader
(Program runs)
Figure 20-1. Object modules, object libraries, LIB, and LINK.
The structures of object files and libraries are compared in Figure
20-2. An object file is a simple concatenation of object modules in
any arbitrary order. (Microsoft discourages the use of object files
that contain more than one object module; Microsoft language
translators never generate more than one object module in an object
file.) In contrast, a library contains a hashed dictionary of all the
public symbols declared in each of the object modules, in addition to
the object modules themselves. Each symbol in the dictionary is as-
sociated with a reference to the object module in which the symbol
was declared.
LINK processes object files differently than it does libraries. When
LINK builds an executable file, it incorporates all the object modules
in all the object files it processes. In contrast, when LINK processes
libraries, it uses the hashed symbol dictionary in each library to
extract object modules selectively--it uses an object module from a
library only when the object module contains a symbol that is
referenced within some other object module. This distinction between
object files and libraries is important in understanding what LINK
does.
(a) ┌──────────────────┐
│ │
│ Object module │
│ │
├──────────────────┤
│ │
│ Object module │
├──────────────────┤
│ │
│ Object module │
│ │
└──────────────────┘
(b) ┌──────────────────┐
│ Library header │
├──────────────────┤
│ │
│ Object module │
│ │
├──────────────────┤
│ │
│ Object module │
├──────────────────┤
│ │
│ Object module │
│ │
├──────────────────┤
│ │
│ │
│Symbol dictionary │
│ │
│ │
└──────────────────┘
Figure 20-2. Structures of an object file and an object library. (a)
An object file contains one or more object modules. (Microsoft
discourages using more than one object module per object file.) (b) An
object library contains one or more object modules plus a hashed
symbol dictionary indicating the object modules in which each public
symbol is defined.
What LINK Does
The function of LINK is to translate object modules into an executable
program. LINK's input consists of one or more object files (.OBJ
files) and, optionally, one or more libraries (.LIB files). LINK's
output is an executable file (.EXE file) containing binary data that
can be loaded directly from the file into memory and executed. LINK
can also generate a symbolic address map listing (.MAP file)--a text
file that describes the organization of the .EXE file and the
correspondence of symbols declared in the object modules to addresses
in the executable file.
Building an executable file
LINK builds two types of information into a .EXE file. First, it
extracts executable code and data from the LEDATA and LIDATA records
in object modules, arranges them in a specified order according to its
rules for segment combination and relocation, and copies the result
into the .EXE file. Second, LINK builds a header for the .EXE file.
The header describes the size of the executable program and also
contains a table of load-time segment relocations and initial values
for certain CPU registers. See Pass 2, below.
Relocation and linking
In building an executable image from object modules, LINK performs two
essential tasks: relocation and linking. As it combines and rearranges
the executable code and data it extracts from the object modules it
processes, LINK frequently adjusts, or relocates, address references
to account for the rearrangements (Figure 20-3). LINK links object
modules by resolving address references among them. It does this by
matching the symbols declared in EXTDEF and PUBDEF object records
(Figure 20-4). LINK uses FIXUPP records to determine exactly how to
compute both address relocations and linked address references.
Object Module Order
LINK processes input files from three sources: object files and
libraries specified explicitly by the user (in the command line, in
response to LINK's prompts, or in a response file) and object
libraries named in object module COMENT records.
┌─────────────────────┐
│ │
│ │
│ │
┌─────────────────────┐ │ Code segment │
│ │ │ (B4H bytes) │
│ Code segment │ │ │
│ (64H bytes) │ │ │
│ │ ┌─────────────────────┐ │Label1 at offset 10H │
│Label1 at offset 10H │ │ Code segment │ │Label2 at offset 74H │
│ │ │ (50H bytes) │ │ │
│ │ │Label2 at offset 10H │ │ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
Module1 Module2 Combined code segment
Figure 20-3. A simple relocation. Both object modules contain code
that LINK combines into one logical segment. In this example, LINK
appends the 50H bytes of code in Module2 to the 64H bytes of code in
Module1. LINK relocates all references to addresses in the code
segment so that they apply to the combined segment.
┌─────────────────────┐
│ Code segment. │
│ . │
│ . │
┌─────────────────────┐ ┌─────────────────────┐ │ . │
│ │ │ │ │ jmp Label2 │
│ Code segment │ │ Code segment │ │ . │
│ EXTDEF Label2 │ │ PUBDEF Label2 │ │ . │
│ │ │ │ │ . │
│ jmp Label2 │ │ Label2: . │ │ Label2: │
│ . │ │ . │ │ . │
│ . │ │ . │ │ . │
│ . │ │ │ │ . │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
Module1 Module2 Combined code segment
Figure 20-4. Resolving an external reference. LINK resolves the
external reference in Module1 (declared in an EXTDEF record) with
the address of Label2 in Module2 (declared in a PUBDEF record).
LINK always uses all the object modules in the object files it
processes. In contrast, it extracts individual object modules from
libraries--only those object modules needed to resolve references to
public symbols are used. This difference is implicit in the order in
which LINK reads its input files:
1. Object files specified in the command line or in response to the
Object Modules prompt
2. Libraries specified in the command line or in response to the
Libraries prompt
3. Libraries specified in COMENT records
The order in which LINK processes object modules influences the
resulting executable file in three ways. First, the order in which
segments appear in LINK's input files is reflected in the segment
structure of the executable file. Second, the order in which LINK
resolves external references to public symbols depends on the order
in which it finds the public symbols in its input files. Finally, LINK
derives the default name of the executable file from the name of the
first input object file.
Segment order in the executable file
In general, LINK builds named segments into the executable file in the
order in which it first encounters the SEGDEF records that declare the
segments. (The /DOSSEG switch also affects segment order. See Using
the /DOSSEG Switch, below.) This means that the order in which
segments appear in the executable file can be controlled by linking
object modules in a specific order. In assembly-language programs, it
is best to declare all the segments used in the program in the first
object module to be linked so that the segment order in the executable
file is under complete control.
Order in which references are resolved
LINK resolves external references in the order in which it encounters
the corresponding public declarations. This fact is important because
it determines the order in which LINK extracts object modules from
libraries. When a public symbol required to resolve an external
reference is declared more than once among the object modules in the
input libraries, LINK uses the first object module that contains the
public symbol. This means that the actual executable code or data
associated with a particular external reference can be varied by
changing the order in which LINK processes its input libraries.
For example, imagine that a C programmer has written two versions of
a function named myfunc() that is called by the program MYPROG.C. One
version of myfunc() is for debugging; its object module is found in
MYFUNC.OBJ. The other is a production version whose object module
resides in MYLIB.LIB. Under normal circumstances, the programmer links
the production version of myfunc() by using MYLIB.LIB (Figure 20-5).
To use the debugging version of myfunc(), the programmer explicitly
includes its object module (MYFUNC.OBJ) when LINK is executed. This
causes LINK to build the debugging version of myfunc() into the
executable file because it encounters the debugging version in
MYFUNC.OBJ before it finds the other version in MYLIB.LIB.
To exploit the order in which LINK resolves external references, it
is important to know LINK's library search strategy: Each individual
library is searched repeatedly (from first library to last, in the
sequence in which they are input to LINK) until no further external
references can be resolved.
┌───────────────────┐ ┌────────────────────┐
│ main() │ │ │
│ { │ │ │
│ x=myfunc(y); ├─────│EXTDEF for myfunc() ├─┐ ┌────────────────────┐
│ } │ │ │ │ │ │
└───────────────────┘ └────────────────────┘ │ │ │
MYPROG.C MYPROG.OBJ │ │ Executable code │
┌───────────────────┐ ┌────────────────────┐ ├─│ contains myfunc() │
│ myfunc(a) │ │ │ │ │derived from either │
│ int a; │ │ │ │ │ MYFUNC.OBJ or │
│ { ├─────│PUBDEF for myfunc() ├─┤ │ MYLIB.OBJ │
│ . │ │ │ │ │ │
│ . │ └────────────────────┘ │ │ │
│ . │ MYFUNC.OBJ │ └────────────────────┘
│ } │ ┌────────────────────┐ │
└───────────────────┘ │ . │ │
MYFUNC.C │ . │ │
│ . │ │
│PUBDEF for myfunc() │ │
│ . ├─┘
│ . │
│ . │
│ │
└────────────────────┘
MYLIB.LIB
Figure 20-5. Ordered object module processing by LINK. (a) With the
command LINK MYPROG,,,MYLIB, the production version of myfunc() in
MYLIB.LIB is used. (b) With the command LINK
MYPROG+<QL>MYFUNC,,,MYLIB, the debugging version of myfunc() in
MYFUNC.OBJ is used.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ModuleA │ │ ModuleC │ │ ModuleMAIN │ │ │ Start
│ Call C │ │ Call B │ │ Call A │ │ ModuleMAIN │ of
├──────────────┤ └──────────────┘ └──────────────┘ ├──────────────┤ pro-
│ │ │ │ gram
│ ModuleB │ │ ModuleA │
└──────────────┘ ├──────────────┤
LIB1.LIB LIB2.LIB MYPROG.OBJ │ │
│ ModuleC │
├──────────────┤
│ │
│ ModuleB │
└──────────────┘
MYPROG.EXE
Figure 20-6. Library search order. Modules are incorporated into the
executable file as LINK extracts them from the libraries to resolve
external references.
The example in Figure 20-6 demonstrates this search strategy. Library
LIB1.LIB contains object modules A and B, library LIB2.LIB contains
object module C, and the object file MYPROG.OBJ contains the object
module MAIN; modules MAIN, A, and C each contain an external reference
to a symbol declared in another module. When this program is linked
with
LINK MYPROG,,,LIB1+LIB2 <ENTER>
LINK starts by incorporating the object module MAIN into the
executable program. It then searches the input libraries until it
resolves all the external references:
1. Process MYPROG.OBJ, find unresolved external reference to A.
2. Search LIB1.LIB, extract A, find unresolved external reference
to C.
3. Search LIB1.LIB again; reference to C remains unresolved.
4. Search LIB2.LIB, extract C, find unresolved external reference
to B.
5. Search LIB2.LIB again; reference to B remains unresolved.
6. Search LIB1.LIB again, extract B.
7. No more unresolved external references, so end library search.
The order in which the modules appear in the executable file thus
reflects the order in which LINK resolves the external references;
this, in turn, depends on which modules were contained in the
libraries and on the order in which the libraries are input to LINK.
Name of the executable file
If no filename is specified in the command line or in response to the
Run File prompt, LINK derives the name of the executable file from the
name of the first object file it processes. For example, if the object
files PROG1.OBJ and PROG2.OBJ are linked with the command
LINK PROG1+PROG2; <ENTER>
the resulting executable file, PROG1.EXE, takes its name from the
first object file processed by LINK.
Segment Order and Segment Combinations
LINK builds segments into the executable file by applying the
following sequence of rules:
1. Segments appear in the executable file in the order in which their
SEGDEF declarations first appear in the input object modules.
2. Segments in different object modules are combined if they have the
same name and class and a public, memory, stack, or common combine
type. All address references within the combined segments are
relocated relative to the start of the combined segment.
- Segments with the same name and either the public or the memory
combine type are combined in the order in which they are
processed by LINK. The size of the resulting segment equals the
total size of the combined segments.
- Segments with the same name and the stack combine type are
overlapped so that the data in each of the overlapped segments
ends at the same address. The size of the resulting segment
equals the total size of the combined segments. The resulting
segment is always paragraph aligned.
- Segments with the same name and the common combine type are
overlapped so that the data in each of the overlapped segments
starts at the same address. The size of the resulting segment
equals the size of the largest of the overlapped segments.
3. Segments with the same class name are concatenated.
4. If the /DOSSEG switch is used, the segments are rearranged in
conjunction with DGROUP. See Using the /DOSSEG Switch below.
These rules allow the programmer to control the organization of
segments in the executable file by ordering SEGMENT declarations in an
assembly-language source module, which produces the same order of
SEGDEF records in the corresponding object module, and by placing this
object module first in the order in which LINK processes its input
files.
A typical MS-DOS program is constructed by declaring all executable
code and data segments with the public combine type, thus enabling the
programmer to compile the program's source code from separate source-
code modules into separate object modules. When these object modules
are linked, LINK combines the segments from the object modules
according to the above rules to create logically unified code and data
segments in the executable file.
Segment classes
LINK concatenates segments with the same class name after it combines
segments with the same segment name and class. For example, Figure
20-7 shows the following compiling and linking:
MASM MYPROG1; <ENTER>
MASM MYPROG2; <ENTER>
LINK MYPROG1+MYPROG2; <ENTER>
┌──────────────────────────────┐ ┌────────────┐ ┌─────────┐
│ │ │ SEGDEF │ │ │▓ ▒
│ _TEXT SEGMENT public 'CODE' │ │ for TEXT │ │ │▓ ▒
│ │ │ │ │ │▓_TEXT ▒
├──────────────────────────────┤ │ SEGDEF │ │ │▓ segment ▒
│FAR_TEXT SEGMENT public 'CODE'├│for FAR_TEXT├┐ │ │▓ ▒
├──────────────────────────────┤ │ ││ │ │▓ ▒─┐
│ │ │ SEGDEF ││ ├─────────┤ ▒ │
│ _DATA SEGMENT public 'CODE' │ │ for _DATA ││ │ │▓ ▒ │
│ │ │ │├│ │▓FAR_TEXT▒ │
└──────────────────────────────┘ └────────────┘│ │ │▓ segment ▒ │
MYPROG1.ASM MYPROG1.OBJ │ │ │▓ ▒ │
┌──────────────────────────────┐ ┌────────────┐│ ├─────────┤ │
│ _TEXT SEGMENT public 'CODE' │ │ ││ │ │▓_DATA │
│ │ │ SEGDEF ││ │ │▓ segment │
│ │ │ for _TEXT ││ └─────────┘ │
├──────────────────────────────┼│ ││ MYPROG1.EXE │
│FAR_TEXT SEGMENT public 'CODE'│ │ SEGDEF ├┘ │
│ │ │for FAR_TEXT│ │
│ │ │ │ │
└──────────────────────────────┘ └────────────┘ 'CODE' ──────┘
MYPROG2.ASM MYPROG2.OBJ class
Figure 20-7. Segment order and concatenation by LINK. The start of
each file, corresponding to the lowest address, is at the top.
After MYPROG1.ASM and MYPROG2.ASM have been compiled, LINK builds the
_TEXT and FAR_TEXT segments by combining segments with the same name
from the different object modules. Then, _TEXT and FAR_TEXT are
concatenated because they have the same class name ('CODE'). _TEXT
appears before FAR_TEXT in the executable file because LINK encounters
the SEGDEF record for _TEXT before it finds the SEGDEF record for
FAR_TEXT.
Segment alignment
LINK aligns the starting address of each segment it processes
according to the alignment specified in each SEGDEF record. It adjusts
the alignment of each segment it encounters regardless of how that
segment is combined with other segments of the same name or class.
(The one exception is stack segments, which always start on a
paragraph boundary.)
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ _DATA SEGMENT byte │ │ _DATA SEGMENT word │ │ _DATA SEGMENT para │
│ public (35H bytes) │ │ public (35H bytes) │ │ public (35H bytes) │
└────────────────────┘ └────────────────────┘ └────────────────────┘
Module1 Module2 Module3
00H ┌────────────────────┐
│ │▓ 35H bytes (byte aligned)
│ Module1 │▓
35H └────────────────────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
36H ┌────────────────────┐
│ │▓ 35H bytes (word aligned)
│ Module2 │▓
6BH └────────────────────┘
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
70H ┌────────────────────┐
│ │▓ 35H bytes (paragraph aligned)
│ Module3 │▓
└────────────────────┘
Resulting _DATA segment
in .EXE file
Figure 20-8. Alignment of combined segments. LINK enforces segment
alignment by padding combined segments with uninitialized data bytes.
Segment alignment is particularly important when public segments with
the same name and class are combined from different object modules.
Note what happens in Figure 20-8, where the three concatenated _DATA
segments have different alignments. To enforce the word alignment and
paragraph alignment of the _DATA segments in Module2 and Module3, LINK
inserts one or more bytes of padding between the segments.
Segment groups
A segment group establishes a logical segment address to which all
offsets in a group of segments can refer. That is, all addresses in
all segments in the group can be expressed as offsets relative to the
segment value associated with the group (Figure 20-9). Declaring
segments in a group does not affect their positions in the executable
file; the segments in a group may or may not be contiguous and can
appear in any order as long as all address references to the group
fall within 64 KB of each other.
DataGroup GROUP DataSeg1,DataSeg2
CodeSeg SEGMENT byte public 'CODE'
ASSUME cs:CodeSeg
mov ax,offset DataSeg2:TestData
mov ax,offset DataGroup:TestData
CodeSeg ENDS
DataSeg1 SEGMENT para public 'DATA'
DB 100h dup(?)
DataSeg1 ENDS
DataSeg2 SEGMENT para public 'DATA'
TestData DB ?
DataSeg2 ENDS
END
Figure 20-9. Example of group addressing. The first MOV loads the
value 00H into AX (the offset of TestData relative to DataSeg2); the
second MOV loads the value 100H into AX (the offset of TestData
relative to the group DataGroup).
LINK reserves one group name, DGROUP, for use by Microsoft language
translators. DGROUP is used to group compiler-generated data segments
and a default stack segment. See DGROUP, below.
LINK Internals
Many programmers use LINK as a "black box" program that transforms
object modules into executable files. Nevertheless, it is helpful to
observe how LINK processes object records to accomplish this task.
LINK is a two-pass linker; that is, it reads all its input object
modules twice. On Pass 1, LINK builds an address map of the segments
and symbols in the object modules. On Pass 2, it extracts the
executable code and program data from the object modules and builds a
memory image--an exact replica--of the executable file.
The reason LINK builds an image of the executable file in memory,
instead of simply copying code and data from object modules into the
executable file, is that it organizes the executable file by segments
and not by the order in which it processes object modules. The most
efficient way to concatenate, combine, and relocate the code and data
is to build a map of the executable file in memory during Pass 1 and
then fill in the map with code and data during Pass 2.
In versions 3.52 and later, whenever the /I (/INFORMATION) switch is
specified in the command line, LINK displays status messages at the
start of each pass and as it processes each object module. If the /M
(/MAP) switch is used in addition to the /I switch, LINK also displays
the total length of each segment declared in the object modules. This
information is helpful in determining how the structure of an
executable file corresponds to the contents of the object modules
processed by LINK.
Pass 1
During Pass 1, LINK processes the LNAMES, SEGDEF, GRPDEF, COMDEF,
EXTDEF, and PUBDEF records in each input object module and uses the
information in these object records to construct a symbol table and an
address map of segments and segment groups.
Symbol table
As each object module is processed, LINK uses the symbol table to
resolve external references (declared in EXTDEF and COMDEF records) to
public symbols. If LINK processes all the object files without
resolving all the external references in the symbol table, it searches
the input libraries for public symbols that match the unresolved
external references. LINK continues to search each library until all
the external references in the symbol table are resolved.
Segments and groups
LINK processes each SEGDEF record according to the segment name, class
name, and attributes specified in the record. LINK constructs a table
of named segments and updates it as it concatenates or combines
segments. This allows LINK to associate each public symbol in the
symbol table with an offset into the segment in which the symbol is
declared.
LINK also generates default segments into which it places communal
variables declared in COMDEF records. Near communal variables are
placed in one paragraph-aligned public segment named c_common, with
class name BSS (block storage space) and group DGROUP. Far communal
variables are placed in a paragraph-aligned segment named FAR_BSS,
with class name FAR_BSS. The combine type of each far communal
variable's FAR_BSS segment is private (that is, not public, memory,
common, or stack). As many FAR_BSS segments as necessary are
generated.
After all the object files have been read and all the external
references in the symbol table have been resolved, LINK has a complete
map of the addresses of all segments and symbols in the program. If a
.MAP file has been requested, LINK creates the file and writes the
address map to it. Then LINK initiates Pass 2.
Pass 2
In Pass 2, LINK extracts executable code and program data from the
LEDATA and LIDATA records in the object modules. It builds the code
and data into a memory image of the executable file. During Pass 2,
LINK also carries out all the address relocations and fixups related
to segment relocation, segment grouping, and resolution of external
references, as well as any other address fixups specified explicitly
in object module FIXUPP records.
If it determines during Pass 2 that not enough RAM is available to
contain the entire image, LINK creates a temporary file in the current
directory on the default disk drive. (LINK versions 3.60 and later use
the environment variable TMP to find the directory for the temporary
scratch file.) LINK then uses this file in addition to all the
available RAM to construct the image of the executable file. (In
versions of MS-DOS earlier than 3.0, the temporary file is named
VM.TMP; in versions 3.0 and later, LINK uses Interrupt 21H Function
5AH to create the file.)
LINK reads each of the input object modules in the same order as it
did in Pass 1. This time it copies the information from each object
module's LEDATA and LIDATA records into the memory image of each
segment in the proper sequence. This is when LINK expands the iterated
data in each LIDATA record it processes.
LINK processes each LEDATA and LIDATA record along with the
corresponding FIXUPP record, if one exists. LINK processes the FIXUPP
record, performs the address calculations required for relocation,
segment grouping, and resolving external references, and then stores
binary data from the LEDATA or LIDATA record, including the results of
the address calculations, in the proper segment in the memory image.
The only exception to this process occurs when a FIXUPP record refers
to a segment address. In this case, LINK adds the address of the fixup
to a table of segment fixups; this table is used later to generate the
segment relocation table in the .EXE header.
When all the data has been extracted from the object modules and all
the fixups have been carried out, the memory image is complete. LINK
now has all the information it needs to build the .EXE header (Table
20-1). At this point, therefore, LINK creates the executable file and
writes the header and all segments into it.
Table 20-1. How LINK Builds a .EXE File Header.
╓┌──────────────┌─────────────────────────────────────┌──────────────────────╖
Offset Contents Comments
──────────────────────────────────────────────────────────────────
00H 'MZ' .EXE file signature
02H Length of executable image MOD 512 ▒
▒ Total size of all
04H Length of executable image in 512- ▒ segments plus
byte pages, including ▒ .EXE file header
last partial page (if any) ▒
06H Number of run-time segment Number of
relocations segment fixups
08H Size of the .EXE header in 16- Size of segment
byte paragraphs relocation table
0AH MINALLOC: Minimum amount of Size of uninitialized
RAM to be allocated above data and/or stack
end ofthe loaded program (in segments at end of
16-byte paragraphs) program (0 if /HI
switch is used)
0CH MAXALLOC: Maximum amount of 0 if /HI switch is
RAM to be allocated above end used; value specified
of the loaded program (in 16-byte with /CP switch;
paragraphs) FFFFH if /CP and /HI
switches are not
used
0EH Stack segment (initial value for SS Address of
program register); prelocated by stack segment
MS-DOS when program is loaded relative to start of
executable image
10H Stack pointer (initial value for Size of stack segment
register SP) in bytes
12H Checksum One's complement of
sum of all words in
file, excluding
checksum itself
14H Entry point offset (initial value ▒
for register IP) ▒ MODEND object
▒ record that
16H Entry point segment (initial value ▒ specifies program
for register CS); relocated by ▒ start address
MS-DOS when program is loaded ▒
18H Offset of start of segment
relocation table relative to
start of .EXE header
1AH Overlay number 0 for resident
segments; >0 for
overlay segments
1CH Reserved
Using LINK to Organize Memory
By using LINK to rearrange and combine segments, a programmer can
generate an executable file in which segment order and addressing
serve specific purposes. As the following examples demonstrate,
careful use of LINK leads to more efficient use of memory and simpler,
more efficient programs.
Segment order for a TSR
In a terminate-and-stay-resident (TSR) program, LINK must be used
carefully to generate segments in the executable file in the proper
order. A typical TSR program consists of a resident portion, in which
the TSR application is implemented, and a transient portion, which
executes only once to initialize the resident portion. See PROGRAMMING
IN THE MS-DOS ENVIRONMENT: CUSTOMIZING MS-DOS: Terminate-and-Stay-
Resident Utilities.
Because the transient portion of the TSR program is executed only
once, the memory it occupies should be freed after the resident
portion has been initialized. To allow the MS-DOS Terminate and Stay
Resident function (Interrupt 21H Function 31H) to free this memory
when it leaves the resident portion of the TSR program in memory, the
TSR program must have its resident portion at lower addresses than its
transient portion.
──────────────────────────────────────────────────────────────────────
Figure 20-10. Segment order for a terminate-and-stay-resident program.
──────────────────────────────────────────────────────────────────────
In Figure 20-10, the segments containing the resident code and data
are declared before the segments that represent the transient portion
of the program. Because LINK preserves this segment order, the
executable program has the desired structure, with resident code and
data at lower addresses than transient code and data. Moreover, the
number of paragraphs in the resident portion of the program, which
must be computed before Interrupt 21H Function 31H is called, is easy
to derive from the segment structure: This value is the difference
between the segment address of the program segment prefix, which
immediately precedes the first segment in the resident portion, and
the address of the first segment in the transient portion of the
program.
Groups for unified segment addressing
In some programs it is desirable to maintain executable code and data
in separate logical segments but to address both code and data with
the same segment register. For example, in a hardware interrupt
handler, using the CS register to address program data is generally
simpler than using DS or ES.
In the routine in Figure 20-11, code and data are maintained in
separate segments for program clarity, yet both can be addressed using
the CS register because both code and data segments are included in
the same group. (The SNAP.ASM listing in the Terminate-and-Stay-
Resident Utilities article is another example of this use of a group
to unify segment addressing.)
ISRgroup GROUP CodeSeg,DataSeg
CodeSeg SEGMENT byte public 'CODE'
ASSUME cs:ISRgroup
mov ax,offset ISRgroup:CodeLabel
CodeLabel: mov bx,ISRgroup:DataLabel
CodeSeg ENDS
DataSeg SEGMENT para public 'DATA'
DataLabel DW ?
DataSeg ENDS
END
Figure 20-11. Code and data included in the same group. In this
example, addresses within both CodeSeg and DataSeg are referenced
relative to the CS register by grouping the segments (using the
assembler GROUP directive) and addressing the group through CS (using
the assembler ASSUME directive).
Uninitialized data segments
A segment that contains only uninitialized data can be processed by
LINK in two ways, depending on the position of the segment in the
program. If the segment is not at the end of the program, LINK
generates a block of bytes initialized to zero to represent the
segment in the executable file. If the segment appears at the end of
the program, however, LINK does not generate a block of zeroed bytes.
Instead, it increases the minimum run-time memory allocation by
increasing MINALLOC (specified at offset 0AH in the .EXE header) by
the amount of memory required for the segment.
Therefore, if it is necessary to reserve a large amount of
uninitialized memory in a segment, the size of the .EXE file can be
decreased by building the segment at the end of a program (Figure
20-12). This is why, for example, Microsoft high-level-language
translators always build BSS and STACK segments at the end of compiled
programs. (The loader does not fill these segments with zeros; a
program must still initialize them with appropriate values.)
(a) CodeSeg SEGMENT byte public 'CODE'
ASSUME cs:CodeSeg,ds:DataSeg
ret
CodeSeg ENDS
DataSeg SEGMENT word public 'DATA'
BigBuffer DB 10000 dup(?)
DataSeg ENDS
END
(b) DataSeg SEGMENT word public 'DATA'
BigBuffer DB 10000 dup(?)
DataSeg ENDS
CodeSeg SEGMENT byte public 'CODE'
ASSUME cs:CodeSeg,ds:DataSeg
ret
CodeSeg ENDS
END
Figure 20-12. LINK processing of uninitialized data segments. (a) When
DataSeg, which contains only uninitialized data, is placed at the end
of this program, the size of the .EXE file is only 513 bytes. (b) When
DataSeg is not placed at the end of the program, the size of the .EXE
file is 10513 bytes.
Overlays
If a program contains two or more subroutines that are mutually
independent--that is, subroutines that do not transfer control to each
other--LINK can be instructed to build each subroutine into a
separately loaded portion of the executable file. (This instruction is
indicated in the command line when LINK is executed by enclosing each
overlay subroutine or group of subroutines in parentheses.) Each of
the subroutines can then be overlaid as it is needed in the same area
of memory (Figure 20-13). The amount of memory required to run a
program that uses overlays is, therefore, less than the amount
required to run the same program without overlays.
A program that uses overlays must include the Microsoft run-time
overlay manager. The overlay manager is responsible for copying
overlay code from the executable file into memory whenever the program
attempts to transfer control to code in an overlay. A program that
uses overlays runs slower than a program that does not use them,
because it takes longer to extract overlays separately from the .EXE
file than it does to read the entire .EXE file into memory at once.
(a)
┌───────────┐
E│ │
├───────────┤
D│ Call E() │ (b)
├───────────┤ ┌───────────┐ ┌───────────┐ ┌───────────┐
C│ │ ▒│ │ C│ │ E│ │
├───────────┤ Overlay ▒│ │ ├───────────┤ ├───────────┤
B│ Call C() │ area ▒│ │ B│ Call C() │ D│ Call E() │
├───────────┤ ├───────────┤ └───────────┘ └───────────┘
A│ Call B() │ A│ Call B() │
│ Call D() │ │ Call D() │
└───────────┘ └───────────┘
LINK A+B+C+D+E; LINK A+(B+C)+(D+E);
Figure 20-13. Memory use in a program linked (a) without overlays and
(b) with overlays. In (b), either modules (B+C) or modules (D+E) can
be loaded into the overlay area at run time.
The default object libraries that accompany Microsoft high-level-
language compilers contain object modules that support the Microsoft
run-time overlay manager. The following description of LINK's
relationship to the run-time overlay manager applies to versions 3.00
through 3.60 of LINK; implementation details may vary in future
versions.
Overlay format in a .EXE file
An executable file that contains overlays has a .EXE header preceding
each overlay (Figure 20-14). The overlays are numbered in sequence,
starting at 0; the overlay number is stored in the word at offset 1AH
in each overlay's .EXE header. When the contents of the .EXE file are
loaded into memory for execution, only the resident, nonoverlaid part
of the program is copied into memory. The overlays must be read into
memory from the .EXE file by the run-time overlay manager.
Start of file ┌───────────────────┐
│ .EXE header │ Overlay number 0
├───────────────────┤
│ │
│ │
│ │
│ │
│ A │
│ Overlay segments │
│ │
│ │
├───────────────────┤
│ .EXE header │ Overlay number 1
├───────────────────┤
│ │
│ B │
│ C │
├───────────────────┤
│ .EXE header │ Overlay number 2
├───────────────────┤
│ │
│ D │
│ E │
End of file └───────────────────┘
Figure 20-14. .EXE file structure produced by LINK A + (B+C) + (D+E).
Segments for overlays
When LINK produces an executable file that contains overlays, it adds
three segments to those defined in the object modules: OVERLAY_AREA,
OVERLAY_END, and OVERLAY_DATA. LINK assigns the segment class name
'CODE' to OVERLAY_AREA and OVERLAY_END and includes OVERLAY_DATA in
the default group DGROUP.
OVERLAY_AREA is a reserved segment into which the run-time overlay
manager is expected to load each overlay as it is needed. Therefore,
LINK sets the size of OVERLAY_AREA to fit the largest overlay in the
program. The OVERLAY_END segment is declared immediately after
OVERLAY_AREA, so a program can determine the size of the OVERLAY_AREA
segment by subtracting its segment address from that of OVERLAY_END.
The OVERLAY_DATA segment is initialized by LINK with information about
the executable file, the number of overlays, and other data useful to
the run-time overlay manager.
LINK requires the executable code used in overlays to be contained in
segments whose class names end in CODE and whose segment names differ
from those of the segments used in the resident (nonoverlaid) portion
of the program. In assembly language, this is accomplished by using
the SEGMENT directive; in high-level languages, the technique of
ensuring unique segment names depends on the compiler. In Microsoft C,
for example, the /A switch in the command line selects the memory
model and thus the segment naming defaults used by the compiler; in
medium, large, and huge memory models, the compiler generates a unique
segment name for each C function in the source code. In Microsoft
FORTRAN, on the other hand, the compiler always generates a uniquely
named segment for each SUBROUTINE and FUNCTION in the source code, so
no special programming is required.
LINK substitutes all far CALL instructions from root to overlay or
from overlay to overlay with a software interrupt followed by an
overlay number and an offset into the overlay segment (Figure 20-15).
The interrupt number can be specified with LINK's /OVERLAYINTERRUPT
switch; if the switch is omitted, LINK uses Interrupt 3FH by default.
By replacing calls to overlay code with a software interrupt, LINK
provides a mechanism for the run-time overlay manager to take control,
load a specified overlay into memory, and transfer control to a
specified offset within the overlay.
(a) EXTRN OverlayEntryPoint:far
call OverlayEntryPoint ; far CALL
(b) int IntNo ; interrupt number
; specified with
; /OVERLAYINTERRUPT
; switch (default 3FH)
DB OverlayNumber ; overlay number
DW OverlayEntry ; offset of overlay entry point
; (the address to which the
; overlay manager transfers
; control)
Figure 20-15. Executable code modification by LINK for accessing
overlays. (a) Code as written. (b) Code as modified by LINK.
Run-time processing of overlays
The resident (nonoverlaid) portion of a program that uses overlays
initializes the overlay interrupt vector specified by LINK with the
address of the run-time overlay manager. (The OVERLAY_DATA segment
contains the interrupt number.) The overlay manager then takes control
wherever LINK has substituted a software interrupt for a far call in
the executable code.
Each time the overlay manager executes, its first task is to determine
which overlay is being called. It does this by using the return
address left on the stack by the INT instruction that invoked the
overlay manager; this address points to the overlay number stored in
the byte after the interrupt instruction that just executed. The
overlay manager then determines whether the destination overlay is
already resident and loads it only if necessary. Next, the overlay
manager opens the .EXE file, using the filename in the OVERLAY_DATA
segment. It locates the start of the specified overlay in the file by
examining the length (offset 02H and offset 04H) and overlay number
(offset 1AH) in each overlay's .EXE header.
The overlay manager can then read the overlay from the .EXE file into
the OVERLAY_AREA segment. It uses the overlay's segment relocation
table to fix up any segment references in the overlay. The overlay
manager transfers control to the overlay with a far call to the
OVERLAY_AREA segment, using the offset stored by LINK 1 byte after the
interrupt instruction (see Figure 20-15).
Interrupt 21H Function 4BH
LINK's protocol for implementing overlays is not recognized by
Interrupt 21H Function 4BH (Load and Execute Program). This MS-DOS
function, when called with AL = 03H, loads an overlay from a .EXE file
into a specified location in memory. See SYSTEM CALLS: INTERRUPT 21H:
Function 4BH. However, Function 4BH does not use an overlay number, so
it cannot find overlays in a .EXE file formatted by LINK with multiple
.EXE headers.
DGROUP
LINK always includes DGROUP in its internal table of segment groups.
In object modules generated by Microsoft high-level-language
translators, DGROUP contains both the default data segment and the
stack segment. LINK's /DOSSEG and /DSALLOCATE switches both affect the
way LINK treats DGROUP. Changing the way LINK manages DGROUP
ultimately affects segment order and addressing in the executable
file.
Using the /DOSSEG switch
The /DOSSEG switch causes LINK to arrange segments in the default
order used by Microsoft high-level-language translators:
1. All segments with a class name ending in CODE. These segments
contain executable code.
2. All other segments outside DGROUP. These segments typically contain
far data items.
3. DGROUP segments. These are a program's near data and stack
segments. The order in which segments appear in DGROUP is
- Any segments of class BEGDATA. (This class name is reserved for
Microsoft use.)
- Any segments not of class BEGDATA, BSS, or STACK.
- Segments of class BSS.
- Segments of class STACK.
This segment order is necessary if programs compiled by Microsoft
translators are to run properly. The /DOSSEG switch can be used
whenever an object module produced by an assembler is linked ahead of
object modules generated by a Microsoft compiler, to ensure that
segments in the executable file are ordered as in the preceding list
regardless of the order of segments in the assembled object module.
When the /DOSSEG switch is in effect, LINK always places DGROUP at the
end of the executable program, with all uninitalized data segments at
the end of the group. As discussed above, this placement helps to
minimize the size of the executable file. The /DOSSEG switch also
causes LINK to restructure the executable program to support certain
conventions used by Microsoft language translators:
■ Compiler-generated segments with the class name BEGDATA are placed
at the beginning of DGROUP.
■ The public symbols _edata and _end are generated to point to the
beginning of the BSS and STACK segments.
■ Sixteen bytes of zero are inserted in front of the _TEXT segment.
Microsoft compilers that rely on /DOSSEG conventions generate a
special COMENT object record that sets the /DOSSEG switch when the
record is processed by LINK.
Using the /HIGH and /DSALLOCATE switches
When a program has been linked without using LINK's /HIGH switch, MS-
DOS loads program code and data segments from the .EXE file at the
lowest address in the first available block of RAM large enough to
contain the program (Figure 20-16). The value in the .EXE header at
offset 0CH specifies the maximum amount of extra RAM MS-DOS must
allocate to the program above what is loaded from the .EXE file. Above
that, all unused RAM is managed by MS-DOS. With this memory allocation
strategy, a program can use Interrupt 21H Functions 48H (Allocate
Memory Block) and 4AH (Resize Memory Block) to increase or decrease
the amount of RAM allocated to it.
When a program is linked with LINK's /HIGH switch, LINK zeros the
words it stores in the .EXE header at offset 0AH and 0CH. Setting the
words at 0AH and 0CH to zero indicates that the program is to be
loaded into RAM at the highest address possible (Figure 20-16). With
this memory layout, however, a program can no longer change its memory
allocation dynamically because all available RAM is allocated to the
program when it is loaded and the uninitialized RAM between the
program segment prefix and the program itself cannot be freed.
┌─ FFFFFH FFFFFH ─┐
└┌─────────────────────┐ ┌─────────────────────┐┘
│ │ │ │
│ System ROM, etc. │ │ System ROM, etc. │
│ │ │ │
├─────────────────────┤ ├─────────────────────┤
│ │ │ │▒
│ │ │ │▒Program code
│ │ │ │▒ and data
│ (Unused) │ ├─────────────────────┤ segments
│ │ │ │ copied from
│ │ │ │ .EXE file
│ │ │ │
├─────────────────────┤ │ │
│ Uninitialized │▒Specified │ Uninitialized │
│ program RAM │▒ in .EXE │ program RAM │
├─────────────────────┤ header │ │
│ │▒ │ │
│ │▒Program code│ │
│ │▒ and data │ │
├─────────────────────┤ segments ├─────────────────────┤
│ Environment, PSP │ copied from │ Environment, PSP │
├─────────────────────┤ .EXE file ├─────────────────────┤
│ │ │ │
│ │ │ │
│ Resident portion │ │ Resident portion │
│ of MS-DOS │ │ of MS-DOS │
│ │ │ │
┌└─────────────────────┘ └─────────────────────┘─┐
└─ 00000H 00000H ──┘
(a) (b)
Figure 20-16. Effect of the /HIGH switch on run-time memory use. (a)
The program is linked without the /HIGH switch. (b) The program is
linked with the /HIGH switch.
The only reason to load a program with this type of memory allocation
is to allow a program data structure to be dynamically extended toward
lower memory addresses. For example, both stacks and heaps can be
implemented in this way. If a program's stack segment is the first
segment in its memory map, the stack can grow downward without
colliding with other program data.
To facilitate addressing in such a segment, LINK provides the
/DSALLOCATE switch. When a program is linked using this switch, all
addresses within DGROUP are relocated in such a way that the last byte
in the group has offset FFFFH. For example, if the program in Figure
20-17 is linked without the /DSALLOCATE and /HIGH switches, the value
of offset DGROUP:DataItem would be 00H; if these switches are used,
the linker adjusts the segment value of DGROUP downward so that the
offset of DataItem within DGROUP becomes FFF0H.
Early versions of Microsoft Pascal (before version 3.30) and Microsoft
FORTRAN (before version 3.30) generated object code that had to be
linked with the /DSALLOCATE switch. For this reason, LINK sets the
/DSALLOCATE switch by default if it processes an object module
containing a COMENT record generated by one of these compilers. (Such
a COMENT record contains the string MS PASCAL or FORTRAN 77. See
PROGRAMMING IN THE MS-DOS ENVIRONMENT: PROGRAMMING TOOLS: Object
Modules.) Apart from this special requirement of certain language
translators, however, the use of /DSALLOCATE and /HIGH should probably
be avoided because of the limitations they place on run-time memory
allocation.
DGROUP GROUP _DATA
_DATA SEGMENT word public 'DATA'
DataItem DB 10h dup (?)
_DATA ENDS
_TEXT SEGMENT byte public 'CODE'
ASSUME cs:_TEXT,ds:DGROUP
mov bx,offset DGROUP:DataItem
_TEXT ENDS
END
Figure 20-17. The value of offset DGROUP:DataItem in this program is
FFF0H if the program is linked with the /DSALLOCATE switch or 00H if
the program is linked without using the switch.
Summary
LINK's characteristic support for segment ordering, for run-time
memory management, and for dynamic overlays has an impact in many
different situations. Programmers who write their own language
translators must bear in mind the special conventions followed by LINK
in support of Microsoft language translators. Application programmers
must be familiar with LINK's capabilities when they use assembly
language or link assembly-language programs with object modules
generated by Microsoft compilers. LINK is a powerful program
development tool and understanding its special capabilities can lead
to more efficient programs.
Richard Wilton
Return to The MS-DOS Encyclopedia: Contents