专栏首页腾讯技术工程官方号的专栏效能优化实践:C/C++单元测试万能插桩工具

效能优化实践:C/C++单元测试万能插桩工具

作者:mannywang,腾讯安全平台后台开发

研发效能是一个涉及面很广的话题,它涵盖了软件交付的整个生命周期,涉及产品、架构、开发、测试、运维,每个环节都可能影响顺畅、高质量地持续有效交付。在腾讯安全平台部实际研发与测试工作中我们发现,代码插桩隔离是单元测试工作中的一个强需求,然而业界现有 C/C++插桩工具由于使用上的局限性,运行效率和体验仍有很大改善空间。本文介绍了团队基于研效优化实践而自研的动态插桩工具,旨在实现单元测试的轻量化运行,提高代码覆盖率,从而助力研发团队的效能提升。

问题&思路

目前存在的 C/C++插桩工具,基本上都有各种使用上的局限,比如流行的 gmock,只能对 C++的虚函数进行插桩替换,针对非虚函数,则需要先对被测代码进行改造;同时对于系统接口,C 风格的第三方库代码,也无能为力。

如果可以绕开编译器,直接从底层入手,比如做机器指令修改,则可以不受语法及编译器的束缚,直接达到目的,这样在使用中就 几乎不受限制

原理

C/C++语言编译后的可执行体,其实就是一个个的函数实现,每个函数的开头就是它的入口。一个函数 A 调用另一个函数 B,就是代码在执行过程中,控制流从函数 A 的某处跳到了函数 B 的开头,所以如果想用一个新的函数 C 取代函数 B,可以在函数 B 的开头用机器码的形式写入如下等价逻辑:

MOVQ ADDRESS_OF_C %RAX //将函数C的地址放到寄存器RAX
JMPQ *RAX           //无条件跳转到RAX所指向的位置

这样,当控制流从函数 A 进入函数 B 的开始位置的时候,即会执行上述代码,从而直接跳转到 C 的开头处。其最终效果,是所有对函数 B 的调用,都如同直接调用了函数 C。

基于上述原理,被插桩的代码包括第三方库,如 MySql、其他同事未完成的模块、甚至是操作系统的 API 接口,如 read、select 等

同时,桩函数不仅可以模拟原函数的返回值,实际上它作为一个普通的 C 函数,对原函数有完全的操作能力,比如可以访问传递给原函数调用真实的参数、C++成员变量(针对对成员函数的模拟),给定任意的返回值,访问全局变量、对调用进行计数等

实际实现中,考虑到不同测试用例间的互不干扰,除了能执行函数替换,还需要在执行完一个测试时还原现场。这些具体细节可以直接参考代码。

使用

对全局函数插桩

原始函数:

int global(int a, int b) {
    return a + b;
}

对应的桩函数:

int fake_global(int a, int b) {
    //校验参数正确性,确定被测代码传入了正确的值
    assert(a == 3);
    assert(b == 2);
    //给一个返回值,配合被测代码走特定分支
    return a - b;
}

插桩示例:

assert(global(3, 2) == 5);

//通过mock调用,完成函数动态替换
assert(0 == mock(&global, &fake_global));

//调用mock后的函数,可以看到返回值变了
assert(global(3, 2) == 1);

//结束mock
reset();

//函数行为恢复
assert(global(3, 2) == 5);

对普通成员函数插桩

被测代码:

class A {
public:
    int member(int a) {return ++a;}
    static int static_member(int a) {return 200;}
    virtual int virtual_member() {return 400;}
};

桩函数:

int fake_member(A *pTihs, int a) {
  //由于是对成员函数插桩,这里需要这个this指针参数
    return --a;
}

插桩示例:

A a;
assert(a.member(100) == 101);

mock(&A::member, fake_member);
assert(a.member(100) == 99);

reset();

assert(a.member(100) == 101);

对静态成员函数插桩

桩函数:

int fake_static_member() {
  //静态函数不需要this指针
    return 300;
}

插桩示例:

assert(A::static_member(200) == 200);

mock(&A::static_member, fake_static_member);
assert(A::static_member(100) == 300);

reset();

assert(A::static_member(200) == 200);

对虚函数插桩

桩函数:

int fake_virtual_member(A *pThis) {
    //虚函数同普通的成员函数由于,同样需要this指针
    return 500;
}

插桩示例:

A a;
assert(a.virtual_member() == 400);

//虚函数mock需要多传一个相关类的对象,任意一个对象即可,跟实际代码中的对象没有关系
A a_obj;
mock(&A::virtual_member, fake_virtual_member, &a_obj);
assert(a.virtual_member() == 500);

reset();
assert(a.virtual_member() == 400);

对系统及第三方库函数插桩

桩函数:

int fake_write(int, char*, int) {
    return 100;
}

插桩示例:

//直接写入一个无效的文件描述符,会失败
assert(write(5, "hello", 5) == -1);

//来一个假的wirte
mock(write, fake_write);
//模拟调用成功
assert(write(5, "hello", 5) == 100);

reset();

assert(write(5, "hello", 5) == -1);

可以看到,对系统函数的 mock,其实跟普通的全局函数并无两样,第三方库函数也是同理。

使用限制&注意事项

  • 目前支持 X86_64 平台上的 Linux、MacOS 系统,如有需求,Windows 和其它硬件平台,如 X86_32、ARM,也可在短期内支持。
  • MacOS 下,需要在执行前对单测可执行文件做以下修改:
printf '\x07' | dd of=<ut_executable> bs=1 seek=160 count=1 conv=notrunc
  • 显然,这种方法对内联函数无效,不过对于单元测试来说,可以关闭内联,同时也建议关闭其它编译器优化。
  • 可以使用-fno-access-control 编译你的测试代码,可以使 g++关闭 c++成员的访问控制(即 protected 及 private 不再生效)。

项目地址

https://github.com/wangyongfeng5/lmock

结语

持续改进是研效工具平台发展的必经之路,欢迎感兴趣的同学与我们交流探讨,共同助力测试效能的优化。

本文分享自微信公众号 - 腾讯技术工程(Tencent_TEG),作者:mannywang

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-06-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 研效优化实践:聊聊单元测试那些事儿

    ? 作者:ciuwaalu,腾讯安全平台部后台开发 研发效能提升是一个系统化的庞大工程,它涵盖了软件交付的整个生命周期,涉及到产品、架构、开发、测试、运维等...

    腾讯技术工程官方号
  • 浅谈代码覆盖率

    经常有人问这样的问题:“我们在做单元测试,那测试覆盖率要到多少才行?”。答案其实很简答,“作为指标的测试覆盖率都是没有用处的。”

    JavaEdge
  • 有赞iOS精准测试实践

    近几年有赞零售业务快速发展,为了满足日益增多的业务需求,2019年起零售客户端发版改成了每周一次,在质量保障方面,技术团队要面对更大的挑战。故此我们团队做了很多...

    有赞coder
  • C++ 动态新闻推送 第16期

    从reddit/hackernews/lobsters/meetingcpp摘抄一些c++动态。

    王很水
  • llvm 编译器高级用法:第三方库插桩

    最近看到一篇有意思的技术文章:《抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%》。

    酷酷的哀殿
  • 方案设计:基于IDEA插件开发和字节码插桩技术,实现研发交付质量自动分析

    业务提需求,产品定方案,研发做实现,测试验流程。四种角色的相互配合是确保一个需求上线的必备条件。在整个需求的交付质量级别划分中,研发与测试是非常重的一环,如果研...

    小傅哥
  • Android Native 内存泄漏系统化解决方案

    导读:C++内存泄漏问题的分析、定位一直是Android平台上困扰开发人员的难题。因为地图渲染、导航等核心功能对性能要求很高,高德地图APP中存在大量的C++代...

    砸漏
  • Redex 初探与 Interdex:Andorid 冷启动优化

    导语 早在去年10月份,facebook就发布了介绍redex的文章,这个据说可以直接对apk做处理,既提高启动性能,又可减少安装包的利器让安卓开发者们都心动不...

    腾讯Bugly
  • 方案设计:基于IDEA插件开发和字节码插桩技术,实现研发交付质量自动分析

    业务提需求,产品定方案,研发做实现,测试验流程。四种角色的相互配合是确保一个需求上线的必备条件。在整个需求的交付质量级别划分中,研发与测试是非常重的一环,如果研...

    小傅哥
  • 集成测试是什么?为什么要做集成测试

    集成测试,也叫组装测试或联合测试。在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试。

    测试小兵
  • 造轮子系列 —— 方法插桩

    今天推荐一个群友开源的插桩框架 —— Mamba ,想学习 Gradle Plugin 和 ASM 的朋友们可以关注一波。

    路遥TM
  • Sonar Scanner 之 C++扫码篇

    本文将解决上一篇中的一个问题 1)为什么C++项目扫出来缺陷、安全漏洞都是0?覆盖率也是0%?

    Antony
  • 初识 Fuzzing 工具 WinAFL

    本文前两节将简要讨论 fuzzing 的基本理念以及 WinAFL 中所用到的插桩框架 DynamoRIO ,而后我们从源码和工具使用角度带你了解这个适用于 W...

    Seebug漏洞平台
  • Sonar Scanner系列之架构与Java篇

    本文系列将介绍Sonar在实际工程项目中落地的场景,例如: 1)多语言项目的扫描,如JAVA/JS/C++/C#/PLSQL 2)多分支扫描 3)覆盖率如何统计...

    Antony
  • Sonar Scanner系列之架构与Java篇

    本文系列将介绍Sonar在实际工程项目中落地的场景,例如: 1)多语言项目的扫描,如JAVA/JS/C++/C#/PLSQL 2)多分支扫描 3)覆盖率如何统计...

    Criss@陈磊
  • 运用AOP思想更优雅地进行性能调优

    在软件测试中,如果想在一个耗时严重的操作中找出其耗时的瓶颈时,一般采用的方法是在每个被调用的函数中写进测试代码,在运行时打出日志。如果该操作涉及到的业务逻辑特别...

    腾讯移动品质中心TMQ
  • 软件测试基础---流程和用例设计方法

      测试流程:需求分析-->编写测试计划-->测试设计-->测试执行-->测试结果输出

    小老鼠
  • JAVA代码覆盖率工具JaCoCo-实践篇

    上周 JAVA代码覆盖率工具JaCoCo-原理篇 简单介绍了JaCoCo其生成覆盖率的基本原理,这周的实践篇的主要内容就是将原理应用到实践中,本篇内容全部都是具...

    腾讯移动品质中心TMQ
  • 关于代码覆盖率(Code Coverage)

    最近做了一些关于代码覆盖率工具的调查,对一些主流的代码覆盖率的工具比如 Gcov,JaCoCo,Istanbul 等都做了一些实践和持续集成的工作,也有了一定的...

    Peter Shen

扫码关注云+社区

领取腾讯云代金券