opt/instrument/BlockInstrument.cpp (1,265 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 "BlockInstrument.h" #include "ConfigFiles.h" #include "DexClass.h" #include "DexUtil.h" #include "GraphUtil.h" #include "MethodReference.h" #include "ScopedMetrics.h" #include "Show.h" #include "SourceBlocks.h" #include "TypeSystem.h" #include "Walkers.h" #include <boost/algorithm/string/join.hpp> #include <fstream> #include <map> #include <string> #include <unordered_map> #include <unordered_set> #include <vector> using namespace instrument; namespace { constexpr bool DEBUG_CFG = false; constexpr size_t BIT_VECTOR_SIZE = 16; constexpr int PROFILING_DATA_VERSION = 3; using OnMethodExitMap = std::map<size_t, // arity of vector arguments (excluding `int offset`) DexMethod*>; enum class BlockType { Unspecified = 0, Instrumentable = 1 << 0, Empty = 1 << 1, Useless = 1 << 2, Normal = 1 << 3, Catch = 1 << 4, MoveException = 1 << 5, NoSourceBlock = 1 << 6, }; enum class InstrumentedType { // Too many basic blocks. We only did method tracing. MethodOnly = 1, Both = 2, // Rare cases: due to infinite loops, no onMethodExit was instrumented. UnableToTrackBlock = 3, }; inline BlockType operator|(BlockType a, BlockType b) { return static_cast<BlockType>(static_cast<int>(a) | static_cast<int>(b)); } inline BlockType operator&(BlockType a, BlockType b) { return static_cast<BlockType>(static_cast<int>(a) & static_cast<int>(b)); } inline BlockType operator^(BlockType a, BlockType b) { return static_cast<BlockType>(static_cast<int>(a) ^ static_cast<int>(b)); } std::ostream& operator<<(std::ostream& os, const BlockType& bt) { if (bt == BlockType::Unspecified) { os << "Unspecified"; return os; } bool written{false}; auto type = bt; if ((type & BlockType::Instrumentable) == BlockType::Instrumentable) { os << "Instrumentable"; written = true; type = type ^ BlockType::Instrumentable; } if ((type & BlockType::Empty) == BlockType::Empty) { if (written) { os << ","; } os << "Empty"; written = true; type = type ^ BlockType::Empty; } if ((type & BlockType::Useless) == BlockType::Useless) { if (written) { os << ","; } os << "Useless"; written = true; type = type ^ BlockType::Useless; } if ((type & BlockType::Normal) == BlockType::Normal) { if (written) { os << ","; } os << "Normal"; written = true; type = type ^ BlockType::Normal; } if ((type & BlockType::Catch) == BlockType::Catch) { if (written) { os << ","; } os << "Catch"; written = true; type = type ^ BlockType::Catch; } if ((type & BlockType::MoveException) == BlockType::MoveException) { if (written) { os << ","; } os << "MoveException"; written = true; type = type ^ BlockType::MoveException; } if ((type & BlockType::NoSourceBlock) == BlockType::NoSourceBlock) { if (written) { os << ","; } os << "NoSourceBlock"; written = true; type = type ^ BlockType::NoSourceBlock; } if (type != BlockType::Unspecified) { if (written) { os << ","; } os << "Unknown"; } return os; } std::string block_type_str(const BlockType& type) { std::ostringstream oss; oss << type; return oss.str(); } using BitId = size_t; struct BlockInfo { cfg::Block* block; BlockType type; IRList::iterator it; BitId bit_id; std::vector<cfg::Block*> merge_in; BlockInfo(cfg::Block* b, BlockType t, const IRList::iterator& i) : block(b), type(t), it(i), bit_id(std::numeric_limits<BitId>::max()) {} bool is_instrumentable() const { return (type & BlockType::Instrumentable) == BlockType::Instrumentable; } void update_merge(const BlockInfo& rhs) { block = rhs.block; type = rhs.type; it = rhs.it; bit_id = rhs.bit_id; merge_in.insert(merge_in.end(), rhs.merge_in.begin(), rhs.merge_in.end()); } }; struct MethodInfo { const DexMethod* method = nullptr; // All eligible methods are at least method instrumented. This indicates // whether this method is only method instrumented because of too many blocks. bool too_many_blocks = false; // The offset is used in `short[] DynamicAnalysis.sMethodStats`. The first two // shorts are for method method profiling, and short[num_vectors] are for // block coverages. size_t offset = 0; size_t num_non_entry_blocks = 0; size_t num_vectors = 0; size_t num_exit_calls = 0; size_t num_empty_blocks = 0; size_t num_useless_blocks = 0; size_t num_no_source_blocks = 0; size_t num_blocks_too_large = 0; size_t num_catches = 0; size_t num_instrumented_catches = 0; size_t num_instrumented_blocks = 0; size_t num_merged{0}; size_t num_merged_not_instrumented{0}; std::vector<cfg::BlockId> bit_id_2_block_id; std::vector<std::vector<SourceBlock*>> bit_id_2_source_blocks; std::map<cfg::BlockId, BlockType> rejected_blocks; std::vector<SourceBlock*> entry_source_blocks; // For stats. size_t num_too_many_blocks{0}; MethodInfo& operator+=(const MethodInfo& rhs) { num_non_entry_blocks += rhs.num_non_entry_blocks; num_vectors += rhs.num_vectors; num_exit_calls += rhs.num_exit_calls; num_empty_blocks += rhs.num_empty_blocks; num_useless_blocks += rhs.num_useless_blocks; num_no_source_blocks += rhs.num_no_source_blocks; num_blocks_too_large += rhs.num_blocks_too_large; num_catches += rhs.num_catches; num_instrumented_catches += rhs.num_instrumented_catches; num_instrumented_blocks += rhs.num_instrumented_blocks; num_merged += rhs.num_merged; num_merged_not_instrumented += rhs.num_merged_not_instrumented; num_too_many_blocks += rhs.num_too_many_blocks; return *this; } }; InstrumentedType get_instrumented_type(const MethodInfo& i) { if (i.too_many_blocks) { return InstrumentedType::MethodOnly; } else if (i.num_exit_calls == 0 && i.num_vectors != 0) { return InstrumentedType::UnableToTrackBlock; } else { return InstrumentedType::Both; } } using MethodDictionary = std::unordered_map<const DexString*, size_t>; MethodDictionary create_method_dictionary( const std::string& file_name, const std::vector<MethodInfo>& all_info) { std::unordered_set<const DexString*> methods_set; for (const auto& info : all_info) { methods_set.insert(info.method->get_deobfuscated_name_or_null()); for (const auto& sb_vec : info.bit_id_2_source_blocks) { for (const auto* sb : sb_vec) { methods_set.insert(sb->src); } } for (const auto* sb : info.entry_source_blocks) { methods_set.insert(sb->src); } } std::vector<const DexString*> methods(methods_set.begin(), methods_set.end()); std::sort(methods.begin(), methods.end(), compare_dexstrings); size_t idx{0}; std::ofstream ofs(file_name, std::ofstream::out | std::ofstream::trunc); ofs << "type,version\nredex-source-block-method-dictionary,1\n"; ofs << "index,deob_name\n"; MethodDictionary method_dictionary; for (const auto* m : methods) { method_dictionary.emplace(m, idx); ofs << idx << "," << m->str() << "\n"; ++idx; } return method_dictionary; } void write_metadata(const ConfigFiles& cfg, const std::string& metadata_base_file_name, const std::vector<MethodInfo>& all_info) { const auto method_dict = create_method_dictionary( cfg.metafile("redex-source-block-method-dictionary.csv"), all_info); // Write a short metadata of this metadata file in the first two lines. auto file_name = cfg.metafile(metadata_base_file_name); std::ofstream ofs(file_name, std::ofstream::out | std::ofstream::trunc); ofs << "profile_type,version,num_methods" << std::endl; ofs << "basic-block-tracing," << PROFILING_DATA_VERSION << "," << all_info.size() << std::endl; // The real CSV-style metadata follows. const std::array<std::string, 8> headers = { "offset", "name", "instrument", "non_entry_blocks", "vectors", "bit_id_2_block_id", "rejected_blocks", "src_blocks"}; ofs << boost::algorithm::join(headers, ",") << "\n"; auto write_block_id_map = [](const auto& bit_id_2_block_id) { std::vector<std::string> fields; fields.reserve(bit_id_2_block_id.size()); for (cfg::BlockId id : bit_id_2_block_id) { fields.emplace_back(std::to_string(id)); } return boost::algorithm::join(fields, ";"); }; auto rejected_blocks = [](const auto& rejected_blocks) { std::vector<std::string> fields; fields.reserve(rejected_blocks.size()); for (const auto& p : rejected_blocks) { fields.emplace_back(std::to_string(p.first) + ":" + std::to_string(static_cast<int>(p.second))); } return boost::algorithm::join(fields, ";"); }; auto to_hex = [](auto n) { std::stringstream ss; ss << std::setfill('0') << std::setw(sizeof(n) * 2) << std::hex << n; return ss.str(); }; auto source_blocks = [&method_dict](const auto& entry_source_blocks, const auto& bit_id_2_source_blocks) { std::stringstream ss; bool first1 = true; auto handle_source_blocks = [&first1, &ss, &method_dict](const auto& v) { if (first1) { first1 = false; } else { ss << ";"; } bool first2 = true; for (auto* sb : v) { // Do not list synthetic source blocks. if (sb->id == SourceBlock::kSyntheticId) { continue; } if (first2) { first2 = false; } else { ss << "|"; } ss << method_dict.at(sb->src) << "#" << sb->id; } }; // Entry block. handle_source_blocks(entry_source_blocks); for (const auto& v : bit_id_2_source_blocks) { handle_source_blocks(v); } return ss.str(); }; for (const auto& info : all_info) { const std::array<std::string, 8> fields = { std::to_string(info.offset), std::to_string( method_dict.at(info.method->get_deobfuscated_name_or_null())), std::to_string(static_cast<int>(get_instrumented_type(info))), std::to_string(info.num_non_entry_blocks), std::to_string(info.num_vectors), write_block_id_map(info.bit_id_2_block_id), rejected_blocks(info.rejected_blocks), source_blocks(info.entry_source_blocks, info.bit_id_2_source_blocks), }; ofs << boost::algorithm::join(fields, ",") << "\n"; } TRACE(INSTRUMENT, 2, "Metadata file was written to: %s", SHOW(file_name)); } std::vector<cfg::Block*> only_terminal_return_or_throw_blocks( cfg::ControlFlowGraph& cfg) { // For example, `real_exit_blocks` returns the following 4 exit blocks. But we // don't need to instrument exit blocks that are still with successors. // // Block B22: <== exit block // preds: (goto B20) // OPCODE: MONITOR_EXIT v3 // succs: (goto B23) (throw B42) // Block B23: <== exit block to be instrumented // preds: (goto B22) // OPCODE: RETURN_VOID // succs: // ... // Block B42: <== exit block // preds: (throw B4) (throw B2) (throw B20) (throw B19) .. // OPCODE: MOVE_EXCEPTION v9 // OPCODE: MONITOR_EXIT v3 // succs: (throw B42) (goto B44) // Block B44: <== exit block to be instrumented // preds: (goto B42) // [0x7f3b1745c440] OPCODE: THROW v9 // succs: // // And note that as of now, we don't consider infinite loop only methods. std::vector<cfg::Block*> blocks = cfg.real_exit_blocks(false); // So, we extract really real exit blocks without any successors. blocks.erase( std::remove_if(blocks.begin(), blocks.end(), [](const auto& b) { return !b->succs().empty(); }), blocks.end()); return blocks; } IRList::iterator get_first_non_move_result_insn(cfg::Block* b) { for (auto it = b->begin(); it != b->end(); ++it) { if (it->type != MFLOW_OPCODE) { continue; } if (!opcode::is_move_result_any(it->insn->opcode())) { return it; } } return b->end(); } IRList::iterator get_first_next_of_move_except(cfg::Block* b) { IRList::iterator insert_pos = std::next(b->get_first_insn()); while (insert_pos != b->end() && insert_pos->type != MFLOW_OPCODE) { insert_pos = std::next(insert_pos); } return insert_pos; } OnMethodExitMap build_onMethodExit_map(const DexClass& cls, const std::string& onMethodExit_name) { OnMethodExitMap onMethodExit_map; for (const auto& m : cls.get_dmethods()) { const auto& name = m->get_name()->str(); if (onMethodExit_name != name) { continue; } // The prototype of onMethodExit must be one of: // - onMethodExit(int offset), or // - onMethodExit(int offset, short vec1, ..., short vecN); const auto* args = m->get_proto()->get_args(); if (args->empty() || *args->begin() != DexType::make_type("I") || std::any_of( std::next(args->begin(), 1), args->end(), [](const auto& type) { return type != DexType::make_type("S"); })) { always_assert_log( false, "[InstrumentPass] error: Proto type of onMethodExit must be " "(int) or (int, short, ..., short), but it was %s", show(m->get_proto()).c_str()); } // -1 is to exclude `int offset`. onMethodExit_map[args->size() - 1] = m; } if (onMethodExit_map.empty()) { std::stringstream ss; for (const auto& m : cls.get_dmethods()) { ss << " " << show(m) << std::endl; } always_assert_log(false, "[InstrumentPass] error: cannot find %s in %s:\n%s", onMethodExit_name.c_str(), SHOW(cls), ss.str().c_str()); } return onMethodExit_map; } DexMethod* load_onMethodBegin(const DexClass& cls, const std::string& method_name) { for (const auto& m : cls.get_dmethods()) { const auto& name = m->get_name()->str(); if (method_name != name) { continue; } const auto* args = m->get_proto()->get_args(); if (args->size() != 1 || *args->begin() != DexType::make_type("I")) { always_assert_log( false, "[InstrumentPass] error: Proto type of onMethodBegin must be " "onMethodBegin(int), but it was %s", show(m->get_proto()).c_str()); } return m; } std::stringstream ss; for (const auto& m : cls.get_dmethods()) { ss << " " << show(m) << std::endl; } always_assert_log(false, "[InstrumentPass] error: cannot find %s in %s:\n%s", method_name.c_str(), SHOW(cls), ss.str().c_str()); } auto insert_prologue_insts(cfg::ControlFlowGraph& cfg, DexMethod* onMethodBegin, const size_t num_vectors, const size_t method_offset, std::vector<BlockInfo>& blocks) { std::vector<reg_t> reg_vectors(num_vectors); std::vector<IRInstruction*> prologues(num_vectors + 2); // Create instructions to allocate a set of 16-bit bit vectors. for (size_t i = 0; i < num_vectors; ++i) { prologues.at(i) = new IRInstruction(OPCODE_CONST); prologues.at(i)->set_literal(0); reg_vectors.at(i) = cfg.allocate_temp(); prologues.at(i)->set_dest(reg_vectors.at(i)); } // Do onMethodBegin instrumentation. We allocate a register that holds the // method offset, which is used for all onMethodBegin/Exit. IRInstruction* method_offset_inst = new IRInstruction(OPCODE_CONST); method_offset_inst->set_literal(method_offset); const reg_t reg_method_offset = cfg.allocate_temp(); method_offset_inst->set_dest(reg_method_offset); prologues.at(num_vectors) = method_offset_inst; IRInstruction* invoke_inst = new IRInstruction(OPCODE_INVOKE_STATIC); invoke_inst->set_method(onMethodBegin); invoke_inst->set_srcs_size(1); invoke_inst->set_src(0, reg_method_offset); prologues.at(num_vectors + 1) = invoke_inst; auto eb = cfg.entry_block(); IRInstruction* eb_insn = nullptr; if (!blocks.empty()) { auto& b = blocks.front(); if (b.block == eb && b.it != b.block->end()) { eb_insn = b.it->insn; } } // Insert all prologue opcodes to the entry block (right after param loading). cfg.entry_block()->insert_before( cfg.entry_block()->to_cfg_instruction_iterator( cfg.entry_block()->get_first_non_param_loading_insn()), prologues); // If the entry block was to be instrumented, the iterator is invalid now. if (eb_insn != nullptr) { auto& b = blocks.front(); auto it = cfg.find_insn(eb_insn); b.block = it.block(); b.it = it.unwrap(); } if (slow_invariants_debug) { for (const auto& b : blocks) { if (b.block == eb) { continue; } auto is_in_block = [&]() { for (auto it = b.block->begin(); it != b.block->end(); ++it) { if (it == b.it) { return true; } } return false; }; assert_log(b.block->end() == b.it || is_in_block(), "%s\n%s", SHOW(b.block), SHOW(*b.it)); } } return std::make_tuple(reg_vectors, reg_method_offset); } size_t insert_onMethodExit_calls( cfg::ControlFlowGraph& cfg, const std::vector<reg_t>& reg_vectors, // May be empty const size_t method_offset, const reg_t reg_method_offset, const std::map<size_t, DexMethod*>& onMethodExit_map, const size_t max_vector_arity) { // If reg_vectors is emptry (methods with a single entry block), no need to // instrument onMethodExit. if (reg_vectors.empty()) { return 0; } // When a method exits, we call onMethodExit to pass all vectors to record. // onMethodExit is overloaded to some degrees (e.g., up to 5 vectors). If // number of vectors > 5, generate one or more onMethodExit calls. const size_t num_vectors = reg_vectors.size(); const size_t num_invokes = std::max(1., std::ceil(double(num_vectors) / double(max_vector_arity))); auto create_invoke_insts = [&]() -> auto { // This code works in case of num_invokes == 1. std::vector<IRInstruction*> invoke_insts(num_invokes * 2 - 1); size_t offset = method_offset; for (size_t i = 0, v = num_vectors; i < num_invokes; ++i, v -= max_vector_arity) { const size_t arity = std::min(v, max_vector_arity); IRInstruction* inst = new IRInstruction(OPCODE_INVOKE_STATIC); inst->set_method(onMethodExit_map.at(arity)); inst->set_srcs_size(arity + 1); inst->set_src(0, reg_method_offset); for (size_t j = 0; j < arity; ++j) { inst->set_src(j + 1, reg_vectors[max_vector_arity * i + j]); } invoke_insts.at(i * 2) = inst; if (i != num_invokes - 1) { inst = new IRInstruction(OPCODE_CONST); // Move forward the offset. offset += max_vector_arity; inst->set_literal(offset); inst->set_dest(reg_method_offset); invoke_insts.at(i * 2 + 1) = inst; } } return invoke_insts; }; // Which blocks should have onMethodExits? Let's ignore infinite loop cases, // and do on returns/throws that have no successors. // Deduping these blocks can help. But it turns out it is too restricted // because it is sensitive to registers. As such, we do this manually. // // Because of catch handlers this is more complicated than it should be. We // do need duplicates to retain the right throw edges. // // For simplicity we will always rename the throw/return-non-void register. // That is easier than remembering and fixing it up later, and reg-alloc // should be able to deal with it. using CatchCoverage = std::vector<std::pair<const DexType*, const cfg::Block*>>; auto create_catch_coverage = [](const cfg::Block* b) { auto index_order = b->get_outgoing_throws_in_order(); CatchCoverage ret{}; ret.reserve(index_order.size()); std::transform(index_order.begin(), index_order.end(), std::back_inserter(ret), [](auto e) { return std::pair<const DexType*, const cfg::Block*>( e->throw_info()->catch_type, e->target()); }); return ret; }; struct CatchCoverageHash { std::size_t operator()(const CatchCoverage& key) const { std::size_t seed = 0; boost::hash_range(seed, key.begin(), key.end()); return seed; } }; using DedupeMap = std::unordered_map<CatchCoverage, cfg::Block*, CatchCoverageHash>; enum RegType { kNone, kObject, kInt, kWide, }; auto handle_instrumentation = [&cfg, &create_invoke_insts]( DedupeMap& map, std::optional<reg_t>& tmp_reg, cfg::Block* b, CatchCoverage& cv, RegType reg_type) { auto pushback_move = [reg_type](cfg::Block* b, reg_t from, reg_t to) { auto move_insn = new IRInstruction(reg_type == RegType::kObject ? OPCODE_MOVE_OBJECT : reg_type == RegType::kWide ? OPCODE_MOVE_WIDE : OPCODE_MOVE); move_insn->set_src(0, from); move_insn->set_dest(to); b->push_back(move_insn); }; auto it = map.find(cv); if (it == map.end()) { // Split before the last instruction. auto new_pred = cfg.split_block_before(b, b->get_last_insn()); auto last_insn = b->get_last_insn()->insn; // If there is a reg involved, check for a temp reg, rename the // operand operand, and insert a move. if (reg_type != RegType::kNone) { // First time, allocate a temp reg. if (!tmp_reg) { tmp_reg = reg_type == RegType::kWide ? cfg.allocate_wide_temp() : cfg.allocate_temp(); } // Insert a move. pushback_move(new_pred, last_insn->src(0), *tmp_reg); // Change the return's operand. last_insn->set_src(0, *tmp_reg); } // Now instrument the return. b->insert_before(b->to_cfg_instruction_iterator(b->get_last_insn()), create_invoke_insts()); // And store in the cache. map.emplace(std::move(cv), b); } else { auto last_insn = b->get_last_insn()->insn; std::optional<reg_t> ret_reg = reg_type == RegType::kNone ? std::nullopt : std::optional<reg_t>(last_insn->src(0)); // Delete the last instruction, possibly add an aligning move, then // fall-through. b->remove_insn(b->get_last_insn()); if (ret_reg) { redex_assert(tmp_reg); pushback_move(b, *ret_reg, *tmp_reg); } cfg.add_edge(b, it->second, cfg::EdgeType::EDGE_GOTO); } }; DedupeMap return_map{}; DedupeMap throw_map{}; std::optional<reg_t> return_temp_reg{std::nullopt}; std::optional<reg_t> throw_temp_reg{std::nullopt}; const auto& exit_blocks = only_terminal_return_or_throw_blocks(cfg); for (cfg::Block* b : exit_blocks) { assert(b->succs().empty()); auto cv = create_catch_coverage(b); if (b->branchingness() == opcode::Branchingness::BRANCH_RETURN) { auto ret_insn = b->get_last_insn()->insn; auto ret_opcode = ret_insn->opcode(); redex_assert(opcode::is_a_return(ret_opcode)); handle_instrumentation( return_map, return_temp_reg, b, cv, opcode::is_return_void(ret_opcode) ? RegType::kNone : opcode::is_return_object(ret_opcode) ? RegType::kObject : opcode::is_return_wide(ret_opcode) ? RegType::kWide : RegType::kInt); redex_assert(return_temp_reg || opcode::is_return_void(ret_opcode)); } else { redex_assert(b->branchingness() == opcode::Branchingness::BRANCH_THROW); handle_instrumentation(throw_map, throw_temp_reg, b, cv, RegType::kObject); } } return exit_blocks.size(); } // Very simplistic setup: if we think we can elide putting instrumentation into // a block by pushing the source blocks into the next, we will do it - under the // strong assumption that two "empty/useless" blocks do not usually follow each // other. void create_block_info( const DexMethod* method, cfg::Block* block, const InstrumentPass::Options& options, const std::unordered_map<const cfg::Block*, BlockInfo*>& block_mapping) { auto* trg_block_info = block_mapping.at(block); auto trace_at_exit = at_scope_exit([&]() { TRACE(INSTRUMENT, 9, "Checking block B%zu for %s: %x=%s\n%s", block->id(), SHOW(method), (uint32_t)trg_block_info->type, block_type_str(trg_block_info->type).c_str(), SHOW(block)); }); // `Block.num_opcodes` skips internal opcodes, but we need the source // blocks. auto has_opcodes = [&]() { for (auto& mie : *block) { if (mie.type == MFLOW_OPCODE) { return true; } } return false; }(); // See if this is a simple chain. For that the current block must have only // one out edge of type GOTO, and the target must have only when in edge. // Otherwise pushing the source blocks over would lose precision. auto single_next_ok = [&]() -> std::optional<const cfg::Block*> { // Find the target block, if any. const auto& succs = block->succs(); if (succs.empty()) { return std::nullopt; } // Check forward direction. if (succs.size() != 1 || succs[0]->type() != cfg::EDGE_GOTO || succs[0]->target() == block->cfg().entry_block() || succs[0]->target() == block) { return std::nullopt; } const auto* trg_block = succs[0]->target(); const auto& preds = trg_block->preds(); redex_assert(!preds.empty()); if (preds.size() != 1) { return std::nullopt; } // Really assume the integrity of the CFG here... return trg_block; }; if (!has_opcodes) { if (!source_blocks::has_source_blocks(block)) { trg_block_info->update_merge({block, BlockType::Empty, {}}); return; } TRACE(INSTRUMENT, 9, "%s Block B%zu has no opcodes but source blocks!\n%s", SHOW(method), block->id(), SHOW(block->cfg())); // Find the target block, if any. if (auto next_opt = single_next_ok()) { // OK, we can virtually merge the source blocks into the following one. TRACE(INSTRUMENT, 9, "Not instrumenting empty block B%zu", block->id()); block_mapping.at(*next_opt)->merge_in.push_back(block); trg_block_info->update_merge({block, BlockType::Empty, {}}); return; } } // TODO: There is a potential register allocation issue when we instrument // extremely large number of basic blocks. We've found a case. So, for now, // we don't instrument catch blocks with the hope these blocks are cold. if (block->is_catch() && !options.instrument_catches) { trg_block_info->update_merge({block, BlockType::Catch, {}}); return; } IRList::iterator insert_pos; BlockType type = block->is_catch() ? BlockType::Catch : BlockType::Normal; if (block->starts_with_move_result()) { insert_pos = get_first_non_move_result_insn(block); } else if (block->starts_with_move_exception()) { // move-exception must only ever occur as the first instruction of an // exception handler; anywhere else is invalid. So, take the next // instruction of the move-exception. insert_pos = get_first_next_of_move_except(block); type = type | BlockType::MoveException; } else { insert_pos = block->get_first_non_param_loading_insn(); } if (insert_pos == block->end()) { if (source_blocks::has_source_blocks(block)) { if (auto next_opt = single_next_ok()) { // OK, we can virtually merge the source blocks into the following one. TRACE(INSTRUMENT, 9, "Not instrumenting useless block B%zu\n%s", block->id(), SHOW(block)); block_mapping.at(*next_opt)->merge_in.push_back(block); trg_block_info->update_merge({block, BlockType::Useless, {}}); return; } } } // No source block? Then we can't map back block coverage data to source // block. No need to instrument unless this block is exit block (no succs). // Exit blocks will have onMethodEnd. We still need to instrument anyhow. if (!options.instrument_blocks_without_source_block && !source_blocks::has_source_blocks(block) && !block->succs().empty()) { trg_block_info->update_merge({block, BlockType::NoSourceBlock | type, {}}); return; } trg_block_info->update_merge( {block, BlockType::Instrumentable | type, insert_pos}); } auto get_blocks_to_instrument(const DexMethod* m, const cfg::ControlFlowGraph& cfg, const size_t max_num_blocks, const InstrumentPass::Options& options) { // Collect basic blocks in the order of the source blocks (DFS). std::vector<cfg::Block*> blocks; auto block_start_fn = [&](cfg::Block* b) { // We don't instrument entry block. // // But there's an exceptional case. If the entry block is in a try-catch // (which actually happens very rarely), inserting onMethodBegin will create // an additional block because onMethodBegin may throw. The original entry // block becomes non-entry. In this case, we still instrument the entry // block at this moment. See testFunc10 in InstrumentBasicBlockTarget.java. // // So, don't add entry block if it is not in any try-catch. if (cfg.entry_block() == b && cfg.entry_block()->get_outgoing_throws_in_order().empty()) { return; } blocks.push_back(b); }; source_blocks::impl::visit_in_order( &cfg, block_start_fn, [](cfg::Block*, const cfg::Edge*) {}, [](cfg::Block*) {}); // Future work: Pick minimal instrumentation candidates. std::vector<BlockInfo> block_info_list; block_info_list.reserve(blocks.size()); std::unordered_map<const cfg::Block*, BlockInfo*> block_mapping; for (cfg::Block* b : blocks) { block_info_list.emplace_back(b, BlockType::Unspecified, b->end()); block_mapping[b] = &block_info_list.back(); } BitId id = 0; for (cfg::Block* b : blocks) { create_block_info(m, b, options, block_mapping); auto* info = block_mapping[b]; if ((info->type & BlockType::Instrumentable) == BlockType::Instrumentable) { if (id >= max_num_blocks) { // This is effectively rejecting all blocks. return std::make_tuple(std::vector<BlockInfo>{}, BitId(0), true /* too many block */); } info->bit_id = id++; } } redex_assert(std::all_of( block_info_list.begin(), block_info_list.end(), [](const auto& bi) { return bi.type != BlockType::Unspecified; })); return std::make_tuple(block_info_list, id, false); } void insert_block_coverage_computations(const std::vector<BlockInfo>& blocks, const std::vector<reg_t>& reg_vectors) { for (const auto& info : blocks) { if (!info.is_instrumentable()) { continue; } const BitId bit_id = info.bit_id; const size_t vector_id = bit_id / BIT_VECTOR_SIZE; cfg::Block* block = info.block; const auto& insert_pos = info.it; // bit_vectors[vector_id] |= 1 << bit_id' IRInstruction* inst = new IRInstruction(OPCODE_OR_INT_LIT16); inst->set_literal(static_cast<int16_t>(1ULL << (bit_id % BIT_VECTOR_SIZE))); inst->set_src(0, reg_vectors.at(vector_id)); inst->set_dest(reg_vectors.at(vector_id)); block->insert_before(block->to_cfg_instruction_iterator(insert_pos), inst); } } MethodInfo instrument_basic_blocks(IRCode& code, DexMethod* method, DexMethod* onMethodBegin, const OnMethodExitMap& onMethodExit_map, const size_t max_vector_arity, const size_t method_offset, const size_t max_num_blocks, const InstrumentPass::Options& options) { MethodInfo info; info.method = method; using namespace cfg; code.build_cfg(/*editable*/ true); ControlFlowGraph& cfg = code.cfg(); std::string before_cfg = traceEnabled(INSTRUMENT, 7) ? show(cfg) : std::string(""); // Step 1: Get sorted basic blocks to instrument with their information. // // The blocks are sorted in RPO. We don't instrument entry blocks. If too many // blocks, it falls back to empty blocks, which is method tracing. std::vector<BlockInfo> blocks; size_t num_to_instrument; bool too_many_blocks; std::tie(blocks, num_to_instrument, too_many_blocks) = get_blocks_to_instrument(method, cfg, max_num_blocks, options); TRACE(INSTRUMENT, DEBUG_CFG ? 0 : 10, "BEFORE: %s, %s\n%s", show_deobfuscated(method).c_str(), SHOW(method), SHOW(cfg)); // Step 2: Fill in some info eagerly. This is necessary as later steps may be // modifying the CFG. info.bit_id_2_block_id.reserve(num_to_instrument); info.bit_id_2_source_blocks.reserve(num_to_instrument); for (const auto& i : blocks) { if (i.is_instrumentable()) { info.bit_id_2_block_id.push_back(i.block->id()); info.bit_id_2_source_blocks.emplace_back( source_blocks::gather_source_blocks(i.block)); for (auto* merged_block : i.merge_in) { auto& vec = info.bit_id_2_source_blocks.back(); auto sb_vec = source_blocks::gather_source_blocks(merged_block); vec.insert(vec.end(), sb_vec.begin(), sb_vec.end()); } TRACE(INSTRUMENT, 10, "%s Block %zu: idx=%zu SBs=%s", show_deobfuscated(method).c_str(), i.block->id(), info.bit_id_2_block_id.size() - 1, [&]() { std::string ret; for (auto* sb : info.bit_id_2_source_blocks.back()) { ret.append(sb->show()); ret.append(";"); } return ret; }() .c_str()); } else { info.rejected_blocks[i.block->id()] = i.type; } } // Step 3: Insert onMethodBegin to track method execution, and bit-vector // allocation code in its method entry point. // const size_t origin_num_non_entry_blocks = cfg.blocks().size() - 1; const size_t num_vectors = std::ceil(num_to_instrument / double(BIT_VECTOR_SIZE)); std::vector<reg_t> reg_vectors; reg_t reg_method_offset; std::tie(reg_vectors, reg_method_offset) = insert_prologue_insts( cfg, onMethodBegin, num_vectors, method_offset, blocks); const size_t after_prologue_num_non_entry_blocks = cfg.blocks().size() - 1; // Step 4: Insert block coverage update instructions to each blocks. // insert_block_coverage_computations(blocks, reg_vectors); TRACE(INSTRUMENT, DEBUG_CFG ? 0 : 10, "WITH COVERAGE INSNS: %s, %s\n%s", show_deobfuscated(method).c_str(), SHOW(method), SHOW(cfg)); // Gather early as step 4 may modify CFG. auto num_non_entry_blocks = cfg.blocks().size() - 1; // Step 5: Insert onMethodExit in exit block(s). // // TODO: What about no exit blocks possibly due to infinite loops? Such case // is extremely rare in our apps. In this case, let us do method tracing by // instrumenting prologues. const size_t num_exit_calls = insert_onMethodExit_calls( cfg, reg_vectors, method_offset, reg_method_offset, onMethodExit_map, max_vector_arity); cfg.recompute_registers_size(); auto count = [&blocks](BlockType type) -> size_t { return std::count_if(blocks.begin(), blocks.end(), [type](const auto& i) { return (i.type & type) == type; }); }; // When there are too many blocks, collect all source blocks into the entry // block to track them conservatively. info.entry_source_blocks = too_many_blocks ? [&]() { std::vector<SourceBlock*> all; for (auto* b : cfg.blocks()) { auto tmp = source_blocks::gather_source_blocks(b); all.insert(all.end(), tmp.begin(), tmp.end()); } return all; }() : source_blocks::gather_source_blocks(cfg.entry_block()); info.too_many_blocks = too_many_blocks; info.num_too_many_blocks = too_many_blocks ? 1 : 0; info.offset = method_offset; info.num_non_entry_blocks = num_non_entry_blocks; info.num_vectors = num_vectors; info.num_exit_calls = num_exit_calls; info.num_empty_blocks = count(BlockType::Empty); info.num_useless_blocks = count(BlockType::Useless); info.num_no_source_blocks = count(BlockType::NoSourceBlock); info.num_blocks_too_large = too_many_blocks ? info.num_non_entry_blocks : 0; info.num_catches = count(BlockType::Catch) - count(BlockType::Catch | BlockType::Useless); info.num_instrumented_catches = count(BlockType::Catch | BlockType::Instrumentable); info.num_instrumented_blocks = num_to_instrument; always_assert(count(BlockType::Instrumentable) == num_to_instrument); redex_assert(std::none_of(blocks.begin(), blocks.end(), [](const auto& b) { return std::find(b.merge_in.begin(), b.merge_in.end(), b.block) != b.merge_in.end(); })); info.num_merged = std::accumulate( blocks.begin(), blocks.end(), 0, [](auto lhs, const auto& rhs) { return lhs + rhs.merge_in.size(); }); info.num_merged_not_instrumented = std::accumulate( blocks.begin(), blocks.end(), 0, [](auto lhs, const auto& rhs) { return lhs + ((rhs.type & BlockType::Instrumentable) != BlockType::Instrumentable ? rhs.merge_in.size() : 0); }); const size_t num_rejected_blocks = info.num_empty_blocks + info.num_useless_blocks + info.num_no_source_blocks + info.num_blocks_too_large + (info.num_catches - info.num_instrumented_catches); always_assert(info.num_non_entry_blocks == info.num_instrumented_blocks + num_rejected_blocks); always_assert(too_many_blocks || info.rejected_blocks.size() == num_rejected_blocks); TRACE(INSTRUMENT, DEBUG_CFG ? 0 : 10, "AFTER: %s, %s\n%s", show_deobfuscated(method).c_str(), SHOW(method), SHOW(cfg)); // Check the post condition: // num_instrumented_blocks == num_non_entry_blocks - num_rejected_blocks if (get_instrumented_type(info) != InstrumentedType::MethodOnly && num_to_instrument != info.num_non_entry_blocks - info.rejected_blocks.size()) { TRACE(INSTRUMENT, 7, "Post condition violation! in %s", SHOW(method)); TRACE(INSTRUMENT, 7, "- Instrumented type: %d", get_instrumented_type(info)); TRACE(INSTRUMENT, 7, " %zu != %zu - %zu", num_to_instrument, info.num_non_entry_blocks, info.rejected_blocks.size()); TRACE(INSTRUMENT, 7, " original non-entry blocks: %zu", origin_num_non_entry_blocks); TRACE(INSTRUMENT, 7, " after prologue instrumentation: %zu", after_prologue_num_non_entry_blocks); TRACE(INSTRUMENT, 7, "===== BEFORE CFG"); TRACE(INSTRUMENT, 7, "%s", SHOW(before_cfg)); TRACE(INSTRUMENT, 7, "===== AFTER CFG"); TRACE(INSTRUMENT, 7, "%s", SHOW(cfg)); } code.clear_cfg(); return info; } std::unordered_set<std::string> get_cold_start_classes(ConfigFiles& cfg) { auto interdex_list = cfg.get_coldstart_classes(); std::unordered_set<std::string> cold_start_classes; std::string dex_end_marker0("LDexEndMarker0;"); for (auto class_string : interdex_list) { if (class_string == dex_end_marker0) { break; } class_string.back() = '/'; cold_start_classes.insert(class_string); } return cold_start_classes; } void print_stats(ScopedMetrics& sm, const std::vector<MethodInfo>& instrumented_methods, const size_t max_num_blocks) { MethodInfo total{}; for (const auto& i : instrumented_methods) { total += i; } const size_t total_instrumented = instrumented_methods.size(); const size_t only_method_instrumented = total.num_too_many_blocks; const size_t total_block_instrumented = total_instrumented - only_method_instrumented; auto print = [](size_t num, size_t total, size_t& accumulate) { std::stringstream ss; accumulate += num; ss << std::fixed << std::setprecision(3) << std::setw(6) << num << " (" << std::setw(6) << (num * 100. / total) << "%, " << std::setw(6) << (accumulate * 100. / total) << "%)"; return ss.str(); }; auto divide = [](size_t a, size_t b) { if (b == 0) { return std::string("N/A"); } std::stringstream ss; ss << std::fixed << std::setprecision(4) << double(a) / double(b); return ss.str(); }; // ----- Print summary { auto summary_scope = sm.scope("summary"); TRACE(INSTRUMENT, 4, "Maximum blocks for block instrumentation: %zu", max_num_blocks); sm.set_metric("max_num_blocks", max_num_blocks); TRACE(INSTRUMENT, 4, "Total instrumented methods: %zu", total_instrumented); sm.set_metric("total_instrumented", total_instrumented); TRACE(INSTRUMENT, 4, "- Block + method instrumented: %zu", total_block_instrumented); sm.set_metric("block_and_method_instrumented", total_block_instrumented); TRACE(INSTRUMENT, 4, "- Only method instrumented: %zu", only_method_instrumented); sm.set_metric("method_instrumented_only", only_method_instrumented); } auto scope_total_avg = [&](const std::string& key, size_t num, size_t denom) { auto scope = sm.scope(key); sm.set_metric("total", num); if (denom != 0) { sm.set_metric("average100", 100 * num / denom); } return scope; }; // ----- Bit-vector stats TRACE(INSTRUMENT, 4, "Bit-vector stats for block instrumented methods:"); { size_t acc = 0; size_t total_bit_vectors = 0; std::map<int /*num_vectors*/, size_t /*num_methods*/> dist; for (const auto& i : instrumented_methods) { if (i.too_many_blocks) { ++dist[-1]; } else { ++dist[i.num_vectors]; total_bit_vectors += i.num_vectors; } } for (const auto& p : dist) { TRACE(INSTRUMENT, 4, " %3d vectors: %s", p.first, SHOW(print(p.second, total_instrumented, acc))); } TRACE(INSTRUMENT, 4, "Total/average bit vectors: %zu, %s", total_bit_vectors, SHOW(divide(total_bit_vectors, total_block_instrumented))); scope_total_avg("bit_vectors", total_bit_vectors, total_block_instrumented); } // ----- Instrumented block stats TRACE(INSTRUMENT, 4, "Instrumented / actual non-entry block stats:"); { std::map<int, std::pair<size_t /*instrumented*/, size_t /*block*/>> dist; for (const auto& i : instrumented_methods) { if (i.too_many_blocks) { ++dist[-1].first; } else { ++dist[i.num_instrumented_blocks].first; } ++dist[i.num_non_entry_blocks].second; } std::array<size_t, 2> accs = {0, 0}; for (const auto& p : dist) { TRACE(INSTRUMENT, 4, " %5d blocks: %s | %s", p.first, SHOW(print(p.second.first, total_instrumented, accs[0])), SHOW(print(p.second.second, total_instrumented, accs[1]))); } TRACE( INSTRUMENT, 4, "Total/average instrumented blocks: %zu, %s", total.num_instrumented_blocks, SHOW(divide(total.num_instrumented_blocks, total_block_instrumented))); scope_total_avg("instrumented_blocks", total.num_instrumented_blocks, total_block_instrumented); TRACE(INSTRUMENT, 4, "Total/average non-entry blocks: %zu, %s", total.num_non_entry_blocks, SHOW(divide(total.num_non_entry_blocks, total_instrumented))); scope_total_avg("non_entry_blocks", total.num_non_entry_blocks, total_block_instrumented); } const size_t total_catches = std::accumulate( instrumented_methods.begin(), instrumented_methods.end(), size_t(0), [](int a, auto&& i) { return a + i.num_catches; }); const size_t total_instrumented_catches = std::accumulate( instrumented_methods.begin(), instrumented_methods.end(), size_t(0), [](int a, auto&& i) { return a + i.num_instrumented_catches; }); // ----- Instrumented/skipped block stats auto print_ratio = [&total](size_t num) { std::stringstream ss; ss << num << std::fixed << std::setprecision(2) << " (" << (num * 100. / total.num_non_entry_blocks) << "%)"; return ss.str(); }; auto metric_ratio = [&sm, &total](const std::string& sub_key, size_t num) { if (total.num_non_entry_blocks == 0) { return; } sm.set_metric(sub_key, num); sm.set_metric(sub_key + ".ratio100.00", 10000 * num / total.num_non_entry_blocks); }; { auto non_entry_scope = sm.scope("non_entry_blocks_stats"); TRACE(INSTRUMENT, 4, "Total non-entry blocks: %zu", total.num_non_entry_blocks); sm.set_metric("total", total.num_non_entry_blocks); TRACE(INSTRUMENT, 4, "- Instrumented blocks: %s", SHOW(print_ratio(total.num_instrumented_blocks))); metric_ratio("total_instrumented_blocks", total.num_instrumented_blocks); TRACE(INSTRUMENT, 4, "- Merged blocks: %s", print_ratio(total.num_merged).c_str()); sm.set_metric("merged", total.num_merged); TRACE(INSTRUMENT, 4, "- Merged blocks (into non-instrumentable): %s", print_ratio(total.num_merged_not_instrumented).c_str()); sm.set_metric("merged_not_instrumentable", total.num_merged_not_instrumented); TRACE(INSTRUMENT, 4, "- Skipped catch blocks: %s", SHOW(print_ratio(total_catches - total_instrumented_catches))); { auto skipped_scope = sm.scope("skipped"); metric_ratio("catch_blocks", total_catches - total_instrumented_catches); auto no_sb = std::accumulate( instrumented_methods.begin(), instrumented_methods.end(), size_t(0), [](size_t a, auto&& i) { return a + i.num_no_source_blocks; }); TRACE(INSTRUMENT, 4, "- Skipped due to no source block: %s", SHOW(print_ratio(no_sb))); metric_ratio("no_source_blocks", no_sb); auto too_large_methods = std::accumulate( instrumented_methods.begin(), instrumented_methods.end(), size_t(0), [](size_t a, auto&& i) { return a + i.num_blocks_too_large; }); TRACE(INSTRUMENT, 4, "- Skipped due to too large methods: %s", SHOW(print_ratio(too_large_methods))); metric_ratio("too_large_methods", too_large_methods); auto empty_blocks = std::accumulate( instrumented_methods.begin(), instrumented_methods.end(), size_t(0), [](size_t a, auto&& i) { return a + i.num_empty_blocks; }); TRACE(INSTRUMENT, 4, "- Skipped empty blocks: %s", SHOW(print_ratio(empty_blocks))); metric_ratio("empty_blocks", empty_blocks); auto useless_blocks = std::accumulate( instrumented_methods.begin(), instrumented_methods.end(), size_t(0), [](size_t a, auto&& i) { return a + i.num_useless_blocks; }); TRACE(INSTRUMENT, 4, "- Skipped useless blocks: %s", SHOW(print_ratio(useless_blocks))); metric_ratio("useless_blocks", useless_blocks); } } // ----- Instrumented exit block stats TRACE(INSTRUMENT, 4, "Instrumented exit block stats:"); { size_t acc = 0; size_t total_exits = 0; size_t no_exit = 0; std::map<int /*num_vectors*/, size_t /*num_methods*/> dist; TRACE(INSTRUMENT, 4, "No onMethodExit but 1+ non-entry blocks:"); int k = 0; for (const auto& i : instrumented_methods) { if (!i.too_many_blocks && i.num_exit_calls == 0 && i.num_non_entry_blocks != 0) { TRACE(INSTRUMENT, 4, "- %d: %zu, %s", ++k, i.num_non_entry_blocks, show_deobfuscated(i.method).c_str()); ++no_exit; } ++dist[i.num_exit_calls]; total_exits += i.num_exit_calls; } for (const auto& p : dist) { TRACE(INSTRUMENT, 4, " %4d exits: %s", p.first, SHOW(print(p.second, total_instrumented, acc))); } TRACE(INSTRUMENT, 4, "Total/average instrumented exits: %zu, %s", total_exits, SHOW(divide(total_exits, total_instrumented))); auto exit_scope = scope_total_avg("instrumented_exits", total_exits, total_instrumented); sm.set_metric("methods_without_exit_calls", no_exit); } // ----- Catch block stats TRACE(INSTRUMENT, 4, "Catch block stats:"); { size_t acc = 0; std::map<int, size_t> dist; for (const auto& i : instrumented_methods) { ++dist[i.num_catches]; } for (const auto& p : dist) { TRACE(INSTRUMENT, 4, " %4d catches: %s", p.first, SHOW(print(p.second, total_instrumented, acc))); } TRACE(INSTRUMENT, 4, "Total/average catch blocks: %zu, %s", total.num_catches, SHOW(divide(total.num_catches, total_instrumented))); scope_total_avg("catch_blocks", total.num_catches, total_instrumented); } auto print_two_dists = [&divide, &print, &instrumented_methods, total_instrumented, total_block_instrumented]( const char* name1, const char* name2, auto&& accessor1, auto&& accessor2) { std::map<int, std::pair<size_t, size_t>> dist; size_t total1 = 0; size_t total2 = 0; for (const auto& i : instrumented_methods) { if (i.too_many_blocks) { ++dist[-1].first; ++dist[-1].second; } else { ++dist[accessor1(i)].first; ++dist[accessor2(i)].second; total1 += accessor1(i); total2 += accessor2(i); } } std::array<size_t, 2> accs = {0, 0}; for (const auto& p : dist) { TRACE(INSTRUMENT, 4, " %5d blocks: %s | %s", p.first, SHOW(print(p.second.first, total_instrumented, accs[0])), SHOW(print(p.second.second, total_instrumented, accs[1]))); } TRACE(INSTRUMENT, 4, "Total/average %s blocks: %zu, %s", name1, total1, SHOW(divide(total1, total_block_instrumented))); TRACE(INSTRUMENT, 4, "Total/average %s blocks: %zu, %s", name2, total2, SHOW(divide(total2, total_block_instrumented))); }; TRACE(INSTRUMENT, 4, "Empty / useless block stats:"); print_two_dists( "empty", "useless", [](auto&& v) { return v.num_empty_blocks; }, [](auto&& v) { return v.num_useless_blocks; }); } } // namespace //------------------------------------------------------------------------------ // A simple basic block instrumentation algorithm using bit vectors: // // Original CFG: // +--------+ +--------+ +--------+ // | block0 | ----> | block1 | ----> | block2 | // | | | | | Return | // +--------+ +--------+ +--------+ // // This CFG is instrumented as following: // - Insert instructions to initialize bit vector(s) at the entry block. // - Set <bb_id>-th bit in the vector using or-lit/16. The bit vector is a // short type. There is no such or-lit/32 instruction. // - Before RETURN, insert INVOKE DynamicAnalysis.onMethodExit(method_id, // bit_vectors), where the recorded bit vectors are reported. // // +------------------+ +------------------+ +-----------------------+ // | * CONST v0, 0 | --> | * OR_LIT16 v0, 2 | --> | * OR_LIT16 v0, 4 | // | * OR_LIT16 v0, 1 | | block1 | | block2 | // | block0 | | | | * CONST v2, method_id | // +------------------+ +------------------+ | * INVOKE v2,v0, ... | // | Return | // +-----------------------+ // // This instrumentation includes the method tracing by inserting onMethodBegin. // We currently don't instrument methods with large number of basic blocks. In // this case, they are only instrumented for method tracing. //------------------------------------------------------------------------------ void BlockInstrumentHelper::do_basic_block_tracing( DexClass* analysis_cls, DexStoresVector& stores, ConfigFiles& cfg, PassManager& pm, const InstrumentPass::Options& options) { // I'm too lazy to support sharding in block instrumentation. Future work. const size_t NUM_SHARDS = options.num_shards; if (NUM_SHARDS != 1 || options.num_stats_per_method != 0) { always_assert_log( false, "[InstrumentPass] error: basic block profiling currently only " "supports num_shard = 1 and num_stats_per_method = 0"); } if (options.analysis_method_names.size() != 2) { always_assert_log(false, "[InstrumentPass] error: basic block profiling must have " "two analysis methods: [onMethodBegin, onMethodExit]"); } const size_t max_num_blocks = options.max_num_blocks; // Even so, we need to update sharded arrays with 1 for the Java-side code. const auto& array_fields = InstrumentPass::patch_sharded_arrays( analysis_cls, NUM_SHARDS, // However, because we have only one shard and don't clone onMethodExits, // we keep the original name. It actually fools patch_sharded_arrays. {{1, InstrumentPass::STATS_FIELD_NAME}}); always_assert(array_fields.size() == NUM_SHARDS); DexMethod* onMethodBegin = load_onMethodBegin(*analysis_cls, options.analysis_method_names[0]); TRACE(INSTRUMENT, 4, "Loaded onMethodBegin: %s", SHOW(onMethodBegin)); const auto& onMethodExit_map = build_onMethodExit_map(*analysis_cls, options.analysis_method_names[1]); const size_t max_vector_arity = onMethodExit_map.rbegin()->first; TRACE(INSTRUMENT, 4, "Max arity for onMethodExit: %zu", max_vector_arity); auto cold_start_classes = get_cold_start_classes(cfg); TRACE(INSTRUMENT, 7, "Cold start classes: %zu", cold_start_classes.size()); // This method_offset is used in sMethodStats[] to locate a method profile. // We have a small header in the beginning of sMethodStats. size_t method_offset = 8; std::vector<MethodInfo> instrumented_methods; int all_methods = 0; int eligibles = 0; int specials = 0; int picked_by_cs = 0; int picked_by_allowlist = 0; int blocklisted = 0; int rejected = 0; int block_instrumented = 0; int non_root_store_methods = 0; Scope scope; if (options.instrument_only_root_store) { DexStoresVector root; for (const auto& store : stores) { if (store.is_root_store()) { root.push_back(store); } else { // We want to collect number of methods that are being excluded. for (const auto& cls : build_class_scope({store})) { non_root_store_methods += cls->get_dmethods().size() + cls->get_vmethods().size(); } } } all_methods += non_root_store_methods; scope = build_class_scope(root); } else { scope = build_class_scope(stores); } walk::code(scope, [&](DexMethod* method, IRCode& code) { TraceContext trace_context(method); all_methods++; if (method == analysis_cls->get_clinit() || method == onMethodBegin) { specials++; return; } if (std::any_of(onMethodExit_map.begin(), onMethodExit_map.end(), [&](const auto& e) { return e.second == method; })) { specials++; return; } eligibles++; if (!options.allowlist.empty() || options.only_cold_start_class) { if (InstrumentPass::is_included(method, options.allowlist)) { picked_by_allowlist++; } else if (InstrumentPass::is_included(method, cold_start_classes)) { picked_by_cs++; } else { // We are using allow or cs list. If not there, reject. rejected++; TRACE(INSTRUMENT, 9, "Not in allow/cold_start: %s, %s", show_deobfuscated(method).c_str(), SHOW(method)); return; } } // Here, `method` is either allow listed or no allowlist. Blocklist has // priority over allowlist or cold start list. So, check additionally. if (InstrumentPass::is_included(method, options.blocklist)) { blocklisted++; TRACE(INSTRUMENT, 9, "Blocklisted: %s, %s", show_deobfuscated(method).c_str(), SHOW(method)); return; } instrumented_methods.emplace_back(instrument_basic_blocks( code, method, onMethodBegin, onMethodExit_map, max_vector_arity, method_offset, max_num_blocks, options)); const auto& method_info = instrumented_methods.back(); if (method_info.too_many_blocks) { TRACE(INSTRUMENT, 7, "Too many blocks: %s", SHOW(show_deobfuscated(method))); } else { block_instrumented++; } // Update method offset for next method. 2 shorts are for method stats. method_offset += 2 + method_info.num_vectors; }); // Patch static fields. const auto field_name = array_fields.at(1)->get_name()->str(); InstrumentPass::patch_array_size(analysis_cls, field_name, method_offset); auto* field = analysis_cls->find_field_from_simple_deobfuscated_name( "sNumStaticallyInstrumented"); always_assert(field != nullptr); InstrumentPass::patch_static_field(analysis_cls, field->get_name()->str(), instrumented_methods.size()); field = analysis_cls->find_field_from_simple_deobfuscated_name("sProfileType"); always_assert(field != nullptr); InstrumentPass::patch_static_field( analysis_cls, field->get_name()->str(), static_cast<int>(ProfileTypeFlags::BasicBlockTracing)); write_metadata(cfg, options.metadata_file_name, instrumented_methods); ScopedMetrics sm(pm); auto block_instr_scope = sm.scope("block_instr"); print_stats(sm, instrumented_methods, max_num_blocks); { auto methods_scope = sm.scope("methods"); TRACE(INSTRUMENT, 4, "Instrumentation selection stats:"); TRACE(INSTRUMENT, 4, "- All methods: %d", all_methods); sm.set_metric("all", all_methods); TRACE(INSTRUMENT, 4, "- Eligible methods: %d", eligibles); sm.set_metric("eligible", eligibles); TRACE(INSTRUMENT, 4, " Uninstrumentable methods: %d", specials); sm.set_metric("special", specials); TRACE(INSTRUMENT, 4, " Non-root methods: %d", non_root_store_methods); sm.set_metric("non_root", non_root_store_methods); } { auto sel_scope = sm.scope("selected"); TRACE(INSTRUMENT, 4, "- Explicitly selected:"); TRACE(INSTRUMENT, 4, " Allow listed: %d", picked_by_allowlist); sm.set_metric("allow_list", picked_by_allowlist); TRACE(INSTRUMENT, 4, " Cold start: %d", picked_by_cs); sm.set_metric("cold_start", picked_by_cs); } { auto rej_scope = sm.scope("rejected"); TRACE(INSTRUMENT, 4, "- Explicitly rejected:"); TRACE(INSTRUMENT, 4, " Not in allow or cold start set: %d", rejected); sm.set_metric("not_allow_or_cold_start", rejected); TRACE(INSTRUMENT, 4, " Block listed: %d", blocklisted); sm.set_metric("block_list", blocklisted); } }