/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * * Copyright 2016 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "wasm/WasmDebug.h" #include "debugger/Debugger.h" #include "ds/Sort.h" #include "jit/MacroAssembler.h" #include "js/ColumnNumber.h" // JS::WasmFunctionIndex #include "wasm/WasmJS.h" #include "wasm/WasmStubs.h" #include "wasm/WasmValidate.h" #include "gc/GCContext-inl.h" #include "wasm/WasmInstance-inl.h" using namespace js; using namespace js::jit; using namespace js::wasm; DebugState::DebugState(const Code& code, const Module& module) : code_(&code), module_(&module), enterFrameTrapsEnabled_(false), enterAndLeaveFrameTrapsCounter_(0) { MOZ_RELEASE_ASSERT(code.debugEnabled()); } void DebugState::trace(JSTracer* trc) { for (auto iter = breakpointSites_.iter(); !iter.done(); iter.next()) { WasmBreakpointSite* site = iter.get().value(); site->trace(trc); } } void DebugState::finalize(JS::GCContext* gcx) { for (auto iter = breakpointSites_.iter(); !iter.done(); iter.next()) { WasmBreakpointSite* site = iter.get().value(); site->delete_(gcx); } } static bool SlowCallSiteSearchByOffset(const CodeBlock& code, uint32_t offset, CallSite* callSite) { for (uint32_t callSiteIndex = 0; callSiteIndex < code.callSites.length(); callSiteIndex++) { if (code.callSites.kind(callSiteIndex) == CallSiteKind::Breakpoint && code.callSites.bytecodeOffset(callSiteIndex).offset() == offset) { *callSite = code.callSites.get(callSiteIndex, code.inliningContext); return true; } } return false; } bool DebugState::getLineOffsets(size_t lineno, Vector* offsets) { CallSite callSite; return !SlowCallSiteSearchByOffset(debugCode(), lineno, &callSite) || offsets->append(lineno); } bool DebugState::getAllColumnOffsets(Vector* offsets) { for (uint32_t callSiteIndex = 0; callSiteIndex < debugCode().callSites.length(); callSiteIndex++) { if (debugCode().callSites.kind(callSiteIndex) != CallSiteKind::Breakpoint) { continue; } uint32_t offset = debugCode().callSites.bytecodeOffset(callSiteIndex).offset(); if (!offsets->emplaceBack( offset, JS::WasmFunctionIndex::DefaultBinarySourceColumnNumberOneOrigin, offset)) { return false; } } return true; } bool DebugState::getOffsetLocation(uint32_t offset, uint32_t* lineno, JS::LimitedColumnNumberOneOrigin* column) { CallSite callSite; if (!SlowCallSiteSearchByOffset(debugCode(), offset, &callSite)) { return false; } *lineno = offset; *column = JS::LimitedColumnNumberOneOrigin( JS::WasmFunctionIndex::DefaultBinarySourceColumnNumberOneOrigin); return true; } bool DebugState::stepModeEnabled(uint32_t funcIndex) const { return stepperCounters_.lookup(funcIndex).found(); } bool DebugState::incrementStepperCount(JSContext* cx, Instance* instance, uint32_t funcIndex) { StepperCounters::AddPtr p = stepperCounters_.lookupForAdd(funcIndex); if (p) { MOZ_ASSERT(p->value() > 0); p->value()++; return true; } if (!stepperCounters_.add(p, funcIndex, 1)) { ReportOutOfMemory(cx); return false; } enableDebuggingForFunction(instance, funcIndex); enableDebugTrapping(instance); return true; } void DebugState::decrementStepperCount(JS::GCContext* gcx, Instance* instance, uint32_t funcIndex) { const CodeRange& codeRange = debugCode().codeRanges[funcToCodeRangeIndex(funcIndex)]; MOZ_ASSERT(codeRange.isFunction()); MOZ_ASSERT(!stepperCounters_.empty()); StepperCounters::Ptr p = stepperCounters_.lookup(funcIndex); MOZ_ASSERT(p); if (--p->value()) { return; } stepperCounters_.remove(p); bool anyStepping = !stepperCounters_.empty(); bool anyBreakpoints = !breakpointSites_.empty(); bool anyEnterAndLeave = enterAndLeaveFrameTrapsCounter_ > 0; bool keepDebugging = false; for (uint32_t callSiteIndex = 0; callSiteIndex < debugCode().callSites.length(); callSiteIndex++) { if (debugCode().callSites.kind(callSiteIndex) != CallSiteKind::Breakpoint) { continue; } uint32_t offset = debugCode().callSites.returnAddressOffset(callSiteIndex); if (codeRange.begin() <= offset && offset <= codeRange.end()) { keepDebugging = keepDebugging || breakpointSites_.has(offset); } } if (!keepDebugging && !anyEnterAndLeave) { disableDebuggingForFunction(instance, funcIndex); if (!anyStepping && !anyBreakpoints) { disableDebugTrapping(instance); } } } bool DebugState::hasBreakpointTrapAtOffset(uint32_t offset) { CallSite callSite; return SlowCallSiteSearchByOffset(debugCode(), offset, &callSite); } void DebugState::toggleBreakpointTrap(JSRuntime* rt, Instance* instance, uint32_t offset, bool enabled) { CallSite callSite; if (!SlowCallSiteSearchByOffset(debugCode(), offset, &callSite)) { return; } size_t debugTrapOffset = callSite.returnAddressOffset(); const CodeRange* codeRange = code_->lookupFuncRange(debugCode().base() + debugTrapOffset); MOZ_ASSERT(codeRange); uint32_t funcIndex = codeRange->funcIndex(); if (stepperCounters_.lookup(funcIndex)) { return; // no need to toggle when step mode is enabled } bool anyEnterAndLeave = enterAndLeaveFrameTrapsCounter_ > 0; bool anyStepping = !stepperCounters_.empty(); bool anyBreakpoints = !breakpointSites_.empty(); if (enabled) { enableDebuggingForFunction(instance, funcIndex); enableDebugTrapping(instance); } else if (!anyEnterAndLeave) { disableDebuggingForFunction(instance, funcIndex); if (!anyStepping && !anyBreakpoints) { disableDebugTrapping(instance); } } } WasmBreakpointSite* DebugState::getBreakpointSite(uint32_t offset) const { WasmBreakpointSiteMap::Ptr p = breakpointSites_.lookup(offset); if (!p) { return nullptr; } return p->value(); } WasmBreakpointSite* DebugState::getOrCreateBreakpointSite(JSContext* cx, Instance* instance, uint32_t offset) { WasmBreakpointSite* site; WasmBreakpointSiteMap::AddPtr p = breakpointSites_.lookupForAdd(offset); if (!p) { site = cx->new_(instance->object(), offset); if (!site) { return nullptr; } if (!breakpointSites_.add(p, offset, site)) { js_delete(site); ReportOutOfMemory(cx); return nullptr; } AddCellMemory(instance->object(), sizeof(WasmBreakpointSite), MemoryUse::BreakpointSite); toggleBreakpointTrap(cx->runtime(), instance, offset, true); } else { site = p->value(); } return site; } bool DebugState::hasBreakpointSite(uint32_t offset) { return breakpointSites_.has(offset); } void DebugState::destroyBreakpointSite(JS::GCContext* gcx, Instance* instance, uint32_t offset) { WasmBreakpointSiteMap::Ptr p = breakpointSites_.lookup(offset); MOZ_ASSERT(p); gcx->delete_(instance->objectUnbarriered(), p->value(), MemoryUse::BreakpointSite); breakpointSites_.remove(p); toggleBreakpointTrap(gcx->runtime(), instance, offset, false); } void DebugState::clearBreakpointsIn(JS::GCContext* gcx, WasmInstanceObject* instance, js::Debugger* dbg, JSObject* handler) { MOZ_ASSERT(instance); // Breakpoints hold wrappers in the instance's compartment for the handler. // Make sure we don't try to search for the unwrapped handler. MOZ_ASSERT_IF(handler, instance->compartment() == handler->compartment()); if (breakpointSites_.empty()) { return; } for (WasmBreakpointSiteMap::Enum e(breakpointSites_); !e.empty(); e.popFront()) { WasmBreakpointSite* site = e.front().value(); MOZ_ASSERT(site->instanceObject == instance); Breakpoint* nextbp; for (Breakpoint* bp = site->firstBreakpoint(); bp; bp = nextbp) { nextbp = bp->nextInSite(); MOZ_ASSERT(bp->site == site); if ((!dbg || bp->debugger == dbg) && (!handler || bp->getHandler() == handler)) { bp->delete_(gcx); } } if (site->isEmpty()) { gcx->delete_(instance, site, MemoryUse::BreakpointSite); e.removeFront(); } } } void DebugState::enableDebuggingForFunction(Instance* instance, uint32_t funcIndex) { instance->setDebugFilter(funcIndex, true); } void DebugState::disableDebuggingForFunction(Instance* instance, uint32_t funcIndex) { instance->setDebugFilter(funcIndex, false); } void DebugState::enableDebugTrapping(Instance* instance) { instance->setDebugStub(code_->sharedStubs().base() + code_->debugStubOffset()); } void DebugState::disableDebugTrapping(Instance* instance) { instance->setDebugStub(nullptr); } void DebugState::adjustEnterAndLeaveFrameTrapsState(JSContext* cx, Instance* instance, bool enabled) { MOZ_ASSERT_IF(!enabled, enterAndLeaveFrameTrapsCounter_ > 0); bool wasEnabled = enterAndLeaveFrameTrapsCounter_ > 0; enterAndLeaveFrameTrapsCounter_ += enabled ? 1 : -1; bool stillEnabled = enterAndLeaveFrameTrapsCounter_ > 0; if (wasEnabled == stillEnabled) { return; } MOZ_RELEASE_ASSERT(&instance->codeMeta() == &codeMeta()); MOZ_RELEASE_ASSERT(instance->codeMetaForAsmJS() == codeMetaForAsmJS()); uint32_t numFuncs = codeMeta().numFuncs(); if (enabled) { MOZ_ASSERT(enterAndLeaveFrameTrapsCounter_ > 0); for (uint32_t funcIdx = 0; funcIdx < numFuncs; funcIdx++) { enableDebuggingForFunction(instance, funcIdx); } enableDebugTrapping(instance); } else { MOZ_ASSERT(enterAndLeaveFrameTrapsCounter_ == 0); bool anyEnabled = false; for (uint32_t funcIdx = 0; funcIdx < numFuncs; funcIdx++) { // For each function, disable the bit if nothing else is going on. This // means determining if there's stepping or breakpoints. bool mustLeaveEnabled = stepperCounters_.lookup(funcIdx).found(); for (auto iter = breakpointSites_.iter(); !iter.done() && !mustLeaveEnabled; iter.next()) { WasmBreakpointSite* site = iter.get().value(); CallSite callSite; const CodeBlock& codeBlock = debugCode(); if (SlowCallSiteSearchByOffset(codeBlock, site->offset, &callSite)) { size_t debugTrapOffset = callSite.returnAddressOffset(); const CodeRange* codeRange = code_->lookupFuncRange(codeBlock.base() + debugTrapOffset); MOZ_ASSERT(codeRange); mustLeaveEnabled = codeRange->funcIndex() == funcIdx; } } if (mustLeaveEnabled) { anyEnabled = true; } else { disableDebuggingForFunction(instance, funcIdx); } } if (!anyEnabled) { disableDebugTrapping(instance); } } } void DebugState::ensureEnterFrameTrapsState(JSContext* cx, Instance* instance, bool enabled) { if (enterFrameTrapsEnabled_ == enabled) { return; } adjustEnterAndLeaveFrameTrapsState(cx, instance, enabled); enterFrameTrapsEnabled_ = enabled; } bool DebugState::debugGetLocalTypes(uint32_t funcIndex, ValTypeVector* locals, size_t* argsLength, StackResults* stackResults) { const TypeContext& types = *codeMeta().types; const FuncType& funcType = codeMeta().getFuncType(funcIndex); const ValTypeVector& args = funcType.args(); const ValTypeVector& results = funcType.results(); ResultType resultType(ResultType::Vector(results)); *argsLength = args.length(); *stackResults = ABIResultIter::HasStackResults(resultType) ? StackResults::HasStackResults : StackResults::NoStackResults; if (!locals->appendAll(args)) { return false; } // Decode local var types from wasm binary function body. const BytecodeRange& funcRange = codeTailMeta().funcDefRange(funcIndex); BytecodeSpan funcBytecode = codeTailMeta().funcDefBody(funcIndex); Decoder d(funcBytecode.data(), funcBytecode.data() + funcBytecode.size(), funcRange.start, /* error = */ nullptr); return DecodeValidatedLocalEntries(types, d, locals); } bool DebugState::getGlobal(Instance& instance, uint32_t globalIndex, MutableHandleValue vp) { const GlobalDesc& global = codeMeta().globals[globalIndex]; if (global.isConstant()) { LitVal value = global.constantValue(); switch (value.type().kind()) { case ValType::I32: vp.set(Int32Value(value.i32())); break; case ValType::I64: // Just display as a Number; it's ok if we lose some precision vp.set(NumberValue((double)value.i64())); break; case ValType::F32: vp.set(NumberValue(JS::CanonicalizeNaN(value.f32()))); break; case ValType::F64: vp.set(NumberValue(JS::CanonicalizeNaN(value.f64()))); break; case ValType::Ref: // It's possible to do better. We could try some kind of hashing // scheme, to make the pointer recognizable without revealing it. vp.set(MagicValue(JS_OPTIMIZED_OUT)); break; case ValType::V128: // Debugger must be updated to handle this, and should be updated to // handle i64 in any case. vp.set(MagicValue(JS_OPTIMIZED_OUT)); break; default: MOZ_CRASH("Global constant type"); } return true; } void* dataPtr = instance.data() + global.offset(); if (global.isIndirect()) { dataPtr = *static_cast(dataPtr); } switch (global.type().kind()) { case ValType::I32: { vp.set(Int32Value(*static_cast(dataPtr))); break; } case ValType::I64: { // Just display as a Number; it's ok if we lose some precision vp.set(NumberValue((double)*static_cast(dataPtr))); break; } case ValType::F32: { vp.set(NumberValue(JS::CanonicalizeNaN(*static_cast(dataPtr)))); break; } case ValType::F64: { vp.set(NumberValue(JS::CanonicalizeNaN(*static_cast(dataPtr)))); break; } case ValType::Ref: { // Just hide it. See above. vp.set(MagicValue(JS_OPTIMIZED_OUT)); break; } case ValType::V128: { // Just hide it. See above. vp.set(MagicValue(JS_OPTIMIZED_OUT)); break; } default: { MOZ_CRASH("Global variable type"); break; } } return true; } bool DebugState::getSourceMappingURL(JSContext* cx, MutableHandleString result) const { result.set(nullptr); for (const CustomSection& customSection : module_->moduleMeta().customSections) { const Bytes& sectionName = customSection.name; if (strlen(SourceMappingURLSectionName) != sectionName.length() || memcmp(SourceMappingURLSectionName, sectionName.begin(), sectionName.length()) != 0) { continue; } // Parse found "SourceMappingURL" custom section. Decoder d(customSection.payload->begin(), customSection.payload->end(), 0, /* error = */ nullptr); uint32_t nchars; if (!d.readVarU32(&nchars)) { return true; // ignoring invalid section data } const uint8_t* chars; if (!d.readBytes(nchars, &chars) || d.currentPosition() != d.end()) { return true; // ignoring invalid section data } JS::UTF8Chars utf8Chars(reinterpret_cast(chars), nchars); JSString* str = JS_NewStringCopyUTF8N(cx, utf8Chars); if (!str) { return false; } result.set(str); return true; } // Check presence of "SourceMap:" HTTP response header. char* sourceMapURL = codeMeta().sourceMapURL().get(); if (sourceMapURL && strlen(sourceMapURL)) { JS::UTF8Chars utf8Chars(sourceMapURL, strlen(sourceMapURL)); JSString* str = JS_NewStringCopyUTF8N(cx, utf8Chars); if (!str) { return false; } result.set(str); } return true; } void DebugState::addSizeOfMisc( mozilla::MallocSizeOf mallocSizeOf, CodeMetadata::SeenSet* seenCodeMeta, CodeMetadataForAsmJS::SeenSet* seenCodeMetaForAsmJS, Code::SeenSet* seenCode, size_t* code, size_t* data) const { code_->addSizeOfMiscIfNotSeen(mallocSizeOf, seenCodeMeta, seenCodeMetaForAsmJS, seenCode, code, data); module_->addSizeOfMisc(mallocSizeOf, seenCodeMeta, seenCodeMetaForAsmJS, seenCode, code, data); }