C#与C++混淆点梳理

从C++到C#,发现C#写多了里面有些知识和C++混淆了,记下来理一理。
1、C++的类定义不会在class关键字前使用访问修饰符,即不会有public class Base {…}这种定义,而C#有。
C#的访问修饰符除了用于修饰member外还用于修饰type,其访问修饰符除了有和C++一样的public、protected和private外,还有internal和protected internal。
public:访问不受限制。
protected:访问仅限于包含类和包含类的派生类。
private:访问仅限于包含类。
internal:访问仅限于当前程序集。
protected internal:访问仅限于当前程序集和包含类的派生类。

修饰type时,如果是top-level types(没有被其它类型嵌套包含)只能使用public和internal,默认是internal。
如果是nested types则访问修饰符的情况有以下情况:
class默认修饰符是private,可以使用public、protected、private、internal、protected internal。
interface默认修饰符是public,只能使用public。
struct默认修饰符是private,可以使用public、private、internal。
enum默认修饰符是public,只能使用public。

当定义一个类型时需要考虑它的可访问级别是否依赖于其它类型或成员。例如一个基类的可访问级别最低要和派生类一致:
class BaseClass {…}
public class MyClass: BaseClass {…} // Error
上面的代码因为BaseClass的可访问级别internal低于MyClass的public编译器会报错。

参考:https://msdn.microsoft.com/zh-cn/library/wxh6fsc7.aspx

2、C++没有静态类,即不会有static class Base {…}这种定义,而C#有。
C#静态类中的成员变量和函数必须都是static的。它不能被继承,不能实例化,不能有实例化的构造函数,但可以有静态构造函数。

参考:http://blog.csdn.net/xiaobai1593/article/details/7278014
https://msdn.microsoft.com/zh-cn/library/98f28cdx.aspx

C运行时库

MS提供的C运行时库实在是多,今天又遇到了一个crtdll.dll竟然也是C运行时库,于是各种查资料,这里整理一下。

参考资料一:C Run-Time Libraries
1、libc(d).lib单线程的静态库,已经不可用了。
2、libcmt(d).lib多线程的静态库,编译选项MT(d),影响预定义_MT(_MT,_DEBUG)。
3、msvcrt(d).lib多线程的动态库,编译选项MD(d),影响预定义_MT,_DLL(_MT,_DLL,_DEBUG)。动态链接库名一般形如msvcr80.dll,其中80代表版本号,不过也看到过msvcrt.dll,估计是历史产物。
4、msvcmrt.lib用于托管代码和本地代码混合使用。
5、msvcurt.lib纯MSIL code编译而成。
6、crtdll.lib支持多线程,并且支持Win32s,动态链接库名为crtdll.dll,估计也是历史产物了。参考资料二:How To Use the C Run-Time

一个模块静态链接C运行时库意味着该C运行时库信息的存储就在这个模块里,这会导致同一进程内的不同模块的C运行时库函数不能相互影响。例如在一个静态链接CRT的DLL中调用_set_se_translator只能捕获该模块产生的异常。

How To Share All Data in a DLL

error C2440: ‘initializing’ : cannot convert from ‘PCTSTR’ to ‘ATL::CStringT’

CAtlString是可以在ANSI和UNICODE之间转换的,例如CAtlStringA strSomething = lpszSomething,这里的lpszSomething是wchar_t字符串类型。

可是你会发现这种语句有时候可以编过,有时候又不可以编过,非要用CAtlStringA strSomething(lpszSomething):
error C2440: 'initializing' : cannot convert from 'PCTSTR' to 'ATL::CStringT<BaseType,StringTraits>'
with
[
BaseType=char,
StringTraits=ATL::StrTraitATL<char,ATL::ChTraitsCRT<char>>
]
Constructor for class 'ATL::CStringT<BaseType,StringTraits>' is declared 'explicit'
with
[
BaseType=char,
StringTraits=ATL::StrTraitATL<char,ATL::ChTraitsCRT<char>>
]

遇到这种问题真是抑郁,明知是2B的问题却不明真相,每次遇到只能退缩改为显式构造。

这次深究了一下,CAtlString的实现会落在CStringT上,CStringT的构造函数对于YCHAR(即另一种字符类型,CStringT把自身的字符类型定义为XCHAR)作参数如下:
CSTRING_EXPLICIT CStringT( __in_z_opt const YCHAR* pszSrc )

注意这里有个CSTRING_EXPLICIT宏,它被定义为:
#ifdef _ATL_CSTRING_EXPLICIT_CONSTRUCTORS
#define CSTRING_EXPLICIT explicit
#else
#define CSTRING_EXPLICIT
#endif

原来是有_ATL_CSTRING_EXPLICIT_CONSTRUCTORS宏定义的时候不同类型的字符串转换需要显式调用构造函数,看看stdafx.h里还真有这个宏定义:
#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // some CString constructors will be explicit

删掉这个宏定义就能使用CAtlStringA strSomething = lpszSomething这类用法了。为什么需要这么一个宏去控制这类构造函数需要显式调用?

MS说得很清楚了,防止无心的字符串转换。_ATL_CSTRING_EXPLICIT_CONSTRUCTORS

Inline Hook

Inline Hook也是光听说过没练过,今天自己动手还是遇到了不少问题,主要是替换后的函数如何调用原函数和平衡其调用堆栈。Demo里没有实现如何先执行被Inline Hook覆盖的代码后跳转到原函数的流程,以后实现后补充。

#include <Windows.h>
#include <tchar.h>

//////////////////////////////////////////////////////////////////////////
// Inline Hook
#define JMP_CODE_SIZE 5
#pragma warning(push)
#pragma warning(disable:4311)
BOOL InlineHook(void* lpOldFunction, void* lpNewFunction)
{
BOOL bRet = FALSE;
byte btCode[JMP_CODE_SIZE] = {0xE9}; // 5字节相对跳转指令
do
{
if(NULL == lpOldFunction || NULL == lpNewFunction)
{
break;
}

DWORD* pdwAddr = (DWORD*)&btCode[1];
*pdwAddr = (DWORD)lpNewFunction – (DWORD)lpOldFunction – JMP_CODE_SIZE; // 计算相对跳转地址

DWORD dwProtect = 0;
if(VirtualProtect(lpOldFunction, JMP_CODE_SIZE, PAGE_READWRITE, &dwProtect))
{
memcpy(lpOldFunction, btCode, JMP_CODE_SIZE);
VirtualProtect(lpOldFunction, JMP_CODE_SIZE, dwProtect, &dwProtect);
bRet = TRUE;
}
} while (0);
return bRet;
}
#pragma warning(pop)

// 用于恢复Inline Hook
void* g_lpMessageBoxA = NULL;
byte g_btCode[JMP_CODE_SIZE] = {0};

//////////////////////////////////////////////////////////////////////////
// 真正实现MessageBoxA, MyMessageBoxA只是一个马甲
int WINAPI InternalMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
MessageBoxW(NULL, L”In MyMessageBoxA”, L”InlineHook”, MB_OK);

// 还原Inline Hook
DWORD dwProtect = 0;
if(VirtualProtect(g_lpMessageBoxA, JMP_CODE_SIZE, PAGE_READWRITE, &dwProtect))
{
memcpy(g_lpMessageBoxA, g_btCode, JMP_CODE_SIZE);
VirtualProtect(g_lpMessageBoxA, JMP_CODE_SIZE, dwProtect, &dwProtect);
}

// 调回真正的MessageBoxA
return MessageBoxA(hWnd, lpText, lpCaption, uType);
}

//////////////////////////////////////////////////////////////////////////
// Inline Hook用MyMessageBoxA替换MessageBoxA
// 编译器生成代码时会在函数前后增加一些堆栈平衡的代码, 导致直接ret 10h堆栈并不会平衡
// 因此需要使用_declspec(naked)修饰, 但函数内需要自己控制寄存器
_declspec(naked) int WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)
{
// 堆栈平衡
_asm
{
push ebp
mov ebp, esp
push dword ptr[ebp+0x14]
push dword ptr[ebp+0x10]
push dword ptr[ebp+0xC]
push dword ptr[ebp+0x8]
call InternalMessageBoxA
pop ebp
ret 10h ; 由于没有jmp回被Inline Hook的函数, 因此需要平衡堆栈.
}
}

int _tmain(int argc, TCHAR* argv[])
{
HMODULE hLib = LoadLibrary(_T(“user32.dll”));
if(hLib != NULL)
{
g_lpMessageBoxA = GetProcAddress(hLib, “MessageBoxA”);
memcpy(g_btCode, g_lpMessageBoxA, JMP_CODE_SIZE); // 保存即将覆盖的5字节代码
InlineHook(g_lpMessageBoxA, MyMessageBoxA); // Inline Hook MessageBoxA到MyMessageBoxA
MessageBoxA(NULL, “Real MessageBoxA”, “InlineHook”, MB_OK);
FreeLibrary(hLib);
}
return 0;
}

IAT Hook

IAT Hook早了解过了一直没有自己动手实现过,写了个demo却也花了不少时间,记下来备用。

#include <Windows.h>
#include <tchar.h>

//////////////////////////////////////////////////////////////////////////
// 用lpNewFunction替换hModule中导入的lpImageName的lpOldFunction
BOOL IATHook(HMODULE hModule, char* lpImageName, void* lpOldFunction, void* lpNewFunction)
{
BOOL bRet = FALSE;
do
{
if(NULL == hModule || NULL == lpImageName || NULL == lpOldFunction || NULL == lpNewFunction)
{
break;
}

PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;
if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE || pDosHeader->e_lfanew <= 0)
{
break;
}

PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + (DWORD_PTR)hModule);
if(pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
{
break;
}

// 从导入表中找到lpImageName的IMAGE_THUNK_DATA
PIMAGE_IMPORT_DESCRIPTOR pDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + (DWORD_PTR)hModule);
PIMAGE_THUNK_DATA pIATThunk = NULL;
while(pDescriptor->Characteristics != 0)
{
char* lpszName = (char*)(pDescriptor->Name + (DWORD_PTR)hModule);
if(0 == _stricmp(lpszName, lpImageName))
{
pIATThunk = (PIMAGE_THUNK_DATA)(pDescriptor->FirstThunk + (DWORD_PTR)hModule);
break;
}
++pDescriptor;
}

// 从IMAGE_THUNK_DATA找到需要替换的具体函数
if(NULL == pIATThunk)
{
break;
}
while(pIATThunk->u1.Function != 0)
{
if(pIATThunk->u1.Function == (DWORD_PTR)lpOldFunction)
{
DWORD dwProtect = 0;
if(VirtualProtect(&(pIATThunk->u1.Function), sizeof(DWORD), PAGE_READWRITE, &dwProtect))
{
bRet = WriteProcessMemory((HANDLE)-1, &(pIATThunk->u1.Function), &lpNewFunction, sizeof(DWORD), NULL);
VirtualProtect(&(pIATThunk->u1.Function), sizeof(DWORD), dwProtect, &dwProtect);
}
break;
}
++pIATThunk;
}
} while (0);
return bRet;
}

//////////////////////////////////////////////////////////////////////////
// 用MyMessageBox替换MessageBoxW
typedef int (WINAPI *MESSAGE_BOX)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
MESSAGE_BOX RealMessageBox = NULL;
int WINAPI MyMessageBox(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType)
{
int nRet = 0;
if(RealMessageBox != NULL)
{
RealMessageBox(NULL, _T(“In MyMessageBox”), _T(“IATHook”), MB_OK);
nRet = RealMessageBox(hWnd, lpText, lpCaption, uType);
}
return nRet;
}

int _tmain(int argc, TCHAR* argv[])
{
HMODULE hLib = LoadLibrary(_T(“user32.dll”));
if(hLib != NULL)
{
RealMessageBox = (MESSAGE_BOX)GetProcAddress(hLib, “MessageBoxW”);            // 需要IAT Hook的函数地址, 用于在IAT中查找并替换
IATHook(GetModuleHandle(NULL), “user32.dll”, RealMessageBox, MyMessageBox);    // IAT Hook本exe模块中的MessageBoxW
MessageBox(NULL, _T(“Real MessageBox”), _T(“IATHook”), MB_OK);
FreeLibrary(hLib);
}
return 0;
}

goto的坑

调试了一个crash,很诡异,程序是崩在一个线程函数中调用一个对象的虚函数——vptr被改了。

Review了这个对象所有引用的代码没能发现问题,只能操起windbg调试了,好在这个问题比较容易重现。调试中发现这个对象被正确的构造了之后一直没有释放,却在另一个模块中通过new申请到了同一块内存,因此造成vptr等内容被修改,线程函数中再调用其虚函数就崩溃了。

通过ba r 4 0xXXXXXXXX对这块内存下了个读断点,几次断下之后终于找到了罪魁祸首——一个函数内一个临时的指针变量delete掉了这块内存。

这不科学!这只是一个函数内的一个临时的指针变量,初始化为NULL,甚至逻辑都没有走到对它进行new赋值的地方,怎么会突然有了值,并且delete掉一个不相干的对象?!伪代码如下:

if(!DoSomething()) // 这里返回的是FALSE, 因此没走到new对pData赋值.
{
goto Exit;
}
BYTE* pData = NULL;

pData = new BYTE[1024];

Exit:
if(pData != NULL)
{
delete [] pData;
pData = NULL;
}

但是事实如此,反复review这段代码发现最有可能出现问题的地方在这处goto。通过编写demo发现VS 2005对于goto后面才定义的变量只会在函数进入前分配栈空间,要在执行到goto后面的赋值语句时才会对变量进行赋值。这也就让delete [] pData却释放了别处申请的内存成为了可能,因为指不定pData的栈内容是什么呢。

注意应尽量避免用goto,如果一定要使用保证函数进入就定义所有的变量并赋初值。