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..7e0eed7a --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.x86_64-unknown-uefi] +runner = "cargo run -p runner" + +[alias] +builder = "run --bin builder --features builder --quiet --" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 56fa6316..c09bde8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,122 +2,100 @@ name: Build on: push: - branches: - - '*' - - '!staging.tmp' - tags: - - '*' pull_request: jobs: - test: - name: "Test" + check: + name: Check - strategy: - fail-fast: false - matrix: - platform: [ - ubuntu-latest, - macos-latest, - windows-latest - ] - - runs-on: ${{ matrix.platform }} - timeout-minutes: 15 + runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - name: "Checkout Repository" - uses: actions/checkout@v1 - - - 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 --version 0.1.7 --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 "$HOME\scoop\shims" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - 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 + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + override: true + - name: "Run `cargo check`" + uses: actions-rs/cargo@v1 + with: + command: check - - 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}' + test: + name: Test - build_example_kernel: - name: "Build Example Kernel" strategy: + fail-fast: false matrix: - platform: [ - ubuntu-latest, - macos-latest, - windows-latest - ] - runs-on: ${{ matrix.platform }} - timeout-minutes: 10 + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 steps: - - uses: actions/checkout@v1 - - 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" + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + override: true + components: rust-src, llvm-tools-preview + + # 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 "$HOME\scoop\shims" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + 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 `cargo test` + uses: actions-rs/cargo@v1 + with: + command: test + + fmt: + 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 + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: profile: minimal + override: true components: rustfmt + - name: Run `cargo fmt --all -- --check` + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal override: true - - run: cargo fmt -- --check + components: clippy + - name: Run `cargo clippy` + uses: actions-rs/cargo@v1 + with: + command: clippy diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..309b3c40 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "rust-analyzer.checkOnSave.allTargets": false, + "rust-analyzer.checkOnSave.extraArgs": [ + "-Zbuild-std=core,alloc" + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 160b7020..7650dde1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,47 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" + +[[package]] +name = "argh" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca1877e24cecacd700d469066e0160c4f8497cc5635367163f50c8beec820154" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e742194e0f43fc932bcb801708c2b279d3ec8f527e3acda05a6a9f342c5ef764" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ba68f4276a778591e36a0c348a269888f3a177c8d2054969389e3b59611ff5" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + [[package]] name = "bit_field" version = "0.9.0" @@ -22,24 +63,124 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bootloader" -version = "0.9.16" +version = "0.10.0-alpha-03" dependencies = [ + "anyhow", + "argh", "bit_field 0.10.1", - "fixedvec", + "conquer-once", + "displaydoc", + "fatfs", "font8x8", + "gpt", + "json", "llvm-tools", - "rlibc", + "log", + "proc-macro2", + "quote", + "rsdp", + "serde", + "spinning_top", + "thiserror", "toml", + "uefi", "usize_conversions", "x86_64", "xmas-elf", ] [[package]] -name = "fixedvec" -version = "0.2.4" +name = "bootloader-locator" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaaa9db3339d32c2622f2e5d0731eb82a468d3439797c9d4fe426744fe2bd551" +dependencies = [ + "json", +] + +[[package]] +name = "build_const" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" + +[[package]] +name = "byteorder" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "conquer-once" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb12fb69466716fbae9009d389e6a30830ae8975e170eff2d2cff579f9efa3" +dependencies = [ + "conquer-util", +] + +[[package]] +name = "conquer-util" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654fb2472cc369d311c547103a1fa81d467bef370ae7a0680f65939895b1182a" + +[[package]] +name = "crc" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb" +dependencies = [ + "build_const", +] + +[[package]] +name = "displaydoc" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc2ab4d5a16117f9029e9a6b5e4e79f4c67f6519bc134210d4d4a04ba31f41b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fatfs" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b395ef2adf62bdeefcd1b59ad0dd2225c6c333ec79656ea79ac5285c46d051ea" +checksum = "93079df23039e52059e1f03b4c29fb0c72da2c792aad91bb2236c9fb81d3592e" +dependencies = [ + "bitflags", + "byteorder", + "chrono", + "log", +] [[package]] name = "font8x8" @@ -47,6 +188,50 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44226c40489fb1d602344a1d8f1b544570c3435e396dda1eda7b5ef010d8f1be" +[[package]] +name = "getrandom" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4060f4657be78b8e766215b02b18a2e862d83745545de804638e2b545e81aee6" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "gpt" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b0e3659ffee31427c4aaa68c1e5115c8c86ba71ff11da5a16e5b70f3471d" +dependencies = [ + "bitflags", + "crc", + "log", + "uuid", +] + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "libc" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" + [[package]] name = "llvm-tools" version = "0.1.1" @@ -54,16 +239,180 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955be5d0ca0465caf127165acb47964f911e2bc26073e865deb8be7189302faf" [[package]] -name = "rlibc" +name = "locate-cargo-manifest" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db985b63431fe09e8d71f50aeceffcc31e720cb86be8dad2f38d084c5a328466" +dependencies = [ + "json", +] + +[[package]] +name = "lock_api" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if 0.1.10", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rsdp" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc874b127765f014d792f16763a81245ab80500e2ad921ed4ee9e82481ee08fe" +checksum = "49b26cb0132c85db7152fc291c9680a9240d4ef482287667819dbaec1d665dbd" +dependencies = [ + "log", +] + +[[package]] +name = "runner" +version = "0.1.0" +dependencies = [ + "bootloader-locator", + "locate-cargo-manifest", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.116" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "spinning_top" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e529d73e80d64b5f2631f9035113347c578a1c9c7774b83a2b880788459ab36" +dependencies = [ + "lock_api", +] + +[[package]] +name = "syn" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "test_kernel_default_settings" +version = "0.1.0" +dependencies = [ + "bootloader", + "uart_16550", + "x86_64", +] + +[[package]] +name = "test_kernel_map_phys_mem" +version = "0.1.0" +dependencies = [ + "bootloader", + "uart_16550", + "x86_64", +] + +[[package]] +name = "thiserror" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] [[package]] name = "toml" @@ -74,12 +423,103 @@ dependencies = [ "serde", ] +[[package]] +name = "uart_16550" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d45a3c9181dc9ba7d35d02b3c36d1604155289d935b7946510fb3d2f4b976d9" +dependencies = [ + "bitflags", + "x86_64", +] + +[[package]] +name = "ucs2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85061f4e43545a613c0da6b87725bf23f8da8613cf2473719c4f71a270c4ce8a" +dependencies = [ + "bit_field 0.10.1", +] + +[[package]] +name = "uefi" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf8b4606744665c071d73d84b4ba9763b464f3500b73d8e2f13ef6f31c99f1be" +dependencies = [ + "bitflags", + "log", + "ucs2", + "uefi-macros", +] + +[[package]] +name = "uefi-macros" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcca10ca861f34a320d178f3fdb29ffbf05087fc2c70d2a99860e3329bee1a8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + [[package]] name = "usize_conversions" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f70329e2cbe45d6c97a5112daad40c34cd9a4e18edb5a2a18fefeb584d8d25e5" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "x86_64" version = "0.13.2" diff --git a/Cargo.toml b/Cargo.toml index df352085..76085f98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,42 +1,73 @@ [package] name = "bootloader" -version = "0.9.16" +version = "0.10.0-alpha-03" authors = ["Philipp Oppermann "] license = "MIT/Apache-2.0" -description = "An experimental pure-Rust x86 bootloader." +description = "An experimental x86_64 bootloader that works on both BIOS and UEFI systems." repository = "https://github.com/rust-osdev/bootloader" edition = "2018" build = "build.rs" +[workspace] +members = [ + "tests/runner", + "tests/test_kernels/default_settings", + "tests/test_kernels/map_phys_mem", +] + [[bin]] -name = "bootloader" -required-features = ["binary"] +name = "builder" +required-features = ["builder"] + +[[bin]] +name = "bios" +required-features = ["bios_bin"] + +[[bin]] +name = "uefi" +required-features = ["uefi_bin"] [dependencies] xmas-elf = { version = "0.6.2", optional = true } x86_64 = { version = "0.13.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 } -rlibc = { version = "1.0.0", optional = true } +log = { version = "0.4.8", optional = true } +uefi = { version = "0.7.0", optional = true } +argh = { version = "0.1.3", optional = true } +displaydoc = { version = "0.1.7", optional = true } +conquer-once = { version = "0.2.1", optional = true, default-features = false } +spinning_top = { version = "0.2.1", optional = true } +anyhow = { version = "1.0.32", optional = true } +llvm-tools = { version = "0.1.1", optional = true } +thiserror = { version = "1.0.20", optional = true } +json = { version = "0.12.4", optional = true } +rsdp = { version = "1.0.0", optional = true } +fatfs = { version = "0.3.4", optional = true } +gpt = { version = "2.0.0", optional = true } [dependencies.font8x8] -version = "0.2.4" +version = "0.2.5" default-features = false features = ["unicode"] optional = true [build-dependencies] -llvm-tools = { version = "0.1", optional = true } +llvm-tools-build = { version = "0.1", optional = true, package = "llvm-tools" } toml = { version = "0.5.1", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true} +quote = { version = "1.0", optional = true} +proc-macro2 = { version = "1.0", optional = true} [features] default = [] -binary = ["xmas-elf", "x86_64", "usize_conversions", "fixedvec", "llvm-tools", "toml", "rlibc"] -vga_320x200 = ["font8x8"] -recursive_page_table = [] -map_physical_memory = [] -sse = ["bit_field"] +builder = ["argh", "thiserror", "displaydoc", "anyhow", "llvm-tools", "json", "fatfs", "gpt"] +bios_bin = ["binary", "rsdp"] +uefi_bin = ["binary", "uefi"] +binary = [ + "llvm-tools-build", "x86_64", "toml", "xmas-elf", "usize_conversions", "log", "conquer-once", + "spinning_top", "serde", "font8x8", "quote", "proc-macro2", +] [profile.dev] panic = "abort" @@ -45,6 +76,7 @@ panic = "abort" panic = "abort" lto = false debug = true +overflow-checks = true [package.metadata.bootloader] target = "x86_64-bootloader.json" diff --git a/README.md b/README.md index 5a5db560..bed48967 100644 --- a/README.md +++ b/README.md @@ -1,190 +1,72 @@ # 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) - -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 - -When you press the power button the computer loads the BIOS from some flash memory stored on the motherboard. -The BIOS initializes and self tests the hardware then loads the first 512 bytes into memory from the media device -(i.e. the cdrom or floppy disk). If the last two bytes equal 0xAA55 then the BIOS will jump to location 0x7C00 effectively -transferring control to the bootloader. At this point the CPU is running in 16 bit mode, -meaning only the 16 bit registers are available. Also since the BIOS only loads the first 512 bytes this means our bootloader -code has to stay below that limit, otherwise we’ll hit uninitialised memory! -Using [Bios interrupt calls](https://en.wikipedia.org/wiki/BIOS_interrupt_call) the bootloader prints debug information to the screen. -For more information on how to write a bootloader click [here](http://3zanders.co.uk/2017/10/13/writing-a-bootloader/). -The assembler files get imported through the [global_asm feature](https://doc.rust-lang.org/unstable-book/library-features/global-asm.html). -The assembler syntax definition used is the one llvm uses: [GNU Assembly](http://microelectronics.esa.int/erc32/doc/as.pdf). - -* stage_1.s -This stage initializes the stack, enables the A20 line, loads the rest of -the bootloader from disk, and jumps to stage_2. - -* stage_2.s -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. - -* stage_3.s -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. - - -## Build chain -The file `.cargo/config` defines an llvm target file called `x86_64-bootloader.json`. -This file defines the architecture sets freestanding flags and tells llvm to use the linker script `linker.ld`. - -The linker script tells the linker at which offsets the sections should be mapped to. In our case it tells the linker -that the bootloader asm files stage_0-3.s should be mapped to the very beginning of the executable. Read more about linker scripts -[here](https://www.sourceware.org/binutils/docs/ld/Scripts.html) - -Another important role plays the file `build.rs`. -Placing a file named `build.rs` in the root of a package will cause -Cargo to compile that script and execute it just before building the package. -You can read more about it [here](https://doc.rust-lang.org/cargo/reference/build-scripts.html). -The `build.rs` file execute the llvm tools you installed with `rustup component add llvm-tools-preview` -in this order: - -* Check size of .text section of the kernel if it's too small throw an error -```bash -llvm-size "../../target/x86_64-os/debug/svm_kernel" -``` - -* Strip debug symbols from kernel to make loading faster -```bash -llvm-objcopy "--strip-debug" "../../target/x86_64-os/debug/svm_kernel" "target/x86_64-bootloader/debug/build/bootloader-c8df27c930d8f65a/out/kernel_stripped-svm_kernel" -``` -* Rename the .data section to .kernel in the stripped kernel. - Objcopy when using `--binary-architecture` flag creates three synthetic symbols - `_binary_objfile_start`, `_binary_objfile_end`, `_binary_objfile_size.`. -These symbols use the project / binary name which is why we have to rename them to something more generic -to be able to reference them. -```bash -llvm-objcopy "-I" "binary" "-O" "elf64-x86-64" "--binary-architecture=i386:x86-64" "--rename-section" ".data=.kernel" "--redefine-sym" "_binary_kernel_stripped_svm_kernel_start=_kernel_start_addr" "--redefine-sym" "_binary_kernel_stripped_svm_kernel_end=_kernel_end_addr" "--redefine-sym" "_binary_kernel_stripped_svm_kernel_size=_kernel_size" "target/x86_64-bootloader/debug/build/bootloader-c8df27c930d8f65a/out/kernel_stripped-svm_kernel" "target/x86_64-bootloader/debug/build/bootloader-c8df27c930d8f65a/out/kernel_bin-svm_kernel.o" -``` -* Now create a static library out of it -```bash -llvm-ar "crs" "bootloader/target/x86_64-bootloader/debug/build/bootloader-c8df27c930d8f65a/out/libkernel_bin-svm_kernel.a" "target/x86_64-bootloader/debug/build/bootloader-c8df27c930d8f65a/out/kernel_bin-svm_kernel.o" -``` -Afterwards `build.rs` tells cargo to use the newly created static library to link against your kernel, with the help of the linker script everything gets placed correctly in the -resulting elf file. -The last step is to strip away the elf header from the resulting elf binary so that the bios can jump directly to the bootloader `stage_1.s`. This is done with: -```bash -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 -``` - - -## 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" - -# The address at which the bootinfo struct will be placed. if not provided, -# the bootloader will dynamically search for a location. -boot-info-address = "0xFFFFFFFF80000000" -``` - -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. +[![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://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) + +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 -``` +## Usage -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. +See our [documentation](https://docs.rs/bootloader). -## Run +## Architecture -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: +This project consists of three separate entities: -``` -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 -``` +- A library with the entry point and boot info definitions that kernels can include as a normal cargo dependency. +- BIOS and UEFI binaries that contain the actual bootloader implementation. +- A `builder` binary to simplify the build process of the BIOS and UEFI binaries. -You can run the `bootloader.bin` file using [QEMU](https://www.qemu.org/): +These three entities are currently all combined in a single crate using cargo feature flags. The reason for this is that the kernel and bootloader must use the exact same version of the `BootInfo` struct to prevent undefined behavior (we did not stabilize the boot info format yet, so it might change between versions). -``` -qemu-system-x86_64 -drive format=raw,file=target/x86_64-bootloader/release/bootloader.bin -``` +### Build and Boot -Or burn it to an USB drive to boot it on real hardware: +The build and boot process works the following way: -``` -dd if=target/x86_64-bootloader/release/bootloader.bin of=/dev/sdX && sync -``` +- The `builder` binary is a small command line tool that takes the path to the kernel manifest and binary as arguments. Optionally, it allows to override the cargo target and output dirs. It also accepts a `--quiet` switch and allows to only build the BIOS or UEFI binary instead of both. +- After parsing the arguments, the `builder` binary invokes the actual build command for the BIOS/UEFI binaries, which includes the correct `--target` and `--features` arguments (and `-Zbuild-std`). The kernel manifest and binary paths are passed as `KERNEL_MANIFEST` and `KERNEL` environment variables. +- The next step in the build process is the `build.rs` build script. It only does something when building the BIOS/UEFI binaries (indicated by the `binary` feature), otherwise it is a no-op. + - The script first runs some sanity checks, e.g. the kernel manifest and binary should be specified in env variables and should exist, the correct target triple should be used, and the `llvm-tools` rustup component should be installed. + - Then it copies the kernel executable and strips the debug symbols from it to make it smaller. This does not affect the original kernel binary. The stripped binary is then converted to a byte array and provided to the BIOS/UEFI binaries, either as a Rust `static` or through a linker argument. + - Next, the bootloader configuration is parsed, which can be specified in a `package.metadata.bootloader` table in the kernel manifest file. This requires some custom string parsing since TOML does not support unsigned 64-bit integers. Parse errors are turned into `compile_error!` calls to give nicer error messages. + - After parsing the configuration, it is written as a Rust struct definition into a new `bootloader_config.rs` file in the cargo `OUT_DIR`. This file is then included by the UEFI/BIOS binaries. +- After the build script, the compilation continues with either the `bin/uefi.rs` or the `bin/bios.rs`: + - The `bin/uefi.rs` specifies an UEFI entry point function called `efi_main`. It uses the [`uefi`](https://docs.rs/uefi/0.8.0/uefi/) crate to set up a pixel-based framebuffer using the UEFI GOP protocol. Then it exits the UEFI boot services and stores the physical memory map. The final step is to create some page table abstractions and call into `load_and_switch_to_kernel` function that is shared with the BIOS boot code. + - The `bin/bios.rs` function does not provide a direct entry point. Instead it includes several assembly files (`asm/stage-*.rs`) that implement the CPU initialization (from real mode to long mode), the framebuffer setup (via VESA), and the memory map creation (via a BIOS call). The assembly stages are explained in more detail below. After the assembly stages, the execution jumps to the `bootloader_main` function in `bios.rs`. There we set up some additional identity mapping, translate the memory map and framebuffer into Rust structs, detect the RSDP table, and create some page table abstractions. Then we call into the `load_and_switch_to_kernel` function like the `bin/uefi.rs`. +- The common `load_and_switch_to_kernel` function is defined in `src/binary/mod.rs`. This is also the file that includes the `bootloader_config.rs` generated by the build script. The `load_and_switch_to_kernel` functions performs the following steps: + - Parse the kernel binary and map it in a new page table. This includes setting up the correct permissions for each page, initializing `.bss` sections, and allocating a stack with guard page. The relevant functions for these steps are `set_up_mappings` and `load_kernel`. + - Create the `BootInfo` struct, which abstracts over the differences between BIOS and UEFI booting. This step is implemented in the `create_boot_info` function. + - Do a context switch and jump to the kernel entry point function. This involves identity-mapping the context switch function itself in both the kernel and bootloader page tables to prevent a page fault after switching page tables. This switch step is implemented in the `switch_to_kernel` and `context_switch` functions. +- As a last step after a successful build, the `builder` binary turns the compiled bootloader executable (includes the kernel) into a bootable disk image. For UEFI, this means that a FAT partition and a GPT disk image are created. For BIOS, the `llvm-objcopy` tool is used to convert the `bootloader` executable to a flat binary, as it already contains a basic MBR. -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. +### BIOS Assembly Stages -## Debugging -Set a breakpoint at address `0x7c00`. Disassemble instructions with gdb: -```bash -qemu-system-x86_64 -drive format=raw,file=target/x86_64-bootloader/release/bootloader.bin -s -S -``` -``` -(gdb) target remote: 1234 -(gdb) b *0x7c00 -(gdb) x/i $rip -``` +When you press the power button the computer loads the BIOS from some flash memory stored on the motherboard. The BIOS initializes and self tests the hardware then loads the first 512 bytes into memory from the media device (i.e. the cdrom or floppy disk). If the last two bytes equal 0xAA55 then the BIOS will jump to location 0x7C00 effectively transferring control to the bootloader. -If you use the `-enable-kvm` flag you need to use hardware breakpoints `hb`. +At this point the CPU is running in 16 bit mode, meaning only the 16 bit registers are available. Also since the BIOS only loads the first 512 bytes this means our bootloader code has to stay below that limit, otherwise we’ll hit uninitialised memory! Using [Bios interrupt calls](https://en.wikipedia.org/wiki/BIOS_interrupt_call) the bootloader prints debug information to the screen. -## Features -The bootloader crate can be configured through some cargo features: +For more information on how to write a bootloader click [here](http://3zanders.co.uk/2017/10/13/writing-a-bootloader/). The assembler files get imported through the [global_asm feature](https://doc.rust-lang.org/unstable-book/library-features/global-asm.html). The assembler syntax definition used is the one llvm uses: [GNU Assembly](http://microelectronics.esa.int/erc32/doc/as.pdf). -- `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`. -- `sse` enables sse instruction support -- 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). +The purposes of the individual assembly stages in this project are the following: +- stage_1.s: This stage initializes the stack, enables the A20 line, loads the rest of the bootloader from disk, and jumps to stage_2. +- stage_2.s: 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. +- stage_3.s: 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. -## Advanced Documentation -See these guides for advanced usage of this crate: +## Future Plans -- [Chainloading](doc/chainloading.md) -- Higher Half Kernel - TODO +- [ ] Allow to configure the desired screen resolution. Right now we just use the first available VESA screen mode on BIOS and the default GOP mode on UEFI. +- [ ] Create a `multiboot2` compatible disk image in addition to the BIOS and UEFI disk images. This would make it possible to use it on top of the GRUB bootloader. +- [ ] Rewrite most of the BIOS assembly stages in Rust. This has already started. +- [ ] Instead of linking the kernel bytes directly with the bootloader, use a filesystem (e.g. FAT) and load the kernel as a separate file. +- [ ] Stabilize the boot info format and make it possible to check the version at runtime. +- [ ] Instead of searching the bootloader source in the cargo cache on building, use the upcoming ["artifact dependencies"](https://github.com/rust-lang/cargo/issues/9096) feature of cargo to download the builder binary separately. Requires doing a boot info version check on build time. +- [ ] Transform this "Future Plans" list into issues and a roadmap. ## License 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 6295c2bb..e8b06b05 100644 --- a/build.rs +++ b/build.rs @@ -2,299 +2,436 @@ fn main() {} #[cfg(feature = "binary")] -#[derive(Default)] -struct BootloaderConfig { - physical_memory_offset: Option, - kernel_stack_address: Option, - kernel_stack_size: Option, - boot_info_address: Option, +fn main() { + binary::main(); } #[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) - }; - - let num = num.expect(&format!( - "`{}` in the kernel manifest must be an integer (is `{}`)", - key, value - )); - - if num % 0x1000 != 0 { - panic!( - "`{}` in the kernel manifest must be aligned to 4KiB (is `{}`)", - key, value - ); - } else { - num - } -} +mod binary { + use quote::quote; + use std::convert::TryInto; -#[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)) - | ("boot-info-address", 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 + pub fn main() { + use llvm_tools_build as llvm_tools; + 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"); + let (firmware, expected_target) = if cfg!(feature = "uefi_bin") { + ("UEFI", "x86_64-unknown-uefi") + } else if cfg!(feature = "bios_bin") { + ("BIOS", "x86_64-bootloader") + } else { + panic!( + "Either the `uefi_bin` or `bios_bin` feature must be enabled when \ + the `binary` feature is enabled" + ); + }; + if Path::new(&target) + .file_stem() + .expect("target has no file stem") + != expected_target + { + panic!( + "The {} bootloader must be compiled for the `{}` target.", + firmware, expected_target, + ); + } + + 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\ + Please use the `cargo builder` command for building." ); + 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 + assert!( + kernel.exists(), + "KERNEL does not exist: {}", + kernel.display() + ); + + // 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); + } + }; + + // 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()); + } + + // 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 + .status() + .expect("failed to run objcopy to strip debug symbols"); + if !exit_status.success() { + eprintln!("Error: Stripping debug symbols failed"); + process::exit(1); + } + + if cfg!(feature = "uefi_bin") { + // write file for including kernel in binary + let file_path = out_dir.join("kernel_info.rs"); + let mut file = File::create(file_path).expect("failed to create kernel_info.rs"); + let kernel_size = fs::metadata(&stripped_kernel) + .expect("Failed to read file metadata of stripped kernel") + .len(); + file.write_all( + format!( + "const KERNEL_SIZE: usize = {}; const KERNEL_BYTES: [u8; KERNEL_SIZE] = *include_bytes!(r\"{}\");", + kernel_size, + stripped_kernel.display(), + ) + .as_bytes(), + ) + .expect("write to kernel_info.rs failed"); + } + + if cfg!(feature = "bios_bin") { + // wrap the kernel executable as binary in a new ELF file + let stripped_kernel_file_name_replaced = stripped_kernel_file_name + .replace('-', "_") + .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); } - ("kernel-stack-address", Value::String(s)) => { - cfg.kernel_stack_address = Some(parse_aligned_addr(key.as_str(), &s)); + + // 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); } - ("boot-info-address", Value::String(s)) => { - cfg.boot_info_address = Some(parse_aligned_addr(key.as_str(), &s)); + + // 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 + ); + } + + // Parse configuration from the kernel's Cargo.toml + let config = match env::var("KERNEL_MANIFEST") { + Err(env::VarError::NotPresent) => { + panic!("The KERNEL_MANIFEST environment variable must be set for building the bootloader.\n\n\ + Please use `cargo builder` for building."); } - #[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" + Err(env::VarError::NotUnicode(_)) => { + panic!("The KERNEL_MANIFEST environment variable contains invalid unicode") + } + Ok(path) + if Path::new(&path).file_name().and_then(|s| s.to_str()) != Some("Cargo.toml") => + { + let err = format!( + "The given `--kernel-manifest` path `{}` does not \ + point to a `Cargo.toml`", + path, ); + quote! { compile_error!(#err) } } - #[cfg(feature = "map_physical_memory")] - ("physical-memory-offset", Value::String(s)) => { - cfg.physical_memory_offset = Some(parse_aligned_addr(key.as_str(), &s)); + Ok(path) if !Path::new(&path).exists() => { + let err = format!( + "The given `--kernel-manifest` path `{}` does not exist.", + path + ); + quote! { + compile_error!(#err) + } } - ("kernel-stack-size", Value::Integer(i)) => { - if i <= 0 { - panic!("`kernel-stack-size` in kernel manifest must be positive"); + 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"); + + if manifest + .get("dependencies") + .and_then(|d| d.get("bootloader")) + .is_some() + { + // it seems to be the correct Cargo.toml + let config_table = manifest + .get("package") + .and_then(|table| table.get("metadata")) + .and_then(|table| table.get("bootloader")) + .cloned() + .unwrap_or_else(|| toml::Value::Table(toml::map::Map::new())); + + config_table + .try_into::() + .map(|c| quote! { #c }) + .unwrap_or_else(|err| { + let err = format!( + "failed to parse bootloader config in {}:\n\n{}", + path, + err.to_string() + ); + quote! { + compile_error!(#err) + } + }) } else { - cfg.kernel_stack_size = Some(i as u64); + let err = format!( + "no bootloader dependency in {}\n\n The \ + `--kernel-manifest` path should point to the `Cargo.toml` \ + of the kernel.", + path + ); + quote! { + compile_error!(#err) + } } } - (s, _) => { - panic!( - "unknown key '{}' in kernel manifest \ - - you may need to update the bootloader crate", - s - ); + }; + + // Write config to file + 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( + quote::quote! { + mod parsed_config { + use crate::config::Config; + pub const CONFIG: Config = #config; + } } - } + .to_string() + .as_bytes(), + ) + .expect("write to bootloader_config.rs failed"); + + 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"); } -} -#[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."); + fn val_true() -> bool { + true } - 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 - assert!( - kernel.exists(), - "KERNEL does not exist: {}", - kernel.display() - ); - - // 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); - } - }; - - // 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()); + /// Must be always identical with the struct in `src/config.rs` + /// + /// This copy is needed because we can't derive Deserialize in the `src/config.rs` + /// module itself, since cargo currently unifies dependencies (the `toml` crate enables + /// serde's standard feature). Also, it allows to separate the parsing special cases + /// such as `AlignedAddress` more cleanly. + #[derive(Debug, serde::Deserialize)] + #[serde(rename_all = "kebab-case", deny_unknown_fields)] + struct ParsedConfig { + #[serde(default)] + pub map_physical_memory: bool, + #[serde(default)] + pub map_page_table_recursively: bool, + #[serde(default = "val_true")] + pub map_framebuffer: bool, + pub kernel_stack_size: Option, + pub physical_memory_offset: Option, + pub recursive_index: Option, + pub kernel_stack_address: Option, + pub boot_info_address: Option, + pub framebuffer_address: Option, } - // 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 - .status() - .expect("failed to run objcopy to strip debug symbols"); - if !exit_status.success() { - eprintln!("Error: Stripping debug symbols failed"); - process::exit(1); + /// Convert to tokens suitable for initializing the `Config` struct. + impl quote::ToTokens for ParsedConfig { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + fn optional(value: Option) -> proc_macro2::TokenStream { + value.map(|v| quote!(Some(#v))).unwrap_or(quote!(None)) + } + + let map_physical_memory = self.map_physical_memory; + let map_page_table_recursively = self.map_page_table_recursively; + let map_framebuffer = self.map_framebuffer; + let kernel_stack_size = optional(self.kernel_stack_size); + let physical_memory_offset = optional(self.physical_memory_offset); + let recursive_index = optional(self.recursive_index); + let kernel_stack_address = optional(self.kernel_stack_address); + let boot_info_address = optional(self.boot_info_address); + let framebuffer_address = optional(self.framebuffer_address); + + tokens.extend(quote! { Config { + map_physical_memory: #map_physical_memory, + map_page_table_recursively: #map_page_table_recursively, + map_framebuffer: #map_framebuffer, + kernel_stack_size: #kernel_stack_size, + physical_memory_offset: #physical_memory_offset, + recursive_index: #recursive_index, + kernel_stack_address: #kernel_stack_address, + boot_info_address: #boot_info_address, + framebuffer_address: #framebuffer_address, + }}); + } } - // wrap the kernel executable as binary in a new ELF file - let stripped_kernel_file_name_replaced = stripped_kernel_file_name - .replace('-', "_") - .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); + #[derive(Debug, Clone, Copy)] + struct AlignedAddress(u64); + + impl quote::ToTokens for AlignedAddress { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.0.to_tokens(tokens); + } } - // 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); + impl<'de> serde::Deserialize<'de> for AlignedAddress { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(AlignedAddressVisitor) + } } - // Parse the kernel's Cargo.toml which is given to us by bootimage - let mut bootloader_config = BootloaderConfig::default(); + /// Helper struct for implementing the `optional_version_deserialize` function. + struct AlignedAddressVisitor; - 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") + impl serde::de::Visitor<'_> for AlignedAddressVisitor { + type Value = AlignedAddress; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + formatter, + "a page-aligned memory address, either as integer or as decimal or hexadecimal \ + string (e.g. \"0xffff0000\"); large addresses must be given as string because \ + TOML does not support unsigned 64-bit integers" + ) } - 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 - )); + fn visit_u64(self, num: u64) -> Result + where + E: serde::de::Error, + { + if num % 0x1000 == 0 { + Ok(AlignedAddress(num)) + } else { + Err(serde::de::Error::custom(format!( + "address {:#x} is not page aligned", + num + ))) + } + } - let manifest = contents - .parse::() - .expect("failed to parse kernel's Cargo.toml"); + fn visit_i64(self, num: i64) -> Result + where + E: serde::de::Error, + { + let unsigned: u64 = num + .try_into() + .map_err(|_| serde::de::Error::custom(format!("address {} is negative", num)))?; + self.visit_u64(unsigned) + } - let table = manifest - .get("package") - .and_then(|table| table.get("metadata")) - .and_then(|table| table.get("bootloader")) - .and_then(|table| table.as_table()); + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + // ignore any `_` (used for digit grouping) + let value = &value.replace('_', ""); - if let Some(table) = table { - parse_to_config(&mut bootloader_config, table); + let num = if value.starts_with("0x") { + u64::from_str_radix(&value[2..], 16) + } else { + u64::from_str_radix(&value, 10) } + .map_err(|_err| { + serde::de::Error::custom(format!( + "string \"{}\" is not a valid memory address", + value + )) + })?; + + self.visit_u64(num) } } - - // 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 = {}; - const BOOT_INFO_ADDRESS: Option = {:?};", - bootloader_config.physical_memory_offset, - bootloader_config.kernel_stack_address, - bootloader_config.kernel_stack_size.unwrap_or(512), // size is in number of pages - bootloader_config.boot_info_address, - ) - .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 - ); - - 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"); } 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 7f2c224a..00000000 --- a/example-kernel/Cargo.lock +++ /dev/null @@ -1,30 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "bit_field" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed8765909f9009617974ab6b7d332625b320b33c326b1e9321382ef1999b5d56" - -[[package]] -name = "bitflags" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" - -[[package]] -name = "example-kernel" -version = "0.1.0" -dependencies = [ - "x86_64", -] - -[[package]] -name = "x86_64" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b871116e3c83dad0795580b10b2b1dd05cb52ec719af36180371877b09681f7f" -dependencies = [ - "bit_field", - "bitflags", -] 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/linker.ld b/linker.ld index 4e58320a..33570c2e 100644 --- a/linker.ld +++ b/linker.ld @@ -37,6 +37,7 @@ SECTIONS { *(.text .text.*) *(.rodata .rodata.*) *(.data .data.*) + *(.bss .bss.*) *(.got) . = ALIGN(512); _rest_of_bootloader_end_addr = .; diff --git a/src/e820.s b/src/asm/e820.s similarity index 100% rename from src/e820.s rename to src/asm/e820.s diff --git a/src/stage_1.s b/src/asm/stage_1.s similarity index 93% rename from src/stage_1.s rename to src/asm/stage_1.s index cbab048d..374aba5f 100644 --- a/src/stage_1.s +++ b/src/asm/stage_1.s @@ -80,6 +80,12 @@ check_int13h_extensions: load_rest_of_bootloader_from_disk: mov eax, offset _rest_of_bootloader_start_addr + mov ecx, 0 + +load_from_disk: + lea eax, _rest_of_bootloader_start_addr + add eax, ecx # add offset + # dap buffer segment mov ebx, eax shr ebx, 4 # divide by 16 @@ -91,12 +97,21 @@ load_rest_of_bootloader_from_disk: mov [dap_buffer_addr], ax mov eax, offset _rest_of_bootloader_start_addr + add eax, ecx # add offset # number of disk blocks to load mov ebx, offset _rest_of_bootloader_end_addr sub ebx, eax # end - start + jz load_from_disk_done shr ebx, 9 # divide by 512 (block size) + cmp ebx, 127 + jle .continue_loading_from_disk + mov ebx, 127 +.continue_loading_from_disk: mov [dap_blocks], bx + # increase offset + shl ebx, 9 + add ecx, ebx # number of start block mov ebx, offset _start @@ -108,7 +123,10 @@ load_rest_of_bootloader_from_disk: mov ah, 0x42 int 0x13 jc rest_of_bootloader_load_failed - + + jmp load_from_disk + +load_from_disk_done: # reset segment to 0 mov word ptr [dap_buffer_seg], 0 diff --git a/src/stage_2.s b/src/asm/stage_2.s similarity index 70% rename from src/stage_2.s rename to src/asm/stage_2.s index f5ad1141..b3704f66 100644 --- a/src/stage_2.s +++ b/src/asm/stage_2.s @@ -87,7 +87,7 @@ create_memory_map: call do_e820 video_mode_config: - call config_video_mode + call vesa enter_protected_mode_again: cli @@ -103,3 +103,70 @@ enter_protected_mode_again: spin32: jmp spin32 + + + +# 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/stage_3.s b/src/asm/stage_3.s similarity index 81% rename from src/stage_3.s rename to src/asm/stage_3.s index 112c68df..200dc1fc 100644 --- a/src/stage_3.s +++ b/src/asm/stage_3.s @@ -13,9 +13,6 @@ stage_3: mov es, bx # set extra segment mov ss, bx # set stack segment - mov si, offset boot_third_stage_str - call vga_println - check_cpu: call check_cpuid call check_long_mode @@ -44,40 +41,14 @@ set_up_page_tables: or eax, (1 | 2) mov [_p3], eax # p2 - mov eax, offset _p1 - or eax, (1 | 2) - mov [_p2], eax - mov eax, (0x400000 | 1 | 2 | (1 << 7)) - mov ecx, 2 - mov edx, offset _kernel_size - add edx, 0x400000 # start address - add edx, 0x200000 - 1 # align up - shr edx, 12 + 9 # end huge page number + mov eax, (1 | 2 | (1 << 7)) + mov ecx, 0 map_p2_table: mov [_p2 + ecx * 8], eax add eax, 0x200000 add ecx, 1 - cmp ecx, edx + cmp ecx, 512 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 - mov eax, offset __page_table_start - and eax, 0xfffff000 - or eax, (1 | 2) - mov ecx, offset __page_table_start - shr ecx, 12 # start page number - mov edx, offset __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 @@ -194,6 +165,5 @@ 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/asm/vesa.s b/src/asm/vesa.s new file mode 100644 index 00000000..eaa59241 --- /dev/null +++ b/src/asm/vesa.s @@ -0,0 +1,329 @@ +# Code originally taken from https://gitlab.redox-os.org/redox-os/bootloader +# +# Copyright (c) 2017 Redox OS, licensed under MIT License + +.section .boot, "awx" +.intel_syntax noprefix +.code16 + +vesa: +vesa_getcardinfo: + mov ax, 0x4F00 + mov di, offset VBECardInfo + int 0x10 + cmp ax, 0x4F + je vesa_findmode + mov eax, 1 + ret +vesa_resetlist: + # if needed, reset mins/maxes/stuff + xor cx, cx + mov [vesa_minx], cx + mov [vesa_miny], cx + mov [config_xres], cx + mov [config_yres], cx +vesa_findmode: + mov si, [VBECardInfo_videomodeptr] + mov ax, [VBECardInfo_videomodeptr+2] + mov fs, ax + sub si, 2 +vesa_searchmodes: + add si, 2 + mov cx, fs:[si] + cmp cx, 0xFFFF + jne vesa_getmodeinfo + cmp word ptr [vesa_goodmode], 0 + je vesa_resetlist + jmp vesa_findmode +vesa_getmodeinfo: + push esi + mov [vesa_currentmode], cx + mov ax, 0x4F01 + mov di, offset VBEModeInfo + int 0x10 + pop esi + cmp ax, 0x4F + je vesa_foundmode + mov eax, 1 + ret +vesa_foundmode: + # check minimum values, really not minimums from an OS perspective but ugly for users + cmp byte ptr [VBEModeInfo_bitsperpixel], 32 + jb vesa_searchmodes +vesa_testx: + mov cx, [VBEModeInfo_xresolution] + cmp word ptr [config_xres], 0 + je vesa_notrequiredx + cmp cx, [config_xres] + je vesa_testy + jmp vesa_searchmodes +vesa_notrequiredx: + cmp cx, [vesa_minx] + jb vesa_searchmodes +vesa_testy: + mov cx, [VBEModeInfo_yresolution] + cmp word ptr [config_yres], 0 + je vesa_notrequiredy + cmp cx, [config_yres] + jne vesa_searchmodes # as if there weren't enough warnings, USE WITH CAUTION + cmp word ptr [config_xres], 0 + jnz vesa_setmode + jmp vesa_testgood +vesa_notrequiredy: + cmp cx, [vesa_miny] + jb vesa_searchmodes +vesa_testgood: + mov al, 13 + call print_char + mov cx, [vesa_currentmode] + mov [vesa_goodmode], cx + push esi + # call print_dec + # mov al, ':' + # call print_char + mov cx, [VBEModeInfo_xresolution] + call print_dec + mov al, 'x' + call print_char + mov cx, [VBEModeInfo_yresolution] + call print_dec + mov al, '@' + call print_char + xor ch, ch + mov cl, [VBEModeInfo_bitsperpixel] + call print_dec +vesa_confirm_mode: + mov si, offset vesa_modeok + call print + # xor ax, ax + # int 0x16 # read key press + pop esi + cmp al, al # originally `cmp al, 'y'` to compare key press + je vesa_setmode + cmp al, 's' + je vesa_savemode + jmp vesa_searchmodes +vesa_savemode: + mov cx, [VBEModeInfo_xresolution] + mov [config_xres], cx + mov cx, [VBEModeInfo_yresolution] + mov [config_yres], cx + # call save_config +vesa_setmode: + mov bx, [vesa_currentmode] + cmp bx, 0 + je vesa_nomode + or bx, 0x4000 + mov ax, 0x4F02 + int 0x10 +vesa_nomode: + cmp ax, 0x4F + je vesa_returngood + mov eax, 1 + ret +vesa_returngood: + xor eax, eax + ret + +vesa_minx: .2byte 640 +vesa_miny: .2byte 480 + +vesa_modeok: + .ascii ": Is this OK? (s)ave/(y)es/(n)o " + .byte 8,8,8,8,0 + +vesa_goodmode: .2byte 0 +vesa_currentmode: .2byte 0 +# useful functions + +# print a number in decimal +# IN +# cx: the number +# CLOBBER +# al, cx, si +print_dec: + mov si, offset print_dec_number +print_dec_clear: + mov al, '0' + mov [si], al + inc si + cmp si, offset print_dec_numberend + jb print_dec_clear + dec si + call convert_dec + mov si, offset print_dec_number +print_dec_lp: + lodsb + cmp si, offset print_dec_numberend + jae print_dec_end + cmp al, '0' + jbe print_dec_lp +print_dec_end: + dec si + call print + ret + +print_dec_number: .skip 7, 0 +print_dec_numberend: .skip 1, 0 + +convert_dec: + dec si + mov bx, si # place to convert into must be in si, number to convert must be in cx +convert_dec_cnvrt: + mov si, bx + sub si, 4 +convert_dec_ten4: inc si + cmp cx, 10000 + jb convert_dec_ten3 + sub cx, 10000 + inc byte ptr [si] + jmp convert_dec_cnvrt +convert_dec_ten3: inc si + cmp cx, 1000 + jb convert_dec_ten2 + sub cx, 1000 + inc byte ptr [si] + jmp convert_dec_cnvrt +convert_dec_ten2: inc si + cmp cx, 100 + jb convert_dec_ten1 + sub cx, 100 + inc byte ptr [si] + jmp convert_dec_cnvrt +convert_dec_ten1: inc si + cmp cx, 10 + jb convert_dec_ten0 + sub cx, 10 + inc byte ptr [si] + jmp convert_dec_cnvrt +convert_dec_ten0: inc si + cmp cx, 1 + jb convert_dec_return + sub cx, 1 + inc byte ptr [si] + jmp convert_dec_cnvrt +convert_dec_return: + ret + + +VBECardInfo: + VBECardInfo_signature: .skip 4, 0 + VBECardInfo_version: .skip 2, 0 + VBECardInfo_oemstring: .skip 4, 0 + VBECardInfo_capabilities: .skip 4, 0 + VBECardInfo_videomodeptr: .skip 4, 0 + VBECardInfo_totalmemory: .skip 2, 0 + VBECardInfo_oemsoftwarerev: .skip 2, 0 + VBECardInfo_oemvendornameptr: .skip 4, 0 + VBECardInfo_oemproductnameptr: .skip 4, 0 + VBECardInfo_oemproductrevptr: .skip 4, 0 + VBECardInfo_reserved: .skip 222, 0 + VBECardInfo_oemdata: .skip 256, 0 + +VBEModeInfo: + VBEModeInfo_attributes: .skip 2, 0 + VBEModeInfo_winA: .skip 1, 0 + VBEModeInfo_winB: .skip 1, 0 + VBEModeInfo_granularity: .skip 2, 0 + VBEModeInfo_winsize: .skip 2, 0 + VBEModeInfo_segmentA: .skip 2, 0 + VBEModeInfo_segmentB: .skip 2, 0 + VBEModeInfo_winfuncptr: .skip 4, 0 + VBEModeInfo_bytesperscanline: .skip 2, 0 + VBEModeInfo_xresolution: .skip 2, 0 + VBEModeInfo_yresolution: .skip 2, 0 + VBEModeInfo_xcharsize: .skip 1, 0 + VBEModeInfo_ycharsize: .skip 1, 0 + VBEModeInfo_numberofplanes: .skip 1, 0 + VBEModeInfo_bitsperpixel: .skip 1, 0 + VBEModeInfo_numberofbanks: .skip 1, 0 + VBEModeInfo_memorymodel: .skip 1, 0 + VBEModeInfo_banksize: .skip 1, 0 + VBEModeInfo_numberofimagepages: .skip 1, 0 + VBEModeInfo_unused: .skip 1, 0 + VBEModeInfo_redmasksize: .skip 1, 0 + VBEModeInfo_redfieldposition: .skip 1, 0 + VBEModeInfo_greenmasksize: .skip 1, 0 + VBEModeInfo_greenfieldposition: .skip 1, 0 + VBEModeInfo_bluemasksize: .skip 1, 0 + VBEModeInfo_bluefieldposition: .skip 1, 0 + VBEModeInfo_rsvdmasksize: .skip 1, 0 + VBEModeInfo_rsvdfieldposition: .skip 1, 0 + VBEModeInfo_directcolormodeinfo: .skip 1, 0 + VBEModeInfo_physbaseptr: .skip 4, 0 + VBEModeInfo_offscreenmemoryoffset: .skip 4, 0 + VBEModeInfo_offscreenmemsize: .skip 2, 0 + VBEModeInfo_reserved: .skip 206, 0 + +# VBE.ModeAttributes: +# ModeAttributes_available equ 1 << 0 +# ModeAttributes_bios equ 1 << 2 +# ModeAttributes_color equ 1 << 3 +# ModeAttributes_graphics equ 1 << 4 +# ModeAttributes_vgacompatible equ 1 << 5 +# ModeAttributes_notbankable equ 1 << 6 +# ModeAttributes_linearframebuffer equ 1 << 7 + +VBEEDID: + VBEEDID_header: .skip 8, 0 + VBEEDID_manufacturer: .skip 2, 0 + VBEEDID_productid: .skip 2, 0 + VBEEDID_serial: .skip 4, 0 + VBEEDID_manufactureweek: .skip 1, 0 + VBEEDID_manufactureyear: .skip 1, 0 + VBEEDID_version: .skip 1, 0 + VBEEDID_revision: .skip 1, 0 + VBEEDID_input: .skip 1, 0 + VBEEDID_horizontalsize: .skip 1, 0 + VBEEDID_verticalsize: .skip 1, 0 + VBEEDID_gamma: .skip 1, 0 + VBEEDID_displaytype: .skip 1, 0 + VBEEDID_chromaticity: .skip 10, 0 + VBEEDID_timingI: .skip 1, 0 + VBEEDID_timingII: .skip 1, 0 + VBEEDID_timingreserved: .skip 1, 0 + VBEEDID_standardtiming: .skip 16, 0 # format: db (horizontal-248)/8, aspectratio | verticalfrequency - 60 + # VBEEDID_standardtiming_aspect.16.10 equ 0 # mul horizontal by 10, shr 4 to get vertical resolution + # VBEEDID_standardtiming_aspect.4.3 equ 1 << 6 # mul horizontal by 3, shr 2 to get vertical resolution + # VBEEDID_standardtiming_aspect.5.4 equ 2 << 6 # shl horizontal by 2, div by 5 to get vertical resolution + # VBEEDID_standardtiming_aspect.16.9 equ 3 << 6 # mul horizontal by 9, shr by 4 to get vertical resolution + VBEEDID_descriptorblock1: .skip 18, 0 + VBEEDID_descriptorblock2: .skip 18, 0 + VBEEDID_descriptorblock3: .skip 18, 0 + VBEEDID_descriptorblock4: .skip 18, 0 + VBEEDID_extensionflag: .skip 1, 0 + VBEEDID_checksum: .skip 1, 0 + +config: + config_xres: .2byte 0 + config_yres: .2byte 0 + +# print a string +# IN +# si: points at zero-terminated String +# CLOBBER +# si, ax +print: + pushf + cld +print_loop: + lodsb + test al, al + jz print_done + call print_char + jmp print_loop +print_done: + popf + ret + + +# print a character +# IN +# al: character to print +print_char: + pusha + mov bx, 7 + mov ah, 0x0e + int 0x10 + popa + ret diff --git a/src/bin/bios.rs b/src/bin/bios.rs new file mode 100644 index 00000000..f0a2954c --- /dev/null +++ b/src/bin/bios.rs @@ -0,0 +1,287 @@ +#![feature(lang_items)] +#![feature(global_asm)] +#![feature(llvm_asm)] +#![feature(asm)] +#![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::{ + binary::SystemInfo, + boot_info::{FrameBufferInfo, PixelFormat}, +}; +use core::panic::PanicInfo; +use core::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}; + +global_asm!(include_str!("../asm/stage_1.s")); +global_asm!(include_str!("../asm/stage_2.s")); +global_asm!(include_str!("../asm/vesa.s")); +global_asm!(include_str!("../asm/e820.s")); +global_asm!(include_str!("../asm/stage_3.s")); + +// values defined in `vesa.s` +extern "C" { + static VBEModeInfo_physbaseptr: u32; + static VBEModeInfo_bytesperscanline: u16; + static VBEModeInfo_xresolution: u16; + static VBEModeInfo_yresolution: u16; + static VBEModeInfo_bitsperpixel: u8; + static VBEModeInfo_redfieldposition: u8; + static VBEModeInfo_greenfieldposition: u8; + static VBEModeInfo_bluefieldposition: u8; +} + +// 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; +} + +#[no_mangle] +pub unsafe extern "C" fn stage_4() -> ! { + // Set stack segment + llvm_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 + + bootloader_main( + PhysAddr::new(kernel_start), + kernel_size, + VirtAddr::new(memory_map_addr), + memory_map_entry_count, + ) +} + +fn bootloader_main( + kernel_start: PhysAddr, + kernel_size: u64, + memory_map_addr: VirtAddr, + memory_map_entry_count: u64, +) -> ! { + use bootloader::binary::{ + bios::memory_descriptor::E820MemoryRegion, legacy_memory_region::LegacyFrameAllocator, + }; + + let e820_memory_map = { + let ptr = usize_from(memory_map_addr.as_u64()) as *const E820MemoryRegion; + unsafe { slice::from_raw_parts(ptr, usize_from(memory_map_entry_count)) } + }; + let max_phys_addr = e820_memory_map + .iter() + .map(|r| r.start_addr + r.len) + .max() + .expect("no physical memory regions found"); + + let mut frame_allocator = { + let kernel_end = PhysFrame::containing_address(kernel_start + kernel_size - 1u64); + let next_free = kernel_end + 1; + LegacyFrameAllocator::new_starting_at(next_free, e820_memory_map.iter().copied()) + }; + + // We identity-map 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 gigabyte is already identity-mapped) + { + let start_frame: PhysFrame = + PhysFrame::containing_address(PhysAddr::new(4096 * 512 * 512)); + let end_frame = PhysFrame::containing_address(PhysAddr::new(max_phys_addr - 1)); + for frame in PhysFrame::range_inclusive(start_frame, end_frame) { + unsafe { + bootloader_page_table + .identity_map( + frame, + PageTableFlags::PRESENT | PageTableFlags::WRITABLE, + &mut frame_allocator, + ) + .unwrap() + .flush() + }; + } + } + + let framebuffer_addr = PhysAddr::new(unsafe { VBEModeInfo_physbaseptr }.into()); + let mut error = None; + let framebuffer_info = unsafe { + let framebuffer_size = + usize::from(VBEModeInfo_yresolution) * usize::from(VBEModeInfo_bytesperscanline); + let bytes_per_pixel = VBEModeInfo_bitsperpixel / 8; + init_logger( + framebuffer_addr, + framebuffer_size.into(), + VBEModeInfo_xresolution.into(), + VBEModeInfo_yresolution.into(), + bytes_per_pixel.into(), + (VBEModeInfo_bytesperscanline / u16::from(bytes_per_pixel)).into(), + match ( + VBEModeInfo_redfieldposition, + VBEModeInfo_greenfieldposition, + VBEModeInfo_bluefieldposition, + ) { + (0, 8, 16) => PixelFormat::RGB, + (16, 8, 0) => PixelFormat::BGR, + (r, g, b) => { + error = Some(("invalid rgb field positions", r, g, b)); + PixelFormat::RGB // default to RBG so that we can print something + } + }, + ) + }; + + log::info!("BIOS boot"); + + if let Some((msg, r, g, b)) = error { + panic!("{}: r: {}, g: {}, b: {}", msg, r, g, b); + } + + let page_tables = create_page_tables(&mut frame_allocator); + + let kernel = { + let ptr = kernel_start.as_u64() as *const u8; + unsafe { slice::from_raw_parts(ptr, usize_from(kernel_size)) } + }; + + let system_info = SystemInfo { + framebuffer_addr, + framebuffer_info, + rsdp_addr: detect_rsdp(), + }; + + bootloader::binary::load_and_switch_to_kernel( + kernel, + frame_allocator, + page_tables, + system_info, + ); +} + +fn init_logger( + framebuffer_start: PhysAddr, + framebuffer_size: usize, + horizontal_resolution: usize, + vertical_resolution: usize, + bytes_per_pixel: usize, + stride: usize, + pixel_format: PixelFormat, +) -> FrameBufferInfo { + let ptr = framebuffer_start.as_u64() as *mut u8; + let slice = unsafe { slice::from_raw_parts_mut(ptr, framebuffer_size) }; + + let info = bootloader::boot_info::FrameBufferInfo { + byte_len: framebuffer_size, + horizontal_resolution, + vertical_resolution, + bytes_per_pixel, + stride, + pixel_format, + }; + + bootloader::binary::init_logger(slice, info); + + info +} + +/// Creates page table abstraction types for both the bootloader and kernel page tables. +fn create_page_tables( + frame_allocator: &mut impl FrameAllocator, +) -> bootloader::binary::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 = 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::binary::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 { + unsafe fn map_physical_region( + &self, + physical_address: usize, + size: usize, + ) -> PhysicalMapping { + PhysicalMapping { + physical_start: physical_address, + virtual_start: NonNull::new(physical_address as *mut _).unwrap(), + region_length: size, + mapped_length: size, + handler: Self, + } + } + + fn unmap_physical_region(&self, _region: &PhysicalMapping) {} + } + + unsafe { + Rsdp::search_for_on_bios(IdentityMapped) + .ok() + .map(|mapping| PhysAddr::new(mapping.physical_start as u64)) + } +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + unsafe { + bootloader::binary::logger::LOGGER + .get() + .map(|l| l.force_unlock()) + }; + log::error!("{}", info); + loop { + unsafe { asm!("cli; hlt") }; + } +} diff --git a/src/bin/builder.rs b/src/bin/builder.rs new file mode 100644 index 00000000..37d39041 --- /dev/null +++ b/src/bin/builder.rs @@ -0,0 +1,376 @@ +use anyhow::{anyhow, bail, Context}; +use argh::FromArgs; +use bootloader::disk_image::create_disk_image; +use std::{ + convert::TryFrom, + fs::{self, File}, + io::{self, Seek}, + path::{Path, PathBuf}, + process::Command, + str::FromStr, +}; + +type ExitCode = i32; + +#[derive(FromArgs)] +/// Build the bootloader +struct BuildArguments { + /// path to the `Cargo.toml` of the kernel + #[argh(option)] + kernel_manifest: PathBuf, + + /// path to the kernel ELF binary + #[argh(option)] + kernel_binary: PathBuf, + + /// which firmware interface to build + #[argh(option, default = "Firmware::All")] + firmware: Firmware, + + /// whether to run the resulting binary in QEMU + #[argh(switch)] + run: bool, + + /// suppress stdout output + #[argh(switch)] + quiet: bool, + + /// build the bootloader with the given cargo features + #[argh(option)] + features: Vec, + + /// use the given path as target directory + #[argh(option)] + target_dir: Option, + + /// place the output binaries at the given path + #[argh(option)] + out_dir: Option, +} + +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +enum Firmware { + Bios, + Uefi, + All, +} + +impl FromStr for Firmware { + type Err = FirmwareParseError; + + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "bios" => Ok(Firmware::Bios), + "uefi" => Ok(Firmware::Uefi), + "all" => Ok(Firmware::All), + _other => Err(FirmwareParseError), + } + } +} + +impl Firmware { + fn uefi(&self) -> bool { + match self { + Firmware::Bios => false, + Firmware::Uefi | Firmware::All => true, + } + } + + fn bios(&self) -> bool { + match self { + Firmware::Bios | Firmware::All => true, + Firmware::Uefi => false, + } + } +} + +/// Firmware must be one of `uefi`, `bios`, or `all`. +#[derive(Debug, displaydoc::Display, Eq, PartialEq, Copy, Clone)] +struct FirmwareParseError; + +fn main() -> anyhow::Result<()> { + let args: BuildArguments = argh::from_env(); + + if args.firmware.uefi() { + let build_or_run = if args.run { "run" } else { "build" }; + let mut cmd = Command::new(env!("CARGO")); + cmd.arg(build_or_run).arg("--bin").arg("uefi"); + cmd.arg("--release"); + cmd.arg("--target").arg("x86_64-unknown-uefi"); + cmd.arg("--features") + .arg(args.features.join(" ") + " uefi_bin"); + cmd.arg("-Zbuild-std=core"); + cmd.arg("-Zbuild-std-features=compiler-builtins-mem"); + if let Some(target_dir) = &args.target_dir { + cmd.arg("--target-dir").arg(target_dir); + } + if args.quiet { + cmd.arg("--quiet"); + } + cmd.env("KERNEL", &args.kernel_binary); + cmd.env("KERNEL_MANIFEST", &args.kernel_manifest); + assert!(cmd.status()?.success()); + + // Retrieve binary paths + cmd.arg("--message-format").arg("json"); + let output = cmd + .output() + .context("failed to execute kernel build with json output")?; + if !output.status.success() { + return Err(anyhow!("{}", String::from_utf8_lossy(&output.stderr))); + } + let mut executables = Vec::new(); + for line in String::from_utf8(output.stdout) + .context("build JSON output is not valid UTF-8")? + .lines() + { + let mut artifact = json::parse(line).context("build JSON output is not valid JSON")?; + if let Some(executable) = artifact["executable"].take_string() { + executables.push(PathBuf::from(executable)); + } + } + + assert_eq!(executables.len(), 1); + let executable_path = executables.pop().unwrap(); + + let executable_name = executable_path + .file_stem() + .and_then(|stem| stem.to_str()) + .ok_or_else(|| { + anyhow!( + "executable path `{}` has invalid file stem", + executable_path.display() + ) + })?; + let kernel_name = args + .kernel_binary + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| { + anyhow!( + "kernel binary path `{}` has invalid file name", + args.kernel_binary.display() + ) + })?; + + if let Some(out_dir) = &args.out_dir { + let efi_file = out_dir.join(format!("boot-{}-{}.efi", executable_name, kernel_name)); + create_uefi_disk_image(&executable_path, &efi_file) + .context("failed to create UEFI disk image")?; + } + } + + if args.firmware.bios() { + let mut cmd = Command::new(env!("CARGO")); + cmd.arg("build").arg("--bin").arg("bios"); + cmd.arg("--profile").arg("release"); + cmd.arg("-Z").arg("unstable-options"); + cmd.arg("--target").arg("x86_64-bootloader.json"); + cmd.arg("--features") + .arg(args.features.join(" ") + " bios_bin"); + cmd.arg("-Zbuild-std=core"); + cmd.arg("-Zbuild-std-features=compiler-builtins-mem"); + if let Some(target_dir) = &args.target_dir { + cmd.arg("--target-dir").arg(target_dir); + } + if args.quiet { + cmd.arg("--quiet"); + } + cmd.env("KERNEL", &args.kernel_binary); + cmd.env("KERNEL_MANIFEST", &args.kernel_manifest); + cmd.env("RUSTFLAGS", "-C opt-level=s"); + assert!(cmd.status()?.success()); + + // Retrieve binary paths + cmd.arg("--message-format").arg("json"); + let output = cmd + .output() + .context("failed to execute kernel build with json output")?; + if !output.status.success() { + return Err(anyhow!("{}", String::from_utf8_lossy(&output.stderr))); + } + let mut executables = Vec::new(); + for line in String::from_utf8(output.stdout) + .context("build JSON output is not valid UTF-8")? + .lines() + { + let mut artifact = json::parse(line).context("build JSON output is not valid JSON")?; + if let Some(executable) = artifact["executable"].take_string() { + executables.push(PathBuf::from(executable)); + } + } + + assert_eq!(executables.len(), 1); + let executable_path = executables.pop().unwrap(); + let executable_name = executable_path.file_name().unwrap().to_str().unwrap(); + let kernel_name = args.kernel_binary.file_name().unwrap().to_str().unwrap(); + let mut output_bin_path = executable_path + .parent() + .unwrap() + .join(format!("boot-{}-{}.img", executable_name, kernel_name)); + + create_disk_image(&executable_path, &output_bin_path) + .context("Failed to create bootable disk image")?; + + if let Some(out_dir) = &args.out_dir { + let file = out_dir.join(output_bin_path.file_name().unwrap()); + fs::copy(output_bin_path, &file)?; + output_bin_path = file; + } + + if !args.quiet { + println!( + "Created bootable disk image at {}", + output_bin_path.display() + ); + } + + if args.run { + bios_run(&output_bin_path)?; + } + } + + Ok(()) +} + +fn create_uefi_disk_image(executable_path: &Path, efi_file: &Path) -> anyhow::Result<()> { + fs::copy(&executable_path, &efi_file).context("failed to copy efi file to out dir")?; + + let efi_size = fs::metadata(&efi_file) + .context("failed to read metadata of efi file")? + .len(); + + // create fat partition + let fat_file_path = { + const MB: u64 = 1024 * 1024; + + let fat_path = efi_file.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")?; + let efi_size_rounded = ((efi_size - 1) / MB + 1) * MB; + fat_file + .set_len(efi_size_rounded) + .context("failed to set UEFI FAT file length")?; + + // create new FAT partition + let format_options = fatfs::FormatVolumeOptions::new().volume_label(*b"FOOO "); + fatfs::format_volume(&fat_file, format_options) + .context("Failed to format UEFI FAT file")?; + + // copy EFI file 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(); + root_dir.create_dir("efi")?; + root_dir.create_dir("efi/boot")?; + let mut bootx64 = root_dir.create_file("efi/boot/bootx64.efi")?; + bootx64.truncate()?; + io::copy(&mut fs::File::open(&executable_path)?, &mut bootx64)?; + + fat_path + }; + + // create gpt disk + { + let image_path = efi_file.with_extension("img"); + let mut image = fs::OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&image_path) + .context("failed to create UEFI disk image")?; + + let partition_size: u64 = fs::metadata(&fat_file_path) + .context("failed to read metadata of UEFI FAT partition")? + .len(); + let image_size = partition_size + 1024 * 64; + image + .set_len(image_size) + .context("failed to set length of UEFI disk image")?; + + // Create a protective MBR at LBA0 + let mbr = gpt::mbr::ProtectiveMBR::with_lb_size( + u32::try_from((image_size / 512) - 1).unwrap_or(0xFF_FF_FF_FF), + ); + mbr.overwrite_lba0(&mut image) + .context("failed to write protective MBR")?; + + // create new GPT in image file + let block_size = gpt::disk::LogicalBlockSize::Lb512; + let block_size_bytes: u64 = block_size.into(); + let mut disk = gpt::GptConfig::new() + .writable(true) + .initialized(false) + .logical_block_size(block_size) + .create_from_device(Box::new(&mut image), None) + .context("failed to open UEFI disk image")?; + disk.update_partitions(Default::default()) + .context("failed to initialize GPT partition table")?; + + // add add EFI system partition + let partition_id = disk + .add_partition("boot", partition_size, gpt::partition_types::EFI, 0) + .context("failed to add boot partition")?; + + let partition = disk + .partitions() + .get(&partition_id) + .ok_or_else(|| anyhow!("Partition doesn't exist after adding it"))?; + let created_partition_size: u64 = + (partition.last_lba - partition.first_lba + 1u64) * block_size_bytes; + if created_partition_size != partition_size { + bail!( + "Created partition has invalid size (size is {:?}, expected {})", + created_partition_size, + partition_size + ); + } + let start_offset = partition + .bytes_start(block_size) + .context("failed to retrieve partition start offset")?; + + // Write the partition table + disk.write() + .context("failed to write GPT partition table to UEFI image file")?; + + image + .seek(io::SeekFrom::Start(start_offset)) + .context("failed to seek to boot partiiton start")?; + let bytes_written = io::copy( + &mut File::open(&fat_file_path).context("failed to open fat image")?, + &mut image, + ) + .context("failed to write boot partition content")?; + if bytes_written != partition_size { + bail!( + "Invalid number of partition bytes written (expected {}, got {})", + partition_size, + bytes_written + ); + } + } + + Ok(()) +} + +fn bios_run(bin_path: &Path) -> anyhow::Result> { + let mut qemu = Command::new("qemu-system-x86_64"); + qemu.arg("-drive") + .arg(format!("format=raw,file={}", bin_path.display())); + qemu.arg("-s"); + qemu.arg("--no-reboot"); + println!("{:?}", qemu); + let exit_status = qemu.status()?; + let ret = if exit_status.success() { + None + } else { + exit_status.code() + }; + Ok(ret) +} diff --git a/src/bin/uefi.rs b/src/bin/uefi.rs new file mode 100644 index 00000000..916e28a5 --- /dev/null +++ b/src/bin/uefi.rs @@ -0,0 +1,185 @@ +#![no_std] +#![no_main] +#![feature(abi_efiapi)] +#![feature(asm)] +#![feature(maybe_uninit_extra)] +#![deny(unsafe_op_in_unsafe_fn)] + +// Defines the constants `KERNEL_BYTES` (array of `u8`) and `KERNEL_SIZE` (`usize`). +include!(concat!(env!("OUT_DIR"), "/kernel_info.rs")); + +static KERNEL: PageAligned<[u8; KERNEL_SIZE]> = PageAligned(KERNEL_BYTES); + +#[repr(align(4096))] +struct PageAligned(T); + +use bootloader::{ + binary::{legacy_memory_region::LegacyFrameAllocator, SystemInfo}, + boot_info::FrameBufferInfo, +}; +use core::{mem, panic::PanicInfo, slice}; +use uefi::{ + prelude::{entry, Boot, Handle, ResultExt, Status, SystemTable}, + proto::console::gop::{GraphicsOutput, PixelFormat}, + table::boot::{MemoryDescriptor, MemoryType}, +}; +use x86_64::{ + structures::paging::{FrameAllocator, OffsetPageTable, PageTable, PhysFrame, Size4KiB}, + PhysAddr, VirtAddr, +}; + +#[entry] +fn efi_main(image: Handle, st: SystemTable) -> Status { + let (framebuffer_addr, framebuffer_info) = init_logger(&st); + log::info!("Hello World from UEFI bootloader!"); + log::info!("Using framebuffer at {:#x}", framebuffer_addr); + + let mmap_storage = { + let max_mmap_size = + st.boot_services().memory_map_size() + 8 * mem::size_of::(); + let ptr = st + .boot_services() + .allocate_pool(MemoryType::LOADER_DATA, max_mmap_size)? + .log(); + unsafe { slice::from_raw_parts_mut(ptr, max_mmap_size) } + }; + + log::trace!("exiting boot services"); + let (system_table, memory_map) = st + .exit_boot_services(image, mmap_storage) + .expect_success("Failed to exit boot services"); + + let mut frame_allocator = LegacyFrameAllocator::new(memory_map.copied()); + + let page_tables = create_page_tables(&mut frame_allocator); + + let system_info = SystemInfo { + framebuffer_addr, + framebuffer_info, + rsdp_addr: { + use uefi::table::cfg; + let mut config_entries = system_table.config_table().iter(); + config_entries + .find(|entry| matches!(entry.guid, cfg::ACPI_GUID | cfg::ACPI2_GUID)) + .map(|entry| PhysAddr::new(entry.address as u64)) + }, + }; + + bootloader::binary::load_and_switch_to_kernel( + &KERNEL.0, + frame_allocator, + page_tables, + system_info, + ); +} + +/// Creates page table abstraction types for both the bootloader and kernel page tables. +fn create_page_tables( + frame_allocator: &mut impl FrameAllocator, +) -> bootloader::binary::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 first entry (we don't need to access more than 512 GiB; also, some UEFI + // implementations seem to create an level 4 table entry 0 in all slots) + new_table[0] = old_table[0].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::binary::PageTables { + bootloader: bootloader_page_table, + kernel: kernel_page_table, + kernel_level_4_frame, + } +} + +fn init_logger(st: &SystemTable) -> (PhysAddr, FrameBufferInfo) { + let gop = st + .boot_services() + .locate_protocol::() + .expect_success("failed to locate gop"); + let gop = unsafe { &mut *gop.get() }; + + 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(), + horizontal_resolution: mode_info.resolution().0, + vertical_resolution: mode_info.resolution().1, + pixel_format: match mode_info.pixel_format() { + PixelFormat::RGB => bootloader::boot_info::PixelFormat::BGR, + PixelFormat::BGR => bootloader::boot_info::PixelFormat::BGR, + PixelFormat::Bitmask | PixelFormat::BltOnly => { + panic!("Bitmask and BltOnly framebuffers are not supported") + } + }, + bytes_per_pixel: 4, + stride: mode_info.stride(), + }; + + log::info!("UEFI boot"); + + bootloader::binary::init_logger(slice, info); + + (PhysAddr::new(framebuffer.as_mut_ptr() as u64), info) +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + unsafe { + bootloader::binary::logger::LOGGER + .get() + .map(|l| l.force_unlock()) + }; + log::error!("{}", info); + loop { + unsafe { asm!("cli; hlt") }; + } +} diff --git a/src/binary/bios/memory_descriptor.rs b/src/binary/bios/memory_descriptor.rs new file mode 100644 index 00000000..fcd80c12 --- /dev/null +++ b/src/binary/bios/memory_descriptor.rs @@ -0,0 +1,32 @@ +use crate::{binary::legacy_memory_region::LegacyMemoryRegion, boot_info::MemoryRegionKind}; +use x86_64::PhysAddr; + +impl LegacyMemoryRegion for E820MemoryRegion { + fn start(&self) -> PhysAddr { + PhysAddr::new(self.start_addr) + } + + fn len(&self) -> u64 { + self.len + } + + fn kind(&self) -> MemoryRegionKind { + match self.region_type { + 1 => MemoryRegionKind::Usable, + other => MemoryRegionKind::UnknownBios(other), + } + } +} + +/// 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 E820MemoryRegion { + pub start_addr: u64, + pub len: u64, + pub region_type: u32, + pub acpi_extended_attributes: u32, +} diff --git a/src/binary/bios/mod.rs b/src/binary/bios/mod.rs new file mode 100644 index 00000000..3a4aeab9 --- /dev/null +++ b/src/binary/bios/mod.rs @@ -0,0 +1,2 @@ +/// Provides an abstraction type for a BIOS-provided memory region. +pub mod memory_descriptor; diff --git a/src/binary/legacy_memory_region.rs b/src/binary/legacy_memory_region.rs new file mode 100644 index 00000000..831f9a94 --- /dev/null +++ b/src/binary/legacy_memory_region.rs @@ -0,0 +1,203 @@ +use crate::boot_info::{MemoryRegion, MemoryRegionKind}; +use core::mem::MaybeUninit; +use x86_64::{ + structures::paging::{FrameAllocator, PhysFrame, Size4KiB}, + PhysAddr, +}; + +/// 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 the type of the region, e.g. whether it is usable or reserved. + fn kind(&self) -> MemoryRegionKind; +} + +/// 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, +} + +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`. + pub fn new(memory_map: I) -> Self { + // skip frame 0 because the rust core library does not see 0 as a valid address + let start_frame = PhysFrame::containing_address(PhysAddr::new(0x1000)); + 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`. + pub fn new_starting_at(frame: PhysFrame, memory_map: I) -> Self { + Self { + original: memory_map.clone(), + memory_map, + current_descriptor: None, + next_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 the largest detected physical memory address. + /// + /// Useful for creating a mapping for all physical memory. + pub fn max_phys_addr(&self) -> PhysAddr { + self.original + .clone() + .map(|r| r.start() + r.len()) + .max() + .unwrap() + } + + /// 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`] pluse 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], + ) -> &mut [MemoryRegion] { + let mut next_index = 0; + + for descriptor in self.original { + let mut start = descriptor.start(); + let end = start + descriptor.len(); + let next_free = self.next_frame.start_address(); + let kind = match descriptor.kind() { + MemoryRegionKind::Usable => { + if end <= next_free { + MemoryRegionKind::Bootloader + } else if descriptor.start() >= next_free { + MemoryRegionKind::Usable + } else { + // part of the region is used -> add it separately + let used_region = MemoryRegion { + start: descriptor.start().as_u64(), + end: next_free.as_u64(), + kind: MemoryRegionKind::Bootloader, + }; + Self::add_region(used_region, regions, &mut next_index) + .expect("Failed to add memory region"); + + // add unused part normally + start = next_free; + MemoryRegionKind::Usable + } + } + // some mappings created by the UEFI firmware become usable again at this point + #[cfg(feature = "uefi_bin")] + MemoryRegionKind::UnknownUefi(other) => { + use uefi::table::boot::MemoryType as M; + match M(other) { + M::LOADER_CODE + | M::LOADER_DATA + | M::BOOT_SERVICES_CODE + | M::BOOT_SERVICES_DATA + | M::RUNTIME_SERVICES_CODE + | M::RUNTIME_SERVICES_DATA => MemoryRegionKind::Usable, + other => MemoryRegionKind::UnknownUefi(other.0), + } + } + other => other, + }; + + let region = MemoryRegion { + start: start.as_u64(), + end: end.as_u64(), + kind, + }; + Self::add_region(region, regions, &mut next_index).unwrap(); + } + + let initialized = &mut regions[..next_index]; + unsafe { MaybeUninit::slice_assume_init_mut(initialized) } + } + + fn add_region( + region: MemoryRegion, + regions: &mut [MaybeUninit], + next_index: &mut usize, + ) -> Result<(), ()> { + unsafe { + regions + .get_mut(*next_index) + .ok_or(())? + .as_mut_ptr() + .write(region) + }; + *next_index += 1; + Ok(()) + } +} + +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 + } +} diff --git a/src/binary/level_4_entries.rs b/src/binary/level_4_entries.rs new file mode 100644 index 00000000..c231cae2 --- /dev/null +++ b/src/binary/level_4_entries.rs @@ -0,0 +1,64 @@ +use core::convert::TryInto; +use x86_64::{ + structures::paging::{Page, PageTableIndex}, + VirtAddr, +}; +use xmas_elf::program::ProgramHeader; + +/// 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 { + entry_state: [bool; 512], // whether an entry is in use by the kernel +} + +impl UsedLevel4Entries { + /// Initializes a new instance from the given ELF program segments. + /// + /// Marks the virtual address range of all segments as used. + pub fn new<'a>(segments: impl Iterator>) -> 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 + } + + /// Returns a unused level 4 entry and marks it as used. + /// + /// 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_entry(&mut self) -> PageTableIndex { + let (idx, entry) = self + .entry_state + .iter_mut() + .enumerate() + .find(|(_, &mut entry)| entry == false) + .expect("no usable level 4 entries found"); + + *entry = true; + PageTableIndex::new(idx.try_into().unwrap()) + } + + /// Returns the virtual start address of an unused level 4 entry and marks it as used. + /// + /// This is a convenience method around [`get_free_entry`], so all of its docs applies here + /// too. + pub fn get_free_address(&mut self) -> VirtAddr { + Page::from_page_table_indices_1gib(self.get_free_entry(), PageTableIndex::new(0)) + .start_address() + } +} diff --git a/src/binary/load_kernel.rs b/src/binary/load_kernel.rs new file mode 100644 index 00000000..1e0870c5 --- /dev/null +++ b/src/binary/load_kernel.rs @@ -0,0 +1,286 @@ +use crate::{ + binary::{level_4_entries::UsedLevel4Entries, PAGE_SIZE}, + boot_info::TlsTemplate, +}; +use x86_64::{ + align_up, + structures::paging::{ + mapper::MapperAllSizes, FrameAllocator, Page, PageSize, PageTableFlags as Flags, PhysFrame, + Size4KiB, + }, + PhysAddr, VirtAddr, +}; +use xmas_elf::{ + header, + program::{self, ProgramHeader, Type}, + ElfFile, +}; + +struct Loader<'a, M, F> { + elf_file: ElfFile<'a>, + inner: Inner<'a, M, F>, +} + +struct Inner<'a, M, F> { + kernel_offset: PhysAddr, + page_table: &'a mut M, + frame_allocator: &'a mut F, +} + +impl<'a, M, F> Loader<'a, M, F> +where + M: MapperAllSizes, + F: FrameAllocator, +{ + fn new( + bytes: &'a [u8], + page_table: &'a mut M, + frame_allocator: &'a mut F, + ) -> Result { + log::info!("Elf file loaded at {:#p}", bytes); + let kernel_offset = PhysAddr::new(&bytes[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 = ElfFile::new(bytes)?; + header::sanity_check(&elf_file)?; + let loader = Loader { + elf_file, + inner: Inner { + kernel_offset, + page_table, + frame_allocator, + }, + }; + + Ok(loader) + } + + fn load_segments(&mut self) -> Result, &'static str> { + let mut tls_template = None; + for program_header in self.elf_file.program_iter() { + program::sanity_check(program_header, &self.elf_file)?; + 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(_) => {} + } + } + Ok(tls_template) + } + + fn entry_point(&self) -> VirtAddr { + VirtAddr::new(self.elf_file.header.pt2.entry_point()) + } + + fn used_level_4_entries(&self) -> UsedLevel4Entries { + UsedLevel4Entries::new(self.elf_file.program_iter()) + } +} + +impl<'a, M, F> Inner<'a, M, F> +where + M: MapperAllSizes, + 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(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 { + self.page_table + .map_to(page, frame, segment_flags, 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(segment.virtual_addr()); + let phys_start_addr = self.kernel_offset + segment.offset(); + let mem_size = segment.mem_size(); + let file_size = segment.file_size(); + + // calculate virual 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. + + // calculate the frame where the last segment page is mapped + let orig_frame: PhysFrame = + PhysFrame::containing_address(phys_start_addr + file_size - 1u64); + // allocate a new frame to replace `orig_frame` + let new_frame = self.frame_allocator.allocate_frame().unwrap(); + + // zero new frame, utilizing that it's identity-mapped + { + let new_frame_ptr = new_frame.start_address().as_u64() as *mut PageArray; + unsafe { new_frame_ptr.write(ZERO_ARRAY) }; + } + + // copy the data bytes from orig_frame to new_frame + { + log::info!("Copy contents"); + let orig_bytes_ptr = orig_frame.start_address().as_u64() as *mut u8; + let new_bytes_ptr = new_frame.start_address().as_u64() as *mut u8; + + for offset in 0..(data_bytes_before_zero as isize) { + unsafe { + let orig_byte = orig_bytes_ptr.offset(offset).read(); + new_bytes_ptr.offset(offset).write(orig_byte); + } + } + } + + // remap last page from orig_frame to `new_frame` + log::info!("Remap last page"); + let last_page = Page::containing_address(virt_start_addr + file_size - 1u64); + self.page_table + .unmap(last_page.clone()) + .map_err(|_err| "Failed to unmap last segment page because of bss memory")? + .1 + .ignore(); + let flusher = unsafe { + self.page_table + .map_to(last_page, new_frame, segment_flags, self.frame_allocator) + } + .map_err(|_err| "Failed to remap last segment page because of bss memory")?; + // we operate on an inactive page table, so we don't need to flush our changes + flusher.ignore(); + } + + // 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); + 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 { + self.page_table + .map_to(page, frame, segment_flags, 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(()) + } + + fn handle_tls_segment(&mut self, segment: ProgramHeader) -> Result { + Ok(TlsTemplate { + start_addr: segment.virtual_addr(), + mem_size: segment.mem_size(), + file_size: segment.file_size(), + }) + } +} + +/// 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( + bytes: &[u8], + page_table: &mut impl MapperAllSizes, + frame_allocator: &mut impl FrameAllocator, +) -> Result<(VirtAddr, Option, UsedLevel4Entries), &'static str> { + let mut loader = Loader::new(bytes, page_table, frame_allocator)?; + let tls_template = loader.load_segments()?; + let used_entries = loader.used_level_4_entries(); + + Ok((loader.entry_point(), tls_template, used_entries)) +} diff --git a/src/binary/logger.rs b/src/binary/logger.rs new file mode 100644 index 00000000..ffb72651 --- /dev/null +++ b/src/binary/logger.rs @@ -0,0 +1,152 @@ +use crate::boot_info::{FrameBufferInfo, PixelFormat}; +use conquer_once::spin::OnceCell; +use core::{ + fmt::{self, Write}, + ptr, +}; +use font8x8::UnicodeFonts; +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(Spinlock); + +/// Additional vertical space between lines +const LINE_SPACING: usize = 0; +/// Additional vertical space between separate log messages +const LOG_SPACING: usize = 2; + +impl LockedLogger { + /// Create a new instance that logs to the given framebuffer. + pub fn new(framebuffer: &'static mut [u8], info: FrameBufferInfo) -> Self { + LockedLogger(Spinlock::new(Logger::new(framebuffer, info))) + } + + /// Force-unlocks the logger to prevent a deadlock. + /// + /// This method is not memory safe and should be only used when absolutely necessary. + pub unsafe fn force_unlock(&self) { + unsafe { self.0.force_unlock() }; + } +} + +impl log::Log for LockedLogger { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + let mut logger = self.0.lock(); + writeln!(logger, "{}: {}", record.level(), record.args()).unwrap(); + logger.add_vspace(LOG_SPACING); + } + + fn flush(&self) {} +} + +/// Allows logging text to a pixel-based framebuffer. +pub struct Logger { + framebuffer: &'static mut [u8], + info: FrameBufferInfo, + x_pos: usize, + y_pos: usize, +} + +impl Logger { + /// 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 += 8 + LINE_SPACING; + self.carriage_return() + } + + fn add_vspace(&mut self, space: usize) { + self.y_pos += space; + } + + 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.horizontal_resolution + } + + fn height(&self) -> usize { + self.info.vertical_resolution + } + + fn write_char(&mut self, c: char) { + match c { + '\n' => self.newline(), + '\r' => self.carriage_return(), + c => { + if self.x_pos >= self.width() { + self.newline(); + } + if self.y_pos >= (self.height() - 8) { + self.clear(); + } + let rendered = font8x8::BASIC_FONTS + .get(c) + .expect("character not found in basic font"); + self.write_rendered_char(rendered); + } + } + } + + fn write_rendered_char(&mut self, rendered_char: [u8; 8]) { + for (y, byte) in rendered_char.iter().enumerate() { + for (x, bit) in (0..8).enumerate() { + let alpha = if *byte & (1 << bit) == 0 { 0 } else { 255 }; + self.write_pixel(self.x_pos + x, self.y_pos + y, alpha); + } + } + self.x_pos += 8; + } + + 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], + }; + 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 Logger {} +unsafe impl Sync for Logger {} + +impl fmt::Write for Logger { + fn write_str(&mut self, s: &str) -> fmt::Result { + for c in s.chars() { + self.write_char(c); + } + Ok(()) + } +} diff --git a/src/binary/mod.rs b/src/binary/mod.rs new file mode 100644 index 00000000..de5d7de6 --- /dev/null +++ b/src/binary/mod.rs @@ -0,0 +1,496 @@ +use crate::{ + binary::legacy_memory_region::{LegacyFrameAllocator, LegacyMemoryRegion}, + boot_info::{BootInfo, FrameBuffer, FrameBufferInfo, MemoryRegion, TlsTemplate}, +}; +use core::{ + mem::{self, MaybeUninit}, + slice, +}; +use level_4_entries::UsedLevel4Entries; +use parsed_config::CONFIG; +use usize_conversions::FromUsize; +use x86_64::{ + registers, + structures::paging::{ + FrameAllocator, Mapper, OffsetPageTable, Page, PageTableFlags, PageTableIndex, PhysFrame, + Size2MiB, Size4KiB, + }, + PhysAddr, VirtAddr, +}; + +/// Provides BIOS-specific types and trait implementations. +#[cfg(feature = "bios_bin")] +pub mod bios; +/// Provides UEFI-specific trait implementations. +#[cfg(feature = "uefi_bin")] +mod uefi; + +/// 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 type that logs output as text to pixel-based framebuffers. +pub mod logger; + +// Contains the parsed configuration table from the kernel's Cargo.toml. +// +// The layout of the file is the following: +// +// ``` +// mod parsed_config { +// pub const CONFIG: Config = Config { … }; +// } +// ``` +// +// The module file is created by the build script. +include!(concat!(env!("OUT_DIR"), "/bootloader_config.rs")); + +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) { + let logger = logger::LOGGER.get_or_init(move || logger::LockedLogger::new(framebuffer, info)); + log::set_logger(logger).expect("logger already set"); + log::set_max_level(log::LevelFilter::Trace); + log::info!("Framebuffer info: {:?}", info); +} + +/// Required system information that should be queried from the BIOS or UEFI firmware. +#[derive(Debug, Copy, Clone)] +pub struct SystemInfo { + /// Start address of the pixel-based framebuffer. + pub framebuffer_addr: PhysAddr, + /// Information about the framebuffer, including layout and pixel format. + pub framebuffer_info: FrameBufferInfo, + /// Address of the _Root System Description Pointer_ structure of the ACPI standard. + pub rsdp_addr: Option, +} + +/// 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_bytes: &[u8], + mut frame_allocator: LegacyFrameAllocator, + mut page_tables: PageTables, + system_info: SystemInfo, +) -> ! +where + I: ExactSizeIterator + Clone, + D: LegacyMemoryRegion, +{ + let mut mappings = set_up_mappings( + kernel_bytes, + &mut frame_allocator, + &mut page_tables, + system_info.framebuffer_addr, + system_info.framebuffer_info.byte_len, + ); + let (boot_info, two_frames) = create_boot_info( + frame_allocator, + &mut page_tables, + &mut mappings, + system_info, + ); + switch_to_kernel(page_tables, mappings, boot_info, two_frames); +} + +/// 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_bytes: &[u8], + frame_allocator: &mut LegacyFrameAllocator, + page_tables: &mut PageTables, + framebuffer_addr: PhysAddr, + framebuffer_size: usize, +) -> Mappings +where + I: ExactSizeIterator + Clone, + D: LegacyMemoryRegion, +{ + let kernel_page_table = &mut page_tables.kernel; + + // 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 (entry_point, tls_template, mut used_entries) = + load_kernel::load_kernel(kernel_bytes, kernel_page_table, frame_allocator) + .expect("no entry point"); + log::info!("Entry point at: {:#x}", entry_point.as_u64()); + + // create a stack + let stack_start_addr = kernel_stack_start_location(&mut used_entries); + let stack_start: Page = Page::containing_address(stack_start_addr); + let stack_end = { + let end_addr = stack_start_addr + CONFIG.kernel_stack_size.unwrap_or(20 * PAGE_SIZE); + Page::containing_address(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; + unsafe { kernel_page_table.map_to(page, frame, flags, frame_allocator) } + .unwrap() + .flush(); + } + + // map framebuffer + let framebuffer_virt_addr = if CONFIG.map_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_size - 1u64); + let start_page = Page::containing_address(frame_buffer_location(&mut used_entries)); + 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; + unsafe { kernel_page_table.map_to(page, frame, flags, frame_allocator) } + .unwrap() + .flush(); + } + let framebuffer_virt_addr = start_page.start_address(); + Some(framebuffer_virt_addr) + } else { + None + }; + + let physical_memory_offset = if CONFIG.map_physical_memory { + log::info!("Map physical memory"); + let offset = CONFIG + .physical_memory_offset + .map(VirtAddr::new) + .unwrap_or_else(|| used_entries.get_free_address()); + + 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); + 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; + unsafe { kernel_page_table.map_to(page, frame, flags, frame_allocator) } + .unwrap() + .ignore(); + } + + Some(offset) + } else { + None + }; + + let recursive_index = if CONFIG.map_page_table_recursively { + log::info!("Map page table recursively"); + let index = CONFIG + .recursive_index + .map(PageTableIndex::new) + .unwrap_or_else(|| used_entries.get_free_entry()); + + let entry = &mut kernel_page_table.level_4_table()[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; + entry.set_frame(page_tables.kernel_level_4_frame, flags); + + Some(index) + } else { + None + }; + + Mappings { + framebuffer: framebuffer_virt_addr, + entry_point, + stack_end, + used_entries, + physical_memory_offset, + recursive_index, + tls_template, + } +} + +/// 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 stack end page of the kernel. + pub stack_end: Page, + /// 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, +} + +/// 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( + mut frame_allocator: LegacyFrameAllocator, + page_tables: &mut PageTables, + mappings: &mut Mappings, + system_info: SystemInfo, +) -> (&'static mut BootInfo, TwoFrames) +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_addr = boot_info_location(&mut mappings.used_entries); + let boot_info_end = boot_info_addr + mem::size_of::(); + let memory_map_regions_addr = + boot_info_end.align_up(u64::from_usize(mem::align_of::())); + let regions = frame_allocator.len() + 1; // one region might be split into used/unused + let memory_map_regions_end = + memory_map_regions_addr + regions * mem::size_of::(); + + 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; + let frame = frame_allocator + .allocate_frame() + .expect("frame allocation for boot info failed"); + unsafe { + page_tables + .kernel + .map_to(page, frame, flags, &mut frame_allocator) + } + .unwrap() + .flush(); + // we need to be able to access it too + unsafe { + page_tables + .bootloader + .map_to(page, frame, flags, &mut frame_allocator) + } + .unwrap() + .flush(); + } + + 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) + }; + + // reserve two unused frames for context switch + let two_frames = TwoFrames::new(&mut frame_allocator); + + log::info!("Create Memory Map"); + + // build memory map + let memory_regions = frame_allocator.construct_memory_map(memory_regions); + + log::info!("Create bootinfo"); + + // create boot info + let boot_info = boot_info.write(BootInfo { + version_major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(), + version_minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(), + version_patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(), + pre_release: !env!("CARGO_PKG_VERSION_PRE").is_empty(), + memory_regions: memory_regions.into(), + framebuffer: mappings + .framebuffer + .map(|addr| FrameBuffer { + buffer_start: addr.as_u64(), + buffer_byte_len: system_info.framebuffer_info.byte_len, + info: system_info.framebuffer_info, + }) + .into(), + physical_memory_offset: mappings.physical_memory_offset.map(VirtAddr::as_u64).into(), + recursive_index: mappings.recursive_index.map(Into::into).into(), + rsdp_addr: system_info.rsdp_addr.map(|addr| addr.as_u64()).into(), + tls_template: mappings.tls_template.into(), + }); + + (boot_info, two_frames) +} + +/// 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, + two_frames: TwoFrames, +) -> ! { + let PageTables { + kernel_level_4_frame, + kernel: kernel_page_table, + .. + } = page_tables; + let addresses = Addresses { + page_table: kernel_level_4_frame, + stack_top: mappings.stack_end.start_address(), + entry_point: mappings.entry_point, + boot_info, + }; + + log::info!( + "Jumping to kernel entry point at {:?}", + addresses.entry_point + ); + + unsafe { + context_switch(addresses, kernel_page_table, two_frames); + } +} + +/// 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. +/// +/// This function uses the given `frame_allocator` to identity map itself in the kernel-level +/// page table. This is required to avoid a page fault after the context switch. Since this +/// function is relatively small, only up to two physical frames are required from the frame +/// allocator, so the [`TwoFrames`] type can be used here. +unsafe fn context_switch( + addresses: Addresses, + mut kernel_page_table: OffsetPageTable, + mut frame_allocator: impl FrameAllocator, +) -> ! { + // identity-map current and next frame, so that we don't get an immediate pagefault + // after switching the active page table + let current_addr = PhysAddr::new(registers::read_rip().as_u64()); + let current_frame: PhysFrame = PhysFrame::containing_address(current_addr); + for frame in PhysFrame::range_inclusive(current_frame, current_frame + 1) { + unsafe { + kernel_page_table.identity_map(frame, PageTableFlags::PRESENT, &mut frame_allocator) + } + .unwrap() + .flush(); + } + + // we don't need the kernel page table anymore + mem::drop(kernel_page_table); + + // do the context switch + unsafe { + asm!( + "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 crate::boot_info::BootInfo, +} + +/// Used for reversing two physical frames for identity mapping the context switch function. +/// +/// In order to prevent a page fault, the context switch function must be mapped identically in +/// both address spaces. The context switch function is small, so this mapping requires only +/// two physical frames (one frame is not enough because the linker might place the function +/// directly before a page boundary). Since the frame allocator no longer exists when the +/// context switch function is invoked, we use this type to reserve two physical frames +/// beforehand. +pub struct TwoFrames { + frames: [Option; 2], +} + +impl TwoFrames { + /// Creates a new instance by allocating two physical frames from the given frame allocator. + pub fn new(frame_allocator: &mut impl FrameAllocator) -> Self { + TwoFrames { + frames: [ + Some(frame_allocator.allocate_frame().unwrap()), + Some(frame_allocator.allocate_frame().unwrap()), + ], + } + } +} + +unsafe impl FrameAllocator for TwoFrames { + fn allocate_frame(&mut self) -> Option> { + self.frames.iter_mut().find_map(|f| f.take()) + } +} + +fn boot_info_location(used_entries: &mut UsedLevel4Entries) -> VirtAddr { + CONFIG + .boot_info_address + .map(VirtAddr::new) + .unwrap_or_else(|| used_entries.get_free_address()) +} + +fn frame_buffer_location(used_entries: &mut UsedLevel4Entries) -> VirtAddr { + CONFIG + .framebuffer_address + .map(VirtAddr::new) + .unwrap_or_else(|| used_entries.get_free_address()) +} + +fn kernel_stack_start_location(used_entries: &mut UsedLevel4Entries) -> VirtAddr { + CONFIG + .kernel_stack_address + .map(VirtAddr::new) + .unwrap_or_else(|| used_entries.get_free_address()) +} + +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/src/binary/uefi/memory_descriptor.rs b/src/binary/uefi/memory_descriptor.rs new file mode 100644 index 00000000..7d1c3a53 --- /dev/null +++ b/src/binary/uefi/memory_descriptor.rs @@ -0,0 +1,22 @@ +use crate::{binary::legacy_memory_region::LegacyMemoryRegion, boot_info::MemoryRegionKind}; +use uefi::table::boot::{MemoryDescriptor, MemoryType}; +use x86_64::PhysAddr; + +const PAGE_SIZE: u64 = 4096; + +impl<'a> LegacyMemoryRegion for MemoryDescriptor { + fn start(&self) -> PhysAddr { + PhysAddr::new(self.phys_start) + } + + fn len(&self) -> u64 { + self.page_count * PAGE_SIZE + } + + fn kind(&self) -> MemoryRegionKind { + match self.ty { + MemoryType::CONVENTIONAL => MemoryRegionKind::Usable, + other => MemoryRegionKind::UnknownUefi(other.0), + } + } +} diff --git a/src/binary/uefi/mod.rs b/src/binary/uefi/mod.rs new file mode 100644 index 00000000..7679880c --- /dev/null +++ b/src/binary/uefi/mod.rs @@ -0,0 +1 @@ +mod memory_descriptor; diff --git a/src/boot_info.rs b/src/boot_info.rs index f50cf2a5..08fa69d4 100644 --- a/src/boot_info.rs +++ b/src/boot_info.rs @@ -1,31 +1,308 @@ -use core::slice; +use core::{ops, slice}; -use bootloader::bootinfo::{E820MemoryRegion, MemoryMap, MemoryRegion, MemoryRegionType}; -use usize_conversions::usize_from; -use x86_64::VirtAddr; +/// 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::BootInfo; +/// # type _SIGNATURE = +/// extern "C" fn(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)] +#[non_exhaustive] +pub struct BootInfo { + /// Bootloader version (major). + pub version_major: u16, + /// Bootloader version (minor). + pub version_minor: u16, + /// Bootloader version (patch). + pub version_patch: u16, + /// Whether the bootloader version is a pre-release. + /// + /// We can't store the full prerelease string of the version number since it could be + /// arbitrarily long. + pub pre_release: bool, + /// 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, +} + +/// 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, +} -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)) }; +impl ops::Deref for MemoryRegions { + type Target = [MemoryRegion]; - let mut memory_map = MemoryMap::new(); - for region in e820_memory_map { - memory_map.add_region(MemoryRegion::from(*region)); + fn deref(&self) -> &Self::Target { + unsafe { slice::from_raw_parts(self.ptr, self.len) } } +} - memory_map.sort(); +impl ops::DerefMut for MemoryRegions { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { slice::from_raw_parts_mut(self.ptr, self.len) } + } +} - 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; - } +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) } + } +} - memory_map +/// 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 kernel and boot info mappings. + /// + /// This memory should _not_ be used by the kernel. + Bootloader, + /// An unknown memory region reported by the UEFI firmware. + /// + /// This should only be used if the UEFI memory type is known as usable. + UnknownUefi(u32), + /// An unknown memory region reported by the BIOS firmware. + /// + /// This should only be used if the BIOS memory type is known as usable. + 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) buffer_byte_len: usize, + pub(crate) info: FrameBufferInfo, +} + +impl FrameBuffer { + /// 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() } + } + + unsafe fn create_buffer<'a>(&self) -> &'a mut [u8] { + unsafe { slice::from_raw_parts_mut(self.buffer_start as *mut u8, self.buffer_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 horizontal_resolution: usize, + /// The height in pixels. + pub vertical_resolution: 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, +} + +/// 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, + } + } +} + +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/src/bootinfo/memory_map.rs b/src/bootinfo/memory_map.rs deleted file mode 100644 index 134188ed..00000000 --- a/src/bootinfo/memory_map.rs +++ /dev/null @@ -1,236 +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)] -#[allow(clippy::new_without_default)] -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. - /// - /// To 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. - /// - /// To 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_memory_map(_memory_map: MemoryMap); -} diff --git a/src/bootinfo/mod.rs b/src/bootinfo/mod.rs deleted file mode 100644 index beccac07..00000000 --- a/src/bootinfo/mod.rs +++ /dev/null @@ -1,120 +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, - tls_template: TlsTemplate, - _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, - tls_template: Option, - recursive_page_table_addr: u64, - physical_memory_offset: u64, - ) -> Self { - let tls_template = tls_template.unwrap_or(TlsTemplate { - start_addr: 0, - file_size: 0, - mem_size: 0, - }); - BootInfo { - memory_map, - tls_template, - #[cfg(feature = "recursive_page_table")] - recursive_page_table_addr, - #[cfg(feature = "map_physical_memory")] - physical_memory_offset, - _non_exhaustive: 0, - } - } - - /// Returns information about the thread local storage segment of the kernel. - /// - /// Returns `None` if the kernel has no thread local storage segment. - /// - /// (The reason this is a method instead of a normal field is that `Option` - /// is not FFI-safe.) - pub fn tls_template(&self) -> Option { - if self.tls_template.mem_size > 0 { - Some(self.tls_template) - } else { - None - } - } - - /// Returns the index into the page tables that recursively maps the page tables themselves. - #[cfg(feature = "recursive_page_table")] - pub fn recursive_index(&self) -> u16 { - ((self.recursive_page_table_addr >> 12) & 0x1FF) as u16 - } -} - -/// 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, -} - -extern "C" { - fn _improper_ctypes_check_bootinfo(_boot_info: BootInfo); -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..6859f947 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,78 @@ +/// Allows configuring the bootloader behavior. +/// +/// To control these, use a `[package.metadata.bootloader]` table in the `Cargo.toml` of +/// your kernel. The naming convention for all config fields is `kebab-case`, otherwise the +/// config keys correspond to the field names of this struct (i.e. just replace `_` with `-`). +/// Unknown config keys lead to an error. +/// +/// ## Example +/// +/// To map the complete physical memory starting at virtual address `0x0000_4000_0000_0000`, add +/// the following to your kernel's `Cargo.toml`: +/// +/// ```toml +/// [package.metadata.bootloader] +/// map-physical-memory = true +/// physical-memory-offset = 0x0000_4000_0000_0000 +/// ``` +/// +/// ## Memory Addresses +/// +/// Memory addresses must be positive and page aligned. Since TOML does not support unsigned 64-bit +/// integers, we also support string input to specify addresses larger than `i64::MAX`. For example: +/// +/// ```toml +/// physical-memory-offset = "0xf000_0000_0000_0000" +/// ``` +/// +/// The above example would fail if the address was specified as integer instead (i.e. without +/// the quotes). +/// +/// All memory addresses are optional, even if their corresponding switch is enabled. If no +/// address is specified, the bootloader will choose an unused entry of the level 4 page table +/// at runtime. +#[derive(Debug)] +pub struct Config { + /// Whether to create a virtual mapping of the complete physical memory. + /// + /// Defaults to `false`. + pub map_physical_memory: bool, + /// Map the physical memory at a specified virtual address. + /// + /// If not given, the bootloader searches for a free virtual address dynamically. + /// + /// Only considered if `map_physical_memory` is `true`. + pub physical_memory_offset: Option, + /// Whether to create a recursive entry in the level 4 page table. + /// + /// Defaults to `false`. + pub map_page_table_recursively: bool, + /// Create the recursive mapping in at the given entry of the level 4 page table. + /// + /// If not given, the bootloader searches for a free level 4 entry dynamically. + /// + /// Only considered if `map_page_table_recursively` is `true`. + pub recursive_index: Option, + /// Use the given stack size for the kernel. + /// + /// Defaults to at least 80KiB if not given. + pub kernel_stack_size: Option, + /// Create the kernel stack at the given virtual address. + /// + /// Looks for a free virtual memory region dynamically if not given. + pub kernel_stack_address: Option, + /// Create the boot information at the given virtual address. + /// + /// Looks for a free virtual memory region dynamically if not given. + pub boot_info_address: Option, + /// Whether to map the framebuffer to virtual memory. + /// + /// Defaults to `true`. + pub map_framebuffer: bool, + /// Map the framebuffer memory at the specified virtual address. + /// + /// If not given, the bootloader searches for a free virtual memory region dynamically. + /// + /// Only considered if `map_framebuffer` is `true`. + pub framebuffer_address: Option, +} diff --git a/src/disk_image.rs b/src/disk_image.rs new file mode 100644 index 00000000..7b8e3610 --- /dev/null +++ b/src/disk_image.rs @@ -0,0 +1,107 @@ +use std::{io, 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, +) -> Result<(), DiskImageError> { + let llvm_tools = llvm_tools::LlvmTools::new()?; + let objcopy = llvm_tools + .tool(&llvm_tools::exe("llvm-objcopy")) + .ok_or(DiskImageError::LlvmObjcopyNotFound)?; + + // convert bootloader 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, + }); + } + + 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 { + /// Desciption of the failed I/O operation + message: &'static str, + /// The I/O error that occured + 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/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/level4_entries.rs b/src/level4_entries.rs deleted file mode 100644 index 60ace846..00000000 --- a/src/level4_entries.rs +++ /dev/null @@ -1,45 +0,0 @@ -use core::convert::TryInto; -use fixedvec::FixedVec; -use x86_64::{ - structures::paging::{Page, PageTableIndex}, - 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) -> PageTableIndex { - let (idx, entry) = self - .entry_state - .iter_mut() - .enumerate() - .find(|(_, &mut entry)| entry == false) - .expect("no usable level 4 entries found"); - - *entry = true; - PageTableIndex::new(idx.try_into().unwrap()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 90d8eebf..27b916de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,97 @@ -//! 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. + +To use this crate, specify it as a dependency in the `Cargo.toml` of your operating system +kernel. Then you can use the [`entry_point`] macro to mark your entry point function. This +gives you access to the [`BootInfo`] struct, which is passed by the bootloader. + +## Disk Image Creation + +Including the `bootloader` crate as a dependency makes the kernel binary suitable for booting, +but does not create any bootable disk images. To create them, two additional steps are needed: + +1. **Locate the source code of the `bootloader` dependency** on your local system. By using the + dependency source code directly, we ensure that the kernel and bootloader use the same version + of the [`BootInfo`] struct. + - When creating a builder binary written in Rust, the + [`bootloader_locator`](https://docs.rs/bootloader-locator/0.0.4/bootloader_locator/) crate can + be used to automate this step. + - Otherwise, the + [`cargo metadata`](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html) subcommand + can be used to locate the dependency. The command outputs a JSON object with various metadata + for the current package. To find the `bootloader` source path in it, first look for the + "bootloader" dependency under `resolve.nodes.deps` to find out its ID (in the `pkg` field). + Then use that ID to find the bootloader in `packages`. Its `manifest_path` field contains the + local path to the `Cargo.toml` of the bootloader. +2. **Run the following command** in the source code directory of the `bootloader` dependency to create + the bootable disk images: + + ```notrust + cargo builder --kernel-manifest path/to/kernel/Cargo.toml --kernel-binary path/to/kernel_bin + ``` + + The `--kernel-manifest` argument should point to the `Cargo.toml` of your kernel. It is used + for applying configuration settings. The `--kernel-binary` argument should point to the kernel + executable that should be used for the bootable disk images. + + In addition to the `--kernel-manifest` and `--kernel-binary` arguments, it is recommended to also + set the `--target-dir` and `--out-dir` arguments. The former specifies the directory that should + used for cargo build artifacts and the latter specfies the directory where the resulting disk + images should be placed. It is recommended to set `--target-dir` to the `target` folder of your + kernel and `--out-dir` to the the parent folder of `--kernel-binary`. + +This will result in the following files, which are placed in the specified `--out-dir`: + +- A disk image suitable for BIOS booting, named `boot-bios-.img`, where `` is the + name of your kernel executable. This image can be started in QEMU or booted on a real machine + after burning it to an USB stick.. +- A disk image suitable for UEFI booting, named `boot-uefi-.img`. Like the BIOS disk image, + this can be started in QEMU (requires OVMF) and burned to an USB stick to run it on a real + machine. +- Intermediate UEFI files + - A FAT partition image named `boot-uefi-.fat`, which can be directly started in QEMU + or written as an EFI system partition to a GPT-formatted disk. + - An EFI file named `boot-uefi-.efi`. This executable is the combination of the + bootloader and kernel executables. It can be started in QEMU or used to construct a bootable + disk image: Create an EFI system partition formatted with the FAT filesystem and place the + EFI file under `efi\boot\bootx64.efi` on that filesystem. + +## Configuration + +The bootloader can be configured through a `[package.metadata.bootloader]` table in the +`Cargo.toml` of the kernel (the one passed as `--kernel-manifest`). See the [`Config`] struct +for all possible configuration options. +*/ + +#![cfg_attr(not(feature = "builder"), no_std)] +#![feature(asm)] +#![feature(maybe_uninit_extra)] +#![feature(maybe_uninit_slice)] +#![deny(unsafe_op_in_unsafe_fn)] #![warn(missing_docs)] -pub use crate::bootinfo::BootInfo; +pub use crate::boot_info::BootInfo; +pub use crate::config::Config; + +/// Configuration options for the bootloader. +mod config; + +/// Contains the boot information struct sent by the bootloader to the kernel on startup. +pub mod boot_info; -pub mod bootinfo; +/// Contains the actual bootloader implementation ("bootloader as a binary"). +/// +/// Useful for reusing part of the bootloader implementation for other crates. +/// +/// Only available when the `binary` feature is enabled. +#[cfg(feature = "binary")] +pub mod binary; + +/// Provides a function to turn a bootloader executable into a disk image. +/// +/// Used by the `builder` binary. Only available when the `builder` feature is enabled. +#[cfg(feature = "builder")] +pub mod disk_image; #[cfg(target_arch = "x86")] compile_error!( @@ -32,9 +113,9 @@ compile_error!("This crate only supports the x86_64 architecture."); macro_rules! entry_point { ($path:path) => { #[export_name = "_start"] - pub extern "C" fn __impl_start(boot_info: &'static $crate::bootinfo::BootInfo) -> ! { + pub extern "C" fn __impl_start(boot_info: &'static mut $crate::boot_info::BootInfo) -> ! { // validate the signature of the program entry point - let f: fn(&'static $crate::bootinfo::BootInfo) -> ! = $path; + let f: fn(&'static mut $crate::boot_info::BootInfo) -> ! = $path; f(boot_info) } diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 3ccac477..00000000 --- a/src/main.rs +++ /dev/null @@ -1,402 +0,0 @@ -#![feature(lang_items)] -#![feature(global_asm)] -#![feature(llvm_asm)] -#![no_std] -#![no_main] - -#[cfg(not(target_os = "none"))] -compile_error!("The bootloader crate must be compiled for the `x86_64-bootloader.json` target"); - -extern crate rlibc; - -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, - PageTableIndex, PhysFrame, RecursivePageTable, Size2MiB, Size4KiB, -}; -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) -> ! { - llvm_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 - llvm_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 = PageTableIndex::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 = match BOOT_INFO_ADDRESS { - Some(addr) => Page::containing_address(VirtAddr::new(addr)), - None => Page::from_page_table_indices( - level4_entries.get_free_entry(), - PageTableIndex::new(0), - PageTableIndex::new(0), - PageTableIndex::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 kernel_memory_info = 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(), - PageTableIndex::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, - kernel_memory_info.tls_segment, - 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, kernel_memory_info.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/page_table.rs b/src/page_table.rs deleted file mode 100644 index ed2ac136..00000000 --- a/src/page_table.rs +++ /dev/null @@ -1,219 +0,0 @@ -use crate::frame_allocator::FrameAllocator; -use bootloader::bootinfo::MemoryRegionType; -use bootloader::bootinfo::TlsTemplate; -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}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MemoryInfo { - pub stack_end: VirtAddr, - pub tls_segment: Option, -} - -#[derive(Debug)] -pub enum MapKernelError { - Mapping(MapToError), - MultipleTlsSegments, -} - -impl From> for MapKernelError { - fn from(e: MapToError) -> Self { - MapKernelError::Mapping(e) - } -} - -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 { - let mut tls_segment = None; - for segment in segments { - let tls = map_segment(segment, kernel_start, page_table, frame_allocator)?; - if let Some(tls) = tls { - if tls_segment.replace(tls).is_some() { - return Err(MapKernelError::MultipleTlsSegments); - } - } - } - - // 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(MemoryInfo { - stack_end: stack_end.start_address(), - tls_segment, - }) -} - -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(None) - } - program::Type::Tls => Ok(Some(TlsTemplate { - start_addr: segment.virtual_addr, - mem_size: segment.mem_size, - file_size: segment.file_size, - })), - _ => Ok(None), - } -} - -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 b4e0a9f2..00000000 --- a/src/sse.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// Enables Streaming SIMD Extensions (SSE) support for loaded kernels. -pub fn enable_sse() { - use x86_64::registers::control::{Cr0, Cr0Flags, Cr4, Cr4Flags}; - let mut flags = Cr0::read(); - flags.remove(Cr0Flags::EMULATE_COPROCESSOR); - flags.insert(Cr0Flags::MONITOR_COPROCESSOR); - unsafe { - Cr0::write(flags); - } - - let mut flags = Cr4::read(); - flags.insert(Cr4Flags::OSFXSR); - flags.insert(Cr4Flags::OSXMMEXCPT_ENABLE); - unsafe { - Cr4::write(flags); - } -} diff --git a/src/video_mode/vga_320x200.s b/src/video_mode/vga_320x200.s deleted file mode 100644 index 17d4a743..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, 0xc0000 - 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 0ddb0d6f..00000000 --- a/src/video_mode/vga_text_80x25.s +++ /dev/null @@ -1,90 +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, 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, 0xc0000 - 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 + 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 5d0ef56f..00000000 --- a/test-kernel/Cargo.lock +++ /dev/null @@ -1,30 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "bit_field" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed8765909f9009617974ab6b7d332625b320b33c326b1e9321382ef1999b5d56" - -[[package]] -name = "bitflags" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" - -[[package]] -name = "test-kernel" -version = "0.1.0" -dependencies = [ - "x86_64", -] - -[[package]] -name = "x86_64" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b871116e3c83dad0795580b10b2b1dd05cb52ec719af36180371877b09681f7f" -dependencies = [ - "bit_field", - "bitflags", -] diff --git a/test-kernel/Cargo.toml b/test-kernel/Cargo.toml deleted file mode 100644 index f39f7503..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.13.2" 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/tests/default_settings.rs b/tests/default_settings.rs new file mode 100644 index 00000000..cf0a817a --- /dev/null +++ b/tests/default_settings.rs @@ -0,0 +1,27 @@ +use std::process::Command; + +#[test] +fn basic_boot() { + run_test_binary("basic_boot"); +} + +#[test] +fn should_panic() { + run_test_binary("should_panic"); +} + +#[test] +fn check_boot_info() { + run_test_binary("check_boot_info"); +} + +fn run_test_binary(bin_name: &str) { + let mut cmd = Command::new(env!("CARGO")); + cmd.current_dir("tests/test_kernels/default_settings"); + cmd.arg("run"); + cmd.arg("--bin").arg(bin_name); + cmd.arg("--target").arg("x86_64-default_settings.json"); + cmd.arg("-Zbuild-std=core"); + cmd.arg("-Zbuild-std-features=compiler-builtins-mem"); + assert!(cmd.status().unwrap().success()); +} diff --git a/tests/map_phys_mem.rs b/tests/map_phys_mem.rs new file mode 100644 index 00000000..f7e1b61a --- /dev/null +++ b/tests/map_phys_mem.rs @@ -0,0 +1,22 @@ +use std::process::Command; + +#[test] +fn check_boot_info() { + run_test_binary("check_boot_info"); +} + +#[test] +fn access_phys_mem() { + run_test_binary("access_phys_mem"); +} + +fn run_test_binary(bin_name: &str) { + let mut cmd = Command::new(env!("CARGO")); + cmd.current_dir("tests/test_kernels/map_phys_mem"); + cmd.arg("run"); + cmd.arg("--bin").arg(bin_name); + cmd.arg("--target").arg("x86_64-map_phys_mem.json"); + cmd.arg("-Zbuild-std=core"); + cmd.arg("-Zbuild-std-features=compiler-builtins-mem"); + assert!(cmd.status().unwrap().success()); +} diff --git a/tests/runner/Cargo.toml b/tests/runner/Cargo.toml new file mode 100644 index 00000000..93550faa --- /dev/null +++ b/tests/runner/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "runner" +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] +bootloader-locator = "0.0.4" +locate-cargo-manifest = "0.2.1" diff --git a/tests/runner/src/main.rs b/tests/runner/src/main.rs new file mode 100644 index 00000000..5a5fc7a7 --- /dev/null +++ b/tests/runner/src/main.rs @@ -0,0 +1,77 @@ +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +const QEMU_ARGS: &[&str] = &[ + "-device", + "isa-debug-exit,iobase=0xf4,iosize=0x04", + "-serial", + "stdio", + "-display", + "none", + "--no-reboot", +]; + +fn main() { + let kernel_binary_path = { + let path = PathBuf::from(std::env::args().nth(1).unwrap()); + path.canonicalize().unwrap() + }; + + let disk_image = create_disk_image(&kernel_binary_path, false); + + let mut run_cmd = Command::new("qemu-system-x86_64"); + run_cmd + .arg("-drive") + .arg(format!("format=raw,file={}", disk_image.display())); + run_cmd.args(QEMU_ARGS); + run_cmd.args(std::env::args().skip(2).collect::>()); + + let exit_status = run_cmd.status().unwrap(); + match exit_status.code() { + Some(33) => {} // success + Some(35) => panic!("Test failed"), // success + other => panic!("Test failed with unexpected exit code `{:?}`", other), + } +} + +pub fn create_disk_image(kernel_binary_path: &Path, bios_only: bool) -> PathBuf { + let bootloader_manifest_path = bootloader_locator::locate_bootloader("bootloader").unwrap(); + let kernel_manifest_path = locate_cargo_manifest::locate_manifest().unwrap(); + + let mut build_cmd = Command::new(env!("CARGO")); + build_cmd.current_dir(bootloader_manifest_path.parent().unwrap()); + build_cmd.arg("builder"); + build_cmd + .arg("--kernel-manifest") + .arg(&kernel_manifest_path); + build_cmd.arg("--kernel-binary").arg(&kernel_binary_path); + build_cmd + .arg("--target-dir") + .arg(kernel_manifest_path.parent().unwrap().join("target")); + build_cmd + .arg("--out-dir") + .arg(kernel_binary_path.parent().unwrap()); + //build_cmd.arg("--quiet"); + if bios_only { + build_cmd.arg("--firmware").arg("bios"); + } + + if !build_cmd.status().unwrap().success() { + panic!("build failed"); + } + + let kernel_binary_name = kernel_binary_path.file_name().unwrap().to_str().unwrap(); + let disk_image = kernel_binary_path + .parent() + .unwrap() + .join(format!("boot-bios-{}.img", kernel_binary_name)); + if !disk_image.exists() { + panic!( + "Disk image does not exist at {} after bootloader build", + disk_image.display() + ); + } + disk_image +} diff --git a/tests/test_kernels/default_settings/.cargo/config.toml b/tests/test_kernels/default_settings/.cargo/config.toml new file mode 100644 index 00000000..08a49f5a --- /dev/null +++ b/tests/test_kernels/default_settings/.cargo/config.toml @@ -0,0 +1,10 @@ +[unstable] +# TODO: Uncomment once https://github.com/rust-lang/cargo/issues/8643 is merged +# build-std = ["core"] + +[build] +# TODO: Uncomment once https://github.com/rust-lang/cargo/issues/8643 is merged +# target = "x86_64-example-kernel.json" + +[target.'cfg(target_os = "none")'] +runner = "cargo run --manifest-path ../../runner/Cargo.toml" \ No newline at end of file diff --git a/tests/test_kernels/default_settings/.gitignore b/tests/test_kernels/default_settings/.gitignore new file mode 100644 index 00000000..1de56593 --- /dev/null +++ b/tests/test_kernels/default_settings/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/example-kernel/Cargo.toml b/tests/test_kernels/default_settings/Cargo.toml similarity index 57% rename from example-kernel/Cargo.toml rename to tests/test_kernels/default_settings/Cargo.toml index 37431378..2a4659c6 100644 --- a/example-kernel/Cargo.toml +++ b/tests/test_kernels/default_settings/Cargo.toml @@ -1,8 +1,10 @@ [package] -name = "example-kernel" +name = "test_kernel_default_settings" version = "0.1.0" authors = ["Philipp Oppermann "] edition = "2018" [dependencies] +bootloader = { path = "../../.." } x86_64 = "0.13.2" +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..50157689 --- /dev/null +++ b/tests/test_kernels/default_settings/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::{entry_point, BootInfo}; +use core::panic::PanicInfo; +use test_kernel_default_settings::{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] +fn panic(_info: &PanicInfo) -> ! { + 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..fab65b53 --- /dev/null +++ b/tests/test_kernels/default_settings/src/bin/check_boot_info.rs @@ -0,0 +1,68 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader::{boot_info::PixelFormat, entry_point, BootInfo}; +use core::panic::PanicInfo; +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 ![640, 1024].contains(&framebuffer.info().horizontal_resolution) { + panic!( + "unexpected horizontal_resolution `{}`", + framebuffer.info().horizontal_resolution + ); + } + if ![480, 768].contains(&framebuffer.info().vertical_resolution) { + panic!( + "unexpected vertical_resolution `{}`", + framebuffer.info().vertical_resolution + ); + } + if ![3, 4].contains(&framebuffer.info().bytes_per_pixel) { + panic!( + "unexpected bytes_per_pixel `{}`", + framebuffer.info().bytes_per_pixel + ); + } + if ![640, 1024].contains(&framebuffer.info().stride) { + panic!("unexpected stride `{}`", framebuffer.info().stride); + } + assert_eq!(framebuffer.info().pixel_format, PixelFormat::BGR); + assert_eq!( + framebuffer.buffer().len(), + framebuffer.info().stride + * framebuffer.info().vertical_resolution + * 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); + assert!(rsdp < 0x000FFFFF); + + // 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. +#[panic_handler] +fn panic(info: &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..48546907 --- /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::{entry_point, BootInfo}; +use core::panic::PanicInfo; +use test_kernel_default_settings::{exit_qemu, QemuExitCode}; + +entry_point!(kernel_main); + +fn kernel_main(_boot_info: &'static mut BootInfo) -> ! { + panic!(); +} + +/// This function is called on panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + 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/example-kernel/x86_64-example-kernel.json b/tests/test_kernels/default_settings/x86_64-default_settings.json similarity index 100% rename from example-kernel/x86_64-example-kernel.json rename to tests/test_kernels/default_settings/x86_64-default_settings.json diff --git a/tests/test_kernels/map_phys_mem/.cargo/config.toml b/tests/test_kernels/map_phys_mem/.cargo/config.toml new file mode 100644 index 00000000..08a49f5a --- /dev/null +++ b/tests/test_kernels/map_phys_mem/.cargo/config.toml @@ -0,0 +1,10 @@ +[unstable] +# TODO: Uncomment once https://github.com/rust-lang/cargo/issues/8643 is merged +# build-std = ["core"] + +[build] +# TODO: Uncomment once https://github.com/rust-lang/cargo/issues/8643 is merged +# target = "x86_64-example-kernel.json" + +[target.'cfg(target_os = "none")'] +runner = "cargo run --manifest-path ../../runner/Cargo.toml" \ No newline at end of file diff --git a/tests/test_kernels/map_phys_mem/.gitignore b/tests/test_kernels/map_phys_mem/.gitignore new file mode 100644 index 00000000..1de56593 --- /dev/null +++ b/tests/test_kernels/map_phys_mem/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file 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..03985391 --- /dev/null +++ b/tests/test_kernels/map_phys_mem/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "test_kernel_map_phys_mem" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2018" + +[dependencies] +bootloader = { path = "../../.." } +x86_64 = "0.13.2" +uart_16550 = "0.2.10" + +[package.metadata.bootloader] +map-physical-memory = true +physical-memory-offset = 0x0000_4000_0000_0000 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..eec06f36 --- /dev/null +++ b/tests/test_kernels/map_phys_mem/src/bin/access_phys_mem.rs @@ -0,0 +1,26 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader::{entry_point, BootInfo}; +use core::panic::PanicInfo; +use test_kernel_map_phys_mem::{exit_qemu, serial, QemuExitCode}; + +entry_point!(kernel_main); + +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. +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + use core::fmt::Write; + + 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..361c4fb4 --- /dev/null +++ b/tests/test_kernels/map_phys_mem/src/bin/check_boot_info.rs @@ -0,0 +1,71 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use bootloader::{boot_info::PixelFormat, entry_point, BootInfo}; +use core::panic::PanicInfo; +use test_kernel_map_phys_mem::{exit_qemu, serial, 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 ![640, 1024].contains(&framebuffer.info().horizontal_resolution) { + panic!( + "unexpected horizontal_resolution `{}`", + framebuffer.info().horizontal_resolution + ); + } + if ![480, 768].contains(&framebuffer.info().vertical_resolution) { + panic!( + "unexpected vertical_resolution `{}`", + framebuffer.info().vertical_resolution + ); + } + if ![3, 4].contains(&framebuffer.info().bytes_per_pixel) { + panic!( + "unexpected bytes_per_pixel `{}`", + framebuffer.info().bytes_per_pixel + ); + } + if ![640, 1024].contains(&framebuffer.info().stride) { + panic!("unexpected stride `{}`", framebuffer.info().stride); + } + assert_eq!(framebuffer.info().pixel_format, PixelFormat::BGR); + assert_eq!( + framebuffer.buffer().len(), + framebuffer.info().stride + * framebuffer.info().vertical_resolution + * 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); + assert!(rsdp < 0x000FFFFF); + + // 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. +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + use core::fmt::Write; + + 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..4e46fdb6 --- /dev/null +++ b/tests/test_kernels/map_phys_mem/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/test-kernel/x86_64-test-kernel.json b/tests/test_kernels/map_phys_mem/x86_64-map_phys_mem.json similarity index 100% rename from test-kernel/x86_64-test-kernel.json rename to tests/test_kernels/map_phys_mem/x86_64-map_phys_mem.json diff --git a/x86_64-bootloader.json b/x86_64-bootloader.json index ad13109d..074ea1bc 100644 --- a/x86_64-bootloader.json +++ b/x86_64-bootloader.json @@ -15,7 +15,7 @@ "os": "none", "features": "-mmx,-sse,+soft-float", "disable-redzone": true, - "panic": "abort", + "panic-strategy": "abort", "executables": true, "relocation_model": "static" }