├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_suggestion.yaml │ └── frug_report.yml └── workflows │ ├── ci.yml │ ├── md_lint.yml │ └── mypy.yml ├── .gitignore ├── .markdownlint.jsonc ├── .vscode └── extensions.json ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── mypy.ini ├── pyproject.toml ├── requirements.txt └── spimdisasm ├── __init__.py ├── __main__.py ├── common ├── CompilerConfig.py ├── Context.py ├── ContextSymbols.py ├── ElementBase.py ├── FileSectionType.py ├── FileSplitFormat.py ├── GlobalConfig.py ├── GpAccesses.py ├── OrderedEnum.py ├── Relocation.py ├── SortedDict.py ├── SymbolsSegment.py ├── Utils.py └── __init__.py ├── disasmdis ├── DisasmdisInternals.py ├── __init__.py └── __main__.py ├── elf32 ├── Elf32Constants.py ├── Elf32Dyns.py ├── Elf32File.py ├── Elf32GlobalOffsetTable.py ├── Elf32Header.py ├── Elf32RegInfo.py ├── Elf32Rels.py ├── Elf32SectionHeaders.py ├── Elf32StringTable.py ├── Elf32Syms.py └── __init__.py ├── elfObjDisasm ├── ElfObjDisasmInternals.py ├── __init__.py └── __main__.py ├── frontendCommon ├── FrontendUtilities.py └── __init__.py ├── mips ├── FilesHandlers.py ├── FuncRodataEntry.py ├── InstructionConfig.py ├── MipsFileBase.py ├── MipsFileSplits.py ├── __init__.py ├── sections │ ├── MipsSectionBase.py │ ├── MipsSectionBss.py │ ├── MipsSectionData.py │ ├── MipsSectionGccExceptTable.py │ ├── MipsSectionRelocZ64.py │ ├── MipsSectionRodata.py │ ├── MipsSectionText.py │ └── __init__.py └── symbols │ ├── MipsSymbolBase.py │ ├── MipsSymbolBss.py │ ├── MipsSymbolData.py │ ├── MipsSymbolFunction.py │ ├── MipsSymbolGccExceptTable.py │ ├── MipsSymbolRodata.py │ ├── MipsSymbolText.py │ ├── __init__.py │ └── analysis │ ├── InstrAnalyzer.py │ └── __init__.py ├── py.typed ├── rspDisasm ├── RspDisasmInternals.py ├── __init__.py └── __main__.py └── singleFileDisasm ├── SingleFileDisasmInternals.py ├── __init__.py └── __main__.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_suggestion.yaml: -------------------------------------------------------------------------------- 1 | name: Feature suggestion 2 | description: Suggest a new feature 3 | title: "[Suggestion]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for suggesting a new feature! 10 | - type: textarea 11 | id: the-feature 12 | attributes: 13 | label: Explain the feature 14 | description: What does this new feature do? How it would be done? 15 | placeholder: | 16 | - Print capybara ascii art on each run 17 | - Introduce achievements 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: advantage 22 | attributes: 23 | label: Advantage 24 | description: What are the pros of this new feature? 25 | placeholder: | 26 | - ... is a common case. 27 | - The produced assembly would be more correct because... 28 | - type: textarea 29 | id: disadvantage 30 | attributes: 31 | label: Disadvantage 32 | description: What could be any drawback of the new feature? 33 | placeholder: | 34 | - Slower runtime. 35 | - Harder to debug. 36 | - type: textarea 37 | id: example 38 | attributes: 39 | label: Example(s) 40 | description: Include examples on how this feature would look like 41 | validations: 42 | required: true 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/frug_report.yml: -------------------------------------------------------------------------------- 1 | name: Frug Report 2 | description: File a frug report 3 | title: "[Frug]: " 4 | labels: ["frug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for filing a frug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: Explain the problem. 14 | description: What happened? What did you expect to happen? 15 | placeholder: What went wrong? 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: reproducer 20 | attributes: 21 | label: Reproducer 22 | description: Please provide instructions to reproduce the problem. 23 | placeholder: | 24 | Clone the repo on github.com/example/example and do XYZ 25 | validations: 26 | required: true 27 | - type: input 28 | id: spimdisasm-version 29 | attributes: 30 | label: spimdisasm version 31 | description: What version of spimdisasm are you running? (`spimdisasm --version`) 32 | validations: 33 | required: true 34 | - type: input 35 | id: splat-version 36 | attributes: 37 | label: "Optional: splat version" 38 | description: What version of splat are you running? 39 | validations: 40 | required: false 41 | - type: textarea 42 | id: other-version 43 | attributes: 44 | label: "Optional: Version of other stuff" 45 | description: Here you can put the version of whatever other software you think may be relevant, like Python, rabbitizer, binutils, OS, etc. 46 | placeholder: | 47 | Python: 4.18 48 | rabbitizer: 72.½ 49 | binutils: 2.π 50 | Wine on WSL2 on Windows 11 on VirtualBox on OpenBSD machine. 51 | Etc 52 | validations: 53 | required: false 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and upload to PyPI 2 | 3 | # Build on every branch push, tag push, and pull request change: 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | build_wheel: 8 | name: Build wheel 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup venv 16 | run: | 17 | python3 -m venv .venv 18 | 19 | - name: Install build module 20 | run: | 21 | . .venv/bin/activate 22 | python3 -m pip install -U build 23 | 24 | - name: Build wheel and source 25 | run: | 26 | . .venv/bin/activate 27 | python3 -m build --sdist --wheel --outdir dist/ . 28 | 29 | - uses: actions/upload-artifact@v4.3.1 30 | with: 31 | path: dist/* 32 | 33 | upload_pypi: 34 | name: Upload release to PyPI 35 | needs: [build_wheel] 36 | runs-on: ubuntu-latest 37 | environment: 38 | name: pypi 39 | url: https://pypi.org/p/spimdisasm 40 | permissions: 41 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 42 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 43 | steps: 44 | - uses: actions/download-artifact@v4.1.2 45 | with: 46 | name: artifact 47 | path: dist 48 | 49 | - name: Publish package distributions to PyPI 50 | uses: pypa/gh-action-pypi-publish@v1.12.4 51 | -------------------------------------------------------------------------------- /.github/workflows/md_lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint markdown files 2 | 3 | # Build on every branch push, tag push, and pull request change: 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | checks: 8 | runs-on: ubuntu-latest 9 | name: Lint md files 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | 14 | - name: Lint markdown files 15 | uses: articulate/actions-markdownlint@v1.1.0 16 | with: 17 | config: .markdownlint.jsonc 18 | -------------------------------------------------------------------------------- /.github/workflows/mypy.yml: -------------------------------------------------------------------------------- 1 | name: Check mypy 2 | 3 | # Build on every branch push, tag push, and pull request change: 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | checks: 8 | runs-on: ubuntu-latest 9 | name: mypy 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | 14 | - name: Set up Python 3.9 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: '3.9' 18 | 19 | - name: Setup venv 20 | run: | 21 | python3 -m venv .venv 22 | 23 | - name: Install Dependencies 24 | run: | 25 | . .venv/bin/activate 26 | python3 -m pip install -U -r requirements.txt 27 | python3 -m pip install -U mypy 28 | 29 | - name: mypy 30 | run: | 31 | . .venv/bin/activate 32 | mypy --show-column-numbers --hide-error-context . 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | cover/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | 164 | # Text editor remnants 165 | .vscode/* 166 | !.vscode/extensions.json 167 | .vs/ 168 | .idea/ 169 | CMakeLists.txt 170 | cmake-build-debug 171 | venv/ 172 | 173 | # Project-specific ignores 174 | build/ 175 | expected/ 176 | notes/ 177 | baserom/ 178 | baserom_*/ 179 | docs/doxygen/ 180 | *.elf 181 | *.sra 182 | *.z64 183 | *.n64 184 | *.v64 185 | *.map 186 | *.dump 187 | out.txt 188 | 189 | # Tool artifacts 190 | ctx.c 191 | graphs/ 192 | 193 | # Assets 194 | *.png 195 | *.jpg 196 | *.mdli 197 | *.anmi 198 | *.obj 199 | *.mtl 200 | *.fbx 201 | !*_custom* 202 | .extracted-assets.json 203 | 204 | # Per-user configuration 205 | .python-version 206 | 207 | asm/ 208 | *.s 209 | splits/ 210 | data/ 211 | 212 | *.ld 213 | *.o 214 | -------------------------------------------------------------------------------- /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | // https://github.com/DavidAnson/markdownlint/blob/main/doc/md024.md 3 | // MD024 - Multiple headings with the same content 4 | "MD024": { 5 | "siblings_only": true 6 | }, 7 | 8 | // https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md 9 | // MD013 - Line length 10 | "MD013": { 11 | "code_block_line_length": 120, 12 | "headings": false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "davidanson.vscode-markdownlint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Decompollaborate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 2 | # SPDX-License-Identifier: MIT 3 | 4 | include requirements.txt 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spimdisasm 2 | 3 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/spimdisasm)](https://pypi.org/project/spimdisasm/) 4 | [![GitHub License](https://img.shields.io/github/license/Decompollaborate/spimdisasm)](https://github.com/Decompollaborate/spimdisasm/releases/latest) 5 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/Decompollaborate/spimdisasm)](https://github.com/Decompollaborate/spimdisasm/releases/latest) 6 | [![PyPI](https://img.shields.io/pypi/v/spimdisasm)](https://pypi.org/project/spimdisasm/) 7 | [![GitHub contributors](https://img.shields.io/github/contributors/Decompollaborate/spimdisasm?logo=purple)](https://github.com/Decompollaborate/spimdisasm/graphs/contributors) 8 | 9 | A matching MIPS disassembler API and front-ends with built-in instruction analysis. 10 | 11 | Currently supports all the CPU instructions for MIPS I, II, III and IV. 12 | 13 | Mainly focused on supporting Nintendo 64 binaries, but it should work with other 14 | MIPS platforms too. 15 | 16 | ## Features 17 | 18 | - Produces matching assembly. 19 | - Supports `.text`, `.data`, `.rodata` and `.bss` disassembly. 20 | - The reloc section from Zelda 64 and some other games is supported too, but 21 | no front-end script uses it yet. 22 | - Generates separated files for each section of a file (`.text`, `.data`, 23 | `.rodata` and `.bss`). 24 | - Supports multiple files spliting from a single input binary. 25 | - Automatic function detection. 26 | - Can detect if a function is handwritten too. 27 | - `hi`/`lo` pairing with high success rate. 28 | - Automatic pointer and symbol detection. 29 | - Function spliting with rodata migration. 30 | - Supports floats and doubles in rodata. 31 | - String detection with medium to high success rate. 32 | - Allows to set user-defined function and symbol names. 33 | - Big, little and middle endian support. 34 | - Autogenerated symbols can be named after the section they come from (`RO_` and 35 | `B_` for `.rodata` and `.bss` sections) or its type (`STR_`, `FLT_` and `DBL_` 36 | for string, floats and doubles respectively). 37 | - Simple file boundary detection. 38 | - Detects boundaries on .text and .rodata sections 39 | - Lots of features can be turned on and off. 40 | - MIPS instructions features: 41 | - Named registers for MIPS VR4300's coprocessors. 42 | - Support for many pseudoinstructions. 43 | - Properly handle move to/from coprocessor instructions. 44 | - Support for numeric, o32, n32 and n64 ABI register names. 45 | - Some workarounds for some specific compilers/assemblers: 46 | - `SN64`/`PSYQ`: 47 | - `div`/`divu` fix: tweaks a bit the produced `div`, `divu` and `break` instructions. 48 | - Support for specific MIPS instruction sets: 49 | - N64's RSP instruction disassembly support. 50 | - RSP decoding has been tested to build back to matching assemblies with 51 | [armips](https://github.com/Kingcom/armips/). 52 | - PS1's R3000 GTE instruction set support. 53 | - PSP's R4000 ALLEGREX instruction set support. 54 | - PS2's R5900 EE instruction set support. 55 | - (Experimental) Same VRAM overlay support. 56 | - Overlays which are able to reference symbols from other overlays in other 57 | categories/types is supported too. 58 | - NOTE: This feature lacks lots of testing and probably has many bugs. 59 | 60 | ## Installing 61 | 62 | The recommended way to install is using from the PyPi release, via `pip`: 63 | 64 | ```bash 65 | python3 -m pip install -U spimdisasm 66 | ``` 67 | 68 | If you use a `requirements.txt` file in your repository, then you can add 69 | this library with the following line: 70 | 71 | ```txt 72 | spimdisasm>=1.32.0,<2.0.0 73 | ``` 74 | 75 | ### Development version 76 | 77 | The unstable development version is located at the [develop](https://github.com/Decompollaborate/spimdisasm/tree/develop) 78 | branch. PRs should be made into that branch instead of the main one. 79 | 80 | The recommended way to install a locally cloned repo is by passing the `-e` 81 | (editable) flag to `pip`. 82 | 83 | ```bash 84 | python3 -m pip install -e . 85 | ``` 86 | 87 | In case you want to mess with the latest development version without wanting to 88 | clone the repository, then you could use the following command: 89 | 90 | ```bash 91 | python3 -m pip uninstall spimdisasm 92 | python3 -m pip install git+https://github.com/Decompollaborate/spimdisasm.git@develop 93 | ``` 94 | 95 | NOTE: Installing the development version is not recommended unless you know what 96 | you are doing. Proceed at your own risk. 97 | 98 | ## Versioning and changelog 99 | 100 | This library follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 101 | We try to always keep backwards compatibility, so no breaking changes should 102 | happen until a major release (i.e. jumping from 1.X.X to 2.0.0). 103 | 104 | To see what changed on each release check either the [CHANGELOG.md](CHANGELOG.md) 105 | file or check the [releases page on Github](https://github.com/Decompollaborate/spimdisasm/releases). 106 | You can also use [this link](https://github.com/Decompollaborate/spimdisasm/releases/latest) 107 | to check the latest release. 108 | 109 | ## How to use 110 | 111 | This repo can be used either by using the existing front-end scripts or by 112 | creating new programs on top of the back-end API. 113 | 114 | ### Front-end 115 | 116 | Every front-end CLI tool has its own `--help` screen. 117 | 118 | The included tool can be executed with either `spimdisasm modulename` (for 119 | example `spimdisasm disasmdis --help`) or directly `modulename` (for example 120 | `spimdisasm --help`) 121 | 122 | - `singleFileDisasm`: Allows to disassemble a single binary file, producing 123 | matching assembly files. 124 | 125 | - `disasmdis`: Disassembles raw hex passed to the CLI as a MIPS instruction. 126 | 127 | - `elfObjDisasm`: \[EXPERIMENTAL\] Allows to disassemble elf files. Generated 128 | assembly files are not guaranteed to match or even be assemblable. 129 | 130 | - `rspDisasm`: Disassemblies RSP binaries. 131 | 132 | ### Back-end 133 | 134 | TODO 135 | 136 | Check the already existing front-ends is recommended for now. 137 | 138 | ## References 139 | 140 | - 141 | - 142 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.9 3 | check_untyped_defs = True 4 | disallow_untyped_defs = True 5 | disallow_any_unimported = True 6 | no_implicit_optional = True 7 | warn_return_any = True 8 | show_error_codes = True 9 | warn_unused_ignores = True 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 2 | # SPDX-License-Identifier: MIT 3 | 4 | [project] 5 | name = "spimdisasm" 6 | # Version should be synced with spimdisasm/__init__.py 7 | version = "1.35.0" 8 | description = "MIPS disassembler" 9 | readme = "README.md" 10 | license = {file = "LICENSE"} 11 | requires-python = ">=3.9" 12 | authors = [ 13 | { name="Anghelo Carvajal", email="angheloalf95@gmail.com" }, 14 | ] 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | 18 | "License :: OSI Approved :: MIT License", 19 | 20 | "Topic :: Software Development :: Disassemblers", 21 | 22 | "Topic :: Software Development :: Libraries", 23 | "Topic :: Software Development :: Libraries :: Python Modules", 24 | 25 | "Typing :: Typed", 26 | ] 27 | dynamic = ["dependencies"] 28 | 29 | [project.urls] 30 | Repository = "https://github.com/Decompollaborate/spimdisasm" 31 | Issues = "https://github.com/Decompollaborate/spimdisasm/issues" 32 | Changelog = "https://github.com/Decompollaborate/spimdisasm/blob/master/CHANGELOG.md" 33 | 34 | [build-system] 35 | requires = ["twine>=6.1.0", "setuptools>=79.0", "wheel"] 36 | build-backend = "setuptools.build_meta" 37 | 38 | [project.scripts] 39 | spimdisasm = "spimdisasm.frontendCommon.FrontendUtilities:cliMain" 40 | singleFileDisasm = "spimdisasm.singleFileDisasm:disassemblerMain" 41 | disasmdis = "spimdisasm.disasmdis:disasmdisMain" 42 | elfObjDisasm = "spimdisasm.elfObjDisasm:elfObjDisasmMain" 43 | rspDisasm = "spimdisasm.rspDisasm:rspDisasmMain" 44 | 45 | [tool.setuptools.packages.find] 46 | where = ["."] 47 | exclude = ["build*"] 48 | 49 | [tool.setuptools.dynamic] 50 | dependencies = {file = "requirements.txt"} 51 | 52 | [tool.setuptools.package-data] 53 | spimdisasm = ["py.typed"] 54 | 55 | [tool.cibuildwheel] 56 | skip = ["cp36-*"] 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | rabbitizer>=1.12.5,<2.0.0 2 | -------------------------------------------------------------------------------- /spimdisasm/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | __version_info__: tuple[int, int, int] = (1, 35, 0) 9 | __version__ = ".".join(map(str, __version_info__)) # + "-dev0" 10 | __author__ = "Decompollaborate" 11 | 12 | from . import common as common 13 | from . import elf32 as elf32 14 | from . import mips as mips 15 | 16 | # Front-end scripts 17 | from . import frontendCommon as frontendCommon 18 | from . import disasmdis as disasmdis 19 | from . import rspDisasm as rspDisasm 20 | from . import elfObjDisasm as elfObjDisasm 21 | from . import singleFileDisasm as singleFileDisasm 22 | -------------------------------------------------------------------------------- /spimdisasm/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import spimdisasm 9 | 10 | 11 | if __name__ == "__main__": 12 | exit(spimdisasm.frontendCommon.FrontendUtilities.cliMain()) 13 | -------------------------------------------------------------------------------- /spimdisasm/common/CompilerConfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import dataclasses 9 | import enum 10 | 11 | 12 | @dataclasses.dataclass 13 | class CompilerProperties: 14 | name: str 15 | 16 | hasLateRodata: bool = False 17 | 18 | prevAlign_double: int|None = None # TODO: Specifying 3 as the default should be harmless. Need to investigate. 19 | prevAlign_jumptable: int|None = None 20 | prevAlign_string: int|None = 2 21 | prevAlign_function: int|None = None 22 | 23 | pairMultipleHiToSameLow: bool = True 24 | 25 | allowRdataMigration: bool = False 26 | 27 | bigAddendWorkaroundForMigratedFunctions: bool = True 28 | """ 29 | Modern GAS can handle big addends (outside the 16-bits range) for the `%lo` 30 | directive just fine, but old assemblers choke on them, so we truncate them 31 | to said range when building with those assemblers. 32 | 33 | Decomp projects usually use two assemblers: 34 | - One to assemble unmigrated files, usually with modern GAS. 35 | - Another one to assemble individual functions that get inserted into C 36 | files, either with asm directives from the compiler (using the built-in 37 | old assembler shipped with the old compiler) or with an external tools 38 | (like asm-proc for IDO). 39 | 40 | Modern GAS requires no addend truncation to produce matching output, so we 41 | don't use the workaround for unmigrated asm files. 42 | 43 | For migrated functions we need to know if the compiler uses modern GAS or 44 | old GAS. If it uses modern GAS (like IDO projects), then this flag should 45 | be turned off, but if the project uses its own old assembler (like most GCC 46 | based projects) then this flag needs to be turned on. 47 | """ 48 | 49 | sectionAlign_text: int|None = None 50 | """ 51 | The value the compiler will use to align the `.text` section of the given 52 | object. 53 | 54 | Used for determining `.text` file splits when disassembling full ROM images. 55 | 56 | The real aligment value will be computed like `1 << x`, where `x` 57 | corresponds to the value given to this property. 58 | 59 | If a compiler emits multiple `.text` sections per object (i.e. each function 60 | is emitted on its own section) then it is better to keep this value as 61 | `None`, since the split detector won't give any meaningful result. 62 | """ 63 | 64 | sectionAlign_rodata: int|None = None 65 | """ 66 | The value the compiler will use to align the `.rodata` section of the given 67 | object. 68 | 69 | Used for determining `.rodata` file splits when disassembling full ROM images. 70 | 71 | The real aligment value will be computed like `1 << x`, where `x` 72 | corresponds to the value given to this property. 73 | """ 74 | 75 | 76 | @enum.unique 77 | class Compiler(enum.Enum): 78 | # General GCC 79 | GCC = CompilerProperties("GCC", prevAlign_jumptable=3) 80 | 81 | # N64 82 | IDO = CompilerProperties("IDO", hasLateRodata=True, pairMultipleHiToSameLow=False, bigAddendWorkaroundForMigratedFunctions=False, sectionAlign_text=4, sectionAlign_rodata=4) 83 | KMC = CompilerProperties("KMC", prevAlign_jumptable=3, sectionAlign_text=4, sectionAlign_rodata=4) 84 | SN64 = CompilerProperties("SN64", prevAlign_double=3, prevAlign_jumptable=3, allowRdataMigration=True, sectionAlign_text=4, sectionAlign_rodata=4) 85 | 86 | # iQue 87 | EGCS = CompilerProperties("EGCS", prevAlign_jumptable=3, sectionAlign_text=4, sectionAlign_rodata=4) 88 | 89 | # PS1 90 | PSYQ = CompilerProperties("PSYQ", prevAlign_double=3, prevAlign_jumptable=3, allowRdataMigration=True) 91 | 92 | # PS2 93 | MWCCPS2 = CompilerProperties("MWCCPS2", prevAlign_jumptable=4) 94 | EEGCC = CompilerProperties("EEGCC", prevAlign_jumptable=3, prevAlign_string=3, prevAlign_function=3) 95 | 96 | @staticmethod 97 | def fromStr(value: str) -> Compiler|None: 98 | return compilerOptions.get(value) 99 | 100 | 101 | compilerOptions: dict[str, Compiler] = { 102 | x.name: x 103 | for x in [ 104 | Compiler.GCC, 105 | Compiler.IDO, 106 | Compiler.KMC, 107 | Compiler.SN64, 108 | Compiler.EGCS, 109 | Compiler.PSYQ, 110 | Compiler.MWCCPS2, 111 | Compiler.EEGCC, 112 | ] 113 | } 114 | -------------------------------------------------------------------------------- /spimdisasm/common/Context.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import argparse 9 | import dataclasses 10 | from pathlib import Path 11 | 12 | from . import Utils 13 | from .ContextSymbols import ContextSymbol 14 | from .SymbolsSegment import SymbolsSegment 15 | from .GpAccesses import GpAccessContainer 16 | from .Relocation import RelocationInfo, RelocType 17 | 18 | 19 | @dataclasses.dataclass 20 | class AddressRange: 21 | start: int 22 | end: int 23 | 24 | def isInRange(self, address: int) -> bool: 25 | return self.start <= address < self.end 26 | 27 | def decreaseStart(self, address: int) -> None: 28 | if address < self.start: 29 | self.start = address 30 | return None 31 | 32 | def increaseEnd(self, address: int) -> None: 33 | if address > self.end: 34 | self.end = address 35 | return None 36 | 37 | def __str__(self) -> str: 38 | return f"AddressRange(0x{self.start:08X}, 0x{self.end:08X})" 39 | 40 | def __repr__(self) -> str: 41 | return self.__str__() 42 | 43 | class SymbolsRanges: 44 | def __init__(self, start: int, end: int) -> None: 45 | self.mainAddressRange = AddressRange(start, end) 46 | self.specialRanges: list[AddressRange] = list() 47 | 48 | def isInRange(self, address: int) -> bool: 49 | if self.mainAddressRange.isInRange(address): 50 | return True 51 | 52 | for spRange in self.specialRanges: 53 | if spRange.isInRange(address): 54 | return True 55 | 56 | return False 57 | 58 | def decreaseStart(self, address: int) -> None: 59 | self.mainAddressRange.decreaseStart(address) 60 | 61 | def increaseEnd(self, address: int) -> None: 62 | self.mainAddressRange.increaseEnd(address) 63 | 64 | def addSpecialRange(self, start: int, end: int) -> AddressRange|None: 65 | # print(f"addSpecialRange: {start:X} {end:X}") 66 | if end <= start: 67 | return None 68 | addrRange = AddressRange(start, end) 69 | self.specialRanges.append(addrRange) 70 | return addrRange 71 | 72 | def __str__(self) -> str: 73 | ret = f"MainRange: {self.mainAddressRange}\n" 74 | for i, spRange in enumerate(self.specialRanges): 75 | ret += f" {i}'th special: {spRange}\n" 76 | return ret 77 | 78 | class Context: 79 | N64DefaultBanned = { 80 | 0x7FFFFFE0, # osInvalICache 81 | 0x7FFFFFF0, # osInvalDCache, osWritebackDCache, osWritebackDCacheAll 82 | 0x7FFFFFFF, 83 | 0x80000010, 84 | 0x80000020, 85 | } 86 | 87 | def __init__(self) -> None: 88 | # Arbitrary initial range 89 | self.globalSegment = SymbolsSegment(self, 0x0, 0x1000, 0x80000000, 0x80001000, overlayCategory=None) 90 | # For symbols that we don't know where they come from 91 | self.unknownSegment = SymbolsSegment(self, None, None, 0x00000000, 0xFFFFFFFF, overlayCategory=None) 92 | self._isTheUnknownSegment = True 93 | 94 | self.overlaySegments: dict[str, dict[int, SymbolsSegment]] = dict() 95 | "Outer key is overlay type, inner key is the vrom of the overlay's segment" 96 | 97 | self.totalVramRange: SymbolsRanges = SymbolsRanges(self.globalSegment.vramStart, self.globalSegment.vramEnd) 98 | self._defaultVramRanges: bool = True 99 | 100 | # Stuff that looks like pointers, but the disassembler shouldn't count it as a pointer 101 | self.bannedSymbols: set[int] = set() 102 | self.bannedRangedSymbols: list[AddressRange] = list() 103 | 104 | self.globalRelocationOverrides: dict[int, RelocationInfo] = dict() 105 | "key: vrom address" 106 | 107 | self.gpAccesses = GpAccessContainer() 108 | 109 | 110 | def changeGlobalSegmentRanges(self, vromStart: int, vromEnd: int, vramStart: int, vramEnd: int) -> None: 111 | if vromStart == vromEnd: 112 | Utils.eprint(f"Warning: globalSegment's will has its vromStart equal to the vromEnd (0x{vromStart:X})") 113 | if vramStart == vramEnd: 114 | Utils.eprint(f"Warning: globalSegment's will has its vramStart equal to the vramEnd (0x{vramStart:X})") 115 | self.globalSegment.changeRanges(vromStart, vromEnd, vramStart, vramEnd) 116 | if self._defaultVramRanges: 117 | self.totalVramRange.mainAddressRange.start = vramStart 118 | self.totalVramRange.mainAddressRange.end = vramEnd 119 | self._defaultVramRanges = False 120 | self.totalVramRange.decreaseStart(vramStart) 121 | self.totalVramRange.increaseEnd(vramEnd) 122 | 123 | def addOverlaySegment(self, overlayCategory: str, segmentVromStart: int, segmentVromEnd: int, segmentVramStart: int, segmentVramEnd: int) -> SymbolsSegment: 124 | if overlayCategory not in self.overlaySegments: 125 | self.overlaySegments[overlayCategory] = dict() 126 | segment = SymbolsSegment(self, segmentVromStart, segmentVromEnd, segmentVramStart, segmentVramEnd, overlayCategory=overlayCategory) 127 | self.overlaySegments[overlayCategory][segmentVromStart] = segment 128 | 129 | if self._defaultVramRanges: 130 | self.totalVramRange.mainAddressRange.start = segmentVramStart 131 | self.totalVramRange.mainAddressRange.end = segmentVramEnd 132 | self._defaultVramRanges = False 133 | self.totalVramRange.decreaseStart(segmentVramStart) 134 | self.totalVramRange.increaseEnd(segmentVramEnd) 135 | 136 | return segment 137 | 138 | def isInTotalVramRange(self, address: int) -> bool: 139 | return self.totalVramRange.isInRange(address) 140 | 141 | def addSpecialVramRange(self, start: int, end: int) -> AddressRange|None: 142 | return self.totalVramRange.addSpecialRange(start, end) 143 | 144 | 145 | def initGotTable(self, pltGot: int, localsTable: list[int], globalsTable: list[int]) -> None: 146 | self.gpAccesses.initGotTable(pltGot, localsTable, globalsTable) 147 | 148 | for gotEntry in self.gpAccesses.got.globalsTable: 149 | contextSym = self.globalSegment.addSymbol(gotEntry) 150 | contextSym.isUserDeclared = True 151 | contextSym.isGotGlobal = True 152 | 153 | def addSmallSection(self, address: int, size: int) -> None: 154 | self.gpAccesses.addSmallSection(address, size) 155 | 156 | 157 | def fillDefaultBannedSymbols(self) -> None: 158 | self.bannedSymbols |= self.N64DefaultBanned 159 | 160 | 161 | def isAddressInGlobalRange(self, address: int) -> bool: 162 | return self.totalVramRange.isInRange(address) 163 | 164 | 165 | def addBannedSymbol(self, address: int) -> None: 166 | self.bannedSymbols.add(address) 167 | 168 | def addBannedSymbolRange(self, rangeStart: int, rangeEnd: int) -> None: 169 | self.bannedRangedSymbols.append(AddressRange(rangeStart, rangeEnd)) 170 | 171 | def addBannedSymbolRangeBySize(self, rangeStart: int, size: int) -> None: 172 | self.bannedRangedSymbols.append(AddressRange(rangeStart, rangeStart + size)) 173 | 174 | def isAddressBanned(self, address: int) -> bool: 175 | if address in self.bannedSymbols: 176 | return True 177 | for ranged in self.bannedRangedSymbols: 178 | if ranged.isInRange(address): 179 | return True 180 | return False 181 | 182 | def addGlobalReloc(self, vromAddres: int, relocType: RelocType, symbol: ContextSymbol|str, addend: int=0) -> RelocationInfo: 183 | reloc = RelocationInfo(relocType, symbol, addend, globalReloc=True) 184 | self.globalRelocationOverrides[vromAddres] = reloc 185 | return reloc 186 | 187 | def saveContextToFile(self, contextPath: Path) -> None: 188 | with contextPath.open("w") as f: 189 | self.globalSegment.saveContextToFile(f) 190 | 191 | # unknownPath = contextPath.with_stem(f"{contextPath.stem}_unksegment") 192 | unknownPath = contextPath.with_name(f"{contextPath.stem}_unksegment" + contextPath.suffix) 193 | with unknownPath.open("w") as f: 194 | self.unknownSegment.saveContextToFile(f) 195 | 196 | for overlayCategory, segmentsPerVrom in self.overlaySegments.items(): 197 | for segmentVrom, overlaySegment in segmentsPerVrom.items(): 198 | 199 | # ovlPath = contextPath.with_stem(f"{contextPath.stem}_{overlayCategory}_{segmentVrom:06X}") 200 | ovlPath = contextPath.with_name(f"{contextPath.stem}_{overlayCategory}_{segmentVrom:06X}" + contextPath.suffix) 201 | with ovlPath.open("w") as f: 202 | overlaySegment.saveContextToFile(f) 203 | 204 | 205 | @staticmethod 206 | def addParametersToArgParse(parser: argparse.ArgumentParser) -> None: 207 | contextParser = parser.add_argument_group("Context configuration") 208 | 209 | contextParser.add_argument("--save-context", help="Saves the context to a file", metavar="FILENAME") 210 | 211 | 212 | csvConfig = parser.add_argument_group("Context .csv input files") 213 | 214 | csvConfig.add_argument("--functions", help="Path to a functions csv", action="append") 215 | csvConfig.add_argument("--variables", help="Path to a variables csv", action="append") 216 | csvConfig.add_argument("--constants", help="Path to a constants csv", action="append") 217 | csvConfig.add_argument("--symbol-addrs", help="Path to a splat-compatible symbol_addrs.txt file", action="append") 218 | 219 | 220 | symbolsConfig = parser.add_argument_group("Context default symbols configuration") 221 | 222 | symbolsConfig.add_argument("--default-banned", help="Toggles filling the list of default banned symbols. Defaults to True", action=Utils.BooleanOptionalAction) 223 | symbolsConfig.add_argument("--libultra-syms", help="Toggles using the built-in libultra symbols. Defaults to True", action=Utils.BooleanOptionalAction) 224 | symbolsConfig.add_argument("--ique-syms", help="Toggles using the built-in iQue libultra symbols. Defaults to True", action=Utils.BooleanOptionalAction) 225 | symbolsConfig.add_argument("--hardware-regs", help="Toggles using the built-in hardware registers symbols. Defaults to True", action=Utils.BooleanOptionalAction) 226 | symbolsConfig.add_argument("--named-hardware-regs", help="Use actual names for the hardware registers", action=Utils.BooleanOptionalAction) 227 | 228 | 229 | def parseArgs(self, args: argparse.Namespace) -> None: 230 | if args.default_banned != False: 231 | self.fillDefaultBannedSymbols() 232 | if args.libultra_syms != False: 233 | self.globalSegment.fillLibultraSymbols() 234 | if args.ique_syms != False: 235 | self.globalSegment.fillIQueSymbols() 236 | if args.hardware_regs != False: 237 | self.globalSegment.fillHardwareRegs(args.named_hardware_regs) 238 | 239 | if args.functions is not None: 240 | for funcsPath in args.functions: 241 | self.globalSegment.readFunctionsCsv(Path(funcsPath)) 242 | if args.variables is not None: 243 | for varsPath in args.variables: 244 | self.globalSegment.readVariablesCsv(Path(varsPath)) 245 | if args.constants is not None: 246 | for constantsPath in args.constants: 247 | self.globalSegment.readConstantsCsv(Path(constantsPath)) 248 | if args.symbol_addrs is not None: 249 | for filepath in args.symbol_addrs: 250 | self.globalSegment.readSplatSymbolAddrs(Path(filepath)) 251 | -------------------------------------------------------------------------------- /spimdisasm/common/FileSectionType.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from .OrderedEnum import OrderedEnum 9 | 10 | 11 | class FileSectionType(OrderedEnum): 12 | Dummy = -4 13 | End = -3 14 | Unknown = -2 15 | Invalid = -1 16 | 17 | Text = 1 18 | Data = 2 19 | Rodata = 3 20 | Bss = 4 21 | Reloc = 5 22 | GccExceptTable = 6 23 | 24 | @staticmethod 25 | def fromId(sectionId: int) -> FileSectionType: 26 | try: 27 | return FileSectionType(sectionId) 28 | except: 29 | return FileSectionType.Invalid 30 | 31 | @staticmethod 32 | def fromStr(x: str) -> FileSectionType: 33 | return gNameToSectionType.get(x, FileSectionType.Invalid) 34 | 35 | @staticmethod 36 | def fromSmallStr(x: str) -> FileSectionType: 37 | return gSmallNameToSectionType.get(x, FileSectionType.Invalid) 38 | 39 | def toStr(self) -> str: 40 | if self == FileSectionType.Text: 41 | return ".text" 42 | if self == FileSectionType.Data: 43 | return ".data" 44 | if self == FileSectionType.Rodata: 45 | return ".rodata" 46 | if self == FileSectionType.Bss: 47 | return ".bss" 48 | if self == FileSectionType.Reloc: 49 | return ".reloc" 50 | if self == FileSectionType.GccExceptTable: 51 | return ".gcc_except_table" 52 | return "" 53 | 54 | def toCapitalizedStr(self) -> str: 55 | if self == FileSectionType.Text: 56 | return "Text" 57 | if self == FileSectionType.Data: 58 | return "Data" 59 | if self == FileSectionType.Rodata: 60 | return "RoData" 61 | if self == FileSectionType.Bss: 62 | return "Bss" 63 | if self == FileSectionType.Reloc: 64 | return "Reloc" 65 | if self == FileSectionType.GccExceptTable: 66 | return "GccExceptTable" 67 | return "" 68 | 69 | def toSectionName(self) -> str: 70 | if self == FileSectionType.Text: 71 | return ".text" 72 | if self == FileSectionType.Data: 73 | return ".data" 74 | if self == FileSectionType.Rodata: 75 | return ".rodata" 76 | if self == FileSectionType.Bss: 77 | return ".bss" 78 | if self == FileSectionType.Reloc: 79 | return ".ovl" 80 | if self == FileSectionType.GccExceptTable: 81 | return ".gcc_except_table" 82 | return "" 83 | 84 | gNameToSectionType = { 85 | ".text": FileSectionType.Text, 86 | ".data": FileSectionType.Data, 87 | ".rodata": FileSectionType.Rodata, 88 | ".rdata": FileSectionType.Rodata, 89 | ".bss": FileSectionType.Bss, 90 | ".ovl": FileSectionType.Reloc, 91 | ".reloc": FileSectionType.Reloc, 92 | ".gcc_except_table": FileSectionType.GccExceptTable, 93 | ".end": FileSectionType.End, 94 | ".dummy": FileSectionType.Dummy, 95 | } 96 | gSmallNameToSectionType = { 97 | ".sdata": FileSectionType.Data, 98 | ".srodata": FileSectionType.Rodata, 99 | ".srdata": FileSectionType.Rodata, 100 | ".sbss": FileSectionType.Bss, 101 | } 102 | 103 | 104 | FileSections_ListBasic = [FileSectionType.Text, FileSectionType.Data, FileSectionType.Rodata, FileSectionType.Bss] 105 | FileSections_ListAll = [FileSectionType.Text, FileSectionType.Data, FileSectionType.Rodata, FileSectionType.Bss, FileSectionType.Reloc, FileSectionType.GccExceptTable] 106 | -------------------------------------------------------------------------------- /spimdisasm/common/FileSplitFormat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from pathlib import Path 9 | from typing import Generator 10 | 11 | from . import Utils 12 | from .FileSectionType import FileSectionType 13 | 14 | 15 | class FileSplitEntry: 16 | def __init__(self, offset: int, vram: int, fileName: str, section: FileSectionType, nextOffset: int, isHandwritten: bool, isRsp: bool) -> None: 17 | self.offset: int = offset 18 | self.vram: int = vram 19 | self.fileName: str = fileName 20 | self.section: FileSectionType = section 21 | self.nextOffset: int = nextOffset 22 | self.isHandwritten: bool = isHandwritten 23 | self.isRsp: bool = isRsp 24 | 25 | 26 | class FileSplitFormat: 27 | def __init__(self, csvPath: Path|None = None) -> None: 28 | self.splits: list[list[str]] = list() 29 | 30 | if csvPath is not None: 31 | self.readCsvFile(csvPath) 32 | 33 | def __len__(self) -> int: 34 | return len(self.splits) 35 | 36 | def __iter__(self) -> Generator[FileSplitEntry, None, None]: 37 | section = FileSectionType.Invalid 38 | 39 | for i, row in enumerate(self.splits): 40 | offsetStr, vramStr, fileName = row 41 | 42 | isHandwritten = False 43 | isRsp = False 44 | offsetStr = offsetStr.upper() 45 | if offsetStr[-1] == "H": 46 | isHandwritten = True 47 | offsetStr = offsetStr[:-1] 48 | elif offsetStr[-1] == "R": 49 | isRsp = True 50 | offsetStr = offsetStr[:-1] 51 | 52 | possibleSection = FileSectionType.fromStr(fileName) 53 | if possibleSection != FileSectionType.Invalid: 54 | if possibleSection == FileSectionType.End: 55 | break 56 | else: 57 | section = possibleSection 58 | continue 59 | 60 | vram = int(vramStr, 16) 61 | offset = int(offsetStr, 16) 62 | nextOffset = 0xFFFFFF 63 | if i + 1 < len(self.splits): 64 | if self.splits[i+1][2] == ".end": 65 | nextOffsetStr = self.splits[i+1][0] 66 | elif self.splits[i+1][2].startswith("."): 67 | nextOffsetStr = self.splits[i+2][0] 68 | else: 69 | nextOffsetStr = self.splits[i+1][0] 70 | if nextOffsetStr.upper()[-1] in ("H", "R"): 71 | nextOffsetStr = nextOffsetStr[:-1] 72 | nextOffset = int(nextOffsetStr, 16) 73 | 74 | yield FileSplitEntry(offset, vram, fileName, section, nextOffset, isHandwritten, isRsp) 75 | 76 | def readCsvFile(self, csvPath: Path) -> None: 77 | self.splits = Utils.readCsv(csvPath) 78 | self.splits = [x for x in self.splits if len(x) > 0] 79 | 80 | def append(self, element: FileSplitEntry | list[str]) -> None: 81 | if isinstance(element, FileSplitEntry): 82 | 83 | offset = f"{element.offset:X}" 84 | if element.isRsp: 85 | offset += "R" 86 | elif element.isHandwritten: 87 | offset += "H" 88 | 89 | vram = f"{element.vram:X}" 90 | fileName = element.fileName 91 | 92 | if element.section != FileSectionType.Invalid: 93 | section = element.section.toStr() 94 | self.splits.append(["offset", "vram", section]) 95 | 96 | self.splits.append([offset, vram, fileName]) 97 | 98 | # nextOffset # ignored 99 | elif isinstance(element, list): 100 | if len(element) != 3: 101 | # TODO: error message 102 | raise TypeError() 103 | for x in element: 104 | if not isinstance(x, str): 105 | # TODO: error message 106 | raise TypeError() 107 | self.splits.append(element) 108 | else: 109 | # TODO: error message 110 | raise TypeError() 111 | 112 | def appendEndSection(self, offset: int, vram: int) -> None: 113 | self.splits.append([f"{offset:X}", f"{vram:X}", ".end"]) 114 | -------------------------------------------------------------------------------- /spimdisasm/common/GpAccesses.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import dataclasses 9 | 10 | from .. import common 11 | 12 | from .SortedDict import SortedDict 13 | 14 | 15 | class GlobalOffsetTable: 16 | def __init__(self) -> None: 17 | self.localsTable: list[int] = list() 18 | self.globalsTable: list[int] = list() 19 | 20 | self.tableAddress: int|None = None 21 | 22 | 23 | def initTables(self, pltGot: int, localsTable: list[int], globalsTable: list[int]) -> None: 24 | self.tableAddress = pltGot 25 | self.localsTable = [address for address in localsTable] 26 | self.globalsTable = [address for address in globalsTable] 27 | 28 | 29 | def getGotSymEntry(self, address: int) -> tuple[int|None, bool|None]: 30 | if self.tableAddress is None: 31 | return None, None 32 | 33 | index = (address - self.tableAddress) // 4 34 | 35 | if index < 0: 36 | common.Utils.eprint(f"Warning: %got address 0x{address:X} not found on local or global table (negative index)") 37 | common.Utils.eprint(f" index: {index}, len(localsTable):{len(self.localsTable)}, len(globalsTable): {len(self.globalsTable)}") 38 | return None, None 39 | 40 | if index < len(self.localsTable): 41 | return self.localsTable[index], False 42 | 43 | index -= len(self.localsTable) 44 | if index >= len(self.globalsTable): 45 | common.Utils.eprint(f"Warning: %got address 0x{address:X} not found on local or global table (out of range)") 46 | common.Utils.eprint(f" index: {index}, len(globalsTable): {len(self.globalsTable)}") 47 | return None, None 48 | return self.globalsTable[index], True 49 | 50 | 51 | @dataclasses.dataclass 52 | class SmallSection: 53 | address: int 54 | size: int 55 | 56 | def isInRange(self, address: int) -> bool: 57 | return self.address <= address < self.address + self.size 58 | 59 | 60 | @dataclasses.dataclass 61 | class GpAccess: 62 | address: int 63 | 64 | isGotLocal: bool = False 65 | isGotGlobal: bool = False 66 | isSmallSection: bool = False 67 | 68 | @property 69 | def isGot(self) -> bool: 70 | return self.isGotLocal or self.isGotGlobal 71 | 72 | 73 | class GpAccessContainer: 74 | def __init__(self) -> None: 75 | self.got = GlobalOffsetTable() 76 | self.smallSections: SortedDict[SmallSection] = SortedDict() 77 | 78 | def addSmallSection(self, address: int, size: int) -> None: 79 | self.smallSections[address] = SmallSection(address, size) 80 | 81 | def initGotTable(self, tableAddress: int, localsTable: list[int], globalsTable: list[int]) -> None: 82 | self.got.initTables(tableAddress, localsTable, globalsTable) 83 | 84 | self.addSmallSection(tableAddress, (len(localsTable) + len(globalsTable)) * 4) 85 | 86 | def requestAddress(self, address: int) -> GpAccess|None: 87 | small = self.smallSections.getKeyRight(address) 88 | if small is None: 89 | common.Utils.eprint(f"Warning: No section found for $gp access at address 0x{address:08X}") 90 | return None 91 | 92 | sectionAddr, sectionData = small 93 | 94 | if sectionAddr != self.got.tableAddress: 95 | if not sectionData.isInRange(address): 96 | common.Utils.eprint(f"Warning: No section found for $gp access at address 0x{address:08X}") 97 | return None 98 | 99 | # small section 100 | access = GpAccess(address) 101 | access.isSmallSection = True 102 | return access 103 | 104 | # got 105 | gotEntry, inGlobalTable = self.got.getGotSymEntry(address) 106 | 107 | if gotEntry is None or inGlobalTable is None: 108 | return None 109 | 110 | access = GpAccess(gotEntry) 111 | access.isGotGlobal = inGlobalTable 112 | access.isGotLocal = not inGlobalTable 113 | 114 | return access 115 | -------------------------------------------------------------------------------- /spimdisasm/common/OrderedEnum.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import enum 9 | 10 | 11 | class OrderedEnum(enum.Enum): 12 | def __ge__(self, other: OrderedEnum) -> bool: 13 | if isinstance(other, self.__class__): 14 | return bool(self.value >= other.value) 15 | return NotImplemented 16 | 17 | def __gt__(self, other: OrderedEnum) -> bool: 18 | if isinstance(other, self.__class__): 19 | return bool(self.value > other.value) 20 | return NotImplemented 21 | 22 | def __le__(self, other: OrderedEnum) -> bool: 23 | if isinstance(other, self.__class__): 24 | return bool(self.value <= other.value) 25 | return NotImplemented 26 | 27 | def __lt__(self, other: OrderedEnum) -> bool: 28 | if isinstance(other, self.__class__): 29 | return bool(self.value < other.value) 30 | return NotImplemented 31 | -------------------------------------------------------------------------------- /spimdisasm/common/Relocation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import dataclasses 9 | import enum 10 | 11 | from .ContextSymbols import ContextSymbol 12 | from .FileSectionType import FileSectionType 13 | from .GlobalConfig import GlobalConfig 14 | 15 | 16 | class RelocType(enum.Enum): 17 | MIPS_NONE = 0 # No reloc 18 | MIPS_16 = 1 # Direct 16 bit 19 | MIPS_32 = 2 # Direct 32 bit 20 | MIPS_REL32 = 3 # PC relative 32 bit 21 | MIPS_26 = 4 # Direct 26 bit shifted 22 | MIPS_HI16 = 5 # High 16 bit 23 | MIPS_LO16 = 6 # Low 16 bit 24 | MIPS_GPREL16 = 7 # GP relative 16 bit 25 | MIPS_LITERAL = 8 # 16 bit literal entry 26 | MIPS_GOT16 = 9 # 16 bit GOT entry 27 | MIPS_PC16 = 10 # PC relative 16 bit 28 | MIPS_CALL16 = 11 # 16 bit GOT entry for function 29 | MIPS_GPREL32 = 12 # GP relative 32 bit 30 | 31 | MIPS_GOT_HI16 = 22 32 | MIPS_GOT_LO16 = 23 33 | MIPS_CALL_HI16 = 30 34 | MIPS_CALL_LO16 = 31 35 | 36 | CUSTOM_CONSTANT_HI = -1 37 | CUSTOM_CONSTANT_LO = -2 38 | 39 | def getPercentRel(self) -> str|None: 40 | return _percentRel.get(self) 41 | 42 | def getWordRel(self) -> str|None: 43 | return _wordRel.get(self) 44 | 45 | @staticmethod 46 | def fromValue(value: int) -> RelocType|None: 47 | try: 48 | return RelocType(value) 49 | except ValueError: 50 | return None 51 | 52 | @staticmethod 53 | def fromStr(value: str) -> RelocType|None: 54 | value = value.upper() 55 | if value == "MIPS_NONE": 56 | return RelocType.MIPS_NONE 57 | if value == "MIPS_16": 58 | return RelocType.MIPS_16 59 | if value == "MIPS_32": 60 | return RelocType.MIPS_32 61 | if value == "MIPS_REL32": 62 | return RelocType.MIPS_REL32 63 | if value == "MIPS_26": 64 | return RelocType.MIPS_26 65 | if value == "MIPS_HI16": 66 | return RelocType.MIPS_HI16 67 | if value == "MIPS_LO16": 68 | return RelocType.MIPS_LO16 69 | if value == "MIPS_GPREL16": 70 | return RelocType.MIPS_GPREL16 71 | if value == "MIPS_LITERAL": 72 | return RelocType.MIPS_LITERAL 73 | if value == "MIPS_GOT16": 74 | return RelocType.MIPS_GOT16 75 | if value == "MIPS_PC16": 76 | return RelocType.MIPS_PC16 77 | if value == "MIPS_CALL16": 78 | return RelocType.MIPS_CALL16 79 | if value == "MIPS_GPREL32": 80 | return RelocType.MIPS_GPREL32 81 | if value == "MIPS_GOT_HI16": 82 | return RelocType.MIPS_GOT_HI16 83 | if value == "MIPS_GOT_LO16": 84 | return RelocType.MIPS_GOT_LO16 85 | if value == "MIPS_CALL_HI16": 86 | return RelocType.MIPS_CALL_HI16 87 | if value == "MIPS_CALL_LO16": 88 | return RelocType.MIPS_CALL_LO16 89 | if value == "CUSTOM_CONSTANT_HI": 90 | return RelocType.CUSTOM_CONSTANT_HI 91 | if value == "CUSTOM_CONSTANT_LO": 92 | return RelocType.CUSTOM_CONSTANT_LO 93 | return None 94 | 95 | 96 | _percentRel = { 97 | # RelocType.MIPS_NONE: f"", 98 | # RelocType.MIPS_16: f"", 99 | # RelocType.MIPS_32: f"", 100 | # RelocType.MIPS_REL32: f"", 101 | # RelocType.MIPS_26: f"", 102 | RelocType.MIPS_HI16: f"%hi", 103 | RelocType.MIPS_LO16: f"%lo", 104 | RelocType.MIPS_GPREL16: f"%gp_rel", 105 | # RelocType.MIPS_LITERAL: f"", 106 | RelocType.MIPS_GOT16: f"%got", 107 | # RelocType.MIPS_PC16: f"", 108 | RelocType.MIPS_CALL16: f"%call16", 109 | # RelocType.MIPS_GPREL32: f"", 110 | 111 | RelocType.MIPS_GOT_HI16: f"%got_hi", 112 | RelocType.MIPS_GOT_LO16: f"%got_lo", 113 | RelocType.MIPS_CALL_HI16: f"%call_hi", 114 | RelocType.MIPS_CALL_LO16: f"%call_lo", 115 | } 116 | 117 | _wordRel = { 118 | # RelocType.MIPS_NONE: f"", 119 | # RelocType.MIPS_16: f"", 120 | RelocType.MIPS_32: f".word", 121 | # RelocType.MIPS_REL32: f"", 122 | # RelocType.MIPS_26: f"", 123 | # RelocType.MIPS_HI16: f"", 124 | # RelocType.MIPS_LO16: f"", 125 | # RelocType.MIPS_GPREL16: f"", 126 | # RelocType.MIPS_LITERAL: f"", 127 | # RelocType.MIPS_GOT16: f"", 128 | # RelocType.MIPS_PC16: f"", 129 | # RelocType.MIPS_CALL16: f"", 130 | RelocType.MIPS_GPREL32: f".gpword", 131 | # RelocType.MIPS_GOT_HI16: f"", 132 | # RelocType.MIPS_GOT_LO16: f"", 133 | # RelocType.MIPS_CALL_HI16: f"", 134 | # RelocType.MIPS_CALL_LO16: f"", 135 | } 136 | 137 | _operationRel = { 138 | RelocType.CUSTOM_CONSTANT_HI: f">> 16", 139 | RelocType.CUSTOM_CONSTANT_LO: f"& 0xFFFF", 140 | } 141 | 142 | @dataclasses.dataclass 143 | class RelocationStaticReference: 144 | # For elfs with relocations to static symbols 145 | sectionType: FileSectionType 146 | sectionVram: int 147 | 148 | @dataclasses.dataclass 149 | class RelocationInfo: 150 | relocType: RelocType 151 | symbol: ContextSymbol|str 152 | 153 | addend: int = 0 154 | 155 | staticReference: RelocationStaticReference|None = None 156 | globalReloc: bool = False 157 | 158 | def getName(self, isSplittedSymbol: bool=False) -> str: 159 | if isinstance(self.symbol, ContextSymbol): 160 | name = self.symbol.getName() 161 | else: 162 | name = self.symbol 163 | 164 | if self.addend == 0: 165 | return name 166 | 167 | if GlobalConfig.COMPILER.value.bigAddendWorkaroundForMigratedFunctions and isSplittedSymbol: 168 | if self.relocType == RelocType.MIPS_LO16: 169 | if self.addend < -0x8000: 170 | return f"{name} - (0x{-self.addend:X} & 0xFFFF)" 171 | if self.addend > 0x7FFF: 172 | return f"{name} + (0x{self.addend:X} & 0xFFFF)" 173 | 174 | if self.addend < 0: 175 | return f"{name} - 0x{-self.addend:X}" 176 | return f"{name} + 0x{self.addend:X}" 177 | 178 | def getNameWithReloc(self, *, isSplittedSymbol: bool=False, ignoredRelocs: set[RelocType]=set()) -> str: 179 | name = self.getName(isSplittedSymbol=isSplittedSymbol) 180 | 181 | percentRel = self.relocType.getPercentRel() 182 | if percentRel is not None: 183 | if self.relocType in ignoredRelocs: 184 | return f"({name})" 185 | return f"{percentRel}({name})" 186 | 187 | wordRel = self.relocType.getWordRel() 188 | if wordRel is not None: 189 | return f"{wordRel} {name}" 190 | 191 | operationRel = _operationRel.get(self.relocType) 192 | if operationRel is not None: 193 | return f"({name} {operationRel})" 194 | 195 | return name 196 | 197 | def getInlineStr(self, isSplittedSymbol: bool=False) -> str: 198 | output = f" # {self.relocType.name} '{self.getName(isSplittedSymbol=isSplittedSymbol)}'" 199 | if self.staticReference is not None: 200 | output += f" (static)" 201 | if self.globalReloc: 202 | output += f" (global reloc)" 203 | output += f"{GlobalConfig.LINE_ENDS}" 204 | return output 205 | 206 | def isRelocNone(self) -> bool: 207 | return self.relocType == RelocType.MIPS_NONE 208 | -------------------------------------------------------------------------------- /spimdisasm/common/SortedDict.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from abc import ABCMeta, abstractmethod 9 | import bisect 10 | from typing import Any, Generator, TypeVar 11 | 12 | # typing.Mapping and typing.MutableMapping are deprecated since Python 3.9. 13 | # Using collections.abc is encouraged instead, but 3.7 and 3.8 will to run this file 14 | # with a "'ABCMeta' object is not subscriptable" exception, so this is a hacky way to 15 | # try to be future proof and not have problems because those types will be removed 16 | # in the future 17 | try: 18 | from collections.abc import Mapping, MutableMapping 19 | MutableMapping[int, int] 20 | except: 21 | from typing import Mapping, MutableMapping 22 | 23 | ValueType = TypeVar("ValueType") 24 | 25 | 26 | class SortedDict(MutableMapping[int, ValueType]): 27 | def __init__(self, other: Mapping[int, ValueType]|None=None) -> None: 28 | self.map: dict[int, ValueType] = dict() 29 | self.sortedKeys: list[int] = list() 30 | 31 | if other is not None: 32 | for key, value in other.items(): 33 | self.add(key, value) 34 | 35 | 36 | def add(self, key: int, value: ValueType) -> None: 37 | if key not in self.map: 38 | # Avoid adding the key twice if it is already on the map 39 | bisect.insort(self.sortedKeys, key) 40 | self.map[key] = value 41 | 42 | def remove(self, key: int) -> None: 43 | del self.map[key] 44 | self.sortedKeys.remove(key) 45 | 46 | 47 | def getKeyRight(self, key: int, inclusive: bool=True) -> tuple[int, ValueType]|None: 48 | """Returns the pair with the greatest key which is less or equal to the `key` parameter, or None if there's no smaller pair than the passed `key`. 49 | 50 | If `inclusive` is `False`, then the returned pair will be strictly less than the passed `key`. 51 | """ 52 | if inclusive: 53 | index = bisect.bisect_right(self.sortedKeys, key) 54 | else: 55 | index = bisect.bisect_left(self.sortedKeys, key) 56 | if index == 0: 57 | return None 58 | currentKey = self.sortedKeys[index - 1] 59 | return currentKey, self.map[currentKey] 60 | 61 | def getKeyLeft(self, key: int, inclusive: bool=True) -> tuple[int, ValueType]|None: 62 | """Returns the pair with the smallest key which is gretest or equal to the `key` parameter, or None if there's no greater pair than the passed `key`. 63 | 64 | If `inclusive` is `False`, then the returned pair will be strictly greater than the passed `key`. 65 | """ 66 | if inclusive: 67 | index = bisect.bisect_left(self.sortedKeys, key) 68 | else: 69 | index = bisect.bisect_right(self.sortedKeys, key) 70 | if index == len(self.sortedKeys): 71 | return None 72 | key = self.sortedKeys[index] 73 | return key, self.map[key] 74 | 75 | 76 | def getRange(self, startKey: int, endKey: int, startInclusive: bool=True, endInclusive: bool=False) -> Generator[tuple[int, ValueType], None, None]: 77 | """Generator which iterates in the range [`startKey`, `endKey`], returining a (key, value) tuple. 78 | 79 | By default the `startKey` is inclusive but the `endKey` isn't, this can be changed with the `startInclusive` and `endInclusive` parameters""" 80 | if startInclusive: 81 | keyIndexStart = bisect.bisect_left(self.sortedKeys, startKey) 82 | else: 83 | keyIndexStart = bisect.bisect_right(self.sortedKeys, startKey) 84 | 85 | if endInclusive: 86 | keyIndexEnd = bisect.bisect_right(self.sortedKeys, endKey) 87 | else: 88 | keyIndexEnd = bisect.bisect_left(self.sortedKeys, endKey) 89 | 90 | for index in range(keyIndexStart, keyIndexEnd): 91 | key = self.sortedKeys[index] 92 | yield (key, self.map[key]) 93 | 94 | def getRangeAndPop(self, startKey: int, endKey: int, startInclusive: bool=True, endInclusive: bool=False) -> Generator[tuple[int, ValueType], None, None]: 95 | """Similar to `getRange`, but every pair is removed from the dictionary. 96 | 97 | Please note this generator iterates in reverse/descending order""" 98 | if startInclusive: 99 | keyIndexStart = bisect.bisect_left(self.sortedKeys, startKey) 100 | else: 101 | keyIndexStart = bisect.bisect_right(self.sortedKeys, startKey) 102 | 103 | if endInclusive: 104 | keyIndexEnd = bisect.bisect_right(self.sortedKeys, endKey) 105 | else: 106 | keyIndexEnd = bisect.bisect_left(self.sortedKeys, endKey) 107 | 108 | for index in range(keyIndexEnd-1, keyIndexStart-1, -1): 109 | key = self.sortedKeys[index] 110 | value = self.map[key] 111 | self.remove(key) 112 | yield (key, value) 113 | 114 | def index(self, key: int) -> int|None: 115 | """Returns the index of the passed `key` in the sorted dictionary, or None if the key is not present.""" 116 | if key not in self.map: 117 | return None 118 | return bisect.bisect_left(self.sortedKeys, key) 119 | 120 | def __getitem__(self, key: int) -> ValueType: 121 | return self.map[key] 122 | 123 | def __setitem__(self, key: int, value: ValueType) -> None: 124 | self.add(key, value) 125 | 126 | def __delitem__(self, key: int) -> None: 127 | self.remove(key) 128 | 129 | def __iter__(self) -> Generator[int, None, None]: 130 | "Iteration is sorted by keys" 131 | for key in self.sortedKeys: 132 | yield key 133 | 134 | def __len__(self) -> int: 135 | return len(self.map) 136 | 137 | def __contains__(self, key: object) -> bool: 138 | return self.map.__contains__(key) 139 | 140 | 141 | def __str__(self) -> str: 142 | ret = "SortedDict({" 143 | comma = False 144 | for key, value in self.items(): 145 | if comma: 146 | ret += ", " 147 | ret += f"{repr(key)}: {repr(value)}" 148 | comma = True 149 | ret += "})" 150 | return ret 151 | 152 | def __repr__(self) -> str: 153 | return self.__str__() 154 | -------------------------------------------------------------------------------- /spimdisasm/common/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 2 | # SPDX-License-Identifier: MIT 3 | 4 | from . import Utils as Utils 5 | 6 | from .SortedDict import SortedDict as SortedDict 7 | from .CompilerConfig import CompilerProperties as CompilerProperties 8 | from .CompilerConfig import Compiler as Compiler 9 | from .CompilerConfig import compilerOptions as compilerOptions 10 | from .GlobalConfig import GlobalConfig as GlobalConfig 11 | from .GlobalConfig import InputEndian as InputEndian 12 | from .GlobalConfig import Abi as Abi 13 | from .GlobalConfig import ArchLevel as ArchLevel 14 | from .GlobalConfig import InputFileType as InputFileType 15 | from .FileSectionType import FileSectionType as FileSectionType 16 | from .FileSectionType import FileSections_ListBasic as FileSections_ListBasic 17 | from .FileSectionType import FileSections_ListAll as FileSections_ListAll 18 | from .ContextSymbols import SymbolSpecialType as SymbolSpecialType 19 | from .ContextSymbols import ContextSymbol as ContextSymbol 20 | from .ContextSymbols import gKnownTypes as gKnownTypes 21 | from .SymbolsSegment import SymbolsSegment as SymbolsSegment 22 | from .Context import Context as Context 23 | from .FileSplitFormat import FileSplitFormat as FileSplitFormat 24 | from .FileSplitFormat import FileSplitEntry as FileSplitEntry 25 | from .ElementBase import ElementBase as ElementBase 26 | from .GpAccesses import GlobalOffsetTable as GlobalOffsetTable 27 | from .OrderedEnum import OrderedEnum as OrderedEnum 28 | from .Relocation import RelocType as RelocType 29 | from .Relocation import RelocationInfo as RelocationInfo 30 | from .Relocation import RelocationStaticReference as RelocationStaticReference 31 | -------------------------------------------------------------------------------- /spimdisasm/disasmdis/DisasmdisInternals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import argparse 9 | from typing import Generator 10 | import rabbitizer 11 | import sys 12 | 13 | from .. import common 14 | from .. import frontendCommon as fec 15 | 16 | from .. import __version__ 17 | 18 | PROGNAME = "disasmdis" 19 | 20 | 21 | def getToolDescription() -> str: 22 | return "CLI tool to disassemble multiples instructions passed as argument" 23 | 24 | def addOptionsToParser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: 25 | parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}") 26 | 27 | parser.add_argument("input", help="Hex words to be disassembled. Leading '0x' must be omitted", nargs='+') 28 | 29 | parser.add_argument("--endian", help="Set the endianness of input files. Defaults to 'big'", choices=["big", "little", "middle"], default="big") 30 | parser.add_argument("--instr-category", help="The instruction category to use when disassembling every passed instruction. Defaults to 'cpu'", choices=["cpu", "rsp", "r3000gte", "r4000allegrex", "r5900"]) 31 | parser.add_argument("--pseudos", help="Enables all pseudo-instructions supported by rabbitizer", action="store_true") 32 | 33 | return parser 34 | 35 | def getArgsParser() -> argparse.ArgumentParser: 36 | parser = argparse.ArgumentParser(description=getToolDescription(), prog=PROGNAME, formatter_class=common.Utils.PreserveWhiteSpaceWrapRawTextHelpFormatter) 37 | return addOptionsToParser(parser) 38 | 39 | 40 | def applyArgs(args: argparse.Namespace) -> None: 41 | common.GlobalConfig.ENDIAN = common.InputEndian(args.endian) 42 | if args.pseudos: 43 | rabbitizer.config.pseudos_enablePseudos = True 44 | else: 45 | rabbitizer.config.pseudos_enablePseudos = False 46 | 47 | def getWordFromStr(inputStr: str) -> int: 48 | arr = bytearray() 49 | for index in range(0, len(inputStr), 2): 50 | byteStr = inputStr[index:index+2] 51 | temp = 0 52 | for char in byteStr: 53 | temp *= 16 54 | temp += int(char, 16) 55 | arr.append(temp) 56 | while len(arr) % 4 != 0: 57 | arr.append(0) 58 | return common.Utils.bytesToWords(arr)[0] 59 | 60 | def wordGeneratorFromStrList(inputlist: list|None) -> Generator[int, None, None]: 61 | if inputlist is None: 62 | return 63 | 64 | wordStr = "" 65 | for inputStr in inputlist: 66 | for character in inputStr: 67 | if character not in "0123456789abcdefABCDEF": 68 | continue 69 | wordStr += character 70 | if len(wordStr) == 8: 71 | yield getWordFromStr(wordStr) 72 | wordStr = "" 73 | 74 | if len(wordStr) > 0: 75 | yield getWordFromStr(wordStr) 76 | 77 | def getWordListFromStdin() -> Generator[int, None, None]: 78 | if sys.stdin.isatty(): 79 | return 80 | 81 | lines = "" 82 | try: 83 | for line in sys.stdin: 84 | lines += line 85 | except KeyboardInterrupt: 86 | pass 87 | for word in wordGeneratorFromStrList(lines.split(" ")): 88 | yield word 89 | 90 | 91 | def processArguments(args: argparse.Namespace) -> int: 92 | applyArgs(args) 93 | 94 | category = fec.FrontendUtilities.getInstrCategoryFromStr(args.instr_category) 95 | 96 | for word in getWordListFromStdin(): 97 | instr = rabbitizer.Instruction(word, category=category) 98 | print(instr.disassemble()) 99 | 100 | for word in wordGeneratorFromStrList(args.input): 101 | instr = rabbitizer.Instruction(word, category=category) 102 | print(instr.disassemble()) 103 | 104 | return 0 105 | 106 | def addSubparser(subparser: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: 107 | parser = subparser.add_parser("disasmdis", help=getToolDescription(), formatter_class=common.Utils.PreserveWhiteSpaceWrapRawTextHelpFormatter) 108 | 109 | addOptionsToParser(parser) 110 | 111 | parser.set_defaults(func=processArguments) 112 | 113 | 114 | def disasmdisMain() -> int: 115 | args = getArgsParser().parse_args() 116 | 117 | return processArguments(args) 118 | -------------------------------------------------------------------------------- /spimdisasm/disasmdis/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | 9 | from .DisasmdisInternals import getToolDescription as getToolDescription 10 | from .DisasmdisInternals import addOptionsToParser as addOptionsToParser 11 | from .DisasmdisInternals import getArgsParser as getArgsParser 12 | from .DisasmdisInternals import applyArgs as applyArgs 13 | from .DisasmdisInternals import wordGeneratorFromStrList as wordGeneratorFromStrList 14 | from .DisasmdisInternals import processArguments as processArguments 15 | from .DisasmdisInternals import addSubparser as addSubparser 16 | from .DisasmdisInternals import disasmdisMain as disasmdisMain 17 | -------------------------------------------------------------------------------- /spimdisasm/disasmdis/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from . import disasmdisMain 9 | 10 | 11 | if __name__ == "__main__": 12 | disasmdisMain() 13 | -------------------------------------------------------------------------------- /spimdisasm/elf32/Elf32Dyns.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import dataclasses 9 | import struct 10 | from typing import Generator 11 | 12 | from .. import common 13 | 14 | from .Elf32Constants import Elf32DynamicTable 15 | 16 | 17 | # a.k.a. Dyn () 18 | @dataclasses.dataclass 19 | class Elf32DynEntry: 20 | tag: int # int32_t # 0x00 21 | "Dynamic entry type" 22 | val: int # uint32_t # 0x04 23 | "Integer value" 24 | # 0x08 25 | 26 | @property 27 | def ptr(self) -> int: 28 | "Address value" 29 | # Elf32_Addr 30 | return self.val 31 | 32 | @staticmethod 33 | def fromBytearray(array_of_bytes: bytes, offset: int = 0) -> Elf32DynEntry: 34 | entryFormat = common.GlobalConfig.ENDIAN.toFormatString() + "II" 35 | unpacked = struct.unpack_from(entryFormat, array_of_bytes, offset) 36 | 37 | return Elf32DynEntry(*unpacked) 38 | 39 | @staticmethod 40 | def structSize() -> int: 41 | return 0x08 42 | 43 | 44 | class Elf32Dyns: 45 | def __init__(self, array_of_bytes: bytes, offset: int, rawSize: int) -> None: 46 | self.dyns: list[Elf32DynEntry] = list() 47 | self.offset: int = offset 48 | self.rawSize: int = rawSize 49 | 50 | self.pltGot: int | None = None 51 | self.localGotNo: int | None = None 52 | self.symTabNo: int | None = None 53 | self.gotSym: int | None = None 54 | 55 | for i in range(rawSize // Elf32DynEntry.structSize()): 56 | entry = Elf32DynEntry.fromBytearray(array_of_bytes, offset + i*Elf32DynEntry.structSize()) 57 | self.dyns.append(entry) 58 | 59 | if entry.tag == Elf32DynamicTable.PLTGOT.value: 60 | self.pltGot = entry.val 61 | elif entry.tag == Elf32DynamicTable.MIPS_LOCAL_GOTNO.value: 62 | self.localGotNo = entry.val 63 | elif entry.tag == Elf32DynamicTable.MIPS_SYMTABNO.value: 64 | self.symTabNo = entry.val 65 | elif entry.tag == Elf32DynamicTable.MIPS_GOTSYM.value: 66 | self.gotSym = entry.val 67 | elif entry.tag == Elf32DynamicTable.NULL.value: 68 | pass 69 | else: 70 | pass 71 | # print(f"Unknown dyn value: tag={entry.tag:08X} val={entry.val:08X}") 72 | 73 | def __getitem__(self, key: int) -> Elf32DynEntry: 74 | return self.dyns[key] 75 | 76 | def __iter__(self) -> Generator[Elf32DynEntry, None, None]: 77 | for entry in self.dyns: 78 | yield entry 79 | 80 | def getGpValue(self) -> int|None: 81 | if self.pltGot is None: 82 | return None 83 | return self.pltGot + 0x7FF0 84 | -------------------------------------------------------------------------------- /spimdisasm/elf32/Elf32GlobalOffsetTable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import dataclasses 9 | import struct 10 | 11 | from .. import common 12 | 13 | from .Elf32Constants import Elf32SectionHeaderNumber 14 | from .Elf32Dyns import Elf32Dyns 15 | from .Elf32Syms import Elf32Syms, Elf32SymEntry 16 | 17 | 18 | @dataclasses.dataclass 19 | class GotEntry: 20 | symEntry: Elf32SymEntry 21 | initial: int|None = None 22 | 23 | def getAddress(self) -> int: 24 | if self.initial is not None: 25 | return self.initial 26 | return self.symEntry.value 27 | 28 | 29 | class Elf32GlobalOffsetTable: 30 | def __init__(self, array_of_bytes: bytes, offset: int, rawSize: int) -> None: 31 | self.entries: list[int] = list() 32 | self.offset: int = offset 33 | self.rawSize: int = rawSize 34 | 35 | entryFormat = common.GlobalConfig.ENDIAN.toFormatString() + f"{rawSize//4}I" 36 | self.entries = list(struct.unpack_from(entryFormat, array_of_bytes, offset)) 37 | 38 | 39 | self.localsTable: list[int] = list() 40 | self.globalsTable: list[GotEntry] = list() 41 | 42 | def __getitem__(self, key: int) -> int: 43 | return self.entries[key] 44 | 45 | def __len__(self) -> int: 46 | return len(self.entries) 47 | 48 | 49 | def initTables(self, dynamic: Elf32Dyns, dynsym: Elf32Syms) -> None: 50 | if dynamic.gotSym is None or dynamic.localGotNo is None: 51 | return 52 | 53 | # for i in range(len(self.entries)): 54 | # print(i, f"{self.entries[i]:X}") 55 | 56 | for i in range(dynamic.localGotNo): 57 | self.localsTable.append(self.entries[i]) 58 | 59 | for i in range(dynamic.gotSym, len(dynsym)): 60 | symEntry = dynsym[i] 61 | gotEntry = GotEntry(symEntry) 62 | 63 | if symEntry.shndx == Elf32SectionHeaderNumber.UNDEF.value or symEntry.shndx == Elf32SectionHeaderNumber.COMMON.value: 64 | gotIndex = dynamic.localGotNo + (i - dynamic.gotSym) 65 | gotEntry.initial = self.entries[gotIndex] 66 | 67 | self.globalsTable.append(gotEntry) 68 | -------------------------------------------------------------------------------- /spimdisasm/elf32/Elf32Header.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import dataclasses 9 | import struct 10 | 11 | from .Elf32Constants import Elf32HeaderIdentifier 12 | 13 | 14 | @dataclasses.dataclass 15 | class Elf32Identifier: 16 | ident: list[int] # 16 bytes # 0x00 17 | # 0x10 18 | 19 | def checkMagic(self) -> bool: 20 | if self.ident[0] != 0x7F: 21 | return False 22 | if self.ident[1] != 0x45: # 'E' 23 | return False 24 | if self.ident[2] != 0x4C: # 'L' 25 | return False 26 | if self.ident[3] != 0x46: # 'F' 27 | return False 28 | return True 29 | 30 | def getFileClass(self) -> Elf32HeaderIdentifier.FileClass: 31 | return Elf32HeaderIdentifier.FileClass(self.ident[4]) 32 | 33 | def getDataEncoding(self) -> Elf32HeaderIdentifier.DataEncoding: 34 | return Elf32HeaderIdentifier.DataEncoding(self.ident[5]) 35 | 36 | def getVersion(self) -> int: 37 | return self.ident[6] 38 | 39 | def getOsAbi(self) -> Elf32HeaderIdentifier.OsAbi: 40 | return Elf32HeaderIdentifier.OsAbi(self.ident[7]) 41 | 42 | def getAbiVersion(self) -> int: 43 | return self.ident[8] 44 | 45 | # should always return 0 46 | def getPad(self) -> int: 47 | return self.ident[9] 48 | 49 | 50 | @staticmethod 51 | def fromBytearray(array_of_bytes: bytes, offset: int = 0) -> Elf32Identifier: 52 | identFormat = "16B" 53 | ident = list(struct.unpack_from(identFormat, array_of_bytes, 0 + offset)) 54 | 55 | identifier = Elf32Identifier(ident) 56 | 57 | if not identifier.checkMagic(): 58 | # TODO: consider making my own exceptions 59 | raise RuntimeError("Not an ELF file: wrong magic") 60 | 61 | if identifier.getFileClass() != Elf32HeaderIdentifier.FileClass.CLASS32: 62 | raise RuntimeError(f"Unsupported ELF file class: {identifier.getFileClass()} (expected CLASS32)") 63 | 64 | assert identifier.getPad() == 0 65 | 66 | return identifier 67 | 68 | @staticmethod 69 | def structSize() -> int: 70 | return 0x10 71 | 72 | 73 | # a.k.a. Ehdr (elf header) 74 | @dataclasses.dataclass 75 | class Elf32Header: 76 | ident: Elf32Identifier # 16 bytes # 0x00 77 | type: int # half # 0x10 78 | machine: int # half # 0x12 79 | version: int # word # 0x14 80 | entry: int # address # 0x18 81 | phoff: int # offset # 0x1C 82 | shoff: int # offset # 0x20 83 | flags: int # word # 0x24 84 | ehsize: int # half # 0x28 85 | phentsize: int # half # 0x2A 86 | phnum: int # half # 0x2C 87 | shentsize: int # half # 0x2E 88 | shnum: int # half # 0x30 89 | shstrndx: int # half # 0x32 90 | # 0x34 91 | 92 | @staticmethod 93 | def fromBytearray(array_of_bytes: bytes, offset: int = 0) -> Elf32Header: 94 | identifier = Elf32Identifier.fromBytearray(array_of_bytes, offset) 95 | 96 | dataEncoding = identifier.getDataEncoding() 97 | if dataEncoding == Elf32HeaderIdentifier.DataEncoding.DATA2MSB: 98 | endianFormat = ">" # big 99 | elif dataEncoding == Elf32HeaderIdentifier.DataEncoding.DATA2LSB: 100 | endianFormat = "<" # little 101 | else: 102 | raise RuntimeError(f"Unsupported ELF data encoding: {dataEncoding}") 103 | 104 | headerFormat = endianFormat + "HHIIIIIHHHHHH" 105 | unpacked = struct.unpack_from(headerFormat, array_of_bytes, Elf32Identifier.structSize() + offset) 106 | # print(unpacked) 107 | 108 | return Elf32Header(identifier, *unpacked) 109 | 110 | @staticmethod 111 | def structSize() -> int: 112 | return 0x34 113 | -------------------------------------------------------------------------------- /spimdisasm/elf32/Elf32RegInfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import dataclasses 9 | import struct 10 | 11 | from .. import common 12 | 13 | 14 | @dataclasses.dataclass 15 | class Elf32RegInfo: 16 | gprmask: int # uint32_t # 0x00 /* General registers used. */ 17 | cprmask: list[int] # uint32_t[4] # 0x04 /* Coprocessor registers used. */ 18 | gpValue: int # int32_t # 0x14 /* $gp register value. */ 19 | # 0x18 20 | 21 | @staticmethod 22 | def fromBytearray(array_of_bytes: bytes, offset: int = 0) -> Elf32RegInfo: 23 | gprFormat = common.GlobalConfig.ENDIAN.toFormatString() + "I" 24 | gpr = struct.unpack_from(gprFormat, array_of_bytes, 0 + offset)[0] 25 | # print(gpr) 26 | 27 | cprFormat = common.GlobalConfig.ENDIAN.toFormatString() + "4I" 28 | cpr = list(struct.unpack_from(cprFormat, array_of_bytes, 4 + offset)) 29 | # print(cpr) 30 | 31 | gpFormat = common.GlobalConfig.ENDIAN.toFormatString() + "i" 32 | gp = struct.unpack_from(gpFormat, array_of_bytes, 0x14 + offset)[0] 33 | # print(gp) 34 | 35 | return Elf32RegInfo(gpr, cpr, gp) 36 | -------------------------------------------------------------------------------- /spimdisasm/elf32/Elf32Rels.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import dataclasses 9 | import struct 10 | from typing import Generator 11 | 12 | from .. import common 13 | 14 | 15 | @dataclasses.dataclass 16 | class Elf32RelEntry: 17 | offset: int # address # 0x00 18 | info: int # word # 0x04 19 | # 0x08 20 | 21 | @property 22 | def rSym(self) -> int: 23 | return self.info >> 8 24 | 25 | @property 26 | def rType(self) -> int: 27 | return self.info & 0xFF 28 | 29 | @staticmethod 30 | def fromBytearray(array_of_bytes: bytes, offset: int = 0) -> Elf32RelEntry: 31 | entryFormat = common.GlobalConfig.ENDIAN.toFormatString() + "II" 32 | unpacked = struct.unpack_from(entryFormat, array_of_bytes, offset) 33 | 34 | return Elf32RelEntry(*unpacked) 35 | 36 | 37 | class Elf32Rels: 38 | def __init__(self, sectionName: str, array_of_bytes: bytes, offset: int, rawSize: int) -> None: 39 | self.sectionName = sectionName 40 | self.relocations: list[Elf32RelEntry] = list() 41 | self.offset: int = offset 42 | self.rawSize: int = rawSize 43 | 44 | for i in range(rawSize // 0x08): 45 | entry = Elf32RelEntry.fromBytearray(array_of_bytes, offset + i*0x08) 46 | self.relocations.append(entry) 47 | 48 | def __iter__(self) -> Generator[Elf32RelEntry, None, None]: 49 | for entry in self.relocations: 50 | yield entry 51 | -------------------------------------------------------------------------------- /spimdisasm/elf32/Elf32SectionHeaders.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import dataclasses 9 | import struct 10 | from typing import Generator 11 | 12 | from .. import common 13 | 14 | from .Elf32Constants import Elf32SectionHeaderNumber 15 | 16 | 17 | # a.k.a. Shdr (section header) 18 | @dataclasses.dataclass 19 | class Elf32SectionHeaderEntry: 20 | name: int # word # 0x00 21 | type: int # word # 0x04 22 | flags: int # word # 0x08 23 | addr: int # address # 0x0C 24 | offset: int # offset # 0x10 25 | size: int # word # 0x14 26 | link: int # word # 0x18 27 | info: int # word # 0x1C 28 | addralign: int # word # 0x20 29 | entsize: int # word # 0x24 30 | # 0x28 31 | 32 | @staticmethod 33 | def fromBytearray(array_of_bytes: bytes, offset: int = 0) -> Elf32SectionHeaderEntry: 34 | headerFormat = common.GlobalConfig.ENDIAN.toFormatString() + "10I" 35 | unpacked = struct.unpack_from(headerFormat, array_of_bytes, offset) 36 | 37 | return Elf32SectionHeaderEntry(*unpacked) 38 | 39 | 40 | class Elf32SectionHeaders: 41 | def __init__(self, array_of_bytes: bytes, shoff: int, shnum: int) -> None: 42 | self.sections: list[Elf32SectionHeaderEntry] = list() 43 | self.shoff: int = shoff 44 | self.shnum: int = shnum 45 | 46 | self.mipsText: Elf32SectionHeaderEntry | None = None 47 | self.mipsData: Elf32SectionHeaderEntry | None = None 48 | 49 | for i in range(shnum): 50 | sectionHeaderEntry = Elf32SectionHeaderEntry.fromBytearray(array_of_bytes, shoff + i * 0x28) 51 | self.sections.append(sectionHeaderEntry) 52 | # print(sectionHeaderEntry) 53 | 54 | def __getitem__(self, key: int) -> Elf32SectionHeaderEntry | None: 55 | if key == Elf32SectionHeaderNumber.UNDEF.value: 56 | return None 57 | if key == Elf32SectionHeaderNumber.COMMON.value: 58 | common.Utils.eprint("Warning: Elf32SectionHeaderNumber.COMMON not implemented\n") 59 | return None 60 | if key == Elf32SectionHeaderNumber.MIPS_ACOMMON.value: 61 | common.Utils.eprint("Warning: Elf32SectionHeaderNumber.MIPS_ACOMMON not implemented\n") 62 | return None 63 | if key == Elf32SectionHeaderNumber.MIPS_TEXT.value: 64 | return self.mipsText 65 | if key == Elf32SectionHeaderNumber.MIPS_DATA.value: 66 | return self.mipsData 67 | if key == Elf32SectionHeaderNumber.MIPS_SCOMMON.value: 68 | common.Utils.eprint("Warning: Elf32SectionHeaderNumber.MIPS_SCOMMON not implemented\n") 69 | return None 70 | if key == Elf32SectionHeaderNumber.MIPS_SUNDEFINED.value: 71 | common.Utils.eprint("Warning: Elf32SectionHeaderNumber.MIPS_SUNDEFINED not implemented\n") 72 | return None 73 | if key > len(self.sections): 74 | return None 75 | return self.sections[key] 76 | 77 | def __iter__(self) -> Generator[Elf32SectionHeaderEntry, None, None]: 78 | for entry in self.sections: 79 | yield entry 80 | 81 | def __len__(self) -> int: 82 | return len(self.sections) 83 | -------------------------------------------------------------------------------- /spimdisasm/elf32/Elf32StringTable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from typing import Generator 9 | 10 | 11 | # a.k.a. strtab (string table) 12 | class Elf32StringTable: 13 | def __init__(self, array_of_bytes: bytes, offset: int, rawsize: int) -> None: 14 | self.strings: bytes = array_of_bytes[offset:offset+rawsize] 15 | self.offset: int = offset 16 | self.rawsize: int = rawsize 17 | 18 | def __getitem__(self, key: int) -> str: 19 | buffer = bytearray() 20 | 21 | i = key 22 | while self.strings[i] != 0: 23 | buffer.append(self.strings[i]) 24 | i += 1 25 | 26 | return buffer.decode() 27 | 28 | def __iter__(self) -> Generator[str, None, None]: 29 | i = 0 30 | while i < self.rawsize: 31 | string = self[i] 32 | yield string 33 | i += len(string.encode()) + 1 34 | -------------------------------------------------------------------------------- /spimdisasm/elf32/Elf32Syms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import dataclasses 9 | import struct 10 | from typing import Generator 11 | 12 | from .. import common 13 | 14 | 15 | # a.k.a. Sym (symbol) 16 | @dataclasses.dataclass 17 | class Elf32SymEntry: 18 | name: int # word # 0x00 19 | value: int # address # 0x04 20 | size: int # word # 0x08 21 | info: int # uchar # 0x0C 22 | other: int # uchar # 0x0D 23 | shndx: int # section # 0x0E 24 | # 0x10 25 | 26 | @property 27 | def stBind(self) -> int: 28 | return self.info >> 4 29 | 30 | @property 31 | def stType(self) -> int: 32 | return self.info & 0xF 33 | 34 | @staticmethod 35 | def fromBytearray(array_of_bytes: bytes, offset: int = 0) -> Elf32SymEntry: 36 | entryFormat = common.GlobalConfig.ENDIAN.toFormatString() + "IIIBBH" 37 | unpacked = struct.unpack_from(entryFormat, array_of_bytes, offset) 38 | 39 | return Elf32SymEntry(*unpacked) 40 | 41 | @staticmethod 42 | def structSize() -> int: 43 | return 0x10 44 | 45 | 46 | class Elf32Syms: 47 | def __init__(self, array_of_bytes: bytes, offset: int, rawSize: int) -> None: 48 | self.symbols: list[Elf32SymEntry] = list() 49 | self.offset: int = offset 50 | self.rawSize: int = rawSize 51 | 52 | for i in range(rawSize // Elf32SymEntry.structSize()): 53 | entry = Elf32SymEntry.fromBytearray(array_of_bytes, offset + i*Elf32SymEntry.structSize()) 54 | self.symbols.append(entry) 55 | 56 | def __getitem__(self, key: int) -> Elf32SymEntry: 57 | return self.symbols[key] 58 | 59 | def __iter__(self) -> Generator[Elf32SymEntry, None, None]: 60 | for entry in self.symbols: 61 | yield entry 62 | 63 | def __len__(self) -> int: 64 | return len(self.symbols) 65 | -------------------------------------------------------------------------------- /spimdisasm/elf32/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from .Elf32Constants import Elf32HeaderIdentifier as Elf32HeaderIdentifier 9 | from .Elf32Constants import Elf32ObjectFileType as Elf32ObjectFileType 10 | from .Elf32Constants import Elf32HeaderFlag as Elf32HeaderFlag 11 | from .Elf32Constants import Elf32SectionHeaderType as Elf32SectionHeaderType 12 | from .Elf32Constants import Elf32SectionHeaderFlag as Elf32SectionHeaderFlag 13 | from .Elf32Constants import Elf32SymbolTableType as Elf32SymbolTableType 14 | from .Elf32Constants import Elf32SymbolTableBinding as Elf32SymbolTableBinding 15 | from .Elf32Constants import Elf32SymbolVisibility as Elf32SymbolVisibility 16 | from .Elf32Constants import Elf32SectionHeaderNumber as Elf32SectionHeaderNumber 17 | from .Elf32Constants import Elf32DynamicTable as Elf32DynamicTable 18 | from .Elf32Dyns import Elf32Dyns as Elf32Dyns 19 | from .Elf32Dyns import Elf32DynEntry as Elf32DynEntry 20 | from .Elf32GlobalOffsetTable import Elf32GlobalOffsetTable as Elf32GlobalOffsetTable 21 | from .Elf32Header import Elf32Header as Elf32Header 22 | from .Elf32RegInfo import Elf32RegInfo as Elf32RegInfo 23 | from .Elf32SectionHeaders import Elf32SectionHeaders as Elf32SectionHeaders 24 | from .Elf32SectionHeaders import Elf32SectionHeaderEntry as Elf32SectionHeaderEntry 25 | from .Elf32StringTable import Elf32StringTable as Elf32StringTable 26 | from .Elf32Syms import Elf32Syms as Elf32Syms 27 | from .Elf32Syms import Elf32SymEntry as Elf32SymEntry 28 | from .Elf32Rels import Elf32Rels as Elf32Rels 29 | from .Elf32Rels import Elf32RelEntry as Elf32RelEntry 30 | 31 | from .Elf32File import Elf32File as Elf32File 32 | 33 | # To avoid breaking backwards compatibility 34 | from ..common.Relocation import RelocType as Elf32Relocs 35 | -------------------------------------------------------------------------------- /spimdisasm/elfObjDisasm/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | 9 | from .ElfObjDisasmInternals import getToolDescription as getToolDescription 10 | from .ElfObjDisasmInternals import addOptionsToParser as addOptionsToParser 11 | from .ElfObjDisasmInternals import getArgsParser as getArgsParser 12 | from .ElfObjDisasmInternals import applyArgs as applyArgs 13 | from .ElfObjDisasmInternals import applyGlobalConfigurations as applyGlobalConfigurations 14 | from .ElfObjDisasmInternals import getOutputPath as getOutputPath 15 | from .ElfObjDisasmInternals import getProcessedSections as getProcessedSections 16 | from .ElfObjDisasmInternals import changeGlobalSegmentRanges as changeGlobalSegmentRanges 17 | from .ElfObjDisasmInternals import insertSymtabIntoContext as insertSymtabIntoContext 18 | from .ElfObjDisasmInternals import insertDynsymIntoContext as insertDynsymIntoContext 19 | from .ElfObjDisasmInternals import injectAllElfSymbols as injectAllElfSymbols 20 | from .ElfObjDisasmInternals import processGlobalOffsetTable as processGlobalOffsetTable 21 | from .ElfObjDisasmInternals import processArguments as processArguments 22 | from .ElfObjDisasmInternals import addSubparser as addSubparser 23 | from .ElfObjDisasmInternals import elfObjDisasmMain as elfObjDisasmMain 24 | -------------------------------------------------------------------------------- /spimdisasm/elfObjDisasm/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from . import elfObjDisasmMain 9 | 10 | 11 | if __name__ == "__main__": 12 | elfObjDisasmMain() 13 | -------------------------------------------------------------------------------- /spimdisasm/frontendCommon/FrontendUtilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import argparse 9 | from pathlib import Path 10 | from typing import Callable 11 | 12 | import spimdisasm 13 | import rabbitizer 14 | 15 | from .. import common 16 | from .. import mips 17 | 18 | from .. import __version__ 19 | 20 | 21 | ProgressCallbackType = Callable[[int, str, int], None] 22 | 23 | 24 | _sLenLastLine = 80 25 | 26 | 27 | def getSplittedSections(context: common.Context, splits: common.FileSplitFormat, array_of_bytes: bytes, inputPath: Path, textOutput: Path, dataOutput: Path) -> tuple[dict[common.FileSectionType, list[mips.sections.SectionBase]], dict[common.FileSectionType, list[Path]]]: 28 | processedFiles: dict[common.FileSectionType, list[mips.sections.SectionBase]] = { 29 | common.FileSectionType.Text: [], 30 | common.FileSectionType.Data: [], 31 | common.FileSectionType.Rodata: [], 32 | common.FileSectionType.Bss: [], 33 | } 34 | processedFilesOutputPaths: dict[common.FileSectionType, list[Path]] = {k: [] for k in processedFiles} 35 | 36 | for row in splits: 37 | if row.section == common.FileSectionType.Text: 38 | outputPath = textOutput 39 | elif row.section == common.FileSectionType.Data: 40 | outputPath = dataOutput 41 | elif row.section == common.FileSectionType.Rodata: 42 | outputPath = dataOutput 43 | elif row.section == common.FileSectionType.Bss: 44 | outputPath = dataOutput 45 | elif row.section == common.FileSectionType.Reloc: 46 | outputPath = dataOutput 47 | elif row.section == common.FileSectionType.Dummy: 48 | # Ignore dummy sections 49 | continue 50 | else: 51 | common.Utils.eprint("Error! Section not set!") 52 | exit(1) 53 | 54 | outputFilePath = outputPath 55 | if str(outputPath) != "-": 56 | fileName = row.fileName 57 | if row.fileName == "": 58 | fileName = f"{inputPath.stem}_{row.vram:08X}" 59 | 60 | outputFilePath = outputPath / fileName 61 | 62 | common.Utils.printVerbose(f"Reading '{row.fileName}'") 63 | f = mips.FilesHandlers.createSectionFromSplitEntry(row, array_of_bytes, context) 64 | f.setCommentOffset(row.offset) 65 | processedFiles[row.section].append(f) 66 | processedFilesOutputPaths[row.section].append(outputFilePath) 67 | 68 | return processedFiles, processedFilesOutputPaths 69 | 70 | 71 | gInstrCategoriesNameMap = { 72 | "cpu": rabbitizer.InstrCategory.CPU, 73 | "rsp": rabbitizer.InstrCategory.RSP, 74 | "r3000gte": rabbitizer.InstrCategory.R3000GTE, 75 | "r4000allegrex": rabbitizer.InstrCategory.R4000ALLEGREX, 76 | "r5900": rabbitizer.InstrCategory.R5900, 77 | } 78 | 79 | def getInstrCategoryFromStr(category: str|None) -> rabbitizer.Enum: 80 | # TODO: consider moving this logic to rabbitizer 81 | if category is None: 82 | category = "cpu" 83 | return gInstrCategoriesNameMap.get(category.lower(), rabbitizer.InstrCategory.CPU) 84 | 85 | 86 | def configureProcessedFiles(processedFiles: dict[common.FileSectionType, list[mips.sections.SectionBase]], category: str|None) -> None: 87 | instrCat = getInstrCategoryFromStr(category) 88 | 89 | for textFile in processedFiles.get(common.FileSectionType.Text, []): 90 | assert isinstance(textFile, mips.sections.SectionText) 91 | 92 | textFile.instrCat = instrCat 93 | 94 | 95 | def analyzeProcessedFiles(processedFiles: dict[common.FileSectionType, list[mips.sections.SectionBase]], processedFilesOutputPaths: dict[common.FileSectionType, list[Path]], processedFilesCount: int, progressCallback: ProgressCallbackType|None=None) -> None: 96 | i = 0 97 | for sectionType, filesInSection in sorted(processedFiles.items()): 98 | pathLists = processedFilesOutputPaths[sectionType] 99 | for fileIndex, f in enumerate(filesInSection): 100 | if progressCallback is not None: 101 | filePath = pathLists[fileIndex] 102 | progressCallback(i, str(filePath), processedFilesCount) 103 | f.analyze() 104 | f.printAnalyzisResults() 105 | 106 | i += 1 107 | return 108 | 109 | def progressCallback_analyzeProcessedFiles(i: int, filePath: str, processedFilesCount: int) -> None: 110 | global _sLenLastLine 111 | 112 | common.Utils.printQuietless(_sLenLastLine*" " + "\r", end="") 113 | progressStr = f"Analyzing: {i/processedFilesCount:%}. File: {filePath}\r" 114 | _sLenLastLine = max(len(progressStr), _sLenLastLine) 115 | common.Utils.printQuietless(progressStr, end="", flush=True) 116 | common.Utils.printVerbose("") 117 | 118 | 119 | def nukePointers(processedFiles: dict[common.FileSectionType, list[mips.sections.SectionBase]], processedFilesOutputPaths: dict[common.FileSectionType, list[Path]], processedFilesCount: int, progressCallback: ProgressCallbackType|None=None) -> None: 120 | i = 0 121 | for sectionType, filesInSection in processedFiles.items(): 122 | pathLists = processedFilesOutputPaths[sectionType] 123 | for fileIndex, f in enumerate(filesInSection): 124 | if progressCallback is not None: 125 | filePath = pathLists[fileIndex] 126 | progressCallback(i, str(filePath), processedFilesCount) 127 | f.removePointers() 128 | i += 1 129 | return 130 | 131 | def progressCallback_nukePointers(i: int, filePath: str, processedFilesCount: int) -> None: 132 | global _sLenLastLine 133 | 134 | common.Utils.printVerbose(f"Nuking pointers of {filePath}") 135 | common.Utils.printQuietless(_sLenLastLine*" " + "\r", end="") 136 | progressStr = f" Nuking pointers: {i/processedFilesCount:%}. File: {filePath}\r" 137 | _sLenLastLine = max(len(progressStr), _sLenLastLine) 138 | common.Utils.printQuietless(progressStr, end="") 139 | 140 | 141 | def writeProcessedFiles(processedFiles: dict[common.FileSectionType, list[mips.sections.SectionBase]], processedFilesOutputPaths: dict[common.FileSectionType, list[Path]], processedFilesCount: int, progressCallback: ProgressCallbackType|None=None) -> None: 142 | common.Utils.printVerbose("Writing files...") 143 | i = 0 144 | for section, filesInSection in processedFiles.items(): 145 | pathLists = processedFilesOutputPaths[section] 146 | for fileIndex, f in enumerate(filesInSection): 147 | filePath = pathLists[fileIndex] 148 | if progressCallback is not None: 149 | progressCallback(i, str(filePath), processedFilesCount) 150 | 151 | common.Utils.printVerbose(f"Writing {filePath}") 152 | mips.FilesHandlers.writeSection(filePath, f) 153 | i += 1 154 | return 155 | 156 | def progressCallback_writeProcessedFiles(i: int, filePath: str, processedFilesCount: int) -> None: 157 | global _sLenLastLine 158 | 159 | common.Utils.printVerbose(f"Writing {filePath}") 160 | common.Utils.printQuietless(_sLenLastLine*" " + "\r", end="") 161 | progressStr = f"Writing: {i/processedFilesCount:%}. File: {filePath}\r" 162 | _sLenLastLine = max(len(progressStr), _sLenLastLine) 163 | common.Utils.printQuietless(progressStr, end="") 164 | 165 | if str(filePath) == "-": 166 | common.Utils.printQuietless() 167 | 168 | 169 | def migrateFunctions(processedFiles: dict[common.FileSectionType, list[mips.sections.SectionBase]], functionMigrationPath: Path, progressCallback: ProgressCallbackType|None=None) -> None: 170 | funcTotal = sum(len(x.symbolList) for x in processedFiles.get(common.FileSectionType.Text, [])) 171 | rodataFileList = processedFiles.get(common.FileSectionType.Rodata, []) 172 | 173 | remainingRodataSyms: list[mips.symbols.SymbolBase] = [] 174 | rodataSectionNamesMapping: dict[int, str] = dict() 175 | for x in rodataFileList: 176 | remainingRodataSyms.extend(x.symbolList) 177 | for y in x.symbolList: 178 | rodataSectionNamesMapping[y.vram] = x.getName() 179 | 180 | i = 0 181 | for textFile in processedFiles.get(common.FileSectionType.Text, []): 182 | filePath = functionMigrationPath / textFile.getName() 183 | filePath.mkdir(parents=True, exist_ok=True) 184 | for func in textFile.symbolList: 185 | if progressCallback is not None: 186 | progressCallback(i, func.getName(), funcTotal) 187 | 188 | assert isinstance(func, mips.symbols.SymbolFunction) 189 | entry = mips.FunctionRodataEntry.getEntryForFuncFromPossibleRodataSections(func, rodataFileList) 190 | 191 | for sym in entry.iterRodataSyms(): 192 | if sym in remainingRodataSyms: 193 | remainingRodataSyms.remove(sym) 194 | 195 | funcPath = filePath / (func.getName()+ ".s") 196 | common.Utils.printVerbose(f"Writing function {funcPath}") 197 | with funcPath.open("w") as f: 198 | entry.writeToFile(f, writeFunction=True) 199 | 200 | i += 1 201 | 202 | for rodataSym in remainingRodataSyms: 203 | rodataPath = functionMigrationPath / rodataSectionNamesMapping[rodataSym.vram] 204 | rodataPath.mkdir(parents=True, exist_ok=True) 205 | rodataSymbolPath = rodataPath / f"{rodataSym.getName()}.s" 206 | common.Utils.printVerbose(f"Writing unmigrated rodata {rodataSymbolPath}") 207 | with rodataSymbolPath.open("w") as f: 208 | f.write(".section .rodata" + common.GlobalConfig.LINE_ENDS) 209 | f.write(rodataSym.disassemble(migrate=True)) 210 | 211 | 212 | def progressCallback_migrateFunctions(i: int, funcName: str, funcTotal: int) -> None: 213 | global _sLenLastLine 214 | 215 | common.Utils.printVerbose(f"Spliting {funcName}", end="") 216 | common.Utils.printQuietless(_sLenLastLine*" " + "\r", end="") 217 | common.Utils.printVerbose() 218 | progressStr = f" Writing: {i/funcTotal:%}. Function: {funcName}\r" 219 | _sLenLastLine = max(len(progressStr), _sLenLastLine) 220 | common.Utils.printQuietless(progressStr, end="") 221 | 222 | 223 | def writeFunctionInfoCsv(processedFiles: dict[common.FileSectionType, list[mips.sections.SectionBase]], csvPath: Path) -> None: 224 | csvPath.parent.mkdir(parents=True, exist_ok=True) 225 | 226 | with csvPath.open("w") as f: 227 | f.write("vrom,address,name,file,length,hash of top bits of words,functions called by this function,non-jal function calls,referenced functions\n") 228 | for textFile in processedFiles.get(common.FileSectionType.Text, []): 229 | for func in textFile.symbolList: 230 | assert isinstance(func, mips.symbols.SymbolFunction) 231 | f.write(f"0x{func.vromStart:06X},0x{func.vram:08X},{func.getName()},{textFile.getName()},0x{func.sizew*4:X},") 232 | 233 | bitswordlist = [] 234 | for instr in func.instructions: 235 | topbits = instr.getRaw() & 0xFC000000 236 | 237 | bitswordlist.append(topbits) 238 | bitbytelist = common.Utils.endianessWordsToBytes(common.Utils.InputEndian.BIG, bitswordlist) 239 | 240 | f.write(common.Utils.getStrHash(bitbytelist)) 241 | f.write(",") 242 | 243 | calledFuncs = [] 244 | for instrOffset, targetVram in func.instrAnalyzer.funcCallInstrOffsets.items(): 245 | funcSym = func.getSymbol(targetVram, tryPlusOffset=False) 246 | if funcSym is None: 247 | continue 248 | 249 | calledFuncs.append(funcSym.getName()) 250 | f.write("[" + ";".join(calledFuncs) + "]") 251 | f.write(",") 252 | 253 | nonJalCalls = [] 254 | for loOffset, targetVram in func.instrAnalyzer.indirectFunctionCallOffsets.items(): 255 | funcSym = func.getSymbol(targetVram, tryPlusOffset=False) 256 | if funcSym is None: 257 | continue 258 | 259 | nonJalCalls.append(funcSym.getName()) 260 | f.write("[" + ";".join(nonJalCalls) + "]") 261 | f.write(",") 262 | 263 | referencedFunctions = [] 264 | for loOffset, symVram in func.instrAnalyzer.symbolLoInstrOffset.items(): 265 | funcSym = func.getSymbol(symVram, tryPlusOffset=False) 266 | if funcSym is None: 267 | continue 268 | 269 | if funcSym.getTypeSpecial() != common.SymbolSpecialType.function: 270 | continue 271 | 272 | referencedFunctions.append(funcSym.getName()) 273 | f.write("[" + ";".join(referencedFunctions) + "]") 274 | 275 | f.write("\n") 276 | 277 | # For adding new lines at the end of each file 278 | # f.write("\n") 279 | 280 | 281 | def cliMain() -> int: 282 | parser = argparse.ArgumentParser(description="Interface to call any of the spimdisasm's CLI utilities", prog="spimdisasm") 283 | 284 | parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}") 285 | 286 | subparsers = parser.add_subparsers(description="action", help="The CLI utility to run", required=True) 287 | 288 | spimdisasm.disasmdis.addSubparser(subparsers) 289 | spimdisasm.singleFileDisasm.addSubparser(subparsers) 290 | spimdisasm.elfObjDisasm.addSubparser(subparsers) 291 | spimdisasm.rspDisasm.addSubparser(subparsers) 292 | 293 | args = parser.parse_args() 294 | return int(args.func(args)) 295 | -------------------------------------------------------------------------------- /spimdisasm/frontendCommon/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | 9 | from . import FrontendUtilities as FrontendUtilities 10 | -------------------------------------------------------------------------------- /spimdisasm/mips/FilesHandlers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from typing import TextIO 9 | from pathlib import Path 10 | 11 | import rabbitizer 12 | 13 | from .. import common 14 | 15 | from . import sections 16 | from . import symbols 17 | from . import FunctionRodataEntry 18 | 19 | 20 | def createSectionFromSplitEntry(splitEntry: common.FileSplitEntry, array_of_bytes: bytes, context: common.Context, vromStart: int = 0) -> sections.SectionBase: 21 | offsetStart = splitEntry.offset 22 | offsetEnd = splitEntry.nextOffset 23 | 24 | if offsetEnd == 0xFFFFFF: 25 | offsetEnd = len(array_of_bytes) 26 | 27 | if offsetStart >= 0 and offsetEnd >= 0: 28 | common.Utils.printVerbose(f"Parsing offset range [{offsetStart:02X}, {offsetEnd:02X}]") 29 | elif offsetEnd >= 0: 30 | common.Utils.printVerbose(f"Parsing until offset 0x{offsetEnd:02X}") 31 | elif offsetStart >= 0: 32 | common.Utils.printVerbose(f"Parsing since offset 0x{offsetStart:02X}") 33 | 34 | sectionStart = vromStart + offsetStart 35 | sectionEnd = vromStart + offsetEnd 36 | 37 | common.Utils.printVerbose(f"Using VRAM {splitEntry.vram:08X}") 38 | vram = splitEntry.vram 39 | 40 | f: sections.SectionBase 41 | if splitEntry.section == common.FileSectionType.Text: 42 | f = sections.SectionText(context, sectionStart, sectionEnd, vram, splitEntry.fileName, array_of_bytes, 0, None) 43 | if splitEntry.isRsp: 44 | f.instrCat = rabbitizer.InstrCategory.RSP 45 | elif splitEntry.section == common.FileSectionType.Data: 46 | f = sections.SectionData(context, sectionStart, sectionEnd, vram, splitEntry.fileName, array_of_bytes, 0, None) 47 | elif splitEntry.section == common.FileSectionType.Rodata: 48 | f = sections.SectionRodata(context, sectionStart, sectionEnd, vram, splitEntry.fileName, array_of_bytes, 0, None) 49 | elif splitEntry.section == common.FileSectionType.Bss: 50 | bssVramEnd = splitEntry.vram + offsetEnd - offsetStart 51 | f = sections.SectionBss(context, sectionStart, sectionEnd, splitEntry.vram, bssVramEnd, splitEntry.fileName, 0, None) 52 | elif splitEntry.section == common.FileSectionType.Reloc: 53 | f = sections.SectionRelocZ64(context, sectionStart, sectionEnd, vram, splitEntry.fileName, array_of_bytes, 0, None) 54 | else: 55 | common.Utils.eprint("Error! Section not set!") 56 | exit(-1) 57 | 58 | f.isHandwritten = splitEntry.isHandwritten 59 | 60 | return f 61 | 62 | def writeSection(path: Path, fileSection: sections.SectionBase) -> Path: 63 | path.parent.mkdir(parents=True, exist_ok=True) 64 | fileSection.saveToFile(str(path)) 65 | return path 66 | 67 | 68 | #! @deprecated: Use the static methods from FunctionRodataEntry instead 69 | def getRdataAndLateRodataForFunctionFromSection(func: symbols.SymbolFunction, rodataSection: sections.SectionRodata) -> tuple[list[symbols.SymbolBase], list[symbols.SymbolBase], int]: 70 | entry = FunctionRodataEntry.getEntryForFuncFromSection(func, rodataSection) 71 | 72 | lateRodataSize = 0 73 | for rodataSym in entry.lateRodataSyms: 74 | lateRodataSize += rodataSym.sizew 75 | 76 | return entry.rodataSyms, entry.lateRodataSyms, lateRodataSize 77 | 78 | #! @deprecated: Use the static methods from FunctionRodataEntry instead 79 | def getRdataAndLateRodataForFunction(func: symbols.SymbolFunction, rodataFileList: list[sections.SectionBase]) -> tuple[list[symbols.SymbolBase], list[symbols.SymbolBase], int]: 80 | entry = FunctionRodataEntry.getEntryForFuncFromPossibleRodataSections(func, rodataFileList) 81 | 82 | lateRodataSize = 0 83 | for rodataSym in entry.lateRodataSyms: 84 | lateRodataSize += rodataSym.sizew 85 | 86 | return entry.rodataSyms, entry.lateRodataSyms, lateRodataSize 87 | 88 | #! @deprecated: Use the static methods from FunctionRodataEntry instead 89 | def writeFunctionRodataToFile(f: TextIO, func: symbols.SymbolFunction, rdataList: list[symbols.SymbolBase], lateRodataList: list[symbols.SymbolBase], lateRodataSize: int=0) -> None: 90 | entry = FunctionRodataEntry(func, rdataList, lateRodataList) 91 | entry.writeToFile(f, writeFunction=False) 92 | 93 | #! @deprecated: Use the static methods from FunctionRodataEntry instead 94 | def writeSplitedFunction(path: Path, func: symbols.SymbolFunction, rodataFileList: list[sections.SectionBase]) -> None: 95 | path.mkdir(parents=True, exist_ok=True) 96 | entry = FunctionRodataEntry.getEntryForFuncFromPossibleRodataSections(func, rodataFileList) 97 | 98 | funcPath = path / (func.getName()+ ".s") 99 | with funcPath.open("w") as f: 100 | entry.writeToFile(f, writeFunction=True) 101 | 102 | def writeOtherRodata(path: Path, rodataFileList: list[sections.SectionBase]) -> None: 103 | for rodataSection in rodataFileList: 104 | assert isinstance(rodataSection, sections.SectionRodata) 105 | 106 | rodataPath = path / rodataSection.name 107 | rodataPath.mkdir(parents=True, exist_ok=True) 108 | 109 | for rodataSym in rodataSection.symbolList: 110 | if rodataSym.shouldMigrate(): 111 | continue 112 | 113 | rodataSymbolPath = rodataPath / (rodataSym.getName() + ".s") 114 | common.Utils.printVerbose(f"Writing unmigrated rodata {rodataSymbolPath}") 115 | 116 | with rodataSymbolPath.open("w") as f: 117 | f.write(".section .rodata" + common.GlobalConfig.LINE_ENDS) 118 | f.write(rodataSym.disassemble(migrate=True)) 119 | 120 | 121 | def writeMigratedFunctionsList(processedSegments: dict[common.FileSectionType, list[sections.SectionBase]], functionMigrationPath: Path, name: str) -> None: 122 | funcAndRodataOrderPath = functionMigrationPath / f"{name}_migrated_functions.txt" 123 | 124 | rodataSymbols: list[tuple[symbols.SymbolBase, symbols.SymbolFunction|None]] = [] 125 | for section in processedSegments.get(common.FileSectionType.Rodata, []): 126 | for sym in section.symbolList: 127 | rodataSymbols.append((sym, None)) 128 | rodataSymbolsVrams = {sym.vram for sym, _ in rodataSymbols} 129 | 130 | funcs: list[symbols.SymbolFunction] = [] 131 | for section in processedSegments.get(common.FileSectionType.Text, []): 132 | for func in section.symbolList: 133 | assert isinstance(func, symbols.SymbolFunction) 134 | funcs.append(func) 135 | 136 | referencedRodata = rodataSymbolsVrams & func.instrAnalyzer.referencedVrams 137 | for i in range(len(rodataSymbols)): 138 | if len(referencedRodata) == 0: 139 | break 140 | 141 | rodataSym, funcReferencingThisSym = rodataSymbols[i] 142 | 143 | if rodataSym.vram not in referencedRodata: 144 | continue 145 | 146 | referencedRodata.remove(rodataSym.vram) 147 | 148 | if funcReferencingThisSym is not None: 149 | # This rodata sym already has a corresponding function associated 150 | continue 151 | 152 | rodataSymbols[i] = (rodataSym, func) 153 | 154 | resultingList: list[symbols.SymbolBase] = [] 155 | alreadyAddedFuncs: set[symbols.SymbolFunction] = set() 156 | 157 | lastFunc = None 158 | for rodataSym, funcReferencingThisSym in rodataSymbols: 159 | if funcReferencingThisSym is None: 160 | resultingList.append(rodataSym) 161 | elif funcReferencingThisSym not in alreadyAddedFuncs: 162 | alreadyAddedFuncs.add(funcReferencingThisSym) 163 | lastFunc = funcReferencingThisSym 164 | 165 | for func in funcs: 166 | if func.vram >= funcReferencingThisSym.vram: 167 | break 168 | if func in alreadyAddedFuncs: 169 | continue 170 | 171 | alreadyAddedFuncs.add(func) 172 | resultingList.append(func) 173 | resultingList.append(funcReferencingThisSym) 174 | 175 | if lastFunc is None: 176 | for func in funcs: 177 | resultingList.append(func) 178 | else: 179 | for func in funcs: 180 | if func.vram <= lastFunc.vram: 181 | continue 182 | resultingList.append(func) 183 | 184 | with funcAndRodataOrderPath.open("w") as f: 185 | for sym in resultingList: 186 | f.write(sym.getName() + "\n") 187 | -------------------------------------------------------------------------------- /spimdisasm/mips/InstructionConfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import argparse 9 | import rabbitizer 10 | 11 | from .. import common 12 | from ..common import Utils 13 | 14 | 15 | class InstructionConfig: 16 | @staticmethod 17 | def addParametersToArgParse(parser: argparse.ArgumentParser) -> None: 18 | registerNames = parser.add_argument_group("MIPS register names options") 19 | 20 | registerNames.add_argument("--named-registers", help=f"(Dis)allows named registers for every instruction. This flag takes precedence over similar flags in this category. Defaults to {rabbitizer.config.regNames_namedRegisters}", action=Utils.BooleanOptionalAction) 21 | 22 | abi_choices = ["numeric", "32", "o32", "n32", "n64"] 23 | registerNames.add_argument("--Mgpr-names", help=f"Use GPR names according to the specified ABI. Defaults to {rabbitizer.config.regNames_gprAbiNames.name.lower()}", choices=abi_choices) 24 | registerNames.add_argument("--Mfpr-names", help=f"Use FPR names according to the specified ABI. Defaults to {rabbitizer.config.regNames_fprAbiNames.name.lower()}", choices=abi_choices) 25 | registerNames.add_argument("--Mreg-names", help=f"Use GPR and FPR names according to the specified ABI. This flag takes precedence over --Mgpr-names and --Mfpr-names", choices=abi_choices) 26 | 27 | registerNames.add_argument("--use-fpccsr", help=f"Toggles using the FpcCsr alias for float register $31 when using the numeric ABI. Defaults to {rabbitizer.config.regNames_userFpcCsr}", action=Utils.BooleanOptionalAction) 28 | 29 | registerNames.add_argument("--cop0-named-registers", help=f"Toggles using the built-in names for registers of the VR4300's Coprocessor 0. Defaults to {rabbitizer.config.regNames_vr4300Cop0NamedRegisters}", action=Utils.BooleanOptionalAction) 30 | registerNames.add_argument("--rsp-cop0-named-registers", help=f"Toggles using the built-in names for registers of the RSP's Coprocessor 0. Defaults to {rabbitizer.config.regNames_vr4300RspCop0NamedRegisters}", action=Utils.BooleanOptionalAction) 31 | 32 | 33 | miscOpts = parser.add_argument_group("MIPS misc instructions options") 34 | 35 | miscOpts.add_argument("--pseudo-instr", help=f"Toggles producing pseudo instructions. Defaults to {rabbitizer.config.pseudos_enablePseudos}", action=Utils.BooleanOptionalAction) 36 | 37 | miscOpts.add_argument("--j-branch", help=f"Treat J instructions as unconditional branches. {rabbitizer.config.toolchainTweaks_treatJAsUnconditionalBranch}", action=Utils.BooleanOptionalAction) 38 | 39 | miscOpts.add_argument("--sn64-div-fix", help=f"Enables a few fixes for SN64's assembler related to div/divu instructions. Defaults to {rabbitizer.config.toolchainTweaks_sn64DivFix}", action=Utils.BooleanOptionalAction) 40 | 41 | miscOpts.add_argument("--opcode-ljust", help=f"Set the minimal number of characters to left-align the opcode name. Defaults to {rabbitizer.config.misc_opcodeLJust}") 42 | 43 | miscOpts.add_argument("--unk-instr-comment", help=f"Disables the extra comment produced after unknown instructions. Defaults to {rabbitizer.config.misc_unknownInstrComment}", action=Utils.BooleanOptionalAction) 44 | 45 | 46 | @staticmethod 47 | def parseArgs(args: argparse.Namespace) -> None: 48 | if args.named_registers is not None: 49 | rabbitizer.config.regNames_namedRegisters = args.named_registers 50 | 51 | if common.GlobalConfig.ABI != common.Abi.O32: 52 | rabbitizer.config.regNames_gprAbiNames = rabbitizer.Abi.fromStr(common.GlobalConfig.ABI.name) 53 | rabbitizer.config.regNames_fprAbiNames = rabbitizer.Abi.fromStr(common.GlobalConfig.ABI.name) 54 | 55 | if args.Mgpr_names: 56 | rabbitizer.config.regNames_gprAbiNames = rabbitizer.Abi.fromStr(args.Mgpr_names) 57 | if args.Mfpr_names: 58 | rabbitizer.config.regNames_fprAbiNames = rabbitizer.Abi.fromStr(args.Mfpr_names) 59 | if args.Mreg_names: 60 | rabbitizer.config.regNames_gprAbiNames = rabbitizer.Abi.fromStr(args.Mreg_names) 61 | rabbitizer.config.regNames_fprAbiNames = rabbitizer.Abi.fromStr(args.Mreg_names) 62 | 63 | if args.use_fpccsr is not None: 64 | rabbitizer.config.regNames_userFpcCsr = args.use_fpccsr 65 | 66 | if args.cop0_named_registers is not None: 67 | rabbitizer.config.regNames_vr4300Cop0NamedRegisters = args.cop0_named_registers 68 | if args.rsp_cop0_named_registers is not None: 69 | rabbitizer.config.regNames_vr4300RspCop0NamedRegisters = args.rsp_cop0_named_registers 70 | 71 | if args.pseudo_instr is not None: 72 | rabbitizer.config.pseudos_enablePseudos = args.pseudo_instr 73 | 74 | if args.j_branch is not None: 75 | rabbitizer.config.toolchainTweaks_treatJAsUnconditionalBranch = args.j_branch 76 | 77 | if args.sn64_div_fix is not None: 78 | rabbitizer.config.toolchainTweaks_sn64DivFix = args.sn64_div_fix 79 | 80 | if args.opcode_ljust is not None: 81 | rabbitizer.config.misc_opcodeLJust = int(args.opcode_ljust, 0) 82 | 83 | if args.unk_instr_comment is not None: 84 | rabbitizer.config.misc_unknownInstrComment = args.unk_instr_comment 85 | -------------------------------------------------------------------------------- /spimdisasm/mips/MipsFileBase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import sys 9 | from typing import TextIO 10 | from pathlib import Path 11 | 12 | from .. import common 13 | 14 | from . import symbols 15 | 16 | 17 | class FileBase(common.ElementBase): 18 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, vram: int, filename: str, words: list[int], sectionType: common.FileSectionType, segmentVromStart: int, overlayCategory: str|None) -> None: 19 | super().__init__(context, vromStart, vromEnd, 0, vram, filename, words, sectionType, segmentVromStart, overlayCategory) 20 | 21 | self.symbolList: list[symbols.SymbolBase] = [] 22 | 23 | self.pointersOffsets: set[int] = set() 24 | 25 | self.isHandwritten: bool = False 26 | 27 | self.fileBoundaries: list[int] = list() 28 | 29 | self.symbolsVRams: set[int] = set() 30 | "addresses of symbols in this section" 31 | 32 | self.bytes: bytes = common.Utils.wordsToBytes(self.words) 33 | 34 | self.sectionAlignment: int|None = 4 35 | """ 36 | In log2 37 | 38 | If the desired alignment for the file is 8, then `sectionAlignment` 39 | should be set to 3. 40 | """ 41 | 42 | self.sectionFlags: str|None = None 43 | 44 | 45 | def setCommentOffset(self, commentOffset: int) -> None: 46 | self.commentOffset = commentOffset 47 | for sym in self.symbolList: 48 | sym.setCommentOffset(self.commentOffset) 49 | 50 | def getAsmPrelude_includes(self) -> str: 51 | output = "" 52 | 53 | output += f".include \"macro.inc\"{common.GlobalConfig.LINE_ENDS}" 54 | output += common.GlobalConfig.LINE_ENDS 55 | return output 56 | 57 | def getAsmPrelude_instructionDirectives(self) -> str: 58 | return "" 59 | 60 | def getAsmPrelude_sectionStart(self) -> str: 61 | output = "" 62 | 63 | output += f".section {self.getSectionName()}" 64 | if self.sectionFlags is not None: 65 | output += f", \"{self.sectionFlags}\"" 66 | output += common.GlobalConfig.LINE_ENDS 67 | 68 | output += common.GlobalConfig.LINE_ENDS 69 | if self.sectionAlignment is not None: 70 | output += f".align {self.sectionAlignment}{common.GlobalConfig.LINE_ENDS}" 71 | output += common.GlobalConfig.LINE_ENDS 72 | return output 73 | 74 | def getAsmPrelude(self) -> str: 75 | output = "" 76 | 77 | if common.GlobalConfig.ASM_PRELUDE_USE_INCLUDES: 78 | output += self.getAsmPrelude_includes() 79 | if common.GlobalConfig.ASM_PRELUDE_USE_INSTRUCTION_DIRECTIVES: 80 | output += self.getAsmPrelude_instructionDirectives() 81 | if common.GlobalConfig.ASM_PRELUDE_USE_SECTION_START: 82 | output += self.getAsmPrelude_sectionStart() 83 | 84 | return output 85 | 86 | def getHash(self) -> str: 87 | buffer = common.Utils.wordsToBytes(self.words) 88 | return common.Utils.getStrHash(buffer) 89 | 90 | 91 | def printNewFileBoundaries(self) -> None: 92 | if not common.GlobalConfig.PRINT_NEW_FILE_BOUNDARIES: 93 | return 94 | 95 | if len(self.fileBoundaries) > 0: 96 | print(f"File {self.name}") 97 | print(f"Section: {self.sectionType.toStr()}") 98 | print(f"Found {len(self.symbolList)} symbols.") 99 | print(f"Found {len(self.fileBoundaries)} file boundaries.") 100 | 101 | print(" offset, size, vram, symbols") 102 | 103 | boundaries = list(self.fileBoundaries) 104 | boundaries.append(self.sizew*4 + self.inFileOffset) 105 | 106 | for i in range(len(boundaries)-1): 107 | start = boundaries[i] 108 | end = boundaries[i+1] 109 | 110 | symbolsInBoundary = 0 111 | for func in self.symbolList: 112 | funcOffset = func.inFileOffset - self.inFileOffset 113 | if start <= funcOffset < end: 114 | symbolsInBoundary += 1 115 | fileVram = 0 116 | if self.vram is not None: 117 | fileVram = start + self.vram 118 | print(f" {start+self.commentOffset:06X}, {end-start:06X}, {fileVram:08X}, {symbolsInBoundary:7}") 119 | 120 | print() 121 | 122 | def printAnalyzisResults(self) -> None: 123 | self.printNewFileBoundaries() 124 | 125 | 126 | def compareToFile(self, other_file: FileBase) -> dict: 127 | hash_one = self.getHash() 128 | hash_two = other_file.getHash() 129 | 130 | result = { 131 | "equal": hash_one == hash_two, 132 | "hash_one": hash_one, 133 | "hash_two": hash_two, 134 | "size_one": self.sizew * 4, 135 | "size_two": other_file.sizew * 4, 136 | "diff_bytes": 0, 137 | "diff_words": 0, 138 | } 139 | 140 | diff_bytes = 0 141 | diff_words = 0 142 | 143 | if not result["equal"]: 144 | min_len = min(self.sizew, other_file.sizew) 145 | for i in range(min_len): 146 | for j in range(4): 147 | if (self.words[i] & (0xFF << (j * 8))) != (other_file.words[i] & (0xFF << (j * 8))): 148 | diff_bytes += 1 149 | 150 | min_len = min(self.sizew, other_file.sizew) 151 | for i in range(min_len): 152 | if self.words[i] != other_file.words[i]: 153 | diff_words += 1 154 | 155 | result["diff_bytes"] = diff_bytes 156 | result["diff_words"] = diff_words 157 | 158 | return result 159 | 160 | def blankOutDifferences(self, other: FileBase) -> bool: 161 | if not common.GlobalConfig.REMOVE_POINTERS: 162 | return False 163 | 164 | return False 165 | 166 | def removePointers(self) -> bool: 167 | if not common.GlobalConfig.REMOVE_POINTERS: 168 | return False 169 | 170 | return False 171 | 172 | 173 | def disassemble(self, migrate: bool=False, useGlobalLabel: bool=True) -> str: 174 | output = "" 175 | 176 | if not migrate: 177 | output += self.getSpimdisasmVersionString() 178 | 179 | for i, sym in enumerate(self.symbolList): 180 | output += sym.disassemble(migrate=migrate, useGlobalLabel=useGlobalLabel, isSplittedSymbol=False) 181 | if i + 1 < len(self.symbolList): 182 | output += common.GlobalConfig.LINE_ENDS 183 | return output 184 | 185 | def disassembleToFile(self, f: TextIO) -> None: 186 | if common.GlobalConfig.ASM_USE_PRELUDE: 187 | f.write(self.getAsmPrelude()) 188 | f.write(self.disassemble()) 189 | 190 | 191 | def saveToFile(self, filepath: str) -> None: 192 | if len(self.symbolList) == 0: 193 | return 194 | 195 | if filepath == "-": 196 | self.disassembleToFile(sys.stdout) 197 | else: 198 | if common.GlobalConfig.WRITE_BINARY: 199 | if self.sizew > 0: 200 | buffer = common.Utils.wordsToBytes(self.words) 201 | common.Utils.writeBytesToFile(Path(filepath + self.sectionType.toStr()), buffer) 202 | with open(filepath + self.sectionType.toStr() + ".s", "w", encoding="utf-8") as f: 203 | self.disassembleToFile(f) 204 | 205 | 206 | def createEmptyFile() -> FileBase: 207 | return FileBase(common.Context(), 0, 0, 0, "", [], common.FileSectionType.Unknown, 0, None) 208 | -------------------------------------------------------------------------------- /spimdisasm/mips/MipsFileSplits.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from pathlib import Path 9 | 10 | from .. import common 11 | 12 | from . import sections 13 | from . import FilesHandlers 14 | 15 | from . import FileBase, createEmptyFile 16 | 17 | 18 | class FileSplits(FileBase): 19 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, vram: int, filename: str, array_of_bytes: bytes, segmentVromStart: int, overlayCategory: str|None, splitsData: common.FileSplitFormat|None=None, relocSection: sections.SectionRelocZ64|None=None) -> None: 20 | super().__init__(context, vromStart, vromEnd, vram, filename, common.Utils.bytesToWords(array_of_bytes, vromStart, vromEnd), common.FileSectionType.Unknown, segmentVromStart, overlayCategory) 21 | 22 | self.sectionsDict: dict[common.FileSectionType, dict[str, sections.SectionBase]] = { 23 | common.FileSectionType.Text: dict(), 24 | common.FileSectionType.Data: dict(), 25 | common.FileSectionType.Rodata: dict(), 26 | common.FileSectionType.Bss: dict(), 27 | common.FileSectionType.Reloc: dict(), 28 | } 29 | 30 | self.splitsDataList: list[common.FileSplitEntry] = [] 31 | 32 | if relocSection is not None: 33 | relocSection.parent = self 34 | # if relocSection.vram is None: 35 | if not relocSection.differentSegment: 36 | relocStart = relocSection.textSize + relocSection.dataSize + relocSection.rodataSize 37 | if relocSection.differentSegment: 38 | relocStart += relocSection.bssSize 39 | relocSection.vram = self.vram + relocStart 40 | self.sectionsDict[common.FileSectionType.Reloc][filename] = relocSection 41 | 42 | if splitsData is None and relocSection is None: 43 | self.sectionsDict[common.FileSectionType.Text][filename] = sections.SectionText(context, vromStart, vromEnd, vram, filename, array_of_bytes, segmentVromStart, overlayCategory) 44 | elif splitsData is not None and len(splitsData) > 0: 45 | for splitEntry in splitsData: 46 | self.splitsDataList.append(splitEntry) 47 | elif relocSection is not None: 48 | start = 0 49 | end = 0 50 | for i in range(len(common.FileSections_ListBasic)): 51 | sectionType = common.FileSections_ListBasic[i] 52 | sectionSize = relocSection.sectionSizes[sectionType] 53 | 54 | if i != 0: 55 | start += relocSection.sectionSizes[common.FileSections_ListBasic[i-1]] 56 | end += relocSection.sectionSizes[sectionType] 57 | 58 | if sectionType == common.FileSectionType.Bss: 59 | # bss is after reloc when the relocation is on the same segment 60 | if not relocSection.differentSegment: 61 | start += relocSection.sizew * 4 62 | end += relocSection.sizew * 4 63 | 64 | if sectionSize == 0: 65 | # There's no need to disassemble empty sections 66 | continue 67 | 68 | vram = self.vram + start 69 | splitEntry = common.FileSplitEntry(start, vram, filename, sectionType, end, False, False) 70 | self.splitsDataList.append(splitEntry) 71 | 72 | 73 | for splitEntry in self.splitsDataList: 74 | # if self.vram is None: 75 | # self.vram = splitEntry.vram 76 | 77 | if splitEntry.section == common.FileSectionType.Dummy: 78 | # Ignore dummy sections 79 | continue 80 | 81 | f = FilesHandlers.createSectionFromSplitEntry(splitEntry, array_of_bytes, context, vromStart) 82 | f.parent = self 83 | f.setCommentOffset(splitEntry.offset) 84 | 85 | self.sectionsDict[splitEntry.section][splitEntry.fileName] = f 86 | 87 | @property 88 | def nFuncs(self) -> int: 89 | nFuncs = 0 90 | for f in self.sectionsDict[common.FileSectionType.Text].values(): 91 | assert(isinstance(f, sections.SectionText)) 92 | text: sections.SectionText = f 93 | nFuncs += text.nFuncs 94 | return nFuncs 95 | 96 | def setVram(self, vram: int) -> None: 97 | super().setVram(vram) 98 | for sectDict in self.sectionsDict.values(): 99 | for section in sectDict.values(): 100 | section.setVram(vram) 101 | 102 | def getHash(self) -> str: 103 | words = list() 104 | for sectDict in self.sectionsDict.values(): 105 | for section in sectDict.values(): 106 | words += section.words 107 | buffer = common.Utils.wordsToBytes(words) 108 | return common.Utils.getStrHash(buffer) 109 | 110 | def analyze(self) -> None: 111 | for filename, relocSection in self.sectionsDict[common.FileSectionType.Reloc].items(): 112 | assert isinstance(relocSection, sections.SectionRelocZ64) 113 | for entry in relocSection.entries: 114 | sectionType = entry.getSectionType() 115 | if entry.reloc == 0: 116 | continue 117 | 118 | for subFile in self.sectionsDict[sectionType].values(): 119 | subFile.pointersOffsets.add(entry.offset) 120 | 121 | for sectDict in self.sectionsDict.values(): 122 | for section in sectDict.values(): 123 | section.analyze() 124 | 125 | def compareToFile(self, other_file: FileBase) -> dict: 126 | if isinstance(other_file, FileSplits): 127 | filesections: dict[common.FileSectionType, dict] = { 128 | common.FileSectionType.Text: dict(), 129 | common.FileSectionType.Data: dict(), 130 | common.FileSectionType.Rodata: dict(), 131 | common.FileSectionType.Bss: dict(), 132 | common.FileSectionType.Reloc: dict(), 133 | } 134 | 135 | for sectionType in common.FileSections_ListAll: 136 | for section_name, section in self.sectionsDict[sectionType].items(): 137 | if section_name in other_file.sectionsDict[sectionType]: 138 | other_section = other_file.sectionsDict[sectionType][section_name] 139 | filesections[sectionType][section_name] = section.compareToFile(other_section) 140 | else: 141 | filesections[sectionType][section_name] = section.compareToFile(createEmptyFile()) 142 | for section_name, other_section in other_file.sectionsDict[sectionType].items(): 143 | if section_name in self.sectionsDict[sectionType]: 144 | section = self.sectionsDict[sectionType][section_name] 145 | if section_name not in filesections[sectionType]: 146 | filesections[sectionType][section_name] = section.compareToFile(other_section) 147 | else: 148 | filesections[sectionType][section_name] = createEmptyFile().compareToFile(other_section) 149 | 150 | return {"filesections": filesections} 151 | 152 | return super().compareToFile(other_file) 153 | 154 | def blankOutDifferences(self, other_file: FileBase) -> bool: 155 | if not common.GlobalConfig.REMOVE_POINTERS: 156 | return False 157 | 158 | if not isinstance(other_file, FileSplits): 159 | return False 160 | 161 | was_updated = False 162 | for sectionType in common.FileSections_ListAll: 163 | for section_name, section in self.sectionsDict[sectionType].items(): 164 | if section_name in other_file.sectionsDict[sectionType]: 165 | other_section = other_file.sectionsDict[sectionType][section_name] 166 | was_updated = section.blankOutDifferences(other_section) or was_updated 167 | for section_name, other_section in other_file.sectionsDict[sectionType].items(): 168 | if section_name in self.sectionsDict[sectionType]: 169 | section = self.sectionsDict[sectionType][section_name] 170 | was_updated = section.blankOutDifferences(other_section) or was_updated 171 | 172 | return was_updated 173 | 174 | def removePointers(self) -> bool: 175 | if not common.GlobalConfig.REMOVE_POINTERS: 176 | return False 177 | 178 | was_updated = False 179 | for sectDict in self.sectionsDict.values(): 180 | for section in sectDict.values(): 181 | was_updated = section.removePointers() or was_updated 182 | 183 | return was_updated 184 | 185 | def saveToFile(self, filepath: str) -> None: 186 | for sectDict in self.sectionsDict.values(): 187 | for name, section in sectDict.items(): 188 | if name != "" and not filepath.endswith("/"): 189 | name = " " + name 190 | section.saveToFile(filepath + name) 191 | -------------------------------------------------------------------------------- /spimdisasm/mips/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from . import sections as sections 9 | from . import symbols as symbols 10 | 11 | from .FuncRodataEntry import FunctionRodataEntry as FunctionRodataEntry 12 | 13 | from . import FilesHandlers as FilesHandlers 14 | 15 | from .InstructionConfig import InstructionConfig as InstructionConfig 16 | from .MipsFileBase import FileBase as FileBase 17 | from .MipsFileBase import createEmptyFile as createEmptyFile 18 | from .MipsFileSplits import FileSplits as FileSplits 19 | -------------------------------------------------------------------------------- /spimdisasm/mips/sections/MipsSectionBase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from ... import common 9 | 10 | from ..MipsFileBase import FileBase 11 | 12 | class SectionBase(FileBase): 13 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, vram: int, filename: str, words: list[int], sectionType: common.FileSectionType, segmentVromStart: int, overlayCategory: str|None) -> None: 14 | super().__init__(context, vromStart, vromEnd, vram, filename, words, sectionType, segmentVromStart, overlayCategory) 15 | 16 | self.stringEncoding: str = common.GlobalConfig.DATA_STRING_ENCODING 17 | self.enableStringGuessing: bool = True 18 | """ 19 | Allows to toggle string guessing at the section level. 20 | 21 | Can be useful for sections that are known to never have strings. 22 | 23 | This option is ignored if the global string guessing option is disabled. 24 | """ 25 | 26 | self.typeForOwnedSymbols: str|None = None 27 | """ 28 | If not `None`, then the `autodetectedType` of the `ContextSymbol`s 29 | found on this section will be set to the specified value, ignoring the 30 | type information from the function analysis. 31 | 32 | This makes each symbol to disassemble as this type. 33 | 34 | This won't override type information given by the user. 35 | 36 | Useful for sections that are known to have one type for all its symbols, 37 | like `.lit4` having only `.float` symbols and `.lit8` having only 38 | `.double` symbols. 39 | """ 40 | 41 | self.sizeForOwnedSymbols: int|None = None 42 | """ 43 | If not `None`, then the `autodetectedSize` of the `ContextSymbol`s 44 | found on this section will be set to the specified value and pads will 45 | be generated to account for the sizing. 46 | 47 | Useful for sections that are known to have one size for all its symbols, 48 | like `.lit4` having only 4 bytes symbols and `.lit8` having only 49 | 8 bytes symbols. 50 | """ 51 | 52 | def checkWordIsASymbolReference(self, word: int) -> bool: 53 | if not self.context.totalVramRange.isInRange(word): 54 | return False 55 | if self.context.isAddressBanned(word): 56 | return False 57 | 58 | contextSym = self.getSymbol(word, tryPlusOffset=True, checkUpperLimit=False) 59 | if contextSym is not None: 60 | symType = contextSym.getTypeSpecial() 61 | if symType in {common.SymbolSpecialType.function, common.SymbolSpecialType.branchlabel, common.SymbolSpecialType.jumptablelabel}: 62 | # Avoid generating extra symbols in the middle of functions 63 | return False 64 | 65 | if word < contextSym.vram + contextSym.getSize(): 66 | # Avoid generating symbols in the middle of other symbols with known sizes 67 | return False 68 | 69 | self.addPointerInDataReference(word) 70 | return True 71 | 72 | def processStaticRelocs(self) -> None: 73 | for i in range(self.sizew): 74 | word = self.words[i] 75 | vrom = self.getVromOffset(i*4) 76 | relocInfo = self.context.globalRelocationOverrides.get(vrom) 77 | if relocInfo is None or relocInfo.staticReference is None: 78 | continue 79 | 80 | relocVram = relocInfo.staticReference.sectionVram + word 81 | sectionType = relocInfo.staticReference.sectionType 82 | if self.sectionType == common.FileSectionType.Rodata and sectionType == common.FileSectionType.Text: 83 | contextSym = self.addJumpTableLabel(relocVram, isAutogenerated=True) 84 | else: 85 | contextSym = self.addSymbol(relocVram, sectionType=sectionType, isAutogenerated=True) 86 | contextSym._isStatic = True 87 | 88 | 89 | def _checkAndCreateFirstSymbol(self) -> common.ContextSymbol: 90 | "Check if the very start of the file has a symbol and create it if it doesn't exist yet" 91 | 92 | currentVram = self.getVramOffset(0) 93 | currentVrom = self.getVromOffsetNone(0) 94 | 95 | return self.addSymbol(currentVram, sectionType=self.sectionType, isAutogenerated=True, symbolVrom=currentVrom) 96 | 97 | def _getOwnedSymbol(self, localOffset: int) -> common.ContextSymbol|None: 98 | currentVram = self.getVramOffset(localOffset) 99 | currentVrom = self.getVromOffsetNone(localOffset) 100 | 101 | contextSym = self.getSymbol(currentVram, vromAddress=currentVrom, tryPlusOffset=False) 102 | if contextSym is None: 103 | return None 104 | 105 | if self.typeForOwnedSymbols is not None: 106 | contextSym.autodetectedType = self.typeForOwnedSymbols 107 | if self.sizeForOwnedSymbols is not None: 108 | contextSym.autodetectedSize = self.sizeForOwnedSymbols 109 | 110 | if self.sectionType != common.FileSectionType.Bss: 111 | contextSym.isMaybeString = self._stringGuesser(contextSym, localOffset) 112 | contextSym.isMaybePascalString = self._pascalStringGuesser(contextSym, localOffset) 113 | 114 | self._createAutoPadFromSymbol(localOffset, contextSym) 115 | 116 | return contextSym 117 | 118 | def _addOwnedSymbol(self, localOffset: int) -> common.ContextSymbol|None: 119 | currentVram = self.getVramOffset(localOffset) 120 | currentVrom = self.getVromOffsetNone(localOffset) 121 | 122 | contextSym = self.addSymbol(currentVram, sectionType=self.sectionType, isAutogenerated=True, symbolVrom=currentVrom, allowAddendInstead=True) 123 | if contextSym.vram != currentVram: 124 | return None 125 | 126 | if self.typeForOwnedSymbols is not None: 127 | contextSym.autodetectedType = self.typeForOwnedSymbols 128 | if self.sizeForOwnedSymbols is not None: 129 | contextSym.autodetectedSize = self.sizeForOwnedSymbols 130 | 131 | if self.sectionType != common.FileSectionType.Bss: 132 | contextSym.isMaybeString = self._stringGuesser(contextSym, localOffset) 133 | contextSym.isMaybePascalString = self._pascalStringGuesser(contextSym, localOffset) 134 | 135 | return contextSym 136 | 137 | def _addOwnedAutocreatedPad(self, localOffset: int) -> common.ContextSymbol|None: 138 | if localOffset >= self.sizew * 4: 139 | return None 140 | 141 | currentVram = self.getVramOffset(localOffset) 142 | currentVrom = self.getVromOffsetNone(localOffset) 143 | 144 | extraContextSym = self.addSymbol(currentVram, sectionType=self.sectionType, isAutogenerated=True, symbolVrom=currentVrom) 145 | extraContextSym.isAutoCreatedPad = True 146 | return extraContextSym 147 | 148 | def _createAutoPadFromSymbol(self, localOffset: int, contextSym: common.ContextSymbol) -> common.ContextSymbol|None: 149 | if not contextSym.hasUserDeclaredSize(): 150 | if self.sizeForOwnedSymbols is not None and self.sizeForOwnedSymbols > 0: 151 | return self._addOwnedAutocreatedPad(localOffset+self.sizeForOwnedSymbols) 152 | 153 | if self.sectionType == common.FileSectionType.Data: 154 | createPads = common.GlobalConfig.CREATE_DATA_PADS 155 | elif self.sectionType == common.FileSectionType.Rodata: 156 | createPads = common.GlobalConfig.CREATE_RODATA_PADS 157 | else: 158 | createPads = False 159 | 160 | if createPads and contextSym.hasUserDeclaredSize(): 161 | symDeclaredSize = contextSym.getSize() 162 | if symDeclaredSize > 0: 163 | # Try to respect the user-declared size for this symbol 164 | return self._addOwnedAutocreatedPad(localOffset+symDeclaredSize) 165 | 166 | return None 167 | 168 | def _stringGuesser(self, contextSym: common.ContextSymbol, localOffset: int) -> bool: 169 | if contextSym._ranStringCheck: 170 | return contextSym.isMaybeString 171 | 172 | if contextSym.isMaybeString or contextSym.isString(): 173 | return True 174 | 175 | if not self.enableStringGuessing: 176 | return False 177 | 178 | if self.sectionType == common.FileSectionType.Rodata: 179 | stringGuesserLevel = common.GlobalConfig.RODATA_STRING_GUESSER_LEVEL 180 | else: 181 | stringGuesserLevel = common.GlobalConfig.DATA_STRING_GUESSER_LEVEL 182 | 183 | if stringGuesserLevel < 1: 184 | return False 185 | 186 | if contextSym.referenceCounter > 1: 187 | if stringGuesserLevel < 2: 188 | return False 189 | 190 | # This would mean the string is an empty string, which is not very likely 191 | if self.words[localOffset//4] == 0: 192 | if stringGuesserLevel < 3: 193 | return False 194 | 195 | if contextSym.hasOnlyAutodetectedType(): 196 | if stringGuesserLevel < 4: 197 | return False 198 | 199 | currentVram = self.getVramOffset(localOffset) 200 | currentVrom = self.getVromOffset(localOffset) 201 | _, rawStringSize = common.Utils.decodeBytesToStrings(self.bytes, localOffset, self.stringEncoding) 202 | if rawStringSize < 0: 203 | # String can't be decoded 204 | return False 205 | 206 | # Check if there is already another symbol after the current one and before the end of the string, 207 | # in which case we say this symbol should not be a string 208 | otherSym = self.getSymbol(currentVram + rawStringSize, vromAddress=currentVrom + rawStringSize, checkUpperLimit=False, checkGlobalSegment=False) 209 | if otherSym != contextSym: 210 | return False 211 | 212 | return True 213 | 214 | def _pascalStringGuesser(self, contextSym: common.ContextSymbol, localOffset: int) -> bool: 215 | if contextSym._ranPascalStringCheck: 216 | return contextSym.isMaybePascalString 217 | 218 | if contextSym.isMaybePascalString or contextSym.isPascalString(): 219 | return True 220 | 221 | if not self.enableStringGuessing: 222 | return False 223 | 224 | if self.sectionType == common.FileSectionType.Rodata: 225 | stringGuesserLevel = common.GlobalConfig.PASCAL_RODATA_STRING_GUESSER_LEVEL 226 | else: 227 | stringGuesserLevel = common.GlobalConfig.PASCAL_DATA_STRING_GUESSER_LEVEL 228 | 229 | if stringGuesserLevel < 1: 230 | return False 231 | 232 | if contextSym.referenceCounter > 1: 233 | if stringGuesserLevel < 2: 234 | return False 235 | 236 | # This would mean the string is an empty string, which is not very likely 237 | if self.words[localOffset//4] == 0: 238 | if stringGuesserLevel < 3: 239 | return False 240 | 241 | if contextSym.hasOnlyAutodetectedType(): 242 | if stringGuesserLevel < 4: 243 | return False 244 | 245 | currentVram = self.getVramOffset(localOffset) 246 | currentVrom = self.getVromOffset(localOffset) 247 | _, rawStringSize = common.Utils.decodeBytesToPascalStrings(self.bytes, localOffset, self.stringEncoding, terminator=0x20) 248 | if rawStringSize < 0: 249 | # String can't be decoded 250 | return False 251 | 252 | # Check if there is already another symbol after the current one and before the end of the string, 253 | # in which case we say this symbol should not be a string 254 | otherSym = self.getSymbol(currentVram + rawStringSize - 1, vromAddress=currentVrom + rawStringSize, checkUpperLimit=False, checkGlobalSegment=False) 255 | if otherSym != contextSym: 256 | return False 257 | 258 | return True 259 | 260 | 261 | def blankOutDifferences(self, other: FileBase) -> bool: 262 | if not common.GlobalConfig.REMOVE_POINTERS: 263 | return False 264 | 265 | was_updated = False 266 | if len(common.GlobalConfig.IGNORE_WORD_LIST) > 0: 267 | min_len = min(self.sizew, other.sizew) 268 | for i in range(min_len): 269 | for upperByte in common.GlobalConfig.IGNORE_WORD_LIST: 270 | word = upperByte << 24 271 | if ((self.words[i] >> 24) & 0xFF) == upperByte and ((other.words[i] >> 24) & 0xFF) == upperByte: 272 | self.words[i] = word 273 | other.words[i] = word 274 | was_updated = True 275 | 276 | return was_updated 277 | 278 | def suspectedPaddingGarbage(self) -> list[int]: 279 | return [] 280 | -------------------------------------------------------------------------------- /spimdisasm/mips/sections/MipsSectionBss.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from ... import common 9 | 10 | from .. import symbols 11 | 12 | from . import SectionBase 13 | 14 | 15 | class SectionBss(SectionBase): 16 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, bssVramStart: int, bssVramEnd: int, filename: str, segmentVromStart: int, overlayCategory: str|None) -> None: 17 | super().__init__(context, vromStart, vromEnd, bssVramStart, filename, [], common.FileSectionType.Bss, segmentVromStart, overlayCategory) 18 | 19 | self.bssVramStart: int = bssVramStart 20 | self.bssVramEnd: int = bssVramEnd 21 | 22 | self.vram = bssVramStart 23 | 24 | @property 25 | def bssTotalSize(self) -> int: 26 | return self.bssVramEnd - self.bssVramStart 27 | 28 | @property 29 | def sizew(self) -> int: 30 | return self.bssTotalSize // 4 31 | 32 | def setVram(self, vram: int) -> None: 33 | super().setVram(vram) 34 | 35 | self.bssVramStart = vram 36 | self.bssVramEnd = vram + self.bssTotalSize 37 | 38 | def analyze(self) -> None: 39 | self._checkAndCreateFirstSymbol() 40 | 41 | # If something that could be a pointer found in data happens to be in 42 | # the middle of this bss file's addresses space then consider it as a 43 | # new bss variable 44 | for ptr in self.getAndPopPointerInDataReferencesRange(self.bssVramStart, self.bssVramEnd): 45 | # Check if the symbol already exists, in case the user has provided size 46 | contextSym = self.getSymbol(ptr, tryPlusOffset=True) 47 | if contextSym is None: 48 | self.addSymbol(ptr, sectionType=self.sectionType, isAutogenerated=True) 49 | 50 | autoCreatedPads: set[int] = set() 51 | bssSymbolOffsets: set[int] = set() 52 | for symbolVram, contextSym in self.getSymbolsRange(self.bssVramStart, self.bssVramEnd): 53 | # Mark every known symbol that happens to be in this address space as defined 54 | contextSym.sectionType = common.FileSectionType.Bss 55 | 56 | # Needs to move this to a list because the algorithm requires to check the size of a bss variable based on the next bss variable' vram 57 | assert symbolVram >= self.bssVramStart 58 | assert symbolVram < self.bssVramEnd 59 | bssSymbolOffsets.add(symbolVram - self.bssVramStart) 60 | 61 | # If the bss has an explicit size then produce an extra symbol after it, so the generated bss symbol uses the user-declared size 62 | if contextSym.hasUserDeclaredSize(): 63 | newSymbolVram = symbolVram + contextSym.getSize() 64 | if newSymbolVram != self.bssVramEnd: 65 | assert newSymbolVram >= self.bssVramStart 66 | assert newSymbolVram < self.bssVramEnd, f"{self.name}, symbolVram={symbolVram:08X}, newSymbolVram={newSymbolVram:08X}, self.bssVramEnd={self.bssVramEnd:08X}" 67 | symOffset = symbolVram + contextSym.getSize() - self.bssVramStart 68 | bssSymbolOffsets.add(symOffset) 69 | autoCreatedPads.add(symOffset) 70 | 71 | 72 | sortedOffsets = sorted(bssSymbolOffsets) 73 | 74 | i = 0 75 | while i < len(sortedOffsets): 76 | symbolOffset = sortedOffsets[i] 77 | symbolVram = self.bssVramStart + symbolOffset 78 | 79 | # Calculate the space of the bss variable 80 | space = self.bssTotalSize - symbolOffset 81 | if i + 1 < len(sortedOffsets): 82 | nextSymbolOffset = sortedOffsets[i+1] 83 | if nextSymbolOffset <= self.bssTotalSize: 84 | space = nextSymbolOffset - symbolOffset 85 | 86 | assert space > 0 87 | 88 | vrom = self.getVromOffset(symbolOffset) 89 | vromEnd = vrom + space 90 | sym = symbols.SymbolBss(self.context, vrom, vromEnd, symbolOffset + self.inFileOffset, symbolVram, space, self.segmentVromStart, self.overlayCategory) 91 | sym.parent = self 92 | sym.contextSym.autodetectedSize = space 93 | sym.setCommentOffset(self.commentOffset) 94 | if symbolOffset in autoCreatedPads: 95 | sym.contextSym.isAutoCreatedPad = True 96 | sym.analyze() 97 | self.symbolList.append(sym) 98 | 99 | self.symbolsVRams.add(symbolVram) 100 | 101 | i += 1 102 | -------------------------------------------------------------------------------- /spimdisasm/mips/sections/MipsSectionData.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from ... import common 9 | 10 | from .. import symbols 11 | 12 | from . import SectionBase 13 | 14 | 15 | class SectionData(SectionBase): 16 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, vram: int, filename: str, array_of_bytes: bytes, segmentVromStart: int, overlayCategory: str|None) -> None: 17 | if common.GlobalConfig.ENDIAN_DATA is not None: 18 | words = common.Utils.endianessBytesToWords(common.GlobalConfig.ENDIAN_DATA, array_of_bytes, vromStart, vromEnd) 19 | else: 20 | words = common.Utils.bytesToWords(array_of_bytes, vromStart, vromEnd) 21 | super().__init__(context, vromStart, vromEnd, vram, filename, words, common.FileSectionType.Data, segmentVromStart, overlayCategory) 22 | 23 | 24 | def analyze(self) -> None: 25 | self._checkAndCreateFirstSymbol() 26 | 27 | symbolList: list[tuple[int, common.ContextSymbol]] = [] 28 | localOffset = 0 29 | localOffsetsWithSymbols: set[int] = set() 30 | 31 | needsFurtherAnalyzis = False 32 | 33 | for w in self.words: 34 | currentVram = self.getVramOffset(localOffset) 35 | 36 | contextSym = self._getOwnedSymbol(localOffset) 37 | if contextSym is not None: 38 | symbolList.append((localOffset, contextSym)) 39 | localOffsetsWithSymbols.add(localOffset) 40 | 41 | elif self.popPointerInDataReference(currentVram) is not None: 42 | contextSym = self._addOwnedSymbol(localOffset) 43 | if contextSym is not None: 44 | symbolList.append((localOffset, contextSym)) 45 | localOffsetsWithSymbols.add(localOffset) 46 | 47 | if self.checkWordIsASymbolReference(w): 48 | if w < currentVram and self.containsVram(w): 49 | # References a data symbol from this section and it is behind this current symbol 50 | needsFurtherAnalyzis = True 51 | 52 | localOffset += 4 53 | 54 | if needsFurtherAnalyzis: 55 | localOffset = 0 56 | for w in self.words: 57 | currentVram = self.getVramOffset(localOffset) 58 | 59 | if self.popPointerInDataReference(currentVram) is not None and localOffset not in localOffsetsWithSymbols: 60 | contextSym = self._addOwnedSymbol(localOffset) 61 | if contextSym is not None: 62 | symbolList.append((localOffset, contextSym)) 63 | localOffsetsWithSymbols.add(localOffset) 64 | 65 | localOffset += 4 66 | 67 | # Since we appended new symbols, this list is not sorted anymore 68 | symbolList.sort() 69 | 70 | self.processStaticRelocs() 71 | 72 | for i, (offset, contextSym) in enumerate(symbolList): 73 | if i + 1 == len(symbolList): 74 | words = self.words[offset//4:] 75 | else: 76 | nextOffset = symbolList[i+1][0] 77 | if offset == nextOffset: 78 | continue 79 | words = self.words[offset//4:nextOffset//4] 80 | 81 | vrom = self.getVromOffset(offset) 82 | vromEnd = vrom + 4*len(words) 83 | sym = symbols.SymbolData(self.context, vrom, vromEnd, offset + self.inFileOffset, contextSym.vram, words, self.segmentVromStart, self.overlayCategory) 84 | sym.parent = self 85 | sym.setCommentOffset(self.commentOffset) 86 | sym.stringEncoding = self.stringEncoding 87 | sym.analyze() 88 | self.symbolList.append(sym) 89 | 90 | self.symbolsVRams.add(contextSym.vram) 91 | 92 | 93 | def removePointers(self) -> bool: 94 | if not common.GlobalConfig.REMOVE_POINTERS: 95 | return False 96 | 97 | was_updated = False 98 | for i in range(self.sizew): 99 | top_byte = (self.words[i] >> 24) & 0xFF 100 | if top_byte == 0x80: 101 | self.words[i] = top_byte << 24 102 | was_updated = True 103 | if (top_byte & 0xF0) == 0x00 and (top_byte & 0x0F) != 0x00: 104 | self.words[i] = top_byte << 24 105 | was_updated = True 106 | 107 | return was_updated 108 | -------------------------------------------------------------------------------- /spimdisasm/mips/sections/MipsSectionGccExceptTable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import rabbitizer 9 | 10 | from ... import common 11 | 12 | from .. import symbols 13 | 14 | from . import SectionBase 15 | 16 | 17 | class SectionGccExceptTable(SectionBase): 18 | """ 19 | A `.gcc_except_table` section contains a single symbol that contains 20 | information to handle C++ exceptions known as the Error Handler Table 21 | (`ehtable` for short). 22 | 23 | The ehtable is a list of references to labels within functions or 24 | references to functions themselves. At the end of the table there are two 25 | `-1`, which seems to exist just as an end marker (TODO: corroborate the 26 | endmarker claim). 27 | 28 | Normally there's a single exception table per section per TU, but the way 29 | it was implemented here allows for multiple exception tables in case file 30 | splits have not been done yet. 31 | 32 | This is implemented similarly to how the jumptable handling is implemented. 33 | """ 34 | 35 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, vram: int, filename: str, array_of_bytes: bytes, segmentVromStart: int, overlayCategory: str|None) -> None: 36 | super().__init__(context, vromStart, vromEnd, vram, filename, common.Utils.bytesToWords(array_of_bytes, vromStart, vromEnd), common.FileSectionType.GccExceptTable, segmentVromStart, overlayCategory) 37 | 38 | 39 | def _analyze_processExceptTable(self, localOffset: int, w: int, contextSym: common.ContextSymbol|None, lastVramSymbol: common.ContextSymbol, exceptTableSym: common.ContextSymbol|None, firstExceptTableWord: int) -> tuple[common.ContextSymbol|None, int]: 40 | if contextSym is not None and contextSym.isGccExceptTable(): 41 | # New jumptable 42 | exceptTableSym = contextSym 43 | firstExceptTableWord = w 44 | 45 | elif exceptTableSym is not None: 46 | # The last symbol found was part of a jumptable, check if this word still is part of the jumptable 47 | 48 | if localOffset not in self.pointersOffsets: 49 | if w == 0xFFFFFFFF: 50 | # Except tables contain 2 negative ones at the end of the table. 51 | pass 52 | 53 | elif w == 0: 54 | return None, firstExceptTableWord 55 | 56 | elif contextSym is not None: 57 | return None, firstExceptTableWord 58 | 59 | elif ((w >> 24) & 0xFF) != ((firstExceptTableWord >> 24) & 0xFF): 60 | if not ( 61 | lastVramSymbol.isGccExceptTable() 62 | and lastVramSymbol.isGot 63 | and common.GlobalConfig.GP_VALUE is not None 64 | ): 65 | return None, firstExceptTableWord 66 | else: 67 | # No jumptable 68 | return None, firstExceptTableWord 69 | 70 | # Generate the current label 71 | if w != 0xFFFFFFFF and w != 0: 72 | labelAddr = w 73 | if lastVramSymbol.isGot and common.GlobalConfig.GP_VALUE is not None: 74 | labelAddr = common.GlobalConfig.GP_VALUE + rabbitizer.Utils.from2Complement(w, 32) 75 | labelSym = self.addGccExceptTableLabel(labelAddr, isAutogenerated=True) 76 | 77 | if labelSym.unknownSegment: 78 | return None, firstExceptTableWord 79 | 80 | labelSym.referenceCounter += 1 81 | 82 | return exceptTableSym, firstExceptTableWord 83 | 84 | 85 | def analyze(self) -> None: 86 | lastVramSymbol: common.ContextSymbol = self._checkAndCreateFirstSymbol() 87 | contextSym: common.ContextSymbol|None = None 88 | 89 | # All symbols on this section are assumed to be except tables 90 | self.addGccExceptTable(self.getVramOffset(0), isAutogenerated=True, symbolVrom=self.getVromOffset(0)) 91 | 92 | symbolList: list[tuple[int, int]] = [] 93 | localOffset = 0 94 | 95 | exceptTableSym: common.ContextSymbol|None = None 96 | firstExceptTableWord = -1 97 | negativeOneCounter = 0 98 | 99 | for w in self.words: 100 | currentVram = self.getVramOffset(localOffset) 101 | currentVrom = self.getVromOffset(localOffset) 102 | 103 | if w == 0: 104 | if not lastVramSymbol.isAutoCreatedPad: 105 | pad = self._addOwnedAutocreatedPad(localOffset) 106 | if pad is not None: 107 | lastVramSymbol = pad 108 | 109 | if negativeOneCounter >= 2 and w != 0: 110 | # The except table ended 111 | contextSym = self._addOwnedSymbol(localOffset) 112 | if contextSym is not None: 113 | lastVramSymbol = contextSym 114 | self.addGccExceptTable(currentVram, isAutogenerated=True, symbolVrom=currentVrom) 115 | 116 | negativeOneCounter = 0 117 | 118 | else: 119 | # Check if we have a symbol at this address, if not then use the last known one 120 | contextSym = self.getSymbol(currentVram, vromAddress=currentVrom, tryPlusOffset=False) 121 | if contextSym is not None: 122 | lastVramSymbol = contextSym 123 | 124 | exceptTableSym, firstExceptTableWord = self._analyze_processExceptTable(localOffset, w, contextSym, lastVramSymbol, exceptTableSym, firstExceptTableWord) 125 | 126 | if exceptTableSym is None: 127 | if contextSym is not None or self.popPointerInDataReference(currentVram) is not None or (lastVramSymbol.isGccExceptTable() and w != 0): 128 | # Somehow this isn't an except table? TODO: check if this can happen 129 | contextSym = self._addOwnedSymbol(localOffset) 130 | if contextSym is not None: 131 | lastVramSymbol = contextSym 132 | 133 | self.checkWordIsASymbolReference(w) 134 | 135 | if contextSym is not None: 136 | self.symbolsVRams.add(currentVram) 137 | symbolList.append((localOffset, currentVram)) 138 | 139 | self._createAutoPadFromSymbol(localOffset, contextSym) 140 | 141 | if w == 0xFFFFFFFF: 142 | negativeOneCounter += 1 143 | 144 | localOffset += 4 145 | 146 | for i, (offset, vram) in enumerate(symbolList): 147 | if i + 1 == len(symbolList): 148 | words = self.words[offset//4:] 149 | else: 150 | nextOffset = symbolList[i+1][0] 151 | words = self.words[offset//4:nextOffset//4] 152 | 153 | vrom = self.getVromOffset(offset) 154 | vromEnd = vrom + len(words)*4 155 | sym = symbols.SymbolGccExceptTable(self.context, vrom, vromEnd, offset + self.inFileOffset, vram, words, self.segmentVromStart, self.overlayCategory) 156 | sym.parent = self 157 | sym.setCommentOffset(self.commentOffset) 158 | sym.analyze() 159 | self.symbolList.append(sym) 160 | 161 | self.processStaticRelocs() 162 | -------------------------------------------------------------------------------- /spimdisasm/mips/sections/MipsSectionRelocZ64.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | # Relocation format used by overlays of Zelda64, Yoshi Story and Doubutsu no Mori (Animal Forest) 7 | 8 | from __future__ import annotations 9 | 10 | from pathlib import Path 11 | 12 | from ... import common 13 | 14 | from .. import symbols 15 | 16 | from . import SectionBase 17 | 18 | 19 | class RelocEntry: 20 | def __init__(self, entry: int) -> None: 21 | self.sectionId = entry >> 30 22 | self.relocType = (entry >> 24) & 0x3F 23 | self.offset = entry & 0x00FFFFFF 24 | 25 | @property 26 | def reloc(self) -> int: 27 | return (self.sectionId << 30) | (self.relocType << 24) | (self.offset) 28 | 29 | def getSectionType(self) -> common.FileSectionType: 30 | return common.FileSectionType.fromId(self.sectionId) 31 | 32 | def getRelocType(self) -> common.RelocType|None: 33 | return common.RelocType.fromValue(self.relocType) 34 | 35 | def __str__(self) -> str: 36 | section = self.getSectionType().toStr() 37 | relocName = "None" 38 | reloc = self.getRelocType() 39 | if reloc is not None: 40 | relocName = reloc.name 41 | 42 | return f"{section} {relocName} 0x{self.offset:X}" 43 | def __repr__(self) -> str: 44 | return self.__str__() 45 | 46 | 47 | class SectionRelocZ64(SectionBase): 48 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, vram: int, filename: str, array_of_bytes: bytes, segmentVromStart: int, overlayCategory: str|None) -> None: 49 | super().__init__(context, vromStart, vromEnd, vram, filename, common.Utils.bytesToWords(array_of_bytes, vromStart, vromEnd), common.FileSectionType.Reloc, segmentVromStart, overlayCategory) 50 | 51 | self.seekup: int = self.words[-1] 52 | 53 | self.setCommentOffset(self.sizew*4 - self.seekup) 54 | 55 | # Remove non reloc stuff 56 | self.words: list[int] = self.words[-self.seekup // 4:] 57 | 58 | self.sectionSizes: dict[common.FileSectionType, int] = { 59 | common.FileSectionType.Text: self.words[0], 60 | common.FileSectionType.Data: self.words[1], 61 | common.FileSectionType.Rodata: self.words[2], 62 | common.FileSectionType.Bss: self.words[3], 63 | } 64 | self.relocCount: int = self.words[4] 65 | 66 | self.tail: list[int] = self.words[self.relocCount+5:-1] 67 | 68 | self.entries: list[RelocEntry] = list() 69 | for word in self.words[5:self.relocCount+5]: 70 | self.entries.append(RelocEntry(word)) 71 | 72 | self.differentSegment: bool = False 73 | 74 | self.sectionFlags = "a" 75 | self.enableStringGuessing = False 76 | 77 | @property 78 | def nRelocs(self) -> int: 79 | return len(self.entries) 80 | 81 | @property 82 | def textSize(self) -> int: 83 | return self.sectionSizes[common.FileSectionType.Text] 84 | @property 85 | def dataSize(self) -> int: 86 | return self.sectionSizes[common.FileSectionType.Data] 87 | @property 88 | def rodataSize(self) -> int: 89 | return self.sectionSizes[common.FileSectionType.Rodata] 90 | @property 91 | def bssSize(self) -> int: 92 | return self.sectionSizes[common.FileSectionType.Bss] 93 | 94 | 95 | def analyze(self) -> None: 96 | # The name may be a path, so we take the name of the file and discard everything else 97 | relocName = Path(self.name).stem 98 | 99 | localOffset = 0 100 | 101 | sym: symbols.SymbolBase 102 | 103 | currentVram = self.getVramOffset(localOffset) 104 | vrom = self.getVromOffset(localOffset) 105 | vromEnd = vrom + 4 * 4 106 | sym = symbols.SymbolData(self.context, vrom, vromEnd, localOffset + self.inFileOffset, currentVram, self.words[0:4], self.segmentVromStart, self.overlayCategory) 107 | sym.contextSym.name = f"{relocName}_OverlayInfo" 108 | sym.contextSym.userDeclaredType = "s32" 109 | sym.contextSym.allowedToReferenceSymbols = False 110 | sym.contextSym.allowedToBeReferenced = False 111 | sym.parent = self 112 | sym.setCommentOffset(self.commentOffset) 113 | sym.endOfLineComment = {i: f" /* _{relocName}Segment{sectName.toCapitalizedStr()}Size */" for i, sectName in enumerate(common.FileSections_ListBasic)} 114 | sym.analyze() 115 | self.symbolList.append(sym) 116 | localOffset += 4 * 4 117 | 118 | currentVram = self.getVramOffset(localOffset) 119 | vrom = self.getVromOffset(localOffset) 120 | vromEnd = vrom + 4 121 | sym = symbols.SymbolData(self.context, vrom, vromEnd, localOffset + self.inFileOffset, currentVram, [self.relocCount], self.segmentVromStart, self.overlayCategory) 122 | sym.contextSym.name = f"{relocName}_RelocCount" 123 | sym.contextSym.userDeclaredType = "s32" 124 | sym.contextSym.allowedToReferenceSymbols = False 125 | sym.contextSym.allowedToBeReferenced = False 126 | sym.parent = self 127 | sym.setCommentOffset(self.commentOffset) 128 | sym.analyze() 129 | self.symbolList.append(sym) 130 | localOffset += 4 131 | 132 | currentVram = self.getVramOffset(localOffset) 133 | vrom = self.getVromOffset(localOffset) 134 | vromEnd = vrom + 4 * len(self.entries) 135 | sym = symbols.SymbolData(self.context, vrom, vromEnd, localOffset + self.inFileOffset, currentVram, [r.reloc for r in self.entries], self.segmentVromStart, self.overlayCategory) 136 | sym.contextSym.name = f"{relocName}_OverlayRelocations" 137 | sym.contextSym.userDeclaredType = "s32" 138 | sym.contextSym.allowedToReferenceSymbols = False 139 | sym.contextSym.allowedToBeReferenced = False 140 | sym.parent = self 141 | sym.setCommentOffset(self.commentOffset) 142 | sym.endOfLineComment = {i: f" /* {str(r)} */" for i, r in enumerate(self.entries)} 143 | sym.analyze() 144 | self.symbolList.append(sym) 145 | localOffset += 4 * len(self.entries) 146 | 147 | if len(self.tail) > 0: 148 | currentVram = self.getVramOffset(localOffset) 149 | vrom = self.getVromOffset(localOffset) 150 | vromEnd = vrom + 4 * len(self.tail) 151 | sym = symbols.SymbolData(self.context, vrom, vromEnd, localOffset + self.inFileOffset, currentVram, self.tail, self.segmentVromStart, self.overlayCategory) 152 | sym.contextSym.name = f"{relocName}_Padding" 153 | sym.contextSym.userDeclaredType = "s32" 154 | sym.contextSym.allowedToReferenceSymbols = False 155 | sym.contextSym.allowedToBeReferenced = False 156 | sym.parent = self 157 | sym.setCommentOffset(self.commentOffset) 158 | sym.analyze() 159 | self.symbolList.append(sym) 160 | localOffset += 4 * len(self.tail) 161 | 162 | currentVram = self.getVramOffset(localOffset) 163 | vrom = self.getVromOffset(localOffset) 164 | vromEnd = vrom + 4 165 | sym = symbols.SymbolData(self.context, vrom, vromEnd, localOffset + self.inFileOffset, currentVram, [self.seekup], self.segmentVromStart, self.overlayCategory) 166 | sym.contextSym.name = f"{relocName}_OverlayInfoOffset" 167 | sym.contextSym.userDeclaredType = "s32" 168 | sym.contextSym.allowedToReferenceSymbols = False 169 | sym.contextSym.allowedToBeReferenced = False 170 | sym.parent = self 171 | sym.setCommentOffset(self.commentOffset) 172 | sym.analyze() 173 | self.symbolList.append(sym) 174 | 175 | for sym in self.symbolList: 176 | if sym.vram is not None: 177 | self.symbolsVRams.add(sym.vram) 178 | -------------------------------------------------------------------------------- /spimdisasm/mips/sections/MipsSectionRodata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import rabbitizer 9 | 10 | from ... import common 11 | 12 | from .. import symbols 13 | 14 | from . import SectionBase 15 | 16 | 17 | class SectionRodata(SectionBase): 18 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, vram: int, filename: str, array_of_bytes: bytes, segmentVromStart: int, overlayCategory: str|None) -> None: 19 | if common.GlobalConfig.ENDIAN_RODATA is not None: 20 | words = common.Utils.endianessBytesToWords(common.GlobalConfig.ENDIAN_RODATA, array_of_bytes, vromStart, vromEnd) 21 | else: 22 | words = common.Utils.bytesToWords(array_of_bytes, vromStart, vromEnd) 23 | super().__init__(context, vromStart, vromEnd, vram, filename, words, common.FileSectionType.Rodata, segmentVromStart, overlayCategory) 24 | 25 | self.stringEncoding = common.GlobalConfig.RODATA_STRING_ENCODING 26 | 27 | 28 | def _analyze_processJumptable(self, localOffset: int, w: int, contextSym: common.ContextSymbol|None, lastVramSymbol: common.ContextSymbol, jumpTableSym: common.ContextSymbol|None, firstJumptableWord: int) -> tuple[common.ContextSymbol|None, int]: 29 | if contextSym is not None and contextSym.isJumpTable(): 30 | # New jumptable 31 | jumpTableSym = contextSym 32 | firstJumptableWord = w 33 | 34 | elif jumpTableSym is not None: 35 | # The last symbol found was part of a jumptable, check if this word still is part of the jumptable 36 | 37 | if localOffset not in self.pointersOffsets: 38 | if w == 0: 39 | return None, firstJumptableWord 40 | 41 | elif contextSym is not None: 42 | return None, firstJumptableWord 43 | 44 | elif ((w >> 24) & 0xFF) != ((firstJumptableWord >> 24) & 0xFF): 45 | if not ( 46 | lastVramSymbol.isJumpTable() 47 | and lastVramSymbol.isGot 48 | and common.GlobalConfig.GP_VALUE is not None 49 | ): 50 | return None, firstJumptableWord 51 | else: 52 | # No jumptable 53 | return None, firstJumptableWord 54 | 55 | # Generate the current label 56 | if lastVramSymbol.isGot and common.GlobalConfig.GP_VALUE is not None: 57 | labelAddr = common.GlobalConfig.GP_VALUE + rabbitizer.Utils.from2Complement(w, 32) 58 | labelVrom = None 59 | else: 60 | labelAddr = w 61 | maybeVrom = self.vromStart + labelAddr - self.vram 62 | segment = self.getSegmentForVrom(self.segmentVromStart) 63 | if not segment._isTheUnknownSegment and segment.isVromInRange(maybeVrom): 64 | labelVrom = maybeVrom 65 | else: 66 | labelVrom = None 67 | labelSym = self.addJumpTableLabel(labelAddr, isAutogenerated=True, symbolVrom=labelVrom) 68 | 69 | if labelSym.unknownSegment: 70 | return None, firstJumptableWord 71 | 72 | labelSym.referenceCounter += 1 73 | if jumpTableSym.parentFunction is not None: 74 | labelSym.parentFunction = jumpTableSym.parentFunction 75 | labelSym.parentFileName = jumpTableSym.parentFunction.parentFileName 76 | jumpTableSym.parentFunction.branchLabels.add(labelSym.vram, labelSym) 77 | 78 | return jumpTableSym, firstJumptableWord 79 | 80 | 81 | def analyze(self) -> None: 82 | lastVramSymbol: common.ContextSymbol = self._checkAndCreateFirstSymbol() 83 | 84 | symbolList: list[tuple[int, common.ContextSymbol]] = [] 85 | localOffset = 0 86 | localOffsetsWithSymbols: set[int] = set() 87 | 88 | needsFurtherAnalyzis = False 89 | 90 | jumpTableSym: common.ContextSymbol|None = None 91 | firstJumptableWord = -1 92 | 93 | for w in self.words: 94 | currentVram = self.getVramOffset(localOffset) 95 | currentVrom = self.getVromOffset(localOffset) 96 | 97 | # Check if we have a symbol at this address, if not then use the last known one 98 | contextSym = self.getSymbol(currentVram, vromAddress=currentVrom, tryPlusOffset=False) 99 | if contextSym is not None: 100 | lastVramSymbol = contextSym 101 | 102 | jumpTableSym, firstJumptableWord = self._analyze_processJumptable(localOffset, w, contextSym, lastVramSymbol, jumpTableSym, firstJumptableWord) 103 | 104 | if jumpTableSym is None: 105 | if contextSym is not None or self.popPointerInDataReference(currentVram) is not None or (lastVramSymbol.isJumpTable() and w != 0): 106 | contextSym = self._addOwnedSymbol(localOffset) 107 | if contextSym is not None: 108 | lastVramSymbol = contextSym 109 | 110 | self.checkWordIsASymbolReference(w) 111 | 112 | if contextSym is not None: 113 | symbolList.append((localOffset, contextSym)) 114 | localOffsetsWithSymbols.add(localOffset) 115 | 116 | self._createAutoPadFromSymbol(localOffset, contextSym) 117 | 118 | elif jumpTableSym is None and self.popPointerInDataReference(currentVram) is not None: 119 | contextSym = self._addOwnedSymbol(localOffset) 120 | if contextSym is not None: 121 | symbolList.append((localOffset, contextSym)) 122 | localOffsetsWithSymbols.add(localOffset) 123 | 124 | if not lastVramSymbol.notPointerByType(): 125 | if self.checkWordIsASymbolReference(w): 126 | if w < currentVram and self.containsVram(w): 127 | # References a data symbol from this section and it is behind this current symbol 128 | needsFurtherAnalyzis = True 129 | 130 | localOffset += 4 131 | 132 | if needsFurtherAnalyzis: 133 | localOffset = 0 134 | for w in self.words: 135 | currentVram = self.getVramOffset(localOffset) 136 | 137 | if self.popPointerInDataReference(currentVram) is not None and localOffset not in localOffsetsWithSymbols: 138 | contextSym = self._addOwnedSymbol(localOffset) 139 | if contextSym is not None: 140 | symbolList.append((localOffset, contextSym)) 141 | localOffsetsWithSymbols.add(localOffset) 142 | 143 | localOffset += 4 144 | 145 | # Since we appended new symbols, this list is not sorted anymore 146 | symbolList.sort() 147 | 148 | previousSymbolWasLateRodata = False 149 | previousSymbolExtraPadding = 0 150 | sectionAlign_rodata = common.GlobalConfig.COMPILER.value.sectionAlign_rodata 151 | rodataAlignment = 1 << sectionAlign_rodata if sectionAlign_rodata is not None else None 152 | 153 | for i, (offset, contextSym) in enumerate(symbolList): 154 | if i + 1 == len(symbolList): 155 | words = self.words[offset//4:] 156 | else: 157 | nextOffset = symbolList[i+1][0] 158 | words = self.words[offset//4:nextOffset//4] 159 | 160 | vrom = self.getVromOffset(offset) 161 | vromEnd = vrom + len(words)*4 162 | sym = symbols.SymbolRodata(self.context, vrom, vromEnd, offset + self.inFileOffset, contextSym.vram, words, self.segmentVromStart, self.overlayCategory) 163 | sym.parent = self 164 | sym.setCommentOffset(self.commentOffset) 165 | sym.stringEncoding = self.stringEncoding 166 | sym.analyze() 167 | self.symbolList.append(sym) 168 | self.symbolsVRams.add(contextSym.vram) 169 | 170 | if rodataAlignment is not None: 171 | # Section boundaries detection 172 | if (self.vromStart + sym.inFileOffset) % rodataAlignment == 0: 173 | if previousSymbolWasLateRodata and not sym.contextSym.isLateRodata(): 174 | # late rodata followed by normal rodata implies a file split 175 | self.fileBoundaries.append(sym.inFileOffset) 176 | elif previousSymbolExtraPadding > 0: 177 | if sym.isDouble(0): 178 | # doubles require a bit extra of alignment 179 | if previousSymbolExtraPadding >= 2: 180 | self.fileBoundaries.append(sym.inFileOffset) 181 | elif sym.isJumpTable(): 182 | if common.GlobalConfig.COMPILER.value.prevAlign_jumptable is not None and common.GlobalConfig.COMPILER.value.prevAlign_jumptable >= 3: 183 | if previousSymbolExtraPadding >= 2: 184 | self.fileBoundaries.append(sym.inFileOffset) 185 | elif sym.isString(): 186 | if common.GlobalConfig.COMPILER.value.prevAlign_string is not None and common.GlobalConfig.COMPILER.value.prevAlign_string >= 3: 187 | if previousSymbolExtraPadding >= 2: 188 | self.fileBoundaries.append(sym.inFileOffset) 189 | else: 190 | self.fileBoundaries.append(sym.inFileOffset) 191 | 192 | previousSymbolWasLateRodata = sym.contextSym.isLateRodata() 193 | previousSymbolExtraPadding = sym.countExtraPadding() 194 | 195 | self.processStaticRelocs() 196 | 197 | # Filter out repeated values and sort 198 | self.fileBoundaries = sorted(set(self.fileBoundaries)) 199 | 200 | 201 | def removePointers(self) -> bool: 202 | if not common.GlobalConfig.REMOVE_POINTERS: 203 | return False 204 | 205 | was_updated = super().removePointers() 206 | for i in range(self.sizew): 207 | top_byte = (self.words[i] >> 24) & 0xFF 208 | if top_byte == 0x80: 209 | self.words[i] = top_byte << 24 210 | was_updated = True 211 | if (top_byte & 0xF0) == 0x00 and (top_byte & 0x0F) != 0x00: 212 | self.words[i] = top_byte << 24 213 | was_updated = True 214 | 215 | return was_updated 216 | -------------------------------------------------------------------------------- /spimdisasm/mips/sections/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from .MipsSectionBase import SectionBase as SectionBase 9 | 10 | from .MipsSectionText import SectionText as SectionText 11 | from .MipsSectionData import SectionData as SectionData 12 | from .MipsSectionRodata import SectionRodata as SectionRodata 13 | from .MipsSectionBss import SectionBss as SectionBss 14 | from .MipsSectionRelocZ64 import SectionRelocZ64 as SectionRelocZ64 15 | from .MipsSectionRelocZ64 import RelocEntry as RelocEntry 16 | from .MipsSectionGccExceptTable import SectionGccExceptTable as SectionGccExceptTable 17 | -------------------------------------------------------------------------------- /spimdisasm/mips/symbols/MipsSymbolBss.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from ... import common 9 | 10 | from . import SymbolBase 11 | 12 | 13 | class SymbolBss(SymbolBase): 14 | def __init__( 15 | self, 16 | context: common.Context, 17 | vromStart: int, 18 | vromEnd: int, 19 | inFileOffset: int, 20 | vram: int, 21 | spaceSize: int, 22 | segmentVromStart: int, 23 | overlayCategory: str | None, 24 | ): 25 | super().__init__( 26 | context, 27 | vromStart, 28 | vromEnd, 29 | inFileOffset, 30 | vram, 31 | list(), 32 | common.FileSectionType.Bss, 33 | segmentVromStart, 34 | overlayCategory, 35 | ) 36 | 37 | self.spaceSize: int = spaceSize 38 | 39 | @property 40 | def sizew(self) -> int: 41 | return self.spaceSize // 4 42 | 43 | def analyze(self) -> None: 44 | super().analyze() 45 | 46 | if self.contextSym.hasUserDeclaredSize(): 47 | # Check user declared size matches the size that will be generated 48 | contextSymSize = self.contextSym.getSize() 49 | if self.spaceSize != contextSymSize: 50 | warningMessage = f""" 51 | Range check triggered: .bss symbol (name: {self.getName()}, address: 0x{self.contextSym.vram:08X}): 52 | User declared size (0x{contextSymSize:X}) does not match the .space that will be emitted (0x{self.spaceSize:X}). 53 | Try checking the size again or look for symbols which overlaps this region""" 54 | if common.GlobalConfig.PANIC_RANGE_CHECK: 55 | assert self.spaceSize == contextSymSize, warningMessage 56 | else: 57 | common.Utils.eprint(f"\n{warningMessage}\n") 58 | 59 | def disassembleAsBss(self, useGlobalLabel: bool = True) -> str: 60 | output = self.contextSym.getReferenceeSymbols() 61 | output += self.getPrevAlignDirective(0) 62 | 63 | output += self.getSymbolAsmDeclaration(self.getName(), useGlobalLabel) 64 | output += self.generateAsmLineComment(0, emitRomOffset=False) 65 | output += f" .space 0x{self.spaceSize:02X}{common.GlobalConfig.LINE_ENDS}" 66 | 67 | nameEnd = self.getNameEnd() 68 | if nameEnd is not None: 69 | output += self.getSymbolAsmDeclaration(nameEnd, useGlobalLabel) 70 | 71 | return output 72 | 73 | def disassemble( 74 | self, 75 | migrate: bool = False, 76 | useGlobalLabel: bool = True, 77 | isSplittedSymbol: bool = False, 78 | ) -> str: 79 | return self.disassembleAsBss() 80 | -------------------------------------------------------------------------------- /spimdisasm/mips/symbols/MipsSymbolData.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from ... import common 9 | 10 | from . import SymbolBase 11 | 12 | 13 | class SymbolData(SymbolBase): 14 | def __init__( 15 | self, 16 | context: common.Context, 17 | vromStart: int, 18 | vromEnd: int, 19 | inFileOffset: int, 20 | vram: int, 21 | words: list[int], 22 | segmentVromStart: int, 23 | overlayCategory: str | None, 24 | ): 25 | super().__init__( 26 | context, 27 | vromStart, 28 | vromEnd, 29 | inFileOffset, 30 | vram, 31 | words, 32 | common.FileSectionType.Data, 33 | segmentVromStart, 34 | overlayCategory, 35 | ) 36 | 37 | def analyze(self) -> None: 38 | super().analyze() 39 | 40 | if self.contextSym.hasUserDeclaredSize(): 41 | # Check user declared size matches the size that will be generated 42 | contextSymSize = self.contextSym.getSize() 43 | actualSize = self.sizew * 4 44 | if actualSize < contextSymSize: 45 | warningMessage = f""" 46 | Range check triggered: .data symbol (name: {self.getName()}, address: 0x{self.contextSym.vram:08X}): 47 | User declared size (0x{contextSymSize:X}) does not match the amount of bytes that will be emitted (0x{actualSize:X}). 48 | Try checking the size again or look for symbols which overlaps this region""" 49 | if common.GlobalConfig.PANIC_RANGE_CHECK: 50 | assert actualSize == contextSymSize, warningMessage 51 | else: 52 | common.Utils.eprint(f"\n{warningMessage}\n") 53 | -------------------------------------------------------------------------------- /spimdisasm/mips/symbols/MipsSymbolGccExceptTable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from ... import common 9 | 10 | from . import SymbolBase 11 | 12 | 13 | class SymbolGccExceptTable(SymbolBase): 14 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, inFileOffset: int, vram: int, words: list[int], segmentVromStart: int, overlayCategory: str|None) -> None: 15 | super().__init__(context, vromStart, vromEnd, inFileOffset, vram, words, common.FileSectionType.GccExceptTable, segmentVromStart, overlayCategory) 16 | -------------------------------------------------------------------------------- /spimdisasm/mips/symbols/MipsSymbolRodata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from ... import common 9 | 10 | from . import SymbolBase 11 | 12 | 13 | class SymbolRodata(SymbolBase): 14 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, inFileOffset: int, vram: int, words: list[int], segmentVromStart: int, overlayCategory: str|None) -> None: 15 | super().__init__(context, vromStart, vromEnd, inFileOffset, vram, words, common.FileSectionType.Rodata, segmentVromStart, overlayCategory) 16 | 17 | self.stringEncoding = common.GlobalConfig.RODATA_STRING_ENCODING 18 | 19 | def isJumpTable(self) -> bool: 20 | # jumptables must have at least 3 labels 21 | if self.sizew < 3: 22 | return False 23 | return self.contextSym.isJumpTable() 24 | 25 | 26 | def isMaybeConstVariable(self) -> bool: 27 | if self.isFloat(0): 28 | if self.sizew > 1: 29 | for w in self.words[1:]: 30 | if w != 0: 31 | return True 32 | return False 33 | elif self.isDouble(0): 34 | if self.sizew > 2: 35 | for w in self.words[2:]: 36 | if w != 0: 37 | return True 38 | return False 39 | elif self.isJumpTable(): 40 | return False 41 | elif self.isString(): 42 | return False 43 | elif self.isPascalString(): 44 | return False 45 | return True 46 | 47 | #! @deprecated 48 | def isRdata(self) -> bool: 49 | "Checks if the current symbol is .rdata" 50 | if self.isMaybeConstVariable(): 51 | return True 52 | 53 | # This symbol could be an unreferenced non-const variable 54 | if len(self.contextSym.referenceFunctions) == 1: 55 | # This const variable was already used in a function 56 | return False 57 | 58 | return True 59 | 60 | def shouldMigrate(self) -> bool: 61 | if self.contextSym.functionOwnerForMigration is not None: 62 | return True 63 | 64 | if self.contextSym.forceMigration: 65 | return True 66 | 67 | if self.contextSym.forceNotMigration: 68 | return False 69 | 70 | if self.contextSym.isMips1Double: 71 | return True 72 | 73 | if len(self.contextSym.referenceSymbols) > 0: 74 | return False 75 | if len(self.contextSym.referenceFunctions) > 1: 76 | return False 77 | 78 | if self.isMaybeConstVariable(): 79 | if common.GlobalConfig.ALLOW_MIGRATING_CONST_VARIABLES: 80 | return True 81 | if not common.GlobalConfig.COMPILER.value.allowRdataMigration: 82 | return False 83 | 84 | return True 85 | 86 | 87 | def analyze(self) -> None: 88 | if self.contextSym.isDouble(): 89 | if self.sizew % 2 != 0: 90 | # doubles require an even amount of words 91 | self.contextSym.setTypeSpecial(None, isAutogenerated=True) 92 | else: 93 | for i in range(self.sizew // 2): 94 | if not self.isDouble(i*2): 95 | # checks there's no other overlaping symbols 96 | self.contextSym.setTypeSpecial(None, isAutogenerated=True) 97 | break 98 | 99 | super().analyze() 100 | 101 | 102 | def countExtraPadding(self) -> int: 103 | if self.contextSym.hasUserDeclaredSize(): 104 | if self.sizew * 4 == self.contextSym.getSize(): 105 | return 0 106 | 107 | count = 0 108 | if self.isString(): 109 | for i in range(len(self.words)-1, 0, -1): 110 | if self.words[i] != 0: 111 | break 112 | if (self.words[i-1] & 0x000000FF) != 0: 113 | break 114 | count += 1 115 | elif self.isDouble(0): 116 | for i in range(len(self.words)-1, 0, -2): 117 | if self.words[i] != 0 or self.words[i-1] != 0: 118 | break 119 | count += 2 120 | else: 121 | for i in range(len(self.words)-1, 0, -1): 122 | if self.words[i] != 0: 123 | break 124 | count += 1 125 | return count 126 | -------------------------------------------------------------------------------- /spimdisasm/mips/symbols/MipsSymbolText.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from ... import common 9 | 10 | from . import SymbolBase 11 | 12 | 13 | class SymbolText(SymbolBase): 14 | def __init__(self, context: common.Context, vromStart: int, vromEnd: int, inFileOffset: int, vram: int, words: list[int], segmentVromStart: int, overlayCategory: str|None) -> None: 15 | super().__init__(context, vromStart, vromEnd, inFileOffset, vram, words, common.FileSectionType.Text, segmentVromStart, overlayCategory) 16 | -------------------------------------------------------------------------------- /spimdisasm/mips/symbols/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from . import analysis 9 | 10 | from .MipsSymbolBase import SymbolBase as SymbolBase 11 | 12 | from .MipsSymbolText import SymbolText as SymbolText 13 | from .MipsSymbolData import SymbolData as SymbolData 14 | from .MipsSymbolRodata import SymbolRodata as SymbolRodata 15 | from .MipsSymbolBss import SymbolBss as SymbolBss 16 | from .MipsSymbolGccExceptTable import SymbolGccExceptTable as SymbolGccExceptTable 17 | 18 | from .MipsSymbolFunction import SymbolFunction as SymbolFunction 19 | -------------------------------------------------------------------------------- /spimdisasm/mips/symbols/analysis/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from .InstrAnalyzer import InstrAnalyzer as InstrAnalyzer 9 | -------------------------------------------------------------------------------- /spimdisasm/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Decompollaborate/spimdisasm/888bc5058069c8b5f46bc6e32fb79841870f78d7/spimdisasm/py.typed -------------------------------------------------------------------------------- /spimdisasm/rspDisasm/RspDisasmInternals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import argparse 9 | import rabbitizer 10 | from pathlib import Path 11 | 12 | from .. import common 13 | from .. import mips 14 | 15 | from .. import __version__ 16 | 17 | PROGNAME = "rspDisasm" 18 | 19 | 20 | def getToolDescription() -> str: 21 | return "N64 RSP disassembler" 22 | 23 | def addOptionsToParser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: 24 | parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}") 25 | 26 | parser.add_argument("binary", help="Path to input binary") 27 | parser.add_argument("output", help="Path to output. Use '-' to print to stdout instead") 28 | 29 | parser.add_argument("--start", help="Raw offset of the input binary file to start disassembling. Expects an hex value", default="0") 30 | parser.add_argument("--end", help="Offset end of the input binary file to start disassembling. Expects an hex value", default="0xFFFFFF") 31 | parser.add_argument("--vram", help="Set the VRAM address. Expects an hex value", default="0x0") 32 | 33 | common.Context.addParametersToArgParse(parser) 34 | 35 | common.GlobalConfig.addParametersToArgParse(parser) 36 | 37 | mips.InstructionConfig.addParametersToArgParse(parser) 38 | 39 | return parser 40 | 41 | def getArgsParser() -> argparse.ArgumentParser: 42 | parser = argparse.ArgumentParser(description=getToolDescription(), prog=PROGNAME, formatter_class=common.Utils.PreserveWhiteSpaceWrapRawTextHelpFormatter) 43 | return addOptionsToParser(parser) 44 | 45 | 46 | def applyArgs(args: argparse.Namespace) -> None: 47 | common.GlobalConfig.parseArgs(args) 48 | mips.InstructionConfig.parseArgs(args) 49 | 50 | def applyGlobalConfigurations() -> None: 51 | common.GlobalConfig.GLABEL_ASM_COUNT = False 52 | common.GlobalConfig.ASM_TEXT_FUNC_AS_LABEL = True 53 | common.GlobalConfig.ASM_USE_PRELUDE = False 54 | common.GlobalConfig.ASM_USE_SYMBOL_LABEL = False 55 | rabbitizer.config.misc_unknownInstrComment = False 56 | 57 | def initializeContext(args: argparse.Namespace, fileSize: int, fileVram: int) -> common.Context: 58 | context = common.Context() 59 | context.parseArgs(args) 60 | 61 | highestVromEnd = fileSize 62 | lowestVramStart = 0x00000000 63 | highestVramEnd = lowestVramStart + highestVromEnd 64 | if fileVram != 0: 65 | lowestVramStart = fileVram 66 | highestVramEnd = fileVram + highestVromEnd 67 | 68 | context.changeGlobalSegmentRanges(0, highestVromEnd, lowestVramStart, highestVramEnd) 69 | return context 70 | 71 | 72 | def processArguments(args: argparse.Namespace) -> int: 73 | applyArgs(args) 74 | 75 | applyGlobalConfigurations() 76 | 77 | binaryPath = Path(args.binary) 78 | array_of_bytes = common.Utils.readFileAsBytearray(binaryPath) 79 | inputName = binaryPath.stem 80 | 81 | start = int(args.start, 16) 82 | end = int(args.end, 16) 83 | if end == 0xFFFFFF: 84 | end = len(array_of_bytes) 85 | fileVram = int(args.vram, 16) 86 | 87 | context = initializeContext(args, len(array_of_bytes), fileVram) 88 | 89 | f = mips.sections.SectionText(context, start, end, fileVram, inputName, array_of_bytes, 0, None) 90 | f.instrCat = rabbitizer.InstrCategory.RSP 91 | 92 | f.analyze() 93 | f.printAnalyzisResults() 94 | 95 | mips.FilesHandlers.writeSection(Path(args.output), f) 96 | 97 | if args.save_context is not None: 98 | contextPath = Path(args.save_context) 99 | contextPath.parent.mkdir(parents=True, exist_ok=True) 100 | context.saveContextToFile(contextPath) 101 | 102 | return 0 103 | 104 | def addSubparser(subparser: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: 105 | parser = subparser.add_parser("rspDisasm", help=getToolDescription(), formatter_class=common.Utils.PreserveWhiteSpaceWrapRawTextHelpFormatter) 106 | 107 | addOptionsToParser(parser) 108 | 109 | parser.set_defaults(func=processArguments) 110 | 111 | 112 | def rspDisasmMain() -> int: 113 | args = getArgsParser().parse_args() 114 | 115 | return processArguments(args) 116 | -------------------------------------------------------------------------------- /spimdisasm/rspDisasm/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | 9 | from .RspDisasmInternals import getToolDescription 10 | from .RspDisasmInternals import addOptionsToParser 11 | from .RspDisasmInternals import getArgsParser 12 | from .RspDisasmInternals import applyArgs 13 | from .RspDisasmInternals import initializeContext 14 | from .RspDisasmInternals import processArguments 15 | from .RspDisasmInternals import addSubparser 16 | from .RspDisasmInternals import rspDisasmMain 17 | -------------------------------------------------------------------------------- /spimdisasm/rspDisasm/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from . import rspDisasmMain 9 | 10 | 11 | if __name__ == "__main__": 12 | rspDisasmMain() 13 | -------------------------------------------------------------------------------- /spimdisasm/singleFileDisasm/SingleFileDisasmInternals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | import argparse 9 | from pathlib import Path 10 | 11 | from .. import common 12 | from .. import mips 13 | from .. import frontendCommon as fec 14 | 15 | from .. import __version__ 16 | 17 | PROGNAME = "singleFileDisasm" 18 | 19 | 20 | def getToolDescription() -> str: 21 | return "General purpose N64-mips disassembler" 22 | 23 | def addOptionsToParser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: 24 | parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}") 25 | 26 | parser.add_argument("binary", help="Path to input binary") 27 | parser.add_argument("output", help="Path to output. Use '-' to print to stdout instead") 28 | 29 | parser.add_argument("--data-output", help="Path to output the data and rodata disassembly") 30 | 31 | parser_singleFile = parser.add_argument_group("Single file disassembly options") 32 | 33 | parser_singleFile.add_argument("--start", help="Raw offset of the input binary file to start disassembling the .text section. Expects an hex value", default="0") 34 | parser_singleFile.add_argument("--end", help="Offset end of the input binary file to start disassembling the .text section. Expects an hex value", default="0xFFFFFF") 35 | parser_singleFile.add_argument("--vram", help="Set the VRAM address. Expects an hex value", default="0x0") 36 | parser_singleFile.add_argument("--data-start", help="Raw offset of the input binary file to start disassembling the .data section. Expects an hex value. Requires --data-end", default=None) 37 | parser_singleFile.add_argument("--data-end", help="Offset end of the input binary file to start disassembling the .data section. Expects an hex value. Requires --data-start", default=None) 38 | 39 | parser_singleFile.add_argument("--disasm-rsp", help=f"Experimental. Disassemble this file using rsp ABI instructions. Warning: In its current state the generated asm may not be assemblable to a matching binary. Defaults to False", action="store_true") 40 | 41 | parser.add_argument("--file-splits", help="Path to a file splits csv") 42 | 43 | parser.add_argument("--split-functions", help="Enables the function and rodata splitter. Expects a path to place the splited functions", metavar="PATH") 44 | 45 | parser.add_argument("--instr-category", help="The instruction category to use when disassembling every passed instruction. Defaults to 'cpu'", choices=["cpu", "rsp", "r3000gte", "r4000allegrex", "r5900"]) 46 | 47 | parser.add_argument("--nuke-pointers", help="Use every technique available to remove pointers", action=common.Utils.BooleanOptionalAction) 48 | parser.add_argument("--ignore-words", help="A space separated list of hex numbers. Any word differences which starts in any of the provided arguments will be ignored. Max value: FF. Only works when --nuke-pointers is passed", action="extend", nargs="+") 49 | 50 | parser.add_argument("--write-binary", help=f"Produce a binary from the processed file. Defaults to {common.GlobalConfig.WRITE_BINARY}", action=common.Utils.BooleanOptionalAction) 51 | 52 | parser.add_argument("--function-info", help="Specifies a path where to output a csvs sumary file of every analyzed function", metavar="PATH") 53 | 54 | 55 | common.Context.addParametersToArgParse(parser) 56 | 57 | common.GlobalConfig.addParametersToArgParse(parser) 58 | 59 | mips.InstructionConfig.addParametersToArgParse(parser) 60 | 61 | return parser 62 | 63 | def getArgsParser() -> argparse.ArgumentParser: 64 | parser = argparse.ArgumentParser(description=getToolDescription(), prog=PROGNAME, formatter_class=common.Utils.PreserveWhiteSpaceWrapRawTextHelpFormatter) 65 | return addOptionsToParser(parser) 66 | 67 | def applyArgs(args: argparse.Namespace) -> None: 68 | common.GlobalConfig.parseArgs(args) 69 | 70 | mips.InstructionConfig.parseArgs(args) 71 | 72 | common.GlobalConfig.REMOVE_POINTERS = args.nuke_pointers 73 | common.GlobalConfig.IGNORE_BRANCHES = args.nuke_pointers 74 | if args.nuke_pointers: 75 | common.GlobalConfig.IGNORE_WORD_LIST.add(0x80) 76 | if args.ignore_words: 77 | for upperByte in args.ignore_words: 78 | common.GlobalConfig.IGNORE_WORD_LIST.add(int(upperByte, 16)) 79 | if args.write_binary is not None: 80 | common.GlobalConfig.WRITE_BINARY = args.write_binary 81 | 82 | def applyGlobalConfigurations() -> None: 83 | common.GlobalConfig.PRODUCE_SYMBOLS_PLUS_OFFSET = True 84 | common.GlobalConfig.TRUST_USER_FUNCTIONS = True 85 | 86 | def getSplits(fileSplitsPath: Path|None, vromStart: int, vromEnd: int, fileVram: int, vromDataStart: int|None, vromDataEnd: int|None, disasmRsp: bool) -> common.FileSplitFormat: 87 | splits = common.FileSplitFormat() 88 | if fileSplitsPath is not None: 89 | splits.readCsvFile(fileSplitsPath) 90 | 91 | if len(splits) == 0: 92 | if fileSplitsPath is not None: 93 | common.Utils.eprint("Warning: Tried to use file split mode, but passed csv splits file was empty") 94 | common.Utils.eprint("\t Using single-file mode instead") 95 | 96 | endVram = fileVram + vromEnd - vromStart 97 | 98 | splitEntry = common.FileSplitEntry(vromStart, fileVram, "", common.FileSectionType.Text, vromEnd, False, disasmRsp) 99 | splits.append(splitEntry) 100 | 101 | if vromDataStart is not None and vromDataEnd is not None: 102 | dataVramStart = endVram 103 | endVram = dataVramStart + vromDataEnd - vromDataStart 104 | vromEnd = vromDataEnd 105 | 106 | splitEntry = common.FileSplitEntry(vromDataStart, dataVramStart, "", common.FileSectionType.Data, vromDataEnd, False, disasmRsp) 107 | splits.append(splitEntry) 108 | 109 | splits.appendEndSection(vromEnd, endVram) 110 | 111 | return splits 112 | 113 | 114 | def changeGlobalSegmentRanges(context: common.Context, processedFiles: dict[common.FileSectionType, list[mips.sections.SectionBase]], fileSize: int, fileVram: int) -> None: 115 | highestVromEnd = fileSize 116 | lowestVramStart = 0x80000000 117 | highestVramEnd = lowestVramStart + highestVromEnd 118 | if fileVram != 0: 119 | lowestVramStart = fileVram 120 | highestVramEnd = (fileVram & 0xF0000000) + highestVromEnd 121 | 122 | for filesInSection in processedFiles.values(): 123 | for mipsSection in filesInSection: 124 | if lowestVramStart is None or mipsSection.vram < lowestVramStart: 125 | lowestVramStart = mipsSection.vram 126 | if highestVramEnd is None or mipsSection.vramEnd > highestVramEnd: 127 | highestVramEnd = mipsSection.vramEnd 128 | 129 | if lowestVramStart is None: 130 | lowestVramStart = 0x0 131 | if highestVramEnd is None: 132 | highestVramEnd = 0xFFFFFFFF 133 | context.changeGlobalSegmentRanges(0, highestVromEnd, lowestVramStart, highestVramEnd) 134 | return 135 | 136 | 137 | def processArguments(args: argparse.Namespace) -> int: 138 | applyArgs(args) 139 | 140 | applyGlobalConfigurations() 141 | 142 | context = common.Context() 143 | context.parseArgs(args) 144 | 145 | inputPath = Path(args.binary) 146 | textOutput = Path(args.output) 147 | 148 | common.Utils.printQuietless(f"Using input file: '{inputPath}'") 149 | common.Utils.printQuietless(f"Using output directory: '{textOutput}'") 150 | 151 | if not inputPath.exists(): 152 | common.Utils.eprint(f"ERROR: Input file '{inputPath}' does not exist") 153 | return 1 154 | 155 | if textOutput.exists() and not textOutput.is_dir(): 156 | common.Utils.eprint(f"ERROR: '{textOutput}' is not a valid directory") 157 | return 2 158 | 159 | array_of_bytes = common.Utils.readFileAsBytearray(inputPath) 160 | if len(array_of_bytes) == 0: 161 | common.Utils.eprint(f"ERROR: Input file '{inputPath}' is empty") 162 | return 3 163 | 164 | fileSplitsPath = None 165 | if args.file_splits is not None: 166 | fileSplitsPath = Path(args.file_splits) 167 | vromStart = int(args.start, 16) 168 | vromEnd = int(args.end, 16) 169 | if vromEnd == 0xFFFFFF: 170 | vromEnd = len(array_of_bytes) 171 | vromDataStart = None if args.data_start is None else int(args.data_start, 16) 172 | vromDataEnd = None if args.data_end is None else int(args.data_end, 16) 173 | fileVram = int(args.vram, 16) 174 | splits = getSplits(fileSplitsPath, vromStart, vromEnd, fileVram, vromDataStart, vromDataEnd, args.disasm_rsp) 175 | 176 | if args.data_output is None: 177 | dataOutput = textOutput 178 | else: 179 | dataOutput = Path(args.data_output) 180 | 181 | processedFiles, processedFilesOutputPaths = fec.FrontendUtilities.getSplittedSections(context, splits, array_of_bytes, inputPath, textOutput, dataOutput) 182 | changeGlobalSegmentRanges(context, processedFiles, len(array_of_bytes), int(args.vram, 16)) 183 | 184 | fec.FrontendUtilities.configureProcessedFiles(processedFiles, args.instr_category) 185 | 186 | processedFilesCount = 0 187 | for sect in processedFiles.values(): 188 | processedFilesCount += len(sect) 189 | 190 | progressCallback: fec.FrontendUtilities.ProgressCallbackType 191 | 192 | progressCallback = fec.FrontendUtilities.progressCallback_analyzeProcessedFiles 193 | fec.FrontendUtilities.analyzeProcessedFiles(processedFiles, processedFilesOutputPaths, processedFilesCount, progressCallback) 194 | 195 | if args.nuke_pointers: 196 | common.Utils.printVerbose("Nuking pointers...") 197 | progressCallback = fec.FrontendUtilities.progressCallback_nukePointers 198 | fec.FrontendUtilities.nukePointers(processedFiles, processedFilesOutputPaths, processedFilesCount, progressCallback) 199 | 200 | progressCallback = fec.FrontendUtilities.progressCallback_writeProcessedFiles 201 | fec.FrontendUtilities.writeProcessedFiles(processedFiles, processedFilesOutputPaths, processedFilesCount, progressCallback) 202 | 203 | if args.split_functions is not None: 204 | common.Utils.printVerbose("\nSpliting functions...") 205 | progressCallback = fec.FrontendUtilities.progressCallback_migrateFunctions 206 | fec.FrontendUtilities.migrateFunctions(processedFiles, Path(args.split_functions), progressCallback) 207 | 208 | if args.save_context is not None: 209 | contextPath = Path(args.save_context) 210 | contextPath.parent.mkdir(parents=True, exist_ok=True) 211 | context.saveContextToFile(contextPath) 212 | 213 | if args.function_info is not None: 214 | fec.FrontendUtilities.writeFunctionInfoCsv(processedFiles, Path(args.function_info)) 215 | 216 | common.Utils.printQuietless(500*" " + "\r", end="") 217 | common.Utils.printQuietless(f"Done: {args.binary}") 218 | 219 | common.Utils.printVerbose() 220 | common.Utils.printVerbose("Disassembling complete!") 221 | common.Utils.printVerbose("Goodbye.") 222 | 223 | return 0 224 | 225 | def addSubparser(subparser: argparse._SubParsersAction[argparse.ArgumentParser]) -> None: 226 | parser = subparser.add_parser("singleFileDisasm", help=getToolDescription(), formatter_class=common.Utils.PreserveWhiteSpaceWrapRawTextHelpFormatter) 227 | 228 | addOptionsToParser(parser) 229 | 230 | parser.set_defaults(func=processArguments) 231 | 232 | 233 | def disassemblerMain() -> int: 234 | args = getArgsParser().parse_args() 235 | 236 | return processArguments(args) 237 | -------------------------------------------------------------------------------- /spimdisasm/singleFileDisasm/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | 9 | from .SingleFileDisasmInternals import getToolDescription as getToolDescription 10 | from .SingleFileDisasmInternals import addOptionsToParser as addOptionsToParser 11 | from .SingleFileDisasmInternals import getArgsParser as getArgsParser 12 | from .SingleFileDisasmInternals import applyArgs as applyArgs 13 | from .SingleFileDisasmInternals import applyGlobalConfigurations as applyGlobalConfigurations 14 | from .SingleFileDisasmInternals import getSplits as getSplits 15 | from .SingleFileDisasmInternals import changeGlobalSegmentRanges as changeGlobalSegmentRanges 16 | from .SingleFileDisasmInternals import processArguments as processArguments 17 | from .SingleFileDisasmInternals import addSubparser as addSubparser 18 | from .SingleFileDisasmInternals import disassemblerMain as disassemblerMain 19 | -------------------------------------------------------------------------------- /spimdisasm/singleFileDisasm/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: © 2022-2024 Decompollaborate 4 | # SPDX-License-Identifier: MIT 5 | 6 | from __future__ import annotations 7 | 8 | from . import disassemblerMain 9 | 10 | 11 | if __name__ == "__main__": 12 | disassemblerMain() 13 | --------------------------------------------------------------------------------