1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
|
#!/usr/bin/env python3
#
# ELF disassembler that attempts to be matching
#
import argparse, struct, sys
from libelf import *
from mdebug import *
from mips_isa import *
from util import *
def debug_log(msg):
print(msg, file=sys.stderr)
class MipsDisasm:
"""
"""
def __init__(self, elf_file) -> None:
self.elf_file = elf_file
mdebug_section = elf_file.find_section_by_type(SHT_MIPS_DEBUG)
if mdebug_section is not None:
self.mdebug = mdebug_section
self.has_mdebug = True
else:
self.has_mdebug = False
self.cur_file = None
self.comment_section_pos = 1
self.section_local_labels = {}
def add_section_local_label(self, section, offset):
if section not in self.section_local_labels:
self.section_local_labels.update({section : set()})
self.section_local_labels[section].add(offset)
def advance_file(self):
seen_cur_file = False
for sym in self.elf_file.symtab.symbol_entries:
if sym.type == ST_FILE:
if seen_cur_file or self.cur_file is None:
self.cur_file = sym
break
elif self.cur_file == sym:
seen_cur_file = True
return self.cur_file is not None
def disassemble_all_sections(self):
print(MipsDisasm.asm_prelude())
# debug_log("Name Type Addr Off Size ES Flg Lk Inf Al")
for section in self.elf_file.sections:
local_labels = self.section_local_labels.get(section.name, None)
# debug_log(section)
if section.name in ['', '.strtab', '.shstrtab', '.symtab', '.reginfo', '.comment', '.note', '.options', '.mdebug', '.gptab.data', '.gptab.bss'] or \
(section.sh_type == SHT_REL or section.sh_type == SHT_RELA):
continue
if section.sh_size == 0:
continue
print("")
print(MipsDisasm.begin_section(section))
if section.is_executable():
self.disassemble_exec(section)
elif section.sh_type == SHT_PROGBITS:
# TODO kmc as doesn't support incbin, byte array this
# print(f".incbin \"libultra.a\", 0x{section.sh_offset:08X}, 0x{section.sh_size:X}")
first = True
for i,b in enumerate(section.data):
if local_labels is not None and i in local_labels:
if not first:
print("")
print(f".{section.name[1].upper()}_{i:08X}:")
print(" .byte ", end='')
first = True
elif first:
print(" .byte ", end='')
if not first:
print(", ", end='')
print(f"0x{int(b):02X}", end='')
first = False
print("")
elif section.sh_type == SHT_NOBITS:
print(f".skip 0x{section.sh_size:X}")
else:
assert False, f"Unhandled section: {section.name}"
# debug_log("/// UNHANDLED ///")
def pass_section(self, section):
pass
@staticmethod
def asm_prelude():
return f""".include "macro.inc"
#include "regdef.h"
// assembler directives
.set noat // allow manual use of $at
.set noreorder // don't insert nops after branches"""
@staticmethod
def begin_section(section):
section_flags = section.flags_str().lower().replace(' ', '')
if section_flags != "":
section_flags = f", \"{section_flags}\""
section_type = ""
if section.sh_type == SHT_PROGBITS:
section_type = ", @progbits"
elif section.sh_type == SHT_NOBITS:
section_type = ", @nobits"
if section_type != "" and section_flags == "":
section_flags = ", \"\""
return f""".section {section.name}{section_flags}{section_type}
.balign {section.sh_addralign}
"""
def get_label_name(self, addr, pdr=None, optional=False):
if pdr is not None:
sym = pdr.lookup_sym(addr, EcoffSt.LABEL)
if sym is not None:
return sym.name
if not optional:
return f".L{addr:08X}"
else:
return None
def get_comment_string(self, start):
comment_section = self.elf_file.find_section_by_name(".comment")
end = comment_section.data.find(b'\0', start)
if end == -1:
return None, None
comment = comment_section.data[start:end].decode("ASCII")
return comment, end + 1
def print_end(self, vaddr, eof):
ends = eof.get(vaddr, None)
if ends is not None:
for sym in ends:
print(f" .type {sym.name}, @{'function' if sym.type == ST_FUNC else 'object'}")
if sym.st_size != 0:
print(f" .size {sym.name}, . - {sym.name}")
print(f" .end {sym.name}\n")
def disassemble_exec(self, section):
raw_insns = as_word_list(section.data)
insns = [decode_insn(raw, section.sh_addr + j * 4) for j,raw in enumerate(raw_insns)]
# enumerate branch labels
branch_labels = set()
for i,insn in enumerate(insns):
if insn.id in MIPS_BRANCH_INSNS or insn.id == MIPS_INS_J:
branch_labels.add(insn.target if insn.id == MIPS_INS_J else insn.offset)
eof = {} # vaddr : name
def add_end(vaddr, sym):
if vaddr not in eof:
eof[vaddr] = set()
eof[vaddr].add(sym)
cur_fdr = None
cur_pdr = None
for i,insn in enumerate(insns):
mnemonic = insn.mnemonic
op_str = insn.op_str
# Update mdebug info
src_inf = ""
if self.has_mdebug:
# Get new fdr if there is one
fdr = self.mdebug.fdr_foraddr(i * 4, extensions=('.c', '.s'))
if fdr is not None:
# debug_log(fdr.name)
cur_fdr = fdr
# Get new pdr if there is one
if cur_fdr is not None:
pdr = cur_fdr.pdr_foraddr(i * 4)
if pdr is not None:
# debug_log(pdr)
cur_pdr = pdr
# Line numbers
if cur_pdr is not None:
asm_line = i - cur_pdr.addr//4
if asm_line < len(cur_pdr.lines):
src_inf = f" {cur_pdr.lines[asm_line]:4}"
else:
src_inf = " PADDING"
# Symbols for this address
syms = section.get_sym(i * 4)
# if len(syms) != 0:
# debug_log("\n".join([str(sym) for sym in syms]))
# Print end
self.print_end(insn.vaddr, eof)
# Print symbol
for sym in syms:
if sym.name == "gcc2_compiled.":
print(f"// compiler generated")
if self.cur_file is None:
print(f".version \"01.01\"")
if self.advance_file():
print(f".file 1 \"{self.cur_file.name}\"")
comment_string = None
while comment_string != "\"GCC: (GNU) 2.7.2\"":
comment_string, self.comment_section_pos = self.get_comment_string(self.comment_section_pos)
if comment_string is None:
break
print(f".ident \"{comment_string}\"")
if sym.bind == SB_GLOBAL:
print(f"glabel {sym.name}")
else:
print(f"{sym.name}:")
if sym.st_size != 0:
print(f" .ent {sym.name}")
add_end(insn.vaddr + sym.st_size, sym)
else:
print(f" .type {sym.name}, @{'function' if sym.type == ST_FUNC else 'object'}\n")
# Print branch labels
lbl = self.get_label_name(insn.vaddr, pdr=cur_pdr, optional=not insn.vaddr in branch_labels)
if lbl is not None:
print(f"{lbl}:")
# Relocations for this address
rels = section.get_rel(i * 4)
assert len(rels) < 2 # There should never be more than 1 relocation for a single address, right?
# if len(rels) != 0:
# debug_log("\n".join([str(rel) for rel in rels]))
# Apply relocation
if len(rels) != 0:
rel = rels[0]
if rel.rel_type == R_MIPS_26:
if insn.id == MIPS_INS_JAL:
op_str = rel.relocated_symbol.name
if op_str == ".text" and cur_fdr is not None:
pdr = cur_fdr.pdr_foraddr(insn.target)
if pdr is not None:
op_str = pdr.name
elif insn.id != MIPS_INS_J: # Branch labels for j instructions are also R_MIPS_26 relocations
assert False , f"Got unexpected R_MIPS_26 relocation {insn.id}"
elif rel.rel_type == R_MIPS_HI16:
assert insn.id in [MIPS_INS_LUI]
rel_name = rel.relocated_symbol.name
if rel.relocated_symbol.type == ST_SECTION:
rel_name = f".{rel_name[1].upper()}_00000000"
if cur_fdr is not None:
pass
op_str = f"{insn.abi.gpr_names[insn.rt]}, %hi({rel_name})"
elif rel.rel_type == R_MIPS_LO16:
# Ideally this should be in the elf code so the relocations don't look identical
addend = insn.imm
rel_name = rel.relocated_symbol.name
if rel.relocated_symbol.type == ST_SECTION:
rel_name = f".{rel_name[1].upper()}_{addend:08X}"
self.add_section_local_label(rel.relocated_symbol.name, addend)
addend = 0
addend_str = f" + 0x{addend:X}" if addend != 0 else ""
if insn.id == MIPS_INS_ADDIU:
op_str = f"{insn.abi.gpr_names[insn.rt]}, {insn.abi.gpr_names[insn.rs]}, %lo({rel_name}{addend_str})"
elif insn.id in MIPS_LOAD_STORE_INSNS:
if insn.id in MIPS_FP_LOAD_STORE_INSNS:
op_str = f"{insn.abi.cop1_names[insn.ft]}, "
else:
op_str = f"{insn.abi.gpr_names[insn.rt]}, "
op_str += f"%lo({rel_name}{addend_str})({insn.abi.gpr_names[insn.base]})"
else:
assert False
else:
assert False
# Apply branch labels
if insn.id in MIPS_BRANCH_INSNS:
op_str_parts = []
for field in insn.fields:
if field == 'offset':
op_str_parts.append(self.get_label_name(insn.offset, cur_pdr))
else:
op_str_parts.append(insn.format_field(field))
op_str = ", ".join(op_str_parts)
elif insn.id == MIPS_INS_J:
op_str = self.get_label_name(insn.target, cur_pdr)
print(f"/* {section.sh_offset + i * 4:06X} {insn.vaddr:08X} {insn.raw:08X}{src_inf} */ {mnemonic:12}{op_str:35}".rstrip())
self.print_end(section.sh_addr + section.sh_size, eof)
def main():
parser = argparse.ArgumentParser(description="Disassemble relocatable ELF object.")
parser.add_argument("filepath", help="path to the ELF file")
# TODO unimplemented optionals
parser.add_argument("--compiler", help="original compiler that produced the ELF (IDO or GCC, IDO default)", default="IDO")
parser.add_argument("--strenc", help="string encoding, default is EUC-JP for IDO and SJIS for GCC")
args = parser.parse_args()
elf_file = None
with open(args.filepath, "rb") as elf:
elf_file = ElfFile(bytearray(elf.read()))
disassembler = MipsDisasm(elf_file)
disassembler.disassemble_all_sections()
if __name__ == '__main__':
main()
|