Mobile Development 16 min read

UprobeStats: Dynamic User‑Space Instrumentation on Android via eBPF uprobe

UprobeStats, introduced in Android 15, uses the Linux kernel eBPF uprobe mechanism to dynamically insert probes into user‑space methods, capture timestamps and arguments, load BPF programs, and forward the data to StatsD via configurable protobufs, enabling flexible, source‑free instrumentation with minimal overhead.

OPPO Kernel Craftsman
OPPO Kernel Craftsman
OPPO Kernel Craftsman
UprobeStats: Dynamic User‑Space Instrumentation on Android via eBPF uprobe

Overview

Android 15 introduces UprobeStats , a framework that leverages the Linux kernel eBPF uprobe mechanism to dynamically collect instrumentation points from user‑space processes and forward the data to the StatsD module.

The overall architecture is shown in the diagram below:

The main code path resides at:

https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/UprobeStats/src/

UprobeStats Introduction

UprobeStats inserts probes into user‑space code using the uprobe mechanism. It can precisely trace method execution, capture arguments, and forward the collected data to StatsD for aggregation, enabling flexible statistical analysis of Android system components.

Key Features

Dynamic monitoring of user‑space functions (primarily compiled methods in oat/odex).

Insertion of probes to capture start/end timestamps and method arguments via registers.

Implementation Details

UprobeStats is packaged as an APEX located at /system/apex/com.android.uprobestats.apex . Its directory layout is:

/apex/com.android.uprobestats
|-- bin
|   `-- uprobestats
|-- bin
|   `-- uprobestatsbpfload
|-- etc
|   `-- bpf
|       |-- BitmapAllocation.o
|       |-- GenericInstrumentation.o
|       `-- ProcessManagement.o
|-- init.rc (defines the uprobestats service)
|-- aconfig_flags.pb
|-- flag.info
|-- flag.map
|-- flag.val
|-- package.map
|-- lib64
|   |-- libbase.so
|   |-- libc++.so
|   `-- libuprobestats_client.so

The executable uprobestats performs the following steps:

Check the feature flag android::uprobestats::flags::enable_uprobestats to determine if the module is enabled.

If enabled, read the probe configuration files from /data/misc/uprobestats-configs/ .

Parse the configuration into a UprobestatsConfig protobuf (example shown below).

Example protobuf fragment:

# proto-file: config.proto
# proto-message: UprobestatsConfig
tasks {
  probe_configs: {
    bpf_name: "prog_ProcessManagement_uprobe_update_device_idle_temp_allowlist"
    file_paths: "/system/framework/oat/arm64/services.odex"
    method_signature: "void com.android.server.am.ActivityManagerService$LocalService.updateDeviceIdleTempAllowlist(int[], int, boolean, long, int, int, java.lang.String, int)"
    fully_qualified_class_name: "com.android.server.am.ActivityManagerService$LocalService"
    method_name: "updateDeviceIdleTempAllowlist"
    fully_qualified_parameters: ["int[]", "int", "boolean", "long", "int", "int", "java.lang.String", "int"]
  }
  bpf_maps: "map_ProcessManagement_update_device_idle_temp_allowlist_records"
  target_process_name: "system_server"
  duration_seconds: 180
  statsd_logging_config { atom_id: 940 }
}

Probe Resolution

The configResolver resolves each probe configuration to an executable offset within the specified oat/odex file. Two resolution strategies are used:

Method 2 (preferred): Calls DynamicInstrumentationManagerService::getExecutableMethodFileOffsets to obtain offsets via ART internals.

Method 1 (fallback): Uses oatdump to parse the oat file and match the method signature.

Relevant code snippet:

int offset = art::getMethodOffsetFromOatdump(file_path, probeConfig.method_signature());
if (offset > 0) { matched_file_path = file_path; break; }

Attaching BPF Programs

For each resolved probe, bpfPerfEventOpen loads the corresponding BPF program and attaches it to the target method via a perf event:

int bpfPerfEventOpen(const char *filename, int offset, int pid, const char *bpfProgramPath) {
  android::base::unique_fd bpfProgramFd(android::bpf::retrieveProgram(bpfProgramPath));
  // read PMU type, configure perf_event_attr, open event, attach BPF, enable event
}

Data Collection Loop

The collector threads poll the RINGBUF MAPs created by the BPF programs. When data arrives, the user‑space doPoll function extracts the payload, builds an AStatsEvent , and forwards it to StatsD using the atom ID defined in the configuration.

void doPoll(PollArgs args) {
  auto result = bpf::pollRingBuf(mapPath.c_str(), timeoutMs);
  for (auto value : result) {
    AStatsEvent *event = AStatsEvent_obtain();
    AStatsEvent_setAtomId(event, atom_id);
    // write primitive arguments or timestamps
    AStatsEvent_write(event);
    AStatsEvent_release(event);
  }
}

Interaction with StatsD

When a StatsD subscription of type kUprobestatsDetails is triggered, the StartUprobeStats function loads libuprobestats_client.so and calls AUprobestatsClient_startUprobestats . This writes the protobuf config to /data/misc/uprobestats-configs/config and sets the system property uprobestats.start_with_config=config , causing the uprobestats service to start.

void AUprobestatsClient_startUprobestats(const uint8_t* config, int64_t size) {
  const char* filename = "/data/misc/uprobestats-configs/config";
  android::base::WriteStringToFile(std::string(reinterpret_cast
(config), size), filename);
  chmod(filename, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
  android::base::SetProperty("uprobestats.start_with_config", "config");
}

The service definition (rc file) ensures the process runs with the required PERFMON capability and reads the configuration on start.

service uprobestats /system/bin/uprobestats ${uprobestats.start_with_config}
  disabled
  user uprobestats
  group uprobestats readproc
  oneshot
  capabilities PERFMON
  on property:uprobestats.start_with_config=*
    start uprobestats

Conclusion

Compared with traditional static instrumentation, UprobeStats requires no source‑level changes in the Android framework. By supplying a configuration that lists target Java methods, developers can dynamically attach eBPF probes, collect execution data, and report it to StatsD. This approach simplifies instrumentation, improves flexibility, and reduces overhead.

InstrumentationAndroideBPFbpfStatsDUprobeStats
OPPO Kernel Craftsman
Written by

OPPO Kernel Craftsman

Sharing Linux kernel-related cutting-edge technology, technical articles, technical news, and curated tutorials

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.