Initial commit: Schunix as it stands before the SchOS rename

An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.

No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.
This commit is contained in:
Nexoniarz 2026-08-09 13:47:01 +02:00
commit d0f7acf9eb
275 changed files with 51417 additions and 0 deletions

BIN
.#README Normal file

Binary file not shown.

18
.gitattributes vendored Normal file
View file

@ -0,0 +1,18 @@
# Keep LF in the working tree, on Windows included.
#
# Git's autocrlf would otherwise rewrite line endings on checkout, and this
# tree does not tolerate that: the build runs under Git Bash, and a Makefile
# recipe or a .S file with CRLF fails in ways that point nowhere near the
# actual cause.
* text eol=lf
# Binary, never touched.
*.png binary
*.jpg binary
*.ttf binary
*.img binary
*.iso binary
*.tar.gz binary
*.tgz binary
*.zip binary
*.wad binary

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Build output. Everything here is reproducible from `make`.
build/
schunix.img
schunix.iso
*.o
*.d
*.asm
*.sym
# The staged root filesystem. Assembled by the build from cmd/, lib/ and
# ports/ -- tracking it would mean committing a second copy of every binary
# on every build.
sysroot/
# Upstream tarballs, fetched not authored: musl, toybox, mksh, dash, the DOOM
# WADs and usb.ids. 61 MB of other people's release artefacts, all of which
# the build downloads again if they are missing. The patches that matter are
# under ports/*/patches/ and those *are* tracked.
dist/
# Test and screenshot output.
*.log
*.ppm
*.png.tmp

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View file

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1920"
height="1080"
viewBox="0 0 1920 1080"
version="1.1"
id="svg1"
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
sodipodi:docname="Schunix Small Logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#000000"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="0.54276993"
inkscape:cx="982.92107"
inkscape:cy="614.44082"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<linearGradient
id="linearGradient5"
inkscape:collect="always">
<stop
style="stop-color:#c9c9c9;stop-opacity:1;"
offset="0"
id="stop6" />
<stop
style="stop-color:#636363;stop-opacity:1;"
offset="0.5"
id="stop8" />
<stop
style="stop-color:#a2a2a2;stop-opacity:1;"
offset="1"
id="stop7" />
</linearGradient>
<linearGradient
id="linearGradient1"
inkscape:collect="always">
<stop
style="stop-color:#444444;stop-opacity:1;"
offset="0"
id="stop1" />
<stop
style="stop-color:#b2b2b2;stop-opacity:1;"
offset="0.25"
id="stop5" />
<stop
style="stop-color:#909090;stop-opacity:1;"
offset="0.5"
id="stop3" />
<stop
style="stop-color:#4c4c4c;stop-opacity:1;"
offset="0.75"
id="stop4" />
<stop
style="stop-color:#808080;stop-opacity:1;"
offset="1"
id="stop2" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="480.19177"
y1="439.3587"
x2="819.08807"
y2="723.7265"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5"
id="linearGradient7"
x1="354.53467"
y1="585.88361"
x2="968.30945"
y2="585.88361"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="logo"
inkscape:groupmode="layer"
id="layer1"
transform="matrix(0.9235878,0,0,0.9235878,-243.88631,0.4743235)">
<g
id="g8"
inkscape:label="SmallLogo"
transform="matrix(1.8840278,0,0,1.8840278,58.429052,-517.93737)">
<path
id="path1"
style="fill:none;stroke:url(#linearGradient7);stroke-width:35.2052;paint-order:markers fill stroke"
d="m 950.70685,585.88361 c 0,159.76758 -129.51721,289.28479 -289.28479,289.28479 -159.76758,0 -289.28479,-129.51721 -289.28479,-289.28479 0,-159.76758 129.51721,-289.28479 289.28479,-289.28479 109.46795,0 204.73463,60.80288 253.87001,150.47863 22.5773,41.20526 35.41478,88.50653 35.41478,138.80616 z"
sodipodi:nodetypes="ssssss"
inkscape:label="2" />
<path
d="m 614.91693,442.64998 9.0023,9.00229 v 12.86042 l -9.0023,9.00229 h -6.4302 -6.43021 -34.72313 -41.15333 -34.72313 l -25.72083,25.72083 25.72083,25.72084 h 34.72313 34.72312 6.43021 6.43021 l 4.50115,4.50114 4.50114,4.50115 32.15104,32.15104 4.50115,4.50115 4.50115,4.50114 v 6.43021 6.43021 34.72312 34.72313 6.43021 6.43021 l -4.50115,4.50114 -4.50115,4.50115 -32.15104,32.15104 -4.50114,4.50115 -4.50115,4.50114 h -6.43021 -6.43021 -34.72312 -34.72313 -6.43021 -6.4302 l -4.50115,-4.50114 -4.50115,-4.50115 -32.15104,-32.15104 -4.50114,-4.50115 -4.50115,-4.50114 v -12.86042 l 9.00229,-9.00229 h 12.86042 l 4.50115,4.50114 4.50114,4.50115 32.15104,32.15104 h 34.72313 34.72312 l 32.15105,-32.15104 v -34.72313 -34.72312 l -32.15105,-32.15104 h -34.72312 -34.72313 -6.43021 -6.4302 l -4.50115,-4.50115 -4.50115,-4.50114 -32.15104,-32.15105 -4.50114,-4.50114 -4.50115,-4.50115 V 492.8056 l 4.50115,-4.50114 4.50114,-4.50115 32.15104,-32.15104 4.50115,-4.50115 4.50115,-4.50114 h 6.4302 6.43021 34.72313 41.15333 34.72313 6.43021 z m 246.91977,0 9.0023,9.00229 v 12.86042 l -4.50115,4.50114 -4.50115,4.50115 -32.15104,32.15104 -4.50114,4.50115 -4.50115,4.50114 -25.72083,25.72084 25.72083,25.72083 4.50115,4.50115 4.50114,4.50114 32.15104,32.15104 4.50115,4.50115 4.50115,4.50115 v 6.4302 6.43021 34.72313 34.72312 6.43021 6.43021 l -9.0023,9.00229 h -12.86041 l -9.00229,-9.00229 v -6.43021 -6.43021 -34.72312 -34.72313 l -32.15105,-32.15104 -4.50114,-4.50115 -4.50115,-4.50114 -25.72083,-25.72083 -25.72084,25.72083 -4.50114,4.50114 -4.50115,4.50115 -32.15104,32.15104 v 34.72313 34.72312 6.43021 6.43021 l -9.00229,9.00229 h -12.86042 l -9.00229,-9.00229 v -6.43021 -6.43021 -34.72312 -34.72313 -6.43021 -6.4302 l 4.50115,-4.50115 4.50114,-4.50115 32.15104,-32.15104 4.50115,-4.50114 4.50115,-4.50115 25.72083,-25.72083 -25.72083,-25.72084 -4.50115,-4.50114 -4.50115,-4.50115 -32.15104,-32.15104 -4.50114,-4.50115 -4.50115,-4.50114 v -12.86042 l 9.00229,-9.00229 h 12.86042 l 4.50114,4.50114 4.50115,4.50115 32.15104,32.15104 4.50115,4.50115 4.50114,4.50114 25.72084,25.72084 25.72083,-25.72084 4.50115,-4.50114 4.50114,-4.50115 32.15105,-32.15104 4.50114,-4.50115 4.50115,-4.50114 z"
id="text1"
style="font-size:370.38px;font-family:miracode;-inkscape-font-specification:miracode;text-align:center;text-anchor:middle;fill:url(#linearGradient2);stroke-width:11.801;paint-order:markers fill stroke"
aria-label="SX"
inkscape:label="1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

187
LICENSE Normal file
View file

@ -0,0 +1,187 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship made available under
the License, as indicated by a copyright notice that is included in
or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean, as submitted to the Licensor for inclusion
in the Work by the copyright owner or by an individual or Legal Entity
authorized to submit on behalf of the copyright owner. For the purposes
of this definition, "contribution" means any work of authorship,
including the original version of the Work and any modifications or
additions to that Work or Derivative Works of the Work.
"Contributor" shall mean Licensor and any Legal Entity on behalf of
whom a Contribution has been received by the Licensor and included
within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by the combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a cross-claim
or counterclaim in a lawsuit) alleging that the Work or any
Contribution embodied within the Work constitutes patent or
contributory patent infringement, then any patent licenses granted to
You under this License for that Work shall terminate as of the date
such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, You must include a readable copy of the
attribution notices contained within such NOTICE file, in
at least one of the following places: within a NOTICE text
file distributed as part of the Derivative Works; within
the Source form or documentation, if provided along with the
Derivative Works; or, within a display generated by the
Derivative Works, if and wherever such third-party notices
normally appear. The contents of the NOTICE file are for
informational purposes only and do not modify the License.
You may add Your own attribution notices within Derivative
Works that You distribute, alongside or in addition to the
NOTICE text from the Work, provided that such additional
attribution notices cannot be construed as modifying the License.
You may add Your own license statement for Your modifications and
may provide additional grant of rights to use, copy, modify, and
distribute those modifications as a separate work, subject to the
terms and conditions of this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any conditions of TITLE,
NONINFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
You are solely responsible for determining the appropriateness of
using or reproducing the Work and assume any risks associated with
Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or exemplary damages of any character arising as a
result of this License or out of the use or inability to use the
Work (even if such Contributor has been advised of the possibility
of such damages).
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may offer such
obligations only on Your behalf and on Your sole responsibility,
not on behalf of any other Contributor, and only if You agree to
indemnify, defend, and hold each Contributor harmless for any
liability incurred by, or claims asserted against, such Contributor
by reason of your accepting any warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2026 Nexoniarz
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
NOTE: The kernel (kernel/) is derived from xv6 (MIT License, Copyright
2006-2019 Frans Kaashoek, Robert Morris, Russ Cox, M.I.T. CSAIL).
The MIT license notice is preserved in LICENSE.MIT and must not be
removed. Third-party software redistributed in a built image -- musl,
toybox and mksh, all under ports/ -- keeps its own license; see NOTICE.
Which outside code may be copied into Schunix and which may not -- the
BSD/GPL rules, the NetBSD advertising clause, and what is deliberately
never shipped -- is written up in docs/licensing.md. Read it before
copying anyone else's code in.

29
LICENSE.MIT Normal file
View file

@ -0,0 +1,29 @@
The Schunix kernel is a derivative work of xv6, which carries the licence
below. The MIT licence requires this notice be preserved in all copies and
substantial portions of the software, so this file must survive any future
change to the Schunix licence. See NOTICE and docs/licensing.md.
--------------------------------------------------------------------------------
The xv6 software is:
Copyright (c) 2006-2019 Frans Kaashoek, Robert Morris, Russ Cox,
Massachusetts Institute of Technology
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

806
Makefile Normal file
View file

@ -0,0 +1,806 @@
BUILD = build
SYSROOT = sysroot
# -----------------------------------------------------------------------
# Parallelism
#
# Nothing here is order-dependent beyond what the rules already declare, so
# there is no reason to compile one file at a time. Override with `make -j1`
# when you want readable output from a failing build.
# -----------------------------------------------------------------------
NPROC := $(shell nproc 2>/dev/null || echo 4)
MAKEFLAGS += -j$(NPROC)
# Kernel object files — organized by subsystem
KERN_ARCH = $(addprefix $(BUILD)/kernel/arch/x86_64/, \
acpi.o e820.o entry.o trapasm.o swtch.o \
trap.o vm.o lapic.o ioapic.o mp.o)
KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \
main.o kalloc.o dma.o spinlock.o sleeplock.o string.o console.o \
panic.o uaccess.o qr.o time.o)
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
bio.o cdev.o device.o fb.o ide.o input.o irq.o kbd.o mouse.o \
pci.o text.o ttf.o uart.o vga.o ramdisk.o procfs.o)
# Drivers for hardware that has to be *found* rather than assumed. Anything
# discovered by walking a bus belongs here; the fixed-address legacy devices
# (console, keyboard, PIT, the IDE ports) stay in dev/.
#
# drivers/<class>/<driver>/ -- one directory per driver, so a driver that grows
# a register header, a second source file or a firmware blob has somewhere to
# put it. Classes match include/device.h's DEVCLASS_*. Everything flat was
# already eight files with four called usb_something, and each driver added made
# the directory harder to read. The compile rule uses `mkdir -p $(dir $@)`, so
# the depth costs nothing.
KERN_DRIVERS = $(BUILD)/kernel/drivers/registry.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/vga/vga_pci.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/virtio_gpu/virtio_gpu.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/net/e1000/e1000.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/bus/xhci/xhci.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/core/usb.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hid/usb_hid.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hub/usb_hub.o
KERN_FS = $(addprefix $(BUILD)/kernel/fs/, \
exec.o file.o fs.o log.o pipe.o procfs.o)
# SchFS. Every one of these is the very same source the host tools compile, so
# the kernel and mkschfs cannot disagree about the on-disk format: there is one
# implementation, found and fixed on the host where a test takes a second.
KERN_FS += $(addprefix $(BUILD)/kernel/fs/schfs/, \
btree.o space.o vol.o obj.o objw.o kmount.o vfs.o)
# Sockets. Unix domain only for now; an IP stack becomes more files here
# rather than a reshaping of what is already written.
KERN_NET = $(addprefix $(BUILD)/kernel/net/, \
unix.o)
KERN_PROC = $(addprefix $(BUILD)/kernel/proc/, \
proc.o)
KERN_SYS = $(addprefix $(BUILD)/kernel/sys/, \
syscall.o sys_proc.o sys_fs.o sys_mem.o sys_net.o \
sys_time.o sys_sig.o sys_compat.o sys_poll.o linux_abi.o)
# vectors.o lives alongside trapasm in the arch dir
KERN_ARCH += $(BUILD)/kernel/arch/x86_64/vectors.o
KERNOBJS = $(KERN_ARCH) $(KERN_CORE) $(KERN_DEV) $(KERN_DRIVERS) \
$(KERN_FS) $(KERN_NET) $(KERN_PROC) $(KERN_SYS)
# entry.o must be first in link order; remove it from KERNOBJS for the rule
KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS))
# -----------------------------------------------------------------------
# First-party userland
#
# lib/ startup glue + syscall stubs every cmd/ program links against
# cmd/ one directory per program: cmd/<name>/main.c -> sysroot/bin/<name>
#
# To add a program: mkdir cmd/foo, write cmd/foo/main.c, add "foo" to CMDS.
# Third-party software does NOT go here — see ports/.
# -----------------------------------------------------------------------
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o)
CMDS = init fbinfo shmtest socktest inputtest
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
# -----------------------------------------------------------------------
# Toolchain
# -----------------------------------------------------------------------
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# Windows, under Git Bash or MSYS2 — WINHOST.
#
# Building natively on Windows rather than in WSL, because WSL2 is a real Linux
# kernel in a Hyper-V VM and so hard-requires virtualisation; turning that on
# puts the Hyper-V hypervisor underneath the whole machine and demotes VMware
# to running nested on top of it, which is a real and permanent cost to pay for
# a build environment.
#
# The usual answer is a cross binutils+gcc, which is a day of work that then
# rots. clang does not need any of it: it is a cross-compiler by construction,
# so one ordinary LLVM install targets both the bare-metal kernel and Linux
# userland, and llvm-objcopy/objdump stand in for binutils. Everything else the
# build shells out to — perl, sed, dd, sha256sum — comes with Git Bash.
ifneq (,$(filter MINGW% MSYS% CYGWIN%,$(UNAME_S)))
WINHOST := 1
endif
ifeq ($(UNAME_S),Linux)
ifeq ($(UNAME_P),aarch64)
TOOLPREFIX = x86_64-linux-gnu-
else
TOOLPREFIX =
endif
else ifeq ($(UNAME_S),Darwin)
TOOLPREFIX = x86_64-elf-
endif
QEMU = qemu-system-x86_64
# Display for every QEMU target.
#
# virtio-gpu is what the display class binds, and what a compositor should be
# developed against: presenting is explicit, so the host never shows a
# half-drawn frame.
#
# stdvga stays alongside it and is not optional. The bootloader draws the splash
# through VBE in 16-bit real mode and there is no virtio path that early, so a
# machine with only virtio-gpu boots blind until the kernel is up. Both devices:
# virtio-gpu wins the display class, stdvga carries the splash and text console.
QEMU_DISPLAY = -vga std -device virtio-gpu-pci
ifdef WINHOST
# An explicit --target is what makes clang emit ELF instead of the COFF it
# would default to on this host. Without it every object is silently the
# wrong format and the first thing to complain is the linker.
CC = clang --target=x86_64-elf
CC32 = clang --target=i386-elf
LD = ld.lld
OBJCOPY = llvm-objcopy
OBJDUMP = llvm-objdump
# ld.lld has no --no-warn-rwx-segments: it does not emit that warning, so
# there is nothing to suppress and the flag is a hard error.
LDFLAGS = -m elf_x86_64 -nostdlib
# The bootloader, the AP trampoline, initcode and the cmd/ programs are all
# linked at addresses below one megabyte. GNU ld places a section wherever
# -Ttext says; lld first checks it against a default image base of 0x400000
# and refuses, because for a hosted program that would be a bug. Saying the
# base is zero is how you tell it this is not a hosted program.
LD_LOWBASE = --image-base=0
# Host tools (mkfs, mkschfs, the SchFS suites) are ordinary Windows programs.
# They must be built with mingw-w64 rather than clang's default MSVC target:
# tools/mkfs.c includes <unistd.h> and <dirent.h>, and the Windows SDK has
# neither.
HOSTCC = gcc
# mingw-w64's gcc warns about things glibc's does not (notably %lld/%zu in
# printf against msvcrt). Those warnings are about the host build only and
# say nothing about the image, so they must not stop it.
HOSTWARN = -Wall
# MinGW gcc appends .exe when the -o name has no extension, so a rule
# whose target lacks it is never considered built -- mkfs was recompiled on
# every single invocation of make, forever, because of this.
EXE = .exe
# `python3` is not on PATH from the python.org installer; `python` is.
PYTHON = python
# Compiler for ports/, which build *Linux userland* against musl -- a third
# target, distinct from both the kernel (bare-metal ELF) and the host tools
# (Windows PE). mingw's gcc cannot do it: it targets Windows, so linking it
# against musl gives undefined `stderr` and `__errno_location`.
#
# -nostdlib with the crt files named explicitly, because clang otherwise
# reaches for gcc's crtbeginT.o and libgcc, which do not exist on this host.
MUSLPREFIX = $(abspath $(BUILD))/ports/musl/prefix
PORTCC = clang --target=x86_64-unknown-linux-musl -fuse-ld=lld
PORTCRT = -nostdlib $(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o
PORTEND = $(MUSLPREFIX)/lib/crtn.o
else
CC = $(TOOLPREFIX)gcc
CC32 = $(CC) -m32
LD = $(TOOLPREFIX)ld
OBJCOPY = $(TOOLPREFIX)objcopy
OBJDUMP = $(TOOLPREFIX)objdump
LDFLAGS = -m elf_x86_64 -nostdlib --no-warn-rwx-segments
HOSTCC = gcc
HOSTWARN = -Werror -Wall
EXE =
PYTHON = python3
LD_LOWBASE =
PORTCC = gcc
PORTCRT =
PORTEND =
endif
# Host tools are generated output and live under build/, per docs/layout.md.
# $(EXE) matters: MinGW appends it, and a rule whose target lacks it never
# looks satisfied.
MKFS = $(BUILD)/tools/mkfs$(EXE)
MKSCHFS = $(BUILD)/tools/mkschfs$(EXE)
SCHFS_TEST = $(BUILD)/tools/schfs_test$(EXE)
SCHFS_OBJT = $(BUILD)/tools/schfs_objtest$(EXE)
SCHFS_FSCK = $(BUILD)/tools/schfs_fsck$(EXE)
OPT ?= -O0
# -ftls-model=local-exec: the kernel is *the* executable, never a shared
# object, so its __thread variables (cpu, proc) are always at a fixed negative
# offset from the thread pointer. Saying so lets the compiler emit a direct
# `movq %rcx, %fs:-0x10` and never materialise the thread pointer as a value.
#
# GCC picked local-exec on its own for this code; clang did not, and generated
# `movq %fs:0x0, %rax; addq $-0x10, %rax; ...` instead -- which reads the TCB
# self-pointer at %fs:0, a word this kernel does not maintain and something
# else has apparently always been scribbling on. Harmless for fifteen years
# because nothing ever read it; instant triple fault the moment a compiler did.
XFLAGS = -m64 -DX64 -mcmodel=large -mtls-direct-seg-refs -mno-red-zone
XFLAGS += -ftls-model=local-exec
# Probed once, with :=, not on every expansion of CFLAGS. CFLAGS is a
# recursively-expanded variable, so a $(shell) appended to it re-ran the probe
# on every single compile -- one extra gcc process per object file, for an
# answer that cannot change during a build.
NO_SSP := $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
CFLAGS = -fno-pic -static -fno-builtin -fno-strict-aliasing -Wall -MD -ggdb -fno-omit-frame-pointer
CFLAGS += -ffreestanding -fno-common -nostdlib -I./include $(XFLAGS) $(OPT)
CFLAGS += $(NO_SSP) $(CFLAGS_EXTRA)
# sys/ also needs to find table.inc via a relative include
CFLAGS_SYS = $(CFLAGS) -I./kernel/sys
# -----------------------------------------------------------------------
# Ramdisk root (opt-in): make RAMROOT=1
#
# Links a complete SCHFS image into the kernel and boots from it instead of
# the IDE disk, so the physical disk can be reformatted from a running system.
# Off by default because it adds RAMDISK_BLOCKS KB to the kernel, and stage2
# loads the kernel by PIO one sector at a time -- a 6 MB kernel takes visibly
# longer to boot than a 350 KB one.
# -----------------------------------------------------------------------
RAMDISK_BLOCKS ?= 6000
# Inodes for the ramdisk root. The inode table is preallocated, so mkfs's
# default of 12000 cost 1501 of those 6000 blocks -- a quarter of the image, to
# describe about 260 files. 2000 is still an order of magnitude of headroom and
# costs 251 blocks.
RAMDISK_NINODES ?= 2000
# Kept out of the ramdisk root.
#
# Empty right now, and the machinery is kept anyway, because this is a licensing
# rule before it is a size one: the ramdisk image is linked *into the kernel
# binary*, so anything staged in sysroot/ when it is built becomes part of a
# distributable artifact. Anything that arrives in sysroot/ under a licence that
# cannot be combined with Apache-2.0, or that simply is not ours to redistribute,
# belongs on this list rather than in someone's memory.
#
# This is not hypothetical: a 33 MB test payload once landed here and overflowed
# the 6 MB image, and mkfs's "increase FSSIZE" message then pointed at a constant
# that had nothing to do with it.
# usb.ids is 730 KB of a 6 MB image -- 12% of the ramdisk root -- and nothing
# needs it to boot. It is a lookup table for a userspace tool, so it stays on
# the disk image and out of the kernel binary.
RAMDISK_EXCLUDE = usr/share/usb.ids
RAMDISK_EXCLUDE_ARGS = $(foreach p,$(RAMDISK_EXCLUDE),--exclude $(p))
ifdef RAMROOT
CFLAGS += -DRAMDISK_ROOT
RAMDISK_OBJ = $(BUILD)/kernel/ramdisk_obj
else
RAMDISK_OBJ =
endif
# -----------------------------------------------------------------------
# Top-level targets
# -----------------------------------------------------------------------
all: schunix.img
schunix.img: $(BUILD)/boot/bootblock $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel $(BUILD)/schfs.img
dd if=/dev/zero of=schunix.img count=10000
dd if=$(BUILD)/boot/bootblock of=schunix.img conv=notrunc
dd if=$(BUILD)/boot/stage2 of=schunix.img seek=1 conv=notrunc
# seek is 1 + STAGE2_SECTORS, not a literal. It was 25 against a 24-sector
# stage2; raising STAGE2_SECTORS for the boot logo without touching it wrote
# the kernel straight over stage2's own tail.
dd if=$(BUILD)/kernel/kernel of=schunix.img seek=$$(( 1 + $(STAGE2_SECTORS) )) conv=notrunc
kernel: $(BUILD)/kernel/kernel
# Build the ramdisk-root variant. This is a target rather than just
# "make RAMROOT=1" because toggling RAMROOT changes CFLAGS (-DRAMDISK_ROOT),
# and make has no way to notice that: objects compiled for one setting would be
# silently reused for the other, leaving ROOTDEV pointing at the wrong device.
# Dropping the compiled objects first makes the switch always correct.
ramroot:
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd
$(MAKE) RAMROOT=1
cmd: $(CMDBINS)
ports:
$(MAKE) -C ports
# -----------------------------------------------------------------------
# Bootable ISO
#
# Always a ramdisk-root build. An ISO that needed a second disk to find its
# root filesystem would not be an install medium, it would be a puzzle -- and
# read-only media cannot host a writable root anyway. Same object-wiping
# preamble as the `ramroot` target, for the same reason: RAMROOT changes
# CFLAGS and make cannot see that.
# -----------------------------------------------------------------------
iso:
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd
$(MAKE) RAMROOT=1 schunix.iso
schunix.iso: $(BUILD)/boot/cdboot $(BUILD)/boot/stage2 $(BUILD)/kernel/kernel
STAGE2_SECTORS=$(STAGE2_SECTORS) sh tools/mkiso.sh $@
# Boot the ISO the way a machine booting off a disc would: no hard disk at
# all, so a kernel that still secretly depends on one fails here rather than
# in someone's optical drive.
qemu-iso: schunix.iso
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) -cpu qemu64,+rdtscp \
-cdrom schunix.iso -boot d \
-m 1G -smp 2 -machine pc-i440fx-9.2
# -----------------------------------------------------------------------
# Bootloader — two stages. Stage 1 (bootblock) is the actual 512-byte MBR
# sector: BIOS-imposed hard limit, so it only loads stage 2 off disk and
# jumps to it. Stage 2 does VBE/A20/protected-mode setup, draws the boot
# splash, and loads the kernel — not size-constrained the same way, just
# padded out to a fixed STAGE2_SECTORS*512 bytes so the kernel's starting
# sector on disk is predictable (kept in sync with STAGE2_SECTORS in
# boot/bootasm.S and boot/stage2main.c).
# -----------------------------------------------------------------------
# STAGE2_SECTORS used to be written out three times -- here, in boot/bootasm.S
# and in boot/stage2main.c -- and docs/layout.md carried a warning about keeping
# them in step. They are now all fed from this one variable via -D, so the
# warning describes a problem that can no longer happen. Both C/asm files keep
# an #ifndef fallback purely so they still read sensibly on their own.
#
# The ceiling is memory, not disk: stage2 is loaded at 0x7E00 and the multiboot
# kernel-search buffer begins at 0x10000, so 65 sectors is the hard maximum.
# stage2main.c static-asserts this. padstage2.pl fails the build if the compiled
# binary overflows the budget, rather than truncating it -- a short stage2 would
# shift the kernel's start sector and fail in a way that looks nothing like its
# cause.
STAGE2_SECTORS = 48
STAGE2_DEF = -DSTAGE2_SECTORS=$(STAGE2_SECTORS)
# Byte budget is computed in the recipe with shell arithmetic, not $(shell expr
# ... \* 512): make hands the backslash through untouched, sh then treats the
# `*` as a glob, and expr gets a directory listing. It "worked" to the extent
# that make carried on and padstage2.pl received nonsense.
STAGE2_BYTES = $$(( $(STAGE2_SECTORS) * 512 ))
$(BUILD)/boot/bootblock: boot/bootasm.S
@mkdir -p $(BUILD)/boot
$(CC32) $(STAGE2_DEF) -fno-builtin -fno-pic -nostdinc -I./include -c boot/bootasm.S -o $(BUILD)/boot/bootasm.o
$(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e start -Ttext 0x7C00 \
-o $(BUILD)/boot/bootblocktmp.o $(BUILD)/boot/bootasm.o
$(OBJCOPY) -S -O binary -j .text $(BUILD)/boot/bootblocktmp.o $(BUILD)/boot/bootblock
./tools/sign.pl $(BUILD)/boot/bootblock
# Stage 1 for optical media. Same job as bootblock, far less of it: El Torito
# has already loaded stage2 alongside this sector by the time it runs, so it
# only records that we came from a CD and jumps. See boot/cdboot.S.
$(BUILD)/boot/cdboot: boot/cdboot.S
@mkdir -p $(BUILD)/boot
$(CC32) -fno-builtin -fno-pic -nostdinc -I./include -c boot/cdboot.S -o $(BUILD)/boot/cdbootasm.o
$(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e start -Ttext 0x7C00 \
-o $(BUILD)/boot/cdboottmp.o $(BUILD)/boot/cdbootasm.o
$(OBJCOPY) -S -O binary -j .text $(BUILD)/boot/cdboottmp.o $@
./tools/sign.pl $@
$(BUILD)/boot/stage2: boot/stage2.S boot/stage2main.c include/logo.h
@mkdir -p $(BUILD)/boot
$(CC32) $(STAGE2_DEF) -fno-builtin -fno-pic -nostdinc -I./include -c boot/stage2.S -o $(BUILD)/boot/stage2asm.o
$(CC32) $(STAGE2_DEF) -fno-builtin -fno-pic -O -nostdinc -I./include -c boot/stage2main.c -o $(BUILD)/boot/stage2main.o
$(LD) -m elf_i386 -nostdlib $(LD_LOWBASE) -n -N -e stage2start -Ttext 0x7E00 \
-o $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2asm.o $(BUILD)/boot/stage2main.o
$(OBJCOPY) -S -O binary -j .text -j .rodata -j .data $(BUILD)/boot/stage2tmp.o $(BUILD)/boot/stage2
./tools/padstage2.pl $(BUILD)/boot/stage2 $(STAGE2_BYTES)
# -----------------------------------------------------------------------
# Kernel binary
# -----------------------------------------------------------------------
$(BUILD)/kernel/kernel: $(KERNOBJS) $(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj $(RAMDISK_OBJ) kernel/kernel.ld
@mkdir -p $(BUILD)/kernel
$(LD) $(LDFLAGS) -T kernel/kernel.ld -o $@ \
$(BUILD)/kernel/arch/x86_64/entry.o $(KERN_NOENTRY) \
$(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj $(RAMDISK_OBJ)
$(OBJDUMP) -S $@ > $(BUILD)/kernel/kernel.asm
# The ramdisk root image, and the object wrapping it for the linker. Built from
# the same staged sysroot as the main image, just capped at RAMDISK_BLOCKS --
# mkfs's content dedup means the whole userland still fits in a few MB.
$(BUILD)/fs-ram.img: $(MKFS) $(BUILD)/sysroot.stamp
@mkdir -p $(BUILD)
@rm -f $@
$(MKFS) $@ --size $(RAMDISK_BLOCKS) --ninodes $(RAMDISK_NINODES) \
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(SYSROOT)
$(BUILD)/kernel/ramdisk_obj: $(BUILD)/fs-ram.img
@mkdir -p $(BUILD)/kernel
$(LD) -r -b binary -m elf_x86_64 -o $@ $<
$(OBJCOPY) \
--redefine-sym _binary_build_fs_ram_img_start=_binary_ramdisk_start \
--redefine-sym _binary_build_fs_ram_img_end=_binary_ramdisk_end \
--redefine-sym _binary_build_fs_ram_img_size=_binary_ramdisk_size \
$@
# AP startup blob
$(BUILD)/kernel/entryother: kernel/arch/x86_64/entryother.S
@mkdir -p $(BUILD)/kernel
$(CC) $(CFLAGS) -fno-pic -nostdinc -I./include -c $< -o $(BUILD)/kernel/entryother.o
$(LD) $(LDFLAGS) $(LD_LOWBASE) -e start -Ttext 0x7000 -o $(BUILD)/kernel/entryothertmp.o $(BUILD)/kernel/entryother.o
$(OBJCOPY) -S -O binary -j .text $(BUILD)/kernel/entryothertmp.o $@
# NOTE: ld -r -b binary derives its symbol names from the *path* of the input
# file, so these --redefine-sym pairs must be kept in sync with the build
# directory layout above. Move the file, and the left-hand names change.
$(BUILD)/kernel/entryother_obj: $(BUILD)/kernel/entryother
$(LD) -r -b binary -m elf_x86_64 -o $@ $<
$(OBJCOPY) \
--redefine-sym _binary_build_kernel_entryother_start=_binary_entryother_start \
--redefine-sym _binary_build_kernel_entryother_end=_binary_entryother_end \
--redefine-sym _binary_build_kernel_entryother_size=_binary_entryother_size \
$@
# Init stub (first user-space code, embedded in the kernel image)
$(BUILD)/lib/initcode: lib/initcode.S
@mkdir -p $(BUILD)/lib
$(CC) $(CFLAGS) -fno-pic -nostdinc -I./include -c $< -o $(BUILD)/lib/initcode.o
$(LD) -m elf_x86_64 -nostdlib $(LD_LOWBASE) -N -e start -Ttext 0 -o $(BUILD)/lib/initcodetmp.o $(BUILD)/lib/initcode.o
$(OBJCOPY) -S -O binary $(BUILD)/lib/initcodetmp.o $@
$(BUILD)/kernel/initcode_obj: $(BUILD)/lib/initcode
@mkdir -p $(BUILD)/kernel
$(LD) -r -b binary -m elf_x86_64 -o $@ $<
$(OBJCOPY) \
--redefine-sym _binary_build_lib_initcode_start=_binary_initcode_start \
--redefine-sym _binary_build_lib_initcode_end=_binary_initcode_end \
--redefine-sym _binary_build_lib_initcode_size=_binary_initcode_size \
$@
# -----------------------------------------------------------------------
# vectors.S — generated from perl script
# -----------------------------------------------------------------------
kernel/arch/x86_64/vectors.S: tools/vectors.pl
perl tools/vectors.pl > $@
# -----------------------------------------------------------------------
# Compile rules for kernel/
# -----------------------------------------------------------------------
$(BUILD)/kernel/arch/x86_64/%.o: kernel/arch/x86_64/%.c
@mkdir -p $(BUILD)/kernel/arch/x86_64
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/arch/x86_64/%.o: kernel/arch/x86_64/%.S
@mkdir -p $(BUILD)/kernel/arch/x86_64
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/core/%.o: kernel/core/%.c
@mkdir -p $(BUILD)/kernel/core
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/dev/%.o: kernel/dev/%.c
@mkdir -p $(BUILD)/kernel/dev
$(CC) $(CFLAGS) -c $< -o $@
# `$(dir $@)` rather than a fixed path: drivers are grouped into one directory
# per device class (gpu/, net/, bus/, usb/), so the output directory varies with
# the target and a hardcoded mkdir only creates the top one.
$(BUILD)/kernel/drivers/%.o: kernel/drivers/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/fs/%.o: kernel/fs/%.c
@mkdir -p $(BUILD)/kernel/fs
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/fs/schfs/%.o: kernel/fs/schfs/%.c
@mkdir -p $(BUILD)/kernel/fs/schfs
$(CC) $(CFLAGS) -I./kernel/fs/schfs -c $< -o $@
$(BUILD)/kernel/net/%.o: kernel/net/%.c
@mkdir -p $(BUILD)/kernel/net
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/kernel/proc/%.o: kernel/proc/%.c
@mkdir -p $(BUILD)/kernel/proc
$(CC) $(CFLAGS) -c $< -o $@
# sys/ gets an extra -I so syscall.c can do #include "table.inc"
$(BUILD)/kernel/sys/%.o: kernel/sys/%.c
@mkdir -p $(BUILD)/kernel/sys
$(CC) $(CFLAGS_SYS) -c $< -o $@
# -----------------------------------------------------------------------
# First-party userland: lib/ + cmd/
# -----------------------------------------------------------------------
$(BUILD)/lib/%.o: lib/%.c
@mkdir -p $(BUILD)/lib
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/lib/%.o: lib/%.S
@mkdir -p $(BUILD)/lib
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/cmd/%: cmd/%/main.c $(ULIB)
@mkdir -p $(BUILD)/cmd
$(CC) $(CFLAGS) -c $< -o $(BUILD)/cmd/$*.o
$(LD) $(LDFLAGS) $(LD_LOWBASE) -e _start -Ttext 0x4000 -o $@ $(BUILD)/cmd/$*.o $(ULIB)
$(OBJCOPY) -S $@
$(SYSROOT)/bin/%: $(BUILD)/cmd/% | $(SYSROOT)/bin
cp $< $@
chmod 755 $@
$(SYSROOT)/bin:
mkdir -p $@
# -----------------------------------------------------------------------
# Filesystem image
# -----------------------------------------------------------------------
# -----------------------------------------------------------------------
# SchFS B-tree torture test (host program).
#
# The B-tree underpins every record type in SchFS, so it is developed and
# tested here -- against a reference model, with simulated crashes -- before
# it is linked into the kernel. Run it after any change to
# kernel/fs/schfs/btree.c.
# -----------------------------------------------------------------------
schfs-test: $(SCHFS_TEST)
$(SCHFS_TEST)
# Use ASan/UBSan when the runtime libraries are actually installed; they catch
# exactly the pointer-arithmetic mistakes this code is most exposed to, but not
# every machine has libasan, so fall back rather than failing the build.
SAN := $(shell echo 'int main(void){return 0;}' > /tmp/.santest.c 2>/dev/null && \
gcc -fsanitize=address,undefined -o /tmp/.santest /tmp/.santest.c \
>/dev/null 2>&1 && echo -fsanitize=address,undefined)
SCHFS_SRC = kernel/fs/schfs/btree.c kernel/fs/schfs/btree.h \
kernel/fs/schfs/space.c kernel/fs/schfs/vol.c \
kernel/fs/schfs/obj.c kernel/fs/schfs/objw.c \
kernel/fs/schfs/schfs_int.h include/schfs.h
# -iquote, not -I: include/ has its own errno.h, stat.h, fcntl.h and types.h,
# and -I would put them ahead of the system ones for a host program that needs
# the real thing. -iquote applies only to "quoted" includes, so btree.h still
# finds schfs.h while <errno.h> still means the C library's.
HOSTCFLAGS = -Wall -Wextra -O1 -g $(SAN) -iquote ./include
$(SCHFS_TEST): tools/schfs_test.c $(SCHFS_SRC)
@mkdir -p $(BUILD)/tools
@mkdir -p $(BUILD)
$(HOSTCC) $(HOSTCFLAGS) -o $@ tools/schfs_test.c
# The object layer -- files, directories, and whether deleting things gives the
# space back. Runs against a memory-backed volume, so a full create/write/
# delete/remount cycle takes milliseconds instead of a rebuild and a boot.
$(SCHFS_OBJT): tools/schfs_objtest.c $(SCHFS_SRC)
@mkdir -p $(BUILD)/tools
@mkdir -p $(BUILD)
$(HOSTCC) $(HOSTCFLAGS) -o $@ tools/schfs_objtest.c
schfs-objtest: $(SCHFS_OBJT)
$(SCHFS_OBJT)
# The image writer and checker. Both #include the real filesystem sources, so
# there is exactly one implementation of the on-disk format: a host tool and the
# kernel cannot drift apart because there is nothing to drift from.
$(MKSCHFS): tools/mkschfs.c $(SCHFS_SRC)
@mkdir -p $(BUILD)/tools
@mkdir -p $(BUILD)
$(HOSTCC) $(HOSTCFLAGS) -o $@ tools/mkschfs.c
$(SCHFS_FSCK): tools/schfs_fsck.c $(SCHFS_SRC)
@mkdir -p $(BUILD)/tools
@mkdir -p $(BUILD)
$(HOSTCC) $(HOSTCFLAGS) -o $@ tools/schfs_fsck.c
mkschfs: $(MKSCHFS)
schfs-fsck: $(SCHFS_FSCK)
# End-to-end filesystem test: build an image from a directory tree, read it
# back with schfs_fsck, and diff the result against the original.
#
# The oracle here is the host's own filesystem, which knows nothing about SchFS.
# schfs-test proves the B-tree matches a model written alongside it; this proves
# the *filesystem* matches a tree that exists independently of it, so a writer
# and reader that agree on something wrong are still caught.
schfs-image-test: $(MKSCHFS) $(SCHFS_FSCK)
@sh tools/schfs_roundtrip.sh $(MKSCHFS) $(SCHFS_FSCK)
# Every SchFS suite. Run this after touching anything under kernel/fs/schfs.
schfs-check: schfs-test schfs-objtest schfs-image-test
# Crash the kernel on purpose and photograph the result.
#
# The crash screen is the one part of the system you cannot look at by using
# the system, which is exactly why it rots. This makes "does it still render"
# a question with a thirty-second answer. Needs a ramroot build first.
crashshot:
pwsh -File tools/crashshot.ps1
# Read the QR code back off a captured crash screen. End-to-end on purpose: it
# reads the pixels that were painted, so payload construction in panic context,
# the encoder, the module scaling and the quiet zone are all covered by whether
# text comes back. Run `make crashshot` first. Needs the same Python packages
# as `qrcheck`.
crashqr:
$(PYTHON) tools/crashqr.py
$(MKFS): tools/mkfs.c include/fs.h include/stat.h include/param.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -I. -o $(MKFS) tools/mkfs.c
# Stage everything a running system needs into sysroot/, then image it.
#
# ports/ is built via its own Makefile rather than per-port from here, because
# that is where the inter-port ordering lives (mksh and toybox both link
# against musl, whose install prefix is under build/ — so `make clean` wipes it
# and musl must be rebuildable as part of the normal chain, not out-of-band).
#
# On Windows the ports are NOT rebuilt. musl, mksh and toybox are Linux
# userland built by configure scripts and kconfig, and getting those to run
# under Git Bash against a clang cross-target is a separate project from
# building the OS. Their outputs are already staged in sysroot/ and change
# perhaps twice a year, so the cost of not rebuilding them here is close to
# zero -- and it is stated out loud rather than silently skipped, because a
# port that quietly does not rebuild is exactly the kind of thing that wastes
# an afternoon later.
sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS))
ifdef WINHOST
@echo "ports: skipped (Windows host) -- musl/mksh/toybox are used as already staged in $(SYSROOT)"
@test -f $(SYSROOT)/bin/sh || { echo "ports: $(SYSROOT)/bin/sh is missing and cannot be built here; build ports on a Linux host once" >&2; exit 1; }
else
$(MAKE) -C ports CC=gcc
endif
# A stamp whose timestamp advances only when the staged tree actually changes.
#
# make cannot depend on "the contents of a directory", and sysroot/ is filled
# by two different builds (cmd/ from here, ports/ recursively), so the images
# below used to depend on the phony sysroot-install -- which meant every image
# was rebuilt on every invocation, whether or not a single byte had moved.
# That cost 100 seconds of a 108-second no-op build.
#
# Path, size and mtime rather than content hashes: reading every staged file
# would undo the point, and a build system that lies about a file whose mtime
# and size both stayed the same has bigger problems. The cmp-then-move keeps
# the stamp's mtime unchanged when the summary is unchanged, which is the
# whole trick -- writing it unconditionally would just move the problem.
$(BUILD)/sysroot.stamp: sysroot-install
@mkdir -p $(BUILD)
@find $(SYSROOT) -type f -printf '%p %s %T@\n' | sort > $@.new
@cmp -s $@.new $@ || mv -f $@.new $@
@rm -f $@.new
$(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp
@mkdir -p $(BUILD)
@echo "Creating Schunix File System Image..."
@# A running QEMU holds this file open, and on Windows that makes rm fail
@# with "Device or resource busy" -- which stops the build *after* the
@# kernel has already relinked, leaving schunix.img stale. The next test run
@# then boots the previous kernel and the change appears to have done
@# nothing, which has cost several debugging cycles. Say so plainly.
@rm -f $@ || { \
echo ""; \
echo " $@ is in use -- a QEMU is still holding it."; \
echo " Close it, or: pwsh -Command \"Get-Process qemu-system-x86_64 |"; \
echo " Where-Object { -not \$$_.MainWindowTitle } | Stop-Process -Force\""; \
echo ""; \
exit 1; \
}
$(MKFS) $@ --sysroot $(SYSROOT)
# -----------------------------------------------------------------------
# QEMU
# -----------------------------------------------------------------------
qemu: schunix.img $(BUILD)/schfs.img
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) -cpu qemu64,+rdtscp \
-hda schunix.img -hdb $(BUILD)/schfs.img \
-m 1G -smp 2 -machine pc-i440fx-9.2
# The root filesystem. Built from the staged sysroot; the kernel mounts it as
# / in forkret().
$(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
@mkdir -p $(BUILD)
@rm -f $@
./$(MKSCHFS) $@ --size 8000 --label schunix --sysroot $(SYSROOT)
# Boot with a SchFS volume attached as the second disk.
#
# Needs the ramdisk-root kernel ("make ramroot" first): with RAMROOT the root
# filesystem comes out of memory and device 1 is free, which is exactly the
# arrangement docs/architecture/schfs.md asks for -- a new-format disk that
# fails to mount cannot stop the system booting. Without RAMROOT, device 1 is
# the live v1 root and the SchFS probe correctly finds nothing there.
#
# Once up: cat /proc/schfs
qemu-schfs: schunix.img $(BUILD)/schfs.img
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) -cpu qemu64,+rdtscp \
-hda schunix.img -hdb $(BUILD)/schfs.img \
-m 1G -smp 2 -machine pc-i440fx-9.2
# -----------------------------------------------------------------------
# Misc
# -----------------------------------------------------------------------
# schunix.img is deliberately NOT here. It is a real file with real
# prerequisites; listing it as phony made make skip the timestamp check and
# re-run the four dd commands -- including zeroing five megabytes -- on every
# single invocation, including ones where nothing had changed.
# include/logo.h is generated but committed, exactly as include/font.h is: it
# derives from art that changes about never, and making it a build dependency
# would put Python on the critical path of every clean build for no benefit.
# Run this by hand after changing the art.
LOGO_SRC = Assets/Schunix Small Logo.png
LOGO_PX = 128
logo:
$(PYTHON) tools/mklogo.py "$(LOGO_SRC)" include/logo.h $(LOGO_PX)
# The USB vendor/product name database, from linux-usb.org.
#
# Dual-licensed GPL-2.0-or-later **or** 3-clause BSD; Schunix takes the BSD
# option, which is what makes it usable at all. See NOTICE.
#
# Cached in dist/ and refreshed only when you ask, NOT on every build. That was
# the request, and it is the one thing here worth pushing back on: fetching on
# every compilation makes an offline build fail, makes the same source produce
# different images on different days, and puts a third-party web server on the
# critical path of `make`. dist/ exists precisely so a rebuild never needs the
# network -- see docs/layout.md. `make usb-ids` is a deliberate act; the file it
# writes is committed and dated, so what shipped is always knowable.
USB_IDS_URL = http://www.linux-usb.org/usb.ids
usb-ids:
@echo "==> fetching $(USB_IDS_URL)"
@curl -sfL --max-time 60 -o dist/usb.ids.new "$(USB_IDS_URL)" || \
{ echo " fetch failed; keeping the cached copy"; rm -f dist/usb.ids.new; exit 1; }
@# A truncated or error-page download would otherwise silently replace a
@# good database with rubbish. Check it looks like what it claims to be.
@grep -q "^# Version:" dist/usb.ids.new || \
{ echo " that is not a usb.ids file; keeping the cached copy"; \
rm -f dist/usb.ids.new; exit 1; }
@mv dist/usb.ids.new dist/usb.ids
@cp dist/usb.ids $(SYSROOT)/usr/share/usb.ids
@grep -m1 "^# Version:" dist/usb.ids
# Build the kernel's QR encoder on the host and check it.
#
# A QR symbol with a mistyped table entry still looks like a perfectly plausible
# grid of black squares, so the self-checks in qrtest.c cover what can be
# verified mechanically and the PNG covers the rest: point a phone at it.
QRTEST = $(BUILD)/tools/qrtest$(EXE)
$(QRTEST): tools/qrtest.c kernel/core/qr.c include/qr.h
@mkdir -p $(BUILD)/tools
$(HOSTCC) $(HOSTWARN) -I./include -o $@ tools/qrtest.c
qrtest: $(QRTEST)
@mkdir -p $(BUILD)
./$(QRTEST) $(BUILD)/qr.png
# The check that actually decides whether the encoder works: build a symbol and
# read it back with a real decoder, plus a module-for-module comparison against
# a second encoder. Needs Python packages that are not part of the build, so it
# is not wired into `make` -- install them wherever you like and point
# PYTHONPATH at it:
#
# pip install --target build/pylibs opencv-python-headless numpy segno
# PYTHONPATH=build/pylibs make qrcheck
qrcheck: $(QRTEST)
$(PYTHON) tools/qrcheck.py
.PHONY: all kernel ramroot cmd ports sysroot-install qemu \
schfs-test schfs-objtest schfs-image-test schfs-check mkschfs \
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
logo qrtest qrcheck crashqr usb-ids
.PRECIOUS: $(BUILD)/%.o
# 'clean' removes generated output only. dist/ (upstream tarballs) survives,
# so a rebuild never needs the network.
clean:
rm -rf $(BUILD) schunix.img schunix.iso kernel/arch/x86_64/vectors.S
distclean: clean
rm -rf dist
# Header dependencies, from gcc -MD.
#
# This used to be a set of globs including `$(BUILD)/kernel/**/*.d`. make's
# wildcard has no recursive `**`: it behaved as a single `*` and matched
# exactly one level, so build/kernel/core/*.d was picked up while
# build/kernel/arch/x86_64/*.d and build/kernel/fs/schfs/*.d were not.
# Everything under arch/ and schfs/ therefore had NO header dependency
# tracking at all -- editing include/schfs.h or include/mmu.h left stale
# objects that linked without complaint. find actually recurses.
-include $(shell find $(BUILD) -name '*.d' 2>/dev/null)

145
NOTICE Normal file
View file

@ -0,0 +1,145 @@
Schunix
Copyright 2026 Nexoniarz
This product includes software developed as part of the Schunix project,
licensed under the Apache License, Version 2.0. See LICENSE.
================================================================================
Third-party software
================================================================================
Schunix incorporates and redistributes software written by others. Their
licences apply to their code and are NOT superseded by the Schunix licence.
A built Schunix image contains binaries derived from all of the following.
--------------------------------------------------------------------------------
xv6-x86_64 / xv6 MIT License
--------------------------------------------------------------------------------
Copyright (c) 2006-2019 Frans Kaashoek, Robert Morris, Russ Cox,
Massachusetts Institute of Technology CSAIL
The Schunix kernel is a derivative work of xv6. Substantial portions of
kernel/ -- notably the process, trap, virtual memory, buffer cache and v1
filesystem code -- descend from it, however heavily rewritten. The MIT licence
requires that this notice be preserved in all copies; see LICENSE.MIT.
--------------------------------------------------------------------------------
musl libc 1.2.5 MIT License
--------------------------------------------------------------------------------
Copyright (c) 2005-2020 Rich Felker and contributors
Built from ports/musl. Provides the C library every userland program links
against. See the extracted source's COPYRIGHT file for the full contributor
list, which the MIT terms require be carried along with binaries.
--------------------------------------------------------------------------------
toybox 0.8.12 Zero-Clause BSD (0BSD)
--------------------------------------------------------------------------------
Copyright (c) 2006-2024 Rob Landley and contributors
Built from ports/toybox. Supplies the standard command set in /bin. 0BSD
imposes no attribution requirement at all; this entry is courtesy, not
obligation.
--------------------------------------------------------------------------------
mksh R59c MirOS Licence
--------------------------------------------------------------------------------
Copyright (c) 2002-2023 Thorsten Glaser and contributors
Portions derived from the Public Domain Korn Shell (pdksh)
Built from ports/mksh. Supplies /bin/sh. The MirOS Licence is permissive and
requires the copyright notice be retained.
--------------------------------------------------------------------------------
Intel 8254x device identifiers BSD 3-Clause
--------------------------------------------------------------------------------
Copyright (c) 2001-2020, Intel Corporation
All rights reserved.
The PCI device IDs in include/e1000.h are transcribed from FreeBSD's
sys/dev/e1000/e1000_hw.h, which carries the Intel 3-clause BSD licence above
(verified: no advertising clause). Only the identifier constants are used --
data, not implementation. kernel/drivers/net/e1000/e1000.c is written from
Intel's public manual 317453 and contains no FreeBSD or Intel driver code.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the conditions of the 3-clause BSD
licence are met, including retention of this notice and the disclaimer, and
that neither the name of Intel Corporation nor the names of its contributors
are used to endorse or promote products derived from this software without
specific prior written permission.
--------------------------------------------------------------------------------
usb.ids -- USB vendor and product database BSD 3-Clause (dual-licensed)
--------------------------------------------------------------------------------
Maintained by Stephen J. Gowdy <linux.usb.ids@gmail.com>
http://www.linux-usb.org/usb.ids
Distributed by its maintainer under the terms of *either* the GNU General
Public License version 2 or later, *or* the 3-clause BSD Licence. Schunix takes
the BSD option; the GPL option is not exercised and could not be, since
Apache-2.0 and GPL-2.0-only cannot be combined (see docs/licensing.md section
2).
Shipped verbatim as /usr/share/usb.ids. Cached in dist/ and refreshed with
`make usb-ids`, never automatically -- the version and date are in the file's
own header, so what shipped is always knowable.
--------------------------------------------------------------------------------
Roboto Apache-2.0
--------------------------------------------------------------------------------
Copyright the Roboto Project Authors (https://github.com/googlefonts/roboto)
Shipped as /usr/share/fonts/Roboto-Regular.ttf, from ports/Roboto. Licensed
under Apache-2.0 -- the same licence as Schunix itself, so it carries no
obligation beyond this notice. Intended for the desktop, window manager and
compositor.
--------------------------------------------------------------------------------
JetBrains Mono SIL OFL 1.1
--------------------------------------------------------------------------------
Copyright 2020 The JetBrains Mono Project Authors
(https://github.com/JetBrains/JetBrainsMono)
Shipped as /usr/share/fonts/JetBrainsMono-Regular.ttf, from ports/JetBrains_Mono
(full licence text at ports/JetBrains_Mono/OFL.txt). Intended for the terminal
and TTY.
Under the SIL Open Font License 1.1 the font remains under OFL even inside this
Apache-2.0 system -- that does not reach any other part of Schunix -- and it may
not be sold on its own. Bundling it with an operating system is expressly
permitted. JetBrains Mono declares no Reserved Font Name, so a modified version
(a pre-rasterised bitmap, for instance) may keep the name.
--------------------------------------------------------------------------------
"New File System For XV6" MIT License
--------------------------------------------------------------------------------
Copyright (c) Qi Ma, Leyi Pan, Ao Sun, Peiran Xu
kernel/fs/fs.c and parts of tools/mkfs.c derive from this project, which extends
xv6's file system with features Schunix still uses today:
- the HTree hashed directory index (BKDRHash, HASHRANGE, the primary/overflow
bucket layout), after Daniel Phillips, "A Directory Index for EXT2",
5th Annual Linux Showcase & Conference, 2001
- Ext4-style extents (T_EXTENT)
- the inode bitmap
- symbolic links, file permissions, lseek and append
Licensed under the MIT License, which permits this use and requires that the
copyright notice and permission notice be retained. This entry is that
retention; it was missing until 2026-08-09 and is recorded here regardless of
what happens to the code, because the obligation attaches to every copy already
made.
Distinct from xv6 itself (LICENSE.MIT), which this project in turn derives from.
================================================================================
The build downloads upstream tarballs into dist/ and applies the patches in
ports/<name>/patches/. Nothing in ports/ is Schunix code; do not add first-party
work there. See docs/layout.md.
Licence questions -- what may be copied in, what may never be, and how to check
a NetBSD file's clause before using it -- are worked through in
docs/licensing.md.

101
boot/bootasm.S Normal file
View file

@ -0,0 +1,101 @@
# Stage 1 — the actual MBR boot sector. Hard-limited to 510 bytes (+2-byte
# 0x55AA signature) by the BIOS, so there's no room here for anything but
# "load stage 2 off disk and jump to it". All the VBE/A20/protected-mode/
# splash/kernel-loading work now lives in stage2.S + stage2main.c, which
# aren't sector-size-constrained.
#
# How many sectors of stage2 to pull in. This, the padding the Makefile gives
# stage2's binary, and the "+(STAGE2_SECTORS+1)" sector math in stage2main.c's
# readseg() all have to agree on where the kernel starts on disk -- so the
# Makefile passes -DSTAGE2_SECTORS and is the single authority. The value below
# is only a fallback for reading this file standalone.
#
# It is 48, not the old 24, because stage2 now carries the boot logo
# (include/logo.h, 8 KiB). The real ceiling is memory rather than this number:
# stage2 loads at 0x7E00 and the multiboot kernel-search buffer starts at
# 0x10000, so stage2 cannot exceed 0x8200 bytes = 65 sectors.
#ifndef STAGE2_SECTORS
#define STAGE2_SECTORS 48
#endif
#define STAGE2_ADDR 0x7E00
.code16
.global start
start:
cli
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
movw $0x7c00, %sp
# Read STAGE2_SECTORS sectors, starting at disk sector 1 (LBA), into
# memory right after this boot sector (0x7E00). Raw IDE PIO (LBA28) —
# same register convention as stage2main.c's own readsect(), just
# inlined here since we can't call C code yet. Port numbers 0x1F2-0x1F7
# are all > 0xFF, so they must go through %dx — `out $imm8,%al` only
# encodes an 8-bit port and silently truncates anything larger.
# "You were not booted from a CD." stage2main.c reads this byte to choose
# between ATA and ATAPI when it loads the kernel, and nothing else clears
# it -- so a warm reboot from a disc could otherwise leave the flag set and
# send a perfectly ordinary hard-disk boot looking for an optical drive.
# Placed one byte past the verbose flag; both sit clear of the 256-byte VBE
# mode info block that stage2.S is about to write at 0x30000.
movw $0x3000, %ax
movw %ax, %es
movw $0x101, %di
movb $0, %al
movb %al, %es:(%di)
xorw %ax, %ax
movw %ax, %es
movw $STAGE2_ADDR, %di # ES:DI destination, ES=0
movw $1, %si # first LBA sector to read
movb $STAGE2_SECTORS, %bl
.Lsector:
.Lwait1:
movw $0x1F7, %dx
inb %dx, %al
andb $0xC0, %al
cmpb $0x40, %al
jne .Lwait1
movw $0x1F2, %dx
movb $1, %al
outb %al, %dx # sector count = 1
incw %dx # 0x1F3
movw %si, %ax
outb %al, %dx # LBA [7:0]
incw %dx # 0x1F4
xorb %al, %al
outb %al, %dx # LBA [15:8]
incw %dx # 0x1F5
outb %al, %dx # LBA [23:16]
incw %dx # 0x1F6
movb $0xE0, %al
outb %al, %dx # drive 0, LBA mode
incw %dx # 0x1F7
movb $0x20, %al
outb %al, %dx # READ SECTORS command
.Lwait2:
inb %dx, %al # %dx still 0x1F7
andb $0xC0, %al
cmpb $0x40, %al
jne .Lwait2
movw $256, %cx # 256 words = 512 bytes
movw $0x1F0, %dx
rep insw # reads into ES:DI, DI += 512 automatically
incw %si
decb %bl
jnz .Lsector
ljmp $0, $STAGE2_ADDR

48
boot/cdboot.S Normal file
View file

@ -0,0 +1,48 @@
# Stage 1 for optical media -- the El Torito "no emulation" boot sector.
#
# This exists because bootasm.S cannot boot a CD. It loads stage 2 by talking
# straight to the primary IDE controller's ATA registers, and a CD is not an
# ATA disk: it answers to the ATAPI packet interface instead, and under
# El Torito it may not even be the drive at 0x1F0. Rather than teach a
# 512-byte sector two protocols, optical media get their own first stage.
#
# It has almost nothing to do, because El Torito's "load segment / sector
# count" fields do the reading for us: tools/mkiso.sh asks for
# 1 + STAGE2_SECTORS virtual sectors at 0x7C00, so by the time this runs the
# BIOS has already placed stage 2 at 0x7E00 -- exactly where bootasm.S would
# have put it. The only real work is recording *which* medium we came from,
# since stage2main.c has to read the kernel back with the matching protocol.
#
# The flag goes next to the verbose-boot flag, one byte past it, for the same
# reason that one sits where it does: it is clear of the 256-byte VBE mode
# info block at 0x30000 that stage2.S fills in a moment from now.
#define STAGE2_ADDR 0x7E00
#define BOOT_MEDIA_FLAG_SEG 0x3000
#define BOOT_MEDIA_FLAG_OFF 0x101
#define BOOT_MEDIA_CD 1
.code16
.global start
start:
cli
xorw %ax, %ax
movw %ax, %ds
movw %ax, %ss
movw $0x7c00, %sp
# "You were booted from a CD." stage2main.c reads this to decide between
# ATA and ATAPI; without it the loader would read the kernel off whatever
# happens to be sitting at 0x1F0, which on a CD boot is usually nothing.
movw $BOOT_MEDIA_FLAG_SEG, %ax
movw %ax, %es
movw $BOOT_MEDIA_FLAG_OFF, %di
movb $BOOT_MEDIA_CD, %al
movb %al, %es:(%di)
# stage2.S expects zeroed segments and an inherited stack, same as it gets
# from bootasm.S.
xorw %ax, %ax
movw %ax, %es
ljmp $0, $STAGE2_ADDR

129
boot/stage2.S Normal file
View file

@ -0,0 +1,129 @@
# Stage 2 — loaded by stage1 at 0x7E00, still in 16-bit real mode. Does the
# VBE mode setup, A20 line, and real->protected mode switch that used to
# live directly in the boot sector, then hands off to stage2main() (C) to
# draw a splash and load the actual kernel. Not sector-size-constrained —
# only bounded by STAGE2_SECTORS in bootasm.S / the Makefile.
#include "../include/asm.h"
#include "../include/memlayout.h"
#include "../include/mmu.h"
.code16
.global stage2start
stage2start:
# Segments are already zeroed by stage1; %sp is inherited too.
movw $bootmsg, %si
call print16
# VBE Setup. Mode info block goes at 0x30000, not 0x9000 — stage2 itself
# is loaded starting at 0x7E00 and, with the boot-menu/progress-bar code,
# is now big enough to reach 0x9000, so writing the VBE block there would
# clobber stage2's own tail end. 0x30000 is clear of stage2 (ends well
# under 0xA000), the multiboot kernel-search buffer (0x10000-0x14000),
# and the kernel's own load address (0x100000).
movw $0x3000, %ax
movw %ax, %es
xorw %di, %di # ES:DI = 3000:0000 = flat 0x30000
movw $0x0143, %cx
movw $0x4F01, %ax
int $0x10
cmpw $0x004F, %ax
jne e820
movw $0x4143, %bx
movw $0x4F02, %ax
int $0x10
# BIOS E820 memory map — the standard firmware interface every serious
# loader (GRUB, Linux's own boot stub, the BSDs) uses to learn the real
# RAM layout instead of guessing. Entry count goes to flat 0x30200,
# 24-byte entries start at 0x30208 (both inside the 0x3000 segment the
# VBE block already claimed; verbose flag sits at +0x100). Capped at 32
# entries so a pathological BIOS can't run us past the buffer.
e820:
movw $0x3000, %ax
movw %ax, %es
xorl %ebx, %ebx # continuation cookie, 0 = first call
movw $0x208, %di
xorl %ebp, %ebp # entry count
e820loop:
movl $0xE820, %eax
movl $0x534D4150, %edx # 'SMAP'
movl $24, %ecx
int $0x15
jc e820done # carry = end of map (or unsupported BIOS)
cmpl $0x534D4150, %eax
jne e820done
addw $24, %di
incl %ebp
cmpl $32, %ebp
jae e820done
testl %ebx, %ebx
jnz e820loop
e820done:
movl %ebp, %es:(0x200)
xorw %ax, %ax
movw %ax, %es
seta20.1:
inb $0x64, %al
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al
outb %al, $0x64
seta20.2:
inb $0x64, %al
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al
outb %al, $0x60
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE, %eax
movl %eax, %cr0
ljmp $(SEG_KCODE<<3), $start32
# Print null-terminated string at DS:SI via BIOS teletype (real mode only).
# Clobbers AX, BX, SI.
print16:
.Lp16loop:
lodsb
testb %al, %al
jz .Lp16done
movb $0x0E, %ah
movw $0x0007, %bx
int $0x10
jmp .Lp16loop
.Lp16done:
ret
bootmsg:
.asciz "Schunix booting...\r\n"
.code32
start32:
movw $(SEG_KDATA<<3), %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
xorw %ax, %ax
movw %ax, %fs
movw %ax, %gs
movl $0x7c00, %esp
call stage2main
spin:
jmp spin
.p2align 2
gdt:
SEG_NULLASM
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)
SEG_ASM(STA_W, 0x0, 0xffffffff)
gdtdesc:
.word (gdtdesc - gdt - 1)
.long gdt

878
boot/stage2main.c Normal file
View file

@ -0,0 +1,878 @@
#include "../include/types.h"
#include "../include/x86.h"
#include "../include/memlayout.h"
#include "../include/font.h"
#define SECTSIZE 512
// Kernel starts right after stage2 on disk. The Makefile passes
// -DSTAGE2_SECTORS so this, bootasm.S and stage2's padding cannot drift apart;
// the value here is a fallback for reading the file on its own.
#ifndef STAGE2_SECTORS
#define STAGE2_SECTORS 48
#endif
// stage2 is loaded at 0x7E00 and must not reach the multiboot kernel-search
// buffer at 0x10000. Nothing checks this at runtime, and the failure it would
// produce -- a kernel image partly overwritten by the loader that loaded it --
// would be spectacularly hard to read backwards from.
_Static_assert(0x7E00 + STAGE2_SECTORS * 512 <= 0x10000,
"stage2 would overlap the kernel-search buffer at 0x10000");
// One palette for the whole system; see include/theme.h. The splash used to
// use the panic screen's deep purple and gold, which meant a healthy boot was
// dressed identically to a crash.
#include "theme.h"
// The boot logo, generated from Assets/ by tools/mklogo.py. Shared verbatim
// with kernel/dev/vga.c so there is one mask, not two.
#include "logo.h"
// VBE mode info block, written here by stage2.S's VBE call. NOT at 0x9000
// (the classic single-stage bootloader's choice) — stage2's own code now
// runs from 0x7E00 up to nearly 0x9000 once the boot-menu/progress-bar
// logic is included, so a block at 0x9000 would get partially overwritten
// by stage2's own tail end. 0x30000 is clear of stage2, the multiboot
// kernel-search buffer (0x10000-0x14000), and the kernel's load address
// (0x100000).
#define VBE_INFO_ADDR 0x30000
// Fixed low-memory scratch byte, read by kernel/core/main.c's kmain() before
// any other init: 1 if the user picked "verbose" from the boot menu. Placed
// just past the VBE info block (which is at most 256 bytes), NOT at 0x7000
// — kernel/core/main.c's startothers() already uses 0x7000 for the AP
// (secondary CPU) boot trampoline. That's write-after-read from this flag's
// perspective (verbose is read before startothers() runs), so it wasn't an
// active bug, but reusing a documented-elsewhere address is exactly the
// kind of thing that turns into one later.
#define BOOT_VERBOSE_FLAG_ADDR (VBE_INFO_ADDR + 0x100)
// Set by boot/cdboot.S when we were booted from optical media. One byte past
// the verbose flag, for the same reason that one sits where it does: clear of
// the 256-byte VBE mode info block at VBE_INFO_ADDR.
#define BOOT_MEDIA_FLAG_ADDR (VBE_INFO_ADDR + 0x101)
#define BOOT_MEDIA_CD 1
// E820 memory map, collected by stage2.S in real mode (the same firmware
// interface GRUB and Linux's boot stub use). Count at +0x200, then 24-byte
// entries: {uint64 base; uint64 length; uint32 type; uint32 acpi_ext}.
// The kernel can read the same addresses later; the bootloader uses it to
// report detected RAM like any grown-up loader does.
#define E820_COUNT_ADDR (VBE_INFO_ADDR + 0x200)
#define E820_MAP_ADDR (VBE_INFO_ADDR + 0x208)
#define E820_USABLE 1
// NOT include/types.h's uint64: that's `unsigned long`, which is 4 bytes
// in this file's -m32 build and would silently halve the struct.
typedef unsigned long long e820_u64;
struct e820entry {
e820_u64 base;
e820_u64 length;
uint32 type;
uint32 acpi_ext;
} __attribute__((packed));
// Total usable RAM in MB according to the firmware's E820 map.
static uint32
e820_usable_mb(void)
{
uint32 count = *(uint32*)E820_COUNT_ADDR;
struct e820entry *e = (struct e820entry*)E820_MAP_ADDR;
e820_u64 total = 0;
if (count > 32) return 0; // garbage count: BIOS didn't fill the map
for (uint32 i = 0; i < count; i++)
if (e[i].type == E820_USABLE)
total += e[i].length;
return (uint32)(total >> 20);
}
// Minimal unsigned-to-decimal for the memory line; no printf down here.
static char *
utoa10(uint32 v, char *buf)
{
char tmp[12];
int n = 0;
do { tmp[n++] = '0' + v % 10; v /= 10; } while (v);
int i = 0;
while (n) buf[i++] = tmp[--n];
buf[i] = 0;
return buf;
}
struct mbheader {
uint32 magic;
uint32 flags;
uint32 checksum;
uint32 header_addr;
uint32 load_addr;
uint32 load_end_addr;
uint32 bss_end_addr;
uint32 entry_addr;
};
void readseg(uchar*, uint, uint);
static void draw_centered(int y, const char *s, uint32 color);
static void draw_memory_line(int y, uint32 color);
// Which medium the kernel has to be read back from. cdboot.S sets the flag
// this is loaded from; bootasm.S clears it, and 0 means "an ATA disk, the way
// it has always been". The ATAPI machinery itself is down at the bottom of
// the file next to readsect(), which is the only thing that uses it.
#define CD_SECTSIZE 2048
static int g_from_cd;
static int g_atapi_io = 0; // 0 until a drive is found
static int g_atapi_slave = 0;
static uint g_cd_base512 = 0; // where our boot image starts, in 512s
static int atapi_find(void);
static int atapi_boot_base(void);
// --- Framebuffer state ---------------------------------------------------
// Standalone drawing (not the kernel's vga.c): paging isn't set up yet, so
// the LFB physical address read out of the VBE mode info block is used
// directly, no P2V translation needed. Only handles 32bpp (what QEMU's
// std VGA actually returns for the mode this bootloader requests) —
// anything else just skips all drawing rather than risk drawing garbage.
static uint8 *g_fb;
static uint32 g_stride;
static int g_width, g_height;
static int g_have_fb;
static void
fb_init(void)
{
g_stride = *(ushort*)(VBE_INFO_ADDR + 16);
g_width = *(ushort*)(VBE_INFO_ADDR + 18);
g_height = *(ushort*)(VBE_INFO_ADDR + 20);
uchar bpp = *(uchar*)(VBE_INFO_ADDR + 25);
uint32 fbaddr = *(uint32*)(VBE_INFO_ADDR + 40);
g_fb = (uint8*)fbaddr;
g_have_fb = (bpp == 32 && fbaddr && g_width && g_height);
}
static void
draw_pixel(int x, int y, uint32 color)
{
*(uint32*)(g_fb + y * g_stride + x * 4) = color;
}
// --- Boot logo -------------------------------------------------------------
//
// include/logo.h is a 4-bit coverage mask generated from Assets/ by
// tools/mklogo.py (`make logo`). It costs 8 KiB, which is most of the reason
// STAGE2_SECTORS went from 24 to 48.
//
// Colour is SCH_FG, not SCH_ACCENT. The source art is a silver gradient and the
// mask preserves its relative values, so compositing coverage straight to
// brightness reproduces that gradient. The accent stays reserved for the single
// rule under the wordmark -- one spot colour, per include/theme.h.
static void
draw_logo(int x0, int y0)
{
if (!g_have_fb)
return;
for (int dy = 0; dy < LOGO_H; dy++) {
int y = y0 + dy;
if (y < 0 || y >= g_height)
continue;
uint32 *row = (uint32*)(g_fb + y * g_stride);
for (int dx = 0; dx < LOGO_W; dx++) {
int x = x0 + dx;
if (x < 0 || x >= g_width)
continue;
unsigned a = LOGO_INK(dy * LOGO_W + dx);
// Skip empty pixels rather than painting them in the background colour.
// The mask is square but the mark is a circle, so most of it is empty;
// filling that would stamp a visible box onto the splash.
if (a == 0)
continue;
if (a >= 255) {
row[x] = SCH_FG;
continue;
}
// Blend towards SCH_BG per channel. Integer only -- there is no FPU set
// up at this point in boot, and no libgcc to fake one with.
uint32 r = (((SCH_FG >> 16) & 0xFF) * a + ((SCH_BG >> 16) & 0xFF) * (255 - a)) / 255;
uint32 g = (((SCH_FG >> 8) & 0xFF) * a + ((SCH_BG >> 8) & 0xFF) * (255 - a)) / 255;
uint32 b = (((SCH_FG ) & 0xFF) * a + ((SCH_BG ) & 0xFF) * (255 - a)) / 255;
row[x] = (r << 16) | (g << 8) | b;
}
}
}
static void
fill_rect(int x0, int y0, int w, int h, uint32 color)
{
// Compute each row's base pointer once instead of re-deriving it (via a
// multiply + add) for every single pixel through draw_pixel() — this is
// the full-screen splash fill, 480,000 pixels at 800x600, done at least
// twice per boot.
for (int y = y0; y < y0 + h; y++) {
uint32 *row = (uint32*)(g_fb + y * g_stride) + x0;
for (int x = 0; x < w; x++)
row[x] = color;
}
}
static void
draw_char(int x, int y, uchar c, uint32 color, uint32 bg)
{
if (c > 127) return;
for (int row = 0; row < 16; row++) {
uchar bits = font_data[c][row];
for (int col = 0; col < 8; col++)
draw_pixel(x + col, y + row, (bits & (0x80 >> col)) ? color : bg);
}
}
static void
draw_string(int x, int y, const char *s, uint32 color, uint32 bg)
{
while (*s) {
draw_char(x, y, *s, color, bg);
x += 8;
s++;
}
}
// Pads with trailing spaces (drawn in bg) so re-drawing a shorter string
// over a longer one doesn't leave stale glyphs behind.
static void
draw_string_padded(int x, int y, const char *s, uint32 color, uint32 bg, int cols)
{
int n = 0;
while (s[n]) { draw_char(x + n * 8, y, s[n], color, bg); n++; }
while (n < cols) { draw_char(x + n * 8, y, ' ', color, bg); n++; }
}
// --- Keyboard: raw PS/2 scancode polling, no interrupts, no BIOS calls --
// (BIOS INT 16h only works in real mode; we're in protected mode by the
// time any of this runs.) Scancode set 1: F8 make code 0x42, arrows are
// extended (0xE0 prefix byte), Enter 0x1C, digits '1'-'3' are 0x02-0x04.
static int
kbd_haskey(void)
{
return inb(0x64) & 1;
}
static int
kbd_readscancode(void)
{
return inb(0x60);
}
// Blocking read of one logical key: plain scancodes pass through, extended
// (0xE0-prefixed) scancodes come back OR'd with 0x100 so callers can tell
// "up arrow" (0x148) apart from a plain byte that happens to equal 0x48.
static int
kbd_getkey(void)
{
int sc;
for (;;) {
while (!kbd_haskey())
;
sc = kbd_readscancode();
// 0xE0 is the extended-key prefix, not a break code, even though it
// also has bit 7 set — must be checked before the break-code test
// below or every arrow/Home/End/etc. press gets silently swallowed.
if (sc == 0xE0) {
while (!kbd_haskey())
;
sc = kbd_readscancode();
if (sc & 0x80) continue; // extended break code: ignore, try again
return 0x100 | sc;
}
if (sc & 0x80) continue; // plain break code: ignore, try again
return sc;
}
}
// Polls for up to `iterations` busy-loop spins, watching for F8's make
// code. Returns 1 if seen, 0 on timeout. Ignores everything else so a
// stray keypress during the splash doesn't do anything surprising.
static int
wait_for_f8(uint32 iterations)
{
for (volatile uint32 i = 0; i < iterations; i++) {
if (kbd_haskey()) {
int sc = kbd_readscancode();
if (sc == 0x42)
return 1;
}
}
return 0;
}
// --- ATA drive presence (primary channel: master/slave) ------------------
// Standard "floating bus" trick: select the drive, write a distinguishable
// pattern to two data-path registers, and read it back. A missing drive
// leaves the bus floating and the readback won't match.
//
// Only the primary channel (0x1F0) is checked. QEMU's IDE emulation
// answers the same pattern test "successfully" on the secondary channel
// (0x170) even with nothing attached to it — its floating-bus behavior
// doesn't match real hardware closely enough to tell present from absent —
// so showing it would just be confidently wrong. This project only ever
// configures drives on the primary channel (-hda/-hdb) anyway.
static int
ata_present(ushort iobase, int slave)
{
outb(iobase + 6, slave ? 0xB0 : 0xA0);
inb(iobase + 7); inb(iobase + 7); inb(iobase + 7); inb(iobase + 7);
outb(iobase + 2, 0xAA);
outb(iobase + 3, 0x55);
outb(iobase + 2, 0x55);
outb(iobase + 3, 0xAA);
outb(iobase + 2, 0xAA);
outb(iobase + 3, 0x55);
return inb(iobase + 2) == 0xAA && inb(iobase + 3) == 0x55;
}
// --- Real-time load progress bar -----------------------------------------
#define BAR_WIDTH 400
#define BAR_HEIGHT 18
// --- Splash layout -------------------------------------------------------
//
// Every vertical position on the splash derives from here. It used to be a
// handful of independent offsets from the screen centre -- wordmark at cy-18,
// rule at cy+6, bar at cy+30 -- which was fine while the tallest element was a
// line of text. Adding a 128px mark on top of that stack pushed the group's
// optical centre about 80px above the screen's, so the whole composition read
// as top-heavy with a large void underneath.
//
// Now the group is measured and centred as a unit. The progress bar counts
// towards that height even though it is only on screen during the kernel load:
// if it did not, everything would jump upwards the moment the bar appeared.
#define SPLASH_MARK_GAP 24 // mark to wordmark
#define SPLASH_TEXT_H 16 // one line of the 8x16 font
#define SPLASH_RULE_GAP 8 // wordmark to rule
#define SPLASH_RULE_H 2
#define SPLASH_BAR_GAP 30 // rule to bar; the bar's label lives in here
#define SPLASH_GROUP_H (LOGO_H + SPLASH_MARK_GAP + SPLASH_TEXT_H + \
SPLASH_RULE_GAP + SPLASH_RULE_H + SPLASH_BAR_GAP + BAR_HEIGHT)
#define SPLASH_MARK_Y ((g_height - SPLASH_GROUP_H) / 2)
#define SPLASH_TEXT_Y (SPLASH_MARK_Y + LOGO_H + SPLASH_MARK_GAP)
#define SPLASH_RULE_Y (SPLASH_TEXT_Y + SPLASH_TEXT_H + SPLASH_RULE_GAP)
#define SPLASH_BAR_Y (SPLASH_RULE_Y + SPLASH_RULE_H + SPLASH_BAR_GAP)
static uint32 g_load_total, g_load_done;
static int g_progress_active;
static int g_bar_x, g_bar_y;
static int g_last_pct;
static void
progress_label(const char *label)
{
draw_string_padded(g_bar_x, g_bar_y - 22, label, SCH_ACCENT, SCH_BG, BAR_WIDTH / 8);
}
// Border never changes once drawn — separated out so it's only ever drawn
// once per bar instead of being redrawn on every single tick.
static void
progress_border(void)
{
for (int x = g_bar_x; x < g_bar_x + BAR_WIDTH; x++) {
draw_pixel(x, g_bar_y, SCH_FG);
draw_pixel(x, g_bar_y + BAR_HEIGHT, SCH_FG);
}
for (int y = g_bar_y; y <= g_bar_y + BAR_HEIGHT; y++) {
draw_pixel(g_bar_x, y, SCH_FG);
draw_pixel(g_bar_x + BAR_WIDTH, y, SCH_FG);
}
}
static void
progress_fill(int pct)
{
if (pct > 100) pct = 100;
int fillw = (BAR_WIDTH - 4) * pct / 100;
for (int y = g_bar_y + 2; y < g_bar_y + BAR_HEIGHT - 1; y++)
for (int x = g_bar_x + 2; x < g_bar_x + BAR_WIDTH - 2; x++)
draw_pixel(x, y, (x - g_bar_x - 2 < fillw) ? SCH_ACCENT : SCH_BG);
}
static void
progress_start(const char *label, uint32 total)
{
if (!g_have_fb) return;
g_bar_x = (g_width - BAR_WIDTH) / 2;
g_bar_y = SPLASH_BAR_Y;
g_load_total = total;
g_load_done = 0;
g_last_pct = -1;
g_progress_active = 1;
progress_label(label);
progress_border();
progress_fill(0);
g_last_pct = 0;
}
static void
progress_tick(uint32 n)
{
if (!g_have_fb || !g_progress_active) return;
g_load_done += n;
// Deliberately all 32-bit. The uint64 cast this used to carry was there to
// stop `done * 100` overflowing, but a 64-bit divide in 32-bit code calls
// libgcc's __udivdi3, which a freestanding bootloader does not link -- it
// only ever built because uint64 was accidentally 32 bits wide here.
//
// Scaling both sides to KiB first keeps the multiply inside 32 bits for any
// image up to 4 GiB, which is comfortably more than a kernel will ever be.
uint32 done_k = g_load_done >> 10;
uint32 total_k = g_load_total >> 10;
uint32 pct = total_k ? (done_k * 100) / total_k : 0;
if (pct > 100) pct = 100;
// A full-bar redraw is a few thousand pixel writes; without this, a
// ~330KB kernel image redraws it ~650 times (once per 512-byte sector)
// instead of at most 100 (once per percentage point) — the single
// biggest chunk of the bootloader's own wall-clock time.
if ((int)pct == g_last_pct) return;
g_last_pct = (int)pct;
progress_fill((int)pct);
}
// --- Boot menu -------------------------------------------------------
static void
reboot_to_firmware(void)
{
if (g_have_fb) {
fill_rect(0, 0, g_width, g_height, SCH_BG);
draw_string((g_width - 26 * 8) / 2, g_height / 2 - 8,
"Rebooting to firmware...", SCH_ACCENT, SCH_BG);
}
outb(0x64, 0xFE); // pulse the keyboard controller's reset line
for (;;)
;
}
static void
show_drive_line(int row, const char *label, int present)
{
draw_string(g_width / 2 - 160, 340 + row * 20, label, SCH_FG, SCH_BG);
draw_string(g_width / 2 + 20, 340 + row * 20,
present ? "present" : "not present",
present ? SCH_ACCENT : SCH_DIM, SCH_BG);
}
// Returns 1 for "boot verbose", 0 for "boot normal". Only returns at all
// once the user picks a boot option; "reboot" never returns.
static int
boot_menu(void)
{
const char *items[] = {
"Boot Schunix",
"Boot Schunix (verbose)",
"Reboot to firmware setup",
};
int nitems = 3;
int selected = 0;
int hda = ata_present(0x1F0, 0);
int hdb = ata_present(0x1F0, 1);
for (;;) {
fill_rect(0, 0, g_width, g_height, SCH_BG);
draw_centered(60, "Schunix Boot Manager", SCH_ACCENT);
draw_memory_line(84, SCH_DIM);
for (int i = 0; i < nitems; i++) {
uint32 fg = (i == selected) ? SCH_ACCENT : SCH_FG;
draw_string(g_width / 2 - 140, 140 + i * 24, (i == selected) ? "> " : " ", fg, SCH_BG);
draw_string(g_width / 2 - 124, 140 + i * 24, items[i], fg, SCH_BG);
}
draw_string(g_width / 2 - 160, 300, "Detected drives:", SCH_FG, SCH_BG);
show_drive_line(0, "Primary Master (hda)", hda);
show_drive_line(1, "Primary Slave (hdb)", hdb);
draw_string(g_width / 2 - 168, g_height - 40,
"Up/Down to choose, Enter to select", SCH_DIM, SCH_BG);
int key = kbd_getkey();
if (key == 0x148) selected = (selected + nitems - 1) % nitems; // up
else if (key == 0x150) selected = (selected + 1) % nitems; // down
else if (key == 0x02) selected = 0; // '1'
else if (key == 0x03) selected = 1; // '2'
else if (key == 0x04) { reboot_to_firmware(); } // '3'
else if (key == 0x1C) { // Enter
if (selected == 2)
reboot_to_firmware();
return selected == 1;
}
}
}
// Centered one-liner helper; strlen computed here so callers stop
// hand-counting characters (three of them had it wrong at some point).
static void
draw_centered(int y, const char *s, uint32 color)
{
int n = 0;
while (s[n]) n++;
draw_string((g_width - n * 8) / 2, y, s, color, SCH_BG);
}
// "Memory: NNNN MB" — assembled by hand, no printf in the bootloader.
static void
draw_memory_line(int y, uint32 color)
{
char line[32];
char num[12];
int i = 0;
const char *pfx = "Memory: ";
while (*pfx) line[i++] = *pfx++;
utoa10(e820_usable_mb(), num);
for (int j = 0; num[j]; j++) line[i++] = num[j];
const char *sfx = " MB";
while (*sfx) line[i++] = *sfx++;
line[i] = 0;
draw_centered(y, line, color);
}
static void
splash(void)
{
if (!g_have_fb) return;
fill_rect(0, 0, g_width, g_height, SCH_BG);
// The product, not the loader.
//
// This used to read "Schunix Boot Manager / version 1.0 / Memory: 1023 MB /
// Press F8 for boot menu...", centred, in gold on purple. Every part of that
// is something a system shows its developer rather than its user: the name
// of the component doing the work, its version, and a memory figure nobody
// asked for. No shipping OS names its bootloader on the splash.
//
// What is left is the name, a rule in the accent colour, and the one hint
// that is actually actionable -- moved to the bottom of the screen, where a
// hint belongs, rather than sitting under the title as though it were part
// of the identity.
// The mark, drawn here in the loader rather than by the kernel for a timing
// reason: the kernel does not reach vga_init() until the last few hundred
// milliseconds of a ~2.8s boot, so a kernel-drawn logo appeared for about one
// frame before console_takeover() wiped it. Here it is up for the whole boot.
draw_logo((g_width - LOGO_W) / 2, SPLASH_MARK_Y);
draw_centered(SPLASH_TEXT_Y, "S C H U N I X", SCH_FG);
// A 120x2 rule, centred, in the accent -- slightly wider than the 104px
// wordmark so it reads as an underline rather than a strikethrough. Two
// pixels rather than one: a single-pixel line looks like a rendering
// artefact at this size.
fill_rect((g_width - 120) / 2, SPLASH_RULE_Y, 120, SPLASH_RULE_H, SCH_ACCENT);
draw_centered(g_height - 40, "Press F8 for boot options", SCH_DIM);
}
void
stage2main(void)
{
struct mbheader *hdr;
uint32 *x = (uint32*) 0x10000;
uint n;
int verbose = 0;
fb_init();
// Sort out where the kernel is coming from before anything is drawn, so a
// disc we cannot read says so on the splash instead of failing silently
// several seconds later in the middle of the progress bar.
if (*(uchar*)BOOT_MEDIA_FLAG_ADDR == BOOT_MEDIA_CD) {
if (atapi_find())
g_cd_base512 = atapi_boot_base() * (CD_SECTSIZE / SECTSIZE);
g_from_cd = (g_atapi_io != 0 && g_cd_base512 != 0);
}
splash();
// ~0.6s at QEMU TCG speeds (measured), watching for F8. Shortened from
// ~1s (5000000) — still enough time to react to, but it's flat overhead
// on every single boot whether or not anyone wants the menu.
if (wait_for_f8(3000000))
verbose = boot_menu();
*(uchar*)BOOT_VERBOSE_FLAG_ADDR = verbose;
if (g_have_fb) {
fill_rect(0, 0, g_width, g_height, SCH_BG);
draw_string((g_width - 7 * 8) / 2, g_height / 2 - 50, "SCHUNIX", SCH_ACCENT, SCH_BG);
}
// Booted from a disc we then could not read. Falling through would send
// readseg() at the primary ATA channel, which on a CD-only machine is empty
// -- so it would grind through a bounded wait per sector and then jump into
// whatever the uninitialised buffer happened to hold. Stopping with a
// legible reason is the only useful thing left to do. This is drawn after
// the clear above on purpose: an earlier version put it on the splash,
// where it was wiped a moment later and nobody ever saw it.
if (*(uchar*)BOOT_MEDIA_FLAG_ADDR == BOOT_MEDIA_CD && !g_from_cd) {
draw_centered(g_height / 2, "No readable boot disc found.", SCH_ACCENT);
draw_centered(g_height / 2 + 20, "The drive did not answer ATAPI.", SCH_DIM);
for (;;)
asm volatile("hlt");
}
if (g_have_fb)
progress_start("Detecting kernel...", 16384);
// Read 32 sectors (16KB) looking for the multiboot header.
readseg((uchar*)x, 16384, 0);
for (n = 0; n < 16384/4; n++) {
if (x[n] == 0x1BADB002) {
if ((x[n] + x[n+1] + x[n+2]) == 0) {
hdr = (struct mbheader *) (x + n);
if (g_have_fb)
progress_start("Loading kernel...", hdr->load_end_addr - hdr->load_addr);
// Load the actual kernel segments
readseg((uchar*) hdr->load_addr,
hdr->load_end_addr - hdr->load_addr,
(n * 4) - (hdr->header_addr - hdr->load_addr));
// Zero out BSS
if (hdr->bss_end_addr > hdr->load_end_addr)
stosb((void*) hdr->load_end_addr, 0, hdr->bss_end_addr - hdr->load_end_addr);
if (g_have_fb)
progress_label("Starting Schunix...");
// Jump to kernel entry. NOT a normal C call: kernel/arch/x86_64/
// entry.S's mboot_entry reads its "argument" (the framebuffer
// physical address) out of EAX directly, not off the stack — a
// multiboot-style raw-register handoff, not cdecl. A plain
// `((void(*)(uint32))entry)(fbaddr)` pushes fbaddr on the stack
// per cdecl and leaves EAX as whatever happened to be lying around;
// it only ever "worked" because simpler bootmain.c code coincidentally
// left the right value in EAX as a side effect of its own codegen.
// Adding the boot-menu/progress-bar code changed that codegen and
// broke it silently (kernel received garbage instead of the real
// LFB address). Pin it down explicitly instead of hoping.
{
uint32 fbaddr = *(uint32*)(VBE_INFO_ADDR + 40);
uint32 entry = hdr->entry_addr;
asm volatile("jmp *%0" : : "r"(entry), "a"(fbaddr));
}
}
}
}
}
// Bounded, because there might not be a disk there at all.
//
// This used to be `while((inb(0x1F7) & 0xC0) != 0x40);`. On a machine with no
// ATA drive on the primary channel the status register reads 0xFF forever and
// the loader hung on a splash screen with no message and no way to tell what
// had gone wrong -- which is exactly what happened the first time a CD boot
// fell back to this path. A bounded wait turns that into a failed read, and
// stage2main() says so on screen.
void waitdisk(void) {
for (int i = 0; i < 4000000; i++)
if ((inb(0x1F7) & 0xC0) == 0x40)
return;
}
void readsect_ata(void *dst, uint offset) {
waitdisk();
outb(0x1F2, 1);
outb(0x1F3, offset);
outb(0x1F4, offset >> 8);
outb(0x1F5, offset >> 16);
outb(0x1F6, (offset >> 24) | 0xE0);
outb(0x1F7, 0x20);
waitdisk();
insl(0x1F0, dst, SECTSIZE/4);
}
// ---------------------------------------------------------------------------
// ATAPI -- reading the kernel back off a CD.
//
// An optical drive does not answer the ATA commands above. It speaks the
// packet interface: you hand it a SCSI command descriptor block through the
// data port and it hands back 2048-byte logical blocks. The register file and
// the PIO loop are otherwise the same, which is why this lives here rather
// than forcing the loader back into real mode to use INT 13h -- by the time
// the kernel is read, stage2.S has long since switched to protected mode and
// the BIOS is gone.
// ---------------------------------------------------------------------------
static void bcopy(void *dst, const void *src, uint n) {
uchar *d = dst;
const uchar *s = src;
while (n--) *d++ = *s++;
}
// A channel has to be reset before its signature means anything.
//
// ATAPI devices report themselves by leaving 0x14 in LBA-mid and 0xEB in
// LBA-high -- but only until something issues a command, after which those
// registers hold that command's byte count instead. The BIOS has just
// finished reading this very disc in order to load us, so on a CD boot the
// signature is *always* stale by the time we look. Resetting the channel is
// what puts it back.
static void atapi_reset(int io) {
int ctrl = io + 0x206; // 0x1F0 -> 0x3F6, 0x170 -> 0x376
outb(ctrl, 0x04); // SRST
for (int i = 0; i < 10000; i++) // hold it well past the required 5us
inb(ctrl);
outb(ctrl, 0x00);
for (int i = 0; i < 1000000; i++) // bounded: an empty channel reads 0xFF
if (!(inb(io + 7) & 0x80)) // and would otherwise spin forever
break;
}
static int atapi_find(void) {
static const int bases[2] = { 0x1F0, 0x170 };
for (int b = 0; b < 2; b++) {
atapi_reset(bases[b]);
for (int s = 0; s < 2; s++) {
outb(bases[b] + 6, s ? 0xB0 : 0xA0);
// Four throwaway status reads: the standard 400ns settle after a drive
// select, before the register file is meaningful.
inb(bases[b] + 7); inb(bases[b] + 7);
inb(bases[b] + 7); inb(bases[b] + 7);
if (inb(bases[b] + 4) == 0x14 && inb(bases[b] + 5) == 0xEB) {
g_atapi_io = bases[b];
g_atapi_slave = s;
return 1;
}
}
}
return 0;
}
// One 2048-byte logical block. Returns 0 on success.
static int atapi_read_block(uint lba, void *dst) {
int io = g_atapi_io;
uchar cdb[12];
int i;
outb(io + 6, g_atapi_slave ? 0xB0 : 0xA0);
inb(io + 7); inb(io + 7); inb(io + 7); inb(io + 7);
outb(io + 1, 0); // features: PIO, no DMA
outb(io + 4, CD_SECTSIZE & 0xFF); // how much we are willing to take
outb(io + 5, CD_SECTSIZE >> 8);
outb(io + 7, 0xA0); // PACKET
// Wait for the drive to ask for the command packet: BSY clear, DRQ set.
for (i = 0; i < 0x1000000; i++) {
uchar st = inb(io + 7);
if (st & 0x01) return -1; // ERR
if (!(st & 0x80) && (st & 0x08)) break;
}
if (i == 0x1000000) return -1;
// READ(12). Big-endian LBA and transfer length, because SCSI is.
for (i = 0; i < 12; i++) cdb[i] = 0;
cdb[0] = 0xA8;
cdb[2] = (uchar)(lba >> 24);
cdb[3] = (uchar)(lba >> 16);
cdb[4] = (uchar)(lba >> 8);
cdb[5] = (uchar)lba;
cdb[9] = 1; // one block
outsl(io, cdb, 3); // 12 bytes = 3 dwords
// Wait for the data itself.
for (i = 0; i < 0x1000000; i++) {
uchar st = inb(io + 7);
if (st & 0x01) return -1;
if (!(st & 0x80) && (st & 0x08)) break;
}
if (i == 0x1000000) return -1;
insl(io, dst, CD_SECTSIZE / 4);
return 0;
}
// Where our boot image sits on the disc.
//
// Sector numbers on a CD are absolute, but readseg() counts from the start of
// the boot image -- which on a hard disk is the start of the disk and on a CD
// is wherever the ISO's filesystem happened to leave room. El Torito writes
// that address down, so we read it rather than guess:
//
// LBA 17 Boot Record Volume Descriptor, holding a pointer at offset 0x47
// catalog validation entry (32 bytes), then the default entry, whose
// "Load RBA" at offset 8 is the boot image's LBA
//
// Returns 0 if the disc does not look like something we booted from.
static int atapi_boot_base(void) {
static uchar buf[CD_SECTSIZE];
uint catalog, image;
if (atapi_read_block(17, buf) != 0)
return 0;
if (buf[0] != 0 || buf[1] != 'C' || buf[2] != 'D' || buf[3] != '0' ||
buf[4] != '0' || buf[5] != '1')
return 0;
catalog = (uint)buf[0x47] | ((uint)buf[0x48] << 8) |
((uint)buf[0x49] << 16) | ((uint)buf[0x4A] << 24);
if (catalog == 0)
return 0;
if (atapi_read_block(catalog, buf) != 0)
return 0;
if (buf[32] != 0x88) // default entry not marked bootable
return 0;
image = (uint)buf[40] | ((uint)buf[41] << 8) |
((uint)buf[42] << 16) | ((uint)buf[43] << 24);
return image;
}
// A CD block holds four 512-byte sectors, and readseg() asks for them one at
// a time and in order -- so caching the last block turns four reads into one.
// Without it every byte of the kernel would be fetched four times.
static uchar g_cd_cache[CD_SECTSIZE];
static uint g_cd_cached = 0xFFFFFFFF;
static void readsect_atapi(void *dst, uint offset) {
uint abs = offset + g_cd_base512;
uint blk = abs / (CD_SECTSIZE / SECTSIZE);
uint sub = abs % (CD_SECTSIZE / SECTSIZE);
if (blk != g_cd_cached) {
if (atapi_read_block(blk, g_cd_cache) != 0) {
// Nothing useful to do here: there is no console yet and no way back.
// Leaving the buffer alone means the multiboot search simply fails to
// match, which stops with the splash up rather than jumping to garbage.
return;
}
g_cd_cached = blk;
}
bcopy(dst, g_cd_cache + sub * SECTSIZE, SECTSIZE);
}
void readsect(void *dst, uint offset) {
if (g_from_cd)
readsect_atapi(dst, offset);
else
readsect_ata(dst, offset);
progress_tick(SECTSIZE);
}
void readseg(uchar* pa, uint count, uint offset) {
uchar* epa = pa + count;
pa -= offset % SECTSIZE;
// Kernel starts right after stage2 (1 boot sector + STAGE2_SECTORS
// stage2 sectors), not immediately after just the boot sector.
offset = (offset / SECTSIZE) + (1 + STAGE2_SECTORS);
for(; pa < epa; pa += SECTSIZE, offset++)
readsect(pa, offset);
}

219
cmd/fbinfo/main.c Normal file
View file

@ -0,0 +1,219 @@
// fbinfo -- ask /dev/fb0 what it is, and optionally draw on it.
//
// This exists because the framebuffer is the one device whose correctness you
// cannot check by reading kernel source: the numbers have to come back from
// the hardware the bootloader actually programmed. Three places in the kernel
// used to assume the framebuffer lived at 0xfd000000 and was 800x600x32; they
// agreed with QEMU's stdvga by luck, and nothing would have told us when the
// luck ran out. Now `fbinfo` would.
//
// fbinfo print the geometry
// fbinfo fill draw a gradient over the whole screen through mmap
// fbinfo bars draw colour bars through write(), no mmap involved
//
// The two drawing modes take different paths on purpose. If `fill` works and
// `bars` does not, the mapping is right and the device's read/write is wrong;
// if it is the other way round, the reverse. Having both makes a failure say
// which half broke.
#include "types.h"
#include "user.h"
#include "fcntl.h"
#include "fb.h"
#define PROT_READ 0x1
#define PROT_WRITE 0x2
#define MAP_SHARED 0x01
static void
puts(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void*)s, n);
}
// Unsigned decimal. No printf in lib/, and pulling one in for six numbers
// would be a worse trade than fourteen lines.
static void
putu(unsigned long v)
{
char buf[24];
int i = 0;
if (v == 0) {
write(1, "0", 1);
return;
}
while (v > 0 && i < (int)sizeof(buf)) {
buf[i++] = (char)('0' + (v % 10));
v /= 10;
}
while (i > 0) {
i--;
write(1, &buf[i], 1);
}
}
static void
puthex(unsigned long v)
{
static const char d[] = "0123456789abcdef";
char buf[16];
int i = 0;
write(1, "0x", 2);
if (v == 0) {
write(1, "0", 1);
return;
}
while (v > 0 && i < (int)sizeof(buf)) {
buf[i++] = d[v & 0xf];
v >>= 4;
}
while (i > 0) {
i--;
write(1, &buf[i], 1);
}
}
// Name, then spaces out to a fixed column. Padding inside each string
// literal is what produced "bits_per_pixel32": the longest name happened to
// be exactly as wide as the column, so it got no separator at all and the
// value ran straight into it.
static void
label(const char *name)
{
int n = 0;
while (name[n]) n++;
write(1, (void*)name, n);
while (n < 16) { write(1, " ", 1); n++; }
}
static void
field(const char *name, unsigned long v)
{
label(name);
putu(v);
puts("\n");
}
static void
chanline(const char *name, struct fb_bitfield *b)
{
label(name);
puts("offset ");
putu(b->offset);
puts(", length ");
putu(b->length);
if (b->length == 0)
puts(" (absent)");
puts("\n");
}
int
main(int argc, char *argv[])
{
struct fb_var_screeninfo var;
struct fb_fix_screeninfo fix;
int fd;
fd = open("/dev/fb0", O_RDWR);
if (fd < 0) {
puts("fbinfo: cannot open /dev/fb0\n");
return 1;
}
if (ioctl(fd, FBIOGET_VSCREENINFO, &var) < 0) {
puts("fbinfo: FBIOGET_VSCREENINFO failed\n");
return 1;
}
if (ioctl(fd, FBIOGET_FSCREENINFO, &fix) < 0) {
puts("fbinfo: FBIOGET_FSCREENINFO failed\n");
return 1;
}
if (argc < 2) {
label("id");
puts(fix.id);
puts("\n");
field("xres", var.xres);
field("yres", var.yres);
field("bits_per_pixel", var.bits_per_pixel);
label("smem_start");
puthex(fix.smem_start);
puts("\n");
field("smem_len", fix.smem_len);
field("line_length", fix.line_length);
// The stride is the number worth staring at. A card is free to pad
// each row, and a program that computes row starts as
// y * xres * bytes_per_pixel instead of y * line_length draws a
// picture that shears progressively down the screen.
if (fix.line_length != var.xres * (var.bits_per_pixel / 8)) {
label("note");
puts("stride is padded; rows are not xres*bpp/8 apart\n");
}
chanline("red", &var.red);
chanline("green", &var.green);
chanline("blue", &var.blue);
chanline("transp", &var.transp);
return 0;
}
if (argv[1][0] == 'f') { // fill
unsigned char *fb = mmap(0, fix.smem_len,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (fb == 0 || (long)fb < 0) {
puts("fbinfo: mmap failed\n");
return 1;
}
unsigned bpp = var.bits_per_pixel / 8;
for (unsigned y = 0; y < var.yres; y++) {
unsigned char *row = fb + (unsigned long)y * fix.line_length;
for (unsigned x = 0; x < var.xres; x++) {
unsigned r = (x * 255) / (var.xres ? var.xres : 1);
unsigned g = (y * 255) / (var.yres ? var.yres : 1);
unsigned long px = ((unsigned long)r << var.red.offset) |
((unsigned long)g << var.green.offset) |
((unsigned long)0x80 << var.blue.offset);
unsigned char *p = row + (unsigned long)x * bpp;
for (unsigned b = 0; b < bpp; b++)
p[b] = (unsigned char)(px >> (b * 8));
}
}
puts("fbinfo: gradient drawn through mmap\n");
return 0;
}
if (argv[1][0] == 'b') { // bars
// One scanline at a time through write(). No mapping: this is the
// path that used to be devsw's nullwrite, i.e. a silent no-op.
static unsigned char row[8192];
unsigned bpp = var.bits_per_pixel / 8;
unsigned len = fix.line_length;
if (len > sizeof(row))
len = sizeof(row);
for (unsigned y = 0; y < var.yres; y++) {
unsigned band = (y * 8) / (var.yres ? var.yres : 1);
unsigned long px =
((unsigned long)((band & 1) ? 255 : 0) << var.red.offset) |
((unsigned long)((band & 2) ? 255 : 0) << var.green.offset) |
((unsigned long)((band & 4) ? 255 : 0) << var.blue.offset);
for (unsigned x = 0; x * bpp + bpp <= len; x++)
for (unsigned b = 0; b < bpp; b++)
row[x * bpp + b] = (unsigned char)(px >> (b * 8));
if (write(fd, row, len) != (int)len) {
puts("fbinfo: short write to /dev/fb0\n");
return 1;
}
}
puts("fbinfo: colour bars drawn through write()\n");
return 0;
}
puts("usage: fbinfo [fill|bars]\n");
return 1;
}

124
cmd/init/main.c Normal file
View file

@ -0,0 +1,124 @@
// SPDX-License-Identifier: Apache-2.0
// Schunix — minimal init (PID 1)
#include "types.h"
#include "user.h"
#include "fcntl.h"
static void
xputs(int fd, const char *s)
{
int n = 0;
while (s[n]) n++;
write(fd, (void *)s, n);
}
int
main(void)
{
if (getpid() != 1)
exit();
// Open console as fd 0; dup to fd 1 (stdout) and fd 2 (stderr).
if (open("console", O_RDWR) < 0) {
mknod("console", 1, 1);
open("console", O_RDWR);
}
dup(0);
dup(0);
// --- /dev ------------------------------------------------------------
// Minor numbers under major 1 follow Linux's mem-major layout where it
// costs nothing (null=2, zero=5, full=7, random=8, urandom=9).
// /dev/hole (was minor 4) is gone: it backed an abandoned experiment and
// nothing has referenced it since.
mkdir("/dev");
mknod("/dev/console", 1, 1);
mknod("/dev/null", 1, 2);
mknod("/dev/keyboard", 1, 3);
mknod("/dev/zero", 1, 5);
mknod("/dev/full", 1, 7);
mknod("/dev/random", 1, 8);
mknod("/dev/urandom", 1, 9);
mknod("/dev/fb0", 2, 0);
mknod("/dev/mouse", 3, 1);
mknod("/dev/tty", 1, 1); // alias for console — must pass TCGETS ioctl
mknod("/dev/ttyS0", 4, 64);
mknod("/dev/stdin", 1, 1);
mknod("/dev/stdout", 1, 1);
mknod("/dev/stderr", 1, 1);
// Raw input events, Linux evdev layout (kernel/dev/input.c). /dev/keyboard
// and /dev/mouse above are the old character-and-position interfaces; these
// are the ones with key releases and button transitions, which is what a
// game or a compositor needs and what neither of the others can express.
mkdir("/dev/input");
mknod("/dev/input/event0", 7, 0); // keyboard
mknod("/dev/input/event1", 7, 1); // mouse
// --- /proc -----------------------------------------------------------
// Synthesised status files, major 6 (kernel/dev/procfs.c). These are the
// global ones; per-process /proc/<pid>/ directories need synthetic
// directory support in the filesystem, which does not exist yet.
mkdir("/proc");
// /proc is a real filesystem now (kernel/fs/procfs.c), not a handful of
// mknod'd nodes: it has to be, because one directory per process cannot be
// created ahead of time. This is what makes `ps` and `top` work.
//
// The mknod'd global files below still happen, and are simply covered by
// the mount. They stay for now so that a failure here degrades to the old
// behaviour instead of losing /proc entirely.
mount("proc", "/proc", "proc", 0, 0);
mknod("/proc/meminfo", 6, 1);
mknod("/proc/uptime", 6, 2);
mknod("/proc/cpuinfo", 6, 3);
mknod("/proc/version", 6, 4);
mknod("/proc/stat", 6, 5);
mknod("/proc/loadavg", 6, 6);
mknod("/proc/mounts", 6, 7);
mknod("/proc/filesystems", 6, 8);
// SchFS volume summary: label, generation, free space, and the root
// directory. Reachable whether or not the volume is mounted into the
// namespace, which is what makes it useful when a mount has gone wrong.
mknod("/proc/schfs", 6, 9);
// Write 'c' here to crash on purpose. The crash screen is otherwise the
// one part of the system that can only be seen by having a bug.
mknod("/proc/sysrq", 6, 10);
mknod("/proc/pci", 6, 11);
mknod("/proc/interrupts", 6, 12);
mkdir("/sys");
xputs(1, "\nSchunix\n");
xputs(1, "init: ready (pid 1)\n");
for (;;) {
int pid = fork();
if (pid < 0) {
xputs(2, "init: fork failed\n");
sleep(100);
continue;
}
if (pid == 0) {
// exec.c auto-enables Linux ABI for musl binaries (entry >= 0x100000).
// Do NOT call enable_linux_abi() here — it corrupts Schunix syscall
// numbers for the exec/exit calls that follow in this child.
// Standardized naming: the shell is always /bin/sh, regardless of
// which implementation backs it (dash, mksh, etc.) — no ksh/mksh/
// other aliases are looked up here.
static char *argv[] = { "sh", 0 };
exec("/bin/sh", argv);
xputs(2, "init: no shell found; place sh in /bin\n");
exit();
}
// Reap zombies; the loop restarts the shell when it exits.
int wpid;
while ((wpid = wait()) >= 0 && wpid != pid)
;
// Brief pause before restarting so we don't spin on a missing shell.
sleep(100);
}
return 0;
}

124
cmd/inputtest/main.c Normal file
View file

@ -0,0 +1,124 @@
// inputtest -- read raw events from /dev/input/eventN.
//
// The point of the raw devices is everything the console cannot say: which
// key was released, whether one is still held, and where the mouse went. None
// of that shows up in a character stream, so none of it can be tested by
// typing into a shell.
//
// inputtest read the keyboard until Esc is released
// inputtest mouse read the mouse until interrupted
// inputtest -n N stop after N events (used by the boot check)
//
// Prints one line per event: type, code and value, decoded where there is a
// name for it.
#include "types.h"
#include "user.h"
#include "fcntl.h"
#include "input.h"
static void
puts_(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void*)s, n);
}
static void
putn_(int v)
{
char buf[12];
int i = 0, neg = 0;
if (v < 0) { neg = 1; v = -v; }
if (v == 0) buf[i++] = '0';
while (v) { buf[i++] = '0' + (v % 10); v /= 10; }
if (neg) buf[i++] = '-';
while (i--) write(1, &buf[i], 1);
}
static const char *
typename_(int t)
{
switch (t) {
case EV_SYN: return "SYN";
case EV_KEY: return "KEY";
case EV_REL: return "REL";
default: return "?";
}
}
static int
streq(const char *a, const char *b)
{
while (*a && *a == *b) { a++; b++; }
return *a == *b;
}
int
main(int argc, char **argv)
{
const char *path = "/dev/input/event0";
int limit = 0, count = 0, i;
int fd;
struct input_event ev;
for (i = 1; i < argc; i++) {
if (streq(argv[i], "mouse"))
path = "/dev/input/event1";
else if (streq(argv[i], "-n") && i + 1 < argc) {
const char *p = argv[++i];
limit = 0;
while (*p >= '0' && *p <= '9') limit = limit * 10 + (*p++ - '0');
}
}
fd = open((char*)path, O_RDONLY);
if (fd < 0) {
puts_("inputtest: cannot open ");
puts_(path);
puts_("\n");
exit();
}
puts_("inputtest: reading ");
puts_(path);
puts_("\n");
for (;;) {
int n = read(fd, &ev, sizeof(ev));
// A short read would mean the device handed back part of a struct,
// which would desynchronise every event after it. Saying so is more
// use than silently carrying on with garbage.
if (n == 0) { puts_("inputtest: end of stream\n"); break; }
if (n < 0) { puts_("inputtest: read error\n"); break; }
if (n != sizeof(ev)) {
puts_("inputtest: SHORT READ -- events are not whole\n");
break;
}
puts_(" ");
puts_(typename_(ev.type));
puts_(" code ");
putn_(ev.code);
puts_(" value ");
putn_(ev.value);
if (ev.type == EV_KEY)
puts_(ev.value ? " (press)" : " (release)");
puts_("\n");
// Esc released ends the keyboard run, so a person can stop it without
// a signal. The boot check uses -n instead.
if (ev.type == EV_KEY && ev.code == KEY_ESC && ev.value == 0)
break;
if (limit && ++count >= limit) {
puts_("inputtest: event limit reached\n");
break;
}
}
close(fd);
puts_("inputtest: done\n");
exit();
}

112
cmd/shmtest/main.c Normal file
View file

@ -0,0 +1,112 @@
// shmtest -- prove that shared memory is actually shared, and that a
// file-backed mmap returns the file.
//
// Both of these are things you cannot check by reading a return value. shmget
// and shmat returned -ENOSYS until now, and mmap of a file "succeeded" while
// handing back zeroed anonymous pages -- a lie that looks exactly like
// success. The only convincing test is for one process to write and a
// different one to read.
//
// shmtest run every check
//
// Prints one line per check, and a final verdict a script can grep for.
#include "types.h"
#include "user.h"
#include "fcntl.h"
#define PROT_READ 0x1
#define PROT_WRITE 0x2
#define MAP_PRIVATE 0x02
#define MAP_SHARED 0x01
#define IPC_PRIVATE 0
#define IPC_CREAT 01000
static int failures;
static void
puts_(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void*)s, n);
}
static void
check(int ok, const char *what)
{
puts_(ok ? " [ok] " : " FAIL: ");
puts_(what);
puts_("\n");
if (!ok) failures++;
}
int
main(void)
{
// --- shared memory ----------------------------------------------------
int id = shmget(IPC_PRIVATE, 8192, IPC_CREAT | 0600);
check(id >= 0, "shmget created a segment");
if (id < 0) { puts_("shmtest: FAILED\n"); return 1; }
char *p = shmat(id, 0, 0);
check((long)p > 0, "shmat mapped it");
if ((long)p <= 0) { puts_("shmtest: FAILED\n"); return 1; }
// A fresh segment must be zeroed, or it leaks whatever the previous
// tenant left in those pages to whoever gets them next.
int zeroed = 1;
for (int i = 0; i < 8192; i++)
if (p[i] != 0) { zeroed = 0; break; }
check(zeroed, "a new segment reads as zeros");
p[0] = 'P';
p[1] = 0;
int pid = fork();
if (pid == 0) {
// The child inherited the attachment through fork. If the pages had
// been copied instead of shared, the parent would never see this.
p[0] = 'C';
p[100] = 'X';
exit();
}
wait();
check(p[0] == 'C', "a write by the child is visible in the parent");
check(p[100] == 'X', "and so is one further into the segment");
// --- file-backed mmap --------------------------------------------------
int fd = open("/tmp/shmtest.dat", O_CREATE | O_RDWR);
check(fd >= 0, "created a file to map");
if (fd >= 0) {
write(fd, "SCHUNIX-MMAP-OK", 15);
close(fd);
}
fd = open("/tmp/shmtest.dat", O_RDONLY);
if (fd >= 0) {
char *m = mmap(0, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
int ok = ((long)m > 0) &&
m[0]=='S' && m[1]=='C' && m[2]=='H' && m[3]=='U' &&
m[8]=='M' && m[13]=='O' && m[14]=='K';
check(ok, "mmap of a file returns the file, not zeros");
close(fd);
} else {
check(0, "mmap of a file returns the file, not zeros");
}
// MAP_SHARED + PROT_WRITE on a file cannot be honoured without a page
// cache, and is refused rather than quietly downgraded to a private copy.
fd = open("/tmp/shmtest.dat", O_RDWR);
if (fd >= 0) {
void *m = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
check((long)m < 0, "writable MAP_SHARED on a file is refused, not faked");
close(fd);
}
check(shmdt(p) == 0, "shmdt released the attachment");
puts_(failures ? "shmtest: FAILED\n" : "shmtest: all checks passed\n");
return failures ? 1 : 0;
}

305
cmd/socktest/main.c Normal file
View file

@ -0,0 +1,305 @@
// socktest -- prove that Unix domain sockets carry data between processes.
//
// Every part of a socket is invisible from a return value. bind() returning 0
// proves nothing; a socket layer that accepts every call and drops every byte
// would pass a test built on return codes alone. So each check here has one
// process write and a *different* process read, and compares the bytes.
//
// socktest run every check
//
// Prints one line per check and a final verdict a script can grep for.
#include "types.h"
#include "user.h"
#include "socket.h"
static int failures;
static void
puts_(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void*)s, n);
}
static void
putn_(int v)
{
char buf[12];
int i = 0, neg = 0;
if (v < 0) { neg = 1; v = -v; }
if (v == 0) buf[i++] = '0';
while (v) { buf[i++] = '0' + (v % 10); v /= 10; }
if (neg) buf[i++] = '-';
while (i--) write(1, &buf[i], 1);
}
static void
check(int ok, const char *what)
{
puts_(ok ? " [ok] " : " FAIL: ");
puts_(what);
puts_("\n");
if (!ok) failures++;
}
static int
streq(const char *a, const char *b)
{
while (*a && *a == *b) { a++; b++; }
return *a == *b;
}
// Fill a sockaddr_un and return the length to pass to bind/connect: the
// family plus the path plus its NUL, which is what musl computes and what the
// kernel side is written to expect.
static int
mkaddr(struct sockaddr_un *sa, const char *path)
{
int i = 0;
for (i = 0; i < UNIX_PATH_MAX - 1 && path[i]; i++)
sa->sun_path[i] = path[i];
sa->sun_path[i] = '\0';
sa->sun_family = AF_UNIX;
return (int)sizeof(sa->sun_family) + i + 1;
}
// ---------------------------------------------------------------------------
// 1. A round trip over a named socket, between two processes.
// ---------------------------------------------------------------------------
static void
test_roundtrip(void)
{
struct sockaddr_un sa;
int len = mkaddr(&sa, "/tmp/socktest");
int srv, pid, cli, n;
char buf[64];
puts_("named socket, two processes:\n");
srv = socket(AF_UNIX, SOCK_STREAM, 0);
check(srv >= 0, "socket() returns a descriptor");
if (srv < 0) return;
check(bind(srv, (struct sockaddr *)&sa, len) == 0, "bind() to /tmp/socktest");
check(listen(srv, 4) == 0, "listen()");
// A second bind to the same name must be refused, or two servers can
// silently steal each other's clients.
{
int dup_ = socket(AF_UNIX, SOCK_STREAM, 0);
check(bind(dup_, (struct sockaddr *)&sa, len) < 0,
"a second bind to the same name is refused");
close(dup_);
}
pid = fork();
if (pid == 0) {
// Child: the client. Connects, sends a question, reads the answer.
int c = socket(AF_UNIX, SOCK_STREAM, 0);
char reply[64];
int r;
close(srv);
if (connect(c, (struct sockaddr *)&sa, len) < 0) {
puts_(" FAIL: child connect()\n");
exit();
}
write(c, "ping", 4);
r = read(c, reply, sizeof(reply) - 1);
if (r < 0) r = 0;
reply[r] = '\0';
if (!streq(reply, "pong"))
puts_(" FAIL: child did not receive pong\n");
close(c);
exit();
}
cli = accept(srv, 0, 0);
check(cli >= 0, "accept() returns a connection");
if (cli < 0) { close(srv); wait(); return; }
n = read(cli, buf, sizeof(buf) - 1);
if (n < 0) n = 0;
buf[n] = '\0';
check(streq(buf, "ping"), "server read the client's bytes");
check(write(cli, "pong", 4) == 4, "server replied on the same socket");
close(cli);
close(srv);
wait();
}
// ---------------------------------------------------------------------------
// 2. Errors that must actually be errors.
// ---------------------------------------------------------------------------
static void
test_errors(void)
{
struct sockaddr_un sa;
int len = mkaddr(&sa, "/tmp/nobody-listening");
int s;
puts_("error paths:\n");
s = socket(AF_UNIX, SOCK_STREAM, 0);
check(connect(s, (struct sockaddr *)&sa, len) < 0,
"connect() to a name nobody bound fails");
close(s);
check(socket(AF_INET, SOCK_STREAM, 0) < 0, "AF_INET is refused, not faked");
check(socket(AF_UNIX, SOCK_DGRAM, 0) < 0, "SOCK_DGRAM is refused, not faked");
// read/write on a plain file descriptor must not be mistaken for a socket,
// and socket calls on one must be refused.
check(listen(1, 4) < 0, "listen() on a non-socket fd fails");
}
// ---------------------------------------------------------------------------
// 3. End-of-file. A reader whose peer closes must be told, not left hanging.
// ---------------------------------------------------------------------------
static void
test_eof(void)
{
struct sockaddr_un sa;
int len = mkaddr(&sa, "/tmp/socktest-eof");
int srv, cli, pid, n;
char buf[16];
puts_("end of file:\n");
srv = socket(AF_UNIX, SOCK_STREAM, 0);
bind(srv, (struct sockaddr *)&sa, len);
listen(srv, 4);
pid = fork();
if (pid == 0) {
int c = socket(AF_UNIX, SOCK_STREAM, 0);
close(srv);
connect(c, (struct sockaddr *)&sa, len);
write(c, "x", 1);
close(c); // the peer goes away with the byte still unread
exit();
}
cli = accept(srv, 0, 0);
n = read(cli, buf, sizeof(buf));
check(n == 1, "the byte sent before close() is still delivered");
n = read(cli, buf, sizeof(buf));
check(n == 0, "the next read returns 0 rather than blocking");
close(cli);
close(srv);
wait();
}
// ---------------------------------------------------------------------------
// 4. socketpair: bidirectional, no name, no listener.
// ---------------------------------------------------------------------------
static void
test_socketpair(void)
{
int sv[2], n;
char buf[16];
puts_("socketpair:\n");
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
check(0, "socketpair() succeeds");
return;
}
check(1, "socketpair() succeeds");
write(sv[0], "a", 1);
n = read(sv[1], buf, sizeof(buf));
check(n == 1 && buf[0] == 'a', "0 -> 1 carries a byte");
write(sv[1], "b", 1);
n = read(sv[0], buf, sizeof(buf));
check(n == 1 && buf[0] == 'b', "1 -> 0 carries a byte, on the same pair");
close(sv[0]);
close(sv[1]);
}
// ---------------------------------------------------------------------------
// 5. A message longer than the socket buffer, so the writer has to block and
// the reader has to drain in more than one go.
// ---------------------------------------------------------------------------
#define BIG 12000
static void
test_big(void)
{
int sv[2], pid, got = 0, ok = 1, i;
static char out[BIG], in[BIG];
puts_("a transfer larger than the buffer:\n");
for (i = 0; i < BIG; i++)
out[i] = (char)('A' + (i % 26));
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
check(0, "socketpair() for the big transfer");
return;
}
pid = fork();
if (pid == 0) {
int sent = 0, r;
close(sv[1]);
while (sent < BIG) {
r = write(sv[0], out + sent, BIG - sent);
if (r <= 0) break;
sent += r;
}
close(sv[0]);
exit();
}
close(sv[0]);
while (got < BIG) {
int r = read(sv[1], in + got, BIG - got);
if (r <= 0) break;
got += r;
}
close(sv[1]);
wait();
check(got == BIG, "every byte arrived");
if (got != BIG) {
puts_(" got ");
putn_(got);
puts_(" of ");
putn_(BIG);
puts_("\n");
}
for (i = 0; i < got; i++)
if (in[i] != out[i]) { ok = 0; break; }
check(ok, "and in the right order");
}
int
main(void)
{
puts_("socktest: unix domain sockets\n");
test_roundtrip();
test_errors();
test_eof();
test_socketpair();
test_big();
if (failures == 0)
puts_("socktest: ALL CHECKS PASSED\n");
else {
puts_("socktest: ");
putn_(failures);
puts_(" CHECK(S) FAILED\n");
}
exit();
}

View file

@ -0,0 +1,666 @@
# Kernel history — what broke, and what the fix actually was
Not a design document. This is the record of bugs that were expensive to find
and whose causes are not visible from the code that fixed them, plus the few
subsystem write-ups (bootloader, boot speed, naming) that have nowhere better
to live.
It is worth keeping because every entry here is a thing that looked like a
different problem than it was.
> **Where the old design plan went.** Sections 1-6 of this file used to hold a
> "Segregated Kernel" design: a Ring 0 Alpha / Ring 0 Beta split with driver
> modules isolated by CR3 switching, from an `IDEA.txt` that no longer exists.
> None of it was ever implemented, and it is superseded by
> [schapi.md](schapi.md) — a macOS-shaped system answers driver isolation with
> userspace drivers and a personality boundary, not a bespoke in-kernel ring
> split. It also referenced a Zig toolchain that is now clang/LLVM. Removed
> rather than left to be mistaken for a plan.
---
## 1. Fixed: boot didn't reliably reach a working shell
Investigated 2026-07-05, before phase 1 started (predates this design, not
caused by it). Originally manifested as `ksh` (mksh) crashing on every
launch, apparently inside musl's mallocng allocator (`get_meta`,
`src/malloc/mallocng/meta.h`, deliberately executing `hlt` on detecting
"corrupted" heap metadata — a `trap 13` GPF at a fixed rip). It wasn't a
musl bug. Six real kernel bugs were found and fixed across three
investigation passes, and mksh boots and runs cleanly with all of them in
place — the calloc assertion was a symptom of real memory/pointer-handling
bugs downstream in the kernel, not anything musl or mallocng did wrong.
Fixed:
- `copyuvm()` (`kernel/arch/x86_64/vm.c`, used by `fork()`) never copied
`mmap()`-backed `proc->vma[]` regions into the child — only `[0, sz)`.
Fixed via new `copy_vma_range()`, called from `fork()` (`kernel/proc/proc.c`).
- `kmem.freelist` (`kernel/core/kalloc.c`) could get corrupted by a wild
write elsewhere and would silently hand back a bad pointer, panicking far
from the actual cause. Added a canary check (`check_run()`) at the top of
both `kalloc()`/`kfree()` so corruption panics immediately at the point
it's touched, with a clear message.
- No TLB invalidation existed anywhere except the full `lcr3` reload on
context switch — `mappages()`/`vm_unmap_range()` never called `invlpg`.
Fixed by adding `invlpg()` (`include/x86.h`) to both functions.
- **`MSR_STAR` was misconfigured** (`syscallinit()`, `kernel/arch/x86_64/vm.c`):
`SYSRETQ` computes the returned `cs`/`ss` as `STAR[63:48]+16`/`+8`, which
requires the *compat* 32-bit code selector (`SEG_UCODE32`) in that field —
the kernel was putting the real 64-bit `USER_CS` there instead. Every
`sysretq` silently loaded an invalid `cs` (`0x3b`, pointing at the TSS
descriptor)/`ss` pair; QEMU's TCG doesn't validate segment descriptors on
`SYSRET`, so this went unnoticed until a hardware interrupt forced a
strictly-validated `IRETQ` with that bogus `cs` still loaded, which
correctly `#GP`'d.
- `sys_mmap`'s non-`MAP_FIXED` path (`kernel/sys/sys_mem.c`) assigned a fixed
8MB virtual-address window per `vma[]` slot regardless of the requested
length. A single allocation over 8MB (e.g. `realloc()` growth past that
size) mapped straight into a neighboring live mapping and hit
`mappages()`'s `panic("remap")`. Replaced with a real bump allocator
(`proc->mmap_next`, `include/proc.h`), so every mapping gets its own
disjoint, exactly-sized range regardless of size.
- **`uva2ka()`** (`kernel/arch/x86_64/vm.c`) returned only the page-aligned
base address of a user pointer's page, silently dropping the in-page byte
offset. Direct callers (`sys_ioctl`'s `TIOCGPGRP`/`TCGETS`, `sys_getcwd`,
signal masks, `times()`, `sysinfo()`, ...) read/wrote near the wrong
address whenever the pointer wasn't page-aligned — i.e. almost always.
Fixed at the source so every caller benefits.
- **`fetchint`/`fetchaddr`/`fetchstr`/`argptr`** (`kernel/sys/syscall.c`)
rejected any address `>= proc->sz`, wrongly assuming every valid user
pointer lives in the heap. Stack addresses and `mmap()`'d buffers (used
constantly by real programs, e.g. path-resolution buffers) live above
`proc->sz` and were being rejected outright. Fixed to rely solely on
`uva2ka()`'s page-table validation.
Bringing in toybox (see "Coreutils" below) surfaced four more, all fixed:
- Linux syscall 58 (`vfork`) wasn't in the syscall-ABI translation table at
all (`kernel/sys/linux_abi.c`) — every external command spawn from a
program that prefers `vfork()` over `fork()` failed outright. Mapped to
the existing `fork()` (a fully correct, if slightly slower, substitute).
- `sys_stat`/`fstat`/`fstatat` (`kernel/sys/sys_fs.c`) filled in Schunix's
own native `struct stat` layout regardless of the caller — musl/toybox
parse a fixed Linux ABI byte layout instead, so `st_mode` was garbage and
tools like `ls` couldn't tell files from directories, or executables from
non-executables. Added `struct linux_stat` + `stati_linux()`, gated on
Linux-ABI mode (`proc->syscall_map`).
- `sys_open`/`sys_openat` required `omode == O_RDONLY` *exactly* to open a
directory, but `opendir()`-style calls legitimately OR in `O_DIRECTORY`/
`O_CLOEXEC` alongside it. Fixed to check just the access-mode bits
(`O_ACCMODE`, added to `include/fcntl.h`).
- `sys_getdents64` read one `struct dirent` (16 bytes) at a time — a full
`bmap()`+`bread()` per slot. Directories here are hash-indexed (see
`tree_search()`/`tree_expand()` below), so a directory with a few dozen
real entries can have a size in the hundreds of KB, nearly all empty
slots — this made listing such a directory look hung (it wasn't a
deadlock, just thousands of redundant single-slot disk reads). Rewrote it
to read a full block at a time and resolve each entry's type via
`iget(inum)` (exported from `fs.c`, it was `static`) instead of
re-searching the whole directory by name with `dirlookup()` for every
entry. That rewrite's first version introduced a **new** self-deadlock:
the "." entry's inode number equals the directory's own, and `iget()`
correctly hands back the *same, already-locked* `struct inode*` — calling
`ilock()` on it again sleeps forever on a lock the process already holds.
Fixed by special-casing `xde->inum == ip->inum` (we already know it's a
directory, no need to re-lock at all).
A synthetic stress test (interleaved malloc/free, realloc growth crossing
`MMAP_THRESHOLD`, calloc, large allocations) runs to completion with zero
crashes, and mksh now boots straight to an interactive prompt, runs external
programs, pipes, and `ls`/`mkdir`/`grep` without issue.
## 2. Naming standardization
Schunix standardizes on exactly one name per role, regardless of the
underlying implementation: the shell is always installed and exec'd as
`/bin/sh` (currently mksh, built from `ports/mksh/`), and PID 1 is always
`/bin/init`. `cmd/init/main.c`'s exec loop only ever tries `/bin/sh` — no
ksh/mksh/dash/zsh/other-name fallbacks. dash was tried first
(BSD-licensed, minimal) while the boot crash above was still open, then
replaced with mksh once the underlying kernel bugs were fixed and dash's
lack of interactive features (history, completion, line editing) made it
feel unusably bare for day-to-day use; the dash port's source tree was left in
place rather than deleted, in case it's useful again (e.g. as a `/bin/sh`
POSIX-strict target distinct from an interactive login shell).
## 3. Coreutils
`ports/toybox/` (0BSD-licensed, https://landley.net/toybox/) provides `ls`,
`cat`, `cp`, `mv`, `rm`, `mkdir`, `grep`, `sed`, `find`, `chmod`, `date`,
`sort`, and more — see the `APPLETS` list in `ports/toybox/Makefile`. It builds
as a single multicall binary; `tools/mkfs.c` doesn't understand symlinks or
hardlink dedup, so `ports/toybox/Makefile`'s `install` target copies the built
binary once per applet name into `sysroot/bin/` rather than symlinking
(costs disk space per applet — currently ~44MB total — instead of one
shared inode, but needed no mkfs changes).
## 4. Two-stage bootloader + PSOGM splash
`boot/` used to be a single BIOS boot sector doing everything (VBE mode
set, A20, GDT, protected-mode switch, multiboot kernel scan/load) — silent,
and packed to within 4 bytes of the hard 510-byte MBR limit, so there was
no room to add so much as a print statement.
Split into two stages, the standard way real bootloaders solve this:
- **Stage 1** (`boot/bootasm.S`, still the literal MBR sector, still
≤510 bytes) does exactly one thing: raw IDE PIO (LBA28) reads
`STAGE2_SECTORS` (16) sectors starting at disk sector 1 into memory at
`0x7E00`, then jumps there. Nothing else fits, nothing else is needed.
(Caught a real bug writing this: `outb $0x1F7,%al`-style immediates
silently truncate to 8 bits — ports 0x1F2-0x1F7 are all > 0xFF and need
the `%dx` register form, or you're hitting the wrong I/O port entirely.)
- **Stage 2** (`boot/stage2.S` + `boot/stage2main.c`, loaded at `0x7E00`,
occupies disk sectors 1-16) does everything stage 1 used to skip straight
to, plus a boot banner (BIOS teletype, real mode, before the VBE mode
switch — note this only reaches the video device, not serial, so it
won't show up in a `-serial file:...`-captured log), a splash screen, a
real-time loading bar, and an F8 boot menu (all described below). Not
size-constrained the same way as stage 1 — `tools/padstage2.pl` zero-pads
it out to a fixed `STAGE2_SECTORS*512` (8192) bytes so the kernel's
on-disk starting sector stays predictable regardless of stage2's actual
compiled size, and errors out (like `tools/sign.pl` does for stage 1) if
it ever grows past that budget instead of silently corrupting the layout.
The kernel now starts at disk sector 17 instead of 1 (`schunix.img`'s `dd
... seek=17` in the root `Makefile`, and the equivalent `+(1+STAGE2_SECTORS)`
in `stage2main.c`'s `readseg()` — all three places that encode
`STAGE2_SECTORS` have to agree, or the kernel won't be found on disk at
all). `boot/bootmain.c` no longer exists; its logic was split between
`stage2.S` (real-mode/protected-mode setup) and `stage2main.c` (splash +
the unchanged multiboot-header-scan-and-load logic).
The VBE mode info block itself lives at `0x30000`, not the classic
single-stage bootloader's `0x9000` — stage2's own code now runs from
`0x7E00` up to nearly `0x9000` once the boot-menu/progress-bar logic is
included, and a block at `0x9000` gets partially overwritten by stage2's
own tail end (found this the hard way: the kernel's `LFB:` cprintf started
reporting a different wrong value on every boot, because it was reading
whatever stage2 code/data now happened to occupy that address).
`kernel/dev/vga.c`'s `vga_init()` has to agree on `0x30000` too, since it
independently re-reads mode fields from the same block.
Splash/progress bar/boot menu rendering is a small standalone pixel
renderer in `stage2main.c` (not the kernel's `vga.c` — paging isn't set up
yet, so the LFB physical address from the VBE block is used as a plain
pointer) reusing the kernel's own font data (`include/font.h`) directly.
It fills the framebuffer with PSOGM's purple (matching
`kernel/core/console.c`'s panic screen) and draws "SCHUNIX" centered, then:
- **Real-time loading bar**: `readsect()` (in `stage2main.c`, not the
kernel) calls `progress_tick()` after every 512-byte disk read, updating
an actual pixel-filled bar against the real total byte count — first a
small one while scanning for the multiboot header, then a second,
properly-sized one once the header's found and the true kernel size is
known, labeled "Loading kernel...". Not simulated/timed — it reflects
real bytes read off disk.
- **F8 boot menu**: `wait_for_f8()` busy-polls the keyboard controller
(`inb(0x64)`/`inb(0x60)`, raw scancodes — real-mode BIOS keyboard calls
aren't available once we're in protected mode) for about a second after
the splash appears. F8 (scancode `0x42`) drops into `boot_menu()`: three
options (boot normal, boot verbose, reboot to firmware setup) navigated
with up/down arrows and Enter, plus a "detected drives" panel from
`ata_present()` — a standard floating-bus presence check (select the
drive, write a pattern to two registers, read it back) on the primary
IDE channel's master/slave (the only channel this project ever actually
puts drives on; QEMU's IDE emulation answers the same test
"successfully" on the unpopulated secondary channel too, so that channel
isn't shown — it'd just be confidently wrong). "Reboot to firmware
setup" pulses the keyboard controller's reset line
(`outb(0x64, 0xFE)`, same technique as `kernel/sys/sys_proc.c`'s
`sys_reboot()`); a real bootloader can't jump into BIOS/UEFI setup
directly, so this is the closest honest equivalent — it gives the user
a fresh POST during which their firmware's own setup-menu key works.
- **Verbose boot**: picking "Boot Schunix (verbose)" writes a flag byte to
`BOOT_VERBOSE_FLAG_ADDR` (`VBE_INFO_ADDR + 0x100`, i.e. just past the VBE
block) before jumping to the kernel. `kernel/core/main.c`'s `kmain()`
reads it into a global `boot_verbose` as its very first action and, if
set, prints a line after every subsystem init call
(`uartearlyinit`, `kinit1`, ..., `sinit`) — real per-step boot logging,
not a canned message.
Two bugs worth remembering if this code gets touched again:
- **Keyboard extended-scancode handling**: `0xE0` (the prefix byte for
arrow keys and friends) also has bit 7 set, the same bit used to detect
key-*release* ("break") codes. A naive "skip anything with bit 7 set,
then check for 0xE0" loop skips the 0xE0 byte itself before ever testing
for it, so extended keys silently never register. The 0xE0 check has to
come before the break-code check, not after.
- **The kernel entry call is not a real C function call.** Both the
original single-stage `bootmain()` and the new `stage2main()` do
`((void(*)(uint32))entry)(fbaddr)` — normal cdecl, which pushes `fbaddr`
on the stack. But `kernel/arch/x86_64/entry.S`'s `mboot_entry` reads its
framebuffer argument out of **EAX** (a multiboot-style raw-register
handoff, then `mov %eax,%ebp` / `mov %ebp,%ebx` / `mov %rbx,%rdi` carries
it through the 32→64-bit transition into `kmain()`'s first argument).
The cdecl call never puts anything in EAX — it "worked" for years purely
because simple bootmain.c code happened, as a side effect of its own
codegen, to leave the right value sitting in EAX right up until the
`call` instruction. Adding the splash/menu/progress-bar code changed
that codegen and broke it silently: the kernel booted fine and reported
a plausible-looking but wrong `LFB:` address (garbage left over in EAX
from unrelated prior code, different on every boot). Fixed by pinning
the argument down explicitly with `asm volatile("jmp *%0" : : "r"(entry),
"a"(fbaddr))` instead of hoping the compiler's register allocation
cooperates. If this code moves around again, re-verify with a screendump
taken *immediately* before the jump compared against the kernel's own
`LFB:` log line — they silently diverging is exactly this bug.
## 5. `*at()` syscalls ignored/rejected real dirfds
`ls -l`/`ls -lR` (and, by the same code path, `find`/`du`/anything using
toybox's `lib/dirtree.c`) would hang on `ls -l` and either hang or silently
misbehave on recursive listings. Root cause: `kernel/sys/sys_fs.c`'s
`*at()` syscalls (`openat`, `fstatat`, `unlinkat`, `renameat`, `linkat`,
`symlinkat`, `mkdirat`, `mknodat`, `readlinkat`, `faccessat`) only handled
two cases for their `dirfd` argument — `AT_FDCWD` (resolve relative to
`proc->cwd`) or an absolute path (resolve from root, ignoring `dirfd`
entirely). A **real** dirfd with a **relative** path — e.g.
`fstatat(open("/bin", O_RDONLY), "ls", &st, ...)`, the standard, efficient
way any directory-tree walker avoids rebuilding full path strings for every
entry — was either rejected outright (`fstatat`, `unlinkat`, ...) or
silently ignored in favor of `proc->cwd` (`openat`, which never checked at
all — worse than rejecting, since it can silently resolve the *wrong*
file if cwd happens to contain a same-named entry).
Fixed by adding a real dirfd-relative path resolver: `kernel/fs/fs.c`'s
`namex()` was refactored into `namex_from(struct inode *start, ...)`,
parameterizing what used to be a hardcoded `idup(proc->cwd)`, with two new
exported entry points, `nameifrom()`/`nameiparentfrom()`. `sys_fs.c` gained
`at_dir(dirfd)` (resolves `AT_FDCWD` or a real fd to a starting inode) and
`at_namei()`/`at_nameiparent()` built on top of it, plus a `createat()`
(parameterizing `create()`'s hardcoded `nameiparent()` call the same way).
Every `*at()` syscall now uses these instead of its own inline
absolute-path/`AT_FDCWD`-only check.
**Known follow-on issue, not yet root-caused**: heavy `ls`/`ls -lR` use
across many commands in one long-running shell session can eventually make
a *later*, unrelated directory hang (reproduced with `ls -lR /var` after
~10 prior commands in the same session; the identical command succeeds
immediately on a fresh boot). Not an inode-table or file-table exhaustion
— both `iget()` and `filealloc()` fail cleanly (panic / return 0
respectively) when full, and neither fired. Smells like a lock or
reference-count leak somewhere in the *at() paths above, or in toybox's
own fd handling, that only manifests after enough accumulated opens/closes.
Next step: instrument `itable`/`ftable` occupancy counts and watch them
across a long `ls`-heavy session to see what's actually climbing.
## 6. Boot speed
Two cheap, real wins in `boot/stage2main.c`, no functional changes:
- `progress_tick()` (called once per 512-byte disk sector) used to trigger
a full progress-bar redraw — border plus fill, a few thousand pixel
writes — on *every* sector. For the ~330KB kernel image that's ~650 full
redraws (the multiboot-header-scan phase adds another ~32) for a bar
that only has 101 visually distinct states (0-100%). Split into
`progress_border()` (drawn once, in `progress_start()`, since the border
never changes) and `progress_fill()`, and `progress_tick()` now skips the
redraw entirely unless the percentage actually changed. This was the
single biggest chunk of the bootloader's own wall-clock time.
- `fill_rect()` (the full-screen splash fill, 480,000 pixels at 800x600,
run at least twice per boot) used to call `draw_pixel()` per pixel, which
recomputes `g_fb + y*stride + x*4` from scratch every time. Now computes
each row's base pointer once and indexes into it.
- The F8 boot-menu wait was shortened from ~1s (`5000000` busy-loop
iterations) to ~0.6s (`3000000`) — flat overhead on every boot whether
or not the menu is wanted, so it's the most direct lever available
without giving up the feature entirely.
## 7. `*at()` syscalls, real signal delivery, and mksh job control (in progress)
`ls -l`/`ls -la` hanging outright turned out to be the same root cause
described in section 11 above (dirfd-relative path resolution) — fixed
and verified extensively (plain `ls`, `ls -l`, `ls -la`, `ls -lR`, piped
output, all confirmed working repeatedly).
A *second*, deeper issue was found investigating a follow-up report
(`cd` with no `HOME` set, then `cd var`, then `ls` hanging): mksh's
interactive job control (`ports/mksh (mksh R59c source): jobs.c`) blocks in
`sigsuspend()` waiting for `SIGCHLD`, expecting its own handler
(`j_sigchld()`) to run, reap the child via `waitpid(-1, &status, WNOHANG)`,
and update its internal job-state tracking. This kernel had **no real
signal delivery at all** — `sys_rt_sigaction()` was a no-op stub, so no
handler was ever invoked for anything, and `sigsuspend()` just slept out
one tick and returned `-EINTR` unconditionally. mksh's wait loop had no
way to ever learn the child had exited, hanging forever.
Real signal delivery was implemented from scratch:
- `struct proc` (`include/proc.h`) gained `sig_handler[32]`/
`sig_restorer[32]` (populated by `sys_rt_sigaction()`,
`kernel/sys/sys_sig.c`), `saved_sigtf` (stashed pre-signal trapframe),
and `in_signal` (re-entrancy guard). `allocproc()` zeroes these for a
freshly claimed ptable slot (they weren't before — a reused slot
silently inherited a previous, unrelated process's handler addresses,
which crashed the *next* process to occupy that slot the moment
anything tried to delivver a signal to it). `fork()` copies handler/
restorer/mask from parent to child (POSIX semantics) but not pending
signals.
- `kernel/proc/proc.c`'s `exit()` sets `SIGCHLD` pending on the parent.
- `kernel/sys/syscall.c`'s `syscall()` calls `deliver_signals(tf)` in its
tail, right before returning to user mode: if there's a pending,
unblocked signal with a registered handler, it redirects the trapframe
to jump to the handler (`rdi`=signal number per SysV ABI, `rip`=handler)
instead of resuming the interrupted code, having first stashed the
original trapframe in `proc->saved_sigtf`.
- The return address is **not** `sig_restorer[sig]` — checked, `SA_RESTORER`
is not defined anywhere in this musl build, so `sigaction()` never fills
in a restorer; it's always 0, and pushing 0 as a return address crashes
with `rip=0` the instant the handler function returns. Instead,
`deliver_signals()` injects a tiny 7-byte trampoline (`mov eax,15;
syscall` — `rt_sigreturn`) directly onto the user's own stack, below the
current `rsp` (clear of the SysV red zone), and points the return
address at that — the standard technique for when the C library doesn't
supply its own restorer.
- `sys_rt_sigreturn()` restores `*proc->tf` from `proc->saved_sigtf`,
clearing `in_signal`.
**A real bug found and fixed along the way**: the first version of
`sys_rt_sigaction()` translated the user's `struct sigaction` pointer
*once* via `uva2ka()` and then did a bulk `memset`+copy across the whole
~152-byte structure. `uva2ka()` only guarantees the *page containing that
address* is mapped, not everything up to some further offset — when the
user's pointer landed close enough to a page boundary, the write ran past
the mapped page into whatever physical page happened to be next in kernel
address space (P2V is a flat offset, so "next page" there has nothing to
do with the user's actual next page), corrupting unrelated kernel memory.
This crashed every freshly `exec`'d `/bin/sh` almost immediately, since
mksh calls `sigaction()` during its own startup. Fixed by only touching
the two individual 8-byte fields actually needed (`sa_handler` at offset
0, `sa_restorer` at offset 144), each through its own `uva2ka()` call.
`sys_waitpid()`/`sys_wait4()` (`kernel/sys/sys_proc.c`) were also rewritten
on top of a new `waitpid(pid, status, options)` in `kernel/proc/proc.c`
(refactored from the old any-child-only, always-blocking `wait()`, which
is now just `waitpid(-1, 0, 0)`): a specific `pid` (not just "any child"),
`WNOHANG` (return 0 immediately instead of blocking), and — importantly —
a genuinely optional status pointer. The old code ran a NULL status
pointer through `argptr()`, which rejects NULL/unmapped addresses outright
(page 0 is deliberately unmapped) — but `waitpid(pid, NULL, WNOHANG)` is
completely standard, and is exactly what mksh's job control does.
**Status: signal delivery mechanically works** — verified directly (a
debug build logged the handler being invoked with the correct trapframe
and `sys_rt_sigreturn()` restoring cleanly, no crash, repeatable). **mksh's
job control still doesn't reliably progress afterward**, including for
plain non-piped foreground commands, not just the original piped-into-head
case. Root cause not yet isolated. Leading suspects, in order of
likelihood:
- `sys_setpgid()`/`sys_getpgid()` are no-op stubs (`kernel/sys/sys_proc.c`)
— `j_sigchld()` (`ports/mksh (mksh R59c source): jobs.c:1344-1348`) has an early-out
keyed on `j->ppid == procpid`, which may never resolve the way mksh
expects without real process-group tracking.
- `getrusage(RUSAGE_CHILDREN, ...)` — called at the top of `j_sigchld()` —
isn't verified to behave sanely under this kernel's implementation
(`kernel/sys/sys_proc.c`'s `sys_getrusage()` only fills in `ru_utime`
from the *calling* process's own tick count, not accumulated child
time; `RUSAGE_CHILDREN` isn't distinguished from `RUSAGE_SELF` at all).
- Possibly something in how `j_sigchld()` matches a reaped `pid` against
its own `job_list` (`ports/mksh (mksh R59c source): jobs.c:1372-1385`) silently fails
to find the job for reasons not yet traced.
This was left here deliberately rather than continuing to iterate blind:
SIGCHLD delivery is left **enabled** (`kernel/proc/proc.c`'s `exit()`)
since disabling it guarantees zero chance of mksh's wait loop ever exiting,
whereas leaving it on at least gives the cases that don't hit the
still-unknown remaining gap a chance to work. Next session should start
by instrumenting `j_sigchld()`'s own control flow (or strace-equivalent
logging in the kernel around `sys_setpgid`/`sys_getrusage`/`sys_waitpid`
calls specifically while reproducing `cd; cd var; ls` or even simpler
`ls -l /bin` followed by a second command) rather than re-deriving the
signal-delivery mechanism, which is now confirmed solid.
---
## 8. Two ABI assumptions that only GCC was keeping true
Found when the build moved from GCC-in-WSL to clang-on-Windows. Neither is a
clang bug; both are places where the code relied on GCC doing something it
never promised, and both had been latent for years.
### `%fs:0` was being used as scratch — it is the TCB self-pointer
`syscall_entry` (`kernel/arch/x86_64/trapasm.S`) stashed `%rax` at `%fs:(0)`
while it fetched `proc` to find the kernel stack. The comment called it a
"per-CPU scratch slot". It is not: the x86-64 TLS ABI reserves the word *at*
the thread pointer to hold the thread pointer, and a compiler may read it to
materialise the TP before applying a variable's offset.
GCC compiles `__thread` access to a direct `%fs:-8` segment reference and never
reads that word, so the clobber was invisible. clang at `-O0` emits
`movq %fs:0, %rax; addq $-8, %rax; (%rax)` — reading back the syscall number
this code had just stored. The first process's first syscall is
`exec` (7), so `proc` resolved to address `7 - 8 = -1` and the kernel
page-faulted recursively until the stack was gone.
Fixed by moving the scratch to `%fs:(8)`. Positive offsets are safe: the TLS
block lives at negative offsets and `seginit` places the thread pointer 2048
bytes into a 4096-byte page.
**How it was found** is the part worth copying next time. Three rounds of
`cprintf` narrowed it to "the word is right at `mpmain`, wrong by the first
trap" and no further — the instrumentation perturbed the timing enough to move
the symptom. A hardware watchpoint through QEMU's gdb stub named the
instruction on the first run:
```
qemu ... -S -gdb tcp::26000
gdb -batch -x tcp.gdb # hbreak mpmain; watch *(unsigned long*)kernel_fs_base
```
`seginit` also now writes the self-pointer, which it never did. That is ABI
-correct regardless and costs one store per CPU.
### `_start` read incoming registers with `register ... asm("rdi")`
`lib/glue.c` captured `argc`/`argv` from `%rdi`/`%rsi` using GCC local register
variables. Both compilers only guarantee that binding for inline-asm operands;
using it to pick up an incoming argument is undefined. clang ignored it and
read two uninitialised stack slots:
```asm
movl -0x4(%rbp), %eax # should have been %edi
movq -0x10(%rbp), %rax # should have been %rsi
```
Every program still ran. `init`, `shmtest` and `socktest` take no arguments and
passed their entire suites; the only symptom in the whole system was
`inputtest -n 6` ignoring its limit. Moved to `lib/start.S`, where reading
incoming registers is expressible and correct — `exec` passes argc/argv in the
SysV argument registers already, so `_start` calls `main` without touching
them.
---
## 9. The boot looked like a hobby OS, and two things were why
Not a bug — a design problem, fixed by looking at a screenshot instead of the
code.
**The display was the kernel log.** Boot printed `pci: 6 devices` and
`vga: display controller 1234:1111, aperture fd000000 (16 MB)` to the screen
the user is looking at. That single fact is most of what separates a hobby
project from a product; every shipping system puts that behind a flag.
`cons_serial_only` already existed for panic, so the fix was to start it *set*.
Everything still goes out the UART — which is where `tools/bootcheck.ps1` reads
it from, so all five suites kept passing untouched — and `console_takeover()`
clears it on the first `exec()`, the moment userspace genuinely starts.
`vga_init` no longer clears the screen, so the splash survives the whole of
boot rather than being wiped a few hundred milliseconds in.
**There were three unrelated designs.** The splash was deep purple with gold
text, deliberately matching the *panic* screen. The console was cream with
near-black text. So a healthy boot was dressed as a crash, then abruptly
became an amber terminal. `include/theme.h` is now the one palette, and the
rule is: **normal states are calm and dark; only failure is loud.** PSOGM keeps
the purple entirely to itself, which is what makes it mean something.
The splash copy went from "Schunix Boot Manager / version 1.0 / Memory: 1023 MB
/ Press F8 for boot menu..." to the product name, a rule in the accent colour,
and the one hint that is actionable — moved to the bottom, where a hint
belongs. No shipping OS names its bootloader on the splash.
### Three latent bugs it flushed out
Retheming meant `stage2main.c` included `types.h` properly for the first time,
and the 32-bit build stopped compiling:
- `_Static_assert(sizeof(uint64) == 8)` fired. `uint64` was `unsigned long`,
which is **4 bytes** in the `-m32` loader — the file even carried a local
`e820_u64` workaround with a comment saying so. Now conditioned on
`__SIZEOF_LONG__` rather than on the platform, which covers LP64, LLP64 *and*
ILP32 correctly.
- `x86.h`'s `stosq` and `xchg` use `rep stosq` and `xchgq`, which do not exist
in 32-bit mode. They only ever compiled because `uint64` was silently
narrowed. Now behind `#ifdef __x86_64__`.
- The progress bar computed `(uint64)done * 100 / total`, a 64-bit divide that
needs libgcc's `__udivdi3` — which a freestanding loader does not link. Now
scaled to KiB and done in 32 bits.
All three had been sitting in a bootloader that worked fine, waiting for
someone to make `uint64` mean what it says.
---
## 10. A full-screen program typed the whole session into the shell
Keys reached `/dev/input/event0` **and** the tty line discipline at the same
time. That is correct for two consumers of one keyboard and completely wrong
for a full-screen program: while it had the display, the shell behind it was
quietly accumulating every keystroke, and the first Enter afterwards ran
whatever had piled up. Arrow keys arrived as CP437 glyphs on the console for
good measure.
The fix is the standard one and it is a *primitive*, not a patch: an exclusive
grab, `EVIOCGRAB`, with Linux's ioctl number. While a device is grabbed its
events still flow to `/dev/input/eventN` but both keyboard drivers withhold the
decoded character from the console.
Three details that matter more than the feature:
- **The grab is released by the devsw `close` hook**, not by the program. A
program that grabs the keyboard and then crashes would otherwise leave the
console permanently deaf, with no way to type the command that fixes it.
- **Modifier state is still tracked while grabbed** (`kbdgetc` keeps updating
shift/caps and only skips the character). Otherwise releasing the grab would
hand the line discipline a stuck Shift.
- **The USB path updates `prev[]` before returning**, or the next report would
look like every held key had just been pressed again.
It is also exactly what Schcom will need, which is why it lives in the input
layer rather than in any one client.
### The near-miss worth remembering
The first attempt appeared to work: the harness passed, every assertion green.
It had not worked at all — the ioctl call was never inserted into the client
source, because a scripted string replacement silently failed to match. The
`grep` that should have caught it printed three hits (a comment, a `#define`,
and the *release* call) and was read as confirmation without counting what
they were.
What actually caught it was reading the transcript rather than the verdict:
four bare `#` prompts after quitting, which is precisely the symptom the change
was supposed to remove. `input_grab()` now announces itself, so "did the grab
happen" is never again a thing anyone has to infer.
---
## 11. `ps` and `top`, and why they never worked
/proc was a set of character devices: init ran `mknod("/proc/uptime", 6, 2)` and
friends, so every file was a real inode on the root filesystem with a
major/minor behind it. That is fine for a fixed list and cannot work at all for
the thing /proc is mostly for -- one directory per process. You cannot mknod a
directory for a process that does not exist yet and will be gone in a minute.
`ps` calls `dirtree_flagread("/proc")`, got a listing with no numbers in it, and
reported nothing. It was never a `ps` problem.
So /proc is now a real filesystem behind the same `struct fs_ops` vtable SchFS
uses (`kernel/fs/procfs.c`, `PROCFS_DEV`), mounted by init with
`mount("proc", "/proc", "proc", ...)`. Nothing is stored: directory listings
are generated from the process table when asked, file contents when read.
Three things worth keeping in mind:
- **The split is deliberate.** `kernel/fs/procfs.c` owns the *namespace* (what
exists); `kernel/dev/procfs.c` keeps the *generators* (what the global files
say). They change for different reasons, so `proc_gen_global()` is the only
thing that crosses between them.
- **procfs never touches ptable.** `proc_snapshot()` copies what is needed out
from under the lock, so a process that exits between the readdir that listed
it and the open that reads it produces a stale line rather than a
use-after-free.
- **Sizes are reported as zero**, exactly as Linux does. A synthetic file has
no length until it is generated, and generating every one on every `stat()`
to fill in a number almost nothing reads would be absurd.
### What the suite caught
Marking every procfs inode read-only broke `echo s > /proc/sysrq`, and three
USB checks failed on it within a minute of the change -- they drive the disk
through sysrq. /proc is read-only *except* sysrq, which is the one deliberate
write target, and the code now says so in one place.
### Still not right
`ps -o comm` prints `[init]` rather than `init`. That field comes from
`/proc/<pid>/exe`, which Linux makes a **symlink** and toybox `readlink()`s;
ours is a regular file, so the readlink fails and toybox brackets the name as
it would for a kernel thread. Default `ps` and `top` output are unaffected.
Fixing it needs symlink support in procfs, which belongs with the rest of the
VFS work rather than as a one-off.
---
## 12. A test program crashed at random, and had been typing its own launch command
Symptom: a full-screen framebuffer program, launched with arguments, worked
four times and then died with
pid 9 <prog>: trap 14 err 4 on cpu 0 rip 0x2269be addr 0x18--kill proc
`err 4` is a user-mode read of a non-present page and `addr 0x18` is a null
pointer at offset 24. `llvm-nm` put `rip` inside the routine that opens the
program's overlay view, which dereferences a pointer the program only fills in
once a level is loaded -- at offset 0x18 on x86-64, because the three 64-bit
words of a base struct sit in front of it. So the program had been asked to open
that view before there was anything to show.
**The technique is the transferable part:** the fault address alone said
"null pointer plus 24", which is nearly useless; `llvm-nm` on the faulting `rip`
turned it into a function name, and the offset then identified the exact field.
Reach for that before adding printfs to a program you did not write.
The cause was ours, and it was visible in the log the whole time: every run
printed a burst of garbage bytes *before* the grab was taken, and the amount
differed each run.
**`/dev/input/eventN` had no notion of being opened.** Events were queued from
boot onwards whether or not anything was listening, into one 256-entry ring
with one read cursor. When the program opened the device it inherited the entire
backlog -- including the keystrokes of the command line that had launched it,
which the shell had already consumed -- and replayed them as input. Whether that
crashed depended on which keys happened to be sitting there, which is exactly
the "sometimes" in the report.
Fixed by giving `struct devsw` an `open` hook to match `close`, and having
`/dev/input`:
- drop the backlog on open (`r = w`) -- a reader starts from *now*;
- only queue events while `opens > 0`, which is what Linux does: an evdev
device with no clients does not accumulate.
Linux gets both for free by giving every open its own client queue. One shared
ring plus "start from now" is the same answer for a single reader, which is the
only case that exists today; per-client queues become necessary the moment
Schcom and a client both want the keyboard.
### Also fixed
`cd` into a file reported "Operation not permitted". It was right to fail, but
`sys_chdir` returned a bare `-1`, so the shell printed whatever errno happened
to be lying around. It returns `-ENOTDIR` now, and `-ENOENT`/`-EFAULT` on the
other two failure paths -- "Not a directory" sends you to the right place,
"Operation not permitted" sends you looking at permissions.

239
docs/architecture/schapi.md Normal file
View file

@ -0,0 +1,239 @@
# SchAPI, and Schunix as a system
Status: **direction agreed, not yet implemented.** Nothing in this document
changes what is being built this week; it changes what everything is being
built *towards*. See §7 for what actually happens now.
---
## 1. The decision
Schunix stops being "a Unix-like kernel with a userland bolted on" and becomes
a **system**: a kernel, a native API, and the services and applications that
sit on it, designed together.
The model is NeXTSTEP/macOS, not Linux. Specifically:
- A kernel that provides mechanism, with **more than one personality** on top.
- **POSIX/Unix is a personality, not the identity.** It stays, completely and
seriously, because it is how you get software. It is not the thing programs
are expected to be written against.
- **SchAPI** is the native interface: higher-level, object-shaped, designed for
the GUI and services rather than retrofitted onto 1970s process semantics.
Two systems are being borrowed from, and they are *different* systems. It is
worth being precise about which, because they disagree:
| Borrowed | From | Why |
|---|---|---|
| Layered architecture, personalities, native API over a Unix substrate | macOS / NeXTSTEP | It is the only mainstream design that got a modern API onto a Unix base without abandoning either |
| Permanent backward compatibility | **Windows** | macOS is emphatically *not* this — Apple dropped 32-bit apps, Carbon, kexts and OpenGL, each time on a deadline |
That mix is coherent, but it is a mix. Taking the architecture from Apple and
the compatibility policy from Microsoft is a deliberate choice, and the rest of
this document is mostly about making that choice survivable — because
compatibility-forever is the single most expensive promise an OS can make, and
Windows pays for it every day.
---
## 2. The one mechanism that makes the promise affordable
**The stable interface is a library, not the syscall table.**
This is the whole trick, and it is what lets macOS keep decade-old binaries
running while freely renumbering its syscalls. On macOS, nothing may issue a
syscall directly; you link `libSystem`. Apple therefore owes compatibility only
at the library boundary, and the kernel stays editable forever.
Linux made the opposite choice — the syscall ABI *is* the contract, "we do not
break userspace" — and consequently every syscall mistake ever made is still
there, with a `2` or a `64` suffixed to the name of its replacement.
So, for Schunix:
```
┌──────────────────────────────────────────────┐
│ applications, SchWM, SchDE │
├──────────────────────────────────────────────┤
│ SchAPI │ POSIX / Linux personality│ ← THE STABLE BOUNDARY
├──────────────────┴───────────────────────────┤
│ libschunix — the only thing that may syscall│
├──────────────────────────────────────────────┤
│ syscall table — PRIVATE, unstable, renumberable
├──────────────────────────────────────────────┤
│ kernel │
└──────────────────────────────────────────────┘
```
Rules that follow, and they are not negotiable if the promise is to hold:
1. **A program that issues `syscall` directly is unsupported.** It may break at
any release. Only `libschunix` may.
2. `libschunix` exports versioned symbols. An entry point's behaviour never
changes; a changed behaviour is a new version of the symbol.
3. The kernel may renumber, merge, split or delete syscalls at will, provided
`libschunix` still presents the same surface.
This *reconciles* with the existing decision to adopt Linux syscall numbering
(`docs/roadmap.md`, and the reason musl/toybox/mksh work at all). Linux
numbering does not go away — it becomes the **Linux personality's** entry path,
which is exactly what it already is in practice. It is a compatibility surface
that happens to be entered via `syscall`, and it is frozen because *Linux*
froze it, not because we did.
---
## 3. Stability tiers
"Nothing can ever be removed" is only affordable if you are precise about what
"anything" covers. Windows' real problem was never the documented API — it was
that undocumented behaviour became load-bearing, and then it was too late.
Every public interface carries exactly one tier:
| Tier | Promise | May change |
|---|---|---|
| `stable` | Forever, from the release that introduced it | Never. Additions only. |
| `provisional` | Until the next release | Freely |
| `internal` | None | Freely, without notice |
**Everything in Schunix today is `provisional`.** The freeze starts at 1.0 and
covers only what is marked `stable` at that moment. This is not a way of
dodging the commitment — it is the only way to make it, because a promise made
now would permanently enshrine, among other things, a `fcntl(F_SETFL)` that
was a documented no-op until last week and a `struct file` whose `O_NONBLOCK`
handling had never been exercised.
Nothing becomes `stable` by being old. It becomes `stable` by someone deciding
it is right and writing that down.
---
## 4. `compat/` — what it actually means
The instinct is right: old interfaces should not litter the current tree. But
moving *source* into a folder does not reduce the *runtime* cost, and the
runtime cost is the one that hurts.
The rule that keeps compatibility cheap:
> **A compatibility entry point must be implementable as a translation into the
> current API. If it cannot be, that is a defect in the current API — fix the
> current API before freezing it.**
So `compat/` holds shims, not parallel implementations. A shim:
- lives in `compat/<release-it-was-current-in>/`,
- is written in terms of today's API and nothing else,
- is never called by new code,
- carries its own tests, which are the only thing that proves the promise is
still being kept.
When a shim *cannot* be expressed as a translation, that is the alarm. It means
the new API lost information the old one carried, and it is far cheaper to
notice that before 1.0 than to maintain two real implementations forever. That
is precisely the trap Win32 fell into.
---
## 5. Taking from the BSDs
`src-trunk/` is a NetBSD checkout, and a FreeBSD one is being added alongside
it. Both are reference material and parts bins; neither is built and neither
may ever end up in an image.
**Two trees is not redundancy.** They are worth having together for three
reasons that a single tree cannot give:
- Where an RFC is ambiguous — and TCP's are, constantly — two independent
implementations agreeing is far stronger evidence than one implementation
asserting.
- Where one is licence-encumbered, the other may not be. NetBSD's
`tcp_input.c` carries a WIDE Project advertising clause that Berkeley's
rescission does not cover; FreeBSD's equivalent may well be 2-clause. See
[../licensing.md](../licensing.md) §3.
- They have opposite biases, and knowing which you are reading matters:
| | FreeBSD | NetBSD |
|---|---|---|
| Stack quality | The better stack in absolute terms — faster, more complete, more actively developed | Solid, conservative, less featureful |
| Extractability | Welded to FreeBSD: `epoch(9)`, `uma`, `netisr`, VIMAGE/vnet, `if_t`, a rich locking vocabulary | Portability *is* the project's identity; assumes far less of its host kernel |
| Use it as | The reference you check yourself against | The code you actually lift |
That table is the same lesson §1 of the roadmap already learned the hard way
with FreeBSD's `sys/dev/usb`: 100k lines welded to newbus, where porting the
USB stack means porting the device framework first. Networking is the same
shape. Read FreeBSD, lift NetBSD, write Schunix.
### Licensing — read this before copying anything
NetBSD is not uniformly licensed: 2-clause, UC 3-clause and a minority of
4-clause-with-advertising files all appear, and only the first two are
comfortable. The per-template rules, the measured counts, and the one-line
check to run before copying a file are in [../licensing.md](../licensing.md)
§3. Do not copy from `src-trunk/` without reading it.
### What is actually worth taking
Ranked by value, not by size:
1. **`sys/net`, `sys/netinet` — the TCP/IP stack.** The largest single win
available. Phase 2 of the roadmap wants ARP/IPv4/ICMP/UDP/TCP, and this is a
mature, clean, readable, 2-clause-heavy implementation of exactly that.
2. **`sys/kern/subr_pool.c` — the pool/slab allocator.** Schunix has `kalloc`
and a page at a time. Every subsystem written since has invented its own
fixed-size table to work around that.
3. **`autoconf` — the driver framework** (`config(1)`, `cfdriver`/`cfattach`,
`device_t`). This is the grown-up version of what `kernel/drivers/registry.c`
and `kernel/dev/irq.c` are hand-rolling. Adopt the *model*; do not try to
port the code wholesale.
4. `sys/kern/subr_*` generally — rbtree, workqueue, condvar, kmem.
Low value: `lib/libc` (musl already fills that role), `sys/dev/usb` (we have a
working xHCI stack written from the spec).
**Take designs before code.** Every file copied is a file to be understood,
relicensed-in-notice, and maintained forever under the §3 promise.
---
## 6. What SchAPI is, concretely
Not yet designed. What is decided about it:
- It is the native interface; POSIX is available beneath it but not the idiom.
- It is reached through `libschunix`, never by direct syscall.
- It is designed *with* Schcom in mind, because a compositor and its clients
are the first real users and their needs (IPC, shared buffers, event streams,
object lifetimes) are what will shape it.
- The IPC primitive comes first. Everything in a NeXT-shaped system rests on
it, and unix domain sockets — already built and working — are the substrate,
not the interface.
What is explicitly *not* decided: whether it is object-oriented in the Objective-C
sense, what language it is expressed in, and whether it takes Mach's port model
or something smaller. Those need their own document, written when Schcom has
told us what it actually needs.
---
## 7. What changes now
Almost nothing, on purpose. The ordering does not change:
- **The native Windows build must boot first.** The clang-built kernel
currently reaches `sinit` and dies on the first trap. A direction is worth
nothing while the tree does not run.
- Then the roadmap continues as written: network, GPU, fonts, Schcom.
What this document changes *today* is only this:
1. `src-trunk/` is reference material, excluded from every build and image.
2. New interfaces are marked `provisional` from now on, so that §3 has
something to work with at 1.0.
3. When the network stack is written, NetBSD's is the starting point rather
than a blank file — with the licence check in §5 applied per file.
4. `libschunix` is created when there is a second personality to justify it.
Until then, the *rule* stands even though the library does not exist yet:
no new first-party program issues raw syscalls.

403
docs/architecture/schfs.md Normal file
View file

@ -0,0 +1,403 @@
# SchFS — design
**Status: phases 1 and 2 are done.** The copy-on-write B-tree, the volume layer,
the object layer, a working image writer and a checker all exist, and the kernel
mounts a SchFS volume read-only at boot. Phase 3 (writes and transaction groups)
is next. See §8 for exactly what is built and how to run its tests.
Supersedes the on-disk format described in `include/fs.h`. Keeps the name SchFS;
the on-disk magic changes so v1 images are detected and rejected rather than
silently misread.
The brief was "the best aspects of BeFS, ZFS and btrfs." Those three disagree
with each other in places, so this document says explicitly what is taken from
each and what is deliberately left behind.
## 1. What each one is actually good at
**ZFS — never be inconsistent.** Copy-on-write: no block that is part of the
live tree is ever overwritten. An update writes new blocks, then atomically
flips a single root pointer. A crash therefore lands on either the old tree or
the new one — never a half-written mix. Consequence: **no fsck**, ever. Paired
with end-to-end checksums where a block's checksum is stored in its *parent*
pointer, not in the block itself, which makes the whole tree self-validating and
catches misdirected writes and bit rot that a self-stored checksum cannot.
**btrfs — one data structure, reused everywhere.** btrfs has essentially one
engine: a copy-on-write B-tree. Inodes, directory entries, extent maps, free
space, and snapshots are all just different key types in that same tree. For a
small team this is the single most important idea in the whole design: you write
and debug *one* tricky data structure, then get five subsystems from it. Also
takes extents with **reference counts**, which is what makes snapshots and
reflink copies fall out almost for free.
**BeFS — the part that feels like a database, at a size one person can build.**
BeFS is a 64-bit journaled filesystem whose distinguishing feature is B+tree
**indexes over file attributes**, with a query interface on top. That is the
"SQL-like" capability, and unlike WinFS it actually shipped. Critically, BeFS is
also the right *scale* model: a small team, a readable codebase, and a book
(Giampaolo, *Practical File System Design*, free online) that describes it
end to end.
## 2. What is deliberately not taken
- **No ZFS storage pools / RAID-Z / vdevs.** Multi-disk redundancy is a large
subsystem that solves a problem Schunix does not have yet.
- **No btrfs-style rebalancing or online format conversion.** Enormous
complexity, and it is the historical source of btrfs's worst bugs.
- **No actual SQL.** No parser, no query planner, no relational engine. See §6 —
the useful part of "database filesystem" is the *index*, not the language.
WinFS spent 2003-2006 and a large team on the other interpretation and
shipped nothing.
- **No deduplication.** ZFS's dedup is famous for needing more RAM than anyone
expects. Extent refcounts give reflink/snapshot benefits without it.
## 3. On-disk shape
```
+--------------------+ block 0 boot block (untouched by the FS)
+--------------------+ blocks 1..N uberblock array (128 slots, rotated)
+--------------------+
| CoW B-tree | one tree, many key types:
| | (ino, INODE, 0) -> inode record
| | (ino, DIRENT, hash) -> directory entry
| | (ino, EXTENT, offset) -> extent mapping
| | (ino, ATTR, nameid) -> extended attribute
| | (idx, INDEX, key) -> attribute index entry
+--------------------+
| free space | extent-based, tracked as tree keys, not a bitmap
+--------------------+
```
**Uberblock array (from ZFS).** 128 slots written round-robin, each carrying a
transaction number and a checksum. Mount scans all slots and takes the
highest-numbered one that validates. This is what makes crash recovery
"pick the newest good root" instead of "replay a journal."
**Everything is one B-tree (from btrfs).** Keyed by
`(objectid, type, offset)`, 24 bytes. Adding a feature usually means adding a
key type, not a new on-disk structure.
**Checksums live in the parent (from ZFS).** Each internal node stores, for
every child pointer, that child's CRC32c. A block that is read is verified
against the pointer that led to it. v1 already chose CRC32c and already has
checksum fields in `dinode` and `superblock` — same algorithm, better placement.
### Widths — every v1 limit that has to die
| v1 | limit | v2 |
|---|---|---|
| `dirent.inum` is `ushort` | 65,535 files, total | `uint64` object ids |
| `dinode.size` is `uint` | 4 GB max file | `uint64` |
| `uint` block numbers, BSIZE 1024 | 4 TB volume | `uint64` blocks, 4096-byte |
| `DIRSIZ` 14 | 14-char filenames | variable, up to 255 |
| fixed hash table per dir | every dir ≥113 KB, capped at 119 KB | B+tree, pay per entry |
| `addrs[NDIRECT+3]` | block-at-a-time | extents with refcounts |
Block size moves 1024 → 4096 to match modern media and make extents worthwhile.
## 4. What already exists and gets reused
Not starting from zero:
- **`struct schfs_extent`** is already defined in `include/fs.h` and unused.
The extent concept is already sketched; v2 gives it refcounts and 64-bit
fields.
- **CRC32c checksum fields** already exist in `dinode` and `superblock`, and
mkfs already computes them. Same algorithm, relocated into parent pointers.
- **`kernel/fs/log.c`** is a working transaction log with `begin_op`/`end_op`
call sites throughout the FS. Under CoW the log is not needed for consistency,
but `begin_op`/`end_op` are exactly the right places to bracket a transaction
group, so the *call sites* survive even though the implementation is replaced.
- **The buffer cache** (`bio.c`) is format-agnostic and needs no change beyond
the block-size constant.
## 5. Phasing
Each phase leaves a bootable system. Do not start phase 2 before phase 1 is
solid — the B-tree is the load-bearing piece and everything else assumes it.
**Phase 1 — the B-tree, in userland first.** Write the CoW B-tree as a plain C
library with a block-device abstraction, and test it on a *host* file with a
fuzz/torture harness (random insert/delete/lookup, simulated crash at every
write point, verify the tree always mounts). This is the one component where a
subtle bug is unfixable later, and testing it in the kernel is far harder. It is
also portable straight into `tools/mkfs`.
**Phase 2 — read-only mount. DONE.** New magic, uberblock scan, tree walk, inode
and dirent key types. `tools/mkschfs` writes images; the kernel only reads them.
The system boots from a ramdisk root (see `docs/layout.md` and the ramdisk work)
so a broken disk cannot stop you from booting.
Went further than planned in two places, both cheaply: data blocks carry
checksums too (a `CSUM` key type, as btrfs does — without it "end-to-end
checksums" would cover only metadata, which is the part least likely to rot),
and extents landed early because reserving a file's blocks in one go was
simpler than not doing it.
Not yet done: SchFS files are not reachable through `open(2)`. The mount is
observable through `/proc/schfs` and through the kernel API in `schfs_int.h`,
but wiring it into `namei`/`readi` means the VFS work that phase 3 needs anyway.
**Phase 3 — writes, transaction groups. DONE.** Every `schfs_k*` call that
changes anything is one transaction group, committed by writing a new uberblock
before it returns. The full write API — create, link, unlink, rename, write at
an arbitrary offset, truncate — lives in `objw.c` and is exercised by
`tools/schfs_objtest.c`.
Two things went differently from the plan above, both for reasons worth
recording.
*Free space is not tracked as tree keys.* Allocating from a free map that lives
in the tree mutates the tree, which allocates, which changes the map; btrfs
resolves that with a fixpoint loop and ZFS with log-structured space maps. SchFS
sidesteps it: the committed tree already says exactly which blocks are live, so
the map is **derived** by walking the tree once at mount and held only in RAM.
It cannot disagree with the filesystem, because it is computed from it, and a
crash raises no question at all. The cost is mount time proportional to metadata
size, and a ceiling on volume size set by how much bitmap fits in memory. If
mount time ever hurts, the map can be cached on disk purely as a hint,
validated against the generation and discarded on a mismatch — an addition, not
a format change. See `kernel/fs/schfs/space.c`.
*Data blocks needed a generation stamp too.* The B-tree already rewrites its own
nodes in place within a group, since a block that no committed root can reach is
nobody's fallback. Data blocks had no such marker, so every hundred-byte write
copied a whole 4 KB block and split an extent record around it. `schfs_extent`
gained a 64-bit `gen`, and the same rule now applies to file data.
**Phase 3½ — reachable through `open(2)`. DONE.** SchFS is a mounted filesystem:
```
mkdir /mnt
mount -t schfs /dev/hdb /mnt
ls /mnt ; echo hi > /mnt/tmp/w ; cat /mnt/tmp/w ; mkdir /mnt/tmp/d
```
The VFS grew a real dispatch layer rather than a set of `if (dev == SCHFS_DEV)`
checks. `struct fs_ops` (include/file.h) is one table per filesystem, chosen by
`iget()` from the device number and hung off the inode; v1's implementations
keep their bodies and are simply named `v1_*`. Adding a third filesystem is a
new table and nothing else. `kernel/fs/schfs/vfs.c` is SchFS's, and it is pure
translation — object ids in, inodes out — because all the filesystem logic
already lives below it.
Three things had to change on the way, none of them SchFS's fault:
- `struct inode.inum` and `.size` were 32-bit, capping objects at 2³² and files
at 4 GiB on a filesystem that promises far more. Both are `uint64` now, as are
`readi`/`writei` offsets and `struct file.off`.
- `struct dirent` carries a 16-bit inum and a 14-byte name, so it cannot
represent a SchFS entry at all. `getdents64` is now an fs_op: each filesystem
emits the record stream itself, and `f->off` is whatever position it needs to
resume. v1 walks its hash buckets; SchFS walks a key range and synthesises
"." and "..", which it does not store.
- Mounting is a substitution made during path resolution (`mount_add` /
`mount_cross` in `fs.c`). Nothing on either disk records it. The reverse
direction matters as much as the forward one: `..` at a mounted root has to
leave the mount, and the filesystem underneath has no idea it is mounted
anywhere.
Three long-standing bugs surfaced the moment real commands ran against it, all
in the shared VFS rather than in SchFS:
- `include/fcntl.h` used xv6's open flags while every program in the userland
sends Linux's. `O_APPEND` (0x400) landed on `O_TRUNC`, so `cmd >> file`
*truncated* it; `O_TRUNC` (0x200) landed on `O_CREATE`, so `cmd > file` left
whatever was past the new end in place. Nothing about either pointed at a
header.
- `sys_openat` silently dropped `O_TRUNC` and `O_APPEND` while `sys_open`
honoured both, so the same redirection behaved differently depending on which
entry point libc happened to call.
- `rmdir` had no Linux syscall number, so every Linux-ABI program got ENOSYS.
**Phase 4 — extents with refcounts.** Enables reflink copies and snapshots
nearly for free.
**Phase 5 — attribute index and query.** See below.
## 6. The "SQL" part, honestly
What you want from "works like SQL" is: *ask a question about a lot of files and
get an answer fast, without walking every directory*. That is an **index**, not
a query language. BeFS's design:
- Files carry typed attributes (`string`, `int`, `time`, …).
- An attribute can be *indexed*: a dedicated B+tree keyed by attribute value,
mapping value → object ids.
- A query is a predicate over indexed attributes, evaluated by intersecting
index ranges.
In SchFS this is just another key type — `(idx, INDEX, key)` — in the tree
that already exists. That is the whole reason to build phases 1-4 the btrfs way:
the "database filesystem" feature becomes a few hundred lines on top of
machinery already written and debugged, rather than a separate engine.
A concrete `query()` syscall and a `find`-like tool come after that, and are
small by comparison.
## 7. Honest cost
Phases 1-3 are roughly 3,000-4,000 lines and are the bulk of the work. Phase 1
alone (B-tree + torture harness) is worth doing carefully and unhurriedly; every
later phase compounds on it.
This is the largest single piece of work in Schunix. It is also entirely
ordinary filesystem engineering with excellent published references
(Giampaolo for BeFS; the btrfs design papers; the ZFS uberblock/checksum-in-
parent scheme) — there is no research risk here, only volume.
**Do this before the installer.** The installer's entire job is to create and
populate this format. Building it against v1 means building it twice.
## 7½. Limits
Three different things get called "the limit", and they are worth keeping apart:
what the **on-disk format** can express, what **this implementation** can
currently mount, and what the **rest of Schunix** can carry. The smallest one is
what you actually hit.
### The format
| | limit | where it comes from |
|---|---|---|
| Volume | **64 ZiB** (2⁶⁴ × 4 KiB blocks) | `uint64` block numbers |
| File | **16 EiB** (2⁶⁴ bytes) | `schfs_inode.size` |
| Files per volume | **1.8 × 10¹⁹** | `uint64` object ids |
| Hard links per file | **4.3 × 10⁹** | `uint32 nlink` |
| Filename | **255 bytes** | `SCHFS_NAME_MAX` |
| Entries per directory | no fixed limit | one tree key per name hash |
| Extent | **64 ZiB** in one record | `uint64 nblocks` |
| Tree depth | **8 levels** | `SCHFS_MAX_LEVEL` |
| Block size | **4096 bytes**, fixed | `SCHFS_BLKSIZE`; a mismatch is refused at mount |
| Transaction groups | **1.8 × 10¹⁹** | `uint64` generation, never reused |
Directory *depth* is unbounded on disk; path resolution is what limits it.
### This implementation
| | limit | where it comes from |
|---|---|---|
| Volume, mounted | **64 GiB** | the in-RAM allocation bitmap: `SCHFS_SPACE_PAGES` (512) pages × 32768 blocks |
| Volume, via the buffer cache | **4 TiB** | `bio.c` addresses blocks with a `uint`, and one SchFS block spans four of them |
| RAM per mount | **8 KiB per GiB** of volume | two bitmaps, one bit per block each |
| Inline value in a leaf | **1016 bytes** | `SCHFS_MAX_INLINE` = a quarter of a node |
| Colliding names in one directory | ~**60**, sharing a hash | the packed list for one hash must fit in one inline value |
| Directory entries at 8-byte names | ~**42** per key, unlimited keys | as above |
| Records per leaf / children per node | **127 / 84** | 4 KiB minus a 32-byte header |
| Mount time | one pass over the metadata | the allocation map is derived, not stored |
The 64 GiB mount ceiling is the one that bites first, and it is one constant:
raising `SCHFS_SPACE_PAGES` to 4096 makes it 512 GiB at 64 KiB of pointers per
mount. It is set where it is because the page-pointer vector lives inside
`struct schfs_vol`, which the kernel holds as static storage.
### The rest of Schunix
These are VFS and driver limits rather than SchFS ones:
- `MAXPATH` caps a path at **128 bytes**.
- `NMOUNT` allows **8** mounts.
- The IDE driver is 28-bit LBA: **128 GiB** per disk.
- A single `read`/`write` call is capped at **4 GiB** (`n` is a `uint`); the
file itself is not.
The two that used to be here — a 16-bit inode number in `struct dirent` and a
32-bit `inum`/`size` in `struct inode` — are gone. `getdents64` is a per-
filesystem operation now, so SchFS emits its own records with 64-bit inode
numbers and names up to 255 bytes, and the inode carries 64-bit numbers and
sizes throughout.
## 8. What exists now
```
include/schfs.h on-disk format + compile-time layout assertions
kernel/fs/schfs/btree.{c,h} the copy-on-write B-tree and its cursor
kernel/fs/schfs/schfs_int.h volume + object layer interface
kernel/fs/schfs/space.c allocation bitmap, derived from the tree at mount
kernel/fs/schfs/vol.c uberblock ring: open, format, commit
kernel/fs/schfs/obj.c read side: inodes, dirents, extents, path lookup
kernel/fs/schfs/objw.c write side: create, link, unlink, rename, write
kernel/fs/schfs/kmount.c kernel: block device, mount, transaction bracket
kernel/fs/schfs/vfs.c the fs_ops table: SchFS behind open/read/write
tools/mkschfs.c build an image from a directory tree
tools/schfs_fsck.c verify, list, extract
tools/schfs_test.c B-tree torture harness
tools/schfs_objtest.c object layer: files, directories, space accounting
tools/schfs_roundtrip.sh end-to-end filesystem test
tools/mutate.sh mutation-tests the suites themselves
tools/bootcheck.ps1 boots under QEMU and drives the shell
```
Everything except `kmount.c` is compiled *both* into the kernel and into the
host tools, so the writer and the reader cannot drift apart: there is one
implementation of the format and nothing to disagree with.
### Running the tests
```
make schfs-check all three suites
make schfs-test B-tree only
make schfs-objtest object layer only
make schfs-image-test round-trip only
```
`make schfs-check` after touching anything under `kernel/fs/schfs`.
The round-trip's oracle is the host's own filesystem: an image is built from a
directory tree, extracted with a separate program, and `diff -r`'d against the
original. It also asserts hard-link counts (which `diff` cannot see), checks
that rebuilding produces byte-identical output, and runs the whole thing a
second time with the name hash cut to 6 bits so the packed-dirent collision path
— which a 64-bit hash would never reach — is actually exercised.
`schfs_objtest` runs against a memory-backed volume, so a full
create/write/delete/remount cycle takes milliseconds. Its most important check
is the least glamorous: after deleting everything, free space must be back to
exactly what it was on an empty volume. Every other test in the suite still
passes on a filesystem that leaks a block on every delete.
`tools/mutate.sh` injects six deliberate bugs and fails if any of them goes
undetected. It is how the suites are kept honest; see the note in §9.
### Verifying on real hardware
```
make ramroot root from RAM, so device 1 is free
make build/schfs.img
make qemu-schfs then, at the shell: cat /proc/schfs
```
`.\run.ps1 -SchFS` does the same on Windows, and `tools\bootcheck.ps1 -SchFS`
does it unattended and asserts on the transcript.
## 9. Things learned the hard way
Recorded because each one cost real time and none is obvious from the code.
- **The root's object id must be reserved.** `next_objectid` started at
`SCHFS_FIRST_OBJECTID`, which *is* the root's id, so the first directory
created silently became the root. It then contained itself, and every tree
walk recursed until the stack ran out. `schfs_fsck --check` now detects
directory loops explicitly rather than crashing.
- **Cursor buffers alias.** A range callback gets a pointer into the cursor's
block buffer, and almost every readdir caller immediately looks the entry up,
which overwrites it. Directory iteration copies each entry out before calling
back. Extracting a tree with directories reparented under their own children
is what this looks like when it goes wrong.
- **A 4096-byte block does not fit on a 4096-byte kernel stack.** The cursor
exists precisely to avoid one block buffer per tree level, but
`schfs_vol_open` still kept a whole uberblock as a local and blew the stack on
the first boot. The kstack canary caught it cleanly.
- **Extents only merge if you allocate the run first.** Writing an extent record
allocates blocks for its own copy-on-write path, so allocating data one block
at a time guarantees the blocks are never adjacent and every block needs its
own extent. `schfs_file_reserve` claims the whole run up front; an 822 KB
binary is one extent instead of 201.
- **Freed blocks are reusable during a build.** Copy-on-write forbids reusing a
released block because the previously committed tree still points at it — but
a build has committed nothing. Recycling them cut the test image from 3596
blocks to 374.
- **Reproducibility dies from atime.** mkschfs reads every source file, which
updates its atime, so copying atime into the image made two builds of the same
tree differ. All three timestamps come from mtime.

130
docs/drivers.md Normal file
View file

@ -0,0 +1,130 @@
# Drivers
How hardware gets found, claimed and used, and where drivers that are not part
of the build will attach.
---
## 1. The device model
There is one list of everything attached, whatever bus found it:
`include/device.h` and `kernel/dev/device.c`, readable at `/proc/devices`.
```
name bus class driver device
kbd0 platform input ps2kbd PS/2 keyboard
mouse0 platform input ps2mouse PS/2 mouse
uart0 platform other uart8250 16550 serial port
ide0 platform storage ide ATA disk controller
vga0 pci display vga display controller
```
Before this there were two registries in `kernel/drivers/registry.c` — an array
of PCI drivers and an array of USB drivers — with nothing that knew about both.
That is sufficient for *binding* and useless for the three things wanted next.
**Enumeration.** "What is in this machine, and what claimed it" had no answer
covering a USB disk and a PCI display at once. A device manager is exactly that
question.
**Classes.** A compositor should ask for *the display* and not care whether that
is the VBE framebuffer or virtio-gpu. Without a class every consumer hardcodes a
driver, and adding the second driver means editing the consumers — which is how
a framebuffer address ended up hardcoded in three places here once already.
**A boundary for drivers that are not compiled in.** They have to attach
somewhere that is not an array fixed at link time.
Two vocabularies are kept deliberately separate: a device's **bus** is
provenance (`platform`, `pci`, `usb`), its **class** is capability
(`display`, `input`, `storage`, `net`, `bus`). A USB disk and a SATA disk are
both `storage` despite sharing no numbering at all. Where a driver claims a
device, the driver's opinion wins over the descriptor's: `struct usb_driver` has
a `devclass` field separate from the USB interface class, because the descriptor
says what the device *claims* to be and the driver knows what it *made* of it.
It is a flat list with a bus tag, **not** a device tree. Nothing yet needs to
walk a hierarchy, and a tree nobody traverses is just a harder list. Hot-unplug
would need more; the `in_use` flag is the hook.
Platform devices are registered in one place
(`platform_register_devices()` in `registry.c`) rather than inside each driver's
init, for the same reason the driver arrays are written out by hand: a list you
can read beats a list assembled by side effects.
---
## 2. Where this is going: a hybrid model
The intended shape is the NeXTSTEP/macOS one rather than Linux's — a device is
an object with a class and an ops vtable, and consumers talk to the class.
Two things often get bundled together as "loadable drivers" and they are
separable:
| | |
|---|---|
| **Loadable modules** | A stable driver ABI, drivers as files loaded at runtime (`.sys`, `.kext`). A linker/loader job on top of the model above. |
| **Drivers out of kernel** | Drivers as user-space processes (macOS DriverKit, which is why kexts are being retired). An IPC and memory-mapping job; the `AF_UNIX` and shared-memory work it needs already exists. |
The second is more in the spirit of the SchAPI direction and is slower and
larger. Neither is built yet. What exists is the attachment point they both
need.
---
## 3. Linux drivers
**Decided 2026-08-08.** Linux drivers are not shipped with Schunix and never
will be. The user may install one on their own machine, through a device
manager or equivalent, and Schunix will load it.
The licensing reasoning matters and is worth stating exactly, because it is
easy to get wrong in both directions:
- **Distribution is what triggers the GPL.** Shipping a GPL-2.0-only driver in
an Apache-2.0 image is what cannot be done — see [licensing.md](licensing.md)
§2. A user obtaining a driver themselves and loading it on their own machine
is their act, not ours, and is not a distribution by us.
- **Implementing a loader is not copying.** Writing support for a module format
and a driver ABI from published documentation produces our own work. It is a
clean-room implementation, and it stays one only if it is written from
documentation rather than transcribed from Linux source. Reading Linux to
understand *hardware* is fine; translating its code is not, whatever the
identifiers say.
So the rule for this subsystem is the same as everywhere else in the tree: the
loader is ours and Apache-2.0, what it loads is the user's business, and nothing
GPL ever enters an image we build. `RAMDISK_EXCLUDE` and `NOTPORTS` are the
existing mechanisms for keeping that true mechanically rather than by intention.
---
## 4. Adding a driver today
1. Write `kernel/drivers/<class>/<name>/<name>.c` exporting a
`struct pci_driver` or `struct usb_driver`. One directory per driver, so a
driver that grows a register header, a second source file or a firmware blob
has somewhere to put it:
kernel/drivers/
registry.c
gpu/vga/ gpu/virtio_gpu/
net/e1000/
bus/xhci/
usb/core/ usb/hid/ usb/storage/ usb/hub/
2. Declare it in `kernel/drivers/registry.c` and add it to the array.
3. Add the object to `KERN_DRIVERS` in the `Makefile`.
Registration in the device model is automatic — `pci_bind()` and
`usb_bind_all()` publish whatever they claim, so a driver cannot forget to. Set
`devclass` on a USB driver, or the device lands in `other`.
The best source for a new driver is the **specification**, not another kernel:
xHCI, AHCI, virtio, NVMe and the Intel datasheets are public documents with no
licence on what you write from them. `kernel/drivers/bus/xhci/xhci.c` came from the spec, and so did
`net/e1000/` and `gpu/virtio_gpu/`. Where a *table* of constants is genuinely
faster to transcribe than to retype from a PDF -- device IDs, register offsets --
taking it from a BSD tree is fine after the licence check, and goes in `NOTICE`.
That is data; the logic stays ours.

175
docs/layout.md Normal file
View file

@ -0,0 +1,175 @@
# Repository layout
One rule holds the whole thing together:
> **A directory contains either hand-written source or generated output. Never both.**
Everything generated lives under `build/`. Delete `build/` and you lose nothing
that can't be rebuilt (except upstream tarballs, which live in `dist/` and are
deliberately kept out of `clean`).
```
schunix/
├─ boot/ Stage 1 MBR + stage 2 loader (splash, F8 menu, kernel load)
├─ kernel/ The kernel, by subsystem: arch/ core/ dev/ fs/ proc/ sys/
├─ include/ Headers shared by kernel, userland, and tools/mkfs
├─ lib/ First-party userland runtime: _start glue, syscall stubs,
│ the initcode blob embedded in the kernel, user.ld
├─ cmd/ First-party programs — one directory per program:
│ cmd/<name>/main.c → sysroot/bin/<name>
├─ ports/ Third-party software — one directory per port.
│ Contains ONLY a Makefile and optional patches/.
├─ projects/ Things that run ON Schunix rather than things that ARE it:
│ the compositor, window manager, desktop, and installer.
│ Not part of the kernel build. See projects/README.md.
├─ tools/ Host-side helpers, SOURCE ONLY: mkfs.c, the SchFS test
│ suites, sign.pl, vectors.pl, and the PowerShell harnesses.
│ Their binaries build into build/tools/ -- this directory
│ used to hold both, which is the one rule this layout has.
│ tools/loc.ps1 counts how much of the tree is first-party.
├─ sysroot/ Staging for the disk image. etc/ is hand-maintained;
│ bin/ lib/ sbin/ are populated by `make sysroot-install`.
├─ dist/ Upstream tarball cache. Survives `make clean`.
├─ Refrence/ A FreeBSD checkout (Refrence/freebsd-src), kept as reference
│ material and a parts bin. Never built, never in an image.
│ Read it when a specification is ambiguous; copy from it only
│ after the per-file licence check in docs/licensing.md.
│ A full clone is 2.1 GB and a previous NetBSD one was deleted
│ for that reason. Only sys/dev, sys/net and sys/netinet have
│ ever been read, so a sparse checkout is ~100 MB:
│ git clone --filter=blob:none --sparse <url> Refrence/freebsd-src
│ git -C Refrence/freebsd-src sparse-checkout set │ sys/dev sys/net sys/netinet
├─ src-trunk/ A NetBSD checkout, kept as reference material and a parts
│ bin. NOT built, NOT installed, and it must never reach an
│ image. Copying from it is governed by the per-file licence
│ check in docs/architecture/schapi.md §5 — NetBSD is not
│ uniformly licensed.
├─ docs/ Design docs
└─ build/ ALL generated output (see below)
```
## build/
```
build/
├─ boot/ bootblock, stage2
├─ kernel/ object files, the linked kernel, kernel.asm
├─ lib/ libc glue objects, initcode blob
├─ cmd/ linked first-party programs
├─ fs.img the filesystem image
└─ ports/<name>/
├─ src/ extracted upstream tarball
├─ obj/ out-of-tree build directory
└─ prefix/ install prefix (musl's headers and libs land here)
```
## Adding your own program
```sh
mkdir cmd/hello
$EDITOR cmd/hello/main.c # include "types.h", "user.h", ...
```
Then add `hello` to `CMDS` in the root `Makefile`. It gets linked against
`lib/` and installed to `sysroot/bin/hello` by `make sysroot-install`.
## Adding a third-party port
```sh
mkdir ports/foo
$EDITOR ports/foo/Makefile # start with `include ../mk/port.mk`
```
`port.mk` derives everything from the directory name — it gives you `$(SRCDIR)`,
`$(BUILDDIR)`, `$(PREFIX)`, `$(DISTDIR)`, `$(SYSROOT)`, and `$(MUSL)` (the
musl install prefix that every port links against). Follow the pattern in
`ports/mksh/Makefile`: fetch tarball to `$(DISTDIR)`, extract to `$(SRCDIR)`,
build into `$(BUILDDIR)`, install into `$(SYSROOT)`. `clean` removes
`$(PORTDIR)` and nothing else.
`ports/Makefile` picks up the new directory automatically. If it links against
musl, add `foo: musl` there so ordering is right.
Local modifications to upstream go in `ports/foo/patches/*.patch` and are
applied after extraction — never by editing files under `build/`, which are
wiped on every clean.
## Ramdisk root
```sh
make # normal: root on the IDE disk (build/fs.img as hdb)
make ramroot # root from a SCHFS image linked into the kernel
```
`make ramroot` builds `build/fs-ram.img` (default 6000 blocks, override with
`RAMDISK_BLOCKS=`) and links it into the kernel, so the system boots to a shell
with **no data disk attached at all**. That is what makes destructive work on
the real disk survivable — reformatting it, or bringing up a new on-disk format
— and it is the first piece of the installer.
Use `make ramroot`, not `make RAMROOT=1`: the flag changes `CFLAGS`, which make
cannot see, so objects compiled for one setting would be silently reused for
the other and `ROOTDEV` would point at the wrong device. The target drops the
compiled objects first. Switching back is the same — plain `make` after a
`ramroot` build reuses stale objects, so run `rm -rf build/kernel build/lib
build/cmd` first (or just `make clean`).
The image costs boot time: stage2 loads the kernel by PIO one sector at a time,
so a 6 MB ramroot kernel is visibly slower to start than the 350 KB default.
**What may go in it is a licensing question, not only a size one.** The image is
linked into the kernel binary, so anything in `sysroot/` when it is built becomes
part of a distributable artifact. A file staged there under a licence that
cannot be combined with Apache-2.0 does not merely ride along — it makes the
kernel itself undistributable. `RAMDISK_EXCLUDE` in the Makefile is the list of
paths kept out, and enforcing it in the build rather than relying on someone
remembering to unstage something first is deliberate. It has been needed once
already: a 33 MB test payload landed in `sysroot/` and both overflowed the image
and would have gone into the kernel binary.
Three knobs, all in the Makefile, none of them `FSSIZE`:
| | |
|---|---|
| `RAMDISK_BLOCKS` | image size, 6000 (× 1 KiB `BSIZE`) |
| `RAMDISK_NINODES` | inode table size, 2000 |
| `RAMDISK_EXCLUDE` | sysroot-relative paths to omit, with their subtrees |
`RAMDISK_NINODES` is worth knowing about because the inode table is
*preallocated*: mkfs's default of 12000 inodes cost 1501 of the 6000 blocks — a
quarter of the image — to describe about 260 files. At 2000 it costs 251, and
metadata overall fell from 1596 blocks to 345.
If `mkfs` reports the image is full, read its message: it now prints the
metadata breakdown and names the right knob for whichever image overflowed. It
used to say only "increase FSSIZE in param.h", which is a constant that has
nothing to do with the ramdisk root and sends you to the wrong file.
## Two things that will bite you
**`ld -r -b binary` derives symbol names from the input file's path.** The
kernel embeds two blobs this way (`initcode`, `entryother`), and the root
Makefile has `--redefine-sym` lines mapping e.g.
`_binary_build_lib_initcode_start` → `_binary_initcode_start`. Move either
blob's build location and those symbol names change; update the Makefile to
match or the kernel fails to link.
**`STAGE2_SECTORS` now lives in exactly one place: the root `Makefile`.** It used
to be written out by hand in five — `boot/bootasm.S`, `boot/stage2main.c`'s
`readseg()`, the `dd ... seek=` offset, and twice more inside `tools/mkiso.sh`
(its stage2 size assertion and `-boot-load-size`). This note used to warn you to
keep three of them in step and did not know about the other two.
Raising it from 24 to 48 for the boot logo demonstrated the problem: the `dd
seek=25` offset stayed behind, so the kernel was written directly over stage2's
own tail. Now the Makefile passes `-DSTAGE2_SECTORS` to both boot sources,
derives the padding budget and the `seek=` offset from the same variable, and
exports it to `mkiso.sh`. The `#define`s that remain in the two boot sources are
`#ifndef`-guarded fallbacks so those files still read sensibly alone; the
Makefile always wins.
Two things still fail the build rather than silently misbooting, and should stay
that way: `tools/padstage2.pl` errors if stage2 overflows its budget instead of
truncating it, and `boot/stage2main.c` static-asserts that stage2 cannot reach
the multiboot kernel-search buffer at `0x10000` — which puts the hard ceiling at
65 sectors, since stage2 loads at `0x7E00`.

241
docs/licensing.md Normal file
View file

@ -0,0 +1,241 @@
# Licensing
The one page to check before copying anyone else's code into Schunix.
Referenced from `LICENSE`, `LICENSE.MIT` and `NOTICE`, all three of which
pointed here long before this file existed.
---
## 1. Schunix is Apache-2.0
Copyright Nexoniarz. See `LICENSE`.
Apache-2.0 was chosen over MIT/BSD for one reason that actually matters at OS
scale: it contains an **express patent grant** and a patent-retaliation clause.
MIT and BSD are silent on patents, which means a contributor can hand you code
and later assert a patent over it. For a kernel that is intended to accept
outside contributions, that silence is the expensive kind.
What that grant means in practice: Apache-2.0 §2 gives everyone a perpetual,
worldwide, irrevocable licence to reproduce, modify, sublicense and distribute
Schunix. Any additional restriction stated elsewhere — in a README, a website,
or a policy — does not narrow it. The licence text is what holds up.
If restricted distribution is ever genuinely wanted, that is a licence change
(a bespoke proprietary licence, or something like BUSL-1.1 with a conversion
date), not a wording change, and it means giving up the Apache grant.
---
## 2. What can be copied in
| Licence | Verdict | Obligation |
|---|---|---|
| Public domain, 0BSD, CC0 | **Yes** | None |
| MIT, ISC | **Yes** | Keep the notice |
| BSD 2-clause | **Yes** | Keep the notice |
| BSD 3-clause | **Yes** | Keep the notice; no endorsement claims |
| BSD **4-clause** (advertising) | **Avoid** — see §3 | Notice *and* an acknowledgement in advertising materials |
| Apache-2.0 | **Yes** | Keep NOTICE contents |
| MPL-2.0 | Case by case | File-level copyleft; keep those files separate |
| **GPL-2.0-only** | **No** | — |
| **GPL-3.0 / LGPL-3.0** | **No** for linked code | — |
**Every "yes" above requires an entry in `NOTICE`.** That file is the project's
attribution record and the thing that makes the permissive licences actually
satisfied rather than merely tolerated.
### Why GPL-2.0-only is a hard no
Apache-2.0 imposes conditions GPLv2 treats as "further restrictions" under its
§6 — principally the patent-retaliation term. The FSF and the ASF agree on
this: **Apache-2.0 code and GPL-2.0-only code cannot be combined into one
distributable work.** It is not a matter of taste or of how carefully the files
are kept apart in the tree; it is that the combined binary cannot satisfy both
licences at once.
GPL-3.0 fixed this in the other direction (Apache-2.0 is compatible *into*
GPLv3), but that does not help: the result would have to ship under GPLv3, and
Schunix does not.
**Practical consequence:** BSD drivers can be adopted. Linux drivers cannot,
ever, no matter how convenient. This is why the USB stack was written from the
specification rather than ported.
---
## 3. NetBSD in particular
`src-trunk/` is a NetBSD checkout kept as reference material and a parts bin
(see [architecture/schapi.md](architecture/schapi.md) §5). NetBSD is **not
uniformly licensed**, and the difference matters per file:
| Template | Status |
|---|---|
| 2-clause BSD — modern NetBSD Foundation files | **Fine** |
| 3-clause, "Regents of the University of California" | **Fine.** Berkeley formally rescinded the advertising clause in 1999 |
| 4-clause with the advertising clause, **non-UC** copyright holder | **Avoid.** Berkeley's rescission covers Berkeley's copyright, not other people's |
The last row is real, not hypothetical. Measured on this checkout:
```
sys/kern 26 of 220 files carry the advertising clause
sys/dev 500 of 3565
lib/libc 101 of 1575
sys/net 5 of 180 ← the TCP/IP stack is nearly clean
```
4-clause is still permissive and still redistributable; the burden is an
advertising obligation that accumulates quietly and is tedious to honour
properly. Prefer a 2-clause or UC-Regents file. If a 4-clause file is genuinely
the right answer, it goes into `NOTICE` with the full clause quoted.
Check before copying, not after:
```sh
grep -l "All advertising materials" <file>
```
### And check *whose* clause it is
A file can carry several licence blocks from several copyright holders. Finding
the advertising clause is only half the check — Berkeley's rescission covers
Berkeley's copyright and nobody else's, so the question is always *whose*
clause you found.
Measured on `src-trunk`, the networking tree:
```
sys/net 106 files, 5 with the advertising clause
sys/netinet 121 files, 25 " " "
sys/netinet6 51 files, 5 " " "
```
Encouraging at first glance — the 25 in `sys/netinet` are almost all SCTP,
DCCP and multicast routing, none of which Schunix needs. `ip_input.c`,
`ip_output.c`, `udp_usrreq.c`, `if_arp.c`, `ip_icmp.c`, `if.c` and `route.c`
are all UC 3-clause, i.e. fine.
**But `tcp_input.c` and `tcp_output.c` are not.** Their advertising clause
belongs to the **WIDE Project** (the KAME IPv6 people), whose copyright sits at
the top of the file; the UC block is a separate licence further down. Berkeley
never spoke for WIDE, so the rescission does not reach it. The two most
valuable files in the stack are the two encumbered ones.
That is not fatal — 4-clause is still permissive — but it is exactly the case
where a **second BSD tree earns its disk space**: FreeBSD's TCP has been
through a relicensing effort NetBSD's has not, so the same file there may well
be 2-clause. Check it the same way before assuming either:
```sh
grep -n "Copyright" <file> | head # who claims it
grep -n -B6 "All advertising" <file> # whose clause is it
```
### FreeBSD's TCP is clean where NetBSD's is not
Checked, 2026-08-08, exactly that way. Against FreeBSD `main`:
`sys/netinet/tcp_input.c` contains **no advertising clause at all**. Its
copyright holders are the UC Regents, Swinburne University, Lawrence Stewart,
the FreeBSD Foundation and Juniper Networks -- all BSD-2 or BSD-3.
So if BSD networking code is taken, it must come from **FreeBSD, not NetBSD**,
and the specific reason is TCP. This reverses the earlier assumption in
`docs/roadmap.md` that NetBSD was the intended starting point; NetBSD remains
the better-written reference to *read*, but FreeBSD is the one that can be
copied from.
### The third option: lwIP
Before porting either BSD stack, weigh **lwIP** against both. Verified
2026-08-08 from its `COPYING`: 3-clause BSD, Swedish Institute of Computer
Science, **no advertising clause anywhere in the file**. Compatible, one NOTICE
entry, done.
The licence is the smaller half of the argument. The larger half is that
FreeBSD's `sys/netinet` is not a library -- `tcp_input.c` alone is 122 KB and
reaches into mbufs, `epoch(9)`, netisr, VNET, `uma(9)`, sockbufs, inpcb hash
tables and FreeBSD's locking. Taking it means porting FreeBSD's kernel
infrastructure first and finding the TCP somewhere underneath, which is the same
conclusion `docs/roadmap.md` §1 already reached about `sys/dev/usb`.
lwIP was written for the case Schunix is in: a small operating system with a raw
Ethernet driver and no infrastructure to speak of. Its port layer is a handful
of files, it has no dependency on the host kernel's allocator or locking, and it
brings ARP, IPv4, ICMP, UDP, TCP, DHCP and DNS.
### Fonts
Chosen 2026-08-09, both licences checked at source rather than assumed:
| Font | Where | Licence |
|---|---|---|
| Roboto | desktop, window manager, compositor | **Apache-2.0** |
| JetBrains Mono | terminal, TTY | **SIL OFL 1.1** |
Roboto is the same licence as Schunix, so it is the easy case: a NOTICE entry
and nothing else.
OFL is permissive and bundling is precisely what it is for, with two conditions
worth knowing before anyone subsets or converts a font:
- **The font stays under OFL** even inside an Apache-2.0 system. It does not
infect anything else; it just cannot be relicensed.
- **It may not be sold on its own.** Shipping it as part of an OS is fine.
- **Reserved Font Names.** JetBrains Mono declares *none* -- its copyright line
names no reserved name -- so a modified version may keep the name. Most OFL
fonts do reserve theirs, and then a derivative must be renamed.
Pre-rasterising a font into bitmaps at build time **is** a modification, so
this is not a theoretical point for this project.
---
## 4. Things that are deliberately not shipped
Some code is fine to *build and run locally* and impossible to *distribute*
with Schunix. Keeping the distinction sharp is what stops it eroding:
- **`Refrence/`** — a FreeBSD checkout (`Refrence/freebsd-src`), kept as
reference material and a parts bin. Never built, never in an image. The
directory name is spelled as it is on disk; renaming it is a one-line change
if it ever grates.
- **`src-trunk/`** — a NetBSD checkout, same rules. Currently absent; it was
deleted for disk space and can be re-fetched sparsely if needed.
- **`dist/`** — upstream tarballs and other downloaded blobs. Cached so a
rebuild never needs the network; not part of any image, and not all of it is
redistributable.
The mechanisms that keep this from eroding, since good intentions have already
failed once here:
- `NOTPORTS` in `ports/Makefile` — a port that lives in the tree but is never
built by `make`.
- `RAMDISK_EXCLUDE` in the root `Makefile` — paths kept out of the ramdisk root.
This one matters more than it looks: the ramdisk image is linked *into the
kernel binary*, so anything sitting in `sysroot/` when that image is built
becomes part of a distributable artifact. A GPL-2.0 or non-redistributable
file staged there does not merely ride along, it makes the kernel itself
undistributable.
A previous acceptance test — a ported 1993 shooter, GPL-2.0 engine on
non-redistributable shareware data — was carried under exactly these rules and
has since been removed. It is worth recording *why* it was safe: it was never in
`PORTS`, and its 33 MB of data was excluded from the ramdisk. It is equally
worth recording that the safety depended on two separate opt-outs staying
correct, which is a lot of standing risk for a test that had stopped finding
bugs. See `docs/roadmap.md` Phase 7.
---
## 5. Specifications are not code
A published specification carries no copyright over an independent
implementation of it. The xHCI, USB, PCI, fbdev and ELF specifications were all
read and implemented directly, which is why those drivers have no licence
entanglement at all despite equivalents existing in both Linux and BSD.
When a subsystem is small enough to write from the spec, that is usually the
cheapest option in the long run — not because copying is hard, but because a
copied file is a file to be understood, attributed and maintained forever.

481
docs/roadmap.md Normal file
View file

@ -0,0 +1,481 @@
# Roadmap
Written 2026-08-04, from the plan: USB, GPU, fonts, networking and sockets,
then a compositor (Schcom), a window manager (SchWM) and a desktop (SchDE),
plus an installer and a `.sip` package format.
This file records two things that are easy to lose: the *order* the work has
to happen in, and the decisions that constrain it.
**Superseded in direction, not in order.** Schunix is now aimed at being a
*system* rather than a Unix-like kernel with a userland attached: a NeXTSTEP/
macOS-shaped layering, with POSIX kept complete but demoted from identity to
personality, and a native API (**SchAPI**) above it. See
[architecture/schapi.md](architecture/schapi.md). That decision changes what
the phases below are building towards; it does not reorder them, and it does
not start until the tree builds and boots natively again.
That assumption about §1 has since been revised. NetBSD's TCP/IP stack was the
intended starting point; the per-file licence check found that NetBSD's
`tcp_input.c` and `tcp_output.c` carry the WIDE Project's 4-clause advertising
clause, which Berkeley's rescission does not reach. FreeBSD's equivalent is
clean, and lwIP is cleaner still and a far smaller port. See Phase 2 below and
docs/licensing.md §3.
---
## 1. Where drivers can come from
**BSD: yes. Linux: no.** This is not a preference, it is a licence
incompatibility, and it decides the whole driver strategy.
The reasoning — Apache-2.0's patent terms counting as "further restrictions"
under GPLv2 §6, which is why the two cannot be combined and redistributed — is
in [licensing.md](licensing.md), along with the per-licence table and the
NetBSD-specific rules. Read it before copying anything. The short version:
BSD-2 and BSD-3 code can be used directly with a `NOTICE` entry; GPL-2.0-only
code can never be, at any distance.
There is a third source that is better than either: **the specifications**.
xHCI, AHCI, virtio, NVMe, the Intel e1000 and i915 datasheets are all public
documents with no licence attached to what you write from them. That is where
`kernel/drivers/xhci.c` came from.
One caveat about "port it from BSD" as a plan, learned by looking: FreeBSD's
`sys/dev/usb` is roughly 100k lines welded to newbus, devclass, `bus_dma`,
callouts, taskqueues and FreeBSD's locking. Porting it means porting
FreeBSD's device framework first and finding the USB stack somewhere
underneath. NetBSD's is more portable and the same argument applies with a
smaller multiplier. **BSD is far more valuable as the reference you check
yourself against than as the code you paste.** Read it when the spec is
ambiguous — which it is, often — and write your own.
Reading Linux for *understanding* is fine and normal. Translating a Linux
driver function-by-function is not; the result is a derivative work whatever
the variable names say.
---
## 2. Ordering, and why it is not the order it was asked in
The requested order was USB → GPU → fonts → network → GUI → installer. That
is a reasonable order by *interest*. By dependency it wants one change:
**Shared driver infrastructure comes before all of it.** USB, any GPU beyond
the VBE framebuffer, and any NIC all need the same three things, none of
which existed:
- a way to register an interrupt handler at runtime (`kernel/dev/irq.c`)
- physically contiguous memory for rings and descriptors
(`kernel/core/dma.c`)
- PCI capability walking and MSI (`kernel/dev/pci.c`)
That work is done. It was the first step of USB, not a detour from it.
The other honest note: **USB is the item that does least for the GUI.** Under
QEMU there is already a PS/2 keyboard and mouse, so USB buys real hardware
support — which matters, but not for getting Schcom on screen. Sockets and
raw input events are what actually block the compositor. USB is being done
first because it was asked for first; the ordering below reflects that with
the dependencies fixed.
---
## 3. The plan
### Phase 1 — USB *(done, bar the boot path)*
- [x] Shared infrastructure: IRQ registration, DMA arena, PCI capabilities
- [x] xHCI: reset, DCBAA, command ring, event ring, scratchpad
- [x] Port enumeration, Enable Slot, Address Device, control transfers
- [x] Device descriptors read from real devices (USB2 and USB3 paths)
- [x] Configuration and interface descriptor parsing, `SET_CONFIGURATION`
- [x] Interrupt endpoints, driven from the IRQ handler
- [x] HID boot protocol: keyboard and mouse into the existing input paths
- [x] Bulk endpoints, bulk-only transport, SCSI, a block device at
`USBDISK_DEV` — a superblock reads back through `bread()`
- [x] Hubs — route strings, hub slot contexts, per-port reset and power. A
mouse behind a hub enumerates a tier deep, which is what proves it
- [ ] Making a USB disk mountable and bootable (needs the root device to be
selectable rather than compiled in)
### Phase 2 — Sockets, raw input, and networking
Do sockets **before** the network stack. **Unix domain sockets are what Schcom
needs** — clients and compositor have to pass messages and share buffers, and
a pipe cannot carry an shm handle. Unix sockets are also far simpler than
TCP/IP, so they are the right thing to build the socket layer's shape around.
- [x] `socket`/`bind`/`listen`/`accept`/`connect`/`send`/`recv` syscalls, at
their real Linux numbers — they were `NO_LINUX` stubs, so a musl program
calling `socket()` never even reached the stub
- [x] `AF_UNIX` SOCK_STREAM, `socketpair`, `shutdown`; names live in a table
rather than the filesystem (see the comment at the top of
`kernel/net/unix.c` for what that costs)
- [x] Raw input events — `/dev/input/event0` and `event1`, Linux evdev layout,
fed by the PS/2 keyboard and mouse and by USB HID. The console keeps its
own path; both consumers see every key
- [ ] `SCM_RIGHTS` for passing descriptors. Possibly unnecessary: an shm id is
an integer and travels fine over a stream socket, so this waits until
something actually needs to pass an open file
- [x] e1000 driver — `kernel/drivers/net/e1000.c`, from Intel's public manual
317453. MAC from EEPROM, transmit and receive rings, link detection.
Run with `-netdev user,id=n0 -device e1000,netdev=n0`.
**Verified to carry real traffic**: `echo n > /proc/sysrq` sends a
hand-built ARP request and receives the gateway's reply, which exercises
the descriptor formats, the DD handshake, the tail bookkeeping, the DMA
addresses and the MAC filter in one exchange. That check came before any
protocol stack on purpose -- a stack on an unproven driver fails in ways
indistinguishable from its own bugs
- [ ] ARP, IPv4, ICMP, UDP, then TCP. **Not written from scratch** -- decided
2026-08-08. Writing TCP is the one part of this where doing it yourself is
a bad trade: it is not the packet format that is hard, it is congestion
control, retransmission, window management and twenty years of accumulated
corner cases that only appear against a real peer.
**Preferred base: lwIP** (BSD-3, no advertising clause, verified). It was
written for exactly this situation -- a small OS with a raw Ethernet
driver -- and its port layer is a handful of files.
**If BSD instead: FreeBSD, not NetBSD.** NetBSD's `tcp_input.c` carries
the WIDE Project's 4-clause advertising clause; FreeBSD's has none. But
FreeBSD's `sys/netinet` is not a library -- it reaches into mbufs,
epoch(9), netisr, VNET and uma(9), so taking it means porting FreeBSD's
kernel infrastructure first. See docs/licensing.md §3.
- [ ] `AF_INET` on top of the same socket layer
### Phase 3 — GPU
"Basic GPU driver" has a right answer and a wrong one. The wrong one is
starting with Intel or AMD hardware: modern GPU drivers are enormous and most
of the size is mode setting, power management and a command submission model
that only pays off once there is something to submit.
- [x] **virtio-gpu**, 2D. `kernel/drivers/virtio_gpu.c`, written from the OASIS
specification. Full path: GET_DISPLAY_INFO, RESOURCE_CREATE_2D,
RESOURCE_ATTACH_BACKING, SET_SCANOUT, TRANSFER_TO_HOST_2D,
RESOURCE_FLUSH. Bound as the display class ahead of the VBE framebuffer.
Run with `-vga std -device virtio-gpu-pci`; keep `-vga std`, because the
bootloader's splash goes through VBE and there is no virtio path that
early
- [ ] A `/dev/dri`-style device with mode setting, so resolution stops being
whatever the bootloader asked VBE for
- [ ] Double buffering and vblank — Schcom needs both to not tear
- [ ] Only then: real hardware, and only 2D/blit acceleration
### Phase 4 — Fonts
This is a bigger job than it sounds and it should be sized honestly. A TTF/OTF
rasteriser needs: `cmap`/`glyf`/`loca`/`head`/`hmtx` table parsing, quadratic
béziers for TrueType outlines and *cubic* ones for OpenType/CFF, a scanline
rasteriser with anti-aliasing, and a glyph cache.
The bigger structural problem is that the console assumes a fixed 8×16 cell
grid throughout — `vga_putc`, the cursor, the ANSI erase operations. Variable
width text is not a font change, it is a text-rendering layer.
- [x] **The text layer** — `include/text.h` + `kernel/dev/text.c`. Glyph
metrics, a `struct surface` target so a compositor can draw off-screen,
8-bit coverage with real blending, UTF-8, and `font_ops` so a rasteriser
plugs in as a second provider. This was the actual blocker: the font was
never the problem, the character-cell grid was.
- [ ] Port the console onto it and delete the cell grid
- [x] A TrueType rasteriser — `kernel/dev/ttf.c`, glyf outlines, composite
glyphs, cmap format 4, anti-aliased scanline fill. Host harness at
`tools/ttftest.c`
- [x] A glyph cache — direct-mapped over ASCII into a 48 KB pool
- [ ] Load a font from disk and point the console at it, deleting the cell grid
- [ ] CFF/Type 2 charstrings, so OTF fonts work too (a separate `font_ops`
provider; a bytecode interpreter, not an extension of the glyf path)
- [ ] A rasteriser. `stb_truetype.h` is public domain and therefore usable
as-is if wanted; writing one is also reasonable and more interesting
- [ ] **The typefaces, decided 2026-08-09.** Both verified, not assumed:
**Roboto** for the desktop, window manager and compositor --
**Apache-2.0**, the same licence as Schunix, so it needs nothing beyond a
NOTICE entry.
**JetBrains Mono** for the terminal and TTY -- **SIL OFL 1.1**, and
notably with *no* Reserved Font Name declared, so it may be modified and
still called JetBrains Mono. That matters more than it sounds:
pre-rasterising a font into bitmaps at build time is a modification, and
an RFN would have forbidden keeping the name on the result.
OFL's one real constraint is that the font may not be sold on its own and
must stay under OFL; bundling it in an Apache-2.0 system is exactly the
case it is written for.
- [ ] Text layout (advance widths, kerning) as a library in `lib/`
- [ ] The console keeps its fixed grid; proportional text belongs to the GUI
**Font licensing: public domain or very permissive only.** No SIL OFL if it
can be avoided, no "free for personal use", no CC-BY-SA. The constraint is
the same one that governs drivers (§1): a font file is redistributed inside
every Schunix image, so its licence attaches to the image.
Safe choices, in rough order of usefulness:
- **CC0 / public domain** — best case, nothing to comply with.
- **MIT / BSD / Apache-2.0 fonts** — fine, needs a `NOTICE` entry.
- **SIL OFL** — usable but has a real string attached: a modified font must
be renamed, and OFL fonts cannot be sold on their own. Workable, but read
it before committing one.
Whatever is chosen, the licence goes in `NOTICE` **before** the file is
committed, not after.
### Phase 5 — The GUI, as separate projects
See `projects/README.md`. Schcom, SchWM and SchDE live outside the kernel
tree on purpose.
- [ ] **Schcom** — the compositor. Owns the framebuffer, composites client
buffers, routes input. Needs: unix sockets, shm (done), raw input
events, and a GPU driver with double buffering.
- [ ] **SchWM** — window management on top of Schcom's protocol
- [ ] **SchDE** — panel, launcher, settings
Raw input is the missing kernel piece nobody thinks of until it bites:
keyboard events currently only arrive through the tty line discipline, cooked
and line-buffered and delivered to one foreground process. A compositor needs
key-down/key-up with modifiers and no line discipline at all — an evdev-style
`/dev/input`.
### Phase 6 — Installer and `.sip`
`.sip` (Schunix Installation Package), the stated model being WIM in Windows
PE: a *file-based* image rather than a block image, so one file can hold a
whole filesystem, be applied to a partition of any size, and hold several
images that share their common files.
The pieces for this mostly exist already. `tools/mkschfs.c` does content
dedup with an `(st_dev, st_ino)` fast path; SchFS's B-tree is content
addressed. A sensible `.sip` is: a header, a file index (path, metadata,
content hash), and a blob store keyed by hash, with compression per blob.
- [ ] `.sip` format specification, written down before any code
- [ ] `tools/mksip` — capture a directory tree into a `.sip`
- [ ] Kernel or userspace apply path
- [ ] An installation environment that boots from the ISO, partitions,
applies the `.sip`, and installs the bootloader
---
### Phase 7 — The full-stack acceptance test *(served its purpose; removed)*
For a while this phase was a ported copy of a well-known 1993 shooter, kept as
an acceptance test rather than a feature. The reasoning was sound and is worth
keeping even though the test is gone: a large program that somebody else wrote,
which exercises `/dev/fb0`, `/dev/input/event0`, `mmap`, the ELF loader,
`gettimeofday` and a fixed-rate tic loop *simultaneously* and notices when any
of them lie, is a much harsher test than anything written in-house — because it
was not written against our bugs.
It found real ones, all of which are fixed and covered by narrower tests now:
- `fcntl(F_SETFL, O_NONBLOCK)` was a documented no-op, so the first read blocked
forever and looked exactly like a hung renderer.
- `/dev/input/eventN` replayed its backlog to each new opener, handing a program
the keystrokes that had launched it as input.
- Without `EVIOCGRAB`, a full-screen program and the shell behind it both read
the same keystrokes.
It was removed once it stopped finding anything: the port was ~100k lines of
third-party source under licences that can never ship in an Apache-2.0 image,
and carrying it had a standing cost — 33 MB of non-redistributable data staged
into `sysroot/`, which is linked into the kernel binary for a ramdisk-root
build. That combination is a licensing accident waiting to happen, for a test
that was passing every time.
**What replaced it:** `tools/bootcheck.ps1` with its `-Sock`, `-Evdev` and `-Ps`
modes, plus `tools/splashshot.ps1` for the boot path. These are narrower and do
not prove the whole stack composes, which is the honest cost of the removal.
**The lesson worth keeping** is the shape of the test, not the program: the next
full-stack test should be something large that we did not write, driven to a
real pixel-level assertion rather than a log line. `tools/splashshot.ps1`'s
colour counting exists because a log-only harness once passed on a blank screen.
### Phase 8 — The system layer: `schinit`, firmware, and a crash channel
Three ideas parked here rather than started, because each one is a project and
the phases above are the agreed order. Recorded so they are not re-derived from
scratch later.
**`schinit`, a launchd-shaped init.** The point of copying launchd rather than
sysvinit is not the config format, it is *demand launching*: launchd holds the
listening socket or watches the path, and starts the job when something actually
arrives. That inverts the dependency graph — jobs stop needing to declare what
must start before them, because the thing they depend on answers on first use.
Schunix already has the two pieces that makes possible: `AF_UNIX` sockets
(`kernel/net/unix.c`) and a real `/proc`. What is missing is a supervisor that
can hold a socket on a job's behalf and hand it over on exec, plus a job
description format. Prerequisite for this being worth anything: `ps` and `top`
must be able to enumerate processes, or the supervisor is unobservable.
**A crash screen you can work in, and a QR code channel out of it.** PSOGM
(`kernel/core/panic.c`) currently renders and stops. The idea is two staged
things, and the staging matters:
- *An interactive mode — "a small OS inside the crash".* This is the right
framing and it is worth stating precisely, because it decides the design. At
panic time the kernel is the only thing running, which is exactly why it can
offer what a live system cannot: walk the stack, read another process's
memory, dump page tables, inspect the process table — with no scheduler and no
locking to respect, because nothing else can race you.
What makes it a *system* rather than a print statement is that it keeps the
base machine's drivers. Concretely, the ones that work with interrupts off:
the VGA/framebuffer (`vga_draw_pixel` is a plain memory write), the PS/2
keyboard (pollable via port 0x60/0x64), and the serial port. Those three are
a complete console. Everything else — IDE, USB, the timer, anything that
sleeps or takes a sleeplock — is off limits, because the lock it needs may be
precisely the one held by the CPU that just died.
So the shape is a **polled event loop over preallocated state**: no `kalloc`,
no `sleep`/`wakeup`, no `acquire` on anything a live path might hold, one
static command buffer, and a driver set restricted to the three above. That
restriction is not a limitation to work around later; it is the invariant that
makes the mode trustworthy when everything else is not. Anything that wants
more should be answered by rebooting into a recovery environment instead.
A useful precedent to read but not copy: DDB (NetBSD/FreeBSD's in-kernel
debugger) is exactly this, and its hard-won rule is the same one — a debugger
that can deadlock is worse than no debugger.
- *A QR code.* **Done.** `kernel/core/qr.c` (byte mode, EC level L, versions
1–6, no allocation) and a card on the right of the crash screen. A panic
report is worth nothing if it cannot leave a machine whose network stack is
dead, and a phone camera is an out-of-band channel needing no driver on our
side. Deliberately *not* a URL: it encodes the report itself and reaches
nothing.
It ended up unconditional rather than behind Enter, which is better — it
costs nothing to always draw, and requiring a keypress would mean the one
case where input is least trustworthy is also the case where the report is
hardest to retrieve. Verified end-to-end by `make crashshot && make crashqr`,
which decodes the symbol out of the captured framebuffer.
**UEFI.** Not a feature so much as the point at which the current bootloader
stops being extendable: it is 16-bit real mode, INT 10h VBE, and PIO ATA, none
of which exist on a UEFI-only machine. The realistic shape is a *second*
loader — an EFI application that gets the framebuffer from GOP, the memory map
from `GetMemoryMap()`, loads the kernel off a FAT ESP, and enters at the same
place `boot/stage2main.c` does — rather than a port of the existing one. Worth
noting that the splash work just done survives that transition: it is a mask, a
palette and a layout, none of which care how the pixels were found. What does
not survive is every assumption in `stage2main.c` about where things live in low
memory (see `docs/layout.md`).
### SchFS beyond Schunix
Three ideas parked 2026-08-08, in the order they should happen.
**An on-disk format version, and an upgrade path.** This one is not optional and
gets harder every day it is postponed: the moment an image exists that someone
cares about, changing the layout means being able to read the old one. The
superblock already carries a version field and a CRC32c; what is missing is a
*policy* — a documented rule for what a version bump means, a refusal to mount
something newer than the driver understands (rather than misreading it), and an
explicit upgrade step. Cheap now, expensive later, and it blocks the other two:
a second implementation is only meaningful against a format that is pinned.
**A Linux implementation.** The realistic first form is **FUSE**, not a kernel
module — it needs no GPL entanglement (`libfuse` is LGPL and used across a
process boundary), it can reuse the read/write logic already in `tools/mkschfs`
and `tools/schfs_fsck` rather than being written twice, and it turns "does our
filesystem work" into something testable with `fsx` and `xfstests`, which is a
far harsher examination than anything in this tree. A Linux *kernel* module is a
different project and carries the licence problem that
[licensing.md](licensing.md) describes.
**The missing features.** Worth listing honestly before adding any: no journal
replay across an unclean shutdown beyond what CoW gives for free, no snapshots
(despite the B-tree being copy-on-write, which is most of the work), no extended
attributes, no sparse-file support, and no online `fsck`. Snapshots are the one
where the existing design already pays for most of the cost.
---
## 3b. Known broken
Two things that are wrong *now*, both reported 2026-08-08 and both diagnosed
rather than merely observed. Neither blocks the GUI work, which is why they are
here and not in a phase.
### ~~SMP is broken~~ *(fixed 2026-08-08)*
**SMP works.** One, two and four processors all boot; every CPU reports in with
`cpu%d: starting`, and `/proc/cpuinfo` lists them all. `smp_start_aps` in
`kernel/core/main.c` is 1.
Five distinct bugs, each hidden behind the one before it, and all invisible
under QEMU because it publishes only one processor in the legacy MP table.
VMware published the real count, which is why the triple fault only ever
appeared there.
| | |
|---|---|
| CPU enumeration | `mpinit()` read the 1994 MP table. `kernel/arch/x86_64/acpi.c` now reads the ACPI MADT. |
| triple fault | `entry64high` had **no AP branch** — every AP took the BSP's stack and ran `kmain()` twice. Now tests `EBX == 1`. |
| `panic("acquire")` | `seginit()` called `kalloc()` before `%fs` existed; kalloc locks, locking reads `cpu`. BSP now preallocates each AP's page. |
| `panic("sched locks")` | `kernel_fs_base` was **one global** read by `trapasm.S` on every entry, so all CPUs shared one per-CPU block. Now `%gs:0`. |
| null `proc` in `sleep()` | `cpu`/`proc` were `__thread` — per *thread*, so the compiler cached the thread pointer across `sched()`, but `%fs` is per *CPU*. Now `mycpu()`/`myproc()`. |
| `#GP` in `syscall_entry` | The new per-CPU slots landed on `%fs:(8)`, the syscall scratch word — the same memory as `%gs:8`. Moved to +16 and +24. |
The `__thread` one is the lesson worth keeping: **`__thread` is wrong for
per-CPU kernel data.** It means per thread, and the compiler is entitled to
cache the thread pointer across calls on that basis. A kernel thread changes CPU
at every `sched()`, so the assumption is false for exactly the variables that
most need to be right. `include/proc.h` carries the disassembly that proves it.
Per-CPU block layout, shared with assembly and not to be reordered:
+0 self-pointer x86-64 TLS ABI (%fs:0)
+8 syscall scratch trapasm.S parks %rsp here
+16 struct cpu *
+24 struct proc *
### ~~The kernel demands 224 MB of RAM~~ *(fixed 2026-08-08)*
`kernel/arch/x86_64/e820.c` reads the firmware's memory map and sets `phystop`
from it. **The floor went from 224 MB to 32 MB**, and the kernel now sees all
the memory a machine actually has: 512 MB in gives `MemTotal: 524160 kB`, where
before it reported 224 MB whatever was installed.
Three details worth keeping:
- It takes the end of the usable region **containing the kernel**, not the
highest usable address anywhere. A memory map has reserved holes in it, and
`freerange()` walks straight from the end of the kernel image to `phystop`
handing out every page between — so the global maximum would put firmware
tables on the free list, failing later and elsewhere and looking like anything
but a memory map problem.
- The ceiling is **3 GB**, because `kvmalloc()` maps 0–4 GB as four 1 GB pages
and marks the top one uncached for PCI BARs.
- Below `PHYSTOP_MIN` it prints why and stops, rather than triple faulting in
silence. At 24 MB: `e820: only 23 MB usable; Schunix needs at least 24 MB`.
The original text is kept below because the reasoning still applies to the SMP
half of §3b, which is the same shape of bug: a constant nobody revisited.
### The kernel demanded 224 MB of RAM
**Symptom as reported:** the OS needs ~256 MB, which is absurd next to Windows 98
or an early Linux.
**Diagnosis:** it is one constant. `PHYSTOP` in `include/memlayout.h` is
`0xE000000` — 224 MB — inherited unchanged from xv6, and `main()` hands
`P2V(PHYSTOP)` straight to `kinit1()`/`dma_init()`. The kernel therefore
*assumes* that much physical memory exists and walks off the end of RAM if it
does not. Measured 2026-08-08: boots at 224M and above, triple faults at 192M
and below. Nothing is actually being *used* — the requirement is entirely
fictional.
**The fix is already half-built.** `boot/stage2.S` collects a full E820 memory
map in real mode and leaves it at `VBE_INFO_ADDR + 0x200` (count) and `+ 0x208`
(24-byte entries); `boot/stage2main.c` already parses it to print detected RAM.
**The kernel never reads it.** So: have `main()` take the top of the highest
usable E820 region instead of the constant, clamp it to what the direct map
covers, and keep `PHYSTOP` only as a fallback for the case where the firmware
returned nothing. That should drop the floor to something in the tens of
megabytes and is a contained change.
---
## 4. Kept out of scope on purpose
- Anything that needs pushing or publishing. This work stays local.

21
include/asm.h Normal file
View file

@ -0,0 +1,21 @@
//
// assembler macros to create x86 segments
//
#define SEG_NULLASM \
.word 0, 0; \
.byte 0, 0, 0, 0
// The 0xC0 means the limit is in 4096-byte units
// and (for executable segments) 32-bit mode.
#define SEG_ASM(type,base,lim) \
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
#define STA_X 0x8 // Executable segment
#define STA_E 0x4 // Expand down (non-executable segments)
#define STA_C 0x4 // Conforming code segment (executable only)
#define STA_W 0x2 // Writeable (non-executable segments)
#define STA_R 0x2 // Readable (executable segments)
#define STA_A 0x1 // Accessed

14
include/buf.h Normal file
View file

@ -0,0 +1,14 @@
struct buf {
int flags;
uint dev;
uint blockno;
struct sleeplock lock;
uint refcnt;
struct buf *prev; // LRU cache list
struct buf *next;
struct buf *qnext; // disk queue
uchar data[BSIZE];
};
#define B_VALID 0x2 // buffer has been read from disk
#define B_DIRTY 0x4 // buffer needs to be written to disk

3
include/config_sinit.h Normal file
View file

@ -0,0 +1,3 @@
static char *const rcinitcmd[] = { "/bin/sh", NULL };
static char *const rcrebootcmd[] = { "/bin/halt", "-rb", NULL };
static char *const rcpoweroffcmd[] = { "/bin/halt", "-po", NULL };

15
include/date.h Normal file
View file

@ -0,0 +1,15 @@
#pragma once
struct rtcdate {
uint second;
uint minute;
uint hour;
uint day;
uint month;
uint year;
};
struct timeval {
long tv_sec;
long tv_usec;
};

466
include/defs.h Normal file
View file

@ -0,0 +1,466 @@
#pragma once
struct buf;
struct context;
struct file;
struct inode;
struct pipe;
struct proc;
struct rtcdate;
struct spinlock;
struct sleeplock;
struct stat;
struct superblock;
struct trapframe;
struct dirent;
// entryother.S / syscall.S
void syscall_entry(void);
void ignore_sysret(void);
// bio.c
void binit(void);
struct buf* bread(uint, uint);
void brelse(struct buf*);
void bwrite(struct buf*);
// console.c
struct termios;
void consoleinit(void);
extern int tty_fg_pgrp;
// The terminal's line-discipline settings. TCGETS/TCSETS in sys_fs.c are the
// only callers; the console honours ICANON, ECHO, ECHOE, ECHONL, ISIG,
// ICRNL, INLCR, IGNCR, ISTRIP and VMIN.
void cons_get_termios(struct termios *);
void cons_set_termios(const struct termios *);
void cprintf(char*, ...);
void consoleintr(int(*)(void));
void panic(char*) __attribute__((noreturn));
// exec.c
int exec(char*, char**);
// file.c
struct file* filealloc(void);
int fdalloc(struct file*);
void fileclose(struct file*);
struct file* filedup(struct file*);
void fileinit(void);
int fileread(struct file*, char*, int n);
int file_poll(struct file*, int events);
int filestat(struct file*, struct stat*);
int filewrite(struct file*, char*, int n);
// fs.c
void fsinit(int);
int dirlink(struct inode*, char*, uint64);
struct inode* dirlookup(struct inode*, char*, uint*);
// Remove the entry `name` at the cookie dirlookup returned in *poff.
int dirunlink(struct inode*, char*, uint);
// 1 if the directory holds no real entries, 0 if it does, -1 on error.
int dirempty(struct inode*);
// Fill `out` with getdents64 records starting at *poff, which is an opaque
// position the filesystem advances. Returns bytes written, 0 at end of
// directory. The inode must be locked.
int getdents(struct inode*, char *out, int cap, uint64 *poff);
// Mount table: a substitution applied during path resolution. Nothing on disk
// records a mount, so unmounting is forgetting.
void mountinit(void);
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root);
int mount_del_by_root(uint fsdev, uint64 root);
struct inode* ialloc(uint, short);
struct inode* iget(uint, uint64);
struct inode* idup(struct inode*);
void iinit(void);
void ilock(struct inode*);
void iput(struct inode*);
void iunlock(struct inode*);
void iunlockput(struct inode*);
void iupdate(struct inode*);
int namecmp(const char*, const char*);
struct inode* namei(char*);
struct inode* nameiparent(char*, char*);
struct inode* nameifrom(struct inode*, char*);
struct inode* nameiparentfrom(struct inode*, char*, char*);
int readi(struct inode*, int, uint64, uint64, uint);
void stati(struct inode*, struct stat*);
int writei(struct inode*, int, uint64, uint64, uint);
void itrunc(struct inode*);
void itrunc_to(struct inode*, uint64);
unsigned int BKDRHash(char * str);
uint tree_expand(struct inode *ip, uint off);
void tree_search(struct inode* dp, char* name, uint* poff, struct dirent* de,uint add);
void build_dir_tree(struct inode* dp,uint64 pinum);
struct inode* divesymlink(struct inode *);
void readsb(int dev, struct superblock *sb);
// pci.c -- bus enumeration. Drivers that need to *find* their hardware use
// these; drivers for things that have lived at a fixed address since 1984
// (the PIC, the PIT, the legacy IDE ports) do not.
struct pci_dev;
struct pci_driver;
void pci_init(void);
uint32 pci_read32(uint8, uint8, uint8, uint8);
uint16 pci_read16(uint8, uint8, uint8, uint8);
uint8 pci_read8(uint8, uint8, uint8, uint8);
void pci_write32(uint8, uint8, uint8, uint8, uint32);
void pci_enable(struct pci_dev *, uint16 bits);
uint8 pci_find_cap(struct pci_dev *, uint8 id);
int pci_msi_enable(struct pci_dev *, int vector, int cpu);
int pci_count(void);
struct pci_dev *pci_get(int);
struct pci_dev *pci_find_class(uint8 class, uint8 subclass);
struct pci_dev *pci_find_id(uint16 vendor, uint16 device);
void pci_bind(const struct pci_driver *);
int pci_info(char *buf, int cap);
// drivers/registry.c -- the one list of every driver in the kernel.
void pci_bind_all(void);
// xhci.c — `echo u > /proc/sysrq`
void xhci_dump(void);
// ide.c
void ideinit(void);
void ideintr(void);
void iderw(struct buf*);
// For probes, not for reads: iderw() panics on an absent drive, so anything
// that is guessing whether a disk exists has to ask here first.
int ide_disk_present(int dev);
// fs/schfs/kmount.c -- SchFS.
//
// Objects are named by 64-bit object id, not by a path: resolution is the
// caller's business, so these compose with a VFS rather than assuming one.
// Every schfs_k* that changes anything is one transaction and is durable by the
// time it returns.
struct schfs_inode;
void schfs_kmount(uint dev);
int schfs_kmounted(void);
uint64 schfs_klookup(const char *path);
int schfs_kstat(uint64 ino, struct schfs_inode *out);
long schfs_kread(uint64 ino, uint64 off, void *buf, unsigned n);
int schfs_kreaddir(uint64 dir,
int (*fn)(uint64, unsigned short, const char *,
unsigned, void *),
void *arg);
int schfs_kinfo(char *buf, int cap);
int schfs_kcreate(uint64 dir, const char *name, unsigned namelen,
unsigned short type, uint mode,
unsigned short major, unsigned short minor, uint64 *out);
long schfs_kwrite(uint64 ino, uint64 off, const void *buf,
unsigned n);
int schfs_ktruncate(uint64 ino, uint64 size);
int schfs_kunlink(uint64 dir, const char *name, unsigned namelen);
int schfs_klink(uint64 dir, const char *name, unsigned namelen,
uint64 ino);
int schfs_krename(uint64 olddir, const char *oldname,
unsigned oldlen, uint64 newdir, const char *newname,
unsigned newlen);
int schfs_ksetattr(uint64 ino, const struct schfs_inode *in);
int schfs_ksync(void);
// Pieces the VFS layer (fs/schfs/vfs.c) needs, where a directory entry and the
// object it names are separate operations. The VFS keeps the link count.
uint64 schfs_klookup_at(uint64 dir, const char *name, unsigned len);
int schfs_kdirempty(uint64 dir);
int schfs_kalloc_object(unsigned short type, uint mode,
uint64 *out);
int schfs_kdiradd(uint64 dir, const char *name, unsigned len,
uint64 ino, unsigned short type);
int schfs_kdirdel(uint64 dir, const char *name, unsigned len);
int schfs_kfree(uint64 ino);
uint64 schfs_kroot(void);
// ramdisk.c
uint uptime(void);
// procfs.c
int procread(struct inode*, uint, char*, int);
int procwrite(struct inode*, uint, char*, int);
// Shared with the /proc filesystem (kernel/fs/procfs.c): one owns the
// namespace, the other the content.
int proc_gen_global(int id, char *buf, int cap);
int proc_write_sysrq(int user_src, uint64 src, uint n);
void procfs_mount(void);
// input.c -- raw evdev event devices
void inputinit(void);
int inputread(struct inode*, uint, char*, int);
int inputwrite(struct inode*, uint, char*, int);
int inputready(struct inode*);
void inputclose(struct inode*);
void inputopen(struct inode*);
int input_ioctl(struct inode*, int request, addr_t arg);
int input_grab(int dev, int on);
int input_is_grabbed(int dev);
void input_report(int dev, int type, int code, int value);
void input_sync(int dev);
int input_ready(int dev);
void input_ps2_scancode(int raw, int e0);
void input_key(int hid_usage, int down);
int input_hid_keycode(int hid_usage);
void input_mouse(int dx, int dy, int buttons);
void ramdiskinit(void);
void ramdiskrw(struct buf*);
int ramdisk_present(void);
// usb_storage.c
void usb_storage_rw(struct buf*);
int usb_storage_present(void);
void usb_storage_dump(void); // `echo s > /proc/sysrq`
// ioapic.c
void ioapicenable(int irq, int cpu);
void ioapicroute(int irq, int cpu, int level_low);
void ioapicmask(int irq);
extern uchar ioapicid;
void ioapicinit(void);
// dma.c — see include/dma.h for the full contract
void dma_init(void*, void*);
void *dma_alloc(uint64 size, uint64 align);
void dma_free(void*, uint64 size);
addr_t dma_phys(const void*);
uint64 dma_free_bytes(void);
uint64 dma_total_bytes(void);
// kalloc.c
char* kalloc(void);
uint64 kalloc_free_bytes(void);
void kfree(char*);
void kinit1(void*, void*);
void kinit2(void);
// kbd.c
void kbdintr(void);
// mouse.c
void mouseinit(void);
void mouseintr(void);
void mouse_inject(int dx, int dy, int buttons);
int get_mouse_x(void);
int get_mouse_y(void);
uint8 get_mouse_buttons(void);
// lapic.c
void cmostime(struct rtcdate *r);
// time.c -- wall-clock time, from the RTC read once at boot plus uptime.
void time_init(void);
void wallclock(uint64 *sec, uint64 *usec);
extern uint64 boot_epoch;
int cpunum(void);
extern volatile uint* lapic;
void lapiceoi(void);
void lapicinit(void);
void lapicstartap(uchar, uint);
void microdelay(int);
// log.c
void initlog(int dev);
void log_write(struct buf*);
void begin_op();
void end_op();
// mp.c
extern int ismp;
void mpinit(void);
// pipe.c
int pipealloc(struct file**, struct file**);
void pipeclose(struct pipe*, int);
int piperead(struct pipe*, char*, int);
int pipewrite(struct pipe*, char*, int);
// net/unix.c
struct socket;
void sockinit(void);
struct socket* socknew(int domain, int type);
void sockclose(struct socket*);
void sockdup(struct socket*);
int sockread(struct socket*, char*, int);
int sockwrite(struct socket*, char*, int);
int sockbind(struct socket*, const char *path);
int socklisten(struct socket*, int backlog);
struct socket* sockaccept(struct socket*);
int sockconnect(struct socket*, const char *path);
int sockreadable(struct socket*);
int sock_hup(struct socket*);
int sock_shutdown(struct socket*, int how);
int sockpair(struct socket **a, struct socket **b);
const char* sockname(struct socket*);
// proc.c
void exit(int status);
int fork(void);
int growproc(int64);
int kill(int);
int sigsend_pgrp(int pgid, int sig);
int proc_pgid(int pid);
int proc_setpgid(int pid, int pgid);
int proc_setsid(void);
int sigsend(int pid, int sig);
int proc_exists(int);
int proc_num(void);
struct procinfo;
int proc_snapshot(int idx, struct procinfo *out);
int proc_snapshot_pid(int pid, struct procinfo *out);
void pinit(void);
void procdump(void);
void scheduler(void) __attribute__((noreturn));
void sched(void);
void sleep(void*, struct spinlock*);
int wait(void);
int waitpid(int, int*, int);
void wakeup(void*);
void yield(void);
void sinit(void);
void kstack_check_canary(const char *where);
// swtch.S
void swtch(struct context**, struct context*);
// spinlock.c
void acquire(struct spinlock*);
void getcallerpcs(void*, addr_t*);
void getstackpcs(addr_t*, addr_t*);
int holding(struct spinlock*);
void initlock(struct spinlock*, char*);
void release(struct spinlock*);
void pushcli(void);
void popcli(void);
// sleeplock.c
void acquiresleep(struct sleeplock*);
void releasesleep(struct sleeplock*);
int holdingsleep(struct sleeplock*);
void initsleeplock(struct sleeplock*, char*);
// string.c
int memcmp(const void*, const void*, uint64);
void* memmove(void*, const void*, uint64);
void* memset(void*, int, uint64);
char* safestrcpy(char*, const char*, int);
int strlen(const char*);
int strnlen(const char*, int);
int strncmp(const char*, const char*, uint64);
char* strncpy(char*, const char*, int);
// syscall.c
void syscall(struct trapframe *);
void syscallinit(void);
void deliver_signals(struct trapframe *);
int argint(int, int*);
int argptr(int, char**, int);
int argstr(int, char**);
int argaddr(int, addr_t*);
int fetchaddr(addr_t, addr_t*);
int fetchstr(addr_t, char**);
int fetchint(addr_t, int*);
// trap.c
void idtinit(void);
extern uint ticks;
void tvinit(void);
extern struct spinlock tickslock;
// uart.c
void uartearlyinit(void);
void uartinit(void);
void uartintr(void);
void uartputc(int);
// vga.c
void vga_init(uint64);
void vga_draw_pixel(int, int, uint32);
void vga_draw_char(int, int, uchar, uint32);
void vga_puts(int, int, char*, uint32);
void vga_putc(int, uint32);
void vga_clear(uint32);
void vga_screen_reset(void);
void vga_set_bg(uint32);
// Crash-screen primitives: the panic handler lays itself out rather than
// printing. See kernel/core/panic.c.
int vga_ready(void);
void vga_dims(int *w, int *h);
int vga_char_w(void);
int vga_char_h(void);
// Framebuffer geometry. vga.c reads the VBE mode info, so it is the only
// thing that knows where the framebuffer is; nobody else may assume.
uint64 vga_fb_phys(void);
uint64 vga_fb_size(void);
uint32 vga_fb_stride(void);
uint32 vga_fb_bpp(void);
volatile uchar *vga_fb_ptr(void);
// fb.c -- /dev/fb0, major 2.
void fbclose(struct inode*);
// Called from sys_mmap: a mapping of /dev/fb0 is a licence to draw with no
// further syscalls, so it is the last the kernel hears about it.
void fb_mapped(void);
int fbread(struct inode*, uint, char*, int);
int fbwrite(struct inode*, uint, char*, int);
int fb_ioctl(int request, addr_t arg);
// drivers/net/e1000 -- raw Ethernet. No protocol stack above it yet, so these
// are the whole interface: queue a frame, take a frame, and the MAC.
int e1000_transmit(const void *frame, int len);
int e1000_receive(void *out, int cap);
const unsigned char *e1000_mac(void);
void e1000_selftest(void);
// registry.c
void platform_register_devices(void);
void e820_init(void);
int acpi_init(void);
void vga_fill_rect(int x, int y, int w, int h, uint32);
void vga_fill_circle(int cx, int cy, int r, uint32);
void vga_text_at(int, int, const char*, uint32);
void vga_draw_logo(int x, int y, uint32 color, uint32 bg);
void vga_logo_centered(int cy, uint32 color, uint32 bg);
// Cursor addressing and erasing, for the console's ANSI escape parser.
int vga_cols(void);
int vga_rows(void);
void vga_cursor_get(int *col, int *row);
void vga_cursor_set(int col, int row);
void vga_erase_display(int mode);
void vga_erase_line(int mode);
// console.c / panic.c
void cons_panic_mode(void);
extern int cons_serial_only;
void console_takeover(void);
// vm.c
void seginit(void);
void kvmalloc(void);
pml4e_t* setupkvm(void);
char* uva2ka(pml4e_t*, char*);
addr_t allocuvm(pml4e_t*, uint64, uint64);
addr_t deallocuvm(pml4e_t*, uint64, uint64);
void freevm(pml4e_t*);
void inituvm(pml4e_t*, char*, uint64);
int loaduvm(pml4e_t*, char*, struct inode*, uint64, uint64);
pml4e_t* copyuvm(pml4e_t*, uint64);
void switchuvm(struct proc*);
void switchkvm(void);
int pipe_poll(struct pipe*, int readable, int writable);
int console_input_ready(void);
int copyin(pml4e_t*, addr_t, void*, uint64);
int copyout(pml4e_t*, addr_t, void*, uint64);
void vm_unmap_range(pml4e_t*, uint64, uint64);
// Unmap without freeing: for pages this process does not own — device memory
// and shared segments. vm_unmap_range() would hand them to kfree.
void vm_unmap_norelease(pml4e_t*, uint64, uint64);
// Shared memory (sys_mem.c). shm_ref/shm_detach maintain the attachment
// count that decides when a segment's pages go back.
void shm_ref(int id);
void shm_detach(int id);
void proc_release_shared(struct proc *);
int copy_vma_range(pml4e_t *dstpgdir, pml4e_t *srcpgdir, uint64 va, uint64 length, int is_fb);
void clearpteu(pml4e_t *pgdir, char *uva);
#define NELEM(x) (sizeof(x)/sizeof((x)[0]))

89
include/device.h Normal file
View file

@ -0,0 +1,89 @@
#pragma once
// The device model: one list of everything attached, whatever bus found it.
//
// Before this there were two registries in kernel/drivers/registry.c -- one
// array of PCI drivers, one of USB drivers -- and nothing that knew about both.
// That is fine for binding drivers to hardware, which is all it was for, and
// useless for the three things wanted next:
//
// *Enumeration.* "What is in this machine, and what claimed it" had no
// answer that covered a USB disk and a PCI display at once. A device manager
// is exactly that question, so it needs exactly this list.
//
// *Classes.* A compositor should ask for "the display" and not care whether
// that is the VBE framebuffer or virtio-gpu. Without a class, every consumer
// hardcodes a driver, and adding the second driver means editing the
// consumers -- which is how a framebuffer address ends up hardcoded in three
// places, as it once was here.
//
// *A boundary for drivers that are not compiled in.* Drivers the user
// installs (see docs/drivers.md) have to attach somewhere that is not "an
// array in registry.c", because that array is fixed at link time.
//
// The shape is deliberately the one NeXTSTEP/macOS uses rather than Linux's:
// a device is an object with a class and an ops vtable, and consumers talk to
// the class. It is *not* a device tree -- there is no parent/child nesting
// here, only a flat list with a bus tag, because nothing yet needs to walk a
// hierarchy and a tree that nobody traverses is just a harder list.
#define DEV_NAMELEN 24
// Which bus found it. This is provenance, not capability -- what the device
// *does* is its class.
enum dev_bus {
DEVBUS_PLATFORM = 0, // found by looking, not by enumerating: PS/2, VBE
DEVBUS_PCI,
DEVBUS_USB,
DEVBUS_NBUS
};
// What it is. A consumer asks for a class and gets whatever is bound.
enum dev_class {
DEVCLASS_OTHER = 0,
DEVCLASS_DISPLAY,
DEVCLASS_INPUT,
DEVCLASS_STORAGE,
DEVCLASS_NET,
DEVCLASS_BUS, // a controller that finds more devices (xHCI, hubs)
DEVCLASS_NCLASS
};
struct device {
char name[DEV_NAMELEN]; // "vga0", "xhci0" -- unique, human-readable
char desc[48]; // free text for the enumerator
enum dev_bus bus;
enum dev_class class;
// Where it came from, for the enumerator to print. Zero when not applicable.
uint16 vendor, product;
uint8 b, s, f; // PCI bus/slot/func
const char *driver; // whatever claimed it, or 0 for unclaimed
void *ops; // class-specific vtable; see display.h
void *priv; // the driver's own state
int in_use;
};
#define NDEVICE 32
// Register a device. Returns it, or 0 if the table is full. The name is made
// unique by appending an index, so a caller passes "vga" and gets "vga0".
struct device *device_add(const char *base, enum dev_bus bus,
enum dev_class class, const char *driver);
// Set the human-readable description. Bounded copy into the fixed field, so
// callers do not each need to know how long it is.
void device_set_desc(struct device *, const char *);
// Walk. Returns 0 past the end; index is dense over registered devices.
struct device *device_at(int i);
int device_count(void);
// First device of a class with a bound driver, or 0. This is how a consumer
// finds "the display" without naming a driver.
struct device *device_find_class(enum dev_class class);
const char *dev_bus_name(enum dev_bus);
const char *dev_class_name(enum dev_class);

56
include/display.h Normal file
View file

@ -0,0 +1,56 @@
#pragma once
// The display class: what a consumer talks to instead of a driver.
//
// A compositor should ask for "the display" and get whatever is bound --
// today the VBE framebuffer, later virtio-gpu. Without this, every consumer
// names a driver, and adding the second display driver means editing all of
// them. That is not hypothetical here: the framebuffer's physical address was
// once hardcoded in three separate places.
//
// Found with device_find_class(DEVCLASS_DISPLAY); see include/device.h.
struct device;
struct display_mode {
int width, height;
int bpp;
uint32 stride; // bytes per row of the *visible* framebuffer
};
struct display_ops {
void (*mode)(struct device *, struct display_mode *);
// The buffer to draw into, and its stride. Returns 0 if double buffering is
// unavailable, in which case the caller should draw directly and accept
// tearing rather than not drawing at all.
//
// Allocated on first call, never at boot: it is around two megabytes at
// 800x600, and a machine whose compositor never starts should not pay that.
void *(*acquire)(struct device *, uint32 *stride_out);
// Copy a rectangle of the back buffer to the display. Coordinates are in
// pixels; the whole screen is (0, 0, width, height).
void (*present)(struct device *, int x, int y, int w, int h);
// Block until the display is between frames. Optional -- a driver that
// cannot tell leaves this null, and present() then simply tears less than
// drawing directly would.
void (*wait_vblank)(struct device *);
};
// The bound display, or 0. Convenience over device_find_class().
struct device *display_get(void);
// Wrappers that do nothing gracefully when there is no display or the driver
// left an op null, so callers are not each written around a maybe-null vtable.
int display_mode(struct display_mode *out);
void *display_acquire(uint32 *stride_out);
void display_present(int x, int y, int w, int h);
void display_wait_vblank(void);
// Drive the whole path once and draw a test pattern. `echo d > /proc/sysrq`.
void display_selftest(void);
// Draw a text specimen through the display class. `echo t > /proc/sysrq`.
void text_selftest(void);

43
include/dma.h Normal file
View file

@ -0,0 +1,43 @@
#pragma once
// Physically contiguous memory for devices that read and write it themselves.
//
// kalloc() hands out 4 KB pages from a free list, in whatever order they were
// released. That is exactly what a page table wants and exactly what a device
// cannot use: an xHCI command ring, a NIC's descriptor array or a virtio
// queue is a single physically contiguous object that the hardware walks
// without any idea that paging exists. Two kalloc'd pages are adjacent in the
// kernel's address space only by luck.
//
// So DMA memory comes from an arena carved out of the top of physical memory
// before kalloc ever sees it, and is handed out contiguous and aligned.
//
// On x86 this memory needs no special cacheability. The PCI bus is cache
// coherent: a device's write invalidates the corresponding cache line, and a
// CPU write is snooped out of the store buffer before the device sees stale
// data. Architectures without that guarantee need explicit cache maintenance
// around every descriptor access, and porting this file is where that would
// go. What is *not* free even here is ordering -- the compiler and the CPU
// will happily reorder stores to a ring, so a driver still needs a barrier
// between filling a descriptor and telling the device about it.
#define DMA_ARENA_BYTES (4*1024*1024)
void dma_init(void *vstart, void *vend);
// Zeroed, physically contiguous, at least `align`-aligned (align must be a
// power of two). Returns a kernel virtual address, or 0.
void *dma_alloc(uint64 size, uint64 align);
// `size` must be the size the allocation was made with, as in Linux's
// dma_free_coherent(). Tracking it here would mean a second table whose only
// job is to remember something the caller already knows.
void dma_free(void *p, uint64 size);
// The address to give the device. Kernel memory is direct-mapped, so this is
// just V2P -- but drivers should say what they mean, because on a machine
// with an IOMMU these stop being the same number.
addr_t dma_phys(const void *p);
uint64 dma_free_bytes(void);
uint64 dma_total_bytes(void);

165
include/e1000.h Normal file
View file

@ -0,0 +1,165 @@
#pragma once
// Intel 8254x gigabit Ethernet — the register and descriptor layout.
//
// From Intel's public "PCI/PCI-X Family of Gigabit Ethernet Controllers
// Software Developer's Manual" (317453). A public document with no licence on
// what you write from it, which is why this is written rather than ported: see
// docs/licensing.md and docs/roadmap.md §1.
//
// QEMU's `-device e1000` is an 82540EM and implements enough of the manual to
// be worth targeting; it is also what the default `-net nic` gives you.
#define E1000_VENDOR 0x8086
// The 8254x family: PCI/PCI-X gigabit controllers that share the register map,
// descriptor format and EEPROM interface this driver implements.
//
// The device IDs are transcribed from FreeBSD's `sys/dev/e1000/e1000_hw.h`
// (Copyright (c) 2001-2020, Intel Corporation, BSD 3-clause -- verified to
// carry no advertising clause; see docs/licensing.md and NOTICE). They are
// data, not logic: the driver below is still written from Intel's manual
// 317453, and nothing of FreeBSD's implementation is used.
//
// This matters more than it looks. The driver bound exactly one device -- the
// 82540EM QEMU emulates -- which is fine for a virtual machine and useless on
// the sort of second-hand hardware a system like this actually gets run on.
// These are the cards in every desktop and server of that generation.
//
// Deliberately *not* included: the PCIe 8257x parts (82571-82574) and anything
// newer. They present a similar register map but differ in NVM access and
// initialisation, and claiming a device this driver has not been shown to
// initialise is worse than not claiming it -- the card binds, reports a MAC of
// nonsense, and nothing works for reasons that look nothing like the cause.
#define E1000_DEV_82540EM 0x100E
#define E1000_DEV_82540EM_LOM 0x1015
#define E1000_DEV_82540EP_LOM 0x1016
#define E1000_DEV_82540EP 0x1017
#define E1000_DEV_82540EP_LP 0x101E
#define E1000_DEV_82541EI 0x1013
#define E1000_DEV_82541EI_MOBILE 0x1018
#define E1000_DEV_82541ER_LOM 0x1014
#define E1000_DEV_82541ER 0x1078
#define E1000_DEV_82541GI 0x1076
#define E1000_DEV_82541GI_MOBILE 0x1077
#define E1000_DEV_82541GI_LF 0x107C
#define E1000_DEV_82544EI_FIBER 0x1009
#define E1000_DEV_82544GC_COPPER 0x100C
#define E1000_DEV_82544GC_LOM 0x100D
#define E1000_DEV_82545EM_COPPER 0x100F
#define E1000_DEV_82545EM_FIBER 0x1011
#define E1000_DEV_82545GM_COPPER 0x1026
#define E1000_DEV_82545GM_FIBER 0x1027
#define E1000_DEV_82545GM_SERDES 0x1028
#define E1000_DEV_82546EB_COPPER 0x1010
#define E1000_DEV_82546EB_FIBER 0x1012
#define E1000_DEV_82546EB_QUAD 0x101D
#define E1000_DEV_82546GB_COPPER 0x1079
#define E1000_DEV_82546GB_FIBER 0x107A
#define E1000_DEV_82546GB_SERDES 0x107B
#define E1000_DEV_82547EI 0x1019
// --- registers, byte offsets into BAR 0 ------------------------------------
#define E1000_CTRL 0x0000 // device control
#define E1000_STATUS 0x0008 // device status
#define E1000_EERD 0x0014 // EEPROM read
#define E1000_ICR 0x00C0 // interrupt cause read
#define E1000_IMS 0x00D0 // interrupt mask set
#define E1000_IMC 0x00D8 // interrupt mask clear
#define E1000_RCTL 0x0100 // receive control
#define E1000_TCTL 0x0400 // transmit control
#define E1000_TIPG 0x0410 // transmit inter-packet gap
#define E1000_RDBAL 0x2800 // receive descriptor base, low
#define E1000_RDBAH 0x2804 // receive descriptor base, high
#define E1000_RDLEN 0x2808 // receive descriptor length, in bytes
#define E1000_RDH 0x2810 // receive descriptor head
#define E1000_RDT 0x2818 // receive descriptor tail
#define E1000_TDBAL 0x3800
#define E1000_TDBAH 0x3804
#define E1000_TDLEN 0x3808
#define E1000_TDH 0x3810
#define E1000_TDT 0x3818
#define E1000_MTA 0x5200 // multicast table array, 128 entries
#define E1000_RAL 0x5400 // receive address low (the MAC filter)
#define E1000_RAH 0x5404 // receive address high
// CTRL
#define E1000_CTRL_RST (1 << 26)
#define E1000_CTRL_ASDE (1 << 5) // auto-speed detection
#define E1000_CTRL_SLU (1 << 6) // set link up
// STATUS
#define E1000_STATUS_LU (1 << 1) // link up
// EERD
#define E1000_EERD_START (1 << 0)
#define E1000_EERD_DONE (1 << 4)
#define E1000_EERD_ADDR_SHIFT 8
#define E1000_EERD_DATA_SHIFT 16
// RCTL
#define E1000_RCTL_EN (1 << 1) // receiver enable
#define E1000_RCTL_SBP (1 << 2) // store bad packets
#define E1000_RCTL_UPE (1 << 3) // unicast promiscuous
#define E1000_RCTL_MPE (1 << 4) // multicast promiscuous
#define E1000_RCTL_LBM_NONE (0 << 6)
#define E1000_RCTL_BAM (1 << 15) // accept broadcast
#define E1000_RCTL_SZ_2048 (0 << 16) // buffer size, with BSEX clear
#define E1000_RCTL_SECRC (1 << 26) // strip the Ethernet CRC
// TCTL
#define E1000_TCTL_EN (1 << 1)
#define E1000_TCTL_PSP (1 << 3) // pad short packets
#define E1000_TCTL_CT_SHIFT 4 // collision threshold
#define E1000_TCTL_COLD_SHIFT 12 // collision distance
// Transmit descriptor command bits
#define E1000_TXD_CMD_EOP (1 << 0) // end of packet
#define E1000_TXD_CMD_IFCS (1 << 1) // insert FCS/CRC
#define E1000_TXD_CMD_RS (1 << 3) // report status
#define E1000_TXD_STAT_DD (1 << 0) // descriptor done
// Receive descriptor status bits
#define E1000_RXD_STAT_DD (1 << 0)
#define E1000_RXD_STAT_EOP (1 << 1)
// Interrupt causes worth unmasking.
#define E1000_ICR_TXDW (1 << 0) // transmit descriptor written back
#define E1000_ICR_LSC (1 << 2) // link status change
#define E1000_ICR_RXDMT0 (1 << 4) // receive descriptor minimum threshold
#define E1000_ICR_RXT0 (1 << 7) // receiver timer
// --- descriptors -----------------------------------------------------------
//
// Both are 16 bytes and the hardware reads them by physical address, so the
// rings must be physically contiguous and 16-byte aligned.
struct e1000_tx_desc {
uint64 addr;
uint16 length;
uint8 cso; // checksum offset
uint8 cmd;
uint8 status;
uint8 css; // checksum start
uint16 special;
} __attribute__((packed));
struct e1000_rx_desc {
uint64 addr;
uint16 length;
uint16 checksum;
uint8 status;
uint8 errors;
uint16 special;
} __attribute__((packed));
// Ring sizes. The manual requires the descriptor ring length to be a multiple
// of 128 bytes, i.e. a multiple of 8 descriptors.
#define E1000_NTX 16
#define E1000_NRX 16
#define E1000_BUFSZ 2048

35
include/elf.h Normal file
View file

@ -0,0 +1,35 @@
#define ELF_MAGIC 0x464C457FU
struct elfhdr {
uint32 magic;
uchar elf[12];
uint16 type;
uint16 machine;
uint32 version;
uint64 entry;
uint64 phoff;
uint64 shoff;
uint32 flags;
uint16 ehsize;
uint16 phentsize;
uint16 phnum;
uint16 shentsize;
uint16 shnum;
uint16 shstrndx;
};
struct proghdr {
uint32 type;
uint32 flags;
uint64 off;
uint64 vaddr;
uint64 paddr;
uint64 filesz;
uint64 memsz;
uint64 align;
};
#define ELF_PROG_LOAD 1
#define ELF_PROG_FLAG_EXEC 1
#define ELF_PROG_FLAG_WRITE 2
#define ELF_PROG_FLAG_READ 4

60
include/errno.h Normal file
View file

@ -0,0 +1,60 @@
// Error numbers, matching the Linux x86_64 ABI values so that musl-linked
// programs interpret syscall failures correctly. Kernel code returns these
// negated (e.g. `return -ENOSYS;`), exactly like Linux does internally.
#ifndef SCHUNIX_ERRNO_H
#define SCHUNIX_ERRNO_H
#define EPERM 1 // Operation not permitted
#define ENOENT 2 // No such file or directory
#define ESRCH 3 // No such process
#define EINTR 4 // Interrupted system call
#define EIO 5 // I/O error
#define ENXIO 6 // No such device or address
#define E2BIG 7 // Argument list too long
#define ENOEXEC 8 // Exec format error
#define EBADF 9 // Bad file descriptor
#define ECHILD 10 // No child processes
#define EAGAIN 11 // Try again
#define ENOMEM 12 // Out of memory
#define EACCES 13 // Permission denied
#define EFAULT 14 // Bad address
#define EBUSY 16 // Device or resource busy
#define EEXIST 17 // File exists
#define EXDEV 18 // Cross-device link
#define ENODEV 19 // No such device
#define ENOTDIR 20 // Not a directory
#define EISDIR 21 // Is a directory
#define EINVAL 22 // Invalid argument
#define ENFILE 23 // File table overflow
#define EMFILE 24 // Too many open files
#define ENOTTY 25 // Not a typewriter
#define EFBIG 27 // File too large
#define ENOSPC 28 // No space left on device
#define ESPIPE 29 // Illegal seek
#define EROFS 30 // Read-only file system
#define EMLINK 31 // Too many links
#define EPIPE 32 // Broken pipe
#define ERANGE 34 // Math result not representable
#define ENAMETOOLONG 36 // File name too long
#define ENOSYS 38 // Function not implemented
#define ENOTEMPTY 39 // Directory not empty
#define ELOOP 40 // Too many symbolic links encountered
// Sockets. Same Linux x86_64 values, so strerror() in a musl-linked program
// prints the message the programmer expected rather than a neighbouring one.
#define ENOTSOCK 88 // Socket operation on non-socket
#define EDESTADDRREQ 89 // Destination address required
#define EMSGSIZE 90 // Message too long
#define EPROTOTYPE 91 // Protocol wrong type for socket
#define ENOPROTOOPT 92 // Protocol not available
#define EPROTONOSUPPORT 93 // Protocol not supported
#define EOPNOTSUPP 95 // Operation not supported on transport endpoint
#define EAFNOSUPPORT 97 // Address family not supported by protocol
#define EADDRINUSE 98 // Address already in use
#define EADDRNOTAVAIL 99 // Cannot assign requested address
#define ECONNRESET 104 // Connection reset by peer
#define EISCONN 106 // Transport endpoint is already connected
#define ENOTCONN 107 // Transport endpoint is not connected
#define ECONNREFUSED 111 // Connection refused
#endif // SCHUNIX_ERRNO_H

95
include/fb.h Normal file
View file

@ -0,0 +1,95 @@
#pragma once
// Framebuffer device ABI -- /dev/fb0.
//
// These are Linux's fbdev request numbers and structure layouts, not ours.
// Schunix invented this device, so nothing forced the choice; matching Linux
// was still the right one. A framebuffer is the first thing a graphics
// program looks for, and every such program already knows how to ask
// FBIOGET_VSCREENINFO. Inventing a private struct would have meant patching
// each one we ever port, and -- worse -- a Linux-ABI binary calling
// FBIOGET_VSCREENINFO would have gotten a *differently shaped* answer to a
// request it thought it understood, which is the failure mode that made
// `>>` truncate back when include/fcntl.h held xv6's open flags instead of
// Linux's. Same lesson, applied before it bites rather than after.
//
// Plain C types throughout (no types.h) so userspace can include this
// directly. Both sides are LP64.
#define FBIOGET_VSCREENINFO 0x4600
#define FBIOPUT_VSCREENINFO 0x4601
#define FBIOGET_FSCREENINFO 0x4602
#define FBIOBLANK 0x4611
// fb_fix_screeninfo.type
#define FB_TYPE_PACKED_PIXELS 0
// fb_fix_screeninfo.visual
#define FB_VISUAL_TRUECOLOR 2
// fb_var_screeninfo.activate
#define FB_ACTIVATE_NOW 0
// One colour channel's position within a pixel. `offset` is the bit position
// of the channel's least significant bit; `length` is how many bits it gets.
// A length of 0 means the channel does not exist -- which is how an opaque
// framebuffer reports `transp`.
struct fb_bitfield {
unsigned int offset;
unsigned int length;
unsigned int msb_right; // != 0 means bit 0 is the MSB; never here
};
struct fb_var_screeninfo {
unsigned int xres; // visible resolution
unsigned int yres;
unsigned int xres_virtual; // virtual resolution; == visible, no panning
unsigned int yres_virtual;
unsigned int xoffset; // offset of visible area within virtual
unsigned int yoffset;
unsigned int bits_per_pixel;
unsigned int grayscale; // 0 == colour
struct fb_bitfield red;
struct fb_bitfield green;
struct fb_bitfield blue;
struct fb_bitfield transp;
unsigned int nonstd; // 0 == standard pixel format
unsigned int activate;
unsigned int height; // physical size in mm; 0 == unknown, and
unsigned int width; // VBE does not tell us, so it stays 0
unsigned int accel_flags;
// Timings. The mode is whatever the bootloader's VBE call established and
// we cannot change it, so these are all reported as zero rather than
// invented.
unsigned int pixclock;
unsigned int left_margin;
unsigned int right_margin;
unsigned int upper_margin;
unsigned int lower_margin;
unsigned int hsync_len;
unsigned int vsync_len;
unsigned int sync;
unsigned int vmode;
unsigned int rotate;
unsigned int colorspace;
unsigned int reserved[4];
};
struct fb_fix_screeninfo {
char id[16]; // "schunixfb"
unsigned long smem_start; // physical address of the framebuffer
unsigned int smem_len; // its length in bytes
unsigned int type;
unsigned int type_aux;
unsigned int visual;
unsigned short xpanstep; // 0 == panning unsupported
unsigned short ypanstep;
unsigned short ywrapstep;
unsigned int line_length; // bytes per scanline (the VBE stride, which
// is NOT always xres * bytes_per_pixel)
unsigned long mmio_start; // no separate register aperture
unsigned int mmio_len;
unsigned int accel; // no acceleration
unsigned short capabilities;
unsigned short reserved[2];
};

43
include/fcntl.h Normal file
View file

@ -0,0 +1,43 @@
#pragma once
// open(2) flags.
//
// These are the *Linux x86-64* values, and they have to be: every program in
// the userland is built against a libc that sends Linux flag values, and the
// kernel reads them with these constants. They disagreed for a long time, with
// results that looked like filesystem corruption rather than like a mistake --
// Linux O_APPEND (0x400) landed on Schunix's O_TRUNC, so `cmd >> file`
// truncated the file it was meant to extend, and Linux O_TRUNC (0x200) landed
// on O_CREATE, so `cmd > file` left whatever was past the new end in place.
// Neither failure pointed anywhere near a header.
//
// Do not tidy these into a neat sequence. They are an external interface.
#define O_RDONLY 0x000000
#define O_WRONLY 0x000001
#define O_RDWR 0x000002
#define O_ACCMODE 0x000003
#define O_CREATE 0x000040 /* Linux O_CREAT (0100) */
#define O_CREAT O_CREATE /* the spelling every program uses */
#define O_EXCL 0x000080 /* Linux O_EXCL (0200) */
#define O_NOCTTY 0x000100 /* Linux O_NOCTTY (0400) */
#define O_TRUNC 0x000200 /* Linux O_TRUNC (01000) */
#define O_APPEND 0x000400 /* Linux O_APPEND (02000) */
#define O_NONBLOCK 0x000800 /* Linux O_NONBLOCK (04000); no-op here */
#define O_DIRECTORY 0x010000 /* Linux O_DIRECTORY (0200000) */
#define O_NOFOLLOW 0x020000 /* Linux O_NOFOLLOW (0400000) */
#define O_CLOEXEC 0x080000 /* Linux O_CLOEXEC (02000000) */
// Schunix-only, placed above every flag Linux defines so it can never be
// mistaken for one. 0x40000 was used before and collides with Linux O_NOATIME.
#define O_EXTENT 0x8000000 /* sparse-extent hint */
#define F_DUPFD 0
#define F_GETFD 1
#define F_SETFD 2
#define F_GETFL 3
#define F_SETFL 4
#define F_DUPFD_CLOEXEC 1030 // Linux value
#define FD_CLOEXEC 1

132
include/file.h Normal file
View file

@ -0,0 +1,132 @@
struct file {
enum { FD_NONE, FD_PIPE, FD_INODE, FD_SOCKET } type;
int ref; // reference count
char readable;
char writable;
// O_NONBLOCK. A property of the open file description, not of the
// descriptor, which is why it lives here and not in myproc()->ofile_flags --
// dup() shares it and F_SETFL on either fd changes it for both, exactly as
// POSIX says.
char nonblock;
struct pipe *pipe;
struct inode *ip;
struct socket *sock;
uint64 off;
};
// ---------------------------------------------------------------------------
// Filesystem operations
//
// One table per filesystem, chosen by iget() from the device number and hung
// off the inode. Everything above this line -- the syscall layer, exec, the
// page cache -- calls the wrappers in fs.c and never learns which filesystem it
// is talking to.
//
// A vtable rather than a chain of `if (dev == SCHFS_DEV)` because there are two
// filesystems today and there will be more: the whole point of the exercise is
// that adding one is a new table, not another branch in fifteen functions.
//
// Every hook takes an inode that is already locked, except ialloc (which has no
// inode yet) and iload (which is what makes one valid in the first place).
// ---------------------------------------------------------------------------
struct stat;
struct fs_ops {
const char *name;
// Fill in the type/size/link fields of a freshly iget()'d inode.
void (*iload)(struct inode *);
// Write those fields back to disk.
void (*istore)(struct inode *);
// The last link is gone and nobody has it open: release the object.
void (*ifree)(struct inode *);
int (*read)(struct inode *, int user_dst, uint64 dst, uint64 off, uint n);
int (*write)(struct inode *, int user_src, uint64 src, uint64 off, uint n);
// Set the length. Growing is allowed only where the filesystem can represent
// the gap; v1 cannot and ignores it.
void (*truncate)(struct inode *, uint64 newsize);
// Directory operations. `poff` is an opaque cookie identifying where the
// entry lives, meaningful only to the same filesystem's dirunlink.
struct inode *(*dirlookup)(struct inode *dp, char *name, uint *poff);
int (*dirlink)(struct inode *dp, char *name, uint64 inum);
int (*dirunlink)(struct inode *dp, char *name, uint off);
int (*dirempty)(struct inode *dp);
// Create an object with no name yet. The caller sets nlink and calls
// dirlink; until it does, the object is unreachable.
struct inode *(*ialloc)(uint dev, short type);
// Read directory entries into a getdents64-shaped stream. `off` is an opaque
// position that starts at 0 and is echoed back; returns bytes written, 0 at
// the end of the directory, or negative on error.
int (*getdents)(struct inode *dp, char *out, int cap, uint64 *off);
};
// In-memory inode.
//
// `inum` and `size` are 64-bit because the filesystem underneath may be: SchFS
// object ids and file sizes are both uint64 on disk, and a 32-bit copy here
// would silently truncate them into a different file. The v1 filesystem uses a
// small fraction of the range and is unaffected.
struct inode {
uint dev;
uint64 inum;
int ref;
struct sleeplock lock;
int valid;
const struct fs_ops *ops; // chosen by iget from dev; never null once held
short type;
short major;
short minor;
short nlink;
uint64 size;
uint rwmode;
uint supermode;
uint showmode;
uint atime;
uint mtime;
uint ctime;
uint addrs[NDIRECT+3];
};
#define I_VALID 0x2
// Device number that means "the mounted SchFS volume". Real disks are 0 and 1,
// and this is deliberately far from them so a stray value never looks like a
// SchFS inode.
#define SCHFS_DEV 64
// Device number that means "the synthetic /proc". Like SCHFS_DEV, it is
// deliberately nowhere near a real disk's 0 or 1.
#define PROCFS_DEV 65
// table mapping major device number to
// device functions
struct devsw {
int (*read)(struct inode*, uint, char*, int);
int (*write)(struct inode*, uint, char*, int);
// Optional. Returns 0 when a read would block, which is the only thing
// fileread needs to know to honour O_NONBLOCK without every device driver
// growing a flags argument. A device that omits this is treated as always
// ready, which is the right answer for a disk and for /dev/zero.
int (*ready)(struct inode*);
// Optional. The last descriptor for this open file is going away. A device
// that hands a program exclusive-feeling control of the hardware needs this
// to put the hardware back -- /dev/fb0 uses it to give the text console its
// screen back when whatever was drawing on it exits.
void (*close)(struct inode*);
// Optional. A descriptor has just been opened on this device. /dev/input
// uses it to discard events that arrived while nobody was listening -- see
// inputopen().
void (*open)(struct inode*);
};
extern struct devsw devsw[];
#define CONSOLE 1
// Major number of /dev/input/eventN. The minor is the device index.
#define INPUT_MAJOR 7

4627
include/font.h Normal file

File diff suppressed because it is too large Load diff

203
include/fs.h Normal file
View file

@ -0,0 +1,203 @@
// SCHFS - On-disk file system format.
// Both the kernel and user programs use this header file.
//
// Changes from original:
// - struct dinode gains atime, mtime, ctime (timestamps) and checksum
// (CRC32c). Padding reduced from 44 to 28 bytes; struct stays 128 bytes.
// - struct superblock gains checksum field.
// - struct fsv1_extent added for T_EXTENT inode block mapping.
// - OVERFLOW_DIR_START / OVERFLOW_DIR_END replace the bare "113 * BSIZE"
// magic numbers used in fs.c and mkfs.c.
#ifndef FS_H
#define FS_H
#define ROOTINO 1 // root i-number
#define BSIZE 1024 // block size
// Permission mode bits (rwmode field)
#define RW 3 // 11 read+write
#define W_ 1 // 01 write-only
#define R_ 2 // 10 read-only
#define NONE 0 // 00 no access
// ---------------------------------------------------------------------------
// Superblock
// ---------------------------------------------------------------------------
// Disk layout:
// [ boot | super | log | inode-bitmap | inode-blocks | block-bitmap | data ]
struct superblock {
uint magic; // Must be FSMAGIC
uint size; // Size of file system image (blocks)
uint nblocks; // Number of data blocks
uint ninodes; // Number of inodes
uint nlog; // Number of log blocks
uint logstart; // Block number of first log block
uint inodestart; // Block number of first inode block
uint bmapstart; // Block number of first free-block bitmap block
uint ibmapstart; // Block number of first inode-bitmap block
uint freeinodes; // Count of free inodes
uint freeblocks; // Count of free blocks
uint checksum; // CRC32c of this struct (field zeroed during computation)
};
extern struct superblock sb;
#define FSMAGIC 0x10203040
// ---------------------------------------------------------------------------
// Block / inode geometry
// ---------------------------------------------------------------------------
#define NDIRECT 12
#define NINDIRECT (BSIZE / sizeof(uint))
// Maximum file size in blocks: direct + 1x + 2x + 3x indirect
#define MAXFILE (NDIRECT \
+ NINDIRECT \
+ (NINDIRECT * NINDIRECT) \
+ (NINDIRECT * NINDIRECT * NINDIRECT))
// ---------------------------------------------------------------------------
// Extent descriptor (T_EXTENT inodes only)
//
// Up to MAX_INLINE_EXTENTS are packed into ip->addrs[0..NDIRECT-1].
// addrs[] = NDIRECT uints = 12*4 = 48 bytes
// each fsv1_extent = 12 bytes → 48/12 = 4 inline extents
// (addrs[NDIRECT], [NDIRECT+1], [NDIRECT+2] remain as indirect pointers)
//
// For files needing more extents, addrs[NDIRECT] points to an indirect block
// of BSIZE/sizeof(struct fsv1_extent) = 85 additional extents.
// ---------------------------------------------------------------------------
// Renamed from "schfs_extent". The new format in include/schfs.h has its own,
// completely different, struct of that name -- 64-bit fields and a refcount --
// and any file including both headers hit a redefinition error. Two structs
// with one name and two layouts is a trap worth removing rather than working
// around; this one belongs to the v1 format and dies with it.
struct fsv1_extent {
uint start_block; // logical block number in the file (first of run)
uint start_blkaddr; // physical disk block address
uint len; // number of contiguous blocks in this run
};
// Inline extents packed into addrs[0 .. NDIRECT-1] only.
#define MAX_INLINE_EXTENTS (NDIRECT * sizeof(uint) / sizeof(struct fsv1_extent))
// ---------------------------------------------------------------------------
// On-disk inode (exactly 128 bytes; IPB = 8 inodes per 1024-byte block)
//
// Byte layout:
// 0 short type 2
// 2 short major 2
// 4 short minor 2
// 6 short nlink 2
// 8 uint size 4
// 12 uint rwmode 4
// 16 uint supermode 4
// 20 uint addrs[15] 60 T_EXTENT: packed fsv1_extent[4] in [0..11], rest normal
// 80 uint showmode 4
// 84 uint atime 4 ← last access time (Unix seconds)
// 88 uint mtime 4 ← last modification time
// 92 uint ctime 4 ← last metadata-change time
// 96 uint checksum 4 ← CRC32c (zeroed during computation)
// 100 uchar pad[28] 28 ← was 44; reduced by 16 for the 4 new fields
// 128 bytes total
// ---------------------------------------------------------------------------
struct dinode {
short type; // file type
short major; // major device number (T_DEVICE only)
short minor; // minor device number (T_DEVICE only)
short nlink; // hard-link count
uint size; // file size in bytes
uint rwmode; // read/write permission flags
uint supermode; // superuser-only flag
uint addrs[NDIRECT + 3]; // block addresses / packed extent entries
uint showmode; // visibility flag
uint atime; // last access time (seconds since epoch)
uint mtime; // last modification time
uint ctime; // last metadata-change time
uint checksum; // CRC32c of this dinode (field = 0 when hashing)
uchar pad[28]; // reserved padding (struct = 128 bytes)
};
// Inodes per block
#define IPB (BSIZE / sizeof(struct dinode))
// Block containing inode i
#define IBLOCK(i, sb) ((i) / IPB + (sb).inodestart)
// Bitmap bits per block
#define BPB (BSIZE * 8)
// Block of free-block bitmap containing bit for block b
#define BBLOCK(b, sb) ((b) / BPB + (sb).bmapstart)
// Block of inode bitmap containing bit for inode i
#define IBBLOCK(i, sb) ((i) / BPB + (sb).ibmapstart)
// ---------------------------------------------------------------------------
// Directory entry
// ---------------------------------------------------------------------------
// 30, not 14.
//
// 14 was xv6's, and the v1 filesystem truncated to it *silently*: a file called
// JetBrainsMono-Regular.ttf was stored as "JetBrainsMono-", could not be opened
// by its real name, and would have collided outright with
// JetBrainsMono-Bold.ttf. Any two files sharing a 14-character prefix mapped to
// one entry. That is data loss, not a display limit.
//
// The value is chosen so that `struct dirent` is exactly 32 bytes, because the
// directory HTree below is laid out in *bytes* and a bucket must stay 256 bytes
// (see DIRENT_BUCKET_BYTES). Going from 16-byte to 32-byte entries therefore
// halves the slots per bucket from 16 to 8 and changes no offset anywhere else
// -- HASHRANGE, the overflow region and MAXFSIZE all keep their values. The
// cost is more overflow use in heavily collided directories, which is a far
// better trade than silently mangling names.
//
// It is still not POSIX's 255. Getting there means reshaping the directory
// format, which wants the on-disk version policy SchFS is getting first.
#define DIRSIZ 30
struct dirent {
ushort inum;
char name[DIRSIZ];
};
// ---------------------------------------------------------------------------
// Directory HTree constants
//
// BKDRHash(name) returns a value in [16, 16 + HASHRANGE).
// Each bucket is 256 bytes: 8 dirent slots of 32 bytes. It was 16 slots of 16
// until DIRSIZ grew; the byte size is what the layout depends on, so it is the
// thing held constant.
//
// With HASHRANGE=443:
// max primary end byte = (16 + 443) * 256 = 117 504 (~114.75 blocks)
// OVERFLOW_DIR_START = 113 * BSIZE = 115 712
// OVERFLOW_DIR_END = 115 * BSIZE = 117 760
// MAXFSIZE = 119 * BSIZE = 121 856
// ---------------------------------------------------------------------------
#define DIRENT_NUM 7168
#define FIRST_LAYER_NODE_NUM 112
#define OVERFLOW_BLOCK (FIRST_LAYER_NODE_NUM + 1)
#define NUM_DIRENT_PER_BLOCK (BSIZE / sizeof(struct dirent))
#define SECOND_LAYER_NODE_NUM NUM_DIRENT_PER_BLOCK
#define HASHRANGE 443
// One hash bucket, in bytes. Every offset in the HTree is derived from this, so
// it must not change when DIRSIZ does -- see the note on DIRSIZ.
#define DIRENT_BUCKET_BYTES 256
#define DIRENT_PER_BUCKET (DIRENT_BUCKET_BYTES / sizeof(struct dirent))
#define MAXFSIZE (119 * BSIZE)
// Overflow bucket: shared between fs.c and mkfs.c.
// These replace the bare "113 * BSIZE" / "115 * BSIZE" literals.
#define OVERFLOW_DIR_START (113 * BSIZE)
#define OVERFLOW_DIR_END (115 * BSIZE)
#endif // FS_H

93
include/input.h Normal file
View file

@ -0,0 +1,93 @@
#pragma once
#include "types.h"
// Raw input events -- Linux's evdev interface.
//
// The console gives characters. That is the right thing for a shell and the
// wrong thing for everything else: a character stream has no key *releases*,
// no way to tell left shift from right, no notion of a key being held, and no
// mouse at all. A game holding W to walk forward, or a compositor deciding
// which window a click landed in, needs the events underneath the characters.
//
// The layout is Linux's, deliberately. It is what every program that reads input
// already speaks -- SDL, X, libinput, every game engine -- so a program that
// runs on Linux reads input here without a porting layer. It costs nothing to
// match: the struct is four fields.
//
// Nodes are /dev/input/event0 (keyboard) and /dev/input/event1 (mouse).
struct input_timeval {
int64 tv_sec;
int64 tv_usec;
};
struct input_event {
struct input_timeval time;
uint16 type;
uint16 code;
int32 value;
};
// Event types.
#define EV_SYN 0x00
#define EV_KEY 0x01
#define EV_REL 0x02
#define EV_ABS 0x03
#define EV_MSC 0x04
#define SYN_REPORT 0
// Relative axes.
#define REL_X 0x00
#define REL_Y 0x01
#define REL_WHEEL 0x08
// Buttons live in the EV_KEY code space alongside keys, which is why the
// keyboard's codes stop well below 0x100.
#define BTN_LEFT 0x110
#define BTN_RIGHT 0x111
#define BTN_MIDDLE 0x112
// Key codes worth naming. The rest are the AT scancode-set-1 numbers, which
// Linux's keycodes were derived from and still match for everything below
// 0x54 -- that is not a coincidence and it is why the PS/2 path needs no
// translation table for unprefixed bytes.
#define KEY_RESERVED 0
#define KEY_ESC 1
#define KEY_ENTER 28
#define KEY_LEFTCTRL 29
#define KEY_LEFTSHIFT 42
#define KEY_RIGHTSHIFT 54
#define KEY_LEFTALT 56
#define KEY_SPACE 57
#define KEY_RIGHTCTRL 97
#define KEY_RIGHTALT 100
#define KEY_HOME 102
#define KEY_UP 103
#define KEY_PAGEUP 104
#define KEY_LEFT 105
#define KEY_RIGHT 106
#define KEY_END 107
#define KEY_DOWN 108
#define KEY_PAGEDOWN 109
#define KEY_INSERT 110
#define KEY_DELETE 111
#define KEY_LEFTMETA 125
#define KEY_RIGHTMETA 126
#define KEY_MAX 255
// EVIOCGRAB -- take/release exclusive access to a device.
//
// Linux's _IOW('E', 0x90, int). Spelled as the resulting constant rather than
// built from an _IOW macro because Schunix has no ioctl-encoding macros and
// inventing a set just for this would be a bigger change than the feature.
// A non-zero argument grabs, zero releases; the argument is passed by value.
#define EVIOCGRAB 0x40044590
// Which device an event came from. Both are minor numbers under the input
// major, and both are the index into the driver's array.
#define INPUT_KBD 0
#define INPUT_MOUSE 1
#define NINPUTDEV 2

43
include/irq.h Normal file
View file

@ -0,0 +1,43 @@
#pragma once
// Hardware interrupt registration.
//
// Until this existed, trap() was a switch statement listing every interrupt
// the kernel knew about, and adding a device meant editing the trap handler.
// That is fine while the devices are the four the PC has always had at fixed
// pins; it stops being fine the moment a device's IRQ is whatever the BIOS
// decided to put in its PCI config space this boot. A driver cannot write
// `case T_IRQ0 + IRQ_XHCI:` because there is no such constant -- it only
// learns its line at probe time.
//
// So: a line has a chain of handlers, a driver adds itself to one at probe
// time, and enabling the line is a consequence of registering rather than a
// separate call somebody can forget.
//
// Chains matter more than they look. PCI interrupt pins are *shared*: four
// INTx lines are routed to a handful of IOAPIC inputs, so two unrelated
// devices routinely land on the same one. A handler must therefore check
// whether its own device actually raised the interrupt and do nothing if it
// did not.
#define NIRQ 24 // IOAPIC redirection entries on a PC
#define NCHAIN 4 // handlers that may share one line
typedef void (*irq_handler_t)(void *arg);
// Returns 0, or -1 if the line is out of range, reserved, or full.
// Registering enables the line; the last unregister masks it again.
//
// `level_low` selects PCI's signalling (level-triggered, active low) rather
// than ISA's (edge-triggered, active high). Getting this wrong does not fail
// cleanly: an edge-configured level interrupt fires once and then wedges,
// because the device keeps the line asserted and no further edge ever comes.
int irq_register(int irq, irq_handler_t fn, void *arg, const char *name,
int level_low);
void irq_unregister(int irq, irq_handler_t fn, void *arg);
// Called from trap() with the IOAPIC pin number, not the vector.
void irq_dispatch(int irq);
void irq_init(void);
int irq_info(char *buf, int cap); // /proc/interrupts

112
include/kbd.h Normal file
View file

@ -0,0 +1,112 @@
// PC keyboard interface constants
#define KBSTATP 0x64 // kbd controller status port(I)
#define KBS_DIB 0x01 // kbd data in buffer
#define KBDATAP 0x60 // kbd data port(I)
#define NO 0
#define SHIFT (1<<0)
#define CTL (1<<1)
#define ALT (1<<2)
#define CAPSLOCK (1<<3)
#define NUMLOCK (1<<4)
#define SCROLLLOCK (1<<5)
#define E0ESC (1<<6)
// Special keycodes
#define KEY_HOME 0xE0
#define KEY_END 0xE1
#define KEY_UP 0xE2
#define KEY_DN 0xE3
#define KEY_LF 0xE4
#define KEY_RT 0xE5
#define KEY_PGUP 0xE6
#define KEY_PGDN 0xE7
#define KEY_INS 0xE8
#define KEY_DEL 0xE9
// C('A') == Control-A
#define C(x) (x - '@')
static uchar shiftcode[256] =
{
[0x1D] CTL,
[0x2A] SHIFT,
[0x36] SHIFT,
[0x38] ALT,
[0x9D] CTL,
[0xB8] ALT
};
static uchar togglecode[256] =
{
[0x3A] CAPSLOCK,
[0x45] NUMLOCK,
[0x46] SCROLLLOCK
};
static uchar normalmap[256] =
{
NO, 0x1B, '1', '2', '3', '4', '5', '6', // 0x00
'7', '8', '9', '0', '-', '=', '\b', '\t',
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', // 0x10
'o', 'p', '[', ']', '\n', NO, 'a', 's',
'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', // 0x20
'\'', '`', NO, '\\', 'z', 'x', 'c', 'v',
'b', 'n', 'm', ',', '.', '/', NO, '*', // 0x30
NO, ' ', NO, NO, NO, NO, NO, NO,
NO, NO, NO, NO, NO, NO, NO, '7', // 0x40
'8', '9', '-', '4', '5', '6', '+', '1',
'2', '3', '0', '.', NO, NO, NO, NO, // 0x50
[0x9C] '\n', // KP_Enter
[0xB5] '/', // KP_Div
[0xC8] KEY_UP, [0xD0] KEY_DN,
[0xC9] KEY_PGUP, [0xD1] KEY_PGDN,
[0xCB] KEY_LF, [0xCD] KEY_RT,
[0x97] KEY_HOME, [0xCF] KEY_END,
[0xD2] KEY_INS, [0xD3] KEY_DEL
};
static uchar shiftmap[256] =
{
NO, 033, '!', '@', '#', '$', '%', '^', // 0x00
'&', '*', '(', ')', '_', '+', '\b', '\t',
'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', // 0x10
'O', 'P', '{', '}', '\n', NO, 'A', 'S',
'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', // 0x20
'"', '~', NO, '|', 'Z', 'X', 'C', 'V',
'B', 'N', 'M', '<', '>', '?', NO, '*', // 0x30
NO, ' ', NO, NO, NO, NO, NO, NO,
NO, NO, NO, NO, NO, NO, NO, '7', // 0x40
'8', '9', '-', '4', '5', '6', '+', '1',
'2', '3', '0', '.', NO, NO, NO, NO, // 0x50
[0x9C] '\n', // KP_Enter
[0xB5] '/', // KP_Div
[0xC8] KEY_UP, [0xD0] KEY_DN,
[0xC9] KEY_PGUP, [0xD1] KEY_PGDN,
[0xCB] KEY_LF, [0xCD] KEY_RT,
[0x97] KEY_HOME, [0xCF] KEY_END,
[0xD2] KEY_INS, [0xD3] KEY_DEL
};
static uchar ctlmap[256] =
{
NO, NO, NO, NO, NO, NO, NO, NO,
NO, NO, NO, NO, NO, NO, NO, NO,
C('Q'), C('W'), C('E'), C('R'), C('T'), C('Y'), C('U'), C('I'),
C('O'), C('P'), NO, NO, '\r', NO, C('A'), C('S'),
C('D'), C('F'), C('G'), C('H'), C('J'), C('K'), C('L'), NO,
NO, NO, NO, C('\\'), C('Z'), C('X'), C('C'), C('V'),
C('B'), C('N'), C('M'), NO, NO, C('/'), NO, NO,
[0x9C] '\r', // KP_Enter
[0xB5] C('/'), // KP_Div
[0xC8] KEY_UP, [0xD0] KEY_DN,
[0xC9] KEY_PGUP, [0xD1] KEY_PGDN,
[0xCB] KEY_LF, [0xCD] KEY_RT,
[0x97] KEY_HOME, [0xCF] KEY_END,
[0xD2] KEY_INS, [0xD3] KEY_DEL
};

534
include/logo.h Normal file
View file

@ -0,0 +1,534 @@
#pragma once
// GENERATED by tools/mklogo.py -- do not edit. Run `make logo`.
// source: Assets/Schunix Small Logo.png
//
// An ink-coverage mask, not colour: 0 is background and full scale is
// solid logo. Whoever draws it picks the colour, so the palette stays in
// include/theme.h and can change without regenerating anything.
//
// Two pixels per byte, high nibble first. Included by boot/stage2main.c
// as well as kernel/dev/vga.c, and stage2 must fit below the 0x10000
// kernel-search buffer -- hence 4 bits rather than 8.
#define LOGO_W 128
#define LOGO_H 128
// Ink at linear pixel index i, 0..255.
#define LOGO_INK(i) ((unsigned char)((((i) & 1) ? (logo_mask[(i) >> 1] & 0xF) \
: (logo_mask[(i) >> 1] >> 4)) * 17))
static const unsigned char logo_mask[LOGO_W * LOGO_H / 2] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x23,0x56,0x79,0xaa,0xbb,
0xbb,0xaa,0x98,0x75,0x42,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x47,0x9b,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xb9,0x74,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x47,0x9a,0xaa,0xbb,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0xbb,0xba,0x75,0x20,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x15,0x8a,0xaa,0xaa,0xaa,0xbb,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0xbb,0xbb,0xbb,0xb9,0x51,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x37,0x99,0xaa,0xaa,0xaa,0xaa,0xbb,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0xbb,0xbb,0xbb,0xba,0xaa,0x84,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x01,0x58,0x99,0x99,0xaa,0xaa,0xaa,0xaa,0xbb,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0xbb,0xbb,0xbb,0xba,0xaa,0xaa,0x96,0x10,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x01,0x69,0x99,0x99,0x99,0xaa,0xaa,0xaa,0xaa,0xbb,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0xbb,0xbb,0xbb,0xba,0xaa,0xaa,0xaa,0xa7,0x10,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x58,0x99,0x99,0x99,0x99,0xaa,0xaa,0xaa,0xaa,0xbb,0xbb,0xba,0x87,0x76,0x66,
0x66,0x67,0x89,0xab,0xbb,0xbb,0xbb,0xbb,0xbb,0xba,0xaa,0xaa,0xaa,0xaa,0xa6,0x10,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x48,0x88,0x99,0x99,0x99,0x99,0xaa,0xaa,0xaa,0xa9,0x65,0x31,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x13,0x57,0x9b,0xbb,0xbb,0xba,0xaa,0xaa,0xaa,0xaa,0xaa,0x95,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x37,
0x88,0x88,0x99,0x99,0x99,0x99,0xaa,0xa9,0x63,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x36,0xab,0xba,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,
0x93,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x16,0x88,
0x88,0x88,0x99,0x99,0x99,0x99,0x95,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x6a,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,
0x99,0x71,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x78,0x88,
0x88,0x88,0x99,0x99,0x99,0x62,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x37,0xaa,0xaa,0xaa,0xaa,0xaa,
0x99,0x99,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x57,0x78,0x88,
0x88,0x88,0x99,0x98,0x51,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x15,0xaa,0xaa,0xaa,0xaa,
0x99,0x99,0x97,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x16,0x77,0x78,0x88,
0x88,0x88,0x98,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0xaa,0xaa,0xaa,
0x99,0x99,0x99,0x81,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x77,0x77,0x78,0x88,
0x88,0x88,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0xaa,0xaa,
0x99,0x99,0x99,0x99,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x47,0x77,0x77,0x78,0x88,
0x88,0x61,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x17,0xaa,
0x99,0x99,0x99,0x99,0x95,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x77,0x77,0x77,0x78,0x88,
0x83,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x49,
0x99,0x99,0x99,0x99,0x99,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x57,0x77,0x77,0x77,0x78,0x86,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,
0x79,0x99,0x99,0x99,0x99,0x96,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x67,0x77,0x77,0x77,0x77,0x30,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x04,0x99,0x99,0x99,0x99,0x99,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x56,0x67,0x77,0x77,0x77,0x61,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x28,0x99,0x99,0x99,0x99,0x97,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x66,0x67,0x77,0x77,0x76,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x79,0x99,0x99,0x99,0x98,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x46,0x66,0x67,0x77,0x77,0x50,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x06,0x99,0x99,0x99,0x98,0x85,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x66,0x66,0x67,0x77,0x74,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x59,0x99,0x99,0x98,0x88,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x26,0x66,0x66,0x67,0x77,0x30,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x04,0x99,0x99,0x98,0x88,0x83,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x56,0x66,0x66,0x67,0x73,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x49,0x99,0x98,0x88,0x88,0x10,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x66,0x66,0x66,0x67,0x30,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x05,0x99,0x98,0x88,0x88,0x70,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x46,0x66,0x66,0x66,0x64,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x59,0x98,0x88,0x88,0x86,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x02,0x56,0x66,0x66,0x66,0x50,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x06,0x98,0x88,0x88,0x88,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x05,0x56,0x66,0x66,0x65,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x78,0x88,0x88,0x88,0x71,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x45,0x56,0x66,0x66,0x61,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x88,0x88,0x88,0x86,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x02,0x55,0x56,0x66,0x66,0x20,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x88,0x88,0x88,0x88,0x30,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x05,0x55,0x56,0x66,0x64,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x68,0x88,0x88,0x88,0x70,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x35,0x55,0x56,0x66,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x88,0x88,0x88,0x84,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x55,0x55,0x56,0x66,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x88,0x88,0x88,0x87,0x10,0x00,0x00,0x00,
0x00,0x00,0x00,0x03,0x55,0x55,0x56,0x64,0x00,0x00,0x00,0x00,0x05,0xaa,0xaa,0xa9,
0x99,0x98,0x88,0x88,0x77,0x77,0x66,0x66,0x65,0x55,0x55,0x30,0x00,0x00,0x00,0x00,
0x00,0x00,0x35,0x54,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x29,0x98,0x10,0x00,0x00,0x68,0x88,0x88,0x88,0x50,0x00,0x00,0x00,
0x00,0x00,0x00,0x15,0x55,0x55,0x56,0x50,0x00,0x00,0x00,0x00,0x5e,0xee,0xdd,0xdc,
0xcc,0xcb,0xbb,0xaa,0xaa,0x99,0x98,0x88,0x77,0x77,0x66,0x63,0x00,0x00,0x00,0x00,
0x00,0x03,0x77,0x77,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x02,0xbc,0xcc,0xb1,0x00,0x00,0x18,0x88,0x88,0x88,0x81,0x00,0x00,0x00,
0x00,0x00,0x00,0x35,0x55,0x55,0x56,0x30,0x00,0x00,0x00,0x06,0xee,0xed,0xdd,0xdc,
0xcc,0xbb,0xba,0xaa,0xa9,0x99,0x88,0x88,0x77,0x76,0x66,0x66,0x10,0x00,0x00,0x00,
0x00,0x07,0x77,0x77,0x75,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x3b,0xcc,0xcc,0xd8,0x00,0x00,0x04,0x88,0x88,0x88,0x85,0x00,0x00,0x00,
0x00,0x00,0x01,0x55,0x55,0x55,0x55,0x00,0x00,0x00,0x00,0x6e,0xee,0xdd,0xdd,0xcc,
0xcb,0xbb,0xba,0xaa,0x99,0x98,0x88,0x87,0x77,0x66,0x66,0x66,0x10,0x00,0x00,0x00,
0x00,0x07,0x77,0x77,0x78,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x03,0xbc,0xcc,0xcc,0xd8,0x00,0x00,0x00,0x88,0x88,0x88,0x87,0x10,0x00,0x00,
0x00,0x00,0x03,0x55,0x55,0x55,0x53,0x00,0x00,0x00,0x05,0xee,0xee,0xdd,0xdc,0xcc,
0xbb,0xbb,0xaa,0xa9,0x99,0x98,0x88,0x77,0x76,0x66,0x66,0x66,0x10,0x00,0x00,0x00,
0x00,0x07,0x77,0x77,0x78,0x85,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x2b,0xcc,0xcc,0xcd,0xd8,0x00,0x00,0x00,0x48,0x88,0x88,0x87,0x40,0x00,0x00,
0x00,0x00,0x04,0x55,0x55,0x55,0x50,0x00,0x00,0x00,0x6e,0xee,0xed,0xdd,0xcc,0xcc,
0xbb,0xba,0xaa,0x99,0x99,0x88,0x87,0x77,0x76,0x66,0x66,0x62,0x00,0x00,0x00,0x00,
0x00,0x02,0x77,0x77,0x88,0x88,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x03,0xbc,0xcc,0xcc,0xdd,0xa1,0x00,0x00,0x00,0x07,0x88,0x88,0x87,0x70,0x00,0x00,
0x00,0x00,0x25,0x55,0x55,0x55,0x20,0x00,0x00,0x06,0xfe,0xee,0xdd,0xdc,0xa7,0x77,
0x77,0x66,0x66,0x65,0x55,0x55,0x54,0x44,0x44,0x33,0x44,0x20,0x00,0x00,0x00,0x00,
0x00,0x00,0x27,0x78,0x88,0x88,0x86,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x3b,0xcc,0xcc,0xcd,0xda,0x00,0x00,0x00,0x00,0x04,0x88,0x88,0x87,0x73,0x00,0x00,
0x00,0x00,0x45,0x55,0x55,0x55,0x00,0x00,0x00,0x5e,0xee,0xed,0xdd,0xc9,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x03,0x78,0x88,0x88,0x88,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,
0xbc,0xcc,0xcc,0xdd,0xb1,0x00,0x00,0x00,0x00,0x00,0x88,0x88,0x87,0x76,0x00,0x00,
0x00,0x01,0x45,0x55,0x55,0x53,0x00,0x00,0x05,0xee,0xee,0xdd,0xdd,0x80,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x28,0x88,0x88,0x88,0x86,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3b,
0xcc,0xcc,0xcd,0xda,0x10,0x00,0x00,0x00,0x00,0x00,0x58,0x88,0x87,0x77,0x10,0x00,
0x00,0x02,0x45,0x55,0x55,0x51,0x00,0x00,0x5e,0xee,0xed,0xdd,0xd8,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x02,0x88,0x88,0x88,0x99,0x60,0x00,0x00,0x00,0x00,0x00,0x03,0xbc,
0xcc,0xcc,0xcd,0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x28,0x88,0x87,0x77,0x40,0x00,
0x00,0x04,0x45,0x55,0x55,0x50,0x00,0x04,0xee,0xee,0xed,0xdd,0x90,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x38,0x88,0x88,0x99,0x96,0x00,0x00,0x00,0x00,0x00,0x2b,0xcc,
0xcc,0xcc,0xdb,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x88,0x87,0x77,0x70,0x00,
0x00,0x14,0x45,0x55,0x55,0x20,0x00,0x08,0xee,0xee,0xdd,0xd8,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x03,0x88,0x89,0x99,0x99,0x60,0x00,0x00,0x00,0x02,0xbc,0xcc,
0xcc,0xcd,0xa1,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x88,0x87,0x77,0x71,0x00,
0x00,0x24,0x45,0x55,0x55,0x00,0x00,0x08,0xee,0xed,0xdd,0xc8,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x38,0x99,0x99,0x9a,0xa7,0x00,0x00,0x00,0x2b,0xcc,0xcc,
0xcc,0xda,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x88,0x87,0x77,0x73,0x00,
0x00,0x34,0x45,0x55,0x54,0x00,0x00,0x05,0xee,0xdd,0xdc,0xcc,0x80,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x03,0x99,0x99,0xaa,0xaa,0x70,0x00,0x02,0xbb,0xcc,0xcc,
0xcd,0xb1,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x78,0x87,0x77,0x75,0x00,
0x00,0x44,0x45,0x55,0x52,0x00,0x00,0x00,0x5d,0xdd,0xdc,0xcc,0xb7,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x39,0x99,0xaa,0xaa,0xa7,0x00,0x2b,0xbc,0xcc,0xcc,
0xda,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x87,0x77,0x77,0x00,
0x01,0x44,0x45,0x55,0x51,0x00,0x00,0x00,0x05,0xdd,0xcc,0xcb,0xbb,0x70,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x9a,0xaa,0xaa,0xab,0x82,0xbb,0xcc,0xcc,0xcc,
0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x28,0x87,0x77,0x77,0x10,
0x02,0x44,0x45,0x55,0x50,0x00,0x00,0x00,0x00,0x5c,0xcc,0xbb,0xbb,0xa7,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3a,0xaa,0xaa,0xbb,0xbb,0xbc,0xcc,0xcc,0xcb,
0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x87,0x77,0x77,0x30,
0x03,0x44,0x45,0x55,0x40,0x00,0x00,0x00,0x00,0x05,0xcc,0xbb,0xba,0xaa,0x75,0x55,
0x55,0x44,0x44,0x43,0x33,0x33,0x33,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0xaa,0xab,0xbb,0xbb,0xcc,0xcc,0xcc,0xa1,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x87,0x77,0x77,0x50,
0x04,0x44,0x45,0x55,0x30,0x00,0x00,0x00,0x00,0x00,0x5b,0xbb,0xaa,0xaa,0x99,0x98,
0x88,0x77,0x77,0x66,0x66,0x66,0x66,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x4a,0xbb,0xbb,0xbc,0xcc,0xcc,0xca,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x87,0x77,0x77,0x60,
0x04,0x44,0x45,0x55,0x20,0x00,0x00,0x00,0x00,0x00,0x04,0xba,0xaa,0xa9,0x99,0x88,
0x87,0x77,0x76,0x66,0x66,0x66,0x66,0x65,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0xbb,0xbb,0xcc,0xcc,0xcc,0xb1,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x87,0x77,0x77,0x70,
0x14,0x44,0x45,0x55,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x4a,0xaa,0x99,0x98,0x88,
0x77,0x77,0x66,0x66,0x66,0x66,0x66,0x66,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x6b,0xbb,0xcc,0xcc,0xcc,0x10,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x87,0x77,0x77,0x71,
0x14,0x44,0x45,0x55,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0xa9,0x99,0x88,0x88,
0x77,0x76,0x66,0x66,0x66,0x66,0x66,0x67,0x75,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0xab,0xbc,0xcc,0xcc,0xcd,0x90,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x87,0x77,0x77,0x72,
0x24,0x44,0x45,0x54,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x39,0x98,0x88,0x87,
0x77,0x66,0x66,0x66,0x66,0x66,0x66,0x77,0x77,0x60,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x3a,0xbb,0xcc,0xcc,0xcc,0xdd,0xd9,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x77,0x77,0x77,0x73,
0x24,0x44,0x45,0x54,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x76,0x66,0x65,
0x55,0x55,0x44,0x55,0x55,0x56,0x67,0x77,0x77,0x76,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0xbb,0xbc,0xcc,0xcc,0xcd,0xdd,0xdd,0xa0,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x67,0x77,0x77,0x74,
0x34,0x44,0x45,0x53,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x02,0x67,0x77,0x77,0x77,0x60,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x2a,0xbb,0xcc,0xcc,0xcc,0xdd,0xdd,0xde,0xea,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x57,0x77,0x77,0x74,
0x34,0x44,0x45,0x53,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x16,0x77,0x77,0x77,0x76,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x03,0xab,0xbc,0xcc,0xcc,0xa5,0xcd,0xdd,0xee,0xee,
0x90,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x47,0x77,0x77,0x75,
0x34,0x44,0x45,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x77,0x77,0x77,0x77,0x60,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x3b,0xbb,0xbc,0xcc,0xca,0x00,0x4d,0xdd,0xee,0xee,
0xe9,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x47,0x77,0x77,0x75,
0x44,0x44,0x45,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x27,0x77,0x77,0x78,0x86,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x02,0xab,0xbb,0xcc,0xcc,0xb1,0x00,0x05,0xde,0xee,0xee,
0xdd,0x90,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x37,0x77,0x77,0x76,
0x44,0x44,0x45,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x77,0x77,0x88,0x88,0x10,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x3a,0xbb,0xbc,0xcc,0xca,0x10,0x00,0x00,0x4d,0xee,0xed,
0xdd,0xd9,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x37,0x77,0x77,0x76,
0x44,0x44,0x45,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x27,0x77,0x88,0x88,0x10,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x03,0xbb,0xbb,0xcc,0xcc,0xa0,0x00,0x00,0x00,0x05,0xee,0xdd,
0xdd,0xdd,0x90,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x37,0x77,0x77,0x76,
0x44,0x44,0x45,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x78,0x88,0x88,0x10,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x2a,0xbb,0xbc,0xcc,0xca,0x10,0x00,0x00,0x00,0x00,0x5d,0xdd,
0xdd,0xdd,0xc9,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x37,0x77,0x77,0x76,
0x44,0x44,0x45,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x88,0x88,0x88,0x10,0x00,0x00,0x00,
0x00,0x00,0x00,0x02,0xab,0xbb,0xcc,0xcc,0xa1,0x00,0x00,0x00,0x00,0x00,0x05,0xdd,
0xdd,0xdd,0xcc,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x37,0x77,0x77,0x76,
0x44,0x44,0x45,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x88,0x88,0x88,0x10,0x00,0x00,0x00,
0x00,0x00,0x00,0x2a,0xbb,0xbb,0xcc,0xca,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x5d,
0xdd,0xdc,0xcc,0xc8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x37,0x77,0x77,0x76,
0x34,0x44,0x45,0x52,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x88,0x88,0x88,0x10,0x00,0x00,0x00,
0x00,0x00,0x02,0xab,0xbb,0xbc,0xcc,0xa1,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,
0xdd,0xcc,0xcc,0xcc,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x47,0x77,0x77,0x76,
0x34,0x44,0x45,0x53,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x88,0x88,0x88,0x10,0x00,0x00,0x00,
0x00,0x00,0x2a,0xbb,0xbb,0xcc,0xca,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x5c,0xcc,0xcc,0xcc,0xc8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x47,0x77,0x77,0x75,
0x34,0x44,0x45,0x53,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x88,0x88,0x88,0x10,0x00,0x00,0x00,
0x00,0x02,0xab,0xbb,0xbc,0xcc,0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x05,0xcc,0xcc,0xcc,0xbb,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x57,0x77,0x77,0x75,
0x24,0x44,0x45,0x54,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x88,0x88,0x88,0x10,0x00,0x00,0x00,
0x00,0x0a,0xbb,0xbb,0xcc,0xca,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x5c,0xcc,0xcb,0xbb,0xb6,0x00,0x00,0x00,0x00,0x00,0x00,0x67,0x77,0x77,0x74,
0x24,0x44,0x45,0x54,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x88,0x88,0x89,0x10,0x00,0x00,0x00,
0x00,0x0b,0xbb,0xbc,0xcc,0xa1,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x05,0xcc,0xbb,0xbb,0xb7,0x00,0x00,0x00,0x00,0x00,0x00,0x77,0x77,0x77,0x73,
0x14,0x44,0x45,0x55,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x88,0x88,0x99,0x10,0x00,0x00,0x00,
0x00,0x1b,0xbb,0xbc,0xca,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x4b,0xbb,0xbb,0xb7,0x00,0x00,0x00,0x00,0x00,0x00,0x87,0x77,0x77,0x72,
0x14,0x44,0x45,0x55,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x88,0x89,0x99,0x10,0x00,0x00,0x00,
0x00,0x1b,0xbb,0xcc,0xc4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x09,0xbb,0xbb,0xb7,0x00,0x00,0x00,0x00,0x00,0x01,0x87,0x77,0x77,0x71,
0x04,0x44,0x45,0x55,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x88,0x99,0x99,0x10,0x00,0x00,0x00,
0x00,0x1b,0xbc,0xcc,0xc4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x09,0xbb,0xbb,0xa7,0x00,0x00,0x00,0x00,0x00,0x02,0x87,0x77,0x77,0x70,
0x04,0x44,0x45,0x55,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x88,0x99,0x99,0x10,0x00,0x00,0x00,
0x00,0x1b,0xcc,0xcc,0xc4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x09,0xbb,0xbb,0xa7,0x00,0x00,0x00,0x00,0x00,0x04,0x87,0x77,0x77,0x70,
0x03,0x44,0x45,0x55,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x89,0x99,0x99,0x10,0x00,0x00,0x00,
0x00,0x1c,0xcc,0xcc,0xc4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x09,0xbb,0xba,0xa7,0x00,0x00,0x00,0x00,0x00,0x06,0x87,0x77,0x77,0x50,
0x02,0x44,0x45,0x55,0x50,0x00,0x00,0x00,0x05,0x65,0x20,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x99,0x99,0x9a,0x10,0x00,0x00,0x00,
0x00,0x1c,0xcc,0xcc,0xd4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xbb,0xaa,0xa7,0x00,0x00,0x00,0x00,0x00,0x07,0x87,0x77,0x77,0x30,
0x01,0x44,0x45,0x55,0x51,0x00,0x00,0x00,0x66,0x66,0x62,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x99,0x99,0xaa,0x10,0x00,0x00,0x00,
0x00,0x1c,0xcc,0xcd,0xd4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xba,0xaa,0xa7,0x00,0x00,0x00,0x00,0x00,0x18,0x87,0x77,0x77,0x20,
0x00,0x44,0x45,0x55,0x52,0x00,0x00,0x03,0x66,0x66,0x66,0x20,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x99,0x99,0xaa,0x10,0x00,0x00,0x00,
0x00,0x1c,0xcc,0xdd,0xd4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x00,0x00,0x48,0x87,0x77,0x77,0x00,
0x00,0x34,0x45,0x55,0x54,0x00,0x00,0x03,0x66,0x66,0x66,0x62,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x69,0x99,0x9a,0xaa,0x20,0x00,0x00,0x00,
0x00,0x1c,0xcc,0xdd,0xd4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x00,0x00,0x68,0x87,0x77,0x76,0x00,
0x00,0x24,0x45,0x55,0x55,0x00,0x00,0x03,0x66,0x66,0x66,0x66,0x20,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x99,0x99,0xaa,0xaa,0x10,0x00,0x00,0x00,
0x00,0x1c,0xcd,0xdd,0xd4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x00,0x01,0x88,0x87,0x77,0x74,0x00,
0x00,0x14,0x45,0x55,0x55,0x20,0x00,0x00,0x46,0x66,0x66,0x66,0x72,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x69,0x99,0x9a,0xaa,0xa3,0x00,0x00,0x00,0x00,
0x00,0x1c,0xdd,0xdd,0xd4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x00,0x04,0x88,0x87,0x77,0x72,0x00,
0x00,0x04,0x45,0x55,0x55,0x40,0x00,0x00,0x04,0x66,0x66,0x67,0x77,0x20,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x99,0x99,0xaa,0xaa,0x30,0x00,0x00,0x00,0x00,
0x00,0x1d,0xdd,0xdd,0xe4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x00,0x07,0x88,0x87,0x77,0x70,0x00,
0x00,0x03,0x45,0x55,0x55,0x51,0x00,0x00,0x00,0x46,0x66,0x77,0x77,0x73,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x69,0x99,0x9a,0xaa,0xa4,0x00,0x00,0x00,0x00,0x00,
0x00,0x1d,0xdd,0xdd,0xe4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x00,0x18,0x88,0x87,0x77,0x50,0x00,
0x00,0x01,0x45,0x55,0x55,0x53,0x00,0x00,0x00,0x04,0x66,0x77,0x77,0x77,0x32,0x22,
0x22,0x22,0x22,0x22,0x22,0x26,0x99,0x99,0x9a,0xaa,0x30,0x00,0x00,0x00,0x00,0x00,
0x00,0x1d,0xdd,0xde,0xe4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x00,0x48,0x88,0x87,0x77,0x20,0x00,
0x00,0x00,0x45,0x55,0x55,0x55,0x00,0x00,0x00,0x00,0x47,0x77,0x77,0x77,0x77,0x77,
0x88,0x88,0x88,0x88,0x88,0x99,0x99,0x99,0xaa,0xa3,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x1d,0xdd,0xee,0xe4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x00,0x78,0x88,0x87,0x76,0x00,0x00,
0x00,0x00,0x25,0x55,0x55,0x55,0x20,0x00,0x00,0x00,0x05,0x77,0x77,0x77,0x77,0x78,
0x88,0x88,0x88,0x88,0x88,0x99,0x99,0x9a,0xaa,0x40,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x1d,0xde,0xee,0xe4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x03,0x88,0x88,0x87,0x73,0x00,0x00,
0x00,0x00,0x04,0x55,0x55,0x55,0x50,0x00,0x00,0x00,0x00,0x47,0x77,0x77,0x77,0x78,
0x88,0x88,0x88,0x88,0x89,0x99,0x99,0xaa,0xa3,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x1d,0xee,0xee,0xe4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x07,0x88,0x88,0x87,0x70,0x00,0x00,
0x00,0x00,0x03,0x55,0x55,0x55,0x52,0x00,0x00,0x00,0x00,0x05,0x77,0x77,0x77,0x88,
0x88,0x88,0x88,0x88,0x99,0x99,0x9a,0xaa,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x1e,0xee,0xee,0xd4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x08,0xaa,0xaa,0xa6,0x00,0x00,0x00,0x38,0x88,0x88,0x87,0x50,0x00,0x00,
0x00,0x00,0x01,0x55,0x55,0x55,0x55,0x00,0x00,0x00,0x00,0x00,0x57,0x77,0x78,0x88,
0x88,0x88,0x88,0x89,0x99,0x99,0xaa,0xa4,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x0b,0xee,0xee,0xc2,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x05,0xaa,0xaa,0xa4,0x00,0x00,0x00,0x78,0x88,0x88,0x87,0x20,0x00,0x00,
0x00,0x00,0x00,0x45,0x55,0x55,0x56,0x20,0x00,0x00,0x00,0x00,0x05,0x77,0x88,0x88,
0x88,0x88,0x88,0x99,0x99,0x99,0xaa,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0xae,0xec,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x5a,0xaa,0x30,0x00,0x00,0x04,0x88,0x88,0x88,0x86,0x00,0x00,0x00,
0x00,0x00,0x00,0x15,0x55,0x55,0x56,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x01,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,0x88,0x88,0x88,0x82,0x00,0x00,0x00,
0x00,0x00,0x00,0x04,0x55,0x55,0x56,0x64,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x58,0x88,0x88,0x88,0x60,0x00,0x00,0x00,
0x00,0x00,0x00,0x01,0x55,0x55,0x56,0x66,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x88,0x88,0x88,0x88,0x10,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x35,0x55,0x56,0x66,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x88,0x88,0x88,0x85,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x05,0x55,0x56,0x66,0x64,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x58,0x88,0x88,0x88,0x71,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x02,0x55,0x56,0x66,0x66,0x20,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x88,0x88,0x88,0x88,0x40,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x45,0x56,0x66,0x66,0x60,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x88,0x88,0x88,0x87,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x15,0x56,0x66,0x66,0x65,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x78,0x88,0x88,0x88,0x81,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x03,0x56,0x66,0x66,0x66,0x40,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x05,0x98,0x88,0x88,0x88,0x40,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x56,0x66,0x66,0x66,0x63,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x49,0x98,0x88,0x88,0x87,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x66,0x66,0x66,0x67,0x30,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x03,0x99,0x98,0x88,0x88,0x71,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x66,0x66,0x66,0x67,0x72,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x39,0x99,0x98,0x88,0x88,0x20,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x36,0x66,0x66,0x67,0x77,0x20,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x03,0x99,0x99,0x98,0x88,0x84,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x66,0x66,0x67,0x77,0x73,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x49,0x99,0x99,0x98,0x88,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x46,0x66,0x67,0x77,0x77,0x40,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x04,0x99,0x99,0x99,0x98,0x86,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x66,0x67,0x77,0x77,0x75,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x69,0x99,0x99,0x99,0x98,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x56,0x67,0x77,0x77,0x77,0x60,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x17,0x99,0x99,0x99,0x99,0x97,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x67,0x77,0x77,0x77,0x77,0x20,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x03,0x99,0x99,0x99,0x99,0x99,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x57,0x77,0x77,0x77,0x78,0x85,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x59,0x99,0x99,0x99,0x99,0x97,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x77,0x77,0x77,0x78,0x88,
0x72,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x28,
0x99,0x99,0x99,0x99,0x99,0x70,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x57,0x77,0x77,0x78,0x88,
0x88,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0xaa,
0x99,0x99,0x99,0x99,0x96,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x77,0x77,0x78,0x88,
0x88,0x88,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x9a,0xaa,
0x99,0x99,0x99,0x99,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x27,0x77,0x78,0x88,
0x88,0x88,0x98,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x9a,0xaa,0xaa,
0x99,0x99,0x99,0x82,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x67,0x78,0x88,
0x88,0x88,0x99,0x98,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x9a,0xaa,0xaa,0xaa,
0x99,0x99,0x98,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x78,0x88,
0x88,0x88,0x99,0x99,0x98,0x51,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x15,0x9a,0xaa,0xaa,0xaa,0xaa,
0x99,0x99,0x50,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x17,0x88,
0x88,0x88,0x99,0x99,0x99,0x99,0x84,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,
0x99,0x82,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,
0x88,0x88,0x99,0x99,0x99,0x99,0xaa,0xa7,0x41,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x15,0x8b,0xba,0xaa,0xaa,0xaa,0xaa,0xaa,0xaa,
0x95,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,
0x58,0x88,0x99,0x99,0x99,0x99,0xaa,0xaa,0xaa,0x97,0x53,0x10,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x01,0x35,0x8a,0xbb,0xbb,0xba,0xaa,0xaa,0xaa,0xaa,0xaa,0xa7,
0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x01,0x68,0x99,0x99,0x99,0x99,0xaa,0xaa,0xaa,0xaa,0xbb,0xbb,0x98,0x76,0x54,0x44,
0x44,0x45,0x67,0x8a,0xbb,0xbb,0xbb,0xbb,0xbb,0xba,0xaa,0xaa,0xaa,0xaa,0xa8,0x20,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x02,0x79,0x99,0x99,0x99,0xaa,0xaa,0xaa,0xaa,0xbb,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0xbb,0xbb,0xbb,0xba,0xaa,0xaa,0xaa,0xa8,0x30,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x02,0x69,0x99,0x99,0xaa,0xaa,0xaa,0xaa,0xbb,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0xbb,0xbb,0xbb,0xba,0xaa,0xaa,0xa7,0x30,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x01,0x58,0x99,0xaa,0xaa,0xaa,0xaa,0xbb,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0xbb,0xbb,0xbb,0xba,0xaa,0x95,0x10,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x36,0x9a,0xaa,0xaa,0xaa,0xbb,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0xbb,0xbb,0xbb,0xba,0x73,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x68,0xaa,0xaa,0xbb,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0xbb,0xbb,0x96,0x30,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x68,0xab,0xbb,0xbb,0xbb,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcb,0xbb,0xbb,0x96,0x31,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x34,0x67,0x89,0xab,0xbb,
0xbb,0xba,0x98,0x76,0x53,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};

49
include/memlayout.h Normal file
View file

@ -0,0 +1,49 @@
#pragma once
// Memory layout
#define EXTMEM 0x100000 // Start of extended memory
// Top of usable physical memory. A *variable*, read from the firmware's E820
// map at boot -- see kernel/arch/x86_64/e820.c. It used to be the constant
// below, which was not a limit on what the kernel used but an assertion that
// exactly 224 MB existed: less and the allocator walked off the end of RAM,
// more and it was invisible.
//
// The macro spelling is kept so that the twenty-odd call sites read the same as
// they always did. It must not be used before e820_init(), which runs first
// thing in main() for that reason.
#ifndef __ASSEMBLER__
extern uint64 phystop;
#define PHYSTOP phystop
#endif
// What to assume when the firmware gives us nothing usable: the old constant,
// so that path behaves exactly as the kernel always did.
#define PHYSTOP_FALLBACK 0xE000000ULL
// kvmalloc() maps physical 0-4 GB as four 1 GB pages and marks the top one
// uncached, because that is where PCI BARs and the framebuffer sit. 3 GB is
// therefore the most that can be treated as RAM.
#define PHYSTOP_MAX 0xC0000000ULL
// Below this the kernel image, its page tables and the DMA arena do not fit.
#define PHYSTOP_MIN (24ULL * 1024 * 1024)
// Key addresses for address space layout
#define KERNBASE 0xFFFF800000000000 // First kernel virtual address
#define KERNLINK (KERNBASE+EXTMEM) // Address where kernel is linked
#ifndef __ASSEMBLER__
static inline addr_t v2p(void *a) {
return ((addr_t)(a)) - ((addr_t)KERNBASE);
}
static inline void *p2v(addr_t a) {
return (void *)((a) + ((addr_t)KERNBASE));
}
#endif
#define V2P(a) (((addr_t) (a)) - KERNBASE)
#define P2V(a) (((void *) (a)) + KERNBASE)
#define V2P_WO(x) ((x) - KERNBASE) // same as V2P, but without casts
#define P2V_WO(x) ((x) + KERNBASE) // same as P2V, but without casts

130
include/mmu.h Normal file
View file

@ -0,0 +1,130 @@
#pragma once
// Eflags register
#define FL_CF 0x00000001 // Carry Flag
#define FL_PF 0x00000004 // Parity Flag
#define FL_AF 0x00000010 // Auxiliary carry Flag
#define FL_ZF 0x00000040 // Zero Flag
#define FL_SF 0x00000080 // Sign Flag
#define FL_TF 0x00000100 // Trap Flag
#define FL_IF 0x00000200 // Interrupt Enable
#define FL_DF 0x00000400 // Direction Flag
#define FL_OF 0x00000800 // Overflow Flag
#define FL_IOPL_MASK 0x00003000 // I/O Privilege Level bitmask
#define FL_NT 0x00004000 // Nested Task
#define FL_RF 0x00010000 // Resume Flag
#define FL_VM 0x00020000 // Virtual 8086 mode
#define FL_AC 0x00040000 // Alignment Check
#define FL_VIF 0x00080000 // Virtual Interrupt Flag
#define FL_VIP 0x00100000 // Virtual Interrupt Pending
#define FL_ID 0x00200000 // ID flag
// Control Register flags
#define CR0_PE 0x00000001 // Protection Enable
#define CR0_MP 0x00000002 // Monitor coProcessor
#define CR0_EM 0x00000004 // Emulation
#define CR0_TS 0x00000008 // Task Switched
#define CR0_ET 0x00000010 // Extension Type
#define CR0_NE 0x00000020 // Numeric Errror
#define CR0_WP 0x00010000 // Write Protect
#define CR0_AM 0x00040000 // Alignment Mask
#define CR0_NW 0x20000000 // Not Writethrough
#define CR0_CD 0x40000000 // Cache Disable
#define CR0_PG 0x80000000 // Paging
#define CR4_PSE 0x00000010 // Page size extension
#define CR4_PAE 0x00000020 // Physical address extensions
#define CR4_OSXFSR 0x00000200 // OS supports FXSAVE and FXRSTOR
#define CR4_OSXMMEXCPT 0x00000400 // OS supports SSE exceptions
// Model specific registers
#define MSR_EFER 0xC0000080 // Extended Feature Enable Register
#define MSR_STAR 0xC0000081 // Legacy Mode SYSCALL Target
#define MSR_LSTAR 0xC0000082 // Long Mode SYSCALL Target
#define MSR_CSTAR 0xC0000083 // Compatibility Mode SYSCALL Target
#define MSR_SFMASK 0xC0000084 // SYSCALL Flag Mask
// Segment selectors
#define SEG_KCODE 1
#define SEG_KDATA 2
#define SEG_UCODE32 3
#define SEG_UDATA 4
#define SEG_UCODE 5
#define SEG_KCPU 6
#define SEG_TSS 7 // TSS takes 2 slots in 64-bit GDT
#define NSEGS 9
#define DPL_USER 0x3
#define USER_CS ((SEG_UCODE<<3)|DPL_USER)
#define USER_DS ((SEG_UDATA<<3)|DPL_USER)
#define KERNEL_CS (SEG_KCODE<<3)
#ifndef __ASSEMBLER__
// Segment Descriptor
struct segdesc {
uint32 lim_15_0 : 16; // Low bits of segment limit
uint32 base_15_0 : 16; // Low bits of segment base address
uint32 base_23_16 : 8; // Middle bits of segment base address
uint32 type : 4; // Segment type
uint32 s : 1; // 0 = system, 1 = application
uint32 dpl : 2; // Descriptor Privilege Level
uint32 p : 1; // Present
uint32 lim_19_16 : 4; // High bits of segment limit
uint32 avl : 1; // Available for software use
uint32 l : 1; // 1 = 64-bit segment (Long mode)
uint32 db : 1; // 0 = 16-bit, 1 = 32-bit
uint32 g : 1; // Granularity
uint32 base_31_24 : 8; // High bits of segment base address
};
// 64-bit System Segment Descriptor (e.g., TSS)
struct desclong {
struct segdesc sd;
uint32 base_63_32; // High bits of 64-bit base address
uint32 res; // Reserved
};
#define SEG(type, base, lim, dpl) (struct segdesc) \
{ ((lim) >> 12) & 0xffff, (uint32)(base) & 0xffff, \
((uint32)(base) >> 16) & 0xff, type, 1, dpl, 1, \
(uint32)(lim) >> 28, 0, 1, 0, 1, (uint32)(base) >> 24 }
#endif
// Application segment type bits
#define STA_X 0x8 // Executable segment
#define STA_W 0x2 // Writeable (non-executable)
#define STA_R 0x2 // Readable (executable)
// System segment type bits
#define STS_T64A 0x9 // Available 64-bit TSS
#define STS_T64B 0xB // Busy 64-bit TSS
#define STS_IG64 0xE // 64-bit Interrupt Gate
#define STS_TG64 0xF // 64-bit Trap Gate
// Page table structure: 4 levels
#define PMX(va) (((addr_t)(va) >> 39) & 0x1FF)
#define PDPX(va) (((addr_t)(va) >> 30) & 0x1FF)
#define PDX(va) (((addr_t)(va) >> 21) & 0x1FF)
#define PTX(va) (((addr_t)(va) >> 12) & 0x1FF)
#define PGSIZE 4096
#define PXMASK 0x1FF
#define PGROUNDUP(sz) (((sz)+((addr_t)PGSIZE-1)) & ~((addr_t)(PGSIZE-1)))
#define PGROUNDDOWN(a) (((a)) & ~((addr_t)(PGSIZE-1)))
// Page table/directory entry flags
#define PTE_P 0x001 // Present
#define PTE_W 0x002 // Writeable
#define PTE_U 0x004 // User
#define PTE_PWT 0x008 // Write-Through
#define PTE_PCD 0x010 // Cache-Disable
#define PTE_A 0x020 // Accessed
#define PTE_D 0x040 // Dirty
#define PTE_PS 0x080 // Page Size (Huge Page)
#define PTE_ADDR(pte) ((addr_t)(pte) & 0x000FFFFFFFFFF000ull)
#define PTE_FLAGS(pte) ((addr_t)(pte) & 0xFFF)

54
include/mp.h Normal file
View file

@ -0,0 +1,54 @@
// See MultiProcessor Specification Version 1.[14]
struct mp { // floating pointer
uchar signature[4]; // "_MP_"
uint32 physaddr; // 32-bit phys addr of MP config table
uchar length; // 1
uchar specrev; // [14]
uchar checksum; // all bytes must add up to 0
uchar type; // MP system config type
uchar imcrp;
uchar reserved[3];
};
struct mpconf { // configuration table header
uchar signature[4]; // "PCMP"
ushort length; // total table length
uchar version; // [14]
uchar checksum; // all bytes must add up to 0
uchar product[20]; // product id
uint32 oemtable_p; // OEM table pointer
ushort oemlength; // OEM table length
ushort entry; // entry count
uint32 lapicaddr_p; // address of local APIC
ushort xlength; // extended table length
uchar xchecksum; // extended table checksum
uchar reserved;
};
struct mpproc { // processor table entry
uchar type; // entry type (0)
uchar apicid; // local APIC id
uchar version; // local APIC verison
uchar flags; // CPU flags
#define MPBOOT 0x02 // This proc is the bootstrap processor.
uchar signature[4]; // CPU signature
uint feature; // feature flags from CPUID instruction
uchar reserved[8];
};
struct mpioapic { // I/O APIC table entry
uchar type; // entry type (2)
uchar apicno; // I/O APIC id
uchar version; // I/O APIC version
uchar flags; // I/O APIC flags
uint32 addr_p; // I/O APIC address
};
// Table entry types
#define MPPROC 0x00 // One per processor
#define MPBUS 0x01 // One per bus
#define MPIOAPIC 0x02 // One per I/O APIC
#define MPIOINTR 0x03 // One per bus interrupt source
#define MPLINTR 0x04 // One per system interrupt source

28
include/param.h Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#define NPROC 128 // maximum number of processes
#define KSTACKSIZE 4096 // size of per-process kernel stack (increased for x64)
#define KSTACK_CANARY 0xDEADC0DEDEADC0DEULL // sentinel at kstack[0], see kstack_check_canary()
#define NCPU 16 // maximum number of CPUs
#define NOFILE 128 // open files per process
#define NFILE 512 // open files per system
#define NINODE 200 // maximum number of active i-nodes
#define NDEV 10 // maximum major device number
#define RAMDISK_DEV 2 // memory-backed block device (kernel/dev/ramdisk.c)
#define USBDISK_DEV 3 // USB mass storage (kernel/drivers/usb_storage.c)
// Device the root filesystem is mounted from. Building with RAMROOT=1 defines
// RAMDISK_ROOT and boots from the SCHFS image linked into the kernel instead
// of the IDE disk, leaving the physical disk free to be reformatted from a
// running system. See kernel/dev/ramdisk.c.
#ifdef RAMDISK_ROOT
#define ROOTDEV RAMDISK_DEV
#else
#define ROOTDEV 1 // device number of file system root disk
#endif
#define MAXARG 32 // max exec arguments
#define MAXPATH 128 // max path
#define MAXOPBLOCKS 30 // max # of blocks any FS op writes
#define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log
#define NBUF (MAXOPBLOCKS*3) // size of disk block cache
#define FSSIZE 150000 // size of file system in blocks

110
include/pci.h Normal file
View file

@ -0,0 +1,110 @@
#pragma once
// PCI bus enumeration and driver binding.
//
// Until this existed, every driver in Schunix found its hardware by knowing
// where it was: IDE at 0x1F0 because IDE has always been at 0x1F0, the
// framebuffer wherever the BIOS's VBE call said. That works for exactly the
// devices that predate PCI and for nothing else. A NIC, an AHCI controller,
// a virtio device or a real GPU has no fixed address at all -- you find it by
// asking the bus what is plugged into it, which is what this does.
// ---------------------------------------------------------------------------
// Config space
// ---------------------------------------------------------------------------
#define PCI_CONFIG_ADDR 0xCF8
#define PCI_CONFIG_DATA 0xCFC
#define PCI_VENDOR_ID 0x00
#define PCI_DEVICE_ID 0x02
#define PCI_COMMAND 0x04
#define PCI_STATUS 0x06
#define PCI_REVISION 0x08
#define PCI_PROG_IF 0x09
#define PCI_SUBCLASS 0x0A
#define PCI_CLASS 0x0B
#define PCI_HEADER_TYPE 0x0E
#define PCI_BAR0 0x10
#define PCI_SECONDARY_BUS 0x19
#define PCI_INTERRUPT_LINE 0x3C
#define PCI_INTERRUPT_PIN 0x3D
// PCI_COMMAND bits a driver usually wants set before touching its device.
#define PCI_CMD_IO 0x0001
#define PCI_CMD_MEMORY 0x0002
#define PCI_CMD_BUS_MASTER 0x0004
// Base classes worth naming. The full list is long and mostly irrelevant here.
#define PCI_CLASS_STORAGE 0x01
#define PCI_CLASS_NETWORK 0x02
#define PCI_CLASS_DISPLAY 0x03
#define PCI_CLASS_MULTIMEDIA 0x04
#define PCI_CLASS_BRIDGE 0x06
#define PCI_CLASS_SERIAL_BUS 0x0C
#define PCI_STATUS_CAPLIST 0x0010 // device has a capability list
#define PCI_CAP_PTR 0x34 // offset of the first capability
#define PCI_CAP_MSI 0x05
#define PCI_CAP_MSIX 0x11
#define PCI_CAP_VENDOR 0x09 // virtio's configuration lives here
#define PCI_CAP_PCIE 0x10
#define PCI_ANY 0xFFFF // wildcard in a match table
#define NPCI 64 // devices we will remember
struct pci_bar {
uint64 base; // already masked; 0 means "no BAR here"
uint64 size; // determined by write-ones-and-read-back
int is_io; // I/O port range rather than memory
int is_64; // consumed the following BAR slot too
int prefetch;
};
struct pci_dev {
uint8 bus, slot, func;
uint16 vendor, device;
uint8 class, subclass, prog_if, revision;
uint8 irq; // interrupt line, 0xFF when unrouted
uint8 header_type;
struct pci_bar bar[6];
const char *driver; // name of whatever claimed it, or 0
};
// One row of a driver's match table. Terminate the table with a row whose
// vendor is 0.
struct pci_match {
uint16 vendor, device; // PCI_ANY to ignore
uint8 class, subclass; // used only when vendor is PCI_ANY
};
// Declared here rather than defs.h because both take a struct pci_dev, which
// only this header defines. See kernel/dev/device.c for the model they feed.
// device.h is included rather than forward-declaring `enum dev_class`, which
// is a compiler extension and warns.
#include "device.h"
enum dev_class pci_class_of(const struct pci_dev *);
void pci_describe(const struct pci_dev *, char *out, int cap);
struct pci_driver {
const char *name;
// Base for the device's name in /proc/devices, if the driver's own name
// makes a bad one. `e1000` plus an index reads as "e10000"; a NIC wants to
// be `eth0`. Optional -- null means use `name`.
const char *devname;
const struct pci_match *match;
// Class-specific vtable, published on the device when this driver claims
// it -- struct display_ops for a display, and so on. Optional; a driver
// with nothing to offer a consumer leaves it null.
//
// It lives on the driver rather than being set inside probe() because
// pci_bind() registers the device *after* probe returns, so a driver has
// nothing to attach it to at the point it would want to.
const void *ops;
// Return 0 to claim the device. A driver that returns non-zero leaves it
// unclaimed for anything else that matches.
int (*probe)(struct pci_dev *);
};

289
include/posix.h Normal file
View file

@ -0,0 +1,289 @@
// SPDX-License-Identifier: Apache-2.0
// Schunix — POSIX-compatibility types used by kernel syscall handlers.
#pragma once
#include "types.h"
#include "date.h"
struct iovec {
void *iov_base;
uint64 iov_len;
};
// Linux's IOV_MAX. readv/writev used to cap iovcnt at 16 -- which is legal,
// since POSIX only requires 16 -- and returned EINVAL above it. But programs
// are written against the platform they run on, and on Linux that limit is
// 1024: toybox's `yes` builds a 256-entry iovec and writev()s it in a loop,
// so `yes | head -2` failed with "Invalid argument" before producing a single
// byte. Nothing here costs per-entry memory; the count is just a loop bound.
#define UIO_MAXIOV 1024
// Simplified termios (matches Linux x86_64 layout enough for basic use).
struct termios {
uint32 c_iflag;
uint32 c_oflag;
uint32 c_cflag;
uint32 c_lflag;
uint8 c_line;
uint8 c_cc[19];
uint32 c_ispeed;
uint32 c_ospeed;
};
// Terminal flags, with Linux's values. The kernel honours the ones marked
// below; the rest are accepted and stored so a program that sets them and
// reads them back sees what it wrote, which is what cfmakeraw() and every
// line-editing library actually check.
//
// c_iflag
#define IGNBRK 0000001
#define BRKINT 0000002
#define IGNPAR 0000004
#define PARMRK 0000010
#define INPCK 0000020
#define ISTRIP 0000040 // honoured: strip the eighth bit
#define INLCR 0000100 // honoured: NL -> CR on input
#define IGNCR 0000200 // honoured: drop CR
#define ICRNL 0000400 // honoured: CR -> NL on input
#define IUCLC 0001000
#define IXON 0002000
#define IXANY 0004000
#define IXOFF 0010000
#define IMAXBEL 0020000
#define IUTF8 0040000
// c_oflag
#define OPOST 0000001 // honoured: enable output processing
#define OLCUC 0000002
#define ONLCR 0000004 // honoured: NL -> CR NL on output
#define OCRNL 0000010
#define ONOCR 0000020
#define ONLRET 0000040
// c_cflag
#define CS8 0000060
#define CREAD 0000200
#define HUPCL 0002000
#define CLOCAL 0004000
// c_lflag
#define ISIG 0000001 // honoured: INTR/QUIT/SUSP generate signals
#define ICANON 0000002 // honoured: line-at-a-time with editing
#define ECHO 0000010 // honoured
#define ECHOE 0000020 // honoured: ERASE rubs the character out
#define ECHOK 0000040
#define ECHONL 0000100 // honoured: echo NL even with ECHO off
#define NOFLSH 0000200
#define TOSTOP 0000400
#define IEXTEN 0100000
// c_cc indices (Linux order). VMIN/VTIME share slots with VEOF/VEOL, which is
// why they are only meaningful in the mode the other pair is not.
#define VINTR 0
#define VQUIT 1
#define VERASE 2
#define VKILL 3
#define VEOF 4
#define VTIME 5
#define VMIN 6
#define VSWTC 7
#define VSTART 8
#define VSTOP 9
#define VSUSP 10
#define VEOL 11
#define VREPRINT 12
#define VDISCARD 13
#define VWERASE 14
#define VLNEXT 15
#define VEOL2 16
struct winsize {
uint16 ws_row;
uint16 ws_col;
uint16 ws_xpixel;
uint16 ws_ypixel;
};
struct rlimit {
uint64 rlim_cur;
uint64 rlim_max;
};
// Linux ioctl codes we handle.
#define TCGETS 0x5401
#define TCSETS 0x5402
#define TCSETSW 0x5403
#define TCSETSF 0x5404
#define TIOCGPGRP 0x540F
#define TIOCSPGRP 0x5410
#define TIOCGWINSZ 0x5413
#define TIOCSWINSZ 0x5414
#define TIOCSCTTY 0x540E
#define TIOCGSERIAL 0x541E
#define FIONREAD 0x541B
#define TIOCEXCL 0x540C
#define TIOCNXCL 0x540D
// arch_prctl commands
#define ARCH_SET_GS 0x1001
#define ARCH_SET_FS 0x1002
#define ARCH_GET_FS 0x1003
#define ARCH_GET_GS 0x1004
// clone flags (subset)
#define CLONE_VM 0x00000100
#define CLONE_THREAD 0x00010000
#define CLONE_SIGHAND 0x00000800
// futex ops
#define FUTEX_WAIT 0
#define FUTEX_WAKE 1
#define FUTEX_WAIT_PRIVATE 128
#define FUTEX_WAKE_PRIVATE 129
// Resource limit codes (POSIX)
#define RLIMIT_AS 9
#define RLIMIT_STACK 3
#define RLIMIT_NOFILE 7
// clock IDs
#define CLOCK_REALTIME 0
#define CLOCK_MONOTONIC 1
struct timespec {
long tv_sec;
long tv_nsec;
};
struct itimerval {
struct timeval it_interval;
struct timeval it_value;
};
struct rusage {
struct timeval ru_utime;
struct timeval ru_stime;
long ru_maxrss;
long ru_ixrss;
long ru_idrss;
long ru_isrss;
long ru_minflt;
long ru_majflt;
long ru_nswap;
long ru_inblock;
long ru_oublock;
long ru_msgsnd;
long ru_msgrcv;
long ru_nsignals;
long ru_nvcsw;
long ru_nivcsw;
};
struct tms {
long tms_utime;
long tms_stime;
long tms_cutime;
long tms_cstime;
};
struct sysinfo {
long uptime;
uint64 loads[3];
uint64 totalram;
uint64 freeram;
uint64 sharedram;
uint64 bufferram;
uint64 totalswap;
uint64 freeswap;
uint16 procs;
uint16 pad;
uint32 pad2;
uint64 totalhigh;
uint64 freehigh;
uint32 mem_unit;
char _f[8];
};
struct statfs {
int64 f_type;
int64 f_bsize;
int64 f_blocks;
int64 f_bfree;
int64 f_bavail;
int64 f_files;
int64 f_ffree;
int64 f_fsid[2];
int64 f_namelen;
int64 f_frsize;
int64 f_flags;
int64 f_spare[4];
};
// at-syscall dirfd sentinel
#define AT_FDCWD (-100)
#define AT_REMOVEDIR 0x200
#define AT_SYMLINK_NOFOLLOW 0x100
#define AT_EMPTY_PATH 0x1000
#define AT_NO_AUTOMOUNT 0x800
// prctl options
#define PR_SET_NAME 15
#define PR_GET_NAME 16
#define PR_SET_DUMPABLE 4
#define PR_GET_DUMPABLE 3
// signal numbers (Linux x86_64 values), used by kill dispatch and
// default-action classification in sigsend()
#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGABRT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
#define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
// Job-control and the remaining standard signals. A shell needs the TSTP/
// TTIN/TTOU trio to implement ^Z and background-job terminal access at all;
// they were simply absent, so any reference to them failed to compile.
#define SIGTRAP 5
#define SIGTSTP 20
#define SIGTTIN 21
#define SIGTTOU 22
#define SIGURG 23
#define SIGXCPU 24
#define SIGXFSZ 25
#define SIGVTALRM 26
#define SIGPROF 27
#define SIGWINCH 28
#define SIGIO 29
#define SIGPWR 30
#define SIGSYS 31
// getrusage who
#define RUSAGE_SELF 0
#define RUSAGE_CHILDREN (-1)
// fallocate mode
#define FALLOC_FL_KEEP_SIZE 1
// poll(2) event bits (Linux values)
#define POLLIN 0x001
#define POLLPRI 0x002
#define POLLOUT 0x004
#define POLLERR 0x008
#define POLLHUP 0x010
#define POLLNVAL 0x020
struct pollfd {
int fd;
short events;
short revents;
};

240
include/proc.h Normal file
View file

@ -0,0 +1,240 @@
#pragma once
#include "types.h"
#include "traps.h"
#define NVMA 64
struct vma {
uint64 addr;
uint64 length;
int used;
// The pages under this mapping are not ours to free on unmap: either they
// are device memory (the framebuffer) or they belong to something with a
// longer life than this process (a shared memory segment). This was
// `is_fb`, which was the only such case until shared memory existed --
// and freeing another process's shared pages on exit is exactly the kind
// of bug that shows up somewhere else entirely, hours later.
int no_free;
int shmid; // shared segment this came from, or -1
};
// Per-CPU state
struct cpu {
uchar id;
uchar apicid; // Local APIC ID
struct context *scheduler; // swtch() here to enter scheduler
volatile uint64 started; // Has the CPU started?
int ncli; // Depth of pushcli nesting.
int intena; // Were interrupts enabled before pushcli?
void *local; // CPU-local storage (GS base)
};
extern struct cpu cpus[NCPU];
extern int ncpu; // processors present
extern int ncpu_online; // processors actually running
// --- per-CPU variables ------------------------------------------------------
//
// These were `__thread struct cpu *cpu` and `__thread struct proc *proc`, and
// that was wrong in a way that only appears once a second processor runs.
//
// `__thread` means *per thread*, and the compiler is entitled to assume a
// thread's pointer never changes: it reads %fs:0 once and keeps the value in a
// register or spilled on the stack across calls. clang did exactly that in
// sleep():
//
// movq %fs:0x0, %rax # read the thread pointer
// movq %rax, -0x18(%rbp) # spill it
// callq sched # <-- the CPU can change here
// movq -0x18(%rbp), %rax # reload the OLD pointer
// movq (%rax,%rcx), %rax # proc, from the previous CPU's block
// movq $0x0, 0x40(%rax) # NULL, because that CPU set proc = 0
//
// But %fs here is *per CPU*, not per thread, and a kernel thread changes CPU at
// every sched(). So the assumption is false for exactly the variables that
// most need to be right, and the failure is a null dereference deep in the
// scheduler on a machine with more than one core.
//
// The fix is to read the value at each use, with no cached base: an explicit
// %gs-relative access that the compiler cannot hoist across a call. `volatile`
// is what forbids that, and the memory clobber on the setters keeps stores in
// order with the rest of the scheduler's bookkeeping.
//
// Layout of the per-CPU block, whose base is in both FS_BASE and GS_BASE, set
// by seginit() in kernel/arch/x86_64/vm.c:
//
// +0 self-pointer required by the x86-64 TLS ABI (%fs:0)
// +8 syscall scratch trapasm.S parks %rsp here across the stack switch
// +16 struct cpu *
// +24 struct proc *
//
// Offset 8 is NOT free. syscall_entry uses %fs:(8) as its scratch slot, and
// because seginit() sets GS base and FS base to the same address, %fs:8 and
// %gs:8 are the same memory. Putting `cpu` there made every syscall overwrite
// it with %rsp -- a #GP in syscall_entry itself, several layers below anything
// that could report it.
//
// trapasm.S reads %gs:24 for proc on the way out to user mode, so these three
// offsets are shared with assembly and must not be reordered.
#define PERCPU_SELF 0
#define PERCPU_SCRATCH 8
#define PERCPU_CPU 16
#define PERCPU_PROC 24
static inline struct cpu *
cpu_get(void)
{
struct cpu *c;
asm volatile("movq %%gs:16, %0" : "=r" (c));
return c;
}
static inline void
cpu_set(struct cpu *c)
{
asm volatile("movq %0, %%gs:16" :: "r" (c) : "memory");
}
static inline struct proc *
proc_get(void)
{
struct proc *p;
asm volatile("movq %%gs:24, %0" : "=r" (p));
return p;
}
static inline void
proc_set(struct proc *p)
{
asm volatile("movq %0, %%gs:24" :: "r" (p) : "memory");
}
// Named mycpu()/myproc(), as xv6-riscv does, and *not* spelled `cpu`/`proc`.
// A macro by those names would also rewrite every `struct cpu` and
// `struct proc`, because macros have no namespace and tags do -- which is
// exactly why the variables could carry those names in the first place.
#define mycpu() cpu_get()
#define myproc() proc_get()
// Saved registers for kernel context switches (x86_64 ABI)
struct context {
uint64 r15;
uint64 r14;
uint64 r13;
uint64 r12;
uint64 rbx;
uint64 rbp;
uint64 rip;
};
enum procstate { UNUSED, EMBRYO, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// Per-process state
// A copy of the interesting fields of one process, taken under ptable.lock.
// procfs reads these rather than struct proc, so nothing outside proc.c needs
// the lock or the layout.
struct procinfo {
int pid, ppid, pgid, sid;
uint uid;
int state; // enum procstate
uint64 sz;
int killed;
char name[16];
char exe[64]; // full path, for /proc/<pid>/exe
};
struct proc {
uint64 sz; // Size of process memory (bytes) [+0x00]
pml4e_t* pgdir; // Page table (PML4 on x86_64) [+0x08]
char *kstack; // Bottom of kernel stack [+0x10]
uint64 fs_base; // User TLS base (IA32_FS_BASE MSR) [+0x18] ← trapasm.S depends on this offset
enum procstate state; // Process state [+0x20]
int pid; // [+0x24]
struct proc *parent; // [+0x28]
struct trapframe *tf; // [+0x30]
struct context *context; // [+0x38]
void *chan; // [+0x40]
int killed; // [+0x48]
int _pad; // [+0x4c]
struct file *ofile[NOFILE]; // Open files [+0x50]
struct inode *cwd; // follows
char name[16]; //
// The path exec() was given. `name` is only the last component and only 15
// characters of it, which is what /proc/<pid>/stat wants; this is the whole
// path, reported by /proc/<pid>/exe.
//
// Note it is exposed as a regular *file*, not the symlink Linux uses, so
// `cat /proc/1/exe` works but readlink() does not. That is why `ps -o comm`
// still prints "[init]": toybox readlink()s exe and brackets the name when
// that fails. Default ps and top output is unaffected. Fixing it properly
// means symlink support in procfs, which is worth doing with the rest of the
// VFS work rather than on its own.
char exe[64];
uint64 signal_mask;
uint64 pending_signals;
int alarm_ticks;
char ofile_flags[NOFILE];
// Real signal delivery (added for SIGCHLD — see kernel/sys/syscall.c's
// deliver_signals() and kernel/proc/proc.c's exit()). sig_handler[sig]/
// sig_restorer[sig] come straight from the user's struct sigaction
// (sys_rt_sigaction); 0 means no handler installed. saved_sigtf stashes
// the pre-signal trapframe so sys_rt_sigreturn() can restore it; in_signal
// prevents re-entering a handler while one is already running (we don't
// support nested signal delivery).
uint64 sig_handler[32];
uint64 sig_restorer[32];
struct trapframe saved_sigtf;
int in_signal;
// rt_sigsuspend() support: the pre-suspend mask to restore once the
// handler that woke us finishes (sigsuspend swaps the mask atomically
// for the duration of the wait -- POSIX). sigsusp_active is set while a
// restore is owed; sys_rt_sigreturn() performs it.
uint64 sigsusp_oldmask;
int sigsusp_active;
struct vma vma[NVMA]; // VMA tracking for mmap
uint64 mmap_next; // next free mmap VA (bump allocator, see sys_mmap)
// ABI translation (Linux compat syscall number map)
int *syscall_map;
int map_size;
// Current working directory path; maintained by sys_chdir / sys_fchdir.
char cwd_path[256];
// --- POSIX process identity -------------------------------------------
// Appended at the end of the struct deliberately: trapasm.S hard-codes the
// offsets of the fields at the top (see the _Static_assert on fs_base in
// kernel/proc/proc.c), so nothing may be inserted above them.
// Exit status, already encoded the way wait(2) reports it -- see
// W_EXITCODE() below. Read by waitpid() before the slot is recycled.
int xstate;
// Signal that killed us, if any. Set by sigsend() alongside ->killed so the
// eventual exit() can report *which* signal, instead of a blanket SIGKILL.
int killsig;
// Process group and session. Job control is defined entirely in terms of
// these: a shell puts each pipeline in its own process group and signals the
// group, not individual pids. Without them a shell's wait loop cannot work,
// which is why mksh's job control never made progress.
int pgid;
int sid;
// Real/effective/saved user and group ids. Nothing enforces permissions with
// them yet (the on-disk format has no owner field until SchFS), but they
// are tracked honestly so getuid()/setuid() stop lying to userland.
uint uid, euid, suid;
uint gid, egid, sgid;
};
#define FD_CLOEXEC 1
// wait(2) status encoding, as understood by musl's WIFEXITED/WEXITSTATUS/
// WIFSIGNALED/WTERMSIG macros:
// normal exit -> (code & 0xff) << 8
// killed by signal -> sig & 0x7f
#define W_EXITCODE(code, sig) ((((code) & 0xff) << 8) | ((sig) & 0x7f))

42
include/qr.h Normal file
View file

@ -0,0 +1,42 @@
#pragma once
// A QR encoder, for getting a crash report off a machine whose network stack is
// dead.
//
// A panic report is worth nothing if it cannot leave the machine, and at panic
// time there is no network, no filesystem write that can be trusted, and often
// no second machine on the desk. A phone camera is a working out-of-band
// channel that needs no drivers on our side at all. See docs/roadmap.md Phase 8.
//
// Deliberately small, in three ways:
//
// - Byte mode only. The report is arbitrary bytes; alphanumeric mode would
// pack tighter but only for a restricted character set.
// - Error correction level L only. The screen is a clean, evenly lit,
// high-contrast source -- the failure mode QR's stronger levels protect
// against (dirt, print damage, curved packaging) does not apply.
// - Versions 1 to 6 only. Version 7 is where QR starts requiring an extra
// 18-bit version-information block in two more places on the symbol, and
// version 10 is where the character count field grows and the alignment
// pattern grid becomes 2-dimensional. Stopping at 6 avoids both, and
// 134 bytes is a large crash report.
//
// If a payload ever needs more than 134 bytes, the honest fix is to shorten the
// report -- not to reach for version 10 and its two extra tables.
#define QR_MAX_VERSION 6
#define QR_MAX_SIZE (17 + 4 * QR_MAX_VERSION) // 41 modules per side
#define QR_MAX_BYTES 134
struct qr {
int size; // modules per side; 0 if unencoded
unsigned char mod[QR_MAX_SIZE][QR_MAX_SIZE]; // 1 = dark
unsigned char fn[QR_MAX_SIZE][QR_MAX_SIZE]; // 1 = function pattern
};
// Encode len bytes into q. Picks the smallest version that fits.
// Returns 0, or -1 if len > QR_MAX_BYTES.
int qr_encode(struct qr *q, const unsigned char *data, int len);
// Largest payload, in bytes, for a given version (1..QR_MAX_VERSION).
int qr_capacity(int version);

246
include/schfs.h Normal file
View file

@ -0,0 +1,246 @@
// SchFS — on-disk format.
//
// Design rationale lives in docs/architecture/schfs.md. In short: copy-on-write
// with the checksum of a block stored in the pointer that leads to it (ZFS),
// one B-tree reused for every kind of record (btrfs), and attribute indexes as
// just another record type (BeFS).
//
// This header is shared by the kernel and by host tools (tools/mkfs,
// tools/schfs_test), so it must stay free of kernel-only types.
#ifndef SCHFS_H
#define SCHFS_H
#include <stdint.h>
#define SCHFS_MAGIC 0x53636846u // "SchF"
#define SCHFS_NODE_MAGIC 0x536e6f64u // "Snod"
// 4096 to match modern media and make extents worth having. The old format
// used 1024, which is also why its directories were so wasteful.
#define SCHFS_BLKSIZE 4096
// Deepest tree we will walk without recursing. 8 levels of a ~84-way internal
// node is far more than any plausible volume needs.
#define SCHFS_MAX_LEVEL 8
// ---------------------------------------------------------------------------
// Keys
//
// Every record in the filesystem is addressed by one 24-byte key, sorted by
// (objectid, type, offset). This is the whole reason there is only one tree:
// inodes, directory entries, extent maps and indexes are just different `type`
// values in the same keyspace.
// ---------------------------------------------------------------------------
#define SCHFS_KEY_INODE 1 // (ino, INODE, 0) -> struct schfs_inode
#define SCHFS_KEY_DIRENT 2 // (dirino,DIRENT, namehash) -> packed dirent list
#define SCHFS_KEY_EXTENT 3 // (ino, EXTENT, fileblk) -> struct schfs_extent
#define SCHFS_KEY_ATTR 4 // (ino, ATTR, namehash) -> attribute value
#define SCHFS_KEY_INDEX 5 // (idxid, INDEX, valuehash) -> objectid
#define SCHFS_KEY_CSUM 6 // (CSUM, CSUM, diskblk) -> uint32 CRC32c
// Data-block checksums live under one reserved object id, exactly as btrfs
// keeps a checksum tree. Tree nodes carry their checksum in their parent
// pointer, but file *data* blocks have no parent to hold it — without these
// records "end-to-end checksums" would only cover metadata, which is the part
// least likely to rot.
#define SCHFS_CSUM_OBJECTID 1ULL
// Object ids below this are reserved; the root directory is the first real one.
#define SCHFS_FIRST_OBJECTID 16ULL
#define SCHFS_ROOT_OBJECTID 16ULL
struct schfs_key {
uint64_t objectid;
uint64_t offset;
uint8_t type;
uint8_t pad[7];
}; /* 24 bytes */
// ---------------------------------------------------------------------------
// Node header — identical for internal nodes and leaves.
//
// `blocknr` is a self-pointer: a block that was written to the wrong place
// still checksums correctly, so the only way to catch a misdirected write is
// for the block to say where it thinks it lives.
// ---------------------------------------------------------------------------
struct schfs_node_hdr {
uint32_t magic; // SCHFS_NODE_MAGIC
uint32_t crc; // CRC32c of the whole block, this field zeroed
uint64_t blocknr; // where this block believes it lives
uint64_t generation; // transaction that wrote it
uint16_t level; // 0 = leaf, >0 = internal
uint16_t nritems;
uint32_t pad;
}; /* 32 bytes */
// Internal node entry: a key and the child it leads to, plus that child's
// checksum and generation. Storing the checksum *here* rather than in the
// child is what makes the tree self-validating.
struct schfs_kptr {
struct schfs_key key;
uint64_t blocknr;
uint64_t generation;
uint32_t crc;
uint32_t pad;
}; /* 48 bytes */
// Leaf entry. Items grow forward from the end of the header; their data grows
// backward from the end of the block, exactly like btrfs. `offset` is measured
// from the start of the block.
struct schfs_item {
struct schfs_key key;
uint32_t offset;
uint32_t size;
}; /* 32 bytes */
#define SCHFS_NODE_SPACE (SCHFS_BLKSIZE - (int)sizeof(struct schfs_node_hdr))
#define SCHFS_MAX_KPTR (SCHFS_NODE_SPACE / (int)sizeof(struct schfs_kptr))
#define SCHFS_MAX_ITEMS (SCHFS_NODE_SPACE / (int)sizeof(struct schfs_item))
// Largest value that can live inline in a leaf. Anything bigger becomes an
// extent record pointing at data blocks.
#define SCHFS_MAX_INLINE (SCHFS_NODE_SPACE / 4)
// ---------------------------------------------------------------------------
// Uberblock
//
// A ring of these is written round-robin. Mounting means reading them all and
// taking the highest generation that checksums — that is the entire crash
// recovery story, in place of a journal replay.
// ---------------------------------------------------------------------------
#define SCHFS_UBER_COUNT 128
#define SCHFS_UBER_START 1 // block 0 stays free for a boot sector
// First block available for the tree and for file data: everything below is
// the boot block and the uberblock ring.
#define SCHFS_FIRST_BLOCK (SCHFS_UBER_START + SCHFS_UBER_COUNT)
#define SCHFS_VERSION 1
#define SCHFS_LABEL_MAX 32
struct schfs_uberblock {
uint32_t magic; // SCHFS_MAGIC
uint32_t crc; // CRC32c of this struct, this field zeroed
uint32_t version; // SCHFS_VERSION
uint32_t blocksize; // SCHFS_BLKSIZE; a reader must refuse a mismatch
uint64_t generation; // transaction number; highest valid one wins
uint64_t root; // block number of the tree root
uint32_t root_crc; // checksum of that root block
uint32_t pad0;
uint64_t root_generation;
uint64_t total_blocks;
// Informational only. Which blocks are actually free is derived from the
// tree at mount rather than stored, so this is what the volume believed at
// commit time — useful for df before anything is mounted, never trusted.
uint64_t free_blocks;
uint64_t reserved0;
uint64_t next_objectid;
uint64_t root_objectid; // normally SCHFS_ROOT_OBJECTID
uint8_t uuid[16];
char label[SCHFS_LABEL_MAX];
uint8_t pad[SCHFS_BLKSIZE - 136];
};
// ---------------------------------------------------------------------------
// Records
// ---------------------------------------------------------------------------
#define SCHFS_T_FILE 1
#define SCHFS_T_DIR 2
#define SCHFS_T_DEV 3
#define SCHFS_T_SYMLINK 4
struct schfs_inode {
uint64_t size;
uint64_t blocks;
uint64_t atime, mtime, ctime;
// Containing directory. Meaningful for directories only, where it is what
// ".." resolves to — neither "." nor ".." is stored on disk, so a directory
// costs nothing until it has real entries. For files with several hard
// links "parent" is simply the first directory that linked them and must
// not be trusted.
uint64_t parent;
uint32_t mode; // real POSIX mode bits, at last
uint32_t uid, gid;
uint32_t nlink;
uint16_t type; // SCHFS_T_*
uint16_t rdev_major, rdev_minor;
uint16_t pad;
uint32_t flags; // SCHFS_I_* — none defined yet
uint32_t gen; // transaction that last changed this inode;
// what an incremental send/snapshot diff needs
};
// An extent maps a run of file blocks onto a run of disk blocks. refs is what
// makes snapshots and reflink copies nearly free: sharing a range is a refcount
// bump rather than a data copy.
struct schfs_extent {
uint64_t disk_block;
uint64_t nblocks;
// Transaction group that allocated this run. A run stamped with the group
// currently being built cannot be reached from any committed root, so its
// blocks may be overwritten in place — which is what stops a program that
// writes a hundred bytes at a time from copying a whole 4 KB block, and
// then a whole tree path, on every call.
//
// A full 64 bits, matching the tree's generation counter, because a 32-bit
// one would wrap after four billion commits and a wrapped comparison says
// "same group" about a block belonging to a committed tree — the one
// mistake this field exists to prevent. Only ever trusted together with
// refs == 1; a shared run belongs to somebody else as well.
uint64_t gen;
uint32_t refs;
uint32_t pad;
};
// Directory entry payload. The key's offset field holds the name hash, so a
// lookup is one tree descent rather than a directory scan.
//
// Two names in one directory can hash the same, and the key would then be
// identical — the second insert would silently replace the first. So the value
// is a *packed list*: one schfs_dirent_rec plus its name bytes, repeated. A
// 64-bit hash makes lists longer than one vanishingly rare, but "vanishingly
// rare" is not "impossible", and a filesystem that loses a file under a hash
// collision is not one to build on.
struct schfs_dirent_rec {
uint64_t objectid;
uint16_t type;
uint16_t namelen;
uint32_t pad;
/* name bytes follow, namelen of them, not NUL-terminated */
};
#define SCHFS_NAME_MAX 255
// Step from one packed dirent to the next. Records are 8-byte aligned so the
// uint64 objectid of the next one is naturally aligned.
#define SCHFS_DIRENT_LEN(namelen) \
((unsigned)((sizeof(struct schfs_dirent_rec) + (namelen) + 7) & ~7u))
// ---------------------------------------------------------------------------
// Compile-time layout checks.
//
// Every one of these is a number that ends up baked into images on disk. A
// silent change to any of them — adding a field, changing a type, a compiler
// padding a struct differently — makes every existing image unreadable in a way
// that looks like corruption rather than like a mistake. Failing the build is
// enormously cheaper than debugging that.
// ---------------------------------------------------------------------------
#define SCHFS_ASSERT_SIZE(t, n) \
typedef char schfs_sizecheck_##t[(sizeof(struct t) == (n)) ? 1 : -1]
SCHFS_ASSERT_SIZE(schfs_key, 24);
SCHFS_ASSERT_SIZE(schfs_node_hdr, 32);
SCHFS_ASSERT_SIZE(schfs_kptr, 48);
SCHFS_ASSERT_SIZE(schfs_item, 32);
SCHFS_ASSERT_SIZE(schfs_uberblock, SCHFS_BLKSIZE);
SCHFS_ASSERT_SIZE(schfs_inode, 80);
SCHFS_ASSERT_SIZE(schfs_extent, 32);
SCHFS_ASSERT_SIZE(schfs_dirent_rec, 16);
#endif /* SCHFS_H */

9
include/sleeplock.h Normal file
View file

@ -0,0 +1,9 @@
// Long-term locks for processes
struct sleeplock {
uint locked; // Is the lock held?
struct spinlock lk; // spinlock protecting this sleep lock
// For debugging:
char *name; // Name of lock.
int pid; // Process holding lock
};

81
include/socket.h Normal file
View file

@ -0,0 +1,81 @@
#pragma once
#include "types.h"
// Unix domain sockets.
//
// These exist for Schcom. A compositor and its clients have to exchange
// messages, and a pipe cannot do it: a pipe is one-directional, has no
// notion of a connection, and gives no way for a server to be *found* by a
// client that starts later. A socket with a name in the filesystem is
// exactly the missing piece.
//
// AF_INET is deliberately absent. The socket layer is being built around the
// local case first because that is the one the GUI needs and the one whose
// semantics are simplest; a network stack plugs in underneath the same
// syscalls later rather than reshaping them.
#define AF_UNIX 1
#define AF_LOCAL 1
#define AF_INET 2 // recognised so it can be refused honestly
#define SOCK_STREAM 1
#define SOCK_DGRAM 2
#define UNIX_PATH_MAX 108
#define SHUT_RD 0
#define SHUT_WR 1
#define SHUT_RDWR 2
struct sockaddr_un {
uint16 sun_family;
char sun_path[UNIX_PATH_MAX];
};
// The generic form syscalls take. Same shape as Linux's, so a musl-linked
// program's struct lines up byte for byte.
struct sockaddr {
uint16 sa_family;
char sa_data[UNIX_PATH_MAX];
};
#ifdef _KERNEL_SOCKET_INTERNALS
#define NSOCK 32 // sockets in the system
#define SOCKBUF 4096 // receive buffer per connected socket
#define NBACKLOG 8
enum sockstate {
SS_FREE = 0,
SS_UNBOUND,
SS_BOUND,
SS_LISTENING,
SS_CONNECTED,
};
// Every field here is protected by the socket table's single lock in
// kernel/net/unix.c -- there is deliberately no per-socket lock; see the
// comment at the top of that file for why.
struct socket {
int ref;
int type;
int state;
char name[UNIX_PATH_MAX]; // the bound path, for a listener
// A connected socket's peer. Writing to a socket appends to the *peer's*
// buffer, which is what makes one buffer per socket enough for a
// bidirectional connection.
struct socket *peer;
int peer_closed;
char data[SOCKBUF];
uint nread, nwrite;
// Connections waiting to be accepted, on a listener.
struct socket *queue[NBACKLOG];
int nqueue;
};
#endif

9
include/spinlock.h Normal file
View file

@ -0,0 +1,9 @@
// Mutual exclusion lock.
struct spinlock {
uint64 locked; // Is the lock held?
// For debugging:
char *name; // Name of lock.
struct cpu *cpu; // The cpu holding the lock.
addr_t pcs[10]; // The call stack (an array of program counters)
// that locked the lock.
};

24
include/stat.h Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include "types.h"
#define T_DIR 1 // Directory
#define T_FILE 2 // File
#define T_DEV 3 // Device
#define T_SYMLINK 4 // Divesymlink
#define T_EXTENT 5 // bmap & itrunc
struct stat {
int32 type; // File type (T_DIR, T_FILE, or T_DEV)
int32 dev; // File system's disk device ID
uint64 ino; // Inode number
int32 nlink; // Number of hard links to file
uint32 pad; // Padding for 64-bit alignment
uint64 size; // Size of file in bytes
uint rwmode; // Permissions
uint supermode; // Mode
uint showmode; // Visiblity
uint atime; // Last access time
uint mtime; // Last modification time
uint ctime; // Last metadata-change time
uint addrs[12+3]; // Block Addresses
};

49
include/syscall.h Normal file
View file

@ -0,0 +1,49 @@
#pragma once
// Syscall numbers and definition macros — all derived from
// kernel/sys/table.inc, which is the single source of truth.
//
// Do NOT add SYS_* constants by hand, and do NOT add anything to
// kernel/sys/linux_abi.c: add one SYSCALL() line to table.inc and write the
// body with SYSCALL_DEFINE(). The constant, the dispatch slot and the Linux
// ABI mapping are all generated from that one line.
#define NSYSCALLS 512
// Scratch slot for syscalls with no Linux equivalent. Every such entry lands
// on this one index of the translation map, which is never dispatched (Linux
// x86_64 numbers currently top out around 450). Using a real index rather
// than a negative sentinel keeps the generated map a plain designated
// initialiser instead of needing a second macro.
#define NO_LINUX 511
#ifdef __ASSEMBLER__
// In .S files GCC passes through the C preprocessor but the assembler
// sees the output, so emit .set directives instead of an enum.
#define SYSCALL(nr, lnr, name) .set SYS_##name, nr
#define SYSCALL_ALIAS(lnr, name)
#include "../kernel/sys/table.inc"
#undef SYSCALL
#undef SYSCALL_ALIAS
#else
// In C files generate a proper enum so constants work in switch/designators.
enum syscall_nr {
_SYS_RESERVED = 0,
#define SYSCALL(nr, lnr, name) SYS_##name = (nr),
#define SYSCALL_ALIAS(lnr, name)
#include "../kernel/sys/table.inc"
#undef SYSCALL
#undef SYSCALL_ALIAS
};
// Every syscall body is written as:
//
// SYSCALL_DEFINE(getpid)
// {
// return myproc()->pid;
// }
//
// so the signature lives in exactly one place and every implementation is
// findable with a single grep.
#define SYSCALL_DEFINE(name) addr_t sys_##name(void)
#endif

86
include/text.h Normal file
View file

@ -0,0 +1,86 @@
#pragma once
// Text rendering.
//
// The console draws text by indexing `font_data[256][16]` and stepping the
// cursor eight pixels. That is a *character cell grid*, and it is the thing in
// the way -- not the font. A grid cannot do variable-width glyphs, cannot
// position text anywhere except on a multiple of eight, cannot anti-alias, and
// cannot represent a codepoint above 255. Swapping the bitmap for a nicer
// bitmap changes none of that.
//
// So this is a layer, not a font. Three things it fixes:
//
// *Glyphs have metrics.* A glyph knows its own size, its bearing and how far
// to advance afterwards. That is what variable-width text is; everything else
// follows from it.
//
// *It draws into a surface, not into the screen.* A compositor draws into an
// off-screen buffer and presents it. Rendering that writes straight to the
// VGA aperture is unusable to it, which is why this takes a target.
//
// *The font is behind an interface.* The bitmap font is one implementation.
// A TrueType rasteriser is another, and arrives without any caller changing:
// glyphs carry 8-bit coverage, so an anti-aliased outline renderer already
// has somewhere to put its output.
struct font;
// Somewhere to draw. Deliberately not a `struct device` or a framebuffer: the
// same code has to work on the virtio-gpu back buffer, on a window's own
// buffer, and on the VGA aperture, and none of those know about each other.
struct surface {
void *pixels; // 32-bit 0x00RRGGBB, the format everything here uses
uint32 stride; // bytes per row, not pixels -- they differ on real hardware
int width, height;
};
// One rendered glyph. `cov` is 8-bit coverage, not a bitmap: 0 is background,
// 255 is solid, and everything between is an anti-aliased edge. A bitmap font
// fills it with 0 and 255 only, and costs nothing for the generality.
struct glyph {
const unsigned char *cov; // w*h bytes, row-major
int w, h; // size of the coverage bitmap
int bearing_x, bearing_y; // offset from the pen to the top-left of `cov`
int advance; // how far the pen moves afterwards
};
struct font_ops {
// Fill `out` for `cp`. Returns 0, or -1 if the font has no such glyph --
// callers substitute rather than failing, because a missing glyph must not
// stop a line of text.
int (*glyph)(struct font *, uint32 cp, struct glyph *out);
int (*height)(struct font *); // line height
int (*baseline)(struct font *); // pen offset from the top of the line
};
struct font {
const struct font_ops *ops;
void *priv;
};
// The built-in bitmap font, as a font provider. Always available, needs no
// allocation, and is what the console falls back to before any filesystem
// exists to load a real typeface from.
struct font *font_builtin(void);
// Draw one UTF-8 string. Returns the x advance.
//
// `x, y` is the pen position on the *baseline*, which is how every text API
// that is not a terminal works -- glyphs of different heights then sit on a
// common line instead of being top-aligned.
int text_draw(struct surface *, struct font *, int x, int y,
const char *utf8, uint32 color);
// Width the same string would occupy, without drawing it. Needed before you
// draw, for centring and for laying out anything at all.
int text_measure(struct font *, const char *utf8);
// One glyph, for callers doing their own layout.
int text_draw_glyph(struct surface *, struct font *, int x, int y,
uint32 cp, uint32 color);
// Decode one UTF-8 sequence. Returns the codepoint and advances *p.
// Malformed input yields U+FFFD and consumes one byte, so a decoder loop
// always terminates.
uint32 utf8_next(const char **p);

57
include/theme.h Normal file
View file

@ -0,0 +1,57 @@
#pragma once
// Schunix visual identity — one palette, shared by everything that draws.
//
// This header exists because there were three unrelated designs and the seams
// showed. The bootloader splash was deep purple with gold text, deliberately
// matching the *panic* screen; the kernel console was cream with near-black
// text; the panic screen was the purple both were copying. So a normal boot
// looked like a crash, then abruptly became an amber terminal, and none of the
// three looked like the same product.
//
// The rule now: **normal states are calm and dark; only failure is loud.**
// Boot, console and shell share SCH_BG/SCH_FG. The panic screen keeps the
// full-bleed purple entirely to itself, which is what makes it mean something.
//
// Included by boot/stage2main.c (freestanding, pre-kernel), kernel/dev/vga.c,
// kernel/core/console.c and kernel/core/panic.c. Plain #defines only — no
// types, no code — because stage2 is compiled 32-bit against a different
// world than the kernel.
//
// Colours are 0xRRGGBB. vga_draw_pixel packs them for whatever depth the
// framebuffer actually is.
// --- the product ---------------------------------------------------------
// Near-black with a slight blue cast rather than pure #000: pure black on an
// LCD reads as "the machine is off", and every serious platform sits a few
// points above it.
#define SCH_BG 0x0C0E13
#define SCH_FG 0xE6E8EC // primary text
#define SCH_DIM 0x707784 // secondary text, hints, version strings
#define SCH_ACCENT 0x7C5CFF // the one accent: violet, keeping the
// project's purple lineage without the
// eye-searing chroma of the old splash
#define SCH_ACCENT_DIM 0x2A2440 // unused track behind a progress bar
#define SCH_OK 0x4ADE80 // reserved: success states
#define SCH_WARN 0xFBBF24 // reserved: warnings that are not panics
// --- failure -------------------------------------------------------------
// PSOGM. Deliberately unlike anything above, because its whole job is to be
// unmistakable. See kernel/core/panic.c.
//
// These are the values panic.c actually renders. It used to carry a second,
// different set of PSOGM_* defines of its own, so the two disagreed about what
// the crash screen looked like and this header was quietly fiction. There was
// also a PSOGM_TITLE in gold here, left over from a splash design that no
// longer exists -- the screen is one background and two weights of text, on
// purpose, because a crash screen with an accent colour is a crash screen
// someone was enjoying designing.
#define PSOGM_BG 0x4B1D9A // the purple
#define PSOGM_FG 0xFFFFFF // the face, the headline, the values
#define PSOGM_DIM 0xC3AEEC // labels and the closing lines
// The one exception to "two weights of text": the QR card has to be a light
// panel with dark modules, because that is what a scanner needs. It reads as a
// label stuck onto the screen, which is roughly what it is.
#define PSOGM_CARD 0xFFFFFF
#define PSOGM_INK 0x000000

69
include/traps.h Normal file
View file

@ -0,0 +1,69 @@
#pragma once
// x86 trap and interrupt constants.
// Processor-defined exceptions:
#define T_DIVIDE 0 // divide error
#define T_DEBUG 1 // debug exception
#define T_NMI 2 // non-maskable interrupt
#define T_BRKPT 3 // breakpoint
#define T_OFLOW 4 // overflow
#define T_BOUND 5 // bounds check
#define T_ILLOP 6 // illegal opcode
#define T_DEVICE 7 // device not available
#define T_DBLFLT 8 // double fault
#define T_TSS 10 // invalid task switch segment
#define T_SEGNP 11 // segment not present
#define T_STACK 12 // stack exception
#define T_GPFLT 13 // general protection fault
#define T_PGFLT 14 // page fault
#define T_FPERR 16 // floating point error
#define T_ALIGN 17 // aligment check
#define T_MCHK 18 // machine check
#define T_SIMDERR 19 // SIMD floating point error
// IRQ offsets
#define T_IRQ0 32 // IRQ 0 corresponds to int T_IRQ
#define IRQ_TIMER 0
#define IRQ_KBD 1
#define IRQ_COM1 4
#define IRQ_MOUSE 12
#define IRQ_IDE 14
#define IRQ_ERROR 19
#define IRQ_SPURIOUS 31
// System call interrupt number for legacy int 0x40 or 0x80 if used
// although syscall/sysret is preferred for x64
#define T_SYSCALL 0x40
#ifndef __ASSEMBLER__
struct trapframe {
// Registers saved by trapasm.S
uint64 rax;
uint64 rbx;
uint64 rcx;
uint64 rdx;
uint64 rbp;
uint64 rsi;
uint64 rdi;
uint64 r8;
uint64 r9;
uint64 r10;
uint64 r11;
uint64 r12;
uint64 r13;
uint64 r14;
uint64 r15;
uint64 trapno;
// Error code and info saved by hardware
uint64 err;
uint64 rip;
uint64 cs;
uint64 rflags;
uint64 rsp;
uint64 ss;
};
#endif

49
include/ttf.h Normal file
View file

@ -0,0 +1,49 @@
#pragma once
// TrueType outlines, as a font provider for include/text.h.
//
// Scope, and why it is this and not "TTF and OTF":
//
// .ttf and .otf are both sfnt containers -- the same table directory, the same
// cmap, the same hmtx. What differs is how outlines are stored. TrueType puts
// quadratic B-spline contours in a `glyf` table; OpenType/PostScript puts cubic
// béziers in a `CFF ` table encoded as Type 2 charstrings, which is a
// stack-based interpreted format and a separate project.
//
// Checked before writing any of it: JetBrains Mono and Roboto -- the two fonts
// this system is going to use -- both ship `glyf` and neither ships `CFF `. So
// CFF support would buy nothing today, and the honest thing is to implement the
// format the fonts actually use and say so, rather than half-implement two.
//
// Everything here is from Apple's TrueType Reference Manual and the OpenType
// specification, both public documents.
struct ttf {
const unsigned char *data;
uint32 len;
uint32 glyf, glyf_len;
uint32 loca, loca_len;
uint32 cmap4; // offset of the format 4 subtable, 0 if none
uint32 hmtx;
int num_glyphs;
int num_hmetrics;
int units_per_em;
int long_loca; // indexToLocFormat: 0 = 16-bit offsets, 1 = 32
int ascent, descent;
// Rasterisation target size, in pixels per em. Set by ttf_open.
int px;
};
// Parse a font already in memory. Returns 0, or -1 if it is not a TrueType
// font this can read. `data` must outlive the struct -- nothing is copied.
int ttf_open(struct ttf *, const void *data, uint32 len, int pixels_per_em);
// Rasterise one codepoint into `cov`. Returns 0, or -1 if the font has no such
// glyph or it will not fit. Exposed because the host harness uses it directly.
int ttf_render(struct ttf *, uint32 cp, struct glyph *out, unsigned char *cov,
int covcap, int *ox, int *oy);
// A font provider over it, for text.h. The struct must outlive the font.
struct font *ttf_font(struct ttf *);

79
include/types.h Normal file
View file

@ -0,0 +1,79 @@
#pragma once
typedef unsigned int uint;
typedef unsigned short ushort;
typedef unsigned char uchar;
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
typedef signed char int8;
typedef signed short int16;
typedef signed int int32;
// uint64 has to be 64 bits everywhere this header is used, and `unsigned long`
// is not that everywhere. Linux and the x86_64-elf kernel target are LP64, so
// long is 64-bit; Windows is LLP64, where long is 32-bit and only long long is
// 64-bit.
//
// This matters far more than a portability nicety. The host tools -- mkfs,
// mkschfs, the SchFS suites -- include this header *and* the on-disk structure
// definitions in fs.h and schfs.h. Building them on Windows with a 32-bit
// uint64 does not fail: it quietly produces images whose inode numbers, sizes
// and block pointers are half the width the kernel reads back, which shows up
// as a corrupt filesystem long after the tool that wrote it has exited.
//
// Spelled as long/long long rather than <stdint.h>'s uint64_t so the `%l`
// format specifiers used throughout the kernel stay correct and untouched.
// Conditioned on the actual width of `long` rather than on the platform,
// because there are three cases and only one of them is about Windows:
// x86_64-elf / Linux LP64 -> long is 64-bit
// Windows LLP64 -> long is 32-bit, long long is 64
// i386-elf (boot/) ILP32 -> long is 32-bit, and stage2main.c includes
// this header too
#if defined(__SIZEOF_LONG__) && __SIZEOF_LONG__ == 8
typedef unsigned long uint64;
typedef signed long int64;
#else
typedef unsigned long long uint64;
typedef signed long long int64;
#endif
// Belt and braces: if the above is ever wrong on some new host, say so at
// compile time rather than in a filesystem image.
_Static_assert(sizeof(uint64) == 8, "uint64 must be 64 bits");
_Static_assert(sizeof(int64) == 8, "int64 must be 64 bits");
typedef uint64 uintptr_t;
typedef int64 intptr_t;
typedef int64 ptrdiff_t;
typedef uint64 addr_t;
typedef uint64 size_t;
typedef int64 ssize_t;
#ifndef MKFS
#ifndef _SIGSET_T
#define _SIGSET_T
typedef uint64 sigset_t;
#endif
#endif
typedef addr_t pte_t;
typedef addr_t pde_t;
typedef addr_t pdpe_t;
typedef addr_t pml4e_t;
struct mouse_data {
int x;
int y;
uint8 buttons;
};
#ifndef NULL
#define NULL ((void*)0)
#endif
#define TRUE 1
#define FALSE 0

49
include/uaccess.h Normal file
View file

@ -0,0 +1,49 @@
// User-memory access API, modeled on Linux's <linux/uaccess.h>.
//
// Rules for kernel code touching user pointers:
//
// 1. Never dereference a user-supplied address without validating it
// first. A syscall argument can point anywhere, including at kernel
// memory or at nothing at all.
//
// 2. Never assume a user buffer is physically contiguous. uva2ka() only
// translates ONE page; the direct-map alias it returns runs into an
// unrelated physical page at the next 4K boundary. Any multi-byte
// access through it must stay within a single page.
//
// These helpers encapsulate both rules. Because Schunix (like old x86
// without SMAP) keeps the user page table active in kernel mode, a raw
// user virtual address is directly dereferenceable once the whole range
// has been validated — that is what user_ptr() hands back, and why the
// result is safe across page boundaries where a uva2ka() pointer is not.
#ifndef SCHUNIX_UACCESS_H
#define SCHUNIX_UACCESS_H
#include "types.h"
// Nonzero if [uaddr, uaddr+size) is entirely mapped, user-accessible
// memory in the current process. size == 0 is always ok.
int access_ok(addr_t uaddr, uint64 size);
// Validate the whole range and return it as a directly usable pointer
// (the raw user VA). 0 on bad/NULL address.
void *user_ptr(addr_t uaddr, uint64 size);
// Copy between kernel and user memory, page by page.
// Return 0 on success, -EFAULT on any unmapped page.
int copy_from_user(void *dst, addr_t uaddr, uint64 n);
int copy_to_user(addr_t uaddr, const void *src, uint64 n);
// Fixed-size scalar variants, in the spirit of Linux's get_user/put_user.
#define get_user(x, uaddr) copy_from_user(&(x), (addr_t)(uaddr), sizeof(x))
#define put_user(x, uaddr) copy_to_user((addr_t)(uaddr), &(x), sizeof(x))
// Longest string fetchstr()/strlen_user() will scan before giving up.
#define USER_STR_MAX 32768
// Length of the NUL-terminated string at uaddr (excluding the NUL),
// validating every page it spans. -EFAULT if it runs off mapped memory
// or exceeds USER_STR_MAX.
long strlen_user(addr_t uaddr);
#endif // SCHUNIX_UACCESS_H

270
include/usb.h Normal file
View file

@ -0,0 +1,270 @@
#pragma once
#include "device.h"
// USB, the parts that are the same whatever host controller you have.
//
// Everything here comes from the USB 2.0 specification chapter 9, which every
// USB device implements regardless of speed, class or vendor. That is what
// makes enumeration possible at all: before you know anything about a device
// you can still ask it what it is, because the question is standardised.
// bmRequestType
#define USB_DIR_OUT 0x00
#define USB_DIR_IN 0x80
#define USB_TYPE_STANDARD 0x00
#define USB_TYPE_CLASS 0x20
#define USB_RECIP_DEVICE 0x00
#define USB_RECIP_INTERFACE 0x01
#define USB_RECIP_ENDPOINT 0x02
#define USB_RECIP_OTHER 0x03 // a hub's downstream port
// bRequest, standard requests
#define USB_REQ_GET_STATUS 0
#define USB_REQ_CLEAR_FEATURE 1
#define USB_REQ_SET_FEATURE 3
#define USB_REQ_SET_ADDRESS 5
#define USB_REQ_GET_DESCRIPTOR 6
#define USB_REQ_SET_DESCRIPTOR 7
#define USB_REQ_GET_CONFIG 8
#define USB_REQ_SET_CONFIG 9
#define USB_REQ_GET_INTERFACE 10
#define USB_REQ_SET_INTERFACE 11
// Descriptor types (the high byte of wValue in a GET_DESCRIPTOR)
#define USB_DT_DEVICE 1
#define USB_DT_CONFIG 2
#define USB_DT_STRING 3
#define USB_DT_INTERFACE 4
#define USB_DT_ENDPOINT 5
// Class codes we care about
#define USB_CLASS_HID 0x03
#define USB_CLASS_MASS_STORAGE 0x08
#define USB_CLASS_HUB 0x09
// HID subclass/protocol. Boot protocol is the one a BIOS uses, and the reason
// a USB keyboard works before any operating system has loaded: the report
// layout is fixed, so it can be read without parsing a report descriptor.
#define USB_HID_SUBCLASS_BOOT 0x01
#define USB_HID_PROTO_KEYBOARD 0x01
#define USB_HID_PROTO_MOUSE 0x02
// Bus speeds.
//
// These are xHCI's numbering, used as the common currency rather than
// translated at the boundary. That is a deliberate choice: there is one host
// controller, the values have to reach a slot context eventually, and a
// second enum whose only job is to be converted into this one would be
// ceremony. If a second controller ever appears, this is the thing to
// revisit -- but a class driver still has no business including xhci.h.
#define USB_SPEED_FULL 1 // 12 Mb/s
#define USB_SPEED_LOW 2 // 1.5 Mb/s
#define USB_SPEED_HIGH 3 // 480 Mb/s
#define USB_SPEED_SUPER 4 // 5 Gb/s
// An eight-byte SETUP packet, the header of every control transfer.
struct usb_setup {
uint8 bmRequestType;
uint8 bRequest;
uint16 wValue;
uint16 wIndex;
uint16 wLength;
} __attribute__((packed));
struct usb_device_desc {
uint8 bLength;
uint8 bDescriptorType;
uint16 bcdUSB;
uint8 bDeviceClass;
uint8 bDeviceSubClass;
uint8 bDeviceProtocol;
uint8 bMaxPacketSize0;
uint16 idVendor;
uint16 idProduct;
uint16 bcdDevice;
uint8 iManufacturer;
uint8 iProduct;
uint8 iSerialNumber;
uint8 bNumConfigurations;
} __attribute__((packed));
struct usb_config_desc {
uint8 bLength;
uint8 bDescriptorType;
uint16 wTotalLength;
uint8 bNumInterfaces;
uint8 bConfigurationValue;
uint8 iConfiguration;
uint8 bmAttributes;
uint8 bMaxPower;
} __attribute__((packed));
struct usb_interface_desc {
uint8 bLength;
uint8 bDescriptorType;
uint8 bInterfaceNumber;
uint8 bAlternateSetting;
uint8 bNumEndpoints;
uint8 bInterfaceClass;
uint8 bInterfaceSubClass;
uint8 bInterfaceProtocol;
uint8 iInterface;
} __attribute__((packed));
struct usb_endpoint_desc {
uint8 bLength;
uint8 bDescriptorType;
uint8 bEndpointAddress; // bit 7: 1 = IN
uint8 bmAttributes; // bits 1:0: 0 ctrl, 1 iso, 2 bulk, 3 interrupt
uint16 wMaxPacketSize;
uint8 bInterval;
} __attribute__((packed));
#define USB_EP_XFERTYPE(a) ((a) & 0x03)
#define USB_EP_BULK 2
#define USB_EP_INTERRUPT 3
#define USB_EP_IS_IN(a) (((a) & 0x80) != 0)
#define USB_EP_NUM(a) ((a) & 0x0F)
// Mass storage: SCSI transparent command set over bulk-only transport.
#define USB_MSC_SUBCLASS_SCSI 0x06
#define USB_MSC_PROTO_BOT 0x50
// ---------------------------------------------------------------------------
// Hubs
//
// A hub is a USB device like any other, and everything it does is done by
// sending it ordinary control requests. There is no separate bus protocol:
// "reset port 3" is a SET_FEATURE addressed to the hub.
// ---------------------------------------------------------------------------
#define USB_DT_HUB 0x29
// Port features, the wValue of SET_FEATURE / CLEAR_FEATURE on a port.
#define PORT_FEAT_ENABLE 1
#define PORT_FEAT_RESET 4
#define PORT_FEAT_POWER 8
#define PORT_FEAT_C_CONNECTION 16
#define PORT_FEAT_C_ENABLE 17
#define PORT_FEAT_C_RESET 20
// wPortStatus, the first half of a port's GET_STATUS reply.
#define PORTSTAT_CONNECTION (1 << 0)
#define PORTSTAT_ENABLE (1 << 1)
#define PORTSTAT_RESET (1 << 4)
#define PORTSTAT_POWER (1 << 8)
#define PORTSTAT_LOWSPEED (1 << 9)
#define PORTSTAT_HIGHSPEED (1 << 10)
struct usb_hub_desc {
uint8 bDescLength;
uint8 bDescriptorType;
uint8 bNbrPorts;
uint16 wHubCharacteristics;
uint8 bPwrOn2PwrGood; // in 2 ms units
uint8 bHubContrCurrent;
} __attribute__((packed));
// Bits 1:0 of wHubCharacteristics: 00 ganged power, 01 per-port.
#define HUB_CHAR_TT_MASK 0x0060
// HID class requests, on the interface rather than the device.
#define USB_HID_SET_IDLE 0x0A
#define USB_HID_SET_PROTOCOL 0x0B
#define USB_HID_PROTOCOL_BOOT 0
// ---------------------------------------------------------------------------
// The interface between the USB core and a host controller
//
// One host controller exists (xHCI) and one probably always will, but the
// core has no business knowing about TRBs and slot IDs, and the controller
// has no business knowing what a HID report is. This is where the line goes.
// ---------------------------------------------------------------------------
struct usb_dev;
struct usb_hc_ops {
// A control transfer on endpoint 0. `buf`, if any, must be DMA memory.
int (*control)(struct usb_dev *, struct usb_setup *, void *buf, int len);
// Make an endpoint usable. Returns 0, or -1 if the controller refused.
int (*open_ep)(struct usb_dev *, const struct usb_endpoint_desc *);
// Queue one IN transfer. Completion arrives via dev->complete.
int (*queue_in)(struct usb_dev *, int ep_addr, void *buf, int len);
// One bulk transfer, synchronously. Sleeps, so process context only.
// Returns bytes transferred, or -1.
int (*bulk)(struct usb_dev *, int ep_addr, void *buf, int len);
// Bring up a device found on a downstream port of `hub`. The controller
// works out the route string from the hub's own position.
void (*attach)(struct usb_dev *hub, int hub_port, int speed);
// Tell the controller this device is a hub with `nports` ports. It has to
// know: a hub is scheduled differently from anything else, and devices
// behind it are addressed relative to it.
int (*set_hub)(struct usb_dev *, int nports, int mtt);
};
#define USB_MAXCONF 256 // bytes of configuration descriptor we keep
struct usb_dev {
const struct usb_hc_ops *ops;
void *hc; // the controller's own handle for this device
int slot;
int speed;
int port; // ROOT hub port this whole branch hangs off
// Where the device sits in the tree.
//
// A USB device has no address of its own that describes its position -- the
// controller finds it by being told the path. `route` is xHCI's route
// string: five nibbles, one per tier, each holding the downstream port
// number taken at that tier. Tier 0 is a device on a root port, and its
// route is 0.
uint32 route;
int tier;
// Transaction translator, for a full or low speed device behind a
// high-speed hub. The controller has to know which hub is doing the
// speed conversion on its behalf, because it is the hub, not the device,
// that it actually schedules against.
int tt_slot, tt_port;
int in_use;
struct usb_device_desc desc;
uchar conf[USB_MAXCONF]; // the configuration descriptor and everything
int conflen; // that follows it, exactly as the device sent
// The first interface, which is all a boot-protocol or bulk-only device
// needs, and the endpoints belonging to it.
struct usb_interface_desc iface;
struct usb_endpoint_desc intr_in;
struct usb_endpoint_desc bulk_in;
struct usb_endpoint_desc bulk_out;
int has_intr_in, has_bulk_in, has_bulk_out;
// Filled in by whichever class driver claimed the device.
const char *driver;
void *drvdata;
void (*complete)(struct usb_dev *, void *buf, int len);
};
// One row per class driver. Matching is on the *interface* descriptor,
// because bDeviceClass is very often 0 ("look at the interfaces").
struct usb_driver {
const char *name;
uint8 class, subclass, protocol; // 0xFF matches anything
// What the rest of the kernel should consider this device to be, once this
// driver has claimed it. Distinct from `class` above, which is the USB
// interface class the descriptor advertises: the driver knows what it made
// of the device, the descriptor only says what the device claims to be.
enum dev_class devclass;
int (*probe)(struct usb_dev *);
};
// Called by a host controller once a device is addressed and its device
// descriptor has been read. The core reads the configuration, selects it, and
// offers the device to the class drivers.
void usb_attach(struct usb_dev *dev);

117
include/user.h Normal file
View file

@ -0,0 +1,117 @@
#pragma once
#ifndef USER_H
#define USER_H
#include "types.h"
#include "date.h"
struct stat;
struct rtcdate;
struct mouse_data;
// --- system calls ---
int fork(void);
void exit(void) __attribute__((noreturn));
int wait(void);
int pipe(int*);
int write(int, void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(char*, char**);
int open(char*, int);
int mknod(char*, short, short);
int unlink(char*);
int fstat(int fd, struct stat*);
int link(char*, char*);
int mkdir(char*);
int chdir(char*);
int dup(int);
int dup2(int, int);
int fcntl(int, int, int);
long lseek(int, long, int);
int getpid(void);
char* sbrk(uint64);
int sleep(int);
int uptime(void);
int vga_sync(void*);
int getmouse(struct mouse_data*);
int reboot(void);
int shutdown(void);
int sigfillset(sigset_t *set);
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigwait(const sigset_t *set, int *sig);
int alarm(int seconds);
int waitpid(int pid, int *status, int options);
int setsid(void);
void* mmap(void*, uint64, int, int, int, uint64);
// Shared memory. shmat returns the attach address, or a small negative value
// on failure — there is no errno in this runtime, so check for < 0.
int shmget(int key, uint64 size, int flags);
void* shmat(int id, void *addr, int flags);
int shmdt(void *addr);
int munmap(void*, uint64);
int delete(char*);
int show(char*);
int showstat(char*, struct stat*);
int ftruncate(int, int);
int truncate(char*, int);
int rename(const char*, const char*);
int symlink(char*, char*);
int readlink(char*, char*, int);
int getuid(void);
int geteuid(void);
int nanosleep(void*);
int mprotect(void*, uint64, int);
int ioctl(int, uint64, void*);
char* getcwd(char*, int);
int arch_prctl(int, uint64);
int set_tid_address(int*);
int set_robust_list(void*, int);
int futex(int*, int, int, void*, int*, int);
int clone(uint64, void*, void*, void*, void*);
void exit_group(int) __attribute__((noreturn));
int wait4(int, int*, int, void*);
int getgid(void);
int getegid(void);
int umask(int);
int writev(int, void*, int);
int readv(int, void*, int);
void* brk(void*);
int prlimit(int, int, void*, void*);
int lstat(const char*, struct stat*);
int access(const char*, int);
int openat(int, const char*, int, ...);
int fchdir(int);
int enable_linux_abi(void);
int getrlimit(int, void*);
int setuid(int);
int setgid(int);
int getsid(int);
int dup3(int, int, int);
int pipe2(int*, int);
int clock_gettime(int, void*);
int getppid(void);
int rt_sigaction(int, void*, void*, uint64);
int mount(const char *src, const char *tgt, const char *fstype, uint64 flags, void *data);
// Sockets. `struct sockaddr` and the AF_*/SOCK_* constants are in socket.h;
// include it as well as this header to use them.
struct sockaddr;
int socket(int domain, int type, int protocol);
int bind(int fd, const struct sockaddr *addr, int addrlen);
int listen(int fd, int backlog);
int accept(int fd, struct sockaddr *addr, int *addrlen);
int connect(int fd, const struct sockaddr *addr, int addrlen);
int socketpair(int domain, int type, int protocol, int sv[2]);
int sendto(int fd, const void *buf, int len, int flags, const struct sockaddr*, int);
int recvfrom(int fd, void *buf, int len, int flags, struct sockaddr*, int*);
int sockshutdown(int fd, int how);
int setsockopt(int fd, int level, int opt, const void *val, int len);
int getsockopt(int fd, int level, int opt, void *val, int *len);
// --- ulib.c ---
int stat(const char*, struct stat*);
char* gets(char*, int max);
#endif // USER_H

174
include/virtio.h Normal file
View file

@ -0,0 +1,174 @@
#pragma once
// virtio 1.0 over PCI — the transport, and virtio-gpu's 2D protocol.
//
// Written from the OASIS specification (virtio 1.1, chapters 2, 4.1 and 5.7),
// which is a public document with no licence on what you write from it. Same
// reasoning as kernel/drivers/xhci.c.
//
// Why virtio-gpu at all, when there is already a framebuffer: the VBE
// framebuffer is a flat block of memory the loader happened to be told about.
// It has no mode setting, no notion of a frame being complete, and no way to
// ask the host to present anything -- so a compositor drawing into it is
// racing the scanout with no way to synchronise. virtio-gpu has resources,
// explicit transfer and an explicit flush, which is the model every real GPU
// driver has. It is the smallest device that teaches the right shape.
// --- PCI transport ---------------------------------------------------------
#define VIRTIO_VENDOR 0x1AF4
// Modern (1.0) device IDs are 0x1040 + the virtio device type. GPU is type 16.
#define VIRTIO_DEV_GPU 0x1050
// Vendor-specific PCI capability, which is how a virtio device says where in
// its BARs each configuration structure lives.
#define PCI_CAP_ID_VNDR 0x09
#define VIRTIO_PCI_CAP_COMMON_CFG 1
#define VIRTIO_PCI_CAP_NOTIFY_CFG 2
#define VIRTIO_PCI_CAP_ISR_CFG 3
#define VIRTIO_PCI_CAP_DEVICE_CFG 4
// Offsets within the common configuration structure. Written out rather than
// declared as a struct: the fields are little-endian by specification and of
// mixed width, and a struct invites the compiler to pad or a caller to forget
// that these are MMIO and must be accessed at exactly the stated width.
#define VCOMMON_DFEAT_SEL 0x00 // le32
#define VCOMMON_DFEAT 0x04 // le32
#define VCOMMON_GFEAT_SEL 0x08 // le32
#define VCOMMON_GFEAT 0x0C // le32
#define VCOMMON_MSIX_CFG 0x10 // le16
#define VCOMMON_NUM_QUEUES 0x12 // le16
#define VCOMMON_STATUS 0x14 // u8
#define VCOMMON_CFG_GEN 0x15 // u8
#define VCOMMON_Q_SELECT 0x16 // le16
#define VCOMMON_Q_SIZE 0x18 // le16
#define VCOMMON_Q_MSIX 0x1A // le16
#define VCOMMON_Q_ENABLE 0x1C // le16
#define VCOMMON_Q_NOTIFY_OFF 0x1E // le16
#define VCOMMON_Q_DESC 0x20 // le64
#define VCOMMON_Q_DRIVER 0x28 // le64
#define VCOMMON_Q_DEVICE 0x30 // le64
// device_status bits, set in this order during bring-up.
#define VSTATUS_ACK 1
#define VSTATUS_DRIVER 2
#define VSTATUS_DRIVER_OK 4
#define VSTATUS_FEATURES_OK 8
#define VSTATUS_FAILED 0x80
#define VIRTIO_F_VERSION_1 32 // feature bit: this is a 1.0 device
// --- split virtqueue -------------------------------------------------------
#define VRING_DESC_F_NEXT 1
#define VRING_DESC_F_WRITE 2 // device writes it; i.e. this is our buffer
struct vring_desc {
uint64 addr;
uint32 len;
uint16 flags;
uint16 next;
} __attribute__((packed));
struct vring_avail {
uint16 flags;
uint16 idx;
uint16 ring[];
} __attribute__((packed));
struct vring_used_elem {
uint32 id;
uint32 len;
} __attribute__((packed));
struct vring_used {
uint16 flags;
uint16 idx;
struct vring_used_elem ring[];
} __attribute__((packed));
// --- virtio-gpu, 2D --------------------------------------------------------
#define VIRTIO_GPU_CMD_GET_DISPLAY_INFO 0x0100
#define VIRTIO_GPU_CMD_RESOURCE_CREATE_2D 0x0101
#define VIRTIO_GPU_CMD_RESOURCE_UNREF 0x0102
#define VIRTIO_GPU_CMD_SET_SCANOUT 0x0103
#define VIRTIO_GPU_CMD_RESOURCE_FLUSH 0x0104
#define VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D 0x0105
#define VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING 0x0106
#define VIRTIO_GPU_RESP_OK_NODATA 0x1100
#define VIRTIO_GPU_RESP_OK_DISPLAY_INFO 0x1101
#define VIRTIO_GPU_MAX_SCANOUTS 16
// B8G8R8X8, which is what a 32-bit little-endian 0x00RRGGBB word looks like in
// memory -- the same layout vga.c already draws into, so a framebuffer can be
// handed over without a conversion pass.
#define VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM 2
struct virtio_gpu_ctrl_hdr {
uint32 type;
uint32 flags;
uint64 fence_id;
uint32 ctx_id;
uint32 padding;
} __attribute__((packed));
struct virtio_gpu_rect {
uint32 x, y, width, height;
} __attribute__((packed));
struct virtio_gpu_display_one {
struct virtio_gpu_rect r;
uint32 enabled;
uint32 flags;
} __attribute__((packed));
struct virtio_gpu_resp_display_info {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_display_one pmodes[VIRTIO_GPU_MAX_SCANOUTS];
} __attribute__((packed));
struct virtio_gpu_resource_create_2d {
struct virtio_gpu_ctrl_hdr hdr;
uint32 resource_id;
uint32 format;
uint32 width;
uint32 height;
} __attribute__((packed));
struct virtio_gpu_mem_entry {
uint64 addr;
uint32 length;
uint32 padding;
} __attribute__((packed));
struct virtio_gpu_resource_attach_backing {
struct virtio_gpu_ctrl_hdr hdr;
uint32 resource_id;
uint32 nr_entries;
} __attribute__((packed));
struct virtio_gpu_set_scanout {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_rect r;
uint32 scanout_id;
uint32 resource_id;
} __attribute__((packed));
struct virtio_gpu_transfer_to_host_2d {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_rect r;
uint64 offset;
uint32 resource_id;
uint32 padding;
} __attribute__((packed));
struct virtio_gpu_resource_flush {
struct virtio_gpu_ctrl_hdr hdr;
struct virtio_gpu_rect r;
uint32 resource_id;
uint32 padding;
} __attribute__((packed));

219
include/x86.h Normal file
View file

@ -0,0 +1,219 @@
#pragma once
struct dtr {
ushort limit;
uint64 base;
} __attribute__((packed));
static inline uchar
inb(ushort port)
{
uchar data;
asm volatile("in %1,%0" : "=a" (data) : "d" (port));
return data;
}
static inline void
insl(int port, void *addr, int cnt)
{
asm volatile("cld; rep insl" :
"=D" (addr), "=c" (cnt) :
"d" (port), "0" (addr), "1" (cnt) :
"memory", "cc");
}
static inline void
outb(ushort port, uchar data)
{
asm volatile("out %0,%1" : : "a" (data), "d" (port));
}
static inline uint
inl(ushort port)
{
uint data;
asm volatile("inl %1,%0" : "=a" (data) : "d" (port));
return data;
}
static inline void
outw(ushort port, ushort data)
{
asm volatile("out %0,%1" : : "a" (data), "d" (port));
}
// PCI configuration space is reached through a pair of 32-bit ports, so this
// is not optional decoration -- see kernel/dev/pci.c.
static inline void
outl(ushort port, uint data)
{
asm volatile("outl %0,%1" : : "a" (data), "d" (port));
}
static inline void
outsl(int port, const void *addr, int cnt)
{
asm volatile("cld; rep outsl" :
"=S" (addr), "=c" (cnt) :
"d" (port), "0" (addr), "1" (cnt) :
"cc");
}
static inline void
stosb(void *addr, int data, int cnt)
{
asm volatile("cld; rep stosb" :
"=D" (addr), "=c" (cnt) :
"0" (addr), "1" (cnt), "a" (data) :
"memory", "cc");
}
// 64-bit-only helpers. `rep stosq` and `xchgq` do not exist in 32-bit mode, so
// these must not even be parsed by the -m32 bootloader build -- they compiled
// there before only because uint64 was accidentally 32 bits wide, which is the
// bug include/types.h now asserts against.
#ifdef __x86_64__
static inline void
stosq(void *addr, uint64 data, int cnt)
{
asm volatile("cld; rep stosq" :
"=D" (addr), "=c" (cnt) :
"0" (addr), "1" (cnt), "a" (data) :
"memory", "cc");
}
struct segdesc;
static inline void
lgdt(struct segdesc *p, int size)
{
struct dtr d;
d.limit = size - 1;
d.base = (uint64)p;
asm volatile("lgdt (%0)" : : "r" (&d));
}
struct gatedesc;
static inline void
lidt(struct gatedesc *p, int size)
{
struct dtr d;
d.limit = size - 1;
d.base = (uint64)p;
asm volatile("lidt (%0)" : : "r" (&d));
}
static inline void
ltr(ushort sel)
{
asm volatile("ltr %0" : : "r" (sel));
}
static inline uint64
readeflags(void)
{
uint64 flags;
asm volatile("pushfq; pop %0" : "=r" (flags));
return flags;
}
static inline void
cli(void)
{
asm volatile("cli");
}
static inline void
sti(void)
{
asm volatile("sti");
}
static inline void
hlt(void)
{
asm volatile("hlt");
}
static inline uint64
xchg(volatile uint64 *addr, uint64 newval)
{
uint64 result;
asm volatile("lock; xchgq %0, %1" :
"+m" (*addr), "=a" (result) :
"1" (newval) :
"cc");
return result;
}
#endif /* __x86_64__ */
static inline uint64
rcr2(void)
{
uint64 val;
asm volatile("mov %%cr2,%0" : "=r" (val));
return val;
}
static inline void
lcr3(uint64 val)
{
asm volatile("mov %0,%%cr3" : : "r" (val));
}
// Invalidate the TLB entry for one page. Needed any time a PTE is changed
// or cleared for an address space that may still be the active CR3 —
// otherwise the CPU can keep translating through the stale entry until the
// next full CR3 reload (context switch), silently reading/writing the
// wrong physical page.
static inline void
invlpg(void *va)
{
asm volatile("invlpg (%0)" : : "r" (va) : "memory");
}
static inline void
wrmsr(uint32 msr, uint64 val)
{
uint32 low = (uint32)val;
uint32 high = (uint32)(val >> 32);
asm volatile("wrmsr" : : "c" (msr), "a" (low), "d" (high));
}
static inline uint64
rdmsr(uint32 msr)
{
uint32 low, high;
asm volatile("rdmsr" : "=a" (low), "=d" (high) : "c" (msr));
return ((uint64)high << 32) | low;
}
static inline uint64
readcs(void)
{
uint64 val;
asm volatile("mov %%cs, %0" : "=r" (val));
return val;
}
// CPUID. `leaf` in EAX, results in EAX/EBX/ECX/EDX.
//
// %rbx is callee-saved *and* the PIC base register on some builds, so it is
// exchanged out rather than named as an operand -- naming it directly is the
// classic way to get "inline asm clobbers reserved register" or, worse, silent
// corruption in the caller.
static inline void
cpuid_leaf(uint32 leaf, uint32 *a, uint32 *b, uint32 *c, uint32 *d)
{
uint32 ra, rb, rc, rd;
asm volatile("xchgq %%rbx, %q1\n\t"
"cpuid\n\t"
"xchgq %%rbx, %q1"
: "=a"(ra), "=&r"(rb), "=c"(rc), "=d"(rd)
: "0"(leaf), "2"(0));
if (a) *a = ra;
if (b) *b = rb;
if (c) *c = rc;
if (d) *d = rd;
}

161
include/xhci.h Normal file
View file

@ -0,0 +1,161 @@
#pragma once
// eXtensible Host Controller Interface -- the register and data-structure
// definitions from the xHCI 1.2 specification.
//
// Why xHCI and not UHCI/OHCI/EHCI: xHCI is the only host controller interface
// that speaks every USB speed, so one driver covers USB 1.1 through 3.x. The
// older controllers would each need their own, and a machine with all of them
// needs the companion-controller handover dance on top. There is no hardware
// worth supporting that has no xHCI.
// ---------------------------------------------------------------------------
// Capability registers -- at the start of BAR0, read-only
// ---------------------------------------------------------------------------
#define XHCI_CAPLENGTH 0x00 // byte: where the operational regs begin
#define XHCI_HCIVERSION 0x02 // word
#define XHCI_HCSPARAMS1 0x04
#define XHCI_HCSPARAMS2 0x08
#define XHCI_HCSPARAMS3 0x0C
#define XHCI_HCCPARAMS1 0x10
#define XHCI_DBOFF 0x14 // doorbell array offset from BAR0
#define XHCI_RTSOFF 0x18 // runtime register offset from BAR0
#define HCS1_MAXSLOTS(x) ((x) & 0xFF)
#define HCS1_MAXINTRS(x) (((x) >> 8) & 0x7FF)
#define HCS1_MAXPORTS(x) (((x) >> 24) & 0xFF)
// Scratchpad buffer count is split across two fields, high part first.
#define HCS2_SPB_HI(x) (((x) >> 21) & 0x1F)
#define HCS2_SPB_LO(x) (((x) >> 27) & 0x1F)
#define HCC1_AC64 (1 << 0) // 64-bit addressing
#define HCC1_CSZ (1 << 2) // contexts are 64 bytes, not 32
#define HCC1_XECP(x) (((x) >> 16) & 0xFFFF) // extended caps, in dwords
// ---------------------------------------------------------------------------
// Operational registers -- at BAR0 + CAPLENGTH
// ---------------------------------------------------------------------------
#define XHCI_USBCMD 0x00
#define XHCI_USBSTS 0x04
#define XHCI_PAGESIZE 0x08
#define XHCI_DNCTRL 0x14
#define XHCI_CRCR 0x18 // 64-bit
#define XHCI_DCBAAP 0x30 // 64-bit
#define XHCI_CONFIG 0x38
#define XHCI_PORTSC(p) (0x400 + 0x10 * ((p) - 1)) // ports are 1-based
#define CMD_RS (1 << 0) // run/stop
#define CMD_HCRST (1 << 1) // host controller reset
#define CMD_INTE (1 << 2) // interrupter enable
#define CMD_HSEE (1 << 3)
#define STS_HCH (1 << 0) // halted
#define STS_HSE (1 << 2) // host system error
#define STS_EINT (1 << 3) // event interrupt (write 1 to clear)
#define STS_PCD (1 << 4) // port change detect (write 1 to clear)
#define STS_CNR (1 << 11) // controller not ready
#define CRCR_RCS (1 << 0) // ring cycle state
// PORTSC. The change bits are write-1-to-clear, and so is PED -- writing back
// a value read from this register clears whatever happened to be set. Every
// write has to mask them off first.
#define PORT_CCS (1 << 0) // current connect status
#define PORT_PED (1 << 1) // port enabled (RW1C!)
#define PORT_OCA (1 << 3)
#define PORT_PR (1 << 4) // port reset
#define PORT_PLS(x) (((x) >> 5) & 0xF)
#define PORT_PP (1 << 9) // port power
#define PORT_SPEED(x) (((x) >> 10) & 0xF)
#define PORT_CSC (1 << 17)
#define PORT_PEC (1 << 18)
#define PORT_PRC (1 << 21)
#define PORT_RW1C 0x00FE0002u // change bits 17..23, plus PED
// PORTSC speed encoding. These are the same numbers as USB_SPEED_* in usb.h,
// which is not a coincidence -- the core uses this encoding directly so that
// nothing has to be translated on its way to a slot context.
#define XSPEED_FULL USB_SPEED_FULL
#define XSPEED_LOW USB_SPEED_LOW
#define XSPEED_HIGH USB_SPEED_HIGH
#define XSPEED_SUPER USB_SPEED_SUPER
// ---------------------------------------------------------------------------
// Runtime registers -- at BAR0 + RTSOFF. Interrupter n is at 0x20 + 32*n.
// ---------------------------------------------------------------------------
#define XHCI_MFINDEX 0x00
#define XHCI_IR0 0x20
#define IR_IMAN 0x00
#define IR_IMOD 0x04
#define IR_ERSTSZ 0x08
#define IR_ERSTBA 0x10 // 64-bit
#define IR_ERDP 0x18 // 64-bit
#define IMAN_IP (1 << 0) // interrupt pending (RW1C)
#define IMAN_IE (1 << 1) // interrupt enable
#define ERDP_EHB (1 << 3) // event handler busy (RW1C)
// ---------------------------------------------------------------------------
// Transfer Request Blocks
// ---------------------------------------------------------------------------
struct trb {
uint64 param;
uint32 status;
uint32 control;
} __attribute__((packed));
#define TRB_TYPE(t) (((t) & 0x3F) << 10)
#define TRB_GET_TYPE(c) (((c) >> 10) & 0x3F)
#define TRB_C (1 << 0) // cycle bit
#define TRB_TC (1 << 1) // toggle cycle (link TRBs)
#define TRB_ISP (1 << 2)
#define TRB_CH (1 << 4) // chain
#define TRB_IOC (1 << 5) // interrupt on completion
#define TRB_IDT (1 << 6) // immediate data
#define TRB_SLOT(s) ((uint32)(s) << 24)
#define TRB_GET_SLOT(c) (((c) >> 24) & 0xFF)
#define TRB_GET_EPID(c) (((c) >> 16) & 0x1F) // transfer events
// Transfer TRBs
#define TRB_NORMAL 1
#define TRB_SETUP 2
#define TRB_DATA 3
#define TRB_STATUS 4
#define TRB_LINK 6
// Command TRBs
#define TRB_ENABLE_SLOT 9
#define TRB_DISABLE_SLOT 10
#define TRB_ADDRESS_DEV 11
#define TRB_CONFIG_EP 12
#define TRB_EVAL_CTX 13
#define TRB_RESET_EP 14
#define TRB_NOOP_CMD 23
// Event TRBs
#define TRB_XFER_EVENT 32
#define TRB_CMD_COMPLETE 33
#define TRB_PORT_STATUS 34
#define TRB_CC(status) (((status) >> 24) & 0xFF)
#define CC_SUCCESS 1
#define CC_SHORT_PACKET 13
// Setup Stage transfer type, and Data Stage direction.
#define TRT_NONE 0
#define TRT_OUT 2
#define TRT_IN 3
#define DIR_IN (1 << 16)
// Endpoint types in an endpoint context. Direction is part of the type, and
// the numbering is not arbitrary: OUT types are 1-3, control is 4, and the IN
// types are the OUT ones plus four.
#define EPTYPE_ISOCH_OUT 1
#define EPTYPE_BULK_OUT 2
#define EPTYPE_INT_OUT 3
#define EPTYPE_CONTROL 4
#define EPTYPE_ISOCH_IN 5
#define EPTYPE_BULK_IN 6
#define EPTYPE_INT_IN 7
// Doorbell targets: 0 is the command ring; for a device slot, DCI 1 is EP0.
#define DCI_EP0 1

264
kernel/arch/x86_64/acpi.c Normal file
View file

@ -0,0 +1,264 @@
// Find the processors, the way firmware from this century describes them.
//
// mpinit() walks the Intel MP table, which is from 1994. Modern firmware fills
// it in minimally or omits it: measured 2026-08-08, QEMU/SeaBIOS with -smp 4
// publishes a 208-byte MP table containing exactly *one* processor entry, then
// buses and interrupt assignments. Everything since has expected the operating
// system to read the ACPI MADT instead.
//
// The consequence was not that SMP was slow, it was that ncpu was 1 and SMP was
// silently never exercised. Machines whose firmware does publish a full MP
// table -- VMware, older hardware -- took the other path, started their APs,
// and triple faulted. So this file is a prerequisite for debugging that: until
// the CPUs are visible under QEMU there is no way to reproduce the crash except
// on the hardware it happens on.
//
// Written from the ACPI specification. No table layout here is copied from
// another kernel; they are all in chapter 5 of the published standard.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
extern struct cpu cpus[NCPU];
extern int ncpu;
extern uchar ioapicid;
extern volatile uint *lapic;
// The Root System Description Pointer. Found by signature, not by being told
// where it is -- it sits either in the Extended BIOS Data Area or in the BIOS
// read-only region, on a 16-byte boundary.
struct acpi_rsdp {
char sig[8]; // "RSD PTR "
uint8 checksum; // over the first 20 bytes
char oemid[6];
uint8 revision; // 0 = ACPI 1.0 (RSDT), 2+ = 2.0 (XSDT)
uint32 rsdt_addr;
// ACPI 2.0 and later only, and only valid when revision >= 2.
uint32 length;
uint64 xsdt_addr;
uint8 ext_checksum; // over `length` bytes
uint8 reserved[3];
} __attribute__((packed));
// Every ACPI table starts with this.
struct acpi_sdt {
char sig[4];
uint32 length;
uint8 revision;
uint8 checksum;
char oemid[6];
char oem_table_id[8];
uint32 oem_revision;
uint32 creator_id;
uint32 creator_revision;
} __attribute__((packed));
// The Multiple APIC Description Table: signature "APIC".
struct acpi_madt {
struct acpi_sdt h;
uint32 lapic_addr;
uint32 flags; // bit 0: legacy 8259 PICs are present
} __attribute__((packed));
#define MADT_PCAT_COMPAT 1
// MADT entry types. Only these three are read; the rest are interrupt
// routing overrides that ioapic.c does not consult yet.
#define MADT_LAPIC 0
#define MADT_IOAPIC 1
#define MADT_LAPIC_OVERRIDE 5
struct madt_entry {
uint8 type;
uint8 length;
} __attribute__((packed));
struct madt_lapic {
struct madt_entry e;
uint8 acpi_id;
uint8 apic_id;
uint32 flags; // bit 0: enabled, bit 1: online-capable
} __attribute__((packed));
#define LAPIC_ENABLED 1
#define LAPIC_ONLINE_CAPABLE 2
struct madt_ioapic {
struct madt_entry e;
uint8 id;
uint8 reserved;
uint32 addr;
uint32 gsi_base;
} __attribute__((packed));
struct madt_lapic_override {
struct madt_entry e;
uint16 reserved;
uint64 addr;
} __attribute__((packed));
static int
checksum_ok(const void *p, uint32 len)
{
const uint8 *b = p;
uint8 sum = 0;
for (uint32 i = 0; i < len; i++)
sum += b[i];
return sum == 0;
}
// Scan a physical range for the RSDP signature. It is 16-byte aligned by
// specification, which makes this cheap enough to do twice.
static struct acpi_rsdp *
scan_rsdp(uint64 base, uint64 len)
{
for (uint64 off = 0; off + sizeof(struct acpi_rsdp) <= len; off += 16) {
struct acpi_rsdp *r = (struct acpi_rsdp *)P2V(base + off);
if (memcmp(r->sig, "RSD PTR ", 8) != 0)
continue;
// Signature alone is not enough: the string can appear in unrelated data,
// and following a bad pointer here means faulting before there is anything
// to report the fault with.
if (!checksum_ok(r, 20))
continue;
return r;
}
return 0;
}
static struct acpi_rsdp *
find_rsdp(void)
{
// The EBDA, whose segment address the BIOS leaves at 0x40E.
uint16 ebda_seg = *(uint16 *)P2V(0x40E);
if (ebda_seg) {
struct acpi_rsdp *r = scan_rsdp((uint64)ebda_seg << 4, 1024);
if (r)
return r;
}
return scan_rsdp(0xE0000, 0x20000);
}
// Walk the RSDT or XSDT looking for a table by signature.
//
// Two nearly identical loops rather than one clever one: the only difference is
// the pointer width -- 32-bit entries in the RSDT, 64-bit in the XSDT -- and
// reading a 64-bit pointer out of a 32-bit array is exactly the sort of bug
// that produces a plausible address pointing at nothing.
static struct acpi_sdt *
find_table(struct acpi_rsdp *rsdp, const char *sig)
{
struct acpi_sdt *root;
int wide;
if (rsdp->revision >= 2 && rsdp->xsdt_addr) {
root = (struct acpi_sdt *)P2V(rsdp->xsdt_addr);
wide = 1;
} else {
root = (struct acpi_sdt *)P2V((uint64)rsdp->rsdt_addr);
wide = 0;
}
if (!checksum_ok(root, root->length))
return 0;
uint32 n = (root->length - sizeof(struct acpi_sdt)) / (wide ? 8 : 4);
for (uint32 i = 0; i < n; i++) {
uint64 pa;
if (wide) {
// Byte-wise, because the array is packed and 8-byte entries in it are
// not guaranteed to be 8-byte aligned.
uint8 *p = (uint8 *)(root + 1) + i * 8;
pa = 0;
for (int b = 7; b >= 0; b--)
pa = (pa << 8) | p[b];
} else {
pa = ((uint32 *)(root + 1))[i];
}
struct acpi_sdt *t = (struct acpi_sdt *)P2V(pa);
if (memcmp(t->sig, sig, 4) == 0)
return t;
}
return 0;
}
// Returns 1 if the processors were enumerated, 0 to fall back to mpinit().
int
acpi_init(void)
{
struct acpi_rsdp *rsdp = find_rsdp();
if (!rsdp)
return 0;
struct acpi_madt *madt = (struct acpi_madt *)find_table(rsdp, "APIC");
if (!madt || !checksum_ok(madt, madt->h.length))
return 0;
lapic = (uint *)P2V((uint64)madt->lapic_addr);
uint8 *p = (uint8 *)madt + sizeof(struct acpi_madt);
uint8 *e = (uint8 *)madt + madt->h.length;
int found_ioapic = 0;
while (p + sizeof(struct madt_entry) <= e) {
struct madt_entry *ent = (struct madt_entry *)p;
// A zero length would spin here forever. Firmware tables are not always
// well formed, and an infinite loop this early is indistinguishable from a
// hang with no output at all.
if (ent->length < sizeof(struct madt_entry))
break;
switch (ent->type) {
case MADT_LAPIC: {
struct madt_lapic *l = (struct madt_lapic *)p;
// Skip processors that are neither enabled nor online-capable: those are
// sockets the firmware is describing but not offering.
if (!(l->flags & (LAPIC_ENABLED | LAPIC_ONLINE_CAPABLE)))
break;
if (ncpu < NCPU) {
cpus[ncpu].apicid = l->apic_id;
ncpu++;
}
break;
}
case MADT_IOAPIC: {
struct madt_ioapic *io = (struct madt_ioapic *)p;
// The first one only. Multi-I/O-APIC routing needs the interrupt source
// overrides too, which nothing here reads yet, so claiming to support it
// would be worse than not.
if (!found_ioapic) {
ioapicid = io->id;
found_ioapic = 1;
}
break;
}
case MADT_LAPIC_OVERRIDE: {
struct madt_lapic_override *o = (struct madt_lapic_override *)p;
lapic = (uint *)P2V(o->addr);
break;
}
}
p += ent->length;
}
if (ncpu == 0)
return 0;
// The 8259s are still there and still wired to the CPU. On a system with an
// IMCR, this switches interrupt delivery from the PICs to the APIC; on one
// without, the ports are unclaimed and the writes go nowhere. mpinit() does
// the same thing guarded by the MP table's imcrp flag.
if (madt->flags & MADT_PCAT_COMPAT) {
outb(0x22, 0x70);
outb(0x23, inb(0x23) | 1);
}
cprintf("acpi: %d cpu%s, lapic at %p, ioapic id %d\n",
ncpu, ncpu == 1 ? "" : "s", lapic, ioapicid);
return 1;
}

105
kernel/arch/x86_64/e820.c Normal file
View file

@ -0,0 +1,105 @@
// Find out how much memory this machine actually has.
//
// PHYSTOP used to be a constant: 0xE000000, 224 MB, inherited from xv6 and
// never revisited. That is not a limit on how much memory the kernel *uses* --
// it is an assertion that exactly that much exists. Boot a machine with less
// and kinit1() walks off the end of RAM into nothing; boot one with more and
// every byte above 224 MB is invisible. Measured before this file existed:
// Schunix booted at 224 MB and triple faulted at 192 MB, for no reason at all.
//
// The firmware has known the answer the whole time. boot/stage2.S asks it with
// INT 0x15 / E820 in real mode -- the same interface Linux's boot stub and GRUB
// use -- and leaves the result in low memory. stage2main.c already parses it to
// print detected RAM on the splash. The kernel simply never read it.
#include "types.h"
#include "defs.h"
#include "memlayout.h"
#include "mmu.h"
// Set before kinit1() and never again. Starts at the old constant so that a
// crash between entry and e820_init() behaves the way it always did rather
// than in some new way.
uint64 phystop = PHYSTOP_FALLBACK;
// Where stage2 left the map. Must match E820_COUNT_ADDR / E820_MAP_ADDR in
// boot/stage2main.c, which are themselves relative to VBE_INFO_ADDR.
#define E820_COUNT_ADDR (0x30000 + 0x200)
#define E820_MAP_ADDR (0x30000 + 0x208)
#define E820_USABLE 1
#define E820_MAXENTRIES 32
struct e820entry {
uint64 base;
uint64 length;
uint32 type;
uint32 acpi_ext;
} __attribute__((packed));
void
e820_init(void)
{
uint32 count = *(uint32 *)P2V(E820_COUNT_ADDR);
struct e820entry *e = (struct e820entry *)P2V(E820_MAP_ADDR);
// A count of zero means the firmware declined; anything absurd means we are
// reading something that is not a memory map. Either way the constant is the
// only honest answer left.
if (count == 0 || count > E820_MAXENTRIES) {
cprintf("e820: no usable map (count %d), assuming %d MB\n",
count, (int)(PHYSTOP_FALLBACK / (1024 * 1024)));
return;
}
// The end of the usable region that *contains the kernel*, not the highest
// usable address anywhere.
//
// This distinction matters and is easy to get wrong. A memory map is not one
// run of RAM: there are reserved holes in it -- ACPI tables, firmware
// scratch, the legacy hole below 1 MB -- and kalloc's freerange() walks
// straight from the end of the kernel image to phystop handing out every page
// between. Taking the global maximum would step over a reserved region and
// put firmware tables on the free list, which fails later, elsewhere, and
// looks like anything but a memory map problem.
uint64 top = 0;
for (uint32 i = 0; i < count; i++) {
if (e[i].type != E820_USABLE)
continue;
uint64 base = e[i].base, end = base + e[i].length;
if (base <= EXTMEM && EXTMEM < end) {
top = end;
break;
}
}
if (top == 0) {
cprintf("e820: no usable region covers 0x%x, assuming %d MB\n",
EXTMEM, (int)(PHYSTOP_FALLBACK / (1024 * 1024)));
return;
}
// kvmalloc() maps physical 0-4 GB with four 1 GB pages, and marks the top one
// uncached because that is where PCI BARs and the framebuffer live. So 3 GB
// is the ceiling for anything the kernel will treat as RAM: above it the
// direct map either does not exist or is device memory that would be
// catastrophically slow to allocate from and is not ours to hand out.
if (top > PHYSTOP_MAX)
top = PHYSTOP_MAX;
top &= ~(uint64)(PGSIZE - 1);
// Below this the kernel image, its page tables and the DMA arena do not fit,
// and continuing produces a failure much harder to read than this line.
if (top < PHYSTOP_MIN) {
cprintf("e820: only %d MB usable; Schunix needs at least %d MB\n",
(int)(top / (1024 * 1024)), (int)(PHYSTOP_MIN / (1024 * 1024)));
// Left at the fallback deliberately rather than lowered: a machine this
// small is going to fail, and it should fail having said why.
return;
}
phystop = top;
cprintf("e820: %d MB usable (%d entries)\n",
(int)(phystop / (1024 * 1024)), count);
}

174
kernel/arch/x86_64/entry.S Normal file
View file

@ -0,0 +1,174 @@
/* entry.S
*
* Copyright (c) 2013 Brian Swetland
* Modified by Nexoniarz
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
#define mboot_magic 0x1badb002
#define mboot_flags 0x00010000
#include "mmu.h"
.code64
.global mboot_header
.global mboot_entry
mboot_header:
.long mboot_magic
.long mboot_flags
.long (-mboot_magic -mboot_flags)
.long mboot_load_addr
.long mboot_load_addr
.long mboot_load_end
.long mboot_bss_end
.long mboot_entry_addr
.code32
mboot_entry:
mov %eax, %ebp
# bootstrap page tables (0x1000 - 0x3000)
xor %eax, %eax
mov $0x1000, %edi
mov $0x2000, %ecx
rep stosb
# Bootstrap page tables (Identity mapping 4GB)
# PML4T[0] i PML4T[256] -> PDPT pod 0x2000
mov $(0x2000 | PTE_P | PTE_W), %edx
mov %edx, 0x1000
mov %edx, 0x1800
# PDPT mapping 4GB using 1GB huge pages
mov $(0x00000000 | PTE_P | PTE_PS | PTE_W), %edx
mov %edx, 0x2000
mov $(0x40000000 | PTE_P | PTE_PS | PTE_W), %edx
mov %edx, 0x2008
mov $(0x80000000 | PTE_P | PTE_PS | PTE_W), %edx
mov %edx, 0x2010
mov $(0xC0000000 | PTE_P | PTE_PS | PTE_W), %edx
mov %edx, 0x2018
mov %ebp, %ebx
.global entry32mp
entry32mp:
mov $0x1000, %eax
mov %eax, %cr3
lgdt (gdtr64 - mboot_header + mboot_load_addr)
mov %cr4, %eax
bts $5, %eax
mov %eax, %cr4
mov $MSR_EFER, %ecx
rdmsr
bts $0, %eax
bts $8, %eax
wrmsr
mov %cr0, %eax
orl $(CR0_PG | CR0_WP | CR0_MP), %eax
mov %eax, %cr0
ljmp $8, $(entry64low - mboot_header + mboot_load_addr)
.align 16
gdtr64:
.word gdt64_end - gdt64_begin - 1;
.quad gdt64_begin - mboot_header + mboot_load_addr
.align 16
gdt64_begin:
.long 0x00000000
.long 0x00000000
.long 0x00000000 # 64-bit code
.long 0x00209800
.long 0x00000000 # 64-bit data
.long 0x00009000
gdt64_end:
.align 16
.code64
entry64low:
movabs $entry64high, %rax
jmp *%rax
.global _start
_start:
entry64high:
xor %rax, %rax
mov %ax, %ss
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
# Is this an application processor, or the boot processor?
#
# This check used to be a comment saying it had not been written yet, and
# every AP fell straight through into the BSP path below: it set RSP to the
# *boot processor's* stack, passed its own flag to kmain() as a framebuffer
# address, and re-ran the entire bootstrap -- e820_init, kinit1, kvmalloc --
# on a second core sharing one stack with the first. That is the SMP triple
# fault. QEMU hid it for years by never reporting more than one CPU (see
# kernel/arch/x86_64/acpi.c); VMware did not.
#
# EBX is the discriminator, and entryother.S sets it to 1 immediately before
# entering long mode for exactly this purpose. It is safe: on the boot
# processor EBX carries the VESA framebuffer address from the loader, which is
# either zero or a physical MMIO address, and can never be 1.
cmp $1, %ebx
je entry64mp
# Setup initial stack
movabs $0xFFFF800000010000, %rax
mov %rax, %rsp
# Pass VESA address to main (1st argument in RDI)
mov %rbx, %rdi
call kmain
.global __deadloop
__deadloop:
jmp .
entry64mp:
mov $0x7000, %rax
mov -16(%rax), %rsp
jmp mpenter
.global wrmsr
wrmsr:
mov %rdi, %rcx
mov %rsi, %rax
shr $32, %rsi
mov %rsi, %rdx
wrmsr
retq
.global ignore_sysret
ignore_sysret:
mov $-38, %rax
sysretq

View file

@ -0,0 +1,78 @@
#include "asm.h"
#include "memlayout.h"
#include "mmu.h"
# Each non-boot CPU ("AP") is started up in response to a STARTUP
# IPI from the boot CPU. Section B.4.2 of the Multi-Processor
# Specification says that the AP will start in real mode with CS:IP
# set to XY00:0000, where XY is an 8-bit value sent with the
# STARTUP. Thus this code must start at a 4096-byte boundary.
#
# Because this code sets DS to zero, it must sit
# at an address in the low 2^16 bytes.
#
# Startothers (in main.c) sends the STARTUPs one at a time.
# It copies this code (start) at 0x7000. It puts the address of
# a newly allocated per-core stack in start-4,the address of the
# place to jump to (mpenter) in start-8, and the physical address
# of entrypgdir in start-12.
#
# This code is identical to bootasm.S except:
# - it does not need to enable A20
# - it uses the address at start-4, start-8, and start-12
.code16
.global start
start:
cli
xorw %ax,%ax
movw %ax,%ds
movw %ax,%es
movw %ax,%ss
lgdt gdtdesc
movl %cr0, %eax
orl $CR0_PE, %eax
movl %eax, %cr0
ljmpl $(SEG_KCODE<<3), $(start32)
//PAGEBREAK!
.code32
start32:
movw $(SEG_KDATA<<3), %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
movw $0, %ax
movw %ax, %fs
movw %ax, %gs
# defer paging until we switch to 64bit mode
# set ebx=1 so shared boot code knows we're booting a secondary core
mov $1, %ebx
# Switch to the stack allocated by startothers()
movl (start-4), %esp
# Call mpenter()
call *(start-8)
movw $0x8a00, %ax
movw %ax, %dx
outw %ax, %dx
movw $0x8ae0, %ax
outw %ax, %dx
spin:
jmp spin
.p2align 2
gdt:
SEG_NULLASM
SEG_ASM(STA_X|STA_R, 0, 0xffffffff)
SEG_ASM(STA_W, 0, 0xffffffff)
gdtdesc:
.word (gdtdesc - gdt - 1)
.long gdt

102
kernel/arch/x86_64/ioapic.c Normal file
View file

@ -0,0 +1,102 @@
// The I/O APIC manages hardware interrupts for an SMP system.
// http://www.intel.com/design/chipsets/datashts/29056601.pdf
// See also picirq.c.
#include "types.h"
#include "defs.h"
#include "traps.h"
#include "memlayout.h"
#define IOAPIC 0xFEC00000 // Default physical address of IO APIC
#define REG_ID 0x00 // Register index: ID
#define REG_VER 0x01 // Register index: version
#define REG_TABLE 0x10 // Redirection table base
// The redirection table starts at REG_TABLE and uses
// two registers to configure each interrupt.
// The first (low) register in a pair contains configuration bits.
// The second (high) register contains a bitmask telling which
// CPUs can serve that interrupt.
#define INT_DISABLED 0x00010000 // Interrupt disabled
#define INT_LEVEL 0x00008000 // Level-triggered (vs edge-)
#define INT_ACTIVELOW 0x00002000 // Active low (vs high)
#define INT_LOGICAL 0x00000800 // Destination is CPU id (vs APIC ID)
volatile struct ioapic *ioapic;
// IO APIC MMIO structure: write reg, then read or write data.
struct ioapic {
uint reg;
uint pad[3];
uint data;
};
static uint
ioapicread(int reg)
{
ioapic->reg = reg;
return ioapic->data;
}
static void
ioapicwrite(int reg, uint data)
{
ioapic->reg = reg;
ioapic->data = data;
}
void
ioapicinit(void)
{
int i, id, maxintr;
ioapic = P2V((volatile struct ioapic*)IOAPIC);
maxintr = (ioapicread(REG_VER) >> 16) & 0xFF;
id = ioapicread(REG_ID) >> 24;
if(id != ioapicid)
cprintf("ioapicinit: id isn't equal to ioapicid; not a MP\n");
// Mark all interrupts edge-triggered, active high, disabled,
// and not routed to any CPUs.
for(i = 0; i <= maxintr; i++){
ioapicwrite(REG_TABLE+2*i, INT_DISABLED | (T_IRQ0 + i));
ioapicwrite(REG_TABLE+2*i+1, 0);
}
}
// Route an interrupt to a CPU and unmask it.
//
// `level_low` picks PCI's signalling instead of ISA's. This distinction did
// not matter while every interrupt in the kernel came from an ISA-era device
// at a fixed pin, all of which are edge-triggered and active high -- which is
// why the original ioapicenable() hardcoded exactly that. A PCI device wired
// through an INTx pin is the opposite on both counts, and configuring it as
// edge/high does not fail cleanly: the device raises the line, we take one
// interrupt, and because the level never goes away there is no second edge
// and the device is never heard from again.
void
ioapicroute(int irq, int cpunum, int level_low)
{
uint lo = T_IRQ0 + irq;
if(level_low)
lo |= INT_LEVEL | INT_ACTIVELOW;
// Program the destination before unmasking, so the entry is never briefly
// live pointing at CPU 0 when it was meant for another.
ioapicwrite(REG_TABLE+2*irq+1, cpunum << 24);
ioapicwrite(REG_TABLE+2*irq, lo);
}
void
ioapicmask(int irq)
{
ioapicwrite(REG_TABLE+2*irq, INT_DISABLED | (T_IRQ0 + irq));
}
void
ioapicenable(int irq, int cpunum)
{
ioapicroute(irq, cpunum, 0);
}

245
kernel/arch/x86_64/lapic.c Normal file
View file

@ -0,0 +1,245 @@
// The local APIC manages internal (non-I/O) interrupts.
// See Chapter 8 & Appendix C of Intel processor manual volume 3.
#include "param.h"
#include "types.h"
#include "defs.h"
#include "date.h"
#include "memlayout.h"
#include "traps.h"
#include "mmu.h"
#include "x86.h"
#include "proc.h" // ncpu
// Local APIC registers, divided by 4 for use as uint[] indices.
#define ID (0x0020/4) // ID
#define VER (0x0030/4) // Version
#define TPR (0x0080/4) // Task Priority
#define EOI (0x00B0/4) // EOI
#define SVR (0x00F0/4) // Spurious Interrupt Vector
#define ENABLE 0x00000100 // Unit Enable
#define ESR (0x0280/4) // Error Status
#define ICRLO (0x0300/4) // Interrupt Command
#define SINIT 0x00000500 // INIT/RESET
#define STARTUP 0x00000600 // Startup IPI
#define DELIVS 0x00001000 // Delivery status
#define ASSERT 0x00004000 // Assert interrupt (vs deassert)
#define LEVEL 0x00008000 // Level triggered
#define BCAST 0x00080000 // Send to all APICs, including self.
#define BUSY 0x00001000
#define ICRHI (0x0310/4) // Interrupt Command [63:32]
#define TIMER (0x0320/4) // Local Vector Table 0 (TIMER)
#define X1 0x0000000B // divide counts by 1
#define PERIODIC 0x00020000 // Periodic
#define PCINT (0x0340/4) // Performance Counter LVT
#define LINT0 (0x0350/4) // Local Vector Table 1 (LINT0)
#define LINT1 (0x0360/4) // Local Vector Table 2 (LINT1)
#define ERROR (0x0370/4) // Local Vector Table 3 (ERROR)
#define MASKED 0x00010000 // Interrupt masked
#define TICR (0x0380/4) // Timer Initial Count
#define TCCR (0x0390/4) // Timer Current Count
#define TDCR (0x03E0/4) // Timer Divide Configuration
volatile uint *lapic; // Initialized in mp.c
static void
lapicw(int index, int value)
{
lapic[index] = value;
lapic[ID]; // wait for write to finish, by reading
}
void
lapicinit(void)
{
if(!lapic)
return;
// Enable local APIC; set spurious interrupt vector.
lapicw(SVR, ENABLE | (T_IRQ0 + IRQ_SPURIOUS));
// The timer repeatedly counts down at bus frequency
// from lapic[TICR] and then issues an interrupt.
// If xv6 cared more about precise timekeeping,
// TICR would be calibrated using an external time source.
lapicw(TDCR, X1);
lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));
lapicw(TICR, 10000000);
// Disable logical interrupt lines.
lapicw(LINT0, MASKED);
lapicw(LINT1, MASKED);
// Disable performance counter overflow interrupts
// on machines that provide that interrupt entry.
if(((lapic[VER]>>16) & 0xFF) >= 4)
lapicw(PCINT, MASKED);
// Map error interrupt to IRQ_ERROR.
lapicw(ERROR, T_IRQ0 + IRQ_ERROR);
// Clear error status register (requires back-to-back writes).
lapicw(ESR, 0);
lapicw(ESR, 0);
// Ack any outstanding interrupts.
lapicw(EOI, 0);
// Send an Init Level De-Assert to synchronise arbitration ID's.
lapicw(ICRHI, 0);
lapicw(ICRLO, BCAST | SINIT | LEVEL);
while(lapic[ICRLO] & DELIVS)
;
// Enable interrupts on the APIC (but not on the processor).
lapicw(TPR, 0);
}
int
cpunum(void)
{
int apicid, i;
// Cannot call cpu when interrupts are enabled:
// result not guaranteed to last long enough to be used!
// Would prefer to panic but even printing is chancy here:
// almost everything, including cprintf and panic, calls cpu,
// often indirectly through acquire and release.
if(readeflags()&FL_IF){
static int n;
if(n++ == 0)
cprintf("cpu called from %x with interrupts enabled\n",
__builtin_return_address(0));
}
if (!lapic)
return 0;
apicid = lapic[ID] >> 24;
for (i = 0; i < ncpu; ++i) {
if (cpus[i].apicid == apicid)
return i;
}
panic("unknown apicid\n");
}
// Acknowledge interrupt.
void
lapiceoi(void)
{
if(lapic)
lapicw(EOI, 0);
}
// Spin for a given number of microseconds.
// On real hardware would want to tune this dynamically.
void
microdelay(int us)
{
}
#define CMOS_PORT 0x70
#define CMOS_RETURN 0x71
// Start additional processor running entry code at addr.
// See Appendix B of MultiProcessor Specification.
void
lapicstartap(uchar apicid, uint addr)
{
int i;
ushort *wrv;
// "The BSP must initialize CMOS shutdown code to 0AH
// and the warm reset vector (DWORD based at 40:67) to point at
// the AP startup code prior to the [universal startup algorithm]."
outb(CMOS_PORT, 0xF); // offset 0xF is shutdown code
outb(CMOS_PORT+1, 0x0A);
wrv = (ushort*)P2V((0x40<<4 | 0x67)); // Warm reset vector
wrv[0] = 0;
wrv[1] = addr >> 4;
// "Universal startup algorithm."
// Send INIT (level-triggered) interrupt to reset other CPU.
lapicw(ICRHI, apicid<<24);
lapicw(ICRLO, SINIT | LEVEL | ASSERT);
microdelay(200);
lapicw(ICRLO, SINIT | LEVEL);
microdelay(100); // should be 10ms, but too slow in Bochs!
// Send startup IPI (twice!) to enter code.
// Regular hardware is supposed to only accept a STARTUP
// when it is in the halted state due to an INIT. So the second
// should be ignored, but it is part of the official Intel algorithm.
// Bochs complains about the second one. Too bad for Bochs.
for(i = 0; i < 2; i++){
lapicw(ICRHI, apicid<<24);
lapicw(ICRLO, STARTUP | (addr>>12));
microdelay(200);
}
}
#define CMOS_STATA 0x0a
#define CMOS_STATB 0x0b
#define CMOS_UIP (1 << 7) // RTC update in progress
#define SECS 0x00
#define MINS 0x02
#define HOURS 0x04
#define DAY 0x07
#define MONTH 0x08
#define YEAR 0x09
static uint cmos_read(uint reg)
{
outb(CMOS_PORT, reg);
microdelay(200);
return inb(CMOS_RETURN);
}
static void fill_rtcdate(struct rtcdate *r)
{
r->second = cmos_read(SECS);
r->minute = cmos_read(MINS);
r->hour = cmos_read(HOURS);
r->day = cmos_read(DAY);
r->month = cmos_read(MONTH);
r->year = cmos_read(YEAR);
}
//PAGEBREAK!
// qemu seems to use 24-hour GWT and the values are BCD encoded
void cmostime(struct rtcdate *r)
{
struct rtcdate t1, t2;
int sb, bcd;
sb = cmos_read(CMOS_STATB);
bcd = (sb & (1 << 2)) == 0;
// make sure CMOS doesn't modify time while we read it
for(;;) {
fill_rtcdate(&t1);
if(cmos_read(CMOS_STATA) & CMOS_UIP)
continue;
fill_rtcdate(&t2);
if(memcmp(&t1, &t2, sizeof(t1)) == 0)
break;
}
// convert
if(bcd) {
#define CONV(x) (t1.x = ((t1.x >> 4) * 10) + (t1.x & 0xf))
CONV(second);
CONV(minute);
CONV(hour );
CONV(day );
CONV(month );
CONV(year );
#undef CONV
}
*r = t1;
r->year += 2000;
}

159
kernel/arch/x86_64/mp.c Normal file
View file

@ -0,0 +1,159 @@
// Multiprocessor support
// Search memory for MP description structures.
// http://developer.intel.com/design/pentium/datashts/24201606.pdf
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mp.h"
#include "x86.h"
#include "mmu.h"
#include "proc.h"
struct cpu cpus[NCPU];
// Processors the firmware says exist.
int ncpu;
// Processors actually running, which is not the same number and must not be
// confused with it. Enumeration is cheap and startup is where the difficulty
// is: right now the APs are found and deliberately not started, so this stays
// at 1 while ncpu is 2, 4 or 16.
//
// /proc/cpuinfo reports *this* one, matching Linux, where cpuinfo lists online
// CPUs. Reporting ncpu there told a VM with four processors that it had four
// working ones, which was a comfortable thing to read and false.
int ncpu_online = 1;
uchar ioapicid;
static uchar
sum(uchar *addr, int len)
{
int i, sum;
sum = 0;
for(i=0; i<len; i++)
sum += addr[i];
return sum;
}
// Look for an MP structure in the len bytes at addr.
static struct mp*
mpsearch1(addr_t a, int len)
{
uchar *e, *p, *addr;
addr = P2V(a);
e = addr+len;
for(p = addr; p < e; p += sizeof(struct mp))
if(memcmp(p, "_MP_", 4) == 0 && sum(p, sizeof(struct mp)) == 0)
return (struct mp*)p;
return 0;
}
// Search for the MP Floating Pointer Structure, which according to the
// spec is in one of the following three locations:
// 1) in the first KB of the EBDA;
// 2) in the last KB of system base memory;
// 3) in the BIOS ROM between 0xE0000 and 0xFFFFF.
static struct mp*
mpsearch(void)
{
uchar *bda;
uint p;
struct mp *mp;
bda = (uchar *) P2V(0x400);
if((p = ((bda[0x0F]<<8)| bda[0x0E]) << 4)){
if((mp = mpsearch1(p, 1024)))
return mp;
} else {
p = ((bda[0x14]<<8)|bda[0x13])*1024;
if((mp = mpsearch1(p-1024, 1024)))
return mp;
}
return mpsearch1(0xF0000, 0x10000);
}
// Search for an MP configuration table. For now,
// don't accept the default configurations (physaddr == 0).
// Check for correct signature, calculate the checksum and,
// if correct, check the version.
// To do: check extended table checksum.
static struct mpconf*
mpconfig(struct mp **pmp)
{
struct mpconf *conf;
struct mp *mp;
if((mp = mpsearch()) == 0 || mp->physaddr == 0)
return 0;
conf = (struct mpconf*) P2V((addr_t) mp->physaddr);
if(memcmp(conf, "PCMP", 4) != 0)
return 0;
if(conf->version != 1 && conf->version != 4)
return 0;
if(sum((uchar*)conf, conf->length) != 0)
return 0;
*pmp = mp;
return conf;
}
void
mpinit(void)
{
uchar *p, *e;
struct mp *mp;
struct mpconf *conf;
struct mpproc *mproc;
struct mpioapic *ioapic;
if((conf = mpconfig(&mp)) == 0) {
cprintf("No other CPUs found.\n");
return;
}
lapic = (uint*)P2V(conf->lapicaddr_p);
for(p=(uchar*)(conf+1), e=(uchar*)conf+conf->length; p<e; ){
switch(*p){
case MPPROC:
mproc = (struct mpproc*)p;
if(ncpu < NCPU) {
cpus[ncpu].apicid = mproc->apicid; // apicid may differ from ncpu
ncpu++;
}
p += sizeof(struct mpproc);
continue;
case MPIOAPIC:
ioapic = (struct mpioapic*)p;
ioapicid = ioapic->apicno;
p += sizeof(struct mpioapic);
continue;
case MPBUS:
case MPIOINTR:
case MPLINTR:
p += 8;
continue;
default:
panic("Major problem parsing mp config.");
break;
}
}
// "Seems we are" is doing a lot of work here, and the number is usually
// wrong. This walks the *legacy Intel MP table* (1994), which modern firmware
// fills in minimally or not at all -- QEMU/SeaBIOS emits exactly one
// processor entry regardless of -smp, because it expects the OS to enumerate
// CPUs from the ACPI MADT instead. Verified 2026-08-08: with -smp 4 the table
// is 208 bytes and contains one type-0 entry, then buses, an I/O APIC and
// interrupt assignments. So ncpu is 1 on QEMU no matter what, and SMP is
// silently never exercised there.
//
// See docs/roadmap.md "Known broken" -- this is half of the SMP bug, and the
// reason the other half (AP startup triple-faulting) cannot be reproduced
// under QEMU until the MADT is parsed.
cprintf("Seems we are SMP, ncpu = %d\n",ncpu);
if(mp->imcrp){
// Bochs doesn't support IMCR, so this doesn't run on Bochs.
// But it would on real hardware.
outb(0x22, 0x70); // Select IMCR
outb(0x23, inb(0x23) | 1); // Mask external interrupts.
}
}

View file

@ -0,0 +1,30 @@
# Context switch
#
# void swtch(struct context **old, struct context *new);
#
# Save current register context in old
# and then load register context from new.
.global swtch
swtch:
# Save old callee-save registers
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
# Switch stacks
movq %rsp, (%rdi)
movq %rsi, %rsp
# Load new callee-save registers
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
retq #??

159
kernel/arch/x86_64/trap.c Normal file
View file

@ -0,0 +1,159 @@
#include "types.h"
#include "defs.h"
#include "irq.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
#include "traps.h"
#include "spinlock.h"
#include "posix.h"
uint *idt;
extern addr_t vectors[];
struct spinlock tickslock;
uint ticks;
static void
mkgate(uint *idt, uint n, addr_t kva, uint pl)
{
uint64 addr = (uint64) kva;
n *= 4;
idt[n+0] = (addr & 0xFFFF) | (KERNEL_CS << 16);
idt[n+1] = (addr & 0xFFFF0000) | 0x8E00 | ((pl & 3) << 13);
idt[n+2] = addr >> 32;
idt[n+3] = 0;
}
void idtinit(void)
{
struct {
ushort limit;
uint64 base;
} __attribute__((packed)) idtdesc;
idtdesc.limit = PGSIZE - 1;
idtdesc.base = (uint64)idt;
asm volatile("lidt %0" : : "m" (idtdesc));
}
void tvinit(void)
{
int n;
idt = (uint*) kalloc();
memset(idt, 0, PGSIZE);
for (n = 0; n < 256; n++)
mkgate(idt, n, vectors[n], 0);
mkgate(idt, T_SYSCALL, vectors[T_SYSCALL], 3);
}
uint
uptime(void)
{
uint xticks;
acquire(&tickslock);
xticks = ticks;
release(&tickslock);
return xticks;
}
// Which signal a CPU fault corresponds to, so a process killed by a fault is
// reported to wait() as died-by-signal with a sensible cause rather than a
// blanket SIGKILL.
static int
trapsig(uint64 trapno)
{
switch(trapno){
case T_DIVIDE: return SIGFPE;
case T_ILLOP: return SIGILL;
case T_GPFLT: return SIGSEGV;
case T_PGFLT: return SIGSEGV;
case T_BRKPT: return SIGTRAP;
case T_DEBUG: return SIGTRAP;
default: return SIGSEGV;
}
}
void
trap(struct trapframe *tf)
{
kstack_check_canary("trap entry");
switch(tf->trapno){
case T_IRQ0 + IRQ_TIMER:
if(cpunum() == 0){
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
lapiceoi();
break;
case T_IRQ0 + IRQ_SPURIOUS:
cprintf("cpu%d: spurious interrupt at %p:%p\n",
cpunum(), tf->cs, tf->rip);
lapiceoi();
break;
default:
// Every other IOAPIC pin goes through the registration table. The device
// interrupts used to be listed here one case at a time, which cannot work
// for anything found on the PCI bus: a driver does not know its line
// until it reads the device's config space at probe time, so there is no
// constant to write a case label for. See include/irq.h.
if(tf->trapno >= T_IRQ0 && tf->trapno < T_IRQ0 + NIRQ){
irq_dispatch(tf->trapno - T_IRQ0);
lapiceoi();
break;
}
if(myproc() == 0 || (tf->cs&3) == 0){
cprintf("unexpected trap %d from cpu %d rip %p (cr2=0x%p) err=0x%p\n",
tf->trapno, cpunum(), tf->rip, rcr2(), tf->err);
if (myproc())
cprintf("proc id: %d\n", myproc()->pid);
if (tf->trapno == T_GPFLT) {
uint64 *iretframe = (uint64*)tf->rsp;
cprintf("iretq frame @ %p: rip=%p cs=%p rflags=%p rsp=%p ss=%p\n",
iretframe, iretframe[0], iretframe[1], iretframe[2],
iretframe[3], iretframe[4]);
if (myproc())
cprintf("this tf=%p myproc()->tf=%p myproc()->kstack=%p (tf-kstack=%p)\n",
tf, myproc()->tf, myproc()->kstack, (uint64)tf - (uint64)myproc()->kstack);
}
panic("trap");
}
cprintf("pid %d %s: trap %d err %d on cpu %d "
"rip 0x%p addr 0x%p--kill proc\n",
myproc()->pid, myproc()->name, tf->trapno, tf->err, cpunum(), tf->rip,
rcr2());
// General-purpose register dump. This used to be followed by ~25 lines
// that walked musl's mallocng group/meta structures out of rax/rdi and
// dumped the user stack -- scaffolding for the boot-crash investigation
// recorded in docs/architecture/kernel-history.md section 1. That
// investigation is closed and the underlying kernel bugs are fixed, so
// the allocator-specific part is gone; the registers stay, since they are
// useful for any fault.
if (tf->trapno == T_GPFLT) {
cprintf(" rax=%p rbx=%p rcx=%p rdx=%p rsi=%p rdi=%p rbp=%p rsp=%p\n",
tf->rax, tf->rbx, tf->rcx, tf->rdx, tf->rsi, tf->rdi, tf->rbp, tf->rsp);
}
myproc()->killed = 1;
if(myproc()->killsig == 0)
myproc()->killsig = trapsig(tf->trapno);
}
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
if(myproc() && myproc()->state == RUNNING && tf->trapno == T_IRQ0+IRQ_TIMER)
yield();
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
kstack_check_canary("trap exit");
}

View file

@ -0,0 +1,257 @@
# trapasm.S — kernel/user entry/exit with FS_BASE swapping for TLS
#
# The kernel uses __thread struct cpu *cpu and __thread struct proc *proc,
# accessed via %fs (IA32_FS_BASE MSR, 0xC0000100). seginit() sets this to
# kernel_fs_base. User-space TLS (musl) uses the same MSR via arch_prctl
# ARCH_SET_FS (saved in proc->fs_base).
#
# Rule: on ENTRY from user (syscall or interrupt), restore kernel FS_BASE.
# on EXIT to user (sysretq or iretq), write proc->fs_base to MSR.
#
# proc->fs_base is at byte offset 0x18 (24) inside struct proc.
# proc pointer is at %gs:24 (PERCPU_PROC in include/proc.h); cpu is at %gs:16.
# -----------------------------------------------------------------------
# Helper macro: write kernel_fs_base to IA32_FS_BASE.
# Clobbers %rax, %rcx, %rdx. Caller must save them if needed.
# -----------------------------------------------------------------------
# Read from %gs, not from a global.
#
# This used to be `movq kernel_fs_base(%rip), %rax`, and kernel_fs_base was a
# single global that every CPU wrote during seginit(). With one processor that
# is merely untidy. With two it is fatal: whichever CPU ran seginit() last wins,
# and from the first trap onward *every* CPU restores that one CPU's per-CPU
# block -- so `cpu` and `proc` are shared, pushcli/popcli nesting counts collide,
# and the kernel dies in panic("sched locks") shortly after the APs start.
#
# GS base is set per CPU by seginit() to the same value as FS base, and %gs:0
# holds the block's self-pointer. So this reads *this* CPU's kernel FS base with
# no global involved.
#
# No swapgs, deliberately. The usual reason for it is that userspace owns GS;
# here it does not -- sys_arch_prctl() implements only ARCH_SET_FS/ARCH_GET_FS
# and rejects everything else, musl uses FS for TLS on x86-64, and the only
# other writes to %gs are selector loads in entry.S and entryother.S that happen
# before seginit(). GS is the kernel's alone, so it can simply stay pointing at
# the per-CPU block. If ARCH_SET_GS is ever implemented, this must become a
# proper swapgs pair.
.macro RESTORE_KERNEL_FS
movq %gs:0, %rax
movq %rax, %rdx
shrq $32, %rdx
movl $0xC0000100, %ecx
wrmsr
.endm
# -----------------------------------------------------------------------
# Helper macro: write proc->fs_base to IA32_FS_BASE.
# Requires %fs to still hold the kernel base.
# Clobbers %rax, %rcx, %rdx. Caller must save them if needed.
# -----------------------------------------------------------------------
.macro WRITE_USER_FS
# proc, from the per-CPU block at %gs:24 (PERCPU_PROC in include/proc.h).
# This used to be %fs:(-8), the __thread slot, which no longer exists --
# see the comment in include/proc.h for why those stopped being __thread.
movq %gs:24, %rax # proc pointer
movq 0x18(%rax), %rax # proc->fs_base (offset 24 = 0x18)
movq %rax, %rdx
shrq $32, %rdx
movl $0xC0000100, %ecx
wrmsr
.endm
# -----------------------------------------------------------------------
# alltraps — IDT interrupt/fault entry
# Stack on entry (from user mode, CPU has done privilege switch):
# [rsp+0] trapno (pushed by vectors.S)
# [rsp+8] errorcode
# [rsp+16] rip
# [rsp+24] cs ← bit 1:0 == 3 means user mode
# [rsp+32] rflags
# [rsp+40] user_rsp
# [rsp+48] user_ss
# -----------------------------------------------------------------------
.global alltraps
alltraps:
# Always restore kernel FS_BASE before touching %fs:, even when the
# saved cs says we were already in kernel mode. A fault/interrupt can
# land in that narrow kernel-mode window after syscall_trapret/trapret
# have already done WRITE_USER_FS (cs is still 0x8 there, but FS_BASE
# is the *user* value) — the old cs-based skip trusted that combination
# never happens and read %fs:(-8) as garbage (proc/cpu == NULL) in that
# case. RESTORE_KERNEL_FS is just a wrmsr of an already-known value when
# we truly were in the kernel, so doing it unconditionally is cheap and
# always correct.
pushq %rax
pushq %rcx
pushq %rdx
RESTORE_KERNEL_FS
popq %rdx
popq %rcx
popq %rax
# Build trap frame.
pushq %r15
pushq %r14
pushq %r13
pushq %r12
pushq %r11
pushq %r10
pushq %r9
pushq %r8
pushq %rdi
pushq %rsi
pushq %rbp
pushq %rdx
pushq %rcx
pushq %rbx
pushq %rax
movq %rsp, %rdi # frame in arg1
callq trap
# Return falls through to trapret...
.global trapret
trapret:
popq %rax
popq %rbx
popq %rcx
popq %rdx
popq %rbp
popq %rsi
popq %rdi
popq %r8
popq %r9
popq %r10
popq %r11
popq %r12
popq %r13
popq %r14
popq %r15
addq $16, %rsp # discard trapno and errorcode
# If returning to user mode, write proc->fs_base to FS_BASE MSR.
# Stack now: [rsp+0]=rip, [rsp+8]=cs, [rsp+16]=rflags, [rsp+24]=rsp, [rsp+32]=ss
testb $3, 8(%rsp)
jz .Ltrapret_kret
# cli before WRITE_USER_FS: close the same race as in syscall_trapret.
# iretq will atomically restore RFLAGS (IF=1) when switching to user mode.
pushq %rax
pushq %rcx
pushq %rdx
cli
WRITE_USER_FS
popq %rdx
popq %rcx
popq %rax
.Ltrapret_kret:
iretq
# -----------------------------------------------------------------------
# syscall_entry — SYSCALL instruction entry point
# On entry: rcx=user rip, r11=user rflags, rsp=user stack, IF cleared.
# FS_BASE may be the user's TLS value (set at last sysretq).
# -----------------------------------------------------------------------
.global syscall_entry
syscall_entry:
# Swap FS_BASE to kernel value BEFORE any %fs: access.
# Save rax/rcx/rdx to single-CPU scratch globals (IF is off, no race).
movq %rax, _sc_rax(%rip)
movq %rcx, _sc_rcx(%rip) # user rip (SYSCALL stores it here)
movq %rdx, _sc_rdx(%rip)
RESTORE_KERNEL_FS # clobbers rax, rcx, rdx — that's OK
movq _sc_rax(%rip), %rax
movq _sc_rcx(%rip), %rcx
movq _sc_rdx(%rip), %rdx
# Switch to kernel stack: store user rsp at top of proc->kstack.
#
# The scratch slot is %fs:(8), NOT %fs:(0). %fs:(0) is the TCB
# self-pointer -- the x86-64 TLS ABI reserves the word at the thread
# pointer to hold the thread pointer itself, and a compiler is entitled to
# read it to materialise the TP before applying a variable's offset.
#
# It used to be %fs:(0), and that survived for years only because GCC
# compiles `proc`/`cpu` into direct `%fs:-8` segment references and never
# touches the word. clang at -O0 does `movq %fs:0, %rax; addq $-8, %rax;
# (%rax)` instead -- so it read the syscall number this line had just
# stored (7, SYS_exec, from initcode), computed 7-8 = -1, and page-faulted
# on the very first syscall of the very first process.
#
# Positive offsets are safe: the TLS block occupies negative offsets
# (-16 = cpu, -8 = proc) and seginit puts the thread pointer 2048 bytes
# into a 4096-byte page, so %fs:(8) onward is 2040 bytes we own outright.
movq %rax, %fs:(8) # save rax in per-CPU scratch slot
movq %gs:24, %rax # proc pointer (PERCPU_PROC)
movq 0x10(%rax), %rax # proc->kstack (offset 16)
addq $(4096-16), %rax # point to tf->rsp slot
movq %rsp, (%rax) # save user rsp
movq %rax, %rsp # switch to kernel stack
movq %fs:(8), %rax # restore rax
pushq %r11 # rflags
pushq $0 # cs (ignored)
pushq %rcx # rip (next user instruction)
pushq $0 # err
pushq $0 # trapno (ignored)
pushq %r15
pushq %r14
pushq %r13
pushq %r12
pushq %r11
pushq %r10
pushq %r9
pushq %r8
pushq %rdi
pushq %rsi
pushq %rbp
pushq %rdx
pushq %rcx
pushq %rbx
pushq %rax
movq %rsp, %rdi # frame in arg1
callq syscall
# Return falls through to syscall_trapret...
# -----------------------------------------------------------------------
# syscall_trapret — return path for SYSCALL-based syscalls
# -----------------------------------------------------------------------
.global syscall_trapret
syscall_trapret:
# Disable interrupts BEFORE writing user FS_BASE to the MSR.
# Without cli here, a timer IRQ between WRITE_USER_FS and the cli below
# would arrive in ring-0 with user FS_BASE set, causing %fs:(-8) to return
# garbage (proc=NULL) → panic in the interrupt handler.
# rax/rcx/rdx are clobbered by WRITE_USER_FS then restored by the pops.
cli
WRITE_USER_FS
popq %rax
popq %rbx
popq %rcx
popq %rdx
popq %rbp
popq %rsi
popq %rdi
popq %r8
popq %r9
popq %r10
popq %r11
popq %r12
popq %r13
popq %r14
popq %r15
addq $40, %rsp # discard trapno, errorcode, rip, cs, rflags
# Mask interrupts: sysretq is unsafe with IF set on non-canonical RSP.
# See https://www.felixcloutier.com/x86/sysret
cli
movq (%rsp), %rsp # restore the user stack
sysretq

1281
kernel/arch/x86_64/vectors.S Normal file

File diff suppressed because it is too large Load diff

631
kernel/arch/x86_64/vm.c Normal file
View file

@ -0,0 +1,631 @@
#include "param.h"
#include "types.h"
#include "defs.h"
#include "x86.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
int mappages(pml4e_t *pgdir, void *va, addr_t size, addr_t pa, int perm);
static pte_t * walkpgdir(pml4e_t *pml4, const void *va, int alloc);
// `cpu` and `proc` are no longer __thread variables; they live at fixed offsets
// in the per-CPU block and are read through %gs at each use. See the long
// comment in include/proc.h for why -- the short version is that __thread means
// per *thread*, the compiler caches the thread pointer across calls on that
// basis, and a kernel thread changes CPU at every sched().
// Kernel FS_BASE set by seginit(); read by trapasm.S to restore after user TLS
uint64 kernel_fs_base;
// Single-CPU scratch for syscall_entry FS swap (safe while IF cleared by SYSCALL/SFMASK)
uint64 _sc_rax, _sc_rcx, _sc_rdx;
static pml4e_t *kpml4;
static pdpe_t *kpdpt;
struct tassdesc {
uint lim_15_0 : 16;
uint base_15_0 : 16;
uint base_23_16 : 8;
uint type : 4;
uint s : 1;
uint dpl : 2;
uint p : 1;
uint lim_19_16 : 4;
uint avl : 1;
uint l : 1;
uint d : 1;
uint g : 1;
uint base_31_24 : 8;
uint32 base_63_32;
uint32 res;
};
void
syscallinit(void)
{
// SYSRETQ does NOT use USER_CS/USER_DS directly. Per the AMD64/Intel
// convention, it computes CS = STAR[63:48]+16 and SS = STAR[63:48]+8
// (RPL forced to 3), so STAR[63:48] must hold the *compat* 32-bit user
// code selector (SEG_UCODE32), relying on the GDT's consecutive
// [UCODE32, UDATA, UCODE] layout — NOT USER_CS itself. Using USER_CS
// here made every sysretq load CS=USER_CS+16 (an invalid selector,
// pointing past UCODE into the TSS descriptor) and SS=USER_CS+8. QEMU's
// TCG apparently doesn't validate segment descriptors on SYSRET, so this
// went unnoticed until an interrupt forced a strictly-validated iretq
// with that same bogus CS still loaded, which correctly #GP'd.
wrmsr(MSR_STAR,
((((uint64)((SEG_UCODE32 << 3) | DPL_USER) << 16) | (uint64)KERNEL_CS) << 32));
wrmsr(MSR_LSTAR, (addr_t)syscall_entry);
wrmsr(MSR_CSTAR, (addr_t)ignore_sysret);
wrmsr(MSR_SFMASK, FL_TF|FL_DF|FL_IF|FL_IOPL_MASK|FL_AC|FL_NT);
}
void
seginit(void)
{
struct segdesc *gdt;
uint *tss;
uint64 addr;
void *local;
struct cpu *c;
// The per-CPU page, preallocated by the boot processor for every AP.
//
// It must not be kalloc()'d here on an application processor. kalloc takes a
// spinlock; acquire() calls holding(); holding() reads `cpu`, which is a
// __thread variable reached through %fs -- and %fs is set up four lines
// below. So an AP allocating its own page reads the per-CPU pointer before
// the per-CPU pointer exists, and the kernel dies in panic("acquire") with
// every lock appearing to be held.
//
// startothers() fills c->local in before starting each AP, on the boot
// processor, where locking works.
c = &cpus[cpunum()];
local = c->local;
if (local == 0) {
local = kalloc(); // the boot processor's own, taken normally
memset(local, 0, PGSIZE);
}
gdt = (struct segdesc*) local;
tss = (uint*) (((char*) local) + 1024);
tss[16] = 0x00680000;
kernel_fs_base = ((uint64) local) + 2048;
wrmsr(0xC0000100, kernel_fs_base);
// The TCB self-pointer: the word at %fs:0 must hold the thread pointer.
//
// This is part of the x86-64 TLS ABI and it was missing, which worked only
// by accident. GCC with -mtls-direct-seg-refs compiles `cpu = c` into a
// single `movq %rcx, %fs:-0x10` and never needs the thread pointer as a
// value, so nothing ever read this word. clang materialises it instead --
// `movq %fs:0x0, %rax; addq $-0x10, %rax; movq %rcx, (%rax)` -- which read
// the zero left by the memset above and stored through address
// 0xfffffffffffffff0, three instructions later.
//
// Writing it costs one store per CPU and removes the dependency on a
// particular compiler's TLS optimisation.
*(uint64 *) kernel_fs_base = kernel_fs_base;
// Clear the two per-CPU slots before anything can read them. Their offsets
// are PERCPU_CPU and PERCPU_PROC in include/proc.h and are shared with
// trapasm.S, which reads %gs:16 for proc on the way out to user mode.
((uint64 *) kernel_fs_base)[2] = 0; // struct cpu * (PERCPU_CPU)
((uint64 *) kernel_fs_base)[3] = 0; // struct proc * (PERCPU_PROC)
// The same address in GS base, per CPU.
//
// trapasm.S needs to find *this* CPU's kernel FS base on every entry from
// user mode, and it has no register to work from at that instant -- which is
// why it used to read the kernel_fs_base global. That global is written by
// every CPU during its own seginit(), so with more than one processor the
// last writer wins and every CPU restores one CPU's per-CPU block. GS base is
// per-CPU in hardware, so %gs:0 answers the question with no global at all.
//
// Safe because GS belongs entirely to the kernel here: sys_arch_prctl()
// implements ARCH_SET_FS and ARCH_GET_FS only, and musl uses FS for TLS on
// x86-64. See the comment on RESTORE_KERNEL_FS.
wrmsr(0xC0000101, kernel_fs_base); // IA32_GS_BASE
c->local = local;
cpu_set(c);
proc_set(0);
// Two of the entries below are deliberately null descriptors, and neither is
// dead code even though both look it:
//
// SEG_UCODE32 is never loaded, but SYSRETQ computes CS from STAR[63:48]+16
// and SS from +8, so the *slot* must exist to keep the
// [UCODE32, UDATA, UCODE] triple consecutive. See syscallinit.
// SEG_KCPU is a leftover slot number from 32-bit xv6, where %gs held the
// per-CPU pointer. Schunix uses FS_BASE/TLS instead, but the
// index has to stay occupied or SEG_TSS stops landing at 7.
//
// Deleting either moves every descriptor after it and breaks sysret.
addr = (uint64) tss;
gdt[0] = (struct segdesc) {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
gdt[SEG_KCODE] = SEG(STA_X|STA_R, 0, 0, 0);
gdt[SEG_KDATA] = SEG(STA_W, 0, 0, 0);
gdt[SEG_UCODE32] = (struct segdesc) {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
gdt[SEG_UDATA] = SEG(STA_W, 0, 0, DPL_USER);
gdt[SEG_UCODE] = SEG(STA_X|STA_R, 0, 0, DPL_USER);
gdt[SEG_KCPU] = (struct segdesc) {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
struct tassdesc *td = (struct tassdesc*)(gdt + SEG_TSS);
td->lim_15_0 = 0x67;
td->base_15_0 = addr & 0xffff;
td->base_23_16 = (addr >> 16) & 0xff;
td->type = STS_T64A;
td->s = 0;
td->dpl = 0;
td->p = 1;
td->lim_19_16 = 0;
td->avl = 0;
td->l = 0;
td->d = 0;
td->g = 0;
td->base_31_24 = (addr >> 24) & 0xff;
td->base_63_32 = (addr >> 32) & 0xffffffff;
td->res = 0;
lgdt((void*) gdt, (NSEGS+1) * sizeof(struct segdesc));
ltr(SEG_TSS << 3);
}
pml4e_t*
setupkvm(void)
{
pml4e_t *pml4 = (pml4e_t*) kalloc();
if(pml4 == 0) return 0;
memset(pml4, 0, PGSIZE);
pml4[PMX(KERNBASE)] = v2p(kpdpt) | PTE_P | PTE_W;
// The framebuffer is deliberately NOT mapped here.
//
// This used to identity-map 0xfd000000 for 800*600*4 bytes, user-writable,
// into every process that ever existed. Three things were wrong with that.
// The address and the size were both guesses that only hold on QEMU's
// stdvga at one resolution; setupkvm runs before vga_init, so on the first
// call nothing had even read the VBE block yet; and every process could
// scribble on the screen without asking anyone.
//
// Nothing used it -- userspace reaches the framebuffer through
// mmap("/dev/fb0"), which maps the real base at the real size. So it is
// gone, and a process gets the framebuffer exactly when it asks for it.
return pml4;
}
void
kvmalloc(void)
{
kpml4 = (pml4e_t*) kalloc();
memset(kpml4, 0, PGSIZE);
kpdpt = (pdpe_t*) kalloc();
memset(kpdpt, 0, PGSIZE);
kpml4[0] = v2p(kpdpt) | PTE_P | PTE_W;
kpml4[PMX(KERNBASE)] = v2p(kpdpt) | PTE_P | PTE_W;
for(uint64 i = 0; i < 4; i++) {
uint64 flags = PTE_PS | PTE_P | PTE_W;
if(i >= 3) {
flags |= PTE_PCD | PTE_PWT;
}
kpdpt[i] = (i << 30) | flags;
}
switchkvm();
}
void
switchuvm(struct proc *p)
{
pushcli();
if(p->pgdir == 0)
panic("switchuvm: no pgdir");
uint *tss = (uint*) (((char*) mycpu()->local) + 1024);
const addr_t stktop = (addr_t)p->kstack + KSTACKSIZE;
tss[1] = (uint)stktop;
tss[2] = (uint)(stktop >> 32);
lcr3(v2p(p->pgdir));
popcli();
}
static pte_t *
walkpgdir(pml4e_t *pml4, const void *va, int alloc)
{
pml4e_t *pml4e;
pdpe_t *pdp, *pdpe;
pde_t *pd, *pde;
pte_t *pgtab;
pml4e = &pml4[PMX(va)];
if(*pml4e & PTE_P)
pdp = (pdpe_t*)P2V(PTE_ADDR(*pml4e));
else {
if(!alloc || (pdp = (pdpe_t*)kalloc()) == 0)
return 0;
memset(pdp, 0, PGSIZE);
*pml4e = V2P(pdp) | PTE_P | PTE_W | PTE_U;
}
pdpe = &pdp[PDPX(va)];
if(*pdpe & PTE_P)
pd = (pde_t*)P2V(PTE_ADDR(*pdpe));
else {
if(!alloc || (pd = (pde_t*)kalloc()) == 0)
return 0;
memset(pd, 0, PGSIZE);
*pdpe = V2P(pd) | PTE_P | PTE_W | PTE_U;
}
pde = &pd[PDX(va)];
if(*pde & PTE_P)
pgtab = (pte_t*)P2V(PTE_ADDR(*pde));
else {
if(!alloc || (pgtab = (pte_t*)kalloc()) == 0)
return 0;
memset(pgtab, 0, PGSIZE);
*pde = V2P(pgtab) | PTE_P | PTE_W | PTE_U;
}
return (pte_t*)&pgtab[PTX(va)];
}
void
switchkvm(void)
{
lcr3(v2p(kpml4));
}
int
mappages(pml4e_t *pgdir, void *va, addr_t size, addr_t pa, int perm)
{
char *a, *last;
pte_t *pte;
a = (char*)PGROUNDDOWN((addr_t)va);
last = (char*)PGROUNDDOWN(((addr_t)va) + size - 1);
for(;;){
if((pte = walkpgdir(pgdir, a, 1)) == 0)
return -1;
if(*pte & PTE_P)
panic("remap");
*pte = pa | perm | PTE_P | PTE_U;
invlpg(a);
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
void
vm_unmap_range(pml4e_t *pgdir, uint64 va, uint64 length)
{
uint64 a = PGROUNDDOWN(va);
uint64 end = PGROUNDUP(va + length);
for (; a < end; a += PGSIZE) {
pte_t *pte = walkpgdir(pgdir, (void*)a, 0);
if (pte && (*pte & PTE_P)) {
uint64 pa = PTE_ADDR(*pte);
kfree((char*)P2V(pa));
*pte = 0;
invlpg((void*)a);
}
}
}
// Tear down a mapping without freeing what is underneath it.
//
// vm_unmap_range() kfree()s every page it unmaps, which is right for an
// anonymous mapping and catastrophic for anything shared: the framebuffer is
// device memory that was never allocated, and a shared memory segment's pages
// belong to the segment, not to whichever process happens to be detaching.
// Handing either of those to kfree puts them on the free list while another
// process is still reading and writing them.
void
vm_unmap_norelease(pml4e_t *pgdir, uint64 va, uint64 length)
{
uint64 a = PGROUNDDOWN(va);
uint64 end = PGROUNDUP(va + length);
for (; a < end; a += PGSIZE) {
pte_t *pte = walkpgdir(pgdir, (void*)a, 0);
if (pte && (*pte & PTE_P)) {
*pte = 0;
invlpg((void*)a);
}
}
}
// Duplicate an mmap()'d VMA region from src into dst for fork().
// Framebuffer mappings are re-pointed at the same physical MMIO range
// (shared, never copied/freed); anonymous mappings get fresh pages with
// the parent's contents copied in — mirrors what copyuvm() does for the
// [0, sz) heap range, which never covers mmap()'d addresses.
int
copy_vma_range(pml4e_t *dstpgdir, pml4e_t *srcpgdir, uint64 va, uint64 length, int share)
{
uint64 a = PGROUNDDOWN(va);
uint64 end = PGROUNDUP(va + length);
for (; a < end; a += PGSIZE) {
if (share) {
// Point the child at the very same physical pages, whatever they are.
//
// This used to special-case the framebuffer and map vga_fb_phys()
// explicitly. Reading the parent's own PTE instead covers device memory
// and shared memory segments with one rule, and cannot disagree with
// what the parent actually has mapped -- which the explicit version
// could, and did, when the address was hardcoded.
pte_t *spte = walkpgdir(srcpgdir, (void*)a, 0);
if (!spte || !(*spte & PTE_P))
continue;
if (mappages(dstpgdir, (void*)a, PGSIZE, PTE_ADDR(*spte), PTE_FLAGS(*spte)) < 0)
return -1;
continue;
}
pte_t *spte = walkpgdir(srcpgdir, (void*)a, 0);
if (!spte || !(*spte & PTE_P))
continue;
char *mem = kalloc();
if (!mem)
return -1;
memmove(mem, (char*)P2V(PTE_ADDR(*spte)), PGSIZE);
if (mappages(dstpgdir, (void*)a, PGSIZE, V2P(mem), PTE_FLAGS(*spte)) < 0) {
kfree(mem);
return -1;
}
}
return 0;
}
void
inituvm(pde_t *pgdir, char *sinit, uint64 sz)
{
char *mem;
if(sz >= PGSIZE)
panic("inituvm: more than a page");
mem = kalloc();
memset(mem, 0, PGSIZE);
mappages(pgdir, 0, PGSIZE, v2p(mem), PTE_W|PTE_U);
memmove(mem, sinit, sz);
}
int
loaduvm(pml4e_t *pgdir, char *addr, struct inode *ip, uint64 offset, uint64 sz)
{
uint64 done, n, off_in_page;
addr_t pa;
pte_t *pte;
char *cur_page;
for(done = 0; done < sz; done += n){
cur_page = (char*)PGROUNDDOWN((uint64)(addr + done));
off_in_page = (uint64)(addr + done) - (uint64)cur_page;
if((pte = walkpgdir(pgdir, cur_page, 0)) == 0)
panic("loaduvm: address should exist");
pa = PTE_ADDR(*pte);
n = PGSIZE - off_in_page;
if(n > sz - done)
n = sz - done;
if(readi(ip, 0, (uint64)(P2V(pa) + off_in_page), offset + done, n) != (int)n)
return -1;
}
return 0;
}
uint64
allocuvm(pml4e_t *pgdir, uint64 oldsz, uint64 newsz)
{
char *mem;
addr_t a;
if(newsz >= KERNBASE)
return 0;
if(newsz < oldsz)
return oldsz;
a = PGROUNDUP(oldsz);
for(; a < newsz; a += PGSIZE){
mem = kalloc();
if(mem == 0){
deallocuvm(pgdir, newsz, oldsz);
return 0;
}
memset(mem, 0, PGSIZE);
if(mappages(pgdir, (char*)a, PGSIZE, V2P(mem), PTE_W|PTE_U) < 0){
deallocuvm(pgdir, newsz, oldsz);
kfree(mem);
return 0;
}
}
return newsz;
}
uint64
deallocuvm(pml4e_t *pgdir, uint64 oldsz, uint64 newsz)
{
pte_t *pte;
addr_t a, pa;
if(newsz >= oldsz)
return oldsz;
a = PGROUNDUP(newsz);
for(; a < oldsz; a += PGSIZE){
pte = walkpgdir(pgdir, (char*)a, 0);
if(pte && (*pte & PTE_P) != 0){
pa = PTE_ADDR(*pte);
if(pa == 0)
panic("kfree");
if(pa < PHYSTOP) {
char *v = P2V(pa);
kfree(v);
}
*pte = 0;
}
}
return newsz;
}
void
freevm(pml4e_t *pml4)
{
uint i, j, k, l;
pdpe_t *pdp;
pde_t *pd;
pte_t *pt;
if(pml4 == 0)
panic("freevm: no pgdir");
for(i = 0; i < (512/2); i++){
if(pml4[i] & PTE_P){
pdp = (pdpe_t*)P2V(PTE_ADDR(pml4[i]));
for(j = 0; j < 512; j++){
if(pdp[j] & PTE_P){
pd = (pde_t*)P2V(PTE_ADDR(pdp[j]));
for(k = 0; k < 512; k++){
if(pd[k] & PTE_P) {
pt = (pte_t*)P2V(PTE_ADDR(pd[k]));
for(l = 0; l < 512; l++){
if(pt[l] & PTE_P) {
addr_t pa = PTE_ADDR(pt[l]);
if(pa < PHYSTOP && pa != 0) {
kfree(P2V(pa));
}
}
}
kfree((char*)pt);
}
}
kfree((char*)pd);
}
}
kfree((char*)pdp);
}
}
kfree((char*)pml4);
}
void
clearpteu(pml4e_t *pgdir, char *uva)
{
pte_t *pte;
pte = walkpgdir(pgdir, uva, 0);
if(pte == 0)
panic("clearpteu");
*pte &= ~PTE_U;
}
pml4e_t*
copyuvm(pml4e_t *pgdir, uint64 sz)
{
pml4e_t *d;
pte_t *pte;
addr_t pa, i, flags;
char *mem;
if((d = setupkvm()) == 0)
return 0;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walkpgdir(pgdir, (void*)i, 0)) == 0)
continue;
if(!(*pte & PTE_P))
continue;
pa = PTE_ADDR(*pte);
flags = PTE_FLAGS(*pte);
if(pa < PHYSTOP) {
if((mem = kalloc()) == 0)
goto bad;
memmove(mem, (char*)P2V(pa), PGSIZE);
if(mappages(d, (void*)i, PGSIZE, V2P(mem), flags) < 0) {
kfree(mem);
goto bad;
}
}
}
return d;
bad:
freevm(d);
return 0;
}
char*
uva2ka(pml4e_t *pgdir, char *uva)
{
if((addr_t)uva >= KERNBASE)
return 0;
pte_t *pte;
pte = walkpgdir(pgdir, uva, 0);
if(pte == 0 || (*pte & PTE_P) == 0)
return 0;
if((*pte & PTE_U) == 0)
return 0;
addr_t pa = (*pte) & 0x000FFFFFFFFFF000ull;
// Include uva's in-page offset — callers dereference the returned pointer
// directly at uva's address, not just somewhere within its page.
return (char*)P2V(pa) + ((addr_t)uva & 0xFFF);
}
int
copyout(pml4e_t *pgdir, addr_t va, void *p, uint64 len)
{
char *buf, *pa0;
addr_t n, va0;
buf = (char*)p;
while(len > 0){
va0 = PGROUNDDOWN(va);
pa0 = uva2ka(pgdir, (char*)va0);
if(pa0 == 0) {
return -1;
}
n = PGSIZE - (va - va0);
if(n > len)
n = len;
memmove(pa0 + (va - va0), buf, n);
len -= n;
buf += n;
va += n;
}
return 0;
}
int
copyin(pml4e_t *pgdir, addr_t srcva, void *dst, uint64 len)
{
uint64 n, va0;
char *pa0;
char *dst_buf = (char*)dst;
while(len > 0){
va0 = PGROUNDDOWN(srcva);
pa0 = uva2ka(pgdir, (char*)va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (srcva - va0);
if(n > len)
n = len;
memmove(dst_buf, pa0 + (srcva - va0), n);
len -= n;
dst_buf += n;
srcva += n;
}
return 0;
}

810
kernel/core/console.c Normal file
View file

@ -0,0 +1,810 @@
#include <stdarg.h>
#include "types.h"
#include "defs.h"
#include "theme.h"
#include "irq.h"
#include "param.h"
#include "traps.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "posix.h"
#include "x86.h"
void consputc(int);
// Set by panic() once the crash screen has been painted: after that, consputc
// spins instead of writing, so nothing else can scribble over the report.
int panicked = 0;
// When set, console output goes to the serial line only and the framebuffer
// is left alone.
//
// **Starts set, on purpose.** The display is the product; the serial line is
// the diagnostics. A machine that boots by printing "pci: 6 devices" and
// "vga: display controller 1234:1111, aperture fd000000" to the screen the
// user is looking at is showing them a developer's log, and that single fact
// is most of what makes an OS look like a hobby project.
//
// Nothing is lost: every one of those lines still goes out the UART, which is
// where tools/bootcheck.ps1 reads them from and where you want them when
// something goes wrong. console_takeover() clears this once userspace is up
// and the display genuinely belongs to the console. The F8 verbose boot never
// sets it, so "show me everything" still works.
int cons_serial_only = 1;
extern void vga_screen_reset(void);
// Text colour used when drawing to the framebuffer. Changed by SGR escapes
// (see sgr() below); reset to the default by ESC[0m and ESC c.
//
// This was initialised to 7, a leftover from when it was a VGA text-mode
// attribute byte. vga_putc takes a 32-bit RGB value, so 7 meant "very dark
// blue" -- it looked black against the cream background purely by accident.
static uint32 cons_fg = SCH_FG;
static struct {
struct spinlock lock;
int locking;
} cons;
static char digits[] = "0123456789abcdef";
static void
print_int(int64 xx, int base, int sign)
{
char buf[64];
int i;
uint64 x;
if(sign && (sign = (xx < 0)))
x = -xx;
else
x = xx;
i = 0;
do {
buf[i++] = digits[x % base];
} while((x /= base) != 0);
if(sign)
buf[i++] = '-';
while(--i >= 0)
consputc(buf[i]);
}
void
cprintf(char *fmt, ...)
{
va_list ap;
int i, c, locking;
char *s;
va_start(ap, fmt);
locking = cons.locking;
if (locking)
acquire(&cons.lock);
if (fmt == 0)
panic("null fmt");
for (i = 0; (c = fmt[i] & 0xff) != 0; i++) {
if (c != '%') {
consputc(c);
continue;
}
c = fmt[++i] & 0xff;
if (c == 0)
break;
switch(c) {
case 'd':
print_int(va_arg(ap, int), 10, 1);
break;
case 'u':
print_int(va_arg(ap, uint32), 10, 0);
break;
case 'l':
print_int(va_arg(ap, int64), 10, 1);
break;
case 'x':
print_int(va_arg(ap, uint32), 16, 0);
break;
case 'p':
print_int(va_arg(ap, uint64), 16, 0);
break;
case 'b':
print_int(va_arg(ap, uint32), 2, 0);
break;
case 's':
if ((s = va_arg(ap, char*)) == 0)
s = "(null)";
while (*s)
consputc(*(s++));
break;
case '%':
consputc('%');
break;
default:
consputc('%');
consputc(c);
break;
}
}
if (locking)
release(&cons.lock);
}
// Hand the console over to panic().
//
// Two things change. The lock stops being taken, so a panic can report itself
// from anywhere — including from inside a routine that already holds it. And
// output stops going to the framebuffer, because from here on the framebuffer
// belongs to the crash screen: without this the report would scroll straight
// over the panel that is trying to display it.
void
cons_panic_mode(void)
{
cons.locking = 0;
cons_serial_only = 1;
}
// Hand the display over to the text console.
//
// Called once, when userspace is far enough along that the screen should stop
// being a boot splash and start being a terminal. Until this runs the kernel
// has been talking to the serial line only (see cons_serial_only), so this is
// the moment the framebuffer changes owner -- which is why it clears first
// rather than scrolling the shell prompt in underneath the splash.
void
console_takeover(void)
{
if (!cons_serial_only)
return; // verbose boot: the console never left
cons_serial_only = 0;
vga_screen_reset();
}
// Foreground process group of the console, i.e. the controlling terminal.
// tcgetpgrp()/tcsetpgrp() read and write this through TIOCGPGRP/TIOCSPGRP in
// sys_fs.c. Initialised to 1 -- init's process group -- so the first shell,
// which inherits that group, already sees itself as the foreground job and
// does not immediately stop itself with SIGTTIN.
int tty_fg_pgrp = 1;
#define BACKSPACE 0x100
// ---------------------------------------------------------------------------
// Output: an ANSI escape parser, and UTF-8
//
// The console used to print every byte it was given, which meant every escape
// sequence any program emitted appeared on screen as literal text -- `clear`
// drew "^[[2J^[[H" and left the screen exactly as it found it. Colour, cursor
// addressing, and anything full-screen were all equally impossible.
//
// The serial line does NOT go through any of this. On the other end of it is
// a real terminal emulator that already understands ANSI far better than this
// ever will, so escapes are passed through to it verbatim and interpreted
// only for the framebuffer. Two consumers, two right answers.
// ---------------------------------------------------------------------------
// The sixteen ANSI colours as RGB, for a linear framebuffer.
//
// consoleinit() used to program these into the VGA DAC with set_vga_palette(),
// which does nothing at all in a 32bpp linear mode -- there is no palette to
// index. The colours had to live somewhere real, and this is it.
static const uint32 ansi_rgb[16] = {
0x000000, 0xAA0000, 0x00AA00, 0xAA5500, // black red green yellow(brown)
0x0000AA, 0xAA00AA, 0x00AAAA, 0xAAAAAA, // blue magenta cyan white
0x555555, 0xFF5555, 0x55FF55, 0xFFFF55, // bright versions
0x5555FF, 0xFF55FF, 0x55FFFF, 0xFFFFFF,
};
#define CONS_DEFAULT_FG SCH_FG // see include/theme.h
#define CONS_DEFAULT_BG SCH_BG
static uint32 cons_bg = CONS_DEFAULT_BG;
static int cons_bold = 0;
// Escape parser state.
enum { E_NORMAL, E_ESC, E_CSI };
static int esc_state = E_NORMAL;
static int esc_parm[8];
static int esc_nparm;
static int esc_private; // saw '?' after CSI
static int saved_col, saved_row;
// UTF-8 decoder state. A codepoint arrives as up to four bytes and has to be
// reassembled before it can be turned into a glyph.
static uint32 u8_cp;
static int u8_need;
// ---------------------------------------------------------------------------
// Unicode -> glyph
//
// The font's upper half is CP437, not Latin-1, so a codepoint cannot be used
// as an index. It looks like it can -- font.h's own comments say so, labelling
// each glyph with the Unicode character of the same number -- but the bitmaps
// disagree: index 0xE9 draws a capital theta, and rendering U+00E9 there turns
// "cafe" into "caf" followed by a theta. Every accented character came out as
// an unrelated symbol, consistently enough to look almost deliberate.
//
// So: a table. Entry i covers codepoint 0x80 + i, and 0 means "this font has
// no glyph for that". The C1 range (0x80-0x9F) has no printable characters at
// all, hence the leading run of zeros.
// ---------------------------------------------------------------------------
static const uchar latin1_to_cp437[128] = {
/* 0x80-0x9F: C1 controls, nothing to draw */
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
/* 0xA0 */ 0x20, 0xAD, 0x9B, 0x9C, 0, 0x9D, 0, 0x15,
/* 0xA8 */ 0, 0, 0xA6, 0xAE, 0xAA, 0x2D, 0, 0,
/* 0xB0 */ 0xF8, 0xF1, 0xFD, 0, 0, 0xE6, 0x14, 0xFA,
/* 0xB8 */ 0, 0, 0xA7, 0xAF, 0xAC, 0xAB, 0, 0xA8,
/* 0xC0 */ 0, 0, 0, 0, 0x8E, 0x8F, 0x92, 0x80,
/* 0xC8 */ 0, 0x90, 0, 0, 0, 0, 0, 0,
/* 0xD0 */ 0, 0xA5, 0, 0, 0, 0, 0x99, 0,
/* 0xD8 */ 0, 0, 0, 0, 0x9A, 0, 0, 0xE1,
/* 0xE0 */ 0x85, 0xA0, 0x83, 0, 0x84, 0x86, 0x91, 0x87,
/* 0xE8 */ 0x8A, 0x82, 0x88, 0x89, 0x8D, 0xA1, 0x8C, 0x8B,
/* 0xF0 */ 0, 0xA4, 0x95, 0xA2, 0x93, 0, 0x94, 0xF6,
/* 0xF8 */ 0, 0x97, 0xA3, 0x96, 0x81, 0, 0, 0x98,
};
// The handful of characters above U+00FF that CP437 does have. Box drawing
// and block elements matter because they are what every text-mode program
// draws frames and progress bars out of.
struct cp_pair { uint32 cp; uchar glyph; };
static const struct cp_pair cp437_extra[] = {
{ 0x2022, 0x07 }, /* bullet */
{ 0x2500, 0xC4 }, { 0x2502, 0xB3 }, /* light horizontal/vertical */
{ 0x250C, 0xDA }, { 0x2510, 0xBF },
{ 0x2514, 0xC0 }, { 0x2518, 0xD9 },
{ 0x251C, 0xC3 }, { 0x2524, 0xB4 },
{ 0x252C, 0xC2 }, { 0x2534, 0xC1 }, { 0x253C, 0xC5 },
{ 0x2550, 0xCD }, { 0x2551, 0xBA }, /* double */
{ 0x2554, 0xC9 }, { 0x2557, 0xBB },
{ 0x255A, 0xC8 }, { 0x255D, 0xBC },
{ 0x2580, 0xDF }, { 0x2584, 0xDC }, { 0x2588, 0xDB },
{ 0x258C, 0xDD }, { 0x2590, 0xDE },
{ 0x2591, 0xB0 }, { 0x2592, 0xB1 }, { 0x2593, 0xB2 },
{ 0x25A0, 0xFE }, /* black square */
{ 0x221A, 0xFB }, { 0x221E, 0xEC }, /* root, infinity */
{ 0x2260, 0, }, { 0x2264, 0xF3 }, { 0x2265, 0xF2 },
{ 0x2248, 0xF7 }, { 0x2261, 0xF0 },
};
// Returns the glyph to draw for a codepoint, or 0 if there is none.
static int
glyph_for(uint32 cp)
{
if (cp < 0x80)
return (int)cp; // ASCII is CP437's lower half
if (cp < 0x100)
return latin1_to_cp437[cp - 0x80];
for (unsigned i = 0; i < NELEM(cp437_extra); i++)
if (cp437_extra[i].cp == cp)
return cp437_extra[i].glyph;
return 0;
}
static void
sgr(void)
{
for (int i = 0; i < esc_nparm; i++) {
int p = esc_parm[i];
if (p == 0) {
cons_fg = CONS_DEFAULT_FG;
cons_bg = CONS_DEFAULT_BG;
cons_bold = 0;
} else if (p == 1) {
cons_bold = 1;
// Re-apply brightness to a colour already chosen.
for (int j = 0; j < 8; j++)
if (cons_fg == ansi_rgb[j]) { cons_fg = ansi_rgb[j + 8]; break; }
} else if (p == 22) {
cons_bold = 0;
} else if (p >= 30 && p <= 37) {
cons_fg = ansi_rgb[(p - 30) + (cons_bold ? 8 : 0)];
} else if (p == 39) {
cons_fg = CONS_DEFAULT_FG;
} else if (p >= 40 && p <= 47) {
cons_bg = ansi_rgb[p - 40];
} else if (p == 49) {
cons_bg = CONS_DEFAULT_BG;
} else if (p >= 90 && p <= 97) {
cons_fg = ansi_rgb[(p - 90) + 8];
} else if (p >= 100 && p <= 107) {
cons_bg = ansi_rgb[(p - 100) + 8];
}
}
vga_set_bg(cons_bg);
}
// One final byte of a CSI sequence, with esc_parm[] already filled in.
static void
csi_dispatch(int final)
{
int col, row;
int n = esc_nparm > 0 ? esc_parm[0] : 0;
int n1 = n ? n : 1; // most sequences default to 1
vga_cursor_get(&col, &row);
switch (final) {
case 'A': vga_cursor_set(col, row - n1); break;
case 'B': vga_cursor_set(col, row + n1); break;
case 'C': vga_cursor_set(col + n1, row); break;
case 'D': vga_cursor_set(col - n1, row); break;
case 'E': vga_cursor_set(0, row + n1); break;
case 'F': vga_cursor_set(0, row - n1); break;
case 'G': vga_cursor_set(n1 - 1, row); break;
case 'd': vga_cursor_set(col, n1 - 1); break;
case 'H': case 'f': {
int r = esc_nparm > 0 && esc_parm[0] ? esc_parm[0] : 1;
int c = esc_nparm > 1 && esc_parm[1] ? esc_parm[1] : 1;
vga_cursor_set(c - 1, r - 1); // ANSI counts from 1
break;
}
case 'J': vga_erase_display(n); break;
case 'K': vga_erase_line(n); break;
case 'm': sgr(); break;
case 's': saved_col = col; saved_row = row; break;
case 'u': vga_cursor_set(saved_col, saved_row); break;
// Cursor visibility (DECTCEM) and the other private modes. There is no
// hardware cursor being drawn, so there is nothing to show or hide --
// swallowing them silently is the correct behaviour, and far better than
// printing "?25l" into the middle of someone's output.
case 'h': case 'l': break;
default: break; // unknown: drop it, do not print it
}
}
// Feed one output byte to the framebuffer side. Returns nothing; the serial
// side has already had the raw byte.
static void
vga_feed(int c)
{
switch (esc_state) {
case E_ESC:
if (c == '[') {
esc_state = E_CSI;
esc_nparm = 0;
esc_private = 0;
for (int i = 0; i < 8; i++) esc_parm[i] = 0;
return;
}
esc_state = E_NORMAL;
if (c == 'c') { // RIS -- full reset
cons_fg = CONS_DEFAULT_FG;
cons_bg = CONS_DEFAULT_BG;
vga_screen_reset();
}
return; // other two-byte escapes: ignored
case E_CSI:
if (c == '?') { esc_private = 1; return; }
if (c >= '0' && c <= '9') {
if (esc_nparm == 0) esc_nparm = 1;
if (esc_nparm <= 8)
esc_parm[esc_nparm - 1] = esc_parm[esc_nparm - 1] * 10 + (c - '0');
return;
}
if (c == ';') {
if (esc_nparm < 8) esc_nparm++;
return;
}
esc_state = E_NORMAL;
if (!esc_private)
csi_dispatch(c);
return;
}
// E_NORMAL
if (c == '\x1b') { esc_state = E_ESC; return; }
// UTF-8 continuation. A codepoint the font cannot draw becomes '?' -- the
// one thing it must not become is a *different* character, which is exactly
// what indexing the font by codepoint did.
if (u8_need > 0) {
if ((c & 0xC0) != 0x80) { // malformed: abandon the sequence
u8_need = 0;
vga_putc('?', cons_fg);
return;
}
u8_cp = (u8_cp << 6) | (c & 0x3F);
if (--u8_need > 0)
return;
int g = glyph_for(u8_cp);
vga_putc(g ? g : '?', cons_fg);
return;
}
if (c >= 0xF0) { u8_cp = c & 0x07; u8_need = 3; return; }
else if (c >= 0xE0) { u8_cp = c & 0x0F; u8_need = 2; return; }
else if (c >= 0xC0) { u8_cp = c & 0x1F; u8_need = 1; return; }
else if (c >= 0x80) { vga_putc('?', cons_fg); return; } // stray continuation
vga_putc(c, cons_fg);
}
void
consputc(int c)
{
if (panicked) {
cli();
for(;;) hlt();
}
if (c == BACKSPACE) {
uartputc('\b'); uartputc(' '); uartputc('\b');
if (!cons_serial_only)
vga_putc(c, cons_fg);
return;
}
uartputc(c);
if (!cons_serial_only)
vga_feed(c & 0xff);
}
#define INPUT_BUF 256
struct {
struct spinlock lock;
char buf[INPUT_BUF];
uint r;
uint w;
uint e;
uint abort_read;
// Set by EOF (Ctrl-D) on an empty line. Canonical-mode end of input is not
// a character -- it is the *absence* of one -- so it cannot be signalled by
// putting something in the buffer. The old code pushed a literal ^D in and
// had consoleread() pick it back out, which meant a ^D typed mid-line was
// indistinguishable from one typed alone.
uint eof;
} input;
#define C(x) ((x)-'@')
// ---------------------------------------------------------------------------
// The line discipline
//
// TCGETS used to return a hardcoded set of flags and TCSETS threw its argument
// away, so the terminal was permanently in one mode: line-at-a-time, always
// echoing. Anything wanting a keypress at a time -- a pager, an editor, a
// shell doing its own line editing, which is most of what zsh is -- had no way
// to ask for one. This is the real thing: a struct programs can write, that
// changes what the kernel actually does.
// ---------------------------------------------------------------------------
static struct termios tio = {
.c_iflag = ICRNL,
.c_oflag = OPOST | ONLCR,
.c_cflag = CS8 | CREAD | CLOCAL,
.c_lflag = ISIG | ICANON | ECHO | ECHOE,
.c_cc = {
[VINTR] = C('C'), [VQUIT] = C('\\'), [VERASE] = '\x7f', [VKILL] = C('U'),
[VEOF] = C('D'), [VSUSP] = C('Z'), [VWERASE] = C('W'), [VMIN] = 1,
},
.c_ispeed = 15, .c_ospeed = 15,
};
void
cons_get_termios(struct termios *out)
{
acquire(&input.lock);
*out = tio;
release(&input.lock);
}
void
cons_set_termios(const struct termios *in)
{
acquire(&input.lock);
tio = *in;
// Switching out of canonical mode has to hand over whatever has been typed
// but not yet submitted, or a shell that turns off ICANON mid-line silently
// eats the characters already in the edit buffer.
if (!(tio.c_lflag & ICANON))
input.w = input.e;
release(&input.lock);
wakeup(&input.r);
}
static void
echo(int c)
{
if (c == '\n' || c == '\r') {
if ((tio.c_lflag & ECHO) || (tio.c_lflag & ECHONL))
consputc('\n');
return;
}
if (!(tio.c_lflag & ECHO))
return;
// Control characters echo as ^X, the way every other terminal shows them.
if (c < 32 && c != '\t') {
consputc('^');
consputc(c + '@');
} else {
consputc(c);
}
}
// Rub out the last character of the current line.
static void
erase_one(void)
{
if (input.e == input.w)
return;
input.e--;
if (tio.c_lflag & ECHOE)
consputc(BACKSPACE);
}
void
consoleintr(int (*getc)(void))
{
int c;
acquire(&input.lock);
while((c = getc()) >= 0){
if (c == 0)
continue;
// Input mapping, before anything looks at the character.
if (tio.c_iflag & ISTRIP) c &= 0x7f;
if (c == '\r') {
if (tio.c_iflag & IGNCR) continue;
if (tio.c_iflag & ICRNL) c = '\n';
} else if (c == '\n' && (tio.c_iflag & INLCR)) {
c = '\r';
}
// Signals. These are the characters that must not reach the reader.
//
// Ctrl-C used to do `myproc()->killed = 1` -- and `proc` in an interrupt
// handler is whatever happened to be running on this CPU when the key was
// pressed, which is almost never the program the user is looking at. It
// also set a kill flag rather than raising a signal, so nothing could
// catch it and the shell never learned the job had died. Both wrong;
// both fixed by sending a real signal to the foreground process group,
// which the kernel has been tracking in tty_fg_pgrp all along.
if (tio.c_lflag & ISIG) {
int sig = 0;
if (c == tio.c_cc[VINTR]) sig = SIGINT;
else if (c == tio.c_cc[VQUIT]) sig = SIGQUIT;
else if (c == tio.c_cc[VSUSP]) sig = SIGTSTP;
if (sig) {
echo(c);
input.e = input.w = input.r; // discard the partial line
input.abort_read = 1;
release(&input.lock);
sigsend_pgrp(tty_fg_pgrp, sig);
acquire(&input.lock);
wakeup(&input.r);
continue;
}
}
// Ctrl-P: dump the process table. Not POSIX, but it is the one debugging
// aid that works when everything else has stopped responding.
if (c == C('P')) {
procdump();
continue;
}
if (tio.c_lflag & ICANON) {
if (c == tio.c_cc[VERASE] || c == C('H')) {
erase_one();
continue;
}
if (c == tio.c_cc[VKILL]) {
while (input.e != input.w &&
input.buf[(input.e-1) % INPUT_BUF] != '\n')
erase_one();
continue;
}
if (c == tio.c_cc[VWERASE]) {
while (input.e != input.w &&
input.buf[(input.e-1) % INPUT_BUF] == ' ')
erase_one();
while (input.e != input.w &&
input.buf[(input.e-1) % INPUT_BUF] != ' ')
erase_one();
continue;
}
if (c == tio.c_cc[VEOF]) {
if (input.e == input.w)
input.eof = 1; // empty line: end of input
input.w = input.e; // otherwise just submit what is there
wakeup(&input.r);
continue;
}
}
if (input.e - input.r >= INPUT_BUF)
continue; // buffer full: drop it
// Everything else goes in, including bytes above 127 -- those are UTF-8
// continuation bytes, and the old `c >= 32 && c <= 126` test silently
// refused every non-ASCII character anyone tried to type.
input.buf[input.e++ % INPUT_BUF] = c;
echo(c);
// Canonical mode hands over a line at a time; raw mode hands over
// everything as it arrives.
if (!(tio.c_lflag & ICANON) ||
c == '\n' || input.e == input.r + INPUT_BUF) {
input.w = input.e;
wakeup(&input.r);
}
}
release(&input.lock);
}
int
consoleread(struct inode *ip, uint off, char *dst, int n)
{
uint target;
int c;
static uint32 prng_seed = 0x9A7B3C1D;
if (ip->minor == 2) {
return 0;
}
if (ip->minor == 4) {
for (int i = 0; i < n; i++) {
prng_seed ^= prng_seed << 13;
prng_seed ^= prng_seed >> 17;
prng_seed ^= prng_seed << 5;
dst[i] = prng_seed & 0xFF;
}
return n;
}
iunlock(ip);
target = n;
acquire(&input.lock);
while(n > 0){
while(input.r == input.w){
if (input.eof) {
input.eof = 0;
release(&input.lock);
ilock(ip);
return 0; // end of input
}
if (myproc()->killed || input.abort_read) {
input.abort_read = 0;
release(&input.lock);
ilock(ip);
return -1;
}
sleep(&input.r, &input.lock);
}
c = input.buf[input.r++ % INPUT_BUF];
*dst++ = c;
--n;
// Canonical mode returns a line at a time. Raw mode returns whatever has
// arrived: a program that asked for a keypress must not be made to wait
// for a newline that is never coming.
if (tio.c_lflag & ICANON) {
if (c == '\n')
break;
} else {
if (target - n >= (uint)(tio.c_cc[VMIN] ? tio.c_cc[VMIN] : 1))
break;
}
}
release(&input.lock);
ilock(ip);
return target - n;
}
// Nonzero if a completed input line is waiting — poll(2)'s POLLIN answer
// for the console tty. Mirrors the r/w cursors consoleread() blocks on.
int
console_input_ready(void)
{
int ready;
acquire(&input.lock);
ready = input.r != input.w;
release(&input.lock);
return ready;
}
int
consolewrite(struct inode *ip, uint off, char *buf, int n)
{
int i;
if (ip->minor == 2 || ip->minor == 3 || ip->minor == 4) {
return n;
}
iunlock(ip);
acquire(&cons.lock);
for(i = 0; i < n; i++)
consputc(buf[i] & 0xff);
release(&cons.lock);
ilock(ip);
return n;
}
void
disable_blink(void)
{
inb(0x3DA);
outb(0x3C0, 0x10 | 0x20);
uchar dummy = inb(0x3C1);
outb(0x3C0, dummy & ~0x08);
}
static void
set_vga_palette(uchar index, uchar r, uchar g, uchar b)
{
outb(0x3C6, 0xFF);
outb(0x3C8, index);
outb(0x3C9, r & 0x3F);
outb(0x3C9, g & 0x3F);
outb(0x3C9, b & 0x3F);
}
// The keyboard handler lives in kbd.c, but the console is what owns the
// keyboard as an input source, so this is where the line gets claimed.
static void
kbd_irq(void *arg)
{
(void)arg;
kbdintr();
}
void
consoleinit(void)
{
initlock(&cons.lock, "console");
initlock(&input.lock, "input");
memset(&input, 0, sizeof(input));
devsw[CONSOLE].write = consolewrite;
devsw[CONSOLE].read = consoleread;
cons.locking = 1;
disable_blink();
set_vga_palette(0, 0, 0, 0); // Black
set_vga_palette(1, 0, 0, 42); // Blue
set_vga_palette(2, 0, 42, 0); // Green
set_vga_palette(3, 0, 42, 42); // Cyan
set_vga_palette(4, 42, 0, 0); // Red
set_vga_palette(5, 42, 0, 42); // Magenta
set_vga_palette(6, 42, 21, 0); // Brown
set_vga_palette(7, 42, 42, 42); // Light Gray
set_vga_palette(8, 21, 21, 21); // Dark Gray
set_vga_palette(9, 21, 21, 63); // Light Blue
set_vga_palette(10, 21, 63, 21); // Light Green
set_vga_palette(11, 21, 63, 63); // Light Cyan
set_vga_palette(12, 63, 21, 21); // Light Red
set_vga_palette(13, 21, 21, 63); // Light Magenta
set_vga_palette(14, 63, 63, 21); // Yellow
set_vga_palette(15, 63, 63, 63); // White
irq_register(IRQ_KBD, kbd_irq, 0, "ps2-kbd", 0);
}

191
kernel/core/dma.c Normal file
View file

@ -0,0 +1,191 @@
// Physically contiguous memory for devices. See include/dma.h.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
#include "dma.h"
#define DMA_GRAIN 64 // one cache line; also xHCI's ring alignment
#define NEXTENT 64 // free extents we can track
struct extent {
addr_t base; // kernel virtual address
uint64 len;
};
static struct {
struct spinlock lock;
struct extent free[NEXTENT];
int n; // free extents, kept sorted by base
addr_t lo, hi;
uint64 total;
} arena;
static uint64
roundup(uint64 v, uint64 to)
{
return (v + to - 1) & ~(to - 1);
}
void
dma_init(void *vstart, void *vend)
{
initlock(&arena.lock, "dma");
arena.lo = roundup((addr_t)vstart, PGSIZE);
arena.hi = (addr_t)vend & ~(addr_t)(PGSIZE - 1);
if(arena.hi <= arena.lo)
panic("dma_init: empty arena");
arena.free[0].base = arena.lo;
arena.free[0].len = arena.hi - arena.lo;
arena.n = 1;
arena.total = arena.free[0].len;
}
// Remove free[i], shifting the tail down.
static void
drop(int i)
{
int j;
for(j = i; j + 1 < arena.n; j++)
arena.free[j] = arena.free[j+1];
arena.n--;
}
// Insert a free extent at index i, shifting the tail up. Returns 0 if the
// table is full, in which case the memory is leaked rather than corrupting
// the list -- a leak is recoverable by rebooting, a corrupt free list hands
// two drivers the same ring.
static int
insert(int i, addr_t base, uint64 len)
{
int j;
if(arena.n >= NEXTENT)
return 0;
for(j = arena.n; j > i; j--)
arena.free[j] = arena.free[j-1];
arena.free[i].base = base;
arena.free[i].len = len;
arena.n++;
return 1;
}
void *
dma_alloc(uint64 size, uint64 align)
{
int i;
addr_t start;
if(size == 0)
return 0;
if(align < DMA_GRAIN)
align = DMA_GRAIN;
if(align & (align - 1))
panic("dma_alloc: alignment not a power of two");
size = roundup(size, DMA_GRAIN);
acquire(&arena.lock);
for(i = 0; i < arena.n; i++){
addr_t base = arena.free[i].base;
uint64 len = arena.free[i].len;
uint64 lead;
start = roundup(base, align);
lead = start - base;
if(lead + size > len)
continue;
// Three cases: the request consumes the extent, sits at its head, sits
// at its tail, or sits in the middle and splits it in two.
if(lead == 0 && size == len){
drop(i);
} else if(lead == 0){
arena.free[i].base += size;
arena.free[i].len -= size;
} else if(lead + size == len){
arena.free[i].len = lead;
} else {
uint64 tail_len = len - lead - size;
arena.free[i].len = lead;
if(!insert(i + 1, start + size, tail_len)){
// No room to record the tail. Give the whole extent to the caller
// rather than lose track of where the tail was.
arena.free[i].len = len;
continue;
}
}
release(&arena.lock);
memset((void*)start, 0, size);
return (void*)start;
}
release(&arena.lock);
cprintf("dma_alloc: out of DMA memory (wanted %d bytes, %d free)\n",
(int)size, (int)dma_free_bytes());
return 0;
}
void
dma_free(void *p, uint64 size)
{
addr_t base = (addr_t)p;
int i;
if(p == 0)
return;
size = roundup(size, DMA_GRAIN);
if(base < arena.lo || base + size > arena.hi)
panic("dma_free: not DMA memory");
acquire(&arena.lock);
// Find the first extent that starts after this block, so the list stays
// sorted and neighbours end up adjacent for coalescing.
for(i = 0; i < arena.n && arena.free[i].base < base; i++)
;
if(i > 0 && arena.free[i-1].base + arena.free[i-1].len == base){
arena.free[i-1].len += size; // merge with the block before
i--;
} else if(!insert(i, base, size)){
release(&arena.lock);
cprintf("dma_free: extent table full, leaking %d bytes\n", (int)size);
return;
}
if(i + 1 < arena.n &&
arena.free[i].base + arena.free[i].len == arena.free[i+1].base){
arena.free[i].len += arena.free[i+1].len; // and with the block after
drop(i + 1);
}
release(&arena.lock);
}
addr_t
dma_phys(const void *p)
{
return V2P((addr_t)p);
}
uint64
dma_free_bytes(void)
{
uint64 n = 0;
int i;
for(i = 0; i < arena.n; i++)
n += arena.free[i].len;
return n;
}
uint64
dma_total_bytes(void)
{
return arena.total;
}

124
kernel/core/kalloc.c Normal file
View file

@ -0,0 +1,124 @@
// Physical memory allocator, intended to allocate
// memory for user processes, kernel stacks, page table pages,
// and pipe buffers. Allocates 4096-byte pages.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
void freerange(void *vstart, void *vend);
extern char end[]; // first address after kernel loaded from ELF file
struct run {
struct run *next;
};
struct {
struct spinlock lock;
int use_lock;
struct run *freelist;
} kmem;
// Catch kmem.freelist corruption at the point it's touched, rather than
// letting a bad pointer be silently returned by kalloc() and crash whatever
// code dereferences it minutes later on an unrelated call stack.
static void
check_run(struct run *r, char *where)
{
if(r == 0)
return;
if((addr_t)r % PGSIZE || (char*)r < end || V2P(r) >= PHYSTOP){
cprintf("kalloc: corrupt freelist entry %p at %s\n", r, where);
panic("kalloc: corrupt freelist");
}
}
void
kinit1(void *vstart, void *vend)
{
initlock(&kmem.lock, "kmem");
kmem.use_lock = 0;
kmem.freelist = 0; // empty
freerange(vstart, vend);
}
void
kinit2()
{
kmem.use_lock = 1;
}
void
freerange(void *vstart, void *vend)
{
char *p;
p = (char*)PGROUNDUP((addr_t)vstart);
for(; p + PGSIZE <= (char*)vend; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(char *v)
{
struct run *r;
if((addr_t)v % PGSIZE || v < end || V2P(v) >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(v, 1, PGSIZE);
if(kmem.use_lock)
acquire(&kmem.lock);
check_run(kmem.freelist, "kfree entry");
r = (struct run*)v;
r->next = kmem.freelist;
kmem.freelist = r;
if(kmem.use_lock)
release(&kmem.lock);
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
char*
kalloc(void)
{
struct run *r;
if(kmem.use_lock)
acquire(&kmem.lock);
check_run(kmem.freelist, "kalloc entry");
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
if(kmem.use_lock)
release(&kmem.lock);
return (char*)r;
}
// Total free physical memory, in bytes. Walks the free list; only /proc
// readers call it, so the O(n) scan is not on any hot path.
uint64
kalloc_free_bytes(void)
{
struct run *r;
uint64 n = 0;
if(kmem.use_lock)
acquire(&kmem.lock);
for(r = kmem.freelist; r; r = r->next)
n += PGSIZE;
if(kmem.use_lock)
release(&kmem.lock);
return n;
}

231
kernel/core/main.c Normal file
View file

@ -0,0 +1,231 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "x86.h"
#include "irq.h"
#include "dma.h"
static void startothers(void);
static void mpmain(void) __attribute__((noreturn));
extern char end[]; // first address after kernel loaded from ELF file
// Set by stage2main.c (boot/stage2main.c, BOOT_VERBOSE_FLAG_ADDR) when the
// user picks "Boot Schunix (verbose)" from the F8 boot menu; read once here,
// before anything else, so subsystem init can log its own steps.
int boot_verbose;
// Must match BOOT_VERBOSE_FLAG_ADDR in boot/stage2main.c exactly: just past
// the VBE info block, not 0x7000 (startothers() below uses that address for
// the AP boot trampoline).
#define BOOT_VERBOSE_FLAG_ADDR (0x30000 + 0x100)
// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
// doing some setup required for memory allocator to work.
int
kmain(uint32 fb_addr)
{
boot_verbose = *(uchar*)P2V(BOOT_VERBOSE_FLAG_ADDR);
uartearlyinit();
if (boot_verbose) cprintf("verbose boot: uartearlyinit done\n");
// The DMA arena is carved off the top of physical memory *before* kalloc is
// given the rest, because a device needs physically contiguous memory and
// kalloc's free list makes no such promise -- two pages from it are adjacent
// only by chance. Taking it first is the only way to be sure the region
// exists; asking for it later would mean asking kalloc for something it
// fundamentally cannot provide. See include/dma.h.
// Before anything touches PHYSTOP, which is now a variable read from the
// firmware's memory map rather than a constant asserting 224 MB exists.
e820_init();
kinit1(end, P2V(PHYSTOP - DMA_ARENA_BYTES));
if (boot_verbose) cprintf("verbose boot: kinit1 done\n");
dma_init(P2V(PHYSTOP - DMA_ARENA_BYTES), P2V(PHYSTOP));
if (boot_verbose) cprintf("verbose boot: dma_init done\n");
kvmalloc();
if (boot_verbose) cprintf("verbose boot: kvmalloc done\n");
// ACPI first, the legacy MP table only if it finds nothing. Modern firmware
// publishes a complete MADT and an MP table with one processor in it, so
// asking the older interface first means never seeing more than one CPU.
// See kernel/arch/x86_64/acpi.c.
if (!acpi_init())
mpinit();
if (boot_verbose) cprintf("verbose boot: cpu enumeration done\n");
lapicinit();
if (boot_verbose) cprintf("verbose boot: lapicinit done\n");
tvinit();
if (boot_verbose) cprintf("verbose boot: tvinit done\n");
seginit();
if (boot_verbose) cprintf("verbose boot: seginit done\n");
ioapicinit();
if (boot_verbose) cprintf("verbose boot: ioapicinit done\n");
irq_init(); // must precede any driver init that registers a handler
if (boot_verbose) cprintf("verbose boot: irq_init done\n");
// Before the keyboard and mouse, which start producing events the moment
// their handlers are registered.
inputinit();
if (boot_verbose) cprintf("verbose boot: inputinit done\n");
consoleinit();
if (boot_verbose) cprintf("verbose boot: consoleinit done\n");
uartinit();
if (boot_verbose) cprintf("verbose boot: uartinit done\n");
mouseinit();
if (boot_verbose) cprintf("verbose boot: mouseinit done\n");
vga_init(fb_addr);
if (boot_verbose) cprintf("verbose boot: vga_init done (fb=0x%x)\n", fb_addr);
// No splash drawing here on purpose. The loader owns the splash, logo and
// all, and vga_init() deliberately does not clear -- so it is still on the
// display right through to console_takeover(). Painting it again from here
// would land in the last few hundred milliseconds of boot and change nothing.
// After vga_init, because the display driver cross-checks the framebuffer
// address it finds on the bus against the one VBE reported -- and it can
// only do that once vga.c has read the VBE block.
// The devices that are simply known to be on a PC rather than enumerated by
// any bus. Registered before pci_init() so that /proc/devices lists them
// first, which puts the machine's fixed furniture above whatever happened to
// be plugged into it.
platform_register_devices();
pci_init();
if (boot_verbose) cprintf("verbose boot: pci_init done\n");
pinit();
if (boot_verbose) cprintf("verbose boot: pinit done\n");
binit();
if (boot_verbose) cprintf("verbose boot: binit done\n");
fileinit();
if (boot_verbose) cprintf("verbose boot: fileinit done\n");
sockinit();
if (boot_verbose) cprintf("verbose boot: sockinit done\n");
// After the timer is running, so boot_epoch can subtract the uptime so far,
// and before userspace, so the very first timestamp any program takes is
// already real. Reading the RTC is slow, which is exactly why it happens once
// here rather than on every gettimeofday().
time_init();
ideinit();
if (boot_verbose) cprintf("verbose boot: ideinit done\n");
ramdiskinit();
if (boot_verbose) cprintf("verbose boot: ramdiskinit done\n");
// Start the application processors.
//
// This was broken from the beginning of the project and is fixed as of
// 2026-08-08. Five distinct bugs, each hidden behind the one before it, and
// all of them invisible under QEMU because it publishes only one processor in
// the legacy MP table -- see kernel/arch/x86_64/acpi.c. VMware published the
// real count, which is why the triple fault only ever appeared there.
//
// 1. entry64high had no AP branch at all; the check was a comment saying it
// had never been written. Every AP fell into the boot processor's path,
// took the BSP's stack, and ran kmain() a second time.
// 2. seginit() called kalloc() for its per-CPU page. kalloc locks, locking
// reads `cpu`, and `cpu` is what seginit() was about to set up.
// 3. kernel_fs_base was one global for all CPUs, and trapasm.S read it on
// every entry from user mode, so both CPUs shared one per-CPU block.
// 4. `cpu` and `proc` were __thread. That means per *thread*, so the compiler
// cached the thread pointer across calls -- but %fs here is per *CPU*, and
// a kernel thread changes CPU at every sched(). See include/proc.h.
// 5. The new per-CPU slots collided with syscall_entry's scratch word at
// %fs:(8), which is the same memory as %gs:8.
//
int smp_start_aps = 1;
if (smp_start_aps) {
startothers();
if (boot_verbose) cprintf("verbose boot: startothers done\n");
} else if (ncpu > 1) {
cprintf("smp: %d cpus found, running on 1 (AP startup is broken)\n", ncpu);
}
kinit2();
if (boot_verbose) cprintf("verbose boot: kinit2 done\n");
sinit();
if (boot_verbose) cprintf("verbose boot: sinit done\n");
mpmain();
}
// Other CPUs jump here from entryother.S.
void
mpenter(void)
{
switchkvm();
seginit();
lapicinit();
mpmain();
}
// Enable SSE/FXSAVE so user binaries can use XMM registers.
static void
enable_sse(void)
{
uint64 cr0, cr4;
asm volatile("mov %%cr0, %0" : "=r"(cr0));
cr0 &= ~(1UL << 2); // clear CR0.EM (no FP emulation)
cr0 |= (1UL << 1); // set CR0.MP (monitor coprocessor)
asm volatile("mov %0, %%cr0" :: "r"(cr0));
asm volatile("mov %%cr4, %0" : "=r"(cr4));
cr4 |= (1UL << 9); // CR4.OSFXSR — OS handles FXSAVE/FXRSTOR
cr4 |= (1UL << 10); // CR4.OSXMMEXCPT — OS handles SSE exceptions
asm volatile("mov %0, %%cr4" :: "r"(cr4));
}
// Common CPU setup code.
static void
mpmain(void)
{
cprintf("cpu%d: starting\n", cpunum());
enable_sse(); // allow user XMM/SSE instructions
idtinit(); // load idt register
syscallinit(); // syscall set up
xchg(&mycpu()->started, 1); // tell startothers() we're up
scheduler(); // start running processes
}
void entry32mp(void);
// Start the non-boot (AP) processors.
static void
startothers(void)
{
extern uchar _binary_entryother_start[], _binary_entryother_size[];
uchar *code;
struct cpu *c;
char *stack;
// Write entry code to unused memory at 0x7000.
// The linker has placed the image of entryother.S in
// _binary_entryother_start.
code = P2V(0x7000);
memmove(code, _binary_entryother_start,
(addr_t)_binary_entryother_size);
for(c = cpus; c < cpus+ncpu; c++){
if(c == cpus+cpunum()) // We've started already.
continue;
// Tell entryother.S what stack to use, where to enter, and what
// pgdir to use. We cannot use kpgdir yet, because the AP processor
// is running in low memory, so we use entrypgdir for the APs too.
// The AP's per-CPU page, allocated here rather than by the AP itself.
// seginit() explains why: an AP cannot call kalloc() before %fs is set up,
// because kalloc locks and locking reads the per-CPU pointer.
if (c->local == 0) {
c->local = kalloc();
memset(c->local, 0, PGSIZE);
}
stack = kalloc();
*(uint32*)(code-4) = 0x8000; // enough stack to get us to entry64mp
*(uint32*)(code-8) = v2p(entry32mp);
*(uint64*)(code-16) = (uint64) (stack + KSTACKSIZE);
lapicstartap(c->apicid, V2P(code));
// wait for cpu to finish mpmain()
while(c->started == 0)
;
// Counted here, on the boot processor, after the AP has actually reported
// in -- not by the AP itself. This is serialised by the loop, so it needs
// no atomic, and it cannot overcount a processor that started and died.
ncpu_online++;
}
}

543
kernel/core/panic.c Normal file
View file

@ -0,0 +1,543 @@
// SPDX-License-Identifier: Apache-2.0
//
// PSOGM — the Schunix crash screen.
//
// The name is a nod to the Amiga's "Guru Meditation", but nothing else about
// this is a nod to 1985. Three things it is built around:
//
// *It does not clear the screen.* Whatever was being displayed stays there,
// dimmed and pushed back, with the report floating over it. A crash screen that
// wipes the display tells you the machine is gone; one that leaves your work
// visible underneath tells you it stopped, which is both truer and much less
// alarming.
//
// *It is not frightening.* No red, no capitals, no "FATAL". A calm sentence, a
// face that is obviously a cartoon, and then the facts. Whoever is reading this
// is already having a bad time.
//
// *It says considerably more than it needs to.* The screen is the only artefact
// of a kernel panic — there is no core dump and no log to go back to — so
// everything cheap to collect is collected: which CPU, which process, how far
// into the boot, where the kernel is loaded, and a call chain. Anything left
// out here is gone.
//
// Everything is also written to the serial console, in the same order. That is
// what tools/bootcheck.ps1 reads, and it is what you get when the crash happens
// before there is a framebuffer at all.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "x86.h"
#include "proc.h"
#include "spinlock.h"
// Purple, because the name says so, and white on it because nothing else is
// legible at every brightness a monitor might be set to. The palette lives in
// theme.h with everything else; this file used to keep its own copy under the
// same names but with different values, which meant theme.h described a crash
// screen nobody had ever seen.
#include "theme.h"
#include "qr.h"
extern uint ticks;
extern int panicked;
/* ------------------------------------------------------------------ */
/* Small formatting helpers */
/* */
/* cprintf goes to the console, which during a panic is also the screen */
/* being painted over -- so the panel builds its strings itself and */
/* places them with vga_text_at. */
/* ------------------------------------------------------------------ */
static char *
put_str(char *p, char *end, const char *s)
{
while (*s && p < end - 1)
*p++ = *s++;
*p = '\0';
return p;
}
static char *
put_dec(char *p, char *end, uint64 v)
{
char tmp[24];
int i = 0;
if (v == 0)
tmp[i++] = '0';
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
while (i-- && p < end - 1)
*p++ = tmp[i];
*p = '\0';
return p;
}
static char *
put_hex(char *p, char *end, uint64 v, int digits)
{
const char *d = "0123456789abcdef";
if (p < end - 2) { *p++ = '0'; *p++ = 'x'; }
for (int i = digits - 1; i >= 0 && p < end - 1; i--)
*p++ = d[(v >> (i * 4)) & 0xF];
*p = '\0';
return p;
}
/* ------------------------------------------------------------------ */
/* The face */
/* */
/* ":(" — two dots and a parenthesis, drawn large. It is a sad face */
/* only if you tilt your head, which is exactly why it works: it is */
/* legible as a mood without being a picture of anything, and it does */
/* not pretend the machine has feelings about what just happened. */
/* */
/* Drawn from primitives rather than stored as a bitmap, so it scales */
/* with the display and costs no space in a kernel image that stage2 */
/* loads one sector at a time by PIO. */
/* ------------------------------------------------------------------ */
static int
isqrt(int v)
{
if (v <= 0)
return 0;
// Newton's method, iterated only while the estimate is still *decreasing*.
//
// The obvious "stop when it stops changing" version does not terminate: on a
// non-square it settles into a two-cycle rather than a fixed point, and
// isqrt(3) alternates between 1 and 2 forever. In a panic handler that is a
// hang with the screen half drawn and no way to find out why.
int x = v;
int y = (x + 1) / 2;
while (y < x) {
x = y;
y = (x + v / x) / 2;
}
return x;
}
// The "(" of ":(" — the left half of a squashed circle, so the tips point right
// and the middle bulges left, the way the glyph does.
static void
draw_paren(int cx, int cy, int h, int thick, uint32 color)
{
int r = h / 2;
for (int dy = -r; dy <= r; dy++) {
int dx = isqrt(r * r - dy * dy);
// Narrow it: a true half-circle reads as a bracket, not a parenthesis.
int x = cx - (dx * 45) / 100;
vga_fill_circle(x, cy + dy, thick / 2, color);
}
}
static void
draw_sad_face(int x, int y, int h, uint32 color)
{
int dot = h / 12;
int gap = h / 4;
// The colon.
vga_fill_circle(x + dot, y + h / 2 - gap, dot, color);
vga_fill_circle(x + dot, y + h / 2 + gap, dot, color);
// The parenthesis, a little to the right of it.
draw_paren(x + dot * 2 + h / 2, y + h / 2, h, dot * 2, color);
}
/* ------------------------------------------------------------------ */
/* The report */
/* ------------------------------------------------------------------ */
struct field {
const char *label;
char value[72];
};
#define NFIELD 10
static struct field fields[NFIELD];
static int nfields;
static struct field *
field(const char *label)
{
if (nfields >= NFIELD)
return &fields[NFIELD - 1];
struct field *f = &fields[nfields++];
f->label = label;
f->value[0] = '\0';
return f;
}
// Collect everything worth knowing while the machine is still in the state
// that produced the fault. Interrupts are already off and no lock is taken:
// a panic that deadlocks trying to report itself tells you nothing at all.
// A short, shoutable name for the failure, derived from the panic message
// rather than declared at every call site.
//
// Windows stop codes are genuinely useful: they are the thing you search for,
// and they stay the same across builds while the prose around them changes.
// Taking the message up to its first colon (or its first few words) gets that
// for free -- "sysrq: crash requested" becomes SYSRQ, "short filewrite" becomes
// SHORT_FILEWRITE -- with no risk of a call site forgetting to supply one.
static void
stop_code(char *out, int cap, const char *reason)
{
int n = 0;
for (int i = 0; reason[i] && n < cap - 1; i++) {
char c = reason[i];
if (c == ':')
break;
if (c == ' ' || c == '\t') {
if (n == 0 || out[n-1] == '_')
continue;
out[n++] = '_';
continue;
}
if (c >= 'a' && c <= 'z') c = c - 'a' + 'A';
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')
out[n++] = c;
}
while (n > 0 && out[n-1] == '_')
n--;
if (n == 0)
{ out[n++] = 'P'; out[n++] = 'A'; out[n++] = 'N'; out[n++] = 'I'; out[n++] = 'C'; }
out[n] = '\0';
}
static void
gather(char *reason, addr_t *pcs, int npcs)
{
char *e;
struct field *f;
nfields = 0;
f = field("Stop code");
stop_code(f->value, sizeof(f->value), reason);
f = field("Reason");
put_str(f->value, f->value + sizeof(f->value), reason);
f = field("CPU");
e = f->value + sizeof(f->value);
put_dec(f->value, e, mycpu() ? (uint64)mycpu()->id : 0);
if (!mycpu())
put_str(f->value, e, "unknown (very early boot)");
f = field("Process");
e = f->value + sizeof(f->value);
if (myproc()) {
char *p = put_str(f->value, e, myproc()->name);
p = put_str(p, e, " pid ");
put_dec(p, e, (uint64)myproc()->pid);
} else {
put_str(f->value, e, "none -- in the kernel, not on anybody's behalf");
}
f = field("Uptime");
e = f->value + sizeof(f->value);
{
char *p = put_dec(f->value, e, (uint64)ticks);
p = put_str(p, e, " ticks (about ");
p = put_dec(p, e, (uint64)ticks / 100);
put_str(p, e, " s)");
}
// Every address below is a link-time address. Printing where the kernel
// actually sits is what lets addr2line resolve them afterwards, and it is
// the single most useful line here when the trace is all you have.
f = field("Kernel base");
e = f->value + sizeof(f->value);
put_hex(f->value, e, (uint64)KERNBASE, 16);
f = field("Fault PC");
e = f->value + sizeof(f->value);
if (npcs > 0 && pcs[0])
put_hex(f->value, e, (uint64)pcs[0], 16);
else
put_str(f->value, e, "unavailable");
}
/* ------------------------------------------------------------------ */
/* The QR code */
/* */
/* A panic report is only worth what can be carried away from it, and */
/* at this point there is no network, no log to write to, and often no */
/* second machine on the desk. Photographing the screen works but */
/* turns a call chain into something that has to be retyped by hand, */
/* with sixteen hex digits per frame and no way to tell 8 from B. */
/* */
/* A phone camera is a working out-of-band channel that needs no */
/* driver on our side at all. See kernel/core/qr.c. */
/* ------------------------------------------------------------------ */
// Deliberately not a URL. This encodes the report itself, so it goes wherever
// the person holding the phone decides -- and reaches nothing on its own.
//
// Compact because the budget is 134 bytes for the largest symbol this encoder
// builds, and a crash with a deep call chain is exactly when the budget is
// tightest. Two decisions buy most of the room: frames are stored as offsets
// from the kernel base rather than absolute addresses, which halves 16 hex
// digits to 8, and the fields are single letters. The base is included, so
// nothing is lost -- addr2line wants the link-time address and can be given it.
//
// Fields are appended in priority order and the loop simply stops when the
// buffer is full, so a long reason string costs call-chain depth rather than
// producing a report that will not encode.
static int
build_qr_payload(char *buf, int cap, char *reason, addr_t *pcs, int npcs)
{
char *p = buf, *end = buf + cap;
// Magic and format version, so whatever reads this later can tell what it is
// looking at and can change the layout without ambiguity.
p = put_str(p, end, "SXPSOGM1\n");
// The reason, not the stop code: the stop code is derived from the reason by
// stop_code() and carries strictly less, so encoding both would spend bytes
// on something the reader can recompute.
//
// Held back from the end of the buffer so that a long message costs some of
// the call chain but never all of it. A report with a sentence and no
// addresses is much harder to act on than one with addresses and a truncated
// sentence.
{
char *reason_end = end - 34; // three frames and their separators
if (reason_end > p)
p = put_str(p, reason_end, reason);
}
p = put_str(p, end, "\nc");
p = put_dec(p, end, mycpu() ? (uint64)mycpu()->id : 0);
p = put_str(p, end, " p");
p = put_dec(p, end, myproc() ? (uint64)myproc()->pid : 0);
p = put_str(p, end, " t");
p = put_dec(p, end, (uint64)ticks);
if (myproc()) {
p = put_str(p, end, " ");
p = put_str(p, end, myproc()->name);
}
p = put_str(p, end, "\nb");
p = put_hex(p, end, (uint64)KERNBASE, 16);
p = put_str(p, end, "\n");
for (int i = 0; i < npcs && pcs[i]; i++) {
// Nine bytes: eight hex digits and the separator. Stopping here rather than
// letting put_hex truncate matters -- half an address in a crash report is
// worse than one fewer address, because it looks like a real one.
if (end - p < 10)
break;
if (i)
p = put_str(p, end, " ");
// put_hex prefixes "0x", which is 14 wasted bytes across seven frames.
{
const char *d = "0123456789abcdef";
uint64 off = (uint64)pcs[i] - (uint64)KERNBASE;
for (int k = 7; k >= 0; k--)
*p++ = d[(off >> (k * 4)) & 0xF];
*p = '\0';
}
}
return (int)(p - buf);
}
// One static symbol rather than a local: struct qr is about 3.4 KB and the
// panic path runs on whatever stack the fault left behind, which may not have
// that much room. It is only ever touched here, after everything else has
// stopped.
static struct qr panic_qr;
// Returns the width used, so the text column knows what it may not overlap.
static int
draw_qr_panel(int sw, int sh, int ch, char *reason, addr_t *pcs, int npcs)
{
char payload[QR_MAX_BYTES];
int n = build_qr_payload(payload, sizeof(payload), reason, pcs, npcs);
if (n <= 0 || qr_encode(&panic_qr, (unsigned char *)payload, n) != 0)
return 0;
// Four modules of quiet zone, which the standard requires and which decoders
// genuinely need -- without it many will not find the symbol at all.
const int quiet = 4;
int cells = panic_qr.size + 2 * quiet;
// Scale to about a quarter of the display, then clamp. The lower bound is
// what a phone can still resolve from a photograph of a monitor; the upper
// bound stops the thing dominating a screen whose actual job is the text.
int scale = (sw / 4) / cells;
if (scale < 3) scale = 3;
if (scale > 8) scale = 8;
int card = cells * scale;
int x0 = sw - card - sw / 24;
int y0 = sh / 8;
vga_fill_rect(x0, y0, card, card, PSOGM_CARD);
for (int y = 0; y < panic_qr.size; y++)
for (int x = 0; x < panic_qr.size; x++)
if (panic_qr.mod[y][x])
vga_fill_rect(x0 + (x + quiet) * scale, y0 + (y + quiet) * scale,
scale, scale, PSOGM_INK);
vga_text_at(x0, y0 + card + ch, "Scan for a text copy", PSOGM_DIM);
vga_text_at(x0, y0 + card + ch * 2 + 2, "of this report.", PSOGM_DIM);
return card + sw / 24;
}
// The frame pointers, one per line, as one string per row.
static void
trace_line(char *buf, int cap, int i, addr_t pc)
{
char *e = buf + cap;
char *p = put_str(buf, e, " #");
p = put_dec(p, e, (uint64)i);
p = put_str(p, e, i < 10 ? " " : " ");
put_hex(p, e, (uint64)pc, 16);
}
/* ------------------------------------------------------------------ */
static void
paint(char *reason, addr_t *pcs, int npcs)
{
int sw, sh;
vga_dims(&sw, &sh);
int ch = vga_char_h();
int cw = vga_char_w();
// The whole display, one colour. An earlier version dimmed the previous
// screen and floated a panel over it, which looked good and was wrong: the
// thing is called a Purple Screen, and half a screen of somebody's abandoned
// work behind the report is a distraction at the worst possible moment.
vga_clear(PSOGM_BG);
// Glyphs are drawn with their background filled, so the console's cream has
// to become the background or every line of text arrives in its own box.
vga_set_bg(PSOGM_BG);
// The symbol goes first, because it decides how much width the text has. It
// is drawn before anything can fail: if the encoder refuses the payload the
// screen simply has no card and the report is unchanged.
draw_qr_panel(sw, sh, ch, reason, pcs, npcs);
int left = sw / 8;
if (left > 110) left = 110;
int y = sh / 8;
int face_h = 96;
draw_sad_face(left, y, face_h, PSOGM_FG);
y += face_h + ch * 2;
// Left-aligned, not centred. Centred text reads as a poster; this is a
// report, and a report is read down the left edge.
vga_text_at(left, y, "Schunix ran into a problem and needs to restart.",
PSOGM_FG);
y += ch + ch / 2;
vga_text_at(left, y, "Nothing on your disks was touched by this.",
PSOGM_DIM);
// A rule in the boot splash's accent, the same width as the one under the
// wordmark there, tying this screen to the one the machine started with.
//
// Equal space above and below on purpose: at any less it sits close enough to
// the line above to read as an underline of that sentence rather than as a
// divider before the facts.
y += ch * 2;
vga_fill_rect(left, y, 120, 2, SCH_ACCENT);
y += ch;
// Facts: quiet labels down the left, bright values in a second column.
int vx = left + 14 * cw;
for (int i = 0; i < nfields; i++) {
vga_text_at(left, y, fields[i].label, PSOGM_DIM);
vga_text_at(vx, y, fields[i].value, PSOGM_FG);
y += ch + 2;
}
y += ch;
vga_text_at(left, y, "Call chain", PSOGM_DIM);
y += ch + 2;
for (int i = 0; i < npcs && pcs[i]; i++) {
char line[48];
trace_line(line, sizeof(line), i, pcs[i]);
vga_text_at(vx - 3 * cw, y, line, PSOGM_FG);
y += ch;
if (y > sh - ch * 5)
break;
}
// Two closing lines, bottom left: what to do with the numbers above, and
// what this screen is called, so it can be searched for by name. Shortened
// from the full command line, which at 62 characters ran under the QR card.
vga_text_at(left, sh - ch * 3,
"Resolve: addr2line -e kernel <address>", PSOGM_DIM);
vga_text_at(left, sh - ch * 3 + ch + 4,
"PSOGM -- Purple Screen of Guru Meditation", PSOGM_DIM);
}
// Same report, down the serial line. This is the copy that survives: it is what
// bootcheck reads, and the only one that exists when the framebuffer does not.
static void
report_serial(char *reason, addr_t *pcs, int npcs)
{
cprintf("\n");
cprintf(" :(\n\n");
cprintf(" Schunix ran into a problem and needs to restart.\n");
cprintf(" PSOGM -- Purple Screen of Guru Meditation\n\n");
for (int i = 0; i < nfields; i++) {
// Pad the label out so the values line up, without a printf width
// specifier -- cprintf has no such thing.
int n = 0;
while (fields[i].label[n]) n++;
cprintf(" %s", fields[i].label);
for (int pad = n; pad < 14; pad++)
cprintf(" ");
cprintf("%s\n", fields[i].value);
}
cprintf("\n Call chain\n");
for (int i = 0; i < npcs && pcs[i]; i++)
cprintf(" #%d %p\n", i, pcs[i]);
cprintf("\n Resolve with: addr2line -e build/kernel/kernel <address>\n");
cprintf(" Reason: %s\n", reason);
cprintf(" System halted.\n");
}
__attribute__((noreturn))
void
panic(char *s)
{
addr_t pcs[10];
cli();
// Stop every other path from writing to the console. consputc checks this
// and spins, which is what keeps another CPU from painting over the report
// while it is being drawn.
cons_panic_mode();
for (int i = 0; i < 10; i++)
pcs[i] = 0;
getcallerpcs(&s, pcs);
gather(s, pcs, 10);
report_serial(s, pcs, 10);
if (vga_ready())
paint(s, pcs, 10);
panicked = 1;
for (;;)
hlt();
}

456
kernel/core/qr.c Normal file
View file

@ -0,0 +1,456 @@
// QR encoder. See include/qr.h for why it exists and why it is this small.
//
// Freestanding on purpose: no allocation, no floating point, no libc beyond
// memset/memmove. It has to run in panic context, where the heap may be exactly
// what is broken. Every buffer here is either on the caller's struct or a
// bounded local.
//
// The structure follows ISO/IEC 18004 in the order the standard describes it,
// which is also the only order in which any of it can be checked:
//
// 1. encode the payload into data codewords
// 2. append Reed-Solomon error correction, per block
// 3. interleave the blocks
// 4. draw the function patterns (finders, timing, alignment, format area)
// 5. lay the codeword bits into the remaining modules in a zigzag
// 6. try all 8 masks, score each, keep the best
//
// Verified by scanning the output with a phone, which is the only test that
// matters and is also literally the feature.
#ifdef QR_HOST
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
#include "qr.h"
// ---------------------------------------------------------------------------
// Version table
// ---------------------------------------------------------------------------
//
// Per version, at error correction level L: total codewords, error correction
// codewords per block, and number of blocks. These are from the standard's
// tables and cannot be derived, so they are the one part of this file that has
// to be right by transcription rather than by reasoning.
//
// A useful check on them: total - ecc*blocks must be divisible by blocks for
// every row below, because levels 1..6 at L happen to have equally sized
// blocks. qr_encode() asserts exactly that at runtime, which is what turns a
// mistyped number here into a visible failure instead of an unscannable symbol.
static const struct {
short total; // total codewords in the symbol
unsigned char ecc; // EC codewords per block
unsigned char blocks; // number of RS blocks
} vinfo[QR_MAX_VERSION + 1] = {
{ 0, 0, 0 }, // version 0 does not exist; keeps indexing natural
{ 26, 7, 1 },
{ 44, 10, 1 },
{ 70, 15, 1 },
{ 100, 20, 1 },
{ 134, 26, 1 },
{ 172, 18, 2 },
};
int
qr_capacity(int version)
{
if (version < 1 || version > QR_MAX_VERSION)
return 0;
// Data codewords, less the two bytes of framing: a 4-bit mode indicator, an
// 8-bit character count (versions 1-9), and a 4-bit terminator come to
// exactly two codewords' worth of overhead.
int data = vinfo[version].total - vinfo[version].ecc * vinfo[version].blocks;
return data - 2;
}
// ---------------------------------------------------------------------------
// GF(256) and Reed-Solomon
// ---------------------------------------------------------------------------
// Multiply in GF(256) modulo x^8 + x^4 + x^3 + x^2 + 1 (0x11D).
//
// Done by shift-and-add rather than log/exp tables. Two 256-byte tables plus
// their initialisation is more code than this loop, and this needs no init step
// at all -- which matters when the caller is a panic handler that cannot assume
// any particular earlier function ran.
static unsigned char
gmul(unsigned char x, unsigned char y)
{
unsigned char z = 0;
for (int i = 7; i >= 0; i--) {
z = (unsigned char)((z << 1) ^ ((z >> 7) * 0x1D));
z ^= (unsigned char)(((y >> i) & 1) * x);
}
return z;
}
// Coefficients of the degree-`degree` generator polynomial, highest power
// first, with the leading 1 left implicit.
static void
rs_divisor(int degree, unsigned char *result)
{
memset(result, 0, degree);
result[degree - 1] = 1; // start at the monomial x^0
// Multiply by (x - r) for r = 1, a, a^2, ... Each pass shifts the polynomial
// up one degree and folds in the new root.
unsigned char root = 1;
for (int i = 0; i < degree; i++) {
for (int j = 0; j < degree; j++) {
result[j] = gmul(result[j], root);
if (j + 1 < degree)
result[j] ^= result[j + 1];
}
root = gmul(root, 0x02);
}
}
// The remainder of data[] divided by the generator: the EC codewords.
static void
rs_remainder(const unsigned char *data, int len,
const unsigned char *gen, int degree, unsigned char *result)
{
memset(result, 0, degree);
for (int i = 0; i < len; i++) {
unsigned char factor = data[i] ^ result[0];
memmove(&result[0], &result[1], degree - 1);
result[degree - 1] = 0;
for (int j = 0; j < degree; j++)
result[j] ^= gmul(gen[j], factor);
}
}
// ---------------------------------------------------------------------------
// Module access
// ---------------------------------------------------------------------------
static void
set_fn(struct qr *q, int x, int y, int dark)
{
if (x < 0 || y < 0 || x >= q->size || y >= q->size)
return;
q->mod[y][x] = (unsigned char)(dark != 0);
q->fn[y][x] = 1;
}
static int
absi(int v)
{
return v < 0 ? -v : v;
}
static int
cheb(int dx, int dy)
{
int ax = absi(dx), ay = absi(dy);
return ax > ay ? ax : ay;
}
// ---------------------------------------------------------------------------
// Function patterns
// ---------------------------------------------------------------------------
// A finder plus its separator, as one 9x9 stamp centred on (cx, cy).
//
// Writing it by Chebyshev distance rather than as a literal bitmap is not
// cleverness for its own sake: the concentric rings *are* the definition -- dark
// 3x3 core, light ring, dark ring, light separator -- and the edge stamps run
// off the symbol, which set_fn() clips.
static void
draw_finder(struct qr *q, int cx, int cy)
{
for (int dy = -4; dy <= 4; dy++)
for (int dx = -4; dx <= 4; dx++) {
int d = cheb(dx, dy);
set_fn(q, cx + dx, cy + dy, d != 2 && d != 4);
}
}
// The 5x5 alignment pattern: dark 3x3 outline with a light ring inside.
static void
draw_alignment(struct qr *q, int cx, int cy)
{
for (int dy = -2; dy <= 2; dy++)
for (int dx = -2; dx <= 2; dx++)
set_fn(q, cx + dx, cy + dy, cheb(dx, dy) != 1);
}
// The 15-bit format information, twice, plus the always-dark module.
//
// Called once with the real mask at the end, and once early with mask 0 purely
// to *reserve* these modules -- the zigzag below skips anything marked as a
// function pattern, so the reservation has to exist before data is placed.
static void
draw_format(struct qr *q, int mask)
{
// 0b01 is error correction level L. Five data bits, then a BCH(15,5) check
// computed with generator 0x537, then XOR 0x5412 so an all-zero format never
// produces an all-zero pattern.
int data = (1 << 3) | mask;
int rem = data;
for (int i = 0; i < 10; i++)
rem = (rem << 1) ^ ((rem >> 9) * 0x537);
int bits = ((data << 10) | rem) ^ 0x5412;
int n = q->size;
// First copy: down the left of the top-right finder, then left along the top
// of the bottom-left one. The 6/7 and 7/8 jogs skip the timing pattern.
for (int i = 0; i <= 5; i++)
set_fn(q, 8, i, (bits >> i) & 1);
set_fn(q, 8, 7, (bits >> 6) & 1);
set_fn(q, 8, 8, (bits >> 7) & 1);
set_fn(q, 7, 8, (bits >> 8) & 1);
for (int i = 9; i < 15; i++)
set_fn(q, 14 - i, 8, (bits >> i) & 1);
// Second copy, so the format survives damage to either corner.
for (int i = 0; i < 8; i++)
set_fn(q, n - 1 - i, 8, (bits >> i) & 1);
for (int i = 8; i < 15; i++)
set_fn(q, 8, n - 15 + i, (bits >> i) & 1);
set_fn(q, 8, n - 8, 1); // the dark module, always set, always here
}
static void
draw_function_patterns(struct qr *q)
{
int n = q->size;
// Timing patterns first: the finders overwrite their ends, which is correct.
for (int i = 0; i < n; i++) {
set_fn(q, 6, i, i % 2 == 0);
set_fn(q, i, 6, i % 2 == 0);
}
draw_finder(q, 3, 3);
draw_finder(q, n - 4, 3);
draw_finder(q, 3, n - 4);
// For versions 2-6 the alignment centres are {6, n-7}, and three of those
// four combinations collide with a finder. Only the bottom-right survives,
// which is why there is no position table here. Version 7 would need one.
if (q->size > 21)
draw_alignment(q, n - 7, n - 7);
draw_format(q, 0); // reserve; overwritten once a mask is chosen
}
// ---------------------------------------------------------------------------
// Masking
// ---------------------------------------------------------------------------
static int
mask_bit(int mask, int x, int y)
{
switch (mask) {
case 0: return (x + y) % 2 == 0;
case 1: return y % 2 == 0;
case 2: return x % 3 == 0;
case 3: return (x + y) % 3 == 0;
case 4: return (x / 3 + y / 2) % 2 == 0;
case 5: return x * y % 2 + x * y % 3 == 0;
case 6: return (x * y % 2 + x * y % 3) % 2 == 0;
case 7: return ((x + y) % 2 + x * y % 3) % 2 == 0;
}
return 0;
}
static void
apply_mask(struct qr *q, int mask)
{
for (int y = 0; y < q->size; y++)
for (int x = 0; x < q->size; x++)
if (!q->fn[y][x])
q->mod[y][x] ^= (unsigned char)mask_bit(mask, x, y);
}
// The standard's four penalty rules. Lower is better.
//
// A decoder will read any legal mask, so this is not correctness -- it is scan
// reliability, which is the whole point when the reader is a handheld camera
// pointed at a screen. Rule 3 in particular exists to stop the data accidentally
// containing something that looks like a finder pattern.
static int
penalty(const struct qr *q)
{
int n = q->size, score = 0, dark = 0;
for (int y = 0; y < n; y++) {
for (int x = 0; x < n; x++)
dark += q->mod[y][x];
// Rule 1, both directions: runs of five or more identical modules.
for (int dir = 0; dir < 2; dir++) {
int run = 1;
int prev = dir ? q->mod[0][y] : q->mod[y][0];
for (int i = 1; i < n; i++) {
int cur = dir ? q->mod[i][y] : q->mod[y][i];
if (cur == prev) {
run++;
} else {
if (run >= 5) score += 3 + (run - 5);
run = 1;
prev = cur;
}
}
if (run >= 5) score += 3 + (run - 5);
}
}
// Rule 2: every 2x2 block of one colour.
for (int y = 0; y + 1 < n; y++)
for (int x = 0; x + 1 < n; x++) {
int c = q->mod[y][x];
if (c == q->mod[y][x + 1] && c == q->mod[y + 1][x] && c == q->mod[y + 1][x + 1])
score += 3;
}
// Rule 3: the finder-lookalike 1011101 with four light modules on either
// side, in either direction.
for (int dir = 0; dir < 2; dir++)
for (int a = 0; a < n; a++) {
unsigned bits = 0;
for (int b = 0; b < n; b++) {
int m = dir ? q->mod[b][a] : q->mod[a][b];
bits = ((bits << 1) | (unsigned)m) & 0x7FF;
if (b >= 10 && (bits == 0x05D || bits == 0x5D0))
score += 40;
}
}
// Rule 4: deviation from an even split of dark and light, in 5% steps.
int total = n * n;
int k = (absi(dark * 20 - total * 10) + total - 1) / total;
score += k * 10;
return score;
}
// ---------------------------------------------------------------------------
// Encode
// ---------------------------------------------------------------------------
static void
put_bits(unsigned char *buf, int *pos, unsigned val, int n)
{
for (int i = n - 1; i >= 0; i--) {
buf[*pos >> 3] |= (unsigned char)(((val >> i) & 1) << (7 - (*pos & 7)));
(*pos)++;
}
}
int
qr_encode(struct qr *q, const unsigned char *data, int len)
{
int v;
if (len < 0)
return -1;
for (v = 1; v <= QR_MAX_VERSION; v++)
if (len <= qr_capacity(v))
break;
if (v > QR_MAX_VERSION)
return -1;
int total = vinfo[v].total;
int ecclen = vinfo[v].ecc;
int blocks = vinfo[v].blocks;
int datacw = total - ecclen * blocks;
// The table claims equal-sized blocks for every version this file supports.
// If that is ever untrue the interleave below is silently wrong, so check it
// rather than trust it.
if (datacw % blocks != 0)
return -1;
int blocklen = datacw / blocks;
// --- 1. payload -> data codewords ---------------------------------------
unsigned char cw[172]; // >= max total codewords
memset(cw, 0, sizeof(cw));
int pos = 0;
put_bits(cw, &pos, 0x4, 4); // byte mode
put_bits(cw, &pos, (unsigned)len, 8);
for (int i = 0; i < len; i++)
put_bits(cw, &pos, data[i], 8);
// Terminator, up to four zero bits, then round up to a whole codeword.
int spare = datacw * 8 - pos;
put_bits(cw, &pos, 0, spare < 4 ? spare : 4);
if (pos & 7)
put_bits(cw, &pos, 0, 8 - (pos & 7));
// Fill the rest with the standard alternating pad bytes. They are specified
// values, not arbitrary filler: a decoder uses the character count to know
// where the data ends, but the pad pattern keeps the symbol from having large
// uniform regions that mask selection would then have to fight.
for (int i = pos / 8; i < datacw; i++)
cw[i] = (i - pos / 8) % 2 == 0 ? 0xEC : 0x11;
// --- 2 and 3. error correction, then interleave -------------------------
unsigned char gen[30];
unsigned char ec[4][30];
rs_divisor(ecclen, gen);
for (int b = 0; b < blocks; b++)
rs_remainder(&cw[b * blocklen], blocklen, gen, ecclen, ec[b]);
unsigned char out[172];
int o = 0;
for (int i = 0; i < blocklen; i++)
for (int b = 0; b < blocks; b++)
out[o++] = cw[b * blocklen + i];
for (int i = 0; i < ecclen; i++)
for (int b = 0; b < blocks; b++)
out[o++] = ec[b][i];
// --- 4. function patterns ------------------------------------------------
memset(q, 0, sizeof(*q));
q->size = 17 + 4 * v;
draw_function_patterns(q);
// --- 5. zigzag placement -------------------------------------------------
//
// Two-module-wide columns walked from the right edge leftwards, alternating
// upward and downward. Column 6 is skipped entirely because it is the vertical
// timing pattern -- without that step every subsequent column lands one place
// out and the symbol is quietly unreadable.
int bit = 0;
int nbits = o * 8;
for (int right = q->size - 1; right >= 1; right -= 2) {
if (right == 6)
right = 5;
for (int vert = 0; vert < q->size; vert++) {
for (int j = 0; j < 2; j++) {
int x = right - j;
int upward = ((right + 1) & 2) == 0;
int y = upward ? q->size - 1 - vert : vert;
if (!q->fn[y][x] && bit < nbits) {
q->mod[y][x] = (unsigned char)((out[bit >> 3] >> (7 - (bit & 7))) & 1);
bit++;
}
}
}
}
// --- 6. pick a mask ------------------------------------------------------
int best = 0, bestscore = 0;
for (int m = 0; m < 8; m++) {
apply_mask(q, m);
draw_format(q, m);
int s = penalty(q);
if (m == 0 || s < bestscore) {
bestscore = s;
best = m;
}
apply_mask(q, m); // XOR again to undo; masking is involutive
}
apply_mask(q, best);
draw_format(q, best);
return 0;
}

55
kernel/core/sleeplock.c Normal file
View file

@ -0,0 +1,55 @@
// Sleeping locks
#include "types.h"
#include "defs.h"
#include "param.h"
#include "x86.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "spinlock.h"
#include "sleeplock.h"
void
initsleeplock(struct sleeplock *lk, char *name)
{
initlock(&lk->lk, "sleep lock");
lk->name = name;
lk->locked = 0;
lk->pid = 0;
}
void
acquiresleep(struct sleeplock *lk)
{
acquire(&lk->lk);
while (lk->locked) {
sleep(lk, &lk->lk);
}
lk->locked = 1;
if (myproc()) {
lk->pid = myproc()->pid;
} else {
lk->pid = 0;
}
release(&lk->lk);
}
void
releasesleep(struct sleeplock *lk)
{
acquire(&lk->lk);
lk->locked = 0;
lk->pid = 0;
wakeup(lk);
release(&lk->lk);
}
int
holdingsleep(struct sleeplock *lk)
{
acquire(&lk->lk);
int r = lk->locked;
release(&lk->lk);
return r;
}

115
kernel/core/spinlock.c Normal file
View file

@ -0,0 +1,115 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "x86.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "spinlock.h"
void
initlock(struct spinlock *lk, char *name)
{
lk->name = name;
lk->locked = 0;
lk->cpu = 0;
}
void
acquire(struct spinlock *lk)
{
pushcli();
if(holding(lk))
panic("acquire");
{
uint64 spins = 0;
while(xchg(&lk->locked, 1) != 0){
if(++spins == 200000000ULL){
cprintf("acquire: stuck on \"%s\" held by cpu=%p pcs0=%p pcs1=%p pcs2=%p, acquirer cpu=%p\n",
lk->name, lk->cpu, lk->pcs[0], lk->pcs[1], lk->pcs[2], mycpu());
panic("acquire: deadlock");
}
}
}
__sync_synchronize();
lk->cpu = mycpu();
getcallerpcs(&lk, lk->pcs);
}
void
release(struct spinlock *lk)
{
if(!holding(lk))
panic("release");
lk->pcs[0] = 0;
lk->cpu = 0;
__sync_synchronize();
asm volatile("movq $0, %0" : "+m" (lk->locked) : );
popcli();
}
void
getcallerpcs(void *v, addr_t pcs[])
{
addr_t *rbp;
asm volatile("mov %%rbp, %0" : "=r" (rbp));
getstackpcs(rbp, pcs);
}
void
getstackpcs(addr_t *rbp, addr_t pcs[])
{
int i;
for(i = 0; i < 10; i++){
if(rbp == 0 || rbp < (addr_t*)KERNBASE || rbp == (addr_t*)0xffffffff)
break;
pcs[i] = rbp[1];
rbp = (addr_t*)rbp[0];
}
for(; i < 10; i++)
pcs[i] = 0;
}
int
holding(struct spinlock *lock)
{
return lock->locked && lock->cpu == mycpu();
}
void
pushcli(void)
{
int eflags;
eflags = readeflags();
cli();
if(mycpu() == 0){
extern uint64 kernel_fs_base;
cprintf("pushcli: cpu==NULL cs=0x%p kernel_fs_base=0x%p actual_fs_base=0x%p\n",
readcs(), kernel_fs_base, rdmsr(0xC0000100));
panic("pushcli: cpu is NULL");
}
if(mycpu()->ncli == 0)
mycpu()->intena = eflags & FL_IF;
mycpu()->ncli += 1;
}
void
popcli(void)
{
if(readeflags()&FL_IF)
panic("popcli - interruptible");
if(--mycpu()->ncli < 0)
panic("popcli");
if(mycpu()->ncli == 0 && mycpu()->intena)
sti();
}

113
kernel/core/string.c Normal file
View file

@ -0,0 +1,113 @@
#include "types.h"
#include "x86.h"
void*
memset(void *dst, int c, uint64 n)
{
uint64 c64;
if ((addr_t)dst % 8 == 0 && n % 8 == 0) {
c &= 0xFF;
c64 = (uint64)c;
c64 = (c64 << 56) | (c64 << 48) | (c64 << 40) | (c64 << 32) |
(c64 << 24) | (c64 << 16) | (c64 << 8) | c64;
stosq(dst, c64, n / 8);
} else {
stosb(dst, c, n);
}
return dst;
}
int
memcmp(const void *v1, const void *v2, uint n)
{
const uchar *s1, *s2;
s1 = v1;
s2 = v2;
while(n-- > 0){
if(*s1 != *s2)
return *s1 - *s2;
s1++, s2++;
}
return 0;
}
void*
memmove(void *dst, const void *src, uint n)
{
const char *s;
char *d;
s = src;
d = dst;
if(s < d && s + n > d){
s += n;
d += n;
while(n-- > 0)
*--d = *--s;
} else
while(n-- > 0)
*d++ = *s++;
return dst;
}
void*
memcpy(void *dst, const void *src, uint n)
{
return memmove(dst, src, n);
}
int
strncmp(const char *p, const char *q, uint n)
{
while(n > 0 && *p && *p == *q)
n--, p++, q++;
if(n == 0)
return 0;
return (uchar)*p - (uchar)*q;
}
char*
strncpy(char *s, const char *t, int n)
{
char *os = s;
while(n-- > 0 && (*s++ = *t++) != 0)
;
while(n-- > 0)
*s++ = 0;
return os;
}
char*
safestrcpy(char *s, const char *t, int n)
{
char *os = s;
if(n <= 0)
return os;
while(--n > 0 && (*s++ = *t++) != 0)
;
*s = 0;
return os;
}
int
strlen(const char *s)
{
int n;
for(n = 0; s[n]; n++)
;
return n;
}
int
strnlen(const char *s, int max)
{
int n;
for(n = 0; n < max && s[n]; n++)
;
return n;
}

116
kernel/core/time.c Normal file
View file

@ -0,0 +1,116 @@
// Wall-clock time.
//
// The kernel has always known how long it had been running -- `ticks`, bumped
// 100 times a second by the timer interrupt -- and has never known what time it
// was. sys_gettimeofday() returned `ticks / 100` as though it were seconds
// since the epoch, so every Schunix machine believed it was the 1st of January
// 1970 and that the date advanced at the rate of its own uptime. `date` printed
// a number that barely moved, which is what it looks like when a clock is
// counting from the wrong place rather than not counting at all.
//
// cmostime() in kernel/arch/x86_64/lapic.c has been able to read the hardware
// clock the whole time. Nothing called it.
//
// The arrangement here is the usual one: read the RTC once at boot, convert it
// to a Unix timestamp, and add uptime to it thereafter. The RTC is slow to read
// (it is polled over two I/O ports and has to be sampled twice to catch it
// mid-update) so reading it on every gettimeofday would be absurd; and `ticks`
// is a perfectly good interval clock once it has something to count from.
#include "types.h"
#include "defs.h"
#include "date.h"
#include "param.h"
#include "spinlock.h"
extern uint ticks;
// Seconds between the Unix epoch and the moment this machine booted. Zero until
// time_init() runs, which makes the behaviour before that point exactly what it
// used to be -- uptime presented as absolute time -- rather than something new
// and differently wrong.
uint64 boot_epoch;
// Days from 1970-01-01 to the given civil date, proleptic Gregorian.
//
// Howard Hinnant's days_from_civil. It is worth using rather than a loop over
// years because it is branchless, exact for any year, and has no table to get
// wrong; the alternative -- accumulating 365 or 366 days per year since 1970 --
// gets leap centuries wrong unless you remember that 1900 was not a leap year
// and 2000 was, which is precisely the sort of thing that is discovered on a
// February morning years later.
//
// The trick is shifting the year so that March is month 1, which puts the leap
// day at the *end* of the year and makes the leap rule depend only on the year
// number rather than on where in the year you are.
static int64
days_from_civil(int64 y, unsigned m, unsigned d)
{
y -= m <= 2;
const int64 era = (y >= 0 ? y : y - 399) / 400;
const unsigned yoe = (unsigned)(y - era * 400); // [0, 399]
const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
return era * 146097 + (int64)doe - 719468;
}
// Read the hardware clock and work out when we booted.
//
// The RTC is read in UTC. That is not universally true on a PC -- a machine
// that has had Windows installed on it usually keeps the RTC in local time --
// but there is nowhere to store a timezone yet and no way to ask, and UTC is
// the honest default for a system with no configuration. When there is a
// settings store, this is where the offset goes.
void
time_init(void)
{
struct rtcdate r;
cmostime(&r);
// cmostime() returns a two-digit year on hardware that only stores one.
// 2000-2099 is the right guess now and stays right for a lifetime; a machine
// reporting 70 means the battery is flat, and the epoch is as good an answer
// as any for that.
int64 year = r.year;
if (year < 100)
year += 2000;
// A wildly implausible reading means the CMOS is not telling us anything.
// Leaving boot_epoch at zero keeps the old behaviour, which is wrong but
// predictable, rather than putting the machine somewhere in the Bronze Age.
if (r.month < 1 || r.month > 12 || r.day < 1 || r.day > 31 ||
r.hour > 23 || r.minute > 59 || r.second > 60 || year < 1970) {
cprintf("time: CMOS clock is not plausible, staying at the epoch\n");
return;
}
int64 days = days_from_civil(year, r.month, r.day);
uint64 now = (uint64)(days * 86400) + r.hour * 3600 + r.minute * 60 + r.second;
// Subtract the uptime so far, so boot_epoch is the moment of boot rather than
// the moment this function ran. It is only a fraction of a second, but it
// keeps boot_epoch meaning exactly what its name says.
uint64 up = ticks / 100;
boot_epoch = now > up ? now - up : now;
cprintf("time: %d-%d-%d %d:%d:%d UTC from the hardware clock\n",
(int)year, r.month, r.day, r.hour, r.minute, r.second);
}
// Seconds and microseconds since the epoch. One place, so gettimeofday,
// clock_gettime and anything added later cannot disagree about what time it is.
void
wallclock(uint64 *sec, uint64 *usec)
{
uint xticks;
acquire(&tickslock);
xticks = ticks;
release(&tickslock);
if (sec)
*sec = boot_epoch + xticks / 100;
if (usec)
*usec = (uint64)(xticks % 100) * 10000;
}

79
kernel/core/uaccess.c Normal file
View file

@ -0,0 +1,79 @@
// User-memory access helpers — see include/uaccess.h for the rules.
//
// Everything here validates against the *current* process's page table
// (myproc()->pgdir), which is also the live CR3 while a syscall runs, so a
// range that passes access_ok() can be dereferenced directly by kernel
// code for the remainder of the syscall (single-threaded processes can't
// unmap it out from under us mid-call).
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "errno.h"
#include "uaccess.h"
int
access_ok(addr_t uaddr, uint64 size)
{
addr_t a, end;
if(size == 0)
return 1;
end = uaddr + size;
if(end < uaddr || end > KERNBASE)
return 0;
for(a = PGROUNDDOWN(uaddr); a < end; a += PGSIZE)
if(uva2ka(myproc()->pgdir, (char*)a) == 0)
return 0;
return 1;
}
void*
user_ptr(addr_t uaddr, uint64 size)
{
if(uaddr == 0 || !access_ok(uaddr, size))
return 0;
return (void*)uaddr;
}
int
copy_from_user(void *dst, addr_t uaddr, uint64 n)
{
if(uaddr + n < uaddr || uaddr + n > KERNBASE)
return -EFAULT;
if(copyin(myproc()->pgdir, uaddr, dst, n) < 0)
return -EFAULT;
return 0;
}
int
copy_to_user(addr_t uaddr, const void *src, uint64 n)
{
if(uaddr + n < uaddr || uaddr + n > KERNBASE)
return -EFAULT;
if(copyout(myproc()->pgdir, uaddr, (void*)src, n) < 0)
return -EFAULT;
return 0;
}
long
strlen_user(addr_t uaddr)
{
addr_t a = uaddr;
while(a - uaddr < USER_STR_MAX){
// Scan through the per-page kernel alias so an unmapped page is a
// clean failure instead of a fault.
char *ka = uva2ka(myproc()->pgdir, (char*)a);
if(ka == 0)
return -EFAULT;
addr_t pgend = PGROUNDDOWN(a) + PGSIZE;
for(; a < pgend; a++, ka++)
if(*ka == 0)
return a - uaddr;
}
return -EFAULT;
}

160
kernel/dev/bio.c Normal file
View file

@ -0,0 +1,160 @@
// Buffer cache.
//
// The buffer cache is a linked list of buf structures holding
// cached copies of disk block contents. Caching disk blocks
// in memory reduces the number of disk reads and also provides
// a synchronization point for disk blocks used by multiple processes.
//
// Interface:
// * To get a buffer for a particular disk block, call bread.
// * After changing buffer data, call bwrite to write it to disk.
// * When done with the buffer, call brelse.
// * Do not use the buffer after calling brelse.
// * Only one process at a time can use a buffer,
// so do not keep them longer than necessary.
//
// The implementation uses two state flags internally:
// * B_VALID: the buffer data has been read from the disk.
// * B_DIRTY: the buffer data has been modified
// and needs to be written to disk.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
struct {
struct spinlock lock;
struct buf buf[NBUF];
// Linked list of all buffers, through prev/next.
// head.next is most recently used.
struct buf head;
} bcache;
void
binit(void)
{
struct buf *b;
initlock(&bcache.lock, "bcache");
//PAGEBREAK!
// Create linked list of buffers
bcache.head.prev = &bcache.head;
bcache.head.next = &bcache.head;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
b->next = bcache.head.next;
b->prev = &bcache.head;
initsleeplock(&b->lock, "buffer");
bcache.head.next->prev = b;
bcache.head.next = b;
}
}
// Route a request to the driver that owns b->dev. Everything above this line
// is device-agnostic; everything below picks a backend. RAMDISK_DEV is served
// out of memory (kernel/dev/ramdisk.c), USBDISK_DEV by bulk transfers to a
// USB stick, everything else by the IDE driver.
static void
bdevrw(struct buf *b)
{
if(b->dev == RAMDISK_DEV){
ramdiskrw(b);
return;
}
if(b->dev == USBDISK_DEV){
usb_storage_rw(b);
return;
}
iderw(b);
}
// Look through buffer cache for block on device dev.
// If not found, allocate a buffer.
// In either case, return locked buffer.
static struct buf*
bget(uint dev, uint blockno)
{
struct buf *b;
acquire(&bcache.lock);
// Is the block already cached?
for(b = bcache.head.next; b != &bcache.head; b = b->next){
if(b->dev == dev && b->blockno == blockno){
b->refcnt++;
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
// Not cached; recycle some unused buffer and clean buffer
// "clean" because B_DIRTY and not locked means log.c
// hasn't yet committed the changes to the buffer.
for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
if(b->refcnt == 0 && (b->flags & B_DIRTY) == 0) {
b->dev = dev;
b->blockno = blockno;
b->flags = 0;
b->refcnt = 1;
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
panic("bget: no buffers");
}
// Return a locked buf with the contents of the indicated block.
struct buf*
bread(uint dev, uint blockno)
{
struct buf *b;
b = bget(dev, blockno);
if(!(b->flags & B_VALID)) {
bdevrw(b);
}
return b;
}
// Write b's contents to disk. Must be locked.
void
bwrite(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("bwrite");
b->flags |= B_DIRTY;
bdevrw(b);
}
// Release a locked buffer.
// Move to the head of the MRU list.
void
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse");
releasesleep(&b->lock);
acquire(&bcache.lock);
b->refcnt--;
if (b->refcnt == 0) {
// no one is waiting for it.
b->next->prev = b->prev;
b->prev->next = b->next;
b->next = bcache.head.next;
b->prev = &bcache.head;
bcache.head.next->prev = b;
bcache.head.next = b;
}
release(&bcache.lock);
}
//PAGEBREAK!

107
kernel/dev/cdev.c Normal file
View file

@ -0,0 +1,107 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "x86.h"
#include "errno.h"
extern int consoleread(struct inode*, uint, char*, int);
extern int consolewrite(struct inode*, uint, char*, int);
extern int mouseread(struct inode*, uint, char*, int);
static uint64 rseed = 0x2A99554433221100ULL;
int
nullwrite(struct inode *ip, uint off, char *dst, int n)
{
return n;
}
int
nullread(struct inode *ip, uint off, char *dst, int n)
{
return 0;
}
int
zeroread(struct inode *ip, uint off, char *dst, int n)
{
memset(dst, 0, n);
return n;
}
int
randomread(struct inode *ip, uint off, char *dst, int n)
{
for(int i = 0; i < n; i++){
rseed = rseed * 6364136223846793005ULL + 1442695040888963407ULL;
dst[i] = (rseed >> 32) & 0xFF;
}
return n;
}
// /dev/full — reads as an endless run of zero bytes, every write fails with
// ENOSPC. Programs use it to exercise their out-of-disk-space paths.
int
fullwrite(struct inode *ip, uint off, char *dst, int n)
{
return -ENOSPC;
}
int
mousewrite(struct inode *ip, uint off, char *dst, int n)
{
return n;
}
int
memread(struct inode *ip, uint off, char *dst, int n)
{
switch(ip->minor) {
case 1: return consoleread(ip, off, dst, n);
case 2: return nullread(ip, off, dst, n);
case 3: return consoleread(ip, off, dst, n);
case 5: return zeroread(ip, off, dst, n);
case 7: return zeroread(ip, off, dst, n); // /dev/full reads as zeros
case 8:
case 9: return randomread(ip, off, dst, n);
default: return -1;
}
}
int
memwrite(struct inode *ip, uint off, char *dst, int n)
{
switch(ip->minor) {
case 1: return consolewrite(ip, off, dst, n);
case 7: return fullwrite(ip, off, dst, n); // /dev/full: always ENOSPC
case 2:
case 5:
case 8:
case 9: return nullwrite(ip, off, dst, n);
default: return -1;
}
}
struct devsw devsw[] = {
[0] = { 0, 0 },
[1] = { memread, memwrite },
// /dev/fb0 (major 2) — the framebuffer, kernel/dev/fb.c. This used to be
// { nullread, nullwrite }, so writing pixels to it succeeded and did
// nothing.
// The close hook gives the text console its screen back when whatever was
// drawing on the framebuffer exits — see fbclose().
[2] = { fbread, fbwrite, 0, fbclose },
[3] = { mouseread, mousewrite },
[4] = { 0, 0 },
// /dev/tty (major 5) — alias for the console
[5] = { memread, memwrite },
// /proc status files (major 6) — see kernel/dev/procfs.c
[6] = { procread, procwrite },
// /dev/input/eventN (major 7) — raw evdev events, kernel/dev/input.c. The
// minor is the device index, so event0 is the keyboard and event1 the mouse.
[7] = { inputread, inputwrite, inputready, inputclose, inputopen },
};

308
kernel/dev/device.c Normal file
View file

@ -0,0 +1,308 @@
// The device model. See include/device.h for why it exists.
//
// A fixed table rather than a linked list: this is populated once during boot
// by whichever bus found each device, and read afterwards. Nothing is removed
// yet -- hot-unplug would need it, and when that arrives the in_use flag is
// already the hook for it.
//
// No lock. Every device_add() happens during single-threaded boot, before
// userspace exists, and every reader afterwards only walks. If a bus ever
// enumerates at runtime -- USB hotplug is the obvious one -- this needs a
// spinlock, and the reader in procfs needs to take it.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "device.h"
#include "display.h"
#include "theme.h"
#include "text.h"
static struct device devices[NDEVICE];
static int ndevices;
static const char *bus_names[DEVBUS_NBUS] = {
[DEVBUS_PLATFORM] = "platform",
[DEVBUS_PCI] = "pci",
[DEVBUS_USB] = "usb",
};
static const char *class_names[DEVCLASS_NCLASS] = {
[DEVCLASS_OTHER] = "other",
[DEVCLASS_DISPLAY] = "display",
[DEVCLASS_INPUT] = "input",
[DEVCLASS_STORAGE] = "storage",
[DEVCLASS_NET] = "net",
[DEVCLASS_BUS] = "bus",
};
const char *
dev_bus_name(enum dev_bus b)
{
if (b < 0 || b >= DEVBUS_NBUS || !bus_names[b])
return "?";
return bus_names[b];
}
const char *
dev_class_name(enum dev_class c)
{
if (c < 0 || c >= DEVCLASS_NCLASS || !class_names[c])
return "?";
return class_names[c];
}
// How many devices already share this base name, so "vga" becomes vga0, vga1.
static int
count_base(const char *base)
{
int n = 0;
for (int i = 0; i < ndevices; i++) {
const char *a = devices[i].name, *b = base;
while (*b && *a == *b) { a++; b++; }
// Matched the whole base and the rest is the index we appended.
if (*b == '\0' && *a >= '0' && *a <= '9')
n++;
}
return n;
}
struct device *
device_add(const char *base, enum dev_bus bus, enum dev_class class,
const char *driver)
{
if (ndevices >= NDEVICE)
return 0;
struct device *d = &devices[ndevices++];
d->bus = bus;
d->class = class;
d->driver = driver;
d->in_use = 1;
int idx = count_base(base);
int n = 0;
while (base[n] && n < DEV_NAMELEN - 3) {
d->name[n] = base[n];
n++;
}
// Single digit is enough for NDEVICE of 32 per base name, and a two-digit
// path that is never taken is a two-digit path that is never tested.
if (idx > 9)
idx = 9;
d->name[n++] = (char)('0' + idx);
d->name[n] = '\0';
return d;
}
void
device_set_desc(struct device *d, const char *s)
{
if (!d)
return;
int n = 0;
while (s[n] && n < (int)sizeof(d->desc) - 1) {
d->desc[n] = s[n];
n++;
}
d->desc[n] = '\0';
}
struct device *
device_at(int i)
{
if (i < 0 || i >= ndevices)
return 0;
return &devices[i];
}
int
device_count(void)
{
return ndevices;
}
struct device *
device_find_class(enum dev_class class)
{
for (int i = 0; i < ndevices; i++)
// Unclaimed devices are deliberately skipped. A display nobody drives is
// not a display you can draw on, and returning it would push the "is there
// a driver" check out to every caller.
if (devices[i].class == class && devices[i].driver)
return &devices[i];
return 0;
}
// --- the display class -----------------------------------------------------
//
// Thin wrappers over device_find_class() plus a null check on each op. They
// exist so that consumers are not each written around a vtable that may be
// absent -- a compositor asking to present on a machine with no display driver
// should be a no-op, not a null dereference in the middle of a frame.
struct device *
display_get(void)
{
return device_find_class(DEVCLASS_DISPLAY);
}
int
display_mode(struct display_mode *out)
{
struct device *d = display_get();
if (!d || !d->ops || !((struct display_ops *)d->ops)->mode)
return -1;
((struct display_ops *)d->ops)->mode(d, out);
return 0;
}
void *
display_acquire(uint32 *stride_out)
{
struct device *d = display_get();
if (!d || !d->ops || !((struct display_ops *)d->ops)->acquire)
return 0;
return ((struct display_ops *)d->ops)->acquire(d, stride_out);
}
void
display_present(int x, int y, int w, int h)
{
struct device *d = display_get();
if (!d || !d->ops || !((struct display_ops *)d->ops)->present)
return;
((struct display_ops *)d->ops)->present(d, x, y, w, h);
}
void
display_wait_vblank(void)
{
struct device *d = display_get();
if (!d || !d->ops || !((struct display_ops *)d->ops)->wait_vblank)
return;
((struct display_ops *)d->ops)->wait_vblank(d);
}
// Drive the whole display path once and report what happened: find the class,
// read the mode, acquire a back buffer, draw into RAM, present it.
//
// A self-test rather than a unit test because none of this can be checked
// without a display: the assertion is "did the right pixels reach the screen",
// and the only instrument for that is a screendump. Triggered with
// `echo d > /proc/sysrq` and photographed by tools/dispshot.ps1.
//
// The pattern is chosen to be wrong in visible ways. Four solid quadrants in
// theme colours catch a stride error (the bands would slant) and a bpp error
// (the colours would be wrong); the white frame one pixel inside the edge
// catches an off-by-one in the present rectangle; and the fact that it is
// drawn entirely into RAM before a single byte reaches the framebuffer is what
// double buffering means.
void
display_selftest(void)
{
struct display_mode m;
uint32 stride;
if (display_mode(&m) != 0) {
cprintf("display: no display bound\n");
return;
}
cprintf("display: %dx%d %dbpp stride %d\n", m.width, m.height, m.bpp, m.stride);
unsigned char *b = display_acquire(&stride);
if (!b) {
cprintf("display: no back buffer available\n");
return;
}
static const uint32 quad[4] = {
SCH_ACCENT, SCH_ACCENT_DIM, SCH_DIM, SCH_BG
};
for (int y = 0; y < m.height; y++) {
uint32 *row = (uint32 *)(b + (uint64)y * stride);
int top = y < m.height / 2;
for (int x = 0; x < m.width; x++) {
int left = x < m.width / 2;
row[x] = quad[(top ? 0 : 2) + (left ? 0 : 1)];
}
}
for (int x = 1; x < m.width - 1; x++) {
*(uint32 *)(b + 1 * (uint64)stride + x * 4) = SCH_FG;
*(uint32 *)(b + (uint64)(m.height - 2) * stride + x * 4) = SCH_FG;
}
for (int y = 1; y < m.height - 1; y++) {
*(uint32 *)(b + (uint64)y * stride + 1 * 4) = SCH_FG;
*(uint32 *)(b + (uint64)y * stride + (m.width - 2) * 4) = SCH_FG;
}
// Between frames, so the blit does not land halfway down a scan-out.
display_wait_vblank();
display_present(0, 0, m.width, m.height);
cprintf("display: presented a full frame\n");
}
// Draw a text specimen through the display class. `echo t > /proc/sysrq`.
//
// It exercises the three things a character-cell console cannot do, so a
// screenshot says whether the layer works rather than whether it compiled:
// glyphs on a common baseline at arbitrary x (not multiples of eight), text
// blended over an existing background rather than drawn into cleared cells, and
// codepoints above 255.
void
text_selftest(void)
{
struct display_mode m;
uint32 stride;
if (display_mode(&m) != 0) {
cprintf("text: no display\n");
return;
}
unsigned char *b = display_acquire(&stride);
if (!b) {
cprintf("text: no back buffer\n");
return;
}
struct surface s = { b, stride, m.width, m.height };
struct font *f = font_builtin();
// A background with structure in it, so blending is visible as blending
// rather than as drawing onto a flat colour.
for (int y = 0; y < m.height; y++) {
uint32 *row = (uint32 *)(b + (uint64)y * stride);
for (int x = 0; x < m.width; x++)
row[x] = ((x / 32 + y / 32) & 1) ? SCH_BG : SCH_ACCENT_DIM;
}
int base = f->ops->baseline(f);
int line = f->ops->height(f) + 6;
int y = 80;
text_draw(&s, f, 60, y + base, "Schunix text rendering", SCH_FG);
y += line * 2;
// Deliberately not on a multiple of eight. On the old console this string
// could only start at x = 56 or x = 64.
text_draw(&s, f, 61, y + base, "positioned at x=61, off the cell grid", SCH_DIM);
y += line;
text_draw(&s, f, 63, y + base, "and here at x=63", SCH_DIM);
y += line * 2;
// Above U+00FF, which a 256-entry table cannot index at all.
text_draw(&s, f, 60, y + base,
"utf-8: \xe2\x94\x8c\xe2\x94\x80\xe2\x94\x90 "
"\xe2\x96\x88\xe2\x96\x93\xe2\x96\x92\xe2\x96\x91 "
"\xc2\xb0 \xc2\xb1 \xe2\x86\x92 \xe2\x80\xa2", SCH_ACCENT);
y += line * 2;
// Measure, then centre on it -- the thing you cannot do without metrics.
const char *msg = "measured and centred";
int w = text_measure(f, msg);
text_draw(&s, f, (m.width - w) / 2, y + base, msg, SCH_FG);
display_present(0, 0, m.width, m.height);
cprintf("text: drew a specimen, \"%s\" measures %d px\n", msg, w);
}

Some files were not shown because too many files have changed in this diff Show more