一种C语言"打桩"的源码实现
程序员文章站
2022-05-25 12:22:00
实际工作中,在我们写好源代码后,通常需要对代码进行UT、FT测试,这个时候我们经常需要“打桩”,考虑以下情形:
1、本模块A的正常业务过程需要调用模块B的...
实际工作中,在我们写好源代码后,通常需要对代码进行UT、FT测试,这个时候我们经常需要“打桩”,考虑以下情形:
1、本模块A的正常业务过程需要调用模块B的函数b1,但函数b1有可能还未实现(或者系统还未集成模块A无法调用b1),这个时侯为了顺利的进行UT,我们就可以对函数b1进行打桩。
2、模块A正常业务过程会向模块C发送消息,而我们想查看消息的内容是否正确,这个时侯就可以对发送消息的函数打桩,改变其行为,打桩后测试过程中模块A不会向C发送消息,而会将消息码流打印到屏幕(或写到文件,这个要看桩函数的实现)。
3、模拟UT、FT测试过程中无法实现的场景,我们的代码肯定都是针对实际运行环境,如果我们代码中有关于数据库的操作、文件的操作,我们不会真的去操作数据库和写文件,而是使用桩将这个场景屏蔽或替代成其它过程,以满足我们的测试需要。
“废话”说了一大通,下面能一段简单的代码了解一下打桩和桩函数:
编译环境:window10 + VS2015
先来看一下”打桩”的效果:
// stub_test.c : 定义控制台应用程序的入口点。 // #include "stub.h" #include void add(int i) { printf("add(%d)\n",i); } void add_stub(int i) { printf("add_stub(%d)\n",i); } int main() { INSTALL_STUB(add,add_stub); add(12); REMOVE_STUB(add_stub); add(11); return 0; }
编译运行结果:
add_stub(12) add(11)
本文就是探究一下这个打桩(INSTALL_STUB)和移除桩(REMOVE_STUB)过程的源码实现,源码中的一些关键点都做了详细注释。
stub.h源码如下:
#pragma once #ifdef __cplusplus extern "C" { #endif int uninstall_stub(void* stub_f); int install_stub(void *orig_f, void *stub_f, char *desc); #define INSTALL_STUB(o,s) install_stub((void*)o,(void*)s,(char*)#o"->"#s) #define REMOVE_STUB(s) uninstall_stub((void*)s) #ifdef __cplusplus } #endif
stub_list.h源码如下:
#pragma once #define __inline__ typedef struct list_head { struct list_head *next, *prev; }list_head_t; #define LIST_HEAD_INIT(name) { &(name),&(name) } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr);(ptr)->prev = (ptr);\ } while (0) static __inline__ void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { next->prev = new; new->next = next; new->prev = prev; prev->next = new; } static __inline__ void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } static __inline__ void __list_del(struct list_head *prev, struct list_head *next) { next->prev = prev; prev->next = next; } static __inline__ void list_del(struct list_head *entry) { __list_del(entry->prev, entry->next); } #define list_entry(ptr,type,member) \ ((type*)((char*)(ptr)-(unsigned long)(&((type*)0)->member))) #define list_for_each(pos,head) \ for(pos=(head)->next;pos!=(head);pos=pos->next) static __inline__ int list_count(struct list_head *head) { struct list_head *pos; int count = 0; list_for_each(pos, head) { count++; } return count; }
stub.c源码如下:
#define _CRT_SECURE_NO_WARNINGS #include #include "stub_list.h" LIST_HEAD(head); struct stub { struct list_head node; char desc[256]; void *orig_f; void *stub_f; unsigned int stubpath; unsigned int old_flg; unsigned char assm[5]; }; HANDLE mutex; int init_lock_flag = 0; void initlock() { mutex = CreateSemaphore(NULL, 1, 1, NULL); } void lock() { (init_lock_flag == 0) && (initlock(), init_lock_flag = 1); while (WaitForSingleObject(mutex, INFINITE) != WAIT_OBJECT_0) ; } void unlock() { ReleaseSemaphore(mutex, 1, NULL); } #define _PAGESIZE 4096 static int set_mprotect(struct stub* pstub) { void *p; /**************************************************************************** *函数VirtualProtectExVirtualProtectEx用来设置内存区域的保护属性,可以将原始函数 *所在的内存设置为可读、可写、可执行,这样就可以改变原始函数所在内存的代码指令。 *由于 VirtualProtectExVirtualProtectEx只能设置从内存页大小(4096)的整数倍开始的 *地址,因此利用(long)pstub->orig_f& ~(_PAGESIZE - 1) 计算出一个比原始函数地址小 *且为内存页大小(4096)的整数倍的地址,把它当作VirtualProtectExVirtualProtectEx的 *作用的起始地址,内存大小为两个内存页_PAGESIZE << 1 *****************************************************************************/ p = (void*)((long)pstub->orig_f& ~(_PAGESIZE - 1)); return TRUE - VirtualProtectEx((HANDLE)-1, p, _PAGESIZE << 1, PAGE_EXECUTE_READWRITE,/* 设置内存属性为可读、可写、可执行 */ &pstub->old_flg);/* 将该内存的之前的属性保存下来,以便移除桩函数时,恢复原始函数 */ } static int set_asm_jmp(struct stub* pstub) { unsigned int offset; /* 保存从原始函数地址开始的5个字节,因为之后我们会改写这块区域 */ memcpy(pstub->assm, pstub->orig_f, sizeof(pstub->assm)); *((char*)pstub->orig_f) = 0xE9;/* 这个是相对跳转指令jmp */ /************************************************************** *计算出桩函数与原始函数之间的相对地址,注意要减去jmp指令的 *5个字节(0xE9加上一个4字节的相对地址),然后用这条jmp指令,改写 *原始函数地址开始的5个字节,这样调用原始函数,就会自动跳到桩函数 **************************************************************/ offset = (unsigned int)((long)pstub->stub_f - ((long)pstub->orig_f + 5)); *((unsigned int*)((char*)pstub->orig_f + 1)) = offset; return 0; } static void restore_asm(struct stub* pstub) { /* 恢复原始函数地址开始的5个字节 */ memcpy(pstub->orig_f, pstub->assm, sizeof(pstub->assm)); } int install_stub(void *orig_f, void *stub_f, char *desc) { struct stub *pstub; pstub = (struct stub*)malloc(sizeof(struct stub)); pstub->orig_f = orig_f; pstub->stub_f = stub_f; do { /* 设置该内存段属性 */ if (set_mprotect(pstub)) { break; } /* 用jmp指令去覆盖orig_f开始的5个字节 */ if (set_asm_jmp(pstub)) { break; } if (desc) { strncpy(pstub->desc, desc, sizeof(pstub->desc)); } lock(); /* 如果有多个线程同时操作链表,要使用锁进行同步 */ /* 如果对多个函数打桩,就需要保存多个函数的相关信息,这里使用链表储存 */ list_add(&pstub->node, &head); unlock(); /* 操作完成,释放锁 */ return 0; } while (0); free(pstub); return -1; } int uninstall_stub(void* stub_f) { struct stub *pstub; struct list_head *pos; /* 移除桩函数就是将原始函数地址开始的5个字节恢复,然后将该 函数的信息从链表中移除同时释放之前动态申请的内存 */ list_for_each(pos, &head) { pstub = list_entry(pos, struct stub, node); if (pstub->stub_f == stub_f) { restore_asm(pstub); lock(); list_del(&pstub->node); unlock(); free(pstub); return 0; } } return -1; }