https://github.com/zatrazz updated https://github.com/llvm/llvm-project/pull/202582
>From 722fa5bb3c0ca662f79522e5eba5a9128967a6e5 Mon Sep 17 00:00:00 2001 From: Adhemerval Zanella <[email protected]> Date: Thu, 4 Jun 2026 15:24:16 -0300 Subject: [PATCH 1/4] [AArch64] Add llvm.aarch64.hvc intrinsic Add a new LLVM intrinsic llvm.aarch64.hvc alongside the existing llvm.aarch64.break and llvm.aarch64.hlt, wired to the HVC (Hypervisor Call) instruction in the AArch64 backend. The intrinsic models the MSVC __hvc semantics: the first operand is the 16-bit instruction immediate, followed by up to four register arguments passed in X0-X3, and it returns the value left in X0. It is custom-lowered in LowerINTRINSIC_W_CHAIN, which copies the arguments into X0-X3, emits an AArch64ISD::HVC node with a caller-saved clobber mask, and reads the result back from X0. Unlike hlt/break it is not IntrNoReturn, since the hypervisor can return control to the caller, and it may read and write memory. --- llvm/include/llvm/IR/IntrinsicsAArch64.td | 9 +++ .../Target/AArch64/AArch64ISelLowering.cpp | 42 +++++++++- llvm/lib/Target/AArch64/AArch64InstrInfo.td | 15 ++++ llvm/test/CodeGen/AArch64/arm64-hvc.ll | 78 +++++++++++++++++++ .../AArch64/Neoverse/V1-misc-instructions.s | 2 +- 5 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 llvm/test/CodeGen/AArch64/arm64-hvc.ll diff --git a/llvm/include/llvm/IR/IntrinsicsAArch64.td b/llvm/include/llvm/IR/IntrinsicsAArch64.td index 29f9742532d33..9e6fd17f8b4db 100644 --- a/llvm/include/llvm/IR/IntrinsicsAArch64.td +++ b/llvm/include/llvm/IR/IntrinsicsAArch64.td @@ -72,6 +72,15 @@ def int_aarch64_break : Intrinsic<[], [llvm_i32_ty], def int_aarch64_hlt : Intrinsic<[], [llvm_i32_ty], [IntrNoMem, IntrHasSideEffects, IntrNoReturn, IntrCold, ImmArg<ArgIndex<0>>]>; +// The first operand is the 16-bit instruction immediate. The remaining four +// operands are the values passed in X0-X3 following the Microsoft __hvc calling +// convention (this is a Microsoft software convention, not an architectural +// requirement); an unused argument is passed as poison. The result is the +// value left in X0. +def int_aarch64_hvc : Intrinsic<[llvm_i64_ty], + [llvm_i32_ty, llvm_i64_ty, llvm_i64_ty, llvm_i64_ty, llvm_i64_ty], + [IntrHasSideEffects, ImmArg<ArgIndex<0>>]>; + def int_aarch64_prefetch : Intrinsic<[], [llvm_ptr_ty, llvm_i32_ty, llvm_i32_ty, llvm_i32_ty, llvm_i32_ty], [IntrInaccessibleMemOrArgMemOnly, IntrWillReturn, ReadOnly<ArgIndex<0>>, diff --git a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp index b01dc35531f4b..69fcf2b8e9f6c 100644 --- a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp +++ b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp @@ -1609,9 +1609,7 @@ AArch64TargetLowering::AArch64TargetLowering(const TargetMachine &TM, setOperationAction(ISD::TRUNCATE_USAT_U, VT, Legal); } - if (Subtarget->hasSME()) { - setOperationAction(ISD::INTRINSIC_W_CHAIN, MVT::Other, Custom); - } + setOperationAction(ISD::INTRINSIC_W_CHAIN, MVT::Other, Custom); // FIXME: Move lowering for more nodes here if those are common between // SVE and SME. @@ -6588,6 +6586,44 @@ SDValue AArch64TargetLowering::LowerINTRINSIC_W_CHAIN(SDValue Op, switch (IntNo) { default: return SDValue(); // Don't custom lower most intrinsics. + case Intrinsic::aarch64_hvc: { + // The MSVC __hvc intrinsic takes the 16-bit instruction immediate as its + // first operand and four further operands passed in X0-X3 (an unused + // argument is passed as poison) and returns the value left in X0. Matching + // MSVC, the instruction is not treated as clobbering the caller-saved + // registers; only X0 (the result) is defined. + SDValue Chain = Op.getOperand(0); + unsigned Imm = Op.getConstantOperandVal(2); + + static const MCPhysReg ArgGPRs[] = {AArch64::X0, AArch64::X1, AArch64::X2, + AArch64::X3}; + + SDValue Glue; + SmallVector<SDValue, 4> RegOps; + for (unsigned I = 0; I < std::size(ArgGPRs); ++I) { + SDValue Arg = Op.getOperand(3 + I); + if (Arg.isUndef()) + continue; + Chain = DAG.getCopyToReg(Chain, DL, ArgGPRs[I], Arg, Glue); + Glue = Chain.getValue(1); + RegOps.push_back(DAG.getRegister(ArgGPRs[I], MVT::i64)); + } + + SmallVector<SDValue, 8> Ops; + Ops.push_back(Chain); + Ops.push_back(DAG.getTargetConstant(Imm, DL, MVT::i32)); + Ops.append(RegOps.begin(), RegOps.end()); + if (Glue.getNode()) + Ops.push_back(Glue); + + SDValue Node = DAG.getNode(AArch64ISD::HVC, DL, + DAG.getVTList(MVT::Other, MVT::Glue), Ops); + Chain = Node.getValue(0); + Glue = Node.getValue(1); + + SDValue Result = DAG.getCopyFromReg(Chain, DL, AArch64::X0, MVT::i64, Glue); + return DAG.getMergeValues({Result.getValue(0), Result.getValue(1)}, DL); + } case Intrinsic::aarch64_mops_memset_tag: { auto Node = cast<MemIntrinsicSDNode>(Op.getNode()); SDValue Chain = Node->getChain(); diff --git a/llvm/lib/Target/AArch64/AArch64InstrInfo.td b/llvm/lib/Target/AArch64/AArch64InstrInfo.td index 53c7af890b6e5..f897727f74d26 100644 --- a/llvm/lib/Target/AArch64/AArch64InstrInfo.td +++ b/llvm/lib/Target/AArch64/AArch64InstrInfo.td @@ -3802,6 +3802,14 @@ def : Pat<(AArch64call texternalsym:$func), (BL texternalsym:$func)>; //===----------------------------------------------------------------------===// // Exception generation instructions. //===----------------------------------------------------------------------===// + +// Node for the HVC exception-generating call. The single operand is the +// 16-bit instruction immediate; the argument registers, clobber mask and glue +// are attached as variadic operands during lowering. +def AArch64hvc : SDNode<"AArch64ISD::HVC", SDTypeProfile<0, 1, [SDTCisInt<0>]>, + [SDNPHasChain, SDNPOptInGlue, SDNPOutGlue, SDNPVariadic, + SDNPMayLoad, SDNPMayStore]>; + let isTrap = 1 in { def BRK : ExceptionGeneration<0b001, 0b00, "brk", [(int_aarch64_break timm32_0_65535:$imm)]>; @@ -3811,9 +3819,16 @@ def DCPS2 : ExceptionGeneration<0b101, 0b10, "dcps2">; def DCPS3 : ExceptionGeneration<0b101, 0b11, "dcps3">, Requires<[HasEL3]>; def HLT : ExceptionGeneration<0b010, 0b00, "hlt", [(int_aarch64_hlt timm32_0_65535:$imm)]>; +// Following the Microsoft __hvc calling convention (a software convention, not +// an architectural rule), HVC passes up to four arguments in X0-X3 and returns +// a value in X0. It is therefore selected from the AArch64hvc node (built by +// LowerINTRINSIC_W_CHAIN) rather than a plain intrinsic pattern, and defines X0 +// for the result. +let mayLoad = 1, mayStore = 1, Defs = [X0] in def HVC : ExceptionGeneration<0b000, 0b10, "hvc">; def SMC : ExceptionGeneration<0b000, 0b11, "smc">, Requires<[HasEL3]>; def SVC : ExceptionGeneration<0b000, 0b01, "svc">; +def : Pat<(AArch64hvc timm32_0_65535:$imm), (HVC timm32_0_65535:$imm)>; // DCPSn defaults to an immediate operand of zero if unspecified. def : InstAlias<"dcps1", (DCPS1 0)>; diff --git a/llvm/test/CodeGen/AArch64/arm64-hvc.ll b/llvm/test/CodeGen/AArch64/arm64-hvc.ll new file mode 100644 index 0000000000000..5b931c2dcc112 --- /dev/null +++ b/llvm/test/CodeGen/AArch64/arm64-hvc.ll @@ -0,0 +1,78 @@ +; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6 +; RUN: llc < %s -mtriple=aarch64-none-eabi | FileCheck %s + +declare i64 @llvm.aarch64.hvc(i32 immarg, i64, i64, i64, i64) nounwind +declare i64 @g() nounwind + +; No register arguments; the immediate is encoded in the instruction. +define i64 @hvc_no_args() nounwind { +; CHECK-LABEL: hvc_no_args: +; CHECK: // %bb.0: +; CHECK-NEXT: hvc #0x2 +; CHECK-NEXT: ret + %r = call i64 @llvm.aarch64.hvc(i32 2, i64 poison, i64 poison, i64 poison, i64 poison) + ret i64 %r +} + +; Register arguments are passed in X0-X3 (here swapped to force the moves), +; and the result is read back from X0. +define i64 @hvc_two_args(i64 %a, i64 %b) nounwind { +; CHECK-LABEL: hvc_two_args: +; CHECK: // %bb.0: +; CHECK-NEXT: mov x8, x0 +; CHECK-NEXT: mov x0, x1 +; CHECK-NEXT: mov x1, x8 +; CHECK-NEXT: hvc #0x1 +; CHECK-NEXT: ret + %r = call i64 @llvm.aarch64.hvc(i32 1, i64 %b, i64 %a, i64 poison, i64 poison) + ret i64 %r +} + +; Up to four register arguments. +define i64 @hvc_four_args(i64 %a, i64 %b, i64 %c, i64 %d) nounwind { +; CHECK-LABEL: hvc_four_args: +; CHECK: // %bb.0: +; CHECK-NEXT: hvc #0x7 +; CHECK-NEXT: ret + %r = call i64 @llvm.aarch64.hvc(i32 7, i64 %a, i64 %b, i64 %c, i64 %d) + ret i64 %r +} + +; The argument side effects must be evaluated and the result kept live across +; the following call (the hvc clobbers the caller-saved registers). +define i64 @hvc_side_effects() nounwind { +; CHECK-LABEL: hvc_side_effects: +; CHECK: // %bb.0: +; CHECK-NEXT: stp x30, x19, [sp, #-16]! // 16-byte Folded Spill +; CHECK-NEXT: bl g +; CHECK-NEXT: mov x19, x0 +; CHECK-NEXT: bl g +; CHECK-NEXT: mov x1, x0 +; CHECK-NEXT: mov x0, x19 +; CHECK-NEXT: hvc #0x3 +; CHECK-NEXT: mov x19, x0 +; CHECK-NEXT: bl g +; CHECK-NEXT: mov x0, x19 +; CHECK-NEXT: ldp x30, x19, [sp], #16 // 16-byte Folded Reload +; CHECK-NEXT: ret + %x = call i64 @g() + %y = call i64 @g() + %r = call i64 @llvm.aarch64.hvc(i32 3, i64 %x, i64 %y, i64 poison, i64 poison) + call i64 @g() + ret i64 %r +} + +; Registers other than X0 are preserved across the call: matching MSVC, the +; intrinsic does not clobber the caller-saved registers. +define i64 @hvc_preserves_regs(i64 %a, i64 %b, i64 %c) nounwind { +; CHECK-LABEL: hvc_preserves_regs: +; CHECK: // %bb.0: +; CHECK-NEXT: hvc #0x9 +; CHECK-NEXT: add x8, x1, x2 +; CHECK-NEXT: add x0, x8, x0 +; CHECK-NEXT: ret + %r = call i64 @llvm.aarch64.hvc(i32 9, i64 poison, i64 poison, i64 poison, i64 poison) + %t0 = add i64 %b, %c + %t1 = add i64 %t0, %r + ret i64 %t1 +} diff --git a/llvm/test/tools/llvm-mca/AArch64/Neoverse/V1-misc-instructions.s b/llvm/test/tools/llvm-mca/AArch64/Neoverse/V1-misc-instructions.s index 37975ab269d10..0edd3a0f0ee2a 100644 --- a/llvm/test/tools/llvm-mca/AArch64/Neoverse/V1-misc-instructions.s +++ b/llvm/test/tools/llvm-mca/AArch64/Neoverse/V1-misc-instructions.s @@ -39,7 +39,7 @@ sysl x16, #5, c11, c8, #5 # CHECK-NEXT: 1 1 0.13 U dcps3 # CHECK-NEXT: 1 1 0.13 * * U dmb sy # CHECK-NEXT: 1 1 0.13 U hlt #0x7a67 -# CHECK-NEXT: 1 1 0.13 U hvc #0xecb9 +# CHECK-NEXT: 1 1 0.13 * * U hvc #0xecb9 # CHECK-NEXT: 1 1 0.13 * * U isb # CHECK-NEXT: 1 1 0.13 * * U pssbb # CHECK-NEXT: 1 1 0.13 U smc #0x7e57 >From fd2916d086aaec7a0fa3ec4b7c8cf1ae5682d475 Mon Sep 17 00:00:00 2001 From: Adhemerval Zanella <[email protected]> Date: Thu, 4 Jun 2026 15:24:17 -0300 Subject: [PATCH 2/4] [AArch64] Add the __hvc MS intrinsic Add support for the __hvc MS intrinsic on AArch64, lowered via the llvm.aarch64.hvc intrinsic. It generates an HVC (Hypervisor Call) instruction with a 16-bit immediate operand plus up to four arguments passed in X0-X3 and returns the X0 result, matching the MSVC prototype "unsigned int __hvc(unsigned int, ...)". Sema restricts the call to at most five arguments (the immediate plus X0-X3). --- clang/include/clang/Basic/BuiltinsAArch64.td | 1 + .../clang/Basic/DiagnosticSemaKinds.td | 3 ++ clang/lib/CodeGen/TargetBuiltins/ARM.cpp | 38 ++++++++++++++++++- clang/lib/Headers/intrin.h | 1 + clang/lib/Sema/SemaARM.cpp | 19 ++++++++++ .../test/CodeGen/arm64-microsoft-intrinsics.c | 13 +++++++ clang/test/Sema/builtins-microsoft-arm64.c | 12 ++++++ 7 files changed, 85 insertions(+), 2 deletions(-) diff --git a/clang/include/clang/Basic/BuiltinsAArch64.td b/clang/include/clang/Basic/BuiltinsAArch64.td index 15257f3db5b41..64380554b0509 100644 --- a/clang/include/clang/Basic/BuiltinsAArch64.td +++ b/clang/include/clang/Basic/BuiltinsAArch64.td @@ -408,4 +408,5 @@ let Attributes = [NoThrow, RequireDeclaration], Languages = "ALL_MS_LANGUAGES", let Attributes = [NoThrow, RequireDeclaration], Languages = "ALL_MS_LANGUAGES", Header = "intrin.h" in { def __hlt : AArch64NoPrefixTargetLibBuiltin<"unsigned int (unsigned int, ...)">; + def __hvc : AArch64NoPrefixTargetLibBuiltin<"unsigned int (unsigned int, ...)">; } diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index b57ff6c197d1d..1b5c6216e27b3 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -9644,6 +9644,9 @@ def err_atomic_builtin_cannot_be_const : Error< def err_atomic_builtin_must_be_pointer_intfltptr : Error< "address argument to atomic builtin must be a pointer to integer," " floating-point or pointer (%0 invalid)">; +def err_aarch64_svc_hvc_invalid_arg_type : Error< + "%ordinal0 argument to %1 must have integer, floating-point, or pointer " + "type (%2 invalid)">; def err_atomic_builtin_pointer_size : Error< "address argument to atomic builtin must be a pointer to 1,2,4,8 or 16 byte " "type (%0 invalid)">; diff --git a/clang/lib/CodeGen/TargetBuiltins/ARM.cpp b/clang/lib/CodeGen/TargetBuiltins/ARM.cpp index f7c37704d908d..85e3bef8e6e15 100644 --- a/clang/lib/CodeGen/TargetBuiltins/ARM.cpp +++ b/clang/lib/CodeGen/TargetBuiltins/ARM.cpp @@ -5258,11 +5258,45 @@ Value *CodeGenFunction::EmitAArch64BuiltinExpr(unsigned BuiltinID, Function *F = CGM.getIntrinsic(Intrinsic::aarch64_hlt); Builder.CreateCall(F, {EmitScalarExpr(E->getArg(0))}); - // Return 0 for convenience, even though MSVC returns some other undefined - // value. + // FIXME: MSVC documents __hlt as taking further arguments in X0-X3 and + // returning the value in X0, like __hvc/__svc below. This ignores the + // extra arguments and returns 0. return ConstantInt::get(Builder.getInt32Ty(), 0); } + if (BuiltinID == AArch64::BI__hvc) { + // The first argument is the instruction immediate; it must be a constant + // (ImmArg on the intrinsic, encoded in the instruction). The remaining + // arguments (at most four, enforced by Sema) are passed in X0-X3, widened + // to 64 bits. The intrinsic takes exactly four register operands, so any + // unused trailing ones are passed as poison and dropped during lowering. + SmallVector<Value *, 5> Args{Builder.getInt32( + GetIntegerConstantValue<uint32_t>(E->getArg(0), getContext()))}; + for (unsigned I = 1, N = E->getNumArgs(); I < N; ++I) { + Value *Arg = EmitScalarExpr(E->getArg(I)); + llvm::Type *ArgTy = Arg->getType(); + if (ArgTy->isPointerTy()) + Arg = Builder.CreatePtrToInt(Arg, Int64Ty); + else if (ArgTy->isFloatingPointTy()) + // Reinterpret the bits into the integer register, matching MSVC (e.g. + // "fmov x0, d0" for a double). + Arg = Builder.CreateZExtOrTrunc( + Builder.CreateBitCast( + Arg, Builder.getIntNTy(ArgTy->getPrimitiveSizeInBits())), + Int64Ty); + else + Arg = Builder.CreateIntCast( + Arg, Int64Ty, E->getArg(I)->getType()->isSignedIntegerType()); + Args.push_back(Arg); + } + while (Args.size() < 5) + Args.push_back(llvm::PoisonValue::get(Int64Ty)); + Value *Call = + Builder.CreateCall(CGM.getIntrinsic(Intrinsic::aarch64_hvc), Args); + // MSVC returns unsigned int, i.e. the low 32 bits of the X0 result. + return Builder.CreateTrunc(Call, Int32Ty); + } + if (BuiltinID == NEON::BI__builtin_neon_vcvth_bf16_f32) return Builder.CreateFPTrunc( Builder.CreateBitCast(EmitScalarExpr(E->getArg(0)), diff --git a/clang/lib/Headers/intrin.h b/clang/lib/Headers/intrin.h index 4cb8cac960bcf..f8a7cbb4f8192 100644 --- a/clang/lib/Headers/intrin.h +++ b/clang/lib/Headers/intrin.h @@ -448,6 +448,7 @@ unsigned int _CountTrailingZeros(unsigned long); unsigned int _CountTrailingZeros64(unsigned __int64); unsigned int __hlt(unsigned int, ...); +unsigned int __hvc(unsigned int, ...); void __cdecl __prefetch(const void *); void __cdecl __prefetch2(const void *, unsigned char); diff --git a/clang/lib/Sema/SemaARM.cpp b/clang/lib/Sema/SemaARM.cpp index f549016bb17ae..4d2938093d69f 100644 --- a/clang/lib/Sema/SemaARM.cpp +++ b/clang/lib/Sema/SemaARM.cpp @@ -1193,6 +1193,25 @@ bool SemaARM::CheckAArch64BuiltinFunctionCall(const TargetInfo &TI, if (BuiltinID == AArch64::BI__hlt) return SemaRef.BuiltinConstantArgRange(TheCall, 0, 0, 0xffff); + if (BuiltinID == AArch64::BI__hvc) { + // The immediate is the instruction number; the remaining arguments (at most + // four) are passed in X0-X3, so the call takes at most five arguments. + if (SemaRef.checkArgCountAtMost(TheCall, 5) || + SemaRef.BuiltinConstantArgRange(TheCall, 0, 0, 0xffff)) + return true; + const FunctionDecl *FD = TheCall->getDirectCallee(); + for (unsigned I = 1, N = TheCall->getNumArgs(); I < N; ++I) { + const Expr *Arg = TheCall->getArg(I); + QualType Ty = Arg->getType(); + if (!Ty->isIntegerType() && !Ty->isAnyPointerType() && + !Ty->isBlockPointerType() && !Ty->isFloatingType()) + return Diag(Arg->getBeginLoc(), + diag::err_aarch64_svc_hvc_invalid_arg_type) + << I + 1 << FD << Ty << Arg->getSourceRange(); + } + return false; + } + if (CheckNeonBuiltinFunctionCall(TI, BuiltinID, TheCall)) return true; diff --git a/clang/test/CodeGen/arm64-microsoft-intrinsics.c b/clang/test/CodeGen/arm64-microsoft-intrinsics.c index e6a415a0d8805..d9797681946a3 100644 --- a/clang/test/CodeGen/arm64-microsoft-intrinsics.c +++ b/clang/test/CodeGen/arm64-microsoft-intrinsics.c @@ -147,6 +147,19 @@ void check__hlt() { // CHECK-MSVC: call void @llvm.aarch64.hlt(i32 0) // CHECK-LINUX: error: call to undeclared function '__hlt' +unsigned int check__hvc(unsigned int a, unsigned int b) { + __hvc(0); + return __hvc(1, a, b); +} + +// CHECK-MSVC-LABEL: define {{.*}} i32 @check__hvc( +// CHECK-MSVC: call i64 @llvm.aarch64.hvc(i32 0, i64 poison, i64 poison, i64 poison, i64 poison) +// CHECK-MSVC: [[HVC_A:%.*]] = zext i32 {{.*}} to i64 +// CHECK-MSVC: [[HVC_B:%.*]] = zext i32 {{.*}} to i64 +// CHECK-MSVC: [[HVC_R:%.*]] = call i64 @llvm.aarch64.hvc(i32 1, i64 [[HVC_A]], i64 [[HVC_B]], i64 poison, i64 poison) +// CHECK-MSVC: trunc i64 [[HVC_R]] to i32 +// CHECK-LINUX: error: call to undeclared function '__hvc' + unsigned __int64 check__getReg(void) { unsigned volatile __int64 reg; reg = __getReg(18); diff --git a/clang/test/Sema/builtins-microsoft-arm64.c b/clang/test/Sema/builtins-microsoft-arm64.c index 22163ab3fa851..01ffa1fae0cd3 100644 --- a/clang/test/Sema/builtins-microsoft-arm64.c +++ b/clang/test/Sema/builtins-microsoft-arm64.c @@ -14,6 +14,18 @@ void check__hlt() { __hlt(65536); // expected-error-re {{argument value {{.*}} is outside the valid range}} } +struct NonScalar { int a, b; }; + +void check__hvc(unsigned int x, double d, void *p, struct NonScalar s) { + __hvc(-1); // expected-error-re {{argument value {{.*}} is outside the valid range}} + __hvc(65536); // expected-error-re {{argument value {{.*}} is outside the valid range}} + __hvc(x); // expected-error {{argument to '__hvc' must be a constant integer}} + __hvc(1, 2, 3, 4, 5); // no-error: immediate plus four register arguments + __hvc(1, 2, 3, 4, 5, 6); // expected-error {{too many arguments to function call, expected at most 5, have 6}} + __hvc(1, d, p, x); // no-error: floating-point, pointer and integer arguments + __hvc(1, s); // expected-error {{2nd argument to '__hvc' must have integer, floating-point, or pointer type}} +} + void check__getReg(void) { __getReg(-1); // expected-error-re {{argument value {{.*}} is outside the valid range}} __getReg(32); // expected-error-re {{argument value {{.*}} is outside the valid range}} >From 1e968b8e9a3cbe5e70dee7585003597608adb774 Mon Sep 17 00:00:00 2001 From: Adhemerval Zanella <[email protected]> Date: Thu, 28 May 2026 13:51:44 -0300 Subject: [PATCH 3/4] [AArch64] Add the llvm.aarch64.svc intrinsic Add the llvm.aarch64.svc intrinsic alongside llvm.aarch64.hvc, wired to the SVC (Supervisor Call) instruction in the AArch64 backend. Like hvc, it takes the 16-bit instruction immediate plus up to four register arguments passed in X0-X3, returns the value left in X0, and is custom-lowered as a small call with a caller-saved clobber mask. It is not IntrNoReturn since the supervisor can return control to the caller. --- llvm/include/llvm/IR/IntrinsicsAArch64.td | 4 + .../Target/AArch64/AArch64ISelLowering.cpp | 11 ++- llvm/lib/Target/AArch64/AArch64InstrInfo.td | 17 ++-- llvm/test/CodeGen/AArch64/arm64-svc.ll | 78 +++++++++++++++++++ .../AArch64/Neoverse/V1-misc-instructions.s | 2 +- 5 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 llvm/test/CodeGen/AArch64/arm64-svc.ll diff --git a/llvm/include/llvm/IR/IntrinsicsAArch64.td b/llvm/include/llvm/IR/IntrinsicsAArch64.td index 9e6fd17f8b4db..074f26cad2132 100644 --- a/llvm/include/llvm/IR/IntrinsicsAArch64.td +++ b/llvm/include/llvm/IR/IntrinsicsAArch64.td @@ -81,6 +81,10 @@ def int_aarch64_hvc : Intrinsic<[llvm_i64_ty], [llvm_i32_ty, llvm_i64_ty, llvm_i64_ty, llvm_i64_ty, llvm_i64_ty], [IntrHasSideEffects, ImmArg<ArgIndex<0>>]>; +def int_aarch64_svc : Intrinsic<[llvm_i64_ty], + [llvm_i32_ty, llvm_i64_ty, llvm_i64_ty, llvm_i64_ty, llvm_i64_ty], + [IntrHasSideEffects, ImmArg<ArgIndex<0>>]>; + def int_aarch64_prefetch : Intrinsic<[], [llvm_ptr_ty, llvm_i32_ty, llvm_i32_ty, llvm_i32_ty, llvm_i32_ty], [IntrInaccessibleMemOrArgMemOnly, IntrWillReturn, ReadOnly<ArgIndex<0>>, diff --git a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp index 69fcf2b8e9f6c..fdb806bdac1ef 100644 --- a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp +++ b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp @@ -6586,9 +6586,10 @@ SDValue AArch64TargetLowering::LowerINTRINSIC_W_CHAIN(SDValue Op, switch (IntNo) { default: return SDValue(); // Don't custom lower most intrinsics. + case Intrinsic::aarch64_svc: case Intrinsic::aarch64_hvc: { - // The MSVC __hvc intrinsic takes the 16-bit instruction immediate as its - // first operand and four further operands passed in X0-X3 (an unused + // The MSVC __svc/__hvc intrinsic takes the 16-bit instruction immediate as + // their first operand and four further operands passed in X0-X3 (an unused // argument is passed as poison) and returns the value left in X0. Matching // MSVC, the instruction is not treated as clobbering the caller-saved // registers; only X0 (the result) is defined. @@ -6616,8 +6617,10 @@ SDValue AArch64TargetLowering::LowerINTRINSIC_W_CHAIN(SDValue Op, if (Glue.getNode()) Ops.push_back(Glue); - SDValue Node = DAG.getNode(AArch64ISD::HVC, DL, - DAG.getVTList(MVT::Other, MVT::Glue), Ops); + unsigned Opc = + IntNo == Intrinsic::aarch64_svc ? AArch64ISD::SVC : AArch64ISD::HVC; + SDValue Node = + DAG.getNode(Opc, DL, DAG.getVTList(MVT::Other, MVT::Glue), Ops); Chain = Node.getValue(0); Glue = Node.getValue(1); diff --git a/llvm/lib/Target/AArch64/AArch64InstrInfo.td b/llvm/lib/Target/AArch64/AArch64InstrInfo.td index f897727f74d26..3308adb748cbe 100644 --- a/llvm/lib/Target/AArch64/AArch64InstrInfo.td +++ b/llvm/lib/Target/AArch64/AArch64InstrInfo.td @@ -3803,12 +3803,15 @@ def : Pat<(AArch64call texternalsym:$func), (BL texternalsym:$func)>; // Exception generation instructions. //===----------------------------------------------------------------------===// -// Node for the HVC exception-generating call. The single operand is the +// Nodes for the HVC/SVC exception-generating calls. The single operand is the // 16-bit instruction immediate; the argument registers, clobber mask and glue // are attached as variadic operands during lowering. def AArch64hvc : SDNode<"AArch64ISD::HVC", SDTypeProfile<0, 1, [SDTCisInt<0>]>, [SDNPHasChain, SDNPOptInGlue, SDNPOutGlue, SDNPVariadic, SDNPMayLoad, SDNPMayStore]>; +def AArch64svc : SDNode<"AArch64ISD::SVC", SDTypeProfile<0, 1, [SDTCisInt<0>]>, + [SDNPHasChain, SDNPOptInGlue, SDNPOutGlue, SDNPVariadic, + SDNPMayLoad, SDNPMayStore]>; let isTrap = 1 in { def BRK : ExceptionGeneration<0b001, 0b00, "brk", @@ -3819,16 +3822,18 @@ def DCPS2 : ExceptionGeneration<0b101, 0b10, "dcps2">; def DCPS3 : ExceptionGeneration<0b101, 0b11, "dcps3">, Requires<[HasEL3]>; def HLT : ExceptionGeneration<0b010, 0b00, "hlt", [(int_aarch64_hlt timm32_0_65535:$imm)]>; -// Following the Microsoft __hvc calling convention (a software convention, not -// an architectural rule), HVC passes up to four arguments in X0-X3 and returns -// a value in X0. It is therefore selected from the AArch64hvc node (built by -// LowerINTRINSIC_W_CHAIN) rather than a plain intrinsic pattern, and defines X0 -// for the result. +// Following the Microsoft __hvc/__svc calling convention (a software +// convention, not an architectural rule), HVC and SVC pass up to four arguments +// in X0-X3 and return a value in X0. They are therefore selected from the +// AArch64hvc/AArch64svc nodes (built by LowerINTRINSIC_W_CHAIN) rather than a +// plain intrinsic pattern, and define X0 for the result. let mayLoad = 1, mayStore = 1, Defs = [X0] in def HVC : ExceptionGeneration<0b000, 0b10, "hvc">; def SMC : ExceptionGeneration<0b000, 0b11, "smc">, Requires<[HasEL3]>; +let mayLoad = 1, mayStore = 1, Defs = [X0] in def SVC : ExceptionGeneration<0b000, 0b01, "svc">; def : Pat<(AArch64hvc timm32_0_65535:$imm), (HVC timm32_0_65535:$imm)>; +def : Pat<(AArch64svc timm32_0_65535:$imm), (SVC timm32_0_65535:$imm)>; // DCPSn defaults to an immediate operand of zero if unspecified. def : InstAlias<"dcps1", (DCPS1 0)>; diff --git a/llvm/test/CodeGen/AArch64/arm64-svc.ll b/llvm/test/CodeGen/AArch64/arm64-svc.ll new file mode 100644 index 0000000000000..9dbba8e75dfeb --- /dev/null +++ b/llvm/test/CodeGen/AArch64/arm64-svc.ll @@ -0,0 +1,78 @@ +; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6 +; RUN: llc < %s -mtriple=aarch64-none-eabi | FileCheck %s + +declare i64 @llvm.aarch64.svc(i32 immarg, i64, i64, i64, i64) nounwind +declare i64 @g() nounwind + +; No register arguments; the immediate is encoded in the instruction. +define i64 @svc_no_args() nounwind { +; CHECK-LABEL: svc_no_args: +; CHECK: // %bb.0: +; CHECK-NEXT: svc #0x2 +; CHECK-NEXT: ret + %r = call i64 @llvm.aarch64.svc(i32 2, i64 poison, i64 poison, i64 poison, i64 poison) + ret i64 %r +} + +; Register arguments are passed in X0-X3 (here swapped to force the moves), +; and the result is read back from X0. +define i64 @svc_two_args(i64 %a, i64 %b) nounwind { +; CHECK-LABEL: svc_two_args: +; CHECK: // %bb.0: +; CHECK-NEXT: mov x8, x0 +; CHECK-NEXT: mov x0, x1 +; CHECK-NEXT: mov x1, x8 +; CHECK-NEXT: svc #0x1 +; CHECK-NEXT: ret + %r = call i64 @llvm.aarch64.svc(i32 1, i64 %b, i64 %a, i64 poison, i64 poison) + ret i64 %r +} + +; Up to four register arguments. +define i64 @svc_four_args(i64 %a, i64 %b, i64 %c, i64 %d) nounwind { +; CHECK-LABEL: svc_four_args: +; CHECK: // %bb.0: +; CHECK-NEXT: svc #0x7 +; CHECK-NEXT: ret + %r = call i64 @llvm.aarch64.svc(i32 7, i64 %a, i64 %b, i64 %c, i64 %d) + ret i64 %r +} + +; The argument side effects must be evaluated and the result kept live across +; the following call (the svc clobbers the caller-saved registers). +define i64 @svc_side_effects() nounwind { +; CHECK-LABEL: svc_side_effects: +; CHECK: // %bb.0: +; CHECK-NEXT: stp x30, x19, [sp, #-16]! // 16-byte Folded Spill +; CHECK-NEXT: bl g +; CHECK-NEXT: mov x19, x0 +; CHECK-NEXT: bl g +; CHECK-NEXT: mov x1, x0 +; CHECK-NEXT: mov x0, x19 +; CHECK-NEXT: svc #0x3 +; CHECK-NEXT: mov x19, x0 +; CHECK-NEXT: bl g +; CHECK-NEXT: mov x0, x19 +; CHECK-NEXT: ldp x30, x19, [sp], #16 // 16-byte Folded Reload +; CHECK-NEXT: ret + %x = call i64 @g() + %y = call i64 @g() + %r = call i64 @llvm.aarch64.svc(i32 3, i64 %x, i64 %y, i64 poison, i64 poison) + call i64 @g() + ret i64 %r +} + +; Registers other than X0 are preserved across the call: matching MSVC, the +; intrinsic does not clobber the caller-saved registers. +define i64 @svc_preserves_regs(i64 %a, i64 %b, i64 %c) nounwind { +; CHECK-LABEL: svc_preserves_regs: +; CHECK: // %bb.0: +; CHECK-NEXT: svc #0x9 +; CHECK-NEXT: add x8, x1, x2 +; CHECK-NEXT: add x0, x8, x0 +; CHECK-NEXT: ret + %r = call i64 @llvm.aarch64.svc(i32 9, i64 poison, i64 poison, i64 poison, i64 poison) + %t0 = add i64 %b, %c + %t1 = add i64 %t0, %r + ret i64 %t1 +} diff --git a/llvm/test/tools/llvm-mca/AArch64/Neoverse/V1-misc-instructions.s b/llvm/test/tools/llvm-mca/AArch64/Neoverse/V1-misc-instructions.s index 0edd3a0f0ee2a..fc2710ae216bd 100644 --- a/llvm/test/tools/llvm-mca/AArch64/Neoverse/V1-misc-instructions.s +++ b/llvm/test/tools/llvm-mca/AArch64/Neoverse/V1-misc-instructions.s @@ -43,7 +43,7 @@ sysl x16, #5, c11, c8, #5 # CHECK-NEXT: 1 1 0.13 * * U isb # CHECK-NEXT: 1 1 0.13 * * U pssbb # CHECK-NEXT: 1 1 0.13 U smc #0x7e57 -# CHECK-NEXT: 1 1 0.13 U svc #0x89cb +# CHECK-NEXT: 1 1 0.13 * * U svc #0x89cb # CHECK-NEXT: 1 1 0.13 U sysl x16, #5, c11, c8, #5 # CHECK: Resources: >From 676b05e350db40635af3b092cfbd6e56442492e1 Mon Sep 17 00:00:00 2001 From: Adhemerval Zanella <[email protected]> Date: Thu, 28 May 2026 13:52:04 -0300 Subject: [PATCH 4/4] [AArch64] Add the __svc MS intrinsic Add support for the __svc MS intrinsic on AArch64, lowered via the llvm.aarch64.svc intrinsic and mirroring __hvc. It generates an SVC (Supervisor Call) instruction with a 16-bit immediate operand plus up to four arguments passed in X0-X3 and returns the X0 result, matching the MSVC prototype "unsigned int __svc(unsigned int, ...)". Sema restricts the call to at most five arguments. --- clang/include/clang/Basic/BuiltinsAArch64.td | 1 + clang/lib/CodeGen/TargetBuiltins/ARM.cpp | 7 ++++--- clang/lib/Headers/intrin.h | 1 + clang/lib/Sema/SemaARM.cpp | 2 +- clang/test/CodeGen/arm64-microsoft-intrinsics.c | 13 +++++++++++++ clang/test/Sema/builtins-microsoft-arm64.c | 10 ++++++++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/clang/include/clang/Basic/BuiltinsAArch64.td b/clang/include/clang/Basic/BuiltinsAArch64.td index 64380554b0509..4c49c874133fb 100644 --- a/clang/include/clang/Basic/BuiltinsAArch64.td +++ b/clang/include/clang/Basic/BuiltinsAArch64.td @@ -409,4 +409,5 @@ let Attributes = [NoThrow, RequireDeclaration], Languages = "ALL_MS_LANGUAGES", let Attributes = [NoThrow, RequireDeclaration], Languages = "ALL_MS_LANGUAGES", Header = "intrin.h" in { def __hlt : AArch64NoPrefixTargetLibBuiltin<"unsigned int (unsigned int, ...)">; def __hvc : AArch64NoPrefixTargetLibBuiltin<"unsigned int (unsigned int, ...)">; + def __svc : AArch64NoPrefixTargetLibBuiltin<"unsigned int (unsigned int, ...)">; } diff --git a/clang/lib/CodeGen/TargetBuiltins/ARM.cpp b/clang/lib/CodeGen/TargetBuiltins/ARM.cpp index 85e3bef8e6e15..45b0f3d0143e5 100644 --- a/clang/lib/CodeGen/TargetBuiltins/ARM.cpp +++ b/clang/lib/CodeGen/TargetBuiltins/ARM.cpp @@ -5264,7 +5264,9 @@ Value *CodeGenFunction::EmitAArch64BuiltinExpr(unsigned BuiltinID, return ConstantInt::get(Builder.getInt32Ty(), 0); } - if (BuiltinID == AArch64::BI__hvc) { + if (BuiltinID == AArch64::BI__hvc || BuiltinID == AArch64::BI__svc) { + unsigned IID = BuiltinID == AArch64::BI__svc ? Intrinsic::aarch64_svc + : Intrinsic::aarch64_hvc; // The first argument is the instruction immediate; it must be a constant // (ImmArg on the intrinsic, encoded in the instruction). The remaining // arguments (at most four, enforced by Sema) are passed in X0-X3, widened @@ -5291,8 +5293,7 @@ Value *CodeGenFunction::EmitAArch64BuiltinExpr(unsigned BuiltinID, } while (Args.size() < 5) Args.push_back(llvm::PoisonValue::get(Int64Ty)); - Value *Call = - Builder.CreateCall(CGM.getIntrinsic(Intrinsic::aarch64_hvc), Args); + Value *Call = Builder.CreateCall(CGM.getIntrinsic(IID), Args); // MSVC returns unsigned int, i.e. the low 32 bits of the X0 result. return Builder.CreateTrunc(Call, Int32Ty); } diff --git a/clang/lib/Headers/intrin.h b/clang/lib/Headers/intrin.h index f8a7cbb4f8192..7dcb5a526afb2 100644 --- a/clang/lib/Headers/intrin.h +++ b/clang/lib/Headers/intrin.h @@ -449,6 +449,7 @@ unsigned int _CountTrailingZeros64(unsigned __int64); unsigned int __hlt(unsigned int, ...); unsigned int __hvc(unsigned int, ...); +unsigned int __svc(unsigned int, ...); void __cdecl __prefetch(const void *); void __cdecl __prefetch2(const void *, unsigned char); diff --git a/clang/lib/Sema/SemaARM.cpp b/clang/lib/Sema/SemaARM.cpp index 4d2938093d69f..f38a35ec08fb0 100644 --- a/clang/lib/Sema/SemaARM.cpp +++ b/clang/lib/Sema/SemaARM.cpp @@ -1193,7 +1193,7 @@ bool SemaARM::CheckAArch64BuiltinFunctionCall(const TargetInfo &TI, if (BuiltinID == AArch64::BI__hlt) return SemaRef.BuiltinConstantArgRange(TheCall, 0, 0, 0xffff); - if (BuiltinID == AArch64::BI__hvc) { + if (BuiltinID == AArch64::BI__hvc || BuiltinID == AArch64::BI__svc) { // The immediate is the instruction number; the remaining arguments (at most // four) are passed in X0-X3, so the call takes at most five arguments. if (SemaRef.checkArgCountAtMost(TheCall, 5) || diff --git a/clang/test/CodeGen/arm64-microsoft-intrinsics.c b/clang/test/CodeGen/arm64-microsoft-intrinsics.c index d9797681946a3..bf2dcf5ce2afd 100644 --- a/clang/test/CodeGen/arm64-microsoft-intrinsics.c +++ b/clang/test/CodeGen/arm64-microsoft-intrinsics.c @@ -160,6 +160,19 @@ unsigned int check__hvc(unsigned int a, unsigned int b) { // CHECK-MSVC: trunc i64 [[HVC_R]] to i32 // CHECK-LINUX: error: call to undeclared function '__hvc' +unsigned int check__svc(unsigned int a, unsigned int b) { + __svc(0); + return __svc(1, a, b); +} + +// CHECK-MSVC-LABEL: define {{.*}} i32 @check__svc( +// CHECK-MSVC: call i64 @llvm.aarch64.svc(i32 0, i64 poison, i64 poison, i64 poison, i64 poison) +// CHECK-MSVC: [[SVC_A:%.*]] = zext i32 {{.*}} to i64 +// CHECK-MSVC: [[SVC_B:%.*]] = zext i32 {{.*}} to i64 +// CHECK-MSVC: [[SVC_R:%.*]] = call i64 @llvm.aarch64.svc(i32 1, i64 [[SVC_A]], i64 [[SVC_B]], i64 poison, i64 poison) +// CHECK-MSVC: trunc i64 [[SVC_R]] to i32 +// CHECK-LINUX: error: call to undeclared function '__svc' + unsigned __int64 check__getReg(void) { unsigned volatile __int64 reg; reg = __getReg(18); diff --git a/clang/test/Sema/builtins-microsoft-arm64.c b/clang/test/Sema/builtins-microsoft-arm64.c index 01ffa1fae0cd3..2093adbb349a4 100644 --- a/clang/test/Sema/builtins-microsoft-arm64.c +++ b/clang/test/Sema/builtins-microsoft-arm64.c @@ -26,6 +26,16 @@ void check__hvc(unsigned int x, double d, void *p, struct NonScalar s) { __hvc(1, s); // expected-error {{2nd argument to '__hvc' must have integer, floating-point, or pointer type}} } +void check__svc(unsigned int x, double d, void *p, struct NonScalar s) { + __svc(-1); // expected-error-re {{argument value {{.*}} is outside the valid range}} + __svc(65536); // expected-error-re {{argument value {{.*}} is outside the valid range}} + __svc(x); // expected-error {{argument to '__svc' must be a constant integer}} + __svc(1, 2, 3, 4, 5); // no-error: immediate plus four register arguments + __svc(1, 2, 3, 4, 5, 6); // expected-error {{too many arguments to function call, expected at most 5, have 6}} + __svc(1, d, p, x); // no-error: floating-point, pointer and integer arguments + __svc(1, s); // expected-error {{2nd argument to '__svc' must have integer, floating-point, or pointer type}} +} + void check__getReg(void) { __getReg(-1); // expected-error-re {{argument value {{.*}} is outside the valid range}} __getReg(32); // expected-error-re {{argument value {{.*}} is outside the valid range}} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
