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 WrapperGenerator(RomlibApplication): 186 """ 187 Generates a wrapper function for each entry in the index file except for the ones that contain 188 the keyword patch. The generated wrapper file is called <lib>_<fn_name>.s. 189 """ 190 191 def __init__(self, prog): 192 RomlibApplication.__init__(self, prog) 193 194 self.args.add_argument("-b", help="Build directory", default=".", metavar="build") 195 self.args.add_argument("--bti", help="Branch Target Identification", type=int) 196 self.args.add_argument("--list", help="Only list assembly files", action="store_true") 197 self.args.add_argument("file", help="Input file") 198 199 def main(self): 200 """ 201 Iterates through the items in the parsed index file and builds the template for each entry. 202 """ 203 204 index_file_parser = IndexFileParser() 205 index_file_parser.parse(self.config.file) 206 207 bti = "_bti" if self.config.bti == 1 else "" 208 function_offset = 0 209 files = [] 210 211 for item_index in range(0, len(index_file_parser.items)): 212 item = index_file_parser.items[item_index] 213 214 if item["type"] == "reserved" or item["patch"]: 215 continue 216 217 asm = self.config.b + "/" + item["function_name"] + ".s" 218 if self.config.list: 219 # Only listing files 220 files.append(asm) 221 else: 222 with open(asm, "w") as asm_file: 223 # The jump instruction is 4 bytes but BTI requires and extra instruction so 224 # this makes it 8 bytes per entry. 225 function_offset = item_index * (8 if self.config.bti else 4) 226 227 item["function_offset"] = function_offset 228 asm_file.write(self.build_template("wrapper" + bti + ".S", item)) 229 230 if self.config.list: 231 print(" ".join(files)) 232 233class VariableGenerator(RomlibApplication): 234 """ Generates the jump table global variable with the absolute address in ROM. """ 235 236 def __init__(self, prog): 237 RomlibApplication.__init__(self, prog) 238 239 self.args.add_argument("-o", "--output", help="Output file", metavar="output", 240 default="jmpvar.s") 241 self.args.add_argument("file", help="Input file") 242 243 def main(self): 244 """ 245 Runs nm -a command on the input file and inserts the address of the .text section into the 246 template as the ROM address of the jmp_table. 247 """ 248 symbols = subprocess.check_output(["nm", "-a", self.config.file]) 249 250 matching_symbol = re.search("([0-9A-Fa-f]+) . \\.text", str(symbols)) 251 if not matching_symbol: 252 raise Exception("No '.text' section was found in %s" % self.config.file) 253 254 mapping = {"jmptbl_address": matching_symbol.group(1)} 255 256 with open(self.config.output, "w") as output_file: 257 output_file.write(self.build_template("jmptbl_glob_var.S", mapping)) 258 259if __name__ == "__main__": 260 APPS = {"genvar": VariableGenerator, "pre": IndexPreprocessor, 261 "gentbl": TableGenerator, "genwrappers": WrapperGenerator} 262 263 if len(sys.argv) < 2 or sys.argv[1] not in APPS: 264 print("usage: romlib_generator.py [%s] [args]" % "|".join(APPS.keys()), file=sys.stderr) 265 sys.exit(1) 266 267 APP = APPS[sys.argv[1]]("romlib_generator.py " + sys.argv[1]) 268 APP.parse_arguments(sys.argv[2:]) 269 try: 270 APP.main() 271 sys.exit(0) 272 except FileNotFoundError as file_not_found_error: 273 print(file_not_found_error, file=sys.stderr) 274 except subprocess.CalledProcessError as called_process_error: 275 print(called_process_error.output, file=sys.stderr) 276 277 sys.exit(1) 278