1#!/usr/bin/env python3
2# SPDX-License-Identifier: BSD-3-Clause
3# SPDX-FileCopyrightText: Copyright TF-RMM Contributors.
4#
5
6"""
7Script for creating a bin file from the compiled app's elf file.
8
9The name of the elf sections that are expected and added in the output binary
10are hardcoded in the script. The resulting binary consists of a header which is
11page in size and content of selected sections which are appended after the
12header.
13```
14+----------------------+
15|                      |
16|       Header         |
17|                      |
18+----------------------+
19|                      |
20| Content of section 1 |
21|                      |
22+----------------------+
23|                      |
24           ...
25|                      |
26+----------------------+
27|                      |
28|Content of section n  |
29|                      |
30+----------------------+
31```
32"""
33
34from argparse import ArgumentParser
35from collections import namedtuple
36import logging
37import struct
38
39
40from elftools.elf.elffile import ELFFile
41
42INDENTATION = "\t"
43APP_HEADER_MAGIC = 0x000E10ABB4EAD000  # El0 APP HEAD
44HEADER_VERSION_MAJOR = 0
45HEADER_VERSION_MINOR = 1
46# TODO: get page size from command line
47PAGE_SIZE = 4096
48APP_HEADER_RESERVED_BYTES = 10 * 8
49APP_NAME_BUF_SIZE = 32
50
51HEADER_VERSION = (HEADER_VERSION_MAJOR << 16) | HEADER_VERSION_MINOR
52
53# The header format needs to match the header in
54# "app/common/framework/include/fake_host/app_header_structures.h" file as defined in
55# 'struct el0_app_header'
56BIN_HEADER_FORMAT = "".join(
57    [
58        "<",  # little endian
59        "Q",  # uint64_t padding;
60        "Q",  # uint64_t app_header_magic;
61        "L",  # uint32_t app_header_version;
62        f"{APP_NAME_BUF_SIZE}s" # const char app_name[APP_NAME_BUF_SIZE];
63        "L",  # uint32_t app_id;
64        "Q",  # uint64_t app_len; /* including header */
65        "Q",  # uintptr_t section_text_offset;
66        "Q",  # uintptr_t section_text_va;
67        "Q",  # size_t section_text_size;
68        "Q",  # uintptr_t section_rodata_offset;
69        "Q",  # uintptr_t section_rodata_va;
70        "Q",  # size_t section_rodata_size;
71        "Q",  # uintptr_t section_data_offset;
72        "Q",  # uintptr_t section_data_va;
73        "Q",  # size_t section_data_size;
74        "Q",  # uintptr_t section_bss_va ;
75        "Q",  # size_t section_bss_size;
76        "Q",  # section_shared_va;
77        "Q",  # size_t stack_page_count;
78        "Q",  # size_t heap_page_count;
79        f"{APP_HEADER_RESERVED_BYTES}s"  # reserved
80        "Q",  # uint64_t app_header_magic2;
81    ]
82)
83
84SectionParams = namedtuple("SectionParams", ["name", "emit"])
85SectionData = namedtuple("SectionData", ["params", "vma", "size", "data"])
86
87
88def get_section(sections, idx, name):
89    """Returns the section in the sections list at the given index.
90
91    The function makes sure that the section at the specified index has the
92    specified name.
93    """
94    if sections[idx].params.name != name:
95        logging.error(
96            f"At idx {idx} section name '{sections[idx].params.name}' doesn't match '{name}'"
97        )
98        assert False
99    return sections[idx]
100
101def get_app_name_bytes(app_name):
102    if len(app_name) >= (APP_NAME_BUF_SIZE):
103        app_name = app_name[:APP_NAME_BUF_SIZE -1]
104    b_appname = app_name.encode()
105    assert(len(b_appname) < APP_NAME_BUF_SIZE)
106    return bytes().join([b_appname, bytes([0] * (APP_NAME_BUF_SIZE - len(b_appname)))])
107
108def emit_bin_file_header(out_bin_file, app_name, app_id, app_len, stack_page_count, heap_page_count, sections):
109    """Emit the bin file header that will be parsed by the RMM code"""
110    text_section = get_section(sections, 0, ".text")
111    rodata_section = get_section(sections, 1, ".rodata")
112    data_section = get_section(sections, 2, ".data")
113    bss_section = get_section(sections, 3, ".bss")
114    shared_section = get_section(sections, 4, ".shared")
115    text_offset = 0
116    rodata_offset = text_offset + text_section.size
117    data_offset = rodata_offset + rodata_section.size
118    header = struct.pack(
119        BIN_HEADER_FORMAT,
120        0,
121        APP_HEADER_MAGIC,
122        HEADER_VERSION,
123        get_app_name_bytes(app_name),
124        app_id,
125        app_len,
126        0,  # text
127        text_section.vma,
128        text_section.size,
129        rodata_offset,  # rodata
130        rodata_section.vma,
131        rodata_section.size,
132        data_offset,  # data
133        data_section.vma,
134        data_section.size,
135        bss_section.vma,  # bss
136        bss_section.size,
137        shared_section.vma,  # shared
138        stack_page_count,  # stack
139        heap_page_count,  # heap
140        bytes(APP_HEADER_RESERVED_BYTES),
141        APP_HEADER_MAGIC,
142    )
143    logging.info(f"Emitting binary header, {len(header)} bytes.")
144    logging.info(
145        f"    app_header_magic: #1={APP_HEADER_MAGIC:016x}, #2={APP_HEADER_MAGIC:016x}"
146    )
147    logging.info(f"    app_name               =   {app_name:16}")
148    logging.info(f"    app_header_version     =   0x{HEADER_VERSION:16x}")
149    logging.info(f"    app_id                 =     {app_id:16}")
150    logging.info(f"    app_len                =   0x{app_len:16x}")
151
152    logging.info("    section  |   offset |               va |     size")
153    logging.info("    ---------|----------|------------------|---------")
154    logging.info(
155        f"    text     | {0:8x} | {text_section.vma:16x} | {text_section.size:8x}"
156    )
157    logging.info(
158        f"    rodata   | {rodata_offset:8x} | {rodata_section.vma:16x} | {rodata_section.size:8x}"
159    )
160    logging.info(
161        f"    data     | {data_offset:8x} | {data_section.vma:16x} | {data_section.size:8x}"
162    )
163    logging.info(
164        f"    bss      |      N/A | {bss_section.vma:16x} | {bss_section.size:8x}"
165    )
166    logging.info(f"    shared   |      N/A | {shared_section.vma:16x} | {PAGE_SIZE:8x}")
167    logging.info(
168        f"    stack    |      N/A |              N/A | {stack_page_count*PAGE_SIZE:8x}"
169    )
170    logging.info(
171        f"    heap     |      N/A |              N/A | {heap_page_count*PAGE_SIZE:8x}"
172    )
173
174    out_bin_file.write(header)
175
176    # emit padding to keep the app binary page aligned
177    assert len(header) < PAGE_SIZE
178    out_bin_file.write(bytearray([0] * (PAGE_SIZE - len(header))))
179
180    return PAGE_SIZE
181
182
183def emit_section_data(out_bin_file, sections):
184    """Emitting content of a section
185
186    Return the number of bytes emitted for this section
187    """
188    bytes_emitted = 0
189    for section in sections:
190        if section.data is not None and section.params.emit:
191            logging.info(
192                f"Emitting section '{section.params.name}', {len(section.data)} bytes."
193            )
194            out_bin_file.write(section.data)
195            bytes_emitted += len(section.data)
196    return bytes_emitted
197
198
199def calc_sections_size(sections):
200    """Calculates the length of the sections part of the bin"""
201    length = PAGE_SIZE  # accounting for header
202    for section in sections:
203        if section.data is not None and section.params.emit:
204            length += len(section.data)
205    if length % PAGE_SIZE != 0:
206        length += PAGE_SIZE - (length % PAGE_SIZE)
207    return length
208
209
210def emit_bin_file(out_bin_file_name, app_name, app_id, stack_page_count, heap_page_count, sections):
211    """Write the bin file"""
212    bytes_emitted = 0
213    with open(out_bin_file_name, "wb") as out_bin_file:
214        # Calculate the length of the bin file payload to be written in to the header
215        app_len = calc_sections_size(sections)
216        # Write the bin header
217        bytes_emitted += emit_bin_file_header(
218            out_bin_file, app_name, app_id, app_len, stack_page_count, heap_page_count, sections
219        )
220        # Write the sections that needs to be emitted
221        bytes_emitted += emit_section_data(out_bin_file, sections)
222
223        # Add padding so that the bin file is aligned to page boundary
224        padding_length = PAGE_SIZE - (((bytes_emitted + PAGE_SIZE - 1) % PAGE_SIZE) + 1)
225        assert (bytes_emitted + padding_length) % PAGE_SIZE == 0
226        assert padding_length < PAGE_SIZE
227        if padding_length:
228            out_bin_file.write(bytearray([0] * padding_length))
229            bytes_emitted += padding_length
230        assert bytes_emitted == app_len
231
232
233def get_sections_data(elffile, sections_params):
234    elf_section_idx = 0
235    section_idx = 0
236    sections_found = []
237
238    expected_section_names = [section.name for section in sections_params]
239
240    # Assume that the interesting sections are in the expected order
241    while elf_section_idx < elffile.num_sections() and section_idx < len(sections_params):
242        elf_section = elffile.get_section(elf_section_idx)
243
244        if elf_section.name not in expected_section_names[section_idx:]:
245            logging.info(
246                f"Skipping section {elf_section.name}, ({elf_section.data_size} bytes)"
247            )
248            elf_section_idx += 1
249            if elf_section_idx == elffile.num_sections():
250                break
251            continue
252
253        section_params = sections_params[section_idx]
254
255        if elf_section.name != section_params.name:
256            logging.info(
257                f"Section {expected_section_names[section_idx]} not found in the elf file"
258            )
259            section_idx += 1
260            continue
261
262        assert elf_section.name == section_params.name
263
264        logging.info(f"Found section {elf_section.name} size={elf_section.data_size}")
265
266        section_data = SectionData(
267            params=section_params,
268            vma=elf_section.header["sh_addr"],
269            size=elf_section.data_size,
270            data=elf_section.data(),
271        )
272        sections_found.append(section_data)
273        assert section_data.size == len(section_data.data)
274
275        elf_section_idx += 1
276        section_idx += 1
277
278    while elf_section_idx < elffile.num_sections():
279        elf_section = elffile.get_section(elf_section_idx)
280        logging.info(
281            f"Skipping section {elf_section.name}, ({elf_section.data_size} bytes)"
282        )
283        elf_section_idx += 1
284
285    while section_idx < len(sections_params):
286        section = sections_params[section_idx]
287        logging.info(f"Section {section.name} not found in the elf file")
288        section_idx += 1
289
290    return sections_found
291
292
293def parse_elf_file(elf_file_name):
294    """parse the elf file
295
296    returns the relevant sections' data found in the file
297    """
298    with open(elf_file_name, "rb") as in_file:
299        sections = [
300            SectionParams(".text", emit=True),
301            SectionParams(".rodata", emit=True),
302            SectionParams(".data", emit=True),
303            SectionParams(".bss", emit=False),
304            SectionParams(".shared", emit=False),
305        ]
306
307        elffile = ELFFile(in_file)
308
309        logging.info(f"{elf_file_name} has {elffile.num_sections()} sections.")
310
311        # Assume that the interesting sections are in the expected order
312        return get_sections_data(elffile, sections)
313
314
315def main():
316    """Main function of the script"""
317    logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.DEBUG)
318
319    parser = ArgumentParser(description="Generate app data files for packaging.")
320    parser.add_argument(
321        "--elf-file", type=str, required=True, help="elf file to generate raw data for"
322    )
323    parser.add_argument(
324        "--app-id",
325        type=lambda v: int(v, 0),
326        required=True,
327        help="The ID of the application used in the RMM code",
328    )
329    parser.add_argument(
330        "--app-name",
331        required=True,
332        help="The name of the app",
333    )
334    parser.add_argument(
335        "--stack-page-count",
336        type=int,
337        required=True,
338        help="The stack size required by the application",
339    )
340    parser.add_argument(
341        "--heap-page-count",
342        type=int,
343        required=True,
344        help="The heap size required by the application (0 is valid)",
345    )
346    parser.add_argument(
347        "--out-bin",
348        type=str,
349        required=True,
350        help="application data for the bin generation",
351    )
352    args = parser.parse_args()
353
354    logging.info(f"Processing {args.elf_file}, app_name='{args.app_name}', app_id={args.app_id:x}")
355    sections = parse_elf_file(args.elf_file)
356    emit_bin_file(args.out_bin, args.app_name, args.app_id, args.stack_page_count, args.heap_page_count, sections)
357
358
359if __name__ == "__main__":
360    main()
361