Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # Copyright lowRISC contributors. |
| 3 | # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| 4 | # SPDX-License-Identifier: Apache-2.0 |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 5 | """Build software running on OTBN |
| 6 | |
Timothy Trippel | efe736b | 2022-04-20 12:50:40 -0700 | [diff] [blame] | 7 | Each assembly source file is first assembled with otbn_as.py. All resulting |
| 8 | objects are then linked with otbn_ld.py. The resulting ELF file is converted |
| 9 | into an embeddable RV32 object file using objcopy. In this object, all symbols |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 10 | are prefixed with `_otbn_app_<appname>_` (only global symbols are included). |
| 11 | |
| 12 | environment variables: |
| 13 | This script, and the tools called from it, rely on the following environment |
| 14 | variables for configuration. All environment variables are optional, and |
| 15 | sensible default values are provided (tools are generally expected to be in |
| 16 | the $PATH). |
| 17 | |
Timothy Trippel | 024e393 | 2022-04-20 15:40:55 -0700 | [diff] [blame] | 18 | OTBN_TOOLS path to the OTBN linker and assemler tools |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 19 | RV32_TOOL_LD path to RV32 ld |
| 20 | RV32_TOOL_AS path to RV32 as |
Chris Frantz | 9b34e4a | 2021-11-24 17:03:12 -0800 | [diff] [blame] | 21 | RV32_TOOL_AR path to RV32 ar |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 22 | RV32_TOOL_OBJCOPY path to RV32 objcopy |
| 23 | |
| 24 | The RV32* environment variables are used by both this script and the OTBN |
Timothy Trippel | efe736b | 2022-04-20 12:50:40 -0700 | [diff] [blame] | 25 | wrappers (otbn_as.py and otbn_ld.py) to find tools in a RV32 toolchain. |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 26 | |
| 27 | outputs: |
| 28 | The build process produces multiple files inside the output directory. |
| 29 | |
| 30 | <src_file>.o the compiled source files |
| 31 | <app_name>.elf the compiled and linked application targeting OTBN |
| 32 | <app_name>.rv32embed.o the application as embeddable object for RV32 |
| 33 | |
| 34 | """ |
| 35 | |
| 36 | import argparse |
| 37 | import logging as log |
| 38 | import os |
| 39 | import shlex |
| 40 | import subprocess |
| 41 | import sys |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 42 | import tempfile |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 43 | from pathlib import Path |
Rupert Swarbrick | 48b9a95 | 2022-01-14 16:20:16 +0000 | [diff] [blame] | 44 | from typing import List, Optional, Tuple |
| 45 | |
| 46 | from elftools.elf.elffile import ELFFile, SymbolTableSection # type: ignore |
| 47 | |
Timothy Trippel | 024e393 | 2022-04-20 15:40:55 -0700 | [diff] [blame] | 48 | # yapf: disable |
| 49 | |
Timothy Trippel | a7b30e6 | 2022-04-20 16:26:09 -0700 | [diff] [blame] | 50 | # TODO: remove with meson; bazel will set the PYTHONPATH to locate otbn tools |
Timothy Trippel | 024e393 | 2022-04-20 15:40:55 -0700 | [diff] [blame] | 51 | otbn_tools_path = os.environ.get('OTBN_TOOLS', None) |
| 52 | if otbn_tools_path: |
| 53 | sys.path.append(otbn_tools_path) |
| 54 | import otbn_as |
Timothy Trippel | a7b30e6 | 2022-04-20 16:26:09 -0700 | [diff] [blame] | 55 | import otbn_ld |
Timothy Trippel | 024e393 | 2022-04-20 15:40:55 -0700 | [diff] [blame] | 56 | |
| 57 | # yapf: enable |
| 58 | |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 59 | |
| 60 | def cmd_to_str(cmd: List[str]) -> str: |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 61 | return ' '.join([shlex.quote(str(a)) for a in cmd]) |
| 62 | |
| 63 | |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 64 | def run_cmd(args, display_cmd=None): |
| 65 | '''Run the command in args. |
| 66 | |
| 67 | If display_cmd is not None, it should be a string that is printed instead |
| 68 | of the actual arguments that ran (for hiding the details of temporary |
| 69 | files). |
| 70 | |
| 71 | ''' |
| 72 | str_args = [str(a) for a in args] |
| 73 | info_msg = cmd_to_str(str_args) if display_cmd is None else display_cmd |
| 74 | log.info(info_msg) |
| 75 | |
| 76 | subprocess.run(str_args, check=True) |
| 77 | |
| 78 | |
Timothy Trippel | 024e393 | 2022-04-20 15:40:55 -0700 | [diff] [blame] | 79 | def run_tool(tool, out_file: Path, args) -> None: |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 80 | '''Run tool to produce out_file (using an '-o' argument) |
| 81 | |
| 82 | This works by writing to a temporary file (in the same directory) and then |
| 83 | atomically replacing any existing destination file when done. This is |
| 84 | needed if we need to run multiple otbn_build processes that generate the |
| 85 | same files in parallel (a requirement because of our current Meson-based |
| 86 | infrastructure). |
| 87 | |
| 88 | ''' |
| 89 | out_dir, out_base = os.path.split(out_file) |
Timothy Trippel | ffc9e54 | 2022-04-20 12:42:50 -0700 | [diff] [blame] | 90 | tmpfile = tempfile.NamedTemporaryFile(prefix=out_base, |
| 91 | dir=out_dir, |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 92 | delete=False) |
| 93 | try: |
Timothy Trippel | 024e393 | 2022-04-20 15:40:55 -0700 | [diff] [blame] | 94 | if type(tool) == str: |
| 95 | run_cmd([tool, '-o', tmpfile.name] + args, |
| 96 | cmd_to_str([tool, '-o', out_file] + args)) |
| 97 | else: |
| 98 | tool(['', '-o', tmpfile.name] + list(map(str, args))) |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 99 | |
| 100 | # If we get here, the tool ran successfully, producing the output file. |
| 101 | # Use os.replace to rename appropriately. |
| 102 | os.replace(tmpfile.name, out_file) |
| 103 | finally: |
| 104 | # When we're done, or if something went wrong, close and try to delete |
| 105 | # the temporary file. The unlink should fail if the os.replace call |
| 106 | # above succeeded. That's fine. |
| 107 | tmpfile.close() |
| 108 | try: |
| 109 | os.unlink(tmpfile.name) |
| 110 | except FileNotFoundError: |
| 111 | pass |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 112 | |
| 113 | |
| 114 | def call_otbn_as(src_file: Path, out_file: Path): |
Timothy Trippel | 024e393 | 2022-04-20 15:40:55 -0700 | [diff] [blame] | 115 | run_tool(otbn_as.main, out_file, [src_file]) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 116 | |
| 117 | |
Timothy Trippel | ffc9e54 | 2022-04-20 12:42:50 -0700 | [diff] [blame] | 118 | def call_otbn_ld(src_files: List[Path], out_file: Path, |
| 119 | linker_script: Optional[Path]): |
Philipp Wagner | 6f51ecb | 2020-11-19 23:05:22 +0000 | [diff] [blame] | 120 | |
Rupert Swarbrick | 3750b48 | 2021-10-26 17:39:44 +0100 | [diff] [blame] | 121 | args = ['-gc-sections', '-gc-keep-exported'] |
Philipp Wagner | 6f51ecb | 2020-11-19 23:05:22 +0000 | [diff] [blame] | 122 | if linker_script: |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 123 | args += ['-T', linker_script] |
| 124 | args += src_files |
Timothy Trippel | a7b30e6 | 2022-04-20 16:26:09 -0700 | [diff] [blame] | 125 | run_tool(otbn_ld.main, out_file, args) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 126 | |
| 127 | |
| 128 | def call_rv32_objcopy(args: List[str]): |
| 129 | rv32_tool_objcopy = os.environ.get('RV32_TOOL_OBJCOPY', |
| 130 | 'riscv32-unknown-elf-objcopy') |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 131 | run_cmd([rv32_tool_objcopy] + args) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 132 | |
| 133 | |
Chris Frantz | 9b34e4a | 2021-11-24 17:03:12 -0800 | [diff] [blame] | 134 | def call_rv32_ar(args: List[str]): |
Timothy Trippel | ffc9e54 | 2022-04-20 12:42:50 -0700 | [diff] [blame] | 135 | rv32_tool_ar = os.environ.get('RV32_TOOL_AR', 'riscv32-unknown-elf-ar') |
Chris Frantz | 9b34e4a | 2021-11-24 17:03:12 -0800 | [diff] [blame] | 136 | run_cmd([rv32_tool_ar] + args) |
| 137 | |
| 138 | |
Rupert Swarbrick | 48b9a95 | 2022-01-14 16:20:16 +0000 | [diff] [blame] | 139 | def get_otbn_syms(elf_path: str) -> List[Tuple[str, int]]: |
| 140 | '''Get externally-visible symbols from an ELF |
| 141 | |
| 142 | Symbols are returned as a list of triples: (name, address). This |
| 143 | discards locals and also anything in .scratchpad, since those addresses |
| 144 | aren't bus-accessible. |
| 145 | ''' |
| 146 | with tempfile.TemporaryDirectory() as tmpdir: |
| 147 | # First, run objcopy to discard local symbols and the .scratchpad |
| 148 | # section. We also use --extract-symbol since we don't care about |
| 149 | # anything but the symbol data anyway. |
| 150 | syms_path = os.path.join(tmpdir, 'syms.elf') |
Timothy Trippel | ffc9e54 | 2022-04-20 12:42:50 -0700 | [diff] [blame] | 151 | call_rv32_objcopy([ |
| 152 | '-O', 'elf32-littleriscv', '--remove-section=.scratchpad', |
| 153 | '--extract-symbol' |
| 154 | ] + [elf_path, syms_path]) |
Rupert Swarbrick | 48b9a95 | 2022-01-14 16:20:16 +0000 | [diff] [blame] | 155 | |
| 156 | # Load the file and use elftools to grab any symbol table |
| 157 | with open(syms_path, 'rb') as syms_fd: |
| 158 | syms_file = ELFFile(syms_fd) |
| 159 | symtab = syms_file.get_section_by_name('.symtab') |
| 160 | if symtab is None or not isinstance(symtab, SymbolTableSection): |
| 161 | # No symbol table found or we did find a section called |
| 162 | # .symtab, but it isn't actually a symbol table (huh?!). Give |
| 163 | # up. |
| 164 | return [] |
| 165 | |
| 166 | ret = [] |
| 167 | for sym in symtab.iter_symbols(): |
| 168 | if sym['st_info']['bind'] != 'STB_GLOBAL': |
| 169 | continue |
| 170 | addr = sym['st_value'] |
| 171 | assert isinstance(addr, int) |
| 172 | ret.append((sym.name, addr)) |
| 173 | return ret |
| 174 | |
| 175 | |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 176 | def main() -> int: |
Timothy Trippel | ffc9e54 | 2022-04-20 12:42:50 -0700 | [diff] [blame] | 177 | parser = argparse.ArgumentParser( |
| 178 | description=__doc__, |
| 179 | formatter_class=argparse.RawDescriptionHelpFormatter) |
| 180 | parser.add_argument('--out-dir', |
| 181 | '-o', |
| 182 | required=False, |
| 183 | default=".", |
| 184 | help="Output directory (default: %(default)s)") |
| 185 | parser.add_argument('--archive', |
| 186 | '-a', |
| 187 | action='store_true', |
| 188 | help='Archive the rv32embed.o file into a library.') |
| 189 | parser.add_argument('--verbose', |
| 190 | '-v', |
| 191 | action='store_true', |
| 192 | help='Print commands that are executed.') |
| 193 | parser.add_argument('--script', |
| 194 | '-T', |
| 195 | dest="linker_script", |
| 196 | required=False, |
| 197 | help="Linker script") |
Philipp Wagner | 6f51ecb | 2020-11-19 23:05:22 +0000 | [diff] [blame] | 198 | parser.add_argument( |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 199 | '--app-name', |
| 200 | '-n', |
| 201 | required=False, |
| 202 | help="Name of the application, used as basename for the output. " |
Timothy Trippel | ffc9e54 | 2022-04-20 12:42:50 -0700 | [diff] [blame] | 203 | "Default: basename of the first source file.") |
Jade Philipoom | 39c9b32 | 2022-03-08 12:27:10 +0000 | [diff] [blame] | 204 | parser.add_argument( |
| 205 | '--no-assembler', |
| 206 | '-x', |
| 207 | action='store_true', |
| 208 | required=False, |
| 209 | help="Use when input files have already been assembled into object " |
Timothy Trippel | ffc9e54 | 2022-04-20 12:42:50 -0700 | [diff] [blame] | 210 | "files and only linking is required.") |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 211 | parser.add_argument('src_files', nargs='+', type=str, metavar='SRC_FILE') |
| 212 | args = parser.parse_args() |
| 213 | |
Rupert Swarbrick | 8dd960c | 2020-12-14 10:26:17 +0000 | [diff] [blame] | 214 | log_level = log.INFO if args.verbose else log.WARNING |
| 215 | log.basicConfig(level=log_level, format="%(message)s") |
| 216 | |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 217 | out_dir = Path(args.out_dir) |
Chris Frantz | 9b34e4a | 2021-11-24 17:03:12 -0800 | [diff] [blame] | 218 | out_dir.mkdir(parents=True, exist_ok=True) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 219 | |
| 220 | src_files = [Path(f) for f in args.src_files] |
| 221 | for src_file in src_files: |
| 222 | if not src_file.exists(): |
| 223 | log.fatal("Source file %s not found." % src_file) |
| 224 | return 1 |
| 225 | obj_files = [out_dir / f.with_suffix('.o').name for f in src_files] |
| 226 | |
| 227 | app_name = args.app_name or str(src_files[0].stem) |
Chris Frantz | 9b34e4a | 2021-11-24 17:03:12 -0800 | [diff] [blame] | 228 | archive = args.archive |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 229 | |
| 230 | try: |
Jade Philipoom | 39c9b32 | 2022-03-08 12:27:10 +0000 | [diff] [blame] | 231 | if not args.no_assembler: |
| 232 | for src_file, obj_file in zip(src_files, obj_files): |
| 233 | call_otbn_as(src_file, obj_file) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 234 | |
| 235 | out_elf = out_dir / (app_name + '.elf') |
Timothy Trippel | ffc9e54 | 2022-04-20 12:42:50 -0700 | [diff] [blame] | 236 | call_otbn_ld(obj_files, out_elf, linker_script=args.linker_script) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 237 | |
Rupert Swarbrick | 48b9a95 | 2022-01-14 16:20:16 +0000 | [diff] [blame] | 238 | # out_elf is a fully-linked OTBN binary, but we want to be able to use |
| 239 | # it from Ibex, the host processor. To make this work, we generate an |
| 240 | # ELF file that can be linked into the Ibex image. |
Rupert Swarbrick | 1b0529b | 2021-02-01 15:08:39 +0000 | [diff] [blame] | 241 | # |
Rupert Swarbrick | 48b9a95 | 2022-01-14 16:20:16 +0000 | [diff] [blame] | 242 | # This ELF contains all initialised data (the .text and .data |
| 243 | # sections). We change the flags to treat them like rodata (since |
| 244 | # they're not executable on Ibex, nor does it make sense for Ibex code |
| 245 | # to manipulate OTBN data sections "in place") and add a .rodata.otbn |
| 246 | # prefix to the section names. |
| 247 | # |
| 248 | # The symbols exposed by the binary will be relocated as part of the |
| 249 | # link, so they'll point into the Ibex address space. To allow linking |
| 250 | # against multiple OTBN applications, we give the symbols an |
| 251 | # application-specific prefix. (Note: This prefix is used in driver |
| 252 | # code: so needs to be kept in sync with that). |
| 253 | # |
| 254 | # As well as the initialised data and relocated symbols, we also want |
| 255 | # to add (absolute) symbols that have the OTBN addresses of the symbols |
| 256 | # in question. Unfortunately, objcopy doesn't seem to have a "make all |
| 257 | # symbols absolute" command, so we have to do it by hand. This also |
| 258 | # means constructing an enormous objcopy command line :-/ If we run out |
| 259 | # of space, we might have to use elftools to inject the addresses after |
| 260 | # the objcopy. |
| 261 | host_side_pfx = '_otbn_local_app_{}_'.format(app_name) |
| 262 | otbn_side_pfx = '_otbn_remote_app_{}_'.format(app_name) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 263 | out_embedded_obj = out_dir / (app_name + '.rv32embed.o') |
Timothy Trippel | ffc9e54 | 2022-04-20 12:42:50 -0700 | [diff] [blame] | 264 | args = [ |
| 265 | '-O', 'elf32-littleriscv', |
| 266 | '--set-section-flags=*=alloc,load,readonly', |
| 267 | '--remove-section=.scratchpad', '--remove-section=.bss', |
| 268 | '--prefix-sections=.rodata.otbn', '--prefix-symbols', host_side_pfx |
| 269 | ] |
Rupert Swarbrick | 48b9a95 | 2022-01-14 16:20:16 +0000 | [diff] [blame] | 270 | for name, addr in get_otbn_syms(out_elf): |
| 271 | args += ['--add-symbol', f'{otbn_side_pfx}{name}=0x{addr:x}'] |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 272 | |
Rupert Swarbrick | 48b9a95 | 2022-01-14 16:20:16 +0000 | [diff] [blame] | 273 | call_rv32_objcopy(args + [out_elf, out_embedded_obj]) |
Rupert Swarbrick | 13dc5c3 | 2021-02-01 15:17:39 +0000 | [diff] [blame] | 274 | |
| 275 | # After objcopy has finished, we have to do a little surgery to |
| 276 | # overwrite the ELF e_type field (a 16-bit little-endian number at file |
| 277 | # offset 0x10). It will currently be 0x2 (ET_EXEC), which means a |
| 278 | # fully-linked executable file. Binutils doesn't want to link with |
| 279 | # anything of type ET_EXEC (since it usually wouldn't make any sense to |
| 280 | # do so). Hack the type to be 0x1 (ET_REL), which means an object file. |
| 281 | with open(out_embedded_obj, 'r+b') as emb_file: |
| 282 | emb_file.seek(0x10) |
| 283 | emb_file.write(b'\1\0') |
| 284 | |
Chris Frantz | 9b34e4a | 2021-11-24 17:03:12 -0800 | [diff] [blame] | 285 | if archive: |
| 286 | out_embedded_a = out_dir / (app_name + '.rv32embed.a') |
| 287 | call_rv32_ar(['rcs', out_embedded_a, out_embedded_obj]) |
| 288 | |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 289 | except subprocess.CalledProcessError as e: |
| 290 | # Show a nicer error message if any of the called programs fail. |
| 291 | log.fatal("Command {!r} returned non-zero exit code {}".format( |
| 292 | cmd_to_str(e.cmd), e.returncode)) |
| 293 | return 1 |
| 294 | |
| 295 | return 0 |
| 296 | |
| 297 | |
| 298 | if __name__ == "__main__": |
| 299 | sys.exit(main()) |