::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::. Dec 99-Feb
00
:::\_____\::::::::::. Issue
7
::::::::::::::::::::::.........................................................
A S S E M B L Y P R O G R A M M I N G J O U R N A L
http://asmjournal.freeservers.com
asmjournal@...
T A B L E O F C O N T E N T S
----------------------------------------------------------------------
Introduction...................................................mammon_
"Extending DOS Executables"..........................Digital.Alchemist
"Creating a User-Friendly Interface"......................S.Sirajudeen
"ASM Building Blocks"...................................Laura.Fairhead
"Converting Strings to Numbers"...........................Chris.Dragan
"List Scan Library Routine".............................Laura.Fairhead
"Using the RTC"..........................................Jan.Verhoeven
"Chaos Animation".......................................Laura.Fairhead
"Inline Assembler With Modula"...........................Jan.Verhoeven
"Assembly on the Alpha Platform"........................Rudolf.Seemann
Column: Win32 Assembly Programming
"Direct Draw Samples"....................................X-Calibre
Column: The Unix World
"Enter fbcon".................................Konstantin.Boldyshev
Column: Assembly Language Snippets
"ToHex".....................................................Ronald
"Hex2ASCII"................................................cpuburn
"MMX ltostr".....................................Cecchinel.Stephan
Column: Issue Solution
"ScreenDump"........................................Laura.Fairhead
----------------------------------------------------------------------
+++++++++++++++++++Issue Challenge++++++++++++++++++
Dump the contents of the current console to a file
----------------------------------------------------------------------
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::..............................................INTRODUCTION
by
mammon_
What? Late again? Wasn't there going to be a December issue?
Well, yeah, there was; unfortunately once again real-world concerns
interfered
with timely distribution. And, as usually happens with late issues, this one
is waaaaay oversized, almost 200K due to all the articles I crammed into it.
I
didn't even get a chance to include my linux kernel modules article...
This issue seems to have a bit of a 'Hex-to-ASCII' bent to it, mostly from
the
snippets but also from the conversion routines offered by Chris and Laura.
In
addition, some 'fringe' asm has been supplied with Jan's Modula article,
along
with an introduction to Alpha assembly language by Rudolph Seeman.
Konstantin
Boldyshev, who helps maintain the linuxassembly.org site, continues the Unix
trend with an introduction to frame-buffer programming under linux.
The two leading articles are both quite large and offer a wealth of
information
for the beginning and experienced asm programmer. Digital Alchemist has
produced
a work on applying virus techniques to non-destructive applications, and S.
Sirajudeen has tackled the huge problem of creating a decent UI in
console-mode
programs.
In this issue I have tried to leave the code comments as untouched as
possible;
the coding styles of the authors vary quite widely, and each clearly
demonstrates
the planning behind the program itself -- showing how the algorithm was
conceived before implementation. Stripping any of these examples of all but
comments will soon reveal the worksheet used by the coders to develop their
programs.
Finally, I have taken to formatting these issues in Vim under linux; to
check
margins and pagination I have begun proofing them in Netscape and
WordPerfect
[10 pt Courier, natch]; they should view fine in any web browser and in most
word processors; to those stuck with Notepad or Edit.com ... my apologies.
_m
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
Extending DOS
Executables
by Digital Alchemist
The reason behind this essay is to show how techniques first developed by
virus
writers can be used for benevolent purposes. It is my opinion that all
knowledge is good and viral techniques are certainly no exception. I will
lead
you through the development of a program called DOSGUARD which benignly
modifies DOS executables, both COM and EXE.
DESCRIPTION OF DOSGUARD
-----------------------
DOSGUARD is a DOS COM program which I developed in order to restrict access
to
certain programs on my computer. DOSGUARD modifies all of the COM and EXE
files in the current directory, adding code to each one that requires the
user
to correctly enter a password before running the original program.
DOSGUARD, while sufficient for this article, could use a little work in the
realm of user friendliness. More user feedback and a better way to specify
which files to be modified are needed. In addition, I have written a
version
of DOSGUARD that uses simple xor encryption to improve security.
DOSGUARD was written using turbo assembler.
STRUCTURE OF COM FILES
----------------------
Unlike the EXE file format, the programmer has no input into the segment
format
of COM files. All COM files consist of 1 segment only, with no predefined
distinction between data and code. After DOS finishes some preparatory
work,
the COM file is loaded at offset 100h. The first 256 bytes are known as the
Program Segment Prefix(PSP). Located at offset 80h is an important data
structure called the DTA or Data Transfer Area. The DTA is important, but
most
of the rest of the PSP can be ignored by the programmer. Before actually
starting execution of the COM program, DOS sets up the stack at the top of
the
segment(the highest memory address).
OUTLINE OF COM MODIFICATION
---------------------------
1. Open the file and read 1st 5 bytes.
2. Make sure the file is not really an EXE file because after DOS 6.0 some
files ending in ".com" were really EXEs.
3. Check to see if the file has already been modified by DOSGUARD by
checking
if the values of the 4th and 5th bytes match the DOSGUARD identification
string of "CG".
4. Make sure the file is not so large that when DOSGUARD adds its code it
doesn't exceed the 64k segment size.
5. If the file passes 2-4 then its ok to modify, so DOSGUARD opens it and
writes the code to the end of the file.
6. Calculate the size of the jump to the code we added and write the jump
instruction along with the identification string to the beginning of the
file.
I'll go over each of these steps in a little more detail with code snippets
where necessary. The complete source code for DOSGUARD can be found at the
end of the article and at my web page. Hopefully, the comments will be
enough
to explain any areas I don't discuss in detail.
Essentially, the way DOSGUARD modifies COM files is by inserting a jump at
the
beginning of the file which goes straight to the password authentication
code,
located at the end of the file. If the correct password is entered by the
user, then it will restore the 5 bytes that were overwritten by the jump and
the identification string and execute the program just like DOSGUARD was
never
there.
COM MODIFICATION - STEP 1
-------------------------
Once we've found a COM file, the first thing to do is open it. Then, after
running some tests on the file, we can determine if it is suitable for
modification. But first, we need to read the first 5 bytes because we'll
need them later.
mov ax, 3D02h ;Open file R/W
mov dx, 9Eh ;Filename, stored in DTA
int 21h
mov bx, ax ;Save file handle in bx
mov ax, 3F00h ;Read first 5 bytes from file
mov cx, 5
mov dx, offset obytes
int 21h
COM MODIFICATION - STEP 2
-------------------------
After DOS 6.0, some files with the COM extension are actually EXEs.
COMMAND.COM, for instance, is one of these. If we try to modify an EXE file
as
if it were a COM file, then we're going to really screw things up. To
prevent
this, we make sure that the string "MZ" doesn't appear in the first two
bytes of
the file. "MZ" is the string which tells DOS that a file is an EXE.
;Check to see if file is really an EXE
cmp word ptr[obytes], 'ZM'
je EXE
COM MODIFICATION - STEP 3
-------------------------
If the file had been previously altered by DOSGUARD, then the 4th and 5th
bytes
will contain the identification string "CG". We need to make sure we skip
files
that have this identification string.
;Check to see if file is already infected
;if it is, then skip it
cmp word ptr [obytes + 3], 'GC'
je NO_INFECT
COM MODIFICATION - STEP 4
-------------------------
Another thing to watch out for is the file's size. If the file will exceed
one segment in size when we add our code, then the file is too big to
modify.
;Make sure file isn't too large
mov ax, ds:[009Ah] ;Size of file from DTA
add ax, offset ENDGUARD - offset COMGUARD + 100h
jc NO_INFECT ;If ax overflows then don't infect
COM MODIFICATION - STEP 5
-------------------------
If the file is a suitable candidate for modification, then we simply write
our
code to the end of the file. Also, we have to save the original first 5
bytes
from the file somewhere in your code. In DOSGUARD's case, the 5 bytes are
already saved in the proper place because "obytes" is located within the
code
which we are about to write.
xor cx, cx ;cx = 0
xor dx, dx ;dx = 0
mov ax, 4202h ;Move file pointer to the end of
file
int 21h
mov ax, 4000h ;Write the code to the end of file
mov dx, offset COMGUARD
mov cx, offset ENDGUARD - offset COMGUARD
int 21h
COM MODIFICATION - STEP 6
-------------------------
The final step is to calculate the size of the jump to our code and write
the
opcode for the jump and the identification string over the first 5 bytes of
the
file.
mov ax, 4200h ;Move file pointer to beginning of
xor cx, cx ; file to write jump
xor dx, dx
int 21h
;Prepare the jump instruction to be written to beginning of file
xor ax, ax
mov byte ptr [bytes], 0E9h ;opcode for jmp
mov ax, ds:[009Ah] ;size of the file
sub ax, 3 ;size of the jump instruction
mov word ptr [bytes + 1], ax;size of the jump
;Write the jump
mov cx, 5; ;size to be written
mov dx, offset bytes
mov ax, 4000h
int 21h
mov ah, 3Eh ;Close file
int 21h
RESPONSIBILITIES OF INSERTED CODE
--------------------------------
There are two problems which the inserted code has to deal with. First,
since
the code could be located at any arbitrary offset within the segment, it
cannot
depend on the compiled absolute addresses of its data labels. To solve this
problem we use a technique virus writers call the delta offset. The delta
offset is the difference between the actual and compiled addresses of data.
Anytime our code accesses data in memory it adds the delta offset to the
data's
compiled address. The following piece of code finds the delta offset.
call GET_START
GET_START:
pop bp
sub bp, offset GET_START
The "call" pushes the current ip onto the stack, which is the actual address
of
the label "GET_START." Subtract the compiled address from the actual one
and
there's our delta offset.
The second problem is to make sure the first 5 bytes of the host are
restored to
their original values before we return from our jump and execute the host.
STRUCTURE OF EXE FILES
----------------------
The EXE file format is much more complicated than the COM format. The big
difference is that EXE files allow the program to specify how it wants its
segments to be laid out in memory, allowing programs to exceed one 64k
segment
in size. Most EXEs will have separate code, data, and stack segments.
All of this information is stored in the EXE Header. Here's a brief rundown
of
what the header looks like:
Offset Size Field
0 2 Signature. Will always be 'MZ'
2 2 Last Page Size. Number of bytes on the last
page of memory.
4 2 Page Count. Number of 512 byte pages in the file.
6 2 Relocation Table Entries. Number of items in the
relocation pointer table.
8 2 Header Size. Size of header in paragraphs,
including the relocation pointer table.
10 2 Minalloc
12 2 Maxalloc
14 2 Initial Stack Segment.
16 2 Initial Stack Pointer.
18 2 Checksum. (Usually ignored)
20 2 Initial Instruction Pointer
22 2 Initial Code Segment
24 2 Relocation Table Offset. Offset to the start of
the relocation pointer table.
26 2 Overlay Number. Primary executables(the ones we
wish to modify) always have this set to zero.
Following the EXE header is the relocation pointer table, with a variable
amount of blank space between the header and the start of the table. The
relocation table is a table of offsets. These offsets are combined with
starting segment values calculated by DOS to point to a word in memory where
the final segment address is written. Essentially, the relocation pointer
table is DOS's way to handle the dynamic placement of segments into physical
memory. This isn't a problem with COM files because there is only one
segment
and the program isn't aware of anything else. Following the relocation
pointer
table is another variable amount of reserved space and finally the program
body.
To successfully add code to an EXE file requires careful manipulation of the
EXE
header and relocation pointer table.
OUTLINE OF EXE MODIFICATION
---------------------------
1. Open the file and read the 1st 2 bytes(DOSGUARD actually reads 5).
2. Check for EXE signature "MZ".
3. Read the EXE header.
4. Check the file for previous infection.
5. Make sure that the Overlay Number is 0.
6. Make sure the file is a DOS EXE.
7. If the file passes 2-6 then it is ok to modify. The first step is to
check
the relocation pointer table to see if there is room to add 2 pointers.
If
there is room, then jump to step 9.
8. If there isn't enough room in the relocation pointer table, then
DOSGUARD
has to make room. It reads in the entire file after the relocation
pointer
table and writes it back out one paragraph higher in memory.
9. Save the original ss, sp, cs, and ip.
10. Adjust the file length to paragraph boundary.
11. Write code to the end of the file.
12. Adjust the EXE header to reflect the new starting segments and file
size.
13. Write out the header.
14. Modify the relocation pointer table.
The easiest way to think about EXE modification is to imagine that we are
adding a complete COM program to the end of the file. Our code will occupy
its
own segment located just after the host. This one segment will serve as a
code,
data, and stack segment just like in a COM program. Instead of inserting a
jump
to take us there, we will simply adjust the starting segment values in the
EXE
header to point to our segment.
EXE MODIFICATION - STEP 1
-------------------------
The same as with COM files, except that the only bytes we actually need are
the
first two. With EXE files we will use different methods for determining
previous modification(I try to avoid using the viral term "infection") and
for
transferring execution to our code.
EXE MODIFICATION - STEP 2
-------------------------
Check the first two bytes for the EXE signature "MZ". If the file doesn't
start with "MZ," then it isn't a DOS EXE.
cmp word ptr[obytes], 'ZM'
je EXE
EXE MODIFICATION - STEP 3
-------------------------
Now, DOSGUARD simply reads the EXE header into a 28 byte buffer. Later, we
will make the necessary changes to the header and write it back out.
xor cx, cx ;Move the file pointer back
xor dx, dx ;to the beginning of the file
mov ax, 4200h
int 21h
mov cx, 1Ch ;read exe header (28 bytes)
mov dx, offset exehead ;into buffer
mov ah, 3Fh
int 21h
EXE MODIFICATION - STEP 4
-------------------------
We don't use a signature string to mark EXE files. Instead, we compare the
code entry point with the size of the file. If the file has been previously
modified by DOSGUARD, then we know that the distance of the code entry point
from the end of the file will be the length of the code that DOSGUARD adds.
To
put things in mathematical terms:
(initial cs * 16) + (size of code DOSGUARD adds) + (size of header)
will equal the size of the file. The initial cs times 16 is the code entry
point, of course. You have to add the header size because it isn't loaded
into
memory along with the rest of the code and data.
;Make sure it hasn't already been infected
;If (initial CS * 16) + (size of code) + (size of header) ==
filesize
; then the file has already been infected
mov ax, word ptr [exehead+22]
mov dx, 16
mul dx
add ax, offset ENDGUARD2 - offset EXEGUARD
adc dx, 0
mov cx, word ptr [exehead+8]
add cx, cx
add cx, cx
add cx, cx
add cx, cx
add ax, cx
adc dx, 0
cmp ax, word ptr cs:[9Ah]
jne EXEOK
cmp dx, word ptr cs:[9Ch]
je NO_INFECT
EXE MODIFICATION - STEP 5
-------------------------
Another simple test that needs to be done is to make sure that the Overlay
Number stored in the EXE header is 0. The code for this is simple.
;Make sure Overlay Number is 0
cmp word ptr [exehead+26], 0
jnz NO_INFECT
EXE MODIFICATION - STEP 6
-------------------------
This part is kind of tricky. There are lots of files out there with the EXE
extension that aren't DOS executables. Both Windows and OS/2 use this
extension as well, for instance. To complicate matters, there isn't an easy
way to automatically distinguish DOS EXEs from the others. The technique
that
I use in DOSGUARD is to check the offset of the relocation pointer table and
make sure that it is less than 40h. This should always detect Windows and
OS/2
programs, but it sometimes raises false alarms on valid DOS files.
;Make sure it is a DOS EXE (as opposed to windows or OS/2)
cmp word ptr [exehead+24], 40h
jae NO_INFECT
EXE MODIFICATION - STEP 7
-------------------------
Now that we know we have a file that we can modify we just have to determine
if
its going to be easy to modify or a real pain. Here's the deal. The
relocation pointer table is always an even multiple of 16 bytes in size.
Each
pointer in the table is 4 bytes. For our purposes, we need to add 2
pointers to
the table. That means the table must have at least 8 bytes free in order to
leave it at its current size. If it doesn't have room for two more
pointers,
then we will have to make room. That means reading in the whole file after
the
table and writing it back out with 16 bytes more space for the table.
To find out if there is enough room, all you have to do is subtract the
offset
of the relocation pointer table and the number of entries in the table from
the
size of the header. The result is the amount of free space in the table.
All
of this information can be found in the handy dandy EXE header. Of course,
you
have to take into account the units that each of these values are stored in
(bytes, paragraphs, etc.)
;Check the relocation pointer table to see if there is
;room. If there isn't then we'll have to make room.
mov ax, word ptr [exehead+8];size of header in paragraphs
add ax, ax ;
add ax, ax ;Convert to double words.
sub ax, word ptr [exehead+6];Subtract # of entries each of
add ax, ax ;which is a double word and then
add ax, ax ;convert the final total to bytes.
sub ax, word ptr [exehead+24];If there are 8 bytes left after
cmp ax, 8 ;you subtract the offset to the
jc NOROOM ;reloc table then there is room.
jmp HAVEROOM
EXE MODIFICATION - STEP 8
-------------------------
The first thing to do is move the file pointer to the correct spot just
after
the last entry in the relocation pointer table.
xor cx, cx ;Move the file pointer to the end of
mov dx, word ptr [exehead+24] ;the relocation pointer table.
mov ax, word ptr [exehead+6];size of relocation table in doubles
add ax, ax ;* 4 to get bytes
add ax, ax
add dx, ax ;add that to start of table
push dx
mov ax, 4200h
int 21h
Now, DOSGUARD calculates the amount which needs to be written. This code is
in
the function called CALC_SIZE. When CALC_SIZE is finished, cx will hold the
number of pages and "lps" will hold the size of the last page since it
probably
will not be a full 512 byte page.
;dx holds the position in the file where we want to start reading.
;So, the amount to read in and write back out is equal to the size
;of the file minus dx.
mov cx, word ptr [exehead+2]
mov word ptr [lps], cx ;Copy Last Page Size into lps
mov cx, word ptr [exehead+4];Copy Num Pages into cx
cmp dx, word ptr [lps] ;If bytes to subtract are less than
jbe FINDLPS ;lps then just subtract them and
exit
mov ax, dx
xor dx, dx
mov cx, 512
div cx ;ax = pages to subtract
mov cx, word ptr [exehead+4];dx = remainder to subtract from lps
sub cx, ax
cmp dx, word ptr [lps]
jbe FINDLPS
sub cx, 1
mov ax, dx
sub ax, word ptr [lps]
mov dx, 512
sub dx, ax
FINDLPS:
sub word ptr [lps], dx ;Subtract start position and leave
;Num Pages the same
Once you know the amount of code you have to move, you have to come up with
a
way to simultaneously read and write from the same file without overwriting
data that hasn't been read yet. DOSGUARD's solution is to use a 16 byte
buffer. DOSGUARD's move loop reads 528 bytes and writes out 512 bytes with
each
iteration. In other words, it reads 16 bytes ahead of where it is writing
so
that it doesn't overwrite bytes before they're read. DOSGUARD has a number
of
functions for reading and writing pages, reading and writing paragraphs,
and
moving the file pointer around. It also has one function for moving the 16
bytes at the end of the 528 byte buffer in memory to the front. Well, I'll
shut
up now and show you the code for the move loop.
mov dx, offset buffer
call READ_PAGE
mov dx, offset para
call READ_PARA
call DECFP_PAGE
call WRITE_PAGE
call MOVE_PARA
dec cx
cmp cx, 1
je LASTPAGE
MOVELOOP:
mov dx, offset buffer + 16
call READ_PAGE
call DECFP_PAGE
call WRITE_PAGE
call MOVE_PARA
dec cx
cmp cx, 1
jne MOVELOOP
When DOSGUARD gets to the last page, it finishes things off by reading the
last
fraction of a page and then writing out those bytes plus the 16 bytes that
were
left buffered from the last iteration of the move loop.
LASTPAGE:
sub word ptr [lps], 16
mov cx, word ptr [lps]
mov dx, offset buffer + 16
mov ah, 3Fh
int 21h
push cx
mov dx, cx
neg dx
mov cx, -1
mov ax, 4201h
int 21h
pop cx
add cx, 16
mov dx, offset buffer
mov ah, 40h
int 21h
Last, but not least, there is a little maintanence to do.
;Got to adjust the file size since it will be used later
add word ptr cs:[9Ah], 16
adc word ptr cs:[9Ch], 0
;Increment the header size within the EXE header
add word ptr cs:[exehead+8], 1
;Change Page Count and Last Page Size in EXE header
cmp word ptr [exehead+2], 496
jae ADDPAGE
add word ptr [exehead+2], 16
jmp HAVEROOM
Oh yeah, there is one more condition that needs to be handled here. If the
last
page was almost full(496 or more bytes), then adding 16 bytes to the file
size
will overflow that page so you have to add a whole new page.
ADDPAGE:
;Adjust the header to add a page if the 16 additional bytes run
;over to a new page.
inc word ptr [exehead+4]
mov ax, 512
sub ax, word ptr [exehead+2]
mov dx, 16
sub dx, ax
mov word ptr [exehead+2], dx
EXE MODIFICATION - STEP 9
-------------------------
Whew! Step 8 was a doozy, but now we're almost done. All Step 9 requires
of
us is to save the original segment values from our victim. DOSGUARD saves
these values in the order that they are found within the EXE header.
mov ax, word ptr [exehead+14] ;save orig stack segment
mov [hosts], ax
mov ax, word ptr [exehead+16] ;save orig stack pointer
mov [hosts+2], ax
mov ax, word ptr [exehead+20] ;save orig ip
mov [hostc], ax
mov ax, word ptr [exehead+22] ;save orig cs
mov [hostc+2], ax
EXE MODIFICATION - STEP 10
--------------------------
It will make things a little easier later on if the end of the file we are
about to modify lies on a paragraph boundary. This way the starting ip for
the
new code that we're adding will always be zero.
;adjust file length to paragraph boundary
mov cx, word ptr cs:[9Ch]
mov dx, word ptr cs:[9Ah]
or dl, 0Fh
add dx, 1
adc cx, 0
mov cs:[9Ch], cx
mov cs:[9Ah], dx
mov ax, 4200h ;move file pointer to end of file
int 21h ;plus boundary
EXE MODIFICATION - STEP 11
--------------------------
Finally, we can write our code to the file. Just like with the COM file, we
will write our code to the end of the file. The difference is in how we get
there when its time to execute it. With COM files we used a jump. With EXE
files we adjust the starting cs:ip to point to our code.
mov cx, offset ENDGUARD2 - offset EXEGUARD ;write code to end
mov dx, offset EXEGUARD ;of the exe file
mov ah, 40h
int 21h
EXE MODIFICATION - STEP 12
--------------------------
With our code neatly tucked after the host program's code, its time to
modify
the EXE header so that our code is the first to execute. We also have to
adjust the size fields in the EXE header to take into account all the code
we
just added.
The first thing to is figure out what the starting segment values need to
be.
The starting cs will simply be the original file size divided by 16 minus
the
header size. The initial ip will be 0 because of Step 11. In DOSGUARD's
case
the ss will be the same as the cs and the sp will point to an address 256
bytes
after the end of our code. 256 bytes is plenty of room for DOSGUARD's
stack.
mov ax, word ptr cs:[9Ah] ;calculate module's CS
mov dx, word ptr cs:[9Ch] ;ax:dx contains orig file size
mov cx, 16 ;CS = file size / 16 - header size
div cx
sub ax, word ptr [exehead+8];header size in paragraphs
mov word ptr [exehead+22], ax ;ax is now initial cs
mov word ptr [exehead+14], ax ;ax is now initial ss
mov word ptr [exehead+20], 0 ;initial ip
mov word ptr [exehead+16], ENDGUARD2 - EXEGUARD + 100h ;initial
sp
This next bit of code calculates the new file size, in pages of course.
;calculate new file size
mov dx, word ptr cs:[9Ch]
mov ax, word ptr cs:[9Ah]
add ax, offset ENDGUARD2 - offset EXEGUARD + 200h
adc dx, 0
mov cx, 200h
div cx
mov word ptr [exehead+4], ax
mov word ptr [exehead+2], dx
add word ptr [exehead+6], 2
EXE MODIFICATION - STEP 13
--------------------------
Now, we should be through with the header so we can write it back out to the
file.
;Write out the new header
mov cx, 1Ch
mov dx, offset exehead
mov ah, 40h
int 21h
EXE MODIFICATION - STEP 14
--------------------------
Last, but not least, we have to modify the relocation pointer table. First,
we need to move the file pointer to where we need to add the new entries.
mov ax, word ptr [exehead+6];Get the # of relocatables
dec ax ;Position to add relocatable equals
dec ax ;(# - 2)*4 + table offset
mov cx, 4
mul cx
add ax, word ptr [exehead+24]
adc dx, 0
mov cx, dx
mov dx, ax
mov ax, 4200h ;move file pointer to position
int 21h
Now, we have to add two pointers to the table. The first points to "hosts,"
which is the stack segment of the original program. The second points to
"hostc+2," which holds the original program's code segment.
;Use exehead as a buffer for relocatables.
;Put two pointers in this buffer, first points to ss in
;hosts and second points to cs in hostc.
mov word ptr [exehead], ENDGUARD2 - EXEGUARD - 10
mov ax, word ptr [exehead+22]
mov word ptr [exehead+2], ax
mov word ptr [exehead+4], ENDGUARD2 - EXEGUARD - 4
mov word ptr [exehead+6], ax
mov cx, 8
mov dx, offset exehead
mov ah, 40h ;Write the 8 bytes.
int 21h
mov ah, 3Eh ;Close the file.
int 21h
RESPONSIBILITIES OF INSERTED CODE
---------------------------------
There are several items which the code module we added must take into
consideration. First of all, when it is finished, the state of registers,
etc.
must be exactly what the original program would expect them to be. For
instance, ax is set by DOS to indicate whether or not the Drive ID stored in
the FCBs is valid. So, the value of ax must be preserved by our code.
Also,
the original program may expect other registers to be set to initial values
of zero. And of course, the segment registers need to be restored after our
code's execution.
In order to actually restore control to the host, our code must restore ss
and
sp to their original values. Then, it jumps to the original cs:ip.
Also, inserted code can't be dependent on absolute addresses for its data.
Therefore, DOSGUARD accesses all data by its offset from the end of the
file.
CONCLUSION
----------
Hopefully, i've explained the techniques I used in developing DOSGUARD well
enough for you to develop your own binary modiying programs. As I mentioned
at
the beginning of this article, DOSGUARD has a lot a room for improvement.
If
you are interested then you should check out my web page and download the
source for ENCGUARD, a more secure version of DOSGUARD. A nice way to
extend
DOSGUARD would be to improve on the encryption techniques used in ENCGUARD.
If
I ever find the time I would like to write a Win32 version of DOSGUARD which
could safely modify the PE file format. If I ever do embark on such a task,
I'll be sure to let the readers of Assembly Programming Journal know about
it.
REFERENCES
----------
"The Giant Black Book of Computer Viruses, 2nd edition" by Mark Ludwig
CONTACT INFORMATION
-------------------
email:
jjsimpso@...
web page:
http://www4.ncsu.edu/~jjsimpso/index.html
Check out my web page for more information on my research into code
modification. Also, feel free to email me with ideas, corrections,
improvements, etc.
---------------------------BEGIN
DOSGUARD.ASM----------------------------------
.model tiny
.code
ORG 100h
START:
jmp BEGINCODE ;Jump the identification string
DB 'CG'
BEGINCODE:
mov dx, offset filter1
call FIND_FILES
mov dx, offset filter2
call FIND_FILES
mov ax, 4C00h ;DOS terminate
int 21h
;-------------------------------------------------------------------------
;Procedure to find and then infect files
;-------------------------------------------------------------------------
FIND_FILES:
mov ah, 4Eh ;Search for files matching filter
int 21h
SLOOP:
jc DONE
mov ax, 3D02h ;Open file R/W
mov dx, 9Eh ;Filename, stored in DTA
int 21h
mov bx, ax ;Save file handle in bx
mov ax, 3F00h ;Read first 5 bytes from file
mov cx, 5
mov dx, offset obytes
int 21h
;Check to see if file is really an EXE
cmp word ptr[obytes], 'ZM'
je EXE
COM:
;Check to see if file is already infected
;if it is, then skip it
cmp word ptr [obytes + 3], 'GC'
je NO_INFECT
;Make sure file isn't too large
mov ax, ds:[009Ah] ;Size of file
add ax, offset ENDGUARD - offset COMGUARD + 100h
jc NO_INFECT ;If ax overflows then don't infect
;If we made it this far then we know the file is safe to modify
call INFECT_COM
jmp NO_INFECT
EXE:
;Read the EXE Header
call READ_HEADER
jc NO_INFECT ;error reading file so skip it
;Make sure it hasn't already been infected
;If (initial CS * 16) + (size of EXEGUARD) + (size of header) ==
size
; then the file has already been infected
mov ax, word ptr [exehead+22]
mov dx, 16
mul dx
add ax, offset ENDGUARD2 - offset EXEGUARD
adc dx, 0
mov cx, word ptr [exehead+8]
add cx, cx
add cx, cx
add cx, cx
add cx, cx
add ax, cx
adc dx, 0
cmp ax, word ptr cs:[9Ah]
jne EXEOK
cmp dx, word ptr cs:[9Ch]
je NO_INFECT
EXEOK:
;Make sure Overlay Number is 0
cmp word ptr [exehead+26], 0
jnz NO_INFECT
;Make sure it is a DOS EXE (as opposed to windows or OS/2
cmp word ptr [exehead+24], 40h
jae NO_INFECT
call INFECT_EXE
NO_INFECT:
mov ax, 4F00h ;Find next file
int 21h
jmp SLOOP
DONE:
ret
;-------------------------------------------------------------------------
;Procedure to infect COM files
;-------------------------------------------------------------------------
INFECT_COM:
xor cx, cx ;cx = 0
xor dx, dx ;dx = 0
mov ax, 4202h ;Move file pointer to the end of
file
int 21h
mov ax, 4000h ;Write the code to the end of file
mov dx, offset COMGUARD
mov cx, offset ENDGUARD - offset COMGUARD
int 21h
mov ax, 4200h ;Move file pointer to beginning of
xor cx, cx ; file to write jump
xor dx, dx
int 21h
;Prepare the jump instruction to be written to beginning of file
xor ax, ax
mov byte ptr [bytes], 0E9h ;opcode for jmp
mov ax, ds:[009Ah] ;size of the file
sub ax, 3 ;size of the jump instruction
mov word ptr [bytes + 1], ax;size of the jump
;Write the jump
mov cx, 5; ;size to be written
mov dx, offset bytes
mov ax, 4000h
int 21h
mov ah, 3Eh ;Close file
int 21h
ret
;-------------------------------------------------------------------------
;Procedure to infect EXE files
;-------------------------------------------------------------------------
INFECT_EXE:
;Check the relocation pointer table to see if there is
;room. If there isn't then we'll have to make room.
mov ax, word ptr [exehead+8];size of header in paragraphs
add ax, ax ;
add ax, ax ;Convert to double words.
sub ax, word ptr [exehead+6];Subtract # of entries each of
add ax, ax ;which is a double word and then
add ax, ax ;convert the final total to bytes.
sub ax, word ptr [exehead+24];If there are 8 bytes left after
cmp ax, 8 ;you subtract the offset to the
jc NOROOM ;reloc table then there is room.
jmp HAVEROOM
NOROOM:
;Not enough room in the relocation table so we are going to
;have to add a paragraph to the table. As a result, we must
;read in the whole file after the relocation table and write
;it back out one paragraph down in memory.
xor cx, cx ;Move the file pointer to the end of
mov dx, word ptr [exehead+24] ;the relocation pointer table.
mov ax, word ptr [exehead+6];size of relocation table in doubles
add ax, ax ;* 4 to get bytes
add ax, ax
add dx, ax ;add that to start of table
push dx
mov ax, 4200h
int 21h
pop dx
call CALC_SIZE
cmp cx, 1
je LASTPAGE
mov dx, offset buffer
call READ_PAGE
mov dx, offset para
call READ_PARA
call DECFP_PAGE
call WRITE_PAGE
call MOVE_PARA
dec cx
cmp cx, 1
je LASTPAGE
MOVELOOP:
mov dx, offset buffer + 16
call READ_PAGE
call DECFP_PAGE
call WRITE_PAGE
call MOVE_PARA
dec cx
cmp cx, 1
jne MOVELOOP
LASTPAGE:
sub word ptr [lps], 16
mov cx, word ptr [lps]
mov dx, offset buffer + 16
mov ah, 3Fh
int 21h
push cx
mov dx, cx
neg dx
mov cx, -1
mov ax, 4201h
int 21h
pop cx
add cx, 16
mov dx, offset buffer
mov ah, 40h
int 21h
;Got to adjust the file size since it will be used later
add word ptr cs:[9Ah], 16
adc word ptr cs:[9Ch], 0
;Increment the header size within the EXE header
add word ptr cs:[exehead+8], 1
;Change Page Count and Last Page Size in EXE header
cmp word ptr [exehead+2], 496
jae ADDPAGE
add word ptr [exehead+2], 16
jmp HAVEROOM
ADDPAGE:
;Adjust the header to add a page if the 16 additional bytes run
;over to a new page.
inc word ptr [exehead+4]
mov ax, 512
sub ax, word ptr [exehead+2]
mov dx, 16
sub dx, ax
mov word ptr [exehead+2], dx
HAVEROOM:
mov ax, word ptr [exehead+14] ;save orig stack segment
mov [hosts], ax
mov ax, word ptr [exehead+16] ;save orig stack pointer
mov [hosts+2], ax
mov ax, word ptr [exehead+20] ;save orig ip
mov [hostc], ax
mov ax, word ptr [exehead+22] ;save orig cs
mov [hostc+2], ax
mov cx, word ptr cs:[9Ch] ;adjust file length to paragraph
mov dx, word ptr cs:[9Ah] ; boundary
or dl, 0Fh
add dx, 1
adc cx, 0
mov cs:[9Ch], cx
mov cs:[9Ah], dx
mov ax, 4200h ;move file pointer to end of file
int 21h ;plus boundary
mov cx, offset ENDGUARD2 - offset EXEGUARD ;write code to end
mov dx, offset EXEGUARD ;of the exe file
mov ah, 40h
int 21h
xor cx, cx ;Move file pointer to beginning of
file
xor dx, dx
mov ax, 4200h
int 21h
;adjust the EXE header and then write it back out
mov ax, word ptr cs:[9Ah] ;calculate module's CS
mov dx, word ptr cs:[9Ch] ;ax:dx contains orig file size
mov cx, 16 ;CS = file size / 16 - header size
div cx
sub ax, word ptr [exehead+8];header size in paragraphs
mov word ptr [exehead+22], ax ;ax is now initial cs
mov word ptr [exehead+14], ax ;ax is now initial ss
mov word ptr [exehead+20], 0 ;initial ip
mov word ptr [exehead+16], ENDGUARD2 - EXEGUARD + 100h ;initial
sp
mov dx, word ptr cs:[9Ch] ;calculate new size file size
mov ax, word ptr cs:[9Ah]
add ax, offset ENDGUARD2 - offset EXEGUARD + 200h
adc dx, 0
mov cx, 200h
div cx
mov word ptr [exehead+4], ax
mov word ptr [exehead+2], dx
add word ptr [exehead+6], 2
mov cx, 1Ch ;Write out the new header
mov dx, offset exehead
mov ah, 40h
int 21h
;modify relocatables table
mov ax, word ptr [exehead+6];Get the # of relocatables
dec ax ;Position to add relocatable equals
dec ax ;(# - 2)*4 + table offset
mov cx, 4
mul cx
add ax, word ptr [exehead+24]
adc dx, 0
mov cx, dx
mov dx, ax
mov ax, 4200h ;move file pointer to position
int 21h
;Use exehead as a buffer for relocatables.
;Put two pointers in this buffer, first points to ss in
;hosts and second points to cs in hostc.
mov word ptr [exehead], ENDGUARD2 - EXEGUARD - 10
mov ax, word ptr [exehead+22]
mov word ptr [exehead+2], ax
mov word ptr [exehead+4], ENDGUARD2 - EXEGUARD - 4
mov word ptr [exehead+6], ax
mov cx, 8
mov dx, offset exehead
mov ah, 40h ;Write the 8 bytes.
int 21h
mov ah, 3Eh ;Close the file.
int 21h
ret ;Done!
;-------------------------------------------------------------------------
;Procedure to calculate the amount that needs to be written
;-------------------------------------------------------------------------
CALC_SIZE:
;dx holds the position in the file where we want to start reading.
;So, the amount to read in and write back out is equal to the size
;of the file minus dx.
mov cx, word ptr [exehead+2]
mov word ptr [lps], cx ;Copy Last Page Size into lps
mov cx, word ptr [exehead+4];Copy Num Pages into cx
cmp dx, word ptr [lps] ;If bytes to subtract are less than
jbe FINDLPS ;lps then just subtract them and
exit
mov ax, dx
xor dx, dx
mov cx, 512
div cx ;ax = pages to subtract
mov cx, word ptr [exehead+4];dx = remainder to subtract from lps
sub cx, ax
cmp dx, word ptr [lps]
jbe FINDLPS
sub cx, 1
mov ax, dx
sub ax, word ptr [lps]
mov dx, 512
sub dx, ax
FINDLPS:
sub word ptr [lps], dx ;Subtract start position and leave
;Num Pages the same
ret
;-------------------------------------------------------------------------
;Procedure to read the EXE Header
;-------------------------------------------------------------------------
READ_HEADER:
xor cx, cx ;Move the file pointer back
xor dx, dx ;to the beginning of the file
mov ax, 4200h
int 21h
mov cx, 1Ch ;read exe header (28 bytes)
mov dx, offset exehead ;into buffer
mov ah, 3Fh
int 21h
ret ;return with cf set properly
;-------------------------------------------------------------------------
;Procedure to read a page
;-------------------------------------------------------------------------
READ_PAGE:
push ax
push cx
mov ah, 3Fh
mov cx, 512
int 21h
pop cx
pop ax
ret
;-------------------------------------------------------------------------
;Procedure to read a paragraph
;-------------------------------------------------------------------------
READ_PARA:
push ax
push cx
mov ah, 3Fh
mov cx, 16
int 21h
pop cx
pop ax
ret
;-------------------------------------------------------------------------
;Procedure to write a page
;-------------------------------------------------------------------------
WRITE_PAGE:
push ax
push cx
push dx
mov ah, 40h
mov cx, 512
mov dx, offset buffer
int 21h
pop dx
pop cx
pop ax
ret
;-------------------------------------------------------------------------
;Procedure to write a paragraph
;-------------------------------------------------------------------------
WRITE_PARA:
push ax
push cx
push dx
mov ah, 40h
mov cx, 16
mov dx, offset buffer
int 21h
pop dx
pop cx
pop ax
ret
;-------------------------------------------------------------------------
;Procedure to move file pointer back a page
;-------------------------------------------------------------------------
DECFP_PAGE:
push ax
push cx
push dx
mov ax, 4201h
mov cx, -1
mov dx, -512
int 21h
pop dx
pop cx
pop ax
ret
;-------------------------------------------------------------------------
;Procedure to move file pointer back a para
;-------------------------------------------------------------------------
DEC_PARA:
push ax
push cx
push dx
mov ax, 4201h
mov cx, -1
mov dx, -16
int 21h
pop dx
pop cx
pop ax
ret
;-------------------------------------------------------------------------
;Procedure to move the paragraph buffer to the front
;-------------------------------------------------------------------------
MOVE_PARA:
push cx
mov si, offset para
mov di, offset buffer
mov cx, 16
rep movsb
pop cx
ret
;-------------------------------------------------------------------------
;Code to add to COM files
;-------------------------------------------------------------------------
COMGUARD:
call GET_START
GET_START:
pop bp
sub bp, offset GET_START
mov ah, 9h ;DOS print string
lea dx, [bp + prompt] ;Print the password prompt
int 21h
lea di, [bp + guess]
xor cx, cx
READLOOP:
mov ah, 7h ;Read without echo
int 21h
inc cx ;Count of characters entered
stosb ;Store guess for comparison later
cmp cx, 10 ;Limit guess to 10 chars including
CR
je CHECKPASS
cmp al, 13 ;Quit loop when CR read
jne READLOOP
CHECKPASS:
lea di, [bp + guess] ;Setup for passwd checking loop
lea si, [bp +passwd] ;Setup addresses for cmpsb
xor cx, cx ;Set counter to zero
cld ;Tell cmpsb to increment si and di
CHECKLOOP:
cmpsb ;Compare passwd with guess
jne FAIL ;Abort program if password is wrong
inc cx ;Increment counter
cmp cx, 8 ;Only check first 8 chars
jne CHECKLOOP ;Loop until you've read first 8
SUCCESS:
mov cx, 5
cld
lea si, [bp + obytes]
mov di, 100h
rep movsb
push 100h ;return from the jump to execute
ret ;the host program
FAIL:
mov ah, 9h ;DOS print string
lea dx, [bp + badpass] ;Print bad password msg
int 21h
mov ax, 4C00h
int 21h
prompt DB 'password: ','$'
badpass DB 'Invalid password!','$'
passwd DB 'smcrocks'
guess DB 10 dup (0)
obytes DB 0,0,0,0,0
ENDGUARD:
;-------------------------------------------------------------------------
;Code to add to EXE files
;-------------------------------------------------------------------------
EXEGUARD:
push ax ;Save startup value in ax
push ds ;Save value of ds
mov ax, cs ;Put cs into ds and es
mov ds, ax
mov es, ax
mov bp, offset ENDGUARD2 - offset EXEGUARD
mov ax, [bp-4]
mov ah, 9h ;DOS print string
lea dx, [bp-57] ;Print the password prompt
int 21h
lea di, [bp-20]
xor cx, cx
EREADLOOP:
mov ah, 7h ;Read without echo
int 21h
inc cx ;Count of characters entered
stosb ;Store guess for comparison later
cmp cx, 10 ;Limit guess to 10 chars including
CR
je ECHECKPASS
cmp al, 13 ;Quit loop when CR read
jne EREADLOOP
ECHECKPASS:
lea di, [bp-20] ;Setup for passwd checking loop
lea si, [bp-28] ;Setup addresses for cmpsb
xor cx, cx ;Set counter to zero
cld ;Tell cmpsb to increment si and di
ECHECKLOOP:
cmpsb ;Compare passwd with guess
jne EFAIL ;Abort program if password is wrong
inc cx ;Increment counter
cmp cx, 8 ;Only check first 8 chars
jne ECHECKLOOP ;Loop until you've read first 8
ESUCCESS:
pop ds
mov ax, ds
mov es, ax
pop ax
cli
mov ss, word ptr cs:[bp-10]
mov sp, word ptr cs:[bp-8]
sti
xor cx, cx
xor dx, dx
xor bp, bp
xor si, si
xor di, di
lahf
xor ah, ah
sahf
jmp dword ptr cs:[ENDGUARD2-EXEGUARD-6]
EFAIL:
mov ah, 9h ;DOS print string
lea dx, [bp-46] ;Print bad password msg
int 21h
mov ax, 4C00h
int 21h
eprompt DB 'password: ','$'
ebadpass DB 'Invalid password!','$'
epasswd DB 'smcrocks'
eguess DB 10 dup (0)
hosts DW 0, 0
hostc DW 0, 0
delta DW 0
ENDGUARD2:
filter1 DB '*.com',0
filter2 DB '*.exe',0
bytes DB 0,0,0,'CG'
exehead DB 28 dup (0)
buffer DB 512 dup (0)
para DB 16 dup (0)
lps DW 0
END START
---------------------------END
DOSGUARD.ASM------------------------------------
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
Creating a User-Friendly
Interface
by S Sirajudeen
Now a days, a programmer of any language has to include user friendly
features in his commercial software, since users desire user friendliness
for easy use. For example, Windows is the most popular OS due to its
Graphical
User Interface.
For an assembly language programmer who tries to develop a DOS-based
program, it is drudgery and challenging to incorporate even a few basic
features of graphical interface like that of Windows.
Sometimes, in assembly language, the time taken to develop the core
of a software may be very less than writing code for its user interface. For
instance, assume that we're writing an addition program which displays a
dialog box to input two numbers and displays result in a dialog box.
Here,the
dialog box is the user interface. What we have to do in this program is,
* Displaying a dialog box.
* Receiving the numbers to be added as string.
* Checking the string whether it contains alphabets and graphics
characters. If so, prompting the user to reenter the numbers.
* Converting the ASCII digits into binary form.
* Performing binary multibyte addition..
* Converting sum which is binary into ASCII digits.
* Displaying sum in a dialog box.
Our intention is only the addition of two numbers. But we have to spend more
time in the user interface design than for addition.
As I say these things, you may become frustrated and decide to skip
user
interface design. Still, in developing utilities or packages for commercial
purpose, a programmer will have to do these things to accomodate users. This
is why I present this article.
This article will focus on user friendly features in DOS text mode.
In DOS text mode, user friendly means features such as menus, message box,
dialog box, list box, text window, radio button, status bar, mouse support
etc.
In this article, I will cover only an about message box and a dialog
box.
However, knowledge of interrupts (for screen and mouse handling) is
essential,
even for a C/C++ programmer, to incorporate user friendly features in a DOS
based program.
GETTING STARTED:
Before going on, some things must be cleared.
i) A text can be displayed in one of the following ways
1) Direct access of video memory
2) Using INT 21h
3) Using INT 10h
In the examples of this article, I have used the function 0Eh of INT 10h
to display text.
ii) To make the example programs as straightforward, I have used |, -
and + as the box characters in the dialog box, since actual box
characters
are EXTENDED ASCII characters which are not allowed in a text article.
The content of dialog box is labeled as DIALOG_BOX_TEXT.
Before compiling this program, in the content of the dialog box,
PLEASE REPLACE the characters |, - and + with the BOX CHARACTERS which
are specified below.
--------------------------------------
ASCII code Description
--------------------------------------
179 | Vertical bar
196 -- Horizontal bar
218 | Upper left corner
191 | Upper right corner
192 |_ Lower left corner
217 _| Lower right corner
--------------------------------------
EXAMPLE 1:
First of all, we're going to put a zooming message box in our program.
It
is an introduction to second example.
You may be seen that some utlities such as Norton Utilities display
zooming message box to alert users.
What this program does is
- n boxes of different size, are continously displayed one after
another for n seconds each. In this case, each time a box
which is larger than previous one is displayed.
It seems like the box is zooming.
LOGIC:
Assume that displaying boxes which are larger than previously
displayed box, means enalarging/zooming the previously displayed
box.
i) Zoom box by n rows
ii) Zoom box by n columns
iii) Zooming box for n times
- It displays horizontal and vertical shadows for the box
- Finally displays text within the box
What you will learn:
i) Screen handling using BIOS interrupt 10h
ii) An introduction to learn the second example.
Below is the source code of our simple program.
;;
+------------------------------------------------------------------------+
;; | Program : MSGBOX.ASM
|
;; | Purpose : Demonstration program about Message Box
|
;; | Assembler : TASM
|
;;
+------------------------------------------------------------------------+
;; MACROS in this program : @SetTextMode, @Cursor, @Display, @Window,
@Delay
;; PROCEDURES in this program: Message_box, Window
;;///////////////////////////////////////////////////////////////////////;;
.386
MODEL USE16 TINY ;; @Always must be TINY model
;;///////////////////////////////////////////////////////////////////////;;
DATASEG ;; Initialize variables
RED EQU 4fh ;; @Color values
BLACK EQU 0fh
BLUE EQU 1fh
screen EQU BLUE
shadow_colour EQU BLACK
box_background_colour EQU RED
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
nl EQU 0Dh,0Ah
label dialog_box_text
db nl
db nl,'
+-------------------------+--------------------------------------+'
db nl,' | ::/ \::::::. | Program to Display a Message Box
|'
db nl,' | :/___\:::::::. |
|'
db nl,' | /| \::::::::. | Written By S.SIRAJUDEEN.
|'
db nl,' | :| _/\:::::::::. | E-Mail:
ssirajudeen@...
|'
db nl,' | :| _|\ \::::::::::. |
|'
db nl,' | :::\_____\::::::::::. | Published in ASMJOURNAL
|'
db nl,' | ::::::::::::::::::::::. | Internet:
asmjournal.freeservers.com |'
db nl,' | AsmJournal |
|'
db nl,'
+-------------------------+--------------------------------------+'
db nl,' | # If you have any comments or suggestions then please email
me|'
db nl,' | at
ssirajudeen@...
|'
db nl,'
+----------------------------------------------------------------+'
db nl,nl,nl,nl
count dw $-offset dialog_box_text
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
upper_x equ 08 ;; Upper left corner of the box to be zoomed
upper_y equ 37
lower_x equ 08 ;; Lower right corner of the box to be zoomed
lower_y equ 39
left_x db upper_x ;; Variables to hold the UPPER LEFT coordinates of the
left_y db upper_y ;; next box to be displayed
right_x db lower_x ;; Variables to hold the LOWER RIGHT coordinates of the
right_y db lower_y ;; next box to be displayed
shadow_vertical_left_x db upper_x+1 ;; Don't Change!
shadow_vertical_left_y db lower_y+1 ;; Coordinates to display the
VERTICAL
shadow_vertical_right_x db lower_x+1 ;; shadow of message box.
shadow_vertical_right_y db lower_y+2
shadow_horizontal_left_x db lower_x+1 ;; Don't Change!
shadow_horizontal_left_y db upper_y+2 ;; Coordinates to display the
HORIZONTAL
shadow_horizontal_right_x db lower_x+1 ;; shadow of message box
shadow_horizontal_right_y db lower_y+2
;;//////////////////////////////////////////////////////////////////////;;
UDATASEG
DW 100H DUP (?)
MyStack LABEL WORD
;;--------------------------< @SetTextMode >------------------------;;
@SetTextMode MACRO
mov ax,0003h
int 10h
ENDM ;;End of macro
;;----------------------------< @Cursor >---------------------------;;
;;PURPOSE : Macro to move cursor
;;SYNTAX : @Cursor <row>, <col>
@Cursor MACRO ROW,COL
mov ah,02
mov bh,00
mov dh,ROW
mov dl,COL
int 10h
ENDM ;;End of macro
;;----------------------------< @Display >---------------------------;;
;;PURPOSE: Macro to display a text
;;SYNTAX : @DISPLAY <text width>, <text address>
@Display MACRO xcount, address
LOCAL display_text
mov cx, xcount ;; Number of characters to be displayed
mov bx, offset address
display_text:
mov ah,0Eh ;; Display the text
mov al,byte ptr [bx]
push bx
mov bh,00
mov bl,07h
int 10h
pop bx
inc bx ;; Point to next character
loop far ptr cs:display_text
ENDM ;;End of macro
;;-----------------------------< @Window >----------------------------;;
;;PURPOSE : Macro to display a window with a given color as background
;;SYNTAX : @window <bacground color>,
;; <Upper letf row of user window>, <Upper left column>,
;; <Lower right row of user window>, <Lower right column>
@window MACRO color, lrow, lcol, rrow, rcol
mov ah,06
mov al,00
mov bh, color ;; Background Color
mov ch, lrow
mov cl, lcol
mov dh, rrow
mov dl, rcol
int 10h
ENDM ;;End of macro
;;-----------------------------< @Delay >-----------------------------;;
@delay MACRO
mov ah,86h ;; Execute a time delay
mov dx,4500h ;;9000
mov cx,0000h
int 15h
ENDM ;;End of macro
;;///////////////////////// MAIN PROGRAM /////////////////////////////;;
CODESEG ;; This marks the start of executable code
STARTUPCODE
mov sp,offset MyStack
push cs ;; Initialize segment registers.
pop ds
push cs
pop ss
mov ah,0Bh ;; Display screen border in WHITE color
mov bx,0007h
int 10h
call message_box ;; Display the message box
mov ax,4C00h ;; Terminate the program.
int 21h
;;//////////////////////////// Message_box ///////////////////////////;;
Message_box PROC
@SetTextMode
@cursor 00,00 ;; Position cursor at 00,00.
@window screen,00,00,24,79 ;; @Clear screen
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
mov cx,0008h ;; Don't change! Calculate how many times to zoom.
zoom:
push cx ;; @@Display a window which is zooming.
call window
pop cx
loop zoom
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
@display count, dialog_box_text
ret
Message_box ENDP
;;/////////////////////////// Window ///////////////////////////////;;
Window PROC
;;Display a window with BLUE colour as background.
@window box_background_colour, left_x, left_y, right_x, right_y
dec byte ptr left_x
sub cl,5
mov byte ptr left_y,cl
inc byte ptr right_x
add dl,5
mov byte ptr right_y,dl
;;---------------------------------------------------------------------;;
;;Display a horizontal shadow.
@window shadow_colour,shadow_vertical_left_x,shadow_vertical_left_y,
shadow_vertical_right_x,shadow_vertical_right_y
dec byte ptr shadow_vertical_left_x
add cl,5
mov byte ptr shadow_vertical_left_y,cl
inc byte ptr shadow_vertical_right_x
add dl,5
mov byte ptr shadow_vertical_right_y,dl
;;--------------------------------------------------------------------;;
;;Display a horizontal shadow.
@window shadow_colour,shadow_horizontal_left_x,
shadow_horizontal_left_y,
shadow_horizontal_right_x,shadow_horizontal_right_y
inc byte ptr shadow_horizontal_left_x
sub cl,5
mov byte ptr shadow_horizontal_left_y,cl
inc byte ptr shadow_horizontal_right_x
add dl,5
mov byte ptr shadow_horizontal_right_y,dl
;;--------------------------------------------------------------------;;
@delay
ret
Window ENDP
END
;;////////////////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\;;
EXAMPLE 2:
Well, next we're going to put a DIALOG BOX in our program.
What it does is:
- Displays a dialog box with YES and NO buttons
- Supports button selection using mouse
(i) Checks for mouse installation
(ii) Shows mouse pointer
(iii) Captures button click of the left mouse button
- Checks for keyboard input
(i) Checks whether EXTENDED keys has pressed
(ii) Checks whether ENTER or TAB key has pressed
- Toggles button selection, on presssing TAB, LEFT ARROW key or RIGHT
ARROW key.
- On pressing ENTER key or clicking OK/YES button, displays different
messages according to button selection and terminates.
What we will learn from this example is:
(i) Mouse handling
(ii) Screen handling using BIOS interrupt 10h
(iii) Key board handling using BIOS interrupt 16h
(iv) Idea of user interface design
I made the following program very straightforward and ignored code
optimization to reduce complexity.
;;
+-------------------------------------------------------------------------+
;; | Program : DLGBOX.ASM
|
;; | Purpose : Demonstration program about Dialog Box with YES & NO button
|
;; | Features : Supports mouse for button selection
|
;; | Assembler : TASM
|
;; | Required Knowledge: INT 21h, INT 10h, INT 16h, INT 33h & Scan Code
|
;;
+-------------------------------------------------------------------------+
;; MACROS in this program : @Cursor, @Display, @window, @Yes & @No
;; PROCEDURES in this program: Dialog_box
;;///////////////////////////////////////////////////////////////////////;;
.386
MODEL USE16 TINY ;; @Always must be TINY model
;;///////////////////////////////////////////////////////////////////////;;
DATASEG ;; Initialize variables
mouse db 'n' ;; Flag to indicate the availability of mouse
mouse_x db 0 ;; Keep track of position of mouse cursor
mouse_y db 0
m_x dw 00
m_y dw 00
left_mouse_button db 0 ;; Flag updated on clicking the left mouse button
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
RED EQU 4fh ;; @Color values
CYAN EQU 3fh
BLACK EQU 0fh
BLUE EQU 1fh
WHITE EQU 7fh
box_height EQU 10
box_width EQU 46
left_x EQU 7 ;; Upper left corner of user window
left_y EQU 20
right_x EQU left_x+box_height-1 ;; Calculate lower right corner of user
window
right_y EQU left_y+box_width-1
upper_left_row db left_x
upper_left_col db left_y
box_background_color EQU RED ; Background color of dialog box
nl EQU 0Dh,0Ah ; New line
label dialog_box_text
db '+--------------- USER COMMENT ---------------+' ;Dialog box. The
variable
db '| |' ;dialog_box_text
contains
db '| Written By S.Sirajudeen |' ;10 lines; width of each
db '| E-mail:
ssirajudeen@... |' ;line is 46 characters.
db '| |'
db '| HAVE YOU ENJOYED THIS PROGRAM? |' ;NOTE:
db '| |' ;If you edit here, you
db '| Yes # No # |' ;should UPDATE the
db '| ####### ####### |' ;text_width and
db '+--------------------------------------------+' ;text_line_count.
count dw $-offset dialog_box_text
text_line_count EQU 10 ;; Variable dialog_box_text contains 10 lines
text_width EQU 46 ;; and width of each line is 46 characters
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
shadow EQU WHITE ;; color of button shadow
;;NOTE: Width of 'yes' and 'yes_button' should be same.
yes_button db 17,' Yes ',16 ;; Displayed on YES button has selected
yes db ' Yes '
yes_horz_shadow db 7 dup(223)
yes_char_count EQU 7
;;NOTE: Width of 'no' and 'no_button' should be same.
no_button db 17,' No ',16 ;; Displayed on NO button has selected
no db ' No '
no_horz_shadow db 7 dup(223)
no_char_count EQU 7
vert_shadow db 220
yes_x EQU right_x-2 ;; Coordinate where YES button to displayed
yes_y EQU left_y+(box_width/2)-yes_char_count-4 ;;32
no_x EQU right_x-2 ;; Coordinate where NO button to displayed
no_y EQU left_y+(box_width/2)+1 ;;44
select EQU BLUE ;; @Background color to highlight the button selection
unselect EQU BLACK
button db 'y' ;; @Flag to keep track of the button selection. If the value
;; is 'y', the YES button has selected; 'n' for the NO button.
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
label thank_you ;; Message to be displayed upon YES button has pressed
db 07,' Written By S.SIRAJUDEEN',nl
db
'4/55,L.M.BUILDING,KUMARESAPURAM,KUTHAPAR(PO),TRICHY-620013,TAMILNADU,INDIA'
db nl,' Email:
ssirajudeen@...'
db nl,nl,' Thank you! Good-bye!!'
thank_you_count dw $-thank_you
label suggest ;; Message to be displayed upon NO button has pressed
db 7h,' If you have any comments or suggestions, then please mail me
at'
db nl,'
ssirajudeen@...'
db nl
suggest_count dw $-suggest
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
;; -------------+----------- When a key has pressed, it returns a code.
;; |Extended Keys| Scan Code | This code is called SCAN CODE.
;; |-------------+-----------| Alphanumeric keys, tab, space and escape
;; | Left Arrow | 75 | keys return one byte code. But Extended
;; | Right Arrow | 77 | keys return two bytes code. The first byte
;; | Up Arrow | 72 | always 0. The second is the actual scan code.
;; | Down Arrow | 80 | Arrow keys, Home, End, PageUp, Page Down,
;; -------------+----------- Insert, Delete, Function keys, Pause/break,
;; Scroll Lock & Print Screen are called
EXTENDED
;; KEYs.
LEFT_ARROW equ 75 ;; Scan code of LEFT ARROW key is 75
RIGHT_ARROW equ 77 ;; ,, RIGHT ARROW keyis 77
TAB_KEY equ 9 ;; Scan code of TAB key is 9
ENTER_KEY equ 13 ;; ,, ENTER key is 13
;;//////////////////////////////////////////////////////////////////////;;
UDATASEG
DW 50H DUP (?)
MyStack LABEL WORD
;;----------------------------< @Cursor >---------------------------;;
;;PURPOSE : Macro to move cursor
;;SYNTAX : @Cursor <row>, <col>
@Cursor MACRO ROW,COL
mov ah,02
mov bh,00
mov dh,ROW
mov dl,COL
int 10h
ENDM ;;End of macro
;;----------------------------< @Display >---------------------------;;
;;PURPOSE: Macro to display a text
;;SYNTAX : @DISPLAY <text width>, <text address>
@Display MACRO xcount, address
LOCAL display_text
mov cx, xcount ;; Number of characters to be displayed
mov bx, offset address
display_text:
mov ah,0Eh ;; Display the text
mov al,byte ptr [bx]
push bx
mov bh,00
mov bl,07h
int 10h
pop bx
inc bx ;; Point to next character
loop far ptr cs:display_text
ENDM ;;End of macro
;;----------------------------< @window >-----------------------------;;
;;PURPOSE : Macro to display a window with a given color as background
;;SYNTAX : @window <bacground color>,
;; <Upper letf row of user window>, <Upper left column>,
;; <Lower right row of user window>, <Lower right column>
@window MACRO color, lrow,lcol, rrow, rcol
mov ah,06
mov al,00
mov bh, color ;;Background Color
mov ch, lrow
mov cl, lcol
mov dh, rrow
mov dl, rcol
int 10h
ENDM ;;End of macro
;;------------------------< @button_shadow >--------------------------;;
;;PURPOSE ; Macro to pad the button with horizontal and vertical char to
;; make it as 3D button.
@button_shadow MACRO
@Cursor yes_x+1, yes_y+1 ;; Display horizontal shadow of YES
button
@Display yes_char_count, yes_horz_shadow
@Cursor yes_x, yes_y+yes_char_count ;; Display vertical shadow
@Display 1, vert_shadow
@Cursor no_x+1, no_y+1 ;; Display horizontal shadow of NO
button
@Display no_char_count, no_horz_shadow
@Cursor no_x, no_y+no_char_count ;; Display vertical shadow
@Display 1, vert_shadow
ENDM
;;-----------------------------< @Yes >-------------------------------;;
;;PURPOSE : Macro to select the YES button.
;; In other words, a window which is used as YES button is
displayed
@Yes MACRO
mov button, 'y' ;; DON'T CHANGE! ; Update flag
@window select, yes_x, yes_y, yes_x, yes_y+(yes_char_count-1)
@window unselect, no_x, no_y, no_x, no_y+(no_char_count-1)
@Cursor yes_x,yes_y ;; Move cursor to YES button
@Display yes_char_count,yes_button ;; Display label of YES
@Cursor no_x,no_y ;; Move cursor to NO button
@Display no_char_count, no ;; Display label of NO button
ENDM ;;End of macro
;;-----------------------------< @No >--------------------------------;;
;;PURPOSE : Macro to select the NO button
;; In other words, a window which is used as NO button is displayed
@No MACRO
mov button, 'n' ;; DON'T CHANGE! ; Update flag
@window unselect,yes_x, yes_y, yes_x, yes_y+(yes_char_count-1)
@window select, no_x, no_y, no_x, no_y+(no_char_count-1)
@Cursor yes_x,yes_y
@Display yes_char_count, yes
@Cursor no_x,no_y
@Display no_char_count, no_button
ENDM ;;End of macro
;;//////////////////////// MAIN PROGRAM /////////////////////////////;;
CODESEG ;;This marks the start of executable code
STARTUPCODE
mov sp,offset MyStack
push cs ;;Initialize segment registers.
pop ds
push cs
pop ss
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
@window BLACK, 00, 00, 24, 79 ;;@Clear screen
call Dialog_box ;;Display the dialog box
display_thank_u:
cmp button,'y' ;; Check whether YES button has pressed/clicked
jne display_suggestion
@Display thank_you_count, thank_you
jmp _end
display_suggestion: ;; NO button has pressed/clicked
@Display suggest_count, suggest
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
_end:
mov ax,4C00h ;; Terminate the program.
int 21h
;;/////////////////////////// Dialog_box ////////////////////////////;;
Dialog_box PROC
mov ax,0003 ;; Don't change! Set text mode in 3. Changing this mode
int 10h ;; causes different resolution. Mouse movement is converted
;; into rows and columns based on the resolution of text
mode.
mov ax,00 ;; Reset mouse
int 33h
cmp ax,00 ;; Check for error
je start
mov ax,01 ;; Show mouse pointer
int 33h
mov mouse, 'y'
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
start:
@window box_background_color,left_x,left_y,right_x,right_y ;;Display a
BOX
@Cursor left_x,left_y ;; Move cursor to upper left corner of dialog
box
mov cx, text_line_count ;; Display n lines as dialog box text
mov bx, offset dialog_box_text ;; Address of text
next_line:
push bx ;; OUTER LOOP
push cx
mov cx,00 ;; INNER LOOP
mov cl, text_width
display_text:
mov ah,0Eh ;; Display the text
mov al,byte ptr [bx]
push bx
mov bh,00
mov bl,07h
int 10h
pop bx
inc bx
loop far ptr cs:display_text ;; INNER LOOP
pop cx
pop bx
mov dx,00 ;; Calculate address of next line
mov dl, text_width
add bx, dx
inc byte ptr upper_left_row
push bx
@Cursor upper_left_row, upper_left_col ;; Move cursor to next line
within
pop bx ;; dialog box
loop far ptr cs:next_line ;; OUTER LOOP
@button_shadow
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
_yes:
@Yes ;; Select the YES button
cmp left_mouse_button,01
je _end_proc
jmp mouse_check
_no:
@No ;;Select the NO button
cmp left_mouse_button,01
je _end_proc
mouse_check:
cmp mouse, 'y' ;; Check whether mouse is available
jne key_check
mov ax,03 ;; Get mouse cursor position
int 33h
mov left_mouse_button,bl
mov word ptr m_x,dx
mov word ptr m_y,cx
mouse_button:
and left_mouse_button, 01 ;; Check whether left mouse button has
pressed
cmp left_mouse_button, 01
jne key_check
mouse_row:
mov mouse_x,0 ;; Mouse movement is converted into rows and
columns
;; to calculate the position of mouse cursor
cmp word ptr m_x,00
je mouse_col
mov ax,word ptr m_x ;; In the text mode 3, to calculate the current
ROW,
mov bl,8 ;; divide the position value for VERTICAL
movement
div bl ;; by 8.
mov mouse_x, al
mouse_col:
mov mouse_y,0 ;; Mouse movement is converted into rows and
columns
;; to calculate the position of mouse cursor
cmp word ptr m_y,00
je key_check
mov ax, word ptr m_y ;; In the text mode 3, to calculate the current
COLUMN,
mov bl,8 ;; divide the position value for HORIZONTAL
movement
div bl ;; by 8.
mov mouse_y, al
mouse_yes:
mov al, mouse_x
cmp al, yes_x ;; Check whether mouse has clicked anywhere on
jne mouse_no ;; the row where YES button is displayed
mov al, mouse_y
cmp al, yes_y
jb mouse_no
cmp al, yes_y+(yes_char_count-1)
ja mouse_no
mov button, 'y'
jmp _yes
mouse_no:
mov al, mouse_x
cmp al, no_x ;; Check whether mouse has clicked anywhere on
jne key_check ;; the row where NO button is displayed
mov al, mouse_y
cmp al, no_y
jb key_check
cmp al, no_y+(no_char_count-1)
ja key_check
mov button, 'n'
jmp _no
key_check:
mov ah,01 ;; @Check whether any character is in keyboard
buffer
int 16h
jz mouse_check
mov ah,08 ;; @Receive character without echoing to screen
int 21h
cmp al, TAB_KEY ;; Check whether TAB key has pressed
je _left
cmp al,ENTER_KEY ;; Check whether ENTER key has pressed.
je _end_proc ;; Exit program
cmp al,00 ;; @Check whether any Extended Key has pressed.
jne mouse_check
mov ah,08
int 21h
cmp al, LEFT_ARROW ;; Check whether LEFT ARROW key has pressed
je _left
cmp al, RIGHT_ARROW ;; Check whether RIGHT ARROW key has pressed
je _right
jmp mouse_check
;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;
_left:
cmp button,'y'
je _no
jmp _yes
_right:
cmp button,'y'
je _no
jmp _yes
;;- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -;;
_end_proc:
@Cursor right_x+1, 0 ;; Move cursor below the dialog box
mov ax,02 ;; Hide mouse cursor
int 33h
RET
Dialog_box ENDP ;; End of procedure
END ;; End of program
;;////////////////////////////////////\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\;;
Now, we have written a superb user friendly program. If you want to embed
the
above examples in your work, you may have to heavily change these programs,
but the basic principles will be the same.
Please, e-mail me your comments and suggestions at
ssirajudeen@...
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
ASM Building
Blocks
by Laura
Fairhead
Here are some simple but very powerful library routines, primarily
concerned with screen output. They all follow the same conventions:
* Routines preserve all registers that they are not specified to return.
* The direction flag (DF) should always be clear before calling.
All code is presented in MASM format. I do not use very many of the
functions of this assembler so it should be trivial to assemble these under
a different one. I do, however, use OPTION SCOPED, this means that labels
within a PROC block are local to that PROC block (a double colon suffixed
label is given global scope though).
First come the primitive routines. These are responsible for the actual
output and simply call DOS to do it. The name for this sort of thing is
called a 'wrapper' function. It does nothing in itself except afford a
particular interface to an application. If all your access to the OS is in
a small number of logical wrapper functions then porting your code to other
systems becomes a lot easier.
;pstrcx- write CX characters to stdout
; uses DOS function 040h
;
;entry: DS:SI=string address
; CX=length of string
;
;exit: (no parameters are returned)
pstrcx PROC NEAR
;assume that DOS can't handle a zero-byte write
;(I don't trust those M$ programmers)
JCXZ don
PUSH AX
PUSH BX
MOV AH,040h
MOV BX,1 ;stdout is handle #1
XCHG DX,SI
INT 021h
XCHG DX,SI
POP BX
POP AX
don: RET
pstrcx ENDP
Note the use of XCHG. XCHG is an extremely useful instruction indeed,
even though there are those who wish to see it's death along with all
those other "horrible, odd-ball, x86 specific". XCHG in essence performs
two operations simultaneously, which is hideously useful considering
they are both MOV's, also if one of the registers is AX (or EAX in 32-bit
code) you get a lovely 1 byte instruction bonus.
XCHG is in fact the real instruction hiding behind the psuedo-op NOP.
If you look at the opcode for a NOP, it is 090h, this is actually the
encoding for XCHG AX,AX, which since it has no effect on the machine state
whatsoever (except of course IP+=1) is ideally suited for this.
I haven't looked back since adding putch to my library. I used to use
the sequence:-
MOV DL,<char>; MOV AH,2; INT 021h
Not only is the putch method much cleaner and more flexible it is
also saving bytes! Of course the pay-back is that this method adds clocks.
However if you think about it the wasted clocks are meaningless really.
Sending characters one at a time to stdout is rather like spelling out
a dictate to your secretary letter-by-letter. In a case where you want
more MIPS you should be looking at your higher level algorithm and not
the output routine, an INT takes a vast amount of time anyway...
;putch- write single character to stdout
; uses DOS function 02h
;
;entry: AL=character to write
;
;exit: (no parameters are returned)
putch PROC NEAR
PUSH DX
XCHG DX,AX
MOV AH,2
INT 021h
XCHG DX,AX
POP DX
RET
putch ENDP
Not hot on speed this strlen, it was written to be compact. You can
if you wish write MUCH faster code than this. I believe X-Bios2 presented
something along these lines in a previous APJ. However, the most important
thing here is certainly not speed, and again if you wanted speed on string
handling so badly, you should really not use asciiz at all; it was never
designed for that.
;strlen- return length of asciiz string
;
;entry: DS:SI=address of asciiz string
;
;exit: CX=length of string
strlen PROC NEAR
PUSH AX
XOR CX,CX
DEC CX
lop: INC CX
LODSB
CMP AL,1
JNC lop
SBB SI,CX
POP AX
RET
strlen ENDP
Now, already, we start getting serious payback for being so good.
The code virtually writes itself.....
;pstr- write asciiz string to stdout
;
;entry: DS:SI=address of asciiz string
;
;exit: (no parameters are returned)
pstr PROC NEAR
PUSH CX
CALL NEAR PTR strlen
CALL NEAR PTR pstrcx
POP CX
RET
pstr ENDP
;pstrcr- write asciiz string to stdout with appended newline
;
;entry: DS:SI=address of asciiz string
;
;exit: (no parameters are returned)
pstrcr PROC NEAR
CALL NEAR PTR pstr
JMP NEAR PTR outcr
pstrcr ENDP
;outcr- write newline to stdout
;
;entry: (no entry parameters)
;
;exit: (no parameters are returned)
outcr PROC NEAR
PUSH AX
MOV AL,0Dh;CALL NEAR PTR putch
MOV AL,0Ah;CALL NEAR PTR putch
POP AX
RET
outcr ENDP
;pchn- write repeated character to stdout
;
;entry: AL=character
; CX=repetitions (0 is valid and does nothing)
;
;exit: (no parameters are returned)
pchn PROC NEAR
JCXZ don
PUSH CX
lop: CALL NEAR PTR putch
LOOP lop
POP CX
don: RET
pchn ENDP
;pstrlcl- output string DS:SI left justified in a field
; of CL spaces
;
; if the field width is smaller than the string length
; then the string is simply output
;
;entry: DS:SI=asciiz string
; CL=field width
;
;exit: (all registers preserved)
pstrlcl PROC NEAR
PUSH AX
PUSH CX
CALL NEAR PTR pstr
MOV CH,0
XCHG CX,AX
CALL NEAR PTR strlen
SUB AX,CX
JNA SHORT don
XCHG CX,AX
MOV AL,020h
CALL NEAR PTR pchn
don: POP CX
POP AX
RET
pstrlcl ENDP
Note the use of JNA. If you look at the logic for the JNA branch
(not many people seem to do this) you find that it branches iff
CF=1 OR ZF=1, hence after the SUB if the result goes <=0
You may notice that all the routine names are <= 8 chars. The reason
for this being that you can save each one as a seperate file, giving it
the name of the routine. This allows easy reference but has a drawback
or two:
(i) you have to remember the dependencies when you INCLUDE them
(ii) you end up with a LOT of files
So far I haven't found either of these 'drawbacks' to be a serious
problem.
I will be referring back to routines a lot in future articles; whenever
routines are required I will state it and the code shall have a list of
INCLUDE's for the routines to be included. In this manner it will be
possible
to present quite untrivial programs within a reasonable amount of space.
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
Converting Strings to
Numbers
by Chris Dragan
Many programs require user input, which is often numbers. For this
purpose
there are library functions, like for example sscanf() in C. But in assembly
all has to be done by hand, even under Windows (with the exception of edit
controls - GetDlgItemInt() function).
My last project required a flexible function for reading numbers stored
as
strings. From this project I carried out a great function which handles most
of common number formats.
The function expects esi register to point at a string, which is a
number.
The string can have one of the following forms:
10 decimal integer
10D decimal integer
1010B binary integer
AH hexadecimal integer (does not require leading zero)
0XA hexadecimal integer
$A hexadecimal integer
12Q octal integer
12O octal integer
10F float
10.0 float
10.0F float
1.0E+1F float
1.E+1 float
The string is required to have all letters (hex digits, number type
specifiers) uppercase. If a number is to contain lowercase letters, it has
to be converted before calling the function.
The function returns in eax number type:
- 0 if the number is invalid,
- 1 if the number is a dword integer,
- 2 if the number is a qword integer and
- 3 if the number is a float.
The number is returned in edx (dword), ecx:edx (qword) or st(0) (float).
The number will be a qword integer if it exceedes 0xFFFFFFFF boundary.
Also notice that the number is assumed to be positive, '-' before the
number is not accepted and has to be handled externally.
Floating point conversion is done using multiplication, not by means
of fbld instruction. This is because fbld instruction limits numbers to
19 characters, but the function can accept longer numbers if only they
are not too large/small.
And here is the function. It was written (and tested) in TASM's ideal
mode,
but it can be easily ported to MASM or NASM. The function preserves all
registers but eax, ecx and edx, which are used for return value.
; This helper macro checks if there was an error on the fpu
macro chkfpu _endinglabel
fxam
fstsw ax
sahf
jc _endinglabel
endm
proc ConvertNumber uses edi
;---------------- Identify number format
; Search for 0 at the end
mov edi, esi
or ecx, -1
xor eax, eax
cld
repne scasb
; Move to the last character
dec edi
dec edi
; Is there anything ?
cmp esi, edi
ja __invalid
; Identify C-style and Pascal-style hexadecimals
cmp [byte esi+1], 'X'
je __c_hex
cmp [byte esi], '$'
je __pas_hex
; Identify other types using the last character
movzx eax, [byte edi]
cmp eax, 'H'
je __asm_hex
cmp eax, 'B'
je __binary
cmp eax, 'D'
je __decimal
cmp eax, 'Q'
je __octal
cmp eax, 'O'
je __octal
cmp eax, 'F'
je __float_clr
; Find a comma (distinguish between integer and float)
not ecx
dec ecx
mov eax, '.'
mov edi, esi
repne scasb
je __float
;---------------- Process decimal integer
; Prepare
__decimal: mov [byte edi], 0
mov edi, esi
xor eax, eax
; Get a digit
__next_decimal: movzx ecx, [byte edi]
inc edi
xor edx, edx
; Zero ends the string
test ecx, ecx
jz __finito
; Multiply the already loaded part by ten
add edx, 10
mul edx
; If an overflow occurs - the number is a quadword
jo __decimal_qword
; Check digit validity
sub ecx, '0'
jc __invalid
cmp ecx, 9
ja __invalid
; Add the digit
add eax, ecx
; Next digit or process a quadword if carry occurs
jnc __next_decimal
jmp __decimal_carry
;---------------- Decimal (appears to be greater than 0FFFF_FFFFh)
; Check digit validity
__decimal_qword: sub ecx, '0'
jc __invalid
cmp ecx, 9
ja __invalid
; Add the digit (qword addition)
add eax, ecx
__decimal_carry: adc edx, 0
; Load next digit
movzx ecx, [byte edi]
inc edi
; Check for ending zero
test ecx, ecx
jz __finito
; Multiply high part by 10
push eax
mov eax, edx
mov edx, 10
mul edx
; Number too large if an overflow occurs
jo __decimal_overflow
; Multiply low part by 10
xchg eax, [esp]
mov edx, 10
mul edx
; Join high parts
add edx, [esp]
; Number too large if carry
jc __decimal_overflow
; Next digit
add esp, 4
jmp __decimal_qword
; Handle overflow
__decimal_overflow: pop eax
jmp __invalid
;---------------- Process hexadecimal integer
; Was Pascal-style hex (leading '$')
__pas_hex: lea edi, [esi+1]
jmp __hex
; Was C-style hex (leading '0X')
__c_hex: cmp [byte esi], '0'
jne __invalid
lea edi, [esi+2]
jmp __hex
; Was asm-style hex (ending with 'H')
__asm_hex: mov [byte edi], 0
mov edi, esi
; Clear what will become the number
__hex: xor eax, eax
xor edx, edx
; Get a digit
__get_hex: movzx ecx, [byte edi]
inc edi
; Zero ends the string
test ecx, ecx
jz __finito
; Number too large if the most significant nibble of edx
; is nonzero
cmp edx, 0FFFFFFFh
ja __invalid
; Multiply the already converted part by 16
shld edx, eax, 4
add eax, eax ; to avoid shift (see lea below)
; Convert ASCII to digit
sub ecx, '0'
jc __invalid
cmp ecx, 9
jna __hex_ok
sub ecx, 7
cmp ecx, 9
jna __invalid
cmp ecx, 15
ja __invalid
; Add the digit
__hex_ok: lea eax, [eax*8+ecx]
jmp __get_hex
;---------------- Return integer
__finito: mov ecx, edx
mov edx, eax
cmp ecx, 1
sbb eax, eax
add eax, 2
ret
;---------------- Process binary integer
; Prepare
__binary: mov [byte edi], 0
xor eax, eax
xor edx, edx
mov edi, esi
; Get a digit
__get_binary: movzx ecx, [byte edi]
inc edi
; Zero ends the string
test ecx, ecx
jz __finito
; Shift everything left and add the digit
shr ecx, 1
adc eax, eax
adc edx, edx
jc __invalid
; Check digit validity and get next digit if OK
cmp ecx, '0' shr 1
jne __invalid
jmp __get_binary
;---------------- Process octal integer
; Prepare
__octal: mov [byte edi], 0
xor eax, eax
xor edx, edx
mov edi, esi
; Get a digit
__get_octal: movzx ecx, [byte edi]
inc edi
; Zero ends the string
test ecx, ecx
jz __finito
; Check if there is a room for another digit
cmp edx, 1FFFFFFFh
ja __invalid
; Multiply the already converted part by 8
shld edx, eax, 3
; Convert ASCII to number
sub ecx, '0'
jc __invalid
cmp ecx, 7
ja __invalid
; Add the digit
lea eax, [eax*8+ecx]
jmp __get_octal
;---------------- Invalid number
__invalid: fninit
xor eax, eax
ret
;---------------- Process integer part of a float
; Prepare (st0=0, st1=10)
__float_clr: mov [byte edi], 0
__float: finit
push 0300h ; mask off all interrupts
fldcw [word esp]
push 10
fild [dword esp]
add esp, 8
fldz
mov edi, esi
; Get a digit
__get_integer: movzx ecx, [byte edi]
inc edi
; Zero ends the string
test ecx, ecx
jz __float_ready
; Comma starts fraction part
cmp ecx, '.'
je __float_fraction
; Multiply the already converted part by 10
fmul st, st(1)
chkfpu __invalid
; Convert ASCII to number
sub ecx, '0'
jc __invalid
cmp ecx, 9
ja __invalid
; Add the digit
push ecx
fiadd [dword esp]
add esp, 4
chkfpu __invalid
jmp __get_integer
;---------------- Process fractional part of a float
; Prepare (st0=0, st1=1, st2=num, st3=10)
__float_fraction: fld1
fldz
; Get a digit
__get_fraction: movzx ecx, [byte edi]
inc edi
; Zero ends the string
test ecx, ecx
jz __fraction_ready
; E starts exponent
cmp ecx, 'E'
je __fraction_ready
; Multiply the already converted part by 10
fmul st, st(3)
; Multiply the divisor by 10
fxch st(1)
fmul st, st(3)
fxch st(1)
chkfpu __invalid
fxch st(1)
chkfpu __invalid
fxch st(1)
; Convert ASCII to number
sub ecx, '0'
jc __invalid
cmp ecx, 9
ja __invalid
; Add the digit
push ecx
fiadd [dword esp]
add esp, 4
chkfpu __invalid
jmp __get_fraction
;---------------- Process exponent part of a float
; Divide the fraction by the divisor
__fraction_ready: fdivrp st(1), st
; Add fraction to integer
faddp st(1), st
; E indicates start of exponent
cmp ecx, 'E'
jne __float_ready
; Prepare (st0=0, st1=num, st2=10)
fldz
; Sign of the exponent
xor edx, edx
cmp [byte edi], '-'
jne __no_minus
not edx
inc edi
__no_minus: cmp [byte edi], '+'
jne __get_exponent
inc edi
; Get a digit
__get_exponent: movzx ecx, [byte edi]
inc edi
; Zero ends the string
test ecx, ecx
jz __exponent_ready
; Multiply the already converted part by 10
fmul st, st(2)
chkfpu __invalid
; Convert ASCII to number
sub ecx, '0'
jc __invalid
cmp ecx, 9
ja __invalid
; Add the digit
push ecx
fiadd [dword esp]
add esp, 4
chkfpu __invalid
jmp __get_exponent
; Multiply by 10**exp (** is a power operation)
__exponent_ready: test edx, edx
jz __positive_exp
fchs
__positive_exp: fldl2t;ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿10**x = 2**(x*log2(10))
fmulp st(1), st ;³
fld st ;³
frndint ;³
fsub st(1), st ;³
fld1 ;³
fscale ;³
fstp st(1) ;³
fxch st(1) ;³
f2xm1 ;³
fld1 ;³
faddp st(1), st ;³
fmulp st(1), st;ÄÄÄÄÄÄÄÙ
fmulp st(1), st
; Return float
__float_ready: chkfpu __invalid
fstp st(1)
mov eax, 3
ret
endp
And that is it. The function is not meant to work as fast possible and was
not optimized, but it does the task it has to do.
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
List Scan Library
Routine
by Laura Fairhead
Firstly let me introduce an auxillary routine this uses. It is
called 'scaws' and scans past white space. It is very simple, and the
definition of whitespace here is SPACE (020h) or TAB (09h):-
========START OF CODE======================================================
;
;scaws- scan whitespace
;
;entry: DS:SI=string
; DF=0
;
;exit: SI=updated to first non-whitespace character
; AL=value of the character
;
scaws PROC NEAR
;
;there is nothing to explain here but you might take note now
;that I always use the same label names in different PROC blocks,
;in MASM you can do this with OPTION SCOPED
;
lop: LODSB
CMP AL,020h
JZ lop
CMP AL,09h
JZ lop
DEC SI
RET
scaws ENDP
========END OF CODE========================================================
'scalst' is basically a routine to scan-convert a list which can
consist of values and strings. The radix of the values must be set
before hand by calling 'scanur' as the routine uses 'scanu' to convert
values and doesn't set the radix itself. The syntax of the list is
almost the same as the list in DEBUG, where in fact I got the idea from.
You have from 0+ data items, optionally seperated by commas. Whitespace
can be used freely as a delimitor and no delimitors are necessary where
there is no need for them (eg: between a value and a string).
The routine takes several parameters, the address of your string
(DS:SI), the address of somewhere to store the converted data (ES:DI),
the size of the data store (CX) and the size of a unit (AL). The unit size
can be byte (AL=1), word (AL=2), dword (AL=4).
Each data item, as in value/string character, is zero-padded to the
unit size for storing. Also values are checked that they are in range for
the unit size. This method therefore allows us to have those silly
word strings.
Here are some examples, all of these assume that we had set the
radix = 010h (by calling 'scanur' with AL=010h) :-
Calling with AL=1, and our string=1 2 3 "ABC" yields:-
01 02 03 41 42 43
Calling with AL=4, and our string="0"1FE08 2 yields:-
30 00 00 00 08 FE 01 00 02 00 00 00
Calling with AL=2, and our string=9A06 87"DEF" yields:-
06 9A 87 00 44 00 45 00 46 00
Calling with AL=2, and our string="ABC"FE0FE 0 1 2 yields:-
ERROR! CF=1 (FE0FE>FFFF)
A particularly powerful feature of this routine is that it takes
a parameter giving the size of your data store (in bytes). This means
that it will be impossible for the program to be crashed because there
was too much data. Programmers are generally too lazy to do this sort of
range checking, and much to their woe as one particularly wily hacker
attack called 'crashing the stack' has taught.
Example; if we called with AL=2, CX=4 and string=1 9 F
ERROR! CF=1 (01 00 09 00 0F 00 > 4bytes)
As an aside, the function is not entirely the same as DEBUG's list
scanner. With DEBUG the strings are always converted to byte lists, no
matter what the unit size is. It is trivial to modify the routine to
work in this way.
One last note is that the end of the list is the first invalid
character in the string, this not being an error of course since it is
the responsibilty of the controlling parser to decide this based on the
context; eg: DEBUG might check for a semicolon comment on the end of
the line, though as a matter of fact it doesn't. A premature ending (ie:
0 byte appearing inside the quotes of a string token) will abort with
error, thus;
AL=1, string=0A 98"unterminated string yields:-
ERROR! CF=1 (unterminated string)
========START OF CODE======================================================
;
;scalst- data list scan/convert routine
;
;entry: DS:SI=string
; ES:DI=store
; CX=#bytes size of store
; AL=unit size (1=byte,2=word,4=dword)
; DF=0
;
; "scanur" must have been called at least once previously
; in order to set the radix of scanned values
;
; !! entry parameters are not validated and invalid entry
; !! parameters will cause undefined behaviour
;
;exit: CF=1=>error (parse/overflow)
; CF=0=>okay, then:
; ZF=1=>no data scanned, ie: CX=0
; ZF=0=>data scanned
; SI=updated to the first invalid character
; DI=updated to the end of converted data + 1
; CX=#bytes converted data (invalid on overflow error)
;
;note: requires routines "scaws" and "scanu"
;
scalst PROC NEAR
;
;initialise stack frame
;[BP-4] (dw) size mask
; =000000FFh for unit size 1
; =0000FFFFh for unit size 2
; =FFFFFFFFh for unit size 4
;[BP-6] (w) unit size
;[BP-8] (w) original data offset DI
;
;EAX is preserved and the main loop is entered
;
ENTER 8,0
PUSH EAX
CBW
MOV [BP-6],AX
NEG AL
AND AL,3
SHL AL,3
PUSH CX
XCHG CX,AX
OR EAX,-1
SHR EAX,CL
POP CX
MOV [BP-4],EAX
MOV [BP-8],DI
JMP SHORT inlop
;
;main loop head
; ignore any whitespace and skip the optional comma
;
lop: CALL NEAR PTR scaws
CMP BYTE PTR [SI],','
JNZ SHORT ko
INC SI
;
;main loop entry
; ignore any whitespace and if a value token is recognised
; write it to data store and continue loop
;
inlop: CALL NEAR PTR scaws
ko: CALL NEAR PTR scanu
JC SHORT don
JZ SHORT ko2
;
; check that the value is in range for the unit size, if not
; abort here with an error
;
CMP [BP-4],EAX
JC SHORT don
CALL NEAR PTR wracc
JMP lop
;
; no value was present so check for a string
;
ko2: CMP BYTE PTR [SI],022h
CLC
JNZ SHORT don
;
; get string into data store
;
INC SI
XOR EAX,EAX
lop1: MOV AL,[SI]
;
; unterminated string causes an error abort, LODSB is not used for the
;load in order to ensure that [SI] will point to the invalid character
;
CMP AL,1
JC SHORT don
INC SI
CMP AL,022h
JZ lop
CALL NEAR PTR wracc
JMP lop1
;
; exit point for 'wracc' routine below, clean-up the stack
;
err0: POP EAX
;
; main exit point. the carry flag is preserved as this is used
; for both error and normal exits. the number of bytes stored
; is calculated into CX, the INC/DEC ensuring ZF=1 if this was zero
;
don: LAHF
MOV CX,DI
SUB CX,[BP-8]
SAHF
INC CX
DEC CX
;
; restore the only corrupted register and 'LEAVE'
;
POP EAX
LEAVE
RET
;
;wracc- write datum in accumalator to data store
; AL/AX/EAX is written to the data store depending on the unit size.
; throughout the routine DI is the offset into the data store and
; CX is the #bytes left in it. these are updated but if there are
; insufficient bytes remaining in the store we abort with error, taking
; care to clear the 4 bytes (AX + return address) off the stack first
;
wracc: PUSH AX
MOV AX,[BP-6]
SUB CX,AX
JC err0
CMP AL,2
POP AX
JZ SHORT ko0
JNS SHORT ko1
STOSB
RET
;
; note that 066h STOSW = STOSD
;
ko1: DB 066h
ko0: STOSW
RET
scalst ENDP
========END OF CODE========================================================
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
Using the RTC
by Jan
Verhoeven
Here are some routines to use the RTC/CMOS chip for serious timing. It's
an introductory tutorial, so you'll be given more than enough opportunity to
experiment with timing via this method.
About the hardware.
===================
The RTC chip used to be a Motorola MC 146818A chip, but nowadays you
will either find a Dallas 1287 or 1387 style chip, or it is embedded in
the chipset. So far for romance... :o)
I will describe the Dallas DS 1287 since this is the configuration which
is most common for many years now, and the majority of the features are
the same as for the other chips.
The DS 1287 is a clock/RAM with a Lithium battery inside the package.
That's why it stays so big: the battery needs space. If the system is
powered on, the RTC gets its power from the powersupply. When the PC is
off, the RTC goes into power-down mode and slowly drains the Lithium
cell. Expected life for the battery is around 10 years.
The DS 1287 has 64 storage locations, 14 of which are clock and control
registers and the remaining 50 are battery-backed general purpose RAM
cells. This is were the CMOS setup of your PC stores it's system setup
data.
The programmable clock can issue an interrupt, which can be triggered by
three independent events: time of day, periodic signal or end of clock-
update.
The 14 registers inside the DS 1287 are:
address purpose
------- ---------------------------------------
0 current value of seconds
1 alarm setting for seconds
2 current value of minutes
3 alarm setting for minutes
4 current value of hours
5 alarm setting for hours
6 Day of the week [Sunday = 1]
7 Day of the month
8 month [0..12]
9 year of this century [0..99]
10 Control register A
11 Control register B
12 Control register C [read-only]
13 Control register D [read-only]
If you want to know the time of day, or any other date related data,
just select the RTC chip and request the contents of the desired
register.
The alarm registers can be set to generate long-time periodical
interrupts, or for having the chip give a signal when it's time for your
nap. The alarm rate ranges from seconds to weeks.
And since these alarm registers are almost never used, they can also be
used for storing some data for your own software. PTS Partition Manager
for example uses these registers to keep track of where it was, while
reformatting the hard disk. If there is a power-fail, it will just
continue where it left off.
In the PC, the RTC chip is hidden from the programmer. It can only be
accessed in an indirect way. The trick is to first select a register
location and then access that one register as follows:
mov al, <register number>
out 70h, al ; select <register number>
in al, 71h ; for a READ operation
out 71h, ah ; for a WRITE operation
So, we use port 70h for selecting a register or storage location and use
port 71h for doing the actual access to that register. A bit tedious,
but that's how the PC was designed in the first place.
In "old style" RTC chips the century is maintained in software. It
resides in a RAM cell, offset 32h/50d, so it will not be affected by a
year-rollover from 99 to 00. If you update it with a short piece of code
on January first 2000, your PC will be ready for many, many, moons to
come.
The control registers.
======================
Registers A, B, C and D are the registers that control the working of
the RTC clock. They have various functions and register D uses just a
singe bit, which is also read-only....
But this chip is well engineered and all registers have a significant
(although not always logical) influence on the operation of it.
Register A: Timing control.
---------------------------
Register A is layed out as follows:
bit function
--- ------------------------------------------------------------
7 UIP bit: Update In Progress. When there's a ONE in this flag
the timing registers are being updated and it is not safe to
read them. Better to wait until this flag is cleared.
This one bit is read-only!
4-6 DV0-DV2: these three bits control the on-chip oscillator. Do
not experiment too much with this setting. There is only ONE
valid combination for these three bits: 010.
0-3 RS0 - RS3: These are the four Rate Selector bits. They
determine how often the IRQ pin is activated. The following
table shows the meaning of the different values.
RS3 RS2 RS1 RS0 Frequency [Hz] Period [ms]
--- --- --- --- -------------- -----------
0 0 0 0 --- ----
0 0 0 1 256 3.906
0 0 1 0 128 7.813
0 0 1 1 8192 0.122
0 1 0 0 4096 0.244
0 1 0 1 2048 0.488
0 1 1 0 1024 0.977
0 1 1 1 512 1.953
1 0 0 0 256 3.906
1 0 0 1 128 7.813
1 0 1 0 64 15.625
1 0 1 1 32 31.25
1 1 0 0 16 62.5
1 1 0 1 8 125.0
1 1 1 0 4 250.0
1 1 1 1 2 500.0
The default value in the average IBM PC is 0110 or 1024 Hz.
Since no IRQ is enabled, you will not notice any difference
if you change the value.
Register B: Internal operation control.
---------------------------------------
This is the most important register for controling operation of the RTC
chip. Register A determines timing and oscillator parameters, but the B-
register determines how the system will notice these conditions.
In a normal PC, only bit 1 (24/12) is set. All other bits are cleared.
bit function
--- ------------------------------------------------------------
7 SET : If you determine to write a ONE in this bit position,
the clockregisters will not be updated anymore. Only when
this bit is ZERO, the clockregisters will be updated.
6 PIE : The Periodic Interrupt Enable bit controls the IRQ
pin. If this bit is ZERO, no IRQ will be given when the
programmable frequency source (selected by RS0 - RS3) times
out.
You need to set this bit to a ONE to enable a periodic IRQ
operation.
5 AIE : Alarm Interrupt Enable. When this bit is ONE, the IRQ
pin is activated when the alarm-time equals the actual time.
4 UIE : "Update Ended" Interrupt Enable. When this bit is set
to ONE, the IRQ line is asserted when the timing registers
have changed contents.
3 SQWE : Put a ONE in this bit to have the programmable
interval timer (which is controlled by RS0 - RS3) output a
square wave on pin 23 of the chip.
Unfortunately this pin 23 is not connected in a PC so for us
this bit has no meaning. But if you are man enough to bring
pin 23 of the DS 1287 to the outside world, you can use it
at will.
2 DM : Data Mode. The timing registers can display their data
in two different modes: binary and BCD. In the PC, this bit
is always ZERO, meaning that BCD is the desired format.
1 24/12 : Controls if hours are shown in 12 or 24 hours mode.
Put a ONE inhere and you have 24 hours in a day. Clear this
bit and you end up with two half days of 12 hours each. In
the 12-hour mode, bit 7 acts as an AM or PM flag.
0 DSE : Daylight Saving Enabled. Always leave this bit cleared
to ZERO. Daylight saving time periods vary worldwide and the
dates of change are determined by politicians and not by
chipmakers. Unfortunately.
Register C: Interrupt sources.
------------------------------
Register C is a status-word only. The bits in this register are read-
only and only have menaing AFTER an IRQ was received.
Since there is just one IRQ pin on the RTC chip, the IRQ can have three
different sources and there's no way to know which one triggered it,
unless there was only one source enabled. The bits mean the following:
bit function
--- ------------------------------------------------------------
7 IRQF : If this bit is ONE, one of the actual interrupt
conditions was enabled and the interrupt condition was met.
6 PF : Periodic interrupt Flag. If this bit is set, the source
of this IRQ source was the programmable interval timer.
5 AF : Alarm interrupt Flag. If this bit is set, the alarm
condition was the same as the actual date/time.
4 UF : The "Update Ended" interrupt Flag. If this bit is set,
the IRQ was issued by an update of the timing registers.
Bits 0 - 3 are meaningless and will always be ZERO.
Register D: Battery status.
---------------------------
On the chip, there is a voltage reference that is constantly being
compared to the battery voltage. If the battery voltage drops below the
reference voltage, the battery is considered empty and bit 7 will be
SET.
If bit 7 is a ONE, the battery has been empty for some period of time
and hence the data in the timing registers and in the RAM locations MAY
have lost their meaning.
Bits 0 - 6 have no meaning in this register and will always return a
ZERO value.
Using the RTC internals.
========================
This, in a nutshell, is what the RTC chip is from the inside. I already
explained some lines above how to access the storage locations and the
timing registers of the DS 1287. This does not mean that everything will
also work the first time.
If you need to change a timing value, you must always first disable
register updates, even if you make sure that the changes you make to the
timing registers will well fit in an RTC timeslot. This means:
- access register B and set the SET flag
- change the timing registers
- access register B and clear the SET flag
Remember, there's not much intelligence inside a DS 1287. More recent
chips might do more tricks for the programmer, but the old beasties just
do as they were told.
In order to set the periodic interrupt rate, we use the following code:
--- Begin ------------------------------------------- SetPIRate -----
SetPIRate: ; Set Periodic Interrupt Rate
mov al, 0A ; ah = rate to set
out 070, al
mov al, ah
out 071, al ; and set it in register A
ret
---- End -------------------------------------------- SetPIRate -----
This code is very straightforward. It relies on the fact that (in the
IBM PC) the contents of register A are always the same:
bit 7 = read-only
bits 4 - 6 = 010
bits 0 - 3 = rate selector
So, it can set the value of bits 4 - 7 in the calling code. It is not
good programming, since we should:
- read in the contents of Register A
- clear bits 0 - 3
- OR in the new value
- write it back to register A
Inside the IBM PC.
==================
The IRQ pin of the RTC is connected to the Intel 8259 PIC (Programmable
Interrupt Controller, although "programmable" is too much honour for
this dumbo). In non-XT machines there are two of them, cascaded. This
means that the second one is connected to what used to be IRQ2. This
gives us a rather stupid PC IRQ priority list:
IRQ Priority IRQ Priority
--- -------- --- --------
0 0 8 2
1 1 9 3
2 10 10 4
3 11 11 5
4 12 12 6
5 13 13 7
6 14 14 8
7 15 15 9
A lower number means a higher priority....
The RTC interrupt line is connected to PC-IRQ8. So it comes in third
place for being serviced. When enabled!
Normally IRQ8 is NOT enabled, so you will first have to settle that with
the PIC, which is far from easy to understand. I use the following code
to enable and disable the IRQ8 processing. Disabling this interrupt is
necessary after your program is unloaded from memory. If you don't do
this, the IRQ service routine vector might point to some random code or
data in the next program loaded (like Command.Com).
----------------------------------------------------- EnableIRQ8 ----
EnableIRQ8: ; enable IRQ 8 in 8259
push ax
in al, 0A1 ; get IRQ mask word
and al, not bit 0
out 0A1, al ; enable IRQ 8
pop ax
ret
----------------------------------------------------- EnableIRQ8 ----
Easy, isn't it? It took some nights to figure this out, 'cause the Intel
databooks are not that clear. I was glad to find some NEC databooks
since these shed some more light. In general, for older chips, NEC is a
good choice of databooks. They used to second source 80x86 chips for
Intel and are still known for their innovations they put into their V20
and V30 chips. The V25, a vastly improved 8088, was contaminated by 8
full banks of 14 registers. Luckily Intel did not copy this. What would
a 386 have been with 250 GP registers?
Here's the code for disabling IRQ8:
--- Begin ------------------------------------------ DisableIRQ8 ----
DisableIRQ8: ; disable IRQ 8 in 8259
push ax
in al, 0A1 ; get IRQ mask word
or al, bit 0
out 0A1, al ; disable IRQ 8
pop ax
ret
---- End ------------------------------------------- DisableIRQ8 ----
Asserting IRQ8 will make the PC generate an INT 70h. So, we need to have
an INT 70h handler ready:
--- Begin ------------------------------------------ NewIRQ8 --------
L0: mov [IrqCount], ax ; and store it
L1: mov al, 020 ; tell stupid PC that IRQ ends
here
out 020, al ; EOI to original PIC
out 0A0, al ; EOI to cascaded PIC
pop ds, ax ; restore registers
iret ; and get out
NewIRQ8: push ax, ds
cs mov ds, [DataSeg] ; restore DS
mov al, 0C
out 070, al
in al, 071 ; clear interrupt flags
test [Flags], Running ; are we running?
jz L1 ; if not, get out
test [Flags], FastMode ; Samplerate over 128 Sps?
jz >L2 ; if not, scram
or [Flags], TimeOut ; else set TimeOut flag
jmp L1
L2: mov ax, [IrqCount] ; medium to slow samplerates
dec ax ; are we at correct value?
jnz L0 ; ... if not, wait some more
or [Flags], TimeOut ; ... if so, set TimeOut flag,
mov ax, [MaxCount] ; ... reload time constant
register
jmp L0
---- End ------------------------------------------- NewIRQ8 --------
I like to do as little as possible in this kind of routines. In this
case I set a flag and rely on the abillities of the background program
to fork execution based on the state of that flag.
I hate the idea of having an INT routine that actually DOES things, but
which, for some obscure reason, cannot complete before the next INT
comes in. You'll be able to figure out what will happen in most cases.
If this routine sets a flag twice, I don't care too much. OK, I loose a
sample, but the program keeps running and it will still terminate when I
ask it to.
This routine:
- saves registers on the user-stack
- restores correct DS
- accesses the FLAGS register in memory
- consults these flags and acts upon them
- eventually reaches L1 and here an EOI is sent to the PIC's
- pops the stored registers from the userstack
- returns with an IRET.
The PIC needs an EOI to enable lower priority interrupts. And since
there are two PIC's in modern PC's, there also must be two EOI's.
The following routine will enable the new IRQ8 handler:
--- Begin ----------------------------------- EnableNewIRQ8 ---------
EnableNewIRQ8: ; program the RTC chip to 1 kSps
push ax ; and enable the 8259 PIC, channel 8
mov al, 0C
out 070, al
in al, 071 ; check register C first
mov ah, 00100110xB
call SetPIRate ; set PI rate to 1 kSps
mov al, 0B
out 070, al
mov al, 01000010xB ; enable the RTC interrupt pin
out 071, al ; and store it in RTC register B
call EnableIRQ8 ; enable the 8259 PIController
pop ax
ret
---- End ------------------------------------ EnableNewIRQ8 ---------
And before going back to the OS of your choice, make sure there will be
no IRQ8's anymore coming this way:
--- Begin ----------------------------------- ResetNewIRQ8 ----------
ResetNewIRQ8: ; restore default values in RTC
push ax ; and disable 8259 PIC, channel 8
mov al, 0A
out 070, al ; select register A
mov al, 00100110xB
out 071, al ; and set it back to PC default
mov al, 0B
out 070, al
mov al, 00000010xB ; disable in
(Message over 64k, truncated.)