Android Hook OpenGL ES (TLSHook)

June 19, 2022

Hook 常用在调试或监控场景,比如 hook malloc/free 可以用来排查内存泄漏,hook OpenGL api 可以用来调试渲染效果等(如 RenderDoc),Android 常用的 native hook 主要有 PLT hook 和 inline hook 两类,PLT hook 通过修改动态库的 plt/got 表来替换链接的函数地址,inline hook 则是修改内存中的方法指令,实现相对复杂,这里介绍一种针对 OpenGL ES 函数的 hook 方法:TLS hook,实现简单并且稳定性&兼容性高。

TLS hook 基于 Android 系统 OpenGL ES 驱动加载逻辑来实现,我们先基于源码梳理下这部分的流程:

线程 TLS

Android 在线程 TLS 中存储了一些基础数据,其结构如下:bionic_tls.h

struct bionic_tcb {
  void* raw_slots_storage[BIONIC_TLS_SLOTS];
};

可以看看该结构体在 arm64 平台上的写入:__set_tls.c

__LIBC_HIDDEN__ void __set_tls(void* tls) {
  asm("msr tpidr_el0, %0" : : "r" (tls));
}

读取:tls.h

# define __get_tls() ({ void** __val; __asm__("mrs %0, tpidr_el0" : "=r"(__val)); __val; })

可以看到操作的是 tpidr_el0 寄存器,这个寄存器是用来给操作系统存储线程信息的,详见文档 TPIDR-EL0–EL0-Read-Write-Software-Thread-ID-Register。tls 指针数组的每一项具体定义具体可以查看 tls_defines.h,其中对我们比较关键的是:

#define TLS_SLOT_OPENGL_API       4

接下来我们来看 EGL 如何和这个 tls slot 交互

EGL 初始化

我们知道 OpenGL ES 的实现(驱动)是不同厂商提供的,因此 EGL 需要从厂商提供的驱动库(libGLESv1_CM.so、libGLESv2.so)中加载 OpenGL ES api 的函数地址。首先是一些结构体定义:hooks.h

struct gl_hooks_t {
    struct gl_t {
        #include "entries.in"
    } gl;
    struct gl_ext_t {
        __eglMustCastToProperFunctionPointerType extensions[MAX_NUMBER_OF_GL_EXTENSIONS];
    } ext;
};

// We have a dedicated TLS slot in bionic
inline gl_hooks_t const * volatile * get_tls_hooks() {
    volatile void *tls_base = __get_tls();
    gl_hooks_t const * volatile * tls_hooks =
            reinterpret_cast<gl_hooks_t const * volatile *>(tls_base);
    return tls_hooks;
}

inline EGLAPI gl_hooks_t const* getGlThreadSpecific() {
    gl_hooks_t const * volatile * tls_hooks = get_tls_hooks();
    gl_hooks_t const* hooks = tls_hooks[TLS_SLOT_OPENGL_API];
    return hooks;
}

这里定义了 getGlThreadSpecific 函数,返回 gl_hooks_t 结构体,而 #include "entries.in" 其实就是定义了一系列函数指针,即 struct gl_t 就是所有 OpenGL ES api 函数指针列表,tls_hooks 地址就是前面提到的特殊寄存器中的值,再通过 TLS_SLOT_OPENGL_API 索引,转化为 gl_hooks_t 结构体指针,如下图:

我们知道 OpenGL ES api 调用是线程关联的,在当前线程没有绑定 Context(MakeCurrent) 情况下调 gl api 会报错,来看一下实现:egl.cpp

static pthread_once_t once_control = PTHREAD_ONCE_INIT;
static int sEarlyInitState = pthread_once(&once_control, &early_egl_init);

void setGLHooksThreadSpecific(gl_hooks_t const* value) {
    setGlThreadSpecific(value);
}

static int gl_no_context() {
    if (egl_tls_t::logNoContextCall()) {
        const char* const error = "call to OpenGL ES API with "
                                  "no current context (logged once per thread)";
        if (LOG_NDEBUG) {
            ALOGE(error);
        } else {
            LOG_ALWAYS_FATAL(error);
        }
        if (base::GetBoolProperty("debug.egl.callstack", false)) {
            CallStack::log(LOG_TAG);
        }
    }
    return 0;
}

static void early_egl_init(void) {
    int numHooks = sizeof(gHooksNoContext) / sizeof(EGLFuncPointer);
    EGLFuncPointer* iter = reinterpret_cast<EGLFuncPointer*>(&gHooksNoContext);
    for (int hook = 0; hook < numHooks; ++hook) {
        *(iter++) = reinterpret_cast<EGLFuncPointer>(gl_no_context);
    }

    setGLHooksThreadSpecific(&gHooksNoContext);
}

即通过 pthread_once 执行一次 early_egl_init,将 gl_no_context 函数指针设置到 gHooksNoContext 实例中的所有函数指针,然后将 gHooksNoContext 设置到 tls,本质上就是用 gl_no_context 来填充 TLS_SLOT_OPENGL_API 指向的函数指针列表,所以这个时候调任何 OpenGL ES api,都会跑到 gl_no_context 函数。

接下来看下 EGL 如何加载厂商驱动中的函数指针,关键函数为 egl_init_drivers (可以从这里看到调用 eglGetDisplay 时会先调用 egl_init_drivers)

egl_connection_t gEGLImpl;
gl_hooks_t gHooks[2];

static EGLBoolean egl_init_drivers_locked() {
    if (sEarlyInitState) {
        // initialized by static ctor. should be set here.
        return EGL_FALSE;
    }

    // get our driver loader
    Loader& loader(Loader::getInstance());

    // dynamically load our EGL implementation
    egl_connection_t* cnx = &gEGLImpl;
    cnx->hooks[egl_connection_t::GLESv1_INDEX] = &gHooks[egl_connection_t::GLESv1_INDEX];
    cnx->hooks[egl_connection_t::GLESv2_INDEX] = &gHooks[egl_connection_t::GLESv2_INDEX];
    cnx->dso = loader.open(cnx);

    // Check to see if any layers are enabled and route functions through them
    if (cnx->dso) {
        // Layers can be enabled long after the drivers have been loaded.
        // They will only be initialized once.
        LayerLoader& layer_loader(LayerLoader::getInstance());
        layer_loader.InitLayers(cnx);
    }

    return cnx->dso ? EGL_TRUE : EGL_FALSE;
}

具体的驱动加载逻辑在 loader.open 函数中(详见源码),加载后的指针存放在 egl_connection_t 结构体中 (gEGLImpl 实例),这个结构体的定义:egldefs.h

struct egl_connection_t {
    enum { GLESv1_INDEX = 0, GLESv2_INDEX = 1 };
    ...
    gl_hooks_t* hooks[2];
    ...
};

其中 hooks 指针数组指向 gHooks 全局对象,其中存储的就是从驱动中加载的不同版本 OpenGL ES 函数指针。

到这里还没完,我们需要在要用的时候把 TLS_SLOT_OPENGL_API slot 填充为从驱动加载的函数指针,这部分逻辑实现在 EGL 的 MakeCurrent 中:egl_platform_entries.cpp

EGLBoolean eglMakeCurrentImpl(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
    ...
    EGLBoolean result = dp->makeCurrent(c, cur_c, draw, read, ctx, impl_draw, impl_read, impl_ctx);
    if (result == EGL_TRUE) {
        if (c) {
            setGLHooksThreadSpecific(c->cnx->hooks[c->version]);
            egl_tls_t::setContext(ctx);
            _c.acquire();
            _r.acquire();
            _d.acquire();
        } else {
            setGLHooksThreadSpecific(&gHooksNoContext);
            egl_tls_t::setContext(EGL_NO_CONTEXT);
        }
    } else {
        // this will ALOGE the error
        egl_connection_t* const cnx = &gEGLImpl;
        result = setError(cnx->egl.eglGetError(), (EGLBoolean)EGL_FALSE);
    }
    return result;
}

EGL 内部对 EGLContext 的存储也是基于 TLS,详见 egl_tls_t 定义, 从上面 eglMakeCurrentImpl 函数实现可以看出,如果是 MakeCurrent 的是 EGL_NO_CONTEXT,则将 TLS_SLOT_OPENGL_API slot 填充为 gl_no_context,否则填充为 egl_connection_t 中的 hooks[version](驱动加载得到的函数指针)。同时,MakeCurrent 还会将 ctx 设置到 TLS egl_tls_t::setContext(ctx)

TLSHook 实现

在理解 EGL 的加载流程后,要实现 Hook 就比较简单了,我们只需要在合适的时候把 TLS_SLOT_OPENGL_API 指向的函数指针(gHooks全局对象)换掉即可,主要步骤如下:

1、获取 tls

这里我们直接使用 Android 源码 以兼容不同平台

#if defined(__aarch64__)  
# define __get_tls() ({ void** __val; __asm__("mrs %0, tpidr_el0" : "=r"(__val)); __val; })  
#elif defined(__arm__)  
# define __get_tls() ({ void** __val; __asm__("mrc p15, 0, %0, c13, c0, 3" : "=r"(__val)); __val; })  
#elif defined(__i386__)  
# define __get_tls() ({ void** __val; __asm__("movl %%gs:0, %0" : "=r"(__val)); __val; })  
#elif defined(__x86_64__)  
# define __get_tls() ({ void** __val; __asm__("mov %%fs:0, %0" : "=r"(__val)); __val; })  
#else  
#error unsupported architecture  
#endif

void *volatile *get_tls_hooks() {  
  volatile void *tls_base = __get_tls();  
  void *volatile *tls_hooks = reinterpret_cast<void *volatile *>(tls_base);  
  return tls_hooks;  
}  

void *getGlThreadSpecific(int idx) {  
  void *volatile *tls_hooks = get_tls_hooks();  
  void *hooks = tls_hooks[idx];  
  return hooks;  
}
2、确保已加载驱动

根据前面的分析,如果线程当前没有设置 EGLContext(MakeCurrent),tls 指向的是 gHooksNoContext,因此需要手动创建一个并 MakeCurrent:

bool needCreateEGL = (eglGetCurrentContext() == EGL_NO_CONTEXT);  
if (needCreateEGL) {   
  if (egl_core.create(1, 1)) {  
    egl_core.makeCurrent();    
  }
}
3、替换函数

如前面的分析,gl_hooks_t 中的指针列表是按 entries.in 中的定义顺序排列,因此每个 OpenGL ES api 都有一个数组 index(也即相对起始地址的偏移),根据这个 index,找到对应位置替换函数指针即可:

size_t *tlsPtr = static_cast<size_t *>(getGlThreadSpecific(TLS_SLOT_OPENGL_API));
size_t *slot = tlsPtr + index;  

*old_func = reinterpret_cast<size_t *>(*slot);  // 保存原指针 
*slot = reinterpret_cast<size_t>(new_func);  // 替换为新指针
4、兼容

整个 Hook 看起来比较简单,但存在一定的兼容性问题,即 OpenGL ES api 的 index 并不是固定的,可以查看 entries.in 文件的提交记录,这里有两种解决方法:分版本兼容或手动计算 index。

分版本兼容:我们根据提交记录,保存现有不同版本的 entries.in,然后根据系统版本加载:

    int idx = 0;  

#define GL_ENTRY(_r, _api, ...) hookMap[#_api] = idx++;  
    if (api_level >= 28) {  
#include "entry/entries.28.in"  
    } else if (api_level >= 24) {  
#include "entry/entries.24.in"  
    } else if (api_level >= 21) {  
#include "entry/entries.21.in"  
    } else if (api_level >= 18) {  
#include "entry/entries.18.in"  
    } else if (api_level >= 16) {  
#include "entry/entries.16.in"  
    }

这样实现简单,除非厂商私自改动这个 entries.in,否则都是兼容的,实测云平台的80+款设备都没有兼容问题。

另一种方法是手动计算 index,即我们先把地址填充成特定的 Hook 函数,然后尝试去调一下要 Hook 的 api,如果 Hook 成功,即找到了该 api 的 index,否则继续测试下一个地址,可以看下微信开源的 Matrix 中的实现

extern "C"
JNIEXPORT jint JNICALL
Java_com_tencent_matrix_openglleak_detector_FuncSeeker_getGlTexImage2DIndex(JNIEnv *env,
                                                                            jclass clazz) {
    gl_hooks_t *hooks = get_gl_hooks();
    if (NULL == hooks) {
        return -1;
    }

    for (i_glTexImage2D = 0; i_glTexImage2D < 1000; i_glTexImage2D++) {
        if (has_hook_glTexImage2D) {
            i_glTexImage2D = i_glTexImage2D - 1;

            void **method = (void **) (&hooks->gl.foo1 + i_glTexImage2D);
            *method = (void *) _system_glTexImage2D;
            break;
        }

        if (_system_glTexImage2D != NULL) {
            void **method = (void **) (&hooks->gl.foo1 + (i_glTexImage2D - 1));
            *method = (void *) _system_glTexImage2D;
        }

        void **replaceMethod = (void **) (&hooks->gl.foo1 + i_glTexImage2D);
        _system_glTexImage2D = (System_GlTexImage2D) *replaceMethod;

        *replaceMethod = (void *) _my_glTexImage2D;

        glTexImage2D(0, 0, 0, 0, 0, 0, 0, 0, NULL);
    }

    // release
    _system_glTexImage2D = NULL;
    has_hook_glTexImage2D = false;
    int result = i_glTexImage2D;
    i_glTexImage2D = 0;

    return result;
}

这种方法兼容性更好,但需要提前对 api 的 index 进行查找,实现相对复杂一点。

示例 demo

首先定义 Hook 函数,这里我们 Hook glClearColor,不管什么参数都改为蓝色 (0.f, 0.f, 1.f, 1.f)

// origin function  
PFNGLCLEARCOLORPROC cb_glClearColor = nullptr;  

// new function  
void hook_glClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) {  
  LOGD("hook call glClear: (%f, %f, %f, %f)", red, green, blue, alpha);  
  if (cb_glClearColor) {  
    cb_glClearColor(0.f, 0.f, 1.f, 1.f);  
  }  
}

然后开始 Hook

TLSHook::tls_hook_init();
TLSHook::tls_hook_func("glClearColor", 
                       (void *) hook_glClearColor,
                       (void **) &cb_glClearColor);

再创建个简单的 GLSurfaceView,指定 clear color 为红色

surfaceView.setRenderer(new GLSurfaceView.Renderer() {  
    @Override  
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {  
    }  

    @Override  
    public void onSurfaceChanged(GL10 gl, int width, int height) {  
    }  

    @Override  
    public void onDrawFrame(GL10 gl) {  
        GLES20.glClearColor(1.f, 0.f, 0.f, 1.f);  
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);  
    }  
});

最终跑起来可以看到 GLSurfaceView 显示蓝色,Hook 成功。

Github

完整代码详见:https://github.com/keith2018/TLSHook