diff --git a/cle/loader.py b/cle/loader.py index 694d6662..17580839 100644 --- a/cle/loader.py +++ b/cle/loader.py @@ -40,6 +40,11 @@ log = logging.getLogger(name=__name__) +# The floor for the guard region at the bottom of the address space. The loader hands it out only +# when there is nowhere else, because a null or uninitialized pointer in the target reads as an +# address inside it. Where the loader knows the target's page size, the guard tracks it instead. +NULL_PAGE_SIZE = 0x1000 + if TYPE_CHECKING: from .backends import Region, Section, Segment from .backends.relocation import Relocation @@ -1074,23 +1079,27 @@ def _find_safe_rebase_addr(self, size): """ # this assumes that self.main_object exists, which should... definitely be safe limit = 2**self.main_object.arch.bits + above_image = self.main_object.max_addr + 1 if self.main_object.arch.bits < 32 or self.main_object.max_addr >= 2 ** (self.main_object.arch.bits - 1): - # HACK: On small arches, we should be more aggressive in packing stuff in. An image - # reaching into the top half of the address space leaves its free space underneath it. - start = 0 + # A small address space, or an image that reaches into the top half of one, may have + # its free space underneath the image. Take that space once the space above the image + # is exhausted, and the null page only when there is nothing else at all. The + # loader's page_size defaults to 1, which means the target's page size is unknown. + starts = (above_image, max(self.page_size, NULL_PAGE_SIZE), 0) else: - start = self.main_object.max_addr + 1 + starts = (above_image,) # The granularity is a preference, not a constraint: it costs up to one granule per object, # which a small address space runs out of long before the space itself is full. alignments = [self._rebase_granularity] alignments += [a for a in (0x1000, 1) if a < self._rebase_granularity] - for alignment in alignments: - for gap_start, gap_end in self._free_gaps(start, limit): - addr = ALIGN_UP(gap_start, alignment) - if addr + size <= gap_end: - return addr + for start in starts: + for alignment in alignments: + for gap_start, gap_end in self._free_gaps(start, limit): + addr = ALIGN_UP(gap_start, alignment) + if addr + size <= gap_end: + return addr raise CLEOperationError("Ran out of room in address space") diff --git a/tests/test_rebase.py b/tests/test_rebase.py index 41b9c1b1..ea2b4074 100644 --- a/tests/test_rebase.py +++ b/tests/test_rebase.py @@ -1,6 +1,14 @@ from __future__ import annotations import os +import unittest + +import archinfo + +try: + import pypcode +except ImportError: + pypcode = None import cle @@ -32,8 +40,9 @@ def check_sparse_elf(name): tls = ld.tls.new_thread() for obj in (extern, tls): - # the main object starts at 0xf800; anything placed inside its span is unreachable - assert obj.max_addr < 0xF800 + # the main object's memory is one backer spanning 0xf800 to 0xfff00fff, so anything + # placed inside that span is unreachable + assert obj.max_addr < 0xF800 or obj.min_addr > 0xFFF00FFF ld.memory.unpack_word(obj.min_addr) @@ -69,7 +78,90 @@ def test_rebase_granularity_is_not_a_hard_object_limit(): assert ld.find_object_containing(obj.min_addr) is obj +def load_blob(name, arch, base_addr): + path = os.path.join(TEST_BASE, "tests", *name) + return cle.Loader(path, auto_load_libs=False, main_opts={"backend": "blob", "arch": arch, "base_addr": base_addr}) + + +def test_image_in_the_top_half_leaves_the_null_page_free(): + """ + An image based at 0x90000000 has free space above it and below it. The extern object may + go either way, but not over address 0, which every uninitialized pointer in the target + reads as. + """ + ld = load_blob(("armel", "i2c_master_read-nucleol152re.bin"), "ARMEL", 0x90000000) + assert ld.main_object.max_addr >= 2**31 + + extern = ld.extern_object + assert extern.min_addr > ld.main_object.max_addr + assert ld.find_object_containing(0) is None + + +@unittest.skipUnless(pypcode is not None, "pypcode not installed") +def test_narrow_address_space_leaves_the_null_page_free(): + """ + A 16-bit image at 0x4000 leaves 48 KB above it, and a z80 reaches address 0 with RST and + a direct-page call, so the extern object must not answer for those addresses. + """ + ld = load_blob(("i386", "rcr_test"), archinfo.ArchPcode("z80:LE:16:default"), 0x4000) + + extern = ld.extern_object + assert extern.min_addr > ld.main_object.max_addr + assert extern.max_addr < 2**16 + assert ld.find_object_containing(0) is None + ld.memory.unpack_word(extern.min_addr, size=1) + + +@unittest.skipUnless(pypcode is not None, "pypcode not installed") +def test_null_page_is_used_when_the_address_space_has_nothing_else(): + """ + Keeping the null page free is a preference, not a constraint. A 16-bit address space with + everything else taken must still place an object rather than fail the load. + """ + ld = load_blob(("i386", "rcr_test"), archinfo.ArchPcode("z80:LE:16:default"), 0x4000) + arch = ld.main_object.arch + main = ld.main_object + + above = MockBackend(2**16 - (main.max_addr + 1), arch=arch) + below = MockBackend(main.min_addr - 0x1000, arch=arch) + for obj in (above, below): + ld.dynamic_load(obj) + assert (above.min_addr, above.max_addr) == (main.max_addr + 1, 2**16 - 1) + assert (below.min_addr, below.max_addr) == (0x1000, main.min_addr - 1) + + last = MockBackend(0x100, arch=arch) + ld.dynamic_load(last) + assert last.min_addr == 0 + + +def test_null_guard_tracks_the_loader_page_size(): + """ + Where the loader is told the target's page size, the guard region at the bottom of the + address space is one such page rather than the conventional 4 KB. + """ + path = os.path.join(TEST_BASE, "tests", "armel", "i2c_master_read-nucleol152re.bin") + ld = cle.Loader( + path, + auto_load_libs=False, + page_size=0x2000, + main_opts={"backend": "blob", "arch": "ARMEL", "base_addr": 0x90000000}, + ) + main = ld.main_object + arch = main.arch + + above = MockBackend(2**32 - (main.max_addr + 1), arch=arch) + below = MockBackend(main.min_addr - 0x2000, arch=arch) + for obj in (above, below): + ld.dynamic_load(obj) + assert (above.min_addr, above.max_addr) == (main.max_addr + 1, 2**32 - 1) + assert (below.min_addr, below.max_addr) == (0x2000, main.min_addr - 1) + + if __name__ == "__main__": test_sparse_main_object() test_sparse_main_object_unsorted_program_headers() test_rebase_granularity_is_not_a_hard_object_limit() + test_image_in_the_top_half_leaves_the_null_page_free() + test_narrow_address_space_leaves_the_null_page_free() + test_null_page_is_used_when_the_address_space_has_nothing_else() + test_null_guard_tracks_the_loader_page_size()