Home | History | Annotate | Download | only in utils
      1 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """ This module provides convenience routines to access Flash ROM (EEPROM)
      6 
      7 saft_flashrom_util is based on utility 'flashrom'.
      8 
      9 Original tool syntax:
     10     (read ) flashrom -r <file>
     11     (write) flashrom -l <layout_fn> [-i <image_name> ...] -w <file>
     12 
     13 The layout_fn is in format of
     14     address_begin:address_end image_name
     15     which defines a region between (address_begin, address_end) and can
     16     be accessed by the name image_name.
     17 
     18 Currently the tool supports multiple partial write but not partial read.
     19 
     20 In the saft_flashrom_util, we provide read and partial write abilities.
     21 For more information, see help(saft_flashrom_util.flashrom_util).
     22 """
     23 
     24 class TestError(Exception):
     25     pass
     26 
     27 
     28 class LayoutScraper(object):
     29     """Object of this class is used to retrieve layout from a BIOS file."""
     30 
     31     # The default conversion table for mosys.
     32     DEFAULT_CHROMEOS_FMAP_CONVERSION = {
     33         "Boot Stub": "FV_BSTUB",
     34         "GBB Area": "FV_GBB",
     35         "Recovery Firmware": "FVDEV",
     36         "RO VPD": "RO_VPD",
     37         "Firmware A Key": "VBOOTA",
     38         "Firmware A Data": "FVMAIN",
     39         "Firmware B Key": "VBOOTB",
     40         "Firmware B Data": "FVMAINB",
     41         "Log Volume": "FV_LOG",
     42         # New layout in Chrome OS Main Processor Firmware Specification,
     43         # used by all newer (>2011) platforms except Mario.
     44         "BOOT_STUB": "FV_BSTUB",
     45         "GBB": "FV_GBB",
     46         "RECOVERY": "FVDEV",
     47         "VBLOCK_A": "VBOOTA",
     48         "VBLOCK_B": "VBOOTB",
     49         "FW_MAIN_A": "FVMAIN",
     50         "FW_MAIN_B": "FVMAINB",
     51         # New sections in Depthcharge.
     52         "EC_MAIN_A": "ECMAINA",
     53         "EC_MAIN_B": "ECMAINB",
     54         # EC firmware layout
     55         "EC_RW": "EC_RW",
     56         }
     57 
     58     def __init__(self, os_if):
     59         self.image = None
     60         self.os_if = os_if
     61 
     62     def _get_text_layout(self, file_name):
     63         """Retrieve text layout from a firmware image file.
     64 
     65         This function uses the 'mosys' utility to scan the firmware image and
     66         retrieve the section layout information.
     67 
     68         The layout is reported as a set of lines with multiple
     69         "<name>"="value" pairs, all this output is passed to the caller.
     70         """
     71 
     72         mosys_cmd = 'mosys -k eeprom map %s' % file_name
     73         return self.os_if.run_shell_command_get_output(mosys_cmd)
     74 
     75     def _line_to_dictionary(self, line):
     76         """Convert a text layout line into a dictionary.
     77 
     78         Get a string consisting of single space separated "<name>"="value>"
     79         pairs and convert it into a dictionary where keys are the <name>
     80         fields, and values are the corresponding <value> fields.
     81 
     82         Return the dictionary to the caller.
     83         """
     84 
     85         rv = {}
     86 
     87         items = line.replace('" ', '"^').split('^')
     88         for item in items:
     89             pieces = item.split('=')
     90             if len(pieces) != 2:
     91                 continue
     92             rv[pieces[0]] = pieces[1].strip('"')
     93         return rv
     94 
     95     def check_layout(self, layout, file_size):
     96         """Verify the layout to be consistent.
     97 
     98         The layout is consistent if there is no overlapping sections and the
     99         section boundaries do not exceed the file size.
    100 
    101         Inputs:
    102           layout: a dictionary keyed by a string (the section name) with
    103                   values being two integers tuples, the first and the last
    104                   bites' offset in the file.
    105           file_size: and integer, the size of the file the layout describes
    106                      the sections in.
    107 
    108         Raises:
    109           TestError in case the layout is not consistent.
    110         """
    111 
    112         # Generate a list of section range tuples.
    113         ost = sorted([layout[section] for section in layout])
    114         base = -1
    115         for section_base, section_end in ost:
    116             if section_base <= base or section_end + 1 < section_base:
    117                 raise TestError('bad section at 0x%x..0x%x' % (
    118                         section_base, section_end))
    119             base = section_end
    120         if base > file_size:
    121             raise TestError('Section end 0x%x exceeds file size %x' % (
    122                     base, file_size))
    123 
    124     def get_layout(self, file_name):
    125         """Generate layout for a firmware file.
    126 
    127         First retrieve the text layout as reported by 'mosys' and then convert
    128         it into a dictionary, replacing section names reported by mosys into
    129         matching names from DEFAULT_CHROMEOS_FMAP_CONVERSION dictionary above,
    130         using the names as keys in the layout dictionary. The elements of the
    131         layout dictionary are the offsets of the first ans last bytes of the
    132         section in the firmware file.
    133 
    134         Then verify the generated layout's consistency and return it to the
    135         caller.
    136         """
    137 
    138         layout_data = {} # keyed by the section name, elements - tuples of
    139                          # (<section start addr>, <section end addr>)
    140 
    141         for line in self._get_text_layout(file_name):
    142             d = self._line_to_dictionary(line)
    143             try:
    144                 name = self.DEFAULT_CHROMEOS_FMAP_CONVERSION[d['area_name']]
    145             except KeyError:
    146                 continue  # This line does not contain an area of interest.
    147 
    148             if name in layout_data:
    149                 raise TestError('%s duplicated in the layout' % name)
    150 
    151             offset = int(d['area_offset'], 0)
    152             size = int(d['area_size'], 0)
    153             layout_data[name] = (offset, offset + size - 1)
    154 
    155         self.check_layout(layout_data, self.os_if.get_file_size(file_name))
    156         return layout_data
    157 
    158 # flashrom utility wrapper
    159 class flashrom_util(object):
    160     """ a wrapper for "flashrom" utility.
    161 
    162     You can read, write, or query flash ROM size with this utility.
    163     Although you can do "partial-write", the tools always takes a
    164     full ROM image as input parameter.
    165 
    166     NOTE before accessing flash ROM, you may need to first "select"
    167     your target - usually BIOS or EC. That part is not handled by
    168     this utility. Please find other external script to do it.
    169 
    170     To perform a read, you need to:
    171      1. Prepare a flashrom_util object
    172         ex: flashrom = flashrom_util.flashrom_util()
    173      2. Perform read operation
    174         ex: image = flashrom.read_whole()
    175 
    176         When the contents of the flashrom is read off the target, it's map
    177         gets created automatically (read from the flashrom image using
    178         'mosys'). If the user wants this object to operate on some other file,
    179         he could either have the map for the file created explicitly by
    180         invoking flashrom.set_firmware_layout(filename), or supply his own map
    181         (which is a dictionary where keys are section names, and values are
    182         tuples of integers, base address of the section and the last address
    183         of the section).
    184 
    185     By default this object operates on the map retrieved from the image and
    186     stored locally, this map can be overwritten by an explicitly passed user
    187     map.
    188 
    189    To perform a (partial) write:
    190 
    191      1. Prepare a buffer storing an image to be written into the flashrom.
    192      2. Have the map generated automatically or prepare your own, for instance:
    193         ex: layout_map_all = { 'all': (0, rom_size - 1) }
    194         ex: layout_map = { 'ro': (0, 0xFFF), 'rw': (0x1000, rom_size-1) }
    195      4. Perform write operation
    196 
    197         ex using default map:
    198           flashrom.write_partial(new_image, (<section_name>, ...))
    199         ex using explicitly provided map:
    200           flashrom.write_partial(new_image, layout_map_all, ('all',))
    201 
    202     Attributes:
    203         keep_temp_files: boolean flag to control cleaning of temporary files
    204     """
    205 
    206     def __init__(self, os_if, keep_temp_files=False,
    207                  target_is_ec=False):
    208         """ constructor of flashrom_util. help(flashrom_util) for more info """
    209         self.os_if = os_if
    210         self.keep_temp_files = keep_temp_files
    211         self.firmware_layout = {}
    212         self._target_command = ''
    213         if target_is_ec:
    214             self._enable_ec_access()
    215         else:
    216             self._enable_bios_access()
    217 
    218     def _enable_bios_access(self):
    219         if not self.os_if.target_hosted():
    220             return
    221         self._target_command = '-p host'
    222 
    223     def _enable_ec_access(self):
    224         if not self.os_if.target_hosted():
    225             return
    226         self._target_command = '-p ec'
    227 
    228     def _get_temp_filename(self, prefix):
    229         """Returns name of a temporary file in /tmp."""
    230         return self.os_if.create_temp_file(prefix)
    231 
    232     def _remove_temp_file(self, filename):
    233         """Removes a temp file if self.keep_temp_files is false."""
    234         if self.keep_temp_files:
    235             return
    236         if self.os_if.path_exists(filename):
    237             self.os_if.remove_file(filename)
    238 
    239     def _create_layout_file(self, layout_map):
    240         """Creates a layout file based on layout_map.
    241 
    242         Returns the file name containing layout information.
    243         """
    244         layout_text = ['0x%08lX:0x%08lX %s' % (v[0], v[1], k)
    245             for k, v in layout_map.items()]
    246         layout_text.sort()  # XXX unstable if range exceeds 2^32
    247         tmpfn = self._get_temp_filename('lay_')
    248         self.os_if.write_file(tmpfn, '\n'.join(layout_text) + '\n')
    249         return tmpfn
    250 
    251     def get_section(self, base_image, section_name):
    252         """
    253         Retrieves a section of data based on section_name in layout_map.
    254         Raises error if unknown section or invalid layout_map.
    255         """
    256         if section_name not in self.firmware_layout:
    257             return []
    258         pos = self.firmware_layout[section_name]
    259         if pos[0] >= pos[1] or pos[1] >= len(base_image):
    260             raise TestError('INTERNAL ERROR: invalid layout map: %s.' %
    261                             section_name)
    262         blob = base_image[pos[0] : pos[1] + 1]
    263         # Trim down the main firmware body to its actual size since the
    264         # signing utility uses the size of the input file as the size of
    265         # the data to sign. Make it the same way as firmware creation.
    266         if section_name in ('FVMAIN', 'FVMAINB', 'ECMAINA', 'ECMAINB'):
    267             align = 4
    268             pad = blob[-1]
    269             blob = blob.rstrip(pad)
    270             blob = blob + ((align - 1) - (len(blob) - 1) % align) * pad
    271         return blob
    272 
    273     def put_section(self, base_image, section_name, data):
    274         """
    275         Updates a section of data based on section_name in firmware_layout.
    276         Raises error if unknown section.
    277         Returns the full updated image data.
    278         """
    279         pos = self.firmware_layout[section_name]
    280         if pos[0] >= pos[1] or pos[1] >= len(base_image):
    281             raise TestError('INTERNAL ERROR: invalid layout map.')
    282         if len(data) != pos[1] - pos[0] + 1:
    283             # Pad the main firmware body since we trimed it down before.
    284             if (len(data) < pos[1] - pos[0] + 1 and section_name in
    285                     ('FVMAIN', 'FVMAINB', 'ECMAINA', 'ECMAINB')):
    286                 pad = base_image[pos[1]]
    287                 data = data + pad * (pos[1] - pos[0] + 1 - len(data))
    288             else:
    289                 raise TestError('INTERNAL ERROR: unmatched data size.')
    290         return base_image[0 : pos[0]] + data + base_image[pos[1] + 1 :]
    291 
    292     def get_size(self):
    293         """ Gets size of current flash ROM """
    294         # TODO(hungte) Newer version of tool (flashrom) may support --get-size
    295         # command which is faster in future. Right now we use back-compatible
    296         # method: read whole and then get length.
    297         image = self.read_whole()
    298         return len(image)
    299 
    300     def set_firmware_layout(self, file_name):
    301         """get layout read from the BIOS """
    302 
    303         scraper = LayoutScraper(self.os_if)
    304         self.firmware_layout = scraper.get_layout(file_name)
    305 
    306     def enable_write_protect(self):
    307         """Enable the write pretection of the flash chip."""
    308         cmd = 'flashrom %s --wp-enable' % self._target_command
    309         self.os_if.run_shell_command(cmd)
    310 
    311     def disable_write_protect(self):
    312         """Disable the write pretection of the flash chip."""
    313         cmd = 'flashrom %s --wp-disable' % self._target_command
    314         self.os_if.run_shell_command(cmd)
    315 
    316     def read_whole(self):
    317         """
    318         Reads whole flash ROM data.
    319         Returns the data read from flash ROM, or empty string for other error.
    320         """
    321         tmpfn = self._get_temp_filename('rd_')
    322         cmd = 'flashrom %s -r "%s"' % (self._target_command, tmpfn)
    323         self.os_if.log('flashrom_util.read_whole(): %s' % cmd)
    324         self.os_if.run_shell_command(cmd)
    325         result = self.os_if.read_file(tmpfn)
    326         self.set_firmware_layout(tmpfn)
    327 
    328         # clean temporary resources
    329         self._remove_temp_file(tmpfn)
    330         return result
    331 
    332     def write_partial(self, base_image, write_list, write_layout_map=None):
    333         """
    334         Writes data in sections of write_list to flash ROM.
    335         An exception is raised if write operation fails.
    336         """
    337 
    338         if write_layout_map:
    339             layout_map = write_layout_map
    340         else:
    341             layout_map = self.firmware_layout
    342 
    343         tmpfn = self._get_temp_filename('wr_')
    344         self.os_if.write_file(tmpfn, base_image)
    345         layout_fn = self._create_layout_file(layout_map)
    346 
    347         cmd = 'flashrom %s -l "%s" -i %s -w "%s"' % (
    348                 self._target_command, layout_fn, ' -i '.join(write_list), tmpfn)
    349         self.os_if.log('flashrom.write_partial(): %s' % cmd)
    350         self.os_if.run_shell_command(cmd)
    351 
    352         # flashrom write will reboot the ec after corruption
    353         # For Android, need to make sure ec is back online
    354         # before continuing, or adb command will cause test failure
    355         if self.os_if.is_android:
    356             self.os_if.wait_for_device(60)
    357 
    358         # clean temporary resources
    359         self._remove_temp_file(tmpfn)
    360         self._remove_temp_file(layout_fn)
    361 
    362     def write_whole(self, base_image):
    363         """Write the whole base image. """
    364         layout_map = { 'all': (0, len(base_image) - 1) }
    365         self.write_partial(base_image, ('all',), layout_map)
    366