1
0

Compare commits

...

50 Commits

Author SHA1 Message Date
a1d788fd5f
Comments
All checks were successful
CI / Typos (push) Successful in 7s
CI / Build (push) Successful in 47s
CI / Clippy (push) Successful in 1m0s
2025-03-04 19:18:21 -08:00
cbf6c48b22
Write README
All checks were successful
CI / Typos (push) Successful in 10s
CI / Clippy (push) Successful in 31s
CI / Build (push) Successful in 1m17s
2025-03-02 21:00:16 -08:00
1b401b46b2
Dead code 2025-03-02 20:36:01 -08:00
f5ede33bc8
Remove cpuid checks 2025-03-02 20:35:26 -08:00
de3ff45f1b
Quick drop and lose condition 2025-03-02 20:35:23 -08:00
8f2edd69f7
Reorganize
All checks were successful
CI / Typos (push) Successful in 11s
CI / Clippy (push) Successful in 30s
CI / Build (push) Successful in 1m8s
2025-03-02 10:44:55 -08:00
4250e51f29
Add noise to rng 2025-03-02 10:38:06 -08:00
a2cf8d2592
Update TODO 2025-03-02 10:24:14 -08:00
c70ed40885
Fix arrow keys 2025-03-02 10:23:37 -08:00
ac5529e924
Minor refactor 2025-03-02 10:11:19 -08:00
6ee868591a
Remove unused start() args 2025-03-02 10:06:10 -08:00
4c75d7cca2
Minimal interrupts, prevent deadlocks 2025-03-02 10:03:16 -08:00
0bd0d9d234
Minor build tweak
All checks were successful
CI / Typos (push) Successful in 11s
CI / Clippy (push) Successful in 30s
CI / Build (push) Successful in 1m15s
2025-03-01 20:37:58 -08:00
6f32d2bb7b
Do not publish hash
All checks were successful
CI / Typos (push) Successful in 7s
CI / Build (push) Successful in 45s
CI / Clippy (push) Successful in 53s
2025-03-01 20:35:26 -08:00
8b714bd197
Clear filled rows
Some checks failed
CI / Typos (push) Successful in 12s
CI / Clippy (push) Successful in 36s
CI / Build (push) Has been cancelled
2025-03-01 20:32:23 -08:00
e44bb2767c
Controls and collisions 2025-03-01 20:09:44 -08:00
0f4330bc2e
Fix rotation 2025-03-01 20:09:08 -08:00
6f43b33697
Manifest tweaks 2025-03-01 19:30:04 -08:00
876de98ca9
Minor cleanup
Some checks failed
CI / Typos (push) Successful in 6s
CI / Build (push) Successful in 40s
CI / Clippy (push) Failing after 58s
2025-03-01 10:33:48 -08:00
76e5f8f22d
Minor cleanup
Some checks failed
CI / Typos (push) Successful in 7s
CI / Build (push) Successful in 40s
CI / Clippy (push) Failing after 52s
2025-03-01 09:41:36 -08:00
10b0939930
Random generation 2025-03-01 09:31:48 -08:00
d96044afa2
Minor cleanup
Some checks failed
CI / Typos (push) Successful in 13s
CI / Clippy (push) Failing after 30s
CI / Build (push) Successful in 1m15s
2025-02-28 22:26:37 -08:00
d983fa57a2
TODO 2025-02-28 22:26:27 -08:00
f3df0d22af
Added falling tetrominos 2025-02-28 22:24:59 -08:00
a81ef17429
Initialize PIC 2025-02-27 21:35:00 -08:00
7f0d1f9d59
IDT tweaks 2025-02-25 20:41:55 -08:00
08b9d6e6d4
Added workflow?
Some checks failed
CI / Typos (push) Successful in 11s
CI / Clippy (push) Failing after 31s
CI / Build (push) Successful in 1m14s
2025-02-25 19:40:55 -08:00
1cf1bfeef9
Added IDT 2025-02-25 19:23:28 -08:00
27e9e5afc5
README 2025-02-25 19:23:28 -08:00
f9b9b5a65f
Update toolchain 2025-02-24 21:42:15 -08:00
a812df4b70
Added qemu-gdb target 2025-02-24 21:41:56 -08:00
af903412ef
Remove long mode GTDs 2025-02-24 21:41:10 -08:00
--global
5d6879ade8
Remove allocator 2025-02-19 21:33:20 +00:00
--global
e89ac58bec
Target and makefile cleanup 2025-02-18 21:32:18 +00:00
--global
e84df2364f
Reorganize code 2025-02-18 21:18:27 +00:00
--global
2094a91cc9
Reorganize 2025-02-18 20:39:57 +00:00
816d06ab54
Basic tetris board 2025-02-17 21:33:40 -08:00
dcd7922448
Added VGA graphics 2025-02-17 21:33:29 -08:00
7967791228
Macro cleanup 2025-02-17 21:33:07 -08:00
64e0c47142
message 2025-02-17 17:50:09 -08:00
7f6e3c1b9e
Remove logger 2025-02-17 17:49:09 -08:00
5831a42a10
Format 2025-02-17 17:48:28 -08:00
447a49ce36
Rename crate 2025-02-17 17:47:58 -08:00
99c5102361
Added serial driver 2025-02-17 17:45:56 -08:00
bfd057c541
Remove kernel_entry 2025-02-17 15:30:45 -08:00
c73d8a6b69
Remove kernel code & long mode 2025-02-17 15:30:22 -08:00
5cda26576d
Remove everything 2025-02-17 11:36:46 -08:00
6e3fcbb0be
Remove 64bit code 2025-02-17 11:14:39 -08:00
eef831cb60
Remove cfg arch 2025-02-17 11:03:26 -08:00
014babe444
fmt 2025-02-17 11:02:36 -08:00
64 changed files with 2967 additions and 3168 deletions

View File

@ -9,4 +9,8 @@ trim_trailing_whitespace = false
insert_final_newline = false insert_final_newline = false
[*.asm] [*.asm]
indent_style = space indent_style = space
[*.yml]
indent_size = space
indent_size = 2

82
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,82 @@
name: CI
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
typos:
name: "Typos"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check typos
uses: crate-ci/typos@master
with:
config: ./tools/typos.toml
clippy:
name: "Clippy"
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: "Install Rust"
run: |
sudo apt update
DEBIAN_FRONTEND=noninteractive \
sudo apt install --yes \
rustup
- name: Run clippy
working-directory: ./tetros
run: cargo clippy --all-targets --all-features
build:
name: "Build"
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: "Install Rust"
run: |
sudo apt update
DEBIAN_FRONTEND=noninteractive \
sudo apt install --yes \
rustup nasm python3-requests
- name: Build
run: make
# Upload build output
- name: "Save output"
uses: actions/upload-artifact@v3
with:
name: "Build output"
path: "build/*"
retention-days: 7
#- name: "Publish package (hash)"
# if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
# run: |
# PUBLISH_USER="${{ secrets.PUBLISH_USER }}" \
# PUBLISH_KEY="${{ secrets.PUBLISH_KEY }}" \
# VERSION="${{ github.sha }}" \
# PACKAGE="${{ vars.PACKAGE }}" \
# python tools/scripts/publish.py
- name: "Publish package (latest)"
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
run: |
PUBLISH_USER="${{ secrets.PUBLISH_USER }}" \
PUBLISH_KEY="${{ secrets.PUBLISH_KEY }}" \
VERSION="latest" \
PACKAGE="${{ vars.PACKAGE }}" \
python tools/scripts/publish.py

105
Makefile
View File

@ -2,30 +2,73 @@ BUILD=./build
# Default rule # Default rule
.PHONY: default .PHONY: default
default: all default: $(BUILD)/disk.img
# Remove all build files # Remove all build files
.PHONY: clean .PHONY: clean
clean: clean:
rm -drf $(BUILD) rm -drf $(BUILD)
cd bootloader; make clean cd tetros; cargo clean
# Make everything #
.PHONY: all # MARK: disk
all: img #
# Make bios bootloader # Compile tetros as a library
# dd if=./bootloader/build/stage2.bin of=$@ conv=notrunc bs=512 seek=40 # (so that we can link it with a custom linker script)
LIB_SRC = ./tetros/Cargo.toml ./tetros/Cargo.lock $(shell find ./tetros/src -type f)
$(BUILD)/tetros.lib: $(LIB_SRC)
@mkdir -p $(BUILD)
cd tetros && \
env RUSTFLAGS="-C soft-float" \
cargo rustc \
-Z build-std=core \
-Z build-std-features=compiler-builtins-mem \
--target "./targets/x86-unknown-none.json" \
--lib \
--release \
-- \
--emit link="$(CURDIR)/$@"
.PHONY: img # Link tetros using custom linker script
img: $(BUILD)/disk.img BIOS_LD = ./tetros/linkers/x86-unknown-none.ld
$(BUILD)/disk.img: $(BUILD)/tetros.elf: $(BUILD)/tetros.lib $(BIOS_LD)
mkdir -p $(BUILD) ld \
cd bootloader; make -m elf_i386 \
dd if=/dev/zero of=$@ bs=512 count=32 --gc-sections \
dd if=./bootloader/build/512.bin of=$@ conv=notrunc bs=512 -z max-page-size=0x1000 \
dd if=./bootloader/build/stage2.bin of=$@ conv=notrunc seek=5 bs=512 -T "$(BIOS_LD)" \
-o "$@" \
"$<"
objcopy --only-keep-debug "$@" "$@.sym"
objcopy --strip-debug "$@"
# Wrap tetros in BIOS loader
# Parameters:
# - BIOS_SRC: source directory of bios assembly
# - STAGE2_SECTOR: the index of the first sector of the stage 2 binary on the disk
BIOS_SRC = ./bios
STAGE2_SECTOR = 1
$(BUILD)/disk.img: $(wildcard $(BIOS_SRC)/*.asm) $(BUILD)/tetros.elf
@mkdir -p "$(BUILD)"
nasm \
-f bin \
-D STAGE3=$(BUILD)/tetros.elf \
-D STAGE2_SECTOR=$(STAGE2_SECTOR) \
-o "$@" \
-l "$@.lst" \
-i "$(BIOS_SRC)" \
"$(BIOS_SRC)/main.asm"
#
# MARK: qemu
#
# Do not use `-enable-kvm` or `-cpu host`,
# this confuses gdb.
#
.PHONY: qemu
qemu: $(BUILD)/disk.img qemu: $(BUILD)/disk.img
qemu-system-i386 \ qemu-system-i386 \
-d cpu_reset \ -d cpu_reset \
@ -33,8 +76,32 @@ qemu: $(BUILD)/disk.img
-smp 1 -m 2048 \ -smp 1 -m 2048 \
-machine q35 \ -machine q35 \
-net none \ -net none \
-serial stdio \
-fda "$<" -fda "$<"
# -gdb tcp::26000 \
# -S # Same as qemu, but with no dependency.
# -enable-kvm \ # Used for remote dev, where build box != run box.
# -cpu host \ .PHONY: qemu-remote
qemu-remote:
qemu-system-i386 \
-d cpu_reset \
-no-reboot \
-smp 1 -m 2048 \
-machine q35 \
-net none \
-serial stdio \
-fda "$(BUILD)/disk.img"
# Same as qemu, but with gdb options
.PHONY: qemu-gdb
qemu-gdb: $(BUILD)/disk.img
qemu-system-i386 \
-d cpu_reset \
-no-reboot \
-smp 1 -m 2048 \
-machine q35 \
-net none \
-serial stdio \
-fda "$<" \
-gdb tcp::26000 \
-S

33
README.md Normal file
View File

@ -0,0 +1,33 @@
# TetrOS: bare-metal tetris
## Features
- Compiles to a standalone disk image
- Written from scratch using only Nasm and Rust
- Custom BIOS bootloader
- 32-bit x86 OS
- Detailed comments. Read the [makefile](./Makefile), then start in [`./bios/main.asm`](./bios/main.asm).
## Non-Features
- Never tested on real hardware
- Minimal gameplay and graphics. These features aren't hard to implement, but also don't present any interesting challenges. I have other things to do.
## 🚀 Building and Running
- All scripts are in the [makefile](./Makefile).
- To build and run, use `make qemu`.
- Dependencies: `nasm`, `cargo`, GNU `binutils`, `qemu`
- This will NOT work on MacOS. BSD `ld` does not work like GNU `ld`.
- Alternatively, a compiled disk image for the latest commit is [here](https://git.betalupi.com/api/packages/Mark/generic/tetros/latest/disk.img). This is the same file produced by `make`.
- Download it and run `qemu-system-i386 -d cpu_reset -no-reboot -smp 1 -m 2048 -machine q35 -net none -serial stdio -fda "disk.img"`
## 📜 Resources
**Used directly:**
- [jdh's video](https://www.youtube.com/watch?v=FaILnmUYS_U)
- [The OSDev wiki](https://wiki.osdev.org/Main_Page)
- [RedoxOS bootloader](https://gitlab.redox-os.org/redox-os/bootloader)
**Useful background knowledge:**
- [Writing an OS in Rust](https://os.phil-opp.com)
- [Operating Systems: From 0 to 1](https://github.com/tuhdo/os01)

View File

@ -1,6 +1,6 @@
SECTION .text ; cannot use .data SECTION .text ; cannot use .data
struc GDTEntry struc GDTEntry ; spell:disable-line
.limitl resw 1 .limitl resw 1
.basel resw 1 .basel resw 1
.basem resb 1 .basem resb 1
@ -26,21 +26,21 @@ gdt_attr:
.accessed equ 1 << 0 .accessed equ 1 << 0
;system ;system
; legacy ; legacy
.tssAvailabe16 equ 0x1 .tssAvailabe16 equ 0x1 ; spell:disable-line
.ldt equ 0x2 .ldt equ 0x2
.tssBusy16 equ 0x3 .tssBusy16 equ 0x3
.call16 equ 0x4 .call16 equ 0x4
.task equ 0x5 .task equ 0x5
.interrupt16 equ 0x6 .interrupt16 equ 0x6
.trap16 equ 0x7 .trap16 equ 0x7
.tssAvailabe32 equ 0x9 .tssAvailabe32 equ 0x9 ; spell:disable-line
.tssBusy32 equ 0xB .tssBusy32 equ 0xB
.call32 equ 0xC .call32 equ 0xC
.interrupt32 equ 0xE .interrupt32 equ 0xE
.trap32 equ 0xF .trap32 equ 0xF
; long mode ; long mode
.ldt32 equ 0x2 .ldt32 equ 0x2
.tssAvailabe64 equ 0x9 .tssAvailabe64 equ 0x9 ; spell:disable-line
.tssBusy64 equ 0xB .tssBusy64 equ 0xB
.call64 equ 0xC .call64 equ 0xC
.interrupt64 equ 0xE .interrupt64 equ 0xE
@ -64,27 +64,6 @@ gdt:
.null equ $ - gdt .null equ $ - gdt
dq 0 dq 0
.lm64_code equ $ - gdt
istruc GDTEntry
at GDTEntry.limitl, dw 0
at GDTEntry.basel, dw 0
at GDTEntry.basem, db 0
at GDTEntry.attribute, db gdt_attr.present | gdt_attr.user | gdt_attr.code
at GDTEntry.flags__limith, db gdt_flag.long_mode
at GDTEntry.baseh, db 0
iend
.lm64_data equ $ - gdt
istruc GDTEntry
at GDTEntry.limitl, dw 0
at GDTEntry.basel, dw 0
at GDTEntry.basem, db 0
; AMD System Programming Manual states that the writeable bit is ignored in long mode, but ss can not be set to this descriptor without it
at GDTEntry.attribute, db gdt_attr.present | gdt_attr.user | gdt_attr.writable
at GDTEntry.flags__limith, db 0
at GDTEntry.baseh, db 0
iend
; All GTD addresses are multiples of 8, ; All GTD addresses are multiples of 8,
; and thus end in three zero bits. ; and thus end in three zero bits.
; ;

64
bios/main.asm Normal file
View File

@ -0,0 +1,64 @@
sectalign off
; The following code expects two external macros:
; STAGE3, a path to the stage3 binary
; STAGE2_SECTOR, the location of stage 2
; on the disk, in 512-byte sectors.
;
; Both of these are set in the makefile.
; BIOS loads stage 1 at 0x7C00
ORG 0x7C00
SECTION .text
; Stage 1 is MBR code, and should fit in LBA 0
; (i.e, in the first 512 bytes).
%include "stage1.asm"
; Stage 1 is at most 440 bytes
; This limit is set by the GPT spec.
; See https://uefi.org/specs/UEFI/2.10/05_GUID_Partition_Table_Format.html
;
; This `times` will throw an error if the subtraction is negative.
times 440-($-$$) db 0
db 0xee
; Pad until 512
times 510-($-$$) db 0
; MBR signature.
; This tells the BIOS that this disk is bootable.
db 0x55
db 0xaa
; Include stage 2. This is loaded into memory by stage 1.
; (stage 1 loads both stage 2 and stage 3)
;
; Stage 2 sets up protected mode, sets up the GDT,
; and initializes a minimal environment for stage 3.
;
; On a "real" boot disk, this data will not immediately follow stage 1.
; It would be stored in a special disk partition.
;
; We don't need this kind of complexity here, though, so we store
; stage 2 right after stage 1. (This is why STAGE2_SECTOR is 1.)
;
; This is nice, because the layout of the code on our boot disk
; matches the layout of the code in memory. THIS IS NOT USUALLY THE CASE.
stage2:
%include "stage2.asm"
align 512, db 0
stage2.end:
; Pad to 0x3000.
; This makes sure that state3 is loaded at the address
; the linker expects. Must match the value in `tetros/linkers/x86-unknown-none.ld`.
times (0x8000 - 0x7c00)-($-$$) db 0
; Include stage 3, the binary compiled from Rust sources.
stage3:
%defstr STAGE3_STR %[STAGE3]
incbin STAGE3_STR
align 512, db 0
.end:

View File

@ -1,22 +1,21 @@
SECTION .text SECTION .text
USE16 USE16
; provide function for printing in x86 real mode ; Print a string and a newline
;
; print a string and a newline ; Clobbers ax
; CLOBBER
; ax
print_line: print_line:
mov al, 13 mov al, 13
call print_char call print_char
mov al, 10 mov al, 10
jmp print_char jmp print_char
; print a string ; Print a string
; IN ;
; Input:
; si: points at zero-terminated String ; si: points at zero-terminated String
; CLOBBER ;
; si, ax ; Clobbers si, ax
print: print:
pushf pushf
cld cld
@ -30,8 +29,9 @@ print:
popf popf
ret ret
; print a character ; Print a character
; IN ;
; Input:
; al: character to print ; al: character to print
print_char: print_char:
pusha pusha
@ -42,10 +42,11 @@ print_char:
ret ret
; print a number in hex ; print a number in hex
; IN ;
; Input:
; bx: the number ; bx: the number
; CLOBBER ;
; al, cx ; Clobbers al, cx
print_hex: print_hex:
mov cx, 4 mov cx, 4
.lp: .lp:

View File

@ -1,37 +1,39 @@
USE16 USE16
stage1: ; dl comes with disk stage1:
; initialize segment registers ; Initialize segment registers
xor ax, ax xor ax, ax ; Set ax to 0
mov ds, ax mov ds, ax
mov es, ax mov es, ax
mov ss, ax mov ss, ax
; initialize stack ; Initialize stack pointer
; (stack grows up)
mov sp, 0x7C00 mov sp, 0x7C00
; initialize CS ; Initialize CS
; far jump sets both CS and IP to a known-good state, ;
; we don't know where the BIOS put us at startup. ; `retf` sets both CS and IP to a known-good state.
; (could be 0x00:0x7C00, could be 0x7C00:0x00. ; This is necessary because we don't know where the BIOS put us at startup.
; Not everybody follows spec.) ; (could be 0x00:0x7C00, could be 0x7C00:0x00. Not everybody follows spec.)
push ax push ax ; `ax` is still 0
push word .set_cs push word .set_cs
retf retf
.set_cs: .set_cs:
; save disk number ; Save disk number.
; BIOS sets `dl` to the number of
; the disk we're booting from.
mov [disk], dl mov [disk], dl
; Print "Stage 1"
mov si, stage_msg mov si, stage_msg
call print call print
mov al, '1' mov al, '1'
call print_char call print_char
call print_line call print_line
; read CHS gemotry, save into [chs]
; read CHS gemotry
; CL (bits 0-5) = maximum sector number ; CL (bits 0-5) = maximum sector number
; CL (bits 6-7) = high bits of max cylinder number ; CL (bits 6-7) = high bits of max cylinder number
; CH = low bits of maximum cylinder number ; CH = low bits of maximum cylinder number
@ -51,11 +53,10 @@ stage1: ; dl comes with disk
and cl, 0x3f and cl, 0x3f
mov [chs.s], cl mov [chs.s], cl
; disk address of stage 2 ; First sector of stage 2
; (start sector)
mov eax, STAGE2_SECTOR mov eax, STAGE2_SECTOR
; where to load stage 2 ; Where to load stage 2
mov bx, stage2 mov bx, stage2
; length of stage2 + stage3 ; length of stage2 + stage3
@ -63,36 +64,40 @@ stage1: ; dl comes with disk
mov cx, (stage3.end - stage2) / 512 mov cx, (stage3.end - stage2) / 512
mov dx, 0 mov dx, 0
; Consume eax, bx, cx, dx
; and load code from disk.
call load call load
jmp stage2.entry jmp stage2.entry
; load some sectors from disk to a buffer in memory ; Load sectors from disk to memory.
; buffer has to be below 1MiB ; Cannot load more than 1MiB.
; IN ;
; Input:
; ax: start sector ; ax: start sector
; bx: offset of buffer ; bx: offset of buffer
; cx: number of sectors (512 Bytes each) ; cx: number of sectors (512 Bytes each)
; dx: segment of buffer ; dx: segment of buffer
; CLOBBER ;
; ax, bx, cx, dx, si ; Clobbers ax, bx, cx, dx, si
; TODO rewrite to (eventually) move larger parts at once
; if that is done increase buffer_size_sectors in startup-common to that (max 0x80000 - startup_end)
load: load:
; replaced 127 with 1. ; Every "replace 1" comment means that the `1`
; see https://stackoverflow.com/questions/58564895/problem-with-bios-int-13h-read-sectors-from-drive ; on that line could be bigger.
; TODO: fix later ;
; See https://stackoverflow.com/questions/58564895/problem-with-bios-int-13h-read-sectors-from-drive
; We have to load one sector at a time to avoid the 1K boundary error.
; Would be nice to read more sectors at a time, though, that's faster.
cmp cx, 1 ;127 cmp cx, 1 ; replace 1
jbe .good_size jbe .good_size
pusha pusha
mov cx, 1; 127 mov cx, 1 ; replace 1
call load call load
popa popa
add eax, 1; 127 add eax, 1 ; replace 1
add dx, 1 * 512 / 16 ; 127 add dx, 1 * 512 / 16 ; replace 1
sub cx, 1;127 sub cx, 1 ; replace 1
jmp load jmp load
.good_size: .good_size:
@ -101,44 +106,37 @@ load:
mov [DAPACK.count], cx mov [DAPACK.count], cx
mov [DAPACK.seg], dx mov [DAPACK.seg], dx
; This should be a subroutine, ; Print the data we're reading
; but we don't call/ret to save a few bytes. ; Prints AAAAAAAA#BBBB CCCC:DDDD, where:
; (we only use this once) ; - A..A is the lba we're reading (printed in two parts)
; ; - BBBB is the number of sectors we're reading
;call print_dapack ; - CCCC is the index we're writing to
;print_dapack: ; - DDDD is the buffer we're writing to
mov bx, [DAPACK.addr + 2] mov bx, [DAPACK.addr + 2] ; last two bytes
call print_hex call print_hex
mov bx, [DAPACK.addr] ; first two bytes
mov bx, [DAPACK.addr]
call print_hex call print_hex
mov al, '#' mov al, '#'
call print_char call print_char
mov bx, [DAPACK.count] mov bx, [DAPACK.count]
call print_hex call print_hex
mov al, ' ' mov al, ' '
call print_char call print_char
mov bx, [DAPACK.seg] mov bx, [DAPACK.seg]
call print_hex call print_hex
mov al, ':' mov al, ':'
call print_char call print_char
mov bx, [DAPACK.buf] mov bx, [DAPACK.buf]
call print_hex call print_hex
call print_line call print_line
;ret
; End of print_dapack
; Read from disk.
; int13h, ah=0x42 does not work on some disks.
; use int13h, ah=0x02 in this case.
cmp byte [chs.s], 0 cmp byte [chs.s], 0
jne .chs jne .chs
;INT 0x13 extended read does not work on CDROM!
mov dl, [disk] mov dl, [disk]
mov si, DAPACK mov si, DAPACK
mov ah, 0x42 mov ah, 0x42
@ -188,6 +186,10 @@ load:
jc error ; carry flag set on error jc error ; carry flag set on error
ret ret
;
; MARK: errors
;
error_chs: error_chs:
mov ah, 0 mov ah, 0
@ -200,13 +202,18 @@ error:
mov si, stage1_error_msg mov si, stage1_error_msg
call print call print
call print_line call print_line
; halt after printing error details
.halt: .halt:
cli cli
hlt hlt
jmp .halt jmp .halt
;
; MARK: data
;
%include "print.asm" %include "print.asm"
stage_msg: db "Stage ",0 stage_msg: db "Stage ",0
@ -215,9 +222,9 @@ stage1_error_msg: db " ERROR",0
disk: db 0 disk: db 0
chs: chs:
.c: dd 0 .c: dd 0
.h: dd 0 .h: dd 0
.s: dd 0 .s: dd 0
DAPACK: DAPACK:
db 0x10 db 0x10
@ -225,6 +232,4 @@ DAPACK:
.count: dw 0 ; int 13 resets this to # of blocks actually read/written .count: dw 0 ; int 13 resets this to # of blocks actually read/written
.buf: dw 0 ; memory buffer destination address (0:7c00) .buf: dw 0 ; memory buffer destination address (0:7c00)
.seg: dw 0 ; in memory page zero .seg: dw 0 ; in memory page zero
.addr: dq 0 ; put the lba to read in this spot .addr: dq 0 ; put the lba to read in this spot
db 0xff

72
bios/stage2.asm Normal file
View File

@ -0,0 +1,72 @@
SECTION .text
USE16
%include "gdt.asm"
%include "thunk.asm"
stage2.entry:
mov si, stage_msg
call print
mov al, '3'
call print_char
call print_line
; enable A20-Line via IO-Port 92, might not work on all motherboards
in al, 0x92
or al, 2
out 0x92, al
protected_mode:
; disable interrupts
cli
; load protected mode GDT
lgdt [gdtr]
; set protected mode bit of cr0
mov eax, cr0
or eax, 1
mov cr0, eax
; far jump to load CS with 32 bit segment
; We need to do this because we are entering 32-bit mode,
; but the instruction pipeline still has 16-bit instructions.
;
; gdt.pm32_code is a multiple of 8, so it always ends with three zero bits.
; The GDT spec abuses this fact, and uses these last three bits to store other
; data (table type and privilege). In this case, 000 is what we need anyway.
;
; Also note that CS isn't an address in protected mode---it's a GDT descriptor.
jmp gdt.pm32_code:protected_mode_inner
; We can now use 32-bit instructions!
USE32
protected_mode_inner:
; load all the other segments with 32 bit data segments
mov eax, gdt.pm32_data
mov ds, eax
mov es, eax
mov fs, eax
mov gs, eax
mov ss, eax
; Place stage 3 stack at 448 KiB
; (512KiB minus 64KiB disk buffer)
mov esp, 0x70000
; push arguments to `start()`
mov eax, thunk.int10
push eax
; Call `start()`.
; 0x18 skips ELF headers.
mov eax, [stage3 + 0x18]
call eax
.halt:
; Halt if `start()` ever returns (it shouldn't, but just in case)
; Without this, we'll try to execute whatever comes next in memory.
cli
hlt
jmp .halt

View File

@ -1,3 +1,10 @@
; Thunk allows stage 3 (rust code)
; to use interrupts that are not
; usually available in protected mode.
;
; "thunk": a subroutine used to inject
; a calculation into another subroutine.
SECTION .text SECTION .text
USE32 USE32
@ -6,18 +13,6 @@ thunk:
mov dword [.func], .int10_real mov dword [.func], .int10_real
jmp .enter jmp .enter
.int13:
mov dword [.func], .int13_real
jmp .enter
.int15:
mov dword [.func], .int15_real
jmp .enter
.int16:
mov dword [.func], .int16_real
jmp .enter
.func: dd 0 .func: dd 0
.esp: dd 0 .esp: dd 0
.cr0: dd 0 .cr0: dd 0
@ -65,18 +60,6 @@ USE16
int 0x10 int 0x10
ret ret
.int13_real:
int 0x13
ret
.int15_real:
int 0x15
ret
.int16_real:
int 0x16
ret
.pm16: .pm16:
; set segment selectors to protected mode 16-bit ; set segment selectors to protected mode 16-bit
mov eax, gdt.pm16_data mov eax, gdt.pm16_data

View File

@ -1 +0,0 @@
build

View File

@ -1,83 +0,0 @@
# This compiles our bootloader as a static library,
# and wraps it in a multistage loader.
BUILD = ./build
.PHONY: all
all: $(BUILD)/mbr.bin $(BUILD)/512.bin $(BUILD)/stage2.bin
.PHONY: clean
clean:
rm -drf $(BUILD)
cd bootloader; cargo clean
# Compile bootloader as library
LIB_SRC = ./bootloader/Cargo.toml ./bootloader/Cargo.lock $(shell find ./bootloader/src -type f)
$(BUILD)/bootloader.lib: $(LIB_SRC)
@mkdir -p $(BUILD)
cd bootloader && \
env RUSTFLAGS="-C soft-float" \
cargo rustc \
--manifest-path="./Cargo.toml" \
-Z build-std=core,alloc \
-Z build-std-features=compiler-builtins-mem \
--target "./targets/x86-unknown-none.json" \
--lib \
--release \
-- \
--emit link="$(CURDIR)/$@"
# Link bootloader
BIOS_LD = ./bootloader/linkers/x86-unknown-none.ld
$(BUILD)/bootloader.elf: $(BUILD)/bootloader.lib $(BIOS_LD)
ld \
-m elf_i386 \
--gc-sections \
-z max-page-size=0x1000 \
-T "$(BIOS_LD)" \
-o "$@" \
"$<"
objcopy --only-keep-debug "$@" "$@.sym"
objcopy --strip-debug "$@"
# Wrap bootloader in three-stage BIOS loader
# Parameters:
# - BIOS_SRC: source directory of bios assembly
# - STAGE3: path to linked stage 3 binary
# - STAGE2_SECTOR: the index of the first sector of the stage 2 binary on the disk
BIOS_SRC = ./bios
STAGE2_SECTOR = 5
STAGE3 = $(BUILD)/bootloader.elf
$(BUILD)/bios.bin: $(wildcard $(BIOS_SRC)/*.asm) $(STAGE3)
@mkdir -p "$(BUILD)"
nasm \
-f bin \
-D STAGE3=$(STAGE3) \
-D STAGE2_SECTOR=$(STAGE2_SECTOR) \
-o "$@" \
-l "$@.lst" \
-i "$(BIOS_SRC)" \
"$(BIOS_SRC)/main.asm"
# Extract MBR code (first 440 bytes)
# This can be used to embed this mbr in gpt-partitioned disks
$(BUILD)/mbr.bin: $(BUILD)/bios.bin
@mkdir -p "$(BUILD)"
@echo ""
dd if="$<" bs=440 count=1 of="$@"
# Extract full mbr (first 512 bytes)
# This can be used to make raw boot disks
$(BUILD)/512.bin: $(BUILD)/bios.bin
@mkdir -p "$(BUILD)"
@echo ""
dd if="$<" bs=512 count=1 of="$@"
# Extract stage 2 (rest of file)
$(BUILD)/stage2.bin: $(BUILD)/bios.bin
@mkdir -p "$(BUILD)"
@echo ""
dd if="$<" bs=512 skip=1 of="$@"

View File

@ -1,176 +0,0 @@
SECTION .text
USE16
cpuid_required_features:
.edx equ cpuid_edx.fpu | cpuid_edx.pse | cpuid_edx.pge | cpuid_edx.fxsr
.ecx equ 0
cpuid_check:
; If bit 21 of EFLAGS can be changed, then CPUID is supported
pushfd ;Save EFLAGS
pushfd ;Store EFLAGS
xor dword [esp],0x00200000 ;Invert the ID bit in stored EFLAGS
popfd ;Load stored EFLAGS (with ID bit inverted)
pushfd ;Store EFLAGS again (ID bit may or may not be inverted)
pop eax ;eax = modified EFLAGS (ID bit may or may not be inverted)
xor eax,[esp] ;eax = whichever bits were changed
popfd ;Restore original EFLAGS
test eax,0x00200000 ;eax = zero if ID bit can't be changed, else non-zero
jz .no_cpuid
mov eax, 1
cpuid
and edx, cpuid_required_features.edx
cmp edx, cpuid_required_features.edx
jne .error
and ecx, cpuid_required_features.ecx
cmp ecx, cpuid_required_features.ecx
jne .error
ret
.no_cpuid:
mov si, .msg_cpuid
call print
mov si, .msg_line
call print
jmp .halt
.error:
push ecx
push edx
mov si, .msg_features
call print
mov si, .msg_line
call print
mov si, .msg_edx
call print
pop ebx
push ebx
shr ebx, 16
call print_hex
pop ebx
call print_hex
mov si, .msg_must_contain
call print
mov ebx, cpuid_required_features.edx
shr ebx, 16
call print_hex
mov ebx, cpuid_required_features.edx
call print_hex
mov si, .msg_line
call print
mov si, .msg_ecx
call print
pop ebx
push ebx
shr ebx, 16
call print_hex
pop ebx
call print_hex
mov si, .msg_must_contain
call print
mov ebx, cpuid_required_features.ecx
shr ebx, 16
call print_hex
mov ebx, cpuid_required_features.ecx
call print_hex
mov si, .msg_line
call print
.halt:
cli
hlt
jmp .halt
.msg_cpuid: db "CPUID not supported",0
.msg_features: db "Required CPU features are not present",0
.msg_line: db 13,10,0
.msg_edx: db "EDX ",0
.msg_ecx: db "ECX ",0
.msg_must_contain: db " must contain ",0
cpuid_edx:
.fpu equ 1 << 0
.vme equ 1 << 1
.de equ 1 << 2
.pse equ 1 << 3
.tsc equ 1 << 4
.msr equ 1 << 5
.pae equ 1 << 6
.mce equ 1 << 7
.cx8 equ 1 << 8
.apic equ 1 << 9
.sep equ 1 << 11
.mtrr equ 1 << 12
.pge equ 1 << 13
.mca equ 1 << 14
.cmov equ 1 << 15
.pat equ 1 << 16
.pse_36 equ 1 << 17
.psn equ 1 << 18
.clfsh equ 1 << 19
.ds equ 1 << 21
.acpi equ 1 << 22
.mmx equ 1 << 23
.fxsr equ 1 << 24
.sse equ 1 << 25
.sse2 equ 1 << 26
.ss equ 1 << 27
.htt equ 1 << 28
.tm equ 1 << 29
.ia64 equ 1 << 30
.pbe equ 1 << 31
cpuid_ecx:
.sse3 equ 1 << 0
.pclmulqdq equ 1 << 1
.dtes64 equ 1 << 2
.monitor equ 1 << 3
.ds_cpl equ 1 << 4
.vmx equ 1 << 5
.smx equ 1 << 6
.est equ 1 << 7
.tm2 equ 1 << 8
.ssse3 equ 1 << 9
.cnxt_id equ 1 << 10
.sdbg equ 1 << 11
.fma equ 1 << 12
.cmpxchg16b equ 1 << 13
.xtpr equ 1 << 14
.pdcm equ 1 << 15
.pcid equ 1 << 17
.dca equ 1 << 18
.sse4_1 equ 1 << 19
.sse4_2 equ 1 << 20
.x2apic equ 1 << 21
.movbe equ 1 << 22
.popcnt equ 1 << 23
.tsc_deadline equ 1 << 24
.aes equ 1 << 25
.xsave equ 1 << 26
.osxsave equ 1 << 27
.avx equ 1 << 28
.f16c equ 1 << 29
.rdrand equ 1 << 30
.hypervisor equ 1 << 31

View File

@ -1,4 +0,0 @@
; sector = 512 bytes
; first sector of stage 2, on disk.
%assign PARAM_STAGE2_SECTOR 34

View File

@ -1,56 +0,0 @@
SECTION .text
USE32
long_mode:
.func: dq 0
.page_table: dd 0
.entry:
; disable interrupts
cli
; disable paging
mov eax, cr0
and eax, 0x7FFFFFFF
mov cr0, eax
; enable FXSAVE/FXRSTOR, Page Global, Page Address Extension, and Page Size Extension
mov eax, cr4
or eax, 1 << 9 | 1 << 7 | 1 << 5 | 1 << 4
mov cr4, eax
; load long mode GDT
lgdt [gdtr]
; enable long mode
mov ecx, 0xC0000080 ; Read from the EFER MSR.
rdmsr
or eax, 1 << 11 | 1 << 8 ; Set the Long-Mode-Enable and NXE bit.
wrmsr
; set page table
mov eax, [.page_table]
mov cr3, eax
; enabling paging and protection simultaneously
mov eax, cr0
or eax, 1 << 31 | 1 << 16 | 1 ;Bit 31: Paging, Bit 16: write protect kernel, Bit 0: Protected Mode
mov cr0, eax
; far jump to enable Long Mode and load CS with 64 bit segment
jmp gdt.lm64_code:.inner
USE64
.inner:
; load all the other segments with 64 bit data segments
mov rax, gdt.lm64_data
mov ds, rax
mov es, rax
mov fs, rax
mov gs, rax
mov ss, rax
; jump to specified function
mov rax, [.func]
jmp rax

View File

@ -1,66 +0,0 @@
sectalign off
; This program expects two external macros:
; STAGE3, a path to the stage3 binary
; STAGE2_SECTOR, the location of stage 2
; on the disk, in 512-byte sectors.
; On a gpt disk, this is probably 34.
; Stage 1 is MBR code, and should fit in LBA 0
; (512 bytes). Layout is as follows:
; (Format is `offset, length: purpose`)
; 0, 424: x86 boot code
; 440, 4: Unique disk signature
; 444, 2: unknown
; 446, 16*4: Array of four legacy MBR records
; 510, 2: signature 0x55 0xAA
; 512 to end of logical block: reserved
;
; See https://uefi.org/specs/UEFI/2.10/05_GUID_Partition_Table_Format.html
ORG 0x7C00
SECTION .text
; stage 1 is sector 0, loaded into memory at 0x7C00
%include "stage1.asm"
; Stage 1 is at most 440 bytes
times 440-($-$$) db 0
db 0xee
; Pad until 512
times 510-($-$$) db 0
; MBR signature.
; This isn't loaded into memory, it's
; only here for debugging.
db 0x55
db 0xaa
stage2:
%include "stage2.asm"
align 512, db 0
stage2.end:
; TODO: why? Stage 1 read limit?
; Can we make this smaller?
; The maximum size of stage2 is 4 KiB,
; This fill will throw an error if the subtraction
; is negative.
times (4*1024)-($-stage2) db 0
; LEGACY
; Pad to 0x13000
; This needs to match the value configured
; in the stage3 linker script
times (0x13000 - 0x7c00)-($-$$) db 0
stage3:
%defstr STAGE3_STR %[STAGE3]
incbin STAGE3_STR
align 512, db 0
.end:
; TODO: why? Of the disk, or of memory?
; the maximum size of the boot loader portion is 384 KiB
times (384*1024)-($-$$) db 0

View File

@ -1,46 +0,0 @@
SECTION .text
USE16
protected_mode:
.func: dd 0
.entry:
; disable interrupts
cli
; load protected mode GDT
lgdt [gdtr]
; set protected mode bit of cr0
mov eax, cr0
or eax, 1
mov cr0, eax
; far jump to load CS with 32 bit segment
; (we are in 32-bit mode, but instruction pipeline
; has 16-bit instructions.
jmp gdt.pm32_code:.inner
; gdt.pm32_code is a multiple of 8, so it always ends with three zero bits.
; The GDT spec abuses this fact, and uses these last three bits to store other
; data (table type and privilege). In this case, 000 is what we need anyway.
;
; Also note that CS isn't an address in protected mode---it's a GDT descriptor.
USE32
.inner:
; load all the other segments with 32 bit data segments
mov eax, gdt.pm32_data
mov ds, eax
mov es, eax
mov fs, eax
mov gs, eax
mov ss, eax
; jump to specified function
mov eax, [.func]
jmp eax

View File

@ -1,141 +0,0 @@
SECTION .text
USE16
stage2.entry:
mov si, stage_msg
call print
mov al, '3'
call print_char
call print_line
; check for required features
call cpuid_check
; enable A20-Line via IO-Port 92, might not work on all motherboards
in al, 0x92
or al, 2
out 0x92, al
mov dword [protected_mode.func], stage3.entry
jmp protected_mode.entry
%include "cpuid.asm"
%include "gdt.asm"
%include "long_mode.asm"
%include "protected_mode.asm"
%include "thunk.asm"
USE32
stage3.entry:
; stage3 stack at 448 KiB (512KiB minus 64KiB disk buffer)
mov esp, 0x70000
; push arguments
mov eax, thunk.int16
push eax
mov eax, thunk.int15
push eax
mov eax, thunk.int13
push eax
mov eax, thunk.int10
push eax
xor eax, eax
mov al, [disk]
push eax
mov eax, kernel.entry
push eax
mov eax, [stage3 + 0x18]
call eax
.halt:
cli
hlt
jmp .halt
kernel:
.stack: dq 0
.func: dq 0
.args: dq 0
.entry:
; page_table: usize
mov eax, [esp + 4]
mov [long_mode.page_table], eax
; stack: u64
mov eax, [esp + 8]
mov [.stack], eax
mov eax, [esp + 12]
mov [.stack + 4], eax
; func: u64
mov eax, [esp + 16]
mov [.func], eax
mov eax, [esp + 20]
mov [.func + 4], eax
; args: *const KernelArgs
mov eax, [esp + 24]
mov [.args], eax
; long_mode: usize
mov eax, [esp + 28]
test eax, eax
jz .inner32
mov eax, .inner64
mov [long_mode.func], eax
jmp long_mode.entry
.inner32:
; disable paging
mov eax, cr0
and eax, 0x7FFFFFFF
mov cr0, eax
;TODO: PAE (1 << 5)
; enable FXSAVE/FXRSTOR, Page Global, and Page Size Extension
mov eax, cr4
or eax, 1 << 9 | 1 << 7 | 1 << 4
mov cr4, eax
; set page table
mov eax, [long_mode.page_table]
mov cr3, eax
; enabling paging and protection simultaneously
mov eax, cr0
; Bit 31: Paging, Bit 16: write protect kernel, Bit 0: Protected Mode
or eax, 1 << 31 | 1 << 16 | 1
mov cr0, eax
; enable FPU
;TODO: move to Rust
mov eax, cr0
and al, 11110011b ; Clear task switched (3) and emulation (2)
or al, 00100010b ; Set numeric error (5) monitor co-processor (1)
mov cr0, eax
fninit
mov esp, [.stack]
mov eax, [.args]
push eax
mov eax, [.func]
call eax
.halt32:
cli
hlt
jmp .halt32
USE64
.inner64:
mov rsp, [.stack]
mov rax, [.func]
mov rdi, [.args]
call rax
.halt64:
cli
hlt
jmp .halt64

View File

@ -1,264 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aes"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"opaque-debug",
]
[[package]]
name = "argon2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73"
dependencies = [
"base64ct",
"blake2",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cipher"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
dependencies = [
"generic-array",
]
[[package]]
name = "cpufeatures"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "endian-num"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8f59926911ef34d1efb9ea1ee8ca78385df62ce700ccf2bcb149011bd226888"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "libc"
version = "0.2.168"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
[[package]]
name = "linked_list_allocator"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afa463f5405ee81cdb9cc2baf37e08ec7e4c8209442b5d72c04cfb2cd6e6286"
dependencies = [
"spinning_top",
]
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "redox-path"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436d45c2b6a5b159d43da708e62b25be3a4a3d5550d654b72216ade4c4bfd717"
[[package]]
name = "redox_bootloader"
version = "1.0.0"
dependencies = [
"bitflags 1.3.2",
"linked_list_allocator",
"log",
"redox_syscall",
"redoxfs",
"spin",
]
[[package]]
name = "redox_syscall"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "redoxfs"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c8756cd15a32143479f4a723ececcbe2b8e422242924b0628a163826aea8c4d"
dependencies = [
"aes",
"argon2",
"base64ct",
"endian-num",
"libc",
"log",
"redox-path",
"redox_syscall",
"seahash",
"uuid",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "seahash"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "spinning_top"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b9eb1a2f4c41445a3a0ff9abc5221c5fcd28e1f13cd7c0397706f9ac938ddb0"
dependencies = [
"lock_api",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "uuid"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"

View File

@ -1,5 +0,0 @@
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
pub use self::x86::*;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
mod x86;

View File

@ -1,31 +0,0 @@
use redoxfs::Disk;
use crate::os::{Os, OsVideoMode};
pub(crate) mod x32;
pub(crate) mod x64;
pub unsafe fn paging_create<D: Disk, V: Iterator<Item = OsVideoMode>>(
os: &dyn Os<D, V>,
kernel_phys: u64,
kernel_size: u64,
) -> Option<usize> {
if crate::KERNEL_64BIT {
x64::paging_create(os, kernel_phys, kernel_size)
} else {
x32::paging_create(os, kernel_phys, kernel_size)
}
}
pub unsafe fn paging_framebuffer<D: Disk, V: Iterator<Item = OsVideoMode>>(
os: &dyn Os<D, V>,
page_phys: usize,
framebuffer_phys: u64,
framebuffer_size: u64,
) -> Option<u64> {
if crate::KERNEL_64BIT {
x64::paging_framebuffer(os, page_phys, framebuffer_phys, framebuffer_size)
} else {
x32::paging_framebuffer(os, page_phys, framebuffer_phys, framebuffer_size)
}
}

View File

@ -1,89 +0,0 @@
use crate::area_add;
use crate::os::{Os, OsMemoryEntry, OsMemoryKind, OsVideoMode};
use core::slice;
use redoxfs::Disk;
const PAGE_ENTRIES: usize = 1024;
const PAGE_SIZE: usize = 4096;
pub(crate) const PHYS_OFFSET: u32 = 0x8000_0000;
unsafe fn paging_allocate<D: Disk, V: Iterator<Item = OsVideoMode>>(
os: &dyn Os<D, V>,
) -> Option<&'static mut [u32]> {
let ptr = os.alloc_zeroed_page_aligned(PAGE_SIZE);
if !ptr.is_null() {
area_add(OsMemoryEntry {
base: ptr as u64,
size: PAGE_SIZE as u64,
kind: OsMemoryKind::Reclaim,
});
Some(slice::from_raw_parts_mut(ptr as *mut u32, PAGE_ENTRIES))
} else {
None
}
}
pub unsafe fn paging_create<D: Disk, V: Iterator<Item = OsVideoMode>>(
os: &dyn Os<D, V>,
kernel_phys: u64,
kernel_size: u64,
) -> Option<usize> {
let pd = paging_allocate(os)?;
//Identity map 1 GiB using 4 MiB pages, also map at PHYS_OFFSET
for pd_i in 0..256 {
let addr = pd_i as u32 * 0x40_0000;
pd[pd_i] = addr | 1 << 7 | 1 << 1 | 1;
pd[pd_i + 512] = addr | 1 << 7 | 1 << 1 | 1;
}
// Map kernel_size at kernel offset
let mut kernel_mapped = 0;
let mut pd_i = 0xC000_0000 / 0x40_0000;
while kernel_mapped < kernel_size && pd_i < pd.len() {
let pt = paging_allocate(os)?;
pd[pd_i] = pt.as_ptr() as u32 | 1 << 1 | 1;
pd_i += 1;
let mut pt_i = 0;
while kernel_mapped < kernel_size && pt_i < pt.len() {
let addr = kernel_phys + kernel_mapped;
pt[pt_i] = addr as u32 | 1 << 1 | 1;
pt_i += 1;
kernel_mapped += PAGE_SIZE as u64;
}
}
assert!(kernel_mapped >= kernel_size);
Some(pd.as_ptr() as usize)
}
pub unsafe fn paging_framebuffer<D: Disk, V: Iterator<Item = OsVideoMode>>(
os: &dyn Os<D, V>,
page_phys: usize,
framebuffer_phys: u64,
framebuffer_size: u64,
) -> Option<u64> {
let framebuffer_virt = 0xD000_0000; // 256 MiB after kernel mapping, but before heap mapping
let pd = slice::from_raw_parts_mut(page_phys as *mut u32, PAGE_ENTRIES);
// Map framebuffer_size at framebuffer offset
let mut framebuffer_mapped = 0;
let mut pd_i = framebuffer_virt / 0x40_0000;
while framebuffer_mapped < framebuffer_size && pd_i < pd.len() {
let pt = paging_allocate(os)?;
pd[pd_i] = pt.as_ptr() as u32 | 1 << 1 | 1;
pd_i += 1;
let mut pt_i = 0;
while framebuffer_mapped < framebuffer_size && pt_i < pt.len() {
let addr = framebuffer_phys + framebuffer_mapped;
pt[pt_i] = addr as u32 | 1 << 1 | 1;
pt_i += 1;
framebuffer_mapped += PAGE_SIZE as u64;
}
}
assert!(framebuffer_mapped >= framebuffer_size);
Some(framebuffer_virt as u64)
}

View File

@ -1,149 +0,0 @@
use core::slice;
use redoxfs::Disk;
use crate::area_add;
use crate::os::{Os, OsMemoryEntry, OsMemoryKind, OsVideoMode};
const ENTRY_ADDRESS_MASK: u64 = 0x000F_FFFF_FFFF_F000;
const PAGE_ENTRIES: usize = 512;
const PAGE_SIZE: usize = 4096;
pub(crate) const PHYS_OFFSET: u64 = 0xFFFF_8000_0000_0000;
unsafe fn paging_allocate<D: Disk, V: Iterator<Item = OsVideoMode>>(
os: &dyn Os<D, V>,
) -> Option<&'static mut [u64]> {
let ptr = os.alloc_zeroed_page_aligned(PAGE_SIZE);
if !ptr.is_null() {
area_add(OsMemoryEntry {
base: ptr as u64,
size: PAGE_SIZE as u64,
kind: OsMemoryKind::Reclaim,
});
Some(slice::from_raw_parts_mut(ptr as *mut u64, PAGE_ENTRIES))
} else {
None
}
}
const PRESENT: u64 = 1;
const WRITABLE: u64 = 1 << 1;
const LARGE: u64 = 1 << 7;
pub unsafe fn paging_create<D: Disk, V: Iterator<Item = OsVideoMode>>(
os: &dyn Os<D, V>,
kernel_phys: u64,
kernel_size: u64,
) -> Option<usize> {
// Create PML4
let pml4 = paging_allocate(os)?;
{
// Create PDP for identity mapping
let pdp = paging_allocate(os)?;
// Link first user and first kernel PML4 entry to PDP
pml4[0] = pdp.as_ptr() as u64 | WRITABLE | PRESENT;
pml4[256] = pdp.as_ptr() as u64 | WRITABLE | PRESENT;
// Identity map 8 GiB using 2 MiB pages
for pdp_i in 0..8 {
let pd = paging_allocate(os)?;
pdp[pdp_i] = pd.as_ptr() as u64 | WRITABLE | PRESENT;
for pd_i in 0..pd.len() {
let addr = pdp_i as u64 * 0x4000_0000 + pd_i as u64 * 0x20_0000;
pd[pd_i] = addr | LARGE | WRITABLE | PRESENT;
}
}
}
{
// Create PDP (spanning 512 GiB) for kernel mapping
let pdp = paging_allocate(os)?;
// Link last PML4 entry to PDP
pml4[511] = pdp.as_ptr() as u64 | WRITABLE | PRESENT;
// Create PD (spanning 1 GiB) for kernel mapping.
let pd = paging_allocate(os)?;
// The kernel is mapped at -2^31, i.e. 0xFFFF_FFFF_8000_0000. Since a PD is 1 GiB, link
// the second last PDP entry to PD.
pdp[510] = pd.as_ptr() as u64 | WRITABLE | PRESENT;
// Map kernel_size bytes to kernel offset, i.e. to the start of the PD.
let mut kernel_mapped = 0;
let mut pd_idx = 0;
while kernel_mapped < kernel_size && pd_idx < pd.len() {
let pt = paging_allocate(os)?;
pd[pd_idx] = pt.as_ptr() as u64 | WRITABLE | PRESENT;
pd_idx += 1;
let mut pt_idx = 0;
while kernel_mapped < kernel_size && pt_idx < pt.len() {
let addr = kernel_phys + kernel_mapped;
pt[pt_idx] = addr | WRITABLE | PRESENT;
pt_idx += 1;
kernel_mapped += PAGE_SIZE as u64;
}
}
assert!(kernel_mapped >= kernel_size);
}
Some(pml4.as_ptr() as usize)
}
pub unsafe fn paging_framebuffer<D: Disk, V: Iterator<Item = OsVideoMode>>(
os: &dyn Os<D, V>,
page_phys: usize,
framebuffer_phys: u64,
framebuffer_size: u64,
) -> Option<u64> {
//TODO: smarter test for framebuffer already mapped
if framebuffer_phys + framebuffer_size <= 0x2_0000_0000 {
return Some(framebuffer_phys + PHYS_OFFSET);
}
let pml4_i = ((framebuffer_phys / 0x80_0000_0000) + 256) as usize;
let mut pdp_i = ((framebuffer_phys % 0x80_0000_0000) / 0x4000_0000) as usize;
let mut pd_i = ((framebuffer_phys % 0x4000_0000) / 0x20_0000) as usize;
assert_eq!(framebuffer_phys % 0x20_0000, 0);
let pml4 = slice::from_raw_parts_mut(page_phys as *mut u64, PAGE_ENTRIES);
// Create PDP for framebuffer mapping
let pdp = if pml4[pml4_i] == 0 {
let pdp = paging_allocate(os)?;
pml4[pml4_i] = pdp.as_ptr() as u64 | 1 << 1 | 1;
pdp
} else {
slice::from_raw_parts_mut(
(pml4[pml4_i] & ENTRY_ADDRESS_MASK) as *mut u64,
PAGE_ENTRIES,
)
};
// Map framebuffer_size at framebuffer offset
let mut framebuffer_mapped = 0;
while framebuffer_mapped < framebuffer_size && pdp_i < pdp.len() {
let pd = paging_allocate(os)?;
assert_eq!(pdp[pdp_i], 0);
pdp[pdp_i] = pd.as_ptr() as u64 | 1 << 1 | 1;
while framebuffer_mapped < framebuffer_size && pd_i < pd.len() {
let addr = framebuffer_phys + framebuffer_mapped;
assert_eq!(pd[pd_i], 0);
pd[pd_i] = addr | 1 << 7 | 1 << 1 | 1;
framebuffer_mapped += 0x20_0000;
pd_i += 1;
}
pdp_i += 1;
pd_i = 0;
}
assert!(framebuffer_mapped >= framebuffer_size);
Some(framebuffer_phys + PHYS_OFFSET)
}

View File

@ -1,26 +0,0 @@
use log::{LevelFilter, Log, Metadata, Record};
pub static LOGGER: Logger = Logger;
pub struct Logger;
impl Logger {
pub fn init(&'static self) {
log::set_logger(self).unwrap();
log::set_max_level(LevelFilter::Info);
}
}
impl Log for Logger {
fn enabled(&self, _metadata: &Metadata<'_>) -> bool {
true
}
fn log(&self, record: &Record<'_>) {
if self.enabled(record.metadata()) {
println!("{} - {}", record.level(), record.args());
}
}
fn flush(&self) {}
}

View File

@ -1,642 +0,0 @@
#![no_std]
#![feature(alloc_error_handler)]
#![feature(int_roundings)]
#![feature(lang_items)]
#![allow(internal_features)]
#![feature(let_chains)]
extern crate alloc;
use alloc::{format, string::String, vec::Vec};
use core::{
cmp,
fmt::{self, Write},
mem, ptr, slice, str,
};
use redoxfs::Disk;
use self::arch::{paging_create, paging_framebuffer};
use self::os::{Os, OsHwDesc, OsKey, OsMemoryEntry, OsMemoryKind, OsVideoMode};
#[macro_use]
mod os;
mod arch;
mod logger;
mod serial_16550;
const KIBI: usize = 1024;
const MIBI: usize = KIBI * KIBI;
//TODO: allocate this in a more reasonable manner
static mut AREAS: [OsMemoryEntry; 1024] = [OsMemoryEntry {
base: 0,
size: 0,
kind: OsMemoryKind::Null,
}; 1024];
static mut AREAS_LEN: usize = 0;
pub fn area_add(area: OsMemoryEntry) {
unsafe {
for existing_area in &mut AREAS[0..AREAS_LEN] {
if existing_area.kind == area.kind {
if existing_area.base.unchecked_add(existing_area.size) == area.base {
existing_area.size += area.size;
return;
}
if area.base.unchecked_add(area.size) == existing_area.base {
existing_area.base = area.base;
return;
}
}
}
*AREAS.get_mut(AREAS_LEN).expect("AREAS overflowed!") = area;
AREAS_LEN += 1;
}
}
pub static mut KERNEL_64BIT: bool = false;
pub static mut LIVE_OPT: Option<(u64, &'static [u8])> = None;
struct SliceWriter<'a> {
slice: &'a mut [u8],
i: usize,
}
impl Write for SliceWriter<'_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
for b in s.bytes() {
if let Some(slice_b) = self.slice.get_mut(self.i) {
*slice_b = b;
self.i += 1;
} else {
return Err(fmt::Error);
}
}
Ok(())
}
}
#[allow(dead_code)]
#[derive(Debug)]
#[repr(C, packed(8))]
pub struct KernelArgs {
kernel_base: u64,
kernel_size: u64,
stack_base: u64,
stack_size: u64,
env_base: u64,
env_size: u64,
/// The base pointer to the saved RSDP.
///
/// This field can be NULL, and if so, the system has not booted with UEFI or in some other way
/// retrieved the RSDPs. The kernel or a userspace driver will thus try searching the BIOS
/// memory instead. On UEFI systems, searching is not guaranteed to actually work though.
acpi_rsdp_base: u64,
/// The size of the RSDP region.
acpi_rsdp_size: u64,
areas_base: u64,
areas_size: u64,
bootstrap_base: u64,
bootstrap_size: u64,
}
fn select_mode<D: Disk, V: Iterator<Item = OsVideoMode>>(
os: &dyn Os<D, V>,
output_i: usize,
) -> Option<OsVideoMode> {
let mut modes = Vec::new();
for mode in os.video_modes(output_i) {
let mut aspect_w = mode.width;
let mut aspect_h = mode.height;
for i in 2..cmp::min(aspect_w / 2, aspect_h / 2) {
while aspect_w % i == 0 && aspect_h % i == 0 {
aspect_w /= i;
aspect_h /= i;
}
}
modes.push((
mode,
format!(
"{:>4}x{:<4} {:>3}:{:<3}",
mode.width, mode.height, aspect_w, aspect_h
),
));
}
if modes.is_empty() {
return None;
}
// Sort modes by pixel area, reversed
modes.sort_by(|a, b| (b.0.width * b.0.height).cmp(&(a.0.width * a.0.height)));
// Set selected based on best resolution
print!("Output {}", output_i);
let mut selected = modes.get(0).map_or(0, |x| x.0.id);
if let Some((best_width, best_height)) = os.best_resolution(output_i) {
print!(", best resolution: {}x{}", best_width, best_height);
for (mode, _text) in modes.iter() {
if mode.width == best_width && mode.height == best_height {
selected = mode.id;
break;
}
}
}
println!();
println!("Arrow keys and enter select mode");
println!();
print!(" ");
let (off_x, off_y) = os.get_text_position();
let rows = 12;
let mut mode_opt = None;
while !modes.is_empty() {
let mut row = 0;
let mut col = 0;
for (mode, text) in modes.iter() {
if row >= rows {
col += 1;
row = 0;
}
os.set_text_position(off_x + col * 20, off_y + row);
os.set_text_highlight(mode.id == selected);
print!("{}", text);
row += 1;
}
// Read keypress
match os.get_key() {
OsKey::Left => {
if let Some(mut mode_i) = modes.iter().position(|x| x.0.id == selected) {
if mode_i < rows {
while mode_i < modes.len() {
mode_i += rows;
}
}
mode_i -= rows;
if let Some(new) = modes.get(mode_i) {
selected = new.0.id;
}
}
}
OsKey::Right => {
if let Some(mut mode_i) = modes.iter().position(|x| x.0.id == selected) {
mode_i += rows;
if mode_i >= modes.len() {
mode_i = mode_i % rows;
}
if let Some(new) = modes.get(mode_i) {
selected = new.0.id;
}
}
}
OsKey::Up => {
if let Some(mut mode_i) = modes.iter().position(|x| x.0.id == selected) {
if mode_i % rows == 0 {
mode_i += rows;
if mode_i > modes.len() {
mode_i = modes.len();
}
}
mode_i -= 1;
if let Some(new) = modes.get(mode_i) {
selected = new.0.id;
}
}
}
OsKey::Down => {
if let Some(mut mode_i) = modes.iter().position(|x| x.0.id == selected) {
mode_i += 1;
if mode_i % rows == 0 {
mode_i -= rows;
}
if mode_i >= modes.len() {
mode_i = mode_i - mode_i % rows;
}
if let Some(new) = modes.get(mode_i) {
selected = new.0.id;
}
}
}
OsKey::Enter => {
if let Some(mode_i) = modes.iter().position(|x| x.0.id == selected) {
if let Some((mode, _text)) = modes.get(mode_i) {
mode_opt = Some(*mode);
}
}
break;
}
_ => (),
}
}
os.set_text_position(0, off_y + rows);
os.set_text_highlight(false);
println!();
mode_opt
}
fn redoxfs<D: Disk, V: Iterator<Item = OsVideoMode>>(
os: &dyn Os<D, V>,
) -> (redoxfs::FileSystem<D>, Option<&'static [u8]>) {
let attempts = 10;
for attempt in 0..=attempts {
let mut password_opt = None;
if attempt > 0 {
print!("\rRedoxFS password ({}/{}): ", attempt, attempts);
let mut password = String::new();
loop {
match os.get_key() {
OsKey::Backspace | OsKey::Delete => {
if !password.is_empty() {
print!("\x08 \x08");
password.pop();
}
}
OsKey::Char(c) => {
print!("*");
password.push(c)
}
OsKey::Enter => break,
_ => (),
}
}
// Erase password information
while os.get_text_position().0 > 0 {
print!("\x08 \x08");
}
if !password.is_empty() {
password_opt = Some(password);
}
}
match os.filesystem(password_opt.as_ref().map(|x| x.as_bytes())) {
Ok(fs) => {
return (
fs,
password_opt.map(|password| {
// Copy password to page aligned memory
let password_size = password.len();
let password_base = os.alloc_zeroed_page_aligned(password_size);
unsafe {
ptr::copy(password.as_ptr(), password_base, password_size);
slice::from_raw_parts(password_base, password_size)
}
}),
);
}
Err(err) => match err.errno {
// Incorrect password, try again
syscall::ENOKEY => (),
_ => {
panic!("Failed to open RedoxFS: {}", err);
}
},
}
}
panic!("RedoxFS out of unlock attempts");
}
#[derive(PartialEq)]
enum Filetype {
Elf,
Initfs,
}
fn load_to_memory<D: Disk>(
os: &dyn Os<D, impl Iterator<Item = OsVideoMode>>,
fs: &mut redoxfs::FileSystem<D>,
dirname: &str,
filename: &str,
filetype: Filetype,
) -> &'static mut [u8] {
fs.tx(|tx| {
let dir_node = tx
.find_node(redoxfs::TreePtr::root(), dirname)
.unwrap_or_else(|err| panic!("Failed to find {} directory: {}", dirname, err));
let node = tx
.find_node(dir_node.ptr(), filename)
.unwrap_or_else(|err| panic!("Failed to find {} file: {}", filename, err));
let size = node.data().size();
print!("{}: 0/{} MiB", filename, size / MIBI as u64);
let ptr = os.alloc_zeroed_page_aligned(size as usize);
if ptr.is_null() {
panic!("Failed to allocate memory for {}", filename);
}
let slice = unsafe { slice::from_raw_parts_mut(ptr, size as usize) };
let mut i = 0;
for chunk in slice.chunks_mut(MIBI) {
print!(
"\r{}: {}/{} MiB",
filename,
i / MIBI as u64,
size / MIBI as u64
);
i +=
tx.read_node_inner(&node, i, chunk)
.unwrap_or_else(|err| panic!("Failed to read `{}` file: {}", filename, err)) as u64;
}
println!(
"\r{}: {}/{} MiB",
filename,
i / MIBI as u64,
size / MIBI as u64
);
if filetype == Filetype::Elf {
let magic = &slice[..4];
if magic != b"\x7FELF" {
panic!("{} has invalid magic number {:#X?}", filename, magic);
}
} else if filetype == Filetype::Initfs {
let magic = &slice[..8];
if magic != b"RedoxFtw" {
panic!("{} has invalid magic number {:#X?}", filename, magic);
}
}
Ok(slice)
})
.unwrap_or_else(|err| {
panic!(
"RedoxFS transaction failed while loading `{}`: {}",
filename, err
)
})
}
fn elf_entry(data: &[u8]) -> (u64, bool) {
match (data[4], data[5]) {
// 32-bit, little endian
(1, 1) => (
u32::from_le_bytes(
<[u8; 4]>::try_from(&data[0x18..0x18 + 4]).expect("conversion cannot fail"),
) as u64,
false,
),
// 32-bit, big endian
(1, 2) => (
u32::from_be_bytes(
<[u8; 4]>::try_from(&data[0x18..0x18 + 4]).expect("conversion cannot fail"),
) as u64,
false,
),
// 64-bit, little endian
(2, 1) => (
u64::from_le_bytes(
<[u8; 8]>::try_from(&data[0x18..0x18 + 8]).expect("conversion cannot fail"),
),
true,
),
// 64-bit, big endian
(2, 2) => (
u64::from_be_bytes(
<[u8; 8]>::try_from(&data[0x18..0x18 + 8]).expect("conversion cannot fail"),
),
true,
),
(ei_class, ei_data) => {
panic!("Unsupported ELF EI_CLASS {} EI_DATA {}", ei_class, ei_data);
}
}
}
fn main<D: Disk, V: Iterator<Item = OsVideoMode>>(os: &dyn Os<D, V>) -> (usize, u64, KernelArgs) {
println!(
"Redox OS Bootloader {} on {}",
env!("CARGO_PKG_VERSION"),
os.name()
);
let hwdesc = os.hwdesc();
println!("Hardware descriptor: {:x?}", hwdesc);
let (acpi_rsdp_base, acpi_rsdp_size) = match hwdesc {
OsHwDesc::Acpi(base, size) => (base, size),
OsHwDesc::DeviceTree(base, size) => (base, size),
OsHwDesc::NotFound => (0, 0),
};
let (mut fs, password_opt) = redoxfs(os);
print!("RedoxFS ");
for i in 0..fs.header.uuid().len() {
if i == 4 || i == 6 || i == 8 || i == 10 {
print!("-");
}
print!("{:>02x}", fs.header.uuid()[i]);
}
println!(": {} MiB", fs.header.size() / MIBI as u64);
println!();
let mut mode_opts = Vec::new();
for output_i in 0..os.video_outputs() {
if output_i > 0 {
os.clear_text();
}
mode_opts.push(select_mode(os, output_i));
}
let stack_size = 128 * KIBI;
let stack_base = os.alloc_zeroed_page_aligned(stack_size);
if stack_base.is_null() {
panic!("Failed to allocate memory for stack");
}
let live_opt = if cfg!(feature = "live") {
let size = fs.header.size();
print!("live: 0/{} MiB", size / MIBI as u64);
let ptr = os.alloc_zeroed_page_aligned(size as usize);
if ptr.is_null() {
panic!("Failed to allocate memory for live");
}
let live = unsafe { slice::from_raw_parts_mut(ptr, size as usize) };
let mut i = 0;
for chunk in live.chunks_mut(MIBI) {
print!("\rlive: {}/{} MiB", i / MIBI as u64, size / MIBI as u64);
i += unsafe {
fs.disk
.read_at(fs.block + i / redoxfs::BLOCK_SIZE, chunk)
.expect("Failed to read live disk") as u64
};
}
println!("\rlive: {}/{} MiB", i / MIBI as u64, size / MIBI as u64);
println!("Switching to live disk");
unsafe {
LIVE_OPT = Some((fs.block, slice::from_raw_parts_mut(ptr, size as usize)));
}
area_add(OsMemoryEntry {
base: live.as_ptr() as u64,
size: live.len() as u64,
kind: OsMemoryKind::Reserved,
});
Some(live)
} else {
None
};
let (kernel, kernel_entry) = {
let kernel = load_to_memory(os, &mut fs, "boot", "kernel", Filetype::Elf);
let (kernel_entry, kernel_64bit) = elf_entry(kernel);
unsafe {
KERNEL_64BIT = kernel_64bit;
}
(kernel, kernel_entry)
};
let (bootstrap_size, bootstrap_base) = {
let initfs_slice = load_to_memory(os, &mut fs, "boot", "initfs", Filetype::Initfs);
let memory = unsafe {
let total_size = initfs_slice.len().next_multiple_of(4096);
let ptr = os.alloc_zeroed_page_aligned(total_size);
assert!(!ptr.is_null(), "failed to allocate bootstrap+initfs memory");
core::slice::from_raw_parts_mut(ptr, total_size)
};
memory[..initfs_slice.len()].copy_from_slice(initfs_slice);
(memory.len() as u64, memory.as_mut_ptr() as u64)
};
let page_phys = unsafe { paging_create(os, kernel.as_ptr() as u64, kernel.len() as u64) }
.expect("Failed to set up paging");
let mut env_size = 64 * KIBI;
let env_base = os.alloc_zeroed_page_aligned(env_size);
if env_base.is_null() {
panic!("Failed to allocate memory for stack");
}
{
let mut w = SliceWriter {
slice: unsafe { slice::from_raw_parts_mut(env_base, env_size) },
i: 0,
};
writeln!(w, "BOOT_MODE={}", os.name()).unwrap();
match hwdesc {
OsHwDesc::Acpi(addr, size) => {
writeln!(w, "RSDP_ADDR={:016x}", addr).unwrap();
writeln!(w, "RSDP_SIZE={:016x}", size).unwrap();
}
OsHwDesc::DeviceTree(addr, size) => {
writeln!(w, "DTB_ADDR={:016x}", addr).unwrap();
writeln!(w, "DTB_SIZE={:016x}", size).unwrap();
}
OsHwDesc::NotFound => {}
}
if let Some(live) = live_opt {
writeln!(w, "DISK_LIVE_ADDR={:016x}", live.as_ptr() as usize).unwrap();
writeln!(w, "DISK_LIVE_SIZE={:016x}", live.len()).unwrap();
writeln!(w, "REDOXFS_BLOCK={:016x}", 0).unwrap();
} else {
writeln!(w, "REDOXFS_BLOCK={:016x}", fs.block).unwrap();
}
write!(w, "REDOXFS_UUID=").unwrap();
for i in 0..fs.header.uuid().len() {
if i == 4 || i == 6 || i == 8 || i == 10 {
write!(w, "-").unwrap();
}
write!(w, "{:>02x}", fs.header.uuid()[i]).unwrap();
}
writeln!(w).unwrap();
if let Some(password) = password_opt {
writeln!(
w,
"REDOXFS_PASSWORD_ADDR={:016x}",
password.as_ptr() as usize
)
.unwrap();
writeln!(w, "REDOXFS_PASSWORD_SIZE={:016x}", password.len()).unwrap();
}
#[cfg(target_arch = "riscv64")]
{
let boot_hartid = os::efi_get_boot_hartid()
.expect("Could not retrieve boot hart id from EFI implementation!");
writeln!(w, "BOOT_HART_ID={:016x}", boot_hartid).unwrap();
}
for output_i in 0..os.video_outputs() {
if let Some(mut mode) = mode_opts[output_i] {
// Set mode to get updated values
os.set_video_mode(output_i, &mut mode);
if output_i == 0 {
let virt = unsafe {
paging_framebuffer(
os,
page_phys,
mode.base,
(mode.stride * mode.height * 4) as u64,
)
}
.expect("Failed to map framebuffer");
writeln!(w, "FRAMEBUFFER_ADDR={:016x}", mode.base).unwrap();
writeln!(w, "FRAMEBUFFER_VIRT={:016x}", virt).unwrap();
writeln!(w, "FRAMEBUFFER_WIDTH={:016x}", mode.width).unwrap();
writeln!(w, "FRAMEBUFFER_HEIGHT={:016x}", mode.height).unwrap();
writeln!(w, "FRAMEBUFFER_STRIDE={:016x}", mode.stride).unwrap();
} else {
writeln!(
w,
"FRAMEBUFFER{}={:#x},{},{},{}",
output_i, mode.base, mode.width, mode.height, mode.stride,
)
.unwrap();
}
}
}
env_size = w.i;
}
(
page_phys,
kernel_entry,
KernelArgs {
kernel_base: kernel.as_ptr() as u64,
kernel_size: kernel.len() as u64,
stack_base: stack_base as u64,
stack_size: stack_size as u64,
env_base: env_base as u64,
env_size: env_size as u64,
acpi_rsdp_base,
acpi_rsdp_size,
areas_base: unsafe { AREAS.as_ptr() as u64 },
areas_size: unsafe { (AREAS.len() * mem::size_of::<OsMemoryEntry>()) as u64 },
bootstrap_base,
bootstrap_size,
},
)
}

View File

@ -1,175 +0,0 @@
use core::{mem, ptr};
use redoxfs::{Disk, BLOCK_SIZE};
use syscall::error::{Error, Result, EIO};
use super::{ThunkData, DISK_ADDRESS_PACKET_ADDR, DISK_BIOS_ADDR};
const SECTOR_SIZE: u64 = 512;
const BLOCKS_PER_SECTOR: u64 = BLOCK_SIZE / SECTOR_SIZE;
// 128 sectors is the amount allocated for DISK_BIOS_ADDR
// 127 sectors is the maximum for many BIOSes
const MAX_SECTORS: u64 = 127;
const MAX_BLOCKS: u64 = MAX_SECTORS * SECTOR_SIZE / BLOCK_SIZE;
#[allow(dead_code)]
#[derive(Clone, Copy)]
#[repr(C, packed)]
pub struct DiskAddressPacket {
size: u8,
reserved: u8,
sectors: u16,
buffer: u16,
segment: u16,
address: u64,
}
impl DiskAddressPacket {
pub fn from_block(block: u64, count: u64) -> DiskAddressPacket {
let address = block * BLOCKS_PER_SECTOR;
let sectors = count * BLOCKS_PER_SECTOR;
assert!(sectors <= MAX_SECTORS);
DiskAddressPacket {
size: mem::size_of::<DiskAddressPacket>() as u8,
reserved: 0,
sectors: sectors as u16,
buffer: (DISK_BIOS_ADDR & 0xF) as u16,
segment: (DISK_BIOS_ADDR >> 4) as u16,
address,
}
}
}
pub struct DiskBios {
boot_disk: u8,
thunk13: extern "C" fn(),
chs_opt: Option<(u32, u32, u32)>,
}
impl DiskBios {
pub fn new(boot_disk: u8, thunk13: extern "C" fn()) -> Self {
let chs_opt = unsafe {
let mut data = ThunkData::new();
data.eax = 0x4100;
data.ebx = 0x55AA;
data.edx = boot_disk as u32;
data.with(thunk13);
if (data.ebx & 0xFFFF) == 0xAA55 {
// Extensions are installed, do not use CHS
None
} else {
// Extensions are not installed, get CHS geometry
data = ThunkData::new();
data.eax = 0x0800;
data.edx = boot_disk as u32;
data.edi = 0;
data.with(thunk13);
//TODO: return result on error
let ah = ({ data.eax } >> 8) & 0xFF;
assert_eq!(ah, 0);
let c = (data.ecx >> 8) & 0xFF | ((data.ecx >> 6) & 0x3) << 8;
let h = ((data.edx >> 8) & 0xFF) + 1;
let s = data.ecx & 0x3F;
Some((c, h, s))
}
};
Self {
boot_disk,
thunk13,
chs_opt,
}
}
}
impl Disk for DiskBios {
unsafe fn read_at(&mut self, block: u64, buffer: &mut [u8]) -> Result<usize> {
// Optimization for live disks
if let Some(live) = crate::LIVE_OPT {
if block >= live.0 {
let start = ((block - live.0) * BLOCK_SIZE) as usize;
let end = start + buffer.len();
if end <= live.1.len() {
buffer.copy_from_slice(&live.1[start..end]);
return Ok(buffer.len());
}
}
}
for (i, chunk) in buffer
.chunks_mut((MAX_BLOCKS * BLOCK_SIZE) as usize)
.enumerate()
{
let dap = DiskAddressPacket::from_block(
block + i as u64 * MAX_BLOCKS,
chunk.len() as u64 / BLOCK_SIZE,
);
if let Some((_, h_max, s_max)) = self.chs_opt {
let s = (dap.address % s_max as u64) + 1;
assert!(s <= 63, "invalid sector {}", s);
let tmp = dap.address / s_max as u64;
let h = tmp % h_max as u64;
assert!(h <= 255, "invalid head {}", h);
let c = tmp / h_max as u64;
assert!(c <= 1023, "invalid cylinder {}", c);
let mut data = ThunkData::new();
data.eax = 0x0200 | (dap.sectors as u32);
data.ebx = dap.buffer as u32;
data.ecx =
(s as u32) | (((c as u32) & 0xFF) << 8) | ((((c as u32) >> 8) & 0x3) << 6);
data.edx = (self.boot_disk as u32) | ((h as u32) << 8);
data.es = dap.segment;
data.with(self.thunk13);
//TODO: return result on error
let ah = ({ data.eax } >> 8) & 0xFF;
assert_eq!(ah, 0);
} else {
ptr::write(DISK_ADDRESS_PACKET_ADDR as *mut DiskAddressPacket, dap);
let mut data = ThunkData::new();
data.eax = 0x4200;
data.edx = self.boot_disk as u32;
data.esi = DISK_ADDRESS_PACKET_ADDR as u32;
data.with(self.thunk13);
//TODO: return result on error
let ah = ({ data.eax } >> 8) & 0xFF;
assert_eq!(ah, 0);
//TODO: check blocks transferred
// dap = ptr::read(DISK_ADDRESS_PACKET_ADDR as *mut DiskAddressPacket);
}
ptr::copy(DISK_BIOS_ADDR as *const u8, chunk.as_mut_ptr(), chunk.len());
}
Ok(buffer.len())
}
unsafe fn write_at(&mut self, block: u64, buffer: &[u8]) -> Result<usize> {
log::error!(
"DiskBios::write_at(0x{:X}, 0x{:X}:0x{:X}) not allowed",
block,
buffer.as_ptr() as usize,
buffer.len()
);
Err(Error::new(EIO))
}
fn size(&mut self) -> Result<u64> {
log::error!("DiskBios::size not implemented");
Err(Error::new(EIO))
}
}

View File

@ -1,20 +0,0 @@
/// Print to console
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ({
use core::fmt::Write;
#[cfg(feature = "serial_debug")]
{
let _ = write!($crate::os::serial::COM1.lock(), $($arg)*);
}
let _ = write!($crate::os::VGA.lock(), $($arg)*);
});
}
/// Print with new line to console
#[macro_export]
macro_rules! println {
() => (print!("\n"));
($fmt:expr) => (print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => (print!(concat!($fmt, "\n"), $($arg)*));
}

View File

@ -1,84 +0,0 @@
use core::{cmp, mem, ptr};
use crate::area_add;
use crate::os::{OsMemoryEntry, OsMemoryKind};
use super::{thunk::ThunkData, MEMORY_MAP_ADDR};
#[repr(C, packed)]
struct MemoryMapEntry {
pub base: u64,
pub size: u64,
pub kind: u32,
}
pub struct MemoryMapIter {
thunk15: extern "C" fn(),
data: ThunkData,
first: bool,
}
impl MemoryMapIter {
pub fn new(thunk15: extern "C" fn()) -> Self {
Self {
thunk15,
data: ThunkData::new(),
first: true,
}
}
}
impl Iterator for MemoryMapIter {
type Item = OsMemoryEntry;
fn next(&mut self) -> Option<Self::Item> {
if self.first {
self.first = false;
} else if self.data.ebx == 0 {
return None;
}
self.data.eax = 0xE820;
self.data.ecx = mem::size_of::<MemoryMapEntry>() as u32;
self.data.edx = 0x534D4150;
self.data.edi = MEMORY_MAP_ADDR as u32;
unsafe {
self.data.with(self.thunk15);
}
//TODO: return error?
assert_eq!({ self.data.eax }, 0x534D4150);
assert_eq!({ self.data.ecx }, mem::size_of::<MemoryMapEntry>() as u32);
let entry = unsafe { ptr::read(MEMORY_MAP_ADDR as *const MemoryMapEntry) };
Some(Self::Item {
base: entry.base,
size: entry.size,
kind: match entry.kind {
0 => OsMemoryKind::Null,
1 => OsMemoryKind::Free,
3 => OsMemoryKind::Reclaim,
_ => OsMemoryKind::Reserved,
},
})
}
}
pub unsafe fn memory_map(thunk15: extern "C" fn()) -> Option<(usize, usize)> {
let mut heap_limits = None;
for entry in MemoryMapIter::new(thunk15) {
let heap_start = 1 * 1024 * 1024;
if { entry.kind } == OsMemoryKind::Free
&& entry.base <= heap_start as u64
&& (entry.base + entry.size) >= heap_start as u64
{
let heap_end = cmp::min(entry.base + entry.size, usize::MAX as u64) as usize;
if heap_end >= heap_start {
heap_limits = Some((heap_start, heap_end - heap_start));
}
}
area_add(entry);
}
heap_limits
}

View File

@ -1,313 +0,0 @@
use alloc::alloc::{alloc_zeroed, Layout};
use core::{convert::TryFrom, mem, ptr, slice};
use linked_list_allocator::LockedHeap;
use spin::Mutex;
use crate::logger::LOGGER;
use crate::os::{Os, OsHwDesc, OsKey, OsVideoMode};
use crate::KernelArgs;
use self::disk::DiskBios;
use self::memory_map::memory_map;
use self::thunk::ThunkData;
use self::vbe::VideoModeIter;
use self::vga::{Vga, VgaTextColor};
#[macro_use]
mod macros;
mod disk;
mod memory_map;
mod panic;
pub(crate) mod serial;
mod thunk;
mod vbe;
mod vga;
// Real mode memory allocation, for use with thunk
// 0x500 to 0x7BFF is free
const DISK_BIOS_ADDR: usize = 0x70000; // 64 KiB at 448 KiB, ends at 512 KiB
const VBE_CARD_INFO_ADDR: usize = 0x1000; // 512 bytes, ends at 0x11FF
const VBE_MODE_INFO_ADDR: usize = 0x1200; // 256 bytes, ends at 0x12FF
const VBE_EDID_ADDR: usize = 0x1300; // 128 bytes, ends at 0x137F
const MEMORY_MAP_ADDR: usize = 0x1380; // 24 bytes, ends at 0x1397
const DISK_ADDRESS_PACKET_ADDR: usize = 0x1398; // 16 bytes, ends at 0x13A7
const THUNK_STACK_ADDR: usize = 0x7C00; // Grows downwards
const VGA_ADDR: usize = 0xB8000;
#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();
pub(crate) static VGA: Mutex<Vga> = Mutex::new(unsafe { Vga::new(VGA_ADDR, 80, 25) });
pub struct OsBios {
boot_disk: usize,
thunk10: extern "C" fn(),
thunk13: extern "C" fn(),
thunk15: extern "C" fn(),
thunk16: extern "C" fn(),
}
#[allow(dead_code)]
#[derive(Copy, Clone, Debug)]
#[repr(C, packed)]
pub struct Rsdp {
signature: [u8; 8],
checksum: u8,
oemid: [u8; 6],
revision: u8,
rsdt_address: u32,
}
#[allow(dead_code)]
#[derive(Copy, Clone, Debug)]
#[repr(C, packed)]
pub struct Xsdp {
rsdp: Rsdp,
length: u32,
xsdt_address: u64,
extended_checksum: u8,
reserved: [u8; 3],
}
unsafe fn search_rsdp(start: usize, end: usize) -> Option<(u64, u64)> {
// Align start up to 16 bytes
let mut addr = ((start + 15) / 16) * 16;
// Search until reading the end of the Rsdp would be past the end of the memory area
while addr + mem::size_of::<Rsdp>() <= end {
let rsdp = ptr::read(addr as *const Rsdp);
if &rsdp.signature == b"RSD PTR " {
//TODO: check checksum?
if rsdp.revision == 0 {
return Some((addr as u64, mem::size_of::<Rsdp>() as u64));
} else if rsdp.revision == 2 {
let xsdp = ptr::read(addr as *const Xsdp);
//TODO: check extended checksum?
return Some((addr as u64, xsdp.length as u64));
}
}
// Rsdp is always aligned to 16 bytes
addr += 16;
}
None
}
impl Os<DiskBios, VideoModeIter> for OsBios {
fn name(&self) -> &str {
"x86/BIOS"
}
fn alloc_zeroed_page_aligned(&self, size: usize) -> *mut u8 {
assert!(size != 0);
let page_size = self.page_size();
let pages = (size + page_size - 1) / page_size;
let ptr =
unsafe { alloc_zeroed(Layout::from_size_align(pages * page_size, page_size).unwrap()) };
assert!(!ptr.is_null());
ptr
}
fn page_size(&self) -> usize {
4096
}
fn filesystem(
&self,
password_opt: Option<&[u8]>,
) -> syscall::Result<redoxfs::FileSystem<DiskBios>> {
let disk = DiskBios::new(u8::try_from(self.boot_disk).unwrap(), self.thunk13);
//TODO: get block from partition table
//let block = 2 * crate::MIBI as u64 / redoxfs::BLOCK_SIZE;
let block = 2048;
redoxfs::FileSystem::open(disk, password_opt, Some(block), false)
}
fn hwdesc(&self) -> OsHwDesc {
// See ACPI specification - Finding the RSDP on IA-PC Systems
unsafe {
let ebda_segment = ptr::read(0x40E as *const u16);
let ebda_addr = (ebda_segment as usize) << 4;
if let Some((addr, size)) =
search_rsdp(ebda_addr, ebda_addr + 1024).or(search_rsdp(0xE0000, 0xFFFFF))
{
// Copy to a page
let page_aligned = self.alloc_zeroed_page_aligned(size as usize);
ptr::copy(addr as *const u8, page_aligned, size as usize);
return OsHwDesc::Acpi(page_aligned as u64, size);
}
}
OsHwDesc::NotFound
}
fn video_outputs(&self) -> usize {
//TODO: return 1 only if vbe supported?
1
}
fn video_modes(&self, _output_i: usize) -> VideoModeIter {
VideoModeIter::new(self.thunk10)
}
fn set_video_mode(&self, _output_i: usize, mode: &mut OsVideoMode) {
// Set video mode
let mut data = ThunkData::new();
data.eax = 0x4F02;
data.ebx = mode.id;
unsafe {
data.with(self.thunk10);
}
//TODO: check result
}
fn best_resolution(&self, _output_i: usize) -> Option<(u32, u32)> {
let mut data = ThunkData::new();
data.eax = 0x4F15;
data.ebx = 0x01;
data.ecx = 0;
data.edx = 0;
data.edi = VBE_EDID_ADDR as u32;
unsafe {
data.with(self.thunk10);
}
if data.eax == 0x4F {
let edid = unsafe { slice::from_raw_parts(VBE_EDID_ADDR as *const u8, 128) };
Some((
(edid[0x38] as u32) | (((edid[0x3A] as u32) & 0xF0) << 4),
(edid[0x3B] as u32) | (((edid[0x3D] as u32) & 0xF0) << 4),
))
} else {
log::warn!("Failed to get VBE EDID: 0x{:X}", { data.eax });
None
}
}
fn get_key(&self) -> OsKey {
// Read keypress
let mut data = ThunkData::new();
unsafe {
data.with(self.thunk16);
}
match (data.eax >> 8) as u8 {
0x4B => OsKey::Left,
0x4D => OsKey::Right,
0x48 => OsKey::Up,
0x50 => OsKey::Down,
0x0E => OsKey::Backspace,
0x53 => OsKey::Delete,
0x1C => OsKey::Enter,
_ => match data.eax as u8 {
0 => OsKey::Other,
b => OsKey::Char(b as char),
},
}
}
fn clear_text(&self) {
//TODO: clear screen for VGA
}
fn get_text_position(&self) -> (usize, usize) {
let vga = VGA.lock();
(vga.x, vga.y)
}
fn set_text_position(&self, x: usize, y: usize) {
//TODO: ensure this is inside bounds!
let mut vga = VGA.lock();
vga.x = x;
vga.y = y;
}
fn set_text_highlight(&self, highlight: bool) {
let mut vga = VGA.lock();
if highlight {
vga.bg = VgaTextColor::Gray;
vga.fg = VgaTextColor::Black;
} else {
vga.bg = VgaTextColor::Black;
vga.fg = VgaTextColor::Gray;
}
}
}
#[no_mangle]
pub unsafe extern "C" fn start(
kernel_entry: extern "C" fn(
page_table: usize,
stack: u64,
func: u64,
args: *const KernelArgs,
long_mode: usize,
) -> !,
boot_disk: usize,
thunk10: extern "C" fn(),
thunk13: extern "C" fn(),
thunk15: extern "C" fn(),
thunk16: extern "C" fn(),
) -> ! {
#[cfg(feature = "serial_debug")]
{
let mut com1 = serial::COM1.lock();
com1.init();
com1.write(b"SERIAL\n");
}
{
// Make sure we are in mode 3 (80x25 text mode)
let mut data = ThunkData::new();
data.eax = 0x03;
data.with(thunk10);
}
{
// Disable cursor
let mut data = ThunkData::new();
data.eax = 0x0100;
data.ecx = 0x3F00;
data.with(thunk10);
}
// Clear screen
VGA.lock().clear();
// Set logger
LOGGER.init();
let mut os = OsBios {
boot_disk,
thunk10,
thunk13,
thunk15,
thunk16,
};
let (heap_start, heap_size) = memory_map(os.thunk15).expect("No memory for heap");
ALLOCATOR.lock().init(heap_start as *mut u8, heap_size);
let (page_phys, func, args) = crate::main(&mut os);
panic!("kernel");
kernel_entry(
page_phys,
args.stack_base
+ args.stack_size
+ if crate::KERNEL_64BIT {
crate::arch::x64::PHYS_OFFSET as u64
} else {
crate::arch::x32::PHYS_OFFSET as u64
},
func,
&args,
if crate::KERNEL_64BIT { 1 } else { 0 },
);
}

View File

@ -1,39 +0,0 @@
//! Intrinsics for panic handling
use core::alloc::Layout;
use core::arch::asm;
use core::panic::PanicInfo;
#[lang = "eh_personality"]
#[no_mangle]
pub extern "C" fn rust_eh_personality() {}
/// Required to handle panics
#[panic_handler]
#[no_mangle]
pub fn rust_begin_unwind(info: &PanicInfo<'_>) -> ! {
unsafe {
println!("BOOTLOADER PANIC:\n{}", info);
loop {
asm!("hlt");
}
}
}
#[alloc_error_handler]
#[no_mangle]
#[allow(improper_ctypes_definitions)] // Layout is not repr(C)
pub extern "C" fn rust_oom(_layout: Layout) -> ! {
panic!("memory allocation failed");
}
#[allow(non_snake_case)]
#[no_mangle]
/// Required to handle panics
pub extern "C" fn _Unwind_Resume() -> ! {
loop {
unsafe {
asm!("hlt");
}
}
}

View File

@ -1,9 +0,0 @@
use spin::Mutex;
use syscall::Pio;
use crate::serial_16550::SerialPort;
pub static COM1: Mutex<SerialPort<Pio<u8>>> = Mutex::new(SerialPort::<Pio<u8>>::new(0x3F8));
pub static COM2: Mutex<SerialPort<Pio<u8>>> = Mutex::new(SerialPort::<Pio<u8>>::new(0x2F8));
pub static COM3: Mutex<SerialPort<Pio<u8>>> = Mutex::new(SerialPort::<Pio<u8>>::new(0x3E8));
pub static COM4: Mutex<SerialPort<Pio<u8>>> = Mutex::new(SerialPort::<Pio<u8>>::new(0x2E8));

View File

@ -1,46 +0,0 @@
use core::ptr;
use super::THUNK_STACK_ADDR;
#[allow(dead_code)]
#[derive(Clone, Copy, Debug)]
#[repr(C, packed)]
pub struct ThunkData {
pub es: u16,
pub edi: u32,
pub esi: u32,
pub ebp: u32,
pub ebx: u32,
pub edx: u32,
pub ecx: u32,
pub eax: u32,
}
impl ThunkData {
pub fn new() -> Self {
Self {
es: 0,
edi: 0,
esi: 0,
ebp: 0,
ebx: 0,
edx: 0,
ecx: 0,
eax: 0,
}
}
pub unsafe fn save(&self) {
ptr::write((THUNK_STACK_ADDR - 64) as *mut ThunkData, *self);
}
pub unsafe fn load(&mut self) {
*self = ptr::read((THUNK_STACK_ADDR - 64) as *const ThunkData);
}
pub unsafe fn with(&mut self, f: extern "C" fn()) {
self.save();
f();
self.load();
}
}

View File

@ -1,151 +0,0 @@
use core::ptr;
use log::error;
use crate::os::OsVideoMode;
use super::{ThunkData, VBE_CARD_INFO_ADDR, VBE_MODE_INFO_ADDR};
#[derive(Clone, Copy, Debug)]
#[repr(C, packed)]
pub struct VbeFarPtr {
pub offset: u16,
pub segment: u16,
}
impl VbeFarPtr {
pub unsafe fn as_ptr<T>(&self) -> *const T {
(((self.segment as usize) << 4) + (self.offset as usize)) as *const T
}
}
#[derive(Clone, Copy, Debug)]
#[repr(C, packed)]
pub struct VbeCardInfo {
pub signature: [u8; 4],
pub version: u16,
pub oemstring: VbeFarPtr,
pub capabilities: [u8; 4],
pub videomodeptr: VbeFarPtr,
pub totalmemory: u16,
pub oemsoftwarerev: u16,
pub oemvendornameptr: VbeFarPtr,
pub oemproductnameptr: VbeFarPtr,
pub oemproductrevptr: VbeFarPtr,
pub reserved: [u8; 222],
pub oemdata: [u8; 256],
}
#[derive(Clone, Copy, Debug)]
#[repr(C, packed)]
pub struct VbeModeInfo {
pub attributes: u16,
pub win_a: u8,
pub win_b: u8,
pub granularity: u16,
pub winsize: u16,
pub segment_a: u16,
pub segment_b: u16,
pub winfuncptr: u32,
pub bytesperscanline: u16,
pub xresolution: u16,
pub yresolution: u16,
pub xcharsize: u8,
pub ycharsize: u8,
pub numberofplanes: u8,
pub bitsperpixel: u8,
pub numberofbanks: u8,
pub memorymodel: u8,
pub banksize: u8,
pub numberofimagepages: u8,
pub unused: u8,
pub redmasksize: u8,
pub redfieldposition: u8,
pub greenmasksize: u8,
pub greenfieldposition: u8,
pub bluemasksize: u8,
pub bluefieldposition: u8,
pub rsvdmasksize: u8,
pub rsvdfieldposition: u8,
pub directcolormodeinfo: u8,
pub physbaseptr: u32,
pub offscreenmemoryoffset: u32,
pub offscreenmemsize: u16,
pub reserved: [u8; 206],
}
pub struct VideoModeIter {
thunk10: extern "C" fn(),
mode_ptr: *const u16,
}
impl VideoModeIter {
pub fn new(thunk10: extern "C" fn()) -> Self {
// Get card info
let mut data = ThunkData::new();
data.eax = 0x4F00;
data.edi = VBE_CARD_INFO_ADDR as u32;
unsafe {
data.with(thunk10);
}
let mode_ptr = if data.eax == 0x004F {
let card_info = unsafe { ptr::read(VBE_CARD_INFO_ADDR as *const VbeCardInfo) };
unsafe { card_info.videomodeptr.as_ptr::<u16>() }
} else {
error!("Failed to read VBE card info: 0x{:04X}", { data.eax });
ptr::null()
};
Self { thunk10, mode_ptr }
}
}
impl Iterator for VideoModeIter {
type Item = OsVideoMode;
fn next(&mut self) -> Option<Self::Item> {
if self.mode_ptr.is_null() {
return None;
}
loop {
// Set bit 14 to get linear frame buffer
let mode = unsafe { *self.mode_ptr } | (1 << 14);
if mode == 0xFFFF {
return None;
}
self.mode_ptr = unsafe { self.mode_ptr.add(1) };
// Get mode info
let mut data = ThunkData::new();
data.eax = 0x4F01;
data.ecx = mode as u32;
data.edi = VBE_MODE_INFO_ADDR as u32;
unsafe {
data.with(self.thunk10);
}
if data.eax == 0x004F {
let mode_info = unsafe { ptr::read(VBE_MODE_INFO_ADDR as *const VbeModeInfo) };
// We only support 32-bits per pixel modes
if mode_info.bitsperpixel != 32 {
continue;
}
let width = mode_info.xresolution as u32;
let height = mode_info.yresolution as u32;
//TODO: support stride that is not a multiple of 4
let stride = mode_info.bytesperscanline as u32 / 4;
return Some(OsVideoMode {
id: mode as u32,
width,
height,
stride,
base: mode_info.physbaseptr as u64,
});
} else {
error!("Failed to read VBE mode 0x{:04X} info: 0x{:04X}", mode, {
data.eax
});
}
}
}
}

View File

@ -1,119 +0,0 @@
use core::{fmt, slice};
#[derive(Clone, Copy)]
#[repr(C, packed)]
pub struct VgaTextBlock {
pub char: u8,
pub color: u8,
}
#[allow(dead_code)]
#[derive(Clone, Copy)]
#[repr(u8)]
pub enum VgaTextColor {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Purple = 5,
Brown = 6,
Gray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
LightPurple = 13,
Yellow = 14,
White = 15,
}
pub struct Vga {
pub base: usize,
pub width: usize,
pub height: usize,
pub x: usize,
pub y: usize,
pub bg: VgaTextColor,
pub fg: VgaTextColor,
}
impl Vga {
pub const unsafe fn new(base: usize, width: usize, height: usize) -> Self {
Self {
base,
width,
height,
x: 0,
y: 0,
bg: VgaTextColor::Black,
fg: VgaTextColor::Gray,
}
}
pub unsafe fn blocks(&mut self) -> &'static mut [VgaTextBlock] {
slice::from_raw_parts_mut(self.base as *mut VgaTextBlock, self.width * self.height)
}
pub fn clear(&mut self) {
self.x = 0;
self.y = 0;
let blocks = unsafe { self.blocks() };
for i in 0..blocks.len() {
blocks[i] = VgaTextBlock {
char: 0,
color: ((self.bg as u8) << 4) | (self.fg as u8),
};
}
}
}
impl fmt::Write for Vga {
fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
let blocks = unsafe { self.blocks() };
for c in s.chars() {
if self.x >= self.width {
self.x = 0;
self.y += 1;
}
while self.y >= self.height {
for y in 1..self.height {
for x in 0..self.width {
let i = y * self.width + x;
let j = i - self.width;
blocks[j] = blocks[i];
if y + 1 == self.height {
blocks[i].char = 0;
}
}
}
self.y -= 1;
}
match c {
'\x08' => {
if self.x > 0 {
self.x -= 1;
}
}
'\r' => {
self.x = 0;
}
'\n' => {
self.x = 0;
self.y += 1;
}
_ => {
let i = self.y * self.width + self.x;
if let Some(block) = blocks.get_mut(i) {
block.char = c as u8;
block.color = ((self.bg as u8) << 4) | (self.fg as u8);
}
self.x += 1;
}
}
}
Ok(())
}
}

View File

@ -1,81 +0,0 @@
use redoxfs::Disk;
#[cfg(all(target_arch = "x86", target_os = "none"))]
pub use self::bios::*;
#[cfg(all(target_arch = "x86", target_os = "none"))]
#[macro_use]
mod bios;
#[derive(Clone, Copy, Debug)]
pub enum OsHwDesc {
Acpi(u64, u64),
DeviceTree(u64, u64),
NotFound,
}
#[derive(Clone, Copy, Debug)]
pub enum OsKey {
Left,
Right,
Up,
Down,
Backspace,
Delete,
Enter,
Char(char),
Other,
}
// Keep synced with BootloaderMemoryKind in kernel
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(u64)]
pub enum OsMemoryKind {
Null = 0,
Free = 1,
Reclaim = 2,
Reserved = 3,
}
// Keep synced with BootloaderMemoryEntry in kernel
#[derive(Clone, Copy, Debug)]
#[repr(C, packed(8))]
pub struct OsMemoryEntry {
pub base: u64,
pub size: u64,
pub kind: OsMemoryKind,
}
#[derive(Clone, Copy, Debug)]
pub struct OsVideoMode {
pub id: u32,
pub width: u32,
pub height: u32,
pub stride: u32,
pub base: u64,
}
pub trait Os<D: Disk, V: Iterator<Item = OsVideoMode>> {
fn name(&self) -> &str;
fn alloc_zeroed_page_aligned(&self, size: usize) -> *mut u8;
#[allow(dead_code)]
fn page_size(&self) -> usize;
fn filesystem(&self, password_opt: Option<&[u8]>) -> syscall::Result<redoxfs::FileSystem<D>>;
fn hwdesc(&self) -> OsHwDesc;
fn video_outputs(&self) -> usize;
fn video_modes(&self, output_i: usize) -> V;
fn set_video_mode(&self, output_i: usize, mode: &mut OsVideoMode);
fn best_resolution(&self, output_i: usize) -> Option<(u32, u32)>;
fn get_key(&self) -> OsKey;
fn clear_text(&self);
fn get_text_position(&self) -> (usize, usize);
fn set_text_position(&self, x: usize, y: usize);
fn set_text_highlight(&self, highlight: bool);
}

View File

@ -1,142 +0,0 @@
use bitflags::bitflags;
use core::convert::TryInto;
use core::fmt;
use core::ptr::{addr_of, addr_of_mut};
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use syscall::io::Pio;
use syscall::io::{Io, Mmio, ReadOnly};
bitflags! {
/// Interrupt enable flags
struct IntEnFlags: u8 {
const RECEIVED = 1;
const SENT = 1 << 1;
const ERRORED = 1 << 2;
const STATUS_CHANGE = 1 << 3;
// 4 to 7 are unused
}
}
bitflags! {
/// Line status flags
struct LineStsFlags: u8 {
const INPUT_FULL = 1;
// 1 to 4 unknown
const OUTPUT_EMPTY = 1 << 5;
// 6 and 7 unknown
}
}
#[allow(dead_code)]
#[repr(C, packed)]
pub struct SerialPort<T: Io> {
/// Data register, read to receive, write to send
data: T,
/// Interrupt enable
int_en: T,
/// FIFO control
fifo_ctrl: T,
/// Line control
line_ctrl: T,
/// Modem control
modem_ctrl: T,
/// Line status
line_sts: ReadOnly<T>,
/// Modem status
modem_sts: ReadOnly<T>,
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
impl SerialPort<Pio<u8>> {
pub const fn new(base: u16) -> SerialPort<Pio<u8>> {
SerialPort {
data: Pio::new(base),
int_en: Pio::new(base + 1),
fifo_ctrl: Pio::new(base + 2),
line_ctrl: Pio::new(base + 3),
modem_ctrl: Pio::new(base + 4),
line_sts: ReadOnly::new(Pio::new(base + 5)),
modem_sts: ReadOnly::new(Pio::new(base + 6)),
}
}
}
impl SerialPort<Mmio<u32>> {
pub unsafe fn new(base: usize) -> &'static mut SerialPort<Mmio<u32>> {
&mut *(base as *mut Self)
}
}
impl<T: Io> SerialPort<T>
where
T::Value: From<u8> + TryInto<u8>,
{
pub fn init(&mut self) {
unsafe {
//TODO: Cleanup
// FIXME: Fix UB if unaligned
(&mut *addr_of_mut!(self.int_en)).write(0x00.into());
(&mut *addr_of_mut!(self.line_ctrl)).write(0x80.into());
(&mut *addr_of_mut!(self.data)).write(0x01.into());
(&mut *addr_of_mut!(self.int_en)).write(0x00.into());
(&mut *addr_of_mut!(self.line_ctrl)).write(0x03.into());
(&mut *addr_of_mut!(self.fifo_ctrl)).write(0xC7.into());
(&mut *addr_of_mut!(self.modem_ctrl)).write(0x0B.into());
(&mut *addr_of_mut!(self.int_en)).write(0x01.into());
}
}
fn line_sts(&self) -> LineStsFlags {
LineStsFlags::from_bits_truncate(
(unsafe { &*addr_of!(self.line_sts) }.read() & 0xFF.into())
.try_into()
.unwrap_or(0),
)
}
pub fn receive(&mut self) -> Option<u8> {
if self.line_sts().contains(LineStsFlags::INPUT_FULL) {
Some(
(unsafe { &*addr_of!(self.data) }.read() & 0xFF.into())
.try_into()
.unwrap_or(0),
)
} else {
None
}
}
pub fn send(&mut self, data: u8) {
while !self.line_sts().contains(LineStsFlags::OUTPUT_EMPTY) {}
unsafe { &mut *addr_of_mut!(self.data) }.write(data.into())
}
pub fn write(&mut self, buf: &[u8]) {
for &b in buf {
match b {
8 | 0x7F => {
self.send(8);
self.send(b' ');
self.send(8);
}
b'\n' => {
self.send(b'\r');
self.send(b'\n');
}
_ => {
self.send(b);
}
}
}
}
}
impl<T: Io> fmt::Write for SerialPort<T>
where
T::Value: From<u8> + TryInto<u8>,
{
fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
self.write(s.as_bytes());
Ok(())
}
}

View File

@ -1,28 +0,0 @@
{
"llvm-target": "i686-unknown-none",
"target-endian": "little",
"target-pointer-width": "32",
"target-c-int-width": "32",
"data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128",
"arch": "x86",
"os": "none",
"env": "",
"vendor": "unknown",
"linker-flavor": "gcc",
"panic-strategy": "abort",
"pre-link-args": {
"gcc": ["-m32", "-nostdlib", "-static"]
},
"features": "-mmx,-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-3dnow,-3dnowa,-avx,-avx2,+soft-float",
"dynamic-linking": false,
"executables": false,
"relocation-model": "static",
"code-model": "large",
"disable-redzone": true,
"frame-pointer": "always",
"exe-suffix": "",
"has-rpath": false,
"no-default-libraries": true,
"position-independent-executables": false,
"tls-model": "global-dynamic"
}

180
tetros/Cargo.lock generated Normal file
View File

@ -0,0 +1,180 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "proc-macro2"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_core",
"zerocopy",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
[[package]]
name = "raw-cpuid"
version = "10.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "rustversion"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "syn"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tetros"
version = "1.0.0"
dependencies = [
"bitflags 2.8.0",
"lazy_static",
"rand",
"spin",
"uart_16550",
]
[[package]]
name = "uart_16550"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e492212ac378a5e00da953718dafb1340d9fbaf4f27d6f3c5cab03d931d1c049"
dependencies = [
"bitflags 2.8.0",
"rustversion",
"x86",
]
[[package]]
name = "unicode-ident"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]]
name = "x86"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2781db97787217ad2a2845c396a5efe286f87467a5810836db6d74926e94a385"
dependencies = [
"bit_field",
"bitflags 1.3.2",
"raw-cpuid",
]
[[package]]
name = "zerocopy"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@ -1,18 +1,17 @@
#
# MARK: meta
#
[package] [package]
name = "redox_bootloader" name = "tetros"
version = "1.0.0" version = "1.0.0"
edition = "2021" edition = "2021"
publish = false publish = false
[lib] [lib]
name = "bootloader" name = "tetros"
path = "src/main.rs" path = "src/lib.rs"
crate-type = ["staticlib"] crate-type = ["staticlib"]
# Silence rust-analyzer errors
test = false
bench = false
# #
# MARK: lints # MARK: lints
@ -29,19 +28,13 @@ elided_lifetimes_in_paths = "deny"
absolute_paths_not_starting_with_crate = "deny" absolute_paths_not_starting_with_crate = "deny"
explicit_outlives_requirements = "warn" explicit_outlives_requirements = "warn"
unused_crate_dependencies = "warn" unused_crate_dependencies = "warn"
#variant_size_differences = "warn"
redundant_lifetimes = "warn" redundant_lifetimes = "warn"
missing_docs = "allow" missing_docs = "warn"
[lints.clippy] [lints.clippy]
needless_return = "allow" needless_return = "allow"
new_without_default = "allow" new_without_default = "allow"
tabs_in_doc_comments = "allow" tabs_in_doc_comments = "allow"
# Extra
expect_used = "deny"
#unwrap_used = "deny"
#panic = "deny"
dbg_macro = "deny" dbg_macro = "deny"
allow_attributes = "deny" allow_attributes = "deny"
create_dir = "deny" create_dir = "deny"
@ -59,18 +52,15 @@ string_to_string = "deny"
unimplemented = "deny" unimplemented = "deny"
use_debug = "deny" use_debug = "deny"
verbose_file_reads = "deny" verbose_file_reads = "deny"
#wildcard_enum_match_arm = "deny"
# Pedantic
large_types_passed_by_value = "deny" large_types_passed_by_value = "deny"
match_on_vec_items = "deny" match_on_vec_items = "deny"
# Cargo
wildcard_dependencies = "deny" wildcard_dependencies = "deny"
negative_feature_names = "deny" negative_feature_names = "deny"
redundant_feature_names = "deny" redundant_feature_names = "deny"
multiple_crate_versions = "deny" multiple_crate_versions = "allow"
missing_safety_doc = "allow"
identity_op = "allow"
comparison_chain = "allow"
# #
# MARK: dependencies # MARK: dependencies
@ -78,21 +68,11 @@ multiple_crate_versions = "deny"
[dependencies] [dependencies]
bitflags = "1.3.2" bitflags = "2.8.0"
linked_list_allocator = "0.10.5" rand = { version = "0.9.0", features = ["small_rng"], default-features = false }
log = "0.4.17" spin = "0.9.8"
redox_syscall = "0.5" uart_16550 = "0.3.2"
spin = "0.9.5"
[dependencies.redoxfs] [dependencies.lazy_static]
version = "0.6.0" version = "1.0"
default-features = false features = ["spin_no_std"]
features = ["force-soft", "log"]
#
# MARK: features
#
[features]
serial_debug = []
live = []

View File

@ -1,9 +1,10 @@
/* This is the name of the Rust function we start in */
ENTRY(start) ENTRY(start)
OUTPUT_FORMAT(elf32-i386) OUTPUT_FORMAT(elf32-i386)
SECTIONS { SECTIONS {
/* The start address must match bootloader.asm */ /* The start address must match main.asm */
. = 0x13000; . = 0x8000;
. += SIZEOF_HEADERS; . += SIZEOF_HEADERS;
. = ALIGN(4096); . = ALIGN(4096);

View File

@ -1,3 +1,3 @@
[toolchain] [toolchain]
channel = "nightly-2024-05-11" channel = "nightly-2025-02-01"
components = ["rust-src"] components = ["rust-src"]

View File

@ -0,0 +1,5 @@
#[macro_use]
pub mod serial;
pub mod pic;
pub mod vga;

94
tetros/src/drivers/pic.rs Normal file
View File

@ -0,0 +1,94 @@
//! Control routines for the x86
//! 8259 Programmable Interrupt Controller
//!
//! This helps us configure interrupts that receive
//! keyboard input and timer pulses.
use crate::os::util::outb;
/// IO base address for master PIC
const PIC_A: u32 = 0x20;
/// Command address for master PIC
const PIC_A_COMMAND: u32 = PIC_A;
/// Data address for master PIC
const PIC_A_DATA: u32 = PIC_A + 1;
/// IO base address for slave PIC
const PIC_B: u32 = 0xA0;
/// Command address for slave PIC
const PIC_B_COMMAND: u32 = PIC_B;
/// Data address for slave PIC
const PIC_B_DATA: u32 = PIC_B + 1;
/// A driver for the PIC
///
/// Reference:
/// - https://wiki.osdev.org/8259_PIC
/// - https://os.phil-opp.com/hardware-interrupts
pub struct PICDriver {
offset_pic_a: u8,
offset_pic_b: u8,
}
impl PICDriver {
/// Create a PIC driver with the given offsets
pub const fn new(offset_pic_a: u8, offset_pic_b: u8) -> Self {
Self {
offset_pic_a,
offset_pic_b,
}
}
fn send_a_cmd(&self, cmd: u8) {
unsafe { outb(PIC_A_COMMAND, cmd) }
}
fn send_a_data(&self, cmd: u8) {
unsafe { outb(PIC_A_DATA, cmd) }
}
fn send_b_cmd(&self, cmd: u8) {
unsafe { outb(PIC_B_COMMAND, cmd) }
}
fn send_b_data(&self, cmd: u8) {
unsafe { outb(PIC_B_DATA, cmd) }
}
/// Send an EOI for the given IRQ.
///
/// This needs to be called at the end of each interrupt handler.
/// If `both` is true, reset both PICs. This is only necessary
/// when we handle interrupts from PIC_B.
pub fn send_eoi(&self, both: bool) {
if both {
self.send_b_cmd(0x20);
}
self.send_a_cmd(0x20);
}
/// Initialize this PIC driver.
/// This should be called as early as possible.
pub fn init(&mut self) {
const ICW1_ICW4: u8 = 0x01; /* Indicates that ICW4 will be present */
const ICW1_INIT: u8 = 0x10; /* Initialization - required! */
const ICW4_8086: u8 = 0x01; /* 8086/88 (MCS-80/85) mode */
self.send_a_cmd(ICW1_INIT | ICW1_ICW4);
self.send_b_cmd(ICW1_INIT | ICW1_ICW4);
self.send_a_data(self.offset_pic_a); // ICW2: Master PIC vector offset
self.send_b_data(self.offset_pic_b); // ICW2: Slave PIC vector offset
self.send_a_data(4); // ICW3: tell Master PIC that there is a slave PIC at IRQ2 (0000 0100)
self.send_b_data(2); // ICW3: tell Slave PIC its cascade identity (0000 0010)
// ICW4: have the PICs use 8086 mode (and not 8080 mode)
self.send_a_data(ICW4_8086);
self.send_b_data(ICW4_8086);
// Unmask both PICs
self.send_a_data(0);
self.send_b_data(0);
}
}

View File

@ -0,0 +1,51 @@
//! Serial port driver, for debug.
//!
//! This file provides the usual `print`
//! and `println` macros (which are usually
//! provided by `std`) that send messages out
//! of the serial port.
use lazy_static::lazy_static;
use spin::Mutex;
use uart_16550::SerialPort;
use crate::os::util::without_interrupts;
lazy_static! {
pub static ref SERIAL1: Mutex<SerialPort> = {
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
serial_port.init();
Mutex::new(serial_port)
};
}
#[doc(hidden)]
pub fn _print(args: core::fmt::Arguments<'_>) {
use core::fmt::Write;
// Disable interrupts to prevent deadlocks
// (we might get an interrupt while printing)
without_interrupts(|| {
SERIAL1
.lock()
.write_fmt(args)
.expect("Printing to serial failed");
})
}
/// Prints to the host through the serial interface.
#[macro_export]
macro_rules! print {
($($arg:tt)*) => {
$crate::drivers::serial::_print(format_args!($($arg)*));
};
}
/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($fmt:expr) => ($crate::print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::print!(
concat!($fmt, "\n"), $($arg)*));
}

116
tetros/src/drivers/vga.rs Normal file
View File

@ -0,0 +1,116 @@
use core::slice;
use rand::seq::IndexedRandom;
use crate::RNG;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VgaColor {
Black,
Gray,
Blue,
Cyan,
Orange,
Red,
Green,
Purple,
Yellow,
}
impl VgaColor {
pub fn as_u8(self) -> u8 {
match self {
Self::Black => 0b0000_0000,
Self::Gray => 0b1110_0000,
Self::Blue => 0b0000_0001,
Self::Cyan => 0b0000_0011,
Self::Orange => 0b0000_0110,
Self::Red => 0b0000_0100,
Self::Green => 0b0000_0010,
Self::Purple => 0b0000_0101,
Self::Yellow => 0b1100_0000,
}
}
/// Pick a random non-utility color
pub fn choose_rand() -> Self {
let colors = [
VgaColor::Blue,
VgaColor::Cyan,
VgaColor::Orange,
VgaColor::Red,
VgaColor::Green,
VgaColor::Purple,
VgaColor::Yellow,
];
let mut rng = RNG.lock();
*(colors.choose(&mut rng).unwrap())
}
}
/// VGA driver for mode 0x13:
///
/// - mode: graphics
/// - text res: 40x25
/// - pixel box: 8x8
/// - pixel res: 320x200
/// - colors: 256/256k
/// - addr: A000
/// - pixel format: RRRGGGBB
pub struct Vga13h {
// Double frame buffers
fb_a: [u8; Vga13h::WIDTH * Vga13h::HEIGHT],
fb_b: [u8; Vga13h::WIDTH * Vga13h::HEIGHT],
/// If true, show fb_a (and write to fb_b).
/// if false, show fb_b.
show_fb_a: bool,
}
impl Vga13h {
pub const WIDTH: usize = 320;
pub const HEIGHT: usize = 200;
pub const ADDR: usize = 0xA0000;
/// Initialize a new VGA driver.
///
/// Only one of these should exist.
pub const unsafe fn new() -> Self {
Self {
fb_a: [0; Vga13h::WIDTH * Vga13h::HEIGHT],
fb_b: [0; Vga13h::WIDTH * Vga13h::HEIGHT],
show_fb_a: true,
}
}
unsafe fn segment(&mut self) -> &'static mut [u8] {
slice::from_raw_parts_mut(Vga13h::ADDR as *mut u8, Vga13h::WIDTH * Vga13h::HEIGHT)
}
pub fn swap(&mut self) {
let seg = unsafe { self.segment() };
if self.show_fb_a {
seg.copy_from_slice(&self.fb_b);
self.show_fb_a = false;
self.fb_a.fill(0);
} else {
seg.copy_from_slice(&self.fb_a);
self.show_fb_a = true;
self.fb_b.fill(0);
}
}
pub fn get_fb(&mut self) -> &mut [u8; Vga13h::WIDTH * Vga13h::HEIGHT] {
if self.show_fb_a {
&mut self.fb_b
} else {
&mut self.fb_a
}
}
pub fn pix_idx(x: usize, y: usize) -> usize {
debug_assert!(x < Vga13h::WIDTH);
debug_assert!(y < Vga13h::HEIGHT);
return y * Vga13h::WIDTH + x;
}
}

183
tetros/src/game/board.rs Normal file
View File

@ -0,0 +1,183 @@
use crate::drivers::vga::{Vga13h, VgaColor};
use super::FallingTetromino;
/// The state of a cell in the game board
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TetrisCell {
Empty,
Filled { color: VgaColor },
}
/// The tetris board
pub struct TetrisBoard {
board: [TetrisCell; TetrisBoard::BOARD_WIDTH * TetrisBoard::BOARD_HEIGHT],
}
impl TetrisBoard {
/// The width of this board, in cells
const BOARD_WIDTH: usize = 10;
/// The height of this board, in cells
const BOARD_HEIGHT: usize = 20;
/// The side length of a (square) cell, in pixels
const CELL_SIZE: usize = 9;
pub const fn new() -> Self {
Self {
board: [TetrisCell::Empty; TetrisBoard::BOARD_WIDTH * TetrisBoard::BOARD_HEIGHT],
}
}
/// Find and remove all filled rows,
/// shifting upper rows down.
pub fn collapse(&mut self) {
let mut y = Self::BOARD_HEIGHT - 1;
'outer: loop {
for x in 0..Self::BOARD_WIDTH {
let cell = self.get_cell(x, y);
if cell == Some(&TetrisCell::Empty) {
if y == 0 {
break 'outer;
}
y -= 1;
continue 'outer;
}
}
// We found a row that needs to be cleared
// Shift everything down
for yy in (1..=y).rev() {
for x in 0..Self::BOARD_WIDTH {
let top = *self.get_cell(x, yy - 1).unwrap();
let bot = self.get_cell_mut(x, yy).unwrap();
*bot = top;
}
}
// Clear the top row
for x in 0..Self::BOARD_WIDTH {
*self.get_cell_mut(x, 0).unwrap() = TetrisCell::Empty;
}
}
}
/// Place the given tetromino on the board,
/// filling the cells it occupies.
///
/// If the tetromino cells that overlap
/// non-empty board cells are ignored.
pub fn place_tetromino(&mut self, tetromino: FallingTetromino) {
for (x, y) in tetromino.tiles() {
let cell = self.get_cell_mut(x, y);
if let Some(cell) = cell {
*cell = TetrisCell::Filled {
color: tetromino.color,
};
}
}
}
/// Returns `false` if the given tetromino intersects a filled cell
/// or exits the board boundary
pub fn tetromino_valid(&self, tetromino: &FallingTetromino) -> bool {
for (x, y) in tetromino.tiles() {
let cell = self.get_cell(x, y);
if cell != Some(&TetrisCell::Empty) {
return false;
}
}
return true;
}
/// Get the value of the cell at the given position.
/// Returns [`None`] if (x, y) exceeds the board's bounds.
pub fn get_cell(&self, x: usize, y: usize) -> Option<&TetrisCell> {
return self.board.get(y * TetrisBoard::BOARD_WIDTH + x);
}
/// Get a mutable reference to the cell at the given position.
/// Returns [`None`] if (x, y) exceeds the board's bounds.
pub fn get_cell_mut(&mut self, x: usize, y: usize) -> Option<&mut TetrisCell> {
return self.board.get_mut(y * TetrisBoard::BOARD_WIDTH + x);
}
}
//
// MARK: draw routines
//
impl TetrisBoard {
/// Draw a cell of the given color on `fb`.
/// (x, y) is the pixel position of the cell (NOT board coordinates).
fn draw_cell(&self, fb: &mut [u8], color: VgaColor, x: usize, y: usize) {
let color = color.as_u8();
for yo in 0..TetrisBoard::CELL_SIZE {
let left = Vga13h::pix_idx(x, y + yo);
let right = Vga13h::pix_idx(x + TetrisBoard::CELL_SIZE, y + yo);
fb[left..right].copy_from_slice(&[color; TetrisBoard::CELL_SIZE]);
}
}
/// Draw the tetris board's frame
fn draw_frame(&self, fb: &mut [u8], x: usize, y: usize) {
let color = VgaColor::Gray.as_u8();
for yo in 0..TetrisBoard::CELL_SIZE {
let left = Vga13h::pix_idx(x, y + yo);
let right = Vga13h::pix_idx(x + TetrisBoard::CELL_SIZE, y + yo);
fb[left..right].copy_from_slice(&[color; TetrisBoard::CELL_SIZE]);
}
}
/// Draw this tetris board using the given VGA driver.
pub fn draw(&self, vga: &mut Vga13h, falling: Option<&FallingTetromino>) {
let fb = vga.get_fb();
// Draw cells
for bx in 0..TetrisBoard::BOARD_WIDTH {
for by in 0..TetrisBoard::BOARD_HEIGHT {
let cell = self.board[by * TetrisBoard::BOARD_WIDTH + bx];
let dx = (bx + 1) * TetrisBoard::CELL_SIZE;
let dy = (by + 1) * TetrisBoard::CELL_SIZE;
if let TetrisCell::Filled { color } = cell {
self.draw_cell(fb, color, dx, dy);
} else {
self.draw_cell(fb, VgaColor::Black, dx, dy);
}
}
}
// Draw falling tetromino
if let Some(falling) = falling {
for (x, y) in falling.tiles() {
let dx = (x + 1) * TetrisBoard::CELL_SIZE;
let dy = (y + 1) * TetrisBoard::CELL_SIZE;
self.draw_cell(fb, falling.color, dx, dy);
}
}
// Draw frame
for bx in 0..TetrisBoard::BOARD_WIDTH + 2 {
self.draw_frame(fb, bx * TetrisBoard::CELL_SIZE, 0);
self.draw_frame(
fb,
bx * TetrisBoard::CELL_SIZE,
(TetrisBoard::BOARD_HEIGHT + 1) * TetrisBoard::CELL_SIZE,
);
}
for by in 0..TetrisBoard::BOARD_HEIGHT + 2 {
self.draw_frame(fb, 0, by * 9);
self.draw_frame(
fb,
(TetrisBoard::BOARD_WIDTH + 1) * TetrisBoard::CELL_SIZE,
by * TetrisBoard::CELL_SIZE,
);
}
}
}

320
tetros/src/game/falling.rs Normal file
View File

@ -0,0 +1,320 @@
use rand::seq::IndexedRandom;
use crate::{drivers::vga::VgaColor, RNG};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Tetromino {
/// The 1x4 line
Straight,
/// The 2x2 square
Square,
/// The "L" shape
AngleRight,
/// The mirror "L" shape
AngleLeft,
/// The skew shape "Z"
SkewRight,
/// The mirror skew shape
SkewLeft,
/// The "T" shape
Tee,
}
impl Tetromino {
/// Pick a random tetromino
pub fn choose_rand() -> Self {
let ominos = [
Tetromino::Straight,
Tetromino::Square,
Tetromino::AngleRight,
Tetromino::AngleLeft,
Tetromino::SkewRight,
Tetromino::SkewLeft,
Tetromino::Tee,
];
let mut rng = RNG.lock();
*(ominos.choose(&mut rng).unwrap())
}
}
#[derive(Debug, Clone, Copy)]
pub enum Direction {
North,
East,
South,
West,
}
impl Direction {
/// Rotate this direction clockwise
pub fn rot_cw(self) -> Self {
match self {
Self::North => Self::East,
Self::East => Self::South,
Self::South => Self::West,
Self::West => Self::North,
}
}
}
#[derive(Debug, Clone)]
pub struct FallingTetromino {
tetromino: Tetromino,
direction: Direction,
center_x: usize,
center_y: usize,
pub color: VgaColor,
}
impl FallingTetromino {
/// Make a new falling tetromino
pub fn new(tetromino: Tetromino, color: VgaColor, center_x: usize, center_y: usize) -> Self {
Self {
tetromino,
direction: Direction::North,
color,
center_x,
center_y,
}
}
/// Generate a random tetromino at the given position
pub fn random(center_x: usize, center_y: usize) -> Self {
Self::new(
Tetromino::choose_rand(),
VgaColor::choose_rand(),
center_x,
center_y,
)
}
// Move this tetromino
pub fn translate(&mut self, x: i16, y: i16) {
if x > 0 {
let x = usize::try_from(x).unwrap();
self.center_x += x;
} else if x < 0 {
let x = usize::try_from(-x).unwrap();
self.center_x -= x;
}
if y > 0 {
let y = usize::try_from(y).unwrap();
self.center_y += y;
} else if y < 0 {
let y = usize::try_from(-y).unwrap();
self.center_y -= y;
}
}
/// Rotate this tetromino clockwise
pub fn rotate_cw(&mut self) {
self.direction = self.direction.rot_cw()
}
/// Returns the positions of this falling tetromino's tiles.
pub fn tiles(&self) -> [(usize, usize); 4] {
match (&self.tetromino, self.direction) {
(Tetromino::Square, _) => [
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
(self.center_x, self.center_y + 1),
(self.center_x + 1, self.center_y + 1),
],
//
// Straight
//
(Tetromino::Straight, Direction::North) => [
(self.center_x, self.center_y - 1),
(self.center_x, self.center_y),
(self.center_x, self.center_y + 1),
(self.center_x, self.center_y + 2),
],
(Tetromino::Straight, Direction::East) => [
(self.center_x - 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
(self.center_x + 2, self.center_y),
],
(Tetromino::Straight, Direction::South) => [
(self.center_x + 1, self.center_y - 1),
(self.center_x + 1, self.center_y),
(self.center_x + 1, self.center_y + 1),
(self.center_x + 1, self.center_y + 2),
],
(Tetromino::Straight, Direction::West) => [
(self.center_x - 1, self.center_y + 1),
(self.center_x, self.center_y + 1),
(self.center_x + 1, self.center_y + 1),
(self.center_x + 2, self.center_y + 1),
],
//
// Right Angle
//
(Tetromino::AngleRight, Direction::North) => [
(self.center_x, self.center_y - 1),
(self.center_x, self.center_y),
(self.center_x, self.center_y + 1),
(self.center_x + 1, self.center_y + 1),
],
(Tetromino::AngleRight, Direction::East) => [
(self.center_x - 1, self.center_y + 1),
(self.center_x - 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
],
(Tetromino::AngleRight, Direction::South) => [
(self.center_x - 1, self.center_y - 1),
(self.center_x, self.center_y - 1),
(self.center_x, self.center_y),
(self.center_x, self.center_y + 1),
],
(Tetromino::AngleRight, Direction::West) => [
(self.center_x - 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
(self.center_x + 1, self.center_y - 1),
],
//
// Left Angle
//
(Tetromino::AngleLeft, Direction::North) => [
(self.center_x, self.center_y - 1),
(self.center_x, self.center_y),
(self.center_x, self.center_y + 1),
(self.center_x - 1, self.center_y + 1),
],
(Tetromino::AngleLeft, Direction::East) => [
(self.center_x - 1, self.center_y - 1),
(self.center_x - 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
],
(Tetromino::AngleLeft, Direction::South) => [
(self.center_x + 1, self.center_y - 1),
(self.center_x, self.center_y - 1),
(self.center_x, self.center_y),
(self.center_x, self.center_y + 1),
],
(Tetromino::AngleLeft, Direction::West) => [
(self.center_x - 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
(self.center_x + 1, self.center_y + 1),
],
//
// Left Skew
//
(Tetromino::SkewLeft, Direction::North) => [
(self.center_x - 1, self.center_y + 1),
(self.center_x, self.center_y + 1),
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
],
(Tetromino::SkewLeft, Direction::East) => [
(self.center_x - 1, self.center_y - 1),
(self.center_x - 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x, self.center_y + 1),
],
(Tetromino::SkewLeft, Direction::South) => [
(self.center_x - 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x, self.center_y - 1),
(self.center_x + 1, self.center_y - 1),
],
(Tetromino::SkewLeft, Direction::West) => [
(self.center_x, self.center_y - 1),
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
(self.center_x + 1, self.center_y + 1),
],
//
// Right Skew
//
(Tetromino::SkewRight, Direction::North) => [
(self.center_x - 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x, self.center_y + 1),
(self.center_x + 1, self.center_y + 1),
],
(Tetromino::SkewRight, Direction::East) => [
(self.center_x, self.center_y - 1),
(self.center_x, self.center_y),
(self.center_x - 1, self.center_y),
(self.center_x - 1, self.center_y + 1),
],
(Tetromino::SkewRight, Direction::South) => [
(self.center_x - 1, self.center_y - 1),
(self.center_x, self.center_y - 1),
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
],
(Tetromino::SkewRight, Direction::West) => [
(self.center_x + 1, self.center_y - 1),
(self.center_x + 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x, self.center_y + 1),
],
//
// Tee
//
(Tetromino::Tee, Direction::North) => [
(self.center_x - 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
(self.center_x, self.center_y - 1),
],
(Tetromino::Tee, Direction::East) => [
(self.center_x, self.center_y - 1),
(self.center_x, self.center_y),
(self.center_x, self.center_y + 1),
(self.center_x + 1, self.center_y),
],
(Tetromino::Tee, Direction::South) => [
(self.center_x - 1, self.center_y),
(self.center_x, self.center_y),
(self.center_x + 1, self.center_y),
(self.center_x, self.center_y + 1),
],
(Tetromino::Tee, Direction::West) => [
(self.center_x, self.center_y - 1),
(self.center_x, self.center_y),
(self.center_x, self.center_y + 1),
(self.center_x - 1, self.center_y),
],
}
}
}

8
tetros/src/game/mod.rs Normal file
View File

@ -0,0 +1,8 @@
//! This crate contains all tetris game logic.
//! No low-level magic here.
mod board;
pub use board::*;
mod falling;
pub use falling::*;

133
tetros/src/idt/entry.rs Normal file
View File

@ -0,0 +1,133 @@
use core::{
fmt::{self},
marker::PhantomData,
};
use crate::os::util::get_cs;
use super::{HandlerFuncType, VirtAddr};
/// An Interrupt Descriptor Table entry.
///
/// The generic parameter is some [`HandlerFuncType`], depending on the interrupt vector.
///
/// For reference, see https://wiki.osdev.org/Interrupt_Descriptor_Table#Gate_Descriptor
#[derive(Clone, Copy)]
#[repr(C)]
pub struct Entry<F> {
phantom: PhantomData<F>,
/// Low 16 bits of the 32-bit ISR pointer
pointer_low: u16,
/// A segment selector that points to a valid code segment in the GDT
segment: u16,
/// Always zero
reserved: u8,
/// Interrupt options.
///
/// Bits 0-3: gate type. One of:
/// - 0101: Task Gate, note that in this case, the Offset value is unused and should be set to zero.
/// - 0110: 16-bit Interrupt Gate
/// - 0111: 16-bit Trap Gate
/// - 1110: 32-bit Interrupt Gate
/// - 1111: 32-bit Trap Gate
///
/// Bit 4: always zero
///
/// Bits 5-6: DPL, defines cpu privilege level require to INT this interrupt.
/// For our purposes, always zero.
///
/// Bit 7: 1 if present, 0 if not.
///
/// Note that these appear in reverse order in Rust.
/// For example, 0b1000_1110 sets `present = 1` and `type = 1110`.
options: u8,
/// High 16 bits of the 32-bit ISR pointer
pointer_high: u16,
}
impl<T> fmt::Debug for Entry<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Entry")
.field("preset", &self.is_present())
.field(
"handler_addr",
&format_args!("{:#x}", self.handler_addr().unwrap_or(VirtAddr::new(0))),
)
.field("options", &self.options)
.finish()
}
}
// spell:off
impl<F> Entry<F> {
/// Create a valid non-present IDT entry.
#[inline]
pub const fn missing() -> Self {
Entry {
pointer_low: 0,
segment: 0,
reserved: 0,
options: 0,
pointer_high: 0,
phantom: PhantomData,
}
}
/// Sets the handler address for the IDT entry and sets the following defaults:
/// - The code selector is the code segment currently active in the CPU
/// - The present bit is set
/// - Interrupts are disabled on handler invocation
/// - The privilege level (DPL) is [`PrivilegeLevel::Ring0`]
/// - No IST is configured (existing stack will be used)
/// - This is a 32-bit interrupt gate
///
/// # Safety
///
/// The caller must ensure that `addr` is the address of a valid interrupt handler function,
/// and the signature of such a function is correct for the entry type.
#[inline]
pub unsafe fn set_handler_addr(&mut self, addr: VirtAddr) {
let ptr = addr.as_u32();
self.pointer_low = ptr as u16;
self.pointer_high = (ptr >> 16) as u16;
self.options = 0b1000_1110; // Present 32-bit interrupt gate
// SAFETY: The current CS is a valid, long-mode code segment.
self.segment = get_cs();
}
/// True if the "present" bit is set, false otherwise.
pub fn is_present(&self) -> bool {
return self.options & 0b1000_0000 == 0b1000_0000;
}
/// Get the address of this entry's handler.
pub fn handler_addr(&self) -> Option<VirtAddr> {
self.is_present().then_some(VirtAddr::new(
self.pointer_low as u32 + ((self.pointer_high as u32) << 16),
))
}
}
// spell:on
// spell:off
impl<F: HandlerFuncType> Entry<F> {
/// Sets the handler address for the IDT entry and sets the following defaults:
/// - The code selector is the code segment currently active in the CPU
/// - The present bit is set
/// - Interrupts are disabled on handler invocation
/// - The privilege level (DPL) is [`PrivilegeLevel::Ring0`]
/// - No IST is configured (existing stack will be used)
/// - This is a 32-bit interrupt gate
#[inline]
pub fn set_handler_fn(&mut self, handler: F) {
unsafe { self.set_handler_addr(handler.to_virt_addr()) }
}
}
// spell:on

122
tetros/src/idt/handler.rs Normal file
View File

@ -0,0 +1,122 @@
use bitflags::bitflags;
use super::{InterruptStackFrame, VirtAddr};
bitflags! {
/// Describes an page fault error code.
///
/// This structure is defined by the following manual sections:
/// * AMD Volume 2: 8.4.2
/// * Intel Volume 3A: 4.7
#[repr(transparent)]
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
pub struct PageFaultErrorCode: u64 {
/// If this flag is set, the page fault was caused by a page-protection violation,
/// else the page fault was caused by a not-present page.
const PROTECTION_VIOLATION = 1;
/// If this flag is set, the memory access that caused the page fault was a write.
/// Else the access that caused the page fault is a memory read. This bit does not
/// necessarily indicate the cause of the page fault was a read or write violation.
const CAUSED_BY_WRITE = 1 << 1;
/// If this flag is set, an access in user mode (CPL=3) caused the page fault. Else
/// an access in supervisor mode (CPL=0, 1, or 2) caused the page fault. This bit
/// does not necessarily indicate the cause of the page fault was a privilege violation.
const USER_MODE = 1 << 2;
/// If this flag is set, the page fault is a result of the processor reading a 1 from
/// a reserved field within a page-translation-table entry.
const MALFORMED_TABLE = 1 << 3;
/// If this flag is set, it indicates that the access that caused the page fault was an
/// instruction fetch.
const INSTRUCTION_FETCH = 1 << 4;
/// If this flag is set, it indicates that the page fault was caused by a protection key.
const PROTECTION_KEY = 1 << 5;
/// If this flag is set, it indicates that the page fault was caused by a shadow stack
/// access.
const SHADOW_STACK = 1 << 6;
/// If this flag is set, it indicates that the page fault was caused by SGX access-control
/// requirements (Intel-only).
const SGX = 1 << 15;
/// If this flag is set, it indicates that the page fault is a result of the processor
/// encountering an RMP violation (AMD-only).
const RMP = 1 << 31;
}
}
//
// MARK: types
//
/// A handler function for an interrupt or an exception without error code.
pub type HandlerFunc = extern "x86-interrupt" fn(InterruptStackFrame);
/// A handler function for an exception that pushes an error code.
pub type HandlerFuncWithErrCode = extern "x86-interrupt" fn(InterruptStackFrame, error_code: u32);
/// A handler function that must not return, e.g. for a machine check exception.
pub type DivergingHandlerFunc = extern "x86-interrupt" fn(InterruptStackFrame) -> !;
/// A handler function with an error code that must not return, e.g. for a double fault exception.
pub type DivergingHandlerFuncWithErrCode =
extern "x86-interrupt" fn(InterruptStackFrame, error_code: u32) -> !;
/// A page fault handler function that pushes a page fault error code.
pub type PageFaultHandlerFunc =
extern "x86-interrupt" fn(InterruptStackFrame, error_code: PageFaultErrorCode);
/// A common trait for all handler functions usable in [`Entry`].
///
/// # Safety
///
/// Implementors have to ensure that `to_virt_addr` returns a valid address.
pub unsafe trait HandlerFuncType {
/// Get the virtual address of the handler function.
fn to_virt_addr(self) -> VirtAddr;
}
unsafe impl HandlerFuncType for HandlerFunc {
#[inline]
fn to_virt_addr(self) -> VirtAddr {
#[expect(clippy::fn_to_numeric_cast_with_truncation)]
VirtAddr::new(self as u32)
}
}
unsafe impl HandlerFuncType for HandlerFuncWithErrCode {
#[inline]
fn to_virt_addr(self) -> VirtAddr {
#[expect(clippy::fn_to_numeric_cast_with_truncation)]
VirtAddr::new(self as u32)
}
}
unsafe impl HandlerFuncType for DivergingHandlerFunc {
#[inline]
fn to_virt_addr(self) -> VirtAddr {
#[expect(clippy::fn_to_numeric_cast_with_truncation)]
VirtAddr::new(self as u32)
}
}
unsafe impl HandlerFuncType for DivergingHandlerFuncWithErrCode {
#[inline]
fn to_virt_addr(self) -> VirtAddr {
#[expect(clippy::fn_to_numeric_cast_with_truncation)]
VirtAddr::new(self as u32)
}
}
unsafe impl HandlerFuncType for PageFaultHandlerFunc {
#[inline]
fn to_virt_addr(self) -> VirtAddr {
#[expect(clippy::fn_to_numeric_cast_with_truncation)]
VirtAddr::new(self as u32)
}
}

20
tetros/src/idt/mod.rs Normal file
View File

@ -0,0 +1,20 @@
//! IDT structures and routines for 32-bit x86.
//!
//! Based on code from the `x86_64` crate.
//! Many comments are copied verbatim.
//! (most notably, in `table.rs`)
mod virtaddr;
pub use virtaddr::*;
mod entry;
pub use entry::*;
mod table;
pub use table::*;
mod handler;
pub use handler::*;
mod stackframe;
pub use stackframe::*;

View File

@ -0,0 +1,62 @@
use core::{fmt, ops::Deref};
use super::VirtAddr;
use crate::os::EFlags;
/// Wrapper type for the interrupt stack frame pushed by the CPU.
///
/// This type derefs to an [`InterruptStackFrameValue`], which allows reading the actual values.
///
/// This wrapper ensures that the stack frame cannot be modified.
/// This prevents undefined behavior.
#[repr(transparent)]
pub struct InterruptStackFrame(InterruptStackFrameValue);
impl Deref for InterruptStackFrame {
type Target = InterruptStackFrameValue;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Debug for InterruptStackFrame {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// Represents the interrupt stack frame pushed by the CPU on interrupt or exception entry.
///
/// See https://wiki.osdev.org/Interrupt_Service_Routines#x86
#[derive(Clone, Copy)]
#[repr(C)]
pub struct InterruptStackFrameValue {
/// This value points to the instruction that should be executed when the interrupt
/// handler returns. For most interrupts, this value points to the instruction immediately
/// following the last executed instruction. However, for some exceptions (e.g., page faults),
/// this value points to the faulting instruction, so that the instruction is restarted on
/// return.
pub eip: VirtAddr,
/// The code segment selector at the time of the interrupt.
pub cs: u16,
/// Padding for CS
_reserved1: [u8; 2],
/// The EFLAGS register before the interrupt handler was invoked.
pub cpu_flags: EFlags,
}
impl fmt::Debug for InterruptStackFrameValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("InterruptStackFrame");
s.field("instruction_pointer", &self.eip);
s.field("code_segment", &self.cs);
s.field("cpu_flags", &self.cpu_flags);
s.finish()
}
}

481
tetros/src/idt/table.rs Normal file
View File

@ -0,0 +1,481 @@
use core::{arch::asm, mem::size_of};
use super::{
handler::HandlerFunc, DivergingHandlerFunc, DivergingHandlerFuncWithErrCode, Entry,
HandlerFuncWithErrCode, PageFaultHandlerFunc,
};
// spell:off
#[derive(Clone, Debug)]
#[repr(C)]
#[repr(align(8))]
pub struct InterruptDescriptorTable {
/// A divide error (`#DE`) occurs when the denominator of a DIV instruction or
/// an IDIV instruction is 0. A `#DE` also occurs if the result is too large to be
/// represented in the destination.
///
/// The saved instruction pointer points to the instruction that caused the `#DE`.
///
/// The vector number of the `#DE` exception is 0.
pub divide_error: Entry<HandlerFunc>,
/// When the debug-exception mechanism is enabled, a `#DB` exception can occur under any
/// of the following circumstances:
///
/// <details>
///
/// - Instruction execution.
/// - Instruction single stepping.
/// - Data read.
/// - Data write.
/// - I/O read.
/// - I/O write.
/// - Task switch.
/// - Debug-register access, or general detect fault (debug register access when DR7.GD=1).
/// - Executing the INT1 instruction (opcode 0F1h).
///
/// </details>
///
/// `#DB` conditions are enabled and disabled using the debug-control register, `DR7`
/// and `RFLAGS.TF`.
///
/// In the following cases, the saved instruction pointer points to the instruction that
/// caused the `#DB`:
///
/// - Instruction execution.
/// - Invalid debug-register access, or general detect.
///
/// In all other cases, the instruction that caused the `#DB` is completed, and the saved
/// instruction pointer points to the instruction after the one that caused the `#DB`.
///
/// The vector number of the `#DB` exception is 1.
pub debug: Entry<HandlerFunc>,
/// An non maskable interrupt exception (NMI) occurs as a result of system logic
/// signaling a non-maskable interrupt to the processor.
///
/// The processor recognizes an NMI at an instruction boundary.
/// The saved instruction pointer points to the instruction immediately following the
/// boundary where the NMI was recognized.
///
/// The vector number of the NMI exception is 2.
pub non_maskable_interrupt: Entry<HandlerFunc>,
/// A breakpoint (`#BP`) exception occurs when an `INT3` instruction is executed. The
/// `INT3` is normally used by debug software to set instruction breakpoints by replacing
///
/// The saved instruction pointer points to the byte after the `INT3` instruction.
///
/// The vector number of the `#BP` exception is 3.
pub breakpoint: Entry<HandlerFunc>,
/// An overflow exception (`#OF`) occurs as a result of executing an `INTO` instruction
/// while the overflow bit in `RFLAGS` is set to 1.
///
/// The saved instruction pointer points to the instruction following the `INTO`
/// instruction that caused the `#OF`.
///
/// The vector number of the `#OF` exception is 4.
pub overflow: Entry<HandlerFunc>,
/// A bound-range exception (`#BR`) exception can occur as a result of executing
/// the `BOUND` instruction. The `BOUND` instruction compares an array index (first
/// operand) with the lower bounds and upper bounds of an array (second operand).
/// If the array index is not within the array boundary, the `#BR` occurs.
///
/// The saved instruction pointer points to the `BOUND` instruction that caused the `#BR`.
///
/// The vector number of the `#BR` exception is 5.
pub bound_range_exceeded: Entry<HandlerFunc>,
/// An invalid opcode exception (`#UD`) occurs when an attempt is made to execute an
/// invalid or undefined opcode. The validity of an opcode often depends on the
/// processor operating mode.
///
/// <details><summary>A `#UD` occurs under the following conditions:</summary>
///
/// - Execution of any reserved or undefined opcode in any mode.
/// - Execution of the `UD2` instruction.
/// - Use of the `LOCK` prefix on an instruction that cannot be locked.
/// - Use of the `LOCK` prefix on a lockable instruction with a non-memory target location.
/// - Execution of an instruction with an invalid-operand type.
/// - Execution of the `SYSENTER` or `SYSEXIT` instructions in long mode.
/// - Execution of any of the following instructions in 64-bit mode: `AAA`, `AAD`,
/// `AAM`, `AAS`, `BOUND`, `CALL` (opcode 9A), `DAA`, `DAS`, `DEC`, `INC`, `INTO`,
/// `JMP` (opcode EA), `LDS`, `LES`, `POP` (`DS`, `ES`, `SS`), `POPA`, `PUSH` (`CS`,
/// `DS`, `ES`, `SS`), `PUSHA`, `SALC`.
/// - Execution of the `ARPL`, `LAR`, `LLDT`, `LSL`, `LTR`, `SLDT`, `STR`, `VERR`, or
/// `VERW` instructions when protected mode is not enabled, or when virtual-8086 mode
/// is enabled.
/// - Execution of any legacy SSE instruction when `CR4.OSFXSR` is cleared to 0.
/// - Execution of any SSE instruction (uses `YMM`/`XMM` registers), or 64-bit media
/// instruction (uses `MMXTM` registers) when `CR0.EM` = 1.
/// - Execution of any SSE floating-point instruction (uses `YMM`/`XMM` registers) that
/// causes a numeric exception when `CR4.OSXMMEXCPT` = 0.
/// - Use of the `DR4` or `DR5` debug registers when `CR4.DE` = 1.
/// - Execution of `RSM` when not in `SMM` mode.
///
/// </details>
///
/// The saved instruction pointer points to the instruction that caused the `#UD`.
///
/// The vector number of the `#UD` exception is 6.
pub invalid_opcode: Entry<HandlerFunc>,
/// A device not available exception (`#NM`) occurs under any of the following conditions:
///
/// <details>
///
/// - An `FWAIT`/`WAIT` instruction is executed when `CR0.MP=1` and `CR0.TS=1`.
/// - Any x87 instruction other than `FWAIT` is executed when `CR0.EM=1`.
/// - Any x87 instruction is executed when `CR0.TS=1`. The `CR0.MP` bit controls whether the
/// `FWAIT`/`WAIT` instruction causes an `#NM` exception when `TS=1`.
/// - Any 128-bit or 64-bit media instruction when `CR0.TS=1`.
///
/// </details>
///
/// The saved instruction pointer points to the instruction that caused the `#NM`.
///
/// The vector number of the `#NM` exception is 7.
pub device_not_available: Entry<HandlerFunc>,
/// A double fault (`#DF`) exception can occur when a second exception occurs during
/// the handling of a prior (first) exception or interrupt handler.
///
/// <details>
///
/// Usually, the first and second exceptions can be handled sequentially without
/// resulting in a `#DF`. In this case, the first exception is considered _benign_, as
/// it does not harm the ability of the processor to handle the second exception. In some
/// cases, however, the first exception adversely affects the ability of the processor to
/// handle the second exception. These exceptions contribute to the occurrence of a `#DF`,
/// and are called _contributory exceptions_. The following exceptions are contributory:
///
/// - Invalid-TSS Exception
/// - Segment-Not-Present Exception
/// - Stack Exception
/// - General-Protection Exception
///
/// A double-fault exception occurs in the following cases:
///
/// - If a contributory exception is followed by another contributory exception.
/// - If a divide-by-zero exception is followed by a contributory exception.
/// - If a page fault is followed by another page fault or a contributory exception.
///
/// If a third interrupting event occurs while transferring control to the `#DF` handler,
/// the processor shuts down.
///
/// </details>
///
/// The returned error code is always zero. The saved instruction pointer is undefined,
/// and the program cannot be restarted.
///
/// The vector number of the `#DF` exception is 8.
pub double_fault: Entry<DivergingHandlerFuncWithErrCode>,
/// This interrupt vector is reserved. It is for a discontinued exception originally used
/// by processors that supported external x87-instruction coprocessors. On those processors,
/// the exception condition is caused by an invalid-segment or invalid-page access on an
/// x87-instruction coprocessor-instruction operand. On current processors, this condition
/// causes a general-protection exception to occur.
coprocessor_segment_overrun: Entry<HandlerFunc>,
/// An invalid TSS exception (`#TS`) occurs only as a result of a control transfer through
/// a gate descriptor that results in an invalid stack-segment reference using an `SS`
/// selector in the TSS.
///
/// The returned error code is the `SS` segment selector. The saved instruction pointer
/// points to the control-transfer instruction that caused the `#TS`.
///
/// The vector number of the `#TS` exception is 10.
pub invalid_tss: Entry<HandlerFuncWithErrCode>,
/// An segment-not-present exception (`#NP`) occurs when an attempt is made to load a
/// segment or gate with a clear present bit.
///
/// The returned error code is the segment-selector index of the segment descriptor
/// causing the `#NP` exception. The saved instruction pointer points to the instruction
/// that loaded the segment selector resulting in the `#NP`.
///
/// The vector number of the `#NP` exception is 11.
pub segment_not_present: Entry<HandlerFuncWithErrCode>,
/// An stack segment exception (`#SS`) can occur in the following situations:
///
/// - Implied stack references in which the stack address is not in canonical
/// form. Implied stack references include all push and pop instructions, and any
/// instruction using `RSP` or `RBP` as a base register.
/// - Attempting to load a stack-segment selector that references a segment descriptor
/// containing a clear present bit.
/// - Any stack access that fails the stack-limit check.
///
/// The returned error code depends on the cause of the `#SS`. If the cause is a cleared
/// present bit, the error code is the corresponding segment selector. Otherwise, the
/// error code is zero. The saved instruction pointer points to the instruction that
/// caused the `#SS`.
///
/// The vector number of the `#NP` exception is 12.
pub stack_segment_fault: Entry<HandlerFuncWithErrCode>,
/// A general protection fault (`#GP`) can occur in various situations. Common causes include:
///
/// - Executing a privileged instruction while `CPL > 0`.
/// - Writing a 1 into any register field that is reserved, must be zero (MBZ).
/// - Attempting to execute an SSE instruction specifying an unaligned memory operand.
/// - Loading a non-canonical base address into the `GDTR` or `IDTR`.
/// - Using WRMSR to write a read-only MSR.
/// - Any long-mode consistency-check violation.
///
/// The returned error code is a segment selector, if the cause of the `#GP` is
/// segment-related, and zero otherwise. The saved instruction pointer points to
/// the instruction that caused the `#GP`.
///
/// The vector number of the `#GP` exception is 13.
pub general_protection_fault: Entry<HandlerFuncWithErrCode>,
/// A page fault (`#PF`) can occur during a memory access in any of the following situations:
///
/// - A page-translation-table entry or physical page involved in translating the memory
/// access is not present in physical memory. This is indicated by a cleared present
/// bit in the translation-table entry.
/// - An attempt is made by the processor to load the instruction TLB with a translation
/// for a non-executable page.
/// - The memory access fails the paging-protection checks (user/supervisor, read/write,
/// or both).
/// - A reserved bit in one of the page-translation-table entries is set to 1. A `#PF`
/// occurs for this reason only when `CR4.PSE=1` or `CR4.PAE=1`.
///
/// The virtual (linear) address that caused the `#PF` is stored in the `CR2` register.
/// The saved instruction pointer points to the instruction that caused the `#PF`.
///
/// The page-fault error code is described by the
/// [`PageFaultErrorCode`](struct.PageFaultErrorCode.html) struct.
///
/// The vector number of the `#PF` exception is 14.
pub page_fault: Entry<PageFaultHandlerFunc>,
/// vector nr. 15
reserved_1: Entry<HandlerFunc>,
/// The x87 Floating-Point Exception-Pending exception (`#MF`) is used to handle unmasked x87
/// floating-point exceptions. In 64-bit mode, the x87 floating point unit is not used
/// anymore, so this exception is only relevant when executing programs in the 32-bit
/// compatibility mode.
///
/// The vector number of the `#MF` exception is 16.
pub x87_floating_point: Entry<HandlerFunc>,
/// An alignment check exception (`#AC`) occurs when an unaligned-memory data reference
/// is performed while alignment checking is enabled. An `#AC` can occur only when CPL=3.
///
/// The returned error code is always zero. The saved instruction pointer points to the
/// instruction that caused the `#AC`.
///
/// The vector number of the `#AC` exception is 17.
pub alignment_check: Entry<HandlerFuncWithErrCode>,
/// The machine check exception (`#MC`) is model specific. Processor implementations
/// are not required to support the `#MC` exception, and those implementations that do
/// support `#MC` can vary in how the `#MC` exception mechanism works.
///
/// There is no reliable way to restart the program.
///
/// The vector number of the `#MC` exception is 18.
pub machine_check: Entry<DivergingHandlerFunc>,
/// The SIMD Floating-Point Exception (`#XF`) is used to handle unmasked SSE
/// floating-point exceptions. The SSE floating-point exceptions reported by
/// the `#XF` exception are (including mnemonics):
///
/// - IE: Invalid-operation exception (also called #I).
/// - DE: Denormalized-operand exception (also called #D).
/// - ZE: Zero-divide exception (also called #Z).
/// - OE: Overflow exception (also called #O).
/// - UE: Underflow exception (also called #U).
/// - PE: Precision exception (also called #P or inexact-result exception).
///
/// The saved instruction pointer points to the instruction that caused the `#XF`.
///
/// The vector number of the `#XF` exception is 19.
pub simd_floating_point: Entry<HandlerFunc>,
/// vector nr. 20
pub virtualization: Entry<HandlerFunc>,
/// A #CP exception is generated when shadow stacks are enabled and mismatch
/// scenarios are detected (possible error code cases below).
///
/// The error code is the #CP error code, for each of the following situations:
/// - A RET (near) instruction encountered a return address mismatch.
/// - A RET (far) instruction encountered a return address mismatch.
/// - A RSTORSSP instruction encountered an invalid shadow stack restore token.
/// - A SETSSBY instruction encountered an invalid supervisor shadow stack token.
/// - A missing ENDBRANCH instruction if indirect branch tracking is enabled.
///
/// vector nr. 21
pub cp_protection_exception: Entry<HandlerFuncWithErrCode>,
/// vector nr. 22-27
reserved_2: [Entry<HandlerFunc>; 6],
/// The Hypervisor Injection Exception (`#HV`) is injected by a hypervisor
/// as a doorbell to inform an `SEV-SNP` enabled guest running with the
/// `Restricted Injection` feature of events to be processed.
///
/// `SEV-SNP` stands for the _"Secure Nested Paging"_ feature of the _"AMD
/// Secure Encrypted Virtualization"_ technology. The `Restricted
/// Injection` feature disables all hypervisor-based interrupt queuing
/// and event injection of all vectors except #HV.
///
/// The `#HV` exception is a benign exception and can only be injected as
/// an exception and without an error code. `SEV-SNP` enabled guests are
/// expected to communicate with the hypervisor about events via a
/// software-managed para-virtualization interface.
///
/// The vector number of the ``#HV`` exception is 28.
pub hv_injection_exception: Entry<HandlerFunc>,
/// The VMM Communication Exception (`#VC`) is always generated by hardware when an `SEV-ES`
/// enabled guest is running and an `NAE` event occurs.
///
/// `SEV-ES` stands for the _"Encrypted State"_ feature of the _"AMD Secure Encrypted Virtualization"_
/// technology. `NAE` stands for an _"Non-Automatic Exit"_, which is an `VMEXIT` event that requires
/// hypervisor emulation. See
/// [this whitepaper](https://www.amd.com/system/files/TechDocs/Protecting%20VM%20Register%20State%20with%20SEV-ES.pdf)
/// for an overview of the `SEV-ES` feature.
///
/// The `#VC` exception is a precise, contributory, fault-type exception utilizing exception vector 29.
/// This exception cannot be masked. The error code of the `#VC` exception is equal
/// to the `#VMEXIT` code of the event that caused the `NAE`.
///
/// In response to a `#VC` exception, a typical flow would involve the guest handler inspecting the error
/// code to determine the cause of the exception and deciding what register state must be copied to the
/// `GHCB` (_"Guest Hypervisor Communication Block"_) for the event to be handled. The handler
/// should then execute the `VMGEXIT` instruction to
/// create an `AE` and invoke the hypervisor. After a later `VMRUN`, guest execution will resume after the
/// `VMGEXIT` instruction where the handler can view the results from the hypervisor and copy state from
/// the `GHCB` back to its internal state as needed.
///
/// Note that it is inadvisable for the hypervisor to set the `VMCB` (_"Virtual Machine Control Block"_)
/// intercept bit for the `#VC` exception as
/// this would prevent proper handling of `NAE`s by the guest. Similarly, the hypervisor should avoid
/// setting intercept bits for events that would occur in the `#VC` handler (such as `IRET`).
///
/// The vector number of the ``#VC`` exception is 29.
pub vmm_communication_exception: Entry<HandlerFuncWithErrCode>,
/// The Security Exception (`#SX`) signals security-sensitive events that occur while
/// executing the VMM, in the form of an exception so that the VMM may take appropriate
/// action. (A VMM would typically intercept comparable sensitive events in the guest.)
/// In the current implementation, the only use of the `#SX` is to redirect external INITs
/// into an exception so that the VMM may — among other possibilities.
///
/// The only error code currently defined is 1, and indicates redirection of INIT has occurred.
///
/// The vector number of the ``#SX`` exception is 30.
pub security_exception: Entry<HandlerFuncWithErrCode>,
/// vector nr. 31
reserved_3: Entry<HandlerFunc>,
/// User-defined interrupts can be initiated either by system logic or software.
/// `interrupts[0]` is interrupt vector 32.
///
///
/// These occur when:
///
/// - System logic signals an external interrupt request to the processor. The signaling
/// mechanism and the method of communicating the interrupt vector to the processor are
/// implementation dependent.
/// - Software executes an `INTn` instruction. The `INTn` instruction operand provides
/// the interrupt vector number.
///
/// Both methods can be used to initiate an interrupt into vectors 0 through 255. However,
/// because vectors 0 through 31 are defined or reserved by the AMD64 architecture,
/// software should not use vectors in this range for purposes other than their defined use.
///
/// The saved instruction pointer depends on the interrupt source:
///
/// - External interrupts are recognized on instruction boundaries. The saved instruction
/// pointer points to the instruction immediately following the boundary where the
/// external interrupt was recognized.
/// - If the interrupt occurs as a result of executing the INTn instruction, the saved
/// instruction pointer points to the instruction after the INTn.
pub interrupts: [Entry<HandlerFunc>; 256 - 32],
}
// spell:on
//
// MARK: impl
//
impl InterruptDescriptorTable {
/// Creates a new IDT filled with non-present entries.
#[inline]
pub const fn new() -> InterruptDescriptorTable {
InterruptDescriptorTable {
divide_error: Entry::missing(),
debug: Entry::missing(),
non_maskable_interrupt: Entry::missing(),
breakpoint: Entry::missing(),
overflow: Entry::missing(),
bound_range_exceeded: Entry::missing(),
invalid_opcode: Entry::missing(),
device_not_available: Entry::missing(),
double_fault: Entry::missing(),
coprocessor_segment_overrun: Entry::missing(),
invalid_tss: Entry::missing(),
segment_not_present: Entry::missing(),
stack_segment_fault: Entry::missing(),
general_protection_fault: Entry::missing(),
page_fault: Entry::missing(),
reserved_1: Entry::missing(),
x87_floating_point: Entry::missing(),
alignment_check: Entry::missing(),
machine_check: Entry::missing(),
simd_floating_point: Entry::missing(),
virtualization: Entry::missing(),
cp_protection_exception: Entry::missing(),
reserved_2: [Entry::missing(); 6],
hv_injection_exception: Entry::missing(),
vmm_communication_exception: Entry::missing(),
security_exception: Entry::missing(),
reserved_3: Entry::missing(),
interrupts: [Entry::missing(); 256 - 32],
}
}
/// Loads the IDT in the CPU using the `lidt` command.
#[inline]
pub fn load(&'static self) {
unsafe { self.load_unsafe() }
}
/// Loads the IDT in the CPU using the `lidt` command.
///
/// # Safety
///
/// As long as it is the active IDT, you must ensure that:
/// - `self` is never destroyed.
/// - `self` always stays at the same memory location.
#[inline]
pub unsafe fn load_unsafe(&self) {
/// The data we push to the IDTR register
#[repr(C, packed(2))]
struct Idtr {
size: u16,
offset: u32,
}
let idtr = {
Idtr {
size: (size_of::<InterruptDescriptorTable>() - 1) as u16,
offset: self as *const _ as u32,
}
};
unsafe {
asm!("lidt [{}]", in(reg) &idtr, options(readonly, nostack, preserves_flags));
}
}
}

View File

@ -0,0 +1,56 @@
use core::fmt::{self};
/// A canonical 32-bit virtual memory address.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct VirtAddr(u32);
impl VirtAddr {
/// Create a new [`VirtAddr`] from a [`u32`].
#[inline]
pub const fn new(addr: u32) -> Self {
Self(addr)
}
/// Converts this address to a `u32`.
#[inline]
pub const fn as_u32(self) -> u32 {
self.0
}
}
impl fmt::Debug for VirtAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("VirtAddr")
.field(&format_args!("{:#x}", self.0))
.finish()
}
}
impl fmt::Binary for VirtAddr {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Binary::fmt(&self.0, f)
}
}
impl fmt::LowerHex for VirtAddr {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::LowerHex::fmt(&self.0, f)
}
}
impl fmt::Octal for VirtAddr {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Octal::fmt(&self.0, f)
}
}
impl fmt::UpperHex for VirtAddr {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::UpperHex::fmt(&self.0, f)
}
}

297
tetros/src/lib.rs Normal file
View File

@ -0,0 +1,297 @@
//! The main code of tetris
#![no_std]
#![feature(int_roundings)]
#![feature(lang_items)]
#![feature(abi_x86_interrupt)]
#![allow(internal_features)]
use lazy_static::lazy_static;
use rand::{rngs::SmallRng, Rng, SeedableRng};
use spin::Mutex;
use drivers::{pic::PICDriver, vga::Vga13h};
use game::{FallingTetromino, TetrisBoard};
use idt::{InterruptDescriptorTable, InterruptStackFrame};
use os::{
util::{inb, sti, without_interrupts},
ThunkData,
};
mod game;
mod idt;
mod os;
#[macro_use]
mod drivers;
//
// MARK: globals
//
// This code has no parallelism, so we don't _really_
// need locks. The Mutexes here satisfy Rust's
// "no mutable global state" rule.
//
// They also help prevent bugs, since we get deadlocks
// instead of hard-to-debug surprising behavior.
//
const PIC_OFFSET: u8 = 32;
static VGA: Mutex<Vga13h> = Mutex::new(unsafe { Vga13h::new() });
static PIC: Mutex<PICDriver> = Mutex::new(PICDriver::new(PIC_OFFSET, PIC_OFFSET + 8));
static TICK_COUNTER: Mutex<u32> = Mutex::new(0);
static BOARD: Mutex<TetrisBoard> = Mutex::new(TetrisBoard::new());
static FALLING: Mutex<Option<FallingTetromino>> = Mutex::new(None);
static LAST_INPUT: Mutex<Option<InputKey>> = Mutex::new(None);
// These values can't be initialized statically,
// so we cheat with `lazy_static`
lazy_static! {
static ref RNG: Mutex<SmallRng> = Mutex::new(SmallRng::seed_from_u64(1337));
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.divide_error.set_handler_fn(divide_handler);
idt.double_fault.set_handler_fn(double_fault_handler);
idt.interrupts[InterruptIndex::Timer.as_idx()].set_handler_fn(timer_handler);
idt.interrupts[InterruptIndex::Keyboard.as_idx()].set_handler_fn(keyboard_handler);
idt
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InputKey {
Left,
Right,
Up,
Down,
}
//
// MARK: interrupts
//
// These functions are called when we receive interrupts.
// This can occur between ANY two instructions---which is
// why we use `without_interrupts` when acquiring locks.
//
// Notice how we do as little work as possible in our
// interrupt handlers. All our business logic goes into
// the main loop.
#[expect(missing_docs)]
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum InterruptIndex {
Timer = PIC_OFFSET,
Keyboard,
}
impl InterruptIndex {
fn as_u8(self) -> u8 {
self as u8
}
fn as_idx(self) -> usize {
usize::from(self.as_u8() - PIC_OFFSET)
}
}
extern "x86-interrupt" fn divide_handler(stack_frame: InterruptStackFrame) {
// Simple interrupt handler, as an example.
// This can be triggered manually using `asm!("int 0")`,
// even if interrupts are disabled.
println!("DIVIDE ERROR {:?}", stack_frame);
}
extern "x86-interrupt" fn keyboard_handler(_stack_frame: InterruptStackFrame) {
{
// Re-seed our rng using user input.
// This is a simple hack that makes our
// "random" tile selector less deterministic.
//
// Getting random seeds from hardware is
// more trouble than its worth.
let mut rng = RNG.lock();
let past: u64 = rng.random();
let tcr = u64::from(*TICK_COUNTER.lock());
*rng = SmallRng::seed_from_u64(past + tcr);
}
'key_block: {
let scancode = unsafe { inb(0x60) };
let key = match scancode {
0x11 => Some(InputKey::Up), // W
0x1E => Some(InputKey::Left), // A
0x1F => Some(InputKey::Down), // S
0x20 => Some(InputKey::Right), // D
// Extended codes
0xE0 => {
let scancode = unsafe { inb(0x60) };
match scancode {
0x48 => Some(InputKey::Up), // Up arrow
0x4B => Some(InputKey::Left), // Left arrow
0x50 => Some(InputKey::Down), // Down arrow
0x4D => Some(InputKey::Right), // Right arrow
_ => break 'key_block, // Ignore unknown codes
}
}
// Ignore unrecognized keycodes.
//
// Note that this does NOT return to None!
// If we do that, "release" keycodes will
// immediately clear "press" keycodes.
_ => break 'key_block,
};
*LAST_INPUT.lock() = key;
}
PIC.lock().send_eoi(false);
}
extern "x86-interrupt" fn timer_handler(_stack_frame: InterruptStackFrame) {
let mut t = TICK_COUNTER.lock();
*t = (*t).wrapping_add(1);
PIC.lock().send_eoi(false);
}
extern "x86-interrupt" fn double_fault_handler(
stack_frame: InterruptStackFrame,
error_code: u32,
) -> ! {
panic!("DOUBLE FAULT (err = 0x{error_code:x}\n{:#?}", stack_frame);
}
//
// MARK: main
//
#[expect(missing_docs)]
#[no_mangle]
pub unsafe extern "C" fn start(thunk10: extern "C" fn()) -> ! {
println!("Entered Rust, serial ready.");
{
// Set vga mode
let mut data = ThunkData::new();
data.eax = 0x13;
data.with(thunk10);
}
{
// Disable cursor
let mut data = ThunkData::new();
data.eax = 0x0100;
data.ecx = 0x3F00;
data.with(thunk10);
}
{
// Initialize IDT
IDT.load();
let mut pic = PIC.lock();
pic.init();
}
// We're ready for interrupts, enable them
sti();
let mut last_t = 0;
loop {
// All locks use `without_interrupts`
// to prevent deadlocks
let t = without_interrupts(|| {
let l = TICK_COUNTER.lock();
let t = *l;
drop(l);
return t;
});
if t == last_t {
continue;
}
last_t = t;
// MARK: input
// Handle user input
without_interrupts(|| {
if let Some(fall) = &mut *FALLING.lock() {
let board = BOARD.lock();
let mut fall_test = fall.clone();
let mut last_input = LAST_INPUT.lock();
match *last_input {
Some(InputKey::Up) => {
fall_test.rotate_cw();
if board.tetromino_valid(&fall_test) {
fall.rotate_cw()
};
}
Some(InputKey::Down) => loop {
fall_test.translate(0, 1);
if board.tetromino_valid(&fall_test) {
fall.translate(0, 1);
} else {
break;
}
},
Some(InputKey::Left) => {
fall_test.translate(-1, 0);
if board.tetromino_valid(&fall_test) {
fall.translate(-1, 0);
};
}
Some(InputKey::Right) => {
fall_test.translate(1, 0);
if board.tetromino_valid(&fall_test) {
fall.translate(1, 0);
};
}
_ => {}
}
// Clear last input, it was handled.
*last_input = None;
}
});
// MARK: update board
// Update board
without_interrupts(|| {
let mut v = VGA.lock();
let mut board = BOARD.lock();
let mut fall = FALLING.lock();
v.swap();
board.draw(&mut v, fall.as_ref());
if let Some(fall_inner) = fall.as_mut() {
if t % 4 == 0 {
let mut fall_test = fall_inner.clone();
fall_test.translate(0, 1);
if board.tetromino_valid(&fall_test) {
fall_inner.translate(0, 1);
} else {
let mut x = None;
core::mem::swap(&mut x, &mut fall);
board.place_tetromino(x.unwrap());
board.collapse();
}
}
} else {
*fall = Some(FallingTetromino::random(5, 1));
if !board.tetromino_valid(fall.as_ref().unwrap()) {
panic!("\nGAME OVER");
}
}
})
}
}

99
tetros/src/os/eflags.rs Normal file
View File

@ -0,0 +1,99 @@
use bitflags::bitflags;
use core::arch::asm;
bitflags! {
/// The EFLAGS register. All bit patterns are valid representations for this type.
///
/// See https://wiki.osdev.org/CPU_Registers_x86#EFLAGS_Register
#[repr(transparent)]
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
pub struct EFlags: u32 {
/// Processor feature identification flag.
///
/// If this flag is modifiable, the CPU supports CPUID.
const ID = 1 << 21;
/// Indicates that an external, maskable interrupt is pending.
///
/// Used when virtual-8086 mode extensions (CR4.VME) or protected-mode virtual
/// interrupts (CR4.PVI) are activated.
const VIRTUAL_INTERRUPT_PENDING = 1 << 20;
/// Virtual image of the INTERRUPT_FLAG bit.
///
/// Used when virtual-8086 mode extensions (CR4.VME) or protected-mode virtual
/// interrupts (CR4.PVI) are activated.
const VIRTUAL_INTERRUPT = 1 << 19;
/// Enable automatic alignment checking if CR0.AM is set. Only works if CPL is 3.
const ALIGNMENT_CHECK = 1 << 18;
/// Enable the virtual-8086 mode.
const VIRTUAL_8086_MODE = 1 << 17;
/// Allows to restart an instruction following an instruction breakpoint.
const RESUME_FLAG = 1 << 16;
/// Used by `iret` in hardware task switch mode to determine if current task is nested.
const NESTED_TASK = 1 << 14;
/// The high bit of the I/O Privilege Level field.
///
/// Specifies the privilege level required for executing I/O address-space instructions.
const IOPL_HIGH = 1 << 13;
/// The low bit of the I/O Privilege Level field.
///
/// Specifies the privilege level required for executing I/O address-space instructions.
const IOPL_LOW = 1 << 12;
/// Set by hardware to indicate that the sign bit of the result of the last signed integer
/// operation differs from the source operands.
const OVERFLOW_FLAG = 1 << 11;
/// Determines the order in which strings are processed.
const DIRECTION_FLAG = 1 << 10;
/// Enable interrupts.
const INTERRUPT_FLAG = 1 << 9;
/// Enable single-step mode for debugging.
const TRAP_FLAG = 1 << 8;
/// Set by hardware if last arithmetic operation resulted in a negative value.
const SIGN_FLAG = 1 << 7;
/// Set by hardware if last arithmetic operation resulted in a zero value.
const ZERO_FLAG = 1 << 6;
/// Set by hardware if last arithmetic operation generated a carry ouf of bit 3 of the
/// result.
const AUXILIARY_CARRY_FLAG = 1 << 4;
/// Set by hardware if last result has an even number of 1 bits (only for some operations).
const PARITY_FLAG = 1 << 2;
/// Set by hardware if last arithmetic operation generated a carry out of the
/// most-significant bit of the result.
const CARRY_FLAG = 1;
}
}
impl EFlags {
/// Read the EFLAGS register
#[inline]
pub fn read() -> EFlags {
EFlags::from_bits_truncate(EFlags::read_raw())
}
#[inline]
fn read_raw() -> u32 {
let r: u32;
unsafe {
asm!("pushfd; pop {0:e}", out(reg) r, options(nomem, preserves_flags));
}
r
}
}

10
tetros/src/os/mod.rs Normal file
View File

@ -0,0 +1,10 @@
mod thunk;
pub use thunk::*;
mod eflags;
pub use eflags::*;
#[macro_use]
pub mod panic;
pub mod util;

34
tetros/src/os/panic.rs Normal file
View File

@ -0,0 +1,34 @@
//! Rust intrinsics for panic handling.
//!
//! These are usually provided by `std`,
//! but we don't have that luxury!
use core::arch::asm;
use core::panic::PanicInfo;
// Use serial println
use crate::println;
#[lang = "eh_personality"]
#[no_mangle]
pub extern "C" fn rust_eh_personality() {}
#[panic_handler]
#[no_mangle]
pub fn rust_begin_unwind(info: &PanicInfo<'_>) -> ! {
unsafe {
println!("TETRIS PANIC:\n{}", info);
loop {
asm!("hlt");
}
}
}
#[no_mangle]
pub extern "C" fn _Unwind_Resume() -> ! {
loop {
unsafe {
asm!("hlt");
}
}
}

46
tetros/src/os/thunk.rs Normal file
View File

@ -0,0 +1,46 @@
use core::ptr;
// Grows downwards
const THUNK_STACK_ADDR: usize = 0x7C00;
#[derive(Clone, Copy, Debug)]
#[repr(C, packed)]
pub struct ThunkData {
pub es: u16,
pub edi: u32,
pub esi: u32,
pub ebp: u32,
pub ebx: u32,
pub edx: u32,
pub ecx: u32,
pub eax: u32,
}
impl ThunkData {
pub fn new() -> Self {
Self {
es: 0,
edi: 0,
esi: 0,
ebp: 0,
ebx: 0,
edx: 0,
ecx: 0,
eax: 0,
}
}
pub unsafe fn save(&self) {
ptr::write((THUNK_STACK_ADDR - 64) as *mut ThunkData, *self);
}
pub unsafe fn load(&mut self) {
*self = ptr::read((THUNK_STACK_ADDR - 64) as *const ThunkData);
}
pub unsafe fn with(&mut self, f: extern "C" fn()) {
self.save();
f();
self.load();
}
}

80
tetros/src/os/util.rs Normal file
View File

@ -0,0 +1,80 @@
use core::arch::asm;
use super::EFlags;
/// Disable interrupts
#[inline]
pub fn cli() {
unsafe {
asm!("cli", options(preserves_flags, nostack));
}
}
/// Enable interrupts
#[inline]
pub fn sti() {
unsafe {
asm!("sti", options(preserves_flags, nostack));
}
}
/// Run the given closure, disabling interrupts before running it (if they aren't already disabled).
/// Afterwards, interrupts are enabling again if they were enabled before.
///
/// This helps us prevent deadlocks, which can occur if
/// an interrupt handler tries to acquire a lock that was
/// locked at the time of the interrupt.
///
/// If you have other `enable` and `disable` calls _within_ the closure, things may not work as expected.
#[inline]
pub fn without_interrupts<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
// true if the interrupt flag is set (i.e. interrupts are enabled)
let saved_intpt_flag = EFlags::read().contains(EFlags::INTERRUPT_FLAG);
// if interrupts are enabled, disable them for now
if saved_intpt_flag {
cli();
}
let ret = f();
if saved_intpt_flag {
sti();
}
ret
}
/// Wraps the `in` instruction
pub unsafe fn inb(port: u32) -> u8 {
let mut out;
asm!(
"in al, dx",
out("al") out,
in("dx") port,
);
return out;
}
/// Wraps the `out` instruction
pub unsafe fn outb(port: u32, value: u8) {
asm!(
"out dx, al",
in("dx") port,
in("al") value,
);
}
/// Get the current value of the CS register
pub fn get_cs() -> u16 {
let segment: u16;
unsafe {
asm!("mov {0:x}, cs", out(reg) segment, options(nomem, nostack, preserves_flags));
}
segment
}

View File

@ -0,0 +1,28 @@
{
"llvm-target": "i686-unknown-none",
"target-endian": "little",
"target-pointer-width": "32",
"target-c-int-width": "32",
"data-layout": "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-i128:128-f64:32:64-f80:32-n8:16:32-S128",
"arch": "x86",
"os": "none",
"env": "",
"vendor": "unknown",
"linker-flavor": "gcc",
"panic-strategy": "abort",
"pre-link-args": {
"gcc": ["-m32", "-nostdlib", "-static"]
},
"features": "-mmx,-sse,-sse2,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2,+soft-float",
"dynamic-linking": false,
"executables": false,
"relocation-model": "static",
"code-model": "large",
"disable-redzone": true,
"frame-pointer": "always",
"exe-suffix": "",
"has-rpath": false,
"no-default-libraries": true,
"position-independent-executables": false,
"tls-model": "global-dynamic"
}

63
tools/scripts/publish.py Normal file
View File

@ -0,0 +1,63 @@
# Publish the output of `build.py`
# as a Gitea package.
from pathlib import Path
import requests
import os
import re
URL = "https://git.betalupi.com"
USER = os.environ["PUBLISH_USER"]
PACKAGE = os.environ["PACKAGE"]
VERSION = os.environ["VERSION"]
AUTH = requests.auth.HTTPBasicAuth(USER, os.environ["PUBLISH_KEY"])
ROOT: Path = Path(os.getcwd())
def log(msg):
print(f"[PUBLISH.PY] {msg}")
log(f"Version is {VERSION}")
log(f"Package is {PACKAGE}")
log(f"Running in {ROOT}")
if not ROOT.is_dir():
log("Root is not a directory, cannot continue")
exit(1)
def del_package():
log(f"Deleting package {PACKAGE}/{VERSION}")
res = requests.delete(
f"{URL}/api/packages/{USER}/generic/{PACKAGE}/{VERSION}",
auth=AUTH,
)
if res.status_code != 204 and res.status_code != 404:
log(f"Deletion failed with code {res.status_code}")
# Delete if already exists
# (important for the `latest` package)
del_package()
def upload(data, target: str):
target = re.sub("[^A-Za-z0-9_. -]+", "", target)
res = requests.put(
f"{URL}/api/packages/{USER}/generic/{PACKAGE}/{VERSION}/{target}",
auth=AUTH,
data=data,
)
if res.status_code != 201:
log(f"Upload failed with code {res.status_code}")
del_package() # Do not keep partial package if upload fails
exit(1)
return f"{URL}/api/packages/{USER}/generic/{PACKAGE}/{VERSION}/{target}"
log("Uploading disk.img")
upload(Path("./build/disk.img").open("rb").read(), "disk.img")

18
tools/scripts/ruff.toml Normal file
View File

@ -0,0 +1,18 @@
exclude = ["venv"]
line-length = 88
indent-width = 4
target-version = "py39"
include = ["scripts/**/*.py"]
[lint]
select = ["E4", "E7", "E9", "F"]
ignore = []
fixable = ["ALL"]
unfixable = []
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[format]
quote-style = "double"
indent-style = "tab"
skip-magic-trailing-comma = false
line-ending = "lf"

8
tools/typos.toml Normal file
View File

@ -0,0 +1,8 @@
[default]
extend-ignore-re = [
# spell:disable-line
"(?Rm)^.*(%|#|//|;)\\s*spell:disable-line$",
# spell:<on|off>
"(?s)(%|#|//|;)\\s*spell:off.*?\\n\\s*(%|#|//)\\s*spell:on",
]