1#!/usr/bin/env python3
2#
3# Copyright (c) 2018 Intel Corporation.
4#
5# SPDX-License-Identifier: Apache-2.0
6#
7
8"""
9This script will relocate .text, .rodata, .data and .bss sections from required files
10and places it in the required memory region. This memory region and file
11are given to this python script in the form of a file.
12A regular expression filter can be applied to select only the required sections from the file.
13
14Example of content in such an input file would be::
15
16   SRAM2:COPY:/home/xyz/zephyr/samples/hello_world/src/main.c,.*foo|.*bar
17   SRAM1:COPY:/home/xyz/zephyr/samples/hello_world/src/main2.c,.*bar
18   FLASH2:NOCOPY:/home/xyz/zephyr/samples/hello_world/src/main3.c,
19
20One can also specify the program header for a given memory region:
21
22   SRAM2\\ :phdr0:COPY:/home/xyz/zephyr/samples/hello_world/src/main.c,
23
24To invoke this script::
25
26   python3 gen_relocate_app.py -i input_file -o generated_linker -c generated_code
27
28Configuration that needs to be sent to the python script.
29
30- If the memory is like SRAM1/SRAM2/CCD/AON then place full object in
31  the sections
32- If the memory type is appended with _DATA / _TEXT/ _RODATA/ _BSS only the
33  selected memory is placed in the required memory region. Others are
34  ignored.
35- COPY/NOCOPY defines whether the script should generate the relocation code in
36  code_relocation.c or not
37- NOKEEP will suppress the default behavior of marking every relocated symbol
38  with KEEP() in the generated linker script.
39
40Multiple regions can be appended together like SRAM2_DATA_BSS
41this will place data and bss inside SRAM2.
42"""
43
44
45import sys
46import argparse
47import os
48import glob
49import re
50import warnings
51from collections import defaultdict
52from enum import Enum
53from pathlib import Path
54from typing import NamedTuple
55from typing import NewType
56from typing import Tuple
57
58from elftools.elf.elffile import ELFFile
59from elftools.elf.sections import SymbolTableSection
60
61MemoryRegion = NewType('MemoryRegion', str)
62
63
64class SectionKind(Enum):
65    TEXT = "text"
66    RODATA = "rodata"
67    DATA = "data"
68    BSS = "bss"
69    LITERAL = "literal"
70
71    def __str__(self):
72        return self.name
73
74    @classmethod
75    def for_section_named(cls, name: str):
76        """
77        Return the kind of section that includes a section with the given name.
78
79        >>> SectionKind.for_section_with_name(".rodata.str1.4")
80        <SectionKind.RODATA: 'rodata'>
81        >>> SectionKind.for_section_with_name(".device_deps")
82        None
83        """
84        if ".text." in name:
85            return cls.TEXT
86        elif ".rodata." in name:
87            return cls.RODATA
88        elif ".data." in name:
89            return cls.DATA
90        elif ".bss." in name:
91            return cls.BSS
92        elif ".literal." in name:
93            return cls.LITERAL
94        else:
95            return None
96
97
98class OutputSection(NamedTuple):
99    obj_file_name: str
100    section_name: str
101    keep: bool = True
102
103
104PRINT_TEMPLATE = """
105                KEEP(*{obj_file_name}({section_name}))
106"""
107
108PRINT_TEMPLATE_NOKEEP = """
109                *{obj_file_name}({section_name})
110"""
111
112SECTION_LOAD_MEMORY_SEQ = """
113        __{0}_{1}_rom_start = LOADADDR(.{0}_{1}_reloc);
114"""
115
116LOAD_ADDRESS_LOCATION_FLASH = """
117#ifdef CONFIG_XIP
118GROUP_DATA_LINK_IN({0}, ROMABLE_REGION)
119#else
120GROUP_DATA_LINK_IN({0}, {0})
121#endif
122"""
123
124LOAD_ADDRESS_LOCATION_FLASH_NOCOPY = """
125GROUP_LINK_IN({0})
126"""
127
128LOAD_ADDRESS_LOCATION_BSS = "GROUP_LINK_IN({0})"
129
130MPU_RO_REGION_START = """
131
132     _{0}_mpu_ro_region_start = ORIGIN({1});
133
134"""
135
136MPU_RO_REGION_END = """
137
138    _{0}_mpu_ro_region_end = .;
139
140"""
141
142# generic section creation format
143LINKER_SECTION_SEQ = """
144
145/* Linker section for memory region {2} for  {3} section  */
146
147	SECTION_PROLOGUE(.{0}_{1}_reloc,,)
148        {{
149                . = ALIGN(4);
150                {4}
151                . = ALIGN(4);
152	}} {5}
153        __{0}_{1}_reloc_end = .;
154        __{0}_{1}_reloc_start = ADDR(.{0}_{1}_reloc);
155        __{0}_{1}_reloc_size = __{0}_{1}_reloc_end - __{0}_{1}_reloc_start;
156"""
157
158LINKER_SECTION_SEQ_MPU = """
159
160/* Linker section for memory region {2} for {3} section  */
161
162	SECTION_PROLOGUE(.{0}_{1}_reloc,,)
163        {{
164                __{0}_{1}_reloc_start = .;
165                {4}
166#if {6}
167                . = ALIGN({6});
168#else
169                MPU_ALIGN(__{0}_{1}_reloc_size);
170#endif
171                __{0}_{1}_reloc_end = .;
172	}} {5}
173        __{0}_{1}_reloc_size = __{0}_{1}_reloc_end - __{0}_{1}_reloc_start;
174"""
175
176SOURCE_CODE_INCLUDES = """
177/* Auto generated code. Do not modify.*/
178#include <zephyr/kernel.h>
179#include <zephyr/linker/linker-defs.h>
180#include <zephyr/kernel_structs.h>
181#include <kernel_internal.h>
182"""
183
184EXTERN_LINKER_VAR_DECLARATION = """
185extern char __{0}_{1}_reloc_start[];
186extern char __{0}_{1}_rom_start[];
187extern char __{0}_{1}_reloc_size[];
188"""
189
190
191DATA_COPY_FUNCTION = """
192void data_copy_xip_relocation(void)
193{{
194{0}
195}}
196"""
197
198BSS_ZEROING_FUNCTION = """
199void bss_zeroing_relocation(void)
200{{
201{0}
202}}
203"""
204
205MEMCPY_TEMPLATE = """
206	z_early_memcpy(&__{0}_{1}_reloc_start, &__{0}_{1}_rom_start,
207		           (size_t) &__{0}_{1}_reloc_size);
208
209"""
210
211MEMSET_TEMPLATE = """
212	z_early_memset(&__{0}_bss_reloc_start, 0,
213		           (size_t) &__{0}_bss_reloc_size);
214"""
215
216
217def region_is_default_ram(region_name: str) -> bool:
218    """
219    Test whether a memory region with the given name is the system's default
220    RAM region or not.
221
222    This is used to determine whether some items need to be omitted from
223    custom regions and instead be placed in the default. In particular, mutable
224    data placed in the default RAM section is ignored and is allowed to be
225    handled normally by the linker because it is placed in that region anyway.
226    """
227    return region_name == args.default_ram_region
228
229
230def find_sections(filename: str, symbol_filter: str) -> 'dict[SectionKind, list[OutputSection]]':
231    """
232    Locate relocatable sections in the given object file.
233
234    The output value maps categories of sections to the list of actual sections
235    located in the object file that fit in that category.
236    """
237    obj_file_path = Path(filename)
238
239    with open(obj_file_path, 'rb') as obj_file_desc:
240        full_lib = ELFFile(obj_file_desc)
241        if not full_lib:
242            sys.exit("Error parsing file: " + filename)
243
244        sections = [x for x in full_lib.iter_sections()]
245        out = defaultdict(list)
246
247        for section in sections:
248            if not re.search(symbol_filter, section.name):
249                # Section is filtered-out
250                continue
251            section_kind = SectionKind.for_section_named(section.name)
252            if section_kind is None:
253                continue
254
255            out[section_kind].append(
256                OutputSection(obj_file_path.name, section.name)
257            )
258
259            # Common variables will be placed in the .bss section
260            # only after linking in the final executable. This "if" finds
261            # common symbols and warns the user of the problem.
262            # The solution to which is simply assigning a 0 to
263            # bss variable and it will go to the required place.
264            if isinstance(section, SymbolTableSection):
265                def is_common_symbol(s):
266                    return s.entry["st_shndx"] == "SHN_COMMON"
267
268                for symbol in filter(is_common_symbol, section.iter_symbols()):
269                    warnings.warn("Common variable found. Move "+
270                                  symbol.name + " to bss by assigning it to 0/NULL")
271
272    return out
273
274
275def assign_to_correct_mem_region(
276    memory_region: str,
277    full_list_of_sections: 'dict[SectionKind, list[OutputSection]]'
278) -> 'dict[MemoryRegion, dict[SectionKind, list[OutputSection]]]':
279    """
280    Generate a mapping of memory region to collection of output sections to be
281    placed in each region.
282    """
283    use_section_kinds, memory_region = section_kinds_from_memory_region(memory_region)
284
285    memory_region, _, align_size = memory_region.partition('_')
286    if align_size:
287        mpu_align[memory_region] = int(align_size)
288
289    keep_sections = '|NOKEEP' not in memory_region
290    memory_region = memory_region.replace('|NOKEEP', '')
291
292    output_sections = {}
293    for used_kind in use_section_kinds:
294        # Pass through section kinds that go into this memory region
295        output_sections[used_kind] = [
296            section._replace(keep=keep_sections)
297            for section in full_list_of_sections[used_kind]
298        ]
299
300    return {MemoryRegion(memory_region): output_sections}
301
302
303def section_kinds_from_memory_region(memory_region: str) -> 'Tuple[set[SectionKind], str]':
304    """
305    Get the section kinds requested by the given memory region name.
306
307    Region names can be like RAM_RODATA_TEXT or just RAM; a section kind may
308    follow the region name. If no kinds are specified all are assumed.
309
310    In addition to the parsed kinds, the input region minus specifiers for those
311    kinds is returned.
312
313    >>> section_kinds_from_memory_region('SRAM2_TEXT')
314    ({<SectionKind.TEXT: 'text'>}, 'SRAM2')
315    """
316    out = set()
317    for kind in SectionKind:
318        specifier = f"_{kind}"
319        if specifier in memory_region:
320            out.add(kind)
321            memory_region = memory_region.replace(specifier, "")
322    if not out:
323        # No listed kinds implies all of the kinds
324        out = set(SectionKind)
325    return (out, memory_region)
326
327
328def print_linker_sections(list_sections: 'list[OutputSection]'):
329    out = ''
330    for section in sorted(list_sections):
331        template = PRINT_TEMPLATE if section.keep else PRINT_TEMPLATE_NOKEEP
332        out += template.format(obj_file_name=section.obj_file_name,
333                               section_name=section.section_name)
334    return out
335
336def add_phdr(memory_type, phdrs):
337    return f'{memory_type} {phdrs[memory_type] if memory_type in phdrs else ""}'
338
339
340def string_create_helper(
341    kind: SectionKind,
342    memory_type,
343    full_list_of_sections: 'dict[SectionKind, list[OutputSection]]',
344    load_address_in_flash,
345    is_copy,
346    phdrs
347):
348    linker_string = ''
349    if load_address_in_flash:
350        if is_copy:
351            load_address_string = LOAD_ADDRESS_LOCATION_FLASH.format(add_phdr(memory_type, phdrs))
352        else:
353            load_address_string = LOAD_ADDRESS_LOCATION_FLASH_NOCOPY.format(add_phdr(memory_type, phdrs))
354    else:
355        load_address_string = LOAD_ADDRESS_LOCATION_BSS.format(add_phdr(memory_type, phdrs))
356    if full_list_of_sections[kind]:
357        # Create a complete list of funcs/ variables that goes in for this
358        # memory type
359        tmp = print_linker_sections(full_list_of_sections[kind])
360        if region_is_default_ram(memory_type) and kind in (SectionKind.DATA, SectionKind.BSS):
361            linker_string += tmp
362        else:
363            if not region_is_default_ram(memory_type) and kind is SectionKind.RODATA:
364                align_size = 0
365                if memory_type in mpu_align:
366                    align_size = mpu_align[memory_type]
367
368                linker_string += LINKER_SECTION_SEQ_MPU.format(memory_type.lower(), kind.value, memory_type.upper(),
369                                                               kind, tmp, load_address_string, align_size)
370            else:
371                if region_is_default_ram(memory_type) and kind in (SectionKind.TEXT, SectionKind.LITERAL):
372                    align_size = 0
373                    linker_string += LINKER_SECTION_SEQ_MPU.format(memory_type.lower(), kind.value, memory_type.upper(),
374                                                                   kind, tmp, load_address_string, align_size)
375                else:
376                    linker_string += LINKER_SECTION_SEQ.format(memory_type.lower(), kind.value, memory_type.upper(),
377                                                               kind, tmp, load_address_string)
378            if load_address_in_flash:
379                linker_string += SECTION_LOAD_MEMORY_SEQ.format(memory_type.lower(), kind.value, memory_type.upper(),
380                                                                kind)
381    return linker_string
382
383
384def generate_linker_script(linker_file, sram_data_linker_file, sram_bss_linker_file,
385                           complete_list_of_sections, phdrs):
386    gen_string = ''
387    gen_string_sram_data = ''
388    gen_string_sram_bss = ''
389
390    for memory_type, full_list_of_sections in \
391            sorted(complete_list_of_sections.items()):
392
393        is_copy = bool("|COPY" in memory_type)
394        memory_type = memory_type.split("|", 1)[0]
395
396        if region_is_default_ram(memory_type) and is_copy:
397            gen_string += MPU_RO_REGION_START.format(memory_type.lower(), memory_type.upper())
398
399        gen_string += string_create_helper(SectionKind.LITERAL, memory_type, full_list_of_sections, 1, is_copy, phdrs)
400        gen_string += string_create_helper(SectionKind.TEXT, memory_type, full_list_of_sections, 1, is_copy, phdrs)
401        gen_string += string_create_helper(SectionKind.RODATA, memory_type, full_list_of_sections, 1, is_copy, phdrs)
402
403        if region_is_default_ram(memory_type) and is_copy:
404            gen_string += MPU_RO_REGION_END.format(memory_type.lower())
405
406        if region_is_default_ram(memory_type):
407            gen_string_sram_data += string_create_helper(SectionKind.DATA, memory_type, full_list_of_sections, 1, 1, phdrs)
408            gen_string_sram_bss += string_create_helper(SectionKind.BSS, memory_type, full_list_of_sections, 0, 1, phdrs)
409        else:
410            gen_string += string_create_helper(SectionKind.DATA, memory_type, full_list_of_sections, 1, 1, phdrs)
411            gen_string += string_create_helper(SectionKind.BSS, memory_type, full_list_of_sections, 0, 1, phdrs)
412
413    # finally writing to the linker file
414    with open(linker_file, "w") as file_desc:
415        file_desc.write(gen_string)
416
417    with open(sram_data_linker_file, "w") as file_desc:
418        file_desc.write(gen_string_sram_data)
419
420    with open(sram_bss_linker_file, "w") as file_desc:
421        file_desc.write(gen_string_sram_bss)
422
423
424def generate_memcpy_code(memory_type, full_list_of_sections, code_generation):
425    generate_sections, memory_type = section_kinds_from_memory_region(memory_type)
426
427    # Non-BSS sections get copied to the destination memory, except data in
428    # main memory which gets copied automatically.
429    for kind in (SectionKind.TEXT, SectionKind.RODATA, SectionKind.DATA):
430        if region_is_default_ram(memory_type) and kind is SectionKind.DATA:
431            continue
432
433        if kind in generate_sections and full_list_of_sections[kind]:
434            code_generation["copy_code"] += MEMCPY_TEMPLATE.format(memory_type.lower(), kind.value)
435            code_generation["extern"] += EXTERN_LINKER_VAR_DECLARATION.format(
436                memory_type.lower(), kind.value)
437
438    # BSS sections in main memory are automatically zeroed; others need to have
439    # zeroing code generated.
440    if (SectionKind.BSS in generate_sections
441        and full_list_of_sections[SectionKind.BSS]
442        and not region_is_default_ram(memory_type)
443    ):
444        code_generation["zero_code"] += MEMSET_TEMPLATE.format(memory_type.lower())
445        code_generation["extern"] += EXTERN_LINKER_VAR_DECLARATION.format(
446            memory_type.lower(), SectionKind.BSS.value)
447
448    return code_generation
449
450
451def dump_header_file(header_file, code_generation):
452    code_string = ''
453    # create a dummy void function if there is no code to generate for
454    # bss/data/text regions
455
456    code_string += code_generation["extern"]
457
458    if code_generation["copy_code"]:
459        code_string += DATA_COPY_FUNCTION.format(code_generation["copy_code"])
460    else:
461        code_string += DATA_COPY_FUNCTION.format("return;")
462    if code_generation["zero_code"]:
463        code_string += BSS_ZEROING_FUNCTION.format(code_generation["zero_code"])
464    else:
465        code_string += BSS_ZEROING_FUNCTION.format("return;")
466
467    with open(header_file, "w") as header_file_desc:
468        header_file_desc.write(SOURCE_CODE_INCLUDES)
469        header_file_desc.write(code_string)
470
471
472def parse_args():
473    global args
474    parser = argparse.ArgumentParser(
475        description=__doc__,
476        formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False)
477    parser.add_argument("-d", "--directory", required=True,
478                        help="obj file's directory")
479    parser.add_argument("-i", "--input_rel_dict", required=True, type=argparse.FileType('r'),
480                        help="input file with dict src:memory type(sram2 or ccm or aon etc)")
481    parser.add_argument("-o", "--output", required=False, help="Output ld file")
482    parser.add_argument("-s", "--output_sram_data", required=False,
483                        help="Output sram data ld file")
484    parser.add_argument("-b", "--output_sram_bss", required=False,
485                        help="Output sram bss ld file")
486    parser.add_argument("-c", "--output_code", required=False,
487                        help="Output relocation code header file")
488    parser.add_argument("-R", "--default_ram_region", default='SRAM',
489                        help="Name of default RAM memory region for system")
490    parser.add_argument("-v", "--verbose", action="count", default=0,
491                        help="Verbose Output")
492    args = parser.parse_args()
493
494
495# return the absolute path for the object file.
496def get_obj_filename(searchpath, filename):
497    # get the object file name which is almost always pended with .obj
498    obj_filename = filename.split("/")[-1] + ".obj"
499
500    for dirpath, _, files in os.walk(searchpath):
501        for filename1 in files:
502            if filename1 == obj_filename:
503                if filename.split("/")[-2] in dirpath.split("/")[-1]:
504                    fullname = os.path.join(dirpath, filename1)
505                    return fullname
506
507
508# Extracts all possible components for the input string:
509# <mem_region>[\ :program_header]:<flag_1>[;<flag_2>...]:<file_1>[;<file_2>...][,filter]
510# Returns a 5-tuple with them: (mem_region, program_header, flags, files, filter)
511# If no `program_header` is defined, returns an empty string
512# If no `filter` is defined, returns an empty string
513def parse_input_string(line):
514    # Be careful when splitting by : to avoid breaking absolute paths on Windows
515    mem_region, rest = line.split(':', 1)
516
517    phdr = ''
518    if mem_region.endswith(' '):
519        mem_region = mem_region.rstrip()
520        phdr, rest = rest.split(':', 1)
521
522    flag_list, rest = rest.split(':', 1)
523    flag_list = flag_list.split(';')
524
525
526    # Split file list by semicolons, in part to support generator expressions
527    file_list, symbol_filter = rest.split(',', 1)
528    file_list = file_list.split(';')
529
530    return mem_region, phdr, flag_list, file_list, symbol_filter
531
532
533# Create a dict with key as memory type and (files, symbol_filter) tuple
534# as a list of values.
535# Also, return another dict with program headers for memory regions
536def create_dict_wrt_mem():
537    # need to support wild card *
538    rel_dict = dict()
539    phdrs = dict()
540
541    input_rel_dict = args.input_rel_dict.read().splitlines()
542    if not input_rel_dict:
543        sys.exit("Disable CONFIG_CODE_DATA_RELOCATION if no file needs relocation")
544
545    for line in input_rel_dict:
546        if ':' not in line:
547            continue
548
549        mem_region, phdr, flag_list, file_list, symbol_filter = parse_input_string(line)
550
551        # Handle any program header
552        if phdr != '':
553            phdrs[mem_region] = f':{phdr}'
554
555        file_name_list = []
556        # Use glob matching on each file in the list
557        for file_glob in file_list:
558            glob_results = glob.glob(file_glob)
559            if not glob_results:
560                warnings.warn("File: "+file_glob+" Not found")
561                continue
562            elif len(glob_results) > 1:
563                warnings.warn("Regex in file lists is deprecated, please use file(GLOB) instead")
564            file_name_list.extend(glob_results)
565        if len(file_name_list) == 0:
566            continue
567        if mem_region == '':
568            continue
569        if args.verbose:
570            print("Memory region ", mem_region, " Selected for files:", file_name_list)
571
572        # Apply filter on files
573        file_name_filter_list = [(f, symbol_filter) for f in file_name_list]
574
575        mem_region = "|".join((mem_region, *flag_list))
576
577        if mem_region in rel_dict:
578            rel_dict[mem_region].extend(file_name_filter_list)
579        else:
580            rel_dict[mem_region] = file_name_filter_list
581
582    return rel_dict, phdrs
583
584
585def main():
586    global mpu_align
587    mpu_align = {}
588    parse_args()
589    searchpath = args.directory
590    linker_file = args.output
591    sram_data_linker_file = args.output_sram_data
592    sram_bss_linker_file = args.output_sram_bss
593    rel_dict, phdrs = create_dict_wrt_mem()
594    complete_list_of_sections: 'dict[MemoryRegion, dict[SectionKind, list[OutputSection]]]' \
595        = defaultdict(lambda: defaultdict(list))
596
597    # Create/or truncate file contents if it already exists
598    # raw = open(linker_file, "w")
599
600    # for each memory_type, create text/rodata/data/bss sections for all obj files
601    for memory_type, files in rel_dict.items():
602        full_list_of_sections: 'dict[SectionKind, list[OutputSection]]' = defaultdict(list)
603
604        for filename, symbol_filter in files:
605            obj_filename = get_obj_filename(searchpath, filename)
606            # the obj file wasn't found. Probably not compiled.
607            if not obj_filename:
608                continue
609
610            file_sections = find_sections(obj_filename, symbol_filter)
611            # Merge sections from file into collection of sections for all files
612            for category, sections in file_sections.items():
613                full_list_of_sections[category].extend(sections)
614
615        # cleanup and attach the sections to the memory type after cleanup.
616        sections_by_category = assign_to_correct_mem_region(memory_type, full_list_of_sections)
617        for (region, section_category_map) in sections_by_category.items():
618            for (category, sections) in section_category_map.items():
619                complete_list_of_sections[region][category].extend(sections)
620
621    generate_linker_script(linker_file, sram_data_linker_file,
622                           sram_bss_linker_file, complete_list_of_sections, phdrs)
623
624    code_generation = {"copy_code": '', "zero_code": '', "extern": ''}
625    for mem_type, list_of_sections in sorted(complete_list_of_sections.items()):
626
627        if "|COPY" in mem_type:
628            mem_type = mem_type.split("|", 1)[0]
629            code_generation = generate_memcpy_code(mem_type,
630                                               list_of_sections, code_generation)
631
632    dump_header_file(args.output_code, code_generation)
633
634
635if __name__ == '__main__':
636    main()
637