Skip to content

Latest commit

 

History

History
908 lines (704 loc) · 37.3 KB

File metadata and controls

908 lines (704 loc) · 37.3 KB

GhostScope Script Language Reference

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.

Table of Contents

  1. Basic Syntax
  2. Trace Statements
  3. Source Language Support
  4. Variables
  5. Print Statement
  6. Conditional Statements
  7. Expressions
  8. Built-in Functions
  9. Special Variables
  10. Stack Backtrace
  11. Examples
  12. Limitations
  13. Runtime Expression Failures (ExprError)

Basic Syntax

Comments

// Single line comment

/*
   Multi-line
   comment
*/

Statement Types

GhostScope supports the following statements:

  • trace — define trace points and their actions
  • print — output formatted text
  • backtrace / bt — print a DWARF-unwound stack backtrace
  • if / else — conditional execution
  • let — script variable declaration
  • Expression statements

Trace 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.

Syntax

trace <pattern> {
    // statements executed when the trace point fires
}

Trace Patterns

Function Name

trace main {
    print "Main called";
}

trace calculate_something {
    print "Calculating...";
}

Source Line

// 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";
}

Address

// 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 -t and -p are both present, all trace patterns (function, source line, bare address, and module-qualified address) are resolved inside the -t target. -p only 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 -t and -p are both present, -t wins and -p only limits runtime events to that PID.
  • module_suffix:0xADDR allows selecting a module by full path or unique suffix; ambiguous suffixes will list candidates. In -t -p sessions, the module must match the -t target.
  • 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.

Source Language Support

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.

Variables

Script Variables

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;

Types and capabilities

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:

  1. 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;
  1. Floats are not supported in scripts or runtime.
  2. Unary minus - is supported and can nest (e.g., -1, -(-1)), parsed as 0 - expr.
  3. Transport encodes booleans as 0/1; renderers display true/false.

Scoping & Shadowing

  • 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

DWARF Variables

DWARF variables include locals, parameters, and globals from the traced program.

DWARF Types

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>

Supported Complex Access

// 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 *ptr or ->; when safe, pointers are read and dereferenced automatically.
  • Array access: supported for one-dimensional arrays such as arr[index], chain-tail a.b.c[index], and array elements followed by member access such as arr[index].field or a.b[index].c, where index can be an integer literal or an integer expression such as i + 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 to memcmp. Not supported: multi-dimensional arrays.

Explicit Casts

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.

Special Variables

Start with $ and expose runtime info:

  • $pid — current process ID (tgid). If a target PID namespace context is configured and the kernel supports bpf_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 supports bpf_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, from bpf_get_current_pid_tgid() >> 32.
  • $input_pid — the original PID passed to ghostscope -p <PID>, meaning the PID visible where ghostscope -p was invoked. Only available in -p mode.
  • $timestamp — monotonic timestamp (ns), from bpf_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/$tid prefer the target PID-namespace view
  • $host_pid is always the host / initial PID-namespace view
  • $input_pid is always the original -p input value

If bpf_get_ns_current_pid_tgid (helper id 120) is unavailable, $pid/$tid may fall back to host-namespace values.

Variable Lookup Order

  1. Script variables declared with let
  2. Locals/params resolved from DWARF
  3. Program globals

Note: script variables can shadow program variables; choose names carefully.

Print Statement

Basic Forms

// String literal
print "Hello, World";

// Variable
print count;

// Complex expressions
print person.name;
print arr[0];
print *ptr;

Formatted Printing

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 variable name as 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.

Conditional Statements

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)”.

Expressions

Arithmetic

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

Bitwise Operators

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.

Precedence

  1. Parentheses ()
  2. Member access ., Array access []
  3. Pointer deref *, Address‑of &, Unary minus -, Logical NOT !, Bitwise NOT ~
  4. Multiplication *, Division /, Modulo %
  5. Addition +, Subtraction -
  6. Shifts <<, >>
  7. Comparisons <, <=, >, >=
  8. Equality ==, !=
  9. Bitwise AND &
  10. Bitwise XOR ^
  11. Bitwise OR |
  12. Logical AND &&
  13. Logical OR ||

Logical Operators

  • ! (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);
}

Unary Minus

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;
}

Cross‑type Operations with DWARF Values

Integer arithmetic and bitwise operations

(+, -, *, /, %, &, |, ^, ~, <<, >>)

  • 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/1 when 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
}

Pointer Arithmetic

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 + 2 advances by 2 * sizeof(int).
  • Typed read in print: when used in print, p ± n reads and renders the value at the computed address using the pointed-to DWARF type. This enables, e.g., print numbers + 1; to show the second int in an int* numbers argument.
  • Unknown/void*: if the pointed-to type is void or 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
}

Comparisons (==, !=, <, <=, >, >=)

  • Supported: script int/bool with DWARF integer‑like scalars.
  • Semantics for ordered comparisons:
    • Script integer values are signed i64. Booleans participate as 0 or 1.
    • GhostScope does not currently support explicit casts such as (uint32_t)x or x 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, and unsigned short are promoted before comparison. On the current supported C target model, that means int8_t(-5) < uint8_t(250) compares as signed int and evaluates true.
    • For mixed signed/unsigned values, GhostScope chooses the converted comparison width and signedness. For example, int32_t(-1) > uint32_t(4000000000) compares as uint32_t, while uint32_t(4000000000) > -10000000000 compares against the script value as signed i64.
    • 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).
  • Pointers: equality/inequality only (pointer==pointer, pointer==0)
  • CString equality: DWARF char* or char[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.

CString Equality (char*/char[])

GhostScope supports equality/inequality between a script string literal and a DWARF‑side C string:

  • Supported forms: const char* / char* and fixed‑size char[N].
  • Operators: == and !=.
  • Semantics (strict NUL): let the literal length be L.
    • For char*: perform a bounded bpf_probe_read_user_str of up to L+1 bytes. Equality requires the helper to return exactly L+1, the byte at index L to be \0, and the first L bytes to match the literal.
    • For char[N]: perform a bounded bpf_probe_read_user of min(N, L+1) bytes. Equality requires L+1 <= N, the byte at index L to be \0, and the first L bytes to match the literal.
    • Any read failure (invalid address, permission, etc.) evaluates to false.
trace foo.c:60 {
    print "greet-ok:{}", gm == "Hello, Global!"; // gm: const char* or char[]
}

Built-in Functions

strncmp(a, b, n)

  • Check equality within the first n bytes (no NUL required).
  • At least one side (a or b) 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).
  • n must be a non‑negative integer literal; effective length is min(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
}

starts_with(a, b)

  • Check if a starts with b, equivalent to strncmp(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*
}

memcmp(expr_a, expr_b, len)

  • Boolean semantics: returns true if the first len bytes at expr_a and expr_b are identical.
  • Pointer sources: expr_a/expr_b may be DWARF pointer or array (any element type), or address‑of forms (e.g., &expr, &arr[0]). For literal string comparisons, use strncmp/starts_with.
  • Bare integer addresses as pointer arguments are not supported. To match raw bytes, use hex("...").
    • If either operand is hex("..."), len may be omitted; the parser infers len from the hex size. If both sides are hex(...), sizes must match.
    • With a literal len and hex(...), negative lengths and lengths greater than the hex size are rejected at parse time.
  • len accepts 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 is true (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"; }
}

Hex Literal Helper (hex)

  • 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), literal len must be non‑negative and must not exceed the hex size.
  • Semantics: parses two hex digits per byte left‑to‑right; no endianness involved; no 0x inside the string.
  • Scope: as an argument to memcmp to 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"; }
}

Stack Backtrace

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 of bt full and is only shown by bt 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.

Examples

This section highlights common patterns.

Basic Function Trace & Process Info

trace main {
    print "Program start";
    print "PID:{} TID:{} TS:{}", $pid, $tid, $timestamp;
}

Conditional Trace

trace malloc {
    if size > 1_048_576 {  // 1 MB
        print "Large allocation: {} bytes", size;
    }
}

DWARF Auto‑Deref & Member Access

trace process_user {
    print "user:{}", user.name;
    print "status:{}", user.status;

    // auto‑deref pointer to struct when safe
    print "friend:{}", user.friend_ref.name;
}

Array Access & Address‑Of

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];
}

CString Comparisons (char*/char[])

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);
}

Raw Memory Compare (memcmp) & hex Byte Strings

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"; }
}

else‑if Chains & ExprError Soft‑Abort

// 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

Struct Pretty Print & Pointer Deref

// 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
}

Dynamic Length Formatting & Dumps

trace foo {
    let n = 32;
    print "h={:x.*}", n, buf;
    print "ascii={:s.n$}", name;
}

Limitations

  1. No loops (for, while)
  2. No user‑defined functions
  3. Read‑only (no mutation of target program state)
  4. Limited string operations (CString equality and built‑ins only)
  5. Integer-only arithmetic/bitwise operators; floating-point arithmetic is not supported
  6. No dynamic memory allocation in eBPF
  7. 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

Best Practices

  1. Keep it simple to minimize overhead
  2. Filter early with conditions
  3. Include context in print outputs
  4. Avoid complicated logic in probes
  5. Build up incrementally

Notes

  • let declares 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)

Runtime Expression Failures (ExprError)

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. An else if chain continues; if a later condition succeeds, its branch runs. The final else behaves 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 inside print, an additional ExprError is emitted.

ExprError Fields

  • expr: human‑readable expression text (UTF‑8 safe truncation)
  • code: aligned with VariableStatus semantics:
    • 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‑fail
      • 0x02 → second‑arg read‑fail
      • 0x04 → len‑clamped (compare length truncated to cap)
      • 0x08 → len=0
    • strncmp/starts_with:
      • 0x01 → read‑fail
      • 0x04, 0x08 reserved (length clamped/zero)
  • failing_addr: the pointer address involved (or 0 if unknown). When zero, renderers show at 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)