1#!/usr/bin/env python3
2#
3# Copyright 2020, Data61, CSIRO (ABN 41 687 119 230)
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7"""
8Generate a C header file to standard output with a preprocessor symbol
9definition of the physical load address in hexadecimal of the ELF-loader in
10`payload_filename`.  The address is calculated using the description of memory
11in `platform_filename` and the CPIO archive members embedded in the payload
12file, including the loadable segments of the ELF objects and a possible DTB
13(device tree binary) file.  The ELF-loader is placed in the first (lowest)
14sufficiently-large memory region.
15
16THIS IS NOT A STABLE API.  Use as a script, not a module.
17"""
18
19import argparse
20import io
21import os.path
22import re
23import sys
24
25import libarchive
26
27import elf_sift
28import platform_sift
29
30do_debug = False
31program_name = 'shoehorn'
32
33
34def write(message: str):
35    """
36    Write diagnostic `message` to standard error.
37    """
38    sys.stderr.write('{}: {}\n'.format(program_name, message))
39
40
41def debug(message: str):
42    """
43    Emit debugging diagnostic `message`.
44    """
45    if do_debug:
46        write('debug: {}'.format(message))
47
48
49def debug_marker_set(mark: int, obj: str):
50    debug('setting marker to 0x{:x} ({})'.format(mark, obj))
51
52
53def die(message: str, status: int = 3):
54    """
55    Emit fatal diagnostic `message` and exit with `status` (3 if not specified).
56    """
57    write('fatal error: {}'.format(message))
58    sys.exit(status)
59
60
61def notice(message: str):
62    """
63    Emit notification diagnostic `message`.
64    """
65    write('notice: {}'.format(message))
66
67
68def warn(message: str):
69    """
70    Emit warning diagnostic `message`.
71    """
72    write('warning: {}'.format(message))
73
74
75def get_bytes(entry: libarchive.entry.ArchiveEntry) -> io.BytesIO:
76    """
77    Return an io.BytesIO object with the contents of the given archive entry.
78    """
79    bytes = bytearray()
80    bytes.extend([byte for block in entry.get_blocks() for byte in block])
81    return io.BytesIO(bytes)
82
83
84def get_cpio(payload_filename: str) -> io.BytesIO:
85    """
86    Return an io.BytesIO object containing the CPIO part of `payload_filename`.
87    The payload file is a CPIO archive with an object file header (e.g., an ELF
88    prologue) prepended.  The embedded CPIO archive file is expected to be of
89    the format the `file` command calls an "ASCII cpio archive (SVR4 with no
90    CRC)".
91
92    We assume that the CPIO "magic number" is not a valid sequence inside the
93    object file header of `payload_filename`.
94    """
95    cpio_magic = b'070701'
96
97    with open(payload_filename, 'rb') as payload:
98        match = re.search(cpio_magic, payload.read())
99
100        if match:
101            debug('found CPIO identifying sequence {} at offset 0x{:x} in {}'
102                  .format(cpio_magic, match.start(), payload_filename))
103            payload.seek(match.start())
104            cpio_bytes = payload.read()
105        else:
106            warn('did not find the CPIO identifying sequence {} expected in {}'
107                 .format(cpio_magic, header_size, payload_filename))
108            cpio_bytes = None
109
110    return cpio_bytes
111
112
113def main() -> int:
114    parser = argparse.ArgumentParser(
115        formatter_class=argparse.RawDescriptionHelpFormatter,
116        description="""
117Generate a C header file to standard output with a preprocessor symbol
118definition of the physical load address in hexadecimal of the ELF-loader in
119`payload_filename`.  The address is calculated using the description of memory
120in `platform_filename` and the CPIO archive members embedded in the payload
121file, including the loadable segments of the ELF objects and a possible DTB
122(device tree binary) file.  The ELF-loader is placed in the first (lowest)
123sufficiently-large memory region.
124""")
125    parser.add_argument('--load-rootservers-high', dest='load_rootservers_high',
126                        default=False, action='store_true',
127                        help='assume ELF-loader will put rootservers at top of'
128                             ' memory')
129    parser.add_argument('platform_filename', nargs=1, type=str,
130                        help='YAML description of platform parameters (e.g.,'
131                             ' platform_gen.yaml)')
132    parser.add_argument('payload_filename', nargs=1, type=str,
133                        help='ELF-loader image file (e.g., archive.o)')
134
135    # Set up some simpler names for argument data and derived information.
136    args = parser.parse_args()
137    image = args.payload_filename[0]
138    image_size = os.path.getsize(image)
139    do_load_rootservers_high = args.load_rootservers_high
140    platform = platform_sift.load_data(args.platform_filename[0])
141
142    rootservers = []
143    is_dtb_present = False
144    is_good_fit = False
145
146    with libarchive.memory_reader(get_cpio(image)) as archive:
147        for entry in archive:
148            name = entry.name
149            debug('encountered CPIO entry name: {}'.format(name))
150
151            if name == 'kernel.elf':
152                kernel_elf = get_bytes(entry)
153            elif name == 'kernel.dtb':
154                # The ELF-loader loads the entire DTB into memory.
155                is_dtb_present = True
156                dtb_size = entry.size
157            elif name.endswith('.bin'):
158                # Skip checksum entries.
159                notice('skipping checkum entry "{}"'.format(name))
160            else:
161                rootservers.append(get_bytes(entry))
162
163    # Enumerate the regions as we encounter them for diagnostic purposes.
164    region_counter = -1
165    last_region = len(platform['memory'])
166
167    for region in platform['memory']:
168        region_counter += 1
169        marker = region['start']
170        debug_marker_set(marker, 'region {} start'.format(region_counter))
171        # Note: We assume that the kernel is loaded at the start of memory
172        # because that is what elfloader-tool/src/arch-arm/linker.lds ensures
173        # will happen.  This assumption may change in the future!
174        kernel_start = region['start']
175        kernel_size = elf_sift.get_memory_usage(kernel_elf, align=True)
176        kernel_end = elf_sift.get_aligned_size(kernel_start + kernel_size)
177        marker = kernel_end
178        debug_marker_set(marker, 'kernel_end')
179
180        if is_dtb_present:
181            dtb_start = marker
182            dtb_end = elf_sift.get_aligned_size(dtb_start + dtb_size)
183            marker = dtb_end
184            debug_marker_set(marker, 'dtb_end')
185
186        if do_load_rootservers_high and (region_counter == last_region):
187            warn('"--load-rootservers-high" specified but placing'
188                 ' ELF-loader in last (or only) region ({} of {}); overlap'
189                 ' may not be detected by this tool'
190                 .format(region_counter, last_region))
191
192        # Deal with the 1..(# of CPUs - 1) possible user payloads, if we're not
193        # loading them in high memory, discontiguously with the kernel.
194        #
195        # TODO: Handle this differently (skipping, or checking to see if we had
196        # to push the kernel so high that it whacks the user payloads--but the
197        # ELF-loader itself should detect that case).  At present the case of
198        # multiple rootservers is difficult to debug because it is not
199        # implemented on the archive-construction side; see JIRA SELFOUR-2368.
200        if not do_load_rootservers_high:
201            for elf in rootservers:
202                marker += elf_sift.get_memory_usage(elf, align=True)
203                debug_marker_set(marker, 'end of rootserver')
204
205            # Note: sel4test_driver eats (almost) 4 more MiB than it claims to.
206            # Fixing this is JIRA SELFOUR-2335.
207            fudge_factor = 4 * 1024 * 1024
208            marker += elf_sift.get_aligned_size(fudge_factor)
209            debug_marker_set(marker, 'end of (aligned) fudge factor')
210
211        image_start_address = marker
212
213        if (image_start_address + image_size) <= region['end']:
214            is_good_fit = True
215            break
216
217    if not is_good_fit:
218        die('ELF-loader image "{image}" of size 0x{size:x} does not fit within'
219            ' any memory region described in "{yaml}"'
220            .format(image=image, size=image_size, yaml=platform), status=1)
221
222    sys.stdout.write('#define IMAGE_START_ADDR 0x{load:x}\n'
223                     .format(load=image_start_address))
224    return 0
225
226
227if __name__ == '__main__':
228    sys.exit(main())
229