src/profiler/symbolizer.cpp
Line | Count | Source |
1 | | #include "symbolizer.h" |
2 | | |
3 | | #include <silk/util/logger.h> |
4 | | #include <silk/util/platform.h> |
5 | | |
6 | | #include <algorithm> |
7 | | #include <cerrno> |
8 | | #include <cstddef> |
9 | | #include <cstdio> |
10 | | #include <cstdlib> |
11 | | #include <cstring> |
12 | | #include <string_view> |
13 | | #include <utility> |
14 | | |
15 | | #include <backtrace.h> |
16 | | #include <cxxabi.h> |
17 | | |
18 | | template <typename Callback> |
19 | | static int parseMapsFile(const char * path, Callback callback) noexcept |
20 | 8 | { |
21 | 8 | FILE * f = ::fopen(path, "r"); |
22 | 8 | if (!f) |
23 | 0 | { |
24 | 0 | int r = errno; |
25 | 0 | SILK_ERROR("open %s: %s", path, std::strerror(r)); |
26 | 0 | return r; |
27 | 0 | } |
28 | | |
29 | 8 | char line[512]; |
30 | 336 | while (::fgets(line, sizeof(line), f)) |
31 | 328 | { |
32 | 328 | uint64_t start, end, fileOffset; |
33 | 328 | char perms[8]; |
34 | 328 | char mapPath[256] = {}; |
35 | | |
36 | | // Format: start-end perms offset dev inode [path] |
37 | 328 | int n = ::sscanf(line, "%lx-%lx %4s %lx %*s %*u %255s", &start, &end, perms, &fileOffset, mapPath); |
38 | 328 | if (n < 4) |
39 | 0 | { |
40 | 0 | continue; |
41 | 0 | } |
42 | 328 | if (perms[2] != 'x') |
43 | 264 | { |
44 | 264 | continue; |
45 | 264 | } |
46 | 64 | if (mapPath[0] == '\0' || mapPath[0] == '[') |
47 | 16 | { |
48 | 16 | continue; |
49 | 16 | } |
50 | | |
51 | 48 | callback(start, end, fileOffset, mapPath); |
52 | 48 | } |
53 | | |
54 | 8 | ::fclose(f); |
55 | 8 | return 0; |
56 | 8 | } symbolizer.cpp:int parseMapsFile<Symbolizer::readMappings(unsigned int)::$_0>(char const*, Symbolizer::readMappings(unsigned int)::$_0) Line | Count | Source | 20 | 4 | { | 21 | 4 | FILE * f = ::fopen(path, "r"); | 22 | 4 | if (!f) | 23 | 0 | { | 24 | 0 | int r = errno; | 25 | 0 | SILK_ERROR("open %s: %s", path, std::strerror(r)); | 26 | 0 | return r; | 27 | 0 | } | 28 | | | 29 | 4 | char line[512]; | 30 | 168 | while (::fgets(line, sizeof(line), f)) | 31 | 164 | { | 32 | 164 | uint64_t start, end, fileOffset; | 33 | 164 | char perms[8]; | 34 | 164 | char mapPath[256] = {}; | 35 | | | 36 | | // Format: start-end perms offset dev inode [path] | 37 | 164 | int n = ::sscanf(line, "%lx-%lx %4s %lx %*s %*u %255s", &start, &end, perms, &fileOffset, mapPath); | 38 | 164 | if (n < 4) | 39 | 0 | { | 40 | 0 | continue; | 41 | 0 | } | 42 | 164 | if (perms[2] != 'x') | 43 | 132 | { | 44 | 132 | continue; | 45 | 132 | } | 46 | 32 | if (mapPath[0] == '\0' || mapPath[0] == '[') | 47 | 8 | { | 48 | 8 | continue; | 49 | 8 | } | 50 | | | 51 | 24 | callback(start, end, fileOffset, mapPath); | 52 | 24 | } | 53 | | | 54 | 4 | ::fclose(f); | 55 | 4 | return 0; | 56 | 4 | } |
symbolizer.cpp:int parseMapsFile<Symbolizer::readSelfMappings()::$_0>(char const*, Symbolizer::readSelfMappings()::$_0) Line | Count | Source | 20 | 4 | { | 21 | 4 | FILE * f = ::fopen(path, "r"); | 22 | 4 | if (!f) | 23 | 0 | { | 24 | 0 | int r = errno; | 25 | 0 | SILK_ERROR("open %s: %s", path, std::strerror(r)); | 26 | 0 | return r; | 27 | 0 | } | 28 | | | 29 | 4 | char line[512]; | 30 | 168 | while (::fgets(line, sizeof(line), f)) | 31 | 164 | { | 32 | 164 | uint64_t start, end, fileOffset; | 33 | 164 | char perms[8]; | 34 | 164 | char mapPath[256] = {}; | 35 | | | 36 | | // Format: start-end perms offset dev inode [path] | 37 | 164 | int n = ::sscanf(line, "%lx-%lx %4s %lx %*s %*u %255s", &start, &end, perms, &fileOffset, mapPath); | 38 | 164 | if (n < 4) | 39 | 0 | { | 40 | 0 | continue; | 41 | 0 | } | 42 | 164 | if (perms[2] != 'x') | 43 | 132 | { | 44 | 132 | continue; | 45 | 132 | } | 46 | 32 | if (mapPath[0] == '\0' || mapPath[0] == '[') | 47 | 8 | { | 48 | 8 | continue; | 49 | 8 | } | 50 | | | 51 | 24 | callback(start, end, fileOffset, mapPath); | 52 | 24 | } | 53 | | | 54 | 4 | ::fclose(f); | 55 | 4 | return 0; | 56 | 4 | } |
|
57 | | |
58 | | int Symbolizer::readMappings(uint32_t pid) noexcept |
59 | 4 | { |
60 | 4 | char path[64]; |
61 | 4 | ::snprintf(path, sizeof(path), "/proc/%u/maps", pid); |
62 | 4 | return parseMapsFile( |
63 | 4 | path, |
64 | 4 | [this](uint64_t start, uint64_t end, uint64_t fileOffset, const char * mapPath) |
65 | 24 | { mappings.emplace_back(start, end, fileOffset, mapPath); }); |
66 | 4 | } |
67 | | |
68 | | int Symbolizer::readSelfMappings() noexcept |
69 | 4 | { |
70 | 4 | return parseMapsFile( |
71 | 4 | "/proc/self/maps", |
72 | 4 | [this](uint64_t start, uint64_t end, uint64_t fileOffset, const char * mapPath) |
73 | 24 | { |
74 | | // Record only the first (lowest) executable mapping per path. |
75 | 24 | SILK_UNUSED(end); |
76 | 24 | selfBase.emplace(mapPath, start - fileOffset); |
77 | 24 | }); |
78 | 4 | } |
79 | | |
80 | | int Symbolizer::readKallsyms() noexcept |
81 | 0 | { |
82 | 0 | FILE * f = ::fopen("/proc/kallsyms", "r"); |
83 | 0 | if (!f) |
84 | 0 | { |
85 | 0 | int r = errno; |
86 | 0 | SILK_ERROR("open /proc/kallsyms: %s", std::strerror(r)); |
87 | 0 | return r; |
88 | 0 | } |
89 | | |
90 | 0 | char line[256]; |
91 | 0 | while (::fgets(line, sizeof(line), f)) |
92 | 0 | { |
93 | 0 | uint64_t addr; |
94 | 0 | char type; |
95 | 0 | char name[128] = {}; |
96 | |
|
97 | 0 | if (::sscanf(line, "%lx %c %127s", &addr, &type, name) < 3) |
98 | 0 | { |
99 | 0 | continue; |
100 | 0 | } |
101 | 0 | if (addr == 0) |
102 | 0 | { |
103 | | // kptr_restrict hides real addresses |
104 | 0 | continue; |
105 | 0 | } |
106 | 0 | if (type != 'T' && type != 't') |
107 | 0 | { |
108 | | // data symbols interleaved with text corrupt nearest-preceding-symbol lookup |
109 | 0 | continue; |
110 | 0 | } |
111 | | |
112 | 0 | kallsyms.emplace_back(addr, name); |
113 | 0 | } |
114 | |
|
115 | 0 | ::fclose(f); |
116 | 0 | return 0; |
117 | 0 | } |
118 | | |
119 | | const std::string & Symbolizer::resolve(uint64_t addr) |
120 | 4 | { |
121 | 4 | auto cached = cache.find(addr); |
122 | 4 | if (cached != cache.end()) |
123 | 1 | { |
124 | 1 | return cached->second; |
125 | 1 | } |
126 | | |
127 | 3 | std::string result; |
128 | | |
129 | | // 0xffff000000000000 is the user/kernel boundary on aarch64 (48-bit VA) and lies |
130 | | // in the non-canonical hole on x86-64 -- no valid user address reaches this range. |
131 | 3 | if (!kallsyms.empty() && addr >= 0xffff000000000000ULL0 ) |
132 | 0 | { |
133 | 0 | auto it = std::upper_bound( |
134 | 0 | kallsyms.begin(), kallsyms.end(), addr, [](uint64_t a, const std::pair<uint64_t, std::string> & sym) { return a < sym.first; }); |
135 | 0 | if (it != kallsyms.begin()) |
136 | 0 | { |
137 | 0 | --it; |
138 | 0 | result = it->second; |
139 | 0 | } |
140 | 0 | if (result.empty()) |
141 | 0 | { |
142 | 0 | char buf[32]; |
143 | 0 | std::snprintf(buf, sizeof(buf), "0x%lx", addr); |
144 | 0 | result = buf; |
145 | 0 | } |
146 | 0 | auto [it2, _] = cache.emplace(addr, std::move(result)); |
147 | 0 | return it2->second; |
148 | 0 | } |
149 | | |
150 | 3 | bool mappingFound = false; |
151 | 3 | uint64_t mappingElfAddr = 0; |
152 | 3 | std::string_view mappingPath; |
153 | 3 | for (const Mapping & m : mappings) |
154 | 8 | { |
155 | 8 | if (addr < m.start || addr >= m.end2 ) |
156 | 6 | { |
157 | 6 | continue; |
158 | 6 | } |
159 | 2 | mappingFound = true; |
160 | 2 | mappingElfAddr = addr - m.start + m.fileOffset; |
161 | 2 | mappingPath = m.path; |
162 | | |
163 | | // one backtrace_state per DSO path, created lazily on first address lookup |
164 | 2 | backtrace_state * state; |
165 | 2 | auto it = states.find(m.path); |
166 | 2 | if (it == states.end()) |
167 | 2 | { |
168 | 2 | state = backtrace_create_state(m.path.c_str(), 0, backtraceErrorCb, nullptr); |
169 | 2 | states[m.path] = state; |
170 | 2 | } |
171 | 0 | else |
172 | 0 | { |
173 | 0 | state = it->second; |
174 | 0 | } |
175 | | |
176 | 2 | if (!state) |
177 | 0 | { |
178 | 0 | SILK_WARN("0x%lx: no backtrace state for %s", addr, m.path.c_str()); |
179 | 0 | break; |
180 | 0 | } |
181 | | |
182 | 2 | uint64_t elfAddr = mappingElfAddr; |
183 | | |
184 | 2 | SymResult sym; |
185 | | |
186 | | // Try runtime address first: works for same-process binaries where |
187 | | // dl_iterate_phdr stores symbols at actual load addresses. |
188 | 2 | backtrace_syminfo(state, addr, syminfoCallback, backtraceErrorCb, &sym); |
189 | | |
190 | | // ELF VMA: works for foreign binaries (our libbacktrace patch uses zero base). |
191 | 2 | if (!sym.found) |
192 | 0 | { |
193 | 0 | backtrace_syminfo(state, elfAddr, syminfoCallback, backtraceErrorCb, &sym); |
194 | 0 | } |
195 | | |
196 | | // Shared libraries loaded in both target and profiler: dl_iterate_phdr stores |
197 | | // symbols at the PROFILER's runtime address (profiler_base + elf_vma). |
198 | 2 | auto selfIt = selfBase.find(m.path); |
199 | 2 | if (!sym.found && selfIt != selfBase.end()0 ) |
200 | 0 | { |
201 | 0 | backtrace_syminfo(state, selfIt->second + elfAddr, syminfoCallback, backtraceErrorCb, &sym); |
202 | 0 | } |
203 | | |
204 | | // DWARF fallback via backtrace_pcinfo. |
205 | 2 | if (!sym.found) |
206 | 0 | { |
207 | 0 | backtrace_pcinfo(state, elfAddr, pcinfoCallback, backtraceErrorCb, &sym); |
208 | 0 | } |
209 | 2 | if (!sym.found && selfIt != selfBase.end()0 ) |
210 | 0 | { |
211 | 0 | backtrace_pcinfo(state, selfIt->second + elfAddr, pcinfoCallback, backtraceErrorCb, &sym); |
212 | 0 | } |
213 | | |
214 | 2 | if (sym.found) |
215 | 2 | { |
216 | | // Demangle and strip parameter list so overloads collapse to one node in the flamegraph |
217 | 2 | result = Symbolizer::demangle(sym.name, DemangleOptions::StripAll); |
218 | 2 | } |
219 | | |
220 | 2 | break; |
221 | 2 | } |
222 | | |
223 | 3 | if (result.empty()) |
224 | 1 | { |
225 | 1 | if (mappingFound) |
226 | 0 | { |
227 | 0 | SILK_DEBUG( |
228 | 0 | "0x%lx: in mapping but not symbolized (elfAddr=0x%lx %.*s)", |
229 | 0 | addr, |
230 | 0 | mappingElfAddr, |
231 | 0 | static_cast<int>(mappingPath.size()), |
232 | 0 | mappingPath.data()); |
233 | 0 | std::string_view base = mappingPath; |
234 | 0 | if (auto slash = base.rfind('/'); slash != std::string_view::npos) |
235 | 0 | { |
236 | 0 | base = base.substr(slash + 1); |
237 | 0 | } |
238 | 0 | char buf[256]; |
239 | 0 | std::snprintf(buf, sizeof(buf), "%.*s+0x%lx", static_cast<int>(base.size()), base.data(), mappingElfAddr); |
240 | 0 | result = buf; |
241 | 0 | } |
242 | 1 | else |
243 | 1 | { |
244 | 1 | SILK_DEBUG("0x%lx: no mapping (library loaded after readMappings?)", addr); |
245 | 1 | char buf[32]; |
246 | 1 | std::snprintf(buf, sizeof(buf), "0x%lx", addr); |
247 | 1 | result = buf; |
248 | 1 | } |
249 | 1 | } |
250 | | |
251 | 3 | auto [it, _] = cache.emplace(addr, std::move(result)); |
252 | 3 | return it->second; |
253 | 3 | } |
254 | | |
255 | | std::string Symbolizer::demangle(const std::string & mangled, DemangleOptions options) |
256 | 28 | { |
257 | 28 | std::string result; |
258 | 28 | int status = 0; |
259 | 28 | char * demangled = abi::__cxa_demangle(mangled.c_str(), nullptr, nullptr, &status); |
260 | 28 | if (demangled && status == 0) |
261 | 28 | { |
262 | 28 | result = demangled; |
263 | | |
264 | | // We need to strip "(anonymous namespace)::", which might appear |
265 | | // as the first token or in the middle of the function name and arguments. |
266 | 28 | if ((static_cast<int>(options) & static_cast<int>(DemangleOptions::StripAnonymousNamespace)) |
267 | 28 | == static_cast<int>(DemangleOptions::StripAnonymousNamespace)) |
268 | 15 | { |
269 | 15 | constexpr std::string_view anonymousNamespace = "(anonymous namespace)::"; |
270 | 15 | if (size_t pos = result.find(anonymousNamespace); pos != std::string::npos) |
271 | 7 | { |
272 | 7 | std::string stripped; |
273 | 7 | stripped.reserve(result.size()); |
274 | | |
275 | 7 | size_t last = 0; |
276 | 7 | do |
277 | 10 | { |
278 | 10 | stripped.append(result, last, pos - last); |
279 | 10 | last = pos + anonymousNamespace.size(); |
280 | 10 | } while ((pos = result.find(anonymousNamespace, last)) != std::string::npos); |
281 | | |
282 | 7 | stripped.append(result, last, std::string::npos); |
283 | 7 | result = std::move(stripped); |
284 | 7 | } |
285 | 15 | } |
286 | | |
287 | | // Strip arguments from the last pair of parentheses |
288 | | // so overloads collapse to one node in the flamegraph. |
289 | 28 | if ((static_cast<int>(options) & static_cast<int>(DemangleOptions::StripArguments)) |
290 | 28 | == static_cast<int>(DemangleOptions::StripArguments)) |
291 | 15 | { |
292 | 15 | if (size_t paren = result.rfind(')'); paren != std::string::npos) |
293 | 15 | { |
294 | 15 | int counter = 0; |
295 | 228 | for (std::ptrdiff_t pos = static_cast<std::ptrdiff_t>(paren); pos >= 0; pos--213 ) |
296 | 228 | { |
297 | 228 | const size_t index = static_cast<size_t>(pos); |
298 | 228 | if (result[index] == ')') |
299 | 17 | { |
300 | 17 | counter++; |
301 | 17 | } |
302 | 211 | else if (result[index] == '(') |
303 | 17 | { |
304 | 17 | counter--; |
305 | 17 | if (counter == 0) |
306 | 15 | { |
307 | 15 | result.erase(index); |
308 | 15 | break; |
309 | 15 | } |
310 | 17 | } |
311 | 228 | } |
312 | 15 | } |
313 | 15 | } |
314 | 28 | } |
315 | 0 | else |
316 | 0 | { |
317 | 0 | result = mangled; |
318 | 0 | } |
319 | | |
320 | 28 | std::free(demangled); |
321 | 28 | return result; |
322 | 28 | } |
323 | | |
324 | | void Symbolizer::backtraceErrorCb(void * data, const char * msg, int errnum) |
325 | 0 | { |
326 | 0 | SILK_UNUSED(data); |
327 | |
|
328 | 0 | if (errnum) |
329 | 0 | { |
330 | 0 | SILK_ERROR("libbacktrace: %s (%s)", msg, std::strerror(errnum)); |
331 | 0 | } |
332 | 0 | else |
333 | 0 | { |
334 | 0 | SILK_ERROR("libbacktrace: %s", msg); |
335 | 0 | } |
336 | 0 | } |
337 | | |
338 | | int Symbolizer::pcinfoCallback(void * data, uintptr_t pc, const char * filename, int lineno, const char * function) |
339 | 0 | { |
340 | 0 | SILK_UNUSED(pc); |
341 | 0 | SILK_UNUSED(filename); |
342 | 0 | SILK_UNUSED(lineno); |
343 | |
|
344 | 0 | SymResult * result = static_cast<SymResult *>(data); |
345 | 0 | if (function) |
346 | 0 | { |
347 | 0 | result->name = function; |
348 | 0 | result->found = true; |
349 | 0 | return 1; |
350 | 0 | } |
351 | 0 | return 0; |
352 | 0 | } |
353 | | |
354 | | void Symbolizer::syminfoCallback(void * data, uintptr_t pc, const char * symname, uintptr_t symval, uintptr_t symsize) |
355 | 2 | { |
356 | 2 | SILK_UNUSED(pc); |
357 | 2 | SILK_UNUSED(symval); |
358 | 2 | SILK_UNUSED(symsize); |
359 | | |
360 | 2 | SymResult * result = static_cast<SymResult *>(data); |
361 | 2 | if (symname) |
362 | 2 | { |
363 | 2 | result->name = symname; |
364 | 2 | result->found = true; |
365 | 2 | } |
366 | 2 | } |