NetBoot/ChainLoader.a
Elliot Nunn c5947f2652 Init performance fix
Quite an embarrassing one. I search the stack for a known 4-byte value.
This search starts at the stack pointer and continues towards low
memory. Oops! I was searching nearly the entire address space.
2021-03-02 19:17:13 +08:00

737 lines
27 KiB
Plaintext

kUserRecLen equ 568
kInitialWaitMsec equ 10000
kSubsequentWaitMsec equ 1000
Code
; The ROM issued a _Read call that eventually reached here.
; We need to handle this _Read trap directly, without the intervening .netBOOT and .ATBOOT drivers.
; We do this by rewinding the Device Manager's return address by 2 bytes.
; Why?
; The ROM network boot mechanism is complicated and buggy.
; The workarounds are difficult.
; Therefore this big hack obviates many little hacks.
; First problem: this is not a safe place to keep code. Jump away.
; (Using the heap while all the netBOOT junk is there would cause fragmentation.)
lea ResumeAfterCopy,A0
move.l #CodeEnd-ResumeAfterCopy,D0
lea 4096(A5),A1
dc.w $A02E ; _BlockMove
jmp 4096(A5)
ResumeAfterCopy
; Salvage some possibly useful information from ATBOOT, while it is still running:
; Server address
move.l 8(SP),A0 ; global pointer
move.l 24(A0),D0 ; AddrBlock
lea gSaveAddr,A0
move.b #10,15(A0) ; hardcode DDP protocol ID
move.b D0,13(A0) ; socket
lsr.w #4,D0
lsr.w #4,D0
move.b D0,11(A0) ; node
swap D0
move.w D0,7(A0) ; network
; User record
lea gUserRec,A1
move.l 8(SP),A0 ; global pointer
lea 46(A0),A0
move.l #kUserRecLen,D0
dc.w $A02E ; _BlockMove
; Currently the call stack looks like this:
; ROM _Read to get boot blocks
; .netBOOT Read routine
; .ATBOOT Control routine
; direct call to this block of code
; But we want to close and remove .netBOOT & .ATBOOT, so we need to return
; from their Device Manager calls, and steal control from the ROM.
; We do this by scanning the stack for the return address of the original _Read.
move.l $2AE,A0 ; A0 = ROMBase (lower limit)
lea $4000(A0),A1 ; A1 = ROMBase + a bit (upper limit)
move.l SP,A2
.loop addq.l #2,A2 ; A2 = where we search the stack
move.l (A2),A3 ; A3 = potential return address
cmp.l A0,A3 ; lower limit check
bls.s .loop
cmp.l A1,A3 ; upper limit check
bhi.s .loop
cmp.w #$A002,-2(A3) ; _Read trap check
bne.s .loop
pea GoHereFromReadTrap ; take over
move.l (SP)+,(A2)
lea ROMAfterReadTrap,A2 ; save original for later
move.l A3,(A2)
moveq.l #-1,D0 ; .netBOOT/.ATBOOT don't do any more damage if an error is returned
rts
ROMAfterReadTrap
dc.l 0
GoHereFromReadTrap
; Now we are outside .netBOOT/.ATBOOT. We can shut them down, and set up our driver in a clean environment.
move.l ROMAfterReadTrap,-(SP) ; our return address is to ROM
sub.l #2,(SP) ; repeating the _Read trap
movem.l A0-A6/D0-D7,-(SP) ; save registers conservatively (especially A0)
; A4 = param block to the .netBOOT _Read call, because we will use it a lot
move.l A0,A4
; Close and delete .netBOOT (which will close .ATBOOT)
lea -$32(SP),SP
move.l SP,A0
move.w $18(A4),$18(A0) ; ioRefNum
dc.w $A001 ; _Close
lea $32(SP),SP
move.w $18(A4),D0 ; ioRefNum
dc.w $A03E ; _DrvrRemove
move.w #-51,D0 ; ioRefNum ; also delete .ATBOOT for neatness
dc.w $A03E ; _DrvrRemove
; A3 = our driver in sysheap (plus user record)
move.l #DrvrEnd-DrvrBase+kUserRecLen,D0
dc.w $A51E ; NewPtrSys
move.l A0,A1
lea DrvrBase,A0
move.l #DrvrEnd-DrvrBase+kUserRecLen,D0
dc.w $A02E ; BlockMove
move.l A1,A3
; Install the driver in the unit table. Take over netBOOT's old unit number.
move.w $18(A4),D0 ; ioRefNum
dc.w $A43D ; _DrvrInstall ReserveMem
; That call created a driver control entry (DCE). Find and lock.
move.l $11C,A0 ; UTableBase
move.w $18(A4),D0 ; ioRefNum
not.w D0
lsl.w #2,D0
add.w D0,A0
move.l (A0),A0
dc.w $A029 ; _HLock
move.l (A0),A0
; Populate the empty DCE that DrvrInstall left us (forget fields related desk accessories)
move.l A3,(A0) ; dCtlDriver = driver pointer (not handle)
move.w (A3),4(A0) ; dCtlFlags = drvrFlags
; Open our driver
lea -$32(SP),SP
move.l SP,A0
bsr DrvrClearBlock
lea DrvrName,A1
move.l A1,$12(A0) ; IOFileName
dc.w $A000 ; _Open
lea $32(SP),SP
; Add the a drive queue entry (DQE).
lea dqLink-DrvrBase(A3),A0
move.l $16(A4),D0
dc.w $A04E ; _AddDrive (A0=DQE, D0=drvnum/drefnum)
; Open .MPP (still open?) & our DDP socket
lea -$32(SP),SP
move.l SP,A0
bsr DrvrClearBlock
pea MPPName
move.l (SP)+,$12(A0) ; ioNamePtr
dc.w $A000 ; _Open
move.w #248,$1A(A0) ; csCode = openSkt
move.b #10,$1C(A0) ; socket = 10, same as ATBOOT uses
pea DrvrSockListener-DrvrBase(A3)
move.l (SP)+,$1E(A0) ; listener
dc.w $A004 ; _Control
lea $32(SP),SP
; Re-execute the _Read trap in ROM
movem.l (SP)+,A0-A6/D0-D7
rts
MPPName dc.b 4, '.MPP', 0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrBase
dc.w $4F00 ; dReadEnable dWritEnable dCtlEnable dStatEnable dNeedLock
dc.w 0 ; delay
dc.w 0 ; evt mask
dc.w 0 ; menu
dc.w DrvrOpen-DrvrBase
dc.w DrvrPrime-DrvrBase
dc.w DrvrControl-DrvrBase
dc.w DrvrStatus-DrvrBase
dc.w DrvrClose-DrvrBase
DrvrName dc.b 8, ".netBOOT", 0
g
gImage dc.l 0 ; "configuration mode" by default
gMyDCE dc.l 0
gExpectHdr dc.l 0
gProgress dc.l 0
gQuery dcb.b 20 ; really need to consider the length of this!
gWDS dcb.b 2+4+2+4+2+4+2 ; room for an address and two data chunks
gMyPB dcb.b $32+2 ; allow us to clear it with move.l's
odd
gSaveAddr dcb.b 16
gAddr dcb.b 16
even
; Time Manager task
tmLink dc.l 0
tmType dc.w 0
tmAddr dc.l 0
tmCount dc.l 0
; Drive queue element
dqFlags dc.l $00080000
dqLink dc.l 0
dqType dc.w 1
dqDrive dc.w 0
dqRefNum dc.w 0
dqFSID dc.w 0
dqDrvSz dc.w 0
dqDrvSz2 dc.w 0
; a0=iopb, a1=dce on entry to all of these...
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrOpen
lea gMyDCE,A2 ; dodgy, need this for IODone
move.l A1,(A2)
move.w #0,$10(A0) ; ioResult
rts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrPrime
; Convert the ioPosOffset in the parameter block to a block offset (allows large vol support).
btst.b #0,$2C(A0) ; test ioPosMode & kUseWidePositioning
bne.s .wide
.notwide move.l $10(A1),D0
bra.s .gotD0
.wide move.l $2E(A0),D0 ; the block offset can only be up to 32 bits
or.l $32(A0),D0
.gotD0 ror.l #4,D0 ; now D0 = (LS 23 bits) followed by (MS 9 bits)
ror.l #5,D0
move.l D0,$2E(A0) ; ioPosOffset = D0 = byte_offset/512
; Return with a "pending" ioResult.
move.w #1,$10(A0) ; ioResult = pending. We will return without an answer.
; Wang the state machine!
cmp.b #2,7(A0) ; ioTrap == _Read?
bne DrvrSendWrite ; transition from "Idle" to "Await Comp Send Write Packet"
bra DrvrSendRead ; transition from "Idle" to "Await Comp Send Read Packet"
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrReSendRead
; Time Manager task
lea tmLink,A0
dc.w $A059 ; _RmvTime
move.l gMyDCE,A1 ; get Device Mgr registers
move.l 6+2(A1),A0
; truncate ioActCount
and.l #-32*512,$28(A0) ; truncate ioActCount
DrvrSendRead
; Called from Prime routine, socket listener or Time Manager:
; PB/DCE in A0/A1 must be preserved (but we only require A0)
lea gExpectHdr,A2
clr.w (A2)+
addq.w #1,(A2) ; packet filter sequence word
lea gProgress,A2
clr.l (A2)
bsr.s DrvrCopyAddrStruct
lea gQuery,A2
move.w #$8000,(A2)+ ; Means a polite request
move.w gExpectHdr+2,(A2)+
move.l gImage,(A2)+
move.l $28(A0),D0 ; [ioActCount (in bytes)
lsr.l #4,D0
lsr.l #5,D0 ; / 512]
add.l $2E(A0),D0 ; + ioPosOffset (in blocks)
move.l D0,(A2)+ ; -> "offset" field of request
move.l $24(A0),D0 ; [ioReqCount (in bytes)
sub.l $28(A0),D0 ; - ioActCount (in bytes)]
lsr.l #4,D0
lsr.l #5,D0 ; / 512
move.l D0,(A2)+ ; -> "length" field of request
lea gWDS,A2
clr.w (A2)+ ; WDS+0: reserved field
pea gAddr
move.l (SP)+,(A2)+ ; WDS+2: pointer to address struct
move.w #16,(A2)+ ; WDS: push pointer/length
pea gQuery
move.l (SP)+,(A2)+
clr.w (A2)+ ; WDS: end with zero
move.l A0,-(SP)
lea gMyPB,A0
bsr DrvrClearBlock
pea DrvrDidSendRead
move.l (SP)+,$C(A0) ; ioCompletion
move.w #-10,$18(A0) ; ioRefNum = .MPP
move.w #246,$1A(A0) ; csCode = writeDDP
move.b #10,$1C(A0) ; socket = 10 (hardcoded)
move.b #1,$1D(A0) ; set checksumFlag
pea gWDS
move.l (SP)+,$1E(A0) ; wdsPointer to our WriteDataStructure
dc.w $A404 ; _Control ,async
move.l (SP)+,A0
rts
DrvrCopyAddrStruct
movem.l A0-A1,-(SP)
lea gSaveAddr,A0
lea gAddr,A1
moveq.l #16,D0
dc.w $A22E ; _BlockMoveData
movem.l (SP)+,A0-A1
rts
DrvrDidSendRead
; Called as completion routine: PB/result in A0/D0, must preserve all registers other than A0/A1/D0-D2
lea gExpectHdr,A0
move.w #$8100,(A0) ; Enable packet reception
move.l #kInitialWaitMsec,D0
DrvrInstallReSendRead
; Called from anywhere, D0=waittime, can clobber anything
lea tmLink,A0
pea DrvrReSendRead
move.l (SP)+,tmAddr-tmLink(A0)
move.l D0,-(SP)
dc.w $A058 ; _InsTime
move.l (SP)+,D0
dc.w $A05A ; _PrimeTime
rts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrReSendWrite
; Time Manager task
lea tmLink,A0
dc.w $A059 ; _RmvTime
move.l gMyDCE,A1 ; get Device Mgr registers
move.l 6+2(A1),A0
; truncate ioActCount
sub.l #512,$28(A0)
and.l #-32*512,$28(A0) ; truncate ioActCount
DrvrSendWrite
; Called from Prime routine, socket listener or Time Manager:
; PB/DCE in A0/A1 must be preserved (but we only require A0)
; D1 = block index within chunk
move.l $28(A0),D1
lsr.l #5,D1
lsr.l #4,D1
move.l D1,D0
and.l #32-1,D1
move.l $28(A0),D2
add.l #512,D2
cmp.l $24(A0),D2
bne.s .notLastBlock
bset #7,D1
.notLastBlock
; D0 = first block of chunk
and.l #-32,D0
add.l $2E(A0),D0
tst.l D1
bne.s .notFirstBlockOfChunk
lea gExpectHdr,A2
clr.w (A2)+
addq.w #1,(A2) ; packet filter sequence word
.notFirstBlockOfChunk
bsr DrvrCopyAddrStruct
lea gQuery,A2
move.b #$82,(A2)+ ; Means a polite request
move.b D1,(A2)+ ; nth block of this chunk follows
move.w gExpectHdr+2,(A2)+
move.l gImage,(A2)+
move.l D0,(A2)+ ; first block of this chunk
lea gWDS,A2
clr.w (A2)+ ; WDS+0: reserved field
pea gAddr
move.l (SP)+,(A2)+ ; WDS+2: pointer to address struct
move.w #12,(A2)+ ; WDS: push length/ptr of header
pea gQuery
move.l (SP)+,(A2)+
move.w #512,(A2)+ ; WDS: push length/ptr of body
move.l $20(A0),D0 ; ptr = ioBuffer
add.l $28(A0),D0 ; + ioActCount
move.l D0,(A2)+
clr.w (A2)+ ; WDS: end with zero
move.l A0,-(SP)
lea gMyPB,A0
bsr DrvrClearBlock
pea DrvrDidSendWrite
move.l (SP)+,$C(A0) ; ioCompletion
move.w #-10,$18(A0) ; ioRefNum = .MPP
move.w #246,$1A(A0) ; csCode = writeDDP
move.b #10,$1C(A0) ; socket = 10 (hardcoded)
move.b #1,$1D(A0) ; set checksumFlag
pea gWDS
move.l (SP)+,$1E(A0) ; wdsPointer to our WriteDataStructure
dc.w $A404 ; _Control ,async
move.l (SP)+,A0
rts
DrvrDidSendWrite ; completion routine for the above control call..
; need to test whether to send another, or switch to wait mode...
move.l gMyDCE,A0
move.l 6+2(A0),A0 ; A3 = dCtlQHdr.qHead = ParamBlk
movem.l $24(A0),D0/D1 ; D0=ioReqCount, D1=ioActCount
add.l #512,D1
move.l D1,$28(A0)
cmp.l D0,D1
beq.s DrvrInstallReSendWrite
and.l #(32-1)*512,D0
beq.s DrvrInstallReSendWrite
bra DrvrSendWrite
DrvrInstallReSendWrite
lea tmLink,A0
pea DrvrReSendWrite
move.l (SP)+,tmAddr-tmLink(A0)
dc.w $A058 ; _InsTime
move.l #kInitialWaitMsec,D0
dc.w $A05A ; _PrimeTime
rts
DrvrClearBlock
move.w #$32/2-1,D0
.loop clr.w (A0)+
dbra D0,.loop
lea -$32(A0),A0
rts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrSockListener
; Registers on call to DDP socket listener:
; A0 Reserved for internal use by the .MPP driver. You must preserve this register
; until after the ReadRest routine has completed execution.
; A1 Reserved for internal use by the .MPP driver. You must preserve this register
; until after the ReadRest routine has completed execution.
; A2 Pointer to the .MPP driver's local variables. Elliot says: the frame and packet
; header ("RHA") are at offset 1 from A2 (keeps the 2-byte fields aligned)
; A3 Pointer to the first byte in the RHA past the DDP header bytes (the first byte
; after the DDP protocol type field).
; A4 Pointer to the ReadPacket routine. The ReadRest routine starts 2 bytes after the
; start of the ReadPacket routine.
; A5 Free for your use before and until your socket listener calls the ReadRest routine.
; D0 Lower byte is the destination socket number of the packet.
; D1 Word indicating the number of bytes in the DDP packet left to be read (that is,
; the number of bytes following the DDP header).
; D2 Free for your use.
; D3 Free for your use.
; Registers on entry to the ReadPacket routine
; A3 Pointer to a buffer to hold the data you want to read
; D3 Number of bytes to read; must be nonzero
; Registers on exit from the ReadPacket routine
; A0 Unchanged
; A1 Unchanged
; A2 Unchanged
; A3 Address of the first byte after the last byte read into buffer
; A4 Unchanged
; D0 Changed
; D1 Number of bytes left to be read
; D2 Unchanged
; D3 Equals 0 if requested number of bytes were read, nonzero if error
; Registers on entry to the ReadRest routine
; A3 Pointer to a buffer to hold the data you want to read
; D3 Size of the buffer (word length); may be 0
; Registers on exit from the ReadRest routine
; A0 Unchanged
; A1 Unchanged
; A2 Unchanged
; A3 Pointer to first byte after the last byte read into buffer
; D0 Changed
; D1 Changed
; D2 Unchanged
; D3 Equals 0 if requested number of bytes exactly equaled the size of the buffer;
; less than 0 if more data was left than would fit in buffer (extra data equals
; -D3 bytes); greater than 0 if less data was left than the size of the buffer
; (extra buffer space equals D3 bytes)
; cmp.b #10,-1(A3) ; DDP protocol type better be ATBOOT
; bne.s DrvrTrashPacket
moveq.l #4,D3
jsr (A4) ; Read the nice short packet header
bne DrvrTrashPacket
move.l -4(A3),D2
move.l gExpectHdr,D3 ; Check the packet header
eor.l D2,D3
swap D3
clr.b D3
bne DrvrTrashPacket
movem.l A0-A1/D0-D2,-(SP)
lea tmLink,A0
dc.w $A059 ; _RmvTime
movem.l (SP)+,A0-A1/D0-D2
btst #25,D2
bne.s DrvrDidReceiveWrite
DrvrDidReceiveRead
swap D2
and.l #32-1,D2 ; D2.L = block offset within 32blk chunk
move.l gMyDCE,A3
move.l 6+2(A3),A3 ; A3 = dCtlQHdr.qHead
move.l $28(A3),D3 ; D3 = .ioActCount...
lsr.l #5,D3
lsr.l #4,D3 ; ...now in number of blocks
and.l #-32,D3 ; ...rounded down to a block chunk
add.l D2,D3 ; ...added back the received block
asl.l #8,D3
add.l D3,D3 ; ... = byte offset within buffer
move.l $20(A3),A3 ; .ioBuffer
add.l D3,A3 ; A3 = ioBuffer + offset
move.l #512,D3 ; D3 = size
jsr 2(A4) ; ReadRest (A3=dest, D3=length)
bne DrvrTrashPacket
lea gProgress,A1 ; Skip the next step if this packet is a repeat
move.l (A1),D1
bset.l D2,D1 ; (we saved blkidx in D2 before ReadRest)
bne.s DrvrTrashPacket
move.l D1,(A1)
move.l gMyDCE,A0
move.l 6+2(A0),A0 ; dCtlQHdr.qHead
move.l $28(A0),D0 ; Increment ioActCount and cmp with ioReqCount
add.l #512,D0
move.l D0,$28(A0)
cmp.l $24(A0),D0
beq.s DrvrIODone
addq.l #1,D1 ; If bitmap=$FFFFFFFF then get the next chunk of 32
beq DrvrSendRead ; A0 must be the PB
move.l #kSubsequentWaitMsec,D0
bra DrvrInstallReSendRead ; Just return to await more packets.
DrvrDidReceiveWrite
moveq.l #0,D3
jsr 2(A4) ; ReadRest (D3=0 i.e. discard)
move.l gMyDCE,A0
move.l 6+2(A0),A0 ; A3 = dCtlQHdr.qHead = ParamBlk
movem.l $24(A0),D0/D1 ; D0=ioReqCount, D1=ioActCount
cmp.l D0,D1
blo DrvrSendWrite
bra.s DrvrIODone
DrvrIODone
lea gExpectHdr,A1 ; Disable this socket listener
clr.w (A1)
move.l gMyDCE,A1
moveq.l #0,D0
move.l $8FC,A0 ; jIODone (D0 = result, A1 = DCE)
jmp (A0)
DrvrTrashPacket
moveq.l #0,D3
jmp 2(A4) ; ReadRest nothing
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrControl
cmp.w #21,$1A(A0)
beq.s control_kDriveIcon
cmp.w #22,$1A(A0)
beq.s control_kMediaIcon
bra.s control_unknown
control_kDriveIcon
control_kMediaIcon
lea DrvrIcon,A2
move.l A2,$1C(A0)
clr.w $10(A0) ; ioResult = noErr
bra DrvrFinish
control_unknown
move.w #-17,$10(A0) ; ioResult = controlErr
bra DrvrFinish
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrStatus
cmp.w #6,$1A(A0)
beq.s status_fmtLstCode
cmp.w #8,$1A(A0)
beq.s status_drvStsCode
bra.s status_unknown
status_fmtLstCode ; tell them about our size
move.l dqDrvSz,D0
swap D0
lsl.l #5,D0 ; convert from blocks to bytes
lsl.l #4,D0
move.w #1,$1C(A0)
move.l $1C+2(A0),A2
move.l D0,0(A2)
move.l #$40000000,4(A2)
move.w #0,$10(A0) ; ioResult = noErr
bra DrvrFinish
status_drvStsCode ; tell them about some of our flags
move.w #0,$1C(A0) ; csParam[0..1] = track no (0)
move.l dqFlags,$1C+2(A0) ; csParam[2..5] = same flags as dqe
move.w #0,$10(A0) ; ioResult = noErr
bra DrvrFinish
status_unknown
move.w #-18,$10(A0) ; ioResult
bra DrvrFinish
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrClose
move.w #0,$10(A0) ; ioResult
rts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrFinish
move.w 6(A0),D1 ; iopb.ioTrap
btst #9,D1 ; noQueueBit
bne.s DrvrNoIoDone
move.l $8FC,-(SP) ; jIODone
DrvrNoIoDone
rts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
DrvrIcon
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %11111111111111111111111111111111
dc.l %10000000000000000000000000000001
dc.l %10000000000000001000000000000001
dc.l %10010010010010011001001001001001
dc.l %10010010010010000001001001001001
dc.l %10010010010010000001001001001001
dc.l %10010010010010010001001001001001
dc.l %10010010010010001001001001001001
dc.l %10010010010010010001001001001001
dc.l %10010010010010001001001001001001
dc.l %10010010010010010001001001001001
dc.l %10000000000000000000000000000001
dc.l %11111111111111111111111111111111
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %00000000000000000000000000000000
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.l %11111111111111111111111111111111
dc.b 22, "AppleTalk NetBoot Disk", 0
gUserRec ; append user record here later on, no need to waste space on zeros
DrvrEnd
CodeEnd
align 12 ; work around unfortunate .ATBOOT bug