Add tools for exporting SP memory regions

Create a Python script and CMake functions for parsing the SP ELF file
and exporting the memory region information into the SP manifest file.

Signed-off-by: Imre Kis <imre.kis@arm.com>
Change-Id: I800bc2c85fa11416843b71958a796a7b5321bd2d
diff --git a/requirements.txt b/requirements.txt
index 7017f69..3e00abc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 #
-# Copyright (c) 2020-2021, Arm Limited and Contributors. All rights reserved.
+# Copyright (c) 2020-2022, Arm Limited and Contributors. All rights reserved.
 #
 # SPDX-License-Identifier: BSD-3-Clause
 #
@@ -13,5 +13,8 @@
 # Include packages needed for build test tool
 -r tools/b-test/requirements.txt
 
+# Include packages needed for custom Python tools
+-r tools/python/requirements.txt
+
 # Include c-picker needed for firmware-test-builder
 git+https://git.trustedfirmware.org/TS/trusted-services.git@topics/c-picker
diff --git a/tools/cmake/common/ExportMemoryRegionsToManifest.cmake b/tools/cmake/common/ExportMemoryRegionsToManifest.cmake
new file mode 100644
index 0000000..affd746
--- /dev/null
+++ b/tools/cmake/common/ExportMemoryRegionsToManifest.cmake
@@ -0,0 +1,46 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2021-2022, Arm Limited and Contributors. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+#-------------------------------------------------------------------------------
+
+#[===[.rst:
+.. cmake:command:: export_memory_regions_to_manifest
+
+	Exports the memory regions from an ELF format SP into a manifest file fragment.
+
+	.. code:: cmake
+
+		export_memory_regions_to_manifest(TARGET NAME RES)
+
+	INPUTS:
+
+	``TARGET``
+	Build target
+
+	``NAME``
+	The UUID of the SP as a string.
+
+	``RES``
+	The name of the SP.
+
+#]===]
+function(export_memory_regions_to_manifest)
+	set(options)
+	set(oneValueArgs TARGET NAME RES)
+	set(multiValueArgs)
+	cmake_parse_arguments(MY "${options}" "${oneValueArgs}"
+						"${multiValueArgs}" ${ARGN} )
+
+	find_package(Python3 REQUIRED COMPONENTS Interpreter)
+
+	add_custom_command(
+		TARGET ${MY_TARGET} POST_BUILD
+		COMMAND ${Python3_EXECUTABLE} ${TS_ROOT}/tools/python/elf_segments_to_manifest.py
+				$<TARGET_FILE:${MY_TARGET}>
+				$<TARGET_FILE_DIR:${MY_TARGET}>/${MY_NAME})
+	if (MY_RES)
+		set(${MY_RES} $<TARGET_FILE_DIR:${MY_TARGET}>/${MY_NAME} PARENT_SCOPE)
+	endif()
+endfunction()
diff --git a/tools/cmake/common/ExportSp.cmake b/tools/cmake/common/ExportSp.cmake
index f324327..f22e9cb 100644
--- a/tools/cmake/common/ExportSp.cmake
+++ b/tools/cmake/common/ExportSp.cmake
@@ -10,7 +10,13 @@
 
 	.. code:: cmake
 
-		export_sp(SP_UUID <uuid> SP_NAME <name> MK_IN <.mk path> DTS_IN <DTS path> JSON_IN <JSON path>)
+		export_sp(
+			SP_UUID <uuid> SP_NAME
+			<name> MK_IN <.mk path>
+			DTS_IN <DTS path>
+			DTS_MEM_REGIONS <Memory region manifest path>
+			JSON_IN <JSON path>
+		)
 
 	INPUTS:
 
@@ -26,13 +32,16 @@
 	``DTS_IN``
 	Manifest file template
 
+	`DTS_MEM_REGIONS`
+	Optional, Memory region manifest file
+
 	``JSON_IN``
 	Optional, SP layout JSON file template for TF-A
 
 #]===]
 function (export_sp)
 	set(options)
-	set(oneValueArgs SP_UUID SP_NAME MK_IN DTS_IN JSON_IN)
+	set(oneValueArgs SP_UUID SP_NAME MK_IN DTS_IN DTS_MEM_REGIONS JSON_IN)
 	set(multiValueArgs)
 	cmake_parse_arguments(EXPORT "${options}" "${oneValueArgs}"
 						"${multiValueArgs}" ${ARGN} )
@@ -74,8 +83,12 @@
 	configure_file(${EXPORT_DTS_IN} ${CMAKE_CURRENT_BINARY_DIR}/${EXPORT_SP_UUID}.dts @ONLY NEWLINE_STYLE UNIX)
 	install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${EXPORT_SP_UUID}.dts DESTINATION ${TS_ENV}/manifest)
 
+	if (DEFINED EXPORT_DTS_MEM_REGIONS)
+		install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${EXPORT_DTS_MEM_REGIONS} DESTINATION ${TS_ENV}/manifest)
+	endif()
+
 	if (DEFINED EXPORT_JSON_IN)
 		configure_file(${EXPORT_JSON_IN} ${CMAKE_CURRENT_BINARY_DIR}/${EXPORT_SP_NAME}.json @ONLY NEWLINE_STYLE UNIX)
 		install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${EXPORT_SP_NAME}.json DESTINATION ${TS_ENV}/json)
 	endif()
-endfunction()
\ No newline at end of file
+endfunction()
diff --git a/tools/python/elf_segments_to_manifest.py b/tools/python/elf_segments_to_manifest.py
new file mode 100644
index 0000000..5528164
--- /dev/null
+++ b/tools/python/elf_segments_to_manifest.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# Copyright (c) 2021-2022, Arm Limited. All rights reserved.
+
+"""
+This module's goal is to take an ELF file as an input and extract the memory
+regions that need to be configured on load. The memory region description is put
+into the manifest file so the SPMC set up the memory layout when loading a
+binary format SP.
+"""
+
+from enum import IntFlag
+from math import ceil
+from elftools.elf.elffile import ELFFile
+from elftools.elf.constants import P_FLAGS
+
+
+class ElfSegmentsToManifest:
+    """
+    The class loads an ELF file and builds up an internal represention of its
+    memory layout then it can write this information into a manifest file.
+    """
+    PAGE_SIZE = 4096
+
+    class Region:
+        """ Describes a memory region and its attributes. """
+
+        class ManifestMemAttr(IntFlag):
+            """ Type for describing the memory flags of a region. """
+            R = 0x01
+            W = 0x02
+            X = 0x04
+
+            def get_attr(self):
+                """ Queries the value of the attributes in manifest format. """
+                return self.value
+
+            def __str__(self):
+                return ",".join([str(f.name) for f in __class__ if f in self])
+
+            @staticmethod
+            def from_p_flags(p_flags):
+                """ Creates an instanced initialized by p_flags. """
+                instance = 0
+                instance |= __class__.R if p_flags & P_FLAGS.PF_R else 0
+                instance |= __class__.W if p_flags & P_FLAGS.PF_W else 0
+                instance |= __class__.X if p_flags & P_FLAGS.PF_X else 0
+                return instance
+
+        class LoadFlags(IntFlag):
+            """ Type for describing the memory load flags of a region. """
+            NO_BITS = 0x01
+
+            def get_flags(self):
+                """ Queries the value of the flags in manifest format. """
+                return self.value
+
+            def is_compatible(self, other):
+                """ Return true of the other flags can be merged with self. """
+                return (self & self.NO_BITS) == (other & self.NO_BITS)
+
+            def __str__(self):
+                return ",".join([str(f.name) for f in __class__ if f in self])
+
+        def __init__(self, segment, section):
+            segment = segment.segment
+            sec_h = section.header
+            self.start_address = sec_h.sh_addr
+            self.end_address = sec_h.sh_addr + sec_h.sh_size
+            self.attributes = self.ManifestMemAttr.from_p_flags(segment.header.p_flags)
+            self.load_flags = self.LoadFlags(0)
+            self.load_flags |= self.LoadFlags.NO_BITS if sec_h.sh_type == "SHT_NOBITS" else 0
+            self.sections = [section.name]
+
+        def is_compatible_region(self, region):
+            """ Checks if the other region has compatible attributes/flags. """
+            return self.load_flags.is_compatible(region.load_flags)
+
+        def append_region(self, region):
+            """ Extends the region by the other region. """
+            self.end_address = region.end_address
+            self.sections += region.sections
+
+        def write_manifest(self, load_base_addr, manifest_file):
+            """
+            Writes the region into the manifest file. The address is adjusted by load_base_address.
+            """
+            manifest_file.write(f"{self.generate_region_name(load_base_addr)} {{\n")
+            manifest_file.write(f"\t/* {self.generate_section_list()} */\n")
+            manifest_file.write(f"\t{self.serialize_offset(load_base_addr)}\n")
+            manifest_file.write(f"\t{self.serialize_pages_count()}\n")
+            manifest_file.write(f"\t{self.serialize_attributes()}\n")
+            manifest_file.write(f"\t{self.serialize_load_flags()}\n")
+            manifest_file.write("};\n")
+
+        def generate_region_name(self, load_base_addr):
+            """ Generates a name for the region using the region_[start address] pattern. """
+            return f"region_{self.start_address - load_base_addr:x}"
+
+        def generate_section_list(self):
+            """ Lists the name of member sections of the region. """
+            return ", ".join(self.sections)
+
+        def serialize_offset(self, load_base_addr):
+            """ Calculates and outputs the offset of the region in manifest format. """
+            base = self.start_address - load_base_addr
+            end = self.end_address - load_base_addr
+            high, low = (base >> 32) & 0xffffffff, base & 0xffffffff
+            return f"load-address-relative-offset = <0x{high:x} 0x{low:x}>;\t" + \
+                f"/* 0x{base:x} - 0x{end:x} */"
+
+        def serialize_pages_count(self):
+            """ Calculates and outputs the page count of the region in manifest format. """
+            region_length = self.end_address - self.start_address
+            pages_count = ceil(region_length / ElfSegmentsToManifest.PAGE_SIZE)
+            return f"pages-count = <{pages_count}>;\t/* {region_length} bytes */"
+
+        def serialize_attributes(self):
+            """ Generates the memory region attribute value in manifest format. """
+            return f"attributes = <{self.attributes.get_attr()}>;\t/* {self.attributes} */"
+
+        def serialize_load_flags(self):
+            """ Generates the memory region load flags value in manifest format. """
+            return f"load-flags = <{self.load_flags.get_flags()}>;\t/* {self.load_flags} */"
+
+    class Segment:
+        """ Stores a segment and its sections. Able to produce a region list. """
+        def __init__(self, segment, sections):
+            def is_aligned(segment):
+                return segment.header.p_align == ElfSegmentsToManifest.PAGE_SIZE
+            assert is_aligned(segment), "Segments must be 4k aligned, check LD script"
+            self.segment = segment
+            self.sections = [s for s in sections if self.segment.section_in_segment(s)]
+            self.regions = []
+            self.merge_sections_to_regions()
+
+        def get_load_address(self):
+            """ Queries the load address of the region. """
+            return self.segment.header.p_vaddr
+
+        def merge_sections_to_regions(self):
+            """ Merges consecutive sections with comptabile attributes/flags into regions. """
+            current_region = None
+            for section in self.sections:
+                region = ElfSegmentsToManifest.Region(self, section)
+                if current_region and current_region.is_compatible_region(region):
+                    current_region.append_region(region)
+                else:
+                    self.regions.append(region)
+                    current_region = region
+
+        def write_manifest(self, load_base_addr, manifest_file):
+            """ Writes the regions into the manifest file. """
+            for region in self.regions:
+                region.write_manifest(load_base_addr, manifest_file)
+
+    def __init__(self):
+        self.segments = []
+        self.load_base_addr = None
+
+    def read_elf(self, elf_file_fp):
+        """ Reads and parses the sections and segments of the ELF file. """
+        elf_file = ELFFile(elf_file_fp)
+        segments = elf_file.iter_segments()
+
+        def is_load(segment):
+            return segment.header.p_type == "PT_LOAD"
+        self.segments = [self.Segment(s, elf_file.iter_sections()) for s in segments if is_load(s)]
+        self.load_base_addr = min([s.get_load_address() for s in self.segments])
+
+    def write_manifest(self, manifest_file):
+        """ Writes the memory regions of each segment into the manifest. """
+        for segment in self.segments:
+            segment.write_manifest(self.load_base_addr, manifest_file)
+
+
+if __name__ == "__main__":
+    import sys
+
+    ELF_SEGMENTS_TO_MANIFEST = ElfSegmentsToManifest()
+
+    with open(sys.argv[1], "rb") as fp:
+        ELF_SEGMENTS_TO_MANIFEST.read_elf(fp)
+
+    with open(sys.argv[2], "wt", encoding="ascii") as fp:
+        ELF_SEGMENTS_TO_MANIFEST.write_manifest(fp)
diff --git a/tools/python/requirements.txt b/tools/python/requirements.txt
new file mode 100644
index 0000000..81bf605
--- /dev/null
+++ b/tools/python/requirements.txt
@@ -0,0 +1,7 @@
+#
+# Copyright (c) 2022, Arm Limited and Contributors. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+pyelftools==0.28