Symbol.hs (4882B)
1 {-# OPTIONS_HADDOCK prune #-} 2 {-# LANGUAGE OverloadedStrings #-} 3 4 -- | 5 -- Module: Vestigia.Symbol 6 -- Copyright: (c) 2026 Jared Tobin 7 -- License: MIT 8 -- Maintainer: Jared Tobin <jared@ppad.tech> 9 -- 10 -- Naming an instruction by its source: an addr2line over an 11 -- object's debug information, inlined frames included, at the 12 -- instruction's address in the object (where the object's program 13 -- headers place it, as its disassembly does). LLVM's names each 14 -- inlined frame by its own function, and an address no symbol covers 15 -- by none, where binutils' gives the innermost frame the name of the 16 -- function it was inlined into, and code without debug information 17 -- (a GHC program's) the name of whatever function precedes it. LLVM 18 -- leaves a legacy Rust symbol as an Itanium demangler does, its 19 -- escapes and hash in place, which are undone here. 20 21 module Vestigia.Symbol ( 22 Place(..) 23 , symbolize 24 , parseAddr2line 25 , rustLegacy 26 ) where 27 28 import Control.Exception (IOException, try) 29 import Data.Char (chr, isHexDigit) 30 import qualified Data.Map.Strict as M 31 import qualified Data.Text as T 32 import Numeric (readHex, showHex) 33 import System.Process (readProcess) 34 import Text.Read (readMaybe) 35 36 -- | One frame of a source location: the function and, where the 37 -- debug information has it, the file and line. The innermost 38 -- frame comes first. 39 data Place = Place { 40 plFunction :: !(Maybe T.Text) 41 , plFile :: !(Maybe T.Text) 42 , plLine :: !(Maybe Int) 43 } deriving (Eq, Show) 44 45 -- | The source of each address, per object, by one addr2line call 46 -- per object. An object addr2line cannot read names nothing. 47 symbolize 48 :: FilePath -> [(T.Text, Integer)] 49 -> IO (M.Map (T.Text, Integer) [Place]) 50 symbolize addr2line keys = do 51 let byObject = M.fromListWith (<>) [ (o, [ off ]) | (o, off) <- keys ] 52 parts <- traverse one (M.toList byObject) 53 pure (M.unions parts) 54 where 55 one (obj, offs) = do 56 r <- try (readProcess addr2line 57 [ "-a", "-f", "-i", "-C", "-e", T.unpack obj ] 58 (unlines [ "0x" <> showHex o "" | o <- offs ])) 59 :: IO (Either IOException String) 60 pure $ case r of 61 Left _ -> M.empty 62 Right out -> M.fromList 63 [ ((obj, a), ps) | (a, ps) <- parseAddr2line (T.pack out) ] 64 65 -- | addr2line's output under @-a -f -i@: an address line, then a 66 -- function line and a location line per frame. 67 -- 68 -- >>> parseAddr2line "0x20\n??\n??:0\n" 69 -- [(32,[Place Nothing Nothing Nothing])] 70 parseAddr2line :: T.Text -> [(Integer, [Place])] 71 parseAddr2line = go . T.lines 72 where 73 go ls = case ls of 74 (a : rest) | Just addr <- address a -> 75 let (body, more) = break (\l -> address l /= Nothing) rest 76 in (addr, frames body) : go more 77 (_ : rest) -> go rest 78 [] -> [] 79 80 address :: T.Text -> Maybe Integer 81 address l = case T.stripPrefix "0x" l of 82 Just h | not (T.null h), [ (v, "") ] <- readHex (T.unpack h) -> 83 Just v 84 _ -> Nothing 85 86 frames ls = case ls of 87 (f : loc : rest) -> place f loc : frames rest 88 _ -> [] 89 90 place f loc = 91 let (file, line) = location loc 92 in Place { 93 plFunction = if f == "??" then Nothing else Just (rustLegacy f) 94 , plFile = file 95 , plLine = line 96 } 97 98 -- "path:line", "path:line (discriminator n)", or "??:0" 99 location loc = 100 let l = T.takeWhile (/= ' ') loc 101 (p, n) = T.breakOnEnd ":" l 102 path = T.dropEnd 1 p 103 in ( if T.null path || path == "??" then Nothing else Just path 104 , case readMaybe (T.unpack n) of 105 Just 0 -> Nothing 106 other -> other ) 107 108 -- | A legacy Rust symbol, as an Itanium demangler leaves it, as 109 -- rustc-demangle gives it: its hash dropped, each path segment's 110 -- escapes undone. Any other name is left as it is. 111 -- 112 -- >>> rustLegacy "a::_$LT$impl$u20$b..C$GT$::f::h346fee1681a4f34f" 113 -- "a::<impl b::C>::f" 114 rustLegacy :: T.Text -> T.Text 115 rustLegacy t = case T.breakOnEnd "::" t of 116 (pre, h) | not (T.null pre), hash h -> 117 T.intercalate "::" 118 (fmap segment (T.splitOn "::" (T.dropEnd 2 pre))) 119 _ -> t 120 where 121 hash h = case T.uncons h of 122 Just ('h', ds) -> T.length ds == 16 && T.all isHexDigit ds 123 _ -> False 124 segment g = T.pack (unescape (T.unpack 125 (maybe g ("$" <>) (T.stripPrefix "_$" g)))) 126 unescape cs = case cs of 127 '$' : rest 128 | (code, '$' : more) <- break (== '$') rest 129 , Just r <- escape code -> r <> unescape more 130 '.' : '.' : more -> "::" <> unescape more 131 c : more -> c : unescape more 132 [] -> [] 133 escape code = case code of 134 "SP" -> Just "@" 135 "BP" -> Just "*" 136 "RF" -> Just "&" 137 "LT" -> Just "<" 138 "GT" -> Just ">" 139 "LP" -> Just "(" 140 "RP" -> Just ")" 141 "C" -> Just "," 142 'u' : h | [ (n, "") ] <- readHex h -> Just [ chr n ] 143 _ -> Nothing