C语言中的运行时模拟?

10
这个问题在我的清单中已经悬而未决很长时间了。简单来说 - 我需要在运行时dummy()的位置上运行mocked_dummy(),而不修改factorial()。我不关心软件的入口点。我可以添加任意数量的额外函数(但不能修改/*---- do not modify ----*/内的代码)。
为什么我需要这个?
为了对一些传统的C模块进行单元测试。我知道周围有很多可用的工具,但如果可以进行运行时 mocking,我可以改变我的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;
}
4个回答

5

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月。

然而,由于存根是使用汇编实现的(已实现),因此可能需要一些努力才能使其支持ARM。


使用这种方法,您仍然使用纯C编写测试,但是框架使用汇编语言。只要您在支持的平台上,实际上您不需要理解(甚至看到)汇编语言。但是,是的,它可能并不完全符合“纯C”的要求 :) - Timothy Jones

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;
}

3

这是我一直在努力回答的问题。我还有一个要求,即我希望模拟方法/工具使用与我的应用程序相同的语言完成。不幸的是,这在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 kernel。

我们用来jmp到模拟函数的方法实际上是放置我们自己的操作码,这可能是可移植性最大的问题,因为我使用的操作码仅适用于小端x86(大多数台式机)。因此,这需要针对每个您计划在其上运行的架构进行更新(可以在CPP宏中处理)。

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

潜在改进...

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

我无法测试,但我已阅读,在ARM中...您会执行类似的操作,但可以使用分支操作码跳转到地址(而非偏移量)...对于无条件分支,第一个字节将是0xEA,下一个3个字节是地址。

Chenz


我的疑问更多地集中在这个语句上:#define dummy (m.dummy),你如何防止预处理器不替换dummy函数? - Pavan Manjunath
你说得很对...我得好好考虑一下。我猜这增加了一个额外的要求,即不能模拟任何在模拟开销之后的代码...或者只需将所有需要模拟的函数放在不同的源文件中。唉。Test-dept看起来是正确的选择,只是希望它不要那么*nix为基础。 - Crazy Chenz
好的...现在我认为我有一个更合适的答案。它不是自动工具,但它允许在运行时将虚拟变量更改为模拟虚拟变量。并且它可以构建 :) (..在GCC 4.4.5中) - Crazy Chenz
如果虚拟对象 dummy 和模拟虚拟对象 mocked_dummy 之间的偏移量超过 4096,会发生什么? - Kamath

2

您可以通过使用LD_PRELOAD来替换每个函数。您需要创建一个共享库,该库由LD_PRELOAD加载。这是一种标准功能,用于将不支持SOCKS的程序转换为SOCKS感知程序这里有一个教程来解释它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接