├── .gitignore ├── LICENSE ├── README.md ├── luma3ds_exception_dump_parser ├── __init__.py └── __main__.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .vscode 132 | .DS_Store 133 | *.dmp 134 | .project 135 | .cproject 136 | .settings 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016-2023 TuxSH 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # luma3ds_exception_dump_parser 2 | Luma3DS exception dump parser 3 | 4 | Usage: `luma3ds_exception_dump_parser crash_dump_.dmp` 5 | 6 | Installation/update: `pip install -U git+https://github.com/LumaTeam/luma3ds_exception_dump_parser.git` 7 | -------------------------------------------------------------------------------- /luma3ds_exception_dump_parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LumaTeam/luma3ds_exception_dump_parser/6ac776b5fe0bd8bb58f905631ffff9141b4c82a3/luma3ds_exception_dump_parser/__init__.py -------------------------------------------------------------------------------- /luma3ds_exception_dump_parser/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Requires Python >= 3.2 or >= 2.7 3 | 4 | __author__ = "TuxSH" 5 | __copyright__ = "Copyright (c) 2016-2025 TuxSH" 6 | __license__ = "MIT" 7 | __version__ = "v1.4.0" 8 | 9 | """ 10 | Parses Luma3DS exception dumps 11 | """ 12 | 13 | import argparse 14 | from struct import unpack_from 15 | import urllib.request 16 | import zipfile 17 | from io import BytesIO 18 | import tempfile 19 | from urllib.parse import urlparse 20 | import os 21 | import subprocess 22 | 23 | # Source of hexdump: https://gist.github.com/1mm0rt41PC/c340564823f283fe530b 24 | # Credits for hexdump go to the original authors 25 | # Slightly edited by TuxSH 26 | 27 | def hexdump(addr, src, length=16, sep='.' ): 28 | ''' 29 | @brief Return {src} in hex dump. 30 | @param[in] length {Int} Nb Bytes by row. 31 | @param[in] sep {Char} For the text part, {sep} will be used for non ASCII char. 32 | @return {Str} The hexdump 33 | @note Full support for python2 and python3 ! 34 | ''' 35 | result = [] 36 | 37 | # Python3 support 38 | try: 39 | xrange(0,1) 40 | except NameError: 41 | xrange = range 42 | 43 | for i in xrange(0, len(src), length): 44 | subSrc = src[i:i+length] 45 | hexa = '' 46 | for h in xrange(0, len(subSrc)): 47 | if h == length//2: 48 | hexa += ' ' 49 | h = subSrc[h] 50 | if not isinstance(h, int): 51 | h = ord(h) 52 | h = hex(h).replace('0x','') 53 | if len(h) == 1: 54 | h = '0'+h 55 | hexa += h+' ' 56 | hexa = hexa.strip(' ') 57 | text = '' 58 | for c in subSrc: 59 | if not isinstance(c, int): 60 | c = ord(c) 61 | if 0x20 <= c < 0x7F: 62 | text += chr(c) 63 | else: 64 | text += sep 65 | result.append(('%08x: %-'+str(length*(2+1)+1)+'s |%s|') % (addr + i, hexa, text)) 66 | return '\n'.join(result) 67 | 68 | def makeRegisterLine(A, rA, B, rB): 69 | return "{0:<15}{1:<20}{2:<15}{3:<20}".format(A, "{0:08x}".format(rA), B, "{0:08x}".format(rB)) 70 | 71 | handledExceptionNames = ("FIQ", "undefined instruction", "prefetch abort", "data abort") 72 | registerNames = tuple("r{0}".format(i) for i in range(13)) + ("sp", "lr", "pc", "cpsr") + ("dfsr", "ifsr", "far") + ("fpexc", "fpinst", "fpinst2") 73 | svcBreakReasons = ("(svcBreak: panic)", "(svcBreak: assertion failed)", "(svcBreak: user-related)") 74 | faultStatusSources = { 75 | 0b1:'Alignment', 0b100:'Instruction cache maintenance operation fault', 76 | 0b1100:'External Abort on translation - First-level', 0b1110:'External Abort on translation - Second-level', 77 | 0b101:'Translation - Section', 0b111:'Translation - Page', 0b11:'Access bit - Section', 0b110:'Access bit - Page', 78 | 0b1001:'Domain - Section', 0b1011:'Domain - Page', 0b1101:'Permission - Section', 0b1111:'Permission - Page', 79 | 0b1000:'Precise External Abort', 0b10110:'Imprecise External Abort', 0b10:'Debug event' 80 | } 81 | 82 | def main(args=None): 83 | parser = argparse.ArgumentParser(description="Parse Luma3DS exception dumps") 84 | parser.add_argument("filename") 85 | args = parser.parse_args() 86 | 87 | is_url = args.filename.startswith(('http://', 'https://')) 88 | temp_file = None 89 | output_basename = None 90 | data = b"" 91 | 92 | try: 93 | if is_url: 94 | parsed_url = urlparse(args.filename) 95 | url_filename = os.path.basename(parsed_url.path) 96 | 97 | if url_filename.lower().endswith('.dmp'): 98 | response = urllib.request.urlopen(args.filename) 99 | data = response.read() 100 | temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.dmp') 101 | temp_file.write(data) 102 | temp_file.close() 103 | args.filename = temp_file.name 104 | output_basename = os.path.splitext(url_filename)[0] 105 | else: 106 | response = urllib.request.urlopen(args.filename) 107 | zip_data = response.read() 108 | zip_buffer = BytesIO(zip_data) 109 | with zipfile.ZipFile(zip_buffer, 'r') as zip_file: 110 | dmp_files = [f for f in zip_file.namelist() if f.lower().endswith('.dmp')] 111 | if len(dmp_files) != 1: 112 | raise SystemExit("ZIP file must contain exactly one .dmp file") 113 | dmp_name = dmp_files[0] 114 | with zip_file.open(dmp_name) as dmp_file: 115 | data = dmp_file.read() 116 | temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.dmp') 117 | temp_file.write(data) 118 | temp_file.close() 119 | args.filename = temp_file.name 120 | output_basename = os.path.splitext(os.path.basename(dmp_name))[0] 121 | else: 122 | with open(args.filename, "rb") as f: 123 | data = f.read() 124 | output_basename = os.path.splitext(os.path.basename(args.filename))[0] 125 | 126 | if unpack_from("<2I", data) != (0xdeadc0de, 0xdeadcafe): 127 | raise SystemExit("Invalid file format") 128 | 129 | version, processor, exceptionType, _, nbRegisters, codeDumpSize, stackDumpSize, additionalDataSize = unpack_from("<8I", data, 8) 130 | nbRegisters //= 4 131 | 132 | processor, coreId = processor & 0xffff, processor >> 16 133 | 134 | if version < (1 << 16) | 2: 135 | raise SystemExit("Incompatible format version, please use the appropriate parser.") 136 | 137 | registers = unpack_from("<{0}I".format(nbRegisters), data, 40) 138 | codeOffset = 40 + 4 * nbRegisters 139 | codeDump = data[codeOffset : codeOffset + codeDumpSize] 140 | stackOffset = codeOffset + codeDumpSize 141 | stackDump = data[stackOffset : stackOffset + stackDumpSize] 142 | addtionalDataOffset = stackOffset + stackDumpSize 143 | additionalData = data[addtionalDataOffset : addtionalDataOffset + additionalDataSize] 144 | 145 | if processor == 9: 146 | print("Processor: Arm9") 147 | else: 148 | print("Processor: Arm11 (core {0})".format(coreId)) 149 | 150 | typeDetailsStr = "" 151 | if exceptionType == 2: 152 | if (registers[16] & 0x20) == 0 and codeDumpSize >= 4: 153 | instr = unpack_from("= 2: 159 | instr = unpack_from("= len(handledExceptionNames) else handledExceptionNames[exceptionType], typeDetailsStr)) 167 | 168 | if processor == 11 and exceptionType >= 2: 169 | xfsr = registers[18] if exceptionType == 2 else registers[17] 170 | print("Fault status: " + faultStatusSources.get(xfsr & 0xf, "Unknown")) 171 | 172 | if additionalDataSize != 0: 173 | if processor == 11: 174 | print("Current process: {0} ({1:016x})".format(additionalData[:8].decode("ascii"), unpack_from("