// 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 #include // @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 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 (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 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 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(&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 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 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(&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 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(this); } else if (id == IID_ICorDebugController) { *pInterface = static_cast(static_cast(this)); } else if (id == IID_ICorDebugProcess2) { *pInterface = static_cast(this); } else if (id == IID_ICorDebugProcess3) { *pInterface = static_cast(this); } else if (id == IID_ICorDebugProcess4) { *pInterface = static_cast(this); } else if (id == IID_ICorDebugProcess5) { *pInterface = static_cast(this); } else if (id == IID_ICorDebugProcess7) { *pInterface = static_cast(this); } else if (id == IID_ICorDebugProcess8) { *pInterface = static_cast(this); } #ifdef FEATURE_LEGACYNETCF_DBG_HOST_CONTROL else if (id == IID_ICorDebugLegacyNetCFHostCallbackInvoker_PrivateWindowsPhoneOnly) { *pInterface = static_cast(this); } #endif else if (id == IID_IUnknown) { *pInterface = static_cast(static_cast(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 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 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(this); } else if (riid == IID_IUnknown) { *ppInterface = static_cast(static_cast(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(this); } else if (riid == IID_IUnknown) { *ppInterface = static_cast(static_cast(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 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 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 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 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(static_cast (pBreakpoint)); break; case CBT_MODULE: return static_cast(static_cast (pBreakpoint)); break; case CBT_VALUE: return static_cast(static_cast (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* 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 (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* 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* pAssemblies, ULONG countAssemblies) { PUBLIC_API_ENTRY_FOR_SHIM(this); RSLockHolder lockHolder(GetProcessLock()); _ASSERTE(GetShim() != NULL); CordbAppDomain * pAppDomainInternal = static_cast (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* 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 (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* 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* pModules, ULONG countModules) { PUBLIC_API_ENTRY_FOR_SHIM(this); RSLockHolder lockHolder(GetProcessLock()); _ASSERTE(GetShim() != NULL); CordbAssembly * pAssemblyInternal = static_cast (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(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(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((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 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 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(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 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 (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 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 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 (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 (pEvent->FirstLogMessage.szCategory.GetString()), const_cast (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 (pstrLogSwitchName), const_cast (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 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 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 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 pRefCurFunction(pCurFunction); RSSmartPtr 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 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 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 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 (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 * 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 pEnum; InternalEnumerateThreads(pEnum.GetAddr()); pEnum.TransferOwnershipExternal(ppThreads); } PUBLIC_API_END(hr); return hr; } // Internal implementation of EnumerateThreads VOID CordbProcess::InternalEnumerateThreads(RSInitHolder *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 (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 (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(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++) { // @todo port: we're making assumptions about the size of opcodes,address pointers, etc 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; ut = NULL; 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 _ASSERTE(pUnmanagedThread == NULL); pUnmanagedThread = pUnmanagedEvent->m_owner; _ASSERTE(pUnmanagedThread != NULL); // 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 (&(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 // 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 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 (&(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(&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 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 (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 pEnum; CordbHashTableEnum::BuildOrThrow( this, GetContinueNeuterList(), &m_appDomains, IID_ICorDebugAppDomainEnum, pEnum.GetAddr()); *ppAppDomains = static_cast (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 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(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 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 pCallback2; pCallback->QueryInterface(IID_ICorDebugManagedCallback2, reinterpret_cast (&pCallback2)); RSExtSmartPtr pCallback3; pCallback->QueryInterface(IID_ICorDebugManagedCallback3, reinterpret_cast (&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 * 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 InterlockedStack::InterlockedStack() { m_pHead = NULL; } template InterlockedStack::~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 void InterlockedStack::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 T * InterlockedStack::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 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 (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(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 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 (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 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 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 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 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(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 (pICorDebugThread); return GetDAC()->IsThreadSuspendedOrHijacked(pCordbThread->m_vmThreadToken); }