1324 lines
64 KiB
Prolog
1324 lines
64 KiB
Prolog
|
IBM Personal Computer Assembly
|
|||
|
Language Tutorial
|
|||
|
|
|||
|
Joshua Auerbach
|
|||
|
Yale University
|
|||
|
Yale Computer Center
|
|||
|
175 Whitney Avenue
|
|||
|
P. O. Box 2112
|
|||
|
New Haven, Connecticut 06520
|
|||
|
Installation Code YU
|
|||
|
Integrated Personal Computers Project
|
|||
|
Communications Group
|
|||
|
Communications and Data Base Division
|
|||
|
Session C316
|
|||
|
|
|||
|
This talk is for people who are just getting started with the PC MACRO
|
|||
|
Assembler. Maybe you are just contemplating doing some coding in assembler,
|
|||
|
maybe you have tried it with mixed success. If you are here to get aimed in
|
|||
|
the right direction, to get off to a good start with the assembler, then you
|
|||
|
have come for the right reason. I can't promise you'll get what you want, but
|
|||
|
I'll do my best.
|
|||
|
|
|||
|
On the other hand, if you have already turned out some working assembler
|
|||
|
code, then this talk is likely to be on the elementary side for you. If you
|
|||
|
want to review a few basics and have no where else pressing to go, then by all
|
|||
|
means stay.
|
|||
|
|
|||
|
Why Learn Assembler?
|
|||
|
____________________
|
|||
|
|
|||
|
The reasons for LEARNING assembler are not the same as the reasons for USING
|
|||
|
it in a particular application. But, we have to start with some of the reasons
|
|||
|
for using it and then I think the reasons for learning it will become clear.
|
|||
|
|
|||
|
First, let's dispose of a bad reason for using it. Don't use it just because
|
|||
|
you think it is going to execute faster. A particular sequence of ordinary
|
|||
|
bread-and-butter computations written in PASCAL, C, FORTRAN, or compiled BASIC
|
|||
|
can do the job just about as fast as the same algorithm coded in assembler. Of
|
|||
|
course, interpretive BASIC is slower, but if you have a BASIC application which
|
|||
|
runs too slow you probably want to try compiling it before you think too much
|
|||
|
about translating parts of it to another language.
|
|||
|
|
|||
|
On the other hand, high level languages do tend to isolate you from the
|
|||
|
machine. That is both their strength and their weakness. Usually, when
|
|||
|
implemented on a micro, a high level language provides an escape mechanism to
|
|||
|
the underlying operating system or to the bare machine. So, for example, BASIC
|
|||
|
has its PEEK and POKE. But, the route to the bare machine is often a
|
|||
|
circuitous one, leading to tricky programming which is hard to follow.
|
|||
|
|
|||
|
For those of us working on PC's connected to SHARE-class mainframes, we are
|
|||
|
generally concerned with three interfaces: the keyboard, the screen, and the
|
|||
|
communication line or lines. All three of these entities raise machine
|
|||
|
dependent issues which are imperfectly addressed by the underlying operating
|
|||
|
system or by high level languages.
|
|||
|
|
|||
|
Sometimes, the system or the language does too little for you. For example,
|
|||
|
with the asynch adapter, the system provides no interrupt handler, no buffer,
|
|||
|
and no flow control. The application is stuck with the responsibility for
|
|||
|
monitoring that port and not missing any characters, then deciding what to do
|
|||
|
with all errors. BASIC does a reasonable job on some of this, but that is only
|
|||
|
BASIC. Most other languages do less. Sometimes, the system may do too much
|
|||
|
for you. System support for the keyboard is an example. At the hardware
|
|||
|
level, all 83 keys on the keyboard send unique codes when they are pressed,
|
|||
|
held down, and released. But, someone has decided that certain keys, like Num
|
|||
|
Lock and Scroll Lock are going to do certain things before the application even
|
|||
|
sees them and can't therefore be used as ordinary keys.
|
|||
|
|
|||
|
Sometimes, the system does about the right amount of stuff but does it less
|
|||
|
efficiently then it should. System support for the screen is in this class.
|
|||
|
If you use only the official interface to the screen you sometimes slow your
|
|||
|
application down unacceptably. I said before, don't use assembler just to
|
|||
|
speed things up, but there I was talking about mainline code, which generally
|
|||
|
can't be speeded up much by assembler coding. A critical system interface is a
|
|||
|
different matter: sometimes we may have to use assembler to bypass a
|
|||
|
hopelessly inefficient implementation. We don't want to do this if we can
|
|||
|
avoid it, but sometimes we can't.
|
|||
|
|
|||
|
Assembly language code can overcome these deficiencies. In some cases, you
|
|||
|
can also overcome these deficiencies by judicious use of the escape valves
|
|||
|
which your high level language provides. In BASIC, you can PEEK and POKE and
|
|||
|
INP and OUT your way around a great many issues. In many other languages you
|
|||
|
can issue system calls and interrupts and usually manage, one way or other, to
|
|||
|
modify system memory. Writing handlers to take real-time hardware interrupts
|
|||
|
from the keyboard or asynch port, though, is still going to be a problem in
|
|||
|
most languages. Some languages claim to let you do it but I have yet to see an
|
|||
|
acceptably clean implementation done that way.
|
|||
|
|
|||
|
The real reason while assembler is better than "tricky POKEs" for writing
|
|||
|
machine-dependent code, though, is the same reason why PASCAL is better than
|
|||
|
assembler for writing a payroll package: it is easier to maintain. Let the
|
|||
|
high level language do what it does best, but recognize that there are some
|
|||
|
things which are best done in assembler code. The assembler, unlike the tricky
|
|||
|
POKE, can make judicious use of equates, macros, labels, and appropriately
|
|||
|
placed comments to show what is really going on in this machine-dependent realm
|
|||
|
where it thrives.
|
|||
|
|
|||
|
So, there are times when it becomes appropriate to write in assembler; given
|
|||
|
that, if you are a responsible programmer or manager, you will want to be
|
|||
|
"assembler-literate" so you can decide when assembler code should be written.
|
|||
|
|
|||
|
What do I mean by "assembler-literate?" I don't just mean understanding the
|
|||
|
8086 architecture; I think, even if you don't write much assembler code
|
|||
|
yourself, you ought to understand the actual process of turning out assembler
|
|||
|
code and the various ways to incorporate it into an application. You ought to
|
|||
|
be able to tell good assembler code from bad, and appropriate assembler code
|
|||
|
from inappropriate.
|
|||
|
|
|||
|
Steps to becoming ASSEMBLER-LITERATE
|
|||
|
____________________________________
|
|||
|
|
|||
|
1. Learn the 8086 architecture and most of the instruction set. Learn what
|
|||
|
you need to know and ignore what you don't. Reading: The 8086 Primer by
|
|||
|
Stephen Morse, published by Hayden. You need to read only two chapters, the
|
|||
|
one on machine organization and the one on the instruction set.
|
|||
|
|
|||
|
2. Learn about a few simple DOS function calls. Know what services the
|
|||
|
operating system provides. If appropriate, learn a little about other systems
|
|||
|
too. It will aid portability later on. Reading: appendices D and E of the PC
|
|||
|
DOS manual.
|
|||
|
|
|||
|
3. Learn enough about the MACRO assembler and the LINKer to write some
|
|||
|
simple things that really work. Here, too, the main thing is figuring out what
|
|||
|
you don't need to know. Whatever you do, don't study the sample programs
|
|||
|
distributed with the assembler unless you have nothing better!
|
|||
|
|
|||
|
4. At the same time as you are learning the assembler itself, you will need
|
|||
|
to learn a few tools and concepts to properly combine your assembler code with
|
|||
|
the other things you do. If you plan to call assembler subroutines from a high
|
|||
|
level language, you will need to study the interface notes provided in your
|
|||
|
language manual. Usually, this forms an appendix of some sort. If you plan to
|
|||
|
package your assembler routines as .COM programs you will need to learn to do
|
|||
|
this. You should also learn to use DEBUG.
|
|||
|
|
|||
|
5. Read the Technical Reference, but very selectively. The most important
|
|||
|
things to know are the header comments in the BIOS listing. Next, you will
|
|||
|
want to learn about the RS 232 port and maybe about the video adapters.
|
|||
|
|
|||
|
Notice that the key thing in all five phases is being selective. It is easy
|
|||
|
to conclude that there is too much to learn unless you can throw away what you
|
|||
|
don't need. Most of the rest of this talk is going to deal with this very
|
|||
|
important question of what you need and don't need to learn in each phase. In
|
|||
|
some cases, I will have to leave you to do almost all of the learning, in
|
|||
|
others, I will teach a few salient points, enough, I hope, to get you started.
|
|||
|
I hope you understand that all I can do in an hour is get you started on the
|
|||
|
way.
|
|||
|
|
|||
|
Phase 1: Learn the architecture and instruction set
|
|||
|
____________________________________________________
|
|||
|
|
|||
|
The Morse book might seem like a lot of book to buy for just two really
|
|||
|
important chapters; other books devote a lot more space to the instruction set
|
|||
|
and give you a big beautiful reference page on each instruction. And, some of
|
|||
|
the other things in the Morse book, although interesting, really aren't very
|
|||
|
vital and are covered too sketchily to be of any real help. The reason I like
|
|||
|
the Morse book is that you can just read it; it has a very conversational
|
|||
|
style, it is very lucid, it tells you what you really need to know, and a
|
|||
|
little bit more which is by way of background; because nothing really gets
|
|||
|
belabored to much, you can gracefully forget the things you don't use. And, I
|
|||
|
very much recommend READING Morse rather than studying it. Get the big picture
|
|||
|
at this point.
|
|||
|
|
|||
|
Now, you want to concentrate on those things which are worth fixing in
|
|||
|
memory. After you read Morse, you should relate what you have learned to this
|
|||
|
outline.
|
|||
|
|
|||
|
1. You want to fix in your mind the idea of the four segment registers CODE,
|
|||
|
DATA, STACK, and EXTRA. This part is pretty easy to grasp. The 8086 and the
|
|||
|
8088 use 20 bit addresses for memory, meaning that they can address up to 1
|
|||
|
megabyte of memory. But, the registers and the address fields in all the
|
|||
|
instructions are no more that 16 bits long. So, how to address all of that
|
|||
|
memory? Their solution is to put together two 16 bit quantities like this:
|
|||
|
|
|||
|
calculation SSSS0 ---- value in the relevant segment register SHL 4
|
|||
|
depicted in AAAA ---- apparent address from register or instruction
|
|||
|
hexadecimal --------
|
|||
|
RRRRR ---- real address placed on address bus
|
|||
|
|
|||
|
In other words, any time memory is accessed, your program will supply a
|
|||
|
sixteen bit address. Another sixteen bit address is acquired from a segment
|
|||
|
register, left shifted four bits (one nibble) and added to it to form the real
|
|||
|
address. You can control the values in the segment registers and thus access
|
|||
|
any part of memory you want. But the segment registers are specialized: one
|
|||
|
for code, one for most data accesses, one for the stack (which we'll mention
|
|||
|
again) and one "extra" one for additional data accesses.
|
|||
|
|
|||
|
Most people, when they first learn about this addressing scheme become
|
|||
|
obsessed with converting everything to real 20 bit addresses. After a while,
|
|||
|
though, you get use to thinking in segment/offset form. You tend to get your
|
|||
|
segment registers set up at the beginning of the program, change them as little
|
|||
|
as possible, and think just in terms of symbolic locations in your program, as
|
|||
|
with any assembly language.
|
|||
|
|
|||
|
EXAMPLE:
|
|||
|
MOV AX,DATASEG
|
|||
|
MOV DS,AX ;Set value of Data segment
|
|||
|
ASSUME DS:DATASEG ;Tell assembler DS is usable
|
|||
|
.......
|
|||
|
MOV AX,PLACE ;Access storage symbolically by 16 bit address
|
|||
|
|
|||
|
In the above example, the assembler knows that no special issues are involved
|
|||
|
because the machine generally uses the DS register to complete a normal data
|
|||
|
reference.
|
|||
|
|
|||
|
If you had used ES instead of DS in the above example, the assembler would
|
|||
|
have known what to do, also. In front of the MOV instruction which accessed
|
|||
|
the location PLACE, it would have placed the ES segment prefix. This would
|
|||
|
tell the machine that ES should be used, instead of DS, to complete the
|
|||
|
address.
|
|||
|
|
|||
|
Some conventions make it especially easy to forget about segment registers.
|
|||
|
For example, any program of the COM type gets control with all four segment
|
|||
|
registers containing the same value. This program executes in a simplified 64K
|
|||
|
address space. You can go outside this address space if you want but you don't
|
|||
|
have to.
|
|||
|
|
|||
|
2. You will want to learn what other registers are available and learn their
|
|||
|
personalities:
|
|||
|
|
|||
|
AX and DX are general purpose registers. They become special only when
|
|||
|
accessing machine and system interfaces.
|
|||
|
|
|||
|
CX is a general purpose register which is slightly specialized for counting.
|
|||
|
|
|||
|
BX is a general purpose register which is slightly specialized for forming
|
|||
|
base-displacement addresses.
|
|||
|
|
|||
|
AX-DX can be divided in half, forming AH, AL, BH, BL, CH, CL, DH, DL.
|
|||
|
|
|||
|
SI and DI are strictly 16 bit. They can be used to form indexed addresses
|
|||
|
(like BX) and they are also used to point to strings.
|
|||
|
|
|||
|
SP is hardly ever manipulated. It is there to provide a stack.
|
|||
|
|
|||
|
BP is a manipulable cousin to SP. Use it to access data which has been
|
|||
|
pushed onto the stack.
|
|||
|
|
|||
|
Most sixteen bit operations are legal (even if unusual) when performed in SI,
|
|||
|
DI, SP, or BP.
|
|||
|
|
|||
|
3. You will want to learn the classifications of operations available
|
|||
|
WITHOUT getting hung up in the details of how 8086 opcodes are constructed.
|
|||
|
|
|||
|
8086 opcodes are complex. Fortunately, the assembler opcodes used to
|
|||
|
assemble them are simple. When you read a book like Morse, you will learn some
|
|||
|
things which are worth knowing but NOT worth dwelling on.
|
|||
|
|
|||
|
a. 8086 and 8088 instructions can be broken up into subfields and bits with
|
|||
|
names like R/M, MOD, S and W. These parts of the instruction modify the basic
|
|||
|
operation in such ways as whether it is 8 bit or 16 bit, if 16 bit, whether all
|
|||
|
16 bits of the data are given, whether the instruction is register to register,
|
|||
|
register to memory, or memory to register, for operands which are registers,
|
|||
|
which register, for operands which are memory, what base and index registers
|
|||
|
should be used in finding the data.
|
|||
|
|
|||
|
b. Also, some instructions are actually represented by several different
|
|||
|
machine opcodes depending on whether they deal with immediate data or not, or
|
|||
|
on other issues, and there are some expedited forms which assume that one of
|
|||
|
the arguments is the most commonly used operand, like AX in the case of
|
|||
|
arithmetic.
|
|||
|
|
|||
|
There is no point in memorizing any of this detail; just distill the bottom
|
|||
|
line, which is, what kinds of operand combinations EXIST in the instruction set
|
|||
|
and what kinds don't. If you ask the assembler to ADD two things and the two
|
|||
|
things are things for which there is a legal ADD instruction somewhere in the
|
|||
|
instruction set, the assembler will find the right instruction and fill in all
|
|||
|
the modifier fields for you.
|
|||
|
|
|||
|
I guess if you memorized all the opcode construction rules you might have a
|
|||
|
crack at being able to disassemble hex dumps by eye, like you may have learned
|
|||
|
to do somewhat with 370 assembler. I submit to you that this feat, if ever
|
|||
|
mastered by anyone, would be in the same class as playing the "Minute Waltz" in
|
|||
|
a minute; a curiosity only. Here is the basic matrix you should remember:
|
|||
|
|
|||
|
Two operands: One operand:
|
|||
|
R <-- M R
|
|||
|
M <-- R M
|
|||
|
R <-- R S *
|
|||
|
R|M <-- I
|
|||
|
R|M <-- S *
|
|||
|
S <-- R|M *
|
|||
|
|
|||
|
* -- data moving instructions (MOV, PUSH, POP) only
|
|||
|
S -- segment register (CS, DS, ES, SS)
|
|||
|
R -- ordinary register (AX, BX, CX, DX, SI, DI, BP, SP,
|
|||
|
AH, AL, BH, BL, CH, CL, DH, DL)
|
|||
|
M -- one of the following
|
|||
|
pure address
|
|||
|
[BX]+offset
|
|||
|
[BP]+offset
|
|||
|
any of the above indexed by SI
|
|||
|
any of the first three indexed by DI
|
|||
|
|
|||
|
4. Of course, you want to learn the operations themselves. As I've
|
|||
|
suggested, you want to learn the op codes as the assembler presents them, not
|
|||
|
as the CPU machine language presents them. So, even though there are many MOV
|
|||
|
op codes you don't need to learn them. Basically, here is the instruction set:
|
|||
|
|
|||
|
a. Ordinary two operand instructions. These instructions perform an
|
|||
|
operation and leave the result in place of one of the operands. They are:
|
|||
|
|
|||
|
1) ADD and ADC -- addition, with or without including a carry from a previous
|
|||
|
addition 2) SUB and SBB -- subtraction, with or without including a borrow from
|
|||
|
a previous subtraction 3) CMP -- compare. It is useful to think of this as a
|
|||
|
subtraction with the answer being thrown away and neither operand actually
|
|||
|
changed 4) AND, OR, XOR -- typical boolean operations 5) TEST -- like an AND,
|
|||
|
except the answer is thrown away and neither operand is changed. 6) MOV --
|
|||
|
move data from source to target 7) LDS, LES, LEA -- some specialized forms of
|
|||
|
MOV with side effects
|
|||
|
|
|||
|
b. Ordinary one operand instructions. These can take any of the operand
|
|||
|
forms described above. Usually, the perform the operation and leave the result
|
|||
|
in the stated place:
|
|||
|
|
|||
|
1) INC -- increment contents
|
|||
|
2) DEC -- decrement contents
|
|||
|
3) NEG -- twos complement
|
|||
|
4) NOT -- ones complement
|
|||
|
5) PUSH -- value goes on stack (operand location itself unchanged)
|
|||
|
6) POP -- value taken from stack, replaces current value
|
|||
|
|
|||
|
c. Now you touch on some instructions which do not follow the general
|
|||
|
operand rules but which require the use of certain registers. The important
|
|||
|
ones are:
|
|||
|
|
|||
|
1) The multiply and divide instructions 2) The "adjust" instructions which
|
|||
|
help in performing arithmetic on ASCII or packed decimal data 3) The shift and
|
|||
|
rotate instructions. These have a restriction on the second operand: it must
|
|||
|
either be the immediate value 1 or the contents of the CL register. 4) IN and
|
|||
|
OUT which send or receive data from one of the 1024 hardware ports. 5) CBW and
|
|||
|
CWD -- convert byte to word or word to doubleword by sign extension
|
|||
|
|
|||
|
d. Flow of control instructions. These deserve study in themselves and we
|
|||
|
will discuss them a little more. They include:
|
|||
|
|
|||
|
1) CALL, RET -- call and return
|
|||
|
2) INT, IRET -- interrupt and return-from-interrupt
|
|||
|
3) JMP -- jump or "branch"
|
|||
|
4) LOOP, LOOPNZ, LOOPZ -- special (and useful) instructions which
|
|||
|
implement a counted loop similar to the 370 BCT instruction
|
|||
|
5) various conditional jump instructions
|
|||
|
|
|||
|
e. String instructions. These implement a limited storage-to-storage
|
|||
|
instruction subset and are quite powerful. All of them have the property that:
|
|||
|
|
|||
|
1) The source of data is described by the combination DS and SI.
|
|||
|
|
|||
|
2) The destination of data is described by the combination ES and DI.
|
|||
|
|
|||
|
3) As part of the operation, the SI and/or DI register(s) is(are) incremented
|
|||
|
or decremented so the operation can be repeated.
|
|||
|
|
|||
|
They include:
|
|||
|
|
|||
|
1) CMPSB/CMPSW -- compare byte or word
|
|||
|
2) LODSB/LODSW -- load byte or word into AL or AX
|
|||
|
3) STOSB/STOSW -- store byte or word from AL or AX
|
|||
|
4) MOVSB/MOVSW -- move byte or word
|
|||
|
5) SCASB/SCASW -- compare byte or word with contents of AL or AX
|
|||
|
6) REP/REPE/REPNE -- a prefix which can be combined with any of
|
|||
|
the above instructions to make them execute repeatedly across a
|
|||
|
string of data whose length is held in CX.
|
|||
|
|
|||
|
f. Flag instructions: CLI, STI, CLD, STD, CLC, STC. These can set or
|
|||
|
clear the interrupt (enabled) direction (for string operations) or
|
|||
|
carry flags.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
The addressing summary and the instruction summary given above masks a lot of
|
|||
|
annoying little exceptions. For example, you can't POP CS, and although the R
|
|||
|
<-- M form of LES is legal, the M <-- R form isn't etc. etc. My advice is:
|
|||
|
|
|||
|
a. Go for the general rules
|
|||
|
|
|||
|
b. Don't try to memorize the exceptions
|
|||
|
|
|||
|
c. Rely on common sense and the assembler to teach you about
|
|||
|
exceptions over time. A lot of the exceptions cover things you
|
|||
|
wouldn't want to do anyway.
|
|||
|
|
|||
|
5. A few instructions are rich enough and useful enough to warrent careful
|
|||
|
study. Here are a few final study guidelines:
|
|||
|
|
|||
|
a. It is well worth the time learning to use the string instruction set
|
|||
|
effectively. Among the most useful are:
|
|||
|
|
|||
|
REP MOVSB ;moves a string
|
|||
|
REP STOSB ;initializes memory
|
|||
|
REPNE SCASB ;look up occurance of character in string
|
|||
|
REPE CMPSB ;compare two strings
|
|||
|
|
|||
|
b. Similarly, if you have never written for a stack machine before, you will
|
|||
|
need to exercise PUSH and POP and get very comfortable with them because they
|
|||
|
are going to be good friends. If you are used to the 370, with lots of general
|
|||
|
purpose registers, you may find yourself feeling cramped at first, with many
|
|||
|
fewer registers and many instructions having register restrictions. But, you
|
|||
|
have a hidden ally: you need a register and you don't want to throw away
|
|||
|
what's in it? Just PUSH it, and when you are done, POP it back. This can lead
|
|||
|
to abuse. Never have more than two "expedient" PUSHes in effect and never
|
|||
|
leave something PUSHed across a major header comment or for more than 15
|
|||
|
instructions or so. An exception is the saving and restoring of registers at
|
|||
|
entrance to and exit from a subroutine; here, if the subroutine is long, you
|
|||
|
should probably PUSH everything which the caller may need saved, whether you
|
|||
|
will use the register or not, and POP it in reverse order at the end. Be aware
|
|||
|
that CALL and INT push return address information on the stack and RET and IRET
|
|||
|
pop it off. It is a good idea to become familiar with the structure of the
|
|||
|
stack.
|
|||
|
|
|||
|
c. In practice, to invoke system services you will use the INT instruction.
|
|||
|
It is quite possible to use this instruction effectively in a cookbook fashion
|
|||
|
without knowing precisely how it works.
|
|||
|
|
|||
|
d. The transfer of control instructions (CALL, RET, JMP) deserve careful
|
|||
|
study to avoid confusion. You will learn that these can be classified as
|
|||
|
follows:
|
|||
|
|
|||
|
1) all three have the capability of being either NEAR (CS register unchanged)
|
|||
|
or FAR (CS register changed)
|
|||
|
|
|||
|
2) JMPs and CALLs can be DIRECT (target is assembled into instruction) or
|
|||
|
INDIRECT (target fetched from memory or register)
|
|||
|
|
|||
|
3) if NEAR and DIRECT, a JMP can be SHORT (less than 128 bytes away) or LONG
|
|||
|
|
|||
|
In general, the third issue is not worth worrying about. On a forward jump
|
|||
|
which is clearly VERY short, you can tell the assembler it is short and save
|
|||
|
one byte of code:
|
|||
|
|
|||
|
JMP SHORT CLOSEBY
|
|||
|
|
|||
|
On a backward jump, the assembler can figure it out for you. On a forward
|
|||
|
jump of dubious length, let the assembler default to a LONG form; at worst you
|
|||
|
waste one byte.
|
|||
|
|
|||
|
Also leave the assembler to worry about how the target address is to be
|
|||
|
represented, in absolute form or relative form.
|
|||
|
|
|||
|
e. The conditional jump set is rather confusing when studied apart from the
|
|||
|
assembler, but you do need to get a feeling for it. The interactions of the
|
|||
|
sign, carry, and overflow flags can get your mind stuttering pretty fast if you
|
|||
|
worry about it too much. What is boils down to, though, is
|
|||
|
|
|||
|
JZ means what it says
|
|||
|
JNZ means what it says
|
|||
|
JG reater this means "if the SIGNED difference is positive"
|
|||
|
JA bove this means "if the UNSIGNED difference is positive"
|
|||
|
JL ess this means "if the SIGNED difference is negative"
|
|||
|
JB elow this means "if the UNSIGNED difference is negative"
|
|||
|
JC arry assembles the same as JB; it's an aesthetic choice
|
|||
|
|
|||
|
You should understand that all conditional jumps are inherently DIRECT, NEAR,
|
|||
|
and "short"; the "short" part means that they can't go more than 128 bytes in
|
|||
|
either direction. Again, this is something you could easily imagine to be more
|
|||
|
of a problem than it is. I follow this simple approach:
|
|||
|
|
|||
|
1) When taking an abnormal exit from a block of code, I always use an
|
|||
|
unconditional jump. Who knows how far you are going to end up jumping by the
|
|||
|
time the program is finished. For example, I wouldn't code this:
|
|||
|
|
|||
|
TEST AL,IDIBIT ;Is the idiot bit on?
|
|||
|
JNZ OYVEY ;Yes. Go to general cleanup
|
|||
|
|
|||
|
Rather, I would probably code this:
|
|||
|
|
|||
|
TEST AL,IDIBIT ;Is the idiot bit on?
|
|||
|
JZ NOIDIOCY ;No. I am saved.
|
|||
|
JMP OYVEY ;Yes. What can we say...
|
|||
|
NOIDIOCY:
|
|||
|
|
|||
|
The latter, of course, is a jump around a jump. Some would say it is evil,
|
|||
|
but I submit it is hard to avoid in this language.
|
|||
|
|
|||
|
2) Otherwise, within a block of code, I use conditional jumps freely. If the
|
|||
|
block eventually grows so long that the assembler starts complaining that my
|
|||
|
conditional jumps are too long I:
|
|||
|
|
|||
|
a) consider reorganizing the block but
|
|||
|
|
|||
|
b) also consider changing some conditional jumps to their opposite and use
|
|||
|
the "jump around a jump" approach as shown above.
|
|||
|
|
|||
|
Enough about specific instructions!
|
|||
|
|
|||
|
6. Finally, in order to use the assembler effectively, you need to know the
|
|||
|
default rules for which segment registers are used to complete addresses in
|
|||
|
which situations.
|
|||
|
|
|||
|
a. CS is used to complete an address which is the target of a NEAR DIRECT
|
|||
|
jump. On an NEAR INDIRECT jump, DS is used to fetch the address from memory
|
|||
|
but then CS is used to complete the address thus fetched. On FAR jumps, of
|
|||
|
course, CS is itself altered. The instruction counter is always implicitly
|
|||
|
pointing in the code segment.
|
|||
|
|
|||
|
b. SS is used to complete an address if BP is used in its formation.
|
|||
|
Otherwise, DS is always used to complete a data address.
|
|||
|
|
|||
|
c. On the string instructions, the target is always formed from ES and DI.
|
|||
|
The source is normally formed from DS and SI. If there is a segment prefix, it
|
|||
|
overrides the source not the target.
|
|||
|
|
|||
|
Learning about DOS
|
|||
|
__________________
|
|||
|
|
|||
|
I think the best way to learn about DOS internals is to read the technical
|
|||
|
appendices in the manual. These are not as complete as we might wish, but they
|
|||
|
really aren't bad; I certainly have learned a lot from them. What you don't
|
|||
|
learn from them you might eventually learn via judicious disassembly of parts
|
|||
|
of DOS, but that shouldn't really be necessary.
|
|||
|
|
|||
|
From reading the technical appendices, you learn that interrupts 20H through
|
|||
|
27H are used to communicate with DOS. Mostly, you will use interrupt 21H, the
|
|||
|
DOS function manager.
|
|||
|
|
|||
|
The function manager implements a great many services. You request the
|
|||
|
individual services by means of a function code in the AH register. For
|
|||
|
example, by putting a nine in the AH register and issuing interrupt 21H you
|
|||
|
tell DOS to print a message on the console screen.
|
|||
|
|
|||
|
Usually, but by no means always, the DX register is used to pass data for the
|
|||
|
service being requested. For example, on the print message service just
|
|||
|
mentioned, you would put the 16 bit address of the message in the DX register.
|
|||
|
The DS register is also implicitly part of this argument, in keeping with the
|
|||
|
universal segmentation rules.
|
|||
|
|
|||
|
In understanding DOS functions, it is useful to understand some history and
|
|||
|
also some of the philosophy of MS-DOS with regard to portability. Generally,
|
|||
|
you will find, once you read the technical information on DOS and also the IBM
|
|||
|
technical reference, you will know more than one way to do almost anything.
|
|||
|
Which is best? For example, to do asynch adapter I/O, you can use the DOS
|
|||
|
calls (pretty incomplete), you can use BIOS, or you can go directly to the
|
|||
|
hardware. The same thing is true for most of the other primitive I/O (keyboard
|
|||
|
or screen) although DOS is more likely to give you added value in these areas.
|
|||
|
When it comes to file I/O, DOS itself offers more than one interface. For
|
|||
|
example, there are four calls which read data from a file.
|
|||
|
|
|||
|
The way to decide rationally among these alternatives is by understanding the
|
|||
|
tradeoffs of functionality versus portability. Three kinds of portability need
|
|||
|
to be considered: machine portability, operating system portability (for
|
|||
|
example, the ability to assemble and run code under CP/M 86) and DOS version
|
|||
|
portability (the ability for a program to run under older versions of DOS>.
|
|||
|
|
|||
|
Most of the functions originally offered in DOS 1.0 were direct descendents
|
|||
|
of CP/M functions; there is even a compatibility interface so that programs
|
|||
|
which have been translated instruction for instruction from 8080 assembler to
|
|||
|
8086 assembler might have a reasonable chance of running if they use only the
|
|||
|
core CP/M function set. Among the most generally useful in this original
|
|||
|
compatibility set are:
|
|||
|
|
|||
|
09 -- print a full message on the screen
|
|||
|
0A -- get a console input line with full DOS editing
|
|||
|
0F -- open a file
|
|||
|
10 -- close a file (really needed only when writing)
|
|||
|
11 -- find first file matching a pattern
|
|||
|
12 -- find next file matching a pattern
|
|||
|
13 -- erase a file
|
|||
|
16 -- create a file
|
|||
|
17 -- rename a file
|
|||
|
1A -- set disk transfer address
|
|||
|
|
|||
|
The next set provide no function above what you can get with BIOS calls or
|
|||
|
more specialized DOS calls. However, they are preferable to BIOS calls when
|
|||
|
portability is an issue.
|
|||
|
|
|||
|
00 -- terminate execution
|
|||
|
01 -- read keyboard character
|
|||
|
02 -- write screen character
|
|||
|
03 -- read COM port character
|
|||
|
04 -- write COM port character
|
|||
|
05 -- print a character
|
|||
|
06 -- read keyboard or write screen with no editing
|
|||
|
|
|||
|
The standard file I/O calls are inferior to the specialized DOS calls but
|
|||
|
have the advantage of making the program easier to port to CP/M style systems.
|
|||
|
Thus they are worth mentioning:
|
|||
|
|
|||
|
14 -- sequential read from file
|
|||
|
15 -- sequential write to file
|
|||
|
21 -- random read from file
|
|||
|
22 -- random write to file
|
|||
|
23 -- determine file size
|
|||
|
24 -- set random record
|
|||
|
|
|||
|
In addition to the CP/M compatible services, DOS also offers some specialized
|
|||
|
services which have been available in all releases of DOS. These include:
|
|||
|
|
|||
|
27 -- multi-record random read.
|
|||
|
28 -- multi-record random write.
|
|||
|
29 -- parse filename
|
|||
|
2A-2D -- get and set date and time
|
|||
|
|
|||
|
All of the calls mentioned above which have anything to do with files make
|
|||
|
use of a data area called the "FILE CONTROL BLOCK" (FCB). The FCB is anywhere
|
|||
|
from 33 to 37 bytes long depending on how it is used. You are responsible for
|
|||
|
creating an FCB and filling in the first 12 bytes, which contain a drive code,
|
|||
|
a file name, and an extension.
|
|||
|
|
|||
|
When you open the FCB, the system fills in the next 20 bytes, which includes
|
|||
|
a logical record length. The initial lrecl is always 128 bytes, to achieve
|
|||
|
CP/M compatibility. The system also provides other useful information such as
|
|||
|
the file size.
|
|||
|
|
|||
|
After you have opened the FCB, you can change the logical record length. If
|
|||
|
you do this, your program is no longer CP/M compatible, but that doesn't make
|
|||
|
it a bad thing to do. DOS documentation suggests you use a logical record
|
|||
|
length of one for maximum flexibility. This is usually a good recommendation.
|
|||
|
|
|||
|
To perform actual I/O to a file, you eventually need to fill in byte 33 or
|
|||
|
possibly bytes 34-37 of the FCB. Here you supply information about the record
|
|||
|
you are interested in reading or writing. For the most part, this part of the
|
|||
|
interface is compatible with CP/M.
|
|||
|
|
|||
|
In general, you do not need to (and should not) modify other parts of the
|
|||
|
FCB.
|
|||
|
|
|||
|
The FCB is pretty well described in appendix E of the DOS manual. Beginning
|
|||
|
with DOS 2.0, there is a whole new system of calls for managing files which
|
|||
|
don't require that you build an FCB at all. These calls are quite incompatible
|
|||
|
with CP/M and also mean that your program cannot run under older releases of
|
|||
|
DOS. However, these calls are very nice and easy to use. They have these
|
|||
|
characteristics
|
|||
|
|
|||
|
1. To open, create, delete, or rename a file, you need only a character
|
|||
|
string representing its name.
|
|||
|
|
|||
|
2. The open and create calls return a 16 bit value which is simply placed in
|
|||
|
the BX register on subsequent calls to refer to the file.
|
|||
|
|
|||
|
3. There is not a separate call required to specify the data buffer.
|
|||
|
|
|||
|
4. Any number of bytes can be transfered on a single call; no data area must
|
|||
|
be manipulated to do this.
|
|||
|
|
|||
|
The "new" DOS calls also include comprehensive functions to manipulate the
|
|||
|
new chained directory structure and to allocate and free memory.
|
|||
|
|
|||
|
Learning the assembler
|
|||
|
______________________
|
|||
|
|
|||
|
It is my feeling that many people can teach themselves to use the assembler
|
|||
|
by reading the MACRO Assembler manual if:
|
|||
|
|
|||
|
1. You have read and understood a book like Morse and thus have a feeling
|
|||
|
for the instruction set
|
|||
|
|
|||
|
2. You know something about DOS services and so can communicate with the
|
|||
|
keyboard and screen and do something marginally useful with files. In the
|
|||
|
absence of this kind of knowledge, you can't write meaningful practice programs
|
|||
|
and so will not progress.
|
|||
|
|
|||
|
3. You have access to some good examples (the ones supplied with the
|
|||
|
assembler are not good, in my opinion. I will try to supply you with some more
|
|||
|
relevant ones.
|
|||
|
|
|||
|
4. You ignore the things which are most confusing and least useful. Some of
|
|||
|
the most confusing aspects of the assembler include the facilities combining
|
|||
|
segments. But, you can avoid using all but the simplest of these facilities in
|
|||
|
many cases, even while writing quite substantial applications.
|
|||
|
|
|||
|
5. The easiest kind of assembler program to write is a COM program. They
|
|||
|
might seem harder, at first, then EXE programs because there is an extra step
|
|||
|
involved in creating the executable file, but COM programs are structurally
|
|||
|
very much simpler.
|
|||
|
|
|||
|
At this point, it is necessary to talk about COM programs and EXE programs.
|
|||
|
As you probably know, DOS supports two kinds of executable files. EXE programs
|
|||
|
are much more general, can contain many segments, and are generally built by
|
|||
|
compilers and sometimes by the assembler. If you follow the lead given by the
|
|||
|
samples distributed with the assembler, you will end up with EXE programs. A
|
|||
|
COM program, in contrast, always contains just one segment, and receives
|
|||
|
control with all four segment registers containing the same value. A COM
|
|||
|
program, thus, executes in a simplified environment, a 64K address space. You
|
|||
|
can go outside this address space simply by temporarily changing one segment
|
|||
|
register, but you don't have to, and that is the thing which makes COM programs
|
|||
|
nice and simple. Let's look at a very simple one.
|
|||
|
|
|||
|
The classic text on writing programs for the C language says that the first
|
|||
|
thing you should write is a program which says
|
|||
|
|
|||
|
HELLO, WORLD.
|
|||
|
|
|||
|
when invoked. What's sauce for C is sauce for assembler, so let's start with
|
|||
|
a HELLO program of our own. My first presentation of this will be bare bones,
|
|||
|
not stylistically complete, but just an illustration of what an assembler
|
|||
|
program absolutely has to have:
|
|||
|
|
|||
|
HELLO SEGMENT ;Set up HELLO code and data section
|
|||
|
ASSUME CS:HELLO,DS:HELLO ;Tell assembler about conditions at entry
|
|||
|
ORG 100H ;A .COM program begins with 100H byte prefix
|
|||
|
MAIN: JMP BEGIN ;Control must start here
|
|||
|
MSG DB 'Hello, world.$' ;But it is generally useful to put data first
|
|||
|
BEGIN: MOV DX,OFFSET MSG ;Let DX --> message.
|
|||
|
MOV AH,9 ;Set DOS function code for printing a message
|
|||
|
INT 21H ;Invoke DOS
|
|||
|
RET ;Return to system
|
|||
|
HELLO ENDS ;End of code and data section
|
|||
|
END MAIN ;Terminate assembler and specify entry point
|
|||
|
|
|||
|
First, let's attend to some obvious points. The macro assembler uses the
|
|||
|
general form
|
|||
|
|
|||
|
name opcode operands
|
|||
|
|
|||
|
Unlike the 370 assembler, though, comments are NOT set off from operands by
|
|||
|
blanks. The syntax uses blanks as delimiters within the operand field (see
|
|||
|
line 6 of the example) and so all comments must be set off by semi-colons.
|
|||
|
Line comments are frequently set off with a semi-colon in column 1. I use this
|
|||
|
approach for block comments too, although there is a COMMENT statement which
|
|||
|
can be used to introduce a block comment.
|
|||
|
|
|||
|
Being an old 370 type, I like to see assembler code in upper case, although
|
|||
|
my comments are mixed case. Actually, the assembler is quite happy with mixed
|
|||
|
case anywhere.
|
|||
|
|
|||
|
As with any assembler, the core of the opcode set consists of opcodes which
|
|||
|
generate machine instructions but there are also opcodes which generate data
|
|||
|
and ones which function as instructions to the assembler itself, sometimes
|
|||
|
called pseudo-ops. In the example, there are five lines which generate machine
|
|||
|
code (JMP, MOV, MOV, INT, RET), one line which generates data (DB) and five
|
|||
|
pseudo-ops (SEGMENT, ASSUME, ORG, ENDS, and END).
|
|||
|
|
|||
|
We will discuss all of them.
|
|||
|
|
|||
|
Now, about labels. You will see that some labels in the example end in a
|
|||
|
colon and some don't. This is just a bit confusing at first, but no real
|
|||
|
mystery. If a label is attached to a piece of code (as opposed to data), then
|
|||
|
the assembler needs to know what to do when you JMP to or CALL that label. By
|
|||
|
convention, if the label ends in a colon, the assembler will use the NEAR form
|
|||
|
of JMP or CALL. If the label does not end in a colon, it will use the FAR
|
|||
|
form. In practice, you will always use the colon on any label you are jumping
|
|||
|
to inside your program because such jumps are always NEAR; there is no reason
|
|||
|
to use a FAR jump within a single code section. I mention this, though,
|
|||
|
because leaving off the colon isn't usually trapped as a syntax error, it will
|
|||
|
generally cause something more abstruse to go wrong.
|
|||
|
|
|||
|
On the other hand, a label attached to a piece of data or a pseudo-op never
|
|||
|
ends in a colon.
|
|||
|
|
|||
|
Machine instructions will generally take zero, one or two operands. Where
|
|||
|
there are two operands, the one which receives the result goes on the left as
|
|||
|
in 370 assembler.
|
|||
|
|
|||
|
I tried to explain this before, now maybe it will be even clearer: there are
|
|||
|
many more 8086 machine opcodes then there are assembler opcodes to represent
|
|||
|
them. For example, there are five kinds of JMP, four kinds of CALL, two kinds
|
|||
|
of RET, and at least five kinds of MOV depending on how you count them. The
|
|||
|
macro assembler makes a lot of decisions for you based on the form taken by the
|
|||
|
operands or on attributes assigned to symbols elsewhere in your program. In
|
|||
|
the example above, the assembler will generate the NEAR DIRECT form of JMP
|
|||
|
because the target label BEGIN labels a piece of code instead of a piece of
|
|||
|
data (this makes the JMP DIRECT) and ends in a colon (this makes the JMP NEAR).
|
|||
|
The assembler will generate the immediate forms of MOV because the form OFFSET
|
|||
|
MSG refers to immediate data and because 9 is a constant. The assembler will
|
|||
|
generate the NEAR form of RET because that is the default and you have not told
|
|||
|
it otherwise.
|
|||
|
|
|||
|
The DB (define byte) pseudo-op is an easy one: it is used to put one or more
|
|||
|
bytes of data into storage. There is also a DW (define word) pseudo-op and a
|
|||
|
DD (define doubleword) pseudo-op; in the PC MACRO assembler, the fact that a
|
|||
|
label refers to a byte of storage, a word of storage, or a doubleword of
|
|||
|
storage can be very significant in ways which we will see presently.
|
|||
|
|
|||
|
About that OFFSET operator, I guess this is the best way to make the point
|
|||
|
about how the assembler decides what instruction to assemble: an analogy with
|
|||
|
370 assembler:
|
|||
|
|
|||
|
PLACE DC ......
|
|||
|
...
|
|||
|
LA R1,PLACE
|
|||
|
L R1,PLACE
|
|||
|
|
|||
|
In 370 assembler, the first instruction puts the address of label PLACE in
|
|||
|
register 1, the second instruction puts the contents of storage at label PLACE
|
|||
|
in register 1. Notice that two different opcodes are used. In the PC
|
|||
|
assembler, the analogous instructions would be:
|
|||
|
|
|||
|
PLACE DW ......
|
|||
|
...
|
|||
|
MOV DX,OFFSET PLACE
|
|||
|
MOV DX,PLACE
|
|||
|
|
|||
|
If PLACE is the label of a word of storage, then the second instruction will
|
|||
|
be understood as a desire to fetch that data into DX. If X is a label, then
|
|||
|
"OFFSET X" means "the ordinary number which represents X's offset from the
|
|||
|
start of the segment." And, if the assembler sees an ordinary number, as
|
|||
|
opposed to a label, it uses the instruction which is equivalent to LA.
|
|||
|
|
|||
|
If PLACE were the label of a DB pseudo-op, instead of a DW, then
|
|||
|
|
|||
|
MOV DX,PLACE
|
|||
|
|
|||
|
would be illegal. The assembler worries about length attributes of its
|
|||
|
operands.
|
|||
|
|
|||
|
Next, numbers and constants in general. The assembler's default radix is
|
|||
|
decimal. You can change this, but I don't recommend it. If you want to
|
|||
|
represent numbers in other forms of notation such as hex or bit, you generally
|
|||
|
use a trailing letter. For example,
|
|||
|
|
|||
|
21H
|
|||
|
is hexidecimal 21,
|
|||
|
00010000B
|
|||
|
is the eight bit binary number pictured.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
The next elements we should point to are the SEGMENT...ENDS pair and the END
|
|||
|
instruction. Every assembler program has to have these elements. SEGMENT
|
|||
|
tells the assembler you are starting a section of contiguous material (code
|
|||
|
and/or data). The symmetrically named ENDS statement tells the assembler you
|
|||
|
are finished with a section of contiguous material. I wish they didn't use the
|
|||
|
word SEGMENT in this context. To me, a "segment" is a hardware construct: it
|
|||
|
is the 64K of real storage which becomes addressable by virtue of having a
|
|||
|
particular value in a segment register. Now, it is true that the "segments"
|
|||
|
you make with the assembler often correspond to real hardware "segments" at
|
|||
|
execution time. But, if you look at things like the GROUP and CLASS options
|
|||
|
supported by the linker, you will discover that this correspondence is by no
|
|||
|
means exact. So, at risk of maybe confusing you even more, I am going to use
|
|||
|
the more informal term "section" to refer to the area set off by means of the
|
|||
|
SEGMENT and ENDS instructions. The sections delimited by SEGMENT...ENDS pairs
|
|||
|
are really a lot like CSECTs and DSECTs in the 370 world.
|
|||
|
|
|||
|
I strongly recommend that you be selective in your study of the SEGMENT
|
|||
|
pseudo-op as described in the manual. Let me just touch on it here.
|
|||
|
|
|||
|
name SEGMENT
|
|||
|
name SEGMENT PUBLIC
|
|||
|
name SEGMENT AT nnn
|
|||
|
|
|||
|
Basically, you can get away with just the three forms given above. The first
|
|||
|
form is what you use when you are writing a single section of assembler code
|
|||
|
which will not be combined with other pieces of code at link time. The second
|
|||
|
form says that this assembly only contains part of the section; other parts
|
|||
|
might be assembled separately and combined later by the linker.
|
|||
|
|
|||
|
I have found that one can construct reasonably large modular applications in
|
|||
|
assembler by simply making every assembly use the same segment name and
|
|||
|
declaring the name to be PUBLIC each time. If you read the assembler and
|
|||
|
linker documentation, you will also be bombarded by information about more
|
|||
|
complex options such as the GROUP statement and the use of other "combine
|
|||
|
types" and "classes." I don't recommend getting into any of that. I will talk
|
|||
|
more about the linker and modular construction of programs a little later. The
|
|||
|
assembler manual also implies that a STACK segment is required. This is not
|
|||
|
really true. There are numerous ways to assure that you have a valid stack at
|
|||
|
execution time.
|
|||
|
|
|||
|
Of course, if you plan to write applications in assembler which are more than
|
|||
|
64K in size, you will need more than what I have told you; but who is really
|
|||
|
going to do that? Any application that large is likely to be coded in a higher
|
|||
|
level language.
|
|||
|
|
|||
|
The third form of the SEGMENT statement makes the delineated section into
|
|||
|
something like a "DSECT;" that is, it doesn't generate any code, it just
|
|||
|
describes what is present somewhere already in the computer's memory.
|
|||
|
Sometimes the AT value you give is meaningful. For example, the BIOS work area
|
|||
|
is located at location 40 hex. So, you might see
|
|||
|
|
|||
|
BIOSAREA SEGMENT AT 40H ;Map BIOS work area
|
|||
|
ORG BIOSAREA+10H
|
|||
|
EQUIP DB ? ;Location of equipment flags, first byte
|
|||
|
BIOSAREA ENDS
|
|||
|
|
|||
|
in a program which was interested in mucking around in the BIOS work area.
|
|||
|
At other times, the AT value you give may be arbitrary, as when you are mapping
|
|||
|
a repeated control block:
|
|||
|
|
|||
|
PROGPREF SEGMENT AT 0 ;Really a DSECT mapping the program prefix
|
|||
|
ORG PROGPREF+6
|
|||
|
MEMSIZE DW ? ;Size of available memory
|
|||
|
PROGPREF ENDS
|
|||
|
|
|||
|
Really, no matter whether the AT value represents truth or fiction, it is
|
|||
|
your responsibility, not the assembler's, to get set up a segment register so
|
|||
|
that you can really reach the storage in question. So, you can't say
|
|||
|
|
|||
|
MOV AL,EQUIP
|
|||
|
|
|||
|
unless you first say something like
|
|||
|
|
|||
|
MOV AX,BIOSAREA ;BIOSAREA becomes a symbol with value 40H
|
|||
|
MOV ES,AX
|
|||
|
ASSUME ES:BIOSAREA
|
|||
|
|
|||
|
Enough about SEGMENT. The END statement is simple. It goes at the end of
|
|||
|
every assembly. When you are assembling a subroutine, you just say
|
|||
|
|
|||
|
END
|
|||
|
|
|||
|
but when you are assembling the main routine of a program you say
|
|||
|
|
|||
|
END label
|
|||
|
|
|||
|
where 'label' is the place where execution is to begin.
|
|||
|
|
|||
|
Another pseudo-op illustrated in the program is ASSUME. ASSUME is like the
|
|||
|
USING statement in 370 assembler. However, ASSUME can ONLY refer to segment
|
|||
|
registers. The assembler uses ASSUME information to decide whether to assemble
|
|||
|
segment override prefixes and to check that the data you are trying to access
|
|||
|
is really accessible. In this case, we can reassure the assembler that both
|
|||
|
the CS and DS registers will address the section called HELLO at execution
|
|||
|
time. Actually, the SS and ES registers will too, but the assembler never
|
|||
|
needs to make use of this information.
|
|||
|
|
|||
|
I guess I have explained everything in the program except that ORG pseudo-op.
|
|||
|
ORG means the same thing as it does in many assembly languages. It tells the
|
|||
|
assembler to move its location counter to some particular address. In this
|
|||
|
case, we have asked the assembler to start assembling code hex 100 bytes from
|
|||
|
the start of the section called HELLO instead of at the very beginning. This
|
|||
|
simply reflects the way COM programs are loaded. When a COM program is loaded
|
|||
|
by the system, the system sets up all four segment registers to address the
|
|||
|
same 64K of storage. The first 100 hex bytes of that storage contains what is
|
|||
|
called the program prefix; this area is described in appendix E of the DOS
|
|||
|
manual. Your COM program physically begins after this. Execution begins with
|
|||
|
the first physical byte of your program; that is why the JMP instruction is
|
|||
|
there.
|
|||
|
|
|||
|
Wait a minute, you say, why the JMP instruction at all? Why not put the data
|
|||
|
at the end? Well, in a simple program like this I probably could have gotten
|
|||
|
away with that. However, I have the habit of putting data first and would
|
|||
|
encourage you to do the same because of the way the assembler has of assembling
|
|||
|
different instructions depending on the nature of the operand. Unfortunately,
|
|||
|
sometimes the different choices of instruction which can assemble from a single
|
|||
|
opcode have different lengths. If the assembler has already seen the data when
|
|||
|
it gets to the instructions it has a good chance of reserving the right number
|
|||
|
of bytes on the first pass. If the data is at the end, the assembler may not
|
|||
|
have enough information on the first pass to reserve the right number of bytes
|
|||
|
for the instruction. Sometimes the assembler will complain about this,
|
|||
|
something like "Forward reference is illegal" but at other times, it will make
|
|||
|
some default assumption. On the second pass, if the assumption turned out to
|
|||
|
be wrong, it will report what is called a "Phase error," a very nasty error to
|
|||
|
track down. So get in the habit of putting data and equated symbols ahead of
|
|||
|
code.
|
|||
|
|
|||
|
OK. Maybe you understand the program now. Let's walk through the steps
|
|||
|
involved in making it into a real COM file.
|
|||
|
|
|||
|
1. The file should be created with the name HELLO.ASM (actually the name is
|
|||
|
arbitrary but the extension .ASM is conventional and useful)
|
|||
|
|
|||
|
2. Now type:
|
|||
|
|
|||
|
ASM HELLO,,;
|
|||
|
|
|||
|
(this is just one example of invoking the assembler; it uses the small
|
|||
|
assembler ASM, it produces an object file and a listing file with the same name
|
|||
|
as the source file. I am not going exhaustively into how to invoke the
|
|||
|
assembler, which the manual goes into pretty well. I guess this is the first
|
|||
|
time I mentioned that there are really two assemblers; the small assembler ASM
|
|||
|
will run in a 64K machine and doesn't support macros. I used to use it all the
|
|||
|
time; now that I have a bigger machine and a lot of macro libraries I use the
|
|||
|
full function assembler MASM. You get both when you buy the package).
|
|||
|
|
|||
|
3. If you issue DIR at this point, you will discover that you have acquired
|
|||
|
HELLO.OBJ (the object code resulting from the assembly) and HELLO.LST (a
|
|||
|
listing file). I guess I can digress for a second here concerning the listing
|
|||
|
file. It contains TAB characters. I have found there are two good ways to get
|
|||
|
it printed and one bad way. The bad way is to use LPT1: as the direct target
|
|||
|
of the listing file or to try copying the LST file to LPT1 without first
|
|||
|
setting the tabs on the printer. The two good ways are to either:
|
|||
|
|
|||
|
a. direct it to the console and activate the printer with CTRL-PRTSC. In
|
|||
|
this case, DOS will expand the tabs for you.
|
|||
|
|
|||
|
b. direct to LPT1: but first send the right escape sequence to LPT1 to set
|
|||
|
the tabs every eight columns. I have found that on some early serial numbers
|
|||
|
of the IBM PC printer, tabs don't work quite right, which forces you to the
|
|||
|
first option.
|
|||
|
|
|||
|
4. Now type:
|
|||
|
|
|||
|
LINK HELLO;
|
|||
|
|
|||
|
(again, there are lots of linker options but this is the simplest. It takes
|
|||
|
HELLO.OBJ and makes HELLO.EXE). HELLO.EXE? I thought we were making a COM
|
|||
|
program, not an EXE program. Right. HELLO.EXE isn't really executable; its
|
|||
|
just that the linker doesn't know about COM programs. That requires another
|
|||
|
utility. You don't have this utility if you are using DOS 1.0; you have it if
|
|||
|
you are using DOS 1.1 or DOS 2.0. Oh, by the way, the linker will warn you
|
|||
|
that you have no stack segment. Don't worry about it.
|
|||
|
|
|||
|
5. Now type:
|
|||
|
|
|||
|
EXE2BIN HELLO HELLO.COM
|
|||
|
|
|||
|
This is the final step. It produces the actual program you will execute.
|
|||
|
Note that you have to spell out HELLO.COM; for a nominally rational but
|
|||
|
actually perverse reason, EXE2BIN uses the default extension BIN instead of COM
|
|||
|
for its output file. At this point, you might want to erase HELLO.EXE; it
|
|||
|
looks a lot more useful than it is. Chances are you won't need to recreate
|
|||
|
HELLO.COM unless you change the source and then you are going to have to redo
|
|||
|
the whole thing.
|
|||
|
|
|||
|
6. Now type:
|
|||
|
|
|||
|
HELLO
|
|||
|
|
|||
|
You type hello, that invokes the program, it says
|
|||
|
|
|||
|
HELLO YOURSELF!!!
|
|||
|
|
|||
|
(oops, what did I do wrong....?)
|
|||
|
|
|||
|
What about subroutines?
|
|||
|
_______________________
|
|||
|
|
|||
|
I started with a simple COM program because I actually think they are easier
|
|||
|
to create than subroutines to be called from high level languages, but maybe
|
|||
|
its really the latter you are interested in. Here, I think you should get
|
|||
|
comfortable with the assembler FIRST with little exercises like the one above
|
|||
|
and also another one which I will finish up with.
|
|||
|
|
|||
|
Next you are ready to look at the interface information for your particular
|
|||
|
language. You usually find this in some sort of an appendix. For example, the
|
|||
|
BASIC manual has Appendix C on Machine Language Subroutines. The PASCAL manual
|
|||
|
buries the information a little more deeply: the interface to a separately
|
|||
|
compiled routine can be found in the Chapter on Procedures and Functions, in a
|
|||
|
subsection called Internal Calling Conventions.
|
|||
|
|
|||
|
Each language is slightly different, but here are what I think are some
|
|||
|
common issues in subroutine construction.
|
|||
|
|
|||
|
1. NEAR versus FAR? Most of the time, your language will probably call your
|
|||
|
assembler routine as a FAR routine. In this case, you need to make sure the
|
|||
|
assembler will generate the right kind of return. You do this with a
|
|||
|
PROC...ENDP statement pair. The PROC statement is probably a good idea for a
|
|||
|
NEAR routine too even though it is not strictly required:
|
|||
|
|
|||
|
FAR linkage: | NEAR linkage:
|
|||
|
|
|
|||
|
ARBITRARY SEGMENT | SPECIFIC SEGMENT PUBLIC
|
|||
|
PUBLIC THENAME | PUBLIC THENAME
|
|||
|
ASSUME CS:ARBITRARY | ASSUME CS:SPECIFIC,DS:SPECIFIC
|
|||
|
THENAME PROC FAR | ASSUME ES:SPECIFIC,SS:SPECIFIC
|
|||
|
..... code and data | THENAME PROC NEAR
|
|||
|
THENAME ENDP | ..... code and data ....
|
|||
|
ARBITRARY ENDS | THENAME ENDP
|
|||
|
END | SPECIFIC ENDS
|
|||
|
| END
|
|||
|
|
|||
|
With FAR linkage, it doesn't really matter what you call the segment. you
|
|||
|
must declare the name by which you will be called in a PUBLIC pseudo-op and
|
|||
|
also show that it is a FAR procedure. Only CS will be initialized to your
|
|||
|
segment when you are called. Generally, the other segment registers will
|
|||
|
continue to point to the caller's segments. With NEAR linkage, you are
|
|||
|
executing in the same segment as the caller. Therefore, you must give the
|
|||
|
segment a specific name as instructed by the language manual. However, you may
|
|||
|
be able to count on all segment registers pointing to your own segment
|
|||
|
(sometimes the situation can be more complicated but I cannot really go into
|
|||
|
all of the details). You should be aware that the code you write will not be
|
|||
|
the only thing in the segment and will be physically relocated within the
|
|||
|
segment by the linker. However, all OFFSET references will be relocated and
|
|||
|
will be correct at execution time.
|
|||
|
|
|||
|
2. Parameters passed on the stack. Usually, high level languages pass
|
|||
|
parameters to subroutines by pushing words onto the stack prior to calling you.
|
|||
|
What may differ from language to language is the nature of what is pushed
|
|||
|
(OFFSET only or OFFSET and SEGMENT) and the order in which it is pushed (left
|
|||
|
to right, right to left within the CALL statement). However, you will need to
|
|||
|
study the examples to figure out how to retrieve the parameters from the stack.
|
|||
|
A useful fact to exploit is the fact that a reference involving the BP register
|
|||
|
defaults to a reference to the stack segment. So, the following strategy can
|
|||
|
work:
|
|||
|
|
|||
|
ARGS STRUC
|
|||
|
DW 3 DUP(?) ;Saved BP and return address
|
|||
|
ARG3 DW ?
|
|||
|
ARG2 DW ?
|
|||
|
ARG1 DW ?
|
|||
|
ARGS ENDS
|
|||
|
...........
|
|||
|
PUSH BP ;save BP register
|
|||
|
MOV BP,SP ;Use BP to address stack
|
|||
|
MOV ...,[BP].ARG2 ;retrieve second argument
|
|||
|
(etc.)
|
|||
|
|
|||
|
This example uses something called a structure, which is only available in
|
|||
|
the large assembler; furthermore, it uses it without allocating it, which is
|
|||
|
not a well-documented option. However, I find the above approach generally
|
|||
|
pleasing. The STRUC is like a DSECT in that it establishes labels as being
|
|||
|
offset a certain distance from an arbitrary point; these labels are then used
|
|||
|
in the body of code by beginning them with a period; the construction ".ARG2"
|
|||
|
means, basically, "+(ARG2-ARGS)."
|
|||
|
|
|||
|
What you are doing here is using BP to address the stack, accounting for the
|
|||
|
word where you saved the caller's BP and also for the two words which were
|
|||
|
pushed by the CALL instruction.
|
|||
|
|
|||
|
3. How big is the stack? BASIC only gives you an eight word stack to play
|
|||
|
with. On the other hand, it doesn't require you to save any registers except
|
|||
|
the segment registers. Other languages give you a liberal stack, which makes
|
|||
|
things a lot easier. If you have to create a new stack segment for yourself,
|
|||
|
the easiest thing is to place the stack at the end of your program and:
|
|||
|
|
|||
|
CLI ;suppress interrupts while changing the stack
|
|||
|
MOV SSAVE,SS ;save old SS in local storage (old SP
|
|||
|
; already saved in BP)
|
|||
|
MOV SP,CS ;switch
|
|||
|
MOV SS,SP ;the
|
|||
|
MOV SP,OFFSET STACKTOP ;stack
|
|||
|
STI ;(maybe)
|
|||
|
|
|||
|
Later, you can reverse these steps before returning to the caller. At the
|
|||
|
end of your program, you place the stack itself:
|
|||
|
|
|||
|
DW 128 DUP(?) ;stack of 128 words (liberal)
|
|||
|
STACKTOP LABEL WORD
|
|||
|
|
|||
|
4. Make sure you save and restore those registers required by the caller.
|
|||
|
|
|||
|
5. Be sure to get the right kind of addressibility. In the FAR call
|
|||
|
example, only CS addresses your segment. If you are careful with your ASSUME
|
|||
|
statements the assembler will keep track of this fact and generate CS prefixes
|
|||
|
when you make data references; however, you might want to do something like:
|
|||
|
|
|||
|
MOV AX,CS ;get current segment address
|
|||
|
MOV DS,AX ;To DS
|
|||
|
ASSUME DS:THISSEG
|
|||
|
|
|||
|
Be sure you keep your ASSUMEs in synch with reality.
|
|||
|
|
|||
|
Learning about BIOS and the hardware
|
|||
|
____________________________________
|
|||
|
|
|||
|
You can't do everything with DOS calls. You may need to learn something
|
|||
|
about the BIOS and about the hardware itself. In this, the Technical Reference
|
|||
|
is a very good thing to look at.
|
|||
|
|
|||
|
The first thing you look at in the Technical Reference, unless you are really
|
|||
|
determined to master the whole ball of wax, is the BIOS listings presented in
|
|||
|
Appendix A. Glory be: here is the whole 8K of ROM which deals with low level
|
|||
|
hardware support layed out with comments and everything. In fact, if you are
|
|||
|
just interested in learning what BIOS can do for you, you just need to read the
|
|||
|
header comments at the beginning of each section of the listing.
|
|||
|
|
|||
|
BIOS services are invoked by means of the INT instruction; the BIOS occupies
|
|||
|
interrupts 10H through 1FH and also interrupt 5H; actually, of these seventeen
|
|||
|
interrupts, five are used for user exit points or data pointers, leaving twelve
|
|||
|
actual services.
|
|||
|
|
|||
|
In most cases, a service deals with a particular hardware interface; for
|
|||
|
example, BIOS interrupt 10H deals with the screen. As with DOS function calls,
|
|||
|
many BIOS services can be passed a function code in the AH register and
|
|||
|
possible other arguments.
|
|||
|
|
|||
|
I am not going to summarize the most useful BIOS features here; you will see
|
|||
|
some examples in the next sample program we will look at.
|
|||
|
|
|||
|
The other thing you might want to get into with the Tech reference is the
|
|||
|
description of some hardware options, particularly the asynch adapter, which
|
|||
|
are not well supported in the BIOS. The writeup on the asynch adapter is
|
|||
|
pretty complete.
|
|||
|
|
|||
|
Actually, the Tech reference itself is pretty complete and very nice as far
|
|||
|
as it goes. One thing which is missing from the Tech reference is information
|
|||
|
on the programmable peripheral chips on the system board. These include:
|
|||
|
|
|||
|
the 8259 interrupt controller
|
|||
|
the 8253 timer
|
|||
|
the 8237 DMA controller and
|
|||
|
the 8255 peripheral interface
|
|||
|
|
|||
|
To make your library absolutely complete, you should order the INTEL data
|
|||
|
sheets for these beasts.
|
|||
|
|
|||
|
I should say, though, that the only I ever found I needed to know about was
|
|||
|
the interrupt controller. If you happen to have the 8086 Family User's Manual,
|
|||
|
the big book put out by INTEL, which is one of the things people sometimes buy
|
|||
|
to learn about 8086 architecture, there is an appendix there which gives an
|
|||
|
adequate description of the 8259.
|
|||
|
|
|||
|
A final example
|
|||
|
_______________
|
|||
|
|
|||
|
I leave you with a more substantial example of code which illustrates some
|
|||
|
good elementary techniques; I won't claim its style is perfect, but I think it
|
|||
|
is adequate. I think this is a much more useful example than what you will get
|
|||
|
with the assembler:
|
|||
|
|
|||
|
PAGE 61,132
|
|||
|
TITLE SETSCRN -- Establish correct monitor use at boot time
|
|||
|
;
|
|||
|
; This program is a variation on many which toggle the equipment flags
|
|||
|
; to support the use of either video option (monochrome or color).
|
|||
|
; The thing about this one is it prompts the user in such a way that he
|
|||
|
; can select the use of the monitor he is currently looking at (or which
|
|||
|
; is currently connected or turned on) without really having to know
|
|||
|
; which is which. SETSCRN is a good program to put first in an
|
|||
|
; AUTOEXEC.BAT file.
|
|||
|
;
|
|||
|
; This program is highly dependent on the hardware and BIOS of the IBMPC
|
|||
|
; and is hardly portable, except to very exact clones. For this reason,
|
|||
|
; BIOS calls are used in lieu of DOS function calls where both provide
|
|||
|
; equal function.
|
|||
|
;
|
|||
|
|
|||
|
OK. That's the first page of the program. Notice the PAGE statement, which
|
|||
|
you can use to tell the assembler how to format the listing. You give it lines
|
|||
|
per page and characters per line. I have mine setup to print on the host
|
|||
|
lineprinter; I routinely upload my listings at 9600 baud and print them on the
|
|||
|
host; it is faster than using the PC printer. There is also a TITLE statement.
|
|||
|
This simply provides a nice title for each page of your listing. Now for the
|
|||
|
second page:
|
|||
|
|
|||
|
SUBTTL -- Provide .COM type environment and Data
|
|||
|
PAGE
|
|||
|
;
|
|||
|
; First, describe the one BIOS byte we are interested in
|
|||
|
;
|
|||
|
BIOSDATA SEGMENT AT 40H ;Describe where BIOS keeps his data
|
|||
|
ORG 10H ;Skip parts we are not interested in
|
|||
|
EQUIP DB ? ;Equipment flag location
|
|||
|
MONO EQU 00110000B ;These bits on if monochrome
|
|||
|
COLOR EQU 11101111B ;Mask to make BIOS think of the color board
|
|||
|
BIOSDATA ENDS ;End of interesting part
|
|||
|
;
|
|||
|
; Next, describe some values for interrupts and functions
|
|||
|
;
|
|||
|
DOS EQU 21H ;DOS Function Handler INT code
|
|||
|
PRTMSG EQU 09H ;Function code to print a message
|
|||
|
KBD EQU 16H ;BIOS keyboard services INT code
|
|||
|
GETKEY EQU 00H ;Function code to read a character
|
|||
|
SCREEN EQU 10H ;BIOS Screen services INT code
|
|||
|
MONOINIT EQU 02H ;Value to initialize monochrome screen
|
|||
|
;COLORINIT EQU 03H ;Value to initialize color screen (80x25)
|
|||
|
COLORINIT EQU 01H ;Value to initialize color screen (40X25)
|
|||
|
;
|
|||
|
; Now, describe our own segment
|
|||
|
;
|
|||
|
SETSCRN SEGMENT ;Set operating segment for CODE and DATA
|
|||
|
;
|
|||
|
ASSUME CS:SETSCRN,DS:SETSCRN,ES:SETSCRN,SS:SETSCRN ;All segments
|
|||
|
;
|
|||
|
ORG 100H ;Begin assembly at standard .COM offset
|
|||
|
;
|
|||
|
MAIN PROC NEAR ;COM files use NEAR linkage
|
|||
|
JMP BEGIN ;And, it is helpful to put the data first, but
|
|||
|
; ;then you must branch around it.
|
|||
|
;
|
|||
|
; Data used in SETSCRN
|
|||
|
;
|
|||
|
CHANGELOC DD EQUIP ;Location of the EQUIP, recorded as far pointer
|
|||
|
MONOPROMPT DB 'Please press the plus ( + ) key.$' ;User sees on mono
|
|||
|
COLORPROMPT DB 'Please press the minus ( - ) key.$' ;User sees on color
|
|||
|
|
|||
|
Several things are illustrated on this page. First, in addition to titles,
|
|||
|
the assembler supports subtitles: hence the SUBTTL pseudo-op. Second, the
|
|||
|
PAGE pseudo-op can be used to go to a new page in the listing. You see an
|
|||
|
example here of the DSECT-style segment in the "SEGMENT AT 40H". Here, our our
|
|||
|
interest is in correctly describing the location of some data in the BIOS work
|
|||
|
area which really is located at segment 40H.
|
|||
|
|
|||
|
You will also see illustrated the EQU instruction, which just gives a
|
|||
|
symbolic name to a number. I don't make a fetish of giving a name to every
|
|||
|
single number in a program. I do feel strongly, though, that interrupts and
|
|||
|
function codes, where the number is arbitrary and the function being performed
|
|||
|
is the thing of interest, should always be given symbolic names.
|
|||
|
|
|||
|
One last new element in this section is the define doubleword (DD)
|
|||
|
instruction. A doubleword constant can refer, as in this case, to a location
|
|||
|
in another segment. The assembler will be happy to use information at its
|
|||
|
disposal to properly assemble it. In this case, the assembler knows that EQUIP
|
|||
|
is offset 10 in the segment BIOSDATA which is at 40H.
|
|||
|
|
|||
|
SUBTTL -- Perform function
|
|||
|
PAGE
|
|||
|
BEGIN: CALL MONOON ;Turn on mono display
|
|||
|
MOV DX,OFFSET MONOPROMPT ;GET MONO PROMPT
|
|||
|
MOV AH,PRTMSG ;ISSUE
|
|||
|
INT DOS ;IT
|
|||
|
CALL COLORON ;Turn on color display
|
|||
|
MOV DX,OFFSET COLORPROMPT ;GET COLOR PROMPT
|
|||
|
MOV AH,PRTMSG ;ISSUE
|
|||
|
INT DOS ;IT
|
|||
|
MOV AH,GETKEY ;Obtain user response
|
|||
|
INT KBD
|
|||
|
CMP AL,'+' ;Does he want MONO?
|
|||
|
JNZ NOMONO
|
|||
|
CALL MONOON ;yes. give it to him
|
|||
|
NOMONO: RET
|
|||
|
MAIN ENDP
|
|||
|
|
|||
|
|
|||
|
The main code section makes use of subroutines to keep the basic flow simple.
|
|||
|
About all that's new to you in this section is the use of the BIOS interrupt
|
|||
|
KBD to read a character from the keyboard.
|
|||
|
|
|||
|
Now for the subroutines, MONOON and COLORON:
|
|||
|
|
|||
|
SUBTTL -- Routines to turn monitors on
|
|||
|
PAGE
|
|||
|
MONOON PROC NEAR ;Turn mono on
|
|||
|
LES DI,CHANGELOC ;Get location to change
|
|||
|
ASSUME ES:BIOSDATA ;TELL ASSEMBLER ABOUT CHANGE TO ES
|
|||
|
OR EQUIP,MONO
|
|||
|
MOV AX,MONOINIT ;Get screen initialization value
|
|||
|
INT SCREEN ;Initialize screen
|
|||
|
RET
|
|||
|
MONOON ENDP
|
|||
|
COLORON PROC NEAR ;Turn color on
|
|||
|
LES DI,CHANGELOC ;Get location to change
|
|||
|
ASSUME ES:BIOSDATA ;TELL ASSEMBLER ABOUT CHANGE TO ES
|
|||
|
AND EQUIP,COLOR
|
|||
|
MOV AX,COLORINIT ;Get screen initialization value
|
|||
|
INT SCREEN ;Initialize screen
|
|||
|
RET
|
|||
|
COLORON ENDP
|
|||
|
SETSCRN ENDS ;End of segment
|
|||
|
END MAIN ;End of assembly; execution at MAIN
|
|||
|
|
|||
|
|
|||
|
The instructions LES and LDS are useful ones for dealing with doubleword
|
|||
|
addresses. The offset is loaded into the operand register and the segment into
|
|||
|
ES (for LES) or DS (for LDS). By telling the assembler, with an ASSUME, that
|
|||
|
ES now addresses the BIOSDATA segment, it is able to correctly assemble the OR
|
|||
|
and AND instructions which refer to the EQUIP byte. An ES segment prefix is
|
|||
|
added.
|
|||
|
|
|||
|
To understand the action here, you simply need to know that flags in that
|
|||
|
particular byte control how the BIOS screen service initializes the adapters.
|
|||
|
BIOS will only work with one adapter at a time; by setting the equipment flags
|
|||
|
to show one or the other as installed and calling BIOS screen initialization,
|
|||
|
we achieve the desired effect.
|
|||
|
|
|||
|
The rest is up to you.
|
|||
|
|