587 lines
19 KiB
ArmAsm
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.
|