vmware-svga/lib/metalkit/boot.S
2009-04-13 07:05:42 +00:00

587 lines
19 KiB
ArmAsm

/*
* boot.S --
*
* This is a tiny but relatively featureful bootloader for
* 32-bit standalone apps and kernels. It compiles into one
* binary that can be used either stand-alone (loaded directly
* by the BIOS, from a floppy or USB disk image) or as a GNU
* Multiboot image, loaded by GRUB.
*
* This bootloader loads itself and the attached main program
* at 1MB, with the available portions of the first megabyte of
* RAM set up as stack space by default.
*
* This loader is capable of loading an arbitrarily big binary
* image from the boot device into high memory. If you're booting
* from a floppy, it can load the whole 1.44MB disk. If you're
* booting from USB, it can load any amount of data from the USB
* disk.
*
* This loader works by using the BIOS's disk services, so we
* should be able to read the whole binary image off of any device
* the BIOS knows how to boot from. Since we have only a tiny
* amount of buffer space, and we need to store the resulting image
* above the 1MB boundary, we have to keep switching back and forth
* between real mode and protected mode.
*
* To avoid device-specific CHS addressing madness, we require LBA
* mode to boot off of anything other than a 1.44MB floppy or a
* Multiboot loader. We try to use the INT 13h AH=42h "Extended Read
* Sectors From Drive" command, which uses LBA addressing. If this
* doesn't work, we fall back to floppy-disk-style CHS addressing.
*
*
* This file is part of Metalkit, a simple collection of modules for
* writing software that runs on the bare metal. Get the latest code
* at http://svn.navi.cx/misc/trunk/metalkit/
*
* Copyright (c) 2008-2009 Micah Dowty
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#define ASM
#include "boot.h"
/*
* Constants that affect our early boot memory map.
*/
#define BIOS_START_ADDRESS 0x7C00 // Defined by the BIOS
#define EARLY_STACK_ADDRESS 0x2000 // In low DOS memory
#define SECTORS_AT_A_TIME 18 // Must equal CHS sectors per head
#define SECTOR_SIZE 512
#define DISK_BUFFER 0x2800
#define DISK_BUFFER_SIZE (SECTORS_AT_A_TIME * SECTOR_SIZE)
#define BIOS_PTR(x) (x - _start + BIOS_START_ADDRESS)
.section .boot
.global _start
/*
* External symbols. main() is self-explanatory, but these
* other symbols must be provided by the linker script. See
* "image.ld" for the actual partition size and LDT calculations.
*/
.extern main
.extern _end
.extern _edata
.extern _bss_size
.extern _stack
.extern _partition_chs_head
.extern _partition_chs_sector_byte
.extern _partition_chs_cylinder_byte
.extern _partition_blocks
.extern _ldt_byte0
.extern _ldt_byte1
.extern _ldt_byte2
.extern _ldt_byte3
/*
* Other modules can optionally define an LDT in uninitialized
* memory. By default this LDT will be all zeroes, but this
* is a simple and code-size-efficient way of letting other
* Metalkit modules allocate segment descriptors when they
* need to.
*
* Note that we page-align the LDT. This isn't strictly
* necessary, but it might be useful for performance in
* some environments.
*/
.comm LDT, BOOT_LDT_SIZE, 4096
/*
* This begins our 16-bit DOS MBR boot sector segment. This
* sits in the first 512 bytes of our floppy image, and it
* gets loaded by the BIOS at START_ADDRESS.
*
* Until we've loaded the memory image off of disk into
* its final location, this code is running at a different
* address than the linker is expecting. Any absolute
* addresses must be fixed up by the BIOS_PTR() macro.
*/
.code16
_start:
ljmp $0, $BIOS_PTR(bios_main)
/*
* gnu_multiboot --
*
* GNU Multiboot header. This can come anywhere in the
* first 8192 bytes of the image file.
*/
.p2align 2
.code32
gnu_multiboot:
#define MULTIBOOT_MAGIC 0x1BADB002
#define MULTIBOOT_FLAGS 0x00010000
.long MULTIBOOT_MAGIC
.long MULTIBOOT_FLAGS
.long -(MULTIBOOT_MAGIC + MULTIBOOT_FLAGS)
.long gnu_multiboot
.long _start
.long _edata
.long _end
.long entry32
/*
* String table, located in the boot sector.
*/
loading_str: .string "\r\nMETALKIT "
disk_error_str: .string " err!"
/*
* bios_main --
*
* Main routine for our BIOS MBR based loader. We set up the
* stack, display some welcome text, then load the rest of
* the boot image from disk. We have to use real mode to
* call the BIOS's floppy driver, then protected mode to
* copy each disk block to its final location above the 1MB
* barrier.
*/
.code16
bios_main:
/*
* Early init: setup our stack and data segments, make sure
* interrupts are off.
*/
cli
xorw %ax, %ax
movw %ax, %ss
movw %ax, %ds
movw %ax, %es
movw $EARLY_STACK_ADDRESS, %sp
/*
* Save parameters that the BIOS gave us via registers.
*/
mov %dl, BIOS_PTR(disk_drive)
/*
* Switch on the A20 gate, so we can access more than 1MB
* of memory. There are multiple ways to do this: The
* original way was to write to bit 1 of the keyboard
* controller's output port. There's also a bit on PS2
* System Control port A to enable A20.
*
* The keyboard controller method should always work, but
* it's kind of slow and it takes a lot of code space in
* our already-cramped bootloader. Instead, we ask the BIOS
* to enable A20.
*
* If your computer doesn't support this BIOS interface,
* you'll see our "err!" message before "METAL" appears.
*
* References:
* http://www.win.tue.nl/~aeb/linux/kbd/A20.html
*/
mov $0x2401, %ax // Enable A20
int $0x15
jc fatal_error
/*
* Load our image, starting at the beginning of whatever disk
* the BIOS told us we booted from. The Disk Address Packet
* (DAP) has already been initialized statically.
*/
mov $BIOS_PTR(loading_str), %si
call print_str
/*
* Fill our DISK_BUFFER, reading SECTORS_AT_A_TIME sectors.
*
* First, try to use LBA addressing. This is required in
* order to boot off of non-floppy devices, like USB drives.
*/
disk_copy_loop:
mov $0x42, %ah
mov BIOS_PTR(disk_drive), %dl
mov $BIOS_PTR(dap_buffer), %si
int $0x13
jnc disk_success
/*
* If LBA fails, fall back to old fashioned CHS addressing.
* This works everywhere, but only if we're on a 1.44MB floppy.
*/
mov $(0x0200 | SECTORS_AT_A_TIME), %ax
mov BIOS_PTR(chs_sector), %cx // Sector and cylinder
mov BIOS_PTR(disk_drive), %dx // Drive and head
mov $DISK_BUFFER, %bx
int $0x13
jnc disk_success
/*
* If both CHS and LBA fail, the error is fatal.
*/
fatal_error:
mov $BIOS_PTR(disk_error_str), %si
call print_str
cli
hlt
disk_success:
mov $'.', %al
call print_char
/*
* Enter protected mode, so we can copy this sector to
* memory above the 1MB boundary.
*
* Note that we reset CS, DS, and ES, but we don't
* modify the stack at all.
*/
cli
lgdt BIOS_PTR(bios_gdt_desc)
movl %cr0, %eax
orl $1, %eax
movl %eax, %cr0
ljmp $BOOT_CODE_SEG, $BIOS_PTR(copy_enter32)
.code32
copy_enter32:
movw $BOOT_DATA_SEG, %ax
movw %ax, %ds
movw %ax, %es
/*
* Copy the buffer to high memory.
*/
mov $DISK_BUFFER, %esi
mov BIOS_PTR(dest_address), %edi
mov $(DISK_BUFFER_SIZE / 4), %ecx
rep movsl
/*
* Next...
*
* Even though the CHS and LBA addresses are mutually exclusive,
* there's no harm in incrementing them both. The LBA increment
* is pretty straightforward, but CHS is of course less so.
* We only support CHS on 1.44MB floppies. We always copy one
* head at a time (SECTORS_AT_A_TIME must equal 18), so we have
* to hop between disk head 0 and 1, and increment the cylinder
* on every other head.
*
* When we're done copying, branch to entry32 while we're
* still in protected mode. Also note that we do a long branch
* to its final address, not it's temporary BIOS_PTR() address.
*/
addl $DISK_BUFFER_SIZE, BIOS_PTR(dest_address)
addl $SECTORS_AT_A_TIME, BIOS_PTR(dap_sector)
xorb $1, BIOS_PTR(chs_head)
jnz same_cylinder
incb BIOS_PTR(chs_cylinder)
same_cylinder:
cmpl $_edata, BIOS_PTR(dest_address)
jl not_done_copying
ljmp $BOOT_CODE_SEG, $entry32
not_done_copying:
/*
* Back to 16-bit mode for the next copy.
*
* To understand this code, it's important to know the difference
* between how segment registers are treated in protected-mode and
* in real-mode. Loading a segment register in PM is actually a
* request for the processor to fill the hidden portion of that
* segment register with data from the GDT. When we switch to
* real-mode, the segment registers change meaning (now they're
* paragraph offsets again) but that hidden portion of the
* register remains set.
*/
/* 1. Load protected-mode segment registers (CS, DS, ES) */
movw $BOOT_DATA16_SEG, %ax
movw %ax, %ds
movw %ax, %es
ljmp $BOOT_CODE16_SEG, $BIOS_PTR(copy_enter16)
/* (We're entering a 16-bit code segment now) */
.code16
copy_enter16:
/* 2. Disable protected mode */
movl %cr0, %eax
andl $(~1), %eax
movl %eax, %cr0
/*
* 3. Load real-mode segment registers. (CS, DS, ES)
*/
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
ljmp $0, $BIOS_PTR(disk_copy_loop)
/*
* print_char --
*
* Use the BIOS's TTY emulation to output one character, from %al.
*/
.code16
print_char:
mov $0x0E, %ah
mov $0x0001, %bx
int $0x10
ret_label:
ret
/*
* print_str --
*
* Print a NUL-terminated string, starting at %si.
*/
.code16
print_str:
lodsb
test %al, %al
jz ret_label
call print_char
jmp print_str
/*
* entry32 --
*
* Main 32-bit entry point. To be here, we require that:
*
* - We're running in protected mode
* - The A20 gate is enabled
* - The entire image is loaded at _start
*
* We jump directly here from GNU Multiboot loaders (like
* GRUB), and this is where we jump directly from our
* protected mode disk block copy routine after we've copied
* the lask block.
*
* We still need to set up our final stack and GDT.
*/
.code32
entry32:
cli
lgdt boot_gdt_desc
movl %cr0, %eax
orl $1, %eax
movl %eax, %cr0
ljmp $BOOT_CODE_SEG, $entry32_gdt_done
entry32_gdt_done:
movw $BOOT_DATA_SEG, %ax
movw %ax, %ds
movw %ax, %ss
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
mov $_stack, %esp
/*
* Zero out the BSS segment.
*/
xor %eax, %eax
mov $_bss_size, %ecx
mov $_edata, %edi
rep stosb
/*
* Set our LDT segment as the current LDT.
*/
mov $BOOT_LDT_SEG, %ax
lldt %ax
/*
* Call main().
*
* If it returns, put the machine in a halt loop. We don't
* disable interrupts: if the main program is in fact done
* with, but the application is still doing useful work in its
* interrupt handlers, no reason to stop them.
*/
call main
halt_loop:
hlt
jmp halt_loop
/*
* boot_gdt --
*
* This is a Global Descriptor Table that gives us a
* code and data segment, with a flat memory model.
*
* See section 3.4.5 of the Intel IA32 software developer's manual.
*/
.code32
.p2align 3
boot_gdt:
/*
* This is BOOT_NULL_SEG, the unusable segment zero.
* Reuse this memory as bios_gdt_desc, a GDT descriptor
* which uses our pre-relocation (BIOS_PTR) GDT address.
*/
bios_gdt_desc:
.word (boot_gdt_end - boot_gdt - 1)
.long BIOS_PTR(boot_gdt)
.word 0 // Unused
.word 0xFFFF, 0x0000 // BOOT_CODE_SEG
.byte 0x00, 0x9A, 0xCF, 0x00
.word 0xFFFF, 0x0000 // BOOT_DATA_SEG
.byte 0x00, 0x92, 0xCF, 0x00
.word 0xFFFF, 0x0000 // BOOT_CODE16_SEG
.byte 0x00, 0x9A, 0x00, 0x00
.word 0xFFFF, 0x0000 // BOOT_DATA16_SEG
.byte 0x00, 0x92, 0x00, 0x00
.word 0xFFFF // BOOT_LDT_SEG
.byte _ldt_byte0
.byte _ldt_byte1
.byte _ldt_byte2
.byte 0x82, 0x40
.byte _ldt_byte3
boot_gdt_end:
boot_gdt_desc: // Uses final address
.word (boot_gdt_end - boot_gdt - 1)
.long boot_gdt
/*
* dap_buffer --
*
* The Disk Address Packet buffer holds the current LBA
* disk address. We pass this to BIOS INT 13h, and we
* statically initialize it here.
*
* Note that the DAP is only used in LBA mode, not CHS mode.
*
* References:
* http://en.wikipedia.org/wiki/INT_13
* #INT_13h_AH.3D42h:_Extended_Read_Sectors_From_Drive
*/
dap_buffer:
.byte 0x10 // DAP structure size
.byte 0x00 // (Unused)
.byte SECTORS_AT_A_TIME // Number of sectors to read
.byte 0x00 // (Unused)
.word DISK_BUFFER // Buffer offset
.word 0x00 // Buffer segment
dap_sector:
.long 0x00000000 // Disk sector number
.long 0x00000000
/*
* Statically initialized disk addressing variables. The CHS
* address here is only used in CHS mode, not LBA mode, but
* the disk drive number and dest address are always used.
*/
chs_sector: // Order matters. Cylinder/sector and head/drive
.byte 0x01 // are packed into words together.
chs_cylinder:
.byte 0x00
disk_drive:
.byte 0x00
chs_head:
.byte 0x00
dest_address:
.long _start // Initial dest address for 16-to-32-bit copy.
/*
* Partition table and Boot Signature --
*
* This must be at the end of the first 512-byte disk
* sector. The partition table marks the end of the
* portion of this binary which is loaded by the BIOS.
*
* Each partition record is 16 bytes.
*
* After installing Metalkit, a disk can be partitioned as
* long as the space used by the Metalkit binary itself is
* reserved. By default, we create a single "Non-FS data"
* partition which holds the Metalkit binary. Note that
* this default partition starts at sector 1 (the first
* sector) so it covers the entire Metalkit image including
* bootloader.
*
* Partitions 2 through 4 are unused, and must be all zero
* or fdisk will complain.
*
* References:
* http://en.wikipedia.org/wiki/Master_boot_record
*/
.org 0x1BE // Partition 1
boot_partition_table:
.byte 0x80 // Status (Bootable)
.byte 0x00 // First block (head, sector/cylinder, cylinder)
.byte 0x01
.byte 0x00
.byte 0xda // Partition type ("Non-FS data" in fdisk)
.byte _partition_chs_head // Last block (head, sector/cylinder, cylinder)
.byte _partition_chs_sector_byte
.byte _partition_chs_cylinder_byte
.long 0 // LBA of first sector
.long _partition_blocks // Number of blocks in partition
.org 0x1CE // Partition 2 (Unused)
.org 0x1DE // Partition 3 (Unused)
.org 0x1EE // Partition 4 (Unused)
.org 0x1FE // Boot signature
.byte 0x55, 0xAA // This marks the end of the 512-byte MBR.