在C中运行时模拟?

Kam*_*ath 9 c gdb arm mocking

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

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

平台/环境?
Linux,ARM,gcc.

我正在尝试的方法?

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

从这里开始存在问题.我还发现这种方法很繁琐,需要大量的编码,有些也在组装中.

我还发现,在gcc下,每个函数调用都可以被挂钩/检测,但同样不是很有用,因为该函数是用来模拟的,无论如何都会被执行.

我还能使用其他方法吗?

#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;
}
Run Code Online (Sandbox Code Playgroud)

Tim*_*nes 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);
}
Run Code Online (Sandbox Code Playgroud)

虽然下载部分有点过时,但似乎相当积极地开发 - 作者修复了我非常及时的问题.你可以通过以下方式获得最新版本(我一直在使用,没有问题):

svn checkout http://test-dept.googlecode.com/svn/trunk/ test-dept-read-only
Run Code Online (Sandbox Code Playgroud)

该版本最后更新于2011年10月.

但是,由于使用汇编程序实现了存根,因此可能需要一些努力才能使其支持ARM.


Cra*_*enz 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;
}
Run Code Online (Sandbox Code Playgroud)

可移植性说明...

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

mprotect 的内存参数与之前的 4k 页对齐,这也可能因系统而异,4k 适用于 vanilla linux 内核。

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

函数本身必须至少有五个字节。通常情况如此,因为每个函数的序言和尾声通常至少有 5 个字节。

潜在的改进...

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

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

陈兹