CHAPTER 8 NUMBERS AND EXPRESSIONS
Numbers and Bases
A86 supports a variety of formats for numbers. In non-computer
life, we write numbers in a decimal format. There are ten
digits, 0 through 9, that we use to describe numbers; and each
digit position is ten times as significant as the position to its
right. The number ten is called the "base" of the decimal
format. Computer programmers often find it convenient to use
other bases to specify numbers used in their programs. The most
commonly-used bases are two (binary format), sixteen (hexadecimal
format), and eight (octal format).
The hexadecimal format requires sixteen digits. The extra six
digits beyond 0 through 9 are denoted by the first six letters of
the alphabet: A for ten, B for eleven, C for twelve, D for
thirteen, E for fourteen, and F for fifteen.
In A86, a number must always begin with a digit from 0 through 9,
even if the base is hexadecimal. This is so that A86 can
distinguish between a number and a symbol that happens to have
digits in its name. If a hexadecimal number would begin with a
letter, you precede the letter with a zero. For example, hex A0,
which is the same as decimal 160, would be written 0A0.
Because it is necessary for you to append leading zeroes to many
hex numbers, and because you never have to do so for decimal
numbers, I decided to make hexadecimal the default base for
numbers with leading zeroes. Decimal is still the default base
for numbers beginning with 1 through 9.
Large numbers can be given as the operands to DD, DQ, or DT
directives. For readability, you may freely intersperse
underscore characters anywhere with your numbers.
The default base can be overridden, with a letter or letters at
the end of the number: B or xB for binary, O or Q for octal, H
for hexadecimal, and D or xD for decimal. Examples:
077Q octal, value is 8*7 + 7 = 63 in decimal notation
123O octal if the "O" is a letter: 64 + 2*8 + 3 = 83 decimal
1230 decimal 1230: shows why you should use "Q" for octal!!
01234567H large constant
0001_0000_0000_0000_0003R real number specified in hexadecimal
100D superfluous D indicates decimal base
0100D hex number 100D, which is 4096 + 13 = 5009 in decimal
0100xD decimal 100, since xD overrides the default hex format
0110B hex 110B, which is 4096 + 256 + 11 = 4363 in decimal
0110xB binary 4+2 = 6 in decimal notation
110B also binary 4+2 = 6, since "B" is not a decimal digit
8-2
The last five examples above illustrate why an "x" is sometimes
necessary before the base-override letter "B" or "D". If that
letter can be interpreted as a hex digit, it is; the "x" forces
an override interpretation for the "B" or "D". By the way, the
usage of lower case for x and upper case for the following
override letter is simply a recommendation; A86 treats upper-and
lower-case letters equivalently.
The RADIX Directive
The above-mentioned set of defaults (hex if leading zero, decimal
otherwise) can be overridden with the RADIX directive. The RADIX
directive consists of the word RADIX followed by a number from 2
to 16. The default base for the number is ALWAYS decimal,
regardless of any (or no) previous RADIX commands. The number
gives the default base for ALL subsequent numbers, up to (but not
including) the next RADIX command. If there is no number
following RADIX, then A86 returns to its initial mixed default of
hex for leading zeroes, decimal for other leading digits.
For compatibility with IBM's assembler, RADIX can appear with a
leading period; although I curse the pinhead designer who put
that period into IBM's language.
As an alternative to the RADIX directive, I provide the D switch,
which causes A86 to start with decimal defaults. You can put +D
into the A86 command invocation, or into the A86 environment
variable. The first RADIX command in the program will override
the D switch setting.
Following are examples of radix usage. The numbers in the
comments are all in decimal notation.
DB 10,010 ; produces 10,16 if RADIX was not seen yet
; and +D switch was not specified
RADIX 10
DB 10,010 ; produces 10,10
RADIX 16
DB 10,010 ; produces 16,16
RADIX 2
DB 10,01010 ; produces 2,10
RADIX 3 ; for Martian programmers in Heinlein novels
DB 10,100 ; produces 3,9
RADIX
DB 10,010 ; produces 10,16
8-3
Floating Point Initializations
A86 allows floating point numbers as the operands to DD, DQ, and
DT directives. The numbers are encoded according to the IEEE
standard, followed by the 8087 and 287 coprocessors. The format
for floating point constants is as follows: First, there is a
decimal number containing a decimal point. There must be a
decimal point, or else the number is interpreted as an integer.
There must also be at least one decimal digit, either to the left
or right of the decimal point, or else the decimal point is
interpreted as an addition (structure element) operator.
Optionally, there may follow immediately after the decimal number
the letter E followed by a decimal number. The E stands for
"exponent", and means "times 10 raised to the power of". You may
provide a + or - between the E and its number. Examples:
0.1 constant one-tenth
.1 the same
300. floating point three hundred
30.E1 30 * 10**1; i.e., three hundred
30.E+1 the same
30.E-1 30 * 10**-1; i.e., three
30E1 not floating point: hex integer 030E1
1.234E20 scientific notation: 1.234 times 10 to the 20th
1.234E-20 a tiny number: 1.234 divided by 10 to the 20th
Overview of Expressions
Most of the operands that you code into your instructions and
data initializations will be simple register names, variable
names, or constants. However, you will regularly wish to code
operands that are the results of arithmetic calculations,
performed either by the machine when the program is running (for
indexing), or by the assembler (to determine the value to
assemble into the program). A86 has a full set of operators that
you can use to create expressions to cover these cases:
* Arithmetic Operators
byte isolation and combination (HIGH, LOW, BY)
addition and subtraction (+,-)
multiplication and division (* , /, MOD)
shifting operators (SHR, SHL, BIT)
* Logical Operators
(AND, OR, XOR, NOT)
* Boolean Negation Operator
(!)
* Relational Operators
(EQ, LE, LT, GE, GT, NE)
* String Comparison Operators
(EQ, NE, =)
8-4
* Attribute Operators/Specifiers
size specifiers (B=BYTE,W=WORD,F=FAR,SHORT,LONG)
attribute specifiers (OFFSET,NEAR,brackets)
segment addressing specifier (:)
compatibility operators (PTR,ST)
built-in value specifiers (TYPE,THIS,$)
* Special Data Duplication Operator
(DUP) --see Chapter 9 for a description
Types of Expression Operands
Numbers and Label Addresses
A number or constant (16-bit number) can be used in most
expressions. A label (defined with a colon) is also treated as
a constant and so can be used in expressions, except when it is a
forward reference.
Variables
A variable stands for a byte- or word-memory location. You may
add or subtract constants from variables; when you do so, the
constant is added to the address of the variable. You typically
do this when the variable is the name of a memory array.
Index Expressions
An index expression consists of a combination of a base register
[BX] or [BP], and/or an index register [SI] or [DI], with an
optional constant added or subtracted. You will usually want to
precede the bracketed expression with B, W, or F; to specify the
kind of memory unit (byte, word, or far pointer) you are
referring to. The expression stands for the memory unit whose
address is the run-time value(s) of the base and/or index
registers added to the constant. See the Effective Address
section and the beginning of this chapter for more details on
indexed memory.
Arithmetic Operators
HIGH/LOW
Syntax: HIGH operand
LOW operand
These operators are called the "byte isolation" operators. The
operand must evaluate to a 16-bit number. HIGH returns the
high order byte of the number; LOW the low order byte.
For example,
MOV AL,HIGH(01234) ; AL = 012
TENHEX EQU LOW(0FF10) ; TENHEX = 010
8-5
These operators can be applied to each other. The following
identities apply:
LOW LOW Q = LOW Q
LOW HIGH Q = HIGH Q
HIGH LOW Q = 0
HIGH HIGH Q = 0
BY
Syntax: operand BY operand
This operator is a "byte combination" operator. It returns the
word whose high byte is the left operand, and whose low byte is
the right operand. For example, the expression 3 BY 5 is the
same as hexadecimal 0305. The BY operator is exclusive to A86. I
added it to cover the following situation: Suppose you are
initializing your registers to immediate values. Suppose you
want to initialize AH to the ASCII value 'A', and AL to decimal
10. You could code this as two instructions MOV AH,'A' and MOV
AL,10; but you realize that a single load into the AX register
would save both program space and execution time. Without the BY
operator, you would have to code MOV AX,0410A, which disguises
the types of the individual byte operands you were thinking
about. With BY, you can code it properly: MOV AX,'A' BY 10.
Addition (combination)
Syntax: operand + operand
operand.operand
operand PTR operand
operand operand
As shown in the above syntax, addition can be accomplished in
four ways: with a plus sign, with a dot operator, with a PTR
operator, and simply by juxtaposing two operands next to each
other. The dot and PTR operators are provided for compatibility
with Intel/IBM assemblers. The dot is used in structure field
notation; PTR is used in expressions such as BYTE PTR 0. (See
Chapter 12 for recommendations concerning PTR.)
If either operand is a constant, the answer is an expression with
the typing of the other operand, with the offsets added. For
example, if BVAR is a byte variable, then BVAR + 100 is the byte
variable 100 bytes beyond BVAR.
Other examples:
DB 100+17 ; simple addition
CTRL EQU -040
MOV AL,CTRL'D' ; a nice notation for control-D!
MOV DX,[BP].SMEM ; --where SMEM was in an unindexed structure
DQ 10.0 + 7.0 ; floating point addition
8-6
Subtraction
Syntax: operand - operand
The subtraction operator may have operands that are:
a. both absolute numbers
b. variable names that have the same type
The result is an absolute number; the difference between the two
operands.
Subtraction is also allowed between floating point numbers; the
answer is the floating point difference.
Multiplication and Division
Syntax: operand * operand (multiplication)
operand / operand (division)
operand MOD operand (modulo)
You may only use these operators with absolute or floating point
numbers, and the result is always the same type. Either operand
may be a numeric expression, as long as the expression evaluates
to an absolute or floating point number. Examples:
CMP AL,2 * 4 ; compare AL to 8
MOV BX,0123/16 ; BX = 012
DT 1.0 / 7.0
Shifting Operators
Syntax: operand SHR count (shift right)
operand SHL count (shift left)
BIT count (bit number)
The shift operators will perform a "bit-wise" shift of the
operand. The operand will be shifted "count" bits either to the
right or the left. Bits shifted into the operand will be set to
0.
The expression "BIT count" is equivalent to "1 SHL count"; i.e.,
BIT returns the mask of the single bit whose number is "count".
The operands must be numeric expressions that evaluate to
absolute numbers. Examples:
MOV BX, 0FACBH SHR 4 ; BX = 0FACH
OR AL,BIT 6 ; AL = AL OR 040; 040 is the mask for bit 6
8-7
Logical Operators
Syntax: operand OR operand
operand XOR operand
operand AND operand
NOT operand
The logical operators may only be used with absolute numbers.
They always return an absolute number.
Logical operators operate on individual bits. Each bit of the
answer depends only on the corresponding bit in the operand(s).
The functions performed are as follows:
1. OR: An answer bit is 1 if either or both of the operand bits
is 1. An answer bit is 0 only if both operand bits are 0.
Example:
11110000xB OR 00110011xB = 11110011xB
2. XOR: This is "exclusive OR." An answer bit is 1 if the
operand bits are different; an answer bit is 0 if the operand
bits are the same. Example:
11110000xB XOR 00110011xB = 11000011xB
3. AND: An answer bit is 1 only if both operand bits are 1. An
answer bit is 0 if either or both operand bits are 0.
Example:
11110000xB AND 00110011xB = 00110000xB
4. NOT: An answer bit is the opposite of the operand bit. It
is 1 if the operand bit is 0; 0 if the operand bit is 1.
Example:
NOT 00110011xB = 11001100xB
Boolean Negation Operator
Syntax: ! operand
The exclamation-point operator, rather than reversing each
individual bit of the operand, considers the entire operand as a
boolean variable to be negated. If the operand is non-zero (any
of the bits are 1), the answer is 0. If the operand is zero, the
answer is 0FFFF.
8-8
Because ! is intended to be used in conditional assembly
expressions (described in Chapter 11), there is also a special
action when ! is applied to an undefined name: the answer is the
defined value 0FFFF, meaning it is TRUE that the symbol is
undefined. Similarly, when ! is applied to some defined quantity
other than an absolute constant, the answer is 0, meaning it is
FALSE that the operand is undefined.
Relational Operators
Syntax: operand EQ operand (equal)
operand NE operand (not equal)
operand LT operand (less than)
operand LE operand (less or equal)
operand GT operand (greater than)
operand GE operand (greater or equal)
The relational operators may have operands that are:
a. both absolute numbers
b. variable names that have the same type
The result of a relational operation is always an absolute
number. They return an 8-or 16-bit result of all 1's for TRUE
and all 0's for FALSE. Examples:
MOV AL, 3 EQ 0 ; AL = 0 (false)
MOV AX, 2 LE 15 ; AX = 0FFFFH (true)
String Comparison Operators
Syntax: string EQ string (equal)
string NE string (not equal)
string = string (equal ignoring case)
In order to subsume the string comparison facilities offered by
That Other Assembler's special conditional-assembly directives
IFIDN and IFDIF, A86 allows the relational operators EQ and NE to
accept string arguments. For this syntax to be accepted by A86,
both strings must be bounded using the same delimiter (either
single quotes for both strings, or double quotes for both
strings). For a match (EQ returns TRUE or NE returns FALSE), the
strings must be the same length, and every character must match
exactly.
8-9
An additional A86-exclusive feature is the = operator, which
returns TRUE if the characters of the strings differ only in the
bit masked by the value 020. Thus you may use = to compare a
macro parameter to a string containing nothing but letters. The
comparison will be TRUE whether the macro parameter is upper-case
or lower-case. No checking is made to detect non-letters, so if
you use = on strings containing non-letters, you may get some
false TRUE results. Also, = is accepted when it is applied to
non-strings as well-- the corresponding values are interpreted as
two-byte strings, with the 020 bits masked away before
comparison.
Attribute Operators/Specifiers
B,W,D,Q,T memory variable specifiers
Syntax: B operand Q operand
operand B operand Q
W operand T operand
operand W operand T
D operand
operand D
B, W, D, F, Q, and T convert the operand into a byte, word,
doubleword, far, quadword, and ten-byte variable, respectively.
The operand can be a constant, or a variable of the other type.
Examples:
ARRAY_PTR:
DB 100 DUP (?)
WVAR DW ?
MOV AL,ARRAY_PTR B ; load first byte of ARRAY_PTR array into AL
MOV AL,WVAR B ; load the low byte of WVAR into AL
MOV AX,W[01000] ; load AX with the memory word at loc. 01000
LDS BX,D[01000] ; load DS:BX with the doubleword at loc. 01000
JMP F[01000] ; jump far to the 4-byte location at 01000
FLD T[BX] ; load ten-byte number at [BX] to 87 stack
For compatibility with Intel/IBM assemblers, A86 accepts the more
verbose synonyms BYTE, WORD, DWORD, FAR, QWORD, and TBYTE for
B,W,D,F,Q,T, respectively.
SHORT and LONG Operators
Syntax: SHORT label
LONG label
8-10
The SHORT operator is used to specify that the label referenced
by a JMP instruction is within 127 bytes of the end of the
instruction. The LONG operator specifies the opposite: that the
label is not within 127 bytes. The appropriate operator can (and
sometimes must) be used if the label is forward referenced in the
instruction.
When a non-local label is forward referenced, the assembler
assumes that it will require two bytes to represent the relative
offset of the label (so the instruction including the opcode byte
will be three bytes). By correctly using the SHORT operator, you
can save a byte of code when you use a forward reference. If the
label is not within the specified range, an error will occur. The
following example illustrates the use of the SHORT operator.
JMP FWDLAB ; three byte instruction
JMP SHORT FWDLAB ; two byte instruction
JMP >L1 ; two byte instruction assumed for a local label
Because the assembler assumes that a forward reference local
label is SHORT, you may sometimes be forced to override this
assumption if the label is in fact not within 127 bytes of the
JMP. This is why LONG is provided:
JMP LONG >L9 ; three byte instruction
If you are bothered by this possibility, you can specify the +L
switch, which causes A86 to pessimistically generate the three
byte JMP for all forward references, unless specifically told not
to with SHORT.
NOTE that LONG will have effect only on the operand to an
unconditional JMP instruction; not to conditional jumps. This is
because the conditional jumps don't have 3-byte forms; the only
conditional jumps are short ones. If you run into this problem,
then chances are your code is getting out of control--time to
rearrange, or to break off some of the intervening code into
separate procedures. If you insist upon leaving the code intact,
you can replace the conditional jump with an "IF cond JMP".
OFFSET Operator
Syntax: OFFSET var-name
OFFSET is used to convert a variable into the constant pointer to
the variable. For example, if you have declared XX DW ?, and
you want to load SI with the pointer to the variable XX, you can
code: MOV SI,OFFSET XX. The simpler instruction MOV SI,XX moves
the variable contents of XX into SI, not the constant pointer to
XX.
8-11
NEAR Operator
Syntax: NEAR operand
NEAR converts the operand to have the type of a code label, as if
it were defined by appearing at the beginning of a program line
with a colon after it. NEAR is provided mainly for compatibility
with Intel/IBM assemblers.
Square Brackets Operator
Syntax: [operand]
Square brackets around an operand give the operand a memory
variable type. Square brackets are generally used to enclose the
names of base and index registers: BX, BP, SI, and DI. When the
size of the memory variable can be deduced from the context of
the expression, square brackets are also used to turn numeric
constants into memory variables. Examples:
MOV B[BX+50],047 ; move imm value 047 into mem byte at BX+50
MOV AL,[050] ; move byte at memory location 050 into AL
MOV AL,050 ; move immediate value 050 into AL
Colon Operator
Syntax: constant:operand
segreg:operand
seg_or_group_name:operand
The colon operator is used to attach a segment register value to
an operand. The segment register value appears to the left of
the colon; the rest of the operand appears to the right of the
colon.
There are three forms to the colon operator. The first form has
a constant as the segment register value. This form is used to
create an operand to a long (inter-segment) JMP or CALL
instruction. An example of this is the instruction JMP 0FFFF:0,
which jumps to the cold-boot reset location of the 86 processor.
The only context other than JMP or CALL in which this first form
is legal, is as the operand to a DD directive or an EQU
directive. The EQU case has a further restriction: the offset
(the part to the right of the colon) must have a value less than
256. This is because there simply isn't room in a symbol table
entry for a segment register value AND a 2-byte offset. I don't
think you will be hurt by this restriction, since references to
other segments are usually to jump tables at the beginning of
those segments.
8-12
The second form has a segment register name to the left of the
colon. This is the segment override form, provided for
compatibility with Intel/IBM assemblers. A86 will generate a
segment override byte when it sees this form, unless the operand
to the right of the colon already has a default segment register
that is the same as the given override.
I prefer the more explicit method of overrides, exclusive to A86:
simply place the segment register name before the instruction
mnemonic. For example, I prefer ES MOV AL,[BX] to MOV
AL,ES:[BX].
The third form has a segment or group name before the colon. This
form is ignored by A86; it is provided for compatibility with
Turbo C, which likes to include spurious DGROUP: overrides, to
satisfy MASM's ASSUME-checking.
ST Operator
ST is ignored whenever it occurs in an expression. It is
provided for compatibility with Intel and IBM assemblers. For
example, you can code FLD ST(0),ST(1), which will be taken by A86
as FLD 0,1.
TYPE Operator
Syntax: TYPE operand
The TYPE operator returns 1 if the operand is a byte variable; 2
if the operand is a word variable; 4 if the operand is a
doubleword variable; 8 if the operand is a quadword variable; 10
if the operand is a ten-byte variable; and the number of bytes
allocated by the structure if the operand is a structure name
(see STRUC in the next chapter).
A common usage of the TYPE operator is to represent the number of
bytes of a named structure. For example, if you have declared a
structure named LINE (as described in the next chapter) that
defines 82 bytes of storage, then two ways you might refer to the
value symbolically are as follows:
MOV CX,TYPE LINE ; loads the size of LINE into CX
DB TYPE LINE DUP ? ; allocates an area of memory for a LINE
THIS and $ Specifiers
THIS returns the value of the current location counter. It is
provided for compatibility with Intel/IBM assemblers. The dollar
sign $ is the more standard and familiar specifier for this
purpose; it is equivalent to THIS NEAR. THIS is typically used
with the BYTE and WORD specifiers to create alternate-typed
symbols at the same memory location:
8-13
BVAR EQU THIS BYTE
WVAR DW ?
I don't recommend the use of THIS. If you wish to retain Intel
compatibility, you can use the less verbose LABEL directive:
BVAR LABEL BYTE
WVAR DW ?
If you are not concerned with compatibility to lesser assemblers,
A86 offers a variety of less verbose forms. The most concise is
DB without an operand:
BVAR DB
WVAR DW ?
If this is too cryptic for you, there is always BVAR EQU B[$].
Operator Precedence
Consider the expression 1 + 2 * 3. When A86 sees this
expression, it could perform the multiplication first, giving an
answer of 1+6 = 7; or it could do the addition first, giving an
answer of 3*3 = 9. In fact, A86 does the multiplication first,
because A86 assigns a higher precedence to multiplication than it
does addition.
The following list specifies the order of precedence A86 assigns
to expression operators. All expressions are evaluated from left
to right following the precedence rules. You may override this
order of evaluation and precedence through the use of parentheses
( ). In the example above, you could override the precedence by
parenthesizing the addition: (1+2) * 3.
Some symbols that we have referred to as operators, are treated
by the assembler as operands having built-in values. These
include B, W, F, $, and ST. In a similar vein, a segment
override term (a segment register name followed by a colon) is
recorded when it is scanned, but not acted upon until the entire
containing expression is scanned and evaluated.
If two operators are adjacent, the rightmost operator must have
precedence; otherwise, parentheses must be used. For example,
the expression BIT ! 1 is illegal because the leftmost operator
BIT has the higher precedence of the two adjacent operators BIT
and "!". You can code BIT (! 1).
--Highest Precedence--
8-14
1. Parenthesized expressions
2. Period
3. OFFSET, SEG, TYPE, and PTR
4. HIGH, LOW, and BIT
5. Multiplication and division: *, /, MOD, SHR, SHL
6. Addition and subtraction: +,-
a. unary
b. binary
7. Relational: EQ, NE, LT, LE, GT, GE =
8. Logical NOT and !
9. Logical AND
10. Logical OR and XOR
11. Colon for long pointer, SHORT, LONG, and BY
12. DUP
--Lowest Precedence--