Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I debug into an unmanaged BCL (InternalCall) method?

I want to debug into the implementation of a [MethodImpl(MethodImplOptions.InternalCall)] BCL method, which is presumably implemented in C++. (In this particular case, I'm looking at System.String.nativeCompareOrdinal.) This is mainly because I'm nosy and want to know how it's implemented.

However, the Visual Studio debugger is refusing to step into that method. I can set a breakpoint on this call:

"Hello".Equals("hello", StringComparison.OrdinalIgnoreCase);

then bring up Debug > Windows > Disassembly, step into the Equals call, and step until it gets to the call x86 instruction. But when I try to use "Step Into" on that call (which I know from Reflector is the nativeCompareOrdinal call), it doesn't step to the first instruction inside nativeCompareOrdinal like I want -- it steps over instead, and goes straight to the next x86 instruction in Equals.

I'm building as x86, since mixed-mode debugging isn't supported for x64 apps. I've unchecked "Just My Code" in Tools > Options > Debugging, and I have "Enable unmanaged code debugging" checked in project properties > Debug tab, but it still steps over the call. I also tried starting the process and then attaching the debugger, and explicitly attaching both the managed and native debuggers, but it still won't step into that InternalCall method.

How can I get the Visual Studio debugger to step into an unmanaged method?

like image 500
Joe White Avatar asked Sep 23 '10 12:09

Joe White


1 Answers

Yes, it is tricky. The offset you see for the CALL instruction is bogus. Plus it won't let you navigate to an unmanaged code address when the current focus is on a managed function.

Start by enabling unmanaged code debugging and setting a breakpoint on the call. Run the code and when the break point hits use Debug + Windows + Disassembly:

            "Hello".Equals("hello", StringComparison.OrdinalIgnoreCase);
00000025  call        6E53D5D0 
0000002a  nop              

The debugger tries to display the absolute address but gets it wrong because it uses the bogus incremental address instead of the real instruction address. So first recover the true relative value: 0x6E53D5D0 - 0x2A = 0x6E53D5A6.

Next you need to find the real code address. Debug + Windows + Registers and look at the value of the EIP register. 0x009A0095 in my case. Add 5 to get to the nop, then add the relative offset: 0x9A0095 + 5 + 0x6E53D5A6 = 0x6EEDD640. The real address of the function.

Debug + Windows + Call Stack and double-click an unmanaged stack frame. Now you can enter the calculated address in the Disassembly window's Address box, prefix with 0x.

6EEDD640  push        ebp  
6EEDD641  mov         ebp,esp 
6EEDD643  push        edi  
6EEDD644  push        esi  
6EEDD645  push        ebx  
6EEDD646  sub         esp,18h 
etc...

Bingo, you're know you're good if you see the stack frame setup code. Set a breakpoint on it and press F5.


Of course, you'll be stepping machine code since there's no source code available. You'll get much better insight in what this code is doing by looking at the SSCLI20 source code. No guarantee that it will be a match for the actual code in your current version of the CLR but my experience is that these low-level code chunks that have been around since 1.0 are highly conserved. The implementation is in clr\src\classlibnative\nls, not sure which source code file. It won't be named "nativeCompareOrdinal", that's just an internal name used by ecall.cpp.

like image 57
Hans Passant Avatar answered Oct 11 '22 10:10

Hans Passant