C 中的运行时模拟? [英] Run-time mocking in C?

查看:23
本文介绍了C 中的运行时模拟?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

This has been pending for a long time in my list now. In brief - I need to run mocked_dummy() in the place of dummy() ON RUN-TIME, without modifying factorial(). I do not care on the entry point of the software. I can add up any number of additional functions (but cannot modify code within /*---- do not modify ----*/).

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

Why do I need this?
To do unit tests of some legacy C modules. I know there are a lot of tools available around, but if run-time mocking is possible I can change my UT approach (add reusable components) make my life easier :).

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

Platform / Environment?
Linux, ARM, gcc.

我正在尝试的方法?

  • 我知道 GDB 使用陷阱/非法指令来添加断点(gdb internals).
  • 使代码可自行修改.
  • 用非法指令替换dummy()代码段,并返回作为下一条指令.
  • 控制转移到陷阱处理程序.
  • 陷阱处理程序是一个可重用的函数,它从 unix 域套接字中读取数据.
  • mocked_dummy() 函数的地址被传递(从地图文件中读取).
  • 模拟函数执行.
  • I know GDB uses trap/illegal instructions for adding up breakpoints (gdb internals).
  • Make the code self modifiable.
  • Replace dummy() code segment with illegal instruction, and return as immediate next instruction.
  • Control transfers to trap handler.
  • Trap handler is a reusable function that reads from a unix domain socket.
  • Address of mocked_dummy() function is passed (read from map file).
  • Mock function executes.

这里有问题.我还发现这种方法很乏味,需要大量的编码,有些还需要汇编.

There are problems going ahead from here. I also found the approach is tedious and requires good amount of coding, some in assembly too.

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

I also found, under gcc each function call can be hooked / instrumented, but again not very useful since the the function is intended to be mocked will anyway get executed.

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

Is there any other approach that I could use?

#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 中完成,所以我求助于你可能称之为蹦床或绕道的东西.这属于使代码可自我修改".你上面提到的方法.这是我们在运行时更改函数的实际字节以跳转到我们的模拟函数.

This is a question I've been trying to answer myself. I also have the requirement that I want the mocking method/tools to be done in the same language as my application. Unfortunately this cannot be done in C in a portable way, so I've resorted to what you might call a trampoline or detour. This falls under the "Make the code self modifiable." approach you mentioned above. This is were we change the actually bytes of a function at runtime to jump to our mock function.

#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() - This changes the memory page access permissions so that we can actually write to memory that holds the function code. This isn't very portable, and in a WINAPI env, you may need to use VirtualProtect() instead.

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

The memory parameter for mprotect is aligned to the previous 4k page, this also can change from system to system, 4k is appropriate for vanilla linux kernel.

我们用来 jmp 到模拟函数的方法是实际放下我们自己的操作码,这可能是可移植性的最大问题,因为我使用的操作码仅适用于小端 x86(大多数桌面).因此,这需要针对您计划在其上运行的每个拱门进行更新(这在 CPP 宏中可能比较容易处理.)

The method that we use to jmp to the mock function is to actually put down our own opcodes, this is probably the biggest issue with portability because the opcode I've used will only work on a little endian x86 (most desktops). So this would need to be updated for each arch you plan to run on (which could be semi-easy to deal with in CPP macros.)

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

The function itself has to be at least five bytes. The is usually the case because every function normally has at least 5 bytes in its prologue and epilogue.

潜在的改进...

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

The set_mock_run() call could easily be setup to accept parameters for reuse. Also, you could save the five overwritten bytes from the original function to restore later in the code if you desire.

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

I'm unable to test, but I've read that in ARM... you'd do similar but you can jump to an address (not an offset) with the branch opcode... which for an unconditional branch you'd have the first bytes be 0xEA and the next 3 bytes are the address.

陈诚

这篇关于C 中的运行时模拟?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆