blob: ceefaf33add892a394e6785f0501a807391cc6e4 [file]
// Copyright 2025 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
// TODO: refactor this file and crate::riscv to separate generic elf code from
// riscv specific code.
#![allow(clippy::print_stdout)]
use core::fmt::Debug;
use std::collections::HashSet;
use std::path::Path;
use anyhow::{Context, anyhow};
use object::read::elf::{ElfFile32, FileHeader};
use object::{LittleEndian, elf};
use crate::riscv::call_graph::{FuncRepo, Function, list_functions};
use crate::riscv::{DecodedInstr, ElfMem, InstrA, Reg};
pub mod riscv;
/// Check to see if the elf-file at `elf_path` contains any calls to the
/// `panic_is_possible`` symbol, and if so, try to find the line numbers where
/// these potential panics originate from.
pub fn check_panic(elf_path: &Path) -> anyhow::Result<()> {
const E: object::LittleEndian = object::LittleEndian;
let ctx = || format!("In elf file {}", elf_path.display());
let elf_bytes = std::fs::read(elf_path).with_context(ctx)?;
let elf =
ElfFile32::<object::LittleEndian, _>::parse(elf_bytes.as_slice()).with_context(ctx)?;
let elf_mem = ElfMem::new(&elf, E)?;
let funcs = list_functions(&elf, &elf_mem)?;
let func_repo = FuncRepo::new(funcs).unwrap();
if let Some(panic_func) = func_repo.get_func_by_symbol("panic_is_possible") {
match elf.elf_header().e_machine(E) {
elf::EM_ARM => solve_arm(&elf_mem, &func_repo, panic_func),
elf::EM_RISCV => solve_riscv(&elf_mem, &func_repo, panic_func),
_ => {
return Err(anyhow!(
"Unsupported machine type: {:?}",
elf.elf_header().e_machine
));
}
}
}
if find_symbol_address(elf.elf_symbol_table(), "panic_is_possible").is_ok() {
return Err(anyhow!(
"File {} contains the symbol panic_is_possible. \n\
This indicates that the optimizer was unable to optimize out all panics. \n\
Please remove the offending panic call site.",
elf_path.display()
));
}
Ok(())
}
fn solve_riscv(elf_mem: &ElfMem, func_repo: &FuncRepo, panic_func: &Function) {
// Define a closure that the solver can use to read from .rodata and
// .text when dereferencing pointers.
let mem_read = |addr: u32| {
elf_mem
.get(addr, 4)
.map(|a| u32::from_le_bytes(a.try_into().unwrap()))
};
// Solve for "all" possible (constant) values to the arguments to
// panic_is_possible(filename: *const u8, filename_len: usize, line: u32, col: u32)
let solutions = solve(
func_repo,
panic_func.start_addr(),
// The RISC-V C calling convention stores the arguments starting at register a0-a3
&[Reg::X10A0, Reg::X11A1, Reg::X12A2, Reg::X13A3],
mem_read,
);
for solution in solutions {
let (filename_ptr, filename_len, line, column) = (
solution.results[0],
solution.results[1],
solution.results[2],
solution.results[3],
);
// Lookup the string contents from .rodata
let Some(filename) = elf_mem.get(filename_ptr, filename_len) else {
println!("Couldn't find filename at addr {filename_ptr:x} len={filename_len}");
continue;
};
let Ok(filename) = core::str::from_utf8(filename) else {
continue;
};
println!();
println!("Found panic {filename} line {line} column {column}. Branch trace:");
for addr in solution.branch_trace {
let Some((func, mut instr_iter)) = func_repo.instructions_at_addr(addr) else {
continue;
};
let Some(instr) = instr_iter.next() else {
continue;
};
println!(
" {: <36} ({})",
instr.to_string(),
rustc_demangle::demangle(func.symbol_name)
);
}
}
}
fn solve_arm(_elf_mem: &ElfMem, _func_repo: &FuncRepo, _panic_func: &Function) {
println!("Panic is possible.");
println!("Location backtrace not supported on ARM.");
// TODO: implement
}
/// Try to find all possible values of the registers specified in `regs` when
/// the PC is pointing at `addr`. The works well when `addr` is a function,
/// `regs` are the ABI arguments to that function, and the function is called
/// from multiple places in the code with different constants.
///
/// `mem_read_fn` is called when the solver needs to inspect .rodata at a
/// particular address. The argument is the address, and response should be the
/// word at that address.
fn solve<'a>(
repo: &FuncRepo,
addr: u32,
regs: &[Reg],
mem_read_fn: impl Fn(u32) -> Option<u32> + 'a,
) -> Vec<Solution> {
let mut solver = Solver {
repo,
solutions: vec![],
seen: HashSet::new(),
depth: 0,
mem_read_fn: Box::new(mem_read_fn),
};
// Set the start node in the jump linked-list to be the
// provided address. This will be used to construct a "branch trace" (similar
// to a stack trace but also include in-function branches) to give to the
// user when the argument constants are solved.
let jump = Jump::new(addr);
let mut exprs: Vec<Expr> = regs.iter().map(|r| Expr::Reg(*r)).collect();
solver.go(jump, &mut exprs);
solver.solutions
}
struct Solver<'a> {
repo: &'a FuncRepo<'a>,
solutions: Vec<Solution>,
// Seen instruction addresses, used to prevent us from getting stuck in a loop
seen: HashSet<u32>,
depth: usize,
// Called to read from memory (typically .rodata).
mem_read_fn: Box<dyn Fn(u32) -> Option<u32> + 'a>,
}
impl Solver<'_> {
fn go(&mut self, jump: Jump, exprs: &mut [Expr]) {
let addr = jump.addr;
self.depth += 1;
let Some((_func, mut iter)) = self.repo.instructions_at_addr(addr) else {
self.depth -= 1;
return;
};
// move forward one slot to include the jump instruction in prev()
iter.next();
// Iterate backwards over the instructions that led execution to this
// point, building up an expression of operations used to construct the
// register contents.
while let Some(instr) = iter.prev() {
if !self.seen.insert(instr.addr) {
self.depth -= 1;
// We've been here before, don't scan backwards any further.
return;
}
let instr_d = instr.decode();
if cfg!(feature = "solver_trace") {
println!("{} {instr}", " ".repeat(self.depth));
}
let mut all_consts = true;
for (expr_index, expr) in exprs.iter_mut().enumerate() {
let old_expr = expr.clone();
if let Err(err) = expr.handle_instr(instr, instr_d) {
if cfg!(feature = "solver_trace") {
println!("{} Aborting branch: {:?}", " ".repeat(self.depth), err);
}
self.depth -= 1;
return;
}
if cfg!(feature = "solver_trace") && *expr != old_expr {
println!("Expr index {expr_index} changed from {old_expr:?} to {expr:?}");
}
let found = expr.const_eval(&self.mem_read_fn).is_some();
all_consts &= found;
}
if all_consts {
// All of the register expressions can be evaluated without
// runtime data; we've found a solution!
let mut row = vec![];
for expr in exprs {
row.push(expr.const_eval(&self.mem_read_fn).unwrap());
}
self.solutions.push(Solution {
results: row,
branch_trace: jump.extend(instr.addr).branch_trace(),
});
self.depth -= 1;
return;
}
for jumper_addr in self.repo.get_jumpers(instr.addr) {
// The instruction at jumper_addr can jump to this instruction.
// Recursively trace back through the code that can jump here.
// Make an independent copy of the exprs so we can resume where
// we left off once go() returns.
let mut sub_exprs = exprs.to_vec();
self.go(jump.extend(*jumper_addr), &mut sub_exprs);
}
}
self.depth -= 1;
}
}
#[derive(Debug)]
enum ExprErr {
#[allow(dead_code)]
RegCloberred(InstrA),
}
#[derive(Clone, Eq, PartialEq)]
enum Expr {
Const(u32),
GlobalDeref(u32),
Reg(Reg),
Add(Box<Expr>, Box<Expr>),
PtrDeref(Box<Expr>),
}
impl Debug for Expr {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Const(val) => {
let val = (*val).cast_signed();
if (-1024..1024).contains(&val) {
write!(f, "Const({val})")
} else {
write!(f, "Const({val:08x})")
}
}
Self::GlobalDeref(val) => write!(f, "GlobalDeref({val:08x})"),
Self::Reg(val) => f.debug_tuple("Reg").field(val).finish(),
Self::Add(a, b) => f.debug_tuple("Add").field(a).field(b).finish(),
Self::PtrDeref(val) => f.debug_tuple("PtrDeref").field(val).finish(),
}
}
}
impl Expr {
fn const_eval(&self, rodata_read_fn: &dyn Fn(u32) -> Option<u32>) -> Option<u32> {
match self {
Self::Const(v) => Some(*v),
Self::GlobalDeref(addr) => rodata_read_fn(*addr),
Self::Reg(_) => None,
Self::Add(a, b) => {
let a = a.const_eval(rodata_read_fn)?;
let b = b.const_eval(rodata_read_fn)?;
Some(a.wrapping_add(b))
}
Self::PtrDeref(val) => {
let addr = val.const_eval(rodata_read_fn)?;
rodata_read_fn(addr)
}
}
}
fn reg(reg: Reg) -> Self {
match reg {
Reg::X0 => Expr::Const(0),
other => Expr::Reg(other),
}
}
fn add(a: Expr, b: Expr) -> Expr {
let mut result = Expr::Add(Box::new(a), Box::new(b));
result.optimize();
result
}
fn ptr_deref(ptr: Expr) -> Expr {
let mut result = Expr::PtrDeref(Box::new(ptr));
result.optimize();
result
}
// Optimize/normalize the expression, folding constants, etc.
fn optimize(&mut self) {
match self {
Expr::Add(a, b) => {
if let (Expr::Const(a), Expr::Const(b)) = (&**a, &**b) {
*self = Expr::Const(a.wrapping_add(*b));
return;
}
if let Expr::Const(_) = &**a {
// Always put the constant last.
core::mem::swap(a, b);
}
if let Expr::Const(b) = &**b
&& *b == 0
{
*self = (**a).clone();
self.optimize();
return;
}
if let (Expr::Add(c, d), Expr::Const(b)) = (&**a, &**b)
&& let Expr::Const(d) = &**d
{
*self = Expr::add((**c).clone(), Expr::Const(b.wrapping_add(*d)));
}
}
Expr::PtrDeref(ptr) => {
if let Expr::Const(ptr) = &**ptr {
*self = Expr::GlobalDeref(*ptr);
}
}
_ => {}
}
}
fn handle_instr(&mut self, instr: InstrA, instr_d: DecodedInstr) -> Result<(), ExprErr> {
match self {
Self::Const(_) => {
// Nothing to do; constants are constant
}
Self::GlobalDeref(_) => {}
Self::Reg(reg) => {
if instr_d.ty().rd() == Some(*reg) {
// This instruction changes the value of this register.
// Update the expression tree.
match instr_d {
// Load constant into this register
DecodedInstr::Auipc(_) => *self = Expr::Const(instr.absolute_imm()),
// Load constant into this register
DecodedInstr::Lui(i) => *self = Expr::Const(i.uimm()),
// Add two registers and put the result into this
// register. (Note: This can also be used to load a constant
// zero by making the source register x0; optimize()
// will collapse it to 0)
DecodedInstr::Add(i) => {
*self = Expr::add(Expr::reg(i.rs1()), Expr::reg(i.rs2()))
}
// Add another register + constant into this register.
// (this can also be used to load a constant by using x0
// as the source register; optimize() will collapse it
// to a constant expression)
DecodedInstr::Addi(i) => {
*self =
Expr::add(Expr::reg(i.rs1()), Expr::Const(i.imm().cast_unsigned()))
}
// Load from memory into this register.
DecodedInstr::Lw(i) => {
*self = Expr::ptr_deref(Expr::add(
Expr::reg(i.rs1()),
Expr::Const(i.imm().cast_unsigned()),
))
}
_ => return Err(ExprErr::RegCloberred(instr)),
}
}
}
Self::Add(a, b) => {
// Forward the instruction down to both sides of the add
// operation, in case it affects them.
a.handle_instr(instr, instr_d)?;
b.handle_instr(instr, instr_d)?;
self.optimize();
}
Self::PtrDeref(ptr) => {
// Forward the instruction down our pointer's address
// expression, in case it affect it.
ptr.handle_instr(instr, instr_d)?;
self.optimize();
// Self may have been changed by the call to optimize(), so make
// sure we're still a PtrDeref to make the borrow checker happy.
if let Self::PtrDeref(ptr) = self
&& let DecodedInstr::Sw(i) = instr_d
{
let store_expr =
Expr::add(Expr::reg(i.rs1()), Expr::Const(i.imm().cast_unsigned()));
if store_expr == **ptr {
// This instruction modifies the memory address we dereferenced.
// Update the expression tree.
*self = Expr::reg(i.rs2());
self.optimize();
}
}
}
}
Ok(())
}
}
struct Jump<'a> {
pub addr: u32,
pub next: Option<&'a Jump<'a>>,
}
impl<'a> Jump<'a> {
pub fn new(addr: u32) -> Self {
Self { addr, next: None }
}
pub fn extend<'b: 'a>(&'b self, addr: u32) -> Jump<'b> {
Self {
addr,
next: Some(self),
}
}
pub fn branch_trace(&self) -> Vec<u32> {
let mut result = vec![self.addr];
let mut jump = self;
while let Some(j) = jump.next {
result.push(j.addr);
jump = j;
}
result
}
}
struct Solution {
// The resolved constants, matching the indexes supplied in the `regs` slice
// to `solve()` (for example, the solution to `regs[1]` will be in
// `Solution::results[1]`)
results: Vec<u32>,
// The address of branch/jump instructions between where the solution was
// found and the address provided to solve().
branch_trace: Vec<u32>,
}
fn find_symbol_address(
t: &object::read::elf::SymbolTable<object::elf::FileHeader32<LittleEndian>>,
name: &str,
) -> anyhow::Result<u64> {
const E: object::LittleEndian = object::LittleEndian;
for sym in t.symbols() {
if t.symbol_name(E, sym)? == name.as_bytes() {
return Ok(sym.st_value.get(E).into());
}
}
Err(anyhow!("Unable to find symbol {name:?}"))
}