sigrok-fwextract-kingst-la2016: update documentation and add a generated README file
[sigrok-util.git] / firmware / saleae-logic16 / sigrok-fwextract-saleae-logic16
1 #!/usr/bin/python3
2 ##
3 ## This file is part of the sigrok-util project.
4 ##
5 ## Copyright (C) 2013 Marcus Comstedt <marcus@mc.pp.se>
6 ##
7 ## This program is free software; you can redistribute it and/or modify
8 ## it under the terms of the GNU General Public License as published by
9 ## the Free Software Foundation; either version 3 of the License, or
10 ## (at your option) any later version.
11 ##
12 ## This program is distributed in the hope that it will be useful,
13 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 ## GNU General Public License for more details.
16 ##
17 ## You should have received a copy of the GNU General Public License
18 ## along with this program; if not, see <http://www.gnu.org/licenses/>.
19 ##
20
21 import sys
22 import struct
23 import parseelf
24
25 class searcher:
26
27     def reset(this, offs=0):
28         if offs < 0 or offs > this.length:
29             raise Exception('Reset past end of section')
30         this.address = this.baseaddr + offs
31         this.offset = offs
32
33     def skip(this, cnt):
34         if this.offset + cnt > this.length:
35             raise Exception('Skip past end of section')
36         this.address += cnt
37         this.offset += cnt
38
39     def peek(this, cnt, offs=0):
40         if this.offset + offs + cnt > this.length:
41             raise Exception('Peek past end of section')
42         return this.data[this.offset + offs : this.offset + offs + cnt]
43
44     def look_for(this, needle):
45         pos = this.data.find(needle, this.offset)
46         if pos < 0:
47             raise Exception('Needle not found in haystack')
48         this.skip(pos - this.offset)
49
50     def look_for_either(this, needle1, needle2):
51         pos1 = this.data.find(needle1, this.offset)
52         pos2 = this.data.find(needle2, this.offset)
53         if pos1 < 0 and pos2 < 0:
54             raise Exception('Needle not found in haystack')
55         if pos1 < 0 or pos2 < pos1:
56             pos1 = pos2
57         this.skip(pos1 - this.offset)
58
59     def __init__(this, data, addr):
60         this.data = data
61         this.baseaddr = addr
62         this.length = len(data)
63         this.reset()
64
65 def search_plt_32(plt, addr):
66     plt.reset()
67     plt.look_for(struct.pack('<BBI', 0xff, 0x25, addr)) # jmp *addr32
68     return plt.address
69
70 def search_plt_64(plt, addr):
71     plt.reset()
72     while True:
73         plt.look_for(b'\xff\x25')               # jmpq *offs32(%rip)
74         offs = struct.unpack('<i', plt.peek(4, 2))[0]
75         if plt.address + offs + 6 == addr:
76             return plt.address
77         plt.skip(2)
78
79 def find_hex_file_lines_constructor_32(text, hex_file_lines_got, got_plt):
80     while True:
81         text.look_for_either(b'\x8b\xbb', b'\x8b\xb3')  # mov offs32(%ebx),{%edi,%esi}
82         offs = struct.unpack('<i', text.peek(4, 2))[0]
83         if got_plt + offs == hex_file_lines_got:
84             text.skip(6)
85             return
86         text.skip(2)
87
88 def find_hex_file_lines_constructor_64(text, hex_file_lines_got):
89     while True:
90         text.look_for(b'\x48\x8b\x2d')          # mov offs32(%rip),%rbp
91         offs = struct.unpack('<i', text.peek(4, 3))[0]
92         if text.address + offs + 7 == hex_file_lines_got:
93             text.skip(7)
94             return
95         text.skip(3)
96
97 def parse_hex_file_lines_constructor_32(text, basic_string_plt, got_plt, lines):
98     text.skip(-5)
99     esi = (text.peek(1) == b'\xb3')
100     text.skip(5)
101     cnt = len(lines)
102     while cnt > 0:
103         if text.peek(2) == b'\x8d\x45':         # lea offs8(%ebp),%eax
104             text.skip(3)
105         elif text.peek(2) == b'\x8d\x85':       # lea offs32(%ebp),%eax
106             text.skip(6)
107         if text.peek(1) == (b'\xbf' if esi else b'\xbe'):       # mov $imm32,%esi
108             text.skip(5)
109         elif text.peek(2) == (b'\x31\xff' if esi else b'\x31\xf6'):     # xor %esi,%esi
110             text.skip(2)
111         if text.peek(4) == b'\x89\x44\x24\x08': # mov %eax,0x8(%esp)
112             text.skip(4)
113         if text.peek(2) == b'\x8d\x83':         # lea offs32(%ebx),%eax
114             straddr = struct.unpack('<i', text.peek(4, 2))[0]
115             text.skip(6)
116             straddr += got_plt
117         else:
118             raise Exception('Expected lea offs32(%ebx),%eax @ ' +
119                             ('0x%x' % text.address))
120         if text.peek(4) == b'\x89\x44\x24\x04': # mov %eax,0x4(%esp)
121             text.skip(4)
122         if text.peek(3) == (b'\x89\x34\x24' if esi else b'\x89\x3c\x24'):       # mov %edi,(%esp)
123             offs = 0
124             text.skip(3)
125         elif text.peek(2) == (b'\x8d\x46' if esi else b'\x8d\x47'):     # lea offs8(%edi),%eax
126             offs = struct.unpack('<b', text.peek(1, 2))[0]
127             text.skip(3)
128         elif text.peek(2) == (b'\x8d\x86' if esi else b'\x8d\x87'):     # lea offs32(%edi),%eax
129             offs = struct.unpack('<i', text.peek(4, 2))[0]
130             text.skip(6)
131         else:
132             raise Exception('Expected lea offs(%e'+('s' if esi else 'd')+'i),%eax @ ' +
133                             ('0x%x' % text.address))
134         if offs < 0 or offs > (len(lines) << 2) or (offs & 3) != 0:
135             raise Exception('Invalid offset %d' % offs)
136         index = offs >> 2
137         if lines[index] != 0:
138             raise Exception('Line %d filled multiple times' % index)
139         if text.peek(3) == b'\x89\x04\x24':     # mov %eax,(%esp)
140             text.skip(3)
141         if text.peek(1) == b'\xe8':             # call offs32
142             offs = struct.unpack('<i', text.peek(4, 1))[0]
143             text.skip(5)
144             if text.address + offs != basic_string_plt:
145                 raise Exception('Expected call ZNSsC1EPKcRKSaIcE@plt @ ' +
146                                 ('0x%x' % text.address))
147         else:
148             raise Exception('Expected call ZNSsC1EPKcRKSaIcE@plt @ ' +
149                             ('0x%x' % text.address))
150         if straddr == 0:
151             raise Exception('NULL pointer stored to index %d' % index)
152         lines[index] = straddr
153         cnt -= 1
154
155 def parse_hex_file_lines_constructor_64(text, basic_string_plt, lines):
156     cnt = len(lines)
157     while cnt > 0:
158         if text.peek(1) == b'\xbb':             # mov $imm32,%ebx
159             text.skip(5)
160         elif text.peek(2) == b'\x31\xdb':       # xor %ebx,%ebx
161             text.skip(2)
162         if text.peek(4) == b'\x48\x8d\x54\x24': # lea offs8(%rsp),%rdx
163             text.skip(5)
164         elif text.peek(4) == b'\x48\x8d\x94\x24': # lea offs32(%rsp),%rdx
165             text.skip(8)
166         if text.peek(3) == b'\x48\x8d\x35':     # lea offs32(%rip),%rsi
167             straddr = struct.unpack('<i', text.peek(4, 3))[0]
168             text.skip(7)
169             straddr += text.address
170         else:
171             raise Exception('Expected lea offs(%rip),%rsi @ ' +
172                             ('0x%x' % text.address))
173         if text.peek(3) == b'\x48\x89\xef':     # mov %rbp,%rdi
174             offs = 0
175             text.skip(3)
176         elif text.peek(3) == b'\x48\x8d\x7d':   # lea offs8(%rbp),%rdi
177             offs = struct.unpack('<b', text.peek(1, 3))[0]
178             text.skip(4)
179         elif text.peek(3) == b'\x48\x8d\xbd':   # lea offs32(%rbp),%rdi
180             offs = struct.unpack('<i', text.peek(4, 3))[0]
181             text.skip(7)
182         else:
183             raise Exception('Expected lea offs(%rbp),%rdi @ ' +
184                             ('0x%x' % text.address))
185         if text.peek(1) == b'\xbb':             # mov $imm32,%ebx
186             text.skip(5)
187         elif text.peek(2) == b'\x31\xdb':       # xor %ebx,%ebx
188             text.skip(2)
189         if offs < 0 or offs > (len(lines) << 3) or (offs & 7) != 0:
190             raise Exception('Invalid offset %d' % offs)
191         index = offs >> 3
192         if lines[index] != 0:
193             raise Exception('Line %d filled multiple times' % index)
194         if text.peek(1) == b'\xe8':             # callq offs32
195             offs = struct.unpack('<i', text.peek(4, 1))[0]
196             text.skip(5)
197             if text.address + offs != basic_string_plt:
198                 raise Exception('Expected callq ZNSsC1EPKcRKSaIcE@plt @ ' +
199                                 ('0x%x' % text.address))
200         else:
201             raise Exception('Expected callq ZNSsC1EPKcRKSaIcE@plt @ ' +
202                             ('0x%x' % text.address))
203         if straddr == 0:
204             raise Exception('NULL pointer stored to index %d' % index)
205         lines[index] = straddr
206         cnt -= 1
207
208 def find_reloc(elf, symname):
209     for section, relocs in elf.relocs.items():
210         if 'symbols' in relocs and symname in relocs['symbols']:
211             symnum = relocs['symbols'][symname]['number']
212             for reloc in relocs['relocs']:
213                 if reloc['r_sym'] == symnum:
214                     return reloc
215     raise Exception('Unable to find a relocation against ' + symname)
216
217 def ihex_to_binary(lines):
218     chunks = {}
219     for line in lines:
220         if line[0] != ':':
221             raise Exception('ihex line does not start with ":"')
222         line = bytes.fromhex(line[1:])
223         if (sum(bytearray(line)) & 0xff) != 0:
224             raise Exception('Invalid checksum in ihex')
225         (byte_count, address, rectype) = struct.unpack('>BHB', line[:4])
226         (data, checksum) = struct.unpack('>%dsB' % (byte_count), line[4:])
227         if rectype == 1 and byte_count == 0:
228             pass
229         elif rectype != 0 or byte_count == 0:
230             raise Exception('Unexpected rectype %d with bytecount %d' %
231                             (rectype, byte_count))
232         elif address in chunks:
233             raise Exception('Multiple ihex lines with address 0x%x' % address)
234         else:
235             chunks[address] = data
236     blob = b''
237     for address in sorted(iter(chunks)):
238         if address < len(blob):
239             raise Exception('Overlapping ihex chunks')
240         elif address > len(blob):
241             blob += b'\x00' * (address - len(blob))
242         blob += chunks[address]
243     return blob
244
245 def extract_fx2_firmware(elf, symname, filename):
246     blob = elf.load_symbol(elf.dynsym[symname + 'Count'])
247     count = struct.unpack('<I', blob)[0]
248     got_plt = elf.find_section('.got.plt')['sh_addr']
249     hex_file_lines_got = find_reloc(elf, symname)['r_offset']
250     basic_string_got = find_reloc(elf, '_ZNSsC1EPKcRKSaIcE')['r_offset']
251     pltsec = elf.find_section('.plt')
252     plt = searcher(elf.read_section(pltsec), pltsec['sh_addr'])
253     try:
254         if elf.elf_wordsize == 64:
255             basic_string_plt = search_plt_64(plt, basic_string_got)
256         else:
257             basic_string_plt = search_plt_32(plt, basic_string_got)
258     except:
259         raise Exception('Unable to find a PLT entry for _ZNSsC1EPKcRKSaIcE')
260     textsec = elf.find_section('.text')
261     text = searcher(elf.read_section(textsec), textsec['sh_addr'])
262     while True:
263         try:
264             if elf.elf_wordsize == 64:
265                 find_hex_file_lines_constructor_64(text, hex_file_lines_got)
266             else:
267                 find_hex_file_lines_constructor_32(text, hex_file_lines_got,
268                                                    got_plt)
269         except:
270             raise Exception('Unable to find constructor for ' + symname)
271         oldoffs = text.offset
272         l = [0]*count
273         try:
274             if elf.elf_wordsize == 64:
275                 parse_hex_file_lines_constructor_64(text, basic_string_plt, l)
276             else:
277                 parse_hex_file_lines_constructor_32(text, basic_string_plt,
278                                                     got_plt, l)
279             break
280         except KeyError:
281             text.reset(oldoffs)
282     rodatasec = elf.find_section('.rodata')
283     rodata = elf.read_section(rodatasec)
284     lo = rodatasec['sh_addr']
285     hi = lo + rodatasec['sh_size']
286     for i in range(count):
287         addr = l[i]
288         if addr < lo or addr >= hi:
289             raise Exception('Address 0x%x outside of .rodata section' % addr)
290         l[i] = elf.get_name(addr - lo, rodata)
291     blob = ihex_to_binary(l)
292     f = open(filename, 'wb')
293     f.write(blob)
294     f.close()
295     print("saved %d bytes to %s" % (len(blob), filename))
296
297 def extract_fx2_firmware_single(elf, symname, filename):
298     if not symname in elf.dynsym:
299         return False
300     hex = bytes.decode(elf.load_symbol(elf.dynsym[symname]))
301     if hex[-1] == '\0':
302         hex = hex[:-1]
303     blob = ihex_to_binary(hex.split(';'))
304     f = open(filename, 'wb')
305     f.write(blob)
306     f.close()
307     print("saved %d bytes to %s" % (len(blob), filename))
308     return True
309
310 def extract_fx3_firmware(elf, symname, filename):
311     index = 0
312     blobs = []
313     while True:
314         sym = elf.dynsym.get(symname + '_' + str(index))
315         if not sym:
316             break
317         index += 1
318         hex = bytes.decode(elf.load_symbol(sym))
319         if hex[-1] == '\0':
320             hex = hex[:-1]
321         for part in hex.split(';'):
322             if not part:
323                 continue
324             if part[0] != ':':
325                 raise Exception('ihex line does not start with ":"')
326             blobs.append(bytes.fromhex(part[1:]))
327     if not blobs:
328         return
329     with open(filename, 'wb') as f:
330         for blob in blobs:
331             f.write(blob)
332     print("saved %d bytes from %d blobs to %s" % (sum(map(len, blobs)),
333         len(blobs), filename))
334
335 def extract_symbol(elf, symname, filename):
336     blob = elf.load_symbol(elf.dynsym[symname])
337     f = open(filename, 'wb')
338     f.write(blob)
339     f.close()
340     print("saved %d bytes to %s" % (len(blob), filename))
341
342 def try_extract_symbol(elf, symname, filename):
343     if not symname in elf.dynsym:
344         return
345     extract_symbol(elf, symname, filename)
346
347 def extract_bitstream(elf, lv):
348     extract_symbol(elf, 'gLogic16Lv' + lv + 'CompressedBitstream',
349                    'saleae-logic16-fpga-' + lv + '.bitstream')
350
351 def usage():
352     print("sigrok-fwextract-saleae-logic16 <programfile>")
353     sys.exit()
354
355
356 #
357 # main
358 #
359
360 if len(sys.argv) != 2:
361     usage()
362
363 try:
364     filename = sys.argv[1]
365     elf = parseelf.elf(filename)
366     if elf.ehdr['e_machine'] != 3 and elf.ehdr['e_machine'] != 62:
367         raise Exception('Unsupported e_machine')
368     if not extract_fx2_firmware_single(elf, 'Logic16FirmwareStrings', 'saleae-logic16-fx2.fw'):
369         extract_fx2_firmware(elf, 'gLogic16HexFileLines', 'saleae-logic16-fx2.fw')
370     extract_bitstream(elf, '18')
371     extract_bitstream(elf, '33')
372     extract_fx3_firmware(elf, 'LogicPro16FirmwareStrings', 'saleae-logicpro16-fx3.fw')
373     extract_fx3_firmware(elf, 'LogicPro8FirmwareStrings', 'saleae-logicpro8-fx3.fw')
374     try_extract_symbol(elf, 'gLogicPro16CompressedBitstream', 'saleae-logicpro16-fpga.bitstream')
375     try_extract_symbol(elf, 'gLogicProCompressedBitstream', 'saleae-logicpro8-fpga.bitstream')
376 except Exception as e:
377     print("Error: %s" % str(e))