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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ Settings are stored in a file `.llef` located in your home directory formatted a
| show_all_registers | Boolean | Enable/disable extended register output |
| displayed_registers | List | Comma separated list of registers to display. Order is preserved. Use `default` as a placeholder for the built-in registers. e.g. `llefsettings set displayed_registers default,ymm0,ymm1` |
| enable_darwin_heap_scan | Boolean | Enable/disable more accurate heap scanning for Darwin-based platforms. Uses the Darwin malloc introspection API, executing code in the address space of the target application using LLDB's evaluation engine |
| dereference_show_heap_boundaries | Boolean | Enable/disable heap allocation boundary separators in telescope/dereference (Darwin only, requires enable_darwin_heap_scan) |
| dereference_print | String | How the resolved end of a telescope/dereference chain is printed: `symbol` (symbol/string only), `pointer` (raw pointer only), or `both` (default, e.g. `0x1000004b0 (<breakpoint_here>)`) |
| max_trace_length | Int | Set the maximum length of the call stack backtrace to display |
| stack_view_size | Int | Set the number of entries in the stack read to display |
| max_disassembly_length | Int | Set the maximum number of instructions to disassemble and display around the current PC |
Expand Down Expand Up @@ -150,6 +152,25 @@ e.g.
0x7fffffffecc8│+0000: 0x6857
```

#### Telescope

Alias for `dereference`. View memory at consecutive addresses, with pointer values colored
according to the type of memory they point to (code/stack/heap):
```
(lldb) telescope address [-l lines]
```
Press enter to page forward through memory. The command detects when repeated and automatically advances:
```
(lldb) telescope 0x7fffffffecc8 -l 8
0x7fffffffecc8│+0000: 0x00007ffff7fc3000 -> ...
0x7fffffffecd0│+0008: 0x0000555555556004 -> ...
...
(lldb) [press enter]
0x7fffffffecd8│+0010: 0x... -> ...
```
To show heap allocation boundaries (Darwin only), enable both `enable_darwin_heap_scan` and
`dereference_show_heap_boundaries` settings.

#### Context

Refresh the LLEF GUI with:
Expand Down
185 changes: 162 additions & 23 deletions commands/dereference.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@
SBTarget,
)

from arch import I386, X86_64
from commands.base_command import BaseCommand
from common.color_settings import LLEFColorSettings
from common.constants import GLYPHS, TERM_COLORS
from common.constants import GLYPHS, MSG_TYPE, TERM_COLORS
from common.context_handler import ContextHandler
from common.output_util import color_string, output_line
from common.output_util import color_string, output_line, print_message
from common.settings import LLEFSettings
from common.state import LLEFState
from common.util import attempt_to_read_string_from_memory, check_process, hex_int, hex_or_str, is_code, positive_int
from common.util import (
attempt_to_read_string_from_memory,
check_process,
hex_int,
hex_or_str,
is_code_section,
positive_int,
)


class DereferenceCommand(BaseCommand):
Expand All @@ -31,12 +40,18 @@ class DereferenceCommand(BaseCommand):
program: str = "dereference"
container = None
context_handler: Union[ContextHandler, None] = None
alias_set = {"telescope": ""}
last_address: Union[int, None] = None
last_base: Union[int, None] = None
last_lines: int = 10
last_command: str = ""

def __init__(self, debugger: SBDebugger, __: dict[Any, Any]) -> None:
super().__init__()
self.parser = self.get_command_parser()
self.context_handler = ContextHandler(debugger)
self.color_settings = LLEFColorSettings()
self.settings = LLEFSettings(debugger)
self.state = LLEFState()

@classmethod
Expand All @@ -59,8 +74,11 @@ def get_command_parser(cls) -> argparse.ArgumentParser:
)
parser.add_argument(
"address",
type=hex_int,
help="A value/address/symbol used as the location to print the dereference from",
nargs="?",
default=None,
help="A value/address/symbol to print the dereference from. Accepts a hex/decimal literal, a"
" register/convenience variable (e.g. $rsp), a variable name, or an expression (e.g. nodes,"
" &buf). If omitted, continues from last position.",
)
return parser

Expand All @@ -74,6 +92,26 @@ def get_long_help() -> str:
"""Return a longer help message"""
return DereferenceCommand.get_command_parser().format_help()

def resolve_address(self, value: str, exe_ctx: SBExecutionContext) -> Union[int, None]:
"""
Resolve @value to an address. Plain hex/decimal literals are parsed directly; anything else
(a register, convenience variable, source variable name, or arbitrary expression) is resolved
via LLDB's expression evaluator in the context of the currently selected frame.

:param value: The raw address argument as typed by the user.
:param exe_ctx: The current execution context.
:return: The resolved address, or None if @value could not be resolved.
"""
try:
return hex_int(value)
except ValueError:
pass

address_value = exe_ctx.GetTarget().EvaluateExpression(value)
if address_value.GetError().Fail():
return None
return address_value.GetValueAsUnsigned()

def read_instruction(self, target: SBTarget, address: int) -> SBInstruction:
"""
We disassemble an instruction at the given memory @address.
Expand All @@ -83,9 +121,30 @@ def read_instruction(self, target: SBTarget, address: int) -> SBInstruction:
:return: An object of the disassembled instruction.
"""
instruction_address = SBAddress(address, target)
instruction_list = target.ReadInstructions(instruction_address, 1, self.state.disassembly_syntax)
if self.context_handler.arch is I386 or self.context_handler.arch is X86_64:
instruction_list = target.ReadInstructions(instruction_address, 1, self.state.disassembly_syntax)
else:
instruction_list = target.ReadInstructions(instruction_address, 1)
return instruction_list.GetInstructionAtIndex(0)

def read_symbol_name(self, target: SBTarget, address: int) -> Union[str, None]:
"""
Resolve @address to a <symbol> or <symbol+offset> string, or None if no symbol is known.

:param target: The target object file.
:param address: The memory address to resolve.
:return: A <symbol> / <symbol+offset> string, or None.
"""
sb_address = SBAddress(address, target)
symbol = sb_address.symbol
if not symbol.IsValid():
return None
name = symbol.GetName()
if name is None:
return None
offset = address - symbol.GetStartAddress().GetLoadAddress(target)
return f"<{name}+{offset}>" if offset else f"<{name}>"

def dereference_last_address(
self,
data: list[Union[int, str]],
Expand All @@ -94,29 +153,60 @@ def dereference_last_address(
regions: Union[SBMemoryRegionInfoList, None],
) -> None:
"""
Memory data at the last address (second to last in @data list) is
either disassembled to an instruction or converted to a string or neither.
Resolve the last address (second to last in @data list) to a symbol, instruction or
string and render the end of the chain according to the `dereference_print` setting.

:param data: List of memory addresses/data.
:param target: The target object file.
:param process: The running process of the target.
:param regions: List of memory regions of the process.
"""
last_address = data[-2]
if isinstance(last_address, str):
# Skip pre-rendered markers such as "[LOOPING]"; there is nothing to resolve.
if isinstance(last_address, str) or isinstance(data[-1], str):
return

if is_code(last_address, process, target, regions):
instruction = self.read_instruction(target, last_address)
if instruction.IsValid():
data[-1] = color_string(
f"{instruction.GetMnemonic(target)}{instruction.GetOperands(target)}",
self.color_settings.instruction_color,
)
# Resolve to a symbol or string. Only genuine code sections are disassembled, so
# const/data in an executable module segment is treated as a value.
annotation = None
if is_code_section(last_address, target):
symbol_name = self.read_symbol_name(target, last_address)
if symbol_name is not None:
# Colour the symbol like a binary pointer to match its address.
annotation = color_string(symbol_name, self.color_settings.code_color)
else:
instruction = self.read_instruction(target, last_address)
if instruction.IsValid():
annotation = color_string(
f"{instruction.GetMnemonic(target)}{instruction.GetOperands(target)}",
self.color_settings.instruction_color,
)
else:
string = attempt_to_read_string_from_memory(process, last_address)
if string != "":
data[-1] = color_string(string, self.color_settings.string_color)
annotation = color_string(f'"{string}"', self.color_settings.string_color)

if annotation is None:
return

mode = self.settings.dereference_print

# With no intermediate pointer hop (data[-2] is the address column itself), show the
# annotation alone, or leave the raw value in "pointer" mode.
if len(data) < 3:
if mode != "pointer":
data[-1] = annotation
return

# Drop the raw bytes read through last_address and render the resolved pointer.
data.pop()
if mode == "symbol":
data[-1] = annotation
elif mode == "pointer":
pass # Leave the raw pointer in place; coloured by print_dereference_result.
else: # "both"
pointer_color = self.context_handler.pointer_type_color(last_address)
data[-1] = f"{color_string(hex_or_str(last_address), pointer_color)} ({annotation})"

def dereference(
self, address: int, target: SBTarget, process: SBProcess, regions: Union[SBMemoryRegionInfoList, None]
Expand Down Expand Up @@ -157,7 +247,16 @@ def print_dereference_result(self, result: list[Union[int, str]], offset: int) -
output += f"+0x{offset:04x}: "
else:
output += f"-0x{-offset:04x}: "
output += " -> ".join(map(hex_or_str, result[1:]))

colored_chain = []
for item in result[1:]:
if isinstance(item, int):
color = self.context_handler.pointer_type_color(item)
colored_chain.append(color_string(hex_or_str(item), color))
else:
colored_chain.append(item)

output += " -> ".join(colored_chain)
output_line(output)

@check_process
Expand All @@ -172,12 +271,31 @@ def __call__(

args = self.parser.parse_args(shlex.split(command))

start_address = args.address
lines = args.lines
if args.base:
base = args.base
if args.address is None:
if DereferenceCommand.last_address is None:
print_message(MSG_TYPE.ERROR, "No address specified and no previous command to continue from")
return
address_size = exe_ctx.target.GetAddressByteSize()
start_address = DereferenceCommand.last_address + (address_size * DereferenceCommand.last_lines)
base = DereferenceCommand.last_base
lines = DereferenceCommand.last_lines
else:
base = start_address
if command == DereferenceCommand.last_command and DereferenceCommand.last_address is not None:
address_size = exe_ctx.target.GetAddressByteSize()
start_address = DereferenceCommand.last_address + (address_size * DereferenceCommand.last_lines)
base = DereferenceCommand.last_base
lines = DereferenceCommand.last_lines
else:
resolved_address = self.resolve_address(args.address, exe_ctx)
if resolved_address is None:
print_message(MSG_TYPE.ERROR, f"Could not resolve address argument: {args.address}")
return
start_address = resolved_address
lines = args.lines
if args.base:
base = args.base
else:
base = start_address

if self.context_handler is None:
raise AttributeError("Class not properly initialised: self.context_handler is None")
Expand All @@ -186,8 +304,29 @@ def __call__(

address_size = exe_ctx.target.GetAddressByteSize()

allocation_map = {}
if self.settings.dereference_show_heap_boundaries:
from common.output_util import print_line

allocation_map = self.context_handler.darwin_allocation_map(start_address, lines, address_size)

end_address = start_address + address_size * lines
previous_allocation = None
first_line = True

for address in range(start_address, end_address, address_size):
if self.settings.dereference_show_heap_boundaries and allocation_map:
current_allocation = allocation_map.get(address)
if not first_line and current_allocation != previous_allocation:
print_line()
previous_allocation = current_allocation
first_line = False

offset = address - base
deref_result = self.dereference(address, exe_ctx.target, exe_ctx.process, self.context_handler.regions)
self.print_dereference_result(deref_result, offset)

DereferenceCommand.last_address = start_address
DereferenceCommand.last_base = base
DereferenceCommand.last_lines = lines
DereferenceCommand.last_command = command
43 changes: 34 additions & 9 deletions common/context_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,16 @@
from common.util import (
address_to_filename,
attempt_to_read_string_from_memory,
find_darwin_allocation_sizes,
find_darwin_heap_regions,
find_stack_regions,
get_frame_arguments,
get_frame_range,
get_function_info_from_frame,
get_registers,
hex_or_str,
is_code,
is_heap,
is_module_image,
is_stack,
)

Expand Down Expand Up @@ -220,6 +221,37 @@ def print_bytes(self, addr: int, size: int) -> None:

output_line(line)

def pointer_type_color(self, value: int) -> Union[str, None]:
"""Return color for @value based on whether it points to the binary image, stack or heap"""
if is_module_image(value, self.target):
return self.color_settings.code_color
if is_stack(value, self.regions, self.darwin_stack_regions):
return self.color_settings.stack_color
if is_heap(value, self.target, self.regions, self.darwin_stack_regions, self.darwin_heap_regions):
return self.color_settings.heap_color
return None

def darwin_allocation_map(self, base_addr: int, count: int, step: int) -> dict[int, Union[int, None]]:
"""Map addresses to their containing heap allocation start, for drawing separators between allocations"""
if LLEFState.platform != "Darwin" or not self.settings.enable_darwin_heap_scan:
return {}

sizes = find_darwin_allocation_sizes(self.process, base_addr, count, step)

allocation_map: dict[int, Union[int, None]] = {}
current_start: Union[int, None] = None
current_end = 0
for i, size in enumerate(sizes):
addr = base_addr + i * step
if size > 0:
current_start = addr
current_end = addr + size
if current_start is not None and addr < current_end:
allocation_map[addr] = current_start
else:
allocation_map[addr] = None
return allocation_map

def print_register(self, register: SBValue) -> None:
"""Print details of a @register"""
reg_name = register.GetName()
Expand All @@ -232,14 +264,7 @@ def print_register(self, register: SBValue) -> None:
# Register value has changed so highlight
highlight = self.color_settings.modified_register_color

if is_code(reg_value, self.process, self.target, self.regions):
color = self.color_settings.code_color
elif is_stack(reg_value, self.regions, self.darwin_stack_regions):
color = self.color_settings.stack_color
elif is_heap(reg_value, self.target, self.regions, self.darwin_stack_regions, self.darwin_heap_regions):
color = self.color_settings.heap_color
else:
color = None
color = self.pointer_type_color(reg_value)
formatted_reg_value = f"{reg_value:x}".ljust(12)
line = color_string(reg_name.ljust(7), highlight, "", ": ")
line += color_string(f"0x{formatted_reg_value}", color)
Expand Down
Loading