Skip to content

Commit

Permalink
JIT: Track GC info accurately into epilogs in codegen (#107283)
Browse files Browse the repository at this point in the history
- Handle tailcall arguments which are consumed by the call node but
  remain live into the epilog
- Handle return nodes which move the operand to the return registers
  which then should remain live into the epilog

Fix #107137
  • Loading branch information
jakobbotsch authored Jan 15, 2025
1 parent d2cdcdd commit 44ad4db
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 136 deletions.
2 changes: 0 additions & 2 deletions src/coreclr/jit/codegenarmarch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -611,8 +611,6 @@ void CodeGen::genEmitGSCookieCheck(bool pushReg)
{
noway_assert(compiler->gsGlobalSecurityCookieAddr || compiler->gsGlobalSecurityCookieVal);

assert(GetEmitter()->emitGCDisabled());

// We need two temporary registers, to load the GS cookie values and compare them. We can't use
// any argument registers if 'pushReg' is true (meaning we have a JMP call). They should be
// callee-trash registers, which should not contain anything interesting at this point.
Expand Down
148 changes: 61 additions & 87 deletions src/coreclr/jit/codegencommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1516,47 +1516,34 @@ void CodeGen::genExitCode(BasicBlock* block)
genIPmappingAdd(IPmappingDscKind::Epilog, DebugInfo(), true);

bool jmpEpilog = block->HasFlag(BBF_HAS_JMP);
if (compiler->getNeedsGSSecurityCookie())
{
#ifndef JIT32_GCENCODER
// At this point the gc info that we track in codegen is often incorrect,
// as it could be missing return registers or arg registers (in a case of tail call).
// GS cookie check will emit a call and that will pass our GC info to emit and potentially mess things up.
// While we could infer returns/args and force them to be live and it seems to work in JIT32_GCENCODER case,
// it appears to be nontrivial in more general case.
// So, instead, we just claim that the whole thing is not GC-interruptible.
// Effectively this starts the epilog a few instructions earlier.
//
// CONSIDER: is that a good place to be that codegen loses track of returns/args at this point?
GetEmitter()->emitDisableGC();
#endif

genEmitGSCookieCheck(jmpEpilog);

#ifdef JIT32_GCENCODER
if (jmpEpilog)
#ifdef DEBUG
// For returnining epilogs do some validation that the GC info looks right.
if (!jmpEpilog)
{
if (compiler->compMethodReturnsRetBufAddr())
{
// Dev10 642944 -
// The GS cookie check created a temp label that has no live
// incoming GC registers, we need to fix that

unsigned varNum;
LclVarDsc* varDsc;

/* Figure out which register parameters hold pointers */
assert((gcInfo.gcRegByrefSetCur & RBM_INTRET) != RBM_NONE);
}
else
{
const ReturnTypeDesc& retTypeDesc = compiler->compRetTypeDesc;
const unsigned regCount = retTypeDesc.GetReturnRegCount();

for (varNum = 0, varDsc = compiler->lvaTable; varNum < compiler->lvaCount && varDsc->lvIsRegArg;
varNum++, varDsc++)
for (unsigned i = 0; i < regCount; ++i)
{
noway_assert(varDsc->lvIsParam);

gcInfo.gcMarkRegPtrVal(varDsc->GetArgReg(), varDsc->TypeGet());
var_types type = retTypeDesc.GetReturnRegType(i);
regNumber reg = retTypeDesc.GetABIReturnReg(i, compiler->info.compCallConv);
assert((type == TYP_BYREF) == ((gcInfo.gcRegByrefSetCur & genRegMask(reg)) != RBM_NONE));
assert((type == TYP_REF) == ((gcInfo.gcRegGCrefSetCur & genRegMask(reg)) != RBM_NONE));
}

GetEmitter()->emitThisGCrefRegs = GetEmitter()->emitInitGCrefRegs = gcInfo.gcRegGCrefSetCur;
GetEmitter()->emitThisByrefRegs = GetEmitter()->emitInitByrefRegs = gcInfo.gcRegByrefSetCur;
}
}
#endif

if (compiler->getNeedsGSSecurityCookie())
{
genEmitGSCookieCheck(jmpEpilog);
}

genReserveEpilog(block);
Expand Down Expand Up @@ -4711,8 +4698,7 @@ void CodeGen::genReserveProlog(BasicBlock* block)

JITDUMP("Reserving prolog IG for block " FMT_BB "\n", block->bbNum);

/* Nothing is live on entry to the prolog */

// Nothing is live on entry to the prolog
GetEmitter()->emitCreatePlaceholderIG(IGPT_PROLOG, block, VarSetOps::MakeEmpty(compiler), 0, 0, false);
}

Expand All @@ -4723,13 +4709,12 @@ void CodeGen::genReserveProlog(BasicBlock* block)

void CodeGen::genReserveEpilog(BasicBlock* block)
{
assert(block != nullptr);

JITDUMP("Reserving epilog IG for block " FMT_BB "\n", block->bbNum);

assert(block != nullptr);
// We pass empty GC info, because epilog is always an extend IG and will ignore what we pass.
// Besides, at this point the GC info that we track in CodeGen is often incorrect.
// See comments in genExitCode for more info.
GetEmitter()->emitCreatePlaceholderIG(IGPT_EPILOG, block, VarSetOps::MakeEmpty(compiler), 0, 0, block->IsLast());
GetEmitter()->emitCreatePlaceholderIG(IGPT_EPILOG, block, VarSetOps::MakeEmpty(compiler), gcInfo.gcRegGCrefSetCur,
gcInfo.gcRegByrefSetCur, block->IsLast());
}

/*****************************************************************************
Expand Down Expand Up @@ -7162,14 +7147,24 @@ void CodeGen::genReturn(GenTree* treeNode)
}
}

const ReturnTypeDesc& retTypeDesc = compiler->compRetTypeDesc;

if (compiler->compMethodReturnsRetBufAddr())
{
gcInfo.gcMarkRegPtrVal(REG_INTRET, TYP_BYREF);
}
else
{
unsigned retRegCount = retTypeDesc.GetReturnRegCount();
for (unsigned i = 0; i < retRegCount; ++i)
{
gcInfo.gcMarkRegPtrVal(retTypeDesc.GetABIReturnReg(i, compiler->info.compCallConv),
retTypeDesc.GetReturnRegType(i));
}
}

#ifdef PROFILING_SUPPORTED
// !! Note !!
// TODO-AMD64-Unix: If the profiler hook is implemented on *nix, make sure for 2 register returned structs
// the RAX and RDX needs to be kept alive. Make the necessary changes in lowerxarch.cpp
// in the handling of the GT_RETURN statement.
// Such structs containing GC pointers need to be handled by calling gcInfo.gcMarkRegSetNpt
// for the return registers containing GC refs.
//

// Reason for not materializing Leave callback as a GT_PROF_HOOK node after GT_RETURN:
// In flowgraph and other places assert that the last node of a block marked as
// BBJ_RETURN is either a GT_RETURN or GT_JMP or a tail call. It would be nice to
Expand All @@ -7180,46 +7175,7 @@ void CodeGen::genReturn(GenTree* treeNode)
//
if (treeNode->OperIs(GT_RETURN, GT_SWIFT_ERROR_RET) && compiler->compIsProfilerHookNeeded())
{
// !! NOTE !!
// Since we are invalidating the assumption that we would slip into the epilog
// right after the "return", we need to preserve the return reg's GC state
// across the call until actual method return.

ReturnTypeDesc retTypeDesc = compiler->compRetTypeDesc;
unsigned retRegCount = retTypeDesc.GetReturnRegCount();

if (compiler->compMethodReturnsRetBufAddr())
{
gcInfo.gcMarkRegPtrVal(REG_INTRET, TYP_BYREF);
}
else
{
for (unsigned i = 0; i < retRegCount; ++i)
{
if (varTypeIsGC(retTypeDesc.GetReturnRegType(i)))
{
gcInfo.gcMarkRegPtrVal(retTypeDesc.GetABIReturnReg(i, compiler->info.compCallConv),
retTypeDesc.GetReturnRegType(i));
}
}
}

genProfilingLeaveCallback(CORINFO_HELP_PROF_FCN_LEAVE);

if (compiler->compMethodReturnsRetBufAddr())
{
gcInfo.gcMarkRegSetNpt(genRegMask(REG_INTRET));
}
else
{
for (unsigned i = 0; i < retRegCount; ++i)
{
if (varTypeIsGC(retTypeDesc.GetReturnRegType(i)))
{
gcInfo.gcMarkRegSetNpt(genRegMask(retTypeDesc.GetABIReturnReg(i, compiler->info.compCallConv)));
}
}
}
}
#endif // PROFILING_SUPPORTED

Expand Down Expand Up @@ -7469,6 +7425,12 @@ void CodeGen::genCallPlaceRegArgs(GenTreeCall* call)
/* canSkip */ true);

use = use->GetNext();

if (call->IsFastTailCall())
{
// We won't actually consume the register here -- keep it alive into the epilog.
gcInfo.gcMarkRegPtrVal(seg.GetRegister(), putArgRegNode->TypeGet());
}
}

assert(use == nullptr);
Expand All @@ -7493,6 +7455,12 @@ void CodeGen::genCallPlaceRegArgs(GenTreeCall* call)
var_types type = argNode->AsPutArgSplit()->GetRegType(regIndex);
inst_Mov(genActualType(type), seg.GetRegister(), allocReg, /* canSkip */ true);

if (call->IsFastTailCall())
{
// We won't actually consume the register here -- keep it alive into the epilog.
gcInfo.gcMarkRegPtrVal(seg.GetRegister(), type);
}

regIndex++;
}

Expand All @@ -7505,6 +7473,12 @@ void CodeGen::genCallPlaceRegArgs(GenTreeCall* call)
regNumber argReg = abiInfo.Segment(0).GetRegister();
genConsumeReg(argNode);
inst_Mov(genActualType(argNode), argReg, argNode->GetRegNum(), /* canSkip */ true);

if (call->IsFastTailCall())
{
// We won't actually consume the register here -- keep it alive into the epilog.
gcInfo.gcMarkRegPtrVal(argReg, argNode->TypeGet());
}
continue;
}

Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/jit/codegeninterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ class CodeGenInterface

protected:
// Keeps track of how many bytes we've pushed on the processor's stack.
unsigned genStackLevel;
unsigned genStackLevel = 0;

public:
//--------------------------------------------
Expand Down
32 changes: 21 additions & 11 deletions src/coreclr/jit/codegenlinear.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -483,24 +483,34 @@ void CodeGen::genCodeForBBlist()

regSet.rsSpillChk();

/* Make sure we didn't bungle pointer register tracking */
// Make sure we didn't bungle pointer register tracking

regMaskTP ptrRegs = gcInfo.gcRegGCrefSetCur | gcInfo.gcRegByrefSetCur;
regMaskTP nonVarPtrRegs = ptrRegs & ~regSet.GetMaskVars();

// If return is a GC-type, clear it. Note that if a common
// epilog is generated (genReturnBB) it has a void return
// even though we might return a ref. We can't use the compRetType
// as the determiner because something we are tracking as a byref
// might be used as a return value of a int function (which is legal)
GenTree* blockLastNode = block->lastNode();
if ((blockLastNode != nullptr) && (blockLastNode->OperIs(GT_RETURN, GT_SWIFT_ERROR_RET)) &&
(varTypeIsGC(compiler->info.compRetType) ||
(blockLastNode->AsOp()->GetReturnValue() != nullptr &&
varTypeIsGC(blockLastNode->AsOp()->GetReturnValue()->TypeGet()))))
// If this is a return block then we expect some live GC regs. Clear those.
if (compiler->compMethodReturnsRetBufAddr())
{
nonVarPtrRegs &= ~RBM_INTRET;
}
else
{
const ReturnTypeDesc& retTypeDesc = compiler->compRetTypeDesc;
const unsigned regCount = retTypeDesc.GetReturnRegCount();

for (unsigned i = 0; i < regCount; ++i)
{
regNumber reg = retTypeDesc.GetABIReturnReg(i, compiler->info.compCallConv);
nonVarPtrRegs &= ~genRegMask(reg);
}
}

// For a tailcall arbitrary argument registers may be live into the
// prolog. Skip validating those.
if (block->HasFlag(BBF_HAS_JMP))
{
nonVarPtrRegs &= ~fullIntArgRegMask(CorInfoCallConvExtension::Managed);
}

if (nonVarPtrRegs)
{
Expand Down
2 changes: 0 additions & 2 deletions src/coreclr/jit/codegenloongarch64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4616,8 +4616,6 @@ void CodeGen::genEmitGSCookieCheck(bool pushReg)
{
noway_assert(compiler->gsGlobalSecurityCookieAddr || compiler->gsGlobalSecurityCookieVal);

assert(GetEmitter()->emitGCDisabled());

// We need two temporary registers, to load the GS cookie values and compare them. We can't use
// any argument registers if 'pushReg' is true (meaning we have a JMP call). They should be
// callee-trash registers, which should not contain anything interesting at this point.
Expand Down
2 changes: 0 additions & 2 deletions src/coreclr/jit/codegenriscv64.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4666,8 +4666,6 @@ void CodeGen::genEmitGSCookieCheck(bool pushReg)
{
noway_assert(compiler->gsGlobalSecurityCookieAddr || compiler->gsGlobalSecurityCookieVal);

assert(GetEmitter()->emitGCDisabled());

// We need two temporary registers, to load the GS cookie values and compare them. We can't use
// any argument registers if 'pushReg' is true (meaning we have a JMP call). They should be
// callee-trash registers, which should not contain anything interesting at this point.
Expand Down
31 changes: 0 additions & 31 deletions src/coreclr/jit/codegenxarch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,37 +96,6 @@ void CodeGen::genEmitGSCookieCheck(bool pushReg)
{
noway_assert(compiler->gsGlobalSecurityCookieAddr || compiler->gsGlobalSecurityCookieVal);

#ifdef JIT32_GCENCODER
if (!pushReg)
{
// Make sure that EAX is reported as live GC-ref so that any GC that kicks in while
// executing GS cookie check will not collect the object pointed to by EAX.
if (compiler->compMethodReturnsRetBufAddr())
{
// This is for returning in an implicit RetBuf.
// If the address of the buffer is returned in REG_INTRET, mark the content of INTRET as ByRef.

// In case the return is in an implicit RetBuf, the native return type should be a struct
assert(varTypeIsStruct(compiler->info.compRetNativeType));

gcInfo.gcMarkRegPtrVal(REG_INTRET, TYP_BYREF);
}
else
{
ReturnTypeDesc retTypeDesc = compiler->compRetTypeDesc;
const unsigned regCount = retTypeDesc.GetReturnRegCount();

for (unsigned i = 0; i < regCount; ++i)
{
gcInfo.gcMarkRegPtrVal(retTypeDesc.GetABIReturnReg(i, compiler->info.compCallConv),
retTypeDesc.GetReturnRegType(i));
}
}
}
#else
assert(GetEmitter()->emitGCDisabled());
#endif

regNumber regGSCheck;
regMaskTP regMaskGSCheck = RBM_NONE;

Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/jit/lclvars.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ void Compiler::lvaInitArgs(InitVarDscInfo* varDscInfo)

#if defined(TARGET_ARM) && defined(PROFILING_SUPPORTED)
// Prespill all argument regs on to stack in case of Arm when under profiler.
// We do this as the arm32 CORINFO_HELP_FCN_ENTER helper does not preserve
// these registers, and is called very early.
if (compIsProfilerHookNeeded())
{
codeGen->regSet.rsMaskPreSpillRegArg |= RBM_ARG_REGS;
Expand Down

0 comments on commit 44ad4db

Please sign in to comment.