Assembly Language for Veggies (And C programmers) Part 4
Welcome Back... As I commenced to write part 4, I did what I usually do which is to re-read over part three.... and hence I found a MISTAKE! <Gasp!>
In the section that tells you all about what uses the first 64k of RAM, I said the area between 0000 and 0100 was used by the interrupt table vectors... now if one takes 256 and multiplies it by 4 bytes per interrupt one gets 1024. 1024 in HEX is of course 400 - so the area 0000 - 03FF is ALL used to store the vectors of the interrupts.... My sincere apologies for those misled, but hey, bugs creep into anything!!
OK... on to Edition 4.. What will we look at? <Sits here, makes cup of coffee, searches a few ASM books, spills coffee, crashes machine, chucks on the Midnight Oil, hassles users then....> thinks of one largely unknown and little looked-at area of PC Programming...... the TSR.
TSR's are without a doubt the weirdest pieces of software! They rely on bugs, twists and turns, undocumented parts of DOS and most of all, they perform HIDDEN MULTITASKING!!!
I'll highlight the situation by taking a simple program as an example...
When I'm doing work on my PC, I find myself constantly flipping in and out of DOS, doing this, that and the other, and soon I begin to lose track of the time.. I can't be stuffed putting a battery in the clock on the wall (Besides it never was accurate!) so waaaaaaay back when I was learning ASM I decided to write a non-interactive TSR that would, as long as I was sitting at the DOS prompt, display the current time in the top right hand corner of the screen. The clock was required to vanish when an application was run (to avoid cluttered screens) and to automatically re-appear at the sign of the DOS prompt. CC (Clock in Corner) is the result. CC's source and COM should be inside the archive that comes with ASMVEG4.
Now, some general stuff on how TSR's work.
A TSR is just like any other COM program EXCEPT for a few rules.. As you know, when a COM program gets loaded, it gets handed all available free RAM, up to the 640k mark for it's own use.. Same for a TSR... in fact at load time, DOS has no idea if a program will be a TSR or not. Now, a TSR does a few things just a little differently that a normal COM program... In a normal COM program, well you just begin executing whatever it was the program was written for.. a TSR instead has to run an initialization routine that installs itself into RAM and makes sure DOS knows that that RAM will be in use from then on. The initialization routine should also set up all the other bits pertinent to that particular TSR (IE setting up it's hotkey, or looking for the COMports or whatever..) Finally, the TSR must discard all unneeded RAM and quit to DOS.
Because of this loader arrangement the TSR should be coded thus:
--------- program start (CS:0100 at load time) -----
JMP INITIALIZATION ; go to the initialize routine
---- TSR CODE
... etc
---- TSR CODE
INITIALIZATION:
--- init routine
... etc
--- init routine
----------- Program END -----------
The initialization routine makes use of a DOS call that lets you quit, yet leave some RAM marked as Used, so that DOS will leave it alone and move it's internal references to where the bottom most free memory location is up to past the end of your TSR.
This function call is called "Terminate & Stay Resident" (No joke) and is number 031h. (Dos 2.0+ required).
One places the amount of memory to reserve (in paragraphs) into the DX register then calls INT 021h as per any normal DOS function call. <note: A paragraph is 16 bytes>
A typical sequence of actions for an initialization routine would be to display a program title screen, set any required interrupt vectors (see next section), calculate the number of bytes to be left in memory (rounded up, of course!) and then call function 031.
The reason for the initialization routine being right at the very END of the code is that you can tell DOS to only keep memory up to the end of the TSR code, but BEFORE the initialization code. This means that DOS will discard your initialization code (which can be as big as you like) and only keep the TSR bit. These days it pays to keep as little in memory as possible.
So we now know how to tell DOS to keep our code in memory without destroying it, but our code will just sit there and do nothing... We have to make sure that our code is regularly executed to do it's work... for this we have several methods...
As you recall all DOS & BIOS functions are run via INTERRUPTS, and interrupts are REDIRECTABLE... SO al we have to do is redirect a few interrupts to our code and off we go.. Perhaps an example will clarify here: Let's say we code a TSR to come to life upon hitting of the ALT-F10 key.. All keyboard presses cause an interrupt, so all we do is redirect the interrupt to ourselves, and bam... every time a key is hit, out routine gets called... our routine should replace the BIOS one (easy!) - and should read they key, see if it's ALT-F10, if so, clear the KB controller and jump to our routine.. if it's not for us, it should pass back to the original routine (the BIOS one) without clearling the key from the keyboard buffer (so that the BIOS can re-read & process it).
Simple as that! CC doesn't use the KB, so you may ask how it runs? Simple also.. There's an interrupt called the "Idle Loop" interrupt just made for us. The idle interrupt is called by DOS which DOS is busy doing nothing... The idle loop by default normally passes control straight back to DOS, but if we direct it to ourselves, we basically get control handed to us WHEN DOS IS NOT BUSY DOING OTHER THINGS.
One time DOS is not busy is when it's waiting for user input at the C:\> prompt... (it is not busy at other times too, but it's a good start).
There's another flag in low memory that indicates for sure if DOS is on the command line or not, but CC doesn't bother checking this (could be done later to improve the program - I leave this to you to try and work out on your OWN.. You need to research the InDos FLAG - Answers in a future ASMVEG!!)
The other thing that all TSR's must watch is that they don't try and run under themselves - take for example if CC was in the middle of displaying the time and another Idle Interrupt occurred? DISASTER!! so CC has a flag it sets the first time it's called.... next time round it checks the flag and if it's set, it quits back to DOS because it knows it's already going... if the flag is clear it knows it's OK to go to work... needless to say the last thing CC does after displaying the time is to clear the flag again.
CC's initialization routine gets the current screen mode, and if it's mode 7 assumes a MONO screen... if it's not 7 it assumes colour... CC also assumes an 80 column mode (hey, I said it was simple!!) and sets a pointer into video memory accordingly..
CC then does the work of getting and setting interrupt vectors... CC is rather rude in that it assumes it's the first Idle interrupt user and won't pass on down the chain (Again, I said it was simple!!). Try to add a pass-on routine if you're feeling extravigant.. you'd have to use the GETintVector function, then save that in a variable addressable in the TSR module, then instead of doing an IRET, you'd have to do a direct JMP to the old address... why not try this as a real challenge of your knowledge.. again, the improved version will appear in a later ASMVEG to show you the way that I did it...
If you don't follow that, take this: assume another program is already making use if the idle interrupt... CC always does an IRET after being called... what it should do if jump to wherever the old idle interrupt pointed to so that if there is another program making use of the interrupt, it too will get called.
Take this for example:
IDLE INT ---> old TSR ---> original idle int ---> Back to Caller
CC Currently:
IDLE INT ---> CC ---> Back to Caller (original idle int & old TSR are no longer called - nasty!)
CC Should do:
IDLE INT ---> CC ---> old TSR ---> old idle int ---> Back to Caller
This way everything behaves.... the simple short term fix is to load CC first, but what if you get 2 programs that both misbehave in this regard? OUCH!
CC then works out how big CC is and goes TSR. just to further balls things up, CC uses an older way of going TSR that basically works the same as function 031 but instead runs off INT 027. INT 027 is DOS 1.0 compatible (as this originally ran under DOS 1.1!!!!). Another option would be to update to the more compatible 031 function call... again, I leave this to you to experiment with...
The TSR Part of CC....
The TSR part of CC begins at the GET_TIME label.. first off, we save ALL registers.. this is essential so that the main program that was interrupted doesn't get screwed up by us...
CC then checks to see if it's already running and quits if it is... If CC isn't already running, it sets the flag to say that it now is, gets the time, saves it in the HOURS, MINUTES, SECONDS variables and gets the screen buffer address. Next it displays the time, works out the AM/PM stuff (which isn't perfect - see where I went wrong if you can...remember, the hour between midnight and 01:00 is AM, the time between noon and 01:00 is pm...). Lastly, it clears it's busy flag, restores the registers and returns to the main system. You might say that CC is naughty in directly accessing the video RAM, but it does it for 2 reasons: The main one is SPEED... if you take too long, then you'll slow things down by having al these idle interrupts bottlenecking... the other reason is often not understood...
YOU MUST NEVER CALL A DOS FUNCTION FROM INSIDE A TSR!! NEVER!!! DOS is not written to be "Re-Entrant". What this means is this: Say DOS is part way thru something or other (OK this probably doesn't apply for CC, but a hotkeyed TSR might certainly have this problem) for arguments sake say DOS is 1/2 way thru a disk write. If we go calling another DOS routine it'll upset all of DOS's internal pointers and stuff and when the original routine resumes (ie when our TSR quits) all of DOS's internal data will be screwed up.. on the other hand most of the BIOS is quite re-entrant (thus the use of BIOS int 02C to get time)... in fact when you issue a mode change command, INT 010 calls itself 12 - 16 times (depending on version) to reposition the cursor, set the colours, reprogram the video controllers, etc...!!!
This could be bypased with caution, reference to the InDos flag (See above) and the like, but it's certainly not consistent over all versions of DOS, nor reliable for all function calls. Best not to take the risk I always say... That's why TSR's often stuff things up... Old sidekick's are notorious for this! The original sidekick uses the timer tick interrupt to see if another TSR or program has stolen the keybard interrupt away from sidekick... if it has, sidekick steals it right back! the result: you can pop up sidekick, but your main program suddenly ignores all your keystrokes!!!
Sidekick does this by making use of yet another interrupt - the TIMER TICK... The BIOS generates an interrupt that executes once every 18.2 milliseconds (read about 55 times a second) NO MATTER WHAT. Again, normally this interrupt returns straight back to the BIOS but we can tap it and thus our TSR will be called at a constant non-machine speed dependent rate... a LOT of CPU speed rating programs use this interrupt to derive a constant speed reference by which to rate things... other TSR's use it as a constant patch to make sure their TSR gets called regularly (Stuff like mouse drivers, Fax card software and even multitaskers use this interrupt!)
A final comment...
CC could be improved even further.... it could make use of INT 010 and get the video mode current at that second - and adjust itself if it found itself in 40 or 132 column mode...
Also, take this situation: If CC is run again, it will simply go TSR and steal a bit more memory.. the old copy of CC won't execute any more, but it will still sit there in memory (Question: WHY? Answer is in text above)
What CC should do in the initialization section is to search RAM for a pre-existing copy of CC (by doing a <say> 16 byte compare of the first 16 bytes of the copy being executed with the first 16 bytes of all possible CS settings up to (but not including) the current CS.. If no match, no CC loaded... if a match, CC already resident, so the running copy should abort..
As for uninstalling... this is a major patch... first off, CC should make sure that the idle interrupt still points to the TSR copy of CC (and hasn't been pinched - if it has uninstall is impossible) then redirect the idle interrupt back to the old, original address (another reason for saving the original address!!), then you need to call the DOS deallocate memory block program and point it to CC's TSR area...
Messy, yes, but can be achieved with a little effort...
Most of these mods will appear in a later ASMVEG (read when I get round to fixing it!!) but for now, print out or read the source code, run the thing and try it out... I know it doesn't behave 100% with doubledos (tends to pop up at the wrong time) - under desqview no idea, but probably needs to be loaded into a "writes directly to screen - Yes" and "uses own colours - yes" window.. probably won't handle the screen correctly either if you're not in 80x25 mode at the time, but then again it wasn't written to be a fully blown program...
So what, may you ask, is all this about HIDDEN MULTITASKING that i was on about before.... consider this: you're at the DOS prompt... you can run anything at any time, and yet this program constantly displays the right time onscreen, and DOS is oblivious to it's existance.... two programs are running at once, yes? is this not multitasking? ... it's not really, rather a sort of task swapping where it runs one program for a bit, then another for a bit, then swaps back, but it does it fast enough to look like true tasking... doubleDos is based around the exact same principal.. each time a timer tick interrupt occurs some base code inside DD hands control over to the other window, first restoring all that window's flags & variables etc... next time it swaps to the other window and sets up that window's setup.. it's a little more complex than that, especially with regard to device sharing, but that's all that basically forms the multitasking core..... on a 386 it's different, but for a 286 and 8088/8086 this is the ONLY way to task DOS!!
Perhaps now you follow why it's referred to as hidden - there's no Multitasking driver, rather just normal DOS and normal BIOS doing things as they always do, just being twisted about to suit us.... now you see why I said TSR's were sneaky?
f there's interest, I might release some better source for some TSR's including a pop-up of some description??? I'll have ta see what I can do for ya's if there's interest...
Ok, well go forth and hack! Until ASMVEG5....
.\\erlin
CC.ASM
BEGIN: JMP INITIALIZE
HOURS DB 0
MINUTES DB 0
SECONDS DB 0
OLD_INT_28 DW 0
OLD_INT_28_2 DW 0
VID_SEG DW 0B800
POINTER DB 0
SCR_BUFFER DB '00:00:00 pm'
SCR_LEN DW $-OFFSET SCR_BUFFER
BUSY DB 0
GET_TIME: PUSH AX,BX,CX,DX,SI,DI,DS,ES
PUSHF
CLD
PUSH CS
POP DS
PUSH CS
POP ES
CMP BUSY,1
JNZ PASS_BUSY
JMP QUICK_EXIT
PASS_BUSY: MOV BUSY,1
MOV AH,02C
INT 021
MOV HOURS,CH
MOV MINUTES,CL
MOV SECONDS,DH
MOV DI,OFFSET SCR_BUFFER
OPT_TIME: MOV DL,HOURS
CMP DL,13
JL NOT_PM
SUB DL,12
MOV POINTER,1
NOT_PM: CMP DL,0
JNZ NOT_ZERO_TIME
MOV DL,12
MOV POINTER,1
NOT_ZERO_TIME: XOR DH,DH
CMP DL,10
JGE NOT_PAST10
MOV AL,' '
STOSB
NOT_PAST10: CALL WRITE_DECIMAL
MOV AL,':'
STOSB
MOV DL,MINUTES
XOR DH,DH
CMP DL,10
JGE NOT_10_3
MOV AL,'0'
STOSB
NOT_10_3: CALL WRITE_DECIMAL
MOV AL,':'
STOSB
MOV DL,SECONDS
XOR DH,DH
CMP DL,10
JGE NOT_10_4
MOV AL,'0'
STOSB
NOT_10_4: CALL WRITE_DECIMAL
MOV AL,' '
STOSB
MOV AL,'a'
CMP POINTER,1
JNZ NOT_PTR_1
MOV AL,'p'
NOT_PTR_1: STOSB
MOV AX,VID_SEG
MOV ES,AX ; sets up base to write to
MOV DI,3978 ; sets up offset to write to
MOV SI,OFFSET SCR_BUFFER ; sets up location to write from
MOV CX,SCR_LEN ; counter for number of bytes to write (length of scr_buffer)
OPT_TIME_SCREEN: MOVSB
MOV AL,0F
STOSB
LOOP OPT_TIME_SCREEN
MOV BUSY,0
QUICK_EXIT: POPF
POP ES,DS,DI,SI,DX,CX,BX,AX
IRET
WRITE_DECIMAL: PUSH AX
PUSH BX
PUSH CX
PUSH DX
MOV AX,DX
MOV BX,10
XOR CX,CX
NEXT_COUNT: XOR DX,DX
DIV BX
PUSH DX
INC CX
OR AX,AX
JNZ NEXT_COUNT
FLOG_IT_BACK: POP AX
ADD AL,'0'
STOSB
LOOP FLOG_IT_BACK
POP DX
POP CX
POP BX
POP AX
RET
INITIALIZE: MOV AH,0F
INT 010
CMP AL,07
JNZ NOT_MONO
MOV VID_SEG,0B000
NOT_MONO: MOV AX,03528
INT 021
MOV OLD_INT_28,BX
MOV OLD_INT_28_2,ES
MOV AX,02528
MOV DX,OFFSET GET_TIME
INT 021
MOV DX,OFFSET INITIALIZE
INT 027