Issue
As part of my operating system I wrote this read sector function.
It takes a sector address to read from a BIOS device id. But when I set to read from sector 19 (Head: 0, Track: 1, Sector 2) the result at 0x1000:0x0000 is likely past that sector (I checked that several times with a hex viewer).
Also, when I read more than one sector, so that sector 19 is included, at the address mentioned above, I can read sector 19 which is copied at 0x1000:(512*19) without a problem.
void __NOINLINE resetDisk(const int device_id) {
__asm__ __volatile__("" : : "d"(0x0000|device_id)); //set device id
__asm__ __volatile__("mov $0x0000,%ax"); //function 0x02
__asm__ __volatile__("int $0x13");
}
void __NOINLINE readDiskSector(const int sector, const int device_id) {
resetDisk(device_id);
int sector_count = 2880;
int heads = 2;
int tracks = 18;
int h = sector/(sector_count/heads);
int c = (sector-h*(sector_count/heads))/tracks;
int s = sector-c*tracks-h*(sector_count/heads)+1;
__asm__ __volatile__("push %es");
__asm__ __volatile__("" : : "a"(c));
__asm__ __volatile__("" : : "b"(s));
__asm__ __volatile__("mov %al,%ch");
__asm__ __volatile__("mov %bl,%cl");
__asm__ __volatile__("" : : "a"(h));
__asm__ __volatile__("" : : "b"(device_id));
__asm__ __volatile__("mov %al,%dh");
__asm__ __volatile__("mov %bl,%dl");
__asm__ __volatile__("mov $0x03,%si");
__asm__ __volatile__("try_again_reading:");
__asm__ __volatile__("cmp $0x00,%si");
__asm__ __volatile__("je stop_trying");
__asm__ __volatile__("mov $0x1000,%bx");
__asm__ __volatile__("mov %bx,%es");
__asm__ __volatile__("mov $0x0000,%bx");
__asm__ __volatile__("mov $0x02,%ah");
__asm__ __volatile__("mov $0x01,%al");
__asm__ __volatile__("int $0x13");
__asm__ __volatile__("dec %si");
__asm__ __volatile__("jc try_again_reading");
__asm__ __volatile__("stop_trying:");
__asm__ __volatile__("pop %es");
}
Solution
The code has some serious issues from the perspective of proper GCC basic inline assembly and extended inline assembly, but fundamentally the problems with accessing Logical Block Address 19 (LBA) is in the calculations. LBA 19 is CHS (Cylinder, Head, Sector) = (0, 1, 2) where the OP suggests it is (Head: 0, Track: 1, Sector 2) which is incorrect.
Int 13h/ah=2 takes CHS values. You can convert an LBA to CHS values with the formula (or equivalent):
C = (LBA ÷ SPT) ÷ HPC H = (LBA ÷ SPT) mod HPC S = (LBA mod SPT) + 1
HPC = Heads per cylinder (aka Number of Heads) SPT = Sectors per Track, LBA = logical block address
"mod" is the modulo operator (to get the remainder of a division)
I have written more about the LBA to CHS calculation in this other Stackoverflow answer in the section Translation of LBA to CHS.
One observation is that the maximum sector number doesn't factor into the equation at all. The real issue here is that the OP's formula is incorrect:
int sector_count = 2880;
int heads = 2; /* Head per cylinder */
int tracks = 18; /* Sectors per Track */
int h = sector/(sector_count/heads);
int c = (sector-h*(sector_count/heads))/tracks;
int s = sector-c*tracks-h*(sector_count/heads)+1;
The only the part of the equation that yields a correct result (in a round about way) is s
(sector). c
(cylinder) and h
(head) are computed incorrectly. Because of this it is causing the issues observed in the question and the OP's followup answer. To get an idea of the values being produced by the OP's equation I wrote a program to compare their values with correct ones using a proper formula:
LBA = 0: CHS = ( 0, 0, 1) | CHS = ( 0, 0, 1)
LBA = 1: CHS = ( 0, 0, 2) | CHS = ( 0, 0, 2)
LBA = 2: CHS = ( 0, 0, 3) | CHS = ( 0, 0, 3)
LBA = 3: CHS = ( 0, 0, 4) | CHS = ( 0, 0, 4)
LBA = 4: CHS = ( 0, 0, 5) | CHS = ( 0, 0, 5)
LBA = 5: CHS = ( 0, 0, 6) | CHS = ( 0, 0, 6)
LBA = 6: CHS = ( 0, 0, 7) | CHS = ( 0, 0, 7)
LBA = 7: CHS = ( 0, 0, 8) | CHS = ( 0, 0, 8)
LBA = 8: CHS = ( 0, 0, 9) | CHS = ( 0, 0, 9)
LBA = 9: CHS = ( 0, 0, 10) | CHS = ( 0, 0, 10)
LBA = 10: CHS = ( 0, 0, 11) | CHS = ( 0, 0, 11)
LBA = 11: CHS = ( 0, 0, 12) | CHS = ( 0, 0, 12)
LBA = 12: CHS = ( 0, 0, 13) | CHS = ( 0, 0, 13)
LBA = 13: CHS = ( 0, 0, 14) | CHS = ( 0, 0, 14)
LBA = 14: CHS = ( 0, 0, 15) | CHS = ( 0, 0, 15)
LBA = 15: CHS = ( 0, 0, 16) | CHS = ( 0, 0, 16)
LBA = 16: CHS = ( 0, 0, 17) | CHS = ( 0, 0, 17)
LBA = 17: CHS = ( 0, 0, 18) | CHS = ( 0, 0, 18)
LBA = 18: CHS = ( 1, 0, 1) | CHS = ( 0, 1, 1)
LBA = 19: CHS = ( 1, 0, 2) | CHS = ( 0, 1, 2)
LBA = 20: CHS = ( 1, 0, 3) | CHS = ( 0, 1, 3)
LBA = 21: CHS = ( 1, 0, 4) | CHS = ( 0, 1, 4)
LBA = 22: CHS = ( 1, 0, 5) | CHS = ( 0, 1, 5)
LBA = 23: CHS = ( 1, 0, 6) | CHS = ( 0, 1, 6)
LBA = 24: CHS = ( 1, 0, 7) | CHS = ( 0, 1, 7)
LBA = 25: CHS = ( 1, 0, 8) | CHS = ( 0, 1, 8)
LBA = 26: CHS = ( 1, 0, 9) | CHS = ( 0, 1, 9)
LBA = 27: CHS = ( 1, 0, 10) | CHS = ( 0, 1, 10)
LBA = 28: CHS = ( 1, 0, 11) | CHS = ( 0, 1, 11)
LBA = 29: CHS = ( 1, 0, 12) | CHS = ( 0, 1, 12)
LBA = 30: CHS = ( 1, 0, 13) | CHS = ( 0, 1, 13)
LBA = 31: CHS = ( 1, 0, 14) | CHS = ( 0, 1, 14)
LBA = 32: CHS = ( 1, 0, 15) | CHS = ( 0, 1, 15)
LBA = 33: CHS = ( 1, 0, 16) | CHS = ( 0, 1, 16)
LBA = 34: CHS = ( 1, 0, 17) | CHS = ( 0, 1, 17)
LBA = 35: CHS = ( 1, 0, 18) | CHS = ( 0, 1, 18)
LBA = 36: CHS = ( 2, 0, 1) | CHS = ( 1, 0, 1)
LBA = 37: CHS = ( 2, 0, 2) | CHS = ( 1, 0, 2)
LBA = 38: CHS = ( 2, 0, 3) | CHS = ( 1, 0, 3)
LBA = 39: CHS = ( 2, 0, 4) | CHS = ( 1, 0, 4)
LBA = 40: CHS = ( 2, 0, 5) | CHS = ( 1, 0, 5)
LBA = 41: CHS = ( 2, 0, 6) | CHS = ( 1, 0, 6)
LBA = 42: CHS = ( 2, 0, 7) | CHS = ( 1, 0, 7)
LBA = 43: CHS = ( 2, 0, 8) | CHS = ( 1, 0, 8)
LBA = 44: CHS = ( 2, 0, 9) | CHS = ( 1, 0, 9)
LBA = 45: CHS = ( 2, 0, 10) | CHS = ( 1, 0, 10)
LBA = 46: CHS = ( 2, 0, 11) | CHS = ( 1, 0, 11)
LBA = 47: CHS = ( 2, 0, 12) | CHS = ( 1, 0, 12)
LBA = 48: CHS = ( 2, 0, 13) | CHS = ( 1, 0, 13)
LBA = 49: CHS = ( 2, 0, 14) | CHS = ( 1, 0, 14)
LBA = 50: CHS = ( 2, 0, 15) | CHS = ( 1, 0, 15)
LBA = 51: CHS = ( 2, 0, 16) | CHS = ( 1, 0, 16)
LBA = 52: CHS = ( 2, 0, 17) | CHS = ( 1, 0, 17)
LBA = 53: CHS = ( 2, 0, 18) | CHS = ( 1, 0, 18)
LBA = 54: CHS = ( 3, 0, 1) | CHS = ( 1, 1, 1)
LBA = 55: CHS = ( 3, 0, 2) | CHS = ( 1, 1, 2)
LBA = 56: CHS = ( 3, 0, 3) | CHS = ( 1, 1, 3)
LBA = 57: CHS = ( 3, 0, 4) | CHS = ( 1, 1, 4)
LBA = 58: CHS = ( 3, 0, 5) | CHS = ( 1, 1, 5)
LBA = 59: CHS = ( 3, 0, 6) | CHS = ( 1, 1, 6)
LBA = 60: CHS = ( 3, 0, 7) | CHS = ( 1, 1, 7)
LBA = 61: CHS = ( 3, 0, 8) | CHS = ( 1, 1, 8)
LBA = 62: CHS = ( 3, 0, 9) | CHS = ( 1, 1, 9)
LBA = 63: CHS = ( 3, 0, 10) | CHS = ( 1, 1, 10)
LBA = 64: CHS = ( 3, 0, 11) | CHS = ( 1, 1, 11)
LBA = 65: CHS = ( 3, 0, 12) | CHS = ( 1, 1, 12)
LBA = 66: CHS = ( 3, 0, 13) | CHS = ( 1, 1, 13)
LBA = 67: CHS = ( 3, 0, 14) | CHS = ( 1, 1, 14)
LBA = 68: CHS = ( 3, 0, 15) | CHS = ( 1, 1, 15)
LBA = 69: CHS = ( 3, 0, 16) | CHS = ( 1, 1, 16)
LBA = 70: CHS = ( 3, 0, 17) | CHS = ( 1, 1, 17)
LBA = 71: CHS = ( 3, 0, 18) | CHS = ( 1, 1, 18)
LBA = 72: CHS = ( 4, 0, 1) | CHS = ( 2, 0, 1)
LBA = 73: CHS = ( 4, 0, 2) | CHS = ( 2, 0, 2)
LBA = 74: CHS = ( 4, 0, 3) | CHS = ( 2, 0, 3)
LBA = 75: CHS = ( 4, 0, 4) | CHS = ( 2, 0, 4)
LBA = 76: CHS = ( 4, 0, 5) | CHS = ( 2, 0, 5)
LBA = 77: CHS = ( 4, 0, 6) | CHS = ( 2, 0, 6)
LBA = 78: CHS = ( 4, 0, 7) | CHS = ( 2, 0, 7)
LBA = 79: CHS = ( 4, 0, 8) | CHS = ( 2, 0, 8)
...
The OP's results are on the left and the correct ones on the right. LBA 0 through LBA 17 are correct. If you start reading one or more sectors with an LBA less than 18 it will be correct. If you use the CHS values computed for LBA 19 they are incorrect.
The OP suggests in their answer that the documentation for the cylinder and head values are incorrect, and that the registers are reversed. The documentation is correct:
AL = number of sectors to read (must be nonzero)
CH = low eight bits of cylinder number
CL = sector number 1-63 (bits 0-5)
high two bits of cylinder (bits 6-7, hard disk only)
DH = head number
DL = drive number (bit 7 set for hard disk)
ES:BX -> data buffer
The OP's answer suggests a fix is to swap the heads and cylinders around. That in fact happens to make his code work by accident for the LBA 0 through LBA 35. LBA >= 36 are incorrect.
The fix is to use a proper calculation in the OP's code:
c = (sector / tracks) / heads;
h = (sector / tracks) % heads;
s = (sector % tracks) + 1;
Code to Test LBA to CHS Equations
#include <stdio.h>
int main()
{
const int sector_count = 2880;
const int heads = 2;
const int tracks = 18; /* tracks per sector */
unsigned char h, h2;
unsigned char c, c2;
unsigned char s, s2;
int sector; /* LBA */
for (sector=0; sector < sector_count; sector++) {
/* Improper calculation */
h = sector/(sector_count/heads);
c = (sector-h*(sector_count/heads))/tracks;
s = sector-c*tracks-h*(sector_count/heads)+1;
/* Proper calculation */
c2 = (sector / tracks) / heads;
h2 = (sector / tracks) % heads;
s2 = (sector % tracks) + 1;
printf ("LBA = %4d: CHS = (%2d, %2d, %2d) | CHS = (%2d, %2d, %2d)\n",
sector, c, h, s, c2, h2, s2);
}
return 0;
}
Sample GCC Code Using Inline Assembly to do Disk Reads
biosdisk.h
#ifndef BIOSDISK_H
#define BIOSDISK_H
#include <stdint.h>
/* BIOS Parameter Block (BPB) on floppy media */
typedef struct __attribute__((packed)) {
char OEMname[8];
uint16_t bytesPerSector;
uint8_t sectPerCluster;
uint16_t reservedSectors;
uint8_t numFAT;
uint16_t numRootDirEntries;
uint16_t numSectors;
uint8_t mediaType;
uint16_t numFATsectors;
uint16_t sectorsPerTrack;
uint16_t numHeads;
uint32_t numHiddenSectors;
uint32_t numSectorsHuge;
uint8_t driveNum;
uint8_t reserved;
uint8_t signature;
uint32_t volumeID;
char volumeLabel[11];
char fileSysType[8];
} disk_bpb_s;
/* State information for CHS disk accesses */
typedef struct __attribute__((packed)) {
uint16_t segment;
uint16_t offset;
uint16_t status;
/* Drive geometry needed to compute CHS from LBA */
uint16_t sectorsPerTrack;
uint16_t numHeads;
/* Disk parameters */
uint16_t cylinder;
uint8_t head;
uint8_t sector;
uint8_t driveNum;
uint8_t numSectors; /* # of sectors to read */
/* Number of retries for disk operations */
uint8_t retries;
} disk_info_s;
extern fastcall uint8_t
reset_disk (disk_info_s *const disk_info);
extern fastcall uint8_t
read_sector_chs (disk_info_s *const disk_info);
/* Forced inline version of reset_sector */
static inline fastcall always_inline uint8_t
reset_disk_i (disk_info_s *const disk_info)
{
uint16_t temp_ax = 0x0000;
uint8_t carryf;
__asm__ __volatile__ (
"int $0x13\n\t"
#ifdef __GCC_ASM_FLAG_OUTPUTS__
: [cf]"=@ccc"(carryf),
#else
"setc %[cf]\n\t"
: [cf]"=qm"(carryf),
#endif
"+a"(temp_ax)
: "d"(disk_info->driveNum)
: "cc");
disk_info->status = temp_ax;
return (carryf);
}
/* Forced inline version of read_sector */
static inline fastcall always_inline uint8_t
read_sector_chs_i (disk_info_s *const disk_info)
{
uint16_t temp_ax;
uint16_t temp_dx;
uint8_t carryf = 0;
uint8_t retry_count = 0;
#ifndef BUGGY_BIOS_SUPPORT
temp_dx = (disk_info->head << 8) | disk_info->driveNum;
#endif
do {
/* Only reset disk if error detected previously */
if (carryf)
reset_disk_i (disk_info);
/* Need to reload AX during each iteration since a previous
* int 0x13 call will destroy its contents. There was a bug on
* earlier BIOSes where DX may have been clobbered.
*/
temp_ax = (0x02 << 8) | disk_info->numSectors;
#ifdef BUGGY_BIOS_SUPPORT
temp_dx = (disk_info->head << 8) | disk_info->driveNum;
#endif
__asm__ __volatile__ (
"push %%es\n\t"
"mov %w[seg], %%es\n\t"
#ifdef BUGGY_BIOS_SUPPORT
"stc\n\t" /* Some early bioses have CF bug */
"int $0x13\n\t"
"sti\n\t" /* Some early bioses don't re-enable interrupts */
#else
"int $0x13\n\t"
#endif
"pop %%es\n\t"
#ifdef __GCC_ASM_FLAG_OUTPUTS__
: [cf]"=@ccc"(carryf),
#else
"setc %[cf]\n\t"
: [cf]"=qm"(carryf),
#endif
#ifdef BUGGY_BIOS_SUPPORT
"+a"(temp_ax),
"+d"(temp_dx)
:
#else
"+a"(temp_ax)
:
"d"(temp_dx),
#endif
"c"(((disk_info->cylinder & 0xff) << 8) |
((disk_info->cylinder >> 2) & 0xC0) |
(disk_info->sector & 0x3f)),
"b"(disk_info->offset),
[seg]"r"(disk_info->segment)
: "memory", "cc");
} while (carryf && (++retry_count < disk_info->retries));
disk_info->status = temp_ax;
return (carryf);
}
/* Forced inline version of read_sector_lba */
static inline fastcall always_inline uint8_t
read_sector_lba_i (disk_info_s *const disk_info, const uint32_t lba)
{
disk_info->cylinder = lba / disk_info->sectorsPerTrack / disk_info->numHeads;
disk_info->head = (lba / disk_info->sectorsPerTrack) % disk_info->numHeads;
disk_info->sector = (lba % disk_info->sectorsPerTrack) + 1;
return read_sector_chs_i (disk_info);
}
#endif
biosdisk.c:
#include <stdint.h>
#include "biosdisk.h"
fastcall uint8_t
reset_disk (disk_info_s *const disk_info)
{
return reset_disk_i (disk_info);
}
fastcall uint8_t
read_sector_chs (disk_info_s *const disk_info)
{
return read_sector_chs_i (disk_info);
}
fastcall uint8_t
read_sector_lba (disk_info_s *const disk_info, const uint32_t lba)
{
return read_sector_lba_i (disk_info, lba);
}
x86helper.h:
#ifndef X86HELPER_H
#define X86HELPER_H
#define fastcall __attribute__((regparm(3)))
/* noreturn lets GCC know that a function that it may detect
won't exit is intentional */
#define noreturn __attribute__((noreturn))
#define always_inline __attribute__((always_inline))
#define used __attribute__((used))
#endif
A small proof of concept project that creates a 2 stage bootloader in GCC can be found on my website
Notes
Int 13h/AH=0h resets the disk system. This operation can take a fair amount of time on real hardware like a floppy disk as it also re-calibrates the drive heads. You should only reset the disk after an error is detected and before you retry the disk operation.
Using GCC to create code that will run in realmode is problematic at best. The code generated with
-m16
can generally only run on 80386 or later processors.Multiple
asm
statements are not guaranteed by the compiler to be emitted in the order they appear in the code. You should combine multipleasm
statements into one. The GCC documentation says this:
Do not expect a sequence of asm statements to remain perfectly consecutive after compilation, even when you are using the volatile qualifier. If certain instructions need to remain consecutive in the output, put them in a single multi-instruction asm statement.
If you modify a register in GCC's inline assembly you should be telling the compiler. Use GCC's extended inline assembly and list the modified registers in the clobber list.
Try to minimize inline assembly to the bare minimum and utilize as much C code as possible. David Wohlferd wrote a good article with reasons not to use inline assembly. If you don't understand the nuances of inline assembly then you might consider writing the code in a separate assembly language module and link it to your C program.
GCC has no notion of realmode's 20-bit segment offset addressing which makes things overly complex and bloated. Rather than using GCC there are other choices for developing 16-bit code in C like Open Watcom C/C++; Alexey Frunze's Smaller C compiler; or the experimental ia16-gcc cross compiler port of GCC.
Answered By - Michael Petch