// // Wasm3 - high performance WebAssembly interpreter written in C. // // Copyright © 2019 Steven Massey, Volodymyr Shymanskyy. // All rights reserved. // #include #include #include #include #include "wasm3.h" #include "m3_api_libc.h" #if defined(d_m3HasWASI) || defined(d_m3HasMetaWASI) || defined(d_m3HasUVWASI) #include "m3_api_wasi.h" #define LINK_WASI #endif #if defined(d_m3HasTracer) #include "m3_api_tracer.h" #endif // TODO: remove #include "m3_env.h" /* * NOTE: Gas metering/limit only applies to pre-instrumented modules. * You can generate a metered version from any wasm file automatically, using * https://github.com/ewasm/wasm-metering */ #define GAS_LIMIT 500000000 #define GAS_FACTOR 10000LL #define MAX_MODULES 16 #define FATAL(msg, ...) { fprintf(stderr, "Error: [Fatal] " msg "\n", ##__VA_ARGS__); goto _onfatal; } static IM3Environment env; static IM3Runtime runtime; static u8* wasm_bins[MAX_MODULES]; static int wasm_bins_qty = 0; #if defined(GAS_LIMIT) static int64_t initial_gas = GAS_FACTOR * GAS_LIMIT; static int64_t current_gas = GAS_FACTOR * GAS_LIMIT; static bool is_gas_metered = false; m3ApiRawFunction(metering_usegas) { m3ApiGetArg (int32_t, gas) current_gas -= gas; if (M3_UNLIKELY(current_gas < 0)) { m3ApiTrap("[trap] Out of gas"); } m3ApiSuccess(); } #endif // GAS_LIMIT M3Result link_all (IM3Module module) { M3Result res; res = m3_LinkSpecTest (module); if (res) return res; res = m3_LinkLibC (module); if (res) return res; #if defined(LINK_WASI) res = m3_LinkWASI (module); if (res) return res; #endif #if defined(d_m3HasTracer) res = m3_LinkTracer (module); if (res) return res; #endif #if defined(GAS_LIMIT) res = m3_LinkRawFunction (module, "metering", "usegas", "v(i)", &metering_usegas); if (!res) { fprintf(stderr, "Warning: Gas is limited to %0.4f\n", (double)(current_gas) / GAS_FACTOR); is_gas_metered = true; } if (res == m3Err_functionLookupFailed) { res = NULL; } #endif return res; } const char* modname_from_fn(const char* fn) { const char* sep = "/\\:*?"; char c; while ((c = *sep++)) { const char* off = strrchr(fn, c) + 1; fn = (fn < off) ? off : fn; } return fn; } M3Result repl_load (const char* fn) { M3Result result = m3Err_none; IM3Module module = NULL; u8* wasm = NULL; u32 fsize = 0; FILE* f = fopen (fn, "rb"); if (!f) { return "cannot open file"; } fseek (f, 0, SEEK_END); fsize = ftell(f); fseek (f, 0, SEEK_SET); if (fsize < 8) { result = "file is too small"; goto on_error; } else if (fsize > 64*1024*1024) { result = "file is too big"; goto on_error; } wasm = (u8*) malloc(fsize); if (!wasm) { result = "cannot allocate memory for wasm binary"; goto on_error; } if (fread (wasm, 1, fsize, f) != fsize) { result = "cannot read file"; goto on_error; } fclose (f); f = NULL; result = m3_ParseModule (env, &module, wasm, fsize); if (result) goto on_error; result = m3_LoadModule (runtime, module); if (result) goto on_error; m3_SetModuleName(module, modname_from_fn(fn)); result = link_all (module); if (result) goto on_error; if (wasm_bins_qty < MAX_MODULES) { wasm_bins[wasm_bins_qty++] = wasm; } return result; on_error: m3_FreeModule(module); if (wasm) free(wasm); if (f) fclose(f); return result; } M3Result repl_load_hex (u32 fsize) { M3Result result = m3Err_none; if (fsize < 8) { return "file is too small"; } else if (fsize > 10*1024*1024) { return "file too big"; } u8* wasm = (u8*) malloc(fsize); if (!wasm) { return "cannot allocate memory for wasm binary"; } { // Load hex data from stdin u32 wasm_idx = 0; char hex[3] = { 0, }; int hex_idx = 0; while (wasm_idx < fsize) { int c = fgetc(stdin); if (!isxdigit(c)) continue; // Skip non-hex chars hex[hex_idx++] = c; if (hex_idx == 2) { int val = strtol(hex, NULL, 16); wasm[wasm_idx++] = val; hex_idx = 0; } } if (!fgets(hex, 3, stdin)) // Consume a newline return "cannot read EOL"; } IM3Module module; result = m3_ParseModule (env, &module, wasm, fsize); if (result) return result; result = m3_LoadModule (runtime, module); if (result) return result; result = link_all (module); return result; } void print_gas_used() { #if defined(GAS_LIMIT) if (is_gas_metered) { fprintf(stderr, "Gas used: %0.4f\n", (double)(initial_gas - current_gas) / GAS_FACTOR); } #endif } void print_backtrace() { IM3BacktraceInfo info = m3_GetBacktrace(runtime); if (!info) { return; } fprintf(stderr, "==== wasm backtrace:"); int frameCount = 0; IM3BacktraceFrame curr = info->frames; while (curr) { fprintf(stderr, "\n %d: 0x%06x - %s!%s", frameCount, curr->moduleOffset, m3_GetModuleName (m3_GetFunctionModule(curr->function)), m3_GetFunctionName (curr->function) ); curr = curr->next; frameCount++; } if (info->lastFrame == M3_BACKTRACE_TRUNCATED) { fprintf(stderr, "\n (truncated)"); } fprintf(stderr, "\n"); } M3Result repl_call (const char* name, int argc, const char* argv[]) { IM3Function func; M3Result result = m3_FindFunction (&func, runtime, name); if (result) return result; if (argc && (!strcmp(name, "main") || !strcmp(name, "_main"))) { return "passing arguments to libc main() not implemented"; } if (!strcmp(name, "_start")) { #if defined(LINK_WASI) // Strip wasm file path if (argc > 0) { argv[0] = modname_from_fn(argv[0]); } m3_wasi_context_t* wasi_ctx = m3_GetWasiContext(); wasi_ctx->argc = argc; wasi_ctx->argv = argv; result = m3_CallArgv(func, 0, NULL); print_gas_used(); if (result == m3Err_trapExit) { exit(wasi_ctx->exit_code); } return result; #else return "WASI not linked"; #endif } int arg_count = m3_GetArgCount(func); int ret_count = m3_GetRetCount(func); if (argc < arg_count) { return "not enough arguments"; } else if (argc > arg_count) { return "too many arguments"; } result = m3_CallArgv (func, argc, argv); print_gas_used(); if (result) return result; static uint64_t valbuff[128]; static const void* valptrs[128]; memset(valbuff, 0, sizeof(valbuff)); for (int i = 0; i < ret_count; i++) { valptrs[i] = &valbuff[i]; } result = m3_GetResults (func, ret_count, valptrs); if (result) return result; if (ret_count <= 0) { fprintf (stderr, "Result: \n"); } for (int i = 0; i < ret_count; i++) { switch (m3_GetRetType(func, i)) { case c_m3Type_i32: fprintf (stderr, "Result: %" PRIi32 "\n", *(i32*)valptrs[i]); break; case c_m3Type_i64: fprintf (stderr, "Result: %" PRIi64 "\n", *(i64*)valptrs[i]); break; # if d_m3HasFloat case c_m3Type_f32: fprintf (stderr, "Result: %" PRIf32 "\n", *(f32*)valptrs[i]); break; case c_m3Type_f64: fprintf (stderr, "Result: %" PRIf64 "\n", *(f64*)valptrs[i]); break; # endif default: return "unknown return type"; } } return result; } // :invoke is used by spec tests, so it treats floats as raw data M3Result repl_invoke (const char* name, int argc, const char* argv[]) { IM3Function func; M3Result result = m3_FindFunction (&func, runtime, name); if (result) return result; int arg_count = m3_GetArgCount(func); int ret_count = m3_GetRetCount(func); if (argc > 128) { return "arguments limit reached"; } else if (argc < arg_count) { return "not enough arguments"; } else if (argc > arg_count) { return "too many arguments"; } static uint64_t valbuff[128]; static const void* valptrs[128]; memset(valbuff, 0, sizeof(valbuff)); memset(valptrs, 0, sizeof(valptrs)); for (int i = 0; i < argc; i++) { u64* s = &valbuff[i]; valptrs[i] = s; switch (m3_GetArgType(func, i)) { case c_m3Type_i32: case c_m3Type_f32: *(u32*)(s) = strtoul(argv[i], NULL, 10); break; case c_m3Type_i64: case c_m3Type_f64: *(u64*)(s) = strtoull(argv[i], NULL, 10); break; default: return "unknown argument type"; } } result = m3_Call (func, argc, valptrs); if (result) return result; // reuse valbuff for return values memset(valbuff, 0, sizeof(valbuff)); for (int i = 0; i < ret_count; i++) { valptrs[i] = &valbuff[i]; } result = m3_GetResults (func, ret_count, valptrs); if (result) return result; fprintf (stderr, "Result: "); if (ret_count <= 0) { fprintf (stderr, ""); } for (int i = 0; i < ret_count; i++) { switch (m3_GetRetType(func, i)) { case c_m3Type_i32: fprintf (stderr, "%" PRIu32 ":i32", *(u32*)valptrs[i]); break; case c_m3Type_f32: fprintf (stderr, "%" PRIu32 ":f32", *(u32*)valptrs[i]); break; case c_m3Type_i64: fprintf (stderr, "%" PRIu64 ":i64", *(u64*)valptrs[i]); break; case c_m3Type_f64: fprintf (stderr, "%" PRIu64 ":f64", *(u64*)valptrs[i]); break; default: return "unknown return type"; } if (i != ret_count-1) { fprintf (stderr, ", "); } } fprintf (stderr, "\n"); return result; } M3Result repl_global_get (const char* name) { IM3Global g = m3_FindGlobal(runtime->modules, name); M3TaggedValue tagged; M3Result err = m3_GetGlobal (g, &tagged); if (err) return err; switch (tagged.type) { case c_m3Type_i32: fprintf (stderr, "%" PRIu32 ":i32\n", tagged.value.i32); break; case c_m3Type_i64: fprintf (stderr, "%" PRIu64 ":i64\n", tagged.value.i64); break; case c_m3Type_f32: fprintf (stderr, "%" PRIf32 ":f32\n", tagged.value.f32); break; case c_m3Type_f64: fprintf (stderr, "%" PRIf64 ":f64\n", tagged.value.f64); break; default: return m3Err_invalidTypeId; } return m3Err_none; } M3Result repl_global_set (const char* name, const char* value) { IM3Global g = m3_FindGlobal(runtime->modules, name); M3TaggedValue tagged = { .type = m3_GetGlobalType(g) }; switch (tagged.type) { case c_m3Type_i32: tagged.value.i32 = strtoul(value, NULL, 10); break; case c_m3Type_i64: tagged.value.i64 = strtoull(value, NULL, 10); break; case c_m3Type_f32: tagged.value.f32 = strtod(value, NULL); break; case c_m3Type_f64: tagged.value.f64 = strtod(value, NULL); break; default: return m3Err_invalidTypeId; } return m3_SetGlobal (g, &tagged); } M3Result repl_compile () { return m3_CompileModule(runtime->modules); } M3Result repl_dump () { uint32_t len; uint8_t* mem = m3_GetMemory(runtime, &len, 0); if (mem) { FILE* f = fopen ("wasm3_dump.bin", "wb"); if (!f) { return "cannot open file"; } if (fwrite (mem, 1, len, f) != len) { return "cannot write file"; } fclose (f); } return m3Err_none; } void repl_free () { if (runtime) { m3_FreeRuntime (runtime); runtime = NULL; } for (int i = 0; i < wasm_bins_qty; i++) { free (wasm_bins[i]); wasm_bins[i] = NULL; } } M3Result repl_init (unsigned stack) { repl_free(); runtime = m3_NewRuntime (env, stack, NULL); if (runtime == NULL) { return "m3_NewRuntime failed"; } return m3Err_none; } static void unescape(char* buff) { char* outp = buff; while (*buff) { if (*buff == '\\') { switch (*(buff+1)) { case '0': *outp++ = '\0'; break; case 'b': *outp++ = '\b'; break; case 'n': *outp++ = '\n'; break; case 'r': *outp++ = '\r'; break; case 't': *outp++ = '\t'; break; case 'x': { char hex[3] = { *(buff+2), *(buff+3), '\0' }; *outp = strtol(hex, NULL, 16); buff += 2; outp += 1; break; } // Otherwise just pass the letter // Also handles '\\' default: *outp++ = *(buff+1); break; } buff += 2; } else { *outp++ = *buff++; } } *outp = '\0'; } static int split_argv(char *str, char** argv) { int result = 0; char* curr = str; int len = 0; for (int i = 0; str[i] != '\0'; i++) { if (strchr(" \n\r\t", str[i])) { if (len) { // Found space after non-space str[i] = '\0'; //unescape(curr); // TODO: breaks windows build? argv[result++] = curr; len = 0; } } else { if (!len) { // Found non-space after space curr = &str[i]; } len++; } } argv[result] = NULL; return result; } void print_version() { const char* wasm3_env = getenv("WASM3"); const char* wasm3_arch = getenv("WASM3_ARCH"); printf("Wasm3 v" M3_VERSION "%s on %s\n", (wasm3_arch || wasm3_env) ? " self-hosting" : "", wasm3_arch ? wasm3_arch : M3_ARCH); printf("Build: " __DATE__ " " __TIME__ ", " M3_COMPILER_VER "\n"); } void print_usage() { puts("Usage:"); puts(" wasm3 [options] [args...]"); puts(" wasm3 --repl [file]"); puts("Options:"); puts(" --func function to run default: _start"); puts(" --stack-size stack size in bytes default: 64KB"); puts(" --compile disable lazy compilation"); puts(" --dump-on-trap dump wasm memory"); puts(" --gas-limit set gas limit"); } #define ARGV_SHIFT() { i_argc--; i_argv++; } #define ARGV_SET(x) { if (i_argc > 0) { x = i_argv[0]; ARGV_SHIFT(); } } int main (int i_argc, const char* i_argv[]) { M3Result result = m3Err_none; env = m3_NewEnvironment (); runtime = NULL; bool argRepl = false; bool argDumpOnTrap = false; bool argCompile = false; const char* argFile = NULL; const char* argFunc = "_start"; unsigned argStackSize = 64*1024; // m3_PrintM3Info (); ARGV_SHIFT(); // Skip executable name while (i_argc > 0) { const char* arg = i_argv[0]; if (arg[0] != '-') break; ARGV_SHIFT(); if (!strcmp("--help", arg) or !strcmp("-h", arg)) { print_usage(); return 0; } else if (!strcmp("--version", arg)) { print_version(); return 0; } else if (!strcmp("--repl", arg)) { argRepl = true; } else if (!strcmp("--dump-on-trap", arg)) { argDumpOnTrap = true; } else if (!strcmp("--compile", arg)) { argCompile = true; } else if (!strcmp("--stack-size", arg)) { const char* tmp = "65536"; ARGV_SET(tmp); argStackSize = atol(tmp); } else if (!strcmp("--gas-limit", arg)) { const char* tmp = "0"; ARGV_SET(tmp); initial_gas = current_gas = GAS_FACTOR * atol(tmp); } else if (!strcmp("--dir", arg)) { const char* argDir; ARGV_SET(argDir); (void)argDir; } else if (!strcmp("--func", arg) or !strcmp("-f", arg)) { ARGV_SET(argFunc); } } if ((argRepl and (i_argc > 1)) or // repl supports 0 or 1 args (not argRepl and (i_argc < 1)) // normal expects at least 1 ) { print_usage(); return 1; } ARGV_SET(argFile); result = repl_init(argStackSize); if (result) FATAL("repl_init: %s", result); if (argFile) { result = repl_load(argFile); if (result) FATAL("repl_load: %s", result); if (argCompile) { repl_compile(); } if (argFunc and not argRepl) { if (!strcmp(argFunc, "_start")) { // When passing args to WASI, include wasm filename as argv[0] result = repl_call(argFunc, i_argc+1, i_argv-1); } else { result = repl_call(argFunc, i_argc, i_argv); } if (result) { if (argDumpOnTrap) { repl_dump(); } print_backtrace(); goto _onfatal; } } } while (argRepl) { char cmd_buff[2048] = { 0, }; char* argv[32] = { 0, }; fprintf(stdout, "wasm3> "); fflush(stdout); if (!fgets(cmd_buff, sizeof(cmd_buff), stdin)) { return 0; } int argc = split_argv(cmd_buff, argv); if (argc <= 0) { continue; } result = m3Err_none; if (!strcmp(":init", argv[0])) { result = repl_init(argStackSize); } else if (!strcmp(":version", argv[0])) { print_version(); } else if (!strcmp(":exit", argv[0])) { repl_free(); return 0; } else if (!strcmp(":load", argv[0])) { // :load result = repl_load(argv[1]); } else if (!strcmp(":load-hex", argv[0])) { // :load-hex \n result = repl_load_hex(atol(argv[1])); } else if (!strcmp(":get-global", argv[0])) { result = repl_global_get(argv[1]); } else if (!strcmp(":set-global", argv[0])) { result = repl_global_set(argv[1], argv[2]); } else if (!strcmp(":dump", argv[0])) { result = repl_dump(); } else if (!strcmp(":compile", argv[0])) { result = repl_compile(); } else if (!strcmp(":invoke", argv[0])) { unescape(argv[1]); result = repl_invoke(argv[1], argc-2, (const char**)(argv+2)); } else if (argv[0][0] == ':') { result = "no such command"; } else { unescape(argv[0]); result = repl_call(argv[0], argc-1, (const char**)(argv+1)); if (result) { print_backtrace(); } } if (result == m3Err_trapExit) { //TODO: fprintf(stderr, M3_ARCH "-wasi: exit(%d)\n", runtime->exit_code); } else if (result) { fprintf (stderr, "Error: %s", result); M3ErrorInfo info; m3_GetErrorInfo (runtime, &info); fprintf (stderr, " (%s)\n", info.message); } } _onfatal: if (result) { fprintf (stderr, "Error: %s", result); if (runtime) { M3ErrorInfo info; m3_GetErrorInfo (runtime, &info); if (strlen(info.message)) { fprintf (stderr, " (%s)", info.message); } } fprintf (stderr, "\n"); } m3_FreeRuntime (runtime); m3_FreeEnvironment (env); return result ? 1 : 0; }