forked from baron/baron-sso
userfront 로그인 후 /dashboard로 이동하게 변경
This commit is contained in:
240
scripts/map_wasm_stack.py
Normal file
240
scripts/map_wasm_stack.py
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
WASM 스택의 `wasm-function[IDX]:0xOFFSET`를 이름/소스 라인으로 매핑합니다.
|
||||
|
||||
사용 예시:
|
||||
python3 scripts/map_wasm_stack.py \
|
||||
--wasm userfront/build/web/main.dart.wasm \
|
||||
--sourcemap userfront/build/web/main.dart.wasm.map \
|
||||
--frame "19112:0x2cd913" --frame "765:0x10af0e"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import bisect
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
BASE64_MAP = {c: i for i, c in enumerate(BASE64_CHARS)}
|
||||
|
||||
|
||||
def read_u32_leb128(buf: bytes, i: int) -> Tuple[int, int]:
|
||||
value = 0
|
||||
shift = 0
|
||||
while True:
|
||||
b = buf[i]
|
||||
i += 1
|
||||
value |= (b & 0x7F) << shift
|
||||
if b < 0x80:
|
||||
return value, i
|
||||
shift += 7
|
||||
|
||||
|
||||
def decode_vlq_segment(segment: str) -> List[int]:
|
||||
out: List[int] = []
|
||||
i = 0
|
||||
while i < len(segment):
|
||||
shift = 0
|
||||
value = 0
|
||||
while True:
|
||||
d = BASE64_MAP[segment[i]]
|
||||
i += 1
|
||||
value |= (d & 0x1F) << shift
|
||||
shift += 5
|
||||
if (d & 0x20) == 0:
|
||||
break
|
||||
sign = value & 1
|
||||
value >>= 1
|
||||
out.append(-value if sign else value)
|
||||
return out
|
||||
|
||||
|
||||
@dataclass
|
||||
class SourcePoint:
|
||||
generated_col: int
|
||||
source_index: Optional[int]
|
||||
source_line: Optional[int]
|
||||
source_col: Optional[int]
|
||||
name_index: Optional[int]
|
||||
|
||||
|
||||
class WasmSourceMap:
|
||||
def __init__(self, sourcemap_path: Path):
|
||||
data = json.loads(sourcemap_path.read_text(encoding="utf-8"))
|
||||
self.sources: List[str] = data["sources"]
|
||||
self.names: List[str] = data.get("names", [])
|
||||
mappings: str = data["mappings"]
|
||||
# wasm sourcemap은 generated line 1개를 쓰는 형태라 ',' 단위로만 파싱합니다.
|
||||
segments = mappings.split(",")
|
||||
|
||||
points: List[SourcePoint] = []
|
||||
generated_col = 0
|
||||
source_index = 0
|
||||
source_line = 0
|
||||
source_col = 0
|
||||
name_index = 0
|
||||
|
||||
for seg in segments:
|
||||
if not seg:
|
||||
continue
|
||||
vals = decode_vlq_segment(seg)
|
||||
generated_col += vals[0]
|
||||
si: Optional[int] = None
|
||||
sl: Optional[int] = None
|
||||
sc: Optional[int] = None
|
||||
ni: Optional[int] = None
|
||||
if len(vals) >= 4:
|
||||
source_index += vals[1]
|
||||
source_line += vals[2]
|
||||
source_col += vals[3]
|
||||
si = source_index
|
||||
sl = source_line
|
||||
sc = source_col
|
||||
if len(vals) >= 5:
|
||||
name_index += vals[4]
|
||||
ni = name_index
|
||||
points.append(
|
||||
SourcePoint(
|
||||
generated_col=generated_col,
|
||||
source_index=si,
|
||||
source_line=sl,
|
||||
source_col=sc,
|
||||
name_index=ni,
|
||||
)
|
||||
)
|
||||
self.points = points
|
||||
self.columns = [p.generated_col for p in points]
|
||||
|
||||
def lookup(self, offset: int) -> Optional[SourcePoint]:
|
||||
idx = bisect.bisect_right(self.columns, offset) - 1
|
||||
if idx < 0:
|
||||
return None
|
||||
return self.points[idx]
|
||||
|
||||
def source_name(self, index: Optional[int]) -> Optional[str]:
|
||||
if index is None or index < 0 or index >= len(self.sources):
|
||||
return None
|
||||
return self.sources[index]
|
||||
|
||||
def symbol_name(self, index: Optional[int]) -> Optional[str]:
|
||||
if index is None or index < 0 or index >= len(self.names):
|
||||
return None
|
||||
return self.names[index]
|
||||
|
||||
|
||||
def parse_wasm_function_names(wasm_path: Path) -> Dict[int, str]:
|
||||
b = wasm_path.read_bytes()
|
||||
if b[:4] != b"\x00asm":
|
||||
raise ValueError(f"Not a wasm binary: {wasm_path}")
|
||||
|
||||
function_names: Dict[int, str] = {}
|
||||
i = 8 # magic + version
|
||||
|
||||
while i < len(b):
|
||||
section_id = b[i]
|
||||
i += 1
|
||||
section_size, i = read_u32_leb128(b, i)
|
||||
section_start = i
|
||||
section_end = i + section_size
|
||||
|
||||
if section_id == 0: # custom section
|
||||
name_len, j = read_u32_leb128(b, i)
|
||||
custom_name = b[j : j + name_len].decode("utf-8", errors="replace")
|
||||
payload_start = j + name_len
|
||||
if custom_name == "name":
|
||||
k = payload_start
|
||||
while k < section_end:
|
||||
subsection_id = b[k]
|
||||
k += 1
|
||||
subsection_size, k = read_u32_leb128(b, k)
|
||||
subsection_end = k + subsection_size
|
||||
if subsection_id == 1: # function names
|
||||
count, k = read_u32_leb128(b, k)
|
||||
for _ in range(count):
|
||||
fn_idx, k = read_u32_leb128(b, k)
|
||||
nlen, k = read_u32_leb128(b, k)
|
||||
name = b[k : k + nlen].decode("utf-8", errors="replace")
|
||||
k += nlen
|
||||
function_names[fn_idx] = name
|
||||
else:
|
||||
k = subsection_end
|
||||
|
||||
i = section_end
|
||||
return function_names
|
||||
|
||||
|
||||
def parse_frame(raw: str) -> Tuple[int, int]:
|
||||
m = re.match(r"^\s*(\d+)\s*:\s*(0x[0-9a-fA-F]+)\s*$", raw)
|
||||
if not m:
|
||||
raise ValueError(f"Invalid --frame format: {raw!r} (expected IDX:0xOFFSET)")
|
||||
return int(m.group(1)), int(m.group(2), 16)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="Map wasm stack frames to source locations")
|
||||
p.add_argument("--wasm", required=True, type=Path, help="WASM binary path")
|
||||
p.add_argument("--sourcemap", required=True, type=Path, help="WASM sourcemap path")
|
||||
p.add_argument(
|
||||
"--frame",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Frame in IDX:0xOFFSET format (repeatable)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--offset",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Offset only (hex), function index unknown",
|
||||
)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
source_map = WasmSourceMap(args.sourcemap)
|
||||
function_names = parse_wasm_function_names(args.wasm)
|
||||
|
||||
targets: List[Tuple[Optional[int], int]] = []
|
||||
for f in args.frame:
|
||||
idx, off = parse_frame(f)
|
||||
targets.append((idx, off))
|
||||
for off in args.offset:
|
||||
targets.append((None, int(off, 16)))
|
||||
|
||||
if not targets:
|
||||
raise SystemExit("No targets. Provide --frame or --offset.")
|
||||
|
||||
for fn_idx, off in targets:
|
||||
point = source_map.lookup(off)
|
||||
fn_name = function_names.get(fn_idx) if fn_idx is not None else None
|
||||
mapped_col = point.generated_col if point else None
|
||||
src = source_map.source_name(point.source_index) if point else None
|
||||
src_line = (point.source_line + 1) if point and point.source_line is not None else None
|
||||
src_col = (point.source_col + 1) if point and point.source_col is not None else None
|
||||
symbol = source_map.symbol_name(point.name_index) if point else None
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"function_index": fn_idx,
|
||||
"function_name": fn_name,
|
||||
"offset_hex": hex(off),
|
||||
"mapped_generated_col_hex": hex(mapped_col) if mapped_col is not None else None,
|
||||
"source": src,
|
||||
"source_line": src_line,
|
||||
"source_column": src_col,
|
||||
"symbol": symbol,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user