Skip to content

Commit

Permalink
Add PPSSPP debugger agent (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
kotcrab committed Oct 23, 2021
1 parent 865c208 commit e9082a8
Show file tree
Hide file tree
Showing 115 changed files with 3,341 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#### Version: 1.9 (built with Ghidra 10.0.4)
- Ghidra Debugger can be used to debug games running in PPSSPP
- Fixed decompilation of `max` and `min`

#### Version: 1.8 (built with Ghidra 10.0.4)
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Features:
- Scripts for importing and exporting PPSSPP `.sym` files (function labels)

In progress:
- Integration with PPSSPP debugger (experimental early stage, not yet released)
- Integration with PPSSPP debugger

### Installation

Expand All @@ -23,10 +23,10 @@ After extracting copy the `Allegrex` directory into `GHIDRA_INSTALL_DIR/Ghidra/P

#### Games

Drag decrypted EBOOT in ELF/PRX format into Ghidra. It should get automatically detected
as `PSP Executable (ELF)` / `Allegrex`. Now is your chance to set initial base address by
clicking `Options...` and changing `Image Base`. I highly recommend you set it to `08804000`.
If you leave it at `0` Ghidra may create many useless labels and references it confuses
Drag decrypted EBOOT in ELF/PRX format into Ghidra. It should get automatically detected
as `PSP Executable (ELF)` / `Allegrex`. Now is your chance to set initial base address by
clicking `Options...` and changing `Image Base`. I highly recommend you set it to `08804000`.
If you leave it at `0` Ghidra may create many useless labels and references it confuses
as memory access. Rebasing the image later is possible but will not remove those labels.

After importing and opening the file you should do the auto analysis. Default options are fine.
Expand All @@ -35,9 +35,9 @@ After importing and opening the file you should do the auto analysis. Default op

PPSSPP identifies many functions automatically, it's useful to get those into Ghidra
after doing the initial analysis. Export the `.sym` file from PPSSPP and in Ghidra run script
`PpssppImportSymFile`. Select the `.sym` file. Enter `0` when asked for offset if your image base is already
`PpssppImportSymFile`. Select the `.sym` file. Enter `0` when asked for offset if your image base is already
at `08804000`.
It's usually fine to run this script after you've started renaming functions in the binary. The script by
It's usually fine to run this script after you've started renaming functions in the binary. The script by
default skips unknown names from PPSSPP so your work can only get overwritten if you've renamed
one of the autodetected function.

Expand Down Expand Up @@ -68,6 +68,7 @@ and set image base.
`GHIDRA_INSTALL_DIR` environment variable must be set to Ghidra root installation directory.

- `./gradlew ghidraInstall` - build and install into Ghidra (warning: contents of `GHIDRA_INSTALL_DIR/Ghidra/Processors/Allegrex` will be deleted before installing)
- `./gradlew ghidraInstallThenRun` - run `ghidraInstall` task then start Ghidra, useful for development
- `./gradlew shadowJar` - create single library jar file with all external dependencies included

After running `./gradlew shadowJar` you can manually install extension by copying:
Expand Down
141 changes: 95 additions & 46 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
buildscript {
ext.kotlinVersion = '1.5.21'
repositories {
mavenCentral()
}
ext.kotlinVersion = '1.5.21'
ext.kotlinCoroutinesVersion = '1.5.1-native-mt'
ext.ktorVersion = '1.6.2'
repositories {
mavenCentral()
}

dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}

plugins {
id "com.github.johnrengelman.shadow" version "7.0.0"
}

repositories {
mavenCentral()
mavenCentral()
}

apply plugin: 'com.github.johnrengelman.shadow'
Expand All @@ -23,59 +25,106 @@ apply plugin: 'kotlin'

def ghidraInstallDir
if (System.env.GHIDRA_INSTALL_DIR) {
ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR
ghidraInstallDir = System.env.GHIDRA_INSTALL_DIR
} else if (project.hasProperty("GHIDRA_INSTALL_DIR")) {
ghidraInstallDir = project.getProperty("GHIDRA_INSTALL_DIR")
ghidraInstallDir = project.getProperty("GHIDRA_INSTALL_DIR")
}
if (!ghidraInstallDir) {
throw new GradleException("GHIDRA_INSTALL_DIR is not defined!")
throw new GradleException("GHIDRA_INSTALL_DIR is not defined!")
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
shadow fileTree(dir: ghidraInstallDir + '/Ghidra/Framework', include: "**/*.jar")
shadow fileTree(dir: ghidraInstallDir + '/Ghidra/Features', include: "**/*.jar")
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinCoroutinesVersion"
implementation "io.ktor:ktor-client-core:$ktorVersion"
implementation "io.ktor:ktor-client-cio:$ktorVersion"
implementation("io.ktor:ktor-client-gson:$ktorVersion") {
exclude group: 'com.google.code.gson', module: 'gson' // already provided by Ghidra
}
implementation "io.ktor:ktor-client-websockets:$ktorVersion"
testImplementation "com.google.code.gson:gson:2.8.7"
testImplementation 'org.apache.logging.log4j:log4j-api:2.14.1'
testImplementation 'org.apache.logging.log4j:log4j-core:2.14.1'
shadow fileTree(dir: ghidraInstallDir + '/Ghidra/Framework', include: "**/*.jar")
shadow fileTree(dir: ghidraInstallDir + '/Ghidra/Features', include: "**/*.jar")
// Debugger
shadow fileTree(dir: ghidraInstallDir + '/Ghidra/Debug/Framework-Debugging', include: "**/*.jar")
shadow fileTree(dir: ghidraInstallDir + '/Ghidra/Debug/Framework-AsyncComm', include: "**/*.jar")
shadow fileTree(dir: ghidraInstallDir + '/Ghidra/Debug/Framework-TraceModeling', include: "**/*.jar")
shadow fileTree(dir: ghidraInstallDir + '/Ghidra/Debug/ProposedUtils', include: "**/*.jar")
shadow fileTree(dir: ghidraInstallDir + '/Ghidra/Debug/Debugger', include: "**/*.jar")
}

compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
kotlinOptions {
jvmTarget = "1.8"
}
}

jar {
manifest {
attributes(
'Specification-Title': "Allegrex",
'Specification-Version': "10.0.4",
)
}
manifest {
attributes(
'Specification-Title': "Allegrex",
'Specification-Version': "10.0.4",
)
}
}

//noinspection GroovyAssignabilityCheck
task ghidraInstall {
dependsOn 'shadowJar'
doLast {
def allegrexOut = ghidraInstallDir + '/Ghidra/Processors/Allegrex'
delete allegrexOut
copy {
from "data"
into allegrexOut + "/data"
}
copy {
from "ghidra_scripts"
into allegrexOut + "/ghidra_scripts"
}
copy {
from "build/libs"
into allegrexOut + "/lib"
include "ghidra-allegrex-all.jar"
rename("ghidra-allegrex-all.jar", "Allegrex.jar")
}
copy {
from "."
into allegrexOut
include "Module.manifest"
}
dependsOn 'shadowJar'
doLast {
def allegrexOut = ghidraInstallDir + '/Ghidra/Processors/Allegrex'
delete allegrexOut
copy {
from "data"
into allegrexOut + "/data"
}
copy {
from "ghidra_scripts"
into allegrexOut + "/ghidra_scripts"
}
copy {
from "build/libs"
into allegrexOut + "/lib"
include "ghidra-allegrex-all.jar"
rename("ghidra-allegrex-all.jar", "Allegrex.jar")
}
copy {
from "."
into allegrexOut
include "Module.manifest"
}
}
}

task ghidraInstallThenRun {
dependsOn 'ghidraInstall'
doLast {
exec {
if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
commandLine 'cmd', '/c', 'ghidraRun.bat'
} else {
commandLine './ghidraRun.sh'
}
workingDir ghidraInstallDir
ignoreExitValue true
}
}
}

task ghidraInstallThenDebug {
dependsOn 'ghidraInstall'
doLast {
exec {
if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
commandLine 'cmd', '/c', 'support\\ghidraDebug.bat'
} else {
commandLine './support/ghidraDebug.sh'
}
workingDir ghidraInstallDir
ignoreExitValue true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package allegrex.agent.ppsspp

import allegrex.agent.ppsspp.client.websocket.PpssppWsClient
import allegrex.agent.ppsspp.model.PpssppDebuggerObjectModel
import ghidra.dbg.DebuggerModelFactory
import ghidra.dbg.DebuggerObjectModel
import ghidra.dbg.util.ConfigurableFactory
import java.util.concurrent.CompletableFuture

@Suppress("unused")
@ConfigurableFactory.FactoryDescription(
brief = "PPSSPP WebSocket debugger (beta)",
htmlDetails = """Connect to a running PPSSPP instance over WebSocket.
Make sure "Allow remote debugger" is enabled in Developer tools.
Note: This integration is in beta. Please report any issues
to kotcrab/ghidra-allegrex GitHub repository"""
)
class PpssppWsDebuggerModelFactory : DebuggerModelFactory {
@JvmField
@ConfigurableFactory.FactoryOption("Connection Address (empty to auto-detect)")
val connectionUrlOption: ConfigurableFactory.Property<String> =
ConfigurableFactory.Property.fromAccessors(String::class.java, { connectionUrl }, { connectionUrl = it })

private var connectionUrl = ""

override fun build(): CompletableFuture<out DebuggerObjectModel> {
val model = PpssppDebuggerObjectModel(PpssppWsClient(connectionUrl))
return model.start().thenApply { model }
}
}

0 comments on commit e9082a8

Please sign in to comment.