From 48e39bc2bdab9ccd4e4321b6e8e53a66ecbc87e2 Mon Sep 17 00:00:00 2001 From: sup39 Date: Fri, 14 Jul 2023 20:48:11 +0900 Subject: [PATCH] init (v0.1.0a4) --- .gitignore | 3 + CHANGELOG.md | 9 ++ LICENSE | 21 ++++ README.md | 16 +++ pyproject.toml | 3 + setup.cfg | 23 ++++ src/supGecko/__init__.py | 4 + src/supGecko/asm.py | 108 +++++++++++++++++ src/supGecko/consts.py | 22 ++++ src/supGecko/gecko.py | 248 +++++++++++++++++++++++++++++++++++++++ src/supGecko/utils.py | 38 ++++++ 11 files changed, 495 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 src/supGecko/__init__.py create mode 100644 src/supGecko/asm.py create mode 100644 src/supGecko/consts.py create mode 100644 src/supGecko/gecko.py create mode 100644 src/supGecko/utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..494d08f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/dist +*.egg-info/ +__pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..67da5d5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# supGecko +## 0.1.0a4 +- Added `compile_flags` property to `Gecko` class +## 0.1.0a3 +- Added C/ASM compilation support for C0, C2, c\_kit +## 0.1.0a2 +- Added `indent` parameter for `Gecko#dump_txt()` +## 0.1.0a1 +- Initialized diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6788b2f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 sup39 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b53bef5 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# supGecko +A helper library to write Gecko code + +## Installation +``` +pip install supGecko +``` + +## Usage +```python +from supGecko import Gecko + +g = Gecko() +# TODO: usage of the Gecko class +print(g.dump_txt()) +``` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b0f0765 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=42"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f8c9f7c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,23 @@ +[metadata] +name = supGecko +version = 0.1.0a4 +author = sup39 +author_email = sms@sup39.dev +description = A helper library to write Gecko code +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/sup39/supGecko +license = MIT +project_urls = + Bug Tracker = https://github.com/sup39/supGecko/issues +classifiers = + Programming Language :: Python :: 3 + License :: OSI Approved :: MIT License + +[options] +include_package_data = True +packages = find: +python_requires = >=3.8 + +[options.packages.find] +where = src diff --git a/src/supGecko/__init__.py b/src/supGecko/__init__.py new file mode 100644 index 0000000..cad1184 --- /dev/null +++ b/src/supGecko/__init__.py @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2023 sup39 + +from .gecko import Gecko diff --git a/src/supGecko/asm.py b/src/supGecko/asm.py new file mode 100644 index 0000000..0fb1ba3 --- /dev/null +++ b/src/supGecko/asm.py @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2023 sup39 + +import shutil +from distutils import spawn +import tempfile +import os +import subprocess + +def system(argv, *args, **kwargs): + r = subprocess.run(argv, *args, capture_output=True, text=True, **kwargs) + if r.returncode: + raise Exception(f'Fail to run {argv[0]} (code={r.returncode}): {r.stderr}') + return r.stdout + +def write_extra_input(x, file): + if type(x) == str: + print(x, file=file) + else: + for line in x: + print(line, file=file) + +def compile( + input_path, addr=None, + extra_c_flags=[], + extra_as_input=[], extra_as_flags=[], + extra_ld_input=[], extra_ld_flags=[], +): + distDir = tempfile.mkdtemp() + distASM, distOBJ, distLD, distLOBJ, distBIN = (f'{distDir}/_.{ext}' for ext in ['s', 'o', 'ld', 'l.o', 'bin']) + + try: + input_name = input_path.rsplit('.', 1)[0] + + if input_path.endswith('.c'): + # compile to OBJ + system([ + 'powerpc-eabi-gcc', + '-fno-asynchronous-unwind-tables', + ','.join(( + '-Wa', '-mregnames', '-mgekko', + *extra_as_flags, + )), + '-c', '-o', distOBJ, + *extra_c_flags, + input_path, + ]) + else: # treat as ASM file + # make ASM file + with open(distASM, 'w') as fw, open(input_path, 'r') as fr: + write_extra_input(extra_as_input, file=fw) + for line in fr: fw.write(line) + # assemble to OBJ + system([ + 'powerpc-eabi-as', + '-mregnames', '-mgekko', + '-o', distOBJ, + *extra_as_flags, + distASM, + ]) + + # link + with open(distLD, 'w') as fw: + inputLD = input_name+'.ld' + ## extra ld + if os.path.isfile(inputLD): + with open(inputLD) as f: + fw.write(f.read()) + ## section + print('SECTIONS {', file=fw) + if addr is not None: print(f' . = {addr};', file=fw) + print(' .text : ALIGN(4) { *(.text) }', file=fw) + if addr is not None: print('.rodata : ALIGN(4) { *(.rodata) }', file=fw) + print('}', file=fw) + write_extra_input(extra_ld_input, file=fw) + system([ + 'powerpc-eabi-ld', + '-o', distLOBJ, + '-T', distLD, + *extra_ld_flags, + distOBJ, + ]) + + # gecko symbols + symbols = {} + lines = system([ + 'powerpc-eabi-objdump', + '-tj.text', distLOBJ, + ]).split('\n') + for line in lines[4:-3]: + ch1, ch2 = line.split('\t') + addr = int(ch1.split(None, 2)[0], 16) + name = ch2.split()[1] + symbols[name] = addr + + # binary + system([ + 'powerpc-eabi-objcopy', + '-O', 'binary', + distLOBJ, distBIN, + ]) + with open(distBIN, 'rb') as f: + codeBin = f.read() + + return codeBin, symbols + + finally: + shutil.rmtree(distDir) diff --git a/src/supGecko/consts.py b/src/supGecko/consts.py new file mode 100644 index 0000000..8b58aa5 --- /dev/null +++ b/src/supGecko/consts.py @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2023 sup39 + +CMP_IDX = {'==': 0, '!=': 2, '>': 4, '<': 6} +UNIT_IDX = { + 8: 0, 'b': 0, 'byte': 0, + 16: 1, 'h': 1, 'halfword': 1, + 32: 2, 'w': 2, 'word': 2, +} +REGOP_IDX = { + 'add': 0, '+': 0, + 'mul': 1, '*': 1, + 'or' : 2, '|': 2, + 'and': 3, '&': 3, + 'xor': 4, '^': 4, + 'slw': 5, '<<': 5, + 'srw': 6, '>>': 6, + 'rol': 7, + 'asr': 8, + 'fadds': 9, + 'fmuls': 10, +} diff --git a/src/supGecko/gecko.py b/src/supGecko/gecko.py new file mode 100644 index 0000000..abd6207 --- /dev/null +++ b/src/supGecko/gecko.py @@ -0,0 +1,248 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2023 sup39 + +from .asm import compile +from .utils import * +from .consts import * + +# TODO: assert range of arg +class Gecko(): + def __init__(self, compile_flags={}): + self.code = bytearray() + self.compile_flags = compile_flags + def append(self, *payloads): + self.code += b''.join(map(parse_binarg, payloads)) + return self + def dump_txt(self, indent=''): + if type(indent) == int: indent = ' '*indent + return '\n'.join( + indent+' '.join(self.code[j:j+4].hex().upper() for j in [i, i+4]) + for i in range(0, len(self.code), 8) + ) + def compile(self, input_path, addr=None, **kwargs0): + kwargs = {'extra_'+k: v for k, v in self.compile_flags.items()} + for k, v in kwargs0.items(): + if type(v) == str: v = [v] + kwargs[k] = kwargs[k] + v if k in kwargs else v + return compile(input_path, addr, **kwargs) + ''' 00 ''' + def write8(self, addr, val, count=1, po=False): + return self.append( + cw_addr(0x00, addr, po), + ((count-1)&0xffff) << 16 | val&0xff, + ) + ''' 02 ''' + def write16(self, addr, val, count=1, po=False): + return self.append( + cw_addr(0x02, addr, po), + ((count-1)&0xffff) << 16 | val&0xffff, + ) + ''' 04 ''' + def write32(self, addr, val, po=False): + return self.append( + cw_addr(0x04, addr, po), + val & 0xffffffff, + ) + ''' 06 ''' + def write_string(self, addr, payload, po=False): + if type(payload) == str: payload = bytes.fromhex(payload) + size = len(payload) + rsize = size%8 + if rsize: payload += b'\x00'*(8-rsize) + return self.append(cw_addr(0x06, addr, po), size, payload) + ''' 08 ''' + def write_addr(self, addr, n, unit, val, addr_step, val_step, po=False): + t = UNIT_IDX[unit] + return self.append( + cw_addr(0x08, addr, po), + val, + t<<28 | (n&0xfff)<<16 | (addr_step&0xffff), + val_step & 0xffffffff, + ) + ''' 20-27 ''' + def if32(self, addr, op, val, po=False, endif=False): + ct = 0x20 | CMP_IDX[op] + return self.append( + cw_addr(ct, addr, po, endif), + val & 0xffffffff, + ) + ''' 28-2F ''' + def if16(self, addr, op, val, mask=-1, po=False, endif=False): + ct = 0x28 | CMP_IDX[op] + return self.append( + cw_addr(ct, addr, po, endif), + (~mask & 0xffff)<<16 | val & 0xffff, + ) + ''' 4_TYZ ''' + def sl_bapo(self, action, target, op, addr, bapo=None, gr=None): + cw = ({'ba': 0x40000, 'po': 0x48000}[target] \ + | {'load': 0x00000, 'set': 0x02000, 'store': 0x04000} \ + | {'+=': 0x00100, '=': 0}[op] \ + | {'ba': 0x00010, 'po': 0x10010, None: 0}[bapo] + ) << 12 + if action == 'store': + assert op == '=', '`op` should be "=" when storing ba/po' + if gr is not None: + cw |= 0x1_000 | gr + return self.append(cw, addr&0xffffffff) + def load_ba(self, *args, **kwargs): + return self.sl_bapo('load', 'ba', *args, **kwargs) + def set_ba(self, *args, **kwargs): + return self.sl_bapo('set', 'ba', *args, **kwargs) + def store_ba(self, *args, **kwargs): + return self.sl_bapo('store', 'ba', *args, **kwargs) + def load_po(self, *args, **kwargs): + return self.sl_bapo('load', 'po', *args, **kwargs) + def set_po(self, *args, **kwargs): + return self.sl_bapo('set', 'po', *args, **kwargs) + def store_po(self, *args, **kwargs): + return self.sl_bapo('store', 'po', *args, **kwargs) + ''' 46, 4E ''' + def store_ncl(self, target, offset): + ct = {'ba': 0x46, 'po': 0x4E}[target] + return self.append(ct<<24 | offset&0xffff, 0) + ''' 60 ''' + def set_repeat(self, n, p): + return self.append(0x60<<24 | n&0xffff, p&0xf) + ''' 62 ''' + def execute_repeat(self, p): + return self.append(0x62<<24, p&0xf) + ''' 64 ''' + def return_(self, p, if_=None): + return self.append(cw_go(0x64, if_, n=0), p&0xf) + ''' 66 ''' + def goto(self, n, p, if_=None): + return self.append(cw_go(0x66, if_, n), p&0xf) + ''' 68 ''' + def gosub(self, n, p, if_=None): + return self.append(cw_go(0x68, if_, n), p&0xf) + + ''' 80 ''' + def set_reg(self, gr, op, addr, bapo=None): + c = {'ba': 0x8001, 'po': 0x9001, None: 0x8000}[bapo] \ + | {'+=': 0x0010, '=': 0}[op] + return self.append(c<<16 | gr&0xf, addr&0xffffffff) + def load_reg(self, gr, addr, unit=None): + c = {'ba': 0x8201, 'po': 0x9201, None: 0x8200}[bapo] + u = UNIT_IDX[unit] + return self.append(c<<16 | u<<20 | gr&0xf, addr&0xffffffff) + def store_reg(self, gr, addr, unit=None, count=1): + c = {'ba': 0x8401, 'po': 0x9401, None: 0x8400}[bapo] + t = UNIT_IDX[unit] + y = (count - 1) & 0xfff + return self.append( + c<<16 | t<<20 | y<<4 | gr&0xf, + addr & 0xffffffff, + ) + ''' 86 ''' + def reg_op_imm(self, lhs, op, rhs): + cw, rhs = parse_regop(0x86, lhs, op, rhs) + return self.append(cw, rhs & 0xffffffff) + ''' 88 ''' + def reg_op_reg(self, lhs, op, rhs): + cw, rhs = parse_regop(0x88, lhs, op, rhs) + return self.append(cw, rhs & 0xf) + ''' + [8A] (K, XXXXXXXX) <- N + [8C] K <- (N, XXXXXXXX) + ''' + def memcpy(self, dst, src, n): + if type(src) == int: + assert type(dst)==tuple and len(dst)==2, \ + '`dst` should be in form of "(K, XXXXXXXX)"' + N = src&0xf + K, off = dst + ct, K = parse_regidx(0x8A, K) + else: + assert type(dst) == int, \ + '`dst` and `src` cannot be tuple at the same time' + assert len(src)==2, '`src` should be in form of "(N, XXXXXXXX)"' + ct = 0x8C + N, off = src + K = dst&0xf + ct, N = parse_regidx(0x8C, N) + return self.append( + ct<<24 | (n & 0xffff)<<8 | N<<4 | K, + off & 0xffffffff, + ) + ''' A0-A7 ''' + def if16_reg(self, N, op, K, offset=None, mask=-1, endif=False): + ct = 0xA0 | CMP_IDX[op] + ctK, K = parse_regidx(ct, K) + ctN, N = parse_regidx(ct, N) + if K==0xf or N==0xf: + assert offset is not None, '`offset` should be set if ba/po is used' + else: + offset = 0 + assert ctK == ctN, 'ba and po cannot be used at the same time' + ct = ctN + return self.append( + cw_addr(ct, addr, po=False, endif=endif) + (K<<28 | N<<24 | ~mask&0xffff), + ) + ''' A8-AF ''' + def if16_cnt(self, cnt, op, val, reset_on_true, mask=-1, endif=False): + T = (8 if reset_on_true else 0) + (1 if endif else 0) + return self.append( + (0xA8 | CMP_IDX[op])<<24 | (cnt&0xffff)<<4 | T, + ~mask<<16 | val&0xffff, + ) + ''' C0 ''' + def C0(self, input_path=None, raw=None, **kwargs): + code = make_asm_code(self.compile, input_path, raw, kwargs) + if len(code)%8 == 4: # pad code with blr + code += b'\x4E\x80\x00\x20' + return self.append(0xC000_0000, len(code)>>3, code) + ''' C2 ''' + def C2(self, addr, input_path, raw=None, po=False, **kwargs): + code = make_asm_code(self.compile, input_path, raw, kwargs) + if len(code)%8 == 0: # pad code with nop + code += b'\x60\x00\x00\x00' + code += b'\x00\x00\x00\x00' # append 0 + return self.append(cw_addr(0xC2, addr, po), len(code)>>3, code) + ''' C6 ''' + def branch(self, addr, dst, po=False): + return self.append(cw_addr(0xC6, addr, po), dst&0xffffffff) + ''' CC ''' + def onoff_switch(self): + return self.append(0xCC00_0000, 0) + ''' CE ''' + def addr_range_check(self, l, u, endif=False): + return self.append( + cw_addr(0xCE, 0, po, endif), + (l&0xffff)<<16 | (u&0xffff), + ) + ''' E0 ''' + def full_terminator(self, ba=0, po=0): + return self.append(0xE000_0000, (ba&0xffff)<<16 | (po&0xffff)) + ''' E2 ''' + def endif(self, count=None, else_=False, ba=0, po=0): + if count is None: + if else_: count = 1 + else: return self.full_terminator(ba, po) + return self.append( + 0xE200_0000 | (0x1_000_00 if else_ else 0) | count&0xff, + (ba&0xffff)<<16 | (po&0xffff), + ) + ''' F0 ''' + def end_of_code(self): + return self.append(0xF000_0000, 0) + + ''' C Kit ''' + def c_kit(self, addr_code, input_path, entries, **kwargs): + code, symbols = self.compile(input_path, addr_code, **kwargs) + # 06 + self.write_string(addr_code, code, po=False) + # 04/C6 + for addr_src, action, name in entries: + assert name in symbols, f'symbol "{name}" is not found in the .text section of {input_path}' + addr_dst = symbols[name] + if action == 'b': + self.branch(addr_src, addr_dst, po=False) + elif action == 'bl': + d_addr = addr_dst - addr_src + assert -0x200_0000 <= d_addr < 0x200_0000, 'cannot bl from %08X to %08X'%(addr_src, addr_dst) + inst = 0x4800_0001 | d_addr&0x3ff_fffc + self.write32(addr_src, inst, po=False) + else: assert False, '`action` should be "b" or "bl", got "{action}"' + return self diff --git a/src/supGecko/utils.py b/src/supGecko/utils.py new file mode 100644 index 0000000..2bfb765 --- /dev/null +++ b/src/supGecko/utils.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2023 sup39 + +def cw_addr(ct, addr, po, endif=False): + return (ct+0x10 if po else ct)<<24 | (addr+1 if endif else addr)&0x1ff_ffff +def cw_go(ct, if_, n): + return ct<<24 | [True, False, None].index(if_)<<20 | n&0xffff + +def parse_regop(ct, lhs, op, rhs): + if op.endswith('='): op = op[:-1] # drop trailing = + lhs, lhs_flag = parse_bracket_operand(lhs, 'lhs') + rhs, rhs_flag = parse_bracket_operand(rhs, 'rhs') + cw = ct<<24 | REGOP_IDX[op]<<20 | lhs_flag<<16 | rhs_flag<<17 | lhs&0xf + return cw, rhs +def parse_regidx(ct, x, name): + if x == 'ba': x = 0xf + elif x == 'po': x = 0xf; ct |= 0x10 + else: x &= 0xf; assert x!=0xf, f'{name} cannot be F' + return ct, x +def parse_bracket_operand(x, name): # returns (value, hasBracket) + if type(x) == list: + assert len(x)==1, f'`{name}` must be "x" or "[x]"' + return x, 1 + return x, 0 +def parse_binarg(x): + return b''.join(map(parse_binarg, x)) if type(x)==list else \ + bytes.fromhex(x) if type(x)==str else \ + x.to_bytes(4, 'big') if type(x)==int else x + +def make_asm_code(compile, input_path, raw, kwargs): + if raw is not None: # raw code + code = parse_binarg(raw) + assert len(code)%4 == 0, \ + f'len(raw) should a multiple of 4, got {len(raw)}' + else: # compile from file + assert input_path is not None, 'either `input_path` or `raw` should be set' + code, symbols = compile(input_path, **kwargs) + return code