如何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的动态库。

附录

目录
相关文章
|
6月前
引入系统调用
引入系统调用
42 0
|
2月前
|
存储 缓存 NoSQL
webFilter实现mock接口
这段代码实现了一个名为 `MockFilter` 的类,继承自 `WebFilter` 接口,用于处理 HTTP 请求和响应。它通过从 Redis 缓存中获取配置信息来决定是否使用模拟数据或缓存数据来响应请求。如果开启了生产模式或关闭了模拟和缓存功能,则直接放行请求。否则,它会检查请求体并根据配置返回相应的模拟或缓存数据。同时,该过滤器支持对响应结果进行处理,并将结果存储回 Redis 中。
|
12月前
|
API 开发工具
【Pintos】实现自定义 UserProg 系统调用 | 添加 syscall-nr 系统调用号 | 编写新的参数调用宏
【Pintos】实现自定义 UserProg 系统调用 | 添加 syscall-nr 系统调用号 | 编写新的参数调用宏
111 0
|
6月前
011.socket函数错误封装处理
·回顾 在 008.一个简单的网络服务器开发----回声服务器中所实现的服务器功能简单,简单到几乎没有什么错误处理,我们知道,系统调用不能保证每次都成功。如 010所示,如果我们因为自己代码写的有些问题那么光是排查错误就够我们受的了。出错的可能千千万,所以我们必须进行出错处理,这样一方面可以保证我们的程序逻辑正常,另一方面可以迅速定位到故障信息。
65 0
|
6月前
|
安全 Java 程序员
“系统调用”究竟是不是个函数?
- **系统调用**和普通**函数**有何区别? - 什么是**内核态** 和 **用户态**? - 操作系统如何让CPU切换状态? - 内中断、外中断、软中断、硬中断是什么意思? - 库函数和系统调
|
JSON 数据格式 Python
09 Tornado - 接口与调用顺序
09 Tornado - 接口与调用顺序
49 0
|
小程序 前端开发 API
小程序api封装 promise使用
小程序api封装 promise使用
63 0
|
安全
驱动开发:取进程模块的函数地址
在笔者上一篇文章`《驱动开发:内核取应用层模块基地址》`中简单为大家介绍了如何通过遍历`PLIST_ENTRY32`链表的方式获取到`32位`应用程序中特定模块的基地址,由于是入门系列所以并没有封装实现太过于通用的获取函数,本章将继续延申这个话题,并依次实现通用版`GetUserModuleBaseAddress()`取远程进程中指定模块的基址和`GetModuleExportAddress()`取远程进程中特定模块中的函数地址,此类功能也是各类安全工具中常用的代码片段。
229 0
|
Linux
linux驱动中如何向模块传递参数方法
大家好,今天主要和大家分享一下,如何向模块传递参数? Linux kernel 提供了一个简单的框架。利用module_param和module_param_arra来实现。
183 0
linux驱动中如何向模块传递参数方法
(转)CreateProcess API函数的妙用
从以上的3种应用可以看出,只要知道系统自带的应用程序名,就可以通过设定szAppName和szCmdLine相应的值,来调用相应的程序,来达到自己想要的目的。只要认真挖掘,一定可以发现更多的应用。