├── .gitignore ├── LICENSE ├── README.md └── criteria.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dave Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lldb-helpers 2 | 3 | A collection of functions that control when a breakpoint stops. These are especially useful for frequently called functions. The functions are also useful to catch programming mistakes, such as functions being called from the wrong thread. 4 | 5 | Inspired by [GDB's convience functions](https://sourceware.org/gdb/current/onlinedocs/gdb/Convenience-Funs.html). 6 | 7 | ## Breakpoint Criteria 8 | 9 | These helper functions place various criteria on a breakpoint. A simple example is to make a breakpoint stop only when called by a certain function. 10 | 11 | The types of criteria are: 12 | 13 | 1. [Caller](#caller) 14 | 2. [Stack](#stack) 15 | 3. [Threads](#threads) 16 | 17 | ### Caller 18 | 19 | The `caller_is` and `caller_contains` helpers are used to make a breakpoint stop only when called by a specific function. Or, to stop when the caller is _not_ a specific function. Use `caller_is` for an exact caller name, and use `caller_contains` to stop based on substring. Prefix the function with `not` to stop when the caller _does not_ match the name. 20 | 21 | Examples: 22 | 23 | ``` 24 | (lldb) breakpoint command add -F 'caller_is("someFunction")' 25 | (lldb) breakpoint command add -F 'not caller_is("someFunction")' 26 | (lldb) breakpoint command add -F 'caller_is("-[SomeClass theMethod:]")' 27 | (lldb) breakpoint command add -F 'caller_contains("SomeClass")' 28 | (lldb) breakpoint command add -F 'caller_matches("OneClass|someFunction")' 29 | (lldb) breakpoint command add -F 'not caller_matches("OneClass|someFunction")' 30 | ``` 31 | 32 | In some cases, you'll want a breakpoint to stop based on the library (module) of the caller. The `caller_from` helper does just this. 33 | 34 | Examples: 35 | 36 | ``` 37 | (lldb) breakpoint command add -F 'caller_from("UIKit")' 38 | (lldb) breakpoint command add -F 'not caller_from("MyAppName")' 39 | ``` 40 | 41 | ### Stack 42 | 43 | The `any_caller_is` and `any_caller_contains` helper functions are just like `caller_is` and `caller_contains`, except they stop if any function in the call stack matches. Use `any_caller_is` to require an exact match with one of the functions in the call stack, and use `any_caller_contains` to stop based on substring. Prefix the function with `not` to stop when the call stack _does not_ contain a matching function. 44 | 45 | Examples: 46 | 47 | ``` 48 | (lldb) breakpoint command add -F 'any_caller_is("someFunction")' 49 | (lldb) breakpoint command add -F 'not any_caller_is("someFunction")' 50 | (lldb) breakpoint command add -F 'any_caller_is("-[SomeClass theMethod:]")' 51 | (lldb) breakpoint command add -F 'any_caller_contains("SomeClass")' 52 | (lldb) breakpoint command add -F 'any_caller_matches("OneClass|someFunction")' 53 | (lldb) breakpoint command add -F 'not any_caller_matches("OneClass|someFunction")' 54 | ``` 55 | 56 | When you want a breakpoint to stop when library (module) is or is not in the call stack, use `any_caller_from`. 57 | 58 | Examples: 59 | 60 | ``` 61 | (lldb) breakpoint command add -F 'any_caller_from("UIKit")' 62 | (lldb) breakpoint command add -F 'not any_caller_from("UIKit")' 63 | ``` 64 | 65 | ### Threads 66 | 67 | The `called_on` helper function is used to stop only when a breakpoint is hit from a specific thread or queue. LLDB breakpoints have the ability to specify a specifc thread (`--thread-index`) or queue (`--queue-name`), but there is no way to specify that a breakpoint *not* stop for a specific thread or queue. The `called_on` helper can do this, and takes either a thread index, for example `1` for the main thread, or takes a thread name or queue name. 68 | 69 | Examples: 70 | 71 | ``` 72 | (lldb) breakpoint command add -F 'not called_on(1)' 73 | (lldb) breakpoint command add -F 'called_on("com.banana.eventfetch-thread")' 74 | ``` 75 | 76 | ## Installation 77 | 78 | 1. Clone this repository to your prefrerred location 79 | 2. Add this `import` command to your `~/.lldbinit`: 80 | 81 | ``` 82 | command script import ~/path/to/lldb-helpers/criteria.py 83 | ``` 84 | -------------------------------------------------------------------------------- /criteria.py: -------------------------------------------------------------------------------- 1 | try: 2 | import __builtin__ 3 | except ImportError: 4 | import builtins 5 | __builtin__ = builtins 6 | import re 7 | from itertools import islice 8 | 9 | # These functions are for adding breakpoint criteria. They are useful for 10 | # constraining when a breakpoint stops, for example only stopping when called 11 | # by this function or from that library. 12 | # 13 | # These helper functions make use of a subtle behavior of breakpoint commands. 14 | # A breakpoint function that returns False tells lldb to continue running the 15 | # process. Think of False as "No, don't stop". 16 | # 17 | # These functions were inspired by: 18 | # https://sourceware.org/gdb/current/onlinedocs/gdb/Convenience-Funs.html 19 | 20 | def break_criteria(predicate): 21 | def decorator(*args): 22 | def breakpoint_command(frame, location, _): 23 | return predicate(frame, *args) 24 | return breakpoint_command 25 | setattr(__builtin__, predicate.__name__, decorator) 26 | return decorator 27 | 28 | # breakpoint command add -F 'caller_is("theCaller")' 29 | # breakpoint command add -F 'not caller_is("theCaller")' 30 | @break_criteria 31 | def caller_is(frame, symbol): 32 | return frame.parent.name == symbol 33 | 34 | # breakpoint command add -F 'any_caller_is("someCaller")' 35 | # breakpoint command add -F 'not any_caller_is("someCaller")' 36 | @break_criteria 37 | def any_caller_is(frame, symbol): 38 | callers = islice(frame.thread, 1, None) # skip current frame 39 | return any(f.name == symbol for f in callers) 40 | 41 | # breakpoint command add -F 'caller_contains("theCaller")' 42 | # breakpoint command add -F 'not caller_contains("theCaller")' 43 | @break_criteria 44 | def caller_contains(frame, substring): 45 | return substring in frame.parent.name 46 | 47 | # breakpoint command add -F 'any_caller_contains("someCaller")' 48 | # breakpoint command add -F 'not any_caller_contains("someCaller")' 49 | @break_criteria 50 | def any_caller_contains(frame, substring): 51 | callers = islice(frame.thread, 1, None) # skip current frame 52 | return any(substring in f.name for f in callers) 53 | 54 | _CACHED_REGEX = {} 55 | def _get_regex(pattern): 56 | global _CACHED_REGEX 57 | regex = _CACHED_REGEX.get(pattern) 58 | if not regex: 59 | regex = re.compile(pattern) 60 | _CACHED_REGEX[pattern] = regex 61 | return regex 62 | 63 | # breakpoint command add -F 'caller_matches("thisCaller|thatCaller")' 64 | # breakpoint command add -F 'not caller_matches("thisCaller|thatCaller")' 65 | @break_criteria 66 | def caller_matches(frame, pattern): 67 | regex = _get_regex(pattern) 68 | return regex.search(frame.parent.name) is not None 69 | 70 | # breakpoint command add -F 'any_caller_matches("oneCaller|anotherCaller")' 71 | # breakpoint command add -F 'not any_caller_matches("oneCaller|anotherCaller")' 72 | @break_criteria 73 | def any_caller_matches(frame, pattern): 74 | regex = _get_regex(pattern) 75 | callers = islice(frame.thread, 1, None) # skip current frame 76 | return any(regex.search(f.name) for f in callers) 77 | 78 | # breakpoint command add -F 'caller_from("FrameworkKit")' 79 | # breakpoint command add -F 'not caller_from("libThat")' 80 | @break_criteria 81 | def caller_from(frame, module): 82 | return frame.parent.module.file.basename == module 83 | 84 | # breakpoint command add -F 'any_caller_from("FrameworkKit")' 85 | # breakpoint command add -F 'not any_caller_from("libThat")' 86 | @break_criteria 87 | def any_caller_from(frame, module): 88 | callers = islice(frame.thread, 1, None) # skip current frame 89 | return any(f.module.file.basename == module for f in callers) 90 | 91 | # breakpoint command add -F 'not called_on(1)' 92 | # breakpoint command add -F 'called_on("namedThread")' 93 | @break_criteria 94 | def called_on(frame, thread_id): 95 | if isinstance(thread_id, int): 96 | return thread_id == frame.thread.idx 97 | else: 98 | return thread_id in (frame.thread.name, frame.thread.queue) 99 | --------------------------------------------------------------------------------