Merge tag 'v2022.04-rc5' into next
[platform/kernel/u-boot.git] / tools / binman / elf.py
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
8 from collections import namedtuple, OrderedDict
9 import io
10 import os
11 import re
12 import shutil
13 import struct
14 import tempfile
15
16 from patman import command
17 from patman import tools
18 from patman import tout
19
20 ELF_TOOLS = True
21 try:
22     from elftools.elf.elffile import ELFFile
23     from elftools.elf.elffile import ELFError
24     from elftools.elf.sections import SymbolTableSection
25 except:  # pragma: no cover
26     ELF_TOOLS = False
27
28 # Information about an EFL symbol:
29 # section (str): Name of the section containing this symbol
30 # address (int): Address of the symbol (its value)
31 # size (int): Size of the symbol in bytes
32 # weak (bool): True if the symbol is weak
33 # offset (int or None): Offset of the symbol's data in the ELF file, or None if
34 #   not known
35 Symbol = namedtuple('Symbol', ['section', 'address', 'size', 'weak', 'offset'])
36
37 # Information about an ELF file:
38 #    data: Extracted program contents of ELF file (this would be loaded by an
39 #           ELF loader when reading this file
40 #    load: Load address of code
41 #    entry: Entry address of code
42 #    memsize: Number of bytes in memory occupied by loading this ELF file
43 ElfInfo = namedtuple('ElfInfo', ['data', 'load', 'entry', 'memsize'])
44
45
46 def GetSymbols(fname, patterns):
47     """Get the symbols from an ELF file
48
49     Args:
50         fname: Filename of the ELF file to read
51         patterns: List of regex patterns to search for, each a string
52
53     Returns:
54         None, if the file does not exist, or Dict:
55           key: Name of symbol
56           value: Hex value of symbol
57     """
58     stdout = tools.run('objdump', '-t', fname)
59     lines = stdout.splitlines()
60     if patterns:
61         re_syms = re.compile('|'.join(patterns))
62     else:
63         re_syms = None
64     syms = {}
65     syms_started = False
66     for line in lines:
67         if not line or not syms_started:
68             if 'SYMBOL TABLE' in line:
69                 syms_started = True
70             line = None  # Otherwise code coverage complains about 'continue'
71             continue
72         if re_syms and not re_syms.search(line):
73             continue
74
75         space_pos = line.find(' ')
76         value, rest = line[:space_pos], line[space_pos + 1:]
77         flags = rest[:7]
78         parts = rest[7:].split()
79         section, size =  parts[:2]
80         if len(parts) > 2:
81             name = parts[2] if parts[2] != '.hidden' else parts[3]
82             syms[name] = Symbol(section, int(value, 16), int(size, 16),
83                                 flags[1] == 'w', None)
84
85     # Sort dict by address
86     return OrderedDict(sorted(syms.items(), key=lambda x: x[1].address))
87
88 def _GetFileOffset(elf, addr):
89     """Get the file offset for an address
90
91     Args:
92         elf (ELFFile): ELF file to check
93         addr (int): Address to search for
94
95     Returns
96         int: Offset of that address in the ELF file, or None if not valid
97     """
98     for seg in elf.iter_segments():
99         seg_end = seg['p_vaddr'] + seg['p_filesz']
100         if seg.header['p_type'] == 'PT_LOAD':
101             if addr >= seg['p_vaddr'] and addr < seg_end:
102                 return addr - seg['p_vaddr'] + seg['p_offset']
103
104 def GetFileOffset(fname, addr):
105     """Get the file offset for an address
106
107     Args:
108         fname (str): Filename of ELF file to check
109         addr (int): Address to search for
110
111     Returns
112         int: Offset of that address in the ELF file, or None if not valid
113     """
114     if not ELF_TOOLS:
115         raise ValueError("Python: No module named 'elftools'")
116     with open(fname, 'rb') as fd:
117         elf = ELFFile(fd)
118         return _GetFileOffset(elf, addr)
119
120 def GetSymbolFromAddress(fname, addr):
121     """Get the symbol at a particular address
122
123     Args:
124         fname (str): Filename of ELF file to check
125         addr (int): Address to search for
126
127     Returns:
128         str: Symbol name, or None if no symbol at that address
129     """
130     if not ELF_TOOLS:
131         raise ValueError("Python: No module named 'elftools'")
132     with open(fname, 'rb') as fd:
133         elf = ELFFile(fd)
134         syms = GetSymbols(fname, None)
135     for name, sym in syms.items():
136         if sym.address == addr:
137             return name
138
139 def GetSymbolFileOffset(fname, patterns):
140     """Get the symbols from an ELF file
141
142     Args:
143         fname: Filename of the ELF file to read
144         patterns: List of regex patterns to search for, each a string
145
146     Returns:
147         None, if the file does not exist, or Dict:
148           key: Name of symbol
149           value: Hex value of symbol
150     """
151     if not ELF_TOOLS:
152         raise ValueError("Python: No module named 'elftools'")
153
154     syms = {}
155     with open(fname, 'rb') as fd:
156         elf = ELFFile(fd)
157
158         re_syms = re.compile('|'.join(patterns))
159         for section in elf.iter_sections():
160             if isinstance(section, SymbolTableSection):
161                 for symbol in section.iter_symbols():
162                     if not re_syms or re_syms.search(symbol.name):
163                         addr = symbol.entry['st_value']
164                         syms[symbol.name] = Symbol(
165                             section.name, addr, symbol.entry['st_size'],
166                             symbol.entry['st_info']['bind'] == 'STB_WEAK',
167                             _GetFileOffset(elf, addr))
168
169     # Sort dict by address
170     return OrderedDict(sorted(syms.items(), key=lambda x: x[1].address))
171
172 def GetSymbolAddress(fname, sym_name):
173     """Get a value of a symbol from an ELF file
174
175     Args:
176         fname: Filename of the ELF file to read
177         patterns: List of regex patterns to search for, each a string
178
179     Returns:
180         Symbol value (as an integer) or None if not found
181     """
182     syms = GetSymbols(fname, [sym_name])
183     sym = syms.get(sym_name)
184     if not sym:
185         return None
186     return sym.address
187
188 def LookupAndWriteSymbols(elf_fname, entry, section):
189     """Replace all symbols in an entry with their correct values
190
191     The entry contents is updated so that values for referenced symbols will be
192     visible at run time. This is done by finding out the symbols offsets in the
193     entry (using the ELF file) and replacing them with values from binman's data
194     structures.
195
196     Args:
197         elf_fname: Filename of ELF image containing the symbol information for
198             entry
199         entry: Entry to process
200         section: Section which can be used to lookup symbol values
201     """
202     fname = tools.get_input_filename(elf_fname)
203     syms = GetSymbols(fname, ['image', 'binman'])
204     if not syms:
205         return
206     base = syms.get('__image_copy_start')
207     if not base:
208         return
209     for name, sym in syms.items():
210         if name.startswith('_binman'):
211             msg = ("Section '%s': Symbol '%s'\n   in entry '%s'" %
212                    (section.GetPath(), name, entry.GetPath()))
213             offset = sym.address - base.address
214             if offset < 0 or offset + sym.size > entry.contents_size:
215                 raise ValueError('%s has offset %x (size %x) but the contents '
216                                  'size is %x' % (entry.GetPath(), offset,
217                                                  sym.size, entry.contents_size))
218             if sym.size == 4:
219                 pack_string = '<I'
220             elif sym.size == 8:
221                 pack_string = '<Q'
222             else:
223                 raise ValueError('%s has size %d: only 4 and 8 are supported' %
224                                  (msg, sym.size))
225
226             # Look up the symbol in our entry tables.
227             value = section.GetImage().LookupImageSymbol(name, sym.weak, msg,
228                                                          base.address)
229             if value is None:
230                 value = -1
231                 pack_string = pack_string.lower()
232             value_bytes = struct.pack(pack_string, value)
233             tout.debug('%s:\n   insert %s, offset %x, value %x, length %d' %
234                        (msg, name, offset, value, len(value_bytes)))
235             entry.data = (entry.data[:offset] + value_bytes +
236                         entry.data[offset + sym.size:])
237
238 def MakeElf(elf_fname, text, data):
239     """Make an elf file with the given data in a single section
240
241     The output file has a several section including '.text' and '.data',
242     containing the info provided in arguments.
243
244     Args:
245         elf_fname: Output filename
246         text: Text (code) to put in the file's .text section
247         data: Data to put in the file's .data section
248     """
249     outdir = tempfile.mkdtemp(prefix='binman.elf.')
250     s_file = os.path.join(outdir, 'elf.S')
251
252     # Spilt the text into two parts so that we can make the entry point two
253     # bytes after the start of the text section
254     text_bytes1 = ['\t.byte\t%#x' % byte for byte in text[:2]]
255     text_bytes2 = ['\t.byte\t%#x' % byte for byte in text[2:]]
256     data_bytes = ['\t.byte\t%#x' % byte for byte in data]
257     with open(s_file, 'w') as fd:
258         print('''/* Auto-generated C program to produce an ELF file for testing */
259
260 .section .text
261 .code32
262 .globl _start
263 .type _start, @function
264 %s
265 _start:
266 %s
267 .ident "comment"
268
269 .comm fred,8,4
270
271 .section .empty
272 .globl _empty
273 _empty:
274 .byte 1
275
276 .globl ernie
277 .data
278 .type ernie, @object
279 .size ernie, 4
280 ernie:
281 %s
282 ''' % ('\n'.join(text_bytes1), '\n'.join(text_bytes2), '\n'.join(data_bytes)),
283         file=fd)
284     lds_file = os.path.join(outdir, 'elf.lds')
285
286     # Use a linker script to set the alignment and text address.
287     with open(lds_file, 'w') as fd:
288         print('''/* Auto-generated linker script to produce an ELF file for testing */
289
290 PHDRS
291 {
292     text PT_LOAD ;
293     data PT_LOAD ;
294     empty PT_LOAD FLAGS ( 6 ) ;
295     note PT_NOTE ;
296 }
297
298 SECTIONS
299 {
300     . = 0xfef20000;
301     ENTRY(_start)
302     .text . : SUBALIGN(0)
303     {
304         *(.text)
305     } :text
306     .data : {
307         *(.data)
308     } :data
309     _bss_start = .;
310     .empty : {
311         *(.empty)
312     } :empty
313     /DISCARD/ : {
314         *(.note.gnu.property)
315     }
316     .note : {
317         *(.comment)
318     } :note
319     .bss _bss_start  (OVERLAY) : {
320         *(.bss)
321     }
322 }
323 ''', file=fd)
324     # -static: Avoid requiring any shared libraries
325     # -nostdlib: Don't link with C library
326     # -Wl,--build-id=none: Don't generate a build ID, so that we just get the
327     #   text section at the start
328     # -m32: Build for 32-bit x86
329     # -T...: Specifies the link script, which sets the start address
330     cc, args = tools.get_target_compile_tool('cc')
331     args += ['-static', '-nostdlib', '-Wl,--build-id=none', '-m32', '-T',
332             lds_file, '-o', elf_fname, s_file]
333     stdout = command.output(cc, *args)
334     shutil.rmtree(outdir)
335
336 def DecodeElf(data, location):
337     """Decode an ELF file and return information about it
338
339     Args:
340         data: Data from ELF file
341         location: Start address of data to return
342
343     Returns:
344         ElfInfo object containing information about the decoded ELF file
345     """
346     file_size = len(data)
347     with io.BytesIO(data) as fd:
348         elf = ELFFile(fd)
349         data_start = 0xffffffff;
350         data_end = 0;
351         mem_end = 0;
352         virt_to_phys = 0;
353
354         for i in range(elf.num_segments()):
355             segment = elf.get_segment(i)
356             if segment['p_type'] != 'PT_LOAD' or not segment['p_memsz']:
357                 skipped = 1  # To make code-coverage see this line
358                 continue
359             start = segment['p_paddr']
360             mend = start + segment['p_memsz']
361             rend = start + segment['p_filesz']
362             data_start = min(data_start, start)
363             data_end = max(data_end, rend)
364             mem_end = max(mem_end, mend)
365             if not virt_to_phys:
366                 virt_to_phys = segment['p_paddr'] - segment['p_vaddr']
367
368         output = bytearray(data_end - data_start)
369         for i in range(elf.num_segments()):
370             segment = elf.get_segment(i)
371             if segment['p_type'] != 'PT_LOAD' or not segment['p_memsz']:
372                 skipped = 1  # To make code-coverage see this line
373                 continue
374             start = segment['p_paddr']
375             offset = 0
376             if start < location:
377                 offset = location - start
378                 start = location
379             # A legal ELF file can have a program header with non-zero length
380             # but zero-length file size and a non-zero offset which, added
381             # together, are greater than input->size (i.e. the total file size).
382             #  So we need to not even test in the case that p_filesz is zero.
383             # Note: All of this code is commented out since we don't have a test
384             # case for it.
385             size = segment['p_filesz']
386             #if not size:
387                 #continue
388             #end = segment['p_offset'] + segment['p_filesz']
389             #if end > file_size:
390                 #raise ValueError('Underflow copying out the segment. File has %#x bytes left, segment end is %#x\n',
391                                  #file_size, end)
392             output[start - data_start:start - data_start + size] = (
393                 segment.data()[offset:])
394     return ElfInfo(output, data_start, elf.header['e_entry'] + virt_to_phys,
395                    mem_end - data_start)
396
397 def UpdateFile(infile, outfile, start_sym, end_sym, insert):
398     tout.notice("Creating file '%s' with data length %#x (%d) between symbols '%s' and '%s'" %
399                 (outfile, len(insert), len(insert), start_sym, end_sym))
400     syms = GetSymbolFileOffset(infile, [start_sym, end_sym])
401     if len(syms) != 2:
402         raise ValueError("Expected two symbols '%s' and '%s': got %d: %s" %
403                          (start_sym, end_sym, len(syms),
404                           ','.join(syms.keys())))
405
406     size = syms[end_sym].offset - syms[start_sym].offset
407     if len(insert) > size:
408         raise ValueError("Not enough space in '%s' for data length %#x (%d); size is %#x (%d)" %
409                          (infile, len(insert), len(insert), size, size))
410
411     data = tools.read_file(infile)
412     newdata = data[:syms[start_sym].offset]
413     newdata += insert + tools.get_bytes(0, size - len(insert))
414     newdata += data[syms[end_sym].offset:]
415     tools.write_file(outfile, newdata)
416     tout.info('Written to offset %#x' % syms[start_sym].offset)
417
418 def read_loadable_segments(data):
419     """Read segments from an ELF file
420
421     Args:
422         data (bytes): Contents of file
423
424     Returns:
425         tuple:
426             list of segments, each:
427                 int: Segment number (0 = first)
428                 int: Start address of segment in memory
429                 bytes: Contents of segment
430             int: entry address for image
431
432     Raises:
433         ValueError: elftools is not available
434     """
435     if not ELF_TOOLS:
436         raise ValueError("Python: No module named 'elftools'")
437     with io.BytesIO(data) as inf:
438         try:
439             elf = ELFFile(inf)
440         except ELFError as err:
441             raise ValueError(err)
442         entry = elf.header['e_entry']
443         segments = []
444         for i in range(elf.num_segments()):
445             segment = elf.get_segment(i)
446             if segment['p_type'] != 'PT_LOAD' or not segment['p_memsz']:
447                 skipped = 1  # To make code-coverage see this line
448                 continue
449             start = segment['p_offset']
450             rend = start + segment['p_filesz']
451             segments.append((i, segment['p_paddr'], data[start:rend]))
452     return segments, entry