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