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 | 14 | import java.io.File; |
522dff53 AM |
15 | import java.nio.file.Files; |
16 | import java.util.Arrays; | |
0c65e461 | 17 | import java.util.Collections; |
522dff53 AM |
18 | import java.util.LinkedList; |
19 | import java.util.List; | |
ba50b376 | 20 | import java.util.logging.Logger; |
522dff53 | 21 | |
0c65e461 | 22 | import org.eclipse.jdt.annotation.NonNull; |
522dff53 | 23 | import org.eclipse.jdt.annotation.Nullable; |
ba50b376 | 24 | import org.eclipse.tracecompass.common.core.log.TraceCompassLog; |
4bd7cc77 | 25 | import org.eclipse.tracecompass.common.core.process.ProcessUtils; |
522dff53 AM |
26 | import org.eclipse.tracecompass.tmf.core.event.lookup.TmfCallsite; |
27 | ||
4d60469d AM |
28 | import com.google.common.base.Objects; |
29 | import com.google.common.cache.CacheBuilder; | |
30 | import com.google.common.cache.CacheLoader; | |
31 | import com.google.common.cache.LoadingCache; | |
0c65e461 | 32 | import com.google.common.collect.Iterables; |
4d60469d | 33 | |
522dff53 AM |
34 | /** |
35 | * Utility class to get file name, function/symbol name and line number from a | |
36 | * given offset. In TMF this is represented as a {@link TmfCallsite}. | |
37 | * | |
38 | * @author Alexandre Montplaisir | |
39 | */ | |
40 | public final class FileOffsetMapper { | |
41 | ||
ba50b376 AM |
42 | private static final Logger LOGGER = TraceCompassLog.getLogger(FileOffsetMapper.class); |
43 | ||
23a8deea | 44 | private static final String DISCRIMINATOR = "\\(discriminator.*\\)"; //$NON-NLS-1$ |
522dff53 AM |
45 | private static final String ADDR2LINE_EXECUTABLE = "addr2line"; //$NON-NLS-1$ |
46 | ||
4d60469d AM |
47 | private static final long CACHE_SIZE = 1000; |
48 | ||
522dff53 AM |
49 | private FileOffsetMapper() {} |
50 | ||
4d60469d AM |
51 | /** |
52 | * Class representing an offset in a specific file | |
53 | */ | |
54 | private static class FileOffset { | |
55 | ||
56 | private final String fFilePath; | |
1633ee0d | 57 | private final @Nullable String fBuildId; |
4d60469d AM |
58 | private final long fOffset; |
59 | ||
1633ee0d | 60 | public FileOffset(String filePath, @Nullable String buildId, long offset) { |
4d60469d | 61 | fFilePath = filePath; |
c84cc3cc | 62 | fBuildId = buildId; |
4d60469d AM |
63 | fOffset = offset; |
64 | } | |
65 | ||
66 | @Override | |
67 | public int hashCode() { | |
c84cc3cc | 68 | return Objects.hashCode(fFilePath, fBuildId, fOffset); |
4d60469d AM |
69 | } |
70 | ||
71 | @Override | |
72 | public boolean equals(@Nullable Object obj) { | |
73 | if (this == obj) { | |
74 | return true; | |
75 | } | |
76 | if (obj == null) { | |
77 | return false; | |
78 | } | |
79 | if (getClass() != obj.getClass()) { | |
80 | return false; | |
81 | } | |
82 | FileOffset other = (FileOffset) obj; | |
c84cc3cc AM |
83 | return Objects.equal(fFilePath, other.fFilePath) && |
84 | Objects.equal(fBuildId, other.fBuildId) && | |
85 | Objects.equal(fOffset, other.fOffset); | |
4d60469d | 86 | } |
ba50b376 AM |
87 | |
88 | @Override | |
89 | public String toString() { | |
90 | return Objects.toStringHelper(this) | |
91 | .add("fFilePath", fFilePath) //$NON-NLS-1$ | |
92 | .add("fBuildId", fBuildId) //$NON-NLS-1$ | |
93 | .add("fOffset", String.format("0x%h", fOffset)) //$NON-NLS-1$ //$NON-NLS-2$ | |
94 | .toString(); | |
95 | } | |
4d60469d AM |
96 | } |
97 | ||
0c65e461 | 98 | |
4d60469d | 99 | /** |
0c65e461 | 100 | * Generate the callsite from a given binary file and address offset. |
4d60469d | 101 | * |
0c65e461 AM |
102 | * Due to function inlining, it is possible for one offset to actually have |
103 | * multiple call sites. We will return the most precise one (inner-most) we | |
104 | * have available. | |
105 | * | |
106 | * @param file | |
107 | * The binary file to look at | |
108 | * @param buildId | |
109 | * The expected buildId of the binary file (is not verified at | |
110 | * the moment) | |
111 | * @param offset | |
112 | * The memory offset in the file | |
113 | * @return The corresponding call site | |
4d60469d | 114 | */ |
0c65e461 AM |
115 | public static @Nullable TmfCallsite getCallsiteFromOffset(File file, @Nullable String buildId, long offset) { |
116 | Iterable<Addr2lineInfo> output = getAddr2lineInfo(file, buildId, offset); | |
117 | if (output == null || Iterables.isEmpty(output)) { | |
118 | return null; | |
119 | } | |
120 | Addr2lineInfo info = Iterables.getLast(output); | |
121 | String sourceFile = info.fSourceFileName; | |
122 | Long sourceLine = info.fSourceLineNumber; | |
123 | ||
124 | if (sourceFile == null) { | |
125 | /* Not enough information to provide a callsite */ | |
126 | return null; | |
127 | } | |
128 | return new TmfCallsite(sourceFile, sourceLine); | |
4d60469d AM |
129 | } |
130 | ||
522dff53 | 131 | /** |
0c65e461 | 132 | * Get the function/symbol name corresponding to binary file and offset. |
522dff53 AM |
133 | * |
134 | * @param file | |
135 | * The binary file to look at | |
c84cc3cc AM |
136 | * @param buildId |
137 | * The expected buildId of the binary file (is not verified at | |
138 | * the moment) | |
522dff53 AM |
139 | * @param offset |
140 | * The memory offset in the file | |
0c65e461 AM |
141 | * @return The corresponding function/symbol name |
142 | */ | |
143 | public static @Nullable String getFunctionNameFromOffset(File file, @Nullable String buildId, long offset) { | |
144 | /* | |
145 | * TODO We are currently also using 'addr2line' to resolve function | |
146 | * names, which requires the binary's DWARF information to be available. | |
147 | * A better approach would be to use the binary's symbol table (if it is | |
148 | * not stripped), since this is usually more readily available than | |
149 | * DWARF. | |
150 | */ | |
151 | Iterable<Addr2lineInfo> output = getAddr2lineInfo(file, buildId, offset); | |
152 | if (output == null || Iterables.isEmpty(output)) { | |
153 | return null; | |
154 | } | |
155 | Addr2lineInfo info = Iterables.getLast(output); | |
156 | return info.fFunctionName; | |
157 | } | |
158 | ||
159 | // ------------------------------------------------------------------------ | |
160 | // Utility methods making use of 'addr2line' | |
161 | // ------------------------------------------------------------------------ | |
162 | ||
574eedca AM |
163 | /** |
164 | * Value used in addr2line output to represent unknown function names or | |
165 | * source files. | |
166 | */ | |
167 | private static final String UNKNOWN_VALUE = "??"; //$NON-NLS-1$ | |
168 | ||
0c65e461 AM |
169 | /** |
170 | * Cache of all calls to 'addr2line', so that we can avoid recalling the | |
171 | * external process repeatedly. | |
172 | * | |
173 | * It is static, meaning one cache for the whole application, since the | |
174 | * symbols in a file on disk are independent from the trace referring to it. | |
522dff53 | 175 | */ |
0c65e461 AM |
176 | private static final LoadingCache<FileOffset, @NonNull Iterable<Addr2lineInfo>> ADDR2LINE_INFO_CACHE; |
177 | static { | |
178 | ADDR2LINE_INFO_CACHE = checkNotNull(CacheBuilder.newBuilder() | |
179 | .maximumSize(CACHE_SIZE) | |
180 | .build(new CacheLoader<FileOffset, @NonNull Iterable<Addr2lineInfo>>() { | |
181 | @Override | |
182 | public @NonNull Iterable<Addr2lineInfo> load(FileOffset fo) { | |
183 | LOGGER.fine(() -> "[FileOffsetMapper:CacheMiss] file/offset=" + fo.toString()); //$NON-NLS-1$ | |
184 | return callAddr2line(fo); | |
185 | } | |
186 | })); | |
187 | } | |
188 | ||
189 | private static class Addr2lineInfo { | |
190 | ||
191 | private final @Nullable String fSourceFileName; | |
192 | private final @Nullable Long fSourceLineNumber; | |
193 | private final @Nullable String fFunctionName; | |
194 | ||
195 | public Addr2lineInfo(@Nullable String sourceFileName, @Nullable String functionName, @Nullable Long sourceLineNumber) { | |
196 | fSourceFileName = sourceFileName; | |
197 | fSourceLineNumber = sourceLineNumber; | |
198 | fFunctionName = functionName; | |
199 | } | |
200 | ||
201 | @Override | |
202 | public String toString() { | |
203 | return Objects.toStringHelper(this) | |
204 | .add("fSourceFileName", fSourceFileName) //$NON-NLS-1$ | |
205 | .add("fSourceLineNumber", fSourceLineNumber) //$NON-NLS-1$ | |
206 | .add("fFunctionName", fFunctionName) //$NON-NLS-1$ | |
207 | .toString(); | |
208 | } | |
209 | } | |
210 | ||
211 | private static @Nullable Iterable<Addr2lineInfo> getAddr2lineInfo(File file, @Nullable String buildId, long offset) { | |
212 | LOGGER.finer(() -> String.format("[FileOffsetMapper:Addr2lineRequest] file=%s, buildId=%s, offset=0x%h", //$NON-NLS-1$ | |
ba50b376 AM |
213 | file.toString(), buildId, offset)); |
214 | ||
522dff53 | 215 | if (!Files.exists((file.toPath()))) { |
ba50b376 | 216 | LOGGER.finer(() -> "[FileOffsetMapper:RequestFailed] File not found"); //$NON-NLS-1$ |
11f39f99 | 217 | return null; |
522dff53 | 218 | } |
c84cc3cc AM |
219 | // TODO We should also eventually verify that the passed buildId matches |
220 | // the file we are attempting to open. | |
c84cc3cc | 221 | FileOffset fo = new FileOffset(checkNotNull(file.toString()), buildId, offset); |
ba50b376 | 222 | |
0c65e461 | 223 | @Nullable Iterable<Addr2lineInfo> callsites = ADDR2LINE_INFO_CACHE.getUnchecked(fo); |
ba50b376 AM |
224 | LOGGER.finer(() -> String.format("[FileOffsetMapper:RequestComplete] callsites=%s", callsites)); //$NON-NLS-1$ |
225 | return callsites; | |
522dff53 AM |
226 | } |
227 | ||
0c65e461 | 228 | private static Iterable<Addr2lineInfo> callAddr2line(FileOffset fo) { |
4d60469d AM |
229 | String filePath = fo.fFilePath; |
230 | long offset = fo.fOffset; | |
231 | ||
0c65e461 | 232 | List<Addr2lineInfo> callsites = new LinkedList<>(); |
522dff53 | 233 | |
38c5f989 | 234 | // FIXME Could eventually use CDT's Addr2line class once it implements --inlines |
4bd7cc77 AM |
235 | List<String> command = Arrays.asList(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$ |
236 | List<String> output = ProcessUtils.getOutputFromCommand(command); | |
522dff53 AM |
237 | |
238 | if (output == null) { | |
239 | /* Command returned an error */ | |
0c65e461 | 240 | return Collections.EMPTY_SET; |
522dff53 AM |
241 | } |
242 | ||
38c5f989 AM |
243 | /* |
244 | * When passing the -f flag, the output alternates between function | |
245 | * names and file/line location. | |
246 | */ | |
1da28b13 | 247 | boolean oddLine = false; // We flip at the start, first loop will be odd |
38c5f989 | 248 | String currentFunctionName = null; |
522dff53 | 249 | for (String outputLine : output) { |
1da28b13 AM |
250 | /* Flip the boolean for the following line */ |
251 | oddLine = !oddLine; | |
252 | ||
91fdda3e MAL |
253 | // Remove discriminator part, for example: /build/buildd/glibc-2.21/elf/dl-object.c:78 (discriminator 8) |
254 | outputLine = outputLine.replaceFirst(DISCRIMINATOR, "").trim(); //$NON-NLS-1$ | |
255 | ||
38c5f989 AM |
256 | if (oddLine) { |
257 | /* This is a line indicating the function name */ | |
574eedca AM |
258 | if (outputLine.equals(UNKNOWN_VALUE)) { |
259 | currentFunctionName = null; | |
260 | } else { | |
261 | currentFunctionName = outputLine; | |
262 | } | |
38c5f989 AM |
263 | } else { |
264 | /* This is a line indicating a call site */ | |
265 | String[] elems = outputLine.split(":"); //$NON-NLS-1$ | |
266 | String fileName = elems[0]; | |
574eedca AM |
267 | if (fileName.equals(UNKNOWN_VALUE)) { |
268 | fileName = null; | |
38c5f989 | 269 | } |
574eedca | 270 | Long lineNumber; |
1da28b13 | 271 | try { |
574eedca | 272 | lineNumber = Long.valueOf(elems[1]); |
1da28b13 | 273 | } catch (NumberFormatException e) { |
574eedca AM |
274 | /* Probably a '?' output, meaning unknown line number. */ |
275 | lineNumber = null; | |
1da28b13 | 276 | } |
574eedca | 277 | callsites.add(new Addr2lineInfo(fileName, currentFunctionName, lineNumber)); |
522dff53 | 278 | } |
522dff53 AM |
279 | } |
280 | ||
281 | return callsites; | |
282 | } | |
522dff53 | 283 | } |