1#!/usr/bin/env python3 2# Copyright (c) 2019, Arm Limited. All rights reserved. 3# 4# SPDX-License-Identifier: BSD-3-Clause 5 6""" 7This module contains a set of classes and a runner that can generate code for the romlib module 8based on the templates in the 'templates' directory. 9""" 10 11import argparse 12import os 13import re 14import subprocess 15import string 16import sys 17 18class IndexFileParser: 19 """ 20 Parses the contents of the index file into the items and dependencies variables. It 21 also resolves included files in the index files recursively with circular inclusion detection. 22 """ 23 24 def __init__(self): 25 self.items = [] 26 self.dependencies = {} 27 self.include_chain = [] 28 29 def add_dependency(self, parent, dependency): 30 """ Adds a dependency into the dependencies variable. """ 31 if parent in self.dependencies: 32 self.dependencies[parent].append(dependency) 33 else: 34 self.dependencies[parent] = [dependency] 35 36 def get_dependencies(self, parent): 37 """ Gets all the recursive dependencies of a parent file. """ 38 parent = os.path.normpath(parent) 39 if parent in self.dependencies: 40 direct_deps = self.dependencies[parent] 41 deps = direct_deps 42 for direct_dep in direct_deps: 43 deps += self.get_dependencies(direct_dep) 44 return deps 45 46 return [] 47 48 def parse(self, file_name): 49 """ Opens and parses index file. """ 50 file_name = os.path.normpath(file_name) 51 52 if file_name not in self.include_chain: 53 self.include_chain.append(file_name) 54 self.dependencies[file_name] = [] 55 else: 56 raise Exception("Circular dependency detected: " + file_name) 57 58 with open(file_name, "r") as index_file: 59 for line in index_file.readlines(): 60 line_elements = line.split() 61 62 if line.startswith("#") or not line_elements: 63 # Comment or empty line 64 continue 65 66 if line_elements[0] == "reserved": 67 # Reserved slot in the jump table 68 self.items.append({"type": "reserved"}) 69 elif line_elements[0] == "include" and len(line_elements) > 1: 70 # Include other index file 71 included_file = os.path.normpath(line_elements[1]) 72 self.add_dependency(file_name, included_file) 73 self.parse(included_file) 74 elif len(line_elements) > 1: 75 # Library function 76 library_name = line_elements[0] 77 function_name = line_elements[1] 78 patch = bool(len(line_elements) > 2 and line_elements[2] == "patch") 79 80 self.items.append({"type": "function", "library_name": library_name, 81 "function_name": function_name, "patch": patch}) 82 else: 83 raise Exception("Invalid line: '" + line + "'") 84 85 self.include_chain.pop() 86 87class RomlibApplication: 88 """ Base class of romlib applications. """ 89 TEMPLATE_DIR = os.path.dirname(os.path.realpath(__file__)) + "/templates/" 90 91 def __init__(self, prog): 92 self.args = argparse.ArgumentParser(prog=prog, description=self.__doc__) 93 self.config = None 94 95 def parse_arguments(self, argv): 96 """ Parses the arguments that should come from the command line arguments. """ 97 self.config = self.args.parse_args(argv) 98 99 def build_template(self, name, mapping=None, remove_comment=False): 100 """ 101 Loads a template and builds it with the defined mapping. Template paths are always relative 102 to this script. 103 """ 104 105 with open(self.TEMPLATE_DIR + name, "r") as template_file: 106 if remove_comment: 107 # Removing copyright comment to make the generated code more readable when the 108 # template is inserted multiple times into the output. 109 template_lines = template_file.readlines() 110 end_of_comment_line = 0 111 for index, line in enumerate(template_lines): 112 if line.find("*/") != -1: 113 end_of_comment_line = index 114 break 115 template_data = "".join(template_lines[end_of_comment_line + 1:]) 116 else: 117 template_data = template_file.read() 118 119 template = string.Template(template_data) 120 return template.substitute(mapping) 121 122class IndexPreprocessor(RomlibApplication): 123 """ Removes empty and comment lines from the index file and resolves includes. """ 124 125 def __init__(self, prog): 126 RomlibApplication.__init__(self, prog) 127 128 self.args.add_argument("-o", "--output", help="Output file", metavar="output", 129 default="jmpvar.s") 130 self.args.add_argument("--deps", help="Dependency file") 131 self.args.add_argument("file", help="Input file") 132 133 def main(self): 134 """ 135 After parsing the input index file it generates a clean output with all includes resolved. 136 Using --deps option it also outputs the dependencies in makefile format like gcc's with -M. 137 """ 138 139 index_file_parser = IndexFileParser() 140 index_file_parser.parse(self.config.file) 141 142 with open(self.config.output, "w") as output_file: 143 for item in index_file_parser.items: 144 if item["type"] == "function": 145 patch = "\tpatch" if item["patch"] else "" 146 output_file.write( 147 item["library_name"] + "\t" + item["function_name"] + patch + "\n") 148 else: 149 output_file.write("reserved\n") 150 151 if self.config.deps: 152 with open(self.config.deps, "w") as deps_file: 153 deps = [self.config.file] + index_file_parser.get_dependencies(self.config.file) 154 deps_file.write(self.config.output + ": " + " \\\n".join(deps) + "\n") 155 156class TableGenerator(RomlibApplication): 157 """ Generates the jump table by parsing the index file. """ 158 159 def __init__(self, prog): 160 RomlibApplication.__init__(self, prog) 161 162 self.args.add_argument("-o", "--output", help="Output file", metavar="output", 163 default="jmpvar.s") 164 self.args.add_argument("--bti", help="Branch Target Identification", type=int) 165 self.args.add_argument("file", help="Input file") 166 167 def main(self): 168 """ 169 Inserts the jmptbl definition and the jump entries into the output file. Also can insert 170 BTI related code before entries if --bti option set. It can output a dependency file of the 171 included index files. This can be directly included in makefiles. 172 """ 173 174 index_file_parser = IndexFileParser() 175 index_file_parser.parse(self.config.file) 176 177 with open(self.config.output, "w") as output_file: 178 output_file.write(self.build_template("jmptbl_header.S")) 179 bti = "_bti" if self.config.bti == 1 else "" 180 181 for item in index_file_parser.items: 182 template_name = "jmptbl_entry_" + item["type"] + bti + ".S" 183 output_file.write(self.build_template(template_name, item, True)) 184 185class LinkArgs(RomlibApplication): 186 """ Generates the link arguments to wrap functions. """ 187 188 def __init__(self, prog): 189 RomlibApplication.__init__(self, prog) 190 self.args.add_argument("file", help="Input file") 191 192 def main(self): 193 index_file_parser = IndexFileParser() 194 index_file_parser.parse(self.config.file) 195 196 fns = [item["function_name"] for item in index_file_parser.items 197 if not item["patch"] and item["type"] != "reserved"] 198 199 print(" ".join("-Wl,--wrap " + f for f in fns)) 200 201class WrapperGenerator(RomlibApplication): 202 """ 203 Generates a wrapper function for each entry in the index file except for the ones that contain 204 the keyword patch. The generated wrapper file is called <lib>_<fn_name>.s. 205 """ 206 207 def __init__(self, prog): 208 RomlibApplication.__init__(self, prog) 209 210 self.args.add_argument("-b", help="Build directory", default=".", metavar="build") 211 self.args.add_argument("--bti", help="Branch Target Identification", type=int) 212 self.args.add_argument("--list", help="Only list assembly files", action="store_true") 213 self.args.add_argument("file", help="Input file") 214 215 def main(self): 216 """ 217 Iterates through the items in the parsed index file and builds the template for each entry. 218 """ 219 220 index_file_parser = IndexFileParser() 221 index_file_parser.parse(self.config.file) 222 223 bti = "_bti" if self.config.bti == 1 else "" 224 function_offset = 0 225 files = [] 226 227 for item_index in range(0, len(index_file_parser.items)): 228 item = index_file_parser.items[item_index] 229 230 if item["type"] == "reserved" or item["patch"]: 231 continue 232 233 if not self.config.list: 234 # The jump instruction is 4 bytes but BTI requires and extra instruction so 235 # this makes it 8 bytes per entry. 236 function_offset = item_index * (8 if self.config.bti else 4) 237 238 item["function_offset"] = function_offset 239 files.append(self.build_template("wrapper" + bti + ".S", item)) 240 241 if self.config.list: 242 print(self.config.b + "/wrappers.s") 243 else: 244 with open(self.config.b + "/wrappers.s", "w") as asm_file: 245 asm_file.write("\n".join(files)) 246 247class VariableGenerator(RomlibApplication): 248 """ Generates the jump table global variable with the absolute address in ROM. """ 249 250 def __init__(self, prog): 251 RomlibApplication.__init__(self, prog) 252 253 self.args.add_argument("-o", "--output", help="Output file", metavar="output", 254 default="jmpvar.s") 255 self.args.add_argument("file", help="Input file") 256 257 def main(self): 258 """ 259 Runs nm -a command on the input file and inserts the address of the .text section into the 260 template as the ROM address of the jmp_table. 261 """ 262 symbols = subprocess.check_output(["nm", "-a", self.config.file]) 263 264 matching_symbol = re.search("([0-9A-Fa-f]+) . \\.text", str(symbols)) 265 if not matching_symbol: 266 raise Exception("No '.text' section was found in %s" % self.config.file) 267 268 mapping = {"jmptbl_address": matching_symbol.group(1)} 269 270 with open(self.config.output, "w") as output_file: 271 output_file.write(self.build_template("jmptbl_glob_var.S", mapping)) 272 273if __name__ == "__main__": 274 APPS = {"genvar": VariableGenerator, "pre": IndexPreprocessor, 275 "gentbl": TableGenerator, "genwrappers": WrapperGenerator, 276 "link-flags": LinkArgs} 277 278 if len(sys.argv) < 2 or sys.argv[1] not in APPS: 279 print("usage: romlib_generator.py [%s] [args]" % "|".join(APPS.keys()), file=sys.stderr) 280 sys.exit(1) 281 282 APP = APPS[sys.argv[1]]("romlib_generator.py " + sys.argv[1]) 283 APP.parse_arguments(sys.argv[2:]) 284 try: 285 APP.main() 286 sys.exit(0) 287 except FileNotFoundError as file_not_found_error: 288 print(file_not_found_error, file=sys.stderr) 289 except subprocess.CalledProcessError as called_process_error: 290 print(called_process_error.output, file=sys.stderr) 291 292 sys.exit(1) 293