---
sidebar_position: 5.5
title: Symbols (yeet:sym)
---

# yeet:sym

`yeet:sym` inspects the symbols and debug information of a binary, the running kernel, or a live process. Use it to enumerate a symbol table, resolve a name to an address (or an address back to a name, file, and line), demangle C++/Rust names, and read DWARF type signatures for decoding function arguments.

```js
import { Inspector } from 'yeet:sym';

const insp = await Inspector.open('libssl.so');
const [hit] = await insp.find('SSL_read');
console.log(hit.name, hit.file_offset);          // SSL_read 261856

const frame = await insp.symbolize(hit.addr);
console.log(frame.name, frame.file, frame.line); // source location, if the binary has DWARF
await insp.close();
```

Parsing happens daemon-side, off the V8 thread, and every parse is cached and shared process-wide — a second `open()` of the same file (from any isolate) is served from memory. The heavy operations are asynchronous; only [`demangle`](#demangle) is synchronous.

## Importing

```js
import { Inspector, demangle } from 'yeet:sym';
```

| Export | Kind | Description |
|---|---|---|
| [`Inspector`](#inspector) | class | Opens a target and answers queries against it |
| [`demangle`](#demangle) | function | Pure, synchronous name demangling |

## Concepts

### Sources

An `Inspector` is opened over one of three sources, each with its own constructor:

| Source | Constructor | Enumerable? | Symbolize input | DWARF queries |
|---|---|---|---|---|
| ELF file | [`Inspector.open`](#inspector-open) | yes | virtual address or file offset | yes (with `debugSyms`) |
| Kernel | [`Inspector.kernel`](#inspector-kernel) | yes (kallsyms) | absolute address | no |
| Process | [`Inspector.process`](#inspector-process) | no (symbolize-only) | absolute (process-virtual) address | no |

### Target resolution

The `path` passed to `Inspector.open` is resolved daemon-side, the same way a uprobe attach target is, so a bare soname lands on the real file:

- An **absolute path** is used as-is (it must exist).
- A **bare name** such as `libssl.so` is resolved through `/etc/ld.so.cache` — the loader's own cache, which already accounts for multiarch directories and per-distro layout. A name matches exactly or as a versioned form (`libssl.so` → `libssl.so.3`).
- The run's `-M` / mapping overrides are applied first, so a mapped name lands on the mapped file.

Non-soname targets (`.a` archives, JIT-dumped images, executables outside the cache) must be passed as absolute paths.

### Debug symbols and separate debug info

By default an inspector reads only the ELF symbol table (`.symtab` / `.dynsym`) — fast, and enough for name↔address work. Pass `debugSyms: true` to additionally parse DWARF, which is required for source-level [`symbolize`](#inspector-symbolize) (file:line and inlined frames) and for [`signature`](#inspector-signature) / [`expandType`](#inspector-expandtype).

When the target is stripped of its own `.debug_*` sections, DWARF queries follow a **separate debug file** the way GDB does:

- by build-id — `.note.gnu.build-id` → `/usr/lib/debug/.build-id/ab/cdef….debug`
- by name — `.gnu_debuglink` → a sibling file, a `.debug/` subdirectory, or under `/usr/lib/debug`

:::note
`symbolize` (via blazesym) always follows separate debug files, but `signature` / `expandType` read DWARF directly. Networked lookup (debuginfod) and `.dwo`/split-DWARF are **not** followed — on a binary whose debug info is only reachable that way, `symbolize` may return file:line while `signature` returns `null`.
:::

### Addresses and BigInt

Kernel and process addresses routinely exceed `2^53`, so numeric fields (a record's `addr` / `size` / `file_offset`, and the arguments to `symbolize`) are **`number | bigint`**: values that fit a JS number arrive as one, larger values arrive as `BigInt`. Kernel addresses are effectively always `BigInt`.

```js
const kernel = await Inspector.kernel();
const [vfs] = await kernel.find('vfs_read');
await kernel.symbolize(vfs.addr + 4n);   // use BigInt arithmetic on kernel addresses
```

`JSON.stringify` throws on `BigInt`; use a replacer if you serialize records.

### Address kinds

For an **ELF** inspector, [`symbolize`](#inspector-symbolize) / [`symbolizeMany`](#inspector-symbolize-many) interpret their input per an `input` option:

| `input` | Meaning | Source of the value |
|---|---|---|
| `"virt"` (default) | link-time virtual address | a record's `addr` |
| `"fileOffset"` | raw file offset | a record's `file_offset` (the value a uprobe attaches at) |

Kernel and process inspectors take **absolute runtime addresses** and ignore `input`.

### Caching and lifetime

Each open source is parsed once and mounted on the calling isolate under an opaque handle. The parsed table (and any DWARF / symbolizer built for it) is shared with every other live holder of the same source and freed when the last one is dropped.

Always [`close()`](#inspector-close) an inspector when done to free the daemon-side parse promptly. As a safety net a `FinalizationRegistry` releases it at GC, but GC timing is not guaranteed.

---

## <Fn /> demangle {#demangle}

```ts
demangle(name: string): string | null
```

Demangles a raw ELF symbol name to a readable form, or returns `null` if it is not a recognized mangled name (an already-plain name, a data symbol). Pure and synchronous — no I/O, runs in-isolate.

Rust is tried first (legacy `_ZN…17h<hash>E` and v0 `_R…`, with the trailing hash dropped), then Itanium C++.

```js
import { demangle } from 'yeet:sym';

demangle('_ZN4core3fmt5write17h05af8562b5e29e21E'); // "core::fmt::write"
demangle('_Z3fooi');                                // "foo(int)"
demangle('SSL_read');                               // null
```

---

## <Obj /> Inspector {#inspector}

An opened target. Construct one with a static method — the constructor is private. All instance methods are asynchronous except where noted, and reject with a [`{ code, message }`](#errors) object.

### <Fn /> Inspector.open {#inspector-open}

```ts
static open(
  path: string,
  opts?: { debugSyms?: boolean; onProgress?: (loaded: number) => void }
): Promise<Inspector>
```

Opens an ELF file (executable or shared object). `path` is [resolved daemon-side](#target-resolution). `debugSyms` additionally parses DWARF (far slower — only worth it for source-level `symbolize` or `signature`). `onProgress` is called with the running symbol count while the table loads, and once more with the final count.

```js
const insp = await Inspector.open('/usr/lib/x86_64-linux-gnu/libc.so.6', {
  debugSyms: false,
  onProgress: n => process_ui(`${n} symbols…`),
});
```

### <Fn /> Inspector.kernel {#inspector-kernel}

```ts
static kernel(opts?: { onProgress?: (loaded: number) => void }): Promise<Inspector>
```

Opens the running kernel's symbol table from `/proc/kallsyms` — the core kernel, loaded modules, and JITed BPF programs. [`symbolize`](#inspector-symbolize) / [`symbolizeMany`](#inspector-symbolize-many) take absolute kernel addresses (a sampled PC, a stack frame). DWARF queries are unavailable.

```js
const kernel = await Inspector.kernel();
```

:::note
Reading kallsyms addresses requires root or `kptr_restrict=0`; otherwise every address reads as zero and `kernel()` rejects with a `SYM_FAILED` error explaining the restriction. Modules loaded after the table is parsed appear only once a fresh `kernel()` re-parses.
:::

### <Fn /> Inspector.process {#inspector-process}

```ts
static process(pid: number, opts?: { debugSyms?: boolean }): Promise<Inspector>
```

Opens a live process for **symbolization only**. [`symbolize`](#inspector-symbolize) / [`symbolizeMany`](#inspector-symbolize-many) take virtual addresses as the process sees them; the daemon routes each through `/proc/<pid>/maps`, so ASLR, every mapped object (deleted binaries included, via `map_files`), the vDSO, and perf-map JIT symbols are handled without folding offsets by hand. `debugSyms` resolves file:line and inline frames through each object's debug info.

[`symbols`](#inspector-symbols) and [`find`](#inspector-find) see an **empty** table (there is no single enumerable table across every mapped object — enumerate a specific object with `Inspector.open`), and DWARF queries reject with `SYM_FAILED`.

```js
const proc = await Inspector.process(1234, { debugSyms: true });
const frame = await proc.symbolize(0x7f9c0a12b4f0n); // an address seen in that process
```

---

### <Method /> symbols {#inspector-symbols}

```ts
symbols(opts?: { pageSize?: number }): AsyncIterableIterator<SymbolRecord>
```

Every symbol in the table, as an async iterator so an enumerate-all never crosses the wire as one oversized frame. The daemon byte-caps each page, so a page may be shorter than `pageSize` (default `512`). Yields [`SymbolRecord`](#symbolrecord) objects.

```js
for await (const s of insp.symbols()) {
  if (s.kind === 'function') console.log(s.demangled ?? s.name);
}

// or collect all
const all = await Array.fromAsync(insp.symbols());
```

### <Method /> find {#inspector-find}

```ts
find(query: string | RegExp, opts?: { exact?: boolean }): Promise<SymbolRecord[]>
```

All symbols matching `query` (see [Query matching](#query-matching)), collected into an array. Convenient for queries whose result set fits comfortably in memory; for a broad match, prefer [`findEach`](#inspector-find-each) and stop early.

```js
const reads = await insp.find(/read/i);           // regex fan-out
const [malloc] = await insp.find('malloc');       // base-path match
const exact = await insp.find('Foo::bar(int)', { exact: true });
```

### <Method /> findEach {#inspector-find-each}

```ts
findEach(query: string | RegExp, opts?: { exact?: boolean }): AsyncIterableIterator<SymbolRecord>
```

Like [`find`](#inspector-find), but yields matches one at a time so a broad match never buffers the whole result set (paged internally, one page in memory at a time).

```js
for await (const s of insp.findEach(/^SSL_/)) {
  if (looksInteresting(s)) { attachTo(s); break; }
}
```

### <Method /> mangle {#inspector-mangle}

```ts
mangle(query: string | RegExp, opts?: { exact?: boolean }): Promise<string>
```

Resolves `query` to the single raw (mangled) symbol name it names — the inverse of `demangle`, useful for pinning a probe to the exact linkage name. Rejects with [`AMBIGUOUS_SYMBOL`](#errors) (naming the leading candidates) when more than one symbol matches, or [`SYMBOL_NOT_FOUND`](#errors) when none does.

```js
await insp.mangle('core::fmt::write'); // "_ZN4core3fmt5write17h….E"
```

### <Method /> symbolize {#inspector-symbolize}

```ts
symbolize(
  addr: number | bigint,
  opts?: { input?: 'virt' | 'fileOffset' }
): Promise<SymFrame | null>
```

Resolves the symbol containing `addr`, or `null` if it lands in no known symbol. See [Address kinds](#address-kinds) for `input` (ELF only; kernel/process take absolute addresses). With `debugSyms` the result carries file:line and the inlined-frame chain. Returns a [`SymFrame`](#symframe).

```js
const f = await insp.symbolize(record.addr);                     // ELF virtual (default)
const g = await insp.symbolize(record.file_offset, { input: 'fileOffset' });
const k = await (await Inspector.kernel()).symbolize(0xffffffff81abc000n);
```

### <Method /> symbolizeMany {#inspector-symbolize-many}

```ts
symbolizeMany(
  addrs: (number | bigint)[],
  opts?: { input?: 'virt' | 'fileOffset' }
): Promise<(SymFrame | null)[]>
```

Batch form of [`symbolize`](#inspector-symbolize), resolved in one round-trip through a persistent symbolizer — far cheaper than one call per address when resolving many frames (a whole flame graph, a stack). The result is aligned with `addrs`; an entry is `null` where nothing resolved. Same `input` semantics as `symbolize`.

```js
const frames = await insp.symbolizeMany(stackIps);                 // kernel/process: absolute
const defs = await insp.symbolizeMany(offsets, { input: 'fileOffset' });
```

### <Method /> signature {#inspector-signature}

```ts
signature(symbol: string, opts?: { addr?: number | bigint }): Promise<FunctionSig | null>
```

The DWARF signature of a function — parameter names and resolved types, plus the return type. Requires `debugSyms: true` on open. Returns `null` when the binary has no debug entry for the symbol (stripped, or not a defined subprogram). Returns a [`FunctionSig`](#functionsig).

`symbol` matches any name the binary records the function under — a raw ELF name (`SSL_read`), a mangled linkage name, or a readable demangled form (`Foo::bar` or `Foo::bar(int)`) — so no hand-mangling is needed. A C++ method's implicit `this` parameter is omitted.

When a base path like `Foo::bar` names several overloads, or a `static` C name repeats across compilation units, pass `addr` (a record's `addr`) to pin the exact one by its entry address; otherwise a defining entry is preferred over a bare declaration, falling back to the first in unit order.

```js
const insp = await Inspector.open('/path/to/prog', { debugSyms: true });
const sig = await insp.signature('process');
// { name: "process", params: [{ name, type }, …], ret: { kind, … } }

// disambiguate an overload by address
const [rec] = await insp.find('Renderer::draw');
const exact = await insp.signature('Renderer::draw', { addr: rec.addr });
```

### <Method /> expandType {#inspector-expandtype}

```ts
expandType(key: string): Promise<Type | null>
```

Re-expands a truncated type branch by the `key` handed out in a [`signature`](#inspector-signature) result. Aggregates ([struct](#type) / [union](#type)) expand only a few levels deep before returning a stub carrying a `key`; call `expandType(key)` to drill further into that branch on demand. This keeps self-referential and giant types bounded on the wire.

```js
const sig = await insp.signature('handle_request');
const arg0 = sig.params[0].type;                // e.g. a pointer to a struct
if (arg0.kind === 'pointer' && arg0.pointee.key) {
  const full = await insp.expandType(arg0.pointee.key);
}
```

:::note
A `key` is tied to the parse that produced it. One persisted across a `close()` and a re-`open()` of a **recompiled** binary is rejected with [`STALE_TYPE_KEY`](#errors) rather than decoding the wrong type — re-run `signature` to get fresh keys.
:::

### <Method /> close {#inspector-close}

```ts
close(): Promise<void>
```

Frees the daemon-side parse now rather than at GC. Idempotent; cancels the finalizer so the parse is never freed twice. Calling any other method after `close()` throws.

```js
try {
  const insp = await Inspector.open('libssl.so');
  // …
} finally {
  await insp.close();
}
```

---

## Query matching {#query-matching}

`find`, `findEach`, and `mangle` accept a `string` or a `RegExp`. The matching mode follows the argument type and the `exact` option:

| Query | Mode | Matches |
|---|---|---|
| `"malloc"` | base path (default) | the demangled **base path** — signature and hash stripped — or the raw name |
| `"Foo::bar(int)"` with `{ exact: true }` | exact | the **full** demangled string, or the raw name, verbatim |
| `/re/flags` | regex | the pattern against the demangled name and the raw name |

The **base path** is the callable path with the C++ argument signature removed: `Foo::bar(int, char const*)` → `Foo::bar`. A Rust name is already hash- and signature-free, so it is unchanged. Base matching normalizes both sides, so `find("Foo::bar")` and `find("Foo::bar(int)")` both match the symbol `Foo::bar(int)` — and match **all** its overloads (that is why `mangle` can report `AMBIGUOUS_SYMBOL`).

RegExp flags `i`, `m`, and `s` are honored; `g` / `y` / `u` / `d` are meaningless for a membership test and ignored.

```js
await insp.find('Foo::bar');                    // every Foo::bar overload
await insp.find('Foo::bar(int)', { exact: true }); // only that overload
await insp.find(/^napi_/);                       // all N-API entry points
```

---

## Data types

### <Interface /> SymbolRecord {#symbolrecord}

A resolved symbol from a table, as yielded by [`symbols`](#inspector-symbols) and [`find`](#inspector-find).

```ts
interface SymbolRecord {
  name: string;                 // raw (possibly mangled) ELF name
  demangled: string | null;     // readable form, or null if not mangled
  alias_of: string | null;      // preferred name at this address, if this is an alias
  addr: number | bigint;        // symbol virtual address
  size: number | bigint | null; // size in bytes, if known
  file_offset: number | bigint | null; // offset within the object (the uprobe-attach value)
  kind: SymKind;
}
```

`alias_of` names the preferred symbol at the same address when this record is an alias of it (e.g. `__libc_malloc` aliasing `malloc`). Kallsyms records have `file_offset: null`. See [Addresses and BigInt](#addresses-and-bigint) for the numeric fields.

### <Interface /> SymKind {#symkind}

```ts
type SymKind = 'function' | 'variable' | 'unknown';
```

Text symbols are `function`; data/bss/rodata are `variable`; anything else (absolute, undefined, ELF markers) is `unknown`.

### <Interface /> SymFrame {#symframe}

A resolved address, as returned by [`symbolize`](#inspector-symbolize) / [`symbolizeMany`](#inspector-symbolize-many).

```ts
interface SymFrame {
  name: string;              // enclosing function (raw)
  demangled: string | null;
  addr: number | bigint;     // symbol start address
  offset: number | bigint;   // offset of the queried address into the symbol
  dir: string | null;        // compilation directory (DWARF only)
  file: string | null;       // source file as the compiler recorded it (DWARF only)
  line: number | null;       // source line (DWARF only)
  inlined: InlineFrame[];    // inlined frames collapsed here, innermost first
}
```

`dir` / `file` / `line` and `inlined` are populated only when the source was opened with `debugSyms` and carries DWARF.

### <Interface /> InlineFrame {#inlineframe}

```ts
interface InlineFrame {
  name: string;
  demangled: string | null;
  dir: string | null;
  file: string | null;
  line: number | null;
}
```

### <Interface /> FunctionSig {#functionsig}

Returned by [`signature`](#inspector-signature).

```ts
interface FunctionSig {
  name: string;
  params: { name: string; type: Type }[];
  ret: Type;
}
```

### <Interface /> Type {#type}

A resolved C/C++ type — a tagged union discriminated by `kind`. Appears in [`FunctionSig`](#functionsig) and [`expandType`](#inspector-expandtype).

```ts
type Type =
  | { kind: 'base'; name: string; size: number; enc: Encoding }
  | { kind: 'pointer'; pointee: Type }
  | { kind: 'struct'; name: string | null; size: number; members: Member[]; truncated: boolean; key: string | null }
  | { kind: 'union';  name: string | null; size: number; members: Member[]; truncated: boolean; key: string | null }
  | { kind: 'array';  elem: Type; count: number | null }
  | { kind: 'enum';   name: string | null; size: number; values: { name: string; value: number }[] }
  | { kind: 'void' }
  | { kind: 'unknown'; tag: string };

type Encoding = 'int' | 'uint' | 'float' | 'bool' | 'char' | 'uchar';
```

- **`base`** — a scalar. `enc` classifies it; `size` is in bytes.
- **`pointer`** — the pointee's leaf shape is always resolved (a `char *` never degrades to `void *` deep in a struct), but an aggregate pointee may itself be `truncated` with a `key`.
- **`struct`** / **`union`** — `members` carries byte offsets. When `truncated` is `true` the members were cut at the depth limit (or a cycle); pass `key` to [`expandType`](#inspector-expandtype) to expand that branch.
- **`array`** — `count` is the element count when the bound is known.
- **`enum`** — `values` lists the enumerators.
- **`unknown`** — a DIE tag not modeled here; `tag` is its DWARF tag name.

### <Interface /> Member {#member}

A field of a [struct](#type) or [union](#type).

```ts
interface Member {
  name: string;
  offset: number;          // byte offset within the aggregate
  type: Type;
  bit_offset?: number;     // present for bitfields
  bit_size?: number;       // present for bitfields
}
```

The `offset` is exactly the `OFF(n)` a struct-walking argument decoder adds to the base pointer.

---

## Errors {#errors}

Every asynchronous method rejects with a plain object — **not** an `Error` — of the shape `{ code, message }`. Match on `code`, not on the string:

```js
try {
  await insp.mangle('Vec::push');
} catch (e) {
  if (e.code === 'AMBIGUOUS_SYMBOL') console.log(e.message);
}
```

| `code` | Raised when |
|---|---|
| `INVALID_ARGS` | An argument is missing or the wrong type (bad `pid`, non-string `path`, bad `input`) |
| `TARGET_NOT_FOUND` | `path` could not be resolved to an on-disk file |
| `SYMBOL_NOT_FOUND` | `mangle` matched no symbol |
| `AMBIGUOUS_SYMBOL` | `mangle` matched more than one symbol (message names the leading candidates) |
| `STALE_TYPE_KEY` | `expandType` got a `key` from a superseded parse — re-query `signature` |
| `INSPECTOR_NOT_FOUND` | The handle is unknown (used after `close()`, or the isolate was torn down) |
| `SYM_FAILED` | Parse or query failure (e.g. kallsyms restricted, DWARF unavailable for a kernel/process source, malformed input) |
| `MOUNT_FAILED` | The daemon could not mount the parse on the isolate |
| `SERIALIZE_FAILED` | A result could not be serialized to the isolate |

Using an inspector after [`close()`](#inspector-close) throws synchronously (`"Inspector used after close()"`).

---

## Related: uprobe symbol matching

The same matching engine backs uprobe attach in [`yeet:bpf`](./ebpf). A uprobe `attach` spec accepts a `symbol` and a `match` mode:

| `match` | Behavior |
|---|---|
| `"raw"` | Hand `symbol` to libbpf verbatim (literal ELF name) |
| `"base"` | Match the demangled base path, attach by resolved file offset |
| `"exact"` | Match the full demangled name |
| `"regex"` | Fan out to one probe per matching function |

A plain-string `symbol` defaults to `"base"`; a `RegExp` becomes `"regex"`. See the [eBPF](./ebpf) reference for the full attach spec.

---

## Recipes

### Resolve a name to a uprobe attach offset

```js
const insp = await Inspector.open('libssl.so');
const [sym] = await insp.find('SSL_write');
// sym.file_offset is the value a uprobe attaches at
await insp.close();
```

### Symbolize a captured user stack

```js
const insp = await Inspector.process(pid);
const frames = await insp.symbolizeMany(stackIps);   // absolute process addresses
const trace = frames.map(f => f ? (f.demangled ?? f.name) : '???');
await insp.close();
```

### Decode a function's first argument for a tracer

```js
const insp = await Inspector.open('/opt/app/server', { debugSyms: true });
const sig = await insp.signature('handle_request');
let arg0 = sig.params[0].type;
if (arg0.kind === 'pointer') arg0 = arg0.pointee;
if (arg0.kind === 'struct') {
  for (const m of arg0.members) console.log(m.name, '@', m.offset, m.type.kind);
}
await insp.close();
```

### Kernel address → name

```js
const kernel = await Inspector.kernel();
const frame = await kernel.symbolize(0xffffffff81abc000n);
console.log(frame ? frame.name : 'unknown');
await kernel.close();
```
