.netprofilingclr-profiling-api

How do you map a native to IL instruction pointer in-process


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.


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.