Retrieve debug info when present to show stack variables (#3451)

* Add stack variable lookup from DWARF debug info

* Add get_stack_var_name to Process API

* Implement get_stack_var_name for GDB

* Implement get_stack_var_name for LLDB

* Display stack variable names in memory view

* Fix linter issue

* Catch RuntimeError for stripped binaries

* Skip stack vars without address

* check prefix is not None instead of truthy

* fix accidental spacing

* address review feedback, adjust test regex, and introduce new test

* fix newlines

* fix lldb init

* facepalm

* Remove frame caching, remove import cache, and remove frame_pc parameter

* Remove frame caching, remove import cache, and remove frame_pc parameter

* Remove test from gdb

* Add caching for get_stack_var_name

* Remove unneeded variable

* Add comments explaining errors

* Add comments, not using the web editor...

* Update test for PR #3457

* revert test name

* fix test, match new schema to work on all architectures

* update test name back

* lldb tests fixy

* just rerunning tests, had http 500 errors

---------

Co-authored-by: Disconnect3d <dominik.b.czarnota@gmail.com>
pull/3416/head^2
jackmisbach 4 days ago committed by GitHub
parent 7084a4321e
commit 832a009864
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -189,3 +189,23 @@ def callstack() -> List[int]:
frame = frame.parent() frame = frame.parent()
return addresses return addresses
@pwndbg.lib.cache.cache_until("stop", "start")
def get_stack_var_name(address: int) -> str | None:
"""
Get the name of the stack variable covering the given address.
Returns the variable name with optional offset (e.g., "buf+0x8") if the address
is within a variable but not at its start. Returns None if no variable is found.
"""
frame = pwndbg.dbg.selected_frame()
if frame is None:
return None
for start, end, name in frame.stack_variables():
if start <= address < end:
offset = address - start
return name if offset == 0 else f"{name}+{offset:#x}"
return None

@ -35,7 +35,9 @@ def get_address_and_symbol(address: int) -> str:
else: else:
page = pwndbg.aglib.vmmap.find(address) page = pwndbg.aglib.vmmap.find(address)
if page and "[stack" in page.objfile: if page and "[stack" in page.objfile:
var = pwndbg.integration.provider.get_stack_var_name(address) var = pwndbg.aglib.stack.get_stack_var_name(address)
if not var:
var = pwndbg.integration.provider.get_stack_var_name(address)
if var: if var:
symbol = f"{address:#x} {{{var}}}" symbol = f"{address:#x} {{{var}}}"
return get(address, symbol) return get(address, symbol)
@ -58,7 +60,9 @@ def attempt_colorized_symbol(address: int) -> str | None:
else: else:
page = pwndbg.aglib.vmmap.find(address) page = pwndbg.aglib.vmmap.find(address)
if page and "[stack" in page.objfile: if page and "[stack" in page.objfile:
var = pwndbg.integration.provider.get_stack_var_name(address) var = pwndbg.aglib.stack.get_stack_var_name(address)
if not var:
var = pwndbg.integration.provider.get_stack_var_name(address)
if var: if var:
return get(address, f"{{{var}}}") return get(address, f"{{{var}}}")
return None return None
@ -108,7 +112,7 @@ def get(
if text is None: if text is None:
text = pwndbg.lib.pretty_print.int_to_string(address) text = pwndbg.lib.pretty_print.int_to_string(address)
if prefix: if prefix is not None:
# Prepend the prefix and a space before the existing text # Prepend the prefix and a space before the existing text
text = f"{prefix} {text}" text = f"{prefix} {text}"

@ -253,6 +253,15 @@ class Frame:
""" """
raise NotImplementedError() raise NotImplementedError()
def stack_variables(self) -> Tuple[Tuple[int, int, str], ...]:
"""
Get all stack variables (local variables and arguments) in current frame.
Returns a tuple of (start_address, end_address, name) for each variable.
Returns an empty tuple if no debug information is available or on error.
"""
raise NotImplementedError()
def __eq__(self, rhs: object) -> bool: def __eq__(self, rhs: object) -> bool:
""" """
Whether this frame is the same as the given frame. Two frames are the Whether this frame is the same as the given frame. Two frames are the

@ -92,6 +92,37 @@ def parse_and_eval(expression: str, global_context: bool) -> gdb.Value:
return gdb.parse_and_eval(expression) return gdb.parse_and_eval(expression)
def _get_frame_stack_variables(frame: gdb.Frame) -> Tuple[Tuple[int, int, str], ...]:
try:
block = frame.block()
except (gdb.error, RuntimeError):
# gdb.error: No frame selected (no active inferior)
# RuntimeError: Frame exists and selected,
# But no DWARF info, such as in the case of stripped binaries
return ()
if not block:
return ()
variables = []
while block:
for sym in block:
if not (sym.is_variable or sym.is_argument):
continue
try:
value = sym.value(frame)
addr = int(value.address)
size = value.type.sizeof
variables.append((addr, addr + size, sym.name))
except (gdb.error, AttributeError, TypeError):
continue
block = block.superblock
return tuple(variables)
class GDBRegisters(pwndbg.dbg_mod.Registers): class GDBRegisters(pwndbg.dbg_mod.Registers):
def __init__(self, frame: GDBFrame): def __init__(self, frame: GDBFrame):
self.frame = frame self.frame = frame
@ -204,6 +235,10 @@ class GDBFrame(pwndbg.dbg_mod.Frame):
return sal.symtab.fullname(), sal.line return sal.symtab.fullname(), sal.line
@override
def stack_variables(self) -> Tuple[Tuple[int, int, str], ...]:
return _get_frame_stack_variables(self.inner)
@override @override
def __eq__(self, rhs: object) -> bool: def __eq__(self, rhs: object) -> bool:
assert isinstance(rhs, GDBFrame), "tried to compare GDBFrame to other type" assert isinstance(rhs, GDBFrame), "tried to compare GDBFrame to other type"

@ -78,6 +78,29 @@ class LLDBRegisters(pwndbg.dbg_mod.Registers):
return None return None
def _get_frame_stack_variables(frame: lldb.SBFrame) -> Tuple[Tuple[int, int, str], ...]:
try:
# GetVariables(arguments, locals, statics, in_scope_only)
variables = frame.GetVariables(True, True, False, True)
result = []
for i in range(variables.GetSize()):
var = variables.GetValueAtIndex(i)
if not var.IsValid():
continue
addr = var.GetLoadAddress()
if addr == lldb.LLDB_INVALID_ADDRESS:
continue
size = var.GetType().GetByteSize()
result.append((int(addr), int(addr) + size, var.GetName()))
return tuple(result)
except Exception:
return ()
class LLDBFrame(pwndbg.dbg_mod.Frame): class LLDBFrame(pwndbg.dbg_mod.Frame):
inner: lldb.SBFrame inner: lldb.SBFrame
proc: LLDBProcess proc: LLDBProcess
@ -262,6 +285,10 @@ class LLDBFrame(pwndbg.dbg_mod.Frame):
return None return None
@override
def stack_variables(self) -> Tuple[Tuple[int, int, str], ...]:
return _get_frame_stack_variables(self.inner)
@override @override
def __eq__(self, rhs: object) -> bool: def __eq__(self, rhs: object) -> bool:
assert isinstance(rhs, LLDBFrame), "tried to compare LLDBFrame to other type" assert isinstance(rhs, LLDBFrame), "tried to compare LLDBFrame to other type"

@ -0,0 +1,23 @@
#include <stdio.h>
#include <string.h>
void inner_function(void) {
char buffer[64];
int local_var = 42;
strcpy(buffer, "Hello from inner function!");
printf("local_var = %d\n", local_var);
printf("buffer = %s\n", buffer);
}
void outer_function() {
inner_function();
}
int main() {
printf("Starting test program...\n");
outer_function();
printf("Done\n");
return 0;
}

@ -15,6 +15,7 @@ USE_FDS_BINARY = get_binary("use-fds.native.out")
TABSTOP_BINARY = get_binary("tabstop.native.out") TABSTOP_BINARY = get_binary("tabstop.native.out")
SYSCALLS_BINARY = get_binary("syscalls.x86-64.out") SYSCALLS_BINARY = get_binary("syscalls.x86-64.out")
MANGLING_BINARY = get_binary("symbol_1600_and_752.native.out") MANGLING_BINARY = get_binary("symbol_1600_and_752.native.out")
STACK_VARS_BINARY = get_binary("stack_vars.native.out")
@pwndbg_test @pwndbg_test
@ -54,7 +55,7 @@ async def test_context_disasm_show_fd_filepath(ctrl: Controller) -> None:
) )
line_buf = line_buf.strip() line_buf = line_buf.strip()
assert re.match(r"buf:\s+0x[0-9a-f]+ ◂— 0", line_buf) assert re.match(r"buf:\s+0x[0-9a-f]+(?: \{buf\})? ◂— 0", line_buf)
line_nbytes = line_nbytes.strip() line_nbytes = line_nbytes.strip()
assert re.match(r"nbytes:\s+0", line_nbytes) assert re.match(r"nbytes:\s+0", line_nbytes)
@ -78,7 +79,7 @@ async def test_context_disasm_show_fd_filepath(ctrl: Controller) -> None:
assert re.match(r"fd:\s+3\s+\(.*?/tests/binaries/host/use-fds.native.out\)", line_fd) assert re.match(r"fd:\s+3\s+\(.*?/tests/binaries/host/use-fds.native.out\)", line_fd)
line_buf = line_buf.strip() line_buf = line_buf.strip()
assert re.match(r"buf:\s+0x[0-9a-f]+ ◂— 0", line_buf) assert re.match(r"buf:\s+0x[0-9a-f]+(?: \{buf\})? ◂— 0", line_buf)
line_nbytes = line_nbytes.strip() line_nbytes = line_nbytes.strip()
assert re.match(r"nbytes:\s+0x10", line_nbytes) assert re.match(r"nbytes:\s+0x10", line_nbytes)
@ -622,3 +623,36 @@ async def test_context_output_redirection(ctrl: Controller) -> None:
assert "STACK" not in receive_output.context_output assert "STACK" not in receive_output.context_output
pwndbg.commands.context.resetcontextoutput("regs") pwndbg.commands.context.resetcontextoutput("regs")
@pwndbg_test
async def test_stack_variable_names_from_dwarf(ctrl: Controller) -> None:
"""
Test that stack variable names from DWARF debug info are displayed correctly
"""
import pwndbg.aglib.stack
import pwndbg.commands.context
import pwndbg.dbg
# Launch directly to inner_function where the variables are
await launch_to(ctrl, STACK_VARS_BINARY, "inner_function")
# Test direct API: pwndbg.aglib.stack.get_stack_var_name()
# Get addresses of local variables
frame = pwndbg.dbg.selected_frame()
buffer_addr = int(frame.evaluate_expression("&buffer"))
local_var_addr = int(frame.evaluate_expression("&local_var"))
# Test that get_stack_var_name returns correct names
assert pwndbg.aglib.stack.get_stack_var_name(buffer_addr) == "buffer"
assert pwndbg.aglib.stack.get_stack_var_name(local_var_addr) == "local_var"
# Test offset notation for addresses within variables
# buffer is 64 bytes, so buffer+0x10 should show "buffer+0x10"
buffer_offset_addr = buffer_addr + 0x10
offset_result = pwndbg.aglib.stack.get_stack_var_name(buffer_offset_addr)
assert offset_result == "buffer+0x10"
# Test that telescope shows variable names
telescope_out = await ctrl.execute_and_capture(f"telescope {buffer_addr:#x} 1")
assert "{buffer}" in telescope_out

@ -52,7 +52,7 @@ def test_context_disasm_show_fd_filepath(start_binary):
) )
line_buf = line_buf.strip() line_buf = line_buf.strip()
assert re.match(r"buf:\s+0x[0-9a-f]+ ◂— 0", line_buf) assert re.match(r"buf:\s+0x[0-9a-f]+(?: \{buf\})? ◂— 0", line_buf)
line_nbytes = line_nbytes.strip() line_nbytes = line_nbytes.strip()
assert re.match(r"nbytes:\s+0", line_nbytes) assert re.match(r"nbytes:\s+0", line_nbytes)
@ -74,7 +74,7 @@ def test_context_disasm_show_fd_filepath(start_binary):
assert re.match(r"fd:\s+3 \(.*?/tests/binaries/host/use-fds.native.out\)", line_fd) assert re.match(r"fd:\s+3 \(.*?/tests/binaries/host/use-fds.native.out\)", line_fd)
line_buf = line_buf.strip() line_buf = line_buf.strip()
assert re.match(r"buf:\s+0x[0-9a-f]+ ◂— 0", line_buf) assert re.match(r"buf:\s+0x[0-9a-f]+(?: \{buf\})? ◂— 0", line_buf)
line_nbytes = line_nbytes.strip() line_nbytes = line_nbytes.strip()
assert re.match(r"nbytes:\s+0x10", line_nbytes) assert re.match(r"nbytes:\s+0x10", line_nbytes)

Loading…
Cancel
Save