llvm / llvm-project

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

[lldb] Setting conditional breakpoints by location inside templated functions is hard. #97334

Open mvanotti opened 1 month ago

mvanotti commented 1 month ago

In lldb it is hard to set up a conditional breakpoint in a templated function, if the condition depends on the template type.

See the following example:

#include <assert.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>

template <typename T>
T Read(uint8_t data[], size_t n) {
  T result;

  assert(n >= sizeof(T));
  memcpy(&result, &data[0], sizeof(T));

  return result;
}

struct FooStruct {
  uint64_t foo;
  bool bar;
};

int main(void) {
  uint8_t data[0x100] = {0};
  if (getrandom(data, sizeof(data), 0) == -1) {
    perror("getrandom");
    exit(EXIT_FAILURE);
  }

  FooStruct foo = Read<FooStruct>(data, sizeof(data));
  if (foo.bar) {
    fprintf(stderr, "foo.bar is true\n");
  }

  uint64_t u64_result = Read<uint64_t>(data, sizeof(data));
  if (u64_result) {
    fprintf(stderr, "u64_result is: %" PRIu64 "\n", u64_result);
  }

  return 0;
}

let's say we want to set a breakpoint on line 18 to check if result is not zero:

(lldb) breakpoint set --file repro.cc --line 18 --condition "result != 0"
Breakpoint 1: 2 locations.
(lldb) run
Process 131954 launched: '/home/user/test/repro' (x86_64)
Process 131954 stopped
* thread #1, name = 'repro', stop reason = breakpoint 1.1
    frame #0: 0x0000555555555ae3 repro`FooStruct Read<FooStruct>(data="'a\b\U00000016\xd6-\x8bK\xb4Gl\xb9\xf6U\U00000002|yGo\xc9\U00000017\xdd\xd1\xd6\U00000016]\U00000012\xc0\U00000006\xa7\U00000006kU\xb4\x9c\xf7\xf7fC\xcfз\U0000007f刺\xa1\"\U00000014lP\x94\xf2\xc9z\xc9|_M\U0000000f\xd9*b\x9d\xaf\xa1D肐\xae\U0000001e,\xbf\U00000019f\xea\xec\x81\xceP\x9f\U0000007f\xa4\xbbn\xe0\xb9&\xdeT\b>\xfeK\xef\xc6b\U00000015Q\xd9ǵt\xff\xaa\xc8", n=256) at repro.cc:18:3
   15     assert(n >= sizeof(T));
   16     memcpy(&result, &data[0], sizeof(T));
   17  
-> 18     return result;
   19   }
   20  
   21   struct FooStruct {
error: stopped due to an error evaluating condition of breakpoint 1.1: "result != 0"
Couldn't parse conditional expression:
error: <user expression 0>:1:8: invalid operands to binary expression ('FooStruct' and 'int')
    1 | result != 0
      | ~~~~~~ ^  ~

This will throw an error when we evaluate it, which is less than ideal. It is understandable that it fails, but it is a bit hard to specify the correct breakpoint location here.

The issue is a bit worse if using the LLDB Python API: When you specify the breakpoint by location (file + line), you have no way to specify which version of the template you want to use (or which function name?). Instead, you would have to disable all the other breakpoints afterwards.

The workaround I found is to look up all the locations, from there call GetAddress(), then get the function name, and disable the breakpoint if the function name does not match what you expect.

I'm not sure what the right fix would look like here.

llvmbot commented 1 month ago

@llvm/issue-subscribers-lldb

Author: Marco Vanotti (mvanotti)

In lldb it is hard to set up a conditional breakpoint in a templated function, if the condition depends on the template type. See the following example: ```c++ #include <assert.h> #include <inttypes.h> #include <stdbool.h> #include <stddef.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/random.h> template <typename T> T Read(uint8_t data[], size_t n) { T result; assert(n >= sizeof(T)); memcpy(&result, &data[0], sizeof(T)); return result; } struct FooStruct { uint64_t foo; bool bar; }; int main(void) { uint8_t data[0x100] = {0}; if (getrandom(data, sizeof(data), 0) == -1) { perror("getrandom"); exit(EXIT_FAILURE); } FooStruct foo = Read<FooStruct>(data, sizeof(data)); if (foo.bar) { fprintf(stderr, "foo.bar is true\n"); } uint64_t u64_result = Read<uint64_t>(data, sizeof(data)); if (u64_result) { fprintf(stderr, "u64_result is: %" PRIu64 "\n", u64_result); } return 0; } ``` let's say we want to set a breakpoint on line 18 to check if `result` is not zero: ``` (lldb) breakpoint set --file repro.cc --line 18 --condition "result != 0" Breakpoint 1: 2 locations. (lldb) run Process 131954 launched: '/home/user/test/repro' (x86_64) Process 131954 stopped * thread #1, name = 'repro', stop reason = breakpoint 1.1 frame #0: 0x0000555555555ae3 repro`FooStruct Read<FooStruct>(data="'a\b\U00000016\xd6-\x8bK\xb4Gl\xb9\xf6U\U00000002|yGo\xc9\U00000017\xdd\xd1\xd6\U00000016]\U00000012\xc0\U00000006\xa7\U00000006kU\xb4\x9c\xf7\xf7fC\xcfз\U0000007f刺\xa1\"\U00000014lP\x94\xf2\xc9z\xc9|_M\U0000000f\xd9*b\x9d\xaf\xa1D肐\xae\U0000001e,\xbf\U00000019f\xea\xec\x81\xceP\x9f\U0000007f\xa4\xbbn\xe0\xb9&\xdeT\b>\xfeK\xef\xc6b\U00000015Q\xd9ǵt\xff\xaa\xc8", n=256) at repro.cc:18:3 15 assert(n >= sizeof(T)); 16 memcpy(&result, &data[0], sizeof(T)); 17 -> 18 return result; 19 } 20 21 struct FooStruct { error: stopped due to an error evaluating condition of breakpoint 1.1: "result != 0" Couldn't parse conditional expression: error: <user expression 0>:1:8: invalid operands to binary expression ('FooStruct' and 'int') 1 | result != 0 | ~~~~~~ ^ ~ ``` This will throw an error when we evaluate it, which is less than ideal. It is understandable that it fails, but it is a bit hard to specify the correct breakpoint location here. The issue is a bit worse if using the LLDB Python API: When you specify the breakpoint by location (file + line), you have no way to specify which version of the template you want to use (or which function name?). Instead, you would have to disable all the other breakpoints afterwards. The workaround I found is to look up all the locations, from there call GetAddress(), then get the function name, and disable the breakpoint if the function name does not match what you expect. I'm not sure what the right fix would look like here.
Michael137 commented 1 month ago

LLDB will have created two breakpoints here, one for each instantiation. You could set a breakpoint on only one of these from the CLI with br se -n 'Read<int>' -c 'result != 0'. E.g.,:

$ lldb a.out -o "br se -n 'Read<int>' -c 'result != 0'" -o run                                 
(lldb) target create "a.out"                                                                   
(lldb) br se -n 'Read<int>' -c 'result != 0'                                                   
Breakpoint 1: where = a.out`int Read<int>() + 4 at break.cpp:4:10, address = 0x0000000100003f74
(lldb) run                                                                                     
Process 20706 stopped                                                                          
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1                     
    frame #0: 0x0000000100003f74 a.out`int Read<int>() at break.cpp:4:10                       
   1    template <typename T>                                                                  
   2    T Read() {                                                                             
   3      T result;                                                                            
-> 4      return result;                                                                       
   5    }                                                                                      
   6                                                                                           
   7    struct FooStruct {};                                                                   
Target 0: (a.out) stopped.                                                                     

Or as you point out, delete the breakpoints in the instantiations you don't care about. I don't see how LLDB could know not to run the expression in one of the instantiations (there's no way we would know that the expression fails to run until we actually run it). CC @jimingham to confirm whether we'd want/can do anything about this in the SBAPI. The breakpoint resolvers could be of use https://lldb.llvm.org/use/python-reference.html#using-the-python-api-s-to-create-custom-breakpoints perhaps?

jimingham commented 1 month ago

If you add a condition to a breakpoint, lldb should run it, it shouldn't try to figure out whether it might work or not. We have to solve this in setting the breakpoint if you want to be able to add the condition to the breakpoint itself.

Note, BTW, you can add conditions to breakpoint locations as well as breakpoints. So you can make this work for each of the locations by adding the condition appropriate to the location. Just set the breakpoint, then do:

(lldb) break list
Current breakpoints:
1: source regex = "return result", exact_match = 0, locations = 2
  1.1: where = templates`FooStruct Read<FooStruct>(unsigned char*, unsigned long) + 88 at templates.cpp:18:3, address = templates[0x0000000100003e88], unresolved, hit count = 0 
  1.2: where = templates`unsigned long long Read<unsigned long long>(unsigned char*, unsigned long) + 88 at templates.cpp:18:10, address = templates[0x0000000100003ef4], unresolved, hit count = 0 

(lldb) break modify -c "result == 0" 1.2

You can also do this with the SB API (SBBreakpointLocation.SetCondition).

If we want to make file & line breakpoint ALSO filter on function name, we could certainly allow -n as an optional option to break set -f <> -l <>. If you supply a -n as well as a file and line, we would only set the breakpoint for that line if the function name also matches the -n value. So then you could say:

(lldb) break set -l 18 -n Read<int> -c "result == 0"

The file and line would be the primary filter, then we'd run over all those matches again, and throw out the ones that are in functions that don't match the given name.

For the SB API, we'd need to add an overload to SBTarget. BreakpointCreateByLocation that takes an optional function_name string.

jimingham commented 1 month ago

You could fix this as it stands by writing a custom breakpoint resolver that sets the file & line breakpoint, then runs through that breakpoint's locations and adds to itself the ones that match the function name, then deletes the file & line breakpoint.

You could alternately write a little python based command:

(lldb) add_condition --condition "result == 0" --name "Read<int>" <BKPTNO> That went through the breakpoint locations in BKPTNO and added the condition only to the ones whose containing function matched the --name option.

The latter is probably the easiest.