diff --git a/docs/functions/index.md b/docs/functions/index.md new file mode 100644 index 000000000..b48f79c2c --- /dev/null +++ b/docs/functions/index.md @@ -0,0 +1,441 @@ +--- +hide: + - navigation +--- + + + + + +# Functions + + +pwndbg provides a set of functions which can be used during expression evaluation to +quickly perform common calculations. These can even be passed to other commands as arguments. +Currently, they only work in gdb. + +To see a list of all functions, including those built into gdb, use `help function`. To see +the help of any given function use `help function function_name`. Function invokation must +include a preceding $ sign and must include brackets. For instance, invoke the `environ` +function like so: +``` +pwndbg> p $environ("LANG") +$2 = (signed char *) 0x7fffffffe6da "LANG=en_US.UTF-8" +``` +If the result of the function is being passed to a pwndbg command, make sure to either escape +the function argument's quotes, or put the whole function call in quotes. +``` +pwndbg> tele $environ("LANG") +usage: telescope [-h] [-r] [-f] [-i] [address] [count] +telescope: error: argument address: debugger couldn't resolve argument '$environ(LANG)': + No symbol "LANG" in current context. +pwndbg> tele $environ(\"LANG\") +00:0000│ 0x7fffffffe6cf ◂— 'LANG=en_US.UTF-8' +01:0008│ 0x7fffffffe6d7 ◂— 'US.UTF-8' +02:0010│ 0x7fffffffe6df ◂— 0x4e49475542454400 +[...] +pwndbg> tele '$environ("LANG")' +00:0000│ 0x7fffffffe6cf ◂— 'LANG=en_US.UTF-8' +01:0008│ 0x7fffffffe6d7 ◂— 'US.UTF-8' +02:0010│ 0x7fffffffe6df ◂— 0x4e49475542454400 +[...] +``` +## pwndbg functions + +### **rebase** + + +``` {.python .no-copy} +rebase(addr: gdb.Value | int) -> int +``` + + +#### Description +Return address rebased onto the executable's mappings. + +#### Example +``` +pwndbg> p/x $rebase(0xd9020) +$1 = 0x55555562d020 +pwndbg> vmmap +0x555555554000 0x55555556f000 r--p 1b000 0 /usr/bin/bash +0x55555556f000 0x55555562d000 r-xp be000 1b000 /usr/bin/bash +0x55555562d000 0x55555565e000 r--p 31000 d9000 /usr/bin/bash +[...] +pwndbg> p $rebase(0xd9020) == 0x555555554000 + 0xd9020 +$2 = 1 +pwndbg> tele $rebase(0xd9020) +00:0000│ 0x55555562d020 ◂— 0x204900636f6c6c61 /* 'alloc' */ +01:0008│ 0x55555562d028 ◂— 'have no name!' +02:0010│ 0x55555562d030 ◂— 0x65720021656d616e /* 'name!' */ +03:0018│ 0x55555562d038 ◂— 'adline stdin' +[...] +``` + +---------- + +### **base** + + +``` {.python .no-copy} +base(name_pattern: gdb.Value | str) -> int +``` + + +#### Description +Return the base address of the first memory mapping containing the given name. + +#### Example +``` +pwndbg> p/x $base("libc") +$4 = 0x7ffff7d4b000 +pwndbg> vmmap libc + 0x7ffff7d4a000 0x7ffff7d4b000 rw-p 1000 6e000 /usr/lib/libncursesw.so.6.5 +► 0x7ffff7d4b000 0x7ffff7d6f000 r--p 24000 0 /usr/lib/libc.so.6 +► 0x7ffff7d6f000 0x7ffff7ed6000 r-xp 167000 24000 /usr/lib/libc.so.6 +► 0x7ffff7ed6000 0x7ffff7f2b000 r--p 55000 18b000 /usr/lib/libc.so.6 +► 0x7ffff7f2b000 0x7ffff7f2f000 r--p 4000 1e0000 /usr/lib/libc.so.6 +► 0x7ffff7f2f000 0x7ffff7f31000 rw-p 2000 1e4000 /usr/lib/libc.so.6 + 0x7ffff7f31000 0x7ffff7f39000 rw-p 8000 0 [anon_7ffff7f31] +pwndbg> tele $base(\"libc\")+0x1337 +00:0000│ 0x7ffff7d4c337 ◂— 0x80480a04214000f0 +01:0008│ 0x7ffff7d4c33f ◂— 0x8040c02204452040 +02:0010│ 0x7ffff7d4c347 ◂— 0x20042400000200 +03:0018│ 0x7ffff7d4c34f ◂— 0x20 /* ' ' */ +[...] +``` + +Beware of accidentally matching the wrong mapping. For instance, if the loaded +executable contained the string "libc" anywhere in it's path, it would've been +returned. + +---------- + +### **hex2ptr** + + +``` {.python .no-copy} +hex2ptr(hex_string: gdb.Value | str) -> int +``` + + +#### Description +Converts a hex string to a little-endian address and returns the address. + +#### Example +``` +pwndbg> p/x $hex2ptr("20 74 ed f7 ff 7f") +$1 = 0x7ffff7ed7420 +pwndbg> p/x $hex2ptr("2074edf7ff7f") +$2 = 0x7ffff7ed7420 +pwndbg> distance '$base("libc")' '$hex2ptr("20 74 ed f7 ff 7f")' +0x7ffff7d4b000->0x7ffff7ed7420 is 0x18c420 bytes (0x31884 words) +``` + +Especially useful for quickly converting pwntools output. + +---------- + +### **argc** + + +``` {.python .no-copy} +argc() -> int +``` + + +#### Description +Get the number of program arguments. +Evaluates to argc. + +#### Example +``` +pwndbg> p $argc() +$1 = 2 +pwndbg> argv +00:0000│ 0x7fffffffe288 —▸ 0x7fffffffe659 ◂— '/usr/bin/cat' +01:0008│ 0x7fffffffe290 —▸ 0x7fffffffe666 ◂— 'gdbinit.py' +02:0010│ 0x7fffffffe298 ◂— 0 +``` + +---------- + +### **argv** + + +``` {.python .no-copy} +argv(index: gdb.Value) -> gdb.Value +``` + + +#### Description +Get the n-th program argument. +Evaluate argv on the supplied value. + +#### Example +``` +pwndbg> p $argv(0) +$11 = (signed char *) 0x7fffffffe666 "/usr/bin/sh" +pwndbg> argv +00:0000│ 0x7fffffffe2a8 —▸ 0x7fffffffe666 ◂— '/usr/bin/sh' +01:0008│ 0x7fffffffe2b0 ◂— 0 +``` + +---------- + +### **environ** + + +``` {.python .no-copy} +environ(env_name: gdb.Value) -> gdb.Value +``` + + +#### Description +Get an environment variable by name. +Evaluate getenv() on the supplied value. + +#### Example +``` +pwndbg> p $environ("LANG") +$2 = (signed char *) 0x7fffffffebfb "LANG=en_US.UTF-8" +``` + +---------- + +### **envp** + + +``` {.python .no-copy} +envp(index: gdb.Value) -> gdb.Value +``` + + +#### Description +Get the n-th environment variable. +Evaluate envp on the supplied value. + +#### Example +``` +pwndbg> p $envp(0x3F) +$13 = (signed char *) 0x7fffffffef7d "LANG=en_US.UTF-8" +pwndbg> p $envp(0x3F) == $environ("LANG") +$14 = 1 +``` + +---------- + +### **fsbase** + + +``` {.python .no-copy} +fsbase(offset: gdb.Value = gdb.Value(0)) -> int +``` + + +#### Description +Get the value of the FS segment register. +Only valid on x86(-64). + +#### Example +``` +pwndbg> p/x $fsbase() +$3 = 0x7ffff7cdab80 +pwndbg> p $fs_base == $fsbase() +$4 = 1 +pwndbg> x/gx $fsbase(0x28) +0x7ffff7cdaba8: 0x4da926e1668e5a00 +pwndbg> x/gx $fsbase(0x30) +0x7ffff7cdabb0: 0x190a86d93bccf0ad +pwndbg> tls +Thread Local Storage (TLS) base: 0x7ffff7cdab80 +TLS is located at: + 0x7ffff7cda000 0x7ffff7cdc000 rw-p 2000 0 [anon_7ffff7cda] +Dumping the address: +tcbhead_t @ 0x7ffff7cdab80 + 0x00007ffff7cdab80 +0x0000 tcb : 0x7ffff7cdab80 + 0x00007ffff7cdab88 +0x0008 dtv : 0x7ffff7cdb4f0 + 0x00007ffff7cdab90 +0x0010 self : 0x7ffff7cdab80 + 0x00007ffff7cdab98 +0x0018 multiple_threads : 0x0 + 0x00007ffff7cdab9c +0x001c gscope_flag : 0x0 + 0x00007ffff7cdaba0 +0x0020 sysinfo : 0x0 + 0x00007ffff7cdaba8 +0x0028 stack_guard : 0x4da926e1668e5a00 + 0x00007ffff7cdabb0 +0x0030 pointer_guard : 0x190a86d93bccf0ad + [...] +pwndbg> canary +[...] +Canary = 0x4da926e1668e5a00 (may be incorrect on != glibc) +[...] +``` +FS will usually point to the start of the TLS. If you're not providing an +offset, it is usually easier to use gdb's builtin $fs_base variable. + +---------- + +### **gsbase** + + +``` {.python .no-copy} +gsbase(offset: gdb.Value = gdb.Value(0)) -> int +``` + + +#### Description +Get the value of the GS segment register. +Only valid on x86(-64). + +#### Example +``` +pwndbg> p/x $gsbase() +$1 = 0x0 +``` +The value of the GS register is more interesting when doing kernel debugging: +``` +pwndbg> p/x $gsbase() +$1 = 0xffff999287a00000 +pwndbg> tele $gsbase() +00:0000│ 0xffff999287a00000 ◂— 0 +... ↓ 4 skipped +05:0028│ 0xffff999287a00028 ◂— 0xd6aa9b336d52a400 +06:0030│ 0xffff999287a00030 ◂— 0 +07:0038│ 0xffff999287a00038 ◂— 0 +pwndbg> p $gsbase() == $gs_base +$2 = 1 +``` +If you're not providing an offset, it is usually easier to use gdb's +builtin $gs_base variable. + +---------- + +### **bn_sym** + + +``` {.python .no-copy} +bn_sym(name_val: gdb.Value) -> int | None +``` + + +#### Description +Lookup a symbol's address by name from Binary Ninja. + +This function sees symbols like functions and global variables, +but not stack local variables, use `bn_var` for that. + +#### Example +``` +pwndbg> set integration-provider binja +Pwndbg successfully connected to Binary Ninja (4.2.6455 Personal) xmlrpc: http://127.0.0.1:31337 +Set which provider to use for integration features to 'binja'. +pwndbg> p main +No symbol "main" in current context. +pwndbg> p/x $bn_sym("main") +$2 = 0x555555555645 +pwndbg> b *($bn_sym("main")) +Breakpoint 1 at 0x555555555645 +``` + +---------- + +### **bn_var** + + +``` {.python .no-copy} +bn_var(name_val: gdb.Value) -> int | None +``` + + +#### Description +Lookup a stack variable's address by name from Binary Ninja. + +This function doesn't see functions or global variables, +use `bn_sym` for that. + +#### Example +``` +pwndbg> set integration-provider binja +Pwndbg successfully connected to Binary Ninja (4.2.6455 Personal) xmlrpc: http://127.0.0.1:31337 +Set which provider to use for integration features to 'binja'. +pwndbg> p user_choice +No symbol "user_choice" in current context. +pwndbg> p/x $bn_var("user_choice") +$4 = 0x7fffffffe118 +pwndbg> vmmap $4 + 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [anon_7ffff7ffe] +► 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] +0x20118 +pwndbg> p/x $bn_var("main") +TypeError: Could not convert Python object: None. +Error while executing Python code. +``` + +---------- + +### **bn_eval** + + +``` {.python .no-copy} +bn_eval(expr: gdb.Value) -> int | None +``` + + +#### Description +Parse and evaluate a Binary Ninja expression. + +Read more about binary ninja expressions here: +https://api.binary.ninja/binaryninja.binaryview-module.html#binaryninja.binaryview.BinaryView.parse_expression + +All registers in the current register set are available as magic variables (e.g. $rip). +The $piebase magic variable is also included, with the computed executable base. + +This function cannot see stack local variables. + +#### Example +``` +pwndbg> set integration-provider binja +Pwndbg successfully connected to Binary Ninja (4.2.6455 Personal) xmlrpc: http://127.0.0.1:31337 +Set which provider to use for integration features to 'binja'. +pwndbg> p/x $bn_eval("10+20") +$6 = 0x30 +pwndbg> p/x $bn_eval("main") +$7 = 0x1645 +pwndbg> p/x $rebase($bn_eval("main")) +$8 = 0x555555555645 +pwndbg> p some_global_var +No symbol "some_global_var" in current context. +pwndbg> p/x $rebase($bn_eval("some_global_var+$rax")) +$9 = 0x5555555586b8 +pwndbg> p $rebase($bn_eval("some_global_var+$rax")) == $bn_sym("some_global_var") + $rax +$10 = 1 +pwndbg> p $bn_eval("$piebase+some_global_var+$rax") == $bn_sym("some_global_var") + $rax +$11 = 1 +``` + +---------- + +### **ida** + + +``` {.python .no-copy} +ida(name: gdb.Value) -> int +``` + + +#### Description +Lookup a symbol's address by name from IDA. +Evaluate ida.LocByName() on the supplied value. + +This functions doesn't see stack local variables. + +#### Example +``` +pwndbg> set integration-provider ida +Pwndbg successfully connected to Ida Pro xmlrpc: http://127.0.0.1:31337 +Set which provider to use for integration features to 'ida'. +pwndbg> p main +No symbol "main" in current context. +pwndbg> p/x $ida("main") +$1 = 0x555555555645 +pwndbg> b *$ida("main") +Breakpoint 2 at 0x555555555645 +``` + +---------- \ No newline at end of file diff --git a/gdbinit.py b/gdbinit.py index c864774e7..93bfcc78e 100644 --- a/gdbinit.py +++ b/gdbinit.py @@ -161,7 +161,7 @@ def init_logger(): return handler -def main() -> None: +def check_doubleload(): if "pwndbg" in sys.modules: print( "Detected double-loading of Pwndbg (likely from both .gdbinit and the Pwndbg portable build)." @@ -170,8 +170,20 @@ def main() -> None: "To fix this, please remove the line 'source your-path/gdbinit.py' from your .gdbinit file." ) sys.stdout.flush() - os._exit(1) + sys.exit(1) + + +def rewire_exit(): + major_ver = int(gdb.VERSION.split(".")[0]) + if major_ver <= 15: + # On certain verions of gdb (used on ubuntu 24.04) using sys.exit() can cause + # a segfault. See: + # https://github.com/pwndbg/pwndbg/pull/2900#issuecomment-2825456636 + # https://sourceware.org/bugzilla/show_bug.cgi?id=31946 + sys.exit = os._exit + +def main() -> None: profiler = cProfile.Profile() start_time = None @@ -179,6 +191,9 @@ def main() -> None: start_time = time.time() profiler.enable() + rewire_exit() + check_doubleload() + handler = init_logger() src_root = Path(__file__).parent.resolve() @@ -187,7 +202,7 @@ def main() -> None: if not venv_path.exists(): print(f"Cannot find Pwndbg virtualenv directory: {venv_path}. Please re-run setup.sh") sys.stdout.flush() - os._exit(1) + sys.exit(1) no_auto_update = os.getenv("PWNDBG_NO_AUTOUPDATE") if no_auto_update is None: update_deps(src_root, venv_path) @@ -234,4 +249,4 @@ try: except Exception: print(traceback.format_exc(), file=sys.stderr) sys.stdout.flush() - os._exit(1) + sys.exit(1) diff --git a/mkdocs.yml b/mkdocs.yml index bf5f13756..3b2a7ea1a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,7 +23,7 @@ theme: features: # https://squidfunk.github.io/mkdocs-material/reference/code-blocks/ # A button to copy code snippets. - - content.code.copy + # - content.code.copy # Enable annotations in code blocks. - content.code.annotate # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/ @@ -183,6 +183,7 @@ markdown_extensions: - md_in_html - toc: permalink: "¤" + toc_depth: 3 - tables # https://facelessuser.github.io/pymdown-extensions/ # Officially supported are: diff --git a/pwndbg/commands/__init__.py b/pwndbg/commands/__init__.py index bbfe2809a..223c65155 100644 --- a/pwndbg/commands/__init__.py +++ b/pwndbg/commands/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse import functools +import inspect import io import logging from enum import Enum @@ -194,7 +195,7 @@ class Command: try: return self.function(*args, **kwargs) except TypeError: - print(f"{self.function.__name__.strip()!r}: {self.function.__doc__.strip()}") + print(f"{self.function.__name__.strip()!r}: {inspect.getdoc(self.function).strip()}") pwndbg.exception.handle(self.function.__name__) except Exception: pwndbg.exception.handle(self.function.__name__) diff --git a/pwndbg/commands/binja_functions.py b/pwndbg/commands/binja_functions.py index c459aa6ab..8d47f2a1a 100644 --- a/pwndbg/commands/binja_functions.py +++ b/pwndbg/commands/binja_functions.py @@ -15,7 +15,25 @@ from pwndbg.color import message @pwndbg.gdblib.functions.GdbFunction() @pwndbg.integration.binja.with_bn() def bn_sym(name_val: gdb.Value) -> int | None: - """Lookup a symbol's address by name from Binary Ninja.""" + """ + Lookup a symbol's address by name from Binary Ninja. + + This function sees symbols like functions and global variables, + but not stack local variables, use `bn_var` for that. + + Example: + ``` + pwndbg> set integration-provider binja + Pwndbg successfully connected to Binary Ninja (4.2.6455 Personal) xmlrpc: http://127.0.0.1:31337 + Set which provider to use for integration features to 'binja'. + pwndbg> p main + No symbol "main" in current context. + pwndbg> p/x $bn_sym("main") + $2 = 0x555555555645 + pwndbg> b *($bn_sym("main")) + Breakpoint 1 at 0x555555555645 + ``` + """ name = name_val.string() addr: int | None = pwndbg.integration.binja._bn.get_symbol_addr(name) if addr is None: @@ -26,7 +44,29 @@ def bn_sym(name_val: gdb.Value) -> int | None: @pwndbg.gdblib.functions.GdbFunction() @pwndbg.integration.binja.with_bn() def bn_var(name_val: gdb.Value) -> int | None: - """Lookup a stack variable's address by name from Binary Ninja.""" + """ + Lookup a stack variable's address by name from Binary Ninja. + + This function doesn't see functions or global variables, + use `bn_sym` for that. + + Example: + ``` + pwndbg> set integration-provider binja + Pwndbg successfully connected to Binary Ninja (4.2.6455 Personal) xmlrpc: http://127.0.0.1:31337 + Set which provider to use for integration features to 'binja'. + pwndbg> p user_choice + No symbol "user_choice" in current context. + pwndbg> p/x $bn_var("user_choice") + $4 = 0x7fffffffe118 + pwndbg> vmmap $4 + 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 [anon_7ffff7ffe] + ► 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] +0x20118 + pwndbg> p/x $bn_var("main") + TypeError: Could not convert Python object: None. + Error while executing Python code. + ``` + """ name = name_val.string() conf_and_offset: Tuple[int, int] | None = pwndbg.integration.binja._bn.get_var_offset_from_sp( pwndbg.integration.binja.l2r(pwndbg.aglib.regs.pc), name @@ -42,12 +82,38 @@ def bn_var(name_val: gdb.Value) -> int | None: @pwndbg.gdblib.functions.GdbFunction() @pwndbg.integration.binja.with_bn() def bn_eval(expr: gdb.Value) -> int | None: - """Parse and evaluate a Binary Ninja expression. + """ + Parse and evaluate a Binary Ninja expression. + + Read more about binary ninja expressions here: + https://api.binary.ninja/binaryninja.binaryview-module.html#binaryninja.binaryview.BinaryView.parse_expression + + All registers in the current register set are available as magic variables (e.g. $rip). + The $piebase magic variable is also included, with the computed executable base. - Docs: https://api.binary.ninja/binaryninja.binaryview-module.html#binaryninja.binaryview.BinaryView.parse_expression + This function cannot see stack local variables. - Adds all registers in the current register set as magic variables (e.g. $rip). - Also adds a $piebase magic variable with the computed executable base.""" + Example: + ``` + pwndbg> set integration-provider binja + Pwndbg successfully connected to Binary Ninja (4.2.6455 Personal) xmlrpc: http://127.0.0.1:31337 + Set which provider to use for integration features to 'binja'. + pwndbg> p/x $bn_eval("10+20") + $6 = 0x30 + pwndbg> p/x $bn_eval("main") + $7 = 0x1645 + pwndbg> p/x $rebase($bn_eval("main")) + $8 = 0x555555555645 + pwndbg> p some_global_var + No symbol "some_global_var" in current context. + pwndbg> p/x $rebase($bn_eval("some_global_var+$rax")) + $9 = 0x5555555586b8 + pwndbg> p $rebase($bn_eval("some_global_var+$rax")) == $bn_sym("some_global_var") + $rax + $10 = 1 + pwndbg> p $bn_eval("$piebase+some_global_var+$rax") == $bn_sym("some_global_var") + $rax + $11 = 1 + ``` + """ magic_vars = {} for r in pwndbg.aglib.regs.current: v = pwndbg.aglib.regs[r] diff --git a/pwndbg/commands/ida.py b/pwndbg/commands/ida.py index 21f337116..f81da2103 100644 --- a/pwndbg/commands/ida.py +++ b/pwndbg/commands/ida.py @@ -138,8 +138,26 @@ save_ida() @GdbFunction() -def ida(name): - """Evaluate ida.LocByName() on the supplied value.""" +def ida(name: gdb.Value) -> int: + """ + Lookup a symbol's address by name from IDA. + Evaluate ida.LocByName() on the supplied value. + + This functions doesn't see stack local variables. + + Example: + ``` + pwndbg> set integration-provider ida + Pwndbg successfully connected to Ida Pro xmlrpc: http://127.0.0.1:31337 + Set which provider to use for integration features to 'ida'. + pwndbg> p main + No symbol "main" in current context. + pwndbg> p/x $ida("main") + $1 = 0x555555555645 + pwndbg> b *$ida("main") + Breakpoint 2 at 0x555555555645 + ``` + """ name = name.string() result = pwndbg.integration.ida.LocByName(name) diff --git a/pwndbg/commands/misc.py b/pwndbg/commands/misc.py index 7126b56fc..6b2cd2527 100644 --- a/pwndbg/commands/misc.py +++ b/pwndbg/commands/misc.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse import errno +import inspect from collections import defaultdict import pwndbg.aglib.memory @@ -9,6 +10,7 @@ import pwndbg.aglib.regs import pwndbg.aglib.symbol import pwndbg.aglib.vmmap import pwndbg.color as C +import pwndbg.color.message as message import pwndbg.commands import pwndbg.dbg from pwndbg.commands import CommandCategory @@ -141,6 +143,8 @@ def pwndbg_(filter_pattern, shell, all_, category_, list_categories) -> None: ) print() + print(message.info("Also check out convenience functions with `help function`!")) + def list_and_filter_commands(filter_str, pwndbg_cmds=True, shell_cmds=False): sorted_commands = list(pwndbg.commands.commands) @@ -165,7 +169,7 @@ def list_and_filter_commands(filter_str, pwndbg_cmds=True, shell_cmds=False): continue name = c.__name__ - docs = c.__doc__ + docs = inspect.getdoc(c) if docs: docs = docs.strip() diff --git a/pwndbg/commands/segments.py b/pwndbg/commands/segments.py index b5797694f..493b53a43 100644 --- a/pwndbg/commands/segments.py +++ b/pwndbg/commands/segments.py @@ -1,30 +1,11 @@ from __future__ import annotations -import gdb - import pwndbg.aglib.proc import pwndbg.aglib.regs import pwndbg.commands from pwndbg.commands import CommandCategory -class segment(gdb.Function): - """Get the flat address of memory based off of the named segment register.""" - - def __init__(self, name: str) -> None: - super().__init__(name) - self.name = name - - def invoke(self, arg: gdb.Value = gdb.Value(0), *args: gdb.Value) -> int: - result = getattr(pwndbg.aglib.regs, self.name) - return result + int(arg) - - -# TODO/FIXME: This should be defined only for x86 and x86_64 -segment("fsbase") -segment("gsbase") - - @pwndbg.commands.ArgparsedCommand( "Prints out the FS base address. See also $fsbase.", category=CommandCategory.REGISTER ) @@ -47,3 +28,6 @@ def gsbase() -> None: Prints out the GS base address. See also $gsbase. """ print(hex(int(pwndbg.aglib.regs.gsbase))) + + +# See pwndbg.gdblib.functions for the $fsbase() and $gsbase() definitions. diff --git a/pwndbg/gdblib/functions.py b/pwndbg/gdblib/functions.py index 2be6abcd5..2c2d8ffad 100644 --- a/pwndbg/gdblib/functions.py +++ b/pwndbg/gdblib/functions.py @@ -32,7 +32,22 @@ class _GdbFunction(gdb.Function): self.name = func.__name__ self.func = func self.only_when_running = only_when_running - self.__doc__ = func.__doc__ + self.__doc__ = func.__doc__.strip() + + assert func.__doc__ and "The function must have a docstring." + _first_line = self.__doc__.split("\n")[0] + assert len(_first_line) <= 80 and ( + "The first line of the function's docstring should be short," + " as it is printed with `help function`." + ) + assert _first_line[-1] == "." and ( + "The first line should be a standalone sentence, as it is " + "printed alone with `help function`." + ) + assert ( + "Example:\n" in func.__doc__ + and "Convenience functions need to provide a usage example." + ) functions.append(self) @@ -54,14 +69,61 @@ class _GdbFunction(gdb.Function): @GdbFunction(only_when_running=True) def rebase(addr: gdb.Value | int) -> int: - """Return rebased address.""" + """ + Return address rebased onto the executable's mappings. + + Example: + ``` + pwndbg> p/x $rebase(0xd9020) + $1 = 0x55555562d020 + pwndbg> vmmap + 0x555555554000 0x55555556f000 r--p 1b000 0 /usr/bin/bash + 0x55555556f000 0x55555562d000 r-xp be000 1b000 /usr/bin/bash + 0x55555562d000 0x55555565e000 r--p 31000 d9000 /usr/bin/bash + [...] + pwndbg> p $rebase(0xd9020) == 0x555555554000 + 0xd9020 + $2 = 1 + pwndbg> tele $rebase(0xd9020) + 00:0000│ 0x55555562d020 ◂— 0x204900636f6c6c61 /* 'alloc' */ + 01:0008│ 0x55555562d028 ◂— 'have no name!' + 02:0010│ 0x55555562d030 ◂— 0x65720021656d616e /* 'name!' */ + 03:0018│ 0x55555562d038 ◂— 'adline stdin' + [...] + ``` + """ base = pwndbg.aglib.elf.exe().address return base + int(addr) @GdbFunction(only_when_running=True) def base(name_pattern: gdb.Value | str) -> int: - """Return base address of the first memory mapping containing the given name.""" + """ + Return the base address of the first memory mapping containing the given name. + + Example: + ``` + pwndbg> p/x $base("libc") + $4 = 0x7ffff7d4b000 + pwndbg> vmmap libc + 0x7ffff7d4a000 0x7ffff7d4b000 rw-p 1000 6e000 /usr/lib/libncursesw.so.6.5 + ► 0x7ffff7d4b000 0x7ffff7d6f000 r--p 24000 0 /usr/lib/libc.so.6 + ► 0x7ffff7d6f000 0x7ffff7ed6000 r-xp 167000 24000 /usr/lib/libc.so.6 + ► 0x7ffff7ed6000 0x7ffff7f2b000 r--p 55000 18b000 /usr/lib/libc.so.6 + ► 0x7ffff7f2b000 0x7ffff7f2f000 r--p 4000 1e0000 /usr/lib/libc.so.6 + ► 0x7ffff7f2f000 0x7ffff7f31000 rw-p 2000 1e4000 /usr/lib/libc.so.6 + 0x7ffff7f31000 0x7ffff7f39000 rw-p 8000 0 [anon_7ffff7f31] + pwndbg> tele $base(\\"libc\\")+0x1337 + 00:0000│ 0x7ffff7d4c337 ◂— 0x80480a04214000f0 + 01:0008│ 0x7ffff7d4c33f ◂— 0x8040c02204452040 + 02:0010│ 0x7ffff7d4c347 ◂— 0x20042400000200 + 03:0018│ 0x7ffff7d4c34f ◂— 0x20 /* ' ' */ + [...] + ``` + + Beware of accidentally matching the wrong mapping. For instance, if the loaded + executable contained the string "libc" anywhere in it's path, it would've been + returned. + """ if isinstance(name_pattern, gdb.Value): name = name_pattern.string() else: @@ -73,10 +135,23 @@ def base(name_pattern: gdb.Value | str) -> int: raise ValueError(f"No mapping named {name}") -@GdbFunction(only_when_running=True) +@GdbFunction() def hex2ptr(hex_string: gdb.Value | str) -> int: - """Converts a hex string to a little-endian address and returns the address. - Example usage: $hex2ptr("00 70 75 c1 cd ef 59 00")""" + """ + Converts a hex string to a little-endian address and returns the address. + + Example: + ``` + pwndbg> p/x $hex2ptr("20 74 ed f7 ff 7f") + $1 = 0x7ffff7ed7420 + pwndbg> p/x $hex2ptr("2074edf7ff7f") + $2 = 0x7ffff7ed7420 + pwndbg> distance '$base("libc")' '$hex2ptr("20 74 ed f7 ff 7f")' + 0x7ffff7d4b000->0x7ffff7ed7420 is 0x18c420 bytes (0x31884 words) + ``` + + Especially useful for quickly converting pwntools output. + """ if isinstance(hex_string, gdb.Value): hex_string = hex_string.string() @@ -86,33 +161,58 @@ def hex2ptr(hex_string: gdb.Value | str) -> int: @GdbFunction(only_when_running=True) -def argv(number_value: gdb.Value) -> gdb.Value: - """Evaluate argv on the supplied value.""" - val = pwndbg.aglib.argv.argv(int(number_value)) - if val is None: - raise gdb.GdbError("Arg not found") - return dbg_value_to_gdb(val) +def argc() -> int: + """ + Get the number of program arguments. + Evaluates to argc. + + Example: + ``` + pwndbg> p $argc() + $1 = 2 + pwndbg> argv + 00:0000│ 0x7fffffffe288 —▸ 0x7fffffffe659 ◂— '/usr/bin/cat' + 01:0008│ 0x7fffffffe290 —▸ 0x7fffffffe666 ◂— 'gdbinit.py' + 02:0010│ 0x7fffffffe298 ◂— 0 + ``` + """ + return pwndbg.aglib.argv.argc() @GdbFunction(only_when_running=True) -def envp(number_value: gdb.Value) -> gdb.Value: - """Evaluate envp on the supplied value.""" - val = pwndbg.aglib.argv.envp(int(number_value)) +def argv(index: gdb.Value) -> gdb.Value: + """ + Get the n-th program argument. + Evaluate argv on the supplied value. + + Example: + ``` + pwndbg> p $argv(0) + $11 = (signed char *) 0x7fffffffe666 "/usr/bin/sh" + pwndbg> argv + 00:0000│ 0x7fffffffe2a8 —▸ 0x7fffffffe666 ◂— '/usr/bin/sh' + 01:0008│ 0x7fffffffe2b0 ◂— 0 + ``` + """ + val = pwndbg.aglib.argv.argv(int(index)) if val is None: - raise gdb.GdbError("Environ not found") + raise gdb.GdbError("Arg not found") return dbg_value_to_gdb(val) @GdbFunction(only_when_running=True) -def argc(*args) -> int: - """Evaluates to argc.""" - return pwndbg.aglib.argv.argc() - - -@GdbFunction(only_when_running=True) -def environ(name_value: gdb.Value) -> gdb.Value: - """Evaluate getenv() on the supplied value.""" - name = name_value.string() +def environ(env_name: gdb.Value) -> gdb.Value: + """ + Get an environment variable by name. + Evaluate getenv() on the supplied value. + + Example: + ``` + pwndbg> p $environ("LANG") + $2 = (signed char *) 0x7fffffffebfb "LANG=en_US.UTF-8" + ``` + """ + name = env_name.string() if not name: raise gdb.GdbError("No environment variable name provided") @@ -122,8 +222,105 @@ def environ(name_value: gdb.Value) -> gdb.Value: return dbg_value_to_gdb(val) +@GdbFunction(only_when_running=True) +def envp(index: gdb.Value) -> gdb.Value: + """ + Get the n-th environment variable. + Evaluate envp on the supplied value. + + Example: + ``` + pwndbg> p $envp(0x3F) + $13 = (signed char *) 0x7fffffffef7d "LANG=en_US.UTF-8" + pwndbg> p $envp(0x3F) == $environ("LANG") + $14 = 1 + ``` + """ + val = pwndbg.aglib.argv.envp(int(index)) + if val is None: + raise gdb.GdbError("Environ not found") + return dbg_value_to_gdb(val) + + def dbg_value_to_gdb(d: pwndbg.dbg_mod.Value) -> gdb.Value: from pwndbg.dbg.gdb import GDBValue assert isinstance(d, GDBValue) return d.inner + + +@GdbFunction(only_when_running=True) +def fsbase(offset: gdb.Value = gdb.Value(0)) -> int: + """ + Get the value of the FS segment register. + Only valid on x86(-64). + + Example: + ``` + pwndbg> p/x $fsbase() + $3 = 0x7ffff7cdab80 + pwndbg> p $fs_base == $fsbase() + $4 = 1 + pwndbg> x/gx $fsbase(0x28) + 0x7ffff7cdaba8: 0x4da926e1668e5a00 + pwndbg> x/gx $fsbase(0x30) + 0x7ffff7cdabb0: 0x190a86d93bccf0ad + pwndbg> tls + Thread Local Storage (TLS) base: 0x7ffff7cdab80 + TLS is located at: + 0x7ffff7cda000 0x7ffff7cdc000 rw-p 2000 0 [anon_7ffff7cda] + Dumping the address: + tcbhead_t @ 0x7ffff7cdab80 + 0x00007ffff7cdab80 +0x0000 tcb : 0x7ffff7cdab80 + 0x00007ffff7cdab88 +0x0008 dtv : 0x7ffff7cdb4f0 + 0x00007ffff7cdab90 +0x0010 self : 0x7ffff7cdab80 + 0x00007ffff7cdab98 +0x0018 multiple_threads : 0x0 + 0x00007ffff7cdab9c +0x001c gscope_flag : 0x0 + 0x00007ffff7cdaba0 +0x0020 sysinfo : 0x0 + 0x00007ffff7cdaba8 +0x0028 stack_guard : 0x4da926e1668e5a00 + 0x00007ffff7cdabb0 +0x0030 pointer_guard : 0x190a86d93bccf0ad + [...] + pwndbg> canary + [...] + Canary = 0x4da926e1668e5a00 (may be incorrect on != glibc) + [...] + ``` + FS will usually point to the start of the TLS. If you're not providing an + offset, it is usually easier to use gdb's builtin $fs_base variable. + """ + if pwndbg.aglib.arch.name not in ("i386", "x86-64"): + raise gdb.GdbError("This function is only valid on i386 and x86-64.") + + return pwndbg.aglib.regs.fsbase + int(offset) + + +@GdbFunction(only_when_running=True) +def gsbase(offset: gdb.Value = gdb.Value(0)) -> int: + """ + Get the value of the GS segment register. + Only valid on x86(-64). + + Example: + ``` + pwndbg> p/x $gsbase() + $1 = 0x0 + ``` + The value of the GS register is more interesting when doing kernel debugging: + ``` + pwndbg> p/x $gsbase() + $1 = 0xffff999287a00000 + pwndbg> tele $gsbase() + 00:0000│ 0xffff999287a00000 ◂— 0 + ... ↓ 4 skipped + 05:0028│ 0xffff999287a00028 ◂— 0xd6aa9b336d52a400 + 06:0030│ 0xffff999287a00030 ◂— 0 + 07:0038│ 0xffff999287a00038 ◂— 0 + pwndbg> p $gsbase() == $gs_base + $2 = 1 + ``` + If you're not providing an offset, it is usually easier to use gdb's + builtin $gs_base variable. + """ + if pwndbg.aglib.arch.name not in ("i386", "x86-64"): + raise gdb.GdbError("This function is only valid on i386 and x86-64.") + return pwndbg.aglib.regs.gsbase + int(offset) diff --git a/scripts/_gen_command_docs.py b/scripts/_gen_command_docs.py index fc14ef3e7..e232ca3d8 100644 --- a/scripts/_gen_command_docs.py +++ b/scripts/_gen_command_docs.py @@ -1,7 +1,5 @@ #!/usr/bin/env python """ -usage: python scripts/_gen_command_docs.py - You should use scripts/generate_docs.sh and scripts/verify_docs.sh instead of using this. @@ -23,7 +21,9 @@ import shutil shutil.get_terminal_size = lambda fallback=(80, 24): shutil.os.terminal_size((80, 24)) import argparse +import os import re +import sys from typing import Dict from mdutils.mdutils import MdUtils @@ -35,11 +35,6 @@ autogen_end_marker1 = "\n" -def save_to_file(filename, data): - with open(filename, "w") as f: - f.write(data) - - def inline_code(code): return f"`{code}`" @@ -87,7 +82,7 @@ def extract_sources() -> (Dict[str, argparse.ArgumentParser], Dict[str, list[str print( f"ERROR: Command function {fn_name} in {obj_name} does not have an assigned category." ) - exit(1) + sys.exit(4) cat_folder = category_to_folder_name(category.value) filename = ( @@ -114,7 +109,7 @@ def convert_to_markdown(filename: str, parser: argparse.ArgumentParser) -> str: if not description: print(f"ERROR: Command {name} ({filename}) does not have a description.") - exit(2) + sys.exit(5) mdFile = MdUtils(filename) @@ -296,7 +291,7 @@ def update_files(filename_to_markdown: Dict[str, str]): print( f"ERROR: In file {filename} found the second autogen marker, but couldn't find the first ({autogen_end_marker1})." ) - exit(7) + sys.exit(6) marker_idx = i - 1 break @@ -304,7 +299,7 @@ def update_files(filename_to_markdown: Dict[str, str]): print( f"ERROR: In file {filename} couldn't find autogen marker ({autogen_end_marker2})." ) - exit(8) + sys.exit(7) handwritten_doc = "".join(file_data[marker_idx:]) # Includes the autogen markers @@ -321,13 +316,13 @@ base_path = "docs/commands/" # Must have trailing slash. if len(sys.argv) > 1: print("This script doesn't accept any arguments.") print("See top of the file for usage.") - exit(3) + sys.exit(1) just_verify = False if os.getenv("PWNDBG_GEN_DOC_JUST_VERIFY"): just_verify = True -print("==== Command Documentation ====") +print("\n==== Command Documentation ====") extracted, cat_to_names = extract_sources() markdowned = convert_all_to_markdown(extracted) @@ -338,7 +333,7 @@ if just_verify: missing, extra = verify_existence(markdowned.keys(), base_path) if missing or extra: print("To fix this please run ./scripts/generate_docs.sh.") - exit(555) + sys.exit(2) print("Every file is where it should be!") print("Verifying contents...") @@ -347,7 +342,7 @@ if just_verify: print("VERIFICATION FAILED. The files differ from what would be auto-generated.") print("Error:", err) print("Please run ./scripts/generate_docs.sh from project root and commit the changes.") - exit(777) + sys.exit(3) print("Verification successful!") else: diff --git a/scripts/_gen_configuration_docs.py b/scripts/_gen_configuration_docs.py index 7c36dd7a9..e77c6dd3a 100644 --- a/scripts/_gen_configuration_docs.py +++ b/scripts/_gen_configuration_docs.py @@ -1,7 +1,5 @@ #!/usr/bin/env python """ -usage: python scripts/_gen_configuration_docs.py - You should use scripts/generate_docs.sh and scripts/verify_docs.sh instead of using this. @@ -10,11 +8,14 @@ is set, then : Exit with non-zero exit status if the docs/configuration/ file aren't up to date with the sources. Don't modify anything. If it isn't, this fixes up the docs/configuration/ files to be up -to date with the information from the sources. +to date with the information from the sources. Except docs/configuration/index.md +which is hand-written. """ from __future__ import annotations +import os +import sys from typing import Dict from mdutils.mdutils import MdUtils @@ -23,7 +24,9 @@ import pwndbg from pwndbg.lib.config import HELP_DEFAULT_PREFIX from pwndbg.lib.config import HELP_VALID_VALUES_PREFIX from pwndbg.lib.config import Parameter +from scripts._gen_docs_generic import update_files_simple from scripts._gen_docs_generic import verify_existence +from scripts._gen_docs_generic import verify_files_simple def extract_params() -> Dict[str, list[Parameter]]: @@ -98,48 +101,6 @@ def convert_to_markdown(scoped: Dict[str, list[Parameter]]) -> Dict[str, str]: return markdowned -def verify_files(filename_to_markdown: Dict[str, str]) -> str | None: - """ - Verify all the markdown files are up to date with the sources. - - Returns: - None if everything is up-to-date. - A string containing the error message if something is not. - """ - - for filename, markdown in filename_to_markdown.items(): - if filename == index_path: - print(f"Skipping {filename} (the index).") - continue - - print(f"Checking {filename} ..") - - if not os.path.exists(filename): - return f"File {filename} does not exist." - - file_data = "" - with open(filename, "r") as file: - file_data = file.read() - if file_data != markdown: - return f"File {filename} differs from auto-generated output." - - return None - - -def update_files(filename_to_markdown: Dict[str, str]): - """ - Fix files so they are up to date with the sources. This also - creates new files if needed. - """ - for filename, markdown in filename_to_markdown.items(): - print(f"Updating {filename} ..") - - # Simple case, just create the file and write it. - with open(filename, "w") as file: - file.seek(0) - file.write(markdown) - - def check_index(scoped_params: Dict[str, list[Parameter]]): assert ( len(scoped_params.keys()) == 3 @@ -159,13 +120,13 @@ index_path = base_path + "index.md" if len(sys.argv) > 1: print("This script doesn't accept any arguments.") print("See top of the file for usage.") - exit(3) + sys.exit(1) just_verify = False if os.getenv("PWNDBG_GEN_DOC_JUST_VERIFY"): just_verify = True -print("==== Parameter Documentation ====") +print("\n==== Parameter Documentation ====") scoped_params = extract_params() markdowned = convert_to_markdown(scoped_params) @@ -176,32 +137,32 @@ if just_verify: if missing or extra: print("To add mising files please run ./scripts/generate_docs.sh.") print("To remove extra files please remove them manually.") - exit(555) + sys.exit(2) print("Every file is where it should be!") print("Verifying contents...") - err = verify_files(markdowned) + err = verify_files_simple(markdowned, skip=[index_path]) if err: print("VERIFICATION FAILED. The files differ from what would be auto-generated.") print("Error:", err) print("Please run ./scripts/generate_docs.sh from project root and commit the changes.") - exit(777) + sys.exit(3) print("Verification successful!") else: print("Updating files...") - update_files(markdowned) + update_files_simple(markdowned) print("Update successful.") missing, extra = verify_existence(list(markdowned.keys()) + [index_path], base_path) if len(missing) == 1 and missing[0] == index_path: print(f"The index ({index_path}) is missing. That is a hand-written file, please write it.") - exit(999) + sys.exit(4) assert not missing and "Some files (and not the index) are missing, which should be impossible." if extra: - exit(888) + sys.exit(5) # Always check if the index is valid since it is not autogenerated. check_index(scoped_params) diff --git a/scripts/_gen_docs_generic.py b/scripts/_gen_docs_generic.py index c1de85101..45af7ae0b 100644 --- a/scripts/_gen_docs_generic.py +++ b/scripts/_gen_docs_generic.py @@ -34,3 +34,49 @@ def verify_existence(filenames: list[str], base_path: str) -> (list[str], list[s print() return missing, extra + + +def update_files_simple(filename_to_markdown: Dict[str, str]): + """ + Fix files so they are up to date with the sources. This also + creates new files if needed. + """ + + for filename, markdown in filename_to_markdown.items(): + print(f"Updating {filename} ..") + + # Make the folder containing the file if it doesn't exist. + os.makedirs(os.path.dirname(filename), exist_ok=True) + + # Simple case, just create the file and write it. + with open(filename, "w") as file: + file.seek(0) + file.write(markdown) + + +def verify_files_simple(filename_to_markdown: Dict[str, str], skip: list[str] = []) -> str | None: + """ + Verify all the markdown files are up to date with the sources. + + Returns: + None if everything is up-to-date. + A string containing the error message if something is not. + """ + + for filename, markdown in filename_to_markdown.items(): + if filename in skip: + print(f"Skipping {filename}") + continue + + print(f"Checking {filename} ..") + + if not os.path.exists(filename): + return f"File {filename} does not exist." + + file_data = "" + with open(filename, "r") as file: + file_data = file.read() + if file_data != markdown: + return f"File {filename} differs from auto-generated output." + + return None diff --git a/scripts/_gen_function_docs.py b/scripts/_gen_function_docs.py new file mode 100644 index 000000000..64225a486 --- /dev/null +++ b/scripts/_gen_function_docs.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +""" +You should use scripts/generate_docs.sh and scripts/verify_docs.sh instead +of using this. + +If the PWNDBG_GEN_DOC_JUST_VERIFY environment variable +is set, then : Exit with non-zero exit status if the docs/functions/ files + aren't up to date with the sources. Don't modify anything. + +If it isn't, this fixes up the docs/functions/ files to be up +to date with the information from the sources. +""" + +from __future__ import annotations + +import os +import re +import sys +from inspect import getdoc +from inspect import signature +from typing import Dict + +from mdutils.mdutils import MdUtils + +import pwndbg +from pwndbg.gdblib.functions import _GdbFunction +from scripts._gen_docs_generic import update_files_simple +from scripts._gen_docs_generic import verify_existence +from scripts._gen_docs_generic import verify_files_simple + + +def extract_functions() -> Dict[str, _GdbFunction]: + """ + Returns a dictionary that mapes function names to + the corresponding _GdbFunction objects. + """ + functions = pwndbg.gdblib.functions.functions + result = {} + + for f in functions: + result[f.name] = f + + return result + + +def sanitize_signature(func_name: str, sig: str) -> str: + """ + We need to strip ' from type annotations, and cleanup + some functions that don't display properly. + """ + sig = sig.replace("'", "") + match func_name: + case "fsbase": + sig = re.sub(r"", "gdb.Value(0)", sig) + case "gsbase": + sig = re.sub(r"", "gdb.Value(0)", sig) + return sig + + +def convert_to_markdown(named_funcs: Dict[str, _GdbFunction]) -> Dict[str, str]: + """ + Returns a dict which maps filenames to their markdown contents. + It will have only one item (the index.md). + """ + markdowned = {} + + mdFile = MdUtils(index_path) + mdFile.new_header(level=1, title="Functions") + intro_text = """ +pwndbg provides a set of functions which can be used during expression evaluation to +quickly perform common calculations. These can even be passed to other commands as arguments. +Currently, they only work in gdb. + +To see a list of all functions, including those built into gdb, use `help function`. To see +the help of any given function use `help function function_name`. Function invokation must +include a preceding $ sign and must include brackets. For instance, invoke the `environ` +function like so: +``` +pwndbg> p $environ("LANG") +$2 = (signed char *) 0x7fffffffe6da "LANG=en_US.UTF-8" +``` +If the result of the function is being passed to a pwndbg command, make sure to either escape +the function argument's quotes, or put the whole function call in quotes. +``` +pwndbg> tele $environ("LANG") +usage: telescope [-h] [-r] [-f] [-i] [address] [count] +telescope: error: argument address: debugger couldn't resolve argument '$environ(LANG)': + No symbol "LANG" in current context. +pwndbg> tele $environ(\\"LANG\\") +00:0000│ 0x7fffffffe6cf ◂— 'LANG=en_US.UTF-8' +01:0008│ 0x7fffffffe6d7 ◂— 'US.UTF-8' +02:0010│ 0x7fffffffe6df ◂— 0x4e49475542454400 +[...] +pwndbg> tele '$environ("LANG")' +00:0000│ 0x7fffffffe6cf ◂— 'LANG=en_US.UTF-8' +01:0008│ 0x7fffffffe6d7 ◂— 'US.UTF-8' +02:0010│ 0x7fffffffe6df ◂— 0x4e49475542454400 +[...] +``` +## pwndbg functions + """ + mdFile.new_paragraph(intro_text.strip()) + + for func_name, func in named_funcs.items(): + mdFile.new_paragraph(f"### **{func_name}**") + func_sig = sanitize_signature(func_name, str(signature(func.func))) + func_signature_code = f""" +``` {{.python .no-copy}} +{func_name}{func_sig} +``` +""" + if " object at " in func_sig or "<" in func_sig: # '>' is valid in type annotation (->) + print(f'Signature of {func_name} is rendered as "{func_sig}", please edit') + print("the sanitize_signature() function to display the signature better in the docs.") + sys.exit(5) + + mdFile.new_paragraph(func_signature_code) + mdFile.new_paragraph( + "#### Description\n" + getdoc(func).replace("Example:", "#### Example") + ) + mdFile.new_paragraph("-" * 10) + + hide_nav = "---\nhide:\n - navigation\n---\n" + autogen_warning = ( + "" + ) + markdowned[index_path] = hide_nav + autogen_warning + "\n" + mdFile.get_md_text() + return markdowned + + +def check_index(scoped_params: Dict[str, list[Parameter]]): + assert ( + len(scoped_params.keys()) == 3 + and "It seems a new scope has been added, " + f"please update the index file ({index_path}) and bump this number accordingly." + ) + + +base_path = "docs/functions/" # Must have trailing slash. +index_path = base_path + "index.md" + +# ==== Start ==== + +if len(sys.argv) > 1: + print("This script doesn't accept any arguments.") + print("See top of the file for usage.") + sys.exit(1) + +just_verify = False +if os.getenv("PWNDBG_GEN_DOC_JUST_VERIFY"): + just_verify = True + +print("\n==== Function Documentation ====") + +named_functions = extract_functions() +markdowned = convert_to_markdown(named_functions) +assert len(markdowned) == 1 # Only index.md + +if just_verify: + print("Checking if all files are in place..") + missing, extra = verify_existence(list(markdowned.keys()), base_path) + if missing or extra: + print("To add mising files please run ./scripts/generate_docs.sh.") + print("To remove extra files please remove them manually.") + sys.exit(2) + print("Every file is where it should be!") + + print("Verifying contents...") + err = verify_files_simple(markdowned) + if err: + print("VERIFICATION FAILED. The files differ from what would be auto-generated.") + print("Error:", err) + print("Please run ./scripts/generate_docs.sh from project root and commit the changes.") + sys.exit(3) + + print("Verification successful!") +else: + print("Updating files...") + update_files_simple(markdowned) + print("Update successful.") + + missing, extra = verify_existence(list(markdowned.keys()), base_path) + assert not missing and "Some files (and not the index) are missing, which should be impossible." + + if extra: + print("Please delete the extra files by hand.") + sys.exit(4) diff --git a/scripts/generate_docs.sh b/scripts/generate_docs.sh index cbc6deff7..9db101953 100755 --- a/scripts/generate_docs.sh +++ b/scripts/generate_docs.sh @@ -1,4 +1,9 @@ #!/bin/sh # Run the generator inside gdb so everything resolves correctly. -uv run --group docs gdb --batch -nx --ex "source ./gdbinit.py" --ex "set exception-verbose on" --ex "source ./scripts/_gen_command_docs.py" --ex "source ./scripts/_gen_configuration_docs.py" --quiet +uv run --group docs gdb --batch -nx --ex "source ./gdbinit.py" \ + --ex "set exception-verbose on" \ + --ex "source ./scripts/_gen_command_docs.py" \ + --ex "source ./scripts/_gen_configuration_docs.py" \ + --ex "source ./scripts/_gen_function_docs.py" \ + --quiet diff --git a/scripts/verify_docs.sh b/scripts/verify_docs.sh index 512daae84..5284b7ad6 100755 --- a/scripts/verify_docs.sh +++ b/scripts/verify_docs.sh @@ -3,4 +3,9 @@ # Tell the script to verify instead of generate files. export PWNDBG_GEN_DOC_JUST_VERIFY=1 # Run the verifier inside gdb so everything resolves correctly. -uv run --group docs gdb --batch -nx --ex "source ./gdbinit.py" --ex "set exception-verbose on" --ex "source ./scripts/_gen_command_docs.py" --ex "source ./scripts/_gen_configuration_docs.py" --quiet +uv run --group docs gdb --batch -nx --ex "source ./gdbinit.py" \ + --ex "set exception-verbose on" \ + --ex "source ./scripts/_gen_command_docs.py" \ + --ex "source ./scripts/_gen_configuration_docs.py" \ + --ex "source ./scripts/_gen_function_docs.py" \ + --quiet diff --git a/tests/gdb-tests/tests/test_misc.py b/tests/gdb-tests/tests/test_misc.py index 1c7d59324..a7d4dfde3 100644 --- a/tests/gdb-tests/tests/test_misc.py +++ b/tests/gdb-tests/tests/test_misc.py @@ -1,5 +1,7 @@ from __future__ import annotations +import inspect + import pytest import pwndbg.commands @@ -30,7 +32,7 @@ def test_list_and_filter_commands_full_list(pwndbg_cmds, shell_cmds): all_commands = list_and_filter_commands("", pwndbg_cmds=pwndbg_cmds, shell_cmds=shell_cmds) def get_doc(c): - return c.__doc__.strip().splitlines()[0] if c.__doc__ else None + return inspect.getdoc(c).strip().splitlines()[0] if c.__doc__ else None commands = [] if pwndbg_cmds: diff --git a/tests/pytests_collect.py b/tests/pytests_collect.py index 61f4ce165..c46eb2a18 100644 --- a/tests/pytests_collect.py +++ b/tests/pytests_collect.py @@ -10,7 +10,7 @@ TESTS_PATH = os.environ.get("TESTS_PATH") if TESTS_PATH is None: print("'TESTS_PATH' environment variable not set. Failed to collect tests.") sys.stdout.flush() - os._exit(1) + sys.exit(1) class CollectTestFunctionNames: @@ -30,7 +30,7 @@ rv = pytest.main(["--collect-only", TESTS_PATH], plugins=[collector]) if rv == pytest.ExitCode.INTERRUPTED: print("Failed to collect all tests, perhaps there is a syntax error in one of test files?") sys.stdout.flush() - os._exit(1) + sys.exit(1) print("Listing collected tests:") @@ -39,4 +39,4 @@ for nodeid in collector.collected: # easy way to exit GDB session sys.stdout.flush() -os._exit(0) +sys.exit(0) diff --git a/tests/pytests_launcher.py b/tests/pytests_launcher.py index 72ac08061..35c679a4e 100644 --- a/tests/pytests_launcher.py +++ b/tests/pytests_launcher.py @@ -30,11 +30,12 @@ if return_code != 0: print("If you want to debug tests locally, run ./tests.sh with the --pdb flag") print("-" * 80) -# We must call these functions manually to flush the code coverage data to disk due to using os._exit() +# We must call these functions manually to flush the code coverage data to disk since the sys.exit() call +# might've been replaced by os._exit() in gdbinit.py. # https://github.com/nedbat/coveragepy/issues/310 if (cov := coverage.Coverage.current()) is not None: cov.stop() cov.save() sys.stdout.flush() -os._exit(return_code) +sys.exit(return_code) diff --git a/tests/qemu-tests/conftest.py b/tests/qemu-tests/conftest.py index 6d7c0ef45..b4a24d110 100644 --- a/tests/qemu-tests/conftest.py +++ b/tests/qemu-tests/conftest.py @@ -33,7 +33,7 @@ def qemu_assembly_run(): if QEMU_PORT is None: print("'QEMU_PORT' environment variable not set") sys.stdout.flush() - os._exit(1) + sys.exit(1) def _start_binary(asm: str, arch: str, *args): nonlocal qemu @@ -100,7 +100,7 @@ def qemu_start_binary(): if QEMU_PORT is None: print("'QEMU_PORT' environment variable not set") sys.stdout.flush() - os._exit(1) + sys.exit(1) def _start_binary(path: str, arch: str, endian: Literal["big", "little"] | None = None): nonlocal qemu diff --git a/tests/tests.py b/tests/tests.py index fb49dd99a..731635a3b 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -81,7 +81,7 @@ def make_binaries(test_dir: str): try: subprocess.check_call(["make", "all"], cwd=str(dir_binaries)) except subprocess.CalledProcessError: - exit(1) + sys.exit(1) def run_gdb( @@ -117,10 +117,10 @@ def get_tests_list( if result.returncode == 1: print(tests_collect_output) - exit(1) + sys.exit(1) elif collect_only == 1: print(tests_collect_output) - exit(0) + sys.exit(0) # Extract the test names from the output using regex pattern = re.compile(rf"{test_dir_path}.*::.*") @@ -237,7 +237,7 @@ def run_tests_and_print_stats( print("\nFailing tests:") for test_case in stats.fail_tests_names: print(f"- {test_case}") - exit(1) + sys.exit(1) def parse_args():