Category : Assembly Language Source Code
Archive   : ALPROG.ZIP
Filename : ALPROG2.TUT

 
Output of file : ALPROG2.TUT contained in archive : ALPROG.ZIP
.. 90 lines per page
.. leave 6/12 lines for article headline
.. put cursor on ruler line & hit Ctrl-OF
.. !------!----!----!----!----!----!----!----!-----





IBM Personal Computer Assembly Language Tutorial
Joshua Auerbach, Yale University

Yale Computer Center
175 Whitney Avenue
P. O. Box 2112
New Haven, Connecticut 06520











Learning the assembler
_____________________

It is my feeling that many people can teach them-
selves 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, than 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 assem-
; bler 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 instruc-
tions 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 repre-
sent 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 double-
word 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 conti-
guous 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 assem-
bler 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 arbi-
trary, 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 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 instruc-
tions 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....?)

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:

ARBITRARY SEGMENT
PUBLIC THENAME
ASSUME CS:ARBITRARY
THENAME PROC FAR
..... code and data
THENAME ENDP
ARBITRARY ENDS
END


NEAR linkage:

SPECIFIC SEGMENT PUBLIC
PUBLIC THENAME
ASSUME CS:SPECIFIC,DS:SPECIFIC
ASSUME ES:SPECIFIC,SS:SPECIFIC
THENAME PROC NEAR
..... code and data ....
THENAME ENDP
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
...........
(continued at top of next column)


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 addressibili-
ty. 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 one 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
ASSUME 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 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.
r the
other as installed and calling BIOS scr

  3 Responses to “Category : Assembly Language Source Code
Archive   : ALPROG.ZIP
Filename : ALPROG2.TUT

  1. Very nice! Thank you for this wonderful archive. I wonder why I found it only now. Long live the BBS file archives!

  2. This is so awesome! 😀 I’d be cool if you could download an entire archive of this at once, though.

  3. But one thing that puzzles me is the “mtswslnkmcjklsdlsbdmMICROSOFT” string. There is an article about it here. It is definitely worth a read: http://www.os2museum.com/wp/mtswslnk/