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/bundle_app_rmm.py b/app/bundle_app_rmm.py
new file mode 100755
index 0000000..eb1deac
--- /dev/null
+++ b/app/bundle_app_rmm.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# SPDX-FileCopyrightText: Copyright TF-RMM Contributors.
+#
+
+"""
+Script creating a bundle from app binaries and RMM binary
+
+The script prepends the app binaries to the RMM binary, and generates a branch
+instruction at the beginning of the binary file. This way the RMM code can start
+running when the execution reaches the beginning of the bundled binary.
+
+A bundled RMM binary has the following structure:
+```
+    +------------------------------+                          -+
+    |     BL rmm_bin_offset        | Generated by this script  |
+    |                              |                           |
+    | Rest of the header unchanged |                           |
+    |..............................|                           |
+    |                              |                           + app_bin_file_1
+    |      App binary content      |                           |
+    |                              |                           |
+    +------------------------------+                          -+
+    |     Unchanged bin header     |                           |
+    |..............................|                           |
+    |                              |                           + app_bin_file_2
+    |      App binary content      |                           |
+    |                              |                           |
+    +------------------------------+                          -+
+    |                              |                           |
+                  ...                                         ...
+    |                              |                           |
+    +------------------------------+                          -+
+    |     Unchanged bin header     |                           |
+    |..............................|                           |
+    |                              |                           + app_bin_file_n
+    |      App binary content      |                           |
+    |                              |                           |
+    +------------------------------+                          -+
+    |                              |                           |
+    |      RMM binary content      |                           +rmm_bin
+    |                              |                           |
+    +------------------------------+                          -+
+```
+"""
+
+from argparse import ArgumentParser
+import logging
+import struct
+
+logger = None
+
+
+def initial_branch_instruction(offset):
+    """Generate the initial branch instruction to jump to RMM text"""
+    assert offset > 0
+    assert offset % 4 == 0
+    imm = offset // 4
+    assert imm < (1 << 26)  # imm can be at most 25 bits
+    template = 0x94000000
+    # Use struct to make sure that the result is a 4 byte integer in
+    # little-endian byte order
+    return struct.pack("<I", template | imm)
+
+
+def main():
+    """Main function of the script"""
+
+    parser = ArgumentParser(
+        description="Create a bundle from the app and RMM binaries."
+    )
+    parser.add_argument(
+        "app_bin_files",
+        metavar="APP_BIN_FILE",
+        type=str,
+        nargs="+",
+        help="input application data file(s) for bin generation",
+    )
+    parser.add_argument(
+        "--out-bin",
+        metavar="FILE",
+        type=str,
+        required=True,
+        help="the output bin file generated by gen_app_bin.py",
+    )
+    parser.add_argument(
+        "--rmm-bin",
+        metavar="FILE",
+        type=str,
+        required=True,
+        help="the RMM bin input file for bin generation",
+    )
+    parser.add_argument(
+        "--log-file-name",
+        metavar="FILE",
+        type=str,
+        required=False,
+        default="",
+        help="write logs to 'FILE' as well",
+    )
+
+    args = parser.parse_args()
+
+    global logger
+    logger = logging.getLogger()
+    logger.setLevel(logging.DEBUG)
+    fmt = logging.Formatter("%(levelname)s: %(message)s")
+    hdl = logging.StreamHandler()
+    hdl.setFormatter(fmt)
+    logger.addHandler(hdl)
+
+    if args.log_file_name:
+        hdl = logging.FileHandler(args.log_file_name, mode="w")
+        hdl.setFormatter(fmt)
+        logger.addHandler(hdl)
+
+    apps_size = 0
+    app_bin_contents = []
+
+    # Collect the contents of the app bin files and concatenate them in a list.
+    for app_bin_file_name in args.app_bin_files:
+        with open(app_bin_file_name, "rb") as app_bin_file:
+            app_bin_content = app_bin_file.read()
+            apps_size += len(app_bin_content)
+            app_bin_contents.append(app_bin_content)
+
+    # Create the bundled bin file
+    with open(args.out_bin, "wb") as out_file:
+        # Write the starting branch instruction
+        out_file.write(initial_branch_instruction(apps_size))
+        # for the first entry, the Initial branch instruction is added in place
+        # the first 4 bytes of the padding in the app header.
+        start_offset = 4
+        for app_bin_content in app_bin_contents:
+            out_file.write(app_bin_content[start_offset:])
+            # For the rest of the files, write the full header
+            start_offset = 0
+
+        # Add the RMM bin file to the bundle
+        with open(args.rmm_bin, "rb") as rmm_bin_file:
+            out_file.write(rmm_bin_file.read())
+
+    logger.info(
+        f"{args.out_bin} was successfully created. Added {len(args.app_bin_files)} app(s)."
+    )
+    logger.info(f"The offset of the RMM bin is {apps_size} (0x{apps_size:x}) bytes")
+
+
+if __name__ == "__main__":
+    main()
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()
diff --git a/docs/getting_started/getting-started.rst b/docs/getting_started/getting-started.rst
index 040d9ce..1c2b7d1 100644
--- a/docs/getting_started/getting-started.rst
+++ b/docs/getting_started/getting-started.rst
@@ -56,6 +56,7 @@
    "gcovr",">=v4.2","Tools(Coverage analysis)"
    "CBMC",">=5.84.0","Tools(CBMC analysis)"
    "Cppcheck",">=2.14.0","Tools(Cppcheck)"
+   "pyelftools","==0.31","Firmware (EL0 app)"
 
 .. _getting_started_toolchain:
 
@@ -136,10 +137,23 @@
 Install python dependencies
 ###########################
 
+RMM's ``requirements.txt`` file declares additional Python dependencies.
+Install them with ``pip3``:
+
+.. code-block:: bash
+
+    pip3 install --upgrade pip
+    cd <rmm source folder>
+    pip3 install -r requirements.txt
+
+#############################################
+Install python dependencies for Documentation
+#############################################
+
 .. note::
 
-    The installation of Python dependencies is an optional step. This is required only
-    if building documentation.
+    The installation of Python dependencies for documentation is an optional
+    step. This is required only if building documentation.
 
 RMM's ``docs/requirements.txt`` file declares additional Python dependencies.
 Install them with ``pip3``:
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..aa6a238
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,6 @@
+#
+# SPDX-License-Identifier: BSD-3-Clause
+# SPDX-FileCopyrightText: Copyright TF-RMM Contributors.
+#
+
+pyelftools>=0.31
\ No newline at end of file