py_spy/
stack_trace.rs

1use std::sync::Arc;
2
3use anyhow::{Context, Error, Result};
4
5use remoteprocess::{Pid, ProcessMemory};
6use serde_derive::Serialize;
7
8use crate::config::{Config, LineNo};
9use crate::python_data_access::{copy_bytes, copy_string};
10use crate::python_interpreters::{
11    CodeObject, FrameObject, InterpreterState, ThreadState, TupleObject,
12};
13
14/// Call stack for a single python thread
15#[derive(Debug, Clone, Serialize)]
16pub struct StackTrace {
17    /// The process id than generated this stack trace
18    pub pid: Pid,
19    /// The python thread id for this stack trace
20    pub thread_id: u64,
21    // The python thread name for this stack trace
22    pub thread_name: Option<String>,
23    /// The OS thread id for this stack tracee
24    pub os_thread_id: Option<u64>,
25    /// Whether or not the thread was active
26    pub active: bool,
27    /// Whether or not the thread held the GIL
28    pub owns_gil: bool,
29    /// The frames
30    pub frames: Vec<Frame>,
31    /// process commandline / parent process info
32    pub process_info: Option<Arc<ProcessInfo>>,
33}
34
35/// Information about a single function call in a stack trace
36#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize)]
37pub struct Frame {
38    /// The function name
39    pub name: String,
40    /// The full filename of the file
41    pub filename: String,
42    /// The module/shared library the
43    pub module: Option<String>,
44    /// A short, more readable, representation of the filename
45    pub short_filename: Option<String>,
46    /// The line number inside the file (or 0 for native frames without line information)
47    pub line: i32,
48    /// Local Variables associated with the frame
49    pub locals: Option<Vec<LocalVariable>>,
50    /// If this is an entry frame. Each entry frame corresponds to one native frame (Python 3.11)
51    pub is_entry: bool,
52    /// If the last frame was a shim. This is used in Python 3.12+ to detect entry frames.
53    pub is_shim_entry: bool,
54}
55
56#[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize)]
57pub struct LocalVariable {
58    pub name: String,
59    pub addr: usize,
60    pub arg: bool,
61    pub repr: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize)]
65pub struct ProcessInfo {
66    pub pid: Pid,
67    pub command_line: String,
68    pub parent: Option<Box<ProcessInfo>>,
69}
70
71/// Given an InterpreterState, this function returns a vector of stack traces for each thread
72pub fn get_stack_traces<I, P>(
73    interpreter_address: usize,
74    process: &P,
75    threadstate_address: usize,
76    config: Option<&Config>,
77) -> Result<Vec<StackTrace>, Error>
78where
79    I: InterpreterState,
80    P: ProcessMemory,
81{
82    let gil_thread_id = get_gil_threadid::<I, P>(threadstate_address, process)?;
83
84    let threadstate_ptr_ptr = I::threadstate_ptr_ptr(interpreter_address);
85    let mut threads: *const I::ThreadState = process
86        .copy_struct(threadstate_ptr_ptr as usize)
87        .context("Failed to copy PyThreadState head pointer")?;
88
89    let mut ret = Vec::new();
90
91    let lineno = config.map(|c| c.lineno).unwrap_or(LineNo::NoLine);
92    let dump_locals = config.map(|c| c.dump_locals).unwrap_or(0);
93
94    while !threads.is_null() {
95        let thread = process
96            .copy_pointer(threads)
97            .context("Failed to copy PyThreadState")?;
98
99        let mut trace = get_stack_trace(&thread, process, dump_locals > 0, lineno)?;
100        trace.owns_gil = trace.thread_id == gil_thread_id;
101
102        ret.push(trace);
103        // This seems to happen occasionally when scanning BSS addresses for valid interpreters
104        if ret.len() > 4096 {
105            return Err(format_err!("Max thread recursion depth reached"));
106        }
107        threads = thread.next();
108    }
109    Ok(ret)
110}
111
112/// Gets a stack trace for an individual thread
113pub fn get_stack_trace<T, P>(
114    thread: &T,
115    process: &P,
116    copy_locals: bool,
117    lineno: LineNo,
118) -> Result<StackTrace, Error>
119where
120    T: ThreadState,
121    P: ProcessMemory,
122{
123    // TODO: just return frames here? everything else probably should be returned out of scope
124    let mut frames = Vec::new();
125
126    // python 3.11+ has an extra level of indirection to get the Frame from the threadstate
127    let mut frame_address = thread.frame_address();
128    if let Some(addr) = frame_address {
129        frame_address = Some(process.copy_struct(addr)?);
130    }
131
132    let mut frame_ptr = thread.frame(frame_address);
133
134    // We are iterating in reverse, i.e. from last call to first call.
135    // Since Python 3.12, there are shim frames inserted before a block
136    // of Python frames. When we encounter one, update the last frame.
137    let set_last_frame_as_shim_entry = &mut |frames: &mut Vec<Frame>| {
138        if let Some(frame) = frames.last_mut() {
139            frame.is_shim_entry = true;
140        }
141    };
142
143    while !frame_ptr.is_null() {
144        let frame = process
145            .copy_pointer(frame_ptr)
146            .context("Failed to copy PyFrameObject")?;
147
148        let code = process
149            .copy_pointer(frame.code())
150            .context("Failed to copy PyCodeObject")?;
151
152        let filename = copy_string(code.filename(), process).context("Failed to copy filename");
153        let name = copy_string(code.name(), process).context("Failed to copy function name");
154
155        // just skip processing the current frame if we can't load the filename or function name.
156        // this can happen in python 3.13+ since the f_executable isn't guaranteed to be
157        // a PyCodeObject. We could check the type (and mimic the logic of PyCode_Check here)
158        // but that would require extra overhead of reading the ob_type per frame - and we
159        // would also have to figure out what the address of PyCode_Type is (which will be
160        // easier if something like https://github.com/python/cpython/issues/100987#issuecomment-1487227139
161        // is merged )
162        if filename.is_err() || name.is_err() {
163            frame_ptr = frame.back();
164            set_last_frame_as_shim_entry(&mut frames);
165            continue;
166        }
167        let filename = filename?;
168        let name = name?;
169
170        // skip <shim> entries in python 3.12+
171        // Unset file/function name in py3.13 means this is a shim.
172        if filename.is_empty() || filename == "<shim>" {
173            frame_ptr = frame.back();
174            set_last_frame_as_shim_entry(&mut frames);
175            continue;
176        }
177
178        let line = match lineno {
179            LineNo::NoLine => 0,
180            LineNo::First => code.first_lineno(),
181            LineNo::LastInstruction => match get_line_number(&code, frame.lasti(), process) {
182                Ok(line) => line,
183                Err(e) => {
184                    // Failling to get the line number really shouldn't be fatal here, but
185                    // can happen in extreme cases (https://github.com/benfred/py-spy/issues/164)
186                    // Rather than fail set the linenumber to 0. This is used by the native extensions
187                    // to indicate that we can't load a line number and it should be handled gracefully
188                    warn!(
189                        "Failed to get line number from {}.{}: {}",
190                        filename, name, e
191                    );
192                    0
193                }
194            },
195        };
196
197        let locals = if copy_locals {
198            Some(get_locals(&code, frame_ptr, &frame, process)?)
199        } else {
200            None
201        };
202
203        let is_entry = frame.is_entry();
204
205        frames.push(Frame {
206            name,
207            filename,
208            line,
209            short_filename: None,
210            module: None,
211            locals,
212            is_entry,
213            is_shim_entry: false,
214        });
215        if frames.len() > 4096 {
216            return Err(format_err!("Max frame recursion depth reached"));
217        }
218
219        frame_ptr = frame.back();
220    }
221
222    // First frame is always a shim
223    set_last_frame_as_shim_entry(&mut frames);
224
225    Ok(StackTrace {
226        pid: 0,
227        frames,
228        thread_id: thread.thread_id(),
229        thread_name: None,
230        owns_gil: false,
231        active: true,
232        os_thread_id: thread.native_thread_id(),
233        process_info: None,
234    })
235}
236
237impl StackTrace {
238    pub fn status_str(&self) -> &str {
239        match (self.owns_gil, self.active) {
240            (_, false) => "idle",
241            (true, true) => "active+gil",
242            (false, true) => "active",
243        }
244    }
245
246    pub fn format_threadid(&self) -> String {
247        // native threadids in osx are kinda useless, use the pthread id instead
248        #[cfg(target_os = "macos")]
249        return format!("{:#X}", self.thread_id);
250
251        // otherwise use the native threadid if given
252        #[cfg(not(target_os = "macos"))]
253        match self.os_thread_id {
254            Some(tid) => format!("{}", tid),
255            None => format!("{:#X}", self.thread_id),
256        }
257    }
258}
259
260/// Returns the line number from a PyCodeObject (given the lasti index from a PyFrameObject)
261fn get_line_number<C: CodeObject, P: ProcessMemory>(
262    code: &C,
263    lasti: i32,
264    process: &P,
265) -> Result<i32, Error> {
266    let table =
267        copy_bytes(code.line_table(), process).context("Failed to copy line number table")?;
268    Ok(code.get_line_number(lasti, &table))
269}
270
271fn get_locals<C: CodeObject, F: FrameObject, P: ProcessMemory>(
272    code: &C,
273    frameptr: *const F,
274    frame: &F,
275    process: &P,
276) -> Result<Vec<LocalVariable>, Error> {
277    let local_count = code.nlocals() as usize;
278    let argcount = code.argcount() as usize;
279    let varnames = process.copy_pointer(code.varnames())?;
280
281    let ptr_size = std::mem::size_of::<*const i32>();
282    let locals_addr = frameptr as usize + std::mem::size_of_val(frame) - ptr_size;
283
284    let mut ret = Vec::new();
285
286    for i in 0..local_count {
287        let nameptr: *const C::StringObject =
288            process.copy_struct(varnames.address(code.varnames() as usize, i))?;
289        let name = copy_string(nameptr, process)?;
290        let addr: usize = process.copy_struct(locals_addr + i * ptr_size)?;
291        if addr == 0 {
292            continue;
293        }
294        ret.push(LocalVariable {
295            name,
296            addr,
297            arg: i < argcount,
298            repr: None,
299        });
300    }
301    Ok(ret)
302}
303
304pub fn get_gil_threadid<I: InterpreterState, P: ProcessMemory>(
305    threadstate_address: usize,
306    process: &P,
307) -> Result<u64, Error> {
308    // happens during initialization when checking to see if we have a valid interpreter (before we've figured out the threadstate_address)
309    if threadstate_address == 0 {
310        return Ok(0);
311    }
312
313    let addr = if I::HAS_GIL_RUNTIME_STATE {
314        // get the gilruntimestate - note that this struct is identical between 3.12/3.13
315        let gil_state: crate::python_bindings::v3_13_0::_gil_runtime_state =
316            process.copy_struct(threadstate_address)?;
317        // check to see if the GIL is locked already
318        if gil_state.locked != 0 {
319            gil_state.last_holder as usize
320        } else {
321            0
322        }
323    } else {
324        process.copy_struct::<usize>(threadstate_address)?
325    };
326
327    // if the addr is 0, no thread is currently holding the GIL
328    let threadid = if addr != 0 {
329        let threadstate: I::ThreadState = process.copy_struct(addr)?;
330        threadstate.thread_id()
331    } else {
332        0
333    };
334
335    Ok(threadid)
336}
337
338impl ProcessInfo {
339    pub fn to_frame(&self) -> Frame {
340        Frame {
341            name: format!("process {}:\"{}\"", self.pid, self.command_line),
342            filename: String::from(""),
343            module: None,
344            short_filename: None,
345            line: 0,
346            locals: None,
347            is_entry: true,
348            is_shim_entry: true,
349        }
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use crate::python_bindings::v3_7_0::PyCodeObject;
357    use crate::python_data_access::tests::to_byteobject;
358    use remoteprocess::LocalProcess;
359
360    #[test]
361    fn test_get_line_number() {
362        let mut lnotab = to_byteobject(&[0u8, 1, 10, 1, 8, 1, 4, 1]);
363        let code = PyCodeObject {
364            co_firstlineno: 3,
365            co_lnotab: &mut lnotab.base.ob_base.ob_base,
366            ..Default::default()
367        };
368        let lineno = get_line_number(&code, 30, &LocalProcess).unwrap();
369        assert_eq!(lineno, 7);
370    }
371}