opt/peephole/Peephole.cpp (1,480 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "Peephole.h" #include <algorithm> #include <cinttypes> #include <cmath> #include <iostream> #include <numeric> #include <unordered_map> #include <unordered_set> #include <utility> #include <vector> #include "CFGMutation.h" #include "ControlFlow.h" #include "DexClass.h" #include "DexInstruction.h" #include "DexUtil.h" #include "IRInstruction.h" #include "PassManager.h" #include "RedundantCheckCastRemover.h" #include "Show.h" #include "SpartaWorkQueue.h" #include "Trace.h" #include "Walkers.h" //////////////////////////////////////////////////////////////////////////////// // PeepholeOptimizer implementation // // Overview of the algorithm: Say we have the following code patterns to match // and replace, and code sequence, where ; denotes basic block boundaries. // // | Match | Replace | // Pattern 1 | a b c | x | // Pattern 2 | a b d | y z | // // Before: ; a a b c a b d a f b d a b ; c a b d ; // ~~~~~ ~~~~~ ~~~~~ // After: ; a x y z a f b d a b ; c y z ; // // Assumptions: // (1) 'match' patterns do not span over multiple basic blocks as of now. // We may relax this restriction later. // (2) 'match' patterns cannot be interleaved by other instructions. In the // above example, "a f b d" won't be matched to "a b d" because of 'f'. // The current peephole implementation allows such interleaving as the // algorithm keeps track of data flow instead of pattern matching. // // This is essentially a string searching problem. We can ideally utilize // std::search. But a full-fledged searching even with an optimal algorithm // (e.g., Boyer-Moore) would take some time. ProGuard's approach is very // interesting. Instead of a thorough searching, they applied a really simple // heuristic when matching fails. For instance: // // Code: a a b c a a b c // | | | // o x ===> o (retry) ===> "a b c" will be matched // | | | // Pattern: a b c a b c Only if matching fails on the second opcode // of the pattern, it retries to match the // current opcode and the pattern. // // Code: a b a b c a b a b c // | | | | // o o x ===> x .. ===> "a b c" won't be matched // | | | | // Pattern: a b c a No retry. No rescan. Search resumes from the // the next opcode. // // So, on a matching failure, PG only retries when the failure occurs on the // second opcode of the pattern. Otherwise, it simply moves forward. I would say // this heuristic as a "sweeping" or "try-and-forget" algorithm because it only // scans the code one time with very minimal retry. We first implement this PG's // approach. (I don't know whether this is really intended or a bug.) // namespace { // The peephole first detects code patterns like "const-string v0, "foo"". // We need identifiers to describe the arguments of each instruction such as // registers, method, literals, etc. For instance, we need an identifier for // an arbitrary literal argument. We may need an identifier only for an empty // string. // // Once a pattern is detected, the original instructions are replaced by new // instructions. Sometimes we need to patch the arguments of the new // instructions. For instance, we want to write the length of string A. // We also need a special identifier for this action. enum class Register : uint16_t { // It reserves only even numbers for wide pairs. A = 1, B = 3, C = 5, D = 7, E = 9, pair_A = 2, pair_B = 4, pair_C = 6, pair_D = 8, }; enum class Literal { // For an arbitrary literal argument A, // Directive: Compare strings A and B and write the result as a 4-bit integer. Compare_Strings_A_B, // Directive: Write the length of string A as a 16-bit integer. Length_String_A, // Directive: Write the hashCode of string A as a 32-bit integer. HashCode_String_A, // Directive: Convert mul/div to shl/shr with log2 of the literal argument. Mul_Div_To_Shift_Log2, // Explicit 0. Zero, }; enum class String { // For arbitrary string arguments A, B, // For only an empty string argument empty, // Special string argument directives for replacements boolean_A_to_string, // e.g., convert literal A as a boolean to a string. char_A_to_string, int_A_to_string, long_int_A_to_string, float_A_to_string, double_A_to_string, concat_A_B_strings, concat_string_A_boolean_A, concat_string_A_char_A, concat_string_A_int_A, concat_string_A_long_int_A, Type_A_get_simple_name, }; enum class Type { A, B, }; enum class Field { A, B, }; // Just a minimal refactor for long string constants. static const char* LjavaString = "Ljava/lang/String;"; static const char* LjavaStringBuilder = "Ljava/lang/StringBuilder;"; static const char* LjavaObject = "Ljava/lang/Object;"; struct DexPattern { const std::unordered_set<uint16_t> opcodes; const std::vector<Register> srcs; const std::vector<Register> dests; const enum class Kind { none, method, string, literal, type, copy, // Replace with the same exact instruction we matched. No change. field, } kind; const union { std::nullptr_t const dummy; DexMethodRef* const method; String const string; Literal const literal; Type const type; unsigned int const copy_index; Field field; }; DexPattern(std::unordered_set<uint16_t>&& opcodes, std::vector<Register>&& srcs, std::vector<Register>&& dests) : opcodes(std::move(opcodes)), srcs(std::move(srcs)), dests(std::move(dests)), kind(DexPattern::Kind::none), dummy(nullptr) {} DexPattern(std::unordered_set<uint16_t>&& opcodes, std::vector<Register>&& srcs, std::vector<Register>&& dests, DexMethodRef* const method) : opcodes(std::move(opcodes)), srcs(std::move(srcs)), dests(std::move(dests)), kind(DexPattern::Kind::method), method(method) {} DexPattern(std::unordered_set<uint16_t>&& opcodes, std::vector<Register>&& srcs, std::vector<Register>&& dests, const String string) : opcodes(std::move(opcodes)), srcs(std::move(srcs)), dests(std::move(dests)), kind(DexPattern::Kind::string), string(string) {} DexPattern(std::unordered_set<uint16_t>&& opcodes, std::vector<Register>&& srcs, std::vector<Register>&& dests, const Literal literal) : opcodes(std::move(opcodes)), srcs(std::move(srcs)), dests(std::move(dests)), kind(DexPattern::Kind::literal), literal(literal) {} DexPattern(std::unordered_set<uint16_t>&& opcodes, std::vector<Register>&& srcs, std::vector<Register>&& dests, const Type type) : opcodes(std::move(opcodes)), srcs(std::move(srcs)), dests(std::move(dests)), kind(DexPattern::Kind::type), type(type) {} DexPattern(std::unordered_set<uint16_t>&& opcodes, std::vector<Register>&& srcs, std::vector<Register>&& dests, const Field field) : opcodes(std::move(opcodes)), srcs(std::move(srcs)), dests(std::move(dests)), kind(DexPattern::Kind::field), field(field) {} static DexPattern copy_matched_instruction(int index) { return DexPattern(index); } private: explicit DexPattern(unsigned int index) : kind(Kind::copy), copy_index(index) {} }; struct Matcher; struct Pattern { const std::string name; const std::vector<DexPattern> match; const std::vector<DexPattern> replace; const std::function<bool(const Matcher&)> predicate; Pattern(std::string name, std::vector<DexPattern> match, std::vector<DexPattern> replace, std::function<bool(const Matcher&)> predicate = {}) : name(std::move(name)), match(std::move(match)), replace(std::move(replace)), predicate(std::move(predicate)) {} }; // Matcher holds the matching state for the given pattern. struct Matcher { const Pattern& pattern; size_t match_index; std::vector<IRInstruction*> matched_instructions; std::unordered_map<Register, reg_t, EnumClassHash> matched_regs; std::unordered_map<String, const DexString*, EnumClassHash> matched_strings; std::unordered_map<Literal, int64_t, EnumClassHash> matched_literals; std::unordered_map<Type, DexType*, EnumClassHash> matched_types; std::unordered_map<Field, DexFieldRef*, EnumClassHash> matched_fields; explicit Matcher(const Pattern& pattern) : pattern(pattern), match_index(0) {} void reset() { match_index = 0; matched_instructions.clear(); matched_regs.clear(); matched_strings.clear(); matched_literals.clear(); matched_types.clear(); matched_fields.clear(); } // It updates the matching state for the given instruction. Returns true if // insn matches to the last 'match' pattern. bool try_match(IRInstruction* insn) { auto match_reg = [&](Register pattern_reg, reg_t insn_reg) { // This register has been observed already. Check whether they are same. if (matched_regs.find(pattern_reg) != end(matched_regs)) { return matched_regs.at(pattern_reg) == insn_reg; } // Newly observed. Remember it. matched_regs.emplace(pattern_reg, insn_reg); return true; }; auto match_literal = [&](Literal lit_pattern, int64_t insn_literal_val) { if (matched_literals.find(lit_pattern) != end(matched_literals)) { return matched_literals.at(lit_pattern) == insn_literal_val; } matched_literals.emplace(lit_pattern, insn_literal_val); return true; }; auto match_string = [&](String str_pattern, const DexString* insn_str) { if (str_pattern == String::empty) { return (insn_str->is_simple() && insn_str->size() == 0); } if (matched_strings.find(str_pattern) != end(matched_strings)) { return matched_strings.at(str_pattern) == insn_str; } matched_strings.emplace(str_pattern, insn_str); return true; }; auto match_type = [&](Type type_pattern, DexType* insn_type) { if (matched_types.find(type_pattern) != end(matched_types)) { return matched_types.at(type_pattern) == insn_type; } matched_types.emplace(type_pattern, insn_type); return true; }; auto match_field = [&](Field field_pattern, DexFieldRef* insn_field) { auto result = matched_fields.emplace(field_pattern, insn_field); bool newly_inserted = result.second; return newly_inserted ? true : result.first->second == insn_field; }; // Does 'insn' match to the given DexPattern? auto match_instruction = [&](const DexPattern& dex_pattern) { if (dex_pattern.opcodes.find(insn->opcode()) == end(dex_pattern.opcodes) || dex_pattern.srcs.size() != insn->srcs_size() || dex_pattern.dests.size() != insn->has_dest()) { return false; } if (!dex_pattern.dests.empty()) { redex_assert(dex_pattern.dests.size() == 1); if (!match_reg(dex_pattern.dests[0], insn->dest())) { return false; } } for (size_t i = 0; i < dex_pattern.srcs.size(); ++i) { if (!match_reg(dex_pattern.srcs[i], insn->src(i))) { return false; } } switch (dex_pattern.kind) { case DexPattern::Kind::none: return true; case DexPattern::Kind::string: return match_string(dex_pattern.string, insn->get_string()); case DexPattern::Kind::literal: return match_literal(dex_pattern.literal, insn->get_literal()); case DexPattern::Kind::method: return dex_pattern.method == insn->get_method(); case DexPattern::Kind::type: return match_type(dex_pattern.type, insn->get_type()); case DexPattern::Kind::field: return match_field(dex_pattern.field, insn->get_field()); case DexPattern::Kind::copy: not_reached_log( "Kind::copy can only be used in replacements. Not matches"); } not_reached(); }; redex_assert(match_index < pattern.match.size()); if (!match_instruction(pattern.match[match_index])) { // Okay, this is the PG's heuristic. Retry only if the failure occurs on // the second opcode of the pattern. bool retry = (match_index == 1); TRACE(PEEPHOLE, 8, "Not Matched: %s[%lu] != %s", pattern.name.c_str(), match_index, SHOW(insn)); reset(); if (retry) { redex_assert(match_index == 0); if (!match_instruction(pattern.match[match_index])) { return false; } } else { return false; } } TRACE(PEEPHOLE, 8, "Matched [%lu/%lu]: %s", match_index + 1, pattern.match.size(), SHOW(insn)); matched_instructions.push_back(insn); ++match_index; bool done = match_index == pattern.match.size(); // if we've matched everything, the predicate may still veto if (done && pattern.predicate && !pattern.predicate(*this)) { reset(); return false; } return done; } // Generate skeleton instruction for the replacement. IRInstruction* generate_dex_instruction(const DexPattern& replace) { if (replace.opcodes.size() != 1) { not_reached_log("Replacement must have unique opcode"); } const auto opcode = *begin(replace.opcodes); switch (opcode) { case OPCODE_INVOKE_DIRECT: case OPCODE_INVOKE_STATIC: case OPCODE_INVOKE_VIRTUAL: redex_assert(replace.kind == DexPattern::Kind::method); return (new IRInstruction((IROpcode)opcode)) ->set_method(replace.method) ->set_srcs_size(replace.srcs.size()); case OPCODE_MOVE: case OPCODE_MOVE_WIDE: case OPCODE_MOVE_OBJECT: case OPCODE_MOVE_RESULT: case OPCODE_MOVE_RESULT_OBJECT: case IOPCODE_MOVE_RESULT_PSEUDO: case IOPCODE_MOVE_RESULT_PSEUDO_OBJECT: case OPCODE_NEG_INT: redex_assert(replace.kind == DexPattern::Kind::none); return new IRInstruction((IROpcode)opcode); case OPCODE_CONST_STRING: redex_assert(replace.kind == DexPattern::Kind::string); return new IRInstruction(OPCODE_CONST_STRING); case OPCODE_CONST: case OPCODE_SHR_INT_LIT8: case OPCODE_SHL_INT_LIT8: redex_assert(replace.kind == DexPattern::Kind::literal); return new IRInstruction((IROpcode)opcode); case OPCODE_IPUT: case OPCODE_IPUT_BYTE: case OPCODE_IPUT_CHAR: case OPCODE_IPUT_BOOLEAN: case OPCODE_IPUT_SHORT: case OPCODE_IPUT_WIDE: case OPCODE_IPUT_OBJECT: case OPCODE_IGET: case OPCODE_IGET_BYTE: case OPCODE_IGET_CHAR: case OPCODE_IGET_BOOLEAN: case OPCODE_IGET_SHORT: case OPCODE_IGET_WIDE: case OPCODE_IGET_OBJECT: case OPCODE_SPUT: case OPCODE_SPUT_BYTE: case OPCODE_SPUT_CHAR: case OPCODE_SPUT_BOOLEAN: case OPCODE_SPUT_SHORT: case OPCODE_SPUT_WIDE: case OPCODE_SPUT_OBJECT: case OPCODE_SGET: case OPCODE_SGET_BYTE: case OPCODE_SGET_CHAR: case OPCODE_SGET_BOOLEAN: case OPCODE_SGET_SHORT: case OPCODE_SGET_WIDE: case OPCODE_SGET_OBJECT: redex_assert(replace.kind == DexPattern::Kind::field); return new IRInstruction(static_cast<IROpcode>(opcode)); case OPCODE_APUT: case OPCODE_APUT_BYTE: case OPCODE_APUT_CHAR: case OPCODE_APUT_BOOLEAN: case OPCODE_APUT_SHORT: case OPCODE_APUT_WIDE: case OPCODE_APUT_OBJECT: case OPCODE_AGET: case OPCODE_AGET_BYTE: case OPCODE_AGET_CHAR: case OPCODE_AGET_BOOLEAN: case OPCODE_AGET_SHORT: case OPCODE_AGET_WIDE: case OPCODE_AGET_OBJECT: case OPCODE_THROW: redex_assert(replace.kind == DexPattern::Kind::none); return new IRInstruction(static_cast<IROpcode>(opcode)); } not_reached_log("Unhandled opcode: 0x%x", opcode); } // After a successful match, get the replacement instructions. We substitute // the placeholders appropriately including special command placeholders. std::vector<IRInstruction*> get_replacements() { always_assert(pattern.match.size() == match_index); std::vector<IRInstruction*> replacements; for (const auto& replace_info : pattern.replace) { // First, generate the instruction object. if (replace_info.kind == DexPattern::Kind::copy) { always_assert(matched_instructions.size() > replace_info.copy_index); replacements.push_back( new IRInstruction(*matched_instructions[replace_info.copy_index])); continue; } auto replace = generate_dex_instruction(replace_info); replacements.push_back(replace); // Fill the arguments appropriately. if (!replace_info.dests.empty()) { redex_assert(replace_info.dests.size() == 1); const Register dest = replace_info.dests[0]; always_assert(matched_regs.find(dest) != end(matched_regs)); replace->set_dest(matched_regs.at(dest)); } for (size_t i = 0; i < replace_info.srcs.size(); ++i) { const Register reg = replace_info.srcs[i]; always_assert(matched_regs.find(reg) != end(matched_regs)); replace->set_src(i, matched_regs.at(reg)); } if (replace_info.kind == DexPattern::Kind::string) { switch (replace_info.string) { case String::A: { auto a = matched_strings.at(String::A); replace->set_string(a); break; } case String::B: { auto b = matched_strings.at(String::B); replace->set_string(b); break; } case String::empty: { auto empty = DexString::make_string(""); replace->set_string(empty); break; } case String::boolean_A_to_string: { bool a = matched_literals.at(Literal::A); replace->set_string( DexString::make_string(a == true ? "true" : "false")); break; } case String::char_A_to_string: { int a = matched_literals.at(Literal::A); auto achar = encode_utf8_char_to_mutf8_string(a); replace->set_string(DexString::make_string(achar)); break; } case String::int_A_to_string: { int a = matched_literals.at(Literal::A); replace->set_string(DexString::make_string(std::to_string(a))); break; } case String::long_int_A_to_string: { int64_t a = matched_literals.at(Literal::A); replace->set_string(DexString::make_string(std::to_string(a))); break; } case String::float_A_to_string: { union { int32_t i; float f; } a; a.i = static_cast<int32_t>(matched_literals.at(Literal::A)); replace->set_string(DexString::make_string(std::to_string(a.f))); break; } case String::double_A_to_string: { union { int64_t i; double d; } a; a.i = matched_literals.at(Literal::A); replace->set_string(DexString::make_string(std::to_string(a.d))); break; } case String::concat_A_B_strings: { auto a = matched_strings.at(String::A)->c_str(); auto b = matched_strings.at(String::B)->c_str(); replace->set_string( DexString::make_string(std::string(a) + std::string(b))); break; } case String::concat_string_A_int_A: { auto a = matched_strings.at(String::A)->c_str(); int b = matched_literals.at(Literal::A); replace->set_string( DexString::make_string(std::string(a) + std::to_string(b))); break; } case String::concat_string_A_boolean_A: { auto a = matched_strings.at(String::A)->c_str(); bool b = matched_literals.at(Literal::A); replace->set_string(DexString::make_string( std::string(a) + (b == true ? "true" : "false"))); break; } case String::concat_string_A_long_int_A: { auto a = matched_strings.at(String::A)->c_str(); int64_t b = matched_literals.at(Literal::A); replace->set_string( DexString::make_string(std::string(a) + std::to_string(b))); break; } case String::concat_string_A_char_A: { auto a = matched_strings.at(String::A)->c_str(); int b = matched_literals.at(Literal::A); auto bchar = encode_utf8_char_to_mutf8_string(b); replace->set_string(DexString::make_string(std::string(a) + bchar)); break; } case String::Type_A_get_simple_name: { DexType* a = matched_types.at(Type::A); std::string simple = type::get_simple_name(a); replace->set_string(DexString::make_string(simple)); break; } default: not_reached_log("Unexpected string directive: 0x%x", replace_info.string); } } else if (replace_info.kind == DexPattern::Kind::literal) { switch (replace_info.literal) { case Literal::Compare_Strings_A_B: { auto a = matched_strings.at(String::A); auto b = matched_strings.at(String::B); // Just DexString* pointer comparison! DexString has uniqueness. replace->set_literal((a == b) ? 1L : 0L); break; } case Literal::Length_String_A: { auto a = matched_strings.at(String::A); replace->set_literal(a->length()); break; } case Literal::HashCode_String_A: { auto a = matched_strings.at(String::A); replace->set_literal(static_cast<int64_t>(a->java_hashcode())); break; } case Literal::A: { auto a = matched_literals.at(Literal::A); replace->set_literal(a); break; } case Literal::Mul_Div_To_Shift_Log2: { auto a = matched_literals.at(Literal::Mul_Div_To_Shift_Log2); redex_assert(a > 0); replace->set_literal(static_cast<uint64_t>(log2(a))); break; } case Literal::Zero: { replace->set_literal(0); break; } } } else if (replace_info.kind == DexPattern::Kind::type) { switch (replace_info.type) { case Type::A: replace->set_type(matched_types.at(Type::A)); break; case Type::B: replace->set_type(matched_types.at(Type::B)); break; default: not_reached_log("Unexpected type directive 0x%x", replace_info.type); } } else if (replace_info.kind == DexPattern::Kind::field) { switch (replace_info.field) { case Field::A: replace->set_field(matched_fields.at(Field::A)); break; case Field::B: replace->set_field(matched_fields.at(Field::B)); break; default: not_reached_log("Unexpected field directive 0x%x", replace_info.field); } } } return replacements; } }; // The optimization MUST NOT change the state of the registers after the viewed // piece of code runs. Changing the registers is unsafe because some later // instruction may depend on that register and the peephole has no clue. So, it // must be conservative. This means that the peephole optimization will create // dead writes that Dead Code Elimination (DCE) will clean up later. // // Another constraint on register state: // When restoring register state, you MUST do so in the same order as before the // optimization. The reason is that multiple symbolic registers (like // Register::A and Register::B) can map to the same real register (like v1). // An example: // // const A, 0 matches const v1, 0 // const B, 1 const v1, 1 // // If you were to change the order, v1 would have the wrong value. // // Individual patterns can be disabled via config // "PeepholePass" : { // "disabled_peepholes" : [ // "Name_OfOpt1", // "etc." // ] // } namespace patterns { // invoke-direct {reg_instance}, Ljava/lang/StringBuilder;.<init>:()V DexPattern invoke_StringBuilder_init(Register instance) { return {{OPCODE_INVOKE_DIRECT}, {instance}, {}, DexMethod::make_method(LjavaStringBuilder, "<init>", "V", {})}; } // invoke-direct {reg_instance, reg_argument}, // Ljava/lang/StringBuilder;.<init>:(Ljava/lang/String;)V DexPattern invoke_StringBuilder_init_String(Register instance, Register argument) { return { {OPCODE_INVOKE_DIRECT}, {instance, argument}, {}, DexMethod::make_method(LjavaStringBuilder, "<init>", "V", {LjavaString})}; }; // clang-format off // invoke-virtual {reg_instance, reg_argument}, // Ljava/lang/StringBuilder;.append:(param_type)Ljava/lang/StringBuilder; DexPattern invoke_StringBuilder_append(Register instance, Register argument, const char* param_type) { return {{OPCODE_INVOKE_VIRTUAL}, {instance, argument}, {}, DexMethod::make_method( LjavaStringBuilder, "append", LjavaStringBuilder, {param_type})}; }; DexPattern invoke_String_valueOf(Register argument, const char* param_type) { return {{OPCODE_INVOKE_STATIC}, {argument}, {}, DexMethod::make_method( LjavaString, "valueOf", LjavaString, {param_type})}; }; DexPattern invoke_String_equals(Register instance, Register argument) { return {{OPCODE_INVOKE_VIRTUAL}, {instance, argument}, {}, DexMethod::make_method(LjavaString, "equals", "Z", {LjavaObject})}; }; DexPattern invoke_String_length(Register instance) { return {{OPCODE_INVOKE_VIRTUAL}, {instance}, {}, DexMethod::make_method(LjavaString, "length", "I", {})}; }; DexPattern invoke_String_hashCode(Register instance) { return {{OPCODE_INVOKE_VIRTUAL}, {instance}, {}, DexMethod::make_method(LjavaString, "hashCode", "I", {})}; }; DexPattern const_string(String string) { return {{OPCODE_CONST_STRING}, {}, {}, string}; }; DexPattern move_result_pseudo_wide(Register dest) { return {{IOPCODE_MOVE_RESULT_PSEUDO_WIDE}, {}, {dest}}; }; DexPattern move_result_pseudo(Register dest) { return {{IOPCODE_MOVE_RESULT_PSEUDO}, {}, {dest}}; }; DexPattern move_result_pseudo_object(Register dest) { return {{IOPCODE_MOVE_RESULT_PSEUDO_OBJECT}, {}, {dest}}; }; DexPattern move_result_object(Register dest) { return {{OPCODE_MOVE_RESULT_OBJECT}, {}, {dest}}; }; DexPattern move_result(Register dest) { return {{OPCODE_MOVE_RESULT}, {}, {dest}}; }; DexPattern const_literal(uint16_t opcode, Register dest, Literal literal) { return {{opcode}, {}, {dest}, literal}; }; DexPattern const_wide(Register dest, Literal literal) { return {{OPCODE_CONST_WIDE}, {}, {dest}, literal}; }; DexPattern const_integer(Register dest, Literal literal) { return {{OPCODE_CONST}, {}, {dest}, literal}; }; DexPattern const_float(Register dest, Literal literal) { return {{OPCODE_CONST}, {}, {dest}, literal}; }; DexPattern const_char(Register dest, Literal literal) { // Modified UTF-8, 1-3 bytes. DX uses const/4 for the null character // (\u0000), and const/16 and const to load a char. return const_integer(dest, literal); }; DexPattern move_object(Register dest, Register src) { return {{OPCODE_MOVE_OBJECT}, {src}, {dest}}; }; std::vector<Pattern> get_string_patterns() { return { // It coalesces init(void) and append(string) into init(string). // new StringBuilder().append("...") = new StringBuilder("...") {"Coalesce_InitVoid_AppendString", {invoke_StringBuilder_init(Register::A), const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C)}, {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_init_String(Register::A, Register::B), move_object(Register::C, Register::A)}}, // It coalesces init(void) and append(string) into init(string). // new StringBuilder().append("...") = new StringBuilder("...") // Difference from Coalesce_InitVoid_AppendString is it don't have // trailing move_result_object {"Coalesce_InitVoid_AppendString_WithoutMoveResult", {invoke_StringBuilder_init(Register::A), const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString)}, {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_init_String(Register::A, Register::B)}}, // It coalesces consecutive two append(string) to a single append call. // StringBuilder.append("A").append("B") = StringBuilder.append("AB") {"Coalesce_AppendString_AppendString", {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C), const_string(String::B), move_result_pseudo_object(Register::D), invoke_StringBuilder_append(Register::C, Register::D, LjavaString), move_result_object(Register::E)}, // pre opt write order: B, C, D, E {const_string(String::concat_A_B_strings), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), const_string(String::A), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A), // maybe dead const_string(String::B), // maybe dead move_result_pseudo_object(Register::D), move_object(Register::E, Register::C)}}, // maybe dead // post opt write order B, B, C, D, E // Explanation of WithoutMoveResult // A variation of the above optimization. The result of append isn't // always moved with move-result-object. But we want to capture both forms // of this pattern. This optimization would not be safe if // AppendString_AppendString doesn't run first because // (1) the last instruction of the pattern is an invoke AND // (2) the last instruction of the replacement is not an invoke AND // (3) the instruction after the pattern may be a move_result_object {"Coalesce_AppendString_AppendString_WithoutMoveResult", {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C), const_string(String::B), move_result_pseudo_object(Register::D), invoke_StringBuilder_append(Register::C, Register::D, LjavaString)}, // pre opt write order: B, C, D {const_string(String::concat_A_B_strings), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), const_string(String::A), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A), // maybe dead const_string(String::B), // maybe dead move_result_pseudo_object(Register::D)}}, // there shouldn't be a move-result-object here because of the // previous pattern // post opt write order: B, B, C, D // It evaluates the length of a literal in compile time. // "stringA".length() ==> length_of_stringA {"CompileTime_StringLength", {const_string(String::A), move_result_pseudo_object(Register::A), invoke_String_length(Register::A), move_result(Register::B)}, {const_string(String::A), // maybe dead move_result_pseudo_object(Register::A), const_literal(OPCODE_CONST, Register::B, Literal::Length_String_A)}}, // Evaluate the hashCode of a String at compile time. // "stringA".hashCode() ==> hashcode_of_stringA {"CompileTime_StringHashCode", {const_string(String::A), move_result_pseudo_object(Register::A), invoke_String_hashCode(Register::A), move_result(Register::B)}, {const_string(String::A), // maybe dead move_result_pseudo_object(Register::A), const_literal(OPCODE_CONST, Register::B, Literal::HashCode_String_A)}}, // It removes an append call with an empty string. // StringBuilder.append("") = nothing {"Remove_AppendEmptyString", {const_string(String::empty), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C)}, {const_string(String::empty), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A)}}, // maybe dead {"Remove_AppendEmptyString_WithoutMoveResult", {const_string(String::empty), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString)}, {const_string(String::empty), move_result_pseudo_object(Register::B)}}, // maybe dead // It coalesces init(void) and append(char) into init(string). // StringBuilder().append(C) = new StringBuilder("....") {"Coalesce_Init_AppendChar", {invoke_StringBuilder_init(Register::A), const_char(Register::B, Literal::A), invoke_StringBuilder_append(Register::A, Register::B, "C"), move_result_object(Register::C)}, {const_string(String::char_A_to_string), move_result_pseudo_object(Register::B), invoke_StringBuilder_init_String(Register::A, Register::B), DexPattern::copy_matched_instruction(1), // const_char. maybe dead move_object(Register::C, Register::A)}}, // maybe dead {"Coalesce_Init_AppendChar_WithoutMoveResult", {invoke_StringBuilder_init(Register::A), const_char(Register::B, Literal::A), invoke_StringBuilder_append(Register::A, Register::B, "C")}, {const_string(String::char_A_to_string), move_result_pseudo_object(Register::B), invoke_StringBuilder_init_String(Register::A, Register::B), DexPattern::copy_matched_instruction(1)}}, // const_char. maybe dead // It coalesces append(string) and append(integer) into append(string). // StringBuilder.append("...").append(I) = StringBuilder.append("....") {"Coalesce_AppendString_AppendInt", {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C), const_integer(Register::D, Literal::A), invoke_StringBuilder_append(Register::C, Register::D, "I"), move_result_object(Register::E)}, // pre opt write order: B, C, D, E {// (2 + 3 + 1 + [1, 2, 3] + 3 + 1) - (2 + 3 + 2 + 1 + [1, 2, 3] + 1) = 1 const_string(String::concat_string_A_int_A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), const_string(String::A), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A), // maybe dead DexPattern::copy_matched_instruction(4), // const_integer. maybe dead move_object(Register::E, Register::C)}}, // maybe dead // post opt write order B, B, C, D, E {"Coalesce_AppendString_AppendInt_WithoutMoveResult", {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C), const_integer(Register::D, Literal::A), invoke_StringBuilder_append(Register::C, Register::D, "I")}, // pre opt write order: B, C, D {// (2 + 3 + 1 + [1, 2, 3] + 3) - (2 + 3 + 2 + 1 + [1, 2, 3]) = 1 const_string(String::concat_string_A_int_A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), const_string(String::A), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A), // maybe dead DexPattern::copy_matched_instruction(4)}}, // const_integer. maybe dead // post opt write order: B, B, C, D // It coalesces append(string) and append(char) into append(string). // StringBuilder.append("...").append(C) = StringBuilder.append("....") {"Coalesce_AppendString_AppendChar", {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C), const_char(Register::D, Literal::A), invoke_StringBuilder_append(Register::C, Register::D, "C"), move_result_object(Register::E)}, // pre opt write order: B, C, D, A {// (2 + 3 + 1 + [1, 2, 3] + 3 + 1) - (2 + 3 + 2 + 1 + [1, 2, 3] + 1) = 1 const_string(String::concat_string_A_char_A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), const_string(String::A), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A), // maybe dead DexPattern::copy_matched_instruction(4), // const_integer. maybe dead move_object(Register::E, Register::C)}}, // maybe dead // post opt write order: B, B, C, D, E {"Coalesce_AppendString_AppendChar_WithoutMoveResult", {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C), const_char(Register::D, Literal::A), invoke_StringBuilder_append(Register::C, Register::D, "C")}, // pre opt write order: B, C, D {// (2 + 3 + 1 + [1, 2, 3] + 3) - (2 + 3 + 2 + 1 + [1, 2, 3]) = 1 const_string(String::concat_string_A_char_A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), const_string(String::A), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A), // maybe dead DexPattern::copy_matched_instruction(4)}}, // const_integer. maybe dead // post opt write order: B, B, C, D // It coalesces append(string) and append(boolean) into append(string). // StringBuilder.append("...").append(Z) = StringBuilder.append("....") {"Coalesce_AppendString_AppendBoolean", {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C), const_literal(OPCODE_CONST, Register::D, Literal::A), invoke_StringBuilder_append(Register::C, Register::D, "Z"), move_result_object(Register::E)}, // pre opt write order: B, C, D, E {const_string(String::concat_string_A_boolean_A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), const_string(String::A), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A), // maybe dead const_literal(OPCODE_CONST, Register::D, Literal::A), // maybe dead move_object(Register::E, Register::C)}}, // maybe dead // post opt write order: B, B, C, D, E {"Coalesce_AppendString_AppendBoolean_WithoutMoveResult", {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C), const_literal(OPCODE_CONST, Register::D, Literal::A), invoke_StringBuilder_append(Register::C, Register::D, "Z")}, // pre opt write order: B, C, D {const_string(String::concat_string_A_boolean_A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), const_string(String::A), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A), // maybe dead const_literal(OPCODE_CONST, Register::D, Literal::A)}}, // maybe dead // post opt write order: B, B, C, D // It coalesces append(string) and append(long int) into append(string). // StringBuilder.append("...").append(J) = StringBuilder.append("....") {"Coalesce_AppendString_AppendLongInt", {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C), const_wide(Register::D, Literal::A), invoke_StringBuilder_append(Register::C, Register::D, "J"), move_result_object(Register::E)}, // pre opt write order: B, C, D, E {// (2 + 3 + 1 + [2, 3, 5] + 3 + 1) - (2 + 3 + 2 + 1 + [2, 3, 5] + 1) = 1 const_string(String::concat_string_A_long_int_A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), const_string(String::A), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A), // maybe dead DexPattern::copy_matched_instruction(4), // const_wide. maybe dead move_object(Register::E, Register::C)}}, // maybe dead // post opt write order: B, B, C, D, E {"Coalesce_AppendString_AppendLongInt_WithoutMoveResult", {const_string(String::A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), move_result_object(Register::C), const_wide(Register::D, Literal::A), invoke_StringBuilder_append(Register::C, Register::D, "J")}, // pre opt write order: B, C, D {// (2 + 3 + 1 + [2, 3, 5] + 3) - (2 + 3 + 2 + 1 + [2, 3, 5]) = 1 const_string(String::concat_string_A_long_int_A), move_result_pseudo_object(Register::B), invoke_StringBuilder_append(Register::A, Register::B, LjavaString), const_string(String::A), // maybe dead move_result_pseudo_object(Register::B), move_object(Register::C, Register::A), // maybe dead DexPattern::copy_matched_instruction(4)}}, // const_wide. maybe dead // post opt write order: B, B, C, D // It evaluates the identify of two literal strings in compile time. // "stringA".equals("stringB") ==> true or false {"CompileTime_StringCompare", {const_string(String::A), move_result_pseudo_object(Register::A), const_string(String::B), move_result_pseudo_object(Register::B), invoke_String_equals(Register::A, Register::B), move_result(Register::C)}, {const_string(String::A), // maybe dead move_result_pseudo_object(Register::A), const_string(String::B), // maybe dead move_result_pseudo_object(Register::B), const_literal( OPCODE_CONST, Register::C, Literal::Compare_Strings_A_B)}}, // It replaces valueOf on a boolean value by "true" or "false" directly. // String.valueof(true/false) ==> "true" or "false" {"Replace_ValueOfBoolean", {const_literal(OPCODE_CONST, Register::A, Literal::A), invoke_String_valueOf(Register::A, "Z"), move_result_object(Register::B)}, {const_literal(OPCODE_CONST, Register::A, Literal::A), // maybe dead const_string(String::boolean_A_to_string), move_result_pseudo_object(Register::B)}}, // It replaces valueOf on a literal character by the character itself. // String.valueOf(char) ==> "char" {"Replace_ValueOfChar", {const_char(Register::A, Literal::A), invoke_String_valueOf(Register::A, "C"), move_result_object(Register::B)}, {DexPattern::copy_matched_instruction(0), // const_char. maybe dead const_string(String::char_A_to_string), // maybe dead move_result_pseudo_object(Register::B)}}, // It replaces valueOf on an integer literal by the integer itself. // String.valueof(int) ==> "int" {"Replace_ValueOfInt", {const_integer(Register::A, Literal::A), invoke_String_valueOf(Register::A, "I"), move_result_object(Register::B)}, {DexPattern::copy_matched_instruction(0), // const_integer. maybe dead const_string(String::int_A_to_string), move_result_pseudo_object(Register::B)}}, // It replaces valueOf on a long integer literal by the number itself. // String.valueof(long int) ==> "long int" {"Replace_ValueOfLongInt", {const_wide(Register::A, Literal::A), invoke_String_valueOf(Register::A, "J"), move_result_object(Register::B)}, {DexPattern::copy_matched_instruction(0), // const_wide. maybe dead const_string(String::long_int_A_to_string), move_result_pseudo_object(Register::B)}}, // It replaces valueOf on a float literal by the float itself. // String.valueof(float) ==> "float" {"Replace_ValueOfFloat", {const_float(Register::A, Literal::A), invoke_String_valueOf(Register::A, "F"), move_result_object(Register::B)}, {DexPattern::copy_matched_instruction(0), // const_float. maybe dead const_string(String::float_A_to_string), move_result_pseudo_object(Register::B)}}, // It replaces valueOf on a double literal by the double itself. // String.valueof(double) ==> "double" {"Replace_ValueOfDouble", {const_wide(Register::A, Literal::A), invoke_String_valueOf(Register::A, "D"), move_result_object(Register::B)}, {DexPattern::copy_matched_instruction(0), // const_wide. maybe dead const_string(String::double_A_to_string), move_result_pseudo_object(Register::B)}}, }; } // clang-format on DexPattern move_ops(Register dest, Register src) { return {{OPCODE_MOVE, OPCODE_MOVE_OBJECT}, {src}, {dest}}; }; std::vector<Pattern> get_nop_patterns() { // Remove redundant move and move_object instructions, // e.g. move v0, v0 return {{"Remove_Redundant_Move", {move_ops(Register::A, Register::A)}, {}}}; } bool second_get_non_volatile(const Matcher& m) { if (m.matched_instructions.size() < 2) { return false; } DexFieldRef* field_ref = m.matched_instructions[1]->get_field(); if (!field_ref->is_concrete()) { return false; } DexField* field = static_cast<DexField*>(field_ref); return !(field->get_access() & ACC_VOLATILE); } DexPattern put_x_op(IROpcode opcode, Register src, Register obj_register, Field field) { if (opcode::is_an_iput(opcode)) { return {{opcode}, {src, obj_register}, {}, field}; } if (opcode::is_an_sput(opcode)) { return {{opcode}, {src}, {}, field}; } not_reached_log("Not supported IROpcode %s", SHOW(opcode)); } DexPattern get_x_op(IROpcode opcode, Register src, Field field) { if (opcode::is_an_iget(opcode)) { return {{opcode}, {src}, {}, field}; } if (opcode::is_an_sget(opcode)) { return {{opcode}, {}, {}, field}; } not_reached_log("Not supported IROpcode %s", SHOW(opcode)); } std::vector<DexPattern> put_x_patterns(IROpcode put_code) { return {put_x_op(put_code, Register::A, Register::B, Field::A)}; } std::vector<DexPattern> put_move_x_patterns(IROpcode put_code, IROpcode move_code) { return {put_x_op(put_code, Register::A, Register::B, Field::A), {{move_code}, {Register::A}, {Register::C}}}; } std::vector<DexPattern> put_get_x_patterns( IROpcode put_code, IROpcode get_code, DexPattern (*move_pseudo_func)(Register reg), bool match_src_register = true) { return {put_x_op(put_code, Register::A, Register::B, Field::A), get_x_op(get_code, Register::B, Field::A), move_pseudo_func(match_src_register ? Register::A : Register::C)}; } DexPattern aput_x_op(IROpcode opcode, Register src, Register array_register, Register index_register) { if (opcode::is_an_aput(opcode)) { return {{opcode}, {src, array_register, index_register}, {}}; } not_reached_log("Not supported IROpcode %s", SHOW(opcode)); } std::vector<DexPattern> aput_x_patterns(IROpcode put_code) { return {aput_x_op(put_code, Register::A, Register::B, Register::C)}; } DexPattern aget_x_op(IROpcode opcode, Register array_register, Register index_register) { if (opcode::is_an_aget(opcode)) { return {{opcode}, {array_register, index_register}, {}}; } not_reached_log("Not supported IROpcode %s", SHOW(opcode)); } std::vector<DexPattern> aput_aget_x_patterns( IROpcode aput_code, IROpcode aget_code, DexPattern (*move_pseudo_func)(Register reg)) { return {aput_x_op(aput_code, Register::A, Register::B, Register::C), aget_x_op(aget_code, Register::B, Register::C), move_pseudo_func(Register::A)}; } std::vector<Pattern> get_aputaget_patterns() { return {{{"Replace_AputAget", aput_aget_x_patterns(OPCODE_APUT, OPCODE_AGET, move_result_pseudo), aput_x_patterns(OPCODE_APUT)}, {"Replace_AputAgetWide", aput_aget_x_patterns(OPCODE_APUT_WIDE, OPCODE_AGET_WIDE, move_result_pseudo_wide), aput_x_patterns(OPCODE_APUT_WIDE)}, /* The following is only valid when aput-object would receive values that match the array type. However, this is not statically checked, and other Redex optimizations may violate that assumption. {"Replace_AputAgetObject", aput_aget_x_patterns(OPCODE_APUT_OBJECT, OPCODE_AGET_OBJECT, move_result_pseudo_object), aput_x_patterns(OPCODE_APUT_OBJECT)}, */ {"Replace_AputAgetShort", aput_aget_x_patterns(OPCODE_APUT_SHORT, OPCODE_AGET_SHORT, move_result_pseudo), aput_x_patterns(OPCODE_APUT_SHORT)}, {"Replace_AputAgetChar", aput_aget_x_patterns(OPCODE_APUT_CHAR, OPCODE_AGET_CHAR, move_result_pseudo), aput_x_patterns(OPCODE_APUT_CHAR)}, {"Replace_AputAgetByte", aput_aget_x_patterns(OPCODE_APUT_BYTE, OPCODE_AGET_BYTE, move_result_pseudo), aput_x_patterns(OPCODE_APUT_BYTE)}, {"Replace_AputAgetBoolean", aput_aget_x_patterns(OPCODE_APUT_BOOLEAN, OPCODE_AGET_BOOLEAN, move_result_pseudo), aput_x_patterns(OPCODE_APUT_BOOLEAN)}}}; } // The following patterns match the case when the destination register of // the put instruction is the same as the destination register of the get // instruction. std::vector<Pattern> get_putget_same_reg_patterns() { return {{{"Replace_PutGet", put_get_x_patterns(OPCODE_IPUT, OPCODE_IGET, move_result_pseudo), put_x_patterns(OPCODE_IPUT), second_get_non_volatile}, {"Replace_PutGetWide", put_get_x_patterns(OPCODE_IPUT_WIDE, OPCODE_IGET_WIDE, move_result_pseudo_wide), put_x_patterns(OPCODE_IPUT_WIDE), second_get_non_volatile}, {"Replace_PutGetObject", put_get_x_patterns(OPCODE_IPUT_OBJECT, OPCODE_IGET_OBJECT, move_result_pseudo_object), put_x_patterns(OPCODE_IPUT_OBJECT), second_get_non_volatile}, {"Replace_PutGetShort", put_get_x_patterns(OPCODE_IPUT_SHORT, OPCODE_IGET_SHORT, move_result_pseudo), put_x_patterns(OPCODE_IPUT_SHORT), second_get_non_volatile}, {"Replace_PutGetChar", put_get_x_patterns(OPCODE_IPUT_CHAR, OPCODE_IGET_CHAR, move_result_pseudo), put_x_patterns(OPCODE_IPUT_CHAR), second_get_non_volatile}, {"Replace_PutGetByte", put_get_x_patterns(OPCODE_IPUT_BYTE, OPCODE_IGET_BYTE, move_result_pseudo), put_x_patterns(OPCODE_IPUT_BYTE), second_get_non_volatile}, {"Replace_PutGetBoolean", put_get_x_patterns(OPCODE_IPUT_BOOLEAN, OPCODE_IGET_BOOLEAN, move_result_pseudo), put_x_patterns(OPCODE_IPUT_BOOLEAN), second_get_non_volatile}, {"Replace_StaticPutGet", put_get_x_patterns(OPCODE_SPUT, OPCODE_SGET, move_result_pseudo), put_x_patterns(OPCODE_SPUT), second_get_non_volatile}, {"Replace_StaticPutGetWide", put_get_x_patterns(OPCODE_SPUT_WIDE, OPCODE_SGET_WIDE, move_result_pseudo_wide), put_x_patterns(OPCODE_SPUT_WIDE), second_get_non_volatile}, {"Replace_StaticPutGetObject", put_get_x_patterns(OPCODE_SPUT_OBJECT, OPCODE_SGET_OBJECT, move_result_pseudo_object), put_x_patterns(OPCODE_SPUT_OBJECT), second_get_non_volatile}, {"Replace_StaticPutGetShort", put_get_x_patterns(OPCODE_SPUT_SHORT, OPCODE_SGET_SHORT, move_result_pseudo), put_x_patterns(OPCODE_SPUT_SHORT), second_get_non_volatile}, {"Replace_StaticPutGetChar", put_get_x_patterns(OPCODE_SPUT_CHAR, OPCODE_SGET_CHAR, move_result_pseudo), put_x_patterns(OPCODE_SPUT_CHAR), second_get_non_volatile}, {"Replace_StaticPutGetByte", put_get_x_patterns(OPCODE_SPUT_BYTE, OPCODE_SGET_BYTE, move_result_pseudo), put_x_patterns(OPCODE_SPUT_BYTE), second_get_non_volatile}, {"Replace_StaticPutGetBoolean", put_get_x_patterns(OPCODE_SPUT_BOOLEAN, OPCODE_SGET_BOOLEAN, move_result_pseudo), put_x_patterns(OPCODE_SPUT_BOOLEAN), second_get_non_volatile}}}; } // The following patterns match the case when the source register of the put // instruction is different from the destination register of the get // instruction. put_move_x_patterns replaces the put/get/move-pseudo sequence // with the put instruction followed by a move instruction. std::vector<Pattern> get_putget_diff_reg_patterns() { return { {{"Replace_PutGetDiffSrcDest", put_get_x_patterns(OPCODE_IPUT, OPCODE_IGET, move_result_pseudo, false), put_move_x_patterns(OPCODE_IPUT, OPCODE_MOVE), second_get_non_volatile}, {"Replace_PutGetWideDiffSrcDest", put_get_x_patterns(OPCODE_IPUT_WIDE, OPCODE_IGET_WIDE, move_result_pseudo_wide, false), put_move_x_patterns(OPCODE_IPUT_WIDE, OPCODE_MOVE_WIDE), second_get_non_volatile}, {"Replace_PutGetObjectDiffSrcDest", put_get_x_patterns(OPCODE_IPUT_OBJECT, OPCODE_IGET_OBJECT, move_result_pseudo_object, false), put_move_x_patterns(OPCODE_IPUT_OBJECT, OPCODE_MOVE_OBJECT), second_get_non_volatile}, {"Replace_PutGetShortDiffSrcDest", put_get_x_patterns(OPCODE_IPUT_SHORT, OPCODE_IGET_SHORT, move_result_pseudo, false), put_move_x_patterns(OPCODE_IPUT_SHORT, OPCODE_MOVE), second_get_non_volatile}, {"Replace_PutGetCharDiffSrcDest", put_get_x_patterns(OPCODE_IPUT_CHAR, OPCODE_IGET_CHAR, move_result_pseudo, false), put_move_x_patterns(OPCODE_IPUT_CHAR, OPCODE_MOVE), second_get_non_volatile}, {"Replace_PutGetByteDiffSrcDest", put_get_x_patterns(OPCODE_IPUT_BYTE, OPCODE_IGET_BYTE, move_result_pseudo, false), put_move_x_patterns(OPCODE_IPUT_BYTE, OPCODE_MOVE), second_get_non_volatile}, {"Replace_PutGetBooleanDiffSrcDest", put_get_x_patterns(OPCODE_IPUT_BOOLEAN, OPCODE_IGET_BOOLEAN, move_result_pseudo, false), put_move_x_patterns(OPCODE_IPUT_BOOLEAN, OPCODE_MOVE), second_get_non_volatile}, {"Replace_StaticPutGetDiffSrcDest", put_get_x_patterns(OPCODE_SPUT, OPCODE_SGET, move_result_pseudo, false), put_move_x_patterns(OPCODE_SPUT, OPCODE_MOVE), second_get_non_volatile}, {"Replace_StaticPutGetWideDiffSrcDest", put_get_x_patterns(OPCODE_SPUT_WIDE, OPCODE_SGET_WIDE, move_result_pseudo_wide, false), put_move_x_patterns(OPCODE_SPUT_WIDE, OPCODE_MOVE_WIDE), second_get_non_volatile}, {"Replace_StaticPutGetObjectDiffSrcDest", put_get_x_patterns(OPCODE_SPUT_OBJECT, OPCODE_SGET_OBJECT, move_result_pseudo_object, false), put_move_x_patterns(OPCODE_SPUT_OBJECT, OPCODE_MOVE_OBJECT), second_get_non_volatile}, {"Replace_StaticPutGetShortDiffSrcDest", put_get_x_patterns(OPCODE_SPUT_SHORT, OPCODE_SGET_SHORT, move_result_pseudo, false), put_move_x_patterns(OPCODE_SPUT_SHORT, OPCODE_MOVE), second_get_non_volatile}, {"Replace_StaticPutGetCharDiffSrcDest", put_get_x_patterns(OPCODE_SPUT_CHAR, OPCODE_SGET_CHAR, move_result_pseudo, false), put_move_x_patterns(OPCODE_SPUT_CHAR, OPCODE_MOVE), second_get_non_volatile}, {"Replace_StaticPutGetByteDiffSrcDest", put_get_x_patterns(OPCODE_SPUT_BYTE, OPCODE_SGET_BYTE, move_result_pseudo, false), put_move_x_patterns(OPCODE_SPUT_BYTE, OPCODE_MOVE), second_get_non_volatile}, {"Replace_StaticPutGetBooleanDiffSrcDest", put_get_x_patterns(OPCODE_SPUT_BOOLEAN, OPCODE_SGET_BOOLEAN, move_result_pseudo, false), put_move_x_patterns(OPCODE_SPUT_BOOLEAN, OPCODE_MOVE), second_get_non_volatile}}}; } template <int64_t VALUE> bool first_instruction_literal_is(const Matcher& m) { if (m.matched_instructions.empty()) { return false; } return m.matched_instructions.front()->get_literal() == VALUE; } bool first_instruction_literal_is_power_of_two(const Matcher& m) { if (m.matched_instructions.empty()) { return false; } auto literal = m.matched_instructions.front()->get_literal(); return literal > 0 && ((literal & (literal - 1)) == 0); } DexPattern mul_lit(Register src, Register dst) { return {{OPCODE_MUL_INT_LIT8, OPCODE_MUL_INT_LIT16}, {src}, {dst}}; } DexPattern mul_literal_kind(Register src, Register dst, Literal lit) { return {{OPCODE_MUL_INT_LIT8, OPCODE_MUL_INT_LIT16}, {src}, {dst}, lit}; } std::vector<DexPattern> div_lit(Register src, Register dst) { return {{{OPCODE_DIV_INT_LIT8, OPCODE_DIV_INT_LIT16}, {src}, {}}, {{IOPCODE_MOVE_RESULT_PSEUDO}, {}, {dst}}}; } std::vector<DexPattern> div_literal_kind(Register src, Register dst, Literal lit) { return {{{OPCODE_DIV_INT_LIT8, OPCODE_DIV_INT_LIT16}, {src}, {}, lit}, {{IOPCODE_MOVE_RESULT_PSEUDO}, {}, {dst}}}; } DexPattern add_lit(Register src, Register dst) { return {{OPCODE_ADD_INT_LIT8, OPCODE_ADD_INT_LIT16}, {src}, {dst}}; } std::vector<Pattern> get_arith_patterns() { return {// Replace *1 with move {"Arith_MulLit_Pos1", {mul_lit(Register::A, Register::B)}, {// x = y * 1 -> x = y {{OPCODE_MOVE}, {Register::A}, {Register::B}}}, first_instruction_literal_is<1>}, // Replace /1 with move {"Arith_DivLit_Pos1", {div_lit(Register::A, Register::B)}, {// x = y * 1 -> x = y {{OPCODE_MOVE}, {Register::A}, {Register::B}}}, first_instruction_literal_is<1>}, // Replace multiplies by -1 with negation {"Arith_MulLit_Neg1", {mul_lit(Register::A, Register::B)}, {// Eliminates the literal-carrying halfword {{OPCODE_NEG_INT}, {Register::A}, {Register::B}}}, first_instruction_literal_is<-1>}, // Replace divides by -1 with negation {"Arith_DivLit_Neg1", {div_lit(Register::A, Register::B)}, {// Eliminates the literal-carrying halfword {{OPCODE_NEG_INT}, {Register::A}, {Register::B}}}, first_instruction_literal_is<-1>}, // Replace +0 with moves {"Arith_AddLit_0", {add_lit(Register::A, Register::B)}, {// Eliminates the literal-carrying halfword {{OPCODE_MOVE}, {Register::A}, {Register::B}}}, first_instruction_literal_is<0>}, // Replace mul 2^n with shl n {"Arith_MulLit_Power2", {mul_literal_kind(Register::A, Register::B, Literal::Mul_Div_To_Shift_Log2)}, {{{OPCODE_SHL_INT_LIT8}, {Register::A}, {Register::B}, Literal::Mul_Div_To_Shift_Log2}}, first_instruction_literal_is_power_of_two}, // Replace div 2^n with shr n {"Arith_DivLit_Power2", {div_literal_kind(Register::A, Register::B, Literal::Mul_Div_To_Shift_Log2)}, {{{OPCODE_SHR_INT_LIT8}, {Register::A}, {Register::B}, Literal::Mul_Div_To_Shift_Log2}}, first_instruction_literal_is_power_of_two}}; } // clang-format off DexPattern invoke_class_get_simple_name() { return {{OPCODE_INVOKE_VIRTUAL, OPCODE_INVOKE_SUPER, OPCODE_INVOKE_DIRECT, OPCODE_INVOKE_STATIC, OPCODE_INVOKE_INTERFACE}, {Register::A}, {}, DexMethod::make_method( "Ljava/lang/Class;", "getSimpleName", "Ljava/lang/String;", {})}; } DexPattern const_class(Type type) { return {{OPCODE_CONST_CLASS}, {}, {}, type}; }; std::vector<Pattern> get_func_patterns() { return { {"Remove_LangClass_GetSimpleName", {const_class(Type::A), move_result_pseudo_object(Register::A), invoke_class_get_simple_name(), move_result_object(Register::B)}, {DexPattern::copy_matched_instruction(0), // const_class (maybe dead) move_result_pseudo_object(Register::A), const_string(String::Type_A_get_simple_name), move_result_pseudo_object(Register::B)}}, }; } DexPattern new_instance(Type type) { return {{OPCODE_NEW_INSTANCE}, {}, {}, type}; }; DexPattern invoke_npe_init(Register r) { return {{OPCODE_INVOKE_DIRECT}, {r}, {}, DexMethod::make_method( "Ljava/lang/NullPointerException;", "<init>", "V", {})}; } DexPattern throw_exception(Register r) { return {{OPCODE_THROW}, {r}, {}}; } std::vector<Pattern> get_throw_empty_npe_patterns() { return { {"Simplify_throw_new_NullPointerException", {new_instance(Type::A), move_result_pseudo_object(Register::A), invoke_npe_init(Register::A), throw_exception(Register::A)}, {const_integer(Register::A, Literal::Zero), // Null. throw_exception(Register::A)}, [](const Matcher& m) { // Check new-instance type.. if (!m.matched_instructions.empty()) { DexType* type = m.matched_instructions.front()->get_type(); return type == DexType::make_type("Ljava/lang/NullPointerException;"); } return true; // Let it pass. }}, }; } // clang-format on std::vector<std::vector<Pattern>> get_all_patterns() { return {get_string_patterns(), get_arith_patterns(), get_func_patterns(), get_nop_patterns(), get_putget_same_reg_patterns(), get_putget_diff_reg_patterns(), get_aputaget_patterns(), get_throw_empty_npe_patterns()}; } } // namespace patterns template <typename T> bool contains(const std::vector<T>& vec, const T& value) { return std::find(vec.begin(), vec.end(), value) != vec.end(); } // Each thread will have its own instance of PeepholeOptimizer, so align it in // order to avoid false sharing. class alignas(CACHE_LINE_SIZE) PeepholeOptimizer { private: std::vector<Matcher> m_matchers; std::vector<size_t> m_stats; PassManager& m_mgr; int m_stats_removed = 0; int m_stats_inserted = 0; struct ReplacementItem { cfg::Block* block; IRInstruction* insn; std::vector<IRInstruction*> replacement; ReplacementItem(cfg::Block* block, IRInstruction* insn, std::vector<IRInstruction*> replacement) : block(block), insn(insn), replacement(std::move(replacement)) {} }; public: explicit PeepholeOptimizer(PassManager& mgr, const std::vector<std::vector<Pattern>>& patterns, const std::vector<std::string>& disabled_peepholes) : m_mgr(mgr) { for (const auto& pattern_list : patterns) { for (const Pattern& pattern : pattern_list) { if (!contains(disabled_peepholes, pattern.name)) { m_matchers.emplace_back(pattern); } else { TRACE(PEEPHOLE, 2, "not running disabled peephole opt %s", pattern.name.c_str()); } } } m_stats.resize(m_matchers.size(), 0); } PeepholeOptimizer(const PeepholeOptimizer&) = delete; PeepholeOptimizer& operator=(const PeepholeOptimizer&) = delete; void peephole(DexMethod* method) { auto code = method->get_code(); code->build_cfg(/* editable */ true); auto& cfg = code->cfg(); // do optimizations one at a time // so they can match on the same pattern without interfering for (size_t i = 0; i < m_matchers.size(); ++i) { auto& matcher = m_matchers[i]; const auto& blocks = cfg.blocks(); cfg::CFGMutation mutator(cfg); for (const auto& block : blocks) { // Currently, all patterns do not span over multiple basic blocks. So // reset all matching states on visiting every basic block. matcher.reset(); // Overlapping matches are not correctly handled by the current // CFGMutation capabilities. std::unordered_set<IRInstruction*> removed_insns; for (auto& mie : InstructionIterable(block)) { if (!matcher.try_match(mie.insn)) { continue; } m_stats.at(i)++; TRACE(PEEPHOLE, 7, "PATTERN %s MATCHED!", matcher.pattern.name.c_str()); // Check that the anchor has not been removed by a previous match. if (removed_insns.count(matcher.matched_instructions[0])) { std::cerr << "WARNING: Overlapping peephole match!"; matcher.reset(); continue; } // First insert before, as we need the anchor. { auto it = cfg.find_insn(matcher.matched_instructions[0], block); redex_assert(!it.is_end()); auto replace = matcher.get_replacements(); for (const auto& r : replace) { TRACE(PEEPHOLE, 8, "-- %s", SHOW(r)); } mutator.insert_before(it, replace); m_stats_inserted += replace.size(); } // Then remove all the matched instructions. for (auto insn : matcher.matched_instructions) { auto it = cfg.find_insn(insn, block); redex_assert(!it.is_end()); mutator.remove(it); } removed_insns.insert(matcher.matched_instructions.begin(), matcher.matched_instructions.end()); m_stats_removed += matcher.match_index; matcher.reset(); } } // Apply the mutator. mutator.flush(); } code->clear_cfg(); } void print_stats() { TRACE(PEEPHOLE, 1, "%d instructions removed", m_stats_removed); TRACE(PEEPHOLE, 1, "%d instructions inserted", m_stats_inserted); TRACE(PEEPHOLE, 1, "%d net instruction change", m_stats_inserted - m_stats_removed); int64_t num_patterns_matched = 0; for (size_t i = 0; i < m_matchers.size(); ++i) { num_patterns_matched += m_mgr.get_metric(m_matchers[i].pattern.name); } TRACE(PEEPHOLE, 1, "%" PRId64 " patterns matched and replaced", num_patterns_matched); TRACE(PEEPHOLE, 5, "Detailed pattern match stats:"); for (size_t i = 0; i < m_matchers.size(); ++i) { std::string current_pattern_name = m_matchers[i].pattern.name; TRACE(PEEPHOLE, 5, "%s: %" PRId64, current_pattern_name.c_str(), m_mgr.get_metric(current_pattern_name)); } } void run_method(DexMethod* m) { if (m->get_code()) { peephole(m); } } void incr_all_metrics() { for (size_t i = 0; i < m_matchers.size(); i++) { m_mgr.incr_metric(m_matchers[i].pattern.name, m_stats[i]); } } }; } // namespace void PeepholePass::run_pass(DexStoresVector& stores, ConfigFiles& /*cfg*/, PassManager& mgr) { auto scope = build_class_scope(stores); auto num_threads = redex_parallel::default_num_threads(); std::vector<std::unique_ptr<PeepholeOptimizer>> peephole_optimizers; const std::vector<std::vector<Pattern>> pats = patterns::get_all_patterns(); for (size_t i = 0; i < num_threads; ++i) { peephole_optimizers.emplace_back(std::make_unique<PeepholeOptimizer>( mgr, pats, config.disabled_peepholes)); } workqueue_run<DexClass*>( [&peephole_optimizers](sparta::SpartaWorkerState<DexClass*>* state, DexClass* cls) { auto& ph = peephole_optimizers[state->worker_id()]; for (const auto& m : cls->get_dmethods()) { TraceContext context(m); ph->run_method(m); } for (const auto& m : cls->get_vmethods()) { TraceContext context(m); ph->run_method(m); } }, scope, num_threads); for (size_t i = 0; i < num_threads; ++i) { peephole_optimizers[i]->incr_all_metrics(); } if (!contains<std::string>(config.disabled_peepholes, RedundantCheckCastRemover::get_name())) { RedundantCheckCastRemover(mgr, scope).run(); } else { TRACE(PEEPHOLE, 2, "not running disabled peephole opt %s", RedundantCheckCastRemover::get_name().c_str()); } } static PeepholePass s_pass;