add stage 0 for BIOS boot
This commit is contained in:
parent
ed4c57a773
commit
ecf4c94da4
2 changed files with 831 additions and 0 deletions
207
build.rs
Normal file
207
build.rs
Normal file
|
@ -0,0 +1,207 @@
|
|||
use std::{env, ffi::OsStr, io, io::Write, path::Path, process::Command};
|
||||
|
||||
// We have to build stage 0 completely separately because it won't be part of
|
||||
// the final binary per se (it's what loads the final binary in the first place).
|
||||
// That's why we have this not-at-all overengineered custom build system.
|
||||
|
||||
fn main() {
|
||||
let make = Make::from_env();
|
||||
|
||||
let stage0_srcs = ["src/boot/stage0.s"];
|
||||
let stage0_objs = make.all(stage0_srcs);
|
||||
make.link(Some("config/stage0.ld"), stage0_objs, "stage0.elf");
|
||||
make.objcopy("stage0.elf", "mbr.bin");
|
||||
|
||||
make.end();
|
||||
}
|
||||
|
||||
struct Make {
|
||||
/// Base directory for all source files
|
||||
src_base: String,
|
||||
/// Base directory for all generated object files
|
||||
obj_base: String,
|
||||
/// Base directory for finished, pre-linked binaries
|
||||
root_dir: String,
|
||||
/// Command to use for assembling
|
||||
cmd_as: String,
|
||||
/// Command to use for linking
|
||||
cmd_ld: String,
|
||||
}
|
||||
|
||||
impl Make {
|
||||
pub fn from_env() -> Self {
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
// XXX this will probably be removed in the final version, i'm only throwing
|
||||
// the finished binaries in the target root directory because that's easier
|
||||
// to access from gdb and various helper scripts to set up a disk image
|
||||
let root_dir = Path::new(&out_dir)
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.and_then(Path::parent)
|
||||
.and_then(Path::to_str)
|
||||
.map(String::from)
|
||||
.unwrap();
|
||||
|
||||
eprintln!("=== BussyBuild starting ===");
|
||||
|
||||
Self {
|
||||
src_base: manifest_dir,
|
||||
obj_base: out_dir,
|
||||
root_dir,
|
||||
cmd_as: env::var("AS").unwrap_or_else(|_| String::from("as")),
|
||||
cmd_ld: env::var("LD")
|
||||
.or_else(|_| env::var("RUSTC_LINKER"))
|
||||
.unwrap_or_else(|_| "ld".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the specified source file (relative to `CARGO_MANIFEST_DIR`)
|
||||
/// and return the absolute path to the object file.
|
||||
/// The program used for building depends on the file extension:
|
||||
///
|
||||
/// | extension | program |
|
||||
/// |:---------:|:-----------|
|
||||
/// | `*.c` | C compiler |
|
||||
/// | `*.s` | assembler |
|
||||
pub fn make(&self, src_file: &str) -> String {
|
||||
assert!(Path::new(src_file).is_relative());
|
||||
|
||||
let src_path = Path::new(&self.src_base).join(src_file);
|
||||
let obj_path = Path::new(&self.obj_base).join(format!("{src_file}.o"));
|
||||
|
||||
println!("cargo:rerun-if-changed={}", src_path.to_str().unwrap());
|
||||
|
||||
let obj_dir = obj_path.parent().expect("output file has no parent??");
|
||||
if !obj_dir.exists() {
|
||||
std::fs::create_dir_all(obj_dir).unwrap();
|
||||
}
|
||||
if !obj_dir.is_dir() {
|
||||
panic!("invalid output path");
|
||||
}
|
||||
|
||||
let extension = src_path
|
||||
.extension()
|
||||
.expect("source file has no extension")
|
||||
.to_str()
|
||||
.expect("invalid (non-Unicode?) source file extension");
|
||||
|
||||
match extension {
|
||||
"c" => unimplemented!(),
|
||||
"s" => self.assemble(src_path.as_os_str(), obj_path.as_os_str()),
|
||||
_ => panic!("unsupported source file extension \".{extension}\""),
|
||||
}
|
||||
|
||||
String::from(obj_path.to_str().unwrap())
|
||||
}
|
||||
|
||||
pub fn all<I, S>(&self, src_files: I) -> Vec<String>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
src_files
|
||||
.into_iter()
|
||||
.map(|f| self.make(f.as_ref()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn link<I, S>(&self, script: Option<&str>, obj_files: I, out: &str)
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
eprintln!("=> LD {out}");
|
||||
|
||||
let mut cmd = Command::new(&self.cmd_ld);
|
||||
|
||||
if let Some(script) = script {
|
||||
let script_path = Path::new(script);
|
||||
let script_path = if script_path.is_relative() {
|
||||
Path::new(&self.src_base).join(script_path)
|
||||
} else {
|
||||
script_path.to_path_buf()
|
||||
};
|
||||
let script = script_path.as_os_str().to_str().unwrap();
|
||||
cmd.arg(format!("-T{script}"));
|
||||
println!("cargo:rerun-if-changed={script}");
|
||||
}
|
||||
|
||||
cmd.arg("-melf_i386")
|
||||
.arg("-static")
|
||||
.arg("-nostdlib")
|
||||
.arg("-o")
|
||||
.arg(Path::new(&self.root_dir).join(out));
|
||||
|
||||
for file in obj_files.into_iter() {
|
||||
cmd.arg(file.as_ref());
|
||||
}
|
||||
|
||||
eprintln!(" {cmd:?}");
|
||||
|
||||
let output = cmd.output().expect("failed to execute linker");
|
||||
self.stdout_raw(output.stdout.as_ref());
|
||||
if !output.status.success() {
|
||||
self.stdout_raw(output.stderr.as_ref());
|
||||
self.cmd_failed(&self.cmd_ld, output.status.code());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn objcopy(&self, elf: &str, out: &str) {
|
||||
eprintln!("=> OBJCOPY {elf}");
|
||||
let elf = Path::new(&self.root_dir).join(elf);
|
||||
let bin = Path::new(&self.root_dir).join(out);
|
||||
let output = Command::new("objcopy")
|
||||
.arg("-O")
|
||||
.arg("binary")
|
||||
.arg(&elf)
|
||||
.arg(&bin)
|
||||
.output()
|
||||
.unwrap();
|
||||
self.stdout_raw(output.stdout.as_ref());
|
||||
if !output.status.success() {
|
||||
self.stdout_raw(output.stderr.as_ref());
|
||||
self.cmd_failed(&self.cmd_ld, output.status.code());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(self) {
|
||||
eprintln!("=== BussyBuild finished ===");
|
||||
}
|
||||
|
||||
/// Assemble a source file to an object file.
|
||||
fn assemble(&self, src_file: &OsStr, obj_file: &OsStr) {
|
||||
eprintln!("=> AS {}", obj_file.to_str().unwrap());
|
||||
|
||||
let mut cmd = Command::new(&self.cmd_as);
|
||||
cmd.arg("--32")
|
||||
// Without this option, the linker mysteriously appends exactly 40 bytes
|
||||
// of nonsense to the final output binary, even after stripping the ELF.
|
||||
// I have no explanation for this, other than the GNU people being sadists.
|
||||
.arg("-mx86-used-note=no")
|
||||
.arg("-g")
|
||||
.arg("-o")
|
||||
.arg(obj_file)
|
||||
.arg(src_file);
|
||||
|
||||
eprintln!(" {cmd:?}");
|
||||
|
||||
let output = cmd.output().expect("failed to execute assembler");
|
||||
self.stdout_raw(output.stdout.as_ref());
|
||||
if !output.status.success() {
|
||||
self.stdout_raw(output.stderr.as_ref());
|
||||
self.cmd_failed(&self.cmd_as, output.status.code());
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_failed(&self, cmd: &str, code: Option<i32>) -> ! {
|
||||
let code = code
|
||||
.map(|code| format!("{code}"))
|
||||
.unwrap_or_else(|| "<unknown>".to_owned());
|
||||
panic!("{cmd} exited with status code {code}",)
|
||||
}
|
||||
|
||||
fn stdout_raw(&self, buf: &[u8]) {
|
||||
io::stdout().write_all(buf).unwrap();
|
||||
}
|
||||
}
|
624
src/boot/stage0.s
Normal file
624
src/boot/stage0.s
Normal file
|
@ -0,0 +1,624 @@
|
|||
/*
|
||||
* fef
|
||||
*
|
||||
* S T A G E Z E R O
|
||||
*
|
||||
* a dystopian novel
|
||||
*/
|
||||
|
||||
/*
|
||||
* PREAMBLE
|
||||
*
|
||||
* This place is a message ...
|
||||
* and part of a system of messages ...
|
||||
* pay attention to it!
|
||||
*
|
||||
* Sending this message was important to us.
|
||||
* We considered ourselves to be a powerful culture.
|
||||
*
|
||||
* This place is not a place of honor ...
|
||||
* no highly esteemed deed is commemorated here ...
|
||||
* nothing valued is here.
|
||||
*
|
||||
* What is here was dangerous and repulsive to us.
|
||||
* This message is a warning about danger.
|
||||
*
|
||||
* The danger is still present, in your time, as it was in ours.
|
||||
*
|
||||
* This place is best shunned and left uninhabited.
|
||||
*/
|
||||
|
||||
/*
|
||||
* PROLOGUE
|
||||
*
|
||||
* Shitposting aside (and ignoring the fact that this entire file is a shitpost
|
||||
* to begin with), bootsectors are kind of awkward because we have to squeeze
|
||||
* quite a lot of stuff into a lousy 440 bytes.
|
||||
* In the case of bussy, we do ALL of the following, in that order:
|
||||
*
|
||||
* 1. relocate ourselves to the end of low memory (0x7800:7c00 = 0x7fc00)
|
||||
* 2. do some rudimentary sanity checks
|
||||
* 3. read the boot drive's GPT header and validate its magic and CRC
|
||||
* 4. read the boot drive's GPT and validate its CRC
|
||||
* 5. search the GPT for the partition containing stage1
|
||||
* 6. copy stage1 to the beginning of low memory (0x0000:0500 = 0x00500)
|
||||
* 7. validate stage1's magic number and CRC
|
||||
* 8. jump to the stage 1 entry point
|
||||
*
|
||||
* What's worse, all of that has to happen in Real Mode x86 assembly. Ugh.
|
||||
* And account for all the stupid BIOS bugs since the 80s. Double ugh.
|
||||
*
|
||||
* These constraints force us to resort to some advanced space saving techniques
|
||||
* because literally every single byte counts. As such, we reuse register
|
||||
* values and side effects, replace instructions that take up multiple bytes
|
||||
* with smaller ones, combine multiple operations into one, reorder code to be
|
||||
* as compact as humanly possible, and so much more. At the very least, any CPU
|
||||
* manufactured in this millennium will easily crunch through all of this within
|
||||
* a matter of milliseconds at most, so performance is a non-issue.
|
||||
*
|
||||
* I tried to keep this code itself location-agnostic and only work with
|
||||
* addresses defined in the linker script, but there are several places where
|
||||
* that's just not possible. And since this is in no way useful other than the
|
||||
* highly specific IBM PC environment, i figured it doesn't really matter if i
|
||||
* hardcode some stuff. Sorry :)
|
||||
*
|
||||
* Keeping track of the stack can become rather difficult, because some values
|
||||
* stay there for quite a while and there is NO %bp (see "error_slide").
|
||||
* Therefore, i annotated all stack transactions with "lifetimes" in a separate
|
||||
* comment towards the far end of the line. The number indicates the current
|
||||
* stack height AFTER a push and BEFORE a pop. The `v` and `^` are "arrows"
|
||||
* that denote whether it is a push or pop respectively:
|
||||
* (1) they indicate the direction in which you have to scroll to get to the
|
||||
* corresponding counterpart to the instruction
|
||||
* (2) if you imagine memory as some sort of pillar where addresses grow in the
|
||||
* upward direction, the "arrows" point in the direction that %sp moves
|
||||
* (3) if you imagine a physical stack of things, the "arrows" match the
|
||||
* direction of putting things on the stack (down) or lifting them off (up)
|
||||
*
|
||||
* Note that this MBR is also used for booting from ZFS drives (when ZFS is
|
||||
* given control of the entire drive, rather than just a partition) because the
|
||||
* ZFS folks had enough foresight to reserve the first couple of sectors, and
|
||||
* OpenZFS is even nice enough to write a GPT to them when creating a pool.
|
||||
* That table contains two entries; one for the ZFS stuff itself and one for
|
||||
* 8 MiB of reserved space that ZFS appears to just leave alone and not touch
|
||||
* at all. 8 MiB are way more than we'll ever need,
|
||||
*/
|
||||
|
||||
.code16
|
||||
|
||||
.macro LOCAL name, type=function
|
||||
.type \name , %\type
|
||||
\name :
|
||||
.endm
|
||||
|
||||
.macro GLOBL name, type
|
||||
.global \name
|
||||
LOCAL \name , \type
|
||||
.endm
|
||||
|
||||
.macro END name
|
||||
.size \name , . - \name
|
||||
.endm
|
||||
|
||||
/*
|
||||
* CHAPTER 1
|
||||
*
|
||||
* "Bootstrap Routine"
|
||||
*
|
||||
* 0x7c00
|
||||
*/
|
||||
|
||||
.text
|
||||
|
||||
GLOBL _start
|
||||
cli
|
||||
cld
|
||||
|
||||
/*
|
||||
* Step 1: Relocate to 0x7fc00
|
||||
*
|
||||
* Since we have to use segments to access anything above 0xffff anyway,
|
||||
* this binary is statically linked against the physical load address of
|
||||
* 0x7c00. That's not too bad though, because we have to initialize all
|
||||
* segment registers one way or the other (can't rely on the BIOS doing
|
||||
* that for us, especially because some of them appear to have this fun
|
||||
* quirk where they jump to 0x07c0:0000 instead of 0x0000:7c00).
|
||||
*
|
||||
* This assumes that the entire memory used by stage0 (including .bss
|
||||
* and stack) is <= 0x400 bytes in size, which is a pretty conservative
|
||||
* guess. In practice, it's probably more like 0x300 at most.
|
||||
*/
|
||||
|
||||
xor %cx, %cx
|
||||
mov %cx, %ds
|
||||
mov $0x7800, %bx /* (0x7fc00 - 0x7c00) >> 4 */
|
||||
mov %bx, %es
|
||||
/* %di should really be 0x7c00 (we're setting it to 0x7800 here), but a
|
||||
* `mov` between two registers is 1 byte smaller than one with an imm16 */
|
||||
mov %bx, %di
|
||||
mov $3, %ch /* 0x300 words (0x7800 to 0x7e00) */
|
||||
1: mov (%di), %ax /* use %ds (0x0000) for reading */
|
||||
stosw /* use %es (0x7800) for writing */
|
||||
loop 1b
|
||||
/* this loop ends with 0xaa55 (MBR magic) in %ax, which we reuse later */
|
||||
mov %bx, %ds
|
||||
mov %bx, %ss
|
||||
/* XXX gdb gets a stroke when we relocate ourselves */
|
||||
ljmp $0x7800, $2f
|
||||
|
||||
/*
|
||||
* Step 2: Rudimentary sanity checks
|
||||
*/
|
||||
|
||||
/* Clear .bss and initialize %sp to the end of low memory. The previous
|
||||
* loop ended with %di = 0x7e00, %cx = 0x0000, and %bx = 0x7800.
|
||||
* `xchg %ax, r16` takes up only 1 byte as opposed to `xor r16, r16`
|
||||
* which takes 2. It also moves the 0xaa55 to %bx, where we need it. */
|
||||
2: xchg %ax, %bx
|
||||
mov $2, %ch /* 0x200 bytes (0x7e00 to 0x8000) */
|
||||
rep stosb
|
||||
mov %di, %sp /* %sp = 0x7800:8000 = 0x80000 */
|
||||
|
||||
/* say hello */
|
||||
mov $msg_loader_info, %si
|
||||
call print
|
||||
|
||||
/* we kinda abuse %bp, see the "error slide" below */
|
||||
mov $'1', %bp
|
||||
|
||||
/* check and save our boot drive number */
|
||||
test $0x80, %dl /* only accept drives 0x80-0xff */
|
||||
jz err_bad_boot_drive
|
||||
|
||||
/* check whether the BIOS supports IBM int13 extensions */
|
||||
mov $0x41, %ah
|
||||
not %bx /* %bx = 0x55aa */
|
||||
int $0x13
|
||||
mov $0, %dh /* now is the perfect opportunity to clear %dh */
|
||||
jc err_no_int13_extensions
|
||||
xor $0xaa55, %bx /* clear %bx on success */
|
||||
jnz err_bad_boot_drive
|
||||
|
||||
/*
|
||||
* Step 3: Read and validate the boot drive's GPT header
|
||||
*/
|
||||
|
||||
/* get the boot drive's logical sector size */
|
||||
mov $0x48, %ah
|
||||
mov $drive_params, %si
|
||||
movb $(drive_params_end - drive_params), (%si)
|
||||
int $0x13 /* int13/48 clears %ah and CF on success */
|
||||
mov $0, %al
|
||||
adc %ax, %ax
|
||||
jnz err_bad_boot_drive
|
||||
|
||||
/* read LBA 1 (where the GPT header is supposed to be) */
|
||||
mov $0x50, %al /* we just checked that %ah is 0 */
|
||||
mov %ax, %fs
|
||||
xor %eax, %eax
|
||||
xor %di, %di
|
||||
inc %ax /* %eax = 1 */
|
||||
mov %ax, %cx /* %cx = 1 */
|
||||
call read_lba /* LBA = 0x00000001, dest = 0x0050:0000 */
|
||||
|
||||
/*
|
||||
* Step 4: validate the GPT's magic number and CRCs
|
||||
*/
|
||||
|
||||
/* we'll work with %fs quite a lot over the next lines, so much in fact
|
||||
* that copying it to %ds and saving the segment overrides is worth it */
|
||||
push %fs
|
||||
pop %ds
|
||||
|
||||
/* check if the GPT signature is correct (%di points to sector) */
|
||||
xor %si, %si
|
||||
mov $gpt_magic, %di
|
||||
mov $(gpt_magic_end - gpt_magic), %cl /* %cx was 1 */
|
||||
repe cmpsb %es:(%di), %ds:(%si)
|
||||
jne err_no_gpt
|
||||
|
||||
dec %ax /* %eax = 0 */
|
||||
xchg 0x08(%si), %eax /* load CRC and replace it with zeroes */
|
||||
push %ax /* save CRC[15:0] */ /* '1 v */
|
||||
xor %si, %si
|
||||
mov $0x5c, %cl
|
||||
call crc32
|
||||
pop %ax /* restore CRC[15:0] */ /* '1 ^ */
|
||||
test %eax, %ebx
|
||||
jnz err_bad_gpt_csum
|
||||
|
||||
/* store everything we need to rember from the GPT header (we assert there
|
||||
* to be no more than 65535 partitions and that each entry is no more than
|
||||
* 65535 bytes, because if that's not the case we're screwed anyway) */
|
||||
mov $0x0048, %si
|
||||
lods (%si), %eax /* first LBA of partition table */
|
||||
mov (%si), %bx /* first LBA of partition table, high bits */
|
||||
mov 0x04(%si), %cx /* number of partition entries */
|
||||
mov 0x0c(%si), %edi /* CRC32 of the partition entry array */
|
||||
mov 0x08(%si), %si /* size of each partition entry in bytes */
|
||||
|
||||
/* restore %ds to 0x7800 */
|
||||
push %es
|
||||
pop %ds
|
||||
|
||||
/* Calculate how many sectors we have to read for the partition table.
|
||||
* This assumes that the entire partition table is less than 64 KiB. */
|
||||
xchg %ax, %si /* %ax = entry size, %si = LBA[15:0] */
|
||||
push %ax /* save entry size */ /* '1 v */
|
||||
push %di /* save crc[15:0] */ /* '2 v */
|
||||
push %dx /* save drive number */ /* '3 v */
|
||||
mul %cx /* %dx:%ax = entry size * entry count */
|
||||
jno start2 /* fail if %dx != 0 (`mul` overflowed %ax) */
|
||||
|
||||
/* pardon the sudden interruption; we have to insert the error slide
|
||||
* here because that way we maximize the number of jumps with 1-byte
|
||||
* relative addresses, which saves a couple of bytes here and there */
|
||||
END _start
|
||||
|
||||
/*
|
||||
* Welcome to the "error slide"!
|
||||
* This ... thing relies on the fact that we don't quite use %bp for
|
||||
* its intended purpose and instead always keep it at 0x31 (ASCII '1').
|
||||
* The reason for this is that errors require a jump anyway, which
|
||||
* takes up at least 2 bytes, while `inc %bp` requires only 1 byte.
|
||||
* Therefore, we quite literally "encode" the error code through the
|
||||
* jump instruction by means of jumping to the respective offset.
|
||||
*/
|
||||
LOCAL error_slide
|
||||
/* err_gpt_too_big is carefully placed so that the `jno` above falls
|
||||
* through exactly here on failure, avoiding an unconditional jump */
|
||||
err_gpt_too_big:
|
||||
inc %bp
|
||||
err_bad_stage1_csum:
|
||||
inc %bp
|
||||
err_bad_stage1_magic:
|
||||
inc %bp
|
||||
err_bad_boot_drive:
|
||||
inc %bp
|
||||
err_no_int13_extensions:
|
||||
inc %bp
|
||||
err_bad_gpt_csum:
|
||||
inc %bp
|
||||
err_no_gpt:
|
||||
inc %bp
|
||||
err_read_failed:
|
||||
inc %bp
|
||||
err_no_stage1:
|
||||
mov $msg_error, %si
|
||||
/* 6 is the offset of the error number character within the error
|
||||
* message, see the definition of msg_error below. We use %es
|
||||
* because that's the only segment always pointing to 0x7800. */
|
||||
or %bp, %es:6(%si)
|
||||
call print
|
||||
|
||||
/*
|
||||
* The GRUB uses int18 ("diskless boot hook" aka "start cassette BASIC")
|
||||
* in its error routine, so we do the same. Issuing this interupt does
|
||||
* any of the following (see <http://www.ctyme.com/intr/rb-2241.htm>):
|
||||
* - start BASIC from the integrated ROM (on the OG IBM PC)
|
||||
* - reboot the system
|
||||
* - display some kind of error message
|
||||
* - attempt a network boot
|
||||
* - try some other boot device
|
||||
* - nothing lmao
|
||||
*/
|
||||
int $0x18
|
||||
|
||||
/* if we make it here, it's over for good */
|
||||
die: cli
|
||||
hlt
|
||||
jmp die
|
||||
END error_slide
|
||||
|
||||
LOCAL start2
|
||||
mov %ax, %di /* %di = table size in bytes */
|
||||
divw (drive_params + 0x18) /* %ax = table size / sector size */
|
||||
test %dx, %dx /* check remainder */
|
||||
jz 1f
|
||||
/* this increment can't overflow because we already checked that the
|
||||
* dividend is <= 0xffff, so unless we're dealing with a 1-byte sector
|
||||
* disk we can't possibly get this anywhere near the 16-bit limit */
|
||||
inc %ax /* round up if modulus > 0 */
|
||||
1: pop %dx /* restore drive number */ /* '3 ^ */
|
||||
xchg %ax, %cx /* %ax = entry count, %cx = sector count */
|
||||
xchg %ax, %si /* %ax = LBA[15:0], %si = entry count */
|
||||
push %di /* save table size in bytes */ /* '3 v */
|
||||
xor %di, %di
|
||||
call read_lba
|
||||
|
||||
/* check the partition table array's CRC */
|
||||
pop %cx /* restore table size in bytes */ /* '3 ^ */
|
||||
push %si /* save entry count */ /* '3 v */
|
||||
xor %si, %si
|
||||
call crc32
|
||||
pop %cx /* restore entry count */ /* '3 ^ */
|
||||
pop %di /* restore CRC[15:0] */ /* '2 ^ */
|
||||
and %ebx, %edi /* %edi = 0 (crc32 returns the inverted value) */
|
||||
jnz err_bad_gpt_csum
|
||||
|
||||
/* search the partition table for our GUID */
|
||||
pop %bx /* restore entry size */ /* '1 ^ */
|
||||
sub $(bussy_guid_end - bussy_guid), %bx /* entry size -= sizeof(bussy_guid) */
|
||||
/* this loop uses %ax for temporarily storing `entry count` and
|
||||
* `sizeof(bussy_guid)` because xchg with %ax is only one byte */
|
||||
1: push %cx /* save remaining entry count */ /* '1 v */
|
||||
mov $bussy_guid, %si
|
||||
mov $(bussy_guid_end - bussy_guid), %cx
|
||||
repe cmpsb %es:(%di), %fs:(%si)
|
||||
pop %cx /* restore remaining entry count */ /* '1 ^ */
|
||||
je 2f /* found it */
|
||||
add %bx, %di /* %di += entry size */
|
||||
loopne 1b /* only loop if %di didn't wrap around */
|
||||
jmp err_no_stage1
|
||||
|
||||
/* gotcha! now load the first 64 K from the partition */
|
||||
2: push %dx /* save drive number */ /* '1 v */
|
||||
xor %ax, %ax
|
||||
mov $1, %dl /* %dh is known to be 0x00 */
|
||||
divw (drive_params + 0x18) /* %ax = 0x10000 / sector size = sector count */
|
||||
xchg %ax, %cx /* %cx = sector count (xchg saves 1 byte) */
|
||||
mov %fs:0x20(%di), %eax /* %eax = LBA[31:0] */
|
||||
mov %fs:0x24(%di), %bx /* %bx = LBA[47:32] */
|
||||
xor %di, %di
|
||||
pop %dx /* restore drive number */ /* '1 ^ */
|
||||
call read_lba
|
||||
|
||||
/* check the stage1 header */
|
||||
lods %fs:(%di), %ax /* offset 0x00: magic number */
|
||||
cmp $0xacab, %ax
|
||||
jne err_bad_stage1_magic
|
||||
lods %fs:(%di), %ax /* offset 0x02: byte count for CRC */
|
||||
xchg %cx, %ax
|
||||
xor %eax, %eax
|
||||
xchg %fs:(%di), %eax /* offset 0x04: CRC (replace with 0) */
|
||||
push %ax /* save CRC[15:0] (crc32 clobbers %al) */ /* '1 v */
|
||||
call crc32
|
||||
pop %ax /* restore CRC[15:0] */ /* '1 ^ */
|
||||
and %ebx, %eax /* check CRC and clear %eax */
|
||||
jnz err_bad_stage1_csum
|
||||
|
||||
/*
|
||||
* Step 8: Jump to stage1 (finally)
|
||||
*/
|
||||
|
||||
ljmp $0x0000, $0x0508 /* entry point is right after the 8-byte header */
|
||||
END start2
|
||||
|
||||
/*
|
||||
* CHAPTER 2
|
||||
*
|
||||
* "Utility Subroutines"
|
||||
*
|
||||
* 0x7c00 + N
|
||||
*/
|
||||
|
||||
/*
|
||||
* Load %cl sectors, starting at LBA %bx:%eax, from disk %dl to %fs:%di.
|
||||
* This subroutine preserves all registers except FLAGS,
|
||||
* and automatically jumps to err_read_failed on failures.
|
||||
*
|
||||
* %eax: logical block address (sector number), bits 31:0
|
||||
* %bx: logical block address (sector number), bits 47:32
|
||||
* %cx: # of sectors to read (should be < 128)
|
||||
* %dl: disk number
|
||||
* %di: destination
|
||||
* %fs: destination segment
|
||||
*/
|
||||
LOCAL read_lba
|
||||
pusha
|
||||
|
||||
mov $dap, %si
|
||||
movb $(dap_end - dap), (%si)
|
||||
mov %cx, 2(%si)
|
||||
mov %di, 4(%si)
|
||||
mov %fs, %cx
|
||||
mov %cx, 6(%si)
|
||||
movl %eax, 8(%si)
|
||||
mov %bx, 12(%si)
|
||||
mov $0x42, %ah
|
||||
int $0x13
|
||||
/* You're supposed to retry several times if this fails because
|
||||
* *floppies* tend to be quite unreliable. We just assume that
|
||||
* nobody in their right mind would boot off of a floppy anymore
|
||||
* unless they're into retro tech, in which case they wouldn't use
|
||||
* a modern bootloader on their machine to begin with. HDDs/SSDs
|
||||
* should be reliable enough that we can assume it always works. */
|
||||
mov $0, %al
|
||||
adc %ax, %ax /* CF and %ah are 0 on success */
|
||||
jnz err_read_failed
|
||||
|
||||
popa
|
||||
ret
|
||||
END read_lba
|
||||
|
||||
/*
|
||||
* Do a CCITT32 ANSI CRC (polynomial 0x04c11db7) of %cx bytes at %fs:%si
|
||||
* and return THE COMPLEMENT (i.e. bitwise NOT of the CRC) in %ebx.
|
||||
* Clobbers %al, %cx, and %si. Does not check for overflows (%si + %cx
|
||||
* must be <= 0x10000). If %cx is 0, the size is 64 KiB (0x10000), in
|
||||
* which case %si should also be 0 because of the missing wrap check.
|
||||
*
|
||||
* Stolen from "Hacker's Delight", second edition by Henry S. Warren, Jr.
|
||||
* and painstakingly ported to x86 assembly with focus on minimum size.
|
||||
* I have zero clue how CRC *actually* works, so there may be room for
|
||||
* optimizations.
|
||||
*
|
||||
* %al: clobber
|
||||
* %ebx: return value (ATTENTION: COMPLEMENT of CRC)
|
||||
* %cx: byte count (clobber)
|
||||
* %si: data (clobber)
|
||||
* %fs: data segment
|
||||
*/
|
||||
LOCAL crc32
|
||||
/* this is expressed as an imm8 w/ sign extension */
|
||||
or $-1, %ebx /* %ebx = 0xffffffff */
|
||||
|
||||
1: lods %fs:(%si), %al
|
||||
xor %al, %bl
|
||||
|
||||
push %cx
|
||||
mov $8, %cx
|
||||
2: shr %ebx
|
||||
jnc 3f
|
||||
xor $0xedb88320, %ebx
|
||||
3: loop 2b
|
||||
pop %cx
|
||||
|
||||
loop 1b
|
||||
|
||||
ret
|
||||
END crc32
|
||||
|
||||
/*
|
||||
* %si: address of string (terminated with last char | 0x80)
|
||||
* %es: segment of string
|
||||
*/
|
||||
LOCAL print
|
||||
/* according to <http://www.ctyme.com/intr/rb-0106.htm>, some BIOSes
|
||||
* destroy %bp when the write causes the screen to scroll (???) */
|
||||
pusha
|
||||
|
||||
1: lods %es:(%si), %al
|
||||
/* Strings are ASCII only, so we know bit 7 (MSB) is always 0.
|
||||
* We use that as a terminator to save space (stolen from BSD). */
|
||||
btr $7, %ax
|
||||
|
||||
mov $0x0001, %bx /* page = 00, foreground color = 01 */
|
||||
mov $0x0e, %ah /* print char, teletype mode */
|
||||
int $0x10
|
||||
jnc 1b /* XXX this breaks if BIOS trashes CF */
|
||||
|
||||
popa
|
||||
ret
|
||||
END print
|
||||
|
||||
/*
|
||||
* CHAPTER 3
|
||||
*
|
||||
* "Constants and Variables"
|
||||
*
|
||||
* 0x7c00 + N + M
|
||||
*/
|
||||
|
||||
.data
|
||||
|
||||
.macro TERMINATOR char
|
||||
.byte \char | 0x80
|
||||
.endm
|
||||
|
||||
/*
|
||||
* The error_slide subroutine will replace the `\0` character with the
|
||||
* actual error code. ATTENTION: the 6-byte offset is hardcoded!
|
||||
*/
|
||||
LOCAL msg_error, object
|
||||
.ascii "error \0\r"
|
||||
TERMINATOR '\n'
|
||||
END msg_error
|
||||
|
||||
.section .rodata
|
||||
|
||||
LOCAL msg_loader_info, object
|
||||
.ascii "BUSSY"
|
||||
TERMINATOR ' '
|
||||
END msg_loader_info
|
||||
|
||||
LOCAL gpt_magic, object
|
||||
.ascii "EFI PART"
|
||||
gpt_magic_end:
|
||||
END gpt_magic
|
||||
|
||||
/* bussy boot partition GUID (61476542-4479-436f-7269-6d6521557755) */
|
||||
LOCAL bussy_guid, object
|
||||
.ascii "BeGayDoCrime!UwU"
|
||||
bussy_guid_end:
|
||||
END bussy_guid
|
||||
|
||||
/*
|
||||
* CHAPTER 4
|
||||
*
|
||||
* "MBR Header"
|
||||
*
|
||||
* 0x7db8
|
||||
*/
|
||||
|
||||
.section .header, "a", "progbits"
|
||||
|
||||
GLOBL mbr_uid, object
|
||||
.long 0xffffffff
|
||||
END mbr_uid
|
||||
|
||||
GLOBL mbr_rsvd, object
|
||||
.word 0
|
||||
END mbr_rsvd
|
||||
|
||||
/* first MBR Partition Table Entry contains the protective thingy */
|
||||
GLOBL mbr_pte1, object
|
||||
/* 0 */ .byte 0x80 /* boot indicator flag */
|
||||
/* 1 */ .byte 0x02 /* starting head */
|
||||
/* 2 */ .word 0x0002 /* starting cylinder[15:6]/sector[5:0] */
|
||||
/* 4 */ .byte 0xee /* system id (GPT protective) */
|
||||
/* 5 */ .byte 0xff /* ending head */
|
||||
/* 6 */ .word 0xffff /* ending cylinder[15:6]/sector[5:0] */
|
||||
/* 8 */ .word 0x0001 /* starting LBA, low word */
|
||||
/* a */ .word 0x0000 /* starting LBA, high word */
|
||||
/* c */ .word 0xffff /* number of sectors, low word */
|
||||
/* e */ .word 0xffff /* number of sectors, high word */
|
||||
END mbr_pte1
|
||||
|
||||
.macro PTE_EMPTY num
|
||||
GLOBL mbr_pte\num , object
|
||||
.rep 16
|
||||
.byte 0
|
||||
.endr
|
||||
END mbr_pte\num
|
||||
.endm
|
||||
|
||||
PTE_EMPTY 2
|
||||
PTE_EMPTY 3
|
||||
PTE_EMPTY 4
|
||||
|
||||
GLOBL mbr_magic, object
|
||||
.word 0xaa55
|
||||
END mbr_magic
|
||||
mbr_end:
|
||||
|
||||
/*
|
||||
* CHAPTER 5
|
||||
*
|
||||
* "Scratch and Stack Space"
|
||||
*
|
||||
* 0x7e00
|
||||
*/
|
||||
|
||||
.section .bss
|
||||
bss_start:
|
||||
|
||||
/* "Disk Address Packet" (for int13/42 "extended read") */
|
||||
.align 8
|
||||
LOCAL dap, object
|
||||
/* 0 */ .byte 0 /* size of DAP (must be initialized to 0x10) */
|
||||
/* 1 */ .byte 0 /* always 0 */
|
||||
/* 2 */ .word 0 /* number of sectors to transfer (<= 127) */
|
||||
/* 4 */ .word 0 /* destination offset (within segment, must be word aligned) */
|
||||
/* 6 */ .word 0 /* destination segment */
|
||||
/* 8 */ .quad 0 /* first LBA (48-bit) */
|
||||
dap_end:
|
||||
END dap
|
||||
|
||||
/*
|
||||
* ATTENTION: this must stay the last .bss member because apparently some
|
||||
* buggy BIOSes ignore the buffer size and write beyond the data structure
|
||||
*/
|
||||
.align 8
|
||||
LOCAL drive_params, object
|
||||
/* 00 */.word 0 /* size of buffer (must be initialized to 0x1a) */
|
||||
/* 02 */.word 0 /* information flags (must be 0) */
|
||||
/* 04 */.long 0 /* number of cylinders */
|
||||
/* 08 */.long 0 /* number of heads */
|
||||
/* 0c */.long 0 /* number of sectors per track */
|
||||
/* 10 */.quad 0 /* number of total logical sectors */
|
||||
/* 18 */.word 0 /* bytes per logical sector */
|
||||
drive_params_end:
|
||||
END drive_params
|
||||
|
||||
/* stack is initialized to 0x8000 */
|
Loading…
Reference in a new issue