diff --git a/cle/backends/coff.py b/cle/backends/coff.py index 556ca93b8..fc17fe820 100644 --- a/cle/backends/coff.py +++ b/cle/backends/coff.py @@ -430,6 +430,23 @@ def value(self): }, } +#: ``IMAGE_SCN_ALIGN_*`` is a four-bit field holding one plus the log2 of the alignment. Zero states none, +#: for which the linker's own default is 16 bytes. +_ALIGN_MASK = 0x00F00000 +_ALIGN_SHIFT = 20 + + +def _section_alignment(section: CoffSectionTableEntry, unstated: int = 16) -> int: + encoded = (section.Characteristics & _ALIGN_MASK) >> _ALIGN_SHIFT + return 1 << (encoded - 1) if encoded else unstated + + +#: ``SizeOfRawData`` is 32 bits wide, and for a section with no bytes in the file nothing else in the file +#: bounds it. This caps the zero-filled space such a section is given, and how far past the image a section +#: may be moved. +MAX_IMAGE_SIZE = 0x10000000 + + COFF_MACHINE_TO_ARCH_NAME = { IMAGE_FILE_MACHINE.I386: "x86", IMAGE_FILE_MACHINE.AMD64: "AMD64", @@ -459,26 +476,76 @@ def __init__(self, *args, **kwargs): # FIXME: Currently we just map the whole object file for convenience. Create a better memory map, discard object # file structure data. - self._image_vmem = self._data + image = bytearray(self._data) # Add each section + self._section_addrs: list[int] = [] + next_vaddr_past_image = len(image) for section_idx, section in enumerate(self._coff.sections): section_name = self._coff.get_section_name(section_idx) - vaddr = section.PointerToRawData + raw_ptr = section.PointerToRawData vsize = section.SizeOfRawData - self.segments.append(Segment(section.PointerToRawData, vaddr, section.SizeOfRawData, vsize)) + alignment = _section_alignment(section) + past_image_vaddr = (next_vaddr_past_image + alignment - 1) & ~(alignment - 1) + # A section marked IMAGE_SCN_CNT_UNINITIALIZED_DATA has no bytes in the file and states + # PointerToRawData 0, which in the layout above is the file header. A section whose file offset + # does not satisfy the alignment its own header states holds nothing that decodes there. Either + # one gets space of its own past the image; every other section keeps its file offset. A header + # stating no alignment states no requirement, so the 16 _section_alignment returns for that case + # is where to put a section that has to be placed somewhere and never a reason to move one. + uninitialized = bool(section.Characteristics & IMAGE_SCN.CNT_UNINITIALIZED_DATA) + no_file_bytes = not raw_ptr and vsize != 0 and uninitialized + misaligned = raw_ptr != 0 and vsize != 0 and raw_ptr % _section_alignment(section, unstated=1) != 0 + if misaligned and past_image_vaddr + vsize > MAX_IMAGE_SIZE: + # Its bytes are in the file and the image cannot grow far enough to hold them anywhere else. + # Leave it at the offset that holds them rather than at an address nothing backs. + log.warning( + "Section %s states %#x bytes at %#x, which does not satisfy the %#x byte alignment it " + "states, and moving it would take the image past %#x. Leaving it where it is.", + section_name, + vsize, + raw_ptr, + alignment, + MAX_IMAGE_SIZE, + ) + misaligned = False + if not no_file_bytes and not misaligned: + vaddr = raw_ptr + filesize = vsize + else: + # Placing a section with no bytes in the file at the header lays it over the header and, once + # it is longer than the section table, over the sections that follow: a mingw object whose + # .bss is 0x1300 bytes long covers the first 0x11fc bytes of its own .text. + vaddr = past_image_vaddr + next_vaddr_past_image = vaddr + vsize + filesize = 0 if no_file_bytes else vsize + if next_vaddr_past_image <= MAX_IMAGE_SIZE: + image.extend(bytes(next_vaddr_past_image - len(image))) + raw = self._data[raw_ptr : raw_ptr + filesize] + image[vaddr : vaddr + len(raw)] = raw + else: + log.warning( + "Section %s states %#x bytes of uninitialized data, which would take the image past " + "%#x. Leaving it unbacked.", + section_name, + vsize, + MAX_IMAGE_SIZE, + ) + self._section_addrs.append(vaddr) + self.segments.append(Segment(raw_ptr, vaddr, filesize, vsize)) self.sections.append( CoffSection( section_name, - section.PointerToRawData, - section.SizeOfRawData, + raw_ptr, + filesize, vaddr, vsize, section, ) ) - self.memory.add_backer(0, bytes(self._image_vmem)) + self._image_vmem = bytes(image) + self.memory.add_backer(0, self._image_vmem) self.mapped_base = self.linked_base = 0 self.pic = True # assume windows, this can be wrong, but is more often right. @@ -501,11 +568,11 @@ def _add_defined_symbols(self) -> None: self.symbols.add(self.get_symbol(sym_name)) def _add_relocs(self) -> None: - for section_idx, section in enumerate(self._coff.sections): + for section_idx in range(len(self._coff.sections)): for reloc in self._coff.relocations[section_idx]: sym = self._coff.symbols[reloc.SymbolTableIndex] sym_name = self._coff.get_symbol_name(reloc.SymbolTableIndex) - patch_offset = section.PointerToRawData + reloc.VirtualAddress + patch_offset = self._section_addrs[section_idx] + reloc.VirtualAddress if sym.StorageClass in { IMAGE_SYM_CLASS.STATIC, @@ -542,7 +609,7 @@ def get_symbol(self, name: str, produce_extern_symbols: bool = False) -> Symbol }: symbol_type = SymbolType.TYPE_FUNCTION if sym.Type == 0x20 else SymbolType.TYPE_OTHER if sym.SectionNumber > 0: - sym_addr = self._coff.sections[sym.SectionNumber - 1].PointerToRawData + sym.Value + sym_addr = self._section_addrs[sym.SectionNumber - 1] + sym.Value return Symbol(self, name, sym_addr, 1, symbol_type) elif sym.SectionNumber == 0: if produce_extern_symbols: diff --git a/tests/test_coff.py b/tests/test_coff.py index fcc30dd91..887d4e26c 100644 --- a/tests/test_coff.py +++ b/tests/test_coff.py @@ -74,6 +74,78 @@ def test_rel32_relocation_encodes_a_negative_displacement(self): field_addr = section_vaddr(ld.main_object, ".text") + field_offset assert ld.memory.load(field_addr, 4) == struct.pack("= text.vaddr + text.memsize + assert obj.find_section_containing(text.vaddr) is text + # Uninitialized data reads as zero, not as whatever the file happens to hold there. + assert ld.memory.load(bss.vaddr, bss.memsize) == bytes(bss.memsize) + buffer_symbol = obj.get_symbol("_buffer") + assert buffer_symbol is not None + assert buffer_symbol.rebased_addr == bss.vaddr + + def test_a_section_with_no_file_bytes_and_no_flag_gets_no_space(self): + # The section states PointerToRawData 0 with SizeOfRawData 0x4000000 and marks itself + # code, not uninitialized data. Its size is under MAX_IMAGE_SIZE, so the ceiling would + # not stop it; only the IMAGE_SCN_CNT_UNINITIALIZED_DATA condition does. + exe = os.path.join(TEST_BASE, "tests", "x86", "coff_bss_no_flag.obj") + ld = cle.Loader(exe, auto_load_libs=False) + obj = ld.main_object + bss = next(section for section in obj.sections if section.name == ".bss") + + assert bss.memsize == 0x4000000 + assert not bss.only_contains_uninitialized_data + # It keeps the address its header states rather than getting space of its own. + assert bss.vaddr == obj.mapped_base + assert sum(len(backer) for _, backer in obj.memory.backers()) == os.path.getsize(exe) + + def test_an_uninitialized_section_past_the_ceiling_is_not_materialized(self): + # .bss states 0x20000000 bytes and the file is 580 long. Nothing in the file bounds + # SizeOfRawData, so zero-filling whatever it says turns a header field into an allocation. + exe = os.path.join(TEST_BASE, "tests", "x86", "coff_huge_bss.obj") + ld = cle.Loader(exe, auto_load_libs=False) + obj = ld.main_object + bss = next(section for section in obj.sections if section.name == ".bss") + text = next(section for section in obj.sections if section.name == ".text") + + assert bss.memsize == 0x20000000 + assert bss.vaddr >= text.vaddr + text.memsize + # The image is the file plus the .text its own alignment moves past the end, and nothing more, so + # it does not grow with the field. + assert sum(len(backer) for _, backer in obj.memory.backers()) == text.vaddr - obj.mapped_base + text.memsize + with self.assertRaises(KeyError): + ld.memory.load(bss.vaddr, 1) + + def test_sections_are_placed_at_the_alignment_they_state(self): + # MSVC packs an object's raw data with no padding between sections, so a section's file offset + # is only as aligned as the packing leaves it. Every .text$mn here states IMAGE_SCN_ALIGN_16BYTES + # and six of the eight begin at a file offset that is not a multiple of 16. + exe = os.path.join(TEST_BASE, "tests", "x86", "fauxware.obj") + with open(exe, "rb") as f: + data = f.read() + ld = cle.Loader(exe, auto_load_libs=False, perform_relocations=False) + obj = ld.main_object + + text_sections = [section for section in obj.sections if section.name == ".text$mn"] + assert len(text_sections) == 8 + assert sum(1 for section in text_sections if section.offset % 16) == 6 + for section in text_sections: + assert section.vaddr % 16 == 0 + # The section's bytes went with it. + assert ( + ld.memory.load(section.vaddr, section.filesize) + == data[section.offset : section.offset + section.filesize] + ) + if __name__ == "__main__": unittest.main()