The MS-DOS Encyclopedia: Section II: Programming in the MS-DOS Environment

Archive of outdated Microsoft articles and reference materials


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