llvm / llvm-project

The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
http://llvm.org
Other
27.81k stars 11.45k forks source link

"Just My Code" support for LLDB #61152

Open DavidSpickett opened 1 year ago

DavidSpickett commented 1 year ago

This issue is to gather information for potentially supporting the "Just My Code" feature in LLDB.

This is a Microsoft Debugger feature announced here: https://devblogs.microsoft.com/cppblog/announcing-jmc-stepping-in-visual-studio/

The Clang code generation for it was implemented for ELF in: https://discourse.llvm.org/t/rfc-just-my-code-stepping-for-non-msvc-debuggers/60279 https://reviews.llvm.org/D119910

And I presume was/is used with another downstream debugger.

This inserts calls to a __CheckForDebuggerJustMyCode function at the start of every compiled function. There is a default implementation provided to prevent a linker error if the C runtime (I assume) doesn't have one. The default implementation is just:

000000000000004c <__CheckForDebuggerJustMyCode>:
      4c: d65f03c0      ret

The signature for this function is:

void __CheckForDebuggerJustMyCode(const char* flag);

There is a global variable inserted that contains the value 1 by default. 1 meaning that it is your code. Code will pass the address of that variable to that function. This means (I think) that a debugger could change that global byte to a 0 to mark a file as not your code at runtime.

The only real implementation I've seen is from MSVC. The basic idea is:

if the flag is 1
    then go to some location that the debugger has placed a breakpoint on
else return

I am not sure if the break is placed exactly there, or how it is placed. You could arrange for a global symbol that the debugger could break on. This symbol being present would also tell the debugger if jmc is present in the process.

The idea is that when you shouldn't stop, the code just runs through the function and returns. When you should, it will end up at the break location and you go into the debugger.

After you've hit the break location you step out once, into the user function, then show the user that you've stopped. So that they never see this intermediate function in the callstack.

What I am unsure about is does this feature only work/work best when all code is compiled with jwc?

Microsoft's debugger has configuration files for what paths you want to ignore. A change to those files does not require a rebuild, this is why I think it is modifying these global vars each time that config updates.

https://learn.microsoft.com/en-us/visualstudio/debugger/just-my-code?view=vs-2022#BKMK_CPP_Customize_call_stack_behavior

If you decide at runtime that some of the jmc compatible code isn't yours, set those global vars to 0 and the jmc function returns without breaking. Which suggests that you can only add to "your code" if the code in question is compiled with jmc. If the code is not jmc compatible it can never be added, unless you ignore jmc completely and fall back to filtering the steps normally.

Which seems to be what the pre-jmc version of the feature did in Microsoft's debugger. (GDB can also do this https://sourceware.org/gdb/download/onlinedocs/gdb/Skipping-Over-Functions-and-Files.html)

Which has the disadvantage of going into the debugger every time, which is what the jmc function aims to avoid. For someone who is almost never debugging system code though, they won't hit that fallback.

Based on that conjecture the work would be:

llvmbot commented 1 year ago

@llvm/issue-subscribers-lldb

pogo59 commented 1 year ago

@yuanfang-chen worked on this, perhaps he can help answer some of the questions.

jimingham commented 1 year ago

It sounds to me like there are two kinds of problems this is trying to solve. One is "I have various sorts of function dispatchers - std::function dispatch, objc_msgSend dispatch, swift async dispatch, etc" that I want "step in" to step in through. The broader one is "I want debugging to always stop in some subset of my functions, but I want to automate which of those functions that is".

lldb's approach to the first problem has been to detect "trampoline functions" and figure out how to predict their results and go to the "interesting code". That's what the thread plans returned by ShouldStopHere are for. For instance, we should be able to figure out how all the C++ function dispatches work and just automatically take you to the actual function. We shouldn't need any user-generated markup for that. We do the same thing for objc_msgSend, async dispatch in swift, etc.

I'm less impressed by the "we got to the target of an STL dispatch" part of that demo because the debugger really should be able to recognize all the standard STL function dispatches, figure out where they are going and go there.

For hand-crafted dispatchers that lldb can't be expected to know about, the "lldb" solution would be to make an affordance for library authors to add to the ShouldStopHere detection mechanism, which would use the knowledge of their dispatch machinery to predict the target, and then we can run to there.

I like this approach since it targets the choke point - the dispatcher - rather than trying to mark all of user code.

Breaking everywhere in user code is easy, we don't need anything fancy to do that. If specifying "which user code functions we really want to stop in" involves some kind of config files, it would be just as easy to consult those in the "set breakpoints almost everywhere in user code" action. Setting a lot of breakpoints isn't super expensive, the expensive part is hitting them, which both solutions will involve. lldb already has a way to implement custom logic for breakpoint setting, so we wouldn't even have to do this in lldb proper.

I think I'm sounding more negative that I mean to. I do think that it should be the debugger's job to grok the common trampolines on the system, and we want to do that regardless of this markup. And doing that is lots more efficient that having every interesting user function on all threads hit breakpoints and continue while we're just trying to get from a trampoline to its target. So I don't think I would implement the feature this way, but OTOH I don't think it would be particularly hard to adopt, so if people are excited that, go for it...

Jim

On Mar 3, 2023, at 4:20 AM, David Spickett @.***> wrote:

This issue is to gather information for potentially supporting the "Just My Code" feature in lldb.

This is a Microsoft Debugger feature announced here: https://devblogs.microsoft.com/cppblog/announcing-jmc-stepping-in-visual-studio/

The clang code generation for it was implemented for ELF in: https://discourse.llvm.org/t/rfc-just-my-code-stepping-for-non-msvc-debuggers/60279 https://reviews.llvm.org/D119910

And I presume was/is used with another downstream debugger.

This inserts calls to a __CheckForDebuggerJustMyCode function at the start of every compiled function. There is a default implementation provided to prevent a linker error if the C runtime (I assume) doesn't have one. The default implementation is just:

000000000000004c <__CheckForDebuggerJustMyCode>: 4c: d65f03c0 ret The signature for this function is:

void __CheckForDebuggerJustMyCode(const char* flag); There is a global variable inserted that contains the value 1 by default. 1 meaning that it is your code. Code will pass the address of that variable to that function. This means (I think) that a debugger could change that global byte to a 0 to mark a file as not your code at runtime.

The only real implementation I've seen is from MSVC. The basic idea is:

if the flag is 1 then go to some location that the debugger has placed a breakpoint on else return I am not sure if the break is placed exactly there, or how it is placed. You could arrange for a global symbol that the debugger could break on. This symbol being present would also tell the debugger if jmc is present in the process.

The idea is that when you shouldn't stop, the code just runs through the function and returns. When you should, it will end up at the break location and you go into the debugger.

After you've hit the break location you step out once, into the user function, then show the user that you've stopped. So that they never see this intermediate function in the callstack.

What I am unsure about is does this feature only work/work best when all code is compiled with jwc?

Microsoft's debugger has configuration files for what paths you want to ignore. A change to those files does not require a rebuild, this is why I think it is modifying these global vars each time that config updates.

https://learn.microsoft.com/en-us/visualstudio/debugger/just-my-code?view=vs-2022#BKMK_CPP_Customize_call_stack_behavior

If you decide at runtime that some of the jmc compatible code isn't yours, set those global vars to 0 and the jmc function returns without breaking. Which suggests that you can only add to "your code" if the code in question is compiled with jmc. If the code is not jmc compatible it can never be added, unless you ignore jmc completely and fall back to filtering the steps normally.

Which seems to be what the pre-jmc version of the feature did in Microsoft's debugger. (GDB can also do this https://sourceware.org/gdb/download/onlinedocs/gdb/Skipping-Over-Functions-and-Files.html)

Which has the disadvantage of going into the debugger every time, which is what the jmc function aims to avoid For someone who is almost never debugging system code though, they won't hit that fallback.

Based on that conjecture the work would be:

An implementation of CheckForDebuggerJustMyCode somewhere (compiler-rt?). Containing some unique symbol for the debugger to find. Some flag to link that in with the compiled code and/or documentation to do that manually. Teaching lldb how to detect the presence of jmc and find the place to break in CheckForDebuggerJustMyCode. Adding a setting to globally ignore jmc. Adding the logic to do step ins using the jmc break. Adding setting(s) to remove from "your code". Adding a fallback to existing filtering if the code you want to add is not jmc compatible. (not sure that we have those filters either) — Reply to this email directly, view it on GitHub https://github.com/llvm/llvm-project/issues/61152, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADUPVW4XC3WZOJWDNWUNBU3W2HOYFANCNFSM6AAAAAAVORMEZA. You are receiving this because you are subscribed to this thread.

yuanfang-chen commented 1 year ago

Hi @DavidSpickett,

Thanks for looking into JMC support in LLDB. Indeed, the ELF port of the JMC instrumentation is to support a downstream debugger. I'm actually not part of the debugger side of the work and I'm not a debugger expert but here is my understanding.

First I want to mention that before JMC came into existence, MSVC's initial tackle on the problem is to annotate well-known STL libraries (https://devblogs.microsoft.com/cppblog/announcing-jmc-stepping-in-visual-studio/) which seems to be a workflow burden to some developers so later they developed JMC as a universal/general solution.

The MSVC implementation of __CheckForDebuggerJustMyCode is https://github.com/ojdkbuild/tools_toolchain_vs2017bt_1416/blob/master/VC/Tools/MSVC/14.16.27023/crt/src/vcruntime/debugger_jmc.c

The debugger would, at startup or before launching the debugged program, set per-function boolean flags according to user configuration files, then before each step in, set one breakpoint at the nop instruction and clear it after. The reason to do this is the preassumption that the debugger is not aware of the dispatch functions and dispatch targets.  The breakpoint on the nop instruction could be used as the global knob to turn JMC on/off.

As you could see, the breakpoint is predicated on the per-function flag and the current thread number, so there should be no unnecessary stop at that breakpoint. And JMC-style step-in is simply setting that breakpoint and doing a continue; instead of step in not my code function and then step through.

@jimingham, thanks a lot for shedding light on the LLDB's capabilities in this regard. It is definitely worthwhile to consider an alternative solution that skips compiler instrumentation. The solution sounds good to me except that it requires work from the user to teach LLDB the no-so-well-known/user dispatchers which contradicts the reason JMC was developed which is to reduce user intervention.

That being said, my personal preference is to first implement JMC in LLDB similar to the MSVC solution (which I believe requires less LLDB change) and gradually improve upon that with LLDB-specific capabilities.

DavidSpickett commented 1 year ago

I do think that it should be the debugger's job to grok the common trampolines on the system, and we want to do that regardless of this markup.

Agreed.

So I don't think I would implement the feature this way, but OTOH I don't think it would be particularly hard to adopt, so if people are excited that, go for it...

My interest is mostly because: A: A real human asked me about it. B: It has users from other debuggers/toolchains.

So I want to see how it works at least. I was also surprised at how it is implemented and don't appreciate the trade-offs yet.

The debugger would, at startup or before launching the debugged program, set per-function boolean flags according to user configuration files, then before each step in, set one breakpoint at the nop instruction and clear it after.

I was on the right track then. Thanks for explaining!

That being said, my personal preference is to first implement JMC in LLDB similar to the MSVC solution (which I believe requires less LLDB change) and gradually improve upon that with LLDB-specific capabilities.

JMC and what Jim is talking about is in the same areas, lldb source wise. So I am leaning toward the known solution first but can always change gears with what I've learned so far.