Commit | Line | Data |
---|---|---|
522dff53 AM |
1 | /******************************************************************************* |
2 | * Copyright (c) 2015 EfficiOS Inc., Alexandre Montplaisir | |
3 | * | |
4 | * All rights reserved. This program and the accompanying materials | |
5 | * are made available under the terms of the Eclipse Public License v1.0 | |
6 | * which accompanies this distribution, and is available at | |
7 | * http://www.eclipse.org/legal/epl-v10.html | |
8 | *******************************************************************************/ | |
9 | ||
10 | package org.eclipse.tracecompass.internal.lttng2.ust.core.analysis.debuginfo; | |
11 | ||
4d60469d AM |
12 | import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; |
13 | ||
522dff53 AM |
14 | import java.io.BufferedReader; |
15 | import java.io.File; | |
16 | import java.io.IOException; | |
17 | import java.io.InputStreamReader; | |
18 | import java.nio.file.Files; | |
19 | import java.util.Arrays; | |
20 | import java.util.LinkedList; | |
21 | import java.util.List; | |
ba50b376 | 22 | import java.util.logging.Logger; |
522dff53 AM |
23 | import java.util.stream.Collectors; |
24 | ||
25 | import org.eclipse.jdt.annotation.Nullable; | |
ba50b376 | 26 | import org.eclipse.tracecompass.common.core.log.TraceCompassLog; |
3335f36e | 27 | import org.eclipse.tracecompass.lttng2.ust.core.analysis.debuginfo.SourceCallsite; |
522dff53 AM |
28 | import org.eclipse.tracecompass.tmf.core.event.lookup.TmfCallsite; |
29 | ||
4d60469d AM |
30 | import com.google.common.base.Objects; |
31 | import com.google.common.cache.CacheBuilder; | |
32 | import com.google.common.cache.CacheLoader; | |
33 | import com.google.common.cache.LoadingCache; | |
34 | ||
522dff53 AM |
35 | /** |
36 | * Utility class to get file name, function/symbol name and line number from a | |
37 | * given offset. In TMF this is represented as a {@link TmfCallsite}. | |
38 | * | |
39 | * @author Alexandre Montplaisir | |
40 | */ | |
41 | public final class FileOffsetMapper { | |
42 | ||
ba50b376 AM |
43 | private static final Logger LOGGER = TraceCompassLog.getLogger(FileOffsetMapper.class); |
44 | ||
23a8deea | 45 | private static final String DISCRIMINATOR = "\\(discriminator.*\\)"; //$NON-NLS-1$ |
522dff53 AM |
46 | private static final String ADDR2LINE_EXECUTABLE = "addr2line"; //$NON-NLS-1$ |
47 | ||
4d60469d AM |
48 | private static final long CACHE_SIZE = 1000; |
49 | ||
522dff53 AM |
50 | private FileOffsetMapper() {} |
51 | ||
4d60469d AM |
52 | /** |
53 | * Class representing an offset in a specific file | |
54 | */ | |
55 | private static class FileOffset { | |
56 | ||
57 | private final String fFilePath; | |
1633ee0d | 58 | private final @Nullable String fBuildId; |
4d60469d AM |
59 | private final long fOffset; |
60 | ||
1633ee0d | 61 | public FileOffset(String filePath, @Nullable String buildId, long offset) { |
4d60469d | 62 | fFilePath = filePath; |
c84cc3cc | 63 | fBuildId = buildId; |
4d60469d AM |
64 | fOffset = offset; |
65 | } | |
66 | ||
67 | @Override | |
68 | public int hashCode() { | |
c84cc3cc | 69 | return Objects.hashCode(fFilePath, fBuildId, fOffset); |
4d60469d AM |
70 | } |
71 | ||
72 | @Override | |
73 | public boolean equals(@Nullable Object obj) { | |
74 | if (this == obj) { | |
75 | return true; | |
76 | } | |
77 | if (obj == null) { | |
78 | return false; | |
79 | } | |
80 | if (getClass() != obj.getClass()) { | |
81 | return false; | |
82 | } | |
83 | FileOffset other = (FileOffset) obj; | |
c84cc3cc AM |
84 | return Objects.equal(fFilePath, other.fFilePath) && |
85 | Objects.equal(fBuildId, other.fBuildId) && | |
86 | Objects.equal(fOffset, other.fOffset); | |
4d60469d | 87 | } |
ba50b376 AM |
88 | |
89 | @Override | |
90 | public String toString() { | |
91 | return Objects.toStringHelper(this) | |
92 | .add("fFilePath", fFilePath) //$NON-NLS-1$ | |
93 | .add("fBuildId", fBuildId) //$NON-NLS-1$ | |
94 | .add("fOffset", String.format("0x%h", fOffset)) //$NON-NLS-1$ //$NON-NLS-2$ | |
95 | .toString(); | |
96 | } | |
4d60469d AM |
97 | } |
98 | ||
99 | /** | |
100 | * Cache of all calls to 'addr2line', so that we can avoid recalling the | |
101 | * external process repeatedly. | |
102 | * | |
103 | * It is static, meaning one cache for the whole application, since the | |
104 | * symbols in a file on disk are independent from the trace referring to it. | |
105 | */ | |
3335f36e | 106 | private static final LoadingCache<FileOffset, @Nullable Iterable<SourceCallsite>> CALLSITE_CACHE; |
4d60469d AM |
107 | static { |
108 | CALLSITE_CACHE = checkNotNull(CacheBuilder.newBuilder() | |
109 | .maximumSize(CACHE_SIZE) | |
3335f36e | 110 | .build(new CacheLoader<FileOffset, @Nullable Iterable<SourceCallsite>>() { |
4d60469d | 111 | @Override |
3335f36e | 112 | public @Nullable Iterable<SourceCallsite> load(FileOffset fo) { |
ba50b376 | 113 | LOGGER.fine(() -> "[FileOffsetMapper:CacheMiss] file/offset=" + fo.toString()); //$NON-NLS-1$ |
4d60469d AM |
114 | return getCallsiteFromOffsetWithAddr2line(fo); |
115 | } | |
116 | })); | |
117 | } | |
118 | ||
522dff53 AM |
119 | /** |
120 | * Generate the callsites from a given binary file and address offset. | |
121 | * | |
122 | * Due to function inlining, it is possible for one offset to actually have | |
123 | * multiple call sites. This is why we can return more than one callsite per | |
124 | * call. | |
125 | * | |
126 | * @param file | |
127 | * The binary file to look at | |
c84cc3cc AM |
128 | * @param buildId |
129 | * The expected buildId of the binary file (is not verified at | |
130 | * the moment) | |
522dff53 AM |
131 | * @param offset |
132 | * The memory offset in the file | |
133 | * @return The list of callsites corresponding to the offset, reported from | |
134 | * the "highest" inlining location, down to the initial definition. | |
135 | */ | |
1633ee0d | 136 | public static @Nullable Iterable<SourceCallsite> getCallsiteFromOffset(File file, @Nullable String buildId, long offset) { |
ba50b376 AM |
137 | LOGGER.finer(() -> String.format("[FileOffsetMapper:Request] file=%s, buildId=%s, offset=0x%h", //$NON-NLS-1$ |
138 | file.toString(), buildId, offset)); | |
139 | ||
522dff53 | 140 | if (!Files.exists((file.toPath()))) { |
ba50b376 | 141 | LOGGER.finer(() -> "[FileOffsetMapper:RequestFailed] File not found"); //$NON-NLS-1$ |
11f39f99 | 142 | return null; |
522dff53 | 143 | } |
c84cc3cc AM |
144 | // TODO We should also eventually verify that the passed buildId matches |
145 | // the file we are attempting to open. | |
c84cc3cc | 146 | FileOffset fo = new FileOffset(checkNotNull(file.toString()), buildId, offset); |
ba50b376 AM |
147 | |
148 | Iterable<SourceCallsite> callsites = CALLSITE_CACHE.getUnchecked(fo); | |
149 | LOGGER.finer(() -> String.format("[FileOffsetMapper:RequestComplete] callsites=%s", callsites)); //$NON-NLS-1$ | |
150 | return callsites; | |
522dff53 AM |
151 | } |
152 | ||
3335f36e | 153 | private static @Nullable Iterable<SourceCallsite> getCallsiteFromOffsetWithAddr2line(FileOffset fo) { |
4d60469d AM |
154 | String filePath = fo.fFilePath; |
155 | long offset = fo.fOffset; | |
156 | ||
3335f36e | 157 | List<SourceCallsite> callsites = new LinkedList<>(); |
522dff53 | 158 | |
38c5f989 | 159 | // FIXME Could eventually use CDT's Addr2line class once it implements --inlines |
0e4f957e | 160 | List<String> output = getOutputFromCommand(Arrays.asList( |
4d60469d | 161 | ADDR2LINE_EXECUTABLE, "-i", "-f", "-C", "-e", filePath, "0x" + Long.toHexString(offset))); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ |
522dff53 AM |
162 | |
163 | if (output == null) { | |
164 | /* Command returned an error */ | |
165 | return null; | |
166 | } | |
167 | ||
38c5f989 AM |
168 | /* |
169 | * When passing the -f flag, the output alternates between function | |
170 | * names and file/line location. | |
171 | */ | |
1da28b13 | 172 | boolean oddLine = false; // We flip at the start, first loop will be odd |
38c5f989 | 173 | String currentFunctionName = null; |
522dff53 | 174 | for (String outputLine : output) { |
1da28b13 AM |
175 | /* Flip the boolean for the following line */ |
176 | oddLine = !oddLine; | |
177 | ||
91fdda3e MAL |
178 | // Remove discriminator part, for example: /build/buildd/glibc-2.21/elf/dl-object.c:78 (discriminator 8) |
179 | outputLine = outputLine.replaceFirst(DISCRIMINATOR, "").trim(); //$NON-NLS-1$ | |
180 | ||
38c5f989 AM |
181 | if (oddLine) { |
182 | /* This is a line indicating the function name */ | |
183 | currentFunctionName = outputLine; | |
184 | } else { | |
185 | /* This is a line indicating a call site */ | |
186 | String[] elems = outputLine.split(":"); //$NON-NLS-1$ | |
187 | String fileName = elems[0]; | |
188 | if (fileName.equals("??")) { //$NON-NLS-1$ | |
189 | continue; | |
190 | } | |
1da28b13 AM |
191 | try { |
192 | long lineNumber = Long.parseLong(elems[1]); | |
193 | callsites.add(new SourceCallsite(fileName, currentFunctionName, lineNumber)); | |
194 | ||
195 | } catch (NumberFormatException e) { | |
196 | /* | |
197 | * Probably a '?' output, meaning unknown line number. | |
198 | * Ignore this entry. | |
199 | */ | |
200 | continue; | |
201 | } | |
522dff53 | 202 | } |
522dff53 AM |
203 | } |
204 | ||
205 | return callsites; | |
206 | } | |
207 | ||
208 | private static @Nullable List<String> getOutputFromCommand(List<String> command) { | |
209 | try { | |
210 | ProcessBuilder builder = new ProcessBuilder(command); | |
211 | builder.redirectErrorStream(true); | |
212 | ||
213 | Process p = builder.start(); | |
a6c5c267 MK |
214 | try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));) { |
215 | int ret = p.waitFor(); | |
216 | List<String> lines = br.lines().collect(Collectors.toList()); | |
522dff53 | 217 | |
a6c5c267 MK |
218 | return (ret == 0 ? lines : null); |
219 | } | |
522dff53 AM |
220 | } catch (IOException | InterruptedException e) { |
221 | return null; | |
222 | } | |
223 | } | |
224 | } |