blob: bf582fe1ac36cf951b692b9705c3b8c86a10a5d6 [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.
use std::fs;
use std::path::Path;
use anyhow::{Context, Result, anyhow};
use object::{Endian, File, Object, ObjectSection};
use crate::{
PROCESS_SECTION_NAME, STACK_SECTION_NAME, THREAD_SECTION_NAME, TRACE_BUFFER_SECTION_NAME,
};
#[derive(Debug, Clone)]
pub struct StackInfo {
pub name: String,
pub stack_addr: u64,
pub stack_size: u64,
}
#[derive(Debug, Clone)]
pub struct ThreadInfo {
pub name: String,
pub id: u64,
pub parent_id: u64,
}
#[derive(Debug, Clone)]
pub struct ProcessInfo {
pub name: String,
pub id: u64,
}
#[derive(Debug, Clone)]
pub struct TraceBufferInfo {
pub name: String,
pub addr: u64,
pub size: u64,
}
#[derive(Debug, Clone)]
pub struct ImageInfo {
pub stacks: Vec<StackInfo>,
pub threads: Vec<ThreadInfo>,
pub processes: Vec<ProcessInfo>,
pub trace_buffers: Vec<TraceBufferInfo>,
pub endian: object::Endianness,
}
impl ImageInfo {
/// Create `ImageInfo` by loading and parsing an ELF binary file at `path`.
pub fn new(path: &Path) -> Result<Self> {
Self::from_path(path)
}
/// Create `ImageInfo` by loading and parsing an ELF binary file at `path`.
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
let bin_data = fs::read(path)?;
Self::from_bytes(&bin_data)
}
/// Create `ImageInfo` by parsing ELF binary data bytes.
pub fn from_bytes(bin_data: &[u8]) -> Result<Self> {
let obj_file = object::File::parse(bin_data)?;
Self::from_object(&obj_file)
}
/// Create `ImageInfo` from an already parsed [`object::File`].
pub fn from_object(obj_file: &File<'_>) -> Result<Self> {
let endian = obj_file.endianness();
let stacks = Self::extract_stacks(obj_file)?
.with_context(|| format!("Failed to find section {STACK_SECTION_NAME}"))?;
let threads = Self::extract_threads(obj_file)?
.with_context(|| format!("Failed to find section {THREAD_SECTION_NAME}"))?;
let processes = Self::extract_processes(obj_file)?
.with_context(|| format!("Failed to find section {PROCESS_SECTION_NAME}"))?;
let trace_buffers = Self::extract_trace_buffers(obj_file)?.unwrap_or_default();
Ok(ImageInfo {
stacks,
threads,
endian,
processes,
trace_buffers,
})
}
fn get_section_data<'a>(obj_file: &File<'a>, section_name: &str) -> Result<Option<&'a [u8]>> {
let Some(section) = obj_file.section_by_name(section_name) else {
return Ok(None);
};
let data = section.data().context("Failed to read section data")?;
Ok(Some(data))
}
fn extract_stacks(obj_file: &File<'_>) -> Result<Option<Vec<StackInfo>>> {
Self::extract_entries(obj_file, STACK_SECTION_NAME, 4, |fields| {
let name = Self::read_string(obj_file, fields[0], fields[1])
.context("Failed to read stack name")?;
Ok(StackInfo {
name,
stack_addr: fields[2],
stack_size: fields[3],
})
})
}
fn extract_threads(obj_file: &File<'_>) -> Result<Option<Vec<ThreadInfo>>> {
Self::extract_entries(obj_file, THREAD_SECTION_NAME, 4, |fields| {
let name = Self::read_string(obj_file, fields[0], fields[1])
.context("Failed to read thread name")?;
Ok(ThreadInfo {
name,
id: fields[2],
parent_id: fields[3],
})
})
}
fn extract_processes(obj_file: &File<'_>) -> Result<Option<Vec<ProcessInfo>>> {
Self::extract_entries(obj_file, PROCESS_SECTION_NAME, 3, |fields| {
let name = Self::read_string(obj_file, fields[0], fields[1])
.context("Failed to read process name")?;
Ok(ProcessInfo {
name,
id: fields[2],
})
})
}
fn extract_trace_buffers(obj_file: &File<'_>) -> Result<Option<Vec<TraceBufferInfo>>> {
Self::extract_entries(obj_file, TRACE_BUFFER_SECTION_NAME, 4, |fields| {
let name = Self::read_string(obj_file, fields[0], fields[1])
.context("Failed to read trace buffer name")?;
Ok(TraceBufferInfo {
name,
addr: fields[2],
size: fields[3],
})
})
}
/// Extracts entries from a named linker section in an ELF object file.
///
/// Returns `Ok(Some(entries))` if the section is present in the object file,
/// `Ok(None)` if the section is not found, or `Err` if reading or parsing
/// the section data fails.
fn extract_entries<T, F>(
obj_file: &File<'_>,
section_name: &str,
num_fields: usize,
mapper: F,
) -> Result<Option<Vec<T>>>
where
F: Fn(&[u64]) -> Result<T>,
{
let Some(data) = Self::get_section_data(obj_file, section_name)? else {
return Ok(None);
};
let is_64 = obj_file.is_64();
let field_size = if is_64 { 8 } else { 4 };
let entry_size = num_fields * field_size;
if data.len() % entry_size != 0 {
return Err(anyhow!(
"{} section size is not a multiple of {}",
section_name,
entry_size
));
}
let endian = obj_file.endianness();
let mut entries = Vec::new();
for entry_data in data.chunks(entry_size) {
let mut fields = Vec::with_capacity(num_fields);
for i in 0..num_fields {
fields.push(Self::extract_usize_field(endian, entry_data, i, is_64)?);
}
entries.push(mapper(&fields)?);
}
Ok(Some(entries))
}
fn extract_usize_field(
endian: object::Endianness,
chunk: &[u8],
index: usize,
is_64: bool,
) -> Result<u64> {
if is_64 {
let start = index * 8;
let bytes = chunk
.get(start..start + 8)
.context("Chunk too small for 64-bit field")?;
Ok(endian.read_u64_bytes(bytes.try_into()?))
} else {
let start = index * 4;
let bytes = chunk
.get(start..start + 4)
.context("Chunk too small for 32-bit field")?;
Ok(u64::from(endian.read_u32_bytes(bytes.try_into()?)))
}
}
fn read_data<'a>(obj_file: &'a object::File, addr: u64, len: u64) -> Result<&'a [u8]> {
// Find the section containing the address
for section in obj_file.sections() {
let section_addr = section.address();
let section_size = section.size();
if addr >= section_addr && addr + len <= section_addr + section_size {
let data = section.data()?;
let offset = usize::try_from(addr - section_addr).context("Offset too large")?;
return Ok(&data[offset..offset + usize::try_from(len).context("Len too large")?]);
}
}
Err(anyhow!("Could not find address {:#x} in any section", addr))
}
fn read_string(obj_file: &object::File, addr: u64, len: u64) -> Result<String> {
let bytes = Self::read_data(obj_file, addr, len)?;
Ok(String::from_utf8_lossy(bytes).into_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::STACK_SECTION_NAME;
#[test]
fn invalid_elf_bytes_returns_error() {
let invalid_bytes = b"not an elf file";
let res = ImageInfo::from_bytes(invalid_bytes);
res.unwrap_err();
}
#[test]
fn non_existent_file_returns_error() {
let res = ImageInfo::from_path(Path::new("non_existent_file_path.elf"));
res.unwrap_err();
}
#[test]
fn missing_section_yields_none() {
let obj = object::write::Object::new(
object::BinaryFormat::Elf,
object::Architecture::X86_64,
object::Endianness::Little,
);
let data = obj.write().unwrap();
let obj_file = object::File::parse(&*data).unwrap();
let res: Result<Option<Vec<StackInfo>>> =
ImageInfo::extract_entries(&obj_file, ".non_existent_section", 4, |_| {
unreachable!();
});
assert!(res.unwrap().is_none());
}
#[test]
fn missing_required_sections_returns_error() {
let obj = object::write::Object::new(
object::BinaryFormat::Elf,
object::Architecture::X86_64,
object::Endianness::Little,
);
let data = obj.write().unwrap();
let res = ImageInfo::from_bytes(&data);
res.unwrap_err();
}
#[test]
fn missing_optional_sections_returns_empty_vec() {
let mut obj = object::write::Object::new(
object::BinaryFormat::Elf,
object::Architecture::X86_64,
object::Endianness::Little,
);
let _ = obj.add_section(
Vec::new(),
STACK_SECTION_NAME.as_bytes().to_vec(),
object::SectionKind::ReadOnlyData,
);
let _ = obj.add_section(
Vec::new(),
THREAD_SECTION_NAME.as_bytes().to_vec(),
object::SectionKind::ReadOnlyData,
);
let _ = obj.add_section(
Vec::new(),
PROCESS_SECTION_NAME.as_bytes().to_vec(),
object::SectionKind::ReadOnlyData,
);
let data = obj.write().unwrap();
let image_info = ImageInfo::from_bytes(&data).unwrap();
assert!(image_info.trace_buffers.is_empty());
assert!(image_info.stacks.is_empty());
assert!(image_info.threads.is_empty());
assert!(image_info.processes.is_empty());
}
}