#!/usr/bin/env python3 # # SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import logging import argparse import csv import os import subprocess import sys import re from io import StringIO import configuration sys.path.append(os.environ['IDF_PATH'] + '/tools/ldgen') sys.path.append(os.environ['IDF_PATH'] + '/tools/ldgen/ldgen') from entity import EntityDB espidf_objdump = None espidf_version = None def lib_secs(lib, file, lib_path): new_env = os.environ.copy() new_env['LC_ALL'] = 'C' try: dump_output = subprocess.check_output([espidf_objdump, '-h', lib_path], env=new_env).decode() except subprocess.CalledProcessError as e: raise RuntimeError(f"Command '{e.cmd}' failed with exit code {e.returncode}") dump = StringIO(dump_output) dump.name = lib sections_infos = EntityDB() sections_infos.add_sections_info(dump) secs = sections_infos.get_sections(lib, file.split('.')[0] + '.c') if not secs: secs = sections_infos.get_sections(lib, file.split('.')[0]) if not secs: raise ValueError(f'Failed to get sections from lib {lib_path}') return secs def filter_secs(secs_a, secs_b): return [s_a for s_a in secs_a if any(s_b in s_a for s_b in secs_b)] def strip_secs(secs_a, secs_b): secs = list(set(secs_a) - set(secs_b)) secs.sort() return secs def func2sect(func): if ' ' in func: func_l = func.split(' ') else: func_l = [func] secs = [] for l in func_l: if '.iram1.' not in l: secs.append(f'.literal.{l}') secs.append(f'.text.{l}') else: secs.append(l) return secs class filter_c: def __init__(self, file): with open(file) as f: lines = f.read().splitlines() self.libs_desc = '' self.libs = '' for line in lines: if ') .iram1 EXCLUDE_FILE(*' in line and ') .iram1.*)' in line: desc = r'\(EXCLUDE_FILE\((.*)\) .iram1 ' self.libs_desc = re.search(desc, line)[1] self.libs = self.libs_desc.replace('*', '') return def match(self, lib): if lib in self.libs: print(f'Remove lib {lib}') return True return False def add(self): return self.libs_desc class target_c: def __init__(self, lib, lib_path, file, fsecs): self.lib = lib self.file = file self.lib_path = lib_path self.fsecs = func2sect(fsecs) self.desc = f'*{lib}:{file.split(".")[0]}.*' secs = lib_secs(lib, file, lib_path) if '.iram1.' in self.fsecs[0]: self.secs = filter_secs(secs, ('.iram1.',)) else: self.secs = filter_secs(secs, ('.iram1.', '.text.', '.literal.')) self.isecs = strip_secs(self.secs, self.fsecs) self.has_exclude_iram = True self.has_exclude_dram = True def __str__(self): return ( f'lib={self.lib}\n' f'file={self.file}\n' f'lib_path={self.lib_path}\n' f'desc={self.desc}\n' f'secs={self.secs}\n' f'fsecs={self.fsecs}\n' f'isecs={self.isecs}\n' ) class target2_c: def __init__(self, lib, file, iram_secs): self.lib = lib self.file = file self.fsecs = iram_secs if file != '*': self.desc = f'*{lib}:{file.split(".")[0]}.*' else: self.desc = f'*{lib}:' self.isecs = iram_secs self.has_exclude_iram = False self.has_exclude_dram = False def __str__(self): return ( f'lib={self.lib}\n' f'file={self.file}\n' f'lib_path={self.lib_path}\n' f'desc={self.desc}\n' f'secs={self.secs}\n' f'fsecs={self.fsecs}\n' f'isecs={self.isecs}\n' ) # Remove specific functions from IRAM class relink_c: def __init__(self, input_file, library_file, object_file, function_file, sdkconfig_file, missing_function_info): self.filter = filter_c(input_file) libraries = configuration.generator(library_file, object_file, function_file, sdkconfig_file, missing_function_info, espidf_objdump) self.targets = [] for lib_name in libraries.libs: lib = libraries.libs[lib_name] if self.filter.match(lib.name): continue for obj_name in lib.objs: obj = lib.objs[obj_name] target = target_c(lib.name, lib.path, obj.name, ' '.join(obj.sections())) self.targets.append(target) self.__transform__() def __transform__(self): iram1_exclude = [] iram1_include = [] flash_include = [] for t in self.targets: secs = filter_secs(t.fsecs, ('.iram1.', )) if len(secs) > 0: iram1_exclude.append(t.desc) secs = filter_secs(t.isecs, ('.iram1.', )) if len(secs) > 0: iram1_include.append(f' {t.desc}({" ".join(secs)})') secs = t.fsecs if len(secs) > 0: flash_include.append(f' {t.desc}({" ".join(secs)})') self.iram1_exclude = f' *(EXCLUDE_FILE({self.filter.add()} {" ".join(iram1_exclude)}) .iram1.*) *(EXCLUDE_FILE({self.filter.add()} {" ".join(iram1_exclude)}) .iram1)' self.iram1_include = '\n'.join(iram1_include) self.flash_include = '\n'.join(flash_include) logging.debug(f'IRAM1 Exclude: {self.iram1_exclude}') logging.debug(f'IRAM1 Include: {self.iram1_include}') logging.debug(f'Flash Include: {self.flash_include}') def __replace__(self, lines): def is_iram_desc(line): return '*(.iram1 .iram1.*)' in line or (') .iram1 EXCLUDE_FILE(*' in line and ') .iram1.*)' in line) iram_start = False flash_done = False flash_text = '(.stub)' if espidf_version == '5.0': flash_text = '(.stub .gnu.warning' new_lines = [] for line in lines: if '.iram0.text :' in line: logging.debug('start to process .iram0.text') iram_start = True new_lines.append(line) elif '.dram0.data :' in line: logging.debug('end to process .iram0.text') iram_start = False new_lines.append(line) elif is_iram_desc(line): if iram_start: new_lines.extend([self.iram1_exclude, self.iram1_include]) else: new_lines.append(line) elif flash_text in line: if not flash_done: new_lines.extend([self.flash_include, line]) flash_done = True else: new_lines.append(line) else: new_lines.append(self._replace_func(line) if iram_start else line) return new_lines def _replace_func(self, line): for t in self.targets: if t.desc in line: S = '.literal .literal.* .text .text.*' if S in line: if len(t.isecs) > 0: return line.replace(S, ' '.join(t.isecs)) else: return '' S = f'{t.desc}({" ".join(t.fsecs)})' if S in line: return '' replaced = False for s in t.fsecs: s2 = s + ' ' if s2 in line: line = line.replace(s2, '') replaced = True s2 = s + ')' if s2 in line: line = line.replace(s2, ')') replaced = True if '( )' in line or '()' in line: return '' if replaced: return line index = f'*{t.lib}:(EXCLUDE_FILE' if index in line and t.file.split('.')[0] not in line: for m in self.targets: index = f'*{m.lib}:(EXCLUDE_FILE' if index in line and m.file.split('.')[0] not in line: line = line.replace('EXCLUDE_FILE(', f'EXCLUDE_FILE({m.desc} ') if len(m.isecs) > 0: line += f'\n {m.desc}({" ".join(m.isecs)})' return line return line def save(self, input_file, output_file, target): with open(input_file) as f: lines = f.read().splitlines() lines = self.__replace__(lines) with open(output_file, 'w+') as f: f.write('\n'.join(lines)) # Link specific functions to IRAM class relink2_c: def __init__(self, input_file, library_file, object_file, function_file, sdkconfig_file, missing_function_info): self.filter = filter_c(input_file) rodata_exclude = [] iram_exclude = [] rodata = [] iram = [] libraries = configuration.generator(library_file, object_file, function_file, sdkconfig_file, missing_function_info, espidf_objdump) self.targets = [] for lib_name in libraries.libs: lib = libraries.libs[lib_name] if self.filter.match(lib.name): continue for obj_name in lib.objs: obj = lib.objs[obj_name] if not obj.section_all: self.targets.append(target_c(lib.name, lib.path, obj.name, ' '.join(obj.sections()))) else: self.targets.append(target2_c(lib.name, obj.name, obj.sections())) for target in self.targets: rodata.append(f' {target.desc}(.rodata .rodata.* .sdata2 .sdata2.* .srodata .srodata.*)') if target.has_exclude_dram: rodata_exclude.append(target.desc) iram.append(f' {target.desc}({" ".join(target.fsecs)})') if target.has_exclude_iram: iram_exclude.append(target.desc) self.rodata_ex = f' *(EXCLUDE_FILE({" ".join(rodata_exclude)}) .rodata EXCLUDE_FILE({" ".join(rodata_exclude)}) .rodata.* EXCLUDE_FILE({" ".join(rodata_exclude)}) .sdata2 EXCLUDE_FILE({" ".join(rodata_exclude)}) .sdata2.* EXCLUDE_FILE({" ".join(rodata_exclude)}) .srodata EXCLUDE_FILE({" ".join(rodata_exclude)}) .srodata.*)' self.rodata_in = '\n'.join(rodata) self.iram_ex = f' *(EXCLUDE_FILE({" ".join(iram_exclude)}) .iram1 EXCLUDE_FILE({" ".join(iram_exclude)}) .iram1.*)' self.iram_in = '\n'.join(iram) logging.debug(f'RODATA In: {self.rodata_in}') logging.debug(f'RODATA Ex: {self.rodata_ex}') logging.debug(f'IRAM In: {self.iram_in}') logging.debug(f'IRAM Ex: {self.iram_ex}') def save(self, input_file, output_file, target): with open(input_file) as f: lines = f.read().splitlines() lines = self.__replace__(lines, target) with open(output_file, 'w+') as f: f.write('\n'.join(lines)) def __replace__(self, lines, target): iram_start = False new_lines = [] iram_cache = [] flash_text = '*(.stub)' iram_text = '*(.iram1 .iram1.*)' if espidf_version == '5.3' and target == 'esp32c2': iram_text = '_iram_text_start = ABSOLUTE(.);' elif espidf_version == '5.0': flash_text = '*(.stub .gnu.warning' for line in lines: if not iram_start: if iram_text in line: new_lines.append(f'{self.iram_in}\n') iram_start = True elif ' _text_start = ABSOLUTE(.);' in line: new_lines.append(f'{line}\n{self.iram_ex}') continue elif flash_text in line: new_lines.extend(iram_cache) iram_cache = [] else: if '} > iram0_0_seg' in line: iram_start = False if not iram_start: new_lines.append(line) else: skip_str = ['libriscv.a:interrupt', 'libriscv.a:vectors'] if not any(s in line for s in skip_str): iram_cache.append(line) return new_lines def main(): argparser = argparse.ArgumentParser(description='Relinker script generator') argparser.add_argument( '--input', '-i', help='Linker template file', type=str, required=True) argparser.add_argument( '--output', '-o', help='Output linker script', type=str, required=True) argparser.add_argument( '--library', '-l', help='Library description directory', type=str, required=True) argparser.add_argument( '--object', '-b', help='Object description file', type=str, required=True) argparser.add_argument( '--function', '-f', help='Function description file', type=str, required=True) argparser.add_argument( '--sdkconfig', '-s', help='sdkconfig file', type=str, required=True) argparser.add_argument( '--target', '-t', help='target chip', type=str, required=True) argparser.add_argument( '--version', '-v', help='IDF version', type=str, required=True) argparser.add_argument( '--objdump', '-g', help='GCC objdump command', type=str, required=True) argparser.add_argument( '--debug', '-d', help='Debug level (option is \'debug\')', default='no', type=str) argparser.add_argument( '--missing_function_info', help='Print error information instead of throwing exception when missing function', action='store_true') argparser.add_argument( '--link_to_iram', help='True: Link specific functions to IRAM, False: Remove specific functions from IRAM', action='store_true') args = argparser.parse_args() if args.debug == 'debug': logging.basicConfig(level=logging.DEBUG) logging.debug(f'input: {args.input}') logging.debug(f'output: {args.output}') logging.debug(f'library: {args.library}') logging.debug(f'object: {args.object}') logging.debug(f'function: {args.function}') logging.debug(f'version: {args.version}') logging.debug(f'sdkconfig:{args.sdkconfig}') logging.debug(f'objdump: {args.objdump}') logging.debug(f'debug: {args.debug}') logging.debug(f'missing_function_info: {args.missing_function_info}') global espidf_objdump espidf_objdump = args.objdump global espidf_version espidf_version = args.version if not args.link_to_iram: relink = relink_c(args.input, args.library, args.object, args.function, args.sdkconfig, args.missing_function_info) else: relink = relink2_c(args.input, args.library, args.object, args.function, args.sdkconfig, args.missing_function_info) relink.save(args.input, args.output, args.target) if __name__ == '__main__': main()