stackdump_trace/platform/
mod.rs

1use crate::{error::TraceError, type_value_tree::TypeValueTree, Frame, FrameType, Location};
2use funty::Fundamental;
3use gimli::{DebugInfoOffset, EndianRcSlice, RunTimeEndian};
4use object::{Object, ObjectSection, ObjectSymbol, SectionKind};
5use stackdump_core::{device_memory::DeviceMemory, memory_region::VecMemoryRegion};
6use std::collections::HashMap;
7
8pub mod cortex_m;
9
10/// The result of an unwinding procedure
11pub enum UnwindResult<ADDR: funty::Integral> {
12    /// The unwinding is done up to the start of the program
13    Finished,
14    /// The unwinding can't continue because the stack is corrupted
15    Corrupted {
16        /// An optional frame that explains the corruption
17        error_frame: Option<Frame<ADDR>>,
18    },
19    /// The unwinding took another step and is not yet finished
20    Proceeded,
21}
22
23pub trait Platform<'data> {
24    type Word: funty::Integral;
25
26    fn create_context(elf: &object::File<'data, &'data [u8]>) -> Result<Self, TraceError>
27    where
28        Self: Sized;
29
30    /// Unwind the stack of the platform to the previous exception if possible
31    ///
32    /// The device memory is mutated so that it is brought back to the state it was before the previous exception.
33    ///
34    /// Based on the unwinding, new information about the previous frame can be discovered.
35    /// In that case, that frame can be updated with that info.
36    fn unwind(
37        &mut self,
38        device_memory: &mut DeviceMemory<Self::Word>,
39        previous_frame: Option<&mut Frame<Self::Word>>,
40    ) -> Result<UnwindResult<Self::Word>, TraceError>;
41}
42
43/// Create the stacktrace for the given platform.
44///
45/// - device_memory: All the captured memory of the device.
46///   It is not necessary to include any data that is present in the elf file because that will automatically be added.
47///   It is required to have a decent chunk of the stack present. If not all of the stack is present,
48///   then eventually the tracing procedure will find a corrupt frame.
49///   The standard set of registers is also required to be present.
50/// - elf_data: The raw bytes of the elf file.
51///   This must be the exact same elf file as the one the device was running. Even a recompilation of the exact same code can change the debug info.
52pub fn trace<'data, P: Platform<'data>>(
53    mut device_memory: DeviceMemory<P::Word>,
54    elf_data: &'data [u8],
55) -> Result<Vec<Frame<P::Word>>, TraceError>
56where
57    <P::Word as funty::Numeric>::Bytes: bitvec::view::BitView<Store = u8>,
58{
59    // Parse the elf data
60    let elf = object::File::parse(elf_data)?;
61
62    // Add all relevant memory sections present in the elf file to the device memory
63    for section in elf.sections().filter(|section| {
64        matches!(
65            section.kind(),
66            SectionKind::Text | SectionKind::ReadOnlyData | SectionKind::ReadOnlyString
67        )
68    }) {
69        device_memory.add_memory_region(VecMemoryRegion::new(
70            section.address(),
71            section.uncompressed_data()?.to_vec(),
72        ));
73    }
74
75    let endian = if elf.is_little_endian() {
76        gimli::RunTimeEndian::Little
77    } else {
78        gimli::RunTimeEndian::Big
79    };
80
81    fn load_section<'data: 'file, 'file, O, Endian>(
82        id: gimli::SectionId,
83        file: &'file O,
84        endian: Endian,
85    ) -> Result<gimli::EndianRcSlice<Endian>, TraceError>
86    where
87        O: object::Object<'data>,
88        Endian: gimli::Endianity,
89    {
90        let data = file
91            .section_by_name(id.name())
92            .and_then(|section| section.uncompressed_data().ok())
93            .unwrap_or(std::borrow::Cow::Borrowed(&[]));
94        Ok(gimli::EndianRcSlice::new(std::rc::Rc::from(&*data), endian))
95    }
96
97    let dwarf = gimli::Dwarf::load(|id| load_section(id, &elf, endian))?;
98
99    // Create the vector we'll be adding our found frames to
100    let mut frames = Vec::new();
101
102    // To find the frames, we need the addr2line context which does a lot of the work for us
103    let addr2line_context =
104        addr2line::Context::from_dwarf(gimli::Dwarf::load(|id| load_section(id, &elf, endian))?)?;
105
106    // To unwind, we need the platform context
107    let mut platform_context = P::create_context(&elf)?;
108
109    let mut type_cache = Default::default();
110
111    // Now we need to keep looping until we unwound to the start of the program
112    loop {
113        // Get the frames of the current state
114        match add_current_frames::<P>(
115            &device_memory,
116            &addr2line_context,
117            &mut frames,
118            &mut type_cache,
119        ) {
120            Ok(_) => {}
121            Err(e @ TraceError::DwarfUnitNotFound { pc: _ }) => {
122                frames.push(Frame {
123                    function: "Unknown".into(),
124                    location: Location::default(),
125                    frame_type: FrameType::Corrupted(e.to_string()),
126                    variables: Vec::default(),
127                });
128                break;
129            }
130            Err(e) => return Err(e),
131        }
132
133        // Try to unwind
134        match platform_context.unwind(&mut device_memory, frames.last_mut())? {
135            UnwindResult::Finished => {
136                frames.push(Frame {
137                    function: "RESET".into(),
138                    location: crate::Location {
139                        file: None,
140                        line: None,
141                        column: None,
142                    },
143                    frame_type: FrameType::Function,
144                    variables: Vec::new(),
145                });
146                break;
147            }
148            UnwindResult::Corrupted {
149                error_frame: Some(error_frame),
150            } => {
151                frames.push(error_frame);
152                break;
153            }
154            UnwindResult::Corrupted { error_frame: None } => {
155                break;
156            }
157            UnwindResult::Proceeded => {
158                continue;
159            }
160        }
161    }
162
163    // We're done with the stack data, but we can also decode the static variables and make a frame out of that
164    let mut static_variables =
165        crate::variables::find_static_variables(&dwarf, &device_memory, &mut type_cache)?;
166
167    // Filter out static variables that are not real (like defmt ones)
168    static_variables.retain(|var| {
169        let Some(linkage_name) = &var.linkage_name else {
170            // For some reason, some variables don't have a linkage name.
171            // So just show them, I guess?
172            return true;
173        };
174
175        if let Some(symbol) = elf.symbol_by_name(linkage_name) {
176            if let Some(section_index) = symbol.section_index() {
177                match elf.section_by_index(section_index) {
178                    // Filter out all weird sections (including defmt)
179                    Ok(section) if section.kind() == SectionKind::Other => false,
180                    Ok(_section) => true,
181                    Err(e) => {
182                        log::error!("Could not get section by index: {e}");
183                        true
184                    }
185                }
186            } else {
187                // The symbol is not defined in a section?
188                // Idk man, just show it I guess
189                true
190            }
191        } else {
192            // We have a linkage name from debug info, but the symbol doesn't exist...
193            // There's two things that might be going on that I know about:
194            // 1. LTO ran and removed the symbol because it was never used.
195            // 2. LLVM merged some globals (including this one) into one symbol.
196            //
197            // If 1, we want to return false. If 2, we want to return true.
198
199            // For 1, if the variable has an address, it tends to be address 0 as far as I can see.
200            // This makes sense because it doesn't exist, and so doesn't have a 'real' address.
201
202            if var.address.is_none() || var.address == Some(0) {
203                // We're likely in number 1 territory
204                false
205            } else {
206                // We _may_ be in number 2 territory
207                true
208            }
209        }
210    });
211
212    let static_frame = Frame {
213        function: "Static".into(),
214        location: Location {
215            file: None,
216            line: None,
217            column: None,
218        },
219        frame_type: FrameType::Static,
220        variables: static_variables,
221    };
222    frames.push(static_frame);
223
224    // We're done
225    Ok(frames)
226}
227
228fn add_current_frames<'a, P: Platform<'a>>(
229    device_memory: &DeviceMemory<P::Word>,
230    addr2line_context: &addr2line::Context<EndianRcSlice<RunTimeEndian>>,
231    frames: &mut Vec<Frame<P::Word>>,
232    type_cache: &mut HashMap<DebugInfoOffset, Result<TypeValueTree<P::Word>, TraceError>>,
233) -> Result<(), TraceError>
234where
235    <P::Word as funty::Numeric>::Bytes: bitvec::view::BitView<Store = u8>,
236{
237    // Find the frames of the current register context
238    let mut context_frames = addr2line_context
239        .find_frames(device_memory.register(gimli::Arm::PC)?.as_u64())
240        .skip_all_loads()?;
241
242    // Get the debug compilation unit of the current register context
243    let unit_ref = addr2line_context
244        .find_dwarf_and_unit(device_memory.register(gimli::Arm::PC)?.as_u64())
245        .skip_all_loads()
246        .ok_or(TraceError::DwarfUnitNotFound {
247            pc: device_memory.register(gimli::Arm::PC)?.as_u64(),
248        })?;
249
250    // Get the abbreviations of the unit
251    let abbreviations = unit_ref.dwarf.abbreviations(&unit_ref.header)?;
252
253    // Loop through the found frames and add them
254    let mut added_frames = 0;
255    while let Some(context_frame) = context_frames.next()? {
256        let (file, line, column) = context_frame
257            .location
258            .map(|l| {
259                (
260                    l.file.map(|f| f.to_string()),
261                    l.line.map(|line| line as _),
262                    l.column.map(|column| column as _),
263                )
264            })
265            .unwrap_or_default();
266
267        let mut variables = Vec::new();
268
269        if let Some(die_offset) = context_frame.dw_die_offset {
270            let mut entries = match unit_ref
271                .header
272                .entries_tree(&abbreviations, Some(die_offset))
273            {
274                Ok(entries) => entries,
275                Err(_) => {
276                    continue;
277                }
278            };
279
280            if let Ok(entry_root) = entries.root() {
281                variables = crate::variables::find_variables_in_function(
282                    unit_ref.dwarf,
283                    unit_ref.unit,
284                    &abbreviations,
285                    device_memory,
286                    entry_root,
287                    type_cache,
288                )?;
289            }
290        }
291
292        frames.push(Frame {
293            function: context_frame
294                .function
295                .and_then(|f| f.demangle().ok().map(|f| f.into_owned()))
296                .unwrap_or_else(|| "UNKNOWN".into()),
297            location: crate::Location { file, line, column },
298            frame_type: FrameType::InlineFunction,
299            variables,
300        });
301
302        added_frames += 1;
303    }
304
305    if added_frames > 0 {
306        // The last frame of `find_frames` is always a real function. All frames before are inline functions.
307        frames.last_mut().unwrap().frame_type = FrameType::Function;
308    }
309
310    Ok(())
311}