blob: 740b15838a13e5d41429318e0b3618fbb5d95812 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015 EfficiOS Inc., Alexandre Montplaisir
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.eclipse.tracecompass.internal.lttng2.ust.core.analysis.debuginfo;
import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils.ScopeLog;
import org.eclipse.tracecompass.common.core.process.ProcessUtils;
import org.eclipse.tracecompass.tmf.core.event.lookup.TmfCallsite;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Iterables;
/**
* Utility class to get file name, function/symbol name and line number from a
* given offset. In TMF this is represented as a {@link TmfCallsite}.
*
* @author Alexandre Montplaisir
*/
public final class FileOffsetMapper {
private static final Logger LOGGER = TraceCompassLog.getLogger(FileOffsetMapper.class);
private static final String DISCRIMINATOR = "\\(discriminator.*\\)"; //$NON-NLS-1$
private static final String ADDR2LINE_EXECUTABLE = "addr2line"; //$NON-NLS-1$
private static final long CACHE_SIZE = 1000;
private FileOffsetMapper() {}
/**
* Class representing an offset in a specific file
*/
private static class FileOffset {
private final String fFilePath;
private final @Nullable String fBuildId;
private final long fOffset;
public FileOffset(String filePath, @Nullable String buildId, long offset) {
fFilePath = filePath;
fBuildId = buildId;
fOffset = offset;
}
@Override
public int hashCode() {
return Objects.hash(fFilePath, fBuildId, fOffset);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
FileOffset other = (FileOffset) obj;
return Objects.equals(fFilePath, other.fFilePath) &&
Objects.equals(fBuildId, other.fBuildId) &&
Objects.equals(fOffset, other.fOffset);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("fFilePath", fFilePath) //$NON-NLS-1$
.append("fBuildId", fBuildId) //$NON-NLS-1$
.append("fOffset", String.format("0x%h", fOffset)) //$NON-NLS-1$ //$NON-NLS-2$
.toString();
}
}
/**
* Generate the callsite from a given binary file and address offset.
*
* Due to function inlining, it is possible for one offset to actually have
* multiple call sites. We will return the most precise one (inner-most) we
* have available.
*
* @param file
* The binary file to look at
* @param buildId
* The expected buildId of the binary file (is not verified at
* the moment)
* @param offset
* The memory offset in the file
* @return The corresponding call site
*/
public static @Nullable TmfCallsite getCallsiteFromOffset(File file, @Nullable String buildId, long offset) {
Iterable<Addr2lineInfo> output = getAddr2lineInfo(file, buildId, offset);
if (output == null || Iterables.isEmpty(output)) {
return null;
}
Addr2lineInfo info = Iterables.getLast(output);
String sourceFile = info.fSourceFileName;
Long sourceLine = info.fSourceLineNumber;
if (sourceFile == null) {
/* Not enough information to provide a callsite */
return null;
}
return new TmfCallsite(sourceFile, sourceLine);
}
/**
* Get the function/symbol name corresponding to binary file and offset.
*
* @param file
* The binary file to look at
* @param buildId
* The expected buildId of the binary file (is not verified at
* the moment)
* @param offset
* The memory offset in the file
* @return The corresponding function/symbol name
*/
public static @Nullable String getFunctionNameFromOffset(File file, @Nullable String buildId, long offset) {
/*
* TODO We are currently also using 'addr2line' to resolve function
* names, which requires the binary's DWARF information to be available.
* A better approach would be to use the binary's symbol table (if it is
* not stripped), since this is usually more readily available than
* DWARF.
*/
Iterable<Addr2lineInfo> output = getAddr2lineInfo(file, buildId, offset);
if (output == null || Iterables.isEmpty(output)) {
return null;
}
Addr2lineInfo info = Iterables.getLast(output);
return info.fFunctionName;
}
// ------------------------------------------------------------------------
// Utility methods making use of 'addr2line'
// ------------------------------------------------------------------------
/**
* Value used in addr2line output to represent unknown function names or
* source files.
*/
private static final String UNKNOWN_VALUE = "??"; //$NON-NLS-1$
/**
* Cache of all calls to 'addr2line', so that we can avoid recalling the
* external process repeatedly.
*
* It is static, meaning one cache for the whole application, since the
* symbols in a file on disk are independent from the trace referring to it.
*/
private static final LoadingCache<FileOffset, @NonNull Iterable<Addr2lineInfo>> ADDR2LINE_INFO_CACHE;
static {
ADDR2LINE_INFO_CACHE = checkNotNull(CacheBuilder.newBuilder()
.maximumSize(CACHE_SIZE)
.build(new CacheLoader<FileOffset, @NonNull Iterable<Addr2lineInfo>>() {
@Override
public @NonNull Iterable<Addr2lineInfo> load(FileOffset fo) {
try (ScopeLog sl = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINER, "FileOffsetMapper:CacheMiss", //$NON-NLS-1$
"File", fo.fFilePath, //$NON-NLS-1$
"Offset", fo.fOffset, //$NON-NLS-1$
"Build id", fo.fBuildId)) { //$NON-NLS-1$
return callAddr2line(fo);
}
}
}));
}
private static class Addr2lineInfo {
private final @Nullable String fSourceFileName;
private final @Nullable Long fSourceLineNumber;
private final @Nullable String fFunctionName;
public Addr2lineInfo(@Nullable String sourceFileName, @Nullable String functionName, @Nullable Long sourceLineNumber) {
fSourceFileName = sourceFileName;
fSourceLineNumber = sourceLineNumber;
fFunctionName = functionName;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("fSourceFileName", fSourceFileName) //$NON-NLS-1$
.append("fSourceLineNumber", fSourceLineNumber) //$NON-NLS-1$
.append("fFunctionName", fFunctionName) //$NON-NLS-1$
.toString();
}
}
private static @Nullable Iterable<Addr2lineInfo> getAddr2lineInfo(File file, @Nullable String buildId, long offset) {
try (ScopeLog sl = new TraceCompassLogUtils.ScopeLog(LOGGER, Level.FINER, "FileOffsetMapper:Addr2lineInfo", //$NON-NLS-1$
"File", file, //$NON-NLS-1$
"Offset", offset, //$NON-NLS-1$
"Build id", buildId)) { //$NON-NLS-1$
/*
* TODO We should also eventually verify that the passed buildId
* matches the file we are attempting to open.
*/
FileOffset fo = new FileOffset(checkNotNull(file.toString()), buildId, offset);
@Nullable
Iterable<Addr2lineInfo> callsites = ADDR2LINE_INFO_CACHE.getUnchecked(fo);
sl.addData("callsites", callsites); //$NON-NLS-1$
return callsites;
}
}
private static Iterable<Addr2lineInfo> callAddr2line(FileOffset fo) {
String filePath = fo.fFilePath;
long offset = fo.fOffset;
List<Addr2lineInfo> callsites = new LinkedList<>();
// FIXME Could eventually use CDT's Addr2line class once it implements --inlines
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$
List<String> output = ProcessUtils.getOutputFromCommand(command);
if (output == null) {
/* Command returned an error */
return Collections.emptySet();
}
/*
* When passing the -f flag, the output alternates between function
* names and file/line location.
*/
boolean oddLine = false; // We flip at the start, first loop will be odd
String currentFunctionName = null;
for (String outputLine : output) {
/* Flip the boolean for the following line */
oddLine = !oddLine;
// Remove discriminator part, for example: /build/buildd/glibc-2.21/elf/dl-object.c:78 (discriminator 8)
outputLine = outputLine.replaceFirst(DISCRIMINATOR, "").trim(); //$NON-NLS-1$
if (oddLine) {
/* This is a line indicating the function name */
if (outputLine.equals(UNKNOWN_VALUE)) {
currentFunctionName = null;
} else {
currentFunctionName = outputLine;
}
} else {
/* This is a line indicating a call site */
String[] elems = outputLine.split(":"); //$NON-NLS-1$
String fileName = elems[0];
if (fileName.equals(UNKNOWN_VALUE)) {
fileName = null;
}
Long lineNumber;
try {
lineNumber = Long.valueOf(elems[1]);
} catch (NumberFormatException e) {
/* Probably a '?' output, meaning unknown line number. */
lineNumber = null;
}
callsites.add(new Addr2lineInfo(fileName, currentFunctionName, lineNumber));
}
}
return callsites;
}
}