Coverage Report

Created: 2026-08-24 19:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
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
}