在软件开发中,有时需要知道是哪个模块调用了函数。这可能是为了验证调用者是否经过认证,或者根据调用上下文返回不同的结果。本文基于Chavdar Dimitrov的文章,对其进行了扩展,使其能够处理.NET调用者的情况,并解决了一些在Windows安全策略下可能遇到的问题。
Dimitrov的文章主要针对原生代码,但并未涉及.NET调用者的情况。旧的DLL可能会在发布版本中崩溃,这可能是由于Windows安全策略的加强。因此,对其进行了改进,以满足新的需求,尽管功能有所减少。
函数是用C++编写的,以便能够轻松地从托管和非托管代码中调用。提供了DLL和EXE文件,以应对可能出现的各种情况。此外,必须安装.NET 2.0运行时,因为stackdumper.dll
是一个混合DLL,其中某些文件是用/clr
选项编译的。外部代码将调用GetCallingModulePath
,这是stackdumper.dll
中的一个入口点。出于某种原因,这个函数必须有一个参数。
const char* _stdcall GetCallingModulePath(int arg) {
long reg_ebd;
__asm {
mov eax, ebp
mov reg_ebd, eax
}
ADDR callerAddr;
unsigned i = 0;
HANDLE h = GetCurrentProcess();
module.empty();
callerAddr = GetCallerAddr(reg_ebd);
if (callerAddr == 0) goto last;
if (getFuncInfo(callerAddr,module) > 0) {
BOOL bnet = IsDotNetRuntime((char*)module.c_str());
if (bnet) {
BOOL bres = GetDotNetCallerFileName(module);
if (bres == TRUE) goto last;
}
}
long temp = 0;
SIZE_T cnt;
long* p = (long*)reg_ebd;
BOOL bres = ReadProcessMemory(h,(LPCVOID)p,(LPVOID)&temp,sizeof(long),&cnt);
reg_ebd = temp;
i++;
last:
// ...
}
如果想要理解原生代码中的栈跟踪是如何工作的,应该从上面提到的Dimitrov的文章开始。
GetDotNetCallerFileName
托管函数将跳过栈中的所有.NET运行时程序集,返回实际调用函数的程序集。
int GetDotNetCallerFileName(string& module) {
int res = FALSE;
try {
Assembly^ callerAssembly = Assembly::GetCallingAssembly();
if (callerAssembly == nullptr) return FALSE;
String^ sysdir = System::Runtime::InteropServices::RuntimeEnvironment::GetRuntimeDirectory();
// 跳过所有.NET框架程序集和来自同一程序集的调用
String^ strCallerPath = callerAssembly->Location;
String^ directoryName = nullptr;
if (strCallerPath != nullptr)
directoryName = Path::GetDirectoryName(strCallerPath) + "\\";
while (directoryName != nullptr && (String::Compare(sysdir, directoryName, true) == 0) || callerAssembly == Assembly::GetExecutingAssembly()) {
strCallerPath = nullptr;
callerAssembly = callerAssembly->GetCallingAssembly();
directoryName = nullptr;
if (strCallerPath != nullptr)
directoryName = Path::GetDirectoryName(strCallerPath) + "\\";
strCallerPath = callerAssembly->Location;
}
// ...
}
}
对于原生调用,无论是直接调用还是间接调用,一切都很直接:
const char* szc = GetCallingModulePath(1);
printf("expected result: test.exe\nfinal result = %s\n\n", szc);
szc = NativeDllCall(1);
printf("expected result: Nativedll.dll\nfinal result = %s\n", szc);
char** pszc = NULL, **original = NULL;
int cnt = GetModuleStackTraceFromNative(pszc);
original = pszc;
printf("\nstack trace---------\n");
while (cnt-- > 0) {
printf("Trace: = %s\n", *pszc);
CoTaskMemFree(*pszc);
pszc++;
}
CoTaskMemFree(original);
不需要在调用GetCallingModulePath
后调用CoTaskMemFree
,因为返回的指针是stackdumper.dll
中的一个全局变量。如果从托管代码(如C#)调用,必须将方法作为extern import
:
[DllImport("stackdumper.dll", CharSet = CharSet.Ansi)]
static extern string GetCallingModulePath(int arg);