aboutsummaryrefslogtreecommitdiffstats
path: root/idatui/domain.py
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-25 22:33:16 +0200
committerblasty <blasty@local>2026-07-25 22:33:16 +0200
commitdcb76f92e8851f747e17cae848e800a394a073e1 (patch)
tree7b2ef9e92049e7a394f707e313cb79ba65309ced /idatui/domain.py
parentnav: one notion of "which pane you're in" — delete _pref (diff)
downloadida-tui-dcb76f92e8851f747e17cae848e800a394a073e1.tar.gz
ida-tui-dcb76f92e8851f747e17cae848e800a394a073e1.tar.xz
ida-tui-dcb76f92e8851f747e17cae848e800a394a073e1.zip
projects phase 3: follow an import into the binary that implements it
Following a call to strcmp reached the PLT/extern entry and stopped there — Hex-Rays has nothing to decompile, because the code lives in a library this binary only references. With the other binary open in the same project we already had everything needed to cross that gap; we just weren't indexing it. Index each binary's imports and exports (KIND_IMPORT / KIND_EXPORT) alongside its functions and strings. On a follow, _import_stub asks whether the target address is one of this binary's import stubs; if so _cross_binary_impl asks the index who exports that name, and we switch there instead of landing on the thunk. Verified end to end on a real echo + libc project: Enter on `strrchr(a1, 47)` in echo's pseudocode switches to libc.so.6 and lands on strrchr at 0xaf960. Three things it turns on: * ELF symbol versioning. The importer sees strrchr@@GLIBC_2.2.5 while the provider may export any of three spellings, so raw names resolve almost nothing. domain.link_name() cuts at the first '@'; Linkage.raw keeps what IDA reported, which is what the listing shows. * Exact match, not substring — ProjectIndex.exact(), so `read` doesn't bind to pread/read_line/thread_start. It also answers below the 3-char trigram floor, and plenty of real exports are that short. * Resolution reads the on-disk index, so a provider resolves while its worker is evicted. That's what the index was for. When nothing in the project provides the symbol _follow_import declines and the normal navigation runs: landing on the stub is still the honest answer, and a single-binary session is unchanged. The PLT-stub PRESENTATION item stays open — an unprovided import should say "imported, provider not in project" rather than show a decompiler error. server/patch_server.py gains list_linkage (idautils.Entries + enum_import_names); a worker without it degrades to no linkage rather than failing. tests: index join +8 (exact vs substring, short names, exclude-self, reverse join, kind isolation, forget unresolves) and link_name +4. 36/0 index, 195/0 scenarios, 23/0 project UI, 33/0 project, 22/0 pool.
Diffstat (limited to 'idatui/domain.py')
-rw-r--r--idatui/domain.py58
1 files changed, 58 insertions, 0 deletions
diff --git a/idatui/domain.py b/idatui/domain.py
index ef37ab9..681542e 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -176,6 +176,35 @@ class StrLit:
type: str = ""
+def link_name(raw: str) -> str:
+ """A linkage name reduced to what actually joins across binaries.
+
+ ELF symbol versioning means the importer sees ``strrchr@@GLIBC_2.2.5`` while
+ the provider may export ``strrchr``, ``strrchr@GLIBC_2.2.5`` or the versioned
+ spelling — comparing raw names silently resolves almost nothing. Cut at the
+ first '@' so both sides meet on the bare symbol.
+ """
+ n = (raw or "").strip()
+ at = n.find("@")
+ return n[:at] if at > 0 else n
+
+
+@dataclass(frozen=True)
+class Linkage:
+ """One import or export: a name this binary takes from, or offers to, other
+ modules. ``module`` is set for imports (the library IDA attributes it to),
+ ``ordinal`` for exports.
+
+ ``name`` is the joinable name; ``raw`` keeps the spelling IDA reported, which
+ is what the user sees in the listing.
+ """
+ addr: int
+ name: str
+ module: str = ""
+ ordinal: int = 0
+ raw: str = ""
+
+
@dataclass
class Decompilation:
ea: int
@@ -818,6 +847,7 @@ class Program:
self._decomp: dict[int, tuple[Decompilation, int]] = {}
self._decomp_maps: dict[int, tuple[list[list[int]], int]] = {} # line->ea sets
self._strings: list["StrLit"] | None = None # whole-binary string literals
+ self._linkage: tuple[list["Linkage"], list["Linkage"]] | None = None
self._name_gen = 0 # bumped on rename; invalidates stale name caches
self._segments_cache: list[tuple[int, int, int, str]] | None = None
self._sections: list[tuple[int, int, str]] | None = None
@@ -1312,6 +1342,34 @@ class Program:
self._strings = out
return out
+ def linkage(self) -> tuple[list[Linkage], list[Linkage]]:
+ """``(imports, exports)`` for this binary, cached. ``([], [])`` if the
+ tool is unavailable — an old worker must not break the caller."""
+ with self._lock:
+ hit = self._linkage
+ if hit is not None:
+ return hit
+ try:
+ payload = self.client.call("list_linkage", kind="both")
+ except IDAToolError:
+ return ([], [])
+ if not isinstance(payload, dict):
+ return ([], [])
+ imps = [Linkage(addr=_as_int(r.get("addr", 0)),
+ name=link_name(r.get("name", "")),
+ module=r.get("module", "") or "",
+ raw=r.get("name", "") or "")
+ for r in payload.get("imports", []) if isinstance(r, dict)]
+ exps = [Linkage(addr=_as_int(r.get("addr", 0)),
+ name=link_name(r.get("name", "")),
+ ordinal=int(r.get("ordinal", 0) or 0),
+ raw=r.get("name", "") or "")
+ for r in payload.get("exports", []) if isinstance(r, dict)]
+ out = ([i for i in imps if i.name], [e for e in exps if e.name])
+ with self._lock:
+ self._linkage = out
+ return out
+
def decomp_map(self, ea: int) -> list[list[int]]:
"""Per-pseudocode-line instruction coverage for the split-view region
highlight: a list aligned to the decompiled lines, each the EAs the