QEMU is not at fault here. The discrepancy comes from the BIOS.
When I create a disk image as described in the question, then attach it to a QEMU virtual machine and use the info qtree
command in the monitor, this is the geometry I see reported:
cyls = 248 (0xf8)
heads = 16 (0x10)
secs = 63 (0x3f)
The same geometry is reported to the guest with the ATA IDENTIFY command. The reason for the discrepancy is what the BIOS firmware does with that hardware-reported geometry; this can be seen within the source code of SeaBIOS, which is the BIOS implementation used in QEMU. The disk_1308
function contains this code:
// read disk drive parameters
static void noinline
disk_1308(struct bregs *regs, struct drive_s *drive_fl)
{
// Get logical geometry from table
struct chs_s chs = getLCHS(drive_fl);
u16 nlc=chs.cylinder, nlh=chs.head, nls=chs.sector;
nlc--;
nlh--;
u8 count;
if (regs->dl < EXTSTART_HD) {
// Floppy
/* [...] */
} else if (regs->dl < EXTSTART_CD) {
// Hard drive
count = GET_BDA(hdcount);
nlc--; // last sector reserved
} else {
/* [...] */
}
/* [...] */
regs->al = 0;
regs->ch = nlc & 0xff;
regs->cl = ((nlc >> 2) & 0xc0) | (nls & 0x3f);
regs->dh = nlh;
disk_ret(regs, DISK_RET_SUCCESS);
regs->dl = count;
}
The interrupt 0x13 service 8 handler reads off the logical geometry from an internal data structure. Since the call is supposed to return the maximum CHS coordinates, the numbers of cylinders and heads are decremented by one, as those coordinates are zero-based. If the block device is a hard disk, the maximum cylinder number is decremented again. The latter is accompanied by a rather laconic comment: ‘last sector reserved’. What could that mean?
Ralf Brown’s entry on that service warns us that:
the maximum cylinder number reported in CX is usually two less than
the total cylinder count reported in the fixed disk parameter table
(see INT 41h,INT 46h) because early hard disks used the last cylinder
for testing purposes; however, on some Zenith machines, the maximum
cylinder number reportedly is three less than the count in the fixed
disk parameter table.
for BIOSes which reserve the last cylinder for testing purposes, the
cylinder count is automatically decremented
So apparently there is some historical precedent for this: some BIOSes would reserve the last cylinder of the drive for their own purposes, and as such, hide its existence from programs querying the drive geometry. This agrees with SeaBIOS’s behaviour: if we look up the geometry in the FDPT, we can see it contains the true number of cylinders.
The key word here seems to be ‘usually’, which implies this behaviour is optional, at the BIOS’s discretion. Nevertheless, it seems to have been pretty common indeed: nearly every BIOS firmware I tested under PCem does in fact reserve the last cylinder, including IBM’s; the only exception being Award BIOS v4.60PGA of a FIC VA-503+ motherboard from 2001, which is pretty late in PC history. All the others I checked report one fewer cylinder than there were configured in the emulator.
The Large Disk HOWTO elaborates a little more on what the reservation was used for and what effects it had on reporting disk size:
Many old IBM PS/2 systems used disks with a defect map written to the end of the disk. (Bit 0x20 in the control word of the disk parameter table is set.) Therefore, FDISK would not use the last cylinder. Just to be sure, the BIOS often already reports the size of the disk as one cylinder smaller than reality, and that may mean that two cylinders are lost. Newer BIOSes have several disk size reporting functions, where internally one calls the other. When both subtract 1 for this reserved cylinder and also FDISK does so, then one may lose three cylinders. These days all of this is irrelevant, but this may provide an explanation if one observes that different utilities have slightly different opinions about the disk size.
It does not seem that SeaBIOS actually makes use of that reservation anywhere though, so there is no reason service 8 should not report the true (emulated) geometry. As such, it is not unreasonable to consider it a bug after all. While there might conceivably be software in the wild that relies on this, by blindly attempting to undo the last-cylinder reservation, I think the probability of such being found is rather small. (I would expect software interested in the true disk geometry to consult the FDPT instead anyway.)
dd if=/dev/null of=hd.img bs=512 count=0 seek=$(( 16 * 63 * 0xf8 ))
would have been faster, as it creates a sparse file instead of copying small blocks of zeroes (and avoids spawning a subprocess for the calculation).