Skip to main content

sys_rs/
profile.rs

1use nix::{
2    sys::{
3        ptrace,
4        signal::Signal,
5        wait::{waitpid, WaitStatus},
6    },
7    unistd::Pid,
8};
9
10use crate::{
11    asm,
12    diag::Result,
13    hwaccess::Registers,
14    print::{self, PrintFn},
15    process,
16    progress::{self, Execution, Mode, ProgressFn, State},
17    trace::terminated,
18};
19
20/// Holds resources needed to parse and trace a target binary.
21///
22/// `Tracer` owns the path to the traced executable and an `asm::Parser`
23/// used to decode instructions from raw bytes.
24pub struct Tracer {
25    path: String,
26    parser: asm::Parser,
27}
28
29impl Tracer {
30    /// Creates a new `Tracer` instance.
31    ///
32    /// # Arguments
33    ///
34    /// * `path` - The path to the file.
35    ///
36    /// # Errors
37    ///
38    /// Returns an `Err` if it fails to build `asm::Parser`.
39    ///
40    /// # Returns
41    ///
42    /// Returns a `Result` containing the newly created `Tracer` instance.
43    pub fn new(path: &str) -> Result<Self> {
44        Ok(Self {
45            path: path.to_string(),
46            parser: asm::Parser::new()?,
47        })
48    }
49
50    #[must_use]
51    /// Return the path to the traced executable as a `&str`.
52    ///
53    /// # Returns
54    ///
55    /// A `&str` reference to the stored path of the traced executable.
56    pub fn path(&self) -> &str {
57        &self.path
58    }
59}
60
61fn init_tracer(process: &process::Info, state: &mut State, pid: Pid) -> Result<()> {
62    state
63        .breakpoint_mgr()
64        .set_breakpoint(*process.entry()?, true, false, None)?;
65    ptrace::cont(pid, None)?;
66
67    Ok(())
68}
69
70fn handle_mode(
71    process: &process::Info,
72    state: &mut State,
73    pid: Pid,
74) -> Result<bool> {
75    match state.mode() {
76        Mode::Continue => {
77            state.set_mode(Mode::StepInto);
78            ptrace::cont(pid, None)?;
79            println!("Continuing");
80            Ok(true)
81        }
82        Mode::StepOverInProgress => {
83            state.set_mode(Mode::StepInto);
84            skip_func(process, state, pid)?;
85            Ok(true)
86        }
87        _ => Ok(false),
88    }
89}
90
91fn handle_step_over(instr: &asm::Instruction, state: &mut State) -> bool {
92    let mut ret = false;
93
94    if let Mode::StepOver = state.mode() {
95        if instr.is_call() {
96            state.set_execution(Execution::Run);
97            state.set_mode(Mode::StepOverInProgress);
98            ret = true;
99        } else {
100            state.set_mode(Mode::StepInto);
101        }
102    }
103
104    ret
105}
106
107fn handle_sigtrap(
108    context: &Tracer,
109    process: &process::Info,
110    state: &mut State,
111    pid: Pid,
112    last_instr: &mut Option<asm::Instruction>,
113    startup_complete: &mut bool,
114    print: &mut impl PrintFn,
115) -> Result<bool> {
116    let mut regs = Registers::read(pid)?;
117    if let Some(bp) = state.breakpoint_mgr().handle_breakpoint(&mut regs)? {
118        let addr = bp.address();
119        if state
120            .breakpoint_mgr()
121            .set_breakpoint(addr, false, true, bp.id())
122            .is_err()
123        {
124            eprintln!("Failed to set breakpoint at address {addr:#x}");
125        }
126    }
127
128    let rip = regs.rip();
129    let mut ret = false;
130
131    if let Some(opcode) = process.get_opcode_from_addr(rip)? {
132        let instr = context.parser.get_instruction_from(opcode, rip)?;
133        state.set_printed(print(&instr, state.layout())?);
134        state.set_prev_rip(rip);
135        *last_instr = Some(instr);
136    } else if let Some(instr) = last_instr.as_ref() {
137        // The purpose of this scope is to prevent single stepping through libraries
138        // execution as a non-neglible amount of time is spent in those.
139        if instr.is_call() {
140            // Keep single stepping after the first call as it is likely to be part
141            // of the startup routine so it might never return.
142            if *startup_complete {
143                skip_func(process, state, pid)?;
144                ret = true;
145            } else {
146                *startup_complete = true;
147            }
148        }
149
150        if !ret {
151            *last_instr = None;
152        }
153    }
154
155    if !ret {
156        state.breakpoint_mgr().save_breakpoint(rip)?;
157        ptrace::step(pid, None)?;
158        state.set_execution(Execution::Run);
159    }
160
161    Ok(ret)
162}
163
164fn do_progress_ui(
165    instr: &asm::Instruction,
166    state: &mut State,
167    print: &mut impl PrintFn,
168    progress: &mut impl ProgressFn,
169) -> Result<()> {
170    progress(state)?;
171
172    if let Some(layout) = state.take_requested_layout() {
173        state.set_layout(layout);
174        if let Some(line) = print(instr, state.layout())? {
175            state.set_printed(Some(line));
176        }
177    }
178
179    Ok(())
180}
181
182fn skip_func(process: &process::Info, state: &mut State, pid: Pid) -> Result<()> {
183    let regs = Registers::read(pid)?;
184    let ret_addr =
185        u64::try_from(ptrace::read(pid, regs.rsp() as ptrace::AddressType)?)?;
186
187    if process.is_addr_in_section(ret_addr, ".text") {
188        state
189            .breakpoint_mgr()
190            .set_breakpoint(ret_addr, true, false, None)?;
191        ptrace::cont(pid, None)?;
192        state.set_execution(Execution::Run);
193    }
194
195    Ok(())
196}
197
198/// Traces the execution of a process and prints the instructions being executed.
199///
200/// This function owns the tracer loop: it drives the traced child using
201/// ptrace, restores and manages breakpoints, decodes instructions with the
202/// provided `Tracer`, and uses the supplied `print` and `progress` callbacks
203/// to render output and update the tracing UI.
204///
205/// # Arguments
206///
207/// * `context` - The `Tracer` instance used to decode instructions and hold
208///   tracer-local resources (e.g., an `asm::Parser`).
209/// * `process` - The `process::Info` describing the traced process (provides
210///   PID, sections, entry point, etc.).
211/// * `state` - A `progress::State` instance which holds runtime tracing state
212///   such as breakpoints, layout, and execution mode.
213/// * `print` - A printing callback used to render a single `asm::Instruction`.
214/// * `progress` - A progress/update callback invoked each trace iteration to
215///   update UI state and possibly change tracing behavior.
216///
217/// # Errors
218///
219/// Returns an `Err` if any underlying operation fails: ELF/DWARF parsing,
220/// ptrace/syscall errors, register reads/writes, or instruction decoding.
221///
222/// # Returns
223///
224/// Returns `Ok(exit_code)` where `exit_code` is the child's exit status when
225/// tracing finishes, or an `Err` on failure.
226pub fn trace_with(
227    context: &Tracer,
228    process: &process::Info,
229    state: State,
230    mut print: impl PrintFn,
231    mut progress: impl ProgressFn,
232) -> Result<i32> {
233    let pid = process.pid();
234    let mut state = state;
235
236    let mut ret = 0;
237    let mut startup_complete = false;
238    let mut last_instr: Option<asm::Instruction> = None;
239
240    init_tracer(process, &mut state, pid)?;
241
242    loop {
243        if let Execution::Run = state.execution() {
244            let status = waitpid(pid, None)?;
245            state.breakpoint_mgr().restore_breakpoint()?;
246
247            if handle_mode(process, &mut state, pid)? {
248                continue;
249            }
250
251            if let Some(code) = terminated(status) {
252                ret = code;
253                break;
254            }
255
256            match status {
257                WaitStatus::Stopped(_, Signal::SIGTRAP) => {
258                    if handle_sigtrap(
259                        context,
260                        process,
261                        &mut state,
262                        pid,
263                        &mut last_instr,
264                        &mut startup_complete,
265                        &mut print,
266                    )? {
267                        continue;
268                    }
269                }
270                WaitStatus::Stopped(_, signal) => {
271                    ptrace::cont(pid, signal)?;
272                    state.set_execution(Execution::Run);
273                }
274                _ => {}
275            }
276        }
277
278        let Some(instr) = last_instr.as_ref() else {
279            continue;
280        };
281
282        if handle_step_over(instr, &mut state) {
283            continue;
284        }
285
286        if state.printed().is_some() {
287            do_progress_ui(instr, &mut state, &mut print, &mut progress)?;
288        }
289
290        if let Execution::Exit = state.execution() {
291            break;
292        }
293    }
294
295    Ok(ret)
296}
297
298/// Convenience wrapper around `trace_with` that uses the default print and
299/// progress callbacks.
300///
301/// This function allocates a fresh `progress::State` and then calls
302/// `trace_with` with `print::default` and `progress::default` to perform the
303/// trace.
304///
305/// # Arguments
306///
307/// * `context` - The `Tracer` used to decode instructions.
308/// * `process` - The traced `process::Info` describing the child process.
309///
310/// # Returns
311///
312/// `Ok(exit_code)` where `exit_code` is the child's exit status when tracing finishes, or `Err` on failure.
313///
314/// # Errors
315///
316/// Returns an `Err` if any underlying operation fails, such as ELF/DWARF parsing,
317/// ptrace/syscall errors, register reads/writes, or instruction decoding.
318pub fn trace_with_default_print(
319    context: &Tracer,
320    process: &process::Info,
321) -> Result<i32> {
322    let state = State::new(process.pid(), None);
323    trace_with(context, process, state, print::default, progress::default)
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    use nix::unistd::Pid;
331
332    use crate::{asm::Parser, print::Layout};
333
334    #[test]
335    fn test_tracer_new() {
336        let path = "/path/to/file";
337        let tracer = Tracer::new(path).expect("Failed to create Tracer instance");
338        assert_eq!(tracer.path(), path);
339    }
340
341    #[test]
342    fn test_handle_step_over_call_and_noncall() {
343        let parser = Parser::new().expect("parser");
344        let opcode_call: [u8; 5] = [0xe8, 0x05, 0x00, 0x00, 0x00];
345        let call_inst = parser.get_instruction_from(&opcode_call, 0x1000).unwrap();
346
347        let opcode_nop: [u8; 1] = [0x90];
348        let nop_inst = parser.get_instruction_from(&opcode_nop, 0x2000).unwrap();
349
350        let mut state = progress::State::new(Pid::from_raw(1), None);
351        state.set_mode(progress::Mode::StepOver);
352
353        let ret = handle_step_over(&call_inst, &mut state);
354        assert!(ret);
355        assert!(matches!(state.execution(), progress::Execution::Run));
356        assert!(matches!(state.mode(), progress::Mode::StepOverInProgress));
357
358        state.set_mode(progress::Mode::StepOver);
359        let ret = handle_step_over(&nop_inst, &mut state);
360        assert!(!ret);
361        assert!(matches!(state.mode(), progress::Mode::StepInto));
362    }
363
364    #[test]
365    fn test_do_progress_ui_applies_requested_layout_and_records_print() {
366        let parser = Parser::new().expect("parser");
367        let opcode_call: [u8; 5] = [0xe8, 0x05, 0x00, 0x00, 0x00];
368        let inst = parser.get_instruction_from(&opcode_call, 0x1000).unwrap();
369
370        let mut state = progress::State::new(Pid::from_raw(1), None);
371        state.set_requested_layout(Layout::Source);
372
373        let mut progress_called = false;
374        let mut print = |_: &asm::Instruction,
375                         _layout: &print::Layout|
376         -> crate::diag::Result<Option<String>> {
377            Ok(Some("printed line".to_string()))
378        };
379
380        let mut progress_fn = |_s: &mut State| -> crate::diag::Result<()> {
381            progress_called = true;
382            Ok(())
383        };
384
385        do_progress_ui(&inst, &mut state, &mut print, &mut progress_fn)
386            .expect("do_progress_ui failed");
387
388        assert!(progress_called);
389        assert!(state.printed().is_some());
390        assert_eq!(state.printed().unwrap(), "printed line");
391        assert!(matches!(state.layout(), print::Layout::Source));
392    }
393}