binman: Handle writing ELF symbols in the Entry class
[platform/kernel/u-boot.git] / tools / binman / image.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2016 Google, Inc
3 # Written by Simon Glass <sjg@chromium.org>
4 #
5 # Class for an image, the output of binman
6 #
7
8 from collections import OrderedDict
9 import fnmatch
10 from operator import attrgetter
11 import os
12 import re
13 import sys
14
15 from binman.entry import Entry
16 from binman.etype import fdtmap
17 from binman.etype import image_header
18 from binman.etype import section
19 from dtoc import fdt
20 from dtoc import fdt_util
21 from patman import tools
22 from patman import tout
23
24 class Image(section.Entry_section):
25     """A Image, representing an output from binman
26
27     An image is comprised of a collection of entries each containing binary
28     data. The image size must be large enough to hold all of this data.
29
30     This class implements the various operations needed for images.
31
32     Attributes:
33         filename: Output filename for image
34         image_node: Name of node containing the description for this image
35         fdtmap_dtb: Fdt object for the fdtmap when loading from a file
36         fdtmap_data: Contents of the fdtmap when loading from a file
37         allow_repack: True to add properties to allow the image to be safely
38             repacked later
39         test_section_timeout: Use a zero timeout for section multi-threading
40             (for testing)
41         symlink: Name of symlink to image
42
43     Args:
44         copy_to_orig: Copy offset/size to orig_offset/orig_size after reading
45             from the device tree
46         test: True if this is being called from a test of Images. This this case
47             there is no device tree defining the structure of the section, so
48             we create a section manually.
49         ignore_missing: Ignore any missing entry arguments (i.e. don't raise an
50             exception). This should be used if the Image is being loaded from
51             a file rather than generated. In that case we obviously don't need
52             the entry arguments since the contents already exists.
53         use_expanded: True if we are updating the FDT wth entry offsets, etc.
54             and should use the expanded versions of the U-Boot entries.
55             Any entry type that includes a devicetree must put it in a
56             separate entry so that it will be updated. For example. 'u-boot'
57             normally just picks up 'u-boot.bin' which includes the
58             devicetree, but this is not updateable, since it comes into
59             binman as one piece and binman doesn't know that it is actually
60             an executable followed by a devicetree. Of course it could be
61             taught this, but then when reading an image (e.g. 'binman ls')
62             it may need to be able to split the devicetree out of the image
63             in order to determine the location of things. Instead we choose
64             to ignore 'u-boot-bin' in this case, and build it ourselves in
65             binman with 'u-boot-dtb.bin' and 'u-boot.dtb'. See
66             Entry_u_boot_expanded and Entry_blob_phase for details.
67         missing_etype: Use a default entry type ('blob') if the requested one
68             does not exist in binman. This is useful if an image was created by
69             binman a newer version of binman but we want to list it in an older
70             version which does not support all the entry types.
71         generate: If true, generator nodes are processed. If false they are
72             ignored which is useful when an existing image is read back from a
73             file.
74     """
75     def __init__(self, name, node, copy_to_orig=True, test=False,
76                  ignore_missing=False, use_expanded=False, missing_etype=False,
77                  generate=True):
78         super().__init__(None, 'section', node, test=test)
79         self.copy_to_orig = copy_to_orig
80         self.name = 'main-section'
81         self.image_name = name
82         self._filename = '%s.bin' % self.image_name
83         self.fdtmap_dtb = None
84         self.fdtmap_data = None
85         self.allow_repack = False
86         self._ignore_missing = ignore_missing
87         self.missing_etype = missing_etype
88         self.use_expanded = use_expanded
89         self.test_section_timeout = False
90         self.bintools = {}
91         self.generate = generate
92         if not test:
93             self.ReadNode()
94
95     def ReadNode(self):
96         super().ReadNode()
97         filename = fdt_util.GetString(self._node, 'filename')
98         if filename:
99             self._filename = filename
100         self.allow_repack = fdt_util.GetBool(self._node, 'allow-repack')
101         self._symlink = fdt_util.GetString(self._node, 'symlink')
102
103     @classmethod
104     def FromFile(cls, fname):
105         """Convert an image file into an Image for use in binman
106
107         Args:
108             fname: Filename of image file to read
109
110         Returns:
111             Image object on success
112
113         Raises:
114             ValueError if something goes wrong
115         """
116         data = tools.read_file(fname)
117         size = len(data)
118
119         # First look for an image header
120         pos = image_header.LocateHeaderOffset(data)
121         if pos is None:
122             # Look for the FDT map
123             pos = fdtmap.LocateFdtmap(data)
124         if pos is None:
125             raise ValueError('Cannot find FDT map in image')
126
127         # We don't know the FDT size, so check its header first
128         probe_dtb = fdt.Fdt.FromData(
129             data[pos + fdtmap.FDTMAP_HDR_LEN:pos + 256])
130         dtb_size = probe_dtb.GetFdtObj().totalsize()
131         fdtmap_data = data[pos:pos + dtb_size + fdtmap.FDTMAP_HDR_LEN]
132         fdt_data = fdtmap_data[fdtmap.FDTMAP_HDR_LEN:]
133         out_fname = tools.get_output_filename('fdtmap.in.dtb')
134         tools.write_file(out_fname, fdt_data)
135         dtb = fdt.Fdt(out_fname)
136         dtb.Scan()
137
138         # Return an Image with the associated nodes
139         root = dtb.GetRoot()
140         image = Image('image', root, copy_to_orig=False, ignore_missing=True,
141                       missing_etype=True, generate=False)
142
143         image.image_node = fdt_util.GetString(root, 'image-node', 'image')
144         image.fdtmap_dtb = dtb
145         image.fdtmap_data = fdtmap_data
146         image._data = data
147         image._filename = fname
148         image.image_name, _ = os.path.splitext(fname)
149         return image
150
151     def Raise(self, msg):
152         """Convenience function to raise an error referencing an image"""
153         raise ValueError("Image '%s': %s" % (self._node.path, msg))
154
155     def PackEntries(self):
156         """Pack all entries into the image"""
157         super().Pack(0)
158
159     def SetImagePos(self):
160         # This first section in the image so it starts at 0
161         super().SetImagePos(0)
162
163     def ProcessEntryContents(self):
164         """Call the ProcessContents() method for each entry
165
166         This is intended to adjust the contents as needed by the entry type.
167
168         Returns:
169             True if the new data size is OK, False if expansion is needed
170         """
171         return super().ProcessContents()
172
173     def WriteSymbols(self):
174         """Write symbol values into binary files for access at run time"""
175         super().WriteSymbols(self)
176
177     def BuildImage(self):
178         """Write the image to a file"""
179         fname = tools.get_output_filename(self._filename)
180         tout.info("Writing image to '%s'" % fname)
181         with open(fname, 'wb') as fd:
182             data = self.GetPaddedData()
183             fd.write(data)
184         tout.info("Wrote %#x bytes" % len(data))
185         # Create symlink to file if symlink given
186         if self._symlink is not None:
187             sname = tools.get_output_filename(self._symlink)
188             os.symlink(fname, sname)
189
190     def WriteMap(self):
191         """Write a map of the image to a .map file
192
193         Returns:
194             Filename of map file written
195         """
196         filename = '%s.map' % self.image_name
197         fname = tools.get_output_filename(filename)
198         with open(fname, 'w') as fd:
199             print('%8s  %8s  %8s  %s' % ('ImagePos', 'Offset', 'Size', 'Name'),
200                   file=fd)
201             super().WriteMap(fd, 0)
202         return fname
203
204     def BuildEntryList(self):
205         """List the files in an image
206
207         Returns:
208             List of entry.EntryInfo objects describing all entries in the image
209         """
210         entries = []
211         self.ListEntries(entries, 0)
212         return entries
213
214     def FindEntryPath(self, entry_path):
215         """Find an entry at a given path in the image
216
217         Args:
218             entry_path: Path to entry (e.g. /ro-section/u-boot')
219
220         Returns:
221             Entry object corresponding to that past
222
223         Raises:
224             ValueError if no entry found
225         """
226         parts = entry_path.split('/')
227         entries = self.GetEntries()
228         parent = '/'
229         for part in parts:
230             entry = entries.get(part)
231             if not entry:
232                 raise ValueError("Entry '%s' not found in '%s'" %
233                                  (part, parent))
234             parent = entry.GetPath()
235             entries = entry.GetEntries()
236         return entry
237
238     def ReadData(self, decomp=True, alt_format=None):
239         tout.debug("Image '%s' ReadData(), size=%#x" %
240                    (self.GetPath(), len(self._data)))
241         return self._data
242
243     def GetListEntries(self, entry_paths):
244         """List the entries in an image
245
246         This decodes the supplied image and returns a list of entries from that
247         image, preceded by a header.
248
249         Args:
250             entry_paths: List of paths to match (each can have wildcards). Only
251                 entries whose names match one of these paths will be printed
252
253         Returns:
254             String error message if something went wrong, otherwise
255             3-Tuple:
256                 List of EntryInfo objects
257                 List of lines, each
258                     List of text columns, each a string
259                 List of widths of each column
260         """
261         def _EntryToStrings(entry):
262             """Convert an entry to a list of strings, one for each column
263
264             Args:
265                 entry: EntryInfo object containing information to output
266
267             Returns:
268                 List of strings, one for each field in entry
269             """
270             def _AppendHex(val):
271                 """Append a hex value, or an empty string if val is None
272
273                 Args:
274                     val: Integer value, or None if none
275                 """
276                 args.append('' if val is None else '>%x' % val)
277
278             args = ['  ' * entry.indent + entry.name]
279             _AppendHex(entry.image_pos)
280             _AppendHex(entry.size)
281             args.append(entry.etype)
282             _AppendHex(entry.offset)
283             _AppendHex(entry.uncomp_size)
284             return args
285
286         def _DoLine(lines, line):
287             """Add a line to the output list
288
289             This adds a line (a list of columns) to the output list. It also updates
290             the widths[] array with the maximum width of each column
291
292             Args:
293                 lines: List of lines to add to
294                 line: List of strings, one for each column
295             """
296             for i, item in enumerate(line):
297                 widths[i] = max(widths[i], len(item))
298             lines.append(line)
299
300         def _NameInPaths(fname, entry_paths):
301             """Check if a filename is in a list of wildcarded paths
302
303             Args:
304                 fname: Filename to check
305                 entry_paths: List of wildcarded paths (e.g. ['*dtb*', 'u-boot*',
306                                                              'section/u-boot'])
307
308             Returns:
309                 True if any wildcard matches the filename (using Unix filename
310                     pattern matching, not regular expressions)
311                 False if not
312             """
313             for path in entry_paths:
314                 if fnmatch.fnmatch(fname, path):
315                     return True
316             return False
317
318         entries = self.BuildEntryList()
319
320         # This is our list of lines. Each item in the list is a list of strings, one
321         # for each column
322         lines = []
323         HEADER = ['Name', 'Image-pos', 'Size', 'Entry-type', 'Offset',
324                   'Uncomp-size']
325         num_columns = len(HEADER)
326
327         # This records the width of each column, calculated as the maximum width of
328         # all the strings in that column
329         widths = [0] * num_columns
330         _DoLine(lines, HEADER)
331
332         # We won't print anything unless it has at least this indent. So at the
333         # start we will print nothing, unless a path matches (or there are no
334         # entry paths)
335         MAX_INDENT = 100
336         min_indent = MAX_INDENT
337         path_stack = []
338         path = ''
339         indent = 0
340         selected_entries = []
341         for entry in entries:
342             if entry.indent > indent:
343                 path_stack.append(path)
344             elif entry.indent < indent:
345                 path_stack.pop()
346             if path_stack:
347                 path = path_stack[-1] + '/' + entry.name
348             indent = entry.indent
349
350             # If there are entry paths to match and we are not looking at a
351             # sub-entry of a previously matched entry, we need to check the path
352             if entry_paths and indent <= min_indent:
353                 if _NameInPaths(path[1:], entry_paths):
354                     # Print this entry and all sub-entries (=higher indent)
355                     min_indent = indent
356                 else:
357                     # Don't print this entry, nor any following entries until we get
358                     # a path match
359                     min_indent = MAX_INDENT
360                     continue
361             _DoLine(lines, _EntryToStrings(entry))
362             selected_entries.append(entry)
363         return selected_entries, lines, widths
364
365     def LookupImageSymbol(self, sym_name, optional, msg, base_addr):
366         """Look up a symbol in an ELF file
367
368         Looks up a symbol in an ELF file. Only entry types which come from an
369         ELF image can be used by this function.
370
371         This searches through this image including all of its subsections.
372
373         At present the only entry properties supported are:
374             offset
375             image_pos - 'base_addr' is added if this is not an end-at-4gb image
376             size
377
378         Args:
379             sym_name: Symbol name in the ELF file to look up in the format
380                 _binman_<entry>_prop_<property> where <entry> is the name of
381                 the entry and <property> is the property to find (e.g.
382                 _binman_u_boot_prop_offset). As a special case, you can append
383                 _any to <entry> to have it search for any matching entry. E.g.
384                 _binman_u_boot_any_prop_offset will match entries called u-boot,
385                 u-boot-img and u-boot-nodtb)
386             optional: True if the symbol is optional. If False this function
387                 will raise if the symbol is not found
388             msg: Message to display if an error occurs
389             base_addr: Base address of image. This is added to the returned
390                 image_pos in most cases so that the returned position indicates
391                 where the targeted entry/binary has actually been loaded. But
392                 if end-at-4gb is used, this is not done, since the binary is
393                 already assumed to be linked to the ROM position and using
394                 execute-in-place (XIP).
395
396         Returns:
397             Value that should be assigned to that symbol, or None if it was
398                 optional and not found
399
400         Raises:
401             ValueError if the symbol is invalid or not found, or references a
402                 property which is not supported
403         """
404         entries = OrderedDict()
405         entries_by_name = {}
406         self._CollectEntries(entries, entries_by_name, self)
407         return self.LookupSymbol(sym_name, optional, msg, base_addr,
408                                  entries_by_name)
409
410     def CollectBintools(self):
411         """Collect all the bintools used by this image
412
413         Returns:
414             Dict of bintools:
415                 key: name of tool
416                 value: Bintool object
417         """
418         bintools = {}
419         super().AddBintools(bintools)
420         self.bintools = bintools
421         return bintools