zeek/src/Debug.cc
Jon Siwek 0c5afc59f7 Improve script debugger backtrace and print commands.
Stack trace context descriptions are no longer limited to 1024 chars
and better error messages are relayed when the arguments to print
commands fail to parse (e.g. an "unknown identifier" was given).
2012-05-29 14:51:45 -05:00

1000 lines
21 KiB
C++

// Debugging support for Bro policy files.
#include "config.h"
#include <stdio.h>
#include <stdarg.h>
#include <signal.h>
#include <ctype.h>
#include <string>
using namespace std;
#include "util.h"
#include "Debug.h"
#include "DebugCmds.h"
#include "DbgBreakpoint.h"
#include "Stmt.h"
#include "Func.h"
#include "Scope.h"
#include "PolicyFile.h"
#ifdef HAVE_READLINE
#include <readline/readline.h>
#include <readline/history.h>
#endif
bool g_policy_debug = false;
DebuggerState g_debugger_state;
TraceState g_trace_state;
PDict(Filemap) g_dbgfilemaps;
// These variables are used only to decide whether or not to print the
// current context; you don't want to do it after a step or next
// command unless you've exited a function.
static bool step_or_next_pending = false;
static Frame* last_frame;
DebuggerState::DebuggerState()
{
next_bp_id = next_watch_id = next_display_id = 1;
BreakBeforeNextStmt(false);
curr_frame_idx = 0;
BreakFromSignal(false);
// ### Don't choose this arbitrary size! Extend Frame.
dbg_locals = new Frame(1024, /* func = */ 0, /* fn_args = */ 0);
}
DebuggerState::~DebuggerState()
{
Unref(dbg_locals);
}
bool StmtLocMapping::StartsAfter(const StmtLocMapping* m2)
{
if ( ! m2 )
reporter->InternalError("Assertion failed: m2 != 0");
return loc.first_line > m2->loc.first_line ||
(loc.first_line == m2->loc.first_line &&
loc.first_column > m2->loc.first_column);
}
// Generic debug message output.
int debug_msg(const char* fmt, ...)
{
va_list args;
int retval;
va_start(args, fmt);
retval = vfprintf(stderr, fmt, args);
va_end(args);
return retval;
}
// Trace message output
FILE* TraceState::SetTraceFile(const char* filename)
{
FILE* newfile;
if ( streq(filename, "-") )
newfile = stderr;
else
newfile = fopen(filename, "w");
FILE* oldfile = trace_file;
if ( newfile )
{
trace_file = newfile;
}
else
{
fprintf(stderr, "Unable to open trace file %s\n", filename);
trace_file = 0;
}
return oldfile;
}
void TraceState::TraceOn()
{
fprintf(stderr, "Execution tracing ON.\n");
dbgtrace = true;
}
void TraceState::TraceOff()
{
fprintf(stderr, "Execution tracing OFF.\n");
dbgtrace = false;
}
int TraceState::LogTrace(const char* fmt, ...)
{
va_list args;
int retval;
va_start(args, fmt);
// Prefix includes timestamp and file/line info.
fprintf(trace_file, "%.6f ", network_time);
const Stmt* stmt;
Location loc;
loc.filename = 0;
if ( g_frame_stack.size() > 0 && g_frame_stack.back() )
{
stmt = g_frame_stack.back()->GetNextStmt();
if ( stmt )
loc = *stmt->GetLocationInfo();
else
{
const BroFunc* f = g_frame_stack.back()->GetFunction();
if ( f )
loc = *f->GetLocationInfo();
}
}
if ( ! loc.filename )
{
loc.filename = copy_string("<no filename>");
loc.last_line = 0;
}
fprintf(trace_file, "%s:%d", loc.filename, loc.last_line);
// Each stack frame is indented.
for ( int i = 0; i < int(g_frame_stack.size()); ++i )
fprintf(trace_file, "\t");
retval = vfprintf(trace_file, fmt, args);
fflush(trace_file);
va_end(args);
return retval;
}
// Helper functions.
void get_first_statement(Stmt* list, Stmt*& first, Location& loc)
{
if ( ! list )
{
first = 0;
return;
}
first = list;
while ( first->Tag() == STMT_LIST )
{
if ( first->AsStmtList()->Stmts()[0] )
first = first->AsStmtList()->Stmts()[0];
else
break;
}
loc = *first->GetLocationInfo();
}
static void parse_function_name(vector<ParseLocationRec>& result,
ParseLocationRec& plr, const string& s)
{ // function name
ID* id = lookup_ID(s.c_str(), current_module.c_str());
if ( ! id )
{
string fullname = make_full_var_name(current_module.c_str(), s.c_str());
debug_msg("Function %s not defined.\n", fullname.c_str());
plr.type = plrUnknown;
return;
}
FuncType* ftype;
if ( ! (ftype = id->Type()->AsFuncType()) )
{
debug_msg("Function %s not declared.\n", id->Name());
plr.type = plrUnknown;
return;
}
if ( ! id->HasVal() )
{
debug_msg("Function %s declared but not defined.\n", id->Name());
plr.type = plrUnknown;
return;
}
const Func* func = id->ID_Val()->AsFunc();
const vector<Func::Body>& bodies = func->GetBodies();
if ( bodies.size() == 0 )
{
debug_msg("Function %s is a built-in function\n", id->Name());
plr.type = plrUnknown;
return;
}
Stmt* body = 0; // the particular body we care about; 0 = all
if ( bodies.size() == 1 )
body = bodies[0].stmts;
else
{
while ( 1 )
{
debug_msg("There are multiple definitions of that event handler.\n"
"Please choose one of the following options:\n");
for ( unsigned int i = 0; i < bodies.size(); ++i )
{
Stmt* first;
Location stmt_loc;
get_first_statement(bodies[i].stmts, first,
stmt_loc);
debug_msg("[%d] %s:%d\n", i+1, stmt_loc.filename, stmt_loc.first_line);
}
debug_msg("[a] All of the above\n");
debug_msg("[n] None of the above\n");
debug_msg("Enter your choice: ");
char charinput[256];
if ( ! fgets(charinput, sizeof(charinput) - 1, stdin) )
{
plr.type = plrUnknown;
return;
}
if ( charinput[strlen(charinput) - 1] == '\n' )
charinput[strlen(charinput) - 1] = 0;
string input = charinput;
if ( input == "a" )
break;
if ( input == "n" )
{
plr.type = plrUnknown;
return;
}
int option = atoi(input.c_str());
if ( option > 0 && option <= (int) bodies.size() )
{
body = bodies[option - 1].stmts;
break;
}
}
}
plr.type = plrFunction;
// Find first atomic (non-STMT_LIST) statement
Stmt* first;
Location stmt_loc;
if ( body )
{
get_first_statement(body, first, stmt_loc);
if ( first )
{
plr.stmt = first;
plr.filename = stmt_loc.filename;
plr.line = stmt_loc.last_line;
}
}
else
{
result.pop_back();
ParseLocationRec plr;
for ( unsigned int i = 0; i < bodies.size(); ++i )
{
get_first_statement(bodies[i].stmts, first, stmt_loc);
if ( ! first )
continue;
plr.type = plrFunction;
plr.stmt = first;
plr.filename = stmt_loc.filename;
plr.line = stmt_loc.last_line;
result.push_back(plr);
}
}
}
vector<ParseLocationRec> parse_location_string(const string& s)
{
vector<ParseLocationRec> result;
result.push_back(ParseLocationRec());
ParseLocationRec& plr = result[0];
const char* full_filename = 0;
// If plrFileAndLine, set this to the filename you want; for
// memory management reasons, the real filename is set when looking
// up the line number to find the corresponding statement.
const char* loc_filename = 0;
if ( sscanf(s.c_str(), "%d", &plr.line) )
{ // just a line number (implicitly referring to the current file)
loc_filename = g_debugger_state.last_loc.filename;
plr.type = plrFileAndLine;
}
else
{
string::size_type pos_colon = s.find(':');
string::size_type pos_dblcolon = s.find("::");
if ( pos_colon == string::npos || pos_dblcolon != string::npos )
parse_function_name(result, plr, s);
else
{ // file:line
string filename = s.substr(0, pos_colon);
string line_string = s.substr(pos_colon + 1, s.length() - pos_colon);
if ( ! sscanf(line_string.c_str(), "%d", &plr.line) )
plr.type = plrUnknown;
FILE* throwaway = search_for_file(filename.c_str(), "bro",
&full_filename, true, 0);
if ( ! throwaway )
{
debug_msg("No such policy file: %s.\n", filename.c_str());
plr.type = plrUnknown;
return result;
}
fclose(throwaway);
loc_filename = full_filename;
plr.type = plrFileAndLine;
}
}
if ( plr.type == plrFileAndLine )
{
Filemap* map = g_dbgfilemaps.Lookup(loc_filename);
if ( ! map )
reporter->InternalError("Policy file %s should have been loaded\n",
loc_filename);
if ( plr.line > how_many_lines_in(loc_filename) )
{
debug_msg("No line %d in %s.\n", plr.line, loc_filename);
delete [] full_filename;
plr.type = plrUnknown;
return result;
}
StmtLocMapping* hit = 0;
loop_over_queue(*map, i)
{
StmtLocMapping* entry = (*map)[i];
plr.filename = (*map)[i]->Loc().filename;
if ( entry->Loc().first_line > plr.line )
break;
if ( plr.line >= entry->Loc().first_line &&
plr.line <= entry->Loc().last_line )
{
hit = (*map)[i];
break;
}
}
if ( hit )
plr.stmt = hit->Statement();
else
plr.stmt = 0;
}
delete [] full_filename;
return result;
}
// Interactive debugging console.
static int dbg_dispatch_cmd(DebugCmd cmd_code, const vector<string>& args);
#ifdef HAVE_READLINE
void using_history(void);
static bool init_readline()
{
// ### Set up custom completion.
rl_outstream = stderr;
using_history();
return false;
}
#endif
void break_signal(int)
{
g_debugger_state.BreakBeforeNextStmt(true);
g_debugger_state.BreakFromSignal(true);
}
int dbg_init_debugger(const char* cmdfile)
{
if ( ! g_policy_debug )
return 0; // probably shouldn't have been called
init_global_dbg_constants();
// Hit the debugger before running anything.
g_debugger_state.BreakBeforeNextStmt(true);
if ( cmdfile )
// ### Implement this
debug_msg("Command files not supported. Using interactive mode.\n");
// ### if ( interactive ) (i.e., not reading cmds from a file)
#ifdef HAVE_READLINE
init_readline();
#endif
signal(SIGINT, &break_signal);
signal(SIGTERM, break_signal);
return 1;
}
int dbg_shutdown_debugger()
{
// ### TODO: Remove signal handlers
return 1;
}
// Umesh: I stole this code from libedit; I modified it here to use
// <string>s to avoid memory management problems. The main command is returned
// by the operation argument; the additional arguments are put in the
// supplied vector.
//
// Parse the string into individual tokens, similarily to how shell
// would do it.
void tokenize(const char* cstr, string& operation, vector<string>& arguments)
{
int num_tokens = 0;
char delim = '\0';
const string str(cstr);
for ( int i = 0; i < (signed int) str.length(); ++i )
{
while ( isspace((unsigned char) str[i]) )
++i;
int start = i;
for ( ; str[i]; ++i )
{
if ( str[i] == '\\' )
{
if ( i < (signed int) str.length() )
++i;
}
else if ( ! delim && (str[i] == '\'' || str[i] == '"') )
delim = str[i];
else if ( delim && str[i] == delim )
{
delim = '\0';
++i;
break;
}
else if ( ! delim && isspace(str[i]) )
break;
}
size_t len = i - start;
if ( ! num_tokens )
operation = string(str, start, len);
else
arguments.push_back(string(str, start, len));
++num_tokens;
}
}
// Given a command string, parse it and send the command to be dispatched.
int dbg_execute_command(const char* cmd)
{
bool matched_history = false;
if ( ! cmd )
return 0;
if ( streq(cmd, "") ) // do the GDB command completion
{
#ifdef HAVE_READLINE
int i;
for ( i = history_length; i >= 1; --i )
{
HIST_ENTRY* entry = history_get(i);
if ( ! entry )
return 0;
const DebugCmdInfo* info =
(const DebugCmdInfo*) entry->data;
if ( info && info->Repeatable() )
{
cmd = entry->line;
matched_history = true;
break;
}
}
#endif
if ( ! matched_history )
return 0;
}
char* localcmd = copy_string(cmd);
string opstring;
vector<string> arguments;
tokenize(localcmd, opstring, arguments);
delete [] localcmd;
// Make sure we know this op name.
const char* matching_cmds[num_debug_cmds()];
int num_matches = find_all_matching_cmds(opstring, matching_cmds);
if ( ! num_matches )
{
debug_msg("No Matching command for '%s'.\n", opstring.c_str());
return 0;
}
if ( num_matches > 1 )
{
debug_msg("Ambiguous command; could be\n");
for ( int i = 0; i < num_debug_cmds(); ++i )
if ( matching_cmds[i] )
debug_msg("\t%s\n", matching_cmds[i]);
return 0;
}
// Matched exactly one command: find out which one.
DebugCmd cmd_code = dcInvalid;
for ( int i = 0; i < num_debug_cmds(); ++i )
if ( matching_cmds[i] )
{
cmd_code = (DebugCmd) i;
break;
}
#ifdef HAVE_READLINE
// Insert command into history.
if ( ! matched_history && cmd && *cmd )
{
/* The prototype for add_history(), at least under MacOS,
* has it taking a char* rather than a const char*.
* But documentation at
* http://tiswww.case.edu/php/chet/readline/history.html
* suggests that it's safe to assume it's really const char*.
*/
add_history((char *) cmd);
HISTORY_STATE* state = history_get_history_state();
state->entries[state->length-1]->data = (histdata_t *) get_debug_cmd_info(cmd_code);
}
#endif
if ( int(cmd_code) >= num_debug_cmds() )
reporter->InternalError("Assertion failed: int(cmd_code) < num_debug_cmds()");
// Dispatch to the op-specific handler (with args).
int retcode = dbg_dispatch_cmd(cmd_code, arguments);
if ( retcode < 0 )
return retcode;
const DebugCmdInfo* info = get_debug_cmd_info(cmd_code);
if ( ! info )
reporter->InternalError("Assertion failed: info");
if ( ! info )
return -2; // ### yuck, why -2?
return info->ResumeExecution();
}
// Call the appropriate function for the command.
static int dbg_dispatch_cmd(DebugCmd cmd_code, const vector<string>& args)
{
switch ( cmd_code ) {
case dcHelp:
dbg_cmd_help(cmd_code, args);
break;
case dcQuit:
debug_msg("Program Terminating\n");
exit(0);
case dcNext:
g_frame_stack.back()->BreakBeforeNextStmt(true);
step_or_next_pending = true;
last_frame = g_frame_stack.back();
break;
case dcStep:
g_debugger_state.BreakBeforeNextStmt(true);
step_or_next_pending = true;
last_frame = g_frame_stack.back();
break;
case dcContinue:
g_debugger_state.BreakBeforeNextStmt(false);
debug_msg("Continuing.\n");
break;
case dcFinish:
g_frame_stack.back()->BreakOnReturn(true);
g_debugger_state.BreakBeforeNextStmt(false);
break;
case dcBreak:
dbg_cmd_break(cmd_code, args);
break;
case dcBreakCondition:
dbg_cmd_break_condition(cmd_code, args);
break;
case dcDeleteBreak:
case dcClearBreak:
case dcDisableBreak:
case dcEnableBreak:
case dcIgnoreBreak:
dbg_cmd_break_set_state(cmd_code, args);
break;
case dcPrint:
dbg_cmd_print(cmd_code, args);
break;
case dcBacktrace:
return dbg_cmd_backtrace(cmd_code, args);
case dcFrame:
case dcUp:
case dcDown:
return dbg_cmd_frame(cmd_code, args);
case dcInfo:
return dbg_cmd_info(cmd_code, args);
case dcList:
return dbg_cmd_list(cmd_code, args);
case dcDisplay:
case dcUndisplay:
debug_msg("Command not yet implemented.\n");
break;
case dcTrace:
return dbg_cmd_trace(cmd_code, args);
default:
debug_msg("INTERNAL ERROR: "
"Got an unknown debugger command in DbgDispatchCmd: %d\n",
cmd_code);
return 0;
}
return 0;
}
static char* get_prompt(bool reset_counter = false)
{
static char prompt[512];
static int counter = 0;
if ( reset_counter )
counter = 0;
safe_snprintf(prompt, sizeof(prompt), "(Bro [%d]) ", counter++);
return prompt;
}
string get_context_description(const Stmt* stmt, const Frame* frame)
{
ODesc d;
const BroFunc* func = frame->GetFunction();
if ( func )
func->DescribeDebug(&d, frame->GetFuncArgs());
else
d.Add("<unknown function>", 0);
Location loc;
if ( stmt )
loc = *stmt->GetLocationInfo();
else
{
loc.filename = copy_string("<no filename>");
loc.last_line = 0;
}
size_t buf_size = strlen(d.Description()) + strlen(loc.filename) + 1024;
char* buf = new char[buf_size];
safe_snprintf(buf, buf_size, "In %s at %s:%d",
d.Description(), loc.filename, loc.last_line);
string retval(buf);
delete [] buf;
return retval;
}
int dbg_handle_debug_input()
{
static char* input_line = 0;
int status = 0;
if ( g_debugger_state.BreakFromSignal() )
{
debug_msg("Program received signal SIGINT: entering debugger\n");
g_debugger_state.BreakFromSignal(false);
}
Frame* curr_frame = g_frame_stack.back();
const BroFunc* func = curr_frame->GetFunction();
if ( func )
current_module = func->GetID()->ModuleName();
else
current_module = GLOBAL_MODULE_NAME;
const Stmt* stmt = curr_frame->GetNextStmt();
if ( ! stmt )
reporter->InternalError("Assertion failed: stmt != 0");
const Location loc = *stmt->GetLocationInfo();
if ( ! step_or_next_pending || g_frame_stack.back() != last_frame )
{
string context =
get_context_description(stmt, g_frame_stack.back());
debug_msg("%s\n", context.c_str());
}
step_or_next_pending = false;
PrintLines(loc.filename, loc.first_line,
loc.last_line - loc.first_line + 1, true);
g_debugger_state.last_loc = loc;
do
{
// readline returns a pointer to a buffer it allocates; it's
// freed at the bottom.
#ifdef HAVE_READLINE
input_line = readline(get_prompt());
#else
printf ("%s", get_prompt());
// readline uses malloc, and we want to be consistent
// with it.
input_line = (char*) safe_malloc(1024);
input_line[1023] = 0;
// ### Maybe it's not always stdin.
input_line = fgets(input_line, 1023, stdin);
#endif
// ### Maybe not stdin; maybe do better cleanup.
if ( feof(stdin) )
exit(0);
status = dbg_execute_command(input_line);
if ( input_line )
{
free(input_line); // this was malloc'ed
input_line = 0;
}
else
exit(0);
}
while ( status == 0 );
// Clear out some state. ### Is there a better place?
g_debugger_state.curr_frame_idx = 0;
g_debugger_state.already_did_list = false;
signal(SIGINT, &break_signal);
signal(SIGTERM, &break_signal);
return 0;
}
// Return true to continue execution, false to abort.
bool pre_execute_stmt(Stmt* stmt, Frame* f)
{
if ( ! g_policy_debug ||
stmt->Tag() == STMT_LIST || stmt->Tag() == STMT_NULL )
return true;
if ( g_trace_state.DoTrace() )
{
ODesc d;
stmt->Describe(&d);
const char* desc = d.Description();
const char* s = strchr(desc, '\n');
int len;
if ( s )
len = s - desc;
else
len = strlen(desc);
g_trace_state.LogTrace("%*s\n", len, desc);
}
bool should_break = false;
if ( g_debugger_state.BreakBeforeNextStmt() ||
f->BreakBeforeNextStmt() )
{
if ( g_debugger_state.BreakBeforeNextStmt() )
g_debugger_state.BreakBeforeNextStmt(false);
if ( f->BreakBeforeNextStmt() )
f->BreakBeforeNextStmt(false);
should_break = true;
}
if ( stmt->BPCount() )
{
pair<BPMapType::iterator, BPMapType::iterator> p;
p = g_debugger_state.breakpoint_map.equal_range(stmt);
if ( p.first == p.second )
reporter->InternalError("Breakpoint count nonzero, but no matching breakpoints");
for ( BPMapType::iterator i = p.first; i != p.second; ++i )
{
int break_code = i->second->ShouldBreak(stmt);
if ( break_code == 2 ) // ### 2?
{
i->second->SetEnable(false);
delete i->second;
}
should_break = should_break || break_code;
}
}
if ( should_break )
dbg_handle_debug_input();
return true;
}
bool post_execute_stmt(Stmt* stmt, Frame* f, Val* result, stmt_flow_type* flow)
{
// Handle the case where someone issues a "next" debugger command,
// but we're at a return statement, so the next statement is in
// some other function.
if ( *flow == FLOW_RETURN && f->BreakBeforeNextStmt() )
g_debugger_state.BreakBeforeNextStmt(true);
// Handle "finish" commands.
if ( *flow == FLOW_RETURN && f->BreakOnReturn() )
{
if ( result )
{
ODesc d;
result->Describe(&d);
debug_msg("Return Value: '%s'\n", d.Description());
}
else
debug_msg("Return Value: <none>\n");
g_debugger_state.BreakBeforeNextStmt(true);
f->BreakOnReturn(false);
}
return true;
}
// Evaluates the given expression in the context of the currently selected
// frame. Returns the resulting value, or nil if none (or there was an error).
Expr* g_curr_debug_expr = 0;
const char* g_curr_debug_error = 0;
bool in_debug = false;
// ### fix this hardwired access to external variables etc.
struct yy_buffer_state;
typedef struct yy_buffer_state* YY_BUFFER_STATE;
YY_BUFFER_STATE bro_scan_string(const char*);
extern YYLTYPE yylloc; // holds start line and column of token
extern int line_number;
extern const char* filename;
Val* dbg_eval_expr(const char* expr)
{
// Push the current frame's associated scope.
// Note: g_debugger_state.curr_frame_idx is the user-visible number,
// while the array index goes in the opposite direction
int frame_idx =
(g_frame_stack.size() - 1) - g_debugger_state.curr_frame_idx;
if ( ! (frame_idx >= 0 && (unsigned) frame_idx < g_frame_stack.size()) )
reporter->InternalError("Assertion failed: frame_idx >= 0 && (unsigned) frame_idx < g_frame_stack.size()");
Frame* frame = g_frame_stack[frame_idx];
if ( ! (frame) )
reporter->InternalError("Assertion failed: frame");
const BroFunc* func = frame->GetFunction();
if ( func )
push_existing_scope(func->GetScope());
// ### Possibly push a debugger-local scope?
// Set up the lexer to read from the string.
string parse_string = string("@DEBUG ") + expr;
bro_scan_string(parse_string.c_str());
// Fix filename and line number for the lexer/parser, which record it.
filename = "<interactive>";
line_number = 1;
yylloc.filename = filename;
yylloc.first_line = yylloc.last_line = line_number = 1;
// Parse the thing into an expr.
Val* result = 0;
if ( yyparse() )
{
if ( g_curr_debug_error )
debug_msg("Parsing expression '%s' failed: %s\n", expr, g_curr_debug_error);
else
debug_msg("Parsing expression '%s' failed\n", expr);
if ( g_curr_debug_expr )
{
delete g_curr_debug_expr;
g_curr_debug_expr = 0;
}
}
else
result = g_curr_debug_expr->Eval(frame);
if ( func )
pop_scope();
delete g_curr_debug_expr;
g_curr_debug_expr = 0;
delete [] g_curr_debug_error;
g_curr_debug_error = 0;
in_debug = false;
return result;
}