From 9f8416511e8b9822e0e85a78b5bc3b80cd284e38 Mon Sep 17 00:00:00 2001 From: Yan Date: Tue, 18 Aug 2026 13:21:22 +0000 Subject: [PATCH 1/2] COFF: Give a section with no file bytes an address of its own The backend maps the object at its own file offsets and gives each section vaddr = PointerToRawData. A section holding no bytes in the file states that field as 0 -- that is what .bss is -- while SizeOfRawData still states its length, so it lands on the file header and, once it is longer than the header and section table, over the sections that follow. .text begins at file offset 0x104 in a six-section mingw object, so a 0x1300-byte .bss covers its first 0x11fc bytes. find_section_containing() then answers .bss for real code, and CFGFast._generate_cfgnode drops any block whose section is not executable, so those functions are never recovered; uninitialized data reads as the file header rather than zeros, and every .bss symbol is given an address inside the code. A section that states PointerToRawData 0 and marks itself IMAGE_SCN_CNT_UNINITIALIZED_DATA now gets zero-filled space of its own past the image, at the alignment its IMAGE_SCN_ALIGN_* states. Relocation patch offsets and symbol addresses read the same layout, so they follow it. The flag is the condition rather than the zero pointer alone, because the zero pointer alone is what a file controls: a 120-byte object can state PointerToRawData 0 with SizeOfRawData 0x4000000 on a section marked code, and zero-filling that is 64 MiB of allocation bought with one header field. Across 1,480 sections with no bytes in the file, in 64 distinct shapes, every one sets the flag, so requiring it costs nothing real. A section without it keeps the address its header states, which is what master does with it. MAX_IMAGE_SIZE bounds what the flag still admits. SizeOfRawData is 32 bits wide and a section that does set the flag can still state close to 4 GiB, so past 0x10000000 the section is placed and reports its stated size but no zero fill is allocated for it and a warning names it -- the outcome pe.py reaches through max_virtual_address. Co-Authored-By: Claude Opus 5 --- cle/backends/coff.py | 58 ++++++++++++++++++++++++++++++++++++++------ tests/test_coff.py | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/cle/backends/coff.py b/cle/backends/coff.py index 556ca93b8..86caf99ed 100644 --- a/cle/backends/coff.py +++ b/cle/backends/coff.py @@ -430,6 +430,22 @@ 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) -> int: + encoded = (section.Characteristics & _ALIGN_MASK) >> _ALIGN_SHIFT + return 1 << (encoded - 1) if encoded else 16 + + +#: ``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. +MAX_IMAGE_SIZE = 0x10000000 + + COFF_MACHINE_TO_ARCH_NAME = { IMAGE_FILE_MACHINE.I386: "x86", IMAGE_FILE_MACHINE.AMD64: "AMD64", @@ -459,26 +475,52 @@ 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_uninitialized_vaddr = len(image) for section_idx, section in enumerate(self._coff.sections): section_name = self._coff.get_section_name(section_idx) - vaddr = section.PointerToRawData vsize = section.SizeOfRawData - self.segments.append(Segment(section.PointerToRawData, vaddr, section.SizeOfRawData, vsize)) + if section.PointerToRawData or not vsize or not section.Characteristics & IMAGE_SCN.CNT_UNINITIALIZED_DATA: + vaddr = section.PointerToRawData + filesize = vsize + else: + # 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. Placing it there 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. Give it + # zero-filled space of its own past the image instead. + alignment = _section_alignment(section) + vaddr = (next_uninitialized_vaddr + alignment - 1) & ~(alignment - 1) + next_uninitialized_vaddr = vaddr + vsize + if next_uninitialized_vaddr <= MAX_IMAGE_SIZE: + image.extend(bytes(next_uninitialized_vaddr - len(image))) + 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, + ) + filesize = 0 + self._section_addrs.append(vaddr) + self.segments.append(Segment(section.PointerToRawData, vaddr, filesize, vsize)) self.sections.append( CoffSection( section_name, section.PointerToRawData, - section.SizeOfRawData, + 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 +543,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 +584,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..b9adeaf98 100644 --- a/tests/test_coff.py +++ b/tests/test_coff.py @@ -74,6 +74,56 @@ 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 and nothing more, so it does not grow with the field. + assert sum(len(backer) for _, backer in obj.memory.backers()) == os.path.getsize(exe) + with self.assertRaises(KeyError): + ld.memory.load(bss.vaddr, 1) + if __name__ == "__main__": unittest.main() From f75cec678b2778c5e2e82ef784bd6e58d6f83807 Mon Sep 17 00:00:00 2001 From: Yan Date: Tue, 1 Sep 2026 01:29:50 +0000 Subject: [PATCH 2/2] COFF: place a section at an address its alignment allows The backend maps an object at its own file offsets and gives each section vaddr = PointerToRawData. That offset is only as aligned as the file's packing leaves it, and MSVC packs raw data with no padding between sections, so a section commonly begins where the IMAGE_SCN_ALIGN_* in its own header does not allow. All eight .text$mn sections in tests/x86/fauxware.obj state 16-byte alignment and six of them start at a file offset that is not a multiple of 16. x86 and AMD64 never notice, because their instructions have no alignment requirement. ARM64 and ARMNT, whose machine types #724 adds, do: a .text$mn placed on an odd address holds no instruction anything can decode, every function symbol in it lifts to a zero-length block, and CFGFast recovers nothing from it. Over 66 ARM64 COFF objects, 298 of 880 function symbols went unrecovered and 16 objects recovered none of their own; with the sections placed where their headers ask, all 880 are recovered. A section whose file offset does not satisfy its alignment now gets space of its own past the image with its bytes copied in, from the same cursor and under the same MAX_IMAGE_SIZE ceiling as the section marked IMAGE_SCN_CNT_UNINITIALIZED_DATA that already goes there. Where that ceiling would stop the move, the section keeps its file offset, which is where its bytes are. A header that states no alignment states no requirement and its section is left where it is; the 16 _section_alignment returns for that case is only where to put a section that has to be placed somewhere. Co-Authored-By: Claude Opus 5 --- cle/backends/coff.py | 63 +++++++++++++++++++++++++++++++------------- tests/test_coff.py | 26 ++++++++++++++++-- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/cle/backends/coff.py b/cle/backends/coff.py index 86caf99ed..fc17fe820 100644 --- a/cle/backends/coff.py +++ b/cle/backends/coff.py @@ -436,13 +436,14 @@ def value(self): _ALIGN_SHIFT = 20 -def _section_alignment(section: CoffSectionTableEntry) -> int: +def _section_alignment(section: CoffSectionTableEntry, unstated: int = 16) -> int: encoded = (section.Characteristics & _ALIGN_MASK) >> _ALIGN_SHIFT - return 1 << (encoded - 1) if encoded else 16 + 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. +#: 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 @@ -479,24 +480,49 @@ def __init__(self, *args, **kwargs): # Add each section self._section_addrs: list[int] = [] - next_uninitialized_vaddr = len(image) + next_vaddr_past_image = len(image) for section_idx, section in enumerate(self._coff.sections): section_name = self._coff.get_section_name(section_idx) + raw_ptr = section.PointerToRawData vsize = section.SizeOfRawData - if section.PointerToRawData or not vsize or not section.Characteristics & IMAGE_SCN.CNT_UNINITIALIZED_DATA: - vaddr = section.PointerToRawData + 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: - # 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. Placing it there 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. Give it - # zero-filled space of its own past the image instead. - alignment = _section_alignment(section) - vaddr = (next_uninitialized_vaddr + alignment - 1) & ~(alignment - 1) - next_uninitialized_vaddr = vaddr + vsize - if next_uninitialized_vaddr <= MAX_IMAGE_SIZE: - image.extend(bytes(next_uninitialized_vaddr - len(image))) + # 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 " @@ -505,13 +531,12 @@ def __init__(self, *args, **kwargs): vsize, MAX_IMAGE_SIZE, ) - filesize = 0 self._section_addrs.append(vaddr) - self.segments.append(Segment(section.PointerToRawData, vaddr, filesize, vsize)) + self.segments.append(Segment(raw_ptr, vaddr, filesize, vsize)) self.sections.append( CoffSection( section_name, - section.PointerToRawData, + raw_ptr, filesize, vaddr, vsize, diff --git a/tests/test_coff.py b/tests/test_coff.py index b9adeaf98..887d4e26c 100644 --- a/tests/test_coff.py +++ b/tests/test_coff.py @@ -119,11 +119,33 @@ def test_an_uninitialized_section_past_the_ceiling_is_not_materialized(self): assert bss.memsize == 0x20000000 assert bss.vaddr >= text.vaddr + text.memsize - # The image is the file and nothing more, so it does not grow with the field. - assert sum(len(backer) for _, backer in obj.memory.backers()) == os.path.getsize(exe) + # 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()