Windows下C/C++单元测试两种打桩方法

敏捷开发,非常强调效率。如果只顾着效率,代码质量必须很低,未来的维护成本必定大增,会得不偿失。
那么敏捷开发中,CodeReview和单元测试是保证代码质量的重要手段。

1. 问题

如下代码,通过宏开关来控制桩函数,虽然功能上能够达到打桩的效果,但是对待测代码增加了大量的宏,影响代码的可读性。

int Fun1()
{
	return 12;
}

int Fun()
{
#ifdef STUB_ENABLED
	return 2 + Fun1_Stub();
#else
	return 2 + Fun1();
#endif
}

2. 方法

面对上述问题,有没有尽量不修改源文件的方法来达到打桩替换函数的效果呢?下面介绍两种效果不错的,用于Windows平台上C/C++打桩的方法。

2.1. 宏

利用宏的不定参加上##连接符,可以完成打桩的效果。主要原理是调用函数和定义函数参数不同来达到宏展开结果的不同。

#define A1(...) FUN1(A1, __VA_ARGS__)(__VA_ARGS__)
#define ARG_int __Dummy,A1,

#define FUN1(arg1, ...) FUN2(arg1, arg1##Stub, __VA_ARGS__)
#define FUN2(arg1, arg2, arg3,...) FUN3(arg2, ARG_##arg3)
#define FUN3(arg1, ...)  FUN4(FUN5(__VA_ARGS__, arg1))
#define FUN4(...)  __VA_ARGS__  // 此句很关键,此句会让上面的(__VA_ARGS__, arg1)先展开成普通字符串再展开FUN5
#define FUN5(x, y,...)   y



int A1Stub(int a)
{
  return 11;
}
int A1(int a)
{
  return 20;
}


void TestMacro()
{
  A1(4);
}

可以通过编译器的预编译命令来查看宏展开的情况。

VS:cl -E xxx.cpp
g++: g++ -E xxx.cpp

在这里插入图片描述

2.2. 注入跳转代码

  1. 待测文件
#include <stdio.h>
#include "Fun.h"



int AFunc(void)
{
	return 55 + BFunc();
}

int BFunc(void)
{
	return 200;
}
  1. 测试代码

#define FLATJMPCODE_LENGTH 5            //x86 平坦内存模式下,绝对跳转指令长度
#define FLATJMPCMD_LENGTH  1            //机械码0xe9长度
#define FLATJMPCMD         0xe9         //相应汇编的jmp指令

// 记录被打桩函数的内容。以便恢复
BYTE g_apiBackup[FLATJMPCODE_LENGTH+FLATJMPCMD_LENGTH];

BOOL SetStub(LPVOID pFun, LPVOID hookFun)
{
	DWORD oldProtect =0;
	DWORD TempProtectVar = 0;
	char newCode[6] = {0};                                     //用于读取函数原有内存信息
	HANDLE hProgress = GetCurrentProcess();           //获取进程伪句柄
	int SIZE = FLATJMPCODE_LENGTH + FLATJMPCMD_LENGTH;     //须要改动的内存大小
	if (!VirtualProtect(pFun, SIZE, PAGE_EXECUTE_READWRITE, &oldProtect))  //改动内存为可读写
	{
		return FALSE;
	}
	if (!ReadProcessMemory(hProgress, pFun, newCode, SIZE, NULL))              //读取内存
	{
		return FALSE;
	}
	memcpy((void*)g_apiBackup, (const void*)newCode, sizeof(g_apiBackup));      //保存被打桩函数信息
	*(BYTE*)pFun = FLATJMPCMD;
	*(DWORD*)((BYTE*)pFun + FLATJMPCMD_LENGTH) = (DWORD)hookFun - (DWORD)pFun - FLATJMPCODE_LENGTH;   //桩函数注入 
	VirtualProtect(pFun, SIZE, oldProtect, &TempProtectVar);  //恢复保护属性

	return TRUE;
}

BOOL ClearStub(LPVOID pFun)
{
	BOOL    IsSuccess = FALSE;
	DWORD   TempProtectVar;              //暂时保护属性变量
	MEMORY_BASIC_INFORMATION MemInfo;    //内存分页属性信息

	VirtualQuery(pFun,&MemInfo,sizeof(MEMORY_BASIC_INFORMATION));

	if(VirtualProtect(MemInfo.BaseAddress,MemInfo.RegionSize, PAGE_EXECUTE_READWRITE,&MemInfo.Protect))                            //改动页面为可写
	{
		memcpy((void*)pFun, (const void*)g_apiBackup, sizeof(g_apiBackup));  //恢复代码段

		VirtualProtect(MemInfo.BaseAddress,MemInfo.RegionSize, MemInfo.Protect,&TempProtectVar);                        //改回原属性

		IsSuccess = TRUE;
	}

	return IsSuccess;
}

int BFuncStub(void)
{
	return 300;
}

int main(int argc, char* argv[])
{
	printf("The value:%d\r\n", AFunc());
	SetStub(BFunc, BFuncStub);
	printf("The value:%d\r\n", AFunc());
	ClearStub(BFunc);
	printf("The value:%d\r\n", AFunc());
	return 0;
}
  1. 关键点

0021100F E9 0C 04 00 00 jmp BFunc (211420h)

E9是跳转jmp指令0C0400是BFunc的相对地址。
在这里插入图片描述
那么如果想跳转到BFuncStub, 就需要将跳转地址之后的相对地址换成BFuncStub的即可。如:

(DWORD)((BYTE*)pFun + FLATJMPCMD_LENGTH) = (DWORD)hookFun - (DWORD)pFun - FLATJMPCODE_LENGTH; //桩函数注入

因为整个跳转指令占5个字节,所以需要减去FLATJMPCODE_LENGTH。

  • 修改跳转地址之前:
    在这里插入图片描述

  • 修改跳转地址内存之后

  • 在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值