Repository: pwndbg/pwndbg

Issue:
The `ctx threads` (or `threads`) should display all threads no matter of context threads limit
cc: @CptGibbon we should probably add this option for convenience :)


Discussion:
Sure I'll look into it 👍
@disconnect3d do you want `context threads` to behave differently to the other context subcommands?
I'm not sure. Maybe we should have a `threads` command similarly to `stack` (alias for `telescope`) or `regs`?

Basically we need a way to display threads nicely, huh

Resolving patch:
diff --git a/pwndbg/commands/tls.py b/pwndbg/commands/tls.py
--- a/pwndbg/commands/tls.py
+++ b/pwndbg/commands/tls.py
@@ -5,6 +5,10 @@
 
 import argparse
 
+import gdb
+from tabulate import tabulate
+
+import pwndbg.color.memory as M
 import pwndbg.commands
 import pwndbg.gdblib.tls
 from pwndbg.color import message
@@ -46,3 +50,97 @@ def tls(pthread_self=False) -> None:
                 "(This might cause problems if the pthread_self() is not in libc or not initialized yet.)"
             )
         )
+
+
+parser = argparse.ArgumentParser(
+    formatter_class=argparse.RawTextHelpFormatter,
+    description="List all threads belonging to the selected inferior.",
+)
+group = parser.add_mutually_exclusive_group()
+
+group.add_argument(
+    "num_threads",
+    type=int,
+    nargs="?",
+    default=None,
+    help="Number of threads to display. Omit to display all threads.",
+)
+
+group.add_argument(
+    "-c",
+    "--config",
+    action="store_true",
+    dest="respect_config",
+    help="Respect context-max-threads config to limit number of threads displayed.",
+)
+
+
+@pwndbg.commands.ArgparsedCommand(parser, category=CommandCategory.LINUX)
+@pwndbg.commands.OnlyWhenRunning
+@pwndbg.commands.OnlyWhenUserspace
+def threads(num_threads, respect_config) -> None:
+    table = []
+    headers = ["global_num", "name", "status", "pc", "symbol"]
+    bold_green = lambda text: pwndbg.color.bold(pwndbg.color.green(text))
+
+    try:
+        original_thread = gdb.selected_thread()
+    except SystemError:
+        original_thread = None
+
+    all_threads = gdb.selected_inferior().threads()[::-1]
+
+    displayed_threads = []
+
+    if original_thread is not None and original_thread.is_valid():
+        displayed_threads.append(original_thread)
+
+    for thread in all_threads:
+        if respect_config and len(displayed_threads) >= int(
+            pwndbg.commands.context.config_max_threads_display
+        ):
+            break
+        elif num_threads is not None and len(displayed_threads) >= num_threads:
+            break
+
+        if thread.is_valid() and thread is not original_thread:
+            displayed_threads.append(thread)
+
+    for thread in displayed_threads:
+        name = thread.name or ""
+
+        if thread is original_thread:
+            row = [
+                bold_green(thread.global_num),
+                bold_green(name),
+            ]
+        else:
+            row = [
+                thread.global_num,
+                name,
+            ]
+
+        row.append(pwndbg.commands.context.get_thread_status(thread))
+
+        if thread.is_stopped():
+            thread.switch()
+            pc = gdb.selected_frame().pc()
+
+            pc_colored = M.get(pc)
+            symbol = pwndbg.gdblib.symbol.get(pc)
+
+            row.append(pc_colored)
+
+            if symbol:
+                if thread is original_thread:
+                    row.append(bold_green(symbol))
+                else:
+                    row.append(symbol)
+
+        table.append(row)
+
+    if original_thread is not None and original_thread.is_valid():
+        original_thread.switch()
+
+    print(tabulate(table, headers))
+    print(f"\nShowing {len(displayed_threads)} of {len(all_threads)} threads.")


Test patch: