fix(memmap): reintroduce support for GNU map files

The intial patch stack only supported ELF files, which proved
particularly problematic when dealing with incomplete builds (i.e. build
didn't complete due to linker errors). This adds support for GNU map
files. Most analysis performed by the tool should be possible with map
files alone.

Change-Id: I89f775a98efc5aef6671a17d0e6e973df555a6fa
Signed-off-by: Harrison Mutai <harrison.mutai@arm.com>
diff --git a/tools/memory/memory/buildparser.py b/tools/memory/memory/buildparser.py
index c128c36..dedff79 100755
--- a/tools/memory/memory/buildparser.py
+++ b/tools/memory/memory/buildparser.py
@@ -8,14 +8,16 @@
 from pathlib import Path
 
 from memory.elfparser import TfaElfParser
+from memory.mapparser import TfaMapParser
 
 
 class TfaBuildParser:
     """A class for performing analysis on the memory layout of a TF-A build."""
 
-    def __init__(self, path: Path):
+    def __init__(self, path: Path, map_backend=False):
         self._modules = dict()
         self._path = path
+        self.map_backend = map_backend
         self._parse_modules()
 
     def __getitem__(self, module: str):
@@ -23,15 +25,24 @@
         return self._modules[module]
 
     def _parse_modules(self):
-        """Parse ELF files in the build path."""
-        for elf_file in self._path.glob("**/*.elf"):
-            module_name = elf_file.name.split("/")[-1].split(".")[0]
-            with open(elf_file, "rb") as file:
-                self._modules[module_name] = TfaElfParser(file)
+        """Parse the build files using the selected backend."""
+        backend = TfaElfParser
+        files = list(self._path.glob("**/*.elf"))
+        io_perms = "rb"
+
+        if self.map_backend or len(files) == 0:
+            backend = TfaMapParser
+            files = self._path.glob("**/*.map")
+            io_perms = "r"
+
+        for file in files:
+            module_name = file.name.split("/")[-1].split(".")[0]
+            with open(file, io_perms) as f:
+                self._modules[module_name] = backend(f)
 
         if not len(self._modules):
             raise FileNotFoundError(
-                f"failed to find ELF files in path {self._path}!"
+                f"failed to find files to analyse in path {self._path}!"
             )
 
     @property
@@ -54,7 +65,7 @@
         """Returns map of memory usage per memory type for each module."""
         mem_map = {}
         for k, v in self._modules.items():
-            mod_mem_map = v.get_elf_memory_layout()
+            mod_mem_map = v.get_memory_layout()
             if len(mod_mem_map):
                 mem_map[k] = mod_mem_map
         return mem_map
diff --git a/tools/memory/memory/elfparser.py b/tools/memory/memory/elfparser.py
index 1bd68b1..2dd2513 100644
--- a/tools/memory/memory/elfparser.py
+++ b/tools/memory/memory/elfparser.py
@@ -131,7 +131,7 @@
         """Get a dictionary of segments and their section mappings."""
         return [asdict(v) for k, v in self._segments.items()]
 
-    def get_elf_memory_layout(self):
+    def get_memory_layout(self):
         """Get the total memory consumed by this module from the memory
         configuration.
             {"rom": {"start": 0x0, "end": 0xFF, "length": ... }
diff --git a/tools/memory/memory/mapparser.py b/tools/memory/memory/mapparser.py
new file mode 100644
index 0000000..b1a4b4c
--- /dev/null
+++ b/tools/memory/memory/mapparser.py
@@ -0,0 +1,75 @@
+#
+# Copyright (c) 2023, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+from re import match, search
+from typing import TextIO
+
+
+class TfaMapParser:
+    """A class representing a map file built for TF-A.
+
+    Provides a basic interface for reading the symbol table. The constructor
+    accepts a file-like object with the contents a Map file. Only GNU map files
+    are supported at this stage.
+    """
+
+    def __init__(self, map_file: TextIO):
+        self._symbols = self.read_symbols(map_file)
+
+    @property
+    def symbols(self):
+        return self._symbols.items()
+
+    @staticmethod
+    def read_symbols(file: TextIO, pattern: str = None) -> dict:
+        pattern = r"\b(0x\w*)\s*(\w*)\s=" if not pattern else pattern
+        symbols = {}
+
+        for line in file.readlines():
+            match = search(pattern, line)
+
+            if match is not None:
+                value, name = match.groups()
+                symbols[name] = int(value, 16)
+
+        return symbols
+
+    def get_memory_layout(self) -> dict:
+        """Get the total memory consumed by this module from the memory
+        configuration.
+            {"rom": {"start": 0x0, "end": 0xFF, "length": ... }
+        """
+        assert len(self._symbols), "Symbol table is empty!"
+        expr = r".*(.?R.M)_REGION.*(START|END|LENGTH)"
+        memory_layout = {}
+
+        region_symbols = filter(lambda s: match(expr, s), self._symbols)
+
+        for symbol in region_symbols:
+            region, _, attr = tuple(symbol.lower().strip("__").split("_"))
+            if region not in memory_layout:
+                memory_layout[region] = {}
+
+            memory_layout[region][attr] = self._symbols[symbol]
+
+            if "start" and "length" and "end" in memory_layout[region]:
+                memory_layout[region]["limit"] = (
+                    memory_layout[region]["end"]
+                    + memory_layout[region]["length"]
+                )
+                memory_layout[region]["free"] = (
+                    memory_layout[region]["limit"]
+                    - memory_layout[region]["end"]
+                )
+                memory_layout[region]["total"] = memory_layout[region][
+                    "length"
+                ]
+                memory_layout[region]["size"] = (
+                    memory_layout[region]["end"]
+                    - memory_layout[region]["start"]
+                )
+
+        return memory_layout
diff --git a/tools/memory/memory/memmap.py b/tools/memory/memory/memmap.py
index 6d6f39d..dda104a 100755
--- a/tools/memory/memory/memmap.py
+++ b/tools/memory/memory/memmap.py
@@ -66,6 +66,11 @@
     default=False,
     help="Display numbers in decimal base.",
 )
+@click.option(
+    "--no-elf-images",
+    is_flag=True,
+    help="Analyse the build's map files instead of ELF images.",
+)
 def main(
     root: Path,
     platform: str,
@@ -76,11 +81,12 @@
     depth: int,
     width: int,
     d: bool,
+    no_elf_images: bool,
 ):
     build_path = root if root else Path("build/", platform, build_type)
     click.echo(f"build-path: {build_path.resolve()}")
 
-    parser = TfaBuildParser(build_path)
+    parser = TfaBuildParser(build_path, map_backend=no_elf_images)
     printer = TfaPrettyPrinter(columns=width, as_decimal=d)
 
     if footprint or not (tree or symbols):
diff --git a/tools/memory/memory/printer.py b/tools/memory/memory/printer.py
index 6bc6bff..4b18560 100755
--- a/tools/memory/memory/printer.py
+++ b/tools/memory/memory/printer.py
@@ -95,13 +95,16 @@
         self,
         symbols: list,
         modules: list,
-        start: int = 11,
+        start: int = 12,
     ):
         assert len(symbols), "Empty symbol list!"
         modules = sorted(modules)
         col_width = int((self.term_size - start) / len(modules))
+        address_fixed_width = 11
 
-        num_fmt = "0=#010x" if not self.as_decimal else ">10"
+        num_fmt = (
+            f"0=#0{address_fixed_width}x" if not self.as_decimal else ">10"
+        )
 
         _symbol_map = [
             " " * start