aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--idatui/app.py8
-rw-r--r--idatui/domain.py8
-rw-r--r--server/patch_server.py39
-rw-r--r--tests/test_scenarios.py25
4 files changed, 77 insertions, 3 deletions
diff --git a/idatui/app.py b/idatui/app.py
index c924fa5..fd8c8c0 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -54,6 +54,8 @@ _S_OPBYTES = Style(color="grey50") # raw opcode bytes column
_S_DATA = Style(color="#c8a15a") # data items in the flat listing (db/dw/strings)
_S_UNK = Style(color="grey54", italic=True) # undefined bytes in the flat listing
_S_MEMBER = Style(color="#9a8a6a") # struct field rows (expanded, indented)
+_S_SEP = Style(color="grey42") # function boundary separators / banners
+_S_FUNCHDR = Style(color="#d7a021", bold=True) # 'name proc near'/'endp' headers
# Tokens that look like identifiers but aren't renamable symbols (so 'n' on them
# in the listing names the address instead of trying to rename the token).
@@ -963,6 +965,8 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
h = self._head(idx)
if h is None:
return None
+ if h.kind in ("sep", "funchdr"):
+ return h.text
indent = " " if h.kind == "member" else ""
return (f"{h.ea:08X} " + self._op_field(h) + indent
+ self._name_prefix(h) + h.text)
@@ -1092,6 +1096,10 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
h = model.get(idx)
if h is None:
strip = Strip([Segment(f" {idx:>8} …", _S_DIM)])
+ elif h.kind == "sep":
+ strip = Strip([Segment(h.text, _S_SEP)])
+ elif h.kind == "funchdr":
+ strip = Strip([Segment(h.text, _S_FUNCHDR)])
else:
segs: list[Segment] = [Segment(f"{h.ea:08X} ", _S_ADDR)]
op = self._op_field(h)
diff --git a/idatui/domain.py b/idatui/domain.py
index e392936..7452ffd 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -595,7 +595,8 @@ class ListingModel:
if self._done or self._next is None:
return 0
frm = self._next
- payload = self._prog.client.call("heads", addr=hex(frm), count=self.PAGE)
+ payload = self._prog.client.call(
+ "heads", addr=hex(frm), count=self.PAGE, annotate=True)
rows = payload.get("heads", []) if isinstance(payload, dict) else []
cur = payload.get("cursor", {}) if isinstance(payload, dict) else {}
page = []
@@ -608,7 +609,10 @@ class ListingModel:
with self._lock:
base = len(self._heads)
for i, h in enumerate(page):
- self._by_ea.setdefault(h.ea, base + i)
+ # Banner rows (function headers/separators) are display-only;
+ # don't index them so navigation lands on real code/data.
+ if h.kind not in ("sep", "funchdr"):
+ self._by_ea.setdefault(h.ea, base + i)
self._heads.append(h)
nxt = cur.get("next")
if nxt is None:
diff --git a/server/patch_server.py b/server/patch_server.py
index ff5c493..99cd584 100644
--- a/server/patch_server.py
+++ b/server/patch_server.py
@@ -338,6 +338,32 @@ def _idatui_struct_member_rows(ea):
return rows
+def _idatui_func_header_rows(ea):
+ """IDA-style subroutine banner rows shown just before a function's entry."""
+ import ida_funcs
+
+ name = ida_funcs.get_func_name(ea) or "sub_%X" % ea
+ bar = "=" * 15 + " S U B R O U T I N E " + "=" * 15
+ return [
+ {"ea": hex(ea), "kind": "sep", "size": 0, "text": ""},
+ {"ea": hex(ea), "kind": "sep", "size": 0, "text": "; " + bar},
+ {"ea": hex(ea), "kind": "funchdr", "size": 0,
+ "text": name + " proc near", "name": name},
+ ]
+
+
+def _idatui_func_footer_rows(ea, func):
+ """End-of-function marker shown just after a function's last item."""
+ import ida_funcs
+
+ name = ida_funcs.get_func_name(func.start_ea) or "sub_%X" % func.start_ea
+ return [
+ {"ea": hex(ea), "kind": "funchdr", "size": 0,
+ "text": name + " endp", "name": name},
+ {"ea": hex(ea), "kind": "sep", "size": 0, "text": "; " + "-" * 60},
+ ]
+
+
@tool
@idasync
def heads(
@@ -346,6 +372,7 @@ def heads(
offset: Annotated[int, "Skip first N heads from addr (default 0)"] = 0,
end: Annotated[str, "Optional exclusive end address; default = segment end"] = "",
back: Annotated[bool, "Walk backwards: return the count heads ENDING just before addr, in forward order"] = False,
+ annotate: Annotated[bool, "Emit IDA-style function boundary banner rows (kind sep/funchdr)"] = False,
) -> dict:
"""Walk item heads from ``addr`` as a flat listing: every head is rendered
(code OR data OR undefined) via generate_disasm_line and stepped with
@@ -415,10 +442,20 @@ def heads(
def _rows_for(e):
if _is_unknown(e):
return [_idatui_unknown_row(e, _run_end(e) - e)]
+ func = idaapi.get_func(e) if annotate else None
+ at_start = func is not None and func.start_ea == e
+ out = []
+ if at_start:
+ out.extend(_idatui_func_header_rows(e))
row = _idatui_head_row(e)
- out = [row]
+ if at_start:
+ row = dict(row)
+ row["name"] = None # the name is shown on the proc header line
+ out.append(row)
if row.get("kind") == "data":
out.extend(_idatui_struct_member_rows(e)) # expand struct fields
+ if func is not None and ida_bytes.get_item_end(e) >= func.end_ea:
+ out.extend(_idatui_func_footer_rows(e, func))
return out
ea = ida_bytes.get_item_head(start)
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
index 8a5df2e..73fc50b 100644
--- a/tests/test_scenarios.py
+++ b/tests/test_scenarios.py
@@ -1619,6 +1619,31 @@ async def s_continuous_view(c: Ctx):
app._active == "listing", f"active={app._active}")
+@scenario("func_banners")
+async def s_func_banners(c: Ctx):
+ """The unified listing shows IDA-style function boundary banners: a
+ SUBROUTINE separator + 'name proc near' header and a 'name endp' footer,
+ and those banner rows are display-only (navigation lands on real code)."""
+ app = c.app
+ fn = await c.open_biggest("listing")
+ c.lst.model.load_all()
+ heads = [c.lst.model.get(i) for i in range(len(c.lst.model))]
+ ci = c.lst.model.index_of_ea(fn.addr)
+ c.check("navigation to a function lands on its code head, not a banner",
+ ci >= 0 and c.lst.model.get(ci).kind == "code",
+ f"kind={c.lst.model.get(ci).kind if ci >= 0 else None}")
+ c.check("a SUBROUTINE separator banner is present",
+ any(h.kind == "sep" and "S U B R O U T I N E" in h.text for h in heads))
+ c.check("a 'name proc near' header is present",
+ any(h.kind == "funchdr" and h.text.endswith("proc near") for h in heads))
+ c.check("a 'name endp' footer is present",
+ any(h.kind == "funchdr" and h.text.endswith("endp") for h in heads))
+ # the proc header for the origin function carries its name
+ hdr = next((h for h in heads if h.kind == "funchdr"
+ and h.text == f"{fn.name} proc near"), None)
+ c.check("the proc header names the function", hdr is not None, f"fn={fn.name}")
+
+
# --------------------------------------------------------------------------- #
# Runner
# --------------------------------------------------------------------------- #