pelfy 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pelfy/__init__.py ADDED
@@ -0,0 +1,563 @@
1
+ from . import fields_data as fdat
2
+ from . import output_formatter
3
+ from typing import TypeVar, Literal, Iterable, Generic, Iterator, Generator
4
+
5
+ _T = TypeVar('_T')
6
+
7
+
8
+ def open_elf_file(file_path: str) -> 'elf_file':
9
+ """Reads ELF data from file
10
+
11
+ Args:
12
+ file_path: path of the ELF file
13
+
14
+ Returns:
15
+ elf_file object
16
+ """
17
+ with open(file_path, mode='rb') as f:
18
+ return elf_file(f.read())
19
+
20
+
21
+ class elf_symbol():
22
+ """A class for representing data of an ELF symbol
23
+
24
+ Args:
25
+ file: ELF file object
26
+
27
+ fields: symbol header fields
28
+
29
+ index: Absolut index in the symbol table
30
+
31
+ Attributes:
32
+ file: Points to the parent ELF file object.
33
+
34
+ name: Name of the symbol
35
+
36
+ index: Absolut index in the symbol table
37
+
38
+ info: Type of the symbol
39
+
40
+ description: Description of the symbol type
41
+
42
+ stb: visibility of the symbol (local, global, etc.)
43
+
44
+ stb_description: Description of the symbol visibility
45
+
46
+ fields: All symbol header fields as dict
47
+ """
48
+
49
+ def __init__(self, file: 'elf_file', fields: dict[str, int], index: int):
50
+ self.fields = fields
51
+ self.file = file
52
+
53
+ if file.string_table_section:
54
+ self.name = file.read_string(file.string_table_section['sh_offset'] + fields['st_name'])
55
+ else:
56
+ self.name = ''
57
+
58
+ self.index = index
59
+
60
+ self.info, self.description = fdat.st_info_values[fields['st_info'] & 0x0F]
61
+ self.stb, self.stb_description = fdat.stb_values[fields['st_info'] >> 4]
62
+
63
+ def read_data(self) -> bytes:
64
+ """Returns the binary data the symbol is pointing to.
65
+ The offset in the ELF file is calculated by:
66
+ sections[symbol.st_shndx].sh_offset + symbol.st_value
67
+
68
+ Returns:
69
+ Symbol data
70
+ """
71
+ offset = self.file.sections[self['st_shndx']]['sh_offset'] + self['st_value']
72
+ return self.file.read_bytes(offset, self['st_size'])
73
+
74
+ def read_data_hex(self) -> str:
75
+ return ' '.join(f'{d:02X}' for d in self.read_data())
76
+
77
+ def get_relocations(self) -> 'relocation_list':
78
+ """List all relocations that are pointing to this symbol.
79
+ The symbol must be of type SHT_PROGBITS (program code). Therefore
80
+ this function lists typically all relocations that will be
81
+ applied to the function represented by the symbol.
82
+
83
+ Returns:
84
+ List of relocations
85
+ """
86
+ ret: list[elf_relocation] = list()
87
+ section = self.file.sections[self.fields['st_shndx']]
88
+ assert section.type == 'SHT_PROGBITS'
89
+ for reloc in self.file.get_relocations():
90
+ if reloc.target_section == section:
91
+ offset = reloc['r_offset'] - self['st_value']
92
+ if 0 <= offset < self['st_size']:
93
+ ret.append(reloc)
94
+ return relocation_list(ret)
95
+
96
+ def __getitem__(self, key: str | int) -> int:
97
+ if isinstance(key, str):
98
+ assert key in self.fields, f'Unknown field name: {key}'
99
+ return self.fields[key]
100
+ else:
101
+ return list(self.fields.values())[key]
102
+
103
+ def __repr__(self) -> str:
104
+ return f'index {self.index}\n' +\
105
+ f'name {self.name}\n' +\
106
+ f'stb {self.stb} ({self.stb_description})\n' +\
107
+ f'info {self.info} ({self.description})\n' +\
108
+ '\n'.join(f'{k:18} {v:4}' for k, v in self.fields.items()) + '\n'
109
+
110
+
111
+ class elf_section():
112
+ """A class for representing data of an ELF section
113
+
114
+ Args:
115
+ file: ELF file object
116
+
117
+ fields: Section header fields
118
+
119
+ name: Name of the section
120
+
121
+ index: Absolut index in the symbol table
122
+
123
+ Attributes:
124
+ file: Points to the parent ELF file object.
125
+
126
+ name: Name of the section
127
+
128
+ index: Absolut index of the section
129
+
130
+ type: Type of the section
131
+
132
+ description: Description of the section type
133
+
134
+ fields: All symbol header fields as dict
135
+ """
136
+ def __init__(self, file: 'elf_file', fields: dict[str, int], name: str, index: int):
137
+ self.fields = fields
138
+ self.file = file
139
+ self.index = index
140
+ self.name = name
141
+
142
+ if fields['sh_type'] > 0x60000000:
143
+ # Range for OS, compiler and application specific types
144
+ self.description = [v for k, v in fdat.section_header_types_ex.items() if k >= fields['sh_type']][0]
145
+ self.type = str(hex(fields['sh_type']))
146
+ elif fields['sh_type'] in fdat.section_header_types:
147
+ self.type, self.description = fdat.section_header_types[fields['sh_type']]
148
+ else:
149
+ self.description = ''
150
+ self.type = str(hex(fields['sh_type']))
151
+
152
+ def read_data(self) -> bytes:
153
+ """Returns the binary data from the section.
154
+ The offset in the ELF file is given by: section.sh_offset
155
+
156
+ Returns:
157
+ Data of the section
158
+ """
159
+ return self.file.read_bytes(self['sh_offset'], self['sh_size'])
160
+
161
+ def get_data_hex(self) -> str:
162
+ data = self.file.read_bytes(self['sh_offset'], self['sh_size'])
163
+ return ' '.join(f'{d:02X}' for d in data)
164
+
165
+ def __getitem__(self, key: str | int) -> int:
166
+ if isinstance(key, str):
167
+ assert key in self.fields, f'Unknown field name: {key}'
168
+ return self.fields[key]
169
+ else:
170
+ return list(self.fields.values())[key]
171
+
172
+ def __repr__(self) -> str:
173
+ return f'index {self.index}\n' +\
174
+ f'name {self.name}\n' +\
175
+ f'type {self.type} ({self.description})\n' +\
176
+ '\n'.join(f"{k:18} {v:4} {fdat.section_header[k]['description']}" for k, v in self.fields.items()) + '\n'
177
+
178
+
179
+ class elf_relocation():
180
+ """A class for representing data of a relocation
181
+
182
+ Args:
183
+ file: ELF file object
184
+
185
+ fields: Relocation header fields
186
+
187
+ symbol_index: Index of the symbol to relocate in the symbol table
188
+
189
+ relocation_type: Type of the relocation (numeric)
190
+
191
+ sh_info: Index of the section this relocation applies to
192
+
193
+ index: Absolut index of the relocation in the associated relocation section
194
+
195
+ Attributes:
196
+ file: Points to the parent ELF file object.
197
+
198
+ index: Absolut index of the relocation the associated relocation section
199
+
200
+ type: Type of the relocation
201
+
202
+ calculation: Description of the relocation calculation
203
+
204
+ target_section: Pointing to the section that is relocation applies to
205
+
206
+ fields: All relocation header fields as dict
207
+ """
208
+ def __init__(self, file: 'elf_file', fields: dict[str, int], symbol_index: int,
209
+ relocation_type: int, sh_info: int, index: int):
210
+ self.fields = fields
211
+ self.file = file
212
+ self.index = index
213
+ self.symbol = file.symbols[symbol_index]
214
+ reloc_types = fdat.relocation_table_types.get(file.architecture)
215
+ if reloc_types and relocation_type in reloc_types:
216
+ self.calculation = reloc_types[relocation_type][2]
217
+ self.type = reloc_types[relocation_type][0]
218
+ else:
219
+ self.calculation = ''
220
+ self.type = str(relocation_type)
221
+ self.target_section: elf_section = file.sections[sh_info]
222
+
223
+ def __getitem__(self, key: str | int) -> int:
224
+ if isinstance(key, str):
225
+ assert key in self.fields, f'Unknown field name: {key}'
226
+ return self.fields[key]
227
+ else:
228
+ return list(self.fields.values())[key]
229
+
230
+ def __repr__(self) -> str:
231
+ return f'index {self.symbol.index}\n' +\
232
+ f'symbol {self.symbol.name}\n' +\
233
+ f'relocation type {self.type} ({self.calculation})\n' +\
234
+ '\n'.join(f'{k:18} {v:4}' for k, v in self.fields.items()) + '\n'
235
+
236
+
237
+ class elf_list(Generic[_T]):
238
+ """A generic class for representing a list of ELF data items
239
+
240
+ Args:
241
+ data: Iterable of ELF data items
242
+ """
243
+ def __init__(self, data: Iterable[_T]):
244
+ self._data = list(data)
245
+
246
+ def __getitem__(self, key: int | str) -> _T:
247
+ if isinstance(key, str):
248
+ elements = [el for el in self._data if getattr(el, 'name', '') == key]
249
+ assert elements, f'Unknown name: {key}'
250
+ return elements[0]
251
+ else:
252
+ return self._data.__getitem__(key)
253
+
254
+ def __len__(self) -> int:
255
+ return len(self._data)
256
+
257
+ def __iter__(self) -> Iterator[_T]:
258
+ return iter(self._data)
259
+
260
+ def _compact_table(self) -> tuple[list[str], list[list[str | int]], list[str]]:
261
+ return [], [[]], []
262
+
263
+ def _repr_table(self, format: output_formatter.table_format, raw_data: bool = False) -> str:
264
+ if raw_data and len(self):
265
+ table_dict: list[dict[str, int]] = [el.__dict__.get('fields', {' ': 0}) for el in self]
266
+ columns = list(table_dict[0].keys())
267
+ data: list[list[str | int]] = [list(el.values()) for el in table_dict]
268
+ radj = columns
269
+ else:
270
+ columns, data, radj = self._compact_table()
271
+ return output_formatter.generate_table(data, columns, right_adj_col=radj, format=format)
272
+
273
+ def to_dict_list(self) -> list[dict[str, str | int]]:
274
+ """Exporting the ELF item data table to a list of dicts. It can be used with pandas:
275
+ df = pandas.DataFrame(elements.to_dict_list())
276
+
277
+ Returns:
278
+ Table data
279
+ """
280
+ columns, data, _ = self._compact_table()
281
+ return [{k: v for k, v in zip(columns, row)} for row in data]
282
+
283
+ def to_html(self) -> str:
284
+ """Exporting the ELF item data table to HTML.
285
+
286
+ Returns:
287
+ HTML table
288
+ """
289
+ return self._repr_table('html')
290
+
291
+ def to_markdown(self) -> str:
292
+ """Exporting the ELF item data table to markdown.
293
+
294
+ Returns:
295
+ Markdown table
296
+ """
297
+ return self._repr_table('markdown')
298
+
299
+ def __repr__(self) -> str:
300
+ return self._repr_table('text')
301
+
302
+ def _repr_html_(self) -> str:
303
+ return self._repr_table('html')
304
+
305
+
306
+ class section_list(elf_list[elf_section]):
307
+ """A class for representing a list of ELF section
308
+ """
309
+ def _compact_table(self) -> tuple[list[str], list[list[int | str]], list[str]]:
310
+ columns = ['index', 'name', 'type', 'description']
311
+ data: list[list[str | int]] = [[item.index, item.name, item.type,
312
+ item.description] for item in self]
313
+ return columns, data, ['index']
314
+
315
+
316
+ class symbol_list(elf_list[elf_symbol]):
317
+ """A class for representing a list of ELF symbols
318
+ """
319
+ def _compact_table(self) -> tuple[list[str], list[list[int | str]], list[str]]:
320
+ columns = ['index', 'name', 'info', 'size', 'stb', 'description']
321
+ data: list[list[str | int]] = [[item.index, item.name, item.info, item.fields['st_size'],
322
+ item.stb, item.description] for item in self]
323
+ return columns, data, ['index', 'size']
324
+
325
+
326
+ class relocation_list(elf_list[elf_relocation]):
327
+ """A class for representing a list of ELF relocations
328
+ """
329
+ def _compact_table(self) -> tuple[list[str], list[list[int | str]], list[str]]:
330
+ columns = ['index', 'symbol name', 'type', 'calculation']
331
+ data: list[list[str | int]] = [[item.index, item.symbol.name, item.type,
332
+ item.calculation] for item in self]
333
+ return columns, data, ['index']
334
+
335
+
336
+ class elf_file:
337
+ """A class for representing data of an ELF file in a structured form
338
+
339
+ Args:
340
+ data: binary ELF data
341
+
342
+ Attributes:
343
+ byteorder: Byte order of the architecture 'little' or 'big'
344
+ (based on e_ident[EI_DATA])
345
+
346
+ bit_width: Bit with of the architecture: 32 or 64 (based on
347
+ e_ident[EI_CLASS])
348
+
349
+ architecture: Name of the architecture (based on e_machine)
350
+
351
+ fields: All ELF header fields as dict
352
+
353
+ sections: A list of all ELF sections
354
+
355
+ symbols: A list of all ELF symbols
356
+
357
+ functions: A list of all function symbols (STT_FUNC)
358
+
359
+ objects: A list of all variable/object symbols (STT_OBJECT)
360
+
361
+ code_relocations: A list of all code relocations (.rela.text and .rel.text)
362
+
363
+ symbol_table_section: The symbol table section (first section with
364
+ the type SHT_SYMTAB)
365
+
366
+ string_table_section: The string table section (first section with
367
+ the name .strtab)
368
+ """
369
+ def __init__(self, data: bytes):
370
+ self._data = data
371
+
372
+ # Defaults required for function _read_int_from_elf_field
373
+ self.bit_width = 32
374
+ self.byteorder: Literal['little', 'big'] = 'little'
375
+
376
+ assert self._read_bytes_from_elf_field('e_ident[EI_MAG]') == bytes([0x7F, 0x45, 0x4c, 0x46]), 'Not an ELF file'
377
+
378
+ self.bit_width = {1: 32, 2: 64}[self._read_int_from_elf_field('e_ident[EI_CLASS]')]
379
+
380
+ byte_order = self._read_int_from_elf_field('e_ident[EI_DATA]')
381
+ assert byte_order in [1, 2], 'Invalid byte order value e_ident[EI_DATA]'
382
+ self.byteorder = 'little' if byte_order == 1 else 'big'
383
+
384
+ self.fields = {fn: self._read_int_from_elf_field(fn) for fn in fdat.elf_header_field.keys()}
385
+
386
+ arch_entr = fdat.e_machine_dict.get(self.fields['e_machine'])
387
+ self.architecture = arch_entr[0] if arch_entr else str(self.fields['e_machine'])
388
+
389
+ section_data = list(self._list_sections())
390
+ name_addr: dict[str, int] = section_data[self.fields['e_shstrndx']] if section_data else dict()
391
+ section_names = (self.read_string(name_addr['sh_offset'] + f['sh_name']) for f in section_data)
392
+
393
+ self.sections = section_list(elf_section(self, sd, sn, i)
394
+ for i, (sd, sn) in enumerate(zip(section_data, section_names)))
395
+
396
+ ret_sections = [sh for sh in self.sections if sh.type == 'SHT_SYMTAB']
397
+ self.symbol_table_section = ret_sections[0] if ret_sections else None
398
+
399
+ ret_sections = [sh for sh in self.sections if sh.name == '.strtab']
400
+ self.string_table_section = ret_sections[0] if ret_sections else None
401
+
402
+ self.symbols = symbol_list(self._list_symbols())
403
+
404
+ self.functions = symbol_list(s for s in self.symbols if s.info == 'STT_FUNC')
405
+ self.objects = symbol_list(s for s in self.symbols if s.info == 'STT_OBJECT')
406
+
407
+ self.code_relocations = self.get_relocations(['.rela.text', '.rel.text'])
408
+
409
+ def _list_sections(self) -> Generator[dict[str, int], None, None]:
410
+ for i in range(self.fields['e_shnum']):
411
+ offs = self.fields['e_shoff'] + i * self.fields['e_shentsize']
412
+ yield {fn: self._read_from_sh_field(offs, fn) for fn in fdat.section_header.keys()}
413
+
414
+ def _list_symbols(self) -> Generator[elf_symbol, None, None]:
415
+ if self.symbol_table_section:
416
+ offs = self.symbol_table_section['sh_offset']
417
+
418
+ for j, i in enumerate(range(offs, self.symbol_table_section['sh_size'] + offs, self.symbol_table_section['sh_entsize'])):
419
+ ret = {'st_name': self.read_int(i, 4)}
420
+
421
+ if self.bit_width == 32:
422
+ ret['st_value'] = self.read_int(i + 4, 4)
423
+ ret['st_size'] = self.read_int(i + 8, 4)
424
+ ret['st_info'] = self.read_int(i + 12, 1)
425
+ ret['st_other'] = self.read_int(i + 13, 1)
426
+ ret['st_shndx'] = self.read_int(i + 14, 2)
427
+ elif self.bit_width == 64:
428
+ ret['st_info'] = self.read_int(i + 4, 1)
429
+ ret['st_other'] = self.read_int(i + 5, 1)
430
+ ret['st_shndx'] = self.read_int(i + 6, 2)
431
+ ret['st_value'] = self.read_int(i + 8, 8)
432
+ ret['st_size'] = self.read_int(i + 16, 8)
433
+
434
+ yield elf_symbol(self, ret, j)
435
+
436
+ def get_relocations(self, reloc_section: elf_section | str | list[str] | None = None) -> relocation_list:
437
+ """List relocations.
438
+
439
+ Args:
440
+ reloc_section: Specifies the relocation section from which the
441
+ relocations should be listed. It can be provided as
442
+ elf_section object or by its name. If not provided
443
+ (reloc_section=None) relocations from all relocation
444
+ sections are returned.
445
+
446
+ Returns:
447
+ List of relocations
448
+ """
449
+ if isinstance(reloc_section, elf_section):
450
+ assert reloc_section.type in ('SHT_REL', 'SHT_RELA'), f'{reloc_section.name} is not a relocation section'
451
+ return relocation_list(self._list_relocations(reloc_section))
452
+ else:
453
+ relocations: list[elf_relocation] = list()
454
+ for sh in self.sections:
455
+ if sh.type in ('SHT_REL', 'SHT_RELA'):
456
+ if reloc_section is None or \
457
+ (isinstance(reloc_section, str) and sh.name == reloc_section) or \
458
+ (isinstance(reloc_section, list) and sh.name in reloc_section):
459
+ relocations += relocation_list(self._list_relocations(sh))
460
+
461
+ return relocation_list(relocations)
462
+
463
+ def _list_relocations(self, sh: elf_section) -> Generator[elf_relocation, None, None]:
464
+ """List relocations for a elf_section.
465
+
466
+ Args:
467
+ elf_section: Specifies the relocation section from which the
468
+ relocations should be listed.
469
+
470
+ Returns:
471
+ Relocations from specified elf_section
472
+ """
473
+ offs = sh['sh_offset']
474
+ for i, el_off in enumerate(range(offs, sh['sh_size'] + offs, sh['sh_entsize'])):
475
+ ret: dict[str, int] = dict()
476
+
477
+ if self.bit_width == 32:
478
+ ret['r_offset'] = self.read_int(el_off, 4)
479
+ r_info = self.read_int(el_off + 4, 4)
480
+ ret['r_info'] = r_info
481
+ ret['r_addend'] = self.read_int(el_off + 8, 4, True) if sh.type == 'SHT_RELA' else 0
482
+ yield elf_relocation(self, ret, r_info >> 8, r_info & 0xFF, sh['sh_info'], i)
483
+ elif self.bit_width == 64:
484
+ ret['r_offset'] = self.read_int(el_off, 8)
485
+ r_info = self.read_int(el_off + 8, 8)
486
+ ret['r_info'] = r_info
487
+ ret['r_addend'] = self.read_int(el_off + 16, 8, True) if sh.type == 'SHT_RELA' else 0
488
+ yield elf_relocation(self, ret, r_info >> 32, r_info & 0xFFFFFFFF, sh['sh_info'], i)
489
+
490
+ def read_bytes(self, offset: int, num_bytes: int) -> bytes:
491
+ """Read bytes from ELF file.
492
+
493
+ Args:
494
+ offset: Specify first byte relative to the start of
495
+ the ELF file.
496
+
497
+ num_bytes: Specify the number of bytes to read.
498
+
499
+ Returns:
500
+ Binary data as bytes
501
+ """
502
+ return self._data[offset:offset + num_bytes]
503
+
504
+ def read_int(self, offset: int, num_bytes: int, signed: bool = False) -> int:
505
+ """Read an integer from the ELF file. Byte order is
506
+ selected according to the architecture (e_ident[EI_DATA]).
507
+
508
+ Args:
509
+ offset: Specify first byte of the integer relative to
510
+ the start of the ELF file.
511
+
512
+ num_bytes: Specify the size of the integer in bytes.
513
+
514
+ signed: Select if the integer is a signed integer.
515
+
516
+ Returns:
517
+ Integer value
518
+ """
519
+ return int.from_bytes(self._data[offset:offset + num_bytes], self.byteorder, signed=signed)
520
+
521
+ # def int_to_bytes(self, value: int, num_bytes: int = 4, signed: bool = False) -> int:
522
+ # return value.to_bytes(length=num_bytes, byteorder=self.byteorder, signed=signed)
523
+
524
+ def read_string(self, offset: int, encoding: str = 'utf-8') -> str:
525
+ """Read a zero-terminated text string from the ELF file.
526
+
527
+ Args:
528
+ offset: Specify first byte of the string relative to
529
+ the start of the ELF file.
530
+
531
+ encoding: Encoding used for text decoding.
532
+
533
+ Returns:
534
+ Text string
535
+ """
536
+ str_end = self._data.find(b'\x00', offset)
537
+ return self._data[offset:str_end].decode(encoding)
538
+
539
+ def _read_int_from_elf_field(self, field_name: str) -> int:
540
+ field = fdat.elf_header_field[field_name]
541
+ offs = int(field[str(self.bit_width)], base=16)
542
+ byte_len = int(field['size' + str(self.bit_width)])
543
+ return self.read_int(offs, byte_len)
544
+
545
+ def _read_bytes_from_elf_field(self, field_name: str) -> bytes:
546
+ field = fdat.elf_header_field[field_name]
547
+ offs = int(field[str(self.bit_width)], base=16)
548
+ byte_len = int(field['size' + str(self.bit_width)])
549
+ return self.read_bytes(offs, byte_len)
550
+
551
+ def _read_from_sh_field(self, offset: int, field_name: str) -> int:
552
+ field = fdat.section_header[field_name]
553
+ offs = int(field[str(self.bit_width)], base=16) + offset
554
+ byte_len = int(field['size' + str(self.bit_width)])
555
+ return self.read_int(offs, byte_len)
556
+
557
+ def __repr__(self) -> str:
558
+ hf_list = ((hf, self.fields[hf['field_name']]) for hf in fdat.elf_header_field.values())
559
+ return '\n'.join(f"{hf['field_name']:24} {v:4} {hf['description']}" for hf, v in hf_list) + '\n'
560
+
561
+ def __getitem__(self, key: str) -> int:
562
+ assert key in self.fields, f'Unknown field name: {key}'
563
+ return self.fields[key]