Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
MorsGames
GitHub Repository: MorsGames/sm64plus
Path: blob/master/tools/asm_processor/asm-processor.py
7857 views
1
#!/usr/bin/env python3
2
import argparse
3
import tempfile
4
import struct
5
import copy
6
import sys
7
import re
8
import os
9
from collections import namedtuple
10
from io import StringIO
11
12
MAX_FN_SIZE = 100
13
SLOW_CHECKS = False
14
15
EI_NIDENT = 16
16
EI_CLASS = 4
17
EI_DATA = 5
18
EI_VERSION = 6
19
EI_OSABI = 7
20
EI_ABIVERSION = 8
21
STN_UNDEF = 0
22
23
SHN_UNDEF = 0
24
SHN_ABS = 0xfff1
25
SHN_COMMON = 0xfff2
26
SHN_XINDEX = 0xffff
27
SHN_LORESERVE = 0xff00
28
29
STT_NOTYPE = 0
30
STT_OBJECT = 1
31
STT_FUNC = 2
32
STT_SECTION = 3
33
STT_FILE = 4
34
STT_COMMON = 5
35
STT_TLS = 6
36
37
STB_LOCAL = 0
38
STB_GLOBAL = 1
39
STB_WEAK = 2
40
41
STV_DEFAULT = 0
42
STV_INTERNAL = 1
43
STV_HIDDEN = 2
44
STV_PROTECTED = 3
45
46
SHT_NULL = 0
47
SHT_PROGBITS = 1
48
SHT_SYMTAB = 2
49
SHT_STRTAB = 3
50
SHT_RELA = 4
51
SHT_HASH = 5
52
SHT_DYNAMIC = 6
53
SHT_NOTE = 7
54
SHT_NOBITS = 8
55
SHT_REL = 9
56
SHT_SHLIB = 10
57
SHT_DYNSYM = 11
58
SHT_INIT_ARRAY = 14
59
SHT_FINI_ARRAY = 15
60
SHT_PREINIT_ARRAY = 16
61
SHT_GROUP = 17
62
SHT_SYMTAB_SHNDX = 18
63
SHT_MIPS_GPTAB = 0x70000003
64
SHT_MIPS_DEBUG = 0x70000005
65
SHT_MIPS_REGINFO = 0x70000006
66
SHT_MIPS_OPTIONS = 0x7000000d
67
68
SHF_WRITE = 0x1
69
SHF_ALLOC = 0x2
70
SHF_EXECINSTR = 0x4
71
SHF_MERGE = 0x10
72
SHF_STRINGS = 0x20
73
SHF_INFO_LINK = 0x40
74
SHF_LINK_ORDER = 0x80
75
SHF_OS_NONCONFORMING = 0x100
76
SHF_GROUP = 0x200
77
SHF_TLS = 0x400
78
79
R_MIPS_32 = 2
80
R_MIPS_26 = 4
81
R_MIPS_HI16 = 5
82
R_MIPS_LO16 = 6
83
84
85
class ElfHeader:
86
"""
87
typedef struct {
88
unsigned char e_ident[EI_NIDENT];
89
Elf32_Half e_type;
90
Elf32_Half e_machine;
91
Elf32_Word e_version;
92
Elf32_Addr e_entry;
93
Elf32_Off e_phoff;
94
Elf32_Off e_shoff;
95
Elf32_Word e_flags;
96
Elf32_Half e_ehsize;
97
Elf32_Half e_phentsize;
98
Elf32_Half e_phnum;
99
Elf32_Half e_shentsize;
100
Elf32_Half e_shnum;
101
Elf32_Half e_shstrndx;
102
} Elf32_Ehdr;
103
"""
104
105
def __init__(self, data):
106
self.e_ident = data[:EI_NIDENT]
107
self.e_type, self.e_machine, self.e_version, self.e_entry, self.e_phoff, self.e_shoff, self.e_flags, self.e_ehsize, self.e_phentsize, self.e_phnum, self.e_shentsize, self.e_shnum, self.e_shstrndx = struct.unpack('>HHIIIIIHHHHHH', data[EI_NIDENT:])
108
assert self.e_ident[EI_CLASS] == 1 # 32-bit
109
assert self.e_ident[EI_DATA] == 2 # big-endian
110
assert self.e_type == 1 # relocatable
111
assert self.e_machine == 8 # MIPS I Architecture
112
assert self.e_phoff == 0 # no program header
113
assert self.e_shoff != 0 # section header
114
assert self.e_shstrndx != SHN_UNDEF
115
116
def to_bin(self):
117
return self.e_ident + struct.pack('>HHIIIIIHHHHHH', self.e_type,
118
self.e_machine, self.e_version, self.e_entry, self.e_phoff,
119
self.e_shoff, self.e_flags, self.e_ehsize, self.e_phentsize,
120
self.e_phnum, self.e_shentsize, self.e_shnum, self.e_shstrndx)
121
122
123
class Symbol:
124
"""
125
typedef struct {
126
Elf32_Word st_name;
127
Elf32_Addr st_value;
128
Elf32_Word st_size;
129
unsigned char st_info;
130
unsigned char st_other;
131
Elf32_Half st_shndx;
132
} Elf32_Sym;
133
"""
134
135
def __init__(self, data, strtab):
136
self.st_name, self.st_value, self.st_size, st_info, self.st_other, self.st_shndx = struct.unpack('>IIIBBH', data)
137
assert self.st_shndx != SHN_XINDEX, "too many sections (SHN_XINDEX not supported)"
138
self.bind = st_info >> 4
139
self.type = st_info & 15
140
self.name = strtab.lookup_str(self.st_name)
141
self.visibility = self.st_other & 3
142
143
def to_bin(self):
144
st_info = (self.bind << 4) | self.type
145
return struct.pack('>IIIBBH', self.st_name, self.st_value, self.st_size, st_info, self.st_other, self.st_shndx)
146
147
148
class Relocation:
149
def __init__(self, data, sh_type):
150
self.sh_type = sh_type
151
if sh_type == SHT_REL:
152
self.r_offset, self.r_info = struct.unpack('>II', data)
153
else:
154
self.r_offset, self.r_info, self.r_addend = struct.unpack('>III', data)
155
self.sym_index = self.r_info >> 8
156
self.rel_type = self.r_info & 0xff
157
158
def to_bin(self):
159
self.r_info = (self.sym_index << 8) | self.rel_type
160
if self.sh_type == SHT_REL:
161
return struct.pack('>II', self.r_offset, self.r_info)
162
else:
163
return struct.pack('>III', self.r_offset, self.r_info, self.r_addend)
164
165
166
class Section:
167
"""
168
typedef struct {
169
Elf32_Word sh_name;
170
Elf32_Word sh_type;
171
Elf32_Word sh_flags;
172
Elf32_Addr sh_addr;
173
Elf32_Off sh_offset;
174
Elf32_Word sh_size;
175
Elf32_Word sh_link;
176
Elf32_Word sh_info;
177
Elf32_Word sh_addralign;
178
Elf32_Word sh_entsize;
179
} Elf32_Shdr;
180
"""
181
182
def __init__(self, header, data, index):
183
self.sh_name, self.sh_type, self.sh_flags, self.sh_addr, self.sh_offset, self.sh_size, self.sh_link, self.sh_info, self.sh_addralign, self.sh_entsize = struct.unpack('>IIIIIIIIII', header)
184
assert not self.sh_flags & SHF_LINK_ORDER
185
if self.sh_entsize != 0:
186
assert self.sh_size % self.sh_entsize == 0
187
if self.sh_type == SHT_NOBITS:
188
self.data = b''
189
else:
190
self.data = data[self.sh_offset:self.sh_offset + self.sh_size]
191
self.index = index
192
self.relocated_by = []
193
194
@staticmethod
195
def from_parts(sh_name, sh_type, sh_flags, sh_link, sh_info, sh_addralign, sh_entsize, data, index):
196
header = struct.pack('>IIIIIIIIII', sh_name, sh_type, sh_flags, 0, 0, len(data), sh_link, sh_info, sh_addralign, sh_entsize)
197
return Section(header, data, index)
198
199
def lookup_str(self, index):
200
assert self.sh_type == SHT_STRTAB
201
to = self.data.find(b'\0', index)
202
assert to != -1
203
return self.data[index:to].decode('latin1')
204
205
def add_str(self, string):
206
assert self.sh_type == SHT_STRTAB
207
ret = len(self.data)
208
self.data += string.encode('latin1') + b'\0'
209
return ret
210
211
def is_rel(self):
212
return self.sh_type == SHT_REL or self.sh_type == SHT_RELA
213
214
def header_to_bin(self):
215
if self.sh_type != SHT_NOBITS:
216
self.sh_size = len(self.data)
217
return struct.pack('>IIIIIIIIII', self.sh_name, self.sh_type, self.sh_flags, self.sh_addr, self.sh_offset, self.sh_size, self.sh_link, self.sh_info, self.sh_addralign, self.sh_entsize)
218
219
def late_init(self, sections):
220
if self.sh_type == SHT_SYMTAB:
221
self.init_symbols(sections)
222
elif self.is_rel():
223
self.rel_target = sections[self.sh_info]
224
self.rel_target.relocated_by.append(self)
225
self.init_relocs()
226
227
def find_symbol(self, name):
228
assert self.sh_type == SHT_SYMTAB
229
for s in self.symbol_entries:
230
if s.name == name:
231
return (s.st_shndx, s.st_value)
232
return None
233
234
def find_symbol_in_section(self, name, section):
235
pos = self.find_symbol(name)
236
assert pos is not None
237
assert pos[0] == section.index
238
return pos[1]
239
240
def init_symbols(self, sections):
241
assert self.sh_type == SHT_SYMTAB
242
assert self.sh_entsize == 16
243
self.strtab = sections[self.sh_link]
244
entries = []
245
for i in range(0, self.sh_size, self.sh_entsize):
246
entries.append(Symbol(self.data[i:i+self.sh_entsize], self.strtab))
247
self.symbol_entries = entries
248
249
def init_relocs(self):
250
assert self.is_rel()
251
entries = []
252
for i in range(0, self.sh_size, self.sh_entsize):
253
entries.append(Relocation(self.data[i:i+self.sh_entsize], self.sh_type))
254
self.relocations = entries
255
256
def local_symbols(self):
257
assert self.sh_type == SHT_SYMTAB
258
return self.symbol_entries[:self.sh_info]
259
260
def global_symbols(self):
261
assert self.sh_type == SHT_SYMTAB
262
return self.symbol_entries[self.sh_info:]
263
264
265
class ElfFile:
266
def __init__(self, data):
267
self.data = data
268
assert data[:4] == b'\x7fELF', "not an ELF file"
269
270
self.elf_header = ElfHeader(data[0:52])
271
272
offset, size = self.elf_header.e_shoff, self.elf_header.e_shentsize
273
null_section = Section(data[offset:offset + size], data, 0)
274
num_sections = self.elf_header.e_shnum or null_section.sh_size
275
276
self.sections = [null_section]
277
for i in range(1, num_sections):
278
ind = offset + i * size
279
self.sections.append(Section(data[ind:ind + size], data, i))
280
281
symtab = None
282
for s in self.sections:
283
if s.sh_type == SHT_SYMTAB:
284
assert not symtab
285
symtab = s
286
assert symtab is not None
287
self.symtab = symtab
288
289
shstr = self.sections[self.elf_header.e_shstrndx]
290
for s in self.sections:
291
s.name = shstr.lookup_str(s.sh_name)
292
s.late_init(self.sections)
293
294
def find_section(self, name):
295
for s in self.sections:
296
if s.name == name:
297
return s
298
return None
299
300
def add_section(self, name, sh_type, sh_flags, sh_link, sh_info, sh_addralign, sh_entsize, data):
301
shstr = self.sections[self.elf_header.e_shstrndx]
302
sh_name = shstr.add_str(name)
303
s = Section.from_parts(sh_name=sh_name, sh_type=sh_type,
304
sh_flags=sh_flags, sh_link=sh_link, sh_info=sh_info,
305
sh_addralign=sh_addralign, sh_entsize=sh_entsize, data=data,
306
index=len(self.sections))
307
self.sections.append(s)
308
s.name = name
309
s.late_init(self.sections)
310
return s
311
312
def drop_irrelevant_sections(self):
313
# We can only drop sections at the end, since otherwise section
314
# references might be wrong. Luckily, these sections typically are.
315
while self.sections[-1].sh_type in [SHT_MIPS_DEBUG, SHT_MIPS_GPTAB]:
316
self.sections.pop()
317
318
def write(self, filename):
319
outfile = open(filename, 'wb')
320
outidx = 0
321
def write_out(data):
322
nonlocal outidx
323
outfile.write(data)
324
outidx += len(data)
325
def pad_out(align):
326
if align and outidx % align:
327
write_out(b'\0' * (align - outidx % align))
328
329
self.elf_header.e_shnum = len(self.sections)
330
write_out(self.elf_header.to_bin())
331
332
for s in self.sections:
333
if s.sh_type != SHT_NOBITS and s.sh_type != SHT_NULL:
334
pad_out(s.sh_addralign)
335
s.sh_offset = outidx
336
write_out(s.data)
337
338
pad_out(4)
339
self.elf_header.e_shoff = outidx
340
for s in self.sections:
341
write_out(s.header_to_bin())
342
343
outfile.seek(0)
344
outfile.write(self.elf_header.to_bin())
345
outfile.close()
346
347
348
def is_temp_name(name):
349
return name.startswith('_asmpp_')
350
351
352
# https://stackoverflow.com/a/241506
353
def re_comment_replacer(match):
354
s = match.group(0)
355
if s[0] in "/#":
356
return " "
357
else:
358
return s
359
360
361
re_comment_or_string = re.compile(
362
r'#.*|/\*.*?\*/|"(?:\\.|[^\\"])*"'
363
)
364
365
366
class Failure(Exception):
367
def __init__(self, message):
368
self.message = message
369
370
def __str__(self):
371
return self.message
372
373
374
class GlobalState:
375
def __init__(self, min_instr_count, skip_instr_count, use_jtbl_for_rodata):
376
# A value that hopefully never appears as a 32-bit rodata constant (or we
377
# miscompile late rodata). Increases by 1 in each step.
378
self.late_rodata_hex = 0xE0123456
379
self.namectr = 0
380
self.min_instr_count = min_instr_count
381
self.skip_instr_count = skip_instr_count
382
self.use_jtbl_for_rodata = use_jtbl_for_rodata
383
384
def next_late_rodata_hex(self):
385
dummy_bytes = struct.pack('>I', self.late_rodata_hex)
386
if (self.late_rodata_hex & 0xffff) == 0:
387
# Avoid lui
388
self.late_rodata_hex += 1
389
self.late_rodata_hex += 1
390
return dummy_bytes
391
392
def make_name(self, cat):
393
self.namectr += 1
394
return '_asmpp_{}{}'.format(cat, self.namectr)
395
396
397
Function = namedtuple('Function', ['text_glabels', 'asm_conts', 'late_rodata_dummy_bytes', 'jtbl_rodata_size', 'late_rodata_asm_conts', 'fn_desc', 'data'])
398
399
400
class GlobalAsmBlock:
401
def __init__(self, fn_desc):
402
self.fn_desc = fn_desc
403
self.cur_section = '.text'
404
self.asm_conts = []
405
self.late_rodata_asm_conts = []
406
self.late_rodata_alignment = 0
407
self.late_rodata_alignment_from_content = False
408
self.text_glabels = []
409
self.fn_section_sizes = {
410
'.text': 0,
411
'.data': 0,
412
'.bss': 0,
413
'.rodata': 0,
414
'.late_rodata': 0,
415
}
416
self.fn_ins_inds = []
417
self.glued_line = ''
418
self.num_lines = 0
419
420
def fail(self, message, line=None):
421
context = self.fn_desc
422
if line:
423
context += ", at line \"" + line + "\""
424
raise Failure(message + "\nwithin " + context)
425
426
def count_quoted_size(self, line, z, real_line, output_enc):
427
line = line.encode(output_enc).decode('latin1')
428
in_quote = False
429
num_parts = 0
430
ret = 0
431
i = 0
432
digits = "0123456789" # 0-7 would be more sane, but this matches GNU as
433
while i < len(line):
434
c = line[i]
435
i += 1
436
if not in_quote:
437
if c == '"':
438
in_quote = True
439
num_parts += 1
440
else:
441
if c == '"':
442
in_quote = False
443
continue
444
ret += 1
445
if c != '\\':
446
continue
447
if i == len(line):
448
self.fail("backslash at end of line not supported", real_line)
449
c = line[i]
450
i += 1
451
# (if c is in "bfnrtv", we have a real escaped literal)
452
if c == 'x':
453
# hex literal, consume any number of hex chars, possibly none
454
while i < len(line) and line[i] in digits + "abcdefABCDEF":
455
i += 1
456
elif c in digits:
457
# octal literal, consume up to two more digits
458
it = 0
459
while i < len(line) and line[i] in digits and it < 2:
460
i += 1
461
it += 1
462
463
if in_quote:
464
self.fail("unterminated string literal", real_line)
465
if num_parts == 0:
466
self.fail(".ascii with no string", real_line)
467
return ret + num_parts if z else ret
468
469
def align2(self):
470
while self.fn_section_sizes[self.cur_section] % 2 != 0:
471
self.fn_section_sizes[self.cur_section] += 1
472
473
def align4(self):
474
while self.fn_section_sizes[self.cur_section] % 4 != 0:
475
self.fn_section_sizes[self.cur_section] += 1
476
477
def add_sized(self, size, line):
478
if self.cur_section in ['.text', '.late_rodata']:
479
if size % 4 != 0:
480
self.fail("size must be a multiple of 4", line)
481
if size < 0:
482
self.fail("size cannot be negative", line)
483
self.fn_section_sizes[self.cur_section] += size
484
if self.cur_section == '.text':
485
if not self.text_glabels:
486
self.fail(".text block without an initial glabel", line)
487
self.fn_ins_inds.append((self.num_lines - 1, size // 4))
488
489
def process_line(self, line, output_enc):
490
self.num_lines += 1
491
if line.endswith('\\'):
492
self.glued_line += line[:-1]
493
return
494
line = self.glued_line + line
495
self.glued_line = ''
496
497
real_line = line
498
line = re.sub(re_comment_or_string, re_comment_replacer, line)
499
line = line.strip()
500
line = re.sub(r'^[a-zA-Z0-9_]+:\s*', '', line)
501
changed_section = False
502
emitting_double = False
503
if line.startswith('glabel ') and self.cur_section == '.text':
504
self.text_glabels.append(line.split()[1])
505
if not line:
506
pass # empty line
507
elif line.startswith('glabel ') or (' ' not in line and line.endswith(':')):
508
pass # label
509
elif line.startswith('.section') or line in ['.text', '.data', '.rdata', '.rodata', '.bss', '.late_rodata']:
510
# section change
511
self.cur_section = '.rodata' if line == '.rdata' else line.split(',')[0].split()[-1]
512
if self.cur_section not in ['.data', '.text', '.rodata', '.late_rodata', '.bss']:
513
self.fail("unrecognized .section directive", real_line)
514
changed_section = True
515
elif line.startswith('.late_rodata_alignment'):
516
if self.cur_section != '.late_rodata':
517
self.fail(".late_rodata_alignment must occur within .late_rodata section", real_line)
518
value = int(line.split()[1])
519
if value not in [4, 8]:
520
self.fail(".late_rodata_alignment argument must be 4 or 8", real_line)
521
if self.late_rodata_alignment and self.late_rodata_alignment != value:
522
self.fail(".late_rodata_alignment alignment assumption conflicts with earlier .double directive. Make sure to provide explicit alignment padding.")
523
self.late_rodata_alignment = value
524
changed_section = True
525
elif line.startswith('.incbin'):
526
self.add_sized(int(line.split(',')[-1].strip(), 0), real_line)
527
elif line.startswith('.word') or line.startswith('.float'):
528
self.align4()
529
self.add_sized(4 * len(line.split(',')), real_line)
530
elif line.startswith('.double'):
531
self.align4()
532
if self.cur_section == '.late_rodata':
533
align8 = self.fn_section_sizes[self.cur_section] % 8
534
# Automatically set late_rodata_alignment, so the generated C code uses doubles.
535
# This gives us correct alignment for the transferred doubles even when the
536
# late_rodata_alignment is wrong, e.g. for non-matching compilation.
537
if not self.late_rodata_alignment:
538
self.late_rodata_alignment = 8 - align8
539
self.late_rodata_alignment_from_content = True
540
elif self.late_rodata_alignment != 8 - align8:
541
if self.late_rodata_alignment_from_content:
542
self.fail("found two .double directives with different start addresses mod 8. Make sure to provide explicit alignment padding.", real_line)
543
else:
544
self.fail(".double at address that is not 0 mod 8 (based on .late_rodata_alignment assumption). Make sure to provide explicit alignment padding.", real_line)
545
self.add_sized(8 * len(line.split(',')), real_line)
546
emitting_double = True
547
elif line.startswith('.space'):
548
self.add_sized(int(line.split()[1], 0), real_line)
549
elif line.startswith('.balign') or line.startswith('.align'):
550
align = int(line.split()[1])
551
if align != 4:
552
self.fail("only .balign 4 is supported", real_line)
553
self.align4()
554
elif line.startswith('.asci'):
555
z = (line.startswith('.asciz') or line.startswith('.asciiz'))
556
self.add_sized(self.count_quoted_size(line, z, real_line, output_enc), real_line)
557
elif line.startswith('.byte'):
558
self.add_sized(len(line.split(',')), real_line)
559
elif line.startswith('.half'):
560
self.align2()
561
self.add_sized(2*len(line.split(',')), real_line)
562
elif line.startswith('.'):
563
# .macro, ...
564
self.fail("asm directive not supported", real_line)
565
else:
566
# Unfortunately, macros are hard to support for .rodata --
567
# we don't know how how space they will expand to before
568
# running the assembler, but we need that information to
569
# construct the C code. So if we need that we'll either
570
# need to run the assembler twice (at least in some rare
571
# cases), or change how this program is invoked.
572
# Similarly, we can't currently deal with pseudo-instructions
573
# that expand to several real instructions.
574
if self.cur_section != '.text':
575
self.fail("instruction or macro call in non-.text section? not supported", real_line)
576
self.add_sized(4, real_line)
577
if self.cur_section == '.late_rodata':
578
if not changed_section:
579
if emitting_double:
580
self.late_rodata_asm_conts.append(".align 0")
581
self.late_rodata_asm_conts.append(real_line)
582
if emitting_double:
583
self.late_rodata_asm_conts.append(".align 2")
584
else:
585
self.asm_conts.append(real_line)
586
587
def finish(self, state):
588
src = [''] * (self.num_lines + 1)
589
late_rodata_dummy_bytes = []
590
jtbl_rodata_size = 0
591
late_rodata_fn_output = []
592
593
num_instr = self.fn_section_sizes['.text'] // 4
594
595
if self.fn_section_sizes['.late_rodata'] > 0:
596
# Generate late rodata by emitting unique float constants.
597
# This requires 3 instructions for each 4 bytes of rodata.
598
# If we know alignment, we can use doubles, which give 3
599
# instructions for 8 bytes of rodata.
600
size = self.fn_section_sizes['.late_rodata'] // 4
601
skip_next = False
602
needs_double = (self.late_rodata_alignment != 0)
603
for i in range(size):
604
if skip_next:
605
skip_next = False
606
continue
607
# Jump tables give 9 instructions for >= 5 words of rodata, and should be
608
# emitted when:
609
# - -O2 or -O2 -g3 are used, which give the right codegen
610
# - we have emitted our first .float/.double (to ensure that we find the
611
# created rodata in the binary)
612
# - we have emitted our first .double, if any (to ensure alignment of doubles
613
# in shifted rodata sections)
614
# - we have at least 5 words of rodata left to emit (otherwise IDO does not
615
# generate a jump table)
616
# - we have at least 10 more instructions to go in this function (otherwise our
617
# function size computation will be wrong since the delay slot goes unused)
618
if (not needs_double and state.use_jtbl_for_rodata and i >= 1 and
619
size - i >= 5 and num_instr - len(late_rodata_fn_output) >= 10):
620
cases = " ".join("case {}:".format(case) for case in range(size - i))
621
late_rodata_fn_output.append("switch (*(volatile int*)0) { " + cases + " ; }")
622
late_rodata_fn_output.extend([""] * 8)
623
jtbl_rodata_size = (size - i) * 4
624
break
625
dummy_bytes = state.next_late_rodata_hex()
626
late_rodata_dummy_bytes.append(dummy_bytes)
627
if self.late_rodata_alignment == 4 * ((i + 1) % 2 + 1) and i + 1 < size:
628
dummy_bytes2 = state.next_late_rodata_hex()
629
late_rodata_dummy_bytes.append(dummy_bytes2)
630
fval, = struct.unpack('>d', dummy_bytes + dummy_bytes2)
631
late_rodata_fn_output.append('*(volatile double*)0 = {};'.format(fval))
632
skip_next = True
633
needs_double = True
634
else:
635
fval, = struct.unpack('>f', dummy_bytes)
636
late_rodata_fn_output.append('*(volatile float*)0 = {}f;'.format(fval))
637
late_rodata_fn_output.append('')
638
late_rodata_fn_output.append('')
639
640
text_name = None
641
if self.fn_section_sizes['.text'] > 0 or late_rodata_fn_output:
642
text_name = state.make_name('func')
643
src[0] = 'void {}(void) {{'.format(text_name)
644
src[self.num_lines] = '}'
645
instr_count = self.fn_section_sizes['.text'] // 4
646
if instr_count < state.min_instr_count:
647
self.fail("too short .text block")
648
tot_emitted = 0
649
tot_skipped = 0
650
fn_emitted = 0
651
fn_skipped = 0
652
rodata_stack = late_rodata_fn_output[::-1]
653
for (line, count) in self.fn_ins_inds:
654
for _ in range(count):
655
if (fn_emitted > MAX_FN_SIZE and instr_count - tot_emitted > state.min_instr_count and
656
(not rodata_stack or rodata_stack[-1])):
657
# Don't let functions become too large. When a function reaches 284
658
# instructions, and -O2 -framepointer flags are passed, the IRIX
659
# compiler decides it is a great idea to start optimizing more.
660
fn_emitted = 0
661
fn_skipped = 0
662
src[line] += ' }} void {}(void) {{ '.format(state.make_name('large_func'))
663
if fn_skipped < state.skip_instr_count:
664
fn_skipped += 1
665
tot_skipped += 1
666
elif rodata_stack:
667
src[line] += rodata_stack.pop()
668
else:
669
src[line] += '*(volatile int*)0 = 0;'
670
tot_emitted += 1
671
fn_emitted += 1
672
if rodata_stack:
673
size = len(late_rodata_fn_output) // 3
674
available = instr_count - tot_skipped
675
self.fail(
676
"late rodata to text ratio is too high: {} / {} must be <= 1/3\n"
677
"add .late_rodata_alignment (4|8) to the .late_rodata "
678
"block to double the allowed ratio."
679
.format(size, available))
680
681
rodata_name = None
682
if self.fn_section_sizes['.rodata'] > 0:
683
rodata_name = state.make_name('rodata')
684
src[self.num_lines] += ' const char {}[{}] = {{1}};'.format(rodata_name, self.fn_section_sizes['.rodata'])
685
686
data_name = None
687
if self.fn_section_sizes['.data'] > 0:
688
data_name = state.make_name('data')
689
src[self.num_lines] += ' char {}[{}] = {{1}};'.format(data_name, self.fn_section_sizes['.data'])
690
691
bss_name = None
692
if self.fn_section_sizes['.bss'] > 0:
693
bss_name = state.make_name('bss')
694
src[self.num_lines] += ' char {}[{}];'.format(bss_name, self.fn_section_sizes['.bss'])
695
696
fn = Function(
697
text_glabels=self.text_glabels,
698
asm_conts=self.asm_conts,
699
late_rodata_dummy_bytes=late_rodata_dummy_bytes,
700
jtbl_rodata_size=jtbl_rodata_size,
701
late_rodata_asm_conts=self.late_rodata_asm_conts,
702
fn_desc=self.fn_desc,
703
data={
704
'.text': (text_name, self.fn_section_sizes['.text']),
705
'.data': (data_name, self.fn_section_sizes['.data']),
706
'.rodata': (rodata_name, self.fn_section_sizes['.rodata']),
707
'.bss': (bss_name, self.fn_section_sizes['.bss']),
708
})
709
return src, fn
710
711
cutscene_data_regexpr = re.compile(r"CutsceneData (.|\n)*\[\] = {")
712
float_regexpr = re.compile(r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?f")
713
714
def repl_float_hex(m):
715
return str(struct.unpack(">I", struct.pack(">f", float(m.group(0).strip().rstrip("f"))))[0])
716
717
def parse_source(f, opt, framepointer, input_enc, output_enc, print_source=None):
718
if opt in ['O2', 'O1']:
719
if framepointer:
720
min_instr_count = 6
721
skip_instr_count = 5
722
else:
723
min_instr_count = 2
724
skip_instr_count = 1
725
elif opt == 'g':
726
if framepointer:
727
min_instr_count = 7
728
skip_instr_count = 7
729
else:
730
min_instr_count = 4
731
skip_instr_count = 4
732
else:
733
if opt != 'g3':
734
raise Failure("must pass one of -g, -O1, -O2, -O2 -g3")
735
if framepointer:
736
min_instr_count = 4
737
skip_instr_count = 4
738
else:
739
min_instr_count = 2
740
skip_instr_count = 2
741
742
use_jtbl_for_rodata = False
743
if opt in ['O2', 'g3'] and not framepointer:
744
use_jtbl_for_rodata = True
745
746
state = GlobalState(min_instr_count, skip_instr_count, use_jtbl_for_rodata)
747
748
global_asm = None
749
asm_functions = []
750
output_lines = []
751
752
is_cutscene_data = False
753
754
for line_no, raw_line in enumerate(f, 1):
755
raw_line = raw_line.rstrip()
756
line = raw_line.lstrip()
757
758
# Print exactly one output line per source line, to make compiler
759
# errors have correct line numbers. These will be overridden with
760
# reasonable content further down.
761
output_lines.append('')
762
763
if global_asm is not None:
764
if line.startswith(')'):
765
src, fn = global_asm.finish(state)
766
for i, line2 in enumerate(src):
767
output_lines[start_index + i] = line2
768
asm_functions.append(fn)
769
global_asm = None
770
else:
771
global_asm.process_line(raw_line, output_enc)
772
else:
773
if line in ['GLOBAL_ASM(', '#pragma GLOBAL_ASM(']:
774
global_asm = GlobalAsmBlock("GLOBAL_ASM block at line " + str(line_no))
775
start_index = len(output_lines)
776
elif ((line.startswith('GLOBAL_ASM("') or line.startswith('#pragma GLOBAL_ASM("'))
777
and line.endswith('")')):
778
fname = line[line.index('(') + 2 : -2]
779
global_asm = GlobalAsmBlock(fname)
780
with open(fname, encoding=input_enc) as f:
781
for line2 in f:
782
global_asm.process_line(line2.rstrip(), output_enc)
783
src, fn = global_asm.finish(state)
784
output_lines[-1] = ''.join(src)
785
asm_functions.append(fn)
786
global_asm = None
787
elif line.startswith('#include "') and line.endswith('" EARLY'):
788
# C includes qualified with EARLY (i.e. #include "file.c" EARLY) will be
789
# processed recursively when encountered
790
fpath = os.path.dirname(f.name)
791
fname = line[line.index(' ') + 2 : -7]
792
include_src = StringIO()
793
with open(fpath + os.path.sep + fname, encoding=input_enc) as include_file:
794
parse_source(include_file, opt, framepointer, input_enc, output_enc, include_src)
795
output_lines[-1] = include_src.getvalue()
796
include_src.write('#line ' + str(line_no) + '\n')
797
include_src.close()
798
else:
799
# This is a hack to replace all floating-point numbers in an array of a particular type
800
# (in this case CutsceneData) with their corresponding IEEE-754 hexadecimal representation
801
if cutscene_data_regexpr.search(line) is not None:
802
is_cutscene_data = True
803
elif line.endswith("};"):
804
is_cutscene_data = False
805
if is_cutscene_data:
806
raw_line = re.sub(float_regexpr, repl_float_hex, raw_line)
807
output_lines[-1] = raw_line
808
809
if print_source:
810
if isinstance(print_source, StringIO):
811
for line in output_lines:
812
print_source.write(line + '\n')
813
else:
814
for line in output_lines:
815
print_source.write(line.encode(output_enc) + b'\n')
816
print_source.flush()
817
if print_source != sys.stdout.buffer:
818
print_source.close()
819
820
return asm_functions
821
822
def fixup_objfile(objfile_name, functions, asm_prelude, assembler, output_enc):
823
SECTIONS = ['.data', '.text', '.rodata', '.bss']
824
825
with open(objfile_name, 'rb') as f:
826
objfile = ElfFile(f.read())
827
828
prev_locs = {
829
'.text': 0,
830
'.data': 0,
831
'.rodata': 0,
832
'.bss': 0,
833
}
834
to_copy = {
835
'.text': [],
836
'.data': [],
837
'.rodata': [],
838
'.bss': [],
839
}
840
asm = []
841
all_late_rodata_dummy_bytes = []
842
all_jtbl_rodata_size = []
843
late_rodata_asm = []
844
late_rodata_source_name_start = None
845
late_rodata_source_name_end = None
846
847
# Generate an assembly file with all the assembly we need to fill in. For
848
# simplicity we pad with nops/.space so that addresses match exactly, so we
849
# don't have to fix up relocations/symbol references.
850
all_text_glabels = set()
851
for function in functions:
852
ifdefed = False
853
for sectype, (temp_name, size) in function.data.items():
854
if temp_name is None:
855
continue
856
assert size > 0
857
loc = objfile.symtab.find_symbol(temp_name)
858
if loc is None:
859
ifdefed = True
860
break
861
loc = loc[1]
862
prev_loc = prev_locs[sectype]
863
if loc < prev_loc:
864
raise Failure("Wrongly computed size for section {} (diff {}). This is an asm-processor bug!".format(sectype, prev_loc- loc))
865
if loc != prev_loc:
866
asm.append('.section ' + sectype)
867
if sectype == '.text':
868
for i in range((loc - prev_loc) // 4):
869
asm.append('nop')
870
else:
871
asm.append('.space {}'.format(loc - prev_loc))
872
to_copy[sectype].append((loc, size, temp_name, function.fn_desc))
873
prev_locs[sectype] = loc + size
874
if not ifdefed:
875
all_text_glabels.update(function.text_glabels)
876
all_late_rodata_dummy_bytes.append(function.late_rodata_dummy_bytes)
877
all_jtbl_rodata_size.append(function.jtbl_rodata_size)
878
late_rodata_asm.append(function.late_rodata_asm_conts)
879
for sectype, (temp_name, size) in function.data.items():
880
if temp_name is not None:
881
asm.append('.section ' + sectype)
882
asm.append('glabel ' + temp_name + '_asm_start')
883
asm.append('.text')
884
for line in function.asm_conts:
885
asm.append(line)
886
for sectype, (temp_name, size) in function.data.items():
887
if temp_name is not None:
888
asm.append('.section ' + sectype)
889
asm.append('glabel ' + temp_name + '_asm_end')
890
if any(late_rodata_asm):
891
late_rodata_source_name_start = '_asmpp_late_rodata_start'
892
late_rodata_source_name_end = '_asmpp_late_rodata_end'
893
asm.append('.rdata')
894
asm.append('glabel {}'.format(late_rodata_source_name_start))
895
for conts in late_rodata_asm:
896
asm.extend(conts)
897
asm.append('glabel {}'.format(late_rodata_source_name_end))
898
899
o_file = tempfile.NamedTemporaryFile(prefix='asm-processor', suffix='.o', delete=False)
900
o_name = o_file.name
901
o_file.close()
902
s_file = tempfile.NamedTemporaryFile(prefix='asm-processor', suffix='.s', delete=False)
903
s_name = s_file.name
904
try:
905
s_file.write(asm_prelude + b'\n')
906
for line in asm:
907
s_file.write(line.encode(output_enc) + b'\n')
908
s_file.close()
909
ret = os.system(assembler + " " + s_name + " -o " + o_name)
910
if ret != 0:
911
raise Failure("failed to assemble")
912
with open(o_name, 'rb') as f:
913
asm_objfile = ElfFile(f.read())
914
915
# Remove some clutter from objdump output
916
objfile.drop_irrelevant_sections()
917
918
# Unify reginfo sections
919
target_reginfo = objfile.find_section('.reginfo')
920
source_reginfo_data = list(asm_objfile.find_section('.reginfo').data)
921
data = list(target_reginfo.data)
922
for i in range(20):
923
data[i] |= source_reginfo_data[i]
924
target_reginfo.data = bytes(data)
925
926
# Move over section contents
927
modified_text_positions = set()
928
jtbl_rodata_positions = set()
929
last_rodata_pos = 0
930
for sectype in SECTIONS:
931
if not to_copy[sectype]:
932
continue
933
source = asm_objfile.find_section(sectype)
934
assert source is not None, "didn't find source section: " + sectype
935
for (pos, count, temp_name, fn_desc) in to_copy[sectype]:
936
loc1 = asm_objfile.symtab.find_symbol_in_section(temp_name + '_asm_start', source)
937
loc2 = asm_objfile.symtab.find_symbol_in_section(temp_name + '_asm_end', source)
938
assert loc1 == pos, "assembly and C files don't line up for section " + sectype + ", " + fn_desc
939
if loc2 - loc1 != count:
940
raise Failure("incorrectly computed size for section " + sectype + ", " + fn_desc + ". If using .double, make sure to provide explicit alignment padding.")
941
if sectype == '.bss':
942
continue
943
target = objfile.find_section(sectype)
944
assert target is not None, "missing target section of type " + sectype
945
data = list(target.data)
946
for (pos, count, _, _) in to_copy[sectype]:
947
data[pos:pos + count] = source.data[pos:pos + count]
948
if sectype == '.text':
949
assert count % 4 == 0
950
assert pos % 4 == 0
951
for i in range(count // 4):
952
modified_text_positions.add(pos + 4 * i)
953
elif sectype == '.rodata':
954
last_rodata_pos = pos + count
955
target.data = bytes(data)
956
957
# Move over late rodata. This is heuristic, sadly, since I can't think
958
# of another way of doing it.
959
moved_late_rodata = {}
960
if any(all_late_rodata_dummy_bytes) or any(all_jtbl_rodata_size):
961
source = asm_objfile.find_section('.rodata')
962
target = objfile.find_section('.rodata')
963
source_pos = asm_objfile.symtab.find_symbol_in_section(late_rodata_source_name_start, source)
964
source_end = asm_objfile.symtab.find_symbol_in_section(late_rodata_source_name_end, source)
965
if source_end - source_pos != sum(map(len, all_late_rodata_dummy_bytes)) * 4 + sum(all_jtbl_rodata_size):
966
raise Failure("computed wrong size of .late_rodata")
967
new_data = list(target.data)
968
for dummy_bytes_list, jtbl_rodata_size in zip(all_late_rodata_dummy_bytes, all_jtbl_rodata_size):
969
for index, dummy_bytes in enumerate(dummy_bytes_list):
970
pos = target.data.index(dummy_bytes, last_rodata_pos)
971
# This check is nice, but makes time complexity worse for large files:
972
if SLOW_CHECKS and target.data.find(dummy_bytes, pos + 4) != -1:
973
raise Failure("multiple occurrences of late_rodata hex magic. Change asm-processor to use something better than 0xE0123456!")
974
if index == 0 and len(dummy_bytes_list) > 1 and target.data[pos+4:pos+8] == b'\0\0\0\0':
975
# Ugly hack to handle double alignment for non-matching builds.
976
# We were told by .late_rodata_alignment (or deduced from a .double)
977
# that a function's late_rodata started out 4 (mod 8), and emitted
978
# a float and then a double. But it was actually 0 (mod 8), so our
979
# double was moved by 4 bytes. To make them adjacent to keep jump
980
# tables correct, move the float by 4 bytes as well.
981
new_data[pos:pos+4] = b'\0\0\0\0'
982
pos += 4
983
new_data[pos:pos+4] = source.data[source_pos:source_pos+4]
984
moved_late_rodata[source_pos] = pos
985
last_rodata_pos = pos + 4
986
source_pos += 4
987
if jtbl_rodata_size > 0:
988
assert dummy_bytes_list, "should always have dummy bytes before jtbl data"
989
pos = last_rodata_pos
990
new_data[pos : pos + jtbl_rodata_size] = \
991
source.data[source_pos : source_pos + jtbl_rodata_size]
992
for i in range(0, jtbl_rodata_size, 4):
993
moved_late_rodata[source_pos + i] = pos + i
994
jtbl_rodata_positions.add(pos + i)
995
last_rodata_pos += jtbl_rodata_size
996
source_pos += jtbl_rodata_size
997
target.data = bytes(new_data)
998
999
# Merge strtab data.
1000
strtab_adj = len(objfile.symtab.strtab.data)
1001
objfile.symtab.strtab.data += asm_objfile.symtab.strtab.data
1002
1003
# Find relocated symbols
1004
relocated_symbols = set()
1005
for sectype in SECTIONS:
1006
for obj in [asm_objfile, objfile]:
1007
sec = obj.find_section(sectype)
1008
if sec is None:
1009
continue
1010
for reltab in sec.relocated_by:
1011
for rel in reltab.relocations:
1012
relocated_symbols.add(obj.symtab.symbol_entries[rel.sym_index])
1013
1014
# Move over symbols, deleting the temporary function labels.
1015
# Sometimes this naive procedure results in duplicate symbols, or UNDEF
1016
# symbols that are also defined the same .o file. Hopefully that's fine.
1017
# Skip over local symbols that aren't used relocated against, to avoid
1018
# conflicts.
1019
new_local_syms = [s for s in objfile.symtab.local_symbols() if not is_temp_name(s.name)]
1020
new_global_syms = [s for s in objfile.symtab.global_symbols() if not is_temp_name(s.name)]
1021
for i, s in enumerate(asm_objfile.symtab.symbol_entries):
1022
is_local = (i < asm_objfile.symtab.sh_info)
1023
if is_local and s not in relocated_symbols:
1024
continue
1025
if is_temp_name(s.name):
1026
continue
1027
if s.st_shndx not in [SHN_UNDEF, SHN_ABS]:
1028
section_name = asm_objfile.sections[s.st_shndx].name
1029
if section_name not in SECTIONS:
1030
raise Failure("generated assembly .o must only have symbols for .text, .data, .rodata, ABS and UNDEF, but found " + section_name)
1031
s.st_shndx = objfile.find_section(section_name).index
1032
# glabel's aren't marked as functions, making objdump output confusing. Fix that.
1033
if s.name in all_text_glabels:
1034
s.type = STT_FUNC
1035
if objfile.sections[s.st_shndx].name == '.rodata' and s.st_value in moved_late_rodata:
1036
s.st_value = moved_late_rodata[s.st_value]
1037
s.st_name += strtab_adj
1038
if is_local:
1039
new_local_syms.append(s)
1040
else:
1041
new_global_syms.append(s)
1042
new_syms = new_local_syms + new_global_syms
1043
for i, s in enumerate(new_syms):
1044
s.new_index = i
1045
objfile.symtab.data = b''.join(s.to_bin() for s in new_syms)
1046
objfile.symtab.sh_info = len(new_local_syms)
1047
1048
# Move over relocations
1049
for sectype in SECTIONS:
1050
source = asm_objfile.find_section(sectype)
1051
target = objfile.find_section(sectype)
1052
1053
if target is not None:
1054
# fixup relocation symbol indices, since we butchered them above
1055
for reltab in target.relocated_by:
1056
nrels = []
1057
for rel in reltab.relocations:
1058
if (sectype == '.text' and rel.r_offset in modified_text_positions or
1059
sectype == '.rodata' and rel.r_offset in jtbl_rodata_positions):
1060
# don't include relocations for late_rodata dummy code
1061
continue
1062
# hopefully we don't have relocations for local or
1063
# temporary symbols, so new_index exists
1064
rel.sym_index = objfile.symtab.symbol_entries[rel.sym_index].new_index
1065
nrels.append(rel)
1066
reltab.relocations = nrels
1067
reltab.data = b''.join(rel.to_bin() for rel in nrels)
1068
1069
if not source:
1070
continue
1071
1072
target_reltab = objfile.find_section('.rel' + sectype)
1073
target_reltaba = objfile.find_section('.rela' + sectype)
1074
for reltab in source.relocated_by:
1075
for rel in reltab.relocations:
1076
rel.sym_index = asm_objfile.symtab.symbol_entries[rel.sym_index].new_index
1077
if sectype == '.rodata' and rel.r_offset in moved_late_rodata:
1078
rel.r_offset = moved_late_rodata[rel.r_offset]
1079
new_data = b''.join(rel.to_bin() for rel in reltab.relocations)
1080
if reltab.sh_type == SHT_REL:
1081
if not target_reltab:
1082
target_reltab = objfile.add_section('.rel' + sectype,
1083
sh_type=SHT_REL, sh_flags=0,
1084
sh_link=objfile.symtab.index, sh_info=target.index,
1085
sh_addralign=4, sh_entsize=8, data=b'')
1086
target_reltab.data += new_data
1087
else:
1088
if not target_reltaba:
1089
target_reltaba = objfile.add_section('.rela' + sectype,
1090
sh_type=SHT_RELA, sh_flags=0,
1091
sh_link=objfile.symtab.index, sh_info=target.index,
1092
sh_addralign=4, sh_entsize=12, data=b'')
1093
target_reltaba.data += new_data
1094
1095
objfile.write(objfile_name)
1096
finally:
1097
s_file.close()
1098
os.remove(s_name)
1099
try:
1100
os.remove(o_name)
1101
except:
1102
pass
1103
1104
def run_wrapped(argv, outfile, functions):
1105
parser = argparse.ArgumentParser(description="Pre-process .c files and post-process .o files to enable embedding assembly into C.")
1106
parser.add_argument('filename', help="path to .c code")
1107
parser.add_argument('--post-process', dest='objfile', help="path to .o file to post-process")
1108
parser.add_argument('--assembler', dest='assembler', help="assembler command (e.g. \"mips-linux-gnu-as -march=vr4300 -mabi=32\")")
1109
parser.add_argument('--asm-prelude', dest='asm_prelude', help="path to a file containing a prelude to the assembly file (with .set and .macro directives, e.g.)")
1110
parser.add_argument('--input-enc', default='latin1', help="Input encoding (default: latin1)")
1111
parser.add_argument('--output-enc', default='latin1', help="Output encoding (default: latin1)")
1112
parser.add_argument('-framepointer', dest='framepointer', action='store_true')
1113
parser.add_argument('-g3', dest='g3', action='store_true')
1114
group = parser.add_mutually_exclusive_group(required=True)
1115
group.add_argument('-O1', dest='opt', action='store_const', const='O1')
1116
group.add_argument('-O2', dest='opt', action='store_const', const='O2')
1117
group.add_argument('-g', dest='opt', action='store_const', const='g')
1118
args = parser.parse_args(argv)
1119
opt = args.opt
1120
if args.g3:
1121
if opt != 'O2':
1122
raise Failure("-g3 is only supported together with -O2")
1123
opt = 'g3'
1124
1125
if args.objfile is None:
1126
with open(args.filename, encoding=args.input_enc) as f:
1127
return parse_source(f, opt=opt, framepointer=args.framepointer, input_enc=args.input_enc, output_enc=args.output_enc, print_source=outfile)
1128
else:
1129
if args.assembler is None:
1130
raise Failure("must pass assembler command")
1131
if functions is None:
1132
with open(args.filename, encoding=args.input_enc) as f:
1133
functions = parse_source(f, opt=opt, framepointer=args.framepointer, input_enc=args.input_enc, output_enc=args.output_enc)
1134
if not functions:
1135
return
1136
asm_prelude = b''
1137
if args.asm_prelude:
1138
with open(args.asm_prelude, 'rb') as f:
1139
asm_prelude = f.read()
1140
fixup_objfile(args.objfile, functions, asm_prelude, args.assembler, args.output_enc)
1141
1142
def run(argv, outfile=sys.stdout.buffer, functions=None):
1143
try:
1144
return run_wrapped(argv, outfile, functions)
1145
except Failure as e:
1146
print("Error:", e, file=sys.stderr)
1147
sys.exit(1)
1148
1149
if __name__ == "__main__":
1150
run(sys.argv[1:])
1151
1152