From 638e3de3bf948fec8e1a0adfa89cbcde487cdeb8 Mon Sep 17 00:00:00 2001 From: blasty Date: Thu, 23 Jul 2026 15:42:23 +0200 Subject: listing: render opcode bytes (shared engine with disasm) — M4/UX step 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flat listing was a subset of the disasm view (no opcodes). Now the listing carries an opcode-bytes column with the same format + o toggle: Head gains a raw field, ListingModel fills it for code heads via one bulk read per page (bounded, variable-length safe) + tracks the widest opcode; ListingView renders/pads the column, settling width as pages stream in. render_line/_line_plain share the addr+opcodes+label+mnem/text layout with DisasmView. Test harness all_funcs() reloads the index if a prior bump_items() cleared it (was crashing biggest() with max([])). Verified: code heads carry raw (max_raw_len 11); listing suite + continuous_view opcode-parity 26/0. --- idatui/domain.py | 50 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) (limited to 'idatui/domain.py') diff --git a/idatui/domain.py b/idatui/domain.py index 3fe0c51..86fb47e 100644 --- a/idatui/domain.py +++ b/idatui/domain.py @@ -92,10 +92,11 @@ class Head: instruction, a data item, or an undefined byte run.""" ea: int - kind: str # 'code' | 'data' | 'unknown' + kind: str # 'code' | 'data' | 'unknown' | 'member' size: int text: str name: str | None = None + raw: bytes | None = None # opcode/item bytes (filled in for code by the model) @classmethod def from_raw(cls, d: dict) -> "Head": @@ -538,11 +539,45 @@ class ListingModel: self._by_ea: dict[int, int] = {} self._next: int | None = seg_start # next address to fetch from self._done = False + self._max_raw = 0 # widest opcode length (bytes) seen, for the op column self._lock = threading.Lock() # Serializes page loads so a background grower and an in-view search can # both drive loading without double-fetching the same page. self._load_lock = threading.Lock() + # Opcode bytes are only worth showing for code; cap the bulk read so a page + # containing a huge coalesced undefined run doesn't pull megabytes. + _OP_SPAN_CAP = 1 << 16 + + def _attach_opcode_bytes(self, page: list[Head]) -> list[Head]: + """Fill ``raw`` (opcode bytes) for the code heads in ``page`` via one + bulk read over their extent (variable-length safe).""" + code = [h for h in page if h.kind == "code" and h.size > 0] + if not code: + return page + lo = code[0].ea + hi = code[-1].ea + code[-1].size + if hi - lo <= 0 or hi - lo > self._OP_SPAN_CAP: + return page + data = self._prog.read_bytes(lo, hi - lo) + biggest = self._max_raw + out = [] + for h in page: + if h.kind == "code" and h.size > 0: + off = h.ea - lo + b = bytes(data[off:off + h.size]) + biggest = max(biggest, len(b)) + out.append(replace(h, raw=b)) + else: + out.append(h) + with self._lock: + self._max_raw = biggest + return out + + def max_raw_len(self) -> int: + with self._lock: + return self._max_raw + def load_next_page(self) -> int: """Load one more page of heads; returns how many were added.""" return self._load_next_page() @@ -559,13 +594,16 @@ class ListingModel: payload = self._prog.client.call("heads", addr=hex(frm), count=self.PAGE) rows = payload.get("heads", []) if isinstance(payload, dict) else [] cur = payload.get("cursor", {}) if isinstance(payload, dict) else {} + page = [] + for r in rows: + try: + page.append(Head.from_raw(r)) + except (KeyError, ValueError, TypeError): + continue + page = self._attach_opcode_bytes(page) with self._lock: base = len(self._heads) - for i, r in enumerate(rows): - try: - h = Head.from_raw(r) - except (KeyError, ValueError, TypeError): - continue + for i, h in enumerate(page): self._by_ea.setdefault(h.ea, base + i) self._heads.append(h) nxt = cur.get("next") -- cgit v1.3.1-sl0p