From e3684bce478a5ee8313312b2f588761a44049584 Mon Sep 17 00:00:00 2001 From: Xu Liangyu Date: Thu, 1 Aug 2024 12:01:22 +0800 Subject: [PATCH] [LoongArch64] Initial DFEATURE_EMULATE_SINGLESTEP for LA64. (#105741) --- src/coreclr/clrdefinitions.cmake | 2 + src/coreclr/debug/ee/controller.cpp | 4 +- src/coreclr/debug/ee/debugger.cpp | 2 +- .../debug/ee/loongarch64/primitives.cpp | 22 + .../debug/inc/loongarch64/primitives.h | 23 +- src/coreclr/vm/CMakeLists.txt | 1 + src/coreclr/vm/loongarch64/singlestepper.cpp | 492 ++++++++++++++++++ src/coreclr/vm/loongarch64singlestepper.h | 87 ++++ src/coreclr/vm/threads.h | 7 +- 9 files changed, 620 insertions(+), 20 deletions(-) create mode 100644 src/coreclr/vm/loongarch64/singlestepper.cpp create mode 100644 src/coreclr/vm/loongarch64singlestepper.h diff --git a/src/coreclr/clrdefinitions.cmake b/src/coreclr/clrdefinitions.cmake index ec09ee61ae1..0a7a73bd4f1 100644 --- a/src/coreclr/clrdefinitions.cmake +++ b/src/coreclr/clrdefinitions.cmake @@ -28,6 +28,8 @@ if (CLR_CMAKE_TARGET_UNIX) add_compile_definitions($<$>>:UNIX_ARM_ABI>) elseif (CLR_CMAKE_TARGET_ARCH_I386) add_compile_definitions($<$>>:UNIX_X86_ABI>) + elseif (CLR_CMAKE_TARGET_ARCH_LOONGARCH64) + add_definitions(-DFEATURE_EMULATE_SINGLESTEP) endif() endif(CLR_CMAKE_TARGET_UNIX) diff --git a/src/coreclr/debug/ee/controller.cpp b/src/coreclr/debug/ee/controller.cpp index f1fc3c00f4e..e929aa8bd3f 100644 --- a/src/coreclr/debug/ee/controller.cpp +++ b/src/coreclr/debug/ee/controller.cpp @@ -3202,7 +3202,7 @@ void DebuggerController::ApplyTraceFlag(Thread *thread) g_pEEInterface->MarkThreadForDebugStepping(thread, true); LOG((LF_CORDB,LL_INFO1000, "DC::ApplyTraceFlag marked thread for debug stepping\n")); - SetSSFlag(reinterpret_cast(context) ARM_ARG(thread) ARM64_ARG(thread) RISCV64_ARG(thread)); + SetSSFlag(reinterpret_cast(context) ARM_ARG(thread) ARM64_ARG(thread) RISCV64_ARG(thread) LOONGARCH64_ARG(thread)); } // @@ -3239,7 +3239,7 @@ void DebuggerController::UnapplyTraceFlag(Thread *thread) // Always need to unmark for stepping g_pEEInterface->MarkThreadForDebugStepping(thread, false); - UnsetSSFlag(reinterpret_cast(context) ARM_ARG(thread) ARM64_ARG(thread) RISCV64_ARG(thread)); + UnsetSSFlag(reinterpret_cast(context) ARM_ARG(thread) ARM64_ARG(thread) RISCV64_ARG(thread) LOONGARCH64_ARG(thread)); } void DebuggerController::EnableExceptionHook() diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index 0c38d9c93e2..8ed914cf310 100644 --- a/src/coreclr/debug/ee/debugger.cpp +++ b/src/coreclr/debug/ee/debugger.cpp @@ -15890,7 +15890,7 @@ BOOL Debugger::IsThreadContextInvalid(Thread *pThread, CONTEXT *pCtx) if (success) { // Check single-step flag - if (IsSSFlagEnabled(reinterpret_cast(pCtx) ARM_ARG(pThread) ARM64_ARG(pThread) RISCV64_ARG(pThread))) + if (IsSSFlagEnabled(reinterpret_cast(pCtx) ARM_ARG(pThread) ARM64_ARG(pThread) RISCV64_ARG(pThread) LOONGARCH64_ARG(pThread))) { // Can't hijack a thread whose SS-flag is set. This could lead to races // with the thread taking the SS-exception. diff --git a/src/coreclr/debug/ee/loongarch64/primitives.cpp b/src/coreclr/debug/ee/loongarch64/primitives.cpp index bef18245f06..981809a4bac 100644 --- a/src/coreclr/debug/ee/loongarch64/primitives.cpp +++ b/src/coreclr/debug/ee/loongarch64/primitives.cpp @@ -10,3 +10,25 @@ void CopyREGDISPLAY(REGDISPLAY* pDst, REGDISPLAY* pSrc) CONTEXT tmp; CopyRegDisplay(pSrc, pDst, &tmp); } + +void SetSSFlag(DT_CONTEXT *, Thread *pThread) +{ + _ASSERTE(pThread != NULL); + + pThread->EnableSingleStep(); +} + +void UnsetSSFlag(DT_CONTEXT *, Thread *pThread) +{ + _ASSERTE(pThread != NULL); + + pThread->DisableSingleStep(); +} + +// Check if single stepping is enabled. +bool IsSSFlagEnabled(DT_CONTEXT *, Thread *pThread) +{ + _ASSERTE(pThread != NULL); + + return pThread->IsSingleStepEnabled(); +} diff --git a/src/coreclr/debug/inc/loongarch64/primitives.h b/src/coreclr/debug/inc/loongarch64/primitives.h index 750f8a617c1..22c9247589f 100644 --- a/src/coreclr/debug/inc/loongarch64/primitives.h +++ b/src/coreclr/debug/inc/loongarch64/primitives.h @@ -219,24 +219,15 @@ inline bool AddressIsBreakpoint(CORDB_ADDRESS_TYPE* address) return CORDbgGetInstruction(address) == CORDbg_BREAK_INSTRUCTION; } -inline void SetSSFlag(DT_CONTEXT *pContext) -{ - // TODO-LoongArch64: LoongArch64 doesn't support cpsr. - _ASSERTE(!"unimplemented on LOONGARCH64 yet"); -} +class Thread; +// Enable single stepping. +void SetSSFlag(DT_CONTEXT *pCtx, Thread *pThread); -inline void UnsetSSFlag(DT_CONTEXT *pContext) -{ - // TODO-LoongArch64: LoongArch64 doesn't support cpsr. - _ASSERTE(!"unimplemented on LOONGARCH64 yet"); -} +// Disable single stepping +void UnsetSSFlag(DT_CONTEXT *pCtx, Thread *pThread); -inline bool IsSSFlagEnabled(DT_CONTEXT * pContext) -{ - // TODO-LoongArch64: LoongArch64 doesn't support cpsr. - _ASSERTE(!"unimplemented on LOONGARCH64 yet"); - return false; -} +// Check if single stepping is enabled. +bool IsSSFlagEnabled(DT_CONTEXT *pCtx, Thread *pThread); inline bool PRDIsEqual(PRD_TYPE p1, PRD_TYPE p2) diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index 203820d14fd..5db31ece3af 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -842,6 +842,7 @@ elseif(CLR_CMAKE_TARGET_ARCH_LOONGARCH64) set(VM_SOURCES_WKS_ARCH ${ARCH_SOURCES_DIR}/profiler.cpp + ${ARCH_SOURCES_DIR}/singlestepper.cpp gcinfodecoder.cpp ) elseif(CLR_CMAKE_TARGET_ARCH_RISCV64) diff --git a/src/coreclr/vm/loongarch64/singlestepper.cpp b/src/coreclr/vm/loongarch64/singlestepper.cpp new file mode 100644 index 00000000000..867894a8d36 --- /dev/null +++ b/src/coreclr/vm/loongarch64/singlestepper.cpp @@ -0,0 +1,492 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// + +// +// Emulate hardware single-step on LOONGARCH64. +// + +#include "common.h" +#include "loongarch64singlestepper.h" + +inline uint64_t SignExtend(uint64_t value, unsigned int signbit) +{ + _ASSERTE(signbit < 64); + + if (signbit == 63) + return value; + + uint64_t sign = value & (1ull << signbit); + + if (sign) + return value | (~0ull << signbit); + else + return value; +} + +inline uint64_t BitExtract(uint64_t value, unsigned int highbit, unsigned int lowbit, bool signExtend = false) +{ + _ASSERTE((highbit < 64) && (lowbit < 64) && (highbit >= lowbit)); + uint64_t extractedValue = (value >> lowbit) & ((1ull << ((highbit - lowbit) + 1)) - 1); + + return signExtend ? SignExtend(extractedValue, highbit - lowbit) : extractedValue; +} + +// +// LoongArch64SingleStepper methods. +// +LoongArch64SingleStepper::LoongArch64SingleStepper() + : m_originalPc(0), m_targetPc(0), m_rgCode(0), m_state(Disabled), + m_fEmulate(false), m_fBypass(false) +{ + m_opcodes[0] = 0; +} + +LoongArch64SingleStepper::~LoongArch64SingleStepper() +{ +#if !defined(DACCESS_COMPILE) + SystemDomain::GetGlobalLoaderAllocator()->GetExecutableHeap()->BackoutMem(m_rgCode, kMaxCodeBuffer * sizeof(uint32_t)); +#endif +} + +void LoongArch64SingleStepper::Init() +{ +#if !defined(DACCESS_COMPILE) + if (m_rgCode == NULL) + { + m_rgCode = (uint32_t *)(void *)SystemDomain::GetGlobalLoaderAllocator()->GetExecutableHeap()->AllocMem(S_SIZE_T(kMaxCodeBuffer * sizeof(uint32_t))); + } +#endif +} + +// Given the context with which a thread will be resumed, modify that context such that resuming the thread +// will execute a single instruction before raising an EXCEPTION_BREAKPOINT. The thread context must be +// cleaned up via the Fixup method below before any further exception processing can occur (at which point the +// caller can behave as though EXCEPTION_SINGLE_STEP was raised). +void LoongArch64SingleStepper::Enable() +{ + _ASSERTE(m_state != Applied); + + if (m_state == Enabled) + { + // We allow single-stepping to be enabled multiple times before the thread is resumed, but we require + // that the thread state is the same in all cases (i.e. additional step requests are treated as + // no-ops). + _ASSERTE(!m_fBypass); + _ASSERTE(m_opcodes[0] == 0); + + return; + } + + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::Enable\n")); + + m_fBypass = false; + m_opcodes[0] = 0; + m_state = Enabled; +} + +void LoongArch64SingleStepper::Bypass(uint64_t ip, uint32_t opcode) +{ + _ASSERTE(m_state != Applied); + + if (m_state == Enabled) + { + // We allow single-stepping to be enabled multiple times before the thread is resumed, but we require + // that the thread state is the same in all cases (i.e. additional step requests are treated as + // no-ops). + if (m_fBypass) + { + _ASSERTE(m_opcodes[0] == opcode); + _ASSERTE(m_originalPc == ip); + return; + } + } + + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::Bypass(pc=%lx, opcode=%x)\n", ip, opcode)); + + m_fBypass = true; + m_originalPc = ip; + m_opcodes[0] = opcode; + m_state = Enabled; +} + +void LoongArch64SingleStepper::Apply(T_CONTEXT *pCtx) +{ + if (m_rgCode == NULL) + { + Init(); + + // OOM. We will simply ignore the single step. + if (m_rgCode == NULL) + return; + } + + _ASSERTE(pCtx != NULL); + + if (!m_fBypass) + { + uint64_t pc = pCtx->Pc; + m_opcodes[0] = *(uint32_t*)pc; // Opcodes are always in little endian, we only support little endian execution mode + } + + uint32_t opcode = m_opcodes[0]; + + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::Apply(pc=%lx, opcode=%x)\n", (uint64_t)pCtx->Pc, opcode)); + +#ifdef _DEBUG + // Make sure that we aren't trying to step through our own buffer. If this asserts, something is horribly + // wrong with the debugging layer. Likely GetManagedStoppedCtx is retrieving a Pc that points to our + // buffer, even though the single stepper is disabled. + uint64_t codestart = (uint64_t)m_rgCode; + uint64_t codeend = codestart + (kMaxCodeBuffer * sizeof(uint32_t)); + _ASSERTE((pCtx->Pc < codestart) || (pCtx->Pc >= codeend)); +#endif + + // All stepping is simulated using a breakpoint instruction. Since other threads are not suspended while + // we step, we avoid race conditions and other complexity by redirecting the thread into a thread-local + // execution buffer. We can either copy the instruction we wish to step over into the buffer followed by a + // breakpoint or we can emulate the instruction (which is useful for instruction that depend on the value + // of the PC or that branch or call to an alternate location). Even in the emulation case we still + // redirect execution into the buffer and insert a breakpoint; this simplifies our interface since the + // rest of the runtime is not set up to expect single stepping to occur inline. Instead there is always a + // 1:1 relationship between setting the single-step mode and receiving an exception once the thread is + // restarted. + // + // There are two parts to the emulation: + // 1) In this method we either emulate the instruction (updating the input thread context as a result) or + // copy the single instruction into the execution buffer. In both cases we copy a breakpoint into the + // execution buffer as well then update the thread context to redirect execution into this buffer. + // 2) In the runtime's first chance vectored exception handler we perform the necessary fixups to make + // the exception look like the result of a single step. This includes resetting the PC to its correct + // value (either the instruction following the stepped instruction or the target PC cached in this + // object when we emulated an instruction that alters the PC). It also involves switching + // EXCEPTION_BREAKPOINT to EXCEPTION_SINGLE_STEP. + // + // If we encounter an exception while emulating an instruction (currently this can only happen if we A/V + // trying to read a value from memory) then we abandon emulation and fall back to the copy instruction + // mechanism. When we run the execution buffer the exception should be raised and handled as normal (we + // still perform context fixup in this case but we don't attempt to alter any exception code other than + // EXCEPTION_BREAKPOINT to EXCEPTION_SINGLE_STEP). There is a very small timing window here where another + // thread could alter memory protections to avoid the A/V when we run the instruction for real but the + // liklihood of this happening (in managed code no less) is judged sufficiently small that it's not worth + // the alternate solution (where we'd have to set the thread up to raise an exception with exactly the + // right thread context). + + // Cache thread's initial PC since we'll overwrite them as part of the emulation and we need + // to get back to the correct values at fixup time. We also cache a target PC (set below) since some + // instructions will set the PC directly or otherwise make it difficult for us to compute the final PC + // from the original. We still need the original PC however since this is the one we'll use if an + // exception (other than a breakpoint) occurs. + _ASSERTE((!m_fBypass || (m_originalPc == pCtx->Pc))); + + m_originalPc = pCtx->Pc; + + // By default assume the next PC is right after the current instruction. + m_targetPc = m_originalPc + sizeof(uint32_t); + m_fEmulate = false; + + // There are two different scenarios we must deal with (listed in priority order). In all cases we will + // redirect the thread to execute code from our buffer and end by raising a breakpoint exception: + // 1) The current instruction either takes the PC as an input or modifies the PC. + // We can't easily run these instructions from the redirect buffer so we emulate their effect (i.e. + // update the current context in the same way as executing the instruction would). The breakpoint + // fixup logic will restore the PC to the real resultant PC we cache in m_targetPc. + // 2) For all other cases (including emulation cases where we aborted due to a memory fault) we copy the + // single instruction into the redirect buffer for execution followed by a breakpoint (once we regain + // control in the breakpoint fixup logic we can then reset the PC to its proper location. + + unsigned int idxNextInstruction = 0; + + ExecutableWriterHolder codeWriterHolder(m_rgCode, kMaxCodeBuffer * sizeof(m_rgCode[0])); + + if (TryEmulate(pCtx, opcode, false)) + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper: Case 1: Emulate\n")); + // Case 1: Emulate an instruction that reads or writes the PC. + m_fEmulate = true; + } + else + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper: Case 2: CopyInstruction.\n")); + // Case 2: In all other cases copy the instruction to the buffer and we'll run it directly. + codeWriterHolder.GetRW()[idxNextInstruction++] = opcode; + } + + // Always terminate the redirection buffer with a breakpoint. + codeWriterHolder.GetRW()[idxNextInstruction++] = kBreakpointOp; + _ASSERTE(idxNextInstruction <= kMaxCodeBuffer); + + // Set the thread up so it will redirect to our buffer when execution resumes. + pCtx->Pc = (uint64_t)m_rgCode; + + // Make sure the CPU sees the updated contents of the buffer. + FlushInstructionCache(GetCurrentProcess(), m_rgCode, kMaxCodeBuffer * sizeof(m_rgCode[0])); + + // Done, set the state. + m_state = Applied; +} + +void LoongArch64SingleStepper::Disable() +{ + _ASSERTE(m_state != Applied); + m_state = Disabled; +} + +// When called in response to an exception (preferably in a first chance vectored handler before anyone else +// has looked at the thread context) this method will (a) determine whether this exception was raised by a +// call to Enable() above, in which case true will be returned and (b) perform final fixup of the thread +// context passed in to complete the emulation of a hardware single step. Note that this routine must be +// called even if the exception code is not EXCEPTION_BREAKPOINT since the instruction stepped might have +// raised its own exception (e.g. A/V) and we still need to fix the thread context in this case. +bool LoongArch64SingleStepper::Fixup(T_CONTEXT *pCtx, DWORD dwExceptionCode) +{ +#ifdef _DEBUG + uint64_t codestart = (uint64_t)m_rgCode; + uint64_t codeend = codestart + (kMaxCodeBuffer * sizeof(uint32_t)); +#endif + + // If we reach fixup, we should either be Disabled or Applied. If we reach here with Enabled it means + // that the debugging layer Enabled the single stepper, but we never applied it to a CONTEXT. + _ASSERTE(m_state != Enabled); + + // Nothing to do if the stepper is disabled on this thread. + if (m_state == Disabled) + { + // We better not be inside our internal code buffer though. + _ASSERTE((pCtx->Pc < codestart) || (pCtx->Pc > codeend)); + return false; + } + + // Turn off the single stepper after we have executed one instruction. + m_state = Disabled; + + // We should always have a PC somewhere in our redirect buffer. +#ifdef _DEBUG + _ASSERTE((pCtx->Pc >= codestart) && (pCtx->Pc <= codeend)); +#endif + + if (dwExceptionCode == EXCEPTION_BREAKPOINT) + { + // The single step went as planned. Set the PC back to its real value (either following the + // instruction we stepped or the computed destination we cached after emulating an instruction that + // modifies the PC). + if (!m_fEmulate) + { + if (m_rgCode[0] != kBreakpointOp) + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::Fixup executed code, ip = %lx\n", m_targetPc)); + + pCtx->Pc = m_targetPc; + } + else + { + // We've hit a breakpoint in the code stream. We will return false here (which causes us to NOT + // replace the breakpoint code with single step), and place the Pc back to the original Pc. The + // debugger patch skipping code will move past this breakpoint. + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::Fixup emulated breakpoint\n")); + pCtx->Pc = m_originalPc; + + _ASSERTE((pCtx->Pc & 0x3) == 0); + return false; + } + } + else + { + bool res = TryEmulate(pCtx, m_opcodes[0], true); + _ASSERTE(res); // We should always successfully emulate since we ran it through TryEmulate already. + + pCtx->Pc = m_targetPc; + + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::Fixup emulated, ip = %lx\n", pCtx->Pc)); + } + } + else + { + // The stepped instruction caused an exception. Reset the PC to its original values we + // cached before stepping. + _ASSERTE(m_fEmulate == false); + pCtx->Pc = m_originalPc; + + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::Fixup hit exception pc = %lx ex = %x\n", pCtx->Pc, dwExceptionCode)); + } + + _ASSERTE((pCtx->Pc & 0x3) == 0); + + return true; +} + +// Get the current value of a register. +uint64_t LoongArch64SingleStepper::GetReg(T_CONTEXT *pCtx, uint64_t reg) +{ + _ASSERTE(reg <= 31); + _ASSERTE(pCtx->R0 == 0); + + return (&pCtx->R0)[reg]; +} + +// Set the current value of a register. +void LoongArch64SingleStepper::SetReg(T_CONTEXT *pCtx, uint64_t reg, uint64_t value) +{ + _ASSERTE(reg <= 31); + + if (reg != 0) // Do nothing for R0, register R0 is hardwired to the constant 0. + (&pCtx->R0)[reg] = value; +} + +// Parse the instruction opcode. If the instruction reads or writes the PC it will be emulated by updating +// the thread context appropriately and true will be returned. If the instruction is not one of those cases +// (or it is but we faulted trying to read memory during the emulation) no state is updated and false is +// returned instead. +bool LoongArch64SingleStepper::TryEmulate(T_CONTEXT *pCtx, uint32_t opcode, bool execute) +{ + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate(opcode=%x, execute=%s)\n", opcode, execute ? "true" : "false")); + + // Track whether instruction emulation wrote a modified PC. + bool fRedirectedPc = false; + + // Track whether we successfully emulated an instruction. If we did and we didn't modify the PC (e.g. a + // PCALAU12I instruction or a conditional branch not taken) then we'll need to explicitly set the PC to the next + // instruction (since our caller expects that whenever we return true m_pCtx->Pc holds the next + // instruction address). + bool fEmulated = false; + + uint32_t op = (opcode >> 26) & 0x3f; + + if ((op == 0x6) || (op == 0x7)) // PC-Rel addressing (PCADDU12I, PCALAU12I, PCADDU18I) + { + fEmulated = true; + if (execute) + { + uint64_t P = BitExtract(opcode, 25, 25); + uint64_t imm = BitExtract(opcode, 24, 5, true); + uint64_t Rd = BitExtract(opcode, 4, 0); + + if ((op == 0x6) && P) // PCALAU12I + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate PCALAU12I\n")); + uint64_t value = (m_originalPc & ~0xfffull) + (imm << 12); + SetReg(pCtx, Rd, value); + } + else if (P)// PCADDU18I + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate PCADDU18I\n")); + uint64_t value = m_originalPc + (imm << 18); + SetReg(pCtx, Rd, value); + } + else // PCADDU12I + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate PCADDU12I\n")); + uint64_t value = m_originalPc + (imm << 12); + SetReg(pCtx, Rd, value); + } + } + } + else if ((op == 0x16) || (op == 0x17) || (op == 0x18) || (op == 0x19) || (op == 0x1a) ||(op == 0x1b)) // B.cond: BEQ, BNE, BLT, BGE, BLTU, BGEU + { + fEmulated = true; + if (execute) + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate B.cond: BEQ, BNE, BLT[U], BGE[U]\n")); + uint64_t imm16 = BitExtract(opcode, 25, 10, true); + uint64_t rd = BitExtract(opcode, 4, 0); + uint64_t rj = BitExtract(opcode, 9, 5); + + int64_t rdSValue = GetReg(pCtx, rd); + int64_t rjSValue = GetReg(pCtx, rj); + uint64_t rdUValue = GetReg(pCtx, rd); + uint64_t rjUValue = GetReg(pCtx, rj); + + if (((op == 0x16) && (rjSValue == rdSValue)) || + ((op == 0x17) && (rjSValue != rdSValue)) || + ((op == 0x18) && (rjSValue < rdSValue)) || + ((op == 0x19) && (rjSValue >= rdSValue)) || + ((op == 0x1a) && (rjUValue < rdUValue)) || + ((op == 0x1b) && (rjUValue >= rdUValue))) + { + uint64_t imm = (imm16 << 2); + fRedirectedPc = true; + m_targetPc = m_originalPc + imm; + } + } + } + else if ((op == 0x10) || (op == 0x11)) // Compare and branch BEQZ, BNEZ + { + fEmulated = true; + if (execute) + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate BEQZ, BNEZ\n")); + + uint64_t immlo = BitExtract(opcode, 25, 10); + uint64_t immhi = BitExtract(opcode, 4, 0, true); + uint64_t imm = ((immhi << 16) | immlo) << 2; + + uint64_t rj = BitExtract(opcode, 9, 5); + uint64_t rjValue = GetReg(pCtx, rj); + + if (((op == 0x10) && (rjValue == 0)) || + ((op == 0x11) && (rjValue != 0))) + { + fRedirectedPc = true; + m_targetPc = m_originalPc + imm; + } + } + } + else if ((op == 0x14) || (op == 0x15)) // Unconditional branch immediate (B, BL) + { + fEmulated = true; + if (execute) + { + uint64_t immlo = BitExtract(opcode, 25, 10); + uint64_t immhi = BitExtract(opcode, 9, 0, true); + uint64_t imm = ((immhi << 16) | immlo) << 2; + + fRedirectedPc = true; + m_targetPc = m_originalPc + imm; + + if (op == 0x14) //B + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate B\n")); + } + else // BL + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate BL\n")); + SetReg(pCtx, 1, m_originalPc + 4); + } + } + } + else if (op == 0x13) // Unconditional branch register JIRL + { + fEmulated = true; + if (execute) + { + int imm = (short)((opcode >> 10) & 0xffff); + imm <<= 2; + + uint64_t rd = BitExtract(opcode, 4, 0); + uint64_t rj = BitExtract(opcode, 9, 5); + + if (rd == 0) // JIRL 0, RJ + { + fRedirectedPc = true; + m_targetPc = GetReg(pCtx, rj) + (int)imm; + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate JIRL 0 RA\n")); + } + else if (rd == 1) // JIRL RA, RJ + { + SetReg(pCtx, 1, m_originalPc + 4); + fRedirectedPc = true; + m_targetPc = GetReg(pCtx, rj) + (int)imm; + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate JIRL RA target\n")); + } + else + { + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate unexpected for JIRL !\n")); + } + } + } + + LOG((LF_CORDB, LL_INFO100000, "LoongArch64SingleStepper::TryEmulate(opcode=%x) emulated=%s redirectedPc=%s\n", + opcode, fEmulated ? "true" : "false", fRedirectedPc ? "true" : "false")); + + return fEmulated; +} diff --git a/src/coreclr/vm/loongarch64singlestepper.h b/src/coreclr/vm/loongarch64singlestepper.h new file mode 100644 index 00000000000..df52a123322 --- /dev/null +++ b/src/coreclr/vm/loongarch64singlestepper.h @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// + +// +// Emulate hardware single-step on LOONGARCH64. +// + +#ifndef __LOONGARCH64_SINGLE_STEPPER_H__ +#define __LOONGARCH64_SINGLE_STEPPER_H__ + +// Class that encapsulates the context needed to single step one thread. +class LoongArch64SingleStepper +{ +public: + LoongArch64SingleStepper(); + ~LoongArch64SingleStepper(); + + // Given the context with which a thread will be resumed, modify that context such that resuming the + // thread will execute a single instruction before raising an EXCEPTION_BREAKPOINT. The thread context + // must be cleaned up via the Fixup method below before any further exception processing can occur (at + // which point the caller can behave as though EXCEPTION_SINGLE_STEP was raised). + void Enable(); + + void Bypass(uint64_t ip, uint32_t opcode); + + void Apply(T_CONTEXT *pCtx); + + // Disables the single stepper. + void Disable(); + + // Returns whether or not the stepper is enabled. + inline bool IsEnabled() const + { + return m_state == Enabled || m_state == Applied; + } + + // When called in response to an exception (preferably in a first chance vectored handler before anyone + // else has looked at the thread context) this method will (a) determine whether this exception was raised + // by a call to Enable() above, in which case true will be returned and (b) perform final fixup of the + // thread context passed in to complete the emulation of a hardware single step. Note that this routine + // must be called even if the exception code is not EXCEPTION_BREAKPOINT since the instruction stepped + // might have raised its own exception (e.g. A/V) and we still need to fix the thread context in this + // case. + bool Fixup(T_CONTEXT *pCtx, DWORD dwExceptionCode); + +private: + enum + { + kMaxCodeBuffer = 2, // max slots in our redirect buffer + // 1 for current instruction + // 1 for final breakpoint + + kBreakpointOp = 0x002A0005, // Opcode for the breakpoint instruction used on LoongArch64 Unix + }; + + enum StepperState + { + Disabled, + Enabled, + Applied + }; + + uint64_t m_originalPc; // PC value before stepping + uint64_t m_targetPc; // Final PC value after stepping if no exception is raised + uint32_t *m_rgCode; // Buffer execution is redirected to during the step + StepperState m_state; // Tracks whether the stepper is Enabled, Disabled, or enabled and applied to a context + uint32_t m_opcodes[1]; + bool m_fEmulate; + bool m_fBypass; + + // Initializes m_rgCode. Not thread safe. + void Init(); + + // Get the current value of a register. + uint64_t GetReg(T_CONTEXT *pCtx, uint64_t reg); + // Set the current value of a register. + void SetReg(T_CONTEXT *pCtx, uint64_t reg, uint64_t value); + + // Parse the instruction opcode. If the instruction reads or writes the PC it will be emulated by updating + // the thread context appropriately and true will be returned. If the instruction is not one of those cases + // (or it is but we faulted trying to read memory during the emulation) no state is updated and false is + // returned instead. + bool TryEmulate(T_CONTEXT *pCtx, uint32_t opcode, bool execute); +}; + +#endif // !__LOONGARCH64_SINGLE_STEPPER_H__ diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index 34a6379f1f3..4a4bd2ff867 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -184,6 +184,9 @@ struct TailCallArgBuffer #if (defined(TARGET_RISCV64) && defined(FEATURE_EMULATE_SINGLESTEP)) #include "riscv64singlestepper.h" #endif +#if (defined(TARGET_LOONGARCH64) && defined(FEATURE_EMULATE_SINGLESTEP)) +#include "loongarch64singlestepper.h" +#endif #if !defined(PLATFORM_SUPPORTS_SAFE_THREADSUSPEND) // DISABLE_THREADSUSPEND controls whether Thread::SuspendThread will be used at all. @@ -2085,6 +2088,8 @@ private: ArmSingleStepper m_singleStepper; #elif defined(TARGET_RISCV64) RiscV64SingleStepper m_singleStepper; +#elif defined(TARGET_LOONGARCH64) + LoongArch64SingleStepper m_singleStepper; #else Arm64SingleStepper m_singleStepper; #endif @@ -2100,7 +2105,7 @@ public: m_singleStepper.Enable(); } - void BypassWithSingleStep(const void* ip ARM_ARG(WORD opcode1) ARM_ARG(WORD opcode2) ARM64_ARG(uint32_t opcode) RISCV64_ARG(uint32_t opcode)) + void BypassWithSingleStep(const void* ip ARM_ARG(WORD opcode1) ARM_ARG(WORD opcode2) ARM64_ARG(uint32_t opcode) RISCV64_ARG(uint32_t opcode) LOONGARCH64_ARG(uint32_t opcode)) { #if defined(TARGET_ARM) m_singleStepper.Bypass((DWORD)ip, opcode1, opcode2);