1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2016 Google, Inc
3# Written by Simon Glass <sjg@chromium.org>
4#
5# Handle various things related to ELF images
6#
7
8from collections import namedtuple, OrderedDict
9import io
10import os
11import re
12import shutil
13import struct
14import tempfile
15
16from patman import command
17from patman import tools
18from patman import tout
19
20ELF_TOOLS = True
21try:
22    from elftools.elf.elffile import ELFFile
23    from elftools.elf.sections import SymbolTableSection
24except:  # pragma: no cover
25    ELF_TOOLS = False
26
27Symbol = namedtuple('Symbol', ['section', 'address', 'size', 'weak'])
28
29# Information about an ELF file:
30#    data: Extracted program contents of ELF file (this would be loaded by an
31#           ELF loader when reading this file
32#    load: Load address of code
33#    entry: Entry address of code
34#    memsize: Number of bytes in memory occupied by loading this ELF file
35ElfInfo = namedtuple('ElfInfo', ['data', 'load', 'entry', 'memsize'])
36
37
38def GetSymbols(fname, patterns):
39    """Get the symbols from an ELF file
40
41    Args:
42        fname: Filename of the ELF file to read
43        patterns: List of regex patterns to search for, each a string
44
45    Returns:
46        None, if the file does not exist, or Dict:
47          key: Name of symbol
48          value: Hex value of symbol
49    """
50    stdout = tools.Run('objdump', '-t', fname)
51    lines = stdout.splitlines()
52    if patterns:
53        re_syms = re.compile('|'.join(patterns))
54    else:
55        re_syms = None
56    syms = {}
57    syms_started = False
58    for line in lines:
59        if not line or not syms_started:
60            if 'SYMBOL TABLE' in line:
61                syms_started = True
62            line = None  # Otherwise code coverage complains about 'continue'
63            continue
64        if re_syms and not re_syms.search(line):
65            continue
66
67        space_pos = line.find(' ')
68        value, rest = line[:space_pos], line[space_pos + 1:]
69        flags = rest[:7]
70        parts = rest[7:].split()
71        section, size =  parts[:2]
72        if len(parts) > 2:
73            name = parts[2] if parts[2] != '.hidden' else parts[3]
74            syms[name] = Symbol(section, int(value, 16), int(size,16),
75                                flags[1] == 'w')
76
77    # Sort dict by address
78    return OrderedDict(sorted(syms.items(), key=lambda x: x[1].address))
79
80def GetSymbolAddress(fname, sym_name):
81    """Get a value of a symbol from an ELF file
82
83    Args:
84        fname: Filename of the ELF file to read
85        patterns: List of regex patterns to search for, each a string
86
87    Returns:
88        Symbol value (as an integer) or None if not found
89    """
90    syms = GetSymbols(fname, [sym_name])
91    sym = syms.get(sym_name)
92    if not sym:
93        return None
94    return sym.address
95
96def LookupAndWriteSymbols(elf_fname, entry, section):
97    """Replace all symbols in an entry with their correct values
98
99    The entry contents is updated so that values for referenced symbols will be
100    visible at run time. This is done by finding out the symbols offsets in the
101    entry (using the ELF file) and replacing them with values from binman's data
102    structures.
103
104    Args:
105        elf_fname: Filename of ELF image containing the symbol information for
106            entry
107        entry: Entry to process
108        section: Section which can be used to lookup symbol values
109    """
110    fname = tools.GetInputFilename(elf_fname)
111    syms = GetSymbols(fname, ['image', 'binman'])
112    if not syms:
113        return
114    base = syms.get('__image_copy_start')
115    if not base:
116        return
117    for name, sym in syms.items():
118        if name.startswith('_binman'):
119            msg = ("Section '%s': Symbol '%s'\n   in entry '%s'" %
120                   (section.GetPath(), name, entry.GetPath()))
121            offset = sym.address - base.address
122            if offset < 0 or offset + sym.size > entry.contents_size:
123                raise ValueError('%s has offset %x (size %x) but the contents '
124                                 'size is %x' % (entry.GetPath(), offset,
125                                                 sym.size, entry.contents_size))
126            if sym.size == 4:
127                pack_string = '<I'
128            elif sym.size == 8:
129                pack_string = '<Q'
130            else:
131                raise ValueError('%s has size %d: only 4 and 8 are supported' %
132                                 (msg, sym.size))
133
134            # Look up the symbol in our entry tables.
135            value = section.GetImage().LookupImageSymbol(name, sym.weak, msg,
136                                                         base.address)
137            if value is None:
138                value = -1
139                pack_string = pack_string.lower()
140            value_bytes = struct.pack(pack_string, value)
141            tout.Debug('%s:\n   insert %s, offset %x, value %x, length %d' %
142                       (msg, name, offset, value, len(value_bytes)))
143            entry.data = (entry.data[:offset] + value_bytes +
144                        entry.data[offset + sym.size:])
145
146def MakeElf(elf_fname, text, data):
147    """Make an elf file with the given data in a single section
148
149    The output file has a several section including '.text' and '.data',
150    containing the info provided in arguments.
151
152    Args:
153        elf_fname: Output filename
154        text: Text (code) to put in the file's .text section
155        data: Data to put in the file's .data section
156    """
157    outdir = tempfile.mkdtemp(prefix='binman.elf.')
158    s_file = os.path.join(outdir, 'elf.S')
159
160    # Spilt the text into two parts so that we can make the entry point two
161    # bytes after the start of the text section
162    text_bytes1 = ['\t.byte\t%#x' % byte for byte in text[:2]]
163    text_bytes2 = ['\t.byte\t%#x' % byte for byte in text[2:]]
164    data_bytes = ['\t.byte\t%#x' % byte for byte in data]
165    with open(s_file, 'w') as fd:
166        print('''/* Auto-generated C program to produce an ELF file for testing */
167
168.section .text
169.code32
170.globl _start
171.type _start, @function
172%s
173_start:
174%s
175.ident "comment"
176
177.comm fred,8,4
178
179.section .empty
180.globl _empty
181_empty:
182.byte 1
183
184.globl ernie
185.data
186.type ernie, @object
187.size ernie, 4
188ernie:
189%s
190''' % ('\n'.join(text_bytes1), '\n'.join(text_bytes2), '\n'.join(data_bytes)),
191        file=fd)
192    lds_file = os.path.join(outdir, 'elf.lds')
193
194    # Use a linker script to set the alignment and text address.
195    with open(lds_file, 'w') as fd:
196        print('''/* Auto-generated linker script to produce an ELF file for testing */
197
198PHDRS
199{
200    text PT_LOAD ;
201    data PT_LOAD ;
202    empty PT_LOAD FLAGS ( 6 ) ;
203    note PT_NOTE ;
204}
205
206SECTIONS
207{
208    . = 0xfef20000;
209    ENTRY(_start)
210    .text . : SUBALIGN(0)
211    {
212        *(.text)
213    } :text
214    .data : {
215        *(.data)
216    } :data
217    _bss_start = .;
218    .empty : {
219        *(.empty)
220    } :empty
221    /DISCARD/ : {
222        *(.note.gnu.property)
223    }
224    .note : {
225        *(.comment)
226    } :note
227    .bss _bss_start  (OVERLAY) : {
228        *(.bss)
229    }
230}
231''', file=fd)
232    # -static: Avoid requiring any shared libraries
233    # -nostdlib: Don't link with C library
234    # -Wl,--build-id=none: Don't generate a build ID, so that we just get the
235    #   text section at the start
236    # -m32: Build for 32-bit x86
237    # -T...: Specifies the link script, which sets the start address
238    cc, args = tools.GetTargetCompileTool('cc')
239    args += ['-static', '-nostdlib', '-Wl,--build-id=none', '-m32', '-T',
240            lds_file, '-o', elf_fname, s_file]
241    stdout = command.Output(cc, *args)
242    shutil.rmtree(outdir)
243
244def DecodeElf(data, location):
245    """Decode an ELF file and return information about it
246
247    Args:
248        data: Data from ELF file
249        location: Start address of data to return
250
251    Returns:
252        ElfInfo object containing information about the decoded ELF file
253    """
254    file_size = len(data)
255    with io.BytesIO(data) as fd:
256        elf = ELFFile(fd)
257        data_start = 0xffffffff;
258        data_end = 0;
259        mem_end = 0;
260        virt_to_phys = 0;
261
262        for i in range(elf.num_segments()):
263            segment = elf.get_segment(i)
264            if segment['p_type'] != 'PT_LOAD' or not segment['p_memsz']:
265                skipped = 1  # To make code-coverage see this line
266                continue
267            start = segment['p_paddr']
268            mend = start + segment['p_memsz']
269            rend = start + segment['p_filesz']
270            data_start = min(data_start, start)
271            data_end = max(data_end, rend)
272            mem_end = max(mem_end, mend)
273            if not virt_to_phys:
274                virt_to_phys = segment['p_paddr'] - segment['p_vaddr']
275
276        output = bytearray(data_end - data_start)
277        for i in range(elf.num_segments()):
278            segment = elf.get_segment(i)
279            if segment['p_type'] != 'PT_LOAD' or not segment['p_memsz']:
280                skipped = 1  # To make code-coverage see this line
281                continue
282            start = segment['p_paddr']
283            offset = 0
284            if start < location:
285                offset = location - start
286                start = location
287            # A legal ELF file can have a program header with non-zero length
288            # but zero-length file size and a non-zero offset which, added
289            # together, are greater than input->size (i.e. the total file size).
290            #  So we need to not even test in the case that p_filesz is zero.
291            # Note: All of this code is commented out since we don't have a test
292            # case for it.
293            size = segment['p_filesz']
294            #if not size:
295                #continue
296            #end = segment['p_offset'] + segment['p_filesz']
297            #if end > file_size:
298                #raise ValueError('Underflow copying out the segment. File has %#x bytes left, segment end is %#x\n',
299                                 #file_size, end)
300            output[start - data_start:start - data_start + size] = (
301                segment.data()[offset:])
302    return ElfInfo(output, data_start, elf.header['e_entry'] + virt_to_phys,
303                   mem_end - data_start)
304