blob: cd92d7ae2e06c7d8a8218802f7309a7c673cc75f [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Wrapper around ndk tools used for symbolization.
library symbolizer.ndk;
import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
final _log = Logger('tools');
class Ndk {
final LlvmTools llvmTools;
Ndk({required this.llvmTools});
/// Return information about .text section from the given [object] file.
Future<SectionInfo> getTextSectionInfo(String object) async {
final result =
await _run(llvmTools.readobj, ['--elf-output-style=GNU', '-l', object]);
for (var match in _loadCommandPattern.allMatches(result)) {
if (match.namedGroup('flags')!.trim() == 'R E') {
// Found .text section
return SectionInfo(
fileOffset: int.parse(match.namedGroup('offset')!),
fileSize: int.parse(match.namedGroup('filesz')!),
virtualAddress: int.parse(match.namedGroup('vma')!),
);
}
}
throw 'Failed to find LOAD command for text section in $object';
}
/// Return build-id of the given [object] file.
Future<String> getBuildId(String object) async {
final result =
await _run(llvmTools.readobj, ['--elf-output-style=GNU', '-n', object]);
final match = _buildIdPattern.firstMatch(result);
if (match == null) {
throw 'Failed to extract build-id from $object';
}
return match.namedGroup('id')!;
}
/// Symbolize given [addresses] using information available for the specified
/// [arch] in the given [object] file.
Future<List<String>> symbolize(
{required String object,
required List<String> addresses,
String? arch}) async {
final result = await _run(llvmTools.symbolizer, [
if (arch != null) '--default-arch=$arch',
'--obj',
object,
'--inlines',
...addresses
]);
final symbolized = result.replaceAllMapped(_builderPathPattern, (m) {
return p.relative(p.normalize(m[0]!),
from: (m as RegExpMatch).namedGroup('root'));
}).split('\n\n');
if (symbolized.last == '') symbolized.length--;
return symbolized;
}
/// Runs `llvm-objdump` on the given [object] and returns all lines produced
/// by it.
Stream<String> objdump({
required String object,
required String arch,
}) async* {
final process =
await Process.start(llvmTools.objdump, ['--arch=$arch', '-d', object]);
await for (var line in process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())) {
yield line;
}
}
/// Run the given binary and return its stdout output if the run is
/// successful. Otherwise throw an error.
static Future<String> _run(String binary, List<String> args) async {
final result = await Process.run(binary, args);
if (result.exitCode != 0) {
throw 'Failed to run ${p.basename(binary)} (${result.exitCode}):\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}';
}
return result.stdout;
}
}
/// Information about a section extracted from ELF file.
class SectionInfo {
final int fileOffset;
final int fileSize;
final int virtualAddress;
SectionInfo({
required this.fileOffset,
required this.fileSize,
required this.virtualAddress,
});
}
class LlvmTools {
final String symbolizer;
final String objdump;
final String readobj;
LlvmTools._({
required this.symbolizer,
required this.readobj,
required this.objdump,
});
@override
String toString() => 'LlvmTools($symbolizer, $objdump, $readobj)';
static LlvmTools? _at(String root) {
_log.info(
'checking for llvm-{symbolizer,readobj,objdump} in ${root.isEmpty ? '\$PATH' : root}');
final tools = LlvmTools._(
symbolizer: p.join(root, 'llvm-symbolizer'),
readobj: p.join(root, 'llvm-readobj'),
objdump: p.join(root, 'llvm-objdump'));
return tools._checkOperational() ? tools : null;
}
bool _checkOperational() {
bool checkBinary(String path) {
try {
final result = Process.runSync(path, ['--help']);
return result.exitCode == 0 && result.stdout.startsWith('OVERVIEW: ');
} catch (_) {
return false;
}
}
return checkBinary(symbolizer) &&
checkBinary(objdump) &&
checkBinary(readobj);
}
static LlvmTools? findTools() {
final homeDir = Platform.environment['HOME'] ?? '';
return LlvmTools._at('') ??
_ndkTools('tools/android-ndk') ??
_tryFindNdkAt(p.join(homeDir, 'Library/Android/sdk/ndk')) ??
_tryFindNdkAt(p.join(homeDir, 'Android/Sdk/ndk'));
}
static LlvmTools? _tryFindNdkAt(String path) {
try {
final ndksDir = Directory(path);
int majorVersion(String v) {
return int.tryParse(v.split('.').first) ?? 0;
}
final ndkVersions = [
for (var entry in ndksDir.listSync())
if (entry is Directory) p.basename(entry.path),
]..sort((a, b) => majorVersion(b).compareTo(majorVersion(a)));
return ndkVersions
.map((name) => _ndkTools(p.join(path, name)))
.whereType<LlvmTools>()
.firstOrNull;
} catch (_) {
return null;
}
}
static LlvmTools? _ndkTools(String path) {
final platform = Platform.isLinux ? 'linux' : 'darwin';
return LlvmTools._at(
'$path/toolchains/llvm/prebuilt/$platform-x86_64/bin/');
}
}
final _buildIdPattern = RegExp(r'Build ID:\s+(?<id>[0-9a-f]+)');
final _loadCommandPattern = RegExp(
r'^\s+LOAD\s+(?<offset>0x[0-9a-f]+)\s+(?<vma>0x[0-9a-f]+)\s+(?<phys>0x[0-9a-f]+)\s+(?<filesz>0x[0-9a-f]+)\s+(?<memsz>0x[0-9a-f]+)\s+(?<flags>[^0]+)\s+0x[0-9a-f]+\s*$',
multiLine: true);
final _builderPathPattern =
RegExp(r'^(?<root>/(b|opt)/s/w/[\w/]+/src)/out/\S+', multiLine: true);