Sunday, April 19, 2015

Generalised disk access routines now work

After a fair bit of refactoring and debugging, I have the new generalised disk access code working,It still has a few wrinkles (like file names longer than 14 letters get chopped off at 14 letters), and the C65GS can now boot up completely using the new code.

To give an idea of the interface now, here is the code that loads the C65 ROM into place:

ldx #<txt_c65gsrom
ldy #>txt_c65gsrom
jsr dos_setname

; Prepare pointer for load address ($0020000)
lda #$00
sta <dos_file_loadaddress+0
sta <dos_file_loadaddress+1
sta <dos_file_loadaddress+3
lda #$02
sta <dos_file_loadaddress+2

jsr dos_readfileintomemory
bcs loadedok

As you can see, for the simplest use-cases, it is pretty simple: just set the name of the file you want to load, provide the load address, and then use the dos_readfileintomemory utility function that looks after everthing else.  We are using the convention of setting the carry flag if a function completes successfully, or clear otherwise.  In the case of an error, dos_error_code contains the reason fore failure. I have tried to provide meaningful error codes for all things that can go wrong at the moment.

Drilling down a little, we can see how the dos_readfileintomemory function works.  There are few little complexities in that function that I will explain:

; file name must be already loaded into
        ; dos_requested_filename,
; with length in dos_requested_filename_length

We keep track of the number of sectors read, so that we don't get stuck forever if we hit a file with a tangled cluster chain.
; Clear number of sectors read
ldx #$00
stx dos_sectorsread
stx dos_sectorsread+1

Next, we need to find the file and get its details in the single directory entry structure (dos_dirent). A call to dos_findfirst leaves the file descriptor for the directory search open, in case you want to call dos_findnext to find more matching files. So we need to take care to close the file descriptor so that we don't run out.  This is really important, because the hypervisor supplies a grand total of only four file descriptors.  We can't trust dos_closefile to not mess with the carry flag, so we save the processor flags on the stack before closing the file descriptor, and then propagate any error up by jumping to dos_return_error_already_set, which preserves the value in dos_error_code.

jsr dos_findfirst
; close directory now that we have what we were
        ; looking for so that we don't leak file descriptors ...
jsr dos_closefile
; ... but report if we hit an error
bcc dos_return_error_already_set

jsr dos_openfile
bcc dos_return_error_already_set

Next we need to make the SD card sector buffer visible at $DE00-$DFFF.  In future, this and the following code will need to be generalised a bit to allow disks on other media, in which case we will need to instead have a pointer to the sector buffer.  But for now we have a structure that works, and has the flexibility to add such functionality later without breaking compatibility.
        ; Make sector buffer visible at $DE00-$DFFF
jsr sd_map_sectorbuffer

Now we enter the main loop where we read the data into memory.  We read the current sector, then copy the 512 bytes into place, update the pointer, and ask for the next sector, if there is one.  For now, we just use a simple copy loop using the 32-bit pointer access.  It would be quite a bit faster if we used DMA to transfer the sector buffer. It might even take a few less bytes, especially if we embed the load address pointer directly into the DMA list.

jsr dos_file_read_current_sector
bcc drfim_eof

; copy sector to memory
ldx #$00
ldz #$00
lda $de00,x
nop ; 32-bit pointer access follows
sta (<dos_file_loadaddress),z
bne drfim_rr1
inw <dos_file_loadaddress+1
lda $df00,x
nop ; 32-bit pointer access follows
sta (<dos_file_loadaddress),z
bne drfim_rr1b

jsr dos_file_advance_to_next_sector
bcc drfim_eof

Here we have a little security feature: You cannot load across a 16MB memory boundary. This is designed to prevent calls from user-space inadvertently or maliciously trying to load code that will end up loading over the top of the hypervisor.  The 16MB boundary is enforced simply by refusing to increment the upper byte of the load address when we update the memory pointer used to write the data into memory. It isn't actually sufficient yet to be completely effective in this function, because any load to the IO memory space, which includes the hypervisor, could potentially overwrite the hypervisor, but I am already thinking about how to make these calls secure, so that, (a) the machine doesn't crash easily, and (b) so that it actually has good security.  

; We only allow loading into a 16MB space
; Provided that we check the load address before starting,
; this ensures that a user-land request cannot load a huge file
; that eventually overwrites the hypervisor and results in privilege
; escalation.
inw <dos_file_loadaddress+1

; Increment number of sectors read (16 bit valie)
inc dos_sectorsread
bne drfim_sector_loop
inc dos_sectorsread+1
; see if there is another sector
bne drfim_sector_loop

This point in the code is reached if we have read 64K sectors = 64K x 512 bytes = 32MB.  Note that we supply a meaninful error code so that the caller knows what has happened.
jsr dos_closefile

; File is >65535 sectors (32MB), report error
lda #dos_errorcode_file_too_long
jmp dos_return_error

Finally, when we reach the end of file, we need to close the file to protect our precious few file descriptors, and indicate success to the caller.
jsr dos_closefile
jmp dos_return_success

You can also hopefully see in the above that there are functions for various other operations that are necessary.  So I now have all the basic building pieces to make a few useful disk access functions available to user-land, which I will look to do soon, so that hopefully in the near future you will be able to load a file of up to a few mega-bytes quickly and easily with just a few lines of assembly code.

No comments:

Post a Comment