【问题标题】:Run-time mocking in C?C中的运行时模拟?
【发布时间】:2012-03-25 16:41:11
【问题描述】:

这已经在我的列表中等待了很长时间。简而言之 - 我需要运行 mocked_dummy() 代替 dummy() ON RUN-TIME,而不修改 factorial()。我不在乎软件的入口点。我可以添加任意数量的附加功能(但不能修改/*---- do not modify ----*/ 中的代码)。

我为什么需要这个?
对一些遗留 C 模块进行单元测试。我知道有很多可用的工具,但如果可以进行运行时模拟,我可以改变我的 UT 方法(添加可重用组件)让我的生活更轻松:)。

平台/环境?
Linux、ARM、gcc。

我正在尝试的方法?

  • 我知道 GDB 使用陷阱/非法指令来添加断点 (gdb internals)。
  • 使代码可自行修改。
  • dummy()代码段替换为非法指令,并返回立即作为下一条指令。
  • 控制转移到陷阱处理程序。
  • 陷阱处理程序是一个可重用的函数,它从 unix 域套接字读取。
  • mocked_dummy() 函数的地址已传递(从映射文件中读取)。
  • 模拟函数执行。

从这里开始会有问题。我还发现这种方法很乏味,并且需要大量的编码,有些还需要汇编。

我还发现,在 gcc 下,每个函数调用都可以是 hooked / instrumented,但又不是很有用,因为要模拟的函数无论如何都会被执行。

我还有其他方法可以使用吗?

#include <stdio.h>
#include <stdlib.h>

void mocked_dummy(void)
{
    printf("__%s__()\n",__func__);
}

/*---- do not modify ----*/
void dummy(void)
{
    printf("__%s__()\n",__func__);
}

int factorial(int num) 
{
    int                      fact = 1;
    printf("__%s__()\n",__func__);
    while (num > 1)
    {
        fact *= num;
        num--;
    }
    dummy();
    return fact;
}
/*---- do not modify ----*/

int main(int argc, char * argv[])
{
    int (*fp)(int) = atoi(argv[1]);
    printf("fp = %x\n",fp);
    printf("factorial of 5 is = %d\n",fp(5));
    printf("factorial of 5 is = %d\n",factorial(5));
    return 1;
}

【问题讨论】:

    标签: c mocking gdb arm


    【解决方案1】:

    test-dept 是一个相对较新的 C 单元测试框架,它允许您对函数进行运行时存根。我发现它非常易于使用 - 这是他们文档中的一个示例:

    void test_stringify_cannot_malloc_returns_sane_result() {
      replace_function(&malloc, &always_failing_malloc);
      char *h = stringify('h');
      assert_string_equals("cannot_stringify", h);
    }
    

    虽然下载部分有点过时,但它似乎相当活跃 - 作者很快解决了我遇到的问题。您可以通过以下方式获得最新版本(我一直在使用没有问题):

    svn checkout http://test-dept.googlecode.com/svn/trunk/ test-dept-read-only
    

    上次更新的版本是 2011 年 10 月。

    但是,由于存根是 achieved using assembler,因此可能需要一些努力才能使其支持 ARM。

    【讨论】:

    • 使用这种方法,您仍然使用纯 C 编写测试,但框架使用汇编。只要您在受支持的平台上,您实际上就不需要了解(甚至查看)程序集。但是,是的,它可能不适合端到端的“纯 C”:)
    【解决方案2】:

    这是我一直试图回答自己的问题。我还要求我希望使用与我的应用程序相同的语言来完成模拟方法/工具。不幸的是,这不能在 C 中以可移植的方式完成,所以我采用了你可能称之为蹦床或绕道的方法。这属于“使代码可自行修改”。接近你上面提到的。这是我们在运行时更改函数的实际字节以跳转到我们的模拟函数。

    #include <stdio.h>
    #include <stdlib.h>
    
    // Additional headers
    #include <stdint.h> // for uint32_t
    #include <sys/mman.h> // for mprotect
    #include <errno.h> // for errno
    
    void mocked_dummy(void)
    {
        printf("__%s__()\n",__func__);
    }
    
    /*---- do not modify ----*/
    void dummy(void)
    {
        printf("__%s__()\n",__func__);
    }
    
    int factorial(int num) 
    {
        int                      fact = 1;
        printf("__%s__()\n",__func__);
        while (num > 1)
        {
            fact *= num;
            num--;
        }
        dummy();
        return fact;
    }
    /*---- do not modify ----*/
    
    typedef void (*dummy_fun)(void);
    
    void set_run_mock()
    {
        dummy_fun run_ptr, mock_ptr;
        uint32_t off;
        unsigned char * ptr, * pg;
    
        run_ptr = dummy;
        mock_ptr = mocked_dummy;
    
        if (run_ptr > mock_ptr) {
            off = run_ptr - mock_ptr;
            off = -off - 5;
        }
        else {
            off = mock_ptr - run_ptr - 5;
        }
    
        ptr = (unsigned char *)run_ptr;
    
        pg = (unsigned char *)(ptr - ((size_t)ptr % 4096));
        if (mprotect(pg, 5, PROT_READ | PROT_WRITE | PROT_EXEC)) {
            perror("Couldn't mprotect");
            exit(errno);
        }
    
        ptr[0] = 0xE9; //x86 JMP rel32
        ptr[1] = off & 0x000000FF;
        ptr[2] = (off & 0x0000FF00) >> 8;
        ptr[3] = (off & 0x00FF0000) >> 16;
        ptr[4] = (off & 0xFF000000) >> 24;
    }
    
    int main(int argc, char * argv[])
    {
        // Run for realz
        factorial(5);
    
        // Set jmp
        set_run_mock();
    
        // Run the mock dummy
        factorial(5);
    
        return 0;
    }
    

    可移植性说明...

    mprotect() - 这会改变内存页面访问权限,以便我们可以真正写入保存函数代码的内存。这不是很便携,在 WINAPI 环境中,您可能需要改用 VirtualProtect()。

    mprotect 的内存参数与前一个 4k 页面对齐,这也可以在不同系统之间更改,4k 适合 vanilla linux 内核。

    我们用来 jmp 到 mock 函数的方法实际上是放下我们自己的操作码,这可能是可移植性的最大问题,因为我使用的操作码只能在小端 x86 上工作(大多数桌面) .所以这需要为您计划运行的每个架构进行更新(这在 CPP 宏中可能很容易处理。)

    函数本身必须至少有五个字节。通常是这种情况,因为每个函数通常在其序言和尾声中至少有 5 个字节。

    可能的改进...

    set_mock_run() 调用可以轻松设置为接受参数以供重用。此外,如果需要,您可以从原始函数中保存五个被覆盖的字节,以便稍后在代码中恢复。

    我无法测试,但我在 ARM 中读到过……你会做类似的事情,但你可以使用分支操作码跳转到一个地址(不是偏移量)……对于无条件分支你的第一个字节是 0xEA,接下来的 3 个字节是地址。

    陈斯

    【讨论】:

    • 我更怀疑这个说法#define dummy (m.dummy)你如何防止预处理器不替换虚拟函数?
    • 你说的太对了……我得考虑一下。我想它增加了一个额外的要求,即您不能在模拟开销之后模拟任何代码......或者只是将所有要模拟的函数保存在不同的源文件中。啊。测试部门看起来像要走的路,我只是希望它不是那么基于 *nix。
    • 好的......所以现在我想我有一个更合适的答案。它不是一种自动工具,但它确实允许在运行时将 dummy 更改为 mocked_dummy。它建立了:) (..in GCC 4.4.5)
    • 如果 dummy 和 mocked_dymmy 的偏移量超过 4096 会怎样?
    【解决方案3】:

    我过去使用的一种效果很好的方法如下。

    对于每个 C 模块,发布一个其他模块可以使用的“接口”。这些接口是包含函数指针的结构体。

    struct Module1 
    {
        int (*getTemperature)(void);
        int (*setKp)(int Kp);
    }
    

    在初始化过程中,每个模块都会用它的实现函数来初始化这些函数指针。

    当您编写模块测试时,您可以动态地将这些函数指针更改为它的模拟实现,并在测试后恢复原始实现。

    例子:

    void mocked_dummy(void)
    {
        printf("__%s__()\n",__func__);
    }
    /*---- do not modify ----*/
    void dummyFn(void)
    {
        printf("__%s__()\n",__func__);
    }
    static void (*dummy)(void) = dummyFn;
    int factorial(int num)
    {
        int                      fact = 1;
            printf("__%s__()\n",__func__);
        while (num > 1)
        {
            fact *= num;
            num--;
        }
        dummy();
        return fact;
    }
    
    /*---- do not modify ----*/
    int main(int argc, char * argv[])
    {
        void (*oldDummy) = dummy;
    
    /* with the original dummy function */
        printf("factorial of 5 is = %d\n",factorial(5));
    
    /* with the mocked dummy */
        oldDummy = dummy;   /* save the old dummy */
        dummy = mocked_dummy; /* put in the mocked dummy */
        printf("factorial of 5 is = %d\n",factorial(5));
        dummy = oldDummy; /* restore the old dummy */
        return 1;
    }
    

    【讨论】:

      【解决方案4】:

      您可以使用 LD_PRELOAD 替换每个函数。您必须创建一个由 LD_PRELOAD 加载的共享库。这是一个标准函数,用于将不支持 SOCKS 的程序转换为SOCKS aware programsHere 是一个解释它的教程。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-11-07
        • 1970-01-01
        • 2018-03-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多