debugger: the kernel can be asked a question
docs/plan/debugger.md, milestones D0, D1 and D2. What they replace is the
ninety-second loop -- add a cprintf, rebuild, boot, read the serial log --
which answers exactly the one question you thought of before the rebuild.
The rule the shape follows: **it observes, it does not control.** No
stepping, no software breakpoints, no stopping the world. Half the bugs
this kernel produces are timing -- an interrupt storm, a level-triggered
line nobody acknowledges, a driver that faults only on its second start --
and every one of them disappears when you halt the machine to look. And
the layering rule, which is the circularity rule the display already has:
a debugger that needs lwIP cannot debug lwIP.
D0 -- symbols. tools/ksyms.pl turns the first link pass into a table the
second pass carries. Two passes is enough because .ksym is placed after
.text and .rodata, so carrying the table cannot move anything recorded in
it; the build lists both passes' text symbols and refuses to link if they
ever disagree, because a symbol table that is subtly wrong gives confident
false answers. ksym.c reads it through weak symbols, so pass one -- which
has no table -- still links. A crash chain now reads panic+0x5f <-
proc_sysrq_cmd+0x99 <- ... <- syscall+0xf6.
D1 -- a log that outlives the kernel. Every character through consputc()
lands in a byte ring with a timestamp per line, readable as /proc/kmsg.
It sits in a region carved before the page allocator gets anything, with a
magic header, so a kernel finds the previous kernel's log still there --
including the whole PSOGM report. panic.c's own comment said "there is no
core dump and no log to go back to"; that is no longer true.
The obvious place for it is the top of RAM beside the DMA arena. That
was the first attempt and the magic came back as ASCII on the reboot,
because the top of usable memory is exactly where the firmware makes its
own allocations. It lives above the kernel image instead, which is a
stable address across boots of the same build.
D2 -- the introspection registry. A subsystem publishes its state with one
line at the bottom of the file that owns the data:
dbg_register("dma", dma_dump, "the DMA arena's free extents");
Seven views -- dma, mem, proc, vma, claims, pci, sock -- reachable three
ways: sysrq 'g' (always, no process and no filesystem), /dev/dbg (echo a
line, cat the answer), and schdbg. The protocol is lines of text because
the same bytes have to work where nothing can allocate or frame a record,
and because a protocol you cannot drive by hand is one you cannot debug.
struct sbuf moved out of kernel/dev/procfs.c into include/sbuf.h rather
than being written a second time.
Several views exist because of a specific bug. `vma` is the table that
decides whether exit() frees a page, and the DMA ownership bug that made
this worth building was a block that was never in it.
Also fixed, found by building with clang for the first time in a while:
struct iovec was defined in both posix.h and socket.h, and every file that
included both was a redefinition. GCC 15 defaults to C23, which permits an
identical struct redeclaration, so the gcc path never noticed -- while
clang defaults to gnu17 and rejected it, breaking the one toolchain
shell.nix exists to provide and the UEFI loader requires. Both are now
guarded.
D3 (`whose <addr>`, the cross-subsystem ownership query) and D4 (hardware
watchpoints on DR0-DR3) are not built and are described honestly as
missing in the architecture doc's "What it does not do".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
d1f50c4da3
commit
1dd4ba0186
35 changed files with 2296 additions and 83 deletions
45
Makefile
45
Makefile
|
|
@ -21,11 +21,12 @@ KERN_ARCH = $(addprefix $(BUILD)/kernel/arch/x86_64/, \
|
|||
|
||||
KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \
|
||||
main.o kalloc.o dma.o spinlock.o sleeplock.o string.o console.o \
|
||||
panic.o uaccess.o qr.o time.o)
|
||||
panic.o uaccess.o qr.o time.o ksym.o klog.o sbuf.o)
|
||||
|
||||
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
|
||||
bio.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o 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)
|
||||
|
||||
# Drivers for hardware that has to be *found* rather than assumed. Anything
|
||||
# discovered by walking a bus belongs here; the fixed-address legacy devices
|
||||
|
|
@ -107,7 +108,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS))
|
|||
# Third-party software does NOT go here — see ports/.
|
||||
# -----------------------------------------------------------------------
|
||||
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o)
|
||||
CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud
|
||||
CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt pcitest vgpud schdbg
|
||||
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
@ -246,6 +247,7 @@ ifdef USE_CLANG
|
|||
LD = ld.lld
|
||||
OBJCOPY = llvm-objcopy
|
||||
OBJDUMP = llvm-objdump
|
||||
NM = llvm-nm
|
||||
# ld.lld has no --no-warn-rwx-segments: it does not emit that warning, so
|
||||
# there is nothing to suppress and the flag is a hard error.
|
||||
LDFLAGS = -m elf_x86_64 -nostdlib
|
||||
|
|
@ -280,6 +282,7 @@ else
|
|||
LD = $(TOOLPREFIX)ld
|
||||
OBJCOPY = $(TOOLPREFIX)objcopy
|
||||
OBJDUMP = $(TOOLPREFIX)objdump
|
||||
NM = $(TOOLPREFIX)nm
|
||||
LDFLAGS = -m elf_x86_64 -nostdlib --no-warn-rwx-segments
|
||||
LD_LOWBASE =
|
||||
PORTCC = $(TOOLPREFIX)gcc
|
||||
|
|
@ -616,11 +619,39 @@ qemu-iso: schos.iso
|
|||
# -----------------------------------------------------------------------
|
||||
# Kernel binary
|
||||
# -----------------------------------------------------------------------
|
||||
$(BUILD)/kernel/kernel: $(KERNOBJS) $(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj $(RAMDISK_OBJ) kernel/kernel.ld
|
||||
@mkdir -p $(BUILD)/kernel
|
||||
$(LD) $(LDFLAGS) -T kernel/kernel.ld -o $@ \
|
||||
$(BUILD)/kernel/arch/x86_64/entry.o $(KERN_NOENTRY) \
|
||||
# The kernel is linked twice, and the second pass is the one that ships.
|
||||
#
|
||||
# Pass one exists to be read: tools/ksyms.pl turns its symbol table into a C
|
||||
# file, which pass two compiles in, so the kernel can name its own addresses
|
||||
# (docs/plan/debugger.md, D0). kernel.ld places .ksym after .text and .rodata
|
||||
# precisely so that carrying the table cannot move anything the table records
|
||||
# -- which is what makes two passes sufficient instead of the three Linux needs.
|
||||
#
|
||||
# That property is checked, not assumed. If the two passes ever disagree about
|
||||
# where a function is, the table is silently wrong for every address, and a
|
||||
# debugger that lies is worse than one that says nothing -- so the build stops.
|
||||
KERN_LINK = $(BUILD)/kernel/arch/x86_64/entry.o $(KERN_NOENTRY) \
|
||||
$(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj $(RAMDISK_OBJ)
|
||||
|
||||
$(BUILD)/kernel/kernel.pass1: $(KERNOBJS) $(BUILD)/kernel/initcode_obj $(BUILD)/kernel/entryother_obj $(RAMDISK_OBJ) kernel/kernel.ld
|
||||
@mkdir -p $(BUILD)/kernel
|
||||
$(LD) $(LDFLAGS) -T kernel/kernel.ld -o $@ $(KERN_LINK)
|
||||
|
||||
$(BUILD)/kernel/ksym.c: $(BUILD)/kernel/kernel.pass1 tools/ksyms.pl
|
||||
$(NM) -n $< | perl tools/ksyms.pl > $@
|
||||
|
||||
$(BUILD)/kernel/ksym.o: $(BUILD)/kernel/ksym.c
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BUILD)/kernel/kernel: $(BUILD)/kernel/kernel.pass1 $(BUILD)/kernel/ksym.o
|
||||
$(LD) $(LDFLAGS) -T kernel/kernel.ld -o $@ $(KERN_LINK) $(BUILD)/kernel/ksym.o
|
||||
@$(NM) -n $(BUILD)/kernel/kernel.pass1 | perl tools/ksyms.pl --list > $(BUILD)/kernel/ksym.pass1.txt
|
||||
@$(NM) -n $@ | perl tools/ksyms.pl --list > $(BUILD)/kernel/ksym.pass2.txt
|
||||
@cmp -s $(BUILD)/kernel/ksym.pass1.txt $(BUILD)/kernel/ksym.pass2.txt || \
|
||||
{ echo "ksyms: the two link passes disagree about where the code is;"; \
|
||||
echo "ksyms: the symbol table would name every address wrongly."; \
|
||||
diff -u $(BUILD)/kernel/ksym.pass1.txt $(BUILD)/kernel/ksym.pass2.txt | head -20; \
|
||||
exit 1; }
|
||||
$(OBJDUMP) -S $@ > $(BUILD)/kernel/kernel.asm
|
||||
|
||||
# The ramdisk root image, and the object wrapping it for the linker. Built from
|
||||
|
|
|
|||
139
cmd/schdbg/main.c
Normal file
139
cmd/schdbg/main.c
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// schdbg -- ask the kernel a question.
|
||||
//
|
||||
// docs/plan/debugger.md, milestone D2. The kernel half is /dev/dbg: write a
|
||||
// line, read the answer. This is the half that makes that pleasant, and it is
|
||||
// deliberately almost nothing -- because the protocol is lines of text, and a
|
||||
// protocol whose client has to be clever is a protocol you cannot use when the
|
||||
// client will not start.
|
||||
//
|
||||
// schdbg the views this kernel publishes
|
||||
// schdbg dma one of them
|
||||
// schdbg all all of them
|
||||
// schdbg -i a prompt, for when you are asking several
|
||||
//
|
||||
// Everything here works equally well as:
|
||||
//
|
||||
// echo dma > /dev/dbg && cat /dev/dbg
|
||||
//
|
||||
// which is the point, and is what you fall back to on a system where this
|
||||
// program is one of the things that is broken.
|
||||
|
||||
#include "types.h"
|
||||
#include "user.h"
|
||||
#include "fcntl.h"
|
||||
|
||||
static void
|
||||
puts_(const char *s)
|
||||
{
|
||||
int n = 0;
|
||||
while (s[n]) n++;
|
||||
write(1, (void *)s, n);
|
||||
}
|
||||
|
||||
// One question, one answer.
|
||||
//
|
||||
// The descriptor is opened fresh for each, because the answer is read from
|
||||
// offset zero and the kernel replaces it on every write. Reusing one open
|
||||
// descriptor across two questions would read the second answer from wherever
|
||||
// the first one left off.
|
||||
static int
|
||||
ask(const char *cmd)
|
||||
{
|
||||
char buf[512];
|
||||
int fd, n;
|
||||
|
||||
fd = open("/dev/dbg", O_RDWR);
|
||||
if (fd < 0) {
|
||||
puts_("schdbg: cannot open /dev/dbg\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (cmd) {
|
||||
int len = 0;
|
||||
while (cmd[len]) len++;
|
||||
if (write(fd, (void *)cmd, len) != len) {
|
||||
puts_("schdbg: the kernel would not take that command\n");
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
// Back to the start. Writing moved this descriptor's offset forward by
|
||||
// the length of the command, and the answer begins at zero -- so
|
||||
// without this the reply arrives with its first few characters missing,
|
||||
// which is subtle enough to look like a formatting bug in the view.
|
||||
lseek(fd, 0, 0); /* SEEK_SET */
|
||||
}
|
||||
|
||||
while ((n = read(fd, buf, sizeof(buf))) > 0)
|
||||
write(1, buf, n);
|
||||
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A line from the terminal, without the newline. Returns 0 at end of input.
|
||||
static int
|
||||
readline(char *buf, int cap)
|
||||
{
|
||||
int n = 0;
|
||||
char c;
|
||||
|
||||
for (;;) {
|
||||
int r = read(0, &c, 1);
|
||||
if (r <= 0)
|
||||
return n > 0;
|
||||
if (c == '\n') {
|
||||
buf[n] = 0;
|
||||
return 1;
|
||||
}
|
||||
if (n < cap - 1)
|
||||
buf[n++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
if (argc > 1 && argv[1][0] == '-' && argv[1][1] == 'i') {
|
||||
char line[128];
|
||||
|
||||
puts_("schdbg: ask the kernel something. 'help' lists the views, "
|
||||
"'quit' leaves.\n");
|
||||
for (;;) {
|
||||
puts_("dbg> ");
|
||||
if (!readline(line, sizeof(line)))
|
||||
break;
|
||||
if (line[0] == 0)
|
||||
continue;
|
||||
if (line[0] == 'q' && line[1] == 'u' && line[2] == 'i' &&
|
||||
line[3] == 't' && line[4] == 0)
|
||||
break;
|
||||
ask(line);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// No argument means "what can I ask", which is the question someone has
|
||||
// the first time and the one an empty response would not answer.
|
||||
if (argc < 2)
|
||||
return ask("help") < 0 ? 1 : 0;
|
||||
|
||||
// Everything after the program name, rejoined with single spaces. The
|
||||
// shell split it and the kernel wants a line; `schdbg vma 4` has to arrive
|
||||
// as "vma 4" or the argument is silently dropped and the view quietly
|
||||
// ignores the narrowing it was asked for.
|
||||
{
|
||||
char line[128];
|
||||
int n = 0;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (i > 1 && n < (int)sizeof(line) - 1)
|
||||
line[n++] = ' ';
|
||||
for (int j = 0; argv[i][j] && n < (int)sizeof(line) - 1; j++)
|
||||
line[n++] = argv[i][j];
|
||||
}
|
||||
line[n] = 0;
|
||||
return ask(line) < 0 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -93,6 +93,7 @@ Read in this order if you are new:
|
|||
| [terminals.md](terminals.md) | The six virtual terminals, and the chord that switches them. |
|
||||
| [networking.md](networking.md) | NIC drivers, lwIP, and both socket families. |
|
||||
| [init.md](init.md) | `schinit`, PID 1 as a role, and the job manifests. |
|
||||
| [debugging.md](debugging.md) | Symbols, a log that outlives the kernel, and asking the kernel a question. |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
237
docs/architecture/debugging.md
Normal file
237
docs/architecture/debugging.md
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
# Finding out what the kernel is doing
|
||||
|
||||
Every bug in this project used to be found the same way: add a `cprintf`,
|
||||
rebuild, boot, read the serial log, guess again. That loop is about ninety
|
||||
seconds long and it answers exactly one question per turn — the question you
|
||||
thought to ask *before* you rebuilt.
|
||||
|
||||
What ended it was a real bug. A DMA block was on two allocators' free lists at
|
||||
once, and finding that took several rounds of that loop, because the kernel
|
||||
could not be asked whether two allocators owned the same page. It could not be
|
||||
asked anything.
|
||||
|
||||
> **The kernel had no way to answer a question it was not already compiled to
|
||||
> answer.**
|
||||
|
||||
Three things close that, and none of them is a debugger in the stepping sense.
|
||||
|
||||
---
|
||||
|
||||
## The rule the shape follows
|
||||
|
||||
**It observes; it does not control.** No stepping, no software breakpoints, no
|
||||
stopping the world.
|
||||
|
||||
That is not a scope compromise. Half the bugs this kernel actually produces are
|
||||
*timing*: an interrupt storm at 77,000 a second, a level-triggered line nobody
|
||||
acknowledges, a driver that faults only on its second start, a supervisor that
|
||||
restarts a job fifteen times in forty-five seconds. **Every one of those
|
||||
disappears when you halt the machine to look at it.**
|
||||
|
||||
And the layering rule, which is the circularity rule the display already has,
|
||||
pointed somewhere else:
|
||||
|
||||
> A debugger that needs lwIP cannot debug lwIP. A debugger that needs a process
|
||||
> cannot debug the scheduler. A debugger that needs to allocate cannot debug the
|
||||
> allocator.
|
||||
|
||||
So the kernel's half is the minimum that works when nothing else does, and
|
||||
everything convenient is a process.
|
||||
|
||||
---
|
||||
|
||||
## Naming an address
|
||||
|
||||
`tools/ksyms.pl` turns the kernel's own symbol table into a table the kernel
|
||||
carries, and `ksym_lookup()` resolves an address through it. A crash chain used
|
||||
to be nine hex numbers:
|
||||
|
||||
```
|
||||
Call chain
|
||||
#0 ffff80000010a9d9 panic+0x5f
|
||||
#1 ffff80000011e07f proc_sysrq_cmd+0x99
|
||||
#2 ffff80000011e205 proc_write_sysrq+0x6e
|
||||
#3 ffff800000130159 procfs_write+0x59
|
||||
#4 ffff80000012e566 writei+0x45
|
||||
#5 ffff800000128a96 filewrite+0x2ec
|
||||
#6 ffff80000016c0fa sys_writev+0x12b
|
||||
#7 ffff80000016986e syscall+0xf6
|
||||
#8 ffff800000100d2e syscall_trapret
|
||||
```
|
||||
|
||||
### Two link passes, and why two is enough
|
||||
|
||||
The kernel is linked twice. Pass one exists to be read; pass two carries the
|
||||
table built from it.
|
||||
|
||||
The obvious objection is that adding forty kilobytes to the image moves
|
||||
everything the table describes — which is why Linux's `kallsyms` needs three
|
||||
passes and a convergence check. **`.ksym` is placed after `.text` and
|
||||
`.rodata`,** so it cannot move a single address the table records. Two passes,
|
||||
and the second is stable by construction.
|
||||
|
||||
Not by assumption, though: the build lists the text symbols of both passes and
|
||||
**fails** if they differ. A symbol table that is subtly wrong for every address
|
||||
is worse than none, because it produces confident, false answers.
|
||||
|
||||
`ksym.c` reads the table through **weak** symbols, so pass one — which has no
|
||||
table — still links, and a tree with the generator removed still boots and
|
||||
simply cannot name anything.
|
||||
|
||||
---
|
||||
|
||||
## A log that outlives the kernel
|
||||
|
||||
`cprintf` used to go to a serial line and a screen and then be gone. `panic.c`
|
||||
said so in its own header: *"there is no core dump and no log to go back to."*
|
||||
|
||||
There is now. Every character that passes through `consputc()` — which is every
|
||||
kernel message, including the whole PSOGM report — is also appended to a byte
|
||||
ring with a timestamp per line. It is readable as **`/proc/kmsg`**.
|
||||
|
||||
The ring lives in a region carved out before the page allocator is given
|
||||
anything, and it has a magic header, so **a kernel that reboots finds the
|
||||
previous kernel's log still there**:
|
||||
|
||||
```
|
||||
--- boot 1 ---
|
||||
[ 0.00] e820: 1018 MB usable in 7 regions, top 1016 MB (19 entries)
|
||||
...
|
||||
[ 7.18] PSOGM -- Purple Screen of Guru Meditation
|
||||
[ 7.18] Call chain
|
||||
[ 7.18] #0 ffff80000010a9d9 panic+0x5f
|
||||
...
|
||||
--- boot 2 ---
|
||||
[ 0.00] klog: 128 KB at ffff80000053e000, 2162 bytes carried over from the previous boot
|
||||
```
|
||||
|
||||
### Where it lives, and why not where you would put it
|
||||
|
||||
The obvious place is the top of RAM, beside the DMA arena. That was the first
|
||||
attempt, and on the reboot the magic came back as ASCII — because **the top of
|
||||
usable memory is exactly where the firmware puts its own allocations** before it
|
||||
hands the machine over.
|
||||
|
||||
It sits immediately above the kernel image instead. `end` is the same address on
|
||||
every boot of the same build, which is the property a region has to have to be
|
||||
found again, and the bytes just past the kernel are the ones the loader has
|
||||
already committed to leaving alone. No hole has to be punched in the allocator:
|
||||
`kinit1`'s lower bound is a parameter.
|
||||
|
||||
The magic and a header checksum mean a region something else has used is
|
||||
re-initialised rather than printed as if it were a log. **A reboot that does not
|
||||
preserve DRAM loses the log, and says so** rather than showing garbage.
|
||||
|
||||
Two properties of the writer are deliberate. It **takes no lock** — it runs
|
||||
inside `consputc()`, and a lock there would be a way for the logger to deadlock
|
||||
the thing it exists to report on. And it **buffers before it exists**: the region
|
||||
is carved after `e820_init()`, so the handful of lines before that go into a
|
||||
static array and are flushed in, because a log that is blank for the earliest
|
||||
part of the boot is blank for the part that is hardest to debug any other way.
|
||||
|
||||
---
|
||||
|
||||
## Asking a question
|
||||
|
||||
`/dev/dbg` is a conversation: write a line, read the answer.
|
||||
|
||||
```
|
||||
# schdbg dma
|
||||
arena 0xffff80003e8ed000-0xffff80003f8ed000
|
||||
16384 KB total, 12295 KB free in 3 extent(s), 4088 KB out
|
||||
|
||||
# base phys bytes
|
||||
0 0xffff80003e8f3000 0x000000003e8f3000 4091904
|
||||
1 0xffff80003ecea200 0x000000003ecea200 3584
|
||||
2 0xffff80003f0d3000 0x000000003f0d3000 8495104
|
||||
```
|
||||
|
||||
It is text, not a typed binary protocol, and the reason is that the same bytes
|
||||
have to work at every level — including the one where nothing can allocate,
|
||||
frame a record, or wait for a client to acknowledge anything. A protocol you
|
||||
cannot drive by hand is a protocol you cannot debug. Everything `schdbg` does is
|
||||
also:
|
||||
|
||||
```
|
||||
# echo dma > /dev/dbg && cat /dev/dbg
|
||||
```
|
||||
|
||||
### The registry is the point, not the views
|
||||
|
||||
A subsystem publishes its state with **one line at the bottom of the file that
|
||||
owns the data**:
|
||||
|
||||
```c
|
||||
dbg_register("dma", dma_dump, "the DMA arena's free extents");
|
||||
```
|
||||
|
||||
Nothing central is edited to add one. That is the difference between something
|
||||
that stays current and something that does not — `/proc/sysrq` is the version
|
||||
that does not, a `switch` in one file with a letter per action, and after two
|
||||
years it holds three self-tests and no state at all.
|
||||
|
||||
| View | |
|
||||
|---|---|
|
||||
| `dma` | the arena's free extents, in virtual and physical |
|
||||
| `mem` | the page allocator's free list, and whether it has a cycle in it |
|
||||
| `proc` | every process, with state, size and its mmap bump pointer |
|
||||
| `vma` | every mapping a process holds but does not own the pages of |
|
||||
| `claims` | devices, DMA blocks and interrupts held by userspace drivers |
|
||||
| `pci` | the bus, with each device's command register decoded |
|
||||
| `sock` | the socket table, with buffered bytes and descriptors in flight |
|
||||
|
||||
`all` prints every view; `help` lists them; a view may take an argument, so
|
||||
`vma 4` narrows to one process.
|
||||
|
||||
Several of those exist because of a specific bug. `vma` is the table that decides
|
||||
whether `exit()` frees a page or leaves it alone, and the bug that made this
|
||||
worth building was a DMA block that was never in it. `claims` shows the three
|
||||
things that go wrong with a userspace driver — a device held by a process that
|
||||
no longer exists, DMA handed out and never returned, an interrupt line left
|
||||
masked with nobody reading it. `pci` decodes the command register because
|
||||
"is bus mastering actually off?" is a question with a wrong answer that looks
|
||||
fine.
|
||||
|
||||
### Three ways in, and the one that always works
|
||||
|
||||
| | Works when |
|
||||
|---|---|
|
||||
| `sysrq g` | always — every view to the console, no process, no filesystem, no client program |
|
||||
| `/dev/dbg` | there is a scheduler and a filesystem |
|
||||
| `schdbg` | there is a shell |
|
||||
|
||||
Views run in ordinary process context and may take locks. **They must not be
|
||||
called from `panic()`.**
|
||||
|
||||
---
|
||||
|
||||
## What it does not do
|
||||
|
||||
- **It does not step, break, or halt.** If a bug genuinely needs a stepper, this
|
||||
is the wrong tool, and that is a deliberate trade rather than an omission.
|
||||
- **No cross-subsystem queries.** *"Who owns physical page `0x3eceb000`?"* — the
|
||||
question that would have ended the investigation that prompted all this — still
|
||||
cannot be asked. Each view answers for its own subsystem and nothing
|
||||
correlates them. That is `whose`, and it is not built.
|
||||
- **No watchpoints.** DR0–DR3 are untouched and `T_DEBUG` maps to `SIGTRAP`, so
|
||||
"something zeroed my page table, tell me who" has no answer.
|
||||
- **No network transport.** The plan puts a TCP bridge in a userspace daemon,
|
||||
deliberately outside the kernel; it does not exist, so the debugger is local
|
||||
or serial only.
|
||||
- **No memory writes**, and no way to poke anything. Reading is what finds bugs.
|
||||
- **`all` truncates at 16 KB** and says so. On a busy machine, ask for one view
|
||||
at a time.
|
||||
- **One conversation at a time.** The answer buffer is global, so two programs
|
||||
asking at once get one answer each in some order — never half of the other's,
|
||||
but not two independent sessions either.
|
||||
- **No userspace symbolization.** The kernel names kernel symbols. A fault in a
|
||||
process still reports a bare address, and resolving it is a host-side job.
|
||||
- **The stack walk stops where the frame pointer stops.** `getcallerpcs()` walks
|
||||
`rbp` chains, so a trap frame in the middle of a chain ends the walk.
|
||||
- **`sysrq g` costs you log history.** Its output goes through `cprintf`, so
|
||||
sixteen kilobytes of views push the same amount out of a 128 KB ring.
|
||||
- **No tracing.** No static tracepoints, no event ring, no counters. "What
|
||||
happened in the two hundred events before this" is not a question this can
|
||||
answer.
|
||||
|
||||
What remains is in [../plan/debugger.md](../plan/debugger.md).
|
||||
|
|
@ -96,6 +96,15 @@ The galling ones. The work exists, is tested, and nothing uses it.
|
|||
| **`foundations.md`** | Item 1 built. Items 2 (a shareable memory object), 3 (`AF_UNIX` in the filesystem namespace), 4 and 5 open. **Wayland stops at item 2.** |
|
||||
| **`hybrid.md`** | All four mechanisms built. H1 and H2 done — the driver can be killed and comes back, which is the milestone the plan calls the one that proves the whole idea. H3 (a modeset API) and H4 (USB) open. |
|
||||
| **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. |
|
||||
| **`debugger.md`** | D0, D1 and D2 built and described in [../architecture/debugging.md](../architecture/debugging.md). D3 (`whose <addr>`, the cross-subsystem ownership query) and D4 (hardware watchpoints) open, and the network transport with them. |
|
||||
|
||||
**The row the debugger spent.** Adding one requires closing one, and *A released
|
||||
PCI device* closed when H2 landed. It is worth saying plainly that the debugger
|
||||
is not a capability of the OS the way every other row here is: nothing in it
|
||||
ships to a user or appears in the acceptance test. The case for it is that
|
||||
**Gate 0 is a list of bugs to find in a kernel that could not be asked
|
||||
anything** — so it is the instrument the rest of this register is surveyed with,
|
||||
and it earns its slot by making every other row cheaper to close.
|
||||
|
||||
**Closed since this register was written.** *A released PCI device* — release is
|
||||
now symmetric with claim (bus mastering cleared before the DMA is freed, a
|
||||
|
|
|
|||
|
|
@ -39,6 +39,12 @@ And the one that says which parts of the above are actually being worked on:
|
|||
|---|---|
|
||||
| **[1.0.md](1.0.md)** | The finish line. Four gates, and everything else explicitly deferred. |
|
||||
|
||||
And one that is a tool rather than a capability:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **[debugger.md](debugger.md)** | A debugger for this kernel — one that observes rather than halts, and answers in this kernel's own vocabulary. **Mostly built**: symbols, a log that outlives the kernel, and `/dev/dbg` are done and described in [../architecture/debugging.md](../architecture/debugging.md). |
|
||||
|
||||
Two longer-horizon documents sit behind these: [schapi.md](schapi.md), the API
|
||||
layering and the compatibility promise, and [roadmap.md](roadmap.md), which holds
|
||||
everything still open that is not one of the three above.
|
||||
|
|
|
|||
239
docs/plan/debugger.md
Normal file
239
docs/plan/debugger.md
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
# A debugger for this kernel
|
||||
|
||||
> **D0, D1 and D2 are built.** Symbols, a log that outlives the kernel, and the
|
||||
> introspection registry behind `/dev/dbg` are described in
|
||||
> [../architecture/debugging.md](../architecture/debugging.md). What is left
|
||||
> here is D3, D4, and the network transport.
|
||||
|
||||
Every bug in this project so far has been found the same way: add a `cprintf`,
|
||||
rebuild, boot, read the serial log, guess again. That loop is about ninety
|
||||
seconds long and it answers exactly one question per turn — the question you
|
||||
thought to ask before you rebuilt.
|
||||
|
||||
The DMA ownership bug in `do_dma()` is what made the case. Finding it took
|
||||
several rounds of that loop, and the thing that eventually found it was not a
|
||||
print at all; it was reading `freevm()` and noticing what `PHYSTOP` meant for
|
||||
the arena. The kernel could not be *asked* whether two allocators owned the same
|
||||
page, because nothing in it can be asked anything.
|
||||
|
||||
> **The kernel has no way to answer a question it was not already compiled to
|
||||
> answer.** That is the gap.
|
||||
|
||||
---
|
||||
|
||||
## What there was, and what was missing
|
||||
|
||||
The state this plan was written against. D0 to D2 have since closed the first
|
||||
three rows; they are kept because they are the argument.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `cprintf` | goes to UART and console and is then gone. **There is no log.** `panic.c` says so itself: *"there is no core dump and no log to go back to."* |
|
||||
| PSOGM | collects a great deal at the moment of a panic — CPU, process, call chain, load address — and paints it. It is the only artefact of a crash, and it does not survive being looked away from. |
|
||||
| `getcallerpcs()` | walks frames and prints **raw addresses**. Turning one into a function name is a manual `objdump` session. |
|
||||
| `/proc/sysrq` | a hardcoded `switch` with one letter per action. `xhci_dump`, `usb_storage_dump`, `display_selftest` — the right instinct, in the wrong shape: they are self-tests, not state, and adding one means editing a central file. |
|
||||
| DR0–DR3 | **unused.** `T_DEBUG` maps to `SIGTRAP` and nothing ever arms a hardware watchpoint. |
|
||||
|
||||
---
|
||||
|
||||
## The two rules
|
||||
|
||||
### It observes; it does not control
|
||||
|
||||
No stepping. No software breakpoints. No stopping the world.
|
||||
|
||||
This is the decision everything else follows from, and it is not a scope
|
||||
compromise. Half the bugs this project actually hits are *timing*: an interrupt
|
||||
storm at 77,000 a second, a level-triggered line nobody acknowledges, a driver
|
||||
that faults only on the second start, a supervisor that restarts a job fifteen
|
||||
times in forty-five seconds. **Every one of those disappears when you stop the
|
||||
world to look at it.** A debugger that can only work by halting the machine
|
||||
cannot see the class of bug this kernel produces.
|
||||
|
||||
So it answers questions, and it watches. Both of those work on a machine that is
|
||||
still running.
|
||||
|
||||
### It must not depend on what it is debugging
|
||||
|
||||
The same circularity rule the display already has, pointed at a different
|
||||
subsystem:
|
||||
|
||||
> A debugger that needs lwIP cannot debug lwIP. A debugger that needs a process
|
||||
> cannot debug the scheduler. A debugger that needs to allocate cannot debug the
|
||||
> allocator.
|
||||
|
||||
That rule decides the layering below rather than taste deciding it, which is the
|
||||
only reason to write a rule down.
|
||||
|
||||
---
|
||||
|
||||
## Why not GDB
|
||||
|
||||
The licence is not the reason — a stub written here from the published protocol
|
||||
would be our own Apache-2.0 code, the same clean-room argument as
|
||||
[../architecture/devices.md](../architecture/devices.md) makes for Linux
|
||||
drivers. Two technical reasons, and they are both about what a *generic*
|
||||
debugger structurally cannot do.
|
||||
|
||||
**It only knows registers and memory.** GDB can tell you that `0x3eceb000`
|
||||
contains zeroes. It cannot tell you that the page is in the DMA arena's free
|
||||
list *and* on `kalloc`'s free list *and* mapped into pid 7 — because those are
|
||||
this kernel's abstractions, and a debugger that knows about them has to be
|
||||
written against them. That query is the one that would have ended the session
|
||||
described above in one line.
|
||||
|
||||
**It stops the world**, per the rule above.
|
||||
|
||||
There is a third, smaller reason: a `gdbstub` is a protocol you implement and
|
||||
then debug, and its failure mode is an unresponsive stub with no way to ask it
|
||||
why. The thing below can always be driven by hand over a serial line.
|
||||
|
||||
---
|
||||
|
||||
## The layers
|
||||
|
||||
Three transports, and the rule decides which gets what. **The kernel's half is
|
||||
the minimum that works when nothing else does; everything convenient lives in a
|
||||
process** — which is the same sentence as [hybrid.md](hybrid.md)'s, about a
|
||||
different thing.
|
||||
|
||||
| | Works when | |
|
||||
|---|---|---|
|
||||
| **0. Console and UART** | always, including panic context, with interrupts off, no allocation, no scheduler, no lock taken | `sysrq`-driven. If the machine is dying this is the one you have. |
|
||||
| **1. `/dev/dbg`** | there is a scheduler and a process | A character device. The local client is an ordinary program — exactly the `/dev/pci` discipline, for exactly the same reason. |
|
||||
| **2. The network** | there is a network | A **userspace daemon** bridging TCP to `/dev/dbg`. Not in the kernel. lwIP must stay out of the debugger's path, or the debugger cannot be pointed at lwIP. |
|
||||
|
||||
Level 2 is deliberately last, and it is not in the first cut.
|
||||
|
||||
---
|
||||
|
||||
## The protocol is lines of text
|
||||
|
||||
A request is a line. A response is lines, ending in a blank one.
|
||||
|
||||
```
|
||||
> whose 0x3eceb000
|
||||
phys 0x3eceb000
|
||||
dma arena extent 2, FREE (4096000 bytes @ 0x3eceb000)
|
||||
kalloc free list, position 41
|
||||
pid 7 vgpud va 0x4000d000 rw-u vma[3] no_free
|
||||
*** three owners -- this page is claimed twice
|
||||
|
||||
> view dma
|
||||
arena 0x3e000000-0x3f000000 16384 KB, 12284 KB free
|
||||
free[0] 0x3e0a4000 8192 KB
|
||||
free[1] 0x3ecec000 4092 KB
|
||||
in use 4096 KB in 3 blocks
|
||||
```
|
||||
|
||||
Text, and not a typed binary protocol, for three reasons that all turn out to be
|
||||
the same reason:
|
||||
|
||||
- **It works at level 0.** Panic context cannot allocate, cannot frame records,
|
||||
cannot wait for a client to acknowledge anything. It can put characters on a
|
||||
serial line. A binary protocol needs a text fallback anyway, and then there are
|
||||
two protocols.
|
||||
- **It is usable before a client exists.** `cat`, `echo`, a serial terminal, the
|
||||
guest's own shell. A protocol that needs its client built first is a protocol
|
||||
nobody can debug.
|
||||
- **It survives the thing it is debugging.** When the answer arrives garbled,
|
||||
you can still read most of it.
|
||||
|
||||
A richer client can be layered on later by parsing this, and if that ever
|
||||
happens the kernel does not change.
|
||||
|
||||
---
|
||||
|
||||
## Milestones
|
||||
|
||||
| | | |
|
||||
|---|---|---|
|
||||
| **D0** | **Symbols.** A compact sorted table emitted from `nm` at link time and linked into the image; `ksym_lookup(addr, &off)`. | **done** |
|
||||
| **D1** | **A log that survives.** A ring behind `cprintf`, timestamped, in a region the allocator never sees — so a panic's last words are readable on the next boot. `/proc/kmsg`. | **done** |
|
||||
| **D2** | **The introspection registry.** `dbg_register("dma", dma_dump)`. Seven views, `/dev/dbg`, `schdbg`, and `sysrq g`. | **done** |
|
||||
| **D3** | **`whose <addr>`** — the cross-subsystem ownership query above. | |
|
||||
| **D4** | **Watchpoints**, DR0–DR3, break-on-write, reporting rip and call chain and **continuing** rather than stopping. | |
|
||||
|
||||
### Why D0 and D1 come before the interesting parts
|
||||
|
||||
Neither is clever. Both multiply everything after them: a view that prints an
|
||||
address is worth less than one that prints a name, and a query you can only run
|
||||
*after* you have noticed something is worth less than a log that was already
|
||||
running when it happened.
|
||||
|
||||
D1 in particular changes the job rather than improving it. Today a triple fault
|
||||
leaves nothing at all — the screen is gone, the serial buffer is gone, and the
|
||||
only evidence is whatever you happened to be watching. A log that outlives the
|
||||
boot turns that into a file.
|
||||
|
||||
### What D1 turned out to cost
|
||||
|
||||
One measurement changed the design. The log was first placed at the top of RAM
|
||||
beside the DMA arena, which is the obvious spot for a region nothing should
|
||||
allocate — and on the first reboot its magic came back as ASCII, because **the
|
||||
top of usable memory is exactly where the firmware makes its own allocations**
|
||||
before handing the machine over. It sits above the kernel image instead, which
|
||||
is both a stable address across boots and memory the loader has already
|
||||
committed to. Recorded because the reasoning that picked the wrong place was
|
||||
sound, and only the test was not.
|
||||
|
||||
### What D2 is really for
|
||||
|
||||
Not the views themselves — the *registry*. A subsystem publishes its state with
|
||||
one line at the bottom of the file that owns the data, and nothing central has to
|
||||
be edited. That is the only version of this that stays current; the `sysrq`
|
||||
switch is the version that does not, and it is already three self-tests and no
|
||||
state.
|
||||
|
||||
---
|
||||
|
||||
## The security note, for the parts not yet built
|
||||
|
||||
A debugger that reads arbitrary physical memory and a TCP listener are, together,
|
||||
a remote root hole. That is true of every kernel debugger ever written and it is
|
||||
not a reason not to build one, but it has to be decided rather than discovered:
|
||||
|
||||
- Level 2 is **off unless a boot flag turns it on**, and binds loopback by
|
||||
default.
|
||||
- No memory *writes* in the first cut. Reading is what finds bugs; writing is
|
||||
what a later milestone can argue for on its own.
|
||||
|
||||
---
|
||||
|
||||
## What it will still not do
|
||||
|
||||
- **It will not step, break, or halt**, per the rule. If a bug genuinely needs a
|
||||
stepper, this is the wrong tool and that is a deliberate trade.
|
||||
- **No stack unwinding through interrupt frames.** `getcallerpcs()` walks `rbp`
|
||||
chains and stops where the frame pointer stops; a trap frame in the middle of
|
||||
a chain ends the walk.
|
||||
- **No userspace symbolization.** The kernel will name kernel symbols. A fault
|
||||
in a process reports the binary and the offset, and resolving that stays a
|
||||
host-side job.
|
||||
- **No profiling.** No sampling, no counters, no flame anything. A timer-driven
|
||||
sampler is a different tool that happens to share a transport.
|
||||
- **Watchpoints are limited to four**, by the hardware, and to 1/2/4/8 bytes.
|
||||
There is no software watchpoint fallback and there should not be one.
|
||||
- **It is not a tracer.** Static tracepoints and an event ring are the obvious
|
||||
next thing after D4 and are not on this plan.
|
||||
|
||||
---
|
||||
|
||||
## On the freeze
|
||||
|
||||
[1.0.md](1.0.md) says no new capability starts until the register is empty, and
|
||||
the register is not empty. Two things about that.
|
||||
|
||||
The bookkeeping answer is that the rule is *"adding a row requires closing one"*,
|
||||
and **A released PCI device** closed when H2 landed. There is a slot.
|
||||
|
||||
The better answer is that the register is a list of **capabilities of the OS**,
|
||||
and this is not one — nothing in it ships to a user or appears in the acceptance
|
||||
test. It is a tool, and the case for spending time on a tool is that it makes
|
||||
everything still on the register cheaper to finish. Gate 0 of [1.0.md](1.0.md)
|
||||
is *"fix what is already wrong"*, assembled from every *What it does not do*
|
||||
section in [../architecture/](../architecture/). That gate is a list of bugs to
|
||||
find in a kernel that cannot currently be asked anything.
|
||||
|
||||
**Build the instrument before the survey**, and then the freeze holds for the
|
||||
rest of it.
|
||||
55
include/dbg.h
Normal file
55
include/dbg.h
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
|
||||
#include "types.h"
|
||||
|
||||
struct sbuf;
|
||||
|
||||
// Asking the kernel a question.
|
||||
//
|
||||
// docs/plan/debugger.md milestone D2. The thing this exists to end is the
|
||||
// ninety-second loop: add a cprintf, rebuild, boot, read the serial log, and
|
||||
// learn the answer to exactly the one question you thought of before the
|
||||
// rebuild.
|
||||
//
|
||||
// The interesting part is not the views. It is that a subsystem publishes its
|
||||
// own state with **one line at the bottom of the file that owns the data**, and
|
||||
// nothing central has to be edited to add one:
|
||||
//
|
||||
// dbg_register("dma", dma_dump, "the DMA arena's free extents");
|
||||
//
|
||||
// That is the difference between something that stays current and something
|
||||
// that does not. /proc/sysrq is the version that does not -- a switch in one
|
||||
// file with a letter per action, which is why after two years it holds three
|
||||
// self-tests and no state at all.
|
||||
//
|
||||
// Views run in ordinary process context, from a write to /dev/dbg or from
|
||||
// sysrq, so they may take locks. **They must not be called from panic().**
|
||||
|
||||
// The argument is whatever followed the view's name on the command line, or
|
||||
// "" when there was none. Most views ignore it; the ones that can produce a
|
||||
// hundred lines use it to narrow -- `vma 4` rather than `vma`. It is also the
|
||||
// shape milestone D3 needs, since `whose <addr>` is nothing but an argument.
|
||||
typedef void (*dbg_view_fn)(struct sbuf *out, const char *arg);
|
||||
|
||||
// Publish a view. `what` is one line, shown by `help`.
|
||||
//
|
||||
// Safe to call from any subsystem's init, in any order: the table is static
|
||||
// and needs no initialisation of its own.
|
||||
void dbg_register(const char *name, dbg_view_fn fn, const char *what);
|
||||
|
||||
// Run one command line and render the answer. Returns the number of bytes
|
||||
// written into `out`.
|
||||
//
|
||||
// The protocol is lines of text -- see docs/plan/debugger.md for why that and
|
||||
// not a typed binary format. Commands:
|
||||
//
|
||||
// help what this kernel can be asked
|
||||
// all every view, in registration order
|
||||
// <name> [arg] that view, optionally narrowed
|
||||
int dbg_command(const char *line, struct sbuf *out);
|
||||
|
||||
// Every view, straight to the console. This is what sysrq 'g' runs, and it is
|
||||
// the level-0 path from the plan: no process, no filesystem, no client.
|
||||
void dbg_dump_all(void);
|
||||
|
||||
#define DBG_MAJOR 10
|
||||
|
|
@ -59,6 +59,7 @@ int pciuser_read(struct inode*, uint64, char*, int);
|
|||
int pciuser_write(struct inode*, uint64, char*, int);
|
||||
int pciuser_mmio_ok(uint64 phys, uint64 len);
|
||||
void pciuser_release_all(struct proc*);
|
||||
int pciuser_owner_pid(int idx);
|
||||
int proc_reincarnate(int status);
|
||||
void proc_kill_init(void);
|
||||
extern uint init_generation;
|
||||
|
|
@ -363,6 +364,23 @@ void ioapicinit(void);
|
|||
|
||||
// dma.c — see include/dma.h for the full contract
|
||||
void dma_init(void*, void*);
|
||||
|
||||
// dbg.c -- /dev/dbg, the introspection registry. See include/dbg.h.
|
||||
int dbg_read(struct inode*, uint64, char*, int);
|
||||
int dbg_write(struct inode*, uint64, char*, int);
|
||||
|
||||
// klog.c -- the kernel log that outlives the kernel. See include/klog.h.
|
||||
void klog_init(void *base, uint64 bytes);
|
||||
void klog_putc(int c);
|
||||
int klog_read(uint64 off, char *dst, int n);
|
||||
uint64 klog_len(void);
|
||||
uint64 klog_prev_bytes(void);
|
||||
int klog_recovered(void);
|
||||
|
||||
// ksym.c -- naming a kernel address. See docs/plan/debugger.md, D0.
|
||||
int ksym_ready(void);
|
||||
const char* ksym_lookup(uint64 addr, uint64 *off);
|
||||
int ksym_fmt(char *buf, int cap, uint64 addr);
|
||||
void *dma_alloc(uint64 size, uint64 align);
|
||||
void dma_free(void*, uint64 size);
|
||||
addr_t dma_phys(const void*);
|
||||
|
|
|
|||
44
include/klog.h
Normal file
44
include/klog.h
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
|
||||
#include "types.h"
|
||||
|
||||
// The kernel log that outlives the kernel.
|
||||
//
|
||||
// docs/plan/debugger.md milestone D1. Until this existed, `cprintf` wrote to a
|
||||
// serial line and a screen and was then gone: a panic's report could be read
|
||||
// only by whoever was watching at the time, and a triple fault left nothing at
|
||||
// all. panic.c said so in its own header comment -- *"there is no core dump and
|
||||
// no log to go back to"* -- and that sentence is what this deletes.
|
||||
//
|
||||
// The buffer is a byte ring in a region carved off physical memory *before*
|
||||
// kalloc or the DMA arena are given anything, at a fixed distance below
|
||||
// PHYSTOP, so its address is a property of the machine rather than of this
|
||||
// boot. Nothing allocates it, nothing frees it, and a reboot that leaves DRAM
|
||||
// alone leaves it intact -- which is what makes the last words of a crashed
|
||||
// kernel readable from the one that comes after.
|
||||
|
||||
#define KLOG_BYTES (128 * 1024) // ring plus header
|
||||
#define KLOG_MAGIC 0x676f4c6bu // "kLog"
|
||||
#define KLOG_VERSION 1
|
||||
|
||||
// Kept at the head of the region. Everything in it has to survive a reboot, so
|
||||
// it is all fixed-width and there are no pointers: the only thing that says
|
||||
// where the data is, is that it follows.
|
||||
struct klog_hdr {
|
||||
uint32 magic;
|
||||
uint32 version;
|
||||
uint32 size; // bytes of data[], = KLOG_BYTES - sizeof(struct klog_hdr)
|
||||
uint32 boot; // how many kernels have written here
|
||||
uint64 head; // total bytes ever written; the ring is head mod size
|
||||
uint64 mark; // head as it stood when this boot started
|
||||
uint32 sum; // over the fields above -- see klog.c
|
||||
uint32 pad;
|
||||
char data[];
|
||||
};
|
||||
|
||||
void klog_init(void *base, uint64 bytes);
|
||||
void klog_putc(int c);
|
||||
int klog_read(uint64 off, char *dst, int n);
|
||||
uint64 klog_len(void);
|
||||
uint64 klog_prev_bytes(void);
|
||||
int klog_recovered(void);
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
#define NOFILE 128 // open files per process
|
||||
#define NFILE 512 // open files per system
|
||||
#define NINODE 200 // maximum number of active i-nodes
|
||||
#define NDEV 10 // maximum major device number
|
||||
#define NDEV 11 // maximum major device number
|
||||
#define RAMDISK_DEV 2 // memory-backed block device (kernel/dev/ramdisk.c)
|
||||
#define USBDISK_DEV 3 // USB mass storage (kernel/drivers/usb_storage.c)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,25 @@
|
|||
#include "types.h"
|
||||
#include "date.h"
|
||||
|
||||
// struct iovec is declared in two headers on purpose, and guarded so that
|
||||
// including both is not an error.
|
||||
//
|
||||
// posix.h has it because readv/writev need it; this file has it because a
|
||||
// userspace program including <socket.h> for sendmsg must get a complete
|
||||
// struct msghdr without also having to know about a kernel header. Both
|
||||
// definitions are identical and both are Linux's layout.
|
||||
//
|
||||
// The guard is not cosmetic. C23 permits redeclaring a struct with identical
|
||||
// members and GCC 15 defaults to C23, so the duplicate was invisible on the
|
||||
// gcc path -- while clang defaults to gnu17 and rejected it, which broke the
|
||||
// one toolchain shell.nix exists to provide and which the UEFI loader needs.
|
||||
#ifndef SCH_IOVEC_DEFINED
|
||||
#define SCH_IOVEC_DEFINED
|
||||
struct iovec {
|
||||
void *iov_base;
|
||||
uint64 iov_len;
|
||||
};
|
||||
#endif
|
||||
|
||||
// Linux's IOV_MAX. readv/writev used to cap iovcnt at 16 -- which is legal,
|
||||
// since POSIX only requires 16 -- and returned EINVAL above it. But programs
|
||||
|
|
|
|||
46
include/sbuf.h
Normal file
46
include/sbuf.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
|
||||
#include "types.h"
|
||||
|
||||
// A bounded string builder, because there is no snprintf in this kernel.
|
||||
//
|
||||
// These lived inside kernel/dev/procfs.c, where they were written, and were
|
||||
// static to it. The debugger's introspection registry needs the same thing --
|
||||
// a subsystem rendering its own state into a caller's buffer, with no
|
||||
// allocation and no way to run off the end -- and the choice was to write a
|
||||
// second set or to move this one. See docs/plan/debugger.md, D2.
|
||||
//
|
||||
// Nothing here can overflow: every writer checks cap, and a full buffer
|
||||
// silently drops the rest. That is the right trade for something whose callers
|
||||
// are a /proc read and a crash-time dump; a builder that can fail is a builder
|
||||
// every caller has to check, and none of them would.
|
||||
|
||||
struct sbuf {
|
||||
char *p;
|
||||
int len;
|
||||
int cap;
|
||||
};
|
||||
|
||||
void sb_putc(struct sbuf *sb, char c);
|
||||
void sb_puts(struct sbuf *sb, const char *s);
|
||||
|
||||
// A string in a fixed-width column, space-padded. Over-long values run past
|
||||
// their column rather than being truncated: a misaligned row is easier to read
|
||||
// than a mangled name.
|
||||
void sb_pad(struct sbuf *sb, const char *s, int width);
|
||||
|
||||
// Four lowercase hex digits, zero-padded. PCI and USB identifiers are always
|
||||
// written this way, and a lookup table keyed on "8086:100e" does not match
|
||||
// "8086:100E" or "8086:10e".
|
||||
void sb_puthex4(struct sbuf *sb, uint32 v);
|
||||
|
||||
void sb_putu(struct sbuf *sb, uint64 v);
|
||||
|
||||
// Zero-padded hex of a given width, for addresses.
|
||||
void sb_puthex(struct sbuf *sb, uint64 v, int digits);
|
||||
|
||||
// A decimal number in a right-aligned column, so tables of sizes line up.
|
||||
void sb_putu_pad(struct sbuf *sb, uint64 v, int width);
|
||||
|
||||
// "<label><value> <unit>\n" -- the shape almost every /proc line takes.
|
||||
void sb_kv(struct sbuf *sb, const char *label, uint64 v, const char *unit);
|
||||
|
|
@ -97,10 +97,25 @@
|
|||
// kernel will enforce.
|
||||
#define SCM_MAXFD 8
|
||||
|
||||
// struct iovec is declared in two headers on purpose, and guarded so that
|
||||
// including both is not an error.
|
||||
//
|
||||
// posix.h has it because readv/writev need it; this file has it because a
|
||||
// userspace program including <socket.h> for sendmsg must get a complete
|
||||
// struct msghdr without also having to know about a kernel header. Both
|
||||
// definitions are identical and both are Linux's layout.
|
||||
//
|
||||
// The guard is not cosmetic. C23 permits redeclaring a struct with identical
|
||||
// members and GCC 15 defaults to C23, so the duplicate was invisible on the
|
||||
// gcc path -- while clang defaults to gnu17 and rejected it, which broke the
|
||||
// one toolchain shell.nix exists to provide and which the UEFI loader needs.
|
||||
#ifndef SCH_IOVEC_DEFINED
|
||||
#define SCH_IOVEC_DEFINED
|
||||
struct iovec {
|
||||
void *iov_base;
|
||||
uint64 iov_len;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct msghdr {
|
||||
void *msg_name; // struct sockaddr *, or 0
|
||||
|
|
|
|||
|
|
@ -143,6 +143,9 @@ e820_init(void)
|
|||
// physically contiguous, so the region that set `top` must be able to hold
|
||||
// it. Picking the largest region makes this true on any machine worth
|
||||
// booting; checking says so out loud if it ever is not.
|
||||
//
|
||||
// The kernel log is *not* up here -- it sits just above the kernel image,
|
||||
// where the firmware does not allocate. See klog_init()'s caller.
|
||||
if (largest < DMA_ARENA_BYTES) {
|
||||
cprintf("e820: largest usable region is %d MB, too small for the %d MB "
|
||||
"DMA arena\n", (int)(largest / (1024 * 1024)),
|
||||
|
|
|
|||
|
|
@ -111,8 +111,14 @@ trap(struct trapframe *tf)
|
|||
}
|
||||
|
||||
if(myproc() == 0 || (tf->cs&3) == 0){
|
||||
cprintf("unexpected trap %d from cpu %d rip %p (cr2=0x%p) err=0x%p\n",
|
||||
tf->trapno, cpunum(), tf->rip, rcr2(), tf->err);
|
||||
// The rip is named where it can be. This is a kernel address, so
|
||||
// ksym_lookup() has something to say about it -- and the whole reason
|
||||
// docs/plan/debugger.md puts D0 first is that "rip ffff8000001294c2"
|
||||
// costs an objdump session and "rip in dma_alloc+0x8c" does not.
|
||||
char where[72];
|
||||
ksym_fmt(where, sizeof(where), tf->rip);
|
||||
cprintf("unexpected trap %d from cpu %d rip %p <%s> (cr2=0x%p) err=0x%p\n",
|
||||
tf->trapno, cpunum(), tf->rip, where, rcr2(), tf->err);
|
||||
if (myproc())
|
||||
cprintf("proc id: %d\n", myproc()->pid);
|
||||
if (tf->trapno == T_GPFLT) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include <stdarg.h>
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#include "klog.h"
|
||||
#include "vt.h"
|
||||
#include "kbd.h" /* KEY_VT_SWITCH -- the terminal-switch chords */
|
||||
#include "gfx.h"
|
||||
|
|
@ -205,6 +206,11 @@ consputc(int c)
|
|||
for(;;) hlt();
|
||||
}
|
||||
|
||||
// Every kernel message passes through here, which is what makes this the
|
||||
// one hook the log needs: cprintf, panic's report, a driver's complaint at
|
||||
// three in the morning. See include/klog.h.
|
||||
klog_putc(c);
|
||||
|
||||
if (c == BACKSPACE) {
|
||||
uartputc('\b'); uartputc(' '); uartputc('\b');
|
||||
if (!cons_serial_only)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
#include "mmu.h"
|
||||
#include "spinlock.h"
|
||||
#include "dma.h"
|
||||
#include "sbuf.h"
|
||||
#include "dbg.h"
|
||||
|
||||
#define DMA_GRAIN 64 // one cache line; also xHCI's ring alignment
|
||||
#define NEXTENT 64 // free extents we can track
|
||||
|
|
@ -30,6 +32,59 @@ roundup(uint64 v, uint64 to)
|
|||
return (v + to - 1) & ~(to - 1);
|
||||
}
|
||||
|
||||
// What the arena is holding, extent by extent.
|
||||
//
|
||||
// This is the view that would have ended the H2 investigation in one line. The
|
||||
// bug was that a 4 MB block was on this free list *and* on kalloc's at the same
|
||||
// time, and the only way to see it at the time was to read two allocators and
|
||||
// reason about PHYSTOP. Printing the list is not clever; not being able to
|
||||
// print it was the problem.
|
||||
static void
|
||||
dma_dump(struct sbuf *out, const char *arg)
|
||||
{
|
||||
(void)arg;
|
||||
acquire(&arena.lock);
|
||||
|
||||
sb_puts(out, "arena 0x");
|
||||
sb_puthex(out, arena.lo, 16);
|
||||
sb_puts(out, "-0x");
|
||||
sb_puthex(out, arena.hi, 16);
|
||||
sb_puts(out, " (virtual; physical is this minus KERNBASE)\n");
|
||||
|
||||
sb_puts(out, " ");
|
||||
sb_putu(out, arena.total / 1024);
|
||||
sb_puts(out, " KB total, ");
|
||||
{
|
||||
uint64 free = 0;
|
||||
for (int i = 0; i < arena.n; i++)
|
||||
free += arena.free[i].len;
|
||||
sb_putu(out, free / 1024);
|
||||
sb_puts(out, " KB free in ");
|
||||
sb_putu(out, arena.n);
|
||||
sb_puts(out, " extent(s), ");
|
||||
sb_putu(out, (arena.total - free) / 1024);
|
||||
sb_puts(out, " KB out\n");
|
||||
}
|
||||
|
||||
sb_puts(out, "\n # base phys bytes\n");
|
||||
for (int i = 0; i < arena.n; i++) {
|
||||
sb_puts(out, " ");
|
||||
sb_putu_pad(out, i, 2);
|
||||
sb_puts(out, " 0x");
|
||||
sb_puthex(out, arena.free[i].base, 16);
|
||||
sb_puts(out, " 0x");
|
||||
sb_puthex(out, arena.free[i].base - (addr_t)KERNBASE, 16);
|
||||
sb_puts(out, " ");
|
||||
sb_putu_pad(out, arena.free[i].len, 10);
|
||||
sb_putc(out, '\n');
|
||||
}
|
||||
if (arena.n == NEXTENT)
|
||||
sb_puts(out, "\n the extent table is FULL -- a free here leaks rather "
|
||||
"than corrupting the list\n");
|
||||
|
||||
release(&arena.lock);
|
||||
}
|
||||
|
||||
void
|
||||
dma_init(void *vstart, void *vend)
|
||||
{
|
||||
|
|
@ -42,6 +97,8 @@ dma_init(void *vstart, void *vend)
|
|||
arena.free[0].len = arena.hi - arena.lo;
|
||||
arena.n = 1;
|
||||
arena.total = arena.free[0].len;
|
||||
|
||||
dbg_register("dma", dma_dump, "the DMA arena's free extents");
|
||||
}
|
||||
|
||||
// Remove free[i], shifting the tail down.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
#include "memlayout.h"
|
||||
#include "mmu.h"
|
||||
#include "spinlock.h"
|
||||
#include "sbuf.h"
|
||||
#include "dbg.h"
|
||||
|
||||
void freerange(void *vstart, void *vend);
|
||||
extern char end[]; // first address after kernel loaded from ELF file
|
||||
|
|
@ -80,10 +82,45 @@ kinit1(void *vstart, void *vend)
|
|||
}
|
||||
}
|
||||
|
||||
// The page allocator, from the outside.
|
||||
//
|
||||
// Counting the free list is O(the free list), which on a gigabyte is a quarter
|
||||
// of a million links. That is fine for a command someone typed and would not
|
||||
// be fine anywhere else, so it lives here and nowhere near a hot path -- and
|
||||
// the walk is bounded, because the one time you most want to run this is when
|
||||
// you suspect the list is a loop.
|
||||
static void
|
||||
mem_dump(struct sbuf *out, const char *arg)
|
||||
{
|
||||
(void)arg;
|
||||
struct run *r;
|
||||
uint64 n = 0;
|
||||
int looped = 0;
|
||||
|
||||
if (kmem.use_lock)
|
||||
acquire(&kmem.lock);
|
||||
for (r = kmem.freelist; r; r = r->next) {
|
||||
if (++n > (PHYSTOP / PGSIZE) + 16) { looped = 1; break; }
|
||||
}
|
||||
if (kmem.use_lock)
|
||||
release(&kmem.lock);
|
||||
|
||||
sb_puts(out, "window 0x");
|
||||
sb_puthex(out, (addr_t)end, 16);
|
||||
sb_puts(out, " upward; PHYSTOP is 0x");
|
||||
sb_puthex(out, (addr_t)PHYSTOP, 16);
|
||||
sb_putc(out, '\n');
|
||||
sb_kv(out, "free ", n, "pages");
|
||||
sb_kv(out, " ", n * PGSIZE / 1024, "KB");
|
||||
if (looped)
|
||||
sb_puts(out, "\n the free list did not end -- it has a CYCLE in it\n");
|
||||
}
|
||||
|
||||
void
|
||||
kinit2()
|
||||
{
|
||||
kmem.use_lock = 1;
|
||||
dbg_register("mem", mem_dump, "the page allocator's free list");
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
|||
267
kernel/core/klog.c
Normal file
267
kernel/core/klog.c
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// The kernel log that outlives the kernel. See include/klog.h.
|
||||
//
|
||||
// Two properties are worth reading the code for, because both are easy to
|
||||
// write and easy to get subtly wrong:
|
||||
//
|
||||
// **It takes no lock.** klog_putc() runs inside consputc(), which runs with
|
||||
// cons.lock held on the ordinary path and with interrupts off and every other
|
||||
// CPU spinning on the panic path. A lock here would add a way for the logger
|
||||
// to deadlock the thing it exists to report on, which is the one failure it
|
||||
// must not have. The cost is that two CPUs printing at once can interleave
|
||||
// characters -- which is already true of the console itself, so the log is no
|
||||
// worse than the screen it mirrors.
|
||||
//
|
||||
// **It buffers before it exists.** The region is carved after e820_init(), so
|
||||
// everything printed before that -- the loader's handover, the memory map
|
||||
// itself -- has nowhere to go yet. Dropping it would mean the log is blank for
|
||||
// exactly the part of the boot that is hardest to debug any other way, so
|
||||
// early output goes into a small static array and is flushed into the ring the
|
||||
// moment there is one.
|
||||
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#include "memlayout.h"
|
||||
#include "klog.h"
|
||||
|
||||
// consputc()'s cursor-movement escape, from kernel/core/console.c.
|
||||
#define BACKSPACE 0x100
|
||||
|
||||
static struct klog_hdr *klog;
|
||||
static int recovered; // the buffer held a previous boot when we found it
|
||||
static uint64 prev_bytes; // how much of it that was
|
||||
|
||||
// Early output, before there is a ring to put it in. One page is enough for
|
||||
// the handful of lines between the first cprintf and klog_init(); anything
|
||||
// past it is dropped rather than overwritten, because the *first* lines of a
|
||||
// boot that died early are the ones worth having.
|
||||
static char early[4096];
|
||||
static int nearly;
|
||||
static int early_lost;
|
||||
|
||||
// Line state, kept here rather than derived, so a timestamp is emitted once
|
||||
// per line and not once per write.
|
||||
static int at_line_start = 1;
|
||||
|
||||
// Over the fields that do not move while a kernel is running -- deliberately
|
||||
// not over head and mark.
|
||||
//
|
||||
// Those two change on every character written, and a sum that covered them
|
||||
// would have to be recomputed on every character too: eight multiplies per
|
||||
// byte of log, for a check whose entire job is to notice that some *other*
|
||||
// program used this memory between two boots. head and mark are validated by
|
||||
// range instead, which catches the same thing without the cost.
|
||||
//
|
||||
// Not a CRC either, for the same reason of proportion: this does not need to
|
||||
// detect a clever corruption, and dragging kernel/fs/schfs's crc32c into core
|
||||
// to do it would be a dependency in the wrong direction.
|
||||
static uint32
|
||||
checksum(const struct klog_hdr *h)
|
||||
{
|
||||
uint32 s = 0x9e3779b9u;
|
||||
s = s * 31 + h->magic;
|
||||
s = s * 31 + h->version;
|
||||
s = s * 31 + h->size;
|
||||
return s;
|
||||
}
|
||||
|
||||
static int
|
||||
looks_valid(const struct klog_hdr *h, uint64 bytes)
|
||||
{
|
||||
if (h->magic != KLOG_MAGIC || h->version != KLOG_VERSION)
|
||||
return 0;
|
||||
if (h->size != bytes - sizeof(struct klog_hdr))
|
||||
return 0;
|
||||
if (h->sum != checksum(h))
|
||||
return 0;
|
||||
// head and mark are outside the sum, so they are checked for sense instead:
|
||||
// a boot cannot have ended before it started, and nothing this kernel does
|
||||
// writes a quarter of a petabyte to a log.
|
||||
if (h->mark > h->head)
|
||||
return 0;
|
||||
if (h->head > (1ULL << 48))
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void
|
||||
raw_putc(char c)
|
||||
{
|
||||
klog->data[klog->head % klog->size] = c;
|
||||
klog->head++;
|
||||
}
|
||||
|
||||
static void
|
||||
raw_puts(const char *s)
|
||||
{
|
||||
while (*s)
|
||||
raw_putc(*s++);
|
||||
}
|
||||
|
||||
// "[ 2.84] ", from the timer tick. Written by hand because the only
|
||||
// formatter in the kernel is cprintf, and cprintf is what calls this.
|
||||
static void
|
||||
stamp(void)
|
||||
{
|
||||
uint t = ticks;
|
||||
uint sec = t / 100;
|
||||
uint cs = t % 100;
|
||||
char b[16];
|
||||
int i = 0, j;
|
||||
|
||||
raw_putc('[');
|
||||
|
||||
if (sec == 0) {
|
||||
b[i++] = '0';
|
||||
} else {
|
||||
while (sec && i < 12) { b[i++] = '0' + (sec % 10); sec /= 10; }
|
||||
}
|
||||
for (j = i; j < 6; j++)
|
||||
raw_putc(' ');
|
||||
while (i-- > 0)
|
||||
raw_putc(b[i]);
|
||||
|
||||
raw_putc('.');
|
||||
raw_putc('0' + (cs / 10));
|
||||
raw_putc('0' + (cs % 10));
|
||||
raw_putc(']');
|
||||
raw_putc(' ');
|
||||
}
|
||||
|
||||
void
|
||||
klog_putc(int c)
|
||||
{
|
||||
char ch = (char)c;
|
||||
|
||||
// Carriage returns and the console's backspace are cursor movement, not
|
||||
// content. A log full of them reads as corruption.
|
||||
if (ch == '\r' || c == BACKSPACE || ch == 0)
|
||||
return;
|
||||
|
||||
if (!klog) {
|
||||
if (nearly < (int)sizeof(early))
|
||||
early[nearly++] = ch;
|
||||
else
|
||||
early_lost = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (at_line_start && ch != '\n')
|
||||
stamp();
|
||||
raw_putc(ch);
|
||||
at_line_start = (ch == '\n');
|
||||
}
|
||||
|
||||
void
|
||||
klog_init(void *base, uint64 bytes)
|
||||
{
|
||||
struct klog_hdr *h = (struct klog_hdr *)base;
|
||||
|
||||
if (bytes <= sizeof(struct klog_hdr))
|
||||
return;
|
||||
|
||||
if (looks_valid(h, bytes)) {
|
||||
recovered = 1;
|
||||
prev_bytes = h->head - h->mark; // what the *last* boot wrote
|
||||
} else {
|
||||
|
||||
// Either the first boot on this machine or a region something else has
|
||||
// been using. Either way the contents cannot be trusted, and printing
|
||||
// somebody else's bytes as if they were a log would be worse than
|
||||
// printing nothing.
|
||||
h->magic = KLOG_MAGIC;
|
||||
h->version = KLOG_VERSION;
|
||||
h->size = (uint32)(bytes - sizeof(struct klog_hdr));
|
||||
h->boot = 0;
|
||||
h->head = 0;
|
||||
recovered = 0;
|
||||
prev_bytes = 0;
|
||||
}
|
||||
|
||||
h->mark = h->head;
|
||||
h->boot++;
|
||||
h->sum = checksum(h);
|
||||
klog = h;
|
||||
|
||||
// A divider in the log itself rather than a flag a reader has to know about:
|
||||
// whoever ends up reading this will be reading text, possibly by eye, and a
|
||||
// line that says where one boot ends is worth more than a field that has to
|
||||
// be interpreted.
|
||||
at_line_start = 1;
|
||||
raw_puts("\n--- boot ");
|
||||
{
|
||||
char b[12];
|
||||
int i = 0;
|
||||
uint32 n = h->boot;
|
||||
if (n == 0) b[i++] = '0';
|
||||
while (n) { b[i++] = '0' + (n % 10); n /= 10; }
|
||||
while (i-- > 0) raw_putc(b[i]);
|
||||
}
|
||||
raw_puts(" ---\n");
|
||||
|
||||
for (int i = 0; i < nearly; i++) {
|
||||
char ch = early[i];
|
||||
if (ch == '\r' || ch == 0)
|
||||
continue;
|
||||
if (at_line_start && ch != '\n')
|
||||
stamp();
|
||||
raw_putc(ch);
|
||||
at_line_start = (ch == '\n');
|
||||
}
|
||||
if (early_lost)
|
||||
raw_puts("klog: early output overflowed and some was dropped\n");
|
||||
nearly = 0;
|
||||
|
||||
h->sum = checksum(h);
|
||||
|
||||
// Said out loud on every boot, because "the log survived" and "the log was
|
||||
// silently started over" look identical in a file that begins with a boot
|
||||
// divider either way.
|
||||
if (recovered)
|
||||
cprintf("klog: %d KB at %p, %d bytes carried over from the previous boot\n",
|
||||
(int)(bytes / 1024), (uint64)base, (int)prev_bytes);
|
||||
else
|
||||
cprintf("klog: %d KB at %p, nothing recoverable was there\n",
|
||||
(int)(bytes / 1024), (uint64)base);
|
||||
}
|
||||
|
||||
// How many bytes the ring currently holds, oldest first.
|
||||
uint64
|
||||
klog_len(void)
|
||||
{
|
||||
if (!klog)
|
||||
return 0;
|
||||
return klog->head < klog->size ? klog->head : klog->size;
|
||||
}
|
||||
|
||||
uint64 klog_prev_bytes(void) { return prev_bytes; }
|
||||
int klog_recovered(void) { return recovered; }
|
||||
|
||||
// Read at a byte offset into the log, where 0 is the oldest byte still held.
|
||||
//
|
||||
// Serving straight out of the ring rather than rendering into a scratch buffer
|
||||
// the way every other /proc file does, because this file is 128 KB and that
|
||||
// pattern regenerates the whole thing on every read into a stack array. See
|
||||
// procread() in kernel/dev/procfs.c.
|
||||
int
|
||||
klog_read(uint64 off, char *dst, int n)
|
||||
{
|
||||
uint64 have, first;
|
||||
int i;
|
||||
|
||||
if (!klog || n <= 0)
|
||||
return 0;
|
||||
|
||||
have = klog_len();
|
||||
if (off >= have)
|
||||
return 0; // EOF
|
||||
if ((uint64)n > have - off)
|
||||
n = (int)(have - off);
|
||||
|
||||
first = klog->head - have; // logical position of the oldest byte
|
||||
for (i = 0; i < n; i++)
|
||||
dst[i] = klog->data[(first + off + i) % klog->size];
|
||||
|
||||
return n;
|
||||
}
|
||||
128
kernel/core/ksym.c
Normal file
128
kernel/core/ksym.c
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Naming an address.
|
||||
//
|
||||
// docs/plan/debugger.md milestone D0. It is the least clever thing on that
|
||||
// plan and the one everything else is worth more with: a call chain of six
|
||||
// hex numbers has to be resolved by hand against a disassembly that may not
|
||||
// be the build that was running, and the resolving is done at exactly the
|
||||
// moment when you least want a second task.
|
||||
//
|
||||
// The table is built by tools/ksyms.pl from the *first* of two link passes and
|
||||
// linked into the second. kernel/kernel.ld puts .ksym after .text and .rodata,
|
||||
// so carrying the table cannot move the addresses recorded in it; the Makefile
|
||||
// checks that rather than trusting it, by listing the text symbols of both
|
||||
// passes and refusing to build if they differ.
|
||||
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
|
||||
// Weak, and the reason is worth stating: the first link pass has no table --
|
||||
// it is the input to the program that writes one. An undefined weak symbol
|
||||
// links as zero, so that pass resolves these to nothing and still links. The
|
||||
// same property means a kernel built with the generator removed boots fine and
|
||||
// simply cannot name anything, which is a much better failure than a build
|
||||
// that stops.
|
||||
extern const uint32 ksym_count __attribute__((weak));
|
||||
extern const uint64 ksym_base __attribute__((weak));
|
||||
extern const uint64 ksym_textend __attribute__((weak));
|
||||
extern const uint32 ksym_addr[] __attribute__((weak));
|
||||
extern const uint32 ksym_nameoff[] __attribute__((weak));
|
||||
extern const char ksym_names[] __attribute__((weak));
|
||||
|
||||
int
|
||||
ksym_ready(void)
|
||||
{
|
||||
return &ksym_count != 0 && ksym_count > 0;
|
||||
}
|
||||
|
||||
// The greatest symbol at or below addr.
|
||||
//
|
||||
// Returns 0 for anything outside the kernel's text, which includes every
|
||||
// userspace address -- naming a user rip with a kernel symbol would be worse
|
||||
// than printing the number, because it would look like an answer.
|
||||
const char *
|
||||
ksym_lookup(uint64 addr, uint64 *off)
|
||||
{
|
||||
if (!ksym_ready())
|
||||
return 0;
|
||||
if (addr < ksym_base || addr >= ksym_textend)
|
||||
return 0;
|
||||
|
||||
uint32 want = (uint32)(addr - ksym_base);
|
||||
|
||||
// Binary search for the last entry <= want, in the half-open form so the
|
||||
// indices stay unsigned and nothing has to go below zero. ksym_addr[0] is 0
|
||||
// by construction -- the base *is* the first symbol -- so the invariant
|
||||
// "ksym_addr[lo] <= want" holds on entry and there is no empty case.
|
||||
uint32 lo = 0, hi = ksym_count;
|
||||
while (hi - lo > 1) {
|
||||
uint32 mid = lo + (hi - lo) / 2;
|
||||
if (ksym_addr[mid] <= want)
|
||||
lo = mid;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
|
||||
if (off)
|
||||
*off = want - ksym_addr[lo];
|
||||
return &ksym_names[ksym_nameoff[lo]];
|
||||
}
|
||||
|
||||
static int
|
||||
put(char *buf, int cap, int n, const char *s)
|
||||
{
|
||||
while (*s && n < cap - 1)
|
||||
buf[n++] = *s++;
|
||||
return n;
|
||||
}
|
||||
|
||||
static int
|
||||
puthex(char *buf, int cap, int n, uint64 v)
|
||||
{
|
||||
static const char h[] = "0123456789abcdef";
|
||||
char tmp[17];
|
||||
int i = 0;
|
||||
|
||||
if (v == 0)
|
||||
return put(buf, cap, n, "0");
|
||||
while (v && i < 16) { tmp[i++] = h[v & 0xF]; v >>= 4; }
|
||||
while (i-- > 0 && n < cap - 1)
|
||||
buf[n++] = tmp[i];
|
||||
return n;
|
||||
}
|
||||
|
||||
// "sched+0x2a", or the bare address when there is nothing to say.
|
||||
//
|
||||
// Writes into a caller's buffer rather than printing, because both callers
|
||||
// that matter are in panic context: one is building a line for the crash
|
||||
// screen and the other is building the QR payload. Neither can call cprintf.
|
||||
int
|
||||
ksym_fmt(char *buf, int cap, uint64 addr)
|
||||
{
|
||||
uint64 off = 0;
|
||||
const char *name;
|
||||
int n = 0;
|
||||
|
||||
if (cap <= 1) {
|
||||
if (cap == 1)
|
||||
buf[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
name = ksym_lookup(addr, &off);
|
||||
if (!name) {
|
||||
n = put(buf, cap, n, "0x");
|
||||
n = puthex(buf, cap, n, addr);
|
||||
buf[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
n = put(buf, cap, n, name);
|
||||
if (off) {
|
||||
n = put(buf, cap, n, "+0x");
|
||||
n = puthex(buf, cap, n, off);
|
||||
}
|
||||
buf[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
|
@ -9,6 +9,9 @@
|
|||
#include "x86.h"
|
||||
#include "irq.h"
|
||||
#include "dma.h"
|
||||
#include "klog.h"
|
||||
|
||||
static void *klog_base; // the persistent log, just above the kernel image
|
||||
|
||||
static void startothers(void);
|
||||
static void mpmain(void) __attribute__((noreturn));
|
||||
|
|
@ -83,7 +86,23 @@ kmain(uint32 fb_addr)
|
|||
// Before anything touches PHYSTOP, which is now a variable read from the
|
||||
// firmware's memory map rather than a constant asserting 224 MB exists.
|
||||
e820_init();
|
||||
kinit1(end, P2V(PHYSTOP - DMA_ARENA_BYTES));
|
||||
|
||||
// The log region: the page after the kernel image, and the page allocator is
|
||||
// started above it rather than at `end`.
|
||||
//
|
||||
// Low memory, deliberately, and it took a measurement to learn why. The
|
||||
// obvious place is the top of RAM beside the DMA arena -- and on a reboot
|
||||
// the magic came back as ASCII, because that is exactly where the firmware
|
||||
// puts its own allocations before it hands the machine over. The bytes below
|
||||
// the kernel image are the ones the loader has already committed to leaving
|
||||
// alone, and `end` is the same address on every boot of the same build,
|
||||
// which is the property a region has to have to be found again.
|
||||
//
|
||||
// No hole has to be punched in the allocator for this: kinit1's lower bound
|
||||
// is a parameter, so moving it up by KLOG_BYTES is the whole of it.
|
||||
klog_base = (void *)PGROUNDUP((addr_t)end);
|
||||
klog_init(klog_base, KLOG_BYTES);
|
||||
kinit1((char *)klog_base + KLOG_BYTES, P2V(PHYSTOP - DMA_ARENA_BYTES));
|
||||
if (boot_verbose) cprintf("verbose boot: kinit1 done\n");
|
||||
dma_init(P2V(PHYSTOP - DMA_ARENA_BYTES), P2V(PHYSTOP));
|
||||
if (boot_verbose) cprintf("verbose boot: dma_init done\n");
|
||||
|
|
|
|||
|
|
@ -414,7 +414,19 @@ trace_line(char *buf, int cap, int i, addr_t pc)
|
|||
char *p = put_str(buf, e, " #");
|
||||
p = put_dec(p, e, (uint64)i);
|
||||
p = put_str(p, e, i < 10 ? " " : " ");
|
||||
put_hex(p, e, (uint64)pc, 16);
|
||||
p = put_hex(p, e, (uint64)pc, 16);
|
||||
|
||||
// And the name, when the kernel is carrying a symbol table. This is the
|
||||
// difference between a crash screen you photograph and then decode against
|
||||
// a disassembly, and one you can read. See kernel/core/ksym.c.
|
||||
if (ksym_ready()) {
|
||||
char name[64];
|
||||
if (ksym_lookup((uint64)pc, 0)) {
|
||||
ksym_fmt(name, sizeof(name), (uint64)pc);
|
||||
p = put_str(p, e, " ");
|
||||
put_str(p, e, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -520,10 +532,16 @@ report_serial(char *reason, addr_t *pcs, int npcs)
|
|||
}
|
||||
|
||||
cprintf("\n Call chain\n");
|
||||
for (int i = 0; i < npcs && pcs[i]; i++)
|
||||
cprintf(" #%d %p\n", i, pcs[i]);
|
||||
for (int i = 0; i < npcs && pcs[i]; i++) {
|
||||
char name[64];
|
||||
ksym_fmt(name, sizeof(name), (uint64)pcs[i]);
|
||||
cprintf(" #%d %p %s\n", i, pcs[i], name);
|
||||
}
|
||||
|
||||
cprintf("\n Resolve with: addr2line -e build/kernel/kernel <address>\n");
|
||||
if (!ksym_ready())
|
||||
cprintf("\n Resolve with: addr2line -e build/kernel/kernel <address>\n");
|
||||
else
|
||||
cprintf("\n For a line number: addr2line -e build/kernel/kernel <address>\n");
|
||||
cprintf(" Reason: %s\n", reason);
|
||||
cprintf(" System halted.\n");
|
||||
}
|
||||
|
|
|
|||
82
kernel/core/sbuf.c
Normal file
82
kernel/core/sbuf.c
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// The bounded string builder. See include/sbuf.h for what it is and why it
|
||||
// moved out of kernel/dev/procfs.c.
|
||||
|
||||
#include "types.h"
|
||||
#include "sbuf.h"
|
||||
|
||||
void
|
||||
sb_putc(struct sbuf *sb, char c)
|
||||
{
|
||||
if (sb->len < sb->cap - 1)
|
||||
sb->p[sb->len++] = c;
|
||||
}
|
||||
|
||||
void
|
||||
sb_puts(struct sbuf *sb, const char *s)
|
||||
{
|
||||
while (*s)
|
||||
sb_putc(sb, *s++);
|
||||
}
|
||||
|
||||
void
|
||||
sb_pad(struct sbuf *sb, const char *s, int width)
|
||||
{
|
||||
int n = 0;
|
||||
while (s[n]) {
|
||||
sb_putc(sb, s[n]);
|
||||
n++;
|
||||
}
|
||||
while (n++ < width)
|
||||
sb_putc(sb, ' ');
|
||||
}
|
||||
|
||||
void
|
||||
sb_puthex4(struct sbuf *sb, uint32 v)
|
||||
{
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (int i = 3; i >= 0; i--)
|
||||
sb_putc(sb, hex[(v >> (i * 4)) & 0xF]);
|
||||
}
|
||||
|
||||
void
|
||||
sb_puthex(struct sbuf *sb, uint64 v, int digits)
|
||||
{
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (int i = digits - 1; i >= 0; i--)
|
||||
sb_putc(sb, hex[(v >> (i * 4)) & 0xF]);
|
||||
}
|
||||
|
||||
void
|
||||
sb_putu(struct sbuf *sb, uint64 v)
|
||||
{
|
||||
char tmp[24];
|
||||
int i = 0;
|
||||
if (v == 0) { sb_putc(sb, '0'); return; }
|
||||
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
|
||||
while (i--) sb_putc(sb, tmp[i]);
|
||||
}
|
||||
|
||||
void
|
||||
sb_putu_pad(struct sbuf *sb, uint64 v, int width)
|
||||
{
|
||||
char tmp[24];
|
||||
int i = 0, n;
|
||||
|
||||
if (v == 0) { tmp[i++] = '0'; }
|
||||
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
|
||||
for (n = i; n < width; n++)
|
||||
sb_putc(sb, ' ');
|
||||
while (i--)
|
||||
sb_putc(sb, tmp[i]);
|
||||
}
|
||||
|
||||
void
|
||||
sb_kv(struct sbuf *sb, const char *label, uint64 v, const char *unit)
|
||||
{
|
||||
sb_puts(sb, label);
|
||||
sb_putu(sb, v);
|
||||
if (unit && *unit) { sb_putc(sb, ' '); sb_puts(sb, unit); }
|
||||
sb_putc(sb, '\n');
|
||||
}
|
||||
|
|
@ -118,4 +118,7 @@ struct devsw devsw[] = {
|
|||
// that do not live in the kernel. Registers arrive through mmap and
|
||||
// everything else through ioctl; see include/pciuser.h.
|
||||
[9] = { pciuser_read, pciuser_write },
|
||||
// /dev/dbg (major 10) -- ask the kernel a question. Write a line, read the
|
||||
// answer; see include/dbg.h and docs/plan/debugger.md.
|
||||
[10] = { dbg_read, dbg_write },
|
||||
};
|
||||
|
|
|
|||
243
kernel/dev/dbg.c
Normal file
243
kernel/dev/dbg.c
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// /dev/dbg -- asking the kernel a question. See include/dbg.h.
|
||||
//
|
||||
// The device is a conversation: write a line, read the answer.
|
||||
//
|
||||
// # echo dma > /dev/dbg && cat /dev/dbg
|
||||
//
|
||||
// That shape is chosen so the debugger is usable with the tools already on the
|
||||
// machine, before `schdbg` is built and on a system where it will not start.
|
||||
// docs/plan/debugger.md argues the case at length; the short version is that a
|
||||
// protocol you cannot drive by hand is a protocol you cannot debug.
|
||||
//
|
||||
// One conversation at a time, deliberately. The answer buffer is global and
|
||||
// the lock is held only while it is being filled, so two programs asking at
|
||||
// once get one answer each, in some order, and neither gets half of the
|
||||
// other's. Making it per-open would mean a struct file hook for something that
|
||||
// has exactly one user.
|
||||
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#include "param.h"
|
||||
#include "spinlock.h"
|
||||
#include "sleeplock.h"
|
||||
#include "fs.h"
|
||||
#include "file.h"
|
||||
#include "errno.h"
|
||||
#include "sbuf.h"
|
||||
#include "dbg.h"
|
||||
|
||||
#define NVIEW 24
|
||||
#define DBGBUF (16 * 1024)
|
||||
|
||||
struct view {
|
||||
const char *name;
|
||||
dbg_view_fn fn;
|
||||
const char *what;
|
||||
};
|
||||
|
||||
static struct view views[NVIEW];
|
||||
static int nview;
|
||||
|
||||
// Static, and 16 KB of it. A view of every process on a 64-slot table is the
|
||||
// largest thing here and comes to a few kilobytes; the alternative -- kalloc
|
||||
// on every read -- would put an allocation on the path of the tool you reach
|
||||
// for when the allocator is what you are investigating.
|
||||
static char dbgbuf[DBGBUF];
|
||||
static int dbglen;
|
||||
static struct spinlock dbglock;
|
||||
static int dbginit;
|
||||
|
||||
void
|
||||
dbg_register(const char *name, dbg_view_fn fn, const char *what)
|
||||
{
|
||||
if (nview >= NVIEW || !name || !fn)
|
||||
return;
|
||||
views[nview].name = name;
|
||||
views[nview].fn = fn;
|
||||
views[nview].what = what ? what : "";
|
||||
nview++;
|
||||
}
|
||||
|
||||
static int
|
||||
streq(const char *a, const char *b)
|
||||
{
|
||||
while (*a && *b && *a == *b) { a++; b++; }
|
||||
return *a == 0 && *b == 0;
|
||||
}
|
||||
|
||||
static void
|
||||
do_help(struct sbuf *out)
|
||||
{
|
||||
sb_puts(out, "views this kernel publishes:\n");
|
||||
for (int i = 0; i < nview; i++) {
|
||||
sb_puts(out, " ");
|
||||
sb_pad(out, views[i].name, 10);
|
||||
sb_puts(out, views[i].what);
|
||||
sb_putc(out, '\n');
|
||||
}
|
||||
sb_puts(out, "\n all every view above\n");
|
||||
sb_puts(out, " help this\n");
|
||||
}
|
||||
|
||||
static void
|
||||
do_all(struct sbuf *out)
|
||||
{
|
||||
for (int i = 0; i < nview; i++) {
|
||||
sb_puts(out, "=== ");
|
||||
sb_puts(out, views[i].name);
|
||||
sb_puts(out, " ===\n");
|
||||
views[i].fn(out, "");
|
||||
sb_putc(out, '\n');
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
dbg_command(const char *line, struct sbuf *out)
|
||||
{
|
||||
char word[32], arg[64];
|
||||
int n = 0;
|
||||
|
||||
while (*line == ' ' || *line == '\t')
|
||||
line++;
|
||||
while (*line && *line != '\n' && *line != ' ' && n < (int)sizeof(word) - 1)
|
||||
word[n++] = *line++;
|
||||
word[n] = 0;
|
||||
|
||||
while (*line == ' ' || *line == '\t')
|
||||
line++;
|
||||
n = 0;
|
||||
while (*line && *line != '\n' && n < (int)sizeof(arg) - 1)
|
||||
arg[n++] = *line++;
|
||||
while (n > 0 && (arg[n-1] == ' ' || arg[n-1] == '\r'))
|
||||
n--;
|
||||
arg[n] = 0;
|
||||
|
||||
if (word[0] == 0 || streq(word, "help")) {
|
||||
do_help(out);
|
||||
return out->len;
|
||||
}
|
||||
if (streq(word, "all")) {
|
||||
do_all(out);
|
||||
return out->len;
|
||||
}
|
||||
|
||||
for (int i = 0; i < nview; i++) {
|
||||
if (streq(word, views[i].name)) {
|
||||
views[i].fn(out, arg);
|
||||
return out->len;
|
||||
}
|
||||
}
|
||||
|
||||
sb_puts(out, "no view called '");
|
||||
sb_puts(out, word);
|
||||
sb_puts(out, "'. try: help\n");
|
||||
return out->len;
|
||||
}
|
||||
|
||||
// Fill dbgbuf, and say so if the answer did not fit.
|
||||
//
|
||||
// sb_* silently drops what will not fit, which is the right behaviour for a
|
||||
// builder that must not fail -- but a table that stops in the middle of a row
|
||||
// with no explanation reads as a kernel that is broken rather than as a buffer
|
||||
// that is full. The last 64 bytes are held back to have somewhere to say it.
|
||||
static int
|
||||
fill(const char *cmd)
|
||||
{
|
||||
struct sbuf sb;
|
||||
|
||||
sb.p = dbgbuf; sb.len = 0; sb.cap = DBGBUF - 64;
|
||||
if (cmd)
|
||||
dbg_command(cmd, &sb);
|
||||
else
|
||||
do_all(&sb);
|
||||
|
||||
if (sb.len >= sb.cap - 1) {
|
||||
sb.cap = DBGBUF;
|
||||
sb_puts(&sb, "\n-- truncated: the answer is larger than the buffer. "
|
||||
"ask for one view at a time.\n");
|
||||
}
|
||||
dbgbuf[sb.len] = 0;
|
||||
return sb.len;
|
||||
}
|
||||
|
||||
void
|
||||
dbg_dump_all(void)
|
||||
{
|
||||
if (!dbginit) { initlock(&dbglock, "dbg"); dbginit = 1; }
|
||||
|
||||
acquire(&dbglock);
|
||||
dbglen = fill(0);
|
||||
// Printed inside the lock so a second sysrq cannot interleave with this one.
|
||||
// cprintf takes cons.lock, which is never held by anything that wants
|
||||
// dbglock, so the order is fixed and there is no cycle.
|
||||
cprintf("%s", dbgbuf);
|
||||
release(&dbglock);
|
||||
}
|
||||
|
||||
// --- the device -------------------------------------------------------------
|
||||
|
||||
// A command. The answer replaces whatever was there.
|
||||
//
|
||||
// **The answer is read from offset zero.** That matters because writing to a
|
||||
// descriptor advances its offset, so a program that writes and then reads on
|
||||
// the same descriptor starts reading as many bytes into the answer as its
|
||||
// command was long -- which is exactly the bug this comment exists to stop
|
||||
// being written again. `cat` is unaffected because it opens the file fresh;
|
||||
// cmd/schdbg seeks back to zero. See lseek() there.
|
||||
int
|
||||
dbg_write(struct inode *ip, uint64 off, char *src, int n)
|
||||
{
|
||||
char line[128];
|
||||
int i;
|
||||
|
||||
(void)ip; (void)off;
|
||||
|
||||
if (n <= 0)
|
||||
return 0;
|
||||
if (n > (int)sizeof(line) - 1)
|
||||
n = (int)sizeof(line) - 1;
|
||||
for (i = 0; i < n; i++)
|
||||
line[i] = src[i];
|
||||
line[n] = 0;
|
||||
|
||||
if (!dbginit) { initlock(&dbglock, "dbg"); dbginit = 1; }
|
||||
|
||||
acquire(&dbglock);
|
||||
dbglen = fill(line);
|
||||
release(&dbglock);
|
||||
|
||||
return n; // the whole line was consumed either way
|
||||
}
|
||||
|
||||
int
|
||||
dbg_read(struct inode *ip, uint64 off, char *dst, int n)
|
||||
{
|
||||
int i, avail;
|
||||
|
||||
(void)ip;
|
||||
|
||||
if (!dbginit) { initlock(&dbglock, "dbg"); dbginit = 1; }
|
||||
|
||||
acquire(&dbglock);
|
||||
if (dbglen == 0) {
|
||||
// Nothing asked yet. Answering with the help text rather than with EOF,
|
||||
// because `cat /dev/dbg` on its own is what someone does first and an
|
||||
// empty file teaches them nothing.
|
||||
struct sbuf sb = { dbgbuf, 0, DBGBUF };
|
||||
do_help(&sb);
|
||||
dbglen = sb.len;
|
||||
}
|
||||
if (off >= (uint64)dbglen) {
|
||||
release(&dbglock);
|
||||
return 0; // EOF
|
||||
}
|
||||
avail = dbglen - (int)off;
|
||||
if (n > avail)
|
||||
n = avail;
|
||||
for (i = 0; i < n; i++)
|
||||
dst[i] = dbgbuf[off + i];
|
||||
release(&dbglock);
|
||||
return n;
|
||||
}
|
||||
|
|
@ -21,6 +21,8 @@
|
|||
#include "fs.h"
|
||||
#include "file.h"
|
||||
#include "pci.h"
|
||||
#include "sbuf.h"
|
||||
#include "dbg.h"
|
||||
#include "device.h"
|
||||
|
||||
static struct pci_dev devs[NPCI];
|
||||
|
|
@ -497,10 +499,54 @@ pci_info(char *buf, int cap)
|
|||
return len;
|
||||
}
|
||||
|
||||
// The same lines /proc/pci carries, plus the command register, which is the
|
||||
// bit that says whether a device can still reach memory. pciuser.c clears bus
|
||||
// mastering when a userspace driver dies -- see "Taking it back" in
|
||||
// docs/architecture/devices.md -- and "did that actually happen" is a question
|
||||
// worth being able to ask without a userspace program.
|
||||
static void
|
||||
pci_dump(struct sbuf *out, const char *arg)
|
||||
{
|
||||
(void)arg;
|
||||
sb_puts(out, " bdf vendor:device class "
|
||||
"cmd bm io mem driver\n");
|
||||
|
||||
for (int i = 0; i < ndevs; i++) {
|
||||
struct pci_dev *d = &devs[i];
|
||||
uint32 cmd = pci_read32(d->bus, d->slot, d->func, PCI_COMMAND) & 0xFFFF;
|
||||
|
||||
sb_puts(out, " ");
|
||||
sb_puthex(out, d->bus, 2); sb_putc(out, ':');
|
||||
sb_puthex(out, d->slot, 2); sb_putc(out, '.');
|
||||
sb_puthex(out, d->func, 1);
|
||||
sb_puts(out, " ");
|
||||
sb_puthex4(out, d->vendor); sb_putc(out, ':');
|
||||
sb_puthex4(out, d->device);
|
||||
sb_puts(out, " ");
|
||||
sb_pad(out, class_name(d->class, d->subclass), 25);
|
||||
sb_puthex4(out, cmd);
|
||||
sb_puts(out, (cmd & PCI_CMD_BUS_MASTER) ? " on " : " -- ");
|
||||
sb_puts(out, (cmd & PCI_CMD_IO) ? " on " : " -- ");
|
||||
sb_puts(out, (cmd & PCI_CMD_MEMORY) ? " on " : " -- ");
|
||||
sb_puts(out, " ");
|
||||
{
|
||||
int pid = pciuser_owner_pid(i);
|
||||
if (pid) {
|
||||
sb_puts(out, "userspace pid ");
|
||||
sb_putu(out, pid);
|
||||
} else {
|
||||
sb_puts(out, d->driver ? d->driver : "-");
|
||||
}
|
||||
}
|
||||
sb_putc(out, '\n');
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
pci_init(void)
|
||||
{
|
||||
ndevs = 0;
|
||||
dbg_register("pci", pci_dump, "the bus, with each device's command register");
|
||||
|
||||
// If there is no host bridge at 0:0.0 there is no PCI, and every config
|
||||
// read will come back 0xFFFFFFFF. Saying so once beats 8192 silent probes.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
#include "uaccess.h"
|
||||
#include "irq.h"
|
||||
#include "pciuser.h"
|
||||
#include "sbuf.h"
|
||||
#include "dbg.h"
|
||||
int mappages(pml4e_t *pgdir, void *va, uint64 size, uint64 pa, int perm);
|
||||
|
||||
// Who holds each device, or 0. Indexed the same way pci_get() is, because the
|
||||
|
|
@ -89,10 +91,122 @@ pciuser_irq_handler(void *arg)
|
|||
wakeup(&irqdev);
|
||||
}
|
||||
|
||||
// The pid holding a device, or 0. For kernel/dev/pci.c's `pci` view, which
|
||||
// otherwise prints "-" in the driver column for a device that is very much
|
||||
// being driven -- just not from in here.
|
||||
int
|
||||
pciuser_owner_pid(int idx)
|
||||
{
|
||||
int pid = 0;
|
||||
if (idx < 0 || idx >= NPCI)
|
||||
return 0;
|
||||
acquire(&pciuser_lock);
|
||||
if (owner[idx])
|
||||
pid = owner[idx]->pid;
|
||||
release(&pciuser_lock);
|
||||
return pid;
|
||||
}
|
||||
|
||||
// Who is holding what, and what memory they were given for it.
|
||||
//
|
||||
// The three things that go wrong with a userspace driver are all here: a
|
||||
// device claimed by a process that no longer exists, DMA handed out and never
|
||||
// returned, and an interrupt line left masked with nobody reading it. Each was
|
||||
// found the hard way, and each used to need a rebuild to see.
|
||||
static void
|
||||
pciuser_dump(struct sbuf *out, const char *arg)
|
||||
{
|
||||
(void)arg;
|
||||
int held = 0, blocks = 0;
|
||||
uint64 bytes = 0;
|
||||
|
||||
acquire(&pciuser_lock);
|
||||
|
||||
sb_puts(out, "claims\n");
|
||||
for (int i = 0; i < NPCI && i < pci_count(); i++) {
|
||||
const struct pci_dev *d;
|
||||
if (!owner[i])
|
||||
continue;
|
||||
held++;
|
||||
d = pci_get(i);
|
||||
sb_puts(out, " index ");
|
||||
sb_putu_pad(out, i, 2);
|
||||
sb_puts(out, " ");
|
||||
if (d) {
|
||||
sb_puthex(out, d->bus, 2); sb_putc(out, ':');
|
||||
sb_puthex(out, d->slot, 2); sb_putc(out, '.');
|
||||
sb_puthex(out, d->func, 1);
|
||||
sb_puts(out, " ");
|
||||
sb_puthex4(out, d->vendor); sb_putc(out, ':');
|
||||
sb_puthex4(out, d->device);
|
||||
}
|
||||
sb_puts(out, " held by pid ");
|
||||
sb_putu(out, owner[i]->pid);
|
||||
sb_puts(out, " (");
|
||||
sb_puts(out, owner[i]->name);
|
||||
sb_puts(out, ")\n");
|
||||
}
|
||||
if (!held)
|
||||
sb_puts(out, " (nothing is claimed)\n");
|
||||
|
||||
sb_puts(out, "\ndma handed out\n");
|
||||
for (int i = 0; i < PCIUSER_MAXDMA; i++) {
|
||||
if (!dmablk[i].proc)
|
||||
continue;
|
||||
blocks++;
|
||||
bytes += dmablk[i].size;
|
||||
sb_puts(out, " slot ");
|
||||
sb_putu_pad(out, i, 2);
|
||||
sb_puts(out, " 0x");
|
||||
sb_puthex(out, (addr_t)dmablk[i].kva, 16);
|
||||
sb_puts(out, " phys 0x");
|
||||
sb_puthex(out, dma_phys(dmablk[i].kva), 16);
|
||||
sb_putu_pad(out, dmablk[i].size, 10);
|
||||
sb_puts(out, " pid ");
|
||||
sb_putu(out, dmablk[i].proc->pid);
|
||||
sb_puts(out, " (");
|
||||
sb_puts(out, dmablk[i].proc->name);
|
||||
sb_puts(out, ")\n");
|
||||
}
|
||||
if (!blocks)
|
||||
sb_puts(out, " (none)\n");
|
||||
else {
|
||||
sb_puts(out, " ");
|
||||
sb_putu(out, blocks);
|
||||
sb_puts(out, " block(s), ");
|
||||
sb_putu(out, bytes / 1024);
|
||||
sb_puts(out, " KB of ");
|
||||
sb_putu(out, PCIUSER_MAXDMA);
|
||||
sb_puts(out, " slots\n");
|
||||
}
|
||||
|
||||
sb_puts(out, "\ninterrupts\n");
|
||||
if (!irqdev.proc) {
|
||||
sb_puts(out, " (no line is delivered to userspace)\n");
|
||||
} else {
|
||||
sb_puts(out, " irq ");
|
||||
sb_putu(out, irqdev.irq);
|
||||
sb_puts(out, " pid ");
|
||||
sb_putu(out, irqdev.proc->pid);
|
||||
sb_puts(out, " taken ");
|
||||
sb_putu(out, irqdev.count);
|
||||
sb_puts(out, ", seen ");
|
||||
sb_putu(out, irqdev.seen);
|
||||
sb_puts(out, irqdev.masked ? ", MASKED" : ", unmasked");
|
||||
if (irqdev.count != irqdev.seen)
|
||||
sb_puts(out, " -- the driver is behind");
|
||||
sb_putc(out, '\n');
|
||||
}
|
||||
|
||||
release(&pciuser_lock);
|
||||
}
|
||||
|
||||
void
|
||||
pciuser_init(void)
|
||||
{
|
||||
initlock(&pciuser_lock, "pciuser");
|
||||
dbg_register("claims", pciuser_dump,
|
||||
"devices, DMA and interrupts held by userspace drivers");
|
||||
}
|
||||
|
||||
// Does `phys` fall inside a memory BAR of a device this process has claimed?
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#include "klog.h"
|
||||
#include "irq.h"
|
||||
#include "x86.h"
|
||||
#include "device.h"
|
||||
|
|
@ -34,77 +35,14 @@
|
|||
#include "errno.h"
|
||||
#include "fsstat.h"
|
||||
#include "schfs.h"
|
||||
#include "sbuf.h"
|
||||
#include "dbg.h"
|
||||
|
||||
#define PROC_BUFSZ 2048
|
||||
|
||||
// --- tiny formatting helpers (no snprintf in the kernel) -------------------
|
||||
|
||||
struct sbuf {
|
||||
char *p;
|
||||
int len;
|
||||
int cap;
|
||||
};
|
||||
|
||||
static void
|
||||
sb_putc(struct sbuf *sb, char c)
|
||||
{
|
||||
if (sb->len < sb->cap - 1)
|
||||
sb->p[sb->len++] = c;
|
||||
}
|
||||
|
||||
static void
|
||||
sb_puts(struct sbuf *sb, const char *s)
|
||||
{
|
||||
while (*s)
|
||||
sb_putc(sb, *s++);
|
||||
}
|
||||
|
||||
// A string in a fixed-width column, space-padded. cprintf has no width
|
||||
// specifier and neither does this buffer, so tables build their own columns.
|
||||
// Over-long values are allowed to run past their column rather than be
|
||||
// truncated: a misaligned row is easier to read than a mangled name.
|
||||
static void
|
||||
sb_pad(struct sbuf *sb, const char *s, int width)
|
||||
{
|
||||
int n = 0;
|
||||
while (s[n]) {
|
||||
sb_putc(sb, s[n]);
|
||||
n++;
|
||||
}
|
||||
while (n++ < width)
|
||||
sb_putc(sb, ' ');
|
||||
}
|
||||
|
||||
// Four lowercase hex digits, zero-padded. PCI and USB identifiers are always
|
||||
// written this way, and a lookup table keyed on "8086:100e" does not match
|
||||
// "8086:100E" or "8086:10e".
|
||||
static void
|
||||
sb_puthex4(struct sbuf *sb, uint32 v)
|
||||
{
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (int i = 3; i >= 0; i--)
|
||||
sb_putc(sb, hex[(v >> (i * 4)) & 0xF]);
|
||||
}
|
||||
|
||||
static void
|
||||
sb_putu(struct sbuf *sb, uint64 v)
|
||||
{
|
||||
char tmp[24];
|
||||
int i = 0;
|
||||
if (v == 0) { sb_putc(sb, '0'); return; }
|
||||
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
|
||||
while (i--) sb_putc(sb, tmp[i]);
|
||||
}
|
||||
|
||||
// "<label><value> <unit>\n" — the shape almost every /proc line takes.
|
||||
static void
|
||||
sb_kv(struct sbuf *sb, const char *label, uint64 v, const char *unit)
|
||||
{
|
||||
sb_puts(sb, label);
|
||||
sb_putu(sb, v);
|
||||
if (unit && *unit) { sb_putc(sb, ' '); sb_puts(sb, unit); }
|
||||
sb_putc(sb, '\n');
|
||||
}
|
||||
// The formatting helpers these files are built out of now live in
|
||||
// include/sbuf.h, because the debugger's introspection registry needs exactly
|
||||
// the same thing and two copies would have drifted. See docs/plan/debugger.md.
|
||||
|
||||
// --- the files -------------------------------------------------------------
|
||||
|
||||
|
|
@ -403,6 +341,7 @@ gen_filesystems(struct sbuf *sb)
|
|||
#define PROC_DEVICES 13
|
||||
#define PROC_FSSTAT 14
|
||||
#define PROC_NETIF 15
|
||||
#define PROC_KMSG 17
|
||||
|
||||
// Render global file `id` (a PROC_* constant) into `buf`. Returns its length.
|
||||
//
|
||||
|
|
@ -446,6 +385,13 @@ proc_gen_global(int id, char *buf, int cap)
|
|||
int
|
||||
procread(struct inode *ip, uint64 off, char *dst, int n)
|
||||
{
|
||||
// The log is served straight out of its ring rather than regenerated into a
|
||||
// scratch buffer like everything else here: it is 128 KB, and this function
|
||||
// renders the whole file on every read into an array on a 4 KB kernel stack.
|
||||
// See klog_read().
|
||||
if (ip->minor == PROC_KMSG)
|
||||
return klog_read(off, dst, n);
|
||||
|
||||
char buf[PROC_BUFSZ];
|
||||
int len = proc_gen_global(ip->minor, buf, PROC_BUFSZ);
|
||||
|
||||
|
|
@ -502,6 +448,16 @@ proc_sysrq_cmd(char cmd)
|
|||
cprintf("sysrq: pid 1 asked to exit -- it should come back\n");
|
||||
break;
|
||||
|
||||
case 'g':
|
||||
// Every registered view, straight to the console.
|
||||
//
|
||||
// This is docs/plan/debugger.md's level 0: no process, no filesystem, no
|
||||
// client program. If /dev/dbg cannot be opened -- because the filesystem
|
||||
// is what is broken -- this still works, and on a machine with only a
|
||||
// serial line it is the whole debugger.
|
||||
dbg_dump_all();
|
||||
break;
|
||||
|
||||
case 'w': {
|
||||
// Wedge the supervisor: make schinitd hang, on purpose, so PID 1's
|
||||
// heartbeat timeout is exercised instead of reasoned about. There is no
|
||||
|
|
@ -518,6 +474,7 @@ proc_sysrq_cmd(char cmd)
|
|||
cprintf("sysrq: unknown command '%c' "
|
||||
"(known: c = crash, u = dump USB, s = read the USB disk, "
|
||||
"d = display self-test, t = text self-test, n = net self-test, "
|
||||
"g = dump every debug view, "
|
||||
"w = wedge the init supervisor, i = kill pid 1)\n",
|
||||
cmd);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ static const struct devnode devnodes[] = {
|
|||
{ "stderr", 1, 1, D_ROOT },
|
||||
{ "fb0", 2, 0, D_ROOT },
|
||||
{ "pci", 9, 0, D_ROOT },
|
||||
{ "dbg", 10, 0, D_ROOT },
|
||||
{ "mouse", 3, 1, D_ROOT },
|
||||
|
||||
// Raw disks. Created whether or not a drive is present: opening one with no
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
#include "types.h"
|
||||
#include "defs.h"
|
||||
#include "klog.h"
|
||||
#include "param.h"
|
||||
#include "memlayout.h"
|
||||
#include "mmu.h"
|
||||
|
|
@ -93,6 +94,7 @@ static const struct global_file globals[] = {
|
|||
{ "devices", 13 },
|
||||
{ "fsstat", 14 },
|
||||
{ "netif", 15 },
|
||||
{ "kmsg", 17 },
|
||||
};
|
||||
#define NGLOBALS ((int)(sizeof(globals) / sizeof(globals[0])))
|
||||
|
||||
|
|
@ -304,6 +306,32 @@ procfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
|
|||
//
|
||||
// A whole page for a 2 KB buffer is not wasteful here -- kalloc's unit is a
|
||||
// page either way, and this is not a hot path.
|
||||
// /proc/kmsg is 128 KB and comes out of a ring, so it does not go through
|
||||
// the render-the-whole-file-then-slice path below. See klog_read().
|
||||
if (INO_IS_GLOBAL(ip->inum) && (int)(ip->inum & 0xFF) == 17) {
|
||||
char *k;
|
||||
int got;
|
||||
|
||||
if (n > PGSIZE)
|
||||
n = PGSIZE;
|
||||
if ((k = kalloc()) == 0)
|
||||
return -ENOMEM;
|
||||
got = klog_read(off, k, (int)n);
|
||||
if (got <= 0) {
|
||||
kfree(k);
|
||||
return 0;
|
||||
}
|
||||
r = got;
|
||||
if (user_dst) {
|
||||
if (copy_to_user(dst, k, got) < 0)
|
||||
r = -EFAULT;
|
||||
} else {
|
||||
memmove((char *)dst, k, got);
|
||||
}
|
||||
kfree(k);
|
||||
return r;
|
||||
}
|
||||
|
||||
if ((buf = kalloc()) == 0)
|
||||
return -ENOMEM;
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,25 @@ SECTIONS
|
|||
*(.ldata .ldata.*)
|
||||
}
|
||||
|
||||
/* The kernel symbol table, generated by tools/ksyms.pl between the two link
|
||||
* passes. See kernel/core/ksym.c.
|
||||
*
|
||||
* The position is the whole trick and it is not cosmetic: everything the
|
||||
* table records is a *text* address, and .ksym sits after .text and .rodata,
|
||||
* so adding several tens of kilobytes here cannot move a single entry in it.
|
||||
* That is what makes two passes enough -- pass one is read to build the
|
||||
* table, pass two carries it, and pass two's text is identical to pass one's.
|
||||
* Put this before .text and the table describes a kernel that no longer
|
||||
* exists, subtly, with every address off by the size of the table.
|
||||
*
|
||||
* It is inside the loaded image (before edata), not in .bss, because it has
|
||||
* contents. mboot_load_end is computed from edata and would otherwise not
|
||||
* cover it. */
|
||||
. = ALIGN(8);
|
||||
.ksym : {
|
||||
*(.ksym .ksym.*)
|
||||
}
|
||||
|
||||
. = ALIGN(0x1000);
|
||||
|
||||
PROVIDE(edata = .);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@
|
|||
|
||||
#define _KERNEL_SOCKET_INTERNALS
|
||||
#include "socket.h"
|
||||
#include "sbuf.h"
|
||||
#include "dbg.h"
|
||||
|
||||
static struct {
|
||||
struct spinlock lock;
|
||||
|
|
@ -49,10 +51,66 @@ static struct {
|
|||
// because sockclose is the one caller that cannot be moved after it.
|
||||
static void scm_drain(struct socket *s);
|
||||
|
||||
// The socket table.
|
||||
//
|
||||
// Two columns here are the ones worth having: `buf`, because a peer that has
|
||||
// stopped reading is invisible until its buffer is full and the writer blocks;
|
||||
// and `scm`, because a descriptor sent over a socket and never received holds
|
||||
// a kernel reference open, and there is no garbage collector for it -- see
|
||||
// "What it does not do" in docs/architecture/networking.md.
|
||||
static void
|
||||
sock_dump(struct sbuf *out, const char *arg)
|
||||
{
|
||||
(void)arg;
|
||||
struct socket *s;
|
||||
int used = 0;
|
||||
|
||||
sb_puts(out, " slot dom state ref buf peer scm name\n");
|
||||
|
||||
acquire(&stab.lock);
|
||||
for (s = stab.sock; s < stab.sock + NSOCK; s++) {
|
||||
if (s->state == SS_FREE && s->ref == 0)
|
||||
continue;
|
||||
used++;
|
||||
sb_putu_pad(out, (int)(s - stab.sock), 6);
|
||||
sb_puts(out, " ");
|
||||
sb_pad(out, s->domain == AF_UNIX ? "unix" :
|
||||
s->domain == AF_INET ? "inet" :
|
||||
s->domain == AF_NETLINK ? "nl" : "?", 7);
|
||||
sb_pad(out, s->state == SS_FREE ? "free" :
|
||||
s->state == SS_UNBOUND ? "unbound" :
|
||||
s->state == SS_BOUND ? "bound" :
|
||||
s->state == SS_LISTENING ? "listening" :
|
||||
s->state == SS_CONNECTED ? "connected" : "?", 11);
|
||||
sb_putu_pad(out, s->ref, 3);
|
||||
sb_putu_pad(out, s->nwrite - s->nread, 7);
|
||||
sb_puts(out, " ");
|
||||
if (s->peer)
|
||||
sb_putu_pad(out, (int)(s->peer - stab.sock), 4);
|
||||
else
|
||||
sb_puts(out, s->peer_closed ? "gone" : " -");
|
||||
sb_putu_pad(out, s->nscm, 5);
|
||||
sb_puts(out, " ");
|
||||
if (s->name[0])
|
||||
sb_puts(out, s->name);
|
||||
else if (s->nqueue)
|
||||
sb_putu(out, s->nqueue);
|
||||
sb_putc(out, '\n');
|
||||
}
|
||||
release(&stab.lock);
|
||||
|
||||
if (!used)
|
||||
sb_puts(out, " (no sockets)\n");
|
||||
sb_puts(out, "\n buf is bytes written and not yet read; scm is descriptors\n"
|
||||
" sent and not yet received, which nothing reclaims.\n");
|
||||
}
|
||||
|
||||
void
|
||||
sockinit(void)
|
||||
{
|
||||
initlock(&stab.lock, "socktab");
|
||||
dbg_register("sock", sock_dump,
|
||||
"the socket table, with buffered bytes and descriptors in flight");
|
||||
}
|
||||
|
||||
// Caller holds stab.lock.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
#include "proc.h"
|
||||
#include "spinlock.h"
|
||||
#include "sleeplock.h"
|
||||
#include "sbuf.h"
|
||||
#include "dbg.h"
|
||||
#include "fs.h"
|
||||
#include "file.h"
|
||||
#include "posix.h"
|
||||
|
|
@ -42,10 +44,129 @@ kstack_check_canary(const char *where)
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// --- what the debugger can be told about processes --------------------------
|
||||
//
|
||||
// docs/plan/debugger.md, D2. These are here rather than in kernel/dev/dbg.c
|
||||
// because this file owns ptable, and a view written anywhere else would need
|
||||
// the lock and the struct exported to get at it. One line at the bottom of the
|
||||
// file that owns the data is the whole discipline.
|
||||
|
||||
static const char *
|
||||
state_name(enum procstate s)
|
||||
{
|
||||
switch (s) {
|
||||
case UNUSED: return "unused";
|
||||
case EMBRYO: return "embryo";
|
||||
case SLEEPING: return "sleep";
|
||||
case RUNNABLE: return "ready";
|
||||
case RUNNING: return "run";
|
||||
case ZOMBIE: return "zombie";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
// "4" means only pid 4. Not a general query language -- just the one filter
|
||||
// that stops a table from being unreadable on a busy machine.
|
||||
static int
|
||||
want_pid(const char *arg)
|
||||
{
|
||||
int v = 0;
|
||||
if (!arg || !*arg)
|
||||
return 0; // no filter
|
||||
while (*arg >= '0' && *arg <= '9')
|
||||
v = v * 10 + (*arg++ - '0');
|
||||
return v;
|
||||
}
|
||||
|
||||
static void
|
||||
proc_dump(struct sbuf *out, const char *arg)
|
||||
{
|
||||
struct proc *p;
|
||||
int only = want_pid(arg);
|
||||
|
||||
sb_puts(out, " pid ppid pgid state sz(KB) mmap_next "
|
||||
"name\n");
|
||||
|
||||
acquire(&ptable.lock);
|
||||
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
|
||||
if (p->state == UNUSED)
|
||||
continue;
|
||||
if (only && p->pid != only)
|
||||
continue;
|
||||
sb_putu_pad(out, p->pid, 5);
|
||||
sb_putu_pad(out, p->parent ? p->parent->pid : 0, 6);
|
||||
sb_putu_pad(out, p->pgid, 6);
|
||||
sb_puts(out, " ");
|
||||
sb_pad(out, state_name(p->state), 8);
|
||||
sb_putu_pad(out, p->sz / 1024, 6);
|
||||
sb_puts(out, " 0x");
|
||||
sb_puthex(out, p->mmap_next, 16);
|
||||
sb_puts(out, " ");
|
||||
sb_puts(out, p->name);
|
||||
if (p->killed)
|
||||
sb_puts(out, " [killed]");
|
||||
if (p->wedged)
|
||||
sb_puts(out, " [wedged]");
|
||||
sb_putc(out, '\n');
|
||||
}
|
||||
release(&ptable.lock);
|
||||
}
|
||||
|
||||
// Every mapping every process holds that it does not own the pages of.
|
||||
//
|
||||
// This is the table that decides whether exit() frees a page or leaves it
|
||||
// alone -- see proc_release_shared() in kernel/sys/sys_mem.c -- and the bug
|
||||
// that made the debugger worth building was a DMA block that was never in it.
|
||||
// A view of it is the cheapest possible check that the rule is being kept.
|
||||
static void
|
||||
vma_dump(struct sbuf *out, const char *arg)
|
||||
{
|
||||
struct proc *p;
|
||||
int any = 0;
|
||||
int only = want_pid(arg);
|
||||
|
||||
sb_puts(out, " pid slot va bytes "
|
||||
"no_free shmid name\n");
|
||||
|
||||
acquire(&ptable.lock);
|
||||
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
|
||||
if (p->state == UNUSED)
|
||||
continue;
|
||||
if (only && p->pid != only)
|
||||
continue;
|
||||
for (int i = 0; i < NVMA; i++) {
|
||||
if (!p->vma[i].used)
|
||||
continue;
|
||||
any = 1;
|
||||
sb_putu_pad(out, p->pid, 5);
|
||||
sb_putu_pad(out, i, 6);
|
||||
sb_puts(out, " 0x");
|
||||
sb_puthex(out, p->vma[i].addr, 16);
|
||||
sb_putu_pad(out, p->vma[i].length, 12);
|
||||
sb_puts(out, p->vma[i].no_free ? " yes " : " no ");
|
||||
if (p->vma[i].shmid >= 0)
|
||||
sb_putu_pad(out, p->vma[i].shmid, 5);
|
||||
else
|
||||
sb_puts(out, " -");
|
||||
sb_puts(out, " ");
|
||||
sb_puts(out, p->name);
|
||||
sb_putc(out, '\n');
|
||||
}
|
||||
}
|
||||
release(&ptable.lock);
|
||||
|
||||
if (!any)
|
||||
sb_puts(out, only ? " (that pid holds no mappings)\n"
|
||||
: " (no process is holding a mapping)\n");
|
||||
}
|
||||
|
||||
void
|
||||
pinit(void)
|
||||
{
|
||||
initlock(&ptable.lock, "ptable");
|
||||
dbg_register("proc", proc_dump, "every process, with its state and size");
|
||||
dbg_register("vma", vma_dump, "mappings a process holds but does not own");
|
||||
}
|
||||
|
||||
static struct proc*
|
||||
|
|
|
|||
145
tools/ksyms.pl
Normal file
145
tools/ksyms.pl
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
#!/usr/bin/perl
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Turn `nm -n kernel` into a symbol table the kernel can carry with it.
|
||||
#
|
||||
# docs/plan/debugger.md milestone D0. Everything the debugger prints is an
|
||||
# address until this exists, and an address has to be resolved by hand against
|
||||
# a disassembly that may not be the one that was running.
|
||||
#
|
||||
# Perl and not Python, deliberately: this runs on every kernel link, so it is
|
||||
# on the critical path of a clean build, and tools/vectors.pl already makes
|
||||
# perl a requirement there. The Makefile comment above include/logo.h says why
|
||||
# that line is worth holding.
|
||||
#
|
||||
# Two modes:
|
||||
# (default) read nm output on stdin, write a C file on stdout
|
||||
# --list read nm output on stdin, write "addr name" lines -- used by the
|
||||
# Makefile to prove the two link passes agree
|
||||
#
|
||||
# The layout trick this depends on: .ksym is placed after .text and .rodata in
|
||||
# kernel/kernel.ld, so adding the table to the image cannot move any of the
|
||||
# text symbols that are *in* the table. Two passes, and the second is stable.
|
||||
# The Makefile verifies that rather than trusting it.
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
# hex() on a 64-bit kernel address is fine on any perl with 64-bit integers,
|
||||
# which is every perl this will meet; the warning is about 32-bit ones.
|
||||
no warnings q(portable);
|
||||
|
||||
my $list = (@ARGV && $ARGV[0] eq '--list');
|
||||
|
||||
my @sym; # [addr, name]
|
||||
my $textend = 0;
|
||||
|
||||
while (my $line = <STDIN>) {
|
||||
chomp $line;
|
||||
# "ffff800000100000 T begin". Symbols with no address print as spaces and
|
||||
# fall out here on the field count.
|
||||
my ($addr, $type, $name) = $line =~ /^([0-9a-fA-F]+)\s+(\S)\s+(\S+)$/
|
||||
or next;
|
||||
|
||||
# etext is the end of the text section, and it is the only non-function we
|
||||
# want: without it a lookup of an address past the last function walks off
|
||||
# the end of the table and names whatever was linked last.
|
||||
if ($name eq 'etext') {
|
||||
$textend = hex($addr);
|
||||
next;
|
||||
}
|
||||
|
||||
# t/T are text, w/W are weak text. Everything else is data, bss or absolute
|
||||
# and naming it would make a code address resolve to a variable.
|
||||
next unless $type =~ /^[tTwW]$/;
|
||||
|
||||
# Compiler-local labels. They have no meaning outside one translation unit
|
||||
# and they would shadow the function they sit inside.
|
||||
next if $name =~ /^\.L/;
|
||||
next if $name =~ /\$/;
|
||||
|
||||
push @sym, [hex($addr), $name, $type];
|
||||
}
|
||||
|
||||
die "ksyms: no text symbols on stdin\n" unless @sym;
|
||||
|
||||
# nm -n sorts numerically, but ties come out in whatever order the symbol table
|
||||
# had them. Prefer the global at a shared address: `begin` and `mboot_header`
|
||||
# are the same byte, and naming that byte `begin` tells you nothing.
|
||||
my @uniq;
|
||||
for my $s (@sym) {
|
||||
if (@uniq && $uniq[-1][0] == $s->[0]) {
|
||||
$uniq[-1] = $s if $s->[2] =~ /^[TW]$/ && $uniq[-1][2] =~ /^[tw]$/;
|
||||
next;
|
||||
}
|
||||
push @uniq, $s;
|
||||
}
|
||||
@sym = @uniq;
|
||||
|
||||
$textend = $sym[-1][0] + 0x1000 unless $textend;
|
||||
|
||||
if ($list) {
|
||||
printf "%016x %s\n", $_->[0], $_->[1] for @sym;
|
||||
exit 0;
|
||||
}
|
||||
|
||||
my $base = $sym[0][0];
|
||||
for my $s (@sym) {
|
||||
die sprintf("ksyms: symbol %s at %x is more than 4 GB above the base\n",
|
||||
$s->[1], $s->[0])
|
||||
if $s->[0] - $base > 0xffffffff;
|
||||
}
|
||||
|
||||
# Offsets into the name blob, computed here so the kernel does not have to
|
||||
# scan for NULs at boot.
|
||||
my @off;
|
||||
my $cur = 0;
|
||||
for my $s (@sym) {
|
||||
push @off, $cur;
|
||||
$cur += length($s->[1]) + 1;
|
||||
}
|
||||
|
||||
my $n = scalar @sym;
|
||||
|
||||
print <<"HDR";
|
||||
// Generated by tools/ksyms.pl from the first link pass. Do not edit.
|
||||
//
|
||||
// See docs/plan/debugger.md, milestone D0, and kernel/core/ksym.c for the
|
||||
// lookup that reads this. Everything lands in .ksym, which kernel/kernel.ld
|
||||
// places after .text and .rodata so that carrying the table cannot move the
|
||||
// addresses recorded in it.
|
||||
|
||||
#include "types.h"
|
||||
|
||||
#define KSYM_SECTION __attribute__((section(".ksym"), used))
|
||||
|
||||
KSYM_SECTION const uint32 ksym_count = ${n}u;
|
||||
KSYM_SECTION const uint64 ksym_base = 0x${\ sprintf("%x", $base) }ULL;
|
||||
KSYM_SECTION const uint64 ksym_textend = 0x${\ sprintf("%x", $textend) }ULL;
|
||||
|
||||
// Offsets from ksym_base rather than absolute addresses: the kernel spans
|
||||
// about a megabyte, so 32 bits is plenty and it halves the table.
|
||||
KSYM_SECTION const uint32 ksym_addr[${n}] = {
|
||||
HDR
|
||||
|
||||
for (my $i = 0; $i < $n; $i += 8) {
|
||||
my $end = $i + 7; $end = $n - 1 if $end > $n - 1;
|
||||
print " ", join(",", map { sprintf("0x%08x", $sym[$_][0] - $base) } ($i .. $end)), ",\n";
|
||||
}
|
||||
|
||||
print "};\n\nKSYM_SECTION const uint32 ksym_nameoff[${n}] = {\n";
|
||||
|
||||
for (my $i = 0; $i < $n; $i += 8) {
|
||||
my $end = $i + 7; $end = $n - 1 if $end > $n - 1;
|
||||
print " ", join(",", map { sprintf("%6d", $off[$_]) } ($i .. $end)), ",\n";
|
||||
}
|
||||
|
||||
print "};\n\nKSYM_SECTION const char ksym_names[] =\n";
|
||||
|
||||
for my $s (@sym) {
|
||||
my $name = $s->[1];
|
||||
$name =~ s/([\\"])/\\$1/g;
|
||||
print " \"$name\\0\"\n";
|
||||
}
|
||||
|
||||
print ";\n";
|
||||
Loading…
Reference in a new issue