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 |
| 5 | |
| 6 | """Build software running on OTBN |
| 7 | |
| 8 | Each assembly source file is first assembled with otbn-as. All resulting objects |
| 9 | are then linked with otbn-ld. The resulting ELF file is converted into an |
| 10 | embeddable RV32 object file using objcopy. In this object, all symbols |
| 11 | are prefixed with `_otbn_app_<appname>_` (only global symbols are included). |
| 12 | |
| 13 | environment variables: |
| 14 | This script, and the tools called from it, rely on the following environment |
| 15 | variables for configuration. All environment variables are optional, and |
| 16 | sensible default values are provided (tools are generally expected to be in |
| 17 | the $PATH). |
| 18 | |
| 19 | OTBN_AS path to otbn-as, the OTBN assembler |
| 20 | OTBN_LD path to otbn-ld, the OTBN linker |
| 21 | RV32_TOOL_LD path to RV32 ld |
| 22 | RV32_TOOL_AS path to RV32 as |
| 23 | RV32_TOOL_OBJCOPY path to RV32 objcopy |
| 24 | |
| 25 | The RV32* environment variables are used by both this script and the OTBN |
| 26 | wrappers (otbn-as and otbn-ld) to find tools in a RV32 toolchain. |
| 27 | |
| 28 | outputs: |
| 29 | The build process produces multiple files inside the output directory. |
| 30 | |
| 31 | <src_file>.o the compiled source files |
| 32 | <app_name>.elf the compiled and linked application targeting OTBN |
| 33 | <app_name>.rv32embed.o the application as embeddable object for RV32 |
| 34 | |
| 35 | """ |
| 36 | |
| 37 | import argparse |
| 38 | import logging as log |
| 39 | import os |
| 40 | import shlex |
| 41 | import subprocess |
| 42 | import sys |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 43 | import tempfile |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 44 | from pathlib import Path |
Philipp Wagner | 6f51ecb | 2020-11-19 23:05:22 +0000 | [diff] [blame] | 45 | from typing import List, Optional |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 46 | |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 47 | REPO_TOP = Path(__file__).parent.parent.resolve() |
| 48 | |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 49 | |
| 50 | def cmd_to_str(cmd: List[str]) -> str: |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 51 | return ' '.join([shlex.quote(str(a)) for a in cmd]) |
| 52 | |
| 53 | |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 54 | def run_cmd(args, display_cmd=None): |
| 55 | '''Run the command in args. |
| 56 | |
| 57 | If display_cmd is not None, it should be a string that is printed instead |
| 58 | of the actual arguments that ran (for hiding the details of temporary |
| 59 | files). |
| 60 | |
| 61 | ''' |
| 62 | str_args = [str(a) for a in args] |
| 63 | info_msg = cmd_to_str(str_args) if display_cmd is None else display_cmd |
| 64 | log.info(info_msg) |
| 65 | |
| 66 | subprocess.run(str_args, check=True) |
| 67 | |
| 68 | |
| 69 | def run_tool(tool: str, out_file: Path, args) -> None: |
| 70 | '''Run tool to produce out_file (using an '-o' argument) |
| 71 | |
| 72 | This works by writing to a temporary file (in the same directory) and then |
| 73 | atomically replacing any existing destination file when done. This is |
| 74 | needed if we need to run multiple otbn_build processes that generate the |
| 75 | same files in parallel (a requirement because of our current Meson-based |
| 76 | infrastructure). |
| 77 | |
| 78 | ''' |
| 79 | out_dir, out_base = os.path.split(out_file) |
| 80 | tmpfile = tempfile.NamedTemporaryFile(prefix=out_base, dir=out_dir, |
| 81 | delete=False) |
| 82 | try: |
| 83 | run_cmd([tool, '-o', tmpfile.name] + args, |
| 84 | cmd_to_str([tool, '-o', out_file] + args)) |
| 85 | |
| 86 | # If we get here, the tool ran successfully, producing the output file. |
| 87 | # Use os.replace to rename appropriately. |
| 88 | os.replace(tmpfile.name, out_file) |
| 89 | finally: |
| 90 | # When we're done, or if something went wrong, close and try to delete |
| 91 | # the temporary file. The unlink should fail if the os.replace call |
| 92 | # above succeeded. That's fine. |
| 93 | tmpfile.close() |
| 94 | try: |
| 95 | os.unlink(tmpfile.name) |
| 96 | except FileNotFoundError: |
| 97 | pass |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 98 | |
| 99 | |
| 100 | def call_otbn_as(src_file: Path, out_file: Path): |
| 101 | otbn_as_cmd = os.environ.get('OTBN_AS', |
| 102 | str(REPO_TOP / 'hw/ip/otbn/util/otbn-as')) |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 103 | run_tool(otbn_as_cmd, out_file, [src_file]) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 104 | |
| 105 | |
Philipp Wagner | 6f51ecb | 2020-11-19 23:05:22 +0000 | [diff] [blame] | 106 | def call_otbn_ld(src_files: List[Path], out_file: Path, linker_script: Optional[Path]): |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 107 | otbn_ld_cmd = os.environ.get('OTBN_LD', |
| 108 | str(REPO_TOP / 'hw/ip/otbn/util/otbn-ld')) |
Philipp Wagner | 6f51ecb | 2020-11-19 23:05:22 +0000 | [diff] [blame] | 109 | |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 110 | args = [] |
Philipp Wagner | 6f51ecb | 2020-11-19 23:05:22 +0000 | [diff] [blame] | 111 | if linker_script: |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 112 | args += ['-T', linker_script] |
| 113 | args += src_files |
| 114 | run_tool(otbn_ld_cmd, out_file, args) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 115 | |
| 116 | |
| 117 | def call_rv32_objcopy(args: List[str]): |
| 118 | rv32_tool_objcopy = os.environ.get('RV32_TOOL_OBJCOPY', |
| 119 | 'riscv32-unknown-elf-objcopy') |
Rupert Swarbrick | d5aa79a | 2020-12-14 13:04:21 +0000 | [diff] [blame] | 120 | run_cmd([rv32_tool_objcopy] + args) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 121 | |
| 122 | |
| 123 | def main() -> int: |
| 124 | parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) |
| 125 | parser.add_argument( |
| 126 | '--out-dir', |
| 127 | '-o', |
| 128 | required=False, |
| 129 | default=".", |
| 130 | help="Output directory (default: %(default)s)") |
| 131 | parser.add_argument( |
Rupert Swarbrick | 8dd960c | 2020-12-14 10:26:17 +0000 | [diff] [blame] | 132 | '--verbose', |
| 133 | '-v', |
| 134 | action='store_true', |
| 135 | help='Print commands that are executed.') |
| 136 | parser.add_argument( |
Philipp Wagner | 6f51ecb | 2020-11-19 23:05:22 +0000 | [diff] [blame] | 137 | '--script', |
| 138 | '-T', |
| 139 | dest="linker_script", |
| 140 | required=False, |
| 141 | help="Linker script") |
| 142 | parser.add_argument( |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 143 | '--app-name', |
| 144 | '-n', |
| 145 | required=False, |
| 146 | help="Name of the application, used as basename for the output. " |
| 147 | "Default: basename of the first source file.") |
| 148 | parser.add_argument('src_files', nargs='+', type=str, metavar='SRC_FILE') |
| 149 | args = parser.parse_args() |
| 150 | |
Rupert Swarbrick | 8dd960c | 2020-12-14 10:26:17 +0000 | [diff] [blame] | 151 | log_level = log.INFO if args.verbose else log.WARNING |
| 152 | log.basicConfig(level=log_level, format="%(message)s") |
| 153 | |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 154 | out_dir = Path(args.out_dir) |
| 155 | out_dir.mkdir(exist_ok=True) |
| 156 | |
| 157 | src_files = [Path(f) for f in args.src_files] |
| 158 | for src_file in src_files: |
| 159 | if not src_file.exists(): |
| 160 | log.fatal("Source file %s not found." % src_file) |
| 161 | return 1 |
| 162 | obj_files = [out_dir / f.with_suffix('.o').name for f in src_files] |
| 163 | |
| 164 | app_name = args.app_name or str(src_files[0].stem) |
| 165 | |
| 166 | try: |
| 167 | for src_file, obj_file in zip(src_files, obj_files): |
| 168 | call_otbn_as(src_file, obj_file) |
| 169 | |
| 170 | out_elf = out_dir / (app_name + '.elf') |
Philipp Wagner | 6f51ecb | 2020-11-19 23:05:22 +0000 | [diff] [blame] | 171 | call_otbn_ld(obj_files, out_elf, linker_script = args.linker_script) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 172 | |
Rupert Swarbrick | 1b0529b | 2021-02-01 15:08:39 +0000 | [diff] [blame] | 173 | # Use objcopy to create an ELF that can be linked into a RISC-V binary |
| 174 | # (to run on Ibex). This should set flags for all sections to look like |
| 175 | # rodata (since they're not executable on Ibex, nor does it make sense |
| 176 | # for Ibex code to manipulate OTBN data sections "in place"). We name |
| 177 | # them with a .otbn prefix, so end up with e.g. .rodata.otbn.text and |
| 178 | # .rodata.otbn.data. |
| 179 | # |
| 180 | # Symbols that are exposed by the binary (including those giving the |
| 181 | # start and end of imem and dmem) will be relocated as part of the |
| 182 | # link, so they'll give addresses in the Ibex address space. So that |
| 183 | # the RISC-V binary can link multiple OTBN applications, we give them |
| 184 | # an application-specific prefix. (Note: This prefix is used in |
| 185 | # sw/device/lib/runtime/otbn.h: so needs to be kept in sync with that). |
| 186 | sym_pfx = '_otbn_app_{}_'.format(app_name) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 187 | out_embedded_obj = out_dir / (app_name + '.rv32embed.o') |
Rupert Swarbrick | 1b0529b | 2021-02-01 15:08:39 +0000 | [diff] [blame] | 188 | args = (['-O', 'elf32-littleriscv', |
| 189 | '--prefix-sections=.rodata.otbn', |
| 190 | '--set-section-flags=*=alloc,load,readonly', |
| 191 | '--prefix-symbols', sym_pfx] + |
| 192 | [out_elf, |
| 193 | out_embedded_obj]) |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 194 | |
| 195 | call_rv32_objcopy(args) |
Rupert Swarbrick | 13dc5c3 | 2021-02-01 15:17:39 +0000 | [diff] [blame] | 196 | |
| 197 | # After objcopy has finished, we have to do a little surgery to |
| 198 | # overwrite the ELF e_type field (a 16-bit little-endian number at file |
| 199 | # offset 0x10). It will currently be 0x2 (ET_EXEC), which means a |
| 200 | # fully-linked executable file. Binutils doesn't want to link with |
| 201 | # anything of type ET_EXEC (since it usually wouldn't make any sense to |
| 202 | # do so). Hack the type to be 0x1 (ET_REL), which means an object file. |
| 203 | with open(out_embedded_obj, 'r+b') as emb_file: |
| 204 | emb_file.seek(0x10) |
| 205 | emb_file.write(b'\1\0') |
| 206 | |
Philipp Wagner | 80cc823 | 2020-10-29 16:26:09 +0000 | [diff] [blame] | 207 | except subprocess.CalledProcessError as e: |
| 208 | # Show a nicer error message if any of the called programs fail. |
| 209 | log.fatal("Command {!r} returned non-zero exit code {}".format( |
| 210 | cmd_to_str(e.cmd), e.returncode)) |
| 211 | return 1 |
| 212 | |
| 213 | return 0 |
| 214 | |
| 215 | |
| 216 | if __name__ == "__main__": |
| 217 | sys.exit(main()) |