2

I am trying to load sector number from [head = 0, cylinder(track) = 1, sector = 1] from floppy using BIOS interrupt 13h, from my FAT12 bootloader.

I use the subroutine read_sectors to read the sector and load it at es:bx.

This code works well with any sector from the first track, but it reads just 0s from any sector from the other tracks, while those sectors are actually populated. With sector 18, for example, cx is 0x0041, which is right. The problem is, the interrupt sets CF, saying that there is an error. It also sets ah (return code) to 1, and al(sectors read) to 1.

This is the complete bootloader .asm file

bits 16
org 0

start: jmp load

nop
OEM:                    DB "ptiaOS  "
bytesPerSector:     DW 512
sectorsPerCluster:  DB 1
reservedSectors:    DW 1
numberOfFATs:       DB 2
rootEntries:        DW 224
totalSectors:       DW 2880
media:              DB 0xf8
sectorsPerFAT:      DW 9
sectorsPerTrack:    DW 18
headsPerCylinder:   DW 2
hiddenSectors:      DD 0
totalSectorsBig:        DD 0
driveNumber:            DB 0
unused:         DB 0
extBootSignature:   DB 0x29
serialNumber:           DD 0xa0a1a2a3
volumeLabel:            DB "PTIAOS FLP "
fileSystem:             DB "FAT12   "

load:
  ;The bootloader is loaded at the address 0x7C00 and is 0x200 (512) bytes long
  cli
  mov ax, 0x07C0 ; setup registers to point to our segment
  mov ds, ax
  mov es, ax
  mov fs, ax
  mov gs, ax
  sti

  mov si, hello_string
  call prints

  mov si, try_string
  call prints

  mov ax, 18
  call lba_to_chs

  mov al, 2
  mov bx, 0x200
  call read_sectors

  mov si, success_string
  call prints

  mov si, 0x200
  call prints

  cli
  hlt ;halt



;--------DATA--------
hello_string db `Hi, bootloader of ptiaOS here\n\r`, 0
success_string db `Successfully loaded from floppy\n\r`, 0
try_string db `Loading more data from floppy...\n\r`, 0
;CHS position of the sector to read
sector_number db 0 ;1 is the first (they're 18 per track)
cilinder_number db 0 ;track number: 0 is the first (they're 80 per side)
head_number db 0 ;0 is the first (the're 2)

;---SOTTOPROGRAMMI---
;print a 0-terminated string pointed by ds:si
prints:
  mov ah, 0x0E ;dico all'interrupt del BIOS video di eseguire la funzione di stampa [al: carattere, bh: pagina] 
  .prints_printchar:
    lodsb ;al = *(si++)
    cmp al, 0
    je .prints_end ;if(al == 0) goto print_string_end
      int 0x10 ;chiamo l'interrupt di i/o dello schermo, con ah = 0x0E per stampare il carattere in al
      jmp .prints_printchar
  .prints_end:
  ret


 ;Read sectors from floppy at the address specified by CHS variables, and load them in es:bx
read_sectors:
  mov ah, 0x02 ;function 0x02, interrupt 0x13: read sectors
  ;al (the number of sectors to read), es:bx (destination) are set as arguments
  xor cx, cx
  mov cl, [cylinder_number]
  shl cl, 6
  or cl, [sector_number]
  mov dh, [head_number]
  mov dl, 0
  int 0x13
  jnc .sectors_read_successfully ;CF = 0 if no errors, 1 otherwise
  ;if errors occured, try to reset floppy
  .flp_reset:
    mov ah, 0 ;function 0, interrupt 0x13: reset disk
    mov dl, 0 ;disk to reset: 0=floppy
    int 0x13
    jc .flp_reset ;CF = 0 if no errors, 1 otherwise
  jmp read_sectors
  .sectors_read_successfully:
  ret

lba_to_chs:
  mov cx, ax

  mov bl, [sectorsPerTrack]
  div bl
  inc ah ;ah = lba % 18 + 1
  mov byte [sector_number], ah

  mov ax, cx
  mov bl, [sectorsPerTrack]
  div bl ;al = lba / 18
  cbw ;ax = lba / 18
  mov bl, [headsPerCylinder]
  div bl ;al = lba / 18 / 2; ah = lba / 18 % 2
  mov byte [cilinder_number], ah
  mov byte [head_number], al

  ret

times 510-($-$$) db 0
dw 0xAA55

I am running this code on qemu, from Ubuntu, and compiling it with

nasm -f bin -o ptiaos.bin ptiaboot.asm
nasm -f bin -o BSTAGE2.SYS blstage2.asm

mkdir floppy
dd status=noxfer conv=notrunc if=ptiaos.bin of=ptiaos.flp
sudo mount -o loop ptiaos.flp floppy
sudo cp BSTAGE2.SYS floppy

sleep 0.1
sudo umount floppy
rm BSTAGE2.SYS
rm ptiaos.bin
rmdir floppy
MARSHMALLOW
  • 1,315
  • 2
  • 12
  • 24
ptia
  • 141
  • 2
  • 3
  • 14
  • 1
    You'll need to calculate the proper CHS address for the geometry of the device you are reading which I don't see in your code. This is not trivial. I found [this OSdev page](http://wiki.osdev.org/ATA_in_x86_RealMode_%28BIOS%29) helpful for [my implementation](https://github.com/neonics/qure/blob/master/bootloader/sector1.s#L1679). – Kenney Nov 30 '15 at 17:46
  • Actually, I am using a subroutine based on the OSdev page to calculate the CHS address. I just omitted it from the question, as it seems to work correctly, and passed chs address directly to the read_sectors function. The address [head = 0, cilinder(track) = 1, sector = 1] is actually LBA (0-indexed) address 18. If you think I need to show the subroutine here, I'll post it. Thanks for answering so quickly. – ptia Nov 30 '15 at 17:53
  • You're welcome. I was just checking - in my experience usually the problem is in the CHS calculation, or assuming the wrong number of cylinders/heads/tracks for the drive. So you're having trouble reading LBA address 19 and onward: have you manually checked that LBA 19 gets converted to the proper CHS? If so, there's no need to post the CHS calculation. One more thing: the data gets read to `es:bx` and I don't see how `bx` is initialized. – Kenney Nov 30 '15 at 18:03
  • I populated sectors 17-18 (0-indexed) by directly modifying the .flp file, it worked for sector 17. If I load 2 sectors starting from 17, the data I put in sector 18 is loaded too. It doesn't work only if I directly load from 18 or after. – ptia Nov 30 '15 at 18:38
  • Yes, the conversion to H=0 C=1 S=1 is right. Maybe the way I put those addresses in CX is wrong? It is in the read_sectors subroutine, just before calling the interrupt. It is based on the algorithm described by http://wiki.osdev.org/ATA_in_x86_RealMode_%28BIOS%29 , section "Reading sectors from CHS address" – ptia Nov 30 '15 at 19:00
  • Ok, I modifyed the code like you told. Thanks for the suggestion. It still works with sectors up to 17. With sector 18, cx is now 0x0041, which is right. The problem is, the interrupt sets CF, saying that there is an error. It also sets ah (return code) to 1, and al(sectors read) to 1. – ptia Nov 30 '15 at 20:21
  • You might need to copy and then update the diskette parameter table pointed to by the INT 1E vector so that it contains the correct sectors per track value. – Ross Ridge Nov 30 '15 at 21:16
  • I'm not sure if this is what you meant, but I tried adding `mov byte [0x007b], 2` (0x007b should be the third element of the DPT, starting from 0x0078) before the call to read_sectors, but it's still giving me the same error. – ptia Nov 30 '15 at 21:41
  • @ptia No. The diskette parameter table exists in an unknown section of memory (maybe ROM, maybe RAM). You need to locate it though the pointer in the INT 1E vector, copy it to RAM, modify it and then point the INT 1E vector to the modified table. In this case, you want to change the sectors per track value at offset 4 to 18. You then need to use INT 13/AH=00h to let the BIOS know that its changed. – Ross Ridge Nov 30 '15 at 22:39
  • You have defined cilinder as a byte. This is okay for floppies (but not smaller hard disk images) but limits you to cylinders 0-255 (rather than 0-1023). Because you don't seem to care about the upper 2 bits, we can assume they are zero. So You should be able to move `cilinder_number` into CH, and simply move the `sector_number` into `cl`. The upper 2 bits of CL will be zero doing this, so we don't need to do anything fancy. – Michael Petch Dec 01 '15 at 00:50
  • If you want to support 0-1023 cylinders you'll need to declare `cilinder_number` as a _dw_ (16-bit word) instead of _db_ and then you'd need to set the upper 2 bits of _CL_ with bits 8 and 9 (zero based) of `cilinder_number` . Your conversion routine would have to change a bit to support 10 bit cylinders. But as it stands if your code won't be seeing a cylinder beyond 255 you don't need to be concerned. – Michael Petch Dec 01 '15 at 00:59
  • Thanks a lot! I solved the bug! The problem was that I thought that heads were divided in cylinders, not the opposite as it is in reality, so the conversion routine was wrong, as you said. – ptia Dec 01 '15 at 17:11

1 Answers1

3

I'm going to suggest a fix, but with one assumption. It appears that lba_to_chs is designed to work with smaller disk sizes where the number of cylinders doesn't exceed 0xff (255). This is fine for conventional floppy disk sizes since the number of cylinders will generally be a lot less than that.

First of all there is a bug in this code:

  mov ax, cx
  mov bl, [sectorsPerTrack]
  div bl ;al = lba / 18
  cbw ;ax = lba / 18
  mov bl, [headsPerCylinder]
  div bl ;al = lba / 18 / 2; ah = lba / 18 % 2
  mov byte [cilinder_number], ah
  mov byte [head_number], al

In the final division AL should contain the cylinder number, and AH should contain the head number. In your code you have reversed these. The last two lines should have read:

  mov byte [cilinder_number], al
  mov byte [head_number], ah

Given the assumption that has been made about cylinders not exceeding 255, one can modify the read_sector code a bit. INT 13h AH=02h requires the cylinder number and sector number to be placed in CX in this manner:

CX =       ---CH--- ---CL---
cylinder : 76543210 98
sector   :            543210

They also give this equation for the bit manipulation:

CX := ( ( cylinder and 255 ) shl 8 ) or ( ( cylinder and 768 ) shr 2 ) or sector;

Since our cylinders will not exceed 255 the equation reduces down to:

CX := ( ( cylinder and 255 ) shl 8 ) or sector;

This is the same as simply storing cylinder in CH and sector in CL . So the code you had for setting CX (CL and CH) that appears as:

  mov ah, 0x02 ;function 0x02, interrupt 0x13: read sectors
  ;al (the number of sectors to read), es:bx (destination) are set as arguments
  xor cx, cx
  mov cl, [cylinder_number]
  shl cl, 6
  or cl, [sector_number]
  mov dh, [head_number]
  mov dl, 0
  int 0x13

Would look something like:

  mov ah, 0x02 ;function 0x02, interrupt 0x13: read sectors
  ;al (the number of sectors to read), es:bx (destination) are set as arguments mov ch, [cilinder_number]
  mov ch, [cilinder_number]
  mov cl, [sector_number]
  mov dl, 0
  mov dh, [head_number]
  int 0x13

The code above has one other flaw, and that is the DL register is being set to 0. This is the drive number to read the sector from. This should be set to the drive number that the BIOS passes in DL to our bootloader when it jumps to memory address 0x07c00. We should save that value at startup and then copy it to DL when we read a sector. This allows us to boot from a drive that may not have been the first boot floppy (disk number 0x00).

The code could be amended by adding a boot_drive variable to your data area:

boot_drive db 0

After initializing the segment registers save the DL (boot drive) passed to our boot loader with:

mov [boot_drive], dl

And then in load_sector change:

mov dl, 0

to:

mov dl, [boot_drive]

The final code after all the suggested fixes and changes above could look something like:

bits 16
org 0

GLOBAL main
main:
start: jmp load

nop
OEM:                    DB "ptiaOS  "
bytesPerSector:     DW 512
sectorsPerCluster:  DB 1
reservedSectors:    DW 1
numberOfFATs:       DB 2
rootEntries:        DW 224
totalSectors:       DW 2880
media:              DB 0xf8
sectorsPerFAT:      DW 9
sectorsPerTrack:    DW 18
headsPerCylinder:   DW 2
hiddenSectors:      DD 0
totalSectorsBig:        DD 0
driveNumber:            DB 0
unused:         DB 0
extBootSignature:   DB 0x29
serialNumber:           DD 0xa0a1a2a3
volumeLabel:            DB "PTIAOS FLP "
fileSystem:             DB "FAT12   "

load:
  ;The bootloader is loaded at the address 0x7C00 and is 0x200 (512) bytes long
  cli
  mov ax, 0x07C0 ; setup registers to point to our segment
  mov ds, ax
  mov es, ax
  mov fs, ax
  mov gs, ax
  sti
  mov [boot_drive], dl

  mov si, hello_string
  call prints

  mov si, try_string
  call prints

  mov ax, 18
  call lba_to_chs

  mov al, 1
  mov bx, 0x200
  call read_sectors

  mov si, success_string
  call prints

  mov si, 0x200
  call prints

  cli
  hlt ;halt



;--------DATA--------
boot_drive db 0
hello_string db `Hi, bootloader of ptiaOS here\n\r`, 0
success_string db `Successfully loaded from floppy\n\r`, 0
try_string db `Loading more data from floppy...\n\r`, 0
;CHS position of the sector to read
sector_number db 0 ;1 is the first (they're 18 per track)
cilinder_number db 0 ;track number: 0 is the first (they're 80 per side)
head_number db 0 ;0 is the first (the're 2)

;---SOTTOPROGRAMMI---
;print a 0-terminated string pointed by ds:si
prints:
  mov ah, 0x0E ;dico all'interrupt del BIOS video di eseguire la funzione di stampa [al: carattere, bh: pagina]
  .prints_printchar:
    lodsb ;al = *(si++)
    cmp al, 0
    je .prints_end ;if(al == 0) goto print_string_end
      int 0x10 ;chiamo l'interrupt di i/o dello schermo, con ah = 0x0E per stampare il carattere in al
      jmp .prints_printchar
  .prints_end:
  ret
;Read sectors from floppy at the address specified by CHS variables, and load them in es:bx
read_sectors:
  mov ah, 0x02 ;function 0x02, interrupt 0x13: read sectors
  ;al (the number of sectors to read), es:bx (destination) are set as arguments  mov ch, [cilinder_number]
  mov ch, [cilinder_number]
  mov cl, [sector_number]
  mov dl, [boot_drive]
  mov dh, [head_number]
  int 0x13
  jnc .sectors_read_successfully ;CF = 0 if no errors, 1 otherwise
  ;if errors occured, try to reset floppy
  .flp_reset:
    mov ah, 0 ;function 0, interrupt 0x13: reset disk
    mov dl, 0 ;disk to reset: 0=floppy
    int 0x13
    jc .flp_reset ;CF = 0 if no errors, 1 otherwise
  jmp read_sectors
  .sectors_read_successfully:
  ret

lba_to_chs:
  mov cx, ax

  mov bl, [sectorsPerTrack]
  div bl
  inc ah ;ah = lba % 18 + 1
  mov byte [sector_number], ah

  mov ax, cx
  mov bl, [sectorsPerTrack]
  div bl ;al = lba / 18
  cbw ;ax = lba / 18
  mov bl, [headsPerCylinder]
  div bl ;al = lba / 18 / 2; ah = lba / 18 % 2
  mov byte [cilinder_number], al
  mov byte [head_number], ah

  ret

times 510-($-$$) db 0
dw 0xAA55
Michael Petch
  • 46,082
  • 8
  • 107
  • 198