我正在写一个类,我想用它来记录使用Windows Vista/7的计算机上的当前调用堆栈。(非常类似于“遍历调用堆栈”http://www.codeproject.com/Articles/11132/Walking-the-callstack)。
首先,我使用RtlCaptureContext获取当前上下文记录,然后使用StackWalk64获取各个堆栈帧。现在,我意识到STACKFRAME64中的Program计数器实际上会在我关闭程序并重新启动时更改特定代码行。出于某种原因,我认为只要我不更改源代码并重新编译它,特定代码行的PC地址就会保持不变。
我需要PC地址来使用SymFromAddr和SymGetLineFromAddr64来获取有关被调用函数、代码文件和行号的信息。不幸的是,只有当程序调试数据库(PDB-File)存在时,它才能工作,但我不能向客户端提供它。
我的计划是记录调用堆栈的PC地址(无论何时需要),然后从客户端将其发送给我。因此,我可以使用我的PDB-Files来找出调用了哪些函数,但这当然只有在PC地址是唯一标识符的情况下才有效。因为它们在我每次启动程序时都会改变,所以我不能使用这种方法。
您是否知道读取调用堆栈或克服更改程序计数器的问题的更好方法?
我认为一种可能的解决方案是始终获取已知位置的PC地址,并将其用作仅确定不同PC地址之间的偏移量的参考。这似乎是有效的,但我不确定这是不是一个有效的方法,并且总是有效。
非常感谢您的帮助!我将在codeproject.com中发布最终的(封装的)解决方案,如果您愿意,我会说您帮助了我。
发布于 2012-02-24 12:59:58
使用信息表CONTEXT,你可以在PE图像中找到函数段和偏移量。例如,您可以使用此信息从链接器生成的.map文件中获取函数名称。
CONTEXT结构。您对程序计数器成员感兴趣。由于CONTEXT是依赖于平台的,您必须自己弄清楚它。您在初始化时就已经这样做了,例如用于x64窗口的STACKFRAME64.AddrPC.Offset = CONTEXT.Rip。现在我们开始堆栈遍历,并使用由StaclkWalk64填充的STACKFRAME64.AddrPC.Offset作为我们的起点。RVA = STACKFRAME64.AddrPC.Offset - AllocationBase将其转换为相对虚拟地址。你可以使用VirtualQuery.AllocationBase,你需要找到这个RVA属于哪个节,并从中减去节的起始地址,得到SectionOffset:SectionOffset = RVA - SectionBase = STACKFRAME64.AddrPC.Offset - AllocationBase - SectionBase。为此,您需要访问PE映像头结构(IMAGE_DOS_HEADER、IMAGE_NT_HEADER、IMAGE_SECTION_HEADER),以获取PE中的区段数量及其起始/结束地址。很不错的straightforward.就这样。现在你在PE镜像中有了节号和偏移量。函数偏移量是.map文件中比SectionOffset小的最大偏移量。
如果你愿意,我可以稍后发布代码。
编辑:打印function address的代码(假设使用x64通用处理器):
#include <iostream>
#include <windows.h>
#include <dbghelp.h>
void GenerateReport( void )
{
::CONTEXT lContext;
::ZeroMemory( &lContext, sizeof( ::CONTEXT ) );
::RtlCaptureContext( &lContext );
::STACKFRAME64 lFrameStack;
::ZeroMemory( &lFrameStack, sizeof( ::STACKFRAME64 ) );
lFrameStack.AddrPC.Offset = lContext.Rip;
lFrameStack.AddrFrame.Offset = lContext.Rbp;
lFrameStack.AddrStack.Offset = lContext.Rsp;
lFrameStack.AddrPC.Mode = lFrameStack.AddrFrame.Mode = lFrameStack.AddrStack.Mode = AddrModeFlat;
::DWORD lTypeMachine = IMAGE_FILE_MACHINE_AMD64;
for( auto i = ::DWORD(); i < 32; i++ )
{
if( !::StackWalk64( lTypeMachine, ::GetCurrentProcess(), ::GetCurrentThread(), &lFrameStack, lTypeMachine == IMAGE_FILE_MACHINE_I386 ? 0 : &lContext,
nullptr, &::SymFunctionTableAccess64, &::SymGetModuleBase64, nullptr ) )
{
break;
}
if( lFrameStack.AddrPC.Offset != 0 )
{
::MEMORY_BASIC_INFORMATION lInfoMemory;
::VirtualQuery( ( ::PVOID )lFrameStack.AddrPC.Offset, &lInfoMemory, sizeof( lInfoMemory ) );
::DWORD64 lBaseAllocation = reinterpret_cast< ::DWORD64 >( lInfoMemory.AllocationBase );
::TCHAR lNameModule[ 1024 ];
::GetModuleFileName( reinterpret_cast< ::HMODULE >( lBaseAllocation ), lNameModule, 1024 );
PIMAGE_DOS_HEADER lHeaderDOS = reinterpret_cast< PIMAGE_DOS_HEADER >( lBaseAllocation );
PIMAGE_NT_HEADERS lHeaderNT = reinterpret_cast< PIMAGE_NT_HEADERS >( lBaseAllocation + lHeaderDOS->e_lfanew );
PIMAGE_SECTION_HEADER lHeaderSection = IMAGE_FIRST_SECTION( lHeaderNT );
::DWORD64 lRVA = lFrameStack.AddrPC.Offset - lBaseAllocation;
::DWORD64 lNumberSection = ::DWORD64();
::DWORD64 lOffsetSection = ::DWORD64();
for( auto lCnt = ::DWORD64(); lCnt < lHeaderNT->FileHeader.NumberOfSections; lCnt++, lHeaderSection++ )
{
::DWORD64 lSectionBase = lHeaderSection->VirtualAddress;
::DWORD64 lSectionEnd = lSectionBase + max( lHeaderSection->SizeOfRawData, lHeaderSection->Misc.VirtualSize );
if( ( lRVA >= lSectionBase ) && ( lRVA <= lSectionEnd ) )
{
lNumberSection = lCnt + 1;
lOffsetSection = lRVA - lSectionBase;
break;
}
}
std::cout << lNameModule << " : 000" << lNumberSection << " : " << reinterpret_cast< void * >( lOffsetSection ) << std::endl;
}
else
{
break;
}
}
}
void Run( void );
void Run( void )
{
GenerateReport();
std::cout << "------------------" << std::endl;
}
int main( void )
{
::SymSetOptions( SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS );
::SymInitialize( ::GetCurrentProcess(), 0, 1 );
try
{
Run();
}
catch( ... )
{
}
::SymCleanup( ::GetCurrentProcess() );
return ( 0 );
}注意,我们的调用堆栈是(从里到外) GenerateReport()->Run()->main()。程序输出(在我的机器上,路径是绝对的):
D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000002F8D
D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 00000000000031EB
D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000003253
D:\Work\C++\Source\Application\Prototype.Console\Prototype.Console.exe : 0001 : 0000000000007947
C:\Windows\system32\kernel32.dll : 0001 : 000000000001552D
C:\Windows\SYSTEM32\ntdll.dll : 0001 : 000000000002B521
------------------现在,就地址而言,调用堆栈是(从内到外) 00002F8D->000031EB->00003253->00007947->0001552D->0002B521。比较前三个偏移量与.map文件内容:
...
0001:00002f40 ?GenerateReport@@YAXXZ 0000000140003f40 f FMain.obj
0001:000031e0 ?Run@@YAXXZ 00000001400041e0 f FMain.obj
0001:00003220 main 0000000140004220 f FMain.obj
...其中00002f40是最接近00002F8D的较小偏移量,依此类推。后三个地址指的是调用main (_tmainCRTstartup等)的CRT/OS函数-我们应该忽略它们...
因此,我们可以看到,我们能够在.map文件的帮助下恢复堆栈跟踪。为了为抛出的异常生成堆栈跟踪,您所要做的就是将GenerateReport()代码放入异常构造函数(实际上,此GenerateReport()取自我的自定义异常类构造函数代码(它的一部分))。
发布于 2012-02-24 11:54:49
堆栈本身是不够的,您需要加载的模块映射,这样您就可以将任何地址(随机、真)与模块关联,并定位PDB符号。但您实际上是在重新发明轮子,因为至少有两种支持良好的开箱即用解决方案可以解决这个问题:
MiniDumpWriteDump。你的应用程序不应该直接调用这个.exe,而是应该附带一个很小的.exe,它所做的一切都需要一个进程的转储(进程ID作为参数),当你的应用程序遇到错误条件时,应该启动这个.exe,然后等待它的完成。原因是“转储”进程将在转储期间冻结转储的进程,因此被转储的进程不能与接受转储的进程相同。此方案在所有实现WER的应用程序中都很常见。更不用说结果转储是一个真正的.mdmp,您可以将其加载到WinDbg中(如果您喜欢,也可以加载到VisualStudio中)。所以,首先,不要重复发明轮子。作为附注,还有一些服务可以为错误报告增加价值,比如聚合、通知、跟踪和自动客户端响应,比如前面提到的微软提供的WER (你的代码必须经过数字签名才能合格),airbreak.io,exceptioneer.com,bugcollect.com (这个是你真正创建的)和其他服务,但afaik。只有WER可以与本机Windows应用程序一起使用。
发布于 2012-02-24 11:41:35
你需要发送程序的运行内存映射,它告诉你从客户端加载的基地址库/程序。
然后你就可以用基址计算符号了。
https://stackoverflow.com/questions/9424568
复制相似问题