Improve convenience function helps and autogenerate documentation (#2900)

* expand and add examples to pwndbg convenience functions

* update usage descriptions in _gen_*

* refactor out some common logic

* implement function doc generation

* change some mkdocs defaults around to be better for the general case

* generate function docs

* cleanup some examples, help, and quote escaping explanation

* move fsbase and gsbase definitions (and use decorator)

* cleanup signature for fsbase and gsbase

* autogen functions

* type annotation

* fix ida and binja descriptions

* rename arguments, reorder docs for `help function` and assert convention

* add missing imports

* use inspect.getdoc instead of directly accessing __doc__

for more consistency across python versions

* regen docs with getdoc()

* rewire exit and lint

* let users know about convenience functions through the `pwndbg` command

* sys.exit instead of exit
pull/2923/head
k4lizen 8 months ago committed by GitHub
parent d7664e2501
commit 52a4be5e50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,441 @@
---
hide:
- navigation
---
<!-- THIS WHOLE FILE IS AUTOGENERATED. DO NOT MODIFY IT. See scripts/generate_docs.sh -->
# 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
```
----------

@ -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)

@ -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:

@ -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__)

@ -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]

@ -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)

@ -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()

@ -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.

@ -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)

@ -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 = "<!-- END OF AUTOGENERATED PART. Do not modify this line o
autogen_end_marker2 = "<!-- ------------\\>8---- ----\\>8---- ----\\>8------------ -->\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:

@ -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)

@ -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

@ -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 object at 0x[0-9a-fA-F]+>", "gdb.Value(0)", sig)
case "gsbase":
sig = re.sub(r"<gdb\.Value object at 0x[0-9a-fA-F]+>", "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 = (
"<!-- THIS WHOLE FILE IS AUTOGENERATED. DO NOT MODIFY IT. See scripts/generate_docs.sh -->"
)
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)

@ -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

@ -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

@ -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:

@ -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)

@ -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)

@ -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

@ -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():

Loading…
Cancel
Save