"Σκόπει εἰς σεαυτόν. Ἐντὸς σοῦ πηγὴ ἀγαθοῦ ἐστιν, ἡ ἀεὶ ἐκβρύειν ἑτοίμη, ἐὰν ἀεὶ σκάπτῃς."
"Look within. Within is the fountain of good, and it will ever bubble up, if you will ever dig."
- Marcus Aurelius (121-180 AD), Roman Emperor and Stoic philosopher
"Ignis aurum probat, miseria fortes."
"Fire tests gold and adversity tests the brave."
-Seneca the Younger (c. 4 BC - AD 65), Roman statesman and Stoic philosopher
Stoic lets you look within your Android processes, giving you the courage to take on difficult bugs.
Stoic is a tool for
- running code inside another process - without any modifications to its APK,
- exposing extra capabilities to code, normally only available to a debugger, and
- blurring the lines between code and debugger
You can write plugins that
- provide command-line access to APIs normally only available inside the process
- leverage debugger functionality (e.g. use breakpoints to hook arbitrary methods)
- examine the internal state of a process without restarting the process
Stoic is fast. The first time you run a Stoic plugin in a process it will take 2-3 seconds to attach. Thereafter, Stoic plugins typically run in less than a second.
- Install with Homebrew:
brew install block/tap/stoic
- Run your first Stoic plugin:
stoic helloworld
- When you don't specify a package, Stoic injects itself into
com.squareup.stoic.demoapp.withoutsdk
by default - a simple app bundled with Stoic. Runstoic --pkg <your-app> helloworld
to inject into your own app instead. - Create a new plugin:
stoic plugin --new scratch
- Run your plugin with:
stoic scratch
- Open up
~/.config/stoic/plugin/scratch
with Android Studio to modify this plugin and explore what Stoic can do.
Stoic works on any API 26+ Android device / emulator, with any debuggable app (that I've tested so far).
Stoic bundles a few plugins:
- appexitinfo - command-line access to the ApplicationExitInfo API
- breakpoint - print when methods get called, optionally with arguments/return-value/stack-trace
- crasher - see how your app handles various types of crashes
Each plugin is a normal Java main
function. You access debugger functionality via the com.squareup.stoic.jvmti
package. e.g.
// get callbacks whenever any method of interest is called
val method = jvmti.virtualMachine.methodBySig("android/view/InputEventReceiver.dispatchInputEvent(ILandroid/view/InputEvent;)V")
jvmti.breakpoint(method.startLocation) { frame ->
println("dispatchInputEvent called")
}
// iterate over each bitmap in the heap
for (bitmap in jvmti.instances(Bitmap::class.java)) {
println("$bitmap: size=${bitmap.allocationByteCount}")
}
The primary technologies powering Stoic are JVMTI, Unix Domain Sockets, and run-as. Stoic is written in Kotlin and uses Clikt for command-line parsing and GraalVM for snappy start-ups.
The first time you run Stoic on a process it will attach a jvmti agent which will start a server inside the process. We connect to this server through a unix domain socket, and multiplex stdin/stdout/stderr over this connection. See https://github.com/square/stoic/blob/main/docs/ARCHITECTURE.md for more details.