opt/delsuper/DelSuper.cpp (263 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 "DelSuper.h" #include <algorithm> #include <string> #include <unordered_map> #include <vector> #include "DexClass.h" #include "DexUtil.h" #include "IRInstruction.h" #include "PassManager.h" #include "ReachableClasses.h" #include "Show.h" #include "Trace.h" #include "Walkers.h" namespace { constexpr const char* METRIC_TOTAL_METHODS = "num_total_methods"; constexpr const char* METRIC_TRIVIAL_METHOD_CANDIDATES = "num_trivial_method_candidates"; constexpr const char* METRIC_REMOVED_TRIVIAL_METHODS = "num_removed_trivial_methods"; constexpr const char* METRIC_METHOD_RELAXED_VISIBILITY = "num_methods_relaxed_visibility"; constexpr const char* METRIC_CLASS_RELAXED_VISIBILITY = "num_class_relaxed_visibility"; static const IROpcode s_return_invoke_super_void_opcs[2] = {OPCODE_INVOKE_SUPER, OPCODE_RETURN_VOID}; static const IROpcode s_return_invoke_super_opcs[3] = { OPCODE_INVOKE_SUPER, OPCODE_MOVE_RESULT, OPCODE_RETURN}; static const IROpcode s_return_invoke_super_wide_opcs[3] = { OPCODE_INVOKE_SUPER, OPCODE_MOVE_RESULT_WIDE, OPCODE_RETURN_WIDE}; static const IROpcode s_return_invoke_super_obj_opcs[3] = { OPCODE_INVOKE_SUPER, OPCODE_MOVE_RESULT_OBJECT, OPCODE_RETURN_OBJECT}; class DelSuper { private: const std::vector<DexClass*>& m_scope; // trivial return invoke super method -> invoked super method std::unordered_map<DexMethod*, DexMethod*> m_delmeths; int m_num_methods; int m_num_passed; int m_num_trivial; int m_num_relaxed_vis; int m_num_cls_relaxed_vis; int m_num_private; int m_num_culled_no_code; int m_num_culled_too_short; int m_num_culled_not_trivial; int m_num_culled_static; int m_num_culled_name_differs; int m_num_culled_proto_differs; int m_num_culled_return_move_result_differs; int m_num_culled_args_differs; int m_num_culled_super_is_non_public_sdk; int m_num_culled_super_cls_non_public; int m_num_culled_super_not_def; /** * This method ensures that the method arguments pass directly through * to the super invocation, for methods where the prototypes are already, * known to match. * * matching: * * void method(int a1, int a2, int a3) { * super.method(a1, a2, a3); * } * * NOT matching: * * void method(int a1, int a2, int a3) { * super.method(a1, a1, a1); * } * * void method(int a1, int a2, int a3) { * super.method(a3, a2, a1); * } * * void method(int a1, int a2, int a3) { * super.method(a1, a2, 0); * } * * Cases where method prototypes don't even match (e.g. different * number of types of arguments) are filtered before hand so we * don't handle that case here. * */ bool do_invoke_meth_args_pass_through(const DexMethod* meth, const IRInstruction* insn) { redex_assert(insn->opcode() == OPCODE_INVOKE_SUPER); size_t src_idx{0}; for (const auto& mie : InstructionIterable(meth->get_code()->get_param_instructions())) { auto load_param = mie.insn; if (load_param->dest() != insn->src(src_idx++)) { return false; } } return true; } bool are_opcs_equal(const std::vector<IRInstruction*>& insns, const IROpcode* opcs, size_t opcs_len) { if (insns.size() != opcs_len) return false; for (size_t i = 0; i < opcs_len; ++i) { if (insns[i]->opcode() != opcs[i]) return false; } return true; } /** * Trivial return invoke supers are: * * - Must have a body (bytecode) * - Opcodes must be match one pattern exactly (no more, no less): * - invoke-super, return-void (void) * - invoke-super, move-result, return (prim) * - invoke-super, move-result-wide, return-wide (wide prim) * - invoke-super, move-result-object, return-object (obj) * - Not static methods * - Method name must match name of super method * - Method proto must match name of super method * - Method vis must match vis of super method * - Method return src register must match move-result dest register * - Method args must all go into invoke without rearrangement * * Returns the super method, or null if this is not a trivial return invoke * super. */ DexMethod* get_trivial_return_invoke_super(const DexMethod* meth) { const auto* code = meth->get_code(); // Must have code if (!code) { m_num_culled_no_code++; return nullptr; } // TODO: rewrite the following code to not require a random-access // container of instructions std::vector<IRInstruction*> insns; for (const auto& mie : InstructionIterable(meth->get_code())) { if (opcode::is_a_load_param(mie.insn->opcode())) { continue; } insns.emplace_back(mie.insn); } // Must have at least two instructions if (insns.size() < 2) { m_num_culled_too_short++; return nullptr; } // Must satisfy one of the four "trivial invoke super" patterns if (!(are_opcs_equal(insns, s_return_invoke_super_void_opcs, 2) || are_opcs_equal(insns, s_return_invoke_super_opcs, 3) || are_opcs_equal(insns, s_return_invoke_super_wide_opcs, 3) || are_opcs_equal(insns, s_return_invoke_super_obj_opcs, 3))) { m_num_culled_not_trivial++; return nullptr; } // Must not be static if (is_static(meth)) { m_num_culled_static++; return nullptr; } // Must not be private if (is_private(meth)) { m_num_private++; } // For non-void scenarios, capture move-result and return opcodes IRInstruction* move_res_opc = nullptr; IRInstruction* return_opc = nullptr; if (insns.size() == 3) { move_res_opc = insns[1]; return_opc = insns[2]; } m_num_trivial++; // Get invoked method DexMethodRef* invoked_meth = insns[0]->get_method(); // Invoked method name must match if (meth->get_name() != invoked_meth->get_name()) { m_num_culled_name_differs++; return nullptr; } // Invoked method proto must match if (meth->get_proto() != invoked_meth->get_proto()) { m_num_culled_proto_differs++; return nullptr; } // Method return src register must match move-result dest register if (move_res_opc && return_opc && move_res_opc->dest() != return_opc->src(0)) { m_num_culled_return_move_result_differs++; return nullptr; } // Method args must pass through directly if (!do_invoke_meth_args_pass_through(meth, insns[0])) { m_num_culled_args_differs++; return nullptr; } // If the invoked method does not have access flags, we can't operate // on it at all. auto meth_def = invoked_meth->as_def(); if (!meth_def) { m_num_culled_super_not_def++; return nullptr; } // If invoked method is not public, make it public if (!is_public(meth_def)) { if (!meth_def->is_concrete()) { m_num_culled_super_is_non_public_sdk++; return nullptr; } set_public(meth_def); m_num_relaxed_vis++; } auto cls = type_class(meth_def->get_class()); if (!is_public(cls)) { if (cls->is_external()) { m_num_culled_super_cls_non_public++; return nullptr; } set_public(cls); m_num_cls_relaxed_vis++; } return meth_def; } public: explicit DelSuper(const std::vector<DexClass*>& scope) : m_scope(scope), m_num_methods(0), m_num_passed(0), m_num_trivial(0), m_num_relaxed_vis(0), m_num_cls_relaxed_vis(0), m_num_private(0), m_num_culled_no_code(0), m_num_culled_too_short(0), m_num_culled_not_trivial(0), m_num_culled_static(0), m_num_culled_name_differs(0), m_num_culled_proto_differs(0), m_num_culled_return_move_result_differs(0), m_num_culled_args_differs(0), m_num_culled_super_is_non_public_sdk(0), m_num_culled_super_cls_non_public(0), m_num_culled_super_not_def(0) {} void run(bool do_delete, PassManager& mgr) { walk::methods(m_scope, [&](DexMethod* meth) { m_num_methods++; auto invoked_meth = get_trivial_return_invoke_super(meth); if (invoked_meth) { TRACE(SUPER, 5, "Found trivial return invoke-super: %s", SHOW(meth)); m_delmeths.emplace(meth, invoked_meth); m_num_passed++; } }); if (do_delete) { // we technically don't have to rewrite the opcodes -- we could just // remove the method declarations and the runtime semantics would be // unchanged -- but this ensures that we have no more references to // that method_id and can avoid emitting it in the dex output. walk::opcodes( m_scope, [](DexMethod* meth) { return true; }, [&](DexMethod* meth, IRInstruction* insn) { if (opcode::is_an_invoke(insn->opcode())) { auto method = insn->get_method()->as_def(); if (!method) { return; } while (m_delmeths.count(method)) { method = m_delmeths.at(method); } insn->set_method(method); } }); for (const auto& pair : m_delmeths) { auto meth = pair.first; auto clazz = type_class(meth->get_class()); always_assert(meth->is_virtual()); clazz->remove_method(meth); DexMethod::erase_method(meth); DexMethod::delete_method(meth); TRACE(SUPER, 5, "Deleted trivial return invoke-super: %s", SHOW(meth)); } } print_stats(do_delete, mgr); } void print_stats(bool do_delete, PassManager& mgr) { TRACE(SUPER, 1, "Examined %d total methods", m_num_methods); TRACE(SUPER, 1, "Found %d candidate trivial methods", m_num_trivial); TRACE(SUPER, 5, "Culled %d due to super not defined", m_num_culled_super_not_def); TRACE(SUPER, 5, "Culled %d due to method is static", m_num_culled_static); TRACE(SUPER, 5, "Culled %d due to method name doesn't match super", m_num_culled_name_differs); TRACE(SUPER, 5, "Culled %d due to method proto doesn't match super", m_num_culled_proto_differs); TRACE(SUPER, 5, "Culled %d due to method doesn't return move result", m_num_culled_return_move_result_differs); TRACE(SUPER, 5, "Culled %d due to method args doesn't match super", m_num_culled_args_differs); TRACE(SUPER, 5, "Culled %d due to non-public super method in sdk", m_num_culled_super_is_non_public_sdk); TRACE(SUPER, 5, "Culled %d due to non-public super class in sdk", m_num_culled_super_cls_non_public); TRACE(SUPER, 1, "Found %d trivial return invoke-super methods", m_num_passed); if (do_delete) { TRACE(SUPER, 1, "Deleted %d trivial return invoke-super methods", m_num_passed); TRACE(SUPER, 1, "Promoted %d methods to public visibility", m_num_relaxed_vis); TRACE(SUPER, 1, "Promoted %d classes to public visibility", m_num_cls_relaxed_vis); } else { TRACE(SUPER, 1, "Preview-only; not performing any changes."); TRACE(SUPER, 1, "Would delete %d trivial return invoke-super methods", m_num_passed); TRACE(SUPER, 1, "Would promote %d methods to public visibility", m_num_relaxed_vis); TRACE(SUPER, 1, "Would promote %d classes to public visibility", m_num_cls_relaxed_vis); } mgr.incr_metric(METRIC_TOTAL_METHODS, m_num_methods); mgr.incr_metric(METRIC_TRIVIAL_METHOD_CANDIDATES, m_num_trivial); mgr.incr_metric(METRIC_REMOVED_TRIVIAL_METHODS, m_num_passed); mgr.incr_metric(METRIC_METHOD_RELAXED_VISIBILITY, m_num_relaxed_vis); mgr.incr_metric(METRIC_CLASS_RELAXED_VISIBILITY, m_num_cls_relaxed_vis); } }; } // namespace void DelSuperPass::run_pass(DexStoresVector& stores, ConfigFiles& /* conf */, PassManager& mgr) { const auto& scope = build_class_scope(stores); DelSuper(scope).run(/* do_delete = */ true, mgr); } static DelSuperPass s_pass;