schdbgd bridges TCP to the debugger, which is the one way in that does not
need you to be sitting at the machine. It is a bridge and nothing else: a line
in becomes a line written to /dev/dbgro and the answer comes back, so `nc` is a
complete client and there is no clever client to be broken on the day you need
this.
# schdbgd &
schdbgd: debugger on port 4444, loopback only
$ nc 127.0.0.1 4444
dma
arena 0xffff80003da41000-0xffff80003ea41000 16384 KB total, ...
A kernel debugger plus a TCP socket is a remote root hole, so three things were
decided rather than discovered:
- It does not start by itself. schdbgd.job ships Disabled. SchOS has no
kernel command line, so the boot flag the plan asked for is the job
manifest, which is the same thing in this system's own idiom.
- It binds loopback; -a names an address and says so in a paragraph.
- It cannot reach a view that changes state, enforced in the kernel.
That last one is /dev/dbgro: /dev/dbg on minor 1. The minor is the one piece of
per-open state a character device already sees, so which node you opened is the
permission -- nothing to latch, nothing to remember between the write and the
read, no way across. Which views are refused comes from dbg_register_rw() on
the registration itself, because a central list of dangerous views goes stale
in the direction where a new view is quietly treated as safe. Today that is
`watch <addr>`; `watch` with no argument is still the listing and still
answered.
Also: socket() now accepts IPPROTO_TCP with SOCK_STREAM and IPPROTO_UDP with
SOCK_DGRAM. They name the default rather than departing from it, and toybox's
nc writes them out -- so TCP was "Protocol not supported" on a system whose
whole network is TCP.
docs/plan/debugger.md retires into docs/architecture/debugging.md; 16 files
repointed. Every plan in docs/plan/ that was open is now closed, so the next
thing is Gate 0 itself: the survey, with the instrument built.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
182 lines
5.7 KiB
C
182 lines
5.7 KiB
C
#include "types.h"
|
|
#include "defs.h"
|
|
#include "irq.h"
|
|
#include "param.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "proc.h"
|
|
#include "x86.h"
|
|
#include "traps.h"
|
|
#include "spinlock.h"
|
|
#include "posix.h"
|
|
|
|
uint *idt;
|
|
extern addr_t vectors[];
|
|
struct spinlock tickslock;
|
|
uint ticks;
|
|
|
|
static void
|
|
mkgate(uint *idt, uint n, addr_t kva, uint pl)
|
|
{
|
|
uint64 addr = (uint64) kva;
|
|
|
|
n *= 4;
|
|
idt[n+0] = (addr & 0xFFFF) | (KERNEL_CS << 16);
|
|
idt[n+1] = (addr & 0xFFFF0000) | 0x8E00 | ((pl & 3) << 13);
|
|
idt[n+2] = addr >> 32;
|
|
idt[n+3] = 0;
|
|
}
|
|
|
|
void idtinit(void)
|
|
{
|
|
struct {
|
|
ushort limit;
|
|
uint64 base;
|
|
} __attribute__((packed)) idtdesc;
|
|
|
|
idtdesc.limit = PGSIZE - 1;
|
|
idtdesc.base = (uint64)idt;
|
|
|
|
asm volatile("lidt %0" : : "m" (idtdesc));
|
|
}
|
|
|
|
void tvinit(void)
|
|
{
|
|
int n;
|
|
idt = (uint*) kalloc();
|
|
memset(idt, 0, PGSIZE);
|
|
for (n = 0; n < 256; n++)
|
|
mkgate(idt, n, vectors[n], 0);
|
|
mkgate(idt, T_SYSCALL, vectors[T_SYSCALL], 3);
|
|
}
|
|
|
|
uint
|
|
uptime(void)
|
|
{
|
|
uint xticks;
|
|
|
|
acquire(&tickslock);
|
|
xticks = ticks;
|
|
release(&tickslock);
|
|
return xticks;
|
|
}
|
|
|
|
// Which signal a CPU fault corresponds to, so a process killed by a fault is
|
|
// reported to wait() as died-by-signal with a sensible cause rather than a
|
|
// blanket SIGKILL.
|
|
static int
|
|
trapsig(uint64 trapno)
|
|
{
|
|
switch(trapno){
|
|
case T_DIVIDE: return SIGFPE;
|
|
case T_ILLOP: return SIGILL;
|
|
case T_GPFLT: return SIGSEGV;
|
|
case T_PGFLT: return SIGSEGV;
|
|
case T_BRKPT: return SIGTRAP;
|
|
case T_DEBUG: return SIGTRAP;
|
|
default: return SIGSEGV;
|
|
}
|
|
}
|
|
|
|
void
|
|
trap(struct trapframe *tf)
|
|
{
|
|
kstack_check_canary("trap entry");
|
|
switch(tf->trapno){
|
|
case T_IRQ0 + IRQ_TIMER:
|
|
if(cpunum() == 0){
|
|
acquire(&tickslock);
|
|
ticks++;
|
|
wakeup(&ticks);
|
|
release(&tickslock);
|
|
}
|
|
// Debug registers are per-CPU, and there is no general IPI here, so this
|
|
// is how a watchpoint armed on one processor reaches the others: each
|
|
// notices the table's generation has moved and reloads. One comparison
|
|
// per tick when nothing has changed. See kernel/arch/x86_64/watch.c.
|
|
watch_sync();
|
|
lapiceoi();
|
|
break;
|
|
|
|
case T_DEBUG:
|
|
// A hardware watchpoint fired. It is a *trap*, not a fault -- the store
|
|
// has already happened -- so reporting and returning carries on with the
|
|
// program none the wiser, which is what lets this be used on a machine
|
|
// that is still running.
|
|
if(watch_trap(tf))
|
|
break;
|
|
// Not ours: a single-step, or a #DB from somewhere else. Falls through to
|
|
// the unexpected-trap path rather than being swallowed.
|
|
goto unexpected;
|
|
case T_IRQ0 + IRQ_SPURIOUS:
|
|
cprintf("cpu%d: spurious interrupt at %p:%p\n",
|
|
cpunum(), tf->cs, tf->rip);
|
|
lapiceoi();
|
|
break;
|
|
|
|
default:
|
|
// Every other IOAPIC pin goes through the registration table. The device
|
|
// interrupts used to be listed here one case at a time, which cannot work
|
|
// for anything found on the PCI bus: a driver does not know its line
|
|
// until it reads the device's config space at probe time, so there is no
|
|
// constant to write a case label for. See include/irq.h.
|
|
if(tf->trapno >= T_IRQ0 && tf->trapno < T_IRQ0 + NIRQ){
|
|
irq_dispatch(tf->trapno - T_IRQ0);
|
|
lapiceoi();
|
|
break;
|
|
}
|
|
|
|
unexpected:
|
|
if(myproc() == 0 || (tf->cs&3) == 0){
|
|
// 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/architecture/debugging.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) {
|
|
uint64 *iretframe = (uint64*)tf->rsp;
|
|
cprintf("iretq frame @ %p: rip=%p cs=%p rflags=%p rsp=%p ss=%p\n",
|
|
iretframe, iretframe[0], iretframe[1], iretframe[2],
|
|
iretframe[3], iretframe[4]);
|
|
if (myproc())
|
|
cprintf("this tf=%p myproc()->tf=%p myproc()->kstack=%p (tf-kstack=%p)\n",
|
|
tf, myproc()->tf, myproc()->kstack, (uint64)tf - (uint64)myproc()->kstack);
|
|
}
|
|
panic("trap");
|
|
}
|
|
cprintf("pid %d %s: trap %d err %d on cpu %d "
|
|
"rip 0x%p addr 0x%p--kill proc\n",
|
|
myproc()->pid, myproc()->name, tf->trapno, tf->err, cpunum(), tf->rip,
|
|
rcr2());
|
|
// General-purpose register dump. This used to be followed by ~25 lines
|
|
// that walked musl's mallocng group/meta structures out of rax/rdi and
|
|
// dumped the user stack -- scaffolding for the boot-crash investigation
|
|
// recorded in docs/history/kernel.md section 1. That
|
|
// investigation is closed and the underlying kernel bugs are fixed, so
|
|
// the allocator-specific part is gone; the registers stay, since they are
|
|
// useful for any fault.
|
|
if (tf->trapno == T_GPFLT) {
|
|
cprintf(" rax=%p rbx=%p rcx=%p rdx=%p rsi=%p rdi=%p rbp=%p rsp=%p\n",
|
|
tf->rax, tf->rbx, tf->rcx, tf->rdx, tf->rsi, tf->rdi, tf->rbp, tf->rsp);
|
|
}
|
|
myproc()->killed = 1;
|
|
if(myproc()->killsig == 0)
|
|
myproc()->killsig = trapsig(tf->trapno);
|
|
}
|
|
|
|
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
|
|
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
|
|
|
|
if(myproc() && myproc()->state == RUNNING && tf->trapno == T_IRQ0+IRQ_TIMER)
|
|
yield();
|
|
|
|
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
|
|
exit(W_EXITCODE(0, myproc()->killsig ? myproc()->killsig : SIGKILL));
|
|
|
|
kstack_check_canary("trap exit");
|
|
}
|