[clang] [llvm] [clang][DebugInfo] Add virtual call-site target information in DWARF. (PR #167666)

David Blaikie via llvm-commits llvm-commits at lists.llvm.org
Fri Dec 5 09:07:58 PST 2025


dwblaikie wrote:

> Ah there's a detail about our debugger that turns out I was a bit out of date on. At load time the debugger makes a note of function symbols with the same address as ICF functions. Stopped in a function, the debugger knows whether the function has been involved in ICF without looking at DWARF. But it does need the DWARF call site information to determine whether the current function was the original callee (ICF "survivor") or not (ICF folded-away). _That_ is how call site information is used in relation to ICF in our debugger.
> 
> > Rather than trying to detect tail calls - can't you rely the call_site to tell you if it's a tail call?
> 
> Yes and no. The call site info in the parent frame describes the call to the function that does the tail call, not the tail-called function:
> 
> ```
> Call graph:
>   a() --direct-call-> b() --tail-call-> c()
> 
> Stopped in c() our call stack is:
>   c()
>   a() - Call site: DW_AT_call_origin('b')
> ```
> 
> If we ignore analyising the call graph for a moment, there's no way to get at the call site info of the call to `c()` in `b()` to see the tail call attribute. But yes, if we analyse the call graph we can in some cases work it out, though not all (e.g. ambiguous paths).
> 
> > until you have a graph from caller to callee following only tail call edges and check that the callee appears nowhere else in that graph
> 
> It turns out that our debugger does actually do this kind of call graph analysis to reconstruct tail-called frames.
> 
> Adding virtual_call_origin lets the debugger determine whether there has been a tail call for virtual calls. It's not completely watertight, because it fails in some cases such as:
> 
> ```
> Call graph:
>    a() --virtual-call-> b() --tail-call-> Base::b()
> 
> Call stack:
>   Base::b()
>   a() - Call site: DW_AT_virtual_call_origin('b')
> ```
> 
> Here the debugger is going to assume `Base::b` was the intended call target, and report no tail call frames. Our debugger folks are happy with the trade off for the coverage gained.

Would `DW_AT_call_target` in `a()` tell you the real function being called (the address of `SomeDerived::b` - might have to jump through some thunks to get there) - and thus cover this case? (& others, without the need for `DW_AT_virtual_call_origin`)

> Both ICF and entry value reconstruction are relevant here but I think maybe we can think of them separetely. The parent-frame-call-site-info to current-frame mismatch can occur due to ICF or tail calls. But the debugger already knows whether the current frame has been involved in ICF (see top of this comment), and I think all of this comes down to trying to work out if there's been a tail call.
> 
> > > I wonder, does this only help you detect ICF specifically on the first step into a call? I can't picture how this would work if you paused execution in the middle of the code-folded function.
> > > because you could differentiate between the start of the function you're in and the caller's call_site's call_target
> 
> Oh right, ok that makes sense. So it works the same way and runs into the same issues with tail calls as direct/static calls do. Right I think I'm grasping the trade off here (ignoring performance/code size). The NOP-sled implementation tells you which function in the ICF folded group was originally called, but doesn't work if there's a tail call to the current frame. The current approach using the sybmol table indicates there's been ICF but not which function was orignally called, unless the callee is the the IFC "survivor". That logic is independant from the current call stack and DWARF, so doesn't get confused by tail calls. Going further, maybe these are these composable?
> 
> I've sent us round in circle a bit here, sorry about that.
> 
> To sumarise:
> 
> * The debugger wants to understand whether there's been a tail call to the current function.
> * It can construct a tail-call-path between the parent frame and current frame today unless there's any indirect calls.

Why can't it reconstruct this in the face of indirect calls - if those calls include `DW_AT_call_target` that resolves to the address of the 

> * We're proposing adding DW_AT_virtual_call_origin so the tail call graph can be constructed (imperfectly) in the presence of virtual calls, which is probably going to be a significant portion of indirect calls in a C++ codebase.

Perhaps this is the place we should focus - concrete examples of what is/isn't possible help me feel a bit more grounded in the conversation - sorry for any other tangents/misleading statements, etc.

So this is the `a() --virtual-call-> b() --tail-call-> Base::b()` example above? in this case, if `a` is non-tail-calling `SomeDerived::b`, the `DW_AT_call_target` should have `SomeDerived::b` in it - so you could use that to determine that `Base::b` wasn't the real target, and so there's been a tail call here somewhere (& with the call graph analysis, if `SomeDerived::b` only has one tail call chain that leads to `Base::b`, then you can reconstruct the call stack correctly - and if there's more than one such chain or possible chain (tail dynamic call sites, etc) then you bail out and say there could be missing frames here)

> The NOP-sleds may be composable with rather than a replacement for what we're doing. However, either way, I think we'd need to understand the size/performance costs to be able to make a call on their use, and don't think we're able to tackle that investigation right away.

Understandable - though I'm not sure I've quite understood the composability/orthogonality yet - but that's on me & probably best set aside for now - I'll take your word for it.



https://github.com/llvm/llvm-project/pull/167666


More information about the llvm-commits mailing list