为什么我需要这个?
对一些传统C模块进行单元测试.我知道有很多工具可用,但如果运行时嘲笑是可能的,我可以改变我的UT方法(添加可重用的组件)使我的生活更轻松:).
平台/环境?
Linux,ARM,gcc.
接近我正在尝试吗?
>我知道GDB使用陷阱/非法指令来添加断点(gdb internals).
>使代码自我修改.
>使用非法指令替换dummy()代码段,并返回作为即时下一条指令.
>控制转移到陷阱处理程序.
>陷阱处理程序是从unix域套接字读取的可重用功能.
> mocked_dummy()函数的地址被传递(从map文件中读取).
> Mock功能执行.
这里有问题.我也发现这个方法是繁琐的,需要很多的编码,一些也是组装的.
我也发现,在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; }
解决方法
#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__); 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适用于香草linux内核.
我们用于jmp到mock函数的方法是实际放下自己的操作码,这可能是可移植性最大的问题,因为我使用的操作码只能在一个小的endian x86(大多数桌面)上工作.因此,您需要针对您计划运行的每个arch(可能会在CPP宏中处理半容易)进行更新.
该函数本身必须至少为5个字节.通常情况是这样,因为每个函数的序言和结尾通常都至少有5个字节.
潜在改进…
set_mock_run()调用可以很容易地设置为接受参数以供重用.另外,您可以从原始函数保存五个覆盖的字节,以便稍后在代码中恢复.
我无法测试,但是我已经在ARM看过了,你会做类似的,但是你可以用分支操作码跳转到一个地址(而不是一个偏移量)…对于一个无条件的分支,你可以第一个字节为0xEA,接下来的3个字节为地址.
Chenz