Prev TOP NEXT HOME


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....?)