diff options
Diffstat (limited to 'src/debug/di/process.cpp')
-rw-r--r-- | src/debug/di/process.cpp | 15235 |
1 files changed, 15235 insertions, 0 deletions
diff --git a/src/debug/di/process.cpp b/src/debug/di/process.cpp new file mode 100644 index 0000000000..44e4a0c667 --- /dev/null +++ b/src/debug/di/process.cpp @@ -0,0 +1,15235 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +//***************************************************************************** +// File: process.cpp +// + +// +//***************************************************************************** +#include "stdafx.h" +#include "primitives.h" +#include "safewrap.h" + +#include "check.h" + +#ifndef SM_REMOTESESSION +#define SM_REMOTESESSION 0x1000 +#endif + +#include "corpriv.h" +#include "corexcep.h" +#include "../../dlls/mscorrc/resource.h" +#include <limits.h> + +#include <sstring.h> + +// @dbgtodo shim: process has some private hooks into the shim. +#include "shimpriv.h" + +#include "metadataexports.h" +#include "readonlydatatargetfacade.h" +#include "metahost.h" + +// Keep this around for retail debugging. It's very very useful because +// it's global state that we can always find, regardless of how many locals the compiler +// optimizes away ;) +struct RSDebuggingInfo; +extern RSDebuggingInfo * g_pRSDebuggingInfo; + +//--------------------------------------------------------------------------------------- +// +// OpenVirtualProcessImpl method called by the shim to get an ICorDebugProcess4 instance +// +// Arguments: +// clrInstanceId - target pointer identifying which CLR in the Target to debug. +// pDataTarget - data target abstraction. +// hDacModule - the handle of the appropriate DAC dll for this runtime +// riid - interface ID to query for. +// ppProcessOut - new object for target, interface ID matches riid. +// ppFlagsOut - currently only has 1 bit to indicate whether or not this runtime +// instance will send a managed event after attach +// +// Return Value: +// S_OK on success. Else failure +// +// Assumptions: +// +// Notes: +// The outgoing process object can be cleaned up by calling Detach (which +// will reset the Attach bit.) +// @dbgtodo attach-bit: need to determine fate of attach bit. +// +//--------------------------------------------------------------------------------------- +STDAPI OpenVirtualProcessImpl( + ULONG64 clrInstanceId, + IUnknown * pDataTarget, + HMODULE hDacModule, + CLR_DEBUGGING_VERSION * pMaxDebuggerSupportedVersion, + REFIID riid, + IUnknown ** ppInstance, + CLR_DEBUGGING_PROCESS_FLAGS* pFlagsOut) +{ + HRESULT hr = S_OK; + RSExtSmartPtr<CordbProcess> pProcess; + PUBLIC_API_ENTRY(NULL); + EX_TRY + { + + if ( (pDataTarget == NULL) || (clrInstanceId == 0) || (pMaxDebuggerSupportedVersion == NULL) || + ((pFlagsOut == NULL) && (ppInstance == NULL)) + ) + { + ThrowHR(E_INVALIDARG); + } + + // We consider the top 8 bits of the struct version to be the only part that represents + // a breaking change. This gives us some freedom in the future to have the debugger + // opt into getting more data. + const WORD kMajorMask = 0xff00; + const WORD kMaxStructMajor = 0; + if ((pMaxDebuggerSupportedVersion->wStructVersion & kMajorMask) > kMaxStructMajor) + { + // Don't know how to interpret the version structure + ThrowHR(CORDBG_E_UNSUPPORTED_VERSION_STRUCT); + } + + // This process object is intended to be used for the V3 pipeline, and so + // much of the process from V2 is not being used. For example, + // - there is no ShimProcess object + // - there is no w32et thread (all threads are effectively an event thread) + // - the stop state is 'live', which corresponds to CordbProcess not knowing what + // its stop state really is (because that is now controlled by the shim). + IfFailThrow(CordbProcess::OpenVirtualProcess( + clrInstanceId, + pDataTarget, // takes a reference + hDacModule, + NULL, // Cordb + (DWORD) 0, // 0 for V3 cases (pShim == NULL). + NULL, // no Shim in V3 cases + &pProcess)); + + // CordbProcess::OpenVirtualProcess already did the external addref to pProcess. + // Since pProcess is a smart ptr, it will external release in this function. + // Living reference will be the one from the QI. + + // get the managed debug event pending flag + if(pFlagsOut != NULL) + { + hr = pProcess->GetAttachStateFlags(pFlagsOut); + if(FAILED(hr)) + { + ThrowHR(hr); + } + } + + // + // Check to make sure the debugger supports debugging this version + // Note that it's important that we still store the flags (above) in this case + // + if (!CordbProcess::IsCompatibleWith(pMaxDebuggerSupportedVersion->wMajor)) + { + // Not compatible - don't keep the process instance, and return this specific error-code + ThrowHR(CORDBG_E_UNSUPPORTED_FORWARD_COMPAT); + } + + // + // Now Query for the requested interface + // + if(ppInstance != NULL) + { + IfFailThrow(pProcess->QueryInterface(riid, reinterpret_cast<void**> (ppInstance))); + } + + // if you have to add code here that could fail make sure ppInstance gets released and NULL'ed at exit + } + EX_CATCH_HRESULT(hr); + + if((FAILED(hr) || ppInstance == NULL) && pProcess != NULL) + { + // The process has a strong reference to itself which is only released by neutering it. + // Since we aren't handing out the ref then we need to clean it up + _ASSERTE(ppInstance == NULL || *ppInstance == NULL); + pProcess->Neuter(); + } + return hr; +}; + +//--------------------------------------------------------------------------------------- +// DEPRECATED - use OpenVirtualProcessImpl +// OpenVirtualProcess method used by the shim in CLR v4 Beta1 +// We'd like a beta1 shim/VS to still be able to open dumps using a CLR v4 Beta2+ mscordbi.dll, +// so we'll leave this in place (at least until after Beta2 is in wide use). +//--------------------------------------------------------------------------------------- +STDAPI OpenVirtualProcess2( + ULONG64 clrInstanceId, + IUnknown * pDataTarget, + HMODULE hDacModule, + REFIID riid, + IUnknown ** ppInstance, + CLR_DEBUGGING_PROCESS_FLAGS* pFlagsOut) +{ + CLR_DEBUGGING_VERSION maxVersion = {0}; + maxVersion.wMajor = 4; + return OpenVirtualProcessImpl(clrInstanceId, pDataTarget, hDacModule, &maxVersion, riid, ppInstance, pFlagsOut); +} + +//--------------------------------------------------------------------------------------- +// DEPRECATED - use OpenVirtualProcessImpl +// Public OpenVirtualProcess method to get an ICorDebugProcess4 instance +// Used directly in CLR v4 pre Beta1 - can probably be safely removed now +//--------------------------------------------------------------------------------------- +STDAPI OpenVirtualProcess( + ULONG64 clrInstanceId, + IUnknown * pDataTarget, + REFIID riid, + IUnknown ** ppInstance) +{ + return OpenVirtualProcess2(clrInstanceId, pDataTarget, NULL, riid, ppInstance, NULL); +}; + +//----------------------------------------------------------------------------- +// Most Hresults to Unrecoverable error indicate an internal error +// in the Right-Side. +// However, a few are legal (eg, "could actually happen in a retail scenario and +// not indicate an issue in mscorbi"). Track that here. +//----------------------------------------------------------------------------- + +bool IsLegalFatalError(HRESULT hr) +{ + return + (hr == CORDBG_E_INCOMPATIBLE_PROTOCOL) || + (hr == CORDBG_E_CANNOT_DEBUG_FIBER_PROCESS) || + (hr == CORDBG_E_UNCOMPATIBLE_PLATFORMS) || + (hr == CORDBG_E_MISMATCHED_CORWKS_AND_DACWKS_DLLS) || + // This should only happen in the case of a security attack on us. + (hr == E_ACCESSDENIED) || + (hr == E_FAIL); +} + +//----------------------------------------------------------------------------- +// Safe wait. Use this anytime we're waiting on: +// - an event signaled by the helper thread. +// - something signaled by a thread that holds the process lock. +// Note that we must preserve GetLastError() semantics. +//----------------------------------------------------------------------------- +inline DWORD SafeWaitForSingleObject(CordbProcess * p, HANDLE h, DWORD dwTimeout) +{ + // Can't hold process lock while blocking + _ASSERTE(!p->ThreadHoldsProcessLock()); + + return ::WaitForSingleObject(h, dwTimeout); +} + +#define CORDB_WAIT_TIMEOUT 360000 // milliseconds + +//--------------------------------------------------------------------------------------- +// +// Get the timeout value used in waits. +// +// Return Value: +// Number of milliseconds to waite or possible INFINITE (-1). +// +// +// Notes: +// Uses registry values for fine tuning. +// + +// static +static inline DWORD CordbGetWaitTimeout() +{ +#ifdef _DEBUG + // 0 = Wait forever + // 1 = Wait for CORDB_WAIT_TIMEOUT + // n = Wait for n milliseconds + static ConfigDWORD cordbWaitTimeout; + DWORD dwTimeoutVal = cordbWaitTimeout.val(CLRConfig::INTERNAL_DbgWaitTimeout); + if (dwTimeoutVal == 0) + return DWORD(-1); + else if (dwTimeoutVal != 1) + return dwTimeoutVal; + else +#endif + { + return CORDB_WAIT_TIMEOUT; + } +} + +//---------------------------------------------------------------------------- +// Implementation of IDacDbiInterface::IMetaDataLookup. +// lookup Internal Metadata Importer keyed by PEFile +// isILMetaDataForNGENImage is true iff the IMDInternalImport returned represents a pointer to +// metadata from an IL image when the module was an ngen'ed image. +IMDInternalImport * CordbProcess::LookupMetaData(VMPTR_PEFile vmPEFile, bool &isILMetaDataForNGENImage) +{ + INTERNAL_DAC_CALLBACK(this); + + HASHFIND hashFindAppDomain; + HASHFIND hashFindModule; + IMDInternalImport * pMDII = NULL; + isILMetaDataForNGENImage = false; + + // Check to see if one of the cached modules has the metadata we need + // If not we will do a more exhaustive search below + for (CordbAppDomain * pAppDomain = m_appDomains.FindFirst(&hashFindAppDomain); + pAppDomain != NULL; + pAppDomain = m_appDomains.FindNext(&hashFindAppDomain)) + { + for (CordbModule * pModule = pAppDomain->m_modules.FindFirst(&hashFindModule); + pModule != NULL; + pModule = pAppDomain->m_modules.FindNext(&hashFindModule)) + { + if (pModule->GetPEFile() == vmPEFile) + { + pMDII = NULL; + ALLOW_DATATARGET_MISSING_MEMORY( + pMDII = pModule->GetInternalMD(); + ); + if(pMDII != NULL) + return pMDII; + } + } + } + + // Cache didn't have it... time to search harder + PrepopulateAppDomainsOrThrow(); + + // There may be perf issues here. The DAC may make a lot of metadata requests, and so + // this may be an area for potential perf optimizations if we find things running slow. + + // enumerate through all Modules + for (CordbAppDomain * pAppDomain = m_appDomains.FindFirst(&hashFindAppDomain); + pAppDomain != NULL; + pAppDomain = m_appDomains.FindNext(&hashFindAppDomain)) + { + pAppDomain->PrepopulateModules(); + + for (CordbModule * pModule = pAppDomain->m_modules.FindFirst(&hashFindModule); + pModule != NULL; + pModule = pAppDomain->m_modules.FindNext(&hashFindModule)) + { + if (pModule->GetPEFile() == vmPEFile) + { + pMDII = NULL; + ALLOW_DATATARGET_MISSING_MEMORY( + pMDII = pModule->GetInternalMD(); + ); + + if ( pMDII == NULL) + { + // If we couldn't get metadata from the CordbModule, then we need to ask the + // debugger if it can find the metadata elsewhere. + // If this was live debugging, we should have just gotten the memory contents. + // Thus this code is for dump debugging, when you don't have the metadata in the dump. + pMDII = LookupMetaDataFromDebugger(vmPEFile, isILMetaDataForNGENImage, pModule); + } + return pMDII; + } + } + } + + return NULL; +} + + +IMDInternalImport * CordbProcess::LookupMetaDataFromDebugger( + VMPTR_PEFile vmPEFile, + bool &isILMetaDataForNGENImage, + CordbModule * pModule) +{ + DWORD dwImageTimeStamp = 0; + DWORD dwImageSize = 0; + bool isNGEN = false; + StringCopyHolder filePath; + IMDInternalImport * pMDII = NULL; + + // First, see if the debugger can locate the exact metadata we want. + if (this->GetDAC()->GetMetaDataFileInfoFromPEFile(vmPEFile, dwImageTimeStamp, dwImageSize, isNGEN, &filePath)) + { + _ASSERTE(filePath.IsSet()); + + // Since we track modules by their IL images, that presents a little bit of oddness here. The correct + // thing to do is preferentially load the NI content. + // We don't discriminate between timestamps & sizes becuase CLRv4 deterministic NGEN guarantees that the + // IL image and NGEN image have the same timestamp and size. Should that guarantee change, this code + // will be horribly broken. + + // If we happen to have an NI file path, use it instead. + const WCHAR * pwszFilePath = pModule->GetNGenImagePath(); + if (pwszFilePath) + { + // Force the issue, regardless of the older codepath's opinion. + isNGEN = true; + } + else + { + pwszFilePath = (WCHAR *)filePath; + } + + ALLOW_DATATARGET_MISSING_MEMORY( + pMDII = LookupMetaDataFromDebuggerForSingleFile(pModule, pwszFilePath, dwImageTimeStamp, dwImageSize); + ); + + // If it's an ngen'ed image and the debugger couldn't find it, we can use the metadata from + // the corresponding IL image if the debugger can locate it. + filePath.Clear(); + if ((pMDII == NULL) && + (isNGEN) && + (this->GetDAC()->GetILImageInfoFromNgenPEFile(vmPEFile, dwImageTimeStamp, dwImageSize, &filePath))) + { + _ASSERTE(filePath.IsSet()); + + WCHAR *mutableFilePath = (WCHAR *)filePath; + +#if defined(FEATURE_CORESYSTEM) + size_t pathLen = wcslen(mutableFilePath); + + const wchar_t *nidll = W(".ni.dll"); + const wchar_t *niexe = W(".ni.exe"); + const size_t dllLen = wcslen(nidll); // used for ni.exe as well + + const wchar_t *niwinmd = W(".ni.winmd"); + const size_t winmdLen = wcslen(niwinmd); + + if (pathLen > dllLen && _wcsicmp(mutableFilePath+pathLen-dllLen, nidll) == 0) + { + wcscpy_s(mutableFilePath+pathLen-dllLen, dllLen, W(".dll")); + } + else if (pathLen > dllLen && _wcsicmp(mutableFilePath+pathLen-dllLen, niexe) == 0) + { + wcscpy_s(mutableFilePath+pathLen-dllLen, dllLen, W(".exe")); + } + else if (pathLen > winmdLen && _wcsicmp(mutableFilePath+pathLen-winmdLen, niwinmd) == 0) + { + wcscpy_s(mutableFilePath+pathLen-winmdLen, winmdLen, W(".winmd")); + } +#endif//FEATURE_CORESYSTEM + + ALLOW_DATATARGET_MISSING_MEMORY( + pMDII = LookupMetaDataFromDebuggerForSingleFile(pModule, mutableFilePath, dwImageTimeStamp, dwImageSize); + ); + + if (pMDII != NULL) + { + isILMetaDataForNGENImage = true; + } + } + } + return pMDII; +} + +// We do not know if the image being sent to us is an IL image or ngen image. +// CordbProcess::LookupMetaDataFromDebugger() has this knowledge when it looks up the file to hand off +// to this function. +// DacDbiInterfaceImpl::GetMDImport() has this knowledge in the isNGEN flag. +// The CLR v2 code that windbg used made a distinction whether the metadata came from +// the exact binary or not (i.e. were we getting metadata from the IL image and using +// it against the ngen image?) but that information was never used and so not brought forward. +// It would probably be more interesting generally to track whether the debugger gives us back +// a file that bears some relationship to the file we asked for, which would catch the NI/IL case +// as well. +IMDInternalImport * CordbProcess::LookupMetaDataFromDebuggerForSingleFile( + CordbModule * pModule, + LPCWSTR pwszFilePath, + DWORD dwTimeStamp, + DWORD dwSize) +{ + INTERNAL_DAC_CALLBACK(this); + + ULONG32 cchLocalImagePath = MAX_LONGPATH; + ULONG32 cchLocalImagePathRequired; + NewArrayHolder<WCHAR> pwszLocalFilePath = NULL; + IMDInternalImport * pMDII = NULL; + + const HRESULT E_NSF_BUFFER = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); + HRESULT hr = E_NSF_BUFFER; + for(unsigned i=0; i<2 && hr == E_NSF_BUFFER; i++) + { + if (pwszLocalFilePath != NULL) + pwszLocalFilePath.Release(); + + if (NULL == (pwszLocalFilePath = new (nothrow) WCHAR[cchLocalImagePath+1])) + ThrowHR(E_OUTOFMEMORY); + + cchLocalImagePathRequired = 0; + + hr = m_pMetaDataLocator->GetMetaData(pwszFilePath, + dwTimeStamp, + dwSize, + cchLocalImagePath, + &cchLocalImagePathRequired, + pwszLocalFilePath); + + pwszLocalFilePath[cchLocalImagePath] = W('\0'); + cchLocalImagePath = cchLocalImagePathRequired; + } + + if (SUCCEEDED(hr)) + { + hr = pModule->InitPublicMetaDataFromFile(pwszLocalFilePath, ofReadOnly, false); + if (SUCCEEDED(hr)) + { + // While we're successfully returning a metadata reader, remember that there's + // absolutely no guarantee this metadata is an exact match for the vmPEFile. + // The debugger could literally send us back a path to any managed file with + // metadata content that is readable and we'll 'succeed'. + // For now, this is by-design. A debugger should be allowed to decide if it wants + // to take a risk by returning 'mostly matching' metadata to see if debugging is + // possible in the absense of a true match. + pMDII = pModule->GetInternalMD(); + } + } + + return pMDII; +} + + +//--------------------------------------------------------------------------------------- +// +// Implement IDacDbiInterface::IAllocator::Alloc +// Expected to throws on error. +// +// Arguments: +// lenBytes - size of the byte array to allocate +// +// Return Value: +// Return the newly allocated byte array, or throw on OOM +// +// Notes: +// Since this function is a callback from DAC, it must not take the process lock. +// If it does, we may deadlock between the DD lock and the process lock. +// If we really need to take the process lock for whatever reason, we must take it in the DBI functions +// which call the DAC API that ends up calling this function. +// See code:InternalDacCallbackHolder for more information. +// + +void * CordbProcess::Alloc(SIZE_T lenBytes) +{ + return new BYTE[lenBytes]; // throws +} + +//--------------------------------------------------------------------------------------- +// +// Implements IDacDbiInterface::IAllocator::Free +// +// Arguments: +// p - pointer to the memory to be released +// +// Notes: +// Since this function is a callback from DAC, it must not take the process lock. +// If it does, we may deadlock between the DD lock and the process lock. +// If we really need to take the process lock for whatever reason, we must take it in the DBI functions +// which call the DAC API that ends up calling this function. +// See code:InternalDacCallbackHolder for more information. +// + +void CordbProcess::Free(void * p) +{ + // This shouldn't throw. + delete [] ((BYTE *) p); +} + + +//--------------------------------------------------------------------------------------- +// +// #DBIVersionChecking +// +// There are a few checks we need to do to make sure we are using the matching DBI and DAC for a particular +// version of the runtime. +// +// 1. Runtime vs. DBI +// - Desktop +// This is done by making sure that the CorDebugInterfaceVersion passed to code:CreateCordbObject is +// compatible with the version of the DBI. +// +// - Windows CoreCLR +// This is done by dbgshim.dll. It checks whether the runtime DLL and the DBI DLL have the same +// product version. See CreateDebuggingInterfaceForVersion() in dbgshim.cpp. +// +// - Remote transport (Mac CoreCLR + CoreSystem CoreCLR) +// Since there is no dbgshim.dll for a remote CoreCLR, we have to do this check in some other place. +// We do this in code:CordbProcess::CreateDacDbiInterface, by calling +// code:DacDbiInterfaceImpl::CheckDbiVersion right after we have created the DDMarshal. +// The IDacDbiInterface implementation on remote device checks the product version of the device +// coreclr by: +// mac - looking at the Info.plist file in the CoreCLR bundle. +// CoreSystem - this check is skipped at the moment, but should be implemented if we release it +// +// The one twist here is that the DBI needs to communicate with the IDacDbiInterface +// implementation on the device BEFORE it can verify the product versions. This means that we need to +// have one IDacDbiInterface API which is consistent across all versions of the IDacDbiInterface. +// This puts two constraints on CheckDbiVersion(): +// +// 1. It has to be the first API on the IDacDbiInterface. +// - Otherwise, a wrong version of the DBI may end up calling a different API on the +// IDacDbiInterface and getting random results. (Really what matters is that it is +// protocol message id 0, at present the source code position implies the message id) +// +// 2. Its parameters cannot change. +// - Otherwise, we may run into random errors when we marshal/unmarshal the arguments for the +// call to CheckDbiVersion(). Debugging will still fail, but we won't get the +// version mismatch error. (Again, the protocol is what ultimately matters) +// - To mitigate the impact of this constraint, we use the code:DbiVersion structure. +// In addition to the DBI version, it also contains a format number (in case we decide to +// check something else in the future), a breaking change number so that we can force +// breaking changes between a DBI and a DAC, and space reserved for future use. +// +// 2. DBI vs. DAC +// - Desktop and Windows CoreCLR (old architecture) +// No verification is done. There is a transitive implication that if DBI matches runtime and DAC matches +// runtime then DBI matches DAC. Technically because the DBI only matches runtime on major version number +// runtime and DAC could be from different builds. However because we service all three binaries together +// and DBI always loads the DAC that is sitting in the same directory DAC and DBI generally get tight +// version coupling. A user with admin privleges could put different builds together and no version check +// would ever fail though. +// +// - Desktop and Windows CoreCLR (new architecture) +// No verification is done. Similar to above its implied that if DBI matches runtime and runtime matches +// DAC then DBI matches DAC. The only difference is that here both the DBI and DAC are provided by the +// debugger. We provide timestamp and filesize for both binaries which are relatively strongly bound hints, +// but there is no enforcement on the returned binaries beyond the runtime compat checking. +// +// - Remote transport (Mac CoreCLR and CoreSystem CoreCLR) +// Because the transport exists between DBI and DAC it becomes much more important to do a versioning check +// +// Mac - currently does a tightly bound version check between DBI and the runtime (CheckDbiVersion() above), +// which transitively gives a tightly bound check to DAC. In same function there is also a check that is +// logically a DAC DBI protocol check, verifying that the m_dwProtocolBreakingChangeCounter of DbiVersion +// matches. However this check should be weaker than the build version check and doesn't add anything here. +// +// CoreSystem - currently skips the tightly bound version check to make internal deployment and usage easier. +// We want to use old desktop side debugger components to target newer CoreCLR builds, only forcing a desktop +// upgrade when the protocol actually does change. To do this we use two checks: +// 1. The breaking change counter in CheckDbiVersion() whenever a dev knows they are breaking back +// compat and wants to be explicit about it. This is the same as mac above. +// 2. During the auto-generation of the DDMarshal classes we take an MD5 hash of IDacDbiInterface source +// code and embed it in two DDMarshal functions, one which runs locally and one that runs remotely. +// If both DBI and DAC were built from the same source then the local and remote hashes will match. If the +// hashes don't match then we assume there has been a been a breaking change in the protocol. Note +// this hash could have both false-positives and false-negatives. False positives could occur when +// IDacDbiInterface is changed in a trivial way, such as changing a comment. False negatives could +// occur when the semantics of the protocol are changed even though the interface is not. Another +// case would be changing the DDMarshal proxy generation code. In addition to the hashes we also +// embed timestamps when the auto-generated code was produced. However this isn't used for version +// matching, only as a hint to indicate which of two mismatched versions is newer. +// +// +// 3. Runtime vs. DAC +// - Desktop, Windows CoreCLR, CoreSystem CoreCLR +// In both cases we check this by matching the timestamp in the debug directory of the runtime image +// and the timestamp we store in the DAC table when we generate the DAC dll. This is done in +// code:ClrDataAccess::VerifyDlls. +// +// - Mac CoreCLR +// On Mac, we don't have a timestamp in the runtime image. Instead, we rely on checking the 16-byte +// UUID in the image. This UUID is used to check whether a symbol file matches the image, so +// conceptually it's the same as the timestamp we use on Windows. This is also done in +// code:ClrDataAccess::VerifyDlls. +// +//--------------------------------------------------------------------------------------- +// +// Instantiates a DacDbi Interface object in a live-debugging scenario that matches +// the current instance of mscorwks in this process. +// +// Return Value: +// Returns on success. Else throws. +// +// Assumptions: +// Client will code:CordbProcess::FreeDac when its done with the DacDbi interface. +// Caller has initialized clrInstanceId. +// +// Notes: +// This looks for the DAC next to this current DBI. This assumes that Dac and Dbi are both on +// the local file system. That assumption will break in zero-copy deployment scenarios. +// +//--------------------------------------------------------------------------------------- +void +CordbProcess::CreateDacDbiInterface() +{ + _ASSERTE(m_pDACDataTarget != NULL); + _ASSERTE(m_pDacPrimitives == NULL); // don't double-init + + // Caller has already determined which CLR in the target is being debugged. + _ASSERTE(m_clrInstanceId != 0); + + m_pDacPrimitives = NULL; + + HRESULT hrStatus = S_OK; + + // Non-marshalling path for live local dac. + // in the new arch we can get the module from OpenVirtualProcess2 but in the shim case + // and the deprecated OpenVirtualProcess case we must assume it comes from DAC in the + // same directory as DBI + if(m_hDacModule == NULL) + { + m_hDacModule.Assign(ShimProcess::GetDacModule()); + } + + // + // Get the access interface, passing our callback interfaces (data target, allocator and metadata lookup) + // + + IDacDbiInterface::IAllocator * pAllocator = this; + IDacDbiInterface::IMetaDataLookup * pMetaDataLookup = this; + + + typedef HRESULT (STDAPICALLTYPE * PFN_DacDbiInterfaceInstance)( + ICorDebugDataTarget *, + CORDB_ADDRESS, + IDacDbiInterface::IAllocator *, + IDacDbiInterface::IMetaDataLookup *, + IDacDbiInterface **); + + IDacDbiInterface* pInterfacePtr = NULL; + PFN_DacDbiInterfaceInstance pfnEntry = (PFN_DacDbiInterfaceInstance)GetProcAddress(m_hDacModule, "DacDbiInterfaceInstance"); + if (!pfnEntry) + { + ThrowLastError(); + } + + hrStatus = pfnEntry(m_pDACDataTarget, m_clrInstanceId, pAllocator, pMetaDataLookup, &pInterfacePtr); + IfFailThrow(hrStatus); + + // We now have a resource, pInterfacePtr, that needs to be freed. + m_pDacPrimitives = pInterfacePtr; + + // Setup DAC target consistency checking based on what we're using for DBI + m_pDacPrimitives->DacSetTargetConsistencyChecks( m_fAssertOnTargetInconsistency ); +} + +//--------------------------------------------------------------------------------------- +// +// Is the DAC/DBI interface initialized? +// +// Return Value: +// TRUE iff init. +// +// Notes: +// The RS will try to initialize DD as soon as it detects the runtime as loaded. +// If the DD interface has not initialized, then it very likely the runtime has not +// been loaded into the target. +// +BOOL CordbProcess::IsDacInitialized() +{ + return m_pDacPrimitives != NULL; +} + +//--------------------------------------------------------------------------------------- +// +// Get the DAC interface. +// +// Return Value: +// the Dac/Dbi interface pointer to the process. +// Never returns NULL. +// +// Assumptions: +// Caller is responsible for ensuring Data-Target is safe to access (eg, not +// currently running). +// Caller is responsible for ensuring DAC-cache is flushed. Call code:CordbProcess::ForceDacFlush +// as needed. +// +//--------------------------------------------------------------------------------------- +IDacDbiInterface * CordbProcess::GetDAC() +{ + // Since the DD primitives may throw, easiest way to model that is to make this throw. + CONTRACTL + { + THROWS; + } + CONTRACTL_END; + + // We should always have the DAC/DBI interface. + _ASSERTE(m_pDacPrimitives != NULL); + return m_pDacPrimitives; +} + +//--------------------------------------------------------------------------------------- +// Get the Data-Target +// +// Returns: +// pointer to the data-target. Should be non-null. +// Lifetime of the pointer is until this process object is neutered. +// +ICorDebugDataTarget * CordbProcess::GetDataTarget() +{ + return m_pDACDataTarget; +} + +//--------------------------------------------------------------------------------------- +// Create a CordbProcess object around an existing OS process. +// +// Arguments: +// pDataTarget - abstracts access to the debuggee. +// clrInstanceId - identifies the CLR instance within the debuggee. (This is the +// base address of mscorwks) +// pCordb - Pointer to the implementation of the owning Cordb object implementing the +// owning ICD interface. +// This should go away - we can get the functionality from the pShim. +// If this is null, then pShim must be null too. +// processID - OS process ID of target process. 0 if pShim == NULL. +// pShim - shim counter part object. This allows hooks back for v2 compat. This will +// go away once we no longer support V2 backwards compat. +// This must be non-null for any V2 paths (including non-DAC-ized code). +// If this is null, then we're in a V3 path. +// ppProcess - out parameter for new process object. This gets addreffed. +// +// Return Value: +// S_OK on success, and *ppProcess set to newly created debuggee object. Else error. +// +// Notes: +// @dbgtodo - , shim: Cordb, and pShim will all eventually go away. +// +//--------------------------------------------------------------------------------------- + +// static +HRESULT CordbProcess::OpenVirtualProcess( + ULONG64 clrInstanceId, + IUnknown * pDataTarget, + HMODULE hDacModule, + Cordb* pCordb, + DWORD dwProcessID, + ShimProcess * pShim, + CordbProcess ** ppProcess) +{ + _ASSERTE(pDataTarget != NULL); + + // In DEBUG builds, verify that we do actually have an ICorDebugDataTarget (i.e. that + // someone hasn't messed up the COM interop marshalling, etc.). +#ifdef _DEBUG + { + IUnknown * pTempDt; + HRESULT hrQi = pDataTarget->QueryInterface(IID_ICorDebugDataTarget, (void**)&pTempDt); + _ASSERTE_MSG(SUCCEEDED(hrQi), "OpenVirtualProcess was passed something that isn't actually an ICorDebugDataTarget"); + pTempDt->Release(); + } +#endif + + // If we're emulating V2, then both pCordb and pShim are non-NULL. + // If we're doing a real V3 path, then they're both NULL. + // Either way, they should have the same null-status. + _ASSERTE((pCordb == NULL) == (pShim == NULL)); + + // If we're doing real V3, then we must have a real instance ID + _ASSERTE(!((pShim == NULL) && (clrInstanceId == 0))); + + *ppProcess = NULL; + + HRESULT hr = S_OK; + RSUnsafeExternalSmartPtr<CordbProcess> pProcess; + pProcess.Assign(new (nothrow) CordbProcess(clrInstanceId, pDataTarget, hDacModule, pCordb, dwProcessID, pShim)); + + if (pProcess == NULL) + { + return E_OUTOFMEMORY; + } + + ICorDebugProcess * pThis = pProcess; + (void)pThis; //prevent "unused variable" error from GCC + + // CordbProcess::Init may need shim hooks, so connect Shim now. + // This will bump reference count. + if (pShim != NULL) + { + pShim->SetProcess(pProcess); + + _ASSERTE(pShim->GetProcess() == pThis); + _ASSERTE(pShim->GetWin32EventThread() != NULL); + } + + hr = pProcess->Init(); + + if (SUCCEEDED(hr)) + { + *ppProcess = pProcess; + pProcess->ExternalAddRef(); + } + else + { + // handle failure path + pProcess->CleanupHalfBakedLeftSide(); + + if (pShim != NULL) + { + // Shim still needs to be disposed to clean up other resources. + pShim->SetProcess(NULL); + } + + // In failure case, pProcess's dtor will do the final release. + } + + + return hr; +} + +//--------------------------------------------------------------------------------------- +// CordbProcess constructor +// +// Arguments: +// pDataTarget - Pointer to an implementation of ICorDebugDataTarget +// (or ICorDebugMutableDataTarget), which virtualizes access to the process. +// clrInstanceId - representation of the CLR to debug in the process. Must be specified +// (non-zero) if pShim is NULL. If 0, use the first CLR that we see. +// pCordb - Pointer to the implementation of the owning Cordb object implementing the +// owning ICD interface. +// pW32 - Pointer to the Win32 event thread to use when processing events for this +// process. +// dwProcessID - For V3, 0. +// Else for shim codepaths, the processID of the process this object will represent. +// pShim - Pointer to the shim for handling V2 debuggers on the V3 architecture. +// +//--------------------------------------------------------------------------------------- + +CordbProcess::CordbProcess(ULONG64 clrInstanceId, + IUnknown * pDataTarget, + HMODULE hDacModule, + Cordb * pCordb, + DWORD dwProcessID, + ShimProcess * pShim) + : CordbBase(NULL, dwProcessID, enumCordbProcess), + m_fDoDelayedManagedAttached(false), + m_cordb(pCordb), + m_handle(NULL), + m_detached(false), + m_uninitializedStop(false), + m_exiting(false), + m_terminated(false), + m_unrecoverableError(false), + m_specialDeferment(false), + m_helperThreadDead(false), + m_loaderBPReceived(false), + m_cOutstandingEvals(0), + m_cOutstandingHandles(0), + m_clrInstanceId(clrInstanceId), + m_stopCount(0), + m_synchronized(false), + m_syncCompleteReceived(false), + m_pShim(pShim), + m_userThreads(11), + m_oddSync(false), +#ifdef FEATURE_INTEROP_DEBUGGING + m_unmanagedThreads(11), +#endif + m_appDomains(11), + m_sharedAppDomain(0), + m_steppers(11), + m_continueCounter(1), + m_flushCounter(0), + m_leftSideEventAvailable(NULL), + m_leftSideEventRead(NULL), +#if defined(FEATURE_INTEROP_DEBUGGING) + m_leftSideUnmanagedWaitEvent(NULL), +#endif // FEATURE_INTEROP_DEBUGGING + m_initialized(false), + m_stopRequested(false), + m_stopWaitEvent(NULL), +#ifdef FEATURE_INTEROP_DEBUGGING + m_cFirstChanceHijackedThreads(0), + m_unmanagedEventQueue(NULL), + m_lastQueuedUnmanagedEvent(NULL), + m_lastQueuedOOBEvent(NULL), + m_outOfBandEventQueue(NULL), + m_lastDispatchedIBEvent(NULL), + m_dispatchingUnmanagedEvent(false), + m_dispatchingOOBEvent(false), + m_doRealContinueAfterOOBBlock(false), + m_state(0), +#endif // FEATURE_INTEROP_DEBUGGING + m_helperThreadId(0), + m_pPatchTable(NULL), + m_cPatch(0), + m_rgData(NULL), + m_rgNextPatch(NULL), + m_rgUncommitedOpcode(NULL), + m_minPatchAddr(MAX_ADDRESS), + m_maxPatchAddr(MIN_ADDRESS), + m_iFirstPatch(0), + m_hHelperThread(NULL), + m_dispatchedEvent(DB_IPCE_DEBUGGER_INVALID), + m_pDefaultAppDomain(NULL), + m_hDacModule(hDacModule), + m_pDacPrimitives(NULL), + m_pEventChannel(NULL), + m_fAssertOnTargetInconsistency(false), + m_runtimeOffsetsInitialized(false), + m_writableMetadataUpdateMode(LegacyCompatPolicy) +{ + _ASSERTE((m_id == 0) == (pShim == NULL)); + + HRESULT hr = pDataTarget->QueryInterface(IID_ICorDebugDataTarget, reinterpret_cast<void **>(&m_pDACDataTarget)); + IfFailThrow(hr); + +#ifdef FEATURE_INTEROP_DEBUGGING + m_DbgSupport.m_DebugEventQueueIdx = 0; + m_DbgSupport.m_TotalNativeEvents = 0; + m_DbgSupport.m_TotalIB = 0; + m_DbgSupport.m_TotalOOB = 0; + m_DbgSupport.m_TotalCLR = 0; +#endif // FEATURE_INTEROP_DEBUGGING + + g_pRSDebuggingInfo->m_MRUprocess = this; + + // This is a strong reference to ourselves. + // This is cleared in code:CordbProcess::Neuter + m_pProcess.Assign(this); + +#ifdef _DEBUG + // On Debug builds, we'll ASSERT by default whenever the target appears to be corrupt or + // otherwise inconsistent (both in DAC and DBI). But we also need the ability to + // explicitly test corrupt targets. + // Tests should set COMPlus_DbgIgnoreInconsistentTarget=1 to suppress these asserts + // Note that this controls two things: + // 1) DAC behavior - see code:IDacDbiInterface::DacSetTargetConsistencyChecks + // 2) RS-only consistency asserts - see code:CordbProcess::TargetConsistencyCheck + if( !CLRConfig::GetConfigValue(CLRConfig::INTERNAL_DbgDisableTargetConsistencyAsserts) ) + { + m_fAssertOnTargetInconsistency = true; + } +#endif +} + +/* + A list of which resources owned by this object are accounted for. + + UNKNOWN + Cordb* m_cordb; + CordbHashTable m_unmanagedThreads; // Released in CordbProcess but not removed from hash + DebuggerIPCEvent* m_lastQueuedEvent; + + // CordbUnmannagedEvent is a struct which is not derrived from CordbBase. + // It contains a CordbUnmannagedThread which may need to be released. + CordbUnmanagedEvent *m_unmanagedEventQueue; + CordbUnmanagedEvent *m_lastQueuedUnmanagedEvent; + CordbUnmanagedEvent *m_outOfBandEventQueue; + CordbUnmanagedEvent *m_lastQueuedOOBEvent; + + BYTE* m_pPatchTable; + BYTE *m_rgData; + void *m_pbRemoteBuf; + + RESOLVED + // Nutered + CordbHashTable m_userThreads; + CordbHashTable m_appDomains; + + // Cleaned up in ExitProcess + DebuggerIPCEvent* m_queuedEventList; + + CordbHashTable m_steppers; // Closed in ~CordbProcess + + // Closed in CloseIPCEventHandles called from ~CordbProcess + HANDLE m_leftSideEventAvailable; + HANDLE m_leftSideEventRead; + + // Closed in ~CordbProcess + HANDLE m_handle; + HANDLE m_leftSideUnmanagedWaitEvent; + HANDLE m_stopWaitEvent; + + // Deleted in ~CordbProcess + CRITICAL_SECTION m_processMutex; + +*/ + + +CordbProcess::~CordbProcess() +{ + LOG((LF_CORDB, LL_INFO1000, "CP::~CP: deleting process 0x%08x\n", this)); + + DTOR_ENTRY(this); + + _ASSERTE(IsNeutered()); + + _ASSERTE(m_cordb == NULL); + + // We shouldn't still be in Cordb's list of processes. Unfortunately, our root Cordb object + // may have already been deleted b/c we're at the mercy of ref-counting, so we can't check. + + _ASSERTE(m_sharedAppDomain == NULL); + + m_processMutex.Destroy(); + m_StopGoLock.Destroy(); + + // These handles were cleared in neuter + _ASSERTE(m_handle == NULL); +#if defined(FEATURE_INTEROP_DEBUGGING) + _ASSERTE(m_leftSideUnmanagedWaitEvent == NULL); +#endif // FEATURE_INTEROP_DEBUGGING + _ASSERTE(m_stopWaitEvent == NULL); + + // Set this to mark that we really did cleanup. +} + +//----------------------------------------------------------------------------- +// Static build helper. +// This will create a process under the pCordb root, and add it to the list. +// We don't return the process - caller gets the pid and looks it up under +// the Cordb object. +// +// Arguments: +// pCordb - Pointer to the implementation of the owning Cordb object implementing the +// owning ICD interface. +// szProgramName - Name of the program to execute. +// szProgramArgs - Command line arguments for the process. +// lpProcessAttributes - OS-specific attributes for process creation. +// lpThreadAttributes - OS-specific attributes for thread creation. +// fInheritFlags - OS-specific flag for child process inheritance. +// dwCreationFlags - OS-specific creation flags. +// lpEnvironment - OS-specific environmental strings. +// szCurrentDirectory - OS-specific string for directory to run in. +// lpStartupInfo - OS-specific info on startup. +// lpProcessInformation - OS-specific process information buffer. +// corDebugFlags - What type of process to create, currently always managed. +//----------------------------------------------------------------------------- +HRESULT ShimProcess::CreateProcess( + Cordb * pCordb, + ICorDebugRemoteTarget * pRemoteTarget, + LPCWSTR szProgramName, + __in_z LPWSTR szProgramArgs, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL fInheritHandles, + DWORD dwCreationFlags, + PVOID lpEnvironment, + LPCWSTR szCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, + CorDebugCreateProcessFlags corDebugFlags +) +{ + _ASSERTE(pCordb != NULL); + +#if defined(FEATURE_DBGIPC_TRANSPORT_DI) + // The transport cannot deal with creating a suspended process (it needs the debugger to start up and + // listen for connections). + _ASSERTE((dwCreationFlags & CREATE_SUSPENDED) == 0); +#endif // FEATURE_DBGIPC_TRANSPORT_DI + + HRESULT hr = S_OK; + + RSExtSmartPtr<ShimProcess> pShim; + EX_TRY + { + pShim.Assign(new ShimProcess()); + + // Indicate that this process was started under the debugger as opposed to attaching later. + pShim->m_attached = false; + + hr = pShim->CreateAndStartWin32ET(pCordb); + IfFailThrow(hr); + + // Call out to newly created Win32-event Thread to create the process. + // If this succeeds, new CordbProcess will add a ref to the ShimProcess + hr = pShim->GetWin32EventThread()->SendCreateProcessEvent(pShim->GetMachineInfo(), + szProgramName, + szProgramArgs, + lpProcessAttributes, + lpThreadAttributes, + fInheritHandles, + dwCreationFlags, + lpEnvironment, + szCurrentDirectory, + lpStartupInfo, + lpProcessInformation, + corDebugFlags); + IfFailThrow(hr); + } + EX_CATCH_HRESULT(hr); + + // If this succeeds, then process takes ownership of thread. Else we need to kill it. + if (FAILED(hr)) + { + if (pShim != NULL) + { + pShim->Dispose(); + } + } + // Always release our ref to ShimProcess. If the Process was created, then it takes a reference. + + return hr; +} + +//----------------------------------------------------------------------------- +// Static build helper for the attach case. +// On success, this will add the process to the pCordb list, and then +// callers can look it up there by pid. +// +// Arguments: +// pCordb - root under which this all lives +// dwProcessID - OS process ID to attach to +// fWin32Attach - are we interop debugging? +//----------------------------------------------------------------------------- +HRESULT ShimProcess::DebugActiveProcess( + Cordb * pCordb, + ICorDebugRemoteTarget * pRemoteTarget, + DWORD dwProcessID, + BOOL fWin32Attach +) +{ + _ASSERTE(pCordb != NULL); + + HRESULT hr = S_OK; + + RSExtSmartPtr<ShimProcess> pShim; + + EX_TRY + { + pShim.Assign(new ShimProcess()); + + // Indicate that this process was attached to, asopposed to being started under the debugger. + pShim->m_attached = true; + + hr = pShim->CreateAndStartWin32ET(pCordb); + IfFailThrow(hr); + + // If this succeeds, new CordbProcess will add a ref to the ShimProcess + hr = pShim->GetWin32EventThread()->SendDebugActiveProcessEvent(pShim->GetMachineInfo(), + dwProcessID, + fWin32Attach == TRUE, + NULL); + IfFailThrow(hr); + + _ASSERTE(SUCCEEDED(hr)); + +#if !defined(FEATURE_DBGIPC_TRANSPORT_DI) + // Don't do this when we are remote debugging since we won't be getting the loader breakpoint. + // We don't support JIT attach in remote debugging scenarios anyway. + // + // When doing jit attach for pure managed debugging we allow the native attach event to be signaled + // after DebugActiveProcess completes which means we must wait here long enough to have set the debuggee + // bit indicating managed attach is coming. + // However in interop debugging we can't do that because there are debug events which come before the + // loader breakpoint (which is how far we need to get to set the debuggee bit). If we blocked + // DebugActiveProcess there then the debug events would be refering to an ICorDebugProcess that hasn't + // yet been returned to the caller of DebugActiveProcess. Instead, for interop debugging we force the + // native debugger to wait until it gets the loader breakpoint to set the event. Note we can't converge + // on that solution for the pure managed case because there is no loader breakpoint event. Hence pure + // managed and interop debugging each require their own solution + // + // See bugs Dev10 600873 and 595322 for examples of what happens if we wait in interop or don't wait + // in pure managed respectively + // + // Long term this should all go away because we won't need to set a managed attach pending bit because + // there shouldn't be any IPC events involved in managed attach. There might not even be a notion of + // being 'managed attached' + if(!pShim->m_fIsInteropDebugging) + { + DWORD dwHandles = 2; + HANDLE arrHandles[2]; + + arrHandles[0] = pShim->m_terminatingEvent; + arrHandles[1] = pShim->m_markAttachPendingEvent; + + // Wait for the completion of marking pending attach bit or debugger detaching + WaitForMultipleObjectsEx(dwHandles, arrHandles, FALSE, INFINITE, FALSE); + } +#endif //!FEATURE_DBGIPC_TRANSPORT_DI + } + EX_CATCH_HRESULT(hr); + + // If this succeeds, then process takes ownership of thread. Else we need to kill it. + if (FAILED(hr)) + { + if (pShim!= NULL) + { + pShim->Dispose(); + } + } + + // Always release our ref to ShimProcess. If the Process was created, then it takes a reference. + + return hr; +} + +//----------------------------------------------------------------------------- +// Neuter all of all children, but not the actual process object. +// +// Assumptions: +// This clears Right-side state. Assumptions about left-side state are either: +// 1. We're in a shutdown scenario, where all left-side state is already +// freed. +// 2. Caller already verified there are no left-side resources (eg, by calling +// code:CordbProcess::IsReadyForDetach) +// 3. Caller did code:CordbProcess::NeuterLeftSideResources first +// to clean up left-side resources. +// +// Notes: +// This could be called multiple times (code:CordbProcess::FlushAll), so +// be sure to null out any potential dangling pointers. State may be rebuilt +// up after each time. +void CordbProcess::NeuterChildren() +{ + _ASSERTE(GetProcessLock()->HasLock()); + + // Frees left-side resources. See assumptions above. + m_LeftSideResourceCleanupList.NeuterAndClear(this); + + + m_EvalTable.Clear(); + + + // Sweep neuter lists. + m_ExitNeuterList.NeuterAndClear(this); + m_ContinueNeuterList.NeuterAndClear(this); + + m_userThreads.NeuterAndClear(GetProcessLock()); + + m_pDefaultAppDomain = NULL; + + // Frees per-appdomain left-side resources. See assumptions above. + m_appDomains.NeuterAndClear(GetProcessLock()); + if (m_sharedAppDomain != NULL) + { + m_sharedAppDomain->Neuter(); + m_sharedAppDomain->InternalRelease(); + m_sharedAppDomain = NULL; + } + + m_steppers.NeuterAndClear(GetProcessLock()); + +#ifdef FEATURE_INTEROP_DEBUGGING + m_unmanagedThreads.NeuterAndClear(GetProcessLock()); +#endif // FEATURE_INTEROP_DEBUGGING + + // Explicitly keep the Win32EventThread alive so that we can use it in the window + // between NeuterChildren + Neuter. +} + +//----------------------------------------------------------------------------- +// Neuter +// +// When the process dies, remove all the resources associated with this object. +// +// Notes: +// Once we neuter ourself, we can no longer send IPC events. So this is useful +// on detach. This will be called on FlushAll (which has Whidbey detach +// semantics) +//----------------------------------------------------------------------------- +void CordbProcess::Neuter() +{ + // Process's Neuter is at the top of the neuter tree. So we take the process-lock + // here and then all child items (appdomains, modules, etc) will assert + // that they hold the lock. + _ASSERTE(!this->ThreadHoldsProcessLock()); + + // Take the process lock. + RSLockHolder lockHolder(GetProcessLock()); + + + NeuterChildren(); + + // Release the metadata interfaces + m_pMetaDispenser.Clear(); + + + if (m_hHelperThread != NULL) + { + CloseHandle(m_hHelperThread); + m_hHelperThread = NULL; + } + + { + lockHolder.Release(); + { + // We may still hold the Stop-Go lock. + // @dbgtodo - left-side resources / shutdown, shim: Currently + // the shim shutdown is too interwoven with CordbProcess to split + // it out from the locks. Must fully hoist the W32ET and make + // it safely outside the RS, and outside the protection of RS + // locks. + PUBLIC_API_UNSAFE_ENTRY_FOR_SHIM(this); + + // Now that all of our children are neutered, it should be safe to kill the W32ET. + // Shutdown the shim, and this will also shutdown the W32ET. + // Do this outside of the process-lock so that we can shutdown the + // W23ET. + if (m_pShim != NULL) + { + m_pShim->Dispose(); + m_pShim.Clear(); + } + } + + lockHolder.Acquire(); + } + + // Unload DAC, and then release our final data target references + FreeDac(); + m_pDACDataTarget.Clear(); + m_pMutableDataTarget.Clear(); + m_pMetaDataLocator.Clear(); + + if (m_pEventChannel != NULL) + { + m_pEventChannel->Delete(); + m_pEventChannel = NULL; + } + + // Need process lock to clear the patch table + ClearPatchTable(); + + CordbProcess::CloseIPCHandles(); + + CordbBase::Neuter(); + + m_cordb.Clear(); + + // Need to release this reference to ourselves. Other leaf objects may still hold + // strong references back to this CordbProcess object. + _ASSERTE(m_pProcess == this); + m_pProcess.Clear(); +} + +// Wrapper to return metadata dispenser. +// +// Notes: +// Does not adjust reference count of dispenser. +// Dispenser is destroyed in code:CordbProcess::Neuter +// Dispenser is non-null. +IMetaDataDispenserEx * CordbProcess::GetDispenser() +{ + _ASSERTE(m_pMetaDispenser != NULL); + return m_pMetaDispenser; +} + + +void CordbProcess::CloseIPCHandles() +{ + INTERNAL_API_ENTRY(this); + + // Close off Right Side's handles. + if (m_leftSideEventAvailable != NULL) + { + CloseHandle(m_leftSideEventAvailable); + m_leftSideEventAvailable = NULL; + } + + if (m_leftSideEventRead != NULL) + { + CloseHandle(m_leftSideEventRead); + m_leftSideEventRead = NULL; + } + + if (m_handle != NULL) + { + // @dbgtodo - We should probably add asserts to all calls to CloseHandles(), but this has been + // a particularly problematic spot in the past for Mac debugging. + BOOL fSuccess = CloseHandle(m_handle); + (void)fSuccess; //prevent "unused variable" error from GCC + _ASSERTE(fSuccess); + + m_handle = NULL; + } + +#if defined(FEATURE_INTEROP_DEBUGGING) + if (m_leftSideUnmanagedWaitEvent != NULL) + { + CloseHandle(m_leftSideUnmanagedWaitEvent); + m_leftSideUnmanagedWaitEvent = NULL; + } +#endif // FEATURE_INTEROP_DEBUGGING + + if (m_stopWaitEvent != NULL) + { + CloseHandle(m_stopWaitEvent); + m_stopWaitEvent = NULL; + } +} + + +//----------------------------------------------------------------------------- +// Create new OS Thread for the Win32 Event Thread (the thread used in interop-debugging to sniff +// native debug events). This is 1:1 w/ a CordbProcess object. +// This will then be used to actuall create the CordbProcess object. +// The process object will then take ownership of the thread. +// +// Arguments: +// pCordb - the root object that the process lives under +// +// Return values: +// S_OK on success. +//----------------------------------------------------------------------------- +HRESULT ShimProcess::CreateAndStartWin32ET(Cordb * pCordb) +{ + + // + // Create the win32 event listening thread + // + CordbWin32EventThread * pWin32EventThread = new (nothrow) CordbWin32EventThread(pCordb, this); + + HRESULT hr = S_OK; + + if (pWin32EventThread != NULL) + { + hr = pWin32EventThread->Init(); + + if (SUCCEEDED(hr)) + { + hr = pWin32EventThread->Start(); + } + + if (FAILED(hr)) + { + delete pWin32EventThread; + pWin32EventThread = NULL; + } + } + else + { + hr = E_OUTOFMEMORY; + } + + m_pWin32EventThread = pWin32EventThread; + return ErrWrapper(hr); +} + + +//--------------------------------------------------------------------------------------- +// +// Try to initialize the DAC. Called in scenarios where it may fail. +// +// Return Value: +// TRUE - DAC is initialized. +// FALSE - Not initialized, but can try again later. Common case if +// target has not yet loaded the runtime. +// Throws exception - fatal. +// +// Assumptions: +// Target is stopped by OS, so we can safely inspect it without it moving on us. +// +// Notes: +// This can be called eagerly to sniff if the LS is initialized. +// +//--------------------------------------------------------------------------------------- +BOOL CordbProcess::TryInitializeDac() +{ + CONTRACTL + { + THROWS; + } + CONTRACTL_END; + + // Target is stopped by OS, so we can safely inspect it without it moving on us. + + // We want to avoid exceptions in the normal case, so we do some pre-checks + // to detect failure without relying on exceptions. + // Can't initialize DAC until mscorwks is loaded. So that's a sanity test. + HRESULT hr = EnsureClrInstanceIdSet(); + if (FAILED(hr)) + { + return FALSE; + } + + // By this point, we know which CLR in the target to debug. That means there is a CLR + // in the target, and it's safe to initialize DAC. + _ASSERTE(m_clrInstanceId != 0); + + // Now expect it to succeed + InitializeDac(); + return TRUE; +} + +//--------------------------------------------------------------------------------------- +// +// Load & Init DAC, expecting to succeed. +// +// Return Value: +// Throws on failure. +// +// Assumptions: +// Caller invokes this at a point where they can expect it to succeed. +// This is called early in the startup path because DAC is needed for accessing +// data in the target. +// +// Notes: +// This needs to succeed, and should always succeed (baring a bad installation) +// so we assert on failure paths. +// This may be called mutliple times. +// +//--------------------------------------------------------------------------------------- +void CordbProcess::InitializeDac() +{ + CONTRACTL + { + THROWS; + } + CONTRACTL_END; + INTERNAL_API_ENTRY(this); + + // For Mac debugginger, m_hDacModule is not used, and it will always be NULL. To check whether DAC has + // been initialized, we need to check something else, namely m_pDacPrimitives. + if (m_pDacPrimitives == NULL) + { + LOG((LF_CORDB, LL_INFO1000, "About to load DAC\n")); + CreateDacDbiInterface(); // throws + } + else + { + LOG((LF_CORDB, LL_INFO1000, "Dac already loaded, 0x%p\n", (HMODULE)m_hDacModule)); + } + + // Always flush dac. + ForceDacFlush(); +} + +//--------------------------------------------------------------------------------------- +// +// Free DAC resources +// +// Notes: +// This should clean up state such that code:CordbProcess::InitializeDac could be called again. +// +//--------------------------------------------------------------------------------------- +void CordbProcess::FreeDac() +{ + CONTRACTL + { + NOTHROW; // backout code. + } + CONTRACTL_END; + + if (m_pDacPrimitives != NULL) + { + m_pDacPrimitives->Destroy(); + m_pDacPrimitives = NULL; + } + + if (m_hDacModule != NULL) + { + LOG((LF_CORDB, LL_INFO1000, "Unloading DAC\n")); + m_hDacModule.Clear(); + } +} + +IEventChannel * CordbProcess::GetEventChannel() +{ + _ASSERTE(m_pEventChannel != NULL); + return m_pEventChannel; +} + +//--------------------------------------------------------------------------------------- +// Mark that the process is being interop-debugged. +// +// Notes: +// @dbgtodo shim: this should eventually move into the shim or go away. +// It's only to support V2 legacy interop-debugging. +// Called after code:CordbProcess::Init if we want to enable interop debugging. +// This allows us to separate out Interop-debugging flags from the core initialization, +// and paves the way for us to eventually remove it. +// +// Since we're always on the naitve-pipeline, the Enabling interop debugging just changes +// how the native debug events are being handled. So this must be called after Init, but +// before any events are actually handled. +// This mus be calle on the win32 event thread to gaurantee that it's called before WFDE. +void CordbProcess::EnableInteropDebugging() +{ + CONTRACTL + { + THROWS; + PRECONDITION(m_pShim != NULL); + } + CONTRACTL_END; + + // Must be on W32ET to gaurantee that we're called after Init yet before WFDE (which + // are both called on the W32et). + _ASSERTE(IsWin32EventThread()); +#ifdef FEATURE_INTEROP_DEBUGGING + + m_state |= PS_WIN32_ATTACHED; + if (GetDCB() != NULL) + { + GetDCB()->m_rightSideIsWin32Debugger = true; + UpdateLeftSideDCBField(&(GetDCB()->m_rightSideIsWin32Debugger), sizeof(GetDCB()->m_rightSideIsWin32Debugger)); + } + + // Tell the Shim we're interop-debugging. + m_pShim->SetIsInteropDebugging(true); +#else + ThrowHR(CORDBG_E_INTEROP_NOT_SUPPORTED); +#endif +} + +//--------------------------------------------------------------------------------------- +// +// Init -- create any objects that the process object needs to operate. +// +// Arguments: +// +// Return Value: +// S_OK on success +// +// Assumptions: +// Called on Win32 Event Thread, after OS debugging pipeline is established but +// before WaitForDebugEvent / ContinueDebugEvent. This means the target is stopped. +// +// Notes: +// To enable interop-debugging, call code:CordbProcess::EnableInteropDebugging +//--------------------------------------------------------------------------------------- +HRESULT CordbProcess::Init() +{ + INTERNAL_API_ENTRY(this); + + HRESULT hr = S_OK; + BOOL fIsLSStarted = FALSE; // see meaning below. + + FAIL_IF_NEUTERED(this); + + + EX_TRY + { + m_processMutex.Init("Process Lock", RSLock::cLockReentrant, RSLock::LL_PROCESS_LOCK); + m_StopGoLock.Init("Stop-Go Lock", RSLock::cLockReentrant, RSLock::LL_STOP_GO_LOCK); + +#ifdef _DEBUG + m_appDomains.DebugSetRSLock(GetProcessLock()); + m_userThreads.DebugSetRSLock(GetProcessLock()); +#ifdef FEATURE_INTEROP_DEBUGGING + m_unmanagedThreads.DebugSetRSLock(GetProcessLock()); +#endif + m_steppers.DebugSetRSLock(GetProcessLock()); +#endif + + // See if the data target is mutable, and cache the mutable interface if it is + // We must initialize this before we try to use the data target to access the memory in the target process. + m_pMutableDataTarget.Clear(); // if we were called already, release + hr = m_pDACDataTarget->QueryInterface(IID_ICorDebugMutableDataTarget, (void**)&m_pMutableDataTarget); + if (!SUCCEEDED(hr)) + { + // The data target doesn't support mutation. We'll fail any requests that require mutation. + m_pMutableDataTarget.Assign(new ReadOnlyDataTargetFacade()); + } + + m_pMetaDataLocator.Clear(); + hr = m_pDACDataTarget->QueryInterface(IID_ICorDebugMetaDataLocator, reinterpret_cast<void **>(&m_pMetaDataLocator)); + + // Get the metadata dispenser. + hr = InternalCreateMetaDataDispenser(IID_IMetaDataDispenserEx, (void **)&m_pMetaDispenser); + + // We statically link in the dispenser. We expect it to succeed, except for OOM, which + // debugger doesn't yet handle. + SIMPLIFYING_ASSUMPTION_SUCCEEDED(hr); + IfFailThrow(hr); + + _ASSERTE(m_pMetaDispenser != NULL); + + // In order to allow users to call the metadata reader from multiple threads we need to set + // a flag on the dispenser to create threadsafe readers. This is done best-effort but + // really shouldn't ever fail. See issue 696511. + VARIANT optionValue; + VariantInit(&optionValue); + V_VT(&optionValue) = VT_UI4; + V_UI4(&optionValue) = MDThreadSafetyOn; + m_pMetaDispenser->SetOption(MetaDataThreadSafetyOptions, &optionValue); + + // + // Setup internal events. + // @dbgtodo shim: these events should eventually be in the shim. + // + + + // Managed debugging is built on the native-pipeline, and that will detect against double-attaches. + + // @dbgtodo shim: In V2, LSEA + LSER were used by the LS's helper thread. Now with the V3 pipeline, + // that helper-thread uses native-debug events. The W32ET gets those events and then uses LSEA, LSER to + // signal existing RS infrastructure. Eventually get rid of LSEA, LSER completely. + // + + m_leftSideEventAvailable = WszCreateEvent(NULL, FALSE, FALSE, NULL); + if (m_leftSideEventAvailable == NULL) + { + ThrowLastError(); + } + + m_leftSideEventRead = WszCreateEvent(NULL, FALSE, FALSE, NULL); + if (m_leftSideEventRead == NULL) + { + ThrowLastError(); + } + + m_stopWaitEvent = WszCreateEvent(NULL, TRUE, FALSE, NULL); + if (m_stopWaitEvent == NULL) + { + ThrowLastError(); + } + + if (m_pShim != NULL) + { + // Get a handle to the debuggee. + // This is not needed in the V3 pipeline because we don't assume we have a live, local, process. + m_handle = GetShim()->GetNativePipeline()->GetProcessHandle(); + + if (m_handle == NULL) + { + ThrowLastError(); + } + } + + // The LS startup goes through the following phases: + // 1) mscorwks not yet loaded (eg, any unmanaged app) + // 2) mscorwks loaded (DAC can now be used) + // 3) IPC Block created at OS level + // 4) IPC block data initialized (so we can read meainingful data from it) + // 5) LS marks that it's initialized (queryable by a DAC primitive) (may not be atomic) + // 6) LS fires a "Startup" exception (sniffed by WFDE). + // + // LS is currently stopped by OS debugging, so it's doesn't shift phases. + // From the RS's perspective: + // - after phase 5 is an attach + // - before phase 6 is a launch. + // This means there's an overlap: if we catch it at phase 5, we'll just get + // an extra Startup exception from phase 6, which is safe. This overlap is good + // because it means there's no bad window to do an attach in. + + // fIsLSStarted means before phase 6 (eg, RS should expect a startup exception) + + // Determines if the LS is started. + + { + BOOL fReady = TryInitializeDac(); + + if (fReady) + { + // Invoke DAC primitive. + _ASSERTE(m_pDacPrimitives != NULL); + fIsLSStarted = m_pDacPrimitives->IsLeftSideInitialized(); + } + else + { + _ASSERTE(m_pDacPrimitives == NULL); + + // DAC is not yet loaded, so we're at least before phase 2, which is before phase 6. + // So leave fIsLSStarted = false. We'll get a startup exception later. + _ASSERTE(!fIsLSStarted); + } + } + + + if (fIsLSStarted) + { + // Left-side has started up. This is common for Attach cases when managed-code is already running. + + if (m_pShim != NULL) + { + FinishInitializeIPCChannelWorker(); // throws + + // At this point, the control block is complete and all four + // events are available and valid for the remote process. + + // Request that the process object send an Attach IPC event. + // This is only used in an attach case. + // @dbgtodo sync: this flag can go away once the + // shim can use real sync APIs. + m_fDoDelayedManagedAttached = true; + } + else + { + // In the V3 pipeline case, if we have the DD-interface, then the runtime is loaded + // and we consider it initialized. + if (IsDacInitialized()) + { + m_initialized = true; + } + } + } + else + { + // LS is not started yet. This would be common for "Launch" cases. + // We will get a Startup Exception notification when it does start. + } + } + EX_CATCH_HRESULT(hr); + + if (FAILED(hr)) + { + CleanupHalfBakedLeftSide(); + } + + return hr; +} + + +COM_METHOD CordbProcess::CanCommitChanges(ULONG cSnapshots, + ICorDebugEditAndContinueSnapshot *pSnapshots[], + ICorDebugErrorInfoEnum **pError) +{ + return E_NOTIMPL; +} + +COM_METHOD CordbProcess::CommitChanges(ULONG cSnapshots, + ICorDebugEditAndContinueSnapshot *pSnapshots[], + ICorDebugErrorInfoEnum **pError) +{ + return E_NOTIMPL; +} + + +// +// Terminating -- places the process into the terminated state. This should +// also get any blocking process functions unblocked so they'll return +// a failure code. +// +void CordbProcess::Terminating(BOOL fDetach) +{ + INTERNAL_API_ENTRY(this); + + LOG((LF_CORDB, LL_INFO1000,"CP::T: Terminating process 0x%x detach=%d\n", m_id, fDetach)); + m_terminated = true; + + m_cordb->ProcessStateChanged(); + + // Set events that may be blocking stuff. + // But don't set RSER unless we actually read the event. We don't block on RSER + // since that wait also checks the leftside's process handle. + SetEvent(m_leftSideEventRead); + SetEvent(m_leftSideEventAvailable); + SetEvent(m_stopWaitEvent); + + if (m_pShim != NULL) + m_pShim->SetTerminatingEvent(); + + if (fDetach && (m_pEventChannel != NULL)) + { + m_pEventChannel->Detach(); + } +} + + +// Wrapper to give shim access to code:CordbProcess::QueueManagedAttachIfNeededWorker +void CordbProcess::QueueManagedAttachIfNeeded() +{ + PUBLIC_API_ENTRY_FOR_SHIM(this); + QueueManagedAttachIfNeededWorker(); +} + +//--------------------------------------------------------------------------------------- +// Hook from Shim to request a managed attach IPC event +// +// Notes: +// Called by shim after the loader-breakpoint is handled. +// @dbgtodo sync: ths should go away once the shim can initiate +// a sync +void CordbProcess::QueueManagedAttachIfNeededWorker() +{ + HRESULT hrQueue = S_OK; + + // m_fDoDelayedManagedAttached ensures that we only send an Attach event if the LS is actually present. + if (m_fDoDelayedManagedAttached && GetShim()->GetAttached()) + { + RSLockHolder lockHolder(&this->m_processMutex); + GetDAC()->MarkDebuggerAttachPending(); + + hrQueue = this->QueueManagedAttach(); + } + + if (m_pShim != NULL) + m_pShim->SetMarkAttachPendingEvent(); + + IfFailThrow(hrQueue); +} + +//--------------------------------------------------------------------------------------- +// +// QueueManagedAttach +// +// Send a managed attach. This is asynchronous and will return immediately. +// +// Return Value: +// S_OK on success +// +//--------------------------------------------------------------------------------------- +HRESULT CordbProcess::QueueManagedAttach() +{ + INTERNAL_API_ENTRY(this); + + _ASSERTE(ThreadHoldsProcessLock()); + + _ASSERTE(m_fDoDelayedManagedAttached); + m_fDoDelayedManagedAttached = false; + + _ASSERTE(IsDacInitialized()); + + // We don't know what Queue it. + SendAttachProcessWorkItem * pItem = new (nothrow) SendAttachProcessWorkItem(this); + + if (pItem == NULL) + { + return E_OUTOFMEMORY; + } + + this->m_cordb->m_rcEventThread->QueueAsyncWorkItem(pItem); + + return S_OK; +} + +// However, we still want to synchronize. +// @dbgtodo sync: when we hoist attaching, we can send an DB_IPCE_ASYNC_BREAK event instead or Attach +// (for V2 semantics, we still need to synchronize the process)? +void SendAttachProcessWorkItem::Do() +{ + HRESULT hr; + + // This is being processed on the RCET, where it's safe to take the Stop-Go lock. + RSLockHolder ch(this->GetProcess()->GetStopGoLock()); + + DebuggerIPCEvent *event = (DebuggerIPCEvent*) _alloca(CorDBIPC_BUFFER_SIZE); + + // This just acts like an async-break, which will kick off things. + // This will not induce any faked attach events from the VM (like it did in V2). + // The Left-side will still slip foward allowing the async-break to happen, so + // we may get normal debug events in addition to the sync-complete. + // + // 1. In the common attach case, we should just get a sync-complete. + // 2. In Jit-attach cases, the LS is sending an event, and so we'll get that event and then the sync-complete. + GetProcess()->InitAsyncIPCEvent(event, DB_IPCE_ATTACHING, VMPTR_AppDomain::NullPtr()); + + // This should result in a sync-complete from the Left-side, which will be raised as an exception + // that the debugger passes into Filter and then internally goes through code:CordbProcess::TriageSyncComplete + // and that triggers code:CordbRCEventThread::FlushQueuedEvents to be called on the RCET. + // We already pre-queued a fake CreateProcess event. + + // The left-side will also mark itself as attached in response to this event. + // We explicitly don't mark it as attached from the right-side because we want to let the left-side + // synchronize first (to stop all running threads) before marking the debugger as attached. + LOG((LF_CORDB, LL_INFO1000, "[%x] CP::S: sending attach.\n", GetCurrentThreadId())); + + hr = GetProcess()->SendIPCEvent(event, CorDBIPC_BUFFER_SIZE); + + LOG((LF_CORDB, LL_INFO1000, "[%x] CP::S: sent attach.\n", GetCurrentThreadId())); +} + +//--------------------------------------------------------------------------------------- +// Try to lookup a cached thread object +// +// Arguments: +// vmThread - vm identifier for thread. +// +// Returns: +// Thread object if cached; null if not yet cached. +// +// Notes: +// This does not create the thread object if it's not cached. Caching is unpredictable, +// and so this may appear to randomly return NULL. +// Callers should prefer code:CordbProcess::LookupOrCreateThread unless they expicitly +// want to check RS state. +CordbThread * CordbProcess::TryLookupThread(VMPTR_Thread vmThread) +{ + return m_userThreads.GetBase(VmPtrToCookie(vmThread)); +} + +//--------------------------------------------------------------------------------------- +// Lookup (or create) a CordbThread object by the given volatile OS id. Returns null if not a manged thread +// +// Arguments: +// dwThreadId - os thread id that a managed thread may be using. +// +// Returns: +// Thread instance if there is currently a managed thread scheduled to run on dwThreadId. +// NULL if this tid is not a valid Managed thread. (This is considered a common case) +// Throws on error. +// +// Notes: +// OS Thread ID is not fiber-safe, so this is a dangerous function to call. +// Avoid this as much as possible. Prefer using VMPTR_Thread and +// code:CordbProcess::LookupOrCreateThread instead of OS thread IDs. +// See code:CordbThread::GetID for details. +CordbThread * CordbProcess::TryLookupOrCreateThreadByVolatileOSId(DWORD dwThreadId) +{ + PrepopulateThreadsOrThrow(); + return TryLookupThreadByVolatileOSId(dwThreadId); +} + +//--------------------------------------------------------------------------------------- +// Lookup a cached CordbThread object by the tid. Returns null if not in the cache (which +// includes unmanged thread) +// +// Arguments: +// dwThreadId - os thread id that a managed thread may be using. +// +// Returns: +// Thread instance if there is currently a managed thread scheduled to run on dwThreadId. +// NULL if this tid is not a valid Managed thread. (This is considered a common case) +// Throws on error. +// +// Notes: +// Avoids this method: +// * OS Thread ID is not fiber-safe, so this is a dangerous function to call. +// * This is juts a Lookup, not LookupOrCreate, so it should only be used by methods +// that care about the RS state (instead of just LS state). +// Prefer using VMPTR_Thread and code:CordbProcess::LookupOrCreateThread +// +CordbThread * CordbProcess::TryLookupThreadByVolatileOSId(DWORD dwThreadId) +{ + HASHFIND find; + for (CordbThread * pThread = m_userThreads.FindFirst(&find); + pThread != NULL; + pThread = m_userThreads.FindNext(&find)) + { + _ASSERTE(pThread != NULL); + + // Get the OS tid. This returns 0 if the thread is switched out. + DWORD dwThreadId2 = GetDAC()->TryGetVolatileOSThreadID(pThread->m_vmThreadToken); + if (dwThreadId2 == dwThreadId) + { + return pThread; + } + } + + // This OS thread ID does not match any managed thread id. + return NULL; +} + +//--------------------------------------------------------------------------------------- +// Preferred CordbThread lookup routine. +// +// Arguments: +// vmThread - LS thread to lookup. Must be non-null. +// +// Returns: +// CordbThread instance for given vmThread. May return a previously cached +// instance or create a new instance. Never returns NULL. +// Throw on error. +CordbThread * CordbProcess::LookupOrCreateThread(VMPTR_Thread vmThread) +{ + _ASSERTE(!vmThread.IsNull()); + + // Return if we have an existing instance. + CordbThread * pReturn = TryLookupThread(vmThread); + if (pReturn != NULL) + { + return pReturn; + } + + RSInitHolder<CordbThread> pThread(new CordbThread(this, vmThread)); // throws + pReturn = pThread.TransferOwnershipToHash(&m_userThreads); + + return pReturn; +} + + + + +HRESULT CordbProcess::QueryInterface(REFIID id, void **pInterface) +{ + if (id == IID_ICorDebugProcess) + { + *pInterface = static_cast<ICorDebugProcess*>(this); + } + else if (id == IID_ICorDebugController) + { + *pInterface = static_cast<ICorDebugController*>(static_cast<ICorDebugProcess*>(this)); + } + else if (id == IID_ICorDebugProcess2) + + { + *pInterface = static_cast<ICorDebugProcess2*>(this); + } + else if (id == IID_ICorDebugProcess3) + { + *pInterface = static_cast<ICorDebugProcess3*>(this); + } + else if (id == IID_ICorDebugProcess4) + { + *pInterface = static_cast<ICorDebugProcess4*>(this); + } + else if (id == IID_ICorDebugProcess5) + { + *pInterface = static_cast<ICorDebugProcess5*>(this); + } + else if (id == IID_ICorDebugProcess7) + { + *pInterface = static_cast<ICorDebugProcess7*>(this); + } + else if (id == IID_ICorDebugProcess8) + { + *pInterface = static_cast<ICorDebugProcess8*>(this); + } +#ifdef FEATURE_LEGACYNETCF_DBG_HOST_CONTROL + else if (id == IID_ICorDebugLegacyNetCFHostCallbackInvoker_PrivateWindowsPhoneOnly) + { + *pInterface = static_cast<ICorDebugLegacyNetCFHostCallbackInvoker_PrivateWindowsPhoneOnly*>(this); + } +#endif + else if (id == IID_IUnknown) + { + *pInterface = static_cast<IUnknown*>(static_cast<ICorDebugProcess*>(this)); + } + + else + { + *pInterface = NULL; + return E_NOINTERFACE; + } + + ExternalAddRef(); + return S_OK; +} + + + + +// Public implementation of ICorDebugProcess4::ProcessStateChanged +HRESULT CordbProcess::ProcessStateChanged(CorDebugStateChange eChange) +{ + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this) + { + switch(eChange) + { + case PROCESS_RUNNING: + FlushProcessRunning(); + break; + + case FLUSH_ALL: + FlushAll(); + break; + + default: + ThrowHR(E_INVALIDARG); + + } + } + PUBLIC_API_END(hr); + return hr; +} + + +HRESULT CordbProcess::EnumerateHeap(ICorDebugHeapEnum **ppObjects) +{ + if (!ppObjects) + return E_POINTER; + + HRESULT hr = S_OK; + PUBLIC_API_ENTRY(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + + EX_TRY + { + if (m_pDacPrimitives->AreGCStructuresValid()) + { + CordbHeapEnum *pHeapEnum = new CordbHeapEnum(this); + GetContinueNeuterList()->Add(this, pHeapEnum); + hr = pHeapEnum->QueryInterface(__uuidof(ICorDebugHeapEnum), (void**)ppObjects); + } + else + { + hr = CORDBG_E_GC_STRUCTURES_INVALID; + } + } + EX_CATCH_HRESULT(hr); + + return hr; +} + +HRESULT CordbProcess::GetGCHeapInformation(COR_HEAPINFO *pHeapInfo) +{ + if (!pHeapInfo) + return E_INVALIDARG; + + HRESULT hr = S_OK; + PUBLIC_API_ENTRY(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + + EX_TRY + { + GetDAC()->GetGCHeapInformation(pHeapInfo); + } + EX_CATCH_HRESULT(hr); + + return hr; +} + +HRESULT CordbProcess::EnumerateHeapRegions(ICorDebugHeapSegmentEnum **ppRegions) +{ + if (!ppRegions) + return E_INVALIDARG; + + HRESULT hr = S_OK; + PUBLIC_API_ENTRY(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + + EX_TRY + { + DacDbiArrayList<COR_SEGMENT> segments; + hr = GetDAC()->GetHeapSegments(&segments); + + if (SUCCEEDED(hr)) + { + if (!segments.IsEmpty()) + { + CordbHeapSegmentEnumerator *segEnum = new CordbHeapSegmentEnumerator(this, &segments[0], (DWORD)segments.Count()); + GetContinueNeuterList()->Add(this, segEnum); + hr = segEnum->QueryInterface(__uuidof(ICorDebugHeapSegmentEnum), (void**)ppRegions); + } + else + { + hr = E_OUTOFMEMORY; + } + } + } + EX_CATCH_HRESULT(hr); + + return hr; +} + +HRESULT CordbProcess::GetObject(CORDB_ADDRESS addr, ICorDebugObjectValue **pObject) +{ + HRESULT hr = S_OK; + + PUBLIC_REENTRANT_API_ENTRY(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + + EX_TRY + { + if (!m_pDacPrimitives->IsValidObject(addr)) + { + hr = CORDBG_E_CORRUPT_OBJECT; + } + else if (pObject == NULL) + { + hr = E_INVALIDARG; + } + else + { + RSLockHolder ch(GetProcess()->GetStopGoLock()); + RSLockHolder procLock(this->GetProcess()->GetProcessLock()); + + CordbAppDomain *cdbAppDomain = NULL; + CordbType *pType = NULL; + hr = GetTypeForObject(addr, &pType, &cdbAppDomain); + + if (SUCCEEDED(hr)) + { + _ASSERTE(pType != NULL); + _ASSERTE(cdbAppDomain != NULL); + + DebuggerIPCE_ObjectData objData; + m_pDacPrimitives->GetBasicObjectInfo(addr, ELEMENT_TYPE_CLASS, cdbAppDomain->GetADToken(), &objData); + + NewHolder<CordbObjectValue> pNewObjectValue(new CordbObjectValue(cdbAppDomain, pType, TargetBuffer(addr, (ULONG)objData.objSize), &objData)); + hr = pNewObjectValue->Init(); + + if (SUCCEEDED(hr)) + { + hr = pNewObjectValue->QueryInterface(__uuidof(ICorDebugObjectValue), (void**)pObject); + if (SUCCEEDED(hr)) + pNewObjectValue.SuppressRelease(); + } + } + } + } + EX_CATCH_HRESULT(hr); + + return hr; +} + + +HRESULT CordbProcess::EnumerateGCReferences(BOOL enumerateWeakReferences, ICorDebugGCReferenceEnum **ppEnum) +{ + if (!ppEnum) + return E_POINTER; + + HRESULT hr = S_OK; + PUBLIC_API_ENTRY(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + + EX_TRY + { + CordbRefEnum *pRefEnum = new CordbRefEnum(this, enumerateWeakReferences); + GetContinueNeuterList()->Add(this, pRefEnum); + hr = pRefEnum->QueryInterface(IID_ICorDebugGCReferenceEnum, (void**)ppEnum); + } + EX_CATCH_HRESULT(hr); + return hr; +} + +HRESULT CordbProcess::EnumerateHandles(CorGCReferenceType types, ICorDebugGCReferenceEnum **ppEnum) +{ + if (!ppEnum) + return E_POINTER; + + HRESULT hr = S_OK; + PUBLIC_API_ENTRY(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + + EX_TRY + { + CordbRefEnum *pRefEnum = new CordbRefEnum(this, types); + GetContinueNeuterList()->Add(this, pRefEnum); + hr = pRefEnum->QueryInterface(IID_ICorDebugGCReferenceEnum, (void**)ppEnum); + } + EX_CATCH_HRESULT(hr); + + return hr; +} + +HRESULT CordbProcess::EnableNGENPolicy(CorDebugNGENPolicy ePolicy) +{ +#ifdef FEATURE_CORECLR + return E_NOTIMPL; +#else + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this); + + IDacDbiInterface* pDAC = GetProcess()->GetDAC(); + hr = pDAC->EnableNGENPolicy(ePolicy); + + PUBLIC_API_END(hr); + return hr; +#endif +} + + +HRESULT CordbProcess::GetTypeID(CORDB_ADDRESS obj, COR_TYPEID *pId) +{ + if (pId == NULL) + return E_POINTER; + + HRESULT hr = S_OK; + PUBLIC_API_ENTRY(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + + EX_TRY + { + hr = GetProcess()->GetDAC()->GetTypeID(obj, pId); + } + EX_CATCH_HRESULT(hr); + + return hr; +} + +HRESULT CordbProcess::GetTypeForTypeID(COR_TYPEID id, ICorDebugType **ppType) +{ + if (ppType == NULL) + return E_POINTER; + + HRESULT hr = S_OK; + + PUBLIC_API_ENTRY(this); + RSLockHolder stopGoLock(this->GetProcess()->GetStopGoLock()); + RSLockHolder procLock(this->GetProcess()->GetProcessLock()); + + EX_TRY + { + DebuggerIPCE_ExpandedTypeData data; + GetDAC()->GetObjectExpandedTypeInfoFromID(AllBoxed, VMPTR_AppDomain::NullPtr(), id, &data); + + CordbType *type = 0; + hr = CordbType::TypeDataToType(GetSharedAppDomain(), &data, &type); + + if (SUCCEEDED(hr)) + hr = type->QueryInterface(IID_ICorDebugType, (void**)ppType); + } + EX_CATCH_HRESULT(hr); + + return hr; +} + + +COM_METHOD CordbProcess::GetArrayLayout(COR_TYPEID id, COR_ARRAY_LAYOUT *pLayout) +{ + if (pLayout == NULL) + return E_POINTER; + + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this); + + hr = GetProcess()->GetDAC()->GetArrayLayout(id, pLayout); + + PUBLIC_API_END(hr); + return hr; +} + +COM_METHOD CordbProcess::GetTypeLayout(COR_TYPEID id, COR_TYPE_LAYOUT *pLayout) +{ + if (pLayout == NULL) + return E_POINTER; + + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this); + + hr = GetProcess()->GetDAC()->GetTypeLayout(id, pLayout); + + PUBLIC_API_END(hr); + return hr; +} + +COM_METHOD CordbProcess::GetTypeFields(COR_TYPEID id, ULONG32 celt, COR_FIELD fields[], ULONG32 *pceltNeeded) +{ + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this); + + hr = GetProcess()->GetDAC()->GetObjectFields(id, celt, fields, pceltNeeded); + + PUBLIC_API_END(hr); + return hr; +} + +COM_METHOD CordbProcess::SetWriteableMetadataUpdateMode(WriteableMetadataUpdateMode flags) +{ + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this); + + if(flags != LegacyCompatPolicy && + flags != AlwaysShowUpdates) + { + hr = E_INVALIDARG; + } + else if(m_pShim != NULL) + { + if(flags != LegacyCompatPolicy) + { + hr = CORDBG_E_UNSUPPORTED; + } + } + + if(SUCCEEDED(hr)) + { + m_writableMetadataUpdateMode = flags; + } + + PUBLIC_API_END(hr); + return hr; +} + +COM_METHOD CordbProcess::EnableExceptionCallbacksOutsideOfMyCode(BOOL enableExceptionsOutsideOfJMC) +{ + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this); + + hr = GetProcess()->GetDAC()->SetSendExceptionsOutsideOfJMC(enableExceptionsOutsideOfJMC); + + PUBLIC_API_END(hr); + return hr; +} + +#ifdef FEATURE_LEGACYNETCF_DBG_HOST_CONTROL + +COM_METHOD CordbProcess::InvokePauseCallback() +{ + return S_OK; +} + +COM_METHOD CordbProcess::InvokeResumeCallback() +{ + return S_OK; +} + +#endif + +HRESULT CordbProcess::GetTypeForObject(CORDB_ADDRESS addr, CordbType **ppType, CordbAppDomain **pAppDomain) +{ + VMPTR_AppDomain appDomain; + VMPTR_Module mod; + VMPTR_DomainFile domainFile; + + HRESULT hr = E_FAIL; + if (GetDAC()->GetAppDomainForObject(addr, &appDomain, &mod, &domainFile)) + { + CordbAppDomain *cdbAppDomain = appDomain.IsNull() ? GetSharedAppDomain() : LookupOrCreateAppDomain(appDomain); + + _ASSERTE(cdbAppDomain); + + DebuggerIPCE_ExpandedTypeData data; + GetDAC()->GetObjectExpandedTypeInfo(AllBoxed, appDomain, addr, &data); + + CordbType *type = 0; + hr = CordbType::TypeDataToType(cdbAppDomain, &data, &type); + + if (SUCCEEDED(hr)) + { + *ppType = type; + if (pAppDomain) + *pAppDomain = cdbAppDomain; + } + } + + return hr; +} + + +// ****************************************** +// CordbRefEnum +// ****************************************** +CordbRefEnum::CordbRefEnum(CordbProcess *proc, BOOL walkWeakRefs) + : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacksFQ(TRUE), + mHandleMask((UINT32)(walkWeakRefs ? CorHandleAll : CorHandleStrongOnly)) +{ +} + +CordbRefEnum::CordbRefEnum(CordbProcess *proc, CorGCReferenceType types) + : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacksFQ(FALSE), + mHandleMask((UINT32)types) +{ +} + +void CordbRefEnum::Neuter() +{ + EX_TRY + { + if (mRefHandle) + { + GetProcess()->GetDAC()->DeleteRefWalk(mRefHandle); + mRefHandle = 0; + } + } + EX_CATCH + { + _ASSERTE(!"Hit an error freeing a ref walk."); + } + EX_END_CATCH(SwallowAllExceptions) + + CordbBase::Neuter(); +} + +HRESULT CordbRefEnum::QueryInterface(REFIID riid, void **ppInterface) +{ + if (ppInterface == NULL) + return E_INVALIDARG; + + if (riid == IID_ICorDebugGCReferenceEnum) + { + *ppInterface = static_cast<ICorDebugGCReferenceEnum*>(this); + } + else if (riid == IID_IUnknown) + { + *ppInterface = static_cast<IUnknown*>(static_cast<ICorDebugGCReferenceEnum*>(this)); + } + else + { + *ppInterface = NULL; + return E_NOINTERFACE; + } + + ExternalAddRef(); + return S_OK; +} + +HRESULT CordbRefEnum::Skip(ULONG celt) +{ + return E_NOTIMPL; +} + +HRESULT CordbRefEnum::Reset() +{ + PUBLIC_API_ENTRY(this); + HRESULT hr = S_OK; + EX_TRY + { + if (mRefHandle) + { + GetProcess()->GetDAC()->DeleteRefWalk(mRefHandle); + mRefHandle = 0; + } + } + EX_CATCH_HRESULT(hr); + + return hr; +} + +HRESULT CordbRefEnum::Clone(ICorDebugEnum **ppEnum) +{ + return E_NOTIMPL; +} + +HRESULT CordbRefEnum::GetCount(ULONG *pcelt) +{ + return E_NOTIMPL; +} + + +// + +HRESULT CordbRefEnum::Next(ULONG celt, COR_GC_REFERENCE refs[], ULONG *pceltFetched) +{ + if (refs == NULL || pceltFetched == NULL) + return E_POINTER; + + CordbProcess *process = GetProcess(); + HRESULT hr = S_OK; + + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(process); + + RSLockHolder procLockHolder(process->GetProcessLock()); + + EX_TRY + { + if (!mRefHandle) + hr = process->GetDAC()->CreateRefWalk(&mRefHandle, mEnumStacksFQ, mEnumStacksFQ, mHandleMask); + + if (SUCCEEDED(hr)) + { + DacGcReference dacRefs[32]; + ULONG toFetch = _countof(dacRefs); + ULONG total = 0; + + for (ULONG c = 0; SUCCEEDED(hr) && c < (celt/_countof(dacRefs) + 1); ++c) + { + // Fetch 32 references at a time, the last time, only fetch the remainder (that is, if + // the user didn't fetch a multiple of 32). + if (c == celt/_countof(dacRefs)) + toFetch = celt % _countof(dacRefs); + + ULONG fetched = 0; + hr = process->GetDAC()->WalkRefs(mRefHandle, toFetch, dacRefs, &fetched); + + if (SUCCEEDED(hr)) + { + for (ULONG i = 0; i < fetched; ++i) + { + CordbAppDomain *pDomain = process->LookupOrCreateAppDomain(dacRefs[i].vmDomain); + + ICorDebugAppDomain *pAppDomain; + ICorDebugValue *pOutObject = NULL; + if (dacRefs[i].pObject & 1) + { + dacRefs[i].pObject &= ~1; + ICorDebugObjectValue *pObjValue = NULL; + + hr = process->GetObject(dacRefs[i].pObject, &pObjValue); + + if (SUCCEEDED(hr)) + { + hr = pObjValue->QueryInterface(IID_ICorDebugValue, (void**)&pOutObject); + pObjValue->Release(); + } + } + else + { + ICorDebugReferenceValue *tmpValue = NULL; + IfFailThrow(CordbReferenceValue::BuildFromGCHandle(pDomain, + dacRefs[i].objHnd, + &tmpValue)); + + if (SUCCEEDED(hr)) + { + hr = tmpValue->QueryInterface(IID_ICorDebugValue, (void**)&pOutObject); + tmpValue->Release(); + } + } + + if (SUCCEEDED(hr) && pDomain) + { + hr = pDomain->QueryInterface(IID_ICorDebugAppDomain, (void**)&pAppDomain); + } + + if (FAILED(hr)) + break; + + refs[total].Domain = pAppDomain; + refs[total].Location = pOutObject; + refs[total].Type = (CorGCReferenceType)dacRefs[i].dwType; + refs[total].ExtraData = dacRefs[i].i64ExtraData; + + total++; + } + } + } + + *pceltFetched = total; + } + } + EX_CATCH_HRESULT(hr); + + return hr; +} + + +// ****************************************** +// CordbHeapEnum +// ****************************************** +CordbHeapEnum::CordbHeapEnum(CordbProcess *proc) + : CordbBase(proc, 0, enumCordbHeap), mHeapHandle(0) +{ +} + +HRESULT CordbHeapEnum::QueryInterface(REFIID riid, void **ppInterface) +{ + if (ppInterface == NULL) + return E_INVALIDARG; + + if (riid == IID_ICorDebugHeapEnum) + { + *ppInterface = static_cast<ICorDebugHeapEnum*>(this); + } + else if (riid == IID_IUnknown) + { + *ppInterface = static_cast<IUnknown*>(static_cast<ICorDebugHeapEnum*>(this)); + } + else + { + *ppInterface = NULL; + return E_NOINTERFACE; + } + + ExternalAddRef(); + return S_OK; +} + +HRESULT CordbHeapEnum::Skip(ULONG celt) +{ + return E_NOTIMPL; +} + +HRESULT CordbHeapEnum::Reset() +{ + Clear(); + return S_OK; +} + +void CordbHeapEnum::Clear() +{ + EX_TRY + { + if (mHeapHandle) + { + GetProcess()->GetDAC()->DeleteHeapWalk(mHeapHandle); + mHeapHandle = 0; + } + } + EX_CATCH + { + _ASSERTE(!"Hit an error freeing the heap walk."); + } + EX_END_CATCH(SwallowAllExceptions) +} + +HRESULT CordbHeapEnum::Clone(ICorDebugEnum **ppEnum) +{ + return E_NOTIMPL; +} + +HRESULT CordbHeapEnum::GetCount(ULONG *pcelt) +{ + return E_NOTIMPL; +} + +HRESULT CordbHeapEnum::Next(ULONG celt, COR_HEAPOBJECT objects[], ULONG *pceltFetched) +{ + HRESULT hr = S_OK; + PUBLIC_API_ENTRY(this); + RSLockHolder stopGoLock(this->GetProcess()->GetStopGoLock()); + RSLockHolder procLock(this->GetProcess()->GetProcessLock()); + ULONG fetched = 0; + + EX_TRY + { + if (mHeapHandle == 0) + { + hr = GetProcess()->GetDAC()->CreateHeapWalk(&mHeapHandle); + } + + if (SUCCEEDED(hr)) + { + hr = GetProcess()->GetDAC()->WalkHeap(mHeapHandle, celt, objects, &fetched); + _ASSERTE(fetched <= celt); + } + + if (SUCCEEDED(hr)) + { + // Return S_FALSE if we've reached the end of the enum. + if (fetched < celt) + hr = S_FALSE; + } + } + EX_CATCH_HRESULT(hr); + + // Set the fetched parameter to reflect the number of elements (if any) + // that were successfully saved to "objects" + if (pceltFetched) + *pceltFetched = fetched; + + return hr; +} + +//--------------------------------------------------------------------------------------- +// Flush state for when the process starts running. +// +// Notes: +// Helper for code:CordbProcess::ProcessStateChanged. +// Since ICD Arrowhead does not own the eventing pipeline, it needs the debugger to +// notifying it of when the process is running again. This is like the counterpart +// to code:CordbProcess::Filter +void CordbProcess::FlushProcessRunning() +{ + _ASSERTE(GetProcessLock()->HasLock()); + + // Update the continue counter. + m_continueCounter++; + + // Safely dispose anything that should be neutered on continue. + MarkAllThreadsDirty(); + ForceDacFlush(); +} + +//--------------------------------------------------------------------------------------- +// Flush all cached state and bring us back to "cold startup" +// +// Notes: +// Helper for code:CordbProcess::ProcessStateChanged. +// This is used if the data-target changes underneath us in a way that is +// not consistent with the process running forward. For example, if for +// a time-travel debugger, the data-target may flow "backwards" in time. +// +void CordbProcess::FlushAll() +{ + CONTRACTL + { + THROWS; + } + CONTRACTL_END; + + HRESULT hr; + _ASSERTE(GetProcessLock()->HasLock()); + + // + // First, determine if it's safe to Flush + // + + hr = IsReadyForDetach(); + IfFailThrow(hr); + + // Check for outstanding CordbHandle values. + if (OutstandingHandles()) + { + ThrowHR(CORDBG_E_DETACH_FAILED_OUTSTANDING_TARGET_RESOURCES); + } + + // FlushAll is a superset of FlushProcessRunning. + // This will also ensure we clear the DAC cache. + FlushProcessRunning(); + + // If we detach before the CLR is loaded into the debuggee, then we can no-op a lot of work. + // We sure can't be sending IPC events to the LS before it exists. + NeuterChildren(); +} + +//--------------------------------------------------------------------------------------- +// +// Detach the Debugger from the LS process. +// +// +// Return Value: +// S_OK on successful detach. Else errror. +// +// Assumptions: +// Target is stopped. +// +// Notes: +// Once we're detached, the LS can resume running and exit. +// So it's possible to get an ExitProcess callback in the middle of the Detach phase. If that happens, +// we must return CORDBG_E_PROCESS_TERMINATED and pretend that the exit happened before we tried to detach. +// Else if we detach successfully, return S_OK. +// +// @dbgtodo attach-bit: need to figure out semantics of Detach +// in V3, especially w.r.t to an attach bit. +//--------------------------------------------------------------------------------------- +HRESULT CordbProcess::Detach() +{ + PUBLIC_API_ENTRY(this); + + FAIL_IF_NEUTERED(this); + + if (IsInteropDebugging()) + { + return CORDBG_E_INTEROP_NOT_SUPPORTED; + } + + + HRESULT hr = S_OK; + // A very important note: we require that the process is synchronized before doing a detach. This ensures + // that no events are on their way from the Left Side. We also require that the user has drained the + // managed event queue, but there is currently no way to really enforce that here. + // @todo- why can't we enforce that the managed event Q is drained? + ATT_REQUIRE_SYNCED_OR_NONINIT_MAY_FAIL(this); + + + hr = IsReadyForDetach(); + if (FAILED(hr)) + { + // Avoid neutering. Gives client a chance to fix detach issue and retry. + return hr; + } + + // Since the detach may resume the LS and allow it to exit, which may invoke the EP callback + // which may destroy this process object, be sure to protect us w/ an extra AddRef/Release + RSSmartPtr<CordbProcess> pRef(this); + + + + LOG((LF_CORDB, LL_INFO1000, "CP::Detach - beginning\n")); + if (m_pShim == NULL) // This API is moved off to the shim + { + + // This is still invasive. + // Ignore failures. This will fail for a non-invasive target. + if (IsDacInitialized()) + { + HRESULT hrIgnore = S_OK; + EX_TRY + { + GetDAC()->MarkDebuggerAttached(FALSE); + } + EX_CATCH_HRESULT(hrIgnore); + } + } + else + { + EX_TRY + { + DetachShim(); + } + EX_CATCH_HRESULT(hr); + } + + // Either way, neuter everything. + this->Neuter(); + + // Implicit release on pRef + LOG((LF_CORDB, LL_INFO1000, "CP::Detach - returning w/ hr=0x%x\n", hr)); + return hr; +} + +// Free up key left-side resources +// +// Called on detach +// This does key neutering of objects that hold left-side resources and require +// preemptively freeing the resources. +// After this, code:CordbProcess::Neuter should only affect right-side state. +void CordbProcess::NeuterChildrenLeftSideResources() +{ + _ASSERTE(GetStopGoLock()->HasLock()); + + _ASSERTE(!GetProcessLock()->HasLock()); + RSLockHolder lockHolder(GetProcessLock()); + + + // Need process-lock to operate on hashtable, but can't yet Neuter under process-lock, + // so we have to copy the contents to an auxilary list which we can then traverse outside the lock. + RSPtrArray<CordbAppDomain> listAppDomains; + m_appDomains.CopyToArray(&listAppDomains); + + + + // Must not hold process lock so that we can be safe to send IPC events + // to cleanup left-side resources. + lockHolder.Release(); + _ASSERTE(!GetProcessLock()->HasLock()); + + // Frees left-side resources. This may send IPC events. + // This will make normal neutering a nop. + m_LeftSideResourceCleanupList.NeuterLeftSideResourcesAndClear(this); + + for(unsigned int idx = 0; idx < listAppDomains.Length(); idx++) + { + CordbAppDomain * pAppDomain = listAppDomains[idx]; + + // CordbHandleValue is in the appdomain exit list, and that needs + // to send an IPC event to cleanup and release the handle from + // the GCs handle table. + pAppDomain->GetSweepableExitNeuterList()->NeuterLeftSideResourcesAndClear(this); + } + listAppDomains.Clear(); + +} + +//--------------------------------------------------------------------------------------- +// Detach the Debugger from the LS process for the V2 case +// +// Assumptions: +// This will NeuterChildren(), caller will do the real Neuter() +// Caller has already ensured that detach is safe. +// +// @dbgtodo attach-bit: this should be moved into the shim; need +// to figure out semantics for freeing left-side resources (especially GC +// handles) on detach. +void CordbProcess::DetachShim() +{ + + HASHFIND hashFind; + HRESULT hr = S_OK; + + // If we detach before the CLR is loaded into the debuggee, then we can no-op a lot of work. + // We sure can't be sending IPC events to the LS before it exists. + if (m_initialized) + { + // The managed event queue is not necessarily drained. Cordbg could call detach between any callback. + // While the process is still stopped, neuter all of our children. + // This will make our Neuter() a nop and saves the W32ET from having to do dangerous work. + this->NeuterChildrenLeftSideResources(); + { + RSLockHolder lockHolder(GetProcessLock()); + this->NeuterChildren(); + } + + // Go ahead and detach from the entire process now. This is like sending a "Continue". + DebuggerIPCEvent * pIPCEvent = (DebuggerIPCEvent *) _alloca(CorDBIPC_BUFFER_SIZE); + InitIPCEvent(pIPCEvent, DB_IPCE_DETACH_FROM_PROCESS, true, VMPTR_AppDomain::NullPtr()); + + hr = m_cordb->SendIPCEvent(this, pIPCEvent, CorDBIPC_BUFFER_SIZE); + hr = WORST_HR(hr, pIPCEvent->hr); + IfFailThrow(hr); + } + else + { + // @dbgtodo attach-bit: push this up, once detach IPC event is hoisted. + RSLockHolder lockHolder(GetProcessLock()); + + // Shouldn't have any appdomains. + (void)hashFind; //prevent "unused variable" error from GCC + _ASSERTE(m_appDomains.FindFirst(&hashFind) == NULL); + } + + LOG((LF_CORDB, LL_INFO10000, "CP::Detach - got reply from LS\n")); + + // It's possible that the LS may exit after they reply to our detach_from_process, but + // before we update our internal state that they're detached. So still have to check + // failure codes here. + hr = this->m_pShim->GetWin32EventThread()->SendDetachProcessEvent(this); + + + // Since we're auto-continuing when we detach, we should set the stop count back to zero. + // This (along w/ m_detached) prevents anyone from calling Continue on this process + // after this call returns. + m_stopCount = 0; + + if (hr != CORDBG_E_PROCESS_TERMINATED) + { + // Remember that we've detached from this process object. This will prevent any further operations on + // this process, just in case... :) + // If LS exited, then don't set this flag because it overrides m_terminated when reporting errors; + // and we want to provide a consistent story about whether we detached or whether the LS exited. + m_detached = true; + } + IfFailThrow(hr); + + + // Now that all complicated cleanup is done, caller can do a final neuter. + // This will implicitly stop our Win32 event thread as well. +} + +// Delete all events from the queue without dispatching. This is useful in shutdown. +// An event that is currently dispatching is not on the queue. +void CordbProcess::DeleteQueuedEvents() +{ + INTERNAL_API_ENTRY(this); + // We must have the process lock to ensure that no one is trying to add an event + _ASSERTE(!ThreadHoldsProcessLock()); + + if (m_pShim != NULL) + { + PUBLIC_CALLBACK_IN_THIS_SCOPE0_NO_LOCK(this); + + // DeleteAll() is part of the shim, and it will change external ref counts, so must really + // be marked as outside the RS. + m_pShim->GetManagedEventQueue()->DeleteAll(); + } +} + +//--------------------------------------------------------------------------------------- +// +// Track that we're about to dispatch a managed event. +// +// Arguments: +// event - event being dispatched +// +// Assumptions: +// This is used to support code:CordbProcess::AreDispatchingEvent +// This is always called on the same thread as code:CordbProcess::FinishEventDispatch +void CordbProcess::StartEventDispatch(DebuggerIPCEventType event) +{ + LIMITED_METHOD_CONTRACT; + + _ASSERTE(m_dispatchedEvent == DB_IPCE_DEBUGGER_INVALID); + _ASSERTE(event != DB_IPCE_DEBUGGER_INVALID); + m_dispatchedEvent = event; +} + +//--------------------------------------------------------------------------------------- +// +// Track that we're done dispatching a managed event. +// +// +// Assumptions: +// This is always called on the same thread as code:CordbProcess::StartEventDispatch +// +// Notes: +// @dbgtodo shim: eventually this goes into the shim when we hoist Continue +void CordbProcess::FinishEventDispatch() +{ + LIMITED_METHOD_CONTRACT; + + _ASSERTE(m_dispatchedEvent != DB_IPCE_DEBUGGER_INVALID); + m_dispatchedEvent = DB_IPCE_DEBUGGER_INVALID; +} + +//--------------------------------------------------------------------------------------- +// +// Are we in the middle of dispatching an event? +// +// Notes: +// This is used by code::CordbProcess::ContinueInternal. Continue logic takes +// a shortcut if the continue is called on the dispatch thread. +// It doesn't matter which event is being dispatch; only that we're on the dispatch thread. +// @dbgtodo shim: eventually this goes into the shim when we hoist Continue +bool CordbProcess::AreDispatchingEvent() +{ + LIMITED_METHOD_CONTRACT; + + return m_dispatchedEvent != DB_IPCE_DEBUGGER_INVALID; +} + + + + + +// Terminate the app. We'll still dispatch an ExitProcess callback, so the app +// must wait for that before calling Cordb::Terminate. +// If this fails, the client can always call the OS's TerminateProcess command +// to rudely kill the debuggee. +HRESULT CordbProcess::Terminate(unsigned int exitCode) +{ + PUBLIC_API_ENTRY(this); + + LOG((LF_CORDB, LL_INFO1000, "CP::Terminate: with exitcode %u\n", exitCode)); + FAIL_IF_NEUTERED(this); + + + // @dbgtodo shutdown: eventually, all of Terminate() will be in the Shim. + // Free all the remaining events. Since this will call into the shim, do this outside of any locks. + // (ATT_ takes locks). + DeleteQueuedEvents(); + + ATT_REQUIRE_SYNCED_OR_NONINIT_MAY_FAIL(this); + + // When we terminate the process, it's handle will become signaled and + // Win32 Event Thread will leap into action and call CordbWin32EventThread::ExitProcess + // Unfortunately, that may destroy this object if the ExitProcess callback + // decides to call Release() on the process. + + + // Indicate that the process is exiting so that (among other things) we don't try and + // send messages to the left side while it's being deleted. + Lock(); + + // In case we're continuing from the loader bp, we don't want to try and kick off an attach. :) + m_fDoDelayedManagedAttached = false; + m_exiting = true; + + + + // We'd like to just take a lock around everything here, but that may deadlock us + // since W32ET will wait on the lock, and Continue may wait on W32ET. + // So we just do an extra AddRef/Release to make sure we're still around. + // @todo - could we move this smartptr up so that it's well-nested w/ the lock? + RSSmartPtr<CordbProcess> pRef(this); + + Unlock(); + + + // At any point after this call, the w32 ET may run the ExitProcess code which will race w/ the continue call. + // This call only posts a request that the process terminate and does not guarantee the process actually + // terminates. In particular, the process can not exit until any outstanding IO requests are done (on cancelled). + // It also can not exit if we have an outstanding not-continued native-debug event. + // Fortunately, the interesting work in terminate is done in ExitProcessWorkItem::Do, which can take the Stop-Go lock. + // Since we're currently holding the stop-go lock, that means we at least get some serialization. + // + // Note that on Windows, the process isn't really terminated until we receive the EXIT_PROCESS_DEBUG_EVENT. + // Before then, we can still still access the debuggee's address space. On the other, for Mac debugging, + // the process can die any time after this call, and so we can no longer call into the DAC. + GetShim()->GetNativePipeline()->TerminateProcess(exitCode); + + // We just call Continue() so that the debugger doesn't have to. (It's arguably odd + // to call Continue() after Terminate). + // We're stopped & Synced. + // For interop-debugging this is very important because the Terminate may not really kill the process + // until after we continue from the current native debug event. + ContinueInternal(FALSE); + + // Implicit release on pRef here (since it's going out of scope)... + // After this release, this object may be destroyed. So don't use any member functions + // (including Locks) after here. + + + return S_OK; +} + +// This can be called at any time, even if we're in an unrecoverable error state. +HRESULT CordbProcess::GetID(DWORD *pdwProcessId) +{ + PUBLIC_REENTRANT_API_ENTRY(this); + OK_IF_NEUTERED(this); + VALIDATE_POINTER_TO_OBJECT(pdwProcessId, DWORD *); + + HRESULT hr = S_OK; + EX_TRY + { + // This shouldn't be used in V3 paths. Normally, we can enforce that by checking against + // m_pShim. However, this API can be called after being neutered, in which case m_pShim is cleared. + // So check against 0 instead. + if (m_id == 0) + { + *pdwProcessId = 0; + ThrowHR(E_NOTIMPL); + } + *pdwProcessId = GetPid(); + } + EX_CATCH_HRESULT(hr); + return hr; +} + +// Helper to get PID internally. We know we'll always succeed. +// This is more convient for internal callers since they can just use it as an expression +// without having to check HRESULTS. +DWORD CordbProcess::GetPid() +{ + // This shouldn't be used in V3 paths, in which case it's set to 0. Only the shim should be + // calling this. Assert to catch anybody else. + _ASSERTE(m_id != 0); + + return (DWORD) m_id; +} + + +HRESULT CordbProcess::GetHandle(HANDLE *phProcessHandle) +{ + PUBLIC_REENTRANT_API_ENTRY(this); + FAIL_IF_NEUTERED(this); // Once we neuter the process, we close our OS handle to it. + VALIDATE_POINTER_TO_OBJECT(phProcessHandle, HANDLE *); + + if (m_pShim == NULL) + { + _ASSERTE(!"CordbProcess::GetHandle() should be not be called on the new architecture"); + *phProcessHandle = NULL; + return E_NOTIMPL; + } + else + { + *phProcessHandle = m_handle; + return S_OK; + } +} + +HRESULT CordbProcess::IsRunning(BOOL *pbRunning) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + VALIDATE_POINTER_TO_OBJECT(pbRunning, BOOL*); + + *pbRunning = !GetSynchronized(); + + return S_OK; +} + +HRESULT CordbProcess::EnableSynchronization(BOOL bEnableSynchronization) +{ + /* !!! */ + PUBLIC_API_ENTRY(this); + return E_NOTIMPL; +} + +HRESULT CordbProcess::Stop(DWORD dwTimeout) +{ + PUBLIC_API_ENTRY(this); + CORDBRequireProcessStateOK(this); + + HRESULT hr = StopInternal(dwTimeout, VMPTR_AppDomain::NullPtr()); + + return ErrWrapper(hr); +} + +HRESULT CordbProcess::StopInternal(DWORD dwTimeout, VMPTR_AppDomain pAppDomainToken) +{ + LOG((LF_CORDB, LL_INFO1000, "CP::S: stopping process 0x%x(%d) with timeout %d\n", m_id, m_id, dwTimeout)); + + INTERNAL_API_ENTRY(this); + + // Stop + Continue are executed under the Stop-Go lock. This makes them atomic. + // We'll toggle the process-lock (b/c we communicate w/ the W32et, so just the process-lock is + // not sufficient to make this atomic). + // It's ok to take this lock before checking if the CordbProcess has been neutered because + // the lock is destroyed in the dtor after neutering. + RSLockHolder ch(&m_StopGoLock); + + // Check if this CordbProcess has been neutered under the SG lock. + // Otherwise it's possible to race with Detach() and Terminate(). + FAIL_IF_NEUTERED(this); + CORDBFailIfOnWin32EventThread(this); + + if (m_pShim == NULL) // Stop/Go is moved off to the shim + { + return E_NOTIMPL; + } + + + DebuggerIPCEvent* event; + HRESULT hr = S_OK; + + STRESS_LOG2(LF_CORDB, LL_INFO1000, "CP::SI, timeout=%d, this=%p\n", dwTimeout, this); + + // Stop() is a syncronous (blocking) operation. Furthermore, we have no way to cancel the async-break request. + // Thus if we returned early on a timeout, then we'll be in a random state b/c the LS may get stopped at any + // later spot. + // One solution just require the param is INFINITE until we fix this and E_INVALIDARG if it's not. + // But that could be a breaking change (what if a debugger passes in a really large value that's effectively + // INFINITE). + // So we'll just ignore it and always treat it as infinite. + dwTimeout = INFINITE; + + // Do the checks on the process state under the SG lock. This ensures that another thread cannot come in + // after we do the checks and take the lock before we do. For example, Detach() can race with Stop() such + // that: + // 1. Thread A calls CordbProcess::Detach() and takes the stop-go lock + // 2. Thread B calls CordbProcess::Stop(), passes all the checks, and then blocks on the stop-go lock + // 3. Thread A finishes the detach, invalides the process state, cleans all the resources, and then + // releases the stop-go lock + // 4. Thread B gets the lock, but everything has changed + CORDBRequireProcessStateOK(this); + + Lock(); + + ASSERT_SINGLE_THREAD_ONLY(HoldsLock(&m_StopGoLock)); + + // Don't need to stop if the process hasn't even executed any managed code yet. + if (!m_initialized) + { + LOG((LF_CORDB, LL_INFO1000, "CP::S: process isn't initialized yet.\n")); + + // Mark the process as synchronized so no events will be dispatched until the thing is continued. + SetSynchronized(true); + + // Remember uninitialized stop... + m_uninitializedStop = true; + +#ifdef FEATURE_INTEROP_DEBUGGING + // If we're Win32 attached, then suspend all the unmanaged threads in the process. + // We may or may not be stopped at a native debug event. + if (IsInteropDebugging()) + { + SuspendUnmanagedThreads(); + } +#endif // FEATURE_INTEROP_DEBUGGING + + // Get the RC Event Thread to stop listening to the process. + m_cordb->ProcessStateChanged(); + + hr = S_OK; + goto Exit; + } + + // Don't need to stop if the process is already synchronized. + // @todo - Issue 129917. It's possible that we'll get a call to Stop when the LS is already stopped. + // Sending an AsyncBreak would deadlock here (b/c the LS will ignore the frivilous request, + // and thus never send a SyncComplete, and thus our Waiting on the SyncComplete will deadlock). + // We avoid this case by checking m_syncCompleteReceived (which should roughly correspond to + // the LS's m_stopped variable). + // One window this can happen is after a Continue() pings the RCET but before the RCET actually sweeps + flushes. + + if (GetSynchronized() || GetSyncCompleteRecv()) + { + LOG((LF_CORDB, LL_INFO1000, "CP::S: process was already synchronized. m_syncCompleteReceived=%d\n", GetSyncCompleteRecv())); + + if (GetSyncCompleteRecv()) + { + // We must be in that window alluded to above (while the RCET is sweeping). Re-ping the RCET. + SetSynchronized(true); + m_cordb->ProcessStateChanged(); + } + hr = S_OK; + goto Exit; + } + + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::S: process not sync'd, requesting stop.\n"); + + m_stopRequested = true; + + // We don't want to dispatch any Win32 debug events while we're trying to stop. + // Setting m_specialDeferment=true means that any debug event we get will be queued and not dispatched. + // We do this to avoid a nested call to Continue. + // These defered events will get dispatched when somebody calls continue (and since they're calling + // stop now, they must call continue eventually). + // Note that if we got a Win32 debug event between when we took the Stop-Go lock above and now, + // that even may have been dispatched. We're ok because SSFW32Stop will hijack that event and continue it, + // and then all future events will be queued. + m_specialDeferment = true; + Unlock(); + + BOOL asyncBreakSent; + + // We need to ensure that the helper thread is alive. + hr = this->StartSyncFromWin32Stop(&asyncBreakSent); + if (FAILED(hr)) + { + return hr; + } + + + if (asyncBreakSent) + { + hr = S_OK; + Lock(); + + m_stopRequested = false; + + goto Exit; + } + + // Send the async break event to the RC. + event = (DebuggerIPCEvent*) _alloca(CorDBIPC_BUFFER_SIZE); + InitIPCEvent(event, DB_IPCE_ASYNC_BREAK, false, pAppDomainToken); + + STRESS_LOG1(LF_CORDB, LL_INFO1000, "CP::S: sending async stop to appd 0x%x.\n", VmPtrToCookie(pAppDomainToken)); + + hr = m_cordb->SendIPCEvent(this, event, CorDBIPC_BUFFER_SIZE); + hr = WORST_HR(hr, event->hr); + if (FAILED(hr)) + { + // We don't hold the lock so just return immediately. Don't adjust stop-count. + _ASSERTE(!ThreadHoldsProcessLock()); + return hr; + } + + LOG((LF_CORDB, LL_INFO1000, "CP::S: sent async stop to appd 0x%x.\n", VmPtrToCookie(pAppDomainToken))); + + // Wait for the sync complete message to come in. Note: when the sync complete message arrives to the RCEventThread, + // it will mark the process as synchronized and _not_ dispatch any events. Instead, it will set m_stopWaitEvent + // which will let this function return. If the user wants to process any queued events, they will need to call + // Continue. + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::S: waiting for event.\n"); + + DWORD ret; + ret = SafeWaitForSingleObject(this, m_stopWaitEvent, dwTimeout); + + STRESS_LOG1(LF_CORDB, LL_INFO1000, "CP::S: got event, %d.\n", ret); + + if (m_terminated) + { + return CORDBG_E_PROCESS_TERMINATED; + } + + if (ret == WAIT_OBJECT_0) + { + LOG((LF_CORDB, LL_INFO1000, "CP::S: process stopped.\n")); + + m_stopRequested = false; + m_cordb->ProcessStateChanged(); + + hr = S_OK; + Lock(); + goto Exit; + } + else if (ret == WAIT_TIMEOUT) + { + hr = ErrWrapper(CORDBG_E_TIMEOUT); + } + else + hr = HRESULT_FROM_GetLastError(); + + // We came out of the wait, but we weren't signaled because a sync complete event came in. Re-check the process and + // remove the stop requested flag. + Lock(); + m_stopRequested = false; + + if (GetSynchronized()) + { + LOG((LF_CORDB, LL_INFO1000, "CP::S: process stopped.\n")); + + m_cordb->ProcessStateChanged(); + + hr = S_OK; + } + +Exit: + _ASSERTE(ThreadHoldsProcessLock()); + + // Stop queuing any Win32 Debug events. We should be synchronized now. + m_specialDeferment = false; + + if (SUCCEEDED(hr)) + { + IncStopCount(); + } + + STRESS_LOG2(LF_CORDB, LL_INFO1000, "CP::S: returning from Stop, hr=0x%08x, m_stopCount=%d.\n", hr, GetStopCount()); + + Unlock(); + + return hr; +} + +//--------------------------------------------------------------------------------------- +// Clear all RS state on all CordbThread objects. +// +// Notes: +// This clears all the thread-related state that the RS may have cached, +// such as locals, frames, etc. +// This would be called if the debugger is resuming execution. +void CordbProcess::MarkAllThreadsDirty() +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(ThreadHoldsProcessLock()); + + CordbThread * pThread; + HASHFIND find; + + // We don't need to prepopulate here (to collect LS state) because we're just updating RS state. + for (pThread = m_userThreads.FindFirst(&find); + pThread != NULL; + pThread = m_userThreads.FindNext(&find)) + { + _ASSERTE(pThread != NULL); + pThread->MarkStackFramesDirty(); + } + + ClearPatchTable(); +} + +HRESULT CordbProcess::Continue(BOOL fIsOutOfBand) +{ + PUBLIC_API_ENTRY(this); + + if (m_pShim == NULL) // This API is moved off to the shim + { + // bias towards failing with CORDBG_E_NUETERED. + FAIL_IF_NEUTERED(this); + return E_NOTIMPL; + } + + HRESULT hr; + + if (fIsOutOfBand) + { +#ifdef FEATURE_INTEROP_DEBUGGING + hr = ContinueOOB(); +#else + hr = E_INVALIDARG; +#endif // FEATURE_INTEROP_DEBUGGING + } + else + { + hr = ContinueInternal(fIsOutOfBand); + } + + return hr; +} + +#ifdef FEATURE_INTEROP_DEBUGGING +//--------------------------------------------------------------------------------------- +// +// ContinueOOB +// +// Continue the Win32 event as an out-of-band event. +// +// Return Value: +// S_OK on successful continue. Else error. +// +//--------------------------------------------------------------------------------------- +HRESULT CordbProcess::ContinueOOB() +{ + INTERNAL_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + + HRESULT hr = S_OK; + + // If we're continuing from an out-of-band unmanaged event, then just go + // ahead and get the Win32 event thread to continue the process. No other + // work needs to be done (i.e., don't need to send a managed continue message + // or dispatch any events) because any processing done due to the out-of-band + // message can't alter the synchronized state of the process. + + Lock(); + _ASSERTE(m_outOfBandEventQueue != NULL); + + // Are we calling this from the unmanaged callback? + if (m_dispatchingOOBEvent) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::CI: continue while dispatching unmanaged out-of-band event.\n"); + // We don't know what thread we're on here. + + // Tell the Win32 event thread to continue when it returns from handling its unmanaged callback. + m_dispatchingOOBEvent = false; + + Unlock(); + } + else + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::CI: continue outside of dispatching.\n"); + + // If we're not dispatching this, then they shouldn't be on the win32 event thread. + _ASSERTE(!this->IsWin32EventThread()); + + Unlock(); + + // Send an event to the Win32 event thread to do the continue. This is an out-of-band continue. + hr = this->m_pShim->GetWin32EventThread()->SendUnmanagedContinue(this, cOobUMContinue); + } + + return hr; + + +} +#endif // FEATURE_INTEROP_DEBUGGING + +//--------------------------------------------------------------------------------------- +// +// ContinueInternal +// +// Continue the Win32 event. +// +// Return Value: +// S_OK on success. Else error. +// +//--------------------------------------------------------------------------------------- +HRESULT CordbProcess::ContinueInternal(BOOL fIsOutOfBand) +{ + INTERNAL_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + + // Continue has an ATT similar to ATT_REQUIRE_STOPPED_MAY_FAIL, but w/ some subtle differences. + // - if we're stopped at a native DE, but not synchronized, we don't want to sync. + // - We may get Debug events (especially native ones) at weird times, and thus we have to continue + // at weird times. + + // External APIs should not have the process lock. + _ASSERTE(!ThreadHoldsProcessLock()); + _ASSERTE(m_pShim != NULL); + + // OutOfBand should use ContinueOOB + _ASSERTE(!fIsOutOfBand); + + // Since Continue is process-wide, just use a null appdomain pointer. + VMPTR_AppDomain pAppDomainToken = VMPTR_AppDomain::NullPtr(); + + HRESULT hr = S_OK; + + if (m_unrecoverableError) + { + return CORDBHRFromProcessState(this, NULL); + } + + + // We can't call ContinueInternal for an inband event on the win32 event thread. + // This is an issue in the CLR (or an API design decision, depending on your perspective). + // Continue() may send an IPC event and we can't do that on the win32 event thread. + + CORDBFailIfOnWin32EventThread(this); + + STRESS_LOG1(LF_CORDB, LL_INFO1000, "CP::CI: continuing IB, this=0x%X\n", this); + + // Stop + Continue are executed under the Stop-Go lock. This makes them atomic. + // We'll toggle the process-lock (b/c we communicate w/ the W32et, so that's not sufficient). + RSLockHolder rsLockHolder(&m_StopGoLock); + + // Check for other failures (do these after we have the SG lock). + if (m_terminated) + { + return CORDBG_E_PROCESS_TERMINATED; + } + if (m_detached) + { + return CORDBG_E_PROCESS_DETACHED; + } + + Lock(); + + ASSERT_SINGLE_THREAD_ONLY(HoldsLock(&m_StopGoLock)); + _ASSERTE(fIsOutOfBand == FALSE); + + // If we've got multiple Stop calls, we need a Continue for each one. So, if the stop count > 1, just go ahead and + // return without doing anything. Note: this is only for in-band or managed events. OOB events are still handled as + // normal above. + _ASSERTE(GetStopCount() > 0); + + if (GetStopCount() == 0) + { + Unlock(); + _ASSERTE(!"Superflous Continue. ICorDebugProcess.Continue() called too many times"); + return CORDBG_E_SUPERFLOUS_CONTINUE; + } + + DecStopCount(); + + // We give managed events priority over unmanaged events. That way, the entire queued managed state can drain before + // we let any other unmanaged events through. + + // Every stop or event must be matched by a corresponding Continue. m_stopCount counts outstanding stopping events + // along with calls to Stop. If the count is high at this point, we simply return. This ensures that even if someone + // calls Stop just as they're receiving an event that they can call Continue for that Stop and for that event + // without problems. + if (GetStopCount() > 0) + { + STRESS_LOG1(LF_CORDB, LL_INFO1000, "CP::CI: m_stopCount=%d, Continue just returning S_OK...\n", GetStopCount()); + + Unlock(); + return S_OK; + } + + // We're no longer stopped, so reset the m_stopWaitEvent. + ResetEvent(m_stopWaitEvent); + + // If we're continuing from an uninitialized stop, then we don't need to do much at all. No event need be sent to + // the Left Side (duh, it isn't even there yet.) We just need to get the RC Event Thread to start listening to the + // process again, and resume any unmanaged threads if necessary. + if (m_uninitializedStop) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::CI: continuing from uninitialized stop.\n"); + + // No longer synchronized (it was a partial sync in the first place.) + SetSynchronized(false); + MarkAllThreadsDirty(); + + // No longer in an uninitialized stop. + m_uninitializedStop = false; + + // Notify the RC Event Thread. + m_cordb->ProcessStateChanged(); + + Unlock(); + +#ifdef FEATURE_INTEROP_DEBUGGING + // We may or may not have a native debug event queued here. + // If Cordbg called Stop() from a native debug event (to get the process Synchronized), then + // we'll have a native debug event, and we need to continue it. + // If Cordbg called Stop() to do an AsyncBreak, then there's no native-debug event. + + // If we're Win32 attached, resume all the unmanaged threads. + if (IsInteropDebugging()) + { + if(m_lastDispatchedIBEvent != NULL) + { + m_lastDispatchedIBEvent->SetState(CUES_UserContinued); + } + + // Send to the Win32 event thread to do the unmanaged continue for us. + // If we're at a debug event, this will continue it. + // Else it will degenerate into ResumeUnmanagedThreads(); + this->m_pShim->GetWin32EventThread()->SendUnmanagedContinue(this, cRealUMContinue); + } +#endif // FEATURE_INTEROP_DEBUGGING + + + return S_OK; + } + + // If there are more managed events, get them dispatched now. + if (!m_pShim->GetManagedEventQueue()->IsEmpty() && GetSynchronized()) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::CI: managed event queued.\n"); + + // Mark that we're not synchronized anymore. + SetSynchronized(false); + + // If the callback queue is not empty, then the LS is not actually continuing, and so our cached + // state is still valid. + + // If we're in the middle of dispatching a managed event, then simply return. This indicates to HandleRCEvent + // that the user called Continue and HandleRCEvent will dispatch the next queued event. But if Continue was + // called outside the managed callback, all we have to do is tell the RC event thread that something about the + // process has changed and it will dispatch the next managed event. + if (!AreDispatchingEvent()) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::CI: continuing while not dispatching managed event.\n"); + + m_cordb->ProcessStateChanged(); + } + + Unlock(); + return S_OK; + } + + // Neuter if we have an outstanding object. + // Only do this if we're really continuining the debuggee. So don't do this if our stop-count is high b/c we + // shouldn't neuter until we're done w/ the current event. And don't do this until we drain the current callback queue. + // Note that we can't hold the process lock while we do this b/c Neutering may send IPC events. + // However, we're still under the StopGo lock b/c that may help us serialize things. + + // Sweep neuter list. This will catch anything that's marked as 'safe to neuter'. This includes + // all objects added to the 'neuter-on-Continue'. + // Only do this if we're synced- we don't want to do this if we're continuing from a Native Debug event. + if (GetSynchronized()) + { + // Need process-lock to operate on hashtable, but can't yet Neuter under process-lock, + // so we have to copy the contents to an auxilary list which we can then traverse outside the lock. + RSPtrArray<CordbAppDomain> listAppDomains; + HRESULT hrCopy = S_OK; + EX_TRY // @dbgtodo cleanup: push this up + { + m_appDomains.CopyToArray(&listAppDomains); + } + EX_CATCH_HRESULT(hrCopy); + SetUnrecoverableIfFailed(GetProcess(), hrCopy); + + m_ContinueNeuterList.NeuterAndClear(this); + + // @dbgtodo left-side resources: eventually (once + // NeuterLeftSideResources is process-lock safe), do this all under the + // lock. Can't hold process lock b/c neutering left-side resources + // may send events. + Unlock(); + + // This may send IPC events. + // This will make normal neutering a nop. + // This will toggle the process lock. + m_LeftSideResourceCleanupList.SweepNeuterLeftSideResources(this); + + + // Many objects (especially CordbValue, FuncEval) don't have clear lifetime semantics and + // so they must be put into an exit-neuter list (Process/AppDomain) for worst-case scenarios. + // These objects are likely released early, and so we sweep them aggressively on each Continue (kind of like a mini-GC). + // + // One drawback is that there may be a lot of useless sweeping if the debugger creates a lot of + // objects that it holds onto. Consider instead of sweeping, have the object explicitly post itself + // to a list that's guaranteed to be cleared. This would let us avoid sweeping not-yet-ready objects. + // This will toggle the process lock + m_ExitNeuterList.SweepAllNeuterAtWillObjects(this); + + + for(unsigned int idx = 0; idx < listAppDomains.Length(); idx++) + { + CordbAppDomain * pAppDomain = listAppDomains[idx]; + + // CordbHandleValue is in the appdomain exit list, and that needs + // to send an IPC event to cleanup and release the handle from + // the GCs handle table. + // This will toggle the process lock. + pAppDomain->GetSweepableExitNeuterList()->SweepNeuterLeftSideResources(this); + } + listAppDomains.Clear(); + + Lock(); + } + + + // At this point, if the managed event queue is empty, m_synchronized may still be true if we had previously + // synchronized. + +#ifdef FEATURE_INTEROP_DEBUGGING + // Next, check for unmanaged events that may be queued. If there are some queued, then we need to get the Win32 + // event thread to go ahead and dispatch the next one. If there aren't any queued, then we can just fall through and + // send the continue message to the left side. This works even if we have an outstanding ownership request, because + // until that answer is received, its just like the event hasn't happened yet. + // + // If we're terminated, then we've already continued from the last win32 event and so don't continue. + // @todo - or we could ensure the PS_SOME_THREADS_SUSPENDED | PS_HIJACKS_IN_PLACE are removed. + // Either way, we're just protecting against exit-process at strange times. + bool fDoWin32Continue = !m_terminated && ((m_state & (PS_WIN32_STOPPED | PS_SOME_THREADS_SUSPENDED | PS_HIJACKS_IN_PLACE)) != 0); + + // We need to store this before marking the event user continued below + BOOL fHasUserUncontinuedEvents = HasUserUncontinuedNativeEvents(); + + if(m_lastDispatchedIBEvent != NULL) + { + m_lastDispatchedIBEvent->SetState(CUES_UserContinued); + } + + if (fHasUserUncontinuedEvents) + { + // ExitProcess is the last debug event we'll get. The Process Handle is not signaled until + // after we continue from ExitProcess. m_terminated is only set once we know the process is signaled. + // (This isn't 100% true for the detach case, but since you can't do interop detach, we don't care) + //_ASSERTE(!m_terminated); + + STRESS_LOG1(LF_CORDB, LL_INFO1000, "CP::CI: there are queued uncontinued events. m_dispatchingUnmanagedEvent = %d\n", m_dispatchingUnmanagedEvent); + + // Are we being called while in the unmanaged event callback? + if (m_dispatchingUnmanagedEvent) + { + LOG((LF_CORDB, LL_INFO1000, "CP::CI: continue while dispatching.\n")); + // The Win32ET could have made a cross-thread call to Continue while dispatching, + // so we don't know if this is the win32 ET. + + // Tell the Win32 thread to continue when it returns from handling its unmanaged callback. + m_dispatchingUnmanagedEvent = false; + + // If there are no more unmanaged events, then we fall through and continue the process for real. Otherwise, + // we can simply return. + if (HasUndispatchedNativeEvents()) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::CI: more unmanaged events need dispatching.\n"); + + // Note: if we tried to access the Left Side while stopped but couldn't, then m_oddSync will be true. We + // need to reset it to false since we're continuing now. + m_oddSync = false; + + Unlock(); + return S_OK; + } + else + { + // Also, if there are no more unmanaged events, then when DispatchUnmanagedInBandEvent sees that + // m_dispatchingUnmanagedEvent is false, it will continue the process. So we set doWin32Continue to + // false here so that we don't try to double continue the process below. + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::CI: no more unmanaged events to dispatch.\n"); + + fDoWin32Continue = false; + } + } + else + { + // after the DebugEvent callback returned the continue still had no been issued. Then later + // on another thread the user called back to continue the event, which gets us to right here + LOG((LF_CORDB, LL_INFO1000, "CP::CI: continue outside of dispatching.\n")); + + // This should be the common place to Dispatch an IB event that was hijacked for sync. + + // If we're not dispatching, this better not be the win32 event thread. + _ASSERTE(!IsWin32EventThread()); + + // If the event at the head of the queue is really the last event, or if the event at the head of the queue + // hasn't been dispatched yet, then we simply fall through and continue the process for real. However, if + // its not the last event, we send to the Win32 event thread and get it to continue, then we return. + if (HasUndispatchedNativeEvents()) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::CI: more unmanaged events need dispatching.\n"); + + // Note: if we tried to access the Left Side while stopped but couldn't, then m_oddSync will be true. We + // need to reset it to false since we're continuing now. + m_oddSync = false; + + Unlock(); + + hr = this->m_pShim->GetWin32EventThread()->SendUnmanagedContinue(this, cRealUMContinue); + + return hr; + } + } + } +#endif // FEATURE_INTEROP_DEBUGGING + + // Both the managed and unmanaged event queues are now empty. Go + // ahead and continue the process for real. + LOG((LF_CORDB, LL_INFO1000, "CP::CI: headed for true continue.\n")); + + // We need to check these while under the lock, but action must be + // taked outside of the lock. + bool fIsExiting = m_exiting; + bool fWasSynchronized = GetSynchronized(); + + // Mark that we're no longer synchronized. + if (fWasSynchronized) + { + LOG((LF_CORDB, LL_INFO1000, "CP::CI: process was synchronized.\n")); + + SetSynchronized(false); + SetSyncCompleteRecv(false); + + // we're no longer in a callback, so set flags to indicate that we've finished. + GetShim()->NotifyOnContinue(); + + // Flush will update state, including continue counter and marking + // frames dirty. + this->FlushProcessRunning(); + + + // Tell the RC event thread that something about this process has changed. + m_cordb->ProcessStateChanged(); + } + + m_continueCounter++; + + // If m_oddSync is set, then out last synchronization was due to us syncing the process because we were Win32 + // stopped. Therefore, while we do need to do most of the work to continue the process below, we don't actually have + // to send the managed continue event. Setting wasSynchronized to false here helps us do that. + if (m_oddSync) + { + fWasSynchronized = false; + m_oddSync = false; + } + +#ifdef FEATURE_INTEROP_DEBUGGING + // We must ensure that all managed threads are suspended here. We're about to let all managed threads run free via + // the managed continue message to the Left Side. If we don't suspend the managed threads, then they may start + // slipping forward even if we receive an in-band unmanaged event. We have to hijack in-band unmanaged events while + // getting the managed continue message over to the Left Side to keep the process running free. Otherwise, the + // SendIPCEvent will hang below. But in doing so, we could let managed threads slip to far. So we ensure they're all + // suspended here. + // + // Note: we only do this suspension if the helper thread hasn't died yet. If the helper thread has died, then we + // know that we're loosing the Runtime. No more managed code is going to run, so we don't bother trying to prevent + // managed threads from slipping via the call below. + // + // Note: we just remember here, under the lock, so we can unlock then wait for the syncing thread to free the + // debugger lock. Otherwise, we may block here and prevent someone from continuing from an OOB event, which also + // prevents the syncing thread from releasing the debugger lock like we want it to. + bool fNeedSuspend = fWasSynchronized && fDoWin32Continue && !m_helperThreadDead; + + // If we receive a new in-band event once we unlock, we need to know to hijack it and keep going while we're still + // trying to send the managed continue event to the process. + if (fWasSynchronized && fDoWin32Continue && !fIsExiting) + { + m_specialDeferment = true; + } + + if (fNeedSuspend) + { + // @todo - what does this actually accomplish? We already suspended everything when we first synced. + + // Any thread that may hold a lock blocking the helper is + // inside of a can't stop region, and thus we won't suspend it. + SuspendUnmanagedThreads(); + } +#endif // FEATURE_INTEROP_DEBUGGING + + Unlock(); + + // Although we've released the Process-lock, we still have the Stop-Go lock. + _ASSERTE(m_StopGoLock.HasLock()); + + // If we're processing an ExitProcess managed event, then we don't want to really continue the process, so just fall + // thru. Note: we did let the unmanaged continue go through above for this case. + if (fIsExiting) + { + LOG((LF_CORDB, LL_INFO1000, "CP::CI: continuing from exit case.\n")); + } + else if (fWasSynchronized) + { + LOG((LF_CORDB, LL_INFO1000, "CP::CI: Sending continue to AppD:0x%x.\n", VmPtrToCookie(pAppDomainToken))); +#ifdef FEATURE_INTEROP_DEBUGGING + STRESS_LOG2(LF_CORDB, LL_INFO1000, "Continue flags:special=%d, dowin32=%d\n", m_specialDeferment, fDoWin32Continue); +#endif + // Send to the RC to continue the process. + DebuggerIPCEvent * pEvent = (DebuggerIPCEvent *) _alloca(CorDBIPC_BUFFER_SIZE); + + InitIPCEvent(pEvent, DB_IPCE_CONTINUE, false, pAppDomainToken); + + hr = m_cordb->SendIPCEvent(this, pEvent, CorDBIPC_BUFFER_SIZE); + + // It is possible that we continue and then the process immediately exits before the helper + // thread is finished continuing and can report success back to us. That's arguably a success + // case sinceu the process did indeed continue, but since we didn't get the acknowledgement, + // we can't be sure it's success. So we call it S_FALSE instead of S_OK. + // @todo - how do we handle other failure here? + if (hr == CORDBG_E_PROCESS_TERMINATED) + { + hr = S_FALSE; + } + _ASSERTE(SUCCEEDED(pEvent->hr)); + + LOG((LF_CORDB, LL_INFO1000, "CP::CI: Continue sent to AppD:0x%x.\n", VmPtrToCookie(pAppDomainToken))); + } + +#ifdef FEATURE_INTEROP_DEBUGGING + // If we're win32 attached to the Left side, then we need to win32 continue the process too (unless, of course, it's + // already been done above.) + // + // Note: we do this here because we want to get the Left Side to receive and ack our continue message above if we + // were sync'd. If we were sync'd, then by definition the process (and the helper thread) is running anyway, so all + // this continue is going to do is to let the threads that have been suspended go. + if (fDoWin32Continue) + { +#ifdef _DEBUG + { + // A little pause here extends the special deferment region and thus causes native-debug + // events to get hijacked. This test some wildly different corner case paths. + // See VSWhidbey bugs 131905, 168971 + static DWORD dwRace = -1; + if (dwRace == -1) + dwRace = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_DbgRace); + + if ((dwRace & 1) == 1) + { + Sleep(30); + } + } +#endif + + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::CI: sending unmanaged continue.\n"); + + // Send to the Win32 event thread to do the unmanaged continue for us. + hr = this->m_pShim->GetWin32EventThread()->SendUnmanagedContinue(this, cRealUMContinue); + } +#endif // FEATURE_INTEROP_DEBUGGING + + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::CI: continue done, returning.\n"); + + return hr; +} + +HRESULT CordbProcess::HasQueuedCallbacks(ICorDebugThread *pThread, + BOOL *pbQueued) +{ + PUBLIC_REENTRANT_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + VALIDATE_POINTER_TO_OBJECT_OR_NULL(pThread,ICorDebugThread *); + VALIDATE_POINTER_TO_OBJECT(pbQueued,BOOL *); + + // Shim owns the event queue + if (m_pShim != NULL) + { + PUBLIC_CALLBACK_IN_THIS_SCOPE0_NO_LOCK(this); // Calling to shim, leaving RS. + *pbQueued = m_pShim->GetManagedEventQueue()->HasQueuedCallbacks(pThread); + return S_OK; + } + return E_NOTIMPL; // Not implemented in V3. +} + +// +// A small helper function to convert a CordbBreakpoint to an ICorDebugBreakpoint based on its type. +// +static ICorDebugBreakpoint *CordbBreakpointToInterface(CordbBreakpoint * pBreakpoint) +{ + _ASSERTE(pBreakpoint != NULL); + + // + // I really dislike this. We've got three subclasses of CordbBreakpoint, but we store them all into the same hash + // (m_breakpoints), so when we get one out of the hash, we don't really know what type it is. But we need to know + // what type it is because we need to cast it to the proper interface before passing it out. I.e., when we create a + // function breakpoint, we return the breakpoint casted to an ICorDebugFunctionBreakpoint. But if we grab that same + // breakpoint out of the hash as a CordbBreakpoint and pass it out as an ICorDebugBreakpoint, then that's a + // different pointer, and its wrong. So I've added the type to the breakpoint so we can cast properly here. I'd love + // to do this a different way, though... + // + // -- Mon Dec 14 21:06:46 1998 + // + switch(pBreakpoint->GetBPType()) + { + case CBT_FUNCTION: + return static_cast<ICorDebugFunctionBreakpoint *>(static_cast<CordbFunctionBreakpoint *> (pBreakpoint)); + break; + + case CBT_MODULE: + return static_cast<ICorDebugModuleBreakpoint*>(static_cast<CordbModuleBreakpoint *> (pBreakpoint)); + break; + + case CBT_VALUE: + return static_cast<ICorDebugValueBreakpoint *>(static_cast<CordbValueBreakpoint *> (pBreakpoint)); + break; + + default: + _ASSERTE(!"Invalid breakpoint type!"); + } + + return NULL; +} + + +// Callback data for code:CordbProcess::GetAssembliesInLoadOrder +class ShimAssemblyCallbackData +{ +public: + // Ctor to intialize callback data + // + // Arguments: + // pAppDomain - appdomain that the assemblies are in. + // pAssemblies - preallocated array of smart pointers to hold assemblies + // countAssemblies - size of pAssemblies in elements. + ShimAssemblyCallbackData( + CordbAppDomain * pAppDomain, + RSExtSmartPtr<ICorDebugAssembly>* pAssemblies, + ULONG countAssemblies) + { + _ASSERTE(pAppDomain != NULL); + _ASSERTE(pAssemblies != NULL); + + m_pProcess = pAppDomain->GetProcess(); + m_pAppDomain = pAppDomain; + m_pAssemblies = pAssemblies; + m_countElements = countAssemblies; + m_index = 0; + + // Just to be safe, clear them all out + for(ULONG i = 0; i < countAssemblies; i++) + { + pAssemblies[i].Clear(); + } + } + + // Dtor + // + // Notes: + // This can assert end-of-enumeration invariants. + ~ShimAssemblyCallbackData() + { + // Ensure that we went through all assemblies. + _ASSERTE(m_index == m_countElements); + } + + // Callback invoked from DAC enumeration. + // + // arguments: + // vmDomainAssembly - VMPTR for assembly + // pData - a 'this' pointer + // + static void Callback(VMPTR_DomainAssembly vmDomainAssembly, void * pData) + { + ShimAssemblyCallbackData * pThis = static_cast<ShimAssemblyCallbackData *> (pData); + INTERNAL_DAC_CALLBACK(pThis->m_pProcess); + + CordbAssembly * pAssembly = pThis->m_pAppDomain->LookupOrCreateAssembly(vmDomainAssembly); + + pThis->SetAndMoveNext(pAssembly); + } + + // Set the current index in the table and increment the cursor. + // + // Arguments: + // pAssembly - assembly from DAC enumerator + void SetAndMoveNext(CordbAssembly * pAssembly) + { + _ASSERTE(pAssembly != NULL); + + if (m_index >= m_countElements) + { + // Enumerating the assemblies in the target should be fixed since + // the target is not running. + // We should never get here unless the target is unstable. + // The caller (the shim) pre-allocated the table of assemblies. + m_pProcess->TargetConsistencyCheck(!"Target changed assembly count"); + return; + } + + m_pAssemblies[m_index].Assign(pAssembly); + m_index++; + } + +protected: + CordbProcess * m_pProcess; + CordbAppDomain * m_pAppDomain; + RSExtSmartPtr<ICorDebugAssembly>* m_pAssemblies; + ULONG m_countElements; + ULONG m_index; +}; + +//--------------------------------------------------------------------------------------- +// Shim Helper to enumerate the assemblies in the load-order +// +// Arguments: +// pAppdomain - non-null appdmomain to enumerate assemblies. +// pAssemblies - caller pre-allocated array to hold assemblies +// countAssemblies - size of the array. +// +// Notes: +// Caller preallocated array (likely from ICorDebugAssemblyEnum::GetCount), +// and now this function fills in the assemblies in the order they were +// loaded. +// +// The target should be stable, such that the number of assemblies in the +// target is stable, and therefore countAssemblies as determined by the +// shim via ICorDebugAssemblyEnum::GetCount should match the number of +// assemblies enumerated here. +// +// Called by code:ShimProcess::QueueFakeAttachEvents. +// This provides the assemblies in load-order. In contrast, +// ICorDebugAppDomain::EnumerateAssemblies is a random order. The shim needs +// load-order to match Whidbey semantics for dispatching fake load-assembly +// callbacks on attach. The debugger then uses the order +// in its module display window. +// +void CordbProcess::GetAssembliesInLoadOrder( + ICorDebugAppDomain * pAppDomain, + RSExtSmartPtr<ICorDebugAssembly>* pAssemblies, + ULONG countAssemblies) +{ + PUBLIC_API_ENTRY_FOR_SHIM(this); + RSLockHolder lockHolder(GetProcessLock()); + + _ASSERTE(GetShim() != NULL); + + CordbAppDomain * pAppDomainInternal = static_cast<CordbAppDomain *> (pAppDomain); + + ShimAssemblyCallbackData data(pAppDomainInternal, pAssemblies, countAssemblies); + + // Enumerate through and fill out pAssemblies table. + GetDAC()->EnumerateAssembliesInAppDomain( + pAppDomainInternal->GetADToken(), + ShimAssemblyCallbackData::Callback, + &data); // user data + + // pAssemblies array has now been updated. +} + +// Callback data for code:CordbProcess::GetModulesInLoadOrder +class ShimModuleCallbackData +{ +public: + // Ctor to intialize callback data + // + // Arguments: + // pAssembly - assembly that the Modules are in. + // pModules - preallocated array of smart pointers to hold Modules + // countModules - size of pModules in elements. + ShimModuleCallbackData( + CordbAssembly * pAssembly, + RSExtSmartPtr<ICorDebugModule>* pModules, + ULONG countModules) + { + _ASSERTE(pAssembly != NULL); + _ASSERTE(pModules != NULL); + + m_pProcess = pAssembly->GetAppDomain()->GetProcess(); + m_pAssembly = pAssembly; + m_pModules = pModules; + m_countElements = countModules; + m_index = 0; + + // Just to be safe, clear them all out + for(ULONG i = 0; i < countModules; i++) + { + pModules[i].Clear(); + } + } + + // Dtor + // + // Notes: + // This can assert end-of-enumeration invariants. + ~ShimModuleCallbackData() + { + // Ensure that we went through all Modules. + _ASSERTE(m_index == m_countElements); + } + + // Callback invoked from DAC enumeration. + // + // arguments: + // vmDomainFile - VMPTR for Module + // pData - a 'this' pointer + // + static void Callback(VMPTR_DomainFile vmDomainFile, void * pData) + { + ShimModuleCallbackData * pThis = static_cast<ShimModuleCallbackData *> (pData); + INTERNAL_DAC_CALLBACK(pThis->m_pProcess); + + CordbModule * pModule = pThis->m_pAssembly->GetAppDomain()->LookupOrCreateModule(vmDomainFile); + + pThis->SetAndMoveNext(pModule); + } + + // Set the current index in the table and increment the cursor. + // + // Arguments: + // pModule - Module from DAC enumerator + void SetAndMoveNext(CordbModule * pModule) + { + _ASSERTE(pModule != NULL); + + if (m_index >= m_countElements) + { + // Enumerating the Modules in the target should be fixed since + // the target is not running. + // We should never get here unless the target is unstable. + // The caller (the shim) pre-allocated the table of Modules. + m_pProcess->TargetConsistencyCheck(!"Target changed Module count"); + return; + } + + m_pModules[m_index].Assign(pModule); + m_index++; + } + +protected: + CordbProcess * m_pProcess; + CordbAssembly * m_pAssembly; + RSExtSmartPtr<ICorDebugModule>* m_pModules; + ULONG m_countElements; + ULONG m_index; +}; + +//--------------------------------------------------------------------------------------- +// Shim Helper to enumerate the Modules in the load-order +// +// Arguments: +// pAppdomain - non-null appdmomain to enumerate Modules. +// pModules - caller pre-allocated array to hold Modules +// countModules - size of the array. +// +// Notes: +// Caller preallocated array (likely from ICorDebugModuleEnum::GetCount), +// and now this function fills in the Modules in the order they were +// loaded. +// +// The target should be stable, such that the number of Modules in the +// target is stable, and therefore countModules as determined by the +// shim via ICorDebugModuleEnum::GetCount should match the number of +// Modules enumerated here. +// +// Called by code:ShimProcess::QueueFakeAssemblyAndModuleEvent. +// This provides the Modules in load-order. In contrast, +// ICorDebugAssembly::EnumerateModules is a random order. The shim needs +// load-order to match Whidbey semantics for dispatching fake load-Module +// callbacks on attach. The most important thing is that the manifest module +// gets a LodModule callback before any secondary modules. For dynamic +// modules, this is necessary for operations on the secondary module +// that rely on manifest metadata (eg. GetSimpleName). +// +// @dbgtodo : This is almost identical to GetAssembliesInLoadOrder, and +// (together wih the CallbackData classes) seems a HUGE amount of code and +// complexity for such a simple thing. We also have extra code to order +// AppDomains and Threads. We should try and rip all of this extra complexity +// out, and replace it with better data structures for storing these items. +// Eg., if we used std::map, we could have efficient lookups and ordered +// enumerations. However, we do need to be careful about exposing new invariants +// through ICorDebug that customers may depend on, which could place a long-term +// compatibility burden on us. We could have a simple generic data structure +// (eg. built on std::hash_map and std::list) which provided efficient look-up +// and both in-order and random enumeration. +// +void CordbProcess::GetModulesInLoadOrder( + ICorDebugAssembly * pAssembly, + RSExtSmartPtr<ICorDebugModule>* pModules, + ULONG countModules) +{ + PUBLIC_API_ENTRY_FOR_SHIM(this); + RSLockHolder lockHolder(GetProcessLock()); + + _ASSERTE(GetShim() != NULL); + + CordbAssembly * pAssemblyInternal = static_cast<CordbAssembly *> (pAssembly); + + ShimModuleCallbackData data(pAssemblyInternal, pModules, countModules); + + // Enumerate through and fill out pModules table. + GetDAC()->EnumerateModulesInAssembly( + pAssemblyInternal->GetDomainAssemblyPtr(), + ShimModuleCallbackData::Callback, + &data); // user data + + // pModules array has now been updated. +} + + +//--------------------------------------------------------------------------------------- +// Callback to count the number of enumerations in a process. +// +// Arguments: +// id - the connection id. +// pName - name of the connection +// pUserData - an EnumerateConnectionsData +// +// Notes: +// Helper function for code:CordbProcess::QueueFakeConnectionEvents +// +// static +void CordbProcess::CountConnectionsCallback(DWORD id, LPCWSTR pName, void * pUserData) +{ +#if defined(FEATURE_INCLUDE_ALL_INTERFACES) + EnumerateConnectionsData * pCallbackData = reinterpret_cast<EnumerateConnectionsData *>(pUserData); + INTERNAL_DAC_CALLBACK(pCallbackData->m_pThis); + + pCallbackData->m_uIndex += 1; +#endif // FEATURE_INCLUDE_ALL_INTERFACES +} + +//--------------------------------------------------------------------------------------- +// Callback to enumerate all the connections in a process. +// +// Arguments: +// id - the connection id. +// pName - name of the connection +// pUserData - an EnumerateConnectionsData +// +// Notes: +// Helper function for code:CordbProcess::QueueFakeConnectionEvents +// +// static +void CordbProcess::EnumerateConnectionsCallback(DWORD id, LPCWSTR pName, void * pUserData) +{ +#if defined(FEATURE_INCLUDE_ALL_INTERFACES) + EnumerateConnectionsData * pCallbackData = reinterpret_cast<EnumerateConnectionsData *>(pUserData); + INTERNAL_DAC_CALLBACK(pCallbackData->m_pThis); + + // get the next entry in the array to be filled in + EnumerateConnectionsEntry * pEntry = &(pCallbackData->m_pEntryArray[pCallbackData->m_uIndex]); + + // initialize the StringCopyHolder in the entry and copy over the name of the connection + new (&(pEntry->m_pName)) StringCopyHolder; + pEntry->m_pName.AssignCopy(pName); + pEntry->m_dwID = id; + + pCallbackData->m_uIndex += 1; +#endif // FEATURE_INCLUDE_ALL_INTERFACES +} + +//--------------------------------------------------------------------------------------- +// Callback from Shim to queue fake Connection events on attach. +// +// Notes: +// See code:ShimProcess::QueueFakeAttachEvents +void CordbProcess::QueueFakeConnectionEvents() +{ + PUBLIC_API_ENTRY_FOR_SHIM(this); + +#ifdef FEATURE_INCLUDE_ALL_INTERFACES + EnumerateConnectionsData callbackData; + callbackData.m_pThis = this; + callbackData.m_uIndex = 0; + callbackData.m_pEntryArray = NULL; + + UINT32 uSize = 0; + + // We must take the process lock before calling DAC primitives which will call back into DBI. + // On the other hand, we must NOT be holding the lock when we call out to the shim. + // So introduce a new scope here. + { + RSLockHolder lockHolder(GetProcessLock()); + GetDAC()->EnumerateConnections(CountConnectionsCallback, &callbackData); + + // save the size for later + uSize = callbackData.m_uIndex; + + // Allocate the array to store the connections. This array will be released when the dtor runs. + callbackData.m_uIndex = 0; + callbackData.m_pEntryArray = new EnumerateConnectionsEntry[uSize]; + GetDAC()->EnumerateConnections(EnumerateConnectionsCallback, &callbackData); + _ASSERTE(uSize == callbackData.m_uIndex); + } + + { + // V2 would send CreateConnection for all connections, and then ChangeConnection + // for all connections. + PUBLIC_CALLBACK_IN_THIS_SCOPE0_NO_LOCK(this); + for (UINT32 i = 0; i < uSize; i++) + { + EnumerateConnectionsEntry * pEntry = &(callbackData.m_pEntryArray[i]); + GetShim()->GetShimCallback()->CreateConnection( + this, + (CONNID)pEntry->m_dwID, + const_cast<WCHAR *>((const WCHAR *)(pEntry->m_pName))); + } + + for (UINT32 i = 0; i < uSize; i++) + { + EnumerateConnectionsEntry * pEntry = &(callbackData.m_pEntryArray[i]); + GetShim()->GetShimCallback()->ChangeConnection(this, (CONNID)pEntry->m_dwID); + } + } +#endif +} + +// +// DispatchRCEvent -- dispatches a previously queued IPC event received +// from the runtime controller. This represents the last amount of processing +// the DI gets to do on an event before giving it to the user. +// +#ifdef _PREFAST_ +#pragma warning(push) +#pragma warning(disable:21000) // Suppress PREFast warning about overly large function +#endif +void CordbProcess::DispatchRCEvent() +{ + INTERNAL_API_ENTRY(this); + + CONTRACTL + { + // This is happening on the RCET thread, so there's no place to propogate an error back up. + NOTHROW; + } + CONTRACTL_END; + + _ASSERTE(m_pShim != NULL); // V2 case + + // + // Note: the current thread should have the process locked when it + // enters this method. + // + _ASSERTE(ThreadHoldsProcessLock()); + + // Create/Launch paths already ensured that we had a callback. + _ASSERTE(m_cordb != NULL); + _ASSERTE(m_cordb->m_managedCallback != NULL); + _ASSERTE(m_cordb->m_managedCallback2 != NULL); + _ASSERTE(m_cordb->m_managedCallback3 != NULL); + + + // Bump up the stop count. Either we'll dispatch a managed event, + // or the logic below will decide not to dispatch one and call + // Continue itself. Either way, the stop count needs to go up by + // one... + _ASSERTE(this->GetSyncCompleteRecv()); + SetSynchronized(true); + IncStopCount(); + + // As soon as we call Unlock(), we might get neutered and lose our reference to + // the shim. Grab it now for use later. + RSExtSmartPtr<ShimProcess> pShim(m_pShim); + + Unlock(); + + _ASSERTE(!ThreadHoldsProcessLock()); + + + // We want to stay synced until after the callbacks return. This is b/c we're on the RCET, + // and we may deadlock if we send IPC events on the RCET if we're not synced (see SendIPCEvent for details). + // So here, stopcount=1. The StopContinueHolder bumps it up to 2. + // - If Cordbg calls continue in the callback, that bumps it back down to 1, but doesn't actually continue. + // The holder dtor then bumps it down to 0, doing the real continue. + // - If Cordbg doesn't call continue in the callback, then stopcount stays at 2, holder dtor drops it down to 1, + // and then the holder was just a nop. + // This gives us delayed continues w/ no extra state flags. + + + // The debugger may call Detach() immediately after it returns from the callback, but before this thread returns + // from this function. Thus after we execute the callbacks, it's possible the CordbProcess object has been neutered. + + // Since we're already sycned, the Stop from the holder here is practically a nop that just bumps up a count. + // Create an extra scope for the StopContinueHolder. + { + StopContinueHolder h; + HRESULT hr = h.Init(this); + if (FAILED(hr)) + { + CORDBSetUnrecoverableError(this, hr, 0); + } + + HRESULT hrCallback = S_OK; + // It's possible a ICorDebugProcess::Detach() may have occurred by now. + { + // @dbgtodo shim: eventually the entire RCET should be considered outside the RS. + PUBLIC_CALLBACK_IN_THIS_SCOPE0_NO_LOCK(this); + + + // Snag the first event off the queue. + // Holder will call Delete, which will invoke virtual Dtor that will release ICD objects. + // Since these are external refs, we want to do it while "outside" the RS. + NewHolder<ManagedEvent> pEvent(pShim->DequeueManagedEvent()); + + // Normally pEvent shouldn't be NULL, since this method is called when the queue is not empty. + // But due to a race between CordbProcess::Terminate(), CordbWin32EventThread::ExitProcess() and this method + // it is totally possible that the queue has already been cleaned up and we can't expect that event is always available. + if (pEvent != NULL) + { + // Since we need to access a member (m_cordb), protect this block with a + // lock and a check for Neutering (in case process detach has just + // occurred). We'll release the lock around the dispatch later on. + RSLockHolder lockHolder(GetProcessLock()); + if (!IsNeutered()) + { +#ifdef _DEBUG + // On a debug build, keep track of the last IPC event we dispatched. + m_pDBGLastIPCEventType = pEvent->GetDebugCookie(); +#endif + + ManagedEvent::DispatchArgs args(m_cordb->m_managedCallback, m_cordb->m_managedCallback2, m_cordb->m_managedCallback3); + + { + // Release lock around the dispatch of the event + RSInverseLockHolder inverseLockHolder(GetProcessLock()); + + EX_TRY + { + // This dispatches almost directly into the user's callbacks. + // It does not update any RS state. + hrCallback = pEvent->Dispatch(args); + } + EX_CATCH_HRESULT(hrCallback); + } + } + } + + } // we're now back inside the RS + + if (hrCallback == E_NOTIMPL) + { + ContinueInternal(FALSE); + } + + + } // forces Continue to be called + + Lock(); + +}; + +#ifdef _DEBUG +//--------------------------------------------------------------------------------------- +// Debug-only callback to ensure that an appdomain is not available after the ExitAppDomain event. +// +// Arguments: +// vmAppDomain - appdomain from enumeration +// pUserData - pointer to a DbgAssertAppDomainDeletedData which contains the VMAppDomain that was just deleted. +// notes: +// see code:CordbProcess::DbgAssertAppDomainDeleted for details. +void CordbProcess::DbgAssertAppDomainDeletedCallback(VMPTR_AppDomain vmAppDomain, void * pUserData) +{ + DbgAssertAppDomainDeletedData * pCallbackData = reinterpret_cast<DbgAssertAppDomainDeletedData *>(pUserData); + INTERNAL_DAC_CALLBACK(pCallbackData->m_pThis); + + VMPTR_AppDomain vmAppDomainDeleted = pCallbackData->m_vmAppDomainDeleted; + CONSISTENCY_CHECK_MSGF((vmAppDomain != vmAppDomainDeleted), + ("An ExitAppDomain event was sent for appdomain, but it still shows up in the enumeration.\n vmAppDomain=%p\n", + VmPtrToCookie(vmAppDomainDeleted))); +} + +//--------------------------------------------------------------------------------------- +// Debug-only helper to Assert that VMPTR is actually removed. +// +// Arguments: +// vmAppDomainDeleted - vmptr of appdomain that we just got exit event for. +// This should not be discoverable from the RS. +// +// Notes: +// See code:IDacDbiInterface#Enumeration for rules that we're asserting. +// Once the exit appdomain event is dispatched, the appdomain should not be discoverable by the RS. +// Else the RS may use the AppDomain* after it's deleted. +// This asserts that the AppDomain* is not discoverable. +// +// Since this is a debug-only function, it should have no side-effects. +void CordbProcess::DbgAssertAppDomainDeleted(VMPTR_AppDomain vmAppDomainDeleted) +{ + DbgAssertAppDomainDeletedData callbackData; + callbackData.m_pThis = this; + callbackData.m_vmAppDomainDeleted = vmAppDomainDeleted; + + GetDAC()->EnumerateAppDomains( + CordbProcess::DbgAssertAppDomainDeletedCallback, + &callbackData); +} + +#endif // _DEBUG + +//--------------------------------------------------------------------------------------- +// Update state and potentially Dispatch a single event. +// +// Arguments: +// pEvent - non-null pointer to debug event. +// pCallback1 - callback object to dispatch on (for V1 callbacks) +// pCallback2 - 2nd callback object to dispatch on (for new V2 callbacks) +// pCallback3 - 3rd callback object to dispatch on (for new V4 callbacks) +// +// +// Returns: +// Nothing. Throws on error. +// +// Notes: +// Generally, this will dispatch exactly 1 callback. It may dispatch 0 callbacks if there is an error +// or in other corner cases (documented within the dispatch code below). +// Errors could occur because: +// - the event is corrupted (exceptional case) +// - the RS is corrupted / OOM (exceptional case) +// Exception errors here will propogate back to the Filter() call, and there's not really anything +// a debugger can do about an error here (perhaps report it to the user). +// Errors must leave IcorDebug in a consistent state. +// +// This is dispatched directly on the Win32Event Thread in response to calling Filter. +// Therefore, this can't send any IPC events (Not an issue once everything is DAC-ized). +// A V2 shim can provide a proxy calllack that takes these events and queues them and +// does the real dispatch to the user to emulate V2 semantics. +// +void CordbProcess::RawDispatchEvent( + DebuggerIPCEvent * pEvent, + RSLockHolder * pLockHolder, + ICorDebugManagedCallback * pCallback1, + ICorDebugManagedCallback2 * pCallback2, + ICorDebugManagedCallback3 * pCallback3) +{ + CONTRACTL + { + THROWS; + } + CONTRACTL_END; + + HRESULT hr = S_OK; + // We start off with the lock, and we'll toggle it. + _ASSERTE(ThreadHoldsProcessLock()); + + + // + // Call StartEventDispatch to true to guard against calls to Continue() + // from within the user's callback. We need Continue() to behave a little + // bit differently in such a case. + // + // Also note that Win32EventThread::ExitProcess will take the lock and free all + // events in the queue. (the current event is already off the queue, so + // it will be ok). But we can't do the EP callback in the middle of this dispatch + // so if this flag is set, EP will wait on the miscWaitEvent (which will + // get set in FlushQueuedEvents when we return from here) and let us finish here. + // + StartEventDispatch(pEvent->type); + + // Keep strong references to these objects in case a callback deletes them from underneath us. + RSSmartPtr<CordbAppDomain> pAppDomain; + CordbThread * pThread = NULL; + + + // Get thread that this event is on. In attach scenarios, this may be the first time ICorDebug has seen this thread. + if (!pEvent->vmThread.IsNull()) + { + pThread = LookupOrCreateThread(pEvent->vmThread); + } + + if (!pEvent->vmAppDomain.IsNull()) + { + pAppDomain.Assign(LookupOrCreateAppDomain(pEvent->vmAppDomain)); + } + + DWORD dwVolatileThreadId = 0; + if (pThread != NULL) + { + dwVolatileThreadId = pThread->GetUniqueId(); + } + + + // + // Update the app domain that this thread lives in. + // + if ((pThread != NULL) && (pAppDomain != NULL)) + { + // It shouldn't be possible for us to see an exited AppDomain here + _ASSERTE( !pAppDomain->IsNeutered() ); + + pThread->m_pAppDomain = pAppDomain; + } + + _ASSERTE(pEvent != NULL); + _ASSERTE(pCallback1 != NULL); + _ASSERTE(pCallback2 != NULL); + _ASSERTE(pCallback3 != NULL); + + + STRESS_LOG1(LF_CORDB, LL_EVERYTHING, "Pre-Dispatch IPC event: %s\n", IPCENames::GetName(pEvent->type)); + + switch (pEvent->type & DB_IPCE_TYPE_MASK) + { + case DB_IPCE_CREATE_PROCESS: + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->CreateProcess(static_cast<ICorDebugProcess*> (this)); + } + break; + + case DB_IPCE_BREAKPOINT: + { + _ASSERTE(pThread != NULL); + _ASSERTE(pAppDomain != NULL); + + // Find the breakpoint object on this side. + CordbBreakpoint *pBreakpoint = NULL; + + // We've found cases out in the wild where we get this event on a thread we don't recognize. + // We're not sure how this happens. Add a runtime check to protect ourselves to avoid the + // an AV. We still assert because this should not be happening. + // It likely means theres some issue where we failed to send a CreateThread notification. + TargetConsistencyCheck(pThread != NULL); + pBreakpoint = pAppDomain->m_breakpoints.GetBase(LsPtrToCookie(pEvent->BreakpointData.breakpointToken)); + + if (pBreakpoint != NULL) + { + ICorDebugBreakpoint * pIBreakpoint = CordbBreakpointToInterface(pBreakpoint); + _ASSERTE(pIBreakpoint != NULL); + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE2(this, pLockHolder, pEvent, "thread=0x%p, bp=0x%p", pThread, pBreakpoint); + pCallback1->Breakpoint(pAppDomain, pThread, pIBreakpoint); + } + } + } + break; + + case DB_IPCE_USER_BREAKPOINT: + { + STRESS_LOG1(LF_CORDB, LL_INFO1000, "[%x] RCET::DRCE: user breakpoint.\n", + GetCurrentThreadId()); + + _ASSERTE(pThread != NULL); + _ASSERTE(pAppDomain != NULL); + _ASSERTE(pThread->m_pAppDomain != NULL); + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->Break(pThread->m_pAppDomain, pThread); + } + + } + break; + + case DB_IPCE_STEP_COMPLETE: + { + STRESS_LOG1(LF_CORDB, LL_INFO1000, "[%x] RCET::DRCE: step complete.\n", + GetCurrentThreadId()); + + PREFIX_ASSUME(pThread != NULL); + + CordbStepper * pStepper = m_steppers.GetBase(LsPtrToCookie(pEvent->StepData.stepperToken)); + + // It's possible the stepper is NULL if: + // - event X & step-complete are both in the queue + // - during dispatch for event X, Cordbg cancels the stepper (thus removing it from m_steppers) + // - the Step-Complete still stays in the queue, and so we're here, but out stepper's been removed. + // (This could happen for breakpoints too) + // Don't dispatch a callback if the stepper is NULL. + if (pStepper != NULL) + { + RSSmartPtr<CordbStepper> pRef(pStepper); + pStepper->m_active = false; + m_steppers.RemoveBase((ULONG_PTR)pStepper->m_id); + + { + _ASSERTE(pThread->m_pAppDomain != NULL); + PUBLIC_CALLBACK_IN_THIS_SCOPE2(this, pLockHolder, pEvent, "thrad=0x%p, stepper=0x%p", pThread, pStepper); + pCallback1->StepComplete(pThread->m_pAppDomain, pThread, pStepper, pEvent->StepData.reason); + } + + // implicit Release on pRef + } + } + break; + + case DB_IPCE_EXCEPTION: + { + STRESS_LOG1(LF_CORDB, LL_INFO1000, "[%x] RCET::DRCE: exception.\n", + GetCurrentThreadId()); + + _ASSERTE(pAppDomain != NULL); + + // For some exceptions very early in startup (eg, TypeLoad), this may have occurred before we + // even executed jitted code on the thread. We may have not received a CreateThread yet. + // In V2, we detected this and sent a LogMessage on a random thread. + // In V3, we lazily create the CordbThread objects (possibly before the CreateThread event), + // and so we know we should have one. + _ASSERTE(pThread != NULL); + + pThread->SetExInfo(pEvent->Exception.vmExceptionHandle); + + _ASSERTE(pThread->m_pAppDomain != NULL); + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->Exception(pThread->m_pAppDomain, pThread, !pEvent->Exception.firstChance); + } + + } + break; + + case DB_IPCE_SYNC_COMPLETE: + _ASSERTE(!"Should have never queued a sync complete pEvent."); + break; + + case DB_IPCE_THREAD_ATTACH: + { + STRESS_LOG1(LF_CORDB, LL_INFO100, "RCET::DRCE: thread attach : ID=%x.\n", dwVolatileThreadId); + + TargetConsistencyCheck(pThread != NULL); + { + PUBLIC_CALLBACK_IN_THIS_SCOPE1(this, pLockHolder, pEvent, "thread=0x%p", pThread); + pCallback1->CreateThread(pAppDomain, pThread); + } + } + break; + + case DB_IPCE_THREAD_DETACH: + { + STRESS_LOG2(LF_CORDB, LL_INFO100, "[%x] RCET::HRCE: thread detach : ID=%x \n", + GetCurrentThreadId(), dwVolatileThreadId); + + // If the runtime thread never entered managed code, there + // won't be a CordbThread, and CreateThread was never + // called, so don't bother calling ExitThread. + if (pThread != NULL) + { + AddToNeuterOnContinueList(pThread); + + RSSmartPtr<CordbThread> pRefThread(pThread); + + _ASSERTE(pAppDomain != NULL); + + // A thread is reported as dead before we get the exit event. + // See code:IDacDbiInterface#IsThreadMarkedDead for the invariant being asserted here. + TargetConsistencyCheck(pThread->IsThreadDead()); + + // Enforce the enumeration invariants (see code:IDacDbiInterface#Enumeration)that the thread is not discoverable. + INDEBUG(pThread->DbgAssertThreadDeleted()); + + // Remove the thread from the hash. If we've removed it from the hash, we really should + // neuter it ... but that causes test failures. + // We'll neuter it in continue. + m_userThreads.RemoveBase(VmPtrToCookie(pThread->m_vmThreadToken)); + + + LOG((LF_CORDB, LL_INFO1000, "[%x] RCET::HRCE: sending thread detach.\n", GetCurrentThreadId())); + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->ExitThread(pAppDomain, pThread); + } + + // Implicit release on thread & pAppDomain + } + } + break; + + case DB_IPCE_METADATA_UPDATE: + { + CordbModule * pModule = pAppDomain->LookupOrCreateModule(pEvent->MetadataUpdateData.vmDomainFile); + pModule->RefreshMetaData(); + } + break; + + case DB_IPCE_LOAD_MODULE: + { + _ASSERTE (pAppDomain != NULL); + CordbModule * pModule = pAppDomain->LookupOrCreateModule(pEvent->LoadModuleData.vmDomainFile); + + { + pModule->SetLoadEventContinueMarker(); + + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->LoadModule(pAppDomain, pModule); + } + + } + break; + + case DB_IPCE_CREATE_CONNECTION: + { + STRESS_LOG1(LF_CORDB, LL_INFO100, + "RCET::HRCE: Connection change %d \n", + pEvent->CreateConnection.connectionId); + + // pass back the connection id and the connection name. + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback2->CreateConnection( + this, + pEvent->CreateConnection.connectionId, + const_cast<WCHAR*> (pEvent->CreateConnection.wzConnectionName.GetString())); + } + break; + + case DB_IPCE_DESTROY_CONNECTION: + { + STRESS_LOG1(LF_CORDB, LL_INFO100, + "RCET::HRCE: Connection destroyed %d \n", + pEvent->ConnectionChange.connectionId); + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback2->DestroyConnection(this, pEvent->ConnectionChange.connectionId); + } + break; + + case DB_IPCE_CHANGE_CONNECTION: + { + STRESS_LOG1(LF_CORDB, LL_INFO100, + "RCET::HRCE: Connection changed %d \n", + pEvent->ConnectionChange.connectionId); + + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback2->ChangeConnection(this, pEvent->ConnectionChange.connectionId); + } + break; + + case DB_IPCE_UNLOAD_MODULE: + { + STRESS_LOG3(LF_CORDB, LL_INFO100, "RCET::HRCE: unload module on thread %#x Mod:0x%x AD:0x%08x\n", + dwVolatileThreadId, + VmPtrToCookie(pEvent->UnloadModuleData.vmDomainFile), + VmPtrToCookie(pEvent->vmAppDomain)); + + PREFIX_ASSUME (pAppDomain != NULL); + + CordbModule *module = pAppDomain->LookupOrCreateModule(pEvent->UnloadModuleData.vmDomainFile); + + if (module == NULL) + { + LOG((LF_CORDB, LL_INFO100, "Already unloaded Module - continue()ing!" )); + break; + } + _ASSERTE(module != NULL); + INDEBUG(module->DbgAssertModuleDeleted()); + + // The appdomain we're unloading in must be the appdomain we were loaded in. Otherwise, we've got mismatched + // module and appdomain pointers. Bugs 65943 & 81728. + _ASSERTE(pAppDomain == module->GetAppDomain()); + + // Ensure the module gets neutered once we call continue. + AddToNeuterOnContinueList(module); // throws + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->UnloadModule(pAppDomain, module); + } + + pAppDomain->m_modules.RemoveBase(VmPtrToCookie(pEvent->UnloadModuleData.vmDomainFile)); + } + break; + + case DB_IPCE_LOAD_CLASS: + { + CordbClass *pClass = NULL; + + LOG((LF_CORDB, LL_INFO10000, + "RCET::HRCE: load class on thread %#x Tok:0x%08x Mod:0x%08x Asm:0x%08x AD:0x%08x\n", + dwVolatileThreadId, + pEvent->LoadClass.classMetadataToken, + VmPtrToCookie(pEvent->LoadClass.vmDomainFile), + LsPtrToCookie(pEvent->LoadClass.classDebuggerAssemblyToken), + VmPtrToCookie(pEvent->vmAppDomain))); + + _ASSERTE (pAppDomain != NULL); + + CordbModule* pModule = pAppDomain->LookupOrCreateModule(pEvent->LoadClass.vmDomainFile); + if (pModule == NULL) + { + LOG((LF_CORDB, LL_INFO100, "Load Class on not-loaded Module - continue()ing!" )); + break; + } + _ASSERTE(pModule != NULL); + + BOOL fDynamic = pModule->IsDynamic(); + + // If this is a class load in a dynamic module, the metadata has become invalid. + if (fDynamic) + { + pModule->RefreshMetaData(); + } + + hr = pModule->LookupOrCreateClass(pEvent->LoadClass.classMetadataToken, &pClass); + _ASSERTE(SUCCEEDED(hr) == (pClass != NULL)); + IfFailThrow(hr); + + // Prevent class load from being sent twice. + // @dbgtodo - Microsoft, cordbclass: this is legacy. Can this really happen? Investigate as we dac-ize CordbClass. + if (pClass->LoadEventSent()) + { + // Dynamic modules are dynamic at the module level - + // you can't add a new version of a class once the module + // is baked. + // EnC adds completely new classes. + // There shouldn't be any other way to send multiple + // ClassLoad events. + // Except that there are race conditions between loading + // an appdomain, and loading a class, so if we get the extra + // class load, we should ignore it. + break; //out of the switch statement + } + pClass->SetLoadEventSent(TRUE); + + + if (pClass != NULL) + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->LoadClass(pAppDomain, pClass); + } + } + break; + + case DB_IPCE_UNLOAD_CLASS: + { + LOG((LF_CORDB, LL_INFO10000, + "RCET::HRCE: unload class on thread %#x Tok:0x%08x Mod:0x%08x AD:0x%08x\n", + dwVolatileThreadId, + pEvent->UnloadClass.classMetadataToken, + VmPtrToCookie(pEvent->UnloadClass.vmDomainFile), + VmPtrToCookie(pEvent->vmAppDomain))); + + // get the appdomain object + _ASSERTE (pAppDomain != NULL); + + CordbModule *pModule = pAppDomain->LookupOrCreateModule(pEvent->UnloadClass.vmDomainFile); + if (pModule == NULL) + { + LOG((LF_CORDB, LL_INFO100, "Unload Class on not-loaded Module - continue()ing!" )); + break; + } + _ASSERTE(pModule != NULL); + + CordbClass *pClass = pModule->LookupClass(pEvent->UnloadClass.classMetadataToken); + + if (pClass != NULL && !pClass->HasBeenUnloaded()) + { + pClass->SetHasBeenUnloaded(true); + + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->UnloadClass(pAppDomain, pClass); + } + } + break; + + case DB_IPCE_FIRST_LOG_MESSAGE: + { + _ASSERTE(pThread != NULL); + _ASSERTE(pAppDomain != NULL); + + const WCHAR * pszContent = pEvent->FirstLogMessage.szContent.GetString(); + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->LogMessage( + pAppDomain, + pThread, + pEvent->FirstLogMessage.iLevel, + const_cast<WCHAR*> (pEvent->FirstLogMessage.szCategory.GetString()), + const_cast<WCHAR*> (pszContent)); + } + } + break; + + case DB_IPCE_LOGSWITCH_SET_MESSAGE: + { + + LOG((LF_CORDB, LL_INFO10000, + "[%x] RCET::DRCE: Log Switch Setting Message.\n", + GetCurrentThreadId())); + + _ASSERTE(pThread != NULL); + + const WCHAR *pstrLogSwitchName = pEvent->LogSwitchSettingMessage.szSwitchName.GetString(); + const WCHAR *pstrParentName = pEvent->LogSwitchSettingMessage.szParentSwitchName.GetString(); + + // from the thread object get the appdomain object + _ASSERTE(pAppDomain == pThread->m_pAppDomain); + _ASSERTE (pAppDomain != NULL); + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->LogSwitch( + pAppDomain, + pThread, + pEvent->LogSwitchSettingMessage.iLevel, + pEvent->LogSwitchSettingMessage.iReason, + const_cast<WCHAR*> (pstrLogSwitchName), + const_cast<WCHAR*> (pstrParentName)); + + } + } + + break; + case DB_IPCE_CUSTOM_NOTIFICATION: + { + _ASSERTE(pThread != NULL); + _ASSERTE(pAppDomain != NULL); + + + // determine first whether custom notifications for this type are enabled -- if not + // we just return without doing anything. + CordbClass * pNotificationClass = LookupClass(pAppDomain, + pEvent->CustomNotification.vmDomainFile, + pEvent->CustomNotification.classToken); + + // if the class is NULL, that means the debugger never enabled notifications for it. Otherwise, + // the CordbClass instance would already have been created when the notifications were + // enabled. + if ((pNotificationClass != NULL) && pNotificationClass->CustomNotificationsEnabled()) + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback3->CustomNotification(pThread, pAppDomain); + } + } + + break; + + case DB_IPCE_CREATE_APP_DOMAIN: + { + STRESS_LOG2(LF_CORDB, LL_INFO100, + "RCET::HRCE: create appdomain on thread %#x AD:0x%08x \n", + dwVolatileThreadId, + VmPtrToCookie(pEvent->vmAppDomain)); + + + // Enumerate may have prepopulated the appdomain, so check if it already exists. + // Either way, still send the CreateEvent. (We don't want to skip the Create event + // just because the debugger did an enumerate) + // We remove AppDomains from the hash as soon as they are exited. + pAppDomain.Assign(LookupOrCreateAppDomain(pEvent->AppDomainData.vmAppDomain)); + _ASSERTE(pAppDomain != NULL); // throws on failure + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + hr = pCallback1->CreateAppDomain(this, pAppDomain); + } + } + + + break; + + case DB_IPCE_EXIT_APP_DOMAIN: + { + STRESS_LOG2(LF_CORDB, LL_INFO100, "RCET::HRCE: exit appdomain on thread %#x AD:0x%08x \n", + dwVolatileThreadId, + VmPtrToCookie(pEvent->vmAppDomain)); + + // In debug-only builds, assert that the appdomain is indeed deleted and not discoverable. + INDEBUG(DbgAssertAppDomainDeleted(pEvent->vmAppDomain)); + + // If we get an ExitAD message for which we have no AppDomain, then ignore it. + // This can happen if an AD gets torn down very early (before the LS AD is to the + // point that it can be published). + // This could also happen if we attach a debugger right before the Exit event is sent. + // In this case, the debuggee is no longer publishing the appdomain. + if (pAppDomain == NULL) + { + break; + } + _ASSERTE (pAppDomain != NULL); + + // See if this is the default AppDomain exiting. This should only happen very late in + // the shutdown cycle, and so we shouldn't do anything significant with m_pDefaultDomain==NULL. + // We should try and remove m_pDefaultDomain entirely since we can't count on it always existing. + if (pAppDomain == m_pDefaultAppDomain) + { + m_pDefaultAppDomain = NULL; + } + + // Update any threads which were last seen in this AppDomain. We don't + // get any notification when a thread leaves an AppDomain, so our idea + // of what AppDomain the thread is in may be out of date. + UpdateThreadsForAdUnload( pAppDomain ); + + // This will still maintain weak references so we could call Continue. + AddToNeuterOnContinueList(pAppDomain); + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + hr = pCallback1->ExitAppDomain(this, pAppDomain); + } + + // @dbgtodo appdomain: This should occur before the callback. + // Even after ExitAppDomain, the outside world will want to continue calling + // Continue (and thus they may need to call CordbAppDomain::GetProcess(), which Neutering + // would clear). Thus we can't neuter yet. + + // Remove this app domain. This means any attempt to lookup the AppDomain + // will fail (which we do at the top of this method). Since any threads (incorrectly) referring + // to this AppDomain have been moved to the default AppDomain, no one should be + // interested in looking this AppDomain up anymore. + m_appDomains.RemoveBase(VmPtrToCookie(pEvent->vmAppDomain)); + } + + break; + + case DB_IPCE_LOAD_ASSEMBLY: + { + LOG((LF_CORDB, LL_INFO100, + "RCET::HRCE: load assembly on thread %#x Asm:0x%08x AD:0x%08x \n", + dwVolatileThreadId, + VmPtrToCookie(pEvent->AssemblyData.vmDomainAssembly), + VmPtrToCookie(pEvent->vmAppDomain))); + + _ASSERTE (pAppDomain != NULL); + + // Determine if this Assembly is cached. + CordbAssembly * pAssembly = pAppDomain->LookupOrCreateAssembly(pEvent->AssemblyData.vmDomainAssembly); + _ASSERTE(pAssembly != NULL); // throws on error + + // If created, or have, an Assembly, notify callback. + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + hr = pCallback1->LoadAssembly(pAppDomain, pAssembly); + } + } + + break; + + case DB_IPCE_UNLOAD_ASSEMBLY: + { + LOG((LF_CORDB, LL_INFO100, "RCET::DRCE: unload assembly on thread %#x Asm:0x%x AD:0x%x\n", + dwVolatileThreadId, + VmPtrToCookie(pEvent->AssemblyData.vmDomainAssembly), + VmPtrToCookie(pEvent->vmAppDomain))); + + _ASSERTE (pAppDomain != NULL); + + CordbAssembly * pAssembly = pAppDomain->LookupOrCreateAssembly(pEvent->AssemblyData.vmDomainAssembly); + + if (pAssembly == NULL) + { + // No assembly. This could happen if we attach right before an unload event is sent. + return; + } + _ASSERTE(pAssembly != NULL); + INDEBUG(pAssembly->DbgAssertAssemblyDeleted()); + + // Ensure the assembly gets neutered when we call continue. + AddToNeuterOnContinueList(pAssembly); // throws + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + hr = pCallback1->UnloadAssembly(pAppDomain, pAssembly); + } + + pAppDomain->RemoveAssemblyFromCache(pEvent->AssemblyData.vmDomainAssembly); + } + + break; + + case DB_IPCE_FUNC_EVAL_COMPLETE: + { + LOG((LF_CORDB, LL_INFO1000, "RCET::DRCE: func eval complete.\n")); + + CordbEval *pEval = NULL; + { + pEval = pEvent->FuncEvalComplete.funcEvalKey.UnWrapAndRemove(this); + if (pEval == NULL) + { + _ASSERTE(!"Bogus FuncEval handle in IPC block."); + // Bogus handle in IPC block. + break; + } + } + _ASSERTE(pEval != NULL); + + _ASSERTE(pThread != NULL); + _ASSERTE(pAppDomain != NULL); + + CONSISTENCY_CHECK_MSGF(pEval->m_DbgAppDomainStarted == pAppDomain, + ("AppDomain changed from Func-Eval. Eval=%p, Started=%p, Now=%p\n", + pEval, pEval->m_DbgAppDomainStarted, (void*) pAppDomain)); + + // Hold the data about the result in the CordbEval for later. + pEval->m_complete = true; + pEval->m_successful = !!pEvent->FuncEvalComplete.successful; + pEval->m_aborted = !!pEvent->FuncEvalComplete.aborted; + pEval->m_resultAddr = pEvent->FuncEvalComplete.resultAddr; + pEval->m_vmObjectHandle = pEvent->FuncEvalComplete.vmObjectHandle; + pEval->m_resultType = pEvent->FuncEvalComplete.resultType; + pEval->m_resultAppDomainToken = pEvent->FuncEvalComplete.vmAppDomain; + + CordbAppDomain *pResultAppDomain = LookupOrCreateAppDomain(pEvent->FuncEvalComplete.vmAppDomain); + + _ASSERTE(OutstandingEvalCount() > 0); + DecrementOutstandingEvalCount(); + + CONSISTENCY_CHECK_MSGF(pEval->m_DbgAppDomainStarted == pAppDomain, + ("AppDomain changed from Func-Eval. Eval=%p, Started=%p, Now=%p\n", + pEval, pEval->m_DbgAppDomainStarted, (void*) pAppDomain)); + + // If we did this func eval with this thread stopped at an excpetion, then we need to pretend as if we + // really didn't continue from the exception, since, of course, we really didn't on the Left Side. + if (pEval->IsEvalDuringException()) + { + pThread->SetExInfo(pEval->m_vmThreadOldExceptionHandle); + } + + bool fEvalCompleted = pEval->m_successful || pEval->m_aborted; + + // If a CallFunction() is aborted, the LHS may not complete the abort + // immediately and hence we cant do a SendCleanup() at that point. Also, + // the debugger may (incorrectly) release the CordbEval before this + // DB_IPCE_FUNC_EVAL_COMPLETE event is received. Hence, we maintain an + // extra ref-count to determine when this can be done. + // Note that this can cause a two-way DB_IPCE_FUNC_EVAL_CLEANUP event + // to be sent. Hence, it has to be done before the Continue (see issue 102745). + + + // Note that if the debugger has already (incorrectly) released the CordbEval, + // pEval will be pointing to garbage and should not be used by the debugger. + if (fEvalCompleted) + { + PUBLIC_CALLBACK_IN_THIS_SCOPE2(this, pLockHolder, pEvent, "thread=0x%p, eval=0x%p. (Complete)", pThread, pEval); + pCallback1->EvalComplete(pResultAppDomain, pThread, pEval); + } + else + { + PUBLIC_CALLBACK_IN_THIS_SCOPE2(this, pLockHolder, pEvent, "pThread=0x%p, eval=0x%p. (Exception)", pThread, pEval); + pCallback1->EvalException(pResultAppDomain, pThread, pEval); + } + + // This release may send an DB_IPCE_FUNC_EVAL_CLEANUP IPC event. That's ok b/c + // we're still synced even if if Continue was called inside the callback. + // That's because the StopContinueHolder bumped up the stopcount. + // Corresponding AddRef() in CallFunction(). + // @todo - this is leaked if we don't get an EvalComplete event (eg, process exits with + // in middle of func-eval). + pEval->Release(); + } + break; + + + case DB_IPCE_NAME_CHANGE: + { + LOG((LF_CORDB, LL_INFO1000, "RCET::HRCE: Name Change %d 0x%p\n", + dwVolatileThreadId, + VmPtrToCookie(pEvent->NameChange.vmAppDomain))); + + pThread = NULL; + pAppDomain.Clear(); + if (pEvent->NameChange.eventType == THREAD_NAME_CHANGE) + { + // Lookup the CordbThread that matches this runtime thread. + if (!pEvent->NameChange.vmThread.IsNull()) + { + pThread = LookupOrCreateThread(pEvent->NameChange.vmThread); + } + } + else + { + _ASSERTE (pEvent->NameChange.eventType == APP_DOMAIN_NAME_CHANGE); + pAppDomain.Assign(LookupOrCreateAppDomain(pEvent->NameChange.vmAppDomain)); + if (pAppDomain) + { + pAppDomain->InvalidateName(); + } + } + + if (pThread || pAppDomain) + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback1->NameChange(pAppDomain, pThread); + } + } + + break; + + case DB_IPCE_UPDATE_MODULE_SYMS: + { + RSExtSmartPtr<IStream> pStream; + + // Find the app domain the module lives in. + _ASSERTE (pAppDomain != NULL); + + // Find the Right Side module for this module. + CordbModule * pModule = pAppDomain->LookupOrCreateModule(pEvent->UpdateModuleSymsData.vmDomainFile); + _ASSERTE(pModule != NULL); + + // This is a legacy event notification for updated PDBs. + // Creates a new IStream object. Ownership is handed off via callback. + IDacDbiInterface::SymbolFormat symFormat = pModule->GetInMemorySymbolStream(&pStream); + + // We shouldn't get this event if there aren't PDB symbols waiting. Specifically we don't want + // to incur the cost of copying over ILDB symbols here without the debugger asking for them. + // Eventually we may remove this callback as well and always rely on explicit requests. + _ASSERTE(symFormat == IDacDbiInterface::kSymbolFormatPDB); + + if (symFormat == IDacDbiInterface::kSymbolFormatPDB) + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + + _ASSERTE(pStream != NULL); // Shouldn't send the event if we don't have a stream. + + pCallback1->UpdateModuleSymbols(pAppDomain, pModule, pStream); + } + + } + break; + + case DB_IPCE_MDA_NOTIFICATION: + { + RSInitHolder<CordbMDA> pMDA(new CordbMDA(this, &pEvent->MDANotification)); // throws + + // Ctor leaves both internal + ext Ref at 0, adding to neuter list bumps int-ref up to 1. + // Neutering will dump it back down to zero. + this->AddToNeuterOnExitList(pMDA); + + // We bump up and down the external ref so that even if the callback doensn't touch the refs, + // our Ext-Release here will still cause a 1->0 ext-ref transition, which will get it + // swept on the neuter list. + RSExtSmartPtr<ICorDebugMDA> pExternalMDARef; + pMDA.TransferOwnershipExternal(&pExternalMDARef); + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + + pCallback2->MDANotification( + this, + pThread, // may be null + pExternalMDARef); + + // pExternalMDARef's dtor will do an external release, + // which is very significant because it may be the one that does the 1->0 ext ref transition, + // which may mean cause the "NeuterAtWill" bit to get flipped on this CordbMDA object. + // Since this is an external release, do it in the PUBLIC_CALLBACK scope. + pExternalMDARef.Clear(); + } + + break; + } + + case DB_IPCE_CONTROL_C_EVENT: + { + hr = S_FALSE; + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + hr = pCallback1->ControlCTrap((ICorDebugProcess*) this); + } + + { + RSLockHolder ch(this->GetStopGoLock()); + + DebuggerIPCEvent eventControlCResult; + + InitIPCEvent(&eventControlCResult, + DB_IPCE_CONTROL_C_EVENT_RESULT, + false, + VMPTR_AppDomain::NullPtr()); + + // Indicate whether the debugger has handled the event. + eventControlCResult.hr = hr; + + // Send the reply to the LS. + SendIPCEvent(&eventControlCResult, sizeof(eventControlCResult)); + } // release SG lock + + } + break; + + // EnC Remap opportunity + case DB_IPCE_ENC_REMAP: + { + LOG((LF_CORDB, LL_INFO1000, "[%x] RCET::DRCE: EnC Remap!.\n", + GetCurrentThreadId())); + + _ASSERTE(NULL != pAppDomain); + + CordbModule * pModule = pAppDomain->LookupOrCreateModule(pEvent->EnCRemap.vmDomainFile); + PREFIX_ASSUME(pModule != NULL); + + CordbFunction * pCurFunction = NULL; + CordbFunction * pResumeFunction = NULL; + + // lookup the version of the function that we are mapping from + // this is the one that is currently running + pCurFunction = pModule->LookupOrCreateFunction( + pEvent->EnCRemap.funcMetadataToken, pEvent->EnCRemap.currentVersionNumber); + + // lookup the version of the function that we are mapping to + // it will always be the most recent + pResumeFunction = pModule->LookupOrCreateFunction( + pEvent->EnCRemap.funcMetadataToken, pEvent->EnCRemap.resumeVersionNumber); + + _ASSERTE(pCurFunction->GetEnCVersionNumber() < pResumeFunction->GetEnCVersionNumber()); + + RSSmartPtr<CordbFunction> pRefCurFunction(pCurFunction); + RSSmartPtr<CordbFunction> pRefResumeFunction(pResumeFunction); + + // Verify we're not about to overwrite an outstanding remap IP + // This should only be set while a remap opportunity is being handled, + // and cleared (by CordbThread::MarkStackFramesDirty) on Continue. + // We want to be absolutely sure we don't accidentally keep a stale pointer + // around because it would point to arbitrary stack space in the CLR potentially + // leading to stack corruption. + _ASSERTE( pThread->m_EnCRemapFunctionIP == NULL ); + + // Stash the address of the remap IP buffer. This indicates that calling + // RemapFunction is valid and provides a communications channel between the RS + // and LS for the remap IL offset. + pThread->m_EnCRemapFunctionIP = pEvent->EnCRemap.resumeILOffset; + + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback2->FunctionRemapOpportunity( + pAppDomain, + pThread, + pCurFunction, + pResumeFunction, + (ULONG32)pEvent->EnCRemap.currentILOffset); + } + + // Implicit release on pCurFunction and pResumeFunction. + } + break; + + // EnC Remap complete + case DB_IPCE_ENC_REMAP_COMPLETE: + { + LOG((LF_CORDB, LL_INFO1000, "[%x] RCET::DRCE: EnC Remap Complete!.\n", + GetCurrentThreadId())); + + _ASSERTE(NULL != pAppDomain); + + CordbModule* pModule = pAppDomain->LookupOrCreateModule(pEvent->EnCRemap.vmDomainFile); + PREFIX_ASSUME(pModule != NULL); + + // Find the function we're remapping to, which must be the latest version + CordbFunction *pRemapFunction= + pModule->LookupFunctionLatestVersion(pEvent->EnCRemapComplete.funcMetadataToken); + PREFIX_ASSUME(pRemapFunction != NULL); + + // Dispatch the FunctionRemapComplete callback + RSSmartPtr<CordbFunction> pRef(pRemapFunction); + { + PUBLIC_CALLBACK_IN_THIS_SCOPE(this, pLockHolder, pEvent); + pCallback2->FunctionRemapComplete(pAppDomain, pThread, pRemapFunction); + } + // Implicit release on pRemapFunction via holder + } + break; + + case DB_IPCE_BREAKPOINT_SET_ERROR: + { + LOG((LF_CORDB, LL_INFO1000, "RCET::DRCE: breakpoint set error.\n")); + + RSSmartPtr<CordbBreakpoint> pRef; + + _ASSERTE(pThread != NULL); + _ASSERTE(pAppDomain != NULL); + + // Find the breakpoint object on this side. + CordbBreakpoint * pBreakpoint = NULL; + + + if (pThread == NULL) + { + // We've found cases out in the wild where we get this event on a thread we don't recognize. + // We're not sure how this happens. Add a runtime check to protect ourselves to avoid the + // an AV. We still assert because this should not be happening. + // It likely means theres some issue where we failed to send a CreateThread notification. + STRESS_LOG1(LF_CORDB, LL_INFO1000, "BreakpointSetError on unrecognized thread. %p\n", pBreakpoint); + + _ASSERTE(!"Missing thread on bp set error"); + break; + } + + pBreakpoint = pAppDomain->m_breakpoints.GetBase(LsPtrToCookie(pEvent->BreakpointSetErrorData.breakpointToken)); + + if (pBreakpoint != NULL) + { + ICorDebugBreakpoint * pIBreakpoint = CordbBreakpointToInterface(pBreakpoint); + _ASSERTE(pIBreakpoint != NULL); + { + PUBLIC_CALLBACK_IN_THIS_SCOPE2(this, pLockHolder, pEvent, "thread=0x%p, bp=0x%p", pThread, pBreakpoint); + pCallback1->BreakpointSetError(pAppDomain, pThread, pIBreakpoint, 0); + } + } + // Implicit release on pRef. + } + break; + + + case DB_IPCE_EXCEPTION_CALLBACK2: + { + STRESS_LOG4(LF_CORDB, LL_INFO100, + "RCET::DRCE: Exception2 0x%p 0x%X 0x%X 0x%X\n", + pEvent->ExceptionCallback2.framePointer.GetSPValue(), + pEvent->ExceptionCallback2.nOffset, + pEvent->ExceptionCallback2.eventType, + pEvent->ExceptionCallback2.dwFlags + ); + + if (pThread == NULL) + { + // We've got an exception on a thread we don't know about. This could be a thread that + // has never run any managed code, so let's just ignore the exception. We should have + // already sent a log message about this situation for the EXCEPTION callback above. + _ASSERTE( pEvent->ExceptionCallback2.eventType == DEBUG_EXCEPTION_UNHANDLED ); + break; + } + + pThread->SetExInfo(pEvent->ExceptionCallback2.vmExceptionHandle); + + // + // Send all the information back to the debugger. + // + RSSmartPtr<CordbFrame> pFrame; + + FramePointer fp = pEvent->ExceptionCallback2.framePointer; + if (fp != LEAF_MOST_FRAME) + { + // The interface forces us to to pass a FramePointer via an ICorDebugFrame. + // However, we can't get a real ICDFrame without a stackwalk, and we don't + // want to do a stackwalk now. so pass a netuered proxy frame. The shim + // can map this to a real frame. + // See comments at CordbPlaceHolderFrame class for details. + pFrame.Assign(new CordbPlaceholderFrame(this, fp)); + } + + CorDebugExceptionCallbackType type = pEvent->ExceptionCallback2.eventType; + { + PUBLIC_CALLBACK_IN_THIS_SCOPE3(this, pLockHolder, pEvent, "pThread=0x%p, frame=%p, type=%d", pThread, (ICorDebugFrame*) pFrame, type); + hr = pCallback2->Exception( + pThread->m_pAppDomain, + pThread, + pFrame, + (ULONG32)(pEvent->ExceptionCallback2.nOffset), + type, + pEvent->ExceptionCallback2.dwFlags); + } + } + break; + + case DB_IPCE_EXCEPTION_UNWIND: + { + STRESS_LOG2(LF_CORDB, LL_INFO100, + "RCET::DRCE: Exception Unwind 0x%X 0x%X\n", + pEvent->ExceptionCallback2.eventType, + pEvent->ExceptionCallback2.dwFlags + ); + + if (pThread == NULL) + { + // We've got an exception on a thread we don't know about. This probably should never + // happen (if it's unwinding, then we expect a managed frame on the stack, and so we should + // know about the thread), but if it does fall back to ignoring the exception. + _ASSERTE( !"Got unwind event for unknown exception" ); + break; + } + + // + // Send all the information back to the debugger. + // + { + PUBLIC_CALLBACK_IN_THIS_SCOPE1(this, pLockHolder, pEvent, "pThread=0x%p", pThread); + hr = pCallback2->ExceptionUnwind( + pThread->m_pAppDomain, + pThread, + pEvent->ExceptionUnwind.eventType, + pEvent->ExceptionUnwind.dwFlags); + } + } + break; + + + case DB_IPCE_INTERCEPT_EXCEPTION_COMPLETE: + { + STRESS_LOG0(LF_CORDB, LL_INFO100, "RCET::DRCE: Exception Interception Complete.\n"); + + if (pThread == NULL) + { + // We've got an exception on a thread we don't know about. This probably should never + // happen (if it's unwinding, then we expect a managed frame on the stack, and so we should + // know about the thread), but if it does fall back to ignoring the exception. + _ASSERTE( !"Got complete event for unknown exception" ); + break; + } + + // + // Tell the debugger that the exception has been intercepted. This is similar to the + // notification we give when we start unwinding for a non-intercepted exception, except that the + // interception has been completed at this point, which means that we are conceptually at the end + // of the second pass. + // + { + PUBLIC_CALLBACK_IN_THIS_SCOPE1(this, pLockHolder, pEvent, "pThread=0x%p", pThread); + hr = pCallback2->ExceptionUnwind( + pThread->m_pAppDomain, + pThread, + DEBUG_EXCEPTION_INTERCEPTED, + 0); + } + } + break; +#ifdef TEST_DATA_CONSISTENCY + case DB_IPCE_TEST_CRST: + { + EX_TRY + { + // the left side has signaled that we should test whether pEvent->TestCrstData.vmCrst is held + GetDAC()->TestCrst(pEvent->TestCrstData.vmCrst); + } + EX_CATCH_HRESULT(hr); + + if (pEvent->TestCrstData.fOkToTake) + { + _ASSERTE(hr == S_OK); + if (hr != S_OK) + { + // we want to catch this in retail builds too + ThrowHR(E_FAIL); + } + } + else // the lock was already held + { + // see if we threw because the lock was held + _ASSERTE(hr == CORDBG_E_PROCESS_NOT_SYNCHRONIZED); + if (hr != CORDBG_E_PROCESS_NOT_SYNCHRONIZED) + { + // we want to catch this in retail builds too + ThrowHR(E_FAIL); + } + } + + } + break; + + case DB_IPCE_TEST_RWLOCK: + { + EX_TRY + { + // the left side has signaled that we should test whether pEvent->TestRWLockData.vmRWLock is held + GetDAC()->TestRWLock(pEvent->TestRWLockData.vmRWLock); + } + EX_CATCH_HRESULT(hr); + + if (pEvent->TestRWLockData.fOkToTake) + { + _ASSERTE(hr == S_OK); + if (hr != S_OK) + { + // we want to catch this in retail builds too + ThrowHR(E_FAIL); + } + } + else // the lock was already held + { + // see if we threw because the lock was held + _ASSERTE(hr == CORDBG_E_PROCESS_NOT_SYNCHRONIZED); + if (hr != CORDBG_E_PROCESS_NOT_SYNCHRONIZED) + { + // we want to catch this in retail builds too + ThrowHR(E_FAIL); + } + } + } + break; +#endif + + default: + _ASSERTE(!"Unknown event"); + LOG((LF_CORDB, LL_INFO1000, + "[%x] RCET::HRCE: Unknown event: 0x%08x\n", + GetCurrentThreadId(), pEvent->type)); + } + + + FinishEventDispatch(); +} +#ifdef _PREFAST_ +#pragma warning(pop) +#endif + +//--------------------------------------------------------------------------------------- +// Callback for prepopulating threads. +// +// Arugments: +// vmThread - thread as part of the eunmeration. +// pUserData - data supplied with callback. It's a CordbProcess* object. +// + +// static +void CordbProcess::ThreadEnumerationCallback(VMPTR_Thread vmThread, void * pUserData) +{ + CordbProcess * pThis = reinterpret_cast<CordbProcess *> (pUserData); + INTERNAL_DAC_CALLBACK(pThis); + + STRESS_LOG0(LF_CORDB, LL_INFO1000, "ThreadEnumerationCallback()\n"); + + // Do lookup / lazy-create. + pThis->LookupOrCreateThread(vmThread); +} + +//--------------------------------------------------------------------------------------- +// Fully build up the CordbThread cache to match VM state. +void CordbProcess::PrepopulateThreadsOrThrow() +{ + RSLockHolder lockHolder(GetProcessLock()); + if (IsDacInitialized()) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "PrepopulateThreadsOrThrow()\n"); + GetDAC()->EnumerateThreads(ThreadEnumerationCallback, this); + } +} + +//--------------------------------------------------------------------------------------- +// Create a Thread enumerator +// +// Arguments: +// pOwnerObj - object (a CordbProcess or CordbThread) that will own the enumerator. +// pOwnerList - the neuter list that the enumerator will live on +// pHolder - an outparameter for the enumerator to be initialized. +// +void CordbProcess::BuildThreadEnum(CordbBase * pOwnerObj, NeuterList * pOwnerList, RSInitHolder<CordbHashTableEnum> * pHolder) +{ + CordbHashTableEnum::BuildOrThrow( + pOwnerObj, + pOwnerList, + &m_userThreads, + IID_ICorDebugThreadEnum, + pHolder); +} + +// Public implementation of ICorDebugProcess::EnumerateThreads +HRESULT CordbProcess::EnumerateThreads(ICorDebugThreadEnum **ppThreads) +{ + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this); + { + if (m_detached) + { + // #Detach_Check: + // + // FUTURE: Consider adding this IF block to the PUBLIC_API macros so that + // typical public APIs fail quickly if we're trying to do a detach. For + // now, I'm hand-adding this check only to the few problematic APIs that get + // called while queuing the fake attach events. In these cases, it is not + // enough to check if CordbProcess::IsNeutered(), as the detaching thread + // may have begun the detaching and neutering process, but not be + // finished--in which case m_detached is true, but + // CordbProcess::IsNeutered() is still false. + ThrowHR(CORDBG_E_PROCESS_DETACHED); + } + + ValidateOrThrow(ppThreads); + + RSInitHolder<CordbHashTableEnum> pEnum; + InternalEnumerateThreads(pEnum.GetAddr()); + + pEnum.TransferOwnershipExternal(ppThreads); + } + PUBLIC_API_END(hr); + return hr; +} + +// Internal implementation of EnumerateThreads +VOID CordbProcess::InternalEnumerateThreads(RSInitHolder<CordbHashTableEnum> *ppThreads) +{ + INTERNAL_API_ENTRY(this); + // Needs to prepopulate + PrepopulateThreadsOrThrow(); + BuildThreadEnum(this, this->GetContinueNeuterList(), ppThreads); +} + +// Implementation of ICorDebugProcess::GetThread +HRESULT CordbProcess::GetThread(DWORD dwThreadId, ICorDebugThread **ppThread) +{ + PUBLIC_API_ENTRY(this); + VALIDATE_POINTER_TO_OBJECT(ppThread, ICorDebugThread **); + + // No good pre-existing ATT_* contract for this. + // Because for legacy, we have to allow this on the win32 event thread. + *ppThread = NULL; + + HRESULT hr = S_OK; + EX_TRY + { + RSLockHolder lockHolder(GetProcessLock()); + if (m_detached) + { + // See code:CordbProcess::EnumerateThreads#Detach_Check + ThrowHR(CORDBG_E_PROCESS_DETACHED); + } + CordbThread * pThread = TryLookupOrCreateThreadByVolatileOSId(dwThreadId); + if (pThread == NULL) + { + // This is a common case because we may be looking up an unmanaged thread. + hr = E_INVALIDARG; + } + else + { + *ppThread = static_cast<ICorDebugThread*> (pThread); + pThread->ExternalAddRef(); + } + } + EX_CATCH_HRESULT(hr); + + LOG((LF_CORDB, LL_INFO10000, "CP::GT returns id=0x%x hr=0x%x ppThread=0x%p", + dwThreadId, hr, *ppThread)); + return hr; +} + +HRESULT CordbProcess::ThreadForFiberCookie(DWORD fiberCookie, + ICorDebugThread **ppThread) +{ + return E_NOTIMPL; +} + +HRESULT CordbProcess::GetHelperThreadID(DWORD *pThreadID) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + + _ASSERTE(m_pShim != NULL); + if (pThreadID == NULL) + { + return (E_INVALIDARG); + } + + HRESULT hr = S_OK; + // Return the ID of the current helper thread. There may be no thread in the process, or there may be a true helper + // thread. + if ((m_helperThreadId != 0) && !m_helperThreadDead) + { + *pThreadID = m_helperThreadId; + } + else if ((GetDCB() != NULL) && (GetDCB()->m_helperThreadId != 0)) + { + EX_TRY + { + // be sure we have the latest information + UpdateRightSideDCB(); + *pThreadID = GetDCB()->m_helperThreadId; + } + EX_CATCH_HRESULT(hr); + + } + else + { + *pThreadID = 0; + } + + return hr; +} + +//--------------------------------------------------------------------------------------- +// +// Sends IPC event to set all the managed threads, except for the one given, to the given state +// +// Arguments: +// state - The state to set the threads to. +// pExceptThread - The thread to not set. This is usually the thread that is currently +// sending an IPC event to the RS, and should be excluded. +// +// Return Value: +// Typical HRESULT symantics, nothing abnormal. +// +HRESULT CordbProcess::SetAllThreadsDebugState(CorDebugThreadState state, + ICorDebugThread * pExceptThread) +{ + PUBLIC_REENTRANT_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + VALIDATE_POINTER_TO_OBJECT_OR_NULL(pExceptThread, ICorDebugThread *); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + + if (GetShim() == NULL) + { + return E_NOTIMPL; + } + CordbThread * pCordbExceptThread = static_cast<CordbThread *> (pExceptThread); + + LOG((LF_CORDB, LL_INFO1000, "CP::SATDS: except thread=0x%08x 0x%x\n", + pExceptThread, + (pCordbExceptThread != NULL) ? pCordbExceptThread->m_id : 0)); + + // Send one event to the Left Side to twiddle each thread's state. + DebuggerIPCEvent event; + + InitIPCEvent(&event, DB_IPCE_SET_ALL_DEBUG_STATE, true, VMPTR_AppDomain::NullPtr()); + + event.SetAllDebugState.vmThreadToken = ((pCordbExceptThread != NULL) ? + pCordbExceptThread->m_vmThreadToken : VMPTR_Thread::NullPtr()); + + event.SetAllDebugState.debugState = state; + + HRESULT hr = SendIPCEvent(&event, sizeof(DebuggerIPCEvent)); + + hr = WORST_HR(hr, event.hr); + + // If that worked, then loop over all the threads on this side and set their states. + if (SUCCEEDED(hr)) + { + RSLockHolder lockHolder(GetProcessLock()); + HASHFIND hashFind; + CordbThread * pThread; + + // We don't need to prepopulate here (to collect LS state) because we're just updating RS state. + for (pThread = m_userThreads.FindFirst(&hashFind); + pThread != NULL; + pThread = m_userThreads.FindNext(&hashFind)) + { + if (pThread != pCordbExceptThread) + { + pThread->m_debugState = state; + } + } + } + + return hr; +} + + +HRESULT CordbProcess::EnumerateObjects(ICorDebugObjectEnum **ppObjects) +{ + /* !!! */ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + VALIDATE_POINTER_TO_OBJECT(ppObjects, ICorDebugObjectEnum **); + + return E_NOTIMPL; +} + +//--------------------------------------------------------------------------------------- +// +// Determines if the target address is a "CLR transition stub". +// +// Arguments: +// address - The address of an instruction to check in the target address space. +// pfTransitionStub - Space to store the result, TRUE if the address belongs to a +// transition stub, FALSE if not. Only valid if this method returns a success code. +// +// Return Value: +// Typical HRESULT symantics, nothing abnormal. +// +//--------------------------------------------------------------------------------------- +HRESULT CordbProcess::IsTransitionStub(CORDB_ADDRESS address, BOOL *pfTransitionStub) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + VALIDATE_POINTER_TO_OBJECT(pfTransitionStub, BOOL *); + + // Default to FALSE + *pfTransitionStub = FALSE; + + if (this->m_helperThreadDead) + { + return S_OK; + } + + // If we're not initialized, then it can't be a stub... + if (!m_initialized) + { + return S_OK; + } + + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + + HRESULT hr = S_OK; + EX_TRY + { + DebuggerIPCEvent eventData; + + InitIPCEvent(&eventData, DB_IPCE_IS_TRANSITION_STUB, true, VMPTR_AppDomain::NullPtr()); + + eventData.IsTransitionStub.address = CORDB_ADDRESS_TO_PTR(address); + + hr = SendIPCEvent(&eventData, sizeof(eventData)); + hr = WORST_HR(hr, eventData.hr); + IfFailThrow(hr); + + _ASSERTE(eventData.type == DB_IPCE_IS_TRANSITION_STUB_RESULT); + + *pfTransitionStub = eventData.IsTransitionStubResult.isStub; + LOG((LF_CORDB, LL_INFO1000, "CP::ITS: addr=0x%p result=%d\n", address, *pfTransitionStub)); + // @todo - beware that IsTransitionStub has a very important sideeffect - it synchronizes the runtime! + // This for example covers an OS bug where SetThreadContext may silently fail if we're not synchronized. + // (See IMDArocess::SetThreadContext for details on that bug). + // If we ever stop using IPC events here and only use DAC; we need to be aware of that. + + // Check against DAC primitives + { + BOOL fIsStub2 = GetDAC()->IsTransitionStub(address); + (void)fIsStub2; //prevent "unused variable" error from GCC + CONSISTENCY_CHECK_MSGF(*pfTransitionStub == fIsStub2, ("IsStub2 failed, DAC2:%d, IPC:%d, addr:0x%p", (int) fIsStub2, (int) *pfTransitionStub, CORDB_ADDRESS_TO_PTR(address))); + + } + } + EX_CATCH_HRESULT(hr); + if(FAILED(hr)) + { + LOG((LF_CORDB, LL_INFO1000, "CP::ITS: FAILED hr=0x%x\n", hr)); + } + return hr; +} + + +HRESULT CordbProcess::SetStopState(DWORD threadID, CorDebugThreadState state) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + return E_NOTIMPL; +} + +HRESULT CordbProcess::IsOSSuspended(DWORD threadID, BOOL *pbSuspended) +{ + PUBLIC_API_ENTRY(this); + // Gotta have a place for the result! + if (!pbSuspended) + return E_INVALIDARG; + + FAIL_IF_NEUTERED(this); + +#ifdef FEATURE_INTEROP_DEBUGGING + RSLockHolder lockHolder(GetProcessLock()); + + // Have we seen this thread? + CordbUnmanagedThread *ut = GetUnmanagedThread(threadID); + + // If we have, and if we've suspended it, then say so. + if (ut && ut->IsSuspended()) + { + *pbSuspended = TRUE; + } + else + { + *pbSuspended = FALSE; + } +#else + // Not interop-debugging, we never OS suspend. + *pbSuspended = FALSE; +#endif + return S_OK; +} + +// +// This routine reads a thread context from the process being debugged, taking into account the fact that the context +// record may be a different size than the one we compiled with. On systems < NT5, then OS doesn't usually allocate +// space for the extended registers. However, the CONTEXT struct that we compile with does have this space. +// +HRESULT CordbProcess::SafeReadThreadContext(LSPTR_CONTEXT pContext, DT_CONTEXT * pCtx) +{ + HRESULT hr = S_OK; + + INTERNAL_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + + EX_TRY + { + + void *pRemoteContext = pContext.UnsafeGet(); + TargetBuffer tbFull(pRemoteContext, sizeof(DT_CONTEXT)); + + // The context may have 2 parts: + // 1. Base register, which are always present. + // 2. Optional extended registers, which are only present if CONTEXT_EXTENDED_REGISTERS is set + // in the flags. + + // At a minimum we have room for a whole context up to the extended registers. + #if defined(DT_CONTEXT_EXTENDED_REGISTERS) + ULONG32 minContextSize = offsetof(DT_CONTEXT, ExtendedRegisters); + #else + ULONG32 minContextSize = sizeof(DT_CONTEXT); + #endif + + // Read the minimum part. + TargetBuffer tbMin = tbFull.SubBuffer(0, minContextSize); + SafeReadBuffer(tbMin, (BYTE*) pCtx); + + #if defined(DT_CONTEXT_EXTENDED_REGISTERS) + void *pCurExtReg = (void*)((UINT_PTR)pCtx + minContextSize); + TargetBuffer tbExtended = tbFull.SubBuffer(minContextSize); + + // Now, read the extended registers if the context contains them. If the context does not have extended registers, + // just set them to zero. + if (SUCCEEDED(hr) && (pCtx->ContextFlags & CONTEXT_EXTENDED_REGISTERS) == CONTEXT_EXTENDED_REGISTERS) + { + SafeReadBuffer(tbExtended, (BYTE*) pCurExtReg); + } + else + { + memset(pCurExtReg, 0, tbExtended.cbSize); + } + #endif + + } + EX_CATCH_HRESULT(hr); + return hr; +} + +// +// This routine writes a thread context to the process being debugged, taking into account the fact that the context +// record may be a different size than the one we compiled with. On systems < NT5, then OS doesn't usually allocate +// space for the extended registers. However, the CONTEXT struct that we compile with does have this space. +// +HRESULT CordbProcess::SafeWriteThreadContext(LSPTR_CONTEXT pContext, const DT_CONTEXT * pCtx) +{ + INTERNAL_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + + HRESULT hr = S_OK; + DWORD sizeToWrite = sizeof(DT_CONTEXT); + + BYTE * pRemoteContext = (BYTE*) pContext.UnsafeGet(); + BYTE * pCtxSource = (BYTE*) pCtx; + + +#if defined(DT_CONTEXT_EXTENDED_REGISTERS) + // If our context has extended registers, then write the whole thing. Otherwise, just write the minimum part. + if ((pCtx->ContextFlags & DT_CONTEXT_EXTENDED_REGISTERS) != DT_CONTEXT_EXTENDED_REGISTERS) + { + sizeToWrite = offsetof(DT_CONTEXT, ExtendedRegisters); + } +#endif + +// 64 bit windows puts space for the first 6 stack parameters in the CONTEXT structure so that +// kernel to usermode transitions don't have to allocate a CONTEXT and do a seperate sub rsp +// to allocate stack spill space for the arguments. This means that writing to P1Home - P6Home +// will overwrite the arguments of some function higher on the stack, very bad. Conceptually you +// can think of these members as not being part of the context, ie they don't represent something +// which gets saved or restored on context switches. They are just space we shouldn't overwrite. +// See issue 630276 for more details. +#if defined DBG_TARGET_AMD64 + pRemoteContext += offsetof(CONTEXT, ContextFlags); // immediately follows the 6 parameters P1-P6 + pCtxSource += offsetof(CONTEXT, ContextFlags); + sizeToWrite -= offsetof(CONTEXT, ContextFlags); +#endif + + EX_TRY + { + // Write the context. + TargetBuffer tb(pRemoteContext, sizeToWrite); + SafeWriteBuffer(tb, (const BYTE*) pCtxSource); + } + EX_CATCH_HRESULT(hr); + + return hr; +} + + +HRESULT CordbProcess::GetThreadContext(DWORD threadID, ULONG32 contextSize, BYTE context[]) +{ + PUBLIC_REENTRANT_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + FAIL_IF_MANAGED_ONLY(this); + + DT_CONTEXT * pContext; + LOG((LF_CORDB, LL_INFO10000, "CP::GTC: thread=0x%x\n", threadID)); + + RSLockHolder lockHolder(GetProcessLock()); + + if (contextSize != sizeof(DT_CONTEXT)) + { + LOG((LF_CORDB, LL_INFO10000, "CP::GTC: thread=0x%x, context size is invalid.\n", threadID)); + return E_INVALIDARG; + } + + pContext = reinterpret_cast<DT_CONTEXT *>(context); + + VALIDATE_POINTER_TO_OBJECT_ARRAY(context, BYTE, contextSize, true, true); + +#if !defined(FEATURE_INTEROP_DEBUGGING) + return E_NOTIMPL; +#else + // Find the unmanaged thread + CordbUnmanagedThread *ut = GetUnmanagedThread(threadID); + + if (ut == NULL) + { + LOG((LF_CORDB, LL_INFO10000, "CP::GTC: thread=0x%x, thread id is invalid.\n", threadID)); + + return E_INVALIDARG; + } + + return ut->GetThreadContext((DT_CONTEXT*)context); +#endif // FEATURE_INTEROP_DEBUGGING +} + +// Public implementation of ICorDebugProcess::SetThreadContext. +// @dbgtodo interop-debugging: this should go away in V3. Use the data-target instead. This is +// interop-debugging aware (and cooperates with hijacks) +HRESULT CordbProcess::SetThreadContext(DWORD threadID, ULONG32 contextSize, BYTE context[]) +{ + PUBLIC_REENTRANT_API_ENTRY(this); + FAIL_IF_MANAGED_ONLY(this); + +#if !defined(FEATURE_INTEROP_DEBUGGING) + return E_NOTIMPL; +#else + HRESULT hr = S_OK; + + RSLockHolder lockHolder(GetProcessLock()); + + CordbUnmanagedThread *ut = NULL; + + if (contextSize != sizeof(DT_CONTEXT)) + { + LOG((LF_CORDB, LL_INFO10000, "CP::STC: thread=0x%x, context size is invalid.\n", threadID)); + hr = E_INVALIDARG; + goto Label_Done; + } + + // @todo - could we look at the context flags and return E_INVALIDARG if they're bad? + FAIL_IF_NEUTERED(this); + VALIDATE_POINTER_TO_OBJECT_ARRAY(context, BYTE, contextSize, true, true); + + // Find the unmanaged thread + ut = GetUnmanagedThread(threadID); + + if (ut == NULL) + { + LOG((LF_CORDB, LL_INFO10000, "CP::STC: thread=0x%x, thread is invalid.\n", threadID)); + hr = E_INVALIDARG; + goto Label_Done; + } + + hr = ut->SetThreadContext((DT_CONTEXT*)context); + + + // Update the register set for the leaf-unmanaged chain so that it's consistent w/ the context. + // We may not necessarily be synchronized, and so these frames may be stale. Even so, no harm done. + if (SUCCEEDED(hr)) + { + // @dbgtodo stackwalk: this should all disappear with V3 stackwalker and getting rid of SetThreadContext. + EX_TRY + { + // Find the managed thread. Returns NULL if thread is not managed. + // If we don't have a thread prveiously cached, then there's no state to update. + CordbThread * pThread = TryLookupThreadByVolatileOSId(threadID); + + if (pThread != NULL) + { + // In V2, we used to update the CONTEXT of the leaf chain if the chain is an unmanaged chain. + // In Arrowhead, we just force a cleanup of the stackwalk cache. This is a more correct + // thing to do anyway, since the CONTEXT being set could be anything. + pThread->CleanupStack(); + } + } + EX_CATCH_HRESULT(hr); + } + +Label_Done: + return ErrWrapper(hr); + +#endif // FEATURE_INTEROP_DEBUGGING +} + + +// @dbgtodo ICDProcess - When we DACize this function, we should use code:DacReplacePatches +HRESULT CordbProcess::ReadMemory(CORDB_ADDRESS address, + DWORD size, + BYTE buffer[], + SIZE_T *read) +{ + PUBLIC_REENTRANT_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + + // A read of 0 bytes is okay. + if (size == 0) + return S_OK; + + VALIDATE_POINTER_TO_OBJECT_ARRAY(buffer, BYTE, size, true, true); + VALIDATE_POINTER_TO_OBJECT(buffer, SIZE_T *); + + if (address == NULL) + return E_INVALIDARG; + + // If no read parameter is supplied, we ignore it. This matches the semantics of kernel32!ReadProcessMemory. + SIZE_T dummyRead; + if (read == NULL) + { + read = &dummyRead; + } + *read = 0; + + HRESULT hr = S_OK; + + CORDBRequireProcessStateOK(this); + + // Grab the memory we want to read + // Note that this will return success on a partial read + ULONG32 cbRead; + hr = GetDataTarget()->ReadVirtual(address, buffer, size, &cbRead); + if (FAILED(hr)) + { + hr = CORDBG_E_READVIRTUAL_FAILURE; + goto LExit; + } + + // Read at least one byte + *read = (SIZE_T) cbRead; + + // There seem to be strange cases where ReadProcessMemory will return a seemingly negative number into *read, which + // is an unsigned value. So we check the sanity of *read by ensuring that its no bigger than the size we tried to + // read. + if ((*read > 0) && (*read <= size)) + { + LOG((LF_CORDB, LL_INFO100000, "CP::RM: read %d bytes from 0x%08x, first byte is 0x%x\n", + *read, (DWORD)address, buffer[0])); + + if (m_initialized) + { + RSLockHolder ch(&this->m_processMutex); + + // If m_pPatchTable is NULL, then it's been cleaned out b/c of a Continue for the left side. Get the table + // again. Only do this, of course, if the managed state of the process is initialized. + if (m_pPatchTable == NULL) + { + hr = RefreshPatchTable(address, *read, buffer); + } + else + { + // The previously fetched table is still good, so run through it & see if any patches are applicable + hr = AdjustBuffer(address, *read, buffer, NULL, AB_READ); + } + } + } + +LExit: + if (FAILED(hr)) + { + RSLockHolder ch(&this->m_processMutex); + ClearPatchTable(); + } + else if (*read < size) + { + // Unlike the DT api, our API is supposed to return an error on partial read + hr = HRESULT_FROM_WIN32(ERROR_PARTIAL_COPY); + } + return hr; +} + +// Update patches & buffer to make the left-side's usage of patches transparent +// to our client. Behavior depends on AB_MODE: +// AB_READ: +// - use the RS patch table structure to replace patch opcodes in buffer. +// AB_WRITE: +// - update the RS patch table structure w/ new replace-opcode values +// if we've written over them. And put the int3 back in for write-memory. +// +// Note: If we're writing memory over top of a patch, then it must be JITted or stub code. +// Writing over JITed or Stub code can be dangerous since the CLR may not expect it +// (eg. JIT data structures about the code layout may be incorrect), but in certain +// narrow cases it may be safe (eg. replacing a constant). VS says they wouldn't expect +// this to work, but we'll keep the support in for legacy reasons. +// +// address, size - describe buffer in LS memory +// buffer - local copy of buffer that will be read/written from/to LS. +// bufferCopy - for writeprocessmemory, copy of original buffer (w/o injected patches) +// pbUpdatePatchTable - flag if patchtable got dirty and needs to be updated. +HRESULT CordbProcess::AdjustBuffer( CORDB_ADDRESS address, + SIZE_T size, + BYTE buffer[], + BYTE **bufferCopy, + AB_MODE mode, + BOOL *pbUpdatePatchTable) +{ + INTERNAL_API_ENTRY(this); + + _ASSERTE(m_initialized); + _ASSERTE(this->ThreadHoldsProcessLock()); + + if ( address == NULL + || size == NULL + || buffer == NULL + || (mode != AB_READ && mode != AB_WRITE) ) + return E_INVALIDARG; + + if (pbUpdatePatchTable != NULL ) + *pbUpdatePatchTable = FALSE; + + // If we don't have a patch table loaded, then return S_OK since there are no patches to adjust + if (m_pPatchTable == NULL) + return S_OK; + + //is the requested memory completely out-of-range? + if ((m_minPatchAddr > (address + (size - 1))) || + (m_maxPatchAddr < address)) + { + return S_OK; + } + + // Without runtime offsets, we can't adjust - this should only ever happen on dumps, where there's + // no W32ET to get the offsets, and so they stay zeroed + if (!m_runtimeOffsetsInitialized) + return S_OK; + + LOG((LF_CORDB,LL_INFO10000, "CordbProcess::AdjustBuffer at addr 0x%p\n", address)); + + if (mode == AB_WRITE) + { + // We don't want to mess up the original copy of the buffer, so + // for right now, just copy it wholesale. + (*bufferCopy) = new (nothrow) BYTE[size]; + if (NULL == (*bufferCopy)) + return E_OUTOFMEMORY; + + memmove((*bufferCopy), buffer, size); + } + + ULONG iNextFree = m_iFirstPatch; + while( iNextFree != DPT_TERMINATING_INDEX ) + { + BYTE *DebuggerControllerPatch = m_pPatchTable + m_runtimeOffsets.m_cbPatch*iNextFree; + PRD_TYPE opcode = *(PRD_TYPE *)(DebuggerControllerPatch + m_runtimeOffsets.m_offOpcode); + CORDB_ADDRESS patchAddress = PTR_TO_CORDB_ADDRESS(*(BYTE**)(DebuggerControllerPatch + m_runtimeOffsets.m_offAddr)); + + if (IsPatchInRequestedRange(address, size, patchAddress)) + { + if (mode == AB_READ) + { + CORDbgSetInstructionEx(buffer, address, patchAddress, opcode, size); + } + else if (mode == AB_WRITE) + { + _ASSERTE( pbUpdatePatchTable != NULL ); + _ASSERTE( bufferCopy != NULL ); + + //There can be multiple patches at the same address: we don't want 2nd+ patches to get the + // break opcode, so we read from the unmodified copy. + m_rgUncommitedOpcode[iNextFree] = + CORDbgGetInstructionEx(*bufferCopy, address, patchAddress, opcode, size); + + //put the breakpoint into the memory itself + CORDbgInsertBreakpointEx(buffer, address, patchAddress, opcode, size); + + *pbUpdatePatchTable = TRUE; + } + else + _ASSERTE( !"CordbProcess::AdjustBuffergiven non(Read|Write) mode!" ); + } + + iNextFree = m_rgNextPatch[iNextFree]; + } + + // If we created a copy of the buffer but didn't modify it, then free it now. + if( ( mode == AB_WRITE ) && ( !*pbUpdatePatchTable ) ) + { + delete [] *bufferCopy; + *bufferCopy = NULL; + } + + return S_OK; +} + + +void CordbProcess::CommitBufferAdjustments( CORDB_ADDRESS start, + CORDB_ADDRESS end ) +{ + INTERNAL_API_ENTRY(this); + + _ASSERTE(m_initialized); + _ASSERTE(this->ThreadHoldsProcessLock()); + _ASSERTE(m_runtimeOffsetsInitialized); + + ULONG iPatch = m_iFirstPatch; + while( iPatch != DPT_TERMINATING_INDEX ) + { + BYTE *DebuggerControllerPatch = m_pPatchTable + + m_runtimeOffsets.m_cbPatch*iPatch; + + BYTE *patchAddress = *(BYTE**)(DebuggerControllerPatch + m_runtimeOffsets.m_offAddr); + + if (IsPatchInRequestedRange(start, (SIZE_T)(end - start), PTR_TO_CORDB_ADDRESS(patchAddress)) && + !PRDIsBreakInst(&(m_rgUncommitedOpcode[iPatch]))) + { + //copy this back to the copy of the patch table + *(PRD_TYPE *)(DebuggerControllerPatch + m_runtimeOffsets.m_offOpcode) = + m_rgUncommitedOpcode[iPatch]; + } + + iPatch = m_rgNextPatch[iPatch]; + } +} + +void CordbProcess::ClearBufferAdjustments( ) +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(this->ThreadHoldsProcessLock()); + + ULONG iPatch = m_iFirstPatch; + while( iPatch != DPT_TERMINATING_INDEX ) + { + InitializePRDToBreakInst(&(m_rgUncommitedOpcode[iPatch])); + iPatch = m_rgNextPatch[iPatch]; + } +} + +void CordbProcess::ClearPatchTable(void ) +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(this->ThreadHoldsProcessLock()); + + if (m_pPatchTable != NULL ) + { + delete [] m_pPatchTable; + m_pPatchTable = NULL; + + delete [] m_rgNextPatch; + m_rgNextPatch = NULL; + + delete [] m_rgUncommitedOpcode; + m_rgUncommitedOpcode = NULL; + + m_iFirstPatch = DPT_TERMINATING_INDEX; + m_minPatchAddr = MAX_ADDRESS; + m_maxPatchAddr = MIN_ADDRESS; + m_rgData = NULL; + m_cPatch = 0; + } +} + +HRESULT CordbProcess::RefreshPatchTable(CORDB_ADDRESS address, SIZE_T size, BYTE buffer[]) +{ + CONTRACTL + { + NOTHROW; + } + CONTRACTL_END; + + INTERNAL_API_ENTRY(this); + _ASSERTE(m_initialized); + _ASSERTE(this->ThreadHoldsProcessLock()); + + HRESULT hr = S_OK; + BYTE *rgb = NULL; + + // All of m_runtimeOffsets will be zeroed out if there's been no call to code:CordbProcess::GetRuntimeOffsets. + // Thus for things to work, we'd have to have a live target that went and got the real values. + // For dumps, things are still all zeroed out because we don't have any events sent to the W32ET, don't + // have a live process to investigate, etc. + if (!m_runtimeOffsetsInitialized) + return S_OK; + + _ASSERTE( m_runtimeOffsets.m_cbOpcode == sizeof(PRD_TYPE) ); + + CORDBRequireProcessStateOK(this); + + if (m_pPatchTable == NULL ) + { + // First, check to be sure the patch table is valid on the Left Side. If its not, then we won't read it. + BOOL fPatchTableValid = FALSE; + + hr = SafeReadStruct(PTR_TO_CORDB_ADDRESS(m_runtimeOffsets.m_pPatchTableValid), &fPatchTableValid); + if (FAILED(hr) || !fPatchTableValid) + { + LOG((LF_CORDB, LL_INFO10000, "Wont refresh patch table because its not valid now.\n")); + return S_OK; + } + + SIZE_T offStart = 0; + SIZE_T offEnd = 0; + UINT cbTableSlice = 0; + + // Grab the patch table info + offStart = min(m_runtimeOffsets.m_offRgData, m_runtimeOffsets.m_offCData); + offEnd = max(m_runtimeOffsets.m_offRgData, m_runtimeOffsets.m_offCData) + sizeof(SIZE_T); + cbTableSlice = (UINT)(offEnd - offStart); + + if (cbTableSlice == 0) + { + LOG((LF_CORDB, LL_INFO10000, "Wont refresh patch table because its not valid now.\n")); + return S_OK; + } + + EX_TRY + { + rgb = new BYTE[cbTableSlice]; // throws + + TargetBuffer tbSlice((BYTE*)m_runtimeOffsets.m_pPatches + offStart, cbTableSlice); + this->SafeReadBuffer(tbSlice, rgb); // Throws; + + // Note that rgData is a pointer in the left side address space + m_rgData = *(BYTE**)(rgb + m_runtimeOffsets.m_offRgData - offStart); + m_cPatch = *(ULONG*)(rgb + m_runtimeOffsets.m_offCData - offStart); + + // Grab the patch table + UINT cbPatchTable = (UINT)(m_cPatch * m_runtimeOffsets.m_cbPatch); + + if (cbPatchTable == 0) + { + LOG((LF_CORDB, LL_INFO10000, "Wont refresh patch table because its not valid now.\n")); + _ASSERTE(hr == S_OK); + goto LExit; // can't return since we're in a Try/Catch + } + + // Throwing news + m_pPatchTable = new BYTE[ cbPatchTable ]; + m_rgNextPatch = new ULONG[m_cPatch]; + m_rgUncommitedOpcode = new PRD_TYPE[m_cPatch]; + + TargetBuffer tb(m_rgData, cbPatchTable); + this->SafeReadBuffer(tb, m_pPatchTable); // Throws + + //As we go through the patch table we do a number of things: + // + // 1. collect min,max address seen for quick fail check + // + // 2. Link all valid entries into a linked list, the first entry of which is m_iFirstPatch + // + // 3. Initialize m_rgUncommitedOpcode, so that we can undo local patch table changes if WriteMemory can't write + // atomically. + // + // 4. If the patch is in the memory we grabbed, unapply it. + + ULONG iDebuggerControllerPatchPrev = DPT_TERMINATING_INDEX; + + m_minPatchAddr = MAX_ADDRESS; + m_maxPatchAddr = MIN_ADDRESS; + m_iFirstPatch = DPT_TERMINATING_INDEX; + + for (ULONG iPatch = 0; iPatch < m_cPatch;iPatch++) + { + // <REVISIT_TODO>@todo port: we're making assumptions about the size of opcodes,address pointers, etc</REVISIT_TODO> + BYTE *DebuggerControllerPatch = m_pPatchTable + m_runtimeOffsets.m_cbPatch * iPatch; + PRD_TYPE opcode = *(PRD_TYPE*)(DebuggerControllerPatch + m_runtimeOffsets.m_offOpcode); + CORDB_ADDRESS patchAddress = PTR_TO_CORDB_ADDRESS(*(BYTE**)(DebuggerControllerPatch + m_runtimeOffsets.m_offAddr)); + + // A non-zero opcode indicates to us that this patch is valid. + if (!PRDIsEmpty(opcode)) + { + _ASSERTE( patchAddress != 0 ); + + // (1), above + // Note that GetPatchEndAddr() returns the address immediately AFTER the patch, + // so we have to subtract 1 from it below. + if (m_minPatchAddr > patchAddress ) + m_minPatchAddr = patchAddress; + if (m_maxPatchAddr < patchAddress ) + m_maxPatchAddr = GetPatchEndAddr(patchAddress) - 1; + + // (2), above + if ( m_iFirstPatch == DPT_TERMINATING_INDEX) + { + m_iFirstPatch = iPatch; + _ASSERTE( iPatch != DPT_TERMINATING_INDEX); + } + + if (iDebuggerControllerPatchPrev != DPT_TERMINATING_INDEX) + { + m_rgNextPatch[iDebuggerControllerPatchPrev] = iPatch; + } + + iDebuggerControllerPatchPrev = iPatch; + + // (3), above + InitializePRDToBreakInst(&(m_rgUncommitedOpcode[iPatch])); + + // (4), above + if (IsPatchInRequestedRange(address, size, patchAddress)) + { + _ASSERTE( buffer != NULL ); + _ASSERTE( size != NULL ); + + + //unapply the patch here. + CORDbgSetInstructionEx(buffer, address, patchAddress, opcode, size); + } + + } + } + + if (iDebuggerControllerPatchPrev != DPT_TERMINATING_INDEX) + { + m_rgNextPatch[iDebuggerControllerPatchPrev] = DPT_TERMINATING_INDEX; + } + } +LExit: + ; + EX_CATCH_HRESULT(hr); + } + + + if (rgb != NULL ) + { + delete [] rgb; + } + + if (FAILED( hr ) ) + { + ClearPatchTable(); + } + + return hr; +} + +//--------------------------------------------------------------------------------------- +// +// Given an address, see if there is a patch in the patch table that matches it and return +// if its an unmanaged patch or not. +// +// Arguments: +// address - The address of an instruction to check in the target address space. +// pfPatchFound - Space to store the result, TRUE if the address belongs to a +// patch, FALSE if not. Only valid if this method returns a success code. +// pfPatchIsUnmanaged - Space to store the result, TRUE if the address is a patch +// and the patch is unmanaged, FALSE if not. Only valid if this method returns a +// success code. +// +// Return Value: +// Typical HRESULT symantics, nothing abnormal. +// +// Note: this method is pretty in-efficient. It refreshes the patch table, then scans it. +// Refreshing the patch table involves a scan, too, so this method could be folded +// with that. +// +//--------------------------------------------------------------------------------------- +HRESULT CordbProcess::FindPatchByAddress(CORDB_ADDRESS address, bool *pfPatchFound, bool *pfPatchIsUnmanaged) +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(ThreadHoldsProcessLock()); + _ASSERTE((pfPatchFound != NULL) && (pfPatchIsUnmanaged != NULL)); + _ASSERTE(m_runtimeOffsetsInitialized); + FAIL_IF_NEUTERED(this); + + *pfPatchFound = false; + *pfPatchIsUnmanaged = false; + + // First things first. If the process isn't initialized, then there can be no patch table, so we know the breakpoint + // doesn't belong to the Runtime. + if (!m_initialized) + { + return S_OK; + } + + // This method is called from the main loop of the win32 event thread in response to a first chance breakpoint event + // that we know is not a flare. The process has been runnning, and it may have invalidated the patch table, so we'll + // flush it here before refreshing it to make sure we've got the right thing. + // + // Note: we really should have the Left Side mark the patch table dirty to help optimize this. + ClearPatchTable(); + + // Refresh the patch table. + HRESULT hr = RefreshPatchTable(); + + if (FAILED(hr)) + { + LOG((LF_CORDB, LL_INFO1000, "CP::FPBA: failed to refresh the patch table\n")); + return hr; + } + + // If there is no patch table yet, then we know there is no patch at the given address, so return S_OK with + // *patchFound = false. + if (m_pPatchTable == NULL) + { + LOG((LF_CORDB, LL_INFO1000, "CP::FPBA: no patch table\n")); + return S_OK; + } + + // Scan the patch table for a matching patch. + for (ULONG iNextPatch = m_iFirstPatch; iNextPatch != DPT_TERMINATING_INDEX; iNextPatch = m_rgNextPatch[iNextPatch]) + { + BYTE *patch = m_pPatchTable + (m_runtimeOffsets.m_cbPatch * iNextPatch); + BYTE *patchAddress = *(BYTE**)(patch + m_runtimeOffsets.m_offAddr); + DWORD traceType = *(DWORD*)(patch + m_runtimeOffsets.m_offTraceType); + + if (address == PTR_TO_CORDB_ADDRESS(patchAddress)) + { + *pfPatchFound = true; + + if (traceType == m_runtimeOffsets.m_traceTypeUnmanaged) + { + *pfPatchIsUnmanaged = true; + +#if defined(_DEBUG) + HRESULT hrDac = S_OK; + EX_TRY + { + // We should be able to double check w/ DAC that this really is outside of the runtime. + IDacDbiInterface::AddressType addrType = GetDAC()->GetAddressType(address); + CONSISTENCY_CHECK_MSGF(addrType == IDacDbiInterface::kAddressUnrecognized, ("Bad address type = %d", addrType)); + } + EX_CATCH_HRESULT(hrDac); + CONSISTENCY_CHECK_MSGF(SUCCEEDED(hrDac), ("DAC::GetAddressType failed, hr=0x%08x", hrDac)); +#endif + } + + break; + } + } + + // If we didn't find a patch, its actually still possible that this breakpoint exception belongs to us. There are + // races with very large numbers of threads entering the Runtime through the same managed function. We will have + // multiple threads adding and removing ref counts to an int 3 in the code stream. Sometimes, this count will go to + // zero and the int 3 will be removed, then it will come back up and the int 3 will be replaced. The in-process + // logic takes pains to ensure that such cases are handled properly, therefore we need to perform the same check + // here to make the correct decision. Basically, the check is to see if there is indeed an int 3 at the exception + // address. If there is _not_ an int 3 there, then we've hit this race. We will lie and say a managed patch was + // found to cover this case. This is tracking the logic in DebuggerController::ScanForTriggers, where we call + // IsPatched. + if (*pfPatchFound == false) + { + // Read one instruction from the faulting address... +#if defined(DBG_TARGET_ARM) || defined(DBG_TARGET_ARM64) + PRD_TYPE TrapCheck = 0; +#else + BYTE TrapCheck = 0; +#endif + + HRESULT hr2 = SafeReadStruct(address, &TrapCheck); + + if (SUCCEEDED(hr2) && (TrapCheck != CORDbg_BREAK_INSTRUCTION)) + { + LOG((LF_CORDB, LL_INFO1000, "CP::FPBA: patchFound=true based on odd missing int 3 case.\n")); + + *pfPatchFound = true; + } + } + + LOG((LF_CORDB, LL_INFO1000, "CP::FPBA: patchFound=%d, patchIsUnmanaged=%d\n", *pfPatchFound, *pfPatchIsUnmanaged)); + + return S_OK; +} + +HRESULT CordbProcess::WriteMemory(CORDB_ADDRESS address, DWORD size, + BYTE buffer[], SIZE_T *written) +{ + PUBLIC_REENTRANT_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + CORDBRequireProcessStateOK(this); + _ASSERTE(m_runtimeOffsetsInitialized); + + + if (size == 0 || address == NULL) + return E_INVALIDARG; + + VALIDATE_POINTER_TO_OBJECT_ARRAY(buffer, BYTE, size, true, true); + VALIDATE_POINTER_TO_OBJECT(written, SIZE_T *); + + +#if defined(_DEBUG) && defined(FEATURE_INTEROP_DEBUGGING) + // Shouldn't be using this to write int3. Use UM BP API instead. + // This is technically legal (what if the '0xcc' is data or something), so we can't fail in retail. + // But we can add this debug-only check to help VS migrate to the new API. + static ConfigDWORD configCheckInt3; + DWORD fCheckInt3 = configCheckInt3.val(CLRConfig::INTERNAL_DbgCheckInt3); + if (fCheckInt3) + { +#if defined(DBG_TARGET_X86) || defined(DBG_TARGET_AMD64) + if (size == 1 && buffer[0] == 0xCC) + { + CONSISTENCY_CHECK_MSGF(false, + ("You're using ICorDebugProcess::WriteMemory() to write an 'int3' (1 byte 0xCC) at address 0x%p.\n" + "If you're trying to set a breakpoint, you should be using ICorDebugProcess::SetUnmanagedBreakpoint() instead.\n" + "(This assert is only enabled under the COM+ knob DbgCheckInt3.)\n", + CORDB_ADDRESS_TO_PTR(address))); + } +#endif // DBG_TARGET_X86 || DBG_TARGET_AMD64 + + // check if we're replaced an opcode. + if (size == 1) + { + RSLockHolder ch(&this->m_processMutex); + + NativePatch * p = GetNativePatch(CORDB_ADDRESS_TO_PTR(address)); + if (p != NULL) + { + CONSISTENCY_CHECK_MSGF(false, + ("You're using ICorDebugProcess::WriteMemory() to write an 'opcode (0x%x)' at address 0x%p.\n" + "There's already a native patch at that address from ICorDebugProcess::SetUnmanagedBreakpoint().\n" + "If you're trying to remove the breakpoint, use ICDProcess::ClearUnmanagedBreakpoint() instead.\n" + "(This assert is only enabled under the COM+ knob DbgCheckInt3.)\n", + (DWORD) (buffer[0]), CORDB_ADDRESS_TO_PTR(address))); + } + } + } +#endif // _DEBUG && FEATURE_INTEROP_DEBUGGING + + + *written = 0; + + HRESULT hr = S_OK; + HRESULT hrSaved = hr; // this will hold the 'real' hresult in case of a + // partially completed operation + HRESULT hrPartialCopy = HRESULT_FROM_WIN32(ERROR_PARTIAL_COPY); + + BOOL bUpdateOriginalPatchTable = FALSE; + BYTE *bufferCopy = NULL; + + // Only update the patch table if the managed state of the process + // is initialized. + if (m_initialized) + { + RSLockHolder ch(&this->m_processMutex); + + if (m_pPatchTable == NULL ) + { + if (!SUCCEEDED( hr = RefreshPatchTable() ) ) + { + goto LExit; + } + } + + if ( !SUCCEEDED( hr = AdjustBuffer( address, + size, + buffer, + &bufferCopy, + AB_WRITE, + &bUpdateOriginalPatchTable))) + { + goto LExit; + } + } + + //conveniently enough, SafeWriteBuffer will throw if it can't complete the entire operation + EX_TRY + { + TargetBuffer tb(address, size); + SafeWriteBuffer(tb, buffer); // throws + *written = tb.cbSize; // DT's Write does everything or fails. + } + EX_CATCH_HRESULT(hr); + + if (FAILED(hr)) + { + if(hr != hrPartialCopy) + goto LExit; + else + hrSaved = hr; + } + + + LOG((LF_CORDB, LL_INFO100000, "CP::WM: wrote %d bytes at 0x%08x, first byte is 0x%x\n", + *written, (DWORD)address, buffer[0])); + + if (bUpdateOriginalPatchTable == TRUE ) + { + { + RSLockHolder ch(&this->m_processMutex); + + //don't tweak patch table for stuff that isn't written to LeftSide + CommitBufferAdjustments(address, address + *written); + } + + // The only way this should be able to fail is if + //someone else fiddles with the memory protections on the + //left side while it's frozen + EX_TRY + { + TargetBuffer tb(m_rgData, (ULONG) (m_cPatch*m_runtimeOffsets.m_cbPatch)); + SafeWriteBuffer(tb, m_pPatchTable); + } + EX_CATCH_HRESULT(hr); + SIMPLIFYING_ASSUMPTION_SUCCEEDED(hr); + } + + // Since we may have + // overwritten anything (objects, code, etc), we should mark + // everything as needing to be re-cached. + m_continueCounter++; + + LExit: + if (m_initialized) + { + RSLockHolder ch(&this->m_processMutex); + ClearBufferAdjustments( ); + } + + //we messed up our local copy, so get a clean copy the next time + //we need it + if (bUpdateOriginalPatchTable==TRUE) + { + if (bufferCopy != NULL) + { + memmove(buffer, bufferCopy, size); + delete bufferCopy; + } + } + + if (FAILED( hr )) + { + //we messed up our local copy, so get a clean copy the next time + //we need it + if (bUpdateOriginalPatchTable==TRUE) + { + RSLockHolder ch(&this->m_processMutex); + ClearPatchTable(); + } + } + else if( FAILED(hrSaved) ) + { + hr = hrSaved; + } + + return hr; +} + +HRESULT CordbProcess::ClearCurrentException(DWORD threadID) +{ +#ifndef FEATURE_INTEROP_DEBUGGING + return E_INVALIDARG; +#else + PUBLIC_API_ENTRY(this); + + RSLockHolder lockHolder(GetProcessLock()); + + // There's something wrong if you're calling this an there are no queued unmanaged events. + if ((m_unmanagedEventQueue == NULL) && (m_outOfBandEventQueue == NULL)) + return E_INVALIDARG; + + // Grab the unmanaged thread object. + CordbUnmanagedThread *pUThread = GetUnmanagedThread(threadID); + + if (pUThread == NULL) + return E_INVALIDARG; + + LOG((LF_CORDB, LL_INFO1000, "CP::CCE: tid=0x%x\n", threadID)); + + // We clear both the IB and OOB event. + if (pUThread->HasIBEvent() && !pUThread->IBEvent()->IsEventUserContinued()) + { + pUThread->IBEvent()->SetState(CUES_ExceptionCleared); + } + + if (pUThread->HasOOBEvent()) + { + // must decide exception status _before_ we continue the event. + _ASSERTE(!pUThread->OOBEvent()->IsEventContinuedUnhijacked()); + pUThread->OOBEvent()->SetState(CUES_ExceptionCleared); + } + + // If the thread is hijacked, then set the thread's debugger word to 0 to indicate to it that the + // exception has been cleared. + if (pUThread->IsGenericHijacked()) + { + HRESULT hr = pUThread->SetEEDebuggerWord(0); + _ASSERTE(SUCCEEDED(hr)); + } + + return S_OK; +#endif // FEATURE_INTEROP_DEBUGGING +} + +#ifdef FEATURE_INTEROP_DEBUGGING +CordbUnmanagedThread *CordbProcess::HandleUnmanagedCreateThread(DWORD dwThreadId, HANDLE hThread, void *lpThreadLocalBase) +{ + INTERNAL_API_ENTRY(this); + CordbUnmanagedThread *ut = new (nothrow) CordbUnmanagedThread(this, dwThreadId, hThread, lpThreadLocalBase); + + if (ut != NULL) + { + HRESULT hr = m_unmanagedThreads.AddBase(ut); // InternalAddRef, release on EXIT_THREAD events. + + if (!SUCCEEDED(hr)) + { + delete ut; + + LOG((LF_CORDB, LL_INFO10000, "Failed adding unmanaged thread to process!\n")); + CORDBSetUnrecoverableError(this, hr, 0); + } + } + else + { + LOG((LF_CORDB, LL_INFO10000, "New CordbThread failed!\n")); + CORDBSetUnrecoverableError(this, E_OUTOFMEMORY, 0); + } + + return ut; +} +#endif // FEATURE_INTEROP_DEBUGGING + + +//----------------------------------------------------------------------------- +// Initializes the DAC +// Arguments: none--initializes the DAC for this CordbProcess instance +// Note: Throws on error +//----------------------------------------------------------------------------- +void CordbProcess::InitDac() +{ + // Go-Go DAC power!! + HRESULT hr = S_OK; + EX_TRY + { + InitializeDac(); + } + EX_CATCH_HRESULT(hr); + + // We Need DAC to debug for both Managed & Interop. + if (FAILED(hr)) + { + // We assert here b/c we're trying to be friendly. Most likely, the cause is either: + // - a bad installation + // - a CLR dev built mscorwks but didn't build DAC. + SIMPLIFYING_ASSUMPTION_MSGF(false, ("Failed to load DAC while for debugging. hr=0x%08x", hr)); + ThrowHR(hr); + } +} //CordbProcess::InitDac + +// Update the entire RS copy of the debugger control block by reading the LS copy. The RS copy is treated as +// a throw-away temporary buffer, rather than a true cache. That is, we make no assumptions about the +// validity of the information over time. Thus, before using any of the values, we need to update it. We +// update everything for simplicity; any perf hit we take by doing this instead of updating the individual +// fields we want at any given point isn't significant, particularly if we are updating multiple fields. + +// Arguments: +// none, but reads process memory from the LS debugger control block +// Return Value: none (copies from LS DCB to RS buffer GetDCB()) +// Note: throws if SafeReadBuffer fails +void CordbProcess::UpdateRightSideDCB() +{ + IfFailThrow(m_pEventChannel->UpdateRightSideDCB()); +} // CordbProcess::UpdateRightSideDCB + +// Update a single field with a value stored in the RS copy of the DCB. We can't update the entire LS DCB +// because in some cases, the LS and RS are simultaneously initializing the DCB. If we initialize a field on +// the RS and write back the whole thing, we may overwrite something the LS has initialized in the interim. + +// Arguments: +// input: rsFieldAddr - the address of the field in the RS copy of the DCB that we want to write back to +// the LS DCB. We use this to compute the offset of the field from the beginning of the +// DCB and then add this offset to the starting address of the LS DCB to get the LS +// address of the field we are updating +// size - the size of the field we're updating. +// Return value: none +// Note: throws if SafeWriteBuffer fails +void CordbProcess::UpdateLeftSideDCBField(void * rsFieldAddr, SIZE_T size) +{ + IfFailThrow(m_pEventChannel->UpdateLeftSideDCBField(rsFieldAddr, size)); +} // CordbProcess::UpdateRightSideDCB + + +//----------------------------------------------------------------------------- +// Gets the remote address of the event block for the Target and verifies that it's valid. +// We use this address when we need to read from or write to the debugger control block. +// Also allocates the RS buffer used for temporary storage for information from the DCB and +// copies the LS DCB into the RS buffer. +// Arguments: +// output: pfBlockExists - true iff the LS DCB has been successfully allocated. Note that +// we need this information even if the function throws, so we can't simply send it back +// as a return value. +// Return value: +// None, but allocates GetDCB() on success. If the LS DCB has not +// been successfully initialized or if this throws, GetDCB() will be NULL. +// +// Notes: +// Throws on error +// +//----------------------------------------------------------------------------- +void CordbProcess::GetEventBlock(BOOL * pfBlockExists) +{ + if (GetDCB() == NULL) // we only need to do this once + { + _ASSERTE(m_pShim != NULL); + _ASSERTE(ThreadHoldsProcessLock()); + + // This will Initialize the DAC/DBI interface. + BOOL fDacReady = TryInitializeDac(); + + if (fDacReady) + { + // Ensure that we have a DAC interface. + _ASSERTE(m_pDacPrimitives != NULL); + + // This is not technically necessary for Mac debugging. The event channel doesn't rely on + // knowing the target address of the DCB on the LS. + CORDB_ADDRESS pLeftSideDCB = NULL; + pLeftSideDCB = (GetDAC()->GetDebuggerControlBlockAddress()); + if (pLeftSideDCB == NULL) + { + *pfBlockExists = false; + ThrowHR(CORDBG_E_DEBUGGING_NOT_POSSIBLE); + } + + IfFailThrow(NewEventChannelForThisPlatform(pLeftSideDCB, + m_pMutableDataTarget, + GetPid(), + m_pShim->GetMachineInfo(), + &m_pEventChannel)); + _ASSERTE(m_pEventChannel != NULL); + + // copy information from left side DCB + UpdateRightSideDCB(); + + // Verify that the control block is valid. + // This will throw on error. + VerifyControlBlock(); + + *pfBlockExists = true; + } + else + { + // we can't initialize the DAC, so we can't get the block + *pfBlockExists = false; + } + } + else // we got the block before + { + *pfBlockExists = true; + } + +} // CordbProcess::GetEventBlock() + + +// +// Verify that the version info in the control block matches what we expect. The minimum supported protocol from the +// Left Side must be greater or equal to the minimum required protocol of the Right Side. Note: its the Left Side's job +// to conform to whatever protocol the Right Side requires, so long as minimum is supported. +// +void CordbProcess::VerifyControlBlock() +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(m_pShim != NULL); + + if (GetDCB()->m_DCBSize == 0) + { + // the LS is still initializing the DCB + ThrowHR(CORDBG_E_DEBUGGING_NOT_POSSIBLE); + } + + // Fill in the protocol numbers for the Right Side and update the LS DCB. + GetDCB()->m_rightSideProtocolCurrent = CorDB_RightSideProtocolCurrent; + UpdateLeftSideDCBField(&(GetDCB()->m_rightSideProtocolCurrent), sizeof(GetDCB()->m_rightSideProtocolCurrent)); + + GetDCB()->m_rightSideProtocolMinSupported = CorDB_RightSideProtocolMinSupported; + UpdateLeftSideDCBField(&(GetDCB()->m_rightSideProtocolMinSupported), + sizeof(GetDCB()->m_rightSideProtocolMinSupported)); + + // For Telesto, Dbi and Wks have a more flexible versioning allowed, as described by the Debugger + // Version Protocol String in DEBUGGER_PROTOCOL_STRING in DbgIpcEvents.h. This allows different build + // numbers, but the other protocol numbers should still match. +#if !defined(FEATURE_CORECLR) + bool fSkipVerCheck = false; +#if _DEBUG + // In debug builds, allow us to disable the version check to help with applying hotfixes. + // The hotfix may be built against a compatible IPC protocol, but have a slightly different build number. + fSkipVerCheck = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_DbgSkipVerCheck) != 0; +#endif + + if (!fSkipVerCheck) + { + // + // These asserts double check that the version of the Right Side matches the version of the left side. + // + // If you hit these asserts, it is probably because you rebuilt mscordbi without rebuilding mscorwks, or rebuilt + // mscorwks without rebuilding mscordbi. You might be able to ignore these asserts, but proceed at your own risk. + // + CONSISTENCY_CHECK_MSGF(VER_PRODUCTBUILD == GetDCB()->m_verMajor, + ("version of %s (%d) in the debuggee does not match version of mscordbi.dll (%d) in the debugger.\n" + "This means your setup is wrong. You can ignore this but proceed at your own risk.\n", + MAIN_CLR_DLL_NAME_A, GetDCB()->m_verMajor, VER_PRODUCTBUILD)); + CONSISTENCY_CHECK_MSGF(VER_PRODUCTBUILD_QFE == GetDCB()->m_verMinor, + ("QFE version of %s (%d) in the debuggee does not match QFE version of mscordbi.dll (%d) in the debugger.\n" + "Both dlls have build # (%d).\n" + "This means your setup is wrong. You can ignore this but proceed at your own risk.\n", + MAIN_CLR_DLL_NAME_A, GetDCB()->m_verMinor, VER_PRODUCTBUILD_QFE, VER_PRODUCTBUILD)); + } +#endif // !FEATURE_CORECLR + + // These assertions verify that the debug manager is behaving correctly. + // An assertion failure here means that the runtime version of the debuggee is different from the runtime version of + // the debugger is capable of debugging. + + // The Debug Manager should properly match LS & RS, and thus guarantee that this assert should never fire. + // But just in case the installation is corrupted, we'll check it. + if (GetDCB()->m_DCBSize != sizeof(DebuggerIPCControlBlock)) + { + CONSISTENCY_CHECK_MSGF(false, ("DCB in LS is %d bytes, in RS is %d bytes. Version mismatch!!\n", + GetDCB()->m_DCBSize, sizeof(DebuggerIPCControlBlock))); + ThrowHR(CORDBG_E_INCOMPATIBLE_PROTOCOL); + } + + // The Left Side has to support at least our minimum required protocol. + if (GetDCB()->m_leftSideProtocolCurrent < GetDCB()->m_rightSideProtocolMinSupported) + { + _ASSERTE(GetDCB()->m_leftSideProtocolCurrent >= GetDCB()->m_rightSideProtocolMinSupported); + ThrowHR(CORDBG_E_INCOMPATIBLE_PROTOCOL); + } + + // The Left Side has to be able to emulate at least our minimum required protocol. + if (GetDCB()->m_leftSideProtocolMinSupported > GetDCB()->m_rightSideProtocolCurrent) + { + _ASSERTE(GetDCB()->m_leftSideProtocolMinSupported <= GetDCB()->m_rightSideProtocolCurrent); + ThrowHR(CORDBG_E_INCOMPATIBLE_PROTOCOL); + } + +#ifdef _DEBUG + char buf[MAX_LONGPATH]; + DWORD len = GetEnvironmentVariableA("CORDBG_NotCompatibleTest", buf, sizeof(buf)); + _ASSERTE(len < sizeof(buf)); + + if (len > 0) + ThrowHR(CORDBG_E_INCOMPATIBLE_PROTOCOL); +#endif + + if (GetDCB()->m_bHostingInFiber) + { + ThrowHR(CORDBG_E_CANNOT_DEBUG_FIBER_PROCESS); + } + + _ASSERTE(!GetDCB()->m_rightSideShouldCreateHelperThread); +} // CordbProcess::VerifyControlBlock + +//----------------------------------------------------------------------------- +// This is the CordbProcess objects chance to inspect the DCB and intialize stuff +// +// Return Value: +// Typical HRESULT return values, nothing abnormal. +// If succeeded, then the block exists and is valid. +// +//----------------------------------------------------------------------------- +HRESULT CordbProcess::GetRuntimeOffsets() +{ + INTERNAL_API_ENTRY(this); + + _ASSERTE(m_pShim != NULL); + UpdateRightSideDCB(); + + // Can't get a handle to the helper thread if the target is remote. + + // If we got this far w/o failing, then we should be able to get the helper thread handle. + // RS will handle not having the helper-thread handle, so we just make a best effort here. + DWORD dwHelperTid = GetDCB()->m_realHelperThreadId; + _ASSERTE(dwHelperTid != 0); + + + { +#if !defined FEATURE_CORESYSTEM + // kernel32!OpenThread does not exist on all platforms (missing on Win98). + // So we need to delay load it. + typedef HANDLE (WINAPI *FPOPENTHREAD)(DWORD dwDesiredAccess, + BOOL bInheritHandle, + DWORD dwThreadId); + + + + HMODULE mod = WszGetModuleHandle(W("kernel32.dll")); + + _ASSERTE(mod != NULL); // can't fail since Kernel32.dll is already loaded. + + const FPOPENTHREAD pfnOpenThread = (FPOPENTHREAD)GetProcAddress(mod, "OpenThread"); + + if (pfnOpenThread != NULL) + { + m_hHelperThread = pfnOpenThread(SYNCHRONIZE, FALSE, dwHelperTid); + CONSISTENCY_CHECK_MSGF(m_hHelperThread != NULL, ("Failed to get helper-thread handle. tid=0x%x\n", dwHelperTid)); + } +#elif FEATURE_PAL + m_hHelperThread = NULL; //RS is supposed to be able to live without a helper thread handle. +#else + m_hHelperThread = OpenThread(SYNCHRONIZE, FALSE, dwHelperTid); + CONSISTENCY_CHECK_MSGF(m_hHelperThread != NULL, ("Failed to get helper-thread handle. tid=0x%x\n", dwHelperTid)); +#endif + } + + // get the remote address of the runtime offsets structure and read the structure itself + HRESULT hrRead = SafeReadStruct(PTR_TO_CORDB_ADDRESS(GetDCB()->m_pRuntimeOffsets), &m_runtimeOffsets); + + if (FAILED(hrRead)) + { + return hrRead; + } + + LOG((LF_CORDB, LL_INFO10000, "CP::GRO: got runtime offsets: \n")); + +#ifdef FEATURE_INTEROP_DEBUGGING + LOG((LF_CORDB, LL_INFO10000, " m_genericHijackFuncAddr= 0x%p\n", + m_runtimeOffsets.m_genericHijackFuncAddr)); + LOG((LF_CORDB, LL_INFO10000, " m_signalHijackStartedBPAddr= 0x%p\n", + m_runtimeOffsets.m_signalHijackStartedBPAddr)); + LOG((LF_CORDB, LL_INFO10000, " m_excepNotForRuntimeBPAddr= 0x%p\n", + m_runtimeOffsets.m_excepNotForRuntimeBPAddr)); + LOG((LF_CORDB, LL_INFO10000, " m_notifyRSOfSyncCompleteBPAddr= 0x%p\n", + m_runtimeOffsets.m_notifyRSOfSyncCompleteBPAddr)); + LOG((LF_CORDB, LL_INFO10000, " m_raiseException= 0x%p\n", + m_runtimeOffsets.m_raiseExceptionAddr)); +#endif // FEATURE_INTEROP_DEBUGGING + + LOG((LF_CORDB, LL_INFO10000, " m_TLSIndex= 0x%08x\n", + m_runtimeOffsets.m_TLSIndex)); + LOG((LF_CORDB, LL_INFO10000, " m_EEThreadStateOffset= 0x%08x\n", + m_runtimeOffsets.m_EEThreadStateOffset)); + LOG((LF_CORDB, LL_INFO10000, " m_EEThreadStateNCOffset= 0x%08x\n", + m_runtimeOffsets.m_EEThreadStateNCOffset)); + LOG((LF_CORDB, LL_INFO10000, " m_EEThreadPGCDisabledOffset= 0x%08x\n", + m_runtimeOffsets.m_EEThreadPGCDisabledOffset)); + LOG((LF_CORDB, LL_INFO10000, " m_EEThreadPGCDisabledValue= 0x%08x\n", + m_runtimeOffsets.m_EEThreadPGCDisabledValue)); + LOG((LF_CORDB, LL_INFO10000, " m_EEThreadDebuggerWordOffset= 0x%08x\n", + m_runtimeOffsets.m_EEThreadDebuggerWordOffset)); + LOG((LF_CORDB, LL_INFO10000, " m_EEThreadFrameOffset= 0x%08x\n", + m_runtimeOffsets.m_EEThreadFrameOffset)); + LOG((LF_CORDB, LL_INFO10000, " m_EEThreadMaxNeededSize= 0x%08x\n", + m_runtimeOffsets.m_EEThreadMaxNeededSize)); + LOG((LF_CORDB, LL_INFO10000, " m_EEThreadSteppingStateMask= 0x%08x\n", + m_runtimeOffsets.m_EEThreadSteppingStateMask)); + LOG((LF_CORDB, LL_INFO10000, " m_EEMaxFrameValue= 0x%08x\n", + m_runtimeOffsets.m_EEMaxFrameValue)); + LOG((LF_CORDB, LL_INFO10000, " m_EEThreadDebuggerFilterContextOffset= 0x%08x\n", + m_runtimeOffsets.m_EEThreadDebuggerFilterContextOffset)); + LOG((LF_CORDB, LL_INFO10000, " m_EEThreadCantStopOffset= 0x%08x\n", + m_runtimeOffsets.m_EEThreadCantStopOffset)); + LOG((LF_CORDB, LL_INFO10000, " m_EEFrameNextOffset= 0x%08x\n", + m_runtimeOffsets.m_EEFrameNextOffset)); + LOG((LF_CORDB, LL_INFO10000, " m_EEIsManagedExceptionStateMask= 0x%08x\n", + m_runtimeOffsets.m_EEIsManagedExceptionStateMask)); + LOG((LF_CORDB, LL_INFO10000, " m_pPatches= 0x%08x\n", + m_runtimeOffsets.m_pPatches)); + LOG((LF_CORDB, LL_INFO10000, " m_offRgData= 0x%08x\n", + m_runtimeOffsets.m_offRgData)); + LOG((LF_CORDB, LL_INFO10000, " m_offCData= 0x%08x\n", + m_runtimeOffsets.m_offCData)); + LOG((LF_CORDB, LL_INFO10000, " m_cbPatch= 0x%08x\n", + m_runtimeOffsets.m_cbPatch)); + LOG((LF_CORDB, LL_INFO10000, " m_offAddr= 0x%08x\n", + m_runtimeOffsets.m_offAddr)); + LOG((LF_CORDB, LL_INFO10000, " m_offOpcode= 0x%08x\n", + m_runtimeOffsets.m_offOpcode)); + LOG((LF_CORDB, LL_INFO10000, " m_cbOpcode= 0x%08x\n", + m_runtimeOffsets.m_cbOpcode)); + LOG((LF_CORDB, LL_INFO10000, " m_offTraceType= 0x%08x\n", + m_runtimeOffsets.m_offTraceType)); + LOG((LF_CORDB, LL_INFO10000, " m_traceTypeUnmanaged= 0x%08x\n", + m_runtimeOffsets.m_traceTypeUnmanaged)); + +#ifdef FEATURE_INTEROP_DEBUGGING + // Flares are only used for interop debugging. + + // Do check that the flares are all at unique offsets. + // Since this is determined at link-time, we need a run-time check (an + // assert isn't good enough, since this would only happen in a super + // optimized / bbt run). + { + const void * flares[] = { + m_runtimeOffsets.m_signalHijackStartedBPAddr, + m_runtimeOffsets.m_excepForRuntimeHandoffStartBPAddr, + m_runtimeOffsets.m_excepForRuntimeHandoffCompleteBPAddr, + m_runtimeOffsets.m_signalHijackCompleteBPAddr, + m_runtimeOffsets.m_excepNotForRuntimeBPAddr, + m_runtimeOffsets.m_notifyRSOfSyncCompleteBPAddr, + }; + + const int NumFlares = NumItems(flares); + + // Ensure that all of the flares are unique. + for(int i = 0; i < NumFlares; i++) + { + for(int j = i+1; j < NumFlares; j++) + { + if (flares[i] == flares[j]) + { + // If we ever fail here, that means the LS build is busted. + + // This assert is useful if we drop a checked RS onto a retail + // LS (that's legal). + _ASSERTE(!"LS has matching Flares."); + LOG((LF_CORDB, LL_ALWAYS, "Failing because of matching flares.\n")); + return CORDBG_E_INCOMPATIBLE_PROTOCOL; + } + } + } + } + +#endif // FEATURE_INTEROP_DEBUGGING + m_runtimeOffsetsInitialized = true; + return S_OK; +} + +#ifdef FEATURE_INTEROP_DEBUGGING + +//----------------------------------------------------------------------------- +// Resume hijacked threads. +//----------------------------------------------------------------------------- +void CordbProcess::ResumeHijackedThreads() +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(m_pShim != NULL); + _ASSERTE(ThreadHoldsProcessLock()); + + LOG((LF_CORDB, LL_INFO10000, "CP::RHT: entered\n")); + if (this->m_state & (CordbProcess::PS_SOME_THREADS_SUSPENDED | CordbProcess::PS_HIJACKS_IN_PLACE)) + { + // On XP, This will also resume the threads suspended for Sync. + this->ResumeUnmanagedThreads(); + } + + // Hijacks send their ownership flares and then wait on this event. By setting this + // we let the hijacks run free. + if (this->m_leftSideUnmanagedWaitEvent != NULL) + { + SetEvent(this->m_leftSideUnmanagedWaitEvent); + } + else + { + // Only reason we expect to not have this event is if the CLR hasn't been loaded yet. + // In that case, we won't hijack, so nobody's listening for this event either. + _ASSERTE(!m_initialized); + } +} + +//----------------------------------------------------------------------------- +// For debugging support, record the win32 events. +// Note that although this is for debugging, we want it in retail because we'll +// be debugging retail most of the time :( +// pEvent - the win32 debug event we just received +// pUThread - our unmanaged thread object for the event. We could look it up +// from pEvent->dwThreadId, but passed in for perf reasons. +//----------------------------------------------------------------------------- +void CordbProcess::DebugRecordWin32Event(const DEBUG_EVENT * pEvent, CordbUnmanagedThread * pUThread) +{ + _ASSERTE(ThreadHoldsProcessLock()); + + // Although we could look up the Unmanaged thread, it's faster to have it just passed in. + // So here we do a consistency check. + _ASSERTE(pUThread != NULL); + _ASSERTE(pUThread->m_id == pEvent->dwThreadId); + + m_DbgSupport.m_TotalNativeEvents++; // bump up the counter. + + MiniDebugEvent * pMiniEvent = &m_DbgSupport.m_DebugEventQueue[m_DbgSupport.m_DebugEventQueueIdx]; + pMiniEvent->code = (BYTE) pEvent->dwDebugEventCode; + pMiniEvent->pUThread = pUThread; + + DWORD tid = pEvent->dwThreadId; + + // Record debug-event specific data. + switch(pEvent->dwDebugEventCode) + { + case LOAD_DLL_DEBUG_EVENT: + pMiniEvent->u.ModuleData.pBaseAddress = pEvent->u.LoadDll.lpBaseOfDll; + STRESS_LOG2(LF_CORDB, LL_INFO1000, "Win32 Debug Event received: tid=0x%8x, Load Dll. Addr=%p\n", + tid, + pEvent->u.LoadDll.lpBaseOfDll); + break; + case UNLOAD_DLL_DEBUG_EVENT: + pMiniEvent->u.ModuleData.pBaseAddress = pEvent->u.UnloadDll.lpBaseOfDll; + STRESS_LOG2(LF_CORDB, LL_INFO1000, "Win32 Debug Event received: tid=0x%8x, Unload Dll. Addr=%p\n", + tid, + pEvent->u.UnloadDll.lpBaseOfDll); + break; + case EXCEPTION_DEBUG_EVENT: + pMiniEvent->u.ExceptionData.pAddress = pEvent->u.Exception.ExceptionRecord.ExceptionAddress; + pMiniEvent->u.ExceptionData.dwCode = pEvent->u.Exception.ExceptionRecord.ExceptionCode; + + STRESS_LOG3(LF_CORDB, LL_INFO1000, "Win32 Debug Event received: tid=%8x, (1) Exception. Code=0x%08x, Addr=%p\n", + tid, + pMiniEvent->u.ExceptionData.dwCode, + pMiniEvent->u.ExceptionData.pAddress + ); + break; + default: + STRESS_LOG2(LF_CORDB, LL_INFO1000, "Win32 Debug Event received tid=%8x, %d\n", tid, pEvent->dwDebugEventCode); + break; + } + + + // Go to the next entry in the queue. + m_DbgSupport.m_DebugEventQueueIdx = (m_DbgSupport.m_DebugEventQueueIdx + 1) % DEBUG_EVENTQUEUE_SIZE; +} + +void CordbProcess::QueueUnmanagedEvent(CordbUnmanagedThread *pUThread, const DEBUG_EVENT *pEvent) +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(ThreadHoldsProcessLock()); + _ASSERTE(m_pShim != NULL); + + LOG((LF_CORDB, LL_INFO10000, "CP::QUE: queued unmanaged event %d for thread 0x%x\n", + pEvent->dwDebugEventCode, pUThread->m_id)); + + + _ASSERTE(pEvent->dwDebugEventCode == EXCEPTION_DEBUG_EVENT); + + // Copy the event into the given thread + CordbUnmanagedEvent *ue; + + // Use the primary IB event slot unless this is the special stack overflow event case. + if (!pUThread->HasSpecialStackOverflowCase()) + ue = pUThread->IBEvent(); + else + ue = pUThread->IBEvent2(); + + if(pUThread->HasIBEvent() && !pUThread->HasSpecialStackOverflowCase()) + { + // Any event being replaced should at least have been continued outside of the hijack + // We don't track whether or not we expect the exception to retrigger but if we are replacing + // the event then it did not. + _ASSERTE(ue->IsEventContinuedUnhijacked()); + LOG((LF_CORDB, LL_INFO10000, "CP::QUE: A previously seen event is being discarded 0x%x 0x%p\n", + ue->m_currentDebugEvent.u.Exception.ExceptionRecord.ExceptionCode, + ue->m_currentDebugEvent.u.Exception.ExceptionRecord.ExceptionAddress)); + DequeueUnmanagedEvent(ue->m_owner); + } + + memcpy(&(ue->m_currentDebugEvent), pEvent, sizeof(DEBUG_EVENT)); + ue->m_state = CUES_IsIBEvent; + ue->m_next = NULL; + + // Enqueue the event. + pUThread->SetState(CUTS_HasIBEvent); + + if (m_unmanagedEventQueue == NULL) + m_unmanagedEventQueue = ue; + else + m_lastQueuedUnmanagedEvent->m_next = ue; + + m_lastQueuedUnmanagedEvent = ue; +} + +void CordbProcess::DequeueUnmanagedEvent(CordbUnmanagedThread *ut) +{ + INTERNAL_API_ENTRY(this); + + _ASSERTE(m_unmanagedEventQueue != NULL); + _ASSERTE(ut->HasIBEvent() || ut->HasSpecialStackOverflowCase()); + _ASSERTE(ThreadHoldsProcessLock()); + + + CordbUnmanagedEvent *ue; + + if (ut->HasIBEvent()) + ue = ut->IBEvent(); + else + { + ue = ut->IBEvent2(); + + // Since we're dequeuing the special stack overflow event, we're no longer in the special stack overflow case. + ut->ClearState(CUTS_HasSpecialStackOverflowCase); + } + + DWORD ec = ue->m_currentDebugEvent.dwDebugEventCode; + LOG((LF_CORDB, LL_INFO10000, "CP::DUE: dequeue unmanaged event %d for thread 0x%x\n", ec, ut->m_id)); + + _ASSERTE(ec == EXCEPTION_DEBUG_EVENT); + + CordbUnmanagedEvent **tmp = &m_unmanagedEventQueue; + CordbUnmanagedEvent **prev = NULL; + + // Note: this supports out-of-order dequeing of unmanaged events. This is necessary because we queue events even if + // we're not clear on the ownership question. When we get the answer, and if the event belongs to the Runtime, we go + // ahead and yank the event out of the queue, wherever it may be. + while (*tmp && *tmp != ue) + { + prev = tmp; + tmp = &((*tmp)->m_next); + } + + _ASSERTE(*tmp == ue); + + *tmp = (*tmp)->m_next; + + if (m_unmanagedEventQueue == NULL) + m_lastQueuedUnmanagedEvent = NULL; + else if (m_lastQueuedUnmanagedEvent == ue) + { + _ASSERTE(prev != NULL); + m_lastQueuedUnmanagedEvent = *prev; + } + + ut->ClearState(CUTS_HasIBEvent); + +} + +void CordbProcess::QueueOOBUnmanagedEvent(CordbUnmanagedThread *pUThread, const DEBUG_EVENT * pEvent) +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(ThreadHoldsProcessLock()); + _ASSERTE(!pUThread->HasOOBEvent()); + _ASSERTE(IsWin32EventThread()); + _ASSERTE(m_pShim != NULL); + + LOG((LF_CORDB, LL_INFO10000, "CP::QUE: queued OOB unmanaged event %d for thread 0x%x\n", + pEvent->dwDebugEventCode, pUThread->m_id)); + + // Copy the event into the given thread + CordbUnmanagedEvent *ue = pUThread->OOBEvent(); + memcpy(&(ue->m_currentDebugEvent), pEvent, sizeof(DEBUG_EVENT)); + ue->m_state = CUES_None; + ue->m_next = NULL; + + // Enqueue the event. + pUThread->SetState(CUTS_HasOOBEvent); + + if (m_outOfBandEventQueue == NULL) + m_outOfBandEventQueue = ue; + else + m_lastQueuedOOBEvent->m_next = ue; + + m_lastQueuedOOBEvent = ue; +} + +void CordbProcess::DequeueOOBUnmanagedEvent(CordbUnmanagedThread *ut) +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(m_outOfBandEventQueue != NULL); + _ASSERTE(ut->HasOOBEvent()); + _ASSERTE(ThreadHoldsProcessLock()); + + CordbUnmanagedEvent *ue = ut->OOBEvent(); + DWORD ec = ue->m_currentDebugEvent.dwDebugEventCode; + + LOG((LF_CORDB, LL_INFO10000, "CP::DUE: dequeue OOB unmanaged event %d for thread 0x%x\n", ec, ut->m_id)); + + CordbUnmanagedEvent **tmp = &m_outOfBandEventQueue; + CordbUnmanagedEvent **prev = NULL; + + // Note: this supports out-of-order dequeing of unmanaged events. This is necessary because we queue events even if + // we're not clear on the ownership question. When we get the answer, and if the event belongs to the Runtime, we go + // ahead and yank the event out of the queue, wherever it may be. + while (*tmp && *tmp != ue) + { + prev = tmp; + tmp = &((*tmp)->m_next); + } + + _ASSERTE(*tmp == ue); + + *tmp = (*tmp)->m_next; + + if (m_outOfBandEventQueue == NULL) + m_lastQueuedOOBEvent = NULL; + else if (m_lastQueuedOOBEvent == ue) + { + _ASSERTE(prev != NULL); + m_lastQueuedOOBEvent = *prev; + } + + ut->ClearState(CUTS_HasOOBEvent); +} + +HRESULT CordbProcess::SuspendUnmanagedThreads() +{ + INTERNAL_API_ENTRY(this); + + _ASSERTE(ThreadHoldsProcessLock()); + + // Iterate over all unmanaged threads... + CordbUnmanagedThread* ut; + HASHFIND find; + + for (ut = m_unmanagedThreads.FindFirst(&find); ut != NULL; ut = m_unmanagedThreads.FindNext(&find)) + { + + // Don't suspend any thread in a can't stop region. This includes cooperative mode threads & preemptive + // threads that haven't pushed a NativeTransitionFrame. The ultimate problem here is that a thread + // in this state is effectively inside the runtime, and thus may take a lock that blocks the helper thread. + // IsCan'tStop also includes the helper thread & hijacked threads - which we shouldn't suspend anyways. + + // Only suspend those unmanaged threads that aren't already suspended by us and that aren't already hijacked by + // us. + + if (!ut->IsSuspended() && + !ut->IsDeleted() && + !ut->IsCantStop() && + !ut->IsBlockingForSync() + ) + { + LOG((LF_CORDB, LL_INFO1000, "CP::SUT: suspending unmanaged thread 0x%x, handle 0x%x\n", ut->m_id, ut->m_handle)); + + DWORD succ = SuspendThread(ut->m_handle); + + if (succ == 0xFFFFFFFF) + { + // This is okay... the thread may be dying after an ExitThread event. + LOG((LF_CORDB, LL_INFO1000, "CP::SUT: failed to suspend thread 0x%x\n", ut->m_id)); + } + else + { + m_state |= PS_SOME_THREADS_SUSPENDED; + + ut->SetState(CUTS_Suspended); + } + } + } + + return S_OK; +} + +HRESULT CordbProcess::ResumeUnmanagedThreads() +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(ThreadHoldsProcessLock()); + FAIL_IF_NEUTERED(this); + + // Iterate over all unmanaged threads... + CordbUnmanagedThread* ut; + HASHFIND find; + + for (ut = m_unmanagedThreads.FindFirst(&find); ut != NULL; ut = m_unmanagedThreads.FindNext(&find)) + { + // Only resume those unmanaged threads that were suspended by us. + if (ut->IsSuspended()) + { + LOG((LF_CORDB, LL_INFO1000, "CP::RUT: resuming unmanaged thread 0x%x\n", ut->m_id)); + + DWORD succ = ResumeThread(ut->m_handle); + + if (succ == 0xFFFFFFFF) + { + LOG((LF_CORDB, LL_INFO1000, "CP::RUT: failed to resume thread 0x%x\n", ut->m_id)); + } + else + ut->ClearState(CUTS_Suspended); + } + } + + m_state &= ~PS_SOME_THREADS_SUSPENDED; + + return S_OK; +} + +//----------------------------------------------------------------------------- +// DispatchUnmanagedInBandEvent +// +// Handler for Win32 events already known to be Unmanaged and in-band. +//----------------------------------------------------------------------------- +void CordbProcess::DispatchUnmanagedInBandEvent() +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(ThreadHoldsProcessLock()); + + // There should be no queued OOB events!!! If there are, then we have a breakdown in our protocol, since all OOB + // events should be dispatched before attempting to really continue from any in-band event. + _ASSERTE(m_outOfBandEventQueue == NULL); + _ASSERTE(m_cordb != NULL); + _ASSERTE(m_cordb->m_unmanagedCallback != NULL); + _ASSERTE(!m_dispatchingUnmanagedEvent); + + CordbUnmanagedThread * pUnmanagedThread = NULL; + CordbUnmanagedEvent * pUnmanagedEvent = m_unmanagedEventQueue; + + while (true) + { + // get the next queued event that isn't dispatched yet + while(pUnmanagedEvent != NULL && pUnmanagedEvent->IsDispatched()) + { + pUnmanagedEvent = pUnmanagedEvent->m_next; + } + + if(pUnmanagedEvent == NULL) + break; + + // Get the thread for this event + pUnmanagedThread = pUnmanagedEvent->m_owner; + + // We better not have dispatched it yet! + _ASSERTE(!pUnmanagedEvent->IsDispatched()); + + // We shouldn't be dispatching IB events on a thread that has exited. + // Though it's possible that the thread may exit *after* the IB event has been dispatched + // if it gets hijacked. + _ASSERTE(!pUnmanagedThread->IsDeleted()); + + // Make sure we keep the thread alive while we're playing with it. + pUnmanagedThread->InternalAddRef(); + + LOG((LF_CORDB, LL_INFO10000, "CP::DUE: dispatching unmanaged event %d for thread 0x%x\n", + pUnmanagedEvent->m_currentDebugEvent.dwDebugEventCode, pUnmanagedThread->m_id)); + + m_dispatchingUnmanagedEvent = true; + + // Add/Remove a reference which is scoped to the time that m_lastDispatchedIBEvent + // is set to pUnmanagedEvent (it is an interior pointer) + // see DevDiv issue 818301 for more details + if(m_lastDispatchedIBEvent != NULL) + { + m_lastDispatchedIBEvent->m_owner->InternalRelease(); + m_lastDispatchedIBEvent = NULL; + } + pUnmanagedThread->InternalAddRef(); + m_lastDispatchedIBEvent = pUnmanagedEvent; + pUnmanagedEvent->SetState(CUES_Dispatched); + + IncStopCount(); + + Unlock(); + + { + // Interface is semantically const, but does not include const in signature. + DEBUG_EVENT * pEvent = const_cast<DEBUG_EVENT *> (&(pUnmanagedEvent->m_currentDebugEvent)); + PUBLIC_WIN32_CALLBACK_IN_THIS_SCOPE(this,pEvent, FALSE); + m_cordb->m_unmanagedCallback->DebugEvent(pEvent, FALSE); + } + + Lock(); + + // Calling IMDA::Continue() will set m_dispatchingUnmanagedEvent = false. + // So if Continue() was called && we have more events, we'll loop and dispatch more events. + // Else we'll break out of the while loop. + if(m_dispatchingUnmanagedEvent) + break; + + // Continue was called in the dispatch callback, but that continue path just + // clears the dispatch flag and returns. The continue right here is the logical + // completion of the user's continue request + // Note it is sometimes the case that these events have already been continued because + // they had defered dispatching. At the time of deferal they were immediately continued. + // If the event is already continued then this continue becomes a no-op. + m_pShim->GetWin32EventThread()->DoDbgContinue(this, pUnmanagedEvent); + + // Release our reference to the unmanaged thread that we dispatched + if (pUnmanagedThread) + { + // This event should have been continued long ago... + _ASSERTE(!pUnmanagedThread->IBEvent()->IsEventWaitingForContinue()); + pUnmanagedThread->InternalRelease(); + pUnmanagedThread = NULL; + } + } + + m_dispatchingUnmanagedEvent = false; + + // Release our reference to the last thread that we dispatched now... + if(pUnmanagedThread) + { + pUnmanagedThread->InternalRelease(); + pUnmanagedThread = NULL; + } +} + +//----------------------------------------------------------------------------- +// DispatchUnmanagedOOBEvent +// +// Handler for Win32 events already known to be Unmanaged and out-of-band. +//----------------------------------------------------------------------------- +void CordbProcess::DispatchUnmanagedOOBEvent() +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(ThreadHoldsProcessLock()); + _ASSERTE(IsWin32EventThread()); + + // There should be OOB events queued... + _ASSERTE(m_outOfBandEventQueue != NULL); + _ASSERTE(m_cordb->m_unmanagedCallback != NULL); + + do + { + // Get the first event in the OOB Queue... + CordbUnmanagedEvent * pUnmanagedEvent = m_outOfBandEventQueue; + CordbUnmanagedThread * pUnmanagedThread = pUnmanagedEvent->m_owner; + + // Make sure we keep the thread alive while we're playing with it. + RSSmartPtr<CordbUnmanagedThread> pRef(pUnmanagedThread); + + LOG((LF_CORDB, LL_INFO10000, "[%x] CP::DUE: dispatching OOB unmanaged event %d for thread 0x%x\n", + GetCurrentThreadId(), pUnmanagedEvent->m_currentDebugEvent.dwDebugEventCode, pUnmanagedThread->m_id)); + + m_dispatchingOOBEvent = true; + pUnmanagedEvent->SetState(CUES_Dispatched); + Unlock(); + + { + // Interface is semantically const, but does not include const in signature. + DEBUG_EVENT * pEvent = const_cast<DEBUG_EVENT *> (&(pUnmanagedEvent->m_currentDebugEvent)); + PUBLIC_WIN32_CALLBACK_IN_THIS_SCOPE(this, pEvent, TRUE); + m_cordb->m_unmanagedCallback->DebugEvent(pEvent, TRUE); + } + + Lock(); + + // If they called Continue from the callback, then continue the OOB event right now before dispatching the next + // one. + if (!m_dispatchingOOBEvent) + { + DequeueOOBUnmanagedEvent(pUnmanagedThread); + + // Should not have continued from this debug event yet. + _ASSERTE(pUnmanagedEvent->IsEventWaitingForContinue()); + + // Do a little extra work if that was an OOB exception event... + HRESULT hr = pUnmanagedEvent->m_owner->FixupAfterOOBException(pUnmanagedEvent); + _ASSERTE(SUCCEEDED(hr)); + + // Go ahead and continue now... + this->m_pShim->GetWin32EventThread()->DoDbgContinue(this, pUnmanagedEvent); + } + + // Implicit release of pUnmanagedThread via pRef + } + while (!m_dispatchingOOBEvent && (m_outOfBandEventQueue != NULL)); + + m_dispatchingOOBEvent = false; + + LOG((LF_CORDB, LL_INFO10000, "CP::DUE: done dispatching OOB events. Queue=0x%08x\n", m_outOfBandEventQueue)); +} +#endif // FEATURE_INTEROP_DEBUGGING + +//----------------------------------------------------------------------------- +// StartSyncFromWin32Stop +// +// Get the process from a Fozen state or a Live state to a Synchronized State. +// Note that Process Exit is considered to be synchronized. +// This is a nop if we're not Interop Debugging. +// If this function succeeds, we're in a synchronized state. +// +// Arguments: +// pfAsyncBreakSent - returns if this method sent an async-break or not. +// +// Return value: +// typical HRESULT return values, nothing sinister here. +//----------------------------------------------------------------------------- +HRESULT CordbProcess::StartSyncFromWin32Stop(BOOL * pfAsyncBreakSent) +{ + INTERNAL_API_ENTRY(this); + if (m_pShim == NULL) // This API is moved off to the shim + { + return E_NOTIMPL; + } + + HRESULT hr = S_OK; + + // Caller should have taken the stop-go lock. This prevents us from racing w/ a continue. + _ASSERTE(m_StopGoLock.HasLock()); + + // Process should be init before we try to sync it. + _ASSERTE(this->m_initialized); + + // If nobody's listening for an AsyncBreak, and we're not stopped, then our caller + // doesn't know if we're sending an AsyncBreak or not; and thus we may not continue. + // Failing this assert means that we're stopping but we don't think we're going to get a continue + // down the road, and thus we're headed for a deadlock. + _ASSERTE((pfAsyncBreakSent != NULL) || (m_stopCount > 0)); + + if (pfAsyncBreakSent) + { + *pfAsyncBreakSent = FALSE; + } + +#ifdef FEATURE_INTEROP_DEBUGGING + + // If we're win32 stopped (but not out-of-band win32 stopped), or if we're running free on the Left Side but we're + // just not synchronized (and we're win32 attached), then go ahead and do an internal continue and send an async + // break event to get the Left Side sync'd up. + // + // The process can be running free as far as Win32 events are concerned, but still not synchronized as far as the + // Runtime is concerned. This can happen in a lot of cases where we end up with the Runtime not sync'd but with the + // process running free due to hijacking, etc... + if (((m_state & CordbProcess::PS_WIN32_STOPPED) && (m_outOfBandEventQueue == NULL)) || + (!GetSynchronized() && IsInteropDebugging())) + { + Lock(); + + if (((m_state & CordbProcess::PS_WIN32_STOPPED) && (m_outOfBandEventQueue == NULL)) || + (!GetSynchronized() && IsInteropDebugging())) + { + // This can't be the win32 ET b/c we need that thread to be alive and pumping win32 DE so that + // our Async Break can get across. + // So nobody should ever be calling this on the w32 ET. But they could, since we do trickle in from + // outside APIs. So we need a retail check. + if (IsWin32EventThread()) + { + _ASSERTE(!"Don't call this API on the W32 Event Thread"); + + Unlock(); + return ErrWrapper(CORDBG_E_CANT_CALL_ON_THIS_THREAD); + } + + STRESS_LOG1(LF_CORDB, LL_INFO1000, "[%x] CP::SSFW32S: sending internal continue\n", GetCurrentThreadId()); + + // Can't do this on the win32 event thread. + _ASSERTE(!this->IsWin32EventThread()); + + // If the helper thread is already dead, then we just return as if we sync'd the process. + if (m_helperThreadDead) + { + if (pfAsyncBreakSent) + { + *pfAsyncBreakSent = TRUE; + } + + // Mark the process as synchronized so no events will be dispatched until the thing is + // continued. However, the marking here is not a usual marking for synchronized. It has special + // semantics when we're interop debugging. We use m_oddSync to remember this so that we can take special + // action in Continue(). + SetSynchronized(true); + m_oddSync = true; + + // Get the RC Event Thread to stop listening to the process. + m_cordb->ProcessStateChanged(); + + Unlock(); + + return S_OK; + } + + m_stopRequested = true; + + // See ::Stop for why we defer this. The delayed events will be dispatched when some one calls continue. + // And we know they'll call continue b/c (stopCount > 0) || (our caller knows we're sending an AsyncBreak). + m_specialDeferment = true; + + Unlock(); + + // If the process gets synchronized between the Unlock() and here, then SendUnmanagedContinue() will end up + // not doing anything at all since a) it holds the process lock when working and b) it gates everything on + // if the process is sync'd or not. This is exactly what we want. + hr = this->m_pShim->GetWin32EventThread()->SendUnmanagedContinue(this, cInternalUMContinue); + + LOG((LF_CORDB, LL_INFO1000, "[%x] CP::SSFW32S: internal continue returned\n", GetCurrentThreadId())); + + // Send an async break to the left side now that its running. + DebuggerIPCEvent * pEvent = (DebuggerIPCEvent *) _alloca(CorDBIPC_BUFFER_SIZE); + InitIPCEvent(pEvent, DB_IPCE_ASYNC_BREAK, false, VMPTR_AppDomain::NullPtr()); + + LOG((LF_CORDB, LL_INFO1000, "[%x] CP::SSFW32S: sending async stop\n", GetCurrentThreadId())); + + // If the process gets synchronized between the Unlock() and here, then this message will do nothing (Left + // Side catches it) and we'll never get a response, and it won't hurt anything. + hr = m_cordb->SendIPCEvent(this, pEvent, CorDBIPC_BUFFER_SIZE); + // @Todo- how do we handle a failure here? + + // If the send returns with the helper thread being dead, then we know we don't need to wait for the process + // to sync. + if (!m_helperThreadDead) + { + STRESS_LOG1(LF_CORDB, LL_INFO1000, "[%x] CP::SSFW32S: sent async stop, waiting for event\n", GetCurrentThreadId()); + + // If we got synchronized between the Unlock() and here its okay since m_stopWaitEvent is still high + // from the last sync. + DWORD dwWaitResult = SafeWaitForSingleObject(this, m_stopWaitEvent, INFINITE); + + STRESS_LOG2(LF_CORDB, LL_INFO1000, "[%x] CP::SSFW32S: got event, %d\n", GetCurrentThreadId(), dwWaitResult); + + _ASSERTE(dwWaitResult == WAIT_OBJECT_0); + } + + Lock(); + + m_specialDeferment = false; + + if (pfAsyncBreakSent) + { + *pfAsyncBreakSent = TRUE; + } + + // If the helper thread died while we were trying to send an event to it, then we just do the same odd sync + // logic we do above. + if (m_helperThreadDead) + { + SetSynchronized(true); + m_oddSync = true; + hr = S_OK; + } + + m_stopRequested = false; + m_cordb->ProcessStateChanged(); + } + + Unlock(); + } +#endif // FEATURE_INTEROP_DEBUGGING + + return hr; +} + +// Check if the left side has exited. If so, get the right-side +// into shutdown mode. Only use this to avert us from going into +// an unrecoverable error. +bool CordbProcess::CheckIfLSExited() +{ +// Check by waiting on the handle with no timeout. + if (WaitForSingleObject(m_handle, 0) == WAIT_OBJECT_0) + { + Lock(); + m_terminated = true; + m_exiting = true; + Unlock(); + } + + LOG((LF_CORDB, LL_INFO10, "CP::IsLSExited() returning '%s'\n", + m_exiting ? "true" : "false")); + + return m_exiting; +} + +// Call this if something really bad happened and we can't do +// anything meaningful with the CordbProcess. +void CordbProcess::UnrecoverableError(HRESULT errorHR, + unsigned int errorCode, + const char *errorFile, + unsigned int errorLine) +{ + LOG((LF_CORDB, LL_INFO10, "[%x] CP::UE: unrecoverable error 0x%08x " + "(%d) %s:%d\n", + GetCurrentThreadId(), + errorHR, errorCode, errorFile, errorLine)); + + // We definitely want to know about any of these. + STRESS_LOG3(LF_CORDB, LL_EVERYTHING, "Unrecoverable Error:0x%08x, File=%s, line=%d\n", errorHR, errorFile, errorLine); + + // It's possible for an unrecoverable error to occur if the user detaches the + // debugger while inside CordbProcess::DispatchRCEvent() (as that function deliberately + // calls Unlock() while calling into the Shim). Detect such cases here & bail before we + // try to access invalid fields on this CordbProcess. + // + // Normally, we'd need to take the cordb process lock around the IsNeutered check + // (and the code that follows). And perhaps this is a good thing to do in the + // future. But for now we're not for two reasons: + // + // 1) It's scary. We're in UnrecoverableError() for gosh sake. I don't know all + // the possible bad states we can be in to get here. Will taking the process lock + // have ordering issues? Will the process lock even be valid to take here (or might + // we AV)? Since this is error handling, we should probably be as light as we can + // not to cause more errors. + // + // 2) It's unnecessary. For the Watson dump I investigated that caused this fix in + // the first place, we already detached before entering UnrecoverableError() + // (indeed, the only reason we're in UnrecoverableError is that we already detached + // and that caused a prior API to fail). Thus, there's no timing issue (in that + // case, anyway), wrt to entering UnrecoverableError() and detaching / neutering. + if (IsNeutered()) + return; + +#ifdef _DEBUG + // Ping our error trapping logic + HRESULT hrDummy; + hrDummy = ErrWrapper(errorHR); +#endif + + if (m_pShim == NULL) + { + // @dbgtodo - , shim: Once everything is hoisted, we can remove + // this code. + // In the v3 case, we should never get an unrecoverable error. Instead, the HR should be propogated + // and returned at the top-level public API. + _ASSERTE(!"Unrecoverable error dispatched in V3 case."); + } + + CONSISTENCY_CHECK_MSGF(IsLegalFatalError(errorHR), ("Unrecoverable internal error: hr=0x%08x!", errorHR)); + + if (!IsLegalFatalError(errorHR) || (errorHR != CORDBG_E_CANNOT_DEBUG_FIBER_PROCESS)) + { + // This will throw everything into a Zombie state. The ATT_ macros will check this and fail immediately. + m_unrecoverableError = true; + + // + // Mark the process as no longer synchronized. + // + Lock(); + SetSynchronized(false); + IncStopCount(); + Unlock(); + } + + // Set the error flags in the process so that if parts of it are + // still alive, it will realize that its in this mode and do the + // right thing. + if (GetDCB() != NULL) + { + GetDCB()->m_errorHR = errorHR; + GetDCB()->m_errorCode = errorCode; + EX_TRY + { + UpdateLeftSideDCBField(&(GetDCB()->m_errorHR), sizeof(GetDCB()->m_errorHR)); + UpdateLeftSideDCBField(&(GetDCB()->m_errorCode), sizeof(GetDCB()->m_errorCode)); + } + EX_CATCH + { + _ASSERTE(!"Writing process memory failed, perhaps due to an unexpected disconnection from the target."); + } + EX_END_CATCH(SwallowAllExceptions); + } + + // + // Let the user know that we've hit an unrecoverable error. + // + if (m_cordb->m_managedCallback) + { + // We are about to send DebuggerError call back. The state of RS is undefined. + // So we use the special Public Callback. We may be holding locks and stuff. + // We may also be deeply nested within the RS. + PUBLIC_CALLBACK_IN_THIS_SCOPE_DEBUGGERERROR(this); + m_cordb->m_managedCallback->DebuggerError((ICorDebugProcess*) this, + errorHR, + errorCode); + } +} + + +HRESULT CordbProcess::CheckForUnrecoverableError() +{ + HRESULT hr = S_OK; + + if (GetDCB() != NULL) + { + // be sure we have the latest information + UpdateRightSideDCB(); + + if (GetDCB()->m_errorHR != S_OK) + { + UnrecoverableError(GetDCB()->m_errorHR, + GetDCB()->m_errorCode, + __FILE__, __LINE__); + + hr = GetDCB()->m_errorHR; + } + } + + return hr; +} + + +/* + * EnableLogMessages enables/disables sending of log messages to the + * debugger for logging. + */ +HRESULT CordbProcess::EnableLogMessages(BOOL fOnOff) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + HRESULT hr = S_OK; + + DebuggerIPCEvent *event = (DebuggerIPCEvent*) _alloca(CorDBIPC_BUFFER_SIZE); + InitIPCEvent(event, DB_IPCE_ENABLE_LOG_MESSAGES, false, VMPTR_AppDomain::NullPtr()); + event->LogSwitchSettingMessage.iLevel = (int)fOnOff; + + hr = m_cordb->SendIPCEvent(this, event, CorDBIPC_BUFFER_SIZE); + hr = WORST_HR(hr, event->hr); + + LOG((LF_CORDB, LL_INFO10000, "[%x] CP::EnableLogMessages: EnableLogMessages=%d sent.\n", + GetCurrentThreadId(), fOnOff)); + + return hr; +} + +/* + * ModifyLogSwitch modifies the specified switch's severity level. + */ +COM_METHOD CordbProcess::ModifyLogSwitch(__in_z WCHAR *pLogSwitchName, LONG lLevel) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + + HRESULT hr = S_OK; + + _ASSERTE (pLogSwitchName != NULL); + + DebuggerIPCEvent *event = (DebuggerIPCEvent*) _alloca(CorDBIPC_BUFFER_SIZE); + InitIPCEvent(event, DB_IPCE_MODIFY_LOGSWITCH, false, VMPTR_AppDomain::NullPtr()); + event->LogSwitchSettingMessage.iLevel = lLevel; + event->LogSwitchSettingMessage.szSwitchName.SetStringTruncate(pLogSwitchName); + + hr = m_cordb->SendIPCEvent(this, event, CorDBIPC_BUFFER_SIZE); + hr = WORST_HR(hr, event->hr); + + LOG((LF_CORDB, LL_INFO10000, "[%x] CP::ModifyLogSwitch: ModifyLogSwitch sent.\n", + GetCurrentThreadId())); + + return hr; +} + +//----------------------------------------------------------------------------- +// Writes a buffer from the target and performs checks similar to SafeWriteStruct +// +// Arguments: +// tb - TargetBuffer which represents the target memory we want to write to +// pLocalBuffer - local pointer into source buffer +// cbSize - the size of local buffer +// +// Exceptions +// On error throws the result of WriteVirtual unless a short write is performed, +// in which case throws ERROR_PARTIAL_COPY +// +void CordbProcess::SafeWriteBuffer(TargetBuffer tb, + const BYTE * pLocalBuffer) +{ + _ASSERTE(m_pMutableDataTarget != NULL); + HRESULT hr = m_pMutableDataTarget->WriteVirtual(tb.pAddress, + pLocalBuffer, + tb.cbSize); + IfFailThrow(hr); +} + +//----------------------------------------------------------------------------- +// Reads a buffer from the target and performs checks similar to SafeWriteStruct +// +// Arguments: +// tb - TargetBuffer which represents the target memory to read from +// pLocalBuffer - local pointer into source buffer +// cbSize - the size of the remote buffer +// throwOnError - determines whether the function throws exceptions or returns HRESULTs +// in failure cases +// +// Exceptions: +// If throwOnError is TRUE +// On error always throws the special CORDBG_E_READVIRTUAL_FAILURE, unless a short write is performed +// in which case throws ERROR_PARTIAL_COPY +// If throwOnError is FALSE +// No exceptions are thrown, and instead the same error codes are returned as HRESULTs +// +HRESULT CordbProcess::SafeReadBuffer(TargetBuffer tb, BYTE * pLocalBuffer, BOOL throwOnError) +{ + ULONG32 cbRead; + HRESULT hr = m_pDACDataTarget->ReadVirtual(tb.pAddress, + pLocalBuffer, + tb.cbSize, + &cbRead); + + if (FAILED(hr)) + { + if (throwOnError) + ThrowHR(CORDBG_E_READVIRTUAL_FAILURE); + else + return CORDBG_E_READVIRTUAL_FAILURE; + } + + if (cbRead != tb.cbSize) + { + if (throwOnError) + ThrowWin32(ERROR_PARTIAL_COPY); + else + return HRESULT_FROM_WIN32(ERROR_PARTIAL_COPY); + } + return S_OK; +} + + +//--------------------------------------------------------------------------------------- +// Lookup or create an appdomain. +// +// Arguments: +// vmAppDomain - CLR appdomain to lookup +// +// Returns: +// Instance of CordbAppDomain for the given appdomain. This is a cached instance. +// If the CordbAppDomain does not yet exist, it will be created and added to the cache. +// Never returns NULL. Throw on error. +CordbAppDomain * CordbProcess::LookupOrCreateAppDomain(VMPTR_AppDomain vmAppDomain) +{ + CordbAppDomain * pAppDomain = m_appDomains.GetBase(VmPtrToCookie(vmAppDomain)); + if (pAppDomain != NULL) + { + return pAppDomain; + } + return CacheAppDomain(vmAppDomain); +} + +CordbAppDomain * CordbProcess::GetSharedAppDomain() +{ + if (m_sharedAppDomain == NULL) + { + CordbAppDomain *pAD = new CordbAppDomain(this, VMPTR_AppDomain::NullPtr()); + if (InterlockedCompareExchangeT<CordbAppDomain*>(&m_sharedAppDomain, pAD, NULL) != NULL) + { + delete pAD; + } + m_sharedAppDomain->InternalAddRef(); + } + + return m_sharedAppDomain; +} + +//--------------------------------------------------------------------------------------- +// +// Add a new appdomain to the cache. +// +// Arguments: +// vmAppDomain - appdomain to add. +// +// Return Value: +// Pointer to newly created appdomain, which should be the normal case. +// Throws on failure. Never returns null. +// +// Assumptions: +// Caller ensure the appdomain is not already cached. +// Caller should have stop-go lock, which provides thread-safety. +// +// Notes: +// This sets unrecoverable error on failure. +// +//--------------------------------------------------------------------------------------- +CordbAppDomain * CordbProcess::CacheAppDomain(VMPTR_AppDomain vmAppDomain) +{ + INTERNAL_API_ENTRY(GetProcess()); + + _ASSERTE(GetProcessLock()->HasLock()); + + RSInitHolder<CordbAppDomain> pAppDomain; + pAppDomain.Assign(new CordbAppDomain(this, vmAppDomain)); // throws + + // Add to the hash. This will addref the pAppDomain. + // Caller ensures we're not already cached. + // The cache will take ownership. + m_appDomains.AddBaseOrThrow(pAppDomain); + + // see if this is the default AppDomain + IDacDbiInterface * pDac = m_pProcess->GetDAC(); + BOOL fIsDefaultDomain = FALSE; + + fIsDefaultDomain = pDac->IsDefaultDomain(vmAppDomain); // throws + + if (fIsDefaultDomain) + { + // If this assert fires, then it likely means the target is corrupted. + TargetConsistencyCheck(m_pDefaultAppDomain == NULL); + m_pDefaultAppDomain = pAppDomain; + } + + CordbAppDomain * pReturn = pAppDomain; + pAppDomain.ClearAndMarkDontNeuter(); + + _ASSERTE(pReturn != NULL); + return pReturn; +} + +//--------------------------------------------------------------------------------------- +// +// Callback for Appdomain enumeration. +// +// Arguments: +// vmAppDomain - new appdomain to add to enumeration +// pUserData - data passed with callback (a 'this' ptr for CordbProcess) +// +// +// Assumptions: +// Invoked as callback from code:CordbProcess::PrepopulateAppDomains +// +// +//--------------------------------------------------------------------------------------- + +// static +void CordbProcess::AppDomainEnumerationCallback(VMPTR_AppDomain vmAppDomain, void * pUserData) +{ + CONTRACTL + { + THROWS; + } + CONTRACTL_END; + + CordbProcess * pProcess = static_cast<CordbProcess *> (pUserData); + INTERNAL_DAC_CALLBACK(pProcess); + + pProcess->LookupOrCreateAppDomain(vmAppDomain); +} + +//--------------------------------------------------------------------------------------- +// +// Traverse appdomains in the target and build up our list. +// +// Arguments: +// +// Return Value: +// returns on success. +// Throws on error. AppDomain cache may be partially populated. +// +// Assumptions: +// This is an non-invasive inspection operation called when the debuggee is stopped. +// +// Notes: +// This can be called multiple times. If the list is non-empty, it will nop. +//--------------------------------------------------------------------------------------- +void CordbProcess::PrepopulateAppDomainsOrThrow() +{ + CONTRACTL + { + THROWS; + } + CONTRACTL_END; + + INTERNAL_API_ENTRY(this); + + if (!IsDacInitialized()) + { + return; + } + + // DD-primitive that invokes a callback. This may throw. + GetDAC()->EnumerateAppDomains( + CordbProcess::AppDomainEnumerationCallback, + this); +} + +//--------------------------------------------------------------------------------------- +// +// EnumerateAppDomains enumerates all app domains in the process. +// +// Arguments: +// ppAppDomains - get appdomain enumerator +// +// Return Value: +// S_OK on success. +// +// Assumptions: +// +// +// Notes: +// This operation is non-invasive target. +// +//--------------------------------------------------------------------------------------- +HRESULT CordbProcess::EnumerateAppDomains(ICorDebugAppDomainEnum **ppAppDomains) +{ + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this); + { + ValidateOrThrow(ppAppDomains); + + // Ensure list is populated. + PrepopulateAppDomainsOrThrow(); + + RSInitHolder<CordbHashTableEnum> pEnum; + CordbHashTableEnum::BuildOrThrow( + this, + GetContinueNeuterList(), + &m_appDomains, + IID_ICorDebugAppDomainEnum, + pEnum.GetAddr()); + + *ppAppDomains = static_cast<ICorDebugAppDomainEnum*> (pEnum); + pEnum->ExternalAddRef(); + + pEnum.ClearAndMarkDontNeuter(); + } + PUBLIC_API_END(hr); + return hr; +} + +/* + * GetObject returns the runtime process object. + * Note: This method is not yet implemented. + */ +HRESULT CordbProcess::GetObject(ICorDebugValue **ppObject) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + VALIDATE_POINTER_TO_OBJECT(ppObject, ICorDebugObjectValue **); + + return E_NOTIMPL; +} + + +//--------------------------------------------------------------------------------------- +// +// Given a taskid, finding the corresponding thread. The function can fail if we do not +// find any thread with the given taskid +// +// Arguments: +// taskId - The task ID to look for. +// ppThread - OUT: Space for storing the thread corresponding to the taskId given. +// +// Return Value: +// Typical HRESULT symantics, nothing abnormal. +// +HRESULT CordbProcess::GetThreadForTaskID(TASKID taskId, ICorDebugThread2 ** ppThread) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(GetProcess()); + + HRESULT hr = S_OK; + + EX_TRY + { + RSLockHolder lockHolder(GetProcessLock()); + + if (ppThread == NULL) + { + ThrowHR(E_INVALIDARG); + } + + // On initialization, the task ID of every thread is INVALID_TASK_ID, unless a host is present and + // the host calls IClrTask::SetTaskIdentifier(). So we need to explicitly check for INVALID_TASK_ID + // here and return NULL if necessary. We return S_FALSE because that's the return value for the case + // where we can't find a thread for the specified task ID. + if (taskId == INVALID_TASK_ID) + { + *ppThread = NULL; + hr = S_FALSE; + } + else + { + PrepopulateThreadsOrThrow(); + + // now find the ICorDebugThread corresponding to it + CordbThread * pThread; + HASHFIND hashFind; + + + for (pThread = m_userThreads.FindFirst(&hashFind); + pThread != NULL; + pThread = m_userThreads.FindNext(&hashFind)) + { + if (pThread->GetTaskID() == taskId) + { + break; + } + } + + if (pThread == NULL) + { + *ppThread = NULL; + hr = S_FALSE; + } + else + { + *ppThread = pThread; + pThread->ExternalAddRef(); + } + } + } + EX_CATCH_HRESULT(hr); + return hr; +} // CordbProcess::GetThreadForTaskid + +HRESULT +CordbProcess::GetVersion(COR_VERSION* pVersion) +{ + if (NULL == pVersion) + { + return E_INVALIDARG; + } + + // + // Because we require a matching version of mscordbi.dll to debug a certain version of the runtime, + // we can just use constants found in this particular mscordbi.dll to determine the version of the left side. + pVersion->dwMajor = VER_MAJORVERSION; + pVersion->dwMinor = VER_MINORVERSION; + pVersion->dwBuild = VER_PRODUCTBUILD; + pVersion->dwSubBuild = VER_PRODUCTBUILD_QFE; + + return S_OK; +} + +#ifdef FEATURE_INTEROP_DEBUGGING +//----------------------------------------------------------------------------- +// Search for a native patch given the address. Return null if not found. +// Since we return an address, this is only valid until the table is disturbed. +//----------------------------------------------------------------------------- +NativePatch * CordbProcess::GetNativePatch(const void * pAddress) +{ + _ASSERTE(ThreadHoldsProcessLock()); + + int cTotal = m_NativePatchList.Count(); + NativePatch * pTable = m_NativePatchList.Table(); + if (pTable == NULL) + { + return NULL; + } + + for(int i = 0; i < cTotal; i++) + { + if (pTable[i].pAddress == pAddress) + { + return &pTable[i]; + } + } + return NULL; +} + +//----------------------------------------------------------------------------- +// Is there an break-opcode (int3 on x86) at the address in the debuggee? +//----------------------------------------------------------------------------- +bool CordbProcess::IsBreakOpcodeAtAddress(const void * address) +{ + // There should have been an int3 there already. Since we already put it in there, + // we should be able to safely read it out. + BYTE opcodeTest = 0; + + HRESULT hr = SafeReadStruct(PTR_TO_CORDB_ADDRESS(address), &opcodeTest); + SIMPLIFYING_ASSUMPTION_SUCCEEDED(hr); + + return (opcodeTest == CORDbg_BREAK_INSTRUCTION); +} +#endif // FEATURE_INTEROP_DEBUGGING + +//----------------------------------------------------------------------------- +// CordbProcess::SetUnmanagedBreakpoint +// Called by a native debugger to add breakpoints during Interop. +// address - remote address into the debuggee +// bufsize, buffer[] - initial size & buffer for the opcode that we're replacing. +// buflen - size of the buffer that we write to. +//----------------------------------------------------------------------------- +HRESULT +CordbProcess::SetUnmanagedBreakpoint(CORDB_ADDRESS address, ULONG32 bufsize, BYTE buffer[], ULONG32 * bufLen) +{ + LOG((LF_CORDB, LL_INFO100, "CP::SetUnBP: pProcess=%x, address=%p.\n", this, CORDB_ADDRESS_TO_PTR(address))); +#ifndef FEATURE_INTEROP_DEBUGGING + return E_NOTIMPL; +#else + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + FAIL_IF_MANAGED_ONLY(this); + _ASSERTE(!ThreadHoldsProcessLock()); + Lock(); + HRESULT hr = SetUnmanagedBreakpointInternal(address, bufsize, buffer, bufLen); + Unlock(); + return hr; +#endif +} + +//----------------------------------------------------------------------------- +// CordbProcess::SetUnmanagedBreakpointInternal +// The worker behind SetUnmanagedBreakpoint, this function can set both public +// breakpoints used by the debugger and internal breakpoints used for utility +// purposes in interop debugging. +// address - remote address into the debuggee +// bufsize, buffer[] - initial size & buffer for the opcode that we're replacing. +// buflen - size of the buffer that we write to. +//----------------------------------------------------------------------------- +HRESULT +CordbProcess::SetUnmanagedBreakpointInternal(CORDB_ADDRESS address, ULONG32 bufsize, BYTE buffer[], ULONG32 * bufLen) +{ + LOG((LF_CORDB, LL_INFO100, "CP::SetUnBPI: pProcess=%x, address=%p.\n", this, CORDB_ADDRESS_TO_PTR(address))); +#ifndef FEATURE_INTEROP_DEBUGGING + return E_NOTIMPL; +#else + + INTERNAL_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + FAIL_IF_MANAGED_ONLY(this); + _ASSERTE(ThreadHoldsProcessLock()); + + HRESULT hr = S_OK; + + NativePatch * p = NULL; +#if defined(DBG_TARGET_X86) || defined(DBG_TARGET_AMD64) + const BYTE patch = CORDbg_BREAK_INSTRUCTION; + BYTE opcode; + + // Make sure args are good + if ((buffer == NULL) || (bufsize < sizeof(patch)) || (bufLen == NULL)) + { + hr = E_INVALIDARG; + goto ErrExit; + } + + // Fail if there's already a patch at this address. + if (GetNativePatch(CORDB_ADDRESS_TO_PTR(address)) != NULL) + { + hr = CORDBG_E_NATIVE_PATCH_ALREADY_AT_ADDR; + goto ErrExit; + } + + // Preallocate this now so that if are oom, we can fail before we get half-way through. + p = m_NativePatchList.Append(); + if (p == NULL) + { + hr = E_OUTOFMEMORY; + goto ErrExit; + } + + + // Read out opcode. 1 byte on x86 + + hr = ApplyRemotePatch(this, CORDB_ADDRESS_TO_PTR(address), &p->opcode); + if (FAILED(hr)) + goto ErrExit; + + // It's all successful, so now update our out-params & internal bookkeaping. + opcode = (BYTE) p->opcode; + buffer[0] = opcode; + *bufLen = sizeof(opcode); + + p->pAddress = CORDB_ADDRESS_TO_PTR(address); + p->opcode = opcode; + + _ASSERTE(SUCCEEDED(hr)); +#elif defined(DBG_TARGET_WIN64) + PORTABILITY_ASSERT("NYI: CordbProcess::SetUnmanagedBreakpoint, interop debugging NYI on this platform"); + hr = E_NOTIMPL; + goto ErrExit; +#else + hr = E_NOTIMPL; + goto ErrExit; +#endif // DBG_TARGET_X8_ + + +ErrExit: + // If we failed, then free the patch + if (FAILED(hr) && (p != NULL)) + { + m_NativePatchList.Delete(*p); + } + + return hr; + +#endif // FEATURE_INTEROP_DEBUGGING +} + + +//----------------------------------------------------------------------------- +// CordbProcess::ClearUnmanagedBreakpoint +// Called by a native debugger to remove breakpoints during Interop. +// The patch is deleted even if the function fails. +//----------------------------------------------------------------------------- +HRESULT +CordbProcess::ClearUnmanagedBreakpoint(CORDB_ADDRESS address) +{ + LOG((LF_CORDB, LL_INFO100, "CP::ClearUnBP: pProcess=%x, address=%p.\n", this, CORDB_ADDRESS_TO_PTR(address))); +#ifndef FEATURE_INTEROP_DEBUGGING + return E_NOTIMPL; +#else + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + FAIL_IF_MANAGED_ONLY(this); + + _ASSERTE(!ThreadHoldsProcessLock()); + + HRESULT hr = S_OK; + PRD_TYPE opcode; + + Lock(); + + // Make sure this is a valid patch. + int cTotal = m_NativePatchList.Count(); + NativePatch * pTable = m_NativePatchList.Table(); + if (pTable == NULL) + { + hr = CORDBG_E_NO_NATIVE_PATCH_AT_ADDR; + goto ErrExit; + } + + int i; + for(i = 0; i < cTotal; i++) + { + if (pTable[i].pAddress == CORDB_ADDRESS_TO_PTR(address)) + break; + } + + if (i >= cTotal) + { + hr = CORDBG_E_NO_NATIVE_PATCH_AT_ADDR; + goto ErrExit; + } + + // Found it! Remove it from our table. Note that this may shuffle table contents + // around, so don't keep pointers into the table. + opcode = pTable[i].opcode; + + m_NativePatchList.Delete(pTable[i]); + _ASSERTE(m_NativePatchList.Count() == cTotal - 1); + + // Now remove the patch. + + + + // Just call through to Write ProcessMemory + hr = RemoveRemotePatch(this, CORDB_ADDRESS_TO_PTR(address), opcode); + if (FAILED(hr)) + goto ErrExit; + + + // Our internal bookeaping was already updated to remove the patch, so now we're done. + // If we had a failure, we should have already bailed. + _ASSERTE(SUCCEEDED(hr)); + +ErrExit: + Unlock(); + return hr; +#endif // FEATURE_INTEROP_DEBUGGING +} + + +//------------------------------------------------------------------------------------ +// StopCount, Sync, SyncReceived form our stop-status. This status is super-critical +// to most hangs, so we stress log it. +//------------------------------------------------------------------------------------ +void CordbProcess::SetSynchronized(bool fSynch) +{ + _ASSERTE(ThreadHoldsProcessLock() || !"Must have process lock to toggle SyncStatus"); + STRESS_LOG1(LF_CORDB, LL_INFO1000, "CP:: set sync=%d\n", fSynch); + m_synchronized = fSynch; +} + +bool CordbProcess::GetSynchronized() +{ + // This can be accessed whether we're Locked or not. This means that the result + // may change underneath us. + return m_synchronized; +} + +void CordbProcess::IncStopCount() +{ + _ASSERTE(ThreadHoldsProcessLock()); + m_stopCount++; + STRESS_LOG1(LF_CORDB, LL_INFO1000, "CP:: Inc StopCount=%d\n", m_stopCount); +} +void CordbProcess::DecStopCount() +{ + // We can inc w/ just the process lock (b/c we can dispatch events from the W32ET) + // But decrementing (eg, Continue), requires the stop-go lock. + // This if an operation takes the SG lock, it ensures we don't continue from underneath it. + ASSERT_SINGLE_THREAD_ONLY(HoldsLock(&m_StopGoLock)); + _ASSERTE(ThreadHoldsProcessLock()); + + m_stopCount--; + STRESS_LOG1(LF_CORDB, LL_INFO1000, "CP:: Dec StopCount=%d\n", m_stopCount); +} + +// Just gets whether we're stopped or not (m_stopped > 0). +// You only need the StopGo lock for this. +bool CordbProcess::IsStopped() +{ + // We don't require the process-lock, just the SG-lock. + // Holding the SG lock prevents another thread from continuing underneath you. + // (see DecStopCount()). + // But you could still be running free, and have another thread stop-underneath you. + // Thus IsStopped() leans towards returning false. + ASSERT_SINGLE_THREAD_ONLY(HoldsLock(&m_StopGoLock)); + + return (m_stopCount > 0); +} + +int CordbProcess::GetStopCount() +{ + _ASSERTE(ThreadHoldsProcessLock()); + return m_stopCount; +} + +bool CordbProcess::GetSyncCompleteRecv() +{ + _ASSERTE(ThreadHoldsProcessLock()); + return m_syncCompleteReceived; +} + +void CordbProcess::SetSyncCompleteRecv(bool fSyncRecv) +{ + _ASSERTE(ThreadHoldsProcessLock()); + STRESS_LOG1(LF_CORDB, LL_INFO1000, "CP:: set syncRecv=%d\n", fSyncRecv); + m_syncCompleteReceived = fSyncRecv; +} + +// This can be used if we ever need the RS to emulate old behavior of previous versions. +// This can not be used in QIs to deny queries for new interfaces. +// QIs must be consistent across the lifetime of an object. Say CordbThread used this in a QI +// do deny returning a ICorDebugThread2 interface when emulating v1.1. Once that Thread is neutered, +// it no longer has a pointer to the process, and it no longer knows if it should be denying +// the v2.0 query. An object's QI can't start returning new interfaces onces its neutered. +bool CordbProcess::SupportsVersion(CorDebugInterfaceVersion featureVersion) +{ + _ASSERTE(featureVersion == CorDebugVersion_2_0); + return true; +} + + +//--------------------------------------------------------------------------------------- +// Add an object to the process's Left-Side resource cleanup list +// +// Arguments: +// pObject - non-null object to be added +// +// Notes: +// This list tracks objects with process-scope that hold left-side +// resources (like func-eval). +// See code:CordbAppDomain::GetSweepableExitNeuterList for per-appdomain +// objects with left-side resources. +void CordbProcess::AddToLeftSideResourceCleanupList(CordbBase * pObject) +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(pObject != NULL); + + m_LeftSideResourceCleanupList.Add(this, pObject); +} + +// This list will get actively swept (looking for objects w/ external ref = 0) between continues. +void CordbProcess::AddToNeuterOnExitList(CordbBase *pObject) +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(pObject != NULL); + + HRESULT hr = S_OK; + EX_TRY + { + this->m_ExitNeuterList.Add(this, pObject); + } + EX_CATCH_HRESULT(hr); + SetUnrecoverableIfFailed(GetProcess(), hr); +} + +// Mark that this object should be neutered the next time we Continue the process. +void CordbProcess::AddToNeuterOnContinueList(CordbBase *pObject) +{ + INTERNAL_API_ENTRY(this); + _ASSERTE(pObject != NULL); + + m_ContinueNeuterList.Add(this, pObject); // throws +} + + +/* ------------------------------------------------------------------------- * + * Runtime Controller Event Thread class + * ------------------------------------------------------------------------- */ + +// +// Constructor +// +CordbRCEventThread::CordbRCEventThread(Cordb* cordb) +{ + _ASSERTE(cordb != NULL); + + m_cordb.Assign(cordb); + m_thread = NULL; + m_threadId = 0; + m_run = TRUE; + m_threadControlEvent = NULL; + m_processStateChanged = FALSE; + + g_pRSDebuggingInfo->m_RCET = this; +} + + +// +// Destructor. Cleans up all of the open handles and such. +// This expects that the thread has been stopped and has terminated +// before being called. +// +CordbRCEventThread::~CordbRCEventThread() +{ + if (m_threadControlEvent != NULL) + CloseHandle(m_threadControlEvent); + + if (m_thread != NULL) + CloseHandle(m_thread); + + g_pRSDebuggingInfo->m_RCET = NULL; +} + +// +// Init sets up all the objects that the thread will need to run. +// +HRESULT CordbRCEventThread::Init() +{ + if (m_cordb == NULL) + return E_INVALIDARG; + + m_threadControlEvent = WszCreateEvent(NULL, FALSE, FALSE, NULL); + + if (m_threadControlEvent == NULL) + return HRESULT_FROM_GetLastError(); + + return S_OK; +} + + +#if defined(FEATURE_INTEROP_DEBUGGING) +// +// Helper to duplicate a handle or thorw +// +// Arguments: +// pLocalHandle - handle to duplicate into the remote process +// pRemoteHandle - RemoteHandle structure in IPC block to hold the remote handle. +// Return value: +// None. Throws on error. +// +void CordbProcess::DuplicateHandleToLocalProcess(HANDLE * pLocalHandle, RemoteHANDLE * pRemoteHandle) +{ + _ASSERTE(m_pShim != NULL); + + // Dup RSEA and RSER into this process if we don't already have them. + // On Launch, we don't have them yet, but on attach we do. + if (*pLocalHandle == NULL) + { + BOOL fSuccess = pRemoteHandle->DuplicateToLocalProcess(m_handle, pLocalHandle); + if (!fSuccess) + { + ThrowLastError(); + } + } + +} +#endif // FEATURE_INTEROP_DEBUGGING + +// Public entry wrapper for code:CordbProcess::FinishInitializeIPCChannelWorker +void CordbProcess::FinishInitializeIPCChannel() +{ + // This is called directly from a shim callback. + PUBLIC_API_ENTRY_FOR_SHIM(this); + FinishInitializeIPCChannelWorker(); +} + +// +// Initialize the IPC channel. After this, IPC events can flow in both ways. +// +// Return value: +// Returns S_OK on success. +// +// Notes: +// This will dispatch an UnrecoverableError callback if it fails. +// This will also initialize key state in the CordbProcess object. +// +// @dbgtodo remove helper-thread: this should eventually go away once we get rid of IPC events. +// +void CordbProcess::FinishInitializeIPCChannelWorker() +{ + CONTRACTL + { + THROWS; + } + CONTRACTL_END; + + HRESULT hr = S_OK; + _ASSERTE(m_pShim != NULL); + + RSLockHolder lockHolder(&this->m_processMutex); + + // If it's already initialized, then nothing left to do. + // this protects us if this function is called multiple times. + if (m_initialized) + { + _ASSERTE(GetDCB() != NULL); + return; + } + + EX_TRY + { + LOG((LF_CORDB, LL_INFO1000, "[%x] RCET::HFRCE: first event..., process %p\n", GetCurrentThreadId(), this)); + + BOOL fBlockExists; + GetEventBlock(&fBlockExists); // throws on error + + LOG((LF_CORDB, LL_EVERYTHING, "Size of CdbP is %d\n", sizeof(CordbProcess))); + + m_pEventChannel->Init(m_handle); + +#if defined(FEATURE_INTEROP_DEBUGGING) + DuplicateHandleToLocalProcess(&m_leftSideUnmanagedWaitEvent, &GetDCB()->m_leftSideUnmanagedWaitEvent); +#endif // FEATURE_INTEROP_DEBUGGING + + // Read the Runtime Offsets struct out of the debuggee. + hr = GetRuntimeOffsets(); + IfFailThrow(hr); + + // we need to be careful here. The LS will have a thread running free that may be initializing + // fields of the DCB (specifically it may be setting up the helper thread), so we need to make sure + // we don't overwrite any fields that the LS is writing. We need to be sure we only write to RS + // status fields. + m_initialized = true; + GetDCB()->m_rightSideIsWin32Debugger = IsInteropDebugging(); + UpdateLeftSideDCBField(&(GetDCB()->m_rightSideIsWin32Debugger), sizeof(GetDCB()->m_rightSideIsWin32Debugger)); + + LOG((LF_CORDB, LL_INFO1000, "[%x] RCET::HFRCE: ...went fine\n", GetCurrentThreadId())); + _ASSERTE(SUCCEEDED(hr)); + + } EX_CATCH_HRESULT(hr); + if (SUCCEEDED(hr)) + { + return; + } + + // We only land here on failure cases. + // We must have jumped to this label. Maybe we didn't set HR, so check now. + STRESS_LOG1(LF_CORDB, LL_INFO1000, "HFCR: FAILED hr=0x%08x\n", hr); + + CloseIPCHandles(); + + // Rethrow + ThrowHR(hr); +} + + +//--------------------------------------------------------------------------------------- +// Marshals over a string buffer in a managed event +// +// Arguments: +// pTarget - data-target for read the buffer from the LeftSide. +// +// Throws on error +void Ls_Rs_BaseBuffer::CopyLSDataToRSWorker(ICorDebugDataTarget * pTarget) +{ + // + const DWORD cbCacheSize = m_cbSize; + + // SHOULD not happen for more than once in well-behaved case. + if (m_pbRS != NULL) + { + SIMPLIFYING_ASSUMPTION(!"m_pbRS is non-null; is this a corrupted event?"); + ThrowHR(E_INVALIDARG); + } + + NewHolder<BYTE> pData(new BYTE[cbCacheSize]); + + ULONG32 cbRead; + HRESULT hrRead = pTarget->ReadVirtual(PTR_TO_CORDB_ADDRESS(m_pbLS), pData, cbCacheSize , &cbRead); + + if(FAILED(hrRead)) + { + hrRead = CORDBG_E_READVIRTUAL_FAILURE; + } + + if (SUCCEEDED(hrRead) && (cbCacheSize != cbRead)) + { + hrRead = HRESULT_FROM_WIN32(ERROR_PARTIAL_COPY); + } + IfFailThrow(hrRead); + + // Now do Transfer + m_pbRS = pData; + pData.SuppressRelease(); +} + +//--------------------------------------------------------------------------------------- +// Marshals over a Byte buffer in a managed event +// +// Arguments: +// pTarget - data-target for read the buffer from the LeftSide. +// +// Throws on error +void Ls_Rs_ByteBuffer::CopyLSDataToRS(ICorDebugDataTarget * pTarget) +{ + CopyLSDataToRSWorker(pTarget); +} + +//--------------------------------------------------------------------------------------- +// Marshals over a string buffer in a managed event +// +// Arguments: +// pTarget - data-target for read the buffer from the LeftSide. +// +// Throws on error +void Ls_Rs_StringBuffer::CopyLSDataToRS(ICorDebugDataTarget * pTarget) +{ + CopyLSDataToRSWorker(pTarget); + + // Ensure we're a valid, well-formed string. + // @dbgtodo - this should only happen in corrupted scenarios. Perhaps a better HR here? + // - null terminated. + // - no embedded nulls. + + const WCHAR * pString = GetString(); + SIZE_T dwExpectedLenWithNull = m_cbSize / sizeof(WCHAR); + + // Should at least have 1 character for the null-terminator. + if (dwExpectedLenWithNull == 0) + { + ThrowHR(CORDBG_E_TARGET_INCONSISTENT); + } + + // Ensure that there's a null where we expect it to be. + if (pString[dwExpectedLenWithNull-1] != 0) + { + ThrowHR(CORDBG_E_TARGET_INCONSISTENT); + } + + // Now we know it's safe to call wcslen. The buffer is local, so we know the pages are there. + // And we know there's a null capping the max length of the string. + SIZE_T dwActualLenWithNull = wcslen(pString) + 1; + if (dwActualLenWithNull != dwExpectedLenWithNull) + { + ThrowHR(CORDBG_E_TARGET_INCONSISTENT); + } +} + +//--------------------------------------------------------------------------------------- +// Marshals the arguments in a managed-debug event. +// +// Arguments: +// pManagedEvent - (IN/OUT) debug event to marshal. Events are not usable in the host process +// until they are marshalled. This will marshal the event in-place, and may convert +// some target addresses to host addresses. +// +// Return Value: +// S_OK on success. Else Error. +// +// Assumptions: +// Target is currently stopped and inspectable. +// After the event is marshalled, it has resources that must be cleaned up +// by calling code:DeleteIPCEventHelper. +// +// Notes: +// Call a Copy function (CopyManagedEventFromTarget, CopyRCEventFromIPCBlock)to +// get the event to marshal. +// This will marshal args from the target into the host. +// The debug event is fixed size. But since the debuggee is stopped, this can copy +// arbitrary-length buffers out of of the debuggee. +// +// This could be rolled into code:CordbProcess::RawDispatchEvent +//--------------------------------------------------------------------------------------- +void CordbProcess::MarshalManagedEvent(DebuggerIPCEvent * pManagedEvent) +{ + CONTRACTL + { + THROWS; + + // Event has already been copied, now we do some quick Marshalling. + // Thsi should be a private local copy, and not the one in the IPC block or Target. + PRECONDITION(CheckPointer(pManagedEvent)); + } + CONTRACTL_END; + + IfFailThrow(pManagedEvent->hr); + + // This may throw part way through marshalling. But that's ok because + // code:DeleteIPCEventHelper can cleanup a partially-marshalled event. + + // Do a pre-processing on the event + switch (pManagedEvent->type & DB_IPCE_TYPE_MASK) + { + case DB_IPCE_MDA_NOTIFICATION: + { + pManagedEvent->MDANotification.szName.CopyLSDataToRS(this->m_pDACDataTarget); + pManagedEvent->MDANotification.szDescription.CopyLSDataToRS(this->m_pDACDataTarget); + pManagedEvent->MDANotification.szXml.CopyLSDataToRS(this->m_pDACDataTarget); + break; + } + + case DB_IPCE_FIRST_LOG_MESSAGE: + { + pManagedEvent->FirstLogMessage.szContent.CopyLSDataToRS(this->m_pDACDataTarget); + break; + } + + default: + break; + } + + +} + + +//--------------------------------------------------------------------------------------- +// Copy a managed debug event from the target process into this local process +// +// Arguments: +// pRecord - native-debug event serving as the envelope for the managed event. +// pLocalManagedEvent - (dst) required local buffer to hold managed event. +// +// Return Value: +// * True if the event belongs to this runtime. This is very useful when multiple CLRs are +// loaded into the target and all sending events wit the same exception code. +// * False if this does not belong to this instance of ICorDebug. (perhaps it's an event +// intended for another instance of the CLR in the target, or some rogue user code happening +// to use our exception code). +// In either case, the event can still be cleaned up via code:DeleteIPCEventHelper. +// +// Throws on error. In the error case, the contents of pLocalManagedEvent are undefined. +// They may have been partially copied from the target. The local managed event does not own +// any resources until it's marshalled, so the buffer can be ignored if this function fails. +// +// Assumptions: +// +// Notes: +// The events are sent form the target via code:Debugger::SendRawEvent +// This just does a raw Byte copy, but does not do any Marshalling. +// This should always succeed in the well-behaved case. However, A bad debuggee can +// always send a poor-formed debug event. +// We don't distinguish between a badly formed event and an event that's not ours. +// The event still needs to be Marshaled before being used. (see code:CordbProcess::MarshalManagedEvent) +// +//--------------------------------------------------------------------------------------- +#if defined(_MSC_VER) && defined(_TARGET_ARM_) +// This is a temporary workaround for an ARM specific MS C++ compiler bug (internal LKG build 18.1). +// Branch < if (ptrRemoteManagedEvent == NULL) > was always taken and the function always returned false. +// TODO: It should be removed once the bug is fixed. +#pragma optimize("", off) +#endif +bool CordbProcess::CopyManagedEventFromTarget( + const EXCEPTION_RECORD * pRecord, + DebuggerIPCEvent * pLocalManagedEvent) +{ + _ASSERTE(pRecord != NULL); + _ASSERTE(pLocalManagedEvent != NULL); + + // Initialize the event enough such backout code can call code:DeleteIPCEventHelper. + pLocalManagedEvent->type = DB_IPCE_DEBUGGER_INVALID; + + // Ensure we have a CLR instance ID by now. Either we had one already, or we're in + // V2 mode and this is the startup event, and so we'll set it now. + HRESULT hr = EnsureClrInstanceIdSet(); + IfFailThrow(hr); + _ASSERTE(m_clrInstanceId != 0); + + // Determine if the event is really a debug event, and for our instance. + CORDB_ADDRESS ptrRemoteManagedEvent = IsEventDebuggerNotification(pRecord, m_clrInstanceId); + + if (ptrRemoteManagedEvent == NULL) + { + return false; + } + + // What we are doing on Windows here is dangerous. Any buffer for IPC events must be at least + // CorDBIPC_BUFFER_SIZE big, but here we are only copying sizeof(DebuggerIPCEvent). Fortunately, the + // only case where an IPC event is bigger than sizeof(DebuggerIPCEvent) is for the second category + // described in the comment for code:IEventChannel. In this case, we are just transferring the IPC + // event from the native pipeline to the event channel, and the event channel will read it directly from + // the send buffer on the LS. See code:CordbRCEventThread::WaitForIPCEventFromProcess. +#if !defined(FEATURE_DBGIPC_TRANSPORT_DI) + hr = SafeReadStruct(ptrRemoteManagedEvent, pLocalManagedEvent); +#else + // For Mac remote debugging the address returned above is actually a local address. + // Also, we need to copy the entire buffer because once a debug event is read from the debugger + // transport, it won't be available afterwards. + memcpy(reinterpret_cast<BYTE *>(pLocalManagedEvent), + CORDB_ADDRESS_TO_PTR(ptrRemoteManagedEvent), + CorDBIPC_BUFFER_SIZE); + hr = S_OK; +#endif + SIMPLIFYING_ASSUMPTION(SUCCEEDED(hr)); + IfFailThrow(hr); + + return true; +} +#if defined(_MSC_VER) && defined(_TARGET_ARM_) +#pragma optimize("", on) +#endif + +//--------------------------------------------------------------------------------------- +// EnsureClrInstanceIdSet - Ensure we have a CLR Instance ID to debug +// +// In Arrowhead scenarios, the debugger is required to pass a valid CLR instance ID +// to us in OpenVirtualProcess. In V2 scenarios, for compatibility, we'll allow a +// CordbProcess object to exist for a process that doesn't yet have the CLR loaded. +// In this case the CLR instance ID will start off as 0, but be filled in when we see the +// startup exception indicating the CLR has been loaded. +// +// If we don't already have an instance ID, this function sets it to the only CLR in the +// target process. This requires that a CLR be loaded in the target process. +// +// Return Value: +// S_OK - if m_clrInstanceId was already set, or is now set to a valid CLR instance ID +// an error HRESULT - if m_clrInstanceId was 0, and cannot be set to a valid value +// (i.e. because we cannot find a CLR in the target process). +// +// Note that we need to probe for this on attach, and it's common to attach before the +// CLR has been loaded, so we avoid using exceptions for this common case. +// +HRESULT CordbProcess::EnsureClrInstanceIdSet() +{ + // If we didn't expect a specific CLR, then attempt to attach to any. + if (m_clrInstanceId == 0) + { + +#ifdef FEATURE_CORESYSTEM + if(m_cordb->GetTargetCLR() != 0) + { + m_clrInstanceId = PTR_TO_CORDB_ADDRESS(m_cordb->GetTargetCLR()); + return S_OK; + } +#endif + + // The only case in which we're allowed to request the "default" CLR instance + // ID is when we're running in V2 mode. In V3, the client is required to pass + // a non-zero value to OpenVirtualProcess. + _ASSERTE(m_pShim != NULL); + + HRESULT hr = m_pShim->FindLoadedCLR(&m_clrInstanceId); + if (FAILED(hr)) + { + // Couldn't find a loaded clr - no CLR instance ID yet + _ASSERTE(m_clrInstanceId == 0); + return hr; + } + } + + // We've (now) got a valid CLR instance id + return S_OK; +} + +//--------------------------------------------------------------------------------------- +// // Copy event from IPC block into local. +// +// Arguments: +// pLocalManagedEvent - required local buffer to hold managed event. +// +// Return Value: +// None. Always succeeds. +// +// Assumptions: +// The IPC block has already been opened and filled in with an event. +// +// Notes: +// This is copying from a shared-memory block, which is treated as local memory. +// This just does a raw Byte copy, but does not do any Marshalling. +// This does no validation on the event. +// The event still needs to be Marshaled before being used. (see code:CordbProcess::MarshalManagedEvent) +// +//--------------------------------------------------------------------------------------- +void inline CordbProcess::CopyRCEventFromIPCBlock(DebuggerIPCEvent * pLocalManagedEvent) +{ + _ASSERTE(pLocalManagedEvent != NULL); + + IfFailThrow(m_pEventChannel->GetEventFromLeftSide(pLocalManagedEvent)); +} + +// Return true if this is the RCEvent thread, else false. +bool CordbRCEventThread::IsRCEventThread() +{ + return (m_threadId == GetCurrentThreadId()); +} + +//--------------------------------------------------------------------------------------- +// Runtime assert, throws CORDBG_E_TARGET_INCONSISTENT if the expression is not true. +// +// Arguments: +// fExpression - assert parameter. If true, this function is a nop. If false, +// this will throw a CORDBG_E_TARGET_INCONSISTENT error. +// +// Notes: +// Use this for runtime checks to validate assumptions about the data-target. +// IcorDebug can't trust that data from the debugee is consistent (perhaps it's +// corrupted). +void CordbProcess::TargetConsistencyCheck(bool fExpression) +{ + if (!fExpression) + { + STRESS_LOG0(LF_CORDB, LL_INFO10000, "Target consistency check failed"); + + // When debugging possibly corrupt targets, this failure may be expected. For debugging purposes, + // assert if we're not expecting any target inconsistencies. + CONSISTENCY_CHECK_MSG( !m_fAssertOnTargetInconsistency, "Target consistency check failed unexpectedly"); + + ThrowHR(CORDBG_E_TARGET_INCONSISTENT); + } +} + +// +// SendIPCEvent -- send an IPC event to the runtime controller. All this +// really does is copy the event into the process's send buffer and sets +// the RSEA then waits on RSER. +// +// Note: when sending a two-way event (replyRequired = true), the +// eventSize must be large enough for both the event sent and the +// result event. +// +// Returns whether the event was sent successfully. This is different than event->eventHr. +// +HRESULT CordbRCEventThread::SendIPCEvent(CordbProcess* process, + DebuggerIPCEvent* event, + SIZE_T eventSize) +{ + + _ASSERTE(process != NULL); + _ASSERTE(event != NULL); + _ASSERTE(process->GetShim() != NULL); + +#ifdef _DEBUG + // We need to be synchronized whenever we're sending an IPC Event. + // This may require our callers' using a Stop-Continue holder. + // Attach + AsyncBreak are the only (obvious) exceptions. + // For continue, we set Sync-Status to false before sending, so we exclude that too. + // Everybody else should only be sending events when synced. We should never ever ever + // send an event from a CorbXYZ dtor (b/c that would be called at any random time). Instead, + // use a NeuterList. + switch (event->type) + { + case DB_IPCE_ATTACHING: + case DB_IPCE_ASYNC_BREAK: + case DB_IPCE_CONTINUE: + break; + + default: + CONSISTENCY_CHECK_MSGF(process->GetSynchronized(), ("Must by synced while sending IPC event: %s (0x%x)", + IPCENames::GetName(event->type), event->type)); + } +#endif + + + LOG((LF_CORDB, LL_EVERYTHING, "SendIPCEvent in CordbRCEventThread called\n")); + + // For simplicity sake, we have the following conservative invariants when sending IPC events: + // - Always hold the Stop-Go lock. + // - never on the W32ET. + // - Never hold the Process-lock (this allows the w32et to take that lock to pump) + + // Must have the stop-go lock to send an IPC event. + CONSISTENCY_CHECK_MSGF(process->GetStopGoLock()->HasLock(), ("Must have stop-go lock to send event. proc=%p, event=%s", + process, IPCENames::GetName(event->type))); + + // The w32 ET will need to take the process lock. So if we're holding it here, then we'll + // deadlock (since W32 ET is blocked on lock, which we would hold; and we're blocked on W32 ET + // to keep pumping. + _ASSERTE(!process->ThreadHoldsProcessLock() || !"Can't hold P-lock while sending blocking IPC event"); + + + // Can't be on the w32 ET, or we can't be pumping. + // Although we can trickle in here from public APIs, our caller should have validated + // that we weren't on the w32et, so the assert here is justified. But just in case there's something we missed, + // we have a runtime check (as a final backstop against a deadlock). + _ASSERTE(!process->IsWin32EventThread()); + CORDBFailIfOnWin32EventThread(process); + + + // If this is an async event, then we expect it to be sent while the process is locked. + if (event->asyncSend) + { + // This may be on the w32et, so we can't hold the stop-go lock. + _ASSERTE(event->type == DB_IPCE_ATTACHING); // only async event should be attaching. + } + + + // This will catch us if we've detached or exited. + // Note if we exited, then we should have been neutered and so shouldn't even be sending an IPC event, + // but just in case, we'll check. + CORDBRequireProcessStateOK(process); + + +#ifdef _DEBUG + // We should never send an Async Break on the RCET. This will deadlock. + // - if we're on the RCET, we should be stopped, and thus Stop() should just bump up a stop count, + // and not actually send an AsyncBreak. + // - Delayed-Continues help enforce this. + // This is a special case of the deadlock check below. + if (IsRCEventThread()) + { + _ASSERTE(event->type != DB_IPCE_ASYNC_BREAK); + } +#endif + +#ifdef _DEBUG + // This assert protects us against a deadlock. + // 1) (RCET) blocked on (This function): If we're on the RCET, then the RCET is blocked until we return (duh). + // 2) (LS) blocked on (RCET): If the LS is not synchronized, then it may be sending an event to the RCET, and thus blocked on the RCET. + // 3) (Helper thread) blocked on (LS): That LS thread may be holding a lock that the helper thread needs, thus blocking the helper thread. + // 4) (This function) blocked on (Helper Thread): We block until the helper thread can process our IPC event. + // #4 is not true for async events. + // + // If we hit this assert, it means we may get the deadlock above and we're calling SendIPCEvent at a time we shouldn't. + // Note this race is as old as dirt. + if (IsRCEventThread() && !event->asyncSend) + { + // Note that w/ Continue & Attach, GetSynchronized() has a different meaning and the race above won't happen. + BOOL fPossibleDeadlock = process->GetSynchronized() || (event->type == DB_IPCE_CONTINUE) || (event->type == DB_IPCE_ATTACHING); + CONSISTENCY_CHECK_MSGF(fPossibleDeadlock, ("Possible deadlock while sending: '%s'\n", IPCENames::GetName(event->type))); + } +#endif + + + + // Cache this process into the MRU so that we can find it if we're debugging in retail. + g_pRSDebuggingInfo->m_MRUprocess = process; + + HRESULT hr = S_OK; + HRESULT hrEvent = S_OK; + _ASSERTE(event != NULL); + + // NOTE: the eventSize parameter is only so you can specify an event size that is SMALLER than the process send + // buffer size!! + if (eventSize > CorDBIPC_BUFFER_SIZE) + return E_INVALIDARG; + + STRESS_LOG4(LF_CORDB, LL_INFO1000, "CRCET::SIPCE: sending %s to AD 0x%x, proc 0x%x(%d)\n", + IPCENames::GetName(event->type), VmPtrToCookie(event->vmAppDomain), process->m_id, process->m_id); + + // For 2-way events, this check is unnecessary (since we already check for LS exit) + // But for async events, we need this. + // So just check it up here and make everyone's life easier. + if (process->m_terminated) + { + STRESS_LOG0(LF_CORDB, LL_INFO10000, "CRCET::SIPCE: LS already terminated, shortcut exiting\n"); + return CORDBG_E_PROCESS_TERMINATED; + } + + // If the helper thread has died, we can't send an IPC event (and it's never coming back either). + // Although we do wait on the thread's handle, there are strange windows where the thread's handle + // is not yet signaled even though we've continued from the exit-thread event for the helper. + if (process->m_helperThreadDead) + { + STRESS_LOG0(LF_CORDB, LL_INFO10000, "CRCET::SIPCE: Helper-thread dead, shortcut exiting\n"); + return CORDBG_E_PROCESS_TERMINATED; + } + + BOOL fUnrecoverableError = TRUE; + EX_TRY + { + hr = process->GetEventChannel()->SendEventToLeftSide(event, eventSize); + fUnrecoverableError = FALSE; + } + EX_CATCH_HRESULT(hr); + + + // If we're sending a Continue() event, then after this, the LS may run free. + // If this is the last managed event before the LS exits, (which is the case + // if we're responding to either an Exit-Thread or if we respond to a Detach) + // the LS may exit at anytime from here on, so we need to be careful. + + + if (fUnrecoverableError) + { + _ASSERTE(FAILED(hr)); + CORDBSetUnrecoverableError(process, hr, 0); + } + else + { + // Get a handle to the target process - this call always succeeds + HANDLE hLSProcess = NULL; + process->GetHandle(&hLSProcess); + + // We take locks to ensure that the CordbProcess object is still alive, + // even if the OS process exited. + _ASSERTE(hLSProcess != NULL); + + // Check if Sending the IPC event failed + if (FAILED(hr)) + { + // The failure to send an event may be due to the target process terminating + // (especially, but not exclusively, in the case of async events). + // There is a race here - we can't rely on any check above SendEventToLeftSide + // to tell us whether the process has exited yet. + // Check for that case and return an accurate hresult. + DWORD ret = WaitForSingleObject(hLSProcess, 0); + if (ret == WAIT_OBJECT_0) + { + return CORDBG_E_PROCESS_TERMINATED; + } + + // Some other failure sending the IPC event - just return it. + return hr; + } + + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CRCET::SIPCE: sent...\n"); + + // If this is an async send, then don't wait for the left side to acknowledge that its read the event. + _ASSERTE(!event->asyncSend || !event->replyRequired); + + if (process->GetEventChannel()->NeedToWaitForAck(event)) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000,"CRCET::SIPCE: waiting for left side to read event. (on RSER)\n"); + + DWORD ret; + + // Wait for either a reply (common case) or the left side to go away. + // We can't detach while waiting for a reply (because detach needs to send events). + // All of the outcomes from this wait are completely disjoint. + // It's possible for the LS to reply and then exit normally (Thread_Detach, Process_Detach) + // and so ExitProcess may have been called, but it doesn't matter. + + enum { + ID_RSER = WAIT_OBJECT_0, + ID_LSPROCESS, + ID_HELPERTHREAD, + }; + + // Only wait on the helper thread for cases where the process is stopped (and thus we don't expect it do exit on us). + // If the process is running and we lose our helper thread, it ought to be during shutdown and we ough to + // follow up with an exit. + // This includes when we've dispatch Native events, and it includes the AsyncBreak sent to get us from a + // win32 frozen state to a synchronized state). + HANDLE hHelperThread = NULL; + if (process->IsStopped()) + { + hHelperThread = process->GetHelperThreadHandle(); + } + + + // Note that in case of a tie (multiple handles signaled), WaitForMultipleObjects gives + // priority to the handle earlier in the array. + HANDLE waitSet[] = { process->GetEventChannel()->GetRightSideEventAckHandle(), hLSProcess, hHelperThread}; + DWORD cWaitSet = NumItems(waitSet); + if (hHelperThread == NULL) + { + cWaitSet--; + } + + do + { + ret = WaitForMultipleObjectsEx(cWaitSet, waitSet, FALSE, CordbGetWaitTimeout(), FALSE); + // If we timeout because we're waiting for an uncontinued OOB event, we need to just keep waiting. + } while ((ret == WAIT_TIMEOUT) && process->IsWaitingForOOBEvent()); + + switch(ret) + { + case ID_RSER: + // Normal reply from LS. + // This is set iff the LS replied to our event. The LS may have exited since it replied + // but we don't care. We still have the reply and we'll pass it on. + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CRCET::SIPCE: left side read the event.\n"); + + // If this was a two-way event, then the result is already ready for us. Simply copy the result back + // over the original event that was sent. Otherwise, the left side has simply read the event and is + // processing it... + if (event->replyRequired) + { + process->GetEventChannel()->GetReplyFromLeftSide(event, eventSize); + hrEvent = event->hr; + } + break; + + case ID_LSPROCESS: + // Left side exited on us. + // ExitProcess may or may not have been called here (since it's on a different thread). + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CRCET::SIPCE: left side exiting while RS was waiting for reply.\n"); + hr = CORDBG_E_PROCESS_TERMINATED; + break; + + case ID_HELPERTHREAD: + // We can only send most IPC events while the LS is synchronized. We shouldn't lose our helper thread + // when synced under any sort of normal conditions. + // This won't fire if the process already exited, because LSPROCESS gets higher priority in the wait + // (since it was placed earlier). + // Thus the only "legitimate" window where this could happen would be in a shutdown scenario after + // the helper is dead but before the process has died. We shouldn't be synced in that scenario, + // so we shouldn't be sending IPC events during it. + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CRCET::SIPCE: lost helper thread.\n"); + + + // Assert because we want to know if we ever actually hit this in any detectable scenario. + // However, shutdown can occur in preemptive mode. Thus if the RS does an AsyncBreak late + // enough, then the LS will appear to be stopped but may still shutdown. + // Since the debuggee can exit asynchronously at any time (eg, suppose somebody forcefully + // kills it with taskman), this doesn't introduce a new case. + // That aside, it would be great to be able to assert this: + //_ASSERTE(!"Potential deadlock - Randomly Lost helper thread"); + + // We'll piggy back this on the terminated case. + hr = CORDBG_E_PROCESS_TERMINATED; + break; + + default: + { + // If we timed out/failed, check the left side to see if it is in the unrecoverable error mode. If it is, + // return the HR from the left side that caused the error. Otherwise, return that we timed out and that + // we don't really know why. + HRESULT realHR = (ret == WAIT_FAILED) ? HRESULT_FROM_GetLastError() : ErrWrapper(CORDBG_E_TIMEOUT); + + hr = process->CheckForUnrecoverableError(); + + if (hr == S_OK) + { + CORDBSetUnrecoverableError(process, realHR, 0); + hr = realHR; + } + + STRESS_LOG1(LF_CORDB, LL_INFO1000, "CRCET::SIPCE: left side timeout/fail while RS waiting for reply. hr = 0x%08x\n", hr); + } + break; + } + + // If the LS picked up RSEA, it will be reset (since it's an auto event). + // But in the case that the wait failed or that the LS exited, we need to explicitly reset RSEA + if (hr != S_OK) + { + process->GetEventChannel()->ClearEventForLeftSide(); + } + + // Done waiting for reply. + + } + } + + process->ForceDacFlush(); + + // The hr and hrEvent are 2 very different things. + // hr tells us whether the event was sent successfully. + // hrEvent tells us how the LS responded to it. + // if FAILED(hr), then hrEvent is useless b/c the LS never got it. + // But if SUCCEEDED(hr), then hrEvent may still have failed and that could be + // valuable information. + + return hr; +} + +//--------------------------------------------------------------------------------------- +// FlushQueuedEvents flushes a process's event queue. +// +// Arguments: +// pProcess - non-null process object whose queue will be drained +// +// Notes: +// @dbgtodo shim: this should be part of the shim. +// This dispatches events that are queued up. The queue is populated by +// the shim's proxy callback (see code:ShimProxyCallback). This will dispatch events +// to the 'real' callback supplied by the debugger. This will dispatch events +// as long as the debugger keeps calling continue. +// +// This requires that the process lock be held, although it will toggle the lock. +void CordbRCEventThread::FlushQueuedEvents(CordbProcess* process) +{ + CONTRACTL + { + NOTHROW; // This is happening on the RCET thread, so there's no place to propogate an error back up. + } + CONTRACTL_END; + + STRESS_LOG0(LF_CORDB,LL_INFO10000, "CRCET::FQE: Beginning to flush queue\n"); + + _ASSERTE(process->GetShim() != NULL); + + // We should only call this is we already have queued events + _ASSERTE(!process->GetShim()->GetManagedEventQueue()->IsEmpty()); + + // + // Dispatch queued events so long as they keep calling Continue() + // before returning from their callback. If they call Continue(), + // process->m_synchronized will be false again and we know to + // loop around and dispatch the next event. + // + _ASSERTE(process->ThreadHoldsProcessLock()); + + + // Give shim a chance to queue any faked attach events. Grab a pointer to the + // ShimProcess now, while we still hold the process lock. Once we release the lock, + // GetShim() may not work. + RSExtSmartPtr<ShimProcess> pShim(process->GetShim()); + + // Release lock before we call out to shim to Queue fake events. + { + RSInverseLockHolder inverseLockHolder(process->GetProcessLock()); + { + PUBLIC_CALLBACK_IN_THIS_SCOPE0_NO_LOCK(pProcess); + + // Because we've released the lock, at any point from here forward the + // CorDbProcess may suddenly get neutered if the user detaches the debugger. + + pShim->QueueFakeAttachEventsIfNeeded(false); + } + } + + // Now that we're holding the process lock again, we can safely check whether + // process has become neutered + if (process->IsNeutered()) + { + return; + } + + { + + // Main dispatch loop here. DispatchRCEvent will take events out of the + // queue and invoke callbacks + do + { + // DispatchRCEvent will mark the process as stopped before dispatching. + process->DispatchRCEvent(); + + LOG((LF_CORDB,LL_INFO10000, "CRCET::FQE: Finished w/ " + "DispatchRCEvent\n")); + } + while (process->GetSyncCompleteRecv() && + (process->GetSynchronized() == false) && + (process->GetShim() != NULL) && // may have lost Shim if we detached while dispatch + (!process->GetShim()->GetManagedEventQueue()->IsEmpty()) && + (process->m_unrecoverableError == false)); + } + + // + // If they returned from a callback without calling Continue() then + // the process is still synchronized, so let the rc event thread + // know that it need to update its process list and remove the + // process's event. + // + if (process->GetSynchronized()) + { + ProcessStateChanged(); + } + + LOG((LF_CORDB,LL_INFO10000, "CRCET::FQE: finished\n")); +} + +//--------------------------------------------------------------------------------------- +// Preliminary Handle an Notification event from the target. This may queue the event, +// but does not actually dispatch the event. +// +// Arguments: +// pManagedEvent - local managed-event. On success, this function assumes ownership of the +// event and will delete its memory. Assumed that caller allocated via 'new'. +// pCallback - callback obecjt to dispatch events on. +// +// Return Value: +// None. Throws on error. On error, caller still owns the pManagedEvent and must free it. +// +// Assumptions: +// This should be called once a notification event is received from the target. +// +// Notes: +// HandleRCEvent -- handle an IPC event received from the runtime controller. +// This will update ICorDebug state and immediately dispatch the event. +// +//--------------------------------------------------------------------------------------- +void CordbProcess::HandleRCEvent( + DebuggerIPCEvent * pManagedEvent, + RSLockHolder * pLockHolder, + ICorDebugManagedCallback * pCallback) +{ + CONTRACTL + { + THROWS; + PRECONDITION(CheckPointer(pManagedEvent)); + PRECONDITION(CheckPointer(pCallback)); + PRECONDITION(ThreadHoldsProcessLock()); + } + CONTRACTL_END; + + if (!this->IsSafeToSendEvents() || this->m_exiting) + { + return; + } + + // Marshals over some standard data from event. + MarshalManagedEvent(pManagedEvent); + + STRESS_LOG4(LF_CORDB, LL_INFO1000, "RCET::TP: Got %s for AD 0x%x, proc 0x%x(%d)\n", + IPCENames::GetName(pManagedEvent->type), VmPtrToCookie(pManagedEvent->vmAppDomain), this->m_id, this->m_id); + + RSExtSmartPtr<ICorDebugManagedCallback2> pCallback2; + pCallback->QueryInterface(IID_ICorDebugManagedCallback2, reinterpret_cast<void **> (&pCallback2)); + + RSExtSmartPtr<ICorDebugManagedCallback3> pCallback3; + pCallback->QueryInterface(IID_ICorDebugManagedCallback3, reinterpret_cast<void **> (&pCallback3)); + + // Dispatch directly. May not necessarily dispatch an event. + // Toggles the lock to dispatch callbacks. + RawDispatchEvent(pManagedEvent, pLockHolder, pCallback, pCallback2, pCallback3); +} + +// +// ProcessStateChanged -- tell the rc event thread that the ICorDebug's +// process list has changed by setting its flag and thread control event. +// This will cause the rc event thread to update its set of handles to wait +// on. +// +void CordbRCEventThread::ProcessStateChanged() +{ + m_cordb->LockProcessList(); + STRESS_LOG0(LF_CORDB, LL_INFO100000, "CRCET::ProcessStateChanged\n"); + m_processStateChanged = TRUE; + SetEvent(m_threadControlEvent); + m_cordb->UnlockProcessList(); +} + + +//--------------------------------------------------------------------------------------- +// Primary loop of the Runtime Controller event thread. This routine loops during the +// debug session taking IPC events from the IPC block and calling out to process them. +// +// Arguments: +// None. +// +// Return Value: +// None. +// +// Notes: +// @dbgtodo shim: eventually hoist the entire RCET into the shim. +//--------------------------------------------------------------------------------------- +void CordbRCEventThread::ThreadProc() +{ + HANDLE waitSet[MAXIMUM_WAIT_OBJECTS]; + CordbProcess * rgProcessSet[MAXIMUM_WAIT_OBJECTS]; + unsigned int waitCount; + +#ifdef _DEBUG + memset(&rgProcessSet, NULL, MAXIMUM_WAIT_OBJECTS * sizeof(CordbProcess *)); + memset(&waitSet, NULL, MAXIMUM_WAIT_OBJECTS * sizeof(HANDLE)); +#endif + + + // First event to wait on is always the thread control event. + waitSet[0] = m_threadControlEvent; + rgProcessSet[0] = NULL; + waitCount = 1; + + while (m_run) + { + DWORD dwStatus = WaitForMultipleObjectsEx(waitCount, waitSet, FALSE, 2000, FALSE); + + if (dwStatus == WAIT_FAILED) + { + STRESS_LOG1(LF_CORDB, LL_INFO10000, "CordbRCEventThread::ThreadProc WaitFor" + "MultipleObjects failed: 0x%x\n", GetLastError()); + } +#ifdef _DEBUG + else if ((dwStatus >= WAIT_OBJECT_0) && (dwStatus < WAIT_OBJECT_0 + waitCount) && m_run) + { + // Got an event. Figure out which process it came from. + unsigned int procNumber = dwStatus - WAIT_OBJECT_0; + + if (procNumber != 0) + { + // @dbgtodo shim: rip all of this out. Leave the assert in for now to verify that we're not accidentally + // going down this codepath. Once we rip this out, we can also simplify some of the code below. + // Notification events (including Sync-complete) should be coming from Win32 event thread via + // V3 pipeline. + _ASSERTE(!"Shouldn't be here"); + + } + } +#endif + + // Empty any queued work items. + DrainWorkerQueue(); + + // Check a flag to see if we need to update our list of processes to wait on. + if (m_processStateChanged) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "RCET::TP: refreshing process list.\n"); + + unsigned int i; + + // + // free the old wait list + // + for (i = 1; i < waitCount; i++) + { + rgProcessSet[i]->InternalRelease(); + } + + // Pass 1: iterate the hash of all processes and collect the unsynchronized ones into the wait list. + // Note that Stop / Continue can still be called on a different thread while we're doing this. + m_cordb->LockProcessList(); + m_processStateChanged = FALSE; + + waitCount = 1; + + CordbSafeHashTable<CordbProcess> * pHashTable = m_cordb->GetProcessList(); + HASHFIND hashFind; + CordbProcess * pProcess; + + for (pProcess = pHashTable->FindFirst(&hashFind); pProcess != NULL; pProcess = pHashTable->FindNext(&hashFind)) + { + _ASSERTE(waitCount < MAXIMUM_WAIT_OBJECTS); + + if( waitCount >= MAXIMUM_WAIT_OBJECTS ) + { + break; + } + + // Only listen to unsynchronized processes. Processes that are synchronized will not send events without + // being asked by us first, so there is no need to async listen to them. + // + // Note: if a process is not synchronized then there is no way for it to transition to the syncrhonized + // state without this thread receiving an event and taking action. So there is no need to lock the + // per-process mutex when checking the process's synchronized flag here. + if (!pProcess->GetSynchronized() && pProcess->IsSafeToSendEvents()) + { + STRESS_LOG2(LF_CORDB, LL_INFO1000, "RCET::TP: listening to process 0x%x(%d)\n", + pProcess->m_id, pProcess->m_id); + + waitSet[waitCount] = pProcess->m_leftSideEventAvailable; + rgProcessSet[waitCount] = pProcess; + rgProcessSet[waitCount]->InternalAddRef(); + waitCount++; + } + } + + m_cordb->UnlockProcessList(); + + // Pass 2: for each process that we placed in the wait list, determine if there are any existing queued + // events that need to be flushed. + + // Start i at 1 to skip the control event... + i = 1; + + while(i < waitCount) + { + pProcess = rgProcessSet[i]; + + // Take the process lock so we can check the queue safely + pProcess->Lock(); + + // Now that we've just locked the processes, we can safely inspect it and dispatch events. + // The process may have changed since when we first added it to the process list in Pass 1, + // so we can't make any assumptions about whether it's sync, live, or exiting. + + // Flush the queue if necessary. Note, we only do this if we've actually received a SyncComplete message + // from this process. If we haven't received a SyncComplete yet, then we don't attempt to drain any + // queued events yet. They'll be drained when the SyncComplete event is actually received. + if (pProcess->GetSyncCompleteRecv() && + (pProcess->GetShim() != NULL) && + !pProcess->GetSynchronized()) + { + if (pProcess->GetShim()->GetManagedEventQueue()->IsEmpty()) + { + // Effectively what we are doing here is to continue everything without actually + // handling an event. We can get here if the event raised by the LS is a duplicate + // creation event, which the shim discards without adding it to the event queue. + // See code:ShimProcess::IsDuplicateCreationEvent. + // + // To continue, we need to increment the stop count first. Also, we can't call + // Continue() while holding the process lock. + pProcess->SetSynchronized(true); + pProcess->IncStopCount(); + pProcess->Unlock(); + pProcess->ContinueInternal(FALSE); + pProcess->Lock(); + } + else + { + // This may toggle the process-lock + FlushQueuedEvents(pProcess); + } + } + + // Flushing could have left the process synchronized... + // Common case is if the callback didn't call Continue(). + if (pProcess->GetSynchronized()) + { + // remove the process from the wait list by moving all the other processes down one. + if ((i + 1) < waitCount) + { + memcpy(&rgProcessSet[i], &(rgProcessSet[i+1]), sizeof(rgProcessSet[0]) * (waitCount - i - 1)); + memcpy(&waitSet[i], &waitSet[i+1], sizeof(waitSet[0]) * (waitCount - i - 1)); + } + + // drop the count of processes to wait on + waitCount--; + + pProcess->Unlock(); + + // make sure to release the reference we added when the process was added to the wait list. + pProcess->InternalRelease(); + + // We don't have to increment i because we've copied the next element into + // the current value at i. + } + else + { + // Even after flushing, its still not syncd, so leave it in the wait list. + pProcess->Unlock(); + + // Increment i normally. + i++; + } + } + } // end ProcessStateChanged + } // while (m_run) + +#ifdef _DEBUG_IMPL + // We intentionally return while leaking some CordbProcess objects inside + // rgProcessSet, in some cases (e.g., I've seen this happen when detaching from a + // debuggee almost immediately after attaching to it). In the future, we should + // really consider not leaking these anymore. However, I'm unsure how safe it is to just + // go and InternalRelease() those guys, as above we intentionally DON'T release them when + // they're not synchronized. So for now, to make debug builds happy, exclude those + // references when we run CheckMemLeaks() later on. In our next side-by-side release, + // consider actually doing InternalRelease() on the remaining CordbProcesses on + // retail, and then we can remove the following loop. + for (UINT i=1; i < waitCount; i++) + { + InterlockedDecrement(&Cordb::s_DbgMemTotalOutstandingInternalRefs); + } +#endif //_DEBUG_IMPL +} + + +// +// This is the thread's real thread proc. It simply calls to the +// thread proc on the given object. +// +/*static*/ +DWORD WINAPI CordbRCEventThread::ThreadProc(LPVOID parameter) +{ + CordbRCEventThread * pThread = (CordbRCEventThread *) parameter; + + INTERNAL_THREAD_ENTRY(pThread); + pThread->ThreadProc(); + return 0; +} + +template<typename T> +InterlockedStack<T>::InterlockedStack() +{ + m_pHead = NULL; +} + +template<typename T> +InterlockedStack<T>::~InterlockedStack() +{ + // This is an arbitrary choice. We expect the stacks be drained. + _ASSERTE(m_pHead == NULL); +} + +// Thread safe pushes + pops. +// Many threads can push simultaneously. +// Only 1 thread can pop. +template<typename T> +void InterlockedStack<T>::Push(T * pItem) +{ + // InterlockedCompareExchangePointer(&dest, ex, comp). + // Really behaves like: + // val = *dest; + // if (*dest == comp) { *dest = ex; } + // return val; + // + // We can do a thread-safe assign { comp = dest; dest = ex } via: + // do { comp = dest } while (ICExPtr(&dest, ex, comp) != comp)); + + + do + { + pItem->m_next = m_pHead; + } + while(InterlockedCompareExchangeT(&m_pHead, pItem, pItem->m_next) != pItem->m_next); +} + +// Returns NULL on empty, +// else returns the head of the list. +template<typename T> +T * InterlockedStack<T>::Pop() +{ + if (m_pHead == NULL) + { + return NULL; + } + + // This allows 1 thread to Pop() and race against N threads doing a Push(). + T * pItem = NULL; + do + { + pItem = m_pHead; + } while(InterlockedCompareExchangeT(&m_pHead, pItem->m_next, pItem) != pItem); + + return pItem; +} + + +// RCET will take ownership of this item and delete it. +// This can be done w/o taking any locks (thus it can be called from any lock context) +// This may race w/ the RCET draining the queue. +void CordbRCEventThread::QueueAsyncWorkItem(RCETWorkItem * pItem) +{ + // @todo - + // Non-blocking insert into queue. + + _ASSERTE(pItem != NULL); + + m_WorkerStack.Push(pItem); + + // Ping the RCET so that it drains the queue. + SetEvent(m_threadControlEvent); +} + +// Execute & delete all workitems in the queue. +// This can be done w/o taking any locks. (though individual items may take locks). +void CordbRCEventThread::DrainWorkerQueue() +{ + _ASSERTE(IsRCEventThread()); + + while(true) + { + RCETWorkItem* pCur = m_WorkerStack.Pop(); + if (pCur == NULL) + { + break; + } + + pCur->Do(); + delete pCur; + } +} + + +//--------------------------------------------------------------------------------------- +// Wait for an reply from the debuggee. +// +// Arguments: +// pProcess - process for debuggee. +// pAppDomain - not used. +// pEvent - caller-allocated event to be filled out. +// This is expected to be at least as big as CorDBIPC_BUFFER_SIZE. +// +// Return Value: +// S_OK on success. else failure. +// +// Assumptions: +// Caller allocates +// +// Notes: +// WaitForIPCEventFromProcess waits for an event from just the specified +// process. This should only be called when the process is in a synchronized +// state, which ensures that the RCEventThread isn't listening to the +// process's event, too, which would get confusing. +// +// @dbgtodo - this function should eventually be obsolete once everything +// is using DAC calls instead of helper-thread. +// +//--------------------------------------------------------------------------------------- +HRESULT CordbRCEventThread::WaitForIPCEventFromProcess(CordbProcess * pProcess, + CordbAppDomain * pAppDomain, + DebuggerIPCEvent * pEvent) +{ + CORDBRequireProcessStateOKAndSync(pProcess, pAppDomain); + + DWORD dwStatus; + HRESULT hr = S_OK; + + do + { + dwStatus = SafeWaitForSingleObject(pProcess, + pProcess->m_leftSideEventAvailable, + CordbGetWaitTimeout()); + + if (pProcess->m_terminated) + { + return CORDBG_E_PROCESS_TERMINATED; + } + // If we timeout because we're waiting for an uncontinued OOB event, we need to just keep waiting. + } while ((dwStatus == WAIT_TIMEOUT) && pProcess->IsWaitingForOOBEvent()); + + + + + if (dwStatus == WAIT_OBJECT_0) + { + pProcess->CopyRCEventFromIPCBlock(pEvent); + + EX_TRY + { + pProcess->MarshalManagedEvent(pEvent); + + STRESS_LOG4(LF_CORDB, LL_INFO1000, "CRCET::SIPCE: Got %s for AD 0x%x, proc 0x%x(%d)\n", + IPCENames::GetName(pEvent->type), + VmPtrToCookie(pEvent->vmAppDomain), + pProcess->m_id, + pProcess->m_id); + + } + EX_CATCH_HRESULT(hr) + + SetEvent(pProcess->m_leftSideEventRead); + + return hr; + } + else if (dwStatus == WAIT_TIMEOUT) + { + // + // If we timed out, check the left side to see if it is in the + // unrecoverable error mode. If it is, return the HR from the + // left side that caused the error. Otherwise, return that we timed + // out and that we don't really know why. + // + HRESULT realHR = ErrWrapper(CORDBG_E_TIMEOUT); + + hr = pProcess->CheckForUnrecoverableError(); + + if (hr == S_OK) + { + CORDBSetUnrecoverableError(pProcess, realHR, 0); + return realHR; + } + else + return hr; + } + else + { + _ASSERTE(dwStatus == WAIT_FAILED); + + hr = HRESULT_FROM_GetLastError(); + + CORDBSetUnrecoverableError(pProcess, hr, 0); + + return hr; + } +} + + +// +// Start actually creates and starts the thread. +// +HRESULT CordbRCEventThread::Start() +{ + if (m_threadControlEvent == NULL) + { + return E_INVALIDARG; + } + + m_thread = CreateThread(NULL, + 0, + &CordbRCEventThread::ThreadProc, + (LPVOID) this, + 0, + &m_threadId); + + if (m_thread == NULL) + { + return HRESULT_FROM_GetLastError(); + } + + return S_OK; +} + + +// +// Stop causes the thread to stop receiving events and exit. It +// waits for it to exit before returning. +// +HRESULT CordbRCEventThread::Stop() +{ + if (m_thread != NULL) + { + LOG((LF_CORDB, LL_INFO100000, "CRCET::Stop\n")); + + m_run = FALSE; + + SetEvent(m_threadControlEvent); + + DWORD ret = WaitForSingleObject(m_thread, INFINITE); + + if (ret != WAIT_OBJECT_0) + { + return HRESULT_FROM_GetLastError(); + } + } + + m_cordb.Clear(); + + return S_OK; +} + + +/* ------------------------------------------------------------------------- * + * Win32 Event Thread class + * ------------------------------------------------------------------------- */ + +enum +{ + W32ETA_NONE = 0, + W32ETA_CREATE_PROCESS = 1, + W32ETA_ATTACH_PROCESS = 2, + W32ETA_CONTINUE = 3, + W32ETA_DETACH = 4 +}; + + + +//--------------------------------------------------------------------------------------- +// Constructor +// +// Arguments: +// pCordb - Pointer to the owning cordb object for this event thread. +// pShim - Pointer to the shim for supporting V2 debuggers on V3 architecture. +// +//--------------------------------------------------------------------------------------- +CordbWin32EventThread::CordbWin32EventThread( + Cordb * pCordb, + ShimProcess * pShim + ) : + m_thread(NULL), m_threadControlEvent(NULL), + m_actionTakenEvent(NULL), m_run(TRUE), + m_action(W32ETA_NONE) +{ + m_cordb.Assign(pCordb); + _ASSERTE(pCordb != NULL); + + m_pShim = pShim; + + m_pNativePipeline = NULL; +} + + +// +// Destructor. Cleans up all of the open handles and such. +// This expects that the thread has been stopped and has terminated +// before being called. +// +CordbWin32EventThread::~CordbWin32EventThread() +{ + if (m_thread != NULL) + CloseHandle(m_thread); + + if (m_threadControlEvent != NULL) + CloseHandle(m_threadControlEvent); + + if (m_actionTakenEvent != NULL) + CloseHandle(m_actionTakenEvent); + + if (m_pNativePipeline != NULL) + { + m_pNativePipeline->Delete(); + m_pNativePipeline = NULL; + } + + m_sendToWin32EventThreadMutex.Destroy(); +} + + +// +// Init sets up all the objects that the thread will need to run. +// +HRESULT CordbWin32EventThread::Init() +{ + if (m_cordb == NULL) + return E_INVALIDARG; + + m_sendToWin32EventThreadMutex.Init("Win32-Send lock", RSLock::cLockFlat, RSLock::LL_WIN32_SEND_LOCK); + + m_threadControlEvent = WszCreateEvent(NULL, FALSE, FALSE, NULL); + if (m_threadControlEvent == NULL) + return HRESULT_FROM_GetLastError(); + + m_actionTakenEvent = WszCreateEvent(NULL, FALSE, FALSE, NULL); + if (m_actionTakenEvent == NULL) + return HRESULT_FROM_GetLastError(); + + m_pNativePipeline = NewPipelineWithDebugChecks(); + if (m_pNativePipeline == NULL) + { + return E_OUTOFMEMORY; + } + + return S_OK; +} + +// +// Main function of the Win32 Event Thread +// +void CordbWin32EventThread::ThreadProc() +{ +#if defined(RSCONTRACTS) + DbgRSThread::GetThread()->SetThreadType(DbgRSThread::cW32ET); + + // The win32 ET conceptually holds a lock (all threads do). + DbgRSThread::GetThread()->TakeVirtualLock(RSLock::LL_WIN32_EVENT_THREAD); +#endif + + // In V2, the debuggee decides what to do if the debugger rudely exits / detaches. (This is + // handled by host policy). With the OS native-debuggging pipeline, the debugger by default + // kills the debuggee if it exits. To emulate V2 behavior, we need to override that default. + BOOL fOk = m_pNativePipeline->DebugSetProcessKillOnExit(FALSE); + (void)fOk; //prevent "unused variable" error from GCC + _ASSERTE(fOk); + + + // Run the top-level event loop. + Win32EventLoop(); + +#if defined(RSCONTRACTS) + // The win32 ET conceptually holds a lock (all threads do). + DbgRSThread::GetThread()->ReleaseVirtualLock(RSLock::LL_WIN32_EVENT_THREAD); +#endif +} + +// Define a holder that calls code:DeleteIPCEventHelper +NEW_WRAPPER_TEMPLATE1(DeleteIPCEventHolderHelper, DeleteIPCEventHelper); +typedef DeleteIPCEventHolderHelper<DebuggerIPCEvent> DeleteIPCEventHolder; + +//--------------------------------------------------------------------------------------- +// +// Helper to clean up IPCEvent before deleting it. +// This must be called after an event is marshalled via code:CordbProcess::MarshalManagedEvent +// +// Arguments: +// pManagedEvent - managed event to delete. +// +// Notes: +// This can delete a partially marshalled event. +// +void DeleteIPCEventHelper(DebuggerIPCEvent *pManagedEvent) +{ + CONTRACTL + { + // This is backout code that shouldn't need to throw. + NOTHROW; + } + CONTRACTL_END; + if (pManagedEvent == NULL) + { + return; + } + switch (pManagedEvent->type & DB_IPCE_TYPE_MASK) + { + // so far only this event need to cleanup. + case DB_IPCE_MDA_NOTIFICATION: + pManagedEvent->MDANotification.szName.CleanUp(); + pManagedEvent->MDANotification.szDescription.CleanUp(); + pManagedEvent->MDANotification.szXml.CleanUp(); + break; + + case DB_IPCE_FIRST_LOG_MESSAGE: + pManagedEvent->FirstLogMessage.szContent.CleanUp(); + break; + + default: + break; + } + delete [] (BYTE *)pManagedEvent; +} + +//--------------------------------------------------------------------------------------- +// Handle a CLR specific notification event. +// +// Arguments: +// pManagedEvent - non-null pointer to a managed event. +// pLockHolder - hold to process lock that gets toggled if this dispatches an event. +// pCallback - callback to dispatch potential managed events. +// +// Return Value: +// Throws on error. +// +// Assumptions: +// Target is stopped. Record was already determined to be a CLR event. +// +// Notes: +// This is called after caller does WaitForDebugEvent. +// Any exception this Filter does not recognize is treated as kNotClr. +// Currently, this includes both managed-exceptions and unmanaged ones. +// For interop-debugging, the interop logic will handle all kNotClr and triage if +// it's really a non-CLR exception. +// +//--------------------------------------------------------------------------------------- +void CordbProcess::FilterClrNotification( + DebuggerIPCEvent * pManagedEvent, + RSLockHolder * pLockHolder, + ICorDebugManagedCallback * pCallback) +{ + CONTRACTL + { + THROWS; + PRECONDITION(CheckPointer(pManagedEvent)); + PRECONDITION(CheckPointer(pCallback)); + PRECONDITION(ThreadHoldsProcessLock()); + } + CONTRACTL_END; + + // There are 3 types of events from the LS: + // 1) Replies (eg, corresponding to WaitForIPCEvent) + // we need to set LSEA/wait on LSER. + // 2) Sync-Complete (kind of like a special notification) + // Ping the helper + // 3) Notifications (eg, Module-load): + // these are dispatched immediately. + // 4) Left-side Startup event + + + // IF we're synced, then we must be getting a "Reply". + bool fReply = this->GetSynchronized(); + + LOG((LF_CORDB, LL_INFO10000, "CP::FCN - Received event %s; fReply: %d\n", + IPCENames::GetName(pManagedEvent->type), + fReply)); + + if (fReply) + { + // + _ASSERTE(m_pShim != NULL); + // + // Case 1: Reply + // + + pLockHolder->Release(); + _ASSERTE(!ThreadHoldsProcessLock()); + + // Save the IPC event and wake up the thread which is waiting for it from the LS. + GetEventChannel()->SaveEventFromLeftSide(pManagedEvent); + SetEvent(this->m_leftSideEventAvailable); + + // Some other thread called code:CordbRCEventThread::WaitForIPCEventFromProcess, and + // that will respond here and set the event. + + DWORD dwResult = WaitForSingleObject(this->m_leftSideEventRead, CordbGetWaitTimeout()); + pLockHolder->Acquire(); + if (dwResult != WAIT_OBJECT_0) + { + // The wait failed. This is probably WAIT_TIMEOUT which suggests a deadlock/assert on + // the RCEventThread. + CONSISTENCY_CHECK_MSGF(false, ("WaitForSingleObject failed: %d", dwResult)); + ThrowHR(CORDBG_E_TIMEOUT); + } + } + else + { + if (pManagedEvent->type == DB_IPCE_LEFTSIDE_STARTUP) + { + // + // Case 4: Left-side startup event. We'll mark that we're attached from oop. + // + + // Now that LS is started, we should definitely be able to instantiate DAC. + InitializeDac(); + + // @dbgtodo 'attach-bit': we don't want the debugger automatically invading the process. + GetDAC()->MarkDebuggerAttached(TRUE); + } + else if (pManagedEvent->type == DB_IPCE_SYNC_COMPLETE) + { + // Since V3 doesn't request syncs, it shouldn't get sync-complete. + // @dbgtodo sync: this changes when V3 can explicitly request an AsyncBreak. + _ASSERTE(m_pShim != NULL); + + // + // Case 2: Sync Complete + // + + HandleSyncCompleteRecieved(); + } + else + { + // + // Case 3: Notification. This will dispatch the event immediately. + // + + // Toggles the process-lock if it dispatches callbacks. + HandleRCEvent(pManagedEvent, pLockHolder, pCallback); + + } // end Notification + } +} + + + +// +// If the thread has an unhandled managed exception, hijack it. +// +// Arguments: +// dwThreadId - OS Thread id. +// +// Returns: +// True if hijacked; false if not. +// +// Notes: +// This is called from shim to emulate being synchronized at an unhandled +// exception. +// Other ICorDebug operations could calls this (eg, func-eval at 2nd chance). +BOOL CordbProcess::HijackThreadForUnhandledExceptionIfNeeded(DWORD dwThreadId) +{ + PUBLIC_API_ENTRY(this); // from Shim + + BOOL fHijacked = FALSE; + HRESULT hr = S_OK; + EX_TRY + { + RSLockHolder lockHolder(GetProcessLock()); + + // OS will not execute the Unhandled Exception Filter under native debugger, so + // we need to hijack the thread to get it to execute the UEF, which will then do + // work for unhandled managed exceptions. + CordbThread * pThread = TryLookupOrCreateThreadByVolatileOSId(dwThreadId); + if (pThread != NULL) + { + // If the thread has a managed exception, then we should have a pThread object. + + if (pThread->HasUnhandledNativeException()) + { + _ASSERTE(pThread->IsThreadExceptionManaged()); // should have been marked earlier + + pThread->HijackForUnhandledException(); + fHijacked = TRUE; + } + } + } + EX_CATCH_HRESULT(hr); + SIMPLIFYING_ASSUMPTION(SUCCEEDED(hr)); + + return fHijacked; +} + +//--------------------------------------------------------------------------------------- +// Validate the given exception record or throw. +// +// Arguments: +// pRawRecord - non-null raw bytes of the exception +// countBytes - number of bytes in pRawRecord buffer. +// format - format of pRawRecord +// +// Returns: +// A type-safe exception record from the raw buffer. +// +// Notes: +// This is a helper for code:CordbProcess::Filter. +// This can do consistency checks on the incoming parameters such as: +// * verify countBytes matches the expected size for the given format. +// * verify the format is supported. +// +// If we let a given ICD understand multiple formats (eg, have x86 understand both Exr32 and +// Exr64), this would be the spot to allow the conversion. +// +const EXCEPTION_RECORD * CordbProcess::ValidateExceptionRecord( + const BYTE pRawRecord[], + DWORD countBytes, + CorDebugRecordFormat format) +{ + ValidateOrThrow(pRawRecord); + + // + // Check format against expected platform. + // + + // @dbgtodo - , cross-plat: Once we do cross-plat, these should be based off target-architecture not host's. +#if defined(_WIN64) + if (format != FORMAT_WINDOWS_EXCEPTIONRECORD64) + { + ThrowHR(E_INVALIDARG); + } +#else + if (format != FORMAT_WINDOWS_EXCEPTIONRECORD32) + { + ThrowHR(E_INVALIDARG); + } +#endif + + // @dbgtodo cross-plat: once we do cross-plat, need to use correct EXCEPTION_RECORD variant. + if (countBytes != sizeof(EXCEPTION_RECORD)) + { + ThrowHR(E_INVALIDARG); + } + + + const EXCEPTION_RECORD * pRecord = reinterpret_cast<const EXCEPTION_RECORD *> (pRawRecord); + + return pRecord; +}; + +// Return value: S_OK or indication that no more room exists for enabled types +HRESULT CordbProcess::SetEnableCustomNotification(ICorDebugClass * pClass, BOOL fEnable) +{ + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this); // takes the lock + + ValidateOrThrow(pClass); + + ((CordbClass *)pClass)->SetCustomNotifications(fEnable); + + PUBLIC_API_END(hr); + return hr; +} // CordbProcess::SetEnableCustomNotification + +//--------------------------------------------------------------------------------------- +// Public implementation of ICDProcess4::Filter +// +// Arguments: +// pRawRecord - non-null raw bytes of the exception +// countBytes - number of bytes in pRawRecord buffer. +// format - format of pRawRecord +// dwFlags - flags providing auxillary info for exception record. +// dwThreadId - thread that exception occurred on. +// pCallback - callback to dispatch potential managed events on. +// pContinueStatus - Continuation status for exception. This dictates what +// to pass to kernel32!ContinueDebugEvent(). +// +// Return Value: +// S_OK on success. +// +// Assumptions: +// Target is stopped. +// +// Notes: +// The exception could be anything, including: +// - a CLR notification, +// - a random managed exception (both from managed code or the runtime), +// - a non-CLR exception +// +// This is cross-platform. The {pRawRecord, countBytes, format} describe events +// on an arbitrary target architecture. On windows, this will be an EXCEPTION_RECORD. +// +HRESULT CordbProcess::Filter( + const BYTE pRawRecord[], + DWORD countBytes, + CorDebugRecordFormat format, + DWORD dwFlags, + DWORD dwThreadId, + ICorDebugManagedCallback * pCallback, + DWORD * pContinueStatus +) +{ + HRESULT hr = S_OK; + PUBLIC_API_BEGIN(this); // takes the lock + { + // + // Validate parameters + // + + // If we don't care about the continue status, we leave it untouched. + ValidateOrThrow(pContinueStatus); + ValidateOrThrow(pCallback); + + const EXCEPTION_RECORD * pRecord = ValidateExceptionRecord(pRawRecord, countBytes, format); + + DWORD dwFirstChance = (dwFlags & IS_FIRST_CHANCE); + + // + // Deal with 2nd-chance exceptions. Don't actually hijack now (that's too invasive), + // but mark that we have the exception in case a future operation (eg, func-eval) needs to hijack. + // + if (!dwFirstChance) + { + CordbThread * pThread = TryLookupOrCreateThreadByVolatileOSId(dwThreadId); + + // If we don't have a managed-thread object, then it certainly can't have a throwable. + // It's possible this is still an exception from the native portion of the runtime, + // but that's ok, we'll just treat it as a native exception. + // This could be expensive, don't want to do it often... (definitely not on every Filter). + + + // OS will not execute the Unhandled Exception Filter under native debugger, so + // we need to hijack the thread to get it to execute the UEF, which will then do + // work for unhandled managed exceptions. + if ((pThread != NULL) && pThread->IsThreadExceptionManaged()) + { + // Copy exception record for future use in case we decide to hijack. + pThread->SetUnhandledNativeException(pRecord); + } + // we don't care about 2nd-chance exceptions, unless we decide to hijack it later. + } + + // + // Deal with CLR notifications + // + else if (pRecord->ExceptionCode == CLRDBG_NOTIFICATION_EXCEPTION_CODE) // Special event code + { + // + // This may not be for us, or we may not have a managed thread object: + // 1. Anybody can raise an exception with this exception code, so can't assume this belongs to us yet. + // 2. Notifications may come on unmanaged threads if they're coming from MDAs or CLR internal events + // fired before the thread is created. + // + BYTE * pManagedEventBuffer = new BYTE[CorDBIPC_BUFFER_SIZE]; + DeleteIPCEventHolder pManagedEvent(reinterpret_cast<DebuggerIPCEvent *>(pManagedEventBuffer)); + + bool fOwner = CopyManagedEventFromTarget(pRecord, pManagedEvent); + if (fOwner) + { + // This toggles the lock if it dispatches callbacks + FilterClrNotification(pManagedEvent, GET_PUBLIC_LOCK_HOLDER(), pCallback); + + // Cancel any notification events from target. These are just supposed to notify ICD and not + // actually be real exceptions in the target. + // Canceling here also prevents a VectoredExceptionHandler in the target from picking + // up exceptions for the CLR. + *pContinueStatus = DBG_CONTINUE; + } + + // holder will invoke DeleteIPCEventHelper(pManagedEvent). + } + + } + PUBLIC_API_END(hr); + // we may not find the correct mscordacwks so fail gracefully + _ASSERTE(SUCCEEDED(hr) || (hr != HRESULT_FROM_WIN32(ERROR_MOD_NOT_FOUND))); + + return hr; +} + +//--------------------------------------------------------------------------------------- +// Wrapper to invoke ICorDebugMutableDataTarget::ContinueStatusChanged +// +// Arguments: +// dwContinueStatus - new continue status +// +// Returns: +// None. Throw on error. +// +// Notes: +// Initial continue status is returned from code:CordbProcess::Filter. +// Some operations (mainly hijacking on a 2nd-chance exception), may need to +// override that continue status. +// ICorDebug operations invoke a callback on the data-target to notify the debugger +// of a change in status. Debugger may fail the request. +// +void CordbProcess::ContinueStatusChanged(DWORD dwThreadId, CORDB_CONTINUE_STATUS dwContinueStatus) +{ + HRESULT hr = m_pMutableDataTarget->ContinueStatusChanged(dwThreadId, dwContinueStatus); + IfFailThrow(hr); +} + +//--------------------------------------------------------------------------------------- +// Request a synchronization to occur after a debug event is dispatched. +// +// Note: +// This is called in response to a managed debug event, and so we know that we have +// a worker thread in the process (the one that just sent the event!) +// This can not be called asynchronously. +//--------------------------------------------------------------------------------------- +void CordbProcess::RequestSyncAtEvent() +{ + GetDAC()->RequestSyncAtEvent(); +} + +//--------------------------------------------------------------------------------------- +// +// Primary loop of the Win32 debug event thread. +// +// +// Arguments: +// None. +// +// Return Value: +// None. +// +// Notes: +// This is it, you've found it, the main guy. This function loops as long as the +// debugger is around calling the OS WaitForDebugEvent() API. It takes the OS Debug +// Event and filters it thru the right-side, continuing the process if not recognized. +// +// @dbgtodo shim: this will become part of the shim. +//--------------------------------------------------------------------------------------- +void CordbWin32EventThread::Win32EventLoop() +{ + // This must be called from the win32 event thread. + _ASSERTE(IsWin32EventThread()); + + LOG((LF_CORDB, LL_INFO1000, "W32ET::W32EL: entered win32 event loop\n")); + + + DEBUG_EVENT event; + + // Allow the timeout for WFDE to be adjustable. Default to 25 ms based off perf numbers (see issue VSWhidbey 132368). + DWORD dwWFDETimeout = CLRConfig::GetConfigValue(CLRConfig::UNSUPPORTED_DbgWFDETimeout); + + while (m_run) + { + BOOL fEventAvailable = FALSE; + + // We should not have any locks right now. + + + // Have to wait on 2 sources: + // WaitForMultipleObjects - ping for messages (create, attach, Continue, detach) and also + // process exits in the managed-only case. + // Native Debug Events - This is a huge perf hit so we want to avoid it whenever we can. + // Only wait on these if we're interop debugging and if the process is not frozen. + // A frozen process can't send any debug events, so don't bother looking for them. + + + unsigned int cWaitCount = 1; + + HANDLE rghWaitSet[2]; + + rghWaitSet[0] = m_threadControlEvent; + + DWORD dwWaitTimeout = INFINITE; + + if (m_pProcess != NULL) + { + // Process is always built on Native debugging pipeline, so it needs to always be prepared to call WFDE + // As an optimization, if the target is stopped, then we can avoid calling WFDE. + { +#ifndef FEATURE_INTEROP_DEBUGGING + // Managed-only, never win32 stopped, so always check for an event. + dwWaitTimeout = 0; + fEventAvailable = m_pNativePipeline->WaitForDebugEvent(&event, dwWFDETimeout, m_pProcess); +#else + // Wait for a Win32 debug event from any processes that we may be attached to as the Win32 debugger. + const bool fIsWin32Stopped = (m_pProcess->m_state & CordbProcess::PS_WIN32_STOPPED) != 0; + const bool fSkipWFDE = fIsWin32Stopped; + + + const bool fIsInteropDebugging = m_pProcess->IsInteropDebugging(); + (void)fIsInteropDebugging; //prevent "unused variable" error from GCC + + // Assert checks + _ASSERTE(fIsInteropDebugging == m_pShim->IsInteropDebugging()); + + if (!fSkipWFDE) + { + dwWaitTimeout = 0; + fEventAvailable = m_pNativePipeline->WaitForDebugEvent(&event, dwWFDETimeout, m_pProcess); + } + else + { + // If we're managed-only debugging, then the process should always be running, + // which means we always need to be calling WFDE to pump potential debug events. + // If we're interop-debugging, then the process can be stopped at a native-debug event, + // in which case we don't have to call WFDE until we resume it again. + // So we can only skip the WFDE when we're interop-debugging. + _ASSERTE(fIsInteropDebugging); + } +#endif // FEATURE_INTEROP_DEBUGGING + } + + + } // end m_pProcess != NULL + +#if defined(FEATURE_INTEROP_DEBUGGING) + // While interop-debugging, the process may get killed rudely underneath us, even if we haven't + // continued the last debug event. In such cases, The process object will get signalled normally. + // If we didn't just get a native-exitProcess event, then listen on the process handle for exit. + // (this includes all managed-only debugging) + // It's very important to establish this before we go into the WaitForMutlipleObjects below + // because the debuggee may exit while we're sitting in that loop (waiting for the debugger to call Continue). + bool fDidNotJustGetExitProcessEvent = !fEventAvailable || (event.dwDebugEventCode != EXIT_PROCESS_DEBUG_EVENT); +#else + // In non-interop scenarios, we'll never get any native debug events, let alone an ExitProcess native event. + bool fDidNotJustGetExitProcessEvent = true; +#endif // FEATURE_INTEROP_DEBUGGING + + + // The m_pProcess handle will get nulled out after we process the ExitProcess event, and + // that will ensure that we only wait for an Exit event once. + if ((m_pProcess != NULL) && fDidNotJustGetExitProcessEvent) + { + rghWaitSet[1] = m_pProcess->UnsafeGetProcessHandle(); + cWaitCount = 2; + } + + // See if any process that we aren't attached to as the Win32 debugger have exited. (Note: this is a + // polling action if we are also waiting for Win32 debugger events. We're also looking at the thread + // control event here, too, to see if we're supposed to do something, like attach. + DWORD dwStatus = WaitForMultipleObjectsEx(cWaitCount, rghWaitSet, FALSE, dwWaitTimeout, FALSE); + + _ASSERTE((dwStatus == WAIT_TIMEOUT) || (dwStatus < cWaitCount)); + + if (!m_run) + { + _ASSERTE(m_action == W32ETA_NONE); + break; + } + + LOG((LF_CORDB, LL_INFO100000, "W32ET::W32EL - got event , ret=%d, has w32 dbg event=%d\n", + dwStatus, fEventAvailable)); + + // If we haven't timed out, or if it wasn't the thread control event + // that was set, then a process has + // exited... + if ((dwStatus != WAIT_TIMEOUT) && (dwStatus != WAIT_OBJECT_0)) + { + // Grab the process that exited. + _ASSERTE((dwStatus - WAIT_OBJECT_0) == 1); + ExitProcess(false); // not detach + fEventAvailable = false; + } + // Should we create a process? + else if (m_action == W32ETA_CREATE_PROCESS) + { + CreateProcess(); + } + // Should we attach to a process? + else if (m_action == W32ETA_ATTACH_PROCESS) + { + AttachProcess(); + } + // Should we detach from a process? + else if (m_action == W32ETA_DETACH) + { + ExitProcess(true); // detach case + + // Once we detach, we don't need to continue any outstanding event. + // So act like we never got the event. + fEventAvailable = false; + PREFIX_ASSUME(m_pProcess == NULL); // W32 cleared process pointer + } + +#ifdef FEATURE_INTEROP_DEBUGGING + // Should we continue the process? + else if (m_action == W32ETA_CONTINUE) + { + HandleUnmanagedContinue(); + } +#endif // FEATURE_INTEROP_DEBUGGING + + // We don't need to sweep the FCH threads since we never hijack a thread in cooperative mode. + + + // Only process an event if one is available. + if (!fEventAvailable) + { + continue; + } + + // The only ref we have is the one in the ProcessList hash; + // If we dispatch an ExitProcess event, we may even lose that. + // But since the CordbProcess is our parent object, we know it won't go away until + // it neuters us, so we can safely proceed. + // Find the process this event is for. + PREFIX_ASSUME(m_pProcess != NULL); + _ASSERTE(m_pProcess->m_id == GetProcessId(&event)); // should only get events for our proc + g_pRSDebuggingInfo->m_MRUprocess = m_pProcess; + + // Must flush the dac cache since we were just running. + m_pProcess->ForceDacFlush(); + + // So we've filtered out CLR events. + // Let the shim handle the remaining events. This will call back into Filter() if appropriate. + // This will also ensure the debug event gets continued. + HRESULT hrShim = S_OK; + { + PUBLIC_CALLBACK_IN_THIS_SCOPE0_NO_LOCK(NULL); + hrShim = m_pShim->HandleWin32DebugEvent(&event); + } + // Any errors from the shim (eg. failure to load DAC) are unrecoverable + SetUnrecoverableIfFailed(m_pProcess, hrShim); + + } // loop + + LOG((LF_CORDB, LL_INFO1000, "W32ET::W32EL: exiting event loop\n")); + + return; +} + +//--------------------------------------------------------------------------------------- +// +// Returns if the current thread is the win32 thread. +// +// Return Value: +// true iff this is the win32 event thread. +// +//--------------------------------------------------------------------------------------- +bool CordbProcess::IsWin32EventThread() +{ + _ASSERTE((m_pShim != NULL) || !"Don't check win32 event thread in V3 cases"); + return m_pShim->IsWin32EventThread(); +} + +//--------------------------------------------------------------------------------------- +// Call when the sync complete event is received and can be processed. +// +// Notes: +// This is called when the RS gets the sync-complete from the LS and can process it. +// +// This has a somewhat elaborate contract to fill between Interop-debugging, Async-Break, draining the +// managed event-queue, and coordinating with the dispatch thread (RCET). +// +// @dbgtodo - this should eventually get hoisted into the shim. +void CordbProcess::HandleSyncCompleteRecieved() +{ + _ASSERTE(ThreadHoldsProcessLock()); + + this->SetSyncCompleteRecv(true); + + // If some thread is waiting for the process to sync, notify that it can go now. + if (this->m_stopRequested) + { + this->SetSynchronized(true); + SetEvent(this->m_stopWaitEvent); + } + else + { + // Note: we set the m_stopWaitEvent all the time and leave it high while we're stopped. This + // must be done after we've checked m_stopRequested. + SetEvent(this->m_stopWaitEvent); + + // Otherwise, simply mark that the state of the process has changed and let the + // managed event dispatch logic take over. + // + // Note: process->m_synchronized remains false, which indicates to the RC event + // thread that it can dispatch the next managed event. + m_cordb->ProcessStateChanged(); + } +} + + +#ifdef FEATURE_INTEROP_DEBUGGING + +// Get a Thread's _user_ starting address (the real starting address may be some +// OS shim.) +// This may return NULL for the Async-Break thread. +void* GetThreadUserStartAddr(const DEBUG_EVENT* pCreateThreadEvent) +{ + // On Win7 and above, we can trust the lpStartAddress field of the CREATE_THREAD_DEBUG_EVENT + // to be the user start address (the actual OS start address is an implementation detail that + // doesn't need to be exposed to users). Note that we are assuming that the target process + // is running on Win7 if mscordbi is. If we ever have some remoting scenario where the target + // can run on a different windows machine with a different OS version we will need a way to + // determine the target's OS version + if(RunningOnWin7()) + { + return pCreateThreadEvent->u.CreateThread.lpStartAddress; + } + + // On pre-Win7 OSes, we rely on an OS implementation detail to get the real user thread start: + // it exists in EAX at thread start time. + // Note that for a brief period of time there was a GetThreadStartInformation API in Longhorn + // we could use for this, but it was removed during the Longhorn reset. + HANDLE hThread = pCreateThreadEvent->u.CreateThread.hThread; +#if defined(DBG_TARGET_X86) + // Grab the thread's context. + DT_CONTEXT c; + c.ContextFlags = DT_CONTEXT_FULL; + BOOL succ = DbiGetThreadContext(hThread, &c); + + if (succ) + { + return (void*) c.Eax; + } +#elif defined(DBG_TARGET_AMD64) + DT_CONTEXT c; + c.ContextFlags = DT_CONTEXT_FULL; + BOOL succ = DbiGetThreadContext(hThread, &c); + + if (succ) + { + return (void*) c.Rcx; + } +#else + PORTABILITY_ASSERT("port GetThreadUserStartAddr"); +#endif + + return NULL; +} + + +//--------------------------------------------------------------------------------------- +// +// Get (create if needed) the unmanaged thread for an unmanaged debug event. +// +// Arguments: +// event - native debug event. +// +// Return Value: +// Unmanaged thread corresponding to the native debug event. +// +// +// Notes: +// Thread may be newly allocated, or may be existing. CordbProcess holds +// list of all CordbUnmanagedThreads, and will handle freeing memory. +// +//--------------------------------------------------------------------------------------- +CordbUnmanagedThread * CordbProcess::GetUnmanagedThreadFromEvent(const DEBUG_EVENT * pEvent) +{ + _ASSERTE(ThreadHoldsProcessLock()); + HRESULT hr; + + CordbUnmanagedThread * pUnmanagedThread = NULL; + + // Remember newly created threads. + if (pEvent->dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) + { + // We absolutely should have an unmanaged callback by this point. + // That means that the client debugger should have called ICorDebug::SetUnmanagedHandler by now. + // However, we can't actually enforce that (see comment in ICorDebug::SetUnmanagedHandler for details), + // so we do a runtime check to check this. + // This is an extremely gross API misuse and an issue in the client if the callback is not set yet. + // Without the unmanaged callback, we absolutely can't do interop-debugging. We assert (checked builds) and + // dispatch unrecoverable error (retail builds) to avoid an AV. + + + if (this->m_cordb->m_unmanagedCallback == NULL) + { + CONSISTENCY_CHECK_MSGF((this->m_cordb->m_unmanagedCallback != NULL), + ("GROSS API misuse!!\nNo unmanaged callback set by the time we've received CreateProcess debug event for proces 0x%x.\n", + pEvent->dwProcessId)); + + CORDBSetUnrecoverableError(this, CORDBG_E_INTEROP_NOT_SUPPORTED, 0); + + // Returning NULL will tell caller not to dispatch event to client. We have no callback object to dispatch upon. + return NULL; + } + + pUnmanagedThread = this->HandleUnmanagedCreateThread(pEvent->dwThreadId, + pEvent->u.CreateProcessInfo.hThread, + pEvent->u.CreateProcessInfo.lpThreadLocalBase); + + // Managed-attach won't start until after Cordbg continues from the loader-bp. + } + else if (pEvent->dwDebugEventCode == CREATE_THREAD_DEBUG_EVENT) + { + pUnmanagedThread = this->HandleUnmanagedCreateThread(pEvent->dwThreadId, + pEvent->u.CreateThread.hThread, + pEvent->u.CreateThread.lpThreadLocalBase); + + BOOL fBlockExists = FALSE; + hr = S_OK; + EX_TRY + { + // See if we have the debugger control block yet... + + this->GetEventBlock(&fBlockExists); + + // If we have the debugger control block, and if that control block has the address of the thread proc for + // the helper thread, then we're initialized enough on the Left Side to recgonize the helper thread based on + // its thread proc's address. + if (this->GetDCB() != NULL) + { + // get the latest LS DCB information + UpdateRightSideDCB(); + if ((this->GetDCB()->m_helperThreadStartAddr != NULL) && (pUnmanagedThread != NULL)) + { + void * pStartAddr = GetThreadUserStartAddr(pEvent); + + if (pStartAddr == this->GetDCB()->m_helperThreadStartAddr) + { + // Remember the ID of the helper thread. + this->m_helperThreadId = pEvent->dwThreadId; + + LOG((LF_CORDB, LL_INFO1000, "W32ET::W32EL: Left Side Helper Thread is 0x%x\n", pEvent->dwThreadId)); + } + } + } + } + EX_CATCH_HRESULT(hr) + { + if (fBlockExists && FAILED(hr)) + { + _ASSERTE(IsLegalFatalError(hr)); + // Send up the DebuggerError event + this->UnrecoverableError(hr, 0, NULL, 0); + + // Kill the process. + // RS will pump events until we LS process exits. + TerminateProcess(this->m_handle, hr); + + return pUnmanagedThread; + } + } + } + else + { + // Find the unmanaged thread that this event is for. + pUnmanagedThread = this->GetUnmanagedThread(pEvent->dwThreadId); + } + + return pUnmanagedThread; +} + +//--------------------------------------------------------------------------------------- +// +// Handle a native-debug event representing a managed sync-complete event. +// +// +// Return Value: +// Reaction telling caller how to respond to the native-debug event. +// +// Assumptions: +// Called within the Triage process after receiving a native-debug event. +// +//--------------------------------------------------------------------------------------- +Reaction CordbProcess::TriageSyncComplete() +{ + _ASSERTE(ThreadHoldsProcessLock()); + + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::TSC: received 'sync complete' flare.\n"); + + _ASSERTE(IsInteropDebugging()); + + // Note: we really don't need to be suspending Runtime threads that we know have tripped + // here. If we ever end up with a nice, quick way to know that about each unmanaged thread, then + // we should put that to good use here. + this->SuspendUnmanagedThreads(); + + this->HandleSyncCompleteRecieved(); + + // Let the process run free. + return REACTION(cIgnore); + + // At this point, all managed threads are stopped at safe places and all unmanaged + // threads are either suspended or hijacked. All stopped managed threads are also hard + // suspended (due to the call to SuspendUnmanagedThreads above) except for the thread + // that sent the sync complete flare. + + // We've handled this exception, so skip all further processing. + UNREACHABLE(); +} + +//----------------------------------------------------------------------------- +// Triage a breakpoint (non-flare) on a "normal" thread. +//----------------------------------------------------------------------------- +Reaction CordbProcess::TriageBreakpoint(CordbUnmanagedThread * pUnmanagedThread, const DEBUG_EVENT * pEvent) +{ + _ASSERTE(ThreadHoldsProcessLock()); + + HRESULT hr = S_OK; + + DWORD dwExCode = pEvent->u.Exception.ExceptionRecord.ExceptionCode; + const void * pExAddress = pEvent->u.Exception.ExceptionRecord.ExceptionAddress; + + _ASSERTE(dwExCode == STATUS_BREAKPOINT); + + // There are three cases here: + // + // 1. The breakpoint definetly belongs to the Runtime. (I.e., a BP in our patch table that + // is in managed code.) In this case, we continue the process with + // DBG_EXCEPTION_NOT_HANDLED, which lets the in-process exception logic kick in as if we + // weren't here. + // + // 2. The breakpoint is definetly not ours. (I.e., a BP that is not in our patch table.) We + // pass these up as regular exception events, doing the can't stop check as usual. + // + // 3. We're not sure. (I.e., a BP in our patch table, but set in unmangaed code.) In this + // case, we hijack as usual, also with can't stop check as usual. + + bool fPatchFound = false; + bool fPatchIsUnmanaged = false; + + hr = this->FindPatchByAddress(PTR_TO_CORDB_ADDRESS(pExAddress), + &fPatchFound, + &fPatchIsUnmanaged); + + if (SUCCEEDED(hr)) + { + if (fPatchFound) + { +#ifdef _DEBUG + // What if managed & native patch the same address? That could happen on a step out M --> U. + { + NativePatch * pNativePatch = GetNativePatch(pExAddress); + SIMPLIFYING_ASSUMPTION_MSGF(pNativePatch == NULL, ("Have Managed & native patch at 0x%p", pExAddress)); + } +#endif + + // BP could be ours... if its unmanaged, then we still need to hijack, so fall + // through to that logic. Otherwise, its ours. + if (!fPatchIsUnmanaged) + { + LOG((LF_CORDB, LL_INFO1000, "W32ET::W32EL: breakpoint exception " + "belongs to runtime due to patch table match.\n")); + + return REACTION(cCLR); + } + else + { + LOG((LF_CORDB, LL_INFO1000, "W32ET::W32EL: breakpoint exception " + "matched in patch table, but its unmanaged so might hijack anyway.\n")); + + // If we're in cooperative mode, then we must have a inproc handler, and don't need to hijack + // One way this can happen is the patch placed for a func-eval complete is hit in coop-mode. + if (pUnmanagedThread->GetEEPGCDisabled()) + { + LOG((LF_CORDB, LL_INFO10000, "Already in coop-mode, don't need to hijack\n")); + return REACTION(cCLR); + } + else + { + return REACTION(cBreakpointRequiringHijack); + } + } + + UNREACHABLE(); + } + else // Patch not found + { + // If we're here, then we have a BP that's not in the managed patch table, and not + // in the native patch list. This should be rare. Perhaps an int3 / DebugBreak() / Assert in + // the native code stream. + // Anyway, we don't know about this patch so we can't skip it. The only thing we can do + // is chuck it up to Cordbg and hope they can help us. Note that this is the same case + // we were in w. V1. + + // BP doesn't belong to CLR ... so dispatch it to Cordbg as either make it IB or OOB. + // @todo - make the runtime 1 giant Can't stop region. + bool fCantStop = pUnmanagedThread->IsCantStop(); + +#ifdef _DEBUG + // We rarely expect a raw int3 here. Add a debug check that will assert. + // Tests that know they don't have raw int3 can enable this regkey to get + // extra coverage. + static DWORD s_fBreakOnRawInt3 = -1; + + if (s_fBreakOnRawInt3 == -1) + s_fBreakOnRawInt3 = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_DbgBreakOnRawInt3); + + if (s_fBreakOnRawInt3) + { + CONSISTENCY_CHECK_MSGF(false, ("Unexpected Raw int3 at:%p on tid 0x%x (%d). CantStop=%d." + "This assert is used by specific tests to get extra checks." + "For normal cases it's ignorable and is enabled by setting DbgBreakOnRawInt3==1.", + pExAddress, pEvent->dwThreadId, pEvent->dwThreadId, fCantStop)); + } +#endif + + if (fCantStop) + { + // If we're in a can't stop region, then its OOB no matter what at this point. + return REACTION(cOOB); + } + else + { + // PGC must be enabled if we're going to stop for an IB event. + bool PGCDisabled = pUnmanagedThread->GetEEPGCDisabled(); + _ASSERTE(!PGCDisabled); + + // Bp is definitely not ours, and PGC is not disabled, so in-band exception. + LOG((LF_CORDB, LL_INFO1000, "W32ET::W32EL: breakpoint exception " + "does not belong to the runtime due to failed patch table match.\n")); + + return REACTION(cInband); + } + + UNREACHABLE(); + } + + UNREACHABLE(); + } + else + { + // Patch table lookup failed? Only on OOM or if ReadProcessMemory fails... + _ASSERTE(!"Patch table lookup failed!"); + CORDBSetUnrecoverableError(this, hr, 0); + return REACTION(cOOB); + } + + UNREACHABLE(); +} + +//--------------------------------------------------------------------------------------- +// +// Triage a "normal" 1st chance exception on a "normal" thread. +// Not hijacked, not the helper thread, not a flare, etc.. This is the common +// case for a native exception from native code. +// +// Arguments: +// pUnmanagedThread - Pointer to the CordbUnmanagedThread object that we want to hijack. +// pEvent - Pointer to the debug event which contains the exception code and address. +// +// Return Value: +// The Reaction tells if the event is in-band, out-of-band, CLR specific or ignorable. +// +//--------------------------------------------------------------------------------------- +Reaction CordbProcess::Triage1stChanceNonSpecial(CordbUnmanagedThread * pUnmanagedThread, const DEBUG_EVENT * pEvent) +{ + _ASSERTE(ThreadHoldsProcessLock()); + CONTRACTL + { + THROWS; + } + CONTRACTL_END; + + HRESULT hr = S_OK; + + DWORD dwExCode = pEvent->u.Exception.ExceptionRecord.ExceptionCode; + const void * pExAddress = pEvent->u.Exception.ExceptionRecord.ExceptionAddress; + + // This had better not be a flare. If it is, that means we have some race that unmarked + // the hijacks. + _ASSERTE(!ExceptionIsFlare(dwExCode, pExAddress)); + + // Any first chance exception could belong to the Runtime, so long as the Runtime has actually been + // initialized. Here we'll setup a first-chance hijack for this thread so that it can give us the + // true answer that we need. + + // But none of those exceptions could possibly be ours unless we have a managed thread to go with + // this unmanaged thread. A non-NULL EEThreadPtr tells us that there is indeed a managed thread for + // this unmanaged thread, even if the Right Side hasn't received a managed ThreadCreate message yet. + REMOTE_PTR pEEThread; + hr = pUnmanagedThread->GetEEThreadPtr(&pEEThread); + _ASSERTE(SUCCEEDED(hr)); + + if (pEEThread == NULL) + { + // No managed thread, so it can't possibly belong to the runtime! + // But it may still be in a can't-stop region (think some goofy shutdown case). + if (pUnmanagedThread->IsCantStop()) + { + return REACTION(cOOB); + } + else + { + return REACTION(cInband); + } + } + + + + // We have to be careful here. A Runtime thread may be in a place where we cannot let an + // unmanaged exception stop it. For instance, an unmanaged user breakpoint set on + // WaitForSingleObject will prevent Runtime threads from sending events to the Right Side. So at + // various points below, we check to see if this Runtime thread is in a place were we can't let + // it stop, and if so then we jump over to the out-of-band dispatch logic and treat this + // exception as out-of-band. The debugger is supposed to continue from the out-of-band event + // properly and help us avoid this problem altogether. + + // Grab a few flags from the thread's state... + bool fThreadStepping = false; + bool fSpecialManagedException = false; + + pUnmanagedThread->GetEEState(&fThreadStepping, &fSpecialManagedException); + + // If we've got a single step exception, and if the Left Side has indicated that it was + // stepping the thread, then the exception is ours. + if (dwExCode == STATUS_SINGLE_STEP) + { + if (fThreadStepping) + { + // Yup, its the Left Side that was stepping the thread... + STRESS_LOG0(LF_CORDB, LL_INFO1000, "W32ET::W32EL: single step exception belongs to the runtime.\n"); + + return REACTION(cCLR); + } + + // Any single step that is triggered when the thread's state doesn't indicate that + // we were stepping the thread automatically gets passed out as an unmanged event. + STRESS_LOG0(LF_CORDB, LL_INFO1000, "W32ET::W32EL: single step exception " + "does not belong to the runtime.\n"); + + if (pUnmanagedThread->IsCantStop()) + { + return REACTION(cOOB); + } + else + { + return REACTION(cInband); + } + + UNREACHABLE(); + } + +#ifdef CorDB_Short_Circuit_First_Chance_Ownership + // If the runtime indicates that this is a special exception being thrown within the runtime, + // then its ours no matter what. + else if (fSpecialManagedException) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "W32ET::W32EL: exception belongs to the runtime due to " + "special managed exception marking.\n"); + + return REACTION(cCLR); + } + else if ((dwExCode == EXCEPTION_COMPLUS) || (dwExCode == EXCEPTION_HIJACK)) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, + "W32ET::W32EL: exception belongs to Runtime due to match on built in exception code\n"); + + return REACTION(cCLR); + } + else if (dwExCode == EXCEPTION_MSVC) + { + // The runtime may use C++ exceptions internally. We can still report these + // to the debugger as long as we're outside of a can't-stop region. + if (pUnmanagedThread->IsCantStop()) + { + return REACTION(cCLR); + } + else + { + return REACTION(cInband); + } + } + else if (dwExCode == STATUS_BREAKPOINT) + { + return TriageBreakpoint(pUnmanagedThread, pEvent); + }// end BP case +#endif + + // It's not a breakpoint or single-step. Now it just comes down to the address from where + // the exception is coming from. If it's managed, we give it back to the CLR. If it's + // from native, then we dispatch to Cordbg. + // We can use DAC to figure this out from Out-of-process. + _ASSERTE(dwExCode != STATUS_BREAKPOINT); // BP were already handled. + + + // Use DAC to decide if it's ours or not w/o going inproc. + CORDB_ADDRESS address = PTR_TO_CORDB_ADDRESS(pExAddress); + + IDacDbiInterface::AddressType addrType; + + addrType = GetDAC()->GetAddressType(address); + bool fIsCorCode =((addrType == IDacDbiInterface::kAddressManagedMethod) || + (addrType == IDacDbiInterface::kAddressRuntimeManagedCode) || + (addrType == IDacDbiInterface::kAddressRuntimeUnmanagedCode)); + + STRESS_LOG2(LF_CORDB, LL_INFO1000, "W32ET::W32EL: IsCorCode(0x%I64p)=%d\n", address, fIsCorCode); + + + if (fIsCorCode) + { + return REACTION(cCLR); + } + else + { + if (pUnmanagedThread->IsCantStop()) + { + return REACTION(cOOB); + } + else + { + return REACTION(cInband); + } + } + + UNREACHABLE(); +} + +//--------------------------------------------------------------------------------------- +// +// Triage a 1st-chance exception when the CLR is initialized. +// +// Arguments: +// pUnmanagedThread - thread that the event has occurred on. +// pEvent - native debug event for the exception that occurred that this is triaging. +// +// Return Value: +// Reaction for how to handle this event. +// +// Assumptions: +// Called when receiving a debug event when the process is stopped. +// +// Notes: +// A 1st-chance event has a wide spectrum of possibility including: +// - It may be unmanaged or managed. +// - Or it may be an execution control exception for managed-exceution +// - thread skipping an OOB event. +// +//--------------------------------------------------------------------------------------- +Reaction CordbProcess::TriageExcep1stChanceAndInit(CordbUnmanagedThread * pUnmanagedThread, + const DEBUG_EVENT * pEvent) +{ + _ASSERTE(ThreadHoldsProcessLock()); + _ASSERTE(m_runtimeOffsetsInitialized); + + NativePatch * pNativePatch = NULL; + DebuggerIPCRuntimeOffsets * pIPCRuntimeOffsets = &(this->m_runtimeOffsets); + + DWORD dwExCode = pEvent->u.Exception.ExceptionRecord.ExceptionCode; + const void * pExAddress = pEvent->u.Exception.ExceptionRecord.ExceptionAddress; + + +#ifdef _DEBUG + // Some Interop bugs involve threads that land at a crazy IP. Since we're interop-debugging, we can't + // attach a debugger to the LS. So we have some debug mode where we enable the SS flag and thus + // produce a trace of where a thread is going. + if (pUnmanagedThread->IsDEBUGTrace() && (dwExCode == STATUS_SINGLE_STEP)) + { + pUnmanagedThread->ClearState(CUTS_DEBUG_SingleStep); + LOG((LF_CORDB, LL_INFO10000, "DEBUG TRACE, thread %4x at IP: 0x%p\n", pUnmanagedThread->m_id, pExAddress)); + + // Clear the exception and pretend this never happened. + return REACTION(cIgnore); + } +#endif + + // If we were stepping for exception retrigger and got the single step and it should be hidden then just ignore it. + // Anything that isn't cInbandExceptionRetrigger will cause the debug event to be dequeued, stepping turned off, and + // it will count as not retriggering + // TODO: I don't think the IsSSFlagNeeded() check is needed here though it doesn't break anything + if (pUnmanagedThread->IsSSFlagNeeded() && pUnmanagedThread->IsSSFlagHidden() && (dwExCode == STATUS_SINGLE_STEP)) + { + LOG((LF_CORDB, LL_INFO10000, "CP::TE1stCAI: ignoring hidden single step\n")); + return REACTION(cIgnore); + } + + // Is this a breakpoint indicating that the Left Side is now synchronized? + if ((dwExCode == STATUS_BREAKPOINT) && + (pExAddress == pIPCRuntimeOffsets->m_notifyRSOfSyncCompleteBPAddr)) + { + return TriageSyncComplete(); + } + else if ((dwExCode == STATUS_BREAKPOINT) && + (pExAddress == pIPCRuntimeOffsets->m_excepForRuntimeHandoffCompleteBPAddr)) + { + _ASSERTE(!"This should be unused now"); + + // This notification means that a thread that had been first-chance hijacked is now + // finally leaving the hijack. + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::TE1stCAI: received 'first chance hijack handoff complete' flare.\n"); + + // Let the process run. + return REACTION(cIgnore); + } + else if ((dwExCode == STATUS_BREAKPOINT) && + (pExAddress == pIPCRuntimeOffsets->m_signalHijackCompleteBPAddr)) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::TE1stCAI: received 'hijack complete' flare.\n"); + return REACTION(cInbandHijackComplete); + } + else if ((dwExCode == STATUS_BREAKPOINT) && + (pExAddress == m_runtimeOffsets.m_signalHijackStartedBPAddr)) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::TE1stCAI: received 'hijack started' flare.\n"); + return REACTION(cFirstChanceHijackStarted); + } + else if ((dwExCode == STATUS_BREAKPOINT) && ((pNativePatch = GetNativePatch(pExAddress)) != NULL) ) + { + // We hit a native BP placed by Cordbg. This could happen on any thread (including helper) + bool fCantStop = pUnmanagedThread->IsCantStop(); + + // REVISIT_TODO: if the user also set a breakpoint here then we should dispatch to the debugger + // and rely on the debugger to get us past this. Should be a rare case though. + if (fCantStop) + { + // Need to skip it completely; never dispatch. + pUnmanagedThread->SetupForSkipBreakpoint(pNativePatch); + + // Debuggee will single step over the patch, and fire a SS exception. + // We'll then call FixupForSkipBreakpoint, and continue the process. + return REACTION(cIgnore); + } + else + { + // Native patch in native code. A very common scenario. + // Dispatch as an IB event to Cordbg. + STRESS_LOG1(LF_CORDB, LL_INFO10000, "Native patch in native code (at %p), dispatching as IB event.\n", pExAddress); + return REACTION(cInband); + } + + UNREACHABLE(); + } + + else if ((dwExCode == STATUS_BREAKPOINT) && !IsBreakOpcodeAtAddress(pExAddress)) + { + // If we got an int3 exception, but there's not actually an int3 at the address, then just reset the IP + // to the address. This can happen if the int 3 is cleared after the thread has dispatched it (in which case + // WFDE will pick it up) but before we realize it's one of ours. + STRESS_LOG2(LF_CORDB, LL_INFO1000, "CP::TE1stCAI: Phantom Int3: Tid=0x%x, addr=%p\n", pEvent->dwThreadId, pExAddress); + + DT_CONTEXT context; + + context.ContextFlags = DT_CONTEXT_FULL; + + BOOL fSuccess = DbiGetThreadContext(pUnmanagedThread->m_handle, &context); + + _ASSERTE(fSuccess); + + if (fSuccess) + { + // Backup IP to point to the instruction we need to execute. Continuing from a breakpoint exception + // continues execution at the instruction after the breakpoint, but we need to continue where the + // breakpoint was. + CORDbgSetIP(&context, (LPVOID) pExAddress); + + fSuccess = DbiSetThreadContext(pUnmanagedThread->m_handle, &context); + _ASSERTE(fSuccess); + } + + return REACTION(cIgnore); + } + else if (pUnmanagedThread->IsSkippingNativePatch()) + { + // If we Single-Step over an exception, then the OS never gives us the single-step event. + // Thus if we're skipping a native patch, we don't care what exception event we got. + LOG((LF_CORDB, LL_INFO100000, "Done skipping native patch. Ex=0x%x\n, IsSS=%d", + dwExCode, + (dwExCode == STATUS_SINGLE_STEP))); + + // This is the 2nd half of skipping a native patch. + // This could happen on any thread (including helper) + // We've already removed the opcode and now we just finished a single-step over it. + // So put the patch back in, and continue the process. + pUnmanagedThread->FixupForSkipBreakpoint(); + + return REACTION(cIgnore); + } + else if (this->IsHelperThreadWorked(pUnmanagedThread->GetOSTid())) + { + // We should never ever get a single-step event from the helper thread. + CONSISTENCY_CHECK_MSGF(dwExCode != STATUS_SINGLE_STEP, ( + "Single-Step exception on helper thread (tid=0x%x/%d) in debuggee process (pid=0x%x/%d).\n" + "For more information, attach a debuggee non-invasively to the LS to get the callstack.\n", + pUnmanagedThread->m_id, + pUnmanagedThread->m_id, + this->m_id, + this->m_id)); + + // We ignore any first chance exceptions from the helper thread. There are lots of places + // on the left side where we attempt to dereference bad object refs and such that will be + // handled by exception handlers already in place. + // + // Note: we check this after checking for the sync complete notification, since that can + // come from the helper thread. + // + // Note: we do let single step and breakpoint exceptions go through to the debugger for processing. + if ((dwExCode != STATUS_BREAKPOINT) && (dwExCode != STATUS_SINGLE_STEP)) + { + return REACTION(cCLR); + } + else + { + // Since the helper thread is part of the "can't stop" region, we should have already + // skipped any BPs on it. + // However, any Assert on the helper thread will hit this case. + CONSISTENCY_CHECK_MSGF((dwExCode != STATUS_BREAKPOINT), ( + "Assert on helper thread (tid=0x%x/%d) in debuggee process (pid=0x%x/%d).\n" + "For more information, attach a debuggee non-invasively to the LS to get the callstack.\n", + pUnmanagedThread->m_id, + pUnmanagedThread->m_id, + this->m_id, + this->m_id)); + + // These breakpoint and single step exceptions have to be dispatched to the debugger as + // out-of-band events. This tells the debugger that they must continue from these events + // immediatly, and that no interaction with the Left Side is allowed until they do so. This + // makes sense, since these events are on the helper thread. + return REACTION(cOOB); + } + UNREACHABLE(); + } + else if (pUnmanagedThread->IsFirstChanceHijacked() && this->ExceptionIsFlare(dwExCode, pExAddress)) + { + _ASSERTE(!"This should be unused now"); + } + else if (pUnmanagedThread->IsGenericHijacked()) + { + if (this->ExceptionIsFlare(dwExCode, pExAddress)) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::TE1stCAI: fixing up from generic hijack.\n"); + + _ASSERTE(dwExCode == STATUS_BREAKPOINT); + + // Fixup the thread from the generic hijack. + pUnmanagedThread->FixupFromGenericHijack(); + + // We force continue from this flare, since its only purpose was to notify us that we had to + // fixup the thread from a generic hijack. + return REACTION(cIgnore); + } + else + { + // We might reach here due to the stack overflow issue, due to target + // memory corruption, or even due to an exception thrown during hijacking + + BOOL bStackOverflow = FALSE; + + if (dwExCode == STATUS_ACCESS_VIOLATION || dwExCode == STATUS_STACK_OVERFLOW) + { + CORDB_ADDRESS stackLimit; + CORDB_ADDRESS stackBase; + if (pUnmanagedThread->GetStackRange(&stackBase, &stackLimit)) + { + TADDR addr = pEvent->u.Exception.ExceptionRecord.ExceptionInformation[1]; + if (stackLimit <= addr && addr < stackBase) + bStackOverflow = TRUE; + } + else + { + // to limit the impact of the change we'll consider failure to retrieve the stack + // bounds as stack overflow as well + bStackOverflow = TRUE; + } + } + + if (!bStackOverflow) + { + // generic hijack means we're in CantStop, so return cOOB + return REACTION(cOOB); + } + + // If generichijacked and its not a flare, and the address referenced is on the stack then we've + // got our special stack overflow case. Take off generic hijacked, mark that the helper thread + // is dead, throw this event on the floor, and pop anyone in SendIPCEvent out of their wait. + pUnmanagedThread->ClearState(CUTS_GenericHijacked); + + this->m_helperThreadDead = true; + + // This only works on Windows, not on Mac. We don't support interop-debugging on Mac anyway. + SetEvent(m_pEventChannel->GetRightSideEventAckHandle()); + + // Note: we remember that this was a second chance event from one of the special stack overflow + // cases with CUES_ExceptionUnclearable. This tells us to force the process to terminate when we + // continue from the event. Since for some odd reason the OS decides to re-raise this exception + // (first chance then second chance) infinitely. + + _ASSERTE(pUnmanagedThread->HasIBEvent()); + + pUnmanagedThread->IBEvent()->SetState(CUES_ExceptionUnclearable); + + //newEvent = false; + return REACTION(cInband_NotNewEvent); + } + } + else + { + Reaction r(REACTION(cOOB)); + HRESULT hrCheck = S_OK;; + EX_TRY + { + r = Triage1stChanceNonSpecial(pUnmanagedThread, pEvent); + } + EX_CATCH_HRESULT(hrCheck); + SIMPLIFYING_ASSUMPTION(SUCCEEDED(hrCheck)); + SetUnrecoverableIfFailed(this, hrCheck); + + return r; + + } + + // At this point, any first-chance exceptions that could be special have been handled. Any + // first-chance exception that we're still processing at this point is destined to be + // dispatched as an unmanaged event. + UNREACHABLE(); +} + + +//--------------------------------------------------------------------------------------- +// +// Triage a 2nd-chance exception when the CLR is initialized. +// +// Arguments: +// pUnmanagedThread - thread that the event has occurred on. +// pEvent - native debug event for the exception that occurred that this is triaging. +// +// Return Value: +// Reaction for how to handle this event. +// +// Assumptions: +// Called when receiving a debug event when the process is stopped. +// +// Notes: +// We already hijacked 2nd-chance managed exceptions, so this is just handling +// some V2 Interop corner cases. +// @dbgtodo interop: this should eventually completely go away with the V3 design. +// +//--------------------------------------------------------------------------------------- +Reaction CordbProcess::TriageExcep2ndChanceAndInit(CordbUnmanagedThread * pUnmanagedThread, const DEBUG_EVENT * pEvent) +{ + _ASSERTE(ThreadHoldsProcessLock()); + + DWORD dwExCode = pEvent->u.Exception.ExceptionRecord.ExceptionCode; + +#ifdef _DEBUG + // For debugging, add an extra knob that let us break on any 2nd chance exceptions. + // Most tests don't throw 2nd-chance, so we could have this enabled most of the time and + // catch bogus 2nd chance exceptions + static DWORD dwNo2ndChance = -1; + + if (dwNo2ndChance == -1) + { + dwNo2ndChance = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_DbgNo2ndChance); + } + + if (dwNo2ndChance) + { + CONSISTENCY_CHECK_MSGF(false, ("2nd chance exception occurred on LS thread=0x%x, code=0x%08x, address=0x%p\n" + "This assert is firing b/c you explicitly requested it by having the 'DbgNo2ndChance' knob enabled.\n" + "Disable it to avoid asserts on 2nd chance.", + pUnmanagedThread->m_id, + dwExCode, + pEvent->u.Exception.ExceptionRecord.ExceptionAddress)); + } +#endif + + + // Second chance exception, Runtime initialized. It could belong to the Runtime, so we'll check. If it + // does, then we'll hijack the thread. Otherwise, well just fall through and let it get + // dispatched. Note: we do this so that the CLR's unhandled exception logic gets a chance to run even + // though we've got a win32 debugger attached. But the unhandled exception logic never touches + // breakpoint or single step exceptions, so we ignore those here, too. + + // There are strange cases with stack overflow exceptions. If a nieve application catches a stack + // overflow exception and handles it, without resetting the guard page, then the app will get an AV when + // it overflows the stack a second time. We will get the first chance AV, but when we continue from it the + // OS won't run any SEH handlers, so our FCH won't actually work. Instead, we'll get the AV back on + // second chance right away, and we'll end up right here. + if (this->IsSpecialStackOverflowCase(pUnmanagedThread, pEvent)) + { + // IsSpecialStackOverflowCase will queue the event for us, so its no longer a "new event". Setting + // newEvent = false here basically prevents us from playing with the event anymore and we fall down + // to the dispatch logic below, which will get our already queued first chance AV dispatched for + // this thread. + //newEvent = false; + return REACTION(cInband_NotNewEvent); + } + else if (this->IsHelperThreadWorked(pUnmanagedThread->GetOSTid())) + { + // A second chance exception from the helper thread. This is pretty bad... we just force continue + // from them and hope for the best. + return REACTION(cCLR); + } + + if(pUnmanagedThread->IsCantStop()) + { + return REACTION(cOOB); + } + else + { + return REACTION(cInband); + } +} + + +//--------------------------------------------------------------------------------------- +// +// Triage a win32 Debug event to get a reaction +// +// Arguments: +// pUnmanagedThread - thread that the event has occurred on. +// pEvent - native debug event for the exception that occurred that this is triaging. +// +// Return Value: +// Reaction for how to handle this event. +// +// Assumptions: +// Called when receiving a debug event when the process is stopped. +// +// Notes: +// This is the main triage routine for Win32 debug events, this delegates to the +// 1st and 2nd chance routines above appropriately. +// +//--------------------------------------------------------------------------------------- +Reaction CordbProcess::TriageWin32DebugEvent(CordbUnmanagedThread * pUnmanagedThread, const DEBUG_EVENT * pEvent) +{ + _ASSERTE(ThreadHoldsProcessLock()); + + // Lots of special cases for exception events. The vast majority of hybrid debugging work that takes + // place is in response to exception events. The work below will consider certian exception events + // special cases and rather than letting them be queued and dispatched, they will be handled right + // here. + if (pEvent->dwDebugEventCode == EXCEPTION_DEBUG_EVENT) + { + STRESS_LOG4(LF_CORDB, LL_INFO1000, "CP::TW32DE: unmanaged exception on " + "tid 0x%x, code 0x%08x, addr 0x%08x, chance %d\n", + pEvent->dwThreadId, + pEvent->u.Exception.ExceptionRecord.ExceptionCode, + pEvent->u.Exception.ExceptionRecord.ExceptionAddress, + 2-pEvent->u.Exception.dwFirstChance); + +#ifdef LOGGING + if (pEvent->u.Exception.ExceptionRecord.ExceptionCode == STATUS_ACCESS_VIOLATION) + { + LOG((LF_CORDB, LL_INFO1000, "\t<%s> address 0x%08x\n", + pEvent->u.Exception.ExceptionRecord.ExceptionInformation[0] ? "write to" : "read from", + pEvent->u.Exception.ExceptionRecord.ExceptionInformation[1])); + } +#endif + + // Mark the loader bp for kicks. We won't start managed attach until native attach is finished. + if (!this->m_loaderBPReceived) + { + // If its a first chance breakpoint, and its the first one, then its the loader breakpoint. + if (pEvent->u.Exception.dwFirstChance && + (pEvent->u.Exception.ExceptionRecord.ExceptionCode == STATUS_BREAKPOINT)) + { + LOG((LF_CORDB, LL_INFO1000, "CP::TW32DE: loader breakpoint received.\n")); + + // Remember that we've received the loader BP event. + this->m_loaderBPReceived = true; + + // We never hijack the loader BP anymore (CLR 2.0+). + // This is b/c w/ interop-attach, we don't start the managed-attach until _after_ Cordbg + // continues from the loader-bp. + } + } // end of loader bp. + + // This event might be the retriggering of an event we already saw but previously had to hijack + if(pUnmanagedThread->HasIBEvent()) + { + const EXCEPTION_RECORD* pRecord1 = &(pEvent->u.Exception.ExceptionRecord); + const EXCEPTION_RECORD* pRecord2 = &(pUnmanagedThread->IBEvent()->m_currentDebugEvent.u.Exception.ExceptionRecord); + if(pRecord1->ExceptionCode == pRecord2->ExceptionCode && + pRecord1->ExceptionFlags == pRecord2->ExceptionFlags && + pRecord1->ExceptionAddress == pRecord2->ExceptionAddress) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "CP::TW32DE: event is continuation of previously hijacked event.\n"); + // if we continued from the hijack then we should have already dispatched this event + _ASSERTE(pUnmanagedThread->IBEvent()->IsDispatched()); + return REACTION(cInbandExceptionRetrigger); + } + } + + // We only care about exception events if they are first chance events and if the Runtime is + // initialized within the process. Otherwise, we don't do anything special with them. + if (pEvent->u.Exception.dwFirstChance && this->m_initialized) + { + return TriageExcep1stChanceAndInit(pUnmanagedThread, pEvent); + } + else if (!pEvent->u.Exception.dwFirstChance && this->m_initialized) + { + return TriageExcep2ndChanceAndInit(pUnmanagedThread, pEvent); + } + else + { + // An exception event, but the Runtime hasn't been initialize. I.e., its an exception event + // that we will never try to hijack. + return REACTION(cInband); + } + + UNREACHABLE(); + } + else + // OOB + { + return REACTION(cOOB); + } + +} + +//--------------------------------------------------------------------------------------- +// +// Top-level handler for a win32 debug event during Interop-debugging. +// +// Arguments: +// event - native debug event to handle. +// +// Assumptions: +// The process just got a native debug event via WaitForDebugEvent +// +// Notes: +// The function will Triage the excpetion and then handle it based on the +// appropriate reaction (see: code:Reaction). +// +// @dbgtodo interop: this should all go into the shim. +//--------------------------------------------------------------------------------------- +void CordbProcess::HandleDebugEventForInteropDebugging(const DEBUG_EVENT * pEvent) +{ + PUBLIC_API_ENTRY_FOR_SHIM(this); + _ASSERTE(IsInteropDebugging() || !"Only do this in real interop handling path"); + + + STRESS_LOG3(LF_CORDB, LL_INFO1000, "W32ET::W32EL: got unmanaged event %d on thread 0x%x, proc 0x%x\n", + pEvent->dwDebugEventCode, pEvent->dwThreadId, pEvent->dwProcessId); + + // Get the Lock. + _ASSERTE(!this->ThreadHoldsProcessLock()); + + RSSmartPtr<CordbProcess> pRef(this); // make sure we're alive... + + RSLockHolder processLockHolder(&this->m_processMutex); + + // If we get a new Win32 Debug event, then we need to flush any cached oop data structures. + // This includes refreshing DAC and our patch table. + ForceDacFlush(); + ClearPatchTable(); + +#ifdef _DEBUG + // We want to detect if we've deadlocked. Unfortunately, w/ interop debugging, there can be a lot of + // deadtime since we need to wait for a debug event. Thus the CPU usage may appear to be at 0%, but + // we're not deadlocked b/c we're still receiving debug events. + // So ping every X debug events. + static int s_cCount = 0; + static int s_iPingLevel = -1; + if (s_iPingLevel == -1) + { + s_iPingLevel = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_DbgPingInterop); + } + if (s_iPingLevel != 0) + { + s_cCount++; + if (s_cCount >= s_iPingLevel) + { + s_cCount = 0; + ::Beep(1000,100); + + // Refresh so we can adjust ping level midstream. + s_iPingLevel = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_DbgPingInterop); + } + } +#endif + + bool fNewEvent = true; + + // Mark the process as stopped. + this->m_state |= CordbProcess::PS_WIN32_STOPPED; + + CordbUnmanagedThread * pUnmanagedThread = GetUnmanagedThreadFromEvent(pEvent); + + // In retail, if there is no unmanaged thread then we just continue and loop back around. UnrecoverableError has + // already been set in this case. Note: there is an issue in the Win32 debugging API that can cause duplicate + // ExitThread events. We therefore must handle not finding an unmanaged thread gracefully. + + _ASSERTE((pUnmanagedThread != NULL) || (pEvent->dwDebugEventCode == EXIT_THREAD_DEBUG_EVENT)); + + if (pUnmanagedThread == NULL) + { + // Note: we use ContinueDebugEvent directly here since our continue is very simple and all of our other + // continue mechanisms rely on having an UnmanagedThread object to play with ;) + STRESS_LOG2(LF_CORDB, LL_INFO1000, "W32ET::W32EL: Continuing without thread on tid 0x%x, code=0x%x\n", + pEvent->dwThreadId, + pEvent->dwDebugEventCode); + + this->m_state &= ~CordbProcess::PS_WIN32_STOPPED; + + BOOL fOk = ContinueDebugEvent(pEvent->dwProcessId, pEvent->dwThreadId, DBG_EXCEPTION_NOT_HANDLED); + + _ASSERTE(fOk || !"ContinueDebugEvent failed when he have no thread. Debuggee is likely hung"); + + return; + } + + // There's an innate race such that we can get a Debug Event even after we've suspended a thread. + // This can happen if the thread has already dispatched the debug event but we haven't called WFDE to pick it up + // yet. This is sufficiently goofy that we want to stress log it. + if (pUnmanagedThread->IsSuspended()) + { + STRESS_LOG1(LF_CORDB, LL_INFO1000, "W32ET::W32EL: Thread 0x%x is suspended\n", pEvent->dwThreadId); + } + + // For debugging crazy races in retail, we'll keep a rolling queue of win32 debug events. + this->DebugRecordWin32Event(pEvent, pUnmanagedThread); + + + // Check to see if shutdown of the in-proc debugging services has begun. If it has, then we know we'll no longer + // be running any managed code, and we know that we can stop hijacking threads. We remember this by setting + // m_initialized to false, thus preventing most things from happening elsewhere. + // Don't even bother checking the DCB fields until it's been verified (m_initialized == true) + if (this->m_initialized && (this->GetDCB() != NULL)) + { + UpdateRightSideDCB(); + if (this->GetDCB()->m_shutdownBegun) + { + STRESS_LOG0(LF_CORDB, LL_INFO1000, "W32ET::W32EL: shutdown begun...\n"); + this->m_initialized = false; + } + } + +#ifdef _DEBUG + //Verify that GetThreadContext agrees with the exception address + if (pEvent->dwDebugEventCode == EXCEPTION_DEBUG_EVENT) + { + DT_CONTEXT tempDebugContext; + tempDebugContext.ContextFlags = DT_CONTEXT_FULL; + DbiGetThreadContext(pUnmanagedThread->m_handle, &tempDebugContext); + CordbUnmanagedThread::LogContext(&tempDebugContext); + _ASSERTE(CORDbgGetIP(&tempDebugContext) == pEvent->u.Exception.ExceptionRecord.ExceptionAddress || + (DWORD)CORDbgGetIP(&tempDebugContext) == ((DWORD)pEvent->u.Exception.ExceptionRecord.ExceptionAddress)+1); + } +#endif + + // This call will decide what to do w/ the the win32 event we just got. It does a lot of work. + Reaction reaction = TriageWin32DebugEvent(pUnmanagedThread, pEvent); + + + // Stress-log the reaction. +#ifdef _DEBUG + STRESS_LOG3(LF_CORDB, LL_INFO1000, "Reaction: %d (%s), line=%d\n", + reaction.GetType(), + reaction.GetReactionName(), + reaction.GetLine()); +#else + STRESS_LOG1(LF_CORDB, LL_INFO1000, "Reaction: %d\n", reaction.GetType()); +#endif + + // Make sure the lock wasn't accidentally released. + _ASSERTE(ThreadHoldsProcessLock()); + CordbWin32EventThread * pW32EventThread = this->m_pShim->GetWin32EventThread(); + _ASSERTE(pW32EventThread != NULL); + + // if we were waiting for a retriggered exception but recieved any other event then turn + // off the single stepping and dequeue the IB event. Right now we only use the SS flag internally + // for stepping during possible retrigger. + if(reaction.GetType() != Reaction::cInbandExceptionRetrigger && pUnmanagedThread->IsSSFlagNeeded()) + { + _ASSERTE(pUnmanagedThread->HasIBEvent()); + CordbUnmanagedEvent* pUnmanagedEvent = pUnmanagedThread->IBEvent(); + _ASSERTE(pUnmanagedEvent->IsIBEvent()); + _ASSERTE(pUnmanagedEvent->IsEventContinuedUnhijacked()); + _ASSERTE(pUnmanagedEvent->IsDispatched()); + LOG((LF_CORDB, LL_INFO100000, "CP::HDEFID: IB event did not retrigger ue=0x%p\n", pUnmanagedEvent)); + + DequeueUnmanagedEvent(pUnmanagedThread); + pUnmanagedThread->EndStepping(); + } + + switch(reaction.GetType()) + { + // Common for flares. + case Reaction::cIgnore: + + // Shouldn't be suspending in the first place with outstanding flares. + _ASSERTE(!pUnmanagedThread->IsSuspended()); + + pW32EventThread->ForceDbgContinue(this, pUnmanagedThread, DBG_CONTINUE, false); + goto LDone; + + case Reaction::cCLR: + // Don't care if thread is suspended here. We'll just let the thread continue whatever it's doing. + + this->m_DbgSupport.m_TotalCLR++; + + // If this is for the CLR, then we just continue unhandled and know that the CLR has + // a handler inplace to deal w/ this exception. + pW32EventThread->ForceDbgContinue(this, pUnmanagedThread, DBG_EXCEPTION_NOT_HANDLED, false); + goto LDone; + + + case Reaction::cInband_NotNewEvent: + fNewEvent = false; + + // fall through to Inband case... + + case Reaction::cInband: + { + this->m_DbgSupport.m_TotalIB++; + + // Hijack in-band events (exception events, exit threads) if there is already an event at the head + // of the queue or if the process is currently synchronized. Of course, we only do this if the + // process is initialized. + // + // Note: we also hijack these left over in-band events if we're activley trying to send the + // managed continue message to the Left Side. This is controlled by m_specialDeferment below. + + // Only exceptions can be IB events - everything else is OOB. + _ASSERTE(pEvent->dwDebugEventCode == EXCEPTION_DEBUG_EVENT); + + // CLR internal exceptions should be sent back to the CLR and never treated as inband events. + // If this assert fires, the event was triaged wrong. + CONSISTENCY_CHECK_MSGF((pEvent->u.Exception.ExceptionRecord.ExceptionCode != EXCEPTION_COMPLUS), + ("Attempting to dispatch a CLR internal exception as an Inband event. Reaction line=%d\n", + reaction.GetLine())); + + + _ASSERTE(!pUnmanagedThread->IsCantStop()); + + // We need to decide whether or not to dispatch this event immediately + // We defer it to enforce that we only dispatch 1 IB event at a time (managed events are + // considered IB here). + // This means if: + // 1) there's already an outstanding unmanaged inband event (an event the user has not continued from) + // 2) If the process is synchronized (since that means we've already dispatched a managed event). + // 3) If we've received a SyncComplete event, but aren't yet Sync. This will almost always be the same as + // whether we're synced, but has a distict quality. It's always set by the w32 event thread in Interop, + // and so it's guaranteed to be serialized against this check here (also on the w32et). + // 4) Special deferment - This covers the region where we're sending a Stop/Continue IPC event across. + // We defer it here to keep the Helper thread alive so that it can handle these IPC events. + // Queued events will be dispatched when continue is called. + BOOL fHasUserUncontinuedNativeEvents = HasUserUncontinuedNativeEvents(); + bool fDeferInbandEvent = (fHasUserUncontinuedNativeEvents || + GetSynchronized() || + GetSyncCompleteRecv() || + m_specialDeferment); + + // If we've got a new event, queue it. + if (fNewEvent) + { + this->QueueUnmanagedEvent(pUnmanagedThread, pEvent); + } + + if (fNewEvent && this->m_initialized && fDeferInbandEvent) + { + STRESS_LOG4(LF_CORDB, LL_INFO1000, "W32ET::W32EL: Needed to defer dispatching event: %d %d %d %d\n", + fHasUserUncontinuedNativeEvents, + GetSynchronized(), + GetSyncCompleteRecv(), + m_specialDeferment); + + // this continues the IB debug event into the hijack + // the process is now running again + pW32EventThread->DoDbgContinue(this, pUnmanagedThread->IBEvent()); + + // Since we've hijacked this event, we don't need to do any further processing. + goto LDone; + } + else + { + // No need to defer the dispatch, do it now + this->DispatchUnmanagedInBandEvent(); + + goto LDone; + } + UNREACHABLE(); + } + + case Reaction::cFirstChanceHijackStarted: + { + // determine the logical event we are handling, if any + CordbUnmanagedEvent* pUnmanagedEvent = NULL; + if(pUnmanagedThread->HasIBEvent()) + { + pUnmanagedEvent = pUnmanagedThread->IBEvent(); + } + LOG((LF_CORDB, LL_INFO100000, "W32ET::W32EL: IB hijack starting, ue=0x%p\n", pUnmanagedEvent)); + + // fetch the LS memory set up for this hijack + REMOTE_PTR pDebuggerWord = NULL; + DebuggerIPCFirstChanceData fcd; + pUnmanagedThread->GetEEDebuggerWord(&pDebuggerWord); + SafeReadStruct(PTR_TO_CORDB_ADDRESS(pDebuggerWord), &fcd); + + LOG((LF_CORDB, LL_INFO100000, "W32ET::W32EL: old fcd DebugCounter=0x%x\n", fcd.debugCounter)); + + // determine what action the LS should take + if(pUnmanagedThread->IsBlockingForSync()) + { + // there should be an event we hijacked in this case + _ASSERTE(pUnmanagedEvent != NULL); + + // block that event + LOG((LF_CORDB, LL_INFO100000, "W32ET::W32EL: blocking\n")); + fcd.action = HIJACK_ACTION_WAIT; + fcd.debugCounter = 0x2; + SafeWriteStruct(PTR_TO_CORDB_ADDRESS(pDebuggerWord), &fcd); + } + else + { + // we don't need to block. We want the vectored handler to just exit + // as if it wasn't there + _ASSERTE(fcd.action == HIJACK_ACTION_EXIT_UNHANDLED); + LOG((LF_CORDB, LL_INFO100000, "W32ET::W32EL: not blocking\n")); + } + + LOG((LF_CORDB, LL_INFO100000, "W32ET::W32EL: continuing from flare\n")); + pW32EventThread->ForceDbgContinue(this, pUnmanagedThread, DBG_CONTINUE, false); + goto LDone; + } + + case Reaction::cInbandHijackComplete: + { + // We now execute the hijack worker even when not actually hijacked + // so can't assert this + //_ASSERTE(pUnmanagedThread->IsFirstChanceHijacked()); + + // we should not be stepping at the end of hijacks + _ASSERTE(!pUnmanagedThread->IsSSFlagHidden()); + _ASSERTE(!pUnmanagedThread->IsSSFlagNeeded()); + + // if we were hijacked then clean up + if(pUnmanagedThread->IsFirstChanceHijacked()) + { + LOG((LF_CORDB, LL_INFO100000, "W32ET::W32EL: hijack complete will restore context...\n")); + DT_CONTEXT tempContext = { 0 }; + tempContext.ContextFlags = DT_CONTEXT_FULL; + HRESULT hr = pUnmanagedThread->GetThreadContext(&tempContext); + _ASSERTE(SUCCEEDED(hr)); + + // The sync hijack returns normally but the m2uHandoff hijack needs to have the IP + // deliberately restored + if(!pUnmanagedThread->IsBlockingForSync()) + { + // restore the context to the current un-hijacked context + BOOL succ = DbiSetThreadContext(pUnmanagedThread->m_handle, &tempContext); + _ASSERTE(succ); + + // Because hijacks don't return normally they might have pushed handlers without poping them + // back off. To take care of that we explicitly restore the old SEH chain. + #ifdef DBG_TARGET_X86 + hr = pUnmanagedThread->RestoreLeafSeh(); + _ASSERTE(SUCCEEDED(hr)); + #endif + } + else + { + _ASSERTE(pUnmanagedThread->HasIBEvent()); + CordbUnmanagedEvent* pUnmanagedEvent = pUnmanagedThread->IBEvent(); + LOG((LF_CORDB, LL_INFO100000, "W32ET::W32EL: IB hijack completing, continuing unhijacked ue=0x%p\n", pUnmanagedEvent)); + _ASSERTE(pUnmanagedEvent->IsEventContinuedHijacked()); + _ASSERTE(pUnmanagedEvent->IsDispatched()); + _ASSERTE(pUnmanagedEvent->IsEventUserContinued()); + _ASSERTE(!pUnmanagedEvent->IsEventContinuedUnhijacked()); + pUnmanagedEvent->SetState(CUES_EventContinuedUnhijacked); + + // fetch the LS memory set up for this hijack + REMOTE_PTR pDebuggerWord = NULL; + DebuggerIPCFirstChanceData fcd; + pUnmanagedThread->GetEEDebuggerWord(&pDebuggerWord); + SafeReadStruct(PTR_TO_CORDB_ADDRESS(pDebuggerWord), &fcd); + + LOG((LF_CORDB, LL_INFO10000, "W32ET::W32EL: pDebuggerWord is 0x%p\n", pDebuggerWord)); + + //set the correct continuation action based upon the user's selection + if(pUnmanagedEvent->IsExceptionCleared()) + { + LOG((LF_CORDB, LL_INFO10000, "W32ET::W32EL: exception cleared\n")); + fcd.action = HIJACK_ACTION_EXIT_HANDLED; + } + else + { + LOG((LF_CORDB, LL_INFO10000, "W32ET::W32EL: exception not cleared\n")); + fcd.action = HIJACK_ACTION_EXIT_UNHANDLED; + } + + // + // LS context is restored here so that execution continues from next instruction that caused the hijack. + // We shouldn't always restore the LS context though. + // Consider the following case where this can cause issues: + // Debuggee process hits an exception and calls KERNELBASE!RaiseException, debugger gets the notification and + // prepares for first-chance hijack. Debugger(DBI) saves the current thread context (see SetupFirstChanceHijackForSync) which is restored + // later below (see SafeWriteThreadContext call) when the process is in VEH (CLRVectoredExceptionHandlerShim->FirstChanceSuspendHijackWorker). + // The thread context that got saved(by SetupFirstChanceHijackForSync) was for when the thread was executing RaiseException and when + // this context gets restored in VEH, the thread resumes after the exception handler with a context that is not same as one with which + // it entered. This inconsistency can lead to bad execution code-paths or even a debuggee crash. + // + // Example case where we should definitely update the LS context: + // After a DbgBreakPoint call, IP gets updated to point to the instruction after int 3 and this is the context saved by debugger. + // The IP in context passed to VEH still points to int 3 though and if we don't update the LS context in VEH, the breakpoint + // instruction will get executed again. + // + // Here's a list of cases when we update the LS context: + // * we know that context was explicitly updated during this hijack, OR + // * if single-stepping flag was set on it originally, OR + // * if this was a breakpoint event + // Note that above list is a heuristic and it is possible that we need to add more such cases in future. + // + BOOL isBreakPointEvent = (pUnmanagedEvent->m_currentDebugEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT && + pUnmanagedEvent->m_currentDebugEvent.u.Exception.ExceptionRecord.ExceptionCode == STATUS_BREAKPOINT); + if (pUnmanagedThread->IsContextSet() || IsSSFlagEnabled(&tempContext) || isBreakPointEvent) + { + _ASSERTE(fcd.pLeftSideContext != NULL); + LOG((LF_CORDB, LL_INFO10000, "W32ET::W32EL: updating LS context at 0x%p\n", fcd.pLeftSideContext)); + // write the new context over the old one on the LS + SafeWriteThreadContext(fcd.pLeftSideContext, &tempContext); + } + + // Write the new Fcd data to the LS + fcd.debugCounter = 0x1; + SafeWriteStruct(PTR_TO_CORDB_ADDRESS(pDebuggerWord), &fcd); + + fcd.debugCounter = 0; + SafeReadStruct(PTR_TO_CORDB_ADDRESS(pDebuggerWord), &fcd); + _ASSERTE(fcd.debugCounter == 1); + + DequeueUnmanagedEvent(pUnmanagedThread); + } + + _ASSERTE(m_cFirstChanceHijackedThreads > 0); + m_cFirstChanceHijackedThreads--; + if(m_cFirstChanceHijackedThreads == 0) + { + m_state &= ~PS_HIJACKS_IN_PLACE; + } + + pUnmanagedThread->ClearState(CUTS_FirstChanceHijacked); + pUnmanagedThread->ClearState(CUTS_BlockingForSync); + + // if the user set the context it either was already applied (m2uHandoff hijack) + // or is about to be applied when the hijack returns (sync hijack). + // There may still a small window where it won't appear accurate that + // we just have to live with + pUnmanagedThread->ClearState(CUTS_HasContextSet); + } + + pW32EventThread->ForceDbgContinue(this, pUnmanagedThread, DBG_CONTINUE, false); + + // We've handled this event. Skip further processing. + goto LDone; + } + + case Reaction::cBreakpointRequiringHijack: + { + HRESULT hr = pUnmanagedThread->SetupFirstChanceHijack(EHijackReason::kM2UHandoff, &(pEvent->u.Exception.ExceptionRecord)); + _ASSERTE(SUCCEEDED(hr)); + pW32EventThread->ForceDbgContinue(this, pUnmanagedThread, DBG_CONTINUE, false); + goto LDone; + } + + case Reaction::cInbandExceptionRetrigger: + { + // this should be unused now + _ASSERTE(FALSE); + _ASSERTE(pUnmanagedThread->HasIBEvent()); + CordbUnmanagedEvent* pUnmanagedEvent = pUnmanagedThread->IBEvent(); + _ASSERTE(pUnmanagedEvent->IsIBEvent()); + _ASSERTE(pUnmanagedEvent->IsEventContinuedUnhijacked()); + _ASSERTE(pUnmanagedEvent->IsDispatched()); + LOG((LF_CORDB, LL_INFO100000, "W32ET::W32EL: IB event completing, continuing ue=0x%p\n", pUnmanagedEvent)); + + DequeueUnmanagedEvent(pUnmanagedThread); + // If this event came from RaiseException then flush the context to ensure we won't use it until we re-enter + if(pUnmanagedEvent->m_owner->IsRaiseExceptionHijacked()) + { + pUnmanagedEvent->m_owner->RestoreFromRaiseExceptionHijack(); + pUnmanagedEvent->m_owner->ClearRaiseExceptionEntryContext(); + } + else // otherwise we should have been stepping + { + pUnmanagedThread->EndStepping(); + } + pW32EventThread->ForceDbgContinue(this, pUnmanagedThread, + pUnmanagedEvent->IsExceptionCleared() ? DBG_CONTINUE : DBG_EXCEPTION_NOT_HANDLED, false); + + // We've handled this event. Skip further processing. + goto LDone; + } + + case Reaction::cOOB: + { + // Don't care if this thread claimed to be suspended or not. Dispatch event anyways. After all, + // OOB events can come at *any* time. + + // This thread may be suspended. We don't care. + this->m_DbgSupport.m_TotalOOB++; + + // Not an inband event. This includes ALL non-exception events (including EXIT_THREAD) as + // well as any exception that can't be hijacked (ex, an exception on the helper thread). + + // If this is an exit thread or exit process event, then we need to mark the unmanaged thread as + // exited for later. + if ((pEvent->dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) || + (pEvent->dwDebugEventCode == EXIT_THREAD_DEBUG_EVENT)) + { + pUnmanagedThread->SetState(CUTS_Deleted); + } + + // If we get an exit process or exit thread event on the helper thread, then we know we're loosing + // the Left Side, so go ahead and remember that the helper thread has died. + if (this->IsHelperThreadWorked(pUnmanagedThread->GetOSTid())) + { + if ((pEvent->dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) || + (pEvent->dwDebugEventCode == EXIT_THREAD_DEBUG_EVENT)) + { + this->m_helperThreadDead = true; + } + } + + // Queue the current out-of-band event. + this->QueueOOBUnmanagedEvent(pUnmanagedThread, pEvent); + + // Go ahead and dispatch the event if its the first one. + if (this->m_outOfBandEventQueue == pUnmanagedThread->OOBEvent()) + { + // Set this to true to indicate to Continue() that we're in the unamnaged callback. + CordbUnmanagedEvent * pUnmanagedEvent = pUnmanagedThread->OOBEvent(); + + this->m_dispatchingOOBEvent = true; + + pUnmanagedEvent->SetState(CUES_Dispatched); + + this->Unlock(); + + // Handler should have been registered by now. + _ASSERTE(this->m_cordb->m_unmanagedCallback != NULL); + + // Call the callback with fIsOutOfBand = TRUE. + { + PUBLIC_WIN32_CALLBACK_IN_THIS_SCOPE(this, pEvent, TRUE); + this->m_cordb->m_unmanagedCallback->DebugEvent(const_cast<DEBUG_EVENT*> (pEvent), TRUE); + } + + this->Lock(); + + // If m_dispatchingOOBEvent is false, that means that the user called Continue() from within + // the callback. We know that we can go ahead and continue the process now. + if (this->m_dispatchingOOBEvent == false) + { + // Note: this call will dispatch more OOB events if necessary. + pW32EventThread->UnmanagedContinue(this, cOobUMContinue); + } + else + { + // We're not dispatching anymore, so set this back to false. + this->m_dispatchingOOBEvent = false; + } + } + + // We've handled this event. Skip further processing. + goto LDone; + } + } // end Switch on Reaction + + UNREACHABLE(); + +LDone: + // Process Lock implicitly released by holder. + + STRESS_LOG0(LF_CORDB, LL_INFO1000, "W32ET::W32EL: done processing event.\n"); + + return; +} + +// +// Returns true if the exception is a flare from the left side, false otherwise. +// +bool CordbProcess::ExceptionIsFlare(DWORD exceptionCode, const void *exceptionAddress) +{ + _ASSERTE(m_runtimeOffsetsInitialized); + + // Can't have a flare if the left side isn't initialized + if (m_initialized) + { + DebuggerIPCRuntimeOffsets *pRO = &m_runtimeOffsets; + + // All flares are breakpoints... + if (exceptionCode == STATUS_BREAKPOINT) + { + // Does the breakpoint address match a flare address? + if ((exceptionAddress == pRO->m_signalHijackStartedBPAddr) || + (exceptionAddress == pRO->m_excepForRuntimeHandoffStartBPAddr) || + (exceptionAddress == pRO->m_excepForRuntimeHandoffCompleteBPAddr) || + (exceptionAddress == pRO->m_signalHijackCompleteBPAddr) || + (exceptionAddress == pRO->m_excepNotForRuntimeBPAddr) || + (exceptionAddress == pRO->m_notifyRSOfSyncCompleteBPAddr)) + return true; + } + } + + return false; +} +#endif // FEATURE_INTEROP_DEBUGGING + +// Allocate a buffer in the target and copy data into it. +// +// Arguments: +// pDomain - an appdomain associated with the allocation request. +// bufferSize - size of the buffer in bytes +// bufferFrom - local buffer of data (bufferSize bytes) to copy data from. +// ppRes - address into target of allocated buffer +// +// Returns: +// S_OK on success, else error. +HRESULT CordbProcess::GetAndWriteRemoteBuffer(CordbAppDomain *pDomain, unsigned int bufferSize, const void *bufferFrom, void **ppRes) +{ + _ASSERTE(ppRes != NULL); + *ppRes = NULL; + + HRESULT hr = S_OK; + + EX_TRY + { + TargetBuffer tbTarget = GetRemoteBuffer(bufferSize); // throws + SafeWriteBuffer(tbTarget, (const BYTE*) bufferFrom); // throws + + // Succeeded. + *ppRes = CORDB_ADDRESS_TO_PTR(tbTarget.pAddress); + } + EX_CATCH_HRESULT(hr); + return hr; +} + +#ifdef FEATURE_INTEROP_DEBUGGING + +// +// Checks to see if the given second chance exception event actually signifies the death of the process due to a second +// stack overflow special case. +// +// There are strange cases with stack overflow exceptions. If a nieve application catches a stack overflow exception and +// handles it, without resetting the guard page, then the app will get an AV when it overflows the stack a second time. We +// will get the first chance AV, but when we continue from it the OS won't run any SEH handlers, so our FCH won't +// actually work. Instead, we'll get the AV back on second chance right away. +// +bool CordbProcess::IsSpecialStackOverflowCase(CordbUnmanagedThread *pUThread, const DEBUG_EVENT *pEvent) +{ + _ASSERTE(pEvent->dwDebugEventCode == EXCEPTION_DEBUG_EVENT); + _ASSERTE(pEvent->u.Exception.dwFirstChance == 0); + + // If this is not an AV, it can't be our special case. + if (pEvent->u.Exception.ExceptionRecord.ExceptionCode != STATUS_ACCESS_VIOLATION) + return false; + + // If the thread isn't already first chance hijacked, it can't be our special case. + if (!pUThread->IsFirstChanceHijacked()) + return false; + + // The first chance hijack didn't take, so we're not FCH anymore and we're not waiting for an answer + // anymore... Note: by leaving this thread completely unhijacked, we'll report its true context, which is correct. + pUThread->ClearState(CUTS_FirstChanceHijacked); + + // The process is techincally dead as a door nail here, so we'll mark that the helper thread is dead so our managed + // API bails nicely. + m_helperThreadDead = true; + + // Remember we're in our special case. + pUThread->SetState(CUTS_HasSpecialStackOverflowCase); + + // Now, remember the second chance AV event in the second IB event slot for this thread and add it to the end of the + // IB event queue. + QueueUnmanagedEvent(pUThread, pEvent); + + // Note: returning true will ensure that the queued first chance AV for this thread is dispatched. + return true; +} + +//----------------------------------------------------------------------------- +// Longhorn broke ContinueDebugEvent. +// In previous OS releases, DBG_CONTINUE would continue a non-continuable exception. +// In longhorn, we need to pass the DBG_FORCE_CONTINUE flag to do that. +// Note that all CLR exceptions are non-continuable. +// Now instead of DBG_CONTINUE, we need to pass DBG_FORCE_CONTINUE. +//----------------------------------------------------------------------------- + +// Currently we don't have headers for the longhorn winnt.h. So we need to privately declare +// this here. We have a check such that if we do get headers, the value won't change underneath us. +#define MY_DBG_FORCE_CONTINUE ((DWORD )0x00010003L) +#ifndef DBG_FORCE_CONTINUE +#define DBG_FORCE_CONTINUE MY_DBG_FORCE_CONTINUE +#else +static_assert_no_msg(DBG_FORCE_CONTINUE == MY_DBG_FORCE_CONTINUE); +#endif + +DWORD GetDbgContinueFlag() +{ + // Currently, default to not using the new DBG_FORCE_CONTINUE flag. + static ConfigDWORD fNoFlagKey; + bool fNoFlag = fNoFlagKey.val(CLRConfig::UNSUPPORTED_DbgNoForceContinue) != 0; + + + if (!fNoFlag) + { + return DBG_FORCE_CONTINUE; + } + else + { + return DBG_CONTINUE; + } +} + + +// Some Interop bugs involve threads that land at a crazy IP. Since we're interop-debugging, we can't +// attach a debugger to the LS. So we have some debug mode where we enable the SS flag and thus +// produce a trace of where a thread is going. +#ifdef _DEBUG +void EnableDebugTrace(CordbUnmanagedThread *ut) +{ + // To enable, attach w/ a debugger and either set fTrace==true, or setip. + static bool fTrace = false; + if (!fTrace) + return; + + // Give us a nop so that we can setip in the optimized case. +#ifdef _TARGET_X86_ + __asm { + nop + } +#endif + + fTrace = true; + CordbProcess *pProcess = ut->GetProcess(); + + // Get the context + HRESULT hr = S_OK; + DT_CONTEXT context; + context.ContextFlags = DT_CONTEXT_FULL; + + + hr = pProcess->GetThreadContext((DWORD) ut->m_id, sizeof(context), (BYTE*)&context); + if (FAILED(hr)) + return; + + // If the flag is already set, then don't set it again - that will just get confusing. + if (IsSSFlagEnabled(&context)) + { + return; + } + _ASSERTE(CORDbgGetIP(&context) != 0); + SetSSFlag(&context); + + // If SS flag not set, enable it. And remeber that it's us so we know how to handle + // it when we get the debug event. + hr = pProcess->SetThreadContext((DWORD)ut->m_id, sizeof(context), (BYTE*)&context); + ut->SetState(CUTS_DEBUG_SingleStep); +} +#endif // _DEBUG + +//----------------------------------------------------------------------------- +// DoDbgContinue +// +// Continues from a specific Win32 DEBUG_EVENT. +// +// Arguments: +// pProcess - The process to continue. +// pUnmanagedEvent - The event to continue. +// +//----------------------------------------------------------------------------- +void CordbWin32EventThread::DoDbgContinue(CordbProcess *pProcess, + CordbUnmanagedEvent *pUnmanagedEvent) +{ + _ASSERTE(pProcess->ThreadHoldsProcessLock()); + _ASSERTE(IsWin32EventThread()); + _ASSERTE(pUnmanagedEvent != NULL); + _ASSERTE(!pUnmanagedEvent->IsEventContinuedUnhijacked()); + + STRESS_LOG3(LF_CORDB, LL_INFO1000, + "W32ET::DDC: continue with ue=0x%p, thread=0x%p, tid=0x%x\n", + pUnmanagedEvent, + pUnmanagedEvent->m_owner, + pUnmanagedEvent->m_owner->m_id); + +#ifdef _DEBUG + EnableDebugTrace(pUnmanagedEvent->m_owner); +#endif + + + if (pUnmanagedEvent->IsEventContinuedHijacked()) + { + LOG((LF_CORDB, LL_INFO100000, "W32ET::DDC: Skiping DoDbgContinue because event was already" + " continued hijacked, ue=0x%p\n", pUnmanagedEvent)); + return; + } + + BOOL threadIsHijacked = (pUnmanagedEvent->m_owner->IsFirstChanceHijacked() || + pUnmanagedEvent->m_owner->IsGenericHijacked()); + + BOOL eventIsIB = (pUnmanagedEvent->m_owner->HasIBEvent() && + pUnmanagedEvent->m_owner->IBEvent() == pUnmanagedEvent); + + _ASSERTE((DWORD) pProcess->m_id == pUnmanagedEvent->m_currentDebugEvent.dwProcessId); + _ASSERTE(pProcess->m_state & CordbProcess::PS_WIN32_STOPPED); + + DWORD dwContType; + if(eventIsIB) + { + // 3 cases here... + // event was already hijacked + if(threadIsHijacked) + { + LOG((LF_CORDB, LL_INFO100000, "W32ET::DDC: Continuing IB, already hijacked, ue=0x%p\n", pUnmanagedEvent)); + pUnmanagedEvent->SetState(CUES_EventContinuedHijacked); + dwContType = !pUnmanagedEvent->m_owner->IsBlockingForSync() ? GetDbgContinueFlag() : DBG_EXCEPTION_NOT_HANDLED; + } + // event was not hijacked but has been dispatched + else if(!threadIsHijacked && pUnmanagedEvent->IsDispatched()) + { + LOG((LF_CORDB, LL_INFO100000, "W32ET::DDC: Continuing IB, not hijacked, ue=0x%p\n", pUnmanagedEvent)); + _ASSERTE(pUnmanagedEvent->IsDispatched()); + _ASSERTE(pUnmanagedEvent->IsEventUserContinued()); + _ASSERTE(!pUnmanagedEvent->IsEventContinuedUnhijacked()); + pUnmanagedEvent->SetState(CUES_EventContinuedUnhijacked); + dwContType = pUnmanagedEvent->IsExceptionCleared() ? GetDbgContinueFlag() : DBG_EXCEPTION_NOT_HANDLED; + + // The event was never hijacked and so will never need to retrigger, get rid + // of it right now. If it had been hijacked then we would dequeue it either after the + // hijack complete flare or one instruction after that when it has had a chance to retrigger + pProcess->DequeueUnmanagedEvent(pUnmanagedEvent->m_owner); + } + // event was not hijacked nor dispatched + else // if(!threadIsHijacked && !pUnmanagedEvent->IsDispatched()) + { + LOG((LF_CORDB, LL_INFO100000, "W32ET::DDC: Continuing IB, now hijacked, ue=0x%p\n", pUnmanagedEvent)); + HRESULT hr = pProcess->HijackIBEvent(pUnmanagedEvent); + _ASSERTE(SUCCEEDED(hr)); + pUnmanagedEvent->SetState(CUES_EventContinuedHijacked); + dwContType = !pUnmanagedEvent->m_owner->IsBlockingForSync() ? GetDbgContinueFlag() : DBG_EXCEPTION_NOT_HANDLED; + } + } + else + { + LOG((LF_CORDB, LL_INFO100000, "W32ET::DDC: Continuing OB, ue=0x%p\n", pUnmanagedEvent)); + // we might actually be hijacked here, but if we are it should be for a previous IB event + // we just mark all OB events as continued unhijacked + pUnmanagedEvent->SetState(CUES_EventContinuedUnhijacked); + dwContType = pUnmanagedEvent->IsExceptionCleared() ? GetDbgContinueFlag() : DBG_EXCEPTION_NOT_HANDLED; + } + + // If the exception is marked as unclearable, then make sure the continue type is correct and force the process + // to terminate. + if (pUnmanagedEvent->IsExceptionUnclearable()) + { + TerminateProcess(pProcess->UnsafeGetProcessHandle(), pUnmanagedEvent->m_currentDebugEvent.u.Exception.ExceptionRecord.ExceptionCode); + dwContType = DBG_EXCEPTION_NOT_HANDLED; + } + + // If we're continuing from the loader-bp, then send the managed attach here. + // (Note this will only be set if the runtime was loaded when we first tried to attach). + // We assume that the loader-bp is the 1st BP exception. This is naive, + // since it's not 100% accurate (someone could CreateThread w/ a threadproc of DebugBreak). + // But it's the best we can do. + // Note that it's critical we do this BEFORE continuing the process. If this is mixed-mode, we've already + // told VS about this breakpoint, and so it's set the attach-complete event. As soon as we continue this debug + // event the process can start moving again, so the CLR needs to know to wait for a managed attach. + DWORD dwEventCode = pUnmanagedEvent->m_currentDebugEvent.dwDebugEventCode; + if (dwEventCode == EXCEPTION_DEBUG_EVENT) + { + EXCEPTION_DEBUG_INFO * pDebugInfo = &pUnmanagedEvent->m_currentDebugEvent.u.Exception; + if (pDebugInfo->dwFirstChance && pDebugInfo->ExceptionRecord.ExceptionCode == STATUS_BREAKPOINT) + { + HRESULT hrIgnore = S_OK; + EX_TRY + { + LOG((LF_CORDB, LL_INFO1000, "W32ET::DDC: Continuing from LdrBp, doing managed attach.\n")); + pProcess->QueueManagedAttachIfNeededWorker(); + } + EX_CATCH_HRESULT(hrIgnore); + SIMPLIFYING_ASSUMPTION(SUCCEEDED(hrIgnore)); + } + } + + STRESS_LOG4(LF_CORDB, LL_INFO1000, + "W32ET::DDC: calling ContinueDebugEvent(0x%x, 0x%x, 0x%x), process state=0x%x\n", + pProcess->m_id, pUnmanagedEvent->m_owner->m_id, dwContType, pProcess->m_state); + + // Actually continue the debug event + pProcess->m_state &= ~CordbProcess::PS_WIN32_STOPPED; + BOOL fSuccess = m_pNativePipeline->ContinueDebugEvent((DWORD)pProcess->m_id, (DWORD)pUnmanagedEvent->m_owner->m_id, dwContType); + + // ContinueDebugEvent may 'fail' if we force kill the debuggee while stopped at the exit-process event. + if (!fSuccess && (dwEventCode != EXIT_PROCESS_DEBUG_EVENT)) + { + _ASSERTE(!"ContinueDebugEvent failed!"); + CORDBSetUnrecoverableError(pProcess, HRESULT_FROM_GetLastError(), 0); + STRESS_LOG1(LF_CORDB, LL_INFO1000, "W32ET::DDC: Last error after ContinueDebugEvent is %d\n", GetLastError()); + } + + // If this thread is marked for deletion (exit thread or exit process event on it), then we need to delete the + // unmanaged thread object. + if ((dwEventCode == EXIT_PROCESS_DEBUG_EVENT) || (dwEventCode == EXIT_THREAD_DEBUG_EVENT)) + { + CordbUnmanagedThread * pUnmanagedThread = pUnmanagedEvent->m_owner; + _ASSERTE(pUnmanagedThread->IsDeleted()); + + + // Thread may have a hijacked inband event on it. Thus it's actually running free from the OS perspective, + // and fair game to be terminated. In that case, we need to auto-dequeue the event. + // This will just prevent the RS from making the underlying call to ContinueDebugEvent on this thread + // for the inband event. Since we've already lost the thread, that's actually exactly what we want. + if (pUnmanagedThread->HasIBEvent()) + { + pProcess->DequeueUnmanagedEvent(pUnmanagedThread); + } + + STRESS_LOG1(LF_CORDB, LL_INFO1000, "Removing thread 0x%x (%d) from process list\n", pUnmanagedThread->m_id); + pProcess->m_unmanagedThreads.RemoveBase((ULONG_PTR)pUnmanagedThread->m_id); + } + + + // If we just continued from an exit process event, then its time to do the exit processing. + if (dwEventCode == EXIT_PROCESS_DEBUG_EVENT) + { + pProcess->Unlock(); + ExitProcess(false); // not detach case + pProcess->Lock(); + } + +} + +//--------------------------------------------------------------------------------------- +// +// ForceDbgContinue continues from the last Win32 DEBUG_EVENT on the given thread, no matter what it was. +// +// Arguments: +// pProcess - process object to continue +// pUnmanagedThread - unmanaged thread object (maybe null if we're doing a raw cotninue) +// contType - continuation status (DBG_CONTINUE or DBG_EXCEPTION_NOT_HANDLED) +// fContinueProcess - do we resume hijacks? +// +void CordbWin32EventThread::ForceDbgContinue(CordbProcess *pProcess, CordbUnmanagedThread *pUnmanagedThread, DWORD contType, + bool fContinueProcess) +{ + _ASSERTE(pProcess->ThreadHoldsProcessLock()); + _ASSERTE(pUnmanagedThread != NULL); + STRESS_LOG4(LF_CORDB, LL_INFO1000, + "W32ET::FDC: force continue with 0x%x (%s), contProcess=%d, tid=0x%x\n", + contType, + (contType == DBG_CONTINUE) ? "DBG_CONTINUE" : "DBG_EXCEPTION_NOT_HANDLED", + fContinueProcess, + pUnmanagedThread->m_id); + + if (fContinueProcess) + { + pProcess->ResumeHijackedThreads(); + } + + if (contType == DBG_CONTINUE) + { + contType = GetDbgContinueFlag(); + } + + _ASSERTE(pProcess->m_state & CordbProcess::PS_WIN32_STOPPED); + + // Remove the Win32 stopped flag so long as the OOB event queue is empty. We're forcing a continue here, so by + // definition this should be the case... + _ASSERTE(pProcess->m_outOfBandEventQueue == NULL); + + pProcess->m_state &= ~CordbProcess::PS_WIN32_STOPPED; + + STRESS_LOG4(LF_CORDB, LL_INFO1000, "W32ET::FDC: calling ContinueDebugEvent(0x%x, 0x%x, 0x%x), process state=0x%x\n", + pProcess->m_id, pUnmanagedThread->m_id, contType, pProcess->m_state); + + + #ifdef _DEBUG + EnableDebugTrace(pUnmanagedThread); + #endif + BOOL ret = m_pNativePipeline->ContinueDebugEvent((DWORD)pProcess->m_id, (DWORD)pUnmanagedThread->m_id, contType); + + if (!ret) + { + // This could in theory fail from Process exit, but that really would only be on the DoDbgContinue path. + _ASSERTE(!"ContinueDebugEvent failed #2!"); + STRESS_LOG1(LF_CORDB, LL_INFO1000, "W32ET::DDC: Last error after ContinueDebugEvent is %d\n", GetLastError()); + } +} +#endif // FEATURE_INTEROP_DEBUGGING + +// +// This is the thread's real thread proc. It simply calls to the +// thread proc on the given object. +// +/*static*/ DWORD WINAPI CordbWin32EventThread::ThreadProc(LPVOID parameter) +{ + CordbWin32EventThread* t = (CordbWin32EventThread*) parameter; + INTERNAL_THREAD_ENTRY(t); + t->ThreadProc(); + return 0; +} + + +// +// Send a CreateProcess event to the Win32 thread to have it create us +// a new process. +// +HRESULT CordbWin32EventThread::SendCreateProcessEvent( + MachineInfo machineInfo, + LPCWSTR programName, + __in_z LPWSTR programArgs, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, + DWORD dwCreationFlags, + PVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, + CorDebugCreateProcessFlags corDebugFlags) +{ + HRESULT hr = S_OK; + + LockSendToWin32EventThreadMutex(); + LOG((LF_CORDB, LL_EVERYTHING, "CordbWin32EventThread::SCPE Called\n")); + m_actionData.createData.machineInfo = machineInfo; + m_actionData.createData.programName = programName; + m_actionData.createData.programArgs = programArgs; + m_actionData.createData.lpProcessAttributes = lpProcessAttributes; + m_actionData.createData.lpThreadAttributes = lpThreadAttributes; + m_actionData.createData.bInheritHandles = bInheritHandles; + m_actionData.createData.dwCreationFlags = dwCreationFlags; + m_actionData.createData.lpEnvironment = lpEnvironment; + m_actionData.createData.lpCurrentDirectory = lpCurrentDirectory; + m_actionData.createData.lpStartupInfo = lpStartupInfo; + m_actionData.createData.lpProcessInformation = lpProcessInformation; + m_actionData.createData.corDebugFlags = corDebugFlags; + + // m_action is set last so that the win32 event thread can inspect + // it and take action without actually having to take any + // locks. The lock around this here is simply to prevent multiple + // threads from making requests at the same time. + m_action = W32ETA_CREATE_PROCESS; + + BOOL succ = SetEvent(m_threadControlEvent); + + if (succ) + { + DWORD ret = WaitForSingleObject(m_actionTakenEvent, INFINITE); + + LOG((LF_CORDB, LL_EVERYTHING, "Process Handle is: %x, m_threadControlEvent is %x\n", + (UINT_PTR)m_actionData.createData.lpProcessInformation->hProcess, (UINT_PTR)m_threadControlEvent)); + + if (ret == WAIT_OBJECT_0) + hr = m_actionResult; + else + hr = HRESULT_FROM_GetLastError(); + } + else + hr = HRESULT_FROM_GetLastError(); + + UnlockSendToWin32EventThreadMutex(); + + return hr; +} + + +//--------------------------------------------------------------------------------------- +// +// Create a process +// +// Assumptions: +// This occurs on the win32 event thread. It is invokved via +// a message sent from code:CordbWin32EventThread::SendCreateProcessEvent +// +// Notes: +// Create a new process. This is called in the context of the Win32 +// event thread to ensure that if we're Win32 debugging the process +// that the same thread that waits for debugging events will be the +// thread that creates the process. +// +//--------------------------------------------------------------------------------------- +void CordbWin32EventThread::CreateProcess() +{ + m_action = W32ETA_NONE; + HRESULT hr = S_OK; + + DWORD dwCreationFlags = m_actionData.createData.dwCreationFlags; + + // If the creation flags has DEBUG_PROCESS in them, then we're + // Win32 debugging this process. Otherwise, we have to create + // suspended to give us time to setup up our side of the IPC + // channel. + BOOL fInteropDebugging = +#if defined(FEATURE_INTEROP_DEBUGGING) + (dwCreationFlags & (DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS)); +#else + false; // Interop not supported. +#endif + + // Have Win32 create the process... + hr = m_pNativePipeline->CreateProcessUnderDebugger( + m_actionData.createData.machineInfo, + m_actionData.createData.programName, + m_actionData.createData.programArgs, + m_actionData.createData.lpProcessAttributes, + m_actionData.createData.lpThreadAttributes, + m_actionData.createData.bInheritHandles, + dwCreationFlags, + m_actionData.createData.lpEnvironment, + m_actionData.createData.lpCurrentDirectory, + m_actionData.createData.lpStartupInfo, + m_actionData.createData.lpProcessInformation); + + if (SUCCEEDED(hr)) + { + // Process ID is filled in after process is succesfully created. + DWORD dwProcessId = m_actionData.createData.lpProcessInformation->dwProcessId; + + RSUnsafeExternalSmartPtr<CordbProcess> pProcess; + hr = m_pShim->InitializeDataTarget(dwProcessId); + + if (SUCCEEDED(hr)) + { + // To emulate V2 semantics, we pass 0 for the clrInstanceID into + // OpenVirtualProcess. This will then connect to the first CLR + // loaded. + const ULONG64 cFirstClrLoaded = 0; + hr = CordbProcess::OpenVirtualProcess(cFirstClrLoaded, m_pShim->GetDataTarget(), NULL, m_cordb, dwProcessId, m_pShim, &pProcess); + } + + // Shouldn't happen on a create, only an attach + _ASSERTE(hr != CORDBG_E_DEBUGGER_ALREADY_ATTACHED); + + // Remember the process in the global list of processes. + if (SUCCEEDED(hr)) + { + EX_TRY + { + // Mark if we're interop-debugging + if (fInteropDebugging) + { + pProcess->EnableInteropDebugging(); + } + + m_cordb->AddProcess(pProcess); // will take ref if it succeeds + } + EX_CATCH_HRESULT(hr); + } + + // If we're Win32 attached to this process, then increment the + // proper count, otherwise add this process to the wait set + // and resume the process's main thread. + if (SUCCEEDED(hr)) + { + _ASSERTE(m_pProcess == NULL); + m_pProcess.Assign(pProcess); + } + } + + + // + // Signal the hr to the caller. + // + m_actionResult = hr; + SetEvent(m_actionTakenEvent); +} + + +// +// Send a DebugActiveProcess event to the Win32 thread to have it attach to +// a new process. +// +HRESULT CordbWin32EventThread::SendDebugActiveProcessEvent( + MachineInfo machineInfo, + DWORD pid, + bool fWin32Attach, + CordbProcess *pProcess) +{ + HRESULT hr = S_OK; + + LockSendToWin32EventThreadMutex(); + + m_actionData.attachData.machineInfo = machineInfo; + m_actionData.attachData.processId = pid; +#if !defined(FEATURE_DBGIPC_TRANSPORT_DI) + m_actionData.attachData.fWin32Attach = fWin32Attach; +#endif + m_actionData.attachData.pProcess = pProcess; + + // m_action is set last so that the win32 event thread can inspect + // it and take action without actually having to take any + // locks. The lock around this here is simply to prevent multiple + // threads from making requests at the same time. + m_action = W32ETA_ATTACH_PROCESS; + + BOOL succ = SetEvent(m_threadControlEvent); + + if (succ) + { + DWORD ret = WaitForSingleObject(m_actionTakenEvent, INFINITE); + + if (ret == WAIT_OBJECT_0) + hr = m_actionResult; + else + hr = HRESULT_FROM_GetLastError(); + } + else + hr = HRESULT_FROM_GetLastError(); + + UnlockSendToWin32EventThreadMutex(); + + return hr; +} + +//----------------------------------------------------------------------------- +// Is the given thread id a helper thread (real or worker?) +//----------------------------------------------------------------------------- +bool CordbProcess::IsHelperThreadWorked(DWORD tid) +{ + // Check against the id gained by sniffing Thread-Create events. + if (tid == this->m_helperThreadId) + { + return true; + } + + // Now check for potential datate in the IPC block. If not there, + // then we know it can't be the helper. + DebuggerIPCControlBlock * pDCB = this->GetDCB(); + + if (pDCB == NULL) + { + return false; + } + + // get the latest information from the LS DCB + UpdateRightSideDCB(); + return + (tid == pDCB->m_realHelperThreadId) || + (tid == pDCB->m_temporaryHelperThreadId); + +} + +//--------------------------------------------------------------------------------------- +// +// Cleans up the Left Side's DCB after a failed attach attempt. +// +// Assumptions: +// Called when the left-site failed initialization +// +// Notes: +// This can be called multiple times. +//--------------------------------------------------------------------------------------- +void CordbProcess::CleanupHalfBakedLeftSide() +{ + if (GetDCB() != NULL) + { + EX_TRY + { + GetDCB()->m_rightSideIsWin32Debugger = false; + UpdateLeftSideDCBField(&(GetDCB()->m_rightSideIsWin32Debugger), sizeof(GetDCB()->m_rightSideIsWin32Debugger)); + + if (m_pEventChannel != NULL) + { + m_pEventChannel->Delete(); + m_pEventChannel = NULL; + } + } + EX_CATCH + { + _ASSERTE(!"Writing process memory failed, perhaps due to an unexpected disconnection from the target."); + } + EX_END_CATCH(SwallowAllExceptions); + } + + // Close and null out the various handles and events, including our process handle m_handle. + CloseIPCHandles(); + + m_cordb.Clear(); + + // This process object is Dead-On-Arrival, so it doesn't really have anything to neuter. + // But for safekeeping, we'll mark it as neutered. + UnsafeNeuterDeadObject(); +} + + +//--------------------------------------------------------------------------------------- +// +// Attach to an existing process. +// +// +// Assumptions: +// Called on W32Event Thread, in response to event sent by +// code:CordbWin32EventThread::SendDebugActiveProcessEvent +// +// Notes: +// Attach to a process. This is called in the context of the Win32 +// event thread to ensure that if we're Win32 debugging the process +// that the same thread that waits for debugging events will be the +// thread that attaches the process. +// +// @dbgtodo shim: this will be part of the shim +//--------------------------------------------------------------------------------------- +void CordbWin32EventThread::AttachProcess() +{ + _ASSERTE(IsWin32EventThread()); + + RSUnsafeExternalSmartPtr<CordbProcess> pProcess; + + m_action = W32ETA_NONE; + + HRESULT hr = S_OK; + + DWORD dwProcessId = m_actionData.attachData.processId; + bool fNativeAttachSucceeded = false; + + + // Always do OS attach to the target. + // By this point, the pid should be valid (because OpenProcess above), pending some race where the process just exited. + // The OS will enforce that only 1 debugger is attached. + // Common failure paths here would be: access denied, double-attach + { + hr = m_pNativePipeline->DebugActiveProcess(m_actionData.attachData.machineInfo, + dwProcessId); + if (FAILED(hr)) + { + goto LExit; + } + fNativeAttachSucceeded = true; + } + + + hr = m_pShim->InitializeDataTarget(m_actionData.attachData.processId); + if (FAILED(hr)) + { + goto LExit; + } + + // To emulate V2 semantics, we pass 0 for the clrInstanceID into + // OpenVirtualProcess. This will then connect to the first CLR + // loaded. + { + const ULONG64 cFirstClrLoaded = 0; + hr = CordbProcess::OpenVirtualProcess(cFirstClrLoaded, m_pShim->GetDataTarget(), NULL, m_cordb, dwProcessId, m_pShim, &pProcess); + if (FAILED(hr)) + { + goto LExit; + } + } + + // Remember the process in the global list of processes. + // The caller back in code:Cordb::DebugActiveProcess will then get this by fetching it from the list. + + EX_TRY + { + // Mark interop-debugging + if (m_actionData.attachData.IsInteropDebugging()) + { + pProcess->EnableInteropDebugging(); // Throwing + } + + m_cordb->AddProcess(pProcess); // will take ref if it succeeds + + + // Queue fake Attach event for CreateProcess + { + PUBLIC_CALLBACK_IN_THIS_SCOPE0_NO_LOCK(pProcess); + m_pShim->BeginQueueFakeAttachEvents(); + } + } + EX_CATCH_HRESULT(hr); + if (FAILED(hr)) + { + goto LExit; + } + + _ASSERTE(m_pProcess == NULL); + m_pProcess.Assign(pProcess); + pProcess.Clear(); // ownership transfered to m_pProcess + + // Should have succeeded if we got to this point. + _ASSERTE(SUCCEEDED(hr)); + + +LExit: + if (FAILED(hr)) + { + // If we succeed to do a native-attach, but then failed elsewhere, try to native-detach. + if (fNativeAttachSucceeded) + { + m_pNativePipeline->DebugActiveProcessStop(dwProcessId); + } + + if (pProcess != NULL) + { + // Safe to call this even if the process wasn't added. + m_cordb->RemoveProcess(pProcess); + pProcess->CleanupHalfBakedLeftSide(); + pProcess.Clear(); + } + m_pProcess.Clear(); + } + + // + // Signal the hr to the caller. + // + m_actionResult = hr; + SetEvent(m_actionTakenEvent); +} + + +// Note that the actual 'DetachProcess' method is really ExitProcess with CW32ET_UNKNOWN_PROCESS_SLOT == +// processSlot +HRESULT CordbWin32EventThread::SendDetachProcessEvent(CordbProcess *pProcess) +{ + LOG((LF_CORDB, LL_INFO1000, "W32ET::SDPE\n")); + HRESULT hr = S_OK; + + LockSendToWin32EventThreadMutex(); + + m_actionData.detachData.pProcess = pProcess; + + // m_action is set last so that the win32 event thread can inspect it and take action without actually + // having to take any locks. The lock around this here is simply to prevent multiple threads from making + // requests at the same time. + m_action = W32ETA_DETACH; + + BOOL succ = SetEvent(m_threadControlEvent); + + if (succ) + { + DWORD ret = WaitForSingleObject(m_actionTakenEvent, INFINITE); + + if (ret == WAIT_OBJECT_0) + hr = m_actionResult; + else + hr = HRESULT_FROM_GetLastError(); + } + else + hr = HRESULT_FROM_GetLastError(); + + UnlockSendToWin32EventThreadMutex(); + + return hr; +} + +#ifdef FEATURE_INTEROP_DEBUGGING +// +// Send a UnmanagedContinue event to the Win32 thread to have it +// continue from an unmanged debug event. +// +HRESULT CordbWin32EventThread::SendUnmanagedContinue(CordbProcess *pProcess, + EUMContinueType eContType) +{ + HRESULT hr = S_OK; + + // If this were being called on the win32 EventThread, we'd deadlock. + _ASSERTE(!IsWin32EventThread()); + + // This can't hold the process lock, b/c we're making a cross-thread call, + // and our target will need the process lock. + _ASSERTE(!pProcess->ThreadHoldsProcessLock()); + + LockSendToWin32EventThreadMutex(); + + m_actionData.continueData.process = pProcess; + m_actionData.continueData.eContType = eContType; + + // m_action is set last so that the win32 event thread can inspect + // it and take action without actually having to take any + // locks. The lock around this here is simply to prevent multiple + // threads from making requests at the same time. + m_action = W32ETA_CONTINUE; + + BOOL succ = SetEvent(m_threadControlEvent); + + if (succ) + { + DWORD ret = WaitForSingleObject(m_actionTakenEvent, INFINITE); + + if (ret == WAIT_OBJECT_0) + hr = m_actionResult; + else + hr = HRESULT_FROM_GetLastError(); + } + else + hr = HRESULT_FROM_GetLastError(); + + UnlockSendToWin32EventThreadMutex(); + + return hr; +} + + +// +// Handle unmanaged continue. Continue an unmanaged debug +// event. Deferes to UnmanagedContinue. This is called in the context +// of the Win32 event thread to ensure that if we're Win32 debugging +// the process that the same thread that waits for debugging events +// will be the thread that continues the process. +// +void CordbWin32EventThread::HandleUnmanagedContinue() +{ + _ASSERTE(IsWin32EventThread()); + + m_action = W32ETA_NONE; + HRESULT hr = S_OK; + + // Continue the process + CordbProcess *pProcess = m_actionData.continueData.process; + + // If we lost the process object, we must have exited. + if (m_pProcess != NULL) + { + _ASSERTE(m_pProcess != NULL); + _ASSERTE(pProcess == m_pProcess); + + _ASSERTE(!pProcess->ThreadHoldsProcessLock()); + + RSSmartPtr<CordbProcess> proc(pProcess); + RSLockHolder ch(&pProcess->m_processMutex); + + hr = UnmanagedContinue(pProcess, m_actionData.continueData.eContType); + } + + // Signal the hr to the caller. + m_actionResult = hr; + SetEvent(m_actionTakenEvent); +} + +// +// Continue an unmanaged debug event. This is called in the context of the Win32 Event thread to ensure that the same +// thread that waits for debug events will be the thread that continues the process. +// +HRESULT CordbWin32EventThread::UnmanagedContinue(CordbProcess *pProcess, + EUMContinueType eContType) +{ + _ASSERTE(pProcess->ThreadHoldsProcessLock()); + _ASSERTE(IsWin32EventThread()); + _ASSERTE(m_pShim != NULL); + + HRESULT hr = S_OK; + + STRESS_LOG1(LF_CORDB, LL_INFO1000, "UM Continue. type=%d\n", eContType); + + if (eContType == cOobUMContinue) + { + _ASSERTE(pProcess->m_outOfBandEventQueue != NULL); + + // Dequeue the OOB event. + CordbUnmanagedEvent *ue = pProcess->m_outOfBandEventQueue; + CordbUnmanagedThread *ut = ue->m_owner; + pProcess->DequeueOOBUnmanagedEvent(ut); + + // Do a little extra work if that was an OOB exception event... + hr = ue->m_owner->FixupAfterOOBException(ue); + _ASSERTE(SUCCEEDED(hr)); + + // Continue from the event. + DoDbgContinue(pProcess, ue); + + // If there are more queued OOB events, dispatch them now. + if (pProcess->m_outOfBandEventQueue != NULL) + pProcess->DispatchUnmanagedOOBEvent(); + + // Note: if we previously skipped letting the entire process go on an IB continue due to a blocking OOB event, + // and if the OOB event queue is now empty, then go ahead and let the process continue now... + if ((pProcess->m_doRealContinueAfterOOBBlock == true) && + (pProcess->m_outOfBandEventQueue == NULL)) + goto doRealContinue; + } + else if (eContType == cInternalUMContinue) + { + // We're trying to get into a synced state which means we need the process running (potentially + // with some threads hijacked) in order to have the helper thread do the sync. + LOG((LF_CORDB, LL_INFO1000, "W32ET::UC: internal continue.\n")); + + if (!pProcess->GetSynchronized()) + { + LOG((LF_CORDB, LL_INFO1000, "W32ET::UC: internal continue, !sync'd.\n")); + pProcess->ResumeUnmanagedThreads(); + + // the event we may need to hijack and continue; + CordbUnmanagedEvent* pEvent = pProcess->m_lastQueuedUnmanagedEvent; + + // It is possible to be stopped at either an IB or an OOB event here. We only want to + // continue from an IB event here though + if(pProcess->m_state & CordbProcess::PS_WIN32_STOPPED && pEvent != NULL && + pEvent->IsEventWaitingForContinue()) + { + LOG((LF_CORDB, LL_INFO1000, "W32ET::UC: internal continue, frozen on IB event.\n")); + + // There should be a uncontinued IB event at the head of the queue + _ASSERTE(pEvent->IsIBEvent()); + _ASSERTE(!pEvent->IsEventContinuedUnhijacked()); + _ASSERTE(!pEvent->IsEventContinuedHijacked()); + + // Ensure that the event is hijacked now (it may not have been before) so that the + // thread does not slip forward during the sync process. After that we can safely continue + // it. + pProcess->HijackIBEvent(pEvent); + m_pShim->GetWin32EventThread()->DoDbgContinue(pProcess, pEvent); + } + } + + LOG((LF_CORDB, LL_INFO1000, "W32ET::UC: internal continue, done.\n")); + } + else + { + // If we're here, then we know 100% for sure that we've successfully gotten the managed continue event to the + // Left Side, so we can stop force hijacking left over in-band events now. Note: if we had hijacked any such + // events, they'll be dispatched below since they're properly queued. + pProcess->m_specialDeferment = false; + + // We don't actually do any work if there is an outstanding out-of-band event. When we do continue from the + // out-of-band event, we'll do this work, too. + if (pProcess->m_outOfBandEventQueue != NULL) + { + LOG((LF_CORDB, LL_INFO1000, "W32ET::UC: ignoring real continue due to block by out-of-band event(s).\n")); + + _ASSERTE(pProcess->m_doRealContinueAfterOOBBlock == false); + pProcess->m_doRealContinueAfterOOBBlock = true; + } + else + { +doRealContinue: + // This is either the Frozen -> Running transition or a + // Synced -> Running transition + _ASSERTE(pProcess->m_outOfBandEventQueue == NULL); + + + pProcess->m_doRealContinueAfterOOBBlock = false; + + LOG((LF_CORDB, LL_INFO1000, "W32ET::UC: continuing the process.\n")); + // Dispatch any more queued in-band events, or if there are none then just continue the process. + // + // Note: don't dispatch more events if we've already sent up the ExitProcess event... those events are just + // lost. + if ((pProcess->HasUndispatchedNativeEvents()) && (pProcess->m_exiting == false)) + { + pProcess->DispatchUnmanagedInBandEvent(); + } + else + { + // If the unmanaged event queue is empty now, and the process is synchronized, and there are queued + // managed events, then go ahead and get more managed events dispatched. + // + // Note: don't dispatch more events if we've already sent up the ExitProcess event... those events are + // just lost. + if (pProcess->GetSynchronized() && (!m_pShim->GetManagedEventQueue()->IsEmpty()) && (pProcess->m_exiting == false)) + { + if(pProcess->m_state & CordbProcess::PS_WIN32_STOPPED) + { + DoDbgContinue(pProcess, pProcess->m_lastDispatchedIBEvent); + + // This if should not be necessary, I am just being extra careful because this + // fix is going in late - see issue 818301 + _ASSERTE(pProcess->m_lastDispatchedIBEvent != NULL); + if(pProcess->m_lastDispatchedIBEvent != NULL) + { + pProcess->m_lastDispatchedIBEvent->m_owner->InternalRelease(); + pProcess->m_lastDispatchedIBEvent = NULL; + } + } + + // Now, get more managed events dispatched. + pProcess->SetSynchronized(false); + pProcess->MarkAllThreadsDirty(); + m_cordb->ProcessStateChanged(); + } + else + { + // free all the hijacked threads that hit native debug events + pProcess->ResumeHijackedThreads(); + + // after continuing the here the process should be running completely + // free... no hijacks, no suspended threads, and of course not frozen + if(pProcess->m_state & CordbProcess::PS_WIN32_STOPPED) + { + DoDbgContinue(pProcess, pProcess->m_lastDispatchedIBEvent); + // This if should not be necessary, I am just being extra careful because this + // fix is going in late - see issue 818301 + _ASSERTE(pProcess->m_lastDispatchedIBEvent != NULL); + if(pProcess->m_lastDispatchedIBEvent != NULL) + { + pProcess->m_lastDispatchedIBEvent->m_owner->InternalRelease(); + pProcess->m_lastDispatchedIBEvent = NULL; + } + } + } + } + + // Implicit Release on UT + } + } + + return hr; +} +#endif // FEATURE_INTEROP_DEBUGGING + +void ExitProcessWorkItem::Do() +{ + STRESS_LOG1(LF_CORDB, LL_INFO1000, "ExitProcessWorkItem proc=%p\n", GetProcess()); + + // This is being called on the RCET. + // That's the thread that dispatches managed events. Since it's calling us now, we know + // it can't be dispatching a managed event, and so we don't need to both waiting for it + + { + // Get the SG lock here to coordinate against any other continues. + RSLockHolder ch(GetProcess()->GetStopGoLock()); + RSLockHolder ch2(&(GetProcess()->m_processMutex)); + + LOG((LF_CORDB, LL_INFO1000,"W32ET::EP: ExitProcess callback\n")); + + // We're synchronized now, so mark the process as such. + GetProcess()->SetSynchronized(true); + GetProcess()->IncStopCount(); + + // By the time we release the SG + Process locks here, the process object has been + // marked as exiting + terminated (by the w32et which queued us). Future attemps to + // continue should fail, and thus we should remain synchronized. + } + + + // Just to be safe, neuter any children before the exit process callback. + { + RSLockHolder ch(GetProcess()->GetProcessLock()); + + // Release the process. + GetProcess()->NeuterChildren(); + } + + RSSmartPtr<Cordb> pCordb(NULL); + + // There is a race condition here where the debuggee process is killed while we are processing a process + // detach. We queue the process exit event for the Win32 event thread before queueing the process detach + // event. By the time this function is executed, we may have neutered the CordbProcess already as a + // result of code:CordbProcess::Detach. Detect that case here under the SG lock. + { + RSLockHolder ch(GetProcess()->GetStopGoLock()); + if (!GetProcess()->IsNeutered()) + { + _ASSERTE(GetProcess()->m_cordb != NULL); + pCordb.Assign(GetProcess()->m_cordb); + } + } + + // Move this into Shim? + + // Invoke the ExitProcess callback. This is very important since the a shell + // may rely on it for proper shutdown and may hang if they don't get it. + // We don't expect Cordbg to continue from this (we're certainly not going to wait for it). + if ((pCordb != NULL) && (pCordb->m_managedCallback != NULL)) + { + PUBLIC_CALLBACK_IN_THIS_SCOPE0_NO_LOCK(GetProcess()); + pCordb->m_managedCallback->ExitProcess(GetProcess()); + } + + // This CordbProcess object now has no reservations against a client calling ICorDebug::Terminate. + // That call may race against the CordbProcess::Neuter below, but since we already neutered the children, + // that neuter call will not do anything interesting that will conflict with Terminate. + + LOG((LF_CORDB, LL_INFO1000,"W32ET::EP: returned from ExitProcess callback\n")); + + { + RSLockHolder ch(GetProcess()->GetStopGoLock()); + + // Release the process. + GetProcess()->Neuter(); + } + + // Our dtor will release the Process object. + // This may be the final release on the process. +} + + +//--------------------------------------------------------------------------------------- +// +// Handles process exiting and detach cases +// +// Arguments: +// fDetach - true if detaching, false if process is exiting. +// +// Return Value: +// The type of the next argument in the signature, +// normalized. +// +// Assumptions: +// On exit, the process has already exited and we detected this by either an EXIT_PROCESS +// native debug event, or by waiting on the process handle. +// On detach, the process is stil live. +// +// Notes: +// ExitProcess is called when a process exits or detaches. +// This does our final cleanup and removes the process from our wait sets. +// We're either here because we're detaching (fDetach == TRUE), or because the process has really exited, +// and we're doing shutdown logic. +// +//--------------------------------------------------------------------------------------- +void CordbWin32EventThread::ExitProcess(bool fDetach) +{ + INTERNAL_API_ENTRY(this); + + // Consider the following when you're modifying this function: + // - The OS can kill the debuggee at any time. + // - ExitProcess can race with detach. + + LOG((LF_CORDB, LL_INFO1000,"W32ET::EP: begin ExitProcess, detach=%d\n", fDetach)); + + + // For the Mac remote debugging transport, DebugActiveProcessStop() is a nop. The transport will be + // shut down later when we neuter the CordbProcess. +#if !defined(FEATURE_DBGIPC_TRANSPORT_DI) + // @dbgtodo shim: this is a primitive workaround for interop-detach + // Eventually, the Debugger owns the detach pipeline, so this won't be necessary. + if (fDetach && (m_pProcess != NULL)) + { + HRESULT hr = m_pNativePipeline->DebugActiveProcessStop(m_pProcess->GetPid()); + + // We don't expect detach to fail (we check earlier for common conditions that + // may cause it to fail) + SIMPLIFYING_ASSUMPTION(SUCCEEDED(hr)); + if( FAILED(hr) ) + { + m_actionResult = hr; + SetEvent(m_actionTakenEvent); + return; + } + } +#endif // !FEATURE_DBGIPC_TRANSPORT_DI + + + // We don't really care if we're on the Win32 thread or not here. We just want to be sure that + // the LS Exit case and the Detach case both occur on the same thread. This makes it much easier + // to assert that if we exit while detaching, EP is only called once. + // If we ever decide to make the RCET listen on the LS process handle for EP(exit), then we should also + // make the EP(detach) handled on the RCET (via DoFavor() ). + _ASSERTE(IsWin32EventThread()); + + // So either the Exit case or Detach case must happen first. + // 1) If Detach first, then LS process is removed from wait set and so EP(Exit) will never happen + // because we check wait set after returning from EP(Detach). + // 2) If Exit is first, m_pProcess gets set=NULL. EP(detach) will still get called, so explicitly check that. + if (fDetach && ((m_pProcess == NULL) || m_pProcess->m_terminated)) + { + // m_terminated is only set after the LS exits. + // So the only way (fDetach && m_terminated) is true is if the LS exited while detaching. In that case + // we already called EP(exit) and we don't want to call it again for EP(detach). So return here. + LOG((LF_CORDB, LL_INFO1000,"W32ET::EP: In EP(detach), but EP(exit) already called. Early failure\n")); + + m_actionResult = CORDBG_E_PROCESS_TERMINATED; + SetEvent(m_actionTakenEvent); + + return; + } + + // We null m_pProcess at the end here, so + // Only way we could get here w/ null process is if we're called twice. We can only be called + // by detach or exit. Can't detach twice, can't exit twice, so must have been one of each. + // If exit is first, we got removed from the wait set, so 2nd call must be detach and we'd catch + // that above. If detach is first, we'd get removed from the wait set and so exit would never happen. + _ASSERTE(m_pProcess != NULL); + _ASSERTE(!m_pProcess->ThreadHoldsProcessLock()); + + + + // Mark the process teminated. After this, the RCET will never call FlushQueuedEvents. It will + // ignore all events it receives (including a SyncComplete) and the RCET also does not listen + // to terminated processes (so ProcessStateChange() won't cause a FQE either). + m_pProcess->Terminating(fDetach); + + // Take care of the race where the process exits right after the user calls Continue() from the last + // managed event but before the handler has actually returned. + // + // Also, To get through this lock means that either: + // 1. FlushQueuedEvents is not currently executing and no one will call FQE. + // 2. FQE is exiting but is in the middle of a callback (so AreDispatchingEvent = true) + // + m_pProcess->Lock(); + + m_pProcess->m_exiting = true; + + if (fDetach) + { + m_pProcess->SetSynchronized(false); + } + + // If we are exiting, we *must* dispatch the ExitProcess callback, but we will delete all the events + // in the queue and not bother dispatching anything else. If (and only if) we are currently dispatching + // an event, then we will wait while that event is finished before invoking ExitProcess. + // (Note that a dispatched event has already been removed from the queue) + + // Remove the process from the global list of processes. + m_cordb->RemoveProcess(m_pProcess); + + if (fDetach) + { + // Signal the hr to the caller. + LOG((LF_CORDB, LL_INFO1000,"W32ET::EP: Detach: send result back!\n")); + + m_actionResult = S_OK; + SetEvent(m_actionTakenEvent); + } + + m_pProcess->Unlock(); + + // Delete all queued events + m_pProcess->DeleteQueuedEvents(); + + + // If we're detaching, then the Detach already neutered everybody, so nothing here. + // If we're exiting, then we still need to neuter things, but we can't do that on this thread, + // so we queue it. We also need to dispatch an exit process callback. We'll queue that onto the RCET + // and dispatch it inband w/the other callbacks. + if (!fDetach) + { +#ifdef FEATURE_PAL + // Cleanup the transport pipe and semaphore files that might be left by the target (LS) process. + m_pNativePipeline->CleanupTargetProcess(); +#endif + ExitProcessWorkItem * pItem = new (nothrow) ExitProcessWorkItem(m_pProcess); + if (pItem != NULL) + { + m_cordb->m_rcEventThread->QueueAsyncWorkItem(pItem); + } + } + + // This will remove the process from our wait lists (so that we don't send multiple ExitProcess events). + m_pProcess.Clear(); +} + + +// +// Start actually creates and starts the thread. +// +HRESULT CordbWin32EventThread::Start() +{ + HRESULT hr = S_OK; + if (m_threadControlEvent == NULL) + return E_INVALIDARG; + + // Create the thread suspended to make sure that m_threadId is set + // before CordbWin32EventThread::ThreadProc runs + // Stack size = 0x80000 = 512KB + m_thread = CreateThread(NULL, 0x80000, &CordbWin32EventThread::ThreadProc, + (LPVOID) this, CREATE_SUSPENDED | STACK_SIZE_PARAM_IS_A_RESERVATION, &m_threadId); + + if (m_thread == NULL) + return HRESULT_FROM_GetLastError(); + + DWORD succ = ResumeThread(m_thread); + if (succ == (DWORD)-1) + return HRESULT_FROM_GetLastError(); + return hr; +} + + +// +// Stop causes the thread to stop receiving events and exit. It +// waits for it to exit before returning. +// +HRESULT CordbWin32EventThread::Stop() +{ + HRESULT hr = S_OK; + + // m_pProcess may be NULL from CordbWin32EventThread::ExitProcess + + // Can't block on W32ET while holding the process-lock since the W32ET may need that to exit. + // But since m_pProcess may be null, we can't enforce that. + + if (m_thread != NULL) + { + LockSendToWin32EventThreadMutex(); + m_action = W32ETA_NONE; + m_run = FALSE; + + SetEvent(m_threadControlEvent); + UnlockSendToWin32EventThreadMutex(); + + DWORD ret = WaitForSingleObject(m_thread, INFINITE); + + if (ret != WAIT_OBJECT_0) + hr = HRESULT_FROM_GetLastError(); + } + + m_pProcess.Clear(); + m_cordb.Clear(); + + return hr; +} + + + + + + + + +// Allocate a buffer of cbBuffer bytes in the target. +// +// Arguments: +// cbBuffer - count of bytes for the buffer. +// +// Returns: +// a TargetBuffer describing the new memory region in the target. +// Throws on error. +TargetBuffer CordbProcess::GetRemoteBuffer(ULONG cbBuffer) +{ + INTERNAL_SYNC_API_ENTRY(this); // + + // Create and initialize the event as synchronous + DebuggerIPCEvent event; + InitIPCEvent(&event, + DB_IPCE_GET_BUFFER, + true, + VMPTR_AppDomain::NullPtr()); + + // Indicate the buffer size wanted + event.GetBuffer.bufSize = cbBuffer; + + // Make the request, which is synchronous + HRESULT hr = SendIPCEvent(&event, sizeof(event)); + IfFailThrow(hr); + _ASSERTE(event.type == DB_IPCE_GET_BUFFER_RESULT); + + IfFailThrow(event.GetBufferResult.hr); + + // The request succeeded. Return the newly allocated range. + return TargetBuffer(event.GetBufferResult.pBuffer, cbBuffer); +} + +/* + * This will release a previously allocated left side buffer. + */ +HRESULT CordbProcess::ReleaseRemoteBuffer(void **ppBuffer) +{ + INTERNAL_SYNC_API_ENTRY(this); // + + _ASSERTE(m_pShim != NULL); + + // Create and initialize the event as synchronous + DebuggerIPCEvent event; + InitIPCEvent(&event, + DB_IPCE_RELEASE_BUFFER, + true, + VMPTR_AppDomain::NullPtr()); + + // Indicate the buffer to release + event.ReleaseBuffer.pBuffer = (*ppBuffer); + + // Make the request, which is synchronous + HRESULT hr = SendIPCEvent(&event, sizeof(event)); + TESTANDRETURNHR(hr); + + (*ppBuffer) = NULL; + + // Indicate success + return event.ReleaseBufferResult.hr; +} + +HRESULT CordbProcess::SetDesiredNGENCompilerFlags(DWORD dwFlags) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + +#if defined(FEATURE_PREJIT) + if ((dwFlags != CORDEBUG_JIT_DEFAULT) && (dwFlags != CORDEBUG_JIT_DISABLE_OPTIMIZATION)) + { + return E_INVALIDARG; + } + + CordbProcess *pProcess = GetProcess(); + ATT_REQUIRE_STOPPED_MAY_FAIL(pProcess); + HRESULT hr = S_OK; + EX_TRY + { + // Left-side checks that this is a valid time to set the Ngen flags. + hr = pProcess->GetDAC()->SetNGENCompilerFlags(dwFlags); + if (!SUCCEEDED(hr) && GetShim() != NULL) + { + // Emulate V2 error semantics. + hr = GetShim()->FilterSetNgenHresult(hr); + } + } + EX_CATCH_HRESULT(hr); + return hr; + +#else // !FEATURE_PREJIT + return CORDBG_E_NGEN_NOT_SUPPORTED; + +#endif // FEATURE_PREJIT +} + +HRESULT CordbProcess::GetDesiredNGENCompilerFlags(DWORD *pdwFlags ) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + VALIDATE_POINTER_TO_OBJECT(pdwFlags, DWORD*); + *pdwFlags = 0; + + CordbProcess *pProcess = GetProcess(); + ATT_REQUIRE_STOPPED_MAY_FAIL(pProcess); + HRESULT hr = S_OK; + EX_TRY + { + hr = pProcess->GetDAC()->GetNGENCompilerFlags(pdwFlags); + } + EX_CATCH_HRESULT(hr); + return hr; +} + +//----------------------------------------------------------------------------- +// Get an ICorDebugReference Value for the GC handle. +// handle - raw bits for the GC handle. +// pOutHandle +//----------------------------------------------------------------------------- +HRESULT CordbProcess::GetReferenceValueFromGCHandle( + UINT_PTR gcHandle, + ICorDebugReferenceValue **pOutValue) +{ + PUBLIC_API_ENTRY(this); + FAIL_IF_NEUTERED(this); + ATT_REQUIRE_STOPPED_MAY_FAIL(this); + VALIDATE_POINTER_TO_OBJECT(pOutValue, ICorDebugReferenceValue*); + + *pOutValue = NULL; + HRESULT hr = S_OK; + + EX_TRY + { + if (gcHandle == NULL) + { + ThrowHR(CORDBG_E_BAD_REFERENCE_VALUE); + } + + IDacDbiInterface* pDAC = GetProcess()->GetDAC(); + VMPTR_OBJECTHANDLE vmObjHandle = pDAC->GetVmObjectHandle(gcHandle); + if(!pDAC->IsVmObjectHandleValid(vmObjHandle)) + { + ThrowHR(CORDBG_E_BAD_REFERENCE_VALUE); + } + ULONG appDomainId = pDAC->GetAppDomainIdFromVmObjectHandle(vmObjHandle); + VMPTR_AppDomain vmAppDomain = pDAC->GetAppDomainFromId(appDomainId); + + RSLockHolder lockHolder(GetProcessLock()); + CordbAppDomain * pAppDomain = LookupOrCreateAppDomain(vmAppDomain); + lockHolder.Release(); + + // Now that we finally have the AppDomain, we can go ahead and get a ReferenceValue + // from the ObjectHandle. + hr = CordbReferenceValue::BuildFromGCHandle(pAppDomain, vmObjHandle, pOutValue); + _ASSERTE(SUCCEEDED(hr) == (*pOutValue != NULL)); + IfFailThrow(hr); + } + EX_CATCH_HRESULT(hr); + return hr; +} + +//----------------------------------------------------------------------------- +// Return count of outstanding GC handles held by CordbHandleValue objects +LONG CordbProcess::OutstandingHandles() +{ + return m_cOutstandingHandles; +} + +//----------------------------------------------------------------------------- +// Increment the outstanding handle count for code:CordbProces::OutstandingHandles +// This is the inverse of code:CordbProces::DecrementOutstandingHandles +void CordbProcess::IncrementOutstandingHandles() +{ + _ASSERTE(ThreadHoldsProcessLock()); + m_cOutstandingHandles++; +} + +//----------------------------------------------------------------------------- +// Decrement the outstanding handle count for code:CordbProces::OutstandingHandles +// This is the inverse of code:CordbProces::IncrementOutstandingHandles +void CordbProcess::DecrementOutstandingHandles() +{ + _ASSERTE(ThreadHoldsProcessLock()); + m_cOutstandingHandles--; +} + + +/* + * IsReadyForDetach + * + * This method encapsulates all logic for deciding if it is ok for a debugger to + * detach from the process at this time. + * + * Parameters: None. + * + * Returns: S_OK if it is ok to detach, else a specific HRESULT describing why it + * is not ok to detach. + * + */ +HRESULT CordbProcess::IsReadyForDetach() +{ + INTERNAL_API_ENTRY(this); + + // Always safe to detach in V3 case. + if (m_pShim == NULL) + { + return S_OK; + } + + // If not initialized yet, then there are no detach liabilities. + if (!m_initialized) + { + return S_OK; + } + + RSLockHolder lockHolder(&this->m_processMutex); + + // + // If there are any outstanding func-evals then fail the detach. + // + if (OutstandingEvalCount() != 0) + { + return CORDBG_E_DETACH_FAILED_OUTSTANDING_EVALS; + } + + // V2 didn't check outstanding handles (code:CordbProcess::OutstandingHandles) + // because it could automatically clean those up on detach. + + // + // If there are any outstanding steppers then fail the detach. + // + if (m_steppers.IsInitialized() && (m_steppers.GetCount() > 0)) + { + return CORDBG_E_DETACH_FAILED_OUTSTANDING_STEPPERS; + } + + // + // If there are any outstanding breakpoints then fail the detach. + // + HASHFIND foundAppDomain; + CordbAppDomain *pAppDomain = m_appDomains.FindFirst(&foundAppDomain); + + while (pAppDomain != NULL) + { + if (pAppDomain->m_breakpoints.IsInitialized() && (pAppDomain->m_breakpoints.GetCount() > 0)) + { + return CORDBG_E_DETACH_FAILED_OUTSTANDING_BREAKPOINTS; + } + + // Check for any outstanding EnC modules. + HASHFIND foundModule; + CordbModule * pModule = pAppDomain->m_modules.FindFirst(&foundModule); + while (pModule != NULL) + { + if (pModule->m_EnCCount > 0) + { + return CORDBG_E_DETACH_FAILED_ON_ENC; + } + pModule = pAppDomain->m_modules.FindNext(&foundModule); + } + + + pAppDomain = m_appDomains.FindNext(&foundAppDomain); + } + + // If we're using the shim, give a chance to early-out if the OS doesn't support detach + // so that the user can continue to debug in that case. + // Ideally we'd just rely on the failure from DebugActiveProcessStop, but by then it's too late + // to recover. This function is our only chance to distinguish between graceful detach failures + // and hard detach failures (after which the process object is neutered). + if (m_pShim != NULL) + { +#if !defined(FEATURE_CORESYSTEM) // CORESYSTEM TODO + HModuleHolder hKernel32; + hKernel32 = WszLoadLibrary(W("kernel32")); + if (hKernel32 == NULL) + return HRESULT_FROM_GetLastError(); + typedef BOOL (*DebugActiveProcessStopSig) (DWORD); + DebugActiveProcessStopSig pDebugActiveProcessStop = + reinterpret_cast<DebugActiveProcessStopSig>(GetProcAddress(hKernel32, "DebugActiveProcessStop")); + if (pDebugActiveProcessStop == NULL) + return COR_E_PLATFORMNOTSUPPORTED; +#endif + } + + return S_OK; +} + + +/* + * Look for any thread which was last seen in the specified AppDomain. + * The CordbAppDomain object is about to be neutered due to an AD Unload + * So the thread must no longer be considered to be in that domain. + * Note that this is a workaround due to the existance of the (possibly incorrect) + * cached AppDomain value. Ideally we would remove the cached value entirely + * and there would be no need for this. + * + * @dbgtodo: , appdomain: We should remove CordbThread::m_pAppDomain in the V3 architecture. + * If we need the thread's current domain, we should get it accurately with DAC. + */ +void CordbProcess::UpdateThreadsForAdUnload(CordbAppDomain * pAppDomain) +{ + INTERNAL_API_ENTRY(this); + + // If we're doing an AD unload then we should have already seen the ATTACH + // notification for the default domain. + //_ASSERTE( m_pDefaultAppDomain != NULL ); + // @dbgtodo appdomain: fix Default domain invariants with DAC-izing Appdomain work. + + RSLockHolder lockHolder(GetProcessLock()); + + CordbThread* t; + HASHFIND find; + + // We don't need to prepopulate here (to collect LS state) because we're just updating RS state. + for (t = m_userThreads.FindFirst(&find); + t != NULL; + t = m_userThreads.FindNext(&find)) + { + if( t->GetAppDomain() == pAppDomain ) + { + // This thread cannot actually be in this AppDomain anymore (since it's being + // unloaded). Reset it to point to the default AppDomain + t->m_pAppDomain = m_pDefaultAppDomain; + } + } +} + +// CordbProcess::LookupClass +// Looks up a previously constructed CordbClass instance without creating. May return NULL if the +// CordbClass instance doesn't exist. +// Argument: (in) vmDomainFile - pointer to the domainfile for the module +// (in) mdTypeDef - metadata token for the class +// Return value: pointer to a previously created CordbClass instance or NULL in none exists +CordbClass * CordbProcess::LookupClass(ICorDebugAppDomain * pAppDomain, VMPTR_DomainFile vmDomainFile, mdTypeDef classToken) +{ + _ASSERTE(ThreadHoldsProcessLock()); + + if (pAppDomain != NULL) + { + CordbModule * pModule = ((CordbAppDomain *)pAppDomain)->m_modules.GetBase(VmPtrToCookie(vmDomainFile)); + if (pModule != NULL) + { + return pModule->LookupClass(classToken); + } + } + return NULL; +} // CordbProcess::LookupClass + +//--------------------------------------------------------------------------------------- +// Look for a specific module in the process. +// +// Arguments: +// vmDomainFile - non-null module to lookup +// +// Returns: +// a CordbModule object for the given cookie. Object may be from the cache, or created +// lazily. +// Never returns null. Throws on error. +// +// Notes: +// A VMPTR_DomainFile has appdomain affinity, but is ultimately scoped to a process. +// So if we get a raw VMPTR_DomainFile (eg, from the stackwalker or from some other +// lookup function), then we need to do a process wide lookup since we don't know which +// appdomain it's in. If you know the appdomain, you can use code:CordbAppDomain::LookupOrCreateModule. +// +CordbModule * CordbProcess::LookupOrCreateModule(VMPTR_DomainFile vmDomainFile) +{ + INTERNAL_API_ENTRY(this); + + RSLockHolder lockHolder(GetProcess()->GetProcessLock()); + _ASSERTE(!vmDomainFile.IsNull()); + + DomainFileInfo data; + GetDAC()->GetDomainFileData(vmDomainFile, &data); // throws + + CordbAppDomain * pAppDomain = LookupOrCreateAppDomain(data.vmAppDomain); + return pAppDomain->LookupOrCreateModule(vmDomainFile); +} + +//--------------------------------------------------------------------------------------- +// Determine if the process has any in-band queued events which have not been dispatched +// +// Returns: +// TRUE iff there are undispatched IB events +// +#ifdef FEATURE_INTEROP_DEBUGGING +BOOL CordbProcess::HasUndispatchedNativeEvents() +{ + INTERNAL_API_ENTRY(this); + + CordbUnmanagedEvent* pEvent = m_unmanagedEventQueue; + while(pEvent != NULL && pEvent->IsDispatched()) + { + pEvent = pEvent->m_next; + } + + return pEvent != NULL; +} +#endif + +//--------------------------------------------------------------------------------------- +// Determine if the process has any in-band queued events which have not been user continued +// +// Returns: +// TRUE iff there are user uncontinued IB events +// +#ifdef FEATURE_INTEROP_DEBUGGING +BOOL CordbProcess::HasUserUncontinuedNativeEvents() +{ + INTERNAL_API_ENTRY(this); + + CordbUnmanagedEvent* pEvent = m_unmanagedEventQueue; + while(pEvent != NULL && pEvent->IsEventUserContinued()) + { + pEvent = pEvent->m_next; + } + + return pEvent != NULL; +} +#endif + +//--------------------------------------------------------------------------------------- +// Hijack the thread which had this event. If the thread is already hijacked this method +// has no effect. +// +// Arguments: +// pUnmanagedEvent - the debug event which requires us to hijack +// +// Returns: +// S_OK on success, failing HRESULT if the hijack could not be set up +// +#ifdef FEATURE_INTEROP_DEBUGGING +HRESULT CordbProcess::HijackIBEvent(CordbUnmanagedEvent * pUnmanagedEvent) +{ + // Can't hijack after the event has already been continued hijacked + _ASSERTE(!pUnmanagedEvent->IsEventContinuedHijacked()); + // Can only hijack IB events + _ASSERTE(pUnmanagedEvent->IsIBEvent()); + + // If we already hijacked the event then there is nothing left to do + if(pUnmanagedEvent->m_owner->IsFirstChanceHijacked() || + pUnmanagedEvent->m_owner->IsGenericHijacked()) + { + return S_OK; + } + + ResetEvent(this->m_leftSideUnmanagedWaitEvent); + if (pUnmanagedEvent->m_currentDebugEvent.u.Exception.dwFirstChance) + { + HRESULT hr = pUnmanagedEvent->m_owner->SetupFirstChanceHijackForSync(); + SIMPLIFYING_ASSUMPTION(SUCCEEDED(hr)); + return hr; + } + else // Second chance exceptions must be generic hijacked. + { + HRESULT hr = pUnmanagedEvent->m_owner->SetupGenericHijack(pUnmanagedEvent->m_currentDebugEvent.dwDebugEventCode, &pUnmanagedEvent->m_currentDebugEvent.u.Exception.ExceptionRecord); + SIMPLIFYING_ASSUMPTION(SUCCEEDED(hr)); + return hr; + } +} +#endif + +// Sets a bitfield reflecting the managed debugging state at the time of +// the jit attach. +HRESULT CordbProcess::GetAttachStateFlags(CLR_DEBUGGING_PROCESS_FLAGS *pFlags) +{ + HRESULT hr = S_OK; + PUBLIC_REENTRANT_API_BEGIN(this) + { + if(pFlags == NULL) + hr = E_POINTER; + else + *pFlags = GetDAC()->GetAttachStateFlags(); + } + PUBLIC_API_END(hr); + + return hr; +} + +// Determine if this version of ICorDebug is compatibile with the ICorDebug in the specified major CLR version +bool CordbProcess::IsCompatibleWith(DWORD clrMajorVersion) +{ + // The debugger versioning policy is that debuggers generally need to opt-in to supporting major new + // versions of the CLR. Often new versions of the CLR violate some invariant that previous debuggers assume + // (eg. hot/cold splitting in Whidbey, multiple CLRs in a process in CLR v4), and neither VS or the CLR + // teams generally want the support burden of forward compatibility. + + // + // If this assert is firing for you, its probably because the major version + // number of the clr.dll has changed. This assert is here to remind you to do a bit of other + // work you may not have realized you needed to do so that our versioning works smoothly + // for debugging. You probably want to contact the CLR DST team if you are a + // non-debugger person hitting this. DON'T JUST DELETE THIS ASSERT!!! + // + // 1) You should ensure new versions of all ICorDebug users in DevDiv (VS Debugger, MDbg, etc.) + // are using a creation path that explicitly specifies that they support this new major + // version of the CLR. + // 2) You should file an issue to track blocking earlier debuggers from targetting this + // version of the CLR (i.e. update requiredVersion to the new CLR major + // version). To enable a smooth internal transition, this often isn't done until absolutely + // necessary (sometimes as late as Beta2). + // 3) You can consider updating the CLR_ID guid used by the shim to recognize a CLR, but only + // if it's important to completely hide newer CLRs from the shim. The expectation now + // is that we won't need to do this (i.e. we'd like VS to give a nice error message about + // needed a newer version of the debugger, rather than just acting as if a process has no CLR). + // 4) Update this assert so that it no longer fires for your new CLR version or any of + // the previous versions, but don't delete the assert... + // the next CLR version after yours will probably need the same reminder + + _ASSERTE_MSG(clrMajorVersion <= 4, + "Found major CLR version greater than 4 in mscordbi.dll from CLRv4 - contact CLRDST"); + + // This knob lets us enable forward compatibility for internal scenarios, and also simulate new + // versions of the runtime for testing the failure user-experience in a version of the debugger + // before it is shipped. + // We don't want to risk customers getting this, so for RTM builds this must be CHK-only. + // To aid in internal transition, we may temporarily enable this in RET builds, but when + // doing so must file a bug to track making it CHK only again before RTM. + // For example, Dev10 Beta2 shipped with this knob, but it was made CHK-only at the start of RC. + // In theory we might have a point release someday where we break debugger compat, but + // it seems unlikely and since this knob is unsupported anyway we can always extend it + // then (support reading a string value, etc.). So for now we just map the number + // to the major CLR version number. + DWORD requiredVersion = 0; +#ifdef _DEBUG + requiredVersion = CLRConfig::GetConfigValue(CLRConfig::UNSUPPORTED_Debugging_RequiredVersion); +#endif + + // If unset (the only supported configuration), then we require a debugger designed for CLRv4 + // for desktop, where we do not allow forward compat. + // For SL, we allow forward compat. Right now, that means SLv2+ debugger requests can be + // honored for SLv4. + if (requiredVersion <= 0) + { +#if defined(FEATURE_CORECLR) + requiredVersion = 2; +#else + requiredVersion = 4; +#endif + } + + // Compare the version we were created for against the minimum required + return (clrMajorVersion >= requiredVersion); +} + +bool CordbProcess::IsThreadSuspendedOrHijacked(ICorDebugThread * pICorDebugThread) +{ + // An RS lock can be held while this is called. Specifically, + // CordbThread::EnumerateChains may be on the stack, and it uses + // ATT_REQUIRE_STOPPED_MAY_FAIL, which holds the CordbProcess::m_StopGoLock lock for + // its entire duration. As a result, this needs to be considered a reentrant API. See + // comments above code:PrivateShimCallbackHolder for more info. + PUBLIC_REENTRANT_API_ENTRY_FOR_SHIM(this); + + CordbThread * pCordbThread = static_cast<CordbThread *> (pICorDebugThread); + return GetDAC()->IsThreadSuspendedOrHijacked(pCordbThread->m_vmThreadToken); +} |