From a91b40089bf7b3d0cfac05cae50a881cec738299 Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Thu, 9 Apr 2020 01:32:27 -0700 Subject: [PATCH 1/3] gdt: Add definitions for 32-bit GDT Our 32-bit GDT just has 1 code and 1 data segment descriptor. They are both flat (spanning the entire 4G address space) and are placed in normal static memory (for now). Signed-off-by: Joe Richey --- src/gdt.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/gdt.rs b/src/gdt.rs index 5b64713d..5f111694 100644 --- a/src/gdt.rs +++ b/src/gdt.rs @@ -26,6 +26,14 @@ bitflags::bitflags! { const COMMON = Self::ACCESSED.bits | Self::USER_SEGMENT.bits | Self::PRESENT.bits; // BIT32 must be 0, all other bits (not yet mentioned) are ignored. const CODE64 = Self::COMMON.bits | Self::EXECUTABLE.bits | Self::BIT64.bits; + + // All 32-bit segments have base = 0, limit = 4G = (0xF_FFFF + 1)*4K + const MAX_LIMIT = Self::LIMIT_0_15.bits | Self::LIMIT_16_19.bits | Self::GRANULARITY.bits; + const COMMON32 = Self::COMMON.bits | Self::MAX_LIMIT.bits | Self::BIT32.bits; + // We set READABLE because the ROM code reads data via cs. + const CODE32 = Self::COMMON32.bits | Self::READABLE.bits | Self::EXECUTABLE.bits; + // We set WRITABLE so the ROM code can write ROM data into RAM. + const DATA32 = Self::COMMON32.bits | Self::WRITABLE.bits; } } @@ -51,3 +59,7 @@ impl Pointer { #[no_mangle] static GDT64_PTR: Pointer = Pointer::new(&GDT64); static GDT64: [Descriptor; 2] = [Descriptor::empty(), Descriptor::CODE64]; + +#[no_mangle] +static GDT32_PTR: Pointer = Pointer::new(&GDT32); +static GDT32: [Descriptor; 3] = [Descriptor::empty(), Descriptor::CODE32, Descriptor::DATA32]; From ae54aedc1392b4cff64f42862b5b35fde84c8910 Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Mon, 4 May 2020 19:03:41 -0700 Subject: [PATCH 2/3] bios: Add code to boot as Firmware ROM The new assembly files handle: - reset.s: Jumping from reset - rom16.s: Transitioning to 32-bit mode - rom32.s: Copying data from ROM to RAM, jumping to PVH entry point To place this code correctly, we add a new Program Header for the code and data that expect to be in ROM. See the comments in layout.ld for more information. We also place the 32-bit GDT in the ROM. This is mostly for convenience, as it lets us use the GDT directly from the ROM code without having to do any complex offset calculations. As laying out the code for a ROM makes the binary ~45% bigger, we gate building as a ROM behind an optional feature. Signed-off-by: Joe Richey --- Cargo.toml | 2 ++ layout.ld | 29 +++++++++++++++++++++++++++++ src/asm/mod.rs | 5 +++++ src/asm/reset.s | 10 ++++++++++ src/asm/rom16.s | 26 ++++++++++++++++++++++++++ src/asm/rom32.s | 38 ++++++++++++++++++++++++++++++++++++++ src/gdt.rs | 4 ++++ 7 files changed, 114 insertions(+) create mode 100644 src/asm/reset.s create mode 100644 src/asm/rom16.s create mode 100644 src/asm/rom32.s diff --git a/Cargo.toml b/Cargo.toml index 32382711..ec0b023d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ log-serial = [] # gets you most of the code size reduction, without losing _all_ debugging. log-panic = ["log-serial"] integration_tests = [] +# Support builing the firmware as a BIOS ROM (i.e. starting in real mode). +rom = [] [dependencies] bitflags = "1.2" diff --git a/layout.ld b/layout.ld index 7ab6066f..7ac269e7 100644 --- a/layout.ld +++ b/layout.ld @@ -4,6 +4,7 @@ PHDRS { ram PT_LOAD FILEHDR PHDRS ; note PT_NOTE ; + rom PT_LOAD ; } /* Loaders like to put stuff in low memory (< 1M), so we don't use it. */ @@ -24,6 +25,7 @@ SECTIONS .text32 : { *(.text32) } .data : { *(.data .data.*) } data_size = . - data_start; + file_size = . - ram_min; /* The BSS section isn't mapped from file data. It is just zeroed in RAM. */ .bss : { @@ -38,6 +40,33 @@ SECTIONS /* ram32.s only maps the first 2 MiB, and that must include the stack. */ ASSERT((. <= 2M), "Stack overflows initial identity-mapped memory region") + /* This is correct because all of the code sections have alignment 16. */ + rom_size = ALIGN(SIZEOF(.gdt32), 16) + ALIGN(SIZEOF(.rom32), 16) + + ALIGN(SIZEOF(.rom16), 16) + ALIGN(SIZEOF(.reset), 16); + /* Our file's length (RAM code/data + ROM) without any additional padding */ + file_size += rom_size; + /* QEMU requires the ROM size to be a multiple of 64K. To achieve this for + our `sstrip`ed binary, we insert enough padding before the ROM code, + so that the end of the ROM code falls on a 64K file bounary. */ + pad_size = ALIGN(file_size, 64K) - file_size; + + /* When using the ROM, the entire firmware is loaded right below 4 GiB. + We work backwards from the end to figure where to place the ROM code, + 32-bit GDT, padding, and the remainder of the firmware. */ + pad_start = (1 << 32) - rom_size - pad_size; + /* The remainder of the firmware code/data expects to be run at addresses + [data_start, data_start + data_size), but will initially be located in + ROM, at addresses [rom_data_start, rom_data_start + data_size). As the + padding comes right after the remainder of the firmware, we have: */ + rom_data_start = pad_start - data_size; + + /* Only insert the padding if we are building as a ROM (to save size). */ + .pad pad_start : { . += rom_size ? pad_size : 0; } :NONE + .gdt32 : { *(.gdt32) } :rom + .rom32 : { *(.rom32) } + .rom16 : { *(.rom16) } + .reset : { KEEP(*(.reset)) } + /* Strip symbols from the output binary (comment out to get symbols) */ /DISCARD/ : { *(.symtab) diff --git a/src/asm/mod.rs b/src/asm/mod.rs index 9d3ccc06..5cc78638 100644 --- a/src/asm/mod.rs +++ b/src/asm/mod.rs @@ -1 +1,6 @@ +#[cfg(feature = "rom")] +global_asm!(include_str!("reset.s")); + +global_asm!(include_str!("rom16.s")); +global_asm!(include_str!("rom32.s")); global_asm!(include_str!("ram32.s")); diff --git a/src/asm/reset.s b/src/asm/reset.s new file mode 100644 index 00000000..f6eeb2bf --- /dev/null +++ b/src/asm/reset.s @@ -0,0 +1,10 @@ +.section .reset, "ax" +.code16 + +# The reset vector must go at the end of ROM, exactly 16 bytes from the end. +.align 16 +reset_vec: # 0x0_FFFF_FFF0 + jmp rom16_start + +.align 16, 0 +reset_end: # 0x1_0000_0000 \ No newline at end of file diff --git a/src/asm/rom16.s b/src/asm/rom16.s new file mode 100644 index 00000000..9cd86993 --- /dev/null +++ b/src/asm/rom16.s @@ -0,0 +1,26 @@ +.section .rom16, "ax" +.code16 + +.align 16 +rom16_start: + # Order of instructions from Intel SDM 9.9.1 "Switching to Protected Mode" + # Step 1: Disable interrupts + cli + + # Step 2: Load the GDT + # We are currently in 16-bit real mode. To enter 32-bit protected mode, we + # need to load 32-bit code/data segments into our GDT. The gdt32 in ROM is + # at too high of an address (right below 4G) for the data segment to reach. + # + # But we can load gdt32 via the code segement. After a reset, the base of + # the CS register is 0xFFFF0000, which means we can access gdt32. + lgdtl %cs:(GDT32_PTR - 0xFFFF0000) + + # Step 3: Set CRO.PE (Protected Mode Enable) + movl %cr0, %eax + orb $0b00000001, %al # Set bit 0 + movl %eax, %cr0 + + # Step 4: Far JMP to change execution flow and serialize the processor. + # Set CS to a 32-bit Code-Segment and jump to 32-bit code. + ljmpl $0x08, $rom32_start diff --git a/src/asm/rom32.s b/src/asm/rom32.s new file mode 100644 index 00000000..ea3f14ea --- /dev/null +++ b/src/asm/rom32.s @@ -0,0 +1,38 @@ +.section .rom32, "ax" +.code32 + +.align 16 +rom32_start: + # Now that we are in 32-bit mode, setup all the Data-Segments to be 32-bit. + movw $0x10, %ax + movw %ax, %ds + movw %ax, %es + movw %ax, %ss + movw %ax, %fs + movw %ax, %gs + + # Needed for the REP instructions below + cld + +copy_rom_to_ram: + # This is equivalent to: memcpy(data_start, rom_data_start, data_size) + movl $rom_data_start, %esi + movl $data_start, %edi + movl $data_size, %ecx + rep movsb (%esi), (%edi) + +zero_bss_in_ram: + # This is equivalent to: memset(bss_start, 0, bss_size) + xorb %al, %al + movl $bss_start, %edi + movl $bss_size, %ecx + rep stosb %al, (%edi) + +jump_to_ram: + # Zero out %ebx, as we don't have a PVH StartInfo struct. + xorl %ebx, %ebx + + # Jumping all that way from ROM (~4 GiB) to RAM (~1 MiB) is too far for a + # 32-bit relative jump, so we use a 32-bit absolute jump. + movl $ram32_start, %eax + jmp *%eax diff --git a/src/gdt.rs b/src/gdt.rs index 5f111694..669889b7 100644 --- a/src/gdt.rs +++ b/src/gdt.rs @@ -60,6 +60,10 @@ impl Pointer { static GDT64_PTR: Pointer = Pointer::new(&GDT64); static GDT64: [Descriptor; 2] = [Descriptor::empty(), Descriptor::CODE64]; +// Our 32-bit GDT lives in ROM, so it can be directly used by the ROM code. We +// should never reference or access this GDT when we are running in RAM. #[no_mangle] +#[link_section = ".gdt32"] static GDT32_PTR: Pointer = Pointer::new(&GDT32); +#[link_section = ".gdt32"] static GDT32: [Descriptor; 3] = [Descriptor::empty(), Descriptor::CODE32, Descriptor::DATA32]; From 8961efcccee73122ca4d35ff6dcdc1ebb3621350 Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Mon, 4 May 2020 20:18:29 -0700 Subject: [PATCH 3/3] bios: Make sure PVH still works with "rom" feature Without this change, QEMU/CloudHV will attempt to load the ROM into the memory region below 4GiB. But something (i.e. SeaBIOS) is already there. We just pick an arbitrary address. It doesn't actually matter where it gets loaded, as the ROM code isn't used when doing PVH boot. Signed-off-by: Joe Richey --- layout.ld | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/layout.ld b/layout.ld index 7ac269e7..6e597a22 100644 --- a/layout.ld +++ b/layout.ld @@ -1,15 +1,17 @@ ENTRY(rust64_start) /* This firmware doesn't use the ELF entrypoint */ +/* Loaders like to put stuff in low memory (< 1M), so we don't use it. */ +ram_min = 1M; +/* ram32.s only maps the first 2 MiB, which must include our whole program. */ +ram_max = 2M; + PHDRS { ram PT_LOAD FILEHDR PHDRS ; note PT_NOTE ; - rom PT_LOAD ; + rom PT_LOAD AT(ram_max) ; } -/* Loaders like to put stuff in low memory (< 1M), so we don't use it. */ -ram_min = 1M; - SECTIONS { /* Mapping the program headers and note into RAM makes the file smaller. */ @@ -37,8 +39,7 @@ SECTIONS /* Our stack grows down and is page-aligned. TODO: Add stack guard pages. */ .stack (NOLOAD) : ALIGN(4K) { . += 64K; } stack_start = .; - /* ram32.s only maps the first 2 MiB, and that must include the stack. */ - ASSERT((. <= 2M), "Stack overflows initial identity-mapped memory region") + ASSERT((. <= ram_max), "Stack overflows initial identity-mapped region") /* This is correct because all of the code sections have alignment 16. */ rom_size = ALIGN(SIZEOF(.gdt32), 16) + ALIGN(SIZEOF(.rom32), 16)