use std::{fs::File, io::{BufRead, BufReader, Seek, SeekFrom}};
use std::collections::HashSet;
use crate::ruwind::elf::{ElfLoadHeader, ElfSymbol, ElfSymbolIterator};
use tracing::{info, trace, warn};
use crate::helpers::exporting::ExportMachine;
pub const SYM_FLAG_MUST_MATCH: u8 = 1 << 0;
pub struct SymbolPageMap {
pages: HashSet<u64>,
page_size: u64,
page_mask: u64,
}
impl SymbolPageMap {
pub fn new(page_size: u64) -> Self {
Self {
pages: HashSet::new(),
page_size,
page_mask: !(page_size.next_power_of_two() - 1),
}
}
pub fn mark_ip(
&mut self,
ip: u64) {
let page = ip & self.page_mask;
self.pages.insert(page);
}
pub fn seen_range(
&self,
start: u64,
end: u64) -> bool {
let mut page = start & self.page_mask;
let end_page = end & self.page_mask;
while page <= end_page {
if self.pages.contains(&page) {
return true;
}
page += self.page_size;
}
false
}
}
pub struct DynamicSymbol<'a> {
time: u64,
pid: u32,
start: u64,
end: u64,
name: &'a str,
flags: u8,
}
impl<'a> DynamicSymbol<'a> {
pub fn new(
time: u64,
pid: u32,
start: u64,
end: u64,
name: &'a str) -> Self {
Self {
time,
pid,
start,
end,
name,
flags: 0,
}
}
pub fn time(&self) -> u64 { self.time }
pub fn pid(&self) -> u32 { self.pid }
pub fn start(&self) -> u64 { self.start }
pub fn end(&self) -> u64 { self.end }
pub fn name(&self) -> &str { self.name }
pub fn flags(&self) -> u8 { self.flags }
pub fn set_flag(
&mut self,
flag: u8) {
self.flags |= flag;
}
pub fn has_flag(
&self,
flag: u8) -> bool {
self.flags & flag == flag
}
pub fn to_export_time_symbol(
&self,
machine: &mut ExportMachine) -> ExportTimeSymbol {
ExportTimeSymbol::new(
self.time,
ExportSymbol {
name_id: machine.strings.to_id(self.name),
start: self.start,
end: self.end,
})
}
}
#[derive(Clone)]
pub struct ExportSymbol {
name_id: usize,
start: u64,
end: u64,
}
impl ExportSymbol {
pub fn new(
name_id: usize,
start: u64,
end: u64) -> Self {
Self {
name_id,
start,
end,
}
}
pub fn name_id(&self) -> usize { self.name_id }
pub fn start(&self) -> u64 { self.start }
pub fn end(&self) -> u64 { self.end }
}
#[derive(Clone)]
pub struct ExportTimeSymbol {
time: u64,
symbol: ExportSymbol,
}
impl ExportTimeSymbol {
pub fn new(
time: u64,
symbol: ExportSymbol) -> Self {
Self {
time,
symbol,
}
}
pub fn time(&self) -> u64 { self.time }
pub fn symbol(&self) -> ExportSymbol { self.symbol.clone() }
}
pub trait ExportSymbolReader {
fn reset(&mut self);
fn next(&mut self) -> bool;
fn start(&self) -> u64;
fn end(&self) -> u64;
fn name(&self) -> &str;
fn demangle(&mut self) -> Option<String>;
}
pub struct KernelSymbolReader {
reader: Option<BufReader<File>>,
buffer: String,
current_ip: u64,
current_end: Option<u64>,
current_name: String,
next_ip: Option<u64>,
next_name: String,
done: bool,
}
impl KernelSymbolReader {
pub fn new() -> Self {
Self {
reader: None,
buffer: String::with_capacity(64),
current_name: String::with_capacity(64),
current_ip: 0,
current_end: None,
next_ip: None,
next_name: String::with_capacity(64),
done: true,
}
}
pub fn set_file(
&mut self,
file: File) {
self.reader = Some(BufReader::new(file));
self.reset()
}
fn load_next(&mut self) {
if let Some(ip) = self.next_ip {
self.current_ip = ip;
self.current_end = None;
self.current_name.clear();
self.current_name.push_str(&self.next_name);
self.next_ip = None;
self.next_name.clear();
}
if let Some(reader) = &mut self.reader {
loop {
self.buffer.clear();
if let Ok(len) = reader.read_line(&mut self.buffer) {
if len == 0 {
break;
}
} else {
break;
}
let mut addr: u64 = 0;
let mut symtype: &str = "";
let mut method: &str = "";
let mut module: Option<&str> = None;
for (index, part) in self.buffer.split_whitespace().enumerate() {
match index {
0 => {
addr = u64::from_str_radix(part, 16).unwrap();
},
1 => {
symtype = part;
},
2 => {
method = part;
},
3 => {
module = Some(part);
},
_ => {},
}
}
if self.current_end.is_none() && self.current_ip != 0 {
self.current_end = Some(addr - 1);
}
if !symtype.starts_with('t') && !symtype.starts_with('T') {
continue;
}
self.next_ip = Some(addr);
if let Some(module) = module {
self.next_name.push_str(module);
self.next_name.push_str(" ");
}
self.next_name.push_str(method);
self.done = false;
return;
}
}
self.done = true;
}
}
impl ExportSymbolReader for KernelSymbolReader {
fn reset(&mut self) {
self.current_ip = 0;
self.current_end = None;
self.next_ip = None;
self.done = true;
if let Some(reader) = &mut self.reader {
if reader.seek(SeekFrom::Start(0)).is_ok() {
self.done = false;
self.load_next();
return;
}
}
if let Ok(file) = File::open("/proc/kallsyms") {
self.reader = Some(BufReader::new(file));
self.done = false;
self.load_next();
info!("Kernel symbol reader initialized from /proc/kallsyms");
} else {
warn!("Failed to open /proc/kallsyms");
}
}
fn next(&mut self) -> bool {
if self.done {
return false;
}
self.load_next();
true
}
fn start(&self) -> u64 {
self.current_ip
}
fn end(&self) -> u64 {
match self.current_end {
Some(end) => { end },
None => { 0xFFFFFFFFFFFFFFFF },
}
}
fn name(&self) -> &str {
&self.current_name
}
fn demangle(&mut self) -> Option<String> {
None
}
}
pub struct ElfSymbolReader<'a> {
iterator: ElfSymbolIterator<'a>,
current_sym: ElfSymbol,
current_sym_valid: bool,
}
impl<'a> ElfSymbolReader<'a> {
pub fn new(file: File, load_header: ElfLoadHeader, system_page_size: u64) -> Self {
Self {
iterator: ElfSymbolIterator::new(file, load_header, system_page_size),
current_sym: ElfSymbol::new(),
current_sym_valid: false,
}
}
}
impl<'a> ExportSymbolReader for ElfSymbolReader<'a> {
fn reset(&mut self) {
self.iterator.reset();
self.current_sym_valid = false;
}
fn next(&mut self) -> bool {
self.current_sym_valid = self.iterator.next(&mut self.current_sym);
self.current_sym_valid
}
fn start(&self) -> u64 {
let mut start = 0u64;
if self.current_sym_valid {
start = self.current_sym.start();
}
start
}
fn end(&self) -> u64 {
let mut end = 0u64;
if self.current_sym_valid {
end = self.current_sym.end();
}
end
}
fn name(&self) -> &str {
let mut name = "";
if self.current_sym_valid {
name = self.current_sym.name();
}
name
}
fn demangle(&mut self) -> Option<String> {
let mut demangled_name = None;
if self.current_sym_valid {
demangled_name = self.current_sym.demangle();
}
demangled_name
}
}
pub struct PerfMapSymbolReader {
reader: BufReader<File>,
buffer: String,
start_ip: u64,
end_ip: u64,
name: String,
done: bool,
}
impl PerfMapSymbolReader {
pub fn new(file: File) -> Self {
Self {
reader: BufReader::new(file),
buffer: String::with_capacity(256),
name: String::with_capacity(256),
start_ip: 0,
end_ip: 0,
done: true,
}
}
fn load_next(&mut self) {
loop {
self.buffer.clear();
self.start_ip = 0;
self.end_ip = 0;
self.name.clear();
if let Ok(len) = self.reader.read_line(&mut self.buffer) {
if len == 0 {
trace!("PerfMap load_next: end of file");
break;
}
} else {
trace!("PerfMap load_next: read error");
break;
}
trace!("PerfMap load_next: parsing line={}", self.buffer.trim());
let mut parts = self.buffer.splitn(3, ' ');
let Some(start_str) = parts.next() else {
continue;
};
let start_str = if start_str.starts_with("0x") || start_str.starts_with("0X") {
&start_str[2..]
} else {
start_str
};
self.start_ip = match u64::from_str_radix(start_str, 16) {
Ok(start_ip) => start_ip,
Err(_) => {
warn!("PerfMap load_next: skipping malformed line={}", self.buffer.trim());
continue;
}
};
let Some(size_str) = parts.next() else {
continue;
};
let Ok(size) = u64::from_str_radix(size_str, 16) else {
warn!("PerfMap load_next: skipping malformed line={}", self.buffer.trim());
continue;
};
self.end_ip = self.start_ip + size;
if let Some(name_part) = parts.next() {
let name_part = name_part.split('\0').next().unwrap();
self.name.push_str(name_part);
if self.name.ends_with("\n") {
self.name.pop();
}
if self.name.ends_with("\r") {
self.name.pop();
}
}
self.done = false;
trace!("PerfMap load_next: parsed symbol start_ip={:#x}, end_ip={:#x}, name={}", self.start_ip, self.end_ip, self.name);
return;
}
self.done = true;
}
}
impl ExportSymbolReader for PerfMapSymbolReader {
fn reset(&mut self) {
if self.reader.seek(SeekFrom::Start(0)).is_ok() {
self.done = false;
return;
}
else {
self.start_ip = 0;
self.end_ip = 0;
self.name.clear();
self.done = true;
}
}
fn next(&mut self) -> bool {
if self.done {
return false;
}
self.load_next();
if self.done {
return false;
}
true
}
fn start(&self) -> u64 {
self.start_ip
}
fn end(&self) -> u64 {
self.end_ip
}
fn name(&self) -> &str {
&self.name
}
fn demangle(&mut self) -> Option<String> {
None
}
}
pub struct R2RMapSymbolReader {
reader: BufReader<File>,
buffer: String,
start_ip: u64,
end_ip: u64,
name: String,
done: bool,
signature: [u8; 16],
}
impl R2RMapSymbolReader {
pub fn new(file: File) -> Self {
Self {
reader: BufReader::new(file),
buffer: String::with_capacity(256),
name: String::with_capacity(256),
start_ip: 0,
end_ip: 0,
done: true,
signature: [0; 16],
}
}
pub fn signature(&self) -> &[u8; 16] {
&self.signature
}
fn initialize(&mut self) {
loop {
self.load_next();
if self.done {
break;
}
if self.start_ip == 0xFFFFFFFF {
if Self::read_signature(self.name.as_str(), &mut self.signature).is_err() {
self.signature = [0; 16];
}
break;
}
}
}
fn read_signature(
name: &str,
buf: &mut [u8; 16]) -> anyhow::Result<()> {
if name.len() != 32 {
return Ok(());
}
for i in 0..16 {
let byte_str = &name[(2*i)..(2*i)+2];
buf[i] = u8::from_str_radix(byte_str, 16)?;
}
Ok(())
}
fn load_next(&mut self) {
'load: {
self.buffer.clear();
self.start_ip = 0;
self.end_ip = 0;
self.name.clear();
if let Ok(len) = self.reader.read_line(&mut self.buffer) {
if len == 0 {
break 'load;
}
} else {
break 'load;
}
for (index, part) in self.buffer.splitn(3, ' ').enumerate() {
match index {
0 => {
if part.starts_with("0x") || part.starts_with("0X") {
self.start_ip = u64::from_str_radix(&part[2..], 16).unwrap();
} else {
self.start_ip = u64::from_str_radix(part, 16).unwrap();
}
},
1 => {
let size = u64::from_str_radix(part, 16).unwrap();
self.end_ip = self.start_ip + size;
},
_ => {
let part = part.split('\0').next().unwrap();
self.name.push_str(part);
if self.name.ends_with("\n") {
self.name.pop();
}
if self.name.ends_with("\r") {
self.name.pop();
}
},
}
}
self.done = false;
return;
}
self.done = true;
}
}
impl ExportSymbolReader for R2RMapSymbolReader {
fn reset(&mut self) {
if self.reader.seek(SeekFrom::Start(0)).is_ok() {
self.done = false;
self.initialize();
return;
}
else {
self.start_ip = 0;
self.end_ip = 0;
self.name.clear();
self.done = true;
}
}
fn next(&mut self) -> bool {
loop {
if self.done {
return false;
}
self.load_next();
if self.done {
return false;
}
if self.start_ip <= 0xFFFFFFF0 {
break;
}
}
true
}
fn start(&self) -> u64 {
self.start_ip
}
fn end(&self) -> u64 {
self.end_ip
}
fn name(&self) -> &str {
&self.name
}
fn demangle(&mut self) -> Option<String> {
None
}
}
pub struct R2RLoadedLayoutSymbolTransformer {
sym_reader: R2RMapSymbolReader,
offset: u64,
}
impl R2RLoadedLayoutSymbolTransformer {
pub fn new(
sym_reader: R2RMapSymbolReader,
offset: u64) -> Self {
Self {
sym_reader,
offset,
}
}
}
impl ExportSymbolReader for R2RLoadedLayoutSymbolTransformer {
fn reset(&mut self) {
self.sym_reader.reset()
}
fn next(&mut self) -> bool {
self.sym_reader.next()
}
fn start(&self) -> u64 {
self.sym_reader.start() - self.offset
}
fn end(&self) -> u64 {
self.sym_reader.end() - self.offset
}
fn name(&self) -> &str {
self.sym_reader.name()
}
fn demangle(&mut self) -> Option<String> {
self.sym_reader.demangle()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
use crate::os::system_page_size;
#[cfg(target_os = "linux")]
use std::path::Path;
#[test]
fn kernel_symbol_reader() {
let kern_syms_path = std::env::current_dir().unwrap().join(
"../test/assets/kernel/symbols.map");
let mut reader = KernelSymbolReader::new();
reader.set_file(File::open(kern_syms_path).unwrap());
for _ in 0..4 {
assert!(reader.next());
assert_eq!(0x0A, reader.start());
assert_eq!(0xA9, reader.end());
assert_eq!("method1", reader.name());
assert!(reader.next());
assert_eq!(0xAC, reader.start());
assert_eq!(0xBA, reader.end());
assert_eq!("[module] method2", reader.name());
assert!(reader.next());
assert_eq!(0xBB, reader.start());
assert_eq!(0xFFFFFFFFFFFFFFFF, reader.end());
assert_eq!("method3", reader.name());
assert!(!reader.next());
reader.reset();
}
}
#[test]
fn perf_map_symbol_reader() {
let expected_count = 2435;
let perf_map_path = std::env::current_dir().unwrap().join(
"../test/assets/perfmap/dotnet-info.map");
if let Ok(file) = File::open(perf_map_path.clone()) {
let mut reader = PerfMapSymbolReader::new(file);
reader.reset();
let mut actual_count = 0;
loop {
if !reader.next() {
break;
}
actual_count+=1;
assert!(reader.start() < reader.end(), "Start must be less than end - start: {}, end: {}", reader.start(), reader.end());
assert!(reader.name().len() > 0);
match reader.start() {
0x00007F148458E6A0 => {
assert_eq!(0x00007F148458E6A0 + 0x1B0, reader.end());
assert_eq!(reader.name(), "int32 [System.Private.CoreLib] System.SpanHelpers::IndexOf(char&,char,int32)[OptimizedTier1]");
},
0x00007F1484597400 => {
assert_eq!(0x00007F1484597400 + 0x121, reader.end());
assert_eq!(reader.name(), "native uint [System.Private.CoreLib] System.Text.ASCIIUtility::NarrowUtf16ToAscii(char*,uint8*,native uint)[Optimized]");
},
0x00007F1484F65380 => {
assert_eq!(0x00007F1484F65380 + 0x17e, reader.end());
assert_eq!(reader.name(), "instance bool [System.Linq] System.Linq.Enumerable+SelectListIterator`2[Microsoft.Extensions.DependencyModel.DependencyContextJsonReader+TargetLibrary,System.__Canon]::MoveNext()[QuickJitted]")
},
_ => {},
}
}
assert_eq!(actual_count, expected_count);
}
else {
assert!(false, "Unable to open file {}", perf_map_path.display());
}
}
#[test]
fn r2r_map_symbol_reader() {
let expected_count = 45433;
let r2r_map_path = std::env::current_dir().unwrap().join(
"../test/assets/r2rmap/System.Private.CoreLib.ni.r2rmap");
if let Ok(file) = File::open(r2r_map_path.clone()) {
let mut reader = R2RMapSymbolReader::new(file);
reader.reset();
let expected_signature: [u8; 16] = [
0x7B, 0x8E, 0x67, 0x11, 0xBF, 0xD1, 0xA8, 0x79,
0x11, 0x84, 0xF7, 0xDB, 0x99, 0xCD, 0xB3, 0xA5
];
assert_eq!(&expected_signature, reader.signature());
let mut actual_count = 0;
let mut s1 = false;
let mut s2 = false;
let mut s3 = false;
loop {
if !reader.next() {
break;
}
actual_count+=1;
assert!(reader.start() < reader.end(), "Start must be less than end - start: {}, end: {}", reader.start(), reader.end());
assert!(reader.name().len() > 0);
match reader.start() {
0x0011D1D0 => {
assert_eq!(0x0011D1D0 + 0x23, reader.end());
assert_eq!(reader.name(), "Interop::CheckIo(Interop+Error, System.String, System.Boolean)");
s1 = true;
},
0x0011E880 => {
assert_eq!(0x0011E880 + 0xA5, reader.end());
assert_eq!(reader.name(), "System.Int32 Interop+Globalization::WindowsIdToIanaId(System.String, System.IntPtr, System.Char*, System.Int32)");
s2 = true;
},
0x0011FB00 => {
assert_eq!(0x0011FB00 + 0x54, reader.end());
assert_eq!(reader.name(), "System.Int32 Interop+ErrorInfo::get_RawErrno()");
s3 = true;
},
_ => {},
}
}
assert_eq!(s1, true);
assert_eq!(s2, true);
assert_eq!(s3, true);
assert_eq!(actual_count, expected_count);
}
else {
assert!(false, "Unable to open file {}", r2r_map_path.display());
}
}
#[test]
#[cfg(target_os = "linux")]
fn elf_symbol_reader() {
#[cfg(all(target_arch = "x86_64", target_env = "gnu"))]
let possible_paths = [
"/usr/lib/x86_64-linux-gnu/libc.so.6",
"/usr/lib/libc.so.6"
];
#[cfg(all(target_arch = "x86_64", target_env = "musl"))]
let possible_paths = [
"/lib/ld-musl-x86_64.so.1",
"/lib/libc.musl-x86_64.so.1",
"/usr/lib/libc.musl-x86_64.so.1"
];
#[cfg(all(target_arch = "aarch64", target_env = "gnu"))]
let possible_paths = [
"/usr/lib/aarch64-linux-gnu/libc.so.6",
"/usr/lib/libc.so.6"
];
#[cfg(all(target_arch = "aarch64", target_env = "musl"))]
let possible_paths = [
"/lib/ld-musl-aarch64.so.1",
"/lib/libc.musl-aarch64.so.1",
"/usr/lib/libc.musl-aarch64.so.1"
];
let path = possible_paths
.iter()
.find(|&p| Path::new(p).exists())
.expect("Could not find libc in any expected location");
if let Ok(file) = File::open(path) {
let load_header = ElfLoadHeader::new(0, 0);
let system_page_size = system_page_size();
let mut reader = ElfSymbolReader::new(file, load_header, system_page_size);
reader.reset();
let mut actual_count = 0;
loop {
if !reader.next() {
break;
}
actual_count+=1;
assert!(reader.start() <= reader.end(), "Start must be less than or equal to end - start: {}, end: {}", reader.start(), reader.end());
assert!(reader.name().len() > 0);
}
assert!(actual_count > 0);
}
else {
assert!(false, "Unable to open file {}", path);
}
}
#[test]
fn symbol_page_map() {
let mut map = SymbolPageMap::new(256);
map.mark_ip(0);
map.mark_ip(257);
map.mark_ip(1024);
assert!(map.seen_range(0, 256));
assert!(map.seen_range(257, 257));
assert!(map.seen_range(511, 511));
assert!(!map.seen_range(512, 1023));
assert!(map.seen_range(1024, 4096));
assert!(map.seen_range(1279, 1279));
assert!(!map.seen_range(1280, 4096));
}
#[test]
fn perf_map_skips_malformed_lines() {
let perf_map_path = std::env::current_dir().unwrap().join(
"../test/assets/perfmap/malformed-lines.map");
let file = File::open(perf_map_path).expect("Unable to open test file");
let mut reader = PerfMapSymbolReader::new(file);
reader.reset();
assert!(reader.next());
assert_eq!(0x7a231c4c0020, reader.start());
assert_eq!(0x7a231c4c0020 + 0xc49, reader.end());
assert_eq!(reader.name(), "instance bool [System.Private.CoreLib] System.IO.Enumeration.FileSystemEnumerator`1[System.__Canon]::MoveNext()[OptimizedTier1]");
assert!(reader.next());
assert_eq!(0x7a231c4c0cb0, reader.start());
assert_eq!(0x7a231c4c0cb0 + 0x4, reader.end());
assert!(reader.next());
assert_eq!(0x7402ec4bb340, reader.start());
assert_eq!(0x7402ec4bb340 + 0x1668, reader.end());
assert_eq!(reader.name(), "JS:*'parserOnIncoming node:_http_server:1084:26");
assert!(reader.next());
assert_eq!(0x7402ec4bca40, reader.start());
assert_eq!(0x7402ec4bca40 + 0x11e0, reader.end());
assert_eq!(reader.name(), "JS:*'write_ node:_http_outgoing:887:16");
assert!(reader.next());
assert_eq!(0x7402EC2F8640, reader.start());
assert_eq!(0x7402EC2F8640 + 0x250, reader.end());
assert_eq!(reader.name(), "JS:+ node:internal/deps/undici/undici:3179:40");
assert!(!reader.next());
}
}