TRS-80 DOS - LDOS 5.3.1 for the Model I - SYS10/SYS Disassembled

Page Customization

Summary:

LDOS 5.3.1 SYS10/SYS Disassembly - File Deletion and Module Lookup (Model I)

SYS10/SYS is the LDOS 5.3.1 overlay that deletes files. It services the @KILL vector at 442CH, which issues RST 28H with request code 9CH, and it is the exact counterpart of SYS8/SYS: where SYS8/SYS claims granules and builds extent entries as a file grows, SYS10/SYS gives every one of them back.

The overlay also carries a second, unrelated service. Request code 0BCH masks to sub-function 30H and selects the resident module and driver control-block lookup at 4F28H, which walks the chain of modules installed in high memory looking for one whose name matches the string the caller points at. That is the service SYS6/SYS LINK and ROUTE and SYS7/SYS SYSTEM, FORMS and SETCOM all use to locate the control block they are about to edit, and it is the source of the "not resident" and "not installed" messages those commands produce.

The file is small: 371 bytes spanning 4E00H through 4F72H, plus a 2-byte load block deposited at 4BC9H in the resident padding area, with a transfer address of 4E00H. It should remain on working SYSTEM diskettes, since without it no file can be removed.

Deletion begins with a permission check. LDOS numbers access levels from 0 for full access upwards, and only levels 0 and 1, full access and remove access, permit a file to be deleted; anything higher is refused with error 37. The overlay then reads the drive's Granule Allocation Table into its buffer at 5000H and walks the file's directory record. Each of the four 2-byte extent entries at DIR+16H through DIR+1DH names a cylinder and, in a packed byte, a starting granule and a granule count; for every one of them the routine at 4E8BH clears the corresponding allocation bits, rolling forward onto the next cylinder whenever a granule number reaches the number of granules a cylinder holds.

With the extents released, bit 4 of the record's attribute byte is cleared to mark the directory slot free, the record is rewritten, and the file's byte in the Hash Index Table is zeroed so the name no longer hashes to anything. If the record ended with the FEH marker at DIR+1EH the file continues in a secondary extended directory record, and the whole sequence repeats for it. Only when the chain is exhausted is the Granule Allocation Table written back, at which point the freed space genuinely becomes available. Finally the caller's 32-byte File Control Block is zeroed so it cannot go on referring to a file that no longer exists.

Deleting a logical device takes none of this. A device owns no granules and no directory record, so its control block is simply rebuilt as the four-byte specification *XX followed by an 03H terminator, in the same shape SYS3/SYS produces when it closes a device.

Two pieces of the overlay are worth noting for their construction. The Granule Allocation Table handler at 4ED4H has two entry points one byte apart which select reading or writing through a single shared body: 4ED4H is OR 0AFH, and entering at 4ED5H executes that instruction's second byte alone as XOR A, setting the Z FLAG that the test at 4EE2H reads. And the helper at 4EC7H builds a RES n,B instruction for a granule number and plants it before executing it, mirroring exactly the SET n,A builder SYS8/SYS uses when it allocates.

Memory Map

Address RangePurpose
4BC9H-4BCAH
2 bytes
Load block deposited into the resident padding area below the RST 28H dispatcher. SYS10/SYS writes the bytes 73H and 01H there.
4E00H-4E09H
10 bytes
Overlay entry and request dispatch across the two services, sub-function 10H for @KILL and 30H for the module lookup.
4E0AH-4E71H
104 bytes
Disk file deletion: access check, extent release, directory and Hash Index Table maintenance, FXDE chain following, File Control Block clear.
4E72H-4E8AH
25 bytes
Logical device close-out, rebuilding the control block as a re-openable device specification.
4E8BH-4EC6H
60 bytes
Release of one extent's granules into the Granule Allocation Table image, with cylinder roll-over.
4EC7H-4ED3H
13 bytes
Self-modifying builder for the granule allocation bit reset.
4ED4H-4EF7H
36 bytes
Granule Allocation Table read and write, with two entry points sharing one body.
4EF8H-4F0EH
23 bytes
Hash Index Table read, with two entry points choosing the destination buffer.
4F0FH-4F27H
25 bytes
Hash Index Table write and verify.
4F28H-4F72H
75 bytes
Resident module and driver control-block lookup, the service behind request code 0BCH.
5000H-50FFH
256 bytes
Granule Allocation Table buffer. Scratch space above the overlay image, not part of the loaded file.

Variables and Self-Modified Operands

Address RangePurpose
4E5BH-4E5CH
2 bytes
Operand of the LD DE,nnnn at 4E5AH. Written at 4E34H with each extent entry as it is read, so that after the record has been processed the terminator can be re-examined to decide whether an FXDE link must be followed.
4EBCH
1 byte
Operand of the CP nn at 4EBBH. Written at 4EA4H with the number of granules on one cylinder, which tells the release loop when to roll over to the next cylinder.
4ED2H
1 byte
Second opcode byte of the RES 0,B at 4ED1H. Written at 4ECEH so the instruction clears the wanted granule's bit in a cylinder allocation byte.
5000H-50FFH
256 bytes
Granule Allocation Table sector image. One byte per cylinder, each set bit marking an allocated granule. SYS8/SYS uses 5100H for the same purpose because its code reaches further up.
4200H-42FFH
256 bytes
Resident shared sector buffer SBUFF$, used here to hold the Hash Index Table image while the file's hash byte is zeroed.

Major Routines

AddressEntryExitName and Purpose
4E00HRegister A = request code, Register Pair DE = File Control Block or nameReturns to the RST 28H dispatcherOverlay Entry
Masks the request code with 70H and dispatches sub-function 10H to @KILL and 30H to the module lookup.
4E0AHRegister Pair DE = File Control Block of an open file or deviceZ set on success, or NZ with an error code in Register A@KILL
Deletes a disk file completely, or closes out a logical device.
4E72HRegister Pair DE = File Control Block of an open deviceZ set, block rebuilt as *XXDevice Close-Out
Rebuilds the control block as a re-openable device specification.
4E8BHRegister Pair DE = extent word, Register C = drive number, Granule Allocation Table image at 5000HRegisters preserved, allocation bits clearedRelease One Extent
Clears the allocation bit of every granule the extent describes, rolling onto the next cylinder as needed.
4EC7HRegister A = granule number 0-7, Register B = cylinder allocation byteRegister B with that granule's bit clearedGranule Bit Reset Builder
Constructs and executes a RES n,B instruction for the given granule number.
4ED4HRegister C = drive numberZ set on success, or NZ with error 20Read Granule Allocation Table
Reads sector 0 of the directory cylinder into 5000H.
4ED5HRegister C = drive numberZ set on success, or NZ with error 21Write Granule Allocation Table
Alternate entry one byte into the instruction at 4ED4H; writes 5000H back to sector 0 and verifies it.
4EFDHRegister C = drive numberZ set on success, or NZ with error 22Read Hash Index Table
Reads sector 1 of the directory cylinder into SBUFF$ at 4200H. The entry at 4EF8H reads it into 5000H instead.
4F0FHRegister C = drive numberZ set on success, or NZ with error 23Write Hash Index Table
Writes 4200H back to sector 1 with a deleted-data address mark and verifies it.
4F28HRegister Pair DE = module or device nameZ set with Register Pair DE = control block address, or NZ with error 8Module Lookup
Walks the chain of resident modules above HIGH$ and returns the matching module's control block.

Error Codes Returned

CodeRaised AtMeaning
08H (8)4F6FHDevice not available. No resident module of the requested name is installed in the high-memory chain.
14H (20)4EE7HGAT read error. Sector 0 of the directory cylinder could not be read.
15H (21)4EF3HGAT write error. The amended Granule Allocation Table could not be written or failed to verify.
16H (22)4F0CHHIT read error. Sector 1 of the directory cylinder could not be read.
17H (23)4F23HHIT write error. The amended Hash Index Table could not be written or failed to verify.
25H (37)4E1BHIllegal access attempted to a protected file. The file was opened at an access level above 1, which does not permit removal.

Cross-References

SYS10/SYS is reached through the resident supervisor. The @KILL vector at 442CH in SYS0/SYS loads request code 9CH and issues RST 28H, and the dispatcher at 4BCDH brings this overlay in at 4E00H. The overlay calls back into the resident core for all disk work: the File Control Block prologue at 49F1H and its second entry at 49F3H, DIRRD at 4B10H and DIRWR at 4B1FH for directory records, DIRCYL at 4B65H and RDSSEC at 4B45H to reach the directory cylinder, WRPROT at 4768H and VERSEC at 4772H to write and verify the two table sectors with a deleted-data address mark, and DCTBYT at 479CH for drive geometry. It uses the resident shared sector buffer SBUFF$ at 4200H, and reads HIGH$ at 4049H and the top-of-memory word at 4403H when searching the module chain.

It is the mirror image of SYS8/SYS, which allocates the granules this overlay releases and builds the extent entries it walks; the two share the directory extent layout, the FXDE link convention and the Granule Allocation Table and Hash Index Table sector handling, and their bit helpers are opposites. SYS3/SYS performs the same kind of partial release when it trims unused granules on close, and produces the same *XX device specification at 5029H. The error codes returned here are turned into readable text by SYS4/SYS. The module lookup at 4F28H is called by SYS6/SYS LINK and ROUTE and by SYS7/SYS SYSTEM, FORMS and SETCOM.

Disassembly:

4BC9H - Overlay Load Block

The two bytes every SYS overlay deposits into the padding area of the resident core immediately below the RST 28H dispatcher. They are data, not instructions, and are never executed. SYS10/SYS writes 73H and 01H here, in the same way SYS8/SYS writes 96H and 02H and SYS4/SYS writes AFH and 03H.

4BC9
DEFB 73H, 01H 73 01
Two-byte load block written into the resident padding area at 4BC9H through 4BCAH by the overlay loader as SYS10/SYS is brought into memory. The bytes are data and are not part of the executable image, which begins at 4E00H.

4E00H - Overlay Entry and Request Dispatch

SYS10/SYS services two supervisor requests. Request code 9CH, which masks to sub-function 10H, is @KILL, the file and device deletion service. Request code 0BCH, which masks to 30H, is the resident module lookup. Any other value returns without action.

4E00
AND 70H E6 70
Register A holds the RST 28H request code that the resident supervisor dispatcher at 4BCDH passed to this overlay. Mask off everything except bits 6-4 to isolate the sub-function selector, which gives 10H for request code 9CH and 30H for request code 0BCH.
4E02
CP 30H FE 30
Compare the isolated sub-function in Register A against 30H, the selector produced by request code 0BCH. If Register A equals 30H, the Z FLAG is set; otherwise the NZ FLAG is set.
4E04
If the Z FLAG has been set the request is the resident module lookup, so JUMP to 4F28H to search the high-memory module chain for the name Register Pair DE points at.
4E07
CP 10H FE 10
Compare the isolated sub-function in Register A against 10H, the selector produced by request code 9CH, which is the @KILL file-deletion service. If Register A equals 10H, the Z FLAG is set.
4E09
RET NZ C0
If the NZ FLAG has been set the request is neither of the two this overlay services; return to the RST 28H dispatcher at 4BCDH having taken no action.

4E0AH - Delete a Disk File

The deletion path checks that the file was opened with an access level that permits removal, then walks the directory extent chain releasing every granule the file owns back into the Granule Allocation Table, marks each directory record free and de-hashes it in the Hash Index Table, follows any FXDE link to a secondary extended record, writes both tables back, and finally clears the caller's File Control Block.

4E0A
LD A,(DE) 1A
Register Pair DE addresses the caller's File Control Block. Fetch its first byte, FCB+00H, which is the type and attribute byte: 80H marks an open disk file and 10H marks an open logical device.
4E0B
AND 0E0H E6 E0
Keep only bits 7-5 of the type byte in Register A. An open disk file carries 80H and leaves a non-zero result; a logical device carries 10H and leaves zero.
4E0D
If the Z FLAG has been set the control block describes a logical device rather than a disk file, so JUMP to 4E72H to rebuild it as a device specification instead of deleting anything.
4E0F
GOSUB to the resident File Control Block prologue at 49F3H, the second entry point of the prologue at 49F1H, which validates the block addressed by Register Pair DE and leaves Register Pair IX addressing it for the indexed accesses that follow.
4E12
LD A,(IX+01H) DD 7E 01
Fetch FCB+01H, the flags byte of the File Control Block addressed by Register Pair IX, into Register A. Its bits 2-0 hold the access level the file was opened with.
4E15
AND 07H E6 07
Keep only bits 2-0 of Register A, the file's access-protection level. LDOS numbers these from 0 for full access upwards, so a smaller number grants more privilege.
4E17
CP 02H FE 02
Compare the access level in Register A against 02H. Deleting a file requires level 0 (full access) or level 1 (remove access). If Register A is below 02H the CARRY FLAG is set.
4E19
If the CARRY FLAG has been set the file was opened with full or remove access and may be deleted, so JUMP to 4E1FH to begin releasing its space.
4E1B
LD A,25H 3E 25
The access level is 2 or higher, which does not permit deletion. Load Register A with 25H, that is decimal 37, the LDOS error code for an illegal access attempted to a protected file.
4E1D
OR A B7
OR Register A with itself. This leaves the value 25H unchanged and sets the NZ FLAG so the caller treats the return as an error.
4E1E
RET C9
Return to the RST 28H dispatcher with error 37 in Register A, having changed nothing on the disk.
4E1F
LD C,(IX+06H) DD 4E 06
Load Register C with FCB+06H, the number of the drive the file resides on, taken from the File Control Block addressed by Register Pair IX.
4E22
LD B,(IX+07H) DD 46 07
Load Register B with FCB+07H, the directory record number of the file's primary directory entry, taken from the File Control Block addressed by Register Pair IX.
4E25
GOSUB to 4ED4H to read the Granule Allocation Table of drive Register C into this overlay's buffer at 5000H. That table holds one byte per cylinder with a set bit for every granule in use, and the bits belonging to this file are about to be cleared in it.
4E28
RET NZ C0
If the NZ FLAG has been set the Granule Allocation Table could not be read; return to the caller with error 20 in Register A.
4E29
GOSUB to the resident DIRRD routine at 4B10H to read directory record number Register B from drive Register C. On return Register Pair HL points at that 32-byte directory record in the resident sector buffer. This is also the re-entry point when a secondary extended directory record has to be processed.
4E2C
RET NZ C0
If the NZ FLAG has been set the directory record could not be read; return that error code in Register A to the caller.
4E2D
LD A,16H 3E 16
Load Register A with 16H, the offset of the first extent entry within a 32-byte LDOS directory record.
4E2F
ADD A,L 85
Add the low byte of the directory record's address, held in Register L, to the offset in Register A.
4E30
LD L,A 6F
Move the sum back into Register L. Register Pair HL now addresses DIR+16H, the first of the four 2-byte extent entries in this record. A directory record never straddles a page boundary, so only the low byte needs adjusting.
4E31
LD E,(HL) 5E
Loop Start
Fetch the cylinder byte of the current extent entry, addressed by Register Pair HL, into Register E. A value below FEH is a real cylinder number, FEH marks a link to a secondary extended directory record, and FFH ends the chain.
4E32
INC L 2C
Advance Register L to the extent's second byte.
4E33
LD D,(HL) 56
Fetch the packed byte into Register D. Bits 7-5 hold the granule on the cylinder at which the extent starts and bits 4-0 hold the number of granules in the extent minus one. For an FEH link entry this byte is instead the number of the linked directory record.
4E34
LD (4E5BH),DE ED 53 5B 4E
Self-Modifying Code
Store the extent word now in Register Pair DE into 4E5BH, the operand of the LD DE,nnnn instruction at 4E5AH. Once the whole record has been dealt with, that instruction reloads this word so the code at 4E5DH can tell whether the record ended with an FXDE link and, if it did, which record it points at.
4E38
LD A,E 7B
Copy the extent's cylinder byte from Register E into Register A for testing.
4E39
CP 0FEH FE FE
Compare it against 0FEH. If Register A is below 0FEH the CARRY FLAG is set, marking a real extent; if it is 0FEH or 0FFH the NO CARRY FLAG is set.
4E3B
If the NO CARRY FLAG has been set this entry is a link or the end of the chain rather than a data extent, so JUMP to 4E43H to finish with this directory record.
4E3D
INC L 2C
Advance Register L to the cylinder byte of the next extent entry, ready for the next pass of the loop.
4E3E
GOSUB to 4E8BH to clear, in the Granule Allocation Table image at 5000H, the allocation bit of every granule this extent describes, which is what actually returns the space to the free pool.
4E41
LOOP BACK to 4E31H to examine the next extent entry of this directory record.
Loop End
4E43
LD A,L 7D
Every extent in this record has been released. Copy the low byte of the current position within the record, held in Register L, into Register A.
4E44
AND 0E0H E6 E0
Keep only bits 7-5 of Register A. Directory records are 32 bytes and start on a 32-byte boundary, so this rounds the pointer back down to DIR+00H of the record being processed.
4E46
LD L,A 6F
Move the rounded value back into Register L, so Register Pair HL addresses DIR+00H, the record's attribute byte.
4E47
RES 4,(HL) CB A6
Clear bit 4 of the attribute byte at DIR+00H, addressed by Register Pair HL. That bit marks a directory record as in use, so clearing it declares the record free.
4E49
GOSUB to the resident DIRWR routine at 4B1FH to write the amended directory record Register B back to drive Register C, committing the record's release to the disk.
4E4C
RET NZ C0
If the NZ FLAG has been set the directory record could not be written; return that error code in Register A to the caller.
4E4D
GOSUB to 4EFDH to read the Hash Index Table of drive Register C into the resident shared sector buffer SBUFF$ at 4200H. That table holds one hash byte per directory record, and a zero byte marks a record as free.
4E50
RET NZ C0
If the NZ FLAG has been set the Hash Index Table could not be read; return to the caller with error 22 in Register A.
4E51
LD H,42H 26 42
Load Register H with 42H, the high byte of the resident shared sector buffer SBUFF$ at 4200H which now holds the Hash Index Table image.
4E53
LD L,B 68
Load Register L with the directory record number in Register B. Register Pair HL now addresses that record's own byte within the Hash Index Table image.
4E54
LD (HL),00H 36 00
Store zero into that Hash Index Table byte, addressed by Register Pair HL, which de-hashes the file name and marks the directory record as available for re-use. The table is ordered by column rather than by row: byte index n of the Hash Index Table is slot 0 of directory sector 2 plus n for the first thirty-two records, so the index is the slot within a sector times thirty-two plus the sector number less two. FORMAT/CMD proves it by writing the BOOT/SYS record to sector 2 slot 0 and the DIR/SYS record to sector 3 slot 0 while setting Hash Index Table bytes 0 and 1, and by having both of its boot sectors read sector 4 slot 0 to find the resident system loader.
4E56
GOSUB to 4F0FH to write the amended Hash Index Table image at 4200H back to sector 1 of the directory cylinder.
4E59
RET NZ C0
If the NZ FLAG has been set the Hash Index Table could not be written; return to the caller with error 23 in Register A.
4E5A
LD DE,0000H 11 00 00
Self-Modifying Code
The 0000H operand at 4E5BH was overwritten at 4E34H with the entry that ended this directory record. Register Pair DE therefore comes back holding that terminator: Register E is FEH for a link or FFH for the end of the chain, and Register D is the linked directory record number when Register E is FEH.
4E5D
LD B,D 42
Copy Register D into Register B. If the record ended with an FXDE link this is the number of the secondary extended directory record that continues the file.
4E5E
LD A,E 7B
Copy the terminator marker from Register E into Register A for testing.
4E5F
CP 0FEH FE FE
Compare it against 0FEH, the marker that says a secondary extended directory record follows. If Register A equals 0FEH, the Z FLAG is set.
4E61
If the Z FLAG has been set the file continues in a secondary extended directory record, so LOOP BACK to 4E29H to read record Register B and release its extents in the same way.
4E63
The whole chain has been released. GOSUB to 4ED5H, the write entry of the Granule Allocation Table handler, to write the amended table image at 5000H back to sector 0 of the directory cylinder, which is the point at which the freed granules actually become available again.
4E66
RET NZ C0
If the NZ FLAG has been set the Granule Allocation Table could not be written; return to the caller with error 21 in Register A.
4E67
PUSH IX DD E5
Push Register Pair IX, which addresses the caller's File Control Block, onto the stack.
4E69
POP HL E1
Pop it into Register Pair HL, so Register Pair HL now addresses the first byte of the caller's File Control Block.
4E6A
LD B,20H 06 20
Load Register B with 20H, that is thirty-two, the size of a File Control Block in bytes.
4E6C
XOR A AF
Set Register A to ZERO and clear all flags, giving the value that will be written through the block and leaving the Z FLAG set for the successful return.
4E6D
LD (HL),A 77
Loop Start
Store zero into the byte of the File Control Block addressed by Register Pair HL.
4E6E
INC HL 23
Advance Register Pair HL to the next byte of the File Control Block.
4E6F
DECrement Register B and loop back to 4E6DH if it has not reached zero, clearing all thirty-two bytes so the caller cannot go on using a block that refers to a file which no longer exists.
Loop End
4E71
RET C9
Return to the RST 28H dispatcher with the Z FLAG set, the file deleted, its granules returned to the Granule Allocation Table, its directory records freed and de-hashed, and the caller's File Control Block cleared.

4E72H - Close Out a Logical Device

A logical device owns no granules and no directory record, so there is nothing to release. The control block is simply rebuilt as the four-byte device specification *XX followed by the 03H terminator, exactly as SYS3/SYS does at 5029H when it closes a device, leaving a block that can be handed straight back to an open request.

4E72
GOSUB to the resident File Control Block prologue at 49F1H, which validates the control block addressed by Register Pair DE and leaves Register Pair IX addressing it.
4E75
LD C,(IX+06H) DD 4E 06
Load Register C with FCB+06H, which for an open logical device holds the first character of the two-character device name.
4E78
LD B,(IX+07H) DD 46 07
Load Register B with FCB+07H, which for an open logical device holds the second character of the two-character device name.
4E7B
LD (IX+00H),2AH DD 36 00 2A
Store 2AH, the ASCII code for *, into FCB+00H of the block addressed by Register Pair IX. That is the type byte of a closed device specification, so the block once again reads as the text of a device name.
4E7F
LD (IX+01H),C DD 71 01
Store the first name character from Register C into FCB+01H, the second byte of the rebuilt device specification.
4E82
LD (IX+02H),B DD 70 02
Store the second name character from Register B into FCB+02H, the third byte of the rebuilt device specification.
4E85
LD (IX+03H),03H DD 36 03 03
Store 03H into FCB+03H, the terminator that ends an LDOS string, completing the four-byte specification *XX which can be handed straight back to an open request.
4E89
XOR A AF
Set Register A to ZERO and clear all flags, setting the Z FLAG to report success. Nothing on the disk is touched, because a logical device owns no granules and no directory record.
4E8A
RET C9
Return to the RST 28H dispatcher with the control block rebuilt as a re-openable device specification.

4E8BH - Release One Extent Into the Granule Allocation Table

Given an extent word in Register Pair DE, this routine clears the allocation bit of every granule the extent describes. It reads the drive geometry to find how many granules a cylinder holds, doubling it for a double-sided drive, then walks the extent granule by granule, rolling over to the next cylinder's allocation byte whenever the granule number reaches the per-cylinder figure.

4E8B
PUSH HL E5
Save Register Pair HL, which addresses the current position in the directory record, onto the stack.
4E8C
PUSH BC C5
Save Register Pair BC, which holds the directory record number and the drive number, onto the stack.
4E8D
LD A,08H 3E 08
Load Register A with 08H to select byte 8 of the Drive Code Table entry, which packs the number of granules per track into bits 7-5 and the number of sectors per granule into bits 4-0.
4E8F
GOSUB to the resident DCTBYT routine at 479CH, which returns in Register A the Drive Code Table geometry byte selected by Register A for the drive in Register C.
4E92
RLCA 07
Rotate Register A left circularly; three of these instructions together bring the granules-per-track field down from bits 7-5 into bits 2-0.
4E93
RLCA 07
Rotate Register A left circularly a second time, continuing to shift the granules-per-track field down.
4E94
RLCA 07
Rotate Register A left circularly a third time, so the granules-per-track field now occupies bits 2-0 of Register A.
4E95
AND 07H E6 07
Keep only bits 2-0 of Register A, discarding the sectors-per-granule field that shared the byte.
4E97
INC A 3C
Increment Register A so it holds the true number of granules on one track rather than that count minus one.
4E98
LD L,A 6F
Park the granules-per-track figure in Register L while Register A is used to read a second geometry byte.
4E99
LD A,04H 3E 04
Load Register A with 04H to select byte 4 of the Drive Code Table entry, whose bit 5 records whether the drive is double sided.
4E9B
GOSUB to the resident DCTBYT routine at 479CH, which returns in Register A the Drive Code Table geometry byte selected by Register A for the drive in Register C, here DCT+04H.
4E9E
BIT 5,A CB 6F
Test bit 5 of Register A, the double-sided flag of the Drive Code Table entry. The Z FLAG is set when the drive is single sided.
4EA0
LD A,L 7D
Recover the granules-per-track figure from Register L into Register A.
4EA1
If the Z FLAG has been set the drive is single sided, so a cylinder is one track and the figure already stands; JUMP to 4EA4H.
4EA3
ADD A,A 87
The drive is double sided, so a cylinder holds two tracks. Add Register A to itself, doubling the figure to the number of granules on a whole cylinder.
4EA4
LD (4EBCH),A 32 BC 4E
Self-Modifying Code
Store the granules-per-cylinder figure held in Register A into 4EBCH, the immediate operand of the CP nn instruction at 4EBBH, so the loop below knows when a granule number has run off the end of a cylinder and must roll over to the next one.
4EA7
LD L,E 6B
Move the extent's cylinder number from Register E into Register L, which is the offset of that cylinder's byte within the Granule Allocation Table image.
4EA8
LD H,50H 26 50
Load Register H with 50H. Register Pair HL now addresses that cylinder's allocation byte inside the Granule Allocation Table buffer based at 5000H.
4EAA
LD A,D 7A
Copy the extent's packed starting-granule and count byte from Register D into Register A.
4EAB
AND 1FH E6 1F
Keep only bits 4-0 of Register A, the number of granules in this extent minus one.
4EAD
LD C,A 4F
Move that figure into Register C.
4EAE
INC C 0C
Increment Register C so it holds the true number of granules the extent covers, which is how many allocation bits have to be cleared.
4EAF
XOR D AA
Exclusive-OR Register A against Register D, the full packed byte. The granule-count field cancels itself out, leaving in Register A only bits 7-5, the granule on the cylinder at which the extent starts.
4EB0
RLCA 07
Rotate Register A left circularly; three of these instructions together bring the starting-granule field down from bits 7-5 into bits 2-0.
4EB1
RLCA 07
Rotate Register A left circularly a second time, continuing to shift the starting-granule field down.
4EB2
RLCA 07
Rotate Register A left circularly a third time, so Register A now holds the starting granule as a plain number from 0 to 7.
4EB3
PUSH AF F5
Loop Start
Save the current granule number held in Register A, since the helper called below uses Register A to build its instruction.
4EB4
LD B,(HL) 46
Fetch the current cylinder's allocation byte from the Granule Allocation Table image, addressed by Register Pair HL, into Register B. Each set bit marks a granule on that cylinder as in use.
4EB5
GOSUB to 4EC7H, which plants and executes a RES n,B instruction for the granule number in Register A, clearing that granule's bit in the allocation byte held in Register B and so returning the granule to the free pool.
4EB8
LD (HL),B 70
Store the amended allocation byte from Register B back into the Granule Allocation Table image at the address in Register Pair HL. The image is written to disk later by 4ED5H.
4EB9
POP AF F1
Restore the current granule number into Register A.
4EBA
INC A 3C
Increment Register A to move on to the next granule of the extent.
4EBB
CP 00H FE 00
Self-Modifying Code
Compare the granule number in Register A against the operand at 4EBCH, which 4EA4H set to the number of granules on one cylinder. If they are equal the extent has run off the end of this cylinder.
4EBD
If the NZ FLAG has been set the granule number is still within the current cylinder, so JUMP to 4EC1H and leave the cylinder pointer alone.
4EBF
XOR A AF
The extent continues onto the next cylinder. Set Register A to ZERO so counting restarts at granule 0.
4EC0
INC L 2C
Advance Register L to the next cylinder's allocation byte within the Granule Allocation Table image.
4EC1
DEC C 0D
Decrement Register C, the count of granules in this extent still to be released.
4EC2
If the NZ FLAG has been set there are still granules to release, so LOOP BACK to 4EB3H to clear the next one.
Loop End
4EC4
POP BC C1
Restore Register Pair BC, giving back the directory record number and the drive number.
4EC5
POP HL E1
Restore Register Pair HL, giving back the caller's position within the directory record.
4EC6
RET C9
Return to 4E3EH with every granule of this extent marked free in the Granule Allocation Table image.

4EC7H - Granule Allocation Bit Reset Builder

A granule number from 0 to 7 cannot be used directly by a Z80 bit instruction, so this helper constructs the second opcode byte of a RES n,B instruction for that granule and plants it in the instruction at 4ED1H before executing it against a cylinder's allocation byte. It is the exact counterpart of the bit-setting helper SYS8/SYS uses when it allocates a granule.

4EC7
AND 07H E6 07
Keep only bits 2-0 of Register A, which hold the number, from 0 to 7, of the granule whose allocation bit is to be cleared.
4EC9
RLCA 07
Rotate Register A left circularly; three of these instructions together move the granule number up into bits 5-3, which is where the bit selector sits in a RES n,r opcode.
4ECA
RLCA 07
Rotate Register A left circularly a second time, continuing to move the granule number into bits 5-3.
4ECB
RLCA 07
Rotate Register A left circularly a third time, so the granule number now occupies bits 5-3 of Register A.
4ECC
OR 80H F6 80
OR Register A with 80H, the base opcode byte for RES 0,B. Register A now holds the second opcode byte of a RES n,B instruction for the wanted granule number.
4ECE
LD (4ED2H),A 32 D2 4E
Self-Modifying Code
Store the constructed opcode byte held in Register A into 4ED2H, which is the second byte of the RES 0,B instruction at 4ED1H, turning it into a reset of the wanted granule's bit.
4ED1
RES 0,B CB 80
Self-Modifying Code
Clear the bit of Register B selected by the operand planted at 4ED2H. Register B holds a cylinder's Granule Allocation Table byte, so this marks that granule as free. This is the exact counterpart of the bit-setting helper SYS8/SYS uses at 5027H when it allocates a granule.
4ED3
RET C9
Return to 4EB5H with the amended allocation byte in Register B.

4ED4H - Read or Write the Granule Allocation Table

One body serves both directions, selected by which of two entry addresses the caller uses. Sector 0 of the directory cylinder holds the Granule Allocation Table, one byte per cylinder with a set bit for every granule in use, and it is held in this overlay's buffer at 5000H. Reads return error 20 on failure; writes go out with a deleted-data address mark, are verified, and return error 21.

Two Entry Points In One Instruction
This routine is entered at 4ED4H to read the Granule Allocation Table and at 4ED5H to write it. 4ED4H is the two-byte instruction OR 0AFH, whose second byte is 0AFH. Entering at 4ED5H therefore executes that byte on its own as XOR A, which sets the Z FLAG, while entering at 4ED4H performs the OR and leaves the NZ FLAG set. The test at 4EE2H reads that flag to choose between reading and writing, so the two entry addresses select the two directions through one shared body. The file-deletion path calls 4ED4H at 4E25H to read the table and 4ED5H at 4E63H to write it back.

4ED4
OR 0AFH F6 AF
OR Register A with 0AFH. The result is never zero, so the NZ FLAG is set and the test at 4EE2H takes the read path. Entering the routine here therefore means read the Granule Allocation Table.
4ED6
PUSH DE D5
Save Register Pair DE onto the stack; the resident sector routines called below use it.
4ED7
PUSH HL E5
Save Register Pair HL onto the stack, preserving the caller's position within the directory record.
4ED8
PUSH AF F5
Save the flags onto the stack. The Z FLAG set here is what decides, at 4EE2H, whether this call reads or writes the table.
4ED9
GOSUB to the resident DIRCYL routine at 4B65H, which selects drive Register C and positions to the directory cylinder, where the Granule Allocation Table and Hash Index Table live.
4EDC
LD E,00H 1E 00
Load Register E with 00H, the sector number of the Granule Allocation Table on the directory cylinder.
4EDE
LD HL,5000H 21 00 50
Load Register Pair HL with 5000H, this overlay's Granule Allocation Table buffer, which sits above the overlay code and holds one allocation byte per cylinder.
4EE1
POP AF F1
Restore the flags saved at 4ED8H, bringing back the Z FLAG that selects reading or writing.
4EE2
If the Z FLAG has been set the caller entered at 4ED5H and wants the table written, so JUMP to 4EEBH. Otherwise the caller entered at 4ED4H and the table is read.
4EE4
GOSUB to the resident RDSSEC routine at 4B45H to read sector Register E of the directory cylinder into the buffer addressed by Register Pair HL.
4EE7
LD A,14H 3E 14
Load Register A with 14H, that is decimal 20, the LDOS error code for a Granule Allocation Table read error, which is returned only when the read left the NZ FLAG set.
4EE9
JUMP to 4EF5H, the common exit, to restore the saved registers and return.
4EEB
GOSUB to the resident WRPROT routine at 4768H to write the buffer addressed by Register Pair HL to sector Register E of the directory cylinder. That routine issues the WD1771 Write Sector command with a deleted-data address mark, which is how every directory-cylinder sector is recorded.
4EEE
If the Z FLAG has been set the write itself succeeded, so GOSUB to the resident VERSEC routine at 4772H to verify the sector by reading its address mark and checking the cyclic redundancy check.
4EF1
CP 06H FE 06
Compare the verify status in Register A against 06H. If they are equal the Z FLAG is set and the sector verified correctly.
4EF3
LD A,15H 3E 15
Load Register A with 15H, that is decimal 21, the LDOS error code for a Granule Allocation Table write error, which is returned only when the flags set above report a failure.
4EF5
POP HL E1
Restore Register Pair HL from the stack, giving back the caller's position within the directory record.
4EF6
POP DE D1
Restore Register Pair DE from the stack.
4EF7
RET C9
Return to the caller, with the Z FLAG set when the Granule Allocation Table operation succeeded and the NZ FLAG set with error 20 or 21 in Register A when it did not.

4EF8H - Read the Hash Index Table

Sector 1 of the directory cylinder holds the Hash Index Table, one hash byte per directory record with zero marking a free record. Two entry points choose the destination buffer: 4EF8H reads it into the overlay buffer at 5000H and 4EFDH into the resident shared buffer SBUFF$ at 4200H, which is the one the deletion path uses. A failure returns error 22.

4EF8
LD HL,5000H 21 00 50
Load Register Pair HL with 5000H, this overlay's Granule Allocation Table buffer, so that the shared code below reads the Hash Index Table into that buffer instead of the resident one. No code in this overlay enters here; 4EFDH is the entry the file-deletion path uses.
4EFB
JUMP to 4F00H to join the common Hash Index Table read code with the buffer address already in Register Pair HL.
4EFD
LD HL,4200H 21 00 42
Load Register Pair HL with 4200H, the resident shared sector buffer SBUFF$, which is where the Hash Index Table is placed for the deletion path to amend.
4F00
PUSH BC C5
Save Register Pair BC onto the stack; it holds the directory record number and the drive number.
4F01
PUSH DE D5
Save Register Pair DE onto the stack; the resident sector routines called below use it.
4F02
GOSUB to the resident DIRCYL routine at 4B65H, which selects drive Register C and positions to the directory cylinder.
4F05
LD E,01H 1E 01
Load Register E with 01H, the sector number of the Hash Index Table on the directory cylinder.
4F07
GOSUB to the resident RDSSEC routine at 4B45H to read sector Register E of the directory cylinder into the buffer addressed by Register Pair HL.
4F0A
POP DE D1
Restore Register Pair DE from the stack.
4F0B
POP BC C1
Restore Register Pair BC from the stack, giving back the directory record number and the drive number.
4F0C
LD A,16H 3E 16
Load Register A with 16H, that is decimal 22, the LDOS error code for a Hash Index Table read error, which is returned only when the read left the NZ FLAG set.
4F0E
RET C9
Return to 4E4DH, with the Z FLAG set when the Hash Index Table was read and the NZ FLAG set with error 22 in Register A when it was not.

4F0FH - Write the Hash Index Table

The amended Hash Index Table image at 4200H is written back to sector 1 of the directory cylinder with a deleted-data address mark and then verified, committing the release of the file's directory records. A failure returns error 23.

4F0F
PUSH BC C5
Save Register Pair BC onto the stack; it holds the directory record number and the drive number.
4F10
PUSH DE D5
Save Register Pair DE onto the stack; the resident sector routines called below use it.
4F11
GOSUB to the resident DIRCYL routine at 4B65H, which selects drive Register C and positions to the directory cylinder.
4F14
LD E,01H 1E 01
Load Register E with 01H, the sector number of the Hash Index Table on the directory cylinder.
4F16
LD HL,4200H 21 00 42
Load Register Pair HL with 4200H, the resident shared sector buffer SBUFF$, which holds the Hash Index Table image whose byte for this file was zeroed at 4E54H.
4F19
GOSUB to the resident WRPROT routine at 4768H to write the buffer addressed by Register Pair HL to sector Register E of the directory cylinder using a deleted-data address mark.
4F1C
If the NZ FLAG has been set the write failed outright, so JUMP to 4F23H to report a Hash Index Table write error.
4F1E
GOSUB to the resident VERSEC routine at 4772H to verify the sector just written by reading its address mark and checking the cyclic redundancy check.
4F21
CP 06H FE 06
Compare the verify status in Register A against 06H. If they are equal the Z FLAG is set and the sector verified correctly.
4F23
LD A,17H 3E 17
Load Register A with 17H, that is decimal 23, the LDOS error code for a Hash Index Table write error, which is returned only when the flags set above report a failure.
4F25
POP DE D1
Restore Register Pair DE from the stack.
4F26
POP BC C1
Restore Register Pair BC from the stack, giving back the directory record number and the drive number.
4F27
RET C9
Return to 4E56H, with the Z FLAG set when the Hash Index Table was written and verified and the NZ FLAG set with error 23 in Register A when it was not.

4F28H - Resident Module and Driver Control Block Lookup

This is the service behind request code 0BCH. Register Pair DE addresses a module or device name; the routine measures it, then walks the chain of resident modules installed above HIGH$ comparing each header's name against it. A match returns the address of that module's control block in Register Pair DE with the Z FLAG set, which is what SYS6/SYS LINK and ROUTE and SYS7/SYS SYSTEM, FORMS and SETCOM use to find the block they are about to edit. No match returns error 8.

Resident Module Chain
Each resident driver, filter or module installed above HIGH$ begins with a header whose first byte is 18H, a relative jump over the header itself. Header offset 2 and 3 hold a link word, offset 4 holds the length of the module name in its low nibble, offset 5 begins the name, and the bytes after the name are the module control block that this service returns. The link word is stored one below the header it points at, so incrementing it yields the next header and the FFFFH end marker becomes zero.

4F28
PUSH BC C5
Save Register Pair BC onto the stack, since Register C is about to be used to measure the name being looked up.
4F29
LD HL,(4049H) 2A 49 40
Fetch HIGH$ from 4049H into Register Pair HL. That resident word holds the current top of user memory, which drops each time a driver, filter or module is installed above it.
4F2C
LD BC,(4403H) ED 4B 03 44
Fetch the resident top-of-memory word from 4403H into Register Pair BC. This is the highest address available when nothing has been installed.
4F30
SBC HL,BC ED 42
Subtract Register Pair BC from Register Pair HL. The CARRY flag was left clear by the comparison at 4E02H, so this is an exact subtraction, and the result is zero only when HIGH$ still equals the bare top of memory.
4F32
If the Z FLAG has been set no resident module has ever been installed, so there is no chain to search; JUMP to 4F6EH to report that the module is not present.
4F35
LD C,0FFH 0E FF
Load Register C with 0FFH so that the increment at the head of the measuring loop brings it to zero on the first pass.
4F37
PUSH DE D5
Save Register Pair DE, which addresses the module or device name supplied by the caller, so it can be recovered after the name has been measured.
4F38
INC C 0C
Loop Start
Increment Register C, the running count of characters in the name supplied by the caller.
4F39
LD A,(DE) 1A
Fetch the next character of the caller's name, addressed by Register Pair DE, into Register A.
4F3A
INC DE 13
Advance Register Pair DE to the following character of the caller's name.
4F3B
CP 21H FE 21
Compare the character in Register A against 21H, the first printable character above the space. Anything below it, such as a space, a carriage return or the 03H string terminator, ends the name. If Register A is below 21H the CARRY FLAG is set.
4F3D
If the NO CARRY FLAG has been set the character is part of the name, so LOOP BACK to 4F38H to count it and examine the next one.
Loop End
4F3F
POP DE D1
Recover the address of the caller's name into Register Pair DE. Register C now holds its length.
4F40
LD A,(HL) 7E
Loop Start
Fetch the first byte of the module header addressed by Register Pair HL into Register A. Every resident LDOS module begins with a jump over its own header.
4F41
CP 18H FE 18
Compare that first byte in Register A against 18H, the Z80 opcode for a relative jump, which is the signature every resident module header carries. If Register A equals 18H, the Z FLAG is set.
4F43
If the NZ FLAG has been set the address does not hold a module header and the chain cannot be trusted, so JUMP to 4F6EH to report that the module is not present.
4F45
PUSH HL E5
Save the address of this module header in Register Pair HL, so the chain can be followed from it if the name does not match.
4F46
INC HL 23
Advance Register Pair HL by one byte; four of these instructions together step from the module header to the byte at header offset 4.
4F47
INC HL 23
Advance Register Pair HL by a second byte toward header offset 4.
4F48
INC HL 23
Advance Register Pair HL by a third byte toward header offset 4.
4F49
INC HL 23
Advance Register Pair HL by a fourth byte, so Register Pair HL now addresses header offset 4, the byte whose low nibble gives the length of the module name.
4F4A
LD A,(HL) 7E
Fetch that byte into Register A.
4F4B
AND 0FH E6 0F
Keep only bits 3-0 of Register A, the number of characters in this module's name.
4F4D
CP C B9
Compare the module's name length in Register A against Register C, the length of the name the caller asked for. If they are equal, the Z FLAG is set.
4F4E
If the NZ FLAG has been set the names are of different lengths and cannot match, so JUMP to 4F62H to move on to the next module in the chain.
4F50
INC HL 23
The lengths agree. Advance Register Pair HL to header offset 5, the first character of the module's name.
4F51
LD B,A 47
Copy the name length from Register A into Register B to serve as the comparison loop counter.
4F52
PUSH DE D5
Save the address of the caller's name in Register Pair DE so it can be restored if this module turns out not to match.
4F53
LD A,(DE) 1A
Loop Start
Fetch the next character of the caller's name, addressed by Register Pair DE, into Register A.
4F54
CP (HL) BE
Compare it against the corresponding character of the module's name, addressed by Register Pair HL. If they are equal, the Z FLAG is set.
4F55
If the NZ FLAG has been set the names differ at this character, so JUMP to 4F61H to restore the name pointer and move on to the next module.
4F57
INC HL 23
The characters match. Advance Register Pair HL to the next character of the module's name.
4F58
INC DE 13
Advance Register Pair DE to the next character of the caller's name.
4F59
DECrement Register B and loop back to 4F53H if it has not reached zero, comparing every character of the two names.
Loop End
4F5B
EX DE,HL EB
Every character matched. Exchange Register Pair DE with Register Pair HL, so Register Pair DE addresses the byte immediately after the module's name, which is the start of that module's control block and the value the caller is asking for.
4F5C
POP HL E1
Discard the caller name pointer saved at 4F52H.
4F5D
POP HL E1
Discard the module header address saved at 4F45H.
4F5E
POP BC C1
Restore the caller's Register Pair BC, saved at 4F28H.
4F5F
XOR A AF
Set Register A to ZERO and clear all flags, setting the Z FLAG to report that the module was found.
4F60
RET C9
Return to the RST 28H dispatcher with the Z FLAG set and Register Pair DE addressing the resident module's control block.
4F61
POP DE D1
The names differ. Restore the address of the caller's name into Register Pair DE, which was saved at 4F52H.
4F62
POP HL E1
Restore the address of this module's header into Register Pair HL, which was saved at 4F45H.
4F63
INC HL 23
Advance Register Pair HL by one byte toward header offset 2, where the link to the next module is held.
4F64
INC HL 23
Advance Register Pair HL by a second byte, so Register Pair HL addresses header offset 2, the low byte of the link word.
4F65
LD A,(HL) 7E
Fetch the low byte of the link word into Register A.
4F66
INC HL 23
Advance Register Pair HL to header offset 3, the high byte of the link word.
4F67
LD H,(HL) 66
Fetch the high byte of the link word into Register H.
4F68
LD L,A 6F
Move the low byte from Register A into Register L, so Register Pair HL now holds the link word read out of this module's header.
4F69
INC HL 23
Increment Register Pair HL. The chain stores each link one below the header it refers to, so this both produces the address of the next module header and turns the FFFFH end marker into zero.
4F6A
LD A,H 7C
Copy Register H, the high byte of the incremented link, into Register A.
4F6B
OR L B5
OR Register A with Register L, the low byte of the incremented link. The Z FLAG is set only when both halves are zero, which is the end of the chain.
4F6C
If the NZ FLAG has been set another module follows, so LOOP BACK to 4F40H to test its header and name.
Loop End
4F6E
POP BC C1
The chain has been exhausted without a match, or there was no chain at all. Restore the caller's Register Pair BC, saved at 4F28H.
4F6F
LD A,08H 3E 08
Load Register A with 08H, that is decimal 8, the LDOS error code for a device that is not available, which is how a request for a module that is not resident is reported.
4F71
OR A B7
OR Register A with itself. This leaves the value 08H unchanged and sets the NZ FLAG so the caller treats the return as an error.
4F72
RET C9
Return to the RST 28H dispatcher with error 8 in Register A, reporting that no resident module of that name is installed.