"but Virtio GPU is still non functional. it only shows logo" -- and that was a fair reading of it. A second monitor showing a mark for ever is indistinguishable from a second monitor that does not work. The mark was the right answer to the question hybrid.md asked: H2 and H3 needed a screenshot that proved a *userspace process* had drawn something, and that the driver came back after being killed. Both are closed. What was left was a display whose entire output was a logo, on a machine whose console was sitting on the other head. So vgpud mirrors the console onto it. **A copy with a diff.** /dev/fb0 maps the console's framebuffer; the virtio backing store is already mapped, because vgpud allocated it. The backing therefore holds, at all times, exactly what was last sent to the device -- which means it *is* the previous frame, and "what changed" needs no third buffer. Ten times a second: compare rows, skip the ones that match, copy the ones that do not, transfer the one rectangle spanning the rows that changed. Hence present() becoming present_rect(). Moving one line of text through a full-screen transfer of a 1280x800 resource is most of a megabyte of copy for a few hundred bytes of difference, and this loop runs whether or not anything is happening. Mapping /dev/fb0 does not take the screen from the console: the pages are the ones the console draws into, and fb_mapped() only arranges a redraw when the fd is closed. This one never is. Sizes differ, so a head larger than the console centres it on the theme ground the mark already draws, and a smaller head shows the top-left corner -- which is where a terminal puts everything worth seeing. Scaling is the alternative and it is the wrong one; text resampled to a non-integer ratio is text you cannot read. The blocking accept() became poll() with the frame interval as its timeout, because holding the device is no longer the only thing to do. Not the endpoint: a compositor will want this head for itself, and when there is one this goes away. Until then the honest thing for a working display to show is the system that is running on it. **And a bug older than any of it.** schmode reported "cannot reach vgpud on /run/vgpud -- it is not running" about a process that was running perfectly well. Every boot after the first, on any image whose root is writable, and nothing to do with this change -- it reproduces on the previous commit, 3 runs in 4. bind() creates a node at the path, and /run is a directory on the root filesystem rather than a tmpfs. The node outlived the process that bound it and the boot it was bound in, so the next boot's vgpud claimed the device, drew, held it -- and then failed bind() on a name that already existed. A driver nobody could talk to, and an error message naming the wrong cause. It unlinks before binding now, which is what a Unix daemon does. The usual objection, that this lets a second instance steal a live socket, does not apply here: two vgpuds cannot coexist, because the first holds the PCI claim and the second is refused the device long before it reaches bind(). The claim is the mutual exclusion; the socket node never was. Verified by screendumping both heads over QMP: with the console at 1280x800 the two are byte-identical, and after `schmode 1024 768` the virtio head carries the console at the new mode. Three consecutive boots of one image all reach a working control socket, where the previous commit managed one. Both supported configurations: one boot each, no traps, no panics, socktest and memfdtest green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1497 lines
72 KiB
Makefile
1497 lines
72 KiB
Makefile
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 is coreutils, so it exists on Linux and under Git Bash but not on
|
|
# macOS or the BSDs, where the same number comes from sysctl. Falling through
|
|
# to 4 is a working build either way, just a slower one.
|
|
NPROC := $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 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 watch.o ioperm.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 ksym.o klog.o sbuf.o kmem.o)
|
|
|
|
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
|
|
bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \
|
|
pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \
|
|
dbg.o netuser.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/vga/vga_fb.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 devfs.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 attr.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 net.o inet.o netlink.o)
|
|
|
|
# lwIP: the TCP/IP stack, BSD-3-clause, unpacked by ports/lwip into
|
|
# build/ports/lwip/src. It is compiled straight into the kernel rather than
|
|
# built as a library, because there is nothing to link it into otherwise -- and
|
|
# because the alternative, a second build system inside ports/, would need the
|
|
# kernel's exact flags anyway.
|
|
#
|
|
# The file list is explicit rather than a wildcard. lwIP ships IPv6, 6LoWPAN,
|
|
# PPP, SLIP, DNS-SD and a dozen apps in the same tree, and building what is not
|
|
# configured produces either dead code or a link error naming a port function
|
|
# that was never meant to exist here.
|
|
LWIPDIR = $(BUILD)/ports/lwip/src/src
|
|
KERN_NET += $(addprefix $(BUILD)/lwip/core/, \
|
|
init.o def.o mem.o memp.o netif.o pbuf.o raw.o stats.o sys.o \
|
|
tcp.o tcp_in.o tcp_out.o timeouts.o udp.o ip.o inet_chksum.o \
|
|
dns.o)
|
|
KERN_NET += $(addprefix $(BUILD)/lwip/core/ipv4/, \
|
|
acd.o dhcp.o etharp.o icmp.o ip4.o ip4_addr.o ip4_frag.o)
|
|
KERN_NET += $(addprefix $(BUILD)/lwip/netif/, ethernet.o)
|
|
KERN_NET += $(addprefix $(BUILD)/kernel/net/lwip/, sys_arch.o netif_eth.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 = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd
|
|
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Toolchain
|
|
# -----------------------------------------------------------------------
|
|
UNAME_S := $(shell uname -s)
|
|
UNAME_P := $(shell uname -p)
|
|
|
|
# Host platform.
|
|
#
|
|
# WINHOST Windows, under Git Bash or MSYS2
|
|
# MACHOST macOS
|
|
# BSDHOST FreeBSD / OpenBSD / NetBSD / DragonFly
|
|
# (else) Linux
|
|
#
|
|
# All four are supported hosts: this is developed from both Windows and Linux,
|
|
# and nothing here should care which.
|
|
#
|
|
# 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.
|
|
ifneq (,$(filter MINGW% MSYS% CYGWIN%,$(UNAME_S)))
|
|
WINHOST := 1
|
|
endif
|
|
ifeq ($(UNAME_S),Darwin)
|
|
MACHOST := 1
|
|
endif
|
|
ifneq (,$(filter FreeBSD OpenBSD NetBSD DragonFly,$(UNAME_S)))
|
|
BSDHOST := 1
|
|
endif
|
|
|
|
SMP ?= 2
|
|
QEMU = qemu-system-x86_64
|
|
|
|
# Hardware virtualisation, when the host has it.
|
|
#
|
|
# Not a micro-optimisation. Without KVM, QEMU emulates every instruction, and
|
|
# measured on this tree that is where most of "the system feels slow" came from:
|
|
# `ls /bin` took 120 ms emulated against 20 ms with KVM, on identical code. Every
|
|
# guess about the kernel being at fault has to be made against the KVM number,
|
|
# or it is really a measurement of TCG.
|
|
#
|
|
# Probed rather than assumed, because /dev/kvm is absent on a machine without
|
|
# the extension, inside another VM, or when the user is not in the kvm group --
|
|
# and a hard -enable-kvm turns each of those into a QEMU that refuses to start.
|
|
# `-cpu host` only makes sense with KVM: it is the physical CPU, which an
|
|
# emulator cannot present.
|
|
ifneq ($(wildcard /dev/kvm),)
|
|
QEMU_ACCEL = -enable-kvm -cpu host
|
|
else
|
|
QEMU_ACCEL = -cpu qemu64,+rdtscp
|
|
endif
|
|
|
|
# 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.
|
|
# Two display devices means **two QEMU displays**, and a window can only show
|
|
# one at a time -- GTK puts them under View, SDL cycles with Ctrl-Alt-2. Which
|
|
# matters more than it sounds once vgpud runs, because the two then show
|
|
# completely different things:
|
|
#
|
|
# stdvga the boot splash, then the text console and your shell
|
|
# virtio-gpu the mark, and then the same console, mirrored there by vgpud
|
|
#
|
|
# So the two windows show the same thing once vgpud is up, which is the point:
|
|
# a head showing the running system is a head you can tell is working. Before
|
|
# vgpud starts, the virtio-gpu display is black -- nothing is wrong when that
|
|
# happens, you are looking at a device nobody has drawn on yet. `make
|
|
# qemu-console` below boots with one display and no GPU to hand over, which is
|
|
# the quickest way to tell that apart from an actual failure.
|
|
QEMU_DISPLAY = -vga std -device virtio-gpu-pci
|
|
|
|
# One display, carrying the console. No virtio-gpu, so vgpud finds nothing to
|
|
# claim and exits, and the single window is the thing you wanted to look at.
|
|
QEMU_DISPLAY_1 = -vga std
|
|
|
|
# A network card, on every QEMU target.
|
|
#
|
|
# Spelled out rather than left to QEMU's default NIC. The default exists and is
|
|
# an e1000 on this machine type today, which is exactly the sort of thing that
|
|
# is true until a QEMU release changes it -- and then the network silently stops
|
|
# being tested rather than failing loudly. Naming the device also means `make
|
|
# qemu` and `bootcheck -Net` are running the same hardware.
|
|
#
|
|
# User-mode networking: the guest gets 10.0.2.15 by DHCP, the host is 10.0.2.2,
|
|
# and no privileges are needed. A bridge or a tap would need root and would put
|
|
# the guest on the real network, which is not what a development boot wants.
|
|
#
|
|
# To point the driver at different hardware once there is a second one, override
|
|
# it: make qemu QEMU_NET='-netdev user,id=net0 -device rtl8139,netdev=net0'
|
|
# The guest's network, and a way in from the host.
|
|
#
|
|
# `-netdev user` is QEMU's NAT: the guest can reach the outside world and
|
|
# nothing outside can reach the guest, which is the safe default and exactly
|
|
# wrong when the thing you are testing is a server. SchOS has an httpd now, and
|
|
# a web server nobody can open is hard to believe in.
|
|
#
|
|
# hostfwd punches three holes, on the ports something is actually likely to be
|
|
# listening on. They bind 127.0.0.1 on purpose: the guest becomes reachable
|
|
# from *this* machine and not from the network this machine is on, which is a
|
|
# development convenience rather than a server somebody forgot about.
|
|
#
|
|
# http://localhost:8080 -> guest port 80 (httpd's default)
|
|
# http://localhost:8000 -> guest port 8000
|
|
# http://localhost:8081 -> guest port 8080
|
|
#
|
|
# Port 80 on the host is not used: binding it needs root, and `make qemu`
|
|
# should not. Override the whole variable for anything else --
|
|
# make qemu QEMU_NET='-netdev user,id=net0,hostfwd=tcp::9000-:9000 ...'
|
|
QEMU_HOSTFWD = hostfwd=tcp:127.0.0.1:8080-:80,hostfwd=tcp:127.0.0.1:8000-:8000,hostfwd=tcp:127.0.0.1:8081-:8080
|
|
QEMU_NET = -netdev user,id=net0,$(QEMU_HOSTFWD) -device e1000,netdev=net0
|
|
|
|
# An absolute pointing device, on an xHCI controller.
|
|
#
|
|
# A PS/2 mouse is relative, so inside a VM the guest cursor and the host's
|
|
# accumulate independently and drift apart -- they are simply never in the same
|
|
# place. usb-tablet reports where the pointer *is*, so the two coincide.
|
|
#
|
|
# EMPTY BY DEFAULT, and that is not an oversight. The driver binds the tablet
|
|
# and identifies it correctly, but no reports have yet been observed arriving
|
|
# on its interrupt endpoint -- so switching the default pointer to it would
|
|
# replace a cursor that drifts with one that does not move. Opt in while
|
|
# working on it:
|
|
#
|
|
# make qemu QEMU_INPUT='-device qemu-xhci,id=xhci -device usb-tablet,bus=xhci.0'
|
|
#
|
|
# See kernel/drivers/usb/hid/usb_hid.c, tablet_report().
|
|
QEMU_INPUT ?=
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Cross-compilation: clang everywhere it can be had
|
|
#
|
|
# The kernel, the bootloader and the ports all need a compiler targeting
|
|
# something other than the host -- bare-metal ELF, 16/32-bit code below one
|
|
# megabyte, and Linux/musl userland respectively. The traditional answer is a
|
|
# cross binutils+gcc per host, which is a day of work per platform that then
|
|
# rots, and on macOS means brewing x86_64-elf-* before anything builds at all.
|
|
#
|
|
# clang needs none of it: it is a cross-compiler by construction, so one
|
|
# ordinary LLVM install produces every target from every host. That was already
|
|
# how the Windows build worked; using it on all four hosts collapses what used
|
|
# to be two divergent toolchain branches -- one of which had drifted into
|
|
# passing GNU-ld-only flags to what is nowadays lld on FreeBSD and macOS -- into
|
|
# a single path that gets exercised on every platform instead of one.
|
|
#
|
|
# GNU is kept as a fallback for hosts without clang, but it is the exception.
|
|
# -----------------------------------------------------------------------
|
|
HAVE_CLANG := $(shell command -v clang 2>/dev/null)
|
|
HAVE_LLD := $(shell command -v ld.lld 2>/dev/null)
|
|
|
|
ifeq ($(origin USE_GNU), undefined)
|
|
ifneq (,$(HAVE_CLANG))
|
|
ifneq (,$(HAVE_LLD))
|
|
USE_CLANG := 1
|
|
endif
|
|
endif
|
|
endif
|
|
|
|
ifdef USE_CLANG
|
|
# An explicit --target is what makes clang emit ELF rather than whatever the
|
|
# host defaults to -- COFF on Windows, Mach-O on macOS. Without it every
|
|
# object is silently the wrong format and the first thing to complain is the
|
|
# linker.
|
|
CC = clang --target=x86_64-elf
|
|
LD = ld.lld
|
|
OBJCOPY = llvm-objcopy
|
|
OBJDUMP = llvm-objdump
|
|
NM = llvm-nm
|
|
# 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
|
|
# ports/ build *Linux userland* against musl -- a third target, distinct from
|
|
# both the kernel (bare-metal ELF) and the host tools (native).
|
|
#
|
|
# -nostdlib with the crt files named explicitly, because clang otherwise
|
|
# reaches for gcc's crtbeginT.o and libgcc, which need not exist on any
|
|
# particular 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
|
|
# GNU fallback. Needs a cross toolchain anywhere the host's own gcc does not
|
|
# already emit x86_64 ELF -- notably macOS, where TOOLPREFIX has to name a
|
|
# brewed x86_64-elf-gcc.
|
|
ifdef MACHOST
|
|
TOOLPREFIX ?= x86_64-elf-
|
|
else ifeq ($(UNAME_P),aarch64)
|
|
TOOLPREFIX ?= x86_64-linux-gnu-
|
|
else
|
|
TOOLPREFIX ?=
|
|
endif
|
|
CC = $(TOOLPREFIX)gcc
|
|
LD = $(TOOLPREFIX)ld
|
|
OBJCOPY = $(TOOLPREFIX)objcopy
|
|
OBJDUMP = $(TOOLPREFIX)objdump
|
|
NM = $(TOOLPREFIX)nm
|
|
LDFLAGS = -m elf_x86_64 -nostdlib --no-warn-rwx-segments
|
|
LD_LOWBASE =
|
|
PORTCC = $(TOOLPREFIX)gcc
|
|
PORTCRT =
|
|
PORTEND =
|
|
endif
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Host tools: mkfs, mkschfs, the SchFS suites, qrtest. Ordinary programs for
|
|
# the machine doing the building, so they use its native compiler.
|
|
# -----------------------------------------------------------------------
|
|
HOSTCC ?= cc
|
|
|
|
ifdef WINHOST
|
|
# mingw-w64's gcc, not clang: clang defaults to an MSVC target here, and
|
|
# tools/mkfs.c includes <unistd.h> and <dirent.h>, which the Windows SDK
|
|
# does not have.
|
|
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
|
|
else
|
|
HOSTWARN = -Werror -Wall
|
|
EXE =
|
|
PYTHON = python3
|
|
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)
|
|
|
|
# -MP with -MD: emit a phony target for every header, so that *moving* or
|
|
# deleting one does not wedge the build. Without it the stale .d still names the
|
|
# old path and make stops with "No rule to make target 'include/e1000.h'" --
|
|
# which happened the day that header moved next to its driver, and the only
|
|
# cure was knowing to delete the .d by hand.
|
|
CFLAGS = -fno-pic -static -fno-builtin -fno-strict-aliasing -Wall -MD -MP -ggdb -fno-omit-frame-pointer
|
|
CFLAGS += -ffreestanding -fno-common -nostdlib -I./include $(XFLAGS) $(OPT)
|
|
CFLAGS += $(NO_SSP) $(CFLAGS_EXTRA)
|
|
|
|
# lwIP's own headers, plus ours (cc.h and lwipopts.h live in kernel/net/lwip).
|
|
# Only the network files are compiled with these: putting them in the global
|
|
# CFLAGS would let any kernel file include lwIP by accident, and the stack is
|
|
# meant to be reachable through kernel/net/ and nowhere else.
|
|
# shim/ comes first so lwIP's <string.h> and friends resolve to the kernel's
|
|
# functions rather than the host's headers, which declare incompatible
|
|
# prototypes and would otherwise be found on the default include path.
|
|
LWIP_CFLAGS = -I./kernel/net/lwip/shim -I$(LWIPDIR)/include -I./kernel/net/lwip
|
|
# lwIP is third-party and warns differently from this tree. Its warnings are
|
|
# not actionable here -- patching them would mean maintaining a fork -- so they
|
|
# are silenced for its files only, never for ours.
|
|
LWIP_QUIET = -Wno-unused-parameter -Wno-sign-compare -Wno-unused-but-set-variable \
|
|
-Wno-address-of-packed-member -Wno-constant-conversion
|
|
|
|
# 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 the image is linked into the kernel binary, and stage2
|
|
# loads that by PIO one sector at a time -- a kernel with a userland inside it
|
|
# takes visibly longer to boot than a 350 KB one.
|
|
#
|
|
# 8000 blocks of 4 KB is 32 MB. It was 6000, which the userland had quietly
|
|
# grown to fill: 5157 of 6000 used, 86%, after toybox gained its pending
|
|
# applets and an editor arrived. That still fit, and the *next* thing added
|
|
# would not have -- and what mkschfs says when an image is full is "try a
|
|
# larger --size", which is a poor way to find out that a build variant you were
|
|
# not using has been broken for a while.
|
|
# -----------------------------------------------------------------------
|
|
RAMDISK_BLOCKS ?= 8000
|
|
|
|
# RAMDISK_NINODES used to live here, sizing mkfs's preallocated inode table.
|
|
# The ramdisk is a SchFS image now (see $(BUILD)/fs-ram.img) and SchFS allocates
|
|
# object ids as it goes, so there is no table to size and nothing to tune.
|
|
|
|
# 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
|
|
|
|
# Drop the objects when RAMROOT is toggled, in *either* direction.
|
|
#
|
|
# RAMROOT changes CFLAGS, and make cannot see that: an object compiled one way
|
|
# is happily reused the other way. The `ramroot` target already wiped on the way
|
|
# in, and nothing wiped on the way out -- so a plain `make` after `make ramroot`
|
|
# kept every RAMDISK_ROOT object and linked the 24 MB ramdisk back in. The
|
|
# result still booted, from the ramdisk, and the only outward symptom was that
|
|
# stage2 now had 25 MB to load through PIO one sector at a time. Measured:
|
|
# 25,413,120 bytes against 836,840 for the same kernel built clean.
|
|
#
|
|
# Evaluated when the Makefile is read, so the wipe happens before anything is
|
|
# compiled against the wrong setting.
|
|
RAMROOT_STATE := $(if $(RAMROOT),on,off)
|
|
RAMROOT_STAMP := $(BUILD)/ramroot.state
|
|
$(shell mkdir -p $(BUILD); \
|
|
if [ "$$(cat $(RAMROOT_STAMP) 2>/dev/null)" != "$(RAMROOT_STATE)" ]; then \
|
|
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd; \
|
|
printf '%s' '$(RAMROOT_STATE)' > $(RAMROOT_STAMP); \
|
|
fi)
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Top-level targets
|
|
# -----------------------------------------------------------------------
|
|
# usbstick.img is built here rather than on demand because bootcheck -Usb
|
|
# cannot start QEMU without it, and a missing test image looks exactly like a
|
|
# broken USB driver: QEMU refuses the -drive and the whole run produces nothing.
|
|
# OVMF is in this list because it is no longer optional: with the BIOS loader
|
|
# gone, nothing in this tree boots without firmware, and `make clean` wipes
|
|
# build/ including the copy fetched from the host. Leaving it out meant a clean
|
|
# followed by a build produced a tree where every run script threw.
|
|
all: esp $(BUILD)/schfs.img $(BUILD)/usbstick.img
|
|
# ...and the firmware, added to `all` further down. It cannot be listed here:
|
|
# make expands a rule's prerequisites when it reads the line, and $(OVMF_CODE)
|
|
# is defined below this point, so naming it here appends nothing at all and
|
|
# does so silently.
|
|
|
|
# schos.img is gone with the BIOS loader that gave it its shape. It was a raw
|
|
# disk with the boot sector at LBA 0, stage2 next and the kernel at a fixed
|
|
# offset -- a layout only a loader that seeks by sector number can use. UEFI
|
|
# firmware wants a filesystem, so what boots now is the staged ESP, offered to
|
|
# QEMU through its virtual FAT.
|
|
#
|
|
# That is a development arrangement, not an installable disk. A real GPT image
|
|
# with a real FAT partition is the installer's job.
|
|
|
|
kernel: $(BUILD)/kernel/kernel
|
|
|
|
# -----------------------------------------------------------------------
|
|
# UEFI loader
|
|
#
|
|
# A PE/COFF executable, not ELF, so it needs a different target and a
|
|
# different linker driver -- but the same clang install. That is the whole
|
|
# reason the toolchain is clang: one compiler reaches every target here.
|
|
#
|
|
# -mno-red-zone is not optional. UEFI firmware may take an interrupt at any
|
|
# point, and the red zone is memory below RSP that a handler is entitled to
|
|
# clobber; leaving it enabled corrupts locals at random.
|
|
# -fshort-wchar makes L"..." UTF-16, which is the only string type UEFI has.
|
|
# -----------------------------------------------------------------------
|
|
UEFI_CFLAGS = --target=x86_64-unknown-windows -ffreestanding -fshort-wchar -mno-red-zone -fno-stack-protector -Wall -Wextra -O1 -I./include
|
|
|
|
# The three headers at the end are the ones boot/common/ui.c actually includes,
|
|
# and leaving them out meant a new logo did not rebuild the loader: the splash
|
|
# kept drawing whatever mask was compiled in months earlier, `make` reported
|
|
# nothing to do, and the only symptom was a boot screen that disagreed with the
|
|
# kernel about what the logo is. The kernel half never showed it because those
|
|
# objects are built with -MD -MP and pick up header changes on their own.
|
|
#
|
|
# **The loader is the one thing here that GCC cannot build.** It is a PE/COFF
|
|
# binary for the Microsoft ABI, which clang cross-targets and gcc does not, so
|
|
# this rule needs clang and lld-link and no other rule does.
|
|
#
|
|
# That matters more than it reads, because of *where* it sits: `make` stops at
|
|
# the first failed rule, and this one runs before the sysroot is staged and the
|
|
# filesystem image is built. So on a machine without clang, touching the loader
|
|
# does not produce "the loader did not build" -- it produces a new kernel, an
|
|
# old userland, and a boot that behaves like a mix of the two, with the real
|
|
# cause forty lines up the screen. That happened, and it cost an afternoon of
|
|
# looking at the wrong thing.
|
|
#
|
|
# Hence the check and the message. It is worth the four lines.
|
|
$(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/schboot.h \
|
|
boot/common/ui.c boot/common/ui.h \
|
|
include/logo.h include/theme.h include/font.h
|
|
@mkdir -p $(BUILD)/uefi
|
|
@command -v clang >/dev/null 2>&1 || { \
|
|
echo ""; \
|
|
echo " The UEFI loader needs clang and lld-link, and nothing else here does."; \
|
|
echo " It is the only PE/COFF binary in the tree -- gcc cannot target it."; \
|
|
echo ""; \
|
|
echo " On NixOS: nix-shell --run 'NIX_HARDENING_ENABLE=\"\" make'"; \
|
|
echo " Elsewhere: install clang and lld, then make again"; \
|
|
echo ""; \
|
|
test -f $@ && { \
|
|
echo " There is an existing loader at $@."; \
|
|
echo " To keep it and build everything else: make UEFI_KEEP=1"; \
|
|
echo ""; }; \
|
|
exit 1; }
|
|
clang $(UEFI_CFLAGS) -c boot/uefi/main.c -o $(BUILD)/uefi/main.obj
|
|
clang $(UEFI_CFLAGS) -c boot/common/ui.c -o $(BUILD)/uefi/ui.obj
|
|
lld-link -subsystem:efi_application -entry:EfiMain -nodefaultlib $(BUILD)/uefi/main.obj $(BUILD)/uefi/ui.obj -out:$@
|
|
|
|
# Keep the loader that is already built and carry on with the rest.
|
|
#
|
|
# For the case the message above describes: a machine with no clang, where the
|
|
# loader has not meaningfully changed and the thing being worked on is the
|
|
# kernel or the userland. It is opt-in and prints what it is doing, because a
|
|
# build that quietly ships a stale bootloader is how you spend an evening
|
|
# debugging code that is not running.
|
|
ifeq ($(UEFI_KEEP),1)
|
|
$(BUILD)/uefi/BOOTX64.EFI:
|
|
@test -f $@ || { echo "make UEFI_KEEP=1: there is no loader to keep"; exit 1; }
|
|
@echo "uefi: keeping the existing loader (UEFI_KEEP=1) -- it may be out of date"
|
|
endif
|
|
|
|
uefi: $(BUILD)/uefi/BOOTX64.EFI
|
|
|
|
# The EFI System Partition, as a directory. QEMU serves a directory as FAT
|
|
# with `-drive file=fat:rw:`, which is enough to boot and test without any
|
|
# image-building tools -- mtools is not present on every supported host.
|
|
# A real ESP image for `make iso` is still to come.
|
|
$(BUILD)/esp/EFI/BOOT/BOOTX64.EFI: $(BUILD)/uefi/BOOTX64.EFI $(BUILD)/kernel/kernel
|
|
@mkdir -p $(BUILD)/esp/EFI/BOOT $(BUILD)/esp/schos
|
|
@cp $(BUILD)/uefi/BOOTX64.EFI $(BUILD)/esp/EFI/BOOT/BOOTX64.EFI
|
|
@cp $(BUILD)/kernel/kernel $(BUILD)/esp/schos/kernel
|
|
@cp boot/uefi/startup.nsh $(BUILD)/esp/startup.nsh
|
|
@echo "ESP staged in $(BUILD)/esp"
|
|
|
|
esp: $(BUILD)/esp/EFI/BOOT/BOOTX64.EFI
|
|
|
|
# Boot the UEFI path under OVMF. The firmware ships with QEMU; the varstore
|
|
# is copied because it must be writable.
|
|
#
|
|
# i440fx, not q35: the kernel's IDE driver speaks to the legacy ports at
|
|
# 0x1f0, and q35 has no legacy IDE at all -- only AHCI. The root volume would
|
|
# simply not be there.
|
|
OVMF_CODE = $(BUILD)/OVMF_CODE.fd
|
|
OVMF_VARS = $(BUILD)/OVMF_VARS.fd
|
|
# OVMF, wherever this host keeps it.
|
|
#
|
|
# There is no common location: QEMU's own Windows build puts it beside the
|
|
# binary as edk2-x86_64-code.fd, Fedora ships edk2-ovmf into
|
|
# /usr/share/edk2/ovmf/OVMF_CODE.fd, Debian and Arch each differ again. Search
|
|
# rather than assume, and say something useful when nothing is found -- an
|
|
# unhelpful "cp: no such file" is a poor way to learn that a package is
|
|
# missing.
|
|
#
|
|
# `$(dir ...)` of the binary is its *bin* directory, so the share directory is
|
|
# one level above it -- `$(dir)/share` asked for `/usr/bin/share`, which exists
|
|
# nowhere. On the distributions below that went unnoticed, because the absolute
|
|
# paths after it matched anyway; on one that keeps nothing in /usr it meant the
|
|
# firmware was never found on a machine that had it, and the only way to build
|
|
# was to copy the file in by hand.
|
|
QEMU_BIN = $(shell command -v $(QEMU) 2>/dev/null)
|
|
QEMU_PREFIX = $(patsubst %/bin/,%,$(dir $(QEMU_BIN)))
|
|
QEMU_SHARE = $(QEMU_PREFIX)/share
|
|
OVMF_CODE_CANDIDATES = $(QEMU_SHARE)/qemu/edk2-x86_64-code.fd $(QEMU_SHARE)/edk2-x86_64-code.fd /usr/share/edk2/ovmf/OVMF_CODE.fd /usr/share/OVMF/OVMF_CODE.fd /usr/share/ovmf/x64/OVMF_CODE.fd /usr/share/qemu/edk2-x86_64-code.fd
|
|
OVMF_VARS_CANDIDATES = $(QEMU_SHARE)/qemu/edk2-i386-vars.fd $(QEMU_SHARE)/edk2-i386-vars.fd /usr/share/edk2/ovmf/OVMF_VARS.fd /usr/share/OVMF/OVMF_VARS.fd /usr/share/ovmf/x64/OVMF_VARS.fd /usr/share/qemu/edk2-i386-vars.fd
|
|
|
|
# Tested through the shell rather than with $(wildcard). On Windows, make is a
|
|
# native binary using Win32 file calls while `command -v` hands back an MSYS
|
|
# path like /c/Users/..., which $(wildcard) cannot see -- it silently returns
|
|
# nothing and the firmware looks missing on a machine that has it.
|
|
OVMF_CODE_SRC = $(shell for f in $(OVMF_CODE_CANDIDATES); do [ -f "$$f" ] && { echo "$$f"; break; }; done)
|
|
OVMF_VARS_SRC = $(shell for f in $(OVMF_VARS_CANDIDATES); do [ -f "$$f" ] && { echo "$$f"; break; }; done)
|
|
|
|
$(OVMF_CODE):
|
|
@mkdir -p $(BUILD)
|
|
@test -n "$(OVMF_CODE_SRC)" || { echo "no OVMF firmware found. Install it:"; echo " Fedora sudo dnf install edk2-ovmf"; echo " Debian sudo apt install ovmf"; echo " Arch sudo pacman -S edk2-ovmf"; exit 1; }
|
|
cp "$(OVMF_CODE_SRC)" $@
|
|
chmod u+w $@
|
|
|
|
$(OVMF_VARS):
|
|
@mkdir -p $(BUILD)
|
|
@test -n "$(OVMF_VARS_SRC)" || { echo "no OVMF varstore found -- install the OVMF package (see above)"; exit 1; }
|
|
cp "$(OVMF_VARS_SRC)" $@
|
|
chmod u+w $@
|
|
|
|
ovmf: $(OVMF_CODE) $(OVMF_VARS)
|
|
|
|
# The other half of `all`, now that OVMF_CODE and OVMF_VARS have values.
|
|
all: $(OVMF_CODE) $(OVMF_VARS)
|
|
|
|
qemu-uefi: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
|
|
$(QEMU) -machine pc-i440fx-9.2 -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) -drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) -drive file=fat:rw:$(BUILD)/esp,index=0,media=disk,format=raw -drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk -m 1G -smp 1 $(QEMU_ACCEL) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_NET)
|
|
|
|
# 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
|
|
|
|
# projects/ -- things that run *on* SchOS rather than things that *are* SchOS.
|
|
#
|
|
# Deliberately not a dependency of anything above: `make` never descends here,
|
|
# and the kernel build does not know these exist. They are ordinary programs
|
|
# built against the sysroot's libc, exactly as toybox is, and the boundary
|
|
# between them and the OS is syscalls and protocols rather than a shared header
|
|
# nobody meant to share. See projects/README.md.
|
|
#
|
|
# Staged into $(SYSROOT) so the next image build picks them up; run
|
|
# `make projects && make` to get them onto a disk.
|
|
PROJECTS ?= schcom
|
|
|
|
projects:
|
|
@test -d $(MUSLPREFIX)/include || { \
|
|
echo "projects: no musl in $(MUSLPREFIX) -- run 'make ports' first" >&2; \
|
|
exit 1; }
|
|
@for p in $(PROJECTS); do \
|
|
echo "==> projects/$$p"; \
|
|
$(MAKE) -C projects/$$p \
|
|
CC='$(PORTCC)' \
|
|
CFLAGS='-O2 -static -D__SCHOS__ -I$(MUSLPREFIX)/include -fno-stack-protector' \
|
|
LDFLAGS='-static -L$(MUSLPREFIX)/lib' \
|
|
DESTDIR='$(abspath $(SYSROOT))' install || exit 1; \
|
|
done
|
|
|
|
# -----------------------------------------------------------------------
|
|
# 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 schos.iso
|
|
|
|
# One El Torito entry, pointing at a FAT image holding the loader and kernel.
|
|
# sysroot-install because the disc also carries the tree as a real hierarchy.
|
|
schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI sysroot-install
|
|
SYSROOT=$(SYSROOT) sh tools/posix/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: schos.iso
|
|
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_ACCEL) $(QEMU_NET) \
|
|
-cdrom schos.iso -boot d \
|
|
-m 1G -smp $(SMP) -machine pc-i440fx-9.2
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Kernel binary
|
|
# -----------------------------------------------------------------------
|
|
# The kernel is linked twice, and the second pass is the one that ships.
|
|
#
|
|
# Pass one exists to be read: tools/ksyms.pl turns its symbol table into a C
|
|
# file, which pass two compiles in, so the kernel can name its own addresses
|
|
# (docs/architecture/debugging.md, D0). kernel.ld places .ksym after .text and .rodata
|
|
# precisely so that carrying the table cannot move anything the table records
|
|
# -- which is what makes two passes sufficient instead of the three Linux needs.
|
|
#
|
|
# That property is checked, not assumed. If the two passes ever disagree about
|
|
# where a function is, the table is silently wrong for every address, and a
|
|
# debugger that lies is worse than one that says nothing -- so the build stops.
|
|
KERN_LINK = $(BUILD)/kernel/arch/x86_64/entry.o $(KERN_NOENTRY) \
|
|
$(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj \
|
|
$(BUILD)/kernel/consfont_obj $(RAMDISK_OBJ)
|
|
|
|
$(BUILD)/kernel/kernel.pass1: $(KERNOBJS) $(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj $(BUILD)/kernel/consfont_obj $(RAMDISK_OBJ) kernel/kernel.ld
|
|
@mkdir -p $(BUILD)/kernel
|
|
$(LD) $(LDFLAGS) -T kernel/kernel.ld -o $@ $(KERN_LINK)
|
|
|
|
$(BUILD)/kernel/ksym.c: $(BUILD)/kernel/kernel.pass1 tools/ksyms.pl
|
|
$(NM) -n $< | perl tools/ksyms.pl > $@
|
|
|
|
$(BUILD)/kernel/ksym.o: $(BUILD)/kernel/ksym.c
|
|
$(CC) $(CFLAGS) -c -o $@ $<
|
|
|
|
$(BUILD)/kernel/kernel: $(BUILD)/kernel/kernel.pass1 $(BUILD)/kernel/ksym.o
|
|
$(LD) $(LDFLAGS) -T kernel/kernel.ld -o $@ $(KERN_LINK) $(BUILD)/kernel/ksym.o
|
|
@$(NM) -n $(BUILD)/kernel/kernel.pass1 | perl tools/ksyms.pl --list > $(BUILD)/kernel/ksym.pass1.txt
|
|
@$(NM) -n $@ | perl tools/ksyms.pl --list > $(BUILD)/kernel/ksym.pass2.txt
|
|
@cmp -s $(BUILD)/kernel/ksym.pass1.txt $(BUILD)/kernel/ksym.pass2.txt || \
|
|
{ echo "ksyms: the two link passes disagree about where the code is;"; \
|
|
echo "ksyms: the symbol table would name every address wrongly."; \
|
|
diff -u $(BUILD)/kernel/ksym.pass1.txt $(BUILD)/kernel/ksym.pass2.txt | head -20; \
|
|
exit 1; }
|
|
$(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.
|
|
# Built with mkschfs, not mkfs.
|
|
#
|
|
# SchFS is the only root the kernel will mount -- it panics in forkret() when
|
|
# handed anything else -- so a v1 image here produced "schfs root: no SchFS
|
|
# volume on the root device" and made `make iso` and `make ramroot` unbootable.
|
|
# RAMDISK_NINODES is gone with it: SchFS has no preallocated inode table, which
|
|
# is what that knob existed to size.
|
|
$(BUILD)/fs-ram.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
|
|
@mkdir -p $(BUILD)
|
|
@rm -f $@
|
|
$(MKSCHFS) $@ --size $(RAMDISK_BLOCKS) --label schos \
|
|
$(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 \
|
|
$@
|
|
|
|
# The console font, linked into the kernel.
|
|
#
|
|
# In the kernel image rather than loaded from disk, because the console has to
|
|
# work before there is a disk: a panic during boot has to be readable, and the
|
|
# only alternative is keeping the 8x16 bitmap around for early messages and
|
|
# switching later, which means two fonts and a seam where they disagree.
|
|
#
|
|
# 115 KB of kernel for every glyph in a modern font is a trade this makes
|
|
# gladly. The 8x16 bitmap it replaces is CP437 -- 256 glyphs, no Ą, no Ę, no Ś,
|
|
# no Ω -- and they did not render as something wrong, they rendered as `?`,
|
|
# because a codepoint the grid could not hold was mapped to one.
|
|
#
|
|
# JetBrains Mono is SIL OFL 1.1, which permits this and requires the name be
|
|
# kept; see NOTICE. The regular static weight, not the variable font -- a
|
|
# variable font needs `fvar`/`gvar` interpolation, which ttf.c does not do and
|
|
# which would render every glyph at the wrong weight without saying so.
|
|
CONSFONT = ports/JetBrains_Mono/static/JetBrainsMono-Regular.ttf
|
|
|
|
$(BUILD)/kernel/consfont_obj: $(CONSFONT)
|
|
@mkdir -p $(BUILD)/kernel
|
|
$(LD) -r -b binary -m elf_x86_64 -o $@ $<
|
|
$(OBJCOPY) \
|
|
--redefine-sym _binary_$(subst .,_,$(subst /,_,$(subst -,_,$(CONSFONT))))_start=_binary_consfont_start \
|
|
--redefine-sym _binary_$(subst .,_,$(subst /,_,$(subst -,_,$(CONSFONT))))_end=_binary_consfont_end \
|
|
$@
|
|
|
|
# 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.
|
|
# -I on the driver's own directory, so a driver can keep its register
|
|
# definitions next to its code instead of in include/. include/ is for headers
|
|
# the rest of the kernel needs; a chip's register map is not one of those, and
|
|
# e1000.h sat there for a year being included by exactly one file.
|
|
$(BUILD)/kernel/drivers/%.o: kernel/drivers/%.c
|
|
@mkdir -p $(dir $@)
|
|
$(CC) $(CFLAGS) -I$(dir $<) -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 $@
|
|
|
|
# kernel/net gets lwIP's headers: net.c and inet.c are the two files that talk
|
|
# to the stack directly, and unix.c is unaffected by the extra include path.
|
|
$(BUILD)/kernel/net/%.o: kernel/net/%.c | $(LWIPDIR)
|
|
@mkdir -p $(BUILD)/kernel/net
|
|
$(CC) $(CFLAGS) $(LWIP_CFLAGS) -c $< -o $@
|
|
|
|
# Our own lwIP port layer. Ours, so it is held to this tree's warnings.
|
|
$(BUILD)/kernel/net/lwip/%.o: kernel/net/lwip/%.c | $(LWIPDIR)
|
|
@mkdir -p $(BUILD)/kernel/net/lwip
|
|
$(CC) $(CFLAGS) $(LWIP_CFLAGS) -c $< -o $@
|
|
|
|
# lwIP's own sources, compiled from the unpacked tarball.
|
|
$(BUILD)/lwip/%.o: $(LWIPDIR)/%.c | $(LWIPDIR)
|
|
@mkdir -p $(dir $@)
|
|
$(CC) $(CFLAGS) $(LWIP_CFLAGS) $(LWIP_QUIET) -c $< -o $@
|
|
|
|
# Unpack lwIP on demand. Order-only everywhere it is used, because the
|
|
# directory's mtime is not a reason to recompile anything.
|
|
$(LWIPDIR):
|
|
$(MAKE) -C ports lwip
|
|
|
|
$(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 $@
|
|
|
|
# ldlsd links the LDLS core as well as its own main, so it needs a rule of its
|
|
# own -- a specific rule beats the pattern one below. ldls/ko.c, btf.c and
|
|
# stub.c are freestanding and build unchanged for either side, which is the
|
|
# whole point of having written them that way.
|
|
LDLS_GUEST = $(addprefix $(BUILD)/ldls/guest/, ko.o btf.o stub.o) \
|
|
$(addprefix $(BUILD)/ldls/guest/kpi/, kpi.o pci.o io.o net.o skb.o string.o work.o thunk.o)
|
|
|
|
$(BUILD)/ldls/guest/%.o: ldls/%.c ldls/ko.h ldls/btf.h ldls/stub.h
|
|
@mkdir -p $(BUILD)/ldls/guest
|
|
$(CC) $(CFLAGS) -c $< -o $@
|
|
|
|
# The KPI -- the Linux symbols, implemented. Guest-only: it reaches /dev/pci
|
|
# and /dev/netdev, which exist on SchOS and nowhere else.
|
|
$(BUILD)/ldls/guest/kpi/%.o: ldls/kpi/%.c ldls/kpi/kpi.h
|
|
@mkdir -p $(BUILD)/ldls/guest/kpi
|
|
$(CC) $(CFLAGS) -I./ldls -c $< -o $@
|
|
|
|
$(BUILD)/cmd/ldlsd: cmd/ldlsd/main.c $(ULIB) $(LDLS_GUEST)
|
|
@mkdir -p $(BUILD)/cmd
|
|
$(CC) $(CFLAGS) -I./ldls -c $< -o $(BUILD)/cmd/ldlsd.o
|
|
$(LD) $(LDFLAGS) $(LD_LOWBASE) -e _start -Ttext 0x4000 -o $@ \
|
|
$(BUILD)/cmd/ldlsd.o $(LDLS_GUEST) $(ULIB)
|
|
$(OBJCOPY) -S $@
|
|
|
|
$(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 $@
|
|
|
|
# rm before cp, and it is not tidiness.
|
|
#
|
|
# toybox installs its 236 applets as hard links to one multicall binary, and
|
|
# some of those names collide with first-party programs here -- /bin/ping is
|
|
# both. `cp` onto an existing hard link writes *through* it, so copying this
|
|
# program over the applet of the same name replaced the contents of the shared
|
|
# inode, and every one of the 236 links became this program. `cat` ran ping.
|
|
#
|
|
# It was invisible until the applets became links: while they were separate
|
|
# copies, overwriting one affected exactly one.
|
|
$(SYSROOT)/bin/%: $(BUILD)/cmd/% | $(SYSROOT)/bin
|
|
rm -f $@
|
|
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/attr.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 slab allocator, on the host. Same argument as schfs-objtest above:
|
|
# kernel/core/kmem.c is written to compile on the build machine, so its
|
|
# bookkeeping -- slab lists, the free list threaded through free objects, the
|
|
# page-to-cache lookup, and whether pages actually come back -- is asserted in
|
|
# a second rather than in a boot.
|
|
KMEMTEST = $(BUILD)/tools/kmemtest$(EXE)
|
|
$(KMEMTEST): tools/kmemtest.c kernel/core/kmem.c include/kmem.h
|
|
@mkdir -p $(BUILD)/tools
|
|
$(HOSTCC) $(HOSTCFLAGS) -I include -o $@ tools/kmemtest.c
|
|
|
|
kmem-test: $(KMEMTEST)
|
|
$(KMEMTEST)
|
|
|
|
# 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/posix/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/windows/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/python/crashqr.py
|
|
|
|
# -----------------------------------------------------------------------
|
|
# LDLS -- the Linux Driver Layer. docs/plan/ldls.md.
|
|
#
|
|
# ldls/ko.c is freestanding and builds on both sides: here as part of a host
|
|
# tool, and later as part of the SchOS-side host. Same discipline as SchFS,
|
|
# and for the same reason -- the failures at this layer are format failures,
|
|
# and one found on the host costs a second instead of a boot.
|
|
# -----------------------------------------------------------------------
|
|
LDLS_INSPECT = $(BUILD)/tools/ldls-inspect$(EXE)
|
|
LDLS_RUN = $(BUILD)/tools/ldls-run$(EXE)
|
|
LDLS_TYPES = $(BUILD)/tools/ldls-types$(EXE)
|
|
LDLS_REFMOD = $(BUILD)/ldls/refmod.o
|
|
|
|
$(LDLS_INSPECT): ldls/host/inspect.c ldls/ko.c ldls/ko.h
|
|
@mkdir -p $(BUILD)/tools
|
|
$(HOSTCC) $(HOSTWARN) -O1 -o $@ ldls/host/inspect.c ldls/ko.c
|
|
|
|
# L1. Calls a module's init with every symbol stubbed, to read off the call
|
|
# order. -fno-strict-aliasing because this deliberately treats loaded bytes as
|
|
# code, and no optimiser assumption about that is one we want.
|
|
$(LDLS_RUN): ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c \
|
|
ldls/ko.h ldls/stub.h ldls/btf.h
|
|
@mkdir -p $(BUILD)/tools
|
|
$(HOSTCC) $(HOSTWARN) -O1 -fno-strict-aliasing -o $@ \
|
|
ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c
|
|
|
|
# The test object, compiled and never linked. Ours, because docs/plan/ldls.md
|
|
# rule 1 means no .ko is committed here -- see the header of refmod.c.
|
|
#
|
|
# -fno-pic matters: a PIC object routes its data through the GOT, and a GOT is
|
|
# a thing the loader would have to build. A module is built -fno-pic for the
|
|
# same reason, so this matches what the real input looks like.
|
|
$(LDLS_REFMOD): ldls/test/refmod.c
|
|
@mkdir -p $(BUILD)/ldls
|
|
$(HOSTCC) -c -O1 -fno-pic -fno-asynchronous-unwind-tables -o $@ $<
|
|
|
|
# L2. Reads struct layouts out of a kernel's own BTF, so they are never in our
|
|
# source. See ldls/btf.h.
|
|
$(LDLS_TYPES): ldls/host/types.c ldls/btf.c ldls/btf.h
|
|
@mkdir -p $(BUILD)/tools
|
|
$(HOSTCC) $(HOSTWARN) -O1 -o $@ ldls/host/types.c ldls/btf.c
|
|
|
|
ldls: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_TYPES)
|
|
|
|
# Put a module and its kernel's BTF into the *local* sysroot, for testing ldlsd
|
|
# in QEMU.
|
|
#
|
|
# make ldls-stage KO=/path/to/8139too.ko BTF=/sys/kernel/btf/vmlinux
|
|
#
|
|
# This is the model docs/plan/ldls.md describes, carried out on the machine
|
|
# that is doing the testing: the user obtains a module and loads it on their
|
|
# own machine. Deliberately **not** a dependency of `all`, so an ordinary build
|
|
# never picks it up, and `.gitignore` refuses `*.ko` so it cannot be committed
|
|
# by accident. An image built after this has somebody else's GPL code in it and
|
|
# must not be redistributed -- which is exactly what licensing.md §6 says about
|
|
# the combination, and why it is one command you have to type on purpose.
|
|
ldls-stage:
|
|
@test -n "$(KO)" || { echo "usage: make ldls-stage KO=<module.ko> [BTF=<vmlinux-btf>]"; exit 1; }
|
|
@mkdir -p $(SYSROOT)/lib/modules
|
|
cp "$(KO)" $(SYSROOT)/lib/modules/
|
|
@test -z "$(BTF)" || cp "$(BTF)" $(SYSROOT)/lib/modules/btf
|
|
@echo "staged into $(SYSROOT)/lib/modules -- this image is now yours alone"
|
|
|
|
ldls-unstage:
|
|
rm -rf $(SYSROOT)/lib/modules
|
|
@echo "removed"
|
|
|
|
# Load our own object and check the loader got it right. No network, no QEMU,
|
|
# no module from anywhere.
|
|
ldls-check: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_TYPES) $(LDLS_REFMOD)
|
|
$(PYTHON) tools/python/ldls_check.py $(LDLS_INSPECT) $(LDLS_REFMOD) $(LDLS_RUN)
|
|
|
|
$(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.
|
|
# schinit's job manifests. Kept under etc/ in the repo and installed here,
|
|
# because sysroot/ is generated and gitignored -- anything only ever written
|
|
# straight into it is lost on a fresh clone, which is already true of
|
|
# /etc/passwd and /etc/motd and should not become true of the boot
|
|
# configuration as well.
|
|
SCHINIT_JOBS = $(wildcard etc/schinit/*.job) etc/schinit/README
|
|
ETC_ZSH = $(wildcard etc/zsh/*)
|
|
ETC_FILES = $(wildcard etc/*)
|
|
ETC_PLAIN = $(filter-out etc/schinit etc/zsh,$(ETC_FILES))
|
|
|
|
# The mark as characters, for anything that draws a logo into a terminal --
|
|
# fastfetch takes it with --file-raw. Installed from docs/branding rather than
|
|
# copied into sysroot by hand, so there is one source and `make` keeps them in
|
|
# step; see docs/branding/README.md.
|
|
#
|
|
# The 50-column one, because that is what fits beside an info block on an
|
|
# 80-column console. The wider renderings are in the same directory for
|
|
# anything with more room.
|
|
LOGO_ASCII = docs/branding/Ascii/ascii_50.txt
|
|
|
|
# What httpd serves, if nobody has said otherwise. One page, whose job is to
|
|
# prove the path works end to end -- a directory listing of an empty /srv
|
|
# proves the same thing far less clearly.
|
|
SRV_FILES = $(wildcard srv/*)
|
|
|
|
etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(ETC_ZSH) $(LOGO_ASCII) $(SRV_FILES)
|
|
@mkdir -p $(SYSROOT)/etc/schinit $(SYSROOT)/etc/zsh $(SYSROOT)/usr/share/schos $(SYSROOT)/srv
|
|
@cp $(ETC_PLAIN) $(SYSROOT)/etc/
|
|
@cp $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/
|
|
@cp $(ETC_ZSH) $(SYSROOT)/etc/zsh/
|
|
@cp $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt
|
|
@cp $(SRV_FILES) $(SYSROOT)/srv/
|
|
@echo "installed $(words $(ETC_PLAIN)) /etc file(s), $(words $(ETC_ZSH)) zsh file(s) and $(words $(SCHINIT_JOBS)) schinit manifest(s)"
|
|
|
|
# The mount points. Created here rather than by init, because a mkdir on a
|
|
# fresh SchFS is a whole transaction -- a copy-on-write rewrite of the path to
|
|
# the root and an uberblock behind a cache flush -- and four of them were most
|
|
# of what remained of a 2.6 second first boot. Making them at image-build time
|
|
# costs nothing at all.
|
|
#
|
|
# Not committed as empty directories with a .gitkeep, because /sys and /run are
|
|
# not mounted over and the marker file would be visible in them.
|
|
SYSROOT_DIRS = dev proc sys run tmp root
|
|
|
|
sysroot-install: $(addprefix $(SYSROOT)/bin/, $(CMDS)) etc-install
|
|
@mkdir -p $(addprefix $(SYSROOT)/, $(SYSROOT_DIRS))
|
|
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
|
|
|
|
# A small block device for the USB test to read.
|
|
#
|
|
# bootcheck -Usb attaches this as a usb-storage device and has the driver read
|
|
# block 1 off it, which is the only check that separates "a device answered
|
|
# INQUIRY" from "the driver can actually transfer data".
|
|
#
|
|
# Its own image, not the root volume and not build/fs.img:
|
|
# - attaching the root volume twice (once as hdb, once over USB) broke the
|
|
# xHCI interrupt check as well as the storage ones;
|
|
# - build/fs.img is no longer built by anything, since SchFS became the root,
|
|
# so pointing at it meant QEMU refused to start at all;
|
|
# - at FSSIZE blocks it would now be ~600 MB, because BSIZE went from 1024 to
|
|
# 4096. The size is load-bearing: tools/windows/bootcheck.ps1 asserts on
|
|
# READ CAPACITY returning 32768 sectors of 512 bytes, which is this.
|
|
#
|
|
# The exclusions matter more than they look, because the size above cannot
|
|
# move to accommodate content. This image exists to prove the USB driver
|
|
# can read a block and for no other reason, so what is in it is irrelevant
|
|
# and only its total size is not.
|
|
#
|
|
# --exclude bin: mkfs writes each hard link as its own copy, so once
|
|
# toybox's applets became links to one binary the /bin tree expanded to
|
|
# more than 60 MB on the way in -- hundreds of copies of something
|
|
# nothing here will ever execute.
|
|
# --exclude lib: `make ldls-stage` puts a kernel module and its BTF here,
|
|
# and a vmlinux BTF is 5 MB on its own. That filled this image and broke
|
|
# `make` for anyone who had staged a module -- a build failure a long way
|
|
# from its cause. It also kept somebody else's GPL module out of a test
|
|
# artifact, which RAMDISK_EXCLUDE does for the ramdisk and nothing was
|
|
# doing for this.
|
|
# --exclude usr: 7 MB of fonts, for a test that reads one block.
|
|
USBSTICK = $(BUILD)/usbstick.img
|
|
$(USBSTICK): $(MKFS) $(BUILD)/sysroot.stamp
|
|
@mkdir -p $(BUILD)
|
|
@rm -f $@
|
|
$(MKFS) $@ --size 4096 --exclude bin --exclude lib --exclude usr \
|
|
--sysroot $(SYSROOT)
|
|
|
|
usbstick: $(USBSTICK)
|
|
|
|
$(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp
|
|
@mkdir -p $(BUILD)
|
|
@echo "Creating SchOS 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 the staged ESP holding a kernel
|
|
@# from the previous build. The next test run then boots that one 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
|
|
# -----------------------------------------------------------------------
|
|
# The same thing with hardware virtualisation. Opt-in, never the default: the
|
|
# test suite's timings were established under TCG and changing the execution
|
|
# engine underneath them is a variable to introduce on purpose.
|
|
#
|
|
# Worth knowing what it does and does not buy. Measured with the guest timing
|
|
# itself, `ls /bin | wc -l` goes from 2.9s to 1.1s. Boot does *not* improve --
|
|
# it gets worse -- because stage2 loads the kernel by PIO one sector at a time,
|
|
# and under KVM every one of those port accesses is a VM exit, where under TCG
|
|
# it is cheap.
|
|
# Schcom, end to end, checked from outside the machine.
|
|
#
|
|
# Boots, starts the compositor and a client, and then screendumps the
|
|
# framebuffer and asserts the pixels on the host. That last part is the whole
|
|
# value: the compositor saying "I composited it" proves nothing, and a
|
|
# screenshot nobody inspects proves less. Needs `make projects` first.
|
|
schcom-test: esp $(BUILD)/schfs.img
|
|
sh tools/posix/schcom_test.sh
|
|
|
|
# The firmware and the disks, in one place. Every run target below boots the
|
|
# same way, because there is only one way to boot now: OVMF, the staged ESP as
|
|
# the first disk, the SchFS volume as the second. Keeping the argument list in
|
|
# a variable is not tidiness -- four copies of a pflash pair is how one of them
|
|
# ends up with a writable firmware image and nobody notices.
|
|
OVMF_DRIVES = -drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) \
|
|
-drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) \
|
|
-drive file=fat:rw:$(BUILD)/esp,index=0,media=disk,format=raw \
|
|
-drive file=$(BUILD)/schfs.img,format=raw,index=1,media=disk
|
|
|
|
# Kept as an explicit spelling, but `make qemu` now picks KVM up on its own
|
|
# whenever /dev/kvm exists -- see QEMU_ACCEL. This target differs only in
|
|
# insisting on it, which is what you want when the point is to find out why the
|
|
# host has no /dev/kvm rather than to quietly get a slow machine.
|
|
# Source-level debugging, which the tree had no way to do.
|
|
#
|
|
# QEMU carries a gdb stub and it costs nothing to expose: -S holds the machine
|
|
# before its first instruction, -gdb waits for a connection. Attach and you get
|
|
# breakpoints, single-stepping and symbols in kernel code -- including the
|
|
# early boot the crash screen cannot reach and the serial console has not
|
|
# started for.
|
|
#
|
|
# terminal 1: make qemu-gdb
|
|
# terminal 2: gdb build/kernel/kernel
|
|
# (gdb) target remote :26000
|
|
# (gdb) break kmain
|
|
# (gdb) continue
|
|
#
|
|
# .gdbinit in the repo root does the connecting, so `gdb` alone is enough if
|
|
# gdb is allowed to load it -- see the file for the one line that needs.
|
|
GDBPORT ?= 26000
|
|
|
|
qemu-gdb: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
|
|
@echo "waiting for gdb on port $(GDBPORT) -- attach, then 'continue'"
|
|
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_ACCEL) $(QEMU_NET) \
|
|
$(OVMF_DRIVES) -m 1G -smp 1 -machine pc-i440fx-9.2 \
|
|
-S -gdb tcp::$(GDBPORT)
|
|
|
|
qemu-kvm: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
|
|
$(QEMU) -enable-kvm -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_NET) -cpu host \
|
|
$(OVMF_DRIVES) -m 1G -smp $(SMP) -machine pc-i440fx-9.2
|
|
|
|
qemu: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
|
|
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_ACCEL) $(QEMU_NET) \
|
|
$(OVMF_DRIVES) -m 1G -smp $(SMP) -machine pc-i440fx-9.2
|
|
|
|
# The same machine with one screen, which is the console. See QEMU_DISPLAY.
|
|
qemu-console: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
|
|
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY_1) $(QEMU_INPUT) $(QEMU_ACCEL) $(QEMU_NET) \
|
|
$(OVMF_DRIVES) -m 1G -smp $(SMP) -machine pc-i440fx-9.2
|
|
|
|
# The root filesystem. Built from the staged sysroot; the kernel mounts it as
|
|
# / in forkret().
|
|
#
|
|
# 25600 blocks of 4 KB is 100 MB. It was 8000 (32 MB), which was comfortable
|
|
# when the whole userland was one multicall binary and stopped being so once
|
|
# there was more than one thing in it -- and mkschfs's answer to a full image
|
|
# is "try a larger --size", which is a worse thing to read than a number here
|
|
# is to change. A raw disk image is sparse on any filesystem worth the name,
|
|
# so the unused part costs nothing until it is used.
|
|
SCHFS_BLOCKS ?= 25600
|
|
|
|
$(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
|
|
@mkdir -p $(BUILD)
|
|
@rm -f $@
|
|
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --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: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
|
|
$(QEMU) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_ACCEL) $(QEMU_NET) \
|
|
$(OVMF_DRIVES) -m 1G -smp $(SMP) -machine pc-i440fx-9.2
|
|
|
|
# -----------------------------------------------------------------------
|
|
# Misc
|
|
# -----------------------------------------------------------------------
|
|
# Nothing that is a real file with real prerequisites belongs here. schos.img
|
|
# used to be listed, and being phony made make skip the timestamp check and
|
|
# re-run its four dd commands -- including zeroing five megabytes -- on every
|
|
# single invocation, including ones where nothing had changed. The file is gone
|
|
# with the BIOS loader, but the rule it taught applies to whatever replaces it.
|
|
# 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.
|
|
# The mark, not the wordmark. "SchOS Text Logo.png" is the text form and would
|
|
# be illegible scaled to LOGO_PX; the small logo this used to name is gone,
|
|
# replaced by the full logo, which is the same mark slightly redrawn.
|
|
LOGO_SRC = docs/branding/shift-mask.png
|
|
LOGO_PX = 128
|
|
|
|
logo:
|
|
$(PYTHON) tools/python/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; SchOS 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/python/qrcheck.py
|
|
|
|
.PHONY: all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm schcom-test \
|
|
schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \
|
|
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
|
|
logo qrtest qrcheck crashqr usb-ids ovmf esp uefi
|
|
.PRECIOUS: $(BUILD)/%.o
|
|
|
|
# 'clean' removes generated output only. dist/ (upstream tarballs) survives,
|
|
# so a rebuild never needs the network.
|
|
clean:
|
|
rm -rf $(BUILD) schos.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)
|