Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .lldbinit
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Load the main LLDB initialization script that dynamically loads custom commands
command script import ~/lldb/lldbinit.py
command script import ~/.lldb/lldb/lldbinit.py

# Global setting to ensure LLDB doesn't skip the prologue of functions during debugging
settings set target.skip-prologue false
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ This project enhances the LLDB debugging experience with custom Python commands,
| **`pdefaults`** | Dumps `NSUserDefaults` contents as a formatted key-value table. | `[-s suite] [-f filter] [-o]` |
| **`hex_dump`** | Reads memory at a specific address and returns a formatted hex dump. | `<address> [-c count] [-o offset]` |
| **`print_frame`** | Displays a detailed low-level architectural snapshot and resolved symbolic context of the current stack frame. | None |
| **`list_threads`** | Iterates through all process threads and displays their status, name, and queue info. | None |

### Useful Aliases & Regex Commands

Expand Down
46 changes: 0 additions & 46 deletions bootstrap

This file was deleted.

18 changes: 10 additions & 8 deletions bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,23 @@ git pull origin main

# Function to update the home directory with the dotfiles
update() {
mkdir -p ~/.lldb

rsync -avh --no-perms ./.lldbinit ~

# rsync parameters:
# -a: archive mode (preserves permissions, symlinks, etc.)
# -v: verbose output
# -h: human-readable numbers
# --no-perms: do not preserve permissions (useful for cross-OS sync)
# --exclude: skip files that should not be in the home directory
rsync --exclude ".git/" \
--exclude ".DS_Store" \
--exclude "bootstrap.sh" \
--exclude "README.md" \
--exclude "LICENSE" \
-avh --no-perms . ~
rsync -avh --no-perms \
--exclude=".DS_Store" \
--exclude="__pycache__/" \
--exclude="*.pyc" \
./lldb ~/.lldb/

# Reload the bash profile to apply environment changes immediately
source ~/.bash_profile
echo "✅ LLDB scripts updated successfully in ~/.lldb/"
}

# Check if the --force or -f flag is provided to skip the confirmation prompt
Expand Down
91 changes: 90 additions & 1 deletion lldb/commands/DTInspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def commands():
Returns a list of custom LLDB command instances defined in this module.
The registration logic in lldbinit.py calls this function.
"""
return [DTHexDumpCommand(), DTPrintFrameCommand()]
return [DTHexDumpCommand(), DTPrintFrameCommand(), DTListThreadsCommand()]


class DTHexDumpCommand(bc.BaseCommand):
Expand Down Expand Up @@ -272,3 +272,92 @@ def run(self, args, options):
print("=" * 80 + "\n")

return


class DTListThreadsCommand(bc.BaseCommand):
"""LLDB command class to inspect and list process threads."""

def name(self):
"""Returns the command name as it will be used in the LLDB console."""
return "list_threads"

def description(self):
"""Returns a short description of the command's functionality."""
return "Iterates through all process threads and displays their status, name, and queue info."

def run(self, args, options):
"""
The main execution logic for the list_threads command.
"""

target = lldb.debugger.GetSelectedTarget()
process = target.GetProcess()

if not process or not process.IsValid():
print("error: valid active process not found")
return

output = (
f"\n{'ID':<6} {'INDEX':<7} {'NAME':<20} {'STATE':<15} {'QUEUE/PRIORITY'}\n"
)
output += "-" * 65 + "\n"

num_threads = process.GetNumThreads()

for i in range(num_threads):
thread = process.GetThreadAtIndex(i)
if not thread.IsValid():
continue

thread_id = f"0x{thread.GetThreadID():x}"
index = thread.GetIndexID()

name = thread.GetName()

if not name:
name = "<unnamed>"

state_str = get_state_string(thread)

queue_name = thread.GetQueueName()
priority_info = queue_name if queue_name else "N/A"

is_selected = "*" if thread == process.GetSelectedThread() else " "

output += f"{is_selected}{thread_id:<5} {index:<7} {name:<20} {state_str:<15} {priority_info}\n"

print(output)

return


def get_state_string(thread):
"""
Converts thread state enum and stop reason into a human-readable string.
Avoids calling `thread.GetStopDescription()` to prevent thread hangs/deadlocks.
"""
if thread.IsStopped():
reason = thread.GetStopReason()
reason_str = stop_reason_to_string(reason)
return f"Stopped ({reason_str})" if reason_str else "Stopped"
elif thread.IsSuspended():
return "Suspended"
else:
return "Running"


def stop_reason_to_string(reason):
"""
Maps lldb.eStopReason enum to a readable short string.
"""
reasons = {
lldb.eStopReasonBreakpoint: "breakpoint",
lldb.eStopReasonWatchpoint: "watchpoint",
lldb.eStopReasonSignal: "signal",
lldb.eStopReasonException: "exception",
lldb.eStopReasonExec: "exec",
lldb.eStopReasonPlanComplete: "step",
lldb.eStopReasonThreadExiting: "exiting",
lldb.eStopReasonInstrumentation: "instrumentation",
}
return reasons.get(reason, "")