aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--util/spdtool/description.md3
-rw-r--r--util/spdtool/spdtool.py235
2 files changed, 238 insertions, 0 deletions
diff --git a/util/spdtool/description.md b/util/spdtool/description.md
new file mode 100644
index 0000000000..3d58c39ec9
--- /dev/null
+++ b/util/spdtool/description.md
@@ -0,0 +1,3 @@
+Dumps SPD ROMs from a given blob to separate files using known patterns
+and reserved bits. Useful for analysing firmware that holds SPDs on boards
+that have soldered down DRAM. `python`
diff --git a/util/spdtool/spdtool.py b/util/spdtool/spdtool.py
new file mode 100644
index 0000000000..f6f9f85122
--- /dev/null
+++ b/util/spdtool/spdtool.py
@@ -0,0 +1,235 @@
+#!/usr/bin/env python
+
+# spdtool - Tool for partial deblobbing of UEFI firmware images
+# Copyright (C) 2019 9elements Agency GmbH <patrick.rudolph@9elements.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+#
+# Parse a blob and search for SPD files.
+# First it is searched for a possible SPD header.
+#
+# For each candidate the function verify_match is invoked to check
+# additional fields (known bits, reserved bits, CRC, ...)
+#
+# Dumps the found SPDs into the current folder.
+#
+# Implemented:
+# DDR4 SPDs
+#
+
+import argparse
+import crc16
+import struct
+
+
+class Parser(object):
+ def __init__(self, blob, verbose=False, ignorecrc=False):
+ self.blob = blob
+ self.ignorecrc = ignorecrc
+ self.verbose = verbose
+
+ @staticmethod
+ def get_matches():
+ """Return the first byte to look for"""
+ raise Exception("Function not implemented")
+
+ def verify_match(self, header, offset):
+ """Return true if it looks like a SPD"""
+ raise Exception("Function not implemented")
+
+ def get_len(self, header, offset):
+ """Return the length of the SPD"""
+ raise Exception("Function not implemented")
+
+ def get_part_number(self, offset):
+ """Return the part number in SPD"""
+ return ""
+
+ def get_manufacturer_id(self, offset):
+ """Return the manufacturer ID in SPD"""
+ return 0xffff
+
+ def get_mtransfers(self, offset):
+ """Return the number of MT/s"""
+ return 0
+
+ def get_manufacturer(self, offset):
+ """Return manufacturer as string"""
+ id = self.get_manufacturer_id(offset)
+ if id == 0xffff:
+ return "Unknown"
+ ids = {
+ 0x2c80: "Crucial/Micron",
+ 0x4304: "Ramaxel",
+ 0x4f01: "Transcend",
+ 0x9801: "Kingston",
+ 0x987f: "Hynix",
+ 0x9e02: "Corsair",
+ 0xb004: "OCZ",
+ 0xad80: "Hynix/Hyundai",
+ 0xb502: "SuperTalent",
+ 0xcd04: "GSkill",
+ 0xce80: "Samsung",
+ 0xfe02: "Elpida",
+ 0xff2c: "Micron",
+ }
+ if id in ids:
+ return ids[id]
+ return "Unknown"
+
+ def blob_as_ord(self, offset):
+ """Helper for python2/python3 compatibility"""
+ return self.blob[offset] if type(self.blob[offset]) is int \
+ else ord(self.blob[offset])
+
+ def search(self, start):
+ """Search for SPD at start. Returns -1 on error or offset
+ if found.
+ """
+ for i in self.get_matches():
+ for offset in range(start, len(self.blob)):
+ if self.blob_as_ord(offset) == i and \
+ self.verify_match(i, offset):
+ return offset, self.get_len(i, offset)
+ return -1, 0
+
+
+class SPD4Parser(Parser):
+ @staticmethod
+ def get_matches():
+ """Return DDR4 possible header candidates"""
+ ret = []
+ for i in [1, 2, 3, 4]:
+ for j in [1, 2]:
+ ret.append(i + j * 16)
+ return ret
+
+ def verify_match(self, header, offset):
+ """Verify DDR4 specific bit fields."""
+ # offset 0 is a candidate, no need to validate
+ if self.blob_as_ord(offset + 1) == 0xff:
+ return False
+ if self.blob_as_ord(offset + 2) != 0x0c:
+ return False
+ if self.blob_as_ord(offset + 5) & 0xc0 > 0:
+ return False
+ if self.blob_as_ord(offset + 6) & 0xc > 0:
+ return False
+ if self.blob_as_ord(offset + 7) & 0xc0 > 0:
+ return False
+ if self.blob_as_ord(offset + 8) != 0:
+ return False
+ if self.blob_as_ord(offset + 9) & 0xf > 0:
+ return False
+ if self.verbose:
+ print("%x: Looks like DDR4 SPD" % offset)
+
+ crc = crc16.crc16xmodem(self.blob[offset:offset + 0x7d + 1])
+ # Vendors ignore the endianness...
+ crc_spd1 = self.blob_as_ord(offset + 0x7f)
+ crc_spd1 |= (self.blob_as_ord(offset + 0x7e) << 8)
+ crc_spd2 = self.blob_as_ord(offset + 0x7e)
+ crc_spd2 |= (self.blob_as_ord(offset + 0x7f) << 8)
+ if crc != crc_spd1 and crc != crc_spd2:
+ if self.verbose:
+ print("%x: CRC16 doesn't match" % offset)
+ if not self.ignorecrc:
+ return False
+
+ return True
+
+ def get_len(self, header, offset):
+ """Return the length of the SPD found."""
+ if (header >> 4) & 7 == 1:
+ return 256
+ if (header >> 4) & 7 == 2:
+ return 512
+ return 0
+
+ def get_part_number(self, offset):
+ """Return part number as string"""
+ if offset + 0x15c >= len(self.blob):
+ return ""
+ tmp = self.blob[offset + 0x149:offset + 0x15c + 1]
+ return tmp.decode('utf-8').rstrip()
+
+ def get_manufacturer_id(self, offset):
+ """Return manufacturer ID"""
+ if offset + 0x141 >= len(self.blob):
+ return 0xffff
+ tmp = self.blob[offset + 0x140:offset + 0x141 + 1]
+ return struct.unpack('H', tmp)[0]
+
+ def get_mtransfers(self, offset):
+ """Return MT/s as specified by MTB and FTB"""
+ if offset + 0x7d >= len(self.blob):
+ return 0
+
+ if self.blob_as_ord(offset + 0x11) != 0:
+ return 0
+ mtb = 8.0
+ ftb = 1000.0
+ tmp = self.blob[offset + 0x12:offset + 0x12 + 1]
+ tckm = struct.unpack('B', tmp)[0]
+ tmp = self.blob[offset + 0x7d:offset + 0x7d + 1]
+ tckf = struct.unpack('b', tmp)[0]
+ return int(2000 / (tckm / mtb + tckf / ftb))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='SPD rom dumper')
+ parser.add_argument('--blob', required=True,
+ help='The ROM to search SPDs in.')
+ parser.add_argument('--spd4', action='store_true', default=False,
+ help='Search for DDR4 SPDs.')
+ parser.add_argument('--hex', action='store_true', default=False,
+ help='Store SPD in hex format otherwise binary.')
+ parser.add_argument('-v', '--verbose', help='increase output verbosity',
+ action='store_true')
+ parser.add_argument('--ignorecrc', help='Ignore CRC mismatch',
+ action='store_true', default=False)
+ args = parser.parse_args()
+
+ blob = open(args.blob, "rb").read()
+
+ if args.spd4:
+ p = SPD4Parser(blob, args.verbose, args.ignorecrc)
+ else:
+ raise Exception("Must specify one of the following arguments:\n--spd4")
+
+ offset = 0
+ cnt = 0
+ while True:
+ offset, length = p.search(offset)
+ if length == 0:
+ break
+ print("Found SPD at 0x%x" % offset)
+ print(" '%s', size %d, manufacturer %s (0x%04x) %d MT/s\n" %
+ (p.get_part_number(offset), length, p.get_manufacturer(offset),
+ p.get_manufacturer_id(offset), p.get_mtransfers(offset)))
+ filename = "spd-%d-%s-%s.bin" % (cnt, p.get_part_number(offset),
+ p.get_manufacturer(offset))
+ filename = filename.replace("/", "_")
+ filename = "".join([c for c in filename if c.isalpha() or c.isdigit()
+ or c == '-' or c == '.' or c == '_']).rstrip()
+ if not args.hex:
+ open(filename, "wb").write(blob[offset:offset + length])
+ else:
+ filename += ".hex"
+ with open(filename, "w") as fn:
+ j = 0
+ for i in blob[offset:offset + length]:
+ fn.write("%02X" % struct.unpack('B', i)[0])
+ fn.write(" " if j < 15 else "\n")
+ j = (j + 1) % 16
+ offset += 1
+ cnt += 1