Xref: news2.mv.net comp.os.msdos.djgpp:6794
From: alaric AT abwillms DOT demon DOT co DOT uk (Alaric B. Williams)
Newsgroups: comp.os.msdos.djgpp
Subject: Hardware interrupts tutorial and "libints" library (UUENCODED)
Date: Sat, 03 Aug 1996 23:15:12 GMT
Lines: 674
Message-ID: <839114004.16777.3@abwillms.demon.co.uk>
NNTP-Posting-Host: abwillms.demon.co.uk
To: djgpp AT delorie DOT com
DJ-Gateway: from newsgroup comp.os.msdos.djgpp
Right! Any suggestions/corrections to my good self - please play with
the libints library if you have a gcc with working
__attribute__((section)), and tell me if you find any bugs.
If nobody finds any bugs, I'll give it to DJ for the FTP site.
The library is so small, I've taken the liberty of posting it
UUENCODED.
Enjoy!
----------------------------------------------------------------------------------
The Dark Art of writing DJGPP Hardware Interrupt Handlers
(Which might conceivably be useful
for other DPMI environments)
Version 1.0
(C) Alaric B. Williams (alaric AT abwillms DOT demon DOT co DOT uk)
Copy in unmodified form, as long as you give all due
credit.
PREFACE
This tutorial assumes an understanding of what a hardware
interrupt is for, and how it works, along with the usual
programming skills. Ideally, you will have coded a
hardware interrupt handler in real mode DOS before.
No guarantee of the correctness of information in this
tutorial is given; if any errors are found, please report
them to alaric AT abwillms DOT demon DOT co DOT uk, and I will fix them
for the next release. I cannot accept responsibility for
any damages caused as a result, direct or indirect, of
using the information presented herein.
Anyway, onto the interesting parts!
INTRODUCTION
Hardware interrupt handling under DPMI is not all that
different to the same under real mode. There are just a
few more things to consider.
All interrupts in DPMI protected mode go through the DPMI
host, first of all. The DPMI host tells the CPU to stop
using the normal interrupt vector table at real mode
address 0000:0000, by setting the 386+ IDT (Interrupt
Descriptor Table) register to a vector table somewhere
else in memory.
The DPMI host also maintains a 'virtual interrupt vector
table', which lists the addresses of protected mode
handlers. When an interrupt comes in, the DPMI host scans
it's internal table; if that interrupt number is claimed
by a protected mode function, it switches to protected
mode if the CPU is in real mode (for example, when you
use getch(), the CPU is running the real mode getch()
routine in the BIOS), and simulates the interrupt. If the
interrupt is not claimed by protmode, it uses the
interrupt vector table in DOS memory to perform the jump
to a real mode handler, after first making sure the CPU
is in real mode.
For example, if our protected mode application is working
away, and the timer tick interrupt goes off (IRQ 0, INT
8):
1) CPU stores protected mode state and looks up the
address to jump to in the IDT.
2) As expected, it jumps to the DPMI host's interrupt
wrapper for INT8.
3) Wrapper checks to see if its interrupt has been
claimed by a protected mode routine.
4) It hasn't, so the wrapper switches to real mode, and
jumps to the address stored in interrupt vector table
entry 8, thus executing the real BIOS interrupt handler.
5) Upon return from the BIOS handler, the wrapper cleans
up after itself, returning to protected mode and
restoring the machine state so our protected mode
application continues to run smoothly.
Evidently, a lot of switches between protected mode and
real mode can be incurred; the DPMI host keeps the
workings of this hidden from us (thankfully!), but the
performance hit can be significant. Think twice about
installing a timer tick interrupt at 20Khz!
While we're on the topic, I'd like to dispel a myth.
People are often heard moaning "Oh! Protmode's so slow!
All that switching about, just to call a DOS interrupt,
or whenever the user presses a key and invokes IRQ1, the
keyboard interrupt! I'll stick to real mode programming,
thankyou!".
Well, if you hear them saying that, snigger quietly, and
let them write their 'fast' real mode games, because
yours will quite likely run faster. Think about it: most
PCs do not run in real mode at all these days; it's all
V86 mode, if they're running any kind of 386 memory
manager, DPMI host, or Windows product; every time an
interrupt occurs, their DPMI/VCPI/whatever host takes a
slice of time to see if any protected mode code wants
that interrupt. And you get the advantages of large flat
virtual memory spaces, coupled with more control about
where your interrupts go. A DPMI application that's
really hungry for speed can install a protected mode
interrupt, or place a real mode routine in DOS memory
hooked to the real mode handler, depending on where their
program spends the most time. If it's a DOS I/O bound
application, the real mode handler will run faster, as it
does not incur a mode switch; and if it's a compute bound
application, sitting in protected mode, you can write a
true protected mode handler, which will execute sooner
than a real mode one.
LOCKING
The second important thing to consider with DPMI
interrupt handlers is the vexed issue of paging. DPMI
memory (in which your DJGPP application executes) can be
paged out onto disk, making 'virtual memory' possible.
Normally, if any 'paged out' RAM is requested, the DPMI
host can bring it back in invisibly; but this cannot be
done during the execution of an interrupt handler. If an
interrupt handler, any other functions it invokes, or any
variables it touches, is 'paged out', your code will bomb
out with a page fault. The solution is to 'lock' the
memory regions that must be available, telling the DPMI
host to keep them in active memory at all times. This can
be done with the DJGPP library functions
_go32_dpmi_lock_code(void *,unsigned long) and
_go32_dpmi_lock_data(void *,unsigned long), which each
take a pointer and a length, specifying the block of
memory to be locked.
Before installing your handler, lock all code and data it
touches. To lock a static variable, use:
_go32_dpmi_lock_data(&my_var, sizeof(my_var));
To lock a dynamically allocated memory block, you must
know its size:
char *buffer = malloc(buflen);
assert(buffer);
_go32_dpmi_lock_data(buffer,buflen);
_go32_dpmi_lock_data(&buffer,sizeof(buffer));
Don't forget to lock the pointer variable, as well as the
data!
Locking code is slightly different; we can't sizeof() a
function, so we have to use a trick:
void my_handler()
{
}
void lock_my_handler()
{
_go32_dpmi_lock_code(my_handler, (unsigned
long)(lock_my_handler - my_handler));
}
(Copied directly from the info page). In other words,
declare a function after your handler - hopefully a
dedicated dummy function, in case you forget later on and
move the function you're using as a marker - and use
pointer arithmetic to deduce the code size of your
handler function, while praying that gcc puts the
functions in the order you specify!
The Allegro games programming library uses a set of
convenient macros to automate this. They are:
#define END_OF_FUNCTION(x) void x##_end() { }
#define LOCK_VARIABLE(x) \
_go32_dpmi_lock_data((void*)&x, sizeof(x))
#define LOCK_FUNCTION(x) \
_go32_dpmi_lock_code(x, (long)x##_end - (long)x)
LOCK_VARIABLE is pretty self explanatory; END_OF_FUNCTION
is used like so:
void my_function() {
...
}
END_OF_FUNCTION(my_function)
and creates a dummy function named my_function_end.
LOCK_FUNCTION uses this to expand:
LOCK_FUNCTION(my_function)
into:
_go32_dpmi_lock_code(my_function, (long)my_function_end \
- (long)my_function);
thus taking care of the paperwork for you.
Perhaps the easiest and safest method of locking I have
seen is to tell GCC to put everything you want locked
into a special section of the COFF executable, which can
then be locked all in one go. This is accomplished using
the GCC "__attribute__ ((section "name"))" feature;
however, in order to make this work (with the current
version of DJGPP, 2.00), you have to download the GCC
source code and fix a little bug. This fix is the
brainchild of Bill Currie, and consists of adding the
lines:
#define ASM_OUTPUT_SECTION_NAME(FILE,DECL,NAME)\
do { \
fprintf(FILE,"\t.section %s\n",NAME); \
} while(0)
to the file "configure/i386/go32.h" in the gcc source,
then recompiling.
You also need to modify /lib/djgpp.lnk to tell the linker
where to put these special 'locked' sections. I have
written a library to do all this for you - "libints". It
should be included with this document. See the sample
code (in the directory /src/libints) for instructions.
THE ACTUAL ACT OF INTERRUPT CLAIMING
This is pleasantly painless. As GCC does not support an
'interrupt' modifier on function names to tell them to
provide the correct wrapper (with an IRET on the end),
you must use a wrapper; but the DJGPP libc.a can do this
for you in one fell swoop.
If you merely want to chain onto a vector, and still have
it passed back to the previous handler (which may be the
original real mode one), simply use the following code:
#define TIMER_INT 8
void my_int_handler() {
...
}
...
_go32_dpmi_seginfo my_handler;
my_handler.pm_offset = (int)my_int_handler;
my_handler.pm_selector = _go32_my_cs();
_go32_dpmi_chain_protected_mode_interrupt_vector( \
TIMER_INT, &my_handler);
If you want to totally take an interrupt over, it gets a
little more complex. Say we were installing the same
handler - we would use:
_go32_dpmi_seginfo my_handler;
my_handler.pm_offset = my_int_handler;
_go32_dpmi_allocate_iret_wrapper(&my_handler);
_go32_dpmi_set_protected_mode_interrupt_handler( \
TIMER_INT, &my_handler);
Since we've allocated a wrapper, we must, at some point:
_go32_dpmi_free_iret_wrapper(&my_handler);
Just make sure you've removed your interrupt handler
before doing that!
Please note: The wrapper DJGPP provides is not locked in
memory, and is as such unreliable. Included in libints is
a proper locked iret-wrapper system which is a little
easier to use than the standard _go32_dpmi functions, and
includes a useful function for simulating a protected
mode interrupt, which makes it easy to chain to the
original handler. This is more flexible than the GO32
chaining function used above, as you have the option of
not chaining (useful for timer interrupts), and can do
things like executing some code before chaining and some
after. See the libints sample source for further
instructions.
If we want to preserve an interrupt vector, in order to
restore it to its original value (which is kinda good
practice!), we can simply use the code:
_go32_dpmi_seginfo old_handler;
_go32_get_protected_mode_interrupt_handler(TIMER_INT, \
&old_handler);
...
_go32_set_protected_mode_interrupt_handler(TIMER_INT, \
&old_handler);
EXAMPLE
#include
#include
#include
#include
#include
#include
_go32_dpmi_seginfo old_handler,my_callback;
volatile int counter;
char *string;
void my_int() {
counter++;
char *ch = string;
int addr = 0xA0000;
while(*ch) {
_farpokeb(_dos_ds,addr,*ch);
ch++;
addr += 2;
}
}
void end_int() {}
void init_handler() {
_go32_dpmi_lock_data(&counter, sizeof(counter));
_go32_dpmi_lock_data(&string, sizeof(string));
_go32_dpmi_lock_code(my_int, (long)end_int - (long)
my_int);
_go32_dpmi_get_protected_mode_interrupt_handler(8,
&old_handler);
my_callback.pm_offset = (int)my_int;
_go32_dpmi_allocate_iret_wrapper(&my_int);
_go32_dpmi_set_protected_mode_interrupt_handler(8,
&my_int);
}
void done_handler() {
_go32_dpmi_set_protected_mode_interrupt_handler(8,
&old_handler);
_go32_dpmi_free_iret_wrapper(&my_int);
}
void update_text(char *s) {
int ints = disable();
string = s;
if(ints) enable();
}
main() {
counter = 0;
update_text("");
init_handler();
char buffer[80],temp[80];
_go32_lock_data(buffer,80);
cout << "Type some text followed by a CR,";
cout << " or EXIT to quit" << endl;
do {
cout << "Counter = " << counter << endl;
cin >> temp;
int ints = disable(); //clear interrupts
strcpy(buffer,temp);
if(ints) enable(); //reenable interrupts
} while(!stricmp(buffer,"EXIT"));
done_handler();
}
POINTS TO NOTE
In a hardware interrupt handler, you must not:
- longjmp out of the handler
- use library functions like printf() that may invoke
DOS calls
When chaining an interrupt handler with the
_go32_dpmi_chain... function, don't forget to fill in the
pm_selector field of the parameter struct with
_go32_my_cs(), or else you'll have some interesting
crashes.
ACKNOWLEDGEMENTS
Allegro is an excellent graphics/sound/related stuff
library for DJGPPv2 games programming. It can generally
be found on x2ftp.oulu.fi, in
/pub/msdos/programmer/djgpp2/. It's author, Shawn
Hargreaves, is currently between email addresses, but his
snail address is:
Shawn Hargreaves,
1 Salisbury Road,
Market Drayton,
Shropshire,
England, TF9 1AJ.
The copyright section, reproduced here since I'm
including his LOCK_ macros, is:
"Allegro is swap-ware. You may use, modify, redistribute,
and generally hack it about in any way you like, but if
you do you must send me something in exchange. This could
be a complimentary copy of a game, an addition or
improvement to Allegro, a bug report, some money (this is
particularly encouraged if you use Allegro in a
commercial product), or just a copy of your autoexec.bat
if you don't have anything better. If you redistribute
parts of Allegro or make a game using it, it would be
nice if you mentioned me somewhere in the credits, but if
you just want to pinch a few routines that is OK too.
I'll trust you not to rip me off."
Seeing as I used his code in my tutorial, I'll send him
my tips on stereo boosting with phase shifts :-)
Bill Currie came up with the patch to make GCC support
__attribute__ ((section)) properly, thus making libints
possible.
Mark Habersack's ideas about using custom COFF sections
for loadable resources led me to think of the locked-
sections scheme used in libints.
And thanks to everyone on comp.os.msdos.djgpp who
answered my questions about interrupt handling!
section 1 of uuencode 5.22 of file ints100s.zip by R.E.M.
begin 644 ints100s.zip
M4$L#!!0``````#>]`R$````````````````(````24Y#3%5$12]02P,$%```
M````-[T#(0````````````````0```!34D,O4$L#!!0``````#B]`R$`````
M```````````$````3$E"+U!+`P04```````XO0,A````````````````"0``
M`$U!3DE&15-4+U!+`P04``````"@O0,A````````````````#````$E.0TQ5
M1$4O4UE3+U!+`P04``(`"``HO`,A)FVDD8\"``#M!```$P```$E.0TQ51$4O
M4UE3+TA724Y4+DAM5,%JW#`0O1O\#Y/D4#ML'-)"(>VE:0K=%EH"3?/>X#-JK,8&RG+]\.7[?;E.DS,YD\5E
M28K4S+"Z-X./OS3!WP&=A=/;4_ AT C"+2:FHB]/%\KIT?E$,@*Q`U]@%99;="!
MHC;\0"CLB)`>3`LS>2O,@;T@%`[U!1`WFQHB[9(DV^L)]T@;SVQ]>!;'HR&
M"J%3[E%NY+T?ZA:HD7:X AT SA3X`C0Y(.C:@BHA6JMM AT C-$%G3Y/QR=C)-/`88
M>AB=ZGOI`5D=U;"M,9]A6R8MCE`HMU@'G^7O#T_K5HE3TDO9YTFD2:^\GQT0
M7=!WI453[TDA.Y$("^V>N AT C)Y'<0%Z6LEF=N_&(NHZ AT 338;K:/#>FDGUWJV7
M!XYC'O:P53&C()B8Q,PDF4^)CQ1:H=A!AQV['4CD AT 25"]"=I<@,.P^!LM.7B
M2B#*1 AT -5B"Y*M#%>R]`XQ*?05L#Q)^NR7(!HIR5MX[HX[(VJ4<^SL2-)1IGGLQ>SW=%1<6CFBH!8E7IAP6B66&?9<)W`OUKSR:2*:'?SB
MZ AT 36TY6?G'HRH-H=E>7%8CO^FW^Q$K/$E[Y+?YL/^^.U)R
M%"5/>TU/E"8F=-4=MBN,OAU>O7WRX7'SU":%7BN&]%P!TZV-L!
M[H4)IR`\\1$9_0NM00X(M4.I`I#/K=JA*9:+7ZQ4C4()Y.N5-1Y\:P5&>P$PS+IG#LUHHEPM2JV_W7M5"0TX&'SBT,BHHH96?P-YC5^D]U%8B)>!;;A-5
MV7)14 AT 0QQ1KED5AK%H+5-+&J,9'E8E5HLX;;$.;SI$_I=A27!W3F[,>]$N\O-=-O96?;ON^T.9N5K\/
M#U)=BZXGEL&+6WS#"YS#FA\`"!J AT L1;*\D!1EI!E8^?^V/R>Y_`=G)U'],XJ
M"95PU(XO./SX\==MGI]'FLTUK&'D*29YHGSC8DG,;])VG.6SIV"2<@(3;P+3
MQV%T4:C^U"%X0RM%5A8]U!*)*;)#9[5@.=&_J0
MVH#.?\4T)R_YEZ^#HQL`XIVA(G,"3;?(@WZ'P1V/T2P:V\]$>PS#!U AT JL7;6/4/QHK'
M:H5#ZJHR5&3`YR6_N?SKIKQ^]_Z
M1D\-7[^*=B.4'AP"JS$&X5-]="S.O\3[WYQL>OH?4$L#!!0``````#>]`R$`
M```````````````,````4U)#+TQ)0DE.5%,O4$L#!!0``@`(`-.]`R&?2_=T
MG@$``+<"```2````4U)#+TQ)0DE.5%,O3$]#2RY#95)MBQ,Q$/X>V/\P(&CW
MJ%OO!.$X%6M;]'S!`RN*7Y;9S6PW-IL_2THQ$.-L`>C9\".O9C,[Y1:Y"!H!U)
M*@_,V:@=F:H0'ZU4G2()S'7*&@>NMT%+:`@&'+=<8;X+;0^J8SLZ`([L:V.#
M5.YF4,E2;W%'T(6H6HB363R=3V,KX]/H).L#>7@!3R[R[E8<(NIRIA;-H]P5
M74"BQVEZUHY:GY*Q%V^4K>^Z6BMI6 AT B&I$U`0\[##]O<*.84M_9W2I,2?A4"
M(`7K4&F2%_G=38XY2QC)A]'D4F[C[/7&/CVKX\^HDVA,.GGH=$H\)J62_G
MZWG]^?+[JKQ'_OT_.TX0V9Y^^EOV>O5M_2_[WNI.,Y:C'<_7IR%^XQ*(C%'S#2G)US;ZGR:#5ZN[V2")MPX\D\V;F
M[)5+>T(7"F^"G"9;Q0EHWE#520PS@%!IK>;KSE*$`(1_'XL_OWZBW\\/19;E
MOEV4E AT L*W&!OVY;ZG.?EQU.P(,^+/"$RM.:R4D`)@E AT IB:`?N$%8)SC_IXDK
MS*=$`:,::AF7->@,K3HQG8=:#W=UE<<1H1VSDDM@`P&(!+'?\`:Y2\$1VW7;
M.&1!L55Z=J2KJC+4!L(TV7G"QHT^D,5XHB&8'4T>:CY]-Q=;KJ2!61X-W**M
M$P]*V$;AU=2B5BOK>BA!C2+4\U*MN[9W*@T7_VZ*V>4(-4=DMW&H'6L%)H3H/?`^^
MDI5>RTC&F+SU?#*[U!+
M`P04``(`"`!%O`,AI:J+LS@#```6$@``%0```%-20R],24))3E13+TE.5%=2
M05`N4\V7T6_;-A#&WP7H?S@@"QH+GFRG;89Z+\O2H$F#%&Z(+ABFA=,(XC4HM9Y9F'!4BY1 AT Q219GKCSKO^=/_E[GP23N%'
MF(IE)D4BD/=!JR AT WMD]3_IHBTCF%X/W'#Y.)[YU<].!<,BUB^#6$+T)*P98&
M3E AT 9^X5%!866)N2X5&D8JS!_Z,'HW;LSW_O-(-B%,)`H#<6"65Q1AAN50\%2
M2H`9D(HN1M],2N`Y0JR1"PLT9RY6F(:^=ZMXF3?07"-4:L`L5"XY1`A+IA_H
M'YIO\G@!(J'+X0:<)JO<"5P8JT646^2$NF(KA"1W5-\+!N[H>Z'!V!(70LF9
MI?%+CB4X&22)74V0:&.<:C0%5"OWN
M.4DJ)?3!H*T@[N]'6U>K-OI2.9-T
MG9S]YXKJ72IRB]FX)VHE>=4WH%)(L:@&?]=#.Q1G4KR&@^]U974;SPY46[9^
M$25V[S!!.6T0E(.:^**]KB./5%87=Q!HM!K%KD__F-`CI3(:_A<>E;ETL(Y<
M79Z]=/Y5CUK<[)]2DDXHV`F%MZ>T>#C<4H1&V\:D$N)[3]Z"A[U&8-0,G#8#
MKYN!-\W`VV;@K!GXJ>?>R[\!4$L#!!0``@`(`'"]`R$=X6VIKP```-X!```4
M````4U)#+TQ)0DE.5%,O34%+149)3$5]D#$.PC`,1>=:\AT\,,"0,+!U[@(C
MG"!$5:FH&I1&*L?'$<$U$B525,?Y[\>_J9W2$/S=ML^6:I)CH,VV.37'\VX_
M]->\^S%-UB%4G?=DNH/6FB'?YD^84B03Y!(!00G5"UY9&:_Z&K:N AT N0ZCIVTZ5HW
M1;MV6#:L0;&NZ(>U$&B1B@^E2(TO=HTB_WTD]>HEP!PCI],=[[F[YTCJ`8J"
M6\K@!:TKG&]>QM%@NI'G9_\QZ;T^W>Q0F&"/H]/I%5%T1Q0#9V1*V=K`A AT C*
MF0*.:T74WJ^[^O3;]5_S-W`"'["J.9;(Z`R47%MM9B[@?@P4-\X$;W__]?W[
M.$K?9/":$X4%_#R'3\@YDDI#2H+M%5GOG*G2<\HJ*>:%G-NO&2R?/;N(HX^:
M@=F AT AE(JV&V(85M7WUY:V!'A"B`:N'3)W)-P#M0R*!2C:,#%W."6B7DC/0+*&JC<&T-HP[JBFP9
ME-:CQM'TU,L'E)4H&%Q_?)=?O;Y^^\Y$]I#/1PN%K(>WYW`Z!2%!6E-;H\$S%,P3-8%T
MU'XV.S1Y!KJE13%Q7D:^3;)5'-UZ)EUBI AT 2,.5O,.FW9:V>]=MYKCWOM2:]=
M]-K3U0$XJG\I.AL*3H?)D*#T0<<1?,O28#$TF0Y>]^O1V7&K8E].=(G5]?ZT-#4G#0](0
MD31,'%L;)`UG24-:TK`6'J&8(U$.Z+6:T;O5+F8'O]O5T=!'+VQWB!4:;P2C
M_OI0)I0DK5C,7-N*4I*].EP4
MK9`HC]8DQFEN6T?;`O!$V0D`U-TCA.9Y_!;A#7V*@C#*JX0H\7X]Y/1#@&WLMSO:KN2=!\!P_I@[J%@BYBOY6\5 AT A"O AT U/*=B
MG_O#@58?+1=T)YKVXF1/N6-E74P(QTO8]1(VN61**(.>A%#JU*8@!<<%#<2X
M(,F9 AT B[G?S7TSCF`_G&G%#"'(`RS`2U[V1%N^L>+E*BY*U@<:
M`!*$VI*,*@@DRHL2"_2*487!EJ,IS4W,3DW+S$E%%2T!6 AT I3#0!02P,$"@``
M````[KP#(6W&)=$K````*P```!4```!-04Y)1D535"])3E13,3`P4RY615)!
M;&%R:6,G2!6,2XP,`T*4$L!
M`A0`%```````-[T#(0````````````````@``````````0`P`````````$E.
M0TQ51$4O4$L!`A0`%```````-[T#(0````````````````0``````````0`P
M````)@```%-20R]02P$"%``4```````XO0,A````````````````!```````
M```!`#````!(````3$E"+U!+`0(4`!0``````#B]`R$````````````````)
M``````````$`,````&H```!-04Y)1D535"]02P$"%``4``````"@O0,A````
M````````````#``````````!`#````"1````24Y#3%5$12]365,O4$L!`A0`
M%``"``@`*+P#(29MI)&/`@``[00``!,``````````0`@````NP```$E.0TQ5
M1$4O4UE3+TA724Y4+DA02P$"%``4``(`"``."0,APG0""4X#```?!P``$@``
M```````!`"````![`P``24Y#3%5$12]365,O3$]#2RY(4$L!`A0`%```````
M-[T#(0````````````````P``````````0`P````^08``%-20R],24))3E13
M+U!+`0(4`!0``@`(`-.]`R&?2_=TG@$``+<"```2``````````$`(````",'
M``!34D,O3$E"24Y44R],3T-++D-02P$"%``4``(`"`",O`,A*=F6!Y(!``";
M`P``%@`````````!`"````#Q"```4U)#+TQ)0DE.5%,O5$535$Q/0TLN0U!+
M`0(4`!0``@`(`$6\`R&EJHNS.`,``!82```5``````````$`(````+<*``!3
M4D,O3$E"24Y44R])3E174D%0+E-02P$"%``4``(`"`!PO0,A'>%MJ:\```#>
M`0``%``````````!`"`````B#@``4U)#+TQ)0DE.5%,O34%+149)3$502P$"
M%``4``(`"`#/O0,AW-http://www.hardcafe.co.uk/Alaric/