Skip to main content

profiler_get_symbols/
lib.rs

1//! This crate allows obtaining symbol information from binaries and compilation artifacts.
2//! It maps raw code addresses to symbol strings, and, if available, file name + line number
3//! information.
4//! The API was designed for the Firefox profiler.
5//!
6//! The main entry point of this crate is the async `query_api` function, which accepts a
7//! JSON string with the query input. The JSON API matches the API of the [Mozilla
8//! symbolication server ("Tecken")](https://tecken.readthedocs.io/en/latest/symbolication.html).
9//! An alternative JSON-free API is available too, but it is not very ergonomic.
10//!
11//! # Design constraints
12//!
13//! This crate operates under the following design constraints:
14//!
15//!  - Must be usable from JavaScript / WebAssembly: The Firefox profiler runs this code in a
16//!    WebAssembly environment, invoked from a privileged piece of JavaScript code inside Firefox itself.
17//!    This setup allows us to download the profiler-get-symbols wasm bundle on demand, rather than shipping
18//!    it with Firefox, which would increase the Firefox download size for a piece of functionality
19//!    that the vast majority of Firefox users don't need.
20//!  - Performance: We want to be able to obtain symbol data from a fresh build of a locally compiled
21//!    Firefox instance as quickly as possible, without an expensive preprocessing step. The time between
22//!    "finished compilation" and "returned symbol data" should be minimized. This means that symbol
23//!    data needs to be obtained directly from the compilation artifacts rather than from, say, a
24//!    dSYM bundle or a Breakpad .sym file.
25//!  - Must scale to large inputs: This applies to both the size of the API request and the size of the
26//!    object files that need to be parsed: The Firefox profiler will supply anywhere between tens of
27//!    thousands and hundreds of thousands of different code addresses in a single symbolication request.
28//!    Firefox build artifacts such as libxul.so can be multiple gigabytes big, and contain around 300000
29//!    function symbols. We want to serve such requests within a few seconds or less.
30//!  - "Best effort" basis: If only limited symbol information is available, for example from system
31//!    libraries, we want to return whatever limited information we have.
32//!
33//! The WebAssembly requirement means that this crate cannot contain any direct file access.
34//! Instead, all file access is mediated through a `FileAndPathHelper` trait which has to be implemented
35//! by the caller. Furthermore, the API request does not carry any absolute file paths, so the resolution
36//! to absolute file paths needs to be done by the caller as well.
37//!
38//! # Supported formats and data
39//!
40//! This crate supports obtaining symbol data from PE binaries (Windows), PDB files (Windows),
41//! mach-o binaries (including fat binaries) (macOS & iOS), and ELF binaries (Linux, Android, etc.).
42//! For mach-o files it also supports finding debug information in external objects, by following
43//! OSO stabs entries.
44//! It supports gathering both basic symbol information (function name strings) as well as information
45//! based on debug data, i.e. inline callstacks where each frame has a function name, a file name,
46//! and a line number.
47//! For debug data we support both DWARF debug data (inside mach-o and ELF binaries) and PDB debug data.
48//!
49//! # Example
50//!
51//! ```
52//! use profiler_get_symbols::{
53//!     FileContents, FileAndPathHelper, FileAndPathHelperResult, OptionallySendFuture,
54//!     CandidatePathInfo, FileLocation
55//! };
56//! use profiler_get_symbols::debugid::DebugId;
57//!
58//! async fn run_query() -> String {
59//!     let this_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
60//!     let helper = ExampleHelper {
61//!         artifact_directory: this_dir.join("..").join("fixtures").join("win64-ci")
62//!     };
63//!     profiler_get_symbols::query_api(
64//!         "/symbolicate/v5",
65//!         r#"{
66//!             "memoryMap": [
67//!               [
68//!                 "firefox.pdb",
69//!                 "AA152DEB2D9B76084C4C44205044422E1"
70//!               ]
71//!             ],
72//!             "stacks": [
73//!               [
74//!                 [0, 204776],
75//!                 [0, 129423],
76//!                 [0, 244290],
77//!                 [0, 244219]
78//!               ]
79//!             ]
80//!           }"#,
81//!         &helper,
82//!     ).await
83//! }
84//!
85//! struct ExampleHelper {
86//!     artifact_directory: std::path::PathBuf,
87//! }
88//!
89//! impl<'h> FileAndPathHelper<'h> for ExampleHelper {
90//!     type F = Vec<u8>;
91//!     type OpenFileFuture =
92//!         std::pin::Pin<Box<dyn std::future::Future<Output = FileAndPathHelperResult<Self::F>> + 'h>>;
93//!
94//!     fn get_candidate_paths_for_binary_or_pdb(
95//!         &self,
96//!         debug_name: &str,
97//!         _debug_id: &DebugId,
98//!     ) -> FileAndPathHelperResult<Vec<CandidatePathInfo>> {
99//!         Ok(vec![CandidatePathInfo::SingleFile(FileLocation::Path(self.artifact_directory.join(debug_name)))])
100//!     }
101//!
102//!     fn open_file(
103//!         &'h self,
104//!         location: &FileLocation,
105//!     ) -> std::pin::Pin<Box<dyn std::future::Future<Output = FileAndPathHelperResult<Self::F>> + 'h>> {
106//!         async fn read_file_impl(path: std::path::PathBuf) -> FileAndPathHelperResult<Vec<u8>> {
107//!             Ok(std::fs::read(&path)?)
108//!         }
109//!
110//!         let path = match location {
111//!             FileLocation::Path(path) => path.clone(),
112//!             FileLocation::Custom(_) => panic!("Unexpected FileLocation::Custom"),
113//!         };
114//!         Box::pin(read_file_impl(path.to_path_buf()))
115//!     }
116//! }
117//! ```
118
119pub use debugid;
120use debugid::DebugId;
121pub use object;
122pub use pdb_addr2line::pdb;
123
124use object::{macho::FatHeader, read::FileKind};
125use pdb::PDB;
126use serde_json::json;
127
128mod cache;
129mod chunked_read_buffer_manager;
130mod compact_symbol_table;
131mod debugid_util;
132mod dwarf;
133mod elf;
134mod error;
135mod macho;
136mod path_mapper;
137mod shared;
138mod source;
139mod symbolicate;
140mod windows;
141
142pub use crate::cache::{FileByteSource, FileContentsWithChunkedCaching};
143pub use crate::compact_symbol_table::CompactSymbolTable;
144pub use crate::error::{GetSymbolsError, Result};
145use crate::shared::FileContentsWrapper;
146pub use crate::shared::{
147    AddressDebugInfo, CandidatePathInfo, FileAndPathHelper, FileAndPathHelperError,
148    FileAndPathHelperResult, FileContents, FileLocation, FilePath, OptionallySendFuture,
149    SymbolicationQuery, SymbolicationResult, SymbolicationResultKind,
150};
151pub use debugid_util::{debug_id_for_object, DebugIdExt};
152
153pub(crate) fn to_debug_id(breakpad_id: &str) -> Result<DebugId> {
154    DebugId::from_breakpad(breakpad_id)
155        .map_err(|_| GetSymbolsError::InvalidBreakpadId(breakpad_id.to_string()))
156}
157
158/// Returns a symbol table in `CompactSymbolTable` format for the requested binary.
159/// `FileAndPathHelper` must be implemented by the caller, to provide file access.
160pub async fn get_compact_symbol_table<'h>(
161    debug_name: &str,
162    debug_id: DebugId,
163    helper: &'h impl FileAndPathHelper<'h>,
164) -> Result<CompactSymbolTable> {
165    get_symbolication_result(
166        SymbolicationQuery {
167            debug_name,
168            debug_id,
169            result_kind: SymbolicationResultKind::AllSymbols,
170        },
171        helper,
172    )
173    .await
174}
175
176/// A generic method which is used in the implementation of both `get_compact_symbol_table`
177/// and `query_api`. Allows obtaining symbol data for a given binary. The level of detail
178/// is determined by `query.result_kind`: The caller can
179/// either get a regular symbol table, or extended information for a set of addresses, if
180/// the information is present in the found files. See `SymbolicationResultKind` for
181/// more details.
182pub async fn get_symbolication_result<'h, R>(
183    query: SymbolicationQuery<'_>,
184    helper: &'h impl FileAndPathHelper<'h>,
185) -> Result<R>
186where
187    R: SymbolicationResult,
188{
189    let candidate_paths_for_binary = helper
190        .get_candidate_paths_for_binary_or_pdb(query.debug_name, &query.debug_id)
191        .map_err(|e| {
192            GetSymbolsError::HelperErrorDuringGetCandidatePathsForBinaryOrPdb(
193                query.debug_name.to_string(),
194                query.debug_id,
195                e,
196            )
197        })?;
198
199    let mut last_err = None;
200    for candidate_info in candidate_paths_for_binary {
201        let result = match candidate_info {
202            CandidatePathInfo::SingleFile(file_location) => {
203                try_get_symbolication_result_from_path(query.clone(), &file_location, helper).await
204            }
205            CandidatePathInfo::InDyldCache {
206                dyld_cache_path,
207                dylib_path,
208            } => {
209                macho::try_get_symbolication_result_from_dyld_shared_cache(
210                    query.clone(),
211                    &dyld_cache_path,
212                    &dylib_path,
213                    helper,
214                )
215                .await
216            }
217        };
218
219        match result {
220            Ok(result) => return Ok(result),
221            Err(err) => last_err = Some(err),
222        };
223    }
224    Err(last_err.unwrap_or_else(|| {
225        GetSymbolsError::NoCandidatePathForBinary(query.debug_name.to_string(), query.debug_id)
226    }))
227}
228
229/// This is the main API of this crate.
230/// It implements the "Tecken" JSON API, which is also used by the Mozilla symbol server.
231/// It's intended to be used as a drop-in "local symbol server" which gathers its data
232/// directly from file artifacts produced during compilation (rather than consulting
233/// e.g. a database).
234/// The caller needs to implement the `FileAndPathHelper` trait to provide file system access.
235/// The return value is a JSON string.
236///
237/// The following "URLs" are supported:
238///  - `/symbolicate/v5`: This API is documented at <https://tecken.readthedocs.io/en/latest/symbolication.html>.
239///    The returned data has two extra fields: inlines (per address) and module_errors (per job).
240///  - `/symbolicate/v5-legacy`: Like v5, but lacking any data that comes from debug information,
241///    i.e. files, lines and inlines. This is faster.
242///  - `/source/v1`: Experimental API. Symbolicates an address and lets you read one of the files in the
243///    symbol information for that address.
244pub async fn query_api<'h>(
245    request_url: &str,
246    request_json_data: &str,
247    helper: &'h impl FileAndPathHelper<'h>,
248) -> String {
249    if request_url == "/symbolicate/v5-legacy" {
250        symbolicate::v5::query_api_json(request_json_data, helper, false).await
251    } else if request_url == "/symbolicate/v5" {
252        symbolicate::v5::query_api_json(request_json_data, helper, true).await
253    } else if request_url == "/source/v1" {
254        source::query_api_json(request_json_data, helper).await
255    } else {
256        json!({ "error": format!("Unrecognized URL {}", request_url) }).to_string()
257    }
258}
259
260async fn try_get_symbolication_result_from_path<'h, R, H>(
261    query: SymbolicationQuery<'_>,
262    file_location: &FileLocation,
263    helper: &'h H,
264) -> Result<R>
265where
266    R: SymbolicationResult,
267    H: FileAndPathHelper<'h>,
268{
269    let file_contents = helper.open_file(file_location).await.map_err(|e| {
270        GetSymbolsError::HelperErrorDuringOpenFile(file_location.to_string_lossy(), e)
271    })?;
272    let base_path = file_location.to_base_path();
273
274    let file_contents = FileContentsWrapper::new(file_contents);
275
276    if let Ok(file_kind) = FileKind::parse(&file_contents) {
277        match file_kind {
278            FileKind::Elf32 | FileKind::Elf64 => {
279                elf::get_symbolication_result(&base_path, file_kind, file_contents, query)
280            }
281            FileKind::MachOFat32 => {
282                let arches = FatHeader::parse_arch32(&file_contents)
283                    .map_err(|e| GetSymbolsError::ObjectParseError(file_kind, e))?;
284                let range = macho::get_arch_range(&file_contents, arches, query.debug_id)?;
285                macho::get_symbolication_result(
286                    &base_path,
287                    file_contents,
288                    Some(range),
289                    query,
290                    helper,
291                )
292                .await
293            }
294            FileKind::MachOFat64 => {
295                let arches = FatHeader::parse_arch64(&file_contents)
296                    .map_err(|e| GetSymbolsError::ObjectParseError(file_kind, e))?;
297                let range = macho::get_arch_range(&file_contents, arches, query.debug_id)?;
298                macho::get_symbolication_result(
299                    &base_path,
300                    file_contents,
301                    Some(range),
302                    query,
303                    helper,
304                )
305                .await
306            }
307            FileKind::MachO32 | FileKind::MachO64 => {
308                macho::get_symbolication_result(&base_path, file_contents, None, query, helper)
309                    .await
310            }
311            FileKind::Pe32 | FileKind::Pe64 => {
312                windows::get_symbolication_result_via_binary(
313                    file_kind,
314                    file_contents,
315                    query,
316                    file_location,
317                    helper,
318                )
319                .await
320            }
321            _ => Err(GetSymbolsError::InvalidInputError(
322                "Input was Archive, Coff or Wasm format, which are unsupported for now",
323            )),
324        }
325    } else if let Ok(pdb) = PDB::open(&file_contents) {
326        // This is a PDB file.
327        windows::get_symbolication_result(&base_path, pdb, query)
328    } else {
329        Err(GetSymbolsError::InvalidInputError(
330            "The file does not have a known format; PDB::open was not able to parse it and object::FileKind::parse was not able to detect the format.",
331        ))
332    }
333}