/* $OpenBSD: mbr.S,v 1.9 2022/09/02 07:46:03 krw Exp $ */ /* * Copyright (c) 1997 Michael Shalayeff and Tobias Weingartner * Copyright (c) 2003 Tom Cosgrove * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * */ /* Copyright (c) 1996 VaX#n8 (vax@linkdead.paranoia.com) * last edited 9 July 1996 * many thanks to Erich Boleyn (erich@uruk.org) for putting up with * all my questions, and for his work on GRUB * You may use this code or fragments thereof in a manner consistent * with the other copyrights as long as you retain my pseudonym and * this copyright notice in the file. */ .file "mbr.S" #include #include /* * Memory layout: * * 0x07C00 -> 0x07DFF BIOS loads us here (at 31k) * 0x07E00 -> 0x17BFC our stack (to 95k) * * 0x07A00 -> 0x07BFF we relocate to here (at 30k5) * * 0x07C00 -> 0x07DFF we load PBR here (at 31k) * * The BIOS loads us at physical address 0x07C00. We use a long jmp to * normalise our address to seg:offset 07C0:0000. We then relocate to * 0x07A00, seg:offset 07A0:0000. * * We use a long jmp to normalise our address to seg:offset 07A0:0000 * We set the stack to start at 07C0:FFFC (grows down on i386) * The partition boot record (PBR) loads /boot at seg:offset 4000:0000 */ #define BOOTSEG 0x7c0 /* segment where we are loaded */ #define BOOTRELOCSEG 0x7a0 /* segment where we relocate to */ #define BOOTSTACKOFF 0xfffc /* stack starts here, grows down */ #define PARTSZ 16 /* each partition table entry is 16 bytes */ #define CHAR_LBA_READ '.' #define CHAR_CHS_READ ';' #ifdef DEBUG #define CHAR_S 'S' /* started */ #define CHAR_R 'R' /* relocated */ #define CHAR_L 'L' /* looking for bootable partition */ #define CHAR_B 'B' /* loading boot */ #define CHAR_G 'G' /* jumping to boot */ #define DBGMSG(c) movb $c, %al; call Lchr #else /* !DEBUG */ #define DBGMSG(c) #endif /* !DEBUG */ /* Clobbers %al - maybe more */ #define putc(c) movb $c, %al; call Lchr /* Clobbers %esi - maybe more */ #define puts(s) movw $s, %si; call Lmessage .text .code16 .globl start start: /* Adjust %cs to be right */ ljmp $BOOTSEG, $1f 1: /* Set up stack */ movw %cs, %ax /* * We don't need to disable and re-enable interrupts around the * the load of ss and sp. * * From 80386 Programmer's Reference Manual: * "A MOV into SS inhibits all interrupts until after the execution * of the next instruction (which is presumably a MOV into eSP)" * * According to Hamarsoft's 86BUGS list (which is distributed with * Ralph Brown's Interrupt List), some early 8086/88 processors * failed to disable interrupts following a load into a segment * register, but this was fixed with later steppings. * * Accordingly, this code will fail on very early 8086/88s, but * nick@ will just have to live with it. Others will note that * we require at least a Pentium compatible processor anyway. */ /* cli */ movw %ax, %ss movw $BOOTSTACKOFF, %sp /* sti */ /* XXX not necessary; see above */ /* Set up data segment */ movw %ax, %ds DBGMSG(CHAR_S) /* * On the PC architecture, the boot record (originally on a floppy * disk) is loaded at 0000:7C00 (hex) and execution starts at the * beginning. * * When hard disk support was added, a scheme to partition disks into * four separate partitions was used, to allow multiple operating * systems to be installed on the one disk. The boot sectors of the * operating systems on each partition would of course expect to be * loaded at 0000:7C00. * * The first sector of the hard disk is the master boot record (MBR). * It is this which defines the partitions and says which one is * bootable. Of course, the BIOS loads the MBR at 0000:7C00, the * same location where the MBR needs to load the partition boot * record (PBR, called biosboot in OpenBSD). * * Therefore, the MBR needs to relocate itself before loading the PBR. * * Make it so. */ movw $BOOTRELOCSEG, %ax movw %ax, %es xorw %si, %si xorw %di, %di movw $0x200, %cx /* Bytes in MBR, relocate it all */ cld rep movsb /* Jump to relocated self */ ljmp $BOOTRELOCSEG, $reloc reloc: DBGMSG(CHAR_R) /* Set up %es and %ds */ pushw %ds popw %es /* next boot is at the same place as we were loaded */ pushw %cs popw %ds /* and %ds is at the %cs */ #ifdef SERIAL /* Initialize the serial port to 9600 baud, 8N1. */ pushw %dx xorw %ax, %ax movb $0xe3, %ax movw $SERIAL, %dx int $0x14 popw %dx #endif /* BIOS passes us drive number in %dl * * XXX - This is not always true. We currently check if %dl * points to a HD, and if not we complain, and set it to point * to the first HDD. Note, this is not 100% correct, since * there is a possibility that you boot from HD #2, and still * get (%dl & 0x80) == 0x00, these type of systems will lose. */ testb $0x80, %dl jnz drive_ok /* MBR on floppy or old BIOS * Note: MBR (this code) should never be on a floppy. It does * not belong there, so %dl should never be 0x00. * * Here we simply complain (should we?), and then hardcode the * boot drive to 0x80. */ puts(efdmbr) /* If we are passed bogus data, set it to HD #1 */ movb $0x80, %dl drive_ok: /* Find the first active partition. * Note: this should be the only active partition. We currently * don't check for that. */ movw $pt, %si movw $NDOSPART, %cx find_active: DBGMSG(CHAR_L) movb (%si), %al cmpb $DOSACTIVE, %al je found addw $PARTSZ, %si loop find_active /* No bootable partition */ no_part: movw $enoboot, %si err_stop: call Lmessage stay_stopped: sti /* Ensure Ctl-Alt-Del will work */ hlt /* Just to make sure */ jmp stay_stopped found: /* * Found bootable partition */ DBGMSG(CHAR_B) /* Store the drive number (from %dl) in decimal */ movb %dl, %al andb $0x0F, %al addb $'0', %al movb %al, drive_num /* * Store the partition number, in decimal. * * We started with cx = 4; if found we want part '0' * cx = 3; part '1' * cx = 2; part '2' * cx = 1; part '3' * * We'll come into this with no other values for cl. */ movb $'0'+4, %al subb %cl, %al movb %al, part_num /* * Tell operator what partition we're trying to boot. * * Using drive X, partition Y * - this used to be printed out after successfully loading the * partition boot record; we now print it out before */ pushw %si movw $info, %si call Lmessage popw %si /* * Partition table entry format: * * 0x00 BYTE boot indicator (0x80 = active, 0x00 = inactive) * 0x01 BYTE start head * 0x02 WORD start cylinder, sector * 0x04 BYTE system type (0xA6 = OpenBSD) * 0x05 BYTE end head * 0x06 WORD end cylinder, sector * 0x08 LONG start LBA sector * 0x0C LONG number of sectors in partition * * In the case of a partition that extends beyond the 8GB boundary, * the LBA values will be correct, the CHS values will have their * maximums (typically (C,H,S) = (1023,255,63)). * * %ds:%si points to the active partition table entry. */ /* We will load the partition boot sector (biosboot) where we * were originally loaded. We'll check to make sure something * valid comes in. So that we don't find ourselves, zero out * the signature at the end. */ movw $0, %es:signature(,1) /* * We will use the LBA sector number if we have LBA support, * so find out. */ /* * BIOS call "INT 0x13 Extensions Installation Check" * Call with %ah = 0x41 * %bx = 0x55AA * %dl = drive (0x80 for 1st hd, 0x81 for 2nd, etc) * Return: * carry set: failure * %ah = error code (0x01, invalid func) * carry clear: success * %bx = 0xAA55 (must verify) * %ah = major version of extensions * %al (internal use) * %cx = capabilities bitmap * 0x0001 - extnd disk access funcs * 0x0002 - rem. drive ctrl funcs * 0x0004 - EDD functions with EBP * %dx (extension version?) */ movb %dl, (%si) /* Store drive here temporarily */ /* (This call trashes %dl) */ /* * XXX This is actually the correct * place to store this. The 0x80 * value used to indicate the * active partition is by intention * the same as the BIOS drive value * for the first hard disk (0x80). * At one point, 0x81 would go here * for the second hard disk; the * 0x80 value is often used as a * bit flag for testing, rather * than an exact byte value. */ movw $0x55AA, %bx movb $0x41, %ah int $0x13 movb (%si), %dl /* Get back drive number */ jc do_chs /* Did the command work? Jump if not */ cmpw $0xAA55, %bx /* Check that bl, bh exchanged */ jne do_chs /* If not, don't have EDD extensions */ testb $0x01, %cl /* And do we have "read" available? */ jz do_chs /* Again, use CHS if not */ do_lba: /* * BIOS call "INT 0x13 Extensions Extended Read" * Call with %ah = 0x42 * %dl = drive (0x80 for 1st hd, 0x81 for 2nd, etc) * %ds:%si = segment:offset of command packet * Return: * carry set: failure * %ah = error code (0x01, invalid func) * command packet's sector count field set * to the number of sectors successfully * transferred * carry clear: success * %ah = 0 (success) * Command Packet: * 0x0000 BYTE packet size (0x10 or 0x18) * 0x0001 BYTE reserved (should be 0) * 0x0002 WORD sectors to transfer (max 127) * 0x0004 DWORD seg:offset of transfer buffer * 0x0008 QWORD starting sector number */ movb $CHAR_LBA_READ, %al call Lchr /* Load LBA sector number from active partition table entry */ movl 8(%si), %ecx movl %ecx, lba_sector pushw %si /* We'll need %si later */ movb $0x42, %ah movw $lba_command, %si int $0x13 popw %si /* get back %si */ jnc booting_os /* If it worked, run the pbr we got */ /* * LBA read failed, fall through to try CHS read */ do_chs: /* * BIOS call "INT 0x13 Function 0x2" to read sectors from disk into * memory * Call with %ah = 0x2 * %al = number of sectors * %ch = cylinder & 0xFF * %cl = sector (0-63) | rest of cylinder bits * %dh = head * %dl = drive (0x80 for hard disk) * %es:%bx = segment:offset of buffer * Return: * carry set: failure * %ah = err code * %al = number of sectors transferred * carry clear: success * %al = 0x0 OR number of sectors transferred * (depends on BIOS!) * (according to Ralph Brown Int List) */ movb $CHAR_CHS_READ, %al call Lchr /* Load values from active partition table entry */ movb 1(%si), %dh /* head */ movw 2(%si), %cx /* sect, cyl */ movw $0x201, %ax /* function and number of blocks */ xorw %bx, %bx /* put it at %es:0 */ int $0x13 jnc booting_os read_error: movw $eread, %si jmp err_stop booting_os: puts(crlf) DBGMSG(CHAR_G) /* * Make sure the pbr we loaded has a valid signature at the end. * This also ensures that something did load where we were expecting * it, as there's still a copy of our code there... */ cmpw $DOSMBR_SIGNATURE, %es:signature(,1) jne missing_os /* jump to the new code (%ds:%si is at the right point) */ ljmp $0, $BOOTSEG << 4 /* not reached */ missing_os: movw $enoos, %si jmp err_stop /* * Display string */ Lmessage: pushw %ax cld 1: lodsb /* %al = *%si++ */ testb %al, %al jz 1f call Lchr jmp 1b /* * Lchr: write the error message in %ds:%si to console */ Lchr: pushw %ax #ifdef SERIAL pushw %dx movb $0x01, %ah movw $SERIAL, %dx int $0x14 popw %dx #else pushw %bx movb $0x0e, %ah movw $1, %bx int $0x10 popw %bx #endif 1: popw %ax ret /* command packet for LBA read of boot sector */ lba_command: .byte 0x10 /* size of command packet */ .byte 0x00 /* reserved */ .word 0x0001 /* sectors to transfer, just 1 */ .word 0 /* target buffer, offset */ .word BOOTSEG /* target buffer, segment */ lba_sector: .long 0, 0 /* sector number */ /* Info messages */ info: .ascii "Using drive " drive_num: .byte 'X' .ascii ", partition " part_num: .asciz "Y" /* Error messages */ efdmbr: .asciz "MBR on floppy or old BIOS\r\n" eread: .asciz "\r\nRead error\r\n" enoos: .asciz "No O/S\r\n" enoboot: .ascii "No active partition" /* runs into crlf... */ crlf: .asciz "\r\n" endofcode: nop /* (MBR) NT disk signature offset */ . = 0x1b8 .space 4, 0 /* partition table */ /* flag, head, sec, cyl, type, ehead, esect, ecyl, start, len */ . = DOSPARTOFF /* starting address of partition table */ pt: .fill 0x40,1,0 /* the last 2 bytes in the sector 0 contain the signature */ . = 0x1fe signature: .short DOSMBR_SIGNATURE . = 0x200