1
0

Compare commits

...

39 Commits

Author SHA1 Message Date
cf60d6734a
Comments
All checks were successful
CI / Typos (push) Successful in 13s
CI / Clippy (push) Successful in 32s
CI / Build (push) Successful in 1m15s
2025-03-04 19:41:22 -08:00
32070e9af4
Write README 2025-03-04 19:41:22 -08:00
1bd27128ae
Dead code 2025-03-04 19:41:22 -08:00
1160c5d143
Remove cpuid checks 2025-03-04 19:41:22 -08:00
f24af6f800
Quick drop and lose condition 2025-03-04 19:41:22 -08:00
f7aa9c6e35
Reorganize 2025-03-04 19:41:22 -08:00
88ed908faa
Add noise to rng 2025-03-04 19:41:22 -08:00
9fc8654e7b
Update TODO 2025-03-04 19:41:22 -08:00
903b2d7ead
Fix arrow keys 2025-03-04 19:41:22 -08:00
bf202a42ba
Minor refactor 2025-03-04 19:41:22 -08:00
1db789889c
Remove unused start() args 2025-03-04 19:41:22 -08:00
e3bdce6065
Minimal interrupts, prevent deadlocks 2025-03-04 19:41:22 -08:00
18113a3f4b
Minor build tweak 2025-03-04 19:41:22 -08:00
8078438d3c
Do not publish hash 2025-03-04 19:41:22 -08:00
921f68a081
Clear filled rows 2025-03-04 19:41:22 -08:00
57a634d4ee
Controls and collisions 2025-03-04 19:41:22 -08:00
23bb3e1153
Fix rotation 2025-03-04 19:41:22 -08:00
428fca40ae
Manifest tweaks 2025-03-04 19:41:22 -08:00
1309ee7489
Random generation 2025-03-04 19:41:16 -08:00
5022c840de
Minor cleanup 2025-03-04 19:41:16 -08:00
ab5f2b200c
TODO 2025-03-04 19:41:16 -08:00
8fb04f9ad0
Added falling tetrominos 2025-03-04 19:41:16 -08:00
a8e8c1fcac
Initialize PIC 2025-03-04 19:41:16 -08:00
4131baa1a5
IDT tweaks 2025-03-04 19:41:16 -08:00
c19af78ae2
Added workflow? 2025-03-04 19:41:16 -08:00
9c515cfab0
Added IDT 2025-03-04 19:41:16 -08:00
db859ddeb8
README 2025-03-04 19:41:16 -08:00
eda305e402
Update toolchain 2025-03-04 19:41:16 -08:00
c03c996a48
Added qemu-gdb target 2025-03-04 19:41:16 -08:00
--global
b8cd346b0a
Reorganize 2025-03-04 19:41:11 -08:00
622f6d1791
Basic tetris board 2025-03-04 19:41:10 -08:00
59df532105
Added VGA graphics 2025-03-04 19:41:10 -08:00
fa2ff36610
Macro cleanup 2025-03-04 19:41:10 -08:00
d6e8bb8859
message 2025-03-04 19:41:10 -08:00
8f71fa0771
Remove logger 2025-03-04 19:41:10 -08:00
fc93078d3f
Format 2025-03-04 19:41:10 -08:00
9cebf49862
Rename crate 2025-03-04 19:41:10 -08:00
db99e90198
Added serial driver 2025-03-04 19:41:10 -08:00
8378376385
Remove everything 2025-03-04 19:41:03 -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
[*.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
.PHONY: default
default: all
default: $(BUILD)/disk.img
# Remove all build files
.PHONY: clean
clean:
rm -drf $(BUILD)
cd bootloader; make clean
cd tetros; cargo clean
# Make everything
.PHONY: all
all: img
#
# MARK: disk
#
# Make bios bootloader
# dd if=./bootloader/build/stage2.bin of=$@ conv=notrunc bs=512 seek=40
# Compile tetros as a library
# (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
img: $(BUILD)/disk.img
$(BUILD)/disk.img:
mkdir -p $(BUILD)
cd bootloader; make
dd if=/dev/zero of=$@ bs=512 count=32
dd if=./bootloader/build/512.bin of=$@ conv=notrunc bs=512
dd if=./bootloader/build/stage2.bin of=$@ conv=notrunc seek=5 bs=512
# Link tetros using custom linker script
BIOS_LD = ./tetros/linkers/x86-unknown-none.ld
$(BUILD)/tetros.elf: $(BUILD)/tetros.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 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-system-i386 \
-d cpu_reset \
@ -33,8 +76,32 @@ qemu: $(BUILD)/disk.img
-smp 1 -m 2048 \
-machine q35 \
-net none \
-serial stdio \
-fda "$<"
# -gdb tcp::26000 \
# -S
# -enable-kvm \
# -cpu host \
# Same as qemu, but with no dependency.
# Used for remote dev, where build box != run box.
.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
struc GDTEntry
struc GDTEntry ; spell:disable-line
.limitl resw 1
.basel resw 1
.basem resb 1
@ -26,21 +26,21 @@ gdt_attr:
.accessed equ 1 << 0
;system
; legacy
.tssAvailabe16 equ 0x1
.tssAvailabe16 equ 0x1 ; spell:disable-line
.ldt equ 0x2
.tssBusy16 equ 0x3
.call16 equ 0x4
.task equ 0x5
.interrupt16 equ 0x6
.trap16 equ 0x7
.tssAvailabe32 equ 0x9
.tssAvailabe32 equ 0x9 ; spell:disable-line
.tssBusy32 equ 0xB
.call32 equ 0xC
.interrupt32 equ 0xE
.trap32 equ 0xF
; long mode
.ldt32 equ 0x2
.tssAvailabe64 equ 0x9
.tssAvailabe64 equ 0x9 ; spell:disable-line
.tssBusy64 equ 0xB
.call64 equ 0xC
.interrupt64 equ 0xE
@ -64,27 +64,6 @@ gdt:
.null equ $ - gdt
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,
; 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
USE16
; provide function for printing in x86 real mode
; print a string and a newline
; CLOBBER
; ax
; Print a string and a newline
;
; Clobbers ax
print_line:
mov al, 13
call print_char
mov al, 10
jmp print_char
; print a string
; IN
; Print a string
;
; Input:
; si: points at zero-terminated String
; CLOBBER
; si, ax
;
; Clobbers si, ax
print:
pushf
cld
@ -30,8 +29,9 @@ print:
popf
ret
; print a character
; IN
; Print a character
;
; Input:
; al: character to print
print_char:
pusha
@ -42,10 +42,11 @@ print_char:
ret
; print a number in hex
; IN
;
; Input:
; bx: the number
; CLOBBER
; al, cx
;
; Clobbers al, cx
print_hex:
mov cx, 4
.lp:

View File

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

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
USE32
@ -6,18 +13,6 @@ thunk:
mov dword [.func], .int10_real
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
.esp: dd 0
.cr0: dd 0
@ -65,18 +60,6 @@ USE16
int 0x10
ret
.int13_real:
int 0x13
ret
.int15_real:
int 0x15
ret
.int16_real:
int 0x16
ret
.pm16:
; set segment selectors to protected mode 16-bit
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]
name = "redox_bootloader"
name = "tetros"
version = "1.0.0"
edition = "2021"
publish = false
[lib]
name = "bootloader"
path = "src/main.rs"
name = "tetros"
path = "src/lib.rs"
crate-type = ["staticlib"]
# Silence rust-analyzer errors
test = false
bench = false
#
# MARK: lints
@ -29,19 +28,13 @@ elided_lifetimes_in_paths = "deny"
absolute_paths_not_starting_with_crate = "deny"
explicit_outlives_requirements = "warn"
unused_crate_dependencies = "warn"
#variant_size_differences = "warn"
redundant_lifetimes = "warn"
missing_docs = "allow"
missing_docs = "warn"
[lints.clippy]
needless_return = "allow"
new_without_default = "allow"
tabs_in_doc_comments = "allow"
# Extra
expect_used = "deny"
#unwrap_used = "deny"
#panic = "deny"
dbg_macro = "deny"
allow_attributes = "deny"
create_dir = "deny"
@ -59,18 +52,15 @@ string_to_string = "deny"
unimplemented = "deny"
use_debug = "deny"
verbose_file_reads = "deny"
#wildcard_enum_match_arm = "deny"
# Pedantic
large_types_passed_by_value = "deny"
match_on_vec_items = "deny"
# Cargo
wildcard_dependencies = "deny"
negative_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
@ -78,21 +68,11 @@ multiple_crate_versions = "deny"
[dependencies]
bitflags = "1.3.2"
linked_list_allocator = "0.10.5"
log = "0.4.17"
redox_syscall = "0.5"
spin = "0.9.5"
bitflags = "2.8.0"
rand = { version = "0.9.0", features = ["small_rng"], default-features = false }
spin = "0.9.8"
uart_16550 = "0.3.2"
[dependencies.redoxfs]
version = "0.6.0"
default-features = false
features = ["force-soft", "log"]
#
# MARK: features
#
[features]
serial_debug = []
live = []
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]

View File

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

View File

@ -1,3 +1,3 @@
[toolchain]
channel = "nightly-2024-05-11"
channel = "nightly-2025-02-01"
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",
]