blob: 9ffeeeb859222c53beaa2aec611f0615430f0323 [file] [log] [blame] [edit]
/* ***************************************************************************
* Copyright (c) 2013-2025 Google, Inc. All rights reserved.
* ***************************************************************************/
/*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of Google, Inc. nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
/* Library Tracing Tool: drltrace
*
* Records calls to exported library routines.
*
* The runtime options for this client are specified in drltrace_options.h,
* see DROPTION_SCOPE_CLIENT options.
*/
#include "drltrace.h"
/* XXX i#1948: features to add:
*
* + Add filtering of which library routines to trace.
* This would likely be via a configuration file.
* Currently we have a simple -only_to_lib option.
*
* + Add argument values and return values. The number and type of each
* argument and return would likely come from the filter configuration
* file, or from querying debug information.
* Today we have simple type-blind printing via -num_unknown_args and
* usage of drsyscall to print symbolic arguments for known library calls.
*
* + Add 2 more modes, both gathering statistics rather than a full
* trace: one mode that counts total calls, and one that just
* records whether each library routine was ever called. For these,
* we'll probably want to insert custom instrumentation rather than
* a clean call via drwrap, and so we'll want our own hashtable of
* the library entries.
*/
using ::dynamorio::droption::droption_parser_t;
using ::dynamorio::droption::DROPTION_SCOPE_CLIENT;
/* Where to write the trace */
static file_t outf;
/* Avoid exe exports, as on Linux many apps have a ton of global symbols. */
static app_pc exe_start;
/****************************************************************************
* Arguments printing
*/
/* XXX i#1978: The functions print_simple_value and print_arg were taken from drstrace.
* It would be better to move them in drsyscall and import in drstrace and here.
*/
static void
print_simple_value(drsys_arg_t *arg, bool leading_zeroes)
{
bool pointer = !TEST(DRSYS_PARAM_INLINED, arg->mode);
dr_fprintf(outf, pointer ? PFX : (leading_zeroes ? PFX : PIFX), arg->value);
if (pointer && ((arg->pre && TEST(DRSYS_PARAM_IN, arg->mode)) ||
(!arg->pre && TEST(DRSYS_PARAM_OUT, arg->mode)))) {
ptr_uint_t deref = 0;
ASSERT(arg->size <= sizeof(deref), "too-big simple type");
/* We assume little-endian */
if (dr_safe_read((void *)arg->value, arg->size, &deref, NULL))
dr_fprintf(outf, (leading_zeroes ? " => " PFX : " => " PIFX), deref);
}
}
static void
print_string(void *drcontext, void *pointer_str, bool is_wide)
{
if (pointer_str == NULL)
dr_fprintf(outf, "<null>");
else {
DR_TRY_EXCEPT(drcontext, {
dr_fprintf(outf, is_wide ? "%S" : "%s", pointer_str);
}, {
dr_fprintf(outf, "<invalid memory>");
});
}
}
static void
print_arg(void *drcontext, drsys_arg_t *arg)
{
if (arg->pre && (TEST(DRSYS_PARAM_OUT, arg->mode) && !TEST(DRSYS_PARAM_IN, arg->mode)))
return;
dr_fprintf(outf, "\n arg %d: ", arg->ordinal);
switch (arg->type) {
case DRSYS_TYPE_VOID: print_simple_value(arg, true); break;
case DRSYS_TYPE_POINTER: print_simple_value(arg, true); break;
case DRSYS_TYPE_BOOL: print_simple_value(arg, false); break;
case DRSYS_TYPE_INT: print_simple_value(arg, false); break;
case DRSYS_TYPE_SIGNED_INT: print_simple_value(arg, false); break;
case DRSYS_TYPE_UNSIGNED_INT: print_simple_value(arg, false); break;
case DRSYS_TYPE_HANDLE: print_simple_value(arg, false); break;
case DRSYS_TYPE_NTSTATUS: print_simple_value(arg, false); break;
case DRSYS_TYPE_ATOM: print_simple_value(arg, false); break;
#ifdef WINDOWS
case DRSYS_TYPE_LCID: print_simple_value(arg, false); break;
case DRSYS_TYPE_LPARAM: print_simple_value(arg, false); break;
case DRSYS_TYPE_SIZE_T: print_simple_value(arg, false); break;
case DRSYS_TYPE_HMODULE: print_simple_value(arg, false); break;
#endif
case DRSYS_TYPE_CSTRING:
print_string(drcontext, (void *)arg->value, false);
break;
case DRSYS_TYPE_CWSTRING:
print_string(drcontext, (void *)arg->value, true);
break;
default: {
if (arg->value == 0)
dr_fprintf(outf, "<null>");
else
dr_fprintf(outf, PFX, arg->value);
}
}
dr_fprintf(outf, " (%s%s%stype=%s%s, size=" PIFX ")",
(arg->arg_name == NULL) ? "" : "name=",
(arg->arg_name == NULL) ? "" : arg->arg_name,
(arg->arg_name == NULL) ? "" : ", ",
(arg->type_name == NULL) ? "\"\"" : arg->type_name,
(arg->type_name == NULL ||
TESTANY(DRSYS_PARAM_INLINED|DRSYS_PARAM_RETVAL, arg->mode)) ? "" : "*",
arg->size);
}
static bool
drlib_iter_arg_cb(drsys_arg_t *arg, void *wrapcxt)
{
if (arg->ordinal == -1)
return true;
if (arg->ordinal >= op_max_args.get_value())
return false; /* limit number of arguments to be printed */
arg->value = (ptr_uint_t)drwrap_get_arg(wrapcxt, arg->ordinal);
print_arg(drwrap_get_drcontext(wrapcxt), arg);
return true; /* keep going */
}
static void
print_args_unknown_call(app_pc func, void *wrapcxt)
{
uint i;
void *drcontext = drwrap_get_drcontext(wrapcxt);
DR_TRY_EXCEPT(drcontext, {
for (i = 0; i < op_unknown_args.get_value(); i++) {
dr_fprintf(outf, "\n arg %d: " PFX, i,
drwrap_get_arg(wrapcxt, i));
}
}, {
dr_fprintf(outf, "<invalid memory>");
/* Just keep going */
});
/* all args have been sucessfully printed */
dr_fprintf(outf, op_print_ret_addr.get_value() ? "\n ": "");
}
static bool
print_libcall_args(std::vector<drsys_arg_t*> *args_vec, void *wrapcxt)
{
if (args_vec == NULL || args_vec->size() <= 0)
return false;
std::vector<drsys_arg_t*>::iterator it;
for (it = args_vec->begin(); it != args_vec->end(); ++it) {
if (!drlib_iter_arg_cb(*it, wrapcxt))
break;
}
return true;
}
static void
print_symbolic_args(const char *name, void *wrapcxt, app_pc func)
{
drmf_status_t res;
drsys_syscall_t *syscall;
std::vector<drsys_arg_t *> *args_vec;
if (op_max_args.get_value() == 0)
return;
if (op_use_config.get_value()) {
/* looking for libcall in libcalls hashtable */
args_vec = libcalls_search(name);
if (print_libcall_args(args_vec, wrapcxt)) {
dr_fprintf(outf, op_print_ret_addr.get_value() ? "\n ": "");
return; /* we found libcall and sucessfully printed all arguments */
}
}
/* trying to find a prototype of the libcall using drsyscall */
res = drsys_name_to_syscall(name, &syscall);
if (res == DRMF_SUCCESS) {
res = drsys_iterate_arg_types(syscall, drlib_iter_arg_cb, wrapcxt);
if (res != DRMF_SUCCESS && res != DRMF_ERROR_DETAILS_UNKNOWN)
ASSERT(false, "drsys_iterate_arg_types failed in print_symbolic_args");
/* all args have been sucessfully printed */
dr_fprintf(outf, op_print_ret_addr.get_value() ? "\n ": "");
return;
} else {
/* use standard type-blind scheme */
if (op_unknown_args.get_value() > 0)
print_args_unknown_call(func, wrapcxt);
}
}
/****************************************************************************
* Library entry wrapping
*/
static void
lib_entry(void *wrapcxt, DR_PARAM_INOUT void **user_data)
{
const char *name = (const char *) *user_data;
const char *modname = NULL;
app_pc func = drwrap_get_func(wrapcxt);
module_data_t *mod;
thread_id_t tid;
uint mod_id;
app_pc mod_start, ret_addr;
drcovlib_status_t res;
void *drcontext = drwrap_get_drcontext(wrapcxt);
if (op_only_from_app.get_value()) {
/* For just this option, the modxfer approach might be better */
app_pc retaddr = NULL;
DR_TRY_EXCEPT(drcontext, {
retaddr = drwrap_get_retaddr(wrapcxt);
}, { /* EXCEPT */
retaddr = NULL;
});
if (retaddr != NULL) {
mod = dr_lookup_module(retaddr);
if (mod != NULL) {
bool from_exe = (mod->start == exe_start);
dr_free_module_data(mod);
if (!from_exe)
return;
}
} else {
/* Nearly all of these cases should be things like KiUserCallbackDispatcher
* or other abnormal transitions.
* If the user really wants to see everything they can not pass
* -only_from_app.
*/
return;
}
}
/* XXX: it may be better to heap-allocate the "module!func" string and
* pass in, to avoid this lookup.
*/
mod = dr_lookup_module(func);
if (mod != NULL)
modname = dr_module_preferred_name(mod);
tid = dr_get_thread_id(drcontext);
if (tid != INVALID_THREAD_ID)
dr_fprintf(outf, "~~%d~~ ", tid);
else
dr_fprintf(outf, "~~Dr.L~~ ");
dr_fprintf(outf, "%s%s%s", modname == NULL ? "" : modname,
modname == NULL ? "" : "!", name);
/* XXX: We employ three schemes of arguments printing. drsyscall is used
* to get a symbolic representation of arguments for known library calls.
* For the rest of library calls we are looking for prototypes in config file
* specified by user. If there is no info in both sources we employ type-blind
* printing and use -num_unknown_args to get a count of arguments to print.
*/
print_symbolic_args(name, wrapcxt, func);
if (op_print_ret_addr.get_value()) {
ret_addr = drwrap_get_retaddr(wrapcxt);
res = drmodtrack_lookup(drcontext, ret_addr, &mod_id, &mod_start);
if (res == DRCOVLIB_SUCCESS) {
dr_fprintf(outf,
op_print_ret_addr.get_value() ?
" and return to module id:%d, offset:" PIFX : "",
mod_id, ret_addr - mod_start);
}
}
dr_fprintf(outf, "\n");
if (mod != NULL)
dr_free_module_data(mod);
}
static void
iterate_exports(const module_data_t *info, bool add)
{
dr_symbol_export_iterator_t *exp_iter =
dr_symbol_export_iterator_start(info->handle);
while (dr_symbol_export_iterator_hasnext(exp_iter)) {
dr_symbol_export_t *sym = dr_symbol_export_iterator_next(exp_iter);
app_pc func = NULL;
if (sym->is_code)
func = sym->addr;
#ifdef LINUX
else if (sym->is_indirect_code) {
/* Invoke the export to get the real entry: */
app_pc (*indir)(void) = (app_pc (*)(void)) cast_to_func(sym->addr);
void *drcontext = dr_get_current_drcontext();
DR_TRY_EXCEPT(drcontext, {
func = (*indir)();
}, { /* EXCEPT */
func = NULL;
});
VNOTIFY(2, "export %s indirected from " PFX " to " PFX NL,
sym->name, sym->addr, func);
}
#endif
if (op_ignore_underscore.get_value() && strstr(sym->name, "_") == sym->name)
func = NULL;
if (func != NULL) {
if (add) {
IF_DEBUG(bool ok =)
drwrap_wrap_ex(func, lib_entry, NULL, (void *) sym->name, 0);
ASSERT(ok, "wrap request failed");
VNOTIFY(2, "wrapping export %s!%s @" PFX NL,
dr_module_preferred_name(info), sym->name, func);
} else {
IF_DEBUG(bool ok =)
drwrap_unwrap(func, lib_entry, NULL);
ASSERT(ok, "unwrap request failed");
}
}
}
dr_symbol_export_iterator_stop(exp_iter);
}
static bool
library_matches_filter(const module_data_t *info)
{
if (!op_only_to_lib.get_value().empty()) {
const char *libname = dr_module_preferred_name(info);
#ifdef WINDOWS
return (libname != NULL && strcasestr(libname,
op_only_to_lib.get_value().c_str()) != NULL);
#else
return (libname != NULL && strstr(libname,
op_only_to_lib.get_value().c_str()) != NULL);
#endif
}
return true;
}
static void
event_module_load(void *drcontext, const module_data_t *info, bool loaded)
{
if (info->start != exe_start && library_matches_filter(info))
iterate_exports(info, true/*add*/);
}
static void
event_module_unload(void *drcontext, const module_data_t *info)
{
if (info->start != exe_start && library_matches_filter(info))
iterate_exports(info, false/*remove*/);
}
/****************************************************************************
* Init and exit
*/
static void
open_log_file(void)
{
char buf[MAXIMUM_PATH];
if (op_logdir.get_value().compare("-") == 0)
outf = STDERR;
else {
outf = drx_open_unique_appid_file(op_logdir.get_value().c_str(),
dr_get_process_id(),
"drltrace", "log",
#ifndef WINDOWS
DR_FILE_CLOSE_ON_FORK |
#endif
DR_FILE_ALLOW_LARGE,
buf, BUFFER_SIZE_ELEMENTS(buf));
ASSERT(outf != INVALID_FILE, "failed to open log file");
VNOTIFY(0, "<drltrace log file is %s>" NL, buf);
}
}
#ifndef WINDOWS
static void
event_fork(void *drcontext)
{
/* The old file was closed by DR b/c we passed DR_FILE_CLOSE_ON_FORK */
open_log_file();
}
#endif
static void
event_exit(void)
{
if (op_max_args.get_value() > 0)
drsys_exit();
if (op_use_config.get_value())
libcalls_hashtable_delete();
if (outf != STDERR) {
if (op_print_ret_addr.get_value())
drmodtrack_dump(outf);
dr_close_file(outf);
}
drx_exit();
drwrap_exit();
drmgr_exit();
if (op_print_ret_addr.get_value())
drmodtrack_exit();
}
DR_EXPORT void
dr_client_main(client_id_t id, int argc, const char *argv[])
{
module_data_t *exe;
drsys_options_t ops = { sizeof(ops), 0, };
IF_DEBUG(bool ok;)
dr_set_client_name("Dr. LTrace", "http://drmemory.org/issues");
if (!droption_parser_t::parse_argv(DROPTION_SCOPE_CLIENT, argc, argv,
NULL, NULL))
ASSERT(false, "unable to parse options specified for drltracelib");
op_print_stderr = true;
IF_DEBUG(ok = )
drmgr_init();
ASSERT(ok, "drmgr failed to initialize");
IF_DEBUG(ok = )
drwrap_init();
ASSERT(ok, "drwrap failed to initialize");
IF_DEBUG(ok = )
drx_init();
ASSERT(ok, "drx failed to initialize");
if (op_print_ret_addr.get_value()) {
IF_DEBUG(ok = )
drmodtrack_init();
ASSERT(ok == DRCOVLIB_SUCCESS, "drmodtrack failed to initialize");
}
exe = dr_get_main_module();
if (exe != NULL)
exe_start = exe->start;
dr_free_module_data(exe);
/* No-frills is safe b/c we're the only module doing wrapping, and
* we're only wrapping at module load and unwrapping at unload.
* Fast cleancalls is safe b/c we're only wrapping func entry and
* we don't care about the app context.
*/
drwrap_set_global_flags((drwrap_global_flags_t)
(DRWRAP_NO_FRILLS | DRWRAP_FAST_CLEANCALLS));
drmgr_register_exit_event(event_exit);
#ifdef UNIX
dr_register_fork_init_event(event_fork);
#endif
drmgr_register_module_load_event(event_module_load);
drmgr_register_module_unload_event(event_module_unload);
#ifdef WINDOWS
dr_enable_console_printing();
#endif
if (op_max_args.get_value() > 0) {
/* get_value() makes a copy which will be thrown away! We need a permanent
* copy.
*/
const std::string& sysnum_permanent = op_sysnum_file.get_value();
const char *sysnum_file = NULL;
if (!op_sysnum_file.get_value().empty()) {
sysnum_file = sysnum_permanent.c_str();
if (dr_file_exists(sysnum_file)) {
VNOTIFY(1, "<Using system call file %s>" NL, sysnum_file);
ops.sysnum_file = sysnum_file;
} else {
VNOTIFY(2, "<System call file %s does not exist>" NL, sysnum_file);
}
} else
VNOTIFY(1, "sysnum_file is empty" NL);
drmf_status_t res = drsys_init(id, &ops);
#ifdef WINDOWS
if (res == DRMF_WARNING_UNSUPPORTED_KERNEL) {
dr_os_version_info_t os_version = {sizeof(os_version),};
dr_get_os_version(&os_version);
NOTIFY_ERROR("System call information is missing for this operating system: "
"WinVer=%u;Rel=%s;Build=%u;Edition=%s. Restarting "
"to trigger auto-generation of system call information." NL,
os_version.version, os_version.release_id,
os_version.build_number, os_version.edition);
dr_abort_with_code(STATUS_INVALID_KERNEL_INFO_VERSION);
}
#endif
if (res != DRMF_SUCCESS)
ASSERT(false, "drsys failed to init");
parse_config();
}
open_log_file();
}