#![allow(non_upper_case_globals)]
use gimli::write::{EndianVec, Writer};
use gimli::{DebugAddrIndex, Reader, RunTimeEndian, Section};
use hashbrown::HashMap;
use itertools::izip;
use std::collections::{BTreeMap, BTreeSet};
use std::mem;
use tracing::{debug, trace, warn};
use crate::{
error::{Error, Result},
package::DwoId,
relocate::Relocate,
};
pub(crate) struct GcResult {
pub rewritten: Option<EndianVec<RunTimeEndian>>,
pub offset_remap: Option<BTreeMap<gimli::UnitOffset, (gimli::UnitOffset, usize)>>,
pub referenced_rnglists: Option<BTreeSet<u64>>,
pub referenced_loclists: Option<BTreeSet<u64>>,
pub referenced_str_offsets: Option<BTreeSet<u64>>,
}
pub(crate) fn is_tombstone(addr: u64, address_size: u8) -> bool {
let negative_one = match address_size {
4 => 0xffff_ffff_u64,
8 => 0xffff_ffff_ffff_ffff_u64,
_ => return false,
};
addr == 0 || addr == negative_one
}
fn encode_raw_rng_entry(
entry: &gimli::RawRngListEntry<usize>,
out: &mut EndianVec<RunTimeEndian>,
address_size: u8,
) -> Result<()> {
use gimli::constants::*;
match *entry {
gimli::RawRngListEntry::BaseAddressx { addr } => {
out.write_u8(DW_RLE_base_addressx.0)?;
out.write_uleb128(addr.0 as u64)?;
}
gimli::RawRngListEntry::StartxEndx { begin, end } => {
out.write_u8(DW_RLE_startx_endx.0)?;
out.write_uleb128(begin.0 as u64)?;
out.write_uleb128(end.0 as u64)?;
}
gimli::RawRngListEntry::StartxLength { begin, length } => {
out.write_u8(DW_RLE_startx_length.0)?;
out.write_uleb128(begin.0 as u64)?;
out.write_uleb128(length)?;
}
gimli::RawRngListEntry::OffsetPair { begin, end }
| gimli::RawRngListEntry::AddressOrOffsetPair { begin, end } => {
out.write_u8(DW_RLE_offset_pair.0)?;
out.write_uleb128(begin)?;
out.write_uleb128(end)?;
}
gimli::RawRngListEntry::BaseAddress { addr } => {
out.write_u8(DW_RLE_base_address.0)?;
out.write_udata(addr, address_size)?;
}
gimli::RawRngListEntry::StartEnd { begin, end } => {
out.write_u8(DW_RLE_start_end.0)?;
out.write_udata(begin, address_size)?;
out.write_udata(end, address_size)?;
}
gimli::RawRngListEntry::StartLength { begin, length } => {
out.write_u8(DW_RLE_start_length.0)?;
out.write_udata(begin, address_size)?;
out.write_uleb128(length)?;
}
}
Ok(())
}
fn reassemble_offset_table_section(
encoded_lists: &[Vec<u8>],
header: &gimli::ListsHeader,
endian: RunTimeEndian,
) -> Result<Vec<u8>> {
let encoding = header.encoding;
let word_size = encoding.format.word_size() as usize;
let new_entry_count = encoded_lists.len() as u32;
let new_offset_array_size = new_entry_count as usize * word_size;
let mut new_offsets: Vec<u64> = Vec::with_capacity(encoded_lists.len());
let mut running_offset: u64 = new_offset_array_size as u64;
for enc in encoded_lists {
new_offsets.push(running_offset);
running_offset += enc.len() as u64;
}
let total_entries_size = running_offset - new_offset_array_size as u64;
let initial_length_size = encoding.format.initial_length_size() as u64;
let new_unit_length: u64 = header.size() as u64 - initial_length_size
+ (new_entry_count as u64 * word_size as u64)
+ total_entries_size;
let mut out = EndianVec::new(endian);
if encoding.format == gimli::Format::Dwarf64 {
out.write_u32(0xffff_ffff)?;
out.write_u64(new_unit_length)?;
} else {
out.write_u32(
new_unit_length.try_into().expect("unit length w/out header larger than u32"),
)?;
}
out.write_u16(encoding.version)?;
out.write_u8(encoding.address_size)?;
out.write_u8(0)?;
out.write_u32(new_entry_count)?;
for &off in &new_offsets {
if encoding.format == gimli::Format::Dwarf64 {
out.write_u64(off)?;
} else {
out.write_u32(off.try_into().expect("offset larger than u32"))?;
}
}
for enc in encoded_lists {
out.write(enc)?;
}
Ok(out.into_vec())
}
pub(crate) fn rewrite_rnglists<IsAddrLive>(
data: gimli::EndianSlice<'_, RunTimeEndian>,
referenced_indices: &BTreeSet<u64>,
is_addr_live: &IsAddrLive,
dwo_id: DwoId,
) -> Result<Option<Vec<u8>>>
where
IsAddrLive: Fn(DebugAddrIndex<usize>) -> Result<bool>,
{
if data.is_empty() {
return Ok(None);
}
let endian = data.endian();
let mut input = data;
let header = gimli::ListsHeader::parse(&mut input)?;
let encoding = header.encoding;
let offset_entry_count = header.offset_entry_count;
let address_size = encoding.address_size;
let header_size = header.size() as usize;
let debug_rnglists = gimli::DebugRngLists::from(data);
let debug_ranges = gimli::DebugRanges::from(gimli::EndianSlice::new(&[][..], endian));
let range_lists = gimli::RangeLists::new(debug_ranges, debug_rnglists);
let base = gimli::DebugRngListsBase(header_size);
struct ParsedList(Vec<gimli::RawRngListEntry<usize>>);
let mut parsed_lists: Vec<ParsedList> = Vec::with_capacity(offset_entry_count as usize);
let mut modified_count = 0;
for list_idx in 0..offset_entry_count as usize {
if !referenced_indices.contains(&(list_idx as u64)) {
trace!(list_idx, "removing unreferenced range list");
continue;
}
let offset = range_lists.get_offset(encoding, base, gimli::DebugRngListsIndex(list_idx))?;
let mut entries: Vec<gimli::RawRngListEntry<usize>> = Vec::new();
let mut base_tombstoned = false;
let mut any_removed = false;
let mut raw_iter = range_lists.raw_ranges(offset, encoding)?;
while let Some(entry) = raw_iter.next()? {
match entry {
gimli::RawRngListEntry::BaseAddressx { addr } => {
if !is_addr_live(addr)? {
base_tombstoned = true;
any_removed = true;
trace!(list_idx, "removing tombstoned base_addressx idx={}", addr.0);
} else {
base_tombstoned = false;
entries.push(entry);
}
}
gimli::RawRngListEntry::BaseAddress { addr } => {
if is_tombstone(addr, address_size) {
base_tombstoned = true;
any_removed = true;
trace!(list_idx, "removing tombstoned base_address addr={:#x}", addr);
} else {
base_tombstoned = false;
entries.push(entry);
}
}
gimli::RawRngListEntry::OffsetPair { .. }
| gimli::RawRngListEntry::AddressOrOffsetPair { .. } => {
if base_tombstoned {
any_removed = true;
trace!(list_idx, "removing offset_pair under tombstoned base");
} else {
entries.push(entry);
}
}
gimli::RawRngListEntry::StartxEndx { begin, .. } => {
if !is_addr_live(begin)? {
any_removed = true;
trace!(list_idx, "removing tombstoned startx_endx");
} else {
entries.push(entry);
}
}
gimli::RawRngListEntry::StartxLength { begin, .. } => {
if !is_addr_live(begin)? {
any_removed = true;
trace!(list_idx, "removing tombstoned startx_length");
} else {
entries.push(entry);
}
}
gimli::RawRngListEntry::StartEnd { begin, .. } => {
if is_tombstone(begin, address_size) {
any_removed = true;
trace!(list_idx, "removing tombstoned start_end");
} else {
entries.push(entry);
}
}
gimli::RawRngListEntry::StartLength { begin, .. } => {
if is_tombstone(begin, address_size) {
any_removed = true;
trace!(list_idx, "removing tombstoned start_length");
} else {
entries.push(entry);
}
}
}
}
if any_removed {
modified_count += 1;
}
parsed_lists.push(ParsedList(entries));
}
let new_entry_count = parsed_lists.len() as u32;
let lists_removed = offset_entry_count - new_entry_count;
if modified_count == 0 && lists_removed == 0 {
return Ok(None);
}
debug!(
?dwo_id,
lists_removed,
modified_count,
total = offset_entry_count,
remaining = new_entry_count,
"rewrite_rnglists: pruned range lists"
);
let mut encoded_lists: Vec<Vec<u8>> = Vec::with_capacity(parsed_lists.len());
for list in &parsed_lists {
let mut buf = EndianVec::new(endian);
for entry in &list.0 {
encode_raw_rng_entry(entry, &mut buf, address_size)?;
}
buf.write_u8(gimli::constants::DW_RLE_end_of_list.0)?;
encoded_lists.push(buf.into_vec());
}
Ok(Some(reassemble_offset_table_section(&encoded_lists, &header, endian)?))
}
pub(crate) fn patch_debug_loc(
data: gimli::EndianSlice<'_, RunTimeEndian>,
encoding: gimli::Encoding,
offset_remap: &BTreeMap<gimli::UnitOffset, (gimli::UnitOffset, usize)>,
) -> Result<Option<Vec<u8>>> {
let raw = data.slice();
if raw.is_empty() {
return Ok(None);
}
let endian = data.endian();
let mut patched = raw.to_vec();
let any_patched = patch_loclist_data(raw, endian, encoding, offset_remap, &mut patched)?;
if any_patched {
Ok(Some(patched))
} else {
Ok(None)
}
}
pub(crate) fn rewrite_loclists(
data: gimli::EndianSlice<'_, RunTimeEndian>,
referenced_indices: Option<&BTreeSet<u64>>,
offset_remap: Option<&BTreeMap<gimli::UnitOffset, (gimli::UnitOffset, usize)>>,
) -> Result<Option<Vec<u8>>> {
if data.is_empty() {
return Ok(None);
}
let endian = data.endian();
let mut input = data;
let header = gimli::ListsHeader::parse(&mut input)?;
let encoding = header.encoding;
let offset_entry_count = header.offset_entry_count;
let header_size = header.size() as usize;
let debug_loclists = gimli::DebugLocLists::from(data);
let debug_loc = gimli::DebugLoc::from(gimli::EndianSlice::new(&[][..], endian));
let loc_lists = gimli::LocationLists::new(debug_loc, debug_loclists);
let raw = data.slice();
if offset_entry_count == 0 {
if let Some(remap) = offset_remap {
let entry_data = &raw[header_size..];
let mut patched = raw.to_vec();
let did_patch = patch_loclist_data(
entry_data,
endian,
encoding,
remap,
&mut patched[header_size..],
)?;
if did_patch {
return Ok(Some(patched));
}
}
return Ok(None);
}
let base = gimli::DebugLocListsBase(header_size);
let mut abs_offsets: Vec<usize> = Vec::with_capacity(offset_entry_count as usize);
for list_idx in 0..offset_entry_count as usize {
let offset = loc_lists.get_offset(encoding, base, gimli::DebugLocListsIndex(list_idx))?;
abs_offsets.push(offset.0);
}
let mut sorted: Vec<(usize, u32)> =
abs_offsets.iter().enumerate().map(|(i, &off)| (off, i as u32)).collect();
sorted.sort_unstable();
let mut list_lengths: Vec<usize> = vec![0; offset_entry_count as usize];
for (i, &(start, idx)) in sorted.iter().enumerate() {
let end = if i + 1 < sorted.len() { sorted[i + 1].0 } else { raw.len() };
list_lengths[idx as usize] = end - start;
}
let mut encoded_lists: Vec<Vec<u8>> =
Vec::with_capacity(referenced_indices.map_or(offset_entry_count as usize, |s| s.len()));
let mut pruned_count: u32 = 0;
let mut any_patched = false;
for list_idx in 0..offset_entry_count as usize {
let keep = referenced_indices.is_none_or(|set| set.contains(&(list_idx as u64)));
if keep {
let start = abs_offsets[list_idx];
let len = list_lengths[list_idx];
let list_bytes = &raw[start..start + len];
if let Some(remap) = offset_remap {
let mut patched = list_bytes.to_vec();
let did_patch =
patch_loclist_data(list_bytes, endian, encoding, remap, &mut patched)?;
any_patched |= did_patch;
encoded_lists.push(patched);
} else {
encoded_lists.push(list_bytes.to_vec());
}
} else {
pruned_count += 1;
trace!(list_idx, "removing unreferenced location list");
}
}
if pruned_count == 0 && !any_patched {
return Ok(None);
}
if pruned_count > 0 {
debug!(
pruned_count,
total = offset_entry_count,
remaining = encoded_lists.len(),
"rewrite_loclists: pruned location lists"
);
}
Ok(Some(reassemble_offset_table_section(&encoded_lists, &header, endian)?))
}
fn patch_loclist_data(
raw: &[u8],
endian: RunTimeEndian,
encoding: gimli::Encoding,
offset_remap: &BTreeMap<gimli::UnitOffset, (gimli::UnitOffset, usize)>,
patched: &mut [u8],
) -> Result<bool> {
use gimli::constants::*;
assert_eq!(raw.len(), patched.len());
let mut any_patched = false;
let mut reader = gimli::EndianSlice::new(raw, endian);
let address_size = encoding.address_size;
while !reader.is_empty() {
let entry_type = reader.read_u8()?;
let has_expr = match DwLle(entry_type) {
DW_LLE_end_of_list => false,
DW_LLE_base_addressx => {
reader.read_uleb128()?;
false
}
DW_LLE_startx_endx => {
reader.read_uleb128()?;
reader.read_uleb128()?;
true
}
DW_LLE_startx_length => {
reader.read_uleb128()?;
if encoding.version >= 5 {
reader.read_uleb128()?;
} else {
reader.read_u32()?;
}
true
}
DW_LLE_offset_pair => {
reader.read_uleb128()?;
reader.read_uleb128()?;
true
}
DW_LLE_default_location => true,
DW_LLE_base_address => {
reader.read_address(address_size)?;
false
}
DW_LLE_start_end => {
reader.read_address(address_size)?;
reader.read_address(address_size)?;
true
}
DW_LLE_start_length => {
reader.read_address(address_size)?;
reader.read_uleb128()?;
true
}
x => return Err(Error::UnsupportedLocListsEntry(x.0)),
};
if has_expr {
let expr_len = if encoding.version >= 5 {
reader.read_uleb128()? as usize
} else {
reader.read_u16()? as usize
};
let expr_slice = reader.split(expr_len)?;
let mut out = EndianVec::new(endian);
emit_expression(expr_slice, offset_remap, encoding, &mut out)?;
if out.slice() != expr_slice.slice() {
let expr_offset = raw.len() - reader.len() - expr_len;
patched[expr_offset..expr_offset + expr_len].copy_from_slice(out.slice());
any_patched = true;
}
}
}
Ok(any_patched)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Liveness {
Dead,
Live,
Retained,
}
struct DieRecord {
parent: Option<usize>,
children: Vec<usize>,
edges: Vec<gimli::UnitOffset>,
liveness: Liveness,
}
fn write_uleb128_padded(
out: &mut EndianVec<RunTimeEndian>,
value: gimli::UnitOffset,
width: usize,
) -> Result<()> {
let mut tmp = value.0;
let mut natural = 0usize;
loop {
natural += 1;
tmp >>= 7;
if tmp == 0 {
break;
}
}
assert!(natural <= width);
let mut v = value.0;
for i in 0..width {
let mut byte = (v & 0x7f) as u8;
v >>= 7;
if i + 1 < width {
byte |= 0x80;
}
out.write_u8(byte)?;
}
Ok(())
}
fn uleb128_len(mut data: gimli::EndianSlice<'_, RunTimeEndian>) -> Result<usize> {
let before = data.len();
data.read_uleb128()?;
Ok(before - data.len())
}
#[derive(Clone, Copy, Debug)]
struct ExprRef {
pos: usize,
old: gimli::UnitOffset,
enc: ExprRefEnc,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ExprRefEnc {
U2,
U4,
Uleb,
}
fn walk_expression<F, G>(
expression: gimli::Expression<gimli::EndianSlice<'_, RunTimeEndian>>,
base: usize,
encoding: gimli::Encoding,
on_ref: &mut F,
on_sec_ref: &mut G,
) -> Result<()>
where
F: FnMut(ExprRef),
G: FnMut(usize),
{
let mut iter = expression.operations(encoding);
let mut start = 0;
while let Some(op) = iter.next()? {
let end = iter.offset_from(&expression);
match op {
gimli::Operation::Call { offset } => match offset {
gimli::DieReference::UnitRef(unit_off) => {
let (enc, opnd_pos) = match gimli::DwOp(expression.0.slice()[start]) {
gimli::constants::DW_OP_call2 => (ExprRefEnc::U2, start + 1),
gimli::constants::DW_OP_call4 => (ExprRefEnc::U4, start + 1),
_ => {
return Err(Error::MalformedDebugInfo);
}
};
on_ref(ExprRef { pos: base + opnd_pos, old: unit_off, enc });
}
gimli::DieReference::DebugInfoRef(off) => {
on_sec_ref(off.0);
}
},
gimli::Operation::ImplicitPointer { value, .. } => {
on_sec_ref(value.0);
}
gimli::Operation::ParameterRef { offset } => {
on_ref(ExprRef { pos: base + start + 1, old: offset, enc: ExprRefEnc::U4 });
}
gimli::Operation::TypedLiteral { base_type, .. } => {
if base_type.0 != 0 {
on_ref(ExprRef {
pos: base + start + 1,
old: base_type,
enc: ExprRefEnc::Uleb,
});
}
}
gimli::Operation::RegisterOffset { base_type, .. } => {
if base_type.0 != 0 {
let reg_len = uleb128_len(expression.0.range_from(start + 1..))?;
on_ref(ExprRef {
pos: base + start + 1 + reg_len,
old: base_type,
enc: ExprRefEnc::Uleb,
});
}
}
gimli::Operation::Deref { base_type, .. } => {
if base_type.0 != 0 {
on_ref(ExprRef {
pos: base + start + 2,
old: base_type,
enc: ExprRefEnc::Uleb,
});
}
}
gimli::Operation::Convert { base_type }
| gimli::Operation::Reinterpret { base_type } => {
if base_type.0 != 0 {
on_ref(ExprRef {
pos: base + start + 1,
old: base_type,
enc: ExprRefEnc::Uleb,
});
}
}
gimli::Operation::EntryValue { expression: sub } => {
let sub_base = base + (end - sub.slice().len());
walk_expression(gimli::Expression(sub), sub_base, encoding, on_ref, on_sec_ref)?;
}
gimli::Operation::VariableValue { offset } => {
on_sec_ref(offset.0);
}
_ => {}
}
start = end;
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RefFormKind {
CuRelative,
SectionAbsolute,
}
fn ref_form_kind(form: gimli::DwForm) -> Option<RefFormKind> {
use gimli::constants::*;
match form {
DW_FORM_ref1 | DW_FORM_ref2 | DW_FORM_ref4 | DW_FORM_ref8 | DW_FORM_ref_udata => {
Some(RefFormKind::CuRelative)
}
DW_FORM_ref_addr => Some(RefFormKind::SectionAbsolute),
_ => None,
}
}
fn ranges_has_live_entry(
range_lists: &gimli::RangeLists<Relocate<gimli::EndianSlice<'_, RunTimeEndian>>>,
offset: gimli::RangeListsOffset<usize>,
encoding: gimli::Encoding,
dwo_id: DwoId,
) -> Result<bool> {
let address_size = encoding.address_size;
let Ok(mut raw_iter) = range_lists.raw_ranges(offset, encoding) else {
debug!(?dwo_id, offset = %format_args!("{:#x}", offset.0), "Can't parse ranges at offset, conservatively assuming live.");
return Ok(true);
};
let mut base_tombstoned = false;
while let Some(entry) = raw_iter.next()? {
match entry {
gimli::RawRngListEntry::BaseAddress { addr } => {
base_tombstoned = is_tombstone(addr, address_size);
}
gimli::RawRngListEntry::AddressOrOffsetPair { begin, .. } => {
if base_tombstoned {
continue;
}
if !is_tombstone(begin, address_size) {
return Ok(true);
}
}
_ => {
debug!(
?dwo_id,
"Unexpected entry type in .debug_ranges, conservatively assuming live."
);
return Ok(true);
}
}
}
Ok(false)
}
fn rnglist_has_live_entry<IsAddrLive>(
range_lists: &gimli::RangeLists<Relocate<gimli::EndianSlice<'_, RunTimeEndian>>>,
base: gimli::DebugRngListsBase<usize>,
index: gimli::DebugRngListsIndex<usize>,
encoding: gimli::Encoding,
is_addr_live: &IsAddrLive,
dwo_id: DwoId,
) -> Result<bool>
where
IsAddrLive: Fn(DebugAddrIndex<usize>) -> Result<bool>,
{
let Ok(offset) = range_lists.get_offset(encoding, base, index) else {
debug!(?dwo_id, base = %format_args!("{:#x}", base.0), index = %format_args!("{:#x}", index.0), "Can't get rnglist offset, conservatively assuming live.");
return Ok(true);
};
let Ok(mut raw_iter) = range_lists.raw_ranges(offset, encoding) else {
debug!(?dwo_id, offset = %format_args!("{:#x}", offset.0), "Can't parse rnglist at offset, conservatively assuming live.");
return Ok(true);
};
let mut base_tombstoned = false;
while let Some(entry) = raw_iter.next()? {
match entry {
gimli::RawRngListEntry::BaseAddressx { addr } => {
base_tombstoned = !is_addr_live(addr)?;
}
gimli::RawRngListEntry::BaseAddress { addr } => {
base_tombstoned = is_tombstone(addr, encoding.address_size);
}
gimli::RawRngListEntry::OffsetPair { .. }
| gimli::RawRngListEntry::AddressOrOffsetPair { .. } => {
if !base_tombstoned {
return Ok(true);
}
}
gimli::RawRngListEntry::StartxEndx { begin, .. }
| gimli::RawRngListEntry::StartxLength { begin, .. } => {
if is_addr_live(begin)? {
return Ok(true);
}
}
gimli::RawRngListEntry::StartEnd { begin, .. }
| gimli::RawRngListEntry::StartLength { begin, .. } => {
if !is_tombstone(begin, encoding.address_size) {
return Ok(true);
}
}
}
}
Ok(false)
}
fn is_root<IsAddrLive>(
tag: gimli::DwTag,
attrs: &[(
gimli::DwAt,
gimli::DwForm,
gimli::AttributeValue<gimli::EndianSlice<'_, RunTimeEndian>>,
)],
range_lists: &gimli::RangeLists<Relocate<gimli::EndianSlice<'_, RunTimeEndian>>>,
ranges_base: gimli::DebugRngListsBase<usize>,
rnglists_base: gimli::DebugRngListsBase<usize>,
is_addr_live: &IsAddrLive,
dwo_id: DwoId,
encoding: gimli::Encoding,
) -> Result<bool>
where
IsAddrLive: Fn(DebugAddrIndex<usize>) -> Result<bool>,
{
match tag {
gimli::DW_TAG_subprogram => {
for (name, _, value) in attrs {
if *name == gimli::DW_AT_low_pc {
if let gimli::AttributeValue::DebugAddrIndex(idx) = value {
if is_addr_live(*idx)? {
return Ok(true);
}
} else if let gimli::AttributeValue::Addr(addr) = value {
if !is_tombstone(*addr, encoding.address_size) {
return Ok(true);
}
} else {
return Ok(true);
}
}
}
for (name, _, value) in attrs {
if *name == gimli::DW_AT_ranges {
if let gimli::AttributeValue::DebugRngListsIndex(idx) = value {
return rnglist_has_live_entry(
range_lists,
rnglists_base,
*idx,
encoding,
is_addr_live,
dwo_id,
);
} else if let gimli::AttributeValue::SecOffset(offset) = value {
return ranges_has_live_entry(
range_lists,
gimli::RangeListsOffset(ranges_base.0 + *offset),
encoding,
dwo_id,
);
} else {
return Ok(true);
}
}
}
Ok(false)
}
gimli::DW_TAG_variable => {
for (name, _, value) in attrs {
if *name == gimli::DW_AT_location {
if let gimli::AttributeValue::Exprloc(expr) = value {
if location_expr_live(*expr, is_addr_live, encoding)? {
return Ok(true);
}
}
}
}
Ok(false)
}
_ => Ok(false),
}
}
fn location_expr_live<IsAddrLive>(
expression: gimli::Expression<gimli::EndianSlice<'_, RunTimeEndian>>,
is_addr_live: &IsAddrLive,
encoding: gimli::Encoding,
) -> Result<bool>
where
IsAddrLive: Fn(DebugAddrIndex<usize>) -> Result<bool>,
{
let mut iter = expression.operations(encoding);
let mut saw_addrx = false;
let mut any_live = false;
while let Some(op) = iter.next()? {
if let gimli::Operation::AddressIndex { index } = op {
saw_addrx = true;
if is_addr_live(index)? {
any_live = true;
}
}
}
if saw_addrx {
Ok(any_live)
} else {
Ok(false)
}
}
fn scan_live_die_attrs(
attrs: &[(
gimli::DwAt,
gimli::DwForm,
gimli::AttributeValue<gimli::EndianSlice<'_, RunTimeEndian>>,
)],
loc_lists: &gimli::LocationLists<gimli::EndianSlice<'_, RunTimeEndian>>,
loclists_base: gimli::DebugLocListsBase<usize>,
encoding: gimli::Encoding,
) -> Result<Vec<gimli::UnitOffset>> {
let mut out = Vec::new();
for (name, form, value) in attrs {
if *name == gimli::DW_AT_sibling {
continue;
}
match ref_form_kind(*form) {
Some(RefFormKind::CuRelative) => {
if let gimli::AttributeValue::UnitRef(unit_off) = value {
out.push(*unit_off);
}
}
None => {
if *form == gimli::DW_FORM_indirect {
if let gimli::AttributeValue::UnitRef(unit_off) = value {
out.push(*unit_off);
}
}
}
Some(RefFormKind::SectionAbsolute) => {
return Err(Error::UnexpectedSectionAbsoluteReference);
}
}
if let gimli::AttributeValue::Exprloc(expr) = value {
let mut on_ref = |r: ExprRef| out.push(r.old);
let mut saw_section_absolute_reference = false;
let mut on_sec = |_: usize| {
saw_section_absolute_reference = true;
};
walk_expression(*expr, 0, encoding, &mut on_ref, &mut on_sec)?;
if saw_section_absolute_reference {
return Err(Error::UnexpectedSectionAbsoluteReference);
}
}
if *name == gimli::DW_AT_location {
let loclist_offset = match value {
gimli::AttributeValue::SecOffset(off) => Some(*off),
gimli::AttributeValue::LocationListsRef(off) => Some(off.0),
gimli::AttributeValue::DebugLocListsIndex(idx) => {
Some(loc_lists.get_offset(encoding, loclists_base, *idx)?.0)
}
_ => None,
};
if let Some(off) = loclist_offset {
scan_loclist_for_refs(loc_lists, off, encoding, &mut out)?;
}
}
}
Ok(out)
}
fn scan_loclist_for_refs(
loc_lists: &gimli::LocationLists<gimli::EndianSlice<'_, RunTimeEndian>>,
start: usize,
encoding: gimli::Encoding,
out: &mut Vec<gimli::UnitOffset>,
) -> Result<()> {
let mut raw_iter = loc_lists.raw_locations_dwo(gimli::LocationListsOffset(start), encoding)?;
while let Some(raw_entry) = raw_iter.next()? {
let expr: Option<gimli::Expression<gimli::EndianSlice<'_, RunTimeEndian>>> =
match &raw_entry {
gimli::RawLocListEntry::StartxEndx { data, .. }
| gimli::RawLocListEntry::StartxLength { data, .. }
| gimli::RawLocListEntry::OffsetPair { data, .. }
| gimli::RawLocListEntry::DefaultLocation { data }
| gimli::RawLocListEntry::StartEnd { data, .. }
| gimli::RawLocListEntry::StartLength { data, .. }
| gimli::RawLocListEntry::AddressOrOffsetPair { data, .. } => Some(*data),
_ => None,
};
if let Some(expr) = expr {
let mut on_ref = |r: ExprRef| out.push(r.old);
let mut saw_section_absolute_reference = false;
let mut on_sec = |_: usize| {
saw_section_absolute_reference = true;
};
walk_expression(expr, 0, encoding, &mut on_ref, &mut on_sec)?;
if saw_section_absolute_reference {
return Err(Error::UnexpectedSectionAbsoluteReference);
}
}
}
Ok(())
}
pub(crate) fn gc_debug_info<IsAddrLive>(
debug_info: gimli::DebugInfo<gimli::EndianSlice<'_, RunTimeEndian>>,
debug_abbrev: gimli::DebugAbbrev<gimli::EndianSlice<'_, RunTimeEndian>>,
loc_lists: gimli::LocationLists<gimli::EndianSlice<'_, RunTimeEndian>>,
range_lists: gimli::RangeLists<Relocate<gimli::EndianSlice<'_, RunTimeEndian>>>,
ranges_base: gimli::DebugRngListsBase<usize>,
is_addr_live: IsAddrLive,
dwo_id: DwoId,
has_type_units: bool,
) -> Result<GcResult>
where
IsAddrLive: Fn(DebugAddrIndex<usize>) -> Result<bool>,
{
debug!(?dwo_id, has_type_units, "gc_debug_info: starting GC pass");
let Some(header) = debug_info.units().next().map_err(Error::ParseUnitHeader)? else {
return Ok(GcResult {
rewritten: None,
offset_remap: None,
referenced_rnglists: None,
referenced_loclists: None,
referenced_str_offsets: None,
});
};
let encoding = header.encoding();
let abbreviations =
header.abbreviations(&debug_abbrev).map_err(Error::ParseUnitAbbreviations)?;
let mut entries = header.entries_raw(&abbreviations, None).map_err(Error::ParseUnit)?;
let mut dies: Vec<DieRecord> = Vec::new();
let mut offset_to_index: HashMap<gimli::UnitOffset, usize> = HashMap::new();
let mut worklist: Vec<usize> = Vec::new();
let mut die_rnglistx: Vec<Vec<u64>> = Vec::new();
let mut die_loclistx: Vec<Vec<u64>> = Vec::new();
let mut die_strx: Vec<Vec<u64>> = Vec::new();
let mut parent_stack: Vec<usize> = Vec::new();
let mut can_prune_rnglists = !has_type_units;
let mut can_prune_loclists = !has_type_units;
let can_prune_str_offsets = !has_type_units;
let mut rnglists_base = gimli::DebugRngListsBase::default_for_encoding_and_file(
encoding,
gimli::DwarfFileType::Dwo,
);
let mut loclists_base = gimli::DebugLocListsBase::default_for_encoding_and_file(
encoding,
gimli::DwarfFileType::Dwo,
);
while !entries.is_empty() {
let offset = entries.next_offset();
let Some(abbrev) = entries.read_abbreviation().map_err(Error::ParseUnit)? else {
parent_stack.pop();
continue;
};
let tag = abbrev.tag();
let has_children = abbrev.has_children();
let mut attrs: Vec<(gimli::DwAt, gimli::DwForm, gimli::AttributeValue<_>)> =
Vec::with_capacity(abbrev.attributes().len());
for spec in abbrev.attributes() {
let attr = entries.read_attribute(*spec).map_err(Error::ParseUnit)?;
attrs.push((spec.name(), spec.form(), attr.raw_value()));
}
if tag == gimli::DW_TAG_compile_unit {
for (name, _, value) in &attrs {
if *name == gimli::DW_AT_rnglists_base {
if let gimli::AttributeValue::SecOffset(offset) = value {
rnglists_base = gimli::DebugRngListsBase(*offset);
}
}
if *name == gimli::DW_AT_loclists_base {
if let gimli::AttributeValue::SecOffset(offset) = value {
loclists_base = gimli::DebugLocListsBase(*offset);
}
}
}
}
let index = dies.len();
offset_to_index.insert(offset, index);
let mut liveness = Liveness::Dead;
if is_root(
tag,
&attrs,
&range_lists,
ranges_base,
rnglists_base,
&is_addr_live,
dwo_id,
encoding,
)? {
liveness = Liveness::Live;
worklist.push(index);
}
let mut rnglistx_vals: Vec<u64> = Vec::new();
let mut loclistx_vals: Vec<u64> = Vec::new();
let mut strx_vals: Vec<u64> = Vec::new();
for (name, form, value) in &attrs {
if *form == gimli::DW_FORM_sec_offset {
if *name == gimli::DW_AT_ranges {
warn!(
?dwo_id,
"DW_AT_ranges uses DW_FORM_sec_offset; disabling rnglists pruning"
);
can_prune_rnglists = false;
} else if *name == gimli::DW_AT_location {
warn!(
?dwo_id,
"DW_AT_location uses DW_FORM_sec_offset; disabling loclists pruning"
);
can_prune_loclists = false;
}
}
if let gimli::AttributeValue::DebugRngListsIndex(idx) = value {
rnglistx_vals.push(idx.0 as u64);
}
if let gimli::AttributeValue::DebugLocListsIndex(idx) = value {
loclistx_vals.push(idx.0 as u64);
}
if let gimli::AttributeValue::DebugStrOffsetsIndex(idx) = value {
strx_vals.push(idx.0 as u64);
}
}
let edges = scan_live_die_attrs(&attrs, &loc_lists, loclists_base, encoding)?;
let parent = parent_stack.last().copied();
dies.push(DieRecord { parent, children: vec![], edges, liveness });
if let Some(parent) = parent {
dies[parent].children.push(index);
}
die_rnglistx.push(rnglistx_vals);
die_loclistx.push(loclistx_vals);
die_strx.push(strx_vals);
if has_children {
parent_stack.push(index);
}
}
if dies.is_empty() {
return Ok(GcResult {
rewritten: None,
offset_remap: None,
referenced_rnglists: None,
referenced_loclists: None,
referenced_str_offsets: None,
});
}
dies[0].liveness = Liveness::Retained;
loop {
while let Some(i) = worklist.pop() {
let children = mem::take(&mut dies[i].children);
for c in children.into_iter() {
if dies[c].liveness != Liveness::Live {
dies[c].liveness = Liveness::Live;
worklist.push(c);
}
}
let edges = mem::take(&mut dies[i].edges);
for target in edges.into_iter() {
if let Some(&ti) = offset_to_index.get(&target) {
if dies[ti].liveness != Liveness::Live {
dies[ti].liveness = Liveness::Live;
worklist.push(ti);
}
}
}
}
for i in 0..dies.len() {
if dies[i].liveness != Liveness::Live {
continue;
}
let mut p = dies[i].parent;
while let Some(pi) = p {
if dies[pi].liveness == Liveness::Dead {
dies[pi].liveness = Liveness::Retained;
p = dies[pi].parent;
} else {
break;
}
}
}
for i in 0..dies.len() {
if dies[i].liveness != Liveness::Retained {
continue;
}
let edges = mem::take(&mut dies[i].edges);
for target in edges.into_iter() {
if let Some(&ti) = offset_to_index.get(&target) {
if dies[ti].liveness != Liveness::Live {
dies[ti].liveness = Liveness::Live;
worklist.push(ti);
}
}
}
}
if worklist.is_empty() {
break;
}
}
let mut referenced_rnglists = BTreeSet::new();
let mut referenced_loclists = BTreeSet::new();
let mut referenced_str_offsets = BTreeSet::new();
let total_dies = dies.len();
let mut dead_dies = 0;
assert_eq!(total_dies, die_rnglistx.len());
assert_eq!(total_dies, die_loclistx.len());
assert_eq!(total_dies, die_strx.len());
for (die, rnglistx, loclistx, strx) in
izip!(&dies, die_rnglistx.into_iter(), die_loclistx.into_iter(), die_strx.into_iter())
{
if die.liveness == Liveness::Dead {
dead_dies += 1;
continue;
}
referenced_rnglists.extend(rnglistx);
referenced_loclists.extend(loclistx);
referenced_str_offsets.extend(strx);
}
let referenced_rnglists = if can_prune_rnglists { Some(referenced_rnglists) } else { None };
let referenced_loclists = if can_prune_loclists { Some(referenced_loclists) } else { None };
let referenced_str_offsets =
if can_prune_str_offsets { Some(referenced_str_offsets) } else { None };
if dead_dies == 0 {
return Ok(GcResult {
rewritten: None,
offset_remap: None,
referenced_rnglists,
referenced_loclists,
referenced_str_offsets,
});
}
debug!(?dwo_id, total_dies, dead_dies, "gc_debug_info: pruning dead DIEs");
let rnglist_remap: Option<HashMap<u64, u64>> = referenced_rnglists
.as_ref()
.map(|set| set.iter().enumerate().map(|(new, &old)| (old, new as u64)).collect());
let loclist_remap: Option<HashMap<u64, u64>> = referenced_loclists
.as_ref()
.map(|set| set.iter().enumerate().map(|(new, &old)| (old, new as u64)).collect());
let strx_remap: Option<HashMap<u64, u64>> = referenced_str_offsets
.as_ref()
.map(|set| set.iter().enumerate().map(|(new, &old)| (old, new as u64)).collect());
let (rewritten, new_offset) = rewrite_unit(
debug_info,
&header,
&abbreviations,
&dies,
&offset_to_index,
rnglist_remap.as_ref(),
loclist_remap.as_ref(),
strx_remap.as_ref(),
)?;
Ok(GcResult {
rewritten: Some(rewritten),
offset_remap: Some(new_offset),
referenced_rnglists,
referenced_loclists,
referenced_str_offsets,
})
}
fn rewrite_unit(
debug_info: gimli::DebugInfo<gimli::EndianSlice<'_, RunTimeEndian>>,
header: &gimli::UnitHeader<gimli::EndianSlice<'_, RunTimeEndian>>,
abbreviations: &gimli::Abbreviations,
dies: &[DieRecord],
offset_to_index: &HashMap<gimli::UnitOffset, usize>,
rnglist_remap: Option<&HashMap<u64, u64>>,
loclist_remap: Option<&HashMap<u64, u64>>,
strx_remap: Option<&HashMap<u64, u64>>,
) -> Result<(EndianVec<RunTimeEndian>, BTreeMap<gimli::UnitOffset, (gimli::UnitOffset, usize)>)> {
let endian = debug_info.reader().endian();
let encoding = header.encoding();
let header_size = header.size_of_header();
let mut new_offset = BTreeMap::new();
{
let mut scratch = EndianVec::new(endian);
emit_dies(
header,
abbreviations,
dies,
offset_to_index,
None,
rnglist_remap,
loclist_remap,
strx_remap,
&mut new_offset,
&mut scratch,
)?;
}
let mut body = EndianVec::new(endian);
{
let mut discard = BTreeMap::new();
emit_dies(
header,
abbreviations,
dies,
offset_to_index,
Some(&new_offset),
rnglist_remap,
loclist_remap,
strx_remap,
&mut discard,
&mut body,
)?;
}
let mut out = EndianVec::new(endian);
out.write(&debug_info.reader().slice()[..header_size])?;
out.write(body.slice())?;
let is_dwarf64 = encoding.format == gimli::Format::Dwarf64;
let new_unit_length = (out.slice().len() - if is_dwarf64 { 12 } else { 4 }) as u64;
if is_dwarf64 {
out.write_u64_at(4, new_unit_length)?;
} else {
out.write_u32_at(
0,
new_unit_length.try_into().expect("unit length w/out header larger than u32"),
)?;
}
Ok((out, new_offset))
}
fn emit_dies(
header: &gimli::UnitHeader<gimli::EndianSlice<'_, RunTimeEndian>>,
abbreviations: &gimli::Abbreviations,
dies: &[DieRecord],
offset_to_index: &HashMap<gimli::UnitOffset, usize>,
patch: Option<&BTreeMap<gimli::UnitOffset, (gimli::UnitOffset, usize)>>,
rnglist_remap: Option<&HashMap<u64, u64>>,
loclist_remap: Option<&HashMap<u64, u64>>,
strx_remap: Option<&HashMap<u64, u64>>,
new_offset: &mut BTreeMap<gimli::UnitOffset, (gimli::UnitOffset, usize)>,
out: &mut EndianVec<RunTimeEndian>,
) -> Result<()> {
let mut entries_raw = header.entries_raw(abbreviations, None).map_err(Error::ParseUnit)?;
let mut emit_stack: Vec<bool> = Vec::new();
while !entries_raw.is_empty() {
let die_start = entries_raw.next_offset();
let new_off = gimli::UnitOffset(header.root_offset().0 + out.slice().len());
let Some(abbrev) =
entries_raw.read_abbreviation().map_err(Error::ParseUnitAbbreviations)?
else {
if emit_stack.pop() == Some(true) {
new_offset.insert(die_start, (new_off, emit_stack.len() + 1));
out.write_u8(0)?;
}
continue;
};
let die_index = *offset_to_index.get(&die_start).ok_or(Error::MalformedDebugInfo)?;
let emit = dies[die_index].liveness != Liveness::Dead;
if emit {
new_offset.insert(die_start, (new_off, emit_stack.len()));
let after_code = entries_raw.next_offset();
out.write(&header.range(die_start..after_code)?)?;
for spec in abbrev.attributes() {
let attr_start = entries_raw.next_offset();
entries_raw.read_attribute(*spec)?;
let attr_end = entries_raw.next_offset();
if attr_end > attr_start {
emit_attribute(
header,
header.range(attr_start..attr_end)?,
spec,
emit_stack.len(),
patch,
rnglist_remap,
loclist_remap,
strx_remap,
out,
)?;
}
}
} else {
entries_raw.skip_attributes(abbrev.attributes())?;
}
if abbrev.has_children() {
emit_stack.push(emit);
}
}
Ok(())
}
fn emit_attribute(
header: &gimli::UnitHeader<gimli::EndianSlice<'_, RunTimeEndian>>,
mut data: gimli::EndianSlice<'_, RunTimeEndian>,
spec: &gimli::AttributeSpecification,
depth: usize,
patch: Option<&BTreeMap<gimli::UnitOffset, (gimli::UnitOffset, usize)>>,
rnglist_remap: Option<&HashMap<u64, u64>>,
loclist_remap: Option<&HashMap<u64, u64>>,
strx_remap: Option<&HashMap<u64, u64>>,
out: &mut EndianVec<RunTimeEndian>,
) -> Result<()> {
use gimli::constants::*;
let name = spec.name();
let form = spec.form();
if form == DW_FORM_indirect {
let orig = data;
let inner = data.read_uleb128()?;
out.write(&orig.range_to(..data.offset_from(orig)))?;
let inner_form = gimli::DwForm(inner as u16);
let inner_spec = gimli::AttributeSpecification::new(name, inner_form, None);
return emit_attribute(
header,
data,
&inner_spec,
depth,
patch,
rnglist_remap,
loclist_remap,
strx_remap,
out,
);
}
let Some(patch) = patch else {
out.write(&data)?;
return Ok(());
};
if matches!(form, DW_FORM_ref1 | DW_FORM_ref2 | DW_FORM_ref4 | DW_FORM_ref8 | DW_FORM_ref_udata)
{
let old = read_ref_value(data, form)?;
let new = match patch.get(&old) {
Some(n) => n.0,
None => {
if name == DW_AT_sibling {
patch
.range(old..)
.skip_while(|&(_, v)| v.1 > depth)
.next()
.ok_or(Error::MalformedDebugInfo)?
.1
.0
} else {
return Err(Error::MalformedDebugInfo);
}
}
};
write_ref_value(out, form, old, new)?;
return Ok(());
}
if form == DW_FORM_exprloc {
let orig = data;
let expr_len = data.read_uleb128()? as usize;
out.write(&orig.range_to(..data.offset_from(orig)))?;
emit_expression(data.range_to(..expr_len), patch, header.encoding(), out)?;
return Ok(());
}
let remap = match form {
DW_FORM_rnglistx => rnglist_remap,
DW_FORM_loclistx => loclist_remap,
DW_FORM_strx | DW_FORM_GNU_str_index => strx_remap,
_ => None,
};
if let Some(remap) = remap {
let old_width = data.len();
let old_idx = data.read_uleb128()?;
let &new_idx = remap.get(&old_idx).ok_or(Error::MalformedDebugInfo)?;
write_uleb128_padded(out, gimli::UnitOffset(new_idx as usize), old_width)?;
return Ok(());
}
let fixed_width: u8 = match form {
DW_FORM_strx1 => 1,
DW_FORM_strx2 => 2,
DW_FORM_strx3 => 3,
DW_FORM_strx4 => 4,
_ => 0,
};
if fixed_width > 0 {
if let Some(strx_remap) = strx_remap {
let old_idx = data.read_uint(fixed_width as usize)?;
let &new_idx = strx_remap.get(&old_idx).ok_or(Error::MalformedDebugInfo)?;
out.write_udata(new_idx, fixed_width)?;
return Ok(());
}
}
out.write(&data)?;
Ok(())
}
fn read_ref_value(
mut data: gimli::EndianSlice<'_, RunTimeEndian>,
form: gimli::DwForm,
) -> Result<gimli::UnitOffset> {
use gimli::constants::*;
Ok(gimli::UnitOffset(match form {
DW_FORM_ref1 => data.read_u8()? as usize,
DW_FORM_ref2 => data.read_u16()? as usize,
DW_FORM_ref4 => data.read_u32()? as usize,
DW_FORM_ref8 => data.read_u64()? as usize,
DW_FORM_ref_udata => data.read_uleb128()? as usize,
_ => return Err(Error::UnsupportedForm(form.0)),
}))
}
fn write_ref_value(
out: &mut EndianVec<RunTimeEndian>,
form: gimli::DwForm,
old: gimli::UnitOffset,
new: gimli::UnitOffset,
) -> Result<()> {
use gimli::constants::*;
match form {
DW_FORM_ref1 => out.write_u8(new.0 as u8)?,
DW_FORM_ref2 => out.write_u16(new.0 as u16)?,
DW_FORM_ref4 => out.write_u32(new.0 as u32)?,
DW_FORM_ref8 => out.write_u64(new.0 as u64)?,
DW_FORM_ref_udata => {
let mut v = old.0 as u64;
let mut width = 0;
loop {
width += 1;
v >>= 7;
if v == 0 {
break;
}
}
write_uleb128_padded(out, new, width)?;
}
_ => return Err(Error::UnsupportedForm(form.0)),
}
Ok(())
}
fn emit_expression(
expr: gimli::EndianSlice<'_, RunTimeEndian>,
patch: &BTreeMap<gimli::UnitOffset, (gimli::UnitOffset, usize)>,
encoding: gimli::Encoding,
out: &mut EndianVec<RunTimeEndian>,
) -> Result<()> {
let mut refs: Vec<ExprRef> = Vec::new();
{
let mut on_ref = |r: ExprRef| refs.push(r);
let mut saw_section_absolute_reference = false;
let mut on_sec = |_: usize| {
saw_section_absolute_reference = true;
};
walk_expression(gimli::Expression(expr), 0, encoding, &mut on_ref, &mut on_sec)?;
if saw_section_absolute_reference {
return Err(Error::UnexpectedSectionAbsoluteReference);
}
}
if refs.is_empty() {
out.write(&expr)?;
return Ok(());
}
refs.sort_by_key(|r| r.pos);
let mut copied = 0usize;
for r in &refs {
out.write(&expr[copied..r.pos])?;
let new = patch.get(&r.old).ok_or(Error::MalformedDebugInfo)?.0;
match r.enc {
ExprRefEnc::U2 => {
out.write_u16(new.0 as u16)?;
copied = r.pos + 2;
}
ExprRefEnc::U4 => {
out.write_u32(new.0 as u32)?;
copied = r.pos + 4;
}
ExprRefEnc::Uleb => {
let old_width = uleb128_len(expr.range_from(r.pos..))?;
write_uleb128_padded(out, new, old_width)?;
copied = r.pos + old_width;
}
}
}
out.write(&expr[copied..])?;
Ok(())
}