build(app): add python scripts for El0 apps

Add python scripts for creating final RMM image file with apps boundled
to it.

* app/gen_app_bin.py is for generating bin file from an app elf file.
  This bin can be bundled with an RMM bin file
* app/bundle_app_rmm.py is for creating a bundle from an RMM bin image,
  and 1 or more app bin files.

Change-Id: I209d6869370659a764b2849d45bb6b147c484e7d
Signed-off-by: Mate Toth-Pal <mate.toth-pal@arm.com>
diff --git a/app/gen_app_bin.py b/app/gen_app_bin.py
new file mode 100755
index 0000000..7c1ac2a
--- /dev/null
+++ b/app/gen_app_bin.py
@@ -0,0 +1,360 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# SPDX-FileCopyrightText: Copyright TF-RMM Contributors.
+#
+
+"""
+Script for creating a bin file from the compiled app's elf file.
+
+The name of the elf sections that are expected and added in the output binary
+are hardcoded in the script. The resulting binary consists of a header which is
+page in size and content of selected sections which are appended after the
+header.
+```
++----------------------+
+|                      |
+|       Header         |
+|                      |
++----------------------+
+|                      |
+| Content of section 1 |
+|                      |
++----------------------+
+|                      |
+           ...
+|                      |
++----------------------+
+|                      |
+|Content of section n  |
+|                      |
++----------------------+
+```
+"""
+
+from argparse import ArgumentParser
+from collections import namedtuple
+import logging
+import struct
+
+
+from elftools.elf.elffile import ELFFile
+
+INDENTATION = "\t"
+APP_HEADER_MAGIC = 0x000E10ABB4EAD000  # El0 APP HEAD
+HEADER_VERSION_MAJOR = 0
+HEADER_VERSION_MINOR = 1
+# TODO: get page size from command line
+PAGE_SIZE = 4096
+APP_HEADER_RESERVED_BYTES = 10 * 8
+APP_NAME_BUF_SIZE = 32
+
+HEADER_VERSION = (HEADER_VERSION_MAJOR << 16) | HEADER_VERSION_MINOR
+
+# The header format needs to match the header in
+# "app/common/framework/include/fake_host/app_header_structures.h" file as defined in
+# 'struct el0_app_header'
+BIN_HEADER_FORMAT = "".join(
+    [
+        "<",  # little endian
+        "Q",  # uint64_t padding;
+        "Q",  # uint64_t app_header_magic;
+        "L",  # uint32_t app_header_version;
+        f"{APP_NAME_BUF_SIZE}s" # const char app_name[APP_NAME_BUF_SIZE];
+        "L",  # uint32_t app_id;
+        "Q",  # uint64_t app_len; /* including header */
+        "Q",  # uintptr_t section_text_offset;
+        "Q",  # uintptr_t section_text_va;
+        "Q",  # size_t section_text_size;
+        "Q",  # uintptr_t section_rodata_offset;
+        "Q",  # uintptr_t section_rodata_va;
+        "Q",  # size_t section_rodata_size;
+        "Q",  # uintptr_t section_data_offset;
+        "Q",  # uintptr_t section_data_va;
+        "Q",  # size_t section_data_size;
+        "Q",  # uintptr_t section_bss_va ;
+        "Q",  # size_t section_bss_size;
+        "Q",  # section_shared_va;
+        "Q",  # size_t stack_page_count;
+        "Q",  # size_t heap_page_count;
+        f"{APP_HEADER_RESERVED_BYTES}s"  # reserved
+        "Q",  # uint64_t app_header_magic2;
+    ]
+)
+
+SectionParams = namedtuple("SectionParams", ["name", "emit"])
+SectionData = namedtuple("SectionData", ["params", "vma", "size", "data"])
+
+
+def get_section(sections, idx, name):
+    """Returns the section in the sections list at the given index.
+
+    The function makes sure that the section at the specified index has the
+    specified name.
+    """
+    if sections[idx].params.name != name:
+        logging.error(
+            f"At idx {idx} section name '{sections[idx].params.name}' doesn't match '{name}'"
+        )
+        assert False
+    return sections[idx]
+
+def get_app_name_bytes(app_name):
+    if len(app_name) >= (APP_NAME_BUF_SIZE):
+        app_name = app_name[:APP_NAME_BUF_SIZE -1]
+    b_appname = app_name.encode()
+    assert(len(b_appname) < APP_NAME_BUF_SIZE)
+    return bytes().join([b_appname, bytes([0] * (APP_NAME_BUF_SIZE - len(b_appname)))])
+
+def emit_bin_file_header(out_bin_file, app_name, app_id, app_len, stack_page_count, heap_page_count, sections):
+    """Emit the bin file header that will be parsed by the RMM code"""
+    text_section = get_section(sections, 0, ".text")
+    rodata_section = get_section(sections, 1, ".rodata")
+    data_section = get_section(sections, 2, ".data")
+    bss_section = get_section(sections, 3, ".bss")
+    shared_section = get_section(sections, 4, ".shared")
+    text_offset = 0
+    rodata_offset = text_offset + text_section.size
+    data_offset = rodata_offset + rodata_section.size
+    header = struct.pack(
+        BIN_HEADER_FORMAT,
+        0,
+        APP_HEADER_MAGIC,
+        HEADER_VERSION,
+        get_app_name_bytes(app_name),
+        app_id,
+        app_len,
+        0,  # text
+        text_section.vma,
+        text_section.size,
+        rodata_offset,  # rodata
+        rodata_section.vma,
+        rodata_section.size,
+        data_offset,  # data
+        data_section.vma,
+        data_section.size,
+        bss_section.vma,  # bss
+        bss_section.size,
+        shared_section.vma,  # shared
+        stack_page_count,  # stack
+        heap_page_count,  # heap
+        bytes(APP_HEADER_RESERVED_BYTES),
+        APP_HEADER_MAGIC,
+    )
+    logging.info(f"Emitting binary header, {len(header)} bytes.")
+    logging.info(
+        f"    app_header_magic: #1={APP_HEADER_MAGIC:016x}, #2={APP_HEADER_MAGIC:016x}"
+    )
+    logging.info(f"    app_name               =   {app_name:16}")
+    logging.info(f"    app_header_version     =   0x{HEADER_VERSION:16x}")
+    logging.info(f"    app_id                 =     {app_id:16}")
+    logging.info(f"    app_len                =   0x{app_len:16x}")
+
+    logging.info("    section  |   offset |               va |     size")
+    logging.info("    ---------|----------|------------------|---------")
+    logging.info(
+        f"    text     | {0:8x} | {text_section.vma:16x} | {text_section.size:8x}"
+    )
+    logging.info(
+        f"    rodata   | {rodata_offset:8x} | {rodata_section.vma:16x} | {rodata_section.size:8x}"
+    )
+    logging.info(
+        f"    data     | {data_offset:8x} | {data_section.vma:16x} | {data_section.size:8x}"
+    )
+    logging.info(
+        f"    bss      |      N/A | {bss_section.vma:16x} | {bss_section.size:8x}"
+    )
+    logging.info(f"    shared   |      N/A | {shared_section.vma:16x} | {PAGE_SIZE:8x}")
+    logging.info(
+        f"    stack    |      N/A |              N/A | {stack_page_count*PAGE_SIZE:8x}"
+    )
+    logging.info(
+        f"    heap     |      N/A |              N/A | {heap_page_count*PAGE_SIZE:8x}"
+    )
+
+    out_bin_file.write(header)
+
+    # emit padding to keep the app binary page aligned
+    assert len(header) < PAGE_SIZE
+    out_bin_file.write(bytearray([0] * (PAGE_SIZE - len(header))))
+
+    return PAGE_SIZE
+
+
+def emit_section_data(out_bin_file, sections):
+    """Emitting content of a section
+
+    Return the number of bytes emitted for this section
+    """
+    bytes_emitted = 0
+    for section in sections:
+        if section.data is not None and section.params.emit:
+            logging.info(
+                f"Emitting section '{section.params.name}', {len(section.data)} bytes."
+            )
+            out_bin_file.write(section.data)
+            bytes_emitted += len(section.data)
+    return bytes_emitted
+
+
+def calc_sections_size(sections):
+    """Calculates the length of the sections part of the bin"""
+    length = PAGE_SIZE  # accounting for header
+    for section in sections:
+        if section.data is not None and section.params.emit:
+            length += len(section.data)
+    if length % PAGE_SIZE != 0:
+        length += PAGE_SIZE - (length % PAGE_SIZE)
+    return length
+
+
+def emit_bin_file(out_bin_file_name, app_name, app_id, stack_page_count, heap_page_count, sections):
+    """Write the bin file"""
+    bytes_emitted = 0
+    with open(out_bin_file_name, "wb") as out_bin_file:
+        # Calculate the length of the bin file payload to be written in to the header
+        app_len = calc_sections_size(sections)
+        # Write the bin header
+        bytes_emitted += emit_bin_file_header(
+            out_bin_file, app_name, app_id, app_len, stack_page_count, heap_page_count, sections
+        )
+        # Write the sections that needs to be emitted
+        bytes_emitted += emit_section_data(out_bin_file, sections)
+
+        # Add padding so that the bin file is aligned to page boundary
+        padding_length = PAGE_SIZE - (((bytes_emitted + PAGE_SIZE - 1) % PAGE_SIZE) + 1)
+        assert (bytes_emitted + padding_length) % PAGE_SIZE == 0
+        assert padding_length < PAGE_SIZE
+        if padding_length:
+            out_bin_file.write(bytearray([0] * padding_length))
+            bytes_emitted += padding_length
+        assert bytes_emitted == app_len
+
+
+def get_sections_data(elffile, sections_params):
+    elf_section_idx = 0
+    section_idx = 0
+    sections_found = []
+
+    expected_section_names = [section.name for section in sections_params]
+
+    # Assume that the interesting sections are in the expected order
+    while elf_section_idx < elffile.num_sections() and section_idx < len(sections_params):
+        elf_section = elffile.get_section(elf_section_idx)
+
+        if elf_section.name not in expected_section_names[section_idx:]:
+            logging.info(
+                f"Skipping section {elf_section.name}, ({elf_section.data_size} bytes)"
+            )
+            elf_section_idx += 1
+            if elf_section_idx == elffile.num_sections():
+                break
+            continue
+
+        section_params = sections_params[section_idx]
+
+        if elf_section.name != section_params.name:
+            logging.info(
+                f"Section {expected_section_names[section_idx]} not found in the elf file"
+            )
+            section_idx += 1
+            continue
+
+        assert elf_section.name == section_params.name
+
+        logging.info(f"Found section {elf_section.name} size={elf_section.data_size}")
+
+        section_data = SectionData(
+            params=section_params,
+            vma=elf_section.header["sh_addr"],
+            size=elf_section.data_size,
+            data=elf_section.data(),
+        )
+        sections_found.append(section_data)
+        assert section_data.size == len(section_data.data)
+
+        elf_section_idx += 1
+        section_idx += 1
+
+    while elf_section_idx < elffile.num_sections():
+        elf_section = elffile.get_section(elf_section_idx)
+        logging.info(
+            f"Skipping section {elf_section.name}, ({elf_section.data_size} bytes)"
+        )
+        elf_section_idx += 1
+
+    while section_idx < len(sections_params):
+        section = sections_params[section_idx]
+        logging.info(f"Section {section.name} not found in the elf file")
+        section_idx += 1
+
+    return sections_found
+
+
+def parse_elf_file(elf_file_name):
+    """parse the elf file
+
+    returns the relevant sections' data found in the file
+    """
+    with open(elf_file_name, "rb") as in_file:
+        sections = [
+            SectionParams(".text", emit=True),
+            SectionParams(".rodata", emit=True),
+            SectionParams(".data", emit=True),
+            SectionParams(".bss", emit=False),
+            SectionParams(".shared", emit=False),
+        ]
+
+        elffile = ELFFile(in_file)
+
+        logging.info(f"{elf_file_name} has {elffile.num_sections()} sections.")
+
+        # Assume that the interesting sections are in the expected order
+        return get_sections_data(elffile, sections)
+
+
+def main():
+    """Main function of the script"""
+    logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.DEBUG)
+
+    parser = ArgumentParser(description="Generate app data files for packaging.")
+    parser.add_argument(
+        "--elf-file", type=str, required=True, help="elf file to generate raw data for"
+    )
+    parser.add_argument(
+        "--app-id",
+        type=lambda v: int(v, 0),
+        required=True,
+        help="The ID of the application used in the RMM code",
+    )
+    parser.add_argument(
+        "--app-name",
+        required=True,
+        help="The name of the app",
+    )
+    parser.add_argument(
+        "--stack-page-count",
+        type=int,
+        required=True,
+        help="The stack size required by the application",
+    )
+    parser.add_argument(
+        "--heap-page-count",
+        type=int,
+        required=True,
+        help="The heap size required by the application (0 is valid)",
+    )
+    parser.add_argument(
+        "--out-bin",
+        type=str,
+        required=True,
+        help="application data for the bin generation",
+    )
+    args = parser.parse_args()
+
+    logging.info(f"Processing {args.elf_file}, app_name='{args.app_name}', app_id={args.app_id:x}")
+    sections = parse_elf_file(args.elf_file)
+    emit_bin_file(args.out_bin, args.app_name, args.app_id, args.stack_page_count, args.heap_page_count, sections)
+
+
+if __name__ == "__main__":
+    main()