Skip to main content

sbpf_coverage/
lib.rs

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    #[allow(dead_code, reason = "might be of use in the future")]
48    start_address: u64,
49    text_section_offset: u64,
50    #[allow(dead_code, reason = "`vaddr` points into `loader`")]
51    loader: &'static Loader,
52    vaddr_entry_map: BTreeMap<u64, Entry<'static>>,
53}
54
55enum Outcome {
56    Lcov(PathBuf),
57    TraceDisassemble,
58}
59
60type Vaddrs = Vec<u64>;
61type Insns = Vec<u64>;
62type Regs = Vec<[u64; 12]>;
63
64type VaddrEntryMap<'a> = BTreeMap<u64, Entry<'a>>;
65
66type FileLineCountMap<'a> = BTreeMap<&'a str, BTreeMap<u32, usize>>;
67
68pub fn run(
69    sbf_trace_dir: PathBuf,
70    src_paths: HashSet<PathBuf>,
71    sbf_paths: Vec<PathBuf>,
72    debug: bool,
73    trace_disassemble: bool,
74    no_color: bool,
75) -> Result<()> {
76    let mut lcov_paths = Vec::new();
77
78    let debug_paths = debug_paths(sbf_paths)?;
79
80    let dwarfs = debug_paths
81        .into_iter()
82        .map(|path| build_dwarf(path, &src_paths, trace_disassemble))
83        .collect::<Result<Vec<_>>>()
84        .expect("Can't build dwarf");
85
86    if dwarfs.is_empty() {
87        bail!("Found no .so/.debug/.so.debug files containing debug sections.");
88    }
89
90    if debug {
91        for dwarf in dwarfs {
92            dump_vaddr_entry_map(dwarf.vaddr_entry_map);
93        }
94        eprintln!("Exiting debug mode.");
95        return Ok(());
96    }
97
98    let mut regs_paths = find_files_with_extension(std::slice::from_ref(&sbf_trace_dir), "regs");
99    if regs_paths.is_empty() {
100        bail!(
101            "Found no regs files in: {}
102Are you sure you run your tests with register tracing enabled",
103            sbf_trace_dir.strip_current_dir().display(),
104        );
105    }
106    // Sort paths by modification time.
107    regs_paths.sort_by_key(|p| {
108        std::fs::metadata(p)
109            .and_then(|m| m.modified())
110            .unwrap_or(std::time::UNIX_EPOCH)
111    });
112
113    for regs_path in &regs_paths {
114        match process_regs_path(&dwarfs, regs_path, &src_paths, trace_disassemble, no_color) {
115            Ok(Outcome::Lcov(lcov_path)) => {
116                lcov_paths.push(lcov_path.strip_current_dir().to_path_buf());
117            }
118            Ok(Outcome::TraceDisassemble) => {}
119            _ => {
120                eprintln!(
121                    "Skipping Regs file: {} (no matching executable)",
122                    regs_path.strip_current_dir().display()
123                );
124            }
125        }
126    }
127
128    if !trace_disassemble {
129        eprintln!(
130            "
131Processed {} of {} regs files
132
133Lcov files written: {lcov_paths:#?}
134
135If you are done generating lcov files, try running:
136
137    genhtml --output-directory coverage {}/*.lcov --rc branch_coverage=1 && open coverage/index.html
138",
139            lcov_paths.len(),
140            regs_paths.len(),
141            sbf_trace_dir.as_path().strip_current_dir().display()
142        );
143    }
144
145    Ok(())
146}
147
148fn debug_paths(sbf_paths: Vec<PathBuf>) -> Result<Vec<DebugPath>> {
149    // It's possible that the debug information is in the .so file itself
150    let so_files = find_files_with_extension(&sbf_paths, "so");
151    // It's also possible that it ends with .debug
152    let debug_files = find_files_with_extension(&sbf_paths, "debug");
153
154    let mut maybe_list = so_files;
155    maybe_list.extend(debug_files);
156
157    // Collect only those files that contain debug sections
158    let full_list: Vec<DebugPath> = maybe_list
159        .into_iter()
160        .filter_map(|maybe_path| {
161            let data = std::fs::read(&maybe_path).ok()?;
162            let object = object::read::File::parse(&*data).ok()?;
163            // check it has debug sections
164            let has_debug = object
165                .sections()
166                .any(|section| section.name().is_ok_and(|n| n.starts_with(".debug_")));
167            // get compiler information if any
168            let producer = get_dwarf_attribute(&object, DW_TAG_compile_unit, DW_AT_producer).ok();
169            // get lang information if any
170            let lang = get_dwarf_attribute(&object, DW_TAG_compile_unit, DW_AT_language).ok();
171
172            has_debug.then_some(DebugPath {
173                path: maybe_path,
174                producer,
175                lang,
176            })
177        })
178        .collect();
179
180    eprintln!("Debug symbols found:");
181    for dp in full_list.iter() {
182        eprintln!(
183            "  {} (producer: {}, lang: {})",
184            dp.path.strip_current_dir().display(),
185            dp.producer.as_deref().unwrap_or("unknown"),
186            dp.lang.as_deref().unwrap_or("unknown"),
187        );
188    }
189    Ok(full_list)
190}
191
192fn build_dwarf(
193    debug_path: DebugPath,
194    src_paths: &HashSet<PathBuf>,
195    trace_disassemble: bool,
196) -> Result<Dwarf> {
197    let start_address = start_address(&debug_path.path)?;
198
199    let loader = Loader::new(&debug_path.path).map_err(|error| {
200        anyhow!(
201            "failed to build loader for {}: {}",
202            debug_path.path.display(),
203            error
204        )
205    })?;
206
207    let loader = Box::leak(Box::new(loader));
208
209    let vaddr_entry_map =
210        build_vaddr_entry_map(loader, &debug_path.path, src_paths, trace_disassemble)?;
211
212    // Suppose debug_path is program.debug, swap with .so and try
213    let mut so_path = debug_path.path.with_extension("so");
214    let so_content = match std::fs::read(&so_path) {
215        Err(e) => {
216            if e.kind() == std::io::ErrorKind::NotFound {
217                // We might have program.so.debug - simply cut debug and try
218                so_path = debug_path.path.with_extension("");
219                std::fs::read(&so_path)?
220            } else {
221                return Err(e.into());
222            }
223        }
224        Ok(c) => c,
225    };
226    let so_hash = compute_hash(&so_content);
227    eprintln!(
228        "DWARF: {} -> {} (exec sha256: {})",
229        debug_path.path.strip_current_dir().display(),
230        so_path.strip_current_dir().display(),
231        &so_hash[..16],
232    );
233
234    Ok(Dwarf {
235        debug_path,
236        so_path,
237        so_hash,
238        start_address,
239        loader,
240        vaddr_entry_map,
241        text_section_offset: get_section_start_address(loader, ".text")?,
242    })
243}
244
245fn process_regs_path(
246    dwarfs: &[Dwarf],
247    regs_path: &Path,
248    src_paths: &HashSet<PathBuf>,
249    trace_disassemble: bool,
250    no_color: bool,
251) -> Result<Outcome> {
252    eprintln!();
253    let exec_sha256 = std::fs::read_to_string(regs_path.with_extension("exec.sha256"))?;
254    let (mut vaddrs, regs) = read_vaddrs(regs_path)?;
255    eprintln!(
256        "Regs: {} ({} entries, exec sha256: {})",
257        regs_path.strip_current_dir().display(),
258        vaddrs.len(),
259        &exec_sha256[..16],
260    );
261    let insns = read_insns(&regs_path.with_extension("insns"))?;
262
263    let dwarf = find_applicable_dwarf(dwarfs, regs_path, &exec_sha256, &mut vaddrs)?;
264
265    if trace_disassemble {
266        return trace_disassemble::trace_disassemble(
267            src_paths, regs_path, &vaddrs, dwarf, !no_color,
268        );
269    }
270
271    // smoelius: If a sequence of Regs refer to the same file and line, treat them as
272    // one hit to that file and line.
273    // vaddrs.dedup_by_key::<_, Option<&Entry>>(|vaddr| dwarf.vaddr_entry_map.get(vaddr));
274
275    if let Ok(branches) = branch::get_branches(&vaddrs, &insns, &regs, dwarf) {
276        let _ = branch::write_branch_coverage(&branches, regs_path, src_paths);
277    }
278
279    // smoelius: A `vaddr` could not have an entry because its file does not exist. Keep only those
280    // `vaddr`s that have entries.
281    let vaddrs = vaddrs
282        .into_iter()
283        .filter(|vaddr| dwarf.vaddr_entry_map.contains_key(vaddr))
284        .collect::<Vec<_>>();
285
286    eprintln!("Line hits: {}", vaddrs.len());
287
288    let file_line_count_map = build_file_line_count_map(&dwarf.vaddr_entry_map, vaddrs);
289
290    write_lcov_file(regs_path, file_line_count_map).map(Outcome::Lcov)
291}
292
293fn build_vaddr_entry_map<'a>(
294    loader: &'a Loader,
295    debug_path: &Path,
296    src_paths: &HashSet<PathBuf>,
297    trace_disassemble: bool,
298) -> Result<VaddrEntryMap<'a>> {
299    let mut vaddr_entry_map = VaddrEntryMap::new();
300    let metadata = metadata(debug_path)?;
301    for vaddr in (0..metadata.len()).step_by(size_of::<u64>()) {
302        let location = loader.find_location(vaddr).map_err(|error| {
303            anyhow!("failed to find location for address 0x{vaddr:x}: {}", error)
304        })?;
305        let Some(location) = location else {
306            continue;
307        };
308        let Some(file) = location.file else {
309            continue;
310        };
311        if !trace_disassemble {
312            // smoelius: Ignore files that do not exist.
313            if !Path::new(file).try_exists()? {
314                continue;
315            }
316            // procdump: ignore files other than what user has provided.
317            if !src_paths
318                .iter()
319                .any(|src_path| file.starts_with(&src_path.to_string_lossy().to_string()))
320            {
321                continue;
322            }
323        }
324        let Some(line) = location.line else {
325            continue;
326        };
327        // smoelius: Even though we ignore columns, fetch them should we ever want to act on them.
328        // let Some(_column) = location.column else {
329        //     continue;
330        // };
331        let entry = vaddr_entry_map.entry(vaddr).or_default();
332        entry.file = file;
333        entry.line = line;
334    }
335    Ok(vaddr_entry_map)
336}
337
338fn dump_vaddr_entry_map(vaddr_entry_map: BTreeMap<u64, Entry<'_>>) {
339    let mut prev = String::new();
340    for (vaddr, Entry { file, line }) in vaddr_entry_map {
341        let curr = format!("{file}:{line}");
342        if prev != curr {
343            eprintln!("0x{vaddr:x}: {curr}");
344            prev = curr;
345        }
346    }
347}
348
349fn read_insns(insns_path: &Path) -> Result<Insns> {
350    let mut insns = Vec::new();
351    let mut insns_file = File::open(insns_path)?;
352    while let Ok(insn) = insns_file.read_u64::<LittleEndian>() {
353        insns.push(insn);
354    }
355    Ok(insns)
356}
357
358fn read_vaddrs(regs_path: &Path) -> Result<(Vaddrs, Regs)> {
359    let mut regs = Regs::new();
360    let mut vaddrs = Vaddrs::new();
361    let mut regs_file = File::open(regs_path)?;
362
363    let mut data_trace = [0u64; 12];
364    'outer: loop {
365        for item in &mut data_trace {
366            match regs_file.read_u64::<LittleEndian>() {
367                Err(_) => break 'outer,
368                Ok(reg) => *item = reg,
369            }
370        }
371
372        // NB: the pc is instruction indexed, not byte indexed, keeps it aligned to 8 bytes - hence << 3 -> *8
373        let vaddr = data_trace[11] << 3;
374
375        vaddrs.push(vaddr);
376        regs.push(data_trace);
377    }
378
379    Ok((vaddrs, regs))
380}
381
382fn find_applicable_dwarf<'a>(
383    dwarfs: &'a [Dwarf],
384    regs_path: &Path,
385    exec_sha256: &str,
386    vaddrs: &mut [u64],
387) -> Result<&'a Dwarf> {
388    let dwarf = dwarfs
389        .iter()
390        .find(|dwarf| dwarf.so_hash == exec_sha256)
391        .ok_or(anyhow!(
392            "Cannot find the shared object that corresponds to: {}",
393            exec_sha256
394        ))?;
395
396    eprintln!(
397        "Matched: {} -> {} (exec sha256: {})",
398        regs_path.strip_current_dir().display(),
399        dwarf.debug_path.path.strip_current_dir().display(),
400        &dwarf.so_hash[..16],
401    );
402
403    // Raw vaddrs from the register dump are byte offsets without the text section base.
404    // Add text_section_offset so they match the DWARF address space used for lookups.
405    for vaddr in vaddrs.iter_mut() {
406        *vaddr += dwarf.text_section_offset;
407    }
408
409    Ok(dwarf)
410}
411
412fn build_file_line_count_map<'a>(
413    vaddr_entry_map: &BTreeMap<u64, Entry<'a>>,
414    vaddrs: Vaddrs,
415) -> FileLineCountMap<'a> {
416    let mut file_line_count_map = FileLineCountMap::new();
417    for Entry { file, line } in vaddr_entry_map.values() {
418        let line_count_map = file_line_count_map.entry(file).or_default();
419        line_count_map.insert(*line, 0);
420    }
421
422    for vaddr in vaddrs {
423        // smoelius: A `vaddr` could not have an entry because its file does not exist.
424        let Some(entry) = vaddr_entry_map.get(&vaddr) else {
425            continue;
426        };
427        let Some(line_count_map) = file_line_count_map.get_mut(entry.file) else {
428            continue;
429        };
430        let Some(count) = line_count_map.get_mut(&entry.line) else {
431            continue;
432        };
433        *count += 1;
434    }
435
436    file_line_count_map
437}
438
439fn write_lcov_file(regs_path: &Path, file_line_count_map: FileLineCountMap<'_>) -> Result<PathBuf> {
440    let lcov_path = regs_path.with_extension("lcov");
441
442    let mut file = OpenOptions::new()
443        .create(true)
444        .truncate(true)
445        .write(true)
446        .open(&lcov_path)?;
447
448    for (source_file, line_count_map) in file_line_count_map {
449        // smoelius: Stripping `current_dir` from `source_file` has not effect on what's displayed.
450        writeln!(file, "SF:{source_file}")?;
451        for (line, count) in line_count_map {
452            writeln!(file, "DA:{line},{count}")?;
453        }
454        writeln!(file, "end_of_record")?;
455    }
456
457    Ok(lcov_path)
458}