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
128 lines
3.8 KiB
C
128 lines
3.8 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// Naming an address.
|
|
//
|
|
// docs/architecture/debugging.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;
|
|
}
|