Question

When using the unmanaged API for the .NET framework to profile a .NET process in-process, is it possible to look up the IL instruction pointer that correlates to the native instruction pointer provided to the StackSnapshotCallback function?

As is probably obvious, I am taking a snapshot of the current stack, and would like to provide file and line number information in the stack dump. The Managed Stack Explorer does this by querying ISymUnmanagedMethod::GetSequencePoints. This is great, but the sequence points are associated to offsets, and I have so far assumed these are offsets from the beginning of the method ( in intermediate language ).

In a follow-up comment to his blog post Profiler stack walking: Basics and beyond, David Broman indicates that this mapping can be achieved using ICorDebugCode::GetILToNativeMapping. However, this is not ideal as getting this interface requires attaching to my process from another, debugger process.

I would like to avoid that step because I would like to continue to be able to run my application from within the visual studio debugger while I am taking these snapshots. It makes it easier to click on the line number in the output window and go to the code in question.

The functionality is possible.... you can spit out a line-numbered stack trace at will inside of managed code, the only question, is it accessible. Also, I don't want to use the System::Diagnostics::StackTrace or System::Environment::StackTrace functionality because, for performance reasons, I need to delay the actual dump of the stack.... so saving the cost for resolution of method names and code location for later is desirable... along with the ability to intermix native and managed frames.

Was it helpful?

Solution

In order to translate from a native instruction pointer as provided by ICorProfilerInfo2::DoStackSnapshot to an intermediate language method offset, you must take two steps since DoStackSnapshot provides a FunctionID and native instruction pointer as a virtual memory address.

Step 1, is to convert the instruction pointer to a native code method offset. ( an offset from the beginning of the JITed method). This can be done with ICorProfilerInfo2::GetCodeInfo2

ULONG32 pcIL(0xffffffff);
HRESULT hr(E_FAIL);
COR_PRF_CODE_INFO* codeInfo(NULL);
COR_DEBUG_IL_TO_NATIVE_MAP* map(NULL);
ULONG32 cItem(0);

UINT_PTR nativePCOffset(0xffffffff);
if (SUCCEEDED(hr = pInfo->GetCodeInfo2(functioId, 0, &cItem, NULL)) &&
    (NULL != (codeInfo = new COR_PRF_CODE_INFO[cItem])))
{
    if (SUCCEEDED(hr = pInfo->GetCodeInfo2(functionId, cItem, &cItem, codeInfo)))
    {
        COR_PRF_CODE_INFO *pCur(codeInfo), *pEnd(codeInfo + cItem);
        nativePCOffset = 0;
        for (; pCur < pEnd; pCur++)
        {
            // 'ip' is the UINT_PTR passed to the StackSnapshotCallback as named in
            // the docs I am looking at 
            if ((ip >= pCur->startAddress) && (ip < (pCur->startAddress + pCur->size)))
            {
                nativePCOffset += (instructionPtr - pCur->startAddress);
                break;
            }
            else
            {
                nativePCOffset += pCur->size;
            }

        }
    }
    delete[] codeInfo; codeInfo = NULL;
}

Step 2. Once you have an offset from the begining of the natvie code method, you can use this to convert to an offset from the begining of the intermediate language method using ICorProfilerInfo2::GetILToNativeMapping.

if ((nativePCOffset != -1) &&
    SUCCEEDED(hr = pInfo->GetILToNativeMapping(functionId, 0, &cItem, NULL)) &&
    (NULL != (map = new COR_DEBUG_IL_TO_NATIVE_MAP[cItem])))
{
    if (SUCCEEDED(pInfo->GetILToNativeMapping(functionId, cItem, &cItem, map)))
    {
        COR_DEBUG_IL_TO_NATIVE_MAP* mapCurrent = map + (cItem - 1);
        for (;mapCurrent >= map; mapCurrent--)
        {
            if ((mapCurrent->nativeStartOffset <= nativePCOffset) && 
                (mapCurrent->nativeEndOffset > nativePCOffset))
            {
                pcIL = mapCurrent->ilOffset;
                break;
            }
        }
    }
    delete[] map; map = NULL;
}

This can then be used to map the code location to a file and line number using the symbol APIs

Thanks to Mithun Shanbhag for direction in finding the solution.

OTHER TIPS

Console.WriteLine("StackTrace: '{0}'", Environment.StackTrace);

Make sure your build generates symbols.

Expanding on the discussion:

As is probably obvious, I am taking a snapshot of the current stack, and would like to provide file and line number information in the stack dump.

Given this - It looks like the only reason you're not attaching to process is so that you can debug your tool , or parts of it , easily, as you're developing it. That IMO is a poor excuse for not choosing a better design (ICorDebug or w/e ) when its available. The reason its poor design is because your code executes in the process space of (presumably) external binaries , causing nasty ('sometimes' rare) side effects (including corrupting somebody else data) in known (or worse - unknown) corrupt process states. That should be enough to begin with, but even otherwise, there are several edge cases with multi-threaded code, etc where the design needs to be worked around.

Most people generally ask "What are you really trying to do?" as a reply to an overtly complex way of doing things. In most cases there is a simpler/easier way. Having written a stack tracer for native code, I know it can get messy.

Now maybe you might end up making everything work , so - Just my $.02

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top