一. 概述
Android15中Google正式引入 UprobeStats,它利用Linux Kernel eBPF uprobe(用户空间探针)机制来动态获取用户进程中的埋点数据,并将其汇总至 StatsD 模块。
整体架构如下:
本文涉及的主要代码路径如下:https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/UprobeStats/src/
二. UprobeStats介绍
UprobeStats 使用uprobe机制,通过在用户空间代码中插入探针来采集数据。它能够精确地跟踪代码执行过程,采集特定函数的调用情况及获取函数参数信息,最终将获取的数据接入StatsD进行聚合,帮助开发者更加高效灵活地对系统框架进行统计分析。
2.1 主要功能
使用 uprobe 动态地监控用户空间函数(当前主要针对oat/odex中已编译的方法)
可以插入探针并捕获特定函数执行的开始和结束时间,通过寄存器读取函数参数等
2.2 代码实现
当前uprobestats以apex形式集成到 /system/apex/com.android.uprobestats.apex。
apex中的目录结构如下:
/apex/com.android.uprobestats |-- bin | |-- uprobestats | `-- uprobestatsbpfload |-- etc |-- bpf | `-- uprobestats | |-- BitmapAllocation.o | |-- GenericInstrumentation.o | `-- ProcessManagement.o |-- init.rc ( 定义uprobestats service ) |-- aconfig_flags.pb |-- flag.info |-- flag.map |-- flag.val `-- package.map |-- lib64 |-- libbase.so |-- libc++.so `-- libuprobestats_client.so bin/uprobestats : 主要代码编译至可执行文件 bin/uprobestatsbpfload:uprobe bpf程序加载器,由NetBpfLoad最终拉起 etc/bpf/*.o: 可以attach的模版bpf程序 /etc/aconfig_flags.pb ~ package.map:模块flags相关配置 lib64/libuprobestats_client.so: 提供给StatsD动态加载,触发拉起uprobestats服务 |
uprobestats 可执行文件执行流程:
上述流程解释如下:
1.uprobestats进程启动检查feature flag
通过 android::uprobestats::flags::enable_uprobestats flag 来判断是否启用 uprobestats 功能
2.如果flag启用,调用 readConfig 开始读取probe config文件
config具体路径为:/data/misc/uprobestats-configs/{argv[1]},{argv[1]}是命令行传入的config名,典型的 config 文件内容如下:
https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/UprobeStats/src/test/test_setUidTempAllowlistStateLSP.textproto
# 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_configs 中指定了:
bpf_name | bpf程序名 |
method_name | 方法名称 |
fully_qualified_class_name | 全限定类名 |
method_signature | 类方法签名 |
file_paths | method所在的oat/odex文件路径 |
bpf_maps | 指定该bpf程序在uprobe触发后写入的数据MAP,步骤7中会读取该MAP中bpf程序写入的数据 |
target_process_name | 指定uprobe探测的目标进程名 |
duration_seconds | 每次从MAP中读取数据的超时等待时间 |
statsd_logging_config #atom_id | uprobestats取到的数据最终写入的原子埋点id |
需要说明一点:bpf_name 指定的bpf程序在开机时,已由 uprobestatsbpfload 加载并pin到指定路径,bpf程序完整路径如:
/sys/fs/bpf/uprobestats/prog_ProcessManagement_uprobe_update_device_idle_temp_allowlist
bpf_maps 是uprobestatsbpfload 在加载上述bpf程序过程中,解析出bpf程序中的bpf MAP,将其加载并将MAP PIN到指定位置,供后续uprobestats程序attach读取访问,如:
/sys/fs/bpf/uprobestats/map_ProcessManagement_update_device_idle_temp_allowlist_records
3.configResolver中读取并解析config文件,生成 UprobestatsConfig 对象
4.guardrail::isAllowed
当前逻辑为:非user版本,允许加载所有probe config中指定的进程/类方法;user版本当前仅允许特定类方法前缀:
https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/UprobeStats/src/Guardrail.cpp;l=37;bpv=0;bpt=1
constexpr std::array kAllowedMethodPrefixes = {
"com.android.server.am.CachedAppOptimizer",
"com.android.server.am.OomAdjuster",
"com.android.server.am.OomAdjusterModernImpl",
};
bool isAllowed(const ::uprobestats::protos::UprobestatsConfig &config,
const string &buildType) {
if (buildType != "user") {
return true;
}
for (const auto &task : config.tasks()) {
for (const auto &probeConfig : task.probe_configs()) {
const string &methodSignature = probeConfig.method_signature();
std::vectorcomponents =
android::base::Split(methodSignature, " ");
if (components.size() < 2) {
return false;
}
const string &fullMethodName = components[1];
bool allowed = false;
for (const std::string allowedPrefix : kAllowedMethodPrefixes) {
if (android::base::StartsWith(fullMethodName, allowedPrefix + ".") ||
android::base::StartsWith(fullMethodName, allowedPrefix + "$")) {
allowed = true;
break;
}
}
if (!allowed) {
return false;
}
}
}
return true;
}
5.resolveProbes 对 UprobestatsConfig 中所有probe config进行解析
解析每个方法在config指定的oat file path中的offset,该offset用于后续uprobe设置。
[注] 最新Mainline代码使用方式2解析offset,AOSP代码当前仅使用oatdump方式1。
Maineline 代码中,通过调用 DynamicInstrumentationManagerService 提供的getExecutableMethodFileOffsets 接口,进而调用到ActivityManagerInternal::getExecutableMethodFileOffsets,再跨进程调用config指定的app process中的 IApplicationThread::getExecutableMethodFileOffsets,从而调用art相关接口,获取方法对应的offset及所在odex文件的起始地址等;
如果方式2不可用则使用低效的方式1,通过oatdump命令输出config指定的oat文件中所有方法,从而匹配config指定的类方法名、offset;
https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/UprobeStats/src/ConfigResolver.cpp;l=108;bpv=1;bpt=1
std::optional <std::vector <ResolvedProbe>
resolveProbes(::uprobestats::protos::UprobestatsConfig::Task taskConfig) {
std::vectorresult;
for (auto &probeConfig : taskConfig.probe_configs()) {
int offset = 0;
std::string matched_file_path;
for (auto &file_path : probeConfig.file_paths()) {
offset = art::getMethodOffsetFromOatdump(file_path, probeConfig.method_signature());
if (offset > 0) {
matched_file_path = file_path;
break;
}
}
ResolvedProbe probe;
probe.filename = matched_file_path;
probe.offset = offset;
probe.probeConfig = probeConfig;
result.push_back(probe);
}
return result;
}
6.对解析出来的probe config(odex路径,method offset,bpf程序路径),逐个调用bpf::bpfPerfEventOpen,attach config指定的bpf程序并对指定method启用uprobe
https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/UprobeStats/src/Bpf.cpp;l=37
int bpfPerfEventOpen(const char *filename, int offset, int pid,
const char *bpfProgramPath) {
android::base::unique_fd bpfProgramFd(
android::bpf::retrieveProgram(bpfProgramPath));
std::string typeStr;
if (!android::base::ReadFileToString(PMU_TYPE_FILE, &typeStr)) {
LOG(ERROR) << "Failed to open pmu type file";
return -1;
}
int pmu_type = (int)strtol(typeStr.c_str(), NULL, 10);
struct perf_event_attr attr = {};
attr.sample_period = 1;
attr.wakeup_events = 1;
attr.config2 = offset;
attr.size = sizeof(attr);
attr.type = pmu_type;
attr.config1 = android::bpf::ptr_to_u64((void *)filename);
attr.exclude_kernel = true;
int perfEventFd = syscall(__NR_perf_event_open, &attr, pid, /*cpu=*/-1,
/* group_fd=*/-1, PERF_FLAG_FD_CLOEXEC);
if (ioctl(perfEventFd, PERF_EVENT_IOC_SET_BPF, int(bpfProgramFd)) < 0) {
LOG(ERROR) << "PERF_EVENT_IOC_SET_BPF failed. " << strerror(errno);
return -1;
}
if (ioctl(perfEventFd, PERF_EVENT_IOC_ENABLE, 0) < 0) {
LOG(ERROR) << "PERF_EVENT_IOC_ENABLE failed. " << strerror(errno);
return -1;
}
return 0;
}
7.启动collector threads,在config指定的RINGBUF MAP fd上执行poll操作
当method uprobe被执行命中后,bpf程序会向MAP写入数据,用户态 doPoll 会被回调,从MAP中读取bpf程序写入的数据,并组装 AStatsEvent 原子埋点数据,上报给StatsD,各MAP对应的数据获取及解析见章节三。
https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/UprobeStats/src/UprobeStats.cpp;l=66;bpv=1;bpt=1
void doPoll(PollArgs args) {
auto mapPath = args.mapPath;
auto durationSeconds = args.taskConfig.duration_seconds();
auto duration = std::chrono::seconds(durationSeconds);
auto startTime = std::chrono::steady_clock::now();
auto now = startTime;
while (now - startTime < duration) {
auto remaining = duration - (std::chrono::steady_clock::now() - startTime);
auto timeoutMs = static_cast(
std::chrono::duration_cast(remaining)
.count());
if (mapPath.find(kGenericBpfMapDetail) != std::string::npos) {
LOG_IF_DEBUG("polling for GenericDetail result");
auto result = bpf::pollRingBuf(mapPath.c_str(), timeoutMs);
for (auto value : result) {
if (!args.taskConfig.has_statsd_logging_config()) {
LOG_IF_DEBUG("no statsd logging config");
continue;
}
auto statsd_logging_config = args.taskConfig.statsd_logging_config();
int atom_id = statsd_logging_config.atom_id();
LOG_IF_DEBUG("attempting to write atom id: " << atom_id);
AStatsEvent *event = AStatsEvent_obtain();
AStatsEvent_setAtomId(event, atom_id);
for (int primitiveArgumentPosition :
statsd_logging_config.primitive_argument_positions()) {
int primitiveArgument = value.regs[primitiveArgumentPosition +
kJavaArgumentRegisterOffset];
LOG_IF_DEBUG("writing argument value: " << primitiveArgument
<< " from position: "
<< primitiveArgumentPosition);
AStatsEvent_writeInt32(event, primitiveArgument);
}
AStatsEvent_write(event);
AStatsEvent_release(event);
LOG_IF_DEBUG("successfully wrote atom id: " << atom_id);
}
} else if (mapPath.find(kGenericBpfMapTimestamp) != std::string::npos) {
LOG_IF_DEBUG("polling for GenericTimestamp result");
auto result =
bpf::pollRingBuf(mapPath.c_str(), timeoutMs);
for (auto value : result) {
LOG_IF_DEBUG("GenericTimestamp result: event "
if (!args.taskConfig.has_statsd_logging_config()) {
LOG_IF_DEBUG("no statsd logging config");
continue;
}
auto statsd_logging_config = args.taskConfig.statsd_logging_config();
int atom_id = statsd_logging_config.atom_id();
LOG_IF_DEBUG("attempting to write atom id: " << atom_id);
AStatsEvent *event = AStatsEvent_obtain();
AStatsEvent_setAtomId(event, atom_id);
AStatsEvent_writeInt32(event, value.event);
AStatsEvent_writeInt64(event, value.timestampNs);
AStatsEvent_write(event);
AStatsEvent_release(event);
LOG_IF_DEBUG("successfully wrote atom id: " << atom_id);
}
} else if (mapPath.find(kProcessManagementMap) != std::string::npos) {
LOG_IF_DEBUG("Polling for SetUidTempAllowlistStateRecord result");
auto result = bpf::pollRingBuf <bpf::SetUidTempAllowlistStateRecord> ( </bpf::SetUidTempAllowlistStateRecord>
mapPath.c_str(), timeoutMs);
for (auto value : result) {
if (!args.taskConfig.has_statsd_logging_config()) {
LOG_IF_DEBUG("no statsd logging config");
continue;
}
auto statsd_logging_config = args.taskConfig.statsd_logging_config();
int atom_id = statsd_logging_config.atom_id();
AStatsEvent *event = AStatsEvent_obtain();
AStatsEvent_setAtomId(event, atom_id);
AStatsEvent_writeInt32(event, value.uid);
AStatsEvent_writeBool(event, value.onAllowlist);
AStatsEvent_write(event);
AStatsEvent_release(event);
}
}
now = std::chrono::steady_clock::now();
}
LOG_IF_DEBUG("finished polling for mapPath: " << mapPath);
}
三. 依赖的BPF程序
目前AOSP upstream中 uprobestats 提供了几个样板 bpf uprobe 程序:
https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/UprobeStats/src/bpf_progs
这些BPF程序将数据写入RINGBUF MAP后,通知用户态 uprobestats 进程,前边流程图中提到的 doPoll 方法会被调用,按照不同BPF程序类型来解析RINGBUF中的数据格式。
BPF程序 | 解析 |
BitmapAllocation.c | |
该bpf程序,当attach的用户态Java方法被调用时触发,BPF中的RingBuf Map仅保存一个固定的u64数据(123),之后通过submit通知用户态 | |
GenericInstrumentation.c | |
包含2个BPF程序,当attach的Java方法被调用时: 1.call_detail程序获取当前调用线程上下文的所有寄存器数据(包括PC),存入RingBuf并通知用户态;对应 doPoll 函数中对 kGenericBpfMapDetail MAP类型的处理;通过config中指定具体参数所在的寄存器位置来获取基本类型的参数信息,写入埋点数据; 2.call_timestampl_* 程序获取当前系统时间(mono),存入RingBuf并通知用户态,对应 doPoll 函数中kGenericBpfMapTimestamp MAP类型的处理,通过埋点数据获取Java方法调用时间; | |
ProcessManagement.c | |
该bpf程序比较特殊,仅适用于attach以下方法: com.android.server.am.OomAdjuster#setUidTempAllowlistStateLSP,该方法原型 void setUidTempAllowlistStateLSP(int uid, boolean onAllowlist) Java 方法JIT后,BPF程序通过ctx->regs对应的寄存器取出方法参数:按照ART Native函数调用标准,函数第一个参数 uid 位于x2寄存器,第二参数onAllowlist 位于x3寄存器 |
四. 与StatsD间的交互流程
StatsD 是Android系统中用于收集、处理和上报系统及应用程序各种统计数据的服务。它提供了一种统一的接口来收集和管理设备上发生的各种事件和统计信息,通过与Android系统中的其他组件和服务进行交互来实现其功能。在特定事件发生时(如应用启动、网络连接变化等),接收相应的事件通知,并收集与该事件相关的统计数据,收集到的数据经过处理后,根据配置的策略进行存储或上报。
4.1 主要功能
收集应用层、系统层的统计数据
将数据汇总成不同的指标
支持自定义的监控和分析
4.2 与uprobestats交互的代码实现
当触发 Subscription::SubscriberInformationCase::kUprobestatsDetails 类型的订阅时,将会调用 StartUprobeStats 启动 uprobestats 服务。
https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/StatsD/statsd/src/anomaly/subscriber_util.cpp
void triggerSubscribers(const int64_t ruleId, const int64_t metricId,
const MetricDimensionKey& dimensionKey, int64_t metricValue,
const ConfigKey& configKey,
const std::vector& subscriptions) {
for (const Subscription& subscription : subscriptions) {
switch (subscription.subscriber_information_case()) {
...
case Subscription::SubscriberInformationCase::kUprobestatsDetails:
if (!StartUprobeStats(subscription.uprobestats_details())) {
ALOGW("Failed to start uprobestats.");
}
break;
default:
break;
}
}
}
如下代码,StartUprobeStats 函数直接加载并调用 libuprobestats_client.so 中的 AUprobestatsClient_startUprobestats 函数。
https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/StatsD/statsd/src/external/Uprobestats.cpp
const char kLibuprobestatsClientPath[] = "libuprobestats_client.so";
AUprobestatsClient_startUprobestatsFn libInit() {
if (__builtin_available(android __ANDROID_API_V__, *)) {
void* handle = dlopen(kLibuprobestatsClientPath, RTLD_NOW | RTLD_LOCAL);
if (!handle) {
ALOGE("dlopen error: %s %s", __func__, dlerror());
return nullptr;
}
auto f = reinterpret_cast <AUprobestatsClient_startUprobestatsFn> ( </AUprobestatsClient_startUprobestatsFn>
dlsym(handle, "AUprobestatsClient_startUprobestats"));
if (!f) {
ALOGE("dlsym error: %s %s", __func__, dlerror());
return nullptr;
}
return f;
}
return nullptr;
}
AUprobestatsClient_startUprobestats 将订阅的config内容写入
[ /data/misc/uprobestats-configs/config ]
之后通过设置 [ uprobestats.start_with_config=config ] 属性触发启动 uprobestats 服务。从而执行 /system/bin/uprobestats 加载 /data/misc/uprobestats-configs/ 下的 config 配置文件来执行埋点数据收集,该流程章节 2 中已描述。
https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/UprobeStats/src/lib/uprobestats_client.cpp
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");
}
rc中定义uprobestats 服务,由 uprobestats.start_with_config 属性指定config文件名来触发启动。
https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/UprobeStats/src/UprobeStats.rc
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
[ mainline 代码中 UprobeStats-mainline.rc ]
service uprobestats /apex/com.android.uprobestats/bin/uprobestats
disabled
user uprobestats
group uprobestats readproc
oneshot
capabilities PERFMON
五. 总结
与传统埋点相比,uprobestats不需要在 framework 代码中插入各种埋点统计代码,仅需生成 uprobestats 配置(在配置中指定需要埋点的Java类方法),并调用 StatsD 相关接口启动uprobestats。后续被探测 Java 方法执行过程的运行数据会自动以埋点形式上报给StatsD,这种动态配置埋点的方式大大简化了埋点流程,提升了数据获取的灵活性和效率。
往
期
推
荐
解析H.266/VVC视频编码标准的关键技术
Android 系统服务DisplayManagerService和DisplayDevice生命周期解读