开发者社区> jeff216> 正文

如何Mock系统调用

简介: ### 背景 ​   Linux下开发存储系统、网络库的时候会用到一系列Linux的系统调用,每一个系统调用都有一些出错的场景,有些场景很极端,比如内存使用达到上限、磁盘写满等,如果对其进行测试的话,很难去构造这样的一个场景,这个时候集成测试就显得力不存心了,只能靠单元测试来覆盖这些场景。现在的问题就是如何去mock这些系统调用,然后通过程序返回对应场景的错误码来模拟各种
+关注继续查看

背景

​   Linux下开发存储系统、网络库的时候会用到一系列Linux的系统调用,每一个系统调用都有一些出错的场景,有些场景很极端,比如内存使用达到上限、磁盘写满等,如果对其进行测试的话,很难去构造这样的一个场景,这个时候集成测试就显得力不存心了,只能靠单元测试来覆盖这些场景。现在的问题就是如何去mock这些系统调用,然后通过程序返回对应场景的错误码来模拟各种场景。也就是将对系统函数的依赖注入到程序中。

系统函数的依赖注入

​   目前实现系统函数的依赖注入的手段有很多,分为编译期注入,和运行期注入,至于什么是依赖注入可以参考知乎的一篇文章如何用最简单的方式解释依赖注入,下面介绍几种依赖注入的方法:

  • 虚函数实现依赖注入(运行期注入)

​   使用传统的面向对象的手法,借助运行期的延迟绑定实现注入和替换,自己实现一个System接口类,把程序用到的系统调用都用虚函数封装一层,然后在调用的时候不直接调用系统调用,而是调用的System对应的方法。这样代码的主动权就交给了System接口类了。写单元测试的时候将这个System接口类替换成我们自己的mock对象就可以。完整的示例代码如下:

  // system.h
  class System {
   public:
    virtual int  open(const char *path, int oflag, ...) = 0;
    virtual ssize_t read(int fildes, void *buf, size_t nbyte) = 0;
    virtual ssize_t write(int fildes, const void *buf, size_t nbyte) = 0;
    virtual int close(int fildes) = 0;

    static System* GetInstance();
    static void set_instance(System* instance) {
      instance_ = instance;
    }

   private:
    static System* instance_;
  };
  // 具体的实现
  class FileOps : public System {
   public:
    int open(const char *path, int oflag, ...) override;
    ssize_t read(int fildes, void *buf, size_t nbyte) override;
    ssize_t write(int fildes, const void *buf, size_t nbyte) override;
    int close(int fildes) override;
    static FileOps* GetInstance();
  };

  // system.cc
  System* System::instance_ = nullptr;
  // 默认实现是FileOps,mock的时候通过改变这个默认实现从而把主动权从默认实现转到了mock的实现
  System* System::GetInstance() { 
    if (!instance_) {
      instance_ = FileOps::GetInstance();
    }
    assert(instance_);
    return instance_;
  }

  int FileOps::open(const char *path, int oflag, ...) {
    return ::open(path, oflag, 0777);
  }

  ssize_t FileOps::read(int fildes, void *buf, size_t nbyte) {
    return ::read(fildes, buf, nbyte);
  }

  ssize_t FileOps::write(int fildes, const void *buf, size_t nbyte) {
    return ::write(fildes, buf, nbyte);
  }

  int FileOps::close(int fildes) {
    return ::close(fildes);
  }

  FileOps* FileOps::GetInstance() {
    static FileOps sys;
    return &sys;
  }
  // 正常调用 main.cc
  int main() {
    assert(System::GetInstance() != nullptr);
    int fd = System::GetInstance()->open("txt", O_RDWR|O_CREAT, 0777);
    assert(fd > 0);
    int ret = System::GetInstance()->write(fd, "12345", 5);
    assert(ret > 0);
    ret = System::GetInstance()->close(fd);
    assert(ret == 0);

    return 0;
  }

  // 测试的时候调用如下,模拟一个IO错误

  // 一个mock版本的实现 test.cc
  class MockFileOps : public System {
  public:
   int open(const char *path, int oflag, ...) override;
   ssize_t read(int fildes, void *buf, size_t nbyte) override;
   ssize_t write(int fildes, const void *buf, size_t nbyte) override;
   int close(int fildes) override;
   static MockFileOps* GetInstance();
  };

  int MockFileOps::open(const char *path, int oflag, ...) {
    return ::open(path, oflag, 0777);
  }

  ssize_t MockFileOps::read(int fildes, void *buf, size_t nbyte) {
    return ::read(fildes, buf, nbyte);
  }
  // 模拟的一个IO错误
  ssize_t MockFileOps::write(int fildes, const void *buf, size_t nbyte) {
    errno = EIO;
    return -1;
  }

  int MockFileOps::close(int fildes) {
    return ::close(fildes);
  }

  MockFileOps* MockFileOps::GetInstance() {
    static MockFileOps sys;
    return &sys;
  }

  int main() {
    // 改变默认实现
    System::set_instance(MockFileOps::GetInstance());
    assert(System::GetInstance() != nullptr);
    int fd = System::GetInstance()->open("txt", O_RDWR|O_CREAT, 0777);
    assert(fd > 0);
    int ret = System::GetInstance()->write(fd, "12345", 5);
    assert(ret ==  -1);    // 发生错误
    perror("write");
    ret = System::GetInstance()->close(fd);
    assert(ret == 0);

    return 0;
  }
  • 编译期延迟绑定(编译期注入)

​   创建一个命名空间,创建一系列和系统调用同名的方法,间接的调用系统调用,写测试代码的时候重新定义这些方法,这就相当于一份代码有了两份实现,根据编译的时候链接哪份代码来决定是否启用mock,这个看起来要比基于虚函数的要简单的多了。完整的示例代码如下:

  // file_ops.h
  namespace FileOps {
    int  open(const char *path, int oflag, ...);
    ssize_t read(int fildes, void *buf, size_t nbyte);
    ssize_t write(int fildes, const void *buf, size_t nbyte);
    int close(int fildes);
  }  // namespace FileOps

  // file_ops.cc
  namespace FileOps {

  int open(const char *path, int oflag, ...) {
    return ::open(path, oflag, 0777);
  }

  ssize_t read(int fildes, void *buf, size_t nbyte) {
    return ::read(fildes, buf, nbyte);
  }

  ssize_t write(int fildes, const void *buf, size_t nbyte) {
    return ::write(fildes, buf, nbyte);
  }

  int close(int fildes) {
    return ::close(fildes);
  }

  }  // namespace FileOps

  // mock_file_ops.cc

  namespace FileOps {

  int open(const char *path, int oflag, ...) {
    return ::open(path, oflag, 0777);
  }

  ssize_t read(int fildes, void *buf, size_t nbyte) {
    return ::read(fildes, buf, nbyte);
  }
  // 这里做了mock,改变了write的行为
  ssize_t write(int fildes, const void *buf, size_t nbyte) {
    errno = EIO;
    return -1;
  }

  int close(int fildes) {
    return ::close(fildes);
  }

  }  // namespace FileOps

  // 测试程序
  int main() {
    int fd = FileOps::open("txt", O_RDWR|O_CREAT, 0777);
    assert(fd > 0);
    int ret = FileOps::write(fd, "12345", 5);
    if (ret == -1) {
      perror("write:");
    }
    ret = FileOps::close(fd);
    assert(ret == 0);
    return 0;
  }

​   ​两种方法都比较好实现,前提是代码在一开始的时候就考虑过这些因素,并按照上述方式来编写,然后现实总是残酷的,面对一个已经编码完成的程序该如何为其编写系统调用的mock呢?就需要用到链接期垫片(link seam)的方法。

链接期垫片(link seam)

​   连接器垫片的方式一般情况有三种,如下:

  • Shadowing functions through linking order (override functions in libraries with new definitions in object files)
  • Wrapping functions with GNU's linker option -wrap (GNU Linux only)
  • Run-time function interception* with the preload functionality of the dynamic linker for shared libraries (GNU Linux and Mac OS X only)

​   第一种就是通过链接顺序来改变链接的对象,将要mock的对象重新实现一遍,链接的时候链接器会优先使用我们自己实现的同名函数,这样就可以将目标替换为要mock的对象了,完整代码如下:

//  一个待测试的对象
int main() {
  int fd = ::open("txt", O_RDWR|O_CREAT, 0777);
  assert(fd > 0);
  int ret = ::write(fd, "12345", 5);
  if (ret == -1) {
    perror("write:");
  }
  ret = ::close(fd);
  assert(ret == 0);
  return 0;
}

// 对目标进行mock,mock的对象是write系统调用
typedef ssize_t (*write_func_t)(int fildes, const void *buf, size_t nbyte);
// 通过dlsym的RTLD_NEXT获取write的下一个定义,也就是libc中的定义,如果想在mock中
// 调用真实的write系统调用不能直接用write,因为write已经被mock了,这样会导致一直递归下去
// 所以这里通过获取真实的write调用的地址,从而难道write的调用入口,这样既可以在mock中调用
// 真实的write调用了
write_func_t old_write_func =
    reinterpret_cast<write_func_t>(dlsym(RTLD_NEXT, "write"));

// 要mock的对象
extern "C" ssize_t write(int fildes, const void *buf, size_t nbyte) {
  errno = EIO;
  return -1;
}

​   另外一种就是Linux下独有的,通过gcc的--wrap选项可以指定要wrap的系统调用,那么相应的就回去调用带有__wrap前缀的对应系统调用实现,比如--wrap=write,那么在链接的时候就会链接到 __wrap_write,而真实的write调用变成了__real_write。完整代码例子如下:

// 测试程序
int main() {
  int fd = ::open("txt", O_RDWR|O_CREAT, 0777);
  assert(fd > 0);
  int ret = ::write(fd, "12345", 5);
  if (ret == -1) {
    perror("write:");
  }
  ret = ::close(fd);
  assert(ret == 0);
  return 0;
}

// mock对象
extern "C" ssize_t __real_write(int fildes, const void *buf, size_t nbyte);

extern "C" ssize_t __wrap_write(int fildes, const void *buf, size_t nbyte) {
  __real_write(fildes, buf, nbyte);
  errno = EIO;
  return -1;
}

​   最后一种就是给系统调用提供一份mock实现,并编译成动态库,然后通过LD_LIBRARY_PATH改变加载动态库的搜索路径让其优先搜索mock版本的动态库,或者是设置LD_PRELOAD环境变量,预先加载mock的动态库。

附录

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
字符设备驱动代码的编写
传统方法,优点简单缺点就是不易宽展,硬件更换了板子都要重新写代码重新比编译,硬件的每次改动都要重新修改软件版本增加工作量。
22 0
linux驱动中如何向模块传递参数方法
大家好,今天主要和大家分享一下,如何向模块传递参数? Linux kernel 提供了一个简单的框架。利用module_param和module_param_arra来实现。
60 0
9、IAR中断向量表与中断服务函数的编写
9、IAR中断向量表与中断服务函数的编写
74 0
驱动开发:挂接SSDT内核钩子
SSDT 中文名称为系统服务描述符表,该表的作用是将Ring3应用层与Ring0内核层,两者的API函数连接起来,起到承上启下的作用,SSDT并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基址、服务函数个数等,SSDT 通过修改此表的函数地址可以对常用 Windows 函数进行内核级的Hook,从而实现对一些核心的系统动作进行过滤、监控的目的,接下来将演示如何通过编写简单的驱动程序,来实现搜索 SSDT 函数的地址,并能够实现简单的内核 Hook 挂钩。
157 0
驱动开发:内核监控FileObject文件回调
本篇文章与上一篇文章`《驱动开发:内核注册并监控对象回调》`所使用的方式是一样的都是使用`ObRegisterCallbacks`注册回调事件,只不过上一篇博文中`LyShark`将回调结构体`OB_OPERATION_REGISTRATION`中的`ObjectType`填充为了`PsProcessType`和`PsThreadType`格式从而实现监控进程与线程,本章我们需要将该结构填充为`IoFileObjectType`以此来实现对文件的监控,文件过滤驱动不仅仅可以用来监控文件的打开,还可以用它实现对文件的保护,一旦驱动加载则文件是不可被删除和改动的。
90 0
驱动开发:封装x64内核驱动读写
内核级别的内存读写可用于绕过各类驱动保护,从而达到强制读写对端内存的目的,本人闲暇之余封装了一个驱动级的内核读写接口,使用此接口可实现对远程字节,字节集,整数,浮点数,多级偏移读写等。
171 0
驱动开发:内核枚举进程与线程ObCall回调
在笔者上一篇文章`《驱动开发:内核枚举Registry注册表回调》`中我们通过特征码定位实现了对注册表回调的枚举,本篇文章`LyShark`将教大家如何枚举系统中的`ProcessObCall`进程回调以及`ThreadObCall`线程回调,之所以放在一起来讲解是因为这两中回调在枚举是都需要使用通用结构体`_OB_CALLBACK`以及`_OBJECT_TYPE`所以放在一起来讲解最好不过。
101 0
驱动开发:恢复SSDT内核钩子
SSDT 中文名称为系统服务描述符表,该表的作用是将Ring3应用层与Ring0内核层,两者的API函数连接起来,起到承上启下的作用,SSDT并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基址、服务函数个数等,SSDT 通过修改此表的函数地址可以对常用 Windows 函数进行内核级的Hook,从而实现对一些核心的系统动作进行过滤、监控的目的。
179 0
小程序调用接口不生效?
小程序调用接口不生效?
205 0
+关注
jeff216
专注与Linux C++、Linux内核、高性能网络编程、DevOps、Docker等
文章
问答
视频
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载