mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2026-05-16 16:02:45 -04:00
We currently retain only StackMap, but InlineInfo is also useful. <!-- THIS COMMENT IS INVISIBLE IN THE FINAL PR, BUT FEEL FREE TO REMOVE IT Thanks for taking the time to improve CE. We really appreciate it. Before opening the PR, please make sure that the tests & linter pass their checks, by running `make check`. In the best case scenario, you are also adding tests to back up your changes, but don't sweat it if you don't. We can discuss them at a later date. Feel free to append your name to the CONTRIBUTORS.md file Thanks again, we really appreciate this! -->
845 lines
32 KiB
TypeScript
845 lines
32 KiB
TypeScript
// Copyright (c) 2023, Compiler Explorer Authors
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
import _ from 'underscore';
|
|
|
|
import type {ParsedAsmResult, ParsedAsmResultLine} from '../../types/asmresult/asmresult.interfaces.js';
|
|
import {CompilationResult, ExecutionOptionsWithEnv} from '../../types/compilation/compilation.interfaces.js';
|
|
import {CompilerOverrideOption, CompilerOverrideType} from '../../types/compilation/compiler-overrides.interfaces.js';
|
|
import type {
|
|
OptPipelineBackendOptions,
|
|
OptPipelineOutput,
|
|
} from '../../types/compilation/opt-pipeline-output.interfaces.js';
|
|
import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
|
|
import type {UnprocessedExecResult} from '../../types/execution/execution.interfaces.js';
|
|
import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.interfaces.js';
|
|
import type {SelectedLibraryVersion} from '../../types/libraries/libraries.interfaces.js';
|
|
import {BaseCompiler, c_value_placeholder} from '../base-compiler.js';
|
|
import {CompilationEnvironment} from '../compilation-env.js';
|
|
import {logger} from '../logger.js';
|
|
import {Dex2OatPassDumpParser} from '../parsers/dex2oat-pass-dump-parser.js';
|
|
import * as utils from '../utils.js';
|
|
import {D8Compiler} from './d8.js';
|
|
|
|
const BOOTCLASSPATH_JARS = [
|
|
'bootjars/core-oj.jar',
|
|
'bootjars/core-libart.jar',
|
|
'bootjars/okhttp.jar',
|
|
'bootjars/bouncycastle.jar',
|
|
'bootjars/apache-xml.jar',
|
|
] as const;
|
|
|
|
const BOOTCLASSPATH_LOCATIONS = [
|
|
'/apex/com.android.art/javalib/core-oj.jar',
|
|
'/apex/com.android.art/javalib/core-libart.jar',
|
|
'/apex/com.android.art/javalib/okhttp.jar',
|
|
'/apex/com.android.art/javalib/bouncycastle.jar',
|
|
'/apex/com.android.art/javalib/apache-xml.jar',
|
|
] as const;
|
|
|
|
export class Dex2OatCompiler extends BaseCompiler {
|
|
readonly fullOutputFlag = '--full-output';
|
|
|
|
static get key() {
|
|
return 'dex2oat';
|
|
}
|
|
|
|
passDumpParser: Dex2OatPassDumpParser;
|
|
|
|
insnSetRegex: RegExp;
|
|
insnSetFeaturesRegex: RegExp;
|
|
|
|
compilerFilterRegex: RegExp;
|
|
classRegex: RegExp;
|
|
methodRegex: RegExp;
|
|
methodSizeRegex: RegExp;
|
|
insnRegex: RegExp;
|
|
extraInfoRegex: RegExp;
|
|
offsetRegex: RegExp;
|
|
|
|
insnSetArgRegex: RegExp;
|
|
compilerFilterArgRegex: RegExp;
|
|
|
|
versionPrefixRegex: RegExp;
|
|
latestVersionRegex: RegExp;
|
|
|
|
fullOutput: boolean;
|
|
|
|
d8Id: string;
|
|
artArtifactDir: string;
|
|
profmanPath: string;
|
|
cwd: string;
|
|
|
|
libs: SelectedLibraryVersion[];
|
|
|
|
smaliLineNumberRegex: RegExp;
|
|
smaliClassRegex: RegExp;
|
|
smaliDexPcRegex: RegExp;
|
|
smaliMethodStartRegex: RegExp;
|
|
smaliMethodEndRegex: RegExp;
|
|
|
|
constructor(compilerInfo: PreliminaryCompilerInfo, env: CompilationEnvironment) {
|
|
super({...compilerInfo}, env);
|
|
this.compiler.optPipeline = {
|
|
arg: ['-print-after-all', '-print-before-all'],
|
|
moduleScopeArg: ['-print-module-scope'],
|
|
noDiscardValueNamesArg: [],
|
|
};
|
|
|
|
this.passDumpParser = new Dex2OatPassDumpParser();
|
|
|
|
// These must be used before the output is split on newlines.
|
|
this.insnSetRegex = /\s*INSTRUCTION\s+SET:\s*\n(.+?)\n/;
|
|
this.insnSetFeaturesRegex = /\s*INSTRUCTION\s+SET\s+FEATURES:\s*\n(.+?)\n/;
|
|
|
|
// These must be used after the output is split on newlines.
|
|
this.compilerFilterRegex = /^compiler-filter\s=\s(.*)$/;
|
|
this.classRegex = /^\s*\d+:\s+L(.*);\s+\(offset=0x\w+\)\s+\(type_idx=\d+\).*$/;
|
|
this.methodRegex = /^\s+\d+:\s+(.*)\s+\(dex_method_idx=\d+\)$/;
|
|
this.methodSizeRegex = /^\s+CODE:\s+\(code_offset=(0x\w+)\s+size=(\d+).*$/;
|
|
this.insnRegex = /^\s+(0x\w+):\s+\w+\s+(.*)$/;
|
|
|
|
this.extraInfoRegex = /^\s+((StackMap|InlineInfo)\[\d+\])\s+\((.*)\).*$/;
|
|
|
|
// Similar to insnRegex above, but this applies after oatdump output has
|
|
// been cleaned up.
|
|
this.offsetRegex = /^\s*(0x\w+)\s+.*$/;
|
|
|
|
// ART version codes in CE are in the format of AABB, where AA is the
|
|
// API level and BB is the number of months since the initial release.
|
|
this.versionPrefixRegex = /^(android-)?(java|kotlin)-dex2oat-(\d\d)\d+$/;
|
|
this.latestVersionRegex = /^(android-)?(java|kotlin)-dex2oat-(latest|default|local)$/;
|
|
|
|
// User-provided arguments (with a default behavior if not provided).
|
|
this.insnSetArgRegex = /^--instruction-set=.*$/;
|
|
this.compilerFilterArgRegex = /^--compiler-filter=.*$/;
|
|
|
|
// Whether the full dex2oat output should be displayed instead of just
|
|
// the parsed and formatted methods.
|
|
this.fullOutput = false;
|
|
|
|
// The underlying D8 version+exe.
|
|
this.d8Id = this.compilerProps<string>(`compiler.${this.compiler.id}.d8Id`);
|
|
|
|
// The directory containing ART artifacts necessary for dex2oat to run.
|
|
this.artArtifactDir = this.compilerProps<string>(`compiler.${this.compiler.id}.artArtifactDir`);
|
|
|
|
// The path to the `profman` binary.
|
|
this.profmanPath = this.compilerProps<string>(`compiler.${this.compiler.id}.profmanPath`);
|
|
|
|
// The path where D8/dex2oat are being run. This is used to find
|
|
// classes.cfg from processAsm().
|
|
this.cwd = '';
|
|
|
|
// Libraries that will flow to D8Compiler and Java/KotlinCompiler.
|
|
this.libs = [];
|
|
|
|
// Regexes that apply to .smali files (R8/D8 dump output).
|
|
this.smaliLineNumberRegex = /^\s+\.line\s+(\d+).*$/;
|
|
this.smaliClassRegex = /^\.class.*\s(L.*;)$/;
|
|
this.smaliDexPcRegex = /^\s+#@(\w+).*$/;
|
|
this.smaliMethodStartRegex = /^\.method\s(.*)$/;
|
|
this.smaliMethodEndRegex = /^\s*\.end\smethod.*$/;
|
|
}
|
|
|
|
override async runCompiler(
|
|
compiler: string,
|
|
options: string[],
|
|
inputFilename: string,
|
|
execOptions: ExecutionOptionsWithEnv,
|
|
filters?: ParseFiltersAndOutputOptions,
|
|
): Promise<CompilationResult> {
|
|
// Make sure --full-output from previous invocations doesn't persist.
|
|
this.fullOutput = false;
|
|
|
|
// Instantiate D8 compiler, which will in turn instantiate a Java or
|
|
// Kotlin compiler based on the current language.
|
|
const d8Compiler = this.env.findCompiler(this.lang.id, this.d8Id) as D8Compiler | undefined;
|
|
if (!d8Compiler) {
|
|
return {
|
|
...this.handleUserError(
|
|
{message: `Compiler ${this.lang.id} ${this.d8Id} not configured correctly`},
|
|
'',
|
|
),
|
|
timedOut: false,
|
|
};
|
|
}
|
|
const d8DirPath = path.dirname(inputFilename);
|
|
const d8OutputFilename = d8Compiler.getOutputFilename(d8DirPath);
|
|
|
|
if (!execOptions) {
|
|
execOptions = this.getDefaultExecOptions();
|
|
}
|
|
|
|
let useDefaultInsnSet = true;
|
|
let useDefaultCompilerFilter = true;
|
|
let d8Flags: string | undefined;
|
|
|
|
// The items in 'options' before the source file are user inputs.
|
|
const sourceFileOptionIndex = options.findIndex(option => {
|
|
return option.endsWith('.java') || option.endsWith('.kt');
|
|
});
|
|
const userOptions: string[] = [];
|
|
for (let i = 0; i < sourceFileOptionIndex; ++i) {
|
|
const option = options[i];
|
|
if (this.insnSetArgRegex.test(option)) {
|
|
useDefaultInsnSet = false;
|
|
userOptions.push(option);
|
|
} else if (this.compilerFilterArgRegex.test(option)) {
|
|
useDefaultCompilerFilter = false;
|
|
userOptions.push(option);
|
|
} else if (option === this.fullOutputFlag) {
|
|
this.fullOutput = true;
|
|
} else if (option === '--d8-flags') {
|
|
d8Flags = options[++i];
|
|
} else {
|
|
userOptions.push(option);
|
|
}
|
|
}
|
|
|
|
const d8Options = _.compact(
|
|
d8Compiler.prepareArguments(
|
|
d8Flags?.split(' ') ?? [],
|
|
d8Compiler.getDefaultFilters(),
|
|
{}, // backendOptions
|
|
inputFilename,
|
|
d8OutputFilename,
|
|
this.libs,
|
|
[], // overrides
|
|
),
|
|
);
|
|
|
|
const compileResult = await d8Compiler.runCompiler(
|
|
d8Compiler.getInfo().exe,
|
|
d8Options,
|
|
this.filename(inputFilename),
|
|
d8Compiler.getDefaultExecOptions(),
|
|
);
|
|
|
|
if (compileResult.code !== 0) {
|
|
return compileResult;
|
|
}
|
|
|
|
const files = await fs.readdir(d8DirPath);
|
|
const dexFile = files.find(f => f.endsWith('.dex'));
|
|
if (!dexFile) {
|
|
throw new Error('Generated dex file not found');
|
|
}
|
|
|
|
await d8Compiler.generateSmali(path.join(d8DirPath, dexFile), 64 * 1024 * 1024);
|
|
|
|
const profileAndResult = await this.generateProfile(d8DirPath, dexFile);
|
|
if (profileAndResult && profileAndResult.result.code !== 0) {
|
|
return {
|
|
...this.transformToCompilationResult(profileAndResult.result, inputFilename),
|
|
languageId: this.getCompilerResultLanguageId(filters),
|
|
};
|
|
}
|
|
|
|
let isLatest = false;
|
|
let versionPrefix = 0;
|
|
let match;
|
|
if (this.versionPrefixRegex.test(this.compiler.id)) {
|
|
match = this.compiler.id.match(this.versionPrefixRegex);
|
|
versionPrefix = Number.parseInt(match![2], 10);
|
|
} else if (this.latestVersionRegex.test(this.compiler.id)) {
|
|
isLatest = true;
|
|
}
|
|
|
|
const dex2oatOptions = [
|
|
'--android-root=include',
|
|
'--generate-debug-info',
|
|
'--dex-location=/system/framework/classes.dex',
|
|
`--dex-file=${d8DirPath}/${dexFile}`,
|
|
'--copy-dex-files=always',
|
|
...(versionPrefix >= 34 || isLatest ? ['--runtime-arg', '-Xgc:CMC'] : []),
|
|
'--runtime-arg',
|
|
'-Xbootclasspath:' + BOOTCLASSPATH_JARS.map(f => path.join(this.artArtifactDir, f)).join(':'),
|
|
'--runtime-arg',
|
|
'-Xbootclasspath-locations:' + BOOTCLASSPATH_LOCATIONS.join(':'),
|
|
`--boot-image=${this.artArtifactDir}/app/system/framework/boot.art`,
|
|
`--oat-file=${d8DirPath}/classes.odex`,
|
|
`--app-image-file=${d8DirPath}/classes.art`,
|
|
'--force-allow-oj-inlines',
|
|
`--dump-cfg=${d8DirPath}/classes.cfg`,
|
|
...userOptions,
|
|
];
|
|
if (useDefaultInsnSet) {
|
|
dex2oatOptions.push('--instruction-set=arm64');
|
|
}
|
|
if (useDefaultCompilerFilter) {
|
|
if (profileAndResult == null) {
|
|
dex2oatOptions.push('--compiler-filter=speed');
|
|
} else {
|
|
dex2oatOptions.push('--compiler-filter=speed-profile');
|
|
}
|
|
}
|
|
if (profileAndResult != null) {
|
|
dex2oatOptions.push(`--profile-file=${profileAndResult.path}`);
|
|
}
|
|
|
|
execOptions.customCwd = d8DirPath;
|
|
this.cwd = d8DirPath;
|
|
|
|
const result = await this.exec(this.compiler.exe, dex2oatOptions, execOptions);
|
|
if (profileAndResult != null) {
|
|
result.stdout = profileAndResult.result.stdout + result.stdout;
|
|
result.stderr = profileAndResult.result.stderr + result.stderr;
|
|
}
|
|
return {
|
|
...this.transformToCompilationResult(result, d8OutputFilename),
|
|
languageId: this.getCompilerResultLanguageId(filters),
|
|
};
|
|
}
|
|
|
|
override getIncludeArguments(libraries: SelectedLibraryVersion[], dirPath: string): string[] {
|
|
this.libs = libraries;
|
|
return super.getIncludeArguments(libraries, dirPath);
|
|
}
|
|
|
|
private async generateProfile(
|
|
d8DirPath: string,
|
|
dexFile: string,
|
|
): Promise<{path: string; result: UnprocessedExecResult} | null> {
|
|
const humanReadableFormatProfile = `${d8DirPath}/profile.prof.txt`;
|
|
try {
|
|
await fs.access(humanReadableFormatProfile);
|
|
} catch {
|
|
// No profile. This is expected.
|
|
return null;
|
|
}
|
|
|
|
const execOptions = this.getDefaultExecOptions();
|
|
execOptions.customCwd = d8DirPath;
|
|
const binaryFormatProfile = `${d8DirPath}/profile.prof`;
|
|
const result = await this.exec(
|
|
this.profmanPath,
|
|
[
|
|
`--create-profile-from=${humanReadableFormatProfile}`,
|
|
`--apk=${d8DirPath}/${dexFile}`,
|
|
'--dex-location=/system/framework/classes.dex',
|
|
...BOOTCLASSPATH_JARS.map(f => `--apk=${path.join(this.artArtifactDir, f)}`),
|
|
...BOOTCLASSPATH_LOCATIONS.map(f => `--dex-location=${f}`),
|
|
`--reference-profile-file=${binaryFormatProfile}`,
|
|
'--output-profile-type=app',
|
|
],
|
|
execOptions,
|
|
);
|
|
|
|
return {path: binaryFormatProfile, result: result};
|
|
}
|
|
|
|
override async objdump(outputFilename: string, result: any, maxSize: number) {
|
|
const dirPath = path.dirname(outputFilename);
|
|
const files = await fs.readdir(dirPath);
|
|
const odexFile = files.find(f => f.endsWith('.odex'));
|
|
const args = [...this.compiler.objdumperArgs, `--oat-file=${odexFile}`];
|
|
const objResult = await this.exec(this.compiler.objdumper, args, {
|
|
maxOutput: maxSize,
|
|
customCwd: dirPath,
|
|
});
|
|
|
|
const asmResult: ParsedAsmResult = {
|
|
asm: [
|
|
{
|
|
text: objResult.stdout,
|
|
},
|
|
],
|
|
};
|
|
if (objResult.code === 0) {
|
|
result.objdumpTime = objResult.execTime;
|
|
} else {
|
|
asmResult.asm = [
|
|
{
|
|
text: `<No output: oatdump returned ${objResult.code}>`,
|
|
},
|
|
];
|
|
}
|
|
result.asm = asmResult.asm;
|
|
return result;
|
|
}
|
|
|
|
override optionsForFilter(filters: ParseFiltersAndOutputOptions) {
|
|
filters.binary = true;
|
|
return [];
|
|
}
|
|
|
|
// dex2oat doesn't have --version, but artArtifactDir contains a file with
|
|
// the build number.
|
|
override async getVersion() {
|
|
logger.info(`Gathering ${this.compiler.id} version information on ${this.compiler.exe}...`);
|
|
const versionFile = this.artArtifactDir + '/snapshot-creation-build-number.txt';
|
|
const version = await fs.readFile(versionFile, {encoding: 'utf8'});
|
|
return {
|
|
stdout: 'Android Build ' + version,
|
|
stderr: '',
|
|
code: 0,
|
|
};
|
|
}
|
|
|
|
// Gets a string of methods parameters in the form used in .smali and
|
|
// splits it.
|
|
// '[Ljava/lang/String;[IIZ' --> ['Ljava/lang/String;', '[I', 'I', 'Z']
|
|
splitMethodParameters(parameters: string): string[] {
|
|
const split: string[] = [];
|
|
let l = 0;
|
|
let r = 0;
|
|
let inReferenceType = false;
|
|
while (l < parameters.length) {
|
|
if (parameters.charAt(r) === '[') {
|
|
r++;
|
|
} else if (parameters.charAt(r) === 'L') {
|
|
inReferenceType = true;
|
|
r++;
|
|
} else if (parameters.charAt(r) === ';') {
|
|
inReferenceType = false;
|
|
split.push(parameters.substring(l, r + 1));
|
|
l = r + 1;
|
|
r = l;
|
|
} else if (inReferenceType) {
|
|
r++;
|
|
} else if (!inReferenceType) {
|
|
// Any character found while not incrementing through the
|
|
// reference type represents a primitive.
|
|
split.push(parameters.substring(l, r + 1));
|
|
l = r + 1;
|
|
r = l;
|
|
}
|
|
}
|
|
return split;
|
|
}
|
|
|
|
prettyDescriptor(descriptor: string): string {
|
|
let prettyOutput = '';
|
|
|
|
let dim = 0;
|
|
let idx = 0;
|
|
|
|
// Determine dimensionality.
|
|
while (descriptor.charAt(idx) === '[') {
|
|
dim++;
|
|
idx++;
|
|
}
|
|
|
|
// Remove leading 'L' for reference types, otherwise translate to
|
|
// primitive type name.
|
|
let primitive = false;
|
|
if (descriptor.charAt(idx) === 'L') {
|
|
idx++;
|
|
prettyOutput = descriptor.slice(idx);
|
|
} else {
|
|
primitive = true;
|
|
switch (descriptor.charAt(idx)) {
|
|
case 'V': {
|
|
prettyOutput = 'void';
|
|
break;
|
|
}
|
|
case 'Z': {
|
|
prettyOutput = 'boolean';
|
|
break;
|
|
}
|
|
case 'B': {
|
|
prettyOutput = 'byte';
|
|
break;
|
|
}
|
|
case 'S': {
|
|
prettyOutput = 'short';
|
|
break;
|
|
}
|
|
case 'C': {
|
|
prettyOutput = 'char';
|
|
break;
|
|
}
|
|
case 'I': {
|
|
prettyOutput = 'int';
|
|
break;
|
|
}
|
|
case 'J': {
|
|
prettyOutput = 'long';
|
|
break;
|
|
}
|
|
case 'F': {
|
|
prettyOutput = 'float';
|
|
break;
|
|
}
|
|
case 'D': {
|
|
prettyOutput = 'double';
|
|
break;
|
|
}
|
|
default: {
|
|
return descriptor;
|
|
}
|
|
}
|
|
}
|
|
|
|
prettyOutput = prettyOutput.replaceAll('/', '.');
|
|
|
|
// Remove trailing ';'.
|
|
if (!primitive) {
|
|
prettyOutput = prettyOutput.substring(0, prettyOutput.length - 1);
|
|
}
|
|
|
|
for (let i = 0; i < dim; i++) {
|
|
prettyOutput += '[]';
|
|
}
|
|
|
|
return prettyOutput;
|
|
}
|
|
|
|
// Converts method signatures in the form of
|
|
// 'methodName([Lsome/ref/Type;[IIZ)Lsome/ref/Type;' to
|
|
// 'some.ref.Type methodName(some.ref.Type[], int[], int, boolean)'.
|
|
// The former is used in .smali files and the latter is used in ART outputs.
|
|
prettyMethodSignature(methodSignature: string): string {
|
|
let prettyOutput = '';
|
|
|
|
// Get just the last word, which is the full method signature in .smali
|
|
// files. This removes preceding strings like 'static', 'constructor',
|
|
// etc.
|
|
const trimmed = methodSignature.split(/\s/).slice(-1)[0];
|
|
|
|
// Matches something like 'methodName(parameterparameter)returnType'.
|
|
const match = trimmed.match(/^(.*)\((.*)\)(.*)$/)!;
|
|
|
|
// 'returnType methodName('
|
|
prettyOutput += this.prettyDescriptor(match[3]) + ' ' + match[1] + '(';
|
|
const parameters = this.splitMethodParameters(match[2]);
|
|
for (let i = 0; i < parameters.length; i++) {
|
|
// 'returnType methodName(parameter, parameter'
|
|
prettyOutput += this.prettyDescriptor(parameters[i]);
|
|
if (i < parameters.length - 1) {
|
|
prettyOutput += ', ';
|
|
}
|
|
}
|
|
// 'returnType methodName(parameter, parameter)'
|
|
prettyOutput += ')';
|
|
return prettyOutput;
|
|
}
|
|
|
|
// Adds the class name that the method is contained in to the method name.
|
|
// 'int square(int)' -> 'int Square.square(int)'.
|
|
insertClassIntoMethodSignature(className: string, methodSignature: string): string {
|
|
const components = methodSignature.split(' ');
|
|
components[1] = className + '.' + components[1];
|
|
return components.join(' ');
|
|
}
|
|
|
|
// Associate each method's dex PCs to line numbers. For example, in this
|
|
// short .smali example below, we should get something like
|
|
// {'int Square.square(int)': {0: 14, 1: 14}}
|
|
//
|
|
// .method static square(I)I
|
|
// .registers 1
|
|
//
|
|
// #@0
|
|
// .line 14
|
|
// mul-int/2addr p0, p0
|
|
//
|
|
// #@1
|
|
// return p0
|
|
// .end method
|
|
parseSmaliForLineNumbers(dexPcsToLines: Record<string, Record<number, number>>, smaliLines: string[]) {
|
|
let className = '';
|
|
let methodSignature = '';
|
|
let lineNumber = -1;
|
|
let dexPc = -1;
|
|
for (const l of smaliLines) {
|
|
if (this.smaliClassRegex.test(l)) {
|
|
className = this.prettyDescriptor(l.match(this.smaliClassRegex)![1]);
|
|
} else if (this.smaliMethodStartRegex.test(l)) {
|
|
methodSignature = this.insertClassIntoMethodSignature(
|
|
className,
|
|
this.prettyMethodSignature(l.match(this.smaliMethodStartRegex)![1]),
|
|
);
|
|
dexPcsToLines[methodSignature] = {};
|
|
} else if (this.smaliMethodEndRegex.test(l)) {
|
|
methodSignature = '';
|
|
lineNumber = -1;
|
|
dexPc = -1;
|
|
} else if (this.smaliLineNumberRegex.test(l)) {
|
|
// Line numbers are given in decimal.
|
|
lineNumber = Number.parseInt(l.match(this.smaliLineNumberRegex)![1], 10);
|
|
dexPcsToLines[methodSignature][dexPc] = lineNumber;
|
|
} else if (this.smaliDexPcRegex.test(l)) {
|
|
// Dex PCs are given in hex.
|
|
dexPc = Number.parseInt(l.match(this.smaliDexPcRegex)![1], 16);
|
|
dexPcsToLines[methodSignature][dexPc] = lineNumber;
|
|
}
|
|
}
|
|
}
|
|
|
|
override async processAsm(result, filters: ParseFiltersAndOutputOptions) {
|
|
let asm = '';
|
|
|
|
if (typeof result.asm === 'string') {
|
|
const asmLines = utils.splitLines(result.asm);
|
|
if (asmLines.length === 1 && asmLines[0][0] === '<') {
|
|
return {
|
|
asm: [{text: asmLines[0], source: null}],
|
|
};
|
|
}
|
|
return {
|
|
asm: [{text: JSON.stringify(asmLines), source: null}],
|
|
};
|
|
}
|
|
// result.asm is an array, but we only expect it to have one value.
|
|
asm = result.asm[0].text;
|
|
|
|
const segments: ParsedAsmResultLine[] = [];
|
|
if (this.fullOutput || !filters.directives) {
|
|
// Returns entire dex2oat output.
|
|
segments.push({text: asm, source: null});
|
|
} else {
|
|
const {
|
|
compileData,
|
|
classNames,
|
|
classToMethods,
|
|
methodsToInstructions,
|
|
methodsToSizes,
|
|
absoluteToRelativeOffsets,
|
|
} = this.parseAsm(asm);
|
|
|
|
const classesCfg = path.join(this.cwd, 'classes.cfg');
|
|
let methodsAndOffsetsToDexPcs: Record<string, Record<number, number>> = {};
|
|
try {
|
|
const rawCfgText = await fs.readFile(classesCfg, {encoding: 'utf8'});
|
|
methodsAndOffsetsToDexPcs = this.passDumpParser.parsePassDumpsForDexPcs(rawCfgText.split(/\n/));
|
|
} catch (e) {
|
|
// This is expected if this is running in a test. If this fails for another reason, we just won't see
|
|
// line highlights.
|
|
segments.push({
|
|
text: `classes.cfg is missing, source lines will not be highlighted: ${e}`,
|
|
source: null,
|
|
});
|
|
}
|
|
|
|
const dexPcsToLines: Record<string, Record<number, number>> = {};
|
|
try {
|
|
const files = await fs.readdir(this.cwd);
|
|
const smaliFiles = files.filter(f => f.endsWith('.smali'));
|
|
for (const smaliFile of smaliFiles) {
|
|
const rawSmaliText = await fs.readFile(path.join(this.cwd, smaliFile), 'utf-8');
|
|
this.parseSmaliForLineNumbers(dexPcsToLines, rawSmaliText.split(/\n/));
|
|
}
|
|
} catch (e) {
|
|
// Same case as above.
|
|
segments.push({
|
|
text: `*.smali is missing, source lines will not be highlighted: ${e}`,
|
|
source: null,
|
|
});
|
|
}
|
|
|
|
segments.push(
|
|
{
|
|
text: 'Instruction set: ' + compileData.insnSet,
|
|
source: null,
|
|
},
|
|
{
|
|
text: 'Instruction set features: ' + compileData.insnSetFeatures,
|
|
source: null,
|
|
},
|
|
{
|
|
text: 'Compiler filter: ' + compileData.compilerFilter,
|
|
source: null,
|
|
},
|
|
{text: '', source: null},
|
|
{text: '', source: null},
|
|
);
|
|
|
|
for (const className of classNames) {
|
|
for (const method of classToMethods[className]) {
|
|
segments.push({
|
|
text: method + ' [' + methodsToSizes[method] + ' bytes]',
|
|
source: null,
|
|
});
|
|
for (const instruction of methodsToInstructions[method]) {
|
|
let absoluteOffset = -1;
|
|
let relativeOffset = -1;
|
|
if (this.offsetRegex.test(instruction)) {
|
|
absoluteOffset = Number.parseInt(instruction.match(this.offsetRegex)![1], 16);
|
|
relativeOffset = absoluteToRelativeOffsets[absoluteOffset];
|
|
}
|
|
const offsetToDexPc = methodsAndOffsetsToDexPcs[method];
|
|
const dexPc = offsetToDexPc ? offsetToDexPc[relativeOffset] : -1;
|
|
const source =
|
|
Number.isInteger(dexPc) && dexPc >= 0 && dexPcsToLines[method]
|
|
? {file: null, line: dexPcsToLines[method][dexPc]}
|
|
: null;
|
|
segments.push({
|
|
text: ' ' + instruction,
|
|
source: source,
|
|
});
|
|
}
|
|
segments.push({text: '', source: null});
|
|
}
|
|
}
|
|
}
|
|
|
|
return {asm: segments};
|
|
}
|
|
|
|
parseAsm(oatdumpOut: string) {
|
|
const compileData: {
|
|
insnSet?: string;
|
|
insnSetFeatures?: string;
|
|
compilerFilter?: string;
|
|
} = {};
|
|
|
|
const classNames: string[] = [];
|
|
const classToMethods: Record<string, string[]> = {};
|
|
const methodsToInstructions: Record<string, string[]> = {};
|
|
const methodsToSizes: Record<string, number> = {};
|
|
const absoluteToRelativeOffsets: Record<number, number> = {};
|
|
|
|
let match;
|
|
if (this.insnSetRegex.test(oatdumpOut)) {
|
|
match = oatdumpOut.match(this.insnSetRegex);
|
|
compileData.insnSet = match![1];
|
|
}
|
|
if (this.insnSetFeaturesRegex.test(oatdumpOut)) {
|
|
match = oatdumpOut.match(this.insnSetFeaturesRegex);
|
|
compileData.insnSetFeatures = match![1];
|
|
}
|
|
|
|
let inCode = false;
|
|
let currentClass = '';
|
|
let currentMethod = '';
|
|
let currentCodeOffset = 0;
|
|
for (const l of oatdumpOut.split(/\n/)) {
|
|
if (this.compilerFilterRegex.test(l)) {
|
|
match = l.match(this.compilerFilterRegex);
|
|
compileData.compilerFilter = match![1];
|
|
} else if (this.classRegex.test(l)) {
|
|
match = l.match(this.classRegex);
|
|
currentClass = match![1];
|
|
classNames.push(currentClass);
|
|
classToMethods[currentClass] = [];
|
|
} else if (this.methodRegex.test(l)) {
|
|
match = l.match(this.methodRegex);
|
|
currentMethod = match![1];
|
|
classToMethods[currentClass].push(currentMethod);
|
|
methodsToInstructions[currentMethod] = [];
|
|
inCode = false;
|
|
} else if (this.methodSizeRegex.test(l)) {
|
|
match = l.match(this.methodSizeRegex);
|
|
methodsToSizes[currentMethod] = Number.parseInt(match![2], 10);
|
|
currentCodeOffset = Number.parseInt(match![1], 16);
|
|
inCode = true;
|
|
} else if (inCode && this.insnRegex.test(l)) {
|
|
match = l.match(this.insnRegex);
|
|
methodsToInstructions[currentMethod].push(match![1] + ' ' + match![2]);
|
|
// We need to convert to relative offsets because that is how
|
|
// instructions are stored in classes.cfg's disassembly step.
|
|
absoluteToRelativeOffsets[Number.parseInt(match![1], 16)] =
|
|
Number.parseInt(match![1], 16) - currentCodeOffset;
|
|
} else if (inCode && this.extraInfoRegex.test(l)) {
|
|
match = l.match(this.extraInfoRegex);
|
|
methodsToInstructions[currentMethod].push(' ' + match![2] + ' ' + match![3]);
|
|
}
|
|
}
|
|
|
|
return {
|
|
compileData,
|
|
classNames,
|
|
classToMethods,
|
|
methodsToInstructions,
|
|
methodsToSizes,
|
|
absoluteToRelativeOffsets,
|
|
};
|
|
}
|
|
|
|
override async generateOptPipeline(
|
|
inputFilename: string,
|
|
options: string[],
|
|
filters: ParseFiltersAndOutputOptions,
|
|
OptPipelineOptions: OptPipelineBackendOptions,
|
|
): Promise<OptPipelineOutput | undefined> {
|
|
const dirPath = path.dirname(inputFilename);
|
|
const execOptions = this.getDefaultExecOptions();
|
|
execOptions.maxOutput = 1024 * 1024 * 1024;
|
|
|
|
const compileStart = performance.now();
|
|
await this.runCompiler(this.compiler.exe, options, inputFilename, execOptions);
|
|
const compileEnd = performance.now();
|
|
|
|
try {
|
|
const classesCfg = dirPath + '/classes.cfg';
|
|
const rawText = await fs.readFile(classesCfg, {encoding: 'utf8'});
|
|
const parseStart = performance.now();
|
|
const optPipeline = this.passDumpParser.process(rawText);
|
|
const parseEnd = performance.now();
|
|
return {
|
|
results: optPipeline,
|
|
parseTime: parseEnd - parseStart,
|
|
compileTime: compileEnd - compileStart,
|
|
};
|
|
} catch (e: any) {
|
|
return {
|
|
error: e.toString(),
|
|
results: {},
|
|
compileTime: compileEnd - compileStart,
|
|
};
|
|
}
|
|
}
|
|
|
|
override async getTargetsAsOverrideValues(): Promise<CompilerOverrideOption[]> {
|
|
return ['arm', 'arm64', 'x86', 'x86_64', 'riscv64'].map(target => ({
|
|
name: target,
|
|
value: target,
|
|
}));
|
|
}
|
|
|
|
override getTargetFlags(): string[] {
|
|
return [`--instruction-set=${c_value_placeholder}`];
|
|
}
|
|
|
|
override async populatePossibleOverrides(): Promise<void> {
|
|
super.populatePossibleOverrides();
|
|
|
|
this.compiler.possibleOverrides?.push({
|
|
type: 'options',
|
|
name: CompilerOverrideType.full_output,
|
|
display_title: 'Show full output',
|
|
description: 'Show the full output of the object dump tool instead of only the assembly',
|
|
flags: ['<value>'],
|
|
values: [
|
|
{name: 'Yes', value: this.fullOutputFlag},
|
|
{name: 'No', value: ''},
|
|
],
|
|
});
|
|
|
|
this.compiler.possibleOverrides?.push({
|
|
type: 'string',
|
|
name: CompilerOverrideType.d8_flags,
|
|
display_title: 'd8 Flags',
|
|
description: 'This compiler uses d8 as a frontend translator. Change the flags of d8.',
|
|
flags: ['--d8-flags', c_value_placeholder],
|
|
default: '--release',
|
|
});
|
|
}
|
|
}
|