Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions cle/backends/elf/elf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,7 @@ def __register_sections(self):

for sec_readelf in self._reader.iter_sections():
remap_offset = 0
occupies_memory = True
if self.is_relocatable and sec_readelf.header["sh_flags"] & 2: # alloc flag
# Relocatable objects' section addresses are meaningless (they are meant to be relocated anyway)
# We thus have to map them manually to valid virtual addresses to emulate a linker's behaviour.
Expand All @@ -1451,10 +1452,14 @@ def __register_sections(self):
new_addr = (new_addr + (align - 1)) // align * align
remap_offset = new_addr - sh_addr

if sec_readelf.header["sh_type"] not in _NON_ALLOCATED_SECTION_NAMES:
if sec_readelf.header["sh_type"] in _NON_ALLOCATED_SECTION_NAMES:
# No space is reserved for these and no backer is added below, so the address just
# computed belongs to whichever section comes next.
occupies_memory = False
else:
new_addr += sec_readelf.header["sh_size"]

section = ELFSection(sec_readelf, remap_offset=remap_offset)
section = ELFSection(sec_readelf, remap_offset=remap_offset, occupies_memory=occupies_memory)
sec_list.append((sec_readelf, section))

# Register sections first, process later - this is required by relocatable objects
Expand Down
5 changes: 3 additions & 2 deletions cle/backends/elf/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ELFSection(Section):
SHF_STRINGS = 0x20
SHT_NULL = "SHT_NULL"

def __init__(self, readelf_sec, remap_offset=0):
def __init__(self, readelf_sec, remap_offset=0, occupies_memory=True):
super().__init__(
maybedecode(readelf_sec.name),
readelf_sec.header.sh_offset,
Expand All @@ -61,6 +61,7 @@ def __init__(self, readelf_sec, remap_offset=0):
self.info = readelf_sec.header.sh_info
self.align = readelf_sec.header.sh_addralign
self.remap_offset = remap_offset
self._occupies_memory = occupies_memory

@property
def is_readable(self):
Expand All @@ -76,7 +77,7 @@ def is_writable(self):

@property
def occupies_memory(self):
return self.flags & self.SHF_ALLOC != 0 and self.memsize > 0
return self._occupies_memory and self.flags & self.SHF_ALLOC != 0 and self.memsize > 0

@property
def is_executable(self):
Expand Down
50 changes: 49 additions & 1 deletion tests/test_regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@

import cle
from cle.address_translator import AT
from cle.backends import Section, Segment
from cle.backends import ELF, Section, Segment

TESTS_BASE = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
os.path.join("..", "..", "binaries", "tests"),
)


def name_of(section: Section | None) -> str | None:
"""The name of a section an address lookup returned, or None when it found nothing."""
return None if section is None else section.name


groundtruth = {
("x86_64", "allcmps"): {
"sections": [
Expand Down Expand Up @@ -175,5 +180,48 @@ def test_segments(self):
self._run_segments(arch, filename, data["segments"])


class TestRelocatableSections(unittest.TestCase):
"""
A relocatable object carries no addresses of its own, so CLE assigns them. The assignment has to
agree with what CLE actually maps.
"""

BINARY = os.path.join(TESTS_BASE, "x86_64", "switch_default_abort.o")

def _load(self):
ld = cle.Loader(self.BINARY, auto_load_libs=False)
main = ld.main_object
assert isinstance(main, ELF)
self.assertTrue(main.is_relocatable)
return ld, main

def test_every_mapped_address_resolves_to_its_section(self):
ld, main = self._load()

mapped = sorted((s for s in main.sections if s.occupies_memory), key=lambda s: s.vaddr)
for lower, upper in zip(mapped, mapped[1:]):
self.assertLessEqual(
lower.vaddr + lower.memsize,
upper.vaddr,
f"{lower.name} overlaps {upper.name}",
)

# The lookup bisects on the section end addresses, so one overlap loses whole sections.
for section in mapped:
for addr in range(section.vaddr, section.vaddr + section.memsize):
self.assertEqual(name_of(main.sections.find_region_containing(addr)), section.name)
self.assertEqual(name_of(ld.find_section_containing(addr)), section.name)

def test_unloaded_section_claims_no_address(self):
_, main = self._load()

# No space is reserved for a note section and none of its bytes are loaded, so the address it
# would take belongs to the section that follows it.
note = main.sections_map[".note.gnu.property"]
self.assertEqual(note.type, "SHT_NOTE")
self.assertFalse(note.occupies_memory)
self.assertEqual(main.sections_map[".text"].vaddr, main.mapped_base)


if __name__ == "__main__":
unittest.main()
Loading