hwreport: describe a machine onto a USB stick, from a Linux-style /sys

hwreport writes one folder to the first read-write FAT mount, or mounts
the first USB stick that takes vfat itself and unmounts it afterwards:
summary.txt (machine, firmware, CPU, memory, every PCI and USB device
with its driver or "-- no driver"), dmesg.txt, lspci.txt in the shape of
lspci -nnk -xxx, lsusb.txt, dmi.txt, memmap.txt, the ACPI tables as raw
.dat files iasl -d reads, and copies of /proc. Serials, the UUID and the
MSDM/SLIC tables (a Windows licence key) are left out.

It reads /sys, which is new: sysfs at Linux's paths and in Linux's
formats -- /sys/bus/pci/devices with uevent, config and resource,
/sys/bus/usb/devices, /sys/firmware/acpi/tables, dmi/tables, memmap
and efi, /sys/class/dmi/id. toybox's lspci and lsusb work unmodified
against it. The USB core now keeps a table of attached devices for it.

The machine's name comes from SMBIOS (kernel/dev/dmi.c). The UEFI loader
hands over the entry point from the configuration table, the kernel
falls back to the 0xF0000 scan, and the log gets Linux's "DMI:" line.

Two bugs found on the way:

- The loader handed over at most 40 memory-map entries, in firmware
  order. A laptop's UEFI map is 60-150 descriptors with RAM above 4 GB
  listed last, so that is what was dropped. The entries now have their
  own area at 0x31000 with room for 256, are sorted before merging, and
  the kernel's 32 usable regions keep the largest when there are more.
- MemTotal and sysinfo's totalram were PHYSTOP, an address, which counts
  the PCI hole: a 6 GB machine reported 8. Now the usable RAM, summed.

usb.ids moves to /usr/share/hwdata, where lsusb looks for it, and
pci.ids joins it (`make pci-ids`, BSD-3-Clause like usb.ids).

Checked under QEMU q35: the report written to a FAT32 stick, fsck.fat
clean, DSDT.dat disassembled by iasl on the host, lspci -k and lsusb
showing names, 6 GB of RAM seen as 6137 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-23 12:54:11 +02:00
commit eab13241a0
25 changed files with 2520 additions and 65 deletions

View file

@ -26,7 +26,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \ KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
bio.o blkdev.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \ bio.o blkdev.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 \ pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \
dbg.o netuser.o audio.o) dbg.o netuser.o audio.o dmi.o)
# Drivers for hardware that has to be *found* rather than assumed. Anything # Drivers for hardware that has to be *found* rather than assumed. Anything
# discovered by walking a bus belongs here; the fixed-address legacy devices # discovered by walking a bus belongs here; the fixed-address legacy devices
@ -55,7 +55,7 @@ KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hub/usb_hub.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hub/usb_hub.o
KERN_FS = $(addprefix $(BUILD)/kernel/fs/, \ KERN_FS = $(addprefix $(BUILD)/kernel/fs/, \
exec.o file.o fs.o log.o pipe.o procfs.o devfs.o) exec.o file.o fs.o log.o pipe.o procfs.o devfs.o sysfs.o)
# SchFS. Every one of these is the very same source the host tools compile, so # 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 # the kernel and mkschfs cannot disagree about the on-disk format: there is one
@ -128,7 +128,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS))
# Third-party software does NOT go here — see ports/. # Third-party software does NOT go here — see ports/.
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) 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 tone CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone hwreport
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
@ -478,10 +478,10 @@ RAMDISK_BLOCKS ?= 8000
# This is not hypothetical: a 33 MB test payload once landed here and overflowed # 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 # the 6 MB image, and mkfs's "increase FSSIZE" message then pointed at a constant
# that had nothing to do with it. # that had nothing to do with it.
# usb.ids is 730 KB of a 6 MB image -- 12% of the ramdisk root -- and nothing # usb.ids and pci.ids are 2 MB of a 6 MB image and nothing needs them to boot.
# needs it to boot. It is a lookup table for a userspace tool, so it stays on # They are lookup tables for userspace tools, so they stay on the disk image
# the disk image and out of the kernel binary. # and out of the kernel binary.
RAMDISK_EXCLUDE = usr/share/usb.ids RAMDISK_EXCLUDE = usr/share/hwdata
RAMDISK_EXCLUDE_ARGS = $(foreach p,$(RAMDISK_EXCLUDE),--exclude $(p)) RAMDISK_EXCLUDE_ARGS = $(foreach p,$(RAMDISK_EXCLUDE),--exclude $(p))
ifdef RAMROOT ifdef RAMROOT
@ -1282,6 +1282,12 @@ etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(ETC_ZSH) $(LOGO_ASCII) $(SRV_FILES)
@cp $(ETC_ZSH) $(SYSROOT)/etc/zsh/ @cp $(ETC_ZSH) $(SYSROOT)/etc/zsh/
@cp $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt @cp $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt
@cp $(SRV_FILES) $(SYSROOT)/srv/ @cp $(SRV_FILES) $(SYSROOT)/srv/
@# The device-name databases, where lspci and lsusb look for them: the
@# hwdata package's directory. They were at /usr/share/usb.ids, which no
@# tool searches, so lsusb printed numbers only.
@mkdir -p $(SYSROOT)/usr/share/hwdata
@rm -f $(SYSROOT)/usr/share/usb.ids
@for f in usb.ids pci.ids; do [ -f dist/$$f ] && cp dist/$$f $(SYSROOT)/usr/share/hwdata/; done; true
@echo "installed $(words $(ETC_PLAIN)) /etc file(s), $(words $(ETC_ZSH)) zsh file(s) and $(words $(SCHINIT_JOBS)) schinit manifest(s)" @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 # The mount points. Created here rather than by init, because a mkdir on a
@ -1515,20 +1521,28 @@ logo:
# critical path of `make`. dist/ exists precisely so a rebuild never needs 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 # 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. # writes is committed and dated, so what shipped is always knowable.
#
# pci.ids is the same arrangement for PCI, from the PCI ID Repository, under
# the same two licences. `make hwdata` refreshes both.
USB_IDS_URL = http://www.linux-usb.org/usb.ids USB_IDS_URL = http://www.linux-usb.org/usb.ids
PCI_IDS_URL = https://pci-ids.ucw.cz/v2.2/pci.ids
usb-ids: usb-ids pci-ids:
@echo "==> fetching $(USB_IDS_URL)" @url="$(if $(filter usb-ids,$@),$(USB_IDS_URL),$(PCI_IDS_URL))"; \
@curl -sfL --max-time 60 -o dist/usb.ids.new "$(USB_IDS_URL)" || \ f=$(subst -,.,$@); \
{ echo " fetch failed; keeping the cached copy"; rm -f dist/usb.ids.new; exit 1; } echo "==> fetching $$url"; \
curl -sfL --max-time 120 -o dist/$$f.new "$$url" || \
{ echo " fetch failed; keeping the cached copy"; rm -f dist/$$f.new; exit 1; }; \
grep -q "^#.*Version:" dist/$$f.new || \
{ echo " that is not a $$f file; keeping the cached copy"; \
rm -f dist/$$f.new; exit 1; }; \
mv dist/$$f.new dist/$$f; \
mkdir -p $(SYSROOT)/usr/share/hwdata; cp dist/$$f $(SYSROOT)/usr/share/hwdata/; \
grep -m1 "^#.*Version:" dist/$$f
@# A truncated or error-page download would otherwise silently replace a @# A truncated or error-page download would otherwise silently replace a
@# good database with rubbish. Check it looks like what it claims to be. @# good database with rubbish, which is what the Version check is for.
@grep -q "^# Version:" dist/usb.ids.new || \
{ echo " that is not a usb.ids file; keeping the cached copy"; \ hwdata: usb-ids pci-ids
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. # Build the kernel's QR encoder on the host and check it.
# #
@ -1559,7 +1573,7 @@ qrcheck: $(QRTEST)
.PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm schcom-test \ .PHONY: disk 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-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \ schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
logo qrtest qrcheck crashqr usb-ids ovmf esp uefi logo qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi
.PRECIOUS: $(BUILD)/%.o .PRECIOUS: $(BUILD)/%.o
# 'clean' removes generated output only. dist/ (upstream tarballs) survives, # 'clean' removes generated output only. dist/ (upstream tarballs) survives,

19
NOTICE
View file

@ -181,9 +181,22 @@ Public License version 2 or later, *or* the 3-clause BSD Licence. SchOS takes
the BSD option, which was chosen while SchOS was Apache-2.0; either option is the BSD option, which was chosen while SchOS was Apache-2.0; either option is
compatible now, and the BSD one is kept because it asks less of anyone. compatible now, and the BSD one is kept because it asks less of anyone.
Shipped verbatim as /usr/share/usb.ids. Cached in dist/ and refreshed with Shipped verbatim as /usr/share/hwdata/usb.ids. Cached in dist/ and refreshed
`make usb-ids`, never automatically -- the version and date are in the file's with `make usb-ids`, never automatically -- the version and date are in the
own header, so what shipped is always knowable. file's own header, so what shipped is always knowable.
--------------------------------------------------------------------------------
pci.ids -- PCI vendor and device database BSD 3-Clause (dual-licensed)
--------------------------------------------------------------------------------
Maintained by Albert Pool, Martin Mares and the PCI ID Project volunteers
https://pci-ids.ucw.cz/
Distributed under the terms of *either* the GNU General Public License version
2 or later, *or* the 3-clause BSD Licence, as its header states. SchOS takes
the BSD option, as for usb.ids.
Shipped verbatim as /usr/share/hwdata/pci.ids. Cached in dist/ and refreshed
with `make pci-ids`, never automatically.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
Roboto Apache-2.0 Roboto Apache-2.0

View file

@ -208,6 +208,10 @@ typedef struct {
{ 0x8868E871, 0xE4F1, 0x11D3, { 0xBC, 0x22, 0x00, 0x80, 0xC7, 0x3C, 0x88, 0x81 } } { 0x8868E871, 0xE4F1, 0x11D3, { 0xBC, 0x22, 0x00, 0x80, 0xC7, 0x3C, 0x88, 0x81 } }
#define EFI_ACPI_10_TABLE_GUID \ #define EFI_ACPI_10_TABLE_GUID \
{ 0xEB9D2D30, 0x2D88, 0x11D3, { 0x9A, 0x16, 0x00, 0x90, 0x27, 0x3F, 0xC1, 0x4D } } { 0xEB9D2D30, 0x2D88, 0x11D3, { 0x9A, 0x16, 0x00, 0x90, 0x27, 0x3F, 0xC1, 0x4D } }
#define SMBIOS_TABLE_GUID \
{ 0xEB9D2D31, 0x2D88, 0x11D3, { 0x9A, 0x16, 0x00, 0x90, 0x27, 0x3F, 0xC1, 0x4D } }
#define SMBIOS3_TABLE_GUID \
{ 0xF2FD1544, 0x9794, 0x4A2C, { 0x99, 0x2E, 0xE5, 0xBB, 0xCF, 0x20, 0xE3, 0x94 } }
typedef struct { typedef struct {
EFI_GUID VendorGuid; EFI_GUID VendorGuid;

View file

@ -1674,23 +1674,32 @@ static UINT32
build_e820(const UINT8 *mapbuf, UINTN mapsize, UINTN descsize, build_e820(const UINT8 *mapbuf, UINTN mapsize, UINTN descsize,
struct e820entry *out) struct e820entry *out)
{ {
UINT32 nent = 0; // Sorted by address first. The specification does not promise an ordered
// map, firmware that hands one over unsorted exists, and merging only
// finds neighbours that sit next to each other in the list.
static struct e820entry raw[1024];
UINT32 nraw = 0, nent = 0;
for (UINTN off = 0; off + descsize <= mapsize && nent < E820_MAXENTRIES; for (UINTN off = 0; off + descsize <= mapsize && nraw < 1024;
off += descsize) { off += descsize) {
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off); EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off);
UINT32 type = efi_type_to_e820(d->Type); struct e820entry e = { d->PhysicalStart, d->NumberOfPages * 0x1000,
efi_type_to_e820(d->Type), 1 };
UINT32 i = nraw++;
while (i > 0 && raw[i - 1].base > e.base) {
raw[i] = raw[i - 1];
i--;
}
raw[i] = e;
}
if (nent > 0 && out[nent-1].type == type && for (UINT32 i = 0; i < nraw && nent < E820_MAXENTRIES; i++) {
out[nent-1].base + out[nent-1].length == d->PhysicalStart) { if (nent > 0 && out[nent-1].type == raw[i].type &&
out[nent-1].length += d->NumberOfPages * 0x1000; out[nent-1].base + out[nent-1].length == raw[i].base) {
out[nent-1].length += raw[i].length;
continue; continue;
} }
out[nent].base = d->PhysicalStart; out[nent++] = raw[i];
out[nent].length = d->NumberOfPages * 0x1000;
out[nent].type = type;
out[nent].acpi_ext = 1;
nent++;
} }
return nent; return nent;
} }
@ -1724,6 +1733,26 @@ find_rsdp(void)
return fallback; return fallback;
} }
// The SMBIOS entry point, the same way: 3.0's 64-bit one first, since a
// table above 4 GB is out of reach of the 2.x one.
static UINT64
find_smbios(void)
{
EFI_GUID sm3 = SMBIOS3_TABLE_GUID;
EFI_GUID sm2 = SMBIOS_TABLE_GUID;
EFI_CONFIGURATION_TABLE *t =
(EFI_CONFIGURATION_TABLE *)ST->ConfigurationTable;
UINT64 fallback = 0;
for (UINTN i = 0; i < ST->NumberOfTableEntries; i++) {
if (guid_eq(&t[i].VendorGuid, &sm3))
return (UINT64)(UINTN)t[i].VendorTable;
if (guid_eq(&t[i].VendorGuid, &sm2))
fallback = (UINT64)(UINTN)t[i].VendorTable;
}
return fallback;
}
EFI_STATUS EFI_STATUS
EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
{ {
@ -1769,6 +1798,7 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
// of it, for the same reason the e820 map is. // of it, for the same reason the e820 map is.
UINT64 rsdp = find_rsdp(); UINT64 rsdp = find_rsdp();
vprint(L"schos: acpi rsdp "); vprint_hex(rsdp); vprint(L"\r\n"); vprint(L"schos: acpi rsdp "); vprint_hex(rsdp); vprint(L"\r\n");
UINT64 smbios = find_smbios();
// The memory map, and then out. GetMemoryMap must be the last thing before // The memory map, and then out. GetMemoryMap must be the last thing before
// ExitBootServices: the key it returns is invalidated by any allocation, // ExitBootServices: the key it returns is invalidated by any allocation,
@ -1813,6 +1843,21 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
static struct e820entry e820[E820_MAXENTRIES]; static struct e820entry e820[E820_MAXENTRIES];
UINT32 nent = build_e820(mapbuf, mapsize, descsize, e820); UINT32 nent = build_e820(mapbuf, mapsize, descsize, e820);
// The hand-off block is written after ExitBootServices into memory nobody
// allocated, which is safe only while the firmware keeps nothing there
// that outlives it. Say so if it does, since the alternative is a kernel
// that dies in a way nothing points back at.
for (UINT32 i = 0; i < nent; i++)
if (e820[i].type != E820_USABLE &&
e820[i].base < SCHBOOT_E820_MAP + SCHBOOT_E820_MAX * 24 &&
e820[i].base + e820[i].length > SCHBOOT_BASE) {
print(L"schos: WARNING: firmware memory overlaps the hand-off "
L"block at 0x30000\r\n");
ui_progress_label("Firmware memory at 0x30000 -- may not boot");
BS->Stall(2000000);
break;
}
// Truncation is silent data loss: phystop comes from the largest usable // Truncation is silent data loss: phystop comes from the largest usable
// region, so dropping the tail of the map can leave the kernel believing a // region, so dropping the tail of the map can leave the kernel believing a
// 16 GB machine has a fraction of that. Say so while there is still a // 16 GB machine has a fraction of that. Say so while there is still a
@ -1863,6 +1908,8 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
copy((void *)SCHBOOT_E820_MAP, e820, nent * sizeof e820[0]); copy((void *)SCHBOOT_E820_MAP, e820, nent * sizeof e820[0]);
*(UINT32 *)SCHBOOT_E820_COUNT = nent; *(UINT32 *)SCHBOOT_E820_COUNT = nent;
*(UINT64 *)SCHBOOT_RSDP_ADDR = rsdp; *(UINT64 *)SCHBOOT_RSDP_ADDR = rsdp;
*(UINT64 *)SCHBOOT_SMBIOS_ADDR = smbios;
*(UINT32 *)SCHBOOT_EFI_FLAG = SCHBOOT_EFI_MAGIC;
*(UINT8 *)SCHBOOT_VERBOSE_FLAG = (UINT8)VERBOSE_BOOT; *(UINT8 *)SCHBOOT_VERBOSE_FLAG = (UINT8)VERBOSE_BOOT;
*(UINT8 *)SCHBOOT_DEBUG_FLAG = (UINT8)DEBUG_BOOT; *(UINT8 *)SCHBOOT_DEBUG_FLAG = (UINT8)DEBUG_BOOT;

938
cmd/hwreport/main.c Normal file
View file

@ -0,0 +1,938 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// hwreport -- write down what this machine is, onto something you can carry.
//
// hwreport onto the first FAT volume mounted read-write, or the
// first USB stick it can mount, or /root
// hwreport <dir> into <dir>
//
// It exists for the machines that do not work. A laptop SchOS half-supports
// has no network driver more often than not, and photos of a screen are a poor
// way to carry a boot log and the tables that explain it. So this collects
// what anybody debugging the port would ask for first -- the log, the PCI and
// USB devices and what claimed each, the ACPI tables, the memory map, the
// machine's name -- into one folder on a stick that opens on any other system.
//
// The layout is Linux's where Linux has one: the ACPI tables are the raw .dat
// files `iasl -d` disassembles, and lspci.txt holds what `lspci -nnk -xxx`
// would, so a report from here can be compared line for line with one taken
// on Linux on the same machine.
//
// What it leaves out on purpose: serial numbers and the system UUID (the
// kernel does not publish them), and the MSDM and SLIC tables, which hold a
// Windows licence key. A report is meant to be handed to somebody.
#include "types.h"
#include "user.h"
#include "fcntl.h"
#include "posix.h"
#include "stat.h"
// ---------------------------------------------------------------------------
// Small things a libc would have
// ---------------------------------------------------------------------------
static int
slen(const char *s)
{
int n = 0;
while (s[n]) n++;
return n;
}
static int
seq(const char *a, const char *b)
{
while (*a && *a == *b) { a++; b++; }
return *a == *b;
}
static int
sprefix(const char *s, const char *p)
{
while (*p && *s == *p) { s++; p++; }
return *p == 0;
}
static void
scpy(char *d, const char *s, int cap)
{
int n = 0;
while (s[n] && n < cap - 1) { d[n] = s[n]; n++; }
d[n] = 0;
}
// An output buffer, flushed to a file in one write -- each write to FAT is a
// whole operation with its own flush, so fewer is much faster.
struct buf { char *p; int n, cap; };
static char textmem[512 * 1024];
static struct buf T = { textmem, 0, sizeof textmem };
static void bc(struct buf *b, char c) { if (b->n < b->cap) b->p[b->n++] = c; }
static void bs(struct buf *b, const char *s) { while (*s) bc(b, *s++); }
static void bsn(struct buf *b, const char *s, int n) { while (n-- > 0) bc(b, *s++); }
static void
bu(struct buf *b, unsigned long long v)
{
char t[24];
int k = 0;
do { t[k++] = '0' + v % 10; v /= 10; } while (v);
while (k) bc(b, t[--k]);
}
static void
bx(struct buf *b, unsigned long long v, int digits)
{
static const char h[] = "0123456789abcdef";
char t[16];
int k = 0;
do { t[k++] = h[v & 0xF]; v >>= 4; } while (v || k < digits);
while (k) bc(b, t[--k]);
}
static void
bpad(struct buf *b, const char *s, int w)
{
int n = slen(s);
bs(b, s);
while (n++ < w) bc(b, ' ');
}
static void
say(const char *s)
{
write(1, (void *)s, slen(s));
}
static void
saybuf(struct buf *b)
{
write(1, b->p, b->n);
b->n = 0;
}
// ---------------------------------------------------------------------------
// Files
// ---------------------------------------------------------------------------
static char iobuf[64 * 1024];
// The whole of a small file into `out`, terminated. Length, or -1.
static int
slurp(const char *path, char *out, int cap)
{
int fd = open((char *)path, O_RDONLY), n = 0, r;
if (fd < 0)
return -1;
while (n < cap - 1 && (r = read(fd, out + n, cap - 1 - n)) > 0)
n += r;
close(fd);
out[n] = 0;
return n;
}
// One line of a file, trailing newline dropped; "" when it is missing.
static char *
line(const char *path)
{
static char l[8][256];
static int k;
char *s = l[k++ & 7];
if (slurp(path, s, 256) < 0)
s[0] = 0;
for (int i = 0; s[i]; i++)
if (s[i] == '\n') { s[i] = 0; break; }
return s;
}
static char dir[512];
static int nfiles, nfail;
static void
path_in(char *out, const char *name)
{
int n = 0;
for (const char *p = dir; *p && n < 500; p++) out[n++] = *p;
out[n++] = '/';
for (const char *p = name; *p && n < 510; p++) out[n++] = *p;
out[n] = 0;
}
static int
put_file(const char *name, const char *data, int len)
{
char p[520];
path_in(p, name);
int fd = open(p, O_WRONLY | O_CREATE | O_TRUNC);
if (fd < 0) {
nfail++;
return -1;
}
int done = 0;
while (done < len) {
int r = write(fd, (void *)(data + done), len - done);
if (r <= 0)
break;
done += r;
}
close(fd);
if (done != len) {
nfail++;
return -1;
}
nfiles++;
return 0;
}
static void
put_text(const char *name)
{
put_file(name, T.p, T.n);
T.n = 0;
}
// Stream `src` to `name`, for files of any size.
static long
copy_to(const char *src, const char *name)
{
char p[520];
int in = open((char *)src, O_RDONLY);
if (in < 0)
return -1;
path_in(p, name);
int fd = open(p, O_WRONLY | O_CREATE | O_TRUNC);
if (fd < 0) {
close(in);
nfail++;
return -1;
}
long total = 0;
int r;
while ((r = read(in, iobuf, sizeof iobuf)) > 0) {
if (write(fd, iobuf, r) != r) {
total = -1;
break;
}
total += r;
}
close(in);
close(fd);
if (total < 0)
nfail++;
else
nfiles++;
return total;
}
// Every entry of a directory but the dots, sorted, into names[].
#define MAXNAMES 256
static char names[MAXNAMES][64];
static int
list(const char *path)
{
static char dbuf[4096];
int fd = open((char *)path, O_RDONLY | O_DIRECTORY), n = 0, r;
if (fd < 0)
return 0;
while ((r = getdents64(fd, dbuf, sizeof dbuf)) > 0)
for (int off = 0; off < r; ) {
struct dirent64 *d = (struct dirent64 *)(dbuf + off);
off += d->d_reclen;
if (seq(d->d_name, ".") || seq(d->d_name, "..") || n >= MAXNAMES)
continue;
scpy(names[n++], d->d_name, 64);
}
close(fd);
for (int i = 1; i < n; i++) /* insertion sort; n is small */
for (int j = i; j > 0; j--) {
const char *a = names[j - 1], *b = names[j];
while (*a && *a == *b) { a++; b++; }
if ((unsigned char)*a <= (unsigned char)*b)
break;
char t[64];
scpy(t, names[j], 64);
scpy(names[j], names[j - 1], 64);
scpy(names[j - 1], t, 64);
}
return n;
}
static void
join(char *out, const char *a, const char *b)
{
int n = 0;
while (*a && n < 250) out[n++] = *a++;
out[n++] = '/';
while (*b && n < 510) out[n++] = *b++;
out[n] = 0;
}
// ---------------------------------------------------------------------------
// Where to write
// ---------------------------------------------------------------------------
static int mounted_here;
static const char *mnt = "/mnt/hwreport";
// The first read-write FAT mount in /proc/mounts.
static int
find_fat_mount(char *out, int cap)
{
static char m[4096];
if (slurp("/proc/mounts", m, sizeof m) < 0)
return -1;
for (char *l = m; *l; ) {
char *f[4] = { 0 };
int nf = 0;
char *e = l;
while (*e && *e != '\n') e++;
char save = *e;
*e = 0;
for (char *p = l; *p && nf < 4; ) {
while (*p == ' ') *p++ = 0;
if (!*p) break;
f[nf++] = p;
while (*p && *p != ' ') p++;
}
if (nf == 4 && seq(f[2], "vfat") && !sprefix(f[3], "ro")) {
scpy(out, f[1], cap);
return 0;
}
l = save ? e + 1 : e;
}
return -1;
}
// Mount the first USB disk or partition that takes FAT. Partitions first,
// since a stick almost always has one and its whole-disk node is the table.
static int
mount_usb(char *out, int cap)
{
int n = list("/dev");
mkdir("/mnt");
mkdir((char *)mnt);
for (int pass = 0; pass < 2; pass++)
for (int i = 0; i < n; i++) {
const char *nm = names[i];
if (!sprefix(nm, "usb"))
continue;
int haspart = 0;
for (const char *p = nm + 3; *p; p++)
if (*p == 'p') haspart = 1;
if (haspart != (pass == 0))
continue;
char dev[80];
join(dev, "/dev", nm);
if (mount(dev, mnt, "vfat", 0, 0) == 0) {
say("hwreport: mounted ");
say(dev);
say(" on ");
say(mnt);
say("\n");
mounted_here = 1;
scpy(out, mnt, cap);
return 0;
}
}
return -1;
}
// "hwreport-20261023-142501", or "hwreport-N" when the clock is not set.
static void
make_dir(const char *base)
{
struct timespec ts;
struct buf b = { dir, 0, sizeof dir - 1 };
bs(&b, base);
if (b.n > 1 || base[0] != '/')
bc(&b, '/');
bs(&b, "hwreport-");
if (clock_gettime(CLOCK_REALTIME, &ts) == 0 && ts.tv_sec > 946684800) {
// Days to a civil date: Howard Hinnant's algorithm.
long long s = ts.tv_sec, days = s / 86400, rem = s % 86400;
long long z = days + 719468, era = z / 146097;
long long doe = z - era * 146097;
long long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
long long y = yoe + era * 400, doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
long long mp = (5 * doy + 2) / 153, d = doy - (153 * mp + 2) / 5 + 1;
long long mo = mp < 10 ? mp + 3 : mp - 9;
if (mo <= 2) y++;
bu(&b, (unsigned long long)y);
if (mo < 10) bc(&b, '0');
bu(&b, (unsigned long long)mo);
if (d < 10) bc(&b, '0');
bu(&b, (unsigned long long)d);
bc(&b, '-');
long long hh = rem / 3600, mm = rem / 60 % 60, ss = rem % 60;
if (hh < 10) bc(&b, '0');
bu(&b, (unsigned long long)hh);
if (mm < 10) bc(&b, '0');
bu(&b, (unsigned long long)mm);
if (ss < 10) bc(&b, '0');
bu(&b, (unsigned long long)ss);
dir[b.n] = 0;
if (mkdir(dir) == 0)
return;
bc(&b, '-');
}
int keep = b.n;
for (int i = 1; i < 1000; i++) {
b.n = keep;
bu(&b, (unsigned long long)i);
dir[b.n] = 0;
if (mkdir(dir) == 0)
return;
}
dir[0] = 0;
}
// ---------------------------------------------------------------------------
// The report
// ---------------------------------------------------------------------------
static const char *
chassis_name(int t)
{
static const char *const n[] = {
0, "Other", "Unknown", "Desktop", "Low Profile Desktop", "Pizza Box",
"Mini Tower", "Tower", "Portable", "Laptop", "Notebook", "Hand Held",
"Docking Station", "All in One", "Sub Notebook", "Space-saving",
"Lunch Box", "Main Server Chassis", "Expansion Chassis", "SubChassis",
"Bus Expansion Chassis", "Peripheral Chassis", "RAID Chassis",
"Rack Mount Chassis", "Sealed-case PC", "Multi-system chassis",
"Compact PCI", "AdvancedTCA", "Blade", "Blade Enclosure", "Tablet",
"Convertible", "Detachable", "IoT Gateway", "Embedded PC", "Mini PC",
"Stick PC",
};
return t > 0 && t < (int)(sizeof n / sizeof n[0]) ? n[t] : "?";
}
static int
atoi_(const char *s)
{
int v = 0;
while (*s >= '0' && *s <= '9') v = v * 10 + (*s++ - '0');
return v;
}
static unsigned
hexv(const char *s)
{
unsigned v = 0;
if (s[0] == '0' && s[1] == 'x') s += 2;
for (;; s++) {
int c = *s;
if (c >= '0' && c <= '9') v = v * 16 + (unsigned)(c - '0');
else if (c >= 'a' && c <= 'f') v = v * 16 + (unsigned)(c - 'a' + 10);
else if (c >= 'A' && c <= 'F') v = v * 16 + (unsigned)(c - 'A' + 10);
else break;
}
return v;
}
// The value of KEY= in a uevent, or "".
static const char *
uevent_get(const char *ev, const char *key, char *out, int cap)
{
int kl = slen(key);
out[0] = 0;
for (const char *l = ev; *l; ) {
if (sprefix(l, key) && l[kl] == '=') {
int n = 0;
for (const char *p = l + kl + 1; *p && *p != '\n' && n < cap - 1; p++)
out[n++] = *p;
out[n] = 0;
break;
}
while (*l && *l != '\n') l++;
if (*l) l++;
}
return out;
}
static struct buf S = { 0, 0, 0 }; // summary.txt, built alongside
static char summem[64 * 1024];
static void
dmi_section(void)
{
static const char *const f[] = {
"sys_vendor", "product_name", "product_version", "product_family",
"product_sku", "board_vendor", "board_name", "board_version",
"bios_vendor", "bios_version", "bios_date", "bios_release",
"chassis_vendor", "chassis_type", "chassis_version",
};
char p[128];
for (int i = 0; i < (int)(sizeof f / sizeof f[0]); i++) {
join(p, "/sys/class/dmi/id", f[i]);
bpad(&T, f[i], 17);
bs(&T, line(p));
bc(&T, '\n');
}
put_text("dmi.txt");
char a[256], b[256];
scpy(a, line("/sys/class/dmi/id/sys_vendor"), 256);
scpy(b, line("/sys/class/dmi/id/product_name"), 256);
bs(&S, "Machine: ");
bs(&S, a[0] || b[0] ? a : "(no SMBIOS table)");
bc(&S, ' ');
bs(&S, b);
const char *v = line("/sys/class/dmi/id/product_version");
if (v[0]) { bs(&S, " / "); bs(&S, v); }
const char *fam = line("/sys/class/dmi/id/product_family");
if (fam[0]) { bs(&S, " ("); bs(&S, fam); bc(&S, ')'); }
bs(&S, "\nBoard: ");
bs(&S, line("/sys/class/dmi/id/board_vendor")); bc(&S, ' ');
bs(&S, line("/sys/class/dmi/id/board_name")); bc(&S, ' ');
bs(&S, line("/sys/class/dmi/id/board_version"));
bs(&S, "\nFirmware: ");
bs(&S, line("/sys/class/dmi/id/bios_vendor")); bc(&S, ' ');
bs(&S, line("/sys/class/dmi/id/bios_version")); bc(&S, ' ');
bs(&S, line("/sys/class/dmi/id/bios_date"));
{
struct stat st;
bs(&S, stat("/sys/firmware/efi", &st) == 0 ? ", booted by UEFI" :
", booted by BIOS");
}
const char *ct = line("/sys/class/dmi/id/chassis_type");
if (ct[0]) {
bs(&S, "\nChassis: ");
bs(&S, chassis_name(atoi_(ct)));
bs(&S, " (type ");
bs(&S, ct);
bc(&S, ')');
}
bc(&S, '\n');
}
static void
cpu_mem_section(void)
{
static char ci[16 * 1024];
int ncpu = 0;
char model[128] = "";
if (slurp("/proc/cpuinfo", ci, sizeof ci) >= 0)
for (char *l = ci; *l; ) {
if (sprefix(l, "processor"))
ncpu++;
if (!model[0] && sprefix(l, "model name")) {
char *p = l;
while (*p && *p != ':') p++;
if (*p) p++;
while (*p == ' ') p++;
int n = 0;
while (p[n] && p[n] != '\n' && n < 127) { model[n] = p[n]; n++; }
model[n] = 0;
}
while (*l && *l != '\n') l++;
if (*l) l++;
}
bs(&S, "CPU: ");
bs(&S, model[0] ? model : "?");
bs(&S, ", ");
bu(&S, (unsigned long long)ncpu);
bs(&S, ncpu == 1 ? " processor running\n" : " processors running\n");
bs(&S, "Memory: ");
char *mt = line("/proc/meminfo");
while (*mt && *mt != ':') mt++;
if (*mt) mt++;
while (*mt == ' ') mt++;
bs(&S, mt);
bs(&S, " usable\nSchOS: ");
bs(&S, line("/proc/version"));
bc(&S, '\n');
}
static const char *
pci_class_name(unsigned cls)
{
switch (cls >> 8) {
case 0x0100: return "SCSI"; case 0x0101: return "IDE";
case 0x0104: return "RAID"; case 0x0106: return "SATA";
case 0x0108: return "NVMe"; case 0x0200: return "Ethernet";
case 0x0280: return "Network (Wi-Fi?)"; case 0x0300: return "VGA";
case 0x0302: return "3D controller"; case 0x0380: return "Display";
case 0x0401: return "Audio"; case 0x0403: return "HD Audio";
case 0x0480: return "Multimedia"; case 0x0600: return "Host bridge";
case 0x0601: return "ISA bridge"; case 0x0604: return "PCI bridge";
case 0x0680: return "Bridge"; case 0x0805: return "SD host";
case 0x0880: return "System peripheral";case 0x0c03: return "USB";
case 0x0c05: return "SMBus"; case 0x0c80: return "Serial bus";
case 0x0d00: return "IrDA"; case 0x0d11: return "Bluetooth";
case 0x1180: return "Signal processing";case 0x0c09: return "CANbus";
case 0x0c10: return "Thunderbolt"; case 0x0780: return "Communication";
case 0x0700: return "Serial"; case 0x0c00: return "FireWire";
}
switch (cls >> 16) {
case 0x01: return "Storage"; case 0x02: return "Network";
case 0x03: return "Display"; case 0x04: return "Multimedia";
case 0x06: return "Bridge"; case 0x08: return "System";
case 0x0c: return "Serial bus";
}
return "Other";
}
static void
pci_section(void)
{
static char ev[1024], cfg[256];
char p[256], q[256], drv[32], id[32];
int n = list("/sys/bus/pci/devices"), nodrv = 0;
struct buf L = { 0, 0, 0 };
static char lmem[160 * 1024];
L.p = lmem; L.cap = sizeof lmem;
// Copy the names: list() is reused below.
static char devs[MAXNAMES][64];
for (int i = 0; i < n; i++)
scpy(devs[i], names[i], 64);
bs(&S, "\nPCI devices (");
bu(&S, (unsigned long long)n);
bs(&S, "):\n");
for (int i = 0; i < n; i++) {
join(p, "/sys/bus/pci/devices", devs[i]);
join(q, p, "uevent");
if (slurp(q, ev, sizeof ev) < 0)
continue;
join(q, p, "class");
unsigned cls = hexv(line(q));
join(q, p, "revision");
unsigned rev = hexv(line(q));
uevent_get(ev, "DRIVER", drv, sizeof drv);
uevent_get(ev, "PCI_ID", id, sizeof id);
for (char *c = id; *c; c++)
if (*c >= 'A' && *c <= 'F') *c += 'a' - 'A';
char sub[32];
uevent_get(ev, "PCI_SUBSYS_ID", sub, sizeof sub);
for (char *c = sub; *c; c++)
if (*c >= 'A' && *c <= 'F') *c += 'a' - 'A';
// lspci -nnk shape.
const char *bdf = devs[i] + 5;
bs(&L, bdf);
bc(&L, ' ');
bs(&L, pci_class_name(cls));
bs(&L, " [");
bx(&L, cls >> 8, 4);
bs(&L, "]: [");
bs(&L, id);
bc(&L, ']');
if (rev) { bs(&L, " (rev "); bx(&L, rev, 2); bc(&L, ')'); }
bs(&L, "\n\tSubsystem: [");
bs(&L, sub);
bs(&L, "]\n");
if (drv[0]) { bs(&L, "\tKernel driver in use: "); bs(&L, drv); bc(&L, '\n'); }
join(q, p, "irq");
bs(&L, "\tIRQ: ");
bs(&L, line(q));
bc(&L, '\n');
join(q, p, "config");
int cl = 0, fd = open(q, O_RDONLY);
if (fd >= 0) { cl = read(fd, cfg, 256); close(fd); }
for (int o = 0; o < cl; o += 16) {
bx(&L, (unsigned)o, 2);
bc(&L, ':');
for (int k = 0; k < 16 && o + k < cl; k++) {
bc(&L, ' ');
bx(&L, (unsigned char)cfg[o + k], 2);
}
bc(&L, '\n');
}
bc(&L, '\n');
bs(&S, " ");
bs(&S, bdf);
bc(&S, ' ');
bs(&S, id);
bc(&S, ' ');
bpad(&S, pci_class_name(cls), 18);
if (drv[0]) {
bs(&S, drv);
} else {
bs(&S, "-- no driver");
nodrv++;
}
bc(&S, '\n');
}
if (n == 0)
bs(&S, " (none found; is /sys mounted?)\n");
else if (nodrv) {
bs(&S, " ");
bu(&S, (unsigned long long)nodrv);
bs(&S, " without a driver.\n");
}
put_file("lspci.txt", L.p, L.n);
}
static void
usb_section(void)
{
static char ev[1024];
char p[256], q[256], drv[32], prod[32];
int n = list("/sys/bus/usb/devices");
static char devs[MAXNAMES][64];
for (int i = 0; i < n; i++)
scpy(devs[i], names[i], 64);
bs(&S, "\nUSB devices (");
bu(&S, (unsigned long long)n);
bs(&S, "):\n");
for (int i = 0; i < n; i++) {
join(p, "/sys/bus/usb/devices", devs[i]);
join(q, p, "uevent");
if (slurp(q, ev, sizeof ev) < 0)
continue;
uevent_get(ev, "DRIVER", drv, sizeof drv);
uevent_get(ev, "PRODUCT", prod, sizeof prod);
join(q, p, "idVendor");
const char *vid = line(q);
join(q, p, "idProduct");
const char *pid = line(q);
join(q, p, "speed");
const char *spd = line(q);
bs(&T, devs[i]);
bs(&T, ": ID ");
bs(&T, vid); bc(&T, ':'); bs(&T, pid);
bs(&T, ", ");
bs(&T, spd);
bs(&T, " Mb/s, driver ");
bs(&T, drv[0] ? drv : "(none)");
bs(&T, "\n");
bsn(&T, ev, slen(ev));
bc(&T, '\n');
bs(&S, " ");
bpad(&S, devs[i], 8);
bs(&S, vid); bc(&S, ':'); bs(&S, pid);
bs(&S, " ");
bpad(&S, spd, 5);
bs(&S, " Mb/s ");
bs(&S, drv[0] ? drv : "-- no driver");
bc(&S, '\n');
}
put_text("lsusb.txt");
}
static void
memmap_section(void)
{
char p[256], q[256];
int n = list("/sys/firmware/memmap");
static char ent[MAXNAMES][64];
unsigned long long ram = 0;
for (int i = 0; i < n; i++)
scpy(ent[i], names[i], 64);
// Numerically, not as the names sort.
for (int want = 0; want < n; want++) {
char nm[16];
struct buf b = { nm, 0, 15 };
bu(&b, (unsigned long long)want);
nm[b.n] = 0;
join(p, "/sys/firmware/memmap", nm);
join(q, p, "start");
char s[64];
scpy(s, line(q), 64);
join(q, p, "end");
char e[64];
scpy(e, line(q), 64);
join(q, p, "type");
const char *t = line(q);
bpad(&T, s, 20);
bpad(&T, e, 20);
bs(&T, t);
bc(&T, '\n');
if (seq(t, "System RAM")) {
unsigned long long a = 0, z = 0;
for (const char *c = s + 2; *c; c++)
a = a * 16 + (unsigned long long)(*c <= '9' ? *c - '0' : (*c | 32) - 'a' + 10);
for (const char *c = e + 2; *c; c++)
z = z * 16 + (unsigned long long)(*c <= '9' ? *c - '0' : (*c | 32) - 'a' + 10);
ram += z - a + 1;
}
}
put_text("memmap.txt");
bs(&S, "Firmware memory map: ");
bu(&S, (unsigned long long)n);
bs(&S, " entries, ");
bu(&S, ram >> 20);
bs(&S, " MB of RAM\n");
}
static void
acpi_section(void)
{
char p[256], q[256];
static unsigned char hdr[36];
int n = list("/sys/firmware/acpi/tables"), skipped = 0, copied = 0;
static char tabs[MAXNAMES][64];
for (int i = 0; i < n; i++)
scpy(tabs[i], names[i], 64);
mkdir((path_in(p, "acpi"), p));
bs(&T, "name length rev oem oem table oem rev\n");
for (int i = 0; i < n; i++) {
join(p, "/sys/firmware/acpi/tables", tabs[i]);
int fd = open(p, O_RDONLY), h = 0;
if (fd >= 0) { h = read(fd, hdr, 36); close(fd); }
// A Windows licence key, which belongs to the machine's owner.
if (sprefix(tabs[i], "MSDM") || sprefix(tabs[i], "SLIC")) {
bpad(&T, tabs[i], 9);
bs(&T, "(left out: holds a Windows licence key)\n");
skipped++;
continue;
}
if (h >= 36) {
unsigned len = hdr[4] | hdr[5] << 8 | hdr[6] << 16 | (unsigned)hdr[7] << 24;
bpad(&T, tabs[i], 9);
char num[16];
struct buf b = { num, 0, 15 };
bu(&b, len);
num[b.n] = 0;
bpad(&T, num, 8);
bu(&T, hdr[8]);
bs(&T, hdr[8] < 10 ? " " : " ");
bsn(&T, (char *)hdr + 10, 6);
bs(&T, " ");
bsn(&T, (char *)hdr + 16, 8);
bs(&T, " 0x");
bx(&T, hdr[24] | hdr[25] << 8 | hdr[26] << 16 | (unsigned)hdr[27] << 24, 8);
bc(&T, '\n');
} else {
bpad(&T, tabs[i], 9);
bs(&T, "(no header)\n");
}
char dst[80];
struct buf b = { dst, 0, 70 };
bs(&b, "acpi/");
bs(&b, tabs[i]);
bs(&b, ".dat");
dst[b.n] = 0;
(void)q;
if (copy_to(p, dst) >= 0)
copied++;
}
put_text("acpi/tables.txt");
bs(&S, "ACPI tables: ");
bu(&S, (unsigned long long)copied);
bs(&S, " saved in acpi/");
if (skipped) {
bs(&S, " (");
bu(&S, (unsigned long long)skipped);
bs(&S, " left out: Windows licence key)");
}
if (n == 0)
bs(&S, " -- none found, so no ACPI at all");
bc(&S, '\n');
}
static void
proc_copies(void)
{
static const char *const f[] = {
"cpuinfo", "meminfo", "interrupts", "devices", "partitions", "mounts",
"pci", "netif", "version", "bootmode", "filesystems", "fsstat", "stat",
};
char src[64], dst[64];
mkdir((path_in(dst, "proc"), dst));
for (int i = 0; i < (int)(sizeof f / sizeof f[0]); i++) {
struct buf a = { src, 0, 60 }, b = { dst, 0, 60 };
bs(&a, "/proc/"); bs(&a, f[i]); src[a.n] = 0;
bs(&b, "proc/"); bs(&b, f[i]); bs(&b, ".txt"); dst[b.n] = 0;
copy_to(src, dst);
}
bs(&S, "\nDisks and partitions:\n");
static char pt[4096];
if (slurp("/proc/partitions", pt, sizeof pt) > 0)
bs(&S, pt);
}
int
main(int argc, char **argv)
{
char base[256];
S.p = summem;
S.cap = sizeof summem;
if (argc > 1 && (seq(argv[1], "-h") || seq(argv[1], "--help"))) {
say("usage: hwreport [dir]\n"
"Writes a folder describing this machine: the boot log, PCI and USB\n"
"devices, ACPI tables, memory map and firmware identity. With no dir,\n"
"onto a mounted FAT volume or the first USB stick that mounts.\n");
exit();
}
if (argc > 1)
scpy(base, argv[1], sizeof base);
else if (find_fat_mount(base, sizeof base) != 0 &&
mount_usb(base, sizeof base) != 0) {
say("hwreport: no FAT volume mounted and no USB stick would mount;\n"
" writing to /root instead. Plug a FAT32 stick in and run\n"
" it again, or give a directory: hwreport /mnt\n");
scpy(base, "/root", sizeof base);
}
make_dir(base);
if (!dir[0]) {
say("hwreport: cannot create a folder in ");
say(base);
say("\n");
exit();
}
say("hwreport: writing ");
say(dir);
say("\n");
bs(&S, "SchOS hardware report\n=====================\n\n");
dmi_section();
cpu_mem_section();
memmap_section();
acpi_section();
pci_section();
usb_section();
proc_copies();
long kl = copy_to("/proc/kmsg", "dmesg.txt");
bs(&S, "\nFiles: summary.txt, dmesg.txt (the boot log");
if (kl >= 0) { bs(&S, ", "); bu(&S, (unsigned long long)kl); bs(&S, " bytes"); }
bs(&S, "), dmi.txt, lspci.txt, lsusb.txt,\n"
"memmap.txt, acpi/*.dat (iasl -d disassembles them), proc/*.txt\n");
put_file("summary.txt", S.p, S.n);
// On the screen too, for a machine whose stick is about to be pulled.
saybuf(&S);
sync();
if (mounted_here) {
if (umount2(mnt, 0) == 0)
say("\nhwreport: unmounted; the stick can be pulled out.\n");
else
say("\nhwreport: written and synced, but the unmount failed.\n");
}
struct buf r = { iobuf, 0, 200 };
bs(&r, "hwreport: ");
bu(&r, (unsigned long long)nfiles);
bs(&r, " files in ");
bs(&r, dir);
if (nfail) {
bs(&r, ", ");
bu(&r, (unsigned long long)nfail);
bs(&r, " could not be written");
}
bs(&r, "\n");
saybuf(&r);
exit();
}

View file

@ -220,6 +220,10 @@ bootstrap(void)
// whole SchFS transaction. // whole SchFS transaction.
mkdir("/sys"); mkdir("/sys");
mkdir("/run"); mkdir("/run");
// The hardware, at Linux's paths, which is where lspci, lsusb and
// hwreport look for it. See kernel/fs/sysfs.c.
mount("sysfs", "/sys", "sysfs", 0, 0);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -104,6 +104,7 @@ good chance it is in there.
| [testing.md](guides/testing.md) | The harnesses, and what each one actually proves. | | [testing.md](guides/testing.md) | The harnesses, and what each one actually proves. |
| [layout.md](guides/layout.md) | Repository map, and how to add a program or a port. | | [layout.md](guides/layout.md) | Repository map, and how to add a program or a port. |
| [adding-a-driver.md](guides/adding-a-driver.md) | Writing one, binding it, publishing its class vtable. | | [adding-a-driver.md](guides/adding-a-driver.md) | Writing one, binding it, publishing its class vtable. |
| [hardware-report.md](guides/hardware-report.md) | `hwreport`: describing a machine onto a USB stick, for whoever fixes it. |
## branding/ — the marks ## branding/ — the marks

View file

@ -52,8 +52,11 @@ and `entry.S` so the two cannot disagree.
| `+0x000` | VBE-shaped mode info: LFB address, stride, width, height, depth | | `+0x000` | VBE-shaped mode info: LFB address, stride, width, height, depth |
| `+0x100` | Verbose-boot flag | | `+0x100` | Verbose-boot flag |
| `+0x101` | Boot medium (`1` = CD; `0` = not stated) | | `+0x101` | Boot medium (`1` = CD; `0` = not stated) |
| `+0x200` | E820 entry count, then up to 40 × 24-byte entries | | `+0x200` | E820 entry count |
| `+0x600` | ACPI RSDP physical address, or 0 | | `+0x600` | ACPI RSDP physical address, or 0 |
| `+0x638` | SMBIOS entry point physical address, or 0 |
| `+0x640` | `"EFI!"` when the UEFI loader booted the kernel |
| `+0x1000` | the E820 entries, up to 256 × 24 bytes |
Two of these exist because of specific failures worth knowing about. Two of these exist because of specific failures worth knowing about.
@ -65,8 +68,14 @@ interrupt routing — and the machine boots to a prompt on polled serial output
and then ignores every keystroke, which looks like a broken console rather than and then ignores every keystroke, which looks like a broken console rather than
a missing table. a missing table.
**The E820 cap is 40 entries, not 32.** Real UEFI firmware produces far more **The E820 entries have an area of their own**, room for 256. They used to sit
descriptors than QEMU's, and the space was already there. between the count and the RSDP, which held 40, and a laptop's UEFI map is 60 to
150 descriptors. The firmware lists RAM above 4 GB last, so that was what fell
off: a 16 GB machine came up with 2 or 3 GB of it.
**The SMBIOS entry point is passed the same way as the RSDP**, from the
configuration table, preferring the 64-bit `_SM3_` one. It names the machine in
the boot log's `DMI:` line and in `/sys/class/dmi/id`.
### Choosing a display, on a machine that has more than one ### Choosing a display, on a machine that has more than one
@ -174,13 +183,14 @@ schos: memory map larger than the loader's buffer (status 0x8000000000000005)
QEMU's own map needs 5664 bytes — 118 descriptors — so the real number on QEMU's own map needs 5664 bytes — 118 descriptors — so the real number on
hardware is larger still, and 256 KB is 5461 descriptors.) hardware is larger still, and 256 KB is 5461 descriptors.)
**A second limit is nearby and is only a warning.** `SCHBOOT_E820_MAX` is 40 **A second limit is nearby and is only a warning.** `SCHBOOT_E820_MAX` is 256
entries, fixed by the hand-off block's layout. `build_e820()` merges adjacent entries. `build_e820()` sorts the descriptors by address first (the
regions of the same type, so 40 survives a long map — but if it ever truncates, specification does not promise an ordered map) and then merges neighbours of
what gets dropped is the *high* end (the map arrives sorted, and `phystop` is the same type, so a long map fits easily. If one ever truncates, it says so on
capped below 4 GB regardless). It now says so on screen as well as on the screen as well as on the serial line, because by that point `print()` goes to a
serial line, because by that point `print()` goes to a port a laptop does not port a laptop does not have. The kernel keeps at most 32 usable regions; past
have. that, the smallest give way to larger ones, so what is lost is crumbs rather
than whatever was listed last.
### Two signals for a machine that cannot speak ### Two signals for a machine that cannot speak

View file

@ -119,8 +119,8 @@ during path resolution. `/proc` is mounted by init:
mount("proc", "/proc", "proc", 0, 0); mount("proc", "/proc", "proc", 0, 0);
``` ```
The filesystem types are `proc`, `devtmpfs`, `schfs` and `vfat` (also accepted The filesystem types are `proc`, `devtmpfs`, `sysfs`, `schfs` and `vfat` (also
as `msdos` and `fat`). Unmounting a FAT volume while anything on it is open, or accepted as `msdos` and `fat`). Unmounting a FAT volume while anything on it is open, or
is some process's working directory, fails with `EBUSY`. is some process's working directory, fails with `EBUSY`.
## procfs ## procfs
@ -222,6 +222,46 @@ selftest, `t` text specimen, `n` the netdev ARP selftest.
--- ---
## sysfs
`/sys`, in Linux's layout for the parts that exist here. `kernel/fs/sysfs.c`,
mounted by init with `mount("sysfs", "/sys", "sysfs", 0, 0)`.
| path | contents |
|------|----------|
| `/sys/bus/pci/devices/0000:BB:DD.F/` | `vendor device class revision subsystem_vendor subsystem_device irq resource config uevent` |
| `/sys/bus/usb/devices/B-P[.P...]/` | `idVendor idProduct bcdDevice busnum devnum speed bDeviceClass bDeviceSubClass bDeviceProtocol bNumConfigurations uevent` |
| `/sys/firmware/acpi/tables/` | each ACPI table, raw; a repeated signature is numbered from 1 (`SSDT1`, `SSDT2`) |
| `/sys/firmware/dmi/tables/` | `smbios_entry_point` and `DMI`, raw |
| `/sys/firmware/memmap/N/` | `start end type`, the firmware's map as the loader handed it over |
| `/sys/firmware/efi/` | present when UEFI booted the kernel; empty |
| `/sys/class/dmi/id/` | `sys_vendor product_name product_version board_name bios_version`, and the rest Linux has, except serials and the UUID |
**Linux's layout because everything that reads hardware reads it from there.**
toybox's `lspci` and `lsusb` walk `/sys/bus/*/devices` and parse each `uevent`
(`DRIVER=`, `PCI_ID=`, `PRODUCT=`, `BUSNUM=`), so with these files they work
unmodified, with names from `/usr/share/hwdata/pci.ids` and `usb.ids`. Formats
follow Linux to the character: `0x8086` in `vendor`, `%04x` in `idVendor`,
`PCI_CLASS` in upper case.
Like `/proc`, nothing is stored: a listing comes from the live PCI list, the
USB core's table of attached devices, the ACPI root table and the SMBIOS table
at the moment it is read, so a stick shows up in `/sys/bus/usb/devices` as soon
as it is attached. Text files report size 0, as `/proc`'s do; binary ones (the
tables, `config`) report their real length, since tools copy them by it.
What is missing, compared with Linux: `/sys/devices` and its symlinks (the
`bus/*/devices` entries are directories rather than links into it),
`/sys/class` beyond `dmi`, `/sys/block`, and anything writable. PCI `config` is
256 bytes, because config space is read through port `0xCF8`, which reaches
no further.
The machine's identity comes from SMBIOS (`kernel/dev/dmi.c`): the UEFI loader
hands over the entry point from the firmware's configuration table, and the
kernel falls back to scanning `0xF0000`-`0xFFFFF`. The boot log carries Linux's
line for it, `DMI: <vendor> <product>/<board>, BIOS <version> <date>`.
## Errno, and why `return -1` is a bug ## Errno, and why `return -1` is a bug
A syscall that fails returns a negative errno. `-1` is therefore **`-EPERM`**, A syscall that fails returns a negative errno. `-1` is therefore **`-EPERM`**,

View file

@ -0,0 +1,59 @@
# Reporting a machine
When SchOS half-works on a machine (no network, a dead touchpad, a disk it
cannot see), the fastest fix starts with a description of that machine.
`hwreport` writes one onto a USB stick that any other computer can read.
```
hwreport # onto a mounted FAT volume, or the first USB stick that mounts
hwreport /mnt # into a directory of your choosing
```
1. Plug in a FAT32 USB stick. Nearly every stick comes formatted that way.
2. Run `hwreport`. It mounts the stick itself if nothing is mounted, writes a
folder named `hwreport-YYYYMMDD-HHMMSS`, syncs, and unmounts again.
3. Pull the stick out once it says so, and send the folder from another
computer. Zip it first if your mail or chat client prefers one file.
It also prints `summary.txt` on the screen. On a machine where even the USB
port is not working, a photo of that screen is the next best thing.
## What is in the folder
| file | what it is |
|------|------------|
| `summary.txt` | machine, firmware, CPU, memory, and every PCI and USB device with the driver that claimed it, or `-- no driver` |
| `dmesg.txt` | the kernel log from this boot (and earlier ones, if a crash left them in memory) |
| `lspci.txt` | each PCI device the way `lspci -nnk -xxx` shows it: IDs, subsystem, driver, IRQ, the first 256 bytes of config space |
| `lsusb.txt` | each USB device and its `uevent` |
| `dmi.txt` | the SMBIOS identity: vendor, model, board, BIOS version |
| `memmap.txt` | the firmware's memory map |
| `acpi/*.dat` | every ACPI table, byte for byte; `iasl -d DSDT.dat` disassembles one |
| `acpi/tables.txt` | the table list with lengths and OEM IDs |
| `proc/*.txt` | `/proc` files: interrupts, devices, partitions, mounts and more |
The file names and formats are Linux's, so a report taken here can be compared
with `lspci`, `acpidump` and `dmesg` output taken under Linux on the same
machine.
## What is left out
- **Serial numbers and the system UUID.** The kernel does not publish them.
- **The MSDM and SLIC ACPI tables.** They hold the machine's Windows licence
key. `acpi/tables.txt` lists them as left out.
Nothing else in the report identifies its owner. Read `summary.txt` before
sending it if you want to be sure.
## Where the information comes from
All of it comes from `/sys`, laid out the way Linux lays it out (see
[filesystem.md](../architecture/filesystem.md#sysfs)), plus `/proc`. The same
files are there to read by hand:
```
cat /sys/class/dmi/id/product_name
ls /sys/firmware/acpi/tables
lspci -nnk
lsusb
```

View file

@ -170,6 +170,11 @@ struct inode {
// The synthetic /dev. See kernel/fs/devfs.c. // The synthetic /dev. See kernel/fs/devfs.c.
#define DEVFS_DEV 81 #define DEVFS_DEV 81
// The synthetic /sys. See kernel/fs/sysfs.c.
#define SYSFS_DEV 82
// Graft it over `target`; 0 or a negative errno. In sysfs.c.
int sysfs_mount(const char *target);
// Mounted FAT volumes, FAT_DEV onward. See include/fat.h. // Mounted FAT volumes, FAT_DEV onward. See include/fat.h.
// Linux's limits, for every filesystem behind the VFS. A path component can be // Linux's limits, for every filesystem behind the VFS. A path component can be

63
include/hwinfo.h Normal file
View file

@ -0,0 +1,63 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// What the firmware and the buses said about this machine, kept so /sys can
// say it again. kernel/fs/sysfs.c is the reader; each producer is named
// beside its part.
//
// Read-only views, copied out under whatever lock the producer has, so a
// caller never holds a pointer into a table that may change under it.
// --- the memory map, as the loader handed it over (kernel/arch/x86_64/e820.c)
struct e820_rec {
uint64 base, len;
uint32 type; // E820: 1 RAM, 2 reserved, 3 ACPI, 4 NVS, 5 bad
};
int e820_nth(int i, struct e820_rec *out); // 0, or -1 past the end
// The RAM the page allocator was given, in bytes: MemTotal and sysinfo's
// totalram. Not PHYSTOP, which is an address and counts the PCI hole.
uint64 ram_total_bytes(void);
// --- ACPI tables (kernel/arch/x86_64/acpi.c)
//
// Every table the RSDT or XSDT lists, then the DSDT and FACS, which only the
// FADT points at. `sig` gets four characters, no terminator. 0 for a table,
// 1 for an empty slot (keep counting), -1 past the end.
int acpi_table_nth(int i, uint64 *pa, uint32 *len, char sig[4]);
// --- SMBIOS (kernel/dev/dmi.c)
void dmi_init(void);
// The entry point structure and the table it describes, both physical.
int dmi_entry(uint64 *pa, uint32 *len);
int dmi_table(uint64 *pa, uint32 *len);
// Bytes of physical memory, through the direct map or ioremap. 0 on failure.
void *phys_view(uint64 pa, uint64 len);
// The strings Linux publishes in /sys/class/dmi/id, under its names. Serial
// numbers and the UUID are left out on purpose: nothing here needs them, and a
// hardware report is meant to be handed to somebody else.
enum {
DMI_BIOS_VENDOR, DMI_BIOS_VERSION, DMI_BIOS_DATE, DMI_BIOS_RELEASE,
DMI_SYS_VENDOR, DMI_PRODUCT_NAME, DMI_PRODUCT_VERSION, DMI_PRODUCT_SKU,
DMI_PRODUCT_FAMILY,
DMI_BOARD_VENDOR, DMI_BOARD_NAME, DMI_BOARD_VERSION,
DMI_CHASSIS_VENDOR, DMI_CHASSIS_TYPE, DMI_CHASSIS_VERSION,
DMI_NFIELDS
};
extern const char *const dmi_field_names[DMI_NFIELDS];
// Length written (no terminator counted), or -1 if the machine does not say.
int dmi_field(int f, char *out, int cap);
// 1 if the UEFI loader booted this kernel.
int boot_is_efi(void);
// --- USB devices (kernel/drivers/usb/core/usb.c), one per addressed device
struct usb_info {
int bus, devnum, speed; // speed: USB_SPEED_*
uint16 vid, pid, bcd;
uint8 cls, subcls, proto; // the device descriptor's
uint8 nconf;
char path[24]; // Linux's name: "1-3", "2-1.4"
char driver[16]; // the first interface's, or empty
};
int usb_info_nth(int i, struct usb_info *out);

View file

@ -85,16 +85,18 @@
#define SCHBOOT_MEDIA_SEG 0x3000 #define SCHBOOT_MEDIA_SEG 0x3000
#define SCHBOOT_MEDIA_OFF 0x101 #define SCHBOOT_MEDIA_OFF 0x101
// The E820 memory map: a uint32 count, then that many 24-byte entries. Capped // The E820 memory map: a uint32 count at BASE+0x200, and the entries, 24 bytes
// at 32 so a pathological firmware cannot run past the buffer; the map ends at // each, in a page-and-a-half of their own from BASE+0x1000.
// 0x508, which is what leaves 0x600 free below. //
// The entries used to sit between the count and the RSDP field, which left
// room for 40. A laptop's UEFI map is 60 to 150 descriptors, and even after
// the loader merges neighbours of the same type the RAM above 4 GB -- which
// the firmware lists last -- was what fell off the end: a 16 GB machine came
// up with 2 or 3 of it. 256 is more than any map seen merges to, and the area
// it takes, 0x31000-0x32800, is clear of everything else at BASE.
#define SCHBOOT_E820_COUNT (SCHBOOT_BASE + 0x200) #define SCHBOOT_E820_COUNT (SCHBOOT_BASE + 0x200)
#define SCHBOOT_E820_MAP (SCHBOOT_BASE + 0x208) #define SCHBOOT_E820_MAP (SCHBOOT_BASE + 0x1000)
/* 40, not 32. The map lives at SCHBOOT_E820_MAP (BASE+0x208) and the next #define SCHBOOT_E820_MAX 256
* field is SCHBOOT_RSDP_ADDR at BASE+0x600, so there are 1016 bytes and each
* entry is 24 -- 42 fit. Real UEFI firmware produces far more descriptors than
* QEMU's, and the headroom costs nothing that was being used. */
#define SCHBOOT_E820_MAX 40
// Where the loader leaves the ACPI RSDP, as a little-endian 64-bit physical // Where the loader leaves the ACPI RSDP, as a little-endian 64-bit physical
// address. Zero means "not provided, go and look for it yourself". // address. Zero means "not provided, go and look for it yourself".
@ -164,6 +166,17 @@
// BGRX in memory, so format 0 is the one that needs its channels swapped. // BGRX in memory, so format 0 is the one that needs its channels swapped.
#define SCHBOOT_FB_FORMAT (SCHBOOT_BASE + 0x634) /* uint32 */ #define SCHBOOT_FB_FORMAT (SCHBOOT_BASE + 0x634) /* uint32 */
// The SMBIOS entry point, from the UEFI configuration table: the 64-bit
// "_SM3_" one when the firmware has it, else the 32-bit "_SM_". Zero means
// none was handed over, and the kernel scans 0xF0000-0xFFFFF the BIOS way.
// It is what names the machine -- vendor, model, firmware version -- in
// /sys/class/dmi/id, and on UEFI the scan alone finds nothing on most laptops.
#define SCHBOOT_SMBIOS_ADDR (SCHBOOT_BASE + 0x638) /* uint64, physical */
// SCHBOOT_EFI_MAGIC when the UEFI loader booted this kernel, which is what
// /sys/firmware/efi existing means on Linux.
#define SCHBOOT_EFI_FLAG (SCHBOOT_BASE + 0x640) /* uint32 */
#define SCHBOOT_EFI_MAGIC 0x21494645u /* "EFI!" */
#ifndef __ASSEMBLER__ #ifndef __ASSEMBLER__
struct sch64_header { struct sch64_header {

View file

@ -121,6 +121,9 @@ int rt_sigaction(int, void*, void*, uint64);
// kernel's internal one. Returns -EINTR always, as sigsuspend must. // kernel's internal one. Returns -EINTR always, as sigsuspend must.
int rt_sigsuspend(const uint64 *mask); int rt_sigsuspend(const uint64 *mask);
int mount(const char *src, const char *tgt, const char *fstype, uint64 flags, void *data); int mount(const char *src, const char *tgt, const char *fstype, uint64 flags, void *data);
int umount2(const char *tgt, int flags);
int sync(void);
int fsync(int fd);
// Directory reading. The kernel has had getdents64 since the Linux ABI landed, // Directory reading. The kernel has had getdents64 since the Linux ABI landed,
// but nothing declared it here, so first-party programs had no way to list a // but nothing declared it here, so first-party programs had no way to list a

View file

@ -17,6 +17,7 @@
// another kernel; they are all in chapter 5 of the published standard. // another kernel; they are all in chapter 5 of the published standard.
#include "types.h" #include "types.h"
#include "hwinfo.h"
#include "defs.h" #include "defs.h"
#include "schboot.h" // SCHBOOT_RSDP_ADDR: the loader/kernel hand-off #include "schboot.h" // SCHBOOT_RSDP_ADDR: the loader/kernel hand-off
#include "param.h" #include "param.h"
@ -252,6 +253,87 @@ find_table(struct acpi_rsdp *rsdp, const char *sig)
return 0; return 0;
} }
// The n-th table, for /sys/firmware/acpi/tables: 0, 1 for a slot with nothing
// readable in it (go on to n + 1), or -1 past the end. The root's entries in order,
// then the DSDT and FACS: no root lists those two, only the FADT does, and
// they are the ones anybody debugging a machine wants first.
int
acpi_table_nth(int n, uint64 *pa, uint32 *len, char sig[4])
{
struct acpi_rsdp *rsdp;
struct acpi_sdt *root;
int wide;
if (rsdp_phys == 0)
return -1;
rsdp = (struct acpi_rsdp *)P2V(rsdp_phys);
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 -1;
uint32 cnt = (root->length - sizeof(struct acpi_sdt)) / (wide ? 8 : 4);
uint64 fadt = 0;
for (uint32 i = 0; i < cnt; i++) {
uint64 p;
if (wide) {
uint8 *b = (uint8 *)(root + 1) + i * 8;
p = 0;
for (int k = 7; k >= 0; k--)
p = (p << 8) | b[k];
} else {
p = ((uint32 *)(root + 1))[i];
}
if (p == 0 || p >= 0x100000000ULL) { /* outside the direct map */
if ((int)i == n)
return 1;
continue;
}
struct acpi_sdt *t = (struct acpi_sdt *)P2V(p);
if (memcmp(t->sig, "FACP", 4) == 0)
fadt = p;
if ((int)i == n) {
*pa = p;
*len = t->length;
memmove(sig, t->sig, 4);
return 0;
}
}
if (fadt == 0)
return -1;
// From the FADT: DSDT at 40 (32-bit) or X_DSDT at 140, FACS at 36 or
// X_FIRMWARE_CTRL at 132. The wide ones win when the table is long enough
// to have them and they are set.
struct acpi_sdt *f = (struct acpi_sdt *)P2V(fadt);
uint8 *fb = (uint8 *)f;
uint64 dsdt = *(uint32 *)(fb + 40), facs = *(uint32 *)(fb + 36);
if (f->length >= 148 && *(uint64 *)(fb + 140))
dsdt = *(uint64 *)(fb + 140);
if (f->length >= 140 && *(uint64 *)(fb + 132))
facs = *(uint64 *)(fb + 132);
uint64 extra[2] = { dsdt, facs };
for (int k = 0; k < 2; k++) {
if ((int)cnt + k != n)
continue;
uint64 p = extra[k];
if (p == 0 || p >= 0x100000000ULL)
return 1;
uint8 *t = (uint8 *)P2V(p);
memmove(sig, t, 4);
*pa = p;
*len = *(uint32 *)(t + 4); /* both keep the length at 4 */
return 0;
}
return -1;
}
// Returns 1 if the processors were enumerated, 0 to fall back to mpinit(). // Returns 1 if the processors were enumerated, 0 to fall back to mpinit().
// One processor from the MADT. // One processor from the MADT.
// //

View file

@ -17,6 +17,7 @@
#include "memlayout.h" #include "memlayout.h"
#include "mmu.h" #include "mmu.h"
#include "dma.h" // DMA_ARENA_BYTES: the top of PHYSTOP must be able to hold it #include "dma.h" // DMA_ARENA_BYTES: the top of PHYSTOP must be able to hold it
#include "hwinfo.h"
// Set before kinit1() and never again. Starts at the old constant so that a // 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 // crash between entry and e820_init() behaves the way it always did rather
@ -60,12 +61,43 @@ struct e820entry {
uint32 acpi_ext; uint32 acpi_ext;
} __attribute__((packed)); } __attribute__((packed));
// The map exactly as handed over, for /sys/firmware/memmap. The low page it
// came in is not the kernel's to keep.
static struct e820_rec map_copy[E820_MAXENTRIES];
static int nmap_copy;
int
e820_nth(int i, struct e820_rec *out)
{
if (i < 0 || i >= nmap_copy)
return -1;
*out = map_copy[i];
return 0;
}
uint64
ram_total_bytes(void)
{
uint64 t = 0;
for (int i = 0; i < nmem_usable; i++)
t += mem_usable[i].end - mem_usable[i].base;
return t;
}
void void
e820_init(void) e820_init(void)
{ {
uint32 count = *(uint32 *)P2V(SCHBOOT_E820_COUNT); uint32 count = *(uint32 *)P2V(SCHBOOT_E820_COUNT);
struct e820entry *e = (struct e820entry *)P2V(SCHBOOT_E820_MAP); struct e820entry *e = (struct e820entry *)P2V(SCHBOOT_E820_MAP);
if (count <= E820_MAXENTRIES)
for (uint32 i = 0; i < count; i++) {
map_copy[i].base = e[i].base;
map_copy[i].len = e[i].length;
map_copy[i].type = e[i].type;
nmap_copy = (int)i + 1;
}
// A count of zero means the firmware declined; anything absurd means we are // 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 // reading something that is not a memory map. Either way the constant is the
// only honest answer left. // only honest answer left.
@ -105,6 +137,20 @@ e820_init(void)
if (base >= end) if (base >= end)
continue; continue;
// Full: the smallest region gives way to a larger one. A map that
// fragments RAM into more than MEMREGION_MAX pieces loses its crumbs
// rather than whatever happens to be listed last, which is the RAM above
// 4 GB.
if (nmem_usable == MEMREGION_MAX) {
int s = 0;
for (int k = 1; k < nmem_usable; k++)
if (mem_usable[k].end - mem_usable[k].base <
mem_usable[s].end - mem_usable[s].base)
s = k;
if (mem_usable[s].end - mem_usable[s].base >= end - base)
continue;
mem_usable[s] = mem_usable[--nmem_usable];
}
mem_usable[nmem_usable].base = base; mem_usable[nmem_usable].base = base;
mem_usable[nmem_usable].end = end; mem_usable[nmem_usable].end = end;
nmem_usable++; nmem_usable++;

View file

@ -13,6 +13,7 @@
#include "usb.h" #include "usb.h"
#include "acpi.h" #include "acpi.h"
#include "vmap.h" #include "vmap.h"
#include "hwinfo.h"
static void *klog_base; // the persistent log, just above the kernel image static void *klog_base; // the persistent log, just above the kernel image
@ -306,6 +307,8 @@ kmain(uint32 fb_addr)
// The firmware's AML, loaded before the bus is: pci_init() asks it where // The firmware's AML, loaded before the bus is: pci_init() asks it where
// each device's interrupt really goes. See kernel/acpi/aml.c. // each device's interrupt really goes. See kernel/acpi/aml.c.
acpi_aml_init(); acpi_aml_init();
// The machine's name, for the log: Linux's "DMI:" line. See kernel/dev/dmi.c.
dmi_init();
pci_init(); pci_init();
if (boot_verbose) cprintf("verbose boot: pci_init done\n"); if (boot_verbose) cprintf("verbose boot: pci_init done\n");

285
kernel/dev/dmi.c Normal file
View file

@ -0,0 +1,285 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// SMBIOS: what the firmware says this machine is.
//
// Vendor, model, board and firmware version live nowhere else. ACPI describes
// the hardware's wiring and PCI its parts, but "Lenovo 20XW / ThinkPad X1
// Carbon Gen 9, BIOS N32ET75W" is only in this table, and it is the first line
// anybody asks for about a machine that will not boot. Linux prints it as the
// "DMI:" line of its log and publishes the fields in /sys/class/dmi/id; this
// does both, under the same names.
//
// Written from the DMTF's SMBIOS specification (DSP0134). The entry point is
// handed over by the UEFI loader from the configuration table, or found by the
// BIOS-era scan of 0xF0000-0xFFFFF when it was not.
#include "types.h"
#include "defs.h"
#include "memlayout.h"
#include "schboot.h"
#include "hwinfo.h"
static int tried;
static uint64 ep_pa; static uint32 ep_len;
static uint64 tab_pa; static uint32 tab_len;
static const uint8 *tab; // mapped, or 0
const char *const dmi_field_names[DMI_NFIELDS] = {
[DMI_BIOS_VENDOR] = "bios_vendor",
[DMI_BIOS_VERSION] = "bios_version",
[DMI_BIOS_DATE] = "bios_date",
[DMI_BIOS_RELEASE] = "bios_release",
[DMI_SYS_VENDOR] = "sys_vendor",
[DMI_PRODUCT_NAME] = "product_name",
[DMI_PRODUCT_VERSION] = "product_version",
[DMI_PRODUCT_SKU] = "product_sku",
[DMI_PRODUCT_FAMILY] = "product_family",
[DMI_BOARD_VENDOR] = "board_vendor",
[DMI_BOARD_NAME] = "board_name",
[DMI_BOARD_VERSION] = "board_version",
[DMI_CHASSIS_VENDOR] = "chassis_vendor",
[DMI_CHASSIS_TYPE] = "chassis_type",
[DMI_CHASSIS_VERSION] = "chassis_version",
};
void *
phys_view(uint64 pa, uint64 len)
{
if (pa + len <= 4ULL * 1024 * 1024 * 1024)
return P2V(pa);
return ioremap(pa, len);
}
int
boot_is_efi(void)
{
return *(uint32 *)P2V(SCHBOOT_EFI_FLAG) == SCHBOOT_EFI_MAGIC;
}
static int
sum_ok(const uint8 *p, uint32 n)
{
uint8 s = 0;
while (n--)
s += *p++;
return s == 0;
}
// One entry point at `pa`, either kind. Sets the table on success.
static int
try_entry(uint64 pa)
{
const uint8 *e = phys_view(pa, 32);
if (e == 0)
return 0;
if (memcmp(e, "_SM3_", 5) == 0) {
uint32 n = e[6];
if (n < 24 || n > 32 || !sum_ok(e, n))
return 0;
ep_len = n;
tab_len = *(uint32 *)(e + 0x0C); // the most it can be
tab_pa = *(uint64 *)(e + 0x10);
} else if (memcmp(e, "_SM_", 4) == 0) {
uint32 n = e[5];
if (n < 0x1F || n > 32 || !sum_ok(e, n) || memcmp(e + 0x10, "_DMI_", 5))
return 0;
ep_len = n;
tab_len = *(uint16 *)(e + 0x16);
tab_pa = *(uint32 *)(e + 0x18);
} else {
return 0;
}
// A table that large is not one; a firmware bug, and walking it would be
// walking whatever memory follows.
if (tab_pa == 0 || tab_len == 0 || tab_len > 1024 * 1024)
return 0;
ep_pa = pa;
return 1;
}
static void
find(void)
{
if (tried)
return;
tried = 1;
uint64 handed = *(uint64 *)P2V(SCHBOOT_SMBIOS_ADDR);
int ok = boot_is_efi() && handed && try_entry(handed);
// The BIOS convention: 16-byte aligned somewhere in the last 64 KB below
// 1 MB. The 64-bit kind first, as Linux prefers it.
for (int pass = 0; !ok && pass < 2; pass++)
for (uint64 a = 0xF0000; !ok && a < 0x100000; a += 16) {
const char *p = (const char *)P2V(a);
if (memcmp(p, pass == 0 ? "_SM3_" : "_SM_", pass == 0 ? 5 : 4) == 0)
ok = try_entry(a);
}
if (!ok) {
ep_pa = tab_pa = 0;
return;
}
tab = phys_view(tab_pa, tab_len);
}
int
dmi_entry(uint64 *pa, uint32 *len)
{
find();
if (ep_pa == 0)
return -1;
*pa = ep_pa;
*len = ep_len;
return 0;
}
// The real length: up to and including the end-of-table structure, which for
// SMBIOS 3 is less than the maximum the entry point gave.
int
dmi_table(uint64 *pa, uint32 *len)
{
find();
if (tab == 0)
return -1;
uint32 off = 0;
while (off + 4 <= tab_len) {
uint8 type = tab[off], flen = tab[off + 1];
if (flen < 4)
break;
uint32 next = off + flen;
while (next + 1 < tab_len && (tab[next] || tab[next + 1]))
next++;
next += 2;
off = next > tab_len ? tab_len : next;
if (type == 127)
break;
}
*pa = tab_pa;
*len = off;
return 0;
}
// The first structure of `type`, and its length. Strings follow it.
static const uint8 *
find_struct(int type, uint8 *flen)
{
uint32 off = 0;
while (tab && off + 4 <= tab_len) {
const uint8 *s = tab + off;
if (s[1] < 4)
return 0;
if (s[0] == type) {
*flen = s[1];
return s;
}
if (s[0] == 127)
return 0;
uint32 next = off + s[1];
while (next + 1 < tab_len && (tab[next] || tab[next + 1]))
next++;
off = next + 2;
}
return 0;
}
// String number `idx` of a structure, trimmed as Linux trims it.
static int
get_str(const uint8 *s, uint8 flen, int field, char *out, int cap)
{
if (field >= flen)
return -1;
int idx = s[field];
if (idx == 0)
return -1;
const char *p = (const char *)s + flen;
const char *end = (const char *)tab + tab_len;
while (--idx > 0 && p < end) {
while (p < end && *p)
p++;
p++;
if (p < end && *p == 0) // the double NUL: there is no such string
return -1;
}
int n = 0;
while (p < end && *p == ' ')
p++;
while (p < end && *p && n < cap - 1)
out[n++] = *p++;
while (n > 0 && out[n - 1] == ' ')
n--;
out[n] = 0;
return n ? n : -1;
}
static int
put_u(char *out, int cap, uint v)
{
char t[12];
int k = 0, n = 0;
do { t[k++] = '0' + v % 10; v /= 10; } while (v);
while (k && n < cap - 1)
out[n++] = t[--k];
out[n] = 0;
return n;
}
int
dmi_field(int f, char *out, int cap)
{
static const struct { uint8 type, off; } at[DMI_NFIELDS] = {
[DMI_BIOS_VENDOR] = { 0, 4 }, [DMI_BIOS_VERSION] = { 0, 5 },
[DMI_BIOS_DATE] = { 0, 8 },
[DMI_SYS_VENDOR] = { 1, 4 }, [DMI_PRODUCT_NAME] = { 1, 5 },
[DMI_PRODUCT_VERSION] = { 1, 6 }, [DMI_PRODUCT_SKU] = { 1, 0x19 },
[DMI_PRODUCT_FAMILY] = { 1, 0x1A },
[DMI_BOARD_VENDOR] = { 2, 4 }, [DMI_BOARD_NAME] = { 2, 5 },
[DMI_BOARD_VERSION] = { 2, 6 },
[DMI_CHASSIS_VENDOR] = { 3, 4 }, [DMI_CHASSIS_VERSION] = { 3, 6 },
};
uint8 flen;
const uint8 *s;
if (f < 0 || f >= DMI_NFIELDS || cap < 2)
return -1;
find();
if (tab == 0)
return -1;
if (f == DMI_BIOS_RELEASE) {
if ((s = find_struct(0, &flen)) == 0 || flen < 0x16 ||
s[0x14] == 0xFF || s[0x15] == 0xFF)
return -1;
int n = put_u(out, cap, s[0x14]);
if (n < cap - 1) out[n++] = '.';
return n + put_u(out + n, cap - n, s[0x15]);
}
if (f == DMI_CHASSIS_TYPE) {
if ((s = find_struct(3, &flen)) == 0 || flen < 6)
return -1;
return put_u(out, cap, s[5] & 0x7F);
}
if ((s = find_struct(at[f].type, &flen)) == 0)
return -1;
return get_str(s, flen, at[f].off, out, cap);
}
// Linux's boot line, word for word in shape, because it is the line people
// search for: "DMI: LENOVO 20XW/20XW, BIOS N32ET75W (1.51 ) 09/05/2023".
void
dmi_init(void)
{
char v[64], p[64], b[64], bv[64], bd[32];
find();
if (tab == 0) {
cprintf("DMI: not present or invalid.\n");
return;
}
if (dmi_field(DMI_SYS_VENDOR, v, sizeof v) < 0) v[0] = 0;
if (dmi_field(DMI_PRODUCT_NAME, p, sizeof p) < 0) p[0] = 0;
if (dmi_field(DMI_BOARD_NAME, b, sizeof b) < 0) b[0] = 0;
if (dmi_field(DMI_BIOS_VERSION, bv, sizeof bv) < 0) bv[0] = 0;
if (dmi_field(DMI_BIOS_DATE, bd, sizeof bd) < 0) bd[0] = 0;
cprintf("DMI: %s %s/%s, BIOS %s %s\n", v, p, b, bv, bd);
}

View file

@ -25,6 +25,7 @@
#include "netdev.h" #include "netdev.h"
#include "param.h" #include "param.h"
#include "memlayout.h" #include "memlayout.h"
#include "hwinfo.h"
#include "mmu.h" #include "mmu.h"
#include "spinlock.h" #include "spinlock.h"
#include "sleeplock.h" #include "sleeplock.h"
@ -49,15 +50,12 @@
// --- the files ------------------------------------------------------------- // --- the files -------------------------------------------------------------
// PHYSTOP is the top of usable physical memory; kalloc hands out everything
// above the kernel image. Reporting the total honestly and leaving "free" as a
// share of it is better than the invented constants sysinfo() used to return.
#define TOTAL_KB (PHYSTOP / 1024)
static void static void
gen_meminfo(struct sbuf *sb) gen_meminfo(struct sbuf *sb)
{ {
uint64 total = TOTAL_KB; // Usable RAM, not PHYSTOP: that is an address and counts the PCI hole, so a
// 6 GB machine reported 8.
uint64 total = ram_total_bytes() / 1024;
uint64 freek = kalloc_free_bytes() / 1024; uint64 freek = kalloc_free_bytes() / 1024;
sb_kv(sb, "MemTotal: ", total, "kB"); sb_kv(sb, "MemTotal: ", total, "kB");
sb_kv(sb, "MemFree: ", freek, "kB"); sb_kv(sb, "MemFree: ", freek, "kB");

View file

@ -15,6 +15,7 @@
#include "spinlock.h" #include "spinlock.h"
#include "dma.h" #include "dma.h"
#include "usb.h" #include "usb.h"
#include "hwinfo.h"
// A staging buffer for control transfers. // A staging buffer for control transfers.
// //
@ -167,6 +168,108 @@ usb_offer(const struct usb_driver *drv, struct usb_dev *dev)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// What is plugged in, for /sys/bus/usb/devices.
//
// The controller owns each usb_dev; this only remembers which ones are
// attached, from usb_attach until usb_detach, and copies their identity out
// under the lock so a reader never follows a pointer the controller may be
// about to reuse. A bus number is handed to each controller the first time one
// of its devices appears, in that order, as Linux numbers usb1, usb2.
#define NPRESENT 64
#define NBUSES 8
static struct spinlock present_lock;
static int present_init;
static struct usb_dev *present[NPRESENT];
static void *buses[NBUSES];
static void
present_add(struct usb_dev *dev)
{
if(!present_init){
initlock(&present_lock, "usbpresent");
present_init = 1;
}
acquire(&present_lock);
for(int i = 0; i < NPRESENT; i++)
if(present[i] == 0){ present[i] = dev; break; }
for(int b = 0; b < NBUSES; b++){
if(buses[b] == dev->hc) break;
if(buses[b] == 0){ buses[b] = dev->hc; break; }
}
release(&present_lock);
}
static void
present_del(struct usb_dev *dev)
{
if(!present_init)
return;
acquire(&present_lock);
for(int i = 0; i < NPRESENT; i++)
if(present[i] == dev) present[i] = 0;
release(&present_lock);
}
static void
put_num(char *out, int *n, int cap, int v)
{
char t[12];
int k = 0;
do { t[k++] = '0' + v % 10; v /= 10; } while(v);
while(k && *n < cap - 1) out[(*n)++] = t[--k];
out[*n] = 0;
}
int
usb_info_nth(int idx, struct usb_info *o)
{
if(!present_init)
return -1;
acquire(&present_lock);
int seen = 0;
for(int i = 0; i < NPRESENT; i++){
struct usb_dev *d = present[i];
if(d == 0 || seen++ != idx)
continue;
memset(o, 0, sizeof *o);
o->bus = 1;
for(int b = 0; b < NBUSES; b++)
if(buses[b] == d->hc){ o->bus = b + 1; break; }
o->devnum = d->slot;
o->speed = d->speed;
o->vid = d->desc.idVendor;
o->pid = d->desc.idProduct;
o->bcd = d->desc.bcdDevice;
o->cls = d->desc.bDeviceClass;
o->subcls = d->desc.bDeviceSubClass;
o->proto = d->desc.bDeviceProtocol;
o->nconf = d->desc.bNumConfigurations;
// "<bus>-<root port>[.<hub port>...]": the route string holds one
// downstream port per tier below the root, a nibble each.
int n = 0, cap = (int)sizeof o->path;
put_num(o->path, &n, cap, o->bus);
o->path[n++] = '-';
put_num(o->path, &n, cap, d->port);
for(int t = 0; t < d->tier && n < cap - 3; t++){
o->path[n++] = '.';
put_num(o->path, &n, cap, (int)((d->route >> (4 * t)) & 0xF));
}
const char *drv = d->driver;
for(int k = 0; !drv && k < NIFDEV; k++)
if(ifdevs[k].in_use && ifdevs[k].base == d)
drv = ifdevs[k].driver;
if(drv)
safestrcpy(o->driver, drv, sizeof o->driver);
release(&present_lock);
return 0;
}
release(&present_lock);
return -1;
}
void void
usb_attach(struct usb_dev *dev) usb_attach(struct usb_dev *dev)
@ -177,6 +280,7 @@ usb_attach(struct usb_dev *dev)
if(buf == 0) if(buf == 0)
return; return;
present_add(dev);
// Nine bytes first, because wTotalLength is in them and there is no other // Nine bytes first, because wTotalLength is in them and there is no other
// way to know how much to ask for. // way to know how much to ask for.
@ -260,6 +364,7 @@ usb_attach(struct usb_dev *dev)
void void
usb_detach(struct usb_dev *dev) usb_detach(struct usb_dev *dev)
{ {
present_del(dev);
for(int i = 0; i < NIFDEV; i++){ for(int i = 0; i < NIFDEV; i++){
struct usb_dev *d = &ifdevs[i]; struct usb_dev *d = &ifdevs[i];
if(!d->in_use || d->base != dev) if(!d->in_use || d->base != dev)

View file

@ -283,6 +283,7 @@ extern const struct fs_ops fat_ops;
// Defined in fs/procfs.c -- the synthetic /proc. // Defined in fs/procfs.c -- the synthetic /proc.
extern const struct fs_ops procfs_ops; extern const struct fs_ops procfs_ops;
extern const struct fs_ops devfs_ops; extern const struct fs_ops devfs_ops;
extern const struct fs_ops sysfs_ops;
// Is this filesystem read-only? // Is this filesystem read-only?
// //
@ -305,6 +306,7 @@ ops_for(uint dev)
if (SCHFS_IS_ANY_DEV(dev)) return &schfs_ops; if (SCHFS_IS_ANY_DEV(dev)) return &schfs_ops;
if (dev == PROCFS_DEV) return &procfs_ops; if (dev == PROCFS_DEV) return &procfs_ops;
if (dev == DEVFS_DEV) return &devfs_ops; if (dev == DEVFS_DEV) return &devfs_ops;
if (dev == SYSFS_DEV) return &sysfs_ops;
if (FAT_IS_DEV(dev)) return &fat_ops; if (FAT_IS_DEV(dev)) return &fat_ops;
return &v1_ops; return &v1_ops;
} }

712
kernel/fs/sysfs.c Normal file
View file

@ -0,0 +1,712 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// /sys, laid out the way Linux lays it out, for the parts that exist here.
//
// /sys/bus/pci/devices/0000:00:1f.2/ vendor device class revision irq
// subsystem_vendor subsystem_device
// resource config uevent
// /sys/bus/usb/devices/1-3.2/ idVendor idProduct bcdDevice busnum
// devnum speed bDevice* uevent
// /sys/firmware/acpi/tables/DSDT the tables, byte for byte
// /sys/firmware/dmi/tables/DMI the SMBIOS table and entry point
// /sys/firmware/memmap/N/ start end type
// /sys/firmware/efi/ present when UEFI booted the kernel
// /sys/class/dmi/id/product_name ... the machine, as text
//
// Why Linux's layout rather than one of its own: everything that reads
// hardware information already reads it from these paths. toybox's lspci and
// lsusb walk /sys/bus/*/devices and parse each uevent; acpidump-style tools,
// fastfetch and every "what laptop is this" script read the others. Getting
// the names and formats right makes all of them work at once, and makes a
// report taken here read the same as one taken on Linux on the same machine.
//
// Like /proc, nothing is stored. A directory is listed and a file generated
// from the live tables when asked, so a USB stick appears in /sys the moment
// the hot-plug thread attaches it.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "kmem.h"
#include "uaccess.h"
#include "errno.h"
#include "stat.h"
#include "pci.h"
#include "usb.h"
#include "hwinfo.h"
// ---------------------------------------------------------------------------
// Inode numbers: kind, index, file -- the whole identity, since iget gives
// nothing else back.
// ---------------------------------------------------------------------------
#define K_DIR 1ULL // a fixed directory; idx is a dirs[] index
#define K_PCI 2ULL // idx a pci_get() index
#define K_USB 3ULL // idx bus << 8 | devnum
#define K_ACPI 4ULL // idx a slot of acpi_table_nth
#define K_MEMMAP 5ULL // idx an entry of e820_nth
#define K_DMIID 6ULL // file a DMI_* field + 1
#define K_DMITAB 7ULL // file 1 the entry point, 2 the table
#define INO(k, idx, f) (((uint64)(k) << 40) | ((uint64)(idx) << 8) | (uint64)(f))
#define I_KIND(i) ((int)((i) >> 40))
#define I_IDX(i) ((int)(((i) >> 8) & 0xFFFFFFFF))
#define I_FILE(i) ((int)((i) & 0xFF))
enum {
D_ROOT, D_BUS, D_PCI, D_PCIDEV, D_USB, D_USBDEV, D_FW, D_ACPI, D_ACPITAB,
D_DMI, D_DMITAB, D_MEMMAP, D_EFI, D_CLASS, D_CLASSDMI, D_DMIID, D_NDIRS
};
static const struct { const char *name; int parent; } dirs[D_NDIRS] = {
[D_ROOT] = { "", D_ROOT },
[D_BUS] = { "bus", D_ROOT },
[D_PCI] = { "pci", D_BUS },
[D_PCIDEV] = { "devices", D_PCI },
[D_USB] = { "usb", D_BUS },
[D_USBDEV] = { "devices", D_USB },
[D_FW] = { "firmware",D_ROOT },
[D_ACPI] = { "acpi", D_FW },
[D_ACPITAB] = { "tables", D_ACPI },
[D_DMI] = { "dmi", D_FW },
[D_DMITAB] = { "tables", D_DMI },
[D_MEMMAP] = { "memmap", D_FW },
[D_EFI] = { "efi", D_FW },
[D_CLASS] = { "class", D_ROOT },
[D_CLASSDMI] = { "dmi", D_CLASS },
[D_DMIID] = { "id", D_CLASSDMI },
};
#define SYSFS_ROOT INO(K_DIR, D_ROOT, 0)
static const char *const pcifiles[] = {
0, "vendor", "device", "class", "revision", "subsystem_vendor",
"subsystem_device", "irq", "resource", "config", "uevent",
};
#define PCI_CONFIG 9
#define NPCIFILES ((int)(sizeof pcifiles / sizeof pcifiles[0]))
static const char *const usbfiles[] = {
0, "idVendor", "idProduct", "bcdDevice", "busnum", "devnum", "speed",
"bDeviceClass", "bDeviceSubClass", "bDeviceProtocol", "bNumConfigurations",
"uevent",
};
#define NUSBFILES ((int)(sizeof usbfiles / sizeof usbfiles[0]))
static const char *const memfiles[] = { 0, "start", "end", "type" };
// ---------------------------------------------------------------------------
// Text
// ---------------------------------------------------------------------------
struct out { char *p; int n, cap; };
static void putc_(struct out *o, char c) { if (o->n < o->cap) o->p[o->n++] = c; }
static void puts_(struct out *o, const char *s) { while (*s) putc_(o, *s++); }
static void
putu(struct out *o, uint64 v)
{
char t[24];
int k = 0;
do { t[k++] = '0' + v % 10; v /= 10; } while (v);
while (k) putc_(o, t[--k]);
}
// `digits` 0 means as few as it takes. Lower case unless `upper`.
static void
putx(struct out *o, uint64 v, int digits, int upper)
{
const char *h = upper ? "0123456789ABCDEF" : "0123456789abcdef";
char t[16];
int k = 0;
do { t[k++] = h[v & 0xF]; v >>= 4; } while (v || k < digits);
while (k) putc_(o, t[--k]);
}
static void
pci_name(struct out *o, const struct pci_dev *d)
{
puts_(o, "0000:");
putx(o, d->bus, 2, 0); putc_(o, ':');
putx(o, d->slot, 2, 0); putc_(o, '.');
putx(o, d->func, 1, 0);
}
static void
memmap_type(struct out *o, uint32 t)
{
// /sys/firmware/memmap's strings, which are e820's as Linux prints them.
switch (t) {
case 1: puts_(o, "System RAM"); break;
case 3: puts_(o, "ACPI Tables"); break;
case 4: puts_(o, "ACPI Non-volatile Storage"); break;
case 5: puts_(o, "Unusable memory"); break;
case 7: puts_(o, "Persistent Memory"); break;
default: puts_(o, "Reserved"); break;
}
}
// ---------------------------------------------------------------------------
// ACPI names: a signature that occurs more than once is numbered from 1, all
// of its instances, as Linux numbers SSDT1, SSDT2.
// ---------------------------------------------------------------------------
static int
acpi_name(int slot, char *name)
{
uint64 pa; uint32 len; char sig[4], s2[4];
if (acpi_table_nth(slot, &pa, &len, sig) != 0)
return -1;
int rank = 0, total = 0;
for (int i = 0, r; (r = acpi_table_nth(i, &pa, &len, s2)) >= 0; i++) {
if (r != 0 || memcmp(sig, s2, 4) != 0)
continue;
if (i < slot)
rank++;
total++;
}
int n = 0;
for (int k = 0; k < 4; k++)
name[n++] = (sig[k] > ' ' && sig[k] < 127 && sig[k] != '/') ? sig[k] : '_';
if (total > 1) {
struct out o = { name + n, 0, 8 };
putu(&o, (uint64)rank + 1);
n += o.n;
}
name[n] = 0;
return 0;
}
// ---------------------------------------------------------------------------
// The namespace: every child of a directory, in order. `cb` returns nonzero
// to stop, which is how both lookup and getdents use the one walk.
// ---------------------------------------------------------------------------
typedef int (*child_fn)(void *ctx, uint64 ino, int isdir, const char *name);
static int
usb_by_idx(int idx, struct usb_info *u)
{
for (int i = 0; usb_info_nth(i, u) == 0; i++)
if (((u->bus << 8) | u->devnum) == idx)
return 0;
return -1;
}
static int
children(uint64 dino, child_fn cb, void *ctx)
{
char name[64];
int kind = I_KIND(dino), idx = I_IDX(dino);
if (kind == K_DIR) {
for (int d = 1; d < D_NDIRS; d++) {
if (dirs[d].parent != idx)
continue;
if (d == D_EFI && !boot_is_efi())
continue;
if (cb(ctx, INO(K_DIR, d, 0), 1, dirs[d].name))
return 1;
}
switch (idx) {
case D_PCIDEV:
for (int i = 0; i < pci_count(); i++) {
struct out o = { name, 0, sizeof name - 1 };
pci_name(&o, pci_get(i));
name[o.n] = 0;
if (cb(ctx, INO(K_PCI, i, 0), 1, name))
return 1;
}
break;
case D_USBDEV: {
struct usb_info u;
for (int i = 0; usb_info_nth(i, &u) == 0; i++)
if (cb(ctx, INO(K_USB, (u.bus << 8) | u.devnum, 0), 1, u.path))
return 1;
break;
}
case D_ACPITAB:
for (int i = 0; ; i++) {
uint64 pa; uint32 len; char sig[4];
int r = acpi_table_nth(i, &pa, &len, sig);
if (r < 0)
break;
if (r == 0 && acpi_name(i, name) == 0 &&
cb(ctx, INO(K_ACPI, i, 1), 0, name))
return 1;
}
break;
case D_DMITAB: {
uint64 pa; uint32 len;
if (dmi_entry(&pa, &len) == 0) {
if (cb(ctx, INO(K_DMITAB, 0, 1), 0, "smbios_entry_point")) return 1;
if (cb(ctx, INO(K_DMITAB, 0, 2), 0, "DMI")) return 1;
}
break;
}
case D_MEMMAP: {
struct e820_rec e;
for (int i = 0; e820_nth(i, &e) == 0; i++) {
struct out o = { name, 0, sizeof name - 1 };
putu(&o, (uint64)i);
name[o.n] = 0;
if (cb(ctx, INO(K_MEMMAP, i, 0), 1, name))
return 1;
}
break;
}
case D_DMIID:
for (int f = 0; f < DMI_NFIELDS; f++)
if (dmi_field(f, name, sizeof name) >= 0 &&
cb(ctx, INO(K_DMIID, 0, f + 1), 0, dmi_field_names[f]))
return 1;
break;
}
return 0;
}
if (kind == K_PCI && I_FILE(dino) == 0) {
for (int f = 1; f < NPCIFILES; f++)
if (cb(ctx, INO(K_PCI, idx, f), 0, pcifiles[f]))
return 1;
} else if (kind == K_USB && I_FILE(dino) == 0) {
for (int f = 1; f < NUSBFILES; f++)
if (cb(ctx, INO(K_USB, idx, f), 0, usbfiles[f]))
return 1;
} else if (kind == K_MEMMAP && I_FILE(dino) == 0) {
for (int f = 1; f < 4; f++)
if (cb(ctx, INO(K_MEMMAP, idx, f), 0, memfiles[f]))
return 1;
}
return 0;
}
static uint64
parent_of(uint64 dino)
{
switch (I_KIND(dino)) {
case K_DIR: return INO(K_DIR, dirs[I_IDX(dino)].parent, 0);
case K_PCI: return INO(K_DIR, D_PCIDEV, 0);
case K_USB: return INO(K_DIR, D_USBDEV, 0);
case K_MEMMAP: return INO(K_DIR, D_MEMMAP, 0);
}
return SYSFS_ROOT;
}
// ---------------------------------------------------------------------------
// Contents
// ---------------------------------------------------------------------------
// Bytes straight out of memory the firmware owns, or a PCI config space.
// Sets *src for a mapped region; fills `buf` for config space. Returns the
// length, or -1 when this is not a binary file.
static int
binary(uint64 ino, const uchar **src, uchar *buf)
{
uint64 pa; uint32 len; char sig[4];
switch (I_KIND(ino)) {
case K_ACPI:
if (acpi_table_nth(I_IDX(ino), &pa, &len, sig) != 0)
return -1;
break;
case K_DMITAB:
if ((I_FILE(ino) == 1 ? dmi_entry(&pa, &len) : dmi_table(&pa, &len)) != 0)
return -1;
break;
case K_PCI:
if (I_FILE(ino) != PCI_CONFIG || I_IDX(ino) >= pci_count())
return -1;
if (buf) {
struct pci_dev *d = pci_get(I_IDX(ino));
for (int off = 0; off < 256; off += 4) {
uint32 v = pci_read32(d->bus, d->slot, d->func, (uint8)off);
memmove(buf + off, &v, 4);
}
*src = buf;
}
return 256;
default:
return -1;
}
if (src) {
*src = phys_view(pa, len);
if (*src == 0)
return -1;
}
return (int)len;
}
static void
gen_pci(struct out *o, struct pci_dev *d, int f)
{
uint16 svid = 0, sdid = 0;
if ((d->header_type & 0x7F) == 0) {
uint32 ss = pci_read32(d->bus, d->slot, d->func, 0x2C);
svid = ss & 0xFFFF;
sdid = ss >> 16;
}
uint32 cls = ((uint32)d->class << 16) | ((uint32)d->subclass << 8) | d->prog_if;
switch (f) {
case 1: puts_(o, "0x"); putx(o, d->vendor, 4, 0); break;
case 2: puts_(o, "0x"); putx(o, d->device, 4, 0); break;
case 3: puts_(o, "0x"); putx(o, cls, 6, 0); break;
case 4: puts_(o, "0x"); putx(o, d->revision, 2, 0); break;
case 5: puts_(o, "0x"); putx(o, svid, 4, 0); break;
case 6: puts_(o, "0x"); putx(o, sdid, 4, 0); break;
case 7: putu(o, d->irq == 0xFF ? 0 : d->irq); break;
case 8:
// start, end, flags per BAR, then the ROM, as Linux prints them.
for (int b = 0; b < 7; b++) {
uint64 s = 0, e = 0, fl = 0;
if (b < 6 && d->bar[b].base) {
s = d->bar[b].base;
e = s + (d->bar[b].size ? d->bar[b].size - 1 : 0);
fl = d->bar[b].is_io ? 0x101 : 0x200;
if (d->bar[b].prefetch) fl |= 0x2000;
if (d->bar[b].is_64) fl |= 0x100000;
}
puts_(o, "0x"); putx(o, s, 16, 0);
puts_(o, " 0x"); putx(o, e, 16, 0);
puts_(o, " 0x"); putx(o, fl, 16, 0);
putc_(o, '\n');
}
return;
case 10:
if (d->driver) { puts_(o, "DRIVER="); puts_(o, d->driver); putc_(o, '\n'); }
puts_(o, "PCI_CLASS="); putx(o, cls, 4, 1);
puts_(o, "\nPCI_ID="); putx(o, d->vendor, 4, 1); putc_(o, ':');
putx(o, d->device, 4, 1);
puts_(o, "\nPCI_SUBSYS_ID="); putx(o, svid, 4, 1); putc_(o, ':');
putx(o, sdid, 4, 1);
puts_(o, "\nPCI_SLOT_NAME="); pci_name(o, d);
puts_(o, "\nMODALIAS=pci:v"); putx(o, d->vendor, 8, 1);
puts_(o, "d"); putx(o, d->device, 8, 1);
puts_(o, "sv"); putx(o, svid, 8, 1);
puts_(o, "sd"); putx(o, sdid, 8, 1);
puts_(o, "bc"); putx(o, d->class, 2, 1);
puts_(o, "sc"); putx(o, d->subclass, 2, 1);
puts_(o, "i"); putx(o, d->prog_if, 2, 1);
break;
}
putc_(o, '\n');
}
static void
gen_usb(struct out *o, const struct usb_info *u, int f)
{
static const char *const speeds[] = { "unknown", "12", "1.5", "480", "5000" };
switch (f) {
case 1: putx(o, u->vid, 4, 0); break;
case 2: putx(o, u->pid, 4, 0); break;
case 3: putx(o, u->bcd, 4, 0); break;
case 4: putu(o, (uint64)u->bus); break;
case 5: putu(o, (uint64)u->devnum); break;
case 6: puts_(o, u->speed >= 1 && u->speed <= 4 ? speeds[u->speed] : speeds[0]); break;
case 7: putx(o, u->cls, 2, 0); break;
case 8: putx(o, u->subcls, 2, 0); break;
case 9: putx(o, u->proto, 2, 0); break;
case 10: putu(o, u->nconf); break;
case 11:
puts_(o, "DEVTYPE=usb_device\n");
if (u->driver[0]) { puts_(o, "DRIVER="); puts_(o, u->driver); putc_(o, '\n'); }
puts_(o, "PRODUCT="); putx(o, u->vid, 0, 0); putc_(o, '/');
putx(o, u->pid, 0, 0); putc_(o, '/'); putx(o, u->bcd, 0, 0);
puts_(o, "\nTYPE="); putu(o, u->cls); putc_(o, '/');
putu(o, u->subcls); putc_(o, '/'); putu(o, u->proto);
puts_(o, "\nBUSNUM=");
if (u->bus < 100) putc_(o, '0');
if (u->bus < 10) putc_(o, '0');
putu(o, (uint64)u->bus);
puts_(o, "\nDEVNUM=");
if (u->devnum < 100) putc_(o, '0');
if (u->devnum < 10) putc_(o, '0');
putu(o, (uint64)u->devnum);
break;
}
putc_(o, '\n');
}
// A text file, whole. Returns its length, or -1 if there is no such file.
static int
text(uint64 ino, char *buf, int cap)
{
struct out o = { buf, 0, cap };
int idx = I_IDX(ino), f = I_FILE(ino);
switch (I_KIND(ino)) {
case K_PCI:
if (idx >= pci_count() || f < 1 || f >= NPCIFILES || f == PCI_CONFIG)
return -1;
gen_pci(&o, pci_get(idx), f);
return o.n;
case K_USB: {
struct usb_info u;
if (usb_by_idx(idx, &u) < 0 || f < 1 || f >= NUSBFILES)
return -1;
gen_usb(&o, &u, f);
return o.n;
}
case K_MEMMAP: {
struct e820_rec e;
if (e820_nth(idx, &e) < 0 || f < 1 || f > 3)
return -1;
if (f == 1) { puts_(&o, "0x"); putx(&o, e.base, 0, 0); }
if (f == 2) { puts_(&o, "0x"); putx(&o, e.base + e.len - 1, 0, 0); }
if (f == 3) memmap_type(&o, e.type);
putc_(&o, '\n');
return o.n;
}
case K_DMIID: {
int n = dmi_field(f - 1, buf, cap - 1);
if (n < 0)
return -1;
buf[n++] = '\n';
return n;
}
}
return -1;
}
// ---------------------------------------------------------------------------
// fs_ops
// ---------------------------------------------------------------------------
static int
exists_dir(uint64 ino)
{
switch (I_KIND(ino)) {
case K_DIR:
return I_IDX(ino) < D_NDIRS && (I_IDX(ino) != D_EFI || boot_is_efi());
case K_PCI:
return I_IDX(ino) < pci_count();
case K_USB: {
struct usb_info u;
return usb_by_idx(I_IDX(ino), &u) == 0;
}
case K_MEMMAP: {
struct e820_rec e;
return e820_nth(I_IDX(ino), &e) == 0;
}
}
return 0;
}
static void
sysfs_iload(struct inode *ip)
{
uint64 ino = ip->inum;
char *buf;
ip->valid = 1;
ip->major = ip->minor = 0;
ip->rwmode = 2; // read-only, as procfs marks it
ip->showmode = 1;
ip->atime = ip->mtime = ip->ctime = 0;
ip->size = 0;
ip->nlink = 1;
if (I_FILE(ino) == 0 && exists_dir(ino)) {
ip->type = T_DIR;
ip->nlink = 2;
return;
}
int n = binary(ino, 0, 0);
if (n >= 0) {
ip->type = T_FILE;
ip->size = (uint64)n; // real: tools copy tables by length
return;
}
// A text file reports 0, as procfs does and for the same reason: its size
// is whatever it says when it is generated. Generating it here only to
// check that it exists is cheap, and a vanished USB device says no.
if ((buf = kmalloc(512)) != 0) {
n = text(ino, buf, 512);
kmfree(buf);
if (n >= 0) {
ip->type = T_FILE;
return;
}
}
ip->type = 0;
ip->nlink = 0;
}
static void sysfs_istore(struct inode *ip) { (void)ip; }
static void sysfs_ifree(struct inode *ip) { (void)ip; }
static void sysfs_truncate(struct inode *ip, uint64 n) { (void)ip; (void)n; }
static int
deliver(int user_dst, uint64 dst, const void *src, uint n)
{
if (user_dst)
return copy_to_user(dst, (void *)src, n) < 0 ? -EFAULT : (int)n;
memmove((void *)dst, src, n);
return (int)n;
}
static int
sysfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
{
const uchar *src;
uchar *buf;
int len, r;
if (ip->type == T_DIR)
return -EISDIR;
if ((buf = (uchar *)kalloc()) == 0)
return -ENOMEM;
len = binary(ip->inum, &src, buf);
if (len < 0) {
len = text(ip->inum, (char *)buf, PGSIZE);
src = buf;
}
if (len < 0) {
kfree((char *)buf);
return -ENOENT;
}
if (off >= (uint64)len) {
kfree((char *)buf);
return 0;
}
if (off + n > (uint64)len)
n = (uint)((uint64)len - off);
r = deliver(user_dst, dst, src + off, n);
kfree((char *)buf);
return r;
}
static int
sysfs_write(struct inode *ip, int u, uint64 s, uint64 o, uint n)
{ (void)ip; (void)u; (void)s; (void)o; (void)n; return -EROFS; }
struct find { const char *name; uint64 ino; };
static int
find_cb(void *ctx, uint64 ino, int isdir, const char *name)
{
struct find *f = ctx;
(void)isdir;
if (namecmp((char *)name, (char *)f->name) != 0)
return 0;
f->ino = ino;
return 1;
}
static struct inode *
sysfs_dirlookup(struct inode *dp, char *name, uint *poff)
{
struct find f = { name, 0 };
if (poff)
*poff = 0;
if (namecmp(name, ".") == 0)
return iget(dp->dev, dp->inum);
if (namecmp(name, "..") == 0)
return iget(dp->dev, parent_of(dp->inum));
if (!children(dp->inum, find_cb, &f))
return 0;
return iget(dp->dev, f.ino);
}
static int sysfs_dirlink(struct inode *dp, char *name, uint64 inum)
{ (void)dp; (void)name; (void)inum; return -EROFS; }
static int sysfs_dirunlink(struct inode *dp, char *name, uint off)
{ (void)dp; (void)name; (void)off; return -EROFS; }
static int sysfs_dirempty(struct inode *dp) { (void)dp; return 0; }
static struct inode *sysfs_ialloc(uint dev, short type)
{ (void)dev; (void)type; return 0; }
struct linux_dirent64 {
uint64 d_ino;
int64 d_off;
ushort d_reclen;
uchar d_type;
char d_name[];
};
struct list {
char *out;
int cap, used, idx, full;
uint64 want, *off;
};
static int
emit_cb(void *ctx, uint64 ino, int isdir, const char *name)
{
struct list *l = ctx;
if (l->idx++ < (int)l->want)
return 0;
int nlen = (int)strlen(name);
int rec = ((int)sizeof(struct linux_dirent64) + nlen + 1 + 7) & ~7;
if (l->used + rec > l->cap) {
l->full = 1;
return 1;
}
struct linux_dirent64 *de = (struct linux_dirent64 *)(l->out + l->used);
de->d_ino = ino;
de->d_off = l->used + rec;
de->d_reclen = (ushort)rec;
de->d_type = isdir ? 4 : 8;
memmove(de->d_name, name, nlen);
memset(de->d_name + nlen, 0, rec - (int)sizeof(*de) - nlen);
l->used += rec;
(*l->off)++;
return 0;
}
// *off is an index into the listing, generated fresh each call -- the same
// contract, and the same race with a device vanishing, as /proc.
static int
sysfs_getdents(struct inode *dp, char *out, int cap, uint64 *off)
{
struct list l = { out, cap, 0, 0, 0, *off, off };
if (dp->type != T_DIR)
return -ENOTDIR;
if (emit_cb(&l, dp->inum, 1, ".") || emit_cb(&l, parent_of(dp->inum), 1, ".."))
return l.used;
children(dp->inum, emit_cb, &l);
return l.used;
}
const struct fs_ops sysfs_ops = {
.name = "sysfs",
.iload = sysfs_iload,
.istore = sysfs_istore,
.ifree = sysfs_ifree,
.read = sysfs_read,
.write = sysfs_write,
.truncate = sysfs_truncate,
.dirlookup = sysfs_dirlookup,
.dirlink = sysfs_dirlink,
.dirunlink = sysfs_dirunlink,
.dirempty = sysfs_dirempty,
.ialloc = sysfs_ialloc,
.getdents = sysfs_getdents,
};
// `mount -t sysfs sysfs /sys`. Init does it.
int
sysfs_mount(const char *target)
{
struct inode *dp = namei((char *)target);
int r;
if (dp == 0)
return -ENOENT;
r = mount_add(dp->dev, dp->inum, SYSFS_DEV, SYSFS_ROOT, (char *)target,
"sysfs", "sysfs");
iput(dp);
return r < 0 ? -EBUSY : 0;
}

View file

@ -2174,6 +2174,10 @@ addr_t sys_mount(void)
return 0; return 0;
} }
// sysfs: the hardware, as Linux's /sys lays it out. kernel/fs/sysfs.c.
if (strncmp(fstype, "sysfs", 6) == 0)
return sysfs_mount(target);
// FAT, under the three names Linux knows it by. vfat is what everything // FAT, under the three names Linux knows it by. vfat is what everything
// means today; msdos is the same driver without long names on Linux, and // means today; msdos is the same driver without long names on Linux, and
// here simply the same driver. // here simply the same driver.

View file

@ -2,6 +2,7 @@
#include "defs.h" #include "defs.h"
#include "param.h" #include "param.h"
#include "memlayout.h" #include "memlayout.h"
#include "hwinfo.h"
#include "mmu.h" #include "mmu.h"
#include "proc.h" #include "proc.h"
#include "x86.h" #include "x86.h"
@ -505,10 +506,10 @@ addr_t sys_sysinfo(void)
uint64 upsec, upusec; uint64 upsec, upusec;
monotonic(&upsec, &upusec); monotonic(&upsec, &upusec);
si->uptime = (long)upsec; si->uptime = (long)upsec;
// Real values: PHYSTOP is the top of usable physical memory and the page // Real values: the RAM the page allocator was given, and how much of it is
// allocator knows exactly how much of it is still on the free list. These // still on the free list. These used to be the constants 256MB/128MB, and
// used to be the constants 256MB/128MB, invented and always wrong. // then PHYSTOP, an address, which counted the PCI hole as memory.
si->totalram = (uint64)PHYSTOP; si->totalram = ram_total_bytes();
si->freeram = kalloc_free_bytes(); si->freeram = kalloc_free_bytes();
si->mem_unit = 1; si->mem_unit = 1;
si->procs = (uint16)proc_num(); si->procs = (uint16)proc_num();

View file

@ -122,6 +122,9 @@ SYSCALL(getsockopt)
SYSCALL(sendmsg) SYSCALL(sendmsg)
SYSCALL(recvmsg) SYSCALL(recvmsg)
SYSCALL(mount) SYSCALL(mount)
SYSCALL(umount2)
SYSCALL(sync)
SYSCALL(fsync)
SYSCALL(getdents64) SYSCALL(getdents64)
// Attributes and the index over them (SchFS phase 5). setxattr/getxattr/ // Attributes and the index over them (SchFS phase 5). setxattr/getxattr/