Programmers Guide
Programmers Guide
Microsoft MASM ®
Microsoft Corporation
Microsoft, MS, MS-DOS, XENIX, CodeView, and QuickC are registered trademarks and Microsoft
QuickBasic, QuickPascal, Windows and Windows NT are trademarks of Microsoft Corporation in
the USA and other countries.
Contents
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii
New and Extended Features in MASM 6.1. . . . . . . . . . . . . . . . . . . . . . . . . . . xiii
MASM Features New Since Version 5.1 . . . . . . . . . . . . . . . . . . . . . . . . . . xiv
MASM Features New Since Version 6.0 . . . . . . . . . . . . . . . . . . . . . . . . . . xv
ML and MASM Command Lines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvi
Compatibility with Earlier Versions of MASM . . . . . . . . . . . . . . . . . . . . . xvi
A Word About Instruction Timings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvii
Books for Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xviii
Document Conventions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xix
Getting Assistance and Reporting Problems . . . . . . . . . . . . . . . . . . . . . . . . . . xx
Logical Instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
Shifting and Rotating Bits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Multiplying and Dividing with Shift Instructions . . . . . . . . . . . . . . . . . . . 102
Procedures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Defining Procedures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Passing Arguments on the Stack. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Declaring Parameters with the PROC Directive . . . . . . . . . . . . . . . . . . . . 184
Using Local Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
Creating Local Variables Automatically . . . . . . . . . . . . . . . . . . . . . . . . . . 190
Declaring Procedure Prototypes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
Calling Procedures with INVOKE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
Generating Prologue and Epilogue Code. . . . . . . . . . . . . . . . . . . . . . . . . . 198
MS-DOS Interrupts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
Calling MS-DOS and ROM-BIOS Interrupts . . . . . . . . . . . . . . . . . . . . . . 204
Replacing an Interrupt Routine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
Chapter 8 Sharing Data and Procedures Among Modules and Libraries . . . . . . 211
Selecting Data-Sharing Methods. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Sharing Symbols with Include Files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
Organizing Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
Declaring Symbols Public and External . . . . . . . . . . . . . . . . . . . . . . . . . . 214
Positioning External Declarations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
Using Alternatives to Include Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
PUBLIC and EXTERN. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Other Alternatives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Developing Libraries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Associating Libraries with Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
Using EXTERN with Library Routines . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Appendixes
Appendix A Differences Between MASM 6.1 and 5.1 . . . . . . . . . . . . . . . . . . . . 341
New Features of Version 6.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
The Assembler, Environment, and Utilities . . . . . . . . . . . . . . . . . . . . . . . . 342
Segment Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343
Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
Procedures, Loops, and Jumps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Simplifying Multiple-Module Projects . . . . . . . . . . . . . . . . . . . . . . . . . . . 348
Expanded State Control. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
Glossary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 421
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
Tables
1.1 8086 Family of Processors. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2 The MS-DOS and Windows Operating Systems Compared . . . . . . . . . . . . 4
1.3 Operator Precedence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.1 Attributes of Memory Models . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.1 Indirect Addressing with 16-Bit Registers . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.1 Division Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
5.1 Requirements for String Instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.1 Ranges of Floating-Point Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
6.2 Coprocessor Operand Formats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
6.3 Control-Flag Settings After Comparison or Test . . . . . . . . . . . . . . . . . . . 151
7.1 Conditional Jumps Based on Comparisons of Two Values . . . . . . . . . . . . 167
9.1 MASM Macro Operators. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234
11.1 MS-DOS Internal Stacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
12.1 Naming and Calling Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309
12.2 Register Conventions for Simple Return Values . . . . . . . . . . . . . . . . . . . 317
A.1 Requirements for String Instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
C.1 Options for Generating or Modifying Listing Files . . . . . . . . . . . . . . . . . 398
C.2 Symbols and Abbreviations in Listings. . . . . . . . . . . . . . . . . . . . . . . . . . 400
C.3 Symbols in Timing Column . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401
Introduction
The Microsoft® Macro Assembler Programmer’s Guide provides the information
you need to write and debug assembly-language programs with the Microsoft
Macro Assembler (MASM), version 6.1. This book documents enhanced features
of the language and the programming environment for MASM 6.1.
This Programmer’s Guide is written for experienced programmers who know
assembly language and are familiar with an assembler. The book does not teach the
basics of assembly language; it does explain Microsoft-specific features. If you
want to learn or review the basics of assembly language, refer to “Books for
Further Reading” in this introduction.
This book teaches you how to write efficient code with the new and advanced
features of MASM. Getting Started explains how to set up MASM 6.1.
Environment and Tools introduces the integrated development environment called
the Programmer’s WorkBench (PWB). It also includes a detailed reference to
Microsoft tools and utilities such as Microsoft ® CodeView ®, LINK, and NMAKE.
The Microsoft Macro Assembler Reference provides a full listing of all MASM
instructions, directives, statements, and operators, and it serves as a quick reference
to utility commands.
For more information on these same topics, see the online Microsoft Advisor, which
is a complete reference to Macro Assembler language topics, to the utilities, and to
PWB. You should be able to find most of the information you need in the Microsoft
Advisor.
The assembler has many new macro features that make complex macros clearer and
easier to write:
◆ You can specify default values for macro arguments or mark arguments as
required. And with the VARARG keyword, one parameter can accept a
variable number of arguments.
◆ You can implement loops inside of macros in various ways. For example, the
new WHILE directive expands the statements in a macro body while an
expression is not zero.
◆ You can define macro functions, which return text macros. Several predefined
text macros are also provided for processing strings. Macro operators and other
features related to processing text macros and macro arguments have been
enhanced. For more information on all these macro features, see Chapter 9,
“Using Macros.”
MASM 6.1 includes many other minor new features as well as extensive support
for features of earlier versions of MASM. For a complete list of enhancements,
refer to Appendix A, “Differences between MASM 6.1 and 5.1.” The cross-
references in Appendix A guide you to the chapters where the new features are
described in detail.
Note The name MASM has traditionally referred to the Microsoft Macro
Assembler. It is used in that context throughout this book. However, MASM also
refers to MASM.EXE, which has been replaced by ML.EXE. In MASM 6.1,
MASM.EXE is a small utility that translates command-line options to those
accepted by ML.EXE, and then calls ML.EXE. The distinction between ML.EXE
and MASM.EXE is made whenever necessary. Otherwise, MASM refers to the
assembler and its features.
accomplish the same thing and have the same timings on 80386/486 processors.
But the first instruction is 3 bytes smaller than the second, and so may reach the
processor faster.
• When possible, use the string instructions described in Chapter 5, “Defining and
Using Complex Data Types.”
Document Conventions
The following document conventions are used throughout this manual:
Example of
Convention Description
SAMPLE2.ASM Uppercase letters indicate filenames, segment names, registers,
and terms used at the command level.
.MODEL Boldface type indicates assembly-language directives, instructions,
type specifiers, and predefined macros, as well as keywords in
other programming languages.
placeholder Italic letters indicate placeholders for information you must supply,
such as a filename. Italics are used occasionally for emphasis in
the text.
target This font is used to indicate example programs, user input, and
screen output.
; A semicolon in the first column of an example signals illegal code.
A semicolon also marks a comment.
SHIFT Small capital letters signify names of keys on the keyboard. Notice
that a plus (+) indicates a combination of keys. For example,
CTRL+E means to hold down the CTRL key while pressing the E
key.
[[argument]] Items inside double square brackets are optional.
{register|memory} Braces and a vertical bar indicate a choice between two or more
items. You must choose one of the items unless double square
brackets surround the braces.
Repeating A horizontal ellipsis (...) following an item indicates that more
elements... items having the same form may appear.
Program A vertical ellipsis tells you that part of a program has been
. intentionally omitted.
.
.
Fragment
If your program is very large, reduce it to the smallest possible program that still
produces the problem.
Note the circumstances of the error and notify Microsoft Corporation by following
the instructions in the section “Microsoft Support Services” in the introduction to
Environment and Tools. If you have comments or suggestions regarding any of the
books accompanying this product, please indicate them on the Document Feedback
page at the back of this book and send it to Microsoft.
If you have not yet registered your copy of the Macro Assembler, you should fill out
and return the Registration Card. This enables Microsoft to keep you informed of
updates and other information about the assembler.
C H A P T E R 1
With the development of the Microsoft Macro Assembler (MASM) version 6.1,
you now have more options available to you for approaching a programming task.
This chapter explains the general concepts of programming in assembly language,
beginning with the environment and a review of the components you need to work
in the assembler environment. Even if you are familiar with previous versions of
MASM, you should examine this chapter for information on new terms and
features.
The first section of this chapter reviews available processors and operating systems
and how they work together. The section also discusses segmented architecture and
how it affects a protected-mode operating environment such as Windows.
The second section describes some of the language components of MASM that are
common to most programs, such as reserved words, constant expressions, operators,
and registers. The remainder of this book was written with the assumption that you
understand the information presented in this section.
The last section summarizes the assembly process, from assembling a program
through running it. You can affect this process by the way you develop your code.
Finally, this section explores how you can change the assembly process with the
OPTION directive and conditional assembly.
8086-Based Processors
The 8086 “family” of processors uses segments to control data and code. The later
8086-based processors have larger instruction sets and more memory capacity, but
they still support the same segmented architecture. Knowing the differences
between the various 8086-based processors can help you select the appropriate
target processor for your programs.
The instruction set of the 8086 processor is upwardly compatible with its
successors. To write code that runs on the widest number of machines, select the
8086 instruction set. By using the instruction set of a more advanced processor, you
increase the capabilities and efficiency of your program, but you also reduce the
number of systems on which the program can run.
Table 1.1 lists modes, memory, and segment size of processors on which your
application may need to run. Each processor is discussed in more detail following.
Table 1.1 8086 Family of Processors
Available Addressable Segment
Processor Modes Memory Size
8086/8088 Real 1 megabyte 16 bits
80186/80188 Real 1 megabyte 16 bits
80286 Real and Protected 16 megabytes 16 bits
80386 Real and Protected 4 gigabytes 16 or 32 bits
80486 Real and Protected 4 gigabytes 16 or 32 bits
Processor Modes
Real mode allows only one process to run at a time. The mode gets its name from
the fact that addresses in real mode always correspond to real locations in memory.
The MS-DOS operating system runs in real mode.
Windows 3.1 operates only in protected mode, but runs MS-DOS programs in real
mode or in a simulation of real mode called virtual-86 mode. In protected mode,
more than one process can be active at any one time. The operating system protects
memory belonging to one process from access by another process; hence the name
protected mode.
Protected-mode addresses do not correspond directly to physical memory. Under
protected-mode operating systems, the processor allocates and manages memory
dynamically. Additional privileged instructions initialize protected mode and
control multiple processes. For more information, see “Operating Systems,”
following.
80286
The 80286 processor adds some instructions to control protected mode, and it runs
faster. It also provides protected mode services, allowing the operating system to
run multiple processes at the same time. The 80286 is the minimum for running
Windows 3.1 and 16-bit versions of OS/2 ®.
80386
Unlike its predecessors, the 80386 processor can handle both 16-bit and 32-bit
data. It supports the entire instruction set of the 80286, and adds several new
instructions as well. Software written for the 80286 runs unchanged on the 80386,
but is faster because the chip operates at higher speeds.
The 80386 implements many new hardware-level features, including paged
memory, multiple virtual 8086 processes, addressing of up to 4 gigabytes of
memory, and specialized debugging registers. Thirty-two–bit operating systems
such as Windows NT and OS/2 2.0 can run only on an 80386 or higher processor.
80486
The 80486 processor is an enhanced version of the 80386, with instruction
“pipelining” that executes many instructions two to three times faster. The chip
incorporates both a math coprocessor and an 8K (kilobyte) memory cache. (The
math coprocessor is disabled on a variation of the chip called the 80486SX.) The
80486 includes new instructions and is fully compatible with 80386 software.
Operating Systems
With MASM, you can create programs that run under MS-DOS, Windows, or
Windows NT — or all three, in some cases. For example, ML.EXE can produce
executable files that run in any of the target environments, regardless of the
programmer’s environment. For information on building programs for different
environments, see “Building and Running Programs” in Help for PWB.
MS-DOS and Windows 3.1 provide different processing modes. MS-DOS runs in
the single-process real mode. Windows 3.1 operates in protected mode, allowing
multiple processes to run simultaneously.
Although Windows requires another operating system for loading and file services,
it provides many functions normally associated with an operating system. When an
application requests an MS-DOS service, Windows often provides the service
without invoking MS-DOS. For consistency, this book refers to Windows as an
operating system.
MS-DOS and Windows (in protected mode) differ primarily in system access
methods, size of addressable memory, and segment selection. Table 1.2 summarizes
these differences.
Table 1.2 The MS-DOS and Windows Operating Systems Compared
Available Contents of
Operating System Active Addressable Segment Word
System Access Processes Memory Register Length
MS-DOS and Direct to One 1 megabyte Actual 16 bits
Windows real hardware address
mode and OS call
Windows Operating Multiple 1 megabyte Segment 16 bits
virtual-86 system call selectors
mode
Windows Operating Multiple 16 megabytes Segment 16 bits
protected mode system call selectors
Windows NT Operating Multiple 512 Segment 32 bits
system call megabytes selectors
MS-DOS
In real-mode programming, you can access system functions by calling MS-DOS,
calling the basic input/output system (BIOS), or directly addressing hardware.
Access is through MS-DOS Interrupt 21h.
Windows
As you can see in Table 1.2, protected mode allows for much larger data structures
than real mode, since addressable memory extends to 16 megabytes. In protected
mode, segment registers contain selector values rather than actual segment
addresses. These selectors cannot be calculated by the program; they must be
obtained by calling the operating system. Programs that attempt to calculate
segment values or to address memory directly do not work in protected mode.
Protected mode uses privilege levels to maintain system integrity and security.
Programs cannot access data or code that is in a higher privilege level. Some
instructions that directly access ports or affect interrupts (such as CLI, STI, IN,
and OUT) are available at privilege levels normally used only by systems
programmers.
Windows protected mode provides each application with up to 16 megabytes of
“virtual memory,” even on computers that have less physical memory. The term
virtual memory refers to the operating system’s ability to use a swap area on the
hard disk as an extension of real memory. When a Windows application requires
more memory than is available, Windows writes sections of occupied memory to
the swap area, thus freeing those sections for other use. It then provides the memory
to the application that made the memory request. When the owner of the swapped
data regains control, Windows restores the data from disk to memory, swapping out
other memory if required.
Windows NT
Windows NT uses the so-called “flat model” of 80386/486 processors. This model
places the processor’s entire address space within one 32-bit segment. The section
“Defining Basic Attributes with .MODEL” in Chapter 2 explains how to use the
flat model. In flat model, your program can (in theory) access up to 4 gigabytes of
virtual memory. Since code, data, and stack reside in the same segment, each
segment register can hold the same value, which need never change.
Segmented Architecture
The 8086 family of processors employs a segmented architecture — that is, each
address is represented as a segment and an offset. Segmented addresses affect many
aspects of assembly-language programming, especially addresses and pointers.
Segmented architecture was originally designed to enable a 16-bit processor to
access an address space larger than 64K. (The section “Segmented Addressing,”
later in this chapter, explains how the processor uses both the segment and offset to
create addresses larger than 64K.) MS-DOS is an example of an operating system
that uses segmented architecture on a 16-bit processor.
With the advent of protected-mode processors such as the 80286, segmented
architecture gained a second purpose. Segments can separate different blocks of
code and data to protect them from undesirable interactions. Windows takes
advantage of the protection features of the 16-bit segments on the 80286.
Segmented architecture went through another significant change with the release of
32-bit processors, starting with the 80386. These processors are compatible with
the older 16-bit processors, but allow flat model 32-bit offset values up to 4
gigabytes. Offset values of this magnitude remove the memory limitations of
segmented architecture. The Windows NT operating system uses 32-bit addressing.
Segment Protection
Segmented architecture is an important part of the Windows memory-protection
scheme. In a “multitasking” operating system in which numerous programs can run
simultaneously, programs cannot access the code and data of another process
without permission.
In MS-DOS, the data and code segments are usually allocated adjacent to each
other, as shown in Figure 1.1. In Windows, the data and code segments can be
anywhere in memory. The programmer knows nothing about, and has no control
over, their location. The operating system can even move the segments to a new
memory location or to disk while the program is running.
Real-Mode Protected-Mode
Program Allocation Program Allocation
First
available Somewhere
address in memory
Code Code
Segment Next address Segment
after Code
Segment
Data Somewhere
Segment in memory
Data
Segment
Segmented Addressing
Segmented addressing refers to the internal mechanism that combines a segment
value and an offset value to form a complete memory address. The two parts of an
address are represented as
segment:offset
The segment portion always consists of a 16-bit value. The offset portion is a 16-bit
value in 16-bit mode or a 32-bit value in 32-bit mode.
In real mode, the segment value is a physical address that has an arithmetic
relationship to the offset value. The segment and offset together create a 20-bit
physical address (explained in the next section). Although 20-bit addresses can
access up to 1 megabyte of memory, the BIOS and operating system on
International Standard Architecture (IBM PC/AT and compatible) computers use
part of this memory, leaving the remainder available for programs.
Segment Arithmetic
Manipulating segment and offset addresses directly in real-mode programming is
called “segment arithmetic.” Programs that perform segment arithmetic are not
portable to protected-mode operating systems, in which addresses do not correspond
to a known segment and offset.
To perform segment arithmetic successfully, it helps to understand how the
processor combines a 16-bit segment and a 16-bit offset to form a 20-bit linear
address. In effect, the segment selects a 64K region of memory, and the offset
selects the byte within that region. Here’s how it works:
1. The processor shifts the segment address to the left by four binary places,
producing a 20-bit address ending in four zeros. This operation has the effect of
multiplying the segment address by 16.
2. The processor adds this 20-bit segment address to the 16-bit offset address. The
offset address is not shifted.
3. The processor uses the resulting 20-bit address, called the “physical address,” to
access an actual location in the 1-megabyte address space.
15 0
5 3 C 2 16-bit segment register...
15 0
5 3 C 2 0
...shifted left 4 bits
15 0
1 0 7 A ...plus 16-bit offset
19 0
5 4 C 9 A
...equals 20-bit physical address
Reserved Words
A reserved word has a special meaning fixed by the language. You can use it only
under certain conditions. Reserved words in MASM include:
◆ Instructions, which correspond to operations the processor can execute.
◆ Directives, which give commands to the assembler.
◆ Attributes, which provide a value for a field, such as segment alignment.
◆ Operators, which are used in expressions.
◆ Predefined symbols, which return information to your program.
MASM reserved words are not case sensitive except for predefined symbols (see
“Predefined Symbols,” later in this chapter).
The assembler generates an error if you use a reserved word as a variable, code
label, or other identifier within your source code. However, if you need to use a
reserved word for another purpose, the OPTION NOKEYWORD directive can
selectively disable a word’s status as a reserved word.
For example, to remove the STR instruction, the MASK operator, and the NAME
directive from the set of words MASM recognizes as reserved, use this statement in
the code segment of your program before the first reference to STR, MASK, or
NAME:
OPTION NOKEYWORD:<STR MASK NAME>
The section “Using the OPTION Directive,” later in this chapter, discusses the
OPTION directive. Appendix D provides a complete list of MASM reserved
words.
With the /Zm command-line option or OPTION M510 in effect, MASM does not
reserve any operators or instructions that do not apply to the current CPU mode. For
example, you can use the symbol ENTER when assembling under the default CPU
mode but not under .286 mode, since the 80186/486 processors recognize ENTER as
an instruction. The USE32, FLAT, FAR32, and NEAR32 segment types and the
80386/486 register names are not keywords with processors other than the
80386/486.
Identifiers
An identifier is a name that you invent and attach to a definition. Identifiers can be
symbols representing variables, constants, procedure names, code labels, segment
names, and user-defined data types such as structures, unions, records, and types
defined with TYPEDEF. Identifiers longer than 247 characters generate an error.
Certain restrictions limit the names you can use for identifiers. Follow these rules to
define a name for an identifier:
◆ The first character of the identifier can be an alphabetic character (A–Z) or any
of these four characters: @ _ $ ?
◆ The other characters in the identifier can be any of the characters listed above or
a decimal digit (0–9).
Avoid starting an identifier with the at sign (@), because MASM 6.1 predefines
some special symbols starting with @ (see “Predefined Symbols,” following).
Beginning an identifier with @ may also cause conflicts with future versions of the
Macro Assembler.
The symbol — and thus the identifier — is visible as long as it remains within scope.
(For more information about visibility and scope, see “Sharing Symbols with
Include Files” in Chapter 8.)
Predefined Symbols
The assembler includes a number of predefined symbols (also called predefined
equates). You can use these symbol names at any point in your code to represent the
equate value. For example, the predefined equate @FileName represents the base
name of the current file. If the current source file is TASK.ASM, the value of
@FileName is TASK. The MASM predefined symbols are listed according to the
kinds of information they provide. Case is important only if the /Cp option is used.
(For additional details, see Help on ML command-line options.)
The predefined symbols for segment information include:
Symbol Description
@code Returns the name of the code segment.
@CodeSize Returns an integer representing the default code distance.
@CurSeg Returns the name of the current segment.
@data Expands to DGROUP.
@DataSize Returns an integer representing the default data distance.
@fardata Returns the name of the segment defined by the .FARDATA directive.
@fardata? Returns the name of the segment defined by the .FARDATA? directive.
@Model Returns the selected memory model.
@stack Expands to DGROUP for near stacks or STACK for far stacks. (See
“Creating a Stack” in Chapter 2.)
@WordSize Provides the size attribute of the current segment.
the numbers 25 and 0B3h are integer constants. The h appended to 0B3 is a radix
specifier. The specifiers are:
◆ y for binary (or b if the default radix is not hexadecimal)
◆ o or q for octal
◆ t for decimal (or d if the default radix is not hexadecimal)
◆ h for hexadecimal
Radix specifiers can be either uppercase or lowercase letters; sample code in this
book is in lowercase. If you do not specify a radix, the assembler interprets the
integer according to the current radix. The default radix is decimal, but you can
change the default with the .RADIX directive.
Hexadecimal numbers must always start with a decimal digit (0–9). If necessary,
add a leading zero to distinguish between symbols and hexadecimal numbers that
start with a letter. For example, MASM interprets ABCh as an identifier. The
hexadecimal digits A through F can be either uppercase or lowercase letters.
Sample code in this book is in uppercase letters.
Constant expressions contain integer constants and (optionally) operators such as
shift, logical, and arithmetic operators. The assembler evaluates constant
expressions at assembly time. (In addition to constants, expressions can contain
labels, types, registers, and their attributes.) Constant expressions do not change
value during program execution.
then use the appropriate symbol in your program rather than the number. Using
symbolic constants instead of undescriptive numbers makes your code more
readable and easier to maintain. The assembler does not allocate data storage when
you use either EQU or =. It simply replaces each occurrence of the symbol with the
value of the expression.
The directives EQU and = have slightly different purposes. Integers defined with
the = directive can be redefined with another value in your source code, but those
defined with EQU cannot. Once you’ve defined a symbolic constant with the EQU
directive, attempting to redefine it generates an error. The syntax is:
symbol EQU expression
The symbol is a unique name of your choice, except for words reserved by MASM.
The expression can be an integer, a constant expression, a one- or two-character
string constant (four-character on the 80386/486), or an expression that evaluates
to an address. Symbolic constants let you change a constant value used throughout
your source code by merely altering expression in the definition. This removes the
potential for error and saves you the inconvenience of having to find and replace
each occurrence of the constant in your program.
The following example shows the correct use of EQU to define symbolic integers.
column EQU 80 ; Constant - 80
row EQU 25 ; Constant - 25
screen EQU column * row ; Constant - 2000
line EQU row ; Constant - 25
.DATA
.CODE
.
.
.
mov cx, column
mov bx, line
The value of a symbol defined with the = directive can be different at different
places in the source code. However, a constant value is assigned during assembly
for each use, and that value does not change at run time.
The syntax for the = directive is:
symbol = expression
Size of Constants
The default word size for MASM 6.1 expressions is 32 bits. This behavior can be
modified using OPTION EXPR16 or OPTION M510. Both of these options set
the expression word size to 16 bits, but OPTION M510 affects other assembler
behavior as well (see Appendix A).
It is illegal to change the expression word size once it has been set with OPTION
M510, OPTION EXPR16, or OPTION EXPR32. However, you can repeat the
same directive in your source code as often as you wish. You can place the same
directive in every include file, for example.
Operators
Operators are used in expressions. The value of the expression is determined at
assembly time and does not change when the program runs.
Operators should not be confused with processor instructions. The reserved
word ADD is an instruction; the plus sign (+) is an operator. For example,
Amount+2 illustrates a valid use of the plus operator (+). It tells the assembler to
add 2 to the constant value Amount, which might be a value or an address. Contrast
this operation, which occurs at assembly time, with the processor’s ADD
instruction. ADD tells the processor at run time to add two numbers and store the
result.
The assembler evaluates expressions that contain more than one operator according
to the following rules:
◆ Operations in parentheses are performed before adjacent operations.
◆ Binary operations of highest precedence are performed first.
◆ Operations of equal precedence are performed from left to right.
◆ Unary operations of equal precedence are performed right to left.
Table 1.3 lists the order of precedence for all operators. Operators on the same line
have equal precedence.
Table 1.3 Operator Precedence
Precedence Operators
1 ( ), [ ]
2 LENGTH, SIZE, WIDTH, MASK, LENGTHOF, SIZEOF
3 . (structure-field-name operator)
4 : (segment-override operator), PTR
5 LROFFSET, OFFSET, SEG, THIS, TYPE
6 HIGH, HIGHWORD, LOW, LOWWORD
7 + ,– (unary)
8 *, /, MOD, SHL, SHR
9 +, – (binary)
10 EQ, NE, LT, LE, GT, GE
11 NOT
12 AND
13 OR, XOR
14 OPATTR, SHORT, .TYPE
Data Types
A “data type” describes a set of values. A variable of a given type can have any of
a set of values within the range specified for that type.
The intrinsic types for MASM 6.1 are BYTE, SBYTE, WORD, SWORD,
DWORD, SDWORD, FWORD, QWORD, and TBYTE. These types define
integers and binary coded decimals (BCDs), as discussed in Chapter 6. The signed
data types SBYTE, SWORD, and SDWORD work in conjunction with directives
such as INVOKE (for calling procedures) and .IF (introduced in Chapter 7). The
REAL4, REAL8, and REAL10 directives define floating-point types. (See
Chapter 6.)
Versions of MASM prior to 6.0 had separate directives for types and initializers.
For example, BYTE is a type and DB is the corresponding initializer. The
distinction does not apply in MASM 6.1. You can use any type (intrinsic or user-
defined) as an initializer.
MASM does not have specific types for arrays and strings. However, you can treat
a sequence of data units as arrays, and character or byte sequences as strings. (See
“Arrays and Strings” in Chapter 5.)
Types can also have attributes such as langtype and distance (NEAR and FAR).
For information on these attributes, see “Declaring Parameters with the PROC
Directive” in Chapter 7.
You can also define your own types with STRUCT, UNION, and RECORD. The
types have fields that contain string or numeric data, or records that contain bits.
These data types are similar to the user-defined data types in high-level languages
such as C, Pascal, and FORTRAN. (See Chapter 5, “Defining and Using Complex
Data Types.”)
You can define new types, including pointer types, with the TYPEDEF directive.
TYPEDEF assigns a qualifiedtype (explained in the following) to a typename of
your choice. This lets you build new types with descriptive names of your choosing,
making your programs more readable. For example, the following statement makes
the symbol CHAR a synonym for the intrinsic type BYTE:
CHAR TYPEDEF BYTE
The typename CHAR in the first line becomes a qualifiedtype in the second line. Use
of the TYPEDEF directive to define pointers is explained in “Accessing Data with
Pointers and Addresses” in Chapter 3.
Since distance and qualifiedtype are optional syntax elements, you can use
variables of type PTR or FAR PTR. You can also define procedure prototypes
Registers
The 8086 family of processors have the same base set of 16-bit registers. Each
processor can treat certain registers as two separate 8-bit registers. The 80386/486
processors have extended 32-bit registers. To maintain compatibility with their
predecessors, 80386/486 processors can access their registers as 16-bit or, where
appropriate, as 8-bit values.
Figure 1.3 shows the registers common to all the 8086-based processors. Each
register has its own special uses and limitations.
General-Purpose Registers
15 7 0
SS Stack Segment
ES Extra Segment
80386/486 Only
The 80386/486 processors use the same 8-bit and 16-bit registers used by the rest
of the 8086 family. All of these registers can be further extended to 32 bits, except
segment registers, which always occupy 16 bits. The extended register names begin
with the letter “E.” For example, the 32-bit extension of AX is EAX. The
80386/486 processors have two additional segment registers, FS and GS. Figure
1.4 shows the extended registers of the 80386/486.
General-Purpose Registers
31 23 15 7 0
EAX Accumulator
AH AX AL
EDX Data
DH DX DL
ECX Count
CH CX CL
EBX Base
BH BX BL
EBP BP Base Pointer
ESI SI Source Index
EDI DI Destination Index
ESP SP Stack Pointer
ES Extra Segment
FS Extra Segment
GS Extra Segment
Segment Registers
At run time, all addresses are relative to one of four segment registers: CS, DS, SS,
or ES. (The 80386/486 processors add two more: FS and GS.) These registers, their
segments, and their purposes include:
Register and Segment Purpose
CS (Code Segment) Contains processor instructions and their immediate
operands.
DS (Data Segment) Normally contains data allocated by the program.
SS (Stack Segment) Contains the program stack for use by PUSH, POP, CALL,
and RET.
General-Purpose Registers
The AX, DX, CX, BX, BP, DI, and SI registers are 16-bit general-purpose
registers, used for temporary data storage. Since the processor accesses registers
more quickly than it accesses memory, you can make your programs run faster by
keeping the most-frequently used data in registers.
The 8086-based processors do not perform memory-to-memory operations. For
example, the processor cannot directly copy a variable from one location in memory
to another. You must first copy from memory to a register, then from the register to
the new memory location. Similarly, to add two variables in memory, you must first
copy one variable to a register, then add the contents of the register to the other
variable in memory.
The processor can access four of the general registers — AX, DX, CX, and BX —
either as two 8-bit registers or as a single 16-bit register. The AH, DH, CH, and
BH registers represent the high-order 8 bits of the corresponding registers.
Similarly, AL, DL, CL, and BL represent the low-order 8 bits of the registers.
The 80386/486 processors can extend all the general registers to 32 bits, though as
Figure 1.4 shows, you cannot treat the upper 16 bits as a separate register as you
can the lower 16 bits. To use EAX as an example, you can directly reference the
low byte as AL, the next lowest byte as AH, and the low word as AX. To access
the high word of EAX, however, you must first shift the upper 16 bits into the lower
16 bits.
Special-Purpose Registers
The 8086 family of processors has two additional registers, SP and IP, whose
values are changed automatically by the processor.
SP (Stack Pointer)
The SP register points to the current location within the stack segment. Pushing a
value onto the stack decreases the value of SP by two; popping from the stack
increases the value of SP by two. Thirty-two–bit operands on 80386/486 processors
increase or decrease SP by four instead of two. The CALL and INT instructions
store the return address on the stack and reduce SP accordingly. Return instructions
retrieve the stored address from the stack and reset SP to its value before the call.
SP can also be adjusted with instructions such as ADD. The program stack is
described in detail in Chapter 3.
IP (Instruction Pointer)
The IP register always contains the address of the next instruction to be executed.
You cannot directly access or change the instruction pointer. However, instructions
that control program flow (such as calls, jumps, loops, and interrupts) automatically
change the instruction pointer.
Flags Register
The 16 bits in the flags register control the execution of certain instructions and
reflect the current status of the processor. In 80386/486 processors, the flags
register is extended to 32 bits. Some bits are undefined, so there are actually 9 flags
for real mode, 11 flags (including a 2-bit flag) for 80286 protected mode, 13 for the
80386, and 14 for the 80486. The extended flags register of the 80386/486 is
sometimes called “Eflags.”
Figure 1.5 shows the bits of the 32-bit flags register for the 80386/486. Earlier
8086-family processors use only the lower word. The unmarked bits are reserved
for processor use, and should not be modified.
Alignment Check
Virtual 8086 Mode
Resume
Nested Task
I/O Protection Level
Overflow
Direction
Interrupt Enable
Trap
Sign
Zero
Auxiliary Carry
Parity
Carry
31 23 15 7 0
A VR N IOP O D I T S Z A P C
In the following descriptions and throughout this book, “set” means a bit value of 1,
and “cleared” means the bit value is 0. The nine flags common to all 8086-family
processors, starting with the low-order flags, include:
Flag Description
Carry Set if an operation generates a carry to or a borrow from a destination
operand.
Parity Set if the low-order bits of the result of an operation contain an even
number of set bits.
Auxiliary Carry Set if an operation generates a carry to or a borrow from the low-order
4 bits of an operand. This flag is used for binary coded decimal
(BCD) arithmetic.
Zero Set if the result of an operation is 0.
Sign Equal to the high-order bit of the result of an operation (0 is positive,
1 is negative).
Trap If set, the processor generates a single-step interrupt after each
instruction. A debugging program can use this feature to execute a
program one instruction at a time.
Interrupt Enable If set, interrupts are recognized and acted on as they are received. The
bit can be cleared to turn off interrupt processing temporarily.
Direction If set, string operations process down from high addresses to low
addresses. If cleared, string operations process up from low addresses
to high addresses.
Overflow Set if the result of an operation is too large or small to fit in the
destination operand.
Although all flags serve a purpose, most programs require only the carry, zero,
sign, and direction flags.
Statements
Statements are the line-by-line components of source files. Each MASM statement
specifies an instruction or directive for the assembler. Statements have up to four
fields, as shown here:
[[name:]] [[operation]] [[operands]] [[;comment]]
The following list explains each field:
Field Purpose
name Labels the statement, so that instructions elsewhere in the program can
refer to the statement by name. The name field can label a variable, type,
segment, or code location.
operation Defines the action of the statement. This field contains either an instruction
or an assembler directive.
operands Lists one or more items on which the instruction or directive operates.
comment Provides a comment for the programmer. Comments are for documentation
only; they are ignored by the assembler.
Here, mainlp is the label, mov is the operation, and ax and 7 are the operands,
separated by a comma. The comment follows the semicolon.
All fields are optional, although certain directives and instructions require an entry
in the name or operand field. Some instructions and directives place restrictions on
the choice of operands. By default, MASM is not case sensitive.
Each field (except the comment field) must be separated from other fields by white-
space characters (spaces or tabs). MASM also requires code labels to be followed
by a colon, operands to be separated by commas, and comments to be preceded by a
semicolon.
A logical line can contain up to 512 characters and occupy one or more physical
lines. To extend a logical line into two or more physical lines, put the backslash
character (\) as the last non-whitespace character before the comment or end of the
line. You can place a comment after the backslash as shown in this example:
.IF (x > 0) \ ; X must be positive
&& (ax > x) \ ; Result from function must be > x
&& (cx == 0) ; Check loop counter, too
mov dx, 20h
.ENDIF
Multiline comments can also be specified with the COMMENT directive. The
assembler ignores all text and code between the delimiters or on the same line as
the delimiters. This example illustrates the use of COMMENT.
COMMENT ^ The assembler
ignores this text
^ mov ax, 1 and this code
Once you have written your assembly-language program, MASM provides several
options for assembling it. The OPTION directive has several different arguments
that let you control the way MASM assembles your programs.
Conditional assembly allows you to create one source file that can generate a
variety of programs, depending on the status of various conditional-assembly
statements.
Assembling
The ML.EXE program does two things to create an executable program. First, it
assembles the source code into an intermediate object file. Second, it calls the
linker, LINK.EXE, which links the object files and libraries into an executable
program.
At assembly time, the assembler:
◆ Evaluates conditional-assembly directives, assembling if the conditions are true.
◆ Expands macros and macro functions.
◆ Evaluates constant expressions such as MYFLAG AND 80H, substituting the
calculated value for the expression.
◆ Encodes instructions and nonaddress operands. For example, mov cx, 13 can
be encoded at assembly time because the instruction does not access memory.
◆ Saves memory offsets as offsets from their segments.
◆ Places segments and segment attributes in the object file.
◆ Saves placeholders for offsets and segments (relocatable addresses).
◆ Outputs a listing if requested.
◆ Passes messages (such as INCLUDELIB and .DOSSEG) directly to the linker.
Linking
Once your source code is assembled, the resulting object file is passed to the linker.
At this point, the linker may combine several object files into an executable
program. The linker:
◆ Combines segments according to the instructions in the object files, rearranging
the positions of segments that share the same class or group.
◆ Fills in placeholders for offsets (relocatable addresses).
◆ Writes relocations for segments into the header of .EXE files (but not .COM
files).
◆ Writes the result as an executable program file.
Loading
After loading the executable file into memory, the operating system:
◆ Creates the program segment prefix (PSP) header in memory.
◆ Allocates memory for the program, based on the values in the PSP.
◆ Loads the program.
◆ Calculates the correct values for absolute addresses from the relocation table.
◆ Loads the segment registers SS, CS, DS, and ES with values that point to the
proper areas of memory.
For information about segment registers, the instruction pointer (IP), and the stack
pointer (SP), see “Registers” earlier in this chapter. For more information on the
PSP see Help or an MS-DOS reference.
Running
To run your program, MS-DOS jumps to the program’s first instruction. Some
program operations, such as resolving indirect memory operands, cannot be handled
until the program runs. For a description of indirect references, see “Indirect
Operands” in Chapter 7.
For example, you may have MASM code in which the first character of a variable,
macro, structure, or field name is a dot (.). Since a leading dot causes MASM 6.1 to
generate an error, you can use this statement in your program:
OPTION DOTNAME
This enables the use of the dot for the first character.
Changes made with OPTION override any corresponding command-line option.
For example, suppose you compile a module with this command line (which enables
M510 compatibility):
ML /Zm TEST.ASM
The assembler disables M510 compatibility options for all code following this
statement:
OPTION NOM510
The following lists explain each of the arguments for the OPTION directive.
Where appropriate, an underline identifies the default argument. If you wish to
place more than one OPTION statement on a line, separate them by commas.
Options for M510 compatibility include:
Argument Description
CASEMAP: maptype CASEMAP:NONE (or /Cx) causes internal
symbol recognition to be case sensitive and
causes the case of identifiers in the .OBJ file to
be the same as specified in the EXTERNDEF,
PUBLIC, or COMM statement. The default is
CASEMAP:NOTPUBLIC (or /Cp). It specifies
case insensitivity for internal symbol recognition
and the same behavior as CASEMAP:NONE for
case of identifiers in .OBJ files. CASEMAP:ALL
(/Cu) specifies case insensitivity for identifiers
and converts all identifier names to uppercase.
DOTNAME | NODOTNAME Enables the use of the dot (.) as the leading
character in variable, macro, structure, union,
and member names.
M510 | NOM510 Sets all features to be compatible with MASM
version 5.1, disabling the SCOPED argument
and enabling OLDMACROS, DOTNAME, and,
OLDSTRUCTS. OPTION M510 conditionally
sets other arguments for the OPTION directive.
For more information on using OPTION M510,
see Appendix A.
Argument Description
OLDMACROS | NOOLDMACROS Enables the version 5.1 treatment of macros.
MASM 6.1 treats macros differently.
OLDSTRUCTS | NOOLDSTRUCTS Enables compatibility with MASM 5.1 for
treatment of structure members. See Chapter 5
for information on structures.
SCOPED | NOSCOPED Guarantees that all labels inside procedures are
local to the procedure when SCOPED (the
default) is enabled.
SETIF2: TRUE | FALSE If TRUE, .ERR2 statements and IF2 and
ELSEIF2 conditional blocks are evaluated on
every pass. If FALSE, they are not evaluated. If
SETIF2 is not specified (or implied), .ERR2,
IF2, and ELSEIF2 expressions cause an error.
Both the /Zm command-line argument and
OPTION M510 imply SETIF2:TRUE.
Argument Description
EMULATOR | NOEMULATOR Controls the generation of floating-point
instructions.The NOEMULATOR option
generates the coprocessor instructions directly.
The EMULATOR option generates instructions
with special fixup records for the linker so that
the Microsoft floating-point emulator, supplied
with other Microsoft languages, can be used. It
produces the same result as setting the /Fpi
command-line option. You can set this option
only once per module.
LJMP | NOLJMP Enables automatic conditional-jump lengthening.
For information about conditional-jump
lengthening, see Chapter 7.
NOKEYWORD:<keywordlist> Disables the specified reserved words. For an
example of the syntax for this argument, see
“Reserved Words” in this chapter.
NOSIGNEXTEND Overrides the default sign-extended opcodes for
the AND, OR, and XOR instructions and
generates the larger non-sign-extended forms of
these instructions. Provided for compatibility
with NEC V25 and NEC V35 controllers.
OFFSET: offsettype Determines the result of OFFSET operator
fixups. SEGMENT sets the defaults for fixups to
be segment-relative (compatible with MASM
5.1). GROUP, the default, generates fixups
relative to the group (if the label is in a group).
FLAT causes fixups to be relative to a flat frame.
(The .386 mode must be enabled to use FLAT.)
See Appendix A.
READONLY | NOREADONLY Enables checking for instructions that modify
code segments, thereby guaranteeing that read-
only code segments are not modified. Same as
the /p command-line option of MASM 5.1,
except that it affects only segments with at least
one assembly instruction, not all segments. The
argument is useful for protected mode programs,
where code segments must remain read-only.
SEGMENT: segSize Allows global default segment size to be set.
Also determines the default address size for
external symbols defined outside any segment.
The segSize can be USE16, USE32, or FLAT.
Conditional Directives
MASM 6.1 provides conditional-assembly directives and conditional-error
directives. Conditional-assembly directives let you test for a specified condition and
assemble a block of statements if the condition is true. Conditional-error directives
allow you to test for a specified condition and generate an assembly error if the
condition is true.
Both kinds of conditional directives test assembly-time conditions, not run-time
conditions. You can test only expressions that evaluate to constants during
assembly. For a list of the predefined symbols often used in conditional assembly,
see “Predefined Symbols,” earlier in this chapter.
Conditional-Assembly Directives
The IF and ENDIF directives enclose the conditional statements. The optional
ELSEIF and ELSE blocks follow the IF directive. There are many forms of the IF
and ELSE directives. Help provides a complete list.
The following statements show the syntax for the IF directives. The syntax for other
condition-assembly directives follow the same form.
IF expression1
ifstatements
[[ELSEIF expression2
elseifstatements]]
[[ELSE
elsestatements]]
ENDIF
The statements within an IF block can be any valid instructions, including other
conditional blocks, which in turn can contain any number of ELSEIF blocks.
ENDIF ends the block.
MASM assembles the statements following the IF directive only if the
corresponding condition is true. If the condition is not true and the block contains an
ELSEIF directive, the assembler checks to see if the corresponding condition is
true. If so, it assembles the statements following the ELSEIF directive. If no IF or
ELSEIF conditions are satisfied, the assembler processes only the statements
following the ELSE directive.
For example, you may want to assemble a line of code only if your program defines
a particular variable. In this example,
IFDEF buffer
buff BYTE buffer DUP(?)
ENDIF
the assembler allocates buff only if buffer has been previously defined.
MASM 6.1 provides the directives IF1, IF2, ELSEIF1, and ELSIF2 to grant
assembly only on pass one or pass two. To use these directives, you must either
enable 5.1 compatibility (with the /Zm command-line switch or OPTION M510)
or set OPTION SETIF2:TRUE, as described in the previous section.
The following list summarizes the conditional-assembly directives:
The Directive Grants Assembly If
IF expression expression is true (nonzero)
IFE expression expression is false (zero)
IFDEF name name has been previously defined
IFNDEF name name has not been previously defined
IFB argument* argument is blank
IFNB argument* argument is not blank
IFIDN[I] arg1, arg2* arg1 equals arg2
IFDIF[I] arg1, arg2* arg1 does not equal arg2
The optional I suffix (IFIDNI and IFDIFI) makes comparisons
insensitive to differences in case.
* Used only in macros.
Conditional-Error Directives
You can use conditional-error directives to debug programs and check for
assembly-time errors. By inserting a conditional-error directive at a key point in
your code, you can test assembly-time conditions at that point. You can also use
conditional-error directives to test for boundary conditions in macros.
Like other severe errors, those generated by conditional-error directives cause the
assembler to return a nonzero exit code. If MASM encounters a severe error during
assembly, it does not generate the object module.
For example, the .ERRNDEF directive produces an error if the program has not
defined a given label. In the following example, .ERRNDEF makes sure a label
called publevel actually exists.
.ERRNDEF publevel
IF publevel LE 2
PUBLIC var1, var2
ELSE
PUBLIC var1, var2, var3
ENDIF
The conditional-error directives use the syntax given in the previous section. The
following list summarizes the conditional-error directives. Note their close
correspondence with the previous list of conditional-assembly directives.
Two special conditional-error directives, .ERR1 and .ERR2, generate an error only
on pass one or pass two. To use these directives, you must either enable 5.1
compatibility (with the /Zm command-line switch or OPTION M510) or set
OPTION SETIF2:TRUE, as described in the previous section.
C H A P T E R 2
Organizing Segments
As you read this chapter, the distinction between the two definitions will become
clear. The adjectives “physical” and “logical” are not often used when speaking of
segments. The beginning programmer is left to infer from context which definition
applies. Fortunately, this is not difficult, and a distinction is often not required.
This chapter begins with a close look at physical memory segments. This lays the
foundation for understanding logical segments, which form the subject of most of
the following sections.
The section “Using Simplified Segment Directives” explains how to begin, end, and
organize segments. It also explains how to access far data and code with simplified
segment directives.
The next section, “Using Full Segment Definitions,” describes how to order,
combine, and divide segments, and how to use the SEGMENT directive to define
full segments. It also explains how to create a segment group so that you can use
one segment address to access all the data.
Most of the information in this chapter also applies to writing modules to be called
from other programs. Exceptions are noted when they apply. For more information
about multiple-module programming, see Chapter 8, “Sharing Data and Procedures
Among Modules and Libraries.”
Logical Segments
Logical segments contain the three components of a program: code, data, and stack.
MASM organizes the three parts for you so they occupy physical segments of
memory. The segment registers CS, DS, and SS contain the addresses of the
physical memory segments where the logical segments reside.
You can define segments in two ways: with simplified segment directives and with
full segment definitions. You can also use both kinds of segment definitions in the
same program.
Simplified segment directives hide many of the details of segment definition and
assume the same conventions used by Microsoft high-level languages. (See the
following section, “Using Simplified Segment Directives.”) The simplified segment
directives generate necessary code, specify segment attributes, and arrange segment
order.
Full segment definitions require more complex syntax but provide more complete
control over how the assembler generates segments. (See “Using Full Segment
Definitions” later in this chapter.) If you use full segment definitions, you must
write code to handle all the tasks performed automatically by the simplified segment
directives.
The .DATA and .CODE statements do not require any separate statements to
define the end of a segment. They close the preceding segment and then open a new
segment. The .STACK directive opens and closes the stack segment but does not
close the current segment. The END statement closes the last segment and marks
the end of the source code. It must be at the end of every module.
You can use no more than one reserved word from each field. The following
examples show how you can combine various fields:
.MODEL small ; Small memory model
.MODEL large, c, farstack ; Large memory model,
; C conventions,
; separate stack
.MODEL medium, pascal ; Medium memory model,
; Pascal conventions,
; near stack (default)
When writing assembler modules for a high-level language, you should use the
same memory model as the calling language. Choose the smallest memory model
available that can contain your data and code, since near references operate more
efficiently than far references.
The predefined symbol @Model returns the memory model, encoding memory
models as integers 1 through 7. For more information on predefined symbols, see
“Predefined Symbols” in Chapter 1. For an example of how to use them, see Help.
The seven memory models supported by MASM 6.1 fall into three groups,
described in the following paragraphs.
Tiny Model
Tiny-model programs run only under MS-DOS. Tiny model places all data and
code in a single segment. Therefore, the total program file size can occupy no more
than 64K. The default is near for code and static data items; you cannot override
this default. However, you can allocate far data dynamically at run time using MS-
DOS memory allocation services.
Tiny model produces MS-DOS .COM files. Specifying .MODEL tiny
automatically sends the /TINY argument to the linker. Therefore, the /AT argument
is not necessary with .MODEL tiny. However, /AT does not insert a .MODEL
directive. It only verifies that there are no base or pointer fixups, and sends /TINY
to the linker.
Flat Model
The flat memory model is a nonsegmented configuration available in 32-bit
operating systems. It is similar to tiny model in that all code and data go in a single
32-bit segment.
To write a flat model program, specify the .386 or .486 directive before .MODEL
FLAT. All data and code (including system resources) are in a single 32-bit
segment. The operating system automatically initializes segment registers at load
time; you need to modify them only when mixing 16-bit and 32-bit segments in a
single application. CS, DS, ES, and SS all occupy the supergroup FLAT.
Addresses and pointers passed to system services are always 32-bit near addresses
and pointers.
and dynamic-link libraries (discussed in Chapters 10 and 11) when you cannot
assume that the caller’s stack is near. You can use the predefined symbol @Stack
to determine if the stack location is DGROUP (for near stacks) or STACK (for far
stacks).
Creating a Stack
The stack is the section of memory used for pushing or popping registers and storing
the return address when a subroutine is called. The stack often holds temporary and
local variables.
If your main module is written in a high-level language, that language handles the
details of creating a stack. Use the .STACK directive only when you write a main
module in assembly language.
The .STACK directive creates a stack segment. By default, the assembler allocates
1K of memory for the stack. This size is sufficient for most small programs.
To create a stack of a size other than the default size, give .STACK a single
numeric argument indicating stack size in bytes:
.STACK 2048 ; Use 2K stack
For a description of how stack memory is used with procedure calls and local
variables, see Chapter 7, “Controlling Program Flow.”
For more information on far data, see “Near and Far Addresses” in Chapter 3.
For far code segments, the assembler names each code segment
MODNAME_TEXT, in which MODNAME is the name of the module. With near
code, the assembler names every code segment _TEXT, causing the linker to
concatenate these segments into one. You can override the default name by
providing an argument after .CODE. (For a complete list of segment names
generated by MASM, see Appendix E, “Default Segment Names.”)
With far code, a single module can contain multiple code segments. The .CODE
directive takes an optional text argument that names the segment. For instance, the
following example creates two distinct code segments, FIRST_TEXT and
SECOND_TEXT.
.CODE FIRST
.
. ; First set of instructions here
.
.CODE SECOND
.
. ; Second set of instructions here
.
Whenever the processor executes a far call or jump, it loads CS with the new
segment address. No special action is necessary other than making sure that you use
far calls and jumps. See “Near and Far Addresses” in Chapter 3.
Note The assembler always assumes that the CS register contains the address of
the current code segment or group.
To start a program, place the .STARTUP directive where you want execution to
begin. Usually, this location immediately follows the .CODE directive:
.CODE
.STARTUP
.
. ; Place executable code here
.
.EXIT
END
Note that .EXIT generates executable code, while END does not. The END
directive informs the assembler that it has reached the end of the module. All
modules must end with the END directive whether you use simplified or full
segments.
If you do not use .STARTUP, you must give the starting address as an argument to
the END directive. For example, the following fragment shows how to identify a
program’s starting instruction with the label start:
.CODE
start:
.
. ; Place executable code here
.
END start
Only the END directive for the module with the starting instruction should have an
argument. When .STARTUP is present, the assembler ignores any argument to
END.
For the default NEARSTACK attribute, .STARTUP points DS to DGROUP and
sets SS:SP relative to DGROUP, generating the following code:
@Startup:
mov dx, DGROUP
mov ds, dx
mov bx, ss
sub bx, dx
shl bx, 1 ; If .286 or higher, this is
shl bx, 1 ; shortened to shl bx, 4
shl bx, 1
shl bx, 1
cli ; Not necessary in .286 or higher
mov ss, dx
add sp, bx
sti ; Not necessary in .286 or higher
.
.
.
END @Startup
An MS-DOS program with the FARSTACK attribute does not need to adjust
SS:SP, so .STARTUP just initializes DS, like this:
@Startup:
mov dx, DGROUP
mov ds, dx
.
.
.
END @Startup
When the program terminates, you can return an exit code to the operating system.
Applications that check exit codes usually assume that an exit code of 0 means no
problem occurred, and that an exit code of 1 means an error terminated the
program. The .EXIT directive accepts a 1-byte exit code as its optional argument:
.EXIT 1 ; Return exit code 1
.EXIT generates the following code that returns control to MS-DOS, thus
terminating the program. The return value, which can be a constant, memory
reference, or 1-byte register, goes into AL:
mov al, value
mov ah, 04Ch
int 21h
If your program does not specify a return value, .EXIT returns whatever value
happens to be in AL.
Types can be specified in any order. You can specify only one attribute from each
of these fields; for example, you cannot have two different align types.
You can close a segment and reopen it later with another SEGMENT directive.
When you reopen a segment, you need only give the segment name. You cannot
change the attributes of a segment once you have defined it.
Note The PAGE align type and the PUBLIC combine type are distinct from the
PAGE and PUBLIC directives. The assembler distinguishes them by means of
context.
Aligning Segments
The optional align type in the SEGMENT directive defines the range of memory
addresses from which a starting address for the segment can be selected. The align
type can be any of the following:
Align Type Starting Address
BYTE Next available byte address.
WORD Next available word address.
DWORD Next available doubleword address.
PARA Next available paragraph address (16 bytes per paragraph). Default.
PAGE Next available page address (256 bytes per page).
The linker uses the alignment information to determine the relative starting address
for each segment. The operating system calculates the actual starting address when
the program is loaded.
Combining Segments
The optional combine type in the SEGMENT directive defines how the linker
combines segments having the same name but appearing in different modules.
The combine type controls linker behavior, not assembler behavior. The combine
types, which are described in full detail in Help, include:
Combine Type Linker Action
PRIVATE Does not combine the segment with segments from other
modules, even if they have the same name. Default.
PUBLIC Concatenates all segments having the same name to form a
single, contiguous segment.
STACK Concatenates all segments having the same name and causes
the operating system to set SS:00 to the bottom and SS:SP to
the top of the resulting segment. Data initialization is
unreliable, as discussed following.
COMMON Overlaps segments. The length of the resulting area is the
length of the largest of the combined segments. Data
initialization is unreliable, as discussed following.
MEMORY Used as a synonym for the PUBLIC combine type.
AT address Assumes address as the segment location. An AT segment
cannot contain any code or initialized data, but is useful for
defining structures or variables that correspond to specific far
memory locations, such as a screen buffer or low memory.
You cannot use the AT combine type in protected-mode
programs.
Note Normally, you should provide at least one stack segment (having STACK
combine type) in a program. If no stack segment is declared, LINK displays a
warning message. You can ignore this message if you have a specific reason for not
declaring a stack segment. For example, you would not have a separate stack
segment in a MS-DOS tiny model (.COM) program, nor would you need a separate
stack in a DLL that uses the caller’s stack.
16-bit offset. If .MODEL precedes the .386 or .486 directive, USE16 is the
default. To make USE32 the default, put .386 or .486 before .MODEL. You can
override the USE32 default with the USE16 attribute, or vice versa.
Note Programs written for MS-DOS must not specify USE32. Mixing 16-bit and
32-bit segments in the same program is possible but usually applies only to systems
programming.
When you declare two or more segments to be in the same class, the linker
automatically makes them contiguous. This rule overrides the segment-ordering
directives. (For more about segment classes, see “Setting Segment Order with Class
Type” in the previous section.)
Linker Control
Most of the segment-ordering techniques (class names, .ALPHA, and .SEQ)
control the order in which the assembler outputs segments. Usually, you are more
interested in the order in which segments appear in the executable file. The linker
controls this order.
The linker processes object files in the order in which they appear on the command
line. Within each module, it then outputs segments in the order given in the object
file. If the first module defines segments DSEG and STACK and the second module
defines CSEG, then CSEG is output last. If you want to place CSEG first, there are
two ways to do so.
The simpler method is to use .DOSSEG. This directive is output as a special record
to the object file linker, and it tells the linker to use the Microsoft segment-ordering
convention. This convention overrides command-line order of object files, and it
places all segments of class 'CODE' first. (See “Defining Segments with the
SEGMENT Directive,” previous.)
The other method is to define all the segments as early as possible (in an include
file, for example, or in the first module). These definitions can be “dummy
segments” — that is, segments with no content. The linker observes the segment
ordering given, then later combines the empty segments with segments in other
modules that have the same name.
For example, you might include the following at the start of the first module of your
program or in an include file:
_TEXT SEGMENT WORD PUBLIC 'CODE'
_TEXT ENDS
_DATA SEGMENT WORD PUBLIC 'DATA'
_DATA ENDS
CONST SEGMENT WORD PUBLIC 'CONST'
CONST ENDS
STACK SEGMENT PARA STACK 'STACK'
STACK ENDS
Later in the program, the order in which you write _TEXT, _DATA, or other
segments does not matter because the ultimate order is controlled by the segment
order defined in the include file.
at the beginning of your program if you want the current segment associated
with CS.
Note Using the ASSUME directive to tell the assembler which segment to
associate with a segment register is not the same as telling the processor. The
ASSUME directive affects only assembly-time assumptions. You may need to use
instructions to change run-time conditions. Initializing segment registers at run time
is discussed in “Informing the Assembler About Segment Values,” Chapter 3.
The ASSUME directive can define a segment for each of the segment registers. The
segregister can be CS, DS, ES, or SS (and FS and GS on the 80386/486). The
seglocation must be one of the following:
◆ The name of a segment defined in the source file with the SEGMENT
directive.
◆ The name of a group defined in the source file with the GROUP directive.
◆ The keyword NOTHING, ERROR, or FLAT.
◆ A SEG expression (see “Immediate Operands” in Chapter 3).
◆ A string equate (text macro) that evaluates to a segment or group name (but not
a string equate that evaluates to a SEG expression).
is perfectly legal. It simply adds CSEG to the group MYGROUP; ASEG and BSEG are
not removed.
Each segment can be any valid segment name (including a segment defined later in
source code), with one restriction: a segment cannot belong to more than one group.
The GROUP directive does not affect the order in which segments of a group are
loaded. You can place any number of 16-bit segments in a group as long as the total
size does not exceed 65,536 bytes. If the processor is in 32-bit mode, the maximum
size is 4 gigabytes. You need to make sure that non-grouped segments do not get
placed between grouped segments in such a way that the size of the group exceeds
64K or 4 gigabytes. Neither can you place a 16-bit and a 32-bit segment in the
same group.
C H A P T E R 3
The segment registers DS, SS, and CS are normally initialized to default segments
at the beginning of a program. If you write the main module in a high-level
language, the compiler initializes the segment registers. If you write the main
module in assembly language, you must initialize the segment registers yourself.
Follow these steps to initialize segments:
1. Tell the assembler which segment is associated with a register. The assembler
must know the default segments at assembly time.
2. Tell the processor which segment is associated with a register by writing the
necessary code to load the correct segment value into the segment register on the
processor.
Correct use of the ASSUME statement can help find addressing errors. With
.CODE, the assembler assumes CS is the current segment. When you use the
simplified segment directives .DATA, .DATA?, .CONST, .FARDATA, or
Prior to the .MODEL statement (or in its absence), the assembler sets the
ASSUME statement for DS, ES, and SS to the current segment.
These steps tell the linker where execution begins in the program. The following
example illustrates the two steps for a tiny model program:
_TEXT SEGMENT WORD PUBLIC 'CODE'
ORG 100h ; Use this declaration for .COM files only
start: . ; First instruction here
.
.
_TEXT ENDS
END start ; Name of starting label
Notice the ORG statement in this example. This statement is mandatory in a tiny
model program without the .STARTUP directive. It places the first instruction at
offset 100h in the code segment to create space for a 256-byte (100h) data area
called the Program Segment Prefix (PSP). The operating system takes care of
initializing the PSP, so you need only make sure the area exists. (For a description
of what data resides in the PSP, refer to the “Tables” chapter in the Reference.)
Initializing DS
The DS register is automatically initialized to the correct value (DGROUP) if you
use .STARTUP or if you are writing a program for Windows. If you do not use
.STARTUP with MS-DOS, you must initialize DS using the following
instructions:
mov ax, DGROUP
mov ds, ax
The initialization requires two instructions because the segment name is a constant
and the assembler does not allow a constant to be loaded directly to a segment
register. The previous example loads DGROUP, but you can load any valid
segment or group.
Initializing SS and SP
The SS and SP registers are initialized automatically if you use the .STACK
directive with simplified segments or if you define a segment that has the STACK
combine type with full segment definitions. Using the STACK directive initializes
SS to the stack segment. If you want SS to be equal to DS, use .STARTUP or its
equivalent. (See “Combining Segments,” page 45.) For an .EXE file, the stack
address is encoded into the executable header and resolved at load time. For a
.COM file, the loader sets SS equal to CS and initializes SP to 0FFFEh.
If your program does not access far data, you do not need to initialize the ES
register. If you choose to initialize, use the same technique as for the DS register.
You can initialize SS to a far stack in the same way.
Near Code
Control transfers within near code do not require changes to segment registers. The
processor automatically handles changes to the offset in the IP register when
control-flow instructions such as JMP, CALL, and RET are used. The statement
call nearproc ; Change code offset
changes the IP register to the new address but leaves the segment unchanged. When
the procedure returns, the processor resets IP to the offset of the next instruction
after the CALL instruction.
Far Code
The processor automatically handles segment register changes when dealing with
far code. The statement
call farproc ; Change code segment and offset
automatically moves the segment and offset of the farproc procedure to the CS
and IP registers. When the procedure returns, the processor sets CS to the original
code segment and sets IP to the offset of the next instruction after the call.
Near Data
A program can access near data directly, because a segment register already holds
the correct segment for the data item. The term “near data” is often used to refer to
the data in the DGROUP group.
After the first initialization of the DS and SS registers, these registers normally
point into DGROUP. If you modify the contents of either of these registers during
the execution of the program, you must reload the register with DGROUP’s address
before referencing any DGROUP data.
The processor assumes all memory references are relative to the segment in the DS
register, with the exception of references using BP or SP. The processor associates
these registers with the SS register. (You can override these assumptions with the
segment override operator, described in “Direct Memory Operands,” on page 62.)
The following lines illustrate how the processor accesses either the DS or SS
segments, depending on whether the pointer operand contains BP or SP. Note the
distinction loses significance when DS and SS are equal.
nearvar WORD 0
.
.
.
mov ax, nearvar ; Reads from DS:[nearvar]
mov di, [bx] ; Reads from DS:[bx]
mov [di], cx ; Writes to DS:[di]
mov [bp+6], ax ; Writes to SS:[bp+6]
mov bx, [bp] ; Reads from SS:[bp]
Far Data
To read or modify a far address, a segment register must point to the segment of the
data. This requires two steps. First load the segment (normally either ES or DS)
with the correct value, and then (optionally) set an assume of the segment register to
the segment of the address.
Note Flat model does not require far addresses. By default, all addressing is
relative to the initial values of the segment registers. Therefore, this section on far
addressing does not apply to flat model programs.
One method commonly used to access far data is to initialize the ES segment
register. This example shows two ways to do this:
; First method
mov ax, SEG farvar ; Load segment of the
mov es, ax , far address into ES
mov ax, es:farvar ; Provide an explicit segment
; override on the addressing
; Second method
mov ax, SEG farvar2 ; Load the segment of the
mov es, ax ; far address into ES
ASSUME ES:SEG farvar2 ; Tell the assembler that ES points
; to the segment containing farvar2
mov ax, farvar2 ; The assembler provides the ES
; override since it knows that
; the label is addressable
After loading the segment of the address into the ES segment register, you can
explicitly override the segment register so that the addressing is correct (method 1)
or allow the assembler to insert the override for you (method 2). The assembler uses
ASSUME statements to determine which segment register can be used to address a
segment of memory. To use the segment override operator, the left operand must be
a segment register, not a segment name. (For more information on segment
overrides, see “Direct Memory Operands” on page 62.)
If an instruction needs a segment override, the resulting code is slightly larger and
slower, since the override must be encoded into the instruction. However, the
resulting code may still be smaller than the code for multiple loads of the default
segment register for the instruction.
The DS, SS, FS, and GS segment registers (FS and GS are available only on the
80386/486 processors) may also be used for addressing through other segments.
If a program uses ES to access far data, it need not restore ES when finished (unless
the program uses flat model). However, some compilers require that you restore ES
before returning to a module written in a high-level language.
To access far data, first set DS to the far segment and then restore the original DS
when finished. Use the ASSUME directive to let the assembler know that DS no
longer points to the default data segment, as shown here:
push ds ; Save original segment
mov ax, SEG fararray ; Move segment into data register
mov ds, ax ; Initialize segment register
ASSUME ds:SEG fararray ; Tell assembler where data is
mov ax, fararray[0] ; Set DX:AX = dword variable
mov dx, fararray[2] ; fararray
.
.
.
pop ds ; Restore segment
ASSUME ds:@DATA ; and default assumption
Operands
With few exceptions, assembly language instructions work on sources of data
called operands. In a listing of assembly code (such as the examples in this book),
operands appear in the operand field immediately to the right of the instructions.
This section describes the four kinds of instruction operands: register, immediate,
direct memory, and indirect memory. Some instructions, such as POPF and STI,
have implied operands which do not appear in the operand field. Otherwise, an
implied operand is just as real as one stated explicitly.
Certain other instructions such as NOP and WAIT deserve special mention. These
instructions affect only processor control and do not require an operand.
The following four types of operands are described in the rest of this section:
Operand Type Addressing Mode
Register An 8-bit or 16-bit register on the 8086–80486; can also be 32-bit on
the 80386/486.
Immediate A constant value contained in the instruction itself.
Direct memory A fixed location in memory.
Indirect memory A memory location determined at run time by using the address stored
in one or two registers.
Instructions that take two or more operands always work right to left. The right
operand is the source operand. It specifies data that will be read, but not changed, in
the operation. The left operand is the destination operand. It specifies the data that
will be acted on and possibly changed by the instruction.
Register Operands
Register operands refer to data stored in registers. The following examples show
typical register operands:
mov bx, 10 ; Load constant to BX
add ax, bx ; Add BX to AX
jmp di ; Jump to the address in DI
An offset stored in a base or index register often serves as a pointer into memory.
You can store an offset in one of the base or index registers, then use the register as
an indirect memory operand. (See “Indirect Memory Operands,” following.) For
example:
mov [bx], dl ; Store DL in indirect memory operand
inc bx ; Increment register operand
mov [bx], dl ; Store DL in new indirect memory operand
Immediate Operands
An immediate operand is a constant or the result of a constant expression. The
assembler encodes immediate values into the instruction at assembly time. Here are
some typical examples showing immediate operands:
mov cx, 20 ; Load constant to register
add var, 1Fh ; Add hex constant to variable
sub bx, 25 * 80 ; Subtract constant expression
Immediate data is never permitted in the destination operand. If the source operand
is immediate, the destination operand must be either a register or direct memory to
provide a place to store the result of the operation.
Immediate expressions often involve the useful OFFSET and SEG operators,
described in the following paragraphs.
For information on differences between MASM 5.1 behavior and MASM 6.1
behavior related to OFFSET, see Appendix A.
Since data in different modules may belong to a single segment, the assembler
cannot know for each module the true offsets within a segment. Thus, the offset for
var, although an immediate value, is not determined until link time.
The actual value of a particular segment is not known until the program is loaded
into memory. For .EXE programs, the linker makes a list in the program’s header of
all locations in which the SEG operator appears. The loader reads this list and fills
in the required segment address at each location. Since .COM programs have no
header, the assembler does not allow relocatable segment expressions in tiny model
programs.
The SEG operator returns a variable’s “frame” if it appears in the instruction. The
frame is the value of the segment, group, or segment override of a nonexternal
variable. For example, the instruction
mov ax, SEG DGROUP:var
places in AX the value of DGROUP, where var is located. If you do not include a
frame, SEG returns the value of the variable’s group if one exists. If the variable is
not defined in a group, SEG returns the variable’s segment address.
This behavior can be changed with the /Zm command-line option or with the
OPTION OFFSET:SEGMENT statement. (See Appendix A, “Differences
between MASM 6.1 and 5.1.”) “Using the OPTION Directive” in Chapter 1
introduces the OPTION directive.
The index operator can contain any direct memory operand. The following
statements are equivalent:
mov ax, var
mov ax, [var]
Some programmers prefer to enclose the operand in brackets to show that the
contents, not the address, are used.
The minus operator behaves as you would expect. Both the following instructions
retrieve the value located at the word preceding array:
mov ax, array[-2]
mov ax, array-2
Structure Field
The structure operator (.) references a particular element of a structure or “field,” to
use C terminology:
mov bx, structvar.field1
The address of the structure operand is the sum of the offsets of structvar and
field1. For more information about structures, see “Structures and Unions” in
Chapter 5.
Segment Override
The segment override operator (:) specifies a segment portion of the address that is
different from the default segment. When used with instructions, this operator can
apply to segment registers or segment names:
mov ax, es:farvar ; Use segment override
The assembler will not generate a segment override if the default segment is
explicitly provided. Thus, the following two statements assemble in exactly the
same way:
mov [bx], ax
mov ds:[bx], ax
A segment name override or the segment override operator identifies the operand as
an address expression.
mov WORD PTR FARSEG:0, ax ; Segment name override
mov WORD PTR es:100h, ax ; Legal and equivalent
mov WORD PTR es:[100h], ax ; expressions
; mov WORD PTR [100h], ax ; Illegal, not an address
When you specify more than one register, the processor adds the contents of the two
addresses together to determine the effective address (the address of the data to
operate on):
mov ax, [bx+si]
Specifying Displacements
You can specify an address displacement, which is a constant value added to the
effective address. A direct memory specifier is the most common displacement:
mov ax, table[si]
Each displacement can be an address or numeric constant. If there is more than one
displacement, the assembler totals them at assembly time and encodes the total
displacement. For example, in the statement
table WORD 100 DUP (0)
.
.
.
mov ax, table[bx][di]+6
both table and 6 are displacements. The assembler adds the value of 6 to table
to get the total displacement. However, the statement
mov ax, mem1[si] + mem2
is not legal, because it attempts to use a single command to join the contents of two
different addresses.
The following lines illustrate all three methods. Assume the size of the table array
is WORD, as declared earlier.
mov table[bx], 0 ; 2 bytes - from size of table
mov BYTE PTR table, 0 ; 1 byte - specified by BYTE
mov ax, [bx] ; 2 bytes - implied by AX
Syntax Options
The assembler allows a variety of syntaxes for indirect memory operands.
However, all registers must be inside brackets. You can enclose each register in its
own pair of brackets, or you can place the registers in the same pair of brackets
separated by a plus operator (+). All the following variations are legal and
assemble the same way:
mov ax, table[bx][di]
mov ax, table[di][bx]
mov ax, table[bx+di]
mov ax, [table+bx+di]
mov ax, [bx][di]+table
All of these statements move the value in table indexed by BX+DI into AX.
Scaling Indexes
The value of index registers pointing into arrays must often be adjusted for zero-
based arrays and scaled according to the size of the array items. For a word array,
the item number must be multiplied by two (shifted left by one place). When using
16-bit registers, you must scale with separate instructions, as shown here:
mov bx, 5 ; Get sixth element (adjust for 0)
shl bx, 1 ; Scale by two (word size)
inc wtable[bx] ; Increment sixth element in table
When using 32-bit registers on the 80386/486 processor, you can include scaling in
the operand, as described in “Indirect Memory Operands with 32-Bit Registers,”
following.
BP and BX are base registers. SI and DI are index registers. You can use either a
base or an index register by itself. But if you combine two registers, one must be a
base and one an index. Here are legal and illegal forms:
mov ax, [bx+di] ; Legal
mov ax, [bx+si] ; Legal
mov ax, [bp+di] ; Legal
mov ax, [bp+si] ; Legal
; mov ax, [bx+bp] ; Illegal - two base registers
; mov ax, [di+si] ; Illegal - two index registers
Table 3.1 shows the register modes in which you can specify indirect memory
operands.
Table 3.1 Indirect Addressing with 16-Bit Registers
Mode Syntax Effective Address
Register indirect [BX] Contents of register
[BP]
[DI]
[SI]
Base or index displacement[BX] Contents of register plus
displacement[BP] displacement
displacement[DI]
displacement[SI]
Base plus index [BX][DI] Contents of base register plus
[BP][DI] contents of index register
[BX][SI]
[BP][SI]
Base plus index with displacement[BX][DI] Sum of base register, index
displacement displacement[BP][DI] register, and displacement
displacement[BX][SI]
displacement[BP][SI]
registers eliminate many of the limitations of 16-bit indirect memory operands. You
can use 80386/486 features to make your MS-DOS programs run faster and more
efficiently if you are willing to sacrifice compatibility with earlier processors.
In 32-bit mode, an offset address can be up to 4 gigabytes. (Segments are still
represented in 16 bits.) This effectively eliminates size restrictions on each
segment, since few programs need 4 gigabytes of memory. Windows NT uses 32-
bit mode and flat model, which spans all segments. XENIX 386 uses 32-bit mode
with multiple segments.
80386/486 Enhancements
On the 80386/486, the processor allows you to use any general-purpose 32-bit
register as a base or index register, except ESP, which can be a base but not an
index. However, you cannot combine 16-bit and 32-bit registers. Several examples
are shown here:
add edx, [eax] ; Add double
mov dl, [esp+10] ; Copy byte from stack
dec WORD PTR [edx][eax] ; Decrement word
cmp ax, array[ebx][ecx] ; Compare word from array
jmp FWORD PTR table[ecx] ; Jump into pointer table
Scaling Factors
With 80386/486 registers, the index register can have a scaling factor of 1, 2, 4, or
8. Any register except ESP can be the index register and can have a scaling factor.
To specify the scaling factor, use the multiplication operator (*) adjacent to the
register.
You can use scaling to index into arrays with different sizes of elements. For
example, the scaling factor is 1 for byte arrays (no scaling needed), 2 for word
arrays, 4 for doubleword arrays, and 8 for quadword arrays. There is no
performance penalty for using a scaling factor. Scaling is illustrated in the
following examples:
mov eax, darray[edx*4] ; Load double of double array
mov eax, [esi*8][edi] ; Load double of quad array
mov ax, wtbl[ecx+2][edx*2] ; Load word of word array
Scaling is also necessary on earlier processors, but it must be done with separate
instructions before the indirect memory operand is used, as described in “Indirect
Memory Operands with 16-Bit Registers,” previous.
The default segment register is SS if the base register is EBP or ESP. However, if
EBP is scaled, the processor treats it as an index register with a value relative to
DS, not SS.
All other base registers are relative to DS. If two registers are used, only one can
have a scaling factor. The register with the scaling factor is defined as the index
register. The other register is defined as the base. If scaling is not used, the first
register is the base. If only one register is used, it is considered the base for
deciding the default segment unless it is scaled. The following examples illustrate
how to determine the base register:
mov eax, [edx][ebp*4] ; EDX base (not scaled - seg DS)
mov eax, [edx*1][ebp] ; EBP base (not scaled - seg SS)
mov eax, [edx][ebp] ; EDX base (first - seg DS)
mov eax, [ebp][edx] ; EBP base (first - seg SS)
mov eax, [ebp] ; EBP base (only - seg SS)
mov eax, [ebp*2] ; EBP*2 index (seg DS)
This statement moves the 32-bit value pointed to by BX into the EAX register.
Although BX is a 16-bit pointer, it can still point into a 32-bit segment.
However, the following statement is never legal, since you cannot use the CX
register as a 16-bit pointer:
; mov eax, [cx] ; illegal
Operands that mix 16-bit and 32-bit registers are also illegal:
; mov eax, [ebx+si] ; illegal
This statement moves the 16-bit value pointed to by EAX into the BX register. This
works in 32-bit mode. However, in 16-bit mode, moving a 32-bit pointer into a 16-
bit segment is illegal. If EAX contains a 16-bit value (the top half of the 32-bit
register is 0), the statement works. However, if the top half of the EAX register is
not 0, the operand points into a part of the segment that doesn’t exist, generating an
error. If you use 32-bit registers as indexes in 16-bit mode, you must make sure that
the index registers contain valid 16-bit addresses.
Note The 8086 and 8088 processors differ from later Intel processors in how they
push and pop the SP register. If you give the statement push sp with the 8086 or
8088, the word pushed is the word in SP after the push operation.
Figure 3.1 illustrates how pushes and pops change the SP register.
High memory
SP
word SP
from AX
Low memory
High memory
SP
word SP
from AX
Low memory
Figure 3.1 Stack Status Before and After Pushes and Pops
On the 8086, PUSH and POP take only registers or memory expressions as their
operands. The other processors allow an immediate value to be an operand for
PUSH. For example, the following statement is legal on the 80186–80486
processors:
push 7 ; 3 clocks on 80286
That statement is faster than these equivalent statements, which are required on the
8088 or 8086:
mov ax, 7 ; 2 clocks plus
push ax ; 3 clocks on 80286
Words are popped off the stack in reverse order: the last item pushed is the first
popped. To return the stack to its original status, you do the same number of pops as
pushes. You can subtract the correct number of words from the SP register if you
want to restore the stack without using the values on it.
To reference operands on the stack, remember that the values pointed to by the BP
(Base Pointer) and SP registers are relative to the SS (Stack Segment) register. The
BP register is often used to point to the base of a frame of reference (a stack frame)
within the stack. This example shows how you can access values on the stack using
indirect memory operands with BP as the base register.
push bp ; Save current value of BP
mov bp, sp ; Set stack frame
push ax ; Push first; SP = BP - 2
push bx ; Push second; SP = BP - 4
push cx ; Push third; SP = BP - 6
.
.
.
mov ax, [bp-6] ; Put third word in AX
mov bx, [bp-4] ; Put second word in BX
mov cx, [bp-2] ; Put first word in CX
.
.
.
add sp, 6 ; Restore stack pointer
; (two bytes per push)
pop bp ; Restore BP
If you often use these stack values in your program, you may want to give them
labels. For example, you can use TEXTEQU to create a label such as count
TEXTEQU <[bp-6]>. Now you can replace the mov ax, [bp - 6] statement in
the previous example with mov ax, count. For more information about the
TEXTEQU directive, see “Text Macros” in Chapter 9.
If you do not need to store the entire flags register, you can use the LAHF
instruction to manually load and store the status of the lower byte of the flag
register in the AH register. SAHF restores the value.
If a variable is initialized with the name of another variable, the initialized variable
is a pointer, as shown in this example. However, in previous versions of MASM,
the CodeView debugger recognizes npVar and fpVar as word and doubleword
variables. CodeView does not treat them as pointers, nor does it recognize the type
of data they point to (bytes, in the example).
still word values in 32-bit mode. If you specify the distance with NEAR or FAR,
the processor uses the default distance of the current segment size. You can use
NEAR16, NEAR32, FAR16, and FAR32 to override the defaults set by the
current segment size. In flat model, NEAR is the default.
You can declare pointer variables with a pointer type created with TYPEDEF.
Here are some examples using these pointer types.
; Type declarations
Array WORD 25 DUP (0)
Msg BYTE "This is a string", 0
pMsg PBYTE Msg ; Pointer to string
pArray PWORD Array ; Pointer to word array
npMsg NPBYTE Msg ; Near pointer to string
npArray NPWORD Array ; Near pointer to word array
fpArray FPWORD Array ; Far pointer to word array
fpMsg FPBYTE Msg ; Far pointer to string
; Procedure prototype
Once defined, pointer types can be used in any context where intrinsic types are
allowed.
When you have used ASSUME, attempts to use the register for other purposes
generate assembly errors. In this example, while the PTR WORD assumption is in
effect, any use of BX inconsistent with its ASSUME declaration generates an
error. For example,
; mov al, [bx] ; Can't move word to byte register
Similarly, you can use ASSUME to prevent the use of a register as a pointer, or
even to disable a register:
ASSUME bx:WORD, dx:ERROR
; mov al, [bx] ; Error - BX is an integer, not a pointer
; mov ax, dx ; Error - DX disabled
For information on using ASSUME with segment registers, refer to “Setting the
ASSUME Directive for Segment Registers” in Chapter 2.
The sections in the rest of this chapter describe variations of these tasks with
pointers and addresses. The examples are used with the assumption that you have
previously defined the following pointer types with the TYPEDEF directive:
PBYTE TYPEDEF PTR BYTE ; Pointer to bytes
NPBYTE TYPEDEF NEAR PTR BYTE ; Near pointer to bytes
FPBYTE TYPEDEF FAR PTR BYTE ; Far pointer to bytes
If you know that the segment for a far pointer is in a register, you can load it
directly:
mov WORD PTR fpMsg[2], ds ; Load segment of
; far pointer
Dynamic Addresses
Often a pointer must point to a dynamic address, meaning the address depends on a
run-time condition. Typical situations include memory allocated by MS-DOS (see
“Interrupt 21h Function 48h” in Help) and addresses found by the SCAS or CMPS
instructions (see “Processing Strings” in Chapter 5). The following illustrates the
technique for saving dynamic addresses:
; Dynamically allocated buffer
fpBuf FPBYTE 0 ; Initialize so offset will be zero
.
.
.
mov ah, 48h ; Allocate memory
mov bx, 10h ; Request 16 paragraphs
int 21h ; Call DOS
jc error ; Return segment in AX
mov WORD PTR fpBuf[2], ax ; Load segment
. ; (offset is already 0)
.
.
error: ; Handle error
Copying Pointers
Sometimes one pointer variable must be initialized by copying from another. Here
are two ways to copy a far pointer:
fpBuf1 FPBYTE ?
fpBuf2 FPBYTE ?
.
.
.
; Copy through registers is faster, but requires a spare register
mov ax, WORD PTR fpBuf1[0]
mov WORD PTR fpBuf2[0], ax
mov ax, WORD PTR fpBuf1[2]
mov WORD PTR fpBuf2[2], ax
Pointers as Arguments
Most high-level-language procedures and library functions accept arguments passed
on the stack. “Passing Arguments on the Stack” in Chapter 7 covers this subject in
detail. A pointer is passed in the same way as any other variable, as this fragment
shows:
; Push a far pointer (segment always pushed first)
push WORD PTR fpMsg[2] ; Push segment
push WORD PTR fpMsg[0] ; Push offset
Pushing an address has the same result as pushing a pointer to the address:
; Push a far address as a far pointer
mov ax, SEG fVar ; Load and push segment
push ax
mov ax, OFFSET fVar ; Load and push offset
push ax
On the 80186 and later processors, you can push a constant in one step:
push SEG fVar ; Push segment
push OFFSET fVar ; Push offset
Here is an example of loading an address into DS:BX from a near data segment:
.DATA
Msg BYTE "String"
.
.
.
mov bx, OFFSET Msg ; Load address to BX
; (DS already loaded)
You can also read a far address from a pointer in one step, using the LES and LDS
instructions described next.
Far Pointers
The LES and LDS instructions load a far pointer into a segment pair. The
instructions copy the pointer’s low word into either ES or DS, and the high word
into a given register. The following example shows how to load a far pointer into
ES:DI:
OutBuf BYTE 20 DUP (0)
Stack Variables
The technique for loading the address of a stack variable is significantly different
from the technique for loading near addresses. You may need to put the correct
segment value into ES for string operations. The following example illustrates how
to load the address of a local (stack) variable to ES:DI:
Task PROC
LOCAL Arg[4]:BYTE
The local variable in this case actually evaluates to SS:[BP-4]. This is an offset
from the stack frame (described in “Passing Arguments on the Stack,” Chapter 7).
Since you cannot use the OFFSET operator to get the offset of an indirect memory
operand, you must use the LEA (Load Effective Address) instruction.
Model-Independent Techniques
Often you may want to write code that is memory-model independent. If you are
writing libraries that must be available for different memory models, you can use
conditional assembly to handle different sizes of pointers. You can use the
predefined symbols @DataSize and @Model to test the current assumptions.
You can use conditional assembly to write code that works with pointer variables
that have no specified distance. The predefined symbol @DataSize tests the pointer
size for the current memory model:
Msg1 BYTE "String1"
pMsg PBYTE ?
.
.
.
IF @DataSize ; @DataSize > 0 for far
mov WORD PTR pMsg[0], OFFSET Msg1 ; Load far offset
mov WORD PTR pMsg[2], SEG Msg1 ; Load far segment
ELSE ; @DataSize = 0 for near
mov pMsg, OFFSET Msg1 ; Load near pointer
ENDIF
IF @DataSize
les bx, arg ; Load far pointer to ES:BX
mov ax, es:[bx] ; Load the data pointed to
ELSE
mov bx, arg ; Load near pointer to BX (assume DS)
mov ax, [bx] ; Load the data pointed to
ENDIF
shl ax, 1 ; Multiply by 8
shl ax, 1
shl ax, 1
ret
mul8 ENDP
If you have many routines, writing the conditionals for each case can be tedious.
The following conditional statements automatically generate the proper instructions
and segment overrides.
; Equates for conditional handling of pointers
IF @DataSize
lesIF TEXTEQU <les>
ldsIF TEXTEQU <lds>
esIF TEXTEQU <es:>
ELSE
lesIF TEXTEQU <mov>
ldsIF TEXTEQU <mov>
esIF TEXTEQU <>
ENDIF
Once you define these conditionals, you can use them to simplify code that must
handle several types of pointers. This next example rewrites the above mul8
procedure to use conditional code.
mul8 PROC arg:PTR WORD
The conditional statements from these examples can be defined once in an include
file and used whenever you need to handle pointers.
C H A P T E R 4
This chapter covers the concepts essential for working with simple data types in
assembly-language programs. The first section shows how to declare integer
variables. The second section describes basic operations including moving, loading,
and sign-extending numbers, as well as calculating. The last section describes how
to do various operations with numbers at the bit level, such as using bitwise logical
instructions and shifting and rotating bits.
The complex data types introduced in the next chapter — arrays, strings, structures,
unions, and records — use many of the operations illustrated in this chapter.
Floating-point operations require a different set of instructions and techniques.
These are covered in Chapter 6, “Using Floating-Point and Binary Coded Decimal
Numbers.”
The following directives indicate the integer’s size and value range:
Directive Description of Initializers
BYTE, DB (byte) Allocates unsigned numbers from 0 to 255.
SBYTE (signed byte) Allocates signed numbers from –128 to +127.
WORD, DW (word = 2 bytes) Allocates unsigned numbers from
0 to 65,535 (64K).
SWORD (signed word) Allocates signed numbers from
–32,768 to +32,767.
DWORD, DD (doubleword = 4 Allocates unsigned numbers from
bytes), 0 to 4,294,967,295 (4 megabytes).
SDWORD (signed doubleword) Allocates signed numbers from
–2,147,483,648 to +2,147,483,647.
FWORD, DF (farword = 6 bytes) Allocates 6-byte (48-bit) integers. These values are
normally used only as pointer variables on the
80386/486 processors.
QWORD, DQ (quadword = 8 Allocates 8-byte integers used with 8087-family
bytes) coprocessor instructions.
TBYTE, DT (10 bytes), Allocates 10-byte (80-bit) integers if the initializer
has a radix specifying the base of the number.
See Chapter 6 for information on the REAL4, REAL8, and REAL10 directives
that allocate real numbers.
The SIZEOF and TYPE operators, when applied to a type, return the size of an
integer of that type. The size attribute associated with each data type is:
Data Type Bytes
BYTE, SBYTE 1
WORD, SWORD 2
DWORD, SDWORD 4
FWORD 6
QWORD 8
TBYTE 10
The data types SBYTE, SWORD, and SDWORD tell the assembler to treat the
initializers as signed data. It is important to use these signed types with high-level
constructs such as .IF, .WHILE, and .REPEAT, and with PROTO and INVOKE
directives. For descriptions of these directives, see the sections “Loop-Generating
Directives,” “Declaring Procedure Prototypes,” and “Calling Procedures with
INVOKE” in Chapter 7.
The assembler stores integers with the least significant bytes lowest in memory.
Note that assembler listings and most debuggers show the bytes of a word in the
opposite order — high byte first.
Figure 4.1 illustrates the integer formats.
Word
0 1
Doubleword
0 1 2 3
Quadword
0 1 2 3 4 5 6 7
allow you to use char, long, float, or double in your programs if you prefer
the C data labels.
Data Initialization
You can initialize variables when you declare them with constants or expressions
that evaluate to constants. The assembler generates an error if you specify an initial
value too large for the variable type.
A ? in place of an initializer indicates you do not require the assembler to initialize
the variable. The assembler allocates the space but does not write in it. Use ? for
buffer areas or variables your program will initialize at run time.
You can declare and initialize variables in one step with the data directives, as
these examples show.
integer BYTE 16 ; Initialize byte to 16
negint SBYTE -16 ; Initialize signed byte to -16
expression WORD 4*3 ; Initialize word to 12
signedexp SWORD 4*3 ; Initialize signed word to 12
empty QWORD ? ; Allocate uninitialized long int
BYTE 1,2,3,4,5,6 ; Initialize six unnamed bytes
long DWORD 4294967295 ; Initialize doubleword to
; 4,294,967,295
longnum SDWORD -2147433648 ; Initialize signed doubleword
; to -2,147,433,648
tb TBYTE 2345t ; Initialize 10-byte binary number
For information on arrays and on using the DUP operator to allocate initializer
lists, see “Arrays and Strings” in Chapter 5.
Copying Data
The primary instructions for moving data from operand to operand and loading them
into registers are MOV (Move), XCHG (Exchange), CWD (Convert Word to
Double), and CBW (Convert Byte to Word).
Moving Data
The most common method of moving data, the MOV instruction, is essentially a
copy instruction, since it always copies the source operand to the destination
operand without affecting the source. After a MOV instruction, the source and
destination operands contain the same value.
The following example illustrates the MOV instruction. As explained in “General-
Purpose Registers,” Chapter 1, you cannot move a value from one location in
memory to another in a single operation.
; Immediate value moves
mov ax, 7 ; Immediate to register
mov mem, 7 ; Immediate to memory direct
mov mem[bx], 7 ; Immediate to memory indirect
; Register moves
mov mem, ax ; Register to memory direct
mov mem[bx], ax ; Register to memory indirect
mov ax, bx ; Register to register
mov ds, ax ; General register to segment register
The following example shows several common types of moves that require two
instructions.
; Move immediate to segment register
mov ax, DGROUP ; Load AX with immediate value
mov ds, ax ; Copy AX to segment register
The MOVSX and MOVZX instructions for the 80386/486 processors extend and
copy values in one step. See “Extending Signed and Unsigned Integers,” following.
Exchanging Integers
The XCHG (Exchange) instruction exchanges the data in the source and
destination operands. You can exchange data between registers or between registers
and memory, but not from memory to memory:
xchg ax, bx ; Put AX in BX and BX in AX
xchg memory, ax ; Put "memory" in AX and AX in "memory"
; xchg mem1, mem2 ; Illegal- can't exchange memory locations
These four instructions efficiently convert unsigned values as well, provided the
sign bit is zero. This example, for instance, correctly widens mem16 whether you
treat the variable as signed or unsigned.
The processor does not differentiate between signed and unsigned values. For
instance, the value of mem8 in the previous example is literally 251 (0FBh) to the
processor. It ignores the human convention of treating the highest bit as an indicator
of sign. The processor can ignore the distinction between signed and unsigned
numbers because binary arithmetic works the same in either case.
If you add 7 to mem8, for example, the result is 258 (102h), a value too large to fit
into a single byte. The byte-sized mem8 can accommodate only the least-significant
digits of the result (02h), and so receives the value of 2. The result is the same
whether we treat mem8 as a signed value (-5) or unsigned value (251).
This overview illustrates how the programmer, not the processor, must keep track of
which values are signed or unsigned, and treat them accordingly. If AL=127
(01111111y), the instruction CBW sets AX=127 because the sign bit is zero. If
AL=128 (10000000y), however, the sign bit is 1. CBW thus sets AX=65,280
(FF00h), which may not be what you had in mind if you assumed AL originally
held an unsigned value.To widen unsigned values, explicitly set the higher register
to zero, as shown in the following example:
.DATA
mem8 BYTE 251
mem16 WORD 251
.CODE
.
.
.
mov al, mem8 ; Load 251 (FBh) from 8-bit memory
sub ah, ah ; Zero upper half (AH)
The 80386/486 processors provide instructions that move and extend a value to a
larger data size in a single step. MOVSX moves a signed value into a register and
sign-extends it. MOVZX moves an unsigned value into a register and zero-
extends it.
; 80386/486 instructions
movzx dx, bl ; Load unsigned 8-bit value into
; 16-bit register and zero-extend
These special 80386/486 instructions usually execute much faster than the
equivalent 8086/286 instructions.
To meet the second requirement, you can use the PTR operator to force an operand
to the size required. (See “Working with Simple Variables,” previous.) For
example, if Buffer is an array of bytes and BX points to an element of the array,
you can add a word from Buffer with
add ax, WORD PTR Buffer[bx] ; Add word from byte array
The next example shows 8-bit signed and unsigned addition and subtraction.
.DATA
mem8 BYTE 39
.CODE
; Addition
; signed unsigned
mov al, 26 ; Start with register 26 26
inc al ; Increment 1 1
add al, 76 ; Add immediate 76 + 76
; ---- ----
; 103 103
add al, mem8 ; Add memory 39 + 39
; ---- ----
mov ah, al ; Copy to AH -114 142
+overflow
add al, ah ; Add register 142
; ----
; 28+carry
; Subtraction
; signed unsigned
mov al, 95 ; Load register 95 95
dec al ; Decrement -1 -1
sub al, 23 ; Subtract immediate -23 -23
; ---- ----
; 71 71
sub al, mem8 ; Subtract memory -122 -122
; ---- ----
; -51 205+sign
The INC and DEC instructions treat integers as unsigned values and do not update
the carry flag for signed carries and borrows.
When the sum of 8-bit signed operands exceeds 127, the processor sets the
overflow flag. (The overflow flag is also set if both operands are negative and the
sum is less than or equal to -128.) Placing a JO (Jump on Overflow) or INTO
(Interrupt on Overflow) instruction in your program at this point can transfer
control to error-recovery statements. When the sum exceeds 255, the processor sets
the carry flag. A JC (Jump on Carry) instruction at this point can transfer control to
error-recovery statements.
In the previous subtraction example, the processor sets the sign flag if the result
goes below 0. At this point, you can use a JS (Jump on Sign) instruction to transfer
control to error-recovery statements. Jump instructions are described in the
“Jumps” section in Chapter 7.
; Subtraction
mov ax, WORD PTR mem32a[0] ; Load mem32 316423
mov dx, WORD PTR mem32a[2] ; into DX:AX
sub ax, WORD PTR mem32b[0] ; Subtract low - 156739
sbb dx, WORD PTR mem32b[2] ; then high ------
; Result in DX:AX 159684
For 32-bit registers on the 80386/486 processors, only two steps are necessary. If
your program needs to be assembled for more than one processor, you can assemble
the statements conditionally, as shown in this example:
.DATA
mem32 DWORD 316423
mem32a DWORD 316423
mem32b DWORD 156739
p386 TEXTEQU (@Cpu AND 08h)
.CODE
.
.
.
; Addition
IF p386
mov eax, 43981 ; Load immediate
add eax, mem32 ; Result in EAX
ELSE
.
. ; do steps in previous example
.
ENDIF
; Subtraction
IF p386
mov eax, mem32a ; Load memory
sub eax, mem32b ; Result in EAX
ELSE
.
. ; do steps in previous example
.
ENDIF
Since the status of the carry flag affects the results of calculations with ADC and
SBB, be sure to turn off the carry flag with the CLC (Clear Carry Flag) instruction
or use ADD or SUB for the first calculation, when appropriate.
for 8-bit numbers, AX for 16-bit numbers, EAX for 32-bit numbers). The other
factor can be in any single register or memory operand. The result overwrites the
contents of the accumulator register.
Multiplying two 8-bit numbers produces a 16-bit result returned in AX. Multiplying
two 16-bit operands yields a 32-bit result in DX:AX. The 80386/486 processor
handles 64-bit products in the same way in the EDX:EAX pair.
This example illustrates multiplication of signed 16- and 32-bit integers.
.DATA
mem16 SWORD -30000
.CODE
.
.
.
; 8-bit unsigned multiply
mov al, 23 ; Load AL 23
mov bl, 24 ; Load BL * 24
mul bl ; Multiply BL -----
; Product in AX 552
; overflow and carry set
A nonzero number in the upper half of the result (AH for byte, DX or EDX for
word) sets the overflow and carry flags.
On the 80186–80486 processors, the IMUL instruction supports three additional
operand combinations. The first syntax option allows for 16-bit multipliers
producing a 16-bit product or 32-bit multipliers for 32-bit products on the
80386/486. The result overwrites the destination. The syntax for this operation is:
IMUL register16, immediate
The second syntax option specifies three operands for IMUL. The first operand
must be a 16-bit register operand, the second a 16-bit memory (or register)
operand, and the third a 16-bit immediate operand. IMUL multiplies the memory
(or register) and immediate operands and stores the product in the register operand
with this syntax:
IMUL register16,{ memory16 | register16}, immediate
For the 80386/486 only, a third option for IMUL allows an additional operand for
multiplication of a register value by a register or memory value. The syntax is:
IMUL register,{register | memory}
The destination can be any 16-bit or 32-bit register. The source must be the same
size as the destination.
In all of these options, products too large to fit in 16 or 32 bits set the overflow and
carry flags. The following examples show these three options for IMUL.
imul dx, 456 ; Multiply DX times 456 on 80186-80486
imul ax, [bx],6 ; Multiply the value pointed to by BX
; by 6 and put the result in AX
The IMUL instruction with multiple operands can be used for either signed or
unsigned multiplication, since the 16-bit product is the same in either case. To get a
32-bit result, you must use the single-operand version of MUL or IMUL.
Unsigned division does not require careful attention to flags. The following
examples illustrate signed division, which can be more complex.
.DATA
mem16 SWORD -2000
mem32 SDWORD 500000
.CODE
.
.
.
; Divide 16-bit unsigned by 8-bit
mov ax, 700 ; Load dividend 700
mov bl, 36 ; Load divisor DIV 36
div bl ; Divide BL ------
; Quotient in AL 19
; Remainder in AH 16
If the dividend and divisor are the same size, sign-extend or zero-extend the
dividend so that it is the length expected by the division instruction. See “Extending
Signed and Unsigned Integers,” earlier in this chapter.
Logical Instructions
The logical instructions AND, OR, and XOR compare bits in two operands. Based
on the results of the comparisons, the instructions alter bits in the first (destination)
operand. The logical instruction NOT also changes bits, but operates on a single
operand.
The following list summarizes these four logical instructions. The list makes
reference to the “destination bit,” meaning the bit in the destination operand. The
terms “both bits” and “either bit” refer to the corresponding bits in the source and
destination operands. These instructions include:
Instruction Sets Destination Bit If Clears Destination Bit If
AND Both bits set Either or both bits clear
OR Either or both bits set Both bits clear
XOR Either bit (but not both) set Both bits set or both clear
NOT Destination bit clear Destination bit set
Note Do not confuse logical instructions with the logical operators, which perform
these operations at assembly time, not run time. Although the names are the same,
the assembler recognizes the difference.
The following example shows the result of the AND, OR, XOR, and NOT
instructions operating on a value in the AX register and in a mask. A mask is any
number with a pattern of bits set for an intended operation.
mov ax, 035h ; Load value 00110101
and ax, 0FBh ; Clear bit 2 AND 11111011
; --------
; Value is now 31h 00110001
or ax, 016h ; Set bits 4,2,1 OR 00010110
; --------
; Value is now 37h 00110111
xor ax, 0ADh ; Toggle bits 7,5,3,2,0 XOR 10101101
; --------
; Value is now 9Ah 10011010
not ax ; Value is now 65h 01100101
The AND instruction clears unmasked bits — that is, bits not protected by 1 in the
mask. To mask off certain bits in an operand and clear the others, use an
appropriate masking value in the source operand. The bits of the mask should be 0
for any bit positions you want to clear and 1 for any bit positions you want to
remain unchanged.
The OR instruction forces specific bits to 1 regardless of their current settings. The
bits of the mask should be 1 for any bit positions you want to set and 0 for any bit
positions you want to remain unchanged.
The XOR instruction toggles the value of specific bits on and off — that is, reverses
them from their current settings. This instruction sets a bit to 1 if the corresponding
bits are different or to 0 if they are the same. The bits of the mask should be 1 for
any bit positions you want to toggle and 0 for any bit positions you want to remain
unchanged.
The following examples show an application for each of these instructions. The
code illustrating the AND instruction converts a “y” or “n” read from the keyboard
to uppercase, since bit 5 is always clear in uppercase letters. In the example for
OR, the first statement is faster and uses fewer bytes than cmp bx, 0. When the
operands for XOR are identical, each bit cancels itself, producing 0.
;AND example - converts characters to uppercase
mov ah, 7 ; Get character without echo
int 21h
and al, 11011111y ; Convert to uppercase by clearing bit 5
cmp al, 'Y' ; Is it Y?
je yes ; If so, do Yes actions
. ; Else do No actions
.
yes: .
On the 80386/486 processors, the BSF (Bit Scan Forward) and the BSR (Bit Scan
Reverse) instructions perform operations like those of the logical instructions. They
scan the contents of a register to find the first-set or last-set bit. You can use BSF
or BSR to find the position of a set bit in a mask or to check if a register value is 0.
Rotate instructions also move bits a specified number of places to the right or left.
For each bit rotated, the last bit in the direction of the rotate operation moves into
the first bit position at the other end of the operand. With some variations, the carry
bit is used as an additional bit of the operand. Figure 4.2 illustrates the eight
variations of shift and rotate instructions for 8-bit operands. Notice that SHL and
SAL are identical.
CF CF
7 0 7 0
0
CF CF
7 0 7 0
CF CF
CF CF
All shift instructions use the same format. Before the instruction executes, the
destination operand contains the value to be shifted; after the instruction executes, it
contains the shifted operand. The source operand contains the number of bits to
shift or rotate. It can be the immediate value 1 or the CL register. The 8088 and
8086 processors do not accept any other values or registers with these instructions.
Starting with the 80186 processor, you can use 8-bit immediate values larger than 1
as the source operand for shift or rotate instructions, as shown here:
shr bx, 4 ; 9 clocks, 3 bytes on 80286
The following statements are equivalent if the program must run on the 8088 or
8086 processor:
mov cl, 4 ; 2 clocks, 3 bytes on 80286
shr bx, cl ; 9 clocks, 2 bytes on 80286
; 11 clocks, 5 bytes total
Masks for logical instructions can be shifted to new bit positions. For example, an
operand that masks off a bit or group of bits can be shifted to move the mask to a
different position, allowing you to mask off a different bit each time the mask is
used. This technique, illustrated in the following example, is useful only if the mask
value is unknown until run time.
.DATA
masker BYTE 00000010y ; Mask that may change at run time
.CODE
.
.
.
mov cl, 2 ; Rotate two at a time
mov bl, 57h ; Load value to be changed 01010111y
rol masker, cl ; Rotate two to left 00001000y
or bl, masker ; Turn on masked values ---------
; New value is 05Fh 01011111y
rol masker, cl ; Rotate two more 00100000y
or bl, masker ; Turn on masked values ---------
; New value is 07Fh 01111111y
Use SHR (Shift Right) to divide unsigned numbers. You can use SAR (Shift
Arithmetic Right) to divide signed numbers, but SAR rounds negative numbers
down — IDIV always rounds negative numbers up (toward 0). Division using SAR
must adjust for this difference. Multiplication by shifting is the same for signed and
unsigned numbers, so you can use either SAL or SHL.
Multiply and divide instructions are relatively slow, particularly on the 8088 and
8086 processors. When multiplying or dividing by a power of two, use shifts to
speed operations by a factor of 10 or more. For example, these statements take only
four clocks on an 8088 or 8086 processor:
sub ah, ah ; Clear AH
shl ax, 1 ; Multiply byte in AL by 2
The following statements produce the same results, but take between 74 and 81
clocks on the 8088 or 8086 processors. The same statements take 15 clocks on the
80286 and between 11 and 16 clocks on the 80386. (For a discussion about
instruction timings, see “A Word on Instruction Timings” in the Introduction.)
mov bl, 2 ; Multiply byte in AL by 2
mul bl
As the following macro shows, it’s possible to multiply by any number — in this
case, 10 — without resorting to the MUL instruction. However, such a procedure is
no more than an interesting arithmetic exercise, since the additional code almost
certainly takes more time to execute than a single MUL. You should consider using
shifts in your program only when multiplying or dividing by a power of two.
mul_10 MACRO factor ; Factor must be unsigned
mov ax, factor ; Load into AX
shl ax, 1 ; AX = factor * 2
mov bx, ax ; Save copy in BX
shl ax, 1 ; AX = factor * 4
shl ax, 1 ; AX = factor * 8
add ax, bx ; AX = (factor * 8) + (factor * 2)
ENDM ; AX = factor * 10
Here’s another macro that divides by 512. In contrast to the previous example, this
macro uses little code and operates faster than an equivalent DIV instruction.
div_512 MACRO dividend ; Dividend must be unsigned
mov ax, dividend ; Load into AX
shr ax, 1 ; AX = dividend / 2 (unsigned)
xchg al, ah ; XCHG is like rotate right 8
; AL = (dividend / 2) / 256
cbw ; Clear upper byte
ENDM ; AX = (dividend / 512)
If you need to shift a value that is too large to fit in one register, you can shift each
part separately. The RCR (Register Carry Right) and RCL (Register Carry Left)
instructions carry values from the first register to the second by passing the leftmost
or rightmost bit through the carry flag.
This example shifts a multiword value.
.DATA
mem32 DWORD 500000
.CODE
Since the carry flag is treated as part of the operand (it’s like using a 9-bit or 17-bit
operand), the flag value before the operation is crucial. The carry flag can be
adjusted by a previous instruction, but you can also set or clear the flag directly
with the CLC (Clear Carry Flag), CMC (Complement Carry Flag), and STC (Set
Carry Flag) instructions.
On the 80386 and 80486 processors, an alternate method for multiplying quickly by
constants takes advantage of the LEA (Load Effective Address) instruction and the
scaling of indirect memory operands. By using a 32-bit value as both the index and
the base register in an indirect memory operand, you can multiply by the constants
2, 3, 4, 5, 8, and 9 more quickly than you can by using the MUL instruction. LEA
calculates the offset of the source operand and stores it into the destination register,
EBX, as this example shows:
lea ebx, [eax*2] ; EBX = 2 * EAX
lea ebx, [eax*2+eax] ; EBX = 3 * EAX
lea ebx, [eax*4] ; EBX = 4 * EAX
lea ebx, [eax*4+eax] ; EBX = 5 * EAX
lea ebx, [eax*8] ; EBX = 8 * EAX
lea ebx, [eax*8+eax] ; EBX = 9 * EAX
C H A P T E R 5
With the complex data types available in MASM 6.1 — arrays, strings, records,
structures, and unions — you can access data as a unit or as individual elements that
make up a unit. The individual elements of complex data types are often the integer
types discussed in Chapter 4, “Defining and Using Simple Data Types.”
“Arrays and Strings” reviews how to declare, reference, and initialize arrays and
strings. This section summarizes the general steps needed to process arrays and
strings and describes the MASM instructions for moving, comparing, searching,
loading, and storing.
“Structures and Unions” covers similar information for structures and unions: how
to declare structure and union types, how to define structure and union variables,
and how to reference structures and unions and their fields.
“Records” explains how to declare record types, define record variables, and use
record operators.
Initializer lists of array declarations can span multiple lines. The first initializer
must appear on the same line as the data type, all entries must be initialized, and, if
you want the array to continue to the new line, the line must end with a comma.
These examples show legal multiple-line array declarations:
big BYTE 21, 22, 23, 24, 25,
26, 27, 28
If you do not use the LENGTHOF and SIZEOF operators discussed later in this
section, an array may span more than one logical line, although a separate type
declaration is needed on each logical line:
var1 BYTE 10, 20, 30
BYTE 40, 50, 60
BYTE 70, 80, 90
Referencing Arrays
Each element in an array is referenced with an index number, beginning with zero.
The array index appears in brackets after the array name, as in
array[9]
wprime[4] represents the third element (5), which is 4 bytes from the beginning of
the array. Similarly, the expression wprime[6] represents the fourth element (7)
and wprime[10] represents the sixth element (13).
The following example determines an index at run time. It multiplies the position by
two (the size of a word element) by shifting it left:
mov si, cx ; CX holds position number
shl si, 1 ; Scale for word referencing
mov ax, wprime[si] ; Move element into AX
The offset required to access an array element can be calculated with the following
formula:
nth element of array = array[(n-1) * size of element]
Referencing an array element by distance rather than position is not difficult to
master, and is actually very consistent with how assembly language works. Recall
that a variable name is a symbol that represents the contents of a particular address
in memory. Thus, if the array wprime begins at address DS:2400h, the reference
wprime[6] means to the processor “the word value contained in the DS segment
at offset 2400h-plus-6-bytes.”
As described in “Direct Memory Operands,” Chapter 3, you can substitute the plus
operator (+) for brackets, as in:
wprime[9]
wprime+9
Since brackets simply add a number to an address, you don’t need them when
referencing the first element. Thus, wprime and wprime[0] both refer to the first
element of the array wprime.
If your program runs only on an 80186 processor or higher, you can use the
BOUND instruction to verify that an index value is within the bounds of an array.
For a description of BOUND, see the Reference.
For data directives other than BYTE, a string may initialize only the first element.
The initializer value must fit into the specified size and conform to the expression
word size in effect (see “Integer Constants and Constant Expressions” in Chapter
1), as shown in these examples:
wstr WORD "OK"
dstr DWORD "DATA" ; Legal under EXPR32 only
As with arrays, string initializers can span multiple lines. The line must end with a
comma if you want the string to continue to the next line.
str1 BYTE "This is a long string that does not ",
"fit on one line."
Strings must be enclosed in single (') or double (") quotation marks. To put a single
quotation mark inside a string enclosed by single quotation marks, use two single
quotation marks. Likewise, if you need quotation marks inside a string enclosed by
double quotation marks, use two sets. These examples show the various uses of
quotation marks:
char BYTE 'a'
message BYTE "That's the message." ; That's the message.
warn BYTE 'Can''t find file.' ; Can't find file.
string BYTE "This ""value"" not found." ; This "value" not found.
You can always use single quotation marks inside a string enclosed by double
quotation marks, as the initialization for message shows, and vice versa.
The ? Initializer
You do not have to initialize an array. The ? operator lets you allocate space for the
array without placing specific values in it. Object files contain records for
initialized data. Unspecified space left in the object file means that no records
contain initialized data for that address. The actual values stored in arrays allocated
with ? depend on certain conditions. The ? initializer is treated as a zero in a DUP
statement that contains initializers in addition to the ? initializer. If the ? initializer
does not appear in a DUP statement, or if the DUP statement contains only ?
initializers, the assembler leaves the allocated space unspecified.
Processing Strings
The 8086-family instruction set has seven string instructions for fast and efficient
processing of entire strings and arrays. The term “string” in “string instructions”
refers to a sequence of elements, not just character strings. These instructions work
directly only on arrays of bytes and words on the 8086–80486 processors, and on
arrays of bytes, words, and doublewords on the 80386/486 processors. Processing
larger elements must be done indirectly with loops.
The following list gives capsule descriptions of the five instructions discussed in
this section.
Instruction Description
MOVS Copies a string from one location to another
STOS Stores contents of the accumulator register to a string
CMPS Compares one string with another
LODS Loads values from a string to the accumulator register
SCAS Scans a string for a specified value
All of these instructions use registers in a similar way and have a similar syntax.
Most are used with the repeat instruction prefixes REP, REPE (or REPZ), and
REPNE (or REPNZ). REPZ is a synonym for REPE (Repeat While Equal) and
REPNZ is a synonym for REPNE (Repeat While Not Equal).
This section first explains the general procedures for using all string instructions. It
then illustrates each instruction with an example.
If the direction flag is clear, the string is processed upward (from low addresses
to high addresses, which is from left to right through the string). If the direction
flag is set, the string is processed downward (from high addresses to low
addresses, or from right to left). Under MS-DOS, the direction flag is normally
clear if your program has not changed it.
2. Load the number of iterations for the string instruction into the CX register.
If you want to process 100 elements in a string, move 100 into CX. If you wish
the string instruction to terminate conditionally (for example, during a search
when a match is found), load the maximum number of iterations that can be
performed without an error.
3. Load the starting offset address of the source string into DS:SI and the starting
address of the destination string into ES:DI. Some string instructions take only a
destination or source, not both (see Table 5.1).
Normally, the segment address of the source string should be DS, but you can
use a segment override to specify a different segment for the source operand.
You cannot override the segment address for the destination string. Therefore,
you may need to change the value of ES. For information on changing segment
registers, see “Programming Segmented Addresses” in Chapter 3.
Note Although you can use a segment override on the source operand, a segment
override combined with a repeat prefix can cause problems in certain situations on
all processors except the 80386/486. If an interrupt occurs during the string
operation, the segment override is lost and the rest of the string operation processes
incorrectly. Segment overrides can be used safely when interrupts are turned off or
with the 80386/486 processors.
You can adapt these steps to the requirements of any particular string operation.
The syntax for the string instructions is:
[[prefix]] CMPS [[segmentregister:]] source, [[ES:]] destination
LODS [[segmentregister:]] source
[[prefix]] MOVS [[ES:]] destination, [[segmentregister:]] source
[[prefix]] SCAS [[ES:]] destination
[[prefix]] STOS [[ES:]] destination
Some instructions have special forms for byte, word, or doubleword operands. If
you use the form of the instruction that ends in B (BYTE), W (WORD), or D
(DWORD) with LODS, SCAS, and STOS, the assembler knows whether the
element is in the AL, AX, or EAX register. Therefore, these instruction forms do
not require operands.
Table 5.1 lists each string instruction with the type of repeat prefix it uses and
indicates whether the instruction works on a source, a destination, or both.
Table 5.1 Requirements for String Instructions
Instruction Repeat Prefix Source/Destination Register Pair
MOVS REP Both DS:SI, ES:DI
SCAS REPE/REPNE Destination ES:DI
CMPS REPE/REPNE Both DS:SI, ES:DI
LODS None Source DS:SI
STOS REP Destination ES:DI
INS REP Destination ES:DI
OUTS REP Source DS:SI
The repeat prefix causes the instruction that follows it to repeat for the number of
times specified in the count register or until a condition becomes true. After each
iteration, the instruction increments or decrements SI and DI so that it points to the
next array element. The direction flag determines whether SI and DI are
incremented (flag clear) or decremented (flag set). The size of the instruction
determines whether SI and DI are altered by 1, 2, or 4 bytes each time.
Each prefix governs the number of repetitions as follows:
Prefix Description
REP Repeats instruction CX times
REPE, REPZ Repeats instruction maximum CX times while values are equal
REPNE, REPNZ Repeats instruction maximum CX times while values are not equal
The prefixes apply to only one string instruction at a time. To repeat a block of
instructions, use a loop construction. (See “Loops” in Chapter 7.)
At run time, if a string instruction is preceded by a repeat sequence, the processor:
1. Checks the CX register and exits if CX is 0.
2. Performs the string operation once.
3. Increases SI and/or DI if the direction flag is clear. Decreases SI and/or DI if the
direction flag is set. The amount of increase or decrease is 1 for byte operations,
2 for word operations, and 4 for doubleword operations.
4. Decrements CX without modifying the flags.
5. Checks the zero flag (for SCAS or CMPS) if the REPE or REPNE prefix is
used. If the repeat condition holds, loops back to step 1. Otherwise, the loop
ends and execution proceeds to the next instruction.
When the repeat loop ends, SI (or DI) points to the position following a match
(when using SCAS or CMPS), so you need to decrement or increment DI or SI to
point to the element where the last match occurred.
Although string instructions (except LODS) are used most often with repeat
prefixes, they can also be used by themselves. In these cases, the SI and/or DI
registers are adjusted as specified by the direction flag and the size of operands.
Filling Arrays
The STOS instruction stores a specified value in each position of a string. The
string is the destination, so it must be pointed to by ES:DI. The value to store must
be in the accumulator.
The next example stores the character 'a' in each byte of a 100-byte string, filling
the entire string with “aaaa....” Notice how the code stores 50 words rather than
100 bytes. This makes the fill operation faster by reducing the number of iterations.
To fill an odd number of bytes, you need to adjust for the last byte.
.MODEL small, C
.DATA
destin BYTE 100 DUP (?)
ldestin EQU (LENGTHOF destin) / 2
.CODE
. ; Assume ES = DS
.
.
cld ; Work upward
mov ax, 'aa' ; Load character to fill
mov cx, ldestin ; Load length of string
mov di, OFFSET destin ; Load address of destination
rep stosw ; Store 'aa' into array
Comparing Arrays
The CMPS instruction compares two strings and points to the address after which a
match or nonmatch occurs. If the values are the same, the zero flag is set. Either
string can be considered the destination or the source unless a segment override is
used. This example using CMPSB assumes that the strings are in different
segments. Both segments must be initialized to the appropriate segment register.
.MODEL large, C
.DATA
string1 BYTE "The quick brown fox jumps over the lazy dog"
.FARDATA
string2 BYTE "The quick brown dog jumps over the lazy fox"
lstring EQU LENGTHOF string2
.CODE
mov ax, @data ; Load data segment
mov ds, ax ; into DS
mov ax, @fardata ; Load far data segment
mov es, ax ; into ES
.
.
.
cld ; Work upward
mov cx, lstring ; Load length of string
mov si, OFFSET string1 ; Load offset of string1
mov di, OFFSET string2 ; Load offset of string2
repe cmpsb ; Compare
je allmatch ; Jump if all match
.
.
.
allmatch: ; Special case for all match
get:
lodsb ; Get a character
add al, '0' ; Convert to ASCII
mov dl, al ; Move to DL
int 21h ; Call DOS to display character
loop get ; Repeat
Searching Arrays
The SCAS instruction compares the value pointed to by ES:DI with the value in the
accumulator. If both values are the same, it sets the zero flag.
A repeat prefix lets SCAS work on an entire string, scanning (from which SCAS
gets its name) for a particular value called the target. REPNE SCAS sets the zero
flag if it finds the target value in the array. REPE SCAS sets the zero flag if the
scanned array contains nothing but the target value.
This example assumes that ES is not the same as DS and that the address of the
string is stored in a pointer variable. The LES instruction loads the far address of
the string into ES:DI.
.DATA
string BYTE "The quick brown fox jumps over the lazy dog"
pstring PBYTE string ; Far pointer to string
lstring EQU LENGTHOF string ; Length of string
.CODE
.
.
.
cld ; Work upward
mov cx, lstring ; Load length of string
les di, pstring ; Load address of string
mov al, 'z' ; Load character to find
repne scasb ; Search
jne notfound ; Jump if not found
. ; ES:DI points to character
. ; after first 'z'
.
notfound: ; Special case for not found
Although AL cannot contain an index value greater than 255, you can use XLAT
with arrays containing more than 256 elements. Simply treat each 256-byte block of
the array as a smaller sub-array. For example, to retrieve the 260th element of an
array, add 256 to BX and set AL=3 (260-256-1).
You can use the entire structure or union variable or just the individual fields as
operands in assembler statements. This section explains the allocating, initializing,
and nesting of structures and unions.
MASM 6.1 extends the functionality of structures and also makes some changes to
MASM 5.1 behavior. If you prefer, you can retain MASM 5.1 behavior by
specifying OPTION OLDSTRUCTS in your program.
Initializing Fields
If you provide initializers for the fields of a structure or union when you declare the
type, these initializers become the default value for the fields when you define a
variable of that type. “Defining Structure and Union Variables,” following,
explains default initializers.
When you initialize the fields of a union type, the type and value of the first field
become the default value and type for the union. In this example of an initialized
union declaration, the default type for the union is DWORD:
DWB UNION
d DWORD 00FFh
w WORD ?
b BYTE ?
DWB ENDS
If the size of the first member is less than the size of the union, the assembler
initializes the rest of the union to zeros. When initializing strings in a type, make
sure the initial values are long enough to accommodate the largest possible string.
Field Names
Structure and union field names must be unique within a nesting level because they
represent the offset from the beginning of the structure to the corresponding field.
A label elsewhere in the code may have the same name as a structure field, but a
text macro cannot. Also, field names between structures need not be unique. Field
names must be unique if you place OPTION M510 or OPTION OLDSTRUCTS
in your code or use the /Zm option from the command line, since versions of
MASM prior to 6.0 require unique field names. (See Appendix A.)
If neither the alignment nor the /Zp command-line option is used, the offset is
incremented by the size of each data directive. This is the same as a default
alignment equal to 1. The alignment specified in the type declaration overrides the
/Zp command-line option.
These examples show how the assembler determines offsets:
STUDENT2 STRUCT 2 ; Alignment value is 2
score WORD 1 ; Offset = 0
id BYTE 2 ; Offset = 2 (1 byte padding added)
year DWORD 3 ; Offset = 4
sname BYTE 4 ; Offset = 8 (1 byte padding added)
STUDENT2 ENDS
One byte of padding is added at the end of the first byte-sized field. Otherwise, the
offset of the year field would be 3, which is not divisible by the alignment value of
2. The size of this structure is now 9 bytes. Since 9 is not evenly divisible by 2, 1
byte of padding is added at the end of student2.
STUDENT4 STRUCT 4 ; Alignment value is 4
sname BYTE 1 ; Offset = 0 (1 byte padding added)
score WORD 10 DUP (100) ; Offset = 2
year BYTE 2 ; Offset = 22 (1 byte padding
; added so offset of next field
; is divisible by 4)
id DWORD 3 ; Offset = 24
STUDENT4 ENDS
The example defines — that is, allocates space for — four structures of the ITEMS
type. The structures are named Item1 through Item4. Each definition requires the
angle brackets or curly braces even when not initialized. If you initialize more than
one field, separate the values with commas, as shown in Item3 and Item4.
You need not initialize all fields in a structure. If a field is blank, the assembler
uses the structure’s initial value given for that field in the declaration. If there is no
default value, the field value is left unspecified.
For nested structures or unions, however, these are equivalent:
Item5 ITEMS {'Bolts', , }
Item6 ITEMS {'Bolts', , { } }
2 0 2 0 2 0 ... 2 0
If a structure has an initialized string field or an array of bytes, any new string
assigned to a variable of the field that is smaller than the default is padded with
spaces. The assembler adds four spaces at the end of 'Bolts' in the variables of
type ITEMS previously shown. The Iname field in the ITEMS structure cannot
contain a field initializer longer than 'Item Name'.
INFO STRUCT
buffer BYTE 100 DUP (?)
crlf BYTE 13, 10
query BYTE 'Filename: ' ; String <= can override
endmark BYTE 36
drives DISKDRIVES <0, 1, 1>
INFO ENDS
0 1 2 99
? ? ? .... ? 13 10 D i r 36 0 1 1 ...
The initialization for drives gives default values for all three fields of the
structure. The fields left blank in info1 use the default values for those fields. The
info2 declaration is illegal because “DirectoryName” is longer than the initial
string for that field.
The Item7 array defined here has 30 elements of type ITEMS, with the third field
of each element (the union) initialized to 10.
You can also list array elements as shown in the following example.
Item8 ITEMS {'Bolts', 126, 10},
{'Pliers',139, 10},
{'Saws', 414, 10}
Redeclaring a Structure
The assembler generates an error when you declare a structure more than once
unless the following are the same:
◆ Field names
◆ Offsets of named fields
◆ Initialization lists
◆ Field alignment value
This example, using the preceding data declarations, shows how to use the
LENGTHOF, SIZEOF, and TYPE operators with structures.
INFO STRUCT
buffer BYTE 100 DUP (?)
crlf BYTE 13, 10
query BYTE 'Filename: '
endmark BYTE 36
drives DISKDRIVES <0, 1, 1>
INFO ENDS
In the following example, the two MOV statements show how you can access the
elements of an array of unions.
WB UNION
w WORD ?
b BYTE ?
WB ENDS
array.w[12] array.b[32]
As the preceding code illustrates, you can use unions to access the same data in
more than one form. One application of structures and unions is to simplify the task
of reinitializing a far pointer. For a far pointer declared as
FPWORD TYPEDEF FAR PTR WORD
.DATA
WordPtr FPWORD ?
you must follow these steps to point WordPtr to a word value named ThisWord in
the current data segment.
mov WORD PTR WordPtr[2], ds
mov WORD PTR WordPtr, OFFSET ThisWord
The preceding method requires that you remember whether the segment or the offset
is stored first. However, if your program declares a union like this:
uptr UNION
dwptr FPWORD 0
STRUCT
offs WORD 0
segm WORD 0
ENDS
uptr ENDS
This code moves the segment and the offset into the pointer and then moves the
pointer into a register with the other field of the union. Although this technique does
not reduce the code size, it avoids confusion about the order for loading the segment
and offset.
INVENTORY STRUCT
UpDate WORD ?
oldItem ITEMS { \
100,
'AF8' \ ; Named variable of
} ; existing structure
ITEMS { ?, '94C' } ; Unnamed variable of
; existing type
STRUCT ups ; Named nested structure
source WORD ?
shipmode BYTE ?
ENDS
STRUCT ; Unnamed nested structure
f1 WORD ?
f2 WORD ?
ENDS
INVENTORY ENDS
.DATA
yearly INVENTORY { }
To nest structures and unions, you can use any of these techniques:
◆ The field of a structure or union can be a named variable of an existing structure
or union type, as in the oldItem field. Because INVENTORY contains two
structures of type ITEMS , the field names in oldItem are not unique.
Therefore, you must use the full field names when referencing those fields, as in
the statement
mov ax, yearly.oldItem.Inum
◆ To declare a named structure or union inside another structure or union, give the
STRUCT or UNION keyword first and then define a label for it. Fields of the
nested structure or union must always be qualified:
mov yearly.ups.shipmode, 'A'
◆ As shown in the Items field of Inventory, you also can use unnamed
variables of existing structures or unions inside another structure or union. In
these cases, you can reference fields directly:
mov yearly.Inum, 'C'
mov ax, yearly.f1
Records
Records are similar to structures, except that fields in records are bit strings. Each
bit field in a record variable can be used separately in constant operands or
expressions. The processor cannot access bits individually at run time, but it can
access bit fields with instructions that manipulate bits.
Records are bytes, words, or doublewords in which the individual bits or groups of
bits are considered fields. In general, the three steps for using record variables are
the same as those for using other complex data types:
1. Declare a record type.
2. Define one or more variables having the record type.
3. Reference record variables using shifts and masks.
Once it is defined, you can use the record variable as an operand in assembler
statements.
This section explains the record declaration syntax and the use of the MASK and
WIDTH operators. It also shows some applications of record variables and
constants.
7 0
0 0 0 0 0 0 0 0
back fore
blink intense
The next example creates a record type CW that has six fields. Each record declared
with this type occupies 16 bits of memory. Initial (default) values are given for each
field. You can use them when declaring data for the record. The bit diagram after
the example shows the contents of the record type.
CW RECORD r1:3=0, ic:1=0, rc:2=0, pc:2=3, r2:2=1, masks:6=63
15 7 0
0 0 0 0 0 0 1 1 0 1 1 1 1 1 1 1 037Fh
initial values need to be enclosed in parentheses. For example, you can define an
array of record variables with
xmas COLOR 50 DUP ( <1, 2, 0, 4> )
You do not have to initialize all fields in a record. If an initial value is blank, the
assembler automatically stores the default initial value of the field. If there is no
default value, the assembler clears each bit in the field.
The definition in the following example creates a variable named warning whose
type is given by the record type COLOR. The initial values of the fields in the
variable are set to the values given in the record definition. The initial values
override any default record values given in the declaration.
COLOR RECORD blink:1,back:3,intense:1,fore:3 ; Record
; declaration
warning COLOR <1, 0, 1, 4> ; Record
; definition
7 0
1 0 0 0 1 1 0 0 8Ch
back fore
blink intense
; Record instance
; 8 bits stored in 1 byte
RGBCOLOR2 RECORD red:3, green:3, blue:2
rgb RGBCOLOR2 <1, 1, 1> ; Initialize to 00100101y
Record Operators
The WIDTH operator (used only with records) returns the width in bits of a record
or record field. The MASK operator returns a bit mask for the bit positions
occupied by the given record field. A bit in the mask contains a 1 if that bit
corresponds to a bit field. The following example shows how to use MASK and
WIDTH.
.DATA
COLOR RECORD blink:1, back:3, intense:1, fore:3
message COLOR <1, 5, 1, 1>
wblink EQU WIDTH blink ; "wblink" = 1
wback EQU WIDTH back ; "wback" = 3
wintens EQU WIDTH intense ; "wintens" = 1
wfore EQU WIDTH fore ; "wfore" = 3
wcolor EQU WIDTH COLOR ; "wcolor" = 8
.CODE
.
.
.
mov ah, message ; Load initial 1101 1001
and ah, NOT MASK back ; Turn off AND 1000 1111
; "back" ---------
; 1000 1001
or ah, MASK blink ; Turn on OR 1000 0000
; "blink" ---------
; 1000 1001
xor ah, MASK intense ; Toggle XOR 0000 1000
; "intense" ---------
; 1000 0001
The example continues by illustrating several ways in which record fields can serve
as operands and expressions:
; Rotate "back" of "message" without changing other values
Record variables are often used with the logical operators to perform logical
operations on the bit fields of the record, as in the previous example using the
MASK operator.
C H A P T E R 6
MASM requires different techniques for handling floating-point (real) numbers and
binary coded decimal (BCD) numbers than for handling integers. You have two
choices for working with real numbers — a math coprocessor or emulation routines.
Math coprocessors — the 8087, 80287, and 80387 chips — work with the main
processor to handle real-number calculations. The 80486 processor performs
floating-point operations directly. All information in this chapter pertaining to the
80387 coprocessor applies to the 80486DX processor as well. It does not apply to
the 80486SX, which does not provide an on-chip coprocessor.
This chapter begins with a summary of the directives and formats of floating-point
data that you need to allocate memory storage and initialize variables before you
can work with floating-point numbers.
The chapter then explains how to use a math coprocessor for floating-point
operations. It covers:
◆ The architecture of the registers.
◆ The operands for the coprocessor instruction formats.
◆ The coordination of coprocessor and main processor memory access.
◆ The basic groups of coprocessor instructions — for loading and storing data,
doing arithmetic calculations, and controlling program flow.
The next main section describes emulation libraries. The emulation routines
provided with all Microsoft high-level languages enable you to use coprocessor
instructions as though your computer had a math coprocessor. However, some
coprocessor instructions are not handled by emulation, as this section explains.
Finally, because math coprocessor and emulation routines can also operate on BCD
numbers, this chapter includes the instruction set for these numbers.
Table 6.1 lists the possible ranges for floating-point variables. The number of
significant digits can vary in an arithmetic operation as the least-significant digit
may be lost through rounding errors. This occurs regularly for short and long real
numbers, so you should assume the lesser value of significant digits shown in Table
6.1. Ten-byte real numbers are much less susceptible to rounding errors for reasons
described in the next section. However, under certain circumstances, 10-byte real
operations can have a precision of only 18 digits.
Table 6.1 Ranges of Floating-Point Variables
Significant
Data Type Bits Digits Approximate Range
Short real 32 6–7 1.18 x 10-38 to 3.40 x 1038
Long real 64 15–16 2.23 x 10-308 to 1.79 x 10308
10-byte real 80 19 3.37 x 10-4932 to 1.18 x 104932
With versions of MASM prior to 6.0, the DD, DQ, and DT directives could
allocate real constants. MASM 6.1 still supports these directives, but the variables
are integers rather than floating-point values. Although this makes no difference in
the assembly code, CodeView displays the values incorrectly.
You can specify floating-point constants either as decimal constants or as encoded
hexadecimal constants. You can express decimal real-number constants in the form:
[[+ | –]] integer[[fraction]][[E[[+ | –]]exponent]]
For example, the numbers 2.523E1 and -3.6E-2 are written in the correct
decimal format. You can use these numbers as initializers for real-number
variables.
The assembler always evaluates digits of real numbers as base 10. It converts real-
number constants given in decimal format to a binary format. The sign, exponent,
and decimal part of the real number are encoded as bit fields within the number.
You can also specify the encoded format directly with hexadecimal digits (0–9 plus
A–F). The number must begin with a decimal digit (0–9) and end with the real-
number designator (R). It cannot be signed. For example, the hexadecimal number
3F800000r can serve as an initializer for a doubleword-sized variable.
The maximum range of exponent values and the number of digits required in the
hexadecimal number depend on the directive. The number of digits for encoded
numbers used with REAL4, REAL8, and REAL10 must be 8, 16, and 20 digits,
respectively. If the number has a leading zero, the number must be 9, 17, or 21
digits.
Examples of decimal constant and hexadecimal specifications are shown here:
; Real numbers
short REAL4 25.23 ; IEEE format
double REAL8 2.523E1 ; IEEE format
tenbyte REAL10 2523.0E-2 ; 10-byte real format
; Encoded as hexadecimals
ieeeshort REAL4 3F800000r ; 1.0 as IEEE short
ieeedouble REAL8 3FF0000000000000r ; 1.0 as IEEE long
temporary REAL10 3FFF8000000000000000r ; 1.0 as 10-byte
; real
-1.110101 x 2 1011
Sign Exponent
Integer part Decimal part
The following list explains how the parts of a real number are stored in the IEEE
format. Each item in the list refers to an item in Figure 6.1.
◆ Sign bit (0 for positive or 1 for negative) in the upper bit of the first byte.
◆ Exponent in the next bits in sequence (8 bits for a short real number, 11 bits for
a long real number, and 15 bits for a 10-byte real number).
◆ The integer part of the significand in bit 63 for the 10-byte real format. By
absorbing carry values, this bit allows 10-byte real operations to preserve
precision to 19 digits. The integer part is always 1 in short and long real
numbers; consequently, these formats do not provide a bit for the integer, since
there is no point in storing it.
◆ Decimal part of the significand in the remaining bits. The length is 23 bits for
short real numbers, 52 bits for long real numbers, and 63 bits for 10-byte real
numbers.
Coprocessor Architecture
The coprocessor accesses memory as the CPU does, but it has its own data and
control registers — eight data registers organized as a stack and seven control
registers similar to the 8086 flag registers. The coprocessor’s instruction set
provides direct access to these registers.
The eight 80-bit data registers of the 8087-based coprocessors are organized as a
stack, although they need not be used as a stack. As data items are pushed into the
top register, previous data items move into higher-numbered registers, which are
lower on the stack. Register 0 is the top of the stack; register 7 is the bottom. The
syntax for specifying registers is:
ST [[(number)]]
The number must be a digit between 0 and 7 or a constant expression that evaluates
to a number from 0 to 7. ST is another way to refer to ST(0).
All coprocessor data is stored in registers in the 10-byte real format. The registers
and the register format are shown in Figure 6.2.
79 63 0
ST
ST(1)
ST(2)
ST(3)
ST(4)
ST(5)
ST(6)
ST(7)
Internally, all calculations are done on numbers of the same type. Since 10-byte real
numbers have the greatest precision, lower-precision numbers are guaranteed not to
lose precision as a result of calculations. The instructions that transfer values
between the main memory and the coprocessor automatically convert numbers to
and from the 10-byte real format.
You can easily recognize coprocessor instructions because, unlike all 8086-family
instruction mnemonics, they start with the letter F. Coprocessor instructions can
never have immediate operands and, with the exception of the FSTSW instruction,
they cannot have processor registers as operands.
Classical-Stack Format
Instructions in the classical-stack format treat the coprocessor registers like items
on a stack — thus its name. Items are pushed onto or popped off the top elements of
the stack. Since only the top item can be accessed on a traditional stack, there is no
need to specify operands. The first (top) register (and the second, if the instruction
needs two operands) is always assumed.
ST (the top of the stack) is the source operand in coprocessor arithmetic operations.
ST(1), the second register, is the destination. The result of the operation replaces
the destination operand, and the source is popped off the stack. This leaves the
result at the top of the stack.
The following example illustrates the classical-stack format; Figure 6.3 shows the
status of the register stack after each instruction.
fld1 ; Push 1 into first position
fldpi ; Push pi into first position
fadd ; Add pi and 1 and pop
Memory Format
Instructions that use the memory format, such as data transfer instructions, also
treat coprocessor registers like items on a stack. However, with this format, items
are pushed from memory onto the top element of the stack, or popped from the top
element to memory. You must specify the memory operand.
Some instructions that use the memory format specify how a memory operand is to
be interpreted — as an integer (I) or as a binary coded decimal (B). The letter I or B
follows the initial F in the syntax. For example, FILD interprets its operand as an
integer and FBLD interprets its operand as a BCD number. If the instruction name
does not include a type letter, the instruction works on real numbers.
You can also use memory operands in calculation instructions that operate on two
values (see “Using Coprocessor Instructions,” later in this section). The memory
operand is always the source. The stack top (ST) is always the implied destination.
The result of the operation replaces the destination without changing its stack
position, as shown in this example and in Figure 6.4:
.DATA
m1 REAL4 1.0
m2 REAL4 2.0
.CODE
.
.
.
fld m1 ; Push m1 into first position
fld m2 ; Push m2 into first position
fadd m1 ; Add m2 to first position
fstp m1 ; Pop first position into m1
fst m2 ; Copy first position to m2
Register Format
Instructions that use the register format treat coprocessor registers as registers
rather than as stack elements. Instructions that use this format require two register
operands; one of them must be the stack top (ST).
In the register format, specify all operands by name. The first operand is the
destination; its value is replaced with the result of the operation. The second
operand is the source; it is not affected by the operation. The stack positions of the
operands do not change.
The only instructions that use the register operand format are the FXCH instruction
and arithmetic instructions for calculations on two values. With the FXCH
instruction, the stack top is implied and need not be specified, as shown in this
example and in Figure 6.5:
fadd st(1), st ; Add second position to first -
; result goes in second position
fadd st, st(2) ; Add first position to third -
; result goes in first position
fxch st(1) ; Exchange first and second positions
Register-Pop Format
The register-pop format treats coprocessor registers as a modified stack. The source
register must always be the stack top. Specify the destination with the register’s
name.
Instructions with this format place the result of the operation into the destination
operand, and the top pops off the stack. The register-pop format is used only for
instructions for calculations on two values, as in this example and in Figure 6.6:
faddp st(2), st ; Add first and third positions and pop -
; first position destroyed;
; third moves to second and holds result
faddp st(2),st
ST 1.0 2.0
ST(1) 2.0 4.0
ST(2) 3.0
Step 2, processing the data, can occur while the main processor is handling other
tasks. Steps 1 and 3 must be coordinated with the main processor so that the
processor and coprocessor do not try to access the same memory at the same time;
otherwise, problems of coordinating memory access can occur. Since the processor
and coprocessor work independently, they may not finish working on memory in the
order in which you give instructions. The two potential timing conflicts that can
occur are handled in different ways.
One timing conflict results from a coprocessor instruction following a processor
instruction. The processor may have to wait until the coprocessor finishes if the next
processor instruction requires the result of the coprocessor’s calculation. You do
not have to write your code to avoid this conflict, however. The assembler
coordinates this timing automatically for the 8088 and 8086 processors, and the
processor coordinates it automatically on the 80186–80486 processors. This is the
case shown in the first example that follows.
Another conflict results from a processor instruction that accesses memory
following a coprocessor instruction that accesses the same memory. The processor
can try to load a variable that is still being used by the coprocessor. You need
careful synchronization to control the timing, and this synchronization is not
automatic on the 8087 coprocessor. For code to run correctly on the 8087, you must
include WAIT or FWAIT (mnemonics for the same instruction) to ensure that the
coprocessor finishes before the processor begins, as shown in the second example.
In this situation, the processor does not generate the FWAIT instruction
automatically.
; Processor instruction first - No wait needed
mov WORD PTR mem32[0], ax ; Load memory
mov WORD PTR mem32[2], dx
fild mem32 ; Load to register
When generating code for the 8087 coprocessor, the assembler automatically
inserts a WAIT instruction before the coprocessor instruction. However, if you use
the .286 or .386 directive, the compiler assumes that the coprocessor instructions
are for the 80287 or 80387 and does not insert the WAIT instruction. If your code
does not need to run on an 8086 or 8088 processor, you can make your programs
smaller and more efficient by using the .286 or .386 directive.
The following sections explain the available instructions and show how to use them
for each of these operations. For general syntax information, see “Instruction and
Operand Formats,” earlier in this section.
You can transfer data to stack registers using load commands. These commands
push data onto the stack from memory or from coprocessor registers. Store
commands remove data. Some store commands pop data off the register stack into
memory or coprocessor registers; others simply copy the data without changing it
on the stack.
If you use constants as operands, you cannot load them directly into coprocessor
registers. You must allocate memory and initialize a variable to a constant value.
That variable can then be loaded by using one of the load instructions in the
following list.
The math coprocessor offers a few special instructions for loading certain constants.
You can load 0, 1, pi, and several common logarithmic values directly. Using these
instructions is faster and often more precise than loading the values from initialized
variables.
All instructions that load constants have the stack top as the implied destination
operand. The constant to be loaded is the implied source operand.
The coprocessor data area, or parts of it, can also be moved to memory and later
loaded back. You may want to do this to save the current state of the coprocessor
before executing a procedure. After the procedure ends, restore the previous status.
Saving coprocessor data is also useful when you want to modify coprocessor
behavior by writing certain data to main memory, operating on the data with 8086-
family instructions, and then loading it back to the coprocessor data area.
Use the following instructions for transferring numbers to and from
registers:
Instruction(s) Description
FLD, FST, FSTP Loads and stores real numbers
FILD, FIST, FISTP Loads and stores binary integers
FBLD Loads BCD
FBSTP Stores BCD
FXCH Exchanges register values
FLDZ Pushes 0 into ST
FLD1 Pushes 1 into ST
FLDPI Pushes the value of pi into ST
FLDCW mem2byte Loads the control word into the coprocessor
F[[N]]STCW mem2byte Stores the control word in memory
FLDENV mem14byte Loads environment from memory
F[[N]]STENV mem14byte Stores environment in memory
Instruction(s) Description
FRSTOR mem94byte Restores state from memory
F[[N]]SAVE mem94byte Saves state in memory
FLDL2E Pushes the value of log2e into ST
FLDL2T Pushes log210 into ST
FLDLG2 Pushes log102 into ST
FLDLN2 Pushes loge2 into ST
The following example and Figure 6.7 illustrate some of these instructions:
.DATA
m1 REAL4 1.0
m2 REAL4 2.0
.CODE
fld m1 ; Push m1 into first item
fld st(2) ; Push third item into first
fst m2 ; Copy first item to m2
fxch st(2) ; Exchange first and third items
fstp m1 ; Pop first item into m1
Main Memory
fld m1 fld st(2) fst m2 fxch st(2) fstp m1
Coprocessor Registers
Figure 6.7 Status of the Register Stack: Main Memory and Coprocessor
instruction if both operands are stack registers, since register values are always 10-
byte real numbers. In most of the arithmetic instructions listed here, the result
replaces the destination register. The instructions include:
Instruction Description
FADD Adds the source and destination
FSUB Subtracts the source from the destination
FSUBR Subtracts the destination from the source
FMUL Multiplies the source and the destination
FDIV Divides the destination by the source
FDIVR Divides the source by the destination
FABS Sets the sign of ST to positive
FCHS Reverses the sign of ST
FRNDINT Rounds ST to an integer
FSQRT Replaces the contents of ST with its square root
FSCALE Multiplies the stack-top value by 2 to the power contained in ST(1)
FPREM Calculates the remainder of ST divided by ST(1)
80387 Only
Instruction Description
FSIN Calculates the sine of the value in ST
FCOS Calculates the cosine of the value in ST
FSINCOS Calculates the sine and cosine of the value in ST
FPREM1 Calculates the partial remainder by performing modulo division on the
top two stack registers
FXTRACT Breaks a number down into its exponent and mantissa and pushes the
mantissa onto the register stack
F2XM1 Calculates 2x–1
FYL2X Calculates Y * log2 X
FYL2XP1 Calculates Y * log2 (X+1)
FPTAN Calculates the tangent of the value in ST
FPATAN Calculates the arctangent of the ratio Y/X
F[[N]]INIT Resets the coprocessor and restores all the default conditions in the
control and status words
F[[N]]CLEX Clears all exception flags and the busy flag of the status word
FINCSTP Adds 1 to the stack pointer in the status word
FDECSTP Subtracts 1 from the stack pointer in the status word
FFREE Marks the specified register as empty
The following example illustrates several arithmetic instructions. The code solves
quadratic equations, but does no error checking and fails for some values because it
attempts to find the square root of a negative number. Both Help and the
MATH.ASM sample file show a complete version of this procedure. The complete
form uses the FTST (Test for Zero) instruction to check for a negative number or 0
before calculating the square root.
.DATA
a REAL4 3.0
b REAL4 7.0
cc REAL4 2.0
posx REAL4 0.0
negx REAL4 0.0
.CODE
.
.
.
; Solve quadratic equation - no error checking
; The formula is: -b +/- squareroot(b2 - 4ac) / (2a)
fld1 ; Get constants 2 and 4
fadd st,st ; 2 at bottom
fld st ; Copy it
fmul a ; = 2a
fmul st(1),st ; = 4a
fxch ; Exchange
fmul cc ; = 4ac
fld b ; Load b
fmul st,st ; = b2
fsubr ; = b2 - 4ac
; Negative value here produces error
fsqrt ; = square root(b2 - 4ac)
fld b ; Load b
fchs ; Make it negative
fxch ; Exchange
The SAHF (Store AH into Flags) instruction in this example transfers AH into the
low bits of the flags register.
You can save several steps by loading the status word directly to AX on the 80287
with the FSTSW and FNSTSW instructions. This is the only case in which data
can be transferred directly between processor and coprocessor registers, as shown
in this example:
fstsw ax
The coprocessor control flags and their relationship to the status word are described
in “Control Registers,” following.
The 8087-family coprocessors provide several instructions for comparing operands
and testing control flags. All these instructions compare the stack top (ST) to a
source operand, which may either be specified or implied as ST(1).
The compare instructions affect the C3, C2, and C0 control flags, but not the C1
flag. Table 6.3 shows the flags’ settings for each possible result of a comparison or
test.
Table 6.3 Control-Flag Settings After Comparison or Test
After FCOM After FTEST C3 C2 C0
ST > source ST is positive 0 0 0
ST < source ST is negative 0 0 1
ST = source ST is 0 1 0 0
Not comparable ST is NAN or projective infinity 1 1 1
Variations on the compare instructions allow you to pop the stack once or twice and
to compare integers and zero. For each instruction, the stack top is always the
Additional instructions for the 80387/486 are FLDENVD and FLDENVW for
loading the environment; FNSTENVD, FNSTENVW, FSTENVD, and
FSTENVW for storing the environment state; FNSAVED, FNSAVEW,
FSAVED, and FSAVEW for saving the coprocessor state; and FRSTORD and
FRSTORW for restoring the coprocessor state.
The size of the code segment, not the operand size, determines the number of bytes
loaded or stored with these instructions. The instructions ending with W store the
16-bit form of the control register data, and the instructions ending with D store the
32-bit form. For example, in 16-bit mode FSAVEW saves the 16-bit control
register data. If you need to store the 32-bit form of the control register data, use
FSAVED.
Control Registers
Some of the flags of the seven 16-bit control registers control coprocessor
operations, while others maintain the current status of the coprocessor. In this sense,
they are much like the 8086-family flags registers (see Figure 6.8).
Control Registers
Control Word
Status Word
Tag Word
Instruction Pointer
Operand Pointer
The status word register is the only commonly used control register. (The others are
used mostly by systems programmers.) The format of the status word register is
shown in Figure 6.9, which shows how the coprocessor control flags align with the
processor flags. C3 overwrites the zero flag, C2 overwrites the parity flag, and C0
overwrites the carry flag. C1 overwrites an undefined bit, so it cannot be used
directly with conditional jumps, although you can use the TEST instruction to
check C1 in memory or in a register. The status word register also overwrites the
sign and auxiliary-carry flags, so you cannot count on their being unchanged after
the operation.
Status Word
15 8
C3 C2 C1 C0
Flags
7 0
SF ZF AF PF CF
Emulator libraries do not allow for all of the coprocessor instructions. The
following floating-point instructions are not emulated:
FBLD FLDENV FSAVE FSTENV
FBSTP FNOP FSAVEW FUCOM
FCOS FPREM1 FSAVED FUCOMP
FDECSTP FRSTOR FSETPM FUCOMPP
FINCSTP FRSTORW FSIN FXTRACT
FINIT FRSTORD FSINCOS
For information about writing assembly-language procedures for high-level
languages, see Chapter 12, “Mixed-Language Programming.”
Unpacked BCD numbers are stored one digit to a byte, with the value in the lower
4 bits. They can be defined using the BYTE directive. For example, an unpacked
BCD number could be defined and initialized as follows:
unpackedr BYTE 1,5,8,2,5,2,9 ; Initialized to 9,252,851
unpackedf BYTE 9,2,5,2,8,5,1 ; Initialized to 9,252,851
As these two lines show, you can arrange digits backward or forward, depending on
how you write the calculation routines that handle the numbers.
pushes the packed BCD number at bcd1 onto the coprocessor stack. When your
code completes calculations on the number, place the result back into memory in
BCD format with the instruction
fbstp bcd1
For processor arithmetic on unpacked BCD numbers, you must do the 8-bit
arithmetic calculations on each digit separately, and assign the result to the AL
register. After each operation, use the corresponding BCD instruction to adjust the
result. The ASCII-adjust instructions do not take an operand and always work on
the value in the AL register.
The following examples show how to use each of these instructions in BCD
addition, subtraction, multiplication, and division.
; To add 9 and 3 as BCDs:
mov ax, 9 ; Load 9
mov bx, 3 ; and 3 as unpacked BCDs
add al, bl ; Add 09h and 03h to get 0Ch
aaa ; Adjust 0Ch in AL to 02h,
; increment AH to 01h, set carry
; Result 12 (unpacked BCD in AX)
; To multiply 9 times 3:
mov ax, 903h ; Load 9 and 3 as unpacked BCDs
mul ah ; Multiply 9 and 3 to get 1Bh
aam ; Adjust 1Bh in AL
; to get 27 (unpacked BCD in AX)
; To divide 25 by 2:
mov ax, 205h ; Load 25
mov bl, 2 ; and 2 as unpacked BCDs
aad ; Adjust 0205h in AX
; to get 19h in AX
div bl ; Divide by 2 to get
; quotient 0Ch in AL
; remainder 1 in AH
aam ; Adjust 0Ch in AL
; to 12 (unpacked BCD in AX)
; (remainder destroyed)
If you process multidigit BCD numbers in loops, each digit is processed and
adjusted in turn.
For processor calculations on packed BCD numbers, you must do the 8-bit
arithmetic calculations on each byte separately, placing the result in the AL
register. After each operation, use the corresponding decimal-adjust instruction to
adjust the result. The decimal-adjust instructions do not take an operand and always
work on the value in the AL register.
The 8086-family processors provide the instructions DAA (Decimal Adjust after
Addition) and DAS (Decimal Adjust after Subtraction) for adjusting packed BCD
numbers after addition and subtraction.
These examples use DAA and DAS to add and subtract BCDs.
;To add 88 and 33:
mov ax, 8833h ; Load 88 and 33 as packed BCDs
add al, ah ; Add 88 and 33 to get 0BBh
daa ; Adjust 0BBh to 121 (packed BCD:)
; 1 in carry and 21 in AL
C H A P T E R 7
Very few programs execute all lines sequentially from .STARTUP to .EXIT.
Rather, complex program logic and efficiency dictate that you control the flow of
your program — jumping from one point to another, repeating an action until a
condition is reached, and passing control to and from procedures. This chapter
describes various ways for controlling program flow and several features that
simplify coding program-control constructs.
The first section covers jumps from one point in the program to another. It explains
how MASM 6.1 optimizes both unconditional and conditional jumps under certain
circumstances, so that you do not have to specify every attribute. The section also
describes instructions you can use to test conditional jumps.
The next section describes loop structures that repeat actions or evaluate conditions.
It discusses MASM directives, such as .WHILE and .REPEAT, that generate
appropriate compare, loop, and jump instructions for you, and the .IF, .ELSE, and
.ELSEIF directives that generate jump instructions.
The “Procedures” section in this chapter explains how to write an assembly-
language procedure. It covers the extended functionality for PROC, a PROTO
directive that lets you write procedure prototypes similar to those used in C, an
INVOKE directive that automates parameter passing, and options for the stack-
frame setup inside procedures.
The last section explains how to pass program control to an interrupt routine.
Jumps
Jumps are the most direct way to change program control from one location to
another. At the processor level, jumps work by changing the value of the IP
(Instruction Pointer) register to a target offset and, for far jumps, by changing the
CS register to a new segment address. Jump instructions fall into only two
categories: conditional and unconditional.
Unconditional Jumps
The JMP instruction transfers control unconditionally to another instruction.
JMP’s single operand contains the address of the target instruction.
Unconditional jumps skip over code that should not be executed, as shown here:
; Handle one case
label1: .
.
.
jmp continue
The distance of the target from the jump instruction and the size of the operand
determine the assembler’s encoding of the instruction. The longer the distance, the
more bytes the assembler uses to code the instruction. In versions of MASM prior
to 6.0, unconditional NEAR jumps sometimes generated inefficient code, but
MASM can now optimize unconditional jumps.
Jump Optimizing
The assembler determines the smallest encoding possible for the direct
unconditional jump. MASM does not require a distance operator, so you do not
have to determine the correct distance of the jump. If you specify a distance, it
overrides any assembler optimization. If the specified distance falls short of the
target address, the assembler generates an error. If the specified distance is longer
than the jump requires, the assembler encodes the given distance and does not
optimize it.
The assembler optimizes jumps when the following conditions are met:
◆ You do not specify SHORT, NEAR, FAR, NEAR16, NEAR32, FAR16,
FAR32, or PROC as the distance of the target.
◆ The target of the jump is not external and is in the same segment as the jump
instruction. If the target is in a different segment (but in the same group), it is
treated as though it were external.
If these two conditions are met, MASM uses the instruction, distance, and size of
the operand to determine how to optimize the encoding for the jump. No syntax
changes are necessary.
Note This information about jump optimizing also applies to conditional jumps on
the 80386/486.
Indirect Operands
An indirect operand provides a pointer to the target address, rather than the address
itself. A pointer is a variable that contains an address. The processor distinguishes
indirect (pointer) operands from direct (address) operands by the instruction’s
context.
You can specify the pointer’s size with the WORD, DWORD, or FWORD
attributes. Default sizes are based on .MODEL and the default segment size.
jmp [bx] ; Uses .MODEL and segment size defaults
jmp WORD PTR [bx] ; A NEAR16 indirect call
If the indirect operand is a register, the jump is always a NEAR16 jump for a 16-
bit register, and NEAR32 for a 32-bit register:
jmp bx ; NEAR16 jump
jmp ebx ; NEAR32 jump
In this case, your code must clear the ambiguity with the NEAR32 or FAR16
keywords. The following example shows how to use TYPEDEF to define
NEAR32 and FAR16 pointer types.
NFP TYPEDEF PTR NEAR32
FFP TYPEDEF PTR FAR16
jmp NFP PTR [var] ; NEAR32 indirect jump
jmp FFP PTR [var] ; FAR16 indirect jump
You can use an unconditional jump as a form of conditional jump by specifying the
address in a register or indirect memory operand. Also, you can use indirect
memory operands to construct jump tables that work like C switch statements,
Pascal CASE statements, or Basic ON GOTO, ON GOSUB, or SELECT CASE
statements, as shown in the following example.
extended:
mov ah, 8h ; Get second key of extended key
int 21h
. ; Use another jump table
. ; for extended keys
.
jmp next
ctrla: . ; CONTROL-A code here
.
.
jmp next
ctrlb: . ; CONTROL-B code here
.
.
jmp next
.
.
next: . ; Continue
In this instance, the indirect memory operands point to addresses of routines for
handling different keystrokes.
Conditional Jumps
The most common way to transfer control in assembly language is to use a
conditional jump. This is a two-step process:
1. First test the condition.
2. Then jump if the condition is true or continue if it is false.
All conditional jumps except two (JCXZ and JECXZ) use the processor flags for
their criteria. Thus, any statement that sets or clears a flag can serve as a test basis
for a conditional jump. The jump statement can be any one of 30 conditional-jump
instructions. A conditional-jump instruction takes a single operand containing the
target address. You cannot use a pointer value as a target as you can with
unconditional jumps.
The last two jumps in the list, JPE (Jump if Parity Even) and JPO (Jump if Parity
Odd), are useful only for communications programs. The processor sets the parity
flag if an operation produces a result with an even number of set bits. A
communications program can compare the flag against the parity bit received
through the serial port to test for transmission errors.
The conditional jumps in the preceding list can follow any instruction that changes
the processor flags, as these examples show:
; Uses JO to handle overflow condition
add ax, bx ; Add two values
jo overflow ; If value too large, adjust
As the second example shows, the jump does not have to immediately follow the
instruction that alters the flags. Since MOV does not change the flags, it can appear
between the SUB instruction and the dependent jump.
There are three categories of conditional jumps:
◆ Comparison of two values
◆ Individual bit settings in a value
◆ Whether a value is zero or nonzero
You can compare signed or unsigned values, but you must choose the subsequent
conditional jump to reflect the correct value type. For example, JL (Jump if Less
Than) and JB (Jump if Below) may seem conceptually similar, but a failure to
understand the difference between them can result in program bugs. Table 7.1
shows the correct conditional jumps for comparisons of signed and unsigned values.
The table shows the zero, carry, sign, and overflow flags as ZF, CF, SF, and OF,
respectively.
Table 7.1 Conditional Jumps Based on Comparisons of Two Values
Signed Comparisons Unsigned Comparisons
Instruction Jump if True Instruction Jump if True
JE ZF = 1 JE ZF = 1
JNE ZF = 0 JNE ZF = 0
JG/JNLE ZF = 0 and SF = OF JA/JNBE CF = 0 and ZF = 0
JLE/JNG ZF = 1 or SF ≠ OF JBE/JNA CF = 1 or ZF = 1
JL/JNGE SF ≠ OF JB/JNAE CF = 1
JGE/JNL SF = OF JAE/JNB CF = 0
The mnemonic names of jumps always refer to the comparison of CMP’s first
operand (destination) with the second operand (source). For instance, in this
example, JG tests whether the first operand is greater than the second.
cmp ax, bx ; Compare AX and BX
jg next1 ; Equivalent to: If ( AX > BX ) goto next1
jl next2 ; Equivalent to: If ( AX < BX ) goto next2
The TEST instruction is the same as the AND instruction, except that TEST
changes neither operand. The following example shows an application of TEST.
.DATA
bits BYTE ?
.CODE
.
.
.
; If bit 2 or bit 4 is set, then call task_a
; Assume "bits" is 0D3h 11010011
test bits, 10100y ; If 2 or 4 is set AND 00010100
jz skip1 ; --------
call task_a ; Then call task_a 00010000
skip1: ; Jump taken
.
.
.
; If bits 2 and 4 are clear, then call task_b
; Assume "bits" is 0E9h 11101001
test bits, 10100y ; If 2 and 4 are clear AND 00010100
jnz skip2 ; --------
call task_b ; Then call task_b 00000000
skip2: ; Jump taken
The source operand for TEST is often a mask in which the test bits are the only bits
set. The destination operand contains the value to be tested. If all the bits set in the
mask are clear in the destination operand, TEST sets the zero flag. If any of the
flags set in the mask are also set in the destination operand, TEST clears the zero
flag.
The 80386/486 processors provide additional bit-testing instructions. The BT (Bit
Test) series of instructions copy a specified bit from the destination operand to the
carry flag. A JC or JNC can then route program flow depending on the result. For
variations on the BT instruction, see the Reference.
but produces smaller and faster code, since it does not use an immediate number as
an operand. The same technique also lets you test a register’s sign bit:
or dx, dx ; Is DX sign bit set?
js sign_set ; Jump if so
Jump Extending
Unlike an unconditional jump, a conditional jump cannot reference a label more
than 128 bytes away. For example, the following statement is valid as long as
target is within a distance of 128 bytes:
; Jump to target less than 128 bytes away
jz target ; If previous operation resulted
; in zero, jump to target
MASM can automate jump-extending for you. If you target a conditional jump to a
label farther than 128 bytes away, MASM rewrites the instruction with an
unconditional jump, which ensures that the jump can reach its target. If target lies
within a 128-byte range, the assembler encodes the instruction jz target as is.
Otherwise, MASM generates two substitute instructions:
jne $ + 2 + (length in bytes of the next instruction)
jmp NEAR PTR target
The assembler generates this same code sequence if you specify the distance with
NEAR PTR, FAR PTR, or SHORT. Therefore,
jz NEAR PTR target
becomes
jne $ + 5
jmp NEAR PTR target
MASM enables automatic jump expansion by default, but you can turn it off with
the NOLJMP form of the OPTION directive. For information about the OPTION
directive, see page 24.
If the assembler generates code to extend a conditional jump, it issues a level 3
warning saying that the conditional jump has been lengthened. You can set the
warning level to 1 for development and to level 3 for a final optimizing pass to see
if you can shorten jumps by reorganizing.
If you specify the distance for the jump and the target is out of range for that
distance, a “Jump out of Range” error results.
Since the JCXZ and JECXZ instructions do not have logical negations, expansion
of the jump instruction to handle targets with unspecified distances cannot be
performed for those instructions. Therefore, the distance must always be short.
The size and distance of the target operand determines the encoding for conditional
or unconditional jumps to externals or targets in different segments. The jump-
extending and optimization features do not apply in this case.
Note Conditional jumps on the 80386 and 80486 processors can be to targets up to
32K away, so jump extension occurs only for targets greater than that distance.
Anonymous Labels
When you code jumps in assembly language, you must invent many label names.
One alternative to continually thinking up new label names is to use anonymous
labels, which you can use anywhere in your program. But because anonymous
labels do not provide meaningful names, they are best used for jumping over only a
few lines of code. You should mark major divisions of a program with actual named
labels.
Use two at signs (@@) followed by a colon (:) as an anonymous label. To jump to
the nearest preceding anonymous label, use @B (back) in the jump instruction’s
operand field; to jump to the nearest following anonymous label, use @F (forward)
in the operand field.
The jump in the following example targets an anonymous label:
jge @F
.
.
.
@@:
The items @B and @F always refer to the nearest occurrences of @@:, so there is
never any conflict between different anonymous labels.
Decision Directives
The high-level structures you can use for decision-making are the .IF, .ELSEIF,
and .ELSE statements. These directives generate conditional jumps. The expression
following the .IF directive is evaluated, and if true, the following instructions are
executed until the next .ENDIF, .ELSE, or .ELSEIF directive is reached. The
.ELSE statements execute if the expression is false. Using the .ELSEIF directive
puts a new expression inside the alternative part of the original .IF statement to be
evaluated. The syntax is:
.IF condition1
statements
[[.ELSEIF condition2
statements]]
[[.ELSE
statements]]
.ENDIF
The decision structure
.IF cx == 20
mov dx, 20
.ELSE
mov dx, 30
.ENDIF
Loops
Loops repeat an action until a termination condition is reached. This condition can
be a counter or the result of an expression’s evaluation. MASM 6.1 offers many
ways to set up loops in your programs. The following list compares MASM loop
structures:
Instructions Action
LOOP Automatically decrements CX. When CX = 0, the loop ends. The
top of the loop cannot be greater than 128 bytes from the LOOP
instruction. (This is true for all LOOP instructions.)
LOOPE/LOOPZ, Loops while equal or not equal. Checks both CX and the state of
LOOPNE/LOOPNZ the zero flag. LOOPZ ends when either CX=0 or the zero flag is
clear, whichever occurs first. LOOPNZ ends when either CX=0 or
the zero flag is set, whichever occurs first. LOOPE and LOOPZ
assemble to the same machine instruction, as do LOOPNE and
LOOPNZ. Use whichever mnemonic best fits the context of your
loop. Set CX to a number out of range if you don’t want a count to
control the loop.
JCXZ, JECXZ Branches to a label only if CX = 0 or ECX = 0. Unlike other
conditional-jump instructions, which can jump to either a near or a
short label under the 80386 or 80486, JCXZ and JECXZ always
jump to a short label.
Conditional jumps Acts only if certain conditions met. Necessary if several conditions
must be tested. See “Conditional Jumps,” page 164.
The JCXZ and JECXZ instructions provide an efficient way to avoid executing
loops when the loop counter CX is empty. For example, consider the following
loops:
mov cx, LoopCount ; Load loop counter
next: . ; Iterate loop CX times
.
.
loop next ; Do again
Loop-Generating Directives
The high-level control structures generate loop structures for you. These directives
are similar to the while and repeat loops of C or Pascal, and can make your
assembly programs easier to code and to read. The assembler generates the
appropriate assembly code. These directives are summarized as follows:
Directives Action
.WHILE ... .ENDW The statements between .WHILE condition and .ENDW
execute while the condition is true.
.REPEAT ... .UNTIL The loop executes at least once and continues until the
condition given after .UNTIL is true. Generates conditional
jumps.
.REPEAT ... .UNTILCXZ Compares label to an expression and generates appropriate
loop instructions.
.BREAK End a .REPEAT or a .WHILE loop unconditionally.
.CONTINUE Jump unconditionally past any remaining code to bottom of
loop.
.WHILE Loops
As with while loops in C or Pascal, the test condition for .WHILE is checked
before the statements inside the loop execute. If the test condition is false, the loop
does not execute. While the condition is true, the statements inside the loop repeat.
Use the .ENDW directive to mark the end of the .WHILE loop. When the
condition becomes false, program execution begins at the first statement following
the .ENDW directive. The .WHILE directive generates appropriate compare and
jump statements. The syntax is:
.WHILE condition
statements
.ENDW
For example, this loop copies the contents of one buffer to another until a ‘$’
character (marking the end of the string) is found:
.DATA
buf1 BYTE "This is a string",'$'
buf2 BYTE 100 DUP (?)
.CODE
sub bx, bx ; Zero out bx
.WHILE (buf1[bx] != '$')
mov al, buf1[bx] ; Get a character
mov buf2[bx], al ; Move it to buffer 2
inc bx ; Count forward
.ENDW
.REPEAT Loops
MASM’s .REPEAT directive allows for loop constructions like the do loop of C
and the REPEAT loop of Pascal. The loop executes until the condition following
the .UNTIL (or .UNTILCXZ) directive becomes true. Since the condition is
checked at the end of the loop, the loop always executes at least once. The
.REPEAT directive generates conditional jumps. The syntax is:
.REPEAT
statements
.UNTIL condition
.REPEAT
statements
.UNTILCXZ [[condition]]
where condition can also be expr1 == expr2 or expr1 != expr2. When two
conditions are used, expr2 can be an immediate expression, a register, or (if expr1
is a register) a memory location.
For example, the following code fills a buffer with characters typed at the keyboard.
The loop ends when the ENTER key (character 13) is pressed:
.DATA
buffer BYTE 100 DUP (0)
.CODE
sub bx, bx ; Zero out bx
.REPEAT
mov ah, 01h
int 21h ; Get a key
mov buffer[bx], al ; Put it in the buffer
inc bx ; Increment the count
.UNTIL (al == 13) ; Continue until al is 13
The .UNTIL directive generates conditional jumps, but the .UNTILCXZ directive
generates a LOOP instruction, as shown by the listing file code for these examples.
In a listing file, assembler-generated code is preceded by an asterisk.
ASSUME bx:PTR SomeStruct
.REPEAT
*@C0001:
inc ax
.UNTIL ax==6
* cmp ax, 006h
* jne @C0001
.REPEAT
*@C0003:
mov ax, 1
.UNTILCXZ
* loop @C0003
.REPEAT
*@C0004:
.UNTILCXZ [bx].field != 6
* cmp [bx].field, 006h
* loope @C0004
If you assemble the preceding source code with the /Fl and /Sg command-line
options and then view the results in the listing file, you will see this code:
.WHILE 1
0017 *@C0001:
0017 B4 08 mov ah, 08h
0019 CD 21 int 21h
.BREAK .IF al == 13
001B 3C 0D * cmp al, 00Dh
001D 74 10 * je @C0002
.CONTINUE .IF (al '0') || (al '9')
001F 3C 30 * cmp al, '0'
0021 72 F4 * jb @C0001
0023 3C 39 * cmp al, '9'
0025 77 F0 * ja @C0001
0027 8A D0 mov dl, al
0029 B4 02 mov ah, 02h
002B CD 21 int 21h
.ENDW
002D EB E8 * jmp @C0001
002F *@C0002:
The high-level control structures can be nested. That is, .REPEAT or .WHILE
loops can contain .REPEAT or .WHILE loops as well as .IF statements.
If the code generated by a .WHILE loop, .REPEAT loop, or .IF statement
generates a conditional or unconditional jump, MASM encodes the jump using the
jump extension and jump optimization techniques described in “Unconditional
Jumps,” page 162, and “Conditional Jumps,” page 164.
Expression Operators
The binary relational operators in MASM 6.1 are the same binary operators used in
C. These operators generate MASM compare, test, and conditional jump
instructions. High-level control instructions include:
Operator Meaning
== Equal
!= Not equal
> Greater than
>= Greater than or equal to
< Less than
<= Less than or equal to
& Bit test
! Logical NOT
&& Logical AND
|| Logical OR
A condition without operators (other than !) tests for nonzero as it does in C. For
example, .WHILE (x) is the same as .WHILE (x != 0), and .WHILE (!x) is
the same as .WHILE (x == 0).
You can also use the flag names (ZERO?, CARRY?, OVERFLOW?, SIGN?,
and PARITY?) as operands in conditions with the high-level control structures.
For example, in .WHILE (CARRY?), the value of the carry flag determines the
outcome of the condition.
You can use the PTR operator to tell the assembler that a particular operand in a
register or constant is a signed number, as in these examples:
.WHILE SWORD PTR [bx] <= 0
.IF SWORD PTR mem1 > 0
Without the PTR operator, the assembler would treat the contents of BX as an
unsigned value.
You can also specify the size attributes of operands in memory locations with
SBYTE, SWORD, and SDWORD, for use with .IF, .WHILE, and .REPEAT.
.DATA
mem1 SBYTE ?
mem2 WORD ?
.IF mem1 > 0
.WHILE mem2 < bx
.WHILE SWORD PTR ax < count
Precedence Level
As with C, you can concatenate conditions with the && operator for AND, the ||
operator for OR, and the ! operator for negate. The precedence level is !, &&, and
||, with ! having the highest priority. Like expressions in high-level languages,
precedence is evaluated left to right.
Expression Evaluation
The assembler evaluates conditions created with high-level control structures
according to short-circuit evaluation. If the evaluation of a particular condition
automatically determines the final result (such as a condition that evaluates to false
in a compound statement concatenated with AND), the evaluation does not
continue.
For example, in this .WHILE statement,
.WHILE (ax > 0) && (WORD PTR [bx] == 0)
the assembler evaluates the first condition. If this condition is false (that is, if AX is
less than or equal to 0), the evaluation is finished. The second condition is not
checked and the loop does not execute, because a compound condition containing
&& requires both expressions to be true for the entire condition to be true.
Procedures
Organizing your code into procedures that execute specific tasks divides large
programs into manageable units, allows for separate testing, and makes code more
efficient for repetitive tasks.
Assembly-language procedures are similar to functions, subroutines, and
procedures in high-level languages such as C, FORTRAN, and Pascal. Two
instructions control the use of assembly-language procedures. CALL pushes the
return address onto the stack and transfers control to a procedure, and RET pops
the return address off the stack and returns control to that location.
The PROC and ENDP directives mark the beginning and end of a procedure.
Additionally, PROC can automatically:
◆ Preserve register values that should not change but that the procedure might
otherwise alter.
◆ Set up a local stack pointer, so that you can access parameters and local
variables placed on the stack.
◆ Adjust the stack when the procedure ends.
Defining Procedures
Procedures require a label at the start of the procedure and a RET instruction at the
end. Procedures are normally defined by using the PROC directive at the start of
the procedure and the ENDP directive at the end. The RET instruction normally is
placed immediately before the ENDP directive. The assembler makes sure the
distance of the RET instruction matches the distance defined by the PROC
directive. The basic syntax for PROC is:
label PROC [[NEAR | FAR]]
.
.
.
RET [[constant]]
label ENDP
The CALL instruction pushes the address of the next instruction in your code onto
the stack and passes control to a specified address. The syntax is:
CALL {label | register | memory}
The operand contains a value calculated at run time. Since that operand can be a
register, direct memory operand, or indirect memory operand, you can write call
tables similar to the example code on page 164.
Calls can be near or far. Near calls push only the offset portion of the calling
address and therefore must target a procedure within the same segment or group.
You can specify the type for the target operand. If you do not, MASM uses the
declared distance (NEAR or FAR) for operands that are labels and for the size of
register or memory operands. The assembler then encodes the call appropriately, as
it does with unconditional jumps. (See previous “Unconditional Jumps” and
“Conditional Jumps.”)
MASM optimizes a call to a far non-external label when the label is in the current
segment by generating the code for a near call, saving one byte.
You can define procedures without PROC and ENDP, but if you do, you must
make sure that the size of the CALL matches the size of the RET. You can specify
the RET instruction as RETN (Return Near) or RETF (Return Far) to override the
default size:
call NEAR PTR task ; Call is declared near
. ; Return comes to here
.
.
task: ; Procedure begins with near label
.
. ; Instructions go here
.
retn ; Return declared near
Figure 7.1 shows the stack condition at key points in the process.
High High
memory memory SP
Argument 3 Argument 3
Argument 2 Argument 2
Argument 1 Argument 1 SP
Return SP
address
Low Low
memory memory
Starting with the 80186 processor, the ENTER and LEAVE instructions simplify
the stack setup and restore instructions at the beginning and end of procedures.
However, ENTER uses a lot of time. It is necessary only with nested, statically-
scoped procedures. Thus, a Pascal compiler may sometimes generate ENTER. The
LEAVE instruction, on the other hand, is an efficient way to do the stack cleanup.
LEAVE reverses the effect of the last ENTER instruction by restoring BP and SP
to their values before the procedure call.
This diagram shows a valid PROC definition that uses several attributes:
Distance
Langtype
Visibility Prologuearg
myproc PROC FAR C PUBLIC <macroarg> USES di si, var1:WORD, arg1:VARARG
Attributes
Attributes
The syntax for the attributes field is:
[[distance]] [[langtype]] [[visibility]] [[<prologuearg>]]
The explanations for these options include:
Argument Description
distance Controls the form of the RET instruction generated. Can be NEAR or FAR. If
distance is not specified, it is determined from the model declared with the
.MODEL directive. NEAR distance is assumed for TINY, SMALL,
COMPACT, and FLAT. The assembler assumes FAR distance for MEDIUM,
LARGE, and HUGE. For 80386/486 programming with 16- and 32-bit
segments, you can specify NEAR16, NEAR32, FAR16, or FAR32.
langtype Determines the calling convention used to access parameters and restore the
stack. The BASIC, FORTRAN, and PASCAL langtypes convert procedure
names to uppercase, place the last parameter in the parameter list lowest on
the stack, and generate a RET num instruction to end the procedure. The RET
adjusts the stack upward by num, which represents the number of bytes in the
argument list. This step, called “cleaning the stack,” returns the stack pointer
SP to the value it had before the caller pushed any arguments.
The C and STDCALL langtype prefixes an underscore to the procedure name
when the procedure’s scope is PUBLIC or EXPORT and places the first
parameter lowest on the stack. SYSCALL is equivalent to the C calling
convention with no underscore prefixed to the procedure’s name. STDCALL
uses caller stack cleanup when :VARARG is specified; otherwise the called
routine must clean up the stack (see Chapter 12).
visibility Indicates whether the procedure is available to other modules. The visibility
can be PRIVATE, PUBLIC, or EXPORT. A procedure name is PUBLIC
unless it is explicitly declared as PRIVATE. If the visibility is EXPORT, the
linker places the procedure’s name in the export table for segmented
executables. EXPORT also enables PUBLIC visibility.
You can explicitly set the default visibility with the OPTION directive.
OPTION PROC:PUBLIC sets the default to public. For more information,
see Chapter 1, “Using the Option Directive.”
prologuearg Specifies the arguments that affect the generation of prologue and epilogue
code (the code MASM generates when it encounters a PROC directive or the
end of a procedure). For an explanation of prologue and epilogue code, see
“Generating Prologue and Epilogue Code,” later in this chapter.
Parameters
The comma that separates parameters from reglist is optional, if both fields appear
on the same line. If parameters appears on a separate line, you must end the reglist
field with a comma. In the syntax:
parmname [[:tag]
parmname is the name of the parameter. The tag can be the qualifiedtype or the
keyword VARARG. However, only the last parameter in a list of param-
eters can use the VARARG keyword. The qualifiedtype is discussed in “Data
Types,” Chapter 1. An example showing how to reference VARARG param-
eters appears later in this section. You can nest procedures if they do not have
parameters or USES register lists. This diagram shows a procedure definition with
one parameter definition.
Parmname
Qualifiedtype
Parameters
The procedure presented in “Passing Arguments on the Stack,” page 182, is here
rewritten using the extended PROC functionality. Prior to the procedure call, you
must push the arguments onto the stack unless you use INVOKE. (See “Calling
Procedures with INVOKE,” later in this chapter.)
addup PROC NEAR C,
arg1:WORD, arg2:WORD, count:WORD
mov ax, arg1
add ax, count
add ax, arg2
ret
addup ENDP
If the arguments for a procedure are pointers, the assembler does not generate any
code to get the value or values that the pointers reference; your program must still
explicitly treat the argument as a pointer. (For more information about using
pointers, see Chapter 3, “Using Addresses and Pointers.”)
In the following example, even though the procedure declares the parameters as
near pointers, you must code two MOV instructions to get the values of the param-
eters. The first MOV gets the address of the parameters, and the second MOV gets
the parameter.
; Call from C as a FUNCTION returning an integer
.MODEL medium, c
.CODE
myadd PROC arg1:NEAR PTR WORD, arg2:NEAR PTR WORD
ret
myadd ENDP
You can use conditional-assembly directives to make sure your pointer parameters
are loaded correctly for the memory model. For example, the following version of
myadd treats the parameters as FAR parameters, if necessary.
.MODEL medium, c ; Could be any model
.CODE
myadd PROC arg1:PTR WORD, arg2:PTR WORD
IF @DataSize
les bx, arg1 ; Far parameters
mov ax, es:[bx]
les bx, arg2
add ax, es:[bx]
ELSE
mov bx, arg1 ; Near parameters
mov ax, [bx]
mov bx, arg2
add ax, [bx]
ENDIF
ret
myadd ENDP
Using VARARG
In the PROC statement, you can append the :VARARG keyword to the last
parameter to indicate that the procedure accepts a variable number of arguments.
However, :VARARG applies only to the C, SYSCALL, or STDCALL calling
conventions (see Chapter 12). A symbol must precede :VARARG so the procedure
can access arguments as offsets from the given variable name, as this example
illustrates:
addup3 PROTO NEAR C, argcount:WORD, arg1:VARARG
invoke addup3, 3, 5, 2, 4
ret ; Total is in AX
addup3 ENDP
You can pass non-default-sized pointers in the VARARG portion of the parameter
list by separately passing the segment portion and the offset portion of the address.
Note When you use the extended PROC features and the assembler encounters a
RET instruction, it automatically generates instructions to pop saved registers,
remove local variables from the stack, and, if necessary, remove parameters. It
generates this code for each RET instruction it encounters. You can reduce code
size by having only one return and jumping to it from various locations.
automatically generate local variables. When you use this directive, the assembler
generates the same instructions as those demonstrated in this section but handles
some of the details for you.
If your procedure has relatively few variables, you can usually write the most
efficient code by placing these values in registers. Use local (stack) data when you
have a large amount of temporary data for the procedure.
To use a local variable, you must save stack space for it at the start of the
procedure. A procedure can then reference the variable by its position in the stack.
At the end of the procedure, you must clean the stack by restoring the stack pointer.
This effectively throws away all local variables and regains the stack space they
occupied.
This example subtracts 2 bytes from the SP register to make room for a local word
variable, then accesses the variable as [bp-2].
push ax ; Push one argument
call task ; Call
.
.
.
Notice the instruction mov sp,bp at the end of the procedure restores the original
value of SP. The statement is required only if the value of SP changes inside the
procedure (usually by allocating local variables). The argument passed to the
procedure is removed with the RET instruction. Contrast this to the example in
“Passing Arguments on the Stack,” page 182, in which the calling code adjusts the
stack for the argument.
Low Low
memory memory
After After After
sub sp,2 mov sp,bp ret 2
pop bp
High High
memory memory SP
assembler generates debugging information for each local variable. If you have
programmed before in a high-level language that allows scoping, local variables
will seem familiar. For example, a C compiler sets up variables with automatic
storage class in the same way as the LOCAL directive.
We can simplify the procedure in the previous section with the following code:
task PROC NEAR arg:WORD
LOCAL loc:WORD
.
.
.
mov loc, 3 ; Initialize local variable
add ax, loc ; Add local variable to AX
sub arg, ax ; Subtract local from argument
. ; Use "loc" and "arg" in other operations
.
.
ret
task ENDP
The LOCAL directive must be on the line immediately following the PROC
statement with the following syntax:
LOCAL vardef [[, vardef]]...
Each vardef defines a local variable. A local variable definition has this form:
label[[[count]]][[:qualifiedtype]]
These are the parameters in local variable definitions:
Argument Description
label The name given to the local variable. You can use this name to access the
variable.
count The number of elements of this name and type to allocate on the stack.
You can allocate a simple array on the stack with count. The brackets
around count are required. If this field is omitted, one data object is
assumed.
qualifiedtype A simple MASM type or a type defined with other types and attributes.
For more information, see “Data Types” in Chapter 1.
If the number of local variables exceeds one line, you can place a comma at the end
of the first line and continue the list on the next line. Alternatively, you can use
several consecutive LOCAL directives.
The assembler does not initialize local variables. Your program must include code
to perform any necessary initializations. For example, the following code fragment
sets up a local array and initializes it to zero:
arraysz EQU 20
Even though you can reference stack variables by name, the assembler treats them
as offsets of BP, and they are not visible outside the procedure. In the following
procedure, array is a local variable.
index EQU 10
test PROC NEAR
LOCAL array[index]:WORD
.
.
.
mov bx, index
; mov array[bx], 5 ; Not legal!
The second MOV statement may appear to be legal, but since array is an
offset of BP, this statement is the same as
; mov [bp + bx + arrayoffset], 5 ; Not legal!
BP and BX can be added only to SI and DI. This example would be legal, however,
if the index value were moved to SI or DI. This type of error in your program can
be difficult to find unless you keep in mind that local variables in procedures are
offsets of BP.
The following example illustrates how to define and then declare two typical
procedures. In both prototype and declaration, the comma before the argument list
is optional only when the list does not appear on a separate line:
; Procedure prototypes.
; Procedure declarations
When you call a procedure with INVOKE, the assembler checks the arguments
given by INVOKE against the parameters expected by the procedure. If the data
types of the arguments do not match, MASM reports an error or converts the type to
the expected type. These conversions are explained in the next section.
If arguments do not match in number or if the type is not one the assembler can
convert, an error results.
If the procedure uses VARARG, INVOKE can pass a number of arguments
different from the number in the parameter list without generating an error or
warning. Any additional arguments must be at the end of the INVOKE argument
list. All other arguments must match those in the prototype parameter list.
The assembler can convert some arguments and parameter type combinations so
that the correct type can be passed. The signed or unsigned qualities of the
arguments in the INVOKE statements determine how the assembler converts them
to the types expected by the procedure.
The addup procedure, for example, expects parameters of type WORD, but the
arguments passed by INVOKE to the addup procedure can be any of these types:
◆ BYTE, SBYTE, WORD, or SWORD
◆ An expression whose type is specified with the PTR operator to be one of those
types
◆ An 8-bit or 16-bit register
◆ An immediate expression in the range –32K to +64K
◆ A NEAR PTR
If the type is smaller than that expected by the procedure, MASM widens the
argument to match.
Widening Arguments
For INVOKE to correctly handle type conversions, you must use the signed data
types for any signed assignments. MASM widens an argument to match the type
expected by a procedure’s parameters in these cases:
Type Passed Type Expected
BYTE, SBYTE WORD, SWORD, DWORD, SDWORD
WORD, SWORD DWORD, SDWORD
The assembler can extend a segment if far data is expected, and it can convert the
type given in the list to the types expected. If the assembler cannot convert the type,
however, it generates an error.
Detecting Errors
If the assembler needs to widen an argument, it first copies the value to AL or AX.
It widens an unsigned value by placing a zero in the higher register area, and
widens a signed value with a CBW, CWD, or CWDE instruction as required.
Similarly, the assembler copies a constant argument value into AL or AX when the
.8086 directive is in effect. You can see these generated instructions in the listing
file when you include the /Sg command-line option.
Using the accumulator register to widen or copy an argument may lead to an error if
you attempt to pass AX as another argument. For example, consider the following
INVOKE statement for a procedure with the C calling convention
INVOKE myprocA, ax, cx, 100, arg
where arg is a BYTE variable and myproc expects four arguments of type
WORD. The assembler widens and then pushes arg like this:
mov al, DGROUP:arg
xor ah, ah
push ax
The generated code thus overwrites the last argument (AX) passed to the procedure.
The assembler generates an error in this case, requiring you to rewrite the
INVOKE statement.
To summarize, the INVOKE directive overwrites AX and perhaps DX when
widening arguments. It also uses AX to push constants on the 8088 and 8086. If
you use these registers (or EAX and EDX on an 80386/486) to pass arguments,
they may be overwritten. The assembler’s error detection prevents this from ever
becoming a run-time bug, but AX and DX should remain your last choice for
holding arguments.
However, INVOKE cannot combine into a single address one argument for the
segment and one for the offset.
Passing an Address
You can use the ADDR operator to pass the address of an expression to a
procedure that expects a NEAR or FAR pointer. This example generates code to
pass a far pointer (to arg1) to the procedure proc1.
PBYTE TYPEDEF FAR PTR BYTE
arg1 BYTE "This is a string"
proc1 PROTO NEAR C fparg:PBYTE
.
.
.
INVOKE proc1, ADDR arg1
For information on defining pointers with TYPEDEF, see “Defining Pointer Types
with TYPEDEF” in Chapter 3.
.DATA
pfunc FUNCPTR OFFSET proc1, OFFSET proc2
.CODE
.
.
.
mov bx, OFFSET pfunc ; BX points to table
mov si, Num ; Num contains 0 or 2
INVOKE FUNCPTR PTR [bx+si], arg1 ; Call proc1 if Num=0
; or proc2 if Num=2
You can also use ASSUME to accomplish the same task. The following ASSUME
statement associates the type FUNCPTR with the BX register.
ASSUME BX:FUNCPTR
mov bx, OFFSET pfunc
mov si, Num
INVOKE [bx+si], arg1
The assembler automatically generates the prologue code when it encounters the
first instruction or label after the PROC directive. This means you cannot label the
prologue for the purpose of jumping to it. The assembler generates the epilogue
code when it encounters a RET or IRET instruction. Using the assembler-
generated prologue and epilogue code saves time and decreases the number of
repetitive lines of code in your procedures.
The generated prologue or epilogue code depends on the:
◆ Local variables defined.
◆ Arguments passed to the procedure.
◆ Current processor selected (affects epilogue code only).
◆ Current calling convention.
◆ Options passed in the prologuearg of the PROC directive.
◆ Registers being saved.
The prologuearg list contains options specifying how to generate the prologue or
epilogue code. The next section explains how to use these options, gives the
standard prologue and epilogue code, and explains the techniques for defining your
own prologue and epilogue code.
The epilogue cancels these three steps in reverse order, then cleans the stack, if
necessary, with a RET num instruction. For example, the procedure declaration
myproc PROC NEAR PASCAL USES di si,
arg1:WORD, arg2:WORD, arg3:WORD
LOCAL local1:WORD, local2:WORD
Notice the RET 6 instruction cleans the stack of the three word-sized arguments.
The instruction appears in the epilogue because the procedure does not use the C
calling convention. If myproc used C conventions, the epilogue would end with a
RET instruction without an operand.
The assembler generates standard epilogue code when it encounters a RET
instruction without an operand. It does not generate an epilogue if RET has a
nonzero operand. To suppress generation of a standard epilogue, use RETN or
RETF with or without an operand, or use RET 0.
The standard prologue and epilogue code recognizes two operands passed in the
prologuearg list, LOADDS and FORCEFRAME. These operands modify the
prologue code. Specifying LOADDS saves and initializes DS. Specifying
FORCEFRAME as an argument generates a stack frame even if no arguments are
sent to the procedure and no local variables are declared. If your procedure has any
parameters or locals, you do not need to specify FORCEFRAME.
For example, adding LOADDS to the argument list for myproc creates this
prologue:
push bp ; Step 1:
mov bp, sp ; point BP to stack top
sub sp, 4 ; Step 2: space for 2 locals
push ds ; Save DS and point it
mov ax, DGROUP ; to DGROUP, as
mov ds, ax ; instructed by LOADDS
push di ; Step 3:
push si ; save registers listed in USES
The assembler expects your prologue or epilogue macro to have this form:
macroname MACRO procname, \
flag, \
parmbytes, \
localbytes, \
<reglist>, \
userparms
Your macro must have formal parameters to match all the actual arguments passed.
The arguments passed to your macro include:
Argument Description
procname The name of the procedure.
flag A 16-bit flag containing the following information:
Bit = Value Description
Bit 0, 1, 2 For calling conventions (000=unspecified language
type, 001=C, 010=SYSCALL, 011=STDCALL,
100=PASCAL, 101=FORTRAN, 110=BASIC).
Bit 3 Undefined (not necessarily zero).
Bit 4 Set if the caller restores the stack
(use RET, not RETn).
Bit 5 Set if procedure is FAR.
Bit 6 Set if procedure is PRIVATE.
Bit 7 Set if procedure is EXPORT.
Bit 8 Set if the epilogue is generated as a result of an IRET
instruction and cleared if the epilogue is generated as a
result of a RET instruction.
Bits 9–15 Undefined (not necessarily zero).
parmbytes The accumulated count in bytes of all parameters given in the PROC
statement.
localbytes The count in bytes of all locals defined with the LOCAL directive.
reglist A list of the registers following the USES operator in the procedure
declaration. Enclose this list with angle brackets (< >) and separate each
item with commas. Reverse the list for epilogues.
userparms Any argument you want to pass to the macro. The prologuearg (if there is
one) specified in the PROC directive is passed to this argument.
Your macro function must return the parmbytes parameter. However, if the
prologue places other values on the stack after pushing BP and these values are not
referenced by any of the local variables, the exit value must be the number of bytes
for procedure locals plus any space between BP and the locals. Therefore,
parmbytes is not always equal to the bytes occupied by the locals.
.DATA
procname&count WORD 0
.CODE
inc procname&count ; Accumulates count of times the
; procedure is called
push bp
mov bp, sp
; Other BP operations
IFNB <regs>
FOR r, regs
push r
ENDM
ENDIF
EXITM %bytecount
ENDM
Your program must also include this statement before calling any procedures that
use the prologue:
OPTION PROLOGUE:ProfilePro
If you define either a prologue or an epilogue macro, the assembler uses the
standard prologue or epilogue code for the one you do not define. The form of the
code generated depends on the .MODEL and PROC options used.
If you want to revert to the standard prologue or epilogue code, use
PROLOGUEDEF or EPILOGUEDEF as the macroname in the OPTION statement.
OPTION EPILOGUE:EPILOGUEDEF
In this case, no user-defined macro is called, and the assembler does not generate a
default code sequence. This state remains in effect until the next OPTION
PROLOGUE or OPTION EPILOGUE is encountered.
For additional information about writing macros, see Chapter 9, “Using Macros.”
The PROLOGUE.INC file provided in the MASM 6.1 distribution disks can create
the prologue and epilogue sequences for the Microsoft C professional development
system.
MS-DOS Interrupts
In addition to jumps, loops, and procedures that alter program execution, interrupt
routines transfer execution to a different location. In this case, control goes to an
interrupt routine.
You can write your own interrupt routines, either to replace an existing routine or to
use an undefined interrupt number. For example, you may want to replace an MS-
DOS interrupt handler, such as the Critical Error (Interrup 24h) and CONTROL+C
(Interrupt 23h) handlers. The BOUND instruction checks array bounds and calls
Interrupt 5 when an error occurs. If you use this instruction, you need to write an
interrupt handler for it.
This section summarizes the following:
◆ How to call interrupts
◆ How the processor handles interrupts
◆ How to redefine an existing interrupt routine
The example routine in this section handles addition or multiplication overflow and
illustrates the steps necessary for writing an interrupt routine. For additional
information about MS-DOS and BIOS interrupts, see Chapter 11, “Writing
Memory-Resident Software.”
SP SP
Stack Previous flags
Previous CS
Previous IP
SP
Low memory
You can write an interrupt routine as a procedure by using the PROC and ENDP
directives. The routine should always be defined as FAR and should end with an
IRET instruction instead of a RET instruction.
Note You can use the full extended PROC syntax (described in “Declaring
Parameters with the PROC Directive,” earlier in this chapter) to write interrupt
procedures. However, you should not make interrupt procedures NEAR or specify
arguments for them. You can use the USES keyword, however, to correctly
generate code to save and restore a register list in interrupt procedures.
The IRET instruction in MASM 6.1 has two forms that suppress epilogue code.
This allows an interrupt to have local variables or use a user-defined prologue.
IRETF pops a FAR16 return address, and IRETFD pops a FAR32 return address.
The following example shows how to replace the handler for Interrupt 4. Once
registered in the Interrupt Vector Table, the new routine takes control when the
processor encounters either an INT 4 instruction or its special variation INTO
(Interrupt on Overflow). INTO is a conditional instruction that acts only when the
overflow flag is set. With INTO after a numerical calculation, your code can
automatically route control to a handler routine if the calculation results in a
numerical overflow. By default, the routine for Interrupt 4 simply consists of an
IRET, so it returns without doing anything. Using INTO is an alternative to using
JO (Jump on Overflow) to jump to another set of instructions.
The following example program first executes INT 21h to invoke MS-DOS
Function 35h (Get Interrupt Vector). This function returns the existing vector for
Interrupt 4. The program stores the vector, then invokes MS-DOS Function 25h
(Set Interrupt Vector) to place the address of the ovrflow procedure in the
Interrupt Vector Table. From this point on, ovrflow gains control whenever the
processor executes INTO while the overflow flag is set. The new routine displays a
message and returns with AX and DX set to 0.
.MODEL LARGE, C
FPFUNC TYPEDEF FAR PTR
.DATA
msg BYTE "Overflow - result set to 0",13,10,'$'
vector FPFUNC ?
.CODE
.STARTUP
push ds ; Save DS
mov ax, cs ; Load segment of new routine
mov ds, ax
mov dx, OFFSET ovrflow ; Load offset of new routine
mov ax, 2504h ; Load Interrupt 4 and call DOS
int 21h ; Set Interrupt Vector
pop ds ; Restore
.
.
.
add ax, bx ; Do arithmetic
into ; Call Interrupt 4 if overflow
.
.
.
lds dx, vector ; Load original address
mov ax, 2504h ; Restore it to vector table
int 21h ; with DOS set vector function
mov ax, 4C00h ; Terminate function
int 21h
Before the program ends, it again uses MS-DOS Function 25h to reset the original
Interrupt 4 vector back into the Interrupt Vector Table. This reestablishes the
original routine as the handler for Interrupt 4.
The first instruction of the ovrflow routine warrants further discussion. When the
processor encounters an INT instruction, it clears the interrupt flag before
branching to the specified interrupt handler routine. The interrupt flag serves a
crucial role in smoothing the processor’s tasks, but must not be abused. When clear,
the flag inhibits hardware interrupts such as the keyboard or system timer. It should
be left clear only briefly and only when absolutely necessary. Unless you have a
compelling reason to leave the flag clear, always include an STI (Set Interrupt
Flag) instruction at the beginning of your interrupt handler routine to reenable
hardware interrupts.
CLI (Clear Interrupt Flag) and its corollary STI are designed to protect small
sections of time-dependent code from interruptions by the hardware. If you use CLI
in your program, be sure to include a matching STI instruction as well. The sample
interrupt handlers in Chapter 11, “Writing Memory-Resident Software,” illustrate
how to use these important instructions.
C H A P T E R 8
To use symbols and procedures in more than one module, the assembler must be
able to recognize the shared data as global to all the modules where they are used.
MASM provides techniques to simplify data-sharing and give a high-level interface
to multiple-module programming. With these techniques, you can place shared
symbols in include files. This makes the data declarations in the file available to all
modules that use the include file.
This chapter explains the two data-sharing methods MASM 6.1 offers. The first
method simplifies data sharing between modules with include files. The second does
not involve include files. Instead, this method allows modules to share procedures
and data items using the PUBLIC and EXTERN directives.
The last section of this chapter explains how to create program libraries and access
their routines.
The next section provides further details on using include files. For more
information on PUBLIC and EXTERN, see “Using Alternatives to Include Files,”
page 219.
Organizing Modules
This section summarizes the organization of declarations and definitions in modules
and include files and the use of the INCLUDE directive.
Include Files
Type declarations that need to be identical in every module should be placed in an
include file. This ensures consistency and saves time when you update programs.
Include files should contain only symbol declarations and any other declarations
that are resolved at assembly time. (For a list of assembly-time operations, see
“Generating and Running Executable Programs” in Chapter 1.)
If more than one module accesses the include file, the file cannot contain statements
that define and allocate memory for symbols. Otherwise, the assembler would
attempt to allocate the same symbol more than once.
Note An include file used in two or more modules should not allocate data
variables.
Modules
An INCLUDE statement is usually placed before data and code segments in your
modules. When the assembler encounters an INCLUDE directive, it opens the
specified file and assembles all its statements. The assembler then returns to the
original module and continues the assembly.
The filename in the INCLUDE directive must be fully specified; no extensions are
assumed. If a full pathname is not given, the assembler first searches the directory
of the source file containing the INCLUDE directive.
If the include file is not in the source file directory, the assembler searches the paths
specified in the assembler’s command-line option /I, or in PWB’s Include Paths
field in the MASM Option dialog box (accessed from the Option menu). The /I
option takes this form:
/I path
You can include more than one /I option on the command line. The assembler then
searches for include files within each specified path in the order given. If none of
these directories contains the include file, the assembler finally searches in the paths
specified in the INCLUDE environment variable. If the include file still cannot be
found, an assembly error occurs. (The /x command-line option tells the assembler to
ignore the INCLUDE environment variable when searching for include files.)
An include file may specify another include file. The assembler processes the
second include file before returning to the first. Your program can nest include files
this way as deeply as the amount of free memory allows.
Using EXTERNDEF
MASM treats EXTERNDEF as a public declaration in the defining module, and as
an external declaration in the referencing module(s). You can use the
EXTERNDEF statement in your include file to make a variable common to two or
more modules. EXTERNDEF works with all types of variables, including arrays,
structures, unions, and records. It also works with procedures.
As a result, a single include file can contain an EXTERNDEF declaration that
works in both the defining module and any referencing module. It is ignored in
modules that neither define nor reference the variable. Therefore, an include file for
a library which is used in multiple .EXE files does not force the definition of a
symbol as EXTERN does.
The EXTERNDEF statement takes this form:
EXTERNDEF [[langtype]] name:qualifiedtype
The name is the variable’s identifier. The qualifiedtype is explained in detail in
“Data Types,” page 14.
The optional langtype specifier sets the naming conventions for the name it
precedes. It overrides any language specified in the .MODEL directive. The
specifier can be C, SYSCALL, STDCALL, PASCAL, FORTRAN, or BASIC.
For information on selecting the appropriate langtype type, see “Naming and
Calling Conventions,” page 308.
The following diagram shows the statements that declare an array, make it public,
and use it in another module.
MOD.INC
EXTERNDEF array1:BYTE
MOD1.ASM MOD2.ASM
INCLUDE MOD.INC INCLUDE MOD.INC
. .
. .
. .
.DATA .CODE
array1 BYTE 2, 4, 6 mov al, array1[2]
You can also use EXTERNDEF to make a code label global between modules so
that one module can reference a label in another module. Give the label global
scope with the double colon operator, like this:
EXTERNDEF codelabel:NEAR
.
.
.
codelabel::
Using PROTO
This section describes how to prototype a procedure with the PROTO directive.
PROTO automatically issues an EXTERNDEF for the procedure unless the
PROC statement declares the procedure PRIVATE. Defining a prototype enables
type-checking for the procedure arguments.
Follow these steps to create an interface for a procedure defined in one module and
called from other modules:
1. Place the PROTO declaration in the include file.
2. Define the procedure with PROC in one module. The PROC directive declares
the procedure PUBLIC by default.
3. Call the procedure with the INVOKE statement (or with CALL). Make sure
that all calling modules access the include file.
For descriptions, syntax, and examples of PROTO, PROC, and INVOKE, see
Chapter 7, “Controlling Program Flow.”
The following example illustrates these three steps. In the example, a PROTO
statement defines the far procedure CopyFile, which uses the C parameter-passing
and naming conventions, and takes the arguments filename and numberlines.
The diagram following the example shows the file placement for these statements.
This definition goes into the include file:
CopyFile PROTO FAR C filename:BYTE, numberlines:WORD
To call the CopyFile procedure, you can use this INVOKE statement:
INVOKE CopyFile, NameVar, 200
TOOLS.INC
CopyFile PROTO FAR C filename:BYTE, numberlines:WORD
TOOLS.ASM FILE1.ASM
INCLUDE TOOLS.INC INCLUDE TOOLS.INC
. .
. .
. .
.CODE .CODE
INVOKE CopyFile, NameVar, 200 CopyFile PROC FAR C USES cx,
filename:BYTE,
numberlines:WORD
Using COMM
Another way to share variables among modules is to add the COMM (communal)
declaration to your include file. Since communal variables are allocated by the
linker and cannot be initialized, you cannot depend on their location or sequence.
Communal variables are supported by MASM primarily for compatibility with
communal variables in Microsoft C. Communal variables are not used in any other
Microsoft language, and they are not compatible with C++ and some other
languages.
COMM declares a data variable external and instructs the linker to allocate the
variable if it has not been explicitly defined in a module. The memory space for
communal variables may not be assigned until load time, so using communal
variables may reduce the size of your executable file.
The COMM declaration has the syntax:
COMM [[langtype]] [[NEAR | FAR]] label:type[[:count]]
The label is the name of the variable. The langtype sets the naming conventions for
the name it precedes. It overrides any language specified in the .MODEL directive.
If NEAR or FAR is not specified, the variable determines the default from the
current memory model (NEAR for TINY, SMALL, COMPACT, and FLAT;
FAR for MEDIUM, LARGE, and HUGE). If you do not provide a memory
model with the .MODEL directive, you must specify a distance when accessing a
communal variable, like this:
mov ax, NEAR PTR CommNear
mov bx, FAR PTR CommFar
The type can be a constant expression, but it is usually a type such as BYTE,
WORD, or DWORD, or a structure, union, or record. If you first declare the type
with TYPEDEF, CodeView can provide type information. The count is the number
of elements. If no count is given, one element is assumed.
The following example creates the on far variable DataBlock, which is a 1,024-
element array of uninitialized signed doublewords:
COMM FAR DataBlock:SDWORD:1024
Note C variables declared outside functions (except static variables) are communal
unless explicitly initialized; they are the same as assembly-language communal
variables. If you are writing assembly-language modules for C, you can declare the
same communal variables in both C and MASM include files. However, communal
variables in C do not have to be declared communal in assembler. The linker will
match the EXTERN, PUBLIC, and COMM statements for the variable.
◆ If you know the group but not the segment, position the EXTERN statement
outside any segment and reference the variable with the group name. For
example, if var1 is in DGROUP, reference the variable as
mov DGROUP:var1, 10
◆ If you know nothing about the location of an external variable, put the
EXTERN statement outside any segment. You can use the SEG directive to
access the external variable like this:
mov ax, SEG var1
mov es, ax
mov ax, es:var1
◆ If the symbol is an absolute symbol or a far code label, you can declare it
external anywhere in the source code.
Always close any segments opened in include files so that external declarations
following an include statement are not incorrectly placed inside a segment. If you
want to be certain an external definition lies outside a segment, you can use
@CurSeg. The @CurSeg predefined symbol returns a blank if the definition is not
in a segment. For example,
.DATA
.
.
.
@CurSeg ENDS ; Close segment
EXTERNDEF var:WORD
In the following example, the procedure BuildTable and the variable Var are
declared public. The procedure uses the Pascal naming and data-passing
conventions:
MOD1.ASM MOD2.ASM
.MODEL small, pascal .MODEL small, pascal
PUBLIC BuildTable, Var EXTERN var:BYTE, BuildTable:NEAR
. .CODE
. .
. .
.DATA .
Var BYTE 0 mov al, var
. call BuildTable
. .
. .
.CODE .
BuildTable PROC USES cx dx,
sizevar:WORD
.
.
.
ret
BuildTable ENDP
Other Alternatives
You can also use the directives discussed earlier (EXTERNDEF, PROTO, and
COMM) without the include file. In this case, place the declarations to make a
symbol global in the same module where the symbol is defined. You might want to
use this technique if you are linking only a few modules that have very little data in
common.
Developing Libraries
As you create reusable procedures, you can place them in a library file for
convenient access. Although you can put any routine into a library, each library file,
recognizable by its .LIB extension, usually contains related routines. For example,
you might place string-manipulation functions in one library, matrix calculations in
another, and port communications in another. Do not place communal variables
(defined with the COMM directive) in a library.
A library consists of combined object modules, each created from a single source
file. The object module is the smallest independent unit in a library. If you link with
one symbol in a module, the linker adds the entire module to your program, but not
the entire library.
Without the INCLUDELIB directive, you must link the program DRAW.ASM
with either of the following commands:
ML DRAW.ASM GRAPHICS.LIB
ML DRAW /link GRAPHICS
The LIB.EXE utility helps you create, organize, and maintain run-time libraries.
Refer to Environment and Tools for instructions on LIB.EXE.
dummy ENDP
.
.
.
call init ; Defined in FLOAT.OBJ
In this example, the reference to the routine init (defined in FLOAT.OBJ) does
not force the module FLOAT.OBJ to be linked into the executable file. If another
reference causes FLOAT.OBJ to be linked into the executable file, then init will
refer to the init label in FLOAT.OBJ. If there are no references that force linkage
with FLOAT.OBJ, the linker will use the alternate name for init(dummy).
C H A P T E R 9
Using Macros
This chapter explains how to use macros for simple code substitutions and how to
write sophisticated macros with parameter lists and repeat loops. It also describes
how to use these features in conjunction with local symbols, macro operators, and
predefined macro functions.
Text Macros
You can give a sequence of characters a symbolic name and then use the name in
place of the text later in the source code. The named text is called a text macro.
The TEXTEQU directive defines a text macro, as these examples show:
name TEXTEQU <text>
name TEXTEQU macroId | textmacro
name TEXTEQU %constExpr
In the previous lines, text is a sequence of characters enclosed in angle brackets,
macroId is a previously defined macro function, textmacro is a previously defined
text macro, and %constExpr is an expression that evaluates to text.
Here are some examples:
msg TEXTEQU <Some text> ; Text assigned to symbol
string TEXTEQU msg ; Text macro assigned to symbol
msg TEXTEQU <Some other text> ; New text assigned to symbol
value TEXTEQU %(3 + num) ; Text representation of resolved
; expression assigned to symbol
The first line assigns text to the symbol msg. The second line equates the text of the
msg text macro with a new text macro called string. The third line assigns new
text to msg. Although msg has new text, string retains its original text value. The
fourth line assigns 7 to value if num equals 4. If a text macro expands to another
text macro (or macro function, as discussed on page 248), the resulting text macro
will expand recursively.
Text macros are useful for naming strings of text that do not evaluate to integers.
For example, you might use a text macro to name a floating-point constant or a
bracketed expression. Here are some practical examples:
pi TEXTEQU <3.1416> ; Floating point constant
WPT TEXTEQU <WORD PTR> ; Sequence of key words
arg1 TEXTEQU <[bp+4]> ; Bracketed expression
Macro Procedures
If your program must perform the same task many times, you can avoid repeatedly
typing the same statements each time by writing a macro procedure. Think of macro
procedures (commonly called macros) as text-processing mechanisms that
automatically generate repeated text.
This section uses the term “macro procedure” rather than “macro” when necessary
to distinguish between a macro procedure and a macro function. Macro functions
are described in “Returning Values with Macro Functions.”
The double semicolons mark the beginning of macro comments. Macro comments
appear in a listing file only at the macro’s initial definition, not at the point where
the macro is referenced and expanded. Listings are usually easier to read if the
comments aren’t repeatedly expanded. However, regular comments (those with a
single semicolon) are listed in macro expansions. See Appendix C for listing files
and examples of how macros are expanded in listings.
Once you define a macro, you can call it anywhere in the program by using the
macro’s name as a statement. The following example calls the beep macro two
times if an error flag has been set.
.IF error ; If error flag is true
beep ; execute macro two times
beep
.ENDIF
During assembly, the instructions in the macro replace the macro reference. The
listing file shows:
.IF error
0017 80 3E 0000 R 00 * cmp error, 000h
001C 74 0C * je @C0001
beep
001E B4 02 1 mov ah, 2
0020 B2 07 1 mov dl, 7
0022 CD 21 1 int 21h
beep
0024 B4 02 1 mov ah, 2
0026 B2 07 1 mov dl, 7
0028 CD 21 1 int 21h
.ENDIF
002A *@C0001:
Contrast this with the results of defining beep as a procedure using the PROC
directive and then calling it with the CALL instruction.
Many such tasks can be handled as either a macro or a procedure. In deciding
which method to use, you must choose between speed and size. For repetitive tasks,
a procedure produces smaller code, because the instructions physically appear only
once in the assembled program. However, each call to the procedure involves the
additional overhead of a CALL and RET instruction. Macros do not require a
change in program flow and so execute faster, but generate the same code multiple
times rather than just once.
The assembler treats as one item all text between matching quotation marks in an
arglist.
The beep macro introduced in the previous section used the MS-DOS interrupt to
write only the bell character (ASCII 7). We can rewrite the macro with a parameter
that accepts any character:
writechar MACRO char
mov ah, 2 ;; Select DOS Print Char function
mov dl, char ;; Select ASCII char
int 21h ;; Call DOS
ENDM
Whenever it expands the macro, the assembler replaces each instance of char with
the given argument value. The rewritten macro now writes any character to the
screen, not just ASCII 7:
writechar 7 ; Causes computer to beep
writechar ‘A’ ; Writes A to screen
If you pass more arguments than there are parameters, the additional arguments
generate a warning (unless you use the VARARG keyword; see page 242). If you
pass fewer arguments than the macro procedure expects, the assembler assigns
empty strings to the remaining parameters (unless you have specified default
values). This may cause errors. For example, a reference to the writechar macro
with no argument results in the following line:
mov dl,
The assembler generates an error for the expanded statement but not for the macro
definition or the macro call.
You can make macros more flexible by leaving off arguments or adding additional
arguments. The next section tells some of the ways your macros can handle missing
or extra arguments.
If the call does not include a matching argument, the assembler reports the error in
the line that contains the macro reference. REQ can thus improve error reporting.
You can also accommodate missing parameters by specifying a default value, like
this:
parameter:=textvalue
Suppose that you often use writechar to beep by printing ASCII 7. The following
macro definition uses an equal sign to tell the assembler to assume the parameter
char is 7 unless you specify otherwise:
If a reference to this macro does not include the argument char, the assembler fills
in the blank with the default value of 7 and the macro beeps when called.
Enclose the default parameter value in angle brackets so the assembler recognizes
the supplied value as a text value. This is explained in detail in “Text Delimiters
and the Literal-Character Operator,” later in this chapter.
Missing arguments can also be handled with the IFB, IFNB, .ERRB, and
.ERRNB directives. They are described in the section “Conditional Directives” in
chapter 1 and in Help. Here is a slightly more complex macro that uses some of
these techniques:
In this macro, the distance parameter is required. The attrib parameter has a
default value of 7 (white on black), but the macro also tests to make sure the
corresponding argument isn’t BH, since it would be inefficient (though legal) to
load a register onto itself. The IFNB directive is used to test for blank arguments.
These are ignored to allow the user to manipulate rows and columns directly in
registers CX and DX at run time.
The following shows two valid ways to call the macro:
; Assume DL and CL already loaded
dec dh ; Decrement top row
inc ch ; Increment bottom row
Scroll -3 ; Scroll white on black dynamic
; window up three lines
Scroll 5, 17h, 2, 2, 14, 12 ; Scroll white on blue constant
; window down five lines
This macro can generate completely different code, depending on its arguments. In
this sense, it is not comparable to a procedure, which always has the same code
regardless of arguments.
If the labels again and gotzero were not declared local, the macro would work
the first time it is called, but it would generate redefinition errors on subsequent
calls. MASM implements local labels by generating different names for them each
time the macro is called. You can see this in listing files. The labels in the power
macro might be expanded to ??0000 and ??0001 on the first call and to ??0002
and ??0003 on the second.
You should avoid using anonymous labels in macros (see “Anonymous Labels” in
Chapter 7). Although legal, they can produce unwanted results if you expand a
macro near another anonymous label. For example, consider what happens in the
following:
Update MACRO arg1
@@: .
.
.
loop @B
ENDM
.
.
.
jcxz @F
Update ax
@@:
Expanding Update places another anonymous label between the jump and its
target. The line
jcxz @F
consequently jumps to the start of the loop rather than over the loop — exactly the
opposite of what the programmer intended.
When the assembler expands a macro, it processes the symbols in the order shown
here. MASM first replaces macro parameters with the text of their actual
arguments, then expands text macros.
Macro parameters are similar to procedure parameters in some ways, but they also
have important differences. In a procedure, a parameter has a type and a memory
location. Its value can be modified within the procedure. In a macro, a parameter is
a placeholder for the argument text. The value can only be assigned to another
symbol or used directly; it cannot be modified. The macro may interpret the
argument text it receives either as a numeric value or as a text value.
It is important to understand the difference between text values and numeric values.
Numeric values can be processed with arithmetic operators and assigned to numeric
equates. Text values can be processed with macro functions and assigned to text
macros.
Macro operators are often helpful when processing assembly-time variables. Table
9.1 shows the macro operators that MASM provides.
Table 9.1 MASM Macro Operators
Symbol Name Description
<> Text Delimiters Opens and closes a literal string.
! Literal-Character Operator Treats the next character as a literal
character, even if it would normally have
another meaning.
% Expansion Operator Causes the assembler to expand a constant
expression or text macro.
& Substitution Operator Tells the assembler to replace a macro
parameter or text macro name with its
actual value.
The literal-character operator (!) lets you include angle brackets as part of a
delimited text value, so the assembler does not interpret them as delimiters. The
assembler treats the character following ! literally rather than as a special
character, like this:
errstr TEXTEQU <Expression !> 255> ; errstr = “Expression > 255”
Text delimiters also have a special use with the FOR directive, as explained in
“FOR Loops and Variable-Length Parameters,” later in this chapter.
Expansion Operator
The expansion operator (%) expands text macros or converts constant expressions
into their text representations. It performs these tasks differently in different
contexts, as discussed in the following.
When assigning text macros, you can use numeric equates in the constant
expressions, but not text macros:
num EQU 4 ; num = 4
numstr TEXTEQU <4> ; numstr = <4>
a TEXTEQU %3 + num ; a = <7>
b TEXTEQU %3 + numstr ; b = <7>
The expansion operator gives you flexibility when passing arguments to macros. It
lets you pass a computed value rather than the literal text of an expression. The
following example illustrates by defining a macro
work MACRO arg
mov ax, arg * 4
ENDM
You must consider operator precedence when using the expansion operator.
Parentheses inside the macro can force evaluation in a desired order:
work MACRO arg
mov ax, (arg) * 4
ENDM
Several other uses for the expansion operator are reviewed in “Returning Values
with Macro Functions,” later in this chapter.
However, you can achieve the desired result by assigning the text of the expression
to a text macro and then using the expansion operator at the beginning of the line to
force expansion of the text macro.
Note that you cannot get the same results simply by putting the % at the beginning
of the first echo line, because % expands only text macros, not numeric equates or
constant expressions.
Here are more examples of the expansion operator at the start of a line:
; Assume memmod, lang, and os specified with /D option
% SUBTITLE Model: memmod Language: lang Operating System: os
Substitution Operator
References to a parameter within a macro can sometimes be ambiguous. In such
cases, the assembler may not expand the argument as you intend. The substitution
operator (&) lets you identify unambiguously any parameter within a macro.
As an example, consider the following macro:
errgen MACRO num, msg
PUBLIC errnum
errnum BYTE “Error num: msg”
ENDM
In each case, the assembler chooses the most literal interpretation. That is, it treats
errnum as a distinct word, and num and msg as literal parts of the string.
The substitution operator can force different interpretations. If we rewrite the macro
with the & operator, it looks like this:
errgen MACRO num, msg
PUBLIC err&num
err&num BYTE “Error &num: &msg”
ENDM
When it encounters the & operator, the assembler interprets subsequent text as a
parameter name until the next & or until the next separator character (such as a
space, tab, or comma). Thus, the assembler correctly parses the expression
err&num because num is delimited by & and a space. The expression could also be
written as err&num&, which again unambiguously identifies num as a parameter.
The rule also works in reverse. You can delimit a parameter reference with & at the
end rather than at the beginning. For example, if num is 5, the expression num&12
resolves to “512.”
The assembler processes substitution operators from left to right. This can have
unexpected results when you are pasting together two macro parameters. For
example, if arg1 has the value var and arg2 has the value 3, you could paste
them together with this statement:
&arg1&&arg2& BYTE “Text”
However, this actually produces the symbol vararg2, because in processing from
left to right, the assembler associates both the first and the second & symbols with
the first parameter. The assembler replaces &arg1& by var, producing vararg2.
The arg2 is never evaluated. The correct abbreviation is:
arg1&&arg2 BYTE “Text”
which produces the desired symbol var3. The symbol arg1&&arg2 is replaced by
var&arg2, which is replaced by var3.
The substitution operator is also necessary if you want to substitute a text macro
inside quotes. For example,
arg TEXTEQU <hello>
%echo This is a string “&arg” ; Produces: This is a string “hello”
%echo This is a string “arg” ; Produces: This is a string “arg”
You can also use the substitution operator in lines beginning with the expansion
operator (%) symbol, even outside macros (see page 236). It may be necessary to
use the substitution operator to paste text macro names to adjacent characters or
symbol names, as shown here:
text TEXTEQU <var>
value TEXTEQU %5
% ECHO textvalue is text&&value
The rules for using the substitution operator have changed significantly since
MASM 5.1, making macro behavior more consistent and flexible. If you have
macros written for MASM 5.1 or earlier, you can specify the old behavior by using
OLDMACROS or M510 with the OPTION directive (see page 24).
This section explains the following four loop directives: REPEAT, WHILE, FOR,
and FORC. In versions of MASM prior to 6.0, REPEAT was called REPT, FOR
was called IRP, and FORC was called IRPC. MASM 6.1 recognizes the old
names.
The assembler evaluates repeat blocks on the first pass only. You should therefore
avoid using address spans as loop counters, as in this example:
REPEAT (OFFSET label1 - OFFSET label2) ; Don't do this!
Since the distance between two labels may change on subsequent assembly passes
as the assembler optimizes code, you should not assume that address spans remain
constant between passes.
Note The REPEAT and WHILE directives should not be confused with the
REPEAT and WHILE directives (see “Loop-Generating Directives” in Chapter
7), which generate loop and jump instructions for run-time program control.
REPEAT Loops
REPEAT is the simplest loop directive. It specifies the number of times to generate
the statements inside the macro. The syntax is:
REPEAT constexpr
statements
ENDM
The constexpr can be a constant or a constant expression, and must contain no
forward references. Since the repeat block expands at assembly time, the number of
iterations must be known then.
Here is an example of a repeat block used to generate data. It initializes an array
containing sequential ASCII values for all uppercase letters.
alpha LABEL BYTE ; Name the data generated
letter = ‘A’ ; Initialize counter
REPEAT 26 ;; Repeat for each letter
BYTE letter ;; Allocate ASCII code for letter
letter = letter + 1 ;; Increment counter
ENDM
WHILE Loops
The WHILE directive is similar to REPEAT, but the loop continues as long as a
given condition is true. The syntax is:
WHILE expression
statements
ENDM
The expression must be a value that can be calculated at assembly time. Normally,
the expression uses relational operators, but it can be any expression that evaluates
to zero (false) or nonzero (true). Usually, the condition changes during the
evaluation of the macro so that the loop won’t attempt to generate an infinite
amount of code. However, you can use the EXITM directive to break out of the
loop.
The following repeat block uses the WHILE directive to allocate variables
initialized to calculated values. This is a common technique for generating lookup
tables. (A lookup table is any list of precalculated results, such as a table of interest
payments or trigonometric values or logarithms. Programs optimized for speed often
use lookup tables, since calculating a value often takes more time than looking it up
in a table.)
cubes LABEL BYTE ;; Name the data generated
root = 1 ;; Initialize root
cube = root * root * root ;; Calculate first cube
WHILE cube LE 32767 ;; Repeat until result too large
WORD cube ;; Allocate cube
root = root + 1 ;; Calculate next root and cube
cube = root * root * root
ENDM
On the first iteration, the arg parameter is replaced with the first argument, the
value 1. On the second iteration, arg is replaced with 2. The result is an array with
the first byte initialized to 1, the next 2 bytes initialized to 2, the next 3 bytes
initialized to 3, and so on.
The argument list is given specifically in this example, but in some cases the list
must be generated as a text macro. The value of the text macro must include the
angle brackets.
arglist TEXTEQU <!<3,6,9!>> ; Generate list as text macro
%FOR arg, arglist
. ; Do something to arg
.
.
ENDM
Note the use of the literal character operator (!) to identify angle brackets as
characters, not delimiters. See “Text Delimiters (< >) and the Literal-Character
Operator,” earlier in this chapter.
The FOR directive also provides a convenient way to process macros with a
variable number of arguments. To do this, add VARARG to the last parameter to
indicate that a single named parameter will have the actual value of all additional
arguments. For example, the following macro definition includes the three possible
parameter attributes — required, default, and variable.
work MACRO rarg:REQ, darg:=<5>, varg:VARARG
The variable argument must always be last. If this macro is called with the
statement
work 4, , 6, 7, a, b
the first argument is received as the value 4, the second is replaced by the default
value 5, and the last four are received as the single argument <6, 7, a, b>. This
is the same format expected by the FOR directive. The FOR directive discards
leading spaces but recognizes trailing spaces.
The following macro illustrates variable arguments:
show MACRO chr:VARARG
mov ah, 02h
FOR arg, <chr>
mov dl, arg
int 21h
ENDM
ENDM
FORC Loops
The FORC directive is similar to FOR, but takes a string of text rather than a list
of arguments. The statements are assembled once for each character (including
spaces) in the string, substituting a different character for the parameter each time
through.
The syntax looks like this:
FORC parameter, < text>
statements
ENDM
The text must be enclosed in angle brackets. The following example illustrates
FORC:
FORC arg, <ABCDEFGHIJKLMNOPQRSTUVWXYZ>
BYTE ‘&arg’ ;; Allocate uppercase letter
BYTE ‘&arg’ + 20h ;; Allocate lowercase letter
BYTE ‘&arg’ - 40h ;; Allocate ordinal of letter
ENDM
Notice that the substitution operator must be used inside the quotation marks to
make sure that arg is expanded to a character rather than treated as a literal string.
With versions of MASM earlier than 6.0, FORC is often used for complex parsing
tasks. A long sentence can be examined character by character. Each character is
then either thrown away or pasted onto a token string, depending on whether it is a
separator character. The new predefined macro functions and string processing
directives discussed in the following section are usually more efficient for these
tasks.
These directives assign a processed value to a text macro or numeric equate. For
example, the following lines
num = 7
newstr CATSTR <3 + >, %num, < = > , %3 + num ; "3 + 7 = 10"
assign the string "3 + 7 = 10" to newstr. CATSTR and SUBSTR assign text
in the same way as the TEXTEQU directive. SIZESTR and INSTR assign a
number in the same way as the = operator. The four string directives take only text
values as arguments. Use the expansion operator (%) when you need to make sure
that constants and numeric equates expand to text, as shown in the preceding lines.
Each of the string directives has a corresponding predefined macro function version:
@SubStr, @InStr, @SizeStr, and @CatStr. Macro functions are similar to the
string directives, but you must enclose their arguments in parentheses. Macro
functions return text values and can appear in any context where text is expected.
The following section, “Returning Values with Macro Functions,” tells how to
write your own macro functions. The following example is equivalent to the
previous CATSTR example:
num = 7
newstr TEXTEQU @CatStr( <3 + >, %num, < = > , %3 + num )
Macro functions are often more convenient than their directive counterparts because
you can use a macro function as an argument to a string directive or to another
macro function. Unlike string directives, predefined macro function names are case
sensitive when you use the /Cp command-line option.
Each string directive and predefined function acts on a string, which can be any
textItem. The textItem can be text enclosed in angle brackets (< >), the name of a
text macro, or a constant expression preceded by % (as in %constExpr). Refer to
Appendix B, “BNF Grammar,” for a list of types that textItem can represent.
The following sections summarize the syntax for each of the string directives and
functions. The explanations focus on the directives, but the functions work the same
except where noted.
SUBSTR
name SUBSTR string, start[[, length]]
@SubStr( string, start[[, length]] )
The SUBSTR directive assigns a substring from a given string to the symbol name.
The start parameter specifies the position in string, beginning with 1, to start the
substring. The length gives the length of the substring. If you do not specify length,
SUBSTR returns the remainder of the string, including the start character.
INSTR
name INSTR [[start,]] string, substring
@InStr( [[start]], string, substring )
The INSTR directive searches a specified string for an occurrence of substring and
assigns its position number to name. The search is case sensitive. The start
parameter is the position in string to start the search for substring. If you do not
specify start, it is assumed to be position 1, the start of the string. If INSTR does
not find substring, it assigns position 0 to name.
The INSTR directive assigns the position value name as if it were a numeric
equate. In contrast, the @InStr returns the value as a string of digits in the current
radix.
The @InStr function has a slightly different syntax than the INSTR directive. You
can omit the first argument and its associated comma from the directive. You can
leave the first argument blank with the function, but a blank function argument must
still have a comma. For example,
pos INSTR <person>, <son>
is the same as
pos = @InStr( , <person>, <son> )
You can also assign the return value to a text macro, like this:
strpos TEXTEQU @InStr( , <person>, <son> )
SIZESTR
name SIZESTR string
@SizeStr( string )
The SIZESTR directive assigns the number of characters in string to name. An
empty string returns a length of zero. The SIZESTR directive assigns the size
value to a name as if it were a numeric equate. The @SizeStr function returns the
value as a string of digits in the current radix.
CATSTR
name CATSTR string[, string]...
@CatStr( string[, string]... )
The CATSTR directive concatenates a list of text values into a single text value
and assigns it to name. TEXTEQU is technically a synonym for CATSTR.
TEXTEQU is normally used for single-string assignments, while CATSTR is
used for multistring concatenations.
The following example pushes and pops one set of registers, illustrating several
uses of string directives and functions:
; SaveRegs - Macro to generate a push instruction for each
; register in argument list. Saves each register name in the
; regpushed text macro.
regpushed TEXTEQU <> ;; Initialize empty string
RestoreRegs MACRO
LOCAL reg
%FOR reg, regpushed ;; Pop each register
pop reg
ENDM
ENDM
Notice how the SaveRegs macro saves its result in the regpushed text macro for
later use by the RestoreRegs macro. In this case, a text macro is used as a global
variable. By contrast, the reg text macro is used only in RestoreRegs. It is
declared LOCAL so it won’t take the name reg from the global name space. The
MACROS.INC file provided with MASM 6.1 includes expanded versions of these
same two macros.
This macro works like the defined operator in the C language. You can use it to test
the defined state of several different symbols with a single statement, as shown
here:
IF DEFINED( DOS ) AND NOT DEFINED( XENIX )
;; Do something
ENDIF
Notice that the macro returns integer values as strings of digits, but the IF statement
evaluates numeric values or expressions. There is no conflict because the assembler
sees the value returned by the macro function exactly as if the user had typed the
values directly into the program:
IF -1 AND NOT 0
Here is another example of a macro function that uses the WHILE directive to
calculate factorials:
factorial MACRO num:REQ
LOCAL i, factor
factor = num
i = 1
WHILE factor GT 1
i = i * factor
factor = factor - 1
ENDM
EXITM %i
ENDM
The integer result of the calculation is changed to a text string with the expansion
operator (%). The factorial macro can define data, as shown here:
var WORD factorial( 4 )
This statement initializes var with the number 24 (the factorial of 4).
You can use @ArgCount inside a macro that has a VARARG parameter, as shown
here:
work MACRO args:VARARG
% ECHO Number of arguments is: @ArgCount( args )
ENDM
Another useful task might be to select an item from an argument list using an index
to indicate the item. The following macro simplifies this.
@ArgI MACRO index:REQ, arglist:VARARG
LOCAL count, retstr
retstr TEXTEQU <> ;; Initialize count
count = 0 ;; Initialize return string
FOR arg, <arglist>
count = count + 1
IF count EQ index ;; Item is found
retstr TEXTEQU <arg> ;; Set return string
EXITM ;; and exit IF
ENDIF
ENDM
EXITM retstr ;; Exit function
ENDM
Finally, you might need to process arguments in reverse order. The following macro
returns a new argument list in reverse order.
@ArgRev MACRO arglist:REQ
LOCAL txt, arg
txt TEXTEQU <>
% FOR arg, <arglist>
txt CATSTR <arg>, <,>, txt ;; Paste each onto list
ENDM
;; Remove terminating comma
txt SUBSTR txt, 1, @SizeStr( %txt ) - 1
txt CATSTR <!<>, txt, <!>> ;; Add angle brackets
EXITM txt
ENDM
These three macro functions appear in the MACROS.INC include file, located on
one of the MASM distribution disks.
The following example demonstrates how one macro can define another. The macro
takes as an argument the name of a shift or rotate instruction, then creates another
macro that simplifies the instruction for 8088/86 processors.
shifts MACRO opname ;; Macro generates macros
opname&s MACRO operand:REQ, rotates:=<1>
IF rotates LE 2 ;; One at a time is faster
REPEAT rotate ;; for 2 or less
opname operand, 1
ENDM
ELSE ;; Using CL is faster for
mov cl, rotates ;; more than 2
opname operand, cl
ENDIF
ENDM
ENDM
Recall that the 8086 processor allows only 1 or CL as an operand for shift and
rotate instructions. Expanding shifts generates a macro for the shift instruction
that uses whichever operand is more efficient. You create the entire series of
macros, one for each shift instruction, like this:
; Call macro repeatedly to make new macros
shifts ror ; Generates rors
shifts rol ; Generates rols
shifts shr ; Generates shrs
shifts shl ; Generates shls
shifts rcl ; Generates rcls
shifts rcr ; Generates rcrs
shifts sal ; Generates sals
shifts sar ; Generates sars
Then use the new macros as replacements for shift instructions, like this:
shrs ax, 5
rols bx, 3
The second method is faster than the first, yet has the same result (with the
byproduct of changing the processor flags).
The following macro illustrates some techniques using OPATTR by loading an
address into a specified offset register:
load MACRO reg:REQ, adr:REQ
IF (OPATTR (adr)) AND 00010000y ;; Register
IFDIFI reg, adr ;; Don’t load register
mov reg, adr ;; onto itself
ENDIF
ELSEIF (OPATTR (adr)) AND 00000100y
mov reg, adr ;; Constant
ELSEIF (TYPE (adr) EQ BYTE) OR (TYPE (adr) EQ SBYTE)
mov reg, OFFSET adr ;; Bytes
ELSEIF (SIZE (TYPE (adr)) EQ 2
mov reg, adr ;; Near pointer
ELSEIF (SIZE (TYPE (adr)) EQ 4
mov reg, WORD PTR adr[0] ;; Far pointer
mov ds, WORD PTR adr[2]
ELSE
.ERR <Illegal argument>
ENDIF
ENDM
A macro also can generate different code depending on the assembly environment.
The predefined text macro @Cpu returns a flag for processor type. The following
example uses the more efficient constant variation of the PUSH instruction if the
processor is an 80186 or higher.
IF @Cpu AND 00000010y
pushc MACRO op ;; 80186 or higher
push op
ENDM
ELSE
pushc MACRO op ;; 8088/8086
mov ax, op
push ax
ENDM
ENDIF
Another macro can now use pushc rather than conditionally testing for processor
type itself. Although either case produces the same code, using pushc assembles
faster because the environment is checked only once.
You can test the language and operating system using the @Interface text macro.
The memory model can be tested with the @Model, @DataSize, or @CodeSize
text macros.
You can save the contexts inside macros with PUSHCONTEXT and
POPCONTEXT. The options for these keywords are:
Option Description
ASSUMES Saves segment register information
RADIX Saves current default radix
LISTING Saves listing and CREF information
CPU Saves current CPU and processor
ALL All of the above
C H A P T E R 1 0
The Windows operating system relies heavily on service routines and data
contained in special libraries called “dynamic-link libraries,” or DLLs for short.
Most of what Windows comprises, from the collections of screen fonts to the
routines that handle the graphical interface, is provided by DLLs. MASM 6.1
contains tools that you can use to write DLLs in assembly language. This chapter
shows you how.
DLLs do not run under MS-DOS. The information in this chapter applies only to
Windows, drawing in part on the chapter “Writing a Module-Definition File” in
Environment and Tools. The acronym API, which appears throughout this chapter,
refers to the application programming interface that Windows provides for
programs. For documentation of API functions, see the Programmer’s Reference,
Volume 2 of the Windows Software Development Kit (SDK).
The first section of this chapter gives an overview of DLLs and their similarities to
normal libraries. The next section explores the parts of a DLL and the rules you
must follow to create one. The third section applies this information to an example
DLL.
Overview of DLLs
A dynamic-link library is similar to a normal run-time library. Both types of
libraries contain a collection of compiled procedures, which serve one or more
calling modules. To link a normal library, the linker copies the required functions
from the library file (which usually has a .LIB extension) and combines them with
other modules to form an executable program in .EXE format. This process is
called static linking.
In dynamic linking, the library functions are not copied to an .EXE file. Instead,
they reside in a separate file in executable form, ready to serve any calling program,
called a “client.” When the first client requires the library, Windows takes care of
loading the functions into memory and establishing linkage. If subsequent clients
also need the library, Windows dynamically links them with the proper library
functions already in memory.
Loading a DLL
How Windows loads a DLL affects the client rather than the DLL itself.
Accordingly, this section focuses on how to set up a client program to use a DLL.
Since the client can itself be a DLL, this is information a DLL programmer should
know. However, MASM 6.1 does not provide all the tools required to create a
stand-alone program for Windows. To create such a program, called an
“application,” you must use tools in the Windows SDK.
Windows provides two methods for loading a dynamic-link library into memory:
Method Description
Implicit loading Windows loads the DLL along with the first client program and links
it before the client begins execution.
Explicit loading Windows does not load the DLL until the first client explicitly
requests it during execution.
When you write a DLL, you do not need to know beforehand which of the two
methods will be used to load the library. The loading method is determined by how
the client is written, not the DLL.
Implicit Loading
The implicit method of loading a DLL offers the advantage of simplicity. The client
requires no extra programming effort and can call the library functions as if they
were normal run-time functions. However, implicit loading carries two constraints:
◆ The name of the library file must have a .DLL extension.
◆ You must either list all DLL functions the client calls in the IMPORTS section
of the client’s module-definition file, or link the client with an import library.
An import library contains no executable code. It consists of only the names and
locations of exported functions in a DLL. The linker uses the locations in the import
library to resolve references to DLL functions in the client and to build an
executable header. For example, the file LIBW.LIB provided with MASM 6.1 is
the import library for the DLL files that contain the Windows API functions.
The IMPLIB utility described in Environment and Tools creates an import library.
Run IMPLIB from the MS-DOS command line like this:
IMPLIB implibfile dllfile
where implibfile is the name of the import library you want to create from the DLL
file dllfile. Once you have created an import library from a DLL, link it with a
client program that relies on implicit loading, but does not list imported functions in
its module-definition file. Continuing the preceding example, here’s the link step for
a client program that calls library procedures in the DLL dllfile:
LINK client.OBJ, client.EXE, , implibfile, client.DEF
This simplified example creates the client program client.EXE, linking it with the
import library implibfile, which in turn was created from the DLL file dllfile.
To summarize implicit loading, a client program must either
◆ List DLL functions in the IMPORTS section of its module-definition file, or
◆ Link with an import library created from the DLL.
Implicit loading is best when a client always requires at least one procedure in the
library, since Windows automatically loads the library with the client. If the client
does not always require the library service, or if the client must choose at run time
between several libraries, you should use explicit loading, discussed next.
Explicit Loading
To explicitly load a DLL, the client does not require linking with an import library,
nor must the DLL file have an extension of .DLL. Explicit loading involves three
steps in which the client calls Windows API functions:
1. The client calls LoadLibrary to load the DLL.
2. The client calls GetProcAddress to obtain the address of each DLL function it
requires.
3. When finished with the DLL, the client calls FreeLibrary to unload the DLL
from memory.
The following example fragment shows how a client written in assembly language
explicitly loads a DLL called SYSINFO.DLL and calls the DLL function
GetSysDate.
INCLUDE windows.inc
.DATA
hInstance HINSTANCE 0
szDLL BYTE 'SYSINFO.DLL', 0
szDate BYTE 'GetSysDate', 0
lpProc DWORD 0
.CODE
.
.
.
INVOKE LoadLibrary, ADDR szDLL ; Load SYSINFO.DLL
mov hInstance, ax ; Save instance count
INVOKE GetProcAddress, ax, ADDR szDate ; Get and save
mov lpProc, ax ; far address of
mov lpProc[2], dx ; GetSysDate
call lpProc ; Call GetSysDate
.
.
.
INVOKE FreeLibrary, hInstance ; Unload SYSINFO.DLL
If Windows does not locate the DLL in any of these directories, it prompts the user
with a message box.
Building a DLL
A DLL has additional programming requirements beyond those for a normal run-
time library. This section describes the requirements pertaining to the library’s
code, data, and stack. It also discusses the effects of the library’s extension name.
DLL Code
The code in a DLL consists of exported and nonexported functions. Exported
functions, listed in the EXPORTS section of the module-definition file, are public
routines serving clients. Nonexported functions provide private, internal support for
the exported procedures. They are not visible to a client.
Under Windows, an exported library routine must appear to the caller as a far
procedure. Your DLL routines can use any calling convention you wish, provided
the caller assumes the same convention. You can think of dynamic-link code as
code for a normal run-time library with the following additions:
◆ An entry procedure
◆ A termination procedure
◆ Special prologue and epilogue code
Entry Procedure
A DLL, like any Windows-based program, must have an entry procedure. Windows
calls the entry procedure only once when it first loads the DLL, passing the
following information in registers:
◆ DS contains the library’s data segment address.
◆ DI holds the library’s instance handle.
◆ CX holds the library’s heap size in bytes.
Note Windows API functions destroy all registers except DI, SI, BP, DS, and the
stack pointer. To preserve the contents of other registers, your program must save
the registers before an API call and restore them afterwards.
a fixed data segment than the amount specified in the HEAPSIZE statement. In any
case, a library’s total heap cannot exceed 64K, less the amount of static data. Static
data and heap reside in the same segment.
Windows does not automatically deallocate unneeded heap while the DLL is in
memory. Therefore, you should not set an unnecessarily large value in the
HEAPSIZE statement, since doing so wastes memory.
The entry procedure calls the Windows API function LocalInit to allocate the heap.
The library must create a heap before its routines call any heap functions, such as
LocalAlloc. The following example illustrates these steps:
DLLEntry PROC FAR PASCAL PUBLIC ; Entry point for DLL
DLLEntry ENDP
This example code is taken from the DLLENTRY.ASM module, contained in the
LIB subdirectory on one of the MASM 6.1 distribution disks. After allocating the
heap, the procedure calls the library’s initialization procedure — called LibMain in
this case. LibMain initializes the library’s static data (if required), then returns to
DLLEntry, which returns to Windows. If Windows receives a return value of 0
(FALSE) from DLLEntry, it unloads the library and displays an error message.
The process is similar to the way MS-DOS loads a terminate-and-stay-resident
program (TSR), described in the next chapter. Both the DLL and TSR return
control immediately to the operating system, then wait passively in memory to be
called.
The following section explains how a DLL gains control when Windows unloads it
from memory.
Termination Procedure
Windows maintains a DLL in memory until the last client program terminates or
explicitly unloads the library. When unloading a DLL, Windows first calls the
library’s termination procedure. This allows the DLL to return resources and do any
necessary cleanup operations before Windows unloads the library from memory.
Libraries that have registered window procedures with RegisterClass need not call
UnregisterClass to remove the class registration. Windows does this automatically
when it unloads the library.
You must name the library’s termination procedure WEP (for Windows Exit
Procedure) and list it in the EXPORTS section of the library’s module-definition
file. To ensure immediate operation, provide an ordinal number and use the
RESIDENTNAME keyword, as described in the chapter “Creating Module-
Definition Files” in Environment and Tools. This keeps the name “WEP” in the
Windows-resident name table at all times.
Besides its name, the code for WEP should also remain constantly in memory. To
ensure this, place WEP in its own code segment and set the segment’s attributes as
PRELOAD FIXED in the SEGMENTS statement of the module-definition file.
Thus, your DLL code should use a memory model that allows multiple code
segments, such as medium model. Since a termination procedure is usually short,
keeping it resident in memory does not burden the operating system.
The termination procedure accepts a single parameter, which can have one of two
values. These values are assigned to the following symbolic constants in the
WINDOWS.INC file located in the LIB subdirectory:
◆ WEP_SYSTEM_EXIT (value 1) indicates Windows is shutting down.
◆ WEP_FREE_DLL (value 0) indicates the library’s last client has terminated or
has called FreeLibrary, and Windows is unloading the DLL.
WEP ENDP
Usually, the WEP procedure takes the same actions regardless of the parameter
value, since in either case Windows will unload the DLL.
Under Windows 3.0, the WEP procedure receives stack space of about 256 bytes.
This allows the procedure to unhook interrupts, but little else. Any other action,
such as calling an API function, usually results in an unrecoverable application
error because of stack overflow. Later versions of Windows provide at least 4K of
stack to the WEP procedure, allowing it to call many API functions.
However, WEP should not send or post a message to a client, because the client may
already be terminated. The WEP procedure should also not attempt file I/O, since
only application processes — not DLLs — can own files. When control reaches WEP,
the client may no longer exist and its files are closed.
The instruction
inc bp
marks the beginning of the stack frame with an odd number. This allows real-mode
Windows to locate segment addresses on the stack and update the addresses when it
moves or discards the corresponding segments. In protected mode, selector values
do not change when segments are moved, so marking the stack frame is not
required. However, certain debugging applications, such as Microsoft Codeview for
Windows and the Microsoft Windows 80386 Debugger (both documented in
Programming Tools of the SDK), search for a marked frame to determine if the
frame belongs to a far procedure. Without the mark, these debuggers give
meaningless information when backtracing through the stack. Therefore, you should
include the INC BP instruction for Windows-based programs that may run in real
mode or that require debugging with a Microsoft debugger.
Another characteristic of the prologue macro may seem puzzling at first glance. The
macro moves DS into AX, then AX back into DS. This sequence of instructions lets
Windows selectively overwrite the prologue code in far procedures. When
Windows loads a program, it compares the names of far procedures with the list of
exported procedures in the module-definition file. For procedures that do not appear
on the list, Windows leaves their prologue code untouched. However, Windows
overwrites the first 3 bytes of all exported procedures with
mov ax, DGROUP
where DGROUP represents the selector value for the library’s data segment. This
explains why the prologue macro reserves the third byte with a NOP instruction.
The 1-byte instruction serves as padding to provide a 3-byte area for Windows to
overwrite.
The epilogue code returns BP to normal, like this:
Epilog MACRO
pop ds ; Recover original DS
pop bp ; and BP+1
dec bp ; Reset to original BP
ENDM
DLL Data
A DLL can have its own local data segment up to 64K. Besides static data, the
segment contains the heap from which a procedure can allocate memory through the
LocalAlloc API function. You should minimize static data in a DLL to reserve as
much memory as possible for temporary allocations. Furthermore, all procedures in
the DLL draw from the same heap space. If more than one procedure in the library
accesses the heap, a procedure should not hold allocated space unnecessarily at the
expense of the other procedures.
A Windows-based program must reserve a “task header” in the first 16 bytes of its
data segment. If you link your program with a C run-time function, the C startup
code automatically allocates the task header. Otherwise, you must explicitly reserve
and initialize the header with zeros. The sample program described in “Example of
a DLL:SYSINFO,” page 267, shows how to allocate a task header.
DLL Stack
A DLL does not declare a stack segment and does not allocate stack space. A client
program calls a library’s exported procedure through a simple far call, and the stack
does not change. The procedure is, in effect, part of the calling program, and
therefore uses the caller’s stack.
This simple arrangement differs from that used in small and medium models, in
which many C run-time functions accept near pointers as arguments. Such functions
assume the pointer is relative to the current data segment. In applications, the call
works even if the argument points to a local variable on the stack, since DS and SS
contain the same segment address.
However, in a DLL, DS and SS point to different segments. Under small and
medium models, a library procedure must always pass pointers to static variables
located in the data segment, not to local variables on the stack.
When you write a DLL, include the FARSTACK keyword with the .MODEL
directive, like this:
.MODEL small, pascal, farstack
This informs the assembler that SS points to a segment other than DGROUP. With
full segment definitions, also add the line:
ASSUME DS:DGROUP, SS:NOTHING
Summary
Following is a summary of the previous information in this chapter.
◆ A dynamic-link library has only one instance — that is, it can load only once
during a Windows session.
◆ A single DLL can service calls from many client programs. Windows takes care
of linkage between the DLL and each client.
◆ Windows loads a DLL either implicitly (along with the first client) or explicitly
(when the first client calls LoadLibrary). It unloads the DLL when the last
client either terminates or calls FreeLibrary.
◆ A client calls a DLL routine as a simple far procedure. The routine can use any
calling convention.
◆ Windows ensures that the first instruction in a DLL procedure moves the
address of the library’s data segment into AX. You must provide the proper
prologue code to allow space for this 3-byte instruction and to copy AX to DS.
◆ All procedures in a DLL have access to a single common data segment. The
segment contains both static variables and heap space, and cannot exceed 64K.
◆ A DLL procedure uses the caller’s stack.
◆ All exported procedures in a DLL must appear in the EXPORTS list in the
library’s module-definition file.
To see SYSINFO in action, follow the steps below. The file SYSDATA.EXE
resides in the SAMPLES\WINDLL subdirectory of MASM if you requested
example files when installing MASM. Otherwise, you must first install the file with
the MASM 6.1 SETUP utility.
◆ Create SYSINFO.DLL as described in the following section and place it in the
SAMPLES\WINDLL subdirectory for MASM 6.1.
◆ From the Windows File Manager, make the SAMPLES\WINDLL subdirectory
the current directory.
◆ In the Program Manager, choose Run from the File menu and type
SYSDATA
to run the example program SYSDATA.EXE. This program calls the routines in
SYSINFO.DLL and displays the returned data.
DLLInit ENDP
END DLLInit
Whatever you call your combined procedure (DLLInit in the preceding example),
place the name on the END statement as shown. This identifies the procedure as the
one that first executes when Windows loads the DLL.
SYSINFO accommodates several international languages. Currently, SYSINFO
recognizes English, French, Spanish, German, Italian, and Swedish, but you can
easily extend the code to include other languages. LibMain calls GetProfileString
to determine the current language, then initializes the variable indx accordingly.
The variable indirectly points to an array of strings containing days and months in
different languages. The GetSysDate procedure uses these strings to create a full
date in the correct language.
Static Data
SYSINFO stores the strings in its static data segment. This data remains in memory
along with the library’s code. All procedures have equal access to the data segment.
Because the library does not call any C run-time functions, it explicitly allocates the
low paragraph of the data segment with the variable TaskHead. This 16-byte area
serves as the required Windows task header, described in “DLL Data,” earlier in
this chapter.
Module-Definition File
The library’s module-definition file, named SYSINFO.DEF, looks like this:
LIBRARY SYSINFO
DESCRIPTION 'Sample assembly-language DLL'
EXETYPE WINDOWS
CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD MOVEABLE SINGLE
SEGMENTS CODE2 PRELOAD FIXED
EXPORTS WEP @1 RESIDENTNAME
GetSysTime @2
GetSysDate @3
GetSysInfo @4
Expanding SYSINFO
SYSINFO is an example of how to write an assembly-language DLL without
overwhelming detail. It has plenty of room for expansion and improvements. The
following list may give you some ideas:
◆ To create a heap area for the library, add the line
HEAPSIZE value
◆ Each time the GetSysInfo procedure is called, it retrieves the version number
of MS-DOS and Windows, gets the processor type, checks for a coprocessor,
and reads the ROM-BIOS release date. Since this information does not change
throughout a Windows session, it would be handled more efficiently in the
LibMain procedure, which executes only once. The code is currently placed in
GetSysInfo for the sake of clarity at the expense of efficiency.
◆ SYSINFO is not a true international program. You can easily add more
languages, extending the days and months arrays accordingly. Moreover, for
the sake of simplicity, the GetSysDate procedure arranges the date with an
American bias. For example, in many parts of the world, the date numeral
appears before the month rather than after. If you use SYSINFO in your own
applications, you should include code in LibMain to determine the correct date
format with additional calls to GetProfileString. You can find more
information on how to do this in Chapter 18 of the Microsoft Windows
Programmer’s Reference, Volume 1, supplied with the Windows SDK.
C H A P T E R 1 1
Terminate-and-Stay-Resident Programs
MS-DOS maintains a pointer to the beginning of unused memory. Programs load
into memory at this position and terminate execution by returning control to MS-
DOS. Normally, the pointer remains unchanged, allowing MS-DOS to reuse the
same memory when loading other programs.
A terminating program can, however, prevent other programs from loading on top
of it. These programs exit to MS-DOS through the terminate-and-stay-resident
function, which resets the free-memory pointer to a higher position. This leaves the
program resident in a protected block of memory, even though it is no longer
running.
Structure of a TSR
TSRs consist of two distinct parts that execute at different times. The first part is
the installation section, which executes only once, when MS-DOS loads the
program. The installation code performs any initialization tasks required by the
TSR and then exits through the terminate-and-stay-resident function.
The second part of the TSR, called the resident section, consists of code and data
left in memory after termination. Though often identified with the TSR itself, the
resident section makes up only part of the entire program.
The TSR’s resident code must be able to regain control of the processor and execute
after the program has terminated. Methods of executing a TSR are classified as
either passive or active.
Passive TSRs
The simplest way to execute a TSR is to transfer control to it explicitly from
another program. Because the TSR in this case does not solicit processor control, it
is said to be passive. If the calling program can determine the TSR’s memory
address, it can grant control via a far jump or call. More commonly, a program
activates a passive TSR through a software interrupt. The installation section of the
TSR writes the address of its resident code to the proper position in the interrupt
vector table (see “MS-DOS Interrupts” in Chapter 7). Any subsequent program can
then execute the TSR by calling the interrupt.
Passive TSRs often replace existing software interrupts. For example, a passive
TSR might replace Interrupt 10h, the BIOS video service. By intercepting calls that
read or write to the screen, the TSR can access the video buffer directly, increasing
display speed.
Passive TSRs allow limited access since they can be invoked only from another
program. They have the advantage of executing within the context of the calling
program, and thus run no risk of interfering with another process. Such a risk does
exist with active TSRs.
Active TSRs
The second method of executing a TSR involves signaling it through some hardware
event, such as a predetermined sequence of keystrokes. This type of TSR is “active”
because it must continually search for its startup signal. The advantage of active
TSRs lies in their accessibility. They can take control from any running application,
execute, and return, all on demand.
An active TSR, however, must not seize processor control blindly. It must contain
additional code that determines the proper moment at which to execute. The extra
code consists of one or more routines called “interrupt handlers,” described in the
following section.
A TSR running on a PS/2 computer cannot reliably read key scan codes using this
method. Instead, the TSR must search for its hot key through a handler for Interrupt
15h (Miscellaneous System Services). The handler determines the current keypress
from the AL register when AH equals 4Fh, as shown here:
MiscServ PROC FAR
sti ; Interrupts okay
.IF ah == 4Fh ; If Keyboard Intercept Service:
call CheckHotKey ; Check for hot key
.IF carry? ; If hot key pressed,
mov cs:TsrRequestFlag, TRUE ; raise flag and
. ; set up for exit
.
.
The example program on page 293 shows how a TSR tests for a PS/2 machine and
then sets up a handler for either Interrupt 09 or Interrupt 15h to audit keystrokes.
Setting a request flag in the keyboard handler allows other code, such as the timer
handler (Interrupt 08), to recognize a request for the TSR. The timer handler gains
control at every timer interrupt, which occurs an average of 18.2 times per second.
The following fragment shows how a timer handler tests the request flag and
continually polls until it can safely execute the TSR.
NewTimer PROC FAR
.
.
.
cmp TsrRequestFlag, FALSE ; Has TSR been requested?
.IF !zero? ; If so, can system be
call CheckSystem ; interrupted safely?
.IF carry? ; If so,
call ActivateTsr ; activate TSR
.
.
.
Only hardware used by the TSR requires monitoring. For example, a TSR that
performs disk input/output (I/O) must monitor disk use through Interrupt 13h. The
disk handler sets an active flag that prevents the TSR from executing during a read
or write operation. Otherwise, the TSR’s own I/O would move the disk head. This
would cause the suspended disk operation to continue with the head incorrectly
positioned when the TSR returned control to the interrupted program.
In the same way, an active TSR that displays to the screen must monitor calls to
Interrupt 10h. The Interrupt 10h BIOS routine does not protect critical sections of
code that program the video controller. The TSR must therefore ensure it does not
interrupt such nonreentrant operations.
The activities of the operating system also affect the system status. With few
exceptions, MS-DOS functions are not reentrant and must not be interrupted.
TSR
TsrActiveFlag clear . . . . . . . . . . . . . . . . . . . . . . . . . . set . . . . . . . . . . . . . . . . . .
1 2 3 4 5 6
Figure 11.1 Time Line of Interactions Between Interrupt Handlers for a Typical
TSR
The following comments describe the chain of events depicted in Figure 11.1. Each
comment refers to one of the numbered pointers in the figure.
1. At time = t, the timer handler activates. It finds the flag TsrRequestFlag
clear, indicating the user has not requested the TSR. The handler terminates
without taking further action. Notice that Interrupt 13h is currently processing a
disk I/O operation.
2. Before the next timer interrupt, the keyboard handler detects the hot key,
signaling a request for the TSR. The keyboard handler sets TsrRequestFlag
and returns.
3. At time = t + 1/18 second, the timer handler again activates and finds
TsrRequestFlag set. The handler checks other active flags to determine if the
TSR can safely execute. Since Interrupt 13h has not yet completed its disk
operation, the timer handler finds DiskActiveFlag set. The handler therefore
terminates without activating the TSR.
4. At time = t + 2/18 second, the timer handler again finds TsrRequestFlag set
and repeats its scan of the active flags. DiskActiveFlag is now clear, but in
the interim, Interrupt 10h has activated as indicated by the flag
VideoActiveFlag. The timer handler accordingly terminates without
activating the TSR.
5. At time = t + 3/18 second, the timer handler repeats the process. This time it
finds all active flags clear, indicating the TSR can safely execute. The timer
handler calls the TSR, which sets its own active flag to ensure it will not
interrupt itself if requested again.
6. The timer and other interrupts continue to function normally while the TSR
executes.
The timer itself can serve as the startup signal if the TSR executes periodically.
Screen clocks that continuously show seconds and minutes are examples of TSRs
that use the timer this way. ALARM.ASM, a program described in the next section,
shows another example of a timer-driven TSR.
The installation section of the program begins with the Install procedure.
Install computes the number of five-second intervals that must elapse before the
alarm sounds and stores this number in the word CountDown. The procedure then
obtains the vector for Interrupt 08 (timer) through MS-DOS Function 35h and
stores it in the far pointer OldTimer. Function 25h replaces the vector with the far
address of the new timer handler NewTimer. Once installed, the new timer handler
executes at every timer interrupt. These interrupts occur 18.2 times per second or
91 times every five seconds.
Each time it executes, NewTimer subtracts one from a secondary counter called
Tick91. By counting 91 timer ticks, Tick91 accurately measures a period of five
seconds. When Tick91 reaches zero, it’s reset to 91 and CountDown is
decremented by one. When CountDown reaches zero, the alarm sounds.
;* ALARM.ASM - A simple memory-resident program that beeps the speaker
;* at a prearranged time. Can be loaded more than once for multiple
;* alarm settings. During installation, ALARM establishes a handler
;* for the timer interrupt (Interrupt 08). It then terminates through
;* the terminate-and-stay-resident function (Function 31h). After the
;* alarm sounds, the resident portion of the program retires by setting
;* a flag that prevents further processing in the handler.
; Data must be in code segment so it won’t be thrown away with Install code.
OldTimer DWORD ? ; Address of original timer routine
tick_91 BYTE 91 ; Counts 91 clock ticks (5 seconds)
TimerActiveFlag BYTE 0 ; Active flag for timer handler
Install PROC
mov dl, dh
sub dh, dh
push dx ; Save DX = current seconds
mov al, 60 ; Multiply current hour by 60
mul ch ; to convert to minutes
sub ch, ch
add cx, ax ; Add current minutes to result
; CX = minutes since midnight
mov al, 60 ; Multiply alarm hour by 60
mul bh ; to convert to minutes
sub bh, bh
add ax, bx ; AX = number of minutes since
; midnight for alarm setting
sub ax, cx ; AX = time in minutes to elapse
; before alarm sounds
.IF carry? ; If alarm time is tomorrow,
add ax, 24 * 60 ; add minutes in a day
.ENDIF
mov bx, 60
mul bx ; DX:AX = minutes-to-elapse-times-60
pop bx ; Recover current seconds
sub ax, bx ; DX:AX = seconds to elapse before
sbb dx, 0 ; alarm activates
.IF carry? ; If negative,
mov ax, 5 ; assume 5 seconds
cwd
.ENDIF
.ENDIF
mov bx, 5 ; Divide result by 5 seconds
div bx ; AX = number of 5-second intervals
mov CountDown, ax ; to elapse before alarm sounds
The solution lies in determining when the low-order functions 01 through 0Ch are
active. MS-DOS provides another service for this purpose: Interrupt 28h, the Idle
Interrupt.
TSRs that perform tasks of long or indefinite duration should themselves call
Interrupt 28h. For example, a TSR that polls for keyboard input should include an
INT 28h instruction in the polling loop, as shown here:
poll: int 28h ; Signal idle state
mov ah, 1
int 16h ; Key waiting?
jnz poll ; If not, repeat polling loop
sub ah, ah
int 16h ; Otherwise, get key
This courtesy gives other TSRs a chance to execute if the InDos flag happens to be
set.
The question mark inside brackets in the preceding PUSH statement indicates that
the operand for the PUSH instruction can be any legal operand.
In either version of MS-DOS, the operand field in the first instruction gives the
flag’s offset. The value in ES determines the segment address. “Example of an
Advanced TSR: SNAP,” later in the chapter, presents a program that shows how to
locate the Critical Error flag with this technique.
Preventing Interference
This section describes how an active TSR can avoid interfering with the process it
interrupts. Interference occurs when a TSR commits an error or performs an action
that affects the interrupted process after the TSR returns. Examples of interference
range from relatively harmless, such as moving the cursor, to serious, such as
overrunning a stack.
Although a TSR can interfere with another process in many different ways,
protection against interference involves only three steps:
1. Recording a current configuration
2. Changing the configuration so it applies to the TSR
3. Restoring the original configuration before terminating
The example program described on page 293 demonstrates all the noninterference
safeguards described in this section. These safeguards by no means exhaust the
subject of noninterference. More sophisticated TSRs may require more
sophisticated methods. However, noninterference methods generally fall into one of
the following categories:
◆ Trapping errors
◆ Preserving an existing condition
◆ Preserving existing data
Trapping Errors
A TSR committing an error that triggers an interrupt must handle the interrupt to
trap the error. Otherwise, the existing interrupt routine, which belongs to the
underlying process, would attempt to service an error the underlying process did not
commit.
For example, a TSR that accepts keyboard input should include handlers for
Interrupts 23h and 1Bh to trap keyboard break signals. When MS-DOS detects
CTRL+C from the keyboard or input stream, it transfers control to Interrupt 23h
(CTRL+C Handler). Similarly, the BIOS keyboard routine calls Interrupt 1Bh
(CTRL+BREAK Handler) when it detects a CTRL+BREAK key combination. Both
routines normally terminate the current process.
A TSR that calls MS-DOS should also trap critical errors through Interrupt 24h
(Critical Error Handler). MS-DOS functions call Interrupt 24h when they encounter
certain hardware errors. The TSR must not allow the existing interrupt routine to
service the error, since the routine might allow the user to abort service and return
control to MS-DOS. This would terminate both the TSR and the underlying
process. By handling Interrupt 24h, the TSR retains control if a critical error
occurs.
An error-trapping handler differs in two ways from a TSR’s other handlers:
1. It is temporary, in service only while the TSR executes. At startup, the TSR
copies the handler’s address to the interrupt vector table; it then restores the
original vector before returning.
2. It provides complete service for the interrupt; it does not pass control on to the
original routine.
Error-trapping handlers often set a flag to let the TSR know the error has occurred.
For example, a handler for Interrupt 1Bh might set a flag when the user presses
CTRL+BREAK. The TSR can check the flag as it polls for keyboard input, as shown
here:
BrkHandler PROC FAR ; Handler for Interrupt 1Bh
.
.
.
mov cs:BreakFlag, TRUE ; Raise break flag
iret ; Terminate interrupt
BrkHandler ENDP
.
.
.
mov BreakFlag, FALSE ; Initialize break flag
poll: .
.
.
cmp BreakFlag, TRUE ; Keyboard break pressed?
je exit ; If so, break polling loop
mov ah, 1
int 16h ; Key waiting?
jnz poll ; If not, repeat polling loop
A TSR that alters the video configuration should return the configuration to its
original state upon return. Video configuration includes cursor position, cursor
shape, and video mode. The services provided through Interrupt 10h enable a TSR
to determine the existing configuration and alter it if necessary.
However, some applications set video parameters by directly programming the
video controller. When this happens, BIOS remains unaware of the new
configuration and consequently returns inaccurate information to the TSR.
Unfortunately, there is no solution to this problem if the controller’s data registers
provide write-only access and thus cannot be queried directly. For more information
on video controllers, refer to Richard Wilton, Programmer’s Guide to the PC &
PS/2 Video Systems. (See “Books for Further Reading” in the Introduction.)
The last method offers the most flexibility. It finds an identity number not currently
in use among the installed multiplex handlers and does not require intervention from
the user.
To use this method, a TSR calls Interrupt 2Fh during installation with AH = 192
and AL = 0. If the call returns AL = 0FFh, the program tests other registers to
determine if it has found a prior installation of itself. If the test fails, the program
resets AL to zero, increments AH to 193, and again calls Interrupt 2Fh. The process
repeats with incrementing values in AH until the TSR locates a prior installation of
itself — in which case it should abort with an appropriate message to the user — or
until AL returns as zero. The TSR can then use the value in AH as its identity
number and proceed with installation.
The SNAP.ASM program in this chapter demonstrates how a TSR can use this
trial-and-error method to select a unique identity number. During installation, the
program calls Interrupt 2Fh to verify that SNAP is not already installed. When
deinstalling, the program again calls Interrupt 2Fh to locate the resident TSR in
memory. SNAP’s multiplex handler services the call and returns the address of the
resident code’s program-segment prefix. The calling program can then locate the
resident code and deinstall it, as explained in “Deinstalling a TSR,” following.
Deinstalling a TSR
A TSR should provide a means for the user to remove or “deinstall” it from
memory. Deinstallation returns occupied memory to the system, offering these
benefits:
◆ The freed memory becomes available to subsequent programs that may require
additional memory space.
◆ Deinstallation restores the system to a normal state. Thus, sensitive programs
that may be incompatible with TSRs can execute without the presence of
installed routines.
The example program in the next section demonstrates how to locate a resident TSR
through its multiplex handler, and deinstall it from memory.
SNAP can deinstall itself, provided another TSR has not been loaded after it.
Deinstall SNAP by executing the main program with the /D option:
SNAP /D
Building SNAP.EXE
SNAP combines four modules: SNAP.ASM, COMMON.ASM,
HANDLERS.ASM, and INSTALL.ASM. Source files are located on one of your
distribution disks. Each module stores temporary code and data in the segments
INSTALLCODE and INSTALLDATA. These segments apply only to SNAP’s
installation phase; MS-DOS recovers the memory they occupy when the program
exits through the terminate-and-stay-resident function. The following briefly
describes each module:
◆ SNAP.ASM contains the TSR’s main code and data.
◆ COMMON.ASM contains procedures used by other example programs.
◆ HANDLERS.ASM contains interrupt handler routines for Interrupts 08, 09,
10h, 13h, 15h, 28h, and 2Fh. It also provides simple error-trapping handlers for
Interrupts 1Bh, 23h, and 24h. Additional routines set up and deinstall the
handlers.
◆ INSTALL.ASM contains an exit routine that calls the terminate-and-stay-
resident function and a deinstallation routine that removes the program from
memory. The module includes error-checking services and a command-line
parser.
Code written in a high-level language must not check for stack overflows.
Compiler-generated stack probes do not recognize the new stack setup when the
TSR executes, and therefore must be disabled. The example program BELL.C,
included on disk with the TSR library routines, demonstrates how to disable stack
checking in Microsoft C using the check_stack pragma.
Outline of SNAP
The following sections outline in detail how SNAP works. Each part of the outline
covers a specific portion of SNAP’s code. Headings refer to earlier sections of this
chapter, providing cross-references to SNAP’s key procedures. For example, the
part of the outline that describes how SNAP searches for its startup signal refers to
the section “Auditing Hardware Events for TSR Requests,” earlier in this chapter.
Figures 11.2 through 11.4 are flowcharts of the SNAP program. Each chart
illustrates a separate phase of SNAP’s operation, from installation through
memory-residency to deinstallation.
Begin
Program entry point
· Call Install
Install
Set up TSR's interrupt handlers
· Call GetVersion
GetVersion
Determine DOS version
If not version 2.0 or higher,
return with error code
· Call GetDosFlags
GetDosFlags
Get addresses of InDos and
Critical Error flags
· Call CallMultiplex
CallMultiplex
Locate and call multiplex handler
If TSR already installed,
return with error code
· Replace interrupt vectors with
addresses of following handlers:
Clock - Interrupt 08h
Video - Interrupt 10h
DiskIO - Interrupt 13h
Idle - Interrupt 28h
Multiplex - Interrupt 2Fh
Keybrd - Interrupt 09h (non-PS/2)
SkipMiscServ - Interrupt 15h (non-PS/2)
KeybrdMonitor - Interrupt 09h (PS/2)
MiscServ - Interrupt 15h (PS/2)
· Return
If error, call FatalError
FatalError
· Display error message
· Terminate through Function 4Ch
· Call KeepTsr
KeepTsr
Terminate through Function 31h to
make program memory resident
Begin
Program entry point
· Call Deinstall
Deinstall
Restore original interrupt vectors
· Call GetVersion
GetVersion
Determine DOS version
Refer to the flowcharts as you read the following outline. They will help you
maintain perspective while exploring the details of SNAP’s operation. Text in the
outline cross-references the charts.
Note that information in both the outline and the flowcharts is generic. Except for
references to the SNAP procedure, all descriptions in the outline and the flowcharts
apply to any TSR created with the HANDLERS and INSTALL modules.
; Test for IBM PS/2 series. If not PS/2, use Keybrd and
; SkipMiscServ as handlers for Interrupts 09 and 15h
; respectively. If PS/2 system, set up KeybrdMonitor as the
; Interrupt 09 handler. Audit keystrokes with MiscServ
; handler, which searches for the hot key by handling calls
; to Interrupt 15h (Miscellaneous System Services). Refer to
; Section 11.2.1 for more information about keyboard handlers.
The jump immediately passes control to the original Interrupt 15h routine; thus,
SkipMiscServ has no effect. It serves only to simplify coding in other parts of
the program.
The BIOS Interrupt 13h routine clears or sets the carry flag to indicate the
operation’s success or failure. DiskIO therefore preserves the flags register when
returning, as shown here:
DiskIO PROC FAR
mov cs:intDiskIO.Flag, TRUE ; Set active flag
; Simulate interrupt by pushing flags and far-calling old
; Int 13h routine
pushf
call cs:intDiskIO.OldHand
; Clear active flag without disturbing flags register
mov cs:intDiskIO.Flag, FALSE
sti ; Enable interrupts
; Simulate IRET without popping flags (since services use
; carry flag)
ret 2
DiskIO ENDP
The terminating RET 2 instruction discards the original flags from the stack when
the handler returns.
Each time it executes, the timer handler Clock calls CheckRequest to read the
flag TsrRequestFlag. If CheckRequest finds the flag set, it scans other flags
maintained by the TSR’s interrupt handlers and by MS-DOS. These flags indicate
the current system status. As the flowchart in Figure 11.3 shows, CheckRequest
calls CheckDos (described following) to determine the status of the operating
system. CheckRequest then calls CheckHardware to check hardware status.
CheckHardware queries the interrupt controller to determine if any device is
currently being serviced. It also reads the active flags maintained by the
KeybrdMonitor, Video, and DiskIO handlers. If the controller, keyboard, video,
and disk are all inactive, CheckHardware clears the carry flag and returns.
CheckRequest indicates system status with the carry flag. If the procedure returns
the carry flag set, the caller exits without invoking the TSR. A clear carry signals
that the caller can safely execute the TSR.
For more information on this topic, see the section “Interrupting MS-DOS
Functions,” earlier in this chapter.
When called from either the Clock or Idle handlers, CheckDos reads the Critical
Error flag. A nonzero value in the flag indicates that the Critical Error Handler
(Interrupt 24h) is processing a critical error and the TSR must not interrupt. In this
case, CheckDos sets the carry flag and returns, causing the caller to exit without
executing the TSR.
Trapping Errors
As Figure 11.3 shows, Clock and Idle invoke the TSR by calling the procedure
Activate. Before calling the main body of the TSR, Activate sets up the
following handlers:
Handler Name For Interrupt Receives Control When
CtrlBreak 1Bh (CTRL+BREAK Handler) CTRL+BREAK sequence entered at
keyboard
CtrlC 23h (CTRL+C Handler) MS-DOS detects a CTRL+C sequence
from the keyboard or input stream
CritError 24h (Critical Error Handler) MS-DOS encounters a critical error
These handlers trap keyboard break signals and critical errors that would otherwise
trigger the original handler routines. The CtrlBreak and CtrlC handlers contain
a single IRET instruction, thus rendering a keyboard break ineffective. The
CritError handler contains the following instructions:
The return code in AL stops MS-DOS from taking further action when it encounters
a critical error.
As an added precaution, Activate also calls Function 33h (Get or Set
CTRL+BREAK Flag) to determine the current setting of the checking flag.
Activate stores the setting, then calls Function 33h again to turn off break
checking.
When the TSR’s main procedure finishes its work, it returns to Activate, which
restores the original setting for the checking flag. It also replaces the original
vectors for Interrupts 1Bh, 23h, and 24h.
SNAP’s error-trapping safeguards enable the TSR to retain control in the event of
an error. Pressing CTRL+BREAK or CTRL+C at SNAP’s prompt has no effect. If the
user specifies a nonexistent drive — a critical error — SNAP merely beeps the
speaker and returns normally.
SNAP does not alter the application’s video configuration other than by moving the
cursor. Figure 11.3 shows that Activate calls the procedure Snap, which
executes Interrupt 10h to determine the current cursor position. Snap stores the row
and column in the word OldPos. The procedure restores the cursor to its original
location before returning to Activate.
For more information about Interrupt 2Fh, see the section “Communicating through
the Multiplex Interrupt,” earlier in this chapter.
SNAP accesses Interrupt 2Fh through the procedure CallMultiplex, as shown in
Figures 11.2 and 11.4. By searching for a prior installation, CallMultiplex
ensures that SNAP is not installed more than once. During deinstallation,
CallMultiplex locates data required to deinstall the resident TSR.
The process repeats until the call to Interrupt 2Fh returns a matching identifier
string at ES:DI, or until AL returns as zero. A matching string verifies that SNAP is
installed, since its multiplex handler has serviced the call. A return value of zero
indicates that SNAP is not installed and that no multiplex handler claims the trial
identity number in AH. In this case, SNAP assigns the number to its own handler.
Deinstalling a TSR
During deinstallation, CallMultiplex locates SNAP’s multiplex handler as
described previously. The handler Multiplex receives the verification request and
returns in ES the code segment of the resident program.
Deinstall reads the addresses of the following interrupt handlers from the data
structure in the resident code segment:
Handler Name Description
Clock Timer handler
Keybrd Keyboard handler (non-PS/2)
KeybrdMonitor Keyboard monitor handler (PS/2)
Video Video monitor handler
DiskIO Disk monitor handler
SkipMiscServ Miscellaneous Systems Services handler (non-PS/2)
MiscServ Miscellaneous Systems Services handler (PS/2)
Idle MS-DOS Idle handler
Multiplex Multiplex handler
Deinstall calls MS-DOS Function 35h (Get Interrupt Vector) to retrieve the
current vectors for each of the listed interrupts. By comparing each handler address
with the corresponding vector, Deinstall ensures that SNAP can be safely
deinstalled. Failure in any of the comparisons indicates that another TSR has been
installed after SNAP and has set up a handler for the same interrupt. In this case,
Deinstall returns an error code, stopping the program with the following
message:
Can’t deinstall TSR
If all addresses match, Deinstall calls Interrupt 2Fh with SNAP’s identity
number in AH and AL set to 1. The handler Multiplex responds by returning in
ES the address of the resident code’s PSP. Deinstall then calls MS-DOS
Function 25h (Set Interrupt Vector) to restore the vectors for the original service
routines. This is called “unhooking” or “unchaining” the interrupt handlers.
After unhooking all of SNAP’s interrupt handlers, Deinstall returns with AX
pointing to the resident code’s PSP. The procedure FreeTsr then calls MS-DOS
Function 49h (Release Memory) to return SNAP’s memory to the operating system.
The program ends with the message
TSR deinstalled
C H A P T E R 1 2
Mixed-Language Programming
from another module written in a high-level language. This represents the essence
of mixed-language programming. Assembly language is often used for creating fast
secondary routines in a large program written in a high-level language.
The third section describes specific conventions for linking assembly-language
procedures with modules in C, C++, Basic, and FORTRAN. These language-
specific sections also provide details on how the language manages various data
structures so that your MASM programs are compatible with the data from the
high-level language.
Naming Conventions
“Naming convention” refers to the way a compiler or assembler stores the names
of identifiers. The first two rows of Table 12.1 show how each language type
affects symbol names. SYSCALL leaves symbol names as they appear in the
source code, but C and STDCALL add an underscore prefix. PASCAL, BASIC,
and FORTRAN change symbols to all uppercase.
The following list describes how these naming conventions affect a variable called
Big Time in your source code:
Argument Passing
With the C calling convention, the caller pushes arguments from right to left as they
appear in the caller’s argument list. The called procedure returns without removing
the arguments from the stack. It is the caller’s responsibility to clean the stack after
the call, either by popping the arguments or by adding an appropriate value to the
stack pointer SP.
Register Preservation
The called routine must return with the original values in BP, SI, DI, DS, and SS. It
must also preserve the direction flag.
The first line passes four arguments (including the string in quotes) and the second
line passes only two arguments. Notice that printf has no reliable way of
determining how many arguments the caller has pushed. Therefore, the function
returns without adjusting the stack. The C calling convention requires the caller to
take responsibility for removing the arguments from the stack, since only the caller
knows how many arguments it passed.
Use INVOKE to call a C-callable function from your assembly-language program,
since INVOKE automatically generates the necessary stack-cleaning code after the
call. You must also prototype the function with the VARARG keyword if
appropriate, as explained in “Procedures,” Chapter 7. Similarly, when you write a
C-callable procedure that accepts a varying number of arguments, include
VARARG in the procedure’s PROC statement.
Argument Passing
Arguments are placed on the stack in the same order in which they appear in the
source code. The first argument is highest in memory (because it is also the first
argument to be placed on the stack), and the stack grows downward.
Register Preservation
A routine that uses the Pascal calling convention must preserve SI, DI, BP, DS, and
SS. For 32-bit code, the EBX, ES, FS, and GS registers must be preserved as well
as EBP, ESI, and EDI. The direction flag is also cleared upon entry and must be
preserved.
Argument Passing
Argument passing order for both STDCALL and SYSCALL is the same as the C
calling convention. The caller pushes the arguments from right to left and must
remove the parameters from the stack after the call. However, STDCALL requires
the called procedure to clean the stack if the procedure does not accept a variable
number of arguments.
Register Preservation
Both conventions require the called procedure to preserve the registers BP, SI, DI,
DS, and SS. Under STDCALL, the direction flag is clear on entry and must be
returned clear.
defined with VARARG, the called procedure does not accept a variable argument
list and must clean the stack before it returns.
The complete syntax and parameter descriptions for these procedure directives are
explained in “Procedures” in Chapter 7. This section provides a template that you
can use for writing a MASM routine to be called from a high-level language.
The template looks like this:
Label PROC [[distance langtype visibility <prologueargs> USES reglist
parmlist]]
LOCAL varlist
.
.
.
RET
Label ENDP
Argument Passing
Each language has its own convention for how an argument is actually passed. If
the argument-passing conventions of your routines do not agree, then a called
routine receives bad data. Microsoft languages support three different methods for
passing an argument:
◆ Near reference. Passes a variable’s near (offset) address, expressed as an offset
from the default data segment. This method gives the called routine direct access
to the variable itself. Any change the routine makes to the parameter is reflected
in the calling routine.
◆ Far reference. Passes a variable’s far (segmented) address. Though slower than
passing a near reference, this method is necessary for passing data that lies
outside the default data segment. (This is not an issue in Basic unless you have
specifically requested far memory.)
◆ Value. Passes only a copy of the variable, not its address. With this method, the
called routine gets a copy of the argument on the stack, but has no access to the
original variable. The copy is discarded when the routine returns, and the
variable retains its original value.
When you pass arguments between routines written in different languages, you
must ensure that the caller and the called routine use the same conventions for
passing and receiving arguments. In most cases, you should check the argument-
passing defaults used by each language and make any necessary adjustments. Most
languages have features that allow you to change argument-passing methods.
Register Preservation
A procedure called from any high-level language should preserve the direction flag
and the values of BP, SI, DI, SS, and DS. Routines called from MASM must not
alter SI, DI, SS, DS, or BP.
Pushing Addresses
Microsoft high-level languages push segment addresses before offsets. This lets the
called routine use the LES and LDS instructions to read far addresses from the
stack. Furthermore, each word of an argument is placed on the stack in order of
significance. Thus, the high word of a long integer is pushed first, followed by the
low word.
Array Storage
Most high-level-language compilers store arrays in row-major order. This means
that all elements of a row are stored consecutively. The first five elements of an
array with four rows and three columns are stored in row-major order as
A[1, 1], A[1, 2], A[1, 3], A[2, 1], A[2, 2]
In column-major order, the column elements are stored consecutively. For example,
this same array would be stored in column-major order as
A[1, 1], A[2, 1], A[3, 1], A[4, 1], A[1, 2], A[2, 2]
Naming Restrictions
C is case-sensitive and does not convert names to uppercase. Since C normally links
with the /NOI command-line option, you should assemble MASM modules with the
/Cx or /Cp option to prevent the assembler from converting names to uppercase.
Argument-Passing Defaults
C always passes arrays by reference and all other variables (including structures)
by value. C programs in tiny, small, and medium model pass near addresses for
arrays, unless another distance is specified. Compact-, large-, and huge-model
programs pass far addresses by default. To pass by reference a variable type other
than array, use the C-language address-of operator (&).
If you need to pass an array by value, declare the array as a structure member and
pass a copy of the entire structure. However, this practice is rarely necessary and
usually impractical except for very small arrays, since it can make substantial
demands on stack space. If your program must maintain an array through a
procedure call, create a temporary copy of the array in heap and provide the copy to
the procedure by reference.
Array Storage
Array declarations give the number of elements. A1[a][b] declares a two-
dimensional array in C with a rows and b columns. By default, the array’s lower
bound is zero. Arrays are stored by the compiler in row-major order. By default,
passing arrays from C passes a pointer to the first element of the array.
String Format
C stores strings as arrays of bytes and uses a null character as the end-of-string
delimiter. For example, consider the string declared as follows:
char msg[] = "string of text"
S t r i n g o f t e x t \0
External Data
In C, the extern keyword tells the compiler that the data or function is external.
You can define a static data object in a C module by defining a data object outside
all functions and subroutines. Do not use the static keyword in C with a data object
that you want to be public.
Structure Alignment
By default, C uses word alignment (unpacked storage) for all data objects longer
than 1 byte. This storage method specifies that occasional bytes may be added as
padding, so that word and doubleword objects start on an even boundary. In
addition, all nested structures and records start on a word boundary. MASM aligns
on byte boundaries by default.
When converting .H files with H2INC, you can use the /Zp command-line option to
specify structure alignment. If you do not specify the /Zp option, H2INC uses word-
alignment. Without H2INC, set the alignment to 2 when declaring the MASM
structure, compile the C module with /Zp1, or assemble the MASM module with
/Zp2.
Returning Values
The assembler returns simple data types in registers. Table 12.2 shows the register
conventions for returning simple data types to a C program.
Table 12.2 Register Conventions for Simple Return Values
Data Type Registers
char AL
short, near, int (16-bit) AX
short, near, int (32-bit) EAX
long, far (16-bit) High-order portion (or segment address) in DX;
low-order portion (or offset address) in AX
long, far (32-bit) High-order portion (or segment address) in EDX;
low-order portion (or offset address) in EAX
Procedures using the C calling convention and returning type float or type double
store their return values into static variables. In multi-threaded programs, this could
mean that the return value may be overwritten. You can avoid this by using the
Pascal calling convention for multi-threaded programs so float or double values are
passed on the stack.
Structures less than 4 bytes long are returned in DX:AX. To return a longer
structure from a procedure that uses the C calling convention, you must copy the
structure to a global variable and then return a pointer to that variable in the AX
register (DX:AX, if you compiled in compact, large, or huge model or if the
variable is declared as a far pointer).
long checktypes (
char *name,
unsigned char a,
int b,
float d,
unsigned int *num );
Example
As shown in the following short example, the main module (written in C) calls an
assembly routine, Power2.
#include <stdio.h>
void main()
{
printf( "3 times 2 to the power of 5 is %d\n", Power2( 3, 5 ) );
}
Figure 12.2 shows how functions that observe the C calling convention use the
stack frame.
High addresses
Parameter n (rightmost)
.
.
.
Stack grows
downward with Parameter 1 (leftmost)
each push or call.
Return address (IP)
Saved frame pointer (BP) Frame pointer (BP)
points here.
Local data space
Saved SI
Saved DI Stack pointer (SP)
Low addresses
points to last item
placed on stack.
High addresses
Parameter n (rightmost)
.
.
.
Stack grows
downward with Parameter 1 (leftmost)
each push or call.
Return address (CS)
Return address (IP)
Saved frame pointer (BP) Frame pointer (BP)
points here.
Local data space
Saved SI
Saved DI Stack pointer (SP)
Low addresses
points to last item
placed on stack.
The MASM module that contains the Power2 routine looks like this:
.MODEL small, c
The MASM procedure declaration for the Power2 routine specifies the C
langtype and the parameters expected by the procedure. The langtype specifies the
calling and naming conventions for the interface between MASM and C. The
routine is public by default. When the C module calls Power2, it passes two
arguments, 3 and 5 by value.
The following example serves as a template for these steps. The program calls the C
run-time function printf to display two variables.
.MODEL small, c ; Step 1: declare C conventions
EXTERN _acrtused:abs ; Step 2: bring in C startup
.
.
.
printf PROTO NEAR, ; Step 3: prototype
pstring:NEAR PTR BYTE, ; external C
num1:WORD, num2:VARARG ; routines
.DATA
format BYTE '%i %i', 13, 0
.CODE
Note the syntax remains the same whether WriteLine and GoExit are exported
C++ functions or imported assembly-language routines. The linkage specification
applies only to called routines, not to external variables. Use the extern keyword
(without the “C”) as you normally would when identifying objects external to the
C++ module.
Naming Restrictions
FORTRAN allows 31 characters for identifier names. A digit or an underscore
cannot be the first character in an identifier name.
Argument-Passing Defaults
By default, FORTRAN passes arguments by reference as far addresses if the
FORTRAN module is compiled in large or huge memory model. It passes them as
near addresses if the FORTRAN module is compiled in medium model. Versions of
FORTRAN prior to Version 4.0 always require large model.
The FORTRAN compiler passes an argument by value when declared with the
VALUE attribute. This declaration can occur either in a FORTRAN
INTERFACE block (which determines how to pass an argument) or in a function
or subroutine declaration (which determines how to receive an argument).
In FORTRAN you can apply the NEAR (or FAR) attribute to reference
parameters. These keywords override the default. They have no effect when they
specify the same method as the default.
Array Storage
When you declare FORTRAN arrays, you can specify any integer for the lower
bound (the default is 1). The FORTRAN compiler stores all arrays in column-major
order — that is, the leftmost subscript increments most rapidly. For example, the
first seven elements of an array defined as A[3,4] are stored as
A[1,1], A[2,1], A[3,1], A[1,2], A[2,2], A[3,2], A[1,3]
String Format
FORTRAN stores strings as a series of bytes at a fixed location in memory, with no
delimiter at the end of the string. When passing a variable-length FORTRAN string
to another language, you need to devise a method by which the target routine can
find the end of the string.
Consider the string declared as
CHARACTER*14 MSG
MSG = 'String of text'
S t r i n g o f t e x t
Strings are passed by reference. Although FORTRAN has a method for passing
length, the variable-length FORTRAN strings cannot be used in a mixed-
language interface because other languages cannot access the temporary variable
that FORTRAN uses to communicate string length. However, fixed-length strings
can be passed if the FORTRAN INTERFACE statement declares the length of the
string in advance.
External Data
FORTRAN routines can directly access external data. In FORTRAN you can
declare data to be external by adding the EXTERN attribute to the data
declaration. You can also access a FORTRAN variable from MASM if it is
declared in a COMMON block.
A FORTRAN program can call an external assembly procedure with the use of the
INTERFACE statement. However, the INTERFACE statement is not strictly
necessary unless you intend to change one of the FORTRAN defaults.
Structure Alignment
By default, FORTRAN uses word alignment (unpacked storage) for all data objects
larger than 1 byte. This storage method specifies that occasional bytes may be
added as padding, so that word and doubleword objects start on an even boundary.
In addition, all nested structures and records start on a word boundary. The MASM
default is byte-alignment, so you should specify an alignment of 2 for MASM
structures or use the /Zp1 option when compiling in FORTRAN.
Returning Values
You must use a special convention to return floating-point values, records, user-
defined types, arrays, and values larger than 4 bytes to a FORTRAN module from
an assembly procedure. The FORTRAN module creates space in the stack segment
to hold the actual return value. When the call to the assembly procedure is made, an
extra parameter is passed. This parameter is the last one pushed. The segment
address of the return value is contained in SS.
In the assembly procedure, put the data for the return value at the location pointed
to by the return value offset. Then copy the return-value offset (located at BP + 6)
to AX, and copy SS to DX. This is necessary because the calling module expects
DX:AX to point to the return value.
Example
In the following example, the FORTRAN module calls an assembly procedure that
calculates A*2^B, where A and B are the first and second parameters, respectively.
This is done by shifting the bits in A to the left B times.
INTERFACE TO INTEGER*2 FUNCTION POWER2(A, B)
INTEGER*2 A, B
END
PROGRAM MAIN
INTEGER*2 POWER2
INTEGER*2 A, B
A = 3
B = 5
WRITE (*, *) '3 TIMES 2 TO THE B OR 5 IS ',POWER2(A, B)
END
To understand the assembly procedure, consider how the parameters are placed on
the stack, as illustrated in Figure 12.4.
High addresses
Low addresses
Figure 12.4 assumes that the FORTRAN module is compiled in large model. If you
compile the FORTRAN module in medium model, then each argument is passed as
a 2-byte, not 4-byte, address. The return address is 4 bytes long because procedures
called from FORTRAN must always be FAR.
.CODE
Naming Conventions
Basic recognizes up to 40 characters of a name. In the object code, Basic also drops
any of its reserved characters: %, &, !, #, @, &.
Argument-Passing Defaults
Basic can pass data in several ways and can receive it by value or by near
reference. By default, Basic arguments are passed by near reference as 2-byte
addresses. To pass a near address, pass only the offset; if you need to pass a far
address, pass the segment and offset separately as integer arguments. Pass the
segment address first, unless you have specified C compatibility with the CDECL
keyword.
Basic passes each argument in a call by far reference when CALLS is used to
invoke a routine. You can also use SEG to modify a parameter in a preceding
DECLARE statement so that Basic passes that argument by far reference. To pass
any other variable type by value, apply the BYVAL keyword to the argument in the
DECLARE statement. You cannot pass arrays and user-defined types by value.
DECLARE SUB Test(BYVAL a%, b%, SEG c%)
This CALL statement passes the first argument (a%) by value, the second argument
(b%) by near reference, and the third argument (c%) by far reference. The statement
CALLS Test2(x%, y%, z%)
Array Storage
The DIM statement sets the number of dimensions for a Basic array and also sets
the array’s maximum subscript value. In the array declaration DIM x(a,b), the
upper bounds (the maximum number of values possible) of the array are a and b.
The default lower bound is 0. The default upper bound for an array subscript is 10.
The default for column storage in Basic is column-major order, as in FORTRAN.
For an array defined as DIM Arr%(3,3), reference the last element as
Arr%(3,3). The first five elements of Arr (3,3) are
When you pass an array from Basic to a language that stores arrays in row-major
order, use the command-line option /R when compiling the Basic module.
Most Microsoft languages permit you to reference arrays directly. Basic uses an
array descriptor, however, which is similar in some respects to a Basic string
descriptor. The array descriptor is necessary because Basic handles memory
allocation for arrays dynamically, and thus may shift the location of the array in
memory.
A reference to an array in Basic is really a near reference to an array descriptor.
Array descriptors are always in DGROUP, even though the data may be in far
memory. Array descriptors contain information about type, dimensions, and
memory locations of data. You can safely pass arrays to MASM routines only if
you follow three rules:
◆ Pass the array’s address by applying the VARPTR function to the first element
of the Basic array and passing the result by value. To pass the far address of the
array, apply both the VARPTR and VARSEG functions and pass each result
by value. The receiving language gets the address of the first element and
considers it to be the address of the entire array. It can then access the array
with its normal array-indexing syntax.
◆ The MASM routine that receives the array should not call back to one of the
calling program’s routines before it has finished processing the array. Changing
data within the caller’s heap — even data unrelated to the array — may change
the array’s location in the heap. This would invalidate any further work the
called routine performs, since the routine would be operating on the array’s old
location.
◆ Basic can pass any member of an array by value. When passing individual array
elements, these restrictions do not apply.
You can apply LBOUND and UBOUND to a Basic array to determine lower and
upper bounds, and then pass the results to another routine. This way, the size of the
array does not need to be determined in advance.
String Format
Basic maintains a 4-byte string descriptor for each string, as shown in the
following. The first field of the string descriptor contains a 2-byte integer indicating
the length of the actual string text. The second field contains the offset address of
this text within the caller’s data segment.
Version 7.0 or later of the Microsoft Basic Compiler provides new functions that
access string descriptors. These functions simplify the process of sharing Basic
string data with routines written in other languages.
Earlier versions of Basic offer the LEN (Length) and SADD (String Address)
functions, which together obtain the information stored in a string descriptor. LEN
returns the length of a string in bytes. SADD returns the offset address of a string in
the data segment. The caller must provide both pieces of information so the called
procedure can locate and read the entire string. The address returned by SADD is
declared as type INTEGER but is actually equivalent to a C near pointer.
If you need to pass the far address of a string, use the SSEGADD (String Segment
Address) function of Microsoft Basic version 7.0 or later. You can also determine
the segment address of the first element with VARSEG.
External Data
Declaring global data in Basic follows the same two-step process as in other
languages:
1. Declare shareable data in Basic with the COMMON statement.
2. Identify the shared variables in your assembly-language procedures with the
EXTERN keyword. Place the EXTERN statement outside of a code or data
segment when declaring far data.
Structure Alignment
Basic packs user-defined types. For MASM structures to be compatible, select
byte-alignment.
Returning Values
Basic follows the usual convention of returning values in AX or DX:AX. If the
value is not floating point, an array, or a structured type, or if it is less than 4 bytes
long, then the 2-byte integers should be returned from the MASM procedure in AX
and 4-byte integers should be returned in DX:AX. For all other types, return the
near offset in AX.
Example
This example calls the Power2 procedure in the MASM 6.1 module.
DEFINT A-Z
END
The first argument, A, is higher in memory than B because Basic pushes arguments
in the same order in which they appear.
Figure 12.6 shows how the arguments are placed on the stack.
High addresses
Note that each parameter must be loaded in a two-step process because the address
of each is passed rather than the value. The return address is 4 bytes long because
procedures called from Basic must be FAR.
C H A P T E R 1 3
This chapter is an introduction to 32-bit programming for the 80386. The guidelines
in this chapter also apply to the 80486 processor, which is basically a faster 80386
with the equivalent of a 80387 floating-point processor. Since you are already
familiar with 16-bit real-mode programming, this chapter covers the differences
between 16-bit programming and 32-bit protected-mode programming.
The 80386 processor (and its successors such as the 80486) can run in real mode,
virtual-86 mode, and in protected mode. In real and virtual-86 modes, the 80386
can run 8086/8088 programs. In protected mode, it can run 80286 programs. The
386 also extends the features of protected mode to include 32-bit operations and
segments larger than 64K.
All segment registers are 16 bits wide. The offset in a 32-bit protected-mode
program is itself 32 bits wide, which means that a single segment can address up to
4 gigabytes of memory. Because of this large range, there is little need to use
segment registers to extend the range of addresses in 32-bit programs. If all six
segment registers are initially set to the same value, then the rest of the program can
ignore them and treat the processor as if it used a 32-bit linear address space. This
is called 0:32, or flat, addressing. (The full segmented 32-bit addressing mode, in
which the segment registers can contain different values, is called 16:32
addressing.) Flat addressing is used by the Windows NT operating system.
EAX ESI
AH AL CS
AX SI SS
EBX EDI
DS
BH BL
BX DI ES
ECX EBP FS
CH CL
GS
CX BP
EDX ESP
DH DL
DX SP
◆ Supply the .386 directive, which enables the 32-bit programming features of the
386 and its successors. The .386 directive must precede the .MODEL directive.
which tells the assembler to assume flat model (0:32) and to use the Windows
NT standard calling convention for subroutine calls.
◆ Precede your data declarations with the .DATA directive.
◆ Precede your instruction codes with the .CODE directive.
◆ At the end of the source file, place an END directive.
Sample Program
The following sample is a 32-bit assembly language subroutine, such as might be
called from a 32-bit C program written for the Windows NT operating system. The
program illustrates the use of a variety of directives to make assembly language
easier to read and maintain. Note that with 32-bit flat model programming, there is
no longer any need to refer to segment registers, since these are artifacts of
segmented addressing.
;* szSearch - An example of 32-bit assembly programming using MASM 6.1
;*
;* Purpose: Search a buffer (rgbSearch) of length cbSearch for the
;* first occurrence of szTok (null terminated string).
;*
;* Method: A variation of the Boyer-Moore method
;* 1. Determine length of szTok (n)
;* 2. Set array of flags (rgfInTok) to TRUE for each character
;* in szTok
;* 3. Set current position of search to rgbSearch (pbCur)
;* 4. Compare current position to szTok by searching backwards
;* from the nth position. When a comparison fails at
;* position (m), check to see if the current character
;* in rgbSearch is in szTok by using rgfInTok. If not,
;* set pbCur to pbCur+(m)+1 and restart compare. If
;* pbCur reached, increment pbCur and restart compare.
;* 5. Reset rgfInTok to all 0 for next instantiation of the
;* routine.
.386
.MODEL flat, stdcall
FALSE EQU 0
TRUE EQU NOT FALSE
.DATA
; Flags buffer - data initialized to FALSE. We will
; set the appropriate flags to TRUE during initialization
; of szSearch and reset them to FALSE before exit.
rgfInTok BYTE 256 DUP (FALSE);
.CODE
end
A P P E N D I X A
Differences Between
MASM 6.1 and 5.1
For the many users who come to version 6.1 of the Microsoft Macro Assembler
directly from the popular MASM 5.1, this appendix describes the differences
between the two versions. Version 6.1 contains significant changes, including:
◆ An integrated development environment called Programmer’s WorkBench
(PWB) from which you can write, edit, debug, and execute code.
◆ Expanded functionality for structures, unions, and type definitions.
◆ New directives for generating loops and decision statements, and for declaring
and calling procedures.
◆ Simplified methods for applying public attributes to variables and routines in
multiple-module programs.
◆ Enhancements for writing and using macros.
◆ Flat-model support for Windows NT and new instructions for the 80486
processor.
The OPTION M510 directive (or the /Zm command-line switch) assures nearly
complete compatibility between MASM 6.1 and MASM 5.1. However, to take full
advantage of the enhancements in MASM 6.1, you will need to rewrite some code
written for MASM 5.1.
The first section of this appendix describes the new or enhanced features in MASM
6.1. The second section, “Compatibility Between MASM 5.1 and 6.1,” explains
how to:
◆ Minimize the number of required changes with the OPTION directive.
◆ Rewrite your existing assembly code, if necessary, to take advantage of the
assembler’s enhancements.
The Assembler
The macro assembler, named ML.EXE, can assemble and link in one step. Its new
32-bit operation gives ML.EXE the ability to handle much larger source files than
MASM 5.1. The command-line options are new. For example, the /Fl and /Sc
options generate instruction timings in the listing file. Command-line options are
case-sensitive and must be separated by spaces.
For backward compatibility with MASM 5.1 makefiles, MASM 6.1 includes the
MASM.EXE utility. MASM.EXE translates MASM 5.1 command-line options to
the new MASM 6.1 command-line options and calls ML.EXE. See the Reference
book for details.
H2INC
H2INC converts C include files to MASM include files. It translates data structures
and declarations but does not translate executable code. For more information, see
Chapter 20 of Environment and Tools.
NMAKE
NMAKE replaces the MAKE utility. NMAKE provides new functions for
evaluating target files and more flexibility with macros and command-line options.
For more information, see Environment and Tools.
Integrated Environment
PWB is an integrated development environment for writing, developing, and
debugging programs. For information on PWB and the CodeView debugging
application, see Environment and Tools.
Online Help
MASM 6.1 incorporates the Microsoft Advisor Help system. Help provides a vast
database of online help about all aspects of MASM, including the syntax and
HELPMAKE
You can use the HELPMAKE utility to create additional help files from ASCII text
files, allowing you to customize the online help system. For more information, see
Environment and Tools.
Other Programs
MASM 6.1 contains the most recent versions of LINK, LIB, BIND, CodeView, and
the mouse driver. The CREF program is not included in MASM 6.1. The Source
Browser provides the information that CREF provided under MASM 5.1. For more
information on the Source Browser, see Chapter 5 of Environment and Tools or
Help.
Segment Management
This section lists the changes and additions to memory-model support and directives
that relate to memory model.
Predefined Symbols
The following predefined symbols (also called predefined equates) provide
information about simplified segments:
Predefined Symbol Value
@stack DGROUP for near stacks, STACK for far stacks
@Interface Information about language parameters
@Model Information about the current memory model
@Line The source line in the current file
@Date The current date
@FileCur The current file
@Time The current time
@Environ The current environment variables
Relocatable Offsets
For compatibility with applications for Windows, the LROFFSET operator can
calculate a relocatable offset, which is resolved by the loader at run time. See Help
for details.
Flat Model
MASM 6.1 supports the flat-memory model of Windows NT, which allows
segments as large as 4 gigabytes. All other memory models limit segment size to
64K for MS-DOS and Windows. For more information about memory models, see
“Defining Basic Attributes with .MODEL” in Chapter 2.
Data Types
MASM 6.1 supports an improved data typing. This section summarizes the
improved forms of data declarations in MASM 6.1.
Signed Types
You can use the SBYTE, SWORD, and SDWORD directives to declare signed
data. For more information about these directives, see “Allocating Memory for
Integer Variables” in Chapter 4.
Floating-Point Types
MASM 6.1 provides the REAL4, REAL8, and REAL10 directives for declaring
floating-point variables. For information on these type directives, see “Declaring
Floating-Point Variables and Constants” in Chapter 6 .
Qualified Types
Type definitions can now include distance and language type attributes. Procedures,
procedure prototypes, and external declarations let you specify the type as a
qualified type. A complete description of qualified types is provided in the section
“Data Types” in Chapter 1.
Structures
Changes to structures since MASM 5.1 include:
◆ Structures can be nested.
◆ The names of structure fields need not be unique. As a result, you must qualify
references to field names.
◆ Initialization of structure variables can continue over multiple lines provided the
last character in the line before the comment field is a comma.
◆ Curly braces and angle brackets are equivalent.
You can use OPTION OLDSTRUCTS or OPTION M510 to enable MASM 5.1
behavior for structures. See “Compatibility between MASM 5.1 and 6.1,” later in
this appendix. For more information on structures and unions, see “Structures and
Unions” in Chapter 5.
Unions
MASM 6.1 allows the definition of unions with the UNION directive. Unions
differ from structures in that all fields within a union occupy the same data space.
For more information, see “Structures and Unions” in Chapter 5.
Names of Identifiers
MASM 6.1 accepts identifier names up to 247 characters long. All characters are
significant, whereas under MASM 5.1, names are significant to 31 characters only.
For more information on identifiers, see “Identifiers” in Chapter 1.
Multiple-Line Initializers
In MASM 6.1, a comma at the end of a line (except in the comment field) implies
that the line continues. For example, the following code is legal in MASM 6.1:
longstring BYTE "This string ",
"continues over two lines."
bitmasks BYTE 80h, 40h, 20h, 10h,
08h, 04h, 02h, 01h
Renamed Directives
Although MASM 6.1 still supports the old names in MASM 5.1, the following
directives have been renamed for language consistency:
MASM 6.1 MASM 5.1
.DOSSEG DOSSEG
.LISTIF .LFCOND
.LISTMACRO .XALL
.LISTMACROALL .LALL
.NOCREF .XCREF
.NOLIST .XLIST
.NOLISTIF .SFCOND
.NOLISTMACRO .SALL
ECHO %OUT
EXTERN EXTRN
FOR IRP
FORC IRPC
REPEAT REPT
STRUCT STRUC
SUBTITLE SUBTTL
with the W (word) and the D (doubleword) suffixes. For details, see the Reference
or Help.
Macro Enhancements
There are significant enhancements to macro functions in MASM 6.1. Directives
provide for a variable number of arguments, loop constructions, definitions of text
equates, and macro functions.
Variable Arguments
MASM 5.1 ignores extra arguments passed to macros. In MASM 6.1, you can pass
a variable number of arguments to a macro by appending the VARARG keyword
to the last macro parameter in the macro definition. The macro can then reference
additional arguments relative to the last declared parameter. This procedure is
explained in “Returning Values with Macro Functions” in Chapter 9.
Text Macros
The EQU directive retains its old functionality, but MASM 6.1 also incorporates a
TEXTEQU directive for defining text macros. TEXTEQU allows greater
flexibility than EQU. For example, TEXTEQU can assign to a label the value
calculated by a macro function. For more information, see “Text Macros” in
Chapter 9.
Macro Functions
At assembly time, macro functions can determine and return a text value using
EXITM. Predefined macro string functions concatenate strings, return the size of a
string, and return the position of a substring within a string. For information on
writing your own macro functions, see “Returning Values with Macro Functions” in
Chapter 9.
For more information on predefined macros, see “String Directives and Predefined
Functions” in Chapter 9.
Table A.1 summarizes the correct use of the instruction prefixes. It lists each string
instruction with the type of repeat prefix it uses, and indicates whether the
instruction works on a source, a destination, or both.
Table A.1 Requirements for String Instructions
Instruction Repeat Prefix Source/Destination Register Pair
MOVS REP Both DS:SI, ES:DI
SCAS REPE/REPNE Destination ES:DI
CMPS REPE/REPNE Both DS:SI, ES:DI
LODS -- Source DS:SI
STOS REP Destination ES:DI
INS REP Destination ES:DI
OUTS REP Source DS:SI
MASM 5.1 considers the remainder of the line to be part of the macro argument
containing the opening quote, as if there were a closing quotation mark at the end of
the line.
By default, MASM 6.1 now generates error A2046:
missing single or double quotation mark in string
so all single and double quotation marks in macro arguments must be matched.
To correct such errors in MASM 6.1, either end the string with a closing quotation
mark as shown in the following example, or use the macro escape character (!) to
treat the quotation mark literally.
; MASM 5.1 code
MyMacro "all this in one argument
If you want to make a label PUBLIC, it must not be local. You can use the double
colon operator to define a non-scoped label, as shown in this example:
PUBLIC publicLabel
publicLabel:: ; Non-scoped label MASM 6.1
MASM 6.1 allows a range of –2n–1 to 2n–1 for default values. Illegal initializers
generate error A2071:
initializer too large for specified size
The operands for the MOV instruction do not match in size, yet the instruction
assembles correctly. It places the contents of AL into var1 and AH into var2,
moving a word of data in one step. If the code defined var1 as a word value, the
instruction
mov var1, al
would also assemble correctly, copying AL into the low byte of var1 while leaving
the high byte unaffected. Except at warning level 0, MASM 5.1 issues a warning to
inform you of the size mismatch, but both scenarios are legal.
MASM 6.1 does not accept instructions with operands that do not agree in size.
You must specifically “coerce” the size of the memory operand, like this:
mov BYTE PTR var1, al
Rewrite your code if necessary. The new SIZEOF operator returns information
about records in MASM 6.1. For more information, see “Defining Record
Variables” in Chapter 5.
does not allow explicit ASSUME statements for CS that contradict the
automatically set ASSUME statement.
MASM 5.1 allows CS to be assumed to the current segment, even if that segment is
a member of a group. With MASM 6.1, this results in warning A4004:
cannot ASSUME CS
To avoid this warning with MASM 6.1, delete the ASSUME statement for CS.
Two-Pass Directives
To assure compatibility, MASM 6.1 supports 5.1 directives referring to two passes.
These include .ERR1, .ERR2, IF1, IF2, ELSEIF1, and ELSEIF2. For second-
pass constructs, you must specify OPTION SETIF2, as discussed in “OPTION
SETIF2,” page 377. Without OPTION SETIF2, the IF2 and .ERR2 directives
cause error A2061:
[[ELSE]]IF2/.ERR2 not allowed : single-pass assembler
MASM 6.1 handles first-pass constructs differently. It treats the .ERR1 directive
as .ERR, and the IF1 directive as IF.
The following examples show you how you can rewrite typical pass-sensitive code
for MASM 6.1:
◆ Declare var external only if not defined in current module:
; MASM 5.1:
IF2
IFNDEF var
EXTRN var:far
ENDIF
ENDIF
; MASM 6.1:
EXTERNDEF var:far
; MASM 6.1:
INCLUDE FILE1.INC
IF2
.ERRNZ A NE B
ENDIF
; MASM 6.1:
ECHO This is my message
; MASM 6.1:
.ERRNDEF var
To resolve the warning, place the symbol definition before the conditional test.
However, the DUP operator allows such an expression as its count value. The
assembler evaluates the DUP count on every pass, so even expressions involving
forward references assemble correctly.
You can also use expressions containing span distances with the .ERR directives,
since the assembler evaluates these directives after calculating all offsets:
.ERRE OFFSET var1 - OFFSET var2 - 10, <span incorrect>
To update MASM 5.1 code, use the coprocessor instructions instead of ESC.
Use IEEE format or, if MSB format is necessary, initialize variables with
hexadecimal values. See “Storing Numbers in Floating-Point Format” in Chapter 6.
Note If your code includes both .MODEL and OPTION M510, the OPTION
M510 statement must appear first. Wherever this appendix suggests using
OPTION M510 in your code, you can set the /Zm command-line option instead.
OPTION M510
This section discusses the M510 argument to the OPTION directive, which selects
the MASM 5.1 compatibility mode. In this mode, MASM 6.1 implements MASM
5.1 behavior relating to macros, offsets, scope of code labels, structures, identifier
names, identifier case, and other behaviors.
The OPTION M510 directive automatically sets the following:
OPTION OLDSTRUCTS ; MASM 5.1 structures
OPTION OLDMACROS ; MASM 5.1 macros
OPTION DOTNAME ; Identifiers may begin with a dot (.)
OPTION SETIF2:TRUE ; Two-pass code activates on every pass
If you do not have a .386, 386P .486, or 486P directive in your module, then
OPTION M510 adds:
OPTION EXPR16 ; 16-bit expression precision
; See "OPTION EXPR16," following
If you do not have a .MODEL directive in your module, OPTION M510 adds:
OPTION OFFSET:SEGMENT ; OFFSET operator defaults to
; segment-relative
; See "OPTION OFFSET," following
If you do not have a .MODEL directive with a language specifier in your module,
OPTION M510 also adds:
OPTION NOSCOPED ; Code labels are not local inside
; procedures
; See "OPTION NOSCOPED," following
OPTION PROC:PRIVATE ; Labels defined with PROC are not
; public by default
; See "OPTION PROC," following
If you want to remove OPTION M510 from your code (or /Zm from the command
line), add the OPTION directive arguments to your module according to the
conditions stated earlier.
There may be compatibility issues affecting your code that are supported under
OPTION M510, but are not covered by the other OPTION directive arguments.
Once you have modified your source code so it no longer requires behavior
supported by OPTION M510, you can replace OPTION M510 with other
OPTION directive arguments. These compatibility issues are discussed in
following sections.
Once you have replaced OPTION M510 with other forms of the OPTION
directive and your code works correctly, try removing the OPTION directives, one
at a time. Make appropriate source modifications as necessary, until your code uses
only MASM 6.1 defaults.
MASM 5.1 behavior. MASM 5.1 does not check for illegal usage of the instruction
prefixes LOCK, REP, REPE, REPZ, REPNE, and REPNZ.
Illegal usage of these prefixes results in error A2068:
instruction prefix not allowed
is legal in OPTION M510 mode. Since 0100h cannot fit in a byte, the assembler
interprets the value as a word.
Without OPTION M510, the assembler never assigns a size automatically. You
must state it explicitly with the PTR operator, as shown in the following example:
; Without OPTION M510
mov [bx], WORD PTR 0100h
In MASM 6.1, the value returned by the SEG operator applied to a nonexternal
variable depends on compatibility mode:
◆ Without OPTION M510, SEG returns the address of the frame (the segment,
group, or the value assumed to the segment register) if one has been explicitly
set.
◆ With OPTION M510, SEG returns the group if one has been specified. In the
absence of a defined group, SEG returns the segment where the variable is
defined.
is parsed as
(var-2)[bx]
Without OPTION M510, you must rewrite the statement, since the assembler
parses it as
var-(2[bx])
parenthesized type. Note that the TYPE operator yields a type expression, but the
SIZE operator yields a constant.
; With OPTION M510
MyData DW 0
MyData WORD 0
In MASM 6.1, the initializer type does not influence CodeView’s type information.
.386
MASM 6.1 offers much greater flexibility in where EXTERN and EXTERNDEF
statements can appear, as described in “Positioning External Declarations” in
Chapter 8. However, in compatibility mode, MASM 6.1 emulates the behavior of
MASM 5.1.
OPTION OLDSTRUCTS
This section describes changes in MASM 6.1 that apply to structures. With
OPTION OLDSTRUCTS or OPTION M510:
◆ You can use plus operator (+) in structure field references.
◆ Labels and structure field names cannot have the same name with OPTION
OLDSTRUCTS.
If you remove OPTION OLDSTRUCTS from your code, the assembler generates
errors for all lines requiring change. Using the dot operator in any context other
than for a structure field results in error A2166:
structure field expected
The following example illustrates how to change MASM 5.1 code from the old
structure references to the new type in MASM 6.1:
; OPTION OLDSTRUCTS (simulates MASM 5.1)
structname STRUC
a BYTE ?
b WORD ?
structname ENDS
OPTION OLDMACROS
This section describes how MASM 5.1 and 6.1 differ in their handling of macros.
Without OPTION OLDMACROS or OPTION M510, MASM 6.1 changes the
behavior of macros in several ways. If you want the MASM 5.1 macro behavior,
add OPTION OLDMACROS or OPTION M510 to your MASM 5.1 code.
OPTION OLDMACROS causes the assembler to treat all four items as separate
arguments. With OPTION NOOLDMACROS, the assembler treats
var1 var2 var3
as one argument, since the items are not separated with commas. To convert your
macro code, replace spaces between macro arguments with a single comma.
OPTION DOTNAME
MASM 5.1 allows names of identifiers to begin with a period. The MASM 6.1
default is OPTION NODOTNAME. Adding OPTION DOTNAME to your code
enables the MASM 5.1 behavior.
If you don’t want to use this directive in your source code, rename the identifiers
whose names begin with a period.
OPTION EXPR16
MASM 5.1 treats expressions as 16-bit words if you do not specify .386 or .386P
directives. MASM 6.1 by default treats expressions as 32-bit words, regardless of
the CPU type. You can force MASM 6.1 to use the smaller expression size with the
OPTION EXPR16 statement.
Unless your MASM 5.1 code specifies .386 or .386P, OPTION M510 also sets
16-bit expression size. You can selectively disable this by following OPTION
M510 with the OPTION EXPR32 directive, which sets the size back to 32 bits.
You cannot have both OPTION EXPR32 and OPTION EXPR16 in your
program.
It may not be easy to determine the effect of changing from 16-bit internal
expression size to 32-bit size. In most cases, the 32-bit word size does not affect the
MASM 5.1 code. However, problems may arise because of differences in
intermediate values during evaluation of expressions. You can compare the files for
differences by generating listing files with the /Fl and /Sa command-line options
with and without OPTION EXPR16.
OPTION OFFSET
The information in this section is relevant only if your MASM 5.1 code does not
use the .MODEL directive. With no .MODEL, MASM 5.1 computes offsets from
the start of the segment, whereas MASM 6.1 computes offsets from the start of the
group. (With .MODEL, MASM 5.1 also computes offsets from the start of the
group.)
To force MASM 6.1 to emulate 5.1 behavior, specify either OFFSET:SEGMENT
or OPTION M510. Both directives cause the assembler to compute offsets relative
to the segment if you do not include .MODEL.
To selectively enable MASM 6.1 behavior, place the directive OPTION
OFFSET:GROUP after OPTION M510. In this case, you should ensure each
OFFSET statement has a segment override where appropriate. The following
example shows how OPTION OFFSET:SEGMENT affects code written for
MASM 5.1:
OPTION OFFSET:SEGMENT
MyGroup GROUP MySeg
In the preceding example, the first OFFSET statement computes the offset of
MyLabel relative to MySeg. Without OFFSET:SEGMENT, MASM 6.1 returns
the offset relative to MyGroup. To maintain the correct behavior with
OFFSET:GROUP, specify a segment override, as shown in the following. The
other two OFFSET statements already include overrides, and so do not require
modification.
OPTION OFFSET:GROUP
MyGroup GROUP MySeg
OPTION NOSCOPED
The information in this section applies only if the .MODEL directive in your
MASM 5.1 code does not specify a language type. Without a language type,
MASM 5.1 assumes code labels in procedures have no “scope” — that is, the labels
are not local to the procedure. When not in compatibility mode, MASM 6.1 always
gives scope to code labels, even without a language type.
To force MASM 5.1 behavior, specify either OPTION M510 or OPTION
NOSCOPED in your code. To selectively enable MASM 6.1 behavior, place the
directive OPTION SCOPED after OPTION M510.
To determine which labels require change, assemble the module without the
OPTION NOSCOPED directive. For each reference to a label that is not local,
the assembler generates error A2006:
undefined symbol : identifier
OPTION PROC
The information in this section applies only if the .MODEL directive in your
MASM 5.1 code does not specify a language type. Without a language type,
MASM 5.1 makes procedures private to the module. By default, MASM 6.1 makes
procedures public. You can explicitly change the default visibility to private with
either OPTION M510, OPTION PROC:PRIVATE, or OPTION
PROC:EXPORT.
To selectively enable MASM 6.1 behavior, place the directive OPTION
PROC:PUBLIC after OPTION M510. You can override the default by adding
the PUBLIC or PRIVATE keyword to selected procedures. The following
example shows how to change MASM 5.1 code to keep a procedure private:
; MASM 5.1 (OPTION PROC:PRIVATE)
MyProc PROC NEAR
This is necessary only to avoid naming conflicts between public names in multiple
modules or libraries. The symbol table in a listing file shows the visibility (public,
private, or export) of each procedure.
OPTION NOKEYWORD
MASM 6.1 has several new keywords that MASM 5.1 does not recognize as
reserved. To resolve any conflicts, you can:
◆ Rename any offending symbols in your code.
◆ Selectively disable keywords with the OPTION NOKEYWORD directive.
The second option lets you retain the offending symbol names in your code by
forcing MASM 6.1 to not recognize them as keywords. For example,
OPTION NOKEYWORD:<INVOKE STRUCT>
removes the keywords INVOKE and STRUCT from the assembler’s list of
reserved words. However, you cannot then use the keywords in their intended
function, since the assembler no longer recognizes them.
The following list shows MASM 6.1 reserved words new since MASM 5.1:
.BREAK .UNTILCXZ FRSTORD
.CONTINUE .WHILE FRSTORW
.DOSSEG ADDR FSAVED
.ELSE ALIAS FSAVEW
.ELSEIF BSWAP FSTENVD
.ENDIF CARRY? FSTENVW
.ENDW CMPXCHG GOTO
.EXIT ECHO HIGHWORD
.IF EXTERN INVD
.LISTALL EXTERNDEF INVLPG
.LISTIF FAR16 INVOKE
.LISTMACRO FAR32 IRETDF
.LISTMACROALL FLAT IRETF
.NO87 FLDENVD LENGTHOF
.NOCREF FLDENVW LOOPD
.NOLIST FNSAVED LOOPED
.NOLISTIF FNSAVEW LOOPEW
.NOLISTMACRO FNSTENVD LOOPNED
.REPEAT FNSTENVW LOOPNEW
.STARTUP FOR LOOPNZD
.UNTIL FORC LOOPNZW
OPTION SETIF2
By default, MASM 6.1 does not recognize pass-dependent constructs. Both the
OPTION M510 and OPTION SETIF2 statements force MASM 6.1 to handle
MASM 5.1 constructs that activate on the second assembly pass, such as .ERR2,
IF2, and ELSEIF2.
Invoke the option like this:
OPTION SETIF2: {TRUE | FALSE}
When set to TRUE, OPTION SETIF2 forces all second-pass constructs to
activate on every assembly pass. When set to FALSE, second-pass constructs do
not activate on any pass. OPTION M510 implies OPTION SETIF2:TRUE.
Coprocessor Instructions
For the 8087 coprocessor, MASM 5.1 adds an extra NOP before the no-wait
versions of coprocessor instructions. MASM 6.1 does not. In the rare case that the
missing NOP affects timing, insert NOP.
For the 80287 coprocessor or better, MASM 5.1 inserts FWAIT before certain
instructions. MASM 6.1 does not prefix any 80287, 80387, or 80486 coprocessor
instruction with FWAIT, except for wait forms of instructions that have a no-wait
form.
RET Instruction
MASM 5.1 generates a 3-byte encoding for RET, RETN, or RETF instructions
with an operand value of zero, unless the operand is an external absolute. In this
case, MASM 5.1 ignores the parameter and generates a 1-byte encoding.
MASM 6.1 does the opposite. It ignores a zero operand for the return instructions
and generates a 1-byte encoding, unless the operand is an external absolute. In this
case, MASM 6.1 generates a 3-byte encoding.
Thus, you can suppress epilogue code in a procedure but still specify the default
size for RET by coding the return as
ret 0
Arithmetic Instructions
Versions 5.1 and 6.1 differ in the way they encode the arithmetic instructions ADC,
ADD, AND, CMP, OR, SUB, SBB, and XOR, under the following conditions:
◆ The first operand is either AX or EAX.
◆ The second operand is a constant value between 0 and 127.
For the AX register, there is no size or speed difference between the two encodings.
For the EAX register, the encoding in MASM 6.1 is 2 bytes smaller. The OPTION
NOSIGNEXTEND directive forces the MASM 5.1 behavior for AND, OR,
and XOR.
A P P E N D I X B
BNF Grammar
BNF Conventions
The conventions use different font attributes for different items in the BNF. The
symbols and formats are as follows:
Attribute Description
nonterminal Italic type indicates nonterminals.
RESERVED Terminals in boldface type are literal reserved words and
symbols that must be entered as shown. Characters in this
context are always case insensitive.
[[ ]] Objects enclosed in double brackets ([[ ]]) are optional. The
brackets do not actually appear in the source code.
| A vertical bar indicates a choice between the items on each
side of the bar.
.8086 Underlined items indicate the default option if one is given.
default typeface Characters in the set described or listed can be used as
terminals in MASM statements.
typedefDir
NEAR | FAR
Nonterminal Definition
;; endOfLine
| comment
=Dir id = immExpr ;;
addOp +|-
aExpr term
| aExpr && term
Nonterminal Definition
altId id
arbitraryText charList
asmInstruction mnemonic [[ exprList ]]
assumeDir ASSUME assumeList ;;
| ASSUME NOTHING ;;
assumeList assumeRegister
| assumeList , assumeRegister
assumeReg register : assumeVal
assumeRegister assumeSegReg
| assumeReg
assumeSegReg segmentRegister : assumeSegVal
assumeSegVal frameExpr
| NOTHING | ERROR
assumeVal qualifiedType
| NOTHING | ERROR
bcdConst [[ sign ]] decNumber
binaryOp == | != | >= | <= | > | < | &
bitDef bitFieldId : bitFieldSize [[ = constExpr ]]
bitDefList bitDef
| bitDefList , [[ ;; ]] bitDef
bitFieldId id
bitFieldSize constExpr
blockStatements directiveList
| .CONTINUE [[ .IF cExpr ]]
| .BREAK [[ .IF cExpr ]]
bool TRUE | FALSE
byteRegister AL | AH | BL | BH | CL | CH | DL | DH
cExpr aExpr
| cExpr || aExpr
character Any character with ordinal in the range 0–255
except linefeed (10)
charList character
| charList character
className string
commDecl [[ nearfar ]] [[ langType ]] id : commType
[[ : constExpr ]]
commDir COMM commList ;;
comment ; text ;;
Nonterminal Definition
commentDir COMMENT delimiter
text
text delimiter text ;;
commList commDecl
| commList , commDecl
commType type
| constExpr
constant digits [[ radixOverride ]]
constExpr expr
contextDir PUSHCONTEXT contextItemList ;;
| POPCONTEXT contextItemList ;;
contextItem ASSUMES | RADIX | LISTING | CPU | ALL
contextItemList contextItem
| contextItemList , contextItem
controlBlock whileBlock
| repeatBlock
controlDir controlIf
| controlBlock
controlElseif .ELSEIF cExpr ;;
directiveList
[[ controlElseif ]]
controlIf .IF cExpr ;;
directiveList
[[ controlElseif ]]
[[ .ELSE ;;
directiveList ]]
.ENDIF ;;
coprocessor .8087 | .287 | .387 | .NO87
crefDir crefOption ;;
crefOption .CREF
| .XCREF [[ idList ]]
| .NOCREF [[ idList ]]
cxzExpr expr
| ! expr
| expr == expr
| expr != expr
dataDecl DB | DW | DD | DF | DQ | DT | dataType | typeId
dataDir [[ id ]] dataItem ;;
Nonterminal Definition
dataItem dataDecl scalarInstList
| structTag structInstList
| typeId structInstList
| unionTag structInstList
| recordTag recordInstList
dataType BYTE | SBYTE | WORD | SWORD | DWORD
| SDWORD | FWORD | QWORD | TBYTE
| REAL4 | REAL8 | REAL10
decdigit 0|1|2|3|4|5|6|7|8|9
decNumber decdigit
| decNumber decdigit
delimiter Any character except whiteSpaceCharacter
digits decdigit
| digits decdigit
| digits hexdigit
directive generalDir
| segmentDef
directiveList directive
| directiveList directive
distance nearfar
| NEAR16 | NEAR32 | FAR16 | FAR32
e01 e01 orOp e02
| e02
e02 e02 AND e03
| e03
e03 NOT e04
| e04
e04 e04 relOp e05
| e05
e05 e05 addOp e06
| e06
e06 e06 mulOp e07
| e06 shiftOp e07
| e07
e07 e07 addOp e08
| e08
e08 HIGH e09
| LOW e09
| HIGHWORD e09
| LOWWORD e09
| e09
Nonterminal Definition
e09 OFFSET e10
| SEG e10
| LROFFSET e10
| TYPE e10
| THIS e10
| e09 PTR e10
| e09 : e10
| e10
e10 e10 . e11
| e10 [[ expr ]]
| e11
e11 ( expr )
| [[ expr ]]
| WIDTH id
| MASK id
| SIZE sizeArg
| SIZEOF sizeArg
| LENGTH id
| LENGTHOF id
| recordConst
| string
| constant
| type
| id
|$
| segmentRegister
| register
| ST
| ST ( expr )
echoDir ECHO arbitraryText ;;
%OUT arbitraryText ;;
elseifBlock elseifStatement ;;
directiveList
[[ elseifBlock ]]
elseifStatement ELSEIF constExpr
| ELSEIFE constExpr
| ELSEIFB textItem
| ELSEIFNB textItem
| ELSEIFDEF id
| ELSEIFNDEF id
| ELSEIFDIF textItem , textItem
| ELSEIFDIFI textItem , textItem
| ELSEIFIDN textItem , textItem
| ELSEIFIDNI textItem , textItem
| ELSEIF1
| ELSEIF2
Nonterminal Definition
endDir END [[ immExpr ]] ;;
endpDir procId ENDP ;;
endsDir id ENDS ;;
equDir textMacroId EQU equType ;;
equType immExpr
| textLiteral
errorDir errorOpt ;;
errorOpt .ERR [[ textItem ]]
| .ERRE constExpr [[ optText ]]
| .ERRNZ constExpr [[ optText ]]
| .ERRB textItem [[ optText ]]
| .ERRNB textItem [[ optText ]]
| .ERRDEF id [[ optText ]]
| .ERRNDEF id [[ optText ]]
| .ERRDIF textItem , textItem [[ optText ]]
| .ERRDIFI textItem , textItem [[ optText ]]
| .ERRIDN textItem , textItem [[ optText ]]
| .ERRIDNI textItem , textItem [[ optText ]]
| .ERR1 [[ textItem ]]
| .ERR2 [[ textItem ]]
exitDir .EXIT [[ expr ]] ;;
exitmDir: EXITM
| EXITM textItem
exponent E [[ sign ]] decNumber
expr SHORT e05
| .TYPE e01
| OPATTR e01
| e01
exprList expr
| exprList , expr
externDef [[ langType ]] id [[ ( altId ) ]] : externType
externDir externKey externList ;;
externKey EXTRN | EXTERN | EXTERNDEF
externList externDef
| externList , [[ ;; ]] externDef
externType ABS
| qualifiedType
fieldAlign constExpr
fieldInit [[ initValue ]]
| structInstance
Nonterminal Definition
fieldInitList fieldInit
| fieldInitList , [[ ;; ]] fieldInit
fileChar delimiter
fileCharList fileChar
| fileCharList fileChar
fileSpec fileCharList
| textLiteral
flagName ZERO? | CARRY? | OVERFLOW?
| SIGN? | PARITY?
floatNumber [[ sign ]] decNumber . [[ decNumber ]] [[ exponent ]]
| digits R
| digits r
forcDir FORC | IRPC
forDir FOR | IRP
forParm id [[ : forParmType ]]
forParmType REQ
| = textLiteral
frameExpr SEG id
| DGROUP : id
| segmentRegister : id
| id
generalDir modelDir | segOrderDir | nameDir
| includeLibDir | commentDir
| groupDir | assumeDir
| structDir | recordDir | typedefDir
| externDir | publicDir | commDir | protoTypeDir
| equDir | =Dir | textDir
| contextDir | optionDir | processorDir
| radixDir
| titleDir | pageDir | listDir
| crefDir | echoDir
| ifDir | errorDir | includeDir
| macroDir | macroCall | macroRepeat | purgeDir
| macroWhile | macroFor | macroForc
| aliasDir
gpRegister AX | EAX | BX | EBX | CX | ECX | DX | EDX
| BP | EBP | SP | ESP | DI | EDI | SI | ESI
groupDir groupId GROUP segIdList
groupId id
hexdigit a|b|c|d|e|f
|A|B|C|D|E|F
Nonterminal Definition
id alpha
| id alpha
| id decdigit
idList id
| idList , id
ifDir ifStatement ;;
directiveList
[[ elseifBlock ]]
[[ ELSE ;;
directiveList ]]
ENDIF ;;
ifStatement IF constExpr
| IFE constExpr
| IFB textItem
| IFNB textItem
| IFDEF id
| IFNDEF id
| IFDIF textItem , textItem
| IFDIFI textItem , textItem
| IFIDN textItem , textItem
| IFIDNI textItem , textItem
| IF1
| IF2
immExpr expr
includeDir INCLUDE fileSpec ;;
includeLibDir INCLUDELIB fileSpec ;;
initValue immExpr
| string
|?
| constExpr DUP ( scalarInstList )
| floatNumber
| bcdConst
inSegDir [[ labelDef ]] inSegmentDir
inSegDirList inSegDir
| inSegDirList inSegDir
Nonterminal Definition
inSegmentDir instruction
| dataDir
| controlDir
| startupDir
| exitDir
| offsetDir
| labelDir
| procDir [[ localDirList ]] [[ inSegDirList ]] endpDir
| invokeDir
| generalDir
instrPrefix REP | REPE | REPZ | REPNE | REPNZ | LOCK
instruction [[ instrPrefix ]] asmInstruction
invokeArg register :: register
| expr
| ADDR expr
invokeDir INVOKE expr [[ , [[ ;; ]] invokeList ]] ;;
invokeList invokeArg
| invokeList , [[ ;; ]] invokeArg
keyword Any reserved word
keywordList keyword
| keyword keywordList
labelDef id :
| id ::
| @@:
labelDir id LABEL qualifiedType ;;
langType C | PASCAL | FORTRAN | BASIC
| SYSCALL | STDCALL
listDir listOption ;;
listOption .LIST
| .NOLIST | .XLIST
| .LISTALL
| .LISTIF | .LFCOND
| .NOLISTIF | .SFCOND
| .TFCOND
| .LISTMACROALL | .LALL
| .NOLISTMACRO | .SALL
| .LISTMACRO | .XALL
localDef LOCAL idList ;;
localDir LOCAL parmList ;;
localDirList localDir
| localDirList localDir
Nonterminal Definition
localList localDef
| localList localDef
macroArg % constExpr
| % textMacroId
| % macroFuncId ( macroArgList )
| string
| arbitraryText
| < arbitraryText >
macroArgList macroArg
| macroArgList , macroArg
macroBody [[ localList ]]
macroStmtList
macroCall id macroArgList ;;
| id ( macroArgList )
macroDir id MACRO [[ macroParmList ]] ;;
macroBody
ENDM ;;
macroFor forDir forParm , < macroArgList > ;;
macroBody
ENDM ;;
macroForc forcDir id , textLiteral ;;
macroBody
ENDM ;;
macroFuncId id
macroId macroProcId
| macroFuncId
macroIdList macroId
| macroIdList , macroId
macroLabel id
macroParm id [[ : parmType ]]
macroParmList macroParm
| macroParmList , [[ ;; ]] macroParm
macroProcId id
macroRepeat repeatDir constExpr ;;
macroBody
ENDM ;;
macroStmt directive
| exitmDir
| : macroLabel
| GOTO macroLabel
Nonterminal Definition
macroStmtList macroStmt ;;
| macroStmtList macroStmt ;;
macroWhile WHILE constExpr ;;
macroBody
ENDM ;;
mapType ALL | NONE | NOTPUBLIC
memOption TINY | SMALL | MEDIUM | COMPACT
| LARGE | HUGE | FLAT
mnemonic Instruction name
modelDir .MODEL memOption [[ , modelOptlist ]] ;;
modelOpt langType
| stackOption
modelOptlist modelOpt
| modelOptlist , modelOpt
module [[ directiveList ]] endDir
mulOp * | / | MOD
nameDir NAME id ;;
nearfar NEAR | FAR
nestedStruct structHdr [[ id ]] ;;
structBody
ENDS ;;
offsetDir offsetDirType ;;
offsetDirType EVEN
| ORG immExpr
| ALIGN [[ constExpr ]]
offsetType GROUP | SEGMENT | FLAT
oldRecordFieldList [[ constExpr ]]
| oldRecordFieldList , [[ constExpr ]]
optionDir OPTION optionList ;;
Nonterminal Definition
optionItem CASEMAP : mapType
| DOTNAME | NODOTNAME
| EMULATOR | NOEMULATOR
| EPILOGUE : macroId
| EXPR16 | EXPR32
| LANGUAGE : langType
| LJMP | NOLJMP
| M510 | NOM510
| NOKEYWORD : < keywordList >
| NOSIGNEXTEND
| OFFSET : offsetType
| OLDMACROS | NOOLDMACROS
| OLDSTRUCTS | NOOLDSTRUCTS
| PROC : oVisibility
| PROLOGUE : macroId
| READONLY | NOREADONLY
| SCOPED | NOSCOPED
| SEGMENT : segSize
| SETIF2 : bool
optionList optionItem
| optionList , [[ ;; ]] optionItem
optText , textItem
orOp OR | XOR
oVisibility PUBLIC | PRIVATE | EXPORT
pageDir PAGE [[ pageExpr ]] ;;
pageExpr +
| [[ pageLength ]] [[ , pageWidth ]]
pageLength constExpr
pageWidth constExpr
parm parmId [[ : qualifiedType ]]
| parmId [[ constExpr ]] [[ : qualifiedType ]]
parmId id
parmList parm
| parmList , [[ ;; ]] parm
parmType REQ
| = textLiteral
| VARARG
pOptions [[ distance ]] [[ langType ]] [[ oVisibility ]]
primary expr binaryOp expr
| flagName
| expr
Nonterminal Definition
procDir procId PROC [[ pOptions ]] [[ < macroArgList > ]]
[[ usesRegs ]] [[ procParmList ]]
processor .8086
| .186
| .286 | .286C | .286P
| .386 | .386C | .386P
| .486 | .486P
processorDir processor ;;
| coprocessor ;;
procId id
procParmList [[ , [[ ;; ]] parmList ]]
[[ , [[ ;; ]] parmId :VARARG]]
protoArg [[ id ]] : qualifiedType
protoArgList [[ , [[ ;; ]] protoList ]]
[[ , [[ ;; ]] [[ id ]] :VARARG ]]
protoList protoArg
| protoList , [[ ;; ]] protoArg
protoSpec [[ distance ]] [[ langType ]] [[ protoArgList ]]
| typeId
protoTypeDir id PROTO protoSpec
pubDef [[ langType ]] id
publicDir PUBLIC pubList ;;
pubList pubDef
| pubList , [[ ;; ]] pubDef
purgeDir PURGE macroIdList
qualifiedType type
| [[ distance ]] PTR [[ qualifiedType ]]
qualifier qualifiedType
| PROTO protoSpec
quote “
|‘
radixDir .RADIX constExpr ;;
radixOverride h|o|q|t|y
|H|O|Q|T|Y
recordConst recordTag { oldRecordFieldList }
| recordTag < oldRecordFieldList >
recordDir recordTag RECORD bitDefList ;;
recordFieldList [[ constExpr ]]
| recordFieldList , [[ ;; ]] [[ constExpr ]]
Nonterminal Definition
recordInstance { [[ ;; ]] recordFieldList [[ ;; ]] }
| < oldRecordFieldList >
| constExpr DUP ( recordInstance )
recordInstList recordInstance
| recordInstList , [[ ;; ]] recordInstance
recordTag id
register specialRegister
| gpRegister
| byteRegister
regList register
| regList register
relOp EQ | NE | LT | LE | GT | GE
repeatBlock .REPEAT ;;
blockStatements ;;
untilDir ;;
repeatDir REPEAT | REPT
scalarInstList initValue
| scalarInstList , [[ ;; ]] initValue
segAlign BYTE | WORD | DWORD | PARA | PAGE
segAttrib PUBLIC
| STACK
| COMMON
| MEMORY
| AT constExpr
| PRIVATE
segDir .CODE [[ segId ]]
| .DATA
| .DATA?
| .CONST
| .FARDATA [[ segId ]]
| .FARDATA? [[ segId ]]
| .STACK [[ constExpr ]]
segId id
segIdList segId
| segIdList , segId
segmentDef segmentDir [[ inSegDirList ]] endsDir
| simpleSegDir [[ inSegDirList ]] [[ endsDir ]]
segmentDir segId SEGMENT [[ segOptionList ]] ;;
segmentRegister CS | DS | ES | FS | GS | SS
Nonterminal Definition
segOption segAlign
| segRO
| segAttrib
| segSize
| className
segOptionList segOption
| segOptionList segOption
segOrderDir .ALPHA | .SEQ | .DOSSEG | DOSSEG
segRO READONLY
segSize USE16 | USE32 | FLAT
shiftOp SHR | SHL
sign -|+
simpleExpr ( cExpr )
| primary
simpleSegDir segDir ;;
sizeArg id
| type
| e10
specialChars : | . | [[ | ]] | ( | ) | < | > | { | }
|+|-|/|*|&|%|!
|’|\|=|;|,|“
| whiteSpaceCharacter
| endOfLine
specialRegister CR0 | CR2 | CR3
| DR0 | DR1 | DR2 | DR3 | DR6 | DR7
| TR3 | TR4 | TR5 | TR6 | TR7
stackOption NEARSTACK | FARSTACK
startupDir .STARTUP ;;
stext stringChar
| stext stringChar
string quote [[ stext ]] quote
stringChar quote quote
| Any character except quote
structBody structItem ;;
| structBody structItem ;;
structDir structTag structHdr [[ fieldAlign ]]
[[, NONUNIQUE ]] ;;
structBody
structTag ENDS ;;
structHdr STRUC | STRUCT | UNION
Nonterminal Definition
structInstance < [[ fieldInitList ]] >
| { [[ ;; ]] [[ fieldInitList ]] [[ ;; ]] }
| constExpr DUP ( structInstList )
structInstList structInstance
| structInstList , [[ ;; ]] structInstance
structItem dataDir
| generalDir
| offsetDir
| nestedStruct
structTag id
term simpleExpr
| ! simpleExpr
text textLiteral
| text character
| ! character text
| character
| ! character
textDir id textMacroDir ;;
textItem textLiteral
| textMacroId
| % constExpr
textLen constExpr
textList textItem
| textList , [[ ;; ]] textItem
textLiteral < text >;;
textMacroDir CATSTR [[ textList ]]
| TEXTEQU [[ textList ]]
| SIZESTR textItem
| SUBSTR textItem , textStart [[ , textLen ]]
| INSTR [[ textStart , ]] textItem , textItem
textMacroId id
textStart constExpr
titleDir titleType arbitraryText ;;
titleType TITLE | SUBTITLE | SUBTTL
type structTag
| unionTag
| recordTag
| distance
| dataType
| typeId
typedefDir typeId TYPEDEF qualifier
Nonterminal Definition
typeId id
unionTag id
untilDir .UNTIL cExpr ;;
.UNTILCXZ [[ cxzExpr ]] ;;
usesRegs USES regList
whileBlock .WHILE cExpr ;;
blockStatements ;;
.ENDW
whiteSpaceCharacter ASCII 8, 9, 11–13, 26, 32
A P P E N D I X C
A listing file shows precisely how the assembler translates your source file into
machine code. The listing documents the assembler’s assumptions, memory
allocations, and optimizations.
MASM creates an assembly listing of your source file whenever you do one of the
following:
◆ Select the appropriate option in PWB.
◆ Use one of the related source code directives.
◆ Specify the /Fl option on the MASM command line.
The assembly listing contains both the statements in the source file and the binary
code (if any) generated for each statement. The listing also shows the names and
values of all labels, variables, and symbols in your file.
The assembler creates tables for macros, structures, unions, records, segments,
groups, and other symbols, and places the tables at the end of the assembly listing.
Only the types of symbols encountered in the program are included. For example, if
your program has no macros, the symbol table does not have a macros section.
The dialog box for Set Debug or Release Options lists the choices summarized in
Table C.1. This table also shows the equivalent source code directives and
command-line options.
Generated Code
The assembler lists the code generated from the statements of a source file. With
the /Sc command-line switch, which generates instruction timings, each line has this
syntax:
offset [[timing]] [[code]]
The offset is the offset from the beginning of the current code segment. The timing
shows the number of cycles the processor needs to execute the instruction. The
value of timing reflects the CPU type; for example, specifying the .386 directive
produces instruction timings for the 80386 processor. If the statement generates
code or data, code shows the numeric value in hexadecimal notation if the value is
known at assembly time. If the value is calculated at run time, the assembler
indicates what action is necessary to compute the value.
When assembling under the default .8086 directive, timing includes an effective
address value if the instruction accesses memory. The 80186/486 processors do not
use effective address values. For more information on effective address timing, see
the “Processor” section in the Reference book.
Error Messages
If any errors occur during assembly, each error message and error number appears
directly below the statement where the error occurred. An example of an error line
and message is:
mov ax, [dx][di]
listtst.asm(77): error A2031: must be index or base register
Table C.3 explains the five symbols that may follow timing values in your listing.
The Reference book will help you determine correct timings for those values
marked with a symbol.
.MODEL small, c
.386
.DOSSEG
.STACK 256
INCLUDE dos.mac
C StrDef MACRO name1, text
C name1 BYTE &text
C BYTE 13d, 10d, '$'
C l&name1 EQU LENGTHOF name1
C ENDM
C
C Display MACRO string
C mov ah, 09h
C mov dx, OFFSET string
C int 21h
C ENDM
= 0020 num EQU 20h
COLOR RECORD b:1, r:3=1, i:1=1, f:3=7
= 35 value TEXTEQU %3 + num
= 32 tnum TEXTEQU %num
= 04 strpos TEXTEQU @InStr( , <person>,
<son> )
0002 U1 UNION
0000 0028 fsize WORD 40
bsize BYTE 60
U1 ENDS
0000 .DATA
0000 .CODE
.STARTUP
0000 *@Startup:
0000 2 B8 ---- R * mov ax, DGROUP
0003 2p 8E D8 * mov ds, ax
0005 2 8C D3 * mov bx, ss
0007 2 2B D8 * sub bx, ax
0009 3 C1 E3 04 * shl bx, 004h
000C 2p 8E D0 * mov ss, ax
000E 2 03 E3 * add sp, bx
EXTERNDEF work:NEAR
0010 7m E8 0000 E call work
EXTERNDEF morework:NEAR
0034 7m E8 0000 E call morework
Display ending
0037 2 B4 09 1 mov ah, 09h
0039 2 BA 0046 R 1 mov dx, OFFSET ending
003C 37 CD 21 1 int 21h
.EXIT
003E 2 B4 4C * mov ah, 04Ch
0040 37 CD 21 * int 021h
0042 2 55 bp * push
0043 4 8B EC *
bp, sp mov
0045 2 B4 02 mov ah, 02H
0047 4 8B 7E 04 mov di, pMsg
004A 4 8A 15 mov dl, [di]
mov ax, [dx][di]
listtst.asm(77): error A2031: must be index or base register
.WHILE (dl)
004C 7m EB 10 * jmp @C0001
0059 *@C0002:
0059 37 CD 21 int 21h
005B 2 47 inc di
005C 4 8A 15 mov dl, [di]
.ENDW
005E *@C0001:
005E 2 0A D2 * or dl, dl
0060 7m,3 75 F7 * jne @C0002
ret
0062 4 5D * pop bp
0063 10m C3 * ret 00000h
0064 PutStr ENDP
END
Macro Table
Lists all macros in the main file or the include files. Differentiates between macro
functions and macro procedures.
Record Table
“Width” gives the number of bits of the entire record. “Shift” provides the offset in
bits from the low-order bit of the record to the low-order bit of the field. “Width”
for fields gives the number of bits in the field. “Mask” gives the maximum value of
the field, expressed in hexadecimal notation. “Initial” gives the initial value
supplied for the field.
Type Table
The “Size” column in this table gives the size of the TYPEDEF type in bytes, and
the “Attr” column gives the base type for the TYPEDEF definition.
Symbol Table
All symbols (except names for macros, structures, unions, records, and segments)
are listed in a symbol table at the end of the listing. The “Name” column lists the
names in alphabetical order. The “Type” column lists each symbol’s type.
The length of a multiple-element variable, such as an array or string, is the length of
a single element, not the length of the entire variable.
If the symbol represents an absolute value defined with an EQU or equal sign (=)
directive, the “Value” column shows the symbol’s value. The value may be another
symbol, a string, or a constant numeric value (in hexadecimal), depending on the
type. If the symbol represents a variable or label, the “Value” column shows the
symbol’s hexadecimal offset from the beginning of the segment in which it is
defined.
The “Attr” column shows the attributes of the symbol. The attributes include the
name of the segment (if any) in which the symbol is defined, the scope of the
symbol, and the code length. A symbol’s scope is given only if the symbol is
defined using the EXTERN and PUBLIC directives. The scope can be external,
global, or communal. The “Attr” column is blank if the symbol has no attribute.
A P P E N D I X D
This appendix lists the reserved words recognized by MASM. They are divided
primarily by their use in the language. The primary categories are:
◆ Operands and symbols
◆ Registers
◆ Operators and directives
◆ Processor instructions
◆ Coprocessor instructions
Reserved words in MASM 6.1 are reserved under all CPU modes. Words enabled
in .8086 mode, the default, can be used in all higher CPU modes. To use words
from subcategories such as “Special Operands for the 80386” (later in this
appendix) requires .386 mode or higher.
You can disable the recognition of any reserved word specified in this appendix by
setting the NOKEYWORD option for the OPTION directive. Once disabled, the
word can be used in any way as a user-defined symbol (provided the word is a valid
identifier). If you want to remove the STR instruction, the MASK operator, and the
NAME directive, for instance, from the set of words MASM recognizes as
reserved, add this statement to your program:
OPTION NOKEYWORD:<STR MASK NAME>
Words in this appendix identified with an asterisk (*) are new since MASM 5.1.
Even though the words on the first list are not reserved, they should not be defined
to be text macros or text macro functions. If they are, they will not be recognized in
their special contexts. The assembler does not give a warning if such a redefinition
occurs.
ABS LARGE NOTHING
ALL LISTING* NOTPUBLIC*
ASSUMES LJMP* OLDMACROS*
AT LOADDS* OLDSTRUCTS*
CASEMAP* M510* OS_DOS*
COMMON MEDIUM PARA
COMPACT MEMORY PRIVATE*
CPU* NEARSTACK* PROLOGUE*
DOTNAME* NODOTNAME* RADIX*
EMULATOR* NOEMULATOR* READONLY*
EPILOGUE* NOKEYWORD* REQ*
ERROR* NOLJMP* SCOPED*
EXPORT* NOM510* SETIF2*
EXPR16* NONE SMALL
EXPR32* NONUNIQUE* STACK
FARSTACK* NOOLDMACROS* TINY
FLAT NOOLDSTRUCTS* USE16
FORCEFRAME NOREADONLY* USE32
HUGE NOSCOPED* USES
LANGUAGE* NOSIGNEXTEND*
These operands are reserved words. Reserved words are not case sensitive.
$ DWORD PASCAL
? FAR QWORD
@B FAR16* REAL4*
@F FORTRAN REAL8*
ADDR* FWORD REAL10*
BASIC NEAR SBYTE*
BYTE NEAR16* SDWORD*
C OVERFLOW?* SIGN?*
CARRY?* PARITY?* STDCALL*
Predefined Symbols
Unlike most MASM reserved words, predefined symbols are case sensitive.
@CatStr* @Environ* @Model*
@code @fardata @SizeStr*
@CodeSize @fardata? @stack*
@Cpu @FileCur* @SubStr*
@CurSeg @FileName @Time*
@data @InStr* @Version
@DataSize @Interface* @WordSize
@Date* @Line*
Registers
AH CS DX SI
AL CX EAX SP
AX DH EBP SS
BH DI EBX ST
BL DL ECX TR3*
BP DR0 EDI TR4*
BX DR1 EDX TR5*
CH DR2 ES TR6
CL DR3 ESI TR7
CR0 DR6 ESP
CR2 DR7 FS
CR3 DS GS
Processor Instructions
Processor instructions are not case sensitive.
Instruction Prefixes
LOCK REPE REPNZ
REP REPNE REPZ
Coprocessor Instructions
Coprocessor instructions are not case sensitive.
80387 Instructions
FCOS FRSTORD* FUCOM
FLDENVD* FSAVED* FUCOMP
FNSAVED* FSIN FUCOMPP
FNSTENVD* FSINCOS
FPREM1 FSTENVD*
A P P E N D I X E
If you use simplified segment directives by themselves, you do not need to know the
names assigned for each segment. However, it is possible to mix full segment
definitions with simplified segment directives, in which case you need to know the
segment names.
Table E.1 shows the default segment names created by each directive.
If you use .MODEL, a _TEXT segment is always defined, even if all .CODE
directives specify a name. The default segment name used as part of far-code
segment names is the filename of the module. The default name associated with the
.CODE directive can be overridden, as can the default names for .FARDATA and
.FARDATA?.
The segment and group table at the end of listings always shows the actual segment
names. However, the GROUP and ASSUME statements generated by the
.MODEL directive are not shown in listing files. For a program that uses all
possible segments, group statements equivalent to the following would be
generated:
DGROUP GROUP _DATA, CONST, _BSS, STACK
For small and compact models with NEARSTACK, these ASSUME statements
would be generated:
ASSUME cs: _TEXT, ds:DGROUP, ss:DGROUP
For medium, large, and huge models with NEARSTACK, these ASSUME
statements would be generated:
ASSUME cs:name_TEXT, ds:DGROUP, ss:DGROUP
Table E.1 Default Segments and Types for Standard Memory Models
Model Directive Name Align Combine Class Group
Tiny .CODE _TEXT WORD PUBLIC 'CODE' DGROUP
.FARDATA FAR_DATA PARA PRIVATE 'FAR_DATA'
.FARDATA? FAR_BSS PARA PRIVATE 'FAR_BSS'
.DATA _DATA WORD PUBLIC 'DATA' DGROUP
.CONST CONST WORD PUBLIC 'CONST' DGROUP
.DATA? _BSS WORD PUBLIC 'BSS' DGROUP
Small .CODE _TEXT WORD PUBLIC 'CODE'
.FARDATA FAR_DATA PARA PRIVATE 'FAR_DATA'
.FARDATA? FAR_BSS PARA PRIVATE 'FAR_BSS'
.DATA _DATA WORD PUBLIC 'DATA' DGROUP
.CONST CONST WORD PUBLIC 'CONST' DGROUP
.DATA? _BSS WORD PUBLIC 'BSS' DGROUP
.STACK STACK PARA STACK 'STACK' DGROUP*
Medium .CODE name_TEXT WORD PUBLIC 'CODE'
.FARDATA FAR_DATA PARA PRIVATE 'FAR_DATA'
.FARDATA? FAR_BSS PARA PRIVATE 'FAR_BSS'
.DATA _DATA WORD PUBLIC 'DATA' DGROUP
.CONST CONST WORD PUBLIC 'CONST' DGROUP
.DATA? _BSS WORD PUBLIC 'BSS' DGROUP
.STACK STACK PARA STACK 'STACK' DGROUP*
Compact .CODE _TEXT WORD PUBLIC 'CODE'
.FARDATA FAR_DATA PARA PRIVATE 'FAR_DATA'
.FARDATA? FAR_BSS PARA PRIVATE 'FAR_BSS'
.DATA _DATA WORD PUBLIC 'DATA' DGROUP
.CONST CONST WORD PUBLIC 'CONST' DGROUP
.DATA? _BSS WORD PUBLIC 'BSS' DGROUP
.STACK STACK PARA STACK 'STACK' DGROUP*
Glossary
8087, 80287, or 80387 coprocessor Intel chips that arg In PWB, a function modifier that introduces
perform high-speed floating-point and binary coded an argument or an editing function. The argument
decimal number processing. Also called math may be of any type and is passed to the next
coprocessors. Floating-point instructions are function as input. For example, the PWB command
supported directly by the 80486 processor. Arg textarg Copy passes the text argument
textarg to the function Copy.
A argument A value passed to a procedure or
address The memory location of a data item or function. See “parameter.”
procedure. The expression can represent just the
offset (in which case the default segment is array An ordered set of continuous elements of the
assumed), or it can be in segment:offset format. same type.
addressing modes The various ways a memory assembler A program that converts a text file
address or device I/O address can be generated. containing mnemonically coded microprocessor
See “far address,” “near address.” instructions into the corresponding binary machine
code. MASM is an assembler. See “compiler.”
aggregate types Data types containing more than
one element, such as arrays, structures, and unions. assembly language A programming language in
which each line of source code corresponds to a
animate A debugging feature in which each line in specific microprocessor instruction. Assembly
a running program is highlighted as it executes. language gives the programmer full access to the
The Animate command from the CodeView computer’s hardware and produces the most
debugger Run menu turns on animation. compact, fastest executing code. See “high-level
API (application programming interface) A set of language.”
system-level routines that can be used in an assembly mode The mode in which the CodeView
application program for tasks such as basic debugger displays the assembly-language
input/output and file management. In a graphics- equivalent of the high-level code being executed.
oriented operating environment like Microsoft CodeView obtains the assembly-language code by
Windows, high-level support for video graphics disassembling the executable file. See “source
output is part of the Windows graphical API. mode.”
combine type are private and are placed in separate device driver A program that transforms I/O
physical segments. requests into the operations necessary to make a
specific piece of hardware fulfill that request.
compact A memory model with multiple data
segments but only one code segment. Dialog Command window The window at the
bottom of the CodeView screen where dialog
compiler A program that translates source code commands can be entered, and previously entered
into machine language. Usually applied only to dialog commands can be reviewed.
high-level languages such as Basic, FORTRAN, or
C. See “assembler.” direct memory operand In an assembly-language
instruction, a memory operand that refers to the
constant A value that does not change during contents of an explicitly specified memory
program execution. A variable, on the other hand, location.
is a value that can—and usually does—change.
See “symbolic constant.” directive An instruction that controls the
assembler’s state.
constant expression Any expression that
evaluates to a constant. It may include integer displacement In an assembly-language
constants, character constants, floating-point instruction, a constant value added to an effective
constants, or other constant expressions. address. This value often specifies the starting
address of a variable, such as an array or
multidimensional table.
D
debugger A utility program that allows the DLL See “dynamic-link library.”
programmer to execute a program one line at a
time and view the contents of registers and memory double-click To rapidly press and release a mouse
in order to help locate the source of bugs or other button twice while pointing the mouse cursor at an
problems. Examples are CodeView and Symdeb. object on the screen.
declaration A construct that associates the name double precision A real (floating-point) value that
and the attributes of a variable, function, or type. occupies 8 bytes of memory (MASM type
See “variable declaration.” REAL8). Double-precision values are accurate to
15 or 16 digits.
default A setting or value that is assumed unless
specified otherwise. doubleword A 4-byte word (MASM type
DWORD).
definition A construct that initializes and allocates
storage for a variable, or that specifies either code drag To move the mouse while pointing at an
labels or the name, formal parameters, body, and object and holding down one of the mouse buttons.
return type of a procedure. See “type definition.”
dump To display or print the contents of memory
description file A text file used as input for the in a specified memory range.
NMAKE utility.
dynamic linking The resolution of external
references at load time or run time (rather than link
time). Dynamic linking allows the called
subroutines to be packaged, distributed, and
IEEE format A standard created by the Institute of interrupt handler A routine that receives processor
Electrical and Electronics Engineers for control when a specific interrupt occurs.
representing floating-point numbers, performing
math with them, and handling underflow/overflow interrupt service routine See “interrupt handler.”
conditions. The 8087 family of coprocessors and
the emulator package implement this format. interrupt vector An address that points to an
interrupt handler.
immediate expression An expression that
evaluates to a number that can be either a interrupt vector table A table maintained by the
component of an address or the entire address. operating system. It contains addresses (vectors) of
current interrupt handlers. When an interrupt
immediate operand In an assembly-language occurs, the CPU branches to the address in the
instruction, a constant operand that is specified at table that corresponds to the interrupt’s number.
assembly time and stored in the program file as See “interrupt handler.”
part of the instruction opcode.
linked list A data structure in which each entry macro A block of text or instructions that has
includes a pointer to the location of the adjoining been assigned an identifier. When the assembler
entries. sees this identifier in the source code, it substitutes
the related text or instructions and assembles them.
linking In normal static linking, the process in
which the linker resolves all external references by main module The module containing the point
searching run-time and user libraries, and then where program execution begins (the program’s
computes absolute offset addresses for these entry point). See “module.”
references. Static linking results in a single
executable file. In dynamic linking (see), the math coprocessor See “8087, 80287, or 80387
operating system, rather than the linker, provides coprocessor.”
the addresses after loading the modules into
separate parts of memory. medium A memory model with multiple code
segments but only one data segment.
local constant A constant whose scope is limited
to a procedure or a module. megabyte 1,024 kilobytes or 1,048,576 bytes.
local variable A variable whose scope is confined member One of the elements of a structure or
to a particular unit of code, such as module-level union; also called a field.
code, or a procedure. See “module-level code.”
memory address A number through which a
logical device A symbolic name for a device that program can reference a location in memory.
can be mapped to a physical (actual) device.
memory map A representation of where in
logical line A complete program statement in memory the computer expects to find certain types
source code, including the initial line of code and of information.
any extension lines.
memory model A convention for specifying the
logical segment A memory area in which a number and types of code and data segments in a
program stores code, data, or stack information. module. See “tiny,” “small,” “medium,”
See “physical segment.” “compact,” “large,” “huge,” and “flat.”
low-level input and output routines Run-time memory operand An operand that specifies a
library routines that perform unbuffered, memory location.
unformatted input/output operations.
meta A prefix that modifies the subsequent PWB
LSB (least-significant bit) The bit lowest in function.
memory in a binary number.
mnemonic A word, abbreviation, or acronym that
replaces something too complex to remember or
M type easily. For example, ADC is the mnemonic
machine code The binary numbers that a for the 8086’s add-with-carry instruction. The
microprocessor interprets as program instructions. assembler converts it into machine (binary) code,
See “instruction.” so it is not necessary to remember or calculate the
binary form.
passing by reference Transferring the address of procedure call An expression that invokes a
an argument to a procedure. This allows the procedure and passes actual arguments (if any) to
procedure to modify the argument’s value. the procedure.
passing by value Transferring the value (rather procedure definition A definition that specifies a
than the address) of an argument to a procedure. procedure’s name, its formal parameters, the
This prevents the procedure from changing the declarations and statements that define what it
argument’s original value. does, and (optionally) its return type and storage
class.
physical segment The true memory address of a
segment, referenced through a segment register. procedure prototype A procedure declaration that
includes a list of the names and types of formal
pointer A variable containing the address of parameters following the procedure name.
another variable. See “indirect memory operand.”
process Generally, any executing program or
precedence The relative position of an operator in code unit. This term implies that the program or
the hierarchy that determines the order in which unit is one of a group of processes executing
expression elements are evaluated. independently.
preemptive Having the power to take precedence Program Segment Prefix (PSP) A 256-byte data
over another event. structure at the base of the memory block allocated
prefix A keyword (LOCK, REP, REPE, to a transient program. It contains data and
REPNE, REPNZ, or REPZ) that modifies the addresses supplied by MS-DOS that a program can
behavior of an instruction. MASM 6.1 ensures the read during execution.
prefix is compatible with the instruction. protected mode The 80286–80486 operating
private Data items and routines local to the mode that permits multiple processes to run and not
module in which they are defined. They cannot be interfere with each other. This feature should not
accessed outside that module. See “public.” be confused with privileged mode.
privilege level A hardware-supported feature of public Data items and procedures that can be
the 80286–80486 processors that allows the accessed outside the module in which they are
programmer to specify the exclusivity of a program defined. See “private.”
or process. Programs running at low-numbered
privilege levels can access data or resources at Q
higher-numbered privilege levels, but the reverse is qualifiedtype A user-defined type consisting of an
not true. This feature reduces the possibility that existing MASM type (intrinsic, structure, union, or
malfunctioning code will corrupt data or crash the record), or a previously defined TYPEDEF type,
operating system. together with its language or distance attributes.
privileged mode The term applied to privilege
level 0. This privilege level should be used only by R
a protected-mode operating system. Special radix The base of a number system. The default
privileged instructions are enabled by .286P, radix for MASM and CodeView is 10.
.386P, and .486P. Privileged mode should not be
confused with protected mode. RAM (random-access memory) Computer memory
that can be both written to and read from. RAM
data is volatile; it is usually lost when the computer
is turned off. Programs are loaded into and contains BIOS routines and parts of the operating
executed from RAM. See “ROM.” system. See “RAM.”
real mode The normal operating mode of the 8086 routine A generic term for a procedure or
family of processors. Addresses correspond to function.
physical (not mapped) memory locations, and there
is no mechanism to keep one application from run-time dynamic linking The act of establishing a
accessing or modifying the code or data of another. link when a process is running. See “dynamic
See “protected mode.” linking.”
record A MASM variable that consists of a run-time error A math or logic error that can be
sequence of bit values. detected only when the program runs. Examples of
run-time errors are dividing by a variable whose
reentrant procedure A procedure that can be value is zero or calling a DLL function that doesn’t
safely interrupted during execution and restarted exist.
from its beginning in response to a call from a
preemptive process. After servicing the preemptive S
call, the procedure continues execution at the point
at which it was interrupted. scope The range of statements over which a
variable or constant can be referenced by name.
register operand In an assembly-language See “global constant,” “global variable,” “local
instruction, an operand that is stored in the register constant,” “local variable.”
specified by the instruction.
screen swapping A screen-exchange method that
register window The optional CodeView window uses buffers to store the debugging and output
in which the CPU registers and the flag register screens. When you request the other screen, the
bits are displayed. two buffers are exchanged. This method is slower
than flipping (the other screen-exchange method),
registers Memory locations in the processor that but it works with most adapters and most types of
temporarily store data, addresses, and processor programs.
flags.
scroll bars The bars that appear at the right side
regular expression A text expression that specifies and bottom of a window and some list boxes.
a pattern of text to be matched (as opposed to Dragging the mouse on the scroll bars allows
matching specific characters). scrolling through the contents of a window or text
relocatable Not having an absolute address. The box.
assembler does not know where the label, data, or segment A section of memory, limited to 64K
code will be located in memory, so it generates a with 16-bit segments or 4 gigabytes with 32-bit
fixup record. The linker provides the address. segments, containing code or data. Also refers to
return value The value returned by a function. the starting address of that memory area.
ROM (read-only memory) Computer memory that sequential mode The mode in CodeView in which
can only be read from and cannot be modified. no windows are available. Input and output scroll
ROM data is permanent; it is not lost when the down the screen, and the old output scrolls off the
machine is turned off. A computer’s ROM often top of the screen when the screen is full. You
cannot examine previous commands after they
scroll off the top. This mode is required with represents the machine code currently being
computers that are not IBM compatible. executed.
selector A value that indirectly references a stack An area of memory in which data items are
segment address. A protected-mode operating consecutively stored and removed on a last-in,
system, such as Windows, assigns selector values first-out basis. A stack can be used to pass
to programs, which use them as segment addresses. parameters to procedures.
If a program attempts to use an unassigned
selector, it triggers a General-Protection fault stack frame The portion of a stack containing a
(see). particular procedure’s local variables and
parameters.
shared memory A memory segment that can be
accessed simultaneously by more than one process. stack probe A short routine called on entry to a
function to verify that there is enough room in the
shell escape A method of gaining access to the program stack to allocate local variables required
operating system without leaving CodeView or by the function.
losing the current debugging context. It is possible
to execute MS-DOS commands, then return to the stack switching Changing the stack pointers to
debugger. point to another stack area.
sign extended The process of widening an integer stack trace A symbolic representation of the
(for example, going from a byte to a word, or a functions that are being executed to reach the
word to a doubleword) while retaining its correct current instruction address. As a function is
value and sign. executed, the function address and any function
arguments are pushed on the stack. Therefore,
signed integer An integer value that uses the tracing the stack shows the active functions and
most-significant bit to represent the value’s sign. If their arguments.
the bit is one, the number is negative; if zero, the
number is positive. See “two’s complement,” standard error The device to which a program can
“unsigned integer,” “MSB.” send error messages. The display is normally
standard error.
single precision A real (floating-point) value that
occupies 4 bytes of memory. Single-precision standard input The device from which a program
values are accurate to six or seven decimal places. reads its input. The keyboard is normally standard
input.
single-tasking environment An environment in
which only one program runs at a time. MS-DOS standard output The device to which a program
is a single-tasking environment. can send its output. The display is normally
standard output.
small A memory model with only one code
segment and only one data segment. statement A combination of labels, data
declarations, directives, or instructions that the
source file A text file containing symbols that assembler can convert into machine code.
define the program.
status bar See “linking.”
source mode The mode in which CodeView
displays the assembly-language source code that
static linking The line at the bottom of the PWB or text box In PWB, a box where you type
CodeView screen. The status bar displays text information needed to carry out a command. A text
position, keyboard status, current context of box appears within a dialog box. The text box may
execution, and other program information. be blank or contain a default entry.
STDCALL A calling convention that uses caller tiny Memory model with a single segment for both
stack cleanup if the VARARG keyword is code and data. This limits the total program size to
specified. Otherwise the called routine must clean 64K. Tiny programs have the filename extension
up the stack. .COM.
string A contiguous sequence of characters toggle A function key or menu selection that turns
identified with a symbolic name. a feature off if it is on, or on if it is off. Used as a
verb, “toggle” means to reverse the status of a
string literal A string of characters and escape feature.
sequences delimited by single quotation marks
(' ') or double quotation marks (" "). TOOLS.INI A file containing initialization
information for many of the Microsoft utilities,
structure A set of variables that may be of including PWB.
different types, grouped under a single name.
two’s complement A form of base-2 notation in
structure member One of the elements of a which negative numbers are formed by inverting
structure. Also called a field. the bit values of the equivalent positive number
switch See “option.” and adding 1 to the result.
symbol A name that identifies a memory location type A description of a set of values and a valid
(usually for data). set of operations on items of that type. For
example, a variable of type BYTE can have any of
symbolic constant A constant represented by a a set of integer values within the range specified
symbol rather than the constant itself. Symbolic for the type on a particular machine.
constants are defined with EQU statements. They
make a program easier to read and modify. type checking An operation in which the
assembler verifies that the operands of an operator
SYSCALL A language type for a procedure. Its are valid or that the actual arguments in a function
conventions are identical to C’s, except no call are of the same types as the function
underscore is prefixed to the name. definition’s parameters.
type definition The storage format and attributes
T for a data unit.
tag The name assigned to a structure, union, or
enumeration type. U
task See “process.” unary expression An expression consisting of a
single operand preceded or followed by a unary
text Ordinary, readable characters, including the operator.
uppercase and lowercase letters of the alphabet, the
numerals 0 through 9, and punctuation marks. unary operator An operator that acts on a single
operand, such as NOT.
underflow An error condition that occurs when a visibility The characteristic of a variable or
calculation produces a result too small for the function that describes the parts of the program in
computer to represent. which it can be accessed. An item has global
visibility if it can be referenced in every source file
unhooking (an interrupt) The act of removing your constituting the program. Otherwise, it has local
interrupt handler and restoring the original vector. visibility.
See “hooking (an interrupt).”
union A set of values (in fields) of different types W
that occupy the same storage space. watch window The window in CodeView that
unresolved external See “unresolved reference.” displays watch statements and their values. A
variable or expression is watchable only while
unresolved reference A reference to a global or execution is occurring in the section of the program
external variable or function that cannot be found, (context) in which the item is defined.
either in the modules being linked or in the
libraries linked with those modules. An unresolved window A discrete area of the screen in PWB or
reference causes a fatal link error. CodeView used to display part of a file or to enter
statements.
unsigned integer An integer in which the most-
significant bit serves as part of the number, rather window commands Commands that work only in
than as an indication of sign. For example, an CodeView’s window mode. Window commands
unsigned byte integer can have a value from 0 to consist of function keys, mouse selections, CTRL
255. A signed byte integer, which reserves its and ALT key combinations, and selections from
eighth bit for the sign, can range from -127 to pop-up menus.
+128. See “signed integer,” “MSB.” window mode The mode in which CodeView
user-defined type A data type defined by the user. displays separate windows, which can change
It is usually a structure, union, record, or pointer. independently. CodeView has mouse support and a
wide variety of window commands in window
mode.
V
variable declaration A statement that initializes word A data unit containing 16 bits (2 bytes). It
and allocates storage for a variable of a given type. can store values from 0 to 65,535 (or -32,768 to
+32,767).
virtual disk A portion of the computer’s random
access memory reserved for use as a simulated disk
drive. Also called an electronic disk or RAM disk.
Unless saved to a physical disk, the contents of a
virtual disk are lost when the computer is turned
off.
virtual memory Memory space allocated on a disk,
rather than in RAM. Virtual memory allows large
data structures that would not fit in conventional
memory, at the expense of slow access.
Index
! (literal-character operator) 235 32-bit programming 335
!= (not equal operator) 178 80186 processor 3
” (double quotation marks) 109, 353 80188 processor 3
$ (current address operator) 368 80286 processor 3
% (expansion operator) 235, 248, 357 80287 math coprocessor 3, 135
& (substitution operator) 238, 372 80386 processor 3, 335
&& (logical AND operator) 178 80387 math coprocessor 3, 135
’ (single quotation mark) 109, 353 80486 processor 3, 135
( ) (parentheses) 106 8086-based processors 2– 3
+ (plus operator) 63, 66, 352, 370 8087 math coprocessor 3, 135
. (dot operator) 126 8088 processor 3
. (structure-member operator) 64, 67, 352, 370
.186 directive 38
.286 directive 38 A
.286P directive 38 AAD instruction 160
.287 directive 38 AAM instruction 160
.386 directive ABS operand 220
FLAT, with 26, 36 Accessing data with pointers See Pointer variables
processor mode, specifying 38, 336 ADC instruction 92– 94
segment mode, setting 46, 68 ADD instruction 92– 94
.386P directive 38 ADDR operator 197
.387 directive 38 Addresses
.486 directive displacement of 65
FLAT, with 36 dynamic 79
processor mode, specifying 38 effective 65
segment mode, setting 46, 68 errors in 54
.486P directive 38 far 57, 74, 80
.8087 directive 38 near 57, 80
: (colon) 22, 352, 354 physical 7
: (segment-override operator) 50, 59– 60, 64 registers, loading into 80
:: (double colon) 197, 215, 352– 354 relocatable 57
; (semicolon) 21 segmented 7– 8, 53
;; (double semicolon) 227 Addressing
< (less than operator) 178 direct registers, used in 62– 63
< > (angle brackets) See Angle brackets indirect registers, used in 65, 68
= = (equal operator) 178 scaling operands 70
> (greater than operator) 178 specifying 60
? (question mark initializer) Aliases 87, 369
array elements 109 ALIGN directive 3
described 368 Align types 45
variables 87 See also individual entries
@ (at sign) 10 .ALPHA directive 47
@@: (anonymous label) 170 AND instruction 27, 99, 100
[ ] (brackets) 107 Angle brackets (< >)
[ ] (index operator) 63 default parameters 230
\ (backslash character), MASM code 22 epilogues 202
\ (line-continuation character) 121 FOR loops 242
{} (curly braces) 121, 131 FORC loops 244
|| (logical OR operator) 178
.ELSEIF directive 171 Expansion operator (%) 235– 236, 248, 357
ELSEIF directive 28 Explicit loading 258
ELSEIF1 directive 358 Exponent bias 139
ELSEIF2 directive 29, 358 EXPORT operand 185
EMULATOR argument, OPTION directive 27, 157 EXPORTS statement 261, 270
Emulator libraries 155– 156 EXPR16 argument, OPTION directive 13, 26, 361, 373
END directive 33, 56 EXPR32 argument, OPTION directive 13, 26, 373
.ENDIF directive 171 Expression operators 178
ENDIF directive 28 Expressions
ENDM directive 227– 239 assembly-time evaluation 23
ENDP directive 180– 181, 206 constant 12
ENDS directive 44 loop conditions, evaluating 179
.ENDW directive 173 OPTION M510 behavior 364, 373
ENTER instruction 183 order of evaluation 14
Environment size 366, 373
target 4 word size 13, 26
variables Extension, filename 266
INCLUDE 213 EXTERN directive
LIB 222 data-sharing 211
returning values of 10 executable file size, limiting 223
/EP command-line option, ML 342 module-specific 220
EPILOGUE argument, OPTION directive 26, 201– 203 overview 16
Epilogue code positioning 218
defined 198 procedure prototypes, declaring 193
macros 201– 202, 264– 265 External declarations 216– 218
PROC statement, specifying arguments in 185 External variables 217, 369
procedures, with 26 EXTERNDEF directive
RET instruction 357 data-sharing 211
standard 199 overview 16
user-defined 201 positioning 218
EQ operator 365 procedure prototypes, declaring 193
EQU directive 12, 369 symbols, declaring 214– 215
Equal directive (=) 12
Equates, predefined See Predefined symbols
.ERR directive 29 F
.ERR1 directive 30, 358 Far addresses, invoking 57, 74, 80– 81, 197
.ERR2 directive 30, 358 Far code 57
.ERRB directive 29, 231 Far data 58– 60
.ERRDEF directive 29 .FARDATA directive 33, 39– 40
.ERRDIF directive 29 .FARDATA? directive 39– 40
.ERRE directive 29 FAR operator 169, 185
.ERRIDN directive 29 Far pointer 74, 80– 81
.ERRNB directive 29, 231 FARSTACK operand
.ERRNDEF directive 29 example 35
.ERRNZ directive 29 grouping 34
Error detection 196 in Windows-based programs 266
ERROR operand 49–50 MS-DOS program, initializing 43
Errors, argument passing 196 special cases, setting for 37
ESC instruction 360 Farwords 86
EVEN directive 3 FCOM instruction 153
Executable (.EXE) files, controlling size of 223 Fields, statements in 21– 22
Exit codes, Windows operating system 263 Files
.EXIT directive 33, 41– 43 .COM
EXITM directive 248 relocatable segment expression, lacking 62
LENGTHOF 346
O Operators (continued)
OFFSET LOW 356
FLAT argument, OPTION directive 27 LOWWORD 346, 366
GROUP argument, OPTION directive 27 LROFFSET 344
SEGMENT argument, OPTION directive 27, 62 macro 251
OFFSET operator 61, 82, 356, 374 MASK 133
Offsets minus (– ) 64
accessing data with 74 NE 365
addresses 7 NEAR 169, 185
described 5– 7 OFFSET 61, 82, See OFFSET operator
determining 23– 24, 360, 374 OPATTR 252– 254
fixups for 26 plus (+) 63, 66
OLDMACROS argument, OPTION directive 25, 239, 361, precedence 14
372 PTR See PTR operator
OLDSTRUCTS argument, OPTION directive PTR, example See PTR operator
MASM 5.1 compatibility 25, 361, 370– 372 relational 357, 365
structures, with 119, 126 relational (list) 178
Online help See Microsoft Advisor SEG 50, 62, 363
OPATTR operator 252– 253 segment-override (:) 59, 64
Operands SHORT 169
ABS 220 SIZE 364– 365
direct memory 60– 64 size See PTR operator
EXPORT 185 SIZEOF 86, 346
FAR 15 structure-member (.) 64– 67, 126, 352, 370
FARSTACK See FARSTACK operand substitution (&) 238
FLAT 46, 49– 50 .TYPE 252, 360
FORCEFRAME 244 TYPE 86
immediate 60– 62 WIDTH 133
indirect memory 60, 64– 70 OPTION directive
NEAR 15 CASEMAP 25
PRIVATE READONLY 44– 45 described 23
registers 61 DOTNAME 25, 361, 373
size 66, 355 emulation mode 157
USE16 44– 46 EMULATOR 26, 157
USE32 44– 46 EPILOGUE 26, 201– 203
Operating systems EXPR16 OPTION directive 13, 26, 361, 373
(list) 4 EXPR32 OPTION directive 13, 26, 373
.MODEL, specifying with 34 LANGUAGE 26, 193
multitasking 6 language types, specifying 308
types See MS-DOS, Windows operating systems list of arguments for 25
Operators LJMP 26
ADDR 197 M510 See M510 argument, OPTION directive
current address ($) 368 NODOTNAME 25
dot (.) 126, 352, 370 NOEMULATOR 26
EQ 365 NOKEYWORD See NOKEYWORD argument, OPTION
expansion (%) 235– 236, 248, 357 directive
expressions, in 12– 13 NOLJMP 27, 170
FAR 169, 185 NOM510 25
HIGH 356 NOOLDMACROS 26
HIGHWORD 346 NOOLDSTRUCTS 26
index ([ ]) 63 NOREADONLY 27
instructions, compared to 13 NOSCOPED 26, 362, 375
LENGTH 356– 357, 364 NOSIGNEXTEND 27, 378
Records with SIZEOF operator See SIZEOF operator RETF instruction 181, 378
Records with TYPE operator See TYPE operator RETN instruction 181, 378
Recursive macros 255 ROL instruction 101– 104
Register operands 61 ROM-BIOS interrupts See Interrupts
Registers ROR instruction 101– 104
16-bit 16– 17, 67 Rotate instructions 101
32-bit 335 Routines, interrupt 206
base 65– 70
coprocessor 140
copying pairs of 82 S
division (table) 98 SAL instruction 101– 104
Eflags 20 SAR instruction 101– 104
extended 17 SBB instruction 92– 94
flags 20 SBYTE directive 86
FS 17 Scaling factor 107
general purpose 19 Scaling index registers 67– 69
GS 17 SCAS instruction 110– 112, 115, 353
index 65– 69 Scope within visibility See also Visibility
indirect addressing 65 SCOPED argument, OPTION directive 26
indirect operands 67– 68 SDWORD directive 86
initializing 44 SEG operator 49, 62, 363
Instruction Pointer (IP) 20, 57, 161 SEGMENT
(list) 409 FLAT argument, OPTION directive 27
loading addresses into 80 USE16 argument, OPTION directive 27
mixed 16-bit, 32-bit 70 USE32 argument, OPTION directive 27
pointers as 77 Segment arithmetic 7
scaling 67– 69 SEGMENT directive 44– 47
segment See Segment registers Segment mode, setting See .386 directive; .486 directive
Stack Pointer (SP) 19 Segment registers
Stack Segment (SS) 73 32-bit 335
stacks, saving on 74 assigning 59, 62
types, defined with ASSUME 77 ASSUME directive 49– 55, 58– 59, 357
Relational operators (list) 178 changing 57
Relocatable default 60, 64
addresses 57 described 18
expressions 62, 65 FS 18
REP instruction 110– 112, 363 GS 18
REPE instruction 110– 112, 363 initializing 43, 54– 57
Repeat blocks 239 MS-DOS, under 24, 43
.REPEAT directive 173 near code 57
REPEAT directive 240 restoring 59
REPNE instruction 110– 112, 353, 363 segment-override operator (:) 50, 59– 60, 64
REPNZ instruction 110– 112, 353, 363 Segment registers
Reporting problems xx initializing See STARTUP directive
REPZ instruction 110– 112 setting See STACK directive
Reserved words Segment selectors 5
described 8, 26 Segment-override operator (:) 50, 59– 60, 64
(list) 407 Segmented architecture 2, 5
OPTION M510 behavior 362 Segments
OPTION NOKEYWORD 376 32-bit 36, 335
RET instruction accessing data 74
epilogue code, generating 200, 378 aligning 44– 45
instruction encodings, changes to 357 class types 44, 47– 48
PROC, with 180
U W
WDEB386 debugger 264
Unconditional jumps 162 WEP (Windows Exit Procedure) 263– 264, 270
UNION directive 118– 119, 122, 125– 129 .WHILE directive 173
Unions WHILE directive 241
arrays as initializers 122 WIDTH operator 133
arrays of 124 Windows operating system
defined 117 API 257, 262
fields 119, 127– 129 applications 258, 261
memory allocation 117 DLLs 261
X
XCHG instruction 90
/X command-line option, ML 213
XLAT instruction 116
XLATB instruction 116
XOR instruction 27, 99– 100
Z
ZERO? flag as operand 178
/Zm command-line option, ML 62, 119
/Zp command-line option, ML 119
Rate each component of the document set: What one thing would you like to see added to or removed from
Rate each from 1 (never use) to 5 (often use). each component?
Getting Started ______________________________________
1 2 3 4 5 Getting Started
__________________________________________________
1 2 3 4 5 Programmer’s Guide __________________________________________________
1 2 3 4 5 Environment and Tools __________________________________________________
.............................................................................................................. Fold...............................................................................................................
Place stamp
here.
Post Office
will not deliver
without proper
postage.
Microsoft Corporation
Languages — MASM 6.1
One Microsoft Way
Redmond WA 98052-9953