// MIT License // // Copyright (c) 2022 Advanced Micro Devices, Inc. All Rights Reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #include "omnitrace.hpp" #include "fwd.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if !defined(OMNITRACE_USE_MPI) # define OMNITRACE_USE_MPI 0 #endif #if !defined(OMNITRACE_USE_MPI_HEADERS) # define OMNITRACE_USE_MPI_HEADERS 0 #endif bool use_return_info = false; bool use_args_info = false; bool use_file_info = false; bool use_line_info = false; bool allow_overlapping = false; bool loop_level_instr = false; bool instr_dynamic_callsites = false; bool instr_traps = false; bool instr_loop_traps = false; size_t min_address_range = (1 << 8); // 256 size_t min_loop_address_range = (1 << 8); // 256 size_t min_instructions = (1 << 6); // 64 size_t min_loop_instructions = (1 << 6); // 64 bool werror = false; bool debug_print = false; bool instr_print = false; int verbose_level = tim::get_env("OMNITRACE_VERBOSE_INSTRUMENT", 0); string_t main_fname = "main"; string_t argv0 = {}; string_t cmdv0 = {}; string_t default_components = "wall_clock"; string_t prefer_library = {}; // // global variables // patch_pointer_t bpatch = {}; call_expr_t* terminate_expr = nullptr; snippet_vec_t init_names = {}; snippet_vec_t fini_names = {}; fmodset_t available_module_functions = {}; fmodset_t instrumented_module_functions = {}; fmodset_t coverage_module_functions = {}; fmodset_t overlapping_module_functions = {}; fmodset_t excluded_module_functions = {}; fixed_modset_t fixed_module_functions = {}; regexvec_t func_include = {}; regexvec_t func_exclude = {}; regexvec_t file_include = {}; regexvec_t file_exclude = {}; regexvec_t file_restrict = {}; regexvec_t func_restrict = {}; CodeCoverageMode coverage_mode = CODECOV_NONE; namespace { bool binary_rewrite = false; bool is_attached = false; bool use_mpi = false; bool is_static_exe = false; bool simulate = false; bool include_uninstr = false; size_t batch_size = 50; strset_t extra_libs = {}; std::vector> hash_ids = {}; std::map use_stubs = {}; std::map beg_stubs = {}; std::map end_stubs = {}; strvec_t init_stub_names = {}; strvec_t fini_stub_names = {}; strset_t used_stub_names = {}; strvec_t env_config_variables = {}; std::vector env_variables = {}; std::map beg_expr = {}; std::map end_expr = {}; const auto npos_v = string_t::npos; string_t instr_mode = "trace"; string_t print_coverage = {}; string_t print_instrumented = {}; string_t print_excluded = {}; string_t print_available = {}; string_t print_overlapping = {}; strset_t print_formats = { "txt", "json" }; std::string modfunc_dump_dir = {}; auto regex_opts = std::regex_constants::egrep | std::regex_constants::optimize; std::string get_absolute_exe_filepath(std::string exe_name, const std::string& env_path = "PATH"); std::string get_absolute_lib_filepath(std::string lib_name, const std::string& env_path = "LD_LIBRARY_PATH"); bool file_exists(const std::string& name); std::string get_realpath(const std::string&); std::string get_cwd(); } // namespace //======================================================================================// // // entry point // //======================================================================================// // int main(int argc, char** argv) { #if defined(DYNINST_API_RT) auto _dyn_api_rt_paths = tim::delimit(DYNINST_API_RT, ":"); #else auto _dyn_api_rt_paths = std::vector{}; #endif auto _dyn_api_rt_abs = get_absolute_lib_filepath("libdyninstAPI_RT.so"); _dyn_api_rt_paths.insert(_dyn_api_rt_paths.begin(), _dyn_api_rt_abs); for(auto&& itr : _dyn_api_rt_paths) { auto _file_exists = [](const std::string& _fname) { struct stat _buffer; if(stat(_fname.c_str(), &_buffer) == 0) return (S_ISREG(_buffer.st_mode) != 0 || S_ISLNK(_buffer.st_mode) != 0); return false; }; if(_file_exists(itr)) tim::set_env("DYNINSTAPI_RT_LIB", itr, 0); else if(_file_exists(TIMEMORY_JOIN('/', itr, "libdyninstAPI_RT.so"))) tim::set_env("DYNINSTAPI_RT_LIB", TIMEMORY_JOIN('/', itr, "libdyninstAPI_RT.so"), 0); else if(_file_exists(TIMEMORY_JOIN('/', itr, "libdyninstAPI_RT.a"))) tim::set_env("DYNINSTAPI_RT_LIB", TIMEMORY_JOIN('/', itr, "libdyninstAPI_RT.a"), 0); } verbprintf(0, "DYNINST_API_RT: %s\n", tim::get_env("DYNINSTAPI_RT_LIB", "").c_str()); argv0 = argv[0]; bpatch = std::make_shared(); address_space_t* addr_space = nullptr; string_t mutname = {}; string_t outfile = {}; std::vector inputlib = { "libomnitrace-dl" }; std::vector libname = {}; std::vector sharedlibname = {}; std::vector staticlibname = {}; tim::process::id_t _pid = -1; fixed_module_functions = { { &available_module_functions, false }, { &instrumented_module_functions, false }, { &coverage_module_functions, false }, { &excluded_module_functions, false }, { &overlapping_module_functions, false }, }; bpatch->setTypeChecking(true); bpatch->setSaveFPR(true); bpatch->setDelayedParsing(true); bpatch->setDebugParsing(false); bpatch->setInstrStackFrames(false); bpatch->setLivenessAnalysis(false); bpatch->setBaseTrampDeletion(false); bpatch->setTrampRecursive(false); bpatch->setMergeTramp(true); std::set dyninst_defs = { "TypeChecking", "SaveFPR", "DelayedParsing", "MergeTramp" }; int _argc = argc; int _cmdc = 0; char** _argv = new char*[_argc]; char** _cmdv = nullptr; for(int i = 0; i < argc; ++i) _argv[i] = nullptr; auto copy_str = [](char*& _dst, const char* _src) { _dst = strdup(_src); }; copy_str(_argv[0], argv[0]); for(int i = 1; i < argc; ++i) { string_t _arg = argv[i]; if(_arg.length() == 2 && _arg == "--") { _argc = i; _cmdc = argc - i - 1; _cmdv = new char*[_cmdc + 1]; _cmdv[_cmdc] = nullptr; int k = 0; for(int j = i + 1; j < argc; ++j, ++k) { auto _v = std::regex_replace(argv[j], std::regex{ "(.*)([ \t\n\r]+)$" }, "$1"); copy_str(_cmdv[k], _v.c_str()); } mutname = _cmdv[0]; break; } else { copy_str(_argv[i], argv[i]); } } auto cmd_string = [](int _ac, char** _av) -> std::string { if(_ac == 0) return std::string{}; stringstream_t ss; for(int i = 0; i < _ac; ++i) ss << " " << _av[i]; return ss.str().substr(1); }; if(_cmdc > 0 && !mutname.empty()) { auto resolved_mutname = get_absolute_exe_filepath(mutname); if(resolved_mutname != mutname) { mutname = resolved_mutname; delete _cmdv[0]; copy_str(_cmdv[0], resolved_mutname.c_str()); } } if(verbose_level > 1) { std::cout << "[omnitrace][exe][original]: " << cmd_string(argc, argv) << std::endl; std::cout << "[omnitrace][exe][cfg-args]: " << cmd_string(_argc, _argv) << std::endl; } verbprintf(0, "\n"); verbprintf(0, "command :: '%s'...\n", cmd_string(_cmdc, _cmdv).c_str()); verbprintf(0, "\n"); if(_cmdc > 0) cmdv0 = _cmdv[0]; // now can loop through the options. If the first character is '-', then we know // we have an option. Check to see if it is one of our options and process it. If // it is unrecognized, then set the errflag to report an error. When we come to a // non '-' charcter, then we must be at the application name. using parser_t = tim::argparse::argument_parser; parser_t parser("omnitrace"); string_t extra_help = "-- "; parser.enable_help(); parser.add_argument({ "" }, ""); parser.add_argument({ "[DEBUG OPTIONS]" }, ""); parser.add_argument({ "" }, ""); parser.add_argument({ "--debug" }, "Debug output") .max_count(1) .action([](parser_t& p) { debug_print = p.get("debug"); if(debug_print && !p.exists("verbose")) verbose_level = 256; }); parser.add_argument({ "-v", "--verbose" }, "Verbose output") .max_count(1) .action([](parser_t& p) { if(p.get_count("v") == 0) verbose_level = 1; else verbose_level = p.get("v"); }); parser.add_argument({ "-e", "--error" }, "All warnings produce runtime errors") .dtype("boolean") .max_count(1) .action([](parser_t& p) { werror = p.get("error"); }); parser .add_argument({ "--simulate" }, "Exit after outputting diagnostic " "{available,instrumented,excluded,overlapping} module " "function lists, e.g. available-instr.txt") .max_count(1) .dtype("bool") .action([](parser_t& p) { simulate = p.get("simulate"); }); parser .add_argument({ "--print-format" }, "Output format for diagnostic " "{available,instrumented,excluded,overlapping} module " "function lists, e.g. {print-dir}/available-instr.txt") .min_count(1) .max_count(3) .dtype("string") .choices({ "xml", "json", "txt" }) .action([](parser_t& p) { print_formats = p.get("print-format"); }); parser .add_argument({ "--print-dir" }, "Output directory for diagnostic " "{available,instrumented,excluded,overlapping} module " "function lists, e.g. {print-dir}/available-instr.txt") .count(1) .dtype("string") .action([](parser_t& p) { tim::settings::output_path() = p.get("print-dir"); }); parser .add_argument( { "--print-available" }, "Print the available entities for instrumentation (functions, modules, or " "module-function pair) to stdout after applying regular expressions") .count(1) .choices({ "functions", "modules", "functions+", "pair", "pair+" }) .action( [](parser_t& p) { print_available = p.get("print-available"); }); parser .add_argument( { "--print-instrumented" }, "Print the instrumented entities (functions, modules, or module-function " "pair) to stdout after applying regular expressions") .count(1) .choices({ "functions", "modules", "functions+", "pair", "pair+" }) .action([](parser_t& p) { print_instrumented = p.get("print-instrumented"); }); parser .add_argument({ "--print-coverage" }, "Print the instrumented coverage entities (functions, modules, or " "module-function " "pair) to stdout after applying regular expressions") .count(1) .choices({ "functions", "modules", "functions+", "pair", "pair+" }) .action( [](parser_t& p) { print_coverage = p.get("print-coverage"); }); parser .add_argument({ "--print-excluded" }, "Print the entities for instrumentation (functions, modules, or " "module-function " "pair) which are excluded from the instrumentation to stdout after " "applying regular expressions") .count(1) .choices({ "functions", "modules", "functions+", "pair", "pair+" }) .action( [](parser_t& p) { print_excluded = p.get("print-excluded"); }); parser .add_argument( { "--print-overlapping" }, "Print the entities for instrumentation (functions, modules, or " "module-function pair) which overlap other function calls or have multiple " "entry points to stdout after applying regular expressions") .count(1) .choices({ "functions", "modules", "functions+", "pair", "pair+" }) .action([](parser_t& p) { print_overlapping = p.get("print-overlapping"); }); parser .add_argument( { "--print-instructions" }, "Print the instructions for each basic-block in the JSON/XML outputs") .max_count(1) .action([](parser_t& p) { instr_print = p.get("print-instructions"); }); parser.add_argument({ "" }, ""); parser.add_argument({ "[MODE OPTIONS]" }, ""); parser.add_argument({ "" }, ""); parser .add_argument({ "-o", "--output" }, "Enable generation of a new executable (binary-rewrite). If a " "filename is not provided, omnitrace will use the basename and " "output to the cwd, unless the target binary is in the cwd. In the " "latter case, omnitrace will either use ${PWD}/.inst " "(non-libraries) or ${PWD}/instrumented/ (libraries)") .min_count(0) .max_count(1) .dtype("string") .action([&outfile](parser_t& p) { binary_rewrite = true; outfile = p.get("output"); }); parser.add_argument({ "-p", "--pid" }, "Connect to running process") .dtype("int") .count(1) .action([&_pid](parser_t& p) { _pid = p.get("pid"); }); parser .add_argument({ "-M", "--mode" }, "Instrumentation mode. 'trace' mode instruments the selected " "functions, 'sampling' mode only instruments the main function to " "start and stop the sampler.") .choices({ "trace", "sampling", "coverage" }) .count(1) .action([](parser_t& p) { instr_mode = p.get("mode"); if(instr_mode == "coverage" && !p.exists("coverage")) coverage_mode = CODECOV_FUNCTION; }); if(_cmdc == 0) { parser .add_argument({ "-c", "--command" }, "Input executable and arguments (if '-- ' not provided)") .count(1) .action([&](parser_t& p) { auto keys = p.get("c"); if(keys.empty()) { p.print_help(extra_help); std::exit(EXIT_FAILURE); } keys.at(0) = get_absolute_exe_filepath(keys.at(0)); mutname = keys.at(0); _cmdc = keys.size(); _cmdv = new char*[_cmdc]; for(int i = 0; i < _cmdc; ++i) copy_str(_cmdv[i], keys.at(i).c_str()); }); } parser.add_argument({ "" }, ""); parser.add_argument({ "[LIBRARY OPTIONS]" }, ""); parser.add_argument({ "" }, ""); parser.add_argument({ "--prefer" }, "Prefer this library types when available") .choices({ "shared", "static" }) .count(1) .action([](parser_t& p) { prefer_library = p.get("prefer"); }); parser .add_argument( { "-L", "--library" }, "Libraries with instrumentation routines (default: \"libomnitrace\")") .action([&inputlib](parser_t& p) { inputlib = p.get("library"); }); parser .add_argument({ "-m", "--main-function" }, "The primary function to instrument around, e.g. 'main'") .count(1) .action([](parser_t& p) { main_fname = p.get("main-function"); }); parser .add_argument({ "--load" }, "Supplemental instrumentation library names w/o extension (e.g. " "'libinstr' for 'libinstr.so' or 'libinstr.a')") .dtype("string") .action([](parser_t& p) { auto _load = p.get("load"); for(const auto& itr : _load) extra_libs.insert(itr); }); parser .add_argument({ "--load-instr" }, "Load {available,instrumented,excluded,overlapping}-instr JSON or " "XML file(s) and override what is read from the binary") .dtype("filepath") .max_count(-1) .action([](parser_t& p) { auto _load = p.get("load-instr"); std::map module_function_map = { { "available_module_functions", &available_module_functions }, { "instrumented_module_functions", &instrumented_module_functions }, { "coverage_module_functions", &coverage_module_functions }, { "excluded_module_functions", &excluded_module_functions }, { "overlapping_module_functions", &overlapping_module_functions }, }; for(const auto& itr : _load) load_info(itr, module_function_map, 0); for(const auto& itr : module_function_map) { auto _empty = itr.second->empty(); if(!_empty) verbprintf(0, "Loaded %zu module functions for %s\n", itr.second->size(), itr.first.c_str()); fixed_module_functions.at(itr.second) = !_empty; } }); parser .add_argument({ "--init-functions" }, "Initialization function(s) for supplemental instrumentation " "libraries (see '--load' option)") .dtype("string") .action([](parser_t& p) { init_stub_names = p.get("init-functions"); }); parser .add_argument({ "--fini-functions" }, "Finalization function(s) for supplemental instrumentation " "libraries (see '--load' option)") .dtype("string") .action([](parser_t& p) { fini_stub_names = p.get("fini-functions"); }); parser .add_argument( { "--all-functions" }, "When finding functions, include the functions which are not instrumentable. " "This is purely diagnostic for the available/excluded functions output") .dtype("bool") .max_count(1) .action([](parser_t& p) { include_uninstr = p.get("all-functions"); }); parser.add_argument({ "" }, ""); parser.add_argument({ "[SYMBOL SELECTION OPTIONS]" }, ""); parser.add_argument({ "" }, ""); parser.add_argument({ "-I", "--function-include" }, "Regex(es) for including functions (despite heuristics)"); parser.add_argument({ "-E", "--function-exclude" }, "Regex(es) for excluding functions (always applied)"); parser.add_argument({ "-R", "--function-restrict" }, "Regex(es) for restricting functions only to those " "that match the provided regular-expressions"); parser.add_argument({ "-MI", "--module-include" }, "Regex(es) for selecting modules/files/libraries " "(despite heuristics)"); parser.add_argument({ "-ME", "--module-exclude" }, "Regex(es) for excluding modules/files/libraries " "(always applied)"); parser.add_argument({ "-MR", "--module-restrict" }, "Regex(es) for restricting modules/files/libraries only to those " "that match the provided regular-expressions"); parser.add_argument({ "" }, ""); parser.add_argument({ "[RUNTIME OPTIONS]" }, ""); parser.add_argument({ "" }, ""); parser .add_argument({ "--label" }, "Labeling info for functions. By default, just the function name " "is recorded. Use these options to gain more information about the " "function signature or location of the functions") .choices({ "file", "line", "return", "args" }) .dtype("string") .action([](parser_t& p) { auto _labels = p.get("label"); for(const auto& itr : _labels) { if(std::regex_match(itr, std::regex("file", std::regex_constants::icase))) use_file_info = true; else if(std::regex_match( itr, std::regex("return", std::regex_constants::icase))) use_return_info = true; else if(std::regex_match(itr, std::regex("args", std::regex_constants::icase))) use_args_info = true; else if(std::regex_match(itr, std::regex("line", std::regex_constants::icase))) use_line_info = true; } }); parser.add_argument() .names({ "-C", "--config" }) .dtype("string") .min_count(1) .description("Read in a configuration file and encode these values as the " "defaults in the executable"); parser.add_argument() .names({ "-d", "--default-components" }) .dtype("string") .description("Default components to instrument (only useful when timemory is " "enabled in omnitrace library)") .action([](parser_t& p) { auto _components = p.get("default-components"); default_components = {}; for(size_t i = 0; i < _components.size(); ++i) { if(_components.at(i) == "none") { default_components = "none"; break; } default_components += _components.at(i); if(i + 1 < _components.size()) default_components += ","; } if(default_components == "none") default_components = {}; else { auto _strcomp = p.get("default-components"); if(!_strcomp.empty() && default_components.empty()) default_components = _strcomp; } }); parser.add_argument({ "--env" }, "Environment variables to add to the runtime in form " "VARIABLE=VALUE. E.g. use '--env OMNITRACE_USE_TIMEMORY=ON' to " "default to using timemory instead of perfetto"); parser .add_argument({ "--mpi" }, "Enable MPI support (requires omnitrace built w/ full or partial " "MPI support). NOTE: this will automatically be activated if " "MPI_Init, MPI_Init_thread, MPI_Finalize, MPI_Comm_rank, or " "MPI_Comm_size are found in the symbol table of target") .max_count(1) .action([](parser_t& p) { use_mpi = p.get("mpi"); #if OMNITRACE_USE_MPI == 0 && OMNITRACE_USE_MPI_HEADERS == 0 errprintf(0, "omnitrace was not built with full or partial MPI support\n"); use_mpi = false; #endif }); parser.add_argument({ "" }, ""); parser.add_argument({ "[GRANULARITY OPTIONS]" }, ""); parser.add_argument({ "" }, ""); parser.add_argument({ "-l", "--instrument-loops" }, "Instrument at the loop level") .dtype("boolean") .max_count(1) .action([](parser_t& p) { loop_level_instr = p.get("instrument-loops"); }); parser .add_argument({ "-i", "--min-instructions" }, "If the number of instructions in a function is less than this " "value, exclude it from instrumentation") .count(1) .dtype("int") .action( [](parser_t& p) { min_instructions = p.get("min-instructions"); }); parser .add_argument({ "--min-instructions-loop" }, "If the number of instructions in a function containing a loop is " "less than this value, exclude it from instrumentation") .count(1) .dtype("int") .action([](parser_t& p) { min_loop_instructions = p.get("min-instructions-loop"); }); parser .add_argument({ "-r", "--min-address-range" }, "If the address range of a function is less than this value, " "exclude it from instrumentation") .count(1) .dtype("int") .action( [](parser_t& p) { min_address_range = p.get("min-address-range"); }); parser .add_argument({ "--min-address-range-loop" }, "If the address range of a function containing a loop is less than " "this value, exclude it from instrumentation") .count(1) .dtype("int") .action([](parser_t& p) { min_loop_address_range = p.get("min-address-range-loop"); }); parser.add_argument({ "--coverage" }, "Enable recording the code coverage") .max_count(1) .choices({ "none", "function", "basic_block" }) .action([](parser_t& p) { auto _v = p.get("coverage"); if(_v == "function" || _v.empty()) coverage_mode = CODECOV_FUNCTION; else if(_v == "basic_block") coverage_mode = CODECOV_BASIC_BLOCK; else coverage_mode = CODECOV_NONE; }); parser .add_argument({ "--dynamic-callsites" }, "Force instrumentation if a function has dynamic callsites (e.g. " "function pointers)") .max_count(1) .dtype("boolean") .action([](parser_t& p) { instr_dynamic_callsites = p.get("dynamic-callsites"); }); parser .add_argument( { "--traps" }, "Instrument points which require using a trap. On the x86 architecture, " "because instructions are of variable size, the instruction at a point may " "be too small for Dyninst to replace it with the normal code sequence used " "to call instrumentation. Also, when instrumentation is placed at points " "other than subroutine entry, exit, or call points, traps may be used to " "ensure the instrumentation fits. In this case, Dyninst replaces the " "instruction with a single-byte instruction that generates a trap.") .max_count(1) .dtype("bool") .set_default(instr_traps) .action([](parser_t& p) { instr_traps = p.get("traps"); }); parser .add_argument({ "--loop-traps" }, "Instrument points within a loop which require using a trap (only " "relevant when --instrument-loops is enabled).") .max_count(1) .dtype("bool") .set_default(instr_loop_traps) .action([](parser_t& p) { instr_loop_traps = p.get("loop-traps"); }); parser .add_argument( { "--allow-overlapping" }, "Allow dyninst to instrument either multiple functions which overlap (share " "part of same function body) or single functions with multiple entry points. " "For more info, see Section 2 of the DyninstAPI documentation.") .count(0) .action([](parser_t&) { allow_overlapping = true; }); parser.add_argument({ "" }, ""); parser.add_argument({ "[DYNINST OPTIONS]" }, ""); parser.add_argument({ "" }, ""); parser .add_argument( { "-b", "--batch-size" }, "Dyninst supports batch insertion of multiple points during runtime " "instrumentation. If one large batch " "insertion fails, this value will be used to create smaller batches. Larger " "batches generally decrease the instrumentation time") .count(1) .dtype("int") .action([](parser_t& p) { batch_size = p.get("batch-size"); }); parser .add_argument({ "--dyninst-options" }, "Advanced dyninst options: BPatch::set