From 9f8416511e8b9822e0e85a78b5bc3b80cd284e38 Mon Sep 17 00:00:00 2001 From: Yan Date: Tue, 18 Aug 2026 13:21:22 +0000 Subject: [PATCH] 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()