Skip to main content

ghostscope_compiler/
lib.rs

1// Keep library clippy-clean without allow attributes
2
3pub mod ebpf;
4pub mod script;
5
6use crate::script::compiler::AstCompiler;
7use ebpf::context::CodeGenError;
8pub use ghostscope_dwarf::RuntimeCapabilities;
9use ghostscope_dwarf::{CfaRulePlan, CompactUnwindRow, RegisterRecoveryPlan};
10use ghostscope_process::module_probe;
11pub use ghostscope_process::{PidFilterSpec, PidNamespaceId};
12use script::parser::ParseError;
13use std::borrow::Cow;
14use tracing::info;
15
16const X86_64_DWARF_RIP: u16 = 16;
17const X86_64_DWARF_RBP: u16 = 6;
18const X86_64_DWARF_RSP: u16 = 7;
19
20pub fn hello() -> &'static str {
21    "Hello from ghostscope-compiler!"
22}
23
24pub use ghostscope_protocol::bpf_abi::{
25    BacktraceTailCallState, BacktraceUnwindRow, BACKTRACE_RA_AT_CFA_OFFSET, BACKTRACE_RA_REGISTER,
26    BACKTRACE_RA_SAME_VALUE, BACKTRACE_RA_UNDEFINED, BACKTRACE_RA_VAL_CFA_OFFSET,
27    BACKTRACE_RECOVERY_AT_CFA_OFFSET, BACKTRACE_RECOVERY_REGISTER, BACKTRACE_RECOVERY_SAME_VALUE,
28    BACKTRACE_RECOVERY_UNDEFINED, BACKTRACE_RECOVERY_VAL_CFA_OFFSET, BACKTRACE_TAIL_NO_NEXT_SLOT,
29    BACKTRACE_TAIL_STATE_ACTIVE_SLOT_OFFSET, BACKTRACE_TAIL_STATE_CURRENT_IP_OFFSET,
30    BACKTRACE_TAIL_STATE_CURRENT_RBP_OFFSET, BACKTRACE_TAIL_STATE_CURRENT_RSP_OFFSET,
31    BACKTRACE_TAIL_STATE_ERROR_CODE_OFFSET, BACKTRACE_TAIL_STATE_EVENT_SIZE_OFFSET,
32    BACKTRACE_TAIL_STATE_FLAGS_OFFSET, BACKTRACE_TAIL_STATE_FRAME_COUNT_OFFSET,
33    BACKTRACE_TAIL_STATE_INST_OFFSET_OFFSET, BACKTRACE_TAIL_STATE_MODULE_BIAS_OFFSET,
34    BACKTRACE_TAIL_STATE_MODULE_COOKIE_OFFSET, BACKTRACE_TAIL_STATE_NEXT_SLOT_OFFSET,
35    BACKTRACE_TAIL_STATE_OFFSETS_FOUND_OFFSET, BACKTRACE_TAIL_STATE_REQUESTED_DEPTH_OFFSET,
36    BACKTRACE_TAIL_STATE_SIZE, BACKTRACE_TAIL_STATE_TAIL_CALLS_OFFSET,
37    BACKTRACE_UNWIND_ROW_CFA_OFFSET_OFFSET, BACKTRACE_UNWIND_ROW_CFA_REGISTER_OFFSET,
38    BACKTRACE_UNWIND_ROW_PC_END_OFFSET, BACKTRACE_UNWIND_ROW_PC_START_OFFSET,
39    BACKTRACE_UNWIND_ROW_RA_KIND_OFFSET, BACKTRACE_UNWIND_ROW_RA_OFFSET_OFFSET,
40    BACKTRACE_UNWIND_ROW_RA_REGISTER_OFFSET, BACKTRACE_UNWIND_ROW_RBP_KIND_OFFSET,
41    BACKTRACE_UNWIND_ROW_RBP_OFFSET_OFFSET, BACKTRACE_UNWIND_ROW_RBP_REGISTER_OFFSET,
42    BACKTRACE_UNWIND_ROW_SIZE, BACKTRACE_UNWIND_WORDS_PER_ROW, BACKTRACE_UNWIND_WORD_CFA_OFFSET,
43    BACKTRACE_UNWIND_WORD_PC_END, BACKTRACE_UNWIND_WORD_PC_START, BACKTRACE_UNWIND_WORD_RA_OFFSET,
44    BACKTRACE_UNWIND_WORD_RBP_OFFSET, BACKTRACE_UNWIND_WORD_REGISTERS,
45};
46
47#[derive(Debug, thiserror::Error)]
48pub enum CompileError {
49    #[error("Parse error: {0}")]
50    Parse(#[from] Box<ParseError>),
51
52    #[error("Code generation error: {0}")]
53    CodeGen(#[from] CodeGenError),
54
55    #[error("LLVM error: {0}")]
56    LLVM(String),
57
58    #[error("{0}")]
59    Other(String),
60}
61
62pub type Result<T> = std::result::Result<T, CompileError>;
63
64impl From<ParseError> for CompileError {
65    fn from(err: ParseError) -> Self {
66        CompileError::Parse(Box::new(err))
67    }
68}
69
70impl CompileError {
71    pub fn user_message(&self) -> Cow<'_, str> {
72        match self {
73            CompileError::Parse(err) => Cow::Owned(format!("Parse error: {err}")),
74            CompileError::CodeGen(err) => err.user_message(),
75            CompileError::LLVM(message) | CompileError::Other(message) => Cow::Borrowed(message),
76        }
77    }
78}
79
80impl CodeGenError {
81    pub fn user_message(&self) -> Cow<'_, str> {
82        match self {
83            CodeGenError::VariableNotInScope(name) => {
84                Cow::Owned(format!("Use of variable '{name}' outside of its scope"))
85            }
86            CodeGenError::VariableUnavailable(message) => Cow::Borrowed(message),
87            CodeGenError::TypeSizeNotAvailable(name) => Cow::Owned(format!(
88                "Variable '{name}' has no concrete DWARF size at this probe PC"
89            )),
90            _ => Cow::Owned(self.to_string()),
91        }
92    }
93}
94
95// Public re-exports from script::compiler module
96pub use script::compiler::{CompilationResult, UProbeConfig};
97
98/// Event output map type for eBPF tracing
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum EventMapType {
101    /// BPF_MAP_TYPE_RINGBUF (requires kernel >= 5.8)
102    RingBuf,
103    /// BPF_MAP_TYPE_PERF_EVENT_ARRAY (kernel >= 4.3, fallback)
104    PerfEventArray,
105}
106
107/// Compilation options including save options and eBPF map configuration
108#[derive(Debug, Clone)]
109pub struct CompileOptions {
110    pub save_llvm_ir: bool,
111    pub save_ebpf: bool,
112    pub save_ast: bool,
113    pub binary_path_hint: Option<String>,
114    /// Explicit `-t` target path. When present, trace target resolution is
115    /// scoped to this module even if DWARF was loaded from a `-p` PID.
116    pub target_binary_path: Option<String>,
117    pub ringbuf_size: u64,
118    pub proc_module_offsets_max_entries: u64,
119    /// Fixed capacity for DWARF compact CFI rows used by `bt`.
120    pub backtrace_unwind_rows_max_entries: u32,
121    pub perf_page_count: u32,
122    pub event_map_type: EventMapType,
123    /// Max bytes to read per memory-dump argument (format {:x}/{:s}).
124    pub mem_dump_cap: u32,
125    /// Max bytes to compare for string/memory comparisons (strncmp/starts_with/memcmp)
126    pub compare_cap: u32,
127    /// Max total bytes in a single trace event (used for PerfEventArray accumulation buffer size).
128    pub max_trace_event_size: u32,
129    /// Max DWARF-unwound frames captured by each `bt`/`backtrace` instruction.
130    pub backtrace_depth: u8,
131    /// Optional single-address filter: if set, only the Nth (1-based) address
132    /// resolved for a target will be compiled. When None, compile all.
133    pub selected_index: Option<usize>,
134    /// Optional PID filter strategy override.
135    /// When None, compiler falls back to HostTgid using compile_script(pid).
136    pub pid_filter_spec: Option<PidFilterSpec>,
137    /// Optional PID namespace context used by special vars like `$pid`/`$tid`.
138    /// This is independent of PID filtering and is primarily for `-t` mode.
139    pub special_pid_ns: Option<PidNamespaceId>,
140    /// Optional PID namespace context used by `proc_module_offsets` lookups.
141    ///
142    /// In `-p` mode this only switches to the target PID-namespace view when
143    /// GhostScope has an explicit namespace-local target PID that can be
144    /// aliased back to the `/proc` key used to populate
145    /// `proc_module_offsets`. Otherwise it stays on GhostScope's current
146    /// `/proc`-visible PID view.
147    ///
148    /// In `-t` mode we continue to use GhostScope's own `/proc` view, because
149    /// offsets are discovered from `/proc/<pid>/maps` in that namespace.
150    pub proc_offsets_pid_ns: Option<PidNamespaceId>,
151    /// Optional original `-p` input PID for `$input_pid`.
152    /// This is only available in `-p` mode.
153    pub input_pid: Option<u32>,
154    /// Runtime/backend capabilities used to validate DWARF variable read plans.
155    pub runtime_capabilities: RuntimeCapabilities,
156}
157
158impl Default for CompileOptions {
159    fn default() -> Self {
160        Self {
161            save_llvm_ir: false,
162            save_ebpf: false,
163            save_ast: false,
164            binary_path_hint: None,
165            target_binary_path: None,
166            ringbuf_size: 262144,                  // 256KB
167            proc_module_offsets_max_entries: 4096, // Default
168            backtrace_unwind_rows_max_entries: DEFAULT_BACKTRACE_UNWIND_ROWS_MAX_ENTRIES,
169            perf_page_count: 64,                   // 64 pages = 256KB per CPU
170            event_map_type: EventMapType::RingBuf, // Default to RingBuf
171            mem_dump_cap: 256,                     // Default per-arg dump cap (bytes)
172            compare_cap: 64,                       // Default compare cap for strncmp/memcmp (bytes)
173            max_trace_event_size: 32768,           // Default event size cap (32KB)
174            backtrace_depth: DEFAULT_BACKTRACE_DEPTH,
175            selected_index: None,
176            pid_filter_spec: None,
177            special_pid_ns: None,
178            proc_offsets_pid_ns: None,
179            input_pid: None,
180            runtime_capabilities: RuntimeCapabilities::default(),
181        }
182    }
183}
184
185pub const DEFAULT_BACKTRACE_DEPTH: u8 = 128;
186pub const MAX_BACKTRACE_DEPTH: u8 = 128;
187pub const DEFAULT_BACKTRACE_UNWIND_ROWS_MAX_ENTRIES: u32 = 65_536;
188pub const MIN_BACKTRACE_UNWIND_ROWS_MAX_ENTRIES: u32 = 1_024;
189pub const MAX_BACKTRACE_UNWIND_ROWS_MAX_ENTRIES: u32 = 1_048_576;
190
191pub fn module_cookie_for_path(module_path: &str) -> u64 {
192    module_probe::cookie_for_path(module_path)
193}
194
195pub fn backtrace_unwind_row_from_compact(
196    row: &CompactUnwindRow,
197) -> Option<ghostscope_protocol::BacktraceUnwindRow> {
198    if !row.bpf_supported {
199        return None;
200    }
201    let CfaRulePlan::RegPlusOffset {
202        register,
203        offset: cfa_offset,
204    } = &row.cfa
205    else {
206        return None;
207    };
208    if !backtrace_supported_state_register(*register) {
209        return None;
210    }
211
212    let mut wire = ghostscope_protocol::BacktraceUnwindRow {
213        pc_start: row.pc_start,
214        pc_end: row.pc_end,
215        cfa_offset: *cfa_offset,
216        cfa_register: *register,
217        ..Default::default()
218    };
219
220    match &row.return_address {
221        RegisterRecoveryPlan::AtCfaOffset { offset } => {
222            wire.ra_kind = BACKTRACE_RECOVERY_AT_CFA_OFFSET;
223            wire.ra_offset = *offset;
224            wire.ra_register = row.return_address_register;
225        }
226        _ => return None,
227    }
228
229    match row.rbp.as_ref() {
230        Some(RegisterRecoveryPlan::AtCfaOffset { offset }) => {
231            wire.rbp_kind = BACKTRACE_RECOVERY_AT_CFA_OFFSET;
232            wire.rbp_offset = *offset;
233            wire.rbp_register = X86_64_DWARF_RBP;
234        }
235        Some(RegisterRecoveryPlan::ValCfaOffset { offset }) => {
236            wire.rbp_kind = BACKTRACE_RECOVERY_VAL_CFA_OFFSET;
237            wire.rbp_offset = *offset;
238            wire.rbp_register = X86_64_DWARF_RBP;
239        }
240        Some(RegisterRecoveryPlan::Register { register }) => {
241            if !backtrace_supported_state_register(*register) {
242                return None;
243            }
244            wire.rbp_kind = BACKTRACE_RECOVERY_REGISTER;
245            wire.rbp_register = *register;
246        }
247        Some(RegisterRecoveryPlan::SameValue { register }) => {
248            if !backtrace_supported_state_register(*register) {
249                return None;
250            }
251            wire.rbp_kind = BACKTRACE_RECOVERY_SAME_VALUE;
252            wire.rbp_register = *register;
253        }
254        Some(RegisterRecoveryPlan::Undefined) | None => {
255            wire.rbp_kind = BACKTRACE_RECOVERY_SAME_VALUE;
256            wire.rbp_register = X86_64_DWARF_RBP;
257        }
258        _ => return None,
259    }
260
261    Some(wire)
262}
263
264fn backtrace_supported_state_register(register: u16) -> bool {
265    matches!(
266        register,
267        X86_64_DWARF_RIP | X86_64_DWARF_RBP | X86_64_DWARF_RSP
268    )
269}
270
271/// Main compilation interface with DwarfAnalyzer (multi-module support)
272///
273/// This is the new multi-module interface that uses DwarfAnalyzer
274/// to perform compilation across main executable and dynamic libraries
275pub fn compile_script(
276    script_source: &str,
277    process_analyzer: &ghostscope_dwarf::DwarfAnalyzer,
278    pid: Option<u32>,
279    trace_id: Option<u32>,
280    compile_options: &CompileOptions,
281) -> Result<CompilationResult> {
282    info!("Starting unified script compilation with DwarfAnalyzer (multi-module support)");
283
284    // Step 1: Parse script to AST
285    let program = script::parser::parse(script_source)?;
286    info!("Parsed script with {} statements", program.statements.len());
287
288    // Step 2: Use AstCompiler with full DwarfAnalyzer integration
289    let mut compiler = AstCompiler::new(
290        Some(process_analyzer),
291        compile_options.binary_path_hint.clone(),
292        trace_id.unwrap_or(0), // Default starting trace_id is 0 if not provided
293        compile_options.clone(),
294    );
295
296    // Step 3: Compile using unified interface
297    let result = compiler.compile_program(&program, pid)?;
298
299    if result.uprobe_configs.is_empty() {
300        if !result.failed_targets.is_empty() {
301            tracing::warn!(
302                "Compilation produced 0 uprobe configs; {} target(s) failed to compile",
303                result.failed_targets.len()
304            );
305        } else {
306            tracing::warn!(
307                "Compilation completed with 0 uprobe configs (no attachable targets resolved)"
308            );
309        }
310    } else {
311        info!(
312            "Successfully compiled script: {} trace points, {} uprobe configs",
313            result.trace_count,
314            result.uprobe_configs.len()
315        );
316    }
317
318    // Concise summary for downstream logs
319    info!(
320        "Compilation summary: trace_points={}, uprobe_configs={}, failed_targets={}",
321        result.trace_count,
322        result.uprobe_configs.len(),
323        result.failed_targets.len()
324    );
325
326    Ok(result)
327}
328
329/// Print AST for debugging
330pub fn print_ast(program: &crate::script::Program) {
331    info!("\n=== AST Tree ===");
332    info!("Program:");
333    for (i, stmt) in program.statements.iter().enumerate() {
334        info!("  Statement {}: {:?}", i, stmt);
335    }
336    info!("=== End AST Tree ===\n");
337}
338
339/// Save AST to file
340pub fn save_ast_to_file(program: &crate::script::Program, filename: &str) -> Result<()> {
341    let mut ast_content = String::new();
342    ast_content.push_str("=== AST Tree ===\n");
343    ast_content.push_str("Program:\n");
344    for (i, stmt) in program.statements.iter().enumerate() {
345        ast_content.push_str(&format!("  Statement {i}: {stmt:?}\n"));
346    }
347    ast_content.push_str("=== End AST Tree ===\n");
348
349    let file_path = format!("{filename}.txt");
350    std::fs::write(&file_path, ast_content)
351        .map_err(|e| CompileError::Other(format!("Failed to save AST file '{file_path}': {e}")))?;
352
353    Ok(())
354}
355
356/// Format eBPF bytecode as hexadecimal string for inspection
357pub fn format_ebpf_bytecode(bytecode: &[u8]) -> String {
358    bytecode
359        .iter()
360        .map(|byte| format!("{byte:02x}"))
361        .collect::<Vec<String>>()
362        .join(" ")
363}
364
365/// Generate filename for AST files
366pub fn generate_file_name_for_ast(pid: Option<u32>, binary_path: Option<&str>) -> String {
367    let pid_part = pid
368        .map(|p| p.to_string())
369        .unwrap_or_else(|| "unknown".to_string());
370    let exec_part = binary_path
371        .and_then(|path| std::path::Path::new(path).file_name())
372        .and_then(|name| name.to_str())
373        .unwrap_or("unknown");
374
375    format!("gs_{pid_part}_{exec_part}_ast")
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn user_message_strips_codegen_prefix_for_unavailable_variable() {
384        let err = CompileError::CodeGen(CodeGenError::VariableUnavailable(
385            "'x' is optimized out at the selected probe PC".to_string(),
386        ));
387
388        assert_eq!(
389            err.user_message().as_ref(),
390            "'x' is optimized out at the selected probe PC"
391        );
392    }
393
394    #[test]
395    fn user_message_formats_scope_errors_for_users() {
396        let err = CompileError::CodeGen(CodeGenError::VariableNotInScope("x".to_string()));
397
398        assert_eq!(
399            err.user_message().as_ref(),
400            "Use of variable 'x' outside of its scope"
401        );
402    }
403
404    #[test]
405    fn backtrace_unwind_row_layout_matches_bpf_map_value() {
406        assert_eq!(BACKTRACE_UNWIND_ROW_SIZE, 56);
407        assert_eq!(BACKTRACE_UNWIND_ROW_PC_START_OFFSET, 0);
408        assert_eq!(BACKTRACE_UNWIND_ROW_PC_END_OFFSET, 8);
409        assert_eq!(BACKTRACE_UNWIND_ROW_CFA_OFFSET_OFFSET, 16);
410        assert_eq!(BACKTRACE_UNWIND_ROW_RA_OFFSET_OFFSET, 24);
411        assert_eq!(BACKTRACE_UNWIND_ROW_RBP_OFFSET_OFFSET, 32);
412        assert_eq!(BACKTRACE_UNWIND_ROW_CFA_REGISTER_OFFSET, 40);
413        assert_eq!(BACKTRACE_UNWIND_ROW_RA_REGISTER_OFFSET, 42);
414        assert_eq!(BACKTRACE_UNWIND_ROW_RBP_REGISTER_OFFSET, 44);
415        assert_eq!(BACKTRACE_UNWIND_ROW_RA_KIND_OFFSET, 46);
416        assert_eq!(BACKTRACE_UNWIND_ROW_RBP_KIND_OFFSET, 47);
417    }
418
419    #[test]
420    fn backtrace_tail_call_state_layout_matches_bpf_accessors() {
421        assert_eq!(BACKTRACE_TAIL_STATE_SIZE, 64);
422        assert_eq!(BACKTRACE_TAIL_STATE_CURRENT_IP_OFFSET, 0);
423        assert_eq!(BACKTRACE_TAIL_STATE_CURRENT_RSP_OFFSET, 8);
424        assert_eq!(BACKTRACE_TAIL_STATE_CURRENT_RBP_OFFSET, 16);
425        assert_eq!(BACKTRACE_TAIL_STATE_MODULE_BIAS_OFFSET, 24);
426        assert_eq!(BACKTRACE_TAIL_STATE_MODULE_COOKIE_OFFSET, 32);
427        assert_eq!(BACKTRACE_TAIL_STATE_INST_OFFSET_OFFSET, 40);
428        assert_eq!(BACKTRACE_TAIL_STATE_EVENT_SIZE_OFFSET, 44);
429        assert_eq!(BACKTRACE_TAIL_STATE_FRAME_COUNT_OFFSET, 48);
430        assert_eq!(BACKTRACE_TAIL_STATE_REQUESTED_DEPTH_OFFSET, 49);
431        assert_eq!(BACKTRACE_TAIL_STATE_OFFSETS_FOUND_OFFSET, 50);
432        assert_eq!(BACKTRACE_TAIL_STATE_TAIL_CALLS_OFFSET, 51);
433        assert_eq!(BACKTRACE_TAIL_STATE_FLAGS_OFFSET, 52);
434        assert_eq!(BACKTRACE_TAIL_STATE_ACTIVE_SLOT_OFFSET, 53);
435        assert_eq!(BACKTRACE_TAIL_STATE_ERROR_CODE_OFFSET, 54);
436        assert_eq!(BACKTRACE_TAIL_STATE_NEXT_SLOT_OFFSET, 56);
437    }
438}