Category : Assembly Language Source Code
Archive   : TSR21.ZIP
Filename : TSR.DOC
Output of file : TSR.DOC contained in archive : TSR21.ZIP
INTRODUCTION
Have you ever wanted to write a TSR (Terminate Stay Resident) program that
issued DOS calls (e.g. disk access)? This program is the answer.
TSR is, itself, a 5k TSR. You make it resident first, then make resident
your programs that are written according to these instructions. When your
program is called for (by hot key or other event) and the aspects are
right (it's ok to issue a DOS call), you are dispatched. Once dispatched,
you can do anything a normal program would do: issue DOS calls, BIOS
calls, allocate memory, write the screen, read the keyboard, etc. When
you're finished doing that, the machine is put back like it was and the
"background" program continues. Your program is allowed to intercept
interrupts. It could even be written in a high-level compiled language
(with a little assembler code, of course).
TSR is re-entrant. Program B can be popped up on top of A (the background
program), then C can be popped up on top of B. When C exits, B is back in
control. When B exits, A is back in control.
I have been testing TSR with a game called King's Quest III by Sierra. I
picked that game because it's a "worst case" program to interrupt. It's
screen is always graphics, it issues DOS calls out of the clock interrupt,
it accelerates the PC's clock to run faster than 18.2 Hz and it plays a
lot of music. TSR can interrupt it, pop up a screen and, then, continue
the game successfully. Borland's Sidekick is supposed to be the "master"
of resident programs, but it fails to handle any of these conditions.
--------------------------------------------------------------------------------
HOW TO START TSR
Just enter TSR/nn (probably in your AUTOEXEC.BAT). nn is the amount of
memory (in kb) to force free for screen save areas. The default is 8kb.
You will want to specify 64 if any background programs do graphics and are
ill-behaved. See discussion of memory mis-management below.
--------------------------------------------------------------------------------
HOW TO START YOUR RESIDENT PROGRAM
Issue Int 2F-2001 (start TSR session)
Issue Int 21-31 (terminate, stay resident)
ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ»
º INT 2FH º
ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ
º Input AH 20h = Talking to TSR º
º AL 01h = Start session º
º DS:SI Points your structure º
ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ
º Output AL FFh = ok º
º FCh = sibling not found º
º FDh = sibling not delinked º
º FEh = sibling deleted º
º else = not ok º
ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ
º Structure: º
º º
º tsr_struc db 'TSR1 ' ; name of program (8 bytes, upper case) º
º tsr_current_ecb db 0 ; ecb º
º tsr_dispat_ecb db 0 ; ecb when dispatched º
º tsr_hotkeys db 42,56 ; hotkeys º
º tsr_entrypoint dd entry ; entry point º
º tsr_emergency dd 0 ; entry point for emergency shutdown º
º tsr_comm_area dd 0 ; points to communication area º
º tsr_int_area dd intarray ; points to interrupt array º
º tsr_psp dw 0 ; psp of the task º
º tsr_screen dw 0 ; B000 or B800 º
º tsr_screen_save dw 0 ; segment of screen save area º
º tsr_passthru db 0 ; if not 0, passthru º
º db 31 dup(0) ; 31 bytes for internal use º
ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ
º Interrupt array (if required): º
º º
º intarray dw (list_end - list_start) / 10 ; number of entries º
º list_start dw 2Fh ; number of vector º
º dd int2F ; address of your routine º
º int2Fa dd 0 ; previous contents º
º list_end label word º
ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ
DS:SI
Current ECB ECB at time of dispatch
ÉÍÍÍÑÍÍÍÑÍÍÍÑÍÍÍÑÍÑÍÑÍÑÍÍ» ÉÍÍÍÑÍÍÍÑÍÍÍÑÍÍÍÑÍÑÍÑÍÑÍÍÍ»
ºHK ³U1 ³U2 ³U3 ³x³x³x³NSº ºHK ³U1 ³U2 ³U3 ³x³x³x³NS º
YOURNAME ÈÍÍÍÏÍÍÍÏÍÍÍÏÍÍÍÏÍÏÍÏÍÏÍͼ ÈÍÍÍÏÍÍÍÏÍÍÍÏÍÍÍÏÍÏÍÏÍÏÍÍͼ .........
byte byte
HK Indicates hot key pressed. You can detect second press by
watching Current ECB.
Un These three bits can be set by you to indicate that you need
to be dispatched (e.g. inside your interrupt routine).
NS Tells TSR that you will not be accessing the screen.
A note on hotkeys:
..The users get upset if there is no provision for changing the hot keys.
An obvious solution is to parse them off the command line and stuff
them into the structure before you start the session. That's a lot
of redundant effort and you're probably too lazy to do it.
Ever helpful, TSR parses your command line for you. If it sees
the letter K followed by one or two decimal numbers, it overrides your
hotkeys. For example: TSRCALC/k=54,56 changes the hot keys to Alt-RShift.
The = and , are cosmetic. Numbers can be seperated by any non-numeric.
The first space on the command line stops TSR's parsing, e.g.
TSRXYZ/k=54,56 addkey=12345, the D and K in "addkey" would not be
misread as an instruction to TSR (a D deletes your program, see below).
..Hotkeys are key-codes, not ASCII values. This is because resident
programs usually deliberately use key combinations that are discarded
by the keyboard BIOS so as not to interfere with running programs.
For those "developers" who don't have a Technical Reference Manual,
keycode values are provided below.
General guidelines for your TSR:
..Inside your interrupt routines you cannot issue DOS calls,
access the screen, etc. You can only set a bit in the ECB
indicating the "main" procedure is ready to be dispatched.
You should not reset the bits; TSR does that for you.
..The "main" procedure can do anything (within reason) and can
be written in a compiled language. See below for how I call it.
..Program TSR1 is a very simple example of a resident program.
I strongly recommend that you write your program as an .EXE file,
not a .COM file. I spent a few years writing .COM files and fighting
MASM/Intel's notion of segments. I can say from experience that you're
better off learning to think like MASM and "go with the flow". After
a while it even begins to seem reasonable. Model your handling of
segments after these examples; let LINK and COMMAND.COM loader do
some of the work for you.
..It's possible that somebody will grab your interrupt vectors for
no good reason. For example, BASIC intercepts the two async interrupts
just in case it might need them. If you're in control of the async
interrupt, take it back (during a clock tick).
--------------------------------------------------------------------------------
HOW TO TERMINATE YOUR RESIDENT PROGRAM
The decision to terminate a program is normally made by the user, not the
program itself. TSR helps by parsing your command line for a D e.g.
TSRCALC/D. If it sees one, it deletes your sibling (a resident program
with the same name) and returns al=FEh. In this case you should just
terminate. If your program wants to terminate itself, here is how:
Issue Int 2F-2002 (end TSR session)
RET
ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ»
º INT 2FH º
ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ
º Input AH 20h = Talking to TSR º
º AL 02h = End session º
º DS:SI Points your structure º
ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ
º Output AL FFh = ok º
º FDh = could not delink, you are transparent º
º else = not ok º
ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ
Terminating resident programs poses two problems: memory fragmentation and
vector linking. Memory fragmentation is not a great worry. Leaving holes
in the memory map will not cause DOS to crash. Vector linking is a real
problem. If another (non-TSR) program came in after you and intercepted
one of the same vectors you intercepted, he removed your value, stored it
in his memory and replaced it. In this case you cannot be deleted because
he is still pointing to you. TSR manages the linking/delinking process
for programs it knows about. If it detects that a "foreign" program
has intercepted your interrupts, it puts a non-zero value in tsr_passthru.
In that case, your interrupt routine should just pass thru.
--------------------------------------------------------------------------------
HOW TO TERMINATE TSR AND ALL ITS CLIENTS
Enter TSR/D. TSR deletes itself and all programs attached to it. If you
made a non-TSR program resident after TSR, it will delete as much as it can
and make the others transparent.
--------------------------------------------------------------------------------
HOW TO TALK TO ANOTHER TSR PROGRAM
ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ»
º INT 2FH º
ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ
º Input AH 20h = Talking to TSR º
º AL 00h = Inquiry º
º DS:SI Points your structure (only the name is used) º
ÇÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄĶ
º Output ES:DI Points to his structure º
º AL FFh = ok (it is resident) º
º else = not ok (it is not resident) º
ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ
You could use tsr_comm_area to pass data, then post his ecb.
Rather than everyone hard coding program ids into AH, which would result in
collisions (two programs using the same value), DS:SI points to an 8 byte
program name (upper case). Everyone should compare it against his own name
to see if the call is for him. Functions 1-4 and DDh are reserved for TSR
itself. Function 0 (inquire) is defined by IBM/Microsoft and answered by
TSR (you don't have to write code to answer it). Other functions are up for
grabs.
--------------------------------------------------------------------------------
WHAT TSR DOES TO DISPATCH YOUR PROGRAM
Assume program B (the new guy) is interrupting program A (the old guy).
Here's what it takes to do a "context switch":
Determine whether it's ok to issue DOS and BIOS calls. This is done by
sitting on the "front door" of all the DOS and BIOS interrupts and setting
busy flags on the way in, clearing them on the way out. There is special
handling for DOS calls that never return (program terminate) or don't
return for a long time (EXEC program, read a key). If all the flags are
clear, check the DOS Critical Vector Flags (undocumented). These are
DOS's own internal busy flags. Sometimes my flags are clear but his
aren't. Issuing a DOS call at this time will cause him (and maybe your
hard disk) to go bye-bye. This check is done on every eighth clock tick
and while the program is checking or waiting for a keystroke (by DOS call
or BIOS call). If I did it on every clock tick, PRINT.COM would run very
slowly.
When checking for aspects clear, I also make sure no interrupt is stacked
on the 8259. I do that by reading port 20 (undocumented). This can occur
when the background program is handling a hardware interrupt (e.g. the
keyboard) and the clock ticked before he had a chance to reset the 8259
(out 20,20). I also check for a "stuck" interrupt, one that stays busy for
a long time (more than 2 clock ticks). I clear these by toggling the bits
at port 21. This sometimes occurs on the async ports.
In the case of read a line (0A) we have a problem. DOS issues an int 28
(undocumented) from inside the wait-for-key functions (01, 07, 08, 0A)
when it's supposedly ok to issue dos calls. Ok, that is, so long as you
don't issue another read keyboard. The keyboard routine, you see, is not
re-entrant (hard to believe). As a result, int 28 is useless for
dispatching tsr's. The solution is to never call DOS keyboard functions
unless there is a key available (determined by function 0B). TSR does
this for 01, 07 and 08. Handling 0A is more complicated. There are many
programs that offer an improvement to DOS's editing; the best known is
DOSEDIT. If you don't have your own favorite, I offer TSRDOSED. I assume
you have one such program and, since it may be installed below (before)
TSR, I do not set the DOSbusy flag on the way into function 0A. If you
don't have a 0A tsr, you will not be able to pop up when DOS is sitting on
the command line.
Having found that the aspects are right, search the TSR's looking for one
who's ecb (event control byte) is posted. This can be done by the program
itself (e.g. he's running off the async interrupt and needs a disk access)
or by me watching for his hot key. When I find one, I have a candidate.
Check to see if A is DOS (sitting on the command line) and he owns all of
memory. If so, make him let go of the last unit (the big piece). You
can't do it with a DOS call (he hangs up) so manipulate the memory
management block directly (undocumented, of course). This is so B can
allocate some of this unused memory if he wants, also so I can use it for
a screen save area.
Get the PSP of A. This is via an undocumented DOS call.
Figure out the size of the screen save area and allocate that much memory
(the owner is B). If I can't get enough memory, I exit and try again
later. For text it's two chunks of 4,000 bytes (25x80x2); for graphics
it's two chunks of 16k bytes. I save both screens (B000 and B800) in case
the machine has two physical screens or a Hercules type display adaptor.
I don't save the EGA graphics screen (A000) because it would require
112,000 bytes (640x350x4/8), it's not necessary unless the resident
program intends to do EGA graphics (EGA text goes to B800) and, last but
not least, the EGA's registers are write-only. There's no way to save and
restore them (thanks, hardware guys). If B does not intend to do any screen
IO, he can set a flag to bypass all this screen stuff.
If A is the "background" program (not a TSR), disable him from receiving
any interrupts. If you don't do this he might issue a DOS call or update
the screen while B is running and at times when it's not ok to issue DOS
calls. I disable him by saving interrupt vectors 0-1F and restoring them
to the state they were in when the last TSR signed on. Thus all the TSR's
get their interrupts but the background guy is on hold.
Make a final check that the background guy didn't initiate any DOS calls
while I was doing this (he may have gotten an async interrupt or a clock
tick).
Copy the ecb that caused this dispatch into a shadow byte so that B will
know why he was dispatched. Clear the real ecb. Note that TSR's commonly
use the same hot key to toggle them on or off. By watching the real ecb,
B can tell when his hot key is hit a second time.
Switch to an internal stack. It would make sense to save A's state on A's
stack. The problem is there is no way of telling how big any stack is or
when it overflows. I'll bet the hardware guys had a good laugh over that
one. Just in case you didn't notice this design feature, DOS advertises
that its stack is only big enough to hold the registers (about 10-20
words). We will often be interrupting DOS (sitting on the command line).
Push all the registers.
Save port 61. Then turn off the speaker (bit 1). If A was making a tone
when he was interrupted, you have to do this to avoid hearing a very
irritating monotone all the time B is up.
Read the speed at which the clock is running (port 40). What, you think
the clock always runs at 18.2 Hz? El wrongo! It's common for programs
that do animation to speed this up. The problem here is that you can't
just read the clock divisor, it's output only (thanks again, guys). To
get it you must be first in line to get the clock interrupt and read the
instant value of the countdown register. That will be slightly under the
divisor. Push that and reset the divisor so the clock runs at 18.2 Hz. If
you neglect to do this the time of day and date will get way off, and the
screen may go blank every second instead of every 10 minutes.
Push A's DTA (disk transfer address). Set it up for B. This is necessary
in case B does any FCB-style IO.
Get the mode, page, cursor size, cursor location. Push all that. Save
the screen contents into the segments acquired above. Set the mode to
text (3) and the page to 0. Don't do a mode change, however, if the
screen was already in text mode (2, 3, 7). This will cause the screen to
jump or be cleared.
Change all the attributes on the screen(s) to 07. If you don't do this
you may find that A set portions of the screen to invisible (BASIC
programs love to do this). If B presents his screen via DOS calls (says
he, sticking his finger in his throat and gagging), DOS writes the data
but doesn't change the attributes already on the screen. B's pop-up
window will be invisible. DOS does have a write-screen function that
sets the attribute to 07. It's INT 29, it's undocumented, so you
probably aren't using it.
Switch to B's stack. I wouldn't mind if he used mine but some compilers
put data at a known location in their stack (at the top) and expect it to
be there. Also reload B's bp. Some compilers make a big deal over this
register.
Set the current PSP to B (undocumented). This is necessary in case B does
any memory allocation.
Issue a far CALL to B's entry point. B will have to load his own
registers if necessary. I load only ss:sp, bp and ds:si like they were
when you started.
At this point B can do all the things a normal program does: issue DOS
calls, BIOS calls, allocate memory, etc. When he's finished doing that,
he issues a far RET to transfer control back to me.
I perform all the above steps (in reverse) to put the machine back like it
was. I restore the screen contents, screen mode, io ports, registers,
PSP, DTA, interrupt vectors, switch back to A's stack, and return from the
clock tick (or whatever).
As a bonus, I change the two obsolete program terminate functions (20 and
21-00), into a good one (21-4C). This is so you can EXEC to old-style
programs.
--------------------------------------------------------------------------------
A DISCUSSION OF MEMORY MIS-MANAGEMENT
Let me say, first of all, that I abhor pre-allocation. For twenty years,
this was IBM's "strategic" method of wasting hardware so they could sell
move of it to mainframe users. Pre-allocation is usually an issue when
carving up address spaces, for example memory and disk. I will not bore you
with the details except to say that it reached its highest form in VM (a
"strategic" operating system) and DOS/VSE (an operating system for the
unsophisticated user). A typical VSE shop uses about 20% of their disk
space.
They couldn't sell this "engineered" inefficiency to PC users, especially
since PC's started with 368kb floppies. But old habits die hard. It
shows up in the way memory is mis-managed under PC DOS.
When DOS starts a program it gives it all of memory. The program is
supposed to release memory it's not using but, until it does, it owns the
whole machine. Even worse, when COMMAND.COM is sitting on the command line,
it owns the whole machine. There's no reason for this. The .EXE header
defines a field for the size of the program. The expectation was that the
loader (COMMAND.COM) would allocate that size to the program and the program
could ask for whatever additional memory it needed. Somehow it didn't work
out that way.
We now have the spectre of well-behaved programs that release memory they
are not using and ill-behaved programs that follow Microsoft's lead. They
own all of memory (COMMAND.COM gave it to them, why give it back?) and
manage it themselves. Some programs, such as QuickBasic, are really
perverse. They release the memory they are not using, then allocate a
chunk of 64k for their data segment, then allocate the rest of available
memory in case they might need it for a heap or buffers or something.
TSR plays by the rules. When it needs memory for a screen save area, it
requests it via a DOS call. When it no longer needs it, it releases it.
With errant programs like QB running amok in the background, I was forced
to take measures to insure that some memory would be available when I needed
it. Repugnant as they are, here's what I do. When I start I take the memory
specified on the command line (or default 8k), divide it in half, allocate
a chunk that big, allocate a chunk of minimal size (16 bytes), release the
first chunk, repeat this process for the other half. The result is four
chunks allocated to TSR (its environment, its program, two 16 byte segments)
and two "holes" in memory, each one-half the requested size. Why two holes
instead of one? Because QB would grab a 64k hole, leaving nothing for TSR.
Note that these holes are not reserved for, nor guaranteed available to,
TSR, but they give it a fighting chance.
I'm really sick over having to do this. Address your cards and letters to
Bill Gates, Microsoft Inc, or IBM, Boca Raton, Fl.
--------------------------------------------------------------------------------
KEYCODES
10 9 20 t 30 a 40 ' 50 m 60 F2 70 SLok 80 2
1 Esc 11 0 21 y 31 s 41 `~ 51 < 61 F3 71 7 81 3
2 1 12 - 22 u 32 d 42 LSh 52 > 62 F4 72 8 82 Ins
3 2 13 = 23 i 33 f 43 \ 53 / 63 F5 73 9 83 Del
4 3 14 Bsp 24 o 34 g 44 z 54 RSh 64 F6 74 -
5 4 15 Tab 25 p 35 h 45 x 55 *PS 65 F7 75 4
6 5 16 q 26 [ 36 j 46 c 56 Alt 66 F8 76 5
7 6 17 w 27 ] 37 k 47 v 57 Sp 67 F9 77 6
8 7 18 e 28 Ent 38 l 48 b 58 CLok 68 F10 78 +
9 8 19 r 29 Ctrl 39 ; 49 n 59 F1 69 NLok 79 1
These are the keycodes for the original PC. The AT Technical Reference manual
tries to confuse you with tables and pictures of other keyboards. In fact,
the AT keyboard always runs in compatibility mode (it's the only mode supported
by the AT BIOS).
--------------------------------------------------------------------------------
SIGN-OFF
This isn't free-ware or share-ware, it's free.
Robert Wagner
Software Lubbock
1500 Broadway (#1208)
Lubbock, Tx 79401
806-744-9940 (w)
806-745-5309 (h)
Other products that are helpful when developing resident programs:
BREAKON Lets you Ctrl-Brk out of anything. Puts the machine back like
it was when you booted up. Removes TSR's above the high-water
mark. By R Wagner. Public Domain.
MAP Displays memory allocation. By Dorn Strickle. Public Domain.
Other fine products of Software Lubbock:
MODEMGA Lets you run cga programs on a Hercules display. $79.95.
PROUNLOK Dis-installs Prolok. Returns original .EXE file. Works on
"two laser hole" version circa 1987. Free.
If you call me with your modem and PROCOMM ready, I'll upload the free
ones for free.
Very nice! Thank you for this wonderful archive. I wonder why I found it only now. Long live the BBS file archives!
This is so awesome! 😀 I’d be cool if you could download an entire archive of this at once, though.
But one thing that puzzles me is the “mtswslnkmcjklsdlsbdmMICROSOFT” string. There is an article about it here. It is definitely worth a read: http://www.os2museum.com/wp/mtswslnk/