1#!/usr/bin/env python3
2# SPDX-License-Identifier: BSD-3-Clause
3#
4# Copyright (c) 2021-2024, Arm Limited. All rights reserved.
5
6"""
7This module's goal is to take an ELF file as an input and extract the memory
8regions that need to be configured on load. The memory region description is put
9into the manifest file so the SPMC set up the memory layout when loading a
10binary format SP.
11"""
12
13from enum import IntFlag
14from math import ceil
15from elftools import __version__ as module_version
16from elftools.elf.elffile import ELFFile
17from elftools.elf.constants import P_FLAGS
18
19assert module_version == "0.31"
20
21class ElfSegmentsToManifest:
22    """
23    The class loads an ELF file and builds up an internal represention of its
24    memory layout then it can write this information into a manifest file.
25    """
26    PAGE_SIZE = 4096
27
28    class GnuNotePropertySection:
29        """ Provides an API to process GNU note property section. """
30
31        GNU_PROPERTY_AARCH64_FEATURE_1_BTI = 1
32
33        def __init__(self, section):
34            self.section = section
35
36        def is_bti_enabled(self):
37            """ Returns whether any of the notes has a BTI enable property """
38
39            def is_bti_property(prop):
40                return prop['pr_type'] == "GNU_PROPERTY_AARCH64_FEATURE_1_AND" and \
41                    prop['pr_data'] & self.GNU_PROPERTY_AARCH64_FEATURE_1_BTI
42
43            def has_bti_property(note):
44                return note["n_name"] == 'GNU' and note["n_type"] == "NT_GNU_PROPERTY_TYPE_0" and \
45                    any(is_bti_property(p) for p in note["n_desc"])
46
47            return any(has_bti_property(n) for n in self.section.iter_notes())
48
49        @staticmethod
50        def is_matching_section(section):
51            """ Checks if the section is a GNU note property section """
52            return section.name == '.note.gnu.property'
53
54
55    class Region:
56        """ Describes a memory region and its attributes. """
57
58        class ManifestMemAttr(IntFlag):
59            """ Type for describing the memory flags of a region. """
60            R = 0x01
61            W = 0x02
62            X = 0x04
63            S = 0x08
64            GP = 0x10
65
66            def get_attr(self):
67                """ Queries the value of the attributes in manifest format. """
68                return self.value
69
70            def __str__(self):
71                return ",".join([str(f.name) for f in __class__ if f in self])
72
73            @staticmethod
74            def from_p_flags(p_flags):
75                """ Creates an instanced initialized by p_flags. """
76                instance = 0
77                instance |= __class__.R if p_flags & P_FLAGS.PF_R else 0
78                instance |= __class__.W if p_flags & P_FLAGS.PF_W else 0
79                instance |= __class__.X if p_flags & P_FLAGS.PF_X else 0
80                return instance
81
82        class LoadFlags(IntFlag):
83            """ Type for describing the memory load flags of a region. """
84            NO_BITS = 0x01
85
86            def get_flags(self):
87                """ Queries the value of the flags in manifest format. """
88                return self.value
89
90            def is_compatible(self, other):
91                """ Return true of the other flags can be merged with self. """
92                return (self & self.NO_BITS) == (other & self.NO_BITS)
93
94            def __str__(self):
95                return ",".join([str(f.name) for f in __class__ if f in self])
96
97        def __init__(self, segment, section):
98            segment = segment.segment
99            sec_h = section.header
100            self.start_address = sec_h.sh_addr
101            self.end_address = sec_h.sh_addr + sec_h.sh_size
102            self.attributes = self.ManifestMemAttr.from_p_flags(segment.header.p_flags)
103            self.load_flags = self.LoadFlags(0)
104            self.load_flags |= self.LoadFlags.NO_BITS if sec_h.sh_type == "SHT_NOBITS" else 0
105            self.sections = [section.name]
106
107        def is_compatible_region(self, region):
108            """ Checks if the other region has compatible attributes/flags. """
109            return self.load_flags.is_compatible(region.load_flags)
110
111        def append_region(self, region):
112            """ Extends the region by the other region. """
113            self.end_address = region.end_address
114            self.sections += region.sections
115
116        def set_bti_if_executable(self):
117            """ Sets GP flag if the region is executable. """
118            if self.attributes & self.ManifestMemAttr.X:
119                self.attributes |= self.ManifestMemAttr.GP
120
121        def write_manifest(self, load_base_addr, manifest_file):
122            """
123            Writes the region into the manifest file. The address is adjusted by load_base_address.
124            """
125            manifest_file.write(f"{self.generate_region_name(load_base_addr)} {{\n")
126            manifest_file.write(f"\t/* {self.generate_section_list()} */\n")
127            manifest_file.write(f"\t{self.serialize_offset(load_base_addr)}\n")
128            manifest_file.write(f"\t{self.serialize_pages_count()}\n")
129            manifest_file.write(f"\t{self.serialize_attributes()}\n")
130            manifest_file.write(f"\t{self.serialize_load_flags()}\n")
131            manifest_file.write("};\n")
132
133        def generate_region_name(self, load_base_addr):
134            """ Generates a name for the region using the region_[start address] pattern. """
135            return f"region_{self.start_address - load_base_addr:x}"
136
137        def generate_section_list(self):
138            """ Lists the name of member sections of the region. """
139            return ", ".join(self.sections)
140
141        def serialize_offset(self, load_base_addr):
142            """ Calculates and outputs the offset of the region in manifest format. """
143            base = self.start_address - load_base_addr
144            end = self.end_address - load_base_addr
145            high, low = (base >> 32) & 0xffffffff, base & 0xffffffff
146            return f"load-address-relative-offset = <0x{high:x} 0x{low:x}>;\t" + \
147                f"/* 0x{base:x} - 0x{end:x} */"
148
149        def serialize_pages_count(self):
150            """ Calculates and outputs the page count of the region in manifest format. """
151            region_length = self.end_address - self.start_address
152            pages_count = ceil(region_length / ElfSegmentsToManifest.PAGE_SIZE)
153            return f"pages-count = <{pages_count}>;\t/* {region_length} bytes */"
154
155        def serialize_attributes(self):
156            """ Generates the memory region attribute value in manifest format. """
157            return f"attributes = <{self.attributes.get_attr()}>;\t/* {self.attributes} */"
158
159        def serialize_load_flags(self):
160            """ Generates the memory region load flags value in manifest format. """
161            return f"load-flags = <{self.load_flags.get_flags()}>;\t/* {self.load_flags} */"
162
163    class Segment:
164        """ Stores a segment and its sections. Able to produce a region list. """
165        def __init__(self, segment, sections):
166            def is_aligned(segment):
167                return segment.header.p_align == ElfSegmentsToManifest.PAGE_SIZE
168            assert is_aligned(segment), "Segments must be 4k aligned, check LD script"
169            self.segment = segment
170            self.sections = []
171            self.gnu_note = None
172
173            for section in sections:
174                if self.segment.section_in_segment(section):
175                    if ElfSegmentsToManifest.GnuNotePropertySection.is_matching_section(section):
176                        self.gnu_note = ElfSegmentsToManifest.GnuNotePropertySection(section)
177                    else:
178                        self.sections.append(section)
179
180            self.regions = []
181            self.merge_sections_to_regions()
182
183        def get_load_address(self):
184            """ Queries the load address of the region. """
185            return self.segment.header.p_vaddr
186
187        def merge_sections_to_regions(self):
188            """ Merges consecutive sections with comptabile attributes/flags into regions. """
189            current_region = None
190            for section in self.sections:
191                region = ElfSegmentsToManifest.Region(self, section)
192                if current_region and current_region.is_compatible_region(region):
193                    current_region.append_region(region)
194                else:
195                    self.regions.append(region)
196                    current_region = region
197
198            # Set GP only for the executable regions if BTI is enabled in the segment
199            if self.gnu_note and self.gnu_note.is_bti_enabled():
200                for region in self.regions:
201                    region.set_bti_if_executable()
202
203        def write_manifest(self, load_base_addr, manifest_file):
204            """ Writes the regions into the manifest file. """
205            for region in self.regions:
206                region.write_manifest(load_base_addr, manifest_file)
207
208    def __init__(self):
209        self.segments = []
210        self.load_base_addr = None
211
212    def read_elf(self, elf_file_fp):
213        """ Reads and parses the sections and segments of the ELF file. """
214        elf_file = ELFFile(elf_file_fp)
215        segments = elf_file.iter_segments()
216
217        def is_load(segment):
218            return segment.header.p_type == "PT_LOAD"
219        self.segments = [self.Segment(s, elf_file.iter_sections()) for s in segments if is_load(s)]
220        self.load_base_addr = min([s.get_load_address() for s in self.segments])
221
222    def write_manifest(self, manifest_file):
223        """ Writes the memory regions of each segment into the manifest. """
224        for segment in self.segments:
225            segment.write_manifest(self.load_base_addr, manifest_file)
226
227
228if __name__ == "__main__":
229    import sys
230
231    ELF_SEGMENTS_TO_MANIFEST = ElfSegmentsToManifest()
232
233    with open(sys.argv[1], "rb") as fp:
234        ELF_SEGMENTS_TO_MANIFEST.read_elf(fp)
235
236    with open(sys.argv[2], "wt", encoding="ascii") as fp:
237        ELF_SEGMENTS_TO_MANIFEST.write_manifest(fp)
238