hook 应用 trace 日志

2,407 阅读5分钟

上回讲到 atrace 的实现,其实主要就是写几个系统属性和节点,然后 APP 再将 trace log 写入 /sys/kernel/debug/tracing/trace_marker。因此,如果想不通过命令行去使用 atrace 的功能,只需要开启 trace 功能和 hook 对这个文件节点的写入操作,即可实现此功能。

分析 hook

APP 在上层一般是使用 Trace.beginSection 方法来记录 trace 日志,可以看到只有 isTagEnabled(TRACE_TAG_APP)true 时,才能打日志

  public static void beginSection(String sectionName) {
      if (isTagEnabled(TRACE_TAG_APP)) {
          nativeTraceBegin(TRACE_TAG_APP, sectionName);
      }
  }

isTagEnabled 是取决于 sEnabledTags 静态变量第 TRACE_TAG_APP(1L << 12)是否置 1

public static boolean isTagEnabled(long traceTag) {
    long tags = sEnabledTags;
    if (tags == TRACE_TAG_NOT_READY) {
        tags = cacheEnabledTags();
    }
    return (tags & traceTag) != 0;
}
  
private static long cacheEnabledTags() {
    long tags = nativeGetEnabledTags();
    sEnabledTags = tags;
    return tags;
}

static jlong android_os_Trace_nativeGetEnabledTags(JNIEnv* env, jclass clazz) {
    return atrace_get_enabled_tags();
}

static inline uint64_t atrace_get_enabled_tags()
{
    atrace_init();
    return atrace_enabled_tags;
}

通过 atrace 目录读取日志时,sEnabledTags 是由命令行参数控制的。

因此需要通过反射设置 sEnabledTags,同时设置 native 层的 atrace_enabled_tags 变量,因为 native 层也有一个类似的判断。

static inline void atrace_begin(uint64_t tag, const char* name)
{
    if (CC_UNLIKELY(atrace_is_tag_enabled(tag))) {
        void atrace_begin_body(const char*);
        atrace_begin_body(name);
    }
}

static inline uint64_t atrace_is_tag_enabled(uint64_t tag)
{
    return atrace_get_enabled_tags() & tag;
}

最终调用到 atrace_begin_body

void atrace_begin_body(const char* name)
{
    char buf[ATRACE_MESSAGE_LENGTH];

    int len = snprintf(buf, sizeof(buf), "B|%d|%s", getpid(), name);
    if (len >= (int) sizeof(buf)) {
        ALOGW("Truncated name in %s: %s\n", __FUNCTION__, name);
        len = sizeof(buf) - 1;
    }
    write(atrace_marker_fd, buf, len);
}

可以看到只是简单的写入 atrace_marker_fd,因此只要 hook 写入过程,再将日志写入到其他文件,即可获取 APPtrace 日志。

获取 trace_marker 的文件描述符和 atrace_enabled_tags

使用 kwai::linkerlibcutils.so 找到 atrace_enabled_tagsatrace_marker_fd 符号。

以下代码是从 profilo 参考的,可以看到就是简单的 dlopendlsym 两部曲,有意思的是 atrace_enabled_tags 在源码里的定义是 uint64_t,确在这里强转成 std::atomic<uint64_t>

uint64_t                atrace_enabled_tags  = ATRACE_TAG_NOT_READY;
int* atrace_marker_fd = nullptr;
std::atomic<uint64_t>* atrace_enabled_tags = nullptr;

auto sdk = android_get_device_api_level();
{
    ALOGE("SDK %d ", sdk);
    using kwai::linker::DlFcn;
    std::string lib_name("libcutils.so");
    std::string enabled_tags_sym("atrace_enabled_tags");
    std::string fd_sym("atrace_marker_fd");

    if (sdk < 18) {
        lib_name = "libutils.so";
        // android::Tracer::sEnabledTags
        enabled_tags_sym = "_ZN7android6Tracer12sEnabledTagsE";
        // android::Tracer::sTraceFD
        fd_sym = "_ZN7android6Tracer8sTraceFDE";
    }

    void* handle;
    if (sdk < 21) {
        handle = DlFcn::dlopen(lib_name.c_str(), RTLD_LOCAL);
    } else {
        handle = DlFcn::dlopen(lib_name.c_str(), RTLD_GLOBAL);
    }

    atrace_enabled_tags = reinterpret_cast<std::atomic<uint64_t>*>(
            DlFcn::dlsym(handle, enabled_tags_sym.c_str()));

    if (atrace_enabled_tags == nullptr) {
        throw std::runtime_error("Enabled Tags not defined");
    }

    atrace_marker_fd = reinterpret_cast<int*>(DlFcn::dlsym(handle, fd_sym.c_str()));

    if (*atrace_marker_fd == -1) {
        // This is a case that can happen for older Android version i.e. 4.4
        // in which scenario the marker fd is not initialized/opened  by Zygote.
        // Nevertheless for Profilo trace it is not necessary to have an open fd,
        // since all we really need is to ensure that we 'know' it is marker
        // fd to continue writing Profilo logs, thus the usage of marker fd
        // acting really as a placeholder for magic id.
        *atrace_marker_fd = -100;
    }

    DlFcn::dlclose(handle);
}

开启 trace

从源码分析得知,atrace_enabled_tags 是一个 int64_t,每一位代表一个 trace 标签,因此全部置 1 代表打开所有功能,这里在 native 设置,同时也需要在上层设置,反射调用 nativeGetEnabledTags,然后将值设置给 Trace.sEnabledTags

atrace_enabled_tags->exchange(UINT64_MAX);
private object SystraceReflector {
    fun updateSystraceTags() {
        if (sTrace_sEnabledTags != null && sTrace_nativeGetEnabledTags != null) {
            try {
                sTrace_sEnabledTags?.set(
                    null,
                    sTrace_nativeGetEnabledTags?.invoke(null)
                )
            } catch (e: IllegalAccessException) {
            } catch (e: InvocationTargetException) {
            }
        }
    }

    private var sTrace_nativeGetEnabledTags: Method? = null
    private var sTrace_sEnabledTags: Field? = null

    init {
        var m: Method?
        try {
            m = Trace::class.java.getDeclaredMethod("nativeGetEnabledTags")
            m.setAccessible(true)
        } catch (e: NoSuchMethodException) {
            m = null
        }
        sTrace_nativeGetEnabledTags = m
        var f: Field?
        try {
            f = Trace::class.java.getDeclaredField("sEnabledTags")
            f.setAccessible(true)
        } catch (e: NoSuchFieldException) {
            f = null
        }
        sTrace_sEnabledTags = f
    }
}

hook write 调用

开启完后,接下来 hookatrace_marker_fd 的写入,这里使用的 hook 库是 xhook。 很简单,直接 hook 所有加载的 sowrite__write_chk 函数。按照 profilohook 以下 7 个 so 时,trace log 会遗漏,导致生成的 trace.html 里的函数出现 Did not finished,不知道遗漏了哪些 so,索性全部 hook 了,但是这样会导致多出来的结束标志位,但是不影响可视化,所有先将就着吧 :P 。

"libandroid_runtime.so",
"libui.so",
"libgui.so",
"libart.so",
"libhwui.so",
"libEGL.so",
"libcutils.so"
bool should_log_systrace(int fd, size_t count) {
    return systrace_installed && fd == *atrace_marker_fd;
}

ssize_t write_hook(int fd, const void* buf, size_t count) {
    if (should_log_systrace(fd, count)) {
        log_systrace(buf, count);
        return count;
    }
    return write(fd, buf, count);
}

size_t (*orig__write_chk)(int fd, const void* buf, size_t count, size_t buf_size);

size_t hook__write_chk(int fd, const void* buf, size_t count, size_t buf_size) {
    if (should_log_systrace(fd, count)) {
        log_systrace(buf, count);
        return count;
    }
    return orig__write_chk(fd, buf, count, buf_size);
}

void hookLoadedLibs() {
    xhook_register(".*\\.so$", "write", (void *) write_hook, nullptr);
    xhook_register(".*\\.so$", "__write_chk", (void *) hook__write_chk,
                   (void **) &orig__write_chk);
    // 忽略对当前 so 的 hook               
    xhook_ignore(".*toy_lib\\.so$", "write");
    xhook_ignore(".*toy_lib\\.so$", "__write_chk");

    xhook_refresh(0);
    xhook_clear();
}

保存日志

从之前文章得知,一个可以使用 systrace.py 生成可视化的 htmltrace log,必须以下 3 行日志开头,可能 2 行也行,没测试。

TRACE:
# tracer: nop
#

同时日志的格式需要是一下格式

<线程名>-<线程id>  [000] ...1 <时间-秒>: tracing_mark_write: <B|E>|<进程id>|<TAG>

但是我们获取到的日志 只有最后一小节 <B|E>|<进程id>|<TAG>,因此需要拼接好后再写入,这里需要获取线程名字、线程 idMONOTONIC 时间。

线程 id 可以通过 gettid()获取。

线程名字可以从 /proc/<pid>/task/<tid>/comm 读取,pid 通过 getpid() 获取。

时间通过 clock_gettime(CLOCK_MONOTONIC, &t) 获取,需要从纳秒转成秒,把 int64_t 的转成 double 后再去转成秒,否则时间精度不对。

拼接完后,写入文件,这里在 /data/local/tmp 目录创建了一个文件,但是并没有创建成功,可能是权限的问题,只能临时收到在命令行创建了。为了代码简洁,这里是都是同步写入日志。cpp 基本没写过,都是临时摸索复制百度谷歌来的,写得有点辣眼睛,请多多包涵,大概意思就是那样了 :P。

int fd = 0;
std::map<int, std::string> mapThreadName;

std::string getThreadName(int tid) {
    std::map<int, std::string>::iterator iter;
    iter = mapThreadName.find(tid);

    if (iter != mapThreadName.end()) {
        return iter->second;
    } else {
        char fname[128];
        int len = sprintf(fname, "/proc/%d/task/%d/comm", getpid(), tid);
        int cfd = open(fname, O_RDONLY);
        if (cfd) {
            char name[32];
            int c = read(cfd, name,  32);
            if (c > 0) {
                mapThreadName[tid] = std::string(name, c - 1);
                return mapThreadName[tid];
            }
        }
    }
    return std::to_string(tid);
}

typedef int64_t nsecs_t; // nano-seconds
nsecs_t systemTime()
{
    struct timespec t;
    t.tv_sec = t.tv_nsec = 0;
    clock_gettime(CLOCK_MONOTONIC, &t);
    return nsecs_t(t.tv_sec)*1000000000LL + t.tv_nsec;
}


void log_systrace(const void* buf, size_t count) {
    const char* msg = reinterpret_cast<const char*>(buf);
    double f = double(systemTime()) / 1000000000;
    switch (msg[0]) {
        case 'B':
            ALOGE("%s", buf); //"B|<pid>|<name>"
            if (fd) {
                char buf1[1024];
                int len = snprintf(buf1, sizeof(buf1), "%s-%d [000] ...1 %.6f: tracing_mark_write: %s\n", getThreadName(gettid()).c_str(), gettid(), f, msg);
                write(fd, buf1, len);
            }
            break;
        case 'E':
            ALOGE("E");  // "E"
            if (fd) {
                char buf1[1024];
                int len = snprintf(buf1, sizeof(buf1), "%s-%d [000] ...1 %.6f: tracing_mark_write: E\n", getThreadName(gettid()).c_str(), gettid(), f);
                write(fd, buf1, len);
            }
            break;
        default:
            return;
    }
}

void installAtrace() {
    fd = open("/data/local/tmp/mytrace.txt", O_WRONLY | O_CREAT, 0666);
    std::string head("TRACE:\n# tracer: nop\n#\n");
    write(fd, head.c_str(), head.size());
    
    // 开始 hook 后,打开 trace 功能
}

查看日志文件,可以看到正确生成了所需的格式

TRACE:
# tracer: nop
#
RenderThread-20036 [000] ...1 241751.465787: tracing_mark_write: B|19945|AttachCurrentThread
RenderThread-20036 [000] ...1 241751.468185: tracing_mark_write: E
com.example.toy-19945 [000] ...1 241751.468293: tracing_mark_write: B|19945|postAndWait
com.example.toy-19945 [000] ...1 241751.477535: tracing_mark_write: E
com.example.toy-19945 [000] ...1 241751.493119: tracing_mark_write: B|19945|postAndWait
com.example.toy-19945 [000] ...1 241751.493345: tracing_mark_write: E
com.example.toy-19945 [000] ...1 241751.493457: tracing_mark_write: B|19945|postAndWait
com.example.toy-19945 [000] ...1 241751.493575: tracing_mark_write: E
RenderThread-20036 [000] ...1 241751.494002: tracing_mark_write: B|19945|notifyFramePending
RenderThread-20036 [000] ...1 241751.494100: tracing_mark_write: E
com.example.toy-19945 [000] ...1 241751.499166: tracing_mark_write: E
com.example.toy-19945 [000] ...1 241751.499489: tracing_mark_write: B|19945|postAndWait
com.example.toy-19945 [000] ...1 241751.499661: tracing_mark_write: E
com.example.toy-19945 [000] ...1 241751.501794: tracing_mark_write: B|19945|Choreographer#doFrame
com.example.toy-19945 [000] ...1 241751.501954: tracing_mark_write: B|19945|input
com.example.toy-19945 [000] ...1 241751.502065: tracing_mark_write: E
com.example.toy-19945 [000] ...1 241751.502129: tracing_mark_write: B|19945|traversal

最后一步就是将保存好的日志,使用 systrace 工具转换,具体就不演示了。

同时这里只是 APP 的日志,如果需要其他日志,如 CPU 调度,还需要像 atrace 工具那样去打开真正的 ftrace 的功能,再将其与整个 APP 日志合并。

参考文档

抖音 Android 性能优化系列:新一代全能型性能分析工具 Rhea

Systrace 的原理、流程及定制

profilo

xhook

AndroidAdvanceWithGeektime_Chapter06