Guest Post - The Expression Evaluator

Guest Post - The Expression Evaluator

Rate This
  • Comments 9

Hi. My name is Ofek Shilon and I blog mostly about various VC++ tricks I come by. Today I’d like to explicitly introduce a debugging feature we all use daily but seldom refer to it by name – the native expression evaluator (abbreviated EE below).

The Basics

Every time you use the Watch window, a lot is going on behind the scenes. Whenever you type a variable name, something needs to map that name to the memory address and type of the named variable, then display that variable, properly formatted based on its type. Conversely, when you modify the contents of a variable - something needs to take your text input, convert it to the right type and correctly update the memory at the right address.

That something is the Expression Evaluator. It is an impressive and often overlooked piece of technology and once familiar with it, you can put it to good use, sometimes in surprising ways!

As the component name suggests, it is in fact able to parse and evaluate expressions:

s

These expressions can be assignment statements or can otherwise modify variables, too:

And more importantly – the expressions can include function calls!

The EE lives in various IDE contexts—the Watch window, the QuickWatch dialog, the Immediate window, Breakpoint conditions and a bit more – but it is most obvious (and most frequently used) in the various Watch windows. When expertly used this feature can raise debugging interactivity to surprising levels, and in fact comes rather close to being a full C++ interpreted environment (especially alongside Edit and Continue). However, there are several important differences between expression evaluation and real code compilation that you should be aware of.

Calling functions

What can and can’t be called

The EE cannot call inlined functions, as they are not really ‘there’ as code to be called.  On the upside, the EE is blind to access privileges and would happily evaluate calls to private methods or file-static functions.

Side effects

The expressions are being evaluated at the context of the currently selected thread (that is, the one whose stack, registers and local variables are shown in the corresponding IDE windows). In particular, expressions are evaluated in the debuggee’s address space, which could have unexpected consequences if one isn’t careful. For example, suppose you’re trying to evaluate an expression which allocates heap memory – but a different thread is frozen holding the heap lock. The debuggee would be left in an unknown state and the IDE should take measures against hanging by itself!  Many similar scenarios might occur: since all debuggee threads are frozen various resources might be in unstable states and one should be careful when messing with them.  That line of thought is probably what caused the EE designers to explicitly forbid usage of a large subset of CRT and WIN32 API. The full list of banned APIs is an implementation detail and is subject to change between versions or in updates (and was indeed expanded considerably in VS2010).

However,

(1)    In practice, side effects as described are extremely rare, and I’ve been messing interactively with debuggee state for years with no problems.

(2)    The tests against this list are shallow, and you can easily bypass them by wrapping API in the list with your own functions (in advance, i.e. at the source itself):

All that being said, such bypasses are undocumented and unsupported – use them at your own risk!

The Context Operator

The EE does not include a full-fledged linker, and when you call functions outside the main executable the EE might require help in resolving the call. You can deliver that help with the context operator:

{,,DllName.dll}FunctionName()

(You can also omit the ‘.dll’ in the module name. )

Some Rough Edges

As useful as it is, the EE parsing and symbol resolution will probably never be as robust as the compiler’s – and sometimes some workarounds are in order.

Symbols sometimes need to be ‘resolved manually’ by taking addresses and casting:

Enum types are mostly recognized, but individual enumerators (enum values) are not. Implicit casting to an enum type can fail too. Luckily, you can easily cast the raw integral values yourself:

If symbols reside in namespaces, they must be fully qualified. If functions are template, the template types must be fully (sometimes very verbosely) specified.  You may come across other similar behaviors. As a rule of thumb, when things aren’t going as you expected – try to be as explicit as possible.

 Applications

 Finally, here are some examples of real life usage – specifically, enhanced investigation of memory issues in debug builds.

_CrtCheckMemory essentially walks the CRT heap and detects out-of-bound writes by inspecting the padding that CRT inserts at the end of allocated blocks. Now you can pin-point the origin of corruptions without repeatedly spreading _CrtCheckMemory at the source and recompiling. Here’s an evaluation right before a corruption:

And here it is again two lines later (after clicking refresh, to re-evaluate):

If you define a _CrtMemState slot at the source, you can populate and inspect it interactively using the many tools the CRT supplies. First fill it:

Then explore its contents – the CRT APIs ultimately call OutputDebugStringA , which still dumps to the output window:

You can also reserve several _CrtMemoryState’s at the source, populate them at different locations and diff them with _CrtMemDifference.  And so forth - you get the idea.

Bottom Line

Hopefully that’s enough as an intro to this underappreciated debugging feature. I’d love to hear (via the comments, my blog or just ofekshilon-at-gmail) whether all this works out for you, and of other cool directions you’re taking it to.

My deep thanks goes to Eric Battalio and James McNellis, for making this post happen and then improving it.

Cheers,

-Ofek

Thanks for the great article, Ofek. Readers, if you have an idea for an article that might appeal to the Visual C++ / C++ community, ping me @ ebattali@microsoft.com. I encourage you to ping me even if you think you can't write, the topic might not fit, or you have any other reservations! :) 

  • Since my experience is quite different, I'll just ask directly: what you're showing should also work *exactly as is*, with the same features and limitations, if done via the Immediate window, right?

    I've never thought to try calling functions via the Watch window, but several times from the Immediate Window.

    And my experience is that it virtually never works. Especially anything C++ (non-POD types, overloaded operators, templates etc) seems to be beyond its capabilities. I've also had very limited luck with simple C code, but that *may* be because of the blacklisted CRT functions you mentioned. (then I'd like to point out that the EE designers have designed a tool which provides the *illusion* that it doesn't work, which might explain why this blog post is necessary, and why no one knows that the tool exists/works. Perhaps they should provide a different error message for those blacklisted functions?)

    Seeing it described as nearly "an interpreted C++ environment" is a bit of a surprise. I wish it was, but that's just not my experience. Maybe I'm just doing it wrong though. I will give it another chance.

    Until now, I would have said that this is the only feature where GDB is actually miles ahead of the MSVC debugger. Perhaps MSVC simply hides this functionality better. Regardless, it's one feature I'd *really* like to see improved, because yes, when it works, it is amazingly useful.

    By the way, forbidding CRT functions from being called in this manner seems completely absurd. Why would anyone prevent developers from (potentially) wrecking their own process' state *during debugging*? When I'm debugging I want the tools at my disposal to do what I ask them to, knowing that I can easily corrupt some state or other. That's no different from when I move the instruction pointer, or when I alter the value of some variable or the contents of memory. I can do all those things, and I can easily wreck my process. I'd expect the expression evaluator to also allow me to call, say, GetCurrentThreadId(), knowing that I'm doing so from a frozen debugged process with all the weird state that entails.

    Anyway, thanks for the interesting read.

  • Hi Ofek, great article.

    AFAIK, the debugger does not have a black list of crt functions, and there are many much more common cases where func-eval will trash your process than just heap lock.

    For instance, if you func-eval a function that waits on a contested lock, it will deadlock in an unrecoverable state. Similarily, func-evals to that call into cross apartment com objects will also deadlock in an unrecoverable state.  

    Secondly,  there are some functions in the crt for which the debugger will interpret the func-eval instead of actually executing it by default. These are most string comparison functions. However, the developer can force a real func-eval using the module prefix. For instance, msvcr110d.dll!strlen(mystr) will actually call strlen on the string. The full list of func-eval intrinsics can be found here: msdn.microsoft.com/.../y2t7ahxk.aspx

    This post is provided “as-is” and confers no warrantees or rights.

    Jackson Davis

    Visual Studio Debugger

  • @grumpy:  AFAIK the mechanics behind the watch window are identical to those behind the immediate window. Can you give a concrete example of an evaluation that fails for you?

    Regarding operators: the VS2012 (vastly improved) documentation gives a potential reason why operator evaluation might fail:

        " The debugger does not support overloaded operators with both const and non-const versions.   "

    While this might be an understandable design decision - the result of any evaluation in the debugger typically isn't assigned to any variable which can be classified as const/non-const - sadly this is a very typical scenario for operators, and could substantially limit their usage in debugger

    (msdn.microsoft.com/.../y2t7ahxk.aspx)

    @Jackson Davis:  Thanks for the inside info!  I learn something every time I blog.   Can you please shed some light on why ntdll/user32 calls from the debugger typically fail to resolve?   Maybe it's because they're forwarded to other dlls?   Can you say why calls that were resolved on VS2005 fail to do so on VS2010/12?

  • Err, that would be kernel32/kernelbase calls. User32 calls are obviously out.

  • Do you plan to build Roslyn-like for C++ ? When Roslyn for C#/VB will be ready, C++ experience another round of drop in popularity and usage, no doubt.

  • Tristan there are already C++ interpreters build using LLVM/Clang tech, although I appreciate you may mean a 'fragment' compiler (what Anders refers to as The Compiler As A Service).

    Really not sure how you're measuring the drop in popularity. Microsoft is re-embracing native after its decade plus long love affair / grand experiment with managed everywhere. Have you taken a look at what powers WinRT (arguably the future of the Windows client) or for that matter what tech underpins just about every mobile / device platform out there?

  • Thanks a lot.

    The "context operator" part and " _CrtMemState " part are something I would never know if I wouldn't read your post.

    Looking forward to see more of these kind of posts!

  • Another nice trick:

    Put "@err" to watch list to see GetLastError() code.

    Put "@err, hr" to see GetLastError() code with description.

  • @Dmitry:  That is one of the special psedovariables the debugger supports. There are many others, and all are useful:

    msdn.microsoft.com/.../dd252945.aspx

    msdn.microsoft.com/.../ms164891.aspx

Page 1 of 1 (9 items)