IBM PC Assembly Language Tutorial 5
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.
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.
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.
EXE2BIN HELLO HELLO.COM
This is the final step. It produces the actual program you will exe-
cute. 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.
HELLO
You type hello, that invokes the program, it says
HELLO YOURSELF!!!
(oops, what did I do wrong....?)