ccoutant / dwarf-locations

2 stars 6 forks source link

DW_OP_entry_value deprecation #1

Open woodard opened 1 year ago

woodard commented 1 year ago

DW_OP_entry_value is a DWARF expression operator. However DW_TAGcall* may specify a DWARF expression which can specify the location of value of a parameter when called from a particular location. I can see how the evaluation of DW_OP_entry_value can refer back to an expression for the parameter in the call site but I do not see how, you can deprecate an a DWARF expression operator with a tag.

Is the idea to replace DW_OP_entry_value with an some sort of indirect DWARF expression call that refers to the value at a particular call site. I can see implementing DW_OP_entry_value by making reference to a call site where the value or location is stored rather than setting a breakpoint on function entry or virtually unwinding the stack. However, since different call sites could have different calling conventions for non-standard (static) calls, I don't see how such an indirection could be implemented in a DWARF expression itself.

Can you explain how you would rewrite the DWARF expressions that currently make use of DW_OP_entry_value it it were removed?

I can also see how if location lists were using the overlay operator you could unwind the overlays to the PC for the function entry rather than virtually unwinding the stack. But the overlay operator hasn't been introduced yet.

t-tye commented 1 year ago

Could you give an example of how DW_OP_entry_value would be used for a non-trivial expression? Gdb only implements the operation if the expression is exactly DW_OP_reg* or DW_OP_breg*; DW_OP_deref*. If the purpose is to get parameter values on entry to the subprogram then the call site attributes could be used. If the value of a register on entry to the subprogram is required then the proposed DW_OP_call_frame_entry_reg (not part of this proposal) can be used.

It seems impractical to require a consumer to halt every subprogram entry and capture all DW_OP_entry_value expressions which is what seems to be necessary for many non-trivial expressions. Consider expressions that read memory that may get clobbered by the subprogrm execution. Using CFI only seems to work if the expression only refers to registers, hence the proposal of DW_OP_call_frame_entry_reg to cover this case.

woodard commented 1 year ago

I recognize that the cost to stop a GPU and transfer all those memory locations would be vastly higher on a GPU than it is on a CPU and I remember John DelSignore not liking having insert a breakpoint to copy down the values even in the CPU case so I understand the impracticality concern.

palves commented 1 year ago

You would basically have to make the debugger stop at entry to all functions, as you never know which function will a thread stop at, given SIGSEGV, SIGINT, etc.

Also, it wouldn't work with core files.

woodard commented 1 year ago

You would basically have to make the debugger stop at entry to all functions, as you never know which function will a thread stop at, given SIGSEGV, SIGINT, etc.

Also, it wouldn't work with core files.

I think it is kind of best effor isn't it. I'm going to admit that I haven't dug into the contents of complex location lists but from what I understand from Jakub is that they are heavily dependent on these entry value expressions and in spite of their limitations, they do seem to work well in practice providing a much broader range of coverage for variables than would be possible if they were not there.

t-tye commented 1 year ago

I don't think it is helpful to have features in the specification that a consumer does not full implement. If a producer heavily used a feature, and the consumer did not support it, then things no longer work. The benefit of a standard is that it mandates what produces and consumers must support.

Looking a gdb, it seems it only supports one case of DW_OP_entry_value involving registers. So it seems maybe that that is the only case that is really needed. So the suggestion is to define an operation that would support registers that all consumers can support using CFI.

Do you know of other cases besides registers in which DW_OP_entry_value is used? If so what debugger supports it since gdb seems limited to only registers?

woodard commented 1 year ago

Jakub was kind enough to provide me an example:

Consider -O2 -gdwarf-5

volatile int v;
__attribute__((noipa)) void baz (int x)
{
}
__attribute__((noipa)) void foo (int x)
{
  int y = 2 * x;
  baz (5);
  ++v;
}
__attribute__((noipa)) void bar (int x)
{
  foo (1);
  foo (2);
  foo (3);
  foo (4 + x);
}
int
main ()
{
  bar (0);
}

and put breakpoint on the ++v; line. Only DW_OP_entry_value can ensure you can see values of x and y correctly there.

When you look at the call site parameters they are different:

 <3><b9>: Abbrev Number: 1 (DW_TAG_call_site_parameter)
    <ba>   DW_AT_location    : 1 byte block: 55         (DW_OP_reg5 (rdi))
    <bc>   DW_AT_call_value  : 1 byte block: 31         (DW_OP_lit1)
in the second by
 <3><d0>: Abbrev Number: 1 (DW_TAG_call_site_parameter)
    <d1>   DW_AT_location    : 1 byte block: 55         (DW_OP_reg5 (rdi))
    <d3>   DW_AT_call_value  : 1 byte block: 32         (DW_OP_lit2)
in the third by
 <3><e7>: Abbrev Number: 1 (DW_TAG_call_site_parameter)
    <e8>   DW_AT_location    : 1 byte block: 55         (DW_OP_reg5 (rdi))
    <ea>   DW_AT_call_value  : 1 byte block: 33         (DW_OP_lit3)
and in the fourth by
 <3><fa>: Abbrev Number: 1 (DW_TAG_call_site_parameter)
    <fb>   DW_AT_location    : 1 byte block: 55         (DW_OP_reg5 (rdi))
    <fd>   DW_AT_call_value  : 5 byte block: a3 1 55 23 4       (DW_OP_entry_value: (DW_OP_reg5 (rdi)); DW_OP_plus_uconst: 4)

The real crux of it is that in the last call by bar, its parameter from its calling scope needs to be referenced to be able to find out what foo's x is. It is not just hoisting it from the calling frame it is a dynamic property.

if you look at real-world code, it is very common that not all arguments are used up to the last instruction in each function and when it isn't needed, there is no point when optimizing to keep it in some register or memory. Yet when debugging, being able to determine those values is often essential.