| // 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::collections::{HashMap, HashSet}; |
| use std::fs::{self, File}; |
| use std::io::BufWriter; |
| use std::path::{Path, PathBuf}; |
| use std::sync::LazyLock; |
| |
| use anyhow::{Context, Result, anyhow, bail}; |
| use clap::Parser; |
| use object::build::elf::{ |
| AttributeTag, AttributesSection, AttributesSubsection, AttributesSubsubsection, Builder, |
| Section, SectionData, SectionId, |
| }; |
| use object::build::{ByteString, Bytes, Id}; |
| use object::{ReadRef, elf}; |
| |
| // Don't copy these sections, as the object writer won't allow multiple |
| // sections of SYMTAB and STRTAB type. All other sections should be copied |
| // even if they're not loaded into memory, as symbols may reference |
| // these non-alloc sections. |
| static SKIPPED_APP_SECTIONS: LazyLock<HashSet<&[u8]>> = LazyLock::new(|| { |
| [&b".symtab"[..], &b".shstrtab"[..], &b".strtab"[..]] |
| .into_iter() |
| .collect() |
| }); |
| |
| #[derive(Debug, Parser)] |
| struct Args { |
| #[arg(long, required(true))] |
| kernel: PathBuf, |
| #[arg(long("app"))] |
| apps: Vec<PathBuf>, |
| #[arg(long, required(true))] |
| output: PathBuf, |
| } |
| |
| struct SystemImage<'data> { |
| builder: Builder<'data>, |
| tokenized_section: Option<SectionId>, |
| } |
| |
| impl<'data> SystemImage<'data> { |
| pub fn new<R: ReadRef<'data>>(kernel_bytes: R) -> Result<Self> { |
| let builder = Builder::read(kernel_bytes) |
| .map_err(|e| anyhow!("Failed to parse kernel image: {e}"))?; |
| |
| let mut instance = Self { |
| builder, |
| tokenized_section: None, |
| }; |
| |
| instance.set_tokenized_section(); |
| Ok(instance) |
| } |
| |
| fn write(self, writer: &mut BufWriter<File>) -> Result<()> { |
| let mut buffer = object::write::StreamingBuffer::new(writer); |
| |
| self.builder |
| .write(&mut buffer) |
| .map_err(|e| anyhow!("Failed to write system image: {e}")) |
| } |
| |
| fn add_app_image<'a, R: ReadRef<'a>>(&mut self, app_bytes: R, app_name: &String) -> Result<()> { |
| let app_builder = |
| Builder::read(app_bytes).map_err(|e| anyhow!("Failed to parse app image: {e}"))?; |
| |
| let mut section_map = HashMap::new(); |
| self.add_app_sections(&app_builder, app_name, &mut section_map) |
| .map_err(|e| anyhow!("Failed adding app sections: {e}"))?; |
| self.add_app_segments(&app_builder, §ion_map) |
| .map_err(|e| anyhow!("Failed adding app segments: {e}"))?; |
| self.add_app_symbols(&app_builder, app_name, §ion_map) |
| .map_err(|e| anyhow!("Failed adding app symbols: {e}")) |
| } |
| |
| fn add_app_sections( |
| &mut self, |
| app: &Builder, |
| app_name: &String, |
| section_map: &mut HashMap<usize, SectionId>, |
| ) -> Result<()> { |
| let mut sections_for_fixup = Vec::new(); |
| for section in &app.sections { |
| let is_tokenizer = Self::is_tokenizer_section(section); |
| let mut add_tokenizer_section = false; |
| if is_tokenizer { |
| add_tokenizer_section = self.update_tokenized_section(section, section_map); |
| // Don't add this section, if a tokenized section already exists |
| if !add_tokenizer_section { |
| continue; |
| } |
| } |
| |
| // TODO: davidroth - This isn't a problem right now as we don't support |
| // debugging of merged self files. Revisit this once we add debugging |
| // support. Possibly we could move the symbols & strings into merged |
| // sections as we do with the tokenized section. |
| if SKIPPED_APP_SECTIONS.contains(§ion.name.as_slice()) { |
| // println!("Skipping section '{}'", section.name); |
| continue; |
| } |
| |
| let new_section = self.builder.sections.add(); |
| section_map.insert(section.id().index(), new_section.id()); |
| |
| if add_tokenizer_section { |
| self.tokenized_section = Some(new_section.id()); |
| // Don't rename tokenizer section if we're adding one. |
| new_section.name = ByteString::from(section.name.to_vec()); |
| } else { |
| let name = format!("{}.{}", section.name, app_name); |
| new_section.name = name.into_bytes().into(); |
| } |
| |
| if section.sh_link_section.is_some() || section.sh_info_section.is_some() { |
| sections_for_fixup.push(new_section.id()); |
| } |
| |
| Self::copy_section(section_map, section, new_section)?; |
| |
| // println!("Added app section '{:?}'", new_section); |
| } |
| |
| // Now that all the sections have been added, go back and update any |
| // references to SectionIds. |
| for section_id in sections_for_fixup { |
| let section = self.builder.sections.get_mut(section_id); |
| if let Some(id) = section.sh_link_section { |
| section.sh_link_section = Self::get_mapped_section_id(section_map, id)?; |
| } |
| if let Some(id) = section.sh_info_section { |
| section.sh_info_section = Self::get_mapped_section_id(section_map, id)?; |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn copy_section( |
| section_map: &mut HashMap<usize, SectionId>, |
| src: &Section, |
| dst: &mut Section<'data>, |
| ) -> Result<()> { |
| dst.sh_type = src.sh_type; |
| dst.sh_flags = src.sh_flags; |
| // Copy sh_addr and sh_offset. They will be updated if they're |
| // added to a segment. |
| dst.sh_addr = src.sh_addr; |
| dst.sh_offset = src.sh_offset; |
| dst.sh_size = src.sh_size; |
| // OK to copy the original SectiondId's. They will be re-mapped |
| // after all the sections have been added. |
| dst.sh_link_section = src.sh_link_section; |
| dst.sh_info_section = src.sh_info_section; |
| dst.sh_addralign = src.sh_addralign; |
| dst.sh_entsize = src.sh_entsize; |
| dst.data = match &src.data { |
| SectionData::Data(data) => SectionData::Data(Bytes::from(data.to_vec())), |
| SectionData::UninitializedData(data) => SectionData::UninitializedData(*data), |
| SectionData::Attributes(data) => { |
| Self::copy_section_attributes(section_map, data).unwrap() |
| } |
| SectionData::SectionString => SectionData::SectionString, |
| SectionData::Symbol => SectionData::Symbol, |
| SectionData::SymbolSectionIndex => SectionData::SymbolSectionIndex, |
| SectionData::String => SectionData::String, |
| SectionData::DynamicSymbol => SectionData::DynamicSymbol, |
| SectionData::DynamicString => SectionData::DynamicString, |
| SectionData::Hash => SectionData::Hash, |
| SectionData::GnuHash => SectionData::GnuHash, |
| SectionData::GnuVersym => SectionData::GnuVersym, |
| SectionData::GnuVerdef => SectionData::GnuVerdef, |
| SectionData::GnuVerneed => SectionData::GnuVerneed, |
| _ => unreachable!("Unsupported section data type: {:?}", src.data), |
| }; |
| |
| Ok(()) |
| } |
| |
| // Copy the section attributes which may GNU or other vendor-specific attributes. Need to |
| // deep copy as the object crate does not provide a Copy and if an attribute tag points |
| // to a section id, it need to be remapped to the new section id in the merged elf. |
| fn copy_section_attributes( |
| section_map: &mut HashMap<usize, SectionId>, |
| data: &AttributesSection, |
| ) -> Result<SectionData<'data>, ()> { |
| let mut attributes_section = AttributesSection::new(); |
| for subsection in &data.subsections { |
| let mut attributes_subsection = |
| AttributesSubsection::new(ByteString::from(subsection.vendor.to_vec())); |
| for subsubsection in &subsection.subsubsections { |
| let tag = match &subsubsection.tag { |
| AttributeTag::File => AttributeTag::File, |
| AttributeTag::Section(section_tag) => { |
| let mut tag_sections = Vec::new(); |
| // Remap the section ids to the new ids in the merged elf. |
| for section_id in section_tag { |
| let mapped_id = Self::get_mapped_section_id(section_map, *section_id); |
| tag_sections.push(mapped_id.unwrap().expect("Section attribute copy")); |
| } |
| AttributeTag::Section(tag_sections) |
| } |
| AttributeTag::Symbol(symbol_tag) => AttributeTag::Symbol(symbol_tag.to_vec()), |
| }; |
| |
| let attributes_subsubsection = AttributesSubsubsection { |
| tag, |
| data: Bytes::from(subsubsection.data.to_vec()), |
| }; |
| attributes_subsection |
| .subsubsections |
| .push(attributes_subsubsection); |
| } |
| attributes_section.subsections.push(attributes_subsection); |
| } |
| Ok(SectionData::Attributes(attributes_section)) |
| } |
| |
| fn add_app_segments( |
| &mut self, |
| app: &Builder, |
| section_map: &HashMap<usize, SectionId>, |
| ) -> Result<()> { |
| for segment in &app.segments { |
| if !segment.is_load() { |
| // println!("Skipping non-load segment {:?}", segment.id()); |
| continue; |
| } |
| let new_segment = self |
| .builder |
| .segments |
| .add_load_segment(segment.p_flags, segment.p_align); |
| // Make sure to preserve the addresses where the original |
| // segment is loaded, as add_load_segment() will change |
| // them. |
| // TODO: davidroth - investigate submitting an upstream |
| // path to make this more robust and not require fixup. |
| new_segment.p_paddr = segment.p_paddr; |
| new_segment.p_vaddr = segment.p_vaddr; |
| for section_id in &segment.sections { |
| // append_section() recalculates sh_addr from the segment's p_vaddr, which |
| // can differ from the section's actual load address when PMSAv7 alignment |
| // padding creates a gap between segment start and section start. Save and |
| // restore sh_addr to preserve the linker's intended memory layout. |
| let original_section = app.sections.get(*section_id); |
| let original_addr = original_section.sh_addr; |
| |
| let mapped_section_id = Self::get_mapped_section_id(section_map, *section_id)?; |
| let section = self.builder.sections.get_mut(mapped_section_id.unwrap()); |
| new_segment.append_section(section); |
| |
| // Restore sh_addr to the value saved before append_section(). |
| section.sh_addr = original_addr; |
| } |
| // println!("Added segment {:?}", new_segment.id()); |
| } |
| |
| Ok(()) |
| } |
| |
| fn add_app_symbols( |
| &mut self, |
| app: &Builder, |
| app_name: &String, |
| section_map: &HashMap<usize, SectionId>, |
| ) -> Result<()> { |
| for symbol in &app.symbols { |
| // println!("Adding app symbol: {:?}", symbol); |
| let new_symbol = self.builder.symbols.add(); |
| if symbol.st_bind() == elf::STB_GLOBAL { |
| let new_name = format!("{}_{}", symbol.name, app_name); |
| new_symbol.name = new_name.into_bytes().into(); |
| } |
| |
| // Recalculate symbol addresses to stay consistent with the section addresses |
| // preserved in add_app_segments(). Section-associated symbols are adjusted via |
| // their section-relative offset. Absolute/linker-defined symbols (e.g. __sdata) |
| // are adjusted if their value falls within a relocated section's address range. |
| |
| if let Some(old_section_id) = symbol.section { |
| let new_section_id = Self::get_mapped_section_id(section_map, old_section_id)?; |
| new_symbol.section = new_section_id; |
| |
| // If symbol is in a section, adjust its address for the section's new location. |
| // The symbol's value is an absolute address, not a section-relative offset. |
| // We need to convert: old_addr -> offset_in_section -> new_addr |
| if let Some(new_id) = new_section_id { |
| let old_section = app.sections.get(old_section_id); |
| let new_section = self.builder.sections.get(new_id); |
| |
| // Calculate offset within the section. |
| // Example: symbol at 0x40420, section at 0x40420 -> offset = 0 |
| let offset_in_section = symbol.st_value.wrapping_sub(old_section.sh_addr); |
| |
| // Set symbol address to new section base + offset. |
| // With our section address preservation fix, old and new section addresses |
| // should match, so this effectively preserves the original symbol address. |
| new_symbol.st_value = new_section.sh_addr.wrapping_add(offset_in_section); |
| } else { |
| // No section mapping (section was filtered out), preserve original value. |
| new_symbol.st_value = symbol.st_value; |
| } |
| } else { |
| // Symbol not explicitly associated with a section (e.g., absolute symbols |
| // or linker-defined symbols like __sdata, __edata, pw_boot_stack_*). |
| // |
| // If the symbol's value falls within a relocated section's address range, |
| // adjust it using the same section-relative offset logic. |
| // Otherwise, leave it unchanged. |
| let mut new_value = symbol.st_value; |
| |
| for old_section in &app.sections { |
| // Only consider allocatable sections that are actually mapped into |
| // the loadable image. Non-allocatable sections (debug info, etc.) |
| // don't contribute to the memory layout. |
| if !old_section.is_alloc() { |
| continue; |
| } |
| |
| let size = old_section.sh_size; |
| if size == 0 { |
| continue; |
| } |
| |
| let start = old_section.sh_addr; |
| let end = start.wrapping_add(size); |
| let addr = symbol.st_value; |
| |
| // Check if symbol's value falls within this section's range [start, end) |
| if addr < start || addr >= end { |
| continue; |
| } |
| |
| // This absolute symbol's value falls within this section. |
| // Treat it as if it were section-relative and apply the same |
| // relocation that we apply to section-based symbols. |
| let old_section_id = old_section.id(); |
| let new_section_id = Self::get_mapped_section_id(section_map, old_section_id)?; |
| |
| if let Some(new_id) = new_section_id { |
| let new_section = self.builder.sections.get(new_id); |
| let offset_in_section = addr.wrapping_sub(start); |
| new_value = new_section.sh_addr.wrapping_add(offset_in_section); |
| } |
| |
| // An absolute symbol should belong to at most one allocatable section |
| // range, so we can stop once we've found and adjusted it. |
| break; |
| } |
| |
| new_symbol.st_value = new_value; |
| } |
| |
| new_symbol.st_info = symbol.st_info; |
| new_symbol.st_other = symbol.st_other; |
| new_symbol.st_shndx = symbol.st_shndx; |
| new_symbol.st_size = symbol.st_size; |
| new_symbol.version = symbol.version; |
| new_symbol.version_hidden = symbol.version_hidden; |
| } |
| Ok(()) |
| } |
| |
| fn get_mapped_section_id( |
| section_map: &HashMap<usize, SectionId>, |
| id: SectionId, |
| ) -> Result<Option<SectionId>> { |
| Self::get_mapped_section_id_from_index(section_map, id.index()) |
| } |
| |
| fn get_mapped_section_id_from_index( |
| section_map: &HashMap<usize, SectionId>, |
| id: usize, |
| ) -> Result<Option<SectionId>> { |
| match section_map.get(&id) { |
| Some(mapped_id) => { |
| // println!("Mapped SectionId {:?} to {:?}", id, mapped_id); |
| Ok(Some(*mapped_id)) |
| } |
| None => bail!("No mapping for {:?}", id), |
| } |
| } |
| |
| // Within the combined elf, there can only be one tokenized section |
| // that must be called `.pw_tokenizer.entries` as this is what the |
| // de-tokenization tooling expects. |
| // The tokenization database is designed to be appended, so for |
| // each token section we encounter in an elf, we just append the |
| // bytes to the end of the first token section we encounter. |
| fn set_tokenized_section(&mut self) { |
| for section in &mut self.builder.sections { |
| let is_tokenizer = Self::is_tokenizer_section(section); |
| if is_tokenizer { |
| // println!("Tokenized section: {:?}", section); |
| self.tokenized_section = Some(section.id()); |
| break; |
| } |
| } |
| } |
| |
| fn update_tokenized_section( |
| &mut self, |
| section: &Section, |
| section_map: &mut HashMap<usize, SectionId>, |
| ) -> bool { |
| let mut add_tokenizer_section = false; |
| match self.tokenized_section { |
| Some(tokenized_section_id) => { |
| // There is already a tokenizer section, so append |
| // this tokenizer database to the existing one. |
| let tokenizer_section = self.builder.sections.get_mut(tokenized_section_id); |
| tokenizer_section.sh_size += section.sh_size; |
| tokenizer_section.data = match &tokenizer_section.data { |
| SectionData::Data(data) => { |
| let mut combined_data = data.to_vec(); |
| match §ion.data { |
| SectionData::Data(new_data) => combined_data.extend(&new_data.to_vec()), |
| _ => unreachable!("Incorrect data type"), |
| }; |
| SectionData::Data(Bytes::from(combined_data)) |
| } |
| _ => unreachable!("Incorrect data type"), |
| }; |
| section_map.insert(section.id().index(), tokenized_section_id); |
| } |
| None => { |
| // No existing tokenized section in the system image, so use |
| // this one. |
| add_tokenizer_section = true; |
| } |
| } |
| |
| add_tokenizer_section |
| } |
| |
| fn is_tokenizer_section(section: &Section) -> bool { |
| if section.is_alloc() { |
| return false; |
| } |
| |
| section.name.starts_with(b".pw_tokenizer.") |
| } |
| } |
| |
| fn get_app_name(path: &Path, index: usize) -> Result<String> { |
| let filename = path |
| .file_stem() |
| .context("Invalid path: No filename found")? |
| .to_str() |
| .context("Invalid path: Filename is not valid UTF-8") |
| .map(|s| s.to_owned())?; |
| |
| // Ensure the app name is a valid elf symbol. |
| // Replace any invalid characters with `_`. |
| // There is no concern over name collisions, as |
| // we also add a unique index suffix |
| let mut valid_name = String::new(); |
| let chars = filename.chars(); |
| for char in chars { |
| if char.is_alphanumeric() || char == '_' { |
| valid_name.push(char); |
| } else { |
| valid_name.push('_'); |
| } |
| } |
| valid_name.push_str(format!("_{index}").as_str()); |
| |
| Ok(valid_name) |
| } |
| |
| fn assemble(args: Args) -> Result<()> { |
| // println!("Adding kernel image: {}", args.kernel.display()); |
| let kernel_bytes = |
| fs::read(&args.kernel).map_err(|e| anyhow!("Failed to read kernel image: {e}"))?; |
| |
| let mut system_image: SystemImage<'_> = SystemImage::new(&*kernel_bytes)?; |
| |
| for (index, app) in args.apps.iter().enumerate() { |
| // println!("Adding app image: {}", app.display()); |
| let app_bytes = fs::read(app).map_err(|e| anyhow!("Failed to read app image: {e}"))?; |
| |
| let app_name = get_app_name(app, index)?; |
| system_image.add_app_image(&*app_bytes, &app_name)?; |
| } |
| |
| // println!("Writing system image: {}", args.output.display()); |
| let mut open_options = fs::OpenOptions::new(); |
| open_options.write(true).create(true).truncate(true); |
| let system_file = open_options |
| .open(args.output) |
| .map_err(|e| anyhow!("Failed to create system image: {e}"))?; |
| let mut writer = BufWriter::new(system_file); |
| system_image.write(&mut writer) |
| } |
| |
| fn main() -> Result<()> { |
| let args = Args::parse(); |
| assemble(args).map_err(|e| anyhow!("{e}")) |
| } |