blob: d88bf357ad5069630fb116eec8e96e45b840b776 [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
5
6"""Build software running on OTBN
7
8Each assembly source file is first assembled with otbn-as. All resulting objects
9are then linked with otbn-ld. The resulting ELF file is converted into an
10embeddable RV32 object file using objcopy. In this object, all symbols
11are prefixed with `_otbn_app_<appname>_` (only global symbols are included).
12
13environment 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
Chris Frantz9b34e4a2021-11-24 17:03:12 -080023 RV32_TOOL_AR path to RV32 ar
Philipp Wagner80cc8232020-10-29 16:26:09 +000024 RV32_TOOL_OBJCOPY path to RV32 objcopy
25
26 The RV32* environment variables are used by both this script and the OTBN
27 wrappers (otbn-as and otbn-ld) to find tools in a RV32 toolchain.
28
29outputs:
30 The build process produces multiple files inside the output directory.
31
32 <src_file>.o the compiled source files
33 <app_name>.elf the compiled and linked application targeting OTBN
34 <app_name>.rv32embed.o the application as embeddable object for RV32
35
36"""
37
38import argparse
39import logging as log
40import os
41import shlex
42import subprocess
43import sys
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +000044import tempfile
Philipp Wagner80cc8232020-10-29 16:26:09 +000045from pathlib import Path
Rupert Swarbrick48b9a952022-01-14 16:20:16 +000046from typing import List, Optional, Tuple
47
48from elftools.elf.elffile import ELFFile, SymbolTableSection # type: ignore
49
Philipp Wagner80cc8232020-10-29 16:26:09 +000050
Philipp Wagner80cc8232020-10-29 16:26:09 +000051REPO_TOP = Path(__file__).parent.parent.resolve()
52
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +000053
54def cmd_to_str(cmd: List[str]) -> str:
Philipp Wagner80cc8232020-10-29 16:26:09 +000055 return ' '.join([shlex.quote(str(a)) for a in cmd])
56
57
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +000058def run_cmd(args, display_cmd=None):
59 '''Run the command in args.
60
61 If display_cmd is not None, it should be a string that is printed instead
62 of the actual arguments that ran (for hiding the details of temporary
63 files).
64
65 '''
66 str_args = [str(a) for a in args]
67 info_msg = cmd_to_str(str_args) if display_cmd is None else display_cmd
68 log.info(info_msg)
69
70 subprocess.run(str_args, check=True)
71
72
73def run_tool(tool: str, out_file: Path, args) -> None:
74 '''Run tool to produce out_file (using an '-o' argument)
75
76 This works by writing to a temporary file (in the same directory) and then
77 atomically replacing any existing destination file when done. This is
78 needed if we need to run multiple otbn_build processes that generate the
79 same files in parallel (a requirement because of our current Meson-based
80 infrastructure).
81
82 '''
83 out_dir, out_base = os.path.split(out_file)
84 tmpfile = tempfile.NamedTemporaryFile(prefix=out_base, dir=out_dir,
85 delete=False)
86 try:
87 run_cmd([tool, '-o', tmpfile.name] + args,
88 cmd_to_str([tool, '-o', out_file] + args))
89
90 # If we get here, the tool ran successfully, producing the output file.
91 # Use os.replace to rename appropriately.
92 os.replace(tmpfile.name, out_file)
93 finally:
94 # When we're done, or if something went wrong, close and try to delete
95 # the temporary file. The unlink should fail if the os.replace call
96 # above succeeded. That's fine.
97 tmpfile.close()
98 try:
99 os.unlink(tmpfile.name)
100 except FileNotFoundError:
101 pass
Philipp Wagner80cc8232020-10-29 16:26:09 +0000102
103
104def call_otbn_as(src_file: Path, out_file: Path):
105 otbn_as_cmd = os.environ.get('OTBN_AS',
106 str(REPO_TOP / 'hw/ip/otbn/util/otbn-as'))
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +0000107 run_tool(otbn_as_cmd, out_file, [src_file])
Philipp Wagner80cc8232020-10-29 16:26:09 +0000108
109
Philipp Wagner6f51ecb2020-11-19 23:05:22 +0000110def call_otbn_ld(src_files: List[Path], out_file: Path, linker_script: Optional[Path]):
Philipp Wagner80cc8232020-10-29 16:26:09 +0000111 otbn_ld_cmd = os.environ.get('OTBN_LD',
112 str(REPO_TOP / 'hw/ip/otbn/util/otbn-ld'))
Philipp Wagner6f51ecb2020-11-19 23:05:22 +0000113
Rupert Swarbrick3750b482021-10-26 17:39:44 +0100114 args = ['-gc-sections', '-gc-keep-exported']
Philipp Wagner6f51ecb2020-11-19 23:05:22 +0000115 if linker_script:
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +0000116 args += ['-T', linker_script]
117 args += src_files
118 run_tool(otbn_ld_cmd, out_file, args)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000119
120
121def call_rv32_objcopy(args: List[str]):
122 rv32_tool_objcopy = os.environ.get('RV32_TOOL_OBJCOPY',
123 'riscv32-unknown-elf-objcopy')
Rupert Swarbrickd5aa79a2020-12-14 13:04:21 +0000124 run_cmd([rv32_tool_objcopy] + args)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000125
126
Chris Frantz9b34e4a2021-11-24 17:03:12 -0800127def call_rv32_ar(args: List[str]):
128 rv32_tool_ar = os.environ.get('RV32_TOOL_AR',
129 'riscv32-unknown-elf-ar')
130 run_cmd([rv32_tool_ar] + args)
131
132
Rupert Swarbrick48b9a952022-01-14 16:20:16 +0000133def get_otbn_syms(elf_path: str) -> List[Tuple[str, int]]:
134 '''Get externally-visible symbols from an ELF
135
136 Symbols are returned as a list of triples: (name, address). This
137 discards locals and also anything in .scratchpad, since those addresses
138 aren't bus-accessible.
139 '''
140 with tempfile.TemporaryDirectory() as tmpdir:
141 # First, run objcopy to discard local symbols and the .scratchpad
142 # section. We also use --extract-symbol since we don't care about
143 # anything but the symbol data anyway.
144 syms_path = os.path.join(tmpdir, 'syms.elf')
145 call_rv32_objcopy(['-O', 'elf32-littleriscv',
146 '--remove-section=.scratchpad',
147 '--extract-symbol'] +
148 [elf_path, syms_path])
149
150 # Load the file and use elftools to grab any symbol table
151 with open(syms_path, 'rb') as syms_fd:
152 syms_file = ELFFile(syms_fd)
153 symtab = syms_file.get_section_by_name('.symtab')
154 if symtab is None or not isinstance(symtab, SymbolTableSection):
155 # No symbol table found or we did find a section called
156 # .symtab, but it isn't actually a symbol table (huh?!). Give
157 # up.
158 return []
159
160 ret = []
161 for sym in symtab.iter_symbols():
162 if sym['st_info']['bind'] != 'STB_GLOBAL':
163 continue
164 addr = sym['st_value']
165 assert isinstance(addr, int)
166 ret.append((sym.name, addr))
167 return ret
168
169
Philipp Wagner80cc8232020-10-29 16:26:09 +0000170def main() -> int:
Chris Frantz9b34e4a2021-11-24 17:03:12 -0800171 parser = argparse.ArgumentParser(description=__doc__,
172 formatter_class=argparse.RawDescriptionHelpFormatter)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000173 parser.add_argument(
174 '--out-dir',
175 '-o',
176 required=False,
177 default=".",
178 help="Output directory (default: %(default)s)")
179 parser.add_argument(
Chris Frantz9b34e4a2021-11-24 17:03:12 -0800180 '--archive',
181 '-a',
182 action='store_true',
183 help='Archive the rv32embed.o file into a library.')
184 parser.add_argument(
Rupert Swarbrick8dd960c2020-12-14 10:26:17 +0000185 '--verbose',
186 '-v',
187 action='store_true',
188 help='Print commands that are executed.')
189 parser.add_argument(
Philipp Wagner6f51ecb2020-11-19 23:05:22 +0000190 '--script',
191 '-T',
192 dest="linker_script",
193 required=False,
194 help="Linker script")
195 parser.add_argument(
Philipp Wagner80cc8232020-10-29 16:26:09 +0000196 '--app-name',
197 '-n',
198 required=False,
199 help="Name of the application, used as basename for the output. "
200 "Default: basename of the first source file.")
Jade Philipoom39c9b322022-03-08 12:27:10 +0000201 parser.add_argument(
202 '--no-assembler',
203 '-x',
204 action='store_true',
205 required=False,
206 help="Use when input files have already been assembled into object "
207 "files and only linking is required.")
Philipp Wagner80cc8232020-10-29 16:26:09 +0000208 parser.add_argument('src_files', nargs='+', type=str, metavar='SRC_FILE')
209 args = parser.parse_args()
210
Rupert Swarbrick8dd960c2020-12-14 10:26:17 +0000211 log_level = log.INFO if args.verbose else log.WARNING
212 log.basicConfig(level=log_level, format="%(message)s")
213
Philipp Wagner80cc8232020-10-29 16:26:09 +0000214 out_dir = Path(args.out_dir)
Chris Frantz9b34e4a2021-11-24 17:03:12 -0800215 out_dir.mkdir(parents=True, exist_ok=True)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000216
217 src_files = [Path(f) for f in args.src_files]
218 for src_file in src_files:
219 if not src_file.exists():
220 log.fatal("Source file %s not found." % src_file)
221 return 1
222 obj_files = [out_dir / f.with_suffix('.o').name for f in src_files]
223
224 app_name = args.app_name or str(src_files[0].stem)
Chris Frantz9b34e4a2021-11-24 17:03:12 -0800225 archive = args.archive
Philipp Wagner80cc8232020-10-29 16:26:09 +0000226
227 try:
Jade Philipoom39c9b322022-03-08 12:27:10 +0000228 if not args.no_assembler:
229 for src_file, obj_file in zip(src_files, obj_files):
230 call_otbn_as(src_file, obj_file)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000231
232 out_elf = out_dir / (app_name + '.elf')
Philipp Wagner6f51ecb2020-11-19 23:05:22 +0000233 call_otbn_ld(obj_files, out_elf, linker_script = args.linker_script)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000234
Rupert Swarbrick48b9a952022-01-14 16:20:16 +0000235 # out_elf is a fully-linked OTBN binary, but we want to be able to use
236 # it from Ibex, the host processor. To make this work, we generate an
237 # ELF file that can be linked into the Ibex image.
Rupert Swarbrick1b0529b2021-02-01 15:08:39 +0000238 #
Rupert Swarbrick48b9a952022-01-14 16:20:16 +0000239 # This ELF contains all initialised data (the .text and .data
240 # sections). We change the flags to treat them like rodata (since
241 # they're not executable on Ibex, nor does it make sense for Ibex code
242 # to manipulate OTBN data sections "in place") and add a .rodata.otbn
243 # prefix to the section names.
244 #
245 # The symbols exposed by the binary will be relocated as part of the
246 # link, so they'll point into the Ibex address space. To allow linking
247 # against multiple OTBN applications, we give the symbols an
248 # application-specific prefix. (Note: This prefix is used in driver
249 # code: so needs to be kept in sync with that).
250 #
251 # As well as the initialised data and relocated symbols, we also want
252 # to add (absolute) symbols that have the OTBN addresses of the symbols
253 # in question. Unfortunately, objcopy doesn't seem to have a "make all
254 # symbols absolute" command, so we have to do it by hand. This also
255 # means constructing an enormous objcopy command line :-/ If we run out
256 # of space, we might have to use elftools to inject the addresses after
257 # the objcopy.
258 host_side_pfx = '_otbn_local_app_{}_'.format(app_name)
259 otbn_side_pfx = '_otbn_remote_app_{}_'.format(app_name)
Philipp Wagner80cc8232020-10-29 16:26:09 +0000260 out_embedded_obj = out_dir / (app_name + '.rv32embed.o')
Rupert Swarbrick48b9a952022-01-14 16:20:16 +0000261 args = ['-O', 'elf32-littleriscv',
262 '--set-section-flags=*=alloc,load,readonly',
263 '--remove-section=.scratchpad',
264 '--remove-section=.bss',
265 '--prefix-sections=.rodata.otbn',
266 '--prefix-symbols', host_side_pfx]
267 for name, addr in get_otbn_syms(out_elf):
268 args += ['--add-symbol', f'{otbn_side_pfx}{name}=0x{addr:x}']
Philipp Wagner80cc8232020-10-29 16:26:09 +0000269
Rupert Swarbrick48b9a952022-01-14 16:20:16 +0000270 call_rv32_objcopy(args + [out_elf, out_embedded_obj])
Rupert Swarbrick13dc5c32021-02-01 15:17:39 +0000271
272 # After objcopy has finished, we have to do a little surgery to
273 # overwrite the ELF e_type field (a 16-bit little-endian number at file
274 # offset 0x10). It will currently be 0x2 (ET_EXEC), which means a
275 # fully-linked executable file. Binutils doesn't want to link with
276 # anything of type ET_EXEC (since it usually wouldn't make any sense to
277 # do so). Hack the type to be 0x1 (ET_REL), which means an object file.
278 with open(out_embedded_obj, 'r+b') as emb_file:
279 emb_file.seek(0x10)
280 emb_file.write(b'\1\0')
281
Chris Frantz9b34e4a2021-11-24 17:03:12 -0800282 if archive:
283 out_embedded_a = out_dir / (app_name + '.rv32embed.a')
284 call_rv32_ar(['rcs', out_embedded_a, out_embedded_obj])
285
Philipp Wagner80cc8232020-10-29 16:26:09 +0000286 except subprocess.CalledProcessError as e:
287 # Show a nicer error message if any of the called programs fail.
288 log.fatal("Command {!r} returned non-zero exit code {}".format(
289 cmd_to_str(e.cmd), e.returncode))
290 return 1
291
292 return 0
293
294
295if __name__ == "__main__":
296 sys.exit(main())