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}