blob: 23ea48a099e9e20963a39a45dc5d7e1e4df3be85 [file] [log] [blame]
Philipp Wagner80cc8232020-10-29 16:26:09 +00001#!/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 Wagner80cc8232020-10-29 16:26:09 +00005"""Build software running on OTBN
6
Timothy Trippelefe736b2022-04-20 12:50:40 -07007Each assembly source file is first assembled with otbn_as.py. All resulting
8objects are then linked with otbn_ld.py. The resulting ELF file is converted
9into an embeddable RV32 object file using objcopy. In this object, all symbols
Philipp Wagner80cc8232020-10-29 16:26:09 +000010are prefixed with `_otbn_app_<appname>_` (only global symbols are included).
11
12environment 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 Trippel024e3932022-04-20 15:40:55 -070018 OTBN_TOOLS path to the OTBN linker and assemler tools
Philipp Wagner80cc8232020-10-29 16:26:09 +000019 RV32_TOOL_LD path to RV32 ld
20 RV32_TOOL_AS path to RV32 as
Chris Frantz9b34e4a2021-11-24 17:03:12 -080021 RV32_TOOL_AR path to RV32 ar
Philipp Wagner80cc8232020-10-29 16:26:09 +000022 RV32_TOOL_OBJCOPY path to RV32 objcopy
23
24 The RV32* environment variables are used by both this script and the OTBN
Timothy Trippelefe736b2022-04-20 12:50:40 -070025 wrappers (otbn_as.py and otbn_ld.py) to find tools in a RV32 toolchain.
Philipp Wagner80cc8232020-10-29 16:26:09 +000026
27outputs:
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
36import argparse
37import logging as log
38import os
39import shlex
40import subprocess
41import sys
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +000042import tempfile
Philipp Wagner80cc8232020-10-29 16:26:09 +000043from pathlib import Path
Rupert Swarbrick48b9a952022-01-14 16:20:16 +000044from typing import List, Optional, Tuple
45
46from elftools.elf.elffile import ELFFile, SymbolTableSection # type: ignore
47
Timothy Trippel024e3932022-04-20 15:40:55 -070048# yapf: disable
49
Timothy Trippela7b30e62022-04-20 16:26:09 -070050# TODO: remove with meson; bazel will set the PYTHONPATH to locate otbn tools
Timothy Trippel024e3932022-04-20 15:40:55 -070051otbn_tools_path = os.environ.get('OTBN_TOOLS', None)
52if otbn_tools_path:
53 sys.path.append(otbn_tools_path)
54import otbn_as
Timothy Trippela7b30e62022-04-20 16:26:09 -070055import otbn_ld
Timothy Trippel024e3932022-04-20 15:40:55 -070056
57# yapf: enable
58
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +000059
60def cmd_to_str(cmd: List[str]) -> str:
Philipp Wagner80cc8232020-10-29 16:26:09 +000061 return ' '.join([shlex.quote(str(a)) for a in cmd])
62
63
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +000064def 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 Trippel024e3932022-04-20 15:40:55 -070079def run_tool(tool, out_file: Path, args) -> None:
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +000080 '''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 Trippelffc9e542022-04-20 12:42:50 -070090 tmpfile = tempfile.NamedTemporaryFile(prefix=out_base,
91 dir=out_dir,
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +000092 delete=False)
93 try:
Timothy Trippel024e3932022-04-20 15:40:55 -070094 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 Swarbrickd5aa79a2020-12-14 13:04:21 +000099
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 Wagner80cc8232020-10-29 16:26:09 +0000112
113
114def call_otbn_as(src_file: Path, out_file: Path):
Timothy Trippel024e3932022-04-20 15:40:55 -0700115 run_tool(otbn_as.main, out_file, [src_file])
Philipp Wagner80cc8232020-10-29 16:26:09 +0000116
117
Timothy Trippelffc9e542022-04-20 12:42:50 -0700118def call_otbn_ld(src_files: List[Path], out_file: Path,
119 linker_script: Optional[Path]):
Philipp Wagner6f51ecb2020-11-19 23:05:22 +0000120
Rupert Swarbrick3750b482021-10-26 17:39:44 +0100121 args = ['-gc-sections', '-gc-keep-exported']
Philipp Wagner6f51ecb2020-11-19 23:05:22 +0000122 if linker_script:
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +0000123 args += ['-T', linker_script]
124 args += src_files
Timothy Trippela7b30e62022-04-20 16:26:09 -0700125 run_tool(otbn_ld.main, out_file, args)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000126
127
128def call_rv32_objcopy(args: List[str]):
129 rv32_tool_objcopy = os.environ.get('RV32_TOOL_OBJCOPY',
130 'riscv32-unknown-elf-objcopy')
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +0000131 run_cmd([rv32_tool_objcopy] + args)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000132
133
Chris Frantz9b34e4a2021-11-24 17:03:12 -0800134def call_rv32_ar(args: List[str]):
Timothy Trippelffc9e542022-04-20 12:42:50 -0700135 rv32_tool_ar = os.environ.get('RV32_TOOL_AR', 'riscv32-unknown-elf-ar')
Chris Frantz9b34e4a2021-11-24 17:03:12 -0800136 run_cmd([rv32_tool_ar] + args)
137
138
Rupert Swarbrick48b9a952022-01-14 16:20:16 +0000139def 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 Trippelffc9e542022-04-20 12:42:50 -0700151 call_rv32_objcopy([
152 '-O', 'elf32-littleriscv', '--remove-section=.scratchpad',
153 '--extract-symbol'
154 ] + [elf_path, syms_path])
Rupert Swarbrick48b9a952022-01-14 16:20:16 +0000155
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 Wagner80cc8232020-10-29 16:26:09 +0000176def main() -> int:
Timothy Trippelffc9e542022-04-20 12:42:50 -0700177 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 Wagner6f51ecb2020-11-19 23:05:22 +0000198 parser.add_argument(
Philipp Wagner80cc8232020-10-29 16:26:09 +0000199 '--app-name',
200 '-n',
201 required=False,
202 help="Name of the application, used as basename for the output. "
Timothy Trippelffc9e542022-04-20 12:42:50 -0700203 "Default: basename of the first source file.")
Jade Philipoom39c9b322022-03-08 12:27:10 +0000204 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 Trippelffc9e542022-04-20 12:42:50 -0700210 "files and only linking is required.")
Philipp Wagner80cc8232020-10-29 16:26:09 +0000211 parser.add_argument('src_files', nargs='+', type=str, metavar='SRC_FILE')
212 args = parser.parse_args()
213
Rupert Swarbrick8dd960c2020-12-14 10:26:17 +0000214 log_level = log.INFO if args.verbose else log.WARNING
215 log.basicConfig(level=log_level, format="%(message)s")
216
Philipp Wagner80cc8232020-10-29 16:26:09 +0000217 out_dir = Path(args.out_dir)
Chris Frantz9b34e4a2021-11-24 17:03:12 -0800218 out_dir.mkdir(parents=True, exist_ok=True)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000219
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 Frantz9b34e4a2021-11-24 17:03:12 -0800228 archive = args.archive
Philipp Wagner80cc8232020-10-29 16:26:09 +0000229
230 try:
Jade Philipoom39c9b322022-03-08 12:27:10 +0000231 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 Wagner80cc8232020-10-29 16:26:09 +0000234
235 out_elf = out_dir / (app_name + '.elf')
Timothy Trippelffc9e542022-04-20 12:42:50 -0700236 call_otbn_ld(obj_files, out_elf, linker_script=args.linker_script)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000237
Rupert Swarbrick48b9a952022-01-14 16:20:16 +0000238 # 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 Swarbrick1b0529b2021-02-01 15:08:39 +0000241 #
Rupert Swarbrick48b9a952022-01-14 16:20:16 +0000242 # 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 Wagner80cc8232020-10-29 16:26:09 +0000263 out_embedded_obj = out_dir / (app_name + '.rv32embed.o')
Timothy Trippelffc9e542022-04-20 12:42:50 -0700264 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 Swarbrick48b9a952022-01-14 16:20:16 +0000270 for name, addr in get_otbn_syms(out_elf):
271 args += ['--add-symbol', f'{otbn_side_pfx}{name}=0x{addr:x}']
Philipp Wagner80cc8232020-10-29 16:26:09 +0000272
Rupert Swarbrick48b9a952022-01-14 16:20:16 +0000273 call_rv32_objcopy(args + [out_elf, out_embedded_obj])
Rupert Swarbrick13dc5c32021-02-01 15:17:39 +0000274
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 Frantz9b34e4a2021-11-24 17:03:12 -0800285 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 Wagner80cc8232020-10-29 16:26:09 +0000289 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
298if __name__ == "__main__":
299 sys.exit(main())