From ff7e8825ac64254aa6d80b41d423d165689bc075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=88=9A=28noham=29=C2=B2?= <100566912+NohamR@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:36:04 +0100 Subject: [PATCH] Initial commit: LLDB Memory Region Parser web app --- README.md | 26 ++++ index.html | 200 +++++++++++++++++++++++++++++ src.js | 360 +++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 41 ++++++ 4 files changed, 627 insertions(+) create mode 100644 README.md create mode 100644 index.html create mode 100644 src.js create mode 100644 style.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f29e2e --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# LLDB Memory Region Parser + +A web-based visualization tool for analyzing and parsing LLDB memory region output. + +> **Note:** This project was vibecoded and built in a rush to support another debugging task. + +## Purpose + +When debugging with LLDB, the `memory region --all` command outputs detailed information about process memory regions. This tool parses that raw output and presents it in a clean, interactive table with filtering, sorting, and statistics. + +## Usage + +```bash +# In LLDB +(lldb) process attach --pid +(lldb) memory region --all +``` +Copy the output that looks like: +``` +[0x0000000100000000-0x0000000100004000) r-x __TEXT +[0x0000000100004000-0x0000000100008000) rw- __DATA +Dirty pages: 0x100004000, 0x100005000 +... +``` + +Then paste it into the input area of this tool to visualize the memory regions. \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..cdf5421 --- /dev/null +++ b/index.html @@ -0,0 +1,200 @@ + + + + + + + LLDB Memory Region Parser + + + + + + + +
+ + +
+ + +

+ + + + LLDB Memory Region Parser +

+

Analyze memory layout, permissions, and dirty pages from LLDB output +

+
+ + +
+
+ + + +
+ + +
+
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ Start Address + + + End Address + + + Size + + + Perms + + + Name / Details + + + Dirty + +
Waiting for input...
+
+ + +
+
+ + + + + \ No newline at end of file diff --git a/src.js b/src.js new file mode 100644 index 0000000..93c8d57 --- /dev/null +++ b/src.js @@ -0,0 +1,360 @@ +let allRegions = []; +let currentSort = { column: null, direction: "asc" }; + +// Dark Mode Toggle +function toggleDarkMode() { + const html = document.documentElement; + const sunIcon = document.getElementById("theme-icon-sun"); + const moonIcon = document.getElementById("theme-icon-moon"); + + if (html.classList.contains("dark")) { + html.classList.remove("dark"); + html.classList.add("light"); + sunIcon.classList.add("hidden"); + moonIcon.classList.remove("hidden"); + localStorage.setItem("theme", "light"); + } else { + html.classList.remove("light"); + html.classList.add("dark"); + sunIcon.classList.remove("hidden"); + moonIcon.classList.add("hidden"); + localStorage.setItem("theme", "dark"); + } +} + +// Initialize theme from localStorage +(function initTheme() { + const savedTheme = localStorage.getItem("theme") || "light"; + const html = document.documentElement; + const sunIcon = document.getElementById("theme-icon-sun"); + const moonIcon = document.getElementById("theme-icon-moon"); + + if (savedTheme === "dark") { + html.classList.remove("light"); + html.classList.add("dark"); + if (sunIcon) sunIcon.classList.remove("hidden"); + if (moonIcon) moonIcon.classList.add("hidden"); + } else { + html.classList.remove("dark"); + html.classList.add("light"); + if (sunIcon) sunIcon.classList.add("hidden"); + if (moonIcon) moonIcon.classList.remove("hidden"); + } +})(); + +// Parse Logic +function parseMemory() { + const input = document.getElementById("input").value; + if (!input.trim()) return; + + const lines = input.split("\n"); + const regions = []; + let currentRegion = null; + + // Updated Regex to be more robust + // Captures: 1: Start, 2: End, 3: Perms, 4: Name (optional) + const regionRegex = + /\[(0x[0-9a-fA-F]+)-(0x[0-9a-fA-F]+)\)\s+([r\-][w\-][x\-][s\-]?)(?:\s+(.*))?/i; + const dirtyRegex = /Dirty pages:\s+(.+)/i; + + for (let line of lines) { + line = line.trim(); + if (!line) continue; + + const regionMatch = line.match(regionRegex); + if (regionMatch) { + const start = regionMatch[1]; + const end = regionMatch[2]; + const perms = regionMatch[3]; + const name = regionMatch[4] || ""; + + currentRegion = { + startStr: start, // Keep string for display + endStr: end, // Keep string for display + startBig: BigInt(start), // Use BigInt for calculation + endBig: BigInt(end), // Use BigInt for calculation + perms, + name, + dirtyPages: [], + dirtyCount: 0, + }; + + // Calculate size safely using BigInt + currentRegion.size = currentRegion.endBig - currentRegion.startBig; + + regions.push(currentRegion); + } else if (currentRegion) { + // Check for metadata lines associated with the previous region + const dirtyMatch = line.match(dirtyRegex); + if (dirtyMatch) { + const pages = dirtyMatch[1].split(",").map((p) => p.trim()); + currentRegion.dirtyPages = pages; + currentRegion.dirtyCount = pages.length; + } + } + } + + allRegions = regions; + updateStats(regions); + document.getElementById("stats").classList.remove("hidden"); + document.getElementById("stats").classList.add("grid"); + document.getElementById("filter-section").classList.remove("hidden"); + document.getElementById("filter-section").classList.add("flex"); + + // Set default sort to Start Address Ascending + sortBy("start"); +} + +function updateStats(regions) { + const container = document.getElementById("stats"); + + const totalRegions = regions.length; + const totalDirty = regions.reduce((acc, r) => acc + r.dirtyCount, 0); + + // BigInt total size summation + let totalSizeBytes = 0n; + regions.forEach((r) => (totalSizeBytes += r.size)); + + const execCount = regions.filter((r) => r.perms.includes("x")).length; + const writeCount = regions.filter((r) => r.perms.includes("w")).length; + + const createCard = (label, value, colorClass) => ` +
+
${label}
+
${value}
+
+ `; + + container.innerHTML = + createCard( + "Total Regions", + totalRegions, + "text-slate-800 dark:text-slate-200", + ) + + createCard( + "Total Size", + formatBytes(totalSizeBytes), + "text-indigo-600 dark:text-indigo-400", + ) + + createCard("Executable", execCount, "text-red-500 dark:text-red-400") + + createCard("Writable", writeCount, "text-amber-500 dark:text-amber-400") + + createCard( + "Dirty Pages", + totalDirty, + "text-emerald-600 dark:text-emerald-400", + ); +} + +function renderTable(regions) { + const tbody = document.getElementById("table-body"); + const footer = document.getElementById("footer-count"); + + if (regions.length === 0) { + tbody.innerHTML = `No matching regions found.`; + footer.textContent = "0 rows"; + return; + } + + // Limit rendering for performance if massive (simple virtualization cap) + const renderLimit = 2000; + const dataset = regions.slice(0, renderLimit); + + const html = dataset + .map((r) => { + const isExecutable = r.perms.includes("x"); + const isWritable = r.perms.includes("w"); + const isReadable = r.perms.includes("r"); + + // Badges for perms + let permBadges = ""; + if (isReadable) + permBadges += `R`; + else + permBadges += `-`; + + if (isWritable) + permBadges += `W`; + else + permBadges += `-`; + + if (isExecutable) + permBadges += `X`; + else + permBadges += `-`; + + // Row highlighting + const rowClass = + r.dirtyCount > 0 + ? "bg-orange-50 dark:bg-orange-900/20 hover:bg-orange-100 dark:hover:bg-orange-900/30" + : "hover:bg-slate-50 dark:hover:bg-slate-700"; + + return ` + + ${r.startStr} + ${r.endStr} + ${formatBytes(r.size)} + ${permBadges} + ${r.name || 'unnamed'} + + ${ + r.dirtyCount > 0 + ? `${r.dirtyCount}` + : '-' + } + + + `; + }) + .join(""); + + tbody.innerHTML = html; + + const remaining = regions.length - renderLimit; + footer.innerHTML = + remaining > 0 + ? `${dataset.length} rows shown (${remaining} hidden for performance)` + : `${dataset.length} rows`; +} + +// --- Helpers & Utilities --- + +function formatBytes(bigIntBytes) { + if (bigIntBytes === 0n) return "0 B"; + + // Convert to Number for display formatting (precision loss at PB scale is acceptable for UI) + const bytes = Number(bigIntBytes); + const k = 1024; + const sizes = ["B", "KB", "MB", "GB", "TB", "PB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; +} + +function sortBy(column) { + if (currentSort.column === column) { + currentSort.direction = currentSort.direction === "asc" ? "desc" : "asc"; + } else { + currentSort.column = column; + currentSort.direction = "asc"; + } + + // Visual indicator helper + document + .querySelectorAll(".sort-icon") + .forEach((el) => (el.textContent = "⇅")); + document + .querySelectorAll(".sort-icon") + .forEach((el) => el.classList.remove("text-indigo-600")); + + // Update active header + const headers = document.querySelectorAll("th"); + headers.forEach((th) => { + if (th.onclick.toString().includes(column)) { + const icon = th.querySelector(".sort-icon"); + icon.textContent = currentSort.direction === "asc" ? "▲" : "▼"; + icon.classList.add("text-indigo-600"); + icon.classList.remove("invisible"); + } + }); + + applyFilters(); +} + +function applyFilters() { + const addrFilter = document + .getElementById("addressFilter") + .value.toLowerCase(); + const permsFilter = document.getElementById("permsFilter").value; + const dirtyFilter = document.getElementById("dirtyFilter").value; + const hideEmpty = document.getElementById("hideEmptyRegions").checked; + + let filtered = allRegions.filter((r) => { + // 1. Empty/Permissions check + if (hideEmpty && r.perms.startsWith("---")) return false; + + // 2. Address/Name Search + if (addrFilter) { + const searchStr = (r.startStr + r.endStr + r.name).toLowerCase(); + if (!searchStr.includes(addrFilter)) return false; + } + + // 3. Permissions Dropdown + if (permsFilter) { + if ( + permsFilter === "rw" && + (!r.perms.includes("r") || !r.perms.includes("w")) + ) + return false; + else if ( + permsFilter === "rx" && + (!r.perms.includes("r") || !r.perms.includes("x")) + ) + return false; + else if (permsFilter.length === 1 && !r.perms.includes(permsFilter)) + return false; + } + + // 4. Dirty Pages + if (dirtyFilter === "dirty" && r.dirtyCount === 0) return false; + if (dirtyFilter === "clean" && r.dirtyCount > 0) return false; + + return true; + }); + + // Apply Sort + filtered.sort((a, b) => { + let valA, valB; + + switch (currentSort.column) { + case "start": + valA = a.startBig; + valB = b.startBig; + break; + case "end": + valA = a.endBig; + valB = b.endBig; + break; + case "size": + valA = a.size; + valB = b.size; + break; + case "perms": + valA = a.perms; + valB = b.perms; + break; // string comparison + case "name": + valA = a.name.toLowerCase(); + valB = b.name.toLowerCase(); + break; + case "dirty": + valA = a.dirtyCount; + valB = b.dirtyCount; + break; + } + + if (valA < valB) return currentSort.direction === "asc" ? -1 : 1; + if (valA > valB) return currentSort.direction === "asc" ? 1 : -1; + return 0; + }); + + renderTable(filtered); +} + +function clearAll() { + document.getElementById("input").value = ""; + document.getElementById("table-body").innerHTML = + `Waiting for input...`; + document.getElementById("stats").classList.add("hidden"); + document.getElementById("stats").classList.remove("grid"); + document.getElementById("filter-section").classList.add("hidden"); + document.getElementById("filter-section").classList.remove("flex"); + document.getElementById("footer-count").innerText = "0 rows"; + + // Reset filters + document.getElementById("addressFilter").value = ""; + document.getElementById("permsFilter").value = ""; + document.getElementById("dirtyFilter").value = ""; + + allRegions = []; + currentSort = { column: null, direction: "asc" }; +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..103d330 --- /dev/null +++ b/style.css @@ -0,0 +1,41 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); + +body { + font-family: 'Inter', sans-serif; +} + +.font-mono { + font-family: 'JetBrains Mono', monospace; +} + +/* Custom scrollbar for table container */ +.custom-scrollbar::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: #f1f1f1; +} + +.dark .custom-scrollbar::-webkit-scrollbar-track { + background: #1e293b; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb { + background: #475569; + border-radius: 4px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: #64748b; +} \ No newline at end of file