GhostScope uses a domain‑specific language to define trace points and actions. You can write scripts inline in the TUI with the trace command or load them from a script file.
- Basic Syntax
- Trace Statements
- Source Language Support
- Variables
- Print Statement
- Conditional Statements
- Expressions
- Built-in Functions
- Special Variables
- Stack Backtrace
- Examples
- Limitations
- Runtime Expression Failures (ExprError)
// Single line comment
/*
Multi-line
comment
*/
GhostScope supports the following statements:
trace— define trace points and their actionsprint— output formatted textbacktrace/bt— print a DWARF-unwound stack backtraceif/else— conditional executionlet— script variable declaration- Expression statements
The trace statement is the top‑level construct used only at the script file level (not nested inside other trace blocks).
Before attaching uprobes, you can validate a script and inspect the resolved targets:
ghostscope -p 1234 --script-file trace.gs --dry-run
ghostscope -p 1234 --script-file trace.gs --dry-run --dry-run-details--dry-run parses DWARF, compiles the script, resolves PCs and uprobe file
offsets, then exits without attaching. It still performs the same startup
privilege and kernel capability checks as a real run, so use sudo or
equivalent eBPF privileges when your system requires them. --dry-run-details
adds source locations, inline classification, script-used variables, visible
variables, and optimized-out/unavailable variable diagnostics.
trace <pattern> {
// statements executed when the trace point fires
}
trace main {
print "Main called";
}
trace calculate_something {
print "Calculating...";
}
// Trace a specific file and line
trace sample.c:42 {
print "Hit line 42";
}
// Supports various path formats
trace /home/user/project/src/utils.c:100 {
print "Utility function";
}
// Module-relative virtual address (DWARF/symbol PC)
trace 0x401234 {
print "Hit address";
}
// Module suffix + address (full path or unique suffix)
trace libc.so.6:0x1234 {
print "Hit libc address";
}
Notes:
- When
-tand-pare both present, all trace patterns (function, source line, bare address, and module-qualified address) are resolved inside the-ttarget.-ponly limits runtime events to that process. - For
0xADDR, the default module depends on startup mode:-t <binary>uses<binary>;-p <pid>uses the main executable. If-tand-pare both present,-twins and-ponly limits runtime events to that PID. module_suffix:0xADDRallows selecting a module by full path or unique suffix; ambiguous suffixes will list candidates. In-t -psessions, the module must match the-ttarget.- Address trace targets always use the module's DWARF/symbol virtual address. Do not pass a raw ELF file offset or a runtime ASLR-adjusted address from
/proc/<pid>/maps; GhostScope converts the virtual address to the uprobe file offset internally.
GhostScope's script syntax is source-language agnostic, but real-world support depends on how directly the traced program's DWARF maps back to runtime memory. Today the support level is uneven:
| Source language | Support level | What to expect |
|---|---|---|
| C | Best | This is the primary target. Plain locals, globals, x86_64 executable static thread-local variables, pointers, arrays, structs, enums, and C strings map most directly to the current DWARF readers and script operators. |
| C++ | Limited | Automatic demangling is supported for function names in trace ... patterns and for global/static variable lookup. Beyond name resolution, most C++-specific language features are not modeled yet, so the best results come from simple, C-like layouts and scalar fields. |
| Rust | Limited | Automatic demangling is supported for function names in trace ... patterns and for global/static variable lookup. Beyond name resolution, most Rust-specific language features are not modeled yet, so the best results come from plain globals, scalar fields, and straightforward struct layouts. |
Practical guidance:
- Prefer C targets when you need the highest success rate for complex DWARF expressions.
- GhostScope recognizes
DW_OP_form_tls_address, which is used for both static and dynamic TLS. Runtime address resolution currently supports only x86_64 executable static TLS; dynamic/shared-library TLS is not modeled yet. - For C++ and Rust, think of GhostScope as "DWARF layout aware" rather than "language semantics aware".
- In C++ and Rust, start from demangled function/global names, then probe simple fields first. If name lookup is ambiguous, fall back to line- or address-based trace patterns.
Script variables are immutable. Within a single trace block, a name can be bound only once; redeclaration is a compile error, and there is no assignment statement (x = ... is not supported).
Declare with let:
let count = 0;
let threshold = 100;
let message = "hello";
let result = a + b;
| Type | Literal/Example | Description | Ops/Comparisons |
|---|---|---|---|
| Integer (i64) | 123, -42 |
64‑bit signed integer | +, -, *, /, %, bitwise &, ` |
| Boolean (bool) | true, false, or from comparison a < b |
From literals/comparisons/logical ops | logical AND/OR; when mixing with DWARF integers, treated as 0/1 |
| String | "hello" |
UTF‑8 string literal | Equality ==, != with DWARF C strings; no ordering |
| Alias (DWARF expr alias) | let a = global.arr;, let p = &buf[0]; |
A named alias to any DWARF expression (variable, member, array, pointer deref, or address‑of). Lets you give short names to complex types/paths and reuse them. | Supports the same complex access as the underlying DWARF type: member access (a.field), literal or expression index (a[0], a[i + 1]), address‑of (&a), and use in memcmp/strncmp/starts_with and {:x.N}/{:s.N}/{:p}. Pointer arithmetic is scaled by the pointed-to type when GhostScope knows the pointee layout. |
Notes:
- Script variables do not expose structs/arrays/pointers. Access those through DWARF variables (member access, deref, literal or expression index) to obtain scalars first. The exception is an alias variable, which can bind to any DWARF expression and be used as a reusable base for member/index access, address‑of, and memory formatting.
Examples:
// Alias a complex DWARF path and reuse it
let a = global_var.arr; // arr is DWARF array/aggregate
print "ptr={:p}", &a; // take address of alias
print a[1]; // integer-literal index on alias
print a[i + 1]; // expression index on alias
// Address-of aliases still work
let p = &conn.buf[0];
print "h={:x.16}", p;
- Floats are not supported in scripts or runtime.
- Unary minus
-is supported and can nest (e.g.,-1,-(-1)), parsed as0 - expr. - Transport encodes booleans as 0/1; renderers display
true/false.
- Block scope: every
{ ... }creates a new lexical scope;if’s then/else are independent sub‑scopes. A variable is visible only within its declaring scope and nested sub‑scopes; it is not visible after the scope ends. - No shadowing between script variables: inner scopes cannot re‑bind a name that exists in an outer scope (even though they are different scopes).
- Friendly errors:
- Assignment:
Assignment is not supported: variables are immutable. Use 'let a = ...' to bind once. - Same‑scope redeclaration:
Redeclaration in the same scope is not allowed: 'x' - Shadowing:
Shadowing is not allowed for immutable variables: 'x' - Out‑of‑scope use:
Use of variable 'y' outside of its scope
- Assignment:
DWARF variables include locals, parameters, and globals from the traced program.
| DWARF Type | Example (source language) | Mapping/Display | Access/Operations |
|---|---|---|---|
| Signed/Unsigned Integers (1/2/4/8 bytes) | int, long, unsigned int, size_t |
I8/I16/I32/I64 or U8/U16/U32/U64 | Printable; mixes with script integers/bools for arithmetic and comparisons (after width/sign normalization) |
| Boolean | bool |
Bool (true/false) |
Printable; comparable with script booleans/integers |
| Float | float, double |
Printable | eBPF has no FP; scripts don’t support float literals/ops |
| Char | char, unsigned char |
1‑byte int/char | Printed as 1‑byte integer; arrays/pointers see below |
| C string | char*, const char*, char[] |
CString (rendered as string) | Printable; equality ==, != with script strings |
| Pointer | T*, void*, function pointer |
Pointer/NullPointer (address) | Supports * deref and ==/!=; auto‑deref for locals/params/globals when safe |
| Array | T[N] |
Array | Literal and expression index reads for one-dimensional arrays, including top-level arrays, chain-tail arrays, and array elements followed by member access; multi-dim arrays are not supported |
| Struct/Class | struct Foo, class Bar |
Struct | Use . member access; operate on scalar members only |
| Union | union U |
Union | Show one member view; access members then treat as scalar |
| Enum | enum E |
Enum (via base int) | Printed as Type::Variant; arithmetic/compare uses base integer |
| Bitfield | int flags:3 |
Bitfield → integer view | Extracted integer; mixes with script ints/bools |
| Typedef/Qualified | typedef, const, volatile |
Typedef/QualifiedType | Treated as underlying type |
| Optimized‑out | variable optimized away | OptimizedOut | Read fails; renders <OPTIMIZED_OUT>; operations follow failure semantics |
| Unknown | unsupported/unknown | Unknown | Renders <UNKNOWN_TYPE_N_BYTES> |
// Simple variable
print x;
// Member access (struct fields)
print person.name;
print config.settings.timeout;
// Array access (literal and expression indices)
print arr[0];
print arr[1];
print arr[i + 1];
// Pointer dereference
print *ptr;
print *(ptr);
// Address‑of
print &variable;
// Chained access
print obj.field.subfield;
print obj.field.items[i + 1];
Tips:
- Auto‑dereference is supported for locals/params/globals. You don’t need to write
*ptror->; when safe, pointers are read and dereferenced automatically. - Array access: supported for one-dimensional arrays such as
arr[index], chain-taila.b.c[index], and array elements followed by member access such asarr[index].fieldora.b[index].c, whereindexcan be an integer literal or an integer expression such asi + 1. Address-of forms like&arr[i]and&a.b.c[i]can be printed as pointers, used as memory-format inputs ({:x.N}/{:s.N}), or passed tomemcmp. Not supported: multi-dimensional arrays.
Use cast(expr, "TYPE") to force GhostScope to interpret an expression as a different type. This is most useful when a value is an address, void*, or otherwise lacks the precise DWARF type you want to inspect.
trace handle_request {
let req = cast(ctx, "struct request *");
print req.id;
print req.headers[0];
}
Cast type names are resolved from DWARF type names. Treat the DWARF DW_AT_name value as the source of truth: C-style prefixes such as struct, union, enum, and class are accepted as convenience hints, but they are not usually part of the stored DWARF name. For example, a C struct request normally appears in DWARF as a structure DIE named request, while a typedef appears under the typedef name.
Base types such as int, unsigned int, bool, float, and double also commonly exist in DWARF as base-type DIEs. GhostScope accepts common builtin spellings for convenience, and scalar casts can still be useful for width, signedness, and boolean normalization. The main value of cast, however, is user-defined and layout-bearing types: structs, classes, unions, enums, typedefs, pointers, and arrays. Those types let GhostScope use DWARF member offsets and element layout to read meaningful fields from memory.
Type lookup is scoped by module. A module is the runtime-loaded ELF object that owns the trace point: the main executable or a shared library, not a source file, package, namespace, or Rust crate. Resolution first checks the current trace module, then falls back to other loaded modules that have DWARF information.
Name collisions are possible when different modules define the same type name, for example a main executable and a shared library both defining struct request. In that case, the type in the current trace module wins. If the type exists in exactly one other loaded module, GhostScope may resolve that fallback match; if multiple fallback modules match, cast reports an ambiguity instead of picking one arbitrarily. There is no explicit module= or module-qualified cast syntax yet; future versions may add it for cross-module disambiguation.
Start with $ and expose runtime info:
$pid— current process ID (tgid). If a target PID namespace context is configured and the kernel supportsbpf_get_ns_current_pid_tgid, this uses the target PID namespace view; otherwise it falls back to the host / initial PID namespace view.$tid— current thread ID (tid). If a target PID namespace context is configured and the kernel supportsbpf_get_ns_current_pid_tgid, this uses the target PID namespace view; otherwise it falls back to the host / initial PID namespace view.$host_pid— current process ID (tgid) in the host / initial PID namespace view, frombpf_get_current_pid_tgid() >> 32.$input_pid— the original PID passed toghostscope -p <PID>, meaning the PID visible whereghostscope -pwas invoked. Only available in-pmode.$timestamp— monotonic timestamp (ns), frombpf_ktime_get_ns.
All behave as integers for comparisons/arithmetic.
Example:
trace sample.c:42 {
if $host_pid == 12345 { print "match"; }
print "PID:{} HOST:{} INPUT:{} TID:{} TS:{}", $pid, $host_pid, $input_pid, $tid, $timestamp;
}
Note: Currently only $pid, $tid, $host_pid, $input_pid, and $timestamp are supported. Register-related specials may be added later if needed.
In container PID-namespace environments, $pid/$tid and $host_pid/$input_pid may differ:
$pid/$tidprefer the target PID-namespace view$host_pidis always the host / initial PID-namespace view$input_pidis always the original-pinput value
If bpf_get_ns_current_pid_tgid (helper id 120) is unavailable, $pid/$tid may fall back to host-namespace values.
- Script variables declared with
let - Locals/params resolved from DWARF
- Program globals
Note: script variables can shadow program variables; choose names carefully.
// String literal
print "Hello, World";
// Variable
print count;
// Complex expressions
print person.name;
print arr[0];
print *ptr;
Rust‑like placeholders:
print "Value: {}", value;
print "X: {}, Y: {}", x, y;
print "Name: {}, Age: {}", person.name, person.age;
Extended specifiers and dynamic length:
// Hex / pointer / ASCII bytes
print "A={:x} B={:X}", a, b;
print "p={:p}", ptr;
print "s={:s}", cstr; // for char*/char[N], `{}` prints quoted C string
// Memory dump from pointer/array
print "h={:x.16}", buf; // read 16B as hex
print "ascii={:s.32}", name; // read 32B as ASCII (char* stops at first NUL)
// Dynamic length (star): length argument comes before value
print "buf={:x.*}", len, buf;
// Dynamic length via capture
let n = tail_len;
print "tail={:s.n$}", p;
Notes:
{}default;{:x}/{:X}render the captured value payload as hex bytes;{:p}renders an address;{:s}renders ASCII bytes.- Length suffixes change
{:x}/{:s}into memory-dump forms.{:x}formats the value already captured for the argument;{:x.4}treats the argument as an addressable memory source and reads 4 bytes from that address/source. - Length suffixes:
.{N}: static length (decimal/0x../0o../0b..)..*: dynamic (consumes two args: len then value)..name$: capture script variablenameas length; does not consume an extra value arg.
- Reads are type-sensitive. For
{:x}, the DWARF/script type controls the captured value size and how the value is materialized. For{:x.N}/{:s.N}, pointers use the pointer value as the read address, arrays/aggregates use their base address, and addressable scalar DWARF variables use their storage address. A pure script integer is not addressable and will be rejected unless you explicitly cast it to a pointer type. - Kernel performs bounded reads for memory-dump forms; user space renders hex/ASCII. For
{:s}ASCII, rendering stops at first NUL; non‑printables show as\xNN. - Per‑argument read cap is controlled by
ebpf.mem_dump_cap(default 256 bytes). Requests beyond cap are truncated; if event payload is exceeded, output may also truncate with…. - On read failure (e.g., null deref, offsets unavailable, permission), extended specifiers print
<MISSING_ARG>.
Example:
trace foo.c:42 {
// int a = 10;
print "value-hex={:x}", a; // hex view of the captured int value, e.g. 0a 00 00 00
print "mem-hex={:x.4}", a; // read 4 bytes from a's DWARF storage address
print "bad={:x.4}", 10; // rejected: script integer literal is not an address
print "forced={:x.4}", cast(10, "u8 *"); // tries to read 4 bytes at address 0xa
}
Note: Format strings use Rust‑style placeholders, not %d/%s.
Rust‑like if-else conditions:
// Simple if
if x > 100 {
print "Large";
}
// If-else
if result == 0 {
print "Success";
} else {
print "Failed";
}
// Nested if-else
if x > 100 {
print "Large";
} else if x > 50 {
print "Medium";
} else {
print "Small";
}
Note: When a conditional depends on DWARF‑backed reads and a read fails at runtime, GhostScope does not silently treat the condition as false. It emits a structured ExprError and applies soft‑abort semantics for that condition. See “Runtime Expression Failures (ExprError)”.
let sum = a + b;
let diff = a - b;
let product = a * b;
let quotient = a / b;
let remainder = a % 4;
// Integer literals
let x = 123; // decimal
let h = 0x1f; // hex (31)
let o = 0o755; // octal (493)
let b = 0b1010; // binary (10)
let neg = -0x10; // unary minus is parsed as 0 - 16
let masked = flags & 0x4;
let combined = flags | 0x10;
let flipped = flags ^ 0xff;
let inverse = ~flags;
let high = value >> 8;
let scaled = value << 2;
Bitwise operators require integer/boolean operands. Variable shift counts are masked to the operand width to avoid undefined runtime shifts.
- Parentheses
() - Member access
., Array access[] - Pointer deref
*, Address‑of&, Unary minus-, Logical NOT!, Bitwise NOT~ - Multiplication
*, Division/, Modulo% - Addition
+, Subtraction- - Shifts
<<,>> - Comparisons
<,<=,>,>= - Equality
==,!= - Bitwise AND
& - Bitwise XOR
^ - Bitwise OR
| - Logical AND
&& - Logical OR
||
!(logical NOT),&&(logical AND),||(logical OR)- Non‑zero is true
||/&&short-circuit
Examples:
trace main {
if a > 10 && b == 0 {
print "AND";
} else if a < 100 || p == 0 {
print "OR";
}
print "NOT1:{}", !starts_with(activity, "main");
print "NOT2:{}", !strncmp(record, "HTTP", 4);
}
Semantics: negate an expression; recursive nesting is supported. Parsing is treated as 0 - expr.
trace foo.c:42 {
let a = -1;
let b = -(-1);
print a;
print "X:{}", b;
}
(+, -, *, /, %, &, |, ^, ~, <<, >>)
- Supported: script int/bool with DWARF integer‑like scalars
- BaseType (signed/unsigned 1/2/4/8 bytes), Enum (as underlying), Bitfield (extracted integer), char/unsigned char (1 byte)
- Booleans participate as
0/1when used arithmetically. - Not supported: aggregates (struct/union/array), pointers, floats
Examples (Arithmetic)
// DWARF integer with script integer
trace foo.c:42 {
print "sum:{}", s.counter + 5;
}
// Enum/bitfield (as integer)
trace foo.c:43 {
print "active:{}", a.active == 1;
}
// Boolean participates as 0/1
trace foo.c:60 {
let ok = true;
print "S:{}", ok + 41; // 42
}
GhostScope supports C-style pointer arithmetic in a restricted, safe form:
- Allowed:
ptr + int,int + ptr,ptr - int. - Scaling: the integer offset is scaled by the element size of the pointer target type (C semantics). For example, for
int* p,p + 2advances by2 * sizeof(int). - Typed read in print: when used in
print,p ± nreads and renders the value at the computed address using the pointed-to DWARF type. This enables, e.g.,print numbers + 1;to show the secondintin anint* numbersargument. - Unknown/void*: if the pointed-to type is
voidor unavailable, scaling falls back to 1 byte. - Not supported: pointer arithmetic on function pointers; pointer–pointer arithmetic (
p + q,p - q). Ordered comparisons on pointers (<,<=,>,>=) are rejected; use==/!=.
Examples:
trace calculate_average {
print numbers; // prints address or first element depending on context
print numbers + 1; // prints the second int (scaled by sizeof(int))
}
trace log_activity {
print activity + 1; // for const char* activity, prints the next character
}
- Supported: script int/bool with DWARF integer‑like scalars.
- Semantics for ordered comparisons:
- Script integer values are signed
i64. Booleans participate as0or1. - GhostScope does not currently support explicit casts such as
(uint32_t)xorx as u32. - For DWARF-backed C integer-like scalars, ordered comparisons follow C-style integer promotions and usual arithmetic conversions before selecting a signed or unsigned comparison.
- Narrow integer types such as
char,unsigned char,short, andunsigned shortare promoted before comparison. On the current supported C target model, that meansint8_t(-5) < uint8_t(250)compares as signedintand evaluates true. - For mixed signed/unsigned values, GhostScope chooses the converted comparison width and signedness. For example,
int32_t(-1) > uint32_t(4000000000)compares asuint32_t, whileuint32_t(4000000000) > -10000000000compares against the script value as signedi64. - When the converted type is unsigned, both operands are converted to that width before applying the unsigned predicate. This preserves C behavior for cases such as
INT32_MIN < uint32_t(4000000000).
- Script integer values are signed
- Pointers: equality/inequality only (pointer==pointer, pointer==0)
- CString equality: DWARF
char*orchar[N]vs script string literal via bounded read - Not supported: relational string comparisons; aggregates; floats
Examples (Comparisons)
// Safe equality across signed/unsigned
if count == size { print "EQ"; }
// Ordered compares follow C integer promotions/usual arithmetic conversions
let t = 1024;
if size > t { print ">1K"; }
// Script integers are signed i64; casts are not supported yet
if u32_count > -10000000000 { print "script-signed-i64"; }
// Pointer equality (no ordering)
trace foo.c:50 {
print "isNull:{}", p == 0; // pointer vs NULL
}
Error semantics: If a DWARF read fails (null deref/read error/offsets unavailable), comparisons return false and arithmetic returns 0; event status carries the error code. See “Runtime Expression Failures (ExprError)” below for details.
GhostScope supports equality/inequality between a script string literal and a DWARF‑side C string:
- Supported forms:
const char*/char*and fixed‑sizechar[N]. - Operators:
==and!=. - Semantics (strict NUL): let the literal length be
L.- For
char*: perform a boundedbpf_probe_read_user_strof up toL+1bytes. Equality requires the helper to return exactlyL+1, the byte at indexLto be\0, and the firstLbytes to match the literal. - For
char[N]: perform a boundedbpf_probe_read_userofmin(N, L+1)bytes. Equality requiresL+1 <= N, the byte at indexLto be\0, and the firstLbytes to match the literal. - Any read failure (invalid address, permission, etc.) evaluates to
false.
- For
trace foo.c:60 {
print "greet-ok:{}", gm == "Hello, Global!"; // gm: const char* or char[]
}
- Check equality within the first
nbytes (no NUL required). - At least one side (
aorb) must be a string: string literal or a script string variable (e.g.,let s = "AB";). - The other side can be: DWARF pointer/array, DWARF alias, or another string.
- If both sides are strings, the result folds at compile time. If exactly one side is a string, runtime reads the other side and compares (read failures produce ExprError).
nmust be a non‑negative integer literal; effective length ismin(n, compare_cap, string length, readable bytes)(compare_cap defaults to 64).
Examples: strncmp
// Function parameter (const char* activity)
trace log_activity {
print "eq5:{}", strncmp(activity, "main_", 5);
}
// Global/rodata C strings or fixed arrays
trace globals_program.c:32 {
print "lm_libw:{}", strncmp(lm, "LIB_", 4); // lm: const char*
}
// Generic pointer (read failure → false)
trace process_record {
print "rec_http:{}", strncmp(record, "HTTP", 4); // record: struct* -> false
}
- Check if
astarts withb, equivalent tostrncmp(a, b, len(b)). - At least one side must be a string (literal or script string variable); the other side can be an address expression (DWARF pointer/array or alias) or a string.
- If both sides are strings, the result folds at compile time; if exactly one side is a string, runtime reads
len(b)bytes from the other side and compares (read failures produce ExprError).
Examples: starts_with
// Prefix match (equivalent to strncmp(expr, lit, len(lit)))
trace log_activity {
print "is_main:{}", starts_with(activity, "main");
}
trace globals_program.c:32 {
print "gm_hello:{}", starts_with(gm, "Hello"); // gm: const char*
}
- Boolean semantics: returns
trueif the firstlenbytes atexpr_aandexpr_bare identical. - Pointer sources:
expr_a/expr_bmay be DWARF pointer or array (any element type), or address‑of forms (e.g.,&expr,&arr[0]). For literal string comparisons, usestrncmp/starts_with. - Bare integer addresses as pointer arguments are not supported. To match raw bytes, use
hex("...").- If either operand is
hex("..."),lenmay be omitted; the parser inferslenfrom the hex size. If both sides arehex(...), sizes must match. - With a literal
lenandhex(...), negative lengths and lengths greater than the hex size are rejected at parse time.
- If either operand is
lenaccepts script integer expressions (decimal,0x..,0o..,0b..). At runtime, negative values are clamped to 0; literal negatives are rejected by the parser.- No NUL semantics; raw byte comparison (length in bytes).
- If
len == 0, result istrue(no user‑memory reads). - Any DWARF read failure on either side evaluates to
false(see ExprError).
Verifier friendliness and performance:
- Compiles to branch‑light byte comparisons (e.g., XOR/OR accumulation) to avoid verifier state explosion.
- Avoid packing many large string checks into a single hot probe; consider splitting trace points or attaching at less‑hot sites.
Examples: memcmp
// Raw memory equality between two pointers
trace globals_program.c:32 {
// Equal bytes
if memcmp(&lib_pattern[0], &lib_pattern[0], 16) { print "EQ"; } else { print "NE"; }
// Different due to offset
if memcmp(&lib_pattern[0], &lib_pattern[1], 16) { print "EQ2"; } else { print "NE2"; }
// len=0 → true (no user-memory reads)
if memcmp(&lib_pattern[0], &lib_pattern[1], 0) { print "Z0"; }
// Dynamic length from script variable
let n = 10;
if memcmp(&lib_pattern[0], &lib_pattern[0], n) { print "DYN_EQ"; }
}
- Syntax:
hex("<HEX BYTES>")- Only hex digits (
0-9a-fA-F) and spaces; after removing spaces, there must be an even number of digits. Tabs and other separators are not allowed. - Parse‑time validation: rejects any non‑hex character and odd digit count; with
memcmp(expr, hex(...), len_literal), literallenmust be non‑negative and must not exceed the hex size.
- Only hex digits (
- Semantics: parses two hex digits per byte left‑to‑right; no endianness involved; no
0xinside the string. - Scope: as an argument to
memcmpto compare memory against raw bytes (headers, magic constants, etc.). - Examples:
trace foo {
if memcmp(buf, hex("50 4F"), 2) { print "HDR"; }
if memcmp(ptr, hex("DE AD BE EF"), 4) { print "MAGIC"; }
}
backtrace; and bt; emit a source-aware stack backtrace at the probe point. The unwinder uses DWARF CFI directly; there is no unwind= option and no helper/fp fallback mode to select.
trace test_function {
print "before";
bt;
print "after";
}
Options:
bt raw;prints raw module cookie, module offset, and runtime IP without source symbolization.bt full;prints symbolized source-aware frames. Raw IP/cookie debug metadata is kept out ofbt fulland is only shown bybt raw.bt inline;enables inline call-chain rendering. This is the default.bt noinline;suppresses inline call-chain rendering.
Backtrace depth is configured globally, not in the script. Use --backtrace-depth <N> or [ebpf] backtrace_depth = N in the config file. Valid range is 1..=128; the default is 128.
In --script-output pretty, backtrace payload lines are colorized when [script] color enables ANSI output. --script-output plain always emits the raw payload text without ANSI color.
Examples:
trace test_function {
bt full;
bt raw noinline;
}
Typical output:
backtrace: complete, 4 frames (max 128)
#0 test_function(int argc, char** argv) at sample_program.c:8:5 [sample_program+0x1189]
#1 caller(int value) at sample_program.c:42:9 [sample_program+0x1234]
#2 main(int argc, char** argv) at sample_program.c:88:12 [sample_program+0x13a0]
#3 <unknown function> at ?? [libc.so.6+0x2a1ca]
bt raw; keeps the same header but prints machine-facing fields for diagnosis:
backtrace: truncated, 2 frames (max 2)
#0 0x1189 [sample_program+0x1189] raw=0x55... cookie=0x...
#1 0x1234 [sample_program+0x1234] raw=0x55... cookie=0x...
status=complete means DWARF unwinding reached a natural stop before the configured depth cap. status=truncated means GhostScope hit the configured depth cap or the eBPF tail-call unwind budget before a natural stop. Other statuses explain where unwinding stopped, for example no unwind rows for the current PC, unsupported CFI, unavailable module offsets, a failed user-memory read, or an invalid next frame. When available, stopped: includes a stable reason label and numeric code.
This section highlights common patterns.
trace main {
print "Program start";
print "PID:{} TID:{} TS:{}", $pid, $tid, $timestamp;
}
trace malloc {
if size > 1_048_576 { // 1 MB
print "Large allocation: {} bytes", size;
}
}
trace process_user {
print "user:{}", user.name;
print "status:{}", user.status;
// auto‑deref pointer to struct when safe
print "friend:{}", user.friend_ref.name;
}
trace foo.c:42 {
print "arr0:{}", arr[0];
print "name0:{}", person.names[0];
print "next:{}", person.names[i + 1];
// address‑of used in builtins or dumps
print "p(&buf[0])={:p}", &buf[0];
print "p(&buf[i])={:p}", &buf[i];
}
trace log_activity {
print "prefix:{}", starts_with(activity, "main");
print "eq:{}", strncmp(activity, "main_", 5);
}
trace globals_program.c:32 {
print "lm_libw:{}", strncmp(lm, "LIB_", 4);
}
trace globals_program.c:32 {
if memcmp(&lib_pattern[0], &lib_pattern[0], 16) { print "EQ"; } else { print "NE"; }
if memcmp(&lib_pattern[0], &lib_pattern[1], 16) { print "EQ2"; } else { print "NE2"; }
if memcmp(&lib_pattern[0], &lib_pattern[1], 0) { print "Z0"; }
let n = 10;
if memcmp(&lib_pattern[0], &lib_pattern[0], n) { print "DYN_EQ"; }
}
trace foo {
if memcmp(buf, hex("50 4F"), 2) { print "HDR"; }
if memcmp(ptr, hex("DE AD BE EF"), 4) { print "MAGIC"; }
}
// G_STATE.lib can be NULL at times; read failure triggers ExprError
trace globals_program.c:32 {
if memcmp(G_STATE.lib, hex("00"), 1) { print "A"; }
else if memcmp(gm, hex("48"), 1) { print "B"; }
else { print "C"; }
}
// Expect: an ExprError line and "B" printed; A/C suppressed by soft‑abort
// Example adapted from complex_types_program
trace complex_types_program.c:25 {
print s.name; // char[16] -> string
print s; // pretty‑print struct
print *ls; // deref pointer then pretty‑print
}
trace foo {
let n = 32;
print "h={:x.*}", n, buf;
print "ascii={:s.n$}", name;
}
- No loops (
for,while) - No user‑defined functions
- Read‑only (no mutation of target program state)
- Limited string operations (CString equality and built‑ins only)
- Integer-only arithmetic/bitwise operators; floating-point arithmetic is not supported
- No dynamic memory allocation in eBPF
- Uneven source-language coverage: C works best; C++ and Rust currently rely mostly on automatic demangling plus DWARF-layout-based access, with most language-specific features unsupported
- Keep it simple to minimize overhead
- Filter early with conditions
- Include context in print outputs
- Avoid complicated logic in probes
- Build up incrementally
letdeclares script‑local variables, not program variables- All variables are dynamically typed in script space
- String literals must use double quotes
- Most statements require semicolons
- Trace pattern matching supports fuzzy file suffix (see Command Reference)
When an if/else if condition or a builtin (memcmp, strncmp, starts_with) depends on DWARF‑backed runtime reads and a read fails, GhostScope does not silently treat the condition as false. Instead, it sends a structured warning (ExprError) to user space and applies soft‑abort semantics:
- Soft‑abort:
- For a failing
if: skip the current then/else. Anelse ifchain continues; if a later condition succeeds, its branch runs. The finalelsebehaves normally unless a previous condition in the chain already succeeded. - For
print: do not abort the line; per‑variable statuses render inline. If a builtin fails insideprint, an additional ExprError is emitted.
- For a failing
expr: human‑readable expression text (UTF‑8 safe truncation)code: aligned withVariableStatussemantics:- 1 = NullDeref
- 2 = ReadError (includes probe_read_user failures)
- 3 = AccessError
- 4 = Truncated
- 5 = OffsetsUnavailable (missing ASLR offsets)
- 6 = ZeroLength (requested length is 0)
flags: bitmask (builtin‑specific meanings)memcmp:0x01→ first‑arg read‑fail0x02→ second‑arg read‑fail0x04→ len‑clamped (compare length truncated to cap)0x08→ len=0
strncmp/starts_with:0x01→ read‑fail0x04,0x08reserved (length clamped/zero)
failing_addr: the pointer address involved (or 0 if unknown). When zero, renderers showat NULL.
Console example:
ExprError: memcmp(buf, hex("504f"), 2) (read error at 0x0000000100000000, flags: first-arg read-fail,len-clamped)
When the failing address is zero:
ExprError: memcmp(G_STATE.lib, hex("00"), 1) (read error at NULL, flags: first-arg read-fail)