1use addr2line::gimli::{DW_AT_language, DW_AT_producer, DW_TAG_compile_unit};
2pub use addr2line::{self, Loader};
3use anyhow::{Result, anyhow, bail};
4use byteorder::{LittleEndian, ReadBytesExt};
5pub use object::{Object, ObjectSection};
6use std::{
7 collections::{BTreeMap, HashSet},
8 fs::{File, OpenOptions, metadata},
9 io::Write,
10 path::{Path, PathBuf},
11};
12
13mod branch;
14mod trace_disassemble;
15
16mod start_address;
17use start_address::start_address;
18
19pub mod toolchain;
20pub mod util;
21use util::StripCurrentDir;
22
23use crate::util::{
24 compute_hash, find_files_with_extension, get_dwarf_attribute, get_section_start_address,
25};
26
27mod vaddr;
28
29#[derive(Debug)]
30pub struct DebugPath {
31 pub path: PathBuf,
32 pub producer: Option<String>,
33 pub lang: Option<String>,
34}
35
36#[derive(Clone, Debug, Default, Eq, PartialEq)]
37struct Entry<'a> {
38 file: &'a str,
39 line: u32,
40}
41
42struct Dwarf {
43 debug_path: DebugPath,
44 #[allow(dead_code)]
45 so_path: PathBuf,
46 so_hash: String,
47 start_address: u64,
48 text_section_offset: u64,
49 #[allow(dead_code, reason = "`vaddr` points into `loader`")]
50 loader: &'static Loader,
51 vaddr_entry_map: BTreeMap<u64, Entry<'static>>,
52}
53
54enum Outcome {
55 Lcov(PathBuf),
56 TraceDisassemble,
57}
58
59type Vaddrs = Vec<u64>;
60type Insns = Vec<u64>;
61type Regs = Vec<[u64; 12]>;
62
63type VaddrEntryMap<'a> = BTreeMap<u64, Entry<'a>>;
64
65type FileLineCountMap<'a> = BTreeMap<&'a str, BTreeMap<u32, usize>>;
66
67pub fn run(
68 sbf_trace_dir: PathBuf,
69 src_paths: HashSet<PathBuf>,
70 sbf_paths: Vec<PathBuf>,
71 debug: bool,
72 trace_disassemble: bool,
73 no_color: bool,
74) -> Result<()> {
75 let mut lcov_paths = Vec::new();
76
77 let debug_paths = debug_paths(sbf_paths)?;
78
79 let dwarfs = debug_paths
80 .into_iter()
81 .map(|path| build_dwarf(path, &src_paths, trace_disassemble))
82 .collect::<Result<Vec<_>>>()
83 .expect("Can't build dwarf");
84
85 if dwarfs.is_empty() {
86 bail!("Found no .so/.debug/.so.debug files containing debug sections.");
87 }
88
89 if debug {
90 for dwarf in dwarfs {
91 dump_vaddr_entry_map(dwarf.vaddr_entry_map);
92 }
93 eprintln!("Exiting debug mode.");
94 return Ok(());
95 }
96
97 let regs_paths = find_files_with_extension(std::slice::from_ref(&sbf_trace_dir), "regs");
98 if regs_paths.is_empty() {
99 bail!(
100 "Found no regs files in: {}
101Are you sure you run your tests with register tracing enabled",
102 sbf_trace_dir.strip_current_dir().display(),
103 );
104 }
105
106 for regs_path in ®s_paths {
107 match process_regs_path(&dwarfs, regs_path, &src_paths, trace_disassemble, no_color) {
108 Ok(Outcome::Lcov(lcov_path)) => {
109 lcov_paths.push(lcov_path.strip_current_dir().to_path_buf());
110 }
111 Ok(Outcome::TraceDisassemble) => {
112 return Ok(());
113 }
114 _ => {
115 eprintln!(
116 "Skipping Regs file: {} (no matching executable)",
117 regs_path.strip_current_dir().display()
118 );
119 }
120 }
121 }
122
123 eprintln!(
124 "
125Processed {} of {} regs files
126
127Lcov files written: {lcov_paths:#?}
128
129If you are done generating lcov files, try running:
130
131 genhtml --output-directory coverage {}/*.lcov --rc branch_coverage=1 && open coverage/index.html
132",
133 lcov_paths.len(),
134 regs_paths.len(),
135 sbf_trace_dir.as_path().strip_current_dir().display()
136 );
137
138 Ok(())
139}
140
141fn debug_paths(sbf_paths: Vec<PathBuf>) -> Result<Vec<DebugPath>> {
142 let so_files = find_files_with_extension(&sbf_paths, "so");
144 let debug_files = find_files_with_extension(&sbf_paths, "debug");
146
147 let mut maybe_list = so_files;
148 maybe_list.extend(debug_files);
149
150 let full_list = maybe_list
152 .into_iter()
153 .filter_map(|maybe_path| {
154 let data = std::fs::read(&maybe_path).ok()?;
155 let object = object::read::File::parse(&*data).ok()?;
156 let has_debug = object
158 .sections()
159 .any(|section| section.name().is_ok_and(|n| n.starts_with(".debug_")));
160 let producer = get_dwarf_attribute(&object, DW_TAG_compile_unit, DW_AT_producer).ok();
162 let lang = get_dwarf_attribute(&object, DW_TAG_compile_unit, DW_AT_language).ok();
164
165 has_debug.then_some(DebugPath {
166 path: maybe_path,
167 producer,
168 lang,
169 })
170 })
171 .collect();
172
173 eprintln!("Files containing debug sections: {:#?}", full_list);
174 Ok(full_list)
175}
176
177fn build_dwarf(
178 debug_path: DebugPath,
179 src_paths: &HashSet<PathBuf>,
180 trace_disassemble: bool,
181) -> Result<Dwarf> {
182 let start_address = start_address(&debug_path.path)?;
183
184 let loader = Loader::new(&debug_path.path).map_err(|error| {
185 anyhow!(
186 "failed to build loader for {}: {}",
187 debug_path.path.display(),
188 error
189 )
190 })?;
191
192 let loader = Box::leak(Box::new(loader));
193
194 eprintln!(
195 "Trying to build a DWARF entry with debug path: {}",
196 debug_path.path.strip_current_dir().display()
197 );
198
199 let vaddr_entry_map =
200 build_vaddr_entry_map(loader, &debug_path.path, src_paths, trace_disassemble)?;
201
202 let mut so_path = debug_path.path.with_extension("so");
204 let so_content = match std::fs::read(&so_path) {
205 Err(e) => {
206 if e.kind() == std::io::ErrorKind::NotFound {
207 so_path = debug_path.path.with_extension("");
209 std::fs::read(&so_path)?
210 } else {
211 return Err(e.into());
212 }
213 }
214 Ok(c) => c,
215 };
216 let so_hash = compute_hash(&so_content);
217 eprintln!(
218 "Found a match:\n{} to\n{} (SHA-256: {})",
219 debug_path.path.strip_current_dir().display(),
220 so_path.strip_current_dir().display(),
221 &so_hash[..16],
222 );
223
224 Ok(Dwarf {
225 debug_path,
226 so_path,
227 so_hash,
228 start_address,
229 loader,
230 vaddr_entry_map,
231 text_section_offset: get_section_start_address(loader, ".text")?,
232 })
233}
234
235fn process_regs_path(
236 dwarfs: &[Dwarf],
237 regs_path: &Path,
238 src_paths: &HashSet<PathBuf>,
239 trace_disassemble: bool,
240 no_color: bool,
241) -> Result<Outcome> {
242 eprintln!();
243 let exec_sha256 = std::fs::read_to_string(regs_path.with_extension("exec.sha256"))?;
244 eprintln!(
245 "Regs file: {} (expecting executable with SHA-256: {})",
246 regs_path.strip_current_dir().display(),
247 &exec_sha256[..16]
248 );
249
250 let (mut vaddrs, regs) = read_vaddrs(regs_path)?;
251 eprintln!("Regs read: {}", vaddrs.len());
252 let insns = read_insns(®s_path.with_extension("insns"))?;
253
254 let dwarf = find_applicable_dwarf(dwarfs, regs_path, &exec_sha256, &mut vaddrs)?;
255
256 eprintln!(
257 "Applicable dwarf: {}",
258 dwarf.debug_path.path.strip_current_dir().display()
259 );
260
261 assert!(
262 vaddrs
263 .first()
264 .is_some_and(|&vaddr| vaddr == dwarf.start_address)
265 );
266
267 if trace_disassemble {
268 return trace_disassemble::trace_disassemble(
269 src_paths, regs_path, &vaddrs, dwarf, !no_color,
270 );
271 }
272
273 if let Ok(branches) = branch::get_branches(&vaddrs, &insns, ®s, dwarf) {
278 let _ = branch::write_branch_coverage(&branches, regs_path, src_paths);
279 }
280
281 let vaddrs = vaddrs
284 .into_iter()
285 .filter(|vaddr| dwarf.vaddr_entry_map.contains_key(vaddr))
286 .collect::<Vec<_>>();
287
288 eprintln!("Line hits: {}", vaddrs.len());
289
290 let file_line_count_map = build_file_line_count_map(&dwarf.vaddr_entry_map, vaddrs);
291
292 write_lcov_file(regs_path, file_line_count_map).map(Outcome::Lcov)
293}
294
295fn build_vaddr_entry_map<'a>(
296 loader: &'a Loader,
297 debug_path: &Path,
298 src_paths: &HashSet<PathBuf>,
299 trace_disassemble: bool,
300) -> Result<VaddrEntryMap<'a>> {
301 let mut vaddr_entry_map = VaddrEntryMap::new();
302 let metadata = metadata(debug_path)?;
303 for vaddr in (0..metadata.len()).step_by(size_of::<u64>()) {
304 let location = loader.find_location(vaddr).map_err(|error| {
305 anyhow!("failed to find location for address 0x{vaddr:x}: {}", error)
306 })?;
307 let Some(location) = location else {
308 continue;
309 };
310 let Some(file) = location.file else {
311 continue;
312 };
313 if !trace_disassemble {
314 if !Path::new(file).try_exists()? {
316 continue;
317 }
318 if !src_paths
320 .iter()
321 .any(|src_path| file.starts_with(&src_path.to_string_lossy().to_string()))
322 {
323 continue;
324 }
325 }
326 let Some(line) = location.line else {
327 continue;
328 };
329 let entry = vaddr_entry_map.entry(vaddr).or_default();
334 entry.file = file;
335 entry.line = line;
336 }
337 Ok(vaddr_entry_map)
338}
339
340fn dump_vaddr_entry_map(vaddr_entry_map: BTreeMap<u64, Entry<'_>>) {
341 let mut prev = String::new();
342 for (vaddr, Entry { file, line }) in vaddr_entry_map {
343 let curr = format!("{file}:{line}");
344 if prev != curr {
345 eprintln!("0x{vaddr:x}: {curr}");
346 prev = curr;
347 }
348 }
349}
350
351fn read_insns(insns_path: &Path) -> Result<Insns> {
352 let mut insns = Vec::new();
353 let mut insns_file = File::open(insns_path)?;
354 while let Ok(insn) = insns_file.read_u64::<LittleEndian>() {
355 insns.push(insn);
356 }
357 Ok(insns)
358}
359
360fn read_vaddrs(regs_path: &Path) -> Result<(Vaddrs, Regs)> {
361 let mut regs = Regs::new();
362 let mut vaddrs = Vaddrs::new();
363 let mut regs_file = File::open(regs_path)?;
364
365 let mut data_trace = [0u64; 12];
366 'outer: loop {
367 for item in &mut data_trace {
368 match regs_file.read_u64::<LittleEndian>() {
369 Err(_) => break 'outer,
370 Ok(reg) => *item = reg,
371 }
372 }
373
374 let vaddr = data_trace[11] << 3;
376
377 vaddrs.push(vaddr);
378 regs.push(data_trace);
379 }
380
381 Ok((vaddrs, regs))
382}
383
384fn find_applicable_dwarf<'a>(
385 dwarfs: &'a [Dwarf],
386 regs_path: &Path,
387 exec_sha256: &str,
388 vaddrs: &mut [u64],
389) -> Result<&'a Dwarf> {
390 let dwarf = dwarfs
391 .iter()
392 .find(|dwarf| dwarf.so_hash == exec_sha256)
393 .ok_or(anyhow!(
394 "Cannot find the shared object that corresponds to: {}",
395 exec_sha256
396 ))?;
397
398 eprintln!(
399 "Matching Regs file {} to executable with SHA-256: {}",
400 regs_path.strip_current_dir().display(),
401 &dwarf.so_hash[..16]
402 );
403 let vaddr_first = *vaddrs.first().ok_or(anyhow!("Vaddrs is empty!"))?;
404 assert!(dwarf.start_address >= vaddr_first);
405 let shift = dwarf.start_address - vaddr_first;
406
407 for vaddr in vaddrs.iter_mut() {
409 *vaddr += shift;
410 }
411
412 Ok(dwarf)
413}
414
415fn build_file_line_count_map<'a>(
416 vaddr_entry_map: &BTreeMap<u64, Entry<'a>>,
417 vaddrs: Vaddrs,
418) -> FileLineCountMap<'a> {
419 let mut file_line_count_map = FileLineCountMap::new();
420 for Entry { file, line } in vaddr_entry_map.values() {
421 let line_count_map = file_line_count_map.entry(file).or_default();
422 line_count_map.insert(*line, 0);
423 }
424
425 for vaddr in vaddrs {
426 let Some(entry) = vaddr_entry_map.get(&vaddr) else {
428 continue;
429 };
430 let Some(line_count_map) = file_line_count_map.get_mut(entry.file) else {
431 continue;
432 };
433 let Some(count) = line_count_map.get_mut(&entry.line) else {
434 continue;
435 };
436 *count += 1;
437 }
438
439 file_line_count_map
440}
441
442fn write_lcov_file(regs_path: &Path, file_line_count_map: FileLineCountMap<'_>) -> Result<PathBuf> {
443 let lcov_path = regs_path.with_extension("lcov");
444
445 let mut file = OpenOptions::new()
446 .create(true)
447 .truncate(true)
448 .write(true)
449 .open(&lcov_path)?;
450
451 for (source_file, line_count_map) in file_line_count_map {
452 writeln!(file, "SF:{source_file}")?;
454 for (line, count) in line_count_map {
455 writeln!(file, "DA:{line},{count}")?;
456 }
457 writeln!(file, "end_of_record")?;
458 }
459
460 Ok(lcov_path)
461}