Skip to content

Commit

Permalink
Add Parser
Browse files Browse the repository at this point in the history
  • Loading branch information
alonalbert committed May 2, 2024
1 parent 0a9b89c commit 7d0370d
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 95 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ kotlin {
implementation("net.java.dev.jna:jna:${extra["jna.version"] as String}")
implementation("androidx.collection:collection:${extra["collections.version"] as String}")
implementation("org.jetbrains.androidx.lifecycle:lifecycle-runtime:${extra["lifecycle.version"] as String}")
runtimeOnly("org.jetbrains.skiko:skiko-awt-runtime-linux-x64:${extra["skiko.version"] as String}")
}
}
}
Expand Down
23 changes: 13 additions & 10 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/DexTextArea.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
import java.awt.Graphics
import java.awt.Graphics2D

private val JumpPattern = Regex("(\\s+)[0-9a-fA-F]{4}: .+([0-9a-fA-F]{4}) // ([+-])[0-9a-fA-F]{4}[\\n\\r]*")
private val AddressedPattern = Regex("(\\s+)([0-9a-fA-F]{4}): .+[\\n\\r]*")
private val JumpPattern = Regex(".{9}[0-9a-fA-F]{4}: .+([0-9a-fA-F]{4}) // ([+-])[0-9a-fA-F]{4}[\\n\\r]*")
private val AddressedPattern = Regex(".{9}([0-9a-fA-F]{4}): .+[\\n\\r]*")

internal fun updateTextArea(textArea: RSyntaxTextArea, text: String) {
val position = textArea.caretPosition
Expand Down Expand Up @@ -49,9 +49,9 @@ class DexTextArea : RSyntaxTextArea() {
var line = document.getText(start, end - start)
var result = JumpPattern.matchEntire(line)
if (result != null) {
val srcHorizontalOffset = start + result.groupValues[1].length
val targetAddress = result.groupValues[2]
val direction = if (result.groupValues[3] == "+") 1 else -1
val srcHorizontalOffset = start + line.countPadding()
val targetAddress = result.groupValues[1]
val direction = if (result.groupValues[2] == "+") 1 else -1

var dstLine = srcLine + direction
while (dstLine in 0..<lineCount) {
Expand All @@ -62,8 +62,8 @@ class DexTextArea : RSyntaxTextArea() {
result = AddressedPattern.matchEntire(line)
if (result == null) {
break
} else if (result.groupValues[2] == targetAddress) {
val dstHorizontalOffset = start + result.groupValues[1].length
} else if (result.groupValues[1] == targetAddress) {
val dstHorizontalOffset = start + line.countPadding()

displayJump = true
jumpRange = srcLine to dstLine
Expand Down Expand Up @@ -97,10 +97,13 @@ class DexTextArea : RSyntaxTextArea() {
val x2 = bounds2.x.toInt() - padding
val y2 = (bounds2.y + lineHeight / 2).toInt()

val x0 = modelToView2D(2).x.toInt()
val g2 = g as Graphics2D
g2.drawLine(x1, y1, x1 / 2, y1)
g2.drawLine(x1 / 2, y1, x2 / 2, y2)
g2.drawLine(x2 / 2, y2, x2, y2)
g2.drawLine(x1, y1, x0, y1)
g2.drawLine(x0, y1, x0, y2)
g2.drawLine(x0, y2, x2, y2)
}
}
}

private fun String.countPadding() = indexOfFirst { it != ' ' }
89 changes: 4 additions & 85 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package dev.romainguy.kotlin.explorer

import dev.romainguy.kotlin.explorer.dex.DexDumpParser
import kotlinx.coroutines.*
import java.nio.file.Files
import java.nio.file.Path
Expand Down Expand Up @@ -95,7 +96,7 @@ suspend fun disassemble(
}

launch(ui) {
onDex(filterDex(dexdump.output))
onDex(DexDumpParser(dexdump.output).parseDexDump())
onStatusUpdate("AOT compilation…")
}

Expand Down Expand Up @@ -248,13 +249,7 @@ private fun cleanupClasses(directory: Path) {
.forEach { path -> path.toFile().delete() }
}

private val BuiltInKotlinClass = Regex("^(kotlin|kotlinx|java|javax|org\\.(intellij|jetbrains))\\..+")

private val DexCodePattern = Regex("^[0-9a-fA-F]+:[^|]+\\|([0-9a-fA-F]+: .+)")
private val DexMethodStartPattern = Regex("^\\s+#[0-9]+\\s+:\\s+\\(in L[^;]+;\\)")
private val DexMethodNamePattern = Regex("^\\s+name\\s+:\\s+'(.+)'")
private val DexMethodTypePattern = Regex("^\\s+type\\s+:\\s+'(.+)'")
private val DexClassNamePattern = Regex("^\\s+Class descriptor\\s+:\\s+'L(.+);'")
internal val BuiltInKotlinClass = Regex("^(kotlin|kotlinx|java|javax|org\\.(intellij|jetbrains))\\..+")

private val OatClassNamePattern = Regex("^\\d+: L([^;]+); \\(offset=[0-9a-zA-Zx]+\\) \\(type_idx=\\d+\\).+")
private val OatMethodPattern = Regex("^\\s+\\d+:\\s+(.+)\\s+\\(dex_method_idx=\\d+\\)")
Expand Down Expand Up @@ -319,83 +314,7 @@ private fun filterOat(oat: String) = buildString {
}
}

private fun filterDex(dex: String) = buildString {
val indent = " "
val lines = dex.lineSequence().iterator()

var insideClass = false
var insideMethod = false
var firstMethod = false
var firstClass = true
var className = "<UNKNOWN>"

while (lines.hasNext()) {
var line = lines.next()

var match: MatchResult? = null

if (insideClass) {
if (insideMethod) {
match = DexCodePattern.matchEntire(line)
if (match != null && match.groupValues.isNotEmpty()) {
appendLine("$indent${match.groupValues[1]}")
}
}

if (match === null) {
match = DexMethodStartPattern.matchEntire(line)
if (match != null) {
if (!lines.hasNext()) return@buildString
line = lines.next()

match = DexMethodNamePattern.matchEntire(line)
if (match != null && match.groupValues.isNotEmpty()) {
val name = match.groupValues[1]
if (!firstMethod) appendLine()
firstMethod = false

if (!lines.hasNext()) return@buildString
line = lines.next()

match = DexMethodTypePattern.matchEntire(line)
if (match != null && match.groupValues.isNotEmpty()) {
val type = match.groupValues[1]
appendLine(" $name$type // $className.$name()")
insideMethod = true
}
}
}
}
}

if (match === null) {
if (line.trim().startsWith("Class #")) {
if (!lines.hasNext()) return@buildString
line = lines.next()

match = DexClassNamePattern.matchEntire(line)
if (match != null && match.groupValues.isNotEmpty()) {
className = match.groupValues[1].replace('/', '.')

val suppress = className.matches(BuiltInKotlinClass)
if (!suppress) {
if (!firstClass) appendLine()
appendLine("class $className")
}

if (!lines.consumeUntil("Direct methods")) break

insideMethod = false
firstMethod = true
firstClass = false
insideClass = !suppress
}
}
}
}
}

private fun Iterator<String>.consumeUntil(prefix: String): Boolean {
internal fun Iterator<String>.consumeUntil(prefix: String): Boolean {
while (hasNext()) {
val line = next()
if (line.trim().startsWith(prefix)) return true
Expand Down
121 changes: 121 additions & 0 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/dex/DexDumpParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright (C) 2023 Romain Guy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.romainguy.kotlin.explorer.dex

import dev.romainguy.kotlin.explorer.BuiltInKotlinClass
import dev.romainguy.kotlin.explorer.consumeUntil

private val PositionRegex = Regex("^\\s*0x(?<address>[0-9a-f]+) line=(?<line>\\d+)$")

private const val ClassStart = "Class #"
private const val ClassEnd = "source_file_idx"
private const val ClassName = "Class descriptor"
private const val Instructions = "insns size"
private const val Positions = "positions"

internal class DexDumpParser(text: String) {
private val lines = text.lineSequence().iterator()
fun parseDexDump(): String {
val classes = buildList {
while (lines.consumeUntil(ClassStart)) {
add(readClass())
}
}
return classes
.filter { it.methods.isNotEmpty() && !it.name.matches(BuiltInKotlinClass)}
.joinToString(separator = "\n") { it.toString() }
}

private fun readClass(): DexClass {
val className = lines.next().getClassName()
val methods = buildList {
while (lines.hasNext()) {
val line = lines.next().trim()
when {
line.startsWith(ClassEnd) -> break
line.startsWith(Instructions) -> add(readMethod(className))
}
}
}
return DexClass(className, methods)
}

private fun readMethod(className: String): DexMethod {
val (name, type) = lines.next().substringAfterLast(".").split(':', limit = 2)
val instructions = readInstructions()
lines.consumeUntil(Positions)
val positions = readPositions()
val code = buildString {
instructions.forEach {
val lineNumber = positions[it.address]
val prefix = if (lineNumber != null) "%3s: ".format(lineNumber) else " "
append(" $prefix${it.address}: ${it.code}\n")
}
}
return DexMethod(className, name, type, code)
}

private fun readInstructions(): List<DexInstruction> {
return buildList {
while (lines.hasNext()) {
val line = lines.next()
if (line.startsWith(" ")) {
break
}
val (address, code) = line.substringAfter('|').split(": ", limit = 2)
add(DexInstruction(address, code))
}
}
}

private fun readPositions(): Map<String, Int> {
return buildMap {
while (lines.hasNext()) {
val line = lines.next()
val match = PositionRegex.matchEntire(line) ?: break
put(match.getValue("address"), match.getValue("line").toInt())
}
}
}

private class DexClass(val name: String, val methods: List<DexMethod>) {
override fun toString() = "class $name\n${methods.joinToString("\n\n") { it.toString() }}"
}

private class DexMethod(val className: String, val name: String, val type: String, val code: String) {
override fun toString() = " $name$type // $className.$name()\n$code"
}

private class DexInstruction(val address: String, val code: String)
}

private fun MatchResult.getValue(group: String): String {
return groups[group]?.value ?: throw IllegalStateException("Value of $group not found in $value")
}

private fun String.getClassName() =
getValue(ClassName)
.removePrefix("L")
.removeSuffix(";")
.replace('/', '.')

private fun String.getValue(name: String): String {
if (!trim().startsWith(name)) {
throw IllegalStateException("Expected '$name'")
}
return substringAfter('\'').substringBefore('\'')
}

0 comments on commit 7d0370d

Please sign in to comment.