现在已经在我的列表中待了很长时间。简而言之 - 我需要在mocked_dummy()
ON RUN-TIME 的位置运行dummy()
,而无需修改factorial()
。我不关心软件的切入点。我可以添加任意数量的附加功能(但不能修改/*---- do not modify ----*/
内的代码)。
为什么我需要这个?
对一些传统C模块进行单元测试。我知道有很多可用的工具,但是如果可以运行时模拟我可以改变我的UT方法(添加可重用的组件)让我的生活更轻松:)。
平台/环境?
Linux,ARM,gcc。
我正在尝试的方法?
dummy()
代码段替换为非法指令,返回作为下一条指令。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;
}
答案 0 :(得分: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月更新。
但是,由于存根是achieved using assembler,因此可能需要付出一些努力来支持ARM。
答案 1 :(得分: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;
}
答案 2 :(得分:2)
您可以使用LD_PRELOAD替换每个函数。您必须创建一个由LD_PRELOAD加载的共享库。这是一个标准函数,用于将不支持SOCKS的程序转换为SOCKS aware programs。 Here是一个解释它的教程。
答案 3 :(得分: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个字节是地址。
Chenz