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 address-
able 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 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.HELLO
You type hello, that invokes the program, it says HELLO YOURSELF!!!
(oops, what did I do wrong....?)