Skip to content
Closed
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
12 changes: 12 additions & 0 deletions declib/api/decompiler_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,18 @@ def get_comment(self, addr: int) -> Optional[Comment]:
def delete_comment(self, addr: int) -> bool:
"""Delete any comment at ``addr`` (lifted)."""
return self._send_request({"type": "method_call", "method_name": "delete_comment", "args": [addr]})

def get_global_var(self, addr: int) -> Optional[GlobalVariable]:
"""Return the GlobalVariable at ``addr`` (lifted), or None. Direct read, no cache."""
return self._send_request({"type": "method_call", "method_name": "get_global_var", "args": [addr]})

def get_function_signature(self, func_addr: int) -> Optional[str]:
"""Return the C prototype string for the function at ``func_addr`` (lifted)."""
return self._send_request({"type": "method_call", "method_name": "get_function_signature", "args": [func_addr]})

def set_function_signature(self, func_addr: int, prototype: str) -> bool:
"""Apply a full C prototype (return type + argument types/names)."""
return self._send_request({"type": "method_call", "method_name": "set_function_signature", "args": [func_addr, prototype]})

def get_callgraph(self, only_names=False):
"""Get the call graph"""
Expand Down
44 changes: 44 additions & 0 deletions declib/api/decompiler_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,50 @@ def delete_comment(self, addr) -> bool:
lowered = self.art_lifter.lower_addr(addr)
return self._del_comment(lowered)

def get_global_var(self, addr) -> Optional[GlobalVariable]:
"""Return the lifted GlobalVariable at ``addr`` (lifted), or None.

Direct backend read (bypasses the light-artifact cache), so a global
renamed/retyped moments earlier is immediately visible.
"""
lowered = self.art_lifter.lower_addr(addr)
gvar = self._get_global_var(lowered)
return self.art_lifter.lift(gvar) if gvar is not None else None

def get_function_signature(self, func_addr) -> Optional[str]:
"""Return the C prototype string for the function at ``func_addr`` (lifted)."""
from declib.api.prototype import format_prototype

func = self.functions[func_addr]
if func is None or func.header is None:
return None
return format_prototype(func.header)

def set_function_signature(self, func_addr, prototype: str) -> bool:
"""Apply a full C prototype (return type + argument types/names).

The generic implementation parses the prototype and overwrites the
function's existing parameters positionally, then re-applies the
header through the normal set path. It retypes/renames the parameters
the backend already recognizes; it does not add or remove parameters.
Backends with an atomic prototype-set API (IDA's ``SetType``) override
this for fidelity.
"""
from declib.api.prototype import parse_prototype

ret_type, arg_specs = parse_prototype(prototype)
func = self.functions[func_addr]
if func is None or func.header is None:
return False
if ret_type:
func.header.type = ret_type
for (_off, arg), (atype, aname) in zip(sorted(func.header.args.items()), arg_specs):
if atype and atype != "...":
arg.type = atype
if aname:
arg.name = aname
return bool(self.set_artifact(func))

# ------------------------------------------------------------------
# Persistence
# ------------------------------------------------------------------
Expand Down
91 changes: 91 additions & 0 deletions declib/api/prototype.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Parsing and formatting of C function prototypes.

Shared by the CLI (``signature`` command) and the DecompilerInterface's
``set_function_signature`` so both agree on how a prototype string maps to a
return type + argument (type, name) pairs.
"""
import re
from typing import List, Optional, Tuple


def split_c_args(body: str) -> List[str]:
"""Split a C argument list on top-level commas (respecting () <> [] nesting)."""
parts, depth, cur = [], 0, ""
for ch in body:
if ch in "(<[":
depth += 1
elif ch in ")>]":
depth -= 1
if ch == "," and depth == 0:
parts.append(cur)
cur = ""
else:
cur += ch
if cur.strip():
parts.append(cur)
return parts


def norm_c_type(t: str) -> str:
"""Normalize whitespace and pointer spacing in a C type string."""
return re.sub(r"\s+", " ", t.replace("*", " * ")).replace(" * ", " *").strip()


def parse_prototype(proto: str) -> Tuple[str, List[Tuple[str, Optional[str]]]]:
"""Parse ``ret name(argtype argname, ...)`` -> (return_type, [(type, name), ...]).

Handles pointer return/argument types, ``void`` argument lists, and varargs
(``...``). Argument names are optional. Raises ``ValueError`` on malformed
input.
"""
proto = proto.strip().rstrip(";").strip()
open_i = proto.find("(")
if open_i == -1:
raise ValueError("no argument list '(...)' found")
close_i = proto.rfind(")")
head = proto[:open_i].strip()
body = proto[open_i + 1:close_i].strip()

m = re.search(r"[A-Za-z_]\w*\s*$", head)
if not m:
raise ValueError(f"cannot find a function name in {head!r}")
ret_type = head[:m.start()].strip()
if not ret_type:
raise ValueError("missing return type")

args: List[Tuple[str, Optional[str]]] = []
if body and body != "void":
for raw in split_c_args(body):
raw = raw.strip()
if raw == "...":
args.append(("...", None))
continue
am = re.search(r"[A-Za-z_]\w*\s*$", raw)
if am and not raw.endswith("*"):
aname = am.group().strip()
atype = raw[:am.start()].strip()
if not atype: # bare type, no name (e.g. "int")
atype, aname = raw, None
else:
atype, aname = raw, None
args.append((norm_c_type(atype), aname))
return norm_c_type(ret_type), args


def format_prototype(header) -> str:
"""Render a declib FunctionHeader as a C prototype string."""
ret = header.type or "void"
name = header.name or "sub"
parts = []
for _off, arg in sorted(header.args.items()):
atype = arg.type or "int"
aname = arg.name or ""
if not aname:
parts.append(atype)
else:
# Glue a pointer type to the name: "char *" + "b" -> "char *b".
sep = "" if atype.endswith("*") else " "
parts.append(f"{atype}{sep}{aname}")
arg_str = ", ".join(parts) if parts else "void"
ret_sep = "" if ret.endswith("*") else " "
return f"{ret}{ret_sep}{name}({arg_str})"
173 changes: 173 additions & 0 deletions declib/cli/decompiler_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
- xref_to data + code references to a target
- xref_from things a function calls (callees)
- comment get/set/append/delete/list comments (annotations)
- global list/get/rename/retype global variables
- signature get/set a function's full signature (prototype)
- rename rename a function or local variable
- create-type define a new struct/enum/typedef from a C string
- retype change the type of a function's variable or argument
Expand Down Expand Up @@ -871,6 +873,124 @@ def cmd_comment(args) -> int:
return EXIT_OK


# ---------------------------------------------------------------------------
# global (list / get / rename / retype)
# ---------------------------------------------------------------------------

def cmd_global(args) -> int:
"""CRUD-lite for global variables, keyed by address."""
from declib.artifacts import GlobalVariable

action = args.global_action
with _with_client(args) as client:
if action == "list":
pattern = re.compile(args.filter) if args.filter else None
entries: List[Dict] = []
for addr, gv in sorted(client.global_vars.items(), key=lambda kv: kv[0]):
name = getattr(gv, "name", None) or ""
if pattern and not pattern.search(name):
continue
entries.append({
"addr": addr, "name": name,
"type": getattr(gv, "type", None), "size": getattr(gv, "size", None),
})
if args.json:
_emit_list(args, entries)
else:
if not entries:
print("No global variables.")
return EXIT_OK
print(f"{'ADDR':<12} {'SIZE':<6} {'TYPE':<20} NAME")
for e in entries:
print(f"{_format_addr_hex(e['addr']):<12} {str(e['size'] or ''):<6} "
f"{str(e['type'] or ''):<20} {e['name']}")
return EXIT_OK

addr_value, _ = _parse_target(args.addr)
if addr_value is None:
raise SystemExit(f"Invalid address {args.addr!r}; expected hex (0x..) or decimal.")
lifted = _to_lifted_addr(client, addr_value)

if action == "get":
gv = client.get_global_var(lifted)
if gv is None:
raise SystemExit(f"No global variable at {_format_addr_hex(lifted)}")
_emit(args, {"addr": lifted, "name": gv.name, "type": gv.type, "size": gv.size})
return EXIT_OK

# rename / retype: build a partial GlobalVariable and push it through.
if action == "rename":
gvar = GlobalVariable(addr=lifted, name=args.new_name)
else: # retype
gvar = GlobalVariable(addr=lifted, type_=args.new_type)
ok = bool(client.set_artifact(gvar))
if not ok:
raise SystemExit(
f"Backend rejected global {action} at {_format_addr_hex(lifted)} "
f"(the backend may not support this — e.g. Ghidra global retype is best-effort)."
)
refreshed = client.get_global_var(lifted)
_emit(args, {
"addr": lifted, "action": action,
"name": getattr(refreshed, "name", None),
"type": getattr(refreshed, "type", None),
"success": ok,
})
return EXIT_OK


# ---------------------------------------------------------------------------
# signature (get / set) — full function prototype
# ---------------------------------------------------------------------------

def cmd_signature(args) -> int:
"""Get or set a function's full signature (return type + argument types/names)."""
from declib.api.prototype import parse_prototype, format_prototype

action = args.signature_action
with _with_client(args) as client:
func_addr = _resolve_function_addr(client, args.function)
if func_addr is None:
raise SystemExit(f"Function not found: {args.function!r}")
known = _known_function_addrs(client)
if known and func_addr not in known:
raise SystemExit(f"Function not found: {args.function!r}")

if action == "get":
func = client.functions[func_addr]
if func is None or func.header is None:
raise SystemExit(f"No signature available for {args.function!r}")
proto = format_prototype(func.header)
_emit(args, {
"function_addr": func_addr,
"signature": proto,
"return_type": func.header.type,
"args": [{"name": a.name, "type": a.type}
for _o, a in sorted(func.header.args.items())],
}, text_field="signature")
return EXIT_OK

# set: validate the prototype client-side for a clean error, then let
# the backend apply it (IDA uses SetType; others use the header path).
try:
parse_prototype(args.prototype)
except ValueError as exc:
raise SystemExit(f"Could not parse prototype: {exc}")

ok = bool(client.set_function_signature(func_addr, args.prototype))
if not ok:
raise SystemExit(f"Backend rejected the signature for {args.function!r}.")

refreshed = client.functions[func_addr]
_emit(args, {
"function_addr": func_addr,
"applied_signature": format_prototype(refreshed.header)
if refreshed is not None and refreshed.header else None,
"success": ok,
})
return EXIT_OK


# ---------------------------------------------------------------------------
# create-type / retype
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1535,6 +1655,59 @@ def _add_comment_addr_action(name, help_text, with_text=False):
_add_output_args(p_clist)
p_clist.set_defaults(func=cmd_comment)

# global
p_glob = sub.add_parser(
"global",
help="List/get/rename/retype global variables.",
)
glob_sub = p_glob.add_subparsers(dest="global_action", required=True)

p_glist = glob_sub.add_parser("list", help="List global variables.")
p_glist.add_argument("--filter", dest="filter", help="Regex to filter global names.")
_add_server_filter_args(p_glist)
_add_output_args(p_glist)
p_glist.set_defaults(func=cmd_global)

p_gget = glob_sub.add_parser("get", help="Show a global variable's name/type/size.")
p_gget.add_argument("addr", help="Address (hex 0x.., decimal, lifted or absolute).")
_add_server_filter_args(p_gget)
_add_output_args(p_gget)
p_gget.set_defaults(func=cmd_global)

p_gren = glob_sub.add_parser("rename", help="Rename the global variable at an address.")
p_gren.add_argument("addr", help="Address of the global variable.")
p_gren.add_argument("new_name", help="New name.")
_add_server_filter_args(p_gren)
_add_output_args(p_gren)
p_gren.set_defaults(func=cmd_global)

p_gret = glob_sub.add_parser("retype", help="Change the type of the global variable at an address.")
p_gret.add_argument("addr", help="Address of the global variable.")
p_gret.add_argument("new_type", help='New C type, e.g. "int", "char[16]", "void *".')
_add_server_filter_args(p_gret)
_add_output_args(p_gret)
p_gret.set_defaults(func=cmd_global)

# signature
p_sig = sub.add_parser(
"signature",
help="Get or set a function's full signature (return type + argument types/names).",
)
sig_sub = p_sig.add_subparsers(dest="signature_action", required=True)

p_sget = sig_sub.add_parser("get", help="Print a function's C prototype.")
p_sget.add_argument("function", help="Function name or address.")
_add_server_filter_args(p_sget)
_add_output_args(p_sget)
p_sget.set_defaults(func=cmd_signature)

p_sset = sig_sub.add_parser("set", help="Set a function's full signature from a C prototype.")
p_sset.add_argument("function", help="Function name or address.")
p_sset.add_argument("prototype", help='C prototype, e.g. "int main(int argc, char **argv)".')
_add_server_filter_args(p_sset)
_add_output_args(p_sset)
p_sset.set_defaults(func=cmd_signature)

# create-type
p_ct = sub.add_parser(
"create-type",
Expand Down
26 changes: 21 additions & 5 deletions declib/decompilers/ghidra/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1054,8 +1054,17 @@ def _set_global_variable(self, gvar: GlobalVariable, **kwargs):

type_str = str(sym_data.getDataType().getPathName()) if sym_data is not None else None
if gvar.type and gvar.type != type_str:
# TODO: set type
pass
gtype = self.typestr_to_gtype(gvar.type)
if gtype is not None:
try:
from ghidra.program.model.data import DataUtilities
DataUtilities.createData(
self.currentProgram, sym.getAddress(), gtype, -1,
DataUtilities.ClearDataMode.CLEAR_ALL_CONFLICT_DATA,
)
changes = True
except Exception as e:
self.warning(f"Failed to set global var type {gvar.type!r}: {e}")

return changes

Expand Down Expand Up @@ -1456,12 +1465,19 @@ def __g_global_variables(self):
# TODO: this just does not work for bigger than 50k syms
from .compat.imports import SymbolType

listing = self.currentProgram.getListing()
# Only real, memory-mapped data symbols are globals. Ghidra also emits
# symbols in synthetic/EXTERNAL address spaces (offset below the image
# base), which would lift to negative addresses and fail to round-trip;
# `memory.contains` filters those out.
memory = self.currentProgram.getMemory()
return [
(int(sym.getAddress().getOffset()), str(sym.getName()), self.currentProgram.getListing().getDataAt(sym.getAddress()), sym)
(int(sym.getAddress().getOffset()), str(sym.getName()), listing.getDataAt(sym.getAddress()), sym)
for sym in self.currentProgram.getSymbolTable().getAllSymbols(True)
if sym.getSymbolType() == SymbolType.LABEL and
self.currentProgram.getListing().getDataAt(sym.getAddress()) and
not self.currentProgram.getListing().getDataAt(sym.getAddress()).isStructure()
memory.contains(sym.getAddress()) and
listing.getDataAt(sym.getAddress()) and
not listing.getDataAt(sym.getAddress()).isStructure()
]


Expand Down
Loading