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