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    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 &regs_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    // It's possible that the debug information is in the .so file itself
143    let so_files = find_files_with_extension(&sbf_paths, "so");
144    // It's also possible that it ends with .debug
145    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    // Collect only those files that contain debug sections
151    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            // check it has debug sections
157            let has_debug = object
158                .sections()
159                .any(|section| section.name().is_ok_and(|n| n.starts_with(".debug_")));
160            // get compiler information if any
161            let producer = get_dwarf_attribute(&object, DW_TAG_compile_unit, DW_AT_producer).ok();
162            // get lang information if any
163            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    // Suppose debug_path is program.debug, swap with .so and try
203    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                // We might have program.so.debug - simply cut debug and try
208                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(&regs_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    // smoelius: If a sequence of Regs refer to the same file and line, treat them as
274    // one hit to that file and line.
275    // vaddrs.dedup_by_key::<_, Option<&Entry>>(|vaddr| dwarf.vaddr_entry_map.get(vaddr));
276
277    if let Ok(branches) = branch::get_branches(&vaddrs, &insns, &regs, dwarf) {
278        let _ = branch::write_branch_coverage(&branches, regs_path, src_paths);
279    }
280
281    // smoelius: A `vaddr` could not have an entry because its file does not exist. Keep only those
282    // `vaddr`s that have entries.
283    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            // smoelius: Ignore files that do not exist.
315            if !Path::new(file).try_exists()? {
316                continue;
317            }
318            // procdump: ignore files other than what user has provided.
319            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        // smoelius: Even though we ignore columns, fetch them should we ever want to act on them.
330        // let Some(_column) = location.column else {
331        //     continue;
332        // };
333        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        // NB: the pc is instruction indexed, not byte indexed, keeps it aligned to 8 bytes - hence << 3 -> *8
375        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    // smoelius: Make the shift "permanent".
408    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        // smoelius: A `vaddr` could not have an entry because its file does not exist.
427        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        // smoelius: Stripping `current_dir` from `source_file` has not effect on what's displayed.
453        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}