Introduction to ART, AOT, and JIT
The Android Runtime (ART) is the managed runtime used by the Android operating system. It replaced Dalvik in Android 5.0 Lollipop, bringing significant performance improvements through a combination of Ahead-Of-Time (AOT) and Just-In-Time (JIT) compilation. Understanding how ART handles application code is crucial for advanced reverse engineering, security analysis, and performance optimization.
AOT compilation, primarily performed by the dex2oat tool, translates DEX bytecode into native machine code (ELF files, typically in /data/dalvik-cache) during app installation or system updates. This pre-compilation minimizes launch times and improves execution speed for frequently used code. However, not all code paths are AOT compiled, and sometimes the AOT-compiled code might be suboptimal for specific runtime conditions.
This is where JIT compilation steps in. JIT dynamically compiles and optimizes frequently executed portions of code at runtime. If a particular method or code block is identified as “hot” (executed many times), ART’s JIT compiler kicks in, translates it into highly optimized machine code, and stores it in a dedicated code cache. This dynamic nature, while beneficial for performance, presents a significant challenge for reverse engineers who rely on static analysis of pre-compiled binaries.
The Elusive Nature of JIT-Compiled Code
The primary difficulty in reverse engineering JIT-compiled code stems from its transient and memory-resident nature. Unlike AOT-compiled code, which persists on disk as ELF binaries, JIT-generated code lives only in memory within the process’s address space. Its memory addresses are dynamic, and it’s not typically associated with traditional symbols that make static analysis straightforward.
Traditional static analysis tools struggle because there’s no fixed file to load. Moreover, the code can be invalidated or re-compiled if optimization profiles change. This necessitates dynamic analysis techniques to observe, capture, and analyze the code as it’s being generated and executed, making the task significantly more complex but also incredibly rewarding for deep insights into application behavior, especially concerning obfuscated or dynamically loaded code.
Capturing JIT Activity with simpleperf
Understanding simpleperf for Android Profiling
simpleperf is Android’s native command-line profiling tool, built on top of the Linux perf subsystem. It’s an indispensable utility for analyzing CPU usage, call stacks, and identifying performance bottlenecks within applications and the system itself. Crucially for our purpose, simpleperf can capture execution traces that include JIT-compiled code, often showing resolved method names or at least indicating entry into JIT-generated regions.
Step-by-Step JIT Call Stack Capture
To use simpleperf, you’ll need adb and a rooted Android device (though some profiling is possible on non-rooted devices, capturing system-wide or deep JIT details often requires root). First, identify the Process ID (PID) of the application you want to profile:
adb shell ps -A | grep <package_name>
Once you have the PID, you can start recording a trace. The --call-graph fp option is critical as it instructs simpleperf to record the call stack using frame pointers, which helps in reconstructing the execution flow, including through JIT-compiled sections:
adb shell simpleperf record -p <PID> -e cpu-cycles --call-graph fp --duration 30 -o /data/local/tmp/perf.data
This command records CPU cycles for 30 seconds for the specified PID, saving the data to /data/local/tmp/perf.data. While the recording is active, interact with your target application to trigger JIT compilation and execution of the code you’re interested in. After the recording completes, pull the data to your host machine:
adb pull /data/local/tmp/perf.data .
Finally, generate a report using simpleperf report:
simpleperf report -i perf.data
In the report, look for symbols or address ranges that do not correspond to known `libart.so` or application native libraries. JIT-compiled code often appears as raw memory addresses or, if ART’s profiling metadata is available, can be resolved to specific Java method names. You might see entries like [JIT code] or specific method signatures that are not part of the initial AOT binary, indicating dynamic compilation.
Advanced JIT Code Extraction using Frida
The Concept: Hooking ART’s JIT Compiler
For direct extraction of JIT-compiled code, we need more control than simpleperf offers. Frida, a dynamic instrumentation toolkit, is ideal for this. The strategy involves hooking into ART’s internal JIT compilation functions to intercept compiled methods as they are generated and added to the code cache. Key functions within libart.so that manage JIT compilation include art::jit::JitCompiler::CompileMethod and functions within art::jit::JitCodeCache responsible for adding new code.
Setting up Frida for ART JIT Hooking
Ensure Frida server is running on your rooted Android device and connect your host machine. Identify the target process where JIT activity occurs. Then, using Frida’s JavaScript API, we can locate libart.so in memory, find the relevant JIT-related functions, and attach hooks.
Frida Script for Dumping JIT Methods
Locating the exact JIT compilation function can be tricky as symbol names and offsets vary between Android versions and architectures. One robust approach involves scanning for patterns or utilizing exported symbols if they exist. For illustration, we’ll demonstrate hooking a conceptual JIT compilation entry point. In a real scenario, you would analyze libart.so with a disassembler to find the precise function responsible for creating new JIT code.
Java.perform(function() { var libart = Module.findBaseAddress(
Android Mobile Specs & Compare Directory
Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!
Compare Devices Specs →