实现无入侵式C++代码mock工具

简介: 为了实现真正无侵入式的mock,我们基于开源Hook框架Frida-gum提供的API,利用C++模板进行封装,作者编写了一个简单实用的mock工具,在此开源分享(代码详见附录)。


背景

在单元测试中,往往需要减少被测函数的外部依赖,如网络访问、数据库访问等。我们希望有一个mock工具能让我们轻松地屏蔽掉外部依赖。
C++的开源mock工具比较少,而且大多是基于多态实现的(如gmock),只支持mock虚函数,需要对原有代码结构进行调整,或编写mock类继承自原有类才能使用,工作量太大,我们的目标是单测代码与原有项目工程隔离,不需要为了单测对线上代码逻辑进行太大修改。为了实现真正无侵入式的mock,我们基于开源Hook框架Frida-gum提供的API,利用C++模板进行封装,编写了一个简单实用的mock工具,在此开源分享,希望能在单测工程的搭建上有所帮助,大家可以根据自己的项目情况,在此基础上扩展自己需要的功能。

Frida是一种动态插桩工具,可以插入一些代码到Win、Mac、Linux、Android或者iOS原生app的内存空间,动态地监视和修改其行为,在安卓逆向工程上应用非常多。


我们的mock工具也正是利用了Frida能动态修改函数执行的功能,在此我们不对其具体原理做过多阐述,有兴趣的同学可以移步附录的参考链接。



实现的功能


mock工具实现了最实用的函数替换功能,支持所有类型的函数替换,如:成员函数、虚函数、静态函数、系统库的全局函数、链接库的函数,暂时不支持对匿名函数进行替换(需要拿到函数指针才行)。


 MOCK宏


MOCK(target, alter)


第一个参数为被mock的函数指针,第二个参数为想要替换的lambda或者函数指针。


 MOCK_RETURN宏


MOCK_RETURN(target, alter)


对MOCK宏的封装,替换掉被mock函数的返回值,只支持值类型。


 ExpectTimes


被mock函数期望的调用次数。

MOCK_RETURN(&MyTest::testMember, 0)->ExpectTimes(1);    //测试运行次数


测试用例运行完成时,如果被mock的函数运行次数和期望的次数不一样,则测试用例运行失败。


 自动回滚mock


TEST(mock, mock脱离作用域自动失效) {    int input = 10; //入参
    {        MOCK_RETURN(&globalFunction, 100);        ASSERT_EQ(globalFunction(input), 100);  //修改了返回值    }    ASSERT_EQ(globalFunction(input), input);  //mock析构后失效}


利用此特性可以使单元测试每个测试用例相对独立,所以在一个测试用例中mock掉的函数,不影响其他用例,如果需要全局性的mock,需要将mock宏写在全局变量区域。


 跨平台


在MacOS、Linux、Windows、Android、iOS等平台都能使用。


使用方式示例

下面的示例都基于这个类来使用mock

/** * 被mock的类 */class MyTest {public:    /**     * 静态函数     * @return      */    static int testStatic() {        return 0;    }
    /**     * 普通成员函数     * @param input      * @return      */    int testMember(int input) {        return input;    }
    /**     * const 成员函数     * @return      */    int constMemberFun() const {        return 0;    }
    /**     * 测试函数重载,输入int     * @param intInput      * @return      */    static int overloadFunction(int intInput) {        return 0;    }
    static int overloadFunction(double doubleInput) {        return 0;    }};


 mock静态函数

// 将testStatic的返回值改为10MOCK(&MyTest::testStatic, []() {    return 10;  // 返回值从0修改为10});
// 简单写法,效果同上,使用MOCK_RETURN 可以替换函数返回值MOCK_RETURN(&MyTest::testStatic, 10);


 mock成员函数

// 将testMember的返回值改为 input + 1MOCK(&MyTest::testMember, [](auto && self, auto && input) {    // 成员函数被mock后,第一个参数为this指针,这里用self替换    return input + 1;  //mock后返回input + 1(之前是返回input)});int input = 10; //入参ASSERT_EQ(MyTest().testMember(input), input + 1);


 mock函数重载


// 使用static_cast指定对应的重载获取函数指针MOCK(static_cast<int(*)(int)>(&MyTest::overloadFunction), [](auto && ...args) {    return 100;});ASSERT_EQ(MyTest().overloadFunction(10), 100);ASSERT_EQ(MyTest().overloadFunction(10.0), 0);


 小结


  1. 通过MOCK宏来替换整个函数
  2. 通过MOCK_RETURN宏来替换函数返回值
  3. 可以使用 auto&& 简化你的函数入参类型



什么时候适合使用mock


考虑在以下场景使用mock工具,可以减少你的单元测试成本:

  1. 代码中依赖的某个功能在你本次测试并不关心,如:db数据读取,发请求
  2. 测试用例依赖一些复杂的数据源,如:db数据读取,流水线上游数据,网络请求
  3. 一些非幂等性的函数调用或者结果返回不稳定的函数调用,如:随机数获取,时间获取,db写入
  4. 对象的某些状态难以创建或者重现,如:网络错误或者文件读写错误
  5. 验证一些中间过程值,如:你的函数没有返回值,或者中间过程值不方便验证,可以mock中间某个函数调用来验证中间过程结果是否正确


注意事项

  1. 成员函数第一个函数入参为this指针
  2. 在新的函数中抛出的异常无法被原有的上层正常catch
  3. 断点调试时行号可能对不上,手动解除宏的封装可以解决,原因未知
  4. 建议单测工程不要开优化,否则会导致一些函数mock失败
  5. Frida会被内存泄漏检测工具报内存泄漏
  6. 对动态库使用mock工具时,库编译时需要加上 -fPIC 参数


附录

 参考文档


  1. 「frida git仓库」https://github.com/frida/frida
  2. 「Frida工作原理学习」https://www.wangan.com/p/7fy7f8bd4ab57950
  3. 「frida-gum代码阅读笔记」https://o0xmuhe.github.io/2019/11/15/frida-gum%E4%BB%A3%E7%A0%81%E9%98%85%E8%AF%BB/
  4. 「FRIDA-GUM源码解读」https://jmpews.github.io/2017/06/27/pwn/frida-gum%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/


 代码


请自行下载对应平台的frida-gum头文件&链接库:https://github.com/frida/frida/releases


代码中使用了gtest,可以视情况决定需要是否保留

#pragma once
#include <frida/frida-gum.h>#include <functional>#include <memory>#include <string>#include <type_traits>#include "gtest/gtest.h"  // 使用了 EXPECT_EQ, 可以视情况保留
#define _COMBINE_(X, Y) X##Y  // helper macro#define COMBINE(X, Y) _COMBINE_(X,Y)#define MOCK(target, alter) auto && COMBINE(mock, __LINE__) = mock::Mock(target, alter)#define MOCK_RETURN(target, alter) auto && COMBINE(mock, __LINE__) = mock::MockReturn(target, alter)
namespace {/** * 将任意对象转换成void * * 用于函数指针的转换 * @tparam T * @param t * @return 转换成void * 的函数指针 */template<typename T>void * toVoidPtr(T src) {    union {        void * ptr;        T src;    } Func{};    Func.src = src;    return Func.ptr;}}
namespace mock {
/** * 一个对象管理一个mock,析构时自动回滚 * @tparam RET * @tparam ARGS */template<typename RET, typename ...ARGS>class MockHandler : public std::enable_shared_from_this<MockHandler<RET, ARGS...>> {public:
    /**     * 这个函数作为回调函数传给frida     * @param args     * @return     */    static RET _invoke(ARGS... args) {        auto * context = gum_interceptor_get_current_invocation();        auto * handler = reinterpret_cast<MockHandler<RET, ARGS...> *>(                gum_invocation_context_get_replacement_data(context));        ++handler->mRunTimes;        return handler->mAlterFun(std::forward<ARGS>(args)...);    }
    MockHandler(void * target, const std::function<RET(ARGS...)> & fun) : mAlterFun(fun), mTarget(target) {        gum_init_embedded();        mInterceptor = gum_interceptor_obtain();        gum_interceptor_begin_transaction(mInterceptor);        gum_interceptor_replace(mInterceptor, mTarget, toVoidPtr(_invoke), toVoidPtr(this));        gum_interceptor_end_transaction(mInterceptor);    }
    /**     * 析构时会回滚已经替换掉的函数,实现测试用例隔离     */    ~MockHandler() {        if (mInterceptor == nullptr) {            return;        }        gum_interceptor_begin_transaction(mInterceptor);        gum_interceptor_revert(mInterceptor, mTarget);        gum_interceptor_end_transaction(mInterceptor);
        g_object_unref(mInterceptor);
        if (mExpectRunTimes >= 0) {            EXPECT_EQ(mExpectRunTimes, mRunTimes) << "运行次数和期望不相等";        }    }
    MockHandler(const MockHandler &) = delete;
    MockHandler & operator=(const MockHandler &) = delete;
    MockHandler(MockHandler &&) = delete;
    MockHandler & operator=(MockHandler &&) = delete;
    /**     * 设置期望的运行次数     * @param times     * @return this指针     */    auto ExpectTimes(int times) {        mExpectRunTimes = times;        return std::enable_shared_from_this<MockHandler<RET, ARGS...>>::shared_from_this();    }
private:    GumInterceptor * mInterceptor{};
    //目标函数地址    void * mTarget{};
    //用于替换的函数    std::function<RET(ARGS...)> mAlterFun;
    //期望的运行次数    int mExpectRunTimes{-1};
    //被mock函数已经运行的次数    std::atomic_int mRunTimes{};};
/** * 使用alter替换掉target函数指针,alter可以是lambda,也可以是函数指针 * @tparam RET * @tparam CLS * @tparam ARG * @tparam T * @param target * @param alter * @return MockHandler */template<typename RET, class CLS, typename ...ARG, typename T>auto Mock(RET(CLS::* target)(ARG...), T alter) {    return std::make_shared<MockHandler<RET, CLS *, ARG...>>(toVoidPtr(target), alter);;}
template<typename RET, class CLS, typename ...ARG, typename T>auto Mock(RET(CLS::* target)(ARG...) const, T alter) {    return std::make_shared<MockHandler<RET, CLS *, ARG...>>(toVoidPtr(target), alter);}
template<typename RET, typename ...ARG, typename T>auto Mock(RET(* target)(ARG...), T alter) {    return std::make_shared<MockHandler<RET, ARG...>>(toVoidPtr(target), alter);;}
/** * mock返回值,mock的简便用法 * @tparam RET * @tparam CLS * @tparam ARG * @param target * @param ret * @return MockHandler */template<typename RET, class CLS, typename ...ARG>auto MockReturn(RET(CLS::* target)(ARG...), RET ret) {    return std::make_shared<MockHandler<RET, CLS *, ARG...>>(toVoidPtr(target), [=](auto && ...) {        return ret;    });}
template<typename RET, class CLS, typename ...ARG>auto MockReturn(RET(CLS::* target)(ARG...) const, RET ret) {    return std::make_shared<MockHandler<RET, CLS *, ARG...>>(toVoidPtr(target), [=](auto && ...) {        return ret;    });}
template<typename RET, typename ...ARG>auto MockReturn(RET(target)(ARG...), RET ret) {    return std::make_unique<MockHandler<RET, ARG...>>(toVoidPtr(target), [=](auto && ...) {        return ret;    });}}





# CMakeLists.txtadd_link_options(-lresolv -lpthread -ldl)target_link_libraries(${YOUR_PROJECT} ${CMAKE_CURRENT_SOURCE_DIR}/libfrida-gum.a)


相关文章
|
3月前
|
C++ Windows
应用程序无法正常启动(0xc0000005)?C++报错0xC0000005如何解决?使命召唤17频频出现闪退,错误代码0xC0000005(0x0)
简介: 本文介绍了Windows应用程序出现错误代码0xc0000005的解决方法,该错误多由C++运行库配置不一致或内存访问越界引起。提供包括统一运行库配置、调试排查及安装Visual C++运行库等解决方案,并附有修复工具下载链接。
1215 1
|
10月前
|
存储 安全 C语言
C++ String揭秘:写高效代码的关键
在C++编程中,字符串操作是不可避免的一部分。从简单的字符串拼接到复杂的文本处理,C++的string类为开发者提供了一种更高效、灵活且安全的方式来管理和操作字符串。本文将从基础操作入手,逐步揭开C++ string类的奥秘,帮助你深入理解其内部机制,并学会如何在实际开发中充分发挥其性能和优势。
|
5月前
|
API 数据安全/隐私保护 C++
永久修改机器码工具, exe一机一码破解工具,软件机器码一键修改工具【c++代码】
程序实现了完整的机器码修改功能,包含进程查找、内存扫描、模式匹配和修改操作。代码使用
|
6月前
|
C++
爱心代码 C++
这段C++代码使用EasyX图形库生成动态爱心图案。程序通过数学公式绘制爱心形状,并以帧动画形式呈现渐变效果。运行时需安装EasyX库,教程链接:http://【EasyX图形库的安装和使用】https://www.bilibili.com/video/BV1Xv4y1p7z1。代码中定义了屏幕尺寸、颜色数组等参数,利用随机数与数学函数生成动态点位,模拟爱心扩散与收缩动画,最终实现流畅的视觉效果。
931 0
|
算法 安全 C++
提高C/C++代码的可读性
提高C/C++代码的可读性
274 4
|
Linux C语言 C++
vsCode远程执行c和c++代码并操控linux服务器完整教程
这篇文章提供了一个完整的教程,介绍如何在Visual Studio Code中配置和使用插件来远程执行C和C++代码,并操控Linux服务器,包括安装VSCode、安装插件、配置插件、配置编译工具、升级glibc和编写代码进行调试的步骤。
2801 0
vsCode远程执行c和c++代码并操控linux服务器完整教程
2合1,整合C++类(Class)代码转换为MASM32代码的平台
2合1,整合C++类(Class)代码转换为MASM32代码的平台
继续更新完善:C++ 结构体代码转MASM32代码
继续更新完善:C++ 结构体代码转MASM32代码
|
C++ Windows
HTML+JavaScript构建C++类代码一键转换MASM32代码平台
HTML+JavaScript构建C++类代码一键转换MASM32代码平台
HTML+JavaScript构建一个将C/C++定义的ANSI字符串转换为MASM32定义的DWUniCode字符串的工具
HTML+JavaScript构建一个将C/C++定义的ANSI字符串转换为MASM32定义的DWUniCode字符串的工具