Skip to content

Instrumentation

Jordan Samhi edited this page Oct 25, 2026 · 4 revisions

Instrumentation

The current API is com.jordansamhi.androspecter.instrumentation.Logger. It registers probes that modify the live Jimple model of an APK. Use it with an analysis session and submit transformations through AndroidAnalysis.apply(...) so cached call graphs are invalidated.

These examples target AndroSpecter 2.0.0, with JDK 17, Soot 4.7.1, and FlowDroid 2.15.1. See Getting started for installation while publication is pending. The historical 1.1.8 release does not provide these APIs.

Register and apply probes

This complete utility class accepts a readable APK, the Android SDK platforms directory, and an application package name without a trailing dot. Call it on the thread that owns the analysis; only one managed analysis may be open in a JVM.

import com.jordansamhi.androspecter.analysis.AndroidAnalysis;
import com.jordansamhi.androspecter.graph.CallGraphSnapshot;
import com.jordansamhi.androspecter.inspection.LibraryFilter;
import com.jordansamhi.androspecter.instrumentation.InstrumentationOptions;
import com.jordansamhi.androspecter.instrumentation.Logger;
import com.jordansamhi.androspecter.instrumentation.TransformationPipeline;
import java.nio.file.Path;

public final class InstrumentExample {
    /**
     * Adds method and call-site probes, then captures the rebuilt graph.
     * @param apk readable APK file
     * @param platforms Android SDK platforms directory
     * @param packageName application package and subpackages to instrument
     */
    public static void instrument(Path apk, Path platforms, String packageName) {
        CallGraphSnapshot before;
        CallGraphSnapshot after;
        LibraryFilter scope = LibraryFilter.packages(packageName);

        try (AndroidAnalysis analysis = AndroidAnalysis.open(apk, platforms)) {
            before = analysis.callGraph();
            InstrumentationOptions options = InstrumentationOptions.builder()
                .classes(clazz -> clazz.isApplicationClass() && scope.test(clazz))
                .libraryFilter(LibraryFilter.packages(packageName + ".thirdparty"))
                .build();
            Logger logger = new Logger(analysis.context(), options);
            logger.logAllMethods("AndroSpecter", false);
            logger.logAllMethodCalls("AndroSpecter", false);

            // Registration above has not inserted probes yet.
            analysis.apply(TransformationPipeline.builder().add(logger).build());
            after = analysis.callGraph();
        }

        // Both snapshots contain detached values and survive session closure.
        System.out.println("Before: " + before.edges().size());
        System.out.println("After: " + after.edges().size());
    }
}

Replace the illustrative .thirdparty package with the library namespaces relevant to your APK. Library classification is caller policy; APK code marked as an application class by Soot can still be third-party code.

The runnable com.jordansamhi.androspecter.examples.InstrumentInMemory example accepts <apk> <platforms> <package-prefix>. See the examples README for build and launch commands. It changes the in-memory model; it does not produce an instrumented APK.

Scope and ordering

  • Default InstrumentationOptions select Soot application classes and all their methods. The default library classifier is LibraryFilter.none().
  • classes(...), methods(...), and libraryFilter(...) replace their previous setting. To retain application-class scope when replacing classes(...), include clazz.isApplicationClass() in your predicate. This differs from inspection predicates, which combine repeated registrations with AND.
  • includeLibraries=false excludes classes matched by the configured library filter. true bypasses that classifier but still enforces class, method, and target-package restrictions. It does not automatically instrument all library classes.
  • LibraryFilter.packages(...) matches exact packages and subpackages. and, or, and negate compose policies; knownLibraries() explicitly opts into the legacy bundled classifier.
  • logger.setTargetPackage(...) adds an exact-package/subpackage restriction. Null or empty removes that restriction. Changes affect operations already registered but not yet applied.
  • Logger operations run in registration order, each over all selected concrete methods before the next operation begins. Pipeline stages also run in registration order. New logger instances do not register global Soot packs.

Reapplying a logger or pipeline can insert probes again. Transformations are not transactional: failure or cancellation can leave changes already made. Soot objects passed to predicates or transformations belong to the current session and thread; do not retain them after closure or reset.

What each operation records

Registration method Probe location and message
logAllMethods(tag, includeLibraries) Method entry, after identities: METHOD=method-signature
logAllClasses(tag, includeLibraries) Instance constructors and static initializers: CLASS=class-name
logAllMethodCalls(tag, includeLibraries) Before an invocation: CALL=caller-signature-->callee-signature; calls to the logging helper are excluded
logAllStatements(tag, includeLibraries) Before units other than identities, returns, and monitors: STATEMENT=method|statement|position
logActivities(tag, includeLibraries) Selected Activity callbacks: ACTIVITY=class-name|callback-subsignature
logServices(tag, includeLibraries) Selected Service callbacks: SERVICE=class-name|callback-subsignature
logBroadcastReceivers(tag, includeLibraries) onReceive: BROADCASTRECEIVER=class-name|callback-subsignature
logContentProviders(tag, includeLibraries) onCreate: CONTENTPROVIDER=class-name|callback-subsignature

The generated LogCheckerClass emits each message at most once per process, regardless of tag, using android.util.Log.d. Its shared set and explicit monitor operations protect concurrent check/log/add operations. These are coverage observations, not execution counts or a chronological trace. Reusing a message under another tag does not produce a second event.

Statement probes precede branches, jumps, and throws. A probe means the statement was reached, even if it did not finish. Statement positions are one-based unit positions at the time the operation runs, including excluded units; they are not source line numbers. A statement operation can see probes inserted by an earlier operation.

Call messages name the method referenced by the invocation, not a runtime-observed dispatch target. Library exclusion applies to the caller body, so a selected application method can still log a call to a library target. Lifecycle operations instrument existing declarations and do not synthesize inherited callbacks; their messages distinguish callbacks but remain deduplicated.

Graph snapshots after mutation

analysis.apply(...) invalidates the cached graph before transformation execution, including when a transformation later fails. The next analysis.callGraph() rebuilds it and captures a new snapshot. Previously returned snapshots remain unchanged and remain readable after the session closes.

A static graph includes only calls represented by the analysis. Inserting probes into unreachable methods does not guarantee new reachable graph edges. See call graphs and inspection for graph-query semantics.

Direct insertion and export

addLogToMethod(method, tag, message) and addLogStatement(units, insertionPoint, tag, message, body, after) mutate immediately. They apply class/method/package scope but do not consult the library classifier. All supplied Soot objects must belong to the same live scene. For managed sessions, perform such changes inside analysis.apply(context -> { ... }) so graph invalidation still occurs.

The managed API does not yet provide an APK export/signing workflow. Instrumentation alone produces no new APK and does not execute the inserted logging calls.

For callers already managing Soot output, the separate low-level logger.exportNewApk() delegates to Soot's configured output settings. logger.exportNewApk(directory) sets global DEX output format and directory before writing; it does not apply pending operations. Soot's input/output configuration determines whether output is an APK container or DEX files. Signing and installation remain separate caller responsibilities. Export must happen while the logger's scene and context are valid.

Soot output can transform the model and normally releases active bodies unless no_writeout_body_releasing is enabled. Finish inspection and snapshot capture before export; it should not be treated as a read-only operation on the scene.

Legacy pack workflow

Logger.v() remains a deprecated adapter. It creates the helper immediately, uses the legacy library classifier, and registers named transforms in Soot's global jtp pack. Its instrument() method runs all configured Soot packs, including unrelated transforms. Reacquire it after G.reset() and avoid registering the same named operation repeatedly.

Call instrument() on the thread that created the logger. It supports Soot 4.7.1's body-pack workers, including num_threads=1 and multiple workers, while preserving the configured pack order. The logger prepares its helper on the calling thread and serializes its own worker callbacks during this call. Public methods, including registration, direct insertion, and export, still require the creating thread; this does not enable concurrent analyses. To run these legacy transforms on Soot workers, use logger.instrument() rather than calling PackManager.runPacks() directly.

Failures propagate to the caller and may leave partial instrumentation. A failed legacy pack run cannot be retried on the same logger: stop other Soot work, reset and reinitialize Soot, then reacquire Logger.v(). Soot can still have unrelated pack callbacks finishing after a failure; the logger's cleanup only governs its own callbacks. Successful runs leave registrations installed, so running again can add duplicate probes.

For new code, construct Logger with an analysis context and use analysis.apply(logger) or an explicit pipeline. The old wiki title Instrumenter is retained only as a migration reference.

Clone this wiki locally