diff --git a/sys/conf/files b/sys/conf/files index cd5b1538f51d..2ceedea4a2e4 100644 --- a/sys/conf/files +++ b/sys/conf/files @@ -361,6 +361,7 @@ ddb/db_output.c optional ddb ddb/db_print.c optional ddb ddb/db_ps.c optional ddb ddb/db_run.c optional ddb +ddb/db_script.c optional ddb ddb/db_sym.c optional ddb ddb/db_thread.c optional ddb ddb/db_variables.c optional ddb diff --git a/sys/ddb/db_command.c b/sys/ddb/db_command.c index c5c820c63e94..c1a062b3c706 100644 --- a/sys/ddb/db_command.c +++ b/sys/ddb/db_command.c @@ -141,6 +141,10 @@ static struct command db_commands[] = { { "kill", db_kill, CS_OWN, 0 }, { "watchdog", db_watchdog, 0, 0 }, { "thread", db_set_thread, CS_OWN, 0 }, + { "run", db_run_cmd, CS_OWN, 0 }, + { "script", db_script_cmd, CS_OWN, 0 }, + { "scripts", db_scripts_cmd, 0, 0 }, + { "unscript", db_unscript_cmd, CS_OWN, 0 }, { "capture", db_capture_cmd, CS_OWN, 0 }, { (char *)0, } }; @@ -187,7 +191,7 @@ static void db_cmd_list(struct command_table *table); static int db_cmd_search(char *name, struct command_table *table, struct command **cmdp); static void db_command(struct command **last_cmdp, - struct command_table *cmd_table); + struct command_table *cmd_table, int dopager); /* * Helper function to match a single command. @@ -284,9 +288,10 @@ db_cmd_list(table) } static void -db_command(last_cmdp, cmd_table) +db_command(last_cmdp, cmd_table, dopager) struct command **last_cmdp; /* IN_OUT */ struct command_table *cmd_table; + int dopager; { struct command *cmd; int t; @@ -398,9 +403,13 @@ db_command(last_cmdp, cmd_table) /* * Execute the command. */ - db_enable_pager(); + if (dopager) + db_enable_pager(); + else + db_disable_pager(); (*cmd->fcn)(addr, have_addr, count, modif); - db_disable_pager(); + if (dopager) + db_disable_pager(); if (cmd->flag & CS_SET_DOT) { /* @@ -451,10 +460,27 @@ db_command_loop() db_printf("db> "); (void) db_read_line(); - db_command(&db_last_command, &db_command_table); + db_command(&db_last_command, &db_command_table, /* dopager */ 1); } } +/* + * Execute a command on behalf of a script. The caller is responsible for + * making sure that the command string is < DB_MAXLINE or it will be + * truncated. + * + * XXXRW: Runs by injecting faked input into DDB input stream; it would be + * nicer to use an alternative approach that didn't mess with the previous + * command buffer. + */ +void +db_command_script(const char *command) +{ + db_prev = db_next = db_dot; + db_inject_line(command); + db_command(&db_last_command, &db_command_table, /* dopager */ 0); +} + void db_error(s) const char *s; diff --git a/sys/ddb/db_command.h b/sys/ddb/db_command.h index 1825b0e3c6fc..db9f495a8082 100644 --- a/sys/ddb/db_command.h +++ b/sys/ddb/db_command.h @@ -38,6 +38,7 @@ */ void db_command_loop(void); +void db_command_script(const char *command); extern db_addr_t db_dot; /* current location */ extern db_addr_t db_last_addr; /* last explicit address typed */ diff --git a/sys/ddb/db_lex.c b/sys/ddb/db_lex.c index a04103b00deb..1c8151e5371c 100644 --- a/sys/ddb/db_lex.c +++ b/sys/ddb/db_lex.c @@ -35,11 +35,12 @@ __FBSDID("$FreeBSD$"); #include +#include #include #include -static char db_line[120]; +static char db_line[DB_MAXLINE]; static char * db_lp, *db_endlp; static int db_lex(void); @@ -60,6 +61,32 @@ db_read_line() return (i); } +/* + * Simulate a line of input into DDB. + */ +void +db_inject_line(const char *command) +{ + + strlcpy(db_line, command, sizeof(db_line)); + db_lp = db_line; + db_endlp = db_lp + strlen(command); +} + +/* + * In rare cases, we may want to pull the remainder of the line input + * verbatim, rather than lexing it. For example, when assigning literal + * values associated with scripts. In that case, return a static pointer to + * the current location in the input buffer. The caller must be aware that + * the contents are not stable if other lex/input calls are made. + */ +char * +db_get_line(void) +{ + + return (db_lp); +} + static void db_flush_line() { @@ -264,6 +291,8 @@ db_lex() return (tDOLLAR); case '!': return (tEXCL); + case ';': + return (tSEMI); case '<': c = db_read_char(); if (c == '<') diff --git a/sys/ddb/db_lex.h b/sys/ddb/db_lex.h index 07955e9dc5cd..8713a27055f1 100644 --- a/sys/ddb/db_lex.h +++ b/sys/ddb/db_lex.h @@ -36,10 +36,12 @@ /* * Lexical analyzer. */ -void db_flush_lex(void); -int db_read_line(void); -int db_read_token(void); -void db_unread_token(int t); +void db_flush_lex(void); +char *db_get_line(void); +void db_inject_line(const char *command); +int db_read_line(void); +int db_read_token(void); +void db_unread_token(int t); extern db_expr_t db_tok_number; #define TOK_STRING_SIZE 120 @@ -66,5 +68,6 @@ extern char db_tok_string[TOK_STRING_SIZE]; #define tSHIFT_L 18 #define tSHIFT_R 19 #define tDOTDOT 20 +#define tSEMI 21 #endif /* !_DDB_DB_LEX_H_ */ diff --git a/sys/ddb/db_main.c b/sys/ddb/db_main.c index 0a9fbf7c8368..e63b4fc0991a 100644 --- a/sys/ddb/db_main.c +++ b/sys/ddb/db_main.c @@ -194,6 +194,7 @@ db_trap(int type, int code) jmp_buf jb; void *prev_jb; boolean_t bkpt, watchpt; + const char *why; /* * Don't handle the trap if the console is unavailable (i.e. it @@ -222,6 +223,8 @@ db_trap(int type, int code) db_printf("Stopped at\t"); db_print_loc_and_inst(db_dot); } + why = kdb_why; + db_script_kdbenter(why != KDB_WHY_UNSET ? why : "unknown"); db_command_loop(); (void)kdb_jmpbuf(prev_jb); } diff --git a/sys/ddb/db_script.c b/sys/ddb/db_script.c new file mode 100644 index 000000000000..407f33d9d737 --- /dev/null +++ b/sys/ddb/db_script.c @@ -0,0 +1,564 @@ +/*- + * Copyright (c) 2007 Robert N. M. Watson + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 THE AUTHOR 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. + * + * $FreeBSD$ + */ + +/*- + * Simple DDB scripting mechanism. Each script consists of a named list of + * DDB commands to execute sequentially. A more sophisticated scripting + * language might be desirable, but would be significantly more complex to + * implement. A more interesting syntax might allow general use of variables + * and extracting of useful values, such as a thread's process identifier, + * for passing into further DDB commands. Certain scripts are run + * automatically at kdb_enter(), if defined, based on how the debugger is + * entered, allowing scripted responses to panics, break signals, etc. + * + * Scripts may be managed from within DDB using the script, scripts, and + * unscript commands. They may also be managed from userspace using ddb(8), + * which operates using a set of sysctls. + * + * TODO: + * - Allow scripts to be defined using tunables so that they can be defined + * before boot and be present in single-user mode without boot scripts + * running. + * - Memory allocation is not possible from within DDB, so we use a set of + * statically allocated buffers to hold defined scripts. However, when + * scripts are being defined from userspace via sysctl, we could in fact be + * using malloc(9) and therefore not impose a static limit, giving greater + * flexibility and avoiding hard-defined buffer limits. + * - When scripts run automatically on entrance to DDB, placing "continue" at + * the end still results in being in the debugger, as we unconditionally + * run db_command_loop() after the script. There should be a way to avoid + * this. + */ + +#include +__FBSDID("$FreeBSD$"); + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +/* + * struct ddb_script describes an individual script. + */ +struct ddb_script { + char ds_scriptname[DB_MAXSCRIPTNAME]; + char ds_script[DB_MAXSCRIPTLEN]; +}; + +/* + * Global list of scripts -- defined scripts have non-empty name fields. + */ +static struct ddb_script db_script_table[DB_MAXSCRIPTS]; + +/* + * While executing a script, we parse it using strsep(), so require a + * temporary buffer that may be used destructively. Since we support weak + * recursion of scripts (one may reference another), we need one buffer for + * each concurrently executing script. + */ +static struct db_recursion_data { + char drd_buffer[DB_MAXSCRIPTLEN]; +} db_recursion_data[DB_MAXSCRIPTRECURSION]; +static int db_recursion = -1; + +/* + * We use a separate static buffer for script validation so that it is safe + * to validate scripts from within a script. This is used only in + * db_script_valid(), which should never be called reentrantly. + */ +static char db_static_buffer[DB_MAXSCRIPTLEN]; + +/* + * Synchronization is not required from within the debugger, as it is + * singe-threaded (although reentrance must be carefully considered). + * However, it is required when interacting with scripts from user space + * processes. Sysctl procedures acquire db_script_mtx before accessing the + * global script data structures. + */ +static struct mtx db_script_mtx; +MTX_SYSINIT(db_script_mtx, &db_script_mtx, "db_script_mtx", MTX_DEF); + +/* + * Some script names have special meaning, such as those executed + * automatically when KDB is entered. + */ +#define DB_SCRIPT_KDBENTER_PREFIX "kdb.enter" /* KDB has entered. */ +#define DB_SCRIPT_KDBENTER_DEFAULT "kdb.enter.default" + +/* + * Find the existing script slot for a named script, if any. + */ +static struct ddb_script * +db_script_lookup(const char *scriptname) +{ + int i; + + for (i = 0; i < DB_MAXSCRIPTS; i++) { + if (strcmp(db_script_table[i].ds_scriptname, scriptname) == + 0) + return (&db_script_table[i]); + } + return (NULL); +} + +/* + * Find a new slot for a script, if available. Does not mark as allocated in + * any way--this must be done by the caller. + */ +static struct ddb_script * +db_script_new(void) +{ + int i; + + for (i = 0; i < DB_MAXSCRIPTS; i++) { + if (strlen(db_script_table[i].ds_scriptname) == 0) + return (&db_script_table[i]); + } + return (NULL); +} + +/* + * Perform very rudimentary validation of a proposed script. It would be + * easy to imagine something more comprehensive. The script string is + * validated in a static buffer. + */ +static int +db_script_valid(const char *scriptname, const char *script) +{ + char *buffer, *command; + + if (strlen(scriptname) == 0) + return (EINVAL); + if (strlen(scriptname) >= DB_MAXSCRIPTNAME) + return (EINVAL); + if (strlen(script) >= DB_MAXSCRIPTLEN) + return (EINVAL); + buffer = db_static_buffer; + strcpy(buffer, script); + while ((command = strsep(&buffer, ";")) != NULL) { + if (strlen(command) >= DB_MAXLINE) + return (EINVAL); + } + return (0); +} + +/* + * Modify an existing script or add a new script with the specified script + * name and contents. If there are no script slots available, an error will + * be returned. + */ +static int +db_script_set(const char *scriptname, const char *script) +{ + struct ddb_script *dsp; + int error; + + error = db_script_valid(scriptname, script); + if (error) + return (error); + dsp = db_script_lookup(scriptname); + if (dsp == NULL) { + dsp = db_script_new(); + if (dsp == NULL) + return (ENOSPC); + strlcpy(dsp->ds_scriptname, scriptname, + sizeof(dsp->ds_scriptname)); + } + strlcpy(dsp->ds_script, script, sizeof(dsp->ds_script)); + return (0); +} + +/* + * Delete an existing script by name, if found. + */ +static int +db_script_unset(const char *scriptname) +{ + struct ddb_script *dsp; + + dsp = db_script_lookup(scriptname); + if (dsp == NULL) + return (ENOENT); + strcpy(dsp->ds_scriptname, ""); + strcpy(dsp->ds_script, ""); + return (0); +} + +/* + * Trim leading/trailing white space in a command so that we don't pass + * carriage returns, etc, into DDB command parser. + */ +static int +db_command_trimmable(char ch) +{ + + switch (ch) { + case ' ': + case '\t': + case '\n': + case '\r': + return (1); + + default: + return (0); + } +} + +static void +db_command_trim(char **commandp) +{ + char *command; + + command = *commandp; + while (db_command_trimmable(*command)) + command++; + while ((strlen(command) > 0) && + db_command_trimmable(command[strlen(command) - 1])) + command[strlen(command) - 1] = 0; + *commandp = command; +} + +/* + * Execute a script, breaking it up into individual commands and passing them + * sequentially into DDB's input processing. Use the KDB jump buffer to + * restore control to the main script loop if things get too wonky when + * processing a command -- i.e., traps, etc. Also, make sure we don't exceed + * practical limits on recursion. + * + * XXXRW: If any individual command is too long, it will be truncated when + * injected into the input at a lower layer. We should validate the script + * before configuring it to avoid this scenario. + */ +static int +db_script_exec(const char *scriptname, int warnifnotfound) +{ + struct db_recursion_data *drd; + struct ddb_script *dsp; + char *buffer, *command; + void *prev_jb; + jmp_buf jb; + + dsp = db_script_lookup(scriptname); + if (dsp == NULL) { + if (warnifnotfound) + db_printf("script '%s' not found\n", scriptname); + return (ENOENT); + } + + if (db_recursion >= DB_MAXSCRIPTRECURSION) { + db_printf("Script stack too deep\n"); + return (E2BIG); + } + db_recursion++; + drd = &db_recursion_data[db_recursion]; + + /* + * Parse script in temporary buffer, since strsep() is destructive. + */ + buffer = drd->drd_buffer; + strcpy(buffer, dsp->ds_script); + while ((command = strsep(&buffer, ";")) != NULL) { + db_printf("db:%d:%s> %s\n", db_recursion, scriptname, + command); + db_command_trim(&command); + prev_jb = kdb_jmpbuf(jb); + if (setjmp(jb) == 0) + db_command_script(command); + else + db_printf("Script command '%s' returned error\n", + command); + kdb_jmpbuf(prev_jb); + } + db_recursion--; + return (0); +} + +/* + * Wrapper for exec path that is called on KDB enter. Map reason for KDB + * enter to a script name, and don't whine if the script doesn't exist. If + * there is no matching script, try the catch-all script. + */ +void +db_script_kdbenter(const char *eventname) +{ + char scriptname[DB_MAXSCRIPTNAME]; + + snprintf(scriptname, sizeof(scriptname), "%s.%s", + DB_SCRIPT_KDBENTER_PREFIX, eventname); + if (db_script_exec(scriptname, 0) == ENOENT) + (void)db_script_exec(DB_SCRIPT_KDBENTER_DEFAULT, 0); +} + +/*- + * DDB commands for scripting, as reached via the DDB user interface: + * + * scripts - lists scripts + * run - run a script + * script - prints script + * script