Skip to content

Commit

Permalink
Add AIX support
Browse files Browse the repository at this point in the history
- Use `loadquery` to get loaded libraries
- Use object crate to parse XCOFF and get image information
  • Loading branch information
Kai Luo committed Aug 23, 2023
1 parent f3af31c commit ecb214b
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ miniz_oxide = { version = "0.7.0", default-features = false }
[dependencies.object]
version = "0.31.1"
default-features = false
features = ['read_core', 'elf', 'macho', 'pe', 'unaligned', 'archive']
features = ['read_core', 'elf', 'macho', 'pe', 'xcoff', 'unaligned', 'archive']

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", optional = true }
Expand Down
2 changes: 1 addition & 1 deletion crates/as-if-std/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ miniz_oxide = { version = "0.7", default-features = false }
version = "0.31.1"
default-features = false
optional = true
features = ['read_core', 'elf', 'macho', 'pe', 'unaligned', 'archive']
features = ['read_core', 'elf', 'macho', 'pe', 'xcoff', 'unaligned', 'archive']

[features]
default = ['backtrace']
Expand Down
43 changes: 39 additions & 4 deletions src/symbolize/gimli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ cfg_if::cfg_if! {
target_os = "openbsd",
target_os = "solaris",
target_os = "illumos",
target_os = "aix",
))] {
#[path = "gimli/mmap_unix.rs"]
mod mmap;
Expand Down Expand Up @@ -116,8 +117,17 @@ impl<'data> Context<'data> {
dwp: Option<Object<'data>>,
) -> Option<Context<'data>> {
let mut sections = gimli::Dwarf::load(|id| -> Result<_, ()> {
let data = object.section(stash, id.name()).unwrap_or(&[]);
Ok(EndianSlice::new(data, Endian))
if cfg!(not(target_os = "aix")) {
let data = object.section(stash, id.name()).unwrap_or(&[]);
Ok(EndianSlice::new(data, Endian))
} else {
if let Some(name) = id.xcoff_name() {
let data = object.section(stash, name).unwrap_or(&[]);
Ok(EndianSlice::new(data, Endian))
} else {
Ok(EndianSlice::new(&[], Endian))
}
}
})
.ok()?;

Expand Down Expand Up @@ -192,6 +202,9 @@ cfg_if::cfg_if! {
))] {
mod macho;
use self::macho::{handle_split_dwarf, Object};
} else if #[cfg(target_os = "aix")] {
mod xcoff;
use self::xcoff::{handle_split_dwarf, Object};
} else {
mod elf;
use self::elf::{handle_split_dwarf, Object};
Expand Down Expand Up @@ -234,6 +247,9 @@ cfg_if::cfg_if! {
} else if #[cfg(target_os = "haiku")] {
mod libs_haiku;
use libs_haiku::native_libraries;
} else if #[cfg(target_os = "aix")] {
mod libs_aix;
use libs_aix::native_libraries;
} else {
// Everything else should doesn't know how to load native libraries.
fn native_libraries() -> Vec<Library> {
Expand Down Expand Up @@ -261,6 +277,13 @@ struct Cache {

struct Library {
name: OsString,
#[cfg(target_os = "aix")]
/// On AIX, the library mmapped can be a member of a big-archive file.
/// For example, with a big-archive named libfoo.a containing libbar.so,
/// one can use `dlopen("libfoo.a(libbar.so)", RTLD_MEMBER | RTLD_LAZY)`
/// to use the `libbar.so` library. In this case, only `libbar.so` is
/// mmapped, not the whole `libfoo.a`.
member_name: OsString,
/// Segments of this library loaded into memory, and where they're loaded.
segments: Vec<LibrarySegment>,
/// The "bias" of this library, typically where it's loaded into memory.
Expand All @@ -280,6 +303,19 @@ struct LibrarySegment {
len: usize,
}

#[cfg(target_os = "aix")]
fn create_mapping(lib: &Library) -> Option<Mapping> {
let name = &lib.name;
let member_name = &lib.member_name;
Mapping::new(name.as_ref(), member_name)
}

#[cfg(not(target_os = "aix"))]
fn create_mapping(lib: &Library) -> Option<Mapping> {
let name = &lib.name;
Mapping::new(name.as_ref())
}

// unsafe because this is required to be externally synchronized
pub unsafe fn clear_symbol_cache() {
Cache::with_global(|cache| cache.mappings.clear());
Expand Down Expand Up @@ -360,8 +396,7 @@ impl Cache {
// When the mapping is not in the cache, create a new mapping,
// insert it into the front of the cache, and evict the oldest cache
// entry if necessary.
let name = &self.libraries[lib].name;
let mapping = Mapping::new(name.as_ref())?;
let mapping = create_mapping(&self.libraries[lib])?;

if self.mappings.len() == MAPPINGS_CACHE_SIZE {
self.mappings.pop();
Expand Down
74 changes: 74 additions & 0 deletions src/symbolize/gimli/libs_aix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use super::mystd::borrow::ToOwned;
use super::mystd::env;
use super::mystd::ffi::{CStr, OsStr};
use super::mystd::io::Error;
use super::mystd::os::unix::prelude::*;
use super::xcoff;
use super::{Library, LibrarySegment, Vec};
use alloc::vec;
use core::mem;

const EXE_IMAGE_BASE: u64 = 0x100000000;

/// On AIX, we use `loadquery` with `L_GETINFO` flag to query libraries mmapped.
/// See https://www.ibm.com/docs/en/aix/7.2?topic=l-loadquery-subroutine for
/// detailed information of `loadquery`.
pub(super) fn native_libraries() -> Vec<Library> {
let mut ret = Vec::new();
unsafe {
let mut buffer = vec![mem::zeroed::<libc::ld_info>(); 64];
loop {
if libc::loadquery(
libc::L_GETINFO,
buffer.as_mut_ptr() as *mut libc::c_char,
(mem::size_of::<libc::ld_info>() * buffer.len()) as u32,
) != -1
{
break;
} else {
match Error::last_os_error().raw_os_error() {
Some(libc::ENOMEM) => {
buffer.resize(buffer.len() * 2, mem::zeroed::<libc::ld_info>());
}
Some(_) => {
// If other error occurs, return empty libraries.
return Vec::new();
}
_ => unreachable!(),
}
}
}
let mut current = buffer.as_mut_ptr();
loop {
let text_base = (*current).ldinfo_textorg as usize;
let filename_ptr: *const libc::c_char = &(*current).ldinfo_filename[0];
let bytes = CStr::from_ptr(filename_ptr).to_bytes();
let member_name_ptr = filename_ptr.offset((bytes.len() + 1) as isize);
let mut filename = OsStr::from_bytes(bytes).to_owned();
if text_base == EXE_IMAGE_BASE as usize {
if let Ok(exe) = env::current_exe() {
filename = exe.into_os_string();
}
}
let bytes = CStr::from_ptr(member_name_ptr).to_bytes();
let member_name = OsStr::from_bytes(bytes).to_owned();
if let Some(image) = xcoff::parse_image(filename.as_ref(), &member_name) {
ret.push(Library {
name: filename,
member_name,
segments: vec![LibrarySegment {
stated_virtual_memory_address: image.base as usize,
len: image.size,
}],
bias: (text_base + image.offset).wrapping_sub(image.base as usize),
});
}
if (*current).ldinfo_next == 0 {
break;
}
current = (current as *mut libc::c_char).offset((*current).ldinfo_next as isize)
as *mut libc::ld_info;
}
}
return ret;
}
186 changes: 186 additions & 0 deletions src/symbolize/gimli/xcoff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use super::mystd::ffi::{OsStr, OsString};
use super::mystd::os::unix::ffi::OsStrExt;
use super::mystd::str;
use super::{gimli, Context, Endian, EndianSlice, Mapping, Path, Stash, Vec};
use alloc::sync::Arc;
use core::ops::Deref;
use object::read::archive::ArchiveFile;
use object::read::xcoff::{FileHeader, SectionHeader, XcoffFile, XcoffSymbol};
use object::Object as _;
use object::ObjectSection as _;
use object::ObjectSymbol as _;
use object::SymbolFlags;

#[cfg(target_pointer_width = "32")]
type Xcoff = object::xcoff::FileHeader32;
#[cfg(target_pointer_width = "64")]
type Xcoff = object::xcoff::FileHeader64;

impl Mapping {
pub fn new(path: &Path, member_name: &OsString) -> Option<Mapping> {
let map = super::mmap(path)?;
Mapping::mk(map, |data, stash| {
if member_name.is_empty() {
Context::new(stash, Object::parse(data)?, None, None)
} else {
let archive = ArchiveFile::parse(data).ok()?;
for member in archive
.members()
.filter_map(|m| m.ok())
.filter(|m| OsStr::from_bytes(m.name()) == member_name)
{
let member_data = member.data(data).ok()?;
if let Some(obj) = Object::parse(member_data) {
return Context::new(stash, obj, None, None);
}
}
None
}
})
}
}

struct ParsedSym<'a> {
address: u64,
size: u64,
name: &'a str,
}

pub struct Object<'a> {
syms: Vec<ParsedSym<'a>>,
file: XcoffFile<'a, Xcoff>,
}

pub struct Image {
pub offset: usize,
pub base: u64,
pub size: usize,
}

pub fn parse_xcoff(data: &[u8]) -> Option<Image> {
let mut offset = 0;
let header = Xcoff::parse(data, &mut offset).ok()?;
let _ = header.aux_header(data, &mut offset).ok()?;
let sections = header.sections(data, &mut offset).ok()?;
if let Some(section) = sections.iter().find(|s| {
if let Ok(name) = str::from_utf8(&s.s_name()[0..5]) {
name == ".text"
} else {
false
}
}) {
Some(Image {
offset: section.s_scnptr() as usize,
base: section.s_paddr() as u64,
size: section.s_size() as usize,
})
} else {
None
}
}

pub fn parse_image(path: &Path, member_name: &OsString) -> Option<Image> {
let map = super::mmap(path)?;
let data = map.deref();
if member_name.is_empty() {
return parse_xcoff(data);
} else {
let archive = ArchiveFile::parse(data).ok()?;
for member in archive
.members()
.filter_map(|m| m.ok())
.filter(|m| OsStr::from_bytes(m.name()) == member_name)
{
let member_data = member.data(data).ok()?;
if let Some(image) = parse_xcoff(member_data) {
return Some(image);
}
}
None
}
}

impl<'a> Object<'a> {
fn get_concrete_size(file: &XcoffFile<'a, Xcoff>, sym: &XcoffSymbol<'a, '_, Xcoff>) -> u64 {
match sym.flags() {
SymbolFlags::Xcoff {
n_sclass: _,
x_smtyp: _,
x_smclas: _,
containing_csect: Some(index),
} => {
if let Ok(tgt_sym) = file.symbol_by_index(index) {
Self::get_concrete_size(file, &tgt_sym)
} else {
0
}
}
_ => sym.size(),
}
}

fn parse(data: &'a [u8]) -> Option<Object<'a>> {
let file = XcoffFile::parse(data).ok()?;
let mut syms = file
.symbols()
.filter_map(|sym| {
let name = sym.name().map_or("", |v| v);
let address = sym.address();
let size = Self::get_concrete_size(&file, &sym);
if name == ".text" || name == ".data" {
// We don't want to include ".text" and ".data" symbols.
// If they are included, since their ranges cover other
// symbols, when searching a symbol for a given address,
// ".text" or ".data" is returned. That's not what we expect.
None
} else {
Some(ParsedSym {
address,
size,
name,
})
}
})
.collect::<Vec<_>>();
syms.sort_by_key(|s| s.address);
Some(Object { syms, file })
}

pub fn section(&self, _: &Stash, name: &str) -> Option<&'a [u8]> {
Some(self.file.section_by_name(name)?.data().ok()?)
}

pub fn search_symtab<'b>(&'b self, addr: u64) -> Option<&'b [u8]> {
// Symbols, except ".text" and ".data", are sorted and are not overlapped each other,
// so we can just perform a binary search here.
let i = match self.syms.binary_search_by_key(&addr, |sym| sym.address) {
Ok(i) => i,
Err(i) => i.checked_sub(1)?,
};
let sym = self.syms.get(i)?;
if (sym.address..sym.address + sym.size).contains(&addr) {
// On AIX, for a function call, for example, `foo()`, we have
// two symbols `foo` and `.foo`. `foo` references the function
// descriptor and `.foo` references the function entry.
// See https://www.ibm.com/docs/en/xl-fortran-aix/16.1.0?topic=calls-linkage-convention-function
// for more information.
// We trim the prefix `.` here, so that the rust demangler can work
// properly.
Some(sym.name.trim_start_matches(".").as_bytes())
} else {
None
}
}

pub(super) fn search_object_map(&self, _addr: u64) -> Option<(&Context<'_>, u64)> {
None
}
}

pub(super) fn handle_split_dwarf<'data>(
_package: Option<&gimli::DwarfPackage<EndianSlice<'data, Endian>>>,
_stash: &'data Stash,
_load: addr2line::SplitDwarfLoad<EndianSlice<'data, Endian>>,
) -> Option<Arc<gimli::Dwarf<EndianSlice<'data, Endian>>>> {
None
}
2 changes: 2 additions & 0 deletions tests/accuracy/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ fn doit() {
dir.push("dylib_dep.dll");
} else if cfg!(target_os = "macos") {
dir.push("libdylib_dep.dylib");
} else if cfg!(target_os = "aix") {
dir.push("libdylib_dep.a");
} else {
dir.push("libdylib_dep.so");
}
Expand Down
Loading

0 comments on commit ecb214b

Please sign in to comment.