diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index fb871b1b..00000000 --- a/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "x86_64-bootloader.json" diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..dfa84e85 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[unstable] +bindeps = true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index a58fcfc4..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,134 +0,0 @@ -name: Build - -on: - push: - branches: - - '*' - - '!staging.tmp' - tags: - - '*' - pull_request: - -jobs: - test: - name: "Test" - - strategy: - matrix: - platform: [ - ubuntu-latest, - macos-latest, - windows-latest - ] - - runs-on: ${{ matrix.platform }} - timeout-minutes: 15 - - steps: - - name: "Checkout Repository" - uses: actions/checkout@v1 - - - name: Install Rustup - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain nightly - echo ::add-path::$HOME/.cargo/bin - if: runner.os == 'macOS' - - - name: "Print Rust Version" - run: | - rustc -Vv - cargo -Vv - - - name: "Install Rustup Components" - run: rustup component add rust-src llvm-tools-preview - - name: "Install cargo-xbuild" - run: cargo install cargo-xbuild --debug - - name: "Install cargo-binutils" - run: cargo install cargo-binutils --debug - - - run: cargo xbuild - working-directory: test-kernel - name: 'Build Test Kernel' - - - name: 'Build Bootloader' - run: cargo xbuild --bin bootloader --features binary --release - env: - KERNEL: "test-kernel/target/x86_64-test-kernel/debug/test-kernel" - KERNEL_MANIFEST: "test-kernel/Cargo.toml" - - - name: 'Convert Bootloader ELF to Binary' - run: cargo objcopy -- -I elf64-x86-64 -O binary --binary-architecture=i386:x86-64 target/x86_64-bootloader/release/bootloader target/x86_64-bootloader/release/bootloader.bin - - # install QEMU - - name: Install QEMU (Linux) - run: sudo apt update && sudo apt install qemu-system-x86 - if: runner.os == 'Linux' - - name: Install QEMU (macOS) - run: brew install qemu - if: runner.os == 'macOS' - env: - HOMEBREW_NO_AUTO_UPDATE: 1 - HOMEBREW_NO_BOTTLE_SOURCE_FALLBACK: 1 - HOMEBREW_NO_INSTALL_CLEANUP: 1 - - name: Install Scoop (Windows) - run: | - Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh') - echo ::add-path::$HOME\scoop\shims - if: runner.os == 'Windows' - shell: pwsh - - name: Install QEMU (Windows) - run: scoop install qemu - if: runner.os == 'Windows' - shell: pwsh - - name: "Print QEMU Version" - run: qemu-system-x86_64 --version - - - name: 'Run Test Kernel with Bootloader' - run: | - qemu-system-x86_64 -drive format=raw,file=target/x86_64-bootloader/release/bootloader.bin -device isa-debug-exit,iobase=0xf4,iosize=0x04 -display none - if [ $? -eq 123 ]; then (exit 0); else (exit 1); fi - shell: 'bash {0}' - - - build_example_kernel: - name: "Build Example Kernel" - strategy: - matrix: - platform: [ - ubuntu-latest, - macos-latest, - windows-latest - ] - runs-on: ${{ matrix.platform }} - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v1 - - name: Install Rustup - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain nightly - echo ::add-path::$HOME/.cargo/bin - if: runner.os == 'macOS' - - name: "Install Rustup Components" - run: rustup component add rust-src llvm-tools-preview - - name: "Install cargo-xbuild" - run: cargo install cargo-xbuild --debug - - name: 'Build Example Kernel' - run: cargo xbuild - working-directory: example-kernel - - - check_formatting: - name: "Check Formatting" - runs-on: ubuntu-latest - timeout-minutes: 2 - steps: - - uses: actions/checkout@v1 - - name: "Use the latest Rust nightly with rustfmt" - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - profile: minimal - components: rustfmt - override: true - - run: cargo fmt -- --check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f136dc79 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,133 @@ +name: CI + +on: + push: + pull_request: + schedule: + - cron: "40 5 * * *" # every day at 5:40 + +# This causes PR pushes to cancel previous builds, but does not impact cron jobs due to use of .ref, which will have the commit. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Check + + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v3 + - run: cargo --version --verbose + - uses: Swatinem/rust-cache@v2 + - uses: r7kamura/rust-problem-matchers@v1.1.0 + - name: "Run `cargo check`" + run: cargo check --all-targets --all + - name: "Check test kernels" + run: cargo check --all + working-directory: tests/test_kernels + - name: "Check docs.rs build" + run: cargo check + env: + RUSTFLAGS: "--cfg docsrs_dummy_build" + + test: + name: Test + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v3 + - run: cargo --version --verbose + - uses: Swatinem/rust-cache@v2 + + # install QEMU + - name: Install QEMU (Linux) + run: sudo apt update && sudo apt install qemu-system-x86 + if: runner.os == 'Linux' + - name: Install QEMU (macOS) + run: brew install qemu + if: runner.os == 'macOS' + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_BOTTLE_SOURCE_FALLBACK: 1 + HOMEBREW_NO_INSTALL_CLEANUP: 1 + - name: Install QEMU (Windows) + run: | + choco install qemu --version 2021.5.5 + echo "$Env:Programfiles\qemu" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + if: runner.os == 'Windows' + shell: pwsh + - name: "Print QEMU Version" + run: qemu-system-x86_64 --version + + - uses: r7kamura/rust-problem-matchers@v1.1.0 + - name: Run api tests + run: cargo test -p bootloader_api + - name: Run bootloader common tests + if: runner.arch == 'X64' + run: cargo test -p bootloader-x86_64-common + - name: Run integration tests + run: cargo test -- --test-threads 1 + + # test feature gates (only on one OS is enough) + - name: Test with only UEFI feature + if: runner.os == 'Linux' + run: cargo test --no-default-features --features uefi + - name: Test with only BIOS feature + if: runner.os == 'Linux' + run: cargo test --no-default-features --features bios + + fmt: + name: Check Formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: r7kamura/rust-problem-matchers@v1.1.0 + - run: cargo fmt --all -- --check + - run: cargo fmt --all -- --check + working-directory: tests/test_kernels + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: cargo --version --verbose + - uses: Swatinem/rust-cache@v2 + - uses: r7kamura/rust-problem-matchers@v1.1.0 + - run: cargo clippy --all --all-targets + - run: cargo clippy --all + working-directory: tests/test_kernels + + semver-checks: + name: Semver Checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + with: + shared-key: "semver-checks" + cache-targets: false + - name: Check semver (API) + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + package: bootloader_api + rust-toolchain: manual + - name: Check semver + uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + rust-toolchain: manual + + typos: + name: Check spelling + runs-on: ubuntu-latest + steps: + - uses: crate-ci/typos@v1.0.4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c56909db --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Release + +on: + release: + types: + - published + +jobs: + release: + name: "Release" + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: crates_io_release + + steps: + - name: "Checkout Repository" + uses: actions/checkout@v1 + + - run: cargo publish -p bootloader_api + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + - run: cargo publish -p bootloader-boot-config + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + - run: cargo publish -p bootloader-x86_64-bios-common + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + - run: cargo publish -p bootloader-x86_64-common + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + - run: cargo publish -p bootloader-x86_64-bios-boot-sector --no-verify + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + - run: cargo publish -p bootloader-x86_64-bios-stage-2 --no-verify + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + - run: cargo publish -p bootloader-x86_64-bios-stage-3 --no-verify + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + - run: cargo publish -p bootloader-x86_64-bios-stage-4 --no-verify + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + - run: cargo publish -p bootloader-x86_64-uefi --no-verify + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + - run: cargo publish -p bootloader + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/trigger-release.yml b/.github/workflows/trigger-release.yml new file mode 100644 index 00000000..51289b2d --- /dev/null +++ b/.github/workflows/trigger-release.yml @@ -0,0 +1,24 @@ +name: Trigger Release + +on: + push: + branches: + - "main" + +jobs: + check: + name: Trigger Release + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: trigger_release + + steps: + - uses: actions/checkout@v3 + + - name: "Install Python Libraries" + run: python -m pip install --user -r .github/workflows/trigger-release/requirements.txt + + - name: "Run release script" + run: "python3 .github/workflows/trigger-release/trigger-release.py" + env: + GITHUB_TOKEN: ${{ secrets.RUST_OSDEV_AUTORELEASE_TOKEN }} diff --git a/.github/workflows/trigger-release/requirements.txt b/.github/workflows/trigger-release/requirements.txt new file mode 100644 index 00000000..bd79a658 --- /dev/null +++ b/.github/workflows/trigger-release/requirements.txt @@ -0,0 +1 @@ +toml diff --git a/.github/workflows/trigger-release/trigger-release.py b/.github/workflows/trigger-release/trigger-release.py new file mode 100644 index 00000000..5eab9c64 --- /dev/null +++ b/.github/workflows/trigger-release/trigger-release.py @@ -0,0 +1,38 @@ +import toml +import requests +import subprocess + +cargo_toml = toml.load("Cargo.toml") +crate_version = cargo_toml["workspace"]["package"]["version"] +print("Detected crate version " + crate_version) + +api_url = "https://crates.io/api/v1/crates/bootloader/" + crate_version +released_version = requests.get(api_url).json() + +if "version" in released_version: + version = released_version["version"] + assert (version["crate"] == "bootloader") + assert (version["num"] == crate_version) + print("Version " + crate_version + " already exists on crates.io") + +else: + print("Could not find version " + crate_version + + " on crates.io; creating a new release") + + tag_name = "v" + crate_version + sha = subprocess.run(["git", "rev-parse", "HEAD"], check=True, + stdout=subprocess.PIPE).stdout.decode("utf-8").strip() + print(f" Tagging commit {sha} as {tag_name}") + + command = [ + "gh", "api", "--method", "POST", "-H", "Accept: application/vnd.github+json", + "/repos/rust-osdev/bootloader/releases", + "-f", f"tag_name={tag_name}", "-f", f"target_commitish={sha}", + "-f", f"name={tag_name}", + "-f", "body=[Changelog](https://github.com/rust-osdev/bootloader/blob/main/Changelog.md)", + "-F", "draft=false", "-F", "prerelease=false", "-F", "generate_release_notes=false", + ] + print(" Running `" + ' '.join(command) + '`') + subprocess.run(command, check=True) + + print(" Done") diff --git a/Cargo.lock b/Cargo.lock index 32128e57..c97f6a54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,129 +1,1080 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + [[package]] -name = "array-init" -version = "0.0.4" +name = "arrayvec" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "atomic-polyfill" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" dependencies = [ - "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "critical-section", ] [[package]] -name = "bit_field" -version = "0.9.0" +name = "autocfg" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] [[package]] name = "bit_field" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" [[package]] name = "bitflags" -version = "1.1.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] name = "bootloader" -version = "0.8.3" +version = "0.11.11" dependencies = [ - "bit_field 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "fixedvec 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "font8x8 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", - "llvm-tools 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "toml 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "usize_conversions 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "x86_64 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", - "xmas-elf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "bootloader-boot-config", + "bootloader_test_runner", + "fatfs", + "gpt", + "llvm-tools", + "mbrman", + "serde_json", + "tempfile", + "test_kernel_config_file", + "test_kernel_default_settings", + "test_kernel_fixed_kernel_address", + "test_kernel_higher_half", + "test_kernel_lower_memory_free", + "test_kernel_map_phys_mem", + "test_kernel_min_stack", + "test_kernel_pie", + "test_kernel_ramdisk", + "test_kernel_write_usable_memory", ] [[package]] -name = "cast" -version = "0.2.2" +name = "bootloader-boot-config" +version = "0.11.11" +dependencies = [ + "serde", +] + +[[package]] +name = "bootloader-x86_64-bios-boot-sector" +version = "0.11.11" + +[[package]] +name = "bootloader-x86_64-bios-common" +version = "0.11.11" + +[[package]] +name = "bootloader-x86_64-bios-stage-2" +version = "0.11.11" +dependencies = [ + "bootloader-x86_64-bios-common", + "byteorder", + "mbr-nostd", +] + +[[package]] +name = "bootloader-x86_64-bios-stage-3" +version = "0.11.11" +dependencies = [ + "bootloader-x86_64-bios-common", + "noto-sans-mono-bitmap 0.1.6", +] + +[[package]] +name = "bootloader-x86_64-bios-stage-4" +version = "0.11.11" +dependencies = [ + "bootloader-boot-config", + "bootloader-x86_64-bios-common", + "bootloader-x86_64-common", + "bootloader_api", + "log", + "rsdp", + "serde-json-core", + "usize_conversions", + "x86_64 0.15.2", +] + +[[package]] +name = "bootloader-x86_64-common" +version = "0.11.11" +dependencies = [ + "bootloader-boot-config", + "bootloader_api", + "conquer-once", + "log", + "noto-sans-mono-bitmap 0.2.0", + "rand", + "rand_hc", + "raw-cpuid", + "spinning_top", + "uart_16550 0.3.2", + "usize_conversions", + "x86_64 0.15.2", + "xmas-elf", +] + +[[package]] +name = "bootloader-x86_64-uefi" +version = "0.11.11" +dependencies = [ + "bootloader-boot-config", + "bootloader-x86_64-common", + "bootloader_api", + "log", + "serde-json-core", + "uefi", + "x86_64 0.15.2", +] + +[[package]] +name = "bootloader_api" +version = "0.11.11" +dependencies = [ + "rand", +] + +[[package]] +name = "bootloader_test_runner" +version = "0.1.0" +dependencies = [ + "bootloader", + "ovmf-prebuilt", + "strip-ansi-escapes", +] + +[[package]] +name = "byteorder" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] -name = "fixedvec" -version = "0.2.4" +name = "cc" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] -name = "font8x8" -version = "0.2.5" +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "conquer-once" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6d3a9775a69f6d1fe2cc888999b67ed30257d3da4d2af91984e722f2ec918a" +dependencies = [ + "conquer-util", +] + +[[package]] +name = "conquer-util" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e763eef8846b13b380f37dfecda401770b0ca4e56e95170237bd7c25c7db3582" + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "critical-section" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6548a0ad5d2549e111e1f6a11a6c2e2d00ce6a3dafe22948d67c2b443f775e52" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fatfs" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05669f8e7e2d7badc545c513710f0eba09c2fbef683eb859fd79c46c355048e0" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "log", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gpt" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8283e7331b8c93b9756e0cfdbcfb90312852f953c6faf9bf741e684cc3b6ad69" +dependencies = [ + "bitflags 2.3.3", + "crc", + "log", + "uuid", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "llvm-tools" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955be5d0ca0465caf127165acb47964f911e2bc26073e865deb8be7189302faf" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "mbr-nostd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e7b12e539fe14d6423dc8864bd5b5fc1f04fd14f5d9152a137de285e4329f7" +dependencies = [ + "byteorder", +] + +[[package]] +name = "mbrman" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c487024623ae38584610237dd1be8932bb2b324474b23c37a25f9fbe6bf5e9e" +dependencies = [ + "bincode", + "bitvec", + "serde", + "serde-big-array", + "thiserror", +] + +[[package]] +name = "noto-sans-mono-bitmap" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f45ec3ff6dc7bea48d52dd14a5565be0d596ac4758c9a55e42539177001a983b" + +[[package]] +name = "noto-sans-mono-bitmap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27daf9557165efe1d09b52f97393bf6283cadb0a76fbe64a1061e15553a994a" + +[[package]] +name = "ovmf-prebuilt" +version = "0.1.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa50141d081512ab30fd9e7e7692476866df5098b028536ad6680212e717fa8d" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcada80daa06c42ed5f48c9a043865edea5dc44cbf9ac009fda3b89526e28607" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca9224df2e20e7c5548aeb5f110a0f3b77ef05f8585139b7148b59056168ed2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] [[package]] -name = "nodrop" -version = "0.1.13" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b363d4f6370f88d62bf586c80405657bde0f0e1b8945d47d2ad59b906cb4f54" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rsdp" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d3add2fc55ef37511bcf81a08ee7a09eff07b23aae38b06a29024a38c604b1" +dependencies = [ + "log", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" + +[[package]] +name = "ryu" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.98" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" +dependencies = [ + "serde_derive", +] [[package]] -name = "toml" -version = "0.5.1" +name = "serde-big-array" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3323f09a748af288c3dc2474ea6803ee81f118321775bffa3ac8f7e65c5e90e7" +dependencies = [ + "serde", +] + +[[package]] +name = "serde-json-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ec3c8fe427f45ee3aaa0ebb9f0d9ab8ae9ad05d12047fe7249ae5ea9374ff0" +dependencies = [ + "heapless", + "ryu", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.166" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "serde_json" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spinning_top" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strip-ansi-escapes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" +dependencies = [ + "vte", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "test_kernel_config_file" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550 0.2.18", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_default_settings" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550 0.2.18", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_fixed_kernel_address" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550 0.2.18", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_higher_half" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550 0.2.18", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_lower_memory_free" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550 0.2.18", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_map_phys_mem" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550 0.2.18", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_min_stack" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550 0.2.18", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_pie" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550 0.2.18", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_ramdisk" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550 0.2.18", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_write_usable_memory" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550 0.2.18", + "x86_64 0.15.2", +] + +[[package]] +name = "thiserror" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.23", +] + +[[package]] +name = "uart_16550" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b074eb9300ad949edd74c529c0e8d451625af71bb948e6b65fe69f72dc1363d9" +dependencies = [ + "bitflags 1.3.2", + "rustversion", + "x86_64 0.14.13", +] + +[[package]] +name = "uart_16550" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e492212ac378a5e00da953718dafb1340d9fbaf4f27d6f3c5cab03d931d1c049" +dependencies = [ + "bitflags 2.3.3", + "rustversion", + "x86", +] + +[[package]] +name = "ucs2" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad643914094137d475641b6bab89462505316ec2ce70907ad20102d28a79ab8" dependencies = [ - "serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)", + "bit_field", ] +[[package]] +name = "uefi" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab39d5e7740f21ed4c46d6659f31038bbe3fe7a8be1f702d8a984348837c43b1" +dependencies = [ + "bitflags 1.3.2", + "log", + "ptr_meta", + "ucs2", + "uefi-macros", +] + +[[package]] +name = "uefi-macros" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0caeb0e7b31b9f1f347e541106be10aa8c66c76fa722a3298a4cd21433fabd4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "unicode-ident" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" + [[package]] name = "usize_conversions" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70329e2cbe45d6c97a5112daad40c34cd9a4e18edb5a2a18fefeb584d8d25e5" [[package]] -name = "ux" -version = "0.1.3" +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" +dependencies = [ + "getrandom", +] + +[[package]] +name = "volatile" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442887c63f2c839b346c192d047a7c87e73d0689c9157b00b53dcc27dd5ea793" + +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x86" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2781db97787217ad2a2845c396a5efe286f87467a5810836db6d74926e94a385" +dependencies = [ + "bit_field", + "bitflags 1.3.2", + "raw-cpuid", +] + +[[package]] +name = "x86_64" +version = "0.14.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c101112411baafbb4bf8d33e4c4a80ab5b02d74d2612331c61e8192fc9710491" +dependencies = [ + "bit_field", + "bitflags 2.3.3", + "rustversion", + "volatile", +] [[package]] name = "x86_64" -version = "0.7.4" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f042214de98141e9c8706e8192b73f56494087cc55ebec28ce10f26c5c364ae" dependencies = [ - "array-init 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "bit_field 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "ux 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "bit_field", + "bitflags 2.3.3", + "rustversion", + "volatile", ] [[package]] name = "xmas-elf" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d29b4d8e7beaceb4e77447ba941a7600d23d0319ab52da0461abea214832d5a" dependencies = [ - "zero 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "zero", ] [[package]] name = "zero" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" - -[metadata] -"checksum array-init 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "23589ecb866b460d3a0f1278834750268c607e8e28a1b982c907219f3178cd72" -"checksum bit_field 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a165d606cf084741d4ac3a28fb6e9b1eb0bd31f6cd999098cfddb0b2ab381dc0" -"checksum bit_field 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed8765909f9009617974ab6b7d332625b320b33c326b1e9321382ef1999b5d56" -"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" -"checksum cast 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "926013f2860c46252efceabb19f4a6b308197505082c609025aa6706c011d427" -"checksum fixedvec 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b395ef2adf62bdeefcd1b59ad0dd2225c6c333ec79656ea79ac5285c46d051ea" -"checksum font8x8 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "44226c40489fb1d602344a1d8f1b544570c3435e396dda1eda7b5ef010d8f1be" -"checksum llvm-tools 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "955be5d0ca0465caf127165acb47964f911e2bc26073e865deb8be7189302faf" -"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" -"checksum serde 1.0.98 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5626ac617da2f2d9c48af5515a21d5a480dbd151e01bb1c355e26a3e68113" -"checksum toml 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b8c96d7873fa7ef8bdeb3a9cda3ac48389b4154f32b9803b4bc26220b677b039" -"checksum usize_conversions 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f70329e2cbe45d6c97a5112daad40c34cd9a4e18edb5a2a18fefeb584d8d25e5" -"checksum ux 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "88dfeb711b61ce620c0cb6fd9f8e3e678622f0c971da2a63c4b3e25e88ed012f" -"checksum x86_64 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1ad37c1665071808d64e65f7cdae32afcdc90fd7ae7fa402bbda36b824f1add6" -"checksum xmas-elf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "22678df5df766e8d1e5d609da69f0c3132d794edf6ab5e75e7abcd2270d4cf58" -"checksum zero 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5f1bc8a6b2005884962297587045002d8cfb8dcec9db332f4ca216ddc5de82c5" +checksum = "2fe21bcc34ca7fe6dd56cc2cb1261ea59d6b93620215aefb5ea6032265527784" diff --git a/Cargo.toml b/Cargo.toml index 480f6533..eec8eb7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,41 +1,66 @@ [package] name = "bootloader" -version = "0.8.3" +description = "An experimental x86_64 bootloader that works on both BIOS and UEFI systems." +license.workspace = true +version.workspace = true +repository.workspace = true authors = ["Philipp Oppermann "] -license = "MIT/Apache-2.0" -description = "An experimental pure-Rust x86 bootloader." -repository = "https://github.com/rust-osdev/bootloader" -edition = "2018" -build = "build.rs" +edition = "2021" -[[bin]] -name = "bootloader" -required-features = ["binary"] +[workspace] +members = [ + "api", + "common", + "common/config", + "uefi", + "bios/boot_sector", + "bios/stage-*", + "bios/common", + "tests/runner", +] +exclude = ["examples/basic", "examples/test_framework", "tests/test_kernels/*"] -[dependencies] -xmas-elf = { version = "0.6.2", optional = true } -x86_64 = { version = "0.7.2", optional = true } -usize_conversions = { version = "0.2.0", optional = true } -fixedvec = { version = "0.2.4", optional = true } -bit_field = { version = "0.10.0", optional = true } - -[dependencies.font8x8] -version = "0.2.4" -default-features = false -features = ["unicode"] -optional = true +[workspace.package] +# don't forget to update `workspace.dependencies` below +version = "0.11.11" +license = "MIT OR Apache-2.0" +repository = "https://github.com/rust-osdev/bootloader" -[build-dependencies] -llvm-tools = { version = "0.1", optional = true } -toml = { version = "0.5.1", optional = true } +[workspace.dependencies] +bootloader_api = { version = "0.11.11", path = "api" } +bootloader-x86_64-common = { version = "0.11.11", path = "common" } +bootloader-boot-config = { version = "0.11.11", path = "common/config" } +bootloader-x86_64-bios-common = { version = "0.11.11", path = "bios/common" } [features] -default = [] -binary = ["xmas-elf", "x86_64", "usize_conversions", "fixedvec", "llvm-tools", "toml"] -vga_320x200 = ["font8x8"] -recursive_page_table = [] -map_physical_memory = [] -sse = ["bit_field"] +default = ["bios", "uefi"] +bios = ["dep:mbrman"] +uefi = ["dep:gpt"] + +[dependencies] +anyhow = "1.0.32" +fatfs = { version = "0.3.4", default-features = false, features = [ + "std", + "alloc", +] } +tempfile = "3.3.0" +mbrman = { version = "0.5.1", optional = true } +gpt = { version = "3.0.0", optional = true } +bootloader-boot-config = { workspace = true } +serde_json = "1.0.91" + +[dev-dependencies] +bootloader_test_runner = { path = "tests/runner" } +test_kernel_default_settings = { path = "tests/test_kernels/default_settings", artifact = "bin", target = "x86_64-unknown-none" } +test_kernel_higher_half = { path = "tests/test_kernels/higher_half", artifact = "bin", target = "x86_64-unknown-none" } +test_kernel_map_phys_mem = { path = "tests/test_kernels/map_phys_mem", artifact = "bin", target = "x86_64-unknown-none" } +test_kernel_pie = { path = "tests/test_kernels/pie", artifact = "bin", target = "x86_64-unknown-none" } +test_kernel_ramdisk = { path = "tests/test_kernels/ramdisk", artifact = "bin", target = "x86_64-unknown-none" } +test_kernel_config_file = { path = "tests/test_kernels/config_file", artifact = "bin", target = "x86_64-unknown-none" } +test_kernel_min_stack = { path = "tests/test_kernels/min_stack", artifact = "bin", target = "x86_64-unknown-none" } +test_kernel_lower_memory_free = { path = "tests/test_kernels/lower_memory_free", artifact = "bin", target = "x86_64-unknown-none" } +test_kernel_write_usable_memory = { path = "tests/test_kernels/write_usable_memory", artifact = "bin", target = "x86_64-unknown-none" } +test_kernel_fixed_kernel_address = { path = "tests/test_kernels/fixed_kernel_address", artifact = "bin", target = "x86_64-unknown-none" } [profile.dev] panic = "abort" @@ -44,9 +69,39 @@ panic = "abort" panic = "abort" lto = false debug = true +overflow-checks = true -[package.metadata.bootloader] -target = "x86_64-bootloader.json" +# duplicated from `bios/boot_sector/Cargo.toml` +[profile.stage-1] +inherits = "release" +opt-level = "s" +lto = true +codegen-units = 1 +debug = false +overflow-checks = false + +# duplicated from `bios/stage-2/Cargo.toml` +[profile.stage-2] +inherits = "release" +opt-level = "s" +codegen-units = 1 +debug = false +overflow-checks = true + +# duplicated from `bios/stage-3/Cargo.toml` +[profile.stage-3] +inherits = "release" +debug = true +overflow-checks = true + +# duplicated from `bios/stage-4/Cargo.toml` +[profile.stage-4] +inherits = "release" +debug = true +overflow-checks = true + +[build-dependencies] +llvm-tools = "0.1.1" [package.metadata.docs.rs] -features = [ "recursive_page_table", "map_physical_memory" ] +rustc-args = ["--cfg", "docsrs_dummy_build"] diff --git a/Changelog.md b/Changelog.md index 88be1c78..7daa1cdf 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,10 +1,328 @@ +# Unreleased + +# 0.11.11 – 2025-07-31 + +This release is compatible with Rust nightlies starting with `nightly-2025-07-24`. + +* [Update uart_16550 to 0.3.2](https://github.com/rust-osdev/bootloader/pull/495) +* [add `kernel_base` mapping to the BootloaderConfig](https://github.com/rust-osdev/bootloader/pull/494) +* [Use Result::ok](https://github.com/rust-osdev/bootloader/pull/496) +* [implement Send+Sync for MemoryRegions](https://github.com/rust-osdev/bootloader/pull/502) +* [Document physical memory mapping size](https://github.com/rust-osdev/bootloader/pull/506) +* [Fixes the type of target-c-int-width in target jsons. #](https://github.com/rust-osdev/bootloader/pull/509) + +**Full Changelog**: https://github.com/rust-osdev/bootloader/compare/v0.11.10...v0.11.11 + +# 0.11.10 – 2025-02-10 + +* [Remove "UEFI boot" log message](https://github.com/rust-osdev/bootloader/pull/476) +* [use threads instead of futures in build.rs](https://github.com/rust-osdev/bootloader/pull/484) +* [Move test kernels to a separate workspace](https://github.com/rust-osdev/bootloader/pull/486) +* [fix condition for running bootloader common tests](https://github.com/rust-osdev/bootloader/pull/487) +* [Update `x86_64` to `0.15.2`](https://github.com/rust-osdev/bootloader/pull/490) +* [change rustc-abi in custom targets to x86-softfloat](https://github.com/rust-osdev/bootloader/pull/491) + +**Full Changelog**: https://github.com/rust-osdev/bootloader/compare/v0.11.9...v0.11.10 + +# 0.11.9 – 2024-11-30 + +This release is compatible with Rust nightlies starting with `nightly-2024-11-23`. + +* [copy more PML4 entries](https://github.com/rust-osdev/bootloader/pull/466) +* [Convert LF to CRLF when writing to serial port](https://github.com/rust-osdev/bootloader/pull/474) +* [Update x86_64 & fix build on latest nightly](https://github.com/rust-osdev/bootloader/pull/478) + +**Full Changelog**: https://github.com/rust-osdev/bootloader/compare/v0.11.8...v0.11.9 + +# 0.11.8 – 2024-11-02 + +* [avoid 32-bit relocation to __BOOTLOADER_CONFIG](https://github.com/rust-osdev/bootloader/pull/428) +* [Fix doc comment and error message only referencing the BIOS but used for UEFI](https://github.com/rust-osdev/bootloader/pull/439) +* [Ensure all page table frames are mapped as writable](https://github.com/rust-osdev/bootloader/pull/444) +* [Guard the lower 1MB of memory](https://github.com/rust-osdev/bootloader/pull/446) +* [always cover at least the first 4 GiB of physical memory](https://github.com/rust-osdev/bootloader/pull/448) +* [Fixed "jc fail" instructions not working properly and updated README.md](https://github.com/rust-osdev/bootloader/pull/453) +* [Remove 3dnow features from stage4 target](https://github.com/rust-osdev/bootloader/pull/471) +* [mention E820 in docs for UnknownBios](https://github.com/rust-osdev/bootloader/pull/461) + +**Full Changelog**: https://github.com/rust-osdev/bootloader/compare/v0.11.7...v0.11.8 + +# 0.11.7 – 2024-02-16 + +* Set `NO_EXECUTE` flag for all writable memory regions by @phil-opp in https://github.com/rust-osdev/bootloader/pull/409 +* adapt data layout to match LLVM's by @tsatke in https://github.com/rust-osdev/bootloader/pull/420 + +**Full Changelog**: https://github.com/rust-osdev/bootloader/compare/v0.11.6...v0.11.7 + +# 0.11.6 – 2024-01-28 + +* [Embed bios and uefi binaries](https://github.com/rust-osdev/bootloader/pull/395) +* [Add a `take` method to `Optional`](https://github.com/rust-osdev/bootloader/pull/411) +* [Fix data layout for stage 3 target](https://github.com/rust-osdev/bootloader/pull/413) + +**Full Changelog**: https://github.com/rust-osdev/bootloader/compare/v0.11.5...v0.11.6 + +# 0.11.5 – 2023-12-28 + +* [RacyCell: Data race allowed on `T`](https://github.com/rust-osdev/bootloader/pull/390) +* [Update license field following SPDX 2.1 license expression standard](https://github.com/rust-osdev/bootloader/pull/391) +* [kernel image fields & zero out rbp](https://github.com/rust-osdev/bootloader/pull/346) +* [Update `rustix` dependency](https://github.com/rust-osdev/bootloader/pull/398) +* [Add an additional MB of space to the generated FAT partition](https://github.com/rust-osdev/bootloader/pull/397) +* [Fix: Enable test runner again](https://github.com/rust-osdev/bootloader/pull/407) +* [Fix: Mark `ramdisk` as used in memory map](https://github.com/rust-osdev/bootloader/pull/408) + +**Full Changelog**: https://github.com/rust-osdev/bootloader/compare/v0.11.4...v0.11.5 + +# 0.11.4 – 2023-07-05 + +- [Fix bug stemming from treating an exclusive range as an inclusive ranges](https://github.com/rust-osdev/bootloader/pull/362) +- [Update `uefi` dependency to `v0.20`](https://github.com/rust-osdev/bootloader/pull/360) +- [Implemented sorting of uefi memory maps](https://github.com/rust-osdev/bootloader/pull/365) +- [Run `cargo update` to fix build on nightly](https://github.com/rust-osdev/bootloader/pull/385) + +# 0.11.3 – 2023-03-26 + +- [Fix docs.rs build](https://github.com/rust-osdev/bootloader/pull/358) + +# 0.11.2 – 2023-03-12 + +- Fix internal error in Cargo.toml setup that prevented publishing 0.11.1 + +# 0.11.1 – 2023-03-12 + +## Features + +* [Load ramdisk feature](https://github.com/rust-osdev/bootloader/pull/302) +* [Support higher half position independent kernels](https://github.com/rust-osdev/bootloader/pull/289) +- New `set_boot_config` method to configure the boot process (frame buffer dimensions and log output). This is the result of a set of PRs: + - [Log level feature](https://github.com/rust-osdev/bootloader/pull/303) + - [Improve Logging](https://github.com/rust-osdev/bootloader/pull/314) + - [Add support for a configuration file](https://github.com/rust-osdev/bootloader/pull/326) + - [Fix loading of boot configuration](https://github.com/rust-osdev/bootloader/pull/342) + - [Minor improvements to `BootConfig`](https://github.com/rust-osdev/bootloader/pull/349) +- [Add `bios` and `uefi` cargo features](https://github.com/rust-osdev/bootloader/pull/304) +- Boot Info: [Add a `FrameBuffer::into_buffer` method for taking ownership](https://github.com/rust-osdev/bootloader/pull/319) +* [Simplified disk builder](https://github.com/rust-osdev/bootloader/pull/320) + +## Fixes +- [Correctly allocate last frame in memory descriptor](https://github.com/rust-osdev/bootloader/pull/316) +- [Correctness fixes for stage2](https://github.com/rust-osdev/bootloader/pull/328) +- [Fix: treat `kernel_slice_end` as an exclusive bound when checking for overlaps](https://github.com/rust-osdev/bootloader/pull/334) +* [Map BIOS stage-4 at lower address to avoid conflicts with the kernel](https://github.com/rust-osdev/bootloader/pull/337) +* [Create kernel stack with correct size and set up a guard page](https://github.com/rust-osdev/bootloader/pull/335) + +## Other improvements +- [Implement faster bios builds](https://github.com/rust-osdev/bootloader/pull/324) +- [Remove dependency on `time` crate](https://github.com/rust-osdev/bootloader/pull/332) +- [Fix warnings from Clippy](https://github.com/rust-osdev/bootloader/pull/336) +* [Make a link in the documentation clickable](https://github.com/rust-osdev/bootloader/pull/341) +* [Fix spelling and add a check](https://github.com/rust-osdev/bootloader/pull/340) +* [Run cargo update](https://github.com/rust-osdev/bootloader/pull/347) + +# 0.11.0 – 2022-12-01 + +Major rewrite of the `bootloader` crate with various breaking changes: + +- **Separate API crate:** The bootloader is now split into two parts: An API crate to make kernels loadable by the bootloader and the actual bootloader implementation. This makes the build process for kernels much easier and faster. +- **New config system:** Instead of configuring the bootloader via a special table in the `Cargo.toml`, the configuration now happens through a normal Rust struct, which is part of the `entry_point!` macro. The macro then serializes the config struct at compile time and places it in a special ELF output section. The compile time serialization happens through a manually implemented `const fn` of the config struct. +- **Load the kernel at runtime:** The bootloader is now able to load files from FAT partitions at runtime. Thus, we don't need to link the kernel into the bootloader executable anymore. As a result, we don't need to recompile the bootloader on kernel changes anymore. We also load the config at runtime from the kernel's ELF section, which eliminates the second reason for recompiling the bootloader as well. +- **Split into sub-crates:** Since the bootloader build process does not need access to the kernel executable or its `Cargo.toml` anymore, we can build the different parts of the bootloader independently. For example, the BIOS boot sector is now a separate crate, and the UEFI bootloader is too. +- **Library to create disk images:** To create an abstraction the complex build steps of the different bootloader executables, we compile them inside cargo build scripts. At the top level, we provide a `bootloader` _library_ crate, which compiles everything as part of its build script. This library includes functions for creating BIOS and UEFI disk images for a given kernel. These functions can be used e.g. from a builder crate or a build script of the downstream operating system. + +See our [migration guides](docs/migration/README.md) for details. + +# 0.10.13 – 2022-09-25 + +- Add dynamic range configuration ([#229](https://github.com/rust-osdev/bootloader/pull/229)) +- Fix boot for machines that report memory regions at high physical addresses (see [#259](https://github.com/rust-osdev/bootloader/issues/259)) + - Limit BIOS bootloader's `max_phys_addr` to 4 GiB ([#260](https://github.com/rust-osdev/bootloader/pull/260)) + - fix `get_free_address` for large sizes (0.10) ([#263](https://github.com/rust-osdev/bootloader/pull/263)) + - Only perform a single TLB flush after identity mapping ([#265](https://github.com/rust-osdev/bootloader/pull/265)) +- Correct typos in `src/binary/level_4_entries.rs` ([#228](https://github.com/rust-osdev/bootloader/pull/228)) + +# 0.10.12 – 2022-02-06 + +- Add support for position independent executables ([#206](https://github.com/rust-osdev/bootloader/pull/206)) +- Add optional ASLR ([#221](https://github.com/rust-osdev/bootloader/pull/221)) +- Logger: nicer font rendering into framebuffer ([#213](https://github.com/rust-osdev/bootloader/pull/213)) +- Fix warnings on latest nightly (`maybe_uninit_extra` is no longer feature-gated) ([#222](https://github.com/rust-osdev/bootloader/pull/222)) +- Rework `UsedLevel4Entries` ([#219](https://github.com/rust-osdev/bootloader/pull/219)) +- Add small doc-comment to entry_point! macro ([#220](https://github.com/rust-osdev/bootloader/pull/220)) + +# 0.10.11 – 2022-01-09 + +- Remove feature flag for `lang_items`, `asm` and `global_asm` ([#210](https://github.com/rust-osdev/bootloader/pull/210)) +- Use `set_reg` method of `CS`, `DS`, `ES` and `SS` segment structs ([#211](https://github.com/rust-osdev/bootloader/pull/211)) + +# 0.10.10 – 2021-12-23 + +- Fix `asm` imports on latest nightly ([#209](https://github.com/rust-osdev/bootloader/pull/209)) + +# 0.10.9 – 2021-10-07 + +- Add support for framebuffer configuration ([#179](https://github.com/rust-osdev/bootloader/pull/179)) + +# 0.10.8 – 2021-08-22 + +- Pad UEFI FAT file length ([#180](https://github.com/rust-osdev/bootloader/pull/180)) +- Also check cfg gated target field for bootloader dependency ([#182](https://github.com/rust-osdev/bootloader/pull/182) + +# 0.10.7 – 2021-08-09 + +- Fix `relocation-model` field name in the target spec json ([#186](https://github.com/rust-osdev/bootloader/pull/186)) + - This effectively changes the `relocation-model` from `pic` to `static`. Please report if you encounter any issues because of this. + - This fixes the compilation warnings on the latest nightlies. + +# 0.10.6 – 2021-05-24 + +- Identity-map GDT into kernel address space to fix `iretq` ([#175](https://github.com/rust-osdev/bootloader/pull/175)) +- Uefi: Look for an ACPI2 RSDP first ([#174](https://github.com/rust-osdev/bootloader/pull/174)) +- Don't check target architecture for builder crate to support cross-compiling ([#176](https://github.com/rust-osdev/bootloader/pull/176)) + +# 0.10.5 – 2021-05-21 + +- Fix build on latest Rust nightlies by updating `uefi-rs` dependency ([#170](https://github.com/rust-osdev/bootloader/pull/170)) + - Also: Fix warnings about `.intel_syntax` attribute in assembly code + +# 0.10.4 – 2021-05-14 + +- Fix build on latest Rust nightly by updating to `uefi` v0.9.0 ([#162](https://github.com/rust-osdev/bootloader/pull/162)) +- Fix higher half kernels by identity mapping context switch fn earlier ([#161](https://github.com/rust-osdev/bootloader/pull/161)) + - Also: improve reporting of mapping errors + +# 0.10.3 – 2021-05-05 + +- Change register used in setting SS in stage_4 ([#156](https://github.com/rust-osdev/bootloader/pull/156)) + +# 0.10.2 – 2021-04-30 + +- Use new `asm!` syntax instead of deprecated `llvm_asm!` ([#154](https://github.com/rust-osdev/bootloader/pull/154)) +- Reduce the number of used unstable features of x86_64 crate ([#155](https://github.com/rust-osdev/bootloader/pull/155)) + +# 0.10.1 – 2021-04-07 + +- Fix docs.rs build: Don't enable any features + +# 0.10.0 – 2021-04-06 + +- Rewrite for UEFI support ([#130](https://github.com/rust-osdev/bootloader/pull/130)) + - Includes a new build process that no longer uses the `bootimage` crate. See the Readme for details. + +# 0.9.19 _(backport) – 2021-08-09 + +- Set `relocation-model: static` and `panic-strategy: abort` and `fix .intel_syntax` warnings ([#185](https://github.com/rust-osdev/bootloader/pull/185)) + - Fixes warnings on the latest Rust nightlies. + - This effectively changes the `relocation-model` and `panic-strategy`. Please report if you encounter any issues because of this. + +# 0.9.18 _(hotfix)_ – 2021-05-20 + +- Fix nightly regression by manually passing --gc-sections ([#168](https://github.com/rust-osdev/bootloader/pull/168)) + +# 0.9.17 _(backport)_ – 2021-04-30 + +- Reduce the number of used unstable features of x86_64 crate (backport [#155](https://github.com/rust-osdev/bootloader/pull/140)) + +# 0.9.16 – 2021-03-07 + +- Replace all remaining `lea`s with `mov` + `offset` ([#140](https://github.com/rust-osdev/bootloader/pull/140)) + +# 0.9.15 – 2021-03-07 + +- Fix linker errors on latest nightlies ([#139](https://github.com/rust-osdev/bootloader/pull/139)) + +# 0.9.14 – 2021-02-24 + +- Fix "panic message is not a string literal" warning ([#138](https://github.com/rust-osdev/bootloader/pull/138)) + +# 0.9.13 – 2021-02-24 + +(accidental release) + +# 0.9.12 – 2021-02-02 + +- Fix build on latest nightly by updating x86_64 to v0.13.2 ([#135](https://github.com/rust-osdev/bootloader/pull/135)) + +# 0.9.11 – 2020-09-29 + +- Update `Cargo.lock` to fix nightly breakage ([#129](https://github.com/rust-osdev/bootloader/pull/129)) + +# 0.9.10 – 2020-09-24 + +- Update `x86_64` again to version 0.12.1 to fix `const fn`-related build errors on latest nightly + +# 0.9.9 – 2020-09-20 + +- Run `cargo update` to fix build errors of `x86_64` on latest nightly + +# 0.9.8 – 2020-07-17 + +- Enable rlibc dependency only with `binary` feature ([#126](https://github.com/rust-osdev/bootloader/pull/126)) + +# 0.9.7 – 2020-07-17 + +- Make bootloader buildable with `-Zbuild-std` ([#125](https://github.com/rust-osdev/bootloader/pull/125)) + +# 0.9.6 – 2020-07-16 + +- Change 1st stage int 13h addressing ([#123](https://github.com/rust-osdev/bootloader/pull/123)) + +# 0.9.5 + +- Fix warning by renaming `_improper_ctypes_check` functions ([#122](https://github.com/rust-osdev/bootloader/pull/122)) + +# 0.9.4 + +- Add recursive_idx for boot info ([#116](https://github.com/rust-osdev/bootloader/pull/116)) +- Remove unused feature gates ([#118](https://github.com/rust-osdev/bootloader/pull/118)) + +# 0.9.3 + +- Update x86_64 dependency to version 0.11.0 ([#117](https://github.com/rust-osdev/bootloader/pull/117)) + +# 0.9.2 + +- **Nightly Breakage:** Use `llvm_asm!` instead of deprecated `asm!` ([#108](https://github.com/rust-osdev/bootloader/pull/108)) + +# 0.9.1 + +- SSE feature: remove inline assembly + don't set reserved bits ([#105](https://github.com/rust-osdev/bootloader/pull/105)) + +# 0.9.0 + +- **Breaking**: Identity-map complete vga region (0xa0000 to 0xc0000) ([#104](https://github.com/rust-osdev/bootloader/pull/104)) + +# 0.8.9 + +- Implement boot-info-address ([#101](https://github.com/rust-osdev/bootloader/pull/101)) + +# 0.8.8 + +- Add basic support for ELF thread local storage segments ([#96](https://github.com/rust-osdev/bootloader/pull/96)) + +# 0.8.7 + +- Fix docs.rs build (see commit 01671dbe449b85b3c0ea73c5796cc8f9661585ee) + +# 0.8.6 + +- Objcopy replaces `.` chars with `_` chars ([#94](https://github.com/rust-osdev/bootloader/pull/94)) + +# 0.8.5 + +- Update x86_64 dependency ([#92](https://github.com/rust-osdev/bootloader/pull/92)) + +# 0.8.4 + +- Move architecture checks from build script into lib.rs ([#91](https://github.com/rust-osdev/bootloader/pull/91)) + # 0.8.3 - Remove unnecessary `extern C` on panic handler to fix not-ffi-safe warning ([#85](https://github.com/rust-osdev/bootloader/pull/85)) # 0.8.2 -- Change the way the kernel entry point is called to honor alignement ABI ([#81](https://github.com/rust-osdev/bootloader/pull/81)) +- Change the way the kernel entry point is called to honor alignment ABI ([#81](https://github.com/rust-osdev/bootloader/pull/81)) # 0.8.1 @@ -78,7 +396,7 @@ - The level 4 page table is only recursively mapped if the `recursive_page_table` feature is enabled. - Rename `BootInfo::p4_table_addr` to `BootInfo::recursive_page_table_addr` (only present if the cargo feature is enabled) -- Remove `From` implemenations for x86_64 `FrameRange` +- Remove `From` implementations for x86_64 `FrameRange` - This only works when the versions align, so it is not a good general solution. - Remove unimplemented `BootInfo::package` field. - Make `BootInfo` non-exhaustive so that we can add additional fields later. diff --git a/README.md b/README.md index 510c65f6..e8f4ef01 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,86 @@ # bootloader -[![Build Status](https://dev.azure.com/rust-osdev/bootloader/_apis/build/status/rust-osdev.bootloader?branchName=master)](https://dev.azure.com/rust-osdev/bootloader/_build/latest?definitionId=1&branchName=master) [![Join the chat at https://gitter.im/rust-osdev/bootloader](https://badges.gitter.im/rust-osdev/bootloader.svg)](https://gitter.im/rust-osdev/bootloader?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Docs](https://docs.rs/bootloader/badge.svg)](https://docs.rs/bootloader) +[![Build Status](https://github.com/rust-osdev/bootloader/actions/workflows/build.yml/badge.svg)](https://github.com/rust-osdev/bootloader/actions/workflows/build.yml) +[![Join the chat at https://rust-osdev.zulipchat.com](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://rust-osdev.zulipchat.com) -An experimental x86 bootloader written in Rust and inline assembly. - -Written for the [second edition](https://github.com/phil-opp/blog_os/issues/360) of the [Writing an OS in Rust](https://os.phil-opp.com) series. - -## Design - -TODO - -## Configuration - -The bootloader exposes a few variables which can be configured through the `Cargo.toml` of your kernel: - -```toml -[package.metadata.bootloader] -# The address at which the kernel stack is placed. If not provided, the bootloader -# dynamically searches for a location. -kernel-stack-address = "0xFFFFFF8000000000" - -# The size of the kernel stack, given in number of 4KiB pages. Defaults to 512. -kernel-stack-size = 128 - -# The virtual address offset from which physical memory is mapped, as described in -# https://os.phil-opp.com/paging-implementation/#map-the-complete-physical-memory -# Only applies if the `map_physical_memory` feature of the crate is enabled. -# If not provided, the bootloader dynamically searches for a location. -physical-memory-offset = "0xFFFF800000000000" -``` - -Note that the addresses **must** be given as strings (in either hex or decimal format), as [TOML](https://github.com/toml-lang/toml) does not support unsigned 64-bit integers. +An experimental x86_64 bootloader that works on both BIOS and UEFI systems. Written in Rust and some inline assembly, buildable on all platforms without additional build-time dependencies (just some `rustup` components). ## Requirements -You need a nightly [Rust](https://www.rust-lang.org) compiler and [cargo xbuild](https://github.com/rust-osdev/cargo-xbuild). You also need the `llvm-tools-preview` component, which can be installed through `rustup component add llvm-tools-preview`. - -## Build - -The simplest way to use the bootloader is in combination with the [bootimage](https://github.com/rust-osdev/bootimage) tool. This crate **requires at least bootimage 0.7.7**. With the tool installed, you can add a normal cargo dependency on the `bootloader` crate to your kernel and then run `bootimage build` to create a bootable disk image. You can also execute `bootimage run` to run your kernel in [QEMU](https://www.qemu.org/) (needs to be installed). - -To compile the bootloader manually, you need to invoke `cargo xbuild` with two environment variables: -* `KERNEL`: points to your kernel executable (in the ELF format) -* `KERNEL_MANIFEST`: points to the `Cargo.toml` describing your kernel - -For example: -``` -KERNEL=/path/to/your/kernel/target/debug/your_kernel KERNEL_MANIFEST=/path/to/your/kernel/Cargo.toml cargo xbuild -``` - -As an example, you can build the bootloader with example kernel from the `example-kernel` directory with the following commands: - -``` -cd example-kernel -cargo xbuild -cd .. -KERNEL=example-kernel/target/x86_64-example-kernel/debug/example-kernel KERNEL_MANIFEST=example-kernel/Cargo.toml cargo xbuild --release --features binary -``` - -The `binary` feature is required to enable the dependencies required for compiling the bootloader executable. The command results in a bootloader executable at `target/x86_64-bootloader.json/release/bootloader`. This executable is still an ELF file, which can't be run directly. - -## Run - -To run the compiled bootloader executable, you need to convert it to a binary file. You can use the `llvm-objcopy` tools that ships with the `llvm-tools-preview` rustup component. The easiest way to use this tool is using [`cargo-binutils`](https://github.com/rust-embedded/cargo-binutils), which can be installed through `cargo install cargo-binutils`. Then you can perform the conversion with the following command: - -``` -cargo objcopy -- -I elf64-x86-64 -O binary --binary-architecture=i386:x86-64 \ - target/x86_64-bootloader/release/bootloader target/x86_64-bootloader/release/bootloader.bin -``` - -You can run the `bootloader.bin` file using [QEMU](https://www.qemu.org/): - -``` -qemu-system-x86_64 -drive format=raw,file=target/x86_64-bootloader/release/bootloader.bin -``` - -Or burn it to an USB drive to boot it on real hardware: - -``` -dd if=target/x86_64-bootloader/release/bootloader.bin of=/dev/sdX && sync -``` - -Where sdX is the device name of your USB stick. **Be careful** to choose the correct device name, because everything on that device is overwritten. - -## Features -The bootloader crate can be configured through some cargo features: - -- `vga_320x200`: This feature switches the VGA hardware to mode 0x13, a graphics mode with resolution 320x200 and 256 colors per pixel. The framebuffer is linear and lives at address `0xa0000`. -- `recursive_page_table`: Maps the level 4 page table recursively and adds the [`recursive_page_table_address`](https://docs.rs/bootloader/0.4.0/bootloader/bootinfo/struct.BootInfo.html#structfield.recursive_page_table_addr) field to the passed `BootInfo`. -- `map_physical_memory`: Maps the complete physical memory in the virtual address space and passes a [`physical_memory_offset`](https://docs.rs/bootloader/0.4.0/bootloader/bootinfo/struct.BootInfo.html#structfield.physical_memory_offset) field in the `BootInfo`. - - The virtual address where the physical memory should be mapped is configurable by setting the `physical-memory-offset` field in the kernel's `Cargo.toml`, as explained in [Configuration](#Configuration). - -## Advanced Documentation -See these guides for advanced usage of this crate: - -- [Chainloading](doc/chainloading.md) -- Higher Half Kernel - TODO +You need a nightly [Rust](https://www.rust-lang.org) compiler with the `llvm-tools-preview` component, which can be installed through `rustup component add llvm-tools-preview`. + +## Usage + +To use this crate, you need to adjust your kernel to be bootable first. Then you can create a bootable disk image from your compiled kernel. These steps are explained in detail below. + +If you're already using an older version of the `bootloader` crate, follow our [migration guides](docs/migration). + +### Kernel + +To make your kernel compatible with `bootloader`: + +- Add a dependency on the `bootloader_api` crate in your kernel's `Cargo.toml`. +- Your kernel binary should be `#![no_std]` and `#![no_main]`. +- Define an entry point function with the signature `fn kernel_main(boot_info: &'static mut bootloader_api::BootInfo) -> !`. The function name can be arbitrary. + - The `boot_info` argument provides information about available memory, the framebuffer, and more. See the API docs for `bootloader_api` crate for details. +- Use the `entry_point` macro to register the entry point function: `bootloader_api::entry_point!(kernel_main);` + - The macro checks the signature of your entry point function and generates a `_start` entry point symbol for it. (If you use a linker script, make sure that you don't change the entry point name to something else.) + - To use non-standard configuration, you can pass a second argument of type `&'static bootloader_api::BootloaderConfig` to the `entry_point` macro. For example, you can require a specific stack size for your kernel: + ```rust + const CONFIG: bootloader_api::BootloaderConfig = { + let mut config = bootloader_api::BootloaderConfig::new_default(); + config.kernel_stack_size = 100 * 1024; // 100 KiB + config + }; + bootloader_api::entry_point!(kernel_main, config = &CONFIG); + ``` +- Compile your kernel to an ELF executable by running **`cargo build --target x86_64-unknown-none`**. You might need to run `rustup target add x86_64-unknown-none` before to download precompiled versions of the `core` and `alloc` crates. +- Thanks to the `entry_point` macro, the compiled executable contains a special section with metadata and the serialized config, which will enable the `bootloader` crate to load it. + +### Booting + +To combine your kernel with a bootloader and create a bootable disk image, follow these steps: + +- Move your full kernel code into a `kernel` subdirectory. +- Create a new `os` crate at the top level that defines a [workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html). +- Add a `build-dependencies` on the `bootloader` crate. +- Create a [`build.rs`](https://doc.rust-lang.org/cargo/reference/build-scripts.html) build script. +- Set up an [artifact dependency](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies) to add your `kernel` crate as a `build-dependency`: + ```toml + # in Cargo.toml + [build-dependencies] + kernel = { path = "kernel", artifact = "bin", target = "x86_64-unknown-none" } + ``` + ```toml + # .cargo/config.toml + + [unstable] + # enable the unstable artifact-dependencies feature, see + # https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies + bindeps = true + ``` + Alternatively, you can use [`std::process::Command`](https://doc.rust-lang.org/stable/std/process/struct.Command.html) to invoke the build command of your kernel in the `build.rs` script. +- Obtain the path to the kernel executable. When using an artifact dependency, you can retrieve this path using `std::env::var_os("CARGO_BIN_FILE_MY_KERNEL_my-kernel")` +- Use `bootloader::UefiBoot` and/or `bootloader::BiosBoot` to create a bootable disk image with your kernel. +- Do something with the bootable disk images in your `main.rs` function. For example, run them with QEMU. + +See our [disk image creation template](docs/create-disk-image.md) for a more detailed example. + +## Architecture + +This project is split into three separate entities: + +- A [`bootloader_api`](./api) library with the entry point, configuration, and boot info definitions. + - Kernels should include this library as a normal cargo dependency. + - The provided `entry_point` macro will encode the configuration settings into a separate ELF section of the compiled kernel executable. +- [BIOS](./bios) and [UEFI](./uefi) binaries that contain the actual bootloader implementation. + - The implementations share a higher-level [common library](./common). + - Both implementations load the kernel at runtime from a FAT partition. This FAT partition is created + - The configuration is read from a special section of the kernel's ELF file, which is created by the `entry_point` macro of the `bootloader_api` library. +- A `bootloader` library to create bootable disk images that run a given kernel. This library is the top-level crate in this project. + - The library builds the BIOS and UEFI implementations in the [`build.rs`](./build.rs). + - It provides functions to create FAT-formatted bootable disk images, based on the compiled BIOS and UEFI bootloaders. ## License diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 00000000..9cc193e5 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,4 @@ +[default.extend-words] +inout = "inout" +rela = "rela" +hda = "hda" diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 00000000..ef687b0c --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "bootloader_api" +license.workspace = true +version.workspace = true +repository.workspace = true +edition = "2021" +description = "Makes a kernel compatible with the bootloader crate" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[dev-dependencies] +rand = "0.8.4" diff --git a/api/build.rs b/api/build.rs new file mode 100644 index 00000000..75754523 --- /dev/null +++ b/api/build.rs @@ -0,0 +1,69 @@ +use std::{env, fs, path::Path}; + +fn main() { + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("concat.rs"); + + let combinations = [ + (1, 8), + (1, 9), + (2, 1), + (2, 2), + (4, 3), + (16, 7), + (23, 8), + (31, 9), + (40, 9), + (49, 9), + (58, 9), + (67, 10), + (77, 10), + (87, 1), + (88, 9), + (97, 9), + (106, 9), + (115, 9), + (124, 9), + ]; + + let mut code = String::new(); + for (i, j) in combinations { + code += &format!( + "pub const fn concat_{i}_{j}(a: [u8; {i}], b: [u8; {j}]) -> [u8; {i} + {j}] {{ + [{a}, {b}] + }}", + i = i, + j = j, + a = (0..i) + .map(|idx| format!("a[{idx}]")) + .collect::>() + .join(","), + b = (0..j) + .map(|idx| format!("b[{idx}]")) + .collect::>() + .join(","), + ); + } + + fs::write(dest_path, code).unwrap(); + println!("cargo:rerun-if-changed=build.rs"); + + let version_major: u16 = env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(); + let version_minor: u16 = env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(); + let version_patch: u16 = env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(); + let pre_release: bool = !env!("CARGO_PKG_VERSION_PRE").is_empty(); + + fs::write( + Path::new(&out_dir).join("version_info.rs"), + format!( + " + pub const VERSION_MAJOR: u16 = {version_major}; + pub const VERSION_MINOR: u16 = {version_minor}; + pub const VERSION_PATCH: u16 = {version_patch}; + pub const VERSION_PRE: bool = {pre_release}; + " + ), + ) + .unwrap(); + println!("cargo:rerun-if-changed=Cargo.toml"); +} diff --git a/api/src/config.rs b/api/src/config.rs new file mode 100644 index 00000000..c0f574bd --- /dev/null +++ b/api/src/config.rs @@ -0,0 +1,613 @@ +#![allow(deprecated)] + +use crate::{concat::*, version_info}; + +/// Allows configuring the bootloader behavior. +/// +/// TODO: describe use together with `entry_point` macro +/// TODO: example +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub struct BootloaderConfig { + /// The version of the bootloader API. + /// + /// Automatically generated from the crate version. Checked on deserialization to + /// ensure that the kernel and bootloader use the same API version, i.e. the same config + /// and boot info format. + pub(crate) version: ApiVersion, + + /// Configuration for (optional) page table mappings created by the bootloader. + pub mappings: Mappings, + + /// The size of the stack that the bootloader should allocate for the kernel (in bytes). + /// + /// The bootloader starts the kernel with a valid stack pointer. This setting defines + /// the stack size that the bootloader should allocate and map. + /// + /// The stack is created with a additional guard page, so a stack overflow will lead to + /// a page fault. + pub kernel_stack_size: u64, + + /// Configuration for the frame buffer that can be used by the kernel to display pixels + /// on the screen. + #[deprecated( + since = "0.11.1", + note = "The frame buffer is now configured through the `BootConfig` struct when creating the bootable disk image" + )] + pub frame_buffer: FrameBuffer, +} + +impl BootloaderConfig { + pub(crate) const UUID: [u8; 16] = [ + 0x74, 0x3C, 0xA9, 0x61, 0x09, 0x36, 0x46, 0xA0, 0xBB, 0x55, 0x5C, 0x15, 0x89, 0x15, 0x25, + 0x3D, + ]; + #[doc(hidden)] + pub const SERIALIZED_LEN: usize = 133; + + /// Creates a new default configuration with the following values: + /// + /// - `kernel_stack_size`: 80kiB + /// - `mappings`: See [`Mappings::new_default()`] + pub const fn new_default() -> Self { + Self { + kernel_stack_size: 80 * 1024, + version: ApiVersion::new_default(), + mappings: Mappings::new_default(), + frame_buffer: FrameBuffer::new_default(), + } + } + + /// Serializes the configuration to a byte array. + /// + /// This is used by the [`crate::entry_point`] macro to store the configuration in a + /// dedicated section in the resulting ELF file. + pub const fn serialize(&self) -> [u8; Self::SERIALIZED_LEN] { + let Self { + version, + mappings, + kernel_stack_size, + frame_buffer, + } = self; + let ApiVersion { + version_major, + version_minor, + version_patch, + pre_release, + } = version; + let Mappings { + kernel_stack, + kernel_base, + boot_info, + framebuffer, + physical_memory, + page_table_recursive, + aslr, + dynamic_range_start, + dynamic_range_end, + ramdisk_memory, + } = mappings; + let FrameBuffer { + minimum_framebuffer_height, + minimum_framebuffer_width, + } = frame_buffer; + + let version = { + let one = concat_2_2(version_major.to_le_bytes(), version_minor.to_le_bytes()); + let two = concat_2_1(version_patch.to_le_bytes(), [*pre_release as u8]); + concat_4_3(one, two) + }; + let buf = concat_16_7(Self::UUID, version); + + let buf = concat_23_8(buf, kernel_stack_size.to_le_bytes()); + + let buf = concat_31_9(buf, kernel_stack.serialize()); + let buf = concat_40_9(buf, kernel_base.serialize()); + + let buf = concat_49_9(buf, boot_info.serialize()); + let buf = concat_58_9(buf, framebuffer.serialize()); + + let buf = concat_67_10( + buf, + match physical_memory { + Option::None => [0; 10], + Option::Some(m) => concat_1_9([1], m.serialize()), + }, + ); + let buf = concat_77_10( + buf, + match page_table_recursive { + Option::None => [0; 10], + Option::Some(m) => concat_1_9([1], m.serialize()), + }, + ); + let buf = concat_87_1(buf, [(*aslr) as u8]); + let buf = concat_88_9( + buf, + match dynamic_range_start { + Option::None => [0; 9], + Option::Some(addr) => concat_1_8([1], addr.to_le_bytes()), + }, + ); + let buf = concat_97_9( + buf, + match dynamic_range_end { + Option::None => [0; 9], + Option::Some(addr) => concat_1_8([1], addr.to_le_bytes()), + }, + ); + + let buf = concat_106_9(buf, ramdisk_memory.serialize()); + + let buf = concat_115_9( + buf, + match minimum_framebuffer_height { + Option::None => [0; 9], + Option::Some(addr) => concat_1_8([1], addr.to_le_bytes()), + }, + ); + + concat_124_9( + buf, + match minimum_framebuffer_width { + Option::None => [0; 9], + Option::Some(addr) => concat_1_8([1], addr.to_le_bytes()), + }, + ) + } + + /// Tries to deserialize a config byte array that was created using [`Self::serialize`]. + /// + /// This is used by the bootloader to deserialize the configuration given in the kernel's + /// ELF file. + /// + /// TODO: return error enum + pub fn deserialize(serialized: &[u8]) -> Result { + if serialized.len() != Self::SERIALIZED_LEN { + return Err("invalid len"); + } + + let s = serialized; + + let (uuid, s) = split_array_ref(s); + if uuid != &Self::UUID { + return Err("invalid UUID"); + } + + let (version, s) = { + let (&major, s) = split_array_ref(s); + let (&minor, s) = split_array_ref(s); + let (&patch, s) = split_array_ref(s); + let (&pre, s) = split_array_ref(s); + let pre = match pre { + [0] => false, + [1] => true, + _ => return Err("invalid pre version"), + }; + + let version = ApiVersion { + version_major: u16::from_le_bytes(major), + version_minor: u16::from_le_bytes(minor), + version_patch: u16::from_le_bytes(patch), + pre_release: pre, + }; + (version, s) + }; + + // TODO check version against this crate version -> error if they're different + + let (&kernel_stack_size, s) = split_array_ref(s); + + let (mappings, s) = { + let (&kernel_stack, s) = split_array_ref(s); + let (&kernel_base, s) = split_array_ref(s); + let (&boot_info, s) = split_array_ref(s); + let (&framebuffer, s) = split_array_ref(s); + let (&physical_memory_some, s) = split_array_ref(s); + let (&physical_memory, s) = split_array_ref(s); + let (&page_table_recursive_some, s) = split_array_ref(s); + let (&page_table_recursive, s) = split_array_ref(s); + let (&[alsr], s) = split_array_ref(s); + let (&dynamic_range_start_some, s) = split_array_ref(s); + let (&dynamic_range_start, s) = split_array_ref(s); + let (&dynamic_range_end_some, s) = split_array_ref(s); + let (&dynamic_range_end, s) = split_array_ref(s); + let (&ramdisk_memory, s) = split_array_ref(s); + + let mappings = Mappings { + kernel_stack: Mapping::deserialize(&kernel_stack)?, + kernel_base: Mapping::deserialize(&kernel_base)?, + boot_info: Mapping::deserialize(&boot_info)?, + framebuffer: Mapping::deserialize(&framebuffer)?, + physical_memory: match physical_memory_some { + [0] if physical_memory == [0; 9] => Option::None, + [1] => Option::Some(Mapping::deserialize(&physical_memory)?), + _ => return Err("invalid phys memory value"), + }, + page_table_recursive: match page_table_recursive_some { + [0] if page_table_recursive == [0; 9] => Option::None, + [1] => Option::Some(Mapping::deserialize(&page_table_recursive)?), + _ => return Err("invalid page table recursive value"), + }, + aslr: match alsr { + 1 => true, + 0 => false, + _ => return Err("invalid aslr value"), + }, + dynamic_range_start: match dynamic_range_start_some { + [0] if dynamic_range_start == [0; 8] => Option::None, + [1] => Option::Some(u64::from_le_bytes(dynamic_range_start)), + _ => return Err("invalid dynamic range start value"), + }, + dynamic_range_end: match dynamic_range_end_some { + [0] if dynamic_range_end == [0; 8] => Option::None, + [1] => Option::Some(u64::from_le_bytes(dynamic_range_end)), + _ => return Err("invalid dynamic range end value"), + }, + ramdisk_memory: Mapping::deserialize(&ramdisk_memory)?, + }; + (mappings, s) + }; + + let (frame_buffer, s) = { + let (&min_framebuffer_height_some, s) = split_array_ref(s); + let (&min_framebuffer_height, s) = split_array_ref(s); + let (&min_framebuffer_width_some, s) = split_array_ref(s); + let (&min_framebuffer_width, s) = split_array_ref(s); + + let frame_buffer = FrameBuffer { + minimum_framebuffer_height: match min_framebuffer_height_some { + [0] if min_framebuffer_height == [0; 8] => Option::None, + [1] => Option::Some(u64::from_le_bytes(min_framebuffer_height)), + _ => return Err("minimum_framebuffer_height invalid"), + }, + minimum_framebuffer_width: match min_framebuffer_width_some { + [0] if min_framebuffer_width == [0; 8] => Option::None, + [1] => Option::Some(u64::from_le_bytes(min_framebuffer_width)), + _ => return Err("minimum_framebuffer_width invalid"), + }, + }; + (frame_buffer, s) + }; + + if !s.is_empty() { + return Err("unexpected rest"); + } + + Ok(Self { + version, + kernel_stack_size: u64::from_le_bytes(kernel_stack_size), + mappings, + frame_buffer, + }) + } + + #[cfg(test)] + fn random() -> Self { + Self { + version: ApiVersion::random(), + mappings: Mappings::random(), + kernel_stack_size: rand::random(), + frame_buffer: FrameBuffer::random(), + } + } +} + +impl Default for BootloaderConfig { + fn default() -> Self { + Self::new_default() + } +} + +/// A semver-compatible version. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[repr(C)] +pub struct ApiVersion { + /// Bootloader version (major). + version_major: u16, + /// Bootloader version (minor). + version_minor: u16, + /// Bootloader version (patch). + version_patch: u16, + /// Whether the bootloader API version is a pre-release. + /// + /// We can't store the full prerelease string of the version number since it could be + /// arbitrarily long. + pre_release: bool, +} + +impl ApiVersion { + pub(crate) const fn new_default() -> Self { + Self { + version_major: version_info::VERSION_MAJOR, + version_minor: version_info::VERSION_MINOR, + version_patch: version_info::VERSION_PATCH, + pre_release: version_info::VERSION_PRE, + } + } + + #[cfg(test)] + fn random() -> ApiVersion { + Self { + version_major: rand::random(), + version_minor: rand::random(), + version_patch: rand::random(), + pre_release: rand::random(), + } + } + + /// Returns the major version number. + pub fn version_major(&self) -> u16 { + self.version_major + } + + /// Returns the minor version number. + pub fn version_minor(&self) -> u16 { + self.version_minor + } + + /// Returns the patch version number. + pub fn version_patch(&self) -> u16 { + self.version_patch + } + + /// Returns whether this version is a pre-release, e.g., an alpha version. + pub fn pre_release(&self) -> bool { + self.pre_release + } +} + +impl Default for ApiVersion { + fn default() -> Self { + Self::new_default() + } +} + +/// Allows to configure the virtual memory mappings created by the bootloader. +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub struct Mappings { + /// Configures how the kernel stack should be mapped. + /// + /// If a fixed address is set, it must be page aligned. + /// + /// Note that the first page of the kernel stack is intentionally left unmapped + /// to act as a guard page. This ensures that a page fault occurs on a stack + /// overflow. For example, setting the kernel stack address to + /// `FixedAddress(0xf_0000_0000)` will result in a guard page at address + /// `0xf_0000_0000` and the kernel stack starting at address `0xf_0000_1000`. + pub kernel_stack: Mapping, + /// Configures the base address of the kernel. + /// + /// If a fixed address is set, it must be paged aligned and the kernel must be + /// a position-independent exectuable. + pub kernel_base: Mapping, + /// Specifies where the [`crate::BootInfo`] struct should be placed in virtual memory. + pub boot_info: Mapping, + /// Specifies the mapping of the frame buffer memory region. + pub framebuffer: Mapping, + /// The bootloader supports mapping the whole physical memory into the virtual address + /// space at some offset. This is useful for accessing and modifying the page tables set + /// up by the bootloader. + /// + /// This mapping will go from physical address `0x0` to whichever is larger: + /// - The end of the last region in the BIOS/UEFI memory map + /// - The address `0x1_0000_0000` (such that at least 4 GiB of physical memory are always mapped). + /// This is to ensure that useful MMIO regions (local APIC, I/O APIC, PCI bars) are + /// accessible to the kernel even if less physical memory than that is on the system. + /// + /// Defaults to `None`, i.e. no mapping of the physical memory. + pub physical_memory: Option, + /// As an alternative to mapping the whole physical memory (see [`Self::physical_memory`]), + /// the bootloader also has support for setting up a + /// [recursive level 4 page table](https://os.phil-opp.com/paging-implementation/#recursive-page-tables). + /// + /// Defaults to `None`, i.e. no recursive mapping. + pub page_table_recursive: Option, + /// Whether to randomize non-statically configured addresses. + /// The kernel base address will be randomized when it's compiled as + /// a position independent executable. + /// + /// Defaults to `false`. + pub aslr: bool, + /// The lowest virtual address for dynamic addresses. + /// + /// Defaults to `0`. + pub dynamic_range_start: Option, + /// The highest virtual address for dynamic addresses. + /// + /// Defaults to `0xffff_ffff_ffff_f000`. + pub dynamic_range_end: Option, + /// Virtual address to map ramdisk image, if present on disk + /// Defaults to dynamic + pub ramdisk_memory: Mapping, +} + +impl Mappings { + /// Creates a new mapping configuration with dynamic mapping for kernel, boot info and + /// frame buffer. Neither physical memory mapping nor recursive page table creation are + /// enabled. + pub const fn new_default() -> Self { + Self { + kernel_stack: Mapping::new_default(), + kernel_base: Mapping::new_default(), + boot_info: Mapping::new_default(), + framebuffer: Mapping::new_default(), + physical_memory: Option::None, + page_table_recursive: Option::None, + aslr: false, + dynamic_range_start: None, + dynamic_range_end: None, + ramdisk_memory: Mapping::new_default(), + } + } + + #[cfg(test)] + fn random() -> Mappings { + let phys = rand::random(); + let recursive = rand::random(); + Self { + kernel_stack: Mapping::random(), + kernel_base: Mapping::random(), + boot_info: Mapping::random(), + framebuffer: Mapping::random(), + physical_memory: if phys { + Option::Some(Mapping::random()) + } else { + Option::None + }, + page_table_recursive: if recursive { + Option::Some(Mapping::random()) + } else { + Option::None + }, + aslr: rand::random(), + dynamic_range_start: if rand::random() { + Option::Some(rand::random()) + } else { + Option::None + }, + dynamic_range_end: if rand::random() { + Option::Some(rand::random()) + } else { + Option::None + }, + ramdisk_memory: Mapping::random(), + } + } +} + +/// Specifies how the bootloader should map a memory region into the virtual address space. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Mapping { + /// Look for an unused virtual memory region at runtime. + Dynamic, + /// Try to map the region at the given virtual address. + /// + /// The given virtual address must be page-aligned. + /// + /// This setting can lead to runtime boot errors if the given address is not aligned, + /// already in use, or invalid for other reasons. + FixedAddress(u64), +} + +impl Mapping { + /// Creates a new [`Mapping::Dynamic`]. + /// + /// This function has identical results as [`Default::default`], the only difference is + /// that this is a `const` function. + pub const fn new_default() -> Self { + Self::Dynamic + } + + #[cfg(test)] + fn random() -> Mapping { + let fixed = rand::random(); + if fixed { + Self::Dynamic + } else { + Self::FixedAddress(rand::random()) + } + } + + const fn serialize(&self) -> [u8; 9] { + match self { + Mapping::Dynamic => [0; 9], + Mapping::FixedAddress(addr) => concat_1_8([1], addr.to_le_bytes()), + } + } + + fn deserialize(serialized: &[u8; 9]) -> Result { + let (&variant, s) = split_array_ref(serialized); + let (&addr, s) = split_array_ref(s); + if !s.is_empty() { + return Err("invalid mapping format"); + } + + match variant { + [0] if addr == [0; 8] => Ok(Mapping::Dynamic), + [1] => Ok(Mapping::FixedAddress(u64::from_le_bytes(addr))), + _ => Err("invalid mapping value"), + } + } +} + +impl Default for Mapping { + fn default() -> Self { + Self::new_default() + } +} + +/// Configuration for the frame buffer used for graphical output. +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub struct FrameBuffer { + /// Instructs the bootloader to set up a framebuffer format that has at least the given height. + /// + /// If this is not possible, the bootloader will fall back to a smaller format. + pub minimum_framebuffer_height: Option, + /// Instructs the bootloader to set up a framebuffer format that has at least the given width. + /// + /// If this is not possible, the bootloader will fall back to a smaller format. + pub minimum_framebuffer_width: Option, +} + +impl FrameBuffer { + /// Creates a default configuration without any requirements. + pub const fn new_default() -> Self { + Self { + minimum_framebuffer_height: Option::None, + minimum_framebuffer_width: Option::None, + } + } + + #[cfg(test)] + fn random() -> FrameBuffer { + Self { + minimum_framebuffer_height: if rand::random() { + Option::Some(rand::random()) + } else { + Option::None + }, + minimum_framebuffer_width: if rand::random() { + Option::Some(rand::random()) + } else { + Option::None + }, + } + } +} + +/// Taken from https://github.com/rust-lang/rust/blob/e100ec5bc7cd768ec17d75448b29c9ab4a39272b/library/core/src/slice/mod.rs#L1673-L1677 +/// +/// TODO replace with `split_array` feature in stdlib as soon as it's stabilized, +/// see https://github.com/rust-lang/rust/issues/90091 +fn split_array_ref(slice: &[T]) -> (&[T; N], &[T]) { + let (a, b) = slice.split_at(N); + // SAFETY: a points to [T; N]? Yes it's [T] of length N (checked by split_at) + unsafe { (&*(a.as_ptr() as *const [T; N]), b) } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mapping_serde() { + for _ in 0..10000 { + let config = Mapping::random(); + assert_eq!(Mapping::deserialize(&config.serialize()), Ok(config)); + } + } + + #[test] + fn config_serde() { + for _ in 0..10000 { + let config = BootloaderConfig::random(); + assert_eq!( + BootloaderConfig::deserialize(&config.serialize()), + Ok(config) + ); + } + } +} diff --git a/api/src/info.rs b/api/src/info.rs new file mode 100644 index 00000000..d934c532 --- /dev/null +++ b/api/src/info.rs @@ -0,0 +1,375 @@ +use core::{ops, slice}; + +use crate::config::ApiVersion; + +/// This structure represents the information that the bootloader passes to the kernel. +/// +/// The information is passed as an argument to the entry point. The entry point function must +/// have the following signature: +/// +/// ``` +/// # use bootloader_api::BootInfo; +/// # type _SIGNATURE = +/// extern "C" fn(boot_info: &'static mut BootInfo) -> !; +/// ``` +/// +/// Note that no type checking occurs for the entry point function, so be careful to +/// use the correct argument types. To ensure that the entry point function has the correct +/// signature, use the [`entry_point`] macro. +#[derive(Debug)] +#[repr(C)] +#[non_exhaustive] +pub struct BootInfo { + /// The version of the `bootloader_api` crate. Must match the `bootloader` version. + pub api_version: ApiVersion, + /// A map of the physical memory regions of the underlying machine. + /// + /// The bootloader queries this information from the BIOS/UEFI firmware and translates this + /// information to Rust types. It also marks any memory regions that the bootloader uses in + /// the memory map before passing it to the kernel. Regions marked as usable can be freely + /// used by the kernel. + pub memory_regions: MemoryRegions, + /// Information about the framebuffer for screen output if available. + pub framebuffer: Optional, + /// The virtual address at which the mapping of the physical memory starts. + /// + /// Physical addresses can be converted to virtual addresses by adding this offset to them. + /// + /// The mapping of the physical memory allows to access arbitrary physical frames. Accessing + /// frames that are also mapped at other virtual addresses can easily break memory safety and + /// cause undefined behavior. Only frames reported as `USABLE` by the memory map in the `BootInfo` + /// can be safely accessed. + /// + /// Only available if the `map-physical-memory` config option is enabled. + pub physical_memory_offset: Optional, + /// The virtual address of the recursively mapped level 4 page table. + /// + /// Only available if the `map-page-table-recursively` config option is enabled. + pub recursive_index: Optional, + /// The address of the `RSDP` data structure, which can be use to find the ACPI tables. + /// + /// This field is `None` if no `RSDP` was found (for BIOS) or reported (for UEFI). + pub rsdp_addr: Optional, + /// The thread local storage (TLS) template of the kernel executable, if present. + pub tls_template: Optional, + /// Ramdisk address, if loaded + pub ramdisk_addr: Optional, + /// Ramdisk image size, set to 0 if addr is None + pub ramdisk_len: u64, + /// Physical address of the kernel ELF in memory. + pub kernel_addr: u64, + /// Size of the kernel ELF in memory. + pub kernel_len: u64, + /// Virtual address of the loaded kernel image. + pub kernel_image_offset: u64, + + #[doc(hidden)] + pub _test_sentinel: u64, +} + +impl BootInfo { + /// Create a new boot info structure with the given memory map. + /// + /// The other fields are initialized with default values. + pub fn new(memory_regions: MemoryRegions) -> Self { + Self { + api_version: ApiVersion::new_default(), + memory_regions, + framebuffer: Optional::None, + physical_memory_offset: Optional::None, + recursive_index: Optional::None, + rsdp_addr: Optional::None, + tls_template: Optional::None, + ramdisk_addr: Optional::None, + ramdisk_len: 0, + kernel_addr: 0, + kernel_len: 0, + kernel_image_offset: 0, + _test_sentinel: 0, + } + } +} + +/// FFI-safe slice of [`MemoryRegion`] structs, semantically equivalent to +/// `&'static mut [MemoryRegion]`. +/// +/// This type implements the [`Deref`][core::ops::Deref] and [`DerefMut`][core::ops::DerefMut] +/// traits, so it can be used like a `&mut [MemoryRegion]` slice. It also implements [`From`] +/// and [`Into`] for easy conversions from and to `&'static mut [MemoryRegion]`. +#[derive(Debug)] +#[repr(C)] +pub struct MemoryRegions { + pub(crate) ptr: *mut MemoryRegion, + pub(crate) len: usize, +} + +impl ops::Deref for MemoryRegions { + type Target = [MemoryRegion]; + + fn deref(&self) -> &Self::Target { + unsafe { slice::from_raw_parts(self.ptr, self.len) } + } +} + +impl ops::DerefMut for MemoryRegions { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { slice::from_raw_parts_mut(self.ptr, self.len) } + } +} + +impl From<&'static mut [MemoryRegion]> for MemoryRegions { + fn from(regions: &'static mut [MemoryRegion]) -> Self { + MemoryRegions { + ptr: regions.as_mut_ptr(), + len: regions.len(), + } + } +} + +impl From for &'static mut [MemoryRegion] { + fn from(regions: MemoryRegions) -> &'static mut [MemoryRegion] { + unsafe { slice::from_raw_parts_mut(regions.ptr, regions.len) } + } +} + +unsafe impl Send for MemoryRegions {} +unsafe impl Sync for MemoryRegions {} + +/// Represent a physical memory region. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[repr(C)] +pub struct MemoryRegion { + /// The physical start address of the region. + pub start: u64, + /// The physical end address (exclusive) of the region. + pub end: u64, + /// The memory type of the memory region. + /// + /// Only [`Usable`][MemoryRegionKind::Usable] regions can be freely used. + pub kind: MemoryRegionKind, +} + +impl MemoryRegion { + /// Creates a new empty memory region (with length 0). + pub const fn empty() -> Self { + MemoryRegion { + start: 0, + end: 0, + kind: MemoryRegionKind::Bootloader, + } + } +} + +/// Represents the different types of memory. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[non_exhaustive] +#[repr(C)] +pub enum MemoryRegionKind { + /// Unused conventional memory, can be used by the kernel. + Usable, + /// Memory mappings created by the bootloader, including the page table and boot info mappings. + /// + /// This memory should _not_ be used by the kernel. + Bootloader, + /// An unknown memory region reported by the UEFI firmware. + /// + /// Contains the UEFI memory type tag. + UnknownUefi(u32), + /// An unknown memory region reported by the BIOS firmware. + /// + /// Contains the E820 memory type. + UnknownBios(u32), +} + +/// A pixel-based framebuffer that controls the screen output. +#[derive(Debug)] +#[repr(C)] +pub struct FrameBuffer { + pub(crate) buffer_start: u64, + pub(crate) info: FrameBufferInfo, +} + +impl FrameBuffer { + /// Creates a new framebuffer instance. + /// + /// ## Safety + /// + /// The given start address and info must describe a valid, accessible, and unaliased + /// framebuffer. + pub unsafe fn new(buffer_start: u64, info: FrameBufferInfo) -> Self { + Self { buffer_start, info } + } + + /// Returns the raw bytes of the framebuffer as slice. + pub fn buffer(&self) -> &[u8] { + unsafe { self.create_buffer() } + } + + /// Returns the raw bytes of the framebuffer as mutable slice. + pub fn buffer_mut(&mut self) -> &mut [u8] { + unsafe { self.create_buffer_mut() } + } + + /// Converts the frame buffer to a raw byte slice. + /// + /// The same as `buffer_mut()` but takes the ownership and returns the + /// mutable buffer with a `'static` lifetime. + pub fn into_buffer(self) -> &'static mut [u8] { + unsafe { self.create_buffer_mut() } + } + + unsafe fn create_buffer<'a>(&self) -> &'a [u8] { + unsafe { slice::from_raw_parts(self.buffer_start as *const u8, self.info.byte_len) } + } + + unsafe fn create_buffer_mut<'a>(&self) -> &'a mut [u8] { + unsafe { slice::from_raw_parts_mut(self.buffer_start as *mut u8, self.info.byte_len) } + } + + /// Returns layout and pixel format information of the framebuffer. + pub fn info(&self) -> FrameBufferInfo { + self.info + } +} + +/// Describes the layout and pixel format of a framebuffer. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct FrameBufferInfo { + /// The total size in bytes. + pub byte_len: usize, + /// The width in pixels. + pub width: usize, + /// The height in pixels. + pub height: usize, + /// The color format of each pixel. + pub pixel_format: PixelFormat, + /// The number of bytes per pixel. + pub bytes_per_pixel: usize, + /// Number of pixels between the start of a line and the start of the next. + /// + /// Some framebuffers use additional padding at the end of a line, so this + /// value might be larger than `horizontal_resolution`. It is + /// therefore recommended to use this field for calculating the start address of a line. + pub stride: usize, +} + +/// Color format of pixels in the framebuffer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +#[repr(C)] +pub enum PixelFormat { + /// One byte red, then one byte green, then one byte blue. + /// + /// Length might be larger than 3, check [`bytes_per_pixel`][FrameBufferInfo::bytes_per_pixel] + /// for this. + Rgb, + /// One byte blue, then one byte green, then one byte red. + /// + /// Length might be larger than 3, check [`bytes_per_pixel`][FrameBufferInfo::bytes_per_pixel] + /// for this. + Bgr, + /// A single byte, representing the grayscale value. + /// + /// Length might be larger than 1, check [`bytes_per_pixel`][FrameBufferInfo::bytes_per_pixel] + /// for this. + U8, + /// Unknown pixel format. + Unknown { + /// Bit offset of the red value. + red_position: u8, + /// Bit offset of the green value. + green_position: u8, + /// Bit offset of the blue value. + blue_position: u8, + }, +} + +/// Information about the thread local storage (TLS) template. +/// +/// This template can be used to set up thread local storage for threads. For +/// each thread, a new memory location of size `mem_size` must be initialized. +/// Then the first `file_size` bytes of this template needs to be copied to the +/// location. The additional `mem_size - file_size` bytes must be initialized with +/// zero. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct TlsTemplate { + /// The virtual start address of the thread local storage template. + pub start_addr: u64, + /// The number of data bytes in the template. + /// + /// Corresponds to the length of the `.tdata` section. + pub file_size: u64, + /// The total number of bytes that the TLS segment should have in memory. + /// + /// Corresponds to the combined length of the `.tdata` and `.tbss` sections. + pub mem_size: u64, +} + +/// FFI-safe variant of [`Option`]. +/// +/// Implements the [`From`] and [`Into`] traits for easy conversion to and from [`Option`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(C)] +pub enum Optional { + /// Some value `T` + Some(T), + /// No value + None, +} + +impl Optional { + /// Converts the `Optional` to an [`Option`]. + pub fn into_option(self) -> Option { + self.into() + } + + /// Converts from `&Optional` to `Option<&T>`. + /// + /// For convenience, this method directly performs the conversion to the standard + /// [`Option`] type. + pub const fn as_ref(&self) -> Option<&T> { + match self { + Self::Some(x) => Some(x), + Self::None => None, + } + } + + /// Converts from `&mut Optional` to `Option<&mut T>`. + /// + /// For convenience, this method directly performs the conversion to the standard + /// [`Option`] type. + pub fn as_mut(&mut self) -> Option<&mut T> { + match self { + Self::Some(x) => Some(x), + Self::None => None, + } + } + + /// Takes the value out of the `Optional`, leaving a `None` in its place. + pub fn take(&mut self) -> Option { + core::mem::replace(self, Optional::None).into_option() + } +} + +impl From> for Optional { + fn from(v: Option) -> Self { + match v { + Some(v) => Optional::Some(v), + None => Optional::None, + } + } +} + +impl From> for Option { + fn from(optional: Optional) -> Option { + match optional { + Optional::Some(v) => Some(v), + Optional::None => None, + } + } +} + +/// Check that bootinfo is FFI-safe +extern "C" fn _assert_ffi(_boot_info: BootInfo) {} diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 00000000..586ae1d0 --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1,147 @@ +//! Provides the interface to make kernels compatible with the +//! [**`bootloader`**](https://docs.rs/bootloader/latest/bootloader/) crate. + +#![cfg_attr(not(test), no_std)] +#![deny(unsafe_op_in_unsafe_fn)] +#![warn(missing_docs)] + +pub use self::{config::BootloaderConfig, info::BootInfo}; + +/// Allows to configure the system environment set up by the bootloader. +pub mod config; +/// Contains the boot information struct sent by the bootloader to the kernel on startup. +pub mod info; + +mod concat { + include!(concat!(env!("OUT_DIR"), "/concat.rs")); +} + +mod version_info { + include!(concat!(env!("OUT_DIR"), "/version_info.rs")); +} + +/// Defines the entry point function. +/// +/// The function must have the signature `fn(&'static mut BootInfo) -> !`. +/// +/// This macro just creates a function named `_start`, which the linker will use as the entry +/// point. The advantage of using this macro instead of providing an own `_start` function is +/// that the macro ensures that the function and argument types are correct. +/// +/// ## Configuration +/// +/// This macro supports an optional second parameter to configure how the bootloader should +/// boot the kernel. The second parameter needs to be given as `config = ...` and be of type +/// [`&BootloaderConfig`](crate::BootloaderConfig). If not given, the configuration defaults to +/// [`BootloaderConfig::new_default`](crate::BootloaderConfig::new_default). +/// +/// ## Examples +/// +/// - With default configuration: +/// +/// ```no_run +/// #![no_std] +/// #![no_main] +/// # #![feature(lang_items)] +/// +/// bootloader_api::entry_point!(main); +/// +/// fn main(bootinfo: &'static mut bootloader_api::BootInfo) -> ! { +/// loop {} +/// } +/// +/// #[panic_handler] +/// fn panic(_info: &core::panic::PanicInfo) -> ! { +/// loop {} +/// } +/// +/// # #[lang = "eh_personality"] fn eh_personality() {} // not needed when disabling unwinding +/// ``` +/// +/// The name of the entry point function does not matter. For example, instead of `main`, we +/// could also name it `fn my_entry_point(...) -> !`. We would then need to specify +/// `entry_point!(my_entry_point)` of course. +/// +/// - With custom configuration: +/// +/// ```no_run +/// #![no_std] +/// #![no_main] +/// # #![feature(lang_items)] +/// +/// use bootloader_api::{entry_point, BootloaderConfig}; +/// +/// pub static BOOTLOADER_CONFIG: BootloaderConfig = { +/// let mut config = BootloaderConfig::new_default(); +/// config.kernel_stack_size = 90 * 1024; +/// config +/// }; +/// +/// entry_point!(main, config = &BOOTLOADER_CONFIG); +/// +/// fn main(bootinfo: &'static mut bootloader_api::BootInfo) -> ! { +/// loop {} +/// } +/// +/// #[panic_handler] +/// fn panic(_info: &core::panic::PanicInfo) -> ! { +/// loop {} +/// } +/// +/// # #[lang = "eh_personality"] fn eh_personality() {} // not needed when disabling unwinding +/// ``` +/// +/// ## Implementation Notes +/// +/// - **Start function:** The `entry_point` macro generates a small wrapper function named +/// `_start` (without name mangling) that becomes the actual entry point function of the +/// executable. This function doesn't do anything itself, it just calls into the function +/// that is provided as macro argument. The purpose of this function is to use the correct +/// ABI and parameter types required by this crate. A user-provided `_start` function could +/// silently become incompatible on dependency updates since the Rust compiler cannot +/// check the signature of custom entry point functions. +/// - **Configuration:** Behind the scenes, the configuration struct is serialized using +/// [`BootloaderConfig::serialize`](crate::BootloaderConfig::serialize). The resulting byte +/// array is then stored as a static variable annotated with +/// `#[link_section = ".bootloader-config"]`, which instructs the Rust compiler to store it +/// in a special section of the resulting ELF executable. From there, the bootloader will +/// automatically read it when loading the kernel. +#[cfg(target_arch = "x86_64")] +#[macro_export] +macro_rules! entry_point { + ($path:path) => { + $crate::entry_point!($path, config = &$crate::BootloaderConfig::new_default()); + }; + ($path:path, config = $config:expr) => { + const _: () = { + #[link_section = ".bootloader-config"] + pub static __BOOTLOADER_CONFIG: [u8; $crate::BootloaderConfig::SERIALIZED_LEN] = { + // validate the type + let config: &$crate::BootloaderConfig = $config; + config.serialize() + }; + + // Workaround for https://github.com/rust-osdev/bootloader/issues/427 + static __BOOTLOADER_CONFIG_REF: &[u8; $crate::BootloaderConfig::SERIALIZED_LEN] = + &__BOOTLOADER_CONFIG; + + #[export_name = "_start"] + pub extern "C" fn __impl_start(boot_info: &'static mut $crate::BootInfo) -> ! { + // validate the signature of the program entry point + let f: fn(&'static mut $crate::BootInfo) -> ! = $path; + + // ensure that the config is used so that the linker keeps it + $crate::__force_use(&__BOOTLOADER_CONFIG_REF); + + f(boot_info) + } + }; + }; +} + +#[doc(hidden)] +#[cfg(target_arch = "x86_64")] +pub fn __force_use(slice: &&[u8; BootloaderConfig::SERIALIZED_LEN]) { + let force_use = slice as *const _ as usize; + unsafe { core::arch::asm!("add {0}, 0", in(reg) force_use, options(nomem, nostack)) }; +} diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 6a6fa827..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,119 +0,0 @@ -# Documentation: https://aka.ms/yaml - -trigger: - batch: true - branches: - include: - # This is where pull requests from "bors r+" are built. - - staging - # This is where pull requests from "bors try" are built. - - trying - # Build pull requests. - - master - -strategy: - matrix: - linux: - image_name: 'ubuntu-16.04' - rustup_toolchain: nightly - mac: - image_name: 'macos-10.13' - rustup_toolchain: nightly - windows: - image_name: 'vs2017-win2016' - rustup_toolchain: nightly - -pool: - vmImage: $(image_name) - -steps: -- bash: | - echo "Hello world from $AGENT_NAME running on $AGENT_OS" - echo "Reason: $BUILD_REASON" - case "$BUILD_REASON" in - "Manual") echo "$BUILD_REQUESTEDFOR manually queued the build." ;; - "PullRequest") echo "This is a CI build for a pull request on $BUILD_REQUESTEDFOR." ;; - "IndividualCI") echo "This is a CI build for $BUILD_REQUESTEDFOR." ;; - "BatchedCI") echo "This is a batched CI build for $BUILD_REQUESTEDFOR." ;; - *) "$BUILD_REASON" ;; - esac - displayName: 'Build Info' - continueOnError: true - -- script: | - set -euxo pipefail - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUSTUP_TOOLCHAIN - echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin" - condition: or(eq( variables['Agent.OS'], 'Linux' ), eq( variables['Agent.OS'], 'Darwin' )) - displayName: 'Install Rust (Linux/macOS)' - -- script: curl -sSf -o rustup-init.exe https://win.rustup.rs && rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN% - condition: eq( variables['Agent.OS'], 'Windows_NT' ) - displayName: 'Install Rust (Windows)' - -- script: | - echo ##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin - condition: eq( variables['Agent.OS'], 'Windows_NT' ) - displayName: 'Add ~/.cargo/bin to PATH (Windows)' - -- script: | - rustc -Vv - cargo -V - displayName: 'Print Rust Version' - continueOnError: true - -- script: rustup component add rust-src llvm-tools-preview - displayName: 'Install Rustup Components' - -- script: cargo install cargo-xbuild cargo-binutils --debug - displayName: 'Install cargo-xbuild cargo-binutils' - -- script: sudo apt update && sudo apt install qemu-system-x86 - condition: eq( variables['Agent.OS'], 'Linux' ) - displayName: 'Install QEMU (Linux)' - -- script: | - set -euxo pipefail - export HOMEBREW_NO_AUTO_UPDATE=1 - export HOMEBREW_NO_BOTTLE_SOURCE_FALLBACK=1 - export HOMEBREW_NO_INSTALL_CLEANUP=1 - brew install qemu - condition: eq( variables['Agent.OS'], 'Darwin' ) - displayName: 'Install QEMU (macOS)' - -- script: | - choco install qemu --limit-output --no-progress - echo ##vso[task.setvariable variable=PATH;]%PATH%;C:\Program Files\qemu - set PATH=%PATH%;C:\Program Files\qemu - qemu-system-x86_64 --version - condition: eq( variables['Agent.OS'], 'Windows_NT' ) - failOnStderr: true - displayName: 'Install QEMU (Windows)' - -- script: cargo xbuild - workingDirectory: test-kernel - displayName: 'Build Test Kernel' - -- script: cargo xbuild --bin bootloader --features binary --release - displayName: 'Build Bootloader' - env: - KERNEL: "test-kernel/target/x86_64-test-kernel/debug/test-kernel" - KERNEL_MANIFEST: "test-kernel/Cargo.toml" - -- script: cargo objcopy -- -I elf64-x86-64 -O binary --binary-architecture=i386:x86-64 target/x86_64-bootloader/release/bootloader target/x86_64-bootloader/release/bootloader.bin - displayName: 'Convert Bootloader ELF to Binary' - -- bash: | - qemu-system-x86_64 -drive format=raw,file=target/x86_64-bootloader/release/bootloader.bin -device isa-debug-exit,iobase=0xf4,iosize=0x04 -display none - if [ $? -eq 123 ]; then (exit 0); else (exit 1); fi - displayName: 'Test Bootloader' - -- script: cargo xbuild - workingDirectory: example-kernel - displayName: 'Build Example Kernel' - -- script: rustup component add rustfmt - displayName: 'Install Rustfmt' - -- script: cargo fmt -- --check - displayName: 'Check Formatting' diff --git a/real_mode/first_stage/.gitignore b/bios/boot_sector/.gitignore similarity index 100% rename from real_mode/first_stage/.gitignore rename to bios/boot_sector/.gitignore diff --git a/bios/boot_sector/Cargo.toml b/bios/boot_sector/Cargo.toml new file mode 100644 index 00000000..ca870600 --- /dev/null +++ b/bios/boot_sector/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bootloader-x86_64-bios-boot-sector" +version.workspace = true +authors = ["Philipp Oppermann "] +edition = "2021" +license.workspace = true +repository.workspace = true +description = "BIOS boot sector for the `bootloader` crate" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +# This currently causes a cargo warning, but it is required for publishing to crates.io. +# See https://github.com/rust-lang/cargo/issues/8264 for details. +[profile.stage-1] +inherits = "release" +opt-level = "s" +lto = true +codegen-units = 1 +debug = false +overflow-checks = false diff --git a/bios/boot_sector/README.md b/bios/boot_sector/README.md new file mode 100644 index 00000000..a783d80e --- /dev/null +++ b/bios/boot_sector/README.md @@ -0,0 +1,16 @@ +# First Stage: Bootsector + +This executable needs to fit into the 512-byte boot sector, so we need to use all kinds of tricks to keep the size down. + +## Build Commands + +1. `cargo build --profile=stage-1 -Zbuild-std=core --target ../../i386-code16-boot-sector.json -Zbuild-std-features=compiler-builtins-mem` +2. `objcopy -I elf32-i386 -O binary ../../target/i386-code16-boot-sector/stage-1/bootloader-x86_64-bios-boot-sector ../../target/disk_image.img` + +To run in QEMU: + +- `qemu-system-x86_64 -drive format=raw,file=../../target/disk_image.img` + +To print the contents of the ELF file, e.g. for trying to bring the size down: + +- `objdump -xsdS -M i8086,intel ../../target/i386-code16-boot-sector/stage-1/bootloader-x86_64-bios-boot-sector` diff --git a/real_mode/first_stage/16-bit-linker.ld b/bios/boot_sector/boot-sector-link.ld similarity index 63% rename from real_mode/first_stage/16-bit-linker.ld rename to bios/boot_sector/boot-sector-link.ld index 5cf5db94..26995bc2 100644 --- a/real_mode/first_stage/16-bit-linker.ld +++ b/bios/boot_sector/boot-sector-link.ld @@ -28,12 +28,21 @@ SECTIONS { _mbr_end = .; . = 0x7c00 + 446; + _partition_table = .; .partition_table : { - SHORT(0x0000) /* partition table entry 0 */ - SHORT(0x0000) /* partition table entry 1 */ - SHORT(0x0000) /* partition table entry 2 */ - SHORT(0x0000) /* partition table entry 3 */ + /* partition table entry 0 */ + QUAD(0) + QUAD(0) + /* partition table entry 1 */ + QUAD(0) + QUAD(0) + /* partition table entry 2 */ + QUAD(0) + QUAD(0) + /* partition table entry 3 */ + QUAD(0) + QUAD(0) } . = 0x7c00 + 510; @@ -42,4 +51,6 @@ SECTIONS { { SHORT(0xaa55) /* magic number for bootable disk */ } + + _second_stage_start = .; } diff --git a/bios/boot_sector/build.rs b/bios/boot_sector/build.rs new file mode 100644 index 00000000..d657e5a4 --- /dev/null +++ b/bios/boot_sector/build.rs @@ -0,0 +1,9 @@ +use std::path::Path; + +fn main() { + let local_path = Path::new(env!("CARGO_MANIFEST_DIR")); + println!( + "cargo:rustc-link-arg-bins=--script={}", + local_path.join("boot-sector-link.ld").display() + ) +} diff --git a/real_mode/first_stage/src/boot.s b/bios/boot_sector/src/boot.s similarity index 88% rename from real_mode/first_stage/src/boot.s rename to bios/boot_sector/src/boot.s index 992ce693..1544cac2 100644 --- a/real_mode/first_stage/src/boot.s +++ b/bios/boot_sector/src/boot.s @@ -36,13 +36,18 @@ check_int13h_extensions: mov bx, 0x55aa # dl contains drive number int 0x13 - jc fail + jnc .int13_pass + call fail +.int13_pass: pop ax # pop error code again rust: # push arguments push dx # disk number call first_stage + # Fail code if first stage returns + push 'x' + call fail spin: hlt diff --git a/bios/boot_sector/src/dap.rs b/bios/boot_sector/src/dap.rs new file mode 100644 index 00000000..11e8a853 --- /dev/null +++ b/bios/boot_sector/src/dap.rs @@ -0,0 +1,57 @@ +use core::arch::asm; + +#[repr(C, packed)] +#[allow(dead_code)] // the structure format is defined by the hardware +pub struct DiskAddressPacket { + /// Size of the DAP structure + packet_size: u8, + /// always zero + zero: u8, + /// Number of sectors to transfer + number_of_sectors: u16, + /// Offset to memory buffer + offset: u16, + /// Segment of memory buffer + segment: u16, + /// Start logical block address + start_lba: u64, +} + +impl DiskAddressPacket { + pub fn from_lba( + start_lba: u64, + number_of_sectors: u16, + target_offset: u16, + target_segment: u16, + ) -> Self { + Self { + packet_size: 0x10, + zero: 0, + number_of_sectors, + offset: target_offset, + segment: target_segment, + start_lba, + } + } + + pub unsafe fn perform_load(&self, disk_number: u16) { + let self_addr = self as *const Self as u16; + unsafe { + asm!( + "push 'z'", // error code `z`, passed to `fail` on error + "mov {1:x}, si", // backup the `si` register, whose contents are required by LLVM + "mov si, {0:x}", + "int 0x13", + "jnc 2f", // carry is set on fail + "call fail", + "2:", + "pop si", // remove error code again + "mov si, {1:x}", // restore the `si` register to its prior state + in(reg) self_addr, + out(reg) _, + in("ax") 0x4200u16, + in("dx") disk_number, + ); + } + } +} diff --git a/real_mode/first_stage/src/fail.rs b/bios/boot_sector/src/fail.rs similarity index 91% rename from real_mode/first_stage/src/fail.rs rename to bios/boot_sector/src/fail.rs index 17fc1ee0..07766f40 100644 --- a/real_mode/first_stage/src/fail.rs +++ b/bios/boot_sector/src/fail.rs @@ -32,7 +32,7 @@ impl UnwrapOrFail for Result { pub extern "C" fn print_char(c: u8) { let ax = u16::from(c) | 0x0e00; unsafe { - asm!("int 0x10", in("ax") ax, in("bx") 0); + asm!("push bx", "mov bx, 0", "int 0x10", "pop bx", in("ax") ax); } } @@ -54,6 +54,7 @@ fn hlt() { } #[panic_handler] +#[cfg(not(test))] pub fn panic(_info: &core::panic::PanicInfo) -> ! { fail(b'P'); } diff --git a/bios/boot_sector/src/main.rs b/bios/boot_sector/src/main.rs new file mode 100644 index 00000000..ab10922e --- /dev/null +++ b/bios/boot_sector/src/main.rs @@ -0,0 +1,71 @@ +#![no_std] +#![no_main] +#![warn(unsafe_op_in_unsafe_fn)] + +use core::{arch::global_asm, slice}; +use fail::UnwrapOrFail; + +global_asm!(include_str!("boot.s")); + +mod dap; +mod fail; +mod mbr; + +extern "C" { + static _partition_table: u8; + static _second_stage_start: u8; +} + +unsafe fn partition_table_raw() -> *const u8 { + unsafe { &_partition_table } +} + +fn second_stage_start() -> *const () { + let ptr: *const u8 = unsafe { &_second_stage_start }; + ptr as *const () +} + +#[no_mangle] +pub extern "C" fn first_stage(disk_number: u16) { + // read partition table and look for second stage partition + let partition_table = unsafe { slice::from_raw_parts(partition_table_raw(), 16 * 4) }; + let second_stage_partition = mbr::get_partition(partition_table, 0); + + // load second stage partition into memory + let entry_point_address = second_stage_start() as u32; + + let mut start_lba = second_stage_partition.logical_block_address.into(); + let mut number_of_sectors = second_stage_partition.sector_count; + let mut target_addr = entry_point_address; + + loop { + let sectors = u32::min(number_of_sectors, 32) as u16; + let dap = dap::DiskAddressPacket::from_lba( + start_lba, + sectors, + (target_addr & 0b1111) as u16, + (target_addr >> 4).try_into().unwrap_or_fail(b'a'), + ); + unsafe { + dap.perform_load(disk_number); + } + + start_lba += u64::from(sectors); + number_of_sectors -= u32::from(sectors); + target_addr += u32::from(sectors) * 512; + + if number_of_sectors == 0 { + break; + } + } + + // jump to second stage + let second_stage_entry_point: extern "C" fn( + disk_number: u16, + partition_table_start: *const u8, + ) = unsafe { core::mem::transmute(entry_point_address as *const ()) }; + let partition_table_start = unsafe { partition_table_raw() }; + second_stage_entry_point(disk_number, partition_table_start); + + fail::fail(b'R'); +} diff --git a/bios/boot_sector/src/mbr.rs b/bios/boot_sector/src/mbr.rs new file mode 100644 index 00000000..05382d28 --- /dev/null +++ b/bios/boot_sector/src/mbr.rs @@ -0,0 +1,63 @@ +use super::fail::UnwrapOrFail; + +pub(crate) fn get_partition(partitions_raw: &[u8], index: usize) -> PartitionTableEntry { + const ENTRY_SIZE: usize = 16; + + let offset = index * ENTRY_SIZE; + let buffer = partitions_raw.get(offset..).unwrap_or_fail(b'c'); + + let bootable_raw = *buffer.first().unwrap_or_fail(b'd'); + let bootable = bootable_raw == 0x80; + + let partition_type = *buffer.get(4).unwrap_or_fail(b'e'); + + let lba = u32::from_le_bytes( + buffer + .get(8..) + .and_then(|s| s.get(..4)) + .and_then(|s| s.try_into().ok()) + .unwrap_or_fail(b'f'), + ); + let len = u32::from_le_bytes( + buffer + .get(12..) + .and_then(|s| s.get(..4)) + .and_then(|s| s.try_into().ok()) + .unwrap_or_fail(b'g'), + ); + PartitionTableEntry::new(bootable, partition_type, lba, len) +} + +/// An entry in a partition table. +/// +/// Based on https://docs.rs/mbr-nostd +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) struct PartitionTableEntry { + /// Whether this partition is a boot partition. + pub(crate) bootable: bool, + + /// The type of partition in this entry. + pub(crate) partition_type: u8, + + /// The index of the first block of this entry. + pub(crate) logical_block_address: u32, + + /// The total number of blocks in this entry. + pub(crate) sector_count: u32, +} + +impl PartitionTableEntry { + pub fn new( + bootable: bool, + partition_type: u8, + logical_block_address: u32, + sector_count: u32, + ) -> PartitionTableEntry { + PartitionTableEntry { + bootable, + partition_type, + logical_block_address, + sector_count, + } + } +} diff --git a/bios/common/Cargo.toml b/bios/common/Cargo.toml new file mode 100644 index 00000000..0417d017 --- /dev/null +++ b/bios/common/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "bootloader-x86_64-bios-common" +version.workspace = true +edition = "2021" +license.workspace = true +repository.workspace = true +description = "Common code for BIOS stages of the `bootloader` crate" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[features] +default = ["debug"] +debug = [] diff --git a/bios/common/src/lib.rs b/bios/common/src/lib.rs new file mode 100644 index 00000000..166e12df --- /dev/null +++ b/bios/common/src/lib.rs @@ -0,0 +1,71 @@ +#![no_std] + +pub mod racy_cell; + +#[cfg_attr(feature = "debug", derive(Debug))] +#[repr(C)] +pub struct BiosInfo { + pub stage_4: Region, + pub kernel: Region, + pub ramdisk: Region, + pub config_file: Region, + pub last_used_addr: u64, + pub framebuffer: BiosFramebufferInfo, + pub memory_map_addr: u32, + pub memory_map_len: u16, +} + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone, Copy)] +#[repr(C)] +pub struct BiosFramebufferInfo { + pub region: Region, + pub width: u16, + pub height: u16, + pub bytes_per_pixel: u8, + pub stride: u16, + pub pixel_format: PixelFormat, +} + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone, Copy)] +#[repr(C)] +pub struct Region { + pub start: u64, + pub len: u64, +} + +#[cfg_attr(feature = "debug", derive(Debug))] +#[derive(Clone, Copy)] +#[repr(C)] +pub enum PixelFormat { + Rgb, + Bgr, + Unknown { + red_position: u8, + green_position: u8, + blue_position: u8, + }, +} + +impl PixelFormat { + pub fn is_unknown(&self) -> bool { + match self { + PixelFormat::Rgb | PixelFormat::Bgr => false, + PixelFormat::Unknown { .. } => true, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct E820MemoryRegion { + pub start_addr: u64, + pub len: u64, + pub region_type: u32, + pub acpi_extended_attributes: u32, +} + +pub fn hlt() { + unsafe { core::arch::asm!("hlt") }; +} diff --git a/bios/common/src/racy_cell.rs b/bios/common/src/racy_cell.rs new file mode 100644 index 00000000..439d1b18 --- /dev/null +++ b/bios/common/src/racy_cell.rs @@ -0,0 +1,21 @@ +use core::cell::UnsafeCell; + +pub struct RacyCell(UnsafeCell); + +impl RacyCell { + pub const fn new(v: T) -> Self { + Self(UnsafeCell::new(v)) + } + + /// Gets a mutable pointer to the wrapped value. + /// + /// ## Safety + /// Ensure that the access is unique (no active references, mutable or not). + #[allow(clippy::mut_from_ref)] + pub unsafe fn get_mut(&self) -> &mut T { + unsafe { &mut *self.0.get() } + } +} + +unsafe impl Send for RacyCell where T: Send {} +unsafe impl Sync for RacyCell {} diff --git a/real_mode/real_mode/.gitignore b/bios/stage-2/.gitignore similarity index 100% rename from real_mode/real_mode/.gitignore rename to bios/stage-2/.gitignore diff --git a/bios/stage-2/Cargo.toml b/bios/stage-2/Cargo.toml new file mode 100644 index 00000000..4e39accb --- /dev/null +++ b/bios/stage-2/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "bootloader-x86_64-bios-stage-2" +version.workspace = true +authors = ["Philipp Oppermann "] +edition = "2021" +license.workspace = true +repository.workspace = true +description = "Second BIOS stage of the `bootloader` crate" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +mbr-nostd = "0.1.0" +byteorder = { version = "1.4.3", default-features = false } +bootloader-x86_64-bios-common = { workspace = true } + +# This currently causes a cargo warning, but it is required for publishing to crates.io. +# See https://github.com/rust-lang/cargo/issues/8264 for details. +[profile.stage-2] +inherits = "release" +opt-level = "s" +codegen-units = 1 +debug = false +overflow-checks = true diff --git a/bios/stage-2/build.rs b/bios/stage-2/build.rs new file mode 100644 index 00000000..674be6e9 --- /dev/null +++ b/bios/stage-2/build.rs @@ -0,0 +1,9 @@ +use std::path::Path; + +fn main() { + let local_path = Path::new(env!("CARGO_MANIFEST_DIR")); + println!( + "cargo:rustc-link-arg-bins=--script={}", + local_path.join("stage-2-link.ld").display() + ) +} diff --git a/real_mode/first_stage/src/dap.rs b/bios/stage-2/src/dap.rs similarity index 59% rename from real_mode/first_stage/src/dap.rs rename to bios/stage-2/src/dap.rs index 35ce6fe6..f94b7fde 100644 --- a/real_mode/first_stage/src/dap.rs +++ b/bios/stage-2/src/dap.rs @@ -1,6 +1,8 @@ -use core::arch::{asm, global_asm}; +use core::arch::asm; -#[repr(packed)] +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +#[repr(C, packed)] pub struct DiskAddressPacket { /// Size of the DAP structure packet_size: u8, @@ -17,24 +19,18 @@ pub struct DiskAddressPacket { } impl DiskAddressPacket { - pub fn new(memory_buffer_start: u16, file_offset: u64, bytes: u32) -> Self { - Self { - packet_size: 0x10, - zero: 0, - number_of_sectors: (bytes / 512) as u16, - offset: memory_buffer_start, - segment: 0, - start_lba: file_offset / 512, - } - } - - pub fn from_lba(memory_buffer_start: u16, start_lba: u64, number_of_sectors: u16) -> Self { + pub fn from_lba( + start_lba: u64, + number_of_sectors: u16, + target_addr: u16, + target_addr_segment: u16, + ) -> Self { Self { packet_size: 0x10, zero: 0, number_of_sectors, - offset: memory_buffer_start, - segment: 0, + offset: target_addr, + segment: target_addr_segment, start_lba, } } @@ -42,11 +38,13 @@ impl DiskAddressPacket { pub unsafe fn perform_load(&self, disk_number: u16) { let self_addr = self as *const Self as u16; asm!( - "push 0x7a", // error code `z`, passed to `fail` on error + "push 'z'", // error code `z`, passed to `fail` on error "mov {1:x}, si", "mov si, {0:x}", "int 0x13", - "jc fail", + "jnc 2f", // carry is set on fail + "call fail", + "2:", "pop si", // remove error code again "mov si, {1:x}", in(reg) self_addr, diff --git a/bios/stage-2/src/disk.rs b/bios/stage-2/src/disk.rs new file mode 100644 index 00000000..ec83eec3 --- /dev/null +++ b/bios/stage-2/src/disk.rs @@ -0,0 +1,103 @@ +use crate::dap; + +#[derive(Clone)] +pub struct DiskAccess { + pub disk_number: u16, + pub base_offset: u64, + pub current_offset: u64, +} + +impl Read for DiskAccess { + unsafe fn read_exact(&mut self, len: usize) -> &[u8] { + let current_sector_offset = usize::try_from(self.current_offset % 512).unwrap(); + + static mut TMP_BUF: AlignedArrayBuffer<1024> = AlignedArrayBuffer { + buffer: [0; 512 * 2], + }; + let buf = unsafe { &mut TMP_BUF }; + assert!(current_sector_offset + len <= buf.buffer.len()); + + self.read_exact_into(buf.buffer.len(), buf); + + &buf.buffer[current_sector_offset..][..len] + } + + fn read_exact_into(&mut self, len: usize, buf: &mut dyn AlignedBuffer) { + assert_eq!(len % 512, 0); + let buf = &mut buf.slice_mut()[..len]; + + let end_addr = self.base_offset + self.current_offset + u64::try_from(buf.len()).unwrap(); + let mut start_lba = (self.base_offset + self.current_offset) / 512; + let end_lba = (end_addr - 1) / 512; + + let mut number_of_sectors = end_lba + 1 - start_lba; + let mut target_addr = buf.as_ptr_range().start as u32; + + loop { + let sectors = u64::min(number_of_sectors, 32) as u16; + let dap = dap::DiskAddressPacket::from_lba( + start_lba, + sectors, + (target_addr & 0b1111) as u16, + (target_addr >> 4).try_into().unwrap(), + ); + unsafe { + dap.perform_load(self.disk_number); + } + + start_lba += u64::from(sectors); + number_of_sectors -= u64::from(sectors); + target_addr += u32::from(sectors) * 512; + + if number_of_sectors == 0 { + break; + } + } + + self.current_offset = end_addr; + } +} + +impl Seek for DiskAccess { + fn seek(&mut self, pos: SeekFrom) -> u64 { + match pos { + SeekFrom::Start(offset) => { + self.current_offset = offset; + self.current_offset + } + } + } +} + +pub trait Read { + unsafe fn read_exact(&mut self, len: usize) -> &[u8]; + fn read_exact_into(&mut self, len: usize, buf: &mut dyn AlignedBuffer); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SeekFrom { + Start(u64), +} + +pub trait Seek { + fn seek(&mut self, pos: SeekFrom) -> u64; +} + +#[repr(align(2))] +pub struct AlignedArrayBuffer { + pub buffer: [u8; LEN], +} + +pub trait AlignedBuffer { + fn slice(&self) -> &[u8]; + fn slice_mut(&mut self) -> &mut [u8]; +} + +impl AlignedBuffer for AlignedArrayBuffer { + fn slice(&self) -> &[u8] { + &self.buffer[..] + } + fn slice_mut(&mut self) -> &mut [u8] { + &mut self.buffer[..] + } +} diff --git a/bios/stage-2/src/fat.rs b/bios/stage-2/src/fat.rs new file mode 100644 index 00000000..79eff1ac --- /dev/null +++ b/bios/stage-2/src/fat.rs @@ -0,0 +1,510 @@ +// based on https://crates.io/crates/mini_fat by https://github.com/gridbugs + +use crate::disk::{AlignedBuffer, Read, Seek, SeekFrom}; +use core::char::DecodeUtf16Error; + +const DIRECTORY_ENTRY_BYTES: usize = 32; +const UNUSED_ENTRY_PREFIX: u8 = 0xE5; +const END_OF_DIRECTORY_PREFIX: u8 = 0; + +pub struct File { + first_cluster: u32, + file_size: u32, +} + +impl File { + pub fn file_size(&self) -> u32 { + self.file_size + } +} + +struct Bpb { + bytes_per_sector: u16, + sectors_per_cluster: u8, + reserved_sector_count: u16, + num_fats: u8, + root_entry_count: u16, + total_sectors_16: u16, + fat_size_16: u16, + total_sectors_32: u32, + fat_size_32: u32, + _root_cluster: u32, +} + +impl Bpb { + fn parse(disk: &mut D) -> Self { + disk.seek(SeekFrom::Start(0)); + let raw = unsafe { disk.read_exact(512) }; + + let bytes_per_sector = u16::from_le_bytes(raw[11..13].try_into().unwrap()); + let sectors_per_cluster = raw[13]; + let reserved_sector_count = u16::from_le_bytes(raw[14..16].try_into().unwrap()); + let num_fats = raw[16]; + let root_entry_count = u16::from_le_bytes(raw[17..19].try_into().unwrap()); + let fat_size_16 = u16::from_le_bytes(raw[22..24].try_into().unwrap()); + + let total_sectors_16 = u16::from_le_bytes(raw[19..21].try_into().unwrap()); + let total_sectors_32 = u32::from_le_bytes(raw[32..36].try_into().unwrap()); + + let root_cluster; + let fat_size_32; + + if (total_sectors_16 == 0) && (total_sectors_32 != 0) { + // FAT32 + fat_size_32 = u32::from_le_bytes(raw[36..40].try_into().unwrap()); + root_cluster = u32::from_le_bytes(raw[44..48].try_into().unwrap()); + } else if (total_sectors_16 != 0) && (total_sectors_32 == 0) { + // FAT12 or FAT16 + fat_size_32 = 0; + root_cluster = 0; + } else { + panic!("ExactlyOneTotalSectorsFieldMustBeZero"); + } + + Self { + bytes_per_sector, + sectors_per_cluster, + reserved_sector_count, + num_fats, + root_entry_count, + total_sectors_16, + fat_size_16, + total_sectors_32, + fat_size_32, + _root_cluster: root_cluster, + } + } + + fn fat_size_in_sectors(&self) -> u32 { + if self.fat_size_16 != 0 && self.fat_size_32 == 0 { + self.fat_size_16 as u32 + } else { + debug_assert!(self.fat_size_16 == 0 && self.fat_size_32 != 0); + self.fat_size_32 + } + } + + fn count_of_clusters(&self) -> u32 { + let root_dir_sectors = ((self.root_entry_count as u32 * 32) + + (self.bytes_per_sector as u32 - 1)) + / self.bytes_per_sector as u32; + let total_sectors = if self.total_sectors_16 != 0 { + self.total_sectors_16 as u32 + } else { + self.total_sectors_32 + }; + let data_sectors = total_sectors + - (self.reserved_sector_count as u32 + + (self.num_fats as u32 * self.fat_size_in_sectors()) + + root_dir_sectors); + data_sectors / self.sectors_per_cluster as u32 + } + + fn fat_type(&self) -> FatType { + let count_of_clusters = self.count_of_clusters(); + if count_of_clusters < 4085 { + FatType::Fat12 + } else if count_of_clusters < 65525 { + FatType::Fat16 + } else { + FatType::Fat32 + } + } + + fn root_directory_size(&self) -> usize { + if self.fat_type() == FatType::Fat32 { + debug_assert_eq!(self.root_entry_count, 0); + } + self.root_entry_count as usize * DIRECTORY_ENTRY_BYTES + } + + fn root_directory_offset(&self) -> u64 { + (self.reserved_sector_count as u64 + (self.num_fats as u64 * self.fat_size_16 as u64)) + * self.bytes_per_sector as u64 + } + + fn maximum_valid_cluster(&self) -> u32 { + self.count_of_clusters() + 1 + } + + fn fat_offset(&self) -> u64 { + self.reserved_sector_count as u64 * self.bytes_per_sector as u64 + } + + fn data_offset(&self) -> u64 { + self.root_directory_size() as u64 + + ((self.reserved_sector_count as u64 + + self.fat_size_in_sectors() as u64 * self.num_fats as u64) + * self.bytes_per_sector as u64) + } + + pub fn bytes_per_cluster(&self) -> u32 { + self.bytes_per_sector as u32 * self.sectors_per_cluster as u32 + } +} + +pub struct FileSystem { + disk: D, + bpb: Bpb, +} + +impl FileSystem { + pub fn parse(mut disk: D) -> Self { + Self { + bpb: Bpb::parse(&mut disk), + disk, + } + } + + pub fn find_file_in_root_dir( + &mut self, + name: &str, + buffer: &mut dyn AlignedBuffer, + ) -> Option { + let mut root_entries = self.read_root_dir(buffer).filter_map(|e| e.ok()); + let raw_entry = root_entries.find(|e| e.eq_name(name))?; + + let entry = match raw_entry { + RawDirectoryEntry::Normal(entry) => DirectoryEntry { + short_name: entry.short_filename_main, + short_name_extension: entry.short_filename_extension, + long_name_1: &[], + long_name_2: &[], + long_name_3: &[], + file_size: entry.file_size, + first_cluster: entry.first_cluster, + attributes: entry.attributes, + }, + RawDirectoryEntry::LongName(long_name) => match root_entries.next() { + Some(RawDirectoryEntry::LongName(_)) => unimplemented!(), + Some(RawDirectoryEntry::Normal(entry)) => DirectoryEntry { + short_name: entry.short_filename_main, + short_name_extension: entry.short_filename_extension, + long_name_1: long_name.name_1, + long_name_2: long_name.name_2, + long_name_3: long_name.name_3, + file_size: entry.file_size, + first_cluster: entry.first_cluster, + attributes: entry.attributes, + }, + None => { + panic!("next none"); + } + }, + }; + + if entry.is_directory() { + None + } else { + Some(File { + first_cluster: entry.first_cluster, + file_size: entry.file_size, + }) + } + } + + fn read_root_dir<'a>( + &'a mut self, + buffer: &'a mut (dyn AlignedBuffer + 'a), + ) -> impl Iterator> + 'a { + match self.bpb.fat_type() { + FatType::Fat32 => { + // self.bpb.root_cluster; + unimplemented!(); + } + FatType::Fat12 | FatType::Fat16 => { + let root_directory_size = self.bpb.root_directory_size(); + + self.disk + .seek(SeekFrom::Start(self.bpb.root_directory_offset())); + self.disk.read_exact_into(root_directory_size, buffer); + + buffer + .slice() + .chunks(DIRECTORY_ENTRY_BYTES) + .take_while(|raw_entry| raw_entry[0] != END_OF_DIRECTORY_PREFIX) + .filter(|raw_entry| raw_entry[0] != UNUSED_ENTRY_PREFIX) + .map(RawDirectoryEntry::parse) + } + } + } + + pub fn file_clusters<'a>( + &'a mut self, + file: &File, + ) -> impl Iterator> + 'a { + Traverser { + current_entry: file.first_cluster, + bpb: &self.bpb, + disk: &mut self.disk, + } + } +} + +#[derive(Debug)] +pub struct Cluster { + pub index: u32, + pub start_offset: u64, + pub len_bytes: u32, +} + +struct Traverser<'a, D> { + disk: &'a mut D, + current_entry: u32, + bpb: &'a Bpb, +} + +impl Traverser<'_, D> +where + D: Read + Seek, +{ + fn next_cluster(&mut self) -> Result, ()> { + let entry = classify_fat_entry( + self.bpb.fat_type(), + self.current_entry, + self.bpb.maximum_valid_cluster(), + ) + .map_err(|_| ())?; + let entry = match entry { + FileFatEntry::AllocatedCluster(cluster) => cluster, + FileFatEntry::EndOfFile => return Ok(None), + }; + let cluster_start = + self.bpb.data_offset() + (u64::from(entry) - 2) * self.bpb.bytes_per_cluster() as u64; + let next_entry = + fat_entry_of_nth_cluster(self.disk, self.bpb.fat_type(), self.bpb.fat_offset(), entry); + let index = self.current_entry; + self.current_entry = next_entry; + + Ok(Some(Cluster { + index, + start_offset: cluster_start, + len_bytes: self.bpb.bytes_per_cluster(), + })) + } +} + +impl Iterator for Traverser<'_, D> +where + D: Read + Seek, +{ + type Item = Result; + + fn next(&mut self) -> Option { + self.next_cluster().transpose() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FatType { + Fat12, + Fat16, + Fat32, +} + +impl FatType { + fn fat_entry_defective(self) -> u32 { + match self { + Self::Fat12 => 0xFF7, + Self::Fat16 => 0xFFF7, + Self::Fat32 => 0x0FFFFFF7, + } + } +} + +#[allow(dead_code)] +#[derive(Clone)] +pub struct DirectoryEntry<'a> { + short_name: &'a str, + short_name_extension: &'a str, + long_name_1: &'a [u8], + long_name_2: &'a [u8], + long_name_3: &'a [u8], + file_size: u32, + first_cluster: u32, + attributes: u8, +} + +impl<'a> DirectoryEntry<'a> { + pub fn is_directory(&self) -> bool { + self.attributes & directory_attributes::DIRECTORY != 0 + } +} + +#[derive(Debug)] +struct RawDirectoryEntryNormal<'a> { + short_filename_main: &'a str, + short_filename_extension: &'a str, + attributes: u8, + first_cluster: u32, + file_size: u32, +} + +#[allow(dead_code)] +#[derive(Debug)] +struct RawDirectoryEntryLongName<'a> { + order: u8, + name_1: &'a [u8], + name_2: &'a [u8], + name_3: &'a [u8], + attributes: u8, + checksum: u8, +} + +impl<'a> RawDirectoryEntryLongName<'a> { + pub fn name(&self) -> impl Iterator> + 'a { + let iter = self + .name_1 + .chunks(2) + .chain(self.name_2.chunks(2)) + .chain(self.name_3.chunks(2)) + .map(|c| u16::from_le_bytes(c.try_into().unwrap())) + .take_while(|&c| c != 0); + char::decode_utf16(iter) + } +} + +#[derive(Debug)] +enum RawDirectoryEntry<'a> { + Normal(RawDirectoryEntryNormal<'a>), + LongName(RawDirectoryEntryLongName<'a>), +} + +impl<'a> RawDirectoryEntry<'a> { + fn parse(raw: &'a [u8]) -> Result { + let attributes = raw[11]; + if attributes == directory_attributes::LONG_NAME { + let order = raw[0]; + let name_1 = &raw[1..11]; + let checksum = raw[13]; + let name_2 = &raw[14..26]; + let name_3 = &raw[28..32]; + + Ok(Self::LongName(RawDirectoryEntryLongName { + order, + name_1, + name_2, + name_3, + attributes, + checksum, + })) + } else { + fn slice_to_string(slice: &[u8]) -> Result<&str, ()> { + const SKIP_SPACE: u8 = 0x20; + let mut iter = slice.iter().copied(); + match iter.position(|c| c != SKIP_SPACE) { + Some(start_idx) => { + let end_idx = + start_idx + iter.position(|c| c == SKIP_SPACE).unwrap_or(slice.len()); + core::str::from_utf8(&slice[start_idx..end_idx]).map_err(|_| ()) + } + None => Ok(""), + } + } + let short_filename_main = slice_to_string(&raw[0..8])?; + let short_filename_extension = slice_to_string(&raw[8..11])?; + let first_cluster_hi = u16::from_le_bytes(raw[20..22].try_into().unwrap()); + let first_cluster_lo = u16::from_le_bytes(raw[26..28].try_into().unwrap()); + let first_cluster = ((first_cluster_hi as u32) << 16) | (first_cluster_lo as u32); + let file_size = u32::from_le_bytes(raw[28..32].try_into().unwrap()); + Ok(Self::Normal(RawDirectoryEntryNormal { + short_filename_main, + short_filename_extension, + attributes, + first_cluster, + file_size, + })) + } + } + + pub fn eq_name(&self, name: &str) -> bool { + match self { + RawDirectoryEntry::Normal(entry) => entry + .short_filename_main + .chars() + .chain(entry.short_filename_extension.chars()) + .eq(name.chars()), + RawDirectoryEntry::LongName(entry) => entry.name().eq(name.chars().map(Ok)), + } + } +} + +mod directory_attributes { + pub const READ_ONLY: u8 = 0x01; + pub const HIDDEN: u8 = 0x02; + pub const SYSTEM: u8 = 0x04; + pub const VOLUME_ID: u8 = 0x08; + pub const DIRECTORY: u8 = 0x10; + + pub const LONG_NAME: u8 = READ_ONLY | HIDDEN | SYSTEM | VOLUME_ID; +} + +fn classify_fat_entry( + fat_type: FatType, + entry: u32, + maximum_valid_cluster: u32, +) -> Result { + match entry { + 0 => Err(FatLookupError::FreeCluster), + 1 => Err(FatLookupError::UnspecifiedEntryOne), + entry => { + if entry <= maximum_valid_cluster { + Ok(FileFatEntry::AllocatedCluster(entry)) + } else if entry < fat_type.fat_entry_defective() { + Err(FatLookupError::ReservedEntry) + } else if entry == fat_type.fat_entry_defective() { + Err(FatLookupError::DefectiveCluster) + } else { + Ok(FileFatEntry::EndOfFile) + } + } + } +} + +#[derive(Debug)] +pub enum FatLookupError { + FreeCluster, + DefectiveCluster, + UnspecifiedEntryOne, + ReservedEntry, +} + +enum FileFatEntry { + AllocatedCluster(u32), + EndOfFile, +} + +fn fat_entry_of_nth_cluster(disk: &mut D, fat_type: FatType, fat_start: u64, n: u32) -> u32 +where + D: Seek + Read, +{ + debug_assert!(n >= 2); + match fat_type { + FatType::Fat32 => { + let base = n as u64 * 4; + disk.seek(SeekFrom::Start(fat_start + base)); + let buf = unsafe { disk.read_exact(4) }; + let buf: [u8; 4] = buf.try_into().unwrap(); + u32::from_le_bytes(buf) & 0x0FFFFFFF + } + FatType::Fat16 => { + let base = n as u64 * 2; + disk.seek(SeekFrom::Start(fat_start + base)); + let buf = unsafe { disk.read_exact(2) }; + let buf: [u8; 2] = buf.try_into().unwrap(); + u16::from_le_bytes(buf) as u32 + } + FatType::Fat12 => { + let base = n as u64 + (n as u64 / 2); + disk.seek(SeekFrom::Start(fat_start + base)); + let buf = unsafe { disk.read_exact(2) }; + let buf: [u8; 2] = buf.try_into().unwrap(); + let entry16 = u16::from_le_bytes(buf); + if n & 1 == 0 { + (entry16 & 0xFFF) as u32 + } else { + (entry16 >> 4) as u32 + } + } + } +} diff --git a/bios/stage-2/src/main.rs b/bios/stage-2/src/main.rs new file mode 100644 index 00000000..b5b07d65 --- /dev/null +++ b/bios/stage-2/src/main.rs @@ -0,0 +1,261 @@ +#![no_std] +#![no_main] + +use crate::{ + disk::{Read, Seek, SeekFrom}, + protected_mode::{ + copy_to_protected_mode, enter_protected_mode_and_jump_to_stage_3, enter_unreal_mode, + }, +}; +use bootloader_x86_64_bios_common::{hlt, BiosFramebufferInfo, BiosInfo, Region}; +use byteorder::{ByteOrder, LittleEndian}; +use core::{fmt::Write as _, slice}; +use disk::AlignedArrayBuffer; +use mbr_nostd::{PartitionTableEntry, PartitionType}; + +mod dap; +mod disk; +mod fat; +mod memory_map; +mod protected_mode; +mod screen; +mod vesa; + +/// We use this partition type to store the second bootloader stage; +const BOOTLOADER_SECOND_STAGE_PARTITION_TYPE: u8 = 0x20; + +// 1MiB (typically 14MiB accessible here) +const STAGE_3_DST: *mut u8 = 0x0010_0000 as *mut u8; +// must match the start address in bios/stage-4/stage-4-link.ld +const STAGE_4_DST: *mut u8 = 0x0013_0000 as *mut u8; +// 16MiB +const KERNEL_DST: *mut u8 = 0x0100_0000 as *mut u8; + +static mut DISK_BUFFER: AlignedArrayBuffer<0x4000> = AlignedArrayBuffer { + buffer: [0; 0x4000], +}; + +#[no_mangle] +#[link_section = ".start"] +pub extern "C" fn _start(disk_number: u16, partition_table_start: *const u8) -> ! { + start(disk_number, partition_table_start) +} + +fn start(disk_number: u16, partition_table_start: *const u8) -> ! { + // Enter unreal mode before doing anything else. + enter_unreal_mode(); + + screen::Writer.write_str(" -> SECOND STAGE\n").unwrap(); + + // parse partition table + let partitions = { + const MAX_ENTRIES: usize = 4; + const ENTRY_SIZE: usize = 16; + + let mut entries = [PartitionTableEntry::empty(); MAX_ENTRIES]; + let raw = unsafe { slice::from_raw_parts(partition_table_start, ENTRY_SIZE * MAX_ENTRIES) }; + for (idx, entry) in entries.iter_mut().enumerate() { + let offset = idx * ENTRY_SIZE; + let partition_type = PartitionType::from_mbr_tag_byte(raw[offset + 4]); + let lba = LittleEndian::read_u32(&raw[offset + 8..]); + let len = LittleEndian::read_u32(&raw[offset + 12..]); + *entry = PartitionTableEntry::new(partition_type, lba, len); + } + entries + }; + // look for second stage partition + let second_stage_partition_idx = partitions + .iter() + .enumerate() + .find(|(_, e)| { + e.partition_type == PartitionType::Unknown(BOOTLOADER_SECOND_STAGE_PARTITION_TYPE) + }) + .unwrap() + .0; + let fat_partition = partitions.get(second_stage_partition_idx + 1).unwrap(); + assert!(matches!( + fat_partition.partition_type, + PartitionType::Fat12(_) | PartitionType::Fat16(_) | PartitionType::Fat32(_) + )); + + // load fat partition + let mut disk = disk::DiskAccess { + disk_number, + base_offset: u64::from(fat_partition.logical_block_address) * 512, + current_offset: 0, + }; + + let mut fs = fat::FileSystem::parse(disk.clone()); + + let disk_buffer = unsafe { &mut DISK_BUFFER }; + + let stage_3_len = load_file("boot-stage-3", STAGE_3_DST, &mut fs, &mut disk, disk_buffer); + writeln!(screen::Writer, "stage 3 loaded at {STAGE_3_DST:#p}").unwrap(); + let stage_4_dst = { + let stage_3_end = STAGE_3_DST.wrapping_add(usize::try_from(stage_3_len).unwrap()); + assert!(STAGE_4_DST > stage_3_end); + STAGE_4_DST + }; + let stage_4_len = load_file("boot-stage-4", stage_4_dst, &mut fs, &mut disk, disk_buffer); + writeln!(screen::Writer, "stage 4 loaded at {stage_4_dst:#p}").unwrap(); + + writeln!(screen::Writer, "loading kernel...").unwrap(); + let kernel_len = load_file("kernel-x86_64", KERNEL_DST, &mut fs, &mut disk, disk_buffer); + writeln!(screen::Writer, "kernel loaded at {KERNEL_DST:#p}").unwrap(); + let kernel_page_size = (((kernel_len - 1) / 4096) + 1) as usize; + let ramdisk_start = KERNEL_DST.wrapping_add(kernel_page_size * 4096); + writeln!(screen::Writer, "Loading ramdisk...").unwrap(); + let ramdisk_len = + try_load_file("ramdisk", ramdisk_start, &mut fs, &mut disk, disk_buffer).unwrap_or(0u64); + + if ramdisk_len == 0 { + writeln!(screen::Writer, "No ramdisk found, skipping.").unwrap(); + } else { + writeln!(screen::Writer, "Loaded ramdisk at {ramdisk_start:#p}").unwrap(); + } + let config_file_start = ramdisk_start.wrapping_add(ramdisk_len.try_into().unwrap()); + let config_file_len = try_load_file( + "boot.json", + config_file_start, + &mut fs, + &mut disk, + disk_buffer, + ) + .unwrap_or(0); + + let memory_map = unsafe { memory_map::query_memory_map() }.unwrap(); + writeln!(screen::Writer, "{memory_map:x?}").unwrap(); + + // TODO: load these from the kernel's config instead of hardcoding + let max_width = 1280; + let max_height = 720; + + let mut vesa_info = vesa::VesaInfo::query(disk_buffer).unwrap(); + let vesa_mode = vesa_info + .get_best_mode(max_width, max_height) + .unwrap() + .expect("no suitable VESA mode found"); + writeln!( + screen::Writer, + "VESA MODE: {}x{}", + vesa_mode.width, + vesa_mode.height + ) + .unwrap(); + vesa_mode.enable().unwrap(); + + let mut info = BiosInfo { + stage_4: Region { + start: stage_4_dst as u64, + len: stage_4_len, + }, + kernel: Region { + start: KERNEL_DST as u64, + len: kernel_len, + }, + ramdisk: Region { + start: ramdisk_start as u64, + len: ramdisk_len, + }, + config_file: Region { + start: config_file_start as u64, + len: config_file_len, + }, + last_used_addr: config_file_start as u64 + config_file_len - 1, + memory_map_addr: memory_map.as_mut_ptr() as u32, + memory_map_len: memory_map.len().try_into().unwrap(), + framebuffer: BiosFramebufferInfo { + region: Region { + start: vesa_mode.framebuffer_start.into(), + len: u64::from(vesa_mode.height) * u64::from(vesa_mode.bytes_per_scanline), + }, + width: vesa_mode.width, + height: vesa_mode.height, + bytes_per_pixel: vesa_mode.bytes_per_pixel, + stride: vesa_mode.bytes_per_scanline / u16::from(vesa_mode.bytes_per_pixel), + pixel_format: vesa_mode.pixel_format, + }, + }; + + enter_protected_mode_and_jump_to_stage_3(STAGE_3_DST, &mut info); + + loop { + hlt(); + } +} + +fn try_load_file( + file_name: &str, + dst: *mut u8, + fs: &mut fat::FileSystem, + disk: &mut disk::DiskAccess, + disk_buffer: &mut AlignedArrayBuffer<16384>, +) -> Option { + let disk_buffer_size = disk_buffer.buffer.len(); + let file = fs.find_file_in_root_dir(file_name, disk_buffer)?; + + let file_size = file.file_size().into(); + + let mut total_offset = 0; + for cluster in fs.file_clusters(&file) { + let cluster = cluster.unwrap(); + let cluster_start = cluster.start_offset; + let cluster_end = cluster_start + u64::from(cluster.len_bytes); + + let mut offset = 0; + loop { + let range_start = cluster_start + offset; + if range_start >= cluster_end { + break; + } + let range_end = u64::min( + range_start + u64::try_from(disk_buffer_size).unwrap(), + cluster_end, + ); + let len = range_end - range_start; + + disk.seek(SeekFrom::Start(range_start)); + disk.read_exact_into(disk_buffer_size, disk_buffer); + + let slice = &disk_buffer.buffer[..usize::try_from(len).unwrap()]; + unsafe { copy_to_protected_mode(dst.wrapping_add(total_offset), slice) }; + let written = + unsafe { protected_mode::read_from_protected_mode(dst.wrapping_add(total_offset)) }; + assert_eq!(slice[0], written); + + offset += len; + total_offset += usize::try_from(len).unwrap(); + } + } + Some(file_size) +} + +fn load_file( + file_name: &str, + dst: *mut u8, + fs: &mut fat::FileSystem, + disk: &mut disk::DiskAccess, + disk_buffer: &mut AlignedArrayBuffer<16384>, +) -> u64 { + try_load_file(file_name, dst, fs, disk, disk_buffer).expect("file not found") +} + +/// Taken from https://github.com/rust-lang/rust/blob/e100ec5bc7cd768ec17d75448b29c9ab4a39272b/library/core/src/slice/mod.rs#L1673-L1677 +/// +/// TODO replace with `split_array` feature in stdlib as soon as it's stabilized, +/// see https://github.com/rust-lang/rust/issues/90091 +fn split_array_ref(slice: &[T]) -> (&[T; N], &[T]) { + if N > slice.len() { + fail(b'S'); + } + let (a, b) = slice.split_at(N); + // SAFETY: a points to [T; N]? Yes it's [T] of length N (checked by split_at) + unsafe { (&*(a.as_ptr() as *const [T; N]), b) } +} + +#[cold] +#[inline(never)] +#[no_mangle] +pub extern "C" fn fail(code: u8) -> ! { + panic!("fail: {}", code as char); +} diff --git a/bios/stage-2/src/memory_map.rs b/bios/stage-2/src/memory_map.rs new file mode 100644 index 00000000..c3ba6c97 --- /dev/null +++ b/bios/stage-2/src/memory_map.rs @@ -0,0 +1,73 @@ +// From http://wiki.osdev.org/Detecting_Memory_(x86)#Getting_an_E820_Memory_Map + +use crate::split_array_ref; +use bootloader_x86_64_bios_common::{racy_cell::RacyCell, E820MemoryRegion}; +use core::arch::asm; + +static MEMORY_MAP: RacyCell<[E820MemoryRegion; 100]> = RacyCell::new( + [E820MemoryRegion { + start_addr: 0, + len: 0, + region_type: 0, + acpi_extended_attributes: 0, + }; 100], +); + +/// use the INT 0x15, eax= 0xE820 BIOS function to get a memory map +pub unsafe fn query_memory_map() -> Result<&'static mut [E820MemoryRegion], ()> { + const SMAP: u32 = 0x534D4150; + + let memory_map = unsafe { MEMORY_MAP.get_mut() }; + + let mut i = 0; + + let mut offset = 0; + let buf = [0u8; 24]; + loop { + let ret: u32; + let buf_written_len; + unsafe { + asm!( + "push ebx", + "mov ebx, edx", + "mov edx, 0x534D4150", + "int 0x15", + "mov edx, ebx", + "pop ebx", + inout("eax") 0xe820 => ret, + inout("edx") offset, + inout("ecx") buf.len() => buf_written_len, + in("di") &buf + ) + }; + if ret != SMAP { + return Err(()); + } + + if buf_written_len != 0 { + let buf = &buf[..buf_written_len]; + + let (&base_raw, rest) = split_array_ref(buf); + let (&len_raw, rest) = split_array_ref(rest); + let (&kind_raw, rest) = split_array_ref(rest); + let acpi_extended_raw: [u8; 4] = rest.try_into().unwrap_or_default(); + + let len = u64::from_ne_bytes(len_raw); + if len != 0 { + memory_map[i] = E820MemoryRegion { + start_addr: u64::from_ne_bytes(base_raw), + len, + region_type: u32::from_ne_bytes(kind_raw), + acpi_extended_attributes: u32::from_ne_bytes(acpi_extended_raw), + }; + i += 1; + } + } + + if offset == 0 { + break; + } + } + + Ok(&mut memory_map[..i]) +} diff --git a/bios/stage-2/src/protected_mode.rs b/bios/stage-2/src/protected_mode.rs new file mode 100644 index 00000000..2b70182a --- /dev/null +++ b/bios/stage-2/src/protected_mode.rs @@ -0,0 +1,161 @@ +use bootloader_x86_64_bios_common::BiosInfo; +use core::{arch::asm, mem::size_of}; + +static GDT: GdtProtectedMode = GdtProtectedMode::new(); + +#[repr(C)] +pub struct GdtProtectedMode { + zero: u64, + code: u64, + data: u64, +} + +impl GdtProtectedMode { + const fn new() -> Self { + let limit = { + let limit_low = 0xffff; + let limit_high = 0xf << 48; + limit_high | limit_low + }; + let access_common = { + let present = 1 << 47; + let user_segment = 1 << 44; + let read_write = 1 << 41; + present | user_segment | read_write + }; + let protected_mode = 1 << 54; + let granularity = 1 << 55; + let base_flags = protected_mode | granularity | access_common | limit; + let executable = 1 << 43; + Self { + zero: 0, + code: base_flags | executable, + data: base_flags, + } + } + + fn clear_interrupts_and_load(&'static self) { + let pointer = GdtPointer { + base: self, + limit: (3 * size_of::() - 1) as u16, + }; + + unsafe { + asm!("cli", "lgdt [{}]", in(reg) &pointer, options(readonly, nostack, preserves_flags)); + } + } +} + +#[repr(C, packed(2))] +pub struct GdtPointer { + /// Size of the DT. + pub limit: u16, + /// Pointer to the memory region containing the DT. + pub base: *const GdtProtectedMode, +} + +unsafe impl Send for GdtPointer {} +unsafe impl Sync for GdtPointer {} + +pub fn enter_unreal_mode() { + let ds: u16; + let ss: u16; + unsafe { + asm!("mov {0:x}, ds", out(reg) ds, options(nomem, nostack, preserves_flags)); + asm!("mov {0:x}, ss", out(reg) ss, options(nomem, nostack, preserves_flags)); + } + + GDT.clear_interrupts_and_load(); + + // set protected mode bit + let cr0 = set_protected_mode_bit(); + + // load GDT + unsafe { + asm!("mov {0}, 0x10", "mov ds, {0}", "mov ss, {0}", out(reg) _); + } + + // unset protected mode bit again + write_cr0(cr0); + + unsafe { + asm!("mov ds, {0:x}", in(reg) ds, options(nostack, preserves_flags)); + asm!("mov ss, {0:x}", in(reg) ss, options(nostack, preserves_flags)); + asm!("sti"); + } +} + +#[no_mangle] +pub unsafe fn copy_to_protected_mode(target: *mut u8, bytes: &[u8]) { + for (offset, byte) in bytes.iter().enumerate() { + let dst = target.wrapping_add(offset); + // we need to do the write in inline assembly because the compiler + // seems to truncate the address + unsafe { + asm!("mov [{}], {}", in(reg) dst, in(reg_byte) *byte, options(nostack, preserves_flags)) + }; + assert_eq!(read_from_protected_mode(dst), *byte); + } +} + +#[no_mangle] +pub unsafe fn read_from_protected_mode(ptr: *mut u8) -> u8 { + let res; + // we need to do the read in inline assembly because the compiler + // seems to truncate the address + unsafe { + asm!("mov {}, [{}]", out(reg_byte) res, in(reg) ptr, options(pure, readonly, nostack, preserves_flags)) + }; + res +} + +pub fn enter_protected_mode_and_jump_to_stage_3(entry_point: *const u8, info: &mut BiosInfo) { + unsafe { asm!("cli") }; + set_protected_mode_bit(); + unsafe { + asm!( + // align the stack + "and esp, 0xffffff00", + // push arguments + "push {info:e}", + // push entry point address + "push {entry_point:e}", + info = in(reg) info as *const _ as u32, + entry_point = in(reg) entry_point as u32, + ); + asm!("ljmp $0x8, $2f", "2:", options(att_syntax)); + asm!( + ".code32", + + // reload segment registers + "mov {0}, 0x10", + "mov ds, {0}", + "mov es, {0}", + "mov ss, {0}", + + // jump to third stage + "pop {1}", + "call {1}", + + // enter endless loop in case third stage returns + "2:", + "jmp 2b", + out(reg) _, + out(reg) _, + ); + } +} + +fn set_protected_mode_bit() -> u32 { + let mut cr0: u32; + unsafe { + asm!("mov {:e}, cr0", out(reg) cr0, options(nomem, nostack, preserves_flags)); + } + let cr0_protected = cr0 | 1; + write_cr0(cr0_protected); + cr0 +} + +fn write_cr0(val: u32) { + unsafe { asm!("mov cr0, {:e}", in(reg) val, options(nostack, preserves_flags)) }; +} diff --git a/bios/stage-2/src/screen.rs b/bios/stage-2/src/screen.rs new file mode 100644 index 00000000..78e5a790 --- /dev/null +++ b/bios/stage-2/src/screen.rs @@ -0,0 +1,46 @@ +use core::{arch::asm, fmt::Write}; + +pub fn print_char(c: u8) { + let ax = u16::from(c) | 0x0e00; + unsafe { + asm!("push bx", "mov bx, 0", "int 0x10", "pop bx", in("ax") ax); + } +} + +pub fn print_str(s: &str) { + for c in s.chars() { + if c.is_ascii() { + print_char(c as u8); + if c == '\n' { + print_char(b'\r'); + } + } else { + print_char(b'X'); + } + } +} + +pub struct Writer; + +impl Write for Writer { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + print_str(s); + Ok(()) + } +} + +#[cfg(all(not(test), target_os = "none"))] +#[panic_handler] +pub fn panic(info: &core::panic::PanicInfo) -> ! { + let _ = write!(Writer, "\nPANIC: "); + if let Some(location) = info.location() { + let _ = writeln!(Writer, "{location} "); + } + let _ = writeln!(Writer, " {info}"); + + loop { + unsafe { + asm!("hlt"); + }; + } +} diff --git a/bios/stage-2/src/vesa.rs b/bios/stage-2/src/vesa.rs new file mode 100644 index 00000000..9bf54b55 --- /dev/null +++ b/bios/stage-2/src/vesa.rs @@ -0,0 +1,240 @@ +// info taken from https://wiki.osdev.org/VESA_Video_Modes + +use bootloader_x86_64_bios_common::PixelFormat; + +use crate::{disk::AlignedBuffer, AlignedArrayBuffer}; +use core::arch::asm; + +#[repr(C, packed)] +#[allow(dead_code)] +struct VbeInfoBlock { + signature: [u8; 4], // should be "VESA" + version: u16, // should be 0x0300 for VBE 3.0 + oem_string_ptr: u32, + capabilities: u32, + video_mode_ptr: u32, + total_memory: u16, // number of 64KB blocks + oem: [u8; 512 - 0x14], +} + +pub struct VesaInfo<'a> { + /// We must store a reference to the full block instead of only copying the + /// required information out because the video mode pointer might point inside the + /// `oem` field. + /// + /// See https://www.ctyme.com/intr/rb-0273.htm for details. + info_block: &'a VbeInfoBlock, + rest_of_buffer: &'a mut [u8], +} + +impl<'a> VesaInfo<'a> { + pub fn query(buffer: &'a mut AlignedArrayBuffer) -> Result { + assert_eq!(core::mem::size_of::(), 512); + + let (slice, rest_of_buffer) = buffer + .slice_mut() + .split_at_mut(core::mem::size_of::()); + slice.fill(0); + let block_ptr = slice.as_mut_ptr(); + let ret; + unsafe { + asm!("push es", "mov es, {:x}", "int 0x10", "pop es", in(reg)0, inout("ax") 0x4f00u16 => ret, in("di") block_ptr) + }; + match ret { + 0x4f => { + let info_block: &VbeInfoBlock = unsafe { &*block_ptr.cast() }; + Ok(VesaInfo { + info_block, + rest_of_buffer, + }) + } + other => Err(other), + } + } + + pub fn get_best_mode( + &mut self, + max_width: u16, + max_height: u16, + ) -> Result, u16> { + let mut best: Option = None; + for i in 0.. { + let mode = match self.get_mode(i) { + Some(mode) => mode, + None => break, + }; + let mode_info = VesaModeInfo::query(mode, self.rest_of_buffer).unwrap(); + + if mode_info.attributes & 0x90 != 0x90 { + // not a graphics mode with linear frame buffer support + continue; + } + + let supported_modes = [ + 4u8, // packed pixel graphics + 6, // direct color (24-bit color) + ]; + if !supported_modes.contains(&mode_info.memory_model) { + // unsupported mode + continue; + } + + if mode_info.width > max_width || mode_info.height > max_height { + continue; + } + + let replace = match &best { + None => true, + Some(best) => { + best.pixel_format.is_unknown() + || best.width < mode_info.width + || (best.width == mode_info.width && best.height < mode_info.height) + } + }; + + if replace { + best = Some(mode_info); + } + } + Ok(best) + } + + fn get_mode(&self, index: usize) -> Option { + let (segment, offset) = { + let raw = self.info_block.video_mode_ptr; + ((raw >> 16) as u16, raw as u16) + }; + let video_mode_ptr = ((segment as u32) << 4) + offset as u32; + + let base_ptr = video_mode_ptr as *const u16; + let ptr = unsafe { base_ptr.add(index) }; + let mode = unsafe { *ptr }; + if mode == 0xffff { + None + } else { + Some(mode) + } + } +} + +#[derive(Debug)] +pub struct VesaModeInfo { + mode: u16, + pub width: u16, + pub height: u16, + pub framebuffer_start: u32, + pub bytes_per_scanline: u16, + pub bytes_per_pixel: u8, + pub pixel_format: PixelFormat, + + memory_model: u8, + attributes: u16, +} + +impl VesaModeInfo { + fn query(mode: u16, buffer: &mut [u8]) -> Result { + #[repr(C, align(256))] + struct VbeModeInfo { + attributes: u16, + window_a: u8, + window_b: u8, + granularity: u16, + window_size: u16, + segment_a: u16, + segment_b: u16, + window_function_ptr: u32, + bytes_per_scanline: u16, + width: u16, + height: u16, + w_char: u8, + y_char: u8, + planes: u8, + bits_per_pixel: u8, + banks: u8, + memory_model: u8, + bank_size: u8, + image_pages: u8, + reserved_0: u8, + red_mask: u8, + red_position: u8, + green_mask: u8, + green_position: u8, + blue_mask: u8, + blue_position: u8, + reserved_mask: u8, + reserved_position: u8, + direct_color_attributes: u8, + framebuffer: u32, + off_screen_memory_offset: u32, + off_screen_memory_size: u16, + reserved: [u8; 206], + } + + assert_eq!(core::mem::size_of::(), 256); + + let slice = &mut buffer[..core::mem::size_of::()]; + slice.fill(0); + let block_ptr = slice.as_mut_ptr(); + + let mut ret: u16; + let mut target_addr = block_ptr as u32; + let segment = target_addr >> 4; + target_addr -= segment << 4; + unsafe { + asm!( + "push es", "mov es, {:x}", "int 0x10", "pop es", + in(reg) segment as u16, + inout("ax") 0x4f01u16 => ret, + in("cx") mode, + in("di") target_addr as u16 + ) + }; + match ret { + 0x4f => { + let block: &VbeModeInfo = unsafe { &*block_ptr.cast() }; + Ok(VesaModeInfo { + mode, + width: block.width, + height: block.height, + framebuffer_start: block.framebuffer, + bytes_per_scanline: block.bytes_per_scanline, + bytes_per_pixel: block.bits_per_pixel / 8, + pixel_format: match ( + block.red_position, + block.green_position, + block.blue_position, + ) { + (0, 8, 16) => PixelFormat::Rgb, + (16, 8, 0) => PixelFormat::Bgr, + (red_position, green_position, blue_position) => PixelFormat::Unknown { + red_position, + green_position, + blue_position, + }, + }, + memory_model: block.memory_model, + attributes: block.attributes, + }) + } + other => Err(other), + } + } + + pub fn enable(&self) -> Result<(), u16> { + let mut ret: u16; + unsafe { + asm!( + "push bx", + "mov bx, {:x}", + "int 0x10", + "pop bx", + in(reg) self.mode, + inout("ax") 0x4f02u16 => ret, + ) + }; + match ret { + 0x4f => Ok(()), + other => Err(other), + } + } +} diff --git a/bios/stage-2/stage-2-link.ld b/bios/stage-2/stage-2-link.ld new file mode 100644 index 00000000..b80c7473 --- /dev/null +++ b/bios/stage-2/stage-2-link.ld @@ -0,0 +1,37 @@ +ENTRY(_start) + +SECTIONS { + . = 0x7c00 + 512; + + .start : { + *(.start) + } + .text : { + *(.text .text.*) + } + .bss : { + *(.bss .bss.*) + } + .rodata : { + *(.rodata .rodata.*) + } + .data : { + *(.data .data.*) + } + .eh_frame : { + *(.eh_frame .eh_frame.*) + } + .eh_frame_hdr : { + *(.eh_frame_hdr .eh_frame_hdr.*) + } + + . = ALIGN(512); + + _second_stage_end = .; + + . = 0x0007FFFF - 2; + .end_marker : + { + SHORT(0xdead) + } +} diff --git a/bios/stage-3/.gitignore b/bios/stage-3/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/bios/stage-3/.gitignore @@ -0,0 +1 @@ +/target diff --git a/bios/stage-3/Cargo.toml b/bios/stage-3/Cargo.toml new file mode 100644 index 00000000..9db2bcce --- /dev/null +++ b/bios/stage-3/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bootloader-x86_64-bios-stage-3" +version.workspace = true +authors = ["Philipp Oppermann "] +edition = "2021" +license.workspace = true +repository.workspace = true +description = "Third BIOS stage of the `bootloader` crate" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bootloader-x86_64-bios-common = { workspace = true } +noto-sans-mono-bitmap = "0.1.5" + +# This currently causes a cargo warning, but it is required for publishing to crates.io. +# See https://github.com/rust-lang/cargo/issues/8264 for details. +[profile.stage-3] +inherits = "release" +debug = true +overflow-checks = true diff --git a/bios/stage-3/build.rs b/bios/stage-3/build.rs new file mode 100644 index 00000000..3aefe234 --- /dev/null +++ b/bios/stage-3/build.rs @@ -0,0 +1,9 @@ +use std::path::Path; + +fn main() { + let local_path = Path::new(env!("CARGO_MANIFEST_DIR")); + println!( + "cargo:rustc-link-arg-bins=--script={}", + local_path.join("stage-3-link.ld").display() + ) +} diff --git a/bios/stage-3/src/gdt.rs b/bios/stage-3/src/gdt.rs new file mode 100644 index 00000000..c577c00e --- /dev/null +++ b/bios/stage-3/src/gdt.rs @@ -0,0 +1,48 @@ +use core::{arch::asm, mem::size_of}; + +pub static LONG_MODE_GDT: GdtLongMode = GdtLongMode::new(); + +#[repr(C)] +pub struct GdtLongMode { + zero: u64, + code: u64, + data: u64, +} + +impl GdtLongMode { + const fn new() -> Self { + let common_flags = { + (1 << 44) // user segment + | (1 << 47) // present + | (1 << 41) // writable + | (1 << 40) // accessed (to avoid changes by the CPU) + }; + Self { + zero: 0, + code: common_flags | (1 << 43) | (1 << 53), // executable and long mode + data: common_flags, + } + } + + pub fn load(&'static self) { + let pointer = GdtPointer { + base: self, + limit: (3 * size_of::() - 1) as u16, + }; + + unsafe { + asm!("lgdt [{}]", in(reg) &pointer, options(readonly, nostack, preserves_flags)); + } + } +} + +#[repr(C, packed(2))] +pub struct GdtPointer { + /// Size of the DT. + pub limit: u16, + /// Pointer to the memory region containing the DT. + pub base: *const GdtLongMode, +} + +unsafe impl Send for GdtPointer {} +unsafe impl Sync for GdtPointer {} diff --git a/bios/stage-3/src/main.rs b/bios/stage-3/src/main.rs new file mode 100644 index 00000000..15d30f8b --- /dev/null +++ b/bios/stage-3/src/main.rs @@ -0,0 +1,78 @@ +#![no_std] +#![no_main] +#![deny(unsafe_op_in_unsafe_fn)] + +use crate::screen::Writer; +use bootloader_x86_64_bios_common::{hlt, BiosInfo}; +use core::{arch::asm, fmt::Write as _}; + +mod gdt; +mod paging; +mod screen; + +#[no_mangle] +#[link_section = ".start"] +pub extern "C" fn _start(info: &mut BiosInfo) { + screen::init(info.framebuffer); + // Writer.clear_screen(); + writeln!(Writer, "Third Stage ({info:x?})").unwrap(); + + // set up identity mapping, enable paging, and switch CPU into long + // mode (32-bit compatibility mode) + paging::init(); + + gdt::LONG_MODE_GDT.load(); + enter_long_mode_and_jump_to_stage_4(info); + + loop { + hlt(); + } +} + +#[no_mangle] +pub fn enter_long_mode_and_jump_to_stage_4(info: &mut BiosInfo) { + let _ = writeln!(Writer, "Paging init done, jumping to stage 4"); + unsafe { + asm!( + // align the stack + "and esp, 0xffffff00", + // push arguments (extended to 64 bit) + "push 0", + "push {info:e}", + // push entry point address (extended to 64 bit) + "push 0", + "push {entry_point:e}", + info = in(reg) info as *const _ as u32, + entry_point = in(reg) info.stage_4.start as u32, + ); + asm!("ljmp $0x8, $2f", "2:", options(att_syntax)); + asm!( + ".code64", + + // reload segment registers + "mov {0}, 0x10", + "mov ds, {0}", + "mov es, {0}", + "mov ss, {0}", + + // jump to 4th stage + "pop rax", + "pop rdi", + "call rax", + + // enter endless loop in case 4th stage returns + "2:", + "jmp 2b", + out(reg) _, + out("rax") _, + out("rdi") _, + ); + } +} + +#[panic_handler] +#[cfg(not(test))] +pub fn panic(info: &core::panic::PanicInfo) -> ! { + let _ = writeln!(Writer, "PANIC: {info}"); + loop {} +} diff --git a/bios/stage-3/src/paging.rs b/bios/stage-3/src/paging.rs new file mode 100644 index 00000000..20b5fd47 --- /dev/null +++ b/bios/stage-3/src/paging.rs @@ -0,0 +1,58 @@ +use bootloader_x86_64_bios_common::racy_cell::RacyCell; +use core::arch::asm; + +static LEVEL_4: RacyCell = RacyCell::new(PageTable::empty()); +static LEVEL_3: RacyCell = RacyCell::new(PageTable::empty()); +static LEVEL_2: RacyCell<[PageTable; 10]> = RacyCell::new([PageTable::empty(); 10]); + +pub fn init() { + create_mappings(); + + enable_paging(); +} + +fn create_mappings() { + let l4 = unsafe { LEVEL_4.get_mut() }; + let l3 = unsafe { LEVEL_3.get_mut() }; + let l2s = unsafe { LEVEL_2.get_mut() }; + let common_flags = 0b11; // PRESENT | WRITEABLE + l4.entries[0] = (l3 as *mut PageTable as u64) | common_flags; + for (i, l2) in l2s.iter_mut().enumerate() { + l3.entries[i] = (l2 as *mut PageTable as u64) | common_flags; + let offset = u64::try_from(i).unwrap() * 1024 * 1024 * 1024; + for (j, entry) in l2.entries.iter_mut().enumerate() { + // map huge pages + *entry = + (offset + u64::try_from(j).unwrap() * (2 * 1024 * 1024)) | common_flags | (1 << 7); + } + } +} + +fn enable_paging() { + // load level 4 table pointer into cr3 register + let l4 = unsafe { LEVEL_4.get_mut() } as *mut PageTable; + unsafe { asm!("mov cr3, {0}", in(reg) l4) }; + + // enable PAE-flag in cr4 (Physical Address Extension) + unsafe { asm!("mov eax, cr4", "or eax, 1<<5", "mov cr4, eax", out("eax")_) }; + + // set the long mode bit in the EFER MSR (model specific register) + unsafe { + asm!("mov ecx, 0xC0000080", "rdmsr", "or eax, 1 << 8", "wrmsr", out("eax") _, out("ecx")_) + }; + + // enable paging in the cr0 register + unsafe { asm!("mov eax, cr0", "or eax, 1 << 31", "mov cr0, eax", out("eax")_) }; +} + +#[derive(Clone, Copy)] +#[repr(align(4096))] +struct PageTable { + pub entries: [u64; 512], +} + +impl PageTable { + pub const fn empty() -> Self { + Self { entries: [0; 512] } + } +} diff --git a/bios/stage-3/src/screen.rs b/bios/stage-3/src/screen.rs new file mode 100644 index 00000000..6e81bf1d --- /dev/null +++ b/bios/stage-3/src/screen.rs @@ -0,0 +1,128 @@ +use bootloader_x86_64_bios_common::{racy_cell::RacyCell, BiosFramebufferInfo, PixelFormat}; +use core::{fmt, ptr}; +use noto_sans_mono_bitmap::{get_bitmap, BitmapChar, BitmapHeight, FontWeight}; + +static WRITER: RacyCell> = RacyCell::new(None); +pub struct Writer; + +impl fmt::Write for Writer { + fn write_str(&mut self, s: &str) -> fmt::Result { + let writer = unsafe { WRITER.get_mut() }.as_mut().unwrap(); + writer.write_str(s) + } +} + +pub fn init(info: BiosFramebufferInfo) { + let framebuffer = unsafe { + core::slice::from_raw_parts_mut( + info.region.start as *mut u8, + info.region.len.try_into().unwrap(), + ) + }; + let writer = ScreenWriter::new(framebuffer, info); + *unsafe { WRITER.get_mut() } = Some(writer); +} + +/// Additional vertical space between lines +const LINE_SPACING: usize = 0; + +struct ScreenWriter { + framebuffer: &'static mut [u8], + info: BiosFramebufferInfo, + x_pos: usize, + y_pos: usize, +} + +impl ScreenWriter { + pub fn new(framebuffer: &'static mut [u8], info: BiosFramebufferInfo) -> Self { + let mut logger = Self { + framebuffer, + info, + x_pos: 0, + y_pos: 0, + }; + logger.clear(); + logger + } + + fn newline(&mut self) { + self.y_pos += 14 + LINE_SPACING; + self.carriage_return() + } + + fn carriage_return(&mut self) { + self.x_pos = 0; + } + + /// Erases all text on the screen. + pub fn clear(&mut self) { + self.x_pos = 0; + self.y_pos = 0; + self.framebuffer.fill(0); + } + + fn width(&self) -> usize { + self.info.width.into() + } + + fn height(&self) -> usize { + self.info.height.into() + } + + fn write_char(&mut self, c: char) { + match c { + '\n' => self.newline(), + '\r' => self.carriage_return(), + c => { + let bitmap_char = get_bitmap(c, FontWeight::Regular, BitmapHeight::Size14).unwrap(); + if self.x_pos + bitmap_char.width() > self.width() { + self.newline(); + } + if self.y_pos + bitmap_char.height() > self.height() { + self.clear(); + } + self.write_rendered_char(bitmap_char); + } + } + } + + fn write_rendered_char(&mut self, rendered_char: BitmapChar) { + for (y, row) in rendered_char.bitmap().iter().enumerate() { + for (x, byte) in row.iter().enumerate() { + self.write_pixel(self.x_pos + x, self.y_pos + y, *byte); + } + } + self.x_pos += rendered_char.width(); + } + + fn write_pixel(&mut self, x: usize, y: usize, intensity: u8) { + let pixel_offset = y * usize::from(self.info.stride) + x; + let color = match self.info.pixel_format { + PixelFormat::Rgb => [intensity, intensity, intensity / 2, 0], + PixelFormat::Bgr => [intensity / 2, intensity, intensity, 0], + other => { + // set a supported (but invalid) pixel format before panicking to avoid a double + // panic; it might not be readable though + self.info.pixel_format = PixelFormat::Rgb; + panic!("pixel format {:?} not supported in logger", other) + } + }; + let bytes_per_pixel = self.info.bytes_per_pixel; + let byte_offset = pixel_offset * usize::from(bytes_per_pixel); + self.framebuffer[byte_offset..(byte_offset + usize::from(bytes_per_pixel))] + .copy_from_slice(&color[..usize::from(bytes_per_pixel)]); + let _ = unsafe { ptr::read_volatile(&self.framebuffer[byte_offset]) }; + } +} + +unsafe impl Send for ScreenWriter {} +unsafe impl Sync for ScreenWriter {} + +impl fmt::Write for ScreenWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + for c in s.chars() { + self.write_char(c); + } + Ok(()) + } +} diff --git a/bios/stage-3/stage-3-link.ld b/bios/stage-3/stage-3-link.ld new file mode 100644 index 00000000..c82e6f52 --- /dev/null +++ b/bios/stage-3/stage-3-link.ld @@ -0,0 +1,36 @@ +ENTRY(_start) + +SECTIONS { + . = 0x00100000; + + .start : { + *(.start) + } + .text : { + *(.text .text.*) + } + .rodata : { + *(.rodata .rodata.*) + } + .data : { + *(.data .data.*) + } + .bss : { + *(.bss .bss.*) + } + .eh_frame : { + *(.eh_frame .eh_frame.*) + } + .eh_frame_hdr : { + *(.eh_frame_hdr .eh_frame_hdr.*) + } + + . = ALIGN(512); + + _third_stage_end = .; + + .end_marker : + { + SHORT(0xdead) + } +} diff --git a/bios/stage-4/Cargo.toml b/bios/stage-4/Cargo.toml new file mode 100644 index 00000000..2afb8654 --- /dev/null +++ b/bios/stage-4/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "bootloader-x86_64-bios-stage-4" +version.workspace = true +edition = "2021" +license.workspace = true +repository.workspace = true +description = "Fourth BIOS stage of the `bootloader` crate" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bootloader_api = { workspace = true } +bootloader-x86_64-common = { workspace = true } +bootloader-x86_64-bios-common = { workspace = true } +bootloader-boot-config = { workspace = true } +log = "0.4.14" +x86_64 = "0.15.2" +rsdp = "2.0.0" +usize_conversions = "0.2.0" +serde-json-core = "0.5.0" + +# This currently causes a cargo warning, but it is required for publishing to crates.io. +# See https://github.com/rust-lang/cargo/issues/8264 for details. +[profile.stage-4] +inherits = "release" +debug = true +overflow-checks = true diff --git a/bios/stage-4/build.rs b/bios/stage-4/build.rs new file mode 100644 index 00000000..1fcfb947 --- /dev/null +++ b/bios/stage-4/build.rs @@ -0,0 +1,9 @@ +use std::path::Path; + +fn main() { + let local_path = Path::new(env!("CARGO_MANIFEST_DIR")); + println!( + "cargo:rustc-link-arg-bins=--script={}", + local_path.join("stage-4-link.ld").display() + ) +} diff --git a/bios/stage-4/src/main.rs b/bios/stage-4/src/main.rs new file mode 100644 index 00000000..cf159a61 --- /dev/null +++ b/bios/stage-4/src/main.rs @@ -0,0 +1,305 @@ +#![no_std] +#![no_main] + +use crate::memory_descriptor::MemoryRegion; +use bootloader_api::info::{FrameBufferInfo, PixelFormat}; +use bootloader_boot_config::{BootConfig, LevelFilter}; +use bootloader_x86_64_bios_common::{BiosFramebufferInfo, BiosInfo, E820MemoryRegion}; +use bootloader_x86_64_common::RawFrameBufferInfo; +use bootloader_x86_64_common::{ + legacy_memory_region::LegacyFrameAllocator, load_and_switch_to_kernel, Kernel, PageTables, + SystemInfo, +}; +use core::{cmp, slice}; +use usize_conversions::usize_from; +use x86_64::structures::paging::{FrameAllocator, OffsetPageTable}; +use x86_64::structures::paging::{ + Mapper, PageTable, PageTableFlags, PhysFrame, Size2MiB, Size4KiB, +}; +use x86_64::{PhysAddr, VirtAddr}; + +const GIGABYTE: u64 = 4096 * 512 * 512; + +mod memory_descriptor; + +#[no_mangle] +#[link_section = ".start"] +pub extern "C" fn _start(info: &mut BiosInfo) -> ! { + let memory_map: &mut [E820MemoryRegion] = unsafe { + core::slice::from_raw_parts_mut( + info.memory_map_addr as *mut _, + info.memory_map_len.try_into().unwrap(), + ) + }; + + memory_map.sort_unstable_by_key(|e| e.start_addr); + + let max_phys_addr = { + let max = memory_map + .iter() + .map(|r| { + log::info!("start: {:#x}, len: {:#x}", r.start_addr, r.len); + r.start_addr + r.len + }) + .max() + .expect("no physical memory regions found"); + // Don't consider addresses > 4GiB when determining the maximum physical + // address for the bootloader, as we are in protected mode and cannot + // address more than 4 GiB of memory anyway. + cmp::min(max, 4 * GIGABYTE) + }; + + let kernel_start = { + assert!(info.kernel.start != 0, "kernel start address must be set"); + PhysAddr::new(info.kernel.start) + }; + let kernel_size = info.kernel.len; + let next_free_frame = PhysFrame::containing_address(PhysAddr::new(info.last_used_addr)) + 1; + let mut frame_allocator = LegacyFrameAllocator::new_starting_at( + next_free_frame, + memory_map.iter().copied().map(MemoryRegion), + ); + + // We identity-mapped all memory, so the offset between physical and virtual addresses is 0 + let phys_offset = VirtAddr::new(0); + + let mut bootloader_page_table = { + let frame = x86_64::registers::control::Cr3::read().0; + let table: *mut PageTable = (phys_offset + frame.start_address().as_u64()).as_mut_ptr(); + unsafe { OffsetPageTable::new(&mut *table, phys_offset) } + }; + // identity-map remaining physical memory (first 10 gigabytes are already identity-mapped) + { + let start_frame: PhysFrame = + PhysFrame::containing_address(PhysAddr::new(GIGABYTE * 10)); + let end_frame = PhysFrame::containing_address(PhysAddr::new(max_phys_addr - 1)); + for frame in PhysFrame::range_inclusive(start_frame, end_frame) { + let flusher = unsafe { + bootloader_page_table + .identity_map( + frame, + PageTableFlags::PRESENT + | PageTableFlags::WRITABLE + | PageTableFlags::NO_EXECUTE, + &mut frame_allocator, + ) + .unwrap() + }; + // skip flushing the entry from the TLB for now, as we will + // flush the entire TLB at the end of the loop. + flusher.ignore(); + } + } + + // once all the physical memory is mapped, flush the TLB by reloading the + // CR3 register. + // + // we perform a single flush here rather than flushing each individual entry as + // it's mapped using `invlpg`, for efficiency. + x86_64::instructions::tlb::flush_all(); + + let page_tables = create_page_tables(&mut frame_allocator); + + let kernel_slice = { + let ptr = kernel_start.as_u64() as *const u8; + unsafe { slice::from_raw_parts(ptr, usize_from(kernel_size)) } + }; + let kernel = Kernel::parse(kernel_slice); + + let mut config_file_slice: Option<&[u8]> = None; + if info.config_file.len != 0 { + config_file_slice = { + let ptr = info.config_file.start as *mut u8; + unsafe { + Some(slice::from_raw_parts_mut( + ptr, + usize_from(info.config_file.len), + )) + } + }; + } + let mut error_loading_config: Option = None; + let mut config: BootConfig = match config_file_slice + .map(serde_json_core::from_slice) + .transpose() + { + Ok(data) => data.unwrap_or_default().0, + Err(err) => { + error_loading_config = Some(err); + Default::default() + } + }; + + #[allow(deprecated)] + if config.frame_buffer.minimum_framebuffer_height.is_none() { + config.frame_buffer.minimum_framebuffer_height = + kernel.config.frame_buffer.minimum_framebuffer_height; + } + #[allow(deprecated)] + if config.frame_buffer.minimum_framebuffer_width.is_none() { + config.frame_buffer.minimum_framebuffer_width = + kernel.config.frame_buffer.minimum_framebuffer_width; + } + let framebuffer_info = init_logger( + info.framebuffer, + config.log_level, + config.frame_buffer_logging, + config.serial_logging, + ); + + if let Some(err) = error_loading_config { + log::warn!("Failed to deserialize the config file {:?}", err); + } + + log::info!("4th Stage"); + log::info!("{info:x?}"); + log::info!("BIOS boot"); + + let system_info = SystemInfo { + framebuffer: Some(RawFrameBufferInfo { + addr: PhysAddr::new(info.framebuffer.region.start), + info: framebuffer_info, + }), + rsdp_addr: detect_rsdp(), + ramdisk_addr: match info.ramdisk.len { + 0 => None, + _ => Some(info.ramdisk.start), + }, + ramdisk_len: info.ramdisk.len, + }; + + load_and_switch_to_kernel(kernel, config, frame_allocator, page_tables, system_info); +} + +fn init_logger( + info: BiosFramebufferInfo, + log_level: LevelFilter, + frame_buffer_logger_status: bool, + serial_logger_status: bool, +) -> FrameBufferInfo { + let framebuffer_info = FrameBufferInfo { + byte_len: info.region.len.try_into().unwrap(), + width: info.width.into(), + height: info.height.into(), + pixel_format: match info.pixel_format { + bootloader_x86_64_bios_common::PixelFormat::Rgb => PixelFormat::Rgb, + bootloader_x86_64_bios_common::PixelFormat::Bgr => PixelFormat::Bgr, + bootloader_x86_64_bios_common::PixelFormat::Unknown { + red_position, + green_position, + blue_position, + } => PixelFormat::Unknown { + red_position, + green_position, + blue_position, + }, + }, + bytes_per_pixel: info.bytes_per_pixel.into(), + stride: info.stride.into(), + }; + + let framebuffer = unsafe { + core::slice::from_raw_parts_mut( + info.region.start as *mut u8, + info.region.len.try_into().unwrap(), + ) + }; + + bootloader_x86_64_common::init_logger( + framebuffer, + framebuffer_info, + log_level, + frame_buffer_logger_status, + serial_logger_status, + ); + + framebuffer_info +} + +/// Creates page table abstraction types for both the bootloader and kernel page tables. +fn create_page_tables(frame_allocator: &mut impl FrameAllocator) -> PageTables { + // We identity-mapped all memory, so the offset between physical and virtual addresses is 0 + let phys_offset = VirtAddr::new(0); + + // copy the currently active level 4 page table, because it might be read-only + let bootloader_page_table = { + let frame = x86_64::registers::control::Cr3::read().0; + let table: *mut PageTable = (phys_offset + frame.start_address().as_u64()).as_mut_ptr(); + unsafe { OffsetPageTable::new(&mut *table, phys_offset) } + }; + + // create a new page table hierarchy for the kernel + let (kernel_page_table, kernel_level_4_frame) = { + // get an unused frame for new level 4 page table + let frame: PhysFrame = frame_allocator.allocate_frame().expect("no unused frames"); + log::info!("New page table at: {frame:#?}"); + // get the corresponding virtual address + let addr = phys_offset + frame.start_address().as_u64(); + // initialize a new page table + let ptr: *mut PageTable = addr.as_mut_ptr(); + unsafe { ptr.write(PageTable::new()) }; + let level_4_table = unsafe { &mut *ptr }; + ( + unsafe { OffsetPageTable::new(level_4_table, phys_offset) }, + frame, + ) + }; + + PageTables { + bootloader: bootloader_page_table, + kernel: kernel_page_table, + kernel_level_4_frame, + } +} + +fn detect_rsdp() -> Option { + use core::ptr::NonNull; + use rsdp::{ + handler::{AcpiHandler, PhysicalMapping}, + Rsdp, + }; + + #[derive(Clone)] + struct IdentityMapped; + impl AcpiHandler for IdentityMapped { + // TODO FIXME: This inline(never) annotation is required. Without it, + // LLVM replaces the `search_for_on_bios` call below with a `ud2` + // instruction. See https://github.com/rust-osdev/bootloader/issues/425 + #[inline(never)] + unsafe fn map_physical_region( + &self, + physical_address: usize, + size: usize, + ) -> PhysicalMapping { + PhysicalMapping::new( + physical_address, + NonNull::new(physical_address as *mut _).unwrap(), + size, + size, + Self, + ) + } + + fn unmap_physical_region(_region: &PhysicalMapping) {} + } + + unsafe { + Rsdp::search_for_on_bios(IdentityMapped) + .ok() + .map(|mapping| PhysAddr::new(mapping.physical_start() as u64)) + } +} + +#[cfg(target_os = "none")] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + unsafe { + bootloader_x86_64_common::logger::LOGGER + .get() + .map(|l| l.force_unlock()) + }; + log::error!("{info}"); + loop { + unsafe { core::arch::asm!("cli; hlt") }; + } +} diff --git a/bios/stage-4/src/memory_descriptor.rs b/bios/stage-4/src/memory_descriptor.rs new file mode 100644 index 00000000..0f02e573 --- /dev/null +++ b/bios/stage-4/src/memory_descriptor.rs @@ -0,0 +1,33 @@ +use bootloader_api::info::MemoryRegionKind; +use bootloader_x86_64_bios_common::E820MemoryRegion; +use bootloader_x86_64_common::legacy_memory_region::LegacyMemoryRegion; +use x86_64::PhysAddr; + +impl LegacyMemoryRegion for MemoryRegion { + fn start(&self) -> PhysAddr { + PhysAddr::new(self.0.start_addr) + } + + fn len(&self) -> u64 { + self.0.len + } + + fn kind(&self) -> MemoryRegionKind { + match self.0.region_type { + 1 => MemoryRegionKind::Usable, + other => MemoryRegionKind::UnknownBios(other), + } + } + + fn usable_after_bootloader_exit(&self) -> bool { + matches!(self.kind(), MemoryRegionKind::Usable) + } +} + +/// A physical memory region returned by an `e820` BIOS call. +/// +/// See http://wiki.osdev.org/Detecting_Memory_(x86)#Getting_an_E820_Memory_Map for more info. +#[doc(hidden)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +pub struct MemoryRegion(pub E820MemoryRegion); diff --git a/bios/stage-4/stage-4-link.ld b/bios/stage-4/stage-4-link.ld new file mode 100644 index 00000000..ff513922 --- /dev/null +++ b/bios/stage-4/stage-4-link.ld @@ -0,0 +1,22 @@ +ENTRY(_start) + +SECTIONS { + # must match STAGE_4_DST address in bios/stage-2/src/main.rs + . = 0x00130000; + + .start : { + *(.start) + } + .text : { + *(.text .text.*) + } + .rodata : { + *(.rodata .rodata.*) + } + .data : { + *(.data .data.*) + } + .bss : { + *(.bss .bss.*) + } +} diff --git a/bors.toml b/bors.toml deleted file mode 100644 index c8009028..00000000 --- a/bors.toml +++ /dev/null @@ -1,4 +0,0 @@ -status = [ - "Test", "Build Example Kernel", "Check Formatting" -] -delete_merged_branches = true diff --git a/build.rs b/build.rs index a83dc9a2..c83c6145 100644 --- a/build.rs +++ b/build.rs @@ -1,299 +1,409 @@ -#[cfg(not(feature = "binary"))] +use std::path::{Path, PathBuf}; +use std::process::Command; +const BOOTLOADER_VERSION: &str = env!("CARGO_PKG_VERSION"); + fn main() { - #[cfg(target_arch = "x86")] - compile_error!( - "This crate currently does not support 32-bit protected mode. \ - See https://github.com/rust-osdev/bootloader/issues/70 for more information." - ); + #[cfg(not(feature = "uefi"))] + fn uefi_main() {} + #[cfg(not(feature = "bios"))] + fn bios_main() {} - #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))] - compile_error!("This crate only supports the x86_64 architecture."); -} + // Spawn two threads to build the uefi and bios code concurrently. + let uefi_main_handle = std::thread::spawn(uefi_main); + let bios_main_handle = std::thread::spawn(bios_main); -#[cfg(feature = "binary")] -#[derive(Default)] -struct BootloaderConfig { - physical_memory_offset: Option, - kernel_stack_address: Option, - kernel_stack_size: Option, + // Wait for the threads to finish. + uefi_main_handle.join().unwrap(); + bios_main_handle.join().unwrap(); } -#[cfg(feature = "binary")] -fn parse_aligned_addr(key: &str, value: &str) -> u64 { - let num = if value.starts_with("0x") { - u64::from_str_radix(&value[2..], 16) - } else { - u64::from_str_radix(&value, 10) - }; +#[cfg(feature = "bios")] +fn bios_main() { + // Run the bios build commands concurrently. + // (Cargo already uses multiple threads for building dependencies, but these + // BIOS crates don't have enough dependencies to utilize all cores on modern + // CPUs. So by running the build commands in parallel, we increase the number + // of utilized cores.) + let bios_boot_sector_path_handle = std::thread::spawn(build_bios_boot_sector); + let bios_stage_2_path_handle = std::thread::spawn(build_bios_stage_2); + let bios_stage_3_path_handle = std::thread::spawn(build_bios_stage_3); + let bios_stage_4_path_handle = std::thread::spawn(build_bios_stage_4); - let num = num.expect(&format!( - "`{}` in the kernel manifest must be an integer (is `{}`)", - key, value - )); + // Wait for the commands to finish. + let bios_boot_sector_path = bios_boot_sector_path_handle.join().unwrap(); + let bios_stage_2_path = bios_stage_2_path_handle.join().unwrap(); + let bios_stage_3_path = bios_stage_3_path_handle.join().unwrap(); + let bios_stage_4_path = bios_stage_4_path_handle.join().unwrap(); - if num % 0x1000 != 0 { - panic!( - "`{}` in the kernel manifest must be aligned to 4KiB (is `{}`)", - key, value + println!( + "cargo:rustc-env=BIOS_BOOT_SECTOR_PATH={}", + bios_boot_sector_path.display() + ); + println!( + "cargo:rustc-env=BIOS_STAGE_2_PATH={}", + bios_stage_2_path.display() + ); + println!( + "cargo:rustc-env=BIOS_STAGE_3_PATH={}", + bios_stage_3_path.display() + ); + println!( + "cargo:rustc-env=BIOS_STAGE_4_PATH={}", + bios_stage_4_path.display() + ); +} + +#[cfg(feature = "uefi")] +fn uefi_main() { + let uefi_path = build_uefi_bootloader(); + println!( + "cargo:rustc-env=UEFI_BOOTLOADER_PATH={}", + uefi_path.display() + ); +} + +#[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "uefi")] +fn build_uefi_bootloader() -> PathBuf { + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); + let mut cmd = Command::new(cargo); + cmd.arg("install").arg("bootloader-x86_64-uefi"); + if Path::new("uefi").exists() { + // local build + cmd.arg("--path").arg("uefi"); + println!("cargo:rerun-if-changed=uefi"); + println!("cargo:rerun-if-changed=common"); + } else { + cmd.arg("--version").arg(BOOTLOADER_VERSION); + } + cmd.arg("--locked"); + cmd.arg("--target").arg("x86_64-unknown-uefi"); + cmd.arg("-Zbuild-std=core") + .arg("-Zbuild-std-features=compiler-builtins-mem"); + cmd.arg("--root").arg(&out_dir); + cmd.env_remove("RUSTFLAGS"); + cmd.env_remove("CARGO_ENCODED_RUSTFLAGS"); + let status = cmd + .status() + .expect("failed to run cargo install for uefi bootloader"); + if status.success() { + let path = out_dir.join("bin").join("bootloader-x86_64-uefi.efi"); + assert!( + path.exists(), + "uefi bootloader executable does not exist after building" ); + path } else { - num + panic!("failed to build uefi bootloader"); } } -#[cfg(feature = "binary")] -fn parse_to_config(cfg: &mut BootloaderConfig, table: &toml::value::Table) { - use toml::Value; - - for (key, value) in table { - match (key.as_str(), value.clone()) { - ("kernel-stack-address", Value::Integer(i)) - | ("physical-memory-offset", Value::Integer(i)) => { - panic!( - "`{0}` in the kernel manifest must be given as a string, \ - as toml does not support unsigned 64-bit integers (try `{0} = \"{1}\"`)", - key.as_str(), - i - ); - } - ("kernel-stack-address", Value::String(s)) => { - cfg.kernel_stack_address = Some(parse_aligned_addr(key.as_str(), &s)); - } - #[cfg(not(feature = "map_physical_memory"))] - ("physical-memory-offset", Value::String(_)) => { - panic!( - "`physical-memory-offset` is only supported when the `map_physical_memory` \ - feature of the crate is enabled" - ); - } - #[cfg(feature = "map_physical_memory")] - ("physical-memory-offset", Value::String(s)) => { - cfg.physical_memory_offset = Some(parse_aligned_addr(key.as_str(), &s)); - } - ("kernel-stack-size", Value::Integer(i)) => { - if i <= 0 { - panic!("`kernel-stack-size` in kernel manifest must be positive"); - } else { - cfg.kernel_stack_size = Some(i as u64); - } - } - (s, _) => { - panic!( - "unknown key '{}' in kernel manifest \ - - you may need to update the bootloader crate", - s - ); - } - } +// dummy implementation because docsrs builds have no network access. +// This will put an empty file in out_dir and return its path. +#[cfg(docsrs_dummy_build)] +#[cfg(feature = "uefi")] +fn build_uefi_bootloader() -> PathBuf { + use std::fs::File; + + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let path = out_dir.join("bootloader-dummy-bootloader-uefi"); + + if File::create(&path).is_err() { + panic!("Failed to create dummy uefi bootloader"); } + assert!( + path.exists(), + "uefi bootloader dummy file does not exist after file creation" + ); + + path } -#[cfg(feature = "binary")] -fn main() { - use std::{ - env, - fs::{self, File}, - io::Write, - path::{Path, PathBuf}, - process::{self, Command}, - }; - use toml::Value; - - let target = env::var("TARGET").expect("TARGET not set"); - if Path::new(&target) - .file_stem() - .expect("target has no file stem") - != "x86_64-bootloader" - { - panic!("The bootloader must be compiled for the `x86_64-bootloader.json` target."); +#[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "bios")] +fn build_bios_boot_sector() -> PathBuf { + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); + let mut cmd = Command::new(cargo); + cmd.arg("install").arg("bootloader-x86_64-bios-boot-sector"); + let local_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("bios") + .join("boot_sector"); + if local_path.exists() { + // local build + cmd.arg("--path").arg(&local_path); + println!("cargo:rerun-if-changed={}", local_path.display()); + } else { + cmd.arg("--version").arg(BOOTLOADER_VERSION); } + cmd.arg("--locked"); + cmd.arg("--target").arg("i386-code16-boot-sector.json"); + cmd.arg("--profile").arg("stage-1"); + cmd.arg("-Zbuild-std=core") + .arg("-Zbuild-std-features=compiler-builtins-mem"); + cmd.arg("--root").arg(&out_dir); + cmd.env_remove("RUSTFLAGS"); + cmd.env_remove("CARGO_ENCODED_RUSTFLAGS"); + cmd.env_remove("RUSTC_WORKSPACE_WRAPPER"); // used by clippy + let status = cmd + .status() + .expect("failed to run cargo install for bios bootsector"); + let elf_path = if status.success() { + let path = out_dir + .join("bin") + .join("bootloader-x86_64-bios-boot-sector"); + assert!( + path.exists(), + "bios boot sector executable does not exist after building" + ); + path + } else { + panic!("failed to build bios boot sector"); + }; + convert_elf_to_bin(elf_path) +} + +// dummy implementation because docsrs builds have no network access. +// This will put an empty file in out_dir and return its path. +#[cfg(docsrs_dummy_build)] +#[cfg(feature = "bios")] +fn build_bios_boot_sector() -> PathBuf { + use std::fs::File; + + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let path = out_dir.join("bootloader-dummy-bios-boot-sector"); - let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set")); - let kernel = PathBuf::from(match env::var("KERNEL") { - Ok(kernel) => kernel, - Err(_) => { - eprintln!( - "The KERNEL environment variable must be set for building the bootloader.\n\n\ - If you use `bootimage` for building you need at least version 0.7.0. You can \ - update `bootimage` by running `cargo install bootimage --force`." - ); - process::exit(1); - } - }); - let kernel_file_name = kernel - .file_name() - .expect("KERNEL has no valid file name") - .to_str() - .expect("kernel file name not valid utf8"); - - // check that the kernel file exists + if File::create(&path).is_err() { + panic!("Failed to create dummy bios boot sector"); + } assert!( - kernel.exists(), - format!("KERNEL does not exist: {}", kernel.display()) + path.exists(), + "bios boot sector dummy file does not exist after file creation" ); - // get access to llvm tools shipped in the llvm-tools-preview rustup component - let llvm_tools = match llvm_tools::LlvmTools::new() { - Ok(tools) => tools, - Err(llvm_tools::Error::NotFound) => { - eprintln!("Error: llvm-tools not found"); - eprintln!("Maybe the rustup component `llvm-tools-preview` is missing?"); - eprintln!(" Install it through: `rustup component add llvm-tools-preview`"); - process::exit(1); - } - Err(err) => { - eprintln!("Failed to retrieve llvm-tools component: {:?}", err); - process::exit(1); - } - }; + path +} - // check that kernel executable has code in it - let llvm_size = llvm_tools - .tool(&llvm_tools::exe("llvm-size")) - .expect("llvm-size not found in llvm-tools"); - let mut cmd = Command::new(llvm_size); - cmd.arg(&kernel); - let output = cmd.output().expect("failed to run llvm-size"); - let output_str = String::from_utf8_lossy(&output.stdout); - let second_line_opt = output_str.lines().skip(1).next(); - let second_line = second_line_opt.expect("unexpected llvm-size line output"); - let text_size_opt = second_line.split_ascii_whitespace().next(); - let text_size = text_size_opt.expect("unexpected llvm-size output"); - if text_size == "0" { - panic!("Kernel executable has an empty text section. Perhaps the entry point was set incorrectly?\n\n\ - Kernel executable at `{}`\n", kernel.display()); +#[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "bios")] +fn build_bios_stage_2() -> PathBuf { + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); + let mut cmd = Command::new(cargo); + cmd.arg("install").arg("bootloader-x86_64-bios-stage-2"); + let local_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("bios") + .join("stage-2"); + if local_path.exists() { + // local build + cmd.arg("--path").arg(&local_path); + println!("cargo:rerun-if-changed={}", local_path.display()); + println!( + "cargo:rerun-if-changed={}", + local_path.with_file_name("common").display() + ); + } else { + cmd.arg("--version").arg(BOOTLOADER_VERSION); } - - // strip debug symbols from kernel for faster loading - let stripped_kernel_file_name = format!("kernel_stripped-{}", kernel_file_name); - let stripped_kernel = out_dir.join(&stripped_kernel_file_name); - let objcopy = llvm_tools - .tool(&llvm_tools::exe("llvm-objcopy")) - .expect("llvm-objcopy not found in llvm-tools"); - let mut cmd = Command::new(&objcopy); - cmd.arg("--strip-debug"); - cmd.arg(&kernel); - cmd.arg(&stripped_kernel); - let exit_status = cmd + cmd.arg("--locked"); + cmd.arg("--target").arg("i386-code16-stage-2.json"); + cmd.arg("--profile").arg("stage-2"); + cmd.arg("-Zbuild-std=core") + .arg("-Zbuild-std-features=compiler-builtins-mem"); + cmd.arg("--root").arg(&out_dir); + cmd.env_remove("RUSTFLAGS"); + cmd.env_remove("CARGO_ENCODED_RUSTFLAGS"); + cmd.env_remove("RUSTC_WORKSPACE_WRAPPER"); // used by clippy + let status = cmd .status() - .expect("failed to run objcopy to strip debug symbols"); - if !exit_status.success() { - eprintln!("Error: Stripping debug symbols failed"); - process::exit(1); + .expect("failed to run cargo install for bios second stage"); + let elf_path = if status.success() { + let path = out_dir.join("bin").join("bootloader-x86_64-bios-stage-2"); + assert!( + path.exists(), + "bios second stage executable does not exist after building" + ); + path + } else { + panic!("failed to build bios second stage"); + }; + convert_elf_to_bin(elf_path) +} + +// dummy implementation because docsrs builds have no network access. +// This will put an empty file in out_dir and return its path. +#[cfg(docsrs_dummy_build)] +#[cfg(feature = "bios")] +fn build_bios_stage_2() -> PathBuf { + use std::fs::File; + + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let path = out_dir.join("bootloader-dummy-bios-stage-2"); + + if File::create(&path).is_err() { + panic!("Failed to create dummy bios second stage"); } + assert!( + path.exists(), + "bios second stage dummy file does not exist after file creation" + ); - // wrap the kernel executable as binary in a new ELF file - let stripped_kernel_file_name_replaced = stripped_kernel_file_name.replace('-', "_"); - let kernel_bin = out_dir.join(format!("kernel_bin-{}.o", kernel_file_name)); - let kernel_archive = out_dir.join(format!("libkernel_bin-{}.a", kernel_file_name)); - let mut cmd = Command::new(&objcopy); - cmd.arg("-I").arg("binary"); - cmd.arg("-O").arg("elf64-x86-64"); - cmd.arg("--binary-architecture=i386:x86-64"); - cmd.arg("--rename-section").arg(".data=.kernel"); - cmd.arg("--redefine-sym").arg(format!( - "_binary_{}_start=_kernel_start_addr", - stripped_kernel_file_name_replaced - )); - cmd.arg("--redefine-sym").arg(format!( - "_binary_{}_end=_kernel_end_addr", - stripped_kernel_file_name_replaced - )); - cmd.arg("--redefine-sym").arg(format!( - "_binary_{}_size=_kernel_size", - stripped_kernel_file_name_replaced - )); - cmd.current_dir(&out_dir); - cmd.arg(&stripped_kernel_file_name); - cmd.arg(&kernel_bin); - let exit_status = cmd.status().expect("failed to run objcopy"); - if !exit_status.success() { - eprintln!("Error: Running objcopy failed"); - process::exit(1); + path +} + +#[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "bios")] +fn build_bios_stage_3() -> PathBuf { + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); + let mut cmd = Command::new(cargo); + cmd.arg("install").arg("bootloader-x86_64-bios-stage-3"); + let local_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("bios") + .join("stage-3"); + if local_path.exists() { + // local build + cmd.arg("--path").arg(&local_path); + println!("cargo:rerun-if-changed={}", local_path.display()); + } else { + cmd.arg("--version").arg(BOOTLOADER_VERSION); } + cmd.arg("--locked"); + cmd.arg("--target").arg("i686-stage-3.json"); + cmd.arg("--profile").arg("stage-3"); + cmd.arg("-Zbuild-std=core") + .arg("-Zbuild-std-features=compiler-builtins-mem"); + cmd.arg("--root").arg(&out_dir); + cmd.env_remove("RUSTFLAGS"); + cmd.env_remove("CARGO_ENCODED_RUSTFLAGS"); + cmd.env_remove("RUSTC_WORKSPACE_WRAPPER"); // used by clippy + let status = cmd + .status() + .expect("failed to run cargo install for bios stage-3"); + let elf_path = if status.success() { + let path = out_dir.join("bin").join("bootloader-x86_64-bios-stage-3"); + assert!( + path.exists(), + "bios stage-3 executable does not exist after building" + ); + path + } else { + panic!("failed to build bios stage-3"); + }; + convert_elf_to_bin(elf_path) +} - // create an archive for linking - let ar = llvm_tools - .tool(&llvm_tools::exe("llvm-ar")) - .unwrap_or_else(|| { - eprintln!("Failed to retrieve llvm-ar component"); - eprint!("This component is available since nightly-2019-03-29,"); - eprintln!("so try updating your toolchain if you're using an older nightly"); - process::exit(1); - }); - let mut cmd = Command::new(ar); - cmd.arg("crs"); - cmd.arg(&kernel_archive); - cmd.arg(&kernel_bin); - let exit_status = cmd.status().expect("failed to run ar"); - if !exit_status.success() { - eprintln!("Error: Running ar failed"); - process::exit(1); +// dummy implementation because docsrs builds have no network access. +// This will put an empty file in out_dir and return its path. +#[cfg(docsrs_dummy_build)] +#[cfg(feature = "bios")] +fn build_bios_stage_3() -> PathBuf { + use std::fs::File; + + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let path = out_dir.join("bootloader-dummy-bios-stage-3"); + + if File::create(&path).is_err() { + panic!("Failed to create dummy bios stage-3"); } + assert!( + path.exists(), + "bios stage-3 dummy file does not exist after file creation" + ); + + path +} - // Parse the kernel's Cargo.toml which is given to us by bootimage - let mut bootloader_config = BootloaderConfig::default(); - - match env::var("KERNEL_MANIFEST") { - Err(env::VarError::NotPresent) => { - panic!("The KERNEL_MANIFEST environment variable must be set for building the bootloader.\n\n\ - If you use `bootimage` for building you need at least version 0.7.7. You can \ - update `bootimage` by running `cargo install bootimage --force`."); - } - Err(env::VarError::NotUnicode(_)) => { - panic!("The KERNEL_MANIFEST environment variable contains invalid unicode") - } - Ok(path) => { - println!("cargo:rerun-if-changed={}", path); - - let contents = fs::read_to_string(&path).expect(&format!( - "failed to read kernel manifest file (path: {})", - path - )); - - let manifest = contents - .parse::() - .expect("failed to parse kernel's Cargo.toml"); - - let table = manifest - .get("package") - .and_then(|table| table.get("metadata")) - .and_then(|table| table.get("bootloader")) - .and_then(|table| table.as_table()); - - if let Some(table) = table { - parse_to_config(&mut bootloader_config, table); - } - } +#[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "bios")] +fn build_bios_stage_4() -> PathBuf { + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()); + let mut cmd = Command::new(cargo); + cmd.arg("install").arg("bootloader-x86_64-bios-stage-4"); + let local_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("bios") + .join("stage-4"); + if local_path.exists() { + // local build + cmd.arg("--path").arg(&local_path); + println!("cargo:rerun-if-changed={}", local_path.display()); + } else { + cmd.arg("--version").arg(BOOTLOADER_VERSION); } + cmd.arg("--locked"); + cmd.arg("--target").arg("x86_64-stage-4.json"); + cmd.arg("--profile").arg("stage-4"); + cmd.arg("-Zbuild-std=core") + .arg("-Zbuild-std-features=compiler-builtins-mem"); + cmd.arg("--root").arg(&out_dir); + cmd.env_remove("RUSTFLAGS"); + cmd.env_remove("CARGO_ENCODED_RUSTFLAGS"); + cmd.env_remove("RUSTC_WORKSPACE_WRAPPER"); // used by clippy + let status = cmd + .status() + .expect("failed to run cargo install for bios stage-4"); + let elf_path = if status.success() { + let path = out_dir.join("bin").join("bootloader-x86_64-bios-stage-4"); + assert!( + path.exists(), + "bios stage-4 executable does not exist after building" + ); + path + } else { + panic!("failed to build bios stage-4"); + }; - // Configure constants for the bootloader - // We leave some variables as Option rather than hardcoding their defaults so that they - // can be calculated dynamically by the bootloader. - let file_path = out_dir.join("bootloader_config.rs"); - let mut file = File::create(file_path).expect("failed to create bootloader_config.rs"); - file.write_all( - format!( - "const PHYSICAL_MEMORY_OFFSET: Option = {:?}; - const KERNEL_STACK_ADDRESS: Option = {:?}; - const KERNEL_STACK_SIZE: u64 = {};", - bootloader_config.physical_memory_offset, - bootloader_config.kernel_stack_address, - bootloader_config.kernel_stack_size.unwrap_or(512), // size is in number of pages - ) - .as_bytes(), - ) - .expect("write to bootloader_config.rs failed"); - - // pass link arguments to rustc - println!("cargo:rustc-link-search=native={}", out_dir.display()); - println!( - "cargo:rustc-link-lib=static=kernel_bin-{}", - kernel_file_name + convert_elf_to_bin(elf_path) +} + +// dummy implementation because docsrs builds have no network access. +// This will put an empty file in out_dir and return its path. +#[cfg(docsrs_dummy_build)] +#[cfg(feature = "bios")] +fn build_bios_stage_4() -> PathBuf { + use std::fs::File; + + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let path = out_dir.join("bootloader-dummy-bios-stage-4"); + + if File::create(&path).is_err() { + panic!("Failed to create dummy bios stage-4"); + } + assert!( + path.exists(), + "bios stage-4 dummy file does not exist after file creation" ); - println!("cargo:rerun-if-env-changed=KERNEL"); - println!("cargo:rerun-if-env-changed=KERNEL_MANIFEST"); - println!("cargo:rerun-if-changed={}", kernel.display()); - println!("cargo:rerun-if-changed=build.rs"); + path +} + +#[cfg(not(docsrs_dummy_build))] +#[cfg(feature = "bios")] +fn convert_elf_to_bin(elf_path: PathBuf) -> PathBuf { + let flat_binary_path = elf_path.with_extension("bin"); + + let llvm_tools = llvm_tools::LlvmTools::new().expect("failed to get llvm tools"); + let objcopy = llvm_tools + .tool(&llvm_tools::exe("llvm-objcopy")) + .expect("LlvmObjcopyNotFound"); + + // convert first stage to binary + let mut cmd = Command::new(objcopy); + cmd.arg("-I").arg("elf64-x86-64"); + cmd.arg("-O").arg("binary"); + cmd.arg("--binary-architecture=i386:x86-64"); + cmd.arg(&elf_path); + cmd.arg(&flat_binary_path); + let output = cmd + .output() + .expect("failed to execute llvm-objcopy command"); + if !output.status.success() { + panic!( + "objcopy failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + flat_binary_path } diff --git a/common/Cargo.toml b/common/Cargo.toml new file mode 100644 index 00000000..7f9531c6 --- /dev/null +++ b/common/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "bootloader-x86_64-common" +version.workspace = true +edition = "2021" +description = "Common code for the x86_64 bootloader implementations" +license.workspace = true +repository.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bootloader_api = { workspace = true } +bootloader-boot-config = { workspace = true } +conquer-once = { version = "0.3.2", default-features = false } +spinning_top = "0.2.4" +usize_conversions = "0.2.0" +x86_64 = { version = "0.15.2" } +xmas-elf = "0.8.0" +raw-cpuid = "10.2.0" +rand = { version = "0.8.4", default-features = false } +rand_hc = "0.3.1" +uart_16550 = "0.3.2" +log = "0.4.17" + +[dependencies.noto-sans-mono-bitmap] +version = "0.2.0" +default-features = false +features = [ + "regular", + "size_16", + "unicode-basic-latin", + # required for the fallback char '�' + "unicode-specials", +] diff --git a/common/config/Cargo.toml b/common/config/Cargo.toml new file mode 100644 index 00000000..560c6284 --- /dev/null +++ b/common/config/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "bootloader-boot-config" +version.workspace = true +edition = "2021" +description = "The runtime configurations that are saved in a JSON file for the bootloader crate" +license.workspace = true +repository.workspace = true + +[dependencies] +serde = { version = "1.0.152", default-features = false, features = ["derive"] } diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs new file mode 100644 index 00000000..1b294331 --- /dev/null +++ b/common/config/src/lib.rs @@ -0,0 +1,82 @@ +#![no_std] + +use serde::{Deserialize, Serialize}; + +/// Configures the boot behavior of the bootloader. +#[derive(Serialize, Deserialize)] +#[serde(default)] +#[non_exhaustive] +pub struct BootConfig { + /// Configuration for the frame buffer setup. + pub frame_buffer: FrameBuffer, + + /// The minimum log level that is printed to the screen during boot. + /// + /// The default is [`LevelFilter::Trace`]. + pub log_level: LevelFilter, + + /// Whether the bootloader should print log messages to the framebuffer during boot. + /// + /// Enabled by default. + pub frame_buffer_logging: bool, + + /// Whether the bootloader should print log messages to the serial port during boot. + /// + /// Enabled by default. + pub serial_logging: bool, + + #[doc(hidden)] + pub _test_sentinel: u64, +} + +impl Default for BootConfig { + fn default() -> Self { + Self { + frame_buffer: Default::default(), + log_level: Default::default(), + frame_buffer_logging: true, + serial_logging: true, + _test_sentinel: 0, + } + } +} + +/// Configuration for the frame buffer used for graphical output. +#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub struct FrameBuffer { + /// Instructs the bootloader to set up a framebuffer format that has at least the given height. + /// + /// If this is not possible, the bootloader will fall back to a smaller format. + pub minimum_framebuffer_height: Option, + /// Instructs the bootloader to set up a framebuffer format that has at least the given width. + /// + /// If this is not possible, the bootloader will fall back to a smaller format. + pub minimum_framebuffer_width: Option, +} + +/// An enum representing the available verbosity level filters of the logger. +/// +/// Based on +/// +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum LevelFilter { + /// A level lower than all log levels. + Off, + /// Corresponds to the `Error` log level. + Error, + /// Corresponds to the `Warn` log level. + Warn, + /// Corresponds to the `Info` log level. + Info, + /// Corresponds to the `Debug` log level. + Debug, + /// Corresponds to the `Trace` log level. + Trace, +} + +impl Default for LevelFilter { + fn default() -> Self { + Self::Trace + } +} diff --git a/common/src/entropy.rs b/common/src/entropy.rs new file mode 100644 index 00000000..93bf2b65 --- /dev/null +++ b/common/src/entropy.rs @@ -0,0 +1,98 @@ +use rand::SeedableRng; +use rand_hc::Hc128Rng; +use raw_cpuid::CpuId; +use x86_64::instructions::{port::Port, random::RdRand}; + +/// Gather entropy from various sources to seed a RNG. +pub fn build_rng() -> Hc128Rng { + const ENTROPY_SOURCES: [fn() -> [u8; 32]; 3] = [rd_rand_entropy, tsc_entropy, pit_entropy]; + + // Collect entropy from different sources and xor them all together. + let mut seed = [0; 32]; + for entropy_source in ENTROPY_SOURCES { + let entropy = entropy_source(); + + for (seed, entropy) in seed.iter_mut().zip(entropy) { + *seed ^= entropy; + } + } + + // Construct the RNG. + Hc128Rng::from_seed(seed) +} + +/// Gather entropy by requesting random numbers with `RDRAND` instruction if it's available. +/// +/// This function provides excellent entropy (unless you don't trust the CPU vendors). +fn rd_rand_entropy() -> [u8; 32] { + let mut entropy = [0; 32]; + + // Check if the CPU supports `RDRAND`. + if let Some(rd_rand) = RdRand::new() { + for i in 0..4 { + if let Some(value) = get_random_64(rd_rand) { + entropy[i * 8..(i + 1) * 8].copy_from_slice(&value.to_ne_bytes()); + } + } + } + + entropy +} + +/// Try to fetch a 64 bit random value with a retry count limit of 10. +/// +/// This function is a port of the C implementation provided in Intel's Software Developer's Manual, Volume 1, 7.3.17.1. +fn get_random_64(rd_rand: RdRand) -> Option { + const RETRY_LIMIT: u32 = 10; + for _ in 0..RETRY_LIMIT { + if let Some(value) = rd_rand.get_u64() { + return Some(value); + } + } + None +} + +/// Gather entropy by reading the current time with the `RDTSC` instruction if it's available. +/// +/// This function doesn't provide particularly good entropy, but it's better than nothing. +fn tsc_entropy() -> [u8; 32] { + let mut entropy = [0; 32]; + + // Check if the CPU supports `RDTSC`. + let cpu_id = CpuId::new(); + if let Some(feature_info) = cpu_id.get_feature_info() { + if !feature_info.has_tsc() { + for i in 0..4 { + let value = unsafe { + // SAFETY: We checked that the cpu supports `RDTSC` and we run in ring 0. + core::arch::x86_64::_rdtsc() + }; + entropy[i * 8..(i + 1) * 8].copy_from_slice(&value.to_ne_bytes()); + } + } + } + + entropy +} + +/// Gather entropy by reading the current count of PIT channel 1-3. +/// +/// This function doesn't provide particularly good entropy, but it's always available. +fn pit_entropy() -> [u8; 32] { + let mut entropy = [0; 32]; + + for (i, entropy_byte) in entropy.iter_mut().enumerate() { + // Cycle through channels 1-3. + let channel = i % 3; + + let mut port = Port::::new(0x40 + channel as u16); + let value = unsafe { + // SAFETY: It's safe to read from ports 0x40-0x42. + port.read() + }; + + *entropy_byte = value; + } + + entropy +} diff --git a/common/src/framebuffer.rs b/common/src/framebuffer.rs new file mode 100644 index 00000000..bfcb52a2 --- /dev/null +++ b/common/src/framebuffer.rs @@ -0,0 +1,154 @@ +use bootloader_api::info::{FrameBufferInfo, PixelFormat}; +use core::{fmt, ptr}; +use font_constants::BACKUP_CHAR; +use noto_sans_mono_bitmap::{ + get_raster, get_raster_width, FontWeight, RasterHeight, RasterizedChar, +}; + +/// Additional vertical space between lines +const LINE_SPACING: usize = 2; +/// Additional horizontal space between characters. +const LETTER_SPACING: usize = 0; + +/// Padding from the border. Prevent that font is too close to border. +const BORDER_PADDING: usize = 1; + +/// Constants for the usage of the [`noto_sans_mono_bitmap`] crate. +mod font_constants { + use super::*; + + /// Height of each char raster. The font size is ~0.84% of this. Thus, this is the line height that + /// enables multiple characters to be side-by-side and appear optically in one line in a natural way. + pub const CHAR_RASTER_HEIGHT: RasterHeight = RasterHeight::Size16; + + /// The width of each single symbol of the mono space font. + pub const CHAR_RASTER_WIDTH: usize = get_raster_width(FontWeight::Regular, CHAR_RASTER_HEIGHT); + + /// Backup character if a desired symbol is not available by the font. + /// The '�' character requires the feature "unicode-specials". + pub const BACKUP_CHAR: char = '�'; + + pub const FONT_WEIGHT: FontWeight = FontWeight::Regular; +} + +/// Returns the raster of the given char or the raster of [`font_constants::BACKUP_CHAR`]. +fn get_char_raster(c: char) -> RasterizedChar { + fn get(c: char) -> Option { + get_raster( + c, + font_constants::FONT_WEIGHT, + font_constants::CHAR_RASTER_HEIGHT, + ) + } + get(c).unwrap_or_else(|| get(BACKUP_CHAR).expect("Should get raster of backup char.")) +} + +/// Allows logging text to a pixel-based framebuffer. +pub struct FrameBufferWriter { + framebuffer: &'static mut [u8], + info: FrameBufferInfo, + x_pos: usize, + y_pos: usize, +} + +impl FrameBufferWriter { + /// Creates a new logger that uses the given framebuffer. + pub fn new(framebuffer: &'static mut [u8], info: FrameBufferInfo) -> Self { + let mut logger = Self { + framebuffer, + info, + x_pos: 0, + y_pos: 0, + }; + logger.clear(); + logger + } + + fn newline(&mut self) { + self.y_pos += font_constants::CHAR_RASTER_HEIGHT.val() + LINE_SPACING; + self.carriage_return() + } + + fn carriage_return(&mut self) { + self.x_pos = BORDER_PADDING; + } + + /// Erases all text on the screen. Resets `self.x_pos` and `self.y_pos`. + pub fn clear(&mut self) { + self.x_pos = BORDER_PADDING; + self.y_pos = BORDER_PADDING; + self.framebuffer.fill(0); + } + + fn width(&self) -> usize { + self.info.width + } + + fn height(&self) -> usize { + self.info.height + } + + /// Writes a single char to the framebuffer. Takes care of special control characters, such as + /// newlines and carriage returns. + fn write_char(&mut self, c: char) { + match c { + '\n' => self.newline(), + '\r' => self.carriage_return(), + c => { + let new_xpos = self.x_pos + font_constants::CHAR_RASTER_WIDTH; + if new_xpos >= self.width() { + self.newline(); + } + let new_ypos = + self.y_pos + font_constants::CHAR_RASTER_HEIGHT.val() + BORDER_PADDING; + if new_ypos >= self.height() { + self.clear(); + } + self.write_rendered_char(get_char_raster(c)); + } + } + } + + /// Prints a rendered char into the framebuffer. + /// Updates `self.x_pos`. + fn write_rendered_char(&mut self, rendered_char: RasterizedChar) { + for (y, row) in rendered_char.raster().iter().enumerate() { + for (x, byte) in row.iter().enumerate() { + self.write_pixel(self.x_pos + x, self.y_pos + y, *byte); + } + } + self.x_pos += rendered_char.width() + LETTER_SPACING; + } + + fn write_pixel(&mut self, x: usize, y: usize, intensity: u8) { + let pixel_offset = y * self.info.stride + x; + let color = match self.info.pixel_format { + PixelFormat::Rgb => [intensity, intensity, intensity / 2, 0], + PixelFormat::Bgr => [intensity / 2, intensity, intensity, 0], + PixelFormat::U8 => [if intensity > 200 { 0xf } else { 0 }, 0, 0, 0], + other => { + // set a supported (but invalid) pixel format before panicking to avoid a double + // panic; it might not be readable though + self.info.pixel_format = PixelFormat::Rgb; + panic!("pixel format {:?} not supported in logger", other) + } + }; + let bytes_per_pixel = self.info.bytes_per_pixel; + let byte_offset = pixel_offset * bytes_per_pixel; + self.framebuffer[byte_offset..(byte_offset + bytes_per_pixel)] + .copy_from_slice(&color[..bytes_per_pixel]); + let _ = unsafe { ptr::read_volatile(&self.framebuffer[byte_offset]) }; + } +} + +unsafe impl Send for FrameBufferWriter {} +unsafe impl Sync for FrameBufferWriter {} + +impl fmt::Write for FrameBufferWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + for c in s.chars() { + self.write_char(c); + } + Ok(()) + } +} diff --git a/common/src/gdt.rs b/common/src/gdt.rs new file mode 100644 index 00000000..5ba16c5b --- /dev/null +++ b/common/src/gdt.rs @@ -0,0 +1,32 @@ +use x86_64::{ + instructions::segmentation::{self, Segment}, + structures::{ + gdt::{Descriptor, GlobalDescriptorTable}, + paging::PhysFrame, + }, + VirtAddr, +}; + +pub fn create_and_load(frame: PhysFrame) { + let phys_addr = frame.start_address(); + log::info!("Creating GDT at {:?}", phys_addr); + let virt_addr = VirtAddr::new(phys_addr.as_u64()); // utilize identity mapping + + let ptr: *mut GlobalDescriptorTable = virt_addr.as_mut_ptr(); + + let mut gdt = GlobalDescriptorTable::new(); + let code_selector = gdt.append(Descriptor::kernel_code_segment()); + let data_selector = gdt.append(Descriptor::kernel_data_segment()); + let gdt = unsafe { + ptr.write(gdt); + &*ptr + }; + + gdt.load(); + unsafe { + segmentation::CS::set_reg(code_selector); + segmentation::DS::set_reg(data_selector); + segmentation::ES::set_reg(data_selector); + segmentation::SS::set_reg(data_selector); + } +} diff --git a/common/src/legacy_memory_region.rs b/common/src/legacy_memory_region.rs new file mode 100644 index 00000000..0c613392 --- /dev/null +++ b/common/src/legacy_memory_region.rs @@ -0,0 +1,596 @@ +use bootloader_api::info::{MemoryRegion, MemoryRegionKind}; +use core::{cmp, mem::MaybeUninit}; +use x86_64::{ + align_down, align_up, + structures::paging::{FrameAllocator, PhysFrame, Size4KiB}, + PhysAddr, +}; + +/// A slice of memory that is used by the bootloader and needs to be reserved +/// in the kernel +#[derive(Clone, Copy, Debug)] +pub struct UsedMemorySlice { + /// the physical start of the slice + pub start: u64, + /// The physical end address (exclusive) of the region. + pub end: u64, +} + +impl UsedMemorySlice { + /// Creates a new slice + pub fn new_from_len(start: u64, len: u64) -> Self { + Self { + start, + end: start + len, + } + } +} + +/// Abstraction trait for a memory region returned by the UEFI or BIOS firmware. +pub trait LegacyMemoryRegion: Copy + core::fmt::Debug { + /// Returns the physical start address of the region. + fn start(&self) -> PhysAddr; + /// Returns the size of the region in bytes. + fn len(&self) -> u64; + /// Returns whether this region is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Returns the type of the region, e.g. whether it is usable or reserved. + fn kind(&self) -> MemoryRegionKind; + + /// Some regions become usable when the bootloader jumps to the kernel. + fn usable_after_bootloader_exit(&self) -> bool; +} + +/// A physical frame allocator based on a BIOS or UEFI provided memory map. +pub struct LegacyFrameAllocator { + original: I, + memory_map: I, + current_descriptor: Option, + next_frame: PhysFrame, + min_frame: PhysFrame, +} + +/// Start address of the first frame that is not part of the lower 1MB of frames +const LOWER_MEMORY_END_PAGE: u64 = 0x10_0000; + +impl LegacyFrameAllocator +where + I: ExactSizeIterator + Clone, + I::Item: LegacyMemoryRegion, +{ + /// Creates a new frame allocator based on the given legacy memory regions. + /// + /// Skips the frame at physical address zero to avoid potential problems. For example + /// identity-mapping the frame at address zero is not valid in Rust, because Rust's `core` + /// library assumes that references can never point to virtual address `0`. + /// Also skips the lower 1MB of frames, there are use cases that require lower conventional memory access (Such as SMP SIPI). + pub fn new(memory_map: I) -> Self { + // skip frame 0 because the rust core library does not see 0 as a valid address + // Also skip at least the lower 1MB of frames, there are use cases that require lower conventional memory access (Such as SMP SIPI). + let start_frame = PhysFrame::containing_address(PhysAddr::new(LOWER_MEMORY_END_PAGE)); + Self::new_starting_at(start_frame, memory_map) + } + + /// Creates a new frame allocator based on the given legacy memory regions. Skips any frames + /// before the given `frame` or `0x10000`(1MB) whichever is higher, there are use cases that require + /// lower conventional memory access (Such as SMP SIPI). + pub fn new_starting_at(frame: PhysFrame, memory_map: I) -> Self { + // skip frame 0 because the rust core library does not see 0 as a valid address + // Also skip at least the lower 1MB of frames, there are use cases that require lower conventional memory access (Such as SMP SIPI). + let lower_mem_end = PhysFrame::containing_address(PhysAddr::new(LOWER_MEMORY_END_PAGE)); + let frame = core::cmp::max(frame, lower_mem_end); + Self { + original: memory_map.clone(), + memory_map, + current_descriptor: None, + next_frame: frame, + min_frame: frame, + } + } + + fn allocate_frame_from_descriptor(&mut self, descriptor: D) -> Option { + let start_addr = descriptor.start(); + let start_frame = PhysFrame::containing_address(start_addr); + let end_addr = start_addr + descriptor.len(); + let end_frame = PhysFrame::containing_address(end_addr - 1u64); + + // increase self.next_frame to start_frame if smaller + if self.next_frame < start_frame { + self.next_frame = start_frame; + } + + if self.next_frame <= end_frame { + let ret = self.next_frame; + self.next_frame += 1; + + Some(ret) + } else { + None + } + } + + /// Returns the number of memory regions in the underlying memory map. + /// + /// The function always returns the same value, i.e. the length doesn't + /// change after calls to `allocate_frame`. + pub fn len(&self) -> usize { + self.original.len() + } + + /// Returns whether this memory map is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the largest detected physical memory address. + /// + /// Useful for creating a mapping for all physical memory. + pub fn max_phys_addr(&self) -> PhysAddr { + let max = self + .original + .clone() + .map(|r| r.start() + r.len()) + .max() + .unwrap(); + + // Always cover at least the first 4 GiB of physical memory. That area + // contains useful MMIO regions (local APIC, I/O APIC, PCI bars) that + // we want to make accessible to the kernel even if no DRAM exists >4GiB. + cmp::max(max, PhysAddr::new(0x1_0000_0000)) + } + + /// Calculate the maximum number of regions produced by [Self::construct_memory_map] + pub fn memory_map_max_region_count(&self) -> usize { + // every used region can split an original region into 3 new regions, + // this means we need to reserve 2 extra spaces for each region. + // There are 3 used regions: kernel, ramdisk and the bootloader heap + self.len() + 6 + } + + /// Converts this type to a boot info memory map. + /// + /// The memory map is placed in the given `regions` slice. The length of the given slice + /// must be at least the value returned by [`len`] plus 1. + /// + /// The return slice is a subslice of `regions`, shortened to the actual number of regions. + pub fn construct_memory_map( + self, + regions: &mut [MaybeUninit], + kernel_slice_start: PhysAddr, + kernel_slice_len: u64, + ramdisk_slice_start: Option, + ramdisk_slice_len: u64, + ) -> &mut [MemoryRegion] { + let used_slices = [ + UsedMemorySlice { + start: self.min_frame.start_address().as_u64(), + end: self.next_frame.start_address().as_u64(), + }, + UsedMemorySlice::new_from_len(kernel_slice_start.as_u64(), kernel_slice_len), + ] + .into_iter() + .chain( + ramdisk_slice_start + .map(|start| UsedMemorySlice::new_from_len(start.as_u64(), ramdisk_slice_len)), + ) + .map(|slice| UsedMemorySlice { + start: align_down(slice.start, 0x1000), + end: align_up(slice.end, 0x1000), + }); + + let mut next_index = 0; + for descriptor in self.original { + let kind = match descriptor.kind() { + _ if descriptor.usable_after_bootloader_exit() => { + // Region was not usable before, but it will be as soon as + // the bootloader passes control to the kernel. We don't + // need to check against `next_free` because the + // LegacyFrameAllocator only allocates memory from usable + // descriptors. + MemoryRegionKind::Usable + } + other => other, + }; + + let end = descriptor.start() + descriptor.len(); + let region = MemoryRegion { + start: descriptor.start().as_u64(), + end: end.as_u64(), + kind, + }; + if region.kind == MemoryRegionKind::Usable { + Self::split_and_add_region(region, regions, &mut next_index, used_slices.clone()); + } else { + Self::add_region(region, regions, &mut next_index); + } + } + + let initialized = &mut regions[..next_index]; + unsafe { + // inlined variant of: `MaybeUninit::slice_assume_init_mut(initialized)` + // TODO: undo inlining when `slice_assume_init_mut` becomes stable + &mut *(initialized as *mut [_] as *mut [_]) + } + } + + fn split_and_add_region<'a, U>( + mut region: MemoryRegion, + regions: &mut [MaybeUninit], + next_index: &mut usize, + used_slices: U, + ) where + U: Iterator + Clone, + { + assert!(region.kind == MemoryRegionKind::Usable); + // Each loop iteration takes a chunk of `region` and adds it to + // `regions`. Do this until `region` is empty. + while region.start != region.end { + // Check if there is overlap between `region` and `used_slices`. + if let Some((overlap_start, overlap_end)) = used_slices + .clone() + .map(|slice| { + // Calculate the start and end points of the overlap + // between `slice` and `region`. If `slice` and `region` + // don't overlap, the range will be ill-formed + // (overlap_start > overlap_end). + let overlap_start = cmp::max(region.start, slice.start); + let overlap_end = cmp::min(region.end, slice.end); + (overlap_start, overlap_end) + }) + .filter(|(overlap_start, overlap_end)| { + // Only consider non-empty overlap. + overlap_start < overlap_end + }) + .min_by_key(|&(overlap_start, _)| { + // Find the earliest overlap. + overlap_start + }) + { + // There's no overlapping used slice before `overlap_start`, so + // we know that memory between `region.start` and + // `overlap_start` is usable. + let usable = MemoryRegion { + start: region.start, + end: overlap_start, + kind: MemoryRegionKind::Usable, + }; + let bootloader = MemoryRegion { + start: overlap_start, + end: overlap_end, + kind: MemoryRegionKind::Bootloader, + }; + Self::add_region(usable, regions, next_index); + Self::add_region(bootloader, regions, next_index); + // Continue after the overlapped region. + region.start = overlap_end; + } else { + // There's no overlap. We can add the whole region. + Self::add_region(region, regions, next_index); + break; + } + } + } + + fn add_region( + region: MemoryRegion, + regions: &mut [MaybeUninit], + next_index: &mut usize, + ) { + if region.start == region.end { + // skip zero sized regions + return; + } + unsafe { + regions + .get_mut(*next_index) + .expect("cannot add region: no more free entries in memory map") + .as_mut_ptr() + .write(region) + }; + *next_index += 1; + } +} + +unsafe impl FrameAllocator for LegacyFrameAllocator +where + I: ExactSizeIterator + Clone, + I::Item: LegacyMemoryRegion, +{ + fn allocate_frame(&mut self) -> Option> { + if let Some(current_descriptor) = self.current_descriptor { + match self.allocate_frame_from_descriptor(current_descriptor) { + Some(frame) => return Some(frame), + None => { + self.current_descriptor = None; + } + } + } + + // find next suitable descriptor + while let Some(descriptor) = self.memory_map.next() { + if descriptor.kind() != MemoryRegionKind::Usable { + continue; + } + if let Some(frame) = self.allocate_frame_from_descriptor(descriptor) { + self.current_descriptor = Some(descriptor); + return Some(frame); + } + } + + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Copy, Clone, Debug)] + struct TestMemoryRegion { + start: PhysAddr, + len: u64, + kind: MemoryRegionKind, + } + + impl LegacyMemoryRegion for TestMemoryRegion { + fn start(&self) -> PhysAddr { + self.start + } + + fn len(&self) -> u64 { + assert!(self.len % 4096 == 0); + self.len + } + + fn kind(&self) -> MemoryRegionKind { + self.kind + } + + fn usable_after_bootloader_exit(&self) -> bool { + match self.kind { + MemoryRegionKind::Usable => true, + _ => false, + } + } + } + + // we need some kind of max phys memory, 4GB seems reasonable + const MAX_PHYS_ADDR: u64 = 0x4000_0000; + + fn create_single_test_region() -> Vec { + vec![TestMemoryRegion { + start: PhysAddr::new(0), + len: MAX_PHYS_ADDR, + kind: MemoryRegionKind::Usable, + }] + } + + #[test] + fn test_all_regions_frame_alligned() { + let regions = create_single_test_region(); + let mut allocator = LegacyFrameAllocator::new(regions.into_iter()); + // allocate at least 1 frame + allocator.allocate_frame(); + + let mut regions = [MaybeUninit::uninit(); 10]; + let kernel_slice_start = PhysAddr::new(0x50000); + let kernel_slice_len = 0x0500; + let ramdisk_slice_start = None; + let ramdisk_slice_len = 0; + + let kernel_regions = allocator.construct_memory_map( + &mut regions, + kernel_slice_start, + kernel_slice_len, + ramdisk_slice_start, + ramdisk_slice_len, + ); + + for region in kernel_regions.iter() { + assert!(region.start % 0x1000 == 0); + assert!(region.end % 0x1000 == 0); + } + } + + #[test] + fn test_kernel_and_ram_in_same_region() { + let regions = create_single_test_region(); + let mut allocator = LegacyFrameAllocator::new(regions.into_iter()); + // allocate at least 1 frame + allocator.allocate_frame(); + + let mut regions = [MaybeUninit::uninit(); 10]; + let kernel_slice_start = PhysAddr::new(0x50000); + let kernel_slice_len = 0x1000; + let ramdisk_slice_start = Some(PhysAddr::new(0x60000)); + let ramdisk_slice_len = 0x2000; + + let kernel_regions = allocator.construct_memory_map( + &mut regions, + kernel_slice_start, + kernel_slice_len, + ramdisk_slice_start, + ramdisk_slice_len, + ); + let mut kernel_regions = kernel_regions.iter(); + // usable memory before the kernel + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x0000, + end: 0x50000, + kind: MemoryRegionKind::Usable + }) + ); + // kernel + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x50000, + end: 0x51000, + kind: MemoryRegionKind::Bootloader + }) + ); + // usabel memory between kernel and ramdisk + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x51000, + end: 0x60000, + kind: MemoryRegionKind::Usable + }) + ); + // ramdisk + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x60000, + end: 0x62000, + kind: MemoryRegionKind::Bootloader + }) + ); + // usabele memory after ramdisk, up until bootloader allocated memory + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x62000, + end: 0x10_0000, + kind: MemoryRegionKind::Usable + }) + ); + // bootloader allocated memory + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x10_0000, + end: 0x10_1000, + kind: MemoryRegionKind::Bootloader + }) + ); + // rest is free + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x10_1000, + end: MAX_PHYS_ADDR, + kind: MemoryRegionKind::Usable + }) + ); + assert_eq!(kernel_regions.next(), None); + } + + #[test] + fn test_multiple_regions() { + let regions = vec![ + TestMemoryRegion { + start: PhysAddr::new(0), + len: 0x10_0000, + kind: MemoryRegionKind::Usable, + }, + TestMemoryRegion { + start: PhysAddr::new(0x10_0000), + len: 0x5000, + kind: MemoryRegionKind::UnknownBios(0), + }, + TestMemoryRegion { + start: PhysAddr::new(0x10_5000), + len: MAX_PHYS_ADDR - 0x10_5000, + kind: MemoryRegionKind::Usable, + }, + ]; + let mut allocator = LegacyFrameAllocator::new(regions.into_iter()); + // allocate at least 1 frame + allocator.allocate_frame(); + + let mut regions = [MaybeUninit::uninit(); 10]; + let kernel_slice_start = PhysAddr::new(0x50000); + let kernel_slice_len = 0x1000; + let ramdisk_slice_start = Some(PhysAddr::new(0x60000)); + let ramdisk_slice_len = 0x2000; + + let kernel_regions = allocator.construct_memory_map( + &mut regions, + kernel_slice_start, + kernel_slice_len, + ramdisk_slice_start, + ramdisk_slice_len, + ); + let mut kernel_regions = kernel_regions.iter(); + + // usable memory before the kernel + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x0000, + end: 0x50000, + kind: MemoryRegionKind::Usable + }) + ); + // kernel + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x50000, + end: 0x51000, + kind: MemoryRegionKind::Bootloader + }) + ); + // usabel memory between kernel and ramdisk + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x51000, + end: 0x60000, + kind: MemoryRegionKind::Usable + }) + ); + // ramdisk + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x60000, + end: 0x62000, + kind: MemoryRegionKind::Bootloader + }) + ); + // usabele memory after ramdisk, up until bootloader allocated memory + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x62000, + end: 0x10_0000, + kind: MemoryRegionKind::Usable + }) + ); + // the unknown bios region + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x10_0000, + end: 0x10_5000, + kind: MemoryRegionKind::UnknownBios(0) + }) + ); + // bootloader allocated memory, this gets pushed back by the bios region + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x10_5000, + end: 0x10_6000, + kind: MemoryRegionKind::Bootloader + }) + ); + // rest is free + assert_eq!( + kernel_regions.next(), + Some(&MemoryRegion { + start: 0x10_6000, + end: MAX_PHYS_ADDR, + kind: MemoryRegionKind::Usable + }) + ); + assert_eq!(kernel_regions.next(), None); + } +} diff --git a/common/src/level_4_entries.rs b/common/src/level_4_entries.rs new file mode 100644 index 00000000..926dd541 --- /dev/null +++ b/common/src/level_4_entries.rs @@ -0,0 +1,230 @@ +use crate::{ + entropy, + load_kernel::{calc_elf_memory_requirements, ElfMemoryRequirements, VirtualAddressOffset}, + BootInfo, RawFrameBufferInfo, +}; +use bootloader_api::{config, info::MemoryRegion, BootloaderConfig}; +use core::{alloc::Layout, iter::Step}; +use rand::{ + distributions::{Distribution, Uniform}, + seq::IteratorRandom, +}; +use rand_hc::Hc128Rng; +use usize_conversions::IntoUsize; +use x86_64::{ + structures::paging::{Page, PageTableIndex, Size4KiB}, + PhysAddr, VirtAddr, +}; +use xmas_elf::{header, program::ProgramHeader, ElfFile}; + +/// Keeps track of used entries in a level 4 page table. +/// +/// Useful for determining a free virtual memory block, e.g. for mapping additional data. +pub struct UsedLevel4Entries { + /// Whether an entry is in use by the kernel. + entry_state: [bool; 512], + /// A random number generator that should be used to generate random addresses or + /// `None` if aslr is disabled. + rng: Option, +} + +impl UsedLevel4Entries { + /// Initializes a new instance. + /// + /// Marks the statically configured virtual address ranges from the config as used. + pub fn new( + max_phys_addr: PhysAddr, + regions_len: usize, + framebuffer: Option<&RawFrameBufferInfo>, + config: &BootloaderConfig, + kernel_elf: &ElfFile<'_>, + ) -> Result { + let mut used = UsedLevel4Entries { + entry_state: [false; 512], + rng: config.mappings.aslr.then(entropy::build_rng), + }; + + // The bootloader maps of the kernel's memory into its own page tables. + // We need to prevent overlaps, so mark all memory that could already + // be used by the bootload as inaccessible. + + // All memory in this range is identity mapped. + used.mark_range_as_used(0, max_phys_addr.as_u64()); + + // The bootload needs to access the frame buffer. + if let Some(frame_buffer) = framebuffer { + used.mark_range_as_used( + frame_buffer.addr.as_u64(), + frame_buffer.info.byte_len as u64, + ); + } + + // Mark the statically configured ranges from the config as used. + + if let Some(config::Mapping::FixedAddress(physical_memory_offset)) = + config.mappings.physical_memory + { + used.mark_range_as_used(physical_memory_offset, max_phys_addr.as_u64()); + } + + if let Some(config::Mapping::FixedAddress(recursive_address)) = + config.mappings.page_table_recursive + { + let recursive_index = VirtAddr::new(recursive_address).p4_index(); + used.mark_p4_index_as_used(recursive_index); + } + + if let config::Mapping::FixedAddress(kernel_stack_address) = config.mappings.kernel_stack { + used.mark_range_as_used(kernel_stack_address, config.kernel_stack_size); + } + + if let config::Mapping::FixedAddress(kernel_base) = config.mappings.kernel_base { + let ElfMemoryRequirements { size, align, .. } = + calc_elf_memory_requirements(kernel_elf); + + if !VirtAddr::new(kernel_base).is_aligned(align) { + return Err("kernel_code mapping alignment does not match elf file"); + } + + used.mark_range_as_used(kernel_base, size); + } + if kernel_elf.header.pt2.type_().as_type() == header::Type::Executable { + let ElfMemoryRequirements { size, min_addr, .. } = + calc_elf_memory_requirements(kernel_elf); + + used.mark_range_as_used(min_addr, size); + } + + if let config::Mapping::FixedAddress(boot_info_address) = config.mappings.boot_info { + let boot_info_layout = Layout::new::(); + let regions = regions_len + 1; // one region might be split into used/unused + let memory_regions_layout = Layout::array::(regions).unwrap(); + let (combined, _) = boot_info_layout.extend(memory_regions_layout).unwrap(); + + used.mark_range_as_used(boot_info_address, combined.size() as u64); + } + + if let config::Mapping::FixedAddress(framebuffer_address) = config.mappings.framebuffer { + if let Some(framebuffer) = framebuffer { + used.mark_range_as_used(framebuffer_address, framebuffer.info.byte_len as u64); + } + } + + // Mark everything before the dynamic range as unusable. + if let Some(dynamic_range_start) = config.mappings.dynamic_range_start { + let dynamic_range_start = VirtAddr::new(dynamic_range_start); + let start_page: Page = Page::containing_address(dynamic_range_start); + if let Some(unusable_page) = Step::backward_checked(start_page, 1) { + for i in 0..=u16::from(unusable_page.p4_index()) { + used.mark_p4_index_as_used(PageTableIndex::new(i)); + } + } + } + + // Mark everything after the dynamic range as unusable. + if let Some(dynamic_range_end) = config.mappings.dynamic_range_end { + let dynamic_range_end = VirtAddr::new(dynamic_range_end); + let end_page: Page = Page::containing_address(dynamic_range_end); + if let Some(unusable_page) = Step::forward_checked(end_page, 1) { + for i in u16::from(unusable_page.p4_index())..512 { + used.mark_p4_index_as_used(PageTableIndex::new(i)); + } + } + } + + Ok(used) + } + + /// Marks all p4 entries in the range `[address..address+size)` as used. + fn mark_range_as_used(&mut self, address: u64, size: u64) { + let start = VirtAddr::new(address); + let end_inclusive = (start + size) - 1; + let start_page = Page::::containing_address(start); + let end_page_inclusive = Page::::containing_address(end_inclusive); + + for p4_index in u16::from(start_page.p4_index())..=u16::from(end_page_inclusive.p4_index()) + { + self.mark_p4_index_as_used(PageTableIndex::new(p4_index)); + } + } + + fn mark_p4_index_as_used(&mut self, p4_index: PageTableIndex) { + self.entry_state[usize::from(p4_index)] = true; + } + + /// Marks the virtual address range of all segments as used. + pub fn mark_segments<'a>( + &mut self, + segments: impl Iterator>, + virtual_address_offset: VirtualAddressOffset, + ) { + for segment in segments.filter(|s| s.mem_size() > 0) { + self.mark_range_as_used( + virtual_address_offset + segment.virtual_addr(), + segment.mem_size(), + ); + } + } + + /// Returns the first index of a `num` contiguous unused level 4 entries and marks them as + /// used. If `CONFIG.aslr` is enabled, this will return random contiguous available entries. + /// + /// Since this method marks each returned index as used, it can be used multiple times + /// to determine multiple unused virtual memory regions. + pub fn get_free_entries(&mut self, num: u64) -> PageTableIndex { + // Create an iterator over all available p4 indices with `num` contiguous free entries. + let mut free_entries = self + .entry_state + .windows(num.into_usize()) + .enumerate() + .filter(|(_, entries)| entries.iter().all(|used| !used)) + .map(|(idx, _)| idx); + + // Choose the free entry index. + let idx_opt = if let Some(rng) = self.rng.as_mut() { + // Randomly choose an index. + free_entries.choose(rng) + } else { + // Choose the first index. + free_entries.next() + }; + let Some(idx) = idx_opt else { + panic!("no usable level 4 entries found ({num} entries requested)"); + }; + + // Mark the entries as used. + for i in 0..num.into_usize() { + self.entry_state[idx + i] = true; + } + + PageTableIndex::new(idx.try_into().unwrap()) + } + + /// Returns a virtual address in one or more unused level 4 entries and marks them as used. + /// + /// This function calls [`get_free_entries`] internally, so all of its docs applies here + /// too. + pub fn get_free_address(&mut self, size: u64, alignment: u64) -> VirtAddr { + assert!(alignment.is_power_of_two()); + + const LEVEL_4_SIZE: u64 = 4096 * 512 * 512 * 512; + + let level_4_entries = (size + (LEVEL_4_SIZE - 1)) / LEVEL_4_SIZE; + let base = Page::from_page_table_indices_1gib( + self.get_free_entries(level_4_entries), + PageTableIndex::new(0), + ) + .start_address(); + + let offset = if let Some(rng) = self.rng.as_mut() { + // Choose a random offset. + let max_offset = LEVEL_4_SIZE - (size % LEVEL_4_SIZE); + let uniform_range = Uniform::from(0..max_offset / alignment); + uniform_range.sample(rng) * alignment + } else { + 0 + }; + + base + offset + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs new file mode 100644 index 00000000..0a4f729d --- /dev/null +++ b/common/src/lib.rs @@ -0,0 +1,695 @@ +#![cfg_attr(not(test), no_std)] +#![feature(step_trait)] +#![deny(unsafe_op_in_unsafe_fn)] + +use crate::legacy_memory_region::{LegacyFrameAllocator, LegacyMemoryRegion}; +use bootloader_api::{ + config::Mapping, + info::{FrameBuffer, FrameBufferInfo, MemoryRegion, TlsTemplate}, + BootInfo, BootloaderConfig, +}; +use bootloader_boot_config::{BootConfig, LevelFilter}; +use core::{alloc::Layout, arch::asm, mem::MaybeUninit, slice}; +use level_4_entries::UsedLevel4Entries; +use usize_conversions::FromUsize; +use x86_64::{ + structures::paging::{ + page_table::PageTableLevel, FrameAllocator, Mapper, OffsetPageTable, Page, PageSize, + PageTableFlags, PageTableIndex, PhysFrame, Size2MiB, Size4KiB, + }, + PhysAddr, VirtAddr, +}; +use xmas_elf::ElfFile; + +/// Provides a function to gather entropy and build a RNG. +mod entropy; +/// Provides a type that logs output as text to pixel-based framebuffers. +pub mod framebuffer; +mod gdt; +/// Provides a frame allocator based on a BIOS or UEFI memory map. +pub mod legacy_memory_region; +/// Provides a type to keep track of used entries in a level 4 page table. +pub mod level_4_entries; +/// Implements a loader for the kernel ELF binary. +pub mod load_kernel; +/// Provides a logger that logs output as text in various formats. +pub mod logger; +/// Provides a type that logs output as text to a Serial Being port. +pub mod serial; + +const PAGE_SIZE: u64 = 4096; + +/// Initialize a text-based logger using the given pixel-based framebuffer as output. +pub fn init_logger( + framebuffer: &'static mut [u8], + info: FrameBufferInfo, + log_level: LevelFilter, + frame_buffer_logger_status: bool, + serial_logger_status: bool, +) { + let logger = logger::LOGGER.get_or_init(move || { + logger::LockedLogger::new( + framebuffer, + info, + frame_buffer_logger_status, + serial_logger_status, + ) + }); + log::set_logger(logger).expect("logger already set"); + log::set_max_level(convert_level(log_level)); + log::info!("Framebuffer info: {:?}", info); +} + +fn convert_level(level: LevelFilter) -> log::LevelFilter { + match level { + LevelFilter::Off => log::LevelFilter::Off, + LevelFilter::Error => log::LevelFilter::Error, + LevelFilter::Warn => log::LevelFilter::Warn, + LevelFilter::Info => log::LevelFilter::Info, + LevelFilter::Debug => log::LevelFilter::Debug, + LevelFilter::Trace => log::LevelFilter::Trace, + } +} + +/// Required system information that should be queried from the BIOS or UEFI firmware. +#[derive(Debug, Copy, Clone)] +pub struct SystemInfo { + /// Information about the (still unmapped) framebuffer. + pub framebuffer: Option, + /// Address of the _Root System Description Pointer_ structure of the ACPI standard. + pub rsdp_addr: Option, + pub ramdisk_addr: Option, + pub ramdisk_len: u64, +} + +/// The physical address of the framebuffer and information about the framebuffer. +#[derive(Debug, Copy, Clone)] +pub struct RawFrameBufferInfo { + /// Start address of the pixel-based framebuffer. + pub addr: PhysAddr, + /// Information about the framebuffer, including layout and pixel format. + pub info: FrameBufferInfo, +} + +pub struct Kernel<'a> { + pub elf: ElfFile<'a>, + pub config: BootloaderConfig, + pub start_address: *const u8, + pub len: usize, +} + +impl<'a> Kernel<'a> { + pub fn parse(kernel_slice: &'a [u8]) -> Self { + let kernel_elf = ElfFile::new(kernel_slice).unwrap(); + let config = { + let section = kernel_elf + .find_section_by_name(".bootloader-config") + .expect("bootloader config section not found; kernel must be compiled against bootloader_api"); + let raw = section.raw_data(&kernel_elf); + BootloaderConfig::deserialize(raw) + .expect("kernel was compiled with incompatible bootloader_api version") + }; + Kernel { + elf: kernel_elf, + config, + start_address: kernel_slice.as_ptr(), + len: kernel_slice.len(), + } + } +} + +/// Loads the kernel ELF executable into memory and switches to it. +/// +/// This function is a convenience function that first calls [`set_up_mappings`], then +/// [`create_boot_info`], and finally [`switch_to_kernel`]. The given arguments are passed +/// directly to these functions, so see their docs for more info. +pub fn load_and_switch_to_kernel( + kernel: Kernel, + boot_config: BootConfig, + mut frame_allocator: LegacyFrameAllocator, + mut page_tables: PageTables, + system_info: SystemInfo, +) -> ! +where + I: ExactSizeIterator + Clone, + D: LegacyMemoryRegion, +{ + let config = kernel.config; + let mut mappings = set_up_mappings( + kernel, + &mut frame_allocator, + &mut page_tables, + system_info.framebuffer.as_ref(), + &config, + &system_info, + ); + let boot_info = create_boot_info( + &config, + &boot_config, + frame_allocator, + &mut page_tables, + &mut mappings, + system_info, + ); + switch_to_kernel(page_tables, mappings, boot_info); +} + +/// Sets up mappings for a kernel stack and the framebuffer. +/// +/// The `kernel_bytes` slice should contain the raw bytes of the kernel ELF executable. The +/// `frame_allocator` argument should be created from the memory map. The `page_tables` +/// argument should point to the bootloader and kernel page tables. The function tries to parse +/// the ELF file and create all specified mappings in the kernel-level page table. +/// +/// The `framebuffer_addr` and `framebuffer_size` fields should be set to the start address and +/// byte length the pixel-based framebuffer. These arguments are required because the functions +/// maps this framebuffer in the kernel-level page table, unless the `map_framebuffer` config +/// option is disabled. +/// +/// This function reacts to unexpected situations (e.g. invalid kernel ELF file) with a panic, so +/// errors are not recoverable. +pub fn set_up_mappings( + kernel: Kernel, + frame_allocator: &mut LegacyFrameAllocator, + page_tables: &mut PageTables, + framebuffer: Option<&RawFrameBufferInfo>, + config: &BootloaderConfig, + system_info: &SystemInfo, +) -> Mappings +where + I: ExactSizeIterator + Clone, + D: LegacyMemoryRegion, +{ + let kernel_page_table = &mut page_tables.kernel; + + let mut used_entries = UsedLevel4Entries::new( + frame_allocator.max_phys_addr(), + frame_allocator.len(), + framebuffer, + config, + &kernel.elf, + ) + .expect("Failed to mark level 4 entries as used"); + + // Enable support for the no-execute bit in page tables. + enable_nxe_bit(); + // Make the kernel respect the write-protection bits even when in ring 0 by default + enable_write_protect_bit(); + + let config = kernel.config; + let kernel_slice_start = PhysAddr::new(kernel.start_address as _); + let kernel_slice_len = u64::try_from(kernel.len).unwrap(); + + let (kernel_image_offset, entry_point, tls_template) = load_kernel::load_kernel( + kernel, + kernel_page_table, + frame_allocator, + &mut used_entries, + ) + .expect("no entry point"); + log::info!("Entry point at: {:#x}", entry_point.as_u64()); + // create a stack + let stack_start = { + // we need page-alignment because we want a guard page directly below the stack + let guard_page = mapping_addr_page_aligned( + config.mappings.kernel_stack, + // allocate an additional page as a guard page + Size4KiB::SIZE + config.kernel_stack_size, + &mut used_entries, + "kernel stack start", + ); + guard_page + 1 + }; + let stack_end_addr = stack_start.start_address() + config.kernel_stack_size; + + let stack_end = Page::containing_address(stack_end_addr - 1u64); + for page in Page::range_inclusive(stack_start, stack_end) { + let frame = frame_allocator + .allocate_frame() + .expect("frame allocation failed when mapping a kernel stack"); + let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE | PageTableFlags::NO_EXECUTE; + match unsafe { kernel_page_table.map_to(page, frame, flags, frame_allocator) } { + Ok(tlb) => tlb.flush(), + Err(err) => panic!("failed to map page {:?}: {:?}", page, err), + } + } + + // identity-map context switch function, so that we don't get an immediate pagefault + // after switching the active page table + let context_switch_function = PhysAddr::new(context_switch as *const () as u64); + let context_switch_function_start_frame: PhysFrame = + PhysFrame::containing_address(context_switch_function); + for frame in PhysFrame::range_inclusive( + context_switch_function_start_frame, + context_switch_function_start_frame + 1, + ) { + let page = Page::containing_address(VirtAddr::new(frame.start_address().as_u64())); + match unsafe { + // The parent table flags need to be both readable and writable to + // support recursive page tables. + // See https://github.com/rust-osdev/bootloader/issues/443#issuecomment-2130010621 + kernel_page_table.map_to_with_table_flags( + page, + frame, + PageTableFlags::PRESENT, + PageTableFlags::PRESENT | PageTableFlags::WRITABLE, + frame_allocator, + ) + } { + Ok(tlb) => tlb.flush(), + Err(err) => panic!("failed to identity map frame {:?}: {:?}", frame, err), + } + } + + // create, load, and identity-map GDT (required for working `iretq`) + let gdt_frame = frame_allocator + .allocate_frame() + .expect("failed to allocate GDT frame"); + gdt::create_and_load(gdt_frame); + let gdt_page = Page::containing_address(VirtAddr::new(gdt_frame.start_address().as_u64())); + match unsafe { + // The parent table flags need to be both readable and writable to + // support recursive page tables. + kernel_page_table.map_to_with_table_flags( + gdt_page, + gdt_frame, + PageTableFlags::PRESENT, + PageTableFlags::PRESENT | PageTableFlags::WRITABLE, + frame_allocator, + ) + } { + Ok(tlb) => tlb.flush(), + Err(err) => panic!("failed to identity map frame {:?}: {:?}", gdt_frame, err), + } + + // map framebuffer + let framebuffer_virt_addr = if let Some(framebuffer) = framebuffer { + log::info!("Map framebuffer"); + + let framebuffer_start_frame: PhysFrame = PhysFrame::containing_address(framebuffer.addr); + let framebuffer_end_frame = PhysFrame::containing_address( + framebuffer.addr + framebuffer.info.byte_len as u64 - 1u64, + ); + let start_page = mapping_addr_page_aligned( + config.mappings.framebuffer, + u64::from_usize(framebuffer.info.byte_len), + &mut used_entries, + "framebuffer", + ); + for (i, frame) in + PhysFrame::range_inclusive(framebuffer_start_frame, framebuffer_end_frame).enumerate() + { + let page = start_page + u64::from_usize(i); + let flags = + PageTableFlags::PRESENT | PageTableFlags::WRITABLE | PageTableFlags::NO_EXECUTE; + match unsafe { kernel_page_table.map_to(page, frame, flags, frame_allocator) } { + Ok(tlb) => tlb.flush(), + Err(err) => panic!( + "failed to map page {:?} to frame {:?}: {:?}", + page, frame, err + ), + } + } + let framebuffer_virt_addr = start_page.start_address(); + Some(framebuffer_virt_addr) + } else { + None + }; + let ramdisk_slice_len = system_info.ramdisk_len; + let ramdisk_slice_phys_start = system_info.ramdisk_addr.map(PhysAddr::new); + let ramdisk_slice_start = if let Some(physical_address) = ramdisk_slice_phys_start { + let start_page = mapping_addr_page_aligned( + config.mappings.ramdisk_memory, + system_info.ramdisk_len, + &mut used_entries, + "ramdisk start", + ); + let ramdisk_physical_start_page: PhysFrame = + PhysFrame::containing_address(physical_address); + let ramdisk_page_count = (system_info.ramdisk_len - 1) / Size4KiB::SIZE; + let ramdisk_physical_end_page = ramdisk_physical_start_page + ramdisk_page_count; + + let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE | PageTableFlags::NO_EXECUTE; + for (i, frame) in + PhysFrame::range_inclusive(ramdisk_physical_start_page, ramdisk_physical_end_page) + .enumerate() + { + let page = start_page + i as u64; + match unsafe { kernel_page_table.map_to(page, frame, flags, frame_allocator) } { + Ok(tlb) => tlb.ignore(), + Err(err) => panic!( + "Failed to map page {:?} to frame {:?}: {:?}", + page, frame, err + ), + }; + } + Some(start_page.start_address()) + } else { + None + }; + + let physical_memory_offset = if let Some(mapping) = config.mappings.physical_memory { + log::info!("Map physical memory"); + + let start_frame = PhysFrame::containing_address(PhysAddr::new(0)); + let max_phys = frame_allocator.max_phys_addr(); + let end_frame: PhysFrame = PhysFrame::containing_address(max_phys - 1u64); + + let size = max_phys.as_u64(); + let alignment = Size2MiB::SIZE; + let offset = mapping_addr(mapping, size, alignment, &mut used_entries) + .expect("start address for physical memory mapping must be 2MiB-page-aligned"); + + for frame in PhysFrame::range_inclusive(start_frame, end_frame) { + let page = Page::containing_address(offset + frame.start_address().as_u64()); + let flags = + PageTableFlags::PRESENT | PageTableFlags::WRITABLE | PageTableFlags::NO_EXECUTE; + match unsafe { kernel_page_table.map_to(page, frame, flags, frame_allocator) } { + Ok(tlb) => tlb.ignore(), + Err(err) => panic!( + "failed to map page {:?} to frame {:?}: {:?}", + page, frame, err + ), + }; + } + + Some(offset) + } else { + None + }; + + let recursive_index = if let Some(mapping) = config.mappings.page_table_recursive { + log::info!("Map page table recursively"); + let index = match mapping { + Mapping::Dynamic => used_entries.get_free_entries(1), + Mapping::FixedAddress(offset) => { + let offset = VirtAddr::new(offset); + let table_level = PageTableLevel::Four; + if !offset.is_aligned(table_level.entry_address_space_alignment()) { + panic!( + "Offset for recursive mapping must be properly aligned (must be \ + a multiple of {:#x})", + table_level.entry_address_space_alignment() + ); + } + + offset.p4_index() + } + }; + + let entry = &mut kernel_page_table.level_4_table_mut()[index]; + if !entry.is_unused() { + panic!( + "Could not set up recursive mapping: index {} already in use", + u16::from(index) + ); + } + let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE | PageTableFlags::NO_EXECUTE; + entry.set_frame(page_tables.kernel_level_4_frame, flags); + + Some(index) + } else { + None + }; + + Mappings { + framebuffer: framebuffer_virt_addr, + entry_point, + // Use the configured stack size, even if it's not page-aligned. However, we + // need to align it down to the next 16-byte boundary because the System V + // ABI requires a 16-byte stack alignment. + stack_top: stack_end_addr.align_down(16u8), + used_entries, + physical_memory_offset, + recursive_index, + tls_template, + + kernel_slice_start, + kernel_slice_len, + kernel_image_offset, + + ramdisk_slice_phys_start, + ramdisk_slice_start, + ramdisk_slice_len, + } +} + +/// Contains the addresses of all memory mappings set up by [`set_up_mappings`]. +pub struct Mappings { + /// The entry point address of the kernel. + pub entry_point: VirtAddr, + /// The (exclusive) end address of the kernel stack. + pub stack_top: VirtAddr, + /// Keeps track of used entries in the level 4 page table, useful for finding a free + /// virtual memory when needed. + pub used_entries: UsedLevel4Entries, + /// The start address of the framebuffer, if any. + pub framebuffer: Option, + /// The start address of the physical memory mapping, if enabled. + pub physical_memory_offset: Option, + /// The level 4 page table index of the recursive mapping, if enabled. + pub recursive_index: Option, + /// The thread local storage template of the kernel executable, if it contains one. + pub tls_template: Option, + + /// Start address of the kernel slice allocation in memory. + pub kernel_slice_start: PhysAddr, + /// Size of the kernel slice allocation in memory. + pub kernel_slice_len: u64, + /// Relocation offset of the kernel image in virtual memory. + pub kernel_image_offset: VirtAddr, + pub ramdisk_slice_phys_start: Option, + pub ramdisk_slice_start: Option, + pub ramdisk_slice_len: u64, +} + +/// Allocates and initializes the boot info struct and the memory map. +/// +/// The boot info and memory map are mapped to both the kernel and bootloader +/// address space at the same address. This makes it possible to return a Rust +/// reference that is valid in both address spaces. The necessary physical frames +/// are taken from the given `frame_allocator`. +pub fn create_boot_info( + config: &BootloaderConfig, + boot_config: &BootConfig, + mut frame_allocator: LegacyFrameAllocator, + page_tables: &mut PageTables, + mappings: &mut Mappings, + system_info: SystemInfo, +) -> &'static mut BootInfo +where + I: ExactSizeIterator + Clone, + D: LegacyMemoryRegion, +{ + log::info!("Allocate bootinfo"); + + // allocate and map space for the boot info + let (boot_info, memory_regions) = { + let boot_info_layout = Layout::new::(); + let regions = frame_allocator.memory_map_max_region_count(); + let memory_regions_layout = Layout::array::(regions).unwrap(); + let (combined, memory_regions_offset) = + boot_info_layout.extend(memory_regions_layout).unwrap(); + + let boot_info_addr = mapping_addr( + config.mappings.boot_info, + u64::from_usize(combined.size()), + u64::from_usize(combined.align()), + &mut mappings.used_entries, + ) + .expect("boot info addr is not properly aligned"); + + let memory_map_regions_addr = boot_info_addr + memory_regions_offset as u64; + let memory_map_regions_end = boot_info_addr + combined.size() as u64; + + let start_page = Page::containing_address(boot_info_addr); + let end_page = Page::containing_address(memory_map_regions_end - 1u64); + for page in Page::range_inclusive(start_page, end_page) { + let flags = + PageTableFlags::PRESENT | PageTableFlags::WRITABLE | PageTableFlags::NO_EXECUTE; + let frame = frame_allocator + .allocate_frame() + .expect("frame allocation for boot info failed"); + match unsafe { + page_tables + .kernel + .map_to(page, frame, flags, &mut frame_allocator) + } { + Ok(tlb) => tlb.flush(), + Err(err) => panic!("failed to map page {:?}: {:?}", page, err), + } + // we need to be able to access it too + match unsafe { + page_tables + .bootloader + .map_to(page, frame, flags, &mut frame_allocator) + } { + Ok(tlb) => tlb.flush(), + Err(err) => panic!("failed to map page {:?}: {:?}", page, err), + } + } + + let boot_info: &'static mut MaybeUninit = + unsafe { &mut *boot_info_addr.as_mut_ptr() }; + let memory_regions: &'static mut [MaybeUninit] = + unsafe { slice::from_raw_parts_mut(memory_map_regions_addr.as_mut_ptr(), regions) }; + (boot_info, memory_regions) + }; + + log::info!("Create Memory Map"); + + // build memory map + let memory_regions = frame_allocator.construct_memory_map( + memory_regions, + mappings.kernel_slice_start, + mappings.kernel_slice_len, + mappings.ramdisk_slice_phys_start, + mappings.ramdisk_slice_len, + ); + + log::info!("Create bootinfo"); + + // create boot info + let boot_info = boot_info.write({ + let mut info = BootInfo::new(memory_regions.into()); + info.framebuffer = mappings + .framebuffer + .map(|addr| unsafe { + FrameBuffer::new( + addr.as_u64(), + system_info + .framebuffer + .expect( + "there shouldn't be a mapping for the framebuffer if there is \ + no framebuffer", + ) + .info, + ) + }) + .into(); + info.physical_memory_offset = mappings.physical_memory_offset.map(VirtAddr::as_u64).into(); + info.recursive_index = mappings.recursive_index.map(Into::into).into(); + info.rsdp_addr = system_info.rsdp_addr.map(|addr| addr.as_u64()).into(); + info.tls_template = mappings.tls_template.into(); + info.ramdisk_addr = mappings + .ramdisk_slice_start + .map(|addr| addr.as_u64()) + .into(); + info.ramdisk_len = mappings.ramdisk_slice_len; + info.kernel_addr = mappings.kernel_slice_start.as_u64(); + info.kernel_len = mappings.kernel_slice_len as _; + info.kernel_image_offset = mappings.kernel_image_offset.as_u64(); + info._test_sentinel = boot_config._test_sentinel; + info + }); + + boot_info +} + +/// Switches to the kernel address space and jumps to the kernel entry point. +pub fn switch_to_kernel( + page_tables: PageTables, + mappings: Mappings, + boot_info: &'static mut BootInfo, +) -> ! { + let PageTables { + kernel_level_4_frame, + .. + } = page_tables; + let addresses = Addresses { + page_table: kernel_level_4_frame, + stack_top: mappings.stack_top, + entry_point: mappings.entry_point, + boot_info, + }; + + log::info!( + "Jumping to kernel entry point at {:?}", + addresses.entry_point + ); + + unsafe { + context_switch(addresses); + } +} + +/// Provides access to the page tables of the bootloader and kernel address space. +pub struct PageTables { + /// Provides access to the page tables of the bootloader address space. + pub bootloader: OffsetPageTable<'static>, + /// Provides access to the page tables of the kernel address space (not active). + pub kernel: OffsetPageTable<'static>, + /// The physical frame where the level 4 page table of the kernel address space is stored. + /// + /// Must be the page table that the `kernel` field of this struct refers to. + /// + /// This frame is loaded into the `CR3` register on the final context switch to the kernel. + pub kernel_level_4_frame: PhysFrame, +} + +/// Performs the actual context switch. +unsafe fn context_switch(addresses: Addresses) -> ! { + unsafe { + asm!( + r#" + xor rbp, rbp + mov cr3, {} + mov rsp, {} + push 0 + jmp {} + "#, + in(reg) addresses.page_table.start_address().as_u64(), + in(reg) addresses.stack_top.as_u64(), + in(reg) addresses.entry_point.as_u64(), + in("rdi") addresses.boot_info as *const _ as usize, + ); + } + unreachable!(); +} + +/// Memory addresses required for the context switch. +struct Addresses { + page_table: PhysFrame, + stack_top: VirtAddr, + entry_point: VirtAddr, + boot_info: &'static mut BootInfo, +} + +fn mapping_addr_page_aligned( + mapping: Mapping, + size: u64, + used_entries: &mut UsedLevel4Entries, + kind: &str, +) -> Page { + match mapping_addr(mapping, size, Size4KiB::SIZE, used_entries) { + Ok(addr) => Page::from_start_address(addr).unwrap(), + Err(addr) => panic!("{kind} address must be page-aligned (is `{addr:?})`"), + } +} + +fn mapping_addr( + mapping: Mapping, + size: u64, + alignment: u64, + used_entries: &mut UsedLevel4Entries, +) -> Result { + let addr = match mapping { + Mapping::FixedAddress(addr) => VirtAddr::new(addr), + Mapping::Dynamic => used_entries.get_free_address(size, alignment), + }; + if addr.is_aligned(alignment) { + Ok(addr) + } else { + Err(addr) + } +} + +fn enable_nxe_bit() { + use x86_64::registers::control::{Efer, EferFlags}; + unsafe { Efer::update(|efer| *efer |= EferFlags::NO_EXECUTE_ENABLE) } +} + +fn enable_write_protect_bit() { + use x86_64::registers::control::{Cr0, Cr0Flags}; + unsafe { Cr0::update(|cr0| *cr0 |= Cr0Flags::WRITE_PROTECT) }; +} diff --git a/common/src/load_kernel.rs b/common/src/load_kernel.rs new file mode 100644 index 00000000..b54bd246 --- /dev/null +++ b/common/src/load_kernel.rs @@ -0,0 +1,825 @@ +use crate::{level_4_entries::UsedLevel4Entries, PAGE_SIZE}; +use bootloader_api::{config::Mapping, info::TlsTemplate}; +use core::{cmp, iter::Step, mem::size_of, ops::Add}; + +use x86_64::{ + align_up, + structures::paging::{ + mapper::{MappedFrame, MapperAllSizes, TranslateResult}, + FrameAllocator, Page, PageSize, PageTableFlags as Flags, PhysFrame, Size4KiB, Translate, + }, + PhysAddr, VirtAddr, +}; +use xmas_elf::{ + dynamic, header, + program::{self, ProgramHeader, SegmentData, Type}, + sections::Rela, + ElfFile, +}; + +use super::Kernel; + +/// Used by [`Inner::make_mut`] and [`Inner::clean_copied_flag`]. +const COPIED: Flags = Flags::BIT_9; + +struct Loader<'a, M, F> { + elf_file: ElfFile<'a>, + inner: Inner<'a, M, F>, +} + +struct Inner<'a, M, F> { + kernel_offset: PhysAddr, + virtual_address_offset: VirtualAddressOffset, + page_table: &'a mut M, + frame_allocator: &'a mut F, +} + +impl<'a, M, F> Loader<'a, M, F> +where + M: MapperAllSizes + Translate, + F: FrameAllocator, +{ + fn new( + kernel: Kernel<'a>, + page_table: &'a mut M, + frame_allocator: &'a mut F, + used_entries: &mut UsedLevel4Entries, + ) -> Result { + log::info!("Elf file loaded at {:#p}", kernel.elf.input); + let kernel_offset = PhysAddr::new(&kernel.elf.input[0] as *const u8 as u64); + if !kernel_offset.is_aligned(PAGE_SIZE) { + return Err("Loaded kernel ELF file is not sufficiently aligned"); + } + + let elf_file = kernel.elf; + for program_header in elf_file.program_iter() { + program::sanity_check(program_header, &elf_file)?; + } + + let virtual_address_offset = match elf_file.header.pt2.type_().as_type() { + header::Type::None => unimplemented!(), + header::Type::Relocatable => unimplemented!(), + header::Type::Executable => match kernel.config.mappings.kernel_base { + Mapping::Dynamic => VirtualAddressOffset::zero(), + _ => { + return Err(concat!( + "Invalid kernel_code mapping. ", + "Executable can only be mapped at virtual_address_offset 0." + )) + } + }, + header::Type::SharedObject => { + let ElfMemoryRequirements { + size, + align, + min_addr, + } = calc_elf_memory_requirements(&elf_file); + match kernel.config.mappings.kernel_base { + Mapping::Dynamic => { + let offset = used_entries.get_free_address(size, align).as_u64(); + VirtualAddressOffset::new(i128::from(offset) - i128::from(min_addr)) + } + Mapping::FixedAddress(address) => { + VirtualAddressOffset::new(i128::from(address)) + } + } + } + header::Type::Core => unimplemented!(), + header::Type::ProcessorSpecific(_) => unimplemented!(), + }; + log::info!( + "virtual_address_offset: {:#x}", + virtual_address_offset.virtual_address_offset() + ); + + used_entries.mark_segments(elf_file.program_iter(), virtual_address_offset); + + header::sanity_check(&elf_file)?; + let loader = Loader { + elf_file, + inner: Inner { + kernel_offset, + virtual_address_offset, + page_table, + frame_allocator, + }, + }; + + Ok(loader) + } + + fn load_segments(&mut self) -> Result, &'static str> { + // Load the segments into virtual memory. + let mut tls_template = None; + for program_header in self.elf_file.program_iter() { + match program_header.get_type()? { + Type::Load => self.inner.handle_load_segment(program_header)?, + Type::Tls => { + if tls_template.is_none() { + tls_template = Some(self.inner.handle_tls_segment(program_header)?); + } else { + return Err("multiple TLS segments not supported"); + } + } + Type::Null + | Type::Dynamic + | Type::Interp + | Type::Note + | Type::ShLib + | Type::Phdr + | Type::GnuRelro + | Type::OsSpecific(_) + | Type::ProcessorSpecific(_) => {} + } + } + + // Apply relocations in virtual memory. + for program_header in self.elf_file.program_iter() { + if let Type::Dynamic = program_header.get_type()? { + self.inner + .handle_dynamic_segment(program_header, &self.elf_file)? + } + } + + // Mark some memory regions as read-only after relocations have been + // applied. + for program_header in self.elf_file.program_iter() { + if let Type::GnuRelro = program_header.get_type()? { + self.inner.handle_relro_segment(program_header); + } + } + + self.inner.remove_copied_flags(&self.elf_file).unwrap(); + + Ok(tls_template) + } + + fn entry_point(&self) -> VirtAddr { + VirtAddr::new(self.inner.virtual_address_offset + self.elf_file.header.pt2.entry_point()) + } +} + +impl<'a, M, F> Inner<'a, M, F> +where + M: MapperAllSizes + Translate, + F: FrameAllocator, +{ + fn handle_load_segment(&mut self, segment: ProgramHeader) -> Result<(), &'static str> { + log::info!("Handling Segment: {:x?}", segment); + + let phys_start_addr = self.kernel_offset + segment.offset(); + let start_frame: PhysFrame = PhysFrame::containing_address(phys_start_addr); + let end_frame: PhysFrame = + PhysFrame::containing_address(phys_start_addr + segment.file_size() - 1u64); + + let virt_start_addr = VirtAddr::new(self.virtual_address_offset + segment.virtual_addr()); + let start_page: Page = Page::containing_address(virt_start_addr); + + let mut segment_flags = Flags::PRESENT; + if !segment.flags().is_execute() { + segment_flags |= Flags::NO_EXECUTE; + } + if segment.flags().is_write() { + segment_flags |= Flags::WRITABLE; + } + + // map all frames of the segment at the desired virtual address + for frame in PhysFrame::range_inclusive(start_frame, end_frame) { + let offset = frame - start_frame; + let page = start_page + offset; + let flusher = unsafe { + // The parent table flags need to be both readable and writable to + // support recursive page tables. + // See https://github.com/rust-osdev/bootloader/issues/443#issuecomment-2130010621 + self.page_table + .map_to_with_table_flags( + page, + frame, + segment_flags, + Flags::PRESENT | Flags::WRITABLE, + self.frame_allocator, + ) + .map_err(|_err| "map_to failed")? + }; + // we operate on an inactive page table, so there's no need to flush anything + flusher.ignore(); + } + + // Handle .bss section (mem_size > file_size) + if segment.mem_size() > segment.file_size() { + // .bss section (or similar), which needs to be mapped and zeroed + self.handle_bss_section(&segment, segment_flags)?; + } + + Ok(()) + } + + fn handle_bss_section( + &mut self, + segment: &ProgramHeader, + segment_flags: Flags, + ) -> Result<(), &'static str> { + log::info!("Mapping bss section"); + + let virt_start_addr = VirtAddr::new(self.virtual_address_offset + segment.virtual_addr()); + let mem_size = segment.mem_size(); + let file_size = segment.file_size(); + + // calculate virtual memory region that must be zeroed + let zero_start = virt_start_addr + file_size; + let zero_end = virt_start_addr + mem_size; + + // a type alias that helps in efficiently clearing a page + type PageArray = [u64; Size4KiB::SIZE as usize / 8]; + const ZERO_ARRAY: PageArray = [0; Size4KiB::SIZE as usize / 8]; + + // In some cases, `zero_start` might not be page-aligned. This requires some + // special treatment because we can't safely zero a frame of the original file. + let data_bytes_before_zero = zero_start.as_u64() & 0xfff; + if data_bytes_before_zero != 0 { + // The last non-bss frame of the segment consists partly of data and partly of bss + // memory, which must be zeroed. Unfortunately, the file representation might have + // reused the part of the frame that should be zeroed to store the next segment. This + // means that we can't simply overwrite that part with zeroes, as we might overwrite + // other data this way. + // + // Example: + // + // XXXXXXXXXXXXXXX000000YYYYYYY000ZZZZZZZZZZZ virtual memory (XYZ are data) + // |·············| /·····/ /·········/ + // |·············| ___/·····/ /·········/ + // |·············|/·····/‾‾‾ /·········/ + // |·············||·····|/·̅·̅·̅·̅·̅·····/‾‾‾‾ + // XXXXXXXXXXXXXXXYYYYYYYZZZZZZZZZZZ file memory (zeros are not saved) + // ' ' ' ' ' + // The areas filled with dots (`·`) indicate a mapping between virtual and file + // memory. We see that the data regions `X`, `Y`, `Z` have a valid mapping, while + // the regions that are initialized with 0 have not. + // + // The ticks (`'`) below the file memory line indicate the start of a new frame. We + // see that the last frames of the `X` and `Y` regions in the file are followed + // by the bytes of the next region. So we can't zero these parts of the frame + // because they are needed by other memory regions. + // + // To solve this problem, we need to allocate a new frame for the last segment page + // and copy all data content of the original frame over. Afterwards, we can zero + // the remaining part of the frame since the frame is no longer shared with other + // segments now. + + let last_page = Page::containing_address(virt_start_addr + file_size - 1u64); + let new_frame = unsafe { self.make_mut(last_page) }; + let new_bytes_ptr = new_frame.start_address().as_u64() as *mut u8; + unsafe { + core::ptr::write_bytes( + new_bytes_ptr.add(data_bytes_before_zero as usize), + 0, + (Size4KiB::SIZE - data_bytes_before_zero) as usize, + ); + } + } + + // map additional frames for `.bss` memory that is not present in source file + let start_page: Page = + Page::containing_address(VirtAddr::new(align_up(zero_start.as_u64(), Size4KiB::SIZE))); + let end_page = Page::containing_address(zero_end - 1u64); + for page in Page::range_inclusive(start_page, end_page) { + // allocate a new unused frame + let frame = self.frame_allocator.allocate_frame().unwrap(); + + // zero frame, utilizing identity-mapping + let frame_ptr = frame.start_address().as_u64() as *mut PageArray; + unsafe { frame_ptr.write(ZERO_ARRAY) }; + + // map frame + let flusher = unsafe { + // The parent table flags need to be both readable and writable to + // support recursive page tables. + // See https://github.com/rust-osdev/bootloader/issues/443#issuecomment-2130010621 + self.page_table + .map_to_with_table_flags( + page, + frame, + segment_flags, + Flags::PRESENT | Flags::WRITABLE, + self.frame_allocator, + ) + .map_err(|_err| "Failed to map new frame for bss memory")? + }; + // we operate on an inactive page table, so we don't need to flush our changes + flusher.ignore(); + } + + Ok(()) + } + + /// Copy from the kernel address space. + /// + /// ## Panics + /// + /// Panics if a page is not mapped in `self.page_table`. + fn copy_from(&self, addr: VirtAddr, buf: &mut [u8]) { + // We can't know for sure that contiguous virtual address are contiguous + // in physical memory, so we iterate of the pages spanning the + // addresses, translate them to frames and copy the data. + + let end_inclusive_addr = Step::forward_checked(addr, buf.len() - 1) + .expect("end address outside of the virtual address space"); + let start_page = Page::::containing_address(addr); + let end_inclusive_page = Page::::containing_address(end_inclusive_addr); + + for page in start_page..=end_inclusive_page { + // Translate the virtual page to the physical frame. + let phys_addr = self + .page_table + .translate_page(page) + .expect("address is not mapped to the kernel's memory space"); + + // Figure out which address range we want to copy from the frame. + + // This page covers these addresses. + let page_start = page.start_address(); + let page_end_inclusive = page.start_address() + 4095u64; + + // We want to copy from the following address in this frame. + let start_copy_address = cmp::max(addr, page_start); + let end_inclusive_copy_address = cmp::min(end_inclusive_addr, page_end_inclusive); + + // These are the offsets into the frame we want to copy from. + let start_offset_in_frame = start_copy_address - page_start; + let end_inclusive_offset_in_frame = end_inclusive_copy_address - page_start; + + // Calculate how many bytes we want to copy from this frame. + let copy_len = end_inclusive_offset_in_frame - start_offset_in_frame + 1; + + // Calculate the physical addresses. + let start_phys_addr = phys_addr.start_address() + start_offset_in_frame; + + // These are the offsets from the start address. These correspond + // to the destination indices in `buf`. + let start_offset_in_buf = Step::steps_between(&addr, &start_copy_address).1.unwrap(); + + // Calculate the source slice. + // Utilize that frames are identity mapped. + let src_ptr = start_phys_addr.as_u64() as *const u8; + let src = unsafe { + // SAFETY: We know that this memory is valid because we got it + // as a result from a translation. There are not other + // references to it. + &*core::ptr::slice_from_raw_parts(src_ptr, copy_len as usize) + }; + + // Calculate the destination pointer. + let dest = &mut buf[start_offset_in_buf..][..copy_len as usize]; + + // Do the actual copy. + dest.copy_from_slice(src); + } + } + + /// Write to the kernel address space. + /// + /// ## Safety + /// - `addr` should refer to a page mapped by a Load segment. + /// + /// ## Panics + /// + /// Panics if a page is not mapped in `self.page_table`. + unsafe fn copy_to(&mut self, addr: VirtAddr, buf: &[u8]) { + // We can't know for sure that contiguous virtual address are contiguous + // in physical memory, so we iterate of the pages spanning the + // addresses, translate them to frames and copy the data. + + let end_inclusive_addr = Step::forward_checked(addr, buf.len() - 1) + .expect("the end address should be in the virtual address space"); + let start_page = Page::::containing_address(addr); + let end_inclusive_page = Page::::containing_address(end_inclusive_addr); + + for page in start_page..=end_inclusive_page { + // Translate the virtual page to the physical frame. + let phys_addr = unsafe { + // SAFETY: The caller asserts that the pages are mapped by a Load segment. + self.make_mut(page) + }; + + // Figure out which address range we want to copy from the frame. + + // This page covers these addresses. + let page_start = page.start_address(); + let page_end_inclusive = page.start_address() + 4095u64; + + // We want to copy from the following address in this frame. + let start_copy_address = cmp::max(addr, page_start); + let end_inclusive_copy_address = cmp::min(end_inclusive_addr, page_end_inclusive); + + // These are the offsets into the frame we want to copy from. + let start_offset_in_frame = start_copy_address - page_start; + let end_inclusive_offset_in_frame = end_inclusive_copy_address - page_start; + + // Calculate how many bytes we want to copy from this frame. + let copy_len = end_inclusive_offset_in_frame - start_offset_in_frame + 1; + + // Calculate the physical addresses. + let start_phys_addr = phys_addr.start_address() + start_offset_in_frame; + + // These are the offsets from the start address. These correspond + // to the destination indices in `buf`. + let start_offset_in_buf = Step::steps_between(&addr, &start_copy_address).1.unwrap(); + + // Calculate the source slice. + // Utilize that frames are identity mapped. + let dest_ptr = start_phys_addr.as_u64() as *mut u8; + let dest = unsafe { + // SAFETY: We know that this memory is valid because we got it + // as a result from a translation. There are not other + // references to it. + &mut *core::ptr::slice_from_raw_parts_mut(dest_ptr, copy_len as usize) + }; + + // Calculate the destination pointer. + let src = &buf[start_offset_in_buf..][..copy_len as usize]; + + // Do the actual copy. + dest.copy_from_slice(src); + } + } + + /// This method is intended for making the memory loaded by a Load segment mutable. + /// + /// All memory from a Load segment starts out by mapped to the same frames that + /// contain the elf file. Thus writing to memory in that state will cause aliasing issues. + /// To avoid that, we allocate a new frame, copy all bytes from the old frame to the new frame, + /// and remap the page to the new frame. At this point the page no longer aliases the elf file + /// and we can write to it. + /// + /// When we map the new frame we also set [`COPIED`] flag in the page table flags, so that + /// we can detect if the frame has already been copied when we try to modify the page again. + /// + /// ## Safety + /// - `page` should be a page mapped by a Load segment. + /// + /// ## Panics + /// Panics if the page is not mapped in `self.page_table`. + unsafe fn make_mut(&mut self, page: Page) -> PhysFrame { + let (frame, flags) = match self.page_table.translate(page.start_address()) { + TranslateResult::Mapped { + frame, + offset: _, + flags, + } => (frame, flags), + TranslateResult::NotMapped => panic!("{:?} is not mapped", page), + TranslateResult::InvalidFrameAddress(_) => unreachable!(), + }; + let frame = if let MappedFrame::Size4KiB(frame) = frame { + frame + } else { + // We only map 4k pages. + unreachable!() + }; + + if flags.contains(COPIED) { + // The frame was already copied, we are free to modify it. + return frame; + } + + // Allocate a new frame and copy the memory, utilizing that both frames are identity mapped. + let new_frame = self.frame_allocator.allocate_frame().unwrap(); + let frame_ptr = frame.start_address().as_u64() as *const u8; + let new_frame_ptr = new_frame.start_address().as_u64() as *mut u8; + unsafe { + core::ptr::copy_nonoverlapping(frame_ptr, new_frame_ptr, Size4KiB::SIZE as usize); + } + + // Replace the underlying frame and update the flags. + self.page_table.unmap(page).unwrap().1.ignore(); + let new_flags = flags | COPIED; + unsafe { + self.page_table + .map_to(page, new_frame, new_flags, self.frame_allocator) + .unwrap() + .ignore(); + } + + new_frame + } + + /// Cleans up the custom flags set by [`Inner::make_mut`]. + fn remove_copied_flags(&mut self, elf_file: &ElfFile) -> Result<(), &'static str> { + for program_header in elf_file.program_iter() { + if let Type::Load = program_header.get_type()? { + let start = self.virtual_address_offset + program_header.virtual_addr(); + let end = start + program_header.mem_size(); + let start = VirtAddr::new(start); + let end = VirtAddr::new(end); + let start_page = Page::containing_address(start); + let end_page = Page::containing_address(end - 1u64); + for page in Page::::range_inclusive(start_page, end_page) { + // Translate the page and get the flags. + let res = self.page_table.translate(page.start_address()); + let flags = match res { + TranslateResult::Mapped { + frame: _, + offset: _, + flags, + } => flags, + TranslateResult::NotMapped | TranslateResult::InvalidFrameAddress(_) => { + unreachable!("has the elf file not been mapped correctly?") + } + }; + + if flags.contains(COPIED) { + // Remove the flag. + unsafe { + self.page_table + .update_flags(page, flags & !COPIED) + .unwrap() + .ignore(); + } + } + } + } + } + Ok(()) + } + + fn handle_tls_segment(&mut self, segment: ProgramHeader) -> Result { + Ok(TlsTemplate { + start_addr: self.virtual_address_offset + segment.virtual_addr(), + mem_size: segment.mem_size(), + file_size: segment.file_size(), + }) + } + + fn handle_dynamic_segment( + &mut self, + segment: ProgramHeader, + elf_file: &ElfFile, + ) -> Result<(), &'static str> { + let data = segment.get_data(elf_file)?; + let data = if let SegmentData::Dynamic64(data) = data { + data + } else { + panic!("expected Dynamic64 segment") + }; + + // Find the `Rela`, `RelaSize` and `RelaEnt` entries. + let mut rela = None; + let mut rela_size = None; + let mut rela_ent = None; + for rel in data { + let tag = rel.get_tag()?; + match tag { + dynamic::Tag::Rela => { + let ptr = rel.get_ptr()?; + let prev = rela.replace(ptr); + if prev.is_some() { + return Err("Dynamic section contains more than one Rela entry"); + } + } + dynamic::Tag::RelaSize => { + let val = rel.get_val()?; + let prev = rela_size.replace(val); + if prev.is_some() { + return Err("Dynamic section contains more than one RelaSize entry"); + } + } + dynamic::Tag::RelaEnt => { + let val = rel.get_val()?; + let prev = rela_ent.replace(val); + if prev.is_some() { + return Err("Dynamic section contains more than one RelaEnt entry"); + } + } + _ => {} + } + } + let offset = if let Some(rela) = rela { + rela + } else { + // The section doesn't contain any relocations. + + if rela_size.is_some() || rela_ent.is_some() { + return Err("Rela entry is missing but RelaSize or RelaEnt have been provided"); + } + + return Ok(()); + }; + let total_size = rela_size.ok_or("RelaSize entry is missing")?; + let entry_size = rela_ent.ok_or("RelaEnt entry is missing")?; + + // Make sure that the reported size matches our `Rela`. + assert_eq!( + entry_size, + size_of::>() as u64, + "unsupported entry size: {entry_size}" + ); + + // Apply the relocations. + let num_entries = total_size / entry_size; + for idx in 0..num_entries { + let rela = self.read_relocation(offset, idx); + self.apply_relocation(rela, elf_file)?; + } + + Ok(()) + } + + /// Reads a relocation from a relocation table. + fn read_relocation(&self, relocation_table: u64, idx: u64) -> Rela { + // Calculate the address of the entry in the relocation table. + let offset = relocation_table + size_of::>() as u64 * idx; + let value = self.virtual_address_offset + offset; + let addr = VirtAddr::try_new(value).expect("relocation table is outside the address space"); + + // Read the Rela from the kernel address space. + let mut buf = [0; 24]; + self.copy_from(addr, &mut buf); + + // Convert the bytes we read into a `Rela`. + unsafe { + // SAFETY: Any bitpattern is valid for `Rela` and buf is + // valid for reads. + core::ptr::read_unaligned(&buf as *const u8 as *const Rela) + } + } + + fn apply_relocation( + &mut self, + rela: Rela, + elf_file: &ElfFile, + ) -> Result<(), &'static str> { + let symbol_idx = rela.get_symbol_table_index(); + assert_eq!( + symbol_idx, 0, + "relocations using the symbol table are not supported" + ); + + match rela.get_type() { + // R_AMD64_RELATIVE + 8 => { + // Make sure that the relocation happens in memory mapped + // by a Load segment. + check_is_in_load(elf_file, rela.get_offset())?; + + // Calculate the destination of the relocation. + let addr = self.virtual_address_offset + rela.get_offset(); + let addr = VirtAddr::new(addr); + + // Calculate the relocated value. + let value = self.virtual_address_offset + rela.get_addend(); + + // Write the relocated value to memory. + unsafe { + // SAFETY: We just verified that the address is in a Load segment. + self.copy_to(addr, &value.to_ne_bytes()); + } + } + ty => unimplemented!("relocation type {:x} not supported", ty), + } + + Ok(()) + } + + /// Mark a region of memory indicated by a GNU_RELRO segment as read-only. + /// + /// This is a security mitigation used to protect memory regions that + /// need to be writable while applying relocations, but should never be + /// written to after relocations have been applied. + fn handle_relro_segment(&mut self, program_header: ProgramHeader) { + let start = self.virtual_address_offset + program_header.virtual_addr(); + let end = start + program_header.mem_size(); + let start = VirtAddr::new(start); + let end = VirtAddr::new(end); + let start_page = Page::containing_address(start); + let end_page = Page::containing_address(end - 1u64); + for page in Page::::range_inclusive(start_page, end_page) { + // Translate the page and get the flags. + let res = self.page_table.translate(page.start_address()); + let flags = match res { + TranslateResult::Mapped { + frame: _, + offset: _, + flags, + } => flags, + TranslateResult::NotMapped | TranslateResult::InvalidFrameAddress(_) => { + unreachable!("has the elf file not been mapped correctly?") + } + }; + + if flags.contains(Flags::WRITABLE) { + // Remove the WRITABLE flag. + unsafe { + self.page_table + .update_flags(page, flags & !Flags::WRITABLE) + .unwrap() + .ignore(); + } + } + } + } +} + +/// Check that the virtual offset belongs to a load segment. +fn check_is_in_load(elf_file: &ElfFile, virt_offset: u64) -> Result<(), &'static str> { + for program_header in elf_file.program_iter() { + if let Type::Load = program_header.get_type()? { + if program_header.virtual_addr() <= virt_offset { + let offset_in_segment = virt_offset - program_header.virtual_addr(); + if offset_in_segment < program_header.mem_size() { + return Ok(()); + } + } + } + } + Err("offset is not in load segment") +} + +/// Loads the kernel ELF file given in `bytes` in the given `page_table`. +/// +/// Returns the kernel entry point address, it's thread local storage template (if any), +/// and a structure describing which level 4 page table entries are in use. +pub fn load_kernel( + kernel: Kernel<'_>, + page_table: &mut (impl MapperAllSizes + Translate), + frame_allocator: &mut impl FrameAllocator, + used_entries: &mut UsedLevel4Entries, +) -> Result<(VirtAddr, VirtAddr, Option), &'static str> { + let mut loader = Loader::new(kernel, page_table, frame_allocator, used_entries)?; + let tls_template = loader.load_segments()?; + + Ok(( + VirtAddr::new(loader.inner.virtual_address_offset.virtual_address_offset() as u64), + loader.entry_point(), + tls_template, + )) +} + +/// Basic information about the memory segments of an elf file. +pub struct ElfMemoryRequirements { + /// total size needed for all segments + pub size: u64, + /// memory alignment for the elf file + pub align: u64, + /// the smallest virtual address used by the elf file + pub min_addr: u64, +} + +/// Calculates basic requirements needed to allocate memory for an elf file. +pub fn calc_elf_memory_requirements(elf_file: &ElfFile) -> ElfMemoryRequirements { + // Find the highest virtual memory address and the biggest alignment. + let load_program_headers = elf_file + .program_iter() + .filter(|h| matches!(h.get_type(), Ok(Type::Load))); + let max_addr = load_program_headers + .clone() + .map(|h| h.virtual_addr() + h.mem_size()) + .max() + .unwrap_or(0); + let min_addr = load_program_headers + .clone() + .map(|h| h.virtual_addr()) + .min() + .unwrap_or(0); + let size = max_addr - min_addr; + let align = load_program_headers.map(|h| h.align()).max().unwrap_or(1); + ElfMemoryRequirements { + size, + align, + min_addr, + } +} + +/// A helper type used to offset virtual addresses for position independent +/// executables. +#[derive(Clone, Copy)] +pub struct VirtualAddressOffset { + virtual_address_offset: i128, +} + +impl VirtualAddressOffset { + pub fn zero() -> Self { + Self::new(0) + } + + pub fn new(virtual_address_offset: i128) -> Self { + Self { + virtual_address_offset, + } + } + + pub fn virtual_address_offset(&self) -> i128 { + self.virtual_address_offset + } +} + +impl Add for VirtualAddressOffset { + type Output = u64; + + fn add(self, offset: u64) -> Self::Output { + u64::try_from( + self.virtual_address_offset + .checked_add(i128::from(offset)) + .unwrap(), + ) + .unwrap() + } +} diff --git a/common/src/logger.rs b/common/src/logger.rs new file mode 100644 index 00000000..a7491c58 --- /dev/null +++ b/common/src/logger.rs @@ -0,0 +1,71 @@ +use crate::{framebuffer::FrameBufferWriter, serial::SerialPort}; +use bootloader_api::info::FrameBufferInfo; +use conquer_once::spin::OnceCell; +use core::fmt::Write; +use spinning_top::Spinlock; + +/// The global logger instance used for the `log` crate. +pub static LOGGER: OnceCell = OnceCell::uninit(); + +/// A logger instance protected by a spinlock. +pub struct LockedLogger { + framebuffer: Option>, + serial: Option>, +} + +impl LockedLogger { + /// Create a new instance that logs to the given framebuffer. + pub fn new( + framebuffer: &'static mut [u8], + info: FrameBufferInfo, + frame_buffer_logger_status: bool, + serial_logger_status: bool, + ) -> Self { + let framebuffer = match frame_buffer_logger_status { + true => Some(Spinlock::new(FrameBufferWriter::new(framebuffer, info))), + false => None, + }; + + let serial = match serial_logger_status { + true => Some(Spinlock::new(unsafe { SerialPort::init() })), + false => None, + }; + + LockedLogger { + framebuffer, + serial, + } + } + + /// Force-unlocks the logger to prevent a deadlock. + /// + /// ## Safety + /// This method is not memory safe and should be only used when absolutely necessary. + pub unsafe fn force_unlock(&self) { + if let Some(framebuffer) = &self.framebuffer { + unsafe { framebuffer.force_unlock() }; + } + if let Some(serial) = &self.serial { + unsafe { serial.force_unlock() }; + } + } +} + +impl log::Log for LockedLogger { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + if let Some(framebuffer) = &self.framebuffer { + let mut framebuffer = framebuffer.lock(); + writeln!(framebuffer, "{:5}: {}", record.level(), record.args()).unwrap(); + } + if let Some(serial) = &self.serial { + let mut serial = serial.lock(); + writeln!(serial, "{:5}: {}", record.level(), record.args()).unwrap(); + } + } + + fn flush(&self) {} +} diff --git a/common/src/serial.rs b/common/src/serial.rs new file mode 100644 index 00000000..8435c8e1 --- /dev/null +++ b/common/src/serial.rs @@ -0,0 +1,28 @@ +use core::fmt; + +pub struct SerialPort { + port: uart_16550::SerialPort, +} + +impl SerialPort { + /// # Safety + /// + /// unsafe because this function must only be called once + pub unsafe fn init() -> Self { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + Self { port } + } +} + +impl fmt::Write for SerialPort { + fn write_str(&mut self, s: &str) -> fmt::Result { + for char in s.bytes() { + match char { + b'\n' => self.port.write_str("\r\n").unwrap(), + byte => self.port.send(byte), + } + } + Ok(()) + } +} diff --git a/doc/chainloading.md b/docs/chainloading.md similarity index 100% rename from doc/chainloading.md rename to docs/chainloading.md diff --git a/docs/create-disk-image.md b/docs/create-disk-image.md new file mode 100644 index 00000000..f4da7e5a --- /dev/null +++ b/docs/create-disk-image.md @@ -0,0 +1,87 @@ +# Template: Create a Disk Image + +The [`bootloader`](https://docs.rs/bootloader/0.11) crate provides simple functions to create bootable disk images from a kernel. The basic idea is to build your kernel first and then invoke a builder function that calls the disk image creation functions of the `bootloader` crate. + +A good way to implement this is to move your kernel into a `kernel` subdirectory. Then you can create +a new `os` crate at the top level that defines a [workspace](https://doc.rust-lang.org/cargo/reference/workspaces.html). The root package has build-dependencies on the `kernel` [artifact](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies) and on the bootloader crate. This allows you to create the bootable disk image in a [cargo build script](https://doc.rust-lang.org/cargo/reference/build-scripts.html) and launch the created image in QEMU in the `main` function. + +The files could look like this: + +```toml +# .cargo/config.toml + +[unstable] +# enable the unstable artifact-dependencies feature, see +# https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies +bindeps = true +``` + +```toml +# Cargo.toml + +[package] +name = "os" # or any other name +version = "0.1.0" + +[build-dependencies] +bootloader = "0.11" +test-kernel = { path = "kernel", artifact = "bin", target = "x86_64-unknown-none" } + +[dependencies] +# used for UEFI booting in QEMU +ovmf-prebuilt = "0.1.0-alpha.1" + +[workspace] +members = ["kernel"] +``` + +```rust +// build.rs + +use std::path::PathBuf; + +fn main() { + // set by cargo, build scripts should use this directory for output files + let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + // set by cargo's artifact dependency feature, see + // https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies + let kernel = PathBuf::from(std::env::var_os("CARGO_BIN_FILE_KERNEL_kernel").unwrap()); + + // create an UEFI disk image (optional) + let uefi_path = out_dir.join("uefi.img"); + bootloader::UefiBoot::new(&kernel).create_disk_image(&uefi_path).unwrap(); + + // create a BIOS disk image + let bios_path = out_dir.join("bios.img"); + bootloader::BiosBoot::new(&kernel).create_disk_image(&bios_path).unwrap(); + + // pass the disk image paths as env variables to the `main.rs` + println!("cargo:rustc-env=UEFI_PATH={}", uefi_path.display()); + println!("cargo:rustc-env=BIOS_PATH={}", bios_path.display()); +} +``` + +```rust +// src/main.rs + +fn main() { + // read env variables that were set in build script + let uefi_path = env!("UEFI_PATH"); + let bios_path = env!("BIOS_PATH"); + + // choose whether to start the UEFI or BIOS image + let uefi = true; + + let mut cmd = std::process::Command::new("qemu-system-x86_64"); + if uefi { + cmd.arg("-bios").arg(ovmf_prebuilt::ovmf_pure_efi()); + cmd.arg("-drive").arg(format!("format=raw,file={uefi_path}")); + } else { + cmd.arg("-drive").arg(format!("format=raw,file={bios_path}")); + } + let mut child = cmd.spawn().unwrap(); + child.wait().unwrap(); +} +``` + +Now you should be able to use `cargo build` to create a bootable disk image and `cargo run` to run in QEMU. Your kernel is automatically recompiled when it changes. For more advanced usage, you can add command-line arguments to your `main.rs` to e.g. pass additional arguments to QEMU or to copy the disk images to some path to make it easier to find them (e.g. for copying them to an thumb drive). diff --git a/docs/migration/README.md b/docs/migration/README.md new file mode 100644 index 00000000..5eef5ed0 --- /dev/null +++ b/docs/migration/README.md @@ -0,0 +1,6 @@ +# Migration Guides + +These guides explain how to migrate your project from older `bootloader` versions: + +- [Migration from `v0.9.X`](v0.9.md) +- [Migration from `v0.10.X`](v0.10.md) diff --git a/docs/migration/v0.10.md b/docs/migration/v0.10.md new file mode 100644 index 00000000..7536f231 --- /dev/null +++ b/docs/migration/v0.10.md @@ -0,0 +1,41 @@ +# Migration from bootloader `v0.10` + +This guide summarizes the steps for migrating from `bootloader v0.10.X` to `bootloader v0.11`. + +## Kernel + +- Replace the `bootloader` dependency of your kernel with a dependency on the `bootloader_api` crate and adjust the import path in your `main.rs`: + ```diff + # in Cargo.toml + + -bootloader = { version = "0.10.13" } + +bootloader_api = "0.11" + ``` + ```diff + // in main.rs + + -use bootloader::{entry_point, BootInfo}; + +use bootloader_api::{entry_point, BootInfo}; + ``` +- If you used optional features, such as `map-physical-memory`, you can enable them again through the `entry_point` macro: + ```rust + use bootloader_api::config::{BootloaderConfig, Mapping}; + + pub static BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.mappings.physical_memory = Some(Mapping::Dynamic); + config + }; + + // add a `config` argument to the `entry_point` macro call + entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + ``` + See the [`BootloaderConfig`](https://docs.rs/bootloader_api/0.11/bootloader_api/config/struct.BootloaderConfig.html) struct for all configuration options. + +To build your kernel, run **`cargo build --target x86_64-unknown-none`**. Since the `x86_64-unknown-none` target is a Tier-2 target, there is no need for `bootimage`, `cargo-xbuild`, or `xargo` anymore. Instead, you can run `rustup target add x86_64-unknown-none` to download precompiled versions of the `core` and `alloc` crates. There is no need for custom JSON-based target files anymore. + +## Booting + +The `bootloader v0.11` release simplifies the disk image creation. The [`bootloader`](https://docs.rs/bootloader/0.11) crate now provides simple functions to create bootable disk images from a kernel. The basic idea is to build your kernel first and then invoke a builder function that calls the disk image creation functions of the `bootloader` crate. + +See our [disk image creation template](../create-disk-image.md) for a detailed explanation of the new build process. diff --git a/docs/migration/v0.9.md b/docs/migration/v0.9.md new file mode 100644 index 00000000..2770e432 --- /dev/null +++ b/docs/migration/v0.9.md @@ -0,0 +1,49 @@ +# Migration from bootloader `v0.9` + +This guide summarizes the steps for migrating from `bootloader v0.9.X` to `bootloader v0.11`. Note that some bigger changes are required to support UEFI booting (to support the framebuffer and APIC). + +## Kernel + +- Replace the `bootloader` dependency of your kernel with a dependency on the `bootloader_api` crate: + ```diff + -bootloader = { version = "0.9.23", features = [...]} + +bootloader_api = "0.11" + ``` +- In your `main.rs`, adjust the import path and change the signature of the entry point function: + ```diff + -use bootloader::{entry_point, BootInfo}; + +use bootloader_api::{entry_point, BootInfo}; + + entry_point!(kernel_main); + + -fn kernel_main(boot_info: &'static BootInfo) -> ! { + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + ``` +- If you used optional features, such as `map_physical_memory`, you can enable them again through the `entry_point` macro: + ```rust + use bootloader_api::config::{BootloaderConfig, Mapping}; + + pub static BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.mappings.physical_memory = Some(Mapping::Dynamic); + config + }; + + // add a `config` argument to the `entry_point` macro call + entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + ``` + See the [`BootloaderConfig`](https://docs.rs/bootloader_api/0.11/bootloader_api/config/struct.BootloaderConfig.html) struct for all configuration options. +- The `v0.11` version of the bootloader sets up a pixel-based framebuffer instead of using the VGA text mode. This means that you have to rewrite your screen output code. See the [`common/src/framebuffer.rs`](../../common/src/framebuffer.rs) module for an example implementation based on the [`noto-sans-mono-bitmap`](https://docs.rs/noto-sans-mono-bitmap/latest/noto_sans_mono_bitmap/index.html) and [`log`](https://docs.rs/log/latest) crates. +- If you want to use UEFI booting, you cannot use the [legacy PIC](https://wiki.osdev.org/8259_PIC) for interrupt handling. Instead, you have to set up the [`APIC`](https://wiki.osdev.org/APIC). Unfortunately, we don't have a guide for this yet, but the basic steps are: + - The `boot_info.rsdp_addr` field will tell you the physical address of the [`RSDP`](https://wiki.osdev.org/RSDP) structure, which is part of the [`ACPI`](https://en.wikipedia.org/wiki/ACPI) standard. Note that `ACPI` (_Advanced Configuration and Power Interface_) and `APIC` (_Advanced Programmable Interrupt Controller_) are completely different things that just have confusingly similar acronyms. + - Use the [`acpi`](https://docs.rs/acpi/4.1.1/acpi/index.html) crate and its [`AcpiTables::from_rsdp`](https://docs.rs/acpi/4.1.1/acpi/struct.AcpiTables.html#method.from_rsdp) function to load the ACPI tables. Then use the [`platform_info`](https://docs.rs/acpi/4.1.1/acpi/struct.AcpiTables.html#method.platform_info) method and read the [`interrupt_model`](https://docs.rs/acpi/4.1.1/acpi/platform/struct.PlatformInfo.html#structfield.interrupt_model) field of the returned `PlatformInfo`. This field gives you all the necessary information about the system's interrupt controller. + - Parse and set up the local and IO APIC. We're working on a [crate](https://github.com/rust-osdev/apic) for that, but it's still in a very early state. We would love contributions! Alternatively, there are some other crates on crates.io that might work too. +- If you reload the GDT at some point, ensure that all segment registers are written, including `ss` and `ds`. The `v0.9` version of the bootloader used to initialize some of them to 0, but this is no longer the case. If you don't do this, [a general protection fault might happen on `iretq`](https://github.com/rust-osdev/bootloader/issues/196). + +To build your kernel, run **`cargo build --target x86_64-unknown-none`**. Since the `x86_64-unknown-none` target is a Tier-2 target, there is no need for `bootimage`, `cargo-xbuild`, or `xargo` anymore. Instead, you can run `rustup target add x86_64-unknown-none` to download precompiled versions of the `core` and `alloc` crates. There is no need for custom JSON-based target files anymore. + +## Booting + +The `bootloader v0.11` release does not use the `bootimage` tool anymore. Instead, the [`bootloader`](https://docs.rs/bootloader/0.11) crate provides functions to create bootable disk images from a kernel. The basic idea is to build your kernel first and then invoke a builder function that calls the disk image creation functions of the `bootloader` crate. + +See our [disk image creation template](../create-disk-image.md) for a detailed explanation of the new build process. diff --git a/example-kernel/.cargo/config b/example-kernel/.cargo/config deleted file mode 100644 index ab792469..00000000 --- a/example-kernel/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "x86_64-example-kernel.json" diff --git a/example-kernel/.gitignore b/example-kernel/.gitignore deleted file mode 100644 index eccd7b4a..00000000 --- a/example-kernel/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target/ -**/*.rs.bk diff --git a/example-kernel/Cargo.lock b/example-kernel/Cargo.lock deleted file mode 100644 index 6cf76349..00000000 --- a/example-kernel/Cargo.lock +++ /dev/null @@ -1,69 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "array-init" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "bit_field" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "bitflags" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "example-kernel" -version = "0.1.0" -dependencies = [ - "x86_64 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "nodrop" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "os_bootinfo" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "usize_conversions" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "ux" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "x86_64" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "array-init 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "bit_field 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "os_bootinfo 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "usize_conversions 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ux 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[metadata] -"checksum array-init 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "23589ecb866b460d3a0f1278834750268c607e8e28a1b982c907219f3178cd72" -"checksum bit_field 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed8765909f9009617974ab6b7d332625b320b33c326b1e9321382ef1999b5d56" -"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" -"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" -"checksum os_bootinfo 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "66481dbeb5e773e7bd85b63cd6042c30786f834338288c5ec4f3742673db360a" -"checksum usize_conversions 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f70329e2cbe45d6c97a5112daad40c34cd9a4e18edb5a2a18fefeb584d8d25e5" -"checksum ux 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "88dfeb711b61ce620c0cb6fd9f8e3e678622f0c971da2a63c4b3e25e88ed012f" -"checksum x86_64 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f9258d7e2dd25008d69e8c9e9ee37865887a5e1e3d06a62f1cb3f6c209e6f177" diff --git a/example-kernel/Cargo.toml b/example-kernel/Cargo.toml deleted file mode 100644 index a9c76f70..00000000 --- a/example-kernel/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "example-kernel" -version = "0.1.0" -authors = ["Philipp Oppermann "] -edition = "2018" - -[dependencies] -x86_64 = "0.3.4" diff --git a/example-kernel/src/main.rs b/example-kernel/src/main.rs deleted file mode 100644 index a953f2e9..00000000 --- a/example-kernel/src/main.rs +++ /dev/null @@ -1,31 +0,0 @@ -#![no_std] // don't link the Rust standard library -#![no_main] // disable all Rust-level entry points - -use core::panic::PanicInfo; - -static HELLO: &[u8] = b"Hello World!"; - -#[no_mangle] // don't mangle the name of this function -pub extern "C" fn _start() -> ! { - // this function is the entry point, since the linker looks for a function - // named `_start` by default - - let vga_buffer = 0xb8000 as *mut u8; - - // print `HELLO` to the screen (see - // https://os.phil-opp.com/minimal-rust-kernel/#printing-to-screen) - for (i, &byte) in HELLO.iter().enumerate() { - unsafe { - *vga_buffer.offset(i as isize * 2) = byte; - *vga_buffer.offset(i as isize * 2 + 1) = 0xb; - } - } - - loop {} -} - -/// This function is called on panic. -#[panic_handler] -fn panic(_info: &PanicInfo) -> ! { - loop {} -} diff --git a/example-kernel/x86_64-example-kernel.json b/example-kernel/x86_64-example-kernel.json deleted file mode 100644 index 9afe809f..00000000 --- a/example-kernel/x86_64-example-kernel.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "llvm-target": "x86_64-unknown-none", - "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", - "arch": "x86_64", - "target-endian": "little", - "target-pointer-width": "64", - "target-c-int-width": "32", - "os": "none", - "executables": true, - "linker-flavor": "ld.lld", - "linker": "rust-lld", - "panic-strategy": "abort", - "disable-redzone": true, - "features": "-mmx,-sse,+soft-float" - } diff --git a/real_mode/first_stage/x86-16bit.json b/i386-code16-boot-sector.json similarity index 73% rename from real_mode/first_stage/x86-16bit.json rename to i386-code16-boot-sector.json index b3f34cbc..9cf5c514 100644 --- a/real_mode/first_stage/x86-16bit.json +++ b/i386-code16-boot-sector.json @@ -1,7 +1,7 @@ { "arch": "x86", "cpu": "i386", - "data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-f64:32:64-f80:32-n8:16:32-S128", + "data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128", "dynamic-linking": false, "executables": true, "linker-flavor": "ld.lld", @@ -10,16 +10,11 @@ "max-atomic-width": 64, "position-independent-executables": false, "disable-redzone": true, - "target-c-int-width": "32", + "target-c-int-width": 32, "target-pointer-width": "32", "target-endian": "little", "panic-strategy": "abort", "os": "none", "vendor": "unknown", - "relocation-model": "static", - "pre-link-args": { - "ld.lld": [ - "--script=16-bit-linker.ld" - ] - } + "relocation-model": "static" } diff --git a/real_mode/real_mode/x86-16bit.json b/i386-code16-stage-2.json similarity index 70% rename from real_mode/real_mode/x86-16bit.json rename to i386-code16-stage-2.json index 35bd2784..9cf5c514 100644 --- a/real_mode/real_mode/x86-16bit.json +++ b/i386-code16-stage-2.json @@ -1,7 +1,7 @@ { "arch": "x86", "cpu": "i386", - "data-layout": "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128", + "data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128", "dynamic-linking": false, "executables": true, "linker-flavor": "ld.lld", @@ -10,12 +10,11 @@ "max-atomic-width": 64, "position-independent-executables": false, "disable-redzone": true, - "target-c-int-width": "32", + "target-c-int-width": 32, "target-pointer-width": "32", "target-endian": "little", "panic-strategy": "abort", "os": "none", "vendor": "unknown", - "relocation_model": "static", - "eliminate_frame_pointer": true -} \ No newline at end of file + "relocation-model": "static" +} diff --git a/i686-stage-3.json b/i686-stage-3.json new file mode 100644 index 00000000..82fc22c6 --- /dev/null +++ b/i686-stage-3.json @@ -0,0 +1,22 @@ +{ + "arch": "x86", + "cpu": "i386", + "data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128", + "dynamic-linking": false, + "executables": true, + "linker-flavor": "ld.lld", + "linker": "rust-lld", + "llvm-target": "i386-unknown-none", + "max-atomic-width": 64, + "position-independent-executables": false, + "disable-redzone": true, + "target-c-int-width": 32, + "target-pointer-width": "32", + "target-endian": "little", + "panic-strategy": "abort", + "os": "none", + "vendor": "unknown", + "relocation-model": "static", + "features": "+soft-float,-sse,-mmx", + "rustc-abi": "x86-softfloat" +} diff --git a/linker.ld b/linker.ld deleted file mode 100644 index 4e58320a..00000000 --- a/linker.ld +++ /dev/null @@ -1,50 +0,0 @@ -ENTRY(_start) - -SECTIONS { - . = 0x500; - /* buffer for loading the kernel */ - _kernel_buffer = .; - . += 512; - /* page tables */ - . = ALIGN(0x1000); - __page_table_start = .; - _p4 = .; - . += 0x1000; - _p3 = .; - . += 0x1000; - _p2 = .; - . += 0x1000; - _p1 = .; - . += 0x1000; - __page_table_end = .; - __bootloader_start = .; - _memory_map = .; - . += 0x1000; - - _stack_start = .; - . = 0x7c00; - _stack_end = .; - - .bootloader : - { - /* first stage */ - *(.boot-first-stage) - - /* rest of bootloader */ - _rest_of_bootloader_start_addr = .; - *(.boot) - *(.context_switch) - *(.text .text.*) - *(.rodata .rodata.*) - *(.data .data.*) - *(.got) - . = ALIGN(512); - _rest_of_bootloader_end_addr = .; - __bootloader_end = .; - } - - .kernel : - { - KEEP(*(.kernel)) - } -} diff --git a/real_mode/.cargo/config b/real_mode/.cargo/config deleted file mode 100644 index 168bd90a..00000000 --- a/real_mode/.cargo/config +++ /dev/null @@ -1,5 +0,0 @@ -[build] -target = "x86-32bit.json" - -[alias] -xbuild = "build -Zbuild-std=core" diff --git a/real_mode/.gitignore b/real_mode/.gitignore deleted file mode 100644 index bae8924d..00000000 --- a/real_mode/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/target/ -**/*.rs.bk -/test.bin - diff --git a/real_mode/Cargo.lock b/real_mode/Cargo.lock deleted file mode 100644 index fd402fe0..00000000 --- a/real_mode/Cargo.lock +++ /dev/null @@ -1,14 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "bootloader" -version = "0.1.0" -dependencies = [ - "llvm-tools", -] - -[[package]] -name = "llvm-tools" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955be5d0ca0465caf127165acb47964f911e2bc26073e865deb8be7189302faf" diff --git a/real_mode/Cargo.toml b/real_mode/Cargo.toml deleted file mode 100644 index da95f3ea..00000000 --- a/real_mode/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "bootloader" -version = "0.1.0" -authors = ["Philipp Oppermann "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] - -[build-dependencies] -llvm-tools = "0.1.1" - -[profile.release] -opt-level = "s" -lto = true -debug = true -codegen-units = 1 diff --git a/real_mode/README.md b/real_mode/README.md deleted file mode 100644 index 29a85726..00000000 --- a/real_mode/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# 16-bit Rust (Experiment) - -This is an experiment to translate the 16-bit code of the bootloader from assembly to Rust. - -## Building - -To build the project, use cargo-xbuild: - -``` -cargo xbuild --release -``` - -The BIOS only loads the first 512 bytes of our executable into memory, so the amount of code that this binary can contain is very limited. This is also the reason why this can only be built in release mode. - -If the code does not fit into 512 bytes, the linker will throw the following error: - -> rust-lld: error: linker.ld:16: unable to move location counter backward for: .bootloader - -## Creating a Disk Image - -The output of `cargo xbuild` is an ELF binary, which can't be loaded directly by the BIOS. To boot our project, we must therefore convert it into a flat binary first. This works with the following `objcopy` command: - -``` -objcopy -I elf32-i386 -O binary target/x86-32bit/release/bootloader image.bin -``` - -This creates a file named `image.bin` in the root folder of the project, which is a bootable disk image. - -## Running it in QEMU - -To run the disk image in QEMU, execute the following command: - -``` -qemu-system-x86_64 -drive format=raw,file=image.bin -``` diff --git a/real_mode/build.rs b/real_mode/build.rs deleted file mode 100644 index 0617a56a..00000000 --- a/real_mode/build.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::process::Command; -use std::env; -use std::path::Path; -use llvm_tools::{LlvmTools, exe}; - -fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); - let llvm_tools = LlvmTools::new().expect("LLVM tools not found"); - let objcopy = llvm_tools.tool(&exe("llvm-objcopy")).expect("llvm-objcopy not found"); - - build_subproject(Path::new("first_stage"), &["_start", "print_char"], "x86-16bit.json", &out_dir, &objcopy); - build_subproject(Path::new("real_mode"), &["second_stage"], "x86-16bit.json", &out_dir, &objcopy); -} - -fn build_subproject(dir: &Path, global_symbols: &[&str], target: &str, out_dir: &str, objcopy: &Path) { - let dir_name = dir.file_name().unwrap().to_str().unwrap(); - let manifest_path = dir.join("Cargo.toml"); - let out_path = Path::new(&out_dir); - assert!(global_symbols.len() > 0, "must have at least one global symbol"); - - // build - let mut cmd = Command::new("cargo"); - cmd.arg("xbuild").arg("--release"); - cmd.arg("--verbose"); - cmd.arg(format!("--manifest-path={}", manifest_path.display())); - cmd.arg(format!("--target={}", dir.join(target).display())); - cmd.arg("-Z").arg("unstable-options"); - cmd.arg("--out-dir").arg(&out_dir); - cmd.arg("--target-dir").arg(out_path.join("target").join(dir_name)); - cmd.env_remove("RUSTFLAGS"); - cmd.env("XBUILD_SYSROOT_PATH", out_path.join("target").join(dir_name).join("sysroot")); - let status = cmd.status().unwrap(); - assert!(status.success()); - - // localize symbols - let mut cmd = Command::new(objcopy); - for symbol in global_symbols { - cmd.arg("-G").arg(symbol); - } - cmd.arg(out_path.join(format!("lib{}.a", dir_name))); - let status = cmd.status().unwrap(); - assert!(status.success()); - - /* - // FIXME: it seems like this messes up relocations - // convert to ELF64 - let mut cmd = Command::new(objcopy); - cmd.arg("-I").arg("elf32-i386").arg("-O").arg("elf64-x86-64"); - cmd.arg(out_path.join(format!("lib{}.a", dir_name))); - let status = cmd.status().unwrap(); - assert!(status.success()); - */ - - // emit linker flags - println!("cargo:rustc-link-search=native={}", out_dir); - println!("cargo:rustc-link-lib=static={}", dir_name); -} \ No newline at end of file diff --git a/real_mode/first_stage/Cargo.lock b/real_mode/first_stage/Cargo.lock deleted file mode 100644 index 7b427282..00000000 --- a/real_mode/first_stage/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "first_stage" -version = "0.1.0" diff --git a/real_mode/first_stage/Cargo.toml b/real_mode/first_stage/Cargo.toml deleted file mode 100644 index 51115870..00000000 --- a/real_mode/first_stage/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "first_stage" -version = "0.1.0" -authors = ["Philipp Oppermann "] -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] - -[profile.release] -opt-level = "s" -lto = true -codegen-units = 1 - -# debug = true diff --git a/real_mode/first_stage/README.md b/real_mode/first_stage/README.md deleted file mode 100644 index 5c9cf4e6..00000000 --- a/real_mode/first_stage/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# First Stage: Bootsector - -This executable needs to fit into the 512-byte boot sector, so we need to use all kinds of tricks to keep the size down. - -## Build Commands - -1. `cargo build --release -Zbuild-std=core --target x86-16bit.json -Zbuild-std-features=compiler-builtins-mem` -2. `objcopy -I elf32-i386 -O binary target/x86-16bit/release/first_stage target/disk_image.bin - -To run in QEMU: - -- `qemu-system-x86_64 -drive format=raw,file=target/disk_image.bin` - -To print the contents of the ELF file, e.g. for trying to bring the size down: - -- `objdump -xsdS -M i8086,intel target/x86-16bit/release/first_stage` diff --git a/real_mode/first_stage/src/fat.rs b/real_mode/first_stage/src/fat.rs deleted file mode 100644 index 41fc0ca4..00000000 --- a/real_mode/first_stage/src/fat.rs +++ /dev/null @@ -1,161 +0,0 @@ -// based on https://github.com/rafalh/rust-fatfs/ - -use super::split_array_ref; - -pub(crate) struct BootSector { - bootjmp: [u8; 3], - oem_name: [u8; 8], - pub(crate) bpb: BiosParameterBlock, - boot_code: [u8; 448], - boot_sig: [u8; 2], -} - -impl BootSector { - pub(crate) fn deserialize(bytes: &[u8]) -> Self { - let mut boot = Self::default(); - let (&bootjmp, bytes) = split_array_ref(bytes); - let (&oem_name, bytes) = split_array_ref(bytes); - - boot.bootjmp = bootjmp; - boot.oem_name = oem_name; - boot.bpb = BiosParameterBlock::deserialize(bytes); - - let bytes = if boot.bpb.is_fat32() { - let (boot_code, bytes): (&[_; 420], _) = split_array_ref(bytes); - boot.boot_code[0..420].copy_from_slice(&boot_code[..]); - bytes - } else { - let (&boot_code, bytes) = split_array_ref(bytes); - boot.boot_code = boot_code; - bytes - }; - let (&boot_sig, bytes) = split_array_ref(bytes); - boot.boot_sig = boot_sig; - boot - } -} - -impl Default for BootSector { - fn default() -> Self { - Self { - bootjmp: Default::default(), - oem_name: Default::default(), - bpb: BiosParameterBlock::default(), - boot_code: [0; 448], - boot_sig: Default::default(), - } - } -} - -#[derive(Default, Debug, Clone)] -pub(crate) struct BiosParameterBlock { - pub(crate) bytes_per_sector: u16, - pub(crate) sectors_per_cluster: u8, - pub(crate) reserved_sectors: u16, - pub(crate) fats: u8, - pub(crate) root_entries: u16, - pub(crate) total_sectors_16: u16, - pub(crate) media: u8, - pub(crate) sectors_per_fat_16: u16, - pub(crate) sectors_per_track: u16, - pub(crate) heads: u16, - pub(crate) hidden_sectors: u32, - pub(crate) total_sectors_32: u32, - - // Extended BIOS Parameter Block - pub(crate) sectors_per_fat_32: u32, - pub(crate) extended_flags: u16, - pub(crate) fs_version: u16, - pub(crate) root_dir_first_cluster: u32, - pub(crate) fs_info_sector: u16, - pub(crate) backup_boot_sector: u16, - pub(crate) reserved_0: [u8; 12], - pub(crate) drive_num: u8, - pub(crate) reserved_1: u8, - pub(crate) ext_sig: u8, - pub(crate) volume_id: u32, - pub(crate) volume_label: [u8; 11], - pub(crate) fs_type_label: [u8; 8], -} - -impl BiosParameterBlock { - pub fn deserialize(bytes: &[u8]) -> Self { - let (&bytes_per_sector, bytes) = split_array_ref(bytes); - let (&[sectors_per_cluster], bytes) = split_array_ref(bytes); - let (&reserved_sectors, bytes) = split_array_ref(bytes); - let (&[fats], bytes) = split_array_ref(bytes); - let (&root_entries, bytes) = split_array_ref(bytes); - let (&total_sectors_16, bytes) = split_array_ref(bytes); - let (&[media], bytes) = split_array_ref(bytes); - let (§ors_per_fat_16, bytes) = split_array_ref(bytes); - let (§ors_per_track, bytes) = split_array_ref(bytes); - let (&heads, bytes) = split_array_ref(bytes); - let (&hidden_sectors, bytes) = split_array_ref(bytes); - let (&total_sectors_32, bytes) = split_array_ref(bytes); - - let mut bpb = Self { - bytes_per_sector: u16::from_le_bytes(bytes_per_sector), - sectors_per_cluster, - reserved_sectors: u16::from_le_bytes(reserved_sectors), - fats, - root_entries: u16::from_le_bytes(root_entries), - total_sectors_16: u16::from_le_bytes(total_sectors_16), - media, - sectors_per_fat_16: u16::from_le_bytes(sectors_per_fat_16), - sectors_per_track: u16::from_le_bytes(sectors_per_track), - heads: u16::from_le_bytes(heads), - hidden_sectors: u32::from_le_bytes(hidden_sectors), - total_sectors_32: u32::from_le_bytes(total_sectors_32), - ..Self::default() - }; - - let (§ors_per_fat_32, bytes) = split_array_ref(bytes); - let (&extended_flags, bytes) = split_array_ref(bytes); - let (&fs_version, bytes) = split_array_ref(bytes); - let (&root_dir_first_cluster, bytes) = split_array_ref(bytes); - let (&fs_info_sector, bytes) = split_array_ref(bytes); - let (&backup_boot_sector, bytes) = split_array_ref(bytes); - let (&reserved_0, bytes) = split_array_ref(bytes); - - if bpb.is_fat32() { - bpb.sectors_per_fat_32 = u32::from_le_bytes(sectors_per_fat_32); - bpb.extended_flags = u16::from_le_bytes(extended_flags); - bpb.fs_version = u16::from_le_bytes(fs_version); - bpb.root_dir_first_cluster = u32::from_le_bytes(root_dir_first_cluster); - bpb.fs_info_sector = u16::from_le_bytes(fs_info_sector); - bpb.backup_boot_sector = u16::from_le_bytes(backup_boot_sector); - bpb.reserved_0 = reserved_0; - } - - let (&[drive_num], bytes) = split_array_ref(bytes); - let (&[reserved_1], bytes) = split_array_ref(bytes); - let (&[ext_sig], bytes) = split_array_ref(bytes); - let (&volume_id, bytes) = split_array_ref(bytes); - let (&volume_label, bytes) = split_array_ref(bytes); - let (&fs_type_label, bytes) = split_array_ref(bytes); - - bpb.drive_num = drive_num; - bpb.reserved_1 = reserved_1; - bpb.ext_sig = ext_sig; // 0x29 - bpb.volume_id = u32::from_le_bytes(volume_id); - bpb.volume_label = volume_label; - bpb.fs_type_label = fs_type_label; - - // when the extended boot signature is anything other than 0x29, the fields are invalid - if bpb.ext_sig != 0x29 { - // fields after ext_sig are not used - clean them - bpb.volume_id = 0; - bpb.volume_label = [0; 11]; - bpb.fs_type_label = [0; 8]; - } - - bpb - } - - pub(crate) fn is_fat32(&self) -> bool { - // because this field must be zero on FAT32, and - // because it must be non-zero on FAT12/FAT16, - // this provides a simple way to detect FAT32 - self.sectors_per_fat_16 == 0 - } -} diff --git a/real_mode/first_stage/src/main.rs b/real_mode/first_stage/src/main.rs deleted file mode 100644 index fdb44033..00000000 --- a/real_mode/first_stage/src/main.rs +++ /dev/null @@ -1,101 +0,0 @@ -#![no_std] -#![no_main] - -use core::{ - arch::{asm, global_asm}, - slice, -}; -use fail::{fail, print_char, UnwrapOrFail}; -use mbr::MasterBootRecord; - -global_asm!(include_str!("boot.s")); - -mod dap; -mod fail; -mod fat; -mod mbr; - -extern "C" { - static _mbr_start: u8; -} - -fn mbr_start() -> *const u8 { - unsafe { &_mbr_start } -} - -#[no_mangle] -pub extern "C" fn first_stage(disk_number: u16) { - print_char(b'1'); - let bytes = &unsafe { slice::from_raw_parts(mbr_start(), 512) }; - let partition = mbr::get_partition(bytes, 0); - - print_char(b'2'); - let partition_buf = u16::try_from(mbr_start() as usize).unwrap_or_fail(b'a') + 512; - - // load first partition into buffer - // TODO: only load headers - let dap = dap::DiskAddressPacket::from_lba( - partition_buf, - partition.logical_block_address.into(), - partition.sector_count.try_into().unwrap_or_fail(b'b'), - ); - unsafe { - dap.perform_load(disk_number); - } - if partition.sector_count == 0 { - fail(b'c'); - } - - print_char(b'3'); - - // try to parse FAT file system - let fat_slice = unsafe { - slice::from_raw_parts( - partition_buf as *const u8, - usize::try_from(partition.sector_count).unwrap_or_else(|_| fail(b'a')) * 512, - ) - }; - - print_char(b'4'); - let boot_sector = fat::BootSector::deserialize(fat_slice); - - // TODO: get root dir - - // TODO: get offset of `second_stage` file - - // TODO: get offset of `kernel-x86_64` file - - // TODO: load `second_stage` file into memory - - // TODO: jump to `second_stage`, pass offset of `kernel-x86_64` and disk number as arguments - - loop {} -} - -fn load_second_stage( - second_stage_start: u32, - second_stage_end: u32, - bootloader_start: u32, - disk_number: u16, -) { - use dap::DiskAddressPacket; - - let file_offset = (second_stage_start - bootloader_start) as u64; - let size = (second_stage_end - second_stage_start) as u32; - - let dap = DiskAddressPacket::new(second_stage_start as u16, file_offset, size); - unsafe { dap.perform_load(disk_number) } -} - -/// Taken from https://github.com/rust-lang/rust/blob/e100ec5bc7cd768ec17d75448b29c9ab4a39272b/library/core/src/slice/mod.rs#L1673-L1677 -/// -/// TODO replace with `split_array` feature in stdlib as soon as it's stabilized, -/// see https://github.com/rust-lang/rust/issues/90091 -fn split_array_ref(slice: &[T]) -> (&[T; N], &[T]) { - if N > slice.len() { - fail(b'S'); - } - let (a, b) = slice.split_at(N); - // SAFETY: a points to [T; N]? Yes it's [T] of length N (checked by split_at) - unsafe { (&*(a.as_ptr() as *const [T; N]), b) } -} diff --git a/real_mode/first_stage/src/mbr.rs b/real_mode/first_stage/src/mbr.rs deleted file mode 100644 index baa3d7fe..00000000 --- a/real_mode/first_stage/src/mbr.rs +++ /dev/null @@ -1,158 +0,0 @@ -// Based on https://docs.rs/mbr-nostd - -use super::fail::{fail, UnwrapOrFail}; - -pub fn get_partition(buffer: &[u8], index: usize) -> PartitionTableEntry { - if buffer.len() < BUFFER_SIZE { - fail(b'a'); - } else if buffer.get(BUFFER_SIZE - SUFFIX_BYTES.len()..BUFFER_SIZE) != Some(&SUFFIX_BYTES[..]) { - fail(b'b'); - } - - let offset = TABLE_OFFSET + index * ENTRY_SIZE; - let buffer = buffer.get(offset..).unwrap_or_fail(b'c'); - - let partition_type = *buffer.get(4).unwrap_or_fail(b'd'); - - let lba = u32::from_le_bytes( - buffer - .get(8..) - .and_then(|s| s.get(..4)) - .and_then(|s| s.try_into().ok()) - .unwrap_or_fail(b'e'), - ); - let len = u32::from_le_bytes( - buffer - .get(12..) - .and_then(|s| s.get(..4)) - .and_then(|s| s.try_into().ok()) - .unwrap_or_fail(b'f'), - ); - PartitionTableEntry::new(partition_type, lba, len) -} - -/// A struct representing an MBR partition table. -pub struct MasterBootRecord { - entries: [PartitionTableEntry; MAX_ENTRIES], -} - -const BUFFER_SIZE: usize = 512; -const TABLE_OFFSET: usize = 446; -const ENTRY_SIZE: usize = 16; -const SUFFIX_BYTES: [u8; 2] = [0x55, 0xaa]; -const MAX_ENTRIES: usize = (BUFFER_SIZE - TABLE_OFFSET - 2) / ENTRY_SIZE; - -impl MasterBootRecord { - /// Parses the MBR table from a raw byte buffer. - - pub fn from_bytes(buffer: &[u8]) -> MasterBootRecord { - if buffer.len() < BUFFER_SIZE { - fail(b'1'); - } else if buffer.get(BUFFER_SIZE - SUFFIX_BYTES.len()..BUFFER_SIZE) - != Some(&SUFFIX_BYTES[..]) - { - fail(b'2'); - } - let mut entries = [PartitionTableEntry::empty(); MAX_ENTRIES]; - - for idx in 0..MAX_ENTRIES { - let offset = TABLE_OFFSET + idx * ENTRY_SIZE; - let buffer = buffer.get(offset..).unwrap_or_fail(b'8'); - - let partition_type = *buffer.get(4).unwrap_or_fail(b'4'); - - let lba = u32::from_le_bytes( - buffer - .get(8..) - .and_then(|s| s.get(..4)) - .and_then(|s| s.try_into().ok()) - .unwrap_or_fail(b'5'), - ); - let len = u32::from_le_bytes( - buffer - .get(12..) - .and_then(|s| s.get(..4)) - .and_then(|s| s.try_into().ok()) - .unwrap_or_fail(b'6'), - ); - *entries.get_mut(idx).unwrap_or_fail(b'7') = - PartitionTableEntry::new(partition_type, lba, len); - } - - MasterBootRecord { entries } - } - - pub fn partition_table_entries(&self) -> &[PartitionTableEntry] { - &self.entries[..] - } -} - -/// The type of a particular partition. -#[derive(Copy, Clone, Eq, PartialEq)] -pub enum PartitionType { - Unused, - Unknown(u8), - Fat12(u8), - Fat16(u8), - Fat32(u8), - LinuxExt(u8), - HfsPlus(u8), - ISO9660(u8), - NtfsExfat(u8), -} - -impl PartitionType { - /// Parses a partition type from the type byte in the MBR's table. - pub fn from_mbr_tag_byte(tag: u8) -> PartitionType { - match tag { - 0x0 => PartitionType::Unused, - 0x01 => PartitionType::Fat12(tag), - 0x04 | 0x06 | 0x0e => PartitionType::Fat16(tag), - 0x0b | 0x0c | 0x1b | 0x1c => PartitionType::Fat32(tag), - 0x83 => PartitionType::LinuxExt(tag), - 0x07 => PartitionType::NtfsExfat(tag), - 0xaf => PartitionType::HfsPlus(tag), - _ => PartitionType::Unknown(tag), - } - } - - pub fn known_type(tag: u8) -> bool { - match tag { - 0x0 | 0x01 | 0x04 | 0x06 | 0x0e | 0x0b | 0x0c | 0x1b | 0x1c | 0x83 | 0x07 | 0xaf => { - true - } - _ => false, - } - } -} - -/// An entry in a partition table. -#[derive(Copy, Clone, Eq, PartialEq)] -pub struct PartitionTableEntry { - /// The type of partition in this entry. - pub partition_type: u8, - - /// The index of the first block of this entry. - pub logical_block_address: u32, - - /// The total number of blocks in this entry. - pub sector_count: u32, -} - -impl PartitionTableEntry { - pub fn new( - partition_type: u8, - logical_block_address: u32, - sector_count: u32, - ) -> PartitionTableEntry { - PartitionTableEntry { - partition_type, - logical_block_address, - sector_count, - } - } - - pub fn empty() -> PartitionTableEntry { - PartitionTableEntry::new(0, 0, 0) - } -} diff --git a/real_mode/linker.ld b/real_mode/linker.ld deleted file mode 100644 index 64f42333..00000000 --- a/real_mode/linker.ld +++ /dev/null @@ -1,68 +0,0 @@ -ENTRY(_start) - -SECTIONS { - . = 0x500; - _stack_start = .; - . = 0x7c00; - _stack_end = .; - - _bootloader_start = .; - .boot : - { - *first_stage*(.boot) - } - .first_stage_text : - { - *first_stage*(.text .text.*) - } - .first_stage_data : - { - *first_stage*(.rodata .rodata.*) - *first_stage*(.data .data.*) - *first_stage*(.got .got.*) - } - - . = 0x7c00 + 510; - .magic_number : - { - SHORT(0xaa55) /* magic number for bootable disk */ - } - - _second_stage_start = .; - - .real_mode_text : - { - *real_mode*(.text .text.*) - } - .real_mode_data : - { - *real_mode*(.rodata .rodata.*) - *real_mode*(.data .data.*) - *real_mode*(.got .got.*) - } - - .text : - { - *(.text .text.*) - } - - .data : - { - *(.rodata .rodata.*) - *(.data .data.*) - *(.got .got.*) - } - - .eh_frame : - { - _EH_FRAME = .; - *(.eh_frame) - } - - .asm : - { - *(.second_stage) - } - . = ALIGN(512); - _second_stage_end = .; -} diff --git a/real_mode/real_mode/Cargo.lock b/real_mode/real_mode/Cargo.lock deleted file mode 100644 index f7dc4c93..00000000 --- a/real_mode/real_mode/Cargo.lock +++ /dev/null @@ -1,6 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "real_mode" -version = "0.1.0" - diff --git a/real_mode/real_mode/Cargo.toml b/real_mode/real_mode/Cargo.toml deleted file mode 100644 index f798ceb8..00000000 --- a/real_mode/real_mode/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "real_mode" -version = "0.1.0" -authors = ["Philipp Oppermann "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -name = "real_mode" -crate-type = ["staticlib"] - -[dependencies] - - -[profile.release] -opt-level = "s" -lto = true -codegen-units = 1 -debug = true diff --git a/real_mode/real_mode/src/lib.rs b/real_mode/real_mode/src/lib.rs deleted file mode 100644 index ec78a355..00000000 --- a/real_mode/real_mode/src/lib.rs +++ /dev/null @@ -1,45 +0,0 @@ -#![feature(asm, global_asm)] -#![no_std] - -global_asm!(include_str!("second_stage.s")); - -extern "C" { - fn print_char(c: u8); - fn second_stage_asm() -> u32; -} - -#[no_mangle] -pub extern "C" fn second_stage(_disk_number: u16) { - let val = unsafe { second_stage_asm() }; - if val == 12345 { - println(b"match"); - } else { - println(b"no match"); - } -} - -#[panic_handler] -pub fn panic(_info: &core::panic::PanicInfo) -> ! { - println(b"PANIC!"); - loop { - hlt() - } -} - -fn hlt() { - unsafe { - asm!("hlt":::: "intel","volatile"); - } -} - -#[inline(never)] -fn println(s: &[u8]) { - print(s); - unsafe { print_char(b'\n') }; -} - -fn print(s: &[u8]) { - for &c in s { - unsafe { print_char(c) }; - } -} diff --git a/real_mode/real_mode/src/second_stage.s b/real_mode/real_mode/src/second_stage.s deleted file mode 100644 index 200b3b08..00000000 --- a/real_mode/real_mode/src/second_stage.s +++ /dev/null @@ -1,8 +0,0 @@ -.section .second_stage, "awx" -.global second_stage -.intel_syntax noprefix -.code16 - -second_stage_asm: - mov eax, 12345 - ret diff --git a/real_mode/src/main.rs b/real_mode/src/main.rs deleted file mode 100644 index 39944883..00000000 --- a/real_mode/src/main.rs +++ /dev/null @@ -1,12 +0,0 @@ -#![no_std] -#![no_main] - -#[no_mangle] -pub extern "C" fn rust_main() -> u32 { - 54321 -} - -#[panic_handler] -pub fn panic(_info: &core::panic::PanicInfo) -> ! { - loop{} -} diff --git a/real_mode/x86-32bit.json b/real_mode/x86-32bit.json deleted file mode 100644 index 7a2d0eaa..00000000 --- a/real_mode/x86-32bit.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "arch": "x86", - "cpu": "i386", - "data-layout": "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128", - "dynamic-linking": false, - "executables": true, - "linker-flavor": "ld.lld", - "linker": "rust-lld", - "pre-link-args": { - "ld.lld": [ - "--script=linker.ld" - ] - }, - "llvm-target": "i386-unknown-none", - "max-atomic-width": 64, - "position-independent-executables": false, - "disable-redzone": true, - "target-c-int-width": "32", - "target-pointer-width": "32", - "target-endian": "little", - "panic-strategy": "abort", - "os": "none", - "vendor": "unknown", - "relocation_model": "static", - "eliminate_frame_pointer": true -} \ No newline at end of file diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index 07ade694..00000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -nightly \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..d04213d1 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly" +components = ["rustfmt", "clippy", "rust-src", "llvm-tools-preview"] +targets = ["x86_64-unknown-none"] diff --git a/src/bios/mod.rs b/src/bios/mod.rs new file mode 100644 index 00000000..6000adcc --- /dev/null +++ b/src/bios/mod.rs @@ -0,0 +1,36 @@ +use std::path::Path; + +use bootloader_boot_config::BootConfig; + +use crate::DiskImageBuilder; + +/// Create disk images for booting on legacy BIOS systems. +pub struct BiosBoot { + image_builder: DiskImageBuilder, +} + +impl BiosBoot { + /// Start creating a disk image for the given bootloader ELF executable. + pub fn new(kernel_path: &Path) -> Self { + Self { + image_builder: DiskImageBuilder::new(kernel_path.to_owned()), + } + } + + /// Add a ramdisk file to the image. + pub fn set_ramdisk(&mut self, ramdisk_path: &Path) -> &mut Self { + self.image_builder.set_ramdisk(ramdisk_path.to_owned()); + self + } + + /// Creates a configuration file (boot.json) that configures the runtime behavior of the bootloader. + pub fn set_boot_config(&mut self, config: &BootConfig) -> &mut Self { + self.image_builder.set_boot_config(config); + self + } + + /// Create a bootable BIOS disk image at the given path. + pub fn create_disk_image(&self, out_path: &Path) -> anyhow::Result<()> { + self.image_builder.create_bios_image(out_path) + } +} diff --git a/src/boot_info.rs b/src/boot_info.rs deleted file mode 100644 index f50cf2a5..00000000 --- a/src/boot_info.rs +++ /dev/null @@ -1,31 +0,0 @@ -use core::slice; - -use bootloader::bootinfo::{E820MemoryRegion, MemoryMap, MemoryRegion, MemoryRegionType}; -use usize_conversions::usize_from; -use x86_64::VirtAddr; - -pub(crate) fn create_from(memory_map_addr: VirtAddr, entry_count: u64) -> MemoryMap { - let memory_map_start_ptr = usize_from(memory_map_addr.as_u64()) as *const E820MemoryRegion; - let e820_memory_map = - unsafe { slice::from_raw_parts(memory_map_start_ptr, usize_from(entry_count)) }; - - let mut memory_map = MemoryMap::new(); - for region in e820_memory_map { - memory_map.add_region(MemoryRegion::from(*region)); - } - - memory_map.sort(); - - let mut iter = memory_map.iter_mut().peekable(); - while let Some(region) = iter.next() { - if let Some(next) = iter.peek() { - if region.range.end_frame_number > next.range.start_frame_number - && region.region_type == MemoryRegionType::Usable - { - region.range.end_frame_number = next.range.start_frame_number; - } - } - } - - memory_map -} diff --git a/src/bootinfo/memory_map.rs b/src/bootinfo/memory_map.rs deleted file mode 100644 index c719bd90..00000000 --- a/src/bootinfo/memory_map.rs +++ /dev/null @@ -1,235 +0,0 @@ -use core::fmt; -use core::ops::{Deref, DerefMut}; - -const PAGE_SIZE: u64 = 4096; - -const MAX_MEMORY_MAP_SIZE: usize = 64; - -/// A map of the physical memory regions of the underlying machine. -#[repr(C)] -pub struct MemoryMap { - entries: [MemoryRegion; MAX_MEMORY_MAP_SIZE], - // u64 instead of usize so that the structure layout is platform - // independent - next_entry_index: u64, -} - -#[doc(hidden)] -impl MemoryMap { - pub fn new() -> Self { - MemoryMap { - entries: [MemoryRegion::empty(); MAX_MEMORY_MAP_SIZE], - next_entry_index: 0, - } - } - - pub fn add_region(&mut self, region: MemoryRegion) { - assert!( - self.next_entry_index() < MAX_MEMORY_MAP_SIZE, - "too many memory regions in memory map" - ); - self.entries[self.next_entry_index()] = region; - self.next_entry_index += 1; - self.sort(); - } - - pub fn sort(&mut self) { - use core::cmp::Ordering; - - self.entries.sort_unstable_by(|r1, r2| { - if r1.range.is_empty() { - Ordering::Greater - } else if r2.range.is_empty() { - Ordering::Less - } else { - let ordering = r1 - .range - .start_frame_number - .cmp(&r2.range.start_frame_number); - - if ordering == Ordering::Equal { - r1.range.end_frame_number.cmp(&r2.range.end_frame_number) - } else { - ordering - } - } - }); - if let Some(first_zero_index) = self.entries.iter().position(|r| r.range.is_empty()) { - self.next_entry_index = first_zero_index as u64; - } - } - - fn next_entry_index(&self) -> usize { - self.next_entry_index as usize - } -} - -impl Deref for MemoryMap { - type Target = [MemoryRegion]; - - fn deref(&self) -> &Self::Target { - &self.entries[0..self.next_entry_index()] - } -} - -impl DerefMut for MemoryMap { - fn deref_mut(&mut self) -> &mut Self::Target { - let next_index = self.next_entry_index(); - &mut self.entries[0..next_index] - } -} - -impl fmt::Debug for MemoryMap { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_list().entries(self.iter()).finish() - } -} - -/// Represents a region of physical memory. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub struct MemoryRegion { - /// The range of frames that belong to the region. - pub range: FrameRange, - /// The type of the region. - pub region_type: MemoryRegionType, -} - -#[doc(hidden)] -impl MemoryRegion { - pub fn empty() -> Self { - MemoryRegion { - range: FrameRange { - start_frame_number: 0, - end_frame_number: 0, - }, - region_type: MemoryRegionType::Empty, - } - } -} - -/// A range of frames with an exclusive upper bound. -#[derive(Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub struct FrameRange { - /// The frame _number_ of the first 4KiB frame in the region. - /// - /// This convert this frame number to a physical address, multiply it with the - /// page size (4KiB). - pub start_frame_number: u64, - /// The frame _number_ of the first 4KiB frame that does no longer belong to the region. - /// - /// This convert this frame number to a physical address, multiply it with the - /// page size (4KiB). - pub end_frame_number: u64, -} - -impl FrameRange { - /// Create a new FrameRange from the passed start_addr and end_addr. - /// - /// The end_addr is exclusive. - pub fn new(start_addr: u64, end_addr: u64) -> Self { - let last_byte = end_addr - 1; - FrameRange { - start_frame_number: start_addr / PAGE_SIZE, - end_frame_number: (last_byte / PAGE_SIZE) + 1, - } - } - - /// Returns true if the frame range contains no frames. - pub fn is_empty(&self) -> bool { - self.start_frame_number == self.end_frame_number - } - - /// Returns the physical start address of the memory region. - pub fn start_addr(&self) -> u64 { - self.start_frame_number * PAGE_SIZE - } - - /// Returns the physical end address of the memory region. - pub fn end_addr(&self) -> u64 { - self.end_frame_number * PAGE_SIZE - } -} - -impl fmt::Debug for FrameRange { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "FrameRange({:#x}..{:#x})", - self.start_addr(), - self.end_addr() - ) - } -} - -/// Represents possible types for memory regions. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub enum MemoryRegionType { - /// Unused memory, can be freely used by the kernel. - Usable, - /// Memory that is already in use. - InUse, - /// Memory reserved by the hardware. Not usable. - Reserved, - /// ACPI reclaimable memory - AcpiReclaimable, - /// ACPI NVS memory - AcpiNvs, - /// Area containing bad memory - BadMemory, - /// Memory used for loading the kernel. - Kernel, - /// Memory used for the kernel stack. - KernelStack, - /// Memory used for creating page tables. - PageTable, - /// Memory used by the bootloader. - Bootloader, - /// Frame at address zero. - /// - /// (shouldn't be used because it's easy to make mistakes related to null pointers) - FrameZero, - /// An empty region with size 0 - Empty, - /// Memory used for storing the boot information. - BootInfo, - /// Memory used for storing the supplied package - Package, - /// Additional variant to ensure that we can add more variants in the future without - /// breaking backwards compatibility. - #[doc(hidden)] - NonExhaustive, -} - -#[doc(hidden)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(C)] -pub struct E820MemoryRegion { - pub start_addr: u64, - pub len: u64, - pub region_type: u32, - pub acpi_extended_attributes: u32, -} - -impl From for MemoryRegion { - fn from(region: E820MemoryRegion) -> MemoryRegion { - let region_type = match region.region_type { - 1 => MemoryRegionType::Usable, - 2 => MemoryRegionType::Reserved, - 3 => MemoryRegionType::AcpiReclaimable, - 4 => MemoryRegionType::AcpiNvs, - 5 => MemoryRegionType::BadMemory, - t => panic!("invalid region type {}", t), - }; - MemoryRegion { - range: FrameRange::new(region.start_addr, region.start_addr + region.len), - region_type, - } - } -} - -extern "C" { - fn _improper_ctypes_check(_boot_info: MemoryMap); -} diff --git a/src/bootinfo/mod.rs b/src/bootinfo/mod.rs deleted file mode 100644 index a936bae1..00000000 --- a/src/bootinfo/mod.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Provides boot information to the kernel. - -#![deny(improper_ctypes)] - -pub use self::memory_map::*; - -mod memory_map; - -/// This structure represents the information that the bootloader passes to the kernel. -/// -/// The information is passed as an argument to the entry point: -/// -/// ```ignore -/// pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { -/// // […] -/// } -/// ``` -/// -/// Note that no type checking occurs for the entry point function, so be careful to -/// use the correct argument types. To ensure that the entry point function has the correct -/// signature, use the [`entry_point`] macro. -#[derive(Debug)] -#[repr(C)] -pub struct BootInfo { - /// A map of the physical memory regions of the underlying machine. - /// - /// The bootloader queries this information from the BIOS/UEFI firmware and translates this - /// information to Rust types. It also marks any memory regions that the bootloader uses in - /// the memory map before passing it to the kernel. Regions marked as usable can be freely - /// used by the kernel. - pub memory_map: MemoryMap, - /// The virtual address of the recursively mapped level 4 page table. - #[cfg(feature = "recursive_page_table")] - pub recursive_page_table_addr: u64, - /// The offset into the virtual address space where the physical memory is mapped. - /// - /// Physical addresses can be converted to virtual addresses by adding this offset to them. - /// - /// The mapping of the physical memory allows to access arbitrary physical frames. Accessing - /// frames that are also mapped at other virtual addresses can easily break memory safety and - /// cause undefined behavior. Only frames reported as `USABLE` by the memory map in the `BootInfo` - /// can be safely accessed. - #[cfg(feature = "map_physical_memory")] - pub physical_memory_offset: u64, - _non_exhaustive: u8, // `()` is not FFI safe -} - -impl BootInfo { - /// Create a new boot information structure. This function is only for internal purposes. - #[allow(unused_variables)] - #[doc(hidden)] - pub fn new( - memory_map: MemoryMap, - recursive_page_table_addr: u64, - physical_memory_offset: u64, - ) -> Self { - BootInfo { - memory_map, - #[cfg(feature = "recursive_page_table")] - recursive_page_table_addr, - #[cfg(feature = "map_physical_memory")] - physical_memory_offset, - _non_exhaustive: 0, - } - } -} - -extern "C" { - fn _improper_ctypes_check(_boot_info: BootInfo); -} diff --git a/src/disk_image.rs b/src/disk_image.rs new file mode 100644 index 00000000..0a2d6823 --- /dev/null +++ b/src/disk_image.rs @@ -0,0 +1,183 @@ +use anyhow::Context; +use std::{ + fs, + io::{self, Seek, Write}, + path::Path, + process::Command, +}; +use thiserror::Error; + +/// Creates a bootable disk image from the given bootloader executable. +pub fn create_disk_image( + bootloader_elf_path: &Path, + output_bin_path: &Path, + kernel_binary: &Path, +) -> anyhow::Result<()> { + let llvm_tools = + llvm_tools::LlvmTools::new().map_err(|err| anyhow::anyhow!("failed to get llvm tools"))?; + let objcopy = llvm_tools + .tool(&llvm_tools::exe("llvm-objcopy")) + .ok_or(DiskImageError::LlvmObjcopyNotFound)?; + + // convert first stage to binary + let mut cmd = Command::new(objcopy); + cmd.arg("-I").arg("elf64-x86-64"); + cmd.arg("-O").arg("binary"); + cmd.arg("--binary-architecture=i386:x86-64"); + cmd.arg(bootloader_elf_path); + cmd.arg(output_bin_path); + let output = cmd.output().map_err(|err| DiskImageError::Io { + message: "failed to execute llvm-objcopy command", + error: err, + })?; + if !output.status.success() { + return Err(DiskImageError::ObjcopyFailed { + stderr: output.stderr, + }) + .context("objcopy failed"); + } + + use std::fs::OpenOptions; + let mut disk_image = OpenOptions::new() + .write(true) + .open(&output_bin_path) + .map_err(|err| DiskImageError::Io { + message: "failed to open boot image", + error: err, + })?; + let file_size = disk_image + .metadata() + .map_err(|err| DiskImageError::Io { + message: "failed to get size of boot image", + error: err, + })? + .len(); + const BLOCK_SIZE: u64 = 512; + assert_eq!(file_size, BLOCK_SIZE); + + let kernel_size = fs::metadata(&kernel_binary) + .context("failed to read metadata of kernel binary")? + .len(); + + // create fat partition + const MB: u64 = 1024 * 1024; + let fat_size = kernel_size; // TODO plus second stage size + let fat_size_padded_and_rounded = ((fat_size + 1024 * 64 - 1) / MB + 1) * MB; + let fat_file_path = { + let fat_path = output_bin_path.with_extension("fat"); + let fat_file = fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(&fat_path) + .context("Failed to create UEFI FAT file")?; + fat_file + .set_len(fat_size_padded_and_rounded) + .context("failed to set UEFI FAT file length")?; + + // create new FAT partition + let format_options = fatfs::FormatVolumeOptions::new().volume_label(*b"BOOT "); + fatfs::format_volume(&fat_file, format_options) + .context("Failed to format UEFI FAT file")?; + + // copy kernel to FAT filesystem + let partition = fatfs::FileSystem::new(&fat_file, fatfs::FsOptions::new()) + .context("Failed to open FAT file system of UEFI FAT file")?; + let root_dir = partition.root_dir(); + let mut kernel_file = root_dir.create_file("kernel-x86_64")?; + kernel_file.truncate()?; + io::copy(&mut fs::File::open(&kernel_binary)?, &mut kernel_file)?; + + fat_path + }; + + disk_image.seek(io::SeekFrom::Start(446))?; + disk_image.write_all(&[0x80, 0, 0, 0, 0x04, 0, 0, 0])?; + let start_sector = 1u32.to_le_bytes(); + let size_sectors = u32::try_from(&fat_size_padded_and_rounded / 512) + .unwrap() + .to_le_bytes(); + disk_image.write_all(&start_sector)?; + disk_image.write_all(&size_sectors)?; + + disk_image.seek(io::SeekFrom::Start(512))?; + io::copy(&mut fs::File::open(&kernel_binary)?, &mut disk_image)?; + + pad_to_nearest_block_size(output_bin_path)?; + Ok(()) +} + +fn pad_to_nearest_block_size(output_bin_path: &Path) -> Result<(), DiskImageError> { + const BLOCK_SIZE: u64 = 512; + use std::fs::OpenOptions; + let file = OpenOptions::new() + .write(true) + .open(&output_bin_path) + .map_err(|err| DiskImageError::Io { + message: "failed to open boot image", + error: err, + })?; + let file_size = file + .metadata() + .map_err(|err| DiskImageError::Io { + message: "failed to get size of boot image", + error: err, + })? + .len(); + let remainder = file_size % BLOCK_SIZE; + let padding = if remainder > 0 { + BLOCK_SIZE - remainder + } else { + 0 + }; + file.set_len(file_size + padding) + .map_err(|err| DiskImageError::Io { + message: "failed to pad boot image to a multiple of the block size", + error: err, + }) +} + +/// Creating the disk image failed. +#[derive(Debug, Error)] +pub enum DiskImageError { + /// The `llvm-tools-preview` rustup component was not found + #[error( + "Could not find the `llvm-tools-preview` rustup component.\n\n\ + You can install by executing `rustup component add llvm-tools-preview`." + )] + LlvmToolsNotFound, + + /// There was another problem locating the `llvm-tools-preview` rustup component + #[error("Failed to locate the `llvm-tools-preview` rustup component: {0:?}")] + LlvmTools(llvm_tools::Error), + + /// The llvm-tools component did not contain the required `llvm-objcopy` executable + #[error("Could not find `llvm-objcopy` in the `llvm-tools-preview` rustup component.")] + LlvmObjcopyNotFound, + + /// The `llvm-objcopy` command failed + #[error("Failed to run `llvm-objcopy`: {}", String::from_utf8_lossy(.stderr))] + ObjcopyFailed { + /// The output of `llvm-objcopy` to standard error + stderr: Vec, + }, + + /// An unexpected I/O error occurred + #[error("I/O error: {message}:\n{error}")] + Io { + /// Description of the failed I/O operation + message: &'static str, + /// The I/O error that occurred + error: io::Error, + }, +} + +impl From for DiskImageError { + fn from(err: llvm_tools::Error) -> Self { + match err { + llvm_tools::Error::NotFound => DiskImageError::LlvmToolsNotFound, + other => DiskImageError::LlvmTools(other), + } + } +} diff --git a/src/e820.s b/src/e820.s deleted file mode 100644 index 114de231..00000000 --- a/src/e820.s +++ /dev/null @@ -1,55 +0,0 @@ -.section .boot, "awx" -.intel_syntax noprefix -.code16 - -# From http://wiki.osdev.org/Detecting_Memory_(x86)#Getting_an_E820_Memory_Map - -# use the INT 0x15, eax= 0xE820 BIOS function to get a memory map -# inputs: es:di -> destination buffer for 24 byte entries -# outputs: bp = entry count, trashes all registers except esi -do_e820: - xor ebx, ebx # ebx must be 0 to start - xor bp, bp # keep an entry count in bp - mov edx, 0x0534D4150 # Place "SMAP" into edx - mov eax, 0xe820 - mov dword ptr es:[di + 20], 1 # force a valid ACPI 3.X entry - mov ecx, 24 # ask for 24 bytes - int 0x15 - jc .failed # carry set on first call means "unsupported function" - mov edx, 0x0534D4150 # Some BIOSes apparently trash this register? - cmp eax, edx # on success, eax must have been reset to "SMAP" - jne .failed - test ebx, ebx # ebx = 0 implies list is only 1 entry long (worthless) - je .failed - jmp .jmpin -.e820lp: - mov eax, 0xe820 # eax, ecx get trashed on every int 0x15 call - mov dword ptr es:[di + 20], 1 # force a valid ACPI 3.X entry - mov ecx, 24 # ask for 24 bytes again - int 0x15 - jc .e820f # carry set means "end of list already reached" - mov edx, 0x0534D4150 # repair potentially trashed register -.jmpin: - jcxz .skipent # skip any 0 length entries - cmp cl, 20 # got a 24 byte ACPI 3.X response? - jbe .notext - test byte ptr es:[di + 20], 1 # if so: is the "ignore this data" bit clear? - je .skipent -.notext: - mov ecx, es:[di + 8] # get lower uint32_t of memory region length - or ecx, es:[di + 12] # "or" it with upper uint32_t to test for zero - jz .skipent # if length uint64_t is 0, skip entry - inc bp # got a good entry: ++count, move to next storage spot - add di, 24 -.skipent: - test ebx, ebx # if ebx resets to 0, list is complete - jne .e820lp -.e820f: - mov [mmap_ent], bp # store the entry count - clc # there is "jc" on end of list to this point, so the carry must be cleared - ret -.failed: - stc # "function unsupported" error exit - ret - -mmap_ent: .word 0 diff --git a/src/fat.rs b/src/fat.rs new file mode 100644 index 00000000..c81033b5 --- /dev/null +++ b/src/fat.rs @@ -0,0 +1,93 @@ +use crate::file_data_source::FileDataSource; +use anyhow::Context; +use fatfs::Dir; +use std::fs::File; +use std::{collections::BTreeMap, fs, path::Path}; + +use crate::KERNEL_FILE_NAME; + +pub fn create_fat_filesystem( + files: BTreeMap<&str, &FileDataSource>, + out_fat_path: &Path, +) -> anyhow::Result<()> { + const MB: u64 = 1024 * 1024; + + // calculate needed size + let mut needed_size = 0; + for source in files.values() { + needed_size += source.len()?; + } + + // create new filesystem image file at the given path and set its length + let fat_file = fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(out_fat_path) + .unwrap(); + let fat_size_padded_and_rounded = ((needed_size + 1024 * 64 - 1) / MB + 1) * MB + MB; + fat_file.set_len(fat_size_padded_and_rounded).unwrap(); + + // choose a file system label + let mut label = *b"MY_RUST_OS!"; + + // This __should__ always be a file, but maybe not. Should we allow the caller to set the volume label instead? + if let Some(FileDataSource::File(path)) = files.get(KERNEL_FILE_NAME) { + if let Some(name) = path.file_stem() { + let converted = name.to_string_lossy(); + let name = converted.as_bytes(); + let mut new_label = [0u8; 11]; + let name = &name[..usize::min(new_label.len(), name.len())]; + let slice = &mut new_label[..name.len()]; + slice.copy_from_slice(name); + label = new_label; + } + } + + // format the file system and open it + let format_options = fatfs::FormatVolumeOptions::new().volume_label(label); + fatfs::format_volume(&fat_file, format_options).context("Failed to format FAT file")?; + let filesystem = fatfs::FileSystem::new(&fat_file, fatfs::FsOptions::new()) + .context("Failed to open FAT file system of UEFI FAT file")?; + let root_dir = filesystem.root_dir(); + + // copy files to file system + add_files_to_image(&root_dir, files) +} + +pub fn add_files_to_image( + root_dir: &Dir<&File>, + files: BTreeMap<&str, &FileDataSource>, +) -> anyhow::Result<()> { + for (target_path_raw, source) in files { + let target_path = Path::new(target_path_raw); + // create parent directories + let ancestors: Vec<_> = target_path.ancestors().skip(1).collect(); + for ancestor in ancestors.into_iter().rev().skip(1) { + root_dir + .create_dir(&ancestor.display().to_string()) + .with_context(|| { + format!( + "failed to create directory `{}` on FAT filesystem", + ancestor.display() + ) + })?; + } + + let mut new_file = root_dir + .create_file(target_path_raw) + .with_context(|| format!("failed to create file at `{}`", target_path.display()))?; + new_file.truncate().unwrap(); + + source.copy_to(&mut new_file).with_context(|| { + format!( + "failed to copy source data `{:?}` to file at `{}`", + source, + target_path.display() + ) + })?; + } + + Ok(()) +} diff --git a/src/file_data_source.rs b/src/file_data_source.rs new file mode 100644 index 00000000..47656ff8 --- /dev/null +++ b/src/file_data_source.rs @@ -0,0 +1,66 @@ +use anyhow::Context; +use core::fmt::{Debug, Formatter}; + +use std::io::Cursor; +use std::path::PathBuf; +use std::{fs, io}; + +#[derive(Clone)] +/// Defines a data source, either a source `std::path::PathBuf`, or a vector of bytes. +pub enum FileDataSource { + File(PathBuf), + Data(Vec), + Bytes(&'static [u8]), +} + +impl Debug for FileDataSource { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + FileDataSource::File(file) => { + f.write_fmt(format_args!("data source: File {}", file.display())) + } + FileDataSource::Data(d) => { + f.write_fmt(format_args!("data source: {} raw bytes ", d.len())) + } + FileDataSource::Bytes(b) => { + f.write_fmt(format_args!("data source: {} raw bytes ", b.len())) + } + } + } +} + +impl FileDataSource { + /// Get the length of the inner data source + pub fn len(&self) -> anyhow::Result { + Ok(match self { + FileDataSource::File(path) => fs::metadata(path) + .with_context(|| format!("failed to read metadata of file `{}`", path.display()))? + .len(), + FileDataSource::Data(v) => v.len() as u64, + FileDataSource::Bytes(s) => s.len() as u64, + }) + } + /// Copy this data source to the specified target that implements io::Write + pub fn copy_to(&self, target: &mut dyn io::Write) -> anyhow::Result<()> { + match self { + FileDataSource::File(file_path) => { + io::copy( + &mut fs::File::open(file_path).with_context(|| { + format!("failed to open `{}` for copying", file_path.display()) + })?, + target, + )?; + } + FileDataSource::Data(contents) => { + let mut cursor = Cursor::new(contents); + io::copy(&mut cursor, target)?; + } + FileDataSource::Bytes(contents) => { + let mut cursor = Cursor::new(contents); + io::copy(&mut cursor, target)?; + } + }; + + Ok(()) + } +} diff --git a/src/frame_allocator.rs b/src/frame_allocator.rs deleted file mode 100644 index 5b77cdb2..00000000 --- a/src/frame_allocator.rs +++ /dev/null @@ -1,127 +0,0 @@ -use super::{frame_range, phys_frame_range}; -use bootloader::bootinfo::{MemoryMap, MemoryRegion, MemoryRegionType}; -use x86_64::structures::paging::{frame::PhysFrameRange, PhysFrame}; - -pub(crate) struct FrameAllocator<'a> { - pub memory_map: &'a mut MemoryMap, -} - -impl<'a> FrameAllocator<'a> { - pub(crate) fn allocate_frame(&mut self, region_type: MemoryRegionType) -> Option { - // try to find an existing region of same type that can be enlarged - let mut iter = self.memory_map.iter_mut().peekable(); - while let Some(region) = iter.next() { - if region.region_type == region_type { - if let Some(next) = iter.peek() { - if next.range.start_frame_number == region.range.end_frame_number - && next.region_type == MemoryRegionType::Usable - && !next.range.is_empty() - { - let frame = phys_frame_range(region.range).end; - region.range.end_frame_number += 1; - iter.next().unwrap().range.start_frame_number += 1; - return Some(frame); - } - } - } - } - - fn split_usable_region<'a, I>(iter: &mut I) -> Option<(PhysFrame, PhysFrameRange)> - where - I: Iterator, - { - for region in iter { - if region.region_type != MemoryRegionType::Usable { - continue; - } - if region.range.is_empty() { - continue; - } - - let frame = phys_frame_range(region.range).start; - region.range.start_frame_number += 1; - return Some((frame, PhysFrame::range(frame, frame + 1))); - } - None - } - - let result = if region_type == MemoryRegionType::PageTable { - // prevent fragmentation when page tables are allocated in between - split_usable_region(&mut self.memory_map.iter_mut().rev()) - } else { - split_usable_region(&mut self.memory_map.iter_mut()) - }; - - if let Some((frame, range)) = result { - self.memory_map.add_region(MemoryRegion { - range: frame_range(range), - region_type, - }); - Some(frame) - } else { - None - } - } - - /// Marks the passed region in the memory map. - /// - /// Panics if a non-usable region (e.g. a reserved region) overlaps with the passed region. - pub(crate) fn mark_allocated_region(&mut self, region: MemoryRegion) { - for r in self.memory_map.iter_mut() { - if region.range.start_frame_number >= r.range.end_frame_number { - continue; - } - if region.range.end_frame_number <= r.range.start_frame_number { - continue; - } - - if r.region_type != MemoryRegionType::Usable { - panic!( - "region {:x?} overlaps with non-usable region {:x?}", - region, r - ); - } - - if region.range.start_frame_number == r.range.start_frame_number { - if region.range.end_frame_number < r.range.end_frame_number { - // Case: (r = `r`, R = `region`) - // ----rrrrrrrrrrr---- - // ----RRRR----------- - r.range.start_frame_number = region.range.end_frame_number; - self.memory_map.add_region(region); - } else { - // Case: (r = `r`, R = `region`) - // ----rrrrrrrrrrr---- - // ----RRRRRRRRRRRRRR- - *r = region; - } - } else if region.range.start_frame_number > r.range.start_frame_number { - if region.range.end_frame_number < r.range.end_frame_number { - // Case: (r = `r`, R = `region`) - // ----rrrrrrrrrrr---- - // ------RRRR--------- - let mut behind_r = r.clone(); - behind_r.range.start_frame_number = region.range.end_frame_number; - r.range.end_frame_number = region.range.start_frame_number; - self.memory_map.add_region(behind_r); - self.memory_map.add_region(region); - } else { - // Case: (r = `r`, R = `region`) - // ----rrrrrrrrrrr---- - // -----------RRRR---- or - // -------------RRRR-- - r.range.end_frame_number = region.range.start_frame_number; - self.memory_map.add_region(region); - } - } else { - // Case: (r = `r`, R = `region`) - // ----rrrrrrrrrrr---- - // --RRRR------------- - r.range.start_frame_number = region.range.end_frame_number; - self.memory_map.add_region(region); - } - return; - } - panic!("region {:x?} is not a usable memory region", region); - } -} diff --git a/src/gpt.rs b/src/gpt.rs new file mode 100644 index 00000000..73959578 --- /dev/null +++ b/src/gpt.rs @@ -0,0 +1,70 @@ +use anyhow::Context; +use std::{ + fs::{self, File}, + io::{self, Seek}, + path::Path, +}; + +pub fn create_gpt_disk(fat_image: &Path, out_gpt_path: &Path) -> anyhow::Result<()> { + // create new file + let mut disk = fs::OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(out_gpt_path) + .with_context(|| format!("failed to create GPT file at `{}`", out_gpt_path.display()))?; + + // set file size + let partition_size: u64 = fs::metadata(fat_image) + .context("failed to read metadata of fat image")? + .len(); + let disk_size = partition_size + 1024 * 64; // for GPT headers + disk.set_len(disk_size) + .context("failed to set GPT image file length")?; + + // create a protective MBR at LBA0 so that disk is not considered + // unformatted on BIOS systems + let mbr = gpt::mbr::ProtectiveMBR::with_lb_size( + u32::try_from((disk_size / 512) - 1).unwrap_or(0xFF_FF_FF_FF), + ); + mbr.overwrite_lba0(&mut disk) + .context("failed to write protective MBR")?; + + // create new GPT structure + let block_size = gpt::disk::LogicalBlockSize::Lb512; + let mut gpt = gpt::GptConfig::new() + .writable(true) + .initialized(false) + .logical_block_size(block_size) + .create_from_device(Box::new(&mut disk), None) + .context("failed to create GPT structure in file")?; + gpt.update_partitions(Default::default()) + .context("failed to update GPT partitions")?; + + // add new EFI system partition and get its byte offset in the file + let partition_id = gpt + .add_partition("boot", partition_size, gpt::partition_types::EFI, 0, None) + .context("failed to add boot EFI partition")?; + let partition = gpt + .partitions() + .get(&partition_id) + .context("failed to open boot partition after creation")?; + let start_offset = partition + .bytes_start(block_size) + .context("failed to get start offset of boot partition")?; + + // close the GPT structure and write out changes + gpt.write().context("failed to write out GPT changes")?; + + // place the FAT filesystem in the newly created partition + disk.seek(io::SeekFrom::Start(start_offset)) + .context("failed to seek to start offset")?; + io::copy( + &mut File::open(fat_image).context("failed to open FAT image")?, + &mut disk, + ) + .context("failed to copy FAT image to GPT disk")?; + + Ok(()) +} diff --git a/src/level4_entries.rs b/src/level4_entries.rs deleted file mode 100644 index ce877a32..00000000 --- a/src/level4_entries.rs +++ /dev/null @@ -1,43 +0,0 @@ -use core::convert::TryInto; -use fixedvec::FixedVec; -use x86_64::ux; -use x86_64::{structures::paging::Page, VirtAddr}; -use xmas_elf::program::ProgramHeader64; - -pub struct UsedLevel4Entries { - entry_state: [bool; 512], // whether an entry is in use by the kernel -} - -impl UsedLevel4Entries { - pub fn new(segments: &FixedVec) -> Self { - let mut used = UsedLevel4Entries { - entry_state: [false; 512], - }; - - used.entry_state[0] = true; // TODO: Can we do this dynamically? - - for segment in segments { - let start_page: Page = Page::containing_address(VirtAddr::new(segment.virtual_addr)); - let end_page: Page = - Page::containing_address(VirtAddr::new(segment.virtual_addr + segment.mem_size)); - - for p4_index in u64::from(start_page.p4_index())..=u64::from(end_page.p4_index()) { - used.entry_state[p4_index as usize] = true; - } - } - - used - } - - pub fn get_free_entry(&mut self) -> ux::u9 { - let (idx, entry) = self - .entry_state - .iter_mut() - .enumerate() - .find(|(_, &mut entry)| entry == false) - .expect("no usable level 4 entries found"); - - *entry = true; - ux::u9::new(idx.try_into().unwrap()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 00d3d069..db5f32e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,33 +1,223 @@ -//! This library part of the bootloader allows kernels to retrieve information from the bootloader. -//! -//! To combine your kernel with the bootloader crate you need a tool such -//! as [`bootimage`](https://github.com/rust-osdev/bootimage). See the -//! [_Writing an OS in Rust_](https://os.phil-opp.com/minimal-rust-kernel/#creating-a-bootimage) -//! blog for an explanation. - -#![no_std] +/*! +An experimental x86_64 bootloader that works on both BIOS and UEFI systems. +*/ + #![warn(missing_docs)] -pub use crate::bootinfo::BootInfo; +extern crate alloc; -pub mod bootinfo; +#[cfg(feature = "bios")] +mod bios; +#[cfg(feature = "uefi")] +mod gpt; +#[cfg(feature = "bios")] +mod mbr; +#[cfg(feature = "uefi")] +mod uefi; -/// Defines the entry point function. -/// -/// The function must have the signature `fn(&'static BootInfo) -> !`. +#[cfg(feature = "uefi")] +pub use uefi::UefiBoot; + +#[cfg(feature = "bios")] +pub use bios::BiosBoot; + +mod fat; +mod file_data_source; + +use std::{ + borrow::Cow, + collections::BTreeMap, + path::{Path, PathBuf}, +}; + +use anyhow::Context; + +use tempfile::NamedTempFile; + +use crate::file_data_source::FileDataSource; +pub use bootloader_boot_config::BootConfig; + +const KERNEL_FILE_NAME: &str = "kernel-x86_64"; +const RAMDISK_FILE_NAME: &str = "ramdisk"; +const CONFIG_FILE_NAME: &str = "boot.json"; + +#[cfg(feature = "uefi")] +const UEFI_BOOTLOADER: &[u8] = include_bytes!(env!("UEFI_BOOTLOADER_PATH")); +#[cfg(feature = "bios")] +const BIOS_BOOT_SECTOR: &[u8] = include_bytes!(env!("BIOS_BOOT_SECTOR_PATH")); +#[cfg(feature = "bios")] +const BIOS_STAGE_2: &[u8] = include_bytes!(env!("BIOS_STAGE_2_PATH")); +#[cfg(feature = "bios")] +const BIOS_STAGE_3: &[u8] = include_bytes!(env!("BIOS_STAGE_3_PATH")); +#[cfg(feature = "bios")] +const BIOS_STAGE_4: &[u8] = include_bytes!(env!("BIOS_STAGE_4_PATH")); + +/// Allows creating disk images for a specified set of files. /// -/// This macro just creates a function named `_start`, which the linker will use as the entry -/// point. The advantage of using this macro instead of providing an own `_start` function is -/// that the macro ensures that the function and argument types are correct. -#[macro_export] -macro_rules! entry_point { - ($path:path) => { - #[export_name = "_start"] - pub extern "C" fn __impl_start(boot_info: &'static $crate::bootinfo::BootInfo) -> ! { - // validate the signature of the program entry point - let f: fn(&'static $crate::bootinfo::BootInfo) -> ! = $path; - - f(boot_info) +/// It can currently create `MBR` (BIOS), `GPT` (UEFI), and `TFTP` (UEFI) images. +pub struct DiskImageBuilder { + files: BTreeMap, FileDataSource>, +} + +impl DiskImageBuilder { + /// Create a new instance of DiskImageBuilder, with the specified kernel. + pub fn new(kernel: PathBuf) -> Self { + let mut obj = Self::empty(); + obj.set_kernel(kernel); + obj + } + + /// Create a new, empty instance of DiskImageBuilder + pub fn empty() -> Self { + Self { + files: BTreeMap::new(), + } + } + + /// Add or replace a kernel to be included in the final image. + pub fn set_kernel(&mut self, path: PathBuf) -> &mut Self { + self.set_file_source(KERNEL_FILE_NAME.into(), FileDataSource::File(path)) + } + + /// Add or replace a ramdisk to be included in the final image. + pub fn set_ramdisk(&mut self, path: PathBuf) -> &mut Self { + self.set_file_source(RAMDISK_FILE_NAME.into(), FileDataSource::File(path)) + } + + /// Configures the runtime behavior of the bootloader. + pub fn set_boot_config(&mut self, boot_config: &BootConfig) -> &mut Self { + let json = serde_json::to_vec_pretty(boot_config).expect("failed to serialize BootConfig"); + self.set_file_source(CONFIG_FILE_NAME.into(), FileDataSource::Data(json)) + } + + /// Add a file with the specified bytes to the disk image + /// + /// Note that the bootloader only loads the kernel and ramdisk files into memory on boot. + /// Other files need to be loaded manually by the kernel. + pub fn set_file_contents(&mut self, destination: String, data: Vec) -> &mut Self { + self.set_file_source(destination.into(), FileDataSource::Data(data)) + } + + /// Add a file with the specified source file to the disk image + /// + /// Note that the bootloader only loads the kernel and ramdisk files into memory on boot. + /// Other files need to be loaded manually by the kernel. + pub fn set_file(&mut self, destination: String, file_path: PathBuf) -> &mut Self { + self.set_file_source(destination.into(), FileDataSource::File(file_path)) + } + + #[cfg(feature = "bios")] + /// Create an MBR disk image for booting on BIOS systems. + pub fn create_bios_image(&self, image_path: &Path) -> anyhow::Result<()> { + const BIOS_STAGE_3_NAME: &str = "boot-stage-3"; + const BIOS_STAGE_4_NAME: &str = "boot-stage-4"; + let stage_3 = FileDataSource::Bytes(BIOS_STAGE_3); + let stage_4 = FileDataSource::Bytes(BIOS_STAGE_4); + let mut internal_files = BTreeMap::new(); + internal_files.insert(BIOS_STAGE_3_NAME, stage_3); + internal_files.insert(BIOS_STAGE_4_NAME, stage_4); + let fat_partition = self + .create_fat_filesystem_image(internal_files) + .context("failed to create FAT partition")?; + mbr::create_mbr_disk( + BIOS_BOOT_SECTOR, + BIOS_STAGE_2, + fat_partition.path(), + image_path, + ) + .context("failed to create BIOS MBR disk image")?; + + fat_partition + .close() + .context("failed to delete FAT partition after disk image creation")?; + Ok(()) + } + + #[cfg(feature = "uefi")] + /// Create a GPT disk image for booting on UEFI systems. + pub fn create_uefi_image(&self, image_path: &Path) -> anyhow::Result<()> { + const UEFI_BOOT_FILENAME: &str = "efi/boot/bootx64.efi"; + + let mut internal_files = BTreeMap::new(); + internal_files.insert(UEFI_BOOT_FILENAME, FileDataSource::Bytes(UEFI_BOOTLOADER)); + let fat_partition = self + .create_fat_filesystem_image(internal_files) + .context("failed to create FAT partition")?; + gpt::create_gpt_disk(fat_partition.path(), image_path) + .context("failed to create UEFI GPT disk image")?; + fat_partition + .close() + .context("failed to delete FAT partition after disk image creation")?; + + Ok(()) + } + + #[cfg(feature = "uefi")] + /// Create a folder containing the needed files for UEFI TFTP/PXE booting. + pub fn create_uefi_tftp_folder(&self, tftp_path: &Path) -> anyhow::Result<()> { + use std::{fs, ops::Deref}; + + const UEFI_TFTP_BOOT_FILENAME: &str = "bootloader"; + fs::create_dir_all(tftp_path) + .with_context(|| format!("failed to create out dir at {}", tftp_path.display()))?; + + let to = tftp_path.join(UEFI_TFTP_BOOT_FILENAME); + fs::write(&to, UEFI_BOOTLOADER).with_context(|| { + format!( + "failed to copy bootloader from the embedded binary to {}", + to.display() + ) + })?; + + for f in &self.files { + let to = tftp_path.join(f.0.deref()); + + let mut new_file = fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(to)?; + + f.1.copy_to(&mut new_file)?; + } + + Ok(()) + } + + /// Add a file source to the disk image + fn set_file_source( + &mut self, + destination: Cow<'static, str>, + source: FileDataSource, + ) -> &mut Self { + self.files.insert(destination, source); + self + } + + fn create_fat_filesystem_image( + &self, + internal_files: BTreeMap<&str, FileDataSource>, + ) -> anyhow::Result { + let mut local_map: BTreeMap<&str, _> = BTreeMap::new(); + + for (name, source) in &self.files { + local_map.insert(name, source); + } + + for k in &internal_files { + if local_map.insert(k.0, k.1).is_some() { + return Err(anyhow::Error::msg(format!( + "Attempted to overwrite internal file: {}", + k.0 + ))); + } } - }; + + let out_file = NamedTempFile::new().context("failed to create temp file")?; + fat::create_fat_filesystem(local_map, out_file.path()) + .context("failed to create FAT filesystem")?; + + Ok(out_file) + } } diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 66b13fbe..00000000 --- a/src/main.rs +++ /dev/null @@ -1,397 +0,0 @@ -#![feature(lang_items)] -#![feature(global_asm)] -#![feature(step_trait)] -#![feature(asm)] -#![feature(nll)] -#![feature(const_fn)] -#![no_std] -#![no_main] - -#[cfg(not(target_os = "none"))] -compile_error!("The bootloader crate must be compiled for the `x86_64-bootloader.json` target"); - -use bootloader::bootinfo::{BootInfo, FrameRange}; -use core::convert::TryInto; -use core::panic::PanicInfo; -use core::{mem, slice}; -use fixedvec::alloc_stack; -use usize_conversions::usize_from; -use x86_64::instructions::tlb; -use x86_64::structures::paging::{ - frame::PhysFrameRange, page_table::PageTableEntry, Mapper, Page, PageTable, PageTableFlags, - PhysFrame, RecursivePageTable, Size2MiB, Size4KiB, -}; -use x86_64::ux::u9; -use x86_64::{PhysAddr, VirtAddr}; - -// The bootloader_config.rs file contains some configuration constants set by the build script: -// PHYSICAL_MEMORY_OFFSET: The offset into the virtual address space where the physical memory -// is mapped if the `map_physical_memory` feature is activated. -// -// KERNEL_STACK_ADDRESS: The virtual address of the kernel stack. -// -// KERNEL_STACK_SIZE: The number of pages in the kernel stack. -include!(concat!(env!("OUT_DIR"), "/bootloader_config.rs")); - -global_asm!(include_str!("stage_1.s")); -global_asm!(include_str!("stage_2.s")); -global_asm!(include_str!("e820.s")); -global_asm!(include_str!("stage_3.s")); - -#[cfg(feature = "vga_320x200")] -global_asm!(include_str!("video_mode/vga_320x200.s")); -#[cfg(not(feature = "vga_320x200"))] -global_asm!(include_str!("video_mode/vga_text_80x25.s")); - -unsafe fn context_switch(boot_info: VirtAddr, entry_point: VirtAddr, stack_pointer: VirtAddr) -> ! { - asm!("call $1; ${:private}.spin.${:uid}: jmp ${:private}.spin.${:uid}" :: - "{rsp}"(stack_pointer), "r"(entry_point), "{rdi}"(boot_info) :: "intel"); - ::core::hint::unreachable_unchecked() -} - -mod boot_info; -mod frame_allocator; -mod level4_entries; -mod page_table; -mod printer; -#[cfg(feature = "sse")] -mod sse; - -pub struct IdentityMappedAddr(PhysAddr); - -impl IdentityMappedAddr { - fn phys(&self) -> PhysAddr { - self.0 - } - - fn virt(&self) -> VirtAddr { - VirtAddr::new(self.0.as_u64()) - } - - fn as_u64(&self) -> u64 { - self.0.as_u64() - } -} - -// Symbols defined in `linker.ld` -extern "C" { - static mmap_ent: usize; - static _memory_map: usize; - static _kernel_start_addr: usize; - static _kernel_end_addr: usize; - static _kernel_size: usize; - static __page_table_start: usize; - static __page_table_end: usize; - static __bootloader_end: usize; - static __bootloader_start: usize; - static _p4: usize; -} - -#[no_mangle] -pub unsafe extern "C" fn stage_4() -> ! { - // Set stack segment - asm!("mov bx, 0x0 - mov ss, bx" ::: "bx" : "intel"); - - let kernel_start = 0x400000; - let kernel_size = &_kernel_size as *const _ as u64; - let memory_map_addr = &_memory_map as *const _ as u64; - let memory_map_entry_count = (mmap_ent & 0xff) as u64; // Extract lower 8 bits - let page_table_start = &__page_table_start as *const _ as u64; - let page_table_end = &__page_table_end as *const _ as u64; - let bootloader_start = &__bootloader_start as *const _ as u64; - let bootloader_end = &__bootloader_end as *const _ as u64; - let p4_physical = &_p4 as *const _ as u64; - - bootloader_main( - IdentityMappedAddr(PhysAddr::new(kernel_start)), - kernel_size, - VirtAddr::new(memory_map_addr), - memory_map_entry_count, - PhysAddr::new(page_table_start), - PhysAddr::new(page_table_end), - PhysAddr::new(bootloader_start), - PhysAddr::new(bootloader_end), - PhysAddr::new(p4_physical), - ) -} - -fn bootloader_main( - kernel_start: IdentityMappedAddr, - kernel_size: u64, - memory_map_addr: VirtAddr, - memory_map_entry_count: u64, - page_table_start: PhysAddr, - page_table_end: PhysAddr, - bootloader_start: PhysAddr, - bootloader_end: PhysAddr, - p4_physical: PhysAddr, -) -> ! { - use bootloader::bootinfo::{MemoryRegion, MemoryRegionType}; - use fixedvec::FixedVec; - use xmas_elf::program::{ProgramHeader, ProgramHeader64}; - - printer::Printer.clear_screen(); - - let mut memory_map = boot_info::create_from(memory_map_addr, memory_map_entry_count); - - let max_phys_addr = memory_map - .iter() - .map(|r| r.range.end_addr()) - .max() - .expect("no physical memory regions found"); - - // Extract required information from the ELF file. - let mut preallocated_space = alloc_stack!([ProgramHeader64; 32]); - let mut segments = FixedVec::new(&mut preallocated_space); - let entry_point; - { - let kernel_start_ptr = usize_from(kernel_start.as_u64()) as *const u8; - let kernel = unsafe { slice::from_raw_parts(kernel_start_ptr, usize_from(kernel_size)) }; - let elf_file = xmas_elf::ElfFile::new(kernel).unwrap(); - xmas_elf::header::sanity_check(&elf_file).unwrap(); - - entry_point = elf_file.header.pt2.entry_point(); - - for program_header in elf_file.program_iter() { - match program_header { - ProgramHeader::Ph64(header) => segments - .push(*header) - .expect("does not support more than 32 program segments"), - ProgramHeader::Ph32(_) => panic!("does not support 32 bit elf files"), - } - } - } - - // Mark used virtual addresses - let mut level4_entries = level4_entries::UsedLevel4Entries::new(&segments); - - // Enable support for the no-execute bit in page tables. - enable_nxe_bit(); - - // Create a recursive page table entry - let recursive_index = u9::new(level4_entries.get_free_entry().try_into().unwrap()); - let mut entry = PageTableEntry::new(); - entry.set_addr( - p4_physical, - PageTableFlags::PRESENT | PageTableFlags::WRITABLE, - ); - - // Write the recursive entry into the page table - let page_table = unsafe { &mut *(p4_physical.as_u64() as *mut PageTable) }; - page_table[recursive_index] = entry; - tlb::flush_all(); - - let recursive_page_table_addr = Page::from_page_table_indices( - recursive_index, - recursive_index, - recursive_index, - recursive_index, - ) - .start_address(); - let page_table = unsafe { &mut *(recursive_page_table_addr.as_mut_ptr()) }; - let mut rec_page_table = - RecursivePageTable::new(page_table).expect("recursive page table creation failed"); - - // Create a frame allocator, which marks allocated frames as used in the memory map. - let mut frame_allocator = frame_allocator::FrameAllocator { - memory_map: &mut memory_map, - }; - - // Mark already used memory areas in frame allocator. - { - let zero_frame: PhysFrame = PhysFrame::from_start_address(PhysAddr::new(0)).unwrap(); - frame_allocator.mark_allocated_region(MemoryRegion { - range: frame_range(PhysFrame::range(zero_frame, zero_frame + 1)), - region_type: MemoryRegionType::FrameZero, - }); - let bootloader_start_frame = PhysFrame::containing_address(bootloader_start); - let bootloader_end_frame = PhysFrame::containing_address(bootloader_end - 1u64); - let bootloader_memory_area = - PhysFrame::range(bootloader_start_frame, bootloader_end_frame + 1); - frame_allocator.mark_allocated_region(MemoryRegion { - range: frame_range(bootloader_memory_area), - region_type: MemoryRegionType::Bootloader, - }); - let kernel_start_frame = PhysFrame::containing_address(kernel_start.phys()); - let kernel_end_frame = - PhysFrame::containing_address(kernel_start.phys() + kernel_size - 1u64); - let kernel_memory_area = PhysFrame::range(kernel_start_frame, kernel_end_frame + 1); - frame_allocator.mark_allocated_region(MemoryRegion { - range: frame_range(kernel_memory_area), - region_type: MemoryRegionType::Kernel, - }); - let page_table_start_frame = PhysFrame::containing_address(page_table_start); - let page_table_end_frame = PhysFrame::containing_address(page_table_end - 1u64); - let page_table_memory_area = - PhysFrame::range(page_table_start_frame, page_table_end_frame + 1); - frame_allocator.mark_allocated_region(MemoryRegion { - range: frame_range(page_table_memory_area), - region_type: MemoryRegionType::PageTable, - }); - } - - // Unmap the ELF file. - let kernel_start_page: Page = Page::containing_address(kernel_start.virt()); - let kernel_end_page: Page = - Page::containing_address(kernel_start.virt() + kernel_size - 1u64); - for page in Page::range_inclusive(kernel_start_page, kernel_end_page) { - rec_page_table.unmap(page).expect("dealloc error").1.flush(); - } - - // Map a page for the boot info structure - let boot_info_page = { - let page: Page = Page::from_page_table_indices( - level4_entries.get_free_entry(), - u9::new(0), - u9::new(0), - u9::new(0), - ); - let frame = frame_allocator - .allocate_frame(MemoryRegionType::BootInfo) - .expect("frame allocation failed"); - let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE; - unsafe { - page_table::map_page( - page, - frame, - flags, - &mut rec_page_table, - &mut frame_allocator, - ) - } - .expect("Mapping of bootinfo page failed") - .flush(); - page - }; - - // If no kernel stack address is provided, map the kernel stack after the boot info page - let kernel_stack_address = match KERNEL_STACK_ADDRESS { - Some(addr) => Page::containing_address(VirtAddr::new(addr)), - None => boot_info_page + 1, - }; - - // Map kernel segments. - let stack_end = page_table::map_kernel( - kernel_start.phys(), - kernel_stack_address, - KERNEL_STACK_SIZE, - &segments, - &mut rec_page_table, - &mut frame_allocator, - ) - .expect("kernel mapping failed"); - - let physical_memory_offset = if cfg!(feature = "map_physical_memory") { - let physical_memory_offset = PHYSICAL_MEMORY_OFFSET.unwrap_or_else(|| { - // If offset not manually provided, find a free p4 entry and map memory here. - // One level 4 entry spans 2^48/512 bytes (over 500gib) so this should suffice. - assert!(max_phys_addr < (1 << 48) / 512); - Page::from_page_table_indices_1gib(level4_entries.get_free_entry(), u9::new(0)) - .start_address() - .as_u64() - }); - - let virt_for_phys = - |phys: PhysAddr| -> VirtAddr { VirtAddr::new(phys.as_u64() + physical_memory_offset) }; - - let start_frame = PhysFrame::::containing_address(PhysAddr::new(0)); - let end_frame = PhysFrame::::containing_address(PhysAddr::new(max_phys_addr)); - - for frame in PhysFrame::range_inclusive(start_frame, end_frame) { - let page = Page::containing_address(virt_for_phys(frame.start_address())); - let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE; - unsafe { - page_table::map_page( - page, - frame, - flags, - &mut rec_page_table, - &mut frame_allocator, - ) - } - .expect("Mapping of bootinfo page failed") - .flush(); - } - - physical_memory_offset - } else { - 0 // Value is unused by BootInfo::new, so this doesn't matter - }; - - // Construct boot info structure. - let mut boot_info = BootInfo::new( - memory_map, - recursive_page_table_addr.as_u64(), - physical_memory_offset, - ); - boot_info.memory_map.sort(); - - // Write boot info to boot info page. - let boot_info_addr = boot_info_page.start_address(); - unsafe { boot_info_addr.as_mut_ptr::().write(boot_info) }; - - // Make sure that the kernel respects the write-protection bits, even when in ring 0. - enable_write_protect_bit(); - - if cfg!(not(feature = "recursive_page_table")) { - // unmap recursive entry - rec_page_table - .unmap(Page::::containing_address( - recursive_page_table_addr, - )) - .expect("error deallocating recursive entry") - .1 - .flush(); - mem::drop(rec_page_table); - } - - #[cfg(feature = "sse")] - sse::enable_sse(); - - let entry_point = VirtAddr::new(entry_point); - unsafe { context_switch(boot_info_addr, entry_point, stack_end) }; -} - -fn enable_nxe_bit() { - use x86_64::registers::control::{Efer, EferFlags}; - unsafe { Efer::update(|efer| *efer |= EferFlags::NO_EXECUTE_ENABLE) } -} - -fn enable_write_protect_bit() { - use x86_64::registers::control::{Cr0, Cr0Flags}; - unsafe { Cr0::update(|cr0| *cr0 |= Cr0Flags::WRITE_PROTECT) }; -} - -#[panic_handler] -#[no_mangle] -pub fn panic(info: &PanicInfo) -> ! { - use core::fmt::Write; - write!(printer::Printer, "{}", info).unwrap(); - loop {} -} - -#[lang = "eh_personality"] -#[no_mangle] -pub extern "C" fn eh_personality() { - loop {} -} - -#[no_mangle] -pub extern "C" fn _Unwind_Resume() { - loop {} -} - -fn phys_frame_range(range: FrameRange) -> PhysFrameRange { - PhysFrameRange { - start: PhysFrame::from_start_address(PhysAddr::new(range.start_addr())).unwrap(), - end: PhysFrame::from_start_address(PhysAddr::new(range.end_addr())).unwrap(), - } -} - -fn frame_range(range: PhysFrameRange) -> FrameRange { - FrameRange::new( - range.start.start_address().as_u64(), - range.end.start_address().as_u64(), - ) -} diff --git a/src/mbr.rs b/src/mbr.rs new file mode 100644 index 00000000..3328a07f --- /dev/null +++ b/src/mbr.rs @@ -0,0 +1,100 @@ +use anyhow::Context; +use mbrman::BOOT_ACTIVE; +use std::{ + fs::{self, File}, + io::{self, Seek, SeekFrom}, + path::Path, +}; + +const SECTOR_SIZE: u32 = 512; + +pub fn create_mbr_disk( + bootsector_binary: &[u8], + second_stage_binary: &[u8], + boot_partition_path: &Path, + out_mbr_path: &Path, +) -> anyhow::Result<()> { + use std::io::Cursor; + let mut boot_sector = Cursor::new(bootsector_binary); + let mut mbr = + mbrman::MBR::read_from(&mut boot_sector, SECTOR_SIZE).context("failed to read MBR")?; + + for (index, partition) in mbr.iter() { + if !partition.is_unused() { + anyhow::bail!("partition {index} should be unused"); + } + } + + let mut second_stage = Cursor::new(second_stage_binary); + let second_stage_size = second_stage_binary.len() as u64; + + let second_stage_start_sector = 1; + let second_stage_sectors = ((second_stage_size - 1) / u64::from(SECTOR_SIZE) + 1) + .try_into() + .context("size of second stage is larger than u32::MAX")?; + mbr[1] = mbrman::MBRPartitionEntry { + boot: BOOT_ACTIVE, + starting_lba: second_stage_start_sector, + sectors: second_stage_sectors, + // see BOOTLOADER_SECOND_STAGE_PARTITION_TYPE in `boot_sector` crate + sys: 0x20, + + first_chs: mbrman::CHS::empty(), + last_chs: mbrman::CHS::empty(), + }; + + let mut boot_partition = + File::open(boot_partition_path).context("failed to open FAT boot partition")?; + let boot_partition_start_sector = second_stage_start_sector + second_stage_sectors; + let boot_partition_size = boot_partition + .metadata() + .context("failed to read file metadata of FAT boot partition")? + .len(); + mbr[2] = mbrman::MBRPartitionEntry { + boot: BOOT_ACTIVE, + starting_lba: boot_partition_start_sector, + sectors: ((boot_partition_size - 1) / u64::from(SECTOR_SIZE) + 1) + .try_into() + .context("size of FAT partition is larger than u32::MAX")?, + //TODO: is this the correct type? + sys: 0x0c, // FAT32 with LBA + + first_chs: mbrman::CHS::empty(), + last_chs: mbrman::CHS::empty(), + }; + + let mut disk = fs::OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(out_mbr_path) + .with_context(|| { + format!( + "failed to create MBR disk image at `{}`", + out_mbr_path.display() + ) + })?; + + mbr.write_into(&mut disk) + .context("failed to write MBR header to disk image")?; + + // second stage + assert_eq!( + disk.stream_position() + .context("failed to get disk image seek position")?, + u64::from(second_stage_start_sector * SECTOR_SIZE) + ); + io::copy(&mut second_stage, &mut disk) + .context("failed to copy second stage binary to MBR disk image")?; + + // fat partition + disk.seek(SeekFrom::Start( + (boot_partition_start_sector * SECTOR_SIZE).into(), + )) + .context("seek failed")?; + io::copy(&mut boot_partition, &mut disk) + .context("failed to copy FAT image to MBR disk image")?; + + Ok(()) +} diff --git a/src/page_table.rs b/src/page_table.rs deleted file mode 100644 index 6d7ef5a7..00000000 --- a/src/page_table.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::frame_allocator::FrameAllocator; -use bootloader::bootinfo::MemoryRegionType; -use fixedvec::FixedVec; -use x86_64::structures::paging::mapper::{MapToError, MapperFlush, UnmapError}; -use x86_64::structures::paging::{ - self, Mapper, Page, PageSize, PageTableFlags, PhysFrame, RecursivePageTable, Size4KiB, -}; -use x86_64::{align_up, PhysAddr, VirtAddr}; -use xmas_elf::program::{self, ProgramHeader64}; - -pub(crate) fn map_kernel( - kernel_start: PhysAddr, - stack_start: Page, - stack_size: u64, - segments: &FixedVec, - page_table: &mut RecursivePageTable, - frame_allocator: &mut FrameAllocator, -) -> Result { - for segment in segments { - map_segment(segment, kernel_start, page_table, frame_allocator)?; - } - - // Create a stack - let stack_start = stack_start + 1; // Leave the first page unmapped as a 'guard page' - let stack_end = stack_start + stack_size; // stack_size is in pages - - let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE; - let region_type = MemoryRegionType::KernelStack; - - for page in Page::range(stack_start, stack_end) { - let frame = frame_allocator - .allocate_frame(region_type) - .ok_or(MapToError::FrameAllocationFailed)?; - unsafe { map_page(page, frame, flags, page_table, frame_allocator)? }.flush(); - } - - Ok(stack_end.start_address()) -} - -pub(crate) fn map_segment( - segment: &ProgramHeader64, - kernel_start: PhysAddr, - page_table: &mut RecursivePageTable, - frame_allocator: &mut FrameAllocator, -) -> Result<(), MapToError> { - let typ = segment.get_type().unwrap(); - match typ { - program::Type::Load => { - let mem_size = segment.mem_size; - let file_size = segment.file_size; - let file_offset = segment.offset; - let phys_start_addr = kernel_start + file_offset; - let virt_start_addr = VirtAddr::new(segment.virtual_addr); - - let start_page: Page = Page::containing_address(virt_start_addr); - let start_frame = PhysFrame::containing_address(phys_start_addr); - let end_frame = PhysFrame::containing_address(phys_start_addr + file_size - 1u64); - - let flags = segment.flags; - let mut page_table_flags = PageTableFlags::PRESENT; - if !flags.is_execute() { - page_table_flags |= PageTableFlags::NO_EXECUTE - }; - if flags.is_write() { - page_table_flags |= PageTableFlags::WRITABLE - }; - - for frame in PhysFrame::range_inclusive(start_frame, end_frame) { - let offset = frame - start_frame; - let page = start_page + offset; - unsafe { map_page(page, frame, page_table_flags, page_table, frame_allocator)? } - .flush(); - } - - if mem_size > file_size { - // .bss section (or similar), which needs to be zeroed - let zero_start = virt_start_addr + file_size; - let zero_end = virt_start_addr + mem_size; - if zero_start.as_u64() & 0xfff != 0 { - // A part of the last mapped frame needs to be zeroed. This is - // not possible since it could already contains parts of the next - // segment. Thus, we need to copy it before zeroing. - - // TODO: search for a free page dynamically - let temp_page: Page = Page::containing_address(VirtAddr::new(0xfeeefeee000)); - let new_frame = frame_allocator - .allocate_frame(MemoryRegionType::Kernel) - .ok_or(MapToError::FrameAllocationFailed)?; - unsafe { - map_page( - temp_page.clone(), - new_frame.clone(), - page_table_flags, - page_table, - frame_allocator, - )? - } - .flush(); - - type PageArray = [u64; Size4KiB::SIZE as usize / 8]; - - let last_page = Page::containing_address(virt_start_addr + file_size - 1u64); - let last_page_ptr = last_page.start_address().as_ptr::(); - let temp_page_ptr = temp_page.start_address().as_mut_ptr::(); - - unsafe { - // copy contents - temp_page_ptr.write(last_page_ptr.read()); - } - - // remap last page - if let Err(e) = page_table.unmap(last_page.clone()) { - return Err(match e { - UnmapError::ParentEntryHugePage => MapToError::ParentEntryHugePage, - UnmapError::PageNotMapped => unreachable!(), - UnmapError::InvalidFrameAddress(_) => unreachable!(), - }); - } - - unsafe { - map_page( - last_page, - new_frame, - page_table_flags, - page_table, - frame_allocator, - )? - } - .flush(); - } - - // Map additional frames. - let start_page: Page = Page::containing_address(VirtAddr::new(align_up( - zero_start.as_u64(), - Size4KiB::SIZE, - ))); - let end_page = Page::containing_address(zero_end); - for page in Page::range_inclusive(start_page, end_page) { - let frame = frame_allocator - .allocate_frame(MemoryRegionType::Kernel) - .ok_or(MapToError::FrameAllocationFailed)?; - unsafe { - map_page(page, frame, page_table_flags, page_table, frame_allocator)? - } - .flush(); - } - - // zero - for offset in file_size..mem_size { - let addr = virt_start_addr + offset; - unsafe { addr.as_mut_ptr::().write(0) }; - } - } - } - _ => {} - } - Ok(()) -} - -pub(crate) unsafe fn map_page<'a, S>( - page: Page, - phys_frame: PhysFrame, - flags: PageTableFlags, - page_table: &mut RecursivePageTable<'a>, - frame_allocator: &mut FrameAllocator, -) -> Result, MapToError> -where - S: PageSize, - RecursivePageTable<'a>: Mapper, -{ - struct PageTableAllocator<'a, 'b: 'a>(&'a mut FrameAllocator<'b>); - - unsafe impl<'a, 'b> paging::FrameAllocator for PageTableAllocator<'a, 'b> { - fn allocate_frame(&mut self) -> Option> { - self.0.allocate_frame(MemoryRegionType::PageTable) - } - } - - page_table.map_to( - page, - phys_frame, - flags, - &mut PageTableAllocator(frame_allocator), - ) -} diff --git a/src/printer/mod.rs b/src/printer/mod.rs deleted file mode 100644 index 726bb765..00000000 --- a/src/printer/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[cfg(not(feature = "vga_320x200"))] -pub use self::vga_text_80x25::*; - -#[cfg(feature = "vga_320x200")] -pub use self::vga_320x200::*; - -#[cfg(feature = "vga_320x200")] -mod vga_320x200; - -#[cfg(not(feature = "vga_320x200"))] -mod vga_text_80x25; diff --git a/src/printer/vga_320x200.rs b/src/printer/vga_320x200.rs deleted file mode 100644 index fb1e36af..00000000 --- a/src/printer/vga_320x200.rs +++ /dev/null @@ -1,80 +0,0 @@ -use core::fmt::{Result, Write}; -use core::sync::atomic::{AtomicUsize, Ordering}; - -const VGA_BUFFER: *mut u8 = 0xa0000 as *mut _; -const SCREEN_WIDTH: usize = 320; -const SCREEN_HEIGHT: usize = 200; - -// must not be 0 so that we don't have a .bss section -pub static X_POS: AtomicUsize = AtomicUsize::new(1); -pub static Y_POS: AtomicUsize = AtomicUsize::new(1); - -pub struct Printer; - -impl Printer { - pub fn clear_screen(&mut self) { - for i in 0..(SCREEN_WIDTH * SCREEN_HEIGHT) { - unsafe { - VGA_BUFFER.offset(i as isize).write_volatile(0); - } - } - - X_POS.store(0, Ordering::SeqCst); - Y_POS.store(0, Ordering::SeqCst); - } - - fn newline(&mut self) { - let y_pos = Y_POS.fetch_add(8, Ordering::SeqCst); - X_POS.store(0, Ordering::SeqCst); - if y_pos >= SCREEN_HEIGHT { - self.clear_screen(); - } - } - - fn write_char(&mut self, c: char) { - use font8x8::UnicodeFonts; - - if c == '\n' { - self.newline(); - return; - } - - let x_pos = X_POS.fetch_add(8, Ordering::SeqCst); - let y_pos = Y_POS.load(Ordering::SeqCst); - - match c { - ' '..='~' => { - let rendered = font8x8::BASIC_FONTS - .get(c) - .expect("character not found in basic font"); - for (y, byte) in rendered.iter().enumerate() { - for (x, bit) in (0..8).enumerate() { - if *byte & (1 << bit) == 0 { - continue; - } - let color = 0xf; - let idx = (y_pos + y) * SCREEN_WIDTH + x_pos + x; - unsafe { - VGA_BUFFER.offset(idx as isize).write_volatile(color); - } - } - } - } - _ => panic!("unprintable character"), - } - - if x_pos + 8 >= SCREEN_WIDTH { - self.newline(); - } - } -} - -impl Write for Printer { - fn write_str(&mut self, s: &str) -> Result { - for c in s.chars() { - self.write_char(c); - } - - Ok(()) - } -} diff --git a/src/printer/vga_text_80x25.rs b/src/printer/vga_text_80x25.rs deleted file mode 100644 index fd689987..00000000 --- a/src/printer/vga_text_80x25.rs +++ /dev/null @@ -1,37 +0,0 @@ -use core::fmt::{Result, Write}; -use core::sync::atomic::{AtomicUsize, Ordering}; - -const VGA_BUFFER: *mut u8 = 0xb8000 as *mut _; -const SCREEN_SIZE: usize = 80 * 25; - -// must not be 0 so that we don't have a .bss section -pub static CURRENT_OFFSET: AtomicUsize = AtomicUsize::new(160); - -pub struct Printer; - -impl Printer { - pub fn clear_screen(&mut self) { - for i in 0..SCREEN_SIZE { - unsafe { - VGA_BUFFER.offset(i as isize).write_volatile(0); - } - } - - CURRENT_OFFSET.store(0, Ordering::Relaxed); - } -} - -impl Write for Printer { - fn write_str(&mut self, s: &str) -> Result { - for byte in s.bytes() { - let index = CURRENT_OFFSET.fetch_add(2, Ordering::Relaxed) as isize; - - unsafe { - VGA_BUFFER.offset(index).write_volatile(byte); - VGA_BUFFER.offset(index + 1).write_volatile(0x4f); - } - } - - Ok(()) - } -} diff --git a/src/sse.rs b/src/sse.rs deleted file mode 100644 index 62204ded..00000000 --- a/src/sse.rs +++ /dev/null @@ -1,23 +0,0 @@ -/// Enables Streaming SIMD Extensions (SSE) support for loaded kernels. -pub fn enable_sse() { - use bit_field::BitField; - use x86_64::registers::control::Cr0; - let mut flags = Cr0::read_raw(); - flags.set_bit(2, false); - flags.set_bit(1, true); - flags.set_bit(9, true); - flags.set_bit(10, true); - unsafe { - Cr0::write_raw(flags); - } - // For now, we must use inline ASM here - let mut cr4: u64; - unsafe { - asm!("mov %cr4, $0" : "=r" (cr4)); - } - cr4.set_bit(9, true); - cr4.set_bit(10, true); - unsafe { - asm!("mov $0, %cr4" :: "r" (cr4) : "memory"); - } -} diff --git a/src/stage_1.s b/src/stage_1.s deleted file mode 100644 index ebfa1a08..00000000 --- a/src/stage_1.s +++ /dev/null @@ -1,233 +0,0 @@ -.section .boot-first-stage, "awx" -.global _start -.intel_syntax noprefix -.code16 - -# This stage initializes the stack, enables the A20 line, loads the rest of -# the bootloader from disk, and jumps to stage_2. - -_start: - # zero segment registers - xor ax, ax - mov ds, ax - mov es, ax - mov ss, ax - mov fs, ax - mov gs, ax - - # clear the direction flag (e.g. go forward in memory when using - # instructions like lodsb) - cld - - # initialize stack - mov sp, 0x7c00 - - lea si, boot_start_str - call real_mode_println - -enable_a20: - # enable A20-Line via IO-Port 92, might not work on all motherboards - in al, 0x92 - test al, 2 - jnz enable_a20_after - or al, 2 - and al, 0xFE - out 0x92, al -enable_a20_after: - - -enter_protected_mode: - # clear interrupts - cli - push ds - push es - - lgdt [gdt32info] - - mov eax, cr0 - or al, 1 # set protected mode bit - mov cr0, eax - - jmp protected_mode # tell 386/486 to not crash - -protected_mode: - mov bx, 0x10 - mov ds, bx # set data segment - mov es, bx # set extra segment - - and al, 0xfe # clear protected mode bit - mov cr0, eax - -unreal_mode: - pop es # get back old extra segment - pop ds # get back old data segment - sti - - # back to real mode, but internal data segment register is still loaded - # with gdt segment -> we can access the full 4GiB of memory - - mov bx, 0x0f01 # attrib/char of smiley - mov eax, 0xb8f00 # note 32 bit offset - mov word ptr ds:[eax], bx - -check_int13h_extensions: - mov ah, 0x41 - mov bx, 0x55aa - # dl contains drive number - int 0x13 - jc no_int13h_extensions - -load_rest_of_bootloader_from_disk: - lea eax, _rest_of_bootloader_start_addr - - # start of memory buffer - mov [dap_buffer_addr], ax - - # number of disk blocks to load - lea ebx, _rest_of_bootloader_end_addr - sub ebx, eax # end - start - shr ebx, 9 # divide by 512 (block size) - mov [dap_blocks], bx - - # number of start block - lea ebx, _start - sub eax, ebx - shr eax, 9 # divide by 512 (block size) - mov [dap_start_lba], eax - - lea si, dap - mov ah, 0x42 - int 0x13 - jc rest_of_bootloader_load_failed - -jump_to_second_stage: - lea eax, [stage_2] - jmp eax - -spin: - jmp spin - -# print a string and a newline -# IN -# si: points at zero-terminated String -# CLOBBER -# ax -real_mode_println: - call real_mode_print - mov al, 13 # \r - call real_mode_print_char - mov al, 10 # \n - jmp real_mode_print_char - -# print a string -# IN -# si: points at zero-terminated String -# CLOBBER -# ax -real_mode_print: - cld -real_mode_print_loop: - # note: if direction flag is set (via std) - # this will DECREMENT the ptr, effectively - # reading/printing in reverse. - lodsb al, BYTE PTR [si] - test al, al - jz real_mode_print_done - call real_mode_print_char - jmp real_mode_print_loop -real_mode_print_done: - ret - -# print a character -# IN -# al: character to print -# CLOBBER -# ah -real_mode_print_char: - mov ah, 0x0e - int 0x10 - ret - -# print a number in hex -# IN -# bx: the number -# CLOBBER -# al, cx -real_mode_print_hex: - mov cx, 4 -.lp: - mov al, bh - shr al, 4 - - cmp al, 0xA - jb .below_0xA - - add al, 'A' - 0xA - '0' -.below_0xA: - add al, '0' - - call real_mode_print_char - - shl bx, 4 - loop .lp - - ret - -real_mode_error: - call real_mode_println - jmp spin - -no_int13h_extensions: - lea si, no_int13h_extensions_str - jmp real_mode_error - -rest_of_bootloader_load_failed: - lea si, rest_of_bootloader_load_failed_str - jmp real_mode_error - -boot_start_str: .asciz "Booting (first stage)..." -error_str: .asciz "Error: " -no_int13h_extensions_str: .asciz "No support for int13h extensions" -rest_of_bootloader_load_failed_str: .asciz "Failed to load rest of bootloader" - -gdt32info: - .word gdt32_end - gdt32 - 1 # last byte in table - .word gdt32 # start of table - -gdt32: - # entry 0 is always unused - .quad 0 -codedesc: - .byte 0xff - .byte 0xff - .byte 0 - .byte 0 - .byte 0 - .byte 0x9a - .byte 0xcf - .byte 0 -datadesc: - .byte 0xff - .byte 0xff - .byte 0 - .byte 0 - .byte 0 - .byte 0x92 - .byte 0xcf - .byte 0 -gdt32_end: - -dap: # disk access packet - .byte 0x10 # size of dap - .byte 0 # unused -dap_blocks: - .word 0 # number of sectors -dap_buffer_addr: - .word 0 # offset to memory buffer -dap_buffer_seg: - .word 0 # segment of memory buffer -dap_start_lba: - .quad 0 # start logical block address - -.org 510 -.word 0xaa55 # magic number for bootable disk diff --git a/src/stage_2.s b/src/stage_2.s deleted file mode 100644 index 5486439a..00000000 --- a/src/stage_2.s +++ /dev/null @@ -1,105 +0,0 @@ -.section .boot, "awx" -.intel_syntax noprefix -.code16 - -# This stage sets the target operating mode, loads the kernel from disk, -# creates an e820 memory map, enters protected mode, and jumps to the -# third stage. - -second_stage_start_str: .asciz "Booting (second stage)..." -kernel_load_failed_str: .asciz "Failed to load kernel from disk" - -kernel_load_failed: - lea si, [kernel_load_failed_str] - call real_mode_println -kernel_load_failed_spin: - jmp kernel_load_failed_spin - -stage_2: - lea si, [second_stage_start_str] - call real_mode_println - -set_target_operating_mode: - # Some BIOSs assume the processor will only operate in Legacy Mode. We change the Target - # Operating Mode to "Long Mode Target Only", so the firmware expects each CPU to enter Long Mode - # once and then stay in it. This allows the firmware to enable mode-specifc optimizations. - # We save the flags, because CF is set if the callback is not supported (in which case, this is - # a NOP) - pushf - mov ax, 0xec00 - mov bl, 0x2 - int 0x15 - popf - -load_kernel_from_disk: - # start of memory buffer - lea eax, _kernel_buffer - mov [dap_buffer_addr], ax - - # number of disk blocks to load - mov word ptr [dap_blocks], 1 - - # number of start block - lea eax, _kernel_start_addr - lea ebx, _start - sub eax, ebx - shr eax, 9 # divide by 512 (block size) - mov [dap_start_lba], eax - - # destination address - mov edi, 0x400000 - - # block count - lea ecx, _kernel_size - add ecx, 511 # align up - shr ecx, 9 - -load_next_kernel_block_from_disk: - # load block from disk - lea si, dap - mov ah, 0x42 - int 0x13 - jc kernel_load_failed - - # copy block to 2MiB - push ecx - push esi - mov ecx, 512 / 4 - # move with zero extension - # because we are moving a word ptr - # to esi, a 32-bit register. - movzx esi, word ptr [dap_buffer_addr] - # move from esi to edi ecx times. - rep movsd [edi], [esi] - pop esi - pop ecx - - # next block - mov eax, [dap_start_lba] - add eax, 1 - mov [dap_start_lba], eax - - sub ecx, 1 - jnz load_next_kernel_block_from_disk - -create_memory_map: - lea di, es:[_memory_map] - call do_e820 - -video_mode_config: - call config_video_mode - -enter_protected_mode_again: - cli - lgdt [gdt32info] - mov eax, cr0 - or al, 1 # set protected mode bit - mov cr0, eax - - push 0x8 - lea eax, [stage_3] - push eax - retf - -spin32: - jmp spin32 diff --git a/src/stage_3.s b/src/stage_3.s deleted file mode 100644 index 06d753c3..00000000 --- a/src/stage_3.s +++ /dev/null @@ -1,199 +0,0 @@ -.section .boot, "awx" -.intel_syntax noprefix -.code32 - -# This stage performs some checks on the CPU (cpuid, long mode), sets up an -# initial page table mapping (identity map the bootloader, map the P4 -# recursively, map the kernel blob to 4MB), enables paging, switches to long -# mode, and jumps to stage_4. - -stage_3: - mov bx, 0x10 - mov ds, bx # set data segment - mov es, bx # set extra segment - mov ss, bx # set stack segment - - lea si, boot_third_stage_str - call vga_println - -check_cpu: - call check_cpuid - call check_long_mode - - cli # disable interrupts - - lidt zero_idt # Load a zero length IDT so that any NMI causes a triple fault. - -# enter long mode - -set_up_page_tables: - # zero out buffer for page tables - lea edi, [__page_table_start] - lea ecx, [__page_table_end] - sub ecx, edi - shr ecx, 2 # one stosd zeros 4 bytes -> divide by 4 - xor eax, eax - rep stosd - - # p4 - lea eax, [_p3] - or eax, (1 | 2) - mov [_p4], eax - # p3 - lea eax, [_p2] - or eax, (1 | 2) - mov [_p3], eax - # p2 - lea eax, [_p1] - or eax, (1 | 2) - mov [_p2], eax - mov eax, (0x400000 | 1 | 2 | (1 << 7)) - mov ecx, 2 - lea edx, _kernel_size - add edx, 0x400000 # start address - add edx, 0x200000 - 1 # align up - shr edx, 12 + 9 # end huge page number - map_p2_table: - mov [_p2 + ecx * 8], eax - add eax, 0x200000 - add ecx, 1 - cmp ecx, edx - jb map_p2_table - # p1 - # start mapping from __page_table_start, as we need to be able to access - # the p4 table from rust. stop mapping at __bootloader_end - lea eax, __page_table_start - and eax, 0xfffff000 - or eax, (1 | 2) - lea ecx, __page_table_start - shr ecx, 12 # start page number - lea edx, __bootloader_end - add edx, 4096 - 1 # align up - shr edx, 12 # end page number - map_p1_table: - mov [_p1 + ecx * 8], eax - add eax, 4096 - add ecx, 1 - cmp ecx, edx - jb map_p1_table - map_framebuffer: - call vga_map_frame_buffer - -enable_paging: - # Write back cache and add a memory fence. I'm not sure if this is - # necessary, but better be on the safe side. - wbinvd - mfence - - # load P4 to cr3 register (cpu uses this to access the P4 table) - lea eax, [_p4] - mov cr3, eax - - # enable PAE-flag in cr4 (Physical Address Extension) - mov eax, cr4 - or eax, (1 << 5) - mov cr4, eax - - # set the long mode bit in the EFER MSR (model specific register) - mov ecx, 0xC0000080 - rdmsr - or eax, (1 << 8) - wrmsr - - # enable paging in the cr0 register - mov eax, cr0 - or eax, (1 << 31) - mov cr0, eax - -load_64bit_gdt: - lgdt gdt_64_pointer # Load GDT.Pointer defined below. - -jump_to_long_mode: - push 0x8 - lea eax, [stage_4] - push eax - retf # Load CS with 64 bit segment and flush the instruction cache - -spin_here: - jmp spin_here - -check_cpuid: - # Check if CPUID is supported by attempting to flip the ID bit (bit 21) - # in the FLAGS register. If we can flip it, CPUID is available. - - # Copy FLAGS in to EAX via stack - pushfd - pop eax - - # Copy to ECX as well for comparing later on - mov ecx, eax - - # Flip the ID bit - xor eax, (1 << 21) - - # Copy EAX to FLAGS via the stack - push eax - popfd - - # Copy FLAGS back to EAX (with the flipped bit if CPUID is supported) - pushfd - pop eax - - # Restore FLAGS from the old version stored in ECX (i.e. flipping the - # ID bit back if it was ever flipped). - push ecx - popfd - - # Compare EAX and ECX. If they are equal then that means the bit - # wasn't flipped, and CPUID isn't supported. - cmp eax, ecx - je no_cpuid - ret -no_cpuid: - lea esi, no_cpuid_str - call vga_println -no_cpuid_spin: - hlt - jmp no_cpuid_spin - -check_long_mode: - # test if extended processor info in available - mov eax, 0x80000000 # implicit argument for cpuid - cpuid # get highest supported argument - cmp eax, 0x80000001 # it needs to be at least 0x80000001 - jb no_long_mode # if it's less, the CPU is too old for long mode - - # use extended info to test if long mode is available - mov eax, 0x80000001 # argument for extended processor info - cpuid # returns various feature bits in ecx and edx - test edx, (1 << 29) # test if the LM-bit is set in the D-register - jz no_long_mode # If it's not set, there is no long mode - ret -no_long_mode: - lea esi, no_long_mode_str - call vga_println -no_long_mode_spin: - hlt - jmp no_long_mode_spin - - -.align 4 -zero_idt: - .word 0 - .byte 0 - -gdt_64: - .quad 0x0000000000000000 # Null Descriptor - should be present. - .quad 0x00209A0000000000 # 64-bit code descriptor (exec/read). - .quad 0x0000920000000000 # 64-bit data descriptor (read/write). - -.align 4 - .word 0 # Padding to make the "address of the GDT" field aligned on a 4-byte boundary - -gdt_64_pointer: - .word gdt_64_pointer - gdt_64 - 1 # 16-bit Size (Limit) of GDT. - .long gdt_64 # 32-bit Base Address of GDT. (CPU will zero extend to 64-bit) - -boot_third_stage_str: .asciz "Booting (third stage)..." -no_cpuid_str: .asciz "Error: CPU does not support CPUID" -no_long_mode_str: .asciz "Error: CPU does not support long mode" diff --git a/src/uefi/mod.rs b/src/uefi/mod.rs new file mode 100644 index 00000000..34c63527 --- /dev/null +++ b/src/uefi/mod.rs @@ -0,0 +1,45 @@ +use std::path::Path; + +use bootloader_boot_config::BootConfig; + +use crate::DiskImageBuilder; + +/// Create disk images for booting on UEFI systems. +pub struct UefiBoot { + image_builder: DiskImageBuilder, +} + +impl UefiBoot { + /// Start creating a disk image for the given bootloader ELF executable. + pub fn new(kernel_path: &Path) -> Self { + Self { + image_builder: DiskImageBuilder::new(kernel_path.to_owned()), + } + } + + /// Add a ramdisk file to the image + pub fn set_ramdisk(&mut self, ramdisk_path: &Path) -> &mut Self { + self.image_builder.set_ramdisk(ramdisk_path.to_owned()); + self + } + + /// Creates a configuration file (boot.json) that configures the runtime behavior of the bootloader. + pub fn set_boot_config(&mut self, config: &BootConfig) -> &mut Self { + self.image_builder.set_boot_config(config); + self + } + + /// Create a bootable UEFI disk image at the given path. + pub fn create_disk_image(&self, out_path: &Path) -> anyhow::Result<()> { + self.image_builder.create_uefi_image(out_path) + } + + /// Prepare a folder for use with booting over UEFI_PXE. + /// + /// This places the bootloader executable under the path "bootloader". The + /// DHCP server should set the filename option to that path, otherwise the + /// bootloader won't be found. + pub fn create_pxe_tftp_folder(&self, out_path: &Path) -> anyhow::Result<()> { + self.image_builder.create_uefi_tftp_folder(out_path) + } +} diff --git a/src/video_mode/vga_320x200.s b/src/video_mode/vga_320x200.s deleted file mode 100644 index e052defb..00000000 --- a/src/video_mode/vga_320x200.s +++ /dev/null @@ -1,90 +0,0 @@ -.section .boot, "awx" -.intel_syntax noprefix -.code16 - -config_video_mode: - mov ah, 0 - mov al, 0x13 # 320x200 256 color graphics - int 0x10 - ret - -.code32 - -vga_map_frame_buffer: - mov eax, 0xa0000 - or eax, (1 | 2) -vga_map_frame_buffer_loop: - mov ecx, eax - shr ecx, 12 - mov [_p1 + ecx * 8], eax - - add eax, 4096 - cmp eax, 0xa0000 + 320 * 200 - jl vga_map_frame_buffer_loop - - ret - -# print a string and a newline -# IN -# esi: points at zero-terminated String -vga_println: - push eax - push ebx - push ecx - push edx - - call vga_print - - # newline - mov edx, 0 - mov eax, vga_position - mov ecx, 80 * 2 - div ecx - add eax, 1 - mul ecx - mov vga_position, eax - - pop edx - pop ecx - pop ebx - pop eax - - ret - -# print a string -# IN -# esi: points at zero-terminated String -# CLOBBER -# ah, ebx -vga_print: - cld -vga_print_loop: - # note: if direction flag is set (via std) - # this will DECREMENT the ptr, effectively - # reading/printing in reverse. - lodsb al, BYTE PTR [esi] - test al, al - jz vga_print_done - call vga_print_char - jmp vga_print_loop -vga_print_done: - ret - - -# print a character -# IN -# al: character to print -# CLOBBER -# ah, ebx -vga_print_char: - mov ebx, vga_position - mov ah, 0x0f - mov [ebx + 0xa0000], ax - - add ebx, 2 - mov [vga_position], ebx - - ret - -vga_position: - .double 0 diff --git a/src/video_mode/vga_text_80x25.s b/src/video_mode/vga_text_80x25.s deleted file mode 100644 index 07208768..00000000 --- a/src/video_mode/vga_text_80x25.s +++ /dev/null @@ -1,84 +0,0 @@ -.section .boot, "awx" -.intel_syntax noprefix -.code16 - -config_video_mode: - mov ah, 0 - mov al, 0x03 # 80x25 16 color text - int 0x10 - ret - -.code32 - -vga_map_frame_buffer: - mov eax, 0xb8000 - or eax, (1 | 2) - mov ecx, 0xb8000 - shr ecx, 12 - mov [_p1 + ecx * 8], eax - ret - -# print a string and a newline -# IN -# esi: points at zero-terminated String -vga_println: - push eax - push ebx - push ecx - push edx - - call vga_print - - # newline - mov edx, 0 - mov eax, vga_position - mov ecx, 80 * 2 - div ecx - add eax, 1 - mul ecx - mov vga_position, eax - - pop edx - pop ecx - pop ebx - pop eax - - ret - -# print a string -# IN -# esi: points at zero-terminated String -# CLOBBER -# ah, ebx -vga_print: - cld -vga_print_loop: - # note: if direction flag is set (via std) - # this will DECREMENT the ptr, effectively - # reading/printing in reverse. - lodsb al, BYTE PTR [esi] - test al, al - jz vga_print_done - call vga_print_char - jmp vga_print_loop -vga_print_done: - ret - - -# print a character -# IN -# al: character to print -# CLOBBER -# ah, ebx -vga_print_char: - mov ebx, vga_position - mov ah, 0x0f - mov [ebx + 0xb8000], ax - - add ebx, 2 - mov [vga_position], ebx - - ret - -vga_position: - .double 0 diff --git a/test-kernel/.cargo/config b/test-kernel/.cargo/config deleted file mode 100644 index 92e8659b..00000000 --- a/test-kernel/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "x86_64-test-kernel.json" diff --git a/test-kernel/.gitignore b/test-kernel/.gitignore deleted file mode 100644 index eccd7b4a..00000000 --- a/test-kernel/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target/ -**/*.rs.bk diff --git a/test-kernel/Cargo.lock b/test-kernel/Cargo.lock deleted file mode 100644 index 19970e08..00000000 --- a/test-kernel/Cargo.lock +++ /dev/null @@ -1,69 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "array-init" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "bit_field" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "bitflags" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "nodrop" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "os_bootinfo" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "test-kernel" -version = "0.1.0" -dependencies = [ - "x86_64 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "usize_conversions" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "ux" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "x86_64" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "array-init 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "bit_field 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "os_bootinfo 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "usize_conversions 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ux 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[metadata] -"checksum array-init 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "23589ecb866b460d3a0f1278834750268c607e8e28a1b982c907219f3178cd72" -"checksum bit_field 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed8765909f9009617974ab6b7d332625b320b33c326b1e9321382ef1999b5d56" -"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd" -"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" -"checksum os_bootinfo 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "66481dbeb5e773e7bd85b63cd6042c30786f834338288c5ec4f3742673db360a" -"checksum usize_conversions 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f70329e2cbe45d6c97a5112daad40c34cd9a4e18edb5a2a18fefeb584d8d25e5" -"checksum ux 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "88dfeb711b61ce620c0cb6fd9f8e3e678622f0c971da2a63c4b3e25e88ed012f" -"checksum x86_64 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f9258d7e2dd25008d69e8c9e9ee37865887a5e1e3d06a62f1cb3f6c209e6f177" diff --git a/test-kernel/Cargo.toml b/test-kernel/Cargo.toml deleted file mode 100644 index 85bcf7a9..00000000 --- a/test-kernel/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "test-kernel" -version = "0.1.0" -authors = ["Philipp Oppermann "] -edition = "2018" - -[dependencies] -x86_64 = "0.3.4" diff --git a/test-kernel/src/main.rs b/test-kernel/src/main.rs deleted file mode 100644 index f0352e22..00000000 --- a/test-kernel/src/main.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![no_std] // don't link the Rust standard library -#![no_main] // disable all Rust-level entry points - -use core::panic::PanicInfo; - -/// This function is called on panic. -#[panic_handler] -fn panic(_info: &PanicInfo) -> ! { - loop {} -} - -#[no_mangle] // don't mangle the name of this function -pub extern "C" fn _start() -> ! { - // this function is the entry point, since the linker looks for a function - // named `_start` by default - - // exit QEMU (see https://os.phil-opp.com/integration-tests/#shutting-down-qemu) - unsafe { exit_qemu(); } - - loop {} -} - -pub unsafe fn exit_qemu() { - use x86_64::instructions::port::Port; - - let mut port = Port::::new(0xf4); - port.write(61); // exit code is (61 << 1) | 1 = 123 -} diff --git a/test-kernel/x86_64-test-kernel.json b/test-kernel/x86_64-test-kernel.json deleted file mode 100644 index 9afe809f..00000000 --- a/test-kernel/x86_64-test-kernel.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "llvm-target": "x86_64-unknown-none", - "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", - "arch": "x86_64", - "target-endian": "little", - "target-pointer-width": "64", - "target-c-int-width": "32", - "os": "none", - "executables": true, - "linker-flavor": "ld.lld", - "linker": "rust-lld", - "panic-strategy": "abort", - "disable-redzone": true, - "features": "-mmx,-sse,+soft-float" - } diff --git a/tests/config_file.rs b/tests/config_file.rs new file mode 100644 index 00000000..d939ab78 --- /dev/null +++ b/tests/config_file.rs @@ -0,0 +1,25 @@ +use bootloader_test_runner::run_test_kernel_internal; + +use bootloader::BootConfig; + +#[test] +fn default_config() { + run_test_kernel_internal( + env!("CARGO_BIN_FILE_TEST_KERNEL_CONFIG_FILE_no_config"), + None, + None, + ); +} + +#[test] +fn custom_boot_config() { + let mut config = BootConfig::default(); + config.frame_buffer_logging = false; + config.serial_logging = true; + config._test_sentinel = 0xb001b001b001; + run_test_kernel_internal( + env!("CARGO_BIN_FILE_TEST_KERNEL_CONFIG_FILE_custom_config"), + None, + Some(&config), + ); +} diff --git a/tests/default_settings.rs b/tests/default_settings.rs new file mode 100644 index 00000000..d610508c --- /dev/null +++ b/tests/default_settings.rs @@ -0,0 +1,22 @@ +use bootloader_test_runner::run_test_kernel; + +#[test] +fn basic_boot() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_DEFAULT_SETTINGS_basic_boot" + )); +} + +#[test] +fn should_panic() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_DEFAULT_SETTINGS_should_panic" + )); +} + +#[test] +fn check_boot_info() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_DEFAULT_SETTINGS_check_boot_info" + )); +} diff --git a/tests/fixed_kernel_address.rs b/tests/fixed_kernel_address.rs new file mode 100644 index 00000000..25e15d67 --- /dev/null +++ b/tests/fixed_kernel_address.rs @@ -0,0 +1,29 @@ +use bootloader_test_runner::run_test_kernel; + +#[test] +fn basic_boot() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_FIXED_KERNEL_ADDRESS_basic_boot" + )); +} + +#[test] +fn should_panic() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_FIXED_KERNEL_ADDRESS_should_panic" + )); +} + +#[test] +fn check_boot_info() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_FIXED_KERNEL_ADDRESS_check_boot_info" + )); +} + +#[test] +fn verify_kernel_address() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_FIXED_KERNEL_ADDRESS_verify_kernel_address" + )); +} diff --git a/tests/higher_half.rs b/tests/higher_half.rs new file mode 100644 index 00000000..c2b9ac91 --- /dev/null +++ b/tests/higher_half.rs @@ -0,0 +1,25 @@ +use bootloader_test_runner::run_test_kernel; + +#[test] +fn basic_boot() { + run_test_kernel(env!("CARGO_BIN_FILE_TEST_KERNEL_HIGHER_HALF_basic_boot")); +} + +#[test] +fn should_panic() { + run_test_kernel(env!("CARGO_BIN_FILE_TEST_KERNEL_HIGHER_HALF_should_panic")); +} + +#[test] +fn check_boot_info() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_HIGHER_HALF_check_boot_info" + )); +} + +#[test] +fn verify_higher_half() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_HIGHER_HALF_verify_higher_half" + )); +} diff --git a/tests/lower_memory_free.rs b/tests/lower_memory_free.rs new file mode 100644 index 00000000..aedaf061 --- /dev/null +++ b/tests/lower_memory_free.rs @@ -0,0 +1,7 @@ +use bootloader_test_runner::run_test_kernel; +#[test] +fn lower_memory_free() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_LOWER_MEMORY_FREE_lower_memory_free" + )); +} diff --git a/tests/lto.rs b/tests/lto.rs new file mode 100644 index 00000000..5c259236 --- /dev/null +++ b/tests/lto.rs @@ -0,0 +1,25 @@ +use std::{path::Path, process::Command}; + +use bootloader_test_runner::run_test_kernel; + +#[test] +fn basic_boot() { + // build test kernel manually to force-enable link-time optimization + let mut cmd = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into())); + cmd.current_dir("tests/test_kernels"); + cmd.arg("build"); + cmd.arg("-p").arg("test_kernel_lto"); + cmd.arg("--profile").arg("lto"); + let status = cmd.status().unwrap(); + assert!(status.success()); + + let root = env!("CARGO_MANIFEST_DIR"); + let kernel_path = Path::new(root) + .join("target") + .join("x86_64-unknown-none") + .join("lto") + .join("basic_boot"); + assert!(kernel_path.exists()); + + run_test_kernel(kernel_path.as_path().to_str().unwrap()); +} diff --git a/tests/map_phys_mem.rs b/tests/map_phys_mem.rs new file mode 100644 index 00000000..b19ba987 --- /dev/null +++ b/tests/map_phys_mem.rs @@ -0,0 +1,15 @@ +use bootloader_test_runner::run_test_kernel; + +#[test] +fn check_boot_info() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_MAP_PHYS_MEM_check_boot_info" + )); +} + +#[test] +fn access_phys_mem() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_MAP_PHYS_MEM_access_phys_mem" + )); +} diff --git a/tests/min_stack.rs b/tests/min_stack.rs new file mode 100644 index 00000000..d320a6cb --- /dev/null +++ b/tests/min_stack.rs @@ -0,0 +1,6 @@ +use bootloader_test_runner::run_test_kernel; + +#[test] +fn basic_boot() { + run_test_kernel(env!("CARGO_BIN_FILE_TEST_KERNEL_MIN_STACK_basic_boot")); +} diff --git a/tests/pie.rs b/tests/pie.rs new file mode 100644 index 00000000..c2d30d80 --- /dev/null +++ b/tests/pie.rs @@ -0,0 +1,21 @@ +use bootloader_test_runner::run_test_kernel; + +#[test] +fn basic_boot() { + run_test_kernel(env!("CARGO_BIN_FILE_TEST_KERNEL_PIE_basic_boot")); +} + +#[test] +fn should_panic() { + run_test_kernel(env!("CARGO_BIN_FILE_TEST_KERNEL_PIE_should_panic")); +} + +#[test] +fn check_boot_info() { + run_test_kernel(env!("CARGO_BIN_FILE_TEST_KERNEL_PIE_check_boot_info")); +} + +#[test] +fn global_variable() { + run_test_kernel(env!("CARGO_BIN_FILE_TEST_KERNEL_PIE_global_variable")); +} diff --git a/tests/ramdisk.rs b/tests/ramdisk.rs new file mode 100644 index 00000000..08c86f9b --- /dev/null +++ b/tests/ramdisk.rs @@ -0,0 +1,28 @@ +use std::path::Path; + +use bootloader_test_runner::run_test_kernel_with_ramdisk; +static RAMDISK_PATH: &str = "tests/ramdisk.txt"; + +#[test] +fn basic_boot() { + run_test_kernel_with_ramdisk( + env!("CARGO_BIN_FILE_TEST_KERNEL_RAMDISK_basic_boot"), + Some(Path::new(RAMDISK_PATH)), + ); +} + +#[test] +fn check_ramdisk() { + run_test_kernel_with_ramdisk( + env!("CARGO_BIN_FILE_TEST_KERNEL_RAMDISK_ramdisk"), + Some(Path::new(RAMDISK_PATH)), + ); +} + +#[test] +fn memory_map() { + run_test_kernel_with_ramdisk( + env!("CARGO_BIN_FILE_TEST_KERNEL_RAMDISK_memory_map"), + Some(Path::new(RAMDISK_PATH)), + ); +} diff --git a/tests/ramdisk.txt b/tests/ramdisk.txt new file mode 100644 index 00000000..09e6dbc1 --- /dev/null +++ b/tests/ramdisk.txt @@ -0,0 +1 @@ +Test ramdisk. \ No newline at end of file diff --git a/tests/runner/Cargo.toml b/tests/runner/Cargo.toml new file mode 100644 index 00000000..796ffb01 --- /dev/null +++ b/tests/runner/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "bootloader_test_runner" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["bios", "uefi"] +bios = ["bootloader/bios"] +uefi = ["bootloader/uefi", "dep:ovmf-prebuilt"] + +[dependencies] +bootloader = { path = "../..", default-features = false } +strip-ansi-escapes = "0.1.1" +ovmf-prebuilt = { version = "0.1.0-alpha.1", optional = true } diff --git a/tests/runner/src/lib.rs b/tests/runner/src/lib.rs new file mode 100644 index 00000000..3edb4d94 --- /dev/null +++ b/tests/runner/src/lib.rs @@ -0,0 +1,150 @@ +use bootloader::BootConfig; +use bootloader::DiskImageBuilder; +use std::path::Path; + +pub fn run_test_kernel(kernel_binary_path: &str) { + run_test_kernel_internal(kernel_binary_path, None, None) +} +pub fn run_test_kernel_with_ramdisk(kernel_binary_path: &str, ramdisk_path: Option<&Path>) { + run_test_kernel_internal(kernel_binary_path, ramdisk_path, None) +} +pub fn run_test_kernel_with_config_file( + kernel_binary_path: &str, + config_file: Option<&BootConfig>, +) { + run_test_kernel_internal(kernel_binary_path, None, config_file) +} + +pub fn run_test_kernel_internal( + kernel_binary_path: &str, + ramdisk_path: Option<&Path>, + config_file_path: Option<&BootConfig>, +) { + let kernel_path = Path::new(kernel_binary_path); + let mut image_builder = DiskImageBuilder::new(kernel_path.to_owned()); + if let Some(rdp) = ramdisk_path { + image_builder.set_ramdisk(rdp.to_owned()); + } + if let Some(cfp) = config_file_path { + image_builder.set_boot_config(cfp); + } + + #[cfg(feature = "uefi")] + { + let gpt_path = kernel_path.with_extension("gpt"); + let tftp_path = kernel_path.with_extension("tftp"); + image_builder.create_uefi_image(&gpt_path).unwrap(); + image_builder.create_uefi_tftp_folder(&tftp_path).unwrap(); + run_test_kernel_on_uefi(&gpt_path); + run_test_kernel_on_uefi_pxe(&tftp_path); + } + + #[cfg(feature = "bios")] + { + // create an MBR disk image for legacy BIOS booting + let mbr_path = kernel_path.with_extension("mbr"); + image_builder.create_bios_image(mbr_path.as_path()).unwrap(); + + run_test_kernel_on_bios(&mbr_path); + } +} + +#[cfg(feature = "uefi")] +pub fn run_test_kernel_on_uefi(out_gpt_path: &Path) { + let ovmf_pure_efi = ovmf_prebuilt::ovmf_pure_efi(); + let args = [ + "-bios", + ovmf_pure_efi.to_str().unwrap(), + "-drive", + &format!("format=raw,file={}", out_gpt_path.display()), + ]; + run_qemu(args); +} + +#[cfg(feature = "bios")] +pub fn run_test_kernel_on_bios(out_mbr_path: &Path) { + let args = [ + "-drive", + &(format!("format=raw,file={}", out_mbr_path.display())), + ]; + run_qemu(args); +} + +#[cfg(feature = "uefi")] +pub fn run_test_kernel_on_uefi_pxe(out_tftp_path: &Path) { + let ovmf_pure_efi = ovmf_prebuilt::ovmf_pure_efi(); + let args = [ + "-netdev", + &format!( + "user,id=net0,net=192.168.17.0/24,tftp={},bootfile=bootloader,id=net0", + out_tftp_path.display() + ), + "-device", + "virtio-net-pci,netdev=net0", + "-bios", + ovmf_pure_efi.to_str().unwrap(), + ]; + run_qemu(args); +} + +#[cfg(any(feature = "uefi", feature = "bios"))] +fn run_qemu<'a, A>(args: A) +where + A: IntoIterator, +{ + use std::{ + io::Read, + process::{Command, Stdio}, + }; + + const QEMU_ARGS: &[&str] = &[ + "-device", + "isa-debug-exit,iobase=0xf4,iosize=0x04", + "-serial", + "stdio", + "-display", + "none", + "--no-reboot", + ]; + + const SEPARATOR: &str = "\n____________________________________\n"; + + let mut run_cmd = Command::new("qemu-system-x86_64"); + run_cmd.args(args); + run_cmd.args(QEMU_ARGS); + let run_cmd_str = format!("{run_cmd:?}"); + + run_cmd.stdout(Stdio::piped()); + run_cmd.stderr(Stdio::piped()); + run_cmd.stdin(Stdio::null()); + + let mut child = run_cmd.spawn().unwrap(); + + let child_stdout = child.stdout.take().unwrap(); + let mut child_stderr = child.stderr.take().unwrap(); + + let copy_stdout = std::thread::spawn(move || { + let print_cmd = format!("\nRunning {run_cmd_str}\n\n").into_bytes(); + let mut output = print_cmd.chain(child_stdout).chain(SEPARATOR.as_bytes()); + std::io::copy( + &mut output, + &mut strip_ansi_escapes::Writer::new(std::io::stdout()), + ) + }); + let copy_stderr = std::thread::spawn(move || { + std::io::copy( + &mut child_stderr, + &mut strip_ansi_escapes::Writer::new(std::io::stderr()), + ) + }); + + let exit_status = child.wait().unwrap(); + match exit_status.code() { + Some(33) => {} // success + Some(35) => panic!("Test failed"), // success + other => panic!("Test failed with unexpected exit code `{other:?}`"), + } + + copy_stdout.join().unwrap().unwrap(); + copy_stderr.join().unwrap().unwrap(); +} diff --git a/tests/test_kernels/.cargo/config.toml b/tests/test_kernels/.cargo/config.toml new file mode 100644 index 00000000..412c4a33 --- /dev/null +++ b/tests/test_kernels/.cargo/config.toml @@ -0,0 +1,3 @@ +[build] +target-dir = "../../target" +target = "x86_64-unknown-none" diff --git a/tests/test_kernels/Cargo.lock b/tests/test_kernels/Cargo.lock new file mode 100644 index 00000000..110fc16d --- /dev/null +++ b/tests/test_kernels/Cargo.lock @@ -0,0 +1,162 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "bootloader_api" +version = "0.11.10" + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "test_kernel_default_settings" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_fixed_kernel_address" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_higher_half" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_lower_memory_free" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_lto" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_map_phys_mem" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_min_stack" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_pie" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_ramdisk" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550", + "x86_64 0.15.2", +] + +[[package]] +name = "test_kernel_write_usable_memory" +version = "0.1.0" +dependencies = [ + "bootloader_api", + "uart_16550", + "x86_64 0.15.2", +] + +[[package]] +name = "uart_16550" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614ff2a87880d4bd4374722268598a970bbad05ced8bf630439417347254ab2e" +dependencies = [ + "bitflags 1.3.2", + "rustversion", + "x86_64 0.14.13", +] + +[[package]] +name = "volatile" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442887c63f2c839b346c192d047a7c87e73d0689c9157b00b53dcc27dd5ea793" + +[[package]] +name = "x86_64" +version = "0.14.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c101112411baafbb4bf8d33e4c4a80ab5b02d74d2612331c61e8192fc9710491" +dependencies = [ + "bit_field", + "bitflags 2.8.0", + "rustversion", + "volatile", +] + +[[package]] +name = "x86_64" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f042214de98141e9c8706e8192b73f56494087cc55ebec28ce10f26c5c364ae" +dependencies = [ + "bit_field", + "bitflags 2.8.0", + "rustversion", + "volatile", +] diff --git a/tests/test_kernels/Cargo.toml b/tests/test_kernels/Cargo.toml new file mode 100644 index 00000000..80daa869 --- /dev/null +++ b/tests/test_kernels/Cargo.toml @@ -0,0 +1,39 @@ +cargo-features = ["profile-rustflags"] + +[workspace] +resolver = "2" +members = [ + "default_settings", + "map_phys_mem", + "higher_half", + "pie", + "lto", + "ramdisk", + "min_stack", + "lower_memory_free", + "write_usable_memory", + "fixed_kernel_address", +] + +[profile.release] +panic = "abort" +lto = false +debug = true +overflow-checks = true + +[profile.lto] +inherits = "release" +lto = true + +[profile.test.package.test_kernel_higher_half] +rustflags = [ + "-C", + "link-args=--image-base 0xFFFF800000000000", + "-C", + "relocation-model=pic", + "-C", + "code-model=large", +] + +[profile.test.package.test_kernel_min_stack] +opt-level = 2 diff --git a/tests/test_kernels/config_file/Cargo.toml b/tests/test_kernels/config_file/Cargo.toml new file mode 100644 index 00000000..86a90cf7 --- /dev/null +++ b/tests/test_kernels/config_file/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test_kernel_config_file" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2021" + +[dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" diff --git a/tests/test_kernels/config_file/src/bin/custom_config.rs b/tests/test_kernels/config_file/src/bin/custom_config.rs new file mode 100644 index 00000000..425e268a --- /dev/null +++ b/tests/test_kernels/config_file/src/bin/custom_config.rs @@ -0,0 +1,22 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use core::fmt::Write; +use test_kernel_config_file::{exit_qemu, serial, QemuExitCode}; + +entry_point!(kernel_main); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + writeln!(serial(), "Entered kernel with boot info: {boot_info:?}").unwrap(); + assert_eq!(boot_info._test_sentinel, 0xb001b001b001); + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[panic_handler] +#[cfg(not(test))] +fn panic(info: &core::panic::PanicInfo) -> ! { + let _ = writeln!(serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/config_file/src/bin/no_config.rs b/tests/test_kernels/config_file/src/bin/no_config.rs new file mode 100644 index 00000000..d0898bde --- /dev/null +++ b/tests/test_kernels/config_file/src/bin/no_config.rs @@ -0,0 +1,22 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use core::fmt::Write; +use test_kernel_config_file::{exit_qemu, serial, QemuExitCode}; + +entry_point!(kernel_main); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + writeln!(serial(), "Entered kernel with boot info: {boot_info:?}").unwrap(); + assert_eq!(boot_info._test_sentinel, 0); + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[panic_handler] +#[cfg(not(test))] +fn panic(info: &core::panic::PanicInfo) -> ! { + let _ = writeln!(serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/config_file/src/lib.rs b/tests/test_kernels/config_file/src/lib.rs new file mode 100644 index 00000000..4e46fdb6 --- /dev/null +++ b/tests/test_kernels/config_file/src/lib.rs @@ -0,0 +1,27 @@ +#![no_std] + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/test_kernels/default_settings/Cargo.toml b/tests/test_kernels/default_settings/Cargo.toml new file mode 100644 index 00000000..8d643cdd --- /dev/null +++ b/tests/test_kernels/default_settings/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test_kernel_default_settings" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2021" + +[dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" diff --git a/tests/test_kernels/default_settings/src/bin/basic_boot.rs b/tests/test_kernels/default_settings/src/bin/basic_boot.rs new file mode 100644 index 00000000..e2b7cc0b --- /dev/null +++ b/tests/test_kernels/default_settings/src/bin/basic_boot.rs @@ -0,0 +1,21 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use core::fmt::Write; +use test_kernel_default_settings::{exit_qemu, serial, QemuExitCode}; + +entry_point!(kernel_main); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + writeln!(serial(), "Entered kernel with boot info: {boot_info:?}").unwrap(); + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[panic_handler] +#[cfg(not(test))] +fn panic(info: &core::panic::PanicInfo) -> ! { + let _ = writeln!(serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/default_settings/src/bin/check_boot_info.rs b/tests/test_kernels/default_settings/src/bin/check_boot_info.rs new file mode 100644 index 00000000..bc8fbad7 --- /dev/null +++ b/tests/test_kernels/default_settings/src/bin/check_boot_info.rs @@ -0,0 +1,50 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, info::PixelFormat, BootInfo}; +use test_kernel_default_settings::{exit_qemu, QemuExitCode}; + +entry_point!(kernel_main); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + // check memory regions + assert!(boot_info.memory_regions.len() > 4); + + // check framebuffer + let framebuffer = boot_info.framebuffer.as_ref().unwrap(); + assert_eq!(framebuffer.info().byte_len, framebuffer.buffer().len()); + if ![3, 4].contains(&framebuffer.info().bytes_per_pixel) { + panic!( + "unexpected bytes_per_pixel `{}`", + framebuffer.info().bytes_per_pixel + ); + } + assert_eq!(framebuffer.info().pixel_format, PixelFormat::Bgr); + assert_eq!( + framebuffer.buffer().len(), + framebuffer.info().stride * framebuffer.info().height * framebuffer.info().bytes_per_pixel + ); + + // check defaults for optional features + assert_eq!(boot_info.physical_memory_offset.into_option(), None); + assert_eq!(boot_info.recursive_index.into_option(), None); + + // check rsdp_addr + let rsdp = boot_info.rsdp_addr.into_option().unwrap(); + assert!(rsdp > 0x000E0000); + + // the test kernel has no TLS template + assert_eq!(boot_info.tls_template.into_option(), None); + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_default_settings::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/default_settings/src/bin/should_panic.rs b/tests/test_kernels/default_settings/src/bin/should_panic.rs new file mode 100644 index 00000000..0f78d0ba --- /dev/null +++ b/tests/test_kernels/default_settings/src/bin/should_panic.rs @@ -0,0 +1,18 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; + +entry_point!(kernel_main); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + panic!(); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + use test_kernel_default_settings::{exit_qemu, QemuExitCode}; + exit_qemu(QemuExitCode::Success); +} diff --git a/tests/test_kernels/default_settings/src/lib.rs b/tests/test_kernels/default_settings/src/lib.rs new file mode 100644 index 00000000..4e46fdb6 --- /dev/null +++ b/tests/test_kernels/default_settings/src/lib.rs @@ -0,0 +1,27 @@ +#![no_std] + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/test_kernels/fixed_kernel_address/Cargo.toml b/tests/test_kernels/fixed_kernel_address/Cargo.toml new file mode 100644 index 00000000..8a402dca --- /dev/null +++ b/tests/test_kernels/fixed_kernel_address/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "test_kernel_fixed_kernel_address" +version = "0.1.0" +edition = "2021" + +[target.'cfg(target_arch = "x86_64")'.dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" diff --git a/tests/test_kernels/fixed_kernel_address/src/bin/basic_boot.rs b/tests/test_kernels/fixed_kernel_address/src/bin/basic_boot.rs new file mode 100644 index 00000000..6d8f696c --- /dev/null +++ b/tests/test_kernels/fixed_kernel_address/src/bin/basic_boot.rs @@ -0,0 +1,18 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_fixed_kernel_address::{exit_qemu, QemuExitCode, BOOTLOADER_CONFIG}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/fixed_kernel_address/src/bin/check_boot_info.rs b/tests/test_kernels/fixed_kernel_address/src/bin/check_boot_info.rs new file mode 100644 index 00000000..24267c7a --- /dev/null +++ b/tests/test_kernels/fixed_kernel_address/src/bin/check_boot_info.rs @@ -0,0 +1,23 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_fixed_kernel_address::{exit_qemu, QemuExitCode, BOOTLOADER_CONFIG, KERNEL_ADDR}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + assert_eq!(boot_info.kernel_image_offset, KERNEL_ADDR); + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_fixed_kernel_address::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/fixed_kernel_address/src/bin/should_panic.rs b/tests/test_kernels/fixed_kernel_address/src/bin/should_panic.rs new file mode 100644 index 00000000..a005c051 --- /dev/null +++ b/tests/test_kernels/fixed_kernel_address/src/bin/should_panic.rs @@ -0,0 +1,20 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_fixed_kernel_address::BOOTLOADER_CONFIG; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + panic!(); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + use test_kernel_fixed_kernel_address::{exit_qemu, QemuExitCode}; + + exit_qemu(QemuExitCode::Success); +} diff --git a/tests/test_kernels/fixed_kernel_address/src/bin/verify_kernel_address.rs b/tests/test_kernels/fixed_kernel_address/src/bin/verify_kernel_address.rs new file mode 100644 index 00000000..13944bd1 --- /dev/null +++ b/tests/test_kernels/fixed_kernel_address/src/bin/verify_kernel_address.rs @@ -0,0 +1,29 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_fixed_kernel_address::{exit_qemu, QemuExitCode, BOOTLOADER_CONFIG, KERNEL_ADDR}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + // verify that kernel is loaded at the specified base address. + let rip = x86_64::registers::read_rip().as_u64(); + let kernel_start = KERNEL_ADDR; + let kernel_end = kernel_start + boot_info.kernel_len; + let kernel_range = kernel_start..kernel_end; + + assert!(kernel_range.contains(&rip)); + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_fixed_kernel_address::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/fixed_kernel_address/src/lib.rs b/tests/test_kernels/fixed_kernel_address/src/lib.rs new file mode 100644 index 00000000..1231418a --- /dev/null +++ b/tests/test_kernels/fixed_kernel_address/src/lib.rs @@ -0,0 +1,37 @@ +#![no_std] + +use bootloader_api::{config::Mapping, BootloaderConfig}; + +pub const KERNEL_ADDR: u64 = 0x1987_6543_0000; + +pub const BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.mappings.kernel_base = Mapping::FixedAddress(KERNEL_ADDR); + config +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/test_kernels/higher_half/Cargo.toml b/tests/test_kernels/higher_half/Cargo.toml new file mode 100644 index 00000000..b7d7f2c7 --- /dev/null +++ b/tests/test_kernels/higher_half/Cargo.toml @@ -0,0 +1,16 @@ +cargo-features = ["profile-rustflags"] + +[package] +name = "test_kernel_higher_half" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2021" + +[dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" + +# set to higher half through profile.test.rustflags key in top-level Cargo.toml diff --git a/tests/test_kernels/higher_half/src/bin/basic_boot.rs b/tests/test_kernels/higher_half/src/bin/basic_boot.rs new file mode 100644 index 00000000..4133963e --- /dev/null +++ b/tests/test_kernels/higher_half/src/bin/basic_boot.rs @@ -0,0 +1,18 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_higher_half::{exit_qemu, QemuExitCode, BOOTLOADER_CONFIG}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/higher_half/src/bin/check_boot_info.rs b/tests/test_kernels/higher_half/src/bin/check_boot_info.rs new file mode 100644 index 00000000..3cc96c3c --- /dev/null +++ b/tests/test_kernels/higher_half/src/bin/check_boot_info.rs @@ -0,0 +1,50 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, info::PixelFormat, BootInfo}; +use test_kernel_higher_half::{exit_qemu, QemuExitCode, BOOTLOADER_CONFIG}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + // check memory regions + assert!(boot_info.memory_regions.len() > 4); + + // check framebuffer + let framebuffer = boot_info.framebuffer.as_ref().unwrap(); + assert_eq!(framebuffer.info().byte_len, framebuffer.buffer().len()); + if ![3, 4].contains(&framebuffer.info().bytes_per_pixel) { + panic!( + "unexpected bytes_per_pixel `{}`", + framebuffer.info().bytes_per_pixel + ); + } + assert_eq!(framebuffer.info().pixel_format, PixelFormat::Bgr); + assert_eq!( + framebuffer.buffer().len(), + framebuffer.info().stride * framebuffer.info().height * framebuffer.info().bytes_per_pixel + ); + + // check defaults for optional features + assert_eq!(boot_info.physical_memory_offset.into_option(), None); + assert_eq!(boot_info.recursive_index.into_option(), None); + + // check rsdp_addr + let rsdp = boot_info.rsdp_addr.into_option().unwrap(); + assert!(rsdp > 0x000E0000); + + // the test kernel has no TLS template + assert_eq!(boot_info.tls_template.into_option(), None); + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_higher_half::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/higher_half/src/bin/should_panic.rs b/tests/test_kernels/higher_half/src/bin/should_panic.rs new file mode 100644 index 00000000..44287806 --- /dev/null +++ b/tests/test_kernels/higher_half/src/bin/should_panic.rs @@ -0,0 +1,20 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_higher_half::BOOTLOADER_CONFIG; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + panic!(); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + use test_kernel_higher_half::{exit_qemu, QemuExitCode}; + + exit_qemu(QemuExitCode::Success); +} diff --git a/tests/test_kernels/higher_half/src/bin/verify_higher_half.rs b/tests/test_kernels/higher_half/src/bin/verify_higher_half.rs new file mode 100644 index 00000000..79a1e964 --- /dev/null +++ b/tests/test_kernels/higher_half/src/bin/verify_higher_half.rs @@ -0,0 +1,39 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_higher_half::{exit_qemu, QemuExitCode, BOOTLOADER_CONFIG}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + // verify that kernel is really running in the higher half of the address space + // (set in `x86_64-higher_half.json` custom target) + let rip = x86_64::registers::read_rip().as_u64(); + assert_eq!(rip & 0xffffffffffff0000, 0xffff800000000000); + + // verify that the boot info is located in the higher half of the address space + assert_eq!( + (boot_info as *const _ as usize) & 0xffff800000000000, + 0xffff800000000000 + ); + + // verify that the stack is located in the higher half of the address space. + let stack_addr = &rip; + assert_eq!( + (stack_addr as *const _ as usize) & 0xffff800000000000, + 0xffff800000000000 + ); + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_higher_half::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/higher_half/src/lib.rs b/tests/test_kernels/higher_half/src/lib.rs new file mode 100644 index 00000000..99ae71dc --- /dev/null +++ b/tests/test_kernels/higher_half/src/lib.rs @@ -0,0 +1,35 @@ +#![no_std] + +use bootloader_api::BootloaderConfig; + +pub const BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.mappings.dynamic_range_start = Some(0xffff_8000_0000_0000); + config +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/test_kernels/lower_memory_free/Cargo.toml b/tests/test_kernels/lower_memory_free/Cargo.toml new file mode 100644 index 00000000..be0249c7 --- /dev/null +++ b/tests/test_kernels/lower_memory_free/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "test_kernel_lower_memory_free" +version = "0.1.0" +edition = "2021" + +[dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" diff --git a/tests/test_kernels/lower_memory_free/src/bin/lower_memory_free.rs b/tests/test_kernels/lower_memory_free/src/bin/lower_memory_free.rs new file mode 100644 index 00000000..9620d1ca --- /dev/null +++ b/tests/test_kernels/lower_memory_free/src/bin/lower_memory_free.rs @@ -0,0 +1,53 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{ + config::Mapping, entry_point, info::MemoryRegionKind, BootInfo, BootloaderConfig, +}; +use test_kernel_lower_memory_free::{exit_qemu, QemuExitCode}; + +const LOWER_MEMORY_END_PAGE: u64 = 0x0010_0000; + +pub const BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.mappings.physical_memory = Some(Mapping::FixedAddress(0x0000_4000_0000_0000)); + config +}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + use core::fmt::Write; + use test_kernel_lower_memory_free::serial; + + let mut count = 0; + for region in boot_info.memory_regions.iter() { + writeln!( + serial(), + "Region: {:016x}-{:016x} - {:?}", + region.start, + region.end, + region.kind + ) + .unwrap(); + if region.kind == MemoryRegionKind::Usable && region.start < LOWER_MEMORY_END_PAGE { + let end = core::cmp::min(region.end, LOWER_MEMORY_END_PAGE); + let pages = (end - region.start) / 4096; + count += pages; + } + } + + writeln!(serial(), "Free lower memory page count: {}", count).unwrap(); + assert!(count > 0x10); // 0x10 chosen arbitrarily, we need _some_ free conventional memory, but not all of it. Some, especially on BIOS, may be reserved for hardware. + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[panic_handler] +#[cfg(not(test))] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_lower_memory_free::serial(), "PANIC: {}", info); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/lower_memory_free/src/lib.rs b/tests/test_kernels/lower_memory_free/src/lib.rs new file mode 100644 index 00000000..4e46fdb6 --- /dev/null +++ b/tests/test_kernels/lower_memory_free/src/lib.rs @@ -0,0 +1,27 @@ +#![no_std] + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/test_kernels/lto/Cargo.toml b/tests/test_kernels/lto/Cargo.toml new file mode 100644 index 00000000..0f8e4e3c --- /dev/null +++ b/tests/test_kernels/lto/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test_kernel_lto" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2018" + +[dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" diff --git a/tests/test_kernels/lto/src/bin/basic_boot.rs b/tests/test_kernels/lto/src/bin/basic_boot.rs new file mode 100644 index 00000000..bce7bf23 --- /dev/null +++ b/tests/test_kernels/lto/src/bin/basic_boot.rs @@ -0,0 +1,21 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_lto::{exit_qemu, QemuExitCode}; + +entry_point!(kernel_main); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_lto::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/lto/src/lib.rs b/tests/test_kernels/lto/src/lib.rs new file mode 100644 index 00000000..4e46fdb6 --- /dev/null +++ b/tests/test_kernels/lto/src/lib.rs @@ -0,0 +1,27 @@ +#![no_std] + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/test_kernels/map_phys_mem/Cargo.toml b/tests/test_kernels/map_phys_mem/Cargo.toml new file mode 100644 index 00000000..5dcb5b16 --- /dev/null +++ b/tests/test_kernels/map_phys_mem/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test_kernel_map_phys_mem" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2021" + +[target.'cfg(target_arch = "x86_64")'.dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" diff --git a/tests/test_kernels/map_phys_mem/src/bin/access_phys_mem.rs b/tests/test_kernels/map_phys_mem/src/bin/access_phys_mem.rs new file mode 100644 index 00000000..538362fb --- /dev/null +++ b/tests/test_kernels/map_phys_mem/src/bin/access_phys_mem.rs @@ -0,0 +1,27 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_map_phys_mem::{exit_qemu, QemuExitCode, BOOTLOADER_CONFIG}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + let phys_mem_offset = boot_info.physical_memory_offset.into_option().unwrap(); + + let ptr = phys_mem_offset as *const u64; + let _ = unsafe { *ptr }; + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + use test_kernel_map_phys_mem::serial; + + let _ = writeln!(serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/map_phys_mem/src/bin/check_boot_info.rs b/tests/test_kernels/map_phys_mem/src/bin/check_boot_info.rs new file mode 100644 index 00000000..5b3a8610 --- /dev/null +++ b/tests/test_kernels/map_phys_mem/src/bin/check_boot_info.rs @@ -0,0 +1,54 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, info::PixelFormat, BootInfo}; +use test_kernel_map_phys_mem::{exit_qemu, QemuExitCode, BOOTLOADER_CONFIG}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + // check memory regions + assert!(boot_info.memory_regions.len() > 4); + + // check framebuffer + let framebuffer = boot_info.framebuffer.as_ref().unwrap(); + assert_eq!(framebuffer.info().byte_len, framebuffer.buffer().len()); + if ![3, 4].contains(&framebuffer.info().bytes_per_pixel) { + panic!( + "unexpected bytes_per_pixel `{}`", + framebuffer.info().bytes_per_pixel + ); + } + assert_eq!(framebuffer.info().pixel_format, PixelFormat::Bgr); + assert_eq!( + framebuffer.buffer().len(), + framebuffer.info().stride * framebuffer.info().height * framebuffer.info().bytes_per_pixel + ); + + // check defaults for optional features + assert_eq!( + boot_info.physical_memory_offset.into_option(), + Some(0x0000_4000_0000_0000), + ); + assert_eq!(boot_info.recursive_index.into_option(), None); + + // check rsdp_addr + let rsdp = boot_info.rsdp_addr.into_option().unwrap(); + assert!(rsdp > 0x000E0000); + + // the test kernel has no TLS template + assert_eq!(boot_info.tls_template.into_option(), None); + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + use test_kernel_map_phys_mem::serial; + + let _ = writeln!(serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/map_phys_mem/src/lib.rs b/tests/test_kernels/map_phys_mem/src/lib.rs new file mode 100644 index 00000000..cfd76ad5 --- /dev/null +++ b/tests/test_kernels/map_phys_mem/src/lib.rs @@ -0,0 +1,35 @@ +#![no_std] + +use bootloader_api::{config::Mapping, BootloaderConfig}; + +pub const BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.mappings.physical_memory = Some(Mapping::FixedAddress(0x0000_4000_0000_0000)); + config +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/test_kernels/min_stack/Cargo.toml b/tests/test_kernels/min_stack/Cargo.toml new file mode 100644 index 00000000..82aae6d3 --- /dev/null +++ b/tests/test_kernels/min_stack/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test_kernel_min_stack" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2021" + +[dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" diff --git a/tests/test_kernels/min_stack/src/bin/basic_boot.rs b/tests/test_kernels/min_stack/src/bin/basic_boot.rs new file mode 100644 index 00000000..29a6602d --- /dev/null +++ b/tests/test_kernels/min_stack/src/bin/basic_boot.rs @@ -0,0 +1,26 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo, BootloaderConfig}; +use core::fmt::Write; +use test_kernel_min_stack::{exit_qemu, serial, QemuExitCode}; + +const BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.kernel_stack_size = 3000; + config +}; +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + writeln!(serial(), "Entered kernel with boot info: {boot_info:?}").unwrap(); + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[panic_handler] +#[cfg(not(test))] +fn panic(info: &core::panic::PanicInfo) -> ! { + let _ = writeln!(serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/min_stack/src/lib.rs b/tests/test_kernels/min_stack/src/lib.rs new file mode 100644 index 00000000..4e46fdb6 --- /dev/null +++ b/tests/test_kernels/min_stack/src/lib.rs @@ -0,0 +1,27 @@ +#![no_std] + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/test_kernels/pie/Cargo.toml b/tests/test_kernels/pie/Cargo.toml new file mode 100644 index 00000000..93f71abd --- /dev/null +++ b/tests/test_kernels/pie/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test_kernel_pie" +version = "0.1.0" +authors = ["Tom Dohrmann "] +edition = "2018" + +[dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" diff --git a/tests/test_kernels/pie/src/bin/basic_boot.rs b/tests/test_kernels/pie/src/bin/basic_boot.rs new file mode 100644 index 00000000..468dab12 --- /dev/null +++ b/tests/test_kernels/pie/src/bin/basic_boot.rs @@ -0,0 +1,21 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_pie::{exit_qemu, QemuExitCode}; + +entry_point!(kernel_main); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_pie::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/pie/src/bin/check_boot_info.rs b/tests/test_kernels/pie/src/bin/check_boot_info.rs new file mode 100644 index 00000000..eba687e0 --- /dev/null +++ b/tests/test_kernels/pie/src/bin/check_boot_info.rs @@ -0,0 +1,50 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, info::PixelFormat, BootInfo}; +use test_kernel_pie::{exit_qemu, QemuExitCode}; + +entry_point!(kernel_main); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + // check memory regions + assert!(boot_info.memory_regions.len() > 4); + + // check framebuffer + let framebuffer = boot_info.framebuffer.as_ref().unwrap(); + assert_eq!(framebuffer.info().byte_len, framebuffer.buffer().len()); + if ![3, 4].contains(&framebuffer.info().bytes_per_pixel) { + panic!( + "unexpected bytes_per_pixel `{}`", + framebuffer.info().bytes_per_pixel + ); + } + assert_eq!(framebuffer.info().pixel_format, PixelFormat::Bgr); + assert_eq!( + framebuffer.buffer().len(), + framebuffer.info().stride * framebuffer.info().height * framebuffer.info().bytes_per_pixel + ); + + // check defaults for optional features + assert_eq!(boot_info.physical_memory_offset.into_option(), None); + assert_eq!(boot_info.recursive_index.into_option(), None); + + // check rsdp_addr + let rsdp = boot_info.rsdp_addr.into_option().unwrap(); + assert!(rsdp > 0x000E0000); + + // the test kernel has no TLS template + assert_eq!(boot_info.tls_template.into_option(), None); + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_pie::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/pie/src/bin/global_variable.rs b/tests/test_kernels/pie/src/bin/global_variable.rs new file mode 100644 index 00000000..e349e7ed --- /dev/null +++ b/tests/test_kernels/pie/src/bin/global_variable.rs @@ -0,0 +1,36 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use core::sync::atomic::{AtomicU64, Ordering}; +use test_kernel_pie::{exit_qemu, QemuExitCode}; + +entry_point!(kernel_main); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + // Initialize with a value that is unlikely to be anywhere in memory. + // If we can later read out this exact value, we can be sure that we actually + // read from this global variable and not some other location in memory. + static FOO: AtomicU64 = AtomicU64::new(0xdeadbeef); + + // Make sure that relocations are actually applied by referencing a `FOO` + // in `FOO_REF`. `FOO`'s address will be calculated and put into `FOO_REF` + // at load time using a relocation. + static FOO_REF: &AtomicU64 = &FOO; + + // Verify that the memory address pointed to by `FOO_REF` contains our value. + let val = FOO_REF.load(Ordering::Relaxed); + assert_eq!(val, 0xdeadbeef); + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_pie::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/pie/src/bin/should_panic.rs b/tests/test_kernels/pie/src/bin/should_panic.rs new file mode 100644 index 00000000..fb08bf5c --- /dev/null +++ b/tests/test_kernels/pie/src/bin/should_panic.rs @@ -0,0 +1,19 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; + +entry_point!(kernel_main); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + panic!(); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + use test_kernel_pie::{exit_qemu, QemuExitCode}; + + exit_qemu(QemuExitCode::Success); +} diff --git a/tests/test_kernels/pie/src/lib.rs b/tests/test_kernels/pie/src/lib.rs new file mode 100644 index 00000000..4e46fdb6 --- /dev/null +++ b/tests/test_kernels/pie/src/lib.rs @@ -0,0 +1,27 @@ +#![no_std] + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/test_kernels/ramdisk/Cargo.toml b/tests/test_kernels/ramdisk/Cargo.toml new file mode 100644 index 00000000..0ca537e7 --- /dev/null +++ b/tests/test_kernels/ramdisk/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "test_kernel_ramdisk" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2021" + +[dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" diff --git a/tests/test_kernels/ramdisk/src/bin/basic_boot.rs b/tests/test_kernels/ramdisk/src/bin/basic_boot.rs new file mode 100644 index 00000000..949c878b --- /dev/null +++ b/tests/test_kernels/ramdisk/src/bin/basic_boot.rs @@ -0,0 +1,21 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use test_kernel_ramdisk::{exit_qemu, QemuExitCode}; + +entry_point!(kernel_main); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[panic_handler] +#[cfg(not(test))] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_ramdisk::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/ramdisk/src/bin/memory_map.rs b/tests/test_kernels/ramdisk/src/bin/memory_map.rs new file mode 100644 index 00000000..b939a420 --- /dev/null +++ b/tests/test_kernels/ramdisk/src/bin/memory_map.rs @@ -0,0 +1,88 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{ + config::Mapping, entry_point, info::MemoryRegionKind, BootInfo, BootloaderConfig, +}; +use core::{fmt::Write, ptr::slice_from_raw_parts}; +use test_kernel_ramdisk::{exit_qemu, serial, QemuExitCode, RAMDISK_CONTENTS}; +use x86_64::{ + structures::paging::{OffsetPageTable, PageTable, PageTableFlags, Translate}, + VirtAddr, +}; + +pub const BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.mappings.physical_memory = Some(Mapping::FixedAddress(0x0000_6000_0000_0000)); + config +}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + writeln!(serial(), "Boot info: {boot_info:?}").unwrap(); + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset.into_option().unwrap()); + let level_4_table = unsafe { active_level_4_table(phys_mem_offset) }; + let page_table = unsafe { OffsetPageTable::new(level_4_table, phys_mem_offset) }; + + let ramdisk_start_addr = VirtAddr::new(boot_info.ramdisk_addr.into_option().unwrap()); + assert_eq!(boot_info.ramdisk_len as usize, RAMDISK_CONTENTS.len()); + let ramdisk_end_addr = ramdisk_start_addr + boot_info.ramdisk_len; + + let mut next_addr = ramdisk_start_addr; + while next_addr < ramdisk_end_addr { + let phys_addr = match page_table.translate(next_addr) { + x86_64::structures::paging::mapper::TranslateResult::Mapped { + frame, + offset: _, + flags, + } => { + assert!(flags.contains(PageTableFlags::PRESENT)); + assert!(flags.contains(PageTableFlags::WRITABLE)); + + next_addr += frame.size(); + + frame.start_address() + } + other => panic!("invalid result: {other:?}"), + }; + let region = boot_info + .memory_regions + .iter() + .find(|r| r.start <= phys_addr.as_u64() && r.end > phys_addr.as_u64()) + .unwrap(); + assert_eq!(region.kind, MemoryRegionKind::Bootloader); + } + + let actual_ramdisk = unsafe { + &*slice_from_raw_parts( + boot_info.ramdisk_addr.into_option().unwrap() as *const u8, + boot_info.ramdisk_len as usize, + ) + }; + writeln!(serial(), "Actual contents: {actual_ramdisk:?}").unwrap(); + assert_eq!(RAMDISK_CONTENTS, actual_ramdisk); + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + let _ = writeln!(test_kernel_ramdisk::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} + +pub unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) -> &'static mut PageTable { + use x86_64::registers::control::Cr3; + + let (level_4_table_frame, _) = Cr3::read(); + + let phys = level_4_table_frame.start_address(); + let virt = physical_memory_offset + phys.as_u64(); + let page_table_ptr: *mut PageTable = virt.as_mut_ptr(); + + &mut *page_table_ptr // unsafe +} diff --git a/tests/test_kernels/ramdisk/src/bin/ramdisk.rs b/tests/test_kernels/ramdisk/src/bin/ramdisk.rs new file mode 100644 index 00000000..44fb5cd6 --- /dev/null +++ b/tests/test_kernels/ramdisk/src/bin/ramdisk.rs @@ -0,0 +1,32 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{entry_point, BootInfo}; +use core::{fmt::Write, ptr::slice_from_raw_parts}; +use test_kernel_ramdisk::{exit_qemu, serial, QemuExitCode, RAMDISK_CONTENTS}; + +entry_point!(kernel_main); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + writeln!(serial(), "Boot info: {boot_info:?}").unwrap(); + assert!(boot_info.ramdisk_addr.into_option().is_some()); + assert_eq!(boot_info.ramdisk_len as usize, RAMDISK_CONTENTS.len()); + let actual_ramdisk = unsafe { + &*slice_from_raw_parts( + boot_info.ramdisk_addr.into_option().unwrap() as *const u8, + boot_info.ramdisk_len as usize, + ) + }; + writeln!(serial(), "Actual contents: {actual_ramdisk:?}").unwrap(); + assert_eq!(RAMDISK_CONTENTS, actual_ramdisk); + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + let _ = writeln!(test_kernel_ramdisk::serial(), "PANIC: {info}"); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/ramdisk/src/lib.rs b/tests/test_kernels/ramdisk/src/lib.rs new file mode 100644 index 00000000..00ea92a6 --- /dev/null +++ b/tests/test_kernels/ramdisk/src/lib.rs @@ -0,0 +1,29 @@ +#![no_std] + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub static RAMDISK_CONTENTS: &[u8] = include_bytes!("../../../ramdisk.txt"); + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/test_kernels/write_usable_memory/Cargo.toml b/tests/test_kernels/write_usable_memory/Cargo.toml new file mode 100644 index 00000000..200abc9d --- /dev/null +++ b/tests/test_kernels/write_usable_memory/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "test_kernel_write_usable_memory" +version = "0.1.0" +edition = "2021" + +[dependencies] +bootloader_api = { path = "../../../api" } +x86_64 = { version = "0.15.2", default-features = false, features = [ + "instructions", +] } +uart_16550 = "0.2.10" diff --git a/tests/test_kernels/write_usable_memory/src/bin/write_usable_memory.rs b/tests/test_kernels/write_usable_memory/src/bin/write_usable_memory.rs new file mode 100644 index 00000000..95a27d37 --- /dev/null +++ b/tests/test_kernels/write_usable_memory/src/bin/write_usable_memory.rs @@ -0,0 +1,42 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader_api::{ + config::Mapping, entry_point, info::MemoryRegionKind, BootInfo, BootloaderConfig, +}; +use test_kernel_write_usable_memory::{exit_qemu, QemuExitCode}; + +pub const BOOTLOADER_CONFIG: BootloaderConfig = { + let mut config = BootloaderConfig::new_default(); + config.mappings.physical_memory = Some(Mapping::FixedAddress(0x0000_4000_0000_0000)); + config +}; + +entry_point!(kernel_main, config = &BOOTLOADER_CONFIG); + +fn kernel_main(boot_info: &'static mut BootInfo) -> ! { + let phys_mem_offset = boot_info.physical_memory_offset.into_option().unwrap(); + + for region in boot_info.memory_regions.iter() { + if region.kind == MemoryRegionKind::Usable { + // ensure region is actually writable + let addr = phys_mem_offset + region.start; + let size = region.end - region.start; + unsafe { + core::ptr::write_bytes(addr as *mut u8, 0xff, size as usize); + } + } + } + + exit_qemu(QemuExitCode::Success); +} + +/// This function is called on panic. +#[panic_handler] +#[cfg(not(test))] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::fmt::Write; + + let _ = writeln!(test_kernel_write_usable_memory::serial(), "PANIC: {}", info); + exit_qemu(QemuExitCode::Failed); +} diff --git a/tests/test_kernels/write_usable_memory/src/lib.rs b/tests/test_kernels/write_usable_memory/src/lib.rs new file mode 100644 index 00000000..4e46fdb6 --- /dev/null +++ b/tests/test_kernels/write_usable_memory/src/lib.rs @@ -0,0 +1,27 @@ +#![no_std] + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) -> ! { + use x86_64::instructions::{nop, port::Port}; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } + + loop { + nop(); + } +} + +pub fn serial() -> uart_16550::SerialPort { + let mut port = unsafe { uart_16550::SerialPort::new(0x3F8) }; + port.init(); + port +} diff --git a/tests/write_usable_memory.rs b/tests/write_usable_memory.rs new file mode 100644 index 00000000..a2c7b11a --- /dev/null +++ b/tests/write_usable_memory.rs @@ -0,0 +1,7 @@ +use bootloader_test_runner::run_test_kernel; +#[test] +fn lower_memory_free() { + run_test_kernel(env!( + "CARGO_BIN_FILE_TEST_KERNEL_WRITE_USABLE_MEMORY_write_usable_memory" + )); +} diff --git a/uefi/Cargo.toml b/uefi/Cargo.toml new file mode 100644 index 00000000..9e96ce64 --- /dev/null +++ b/uefi/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bootloader-x86_64-uefi" +version.workspace = true +edition = "2021" +description = "UEFI bootloader for x86_64" +license.workspace = true +repository.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bootloader_api = { workspace = true } +bootloader-x86_64-common = { workspace = true } +bootloader-boot-config = { workspace = true } +log = "0.4.14" +x86_64 = "0.15.2" +serde-json-core = "0.5.0" +uefi = "0.20.0" diff --git a/uefi/README.md b/uefi/README.md new file mode 100644 index 00000000..19630fad --- /dev/null +++ b/uefi/README.md @@ -0,0 +1,7 @@ +# UEFI Bootloader for `x86_64` + +## Build + +``` +cargo b --target x86_64-unknown-uefi --release -Zbuild-std=core -Zbuild-std-features=compiler-builtins-mem +``` diff --git a/uefi/src/main.rs b/uefi/src/main.rs new file mode 100644 index 00000000..99a4821c --- /dev/null +++ b/uefi/src/main.rs @@ -0,0 +1,566 @@ +#![no_std] +#![no_main] +#![deny(unsafe_op_in_unsafe_fn)] + +use crate::memory_descriptor::UefiMemoryDescriptor; +use bootloader_api::info::FrameBufferInfo; +use bootloader_boot_config::BootConfig; +use bootloader_x86_64_common::{ + legacy_memory_region::LegacyFrameAllocator, Kernel, RawFrameBufferInfo, SystemInfo, +}; +use core::{ + cell::UnsafeCell, + ops::{Deref, DerefMut}, + ptr, slice, +}; +use uefi::{ + prelude::{entry, Boot, Handle, Status, SystemTable}, + proto::{ + console::gop::{GraphicsOutput, PixelFormat}, + device_path::DevicePath, + loaded_image::LoadedImage, + media::{ + file::{File, FileAttribute, FileInfo, FileMode}, + fs::SimpleFileSystem, + }, + network::{ + pxe::{BaseCode, DhcpV4Packet}, + IpAddress, + }, + ProtocolPointer, + }, + table::boot::{ + AllocateType, MemoryType, OpenProtocolAttributes, OpenProtocolParams, ScopedProtocol, + }, + CStr16, CStr8, +}; +use x86_64::{ + structures::paging::{FrameAllocator, OffsetPageTable, PageTable, PhysFrame, Size4KiB}, + PhysAddr, VirtAddr, +}; + +mod memory_descriptor; + +static SYSTEM_TABLE: RacyCell>> = RacyCell::new(None); + +struct RacyCell(UnsafeCell); + +impl RacyCell { + const fn new(v: T) -> Self { + Self(UnsafeCell::new(v)) + } +} + +unsafe impl Sync for RacyCell {} + +impl core::ops::Deref for RacyCell { + type Target = UnsafeCell; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[entry] +fn efi_main(image: Handle, st: SystemTable) -> Status { + main_inner(image, st) +} + +fn main_inner(image: Handle, mut st: SystemTable) -> Status { + // temporarily clone the y table for printing panics + unsafe { + *SYSTEM_TABLE.get() = Some(st.unsafe_clone()); + } + + let mut boot_mode = BootMode::Disk; + + let mut kernel = load_kernel(image, &mut st, boot_mode); + if kernel.is_none() { + // Try TFTP boot + boot_mode = BootMode::Tftp; + kernel = load_kernel(image, &mut st, boot_mode); + } + let kernel = kernel.expect("Failed to load kernel"); + + let config_file = load_config_file(image, &mut st, boot_mode); + let mut error_loading_config: Option = None; + let mut config: BootConfig = match config_file + .as_deref() + .map(serde_json_core::from_slice) + .transpose() + { + Ok(data) => data.unwrap_or_default().0, + Err(err) => { + error_loading_config = Some(err); + Default::default() + } + }; + + #[allow(deprecated)] + if config.frame_buffer.minimum_framebuffer_height.is_none() { + config.frame_buffer.minimum_framebuffer_height = + kernel.config.frame_buffer.minimum_framebuffer_height; + } + #[allow(deprecated)] + if config.frame_buffer.minimum_framebuffer_width.is_none() { + config.frame_buffer.minimum_framebuffer_width = + kernel.config.frame_buffer.minimum_framebuffer_width; + } + let framebuffer = init_logger(image, &st, &config); + + unsafe { + *SYSTEM_TABLE.get() = None; + } + + log::info!("UEFI bootloader started"); + + if let Some(framebuffer) = framebuffer { + log::info!("Using framebuffer at {:#x}", framebuffer.addr); + } + + if let Some(err) = error_loading_config { + log::warn!("Failed to deserialize the config file {:?}", err); + } else { + log::info!("Reading configuration from disk was successful"); + } + + log::info!("Trying to load ramdisk via {:?}", boot_mode); + // Ramdisk must load from same source, or not at all. + let ramdisk = load_ramdisk(image, &mut st, boot_mode); + + log::info!( + "{}", + match ramdisk { + Some(_) => "Loaded ramdisk", + None => "Ramdisk not found.", + } + ); + + log::trace!("exiting boot services"); + let (system_table, mut memory_map) = st.exit_boot_services(); + + memory_map.sort(); + + let mut frame_allocator = + LegacyFrameAllocator::new(memory_map.entries().copied().map(UefiMemoryDescriptor)); + + let max_phys_addr = frame_allocator.max_phys_addr(); + let page_tables = create_page_tables(&mut frame_allocator, max_phys_addr, framebuffer.as_ref()); + let mut ramdisk_len = 0u64; + let ramdisk_addr = if let Some(rd) = ramdisk { + ramdisk_len = rd.len() as u64; + Some(rd.as_ptr() as usize as u64) + } else { + None + }; + let system_info = SystemInfo { + framebuffer, + rsdp_addr: { + use uefi::table::cfg; + let mut config_entries = system_table.config_table().iter(); + // look for an ACPI2 RSDP first + let acpi2_rsdp = config_entries.find(|entry| matches!(entry.guid, cfg::ACPI2_GUID)); + // if no ACPI2 RSDP is found, look for a ACPI1 RSDP + let rsdp = acpi2_rsdp + .or_else(|| config_entries.find(|entry| matches!(entry.guid, cfg::ACPI_GUID))); + rsdp.map(|entry| PhysAddr::new(entry.address as u64)) + }, + ramdisk_addr, + ramdisk_len, + }; + + bootloader_x86_64_common::load_and_switch_to_kernel( + kernel, + config, + frame_allocator, + page_tables, + system_info, + ); +} + +#[derive(Clone, Copy, Debug)] +pub enum BootMode { + Disk, + Tftp, +} + +fn load_ramdisk( + image: Handle, + st: &mut SystemTable, + boot_mode: BootMode, +) -> Option<&'static mut [u8]> { + load_file_from_boot_method(image, st, "ramdisk\0", boot_mode) +} + +fn load_config_file( + image: Handle, + st: &mut SystemTable, + boot_mode: BootMode, +) -> Option<&'static mut [u8]> { + load_file_from_boot_method(image, st, "boot.json\0", boot_mode) +} + +fn load_kernel( + image: Handle, + st: &mut SystemTable, + boot_mode: BootMode, +) -> Option> { + let kernel_slice = load_file_from_boot_method(image, st, "kernel-x86_64\0", boot_mode)?; + Some(Kernel::parse(kernel_slice)) +} + +fn load_file_from_boot_method( + image: Handle, + st: &mut SystemTable, + filename: &str, + boot_mode: BootMode, +) -> Option<&'static mut [u8]> { + match boot_mode { + BootMode::Disk => load_file_from_disk(filename, image, st), + BootMode::Tftp => load_file_from_tftp_boot_server(filename, image, st), + } +} + +fn open_device_path_protocol( + image: Handle, + st: &SystemTable, +) -> Option> { + let this = st.boot_services(); + let loaded_image = unsafe { + this.open_protocol::( + OpenProtocolParams { + handle: image, + agent: image, + controller: None, + }, + OpenProtocolAttributes::Exclusive, + ) + }; + + if loaded_image.is_err() { + log::error!("Failed to open protocol LoadedImage"); + return None; + } + let loaded_image = loaded_image.unwrap(); + let loaded_image = loaded_image.deref(); + + let device_handle = loaded_image.device(); + + let device_path = unsafe { + this.open_protocol::( + OpenProtocolParams { + handle: device_handle, + agent: image, + controller: None, + }, + OpenProtocolAttributes::Exclusive, + ) + }; + if device_path.is_err() { + log::error!("Failed to open protocol DevicePath"); + return None; + } + Some(device_path.unwrap()) +} + +fn locate_and_open_protocol( + image: Handle, + st: &SystemTable, +) -> Option> { + let this = st.boot_services(); + let device_path = open_device_path_protocol(image, st)?; + let mut device_path = device_path.deref(); + + let fs_handle = this.locate_device_path::

(&mut device_path); + if fs_handle.is_err() { + log::error!("Failed to open device path"); + return None; + } + + let fs_handle = fs_handle.unwrap(); + + let opened_handle = unsafe { + this.open_protocol::

( + OpenProtocolParams { + handle: fs_handle, + agent: image, + controller: None, + }, + OpenProtocolAttributes::Exclusive, + ) + }; + + if opened_handle.is_err() { + log::error!("Failed to open protocol {}", core::any::type_name::

()); + return None; + } + Some(opened_handle.unwrap()) +} + +fn load_file_from_disk( + name: &str, + image: Handle, + st: &SystemTable, +) -> Option<&'static mut [u8]> { + let mut file_system_raw = locate_and_open_protocol::(image, st)?; + let file_system = file_system_raw.deref_mut(); + + let mut root = file_system.open_volume().unwrap(); + let mut buf = [0u16; 256]; + assert!(name.len() < 256); + let filename = CStr16::from_str_with_buf(name.trim_end_matches('\0'), &mut buf) + .expect("Failed to convert string to utf16"); + + let file_handle_result = root.open(filename, FileMode::Read, FileAttribute::empty()); + + let file_handle = file_handle_result.ok()?; + + let mut file = match file_handle.into_type().unwrap() { + uefi::proto::media::file::FileType::Regular(f) => f, + uefi::proto::media::file::FileType::Dir(_) => panic!(), + }; + + let mut buf = [0; 500]; + let file_info: &mut FileInfo = file.get_info(&mut buf).unwrap(); + let file_size = usize::try_from(file_info.file_size()).unwrap(); + + let file_ptr = st + .boot_services() + .allocate_pages( + AllocateType::AnyPages, + MemoryType::LOADER_DATA, + ((file_size - 1) / 4096) + 1, + ) + .unwrap() as *mut u8; + unsafe { ptr::write_bytes(file_ptr, 0, file_size) }; + let file_slice = unsafe { slice::from_raw_parts_mut(file_ptr, file_size) }; + file.read(file_slice).unwrap(); + + Some(file_slice) +} + +/// Try to load a kernel from a TFTP boot server. +fn load_file_from_tftp_boot_server( + name: &str, + image: Handle, + st: &SystemTable, +) -> Option<&'static mut [u8]> { + let mut base_code_raw = locate_and_open_protocol::(image, st)?; + let base_code = base_code_raw.deref_mut(); + + // Find the TFTP boot server. + let mode = base_code.mode(); + assert!(mode.dhcp_ack_received); + let dhcpv4: &DhcpV4Packet = mode.dhcp_ack.as_ref(); + let server_ip = IpAddress::new_v4(dhcpv4.bootp_si_addr); + assert!(name.len() < 256); + + let filename = CStr8::from_bytes_with_nul(name.as_bytes()).unwrap(); + + // Determine the kernel file size. + let file_size = base_code.tftp_get_file_size(&server_ip, filename).ok()?; + let kernel_size = usize::try_from(file_size).expect("The file size should fit into usize"); + + // Allocate some memory for the kernel file. + let ptr = st + .boot_services() + .allocate_pages( + AllocateType::AnyPages, + MemoryType::LOADER_DATA, + ((kernel_size - 1) / 4096) + 1, + ) + .expect("Failed to allocate memory for the file") as *mut u8; + let slice = unsafe { slice::from_raw_parts_mut(ptr, kernel_size) }; + + // Load the kernel file. + base_code + .tftp_read_file(&server_ip, filename, Some(slice)) + .expect("Failed to read kernel file from the TFTP boot server"); + + Some(slice) +} + +/// Creates page table abstraction types for both the bootloader and kernel page tables. +fn create_page_tables( + frame_allocator: &mut impl FrameAllocator, + max_phys_addr: PhysAddr, + frame_buffer: Option<&RawFrameBufferInfo>, +) -> bootloader_x86_64_common::PageTables { + // UEFI identity-maps all memory, so the offset between physical and virtual addresses is 0 + let phys_offset = VirtAddr::new(0); + + // copy the currently active level 4 page table, because it might be read-only + log::trace!("switching to new level 4 table"); + let bootloader_page_table = { + let old_table = { + let frame = x86_64::registers::control::Cr3::read().0; + let ptr: *const PageTable = (phys_offset + frame.start_address().as_u64()).as_ptr(); + unsafe { &*ptr } + }; + let new_frame = frame_allocator + .allocate_frame() + .expect("Failed to allocate frame for new level 4 table"); + let new_table: &mut PageTable = { + let ptr: *mut PageTable = + (phys_offset + new_frame.start_address().as_u64()).as_mut_ptr(); + // create a new, empty page table + unsafe { + ptr.write(PageTable::new()); + &mut *ptr + } + }; + + // copy the pml4 entries for all identity mapped memory. + let end_addr = VirtAddr::new(max_phys_addr.as_u64() - 1); + for p4 in 0..=usize::from(end_addr.p4_index()) { + new_table[p4] = old_table[p4].clone(); + } + + // copy the pml4 entry for the frame buffer (the frame buffer is not + // necessarily part of the identity mapping). + if let Some(frame_buffer) = frame_buffer { + let start_addr = VirtAddr::new(frame_buffer.addr.as_u64()); + let end_addr = start_addr + frame_buffer.info.byte_len as u64; + for p4 in usize::from(start_addr.p4_index())..=usize::from(end_addr.p4_index()) { + new_table[p4] = old_table[p4].clone(); + } + } + + // the first level 4 table entry is now identical, so we can just load the new one + unsafe { + x86_64::registers::control::Cr3::write( + new_frame, + x86_64::registers::control::Cr3Flags::empty(), + ); + OffsetPageTable::new(&mut *new_table, phys_offset) + } + }; + + // create a new page table hierarchy for the kernel + let (kernel_page_table, kernel_level_4_frame) = { + // get an unused frame for new level 4 page table + let frame: PhysFrame = frame_allocator.allocate_frame().expect("no unused frames"); + log::info!("New page table at: {:#?}", &frame); + // get the corresponding virtual address + let addr = phys_offset + frame.start_address().as_u64(); + // initialize a new page table + let ptr = addr.as_mut_ptr(); + unsafe { *ptr = PageTable::new() }; + let level_4_table = unsafe { &mut *ptr }; + ( + unsafe { OffsetPageTable::new(level_4_table, phys_offset) }, + frame, + ) + }; + + bootloader_x86_64_common::PageTables { + bootloader: bootloader_page_table, + kernel: kernel_page_table, + kernel_level_4_frame, + } +} + +fn init_logger( + image_handle: Handle, + st: &SystemTable, + config: &BootConfig, +) -> Option { + let gop_handle = st + .boot_services() + .get_handle_for_protocol::() + .ok()?; + let mut gop = unsafe { + st.boot_services() + .open_protocol::( + OpenProtocolParams { + handle: gop_handle, + agent: image_handle, + controller: None, + }, + OpenProtocolAttributes::Exclusive, + ) + .ok()? + }; + + let mode = { + let modes = gop.modes(); + match ( + config + .frame_buffer + .minimum_framebuffer_height + .map(|v| usize::try_from(v).unwrap()), + config + .frame_buffer + .minimum_framebuffer_width + .map(|v| usize::try_from(v).unwrap()), + ) { + (Some(height), Some(width)) => modes + .filter(|m| { + let res = m.info().resolution(); + res.1 >= height && res.0 >= width + }) + .last(), + (Some(height), None) => modes.filter(|m| m.info().resolution().1 >= height).last(), + (None, Some(width)) => modes.filter(|m| m.info().resolution().0 >= width).last(), + _ => None, + } + }; + if let Some(mode) = mode { + gop.set_mode(&mode) + .expect("Failed to apply the desired display mode"); + } + + let mode_info = gop.current_mode_info(); + let mut framebuffer = gop.frame_buffer(); + let slice = unsafe { slice::from_raw_parts_mut(framebuffer.as_mut_ptr(), framebuffer.size()) }; + let info = FrameBufferInfo { + byte_len: framebuffer.size(), + width: mode_info.resolution().0, + height: mode_info.resolution().1, + pixel_format: match mode_info.pixel_format() { + PixelFormat::Rgb => bootloader_api::info::PixelFormat::Rgb, + PixelFormat::Bgr => bootloader_api::info::PixelFormat::Bgr, + PixelFormat::Bitmask | PixelFormat::BltOnly => { + panic!("Bitmask and BltOnly framebuffers are not supported") + } + }, + bytes_per_pixel: 4, + stride: mode_info.stride(), + }; + + bootloader_x86_64_common::init_logger( + slice, + info, + config.log_level, + config.frame_buffer_logging, + config.serial_logging, + ); + + Some(RawFrameBufferInfo { + addr: PhysAddr::new(framebuffer.as_mut_ptr() as u64), + info, + }) +} + +#[cfg(target_os = "uefi")] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + use core::arch::asm; + use core::fmt::Write; + + if let Some(st) = unsafe { &mut *SYSTEM_TABLE.get() } { + let _ = st.stdout().clear(); + let _ = writeln!(st.stdout(), "{}", info); + } + + unsafe { + bootloader_x86_64_common::logger::LOGGER + .get() + .map(|l| l.force_unlock()) + }; + log::error!("{}", info); + + loop { + unsafe { asm!("cli; hlt") }; + } +} diff --git a/uefi/src/memory_descriptor.rs b/uefi/src/memory_descriptor.rs new file mode 100644 index 00000000..dcf05a9c --- /dev/null +++ b/uefi/src/memory_descriptor.rs @@ -0,0 +1,46 @@ +use bootloader_api::info::MemoryRegionKind; +use bootloader_x86_64_common::legacy_memory_region::LegacyMemoryRegion; +use uefi::table::boot::{MemoryDescriptor, MemoryType}; +use x86_64::PhysAddr; + +#[derive(Debug, Copy, Clone)] +pub struct UefiMemoryDescriptor(pub MemoryDescriptor); + +const PAGE_SIZE: u64 = 4096; + +impl LegacyMemoryRegion for UefiMemoryDescriptor { + fn start(&self) -> PhysAddr { + PhysAddr::new(self.0.phys_start) + } + + fn len(&self) -> u64 { + self.0.page_count * PAGE_SIZE + } + + fn kind(&self) -> MemoryRegionKind { + match self.0.ty { + MemoryType::CONVENTIONAL => MemoryRegionKind::Usable, + other => MemoryRegionKind::UnknownUefi(other.0), + } + } + + fn usable_after_bootloader_exit(&self) -> bool { + match self.0.ty { + MemoryType::CONVENTIONAL => true, + MemoryType::LOADER_CODE + | MemoryType::LOADER_DATA + | MemoryType::BOOT_SERVICES_CODE + | MemoryType::BOOT_SERVICES_DATA => { + // we don't need this data anymore after the bootloader + // passes control to the kernel + true + } + MemoryType::RUNTIME_SERVICES_CODE | MemoryType::RUNTIME_SERVICES_DATA => { + // the UEFI standard specifies that these should be presevered + // by the bootloader and operating system + false + } + _ => false, + } + } +} diff --git a/x86_64-bootloader.json b/x86_64-bootloader.json deleted file mode 100644 index ad13109d..00000000 --- a/x86_64-bootloader.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "llvm-target": "x86_64-unknown-none-gnu", - "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", - "linker-flavor": "ld.lld", - "linker": "rust-lld", - "pre-link-args": { - "ld.lld": [ - "--script=linker.ld" - ] - }, - "target-endian": "little", - "target-pointer-width": "64", - "target-c-int-width": "32", - "arch": "x86_64", - "os": "none", - "features": "-mmx,-sse,+soft-float", - "disable-redzone": true, - "panic": "abort", - "executables": true, - "relocation_model": "static" -} diff --git a/x86_64-stage-4.json b/x86_64-stage-4.json new file mode 100644 index 00000000..b2e7e997 --- /dev/null +++ b/x86_64-stage-4.json @@ -0,0 +1,23 @@ +{ + "arch": "x86_64", + "code-model": "kernel", + "cpu": "x86-64", + "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", + "disable-redzone": true, + "features": "-mmx,-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2,+soft-float", + "linker": "rust-lld", + "linker-flavor": "ld.lld", + "llvm-target": "x86_64-unknown-none-elf", + "max-atomic-width": 64, + "panic-strategy": "abort", + "position-independent-executables": true, + "relro-level": "off", + "stack-probes": { + "kind": "call" + }, + "static-position-independent-executables": true, + "target-pointer-width": "64", + "relocation-model": "static", + "os": "none", + "rustc-abi": "x86-softfloat" +}