1458 lines
35 KiB
C++
1458 lines
35 KiB
C++
#include "callgraph.h"
|
|
|
|
|
|
int data_id;
|
|
|
|
static bool get_graph_title(ea_t ea, qstring *out);
|
|
|
|
//-------------------------------------------------------------------------
|
|
#define ACTION_NAME "callgraph:ShowCallgraph"
|
|
#define ACTION_LABEL "Function call graph"
|
|
|
|
plugin_ctx_t::plugin_ctx_t()
|
|
: main_action(ACTION_DESC_LITERAL_PLUGMOD(
|
|
ACTION_NAME,
|
|
ACTION_LABEL,
|
|
&show_callgraph_ah,
|
|
this,
|
|
"Ctrl+Shift+B",
|
|
nullptr, -1))
|
|
{
|
|
::hook_event_listener(HT_IDP, &idp_merge_listener, this);
|
|
::hook_event_listener(HT_VIEW, &view_listener, this);
|
|
}
|
|
|
|
bool plugin_ctx_t::register_main_action()
|
|
{
|
|
return register_action(main_action)
|
|
&& attach_action_to_menu("View/Open subviews/Function calls",
|
|
ACTION_NAME, SETMENU_APP);
|
|
}
|
|
|
|
plugin_ctx_t::~plugin_ctx_t()
|
|
{
|
|
clr_module_data(data_id);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
int idaapi show_callgraph_ah_t::activate(action_activation_ctx_t *)
|
|
{
|
|
ctx.run(0);
|
|
return 0;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Checks if a function is visited already
|
|
// If it is visited then true is returned and nid contains the node ID
|
|
bool callgraph_t::visited(ea_t func_ea, int *nid)
|
|
{
|
|
ea_int_map_t::const_iterator it = ea2node.find(func_ea);
|
|
if ( it != ea2node.end() )
|
|
{
|
|
if ( nid != nullptr )
|
|
*nid = it->second;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void callgraph_t::add_fathers(
|
|
func_t * /*func*/,
|
|
ea_t func_start,
|
|
int id,
|
|
funcs_walk_options_t *opt,
|
|
int level)
|
|
{
|
|
if ( level >= (opt->callers_recurse_limit+2) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
//msg("Level %d, node 0x%08x\n", level, func_start);
|
|
xrefblk_t xb_to;
|
|
|
|
for ( bool xb_to_ok = xb_to.first_to(func_start, XREF_FAR);
|
|
xb_to_ok && xb_to.iscode;
|
|
xb_to_ok = xb_to.next_to() )
|
|
{
|
|
func_t *f_from = get_func(xb_to.from);
|
|
if ( f_from == nullptr )
|
|
continue;
|
|
|
|
int idto = add(f_from->start_ea);
|
|
//msg("Adding XREF to 1st node %d\n", idto);
|
|
create_edge(idto, id);
|
|
|
|
add_fathers(f_from, f_from->start_ea, idto, opt, level+1);
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
int callgraph_t::walk_func(
|
|
eavec_t *hide_nodes,
|
|
func_t *func,
|
|
funcs_walk_options_t *opt,
|
|
int level)
|
|
{
|
|
// add a node for this function
|
|
ea_t func_start = func->start_ea;
|
|
|
|
int id = add(func_start);
|
|
|
|
// Add the callers of the 1st function
|
|
if ( level == 2 )
|
|
{
|
|
add_fathers(func, func_start, id, opt, 2);
|
|
}
|
|
|
|
int total = 0;
|
|
func_item_iterator_t fii;
|
|
|
|
for ( bool fi_ok=fii.set(func); fi_ok; fi_ok=fii.next_code() )
|
|
{
|
|
xrefblk_t xb;
|
|
for ( bool xb_ok = xb.first_from(fii.current(), XREF_FAR);
|
|
xb_ok && xb.iscode;
|
|
xb_ok = xb.next_from() )
|
|
{
|
|
bool is_func_lib;
|
|
ea_t ea;
|
|
|
|
func_t *f = get_func(xb.to);
|
|
if ( f == nullptr )
|
|
{
|
|
ea = xb.to;
|
|
is_func_lib = true;
|
|
|
|
if ( (opt->flags & FWO_SKIPLIB) != 0 )
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
ea = f->start_ea;
|
|
is_func_lib = false;
|
|
}
|
|
|
|
eavec_t::iterator hide_nodes_it;
|
|
|
|
// Any node to hide?
|
|
if ( !hide_nodes->empty() )
|
|
{
|
|
hide_nodes_it = std::find(hide_nodes->begin(), hide_nodes->end(), ea);
|
|
if ( *hide_nodes_it == ea )
|
|
{
|
|
//msg("Hiding node 0x%08x\n", *hide_nodes_it);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
int id2 = -1;
|
|
if ( !visited(ea, &id2) )
|
|
{
|
|
if ( func_contains(func, xb.to) )
|
|
continue;
|
|
|
|
bool skip = false;
|
|
|
|
if ( opt != nullptr )
|
|
{
|
|
skip = is_func_lib && (opt->flags & FWO_SKIPLIB) != 0 // skip lib funcs?
|
|
|| ((opt->flags & FWO_CALLEE_RECURSE_UNLIM) == 0 // max recursion is off, and limit is reached?
|
|
&& level > opt->callees_recurse_limit);
|
|
}
|
|
|
|
// More nodes in this level than the maximum specified?
|
|
if ( total++ >= ctx.fg_opts.max_nodes )
|
|
{
|
|
id2 = add((ea_t)VERTEX_HIDDEN_NODES);
|
|
create_edge(id, id2);
|
|
break;
|
|
}
|
|
|
|
if ( skip )
|
|
id2 = add(ea);
|
|
else if ( !is_func_lib )
|
|
id2 = walk_func(hide_nodes, f, opt, level+1);
|
|
else if ( (opt->flags & FWO_SKIPLIB) == 0 )
|
|
id2 = add(ea);
|
|
|
|
if ( id2 != -1 )
|
|
create_edge(id, id2);
|
|
}
|
|
//msg("Adding edge between %d and %d\n", id, id2);
|
|
}
|
|
}
|
|
return id;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
int callgraph_t::find_first(const char *text)
|
|
{
|
|
if ( text == nullptr || text[0] == '\0' )
|
|
return -1;
|
|
|
|
qstrncpy(cur_text, text, sizeof(cur_text));
|
|
cur_node = 0;
|
|
return find_next();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
int callgraph_t::find_next()
|
|
{
|
|
for ( int i = cur_node; i < node_count; i++ )
|
|
{
|
|
const char *s = get_name(i);
|
|
if ( stristr(s, cur_text) != nullptr )
|
|
{
|
|
cur_node = i + 1;
|
|
return i;
|
|
}
|
|
}
|
|
// reset search
|
|
cur_node = 0;
|
|
// nothing is found
|
|
return -1;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void callgraph_t::create_edge(int id1, int id2)
|
|
{
|
|
edges.push_back(edge_t(id1, id2));
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void callgraph_t::reset()
|
|
{
|
|
node_count = 0;
|
|
cur_node = 0;
|
|
cur_text[0] = '\0';
|
|
ea2node.clear();
|
|
node2ea.clear();
|
|
cached_funcs.clear();
|
|
edges.clear();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
ea_t callgraph_t::get_addr(int nid) const
|
|
{
|
|
int_ea_map_t::const_iterator it = node2ea.find(nid);
|
|
return it == node2ea.end() ? BADADDR : it->second;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Given an address, this function first returns ASCII string if found
|
|
// otherwise it returns a UNICODE string
|
|
// FIXME: not comprehensive, better follow the settings in strings options
|
|
size_t get_string(ea_t ea, qstring *out)
|
|
{
|
|
const char *encodings[2] =
|
|
{
|
|
encoding_from_strtype(STRTYPE_C),
|
|
inf_is_be() ? ENC_UTF16BE : ENC_UTF16LE
|
|
};
|
|
for ( int i = 0; i < qnumber(encodings); i++ )
|
|
{
|
|
int enc_idx = add_encoding(encodings[i]);
|
|
uint32 strtype = STRTYPE_C | (enc_idx << 24);
|
|
size_t len = get_max_strlit_length(ea, strtype);
|
|
if ( len > 4 && get_strlit_contents(out, ea, len, strtype) > 0 )
|
|
break;
|
|
out->qclear();
|
|
}
|
|
return out->size();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool get_strings(ea_t ea, qstring *out)
|
|
{
|
|
qstring tmp;
|
|
|
|
func_t *func = get_func(ea);
|
|
func_item_iterator_t fii;
|
|
for ( bool fi_ok=fii.set(func); fi_ok; fi_ok=fii.next_code() )
|
|
{
|
|
xrefblk_t xb;
|
|
for ( bool xb_ok = xb.first_from(fii.current(), XREF_DATA);
|
|
xb_ok;
|
|
xb_ok = xb.next_from() )
|
|
{
|
|
if ( get_string(xb.to, &tmp) > 0 )
|
|
*out += tmp + "\n";
|
|
}
|
|
}
|
|
|
|
if ( out->size() > 1 )
|
|
out->insert("\n\nStrings:\n");
|
|
|
|
return !out->empty();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
callgraph_t::funcinfo_t *callgraph_t::get_info(int nid)
|
|
{
|
|
funcinfo_t *ret = nullptr;
|
|
|
|
do
|
|
{
|
|
// returned cached name
|
|
int_funcinfo_map_t::iterator it = cached_funcs.find(nid);
|
|
|
|
if ( it != cached_funcs.end() )
|
|
{
|
|
ret = &it->second;
|
|
break;
|
|
}
|
|
|
|
// node does not exist?
|
|
int_ea_map_t::const_iterator it_ea = node2ea.find(nid);
|
|
if ( it_ea == node2ea.end() )
|
|
break;
|
|
|
|
funcinfo_t fi;
|
|
|
|
qstring buf;
|
|
if ( ::get_name(&buf, it_ea->second) <= 0 )
|
|
{
|
|
/*
|
|
** NOTE: With patched databases it may fail for a reason unknown (ATM).
|
|
** To test it, open an Objective-C app and patch it with the following
|
|
** script: https://github.com/zynamics/objc-helper-plugin-ida
|
|
*/
|
|
if ( (int32)it_ea->second == VERTEX_HIDDEN_NODES )
|
|
{
|
|
fi.name = "More nodes hidden...";
|
|
}
|
|
else
|
|
{
|
|
msg("%a: Invalid address\n", it_ea->second);
|
|
fi.name = "?";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qstring outbuf = buf;
|
|
qstring demangled;
|
|
if ( demangle_name(&demangled, buf.begin(), MNG_SHORT_FORM) > 0 )
|
|
{
|
|
outbuf.append("\n");
|
|
outbuf.append(demangled);
|
|
}
|
|
|
|
// Assign the name
|
|
fi.name = outbuf;
|
|
|
|
// Add the strings reference if set
|
|
qstring strings;
|
|
if ( (ctx.fg_opts.flags & FWO_SHOWSTRING) != 0 && get_strings(it_ea->second, &strings) )
|
|
fi.strings = strings;
|
|
}
|
|
|
|
// XXX: FIXME: UGLY HACK
|
|
// Use a special color for the selected node
|
|
if ( nid == 0 )
|
|
{
|
|
fi.color = 0x44FF55;
|
|
}
|
|
else
|
|
{
|
|
// Is it an imported function?
|
|
segment_t *seg = getseg(it_ea->second);
|
|
if ( seg != nullptr && seg->type == SEG_XTRN )
|
|
{
|
|
fi.color = 0xf000f0;
|
|
}
|
|
else
|
|
{
|
|
// XXX: FIXME Horrible...
|
|
func_t *f = get_func(it_ea->second);
|
|
if ( f != nullptr
|
|
&& ((f->flags & FUNC_LIB) != 0 || buf[0] == '.') )
|
|
{
|
|
fi.color = 0xfff000;
|
|
}
|
|
else
|
|
{
|
|
fi.color = calc_bg_color(it_ea->second);
|
|
}
|
|
}
|
|
}
|
|
|
|
fi.ea = it_ea->second;
|
|
|
|
it = cached_funcs.insert(cached_funcs.end(), std::make_pair(nid, fi)); //-V783 Dereferencing of the invalid iterator
|
|
ret = &it->second;
|
|
} while ( false );
|
|
|
|
return ret;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
const char *callgraph_t::get_name(int nid)
|
|
{
|
|
funcinfo_t *fi = get_info(nid);
|
|
if ( fi == nullptr )
|
|
return "?";
|
|
else
|
|
return fi->name.c_str();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
int callgraph_t::add(ea_t func_ea)
|
|
{
|
|
ea_int_map_t::const_iterator it = ea2node.find(func_ea);
|
|
if ( it != ea2node.end() )
|
|
return it->second;
|
|
|
|
ea2node[func_ea] = node_count;
|
|
node2ea[node_count] = func_ea;
|
|
return node_count++;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
callgraph_t::callgraph_t(plugin_ctx_t &_ctx) : ctx(_ctx)
|
|
{
|
|
cur_text[0] = '\0';
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void callgraph_t::clear_edges()
|
|
{
|
|
edges.clear();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
//--------------------------------------------------------------------------
|
|
//--------------------------------------------------------------------------
|
|
bool graph_info_t::find(plugin_ctx_t &ctx, ea_t ea, iterator *out)
|
|
{
|
|
iterator end = ctx.instances.end();
|
|
for ( iterator it = ctx.instances.begin(); it != end; ++it )
|
|
{
|
|
if ( (*it)->func_ea == ea )
|
|
{
|
|
if ( out != nullptr )
|
|
*out = it;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
graph_info_t *graph_info_t::find(plugin_ctx_t &ctx, ea_t ea)
|
|
{
|
|
iterator it;
|
|
return find(ctx, ea, &it) ? *it : nullptr;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
graph_info_t *graph_info_t::find(plugin_ctx_t &ctx, const char *_title)
|
|
{
|
|
for ( auto &gi : ctx.instances )
|
|
if ( gi->title == _title )
|
|
return gi;
|
|
return nullptr;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
graph_info_t *graph_info_t::find(plugin_ctx_t &ctx, const graph_viewer_t *v)
|
|
{
|
|
for ( auto &gi : ctx.instances )
|
|
if ( gi->gv == v )
|
|
return gi;
|
|
return nullptr;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
graph_info_t *graph_info_t::create(plugin_ctx_t &ctx, ea_t ea)
|
|
{
|
|
graph_info_t *r = find(ctx, ea);
|
|
|
|
// not there? create it
|
|
if ( r == nullptr )
|
|
{
|
|
// we need a function!
|
|
func_t *pfn = get_func(ea);
|
|
if ( pfn == nullptr )
|
|
return nullptr;
|
|
|
|
r = new graph_info_t(ctx);
|
|
get_graph_title(ea, &r->title);
|
|
r->func_ea = pfn->start_ea;
|
|
ctx.instances.push_back(r);
|
|
|
|
r->install_hooks();
|
|
}
|
|
return r;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Check if the user changed any of the functions in the current graph
|
|
static void check_func_changed(ea_t ea, graph_info_t &gi)
|
|
{
|
|
ea_int_map_t::const_iterator it = gi.fg.ea2node.find(ea);
|
|
if ( it != gi.fg.ea2node.end() )
|
|
{
|
|
// The center node has been changed, destroy the current callgraph
|
|
if ( it->second == 0 )
|
|
close_widget(gi.widget, WCLS_SAVE);
|
|
else
|
|
// A function shown in the callgraph has been changed, refresh
|
|
// the callgraph
|
|
gi.refresh();
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// We hook to IDP event to receive processor module notifications
|
|
ssize_t idaapi idb_gi_listener_t::on_event(ssize_t code, va_list va)
|
|
{
|
|
switch ( code )
|
|
{
|
|
case idb_event::func_added:
|
|
case idb_event::func_updated:
|
|
case idb_event::deleting_func:
|
|
case idb_event::set_func_start:
|
|
case idb_event::set_func_end:
|
|
{
|
|
func_t *pfn = va_arg(va, func_t *);
|
|
check_func_changed(pfn->start_ea, gi);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// We hook to IDP event to receive processor module notifications
|
|
ssize_t idaapi idp_gi_listener_t::on_event(ssize_t code, va_list va)
|
|
{
|
|
switch ( code )
|
|
{
|
|
case processor_t::ev_create_switch_xrefs:
|
|
case processor_t::ev_add_cref:
|
|
case processor_t::ev_del_cref:
|
|
{
|
|
ea_t ea = va_arg(va, ea_t);
|
|
check_func_changed(ea, gi);
|
|
}
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
ssize_t idaapi idp_merge_listener_t::on_event(ssize_t code, va_list va)
|
|
{
|
|
switch ( code )
|
|
{
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void graph_info_t::install_hooks()
|
|
{
|
|
hook_event_listener(HT_IDP, &idp_gi_listener, &ctx);
|
|
hook_event_listener(HT_IDB, &idb_gi_listener, &ctx);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void graph_info_t::remove_hooks()
|
|
{
|
|
unhook_event_listener(HT_IDP, &idp_gi_listener);
|
|
unhook_event_listener(HT_IDB, &idb_gi_listener);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void graph_info_t::destroy_graph(plugin_ctx_t &ctx, graph_info_t *gi)
|
|
{
|
|
iterator it;
|
|
if ( find(ctx, gi->func_ea, &it) )
|
|
{
|
|
ctx.instances.erase(it);
|
|
gi->remove_hooks();
|
|
delete gi;
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
// Get a new title for the form to be opened
|
|
qstring plugin_ctx_t::gen_graph_title(ea_t ea)
|
|
{
|
|
// We should succeed in getting the name
|
|
qstring func_name;
|
|
if ( get_func_name(&func_name, ea) > 0 )
|
|
{
|
|
qstring tmp;
|
|
for ( int i=1; i < 255; i++ )
|
|
{
|
|
tmp.sprnt("Call graph: %s (%d)", func_name.begin(), i);
|
|
if ( find_widget(tmp.c_str()) == nullptr )
|
|
return tmp;
|
|
}
|
|
}
|
|
return qstring();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
static bool get_graph_title(ea_t ea, qstring *out)
|
|
{
|
|
// we should succeed in getting the name
|
|
qstring func_name;
|
|
if ( get_func_name(&func_name, ea) <= 0 )
|
|
return false;
|
|
|
|
out->sprnt("Call graph: %s", func_name.begin());
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void graph_info_t::mark_for_refresh()
|
|
{
|
|
refresh_needed = true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void graph_info_t::mark_as_refreshed()
|
|
{
|
|
refresh_needed = false;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void graph_info_t::refresh()
|
|
{
|
|
mark_for_refresh();
|
|
refresh_viewer(gv);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
void idaapi callgraph_t::user_refresh(
|
|
void *ud,
|
|
int code,
|
|
va_list va,
|
|
int current_node)
|
|
{
|
|
graph_info_t *gi = (graph_info_t *) ud;
|
|
callgraph_t *fg = &gi->fg;
|
|
qnotused(code);
|
|
qnotused(current_node);
|
|
if ( !gi->is_refresh_needed() )
|
|
return;
|
|
|
|
gi->mark_as_refreshed();
|
|
fg->reset();
|
|
|
|
func_t *f = get_func(gi->func_ea);
|
|
if ( f == nullptr )
|
|
{
|
|
msg("%a: Invalid function\n", gi->func_ea);
|
|
return;
|
|
}
|
|
|
|
fg->walk_func(&gi->hide_nodes, f, &fg->ctx.fg_opts, 2);
|
|
|
|
mutable_graph_t *mg = va_arg(va, mutable_graph_t *);
|
|
|
|
// we have to resize
|
|
mg->reset();
|
|
mg->resize(fg->count());
|
|
|
|
callgraph_t::edge_iterator it;
|
|
callgraph_t::edge_iterator end = fg->end_edges();
|
|
|
|
for ( it=fg->begin_edges(); it != end; ++it )
|
|
mg->add_edge(it->id1, it->id2, nullptr);
|
|
|
|
fg->clear_edges();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
ssize_t idaapi callgraph_t::gr_callback(void *ud, int code, va_list va)
|
|
{
|
|
bool result = false;
|
|
graph_info_t *gi = (graph_info_t *) ud;
|
|
callgraph_t *fg = &gi->fg;
|
|
switch ( code )
|
|
{
|
|
// a graph node has been double clicked
|
|
// in: graph_viewer_t *gv
|
|
// selection_item_t *current_item
|
|
// out: 0-ok, 1-ignore click
|
|
case grcode_dblclicked:
|
|
result = fg->center(gi);
|
|
break;
|
|
// refresh user-defined graph nodes and edges
|
|
// in: mutable_graph_t *g
|
|
// out: success
|
|
case grcode_user_refresh:
|
|
user_refresh(ud, code, va, -1);
|
|
result = true;
|
|
break;
|
|
|
|
// retrieve text for user-defined graph node
|
|
// in: mutable_graph_t *g
|
|
// int node
|
|
// const char **result
|
|
// bgcolor_t *bg_color (maybe nullptr)
|
|
// out: must return 0, result must be filled
|
|
case grcode_user_text:
|
|
{
|
|
va_arg(va, mutable_graph_t *);
|
|
int node = va_arg(va, int);
|
|
const char **text = va_arg(va, const char **);
|
|
bgcolor_t *bgcolor = va_arg(va, bgcolor_t *);
|
|
|
|
callgraph_t::funcinfo_t *fi = fg->get_info(node);
|
|
result = fi != nullptr;
|
|
if ( result )
|
|
{
|
|
qstring hint;
|
|
|
|
hint = fi->name + fi->strings;
|
|
*text = hint.extract();
|
|
//*text = fi->name.c_str();
|
|
if ( bgcolor != nullptr )
|
|
*bgcolor = fi->color;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// retrieve hint for the user-defined graph
|
|
// in: mutable_graph_t *g
|
|
// int mousenode
|
|
// int mouseedge_src
|
|
// int mouseedge_dst
|
|
// char **hint
|
|
// 'hint' must be allocated by qalloc() or qstrdup()
|
|
// out: 0-use default hint, 1-use proposed hint
|
|
case grcode_user_hint:
|
|
{
|
|
va_arg(va, mutable_graph_t *);
|
|
|
|
int mousenode = va_argi(va, int);
|
|
int to = va_argi(va, int);
|
|
int from = va_argi(va, int);
|
|
char **hint = va_arg(va, char **);
|
|
|
|
result = true;
|
|
|
|
ea_t addr;
|
|
if ( mousenode != -1 && (addr = fg->get_addr(mousenode)) != BADADDR )
|
|
{
|
|
qstrvec_t lines;
|
|
qstring all_lines;
|
|
|
|
for ( int j=0; j < 16; j++ )
|
|
{
|
|
int nl = generate_disassembly(&lines, nullptr, addr, 1024, false);
|
|
|
|
for ( int i = 0; i < nl; i++ )
|
|
{
|
|
all_lines.append(lines[i]);
|
|
all_lines.append('\n');
|
|
}
|
|
addr = get_item_end(addr);
|
|
}
|
|
|
|
*hint = all_lines.extract();
|
|
}
|
|
else if ( mousenode == -1 )
|
|
{
|
|
qstring line;
|
|
|
|
if ( from != -1 && to != -1 )
|
|
{
|
|
funcinfo_t *fifrom = fg->get_info(from);
|
|
funcinfo_t *fito = fg->get_info(to);
|
|
|
|
// XXX: FIXME: Hack. It should be fixed hooking to del_func, etc...
|
|
if ( fifrom == nullptr || fito == nullptr )
|
|
{
|
|
msg("Invalid function\n");
|
|
result = false;
|
|
}
|
|
else
|
|
{
|
|
line.insert(fifrom->name.c_str());
|
|
line.insert(" -> ");
|
|
line.insert(fito->name.c_str());
|
|
|
|
*hint = line.extract();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return (int)result;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool plugin_ctx_t::load_options()
|
|
{
|
|
funcs_walk_options_t opt;
|
|
netnode n(PROCMOD_NODE_NAME);
|
|
if ( !exist(n) )
|
|
return false;
|
|
|
|
n.supval(1, &opt, sizeof(opt));
|
|
|
|
if ( opt.version != FWO_VERSION )
|
|
return false;
|
|
|
|
fg_opts = opt;
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void plugin_ctx_t::save_options()
|
|
{
|
|
netnode n;
|
|
n.create(PROCMOD_NODE_NAME);
|
|
n.supset(1, &fg_opts, sizeof(fg_opts));
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
static int idaapi options_cb(int fid, form_actions_t &fa)
|
|
{
|
|
ushort opt = 0;
|
|
|
|
if ( fid == FIELD_ID_CHILDS || fid == CB_INIT )
|
|
{
|
|
if ( !fa.get_checkbox_value(FIELD_ID_CHILDS, &opt) )
|
|
INTERR(562);
|
|
|
|
// Disable recursion level textbox
|
|
fa.enable_field(FIELD_ID_CHILDS_LEVEL, !opt);//(opt & FWO_CALLEE_RECURSE_UNLIM) == 0);
|
|
}
|
|
|
|
if ( fid == FIELD_ID_FATHERS )
|
|
{
|
|
if ( !fa.get_checkbox_value(FIELD_ID_FATHERS, &opt) )
|
|
INTERR(563);
|
|
|
|
if ( opt > MAX_CALLERS_LEVEL )
|
|
{
|
|
info("Sorry, value is too big: %d", opt);
|
|
opt = MAX_CALLERS_LEVEL;
|
|
fa.set_checkbox_value(FIELD_ID_FATHERS, &opt);
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool plugin_ctx_t::show_options()
|
|
{
|
|
static const char opt_form[] =
|
|
"Call graph configuration\n"
|
|
"%/"
|
|
"<##Show ~s~tring references:C1>\n"
|
|
"<##Options##Hide ~l~ibrary functions:C2>\n"
|
|
"<##Max ~p~arents recursion level:D3:5:5::>\n"
|
|
"<##Unlimited children recursion:C4>5>\n"
|
|
"<##Max ~c~hildren recursion level:D6:5:5::>\n"
|
|
"<##~L~imit of nodes per level:D7:5:5::>\n"
|
|
;
|
|
|
|
ushort opt = fg_opts.flags;
|
|
|
|
// When analyzing big functions, fg_opts.recurse_limit is too big
|
|
sval_t callers_limit = fg_opts.callers_recurse_limit;
|
|
sval_t callees_limit = fg_opts.callees_recurse_limit;
|
|
sval_t max_nodes = fg_opts.max_nodes;
|
|
|
|
if ( !ask_form(opt_form,
|
|
options_cb,
|
|
&callers_limit,
|
|
&opt,
|
|
&callees_limit,
|
|
&max_nodes) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( callees_limit <= 0 )
|
|
{
|
|
callers_limit = 0;
|
|
opt |= FWO_CALLEE_RECURSE_UNLIM;
|
|
}
|
|
|
|
fg_opts.flags = opt;
|
|
fg_opts.callees_recurse_limit = callees_limit;
|
|
fg_opts.callers_recurse_limit = callers_limit;
|
|
fg_opts.max_nodes = max_nodes;
|
|
|
|
save_options();
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
static void jump_to_node(const graph_info_t *gi, const int nid)
|
|
{
|
|
viewer_center_on(gi->gv, nid);
|
|
int x, y;
|
|
|
|
// will return a place only when a node was previously selected
|
|
place_t *old_pl = get_custom_viewer_place(gi->gv, false, &x, &y);
|
|
if ( old_pl != nullptr )
|
|
{
|
|
user_graph_place_t *new_pl = (user_graph_place_t *) old_pl->clone();
|
|
new_pl->node = nid;
|
|
jumpto(gi->gv, new_pl, x, y);
|
|
::qfree(new_pl);
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
static int findfirst_node(callgraph_t *fg)
|
|
{
|
|
static const char form[] =
|
|
"Enter search substring\n"
|
|
"\n"
|
|
" <#Search is not case sensitive#Function name:q:1000:50::>\n\n";
|
|
|
|
CASSERT(IS_QSTRING(fg->ctx.last_text));
|
|
if ( !ask_form(form, &fg->ctx.last_text) )
|
|
return -2;
|
|
|
|
return fg->find_first(fg->ctx.last_text.c_str());
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
static void display_node_search_result(graph_info_t *gi, int nid)
|
|
{
|
|
// search was cancelled
|
|
if ( nid == -2 )
|
|
return;
|
|
|
|
const char *txt = gi->fg.get_findtext();
|
|
if ( nid == -1 )
|
|
{
|
|
msg("No match for '%s'\n", txt);
|
|
}
|
|
else
|
|
{
|
|
msg("%a: matched '%s'\n", gi->fg.get_addr(nid), txt);
|
|
jump_to_node(gi, nid);
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::options(graph_info_t *gi) const
|
|
{
|
|
if ( ctx.show_options() )
|
|
gi->refresh();
|
|
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::refresh(graph_info_t *gi) const
|
|
{
|
|
gi->refresh();
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::jumpxref(graph_info_t *gi) const
|
|
{
|
|
int node;
|
|
ea_t addr;
|
|
|
|
node = viewer_get_curnode(gi->gv);
|
|
if ( node != -1 )
|
|
{
|
|
addr = gi->fg.get_addr(node);
|
|
ea_t xref = choose_xref(addr);
|
|
|
|
if ( xref != 0 && xref != BADADDR )
|
|
navigate(gi, xref);
|
|
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::jumpaddr(graph_info_t *gi) const
|
|
{
|
|
ea_t addr;
|
|
|
|
if ( ask_addr(&addr, "Jump address") )
|
|
{
|
|
func_t *pfn = get_func(addr);
|
|
if ( pfn == nullptr )
|
|
{
|
|
warning("You have entered an invalid address");
|
|
return false;
|
|
}
|
|
navigate(gi, addr);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::jump(const graph_info_t *gi) const
|
|
{
|
|
int node;
|
|
ea_t addr;
|
|
|
|
node = viewer_get_curnode(gi->gv);
|
|
if ( node != -1 )
|
|
{
|
|
addr = gi->fg.get_addr(node);
|
|
jumpto(addr);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::back(graph_info_t *gi) const
|
|
{
|
|
if ( gi->queue.empty() )
|
|
close_widget(gi->widget, WCLS_SAVE);
|
|
else
|
|
go_back(gi);
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::forward(graph_info_t *gi) const
|
|
{
|
|
if ( !gi->forward_queue.empty() )
|
|
go_forward(gi);
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::center(graph_info_t *gi) const
|
|
{
|
|
int node;
|
|
ea_t addr;
|
|
|
|
node = viewer_get_curnode(gi->gv);
|
|
if ( node != -1 )
|
|
{
|
|
addr = gi->fg.get_addr(node);
|
|
return navigate(gi, addr);
|
|
}
|
|
else
|
|
return false;
|
|
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// node chooser's helper
|
|
struct node_chooser_t : public chooser_t
|
|
{
|
|
protected:
|
|
static const int widths_[];
|
|
static const char *const header_[];
|
|
|
|
public:
|
|
// this chooser is modal
|
|
node_chooser_t(const char * title);
|
|
};
|
|
|
|
const int node_chooser_t::widths_[] =
|
|
{
|
|
CHCOL_HEX | 32, // Function
|
|
10, // Address
|
|
};
|
|
const char *const node_chooser_t::header_[] =
|
|
{
|
|
"Function", // 0
|
|
"Address", // 1
|
|
};
|
|
|
|
inline node_chooser_t::node_chooser_t(const char *title_)
|
|
: chooser_t(CH_MODAL | CH_KEEP,
|
|
qnumber(widths_), widths_, header_,
|
|
title_)
|
|
{
|
|
CASSERT(qnumber(widths_) == qnumber(header_));
|
|
}
|
|
|
|
|
|
//-------------------------------------------------------------------------
|
|
// modal call node chooser
|
|
struct call_node_chooser_t : public node_chooser_t
|
|
{
|
|
const callgraph_t &fg;
|
|
|
|
// this chooser is modal
|
|
call_node_chooser_t(const callgraph_t &fg_)
|
|
: node_chooser_t("Select function"), fg(fg_) {}
|
|
|
|
virtual size_t idaapi get_count() const override { return fg.count(); }
|
|
virtual void idaapi get_row(
|
|
qstrvec_t *cols,
|
|
int *icon_,
|
|
chooser_item_attrs_t *attrs,
|
|
size_t n) const override;
|
|
};
|
|
|
|
void idaapi call_node_chooser_t::get_row(
|
|
qstrvec_t *cols_,
|
|
int *,
|
|
chooser_item_attrs_t *,
|
|
size_t n) const
|
|
{
|
|
ea_t ea = fg.get_addr(n);
|
|
|
|
qstrvec_t &cols = *cols_;
|
|
if ( get_name(&cols[0], ea) > 0 )
|
|
cols[1].sprnt("%a", ea);
|
|
CASSERT(qnumber(header_) == 2);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::select(const graph_info_t *gi) const
|
|
{
|
|
call_node_chooser_t ch(gi->fg);
|
|
ssize_t n = ch.choose(chooser_base_t::NO_SELECTION); // why?
|
|
if ( n >= 0 )
|
|
jump_to_node(gi, n);
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::home(const graph_info_t *gi) const
|
|
{
|
|
if ( count() > 1 )
|
|
jump_to_node(gi, 0);
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::searchfirst(graph_info_t *gi)
|
|
{
|
|
display_node_search_result(gi, findfirst_node(this));
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::searchnext(graph_info_t *gi)
|
|
{
|
|
display_node_search_result(gi, find_next());
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::hidenode(graph_info_t *gi) const
|
|
{
|
|
int node;
|
|
ea_t addr;
|
|
|
|
node = viewer_get_curnode(gi->gv);
|
|
if ( node != -1 )
|
|
{
|
|
addr = gi->fg.get_addr(node);
|
|
//msg("Should hide 0x%08x\n", addr);
|
|
gi->hide_nodes.push_back(addr);
|
|
gi->refresh();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// modal hidden node chooser
|
|
struct hidden_node_chooser_t : public node_chooser_t
|
|
{
|
|
const eavec_t &hn;
|
|
|
|
// this chooser is modal
|
|
hidden_node_chooser_t(const eavec_t &hn_)
|
|
: node_chooser_t("Show function"), hn(hn_) {}
|
|
|
|
virtual size_t idaapi get_count() const override { return hn.size(); }
|
|
virtual void idaapi get_row(
|
|
qstrvec_t *cols,
|
|
int *icon_,
|
|
chooser_item_attrs_t *attrs,
|
|
size_t n) const override;
|
|
};
|
|
|
|
void idaapi hidden_node_chooser_t::get_row(
|
|
qstrvec_t *cols_,
|
|
int *,
|
|
chooser_item_attrs_t *,
|
|
size_t n) const
|
|
{
|
|
ea_t ea = hn[n];
|
|
|
|
qstrvec_t &cols = *cols_;
|
|
if ( get_name(&cols[0], ea) > 0 )
|
|
cols[1].sprnt("%a", ea);
|
|
CASSERT(qnumber(header_) == 2);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::showhidden(graph_info_t *gi) const
|
|
{
|
|
if ( gi->hide_nodes.empty() )
|
|
{
|
|
info("No functions hidden\n");
|
|
return true;
|
|
}
|
|
|
|
hidden_node_chooser_t ch(gi->hide_nodes);
|
|
ssize_t n = ch.choose(chooser_base_t::NO_SELECTION); // why?
|
|
if ( n >= 0 )
|
|
{
|
|
eavec_t::iterator it = gi->hide_nodes.begin() + n;
|
|
gi->hide_nodes.erase(it);
|
|
gi->refresh();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::showall(graph_info_t *gi) const
|
|
{
|
|
gi->hide_nodes.clear();
|
|
gi->refresh();
|
|
|
|
return true;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool callgraph_t::navigate(graph_info_t *gi, ea_t addr) const
|
|
{
|
|
ea_t func_ea;
|
|
|
|
func_ea = gi->fg.get_addr(0);
|
|
|
|
// Is it a function?
|
|
func_t *pfn = get_func(addr);
|
|
if ( pfn != nullptr )
|
|
{
|
|
// Is it the same function?
|
|
if ( gi->func_ea != addr )
|
|
{
|
|
// Clear the forward queue
|
|
gi->forward_queue.clear();
|
|
|
|
// Enqueue the current center node
|
|
gi->queue.push_front(func_ea);
|
|
|
|
gi->func_ea = addr;
|
|
gi->refresh();
|
|
|
|
jump_to_node(gi, 0);
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void callgraph_t::go_back(graph_info_t *gi) const
|
|
{
|
|
gi->forward_queue.push_front(gi->func_ea);
|
|
gi->func_ea = gi->queue.front();
|
|
gi->queue.pop_front();
|
|
gi->refresh();
|
|
|
|
jump_to_node(gi, 0);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
void callgraph_t::go_forward(graph_info_t *gi) const
|
|
{
|
|
ea_t ea;
|
|
|
|
ea = gi->forward_queue.front();
|
|
gi->forward_queue.pop_front();
|
|
gi->queue.push_front(gi->func_ea);
|
|
gi->func_ea = ea;
|
|
gi->refresh();
|
|
|
|
jump_to_node(gi, 0);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
void plugin_ctx_t::ensure_actions_registered()
|
|
{
|
|
if ( !actions_registered )
|
|
{
|
|
for ( int i = 0, n = qnumber(actions); i < n; ++i )
|
|
register_action(actions[i]);
|
|
|
|
actions_registered = true;
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
bool idaapi plugin_ctx_t::run(size_t arg)
|
|
{
|
|
if ( ssize_t(arg) == -1 )
|
|
{
|
|
load_options();
|
|
show_options();
|
|
return true;
|
|
}
|
|
|
|
func_t *pfn = get_func(get_screen_ea());
|
|
if ( pfn == nullptr )
|
|
{
|
|
warning("Please position the cursor in a function first!");
|
|
return true;
|
|
}
|
|
|
|
load_options();
|
|
qstring title = gen_graph_title(pfn->start_ea);
|
|
TWidget *form = find_widget(title.c_str());
|
|
if ( form == nullptr )
|
|
{
|
|
// no current window, but instance is in the list?
|
|
graph_info_t *gi = graph_info_t::find(*this, title.c_str());
|
|
if ( gi != nullptr )
|
|
{
|
|
// In that case let us "recycle" the instance
|
|
gi->func_ea = pfn->start_ea;
|
|
}
|
|
else
|
|
{
|
|
// we create a new instance
|
|
gi = graph_info_t::create(*this, pfn->start_ea);
|
|
}
|
|
|
|
if ( gi != nullptr )
|
|
{
|
|
// get a unique graph id
|
|
netnode id;
|
|
id.create("$ callgraph sample");
|
|
|
|
gi->hide_nodes.begin();
|
|
|
|
gi->mark_for_refresh();
|
|
// gi->form = form;
|
|
gi->gv = create_graph_viewer(title.c_str(), id, callgraph_t::gr_callback, gi, 0);
|
|
if ( gi->gv != nullptr )
|
|
{
|
|
display_widget(/*form*/ gi->gv, WOPN_DP_TAB);
|
|
|
|
ensure_actions_registered();
|
|
|
|
viewer_fit_window(gi->gv);
|
|
#define ADD_POPUP(Method) viewer_attach_menu_item(gi->gv, "callgraph:" #Method)
|
|
#define ADD_SEPARATOR() viewer_attach_menu_item(gi->gv, nullptr)
|
|
ADD_POPUP(options);
|
|
ADD_POPUP(refresh);
|
|
ADD_SEPARATOR();
|
|
ADD_POPUP(jumpxref);
|
|
ADD_POPUP(jumpaddr);
|
|
ADD_POPUP(jump);
|
|
ADD_POPUP(back);
|
|
ADD_POPUP(forward);
|
|
ADD_SEPARATOR();
|
|
ADD_POPUP(center);
|
|
ADD_POPUP(select);
|
|
ADD_POPUP(home);
|
|
ADD_POPUP(searchfirst);
|
|
ADD_POPUP(searchnext);
|
|
ADD_POPUP(hidenode);
|
|
ADD_POPUP(showhidden);
|
|
ADD_POPUP(showall);
|
|
#undef ADD_SEPARATOR
|
|
#undef ADD_POPUP
|
|
}
|
|
else
|
|
{
|
|
graph_info_t::destroy_graph(*this, gi);
|
|
gi = nullptr;
|
|
}
|
|
}
|
|
|
|
// Failed to create a graph view?
|
|
if ( gi == nullptr )
|
|
{
|
|
warning("Failed to create call graph window!");
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
graph_info_t *gi = graph_info_t::find(*this, title.c_str());
|
|
if ( gi != nullptr )
|
|
{
|
|
gi->refresh();
|
|
display_widget(gi->gv, WOPN_DP_TAB);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
ssize_t idaapi view_listener_t::on_event(ssize_t code, va_list va)
|
|
{
|
|
if ( code == view_close )
|
|
{
|
|
TWidget *cc = va_arg(va, TWidget *);
|
|
graph_info_t *gi = graph_info_t::find(ctx, (graph_viewer_t *) cc);
|
|
if ( gi != nullptr )
|
|
graph_info_t::destroy_graph(ctx, gi);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
static plugmod_t *idaapi init()
|
|
{
|
|
if ( !is_idaq() ) // GUI version?
|
|
return nullptr;
|
|
plugin_ctx_t *ctx = new plugin_ctx_t;
|
|
if ( !ctx->register_main_action() )
|
|
{
|
|
msg("Failed to register menu item for <" ACTION_LABEL "> plugin!\n");
|
|
delete ctx;
|
|
return nullptr;
|
|
}
|
|
set_module_data(&data_id, ctx);
|
|
return ctx;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
//
|
|
// PLUGIN DESCRIPTION BLOCK
|
|
//
|
|
//--------------------------------------------------------------------------
|
|
plugin_t PLUGIN =
|
|
{
|
|
IDP_INTERFACE_VERSION,
|
|
PLUGIN_HIDE|PLUGIN_MULTI, // plugin flags
|
|
init, // initialize
|
|
|
|
nullptr, // terminate. this pointer may be nullptr.
|
|
nullptr, // invoke plugin
|
|
|
|
// long comment about the plugin
|
|
"Proximity browser plugin.",
|
|
// it could appear in the status line
|
|
// or as a hint
|
|
|
|
// multiline help about the plugin
|
|
"Proximity browser using the graph SDK\n"
|
|
"\n"
|
|
"Position the cursor in a function and run the plugin.",
|
|
|
|
ACTION_LABEL, // the preferred short name of the plugin
|
|
"" // the preferred hotkey to run the plugin
|
|
};
|