From 2ab1fb2048db4d75a5eda41baf4031f137843083 Mon Sep 17 00:00:00 2001 From: Felipe Jovino Date: Thu, 23 Jul 2026 13:46:25 -0300 Subject: [PATCH] Preserve empty REL section slots dropped by mwld Some games reserve an empty section slot before .text. mwld drops it during the partial link because nothing lands in it, so the object has .text at index 1 while the original REL has it at 2, and every later section is written one index low. prologSection, epilogSection and unresolvedSection shift with them, which leaves rel info unable to read the REL that rel make just wrote. Fill in match_section_index, which was a stub returning the ELF index with the working version commented out below it, and carry the same mapping through write_rel: sections are now placed at the index they occupy in the original REL, and the slots mwld dropped are written back as empty. The map is only built when it differs from the ELF's own indices, so RELs without a dropped slot take the same path as before. Fixes the 17 RELs in Sonic Heroes (G9SE8P), which now match the originals byte for byte. --- src/cmd/rel.rs | 70 +++++++++++++++++++++++++++++-------------------- src/util/rel.rs | 54 ++++++++++++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 34 deletions(-) diff --git a/src/cmd/rel.rs b/src/cmd/rel.rs index ee642637..a8edf2f4 100644 --- a/src/cmd/rel.rs +++ b/src/cmd/rel.rs @@ -42,7 +42,8 @@ use crate::{ path::native_path, rel::{ PERMITTED_SECTIONS, RelHeader, RelReloc, RelSectionHeader, RelWriteInfo, - print_relocations, process_rel, process_rel_header, process_rel_sections, write_rel, + is_permitted_section, print_relocations, process_rel, process_rel_header, + process_rel_sections, write_rel, }, }, vfs::open_file, @@ -140,34 +141,27 @@ fn match_section_index( section_index: SectionIndex, rel_sections: &[RelSectionHeader], ) -> Result { - let (_, _) = (obj, rel_sections); - Ok(section_index.0) - // TODO - // rel_sections - // .iter() - // .enumerate() - // .filter(|(_, s)| s.size() > 0) - // .zip(obj.sections().filter(|s| s.size() > 0)) - // .find_map( - // |((rel_section_index, _), obj_section)| { - // if obj_section.index() == section_index { - // Some(rel_section_index) - // } else { - // None - // } - // }, - // ) - // .ok_or_else(|| { - // anyhow!( - // "Failed to find matching section index for {} ({}), REL section count: {}", - // obj.section_by_index(section_index) - // .ok() - // .and_then(|s| s.name().ok().map(|s| s.to_string())) - // .unwrap_or("[invalid]".to_string()), - // section_index.0, - // rel_sections.len() - // ) - // }) + rel_sections + .iter() + .enumerate() + .filter(|(_, s)| s.size() > 0) + .zip(obj.sections().filter(|s| is_permitted_section(s) && s.size() > 0)) + .find_map( + |((rel_section_index, _), obj_section)| { + if obj_section.index() == section_index { Some(rel_section_index) } else { None } + }, + ) + .ok_or_else(|| { + anyhow!( + "Failed to find matching section index for {} ({}), REL section count: {}", + obj.section_by_index(section_index) + .ok() + .and_then(|s| s.name().ok().map(|s| s.to_string())) + .unwrap_or("[invalid]".to_string()), + section_index.0, + rel_sections.len() + ) + }) } fn load_rel(module_config: &ModuleConfig, object_base: &ObjectBase) -> Result { @@ -384,6 +378,7 @@ fn make(args: MakeArgs) -> Result<()> { quiet: args.no_warn, section_align: None, section_exec: None, + section_index_map: None, }; if let Some((header, section_headers, section_defs)) = existing_headers.get(&module_info.module_id) @@ -399,6 +394,23 @@ fn make(args: MakeArgs) -> Result<()> { .map(|defs| defs.iter().map(|def| def.align).collect()) .unwrap_or_default(); info.section_exec = Some(section_headers.iter().map(|s| s.exec()).collect()); + // Where each ELF section lands in the original's section table. They + // differ whenever the original reserved a slot that mwld dropped for + // being empty, which shifts everything after it by one. + let mut map = vec![0usize; module_info.file.sections().count()]; + for ((rel_idx, _), obj_section) in + section_headers.iter().enumerate().filter(|(_, s)| s.size() > 0).zip( + module_info.file.sections().filter(|s| is_permitted_section(s) && s.size() > 0), + ) + { + map[obj_section.index().0] = rel_idx; + } + // Only carry the map when it actually differs from the ELF's own + // indices. Projects whose RELs have no dropped slot then take the + // exact path they took before. + if map.iter().enumerate().any(|(elf_idx, &rel_idx)| elf_idx != rel_idx) { + info.section_index_map = Some(map); + } } let rel_path = module_info.path.with_extension("rel"); let mut w = buf_writer(&rel_path)?; diff --git a/src/util/rel.rs b/src/util/rel.rs index bae64c89..75033e10 100644 --- a/src/util/rel.rs +++ b/src/util/rel.rs @@ -691,6 +691,11 @@ pub struct RelWriteInfo { /// This is used to match empty sections: mwld will emit them with /// NULL type, but the original REL may have them marked executable. pub section_exec: Option>, + /// Map from ELF section index to REL section index. + /// Some games reserve an empty section slot that mwld drops during the + /// partial link, because nothing lands in it. Without this the rebuilt + /// REL packs its sections one index lower than the original. + pub section_index_map: Option>, } pub const PERMITTED_SECTIONS: [&str; 7] = @@ -968,21 +973,24 @@ where header.fix_size = Some(offset); } + let map_section = |idx: usize| -> u8 { + info.section_index_map.as_ref().and_then(|m| m.get(idx).copied()).unwrap_or(idx) as u8 + }; for symbol in file.symbols().filter(|s| s.is_definition()) { let Some(symbol_section) = symbol.section_index() else { continue; }; match symbol.name() { Ok("_prolog") => { - header.prolog_section = symbol_section.0 as u8; + header.prolog_section = map_section(symbol_section.0); header.prolog_offset = symbol.address() as u32; } Ok("_epilog") => { - header.epilog_section = symbol_section.0 as u8; + header.epilog_section = map_section(symbol_section.0); header.epilog_offset = symbol.address() as u32; } Ok("_unresolved") => { - header.unresolved_section = symbol_section.0 as u8; + header.unresolved_section = map_section(symbol_section.0); header.unresolved_offset = symbol.address() as u32; } _ => {} @@ -993,9 +1001,45 @@ where ensure!(w.stream_position()? as u32 == header.section_info_offset); let mut current_data_offset = section_data_offset; let mut permitted_section_idx = 0; + // Reverse of section_index_map: for each REL slot, the ELF section that + // belongs in it. Slots with no ELF section are the placeholders mwld drops. + let rel_to_elf: Option>> = info.section_index_map.as_ref().map(|map| { + let mut rev = vec![None; num_sections as usize]; + for (elf_idx, &rel_idx) in map.iter().enumerate() { + if rel_idx < rev.len() { + rev[rel_idx] = Some(elf_idx); + } + } + rev + }); for section_index in 0..num_sections { - let Ok(section) = file.section_by_index(object::SectionIndex(section_index as usize)) - else { + let elf_index = match &rel_to_elf { + Some(rev) => match rev[section_index as usize] { + Some(i) => i, + None => { + // No ELF section belongs in this slot. Two cases: a slot the + // original reserved between real sections, which mwld drops + // for being empty and which points at where its data would + // start; or a trailing slot past the last real section, + // which is left at zero. + let last_real = rev.iter().rposition(|e| e.is_some()).unwrap_or(0); + let (offset, exec) = if (section_index as usize) < last_real { + let exec = info + .section_exec + .as_ref() + .and_then(|m| m.get(section_index as usize).copied()) + .unwrap_or(false); + (current_data_offset, exec) + } else { + (0, false) + }; + RelSectionHeader::new(offset, 0, exec).to_writer(w, Endian::Big)?; + continue; + } + }, + None => section_index as usize, + }; + let Ok(section) = file.section_by_index(object::SectionIndex(elf_index)) else { RelSectionHeader::new(0, 0, false).to_writer(w, Endian::Big)?; continue; };