COPY ME - I WANT TO TRAVEL (HOW TO PROTECT YOUR SOFTWARE) by Claus Brod There's nothing more senseless than copy protection (surprising intro for an article about self-made protection methods, isn't it?) - for there will never be the perfect one. Nevertheless, it is a fascinating subject, anyway. And one of the most challenging, too. This article shows you how to install a particularly nasty copy protection on your didks... To understand how to install a copy protection method you should be able to tell a disk from fish & chips (frivolous as I am, I suppose that you can manage this). Furthermore, you'll need some background information. Et voila: A standard disk has got 80 concentrical circles on it, called tracks. Every track carries its data in specific blocks, called sectors. Between two sectors (and even inside sectors!) is a gap that the controller needs as a little spare time to relax from the hard work of reading data. Controller? What's that? Well, simplified version first: A device that gets data from the processor, codes it, and writes it onto disk. It also reads bit streams from the disk and decodes it. To transfer data to and from the controller, the built-in DMA chip in the ST carries data from main memory to the FDC (floppy disk controller) and also from the controller to memory. The processor itself doesn't have to do this dirty work; while DMA (direct memory access) is working, the processor can do anything else (have tea or dinner with Gershwin or go shopping...). The controller sends electrical impulses to the disk drive's read/write head (very similar to the one in the ghetto blaster cassette recorder that the son of your neighbour uses to test your resistance against noise pressure). These impulses are coded using the so-called MFM method (modified frequency modulation) which packs clock bits into the data stream. This ensures that reading and writing is safe even with drives that have problems with holding the correct speed. MORE THAN PURE DATA On a disk you'll find more than data that you have transferred thereto by a XBIOS or BIOS call. Additionally, some control information is hidden between the data. A typical track looks like this (well, OK, it's concentric in reality; please use your imagination): ================================================================ Intro (trackheader or "Post Index Mark") (*) gap bytes (Pre Address Mark) 3 synchronization bytes address mark & sector info checksum gap bytes (Post Address Mark) 3 synchronization bytes data mark & data checksum gap bytes (Post Data Mark) back to (*), until all sectors are written, then: gap bytes to track end =============================== What the heck are synchronization bytes? In short: Bit sequences, that tell the controller as it reads them "Go on, old fella, a new byte's starting right now". For the controller just reads a string of bits from disk without knowing exactly where a byte begins. The controller has a dedicated department inside that only fiddles with sorting those bit combinations out of the bit stream that are meant as sync bytes (as you call them shortly). A BUG IN THE CONTROLLER In normal cases, this sync unit (officially called Address Mark Detector) only reacts upon the bytes $A1 and $C2 which have to be stored in a special format (MFM with missing clock bits; just to mention it for the professionals who read my articles in "ST- Computer"). Unfortunately, there is an eddy in the controller (if you want to know exactly about eddies, I recommend you "Life, The Universe And Everything" by Douglas Adams). A bit string of %000101001 can fool the sync byte detection department completely! Whenever this bit string occurs, the controller thinks there is a sync byte and starts to synchronize (what else should it do). All the following bytes are shifted in a certain way and their bits shattered all over the place. This error only occurs when reading a complete track with all informations on it (with the controller's read-track command). When reading sectors (with read-sector), the controller switches off the sync unit, so that it can read without the sync bug confusing it. To make it clear (for the ones who only believe what they see): Suppose you want to write the following onto disk: FE 29 00 01 (all bytes in hex) Reading these bytes with a read-track command you get: FE 14 7F FE Strange, eh? The error occurs with the following bytes: $29 and previous even byte $52/$53 and previous byte dividable by 4 $A4 to $A7 and previous byte dividable by 8 $14 and following byte's first bit (MSB) set to 1 Some other byte combinations can cause the error, too (shifts of the byte sequences above, for example). As long as these data are packed into a sector (which you can easily read correctly using the read-sector command), everything's fine. As soon as you put $FE29 (for example) in an area unreachable by read-sector (or read-address, which reads a sector info sequence), however, you're in a mess. Naturally, copy programmers know about this quirk. Consequently, they use read-sector commands. They also know that gap bytes (which are normally $4E or $00) aren't read correctly in most cases but somehow shifted (from one to seven bits, even by "half bits"). So you cannot rely on what you're reading with read-track. Most programmers therefore suppose that gaps consist of $4E's and $00's and nothing else, basta. DEVELOPING THE PROTECTION Now you could insert a string like "(C) 1987 by Claus Brod" into those gap bytes. As normal copy programs don't check the contents of the gap areas, they would fail in copying this string - so you can detect a copy from within your program. Stop press! How can you check your copyright string if the controller reads something different every time you use the read- track command? In one in ten cases, approximately, you will read the correct string (perhaps never!); in all other cases you will get something that your copy protection doesn't recognize - crashdown & bomb attack (very user friendly, isn't it?)... Now those mysterious sync bytes come into action. These bytes make the controller read the following stuff correctly (in most cases). Normally, a track starts with approx 60 gap bytes of $4E's. Instead, we write: 4E 4E 4E A1 A1 A1 copyright string 4E 4E ... This synchronizes the controller in a read-track command so that it can at least read the copyright string correctly. Of course, the copyright string must not contain any bytes or byte sequences that may fool the sync department. In your copy protection routine you read a complete track and look for your copyright text. If it's there, it's the original disk (or someone's got a very very good copy program). If not, something went into wrong channels. I tried to copy this protection method with several copy programs - all of them were a complete loss except one (I won't tell you which one, no, no, no). So I had to refine the protection. Now I use the "forbidden" sync byte $29 with a previous even byte to synchronize the controller. Synchronization is shifted by half a byte after $29, so we have to add an ordinary $A1 sync byte to make the sun shine again. These two bytes are read as $14 $0B which no copy program recognizes as correct sync sequence. The following byte sequence (your copyright string) is now interpreted correctly with every Read-Track command. And there you are: the perfect protection (at least until someone comes up with a hyper-ultra-jolly-good copy program). MEN AT WORK Let's work: How do you program such a protection? To give you full information about programming the controller and DMA chip and whatever relates to this problem I would have to write an article that spans three or four issues of ST NEWS, leaving just enough space for the editorial and nothing else. In order to cut this neverending story short, you're presented a prefab routine from my software lab which you can use to implement the protection method. In the PROGRAMS folder, you'll find a GfA Basic program called PROTECT.BAS, which creates a machine routine in a string (for the real big chucks, there is an assembler listing on this disk). This machine code reads and writes an ordinary 9-sector track 41 - nearly ordinary: the sync byte combination followed by a copyright message is copied into the gap before the first sector. Why do I write track 41? 41 in hex gives $29; this byte occurs in every sector intro of track 41 and therefore confuses the read- track command. Therefore, this track is never read correctly; all the copy programs I know switch to sector mode on this track - reducing the probability that someone can copy our protection (which is only possible by reading the whole track, analyzing it, and writing it back with some corrections). HOW TO USE "THE PROTECTOR" Just format a disk (single or double sided, doesn't matter) with the DESKTOP format utility and then start the routine in PROTECT.BAS. Choose option 2 to write a protected track 41. You are asked a password string that you want to write onto disk. Afterwards, you copy data and programs to this disk; among them the program to be protected. Option 1 of "The Track 41 Protector" reads a complete track 41 from disk and searches for a password that you can freely choose. The password can have up to 50 characters. If you need more, adjust Gap1$ in Procedure Mktrk. How do you include the routines in your own programs? GfABASIC programmers are better off this time; it should be no problem, however, to adapt the routines for FASTBASIC or whatever. To call the machine code routines, you have to poke values into the routine's parameter field. If the program starts at location START, you have to store a mode longword into location START+2 and the address of a track buffer into START+6 (see BASIC listing). A mode of '0' activates the Read-Track routine, '1' makes the program write a track from the buffer referred to by the address pointer in START+6. Assembler programmers are supplied with the source code (for AS68) on this disk. The routine must be called with 'bsr' or 'jsr' as it terminates with a simple 'rts'. It is fully relocatable (for it had to be transferred into a BASIC program) and PC-relative. You have to set up the track to be written before calling the machine code. In Procedure Mktrk you'll find all the information you're looking for. To satisfy the curious (curiosity kills the cat, fellas!): Here is another version of the table above including the codes that the controller needs to create a track. =============================== Intro: 60 bytes of $4E (*) gap bytes: 12 bytes of $00 sync bytes: 3 bytes of $F5 address mark: $FE sector info: track number, side number, sector number, size (0=128 bytes, 1=256 bytes, 2=512 bytes, 3=1024 bytes) checksum: $F7 gap bytes: 22 bytes of $4E 12 bytes of $00 sync bytes: 3 bytes of $F5 data mark: $FB data: 512 bytes (lower than $F4!!!) checksum: $F7 gap bytes: 40 bytes of $4E back to (*), until all sectors are written, then: gap bytes to track end (normally 1400 bytes of $4E, which is more than enough to fill the track) =============================== You may wonder why sync bytes are written as $F5 but read as $A1. The reason is that the controller needs an own control "language" when writing a track. Bytes exceeding a value of $F4 are special control bytes that are written in other formats than ordinary bytes. $F5, for example, is a sync byte of $A1 with certain clock bits missing. $F7 writes a checksum onto disk (consisting of two bytes). $FB and $FE announce data and sector info, respectively. By the way, this control language is the reason why you must not format your disks (using the XBIOS call) with certain "virgin" values (the virgin word's bytes must not exceed $F4). Some additional comments concerning the BASIC program and its usage: '0' in the main menu terminates the program (what else did you expect?). '3' shows the contents of the track buffer in both hex and ASCII; to stop output, press any key; then press 'X' to leave the routine or any other key to continue. The track buffer (in this program buf$) must be at least 7K. If you want to use 10-sector disks you only have to change the "FOR T=1 TO 9" loop in Procedure Mktrk into "FOR T=1 TO 10". If you want to know more about disk drives you are given three possibilities: read my "Floppyspielereien" articles in the German computer magazine "ST Computer" or buy my book about floppy programming (out in late fall) or write to ST NEWS. If there are some readers out there who are interested to read more about disk drives in ST NEWS: Write to ST NEWS and reveal your innermost wishes... depending on demand I will continue to write about floppy programming in ST NEWS from time to time. Claus Brod Am Felsenkeller 2 D-8772 Marktheidenfeld West Germany Originally published in ST NEWS Volume 2 Issue 7. ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo '*************************** '**** Gfa Basic listing **** '*************************** ' ' THE TRACK 41 PROTECTOR ' (C) 1987 by Claus Brod ' Am Felsenkeller 2 ' D-8772 Marktheidenfeld ' Tel. (West Germany) 09391-3206 ' ' Last update 28.9.87 ' Installs a copy protection on a blank disk (track 41) ' Written exclusively for STNEWS ' ' Ron$=Chr$(27)+"p" Roff$=Chr$(27)+"q" @Init @Dispatch End ' ************ ' ****dispatch: shows menu and waits for choice ' ************ Procedure Dispatch Repeat Cls Print At(28,1);Ron$;"THE TRACK 41 PROTECTOR";Roff$ Print At(28,2);"(C) 1987 by Claus Brod" Print At(28,5);"(0) Exit" Print At(28,6);"(1) Read Track 41 & Check" Print At(28,7);"(2) Write Track 41" Print At(28,8);"(3) Show Track Buffer" Repeat A$=Upper$(Input$(1)) Until A$>="0" And A$<"4" Choice=Val(A$) On Choice Gosub Read,Write,Showtrk Until Choice=0 Return ' ' *************** ' ****** read: inputs password, reads track 41 into buffer ' ****** and looks for password ' *************** Procedure Read Print Print Ron$;"Read Track 41";Roff$ Line Input "Copyright string";Copy$ Buf$=String$(8000,Chr$(0)) @Rdtrk(Varptr(Buf$)) A=Instr(Buf$,Copy$) If A=0 Print "Copyright message not found" Else Print "Copyright message found at offset ";A Endif Void Inp(2) Return ' ' ************** ' ***** rdtrk: Reads track 41 from disk into buffer ' ***** (pointed to by pointer%) ' ************** Procedure Rdtrk(Pointer%) Prg%=Varptr(Prg$) ! start of machine code Lpoke Prg%+2,0 ! mode 0 (read track) Lpoke Prg%+6,Pointer% ! pointer to buffer (at least 7K please!) Call Prg% ! call mcode Return ' ' ************* ' ***** write: creates & writes track 41 from buffer including a password ' ************* Procedure Write Print Print Ron$;"Write Track 41";Roff$ Line Input "Copyright string";Copy$ @Mktrk(Copy$) @Wrtrk(Varptr(Buf$)) Return ' ' ************** ' ****** wrtrk: writes track 41 from buffer pointed to by ' ****** pointer% ' ************** Procedure Wrtrk(Pointer%) Prg%=Varptr(Prg$) ! start of machine code Lpoke Prg%+2,1 ! mode 1 (write track) Lpoke Prg%+6,Pointer% ! start of buffer containing track data Call Prg% ! call mcode Return ' ' *************** ' ****** mktrk: constructs 9-sector track 41 with password c$ ' *************** Procedure Mktrk(C$) Sync$=Chr$(&HF5)+Chr$(&HF5)+Chr$(&HF5) ! sync bytes Sec$=String$(512,Chr$(203)) ! sector data Gap1$=String$(60,Chr$(&H4E)) ! gap1 Gap2$=String$(12,Chr$(0)) ! gap2 Gap31$=String$(22,Chr$(&H4E)) ! gap31 Gap32$=Gap2$ ! gap32 Gap4$=String$(40,Chr$(&H4E)) ! gap4 Gap5$=String$(1401,Chr$(&H4E)) ! gap5 Mid$(Gap1$,3)=Chr$(&H29)+Chr$(&HF5) ! syncs into gap1 Print "Password is ";C$ ! print password again Mid$(Gap1$,5)=C$ ! password to gap1 ' Buf$=Gap1$ ! start of buf$ For T=1 To 9 Buf$=Buf$+Gap2$+Sync$+Chr$(&HFE)+Chr$(41)+Chr$(0)+Chr$(T)+Chr$(2) ' start of sector, address header Buf$=Buf$+Chr$(&HF7) ' checksum Buf$=Buf$+Gap31$+Gap32$+Sync$+Chr$(&HFB) ' gap before data and data mark Buf$=Buf$+Sec$+Chr$(&HF7)+Gap4$ ' sector data and checksum and end gap Next T Buf$=Buf$+Gap5$ ! gaps to track end Return ' ' ********************** ' ***** init: reads machine code into prg$ ' ********************** Procedure Init Restore Protect Do Read A$ Exit If A$="*" Prg$=Prg$+Chr$(Val("&h"+A$)) Loop Return ' ' ********************** ' ****** showtrk: Shows buf$ in hex and ASCII ' ****** press any key to stop then 'X' to exit ' ****** any other key continues ' ********************** Procedure Showtrk Print Print Ron$;"Show Track Buffer - press any key to stop, then X to exit";Roff$ For T=1 To Len(Buf$) Step 16 Z$="" A$=Hex$(T-1) While Len(A$)<4 A$="0"+A$ Wend Print "$";A$;" "; For I=0 To 15 A$=Hex$(Asc(Mid$(Buf$,T+I,1))) If Len(A$)=1 A$="0"+A$ Endif Print A$' Z=Val("&h"+A$) If Z>31 And Z<128 Z$=Z$+Chr$(Val("&h"+A$)) Else Z$=Z$+"." Endif Next I Print " ";Z$ K$="" If Inkey$>"" K$=Input$(1) Endif Exit If Upper$(K$)="X" Next T Return ' ' ********************** ' * machine code data ' ********************** Protect: Data 60,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0 Data 0,0,0,0,0,0,0,0,48,E7,FF,FE,42,80,61,0 Data 0,AE,45,FA,FF,E6,24,80,45,FA,FF,E4,34,BC,0,0 Data 2C,3A,FF,D0,50,F9,0,0,4,3E,BC,BC,0,0,0,0 Data 67,0,0,A4,BC,BC,0,0,0,1,67,0,0,E0,51,F9 Data 0,0,4,3E,45,FA,FF,B4,20,12,61,72,4C,DF,7F,FF Data 4E,75,32,3C,0,1E,61,A,33,C7,0,FF,86,4,32,3C Data 0,1E,51,C9,FF,FE,4E,75,32,3C,0,FA,51,C9,FF,FE Data 22,3C,0,4,0,0,8,39,0,5,0,FF,FA,1,67,C Data 53,81,66,F2,1E,3C,0,D0,61,40,4E,75,33,FC,1,80 Data 0,FF,86,6,32,3C,0,1E,61,C8,30,39,0,FF,86,4 Data 32,3C,0,1E,60,BC,13,C7,0,FF,86,D,E0,8F,13,C7 Data 0,FF,86,B,E0,8F,13,C7,0,FF,86,9,4E,75,2F,0 Data 3F,3C,0,20,4E,41,5C,8F,4E,75,1E,3C,0,D0,61,82 Data 32,3C,0,FA,60,8C,38,3C,0,2,61,0,0,D6,38,3C Data 0,29,61,0,0,92,61,C,38,3C,0,0,61,0,0,C4 Data 60,0,FF,4C,2E,3A,FF,0,61,AC,61,0,0,9C,3E,3C Data 0,E,61,0,FF,4E,33,FC,0,80,0,FF,86,6,3E,3C Data 0,E0,61,0,FF,3E,61,0,FF,50,4E,75,38,3C,0,2 Data 61,0,0,90,38,3C,0,29,61,4C,61,C,38,3C,0,0 Data 61,0,0,80,60,0,FF,8,2E,3A,FE,BC,61,0,FF,68 Data 33,FC,1,90,0,FF,86,6,33,FC,0,90,0,FF,86,6 Data 33,FC,1,90,0,FF,86,6,3E,3C,0,1F,61,0,FE,F4 Data 33,FC,1,80,0,FF,86,6,3E,3C,0,F0,61,0,FE,E4 Data 61,0,FE,F6,4E,75,33,FC,0,86,0,FF,86,6,3E,4 Data 61,0,FE,D0,33,FC,0,80,0,FF,86,6,3E,3C,0,11 Data 61,0,FE,C0,60,0,FE,D2,33,FC,0,90,0,FF,86,6 Data 33,FC,1,90,0,FF,86,6,33,FC,0,90,0,FF,86,6 Data 4E,75,48,E7,FF,FE,3E,4,66,14,33,FC,0,80,0,FF Data 86,6,32,39,0,FF,86,4,8,1,0,7,66,F4,A,7 Data 0,7,CE,3C,0,7,40,E7,0,7C,7,0,13,FC,0,E Data 0,FF,88,0,10,39,0,FF,88,0,C0,3C,0,F8,8E,0 Data 13,C7,0,FF,88,2,46,DF,4C,DF,7F,FF,4E,75,0,0 Data * ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ************************** * Assembler Code Listing * ************************** * Track 41 Protector * Machine code routine for * protecting track 41 * * Reads and writes track 41 into or from buffer * (C) 1987 by Claus Brod * Am Felsenkeller 2 * D-8772 Marktheidenfeld * Tel. (West Germany) 09391/3206 * ************************** ************************* * Some definitions for the long way home ************************* mfp = $fffa01 * Address of MFP68901 for polling daccess= $ff8604 * DMA-Controller, FDC access or sector counter dmode = $ff8606 * DMA-Controller, DMA mode dlow = $ff860d * DMA-Controller, start of transfer, lowbyte dmid = $ff860b * DMA-Controller, start of transfer, midbyte dhigh = $ff8609 * DMA-Controller, start of transfer, highbyte time = $40000 * Timeout constant snd = $ff8800 * Address of sound chip sndwrt = $ff8802 * I/O of sound chip ************************* * Jump into the unknown... ************************* bra main * Jump to program start ************************* * I/O field for parameter passing ************************* mode: .dc.l 0 * 0 = read track 41 into buffer * * 1 = write track 41 from buffer buf: .dc.l 0 * buffer address stk: .dc.l 0 * buffer for stackpointer *************************** * main routine; inits and dispatches *************************** main: movem.l d0-d7/a0-a6,-(sp) * hide them away! clr.l d0 * user stack becomes supervisor stack bsr super * supervisor mode on lea stk(pc),a2 * address of stackpointer buffer move.l d0,(a2) * save stackpointer move.l mode(pc),d6 * get mode st $43e * block disk VBL cmp.l #0,d6 * read track? beq rdtrack * yessir cmp.l #1,d6 * write track? beq wrtrack * yesma'am exitus: sf $43e * free disk VBL lea stk(pc),a2 * address of stackpointer buffer move.l (a2),d0 * get old stackpointer bsr super * supervisor mode off movem.l (sp)+,d0-d7/a0-a6 * get registers rts * and exit ****************************** * wrfdc: send d7.b to controller ****************************** wrfdc: move.w #30,d1 * counter is 30 bsr as_time_goes_by * time flies... move.w d7,daccess * d7 into access register of DMA chip move.w #30,d1 * counter is 30 ****************************** * as_time_goes_by: loop with d1 iterations ****************************** as_time_goes_by: dbf d1,as_time_goes_by * Looping (huiii...) rts * back to the future ****************************** * wait_until_dawn : waits for FDC IRQ ****************************** wait_until_dawn: move.w #250,d1 * wait for BUSY wt: dbra d1,wt move.l #time,d1 * timeout constant poll: btst #5,mfp * IRQ on MFP? beq ready * yes, command is executed subq.l #1,d1 * timeout counter shrinks bne poll * ready already? bsr irq * interrupt controller rts * go to Hollywood *************************** * DMA transfer or FDC ready *************************** ready: move.w #$180,dmode * command register move.w #30,d1 * wait bsr as_time_goes_by move.w daccess,d0 * read FDC status move.w #30,d1 * wait bra as_time_goes_by **************************** * dma: set DMA;IN: d7 start address **************************** dma: move.b d7,dlow * lowbyte lsr.l #8,d7 * shift one byte move.b d7,dmid * midbyte lsr.l #8,d7 * shift one byte move.b d7,dhigh * highbyte rts ***************************** * super: switches from usermode to * supervisormode and vice versa * d0: stackpointer **************************** super: move.l d0,-(sp) * stackpointer to stack move.w #$20,-(sp) * SUPER trap #1 * in GEMDOS addq.l #6,sp * correct stack rts ************************** * irq: interrupts the controller ************************** irq: move.b #$D0,d7 * FORCE IRQ bsr wrfdc * d7 to FDC move.w #250,d1 * 250 loops bra as_time_goes_by * patience! ***************************** * rdtrack: read track 41, side 0 on drive A ***************************** rdtrack: move.w #2,d4 * select drive A, side 0 bsr do_select * select routine move.w #41,d4 * track 41 bsr seek_it * go for it! bsr rdtrk * read track move.w #0,d4 * deselect drive A bsr do_select bra exitus * and back to mothership ***************************** * rdtrk: read track; IN: d4 track to read ***************************** rdtrk: move.l buf(pc),d7 * address of track buffer bsr dma * init DMA bsr toggle * clear DMA status move.w #14,d7 * 14 sectors bsr wrfdc * d7 to FDC move.w #$80,dmode * command register move.w #$E0,d7 * ReadTrack command bsr wrfdc * d7 to FDC bsr wait_until_dawn * wait for command to terminate rts ***************************** * wrtrack: write track 41, side 0 on drive A ***************************** wrtrack: move.w #2,d4 * drive A, side 0 bsr do_select * select it move.w #41,d4 * track 41 bsr seek_it * go for it! bsr wrtrk * write track from buffer move.w #0,d4 * deselect drives bsr do_select bra exitus * don't leave me this way... ***************************** * wrtrk: write track; IN: d4 track no ***************************** wrtrk: move.l buf(pc),d7 * track buffer address to d7 bsr dma * init DMA move.w #$190,dmode * toggle read/write line move.w #$90,dmode * clears DMA status move.w #$190,dmode * select sector counter of DMA chip move.w #$1F,d7 * 31 sectors bsr wrfdc * d7 to FDC move.w #$180,dmode * select command register move.w #$F0,d7 * WriteTrack command bsr wrfdc * d7 to FDC bsr wait_until_dawn * wait for command to terminate rts ***************************** * seek_it : seek track in d4 ***************************** seek_it: move.w #$86,dmode * select data register move.w d4,d7 * get track number bsr wrfdc * d7 to FDC move.w #$80,dmode * command register move.w #17,d7 * seek command for 3ms step rate bsr wrfdc * d7 to FDC bra wait_until_dawn * wait for FDC **************************** * toggle: toggles R/W-line of the DMA chip, * thereby clearing DMA status **************************** toggle: move.w #$90,dmode move.w #$190,dmode move.w #$90,dmode * select DMA sector register rts ***************************** * do_select; IN: d4 drive number (2 for A, 4 for B, side no in bit 0) ***************************** do_select: movem.l d0-d7/a0-a6,-(sp) * save registers move.w d4,d7 * get drive number bne mach_mal * if not zero, go to start move.w #$80,dmode * status register motor: move.w daccess,d1 * read FDC status btst #7,d1 * motor still running? bne motor * yessir mach_mal: eor.b #7,d7 * invert bits and.b #7,d7 * and mask them move.w sr,-(sp) * save status (not necessary here) or.w #$700,sr * switch off interrupts move.b #14,snd * select port A register move.b snd,d0 * read port A and.b #$f8,d0 * mask the lower three bits or.b d0,d7 * set new side/drive move.b d7,sndwrt * in port A move.w (sp)+,sr * get status back (not necessary) movem.l (sp)+,d0-d7/a0-a6 * get registers rts * and leave me alone