/** @file\r
CPU MP Initialize Library common functions.\r
\r
- Copyright (c) 2016 - 2018, Intel Corporation. All rights reserved.<BR>\r
- This program and the accompanying materials\r
- are licensed and made available under the terms and conditions of the BSD License\r
- which accompanies this distribution. The full text of the license may be found at\r
- http://opensource.org/licenses/bsd-license.php\r
-\r
- THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,\r
- WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.\r
+ Copyright (c) 2016 - 2019, Intel Corporation. All rights reserved.<BR>\r
+ SPDX-License-Identifier: BSD-2-Clause-Patent\r
\r
**/\r
\r
CPUID_VERSION_INFO_EDX VersionInfoEdx;\r
IA32_TSS_DESCRIPTOR *Tss;\r
\r
- AsmWriteCr0 (VolatileRegisters->Cr0);\r
AsmWriteCr3 (VolatileRegisters->Cr3);\r
AsmWriteCr4 (VolatileRegisters->Cr4);\r
+ AsmWriteCr0 (VolatileRegisters->Cr0);\r
\r
if (IsRestoreDr) {\r
AsmCpuid (CPUID_VERSION_INFO, NULL, NULL, NULL, &VersionInfoEdx.Uint32);\r
//\r
// Load microcode on AP\r
//\r
- MicrocodeDetect (CpuMpData);\r
+ MicrocodeDetect (CpuMpData, FALSE);\r
//\r
// Sync BSP's MTRR table to AP\r
//\r
UINTN TotalProcessorNumber;\r
UINTN Index;\r
CPU_INFO_IN_HOB *CpuInfoInHob;\r
+ UINT32 CurrentApicId;\r
\r
CpuInfoInHob = (CPU_INFO_IN_HOB *) (UINTN) CpuMpData->CpuInfoInHob;\r
\r
TotalProcessorNumber = CpuMpData->CpuCount;\r
+ CurrentApicId = GetApicId ();\r
for (Index = 0; Index < TotalProcessorNumber; Index ++) {\r
- if (CpuInfoInHob[Index].ApicId == GetApicId ()) {\r
+ if (CpuInfoInHob[Index].ApicId == CurrentApicId) {\r
*ProcessorNumber = Index;\r
return EFI_SUCCESS;\r
}\r
}\r
+\r
return EFI_NOT_FOUND;\r
}\r
\r
//\r
CpuMpData->InitFlag = ApInitConfig;\r
CpuMpData->X2ApicEnable = FALSE;\r
- WakeUpAP (CpuMpData, TRUE, 0, NULL, NULL);\r
+ WakeUpAP (CpuMpData, TRUE, 0, NULL, NULL, TRUE);\r
CpuMpData->InitFlag = ApInitDone;\r
ASSERT (CpuMpData->CpuCount <= PcdGet32 (PcdCpuMaxLogicalProcessorNumber));\r
//\r
//\r
// Wakeup all APs to enable x2APIC mode\r
//\r
- WakeUpAP (CpuMpData, TRUE, 0, ApFuncEnableX2Apic, NULL);\r
+ WakeUpAP (CpuMpData, TRUE, 0, ApFuncEnableX2Apic, NULL, TRUE);\r
//\r
// Wait for all known APs finished\r
//\r
RestoreVolatileRegisters (&CpuMpData->CpuData[0].VolatileRegisters, FALSE);\r
InitializeApData (CpuMpData, ProcessorNumber, BistData, ApTopOfStack);\r
ApStartupSignalBuffer = CpuMpData->CpuData[ProcessorNumber].StartupApSignal;\r
+\r
+ InterlockedDecrement ((UINT32 *) &CpuMpData->MpCpuExchangeInfo->NumApsExecuting);\r
} else {\r
//\r
// Execute AP function if AP is ready\r
// AP finished executing C code\r
//\r
InterlockedIncrement ((UINT32 *) &CpuMpData->FinishedCount);\r
- InterlockedDecrement ((UINT32 *) &CpuMpData->MpCpuExchangeInfo->NumApsExecuting);\r
\r
//\r
// Place AP is specified loop mode\r
volatile MP_CPU_EXCHANGE_INFO *ExchangeInfo;\r
UINTN Size;\r
IA32_SEGMENT_DESCRIPTOR *Selector;\r
+ IA32_CR4 Cr4;\r
\r
ExchangeInfo = CpuMpData->MpCpuExchangeInfo;\r
ExchangeInfo->Lock = 0;\r
\r
ExchangeInfo->InitializeFloatingPointUnitsAddress = (UINTN)InitializeFloatingPointUnits;\r
\r
+ //\r
+ // We can check either CPUID(7).ECX[bit16] or check CR4.LA57[bit12]\r
+ // to determin whether 5-Level Paging is enabled.\r
+ // CPUID(7).ECX[bit16] shows CPU's capability, CR4.LA57[bit12] shows\r
+ // current system setting.\r
+ // Using latter way is simpler because it also eliminates the needs to\r
+ // check whether platform wants to enable it.\r
+ //\r
+ Cr4.UintN = AsmReadCr4 ();\r
+ ExchangeInfo->Enable5LevelPaging = (BOOLEAN) (Cr4.Bits.LA57 == 1);\r
+ DEBUG ((DEBUG_INFO, "%a: 5-Level Paging = %d\n", gEfiCallerBaseName, ExchangeInfo->Enable5LevelPaging));\r
+\r
//\r
// Get the BSP's data of GDT and IDT\r
//\r
@param[in] ProcessorNumber The handle number of specified processor\r
@param[in] Procedure The function to be invoked by AP\r
@param[in] ProcedureArgument The argument to be passed into AP function\r
+ @param[in] WakeUpDisabledAps Whether need to wake up disabled APs in broadcast mode.\r
**/\r
VOID\r
WakeUpAP (\r
IN BOOLEAN Broadcast,\r
IN UINTN ProcessorNumber,\r
IN EFI_AP_PROCEDURE Procedure, OPTIONAL\r
- IN VOID *ProcedureArgument OPTIONAL\r
+ IN VOID *ProcedureArgument, OPTIONAL\r
+ IN BOOLEAN WakeUpDisabledAps\r
)\r
{\r
volatile MP_CPU_EXCHANGE_INFO *ExchangeInfo;\r
CpuMpData->FinishedCount = 0;\r
ResetVectorRequired = FALSE;\r
\r
- if (CpuMpData->ApLoopMode == ApInHltLoop ||\r
+ if (CpuMpData->WakeUpByInitSipiSipi ||\r
CpuMpData->InitFlag != ApInitDone) {\r
ResetVectorRequired = TRUE;\r
AllocateResetVector (CpuMpData);\r
FillExchangeInfoData (CpuMpData);\r
SaveLocalApicTimerSetting (CpuMpData);\r
- } else if (CpuMpData->ApLoopMode == ApInMwaitLoop) {\r
+ }\r
+\r
+ if (CpuMpData->ApLoopMode == ApInMwaitLoop) {\r
//\r
// Get AP target C-state each time when waking up AP,\r
// for it maybe updated by platform again\r
for (Index = 0; Index < CpuMpData->CpuCount; Index++) {\r
if (Index != CpuMpData->BspNumber) {\r
CpuData = &CpuMpData->CpuData[Index];\r
+ //\r
+ // All AP(include disabled AP) will be woke up by INIT-SIPI-SIPI, but\r
+ // the AP procedure will be skipped for disabled AP because AP state\r
+ // is not CpuStateReady.\r
+ //\r
+ if (GetApState (CpuData) == CpuStateDisabled && !WakeUpDisabledAps) {\r
+ continue;\r
+ }\r
+\r
CpuData->ApFunction = (UINTN) Procedure;\r
CpuData->ApFunctionArgument = (UINTN) ProcedureArgument;\r
SetApState (CpuData, CpuStateReady);\r
}\r
if (CpuMpData->InitFlag == ApInitConfig) {\r
//\r
- // Here support two methods to collect AP count through adjust\r
- // PcdCpuApInitTimeOutInMicroSeconds values.\r
+ // The AP enumeration algorithm below is suitable for two use cases.\r
+ //\r
+ // (1) The check-in time for an individual AP is bounded, and APs run\r
+ // through their initialization routines strongly concurrently. In\r
+ // particular, the number of concurrently running APs\r
+ // ("NumApsExecuting") is never expected to fall to zero\r
+ // *temporarily* -- it is expected to fall to zero only when all\r
+ // APs have checked-in.\r
+ //\r
+ // In this case, the platform is supposed to set\r
+ // PcdCpuApInitTimeOutInMicroSeconds to a low-ish value (just long\r
+ // enough for one AP to start initialization). The timeout will be\r
+ // reached soon, and remaining APs are collected by watching\r
+ // NumApsExecuting fall to zero. If NumApsExecuting falls to zero\r
+ // mid-process, while some APs have not completed initialization,\r
+ // the behavior is undefined.\r
+ //\r
+ // (2) The check-in time for an individual AP is unbounded, and/or APs\r
+ // may complete their initializations widely spread out. In\r
+ // particular, some APs may finish initialization before some APs\r
+ // even start.\r
//\r
- // one way is set a value to just let the first AP to start the\r
- // initialization, then through the later while loop to wait all Aps\r
- // finsh the initialization.\r
- // The other way is set a value to let all APs finished the initialzation.\r
- // In this case, the later while loop is useless.\r
+ // In this case, the platform is supposed to set\r
+ // PcdCpuApInitTimeOutInMicroSeconds to a high-ish value. The AP\r
+ // enumeration will always take that long (except when the boot CPU\r
+ // count happens to be maximal, that is,\r
+ // PcdCpuMaxLogicalProcessorNumber). All APs are expected to\r
+ // check-in before the timeout, and NumApsExecuting is assumed zero\r
+ // at timeout. APs that miss the time-out may cause undefined\r
+ // behavior.\r
//\r
TimedWaitForApFinish (\r
CpuMpData,\r
if (ResetVectorRequired) {\r
FreeResetVector (CpuMpData);\r
}\r
+\r
+ //\r
+ // After one round of Wakeup Ap actions, need to re-sync ApLoopMode with\r
+ // WakeUpByInitSipiSipi flag. WakeUpByInitSipiSipi flag maybe changed by\r
+ // S3SmmInitDone Ppi.\r
+ //\r
+ CpuMpData->WakeUpByInitSipiSipi = (CpuMpData->ApLoopMode == ApInHltLoop);\r
}\r
\r
/**\r
CpuMpData = GetCpuMpData ();\r
\r
CpuMpData->InitFlag = ApInitReconfig;\r
- WakeUpAP (CpuMpData, FALSE, ProcessorNumber, NULL, NULL);\r
+ WakeUpAP (CpuMpData, FALSE, ProcessorNumber, NULL, NULL, TRUE);\r
while (CpuMpData->FinishedCount < 1) {\r
CpuPause ();\r
}\r
CpuData = &CpuMpData->CpuData[ProcessorNumber];\r
\r
//\r
- // Check the CPU state of AP. If it is CpuStateFinished, then the AP has finished its task.\r
+ // Check the CPU state of AP. If it is CpuStateIdle, then the AP has finished its task.\r
// Only BSP and corresponding AP access this unit of CPU Data. This means the AP will not modify the\r
- // value of state after setting the it to CpuStateFinished, so BSP can safely make use of its value.\r
+ // value of state after setting the it to CpuStateIdle, so BSP can safely make use of its value.\r
//\r
//\r
// If the AP finishes for StartupThisAP(), return EFI_SUCCESS.\r
\r
CpuData = &CpuMpData->CpuData[ProcessorNumber];\r
//\r
- // Check the CPU state of AP. If it is CpuStateFinished, then the AP has finished its task.\r
+ // Check the CPU state of AP. If it is CpuStateIdle, then the AP has finished its task.\r
// Only BSP and corresponding AP access this unit of CPU Data. This means the AP will not modify the\r
- // value of state after setting the it to CpuStateFinished, so BSP can safely make use of its value.\r
+ // value of state after setting the it to CpuStateIdle, so BSP can safely make use of its value.\r
//\r
if (GetApState(CpuData) == CpuStateFinished) {\r
- CpuMpData->RunningCount ++;\r
+ CpuMpData->RunningCount --;\r
CpuMpData->CpuData[ProcessorNumber].Waiting = FALSE;\r
SetApState(CpuData, CpuStateIdle);\r
\r
FALSE,\r
(UINT32) NextProcessorNumber,\r
CpuMpData->Procedure,\r
- CpuMpData->ProcArguments\r
+ CpuMpData->ProcArguments,\r
+ TRUE\r
);\r
}\r
}\r
//\r
// If all APs finish, return EFI_SUCCESS.\r
//\r
- if (CpuMpData->RunningCount == CpuMpData->StartCount) {\r
+ if (CpuMpData->RunningCount == 0) {\r
return EFI_SUCCESS;\r
}\r
\r
//\r
if (CpuMpData->FailedCpuList != NULL) {\r
*CpuMpData->FailedCpuList =\r
- AllocatePool ((CpuMpData->StartCount - CpuMpData->FinishedCount + 1) * sizeof (UINTN));\r
+ AllocatePool ((CpuMpData->RunningCount + 1) * sizeof (UINTN));\r
ASSERT (*CpuMpData->FailedCpuList != NULL);\r
}\r
ListIndex = 0;\r
ApLoopMode = GetApLoopMode (&MonitorFilterSize);\r
\r
//\r
- // Save BSP's Control registers for APs\r
+ // Save BSP's Control registers for APs.\r
//\r
SaveVolatileRegisters (&VolatileRegisters);\r
\r
CpuMpData->SwitchBspFlag = FALSE;\r
CpuMpData->CpuData = (CPU_AP_DATA *) (CpuMpData + 1);\r
CpuMpData->CpuInfoInHob = (UINT64) (UINTN) (CpuMpData->CpuData + MaxLogicalProcessorNumber);\r
- CpuMpData->MicrocodePatchRegionSize = PcdGet64 (PcdCpuMicrocodePatchRegionSize);\r
- //\r
- // If platform has more than one CPU, relocate microcode to memory to reduce\r
- // loading microcode time.\r
- //\r
- MicrocodePatchInRam = NULL;\r
- if (MaxLogicalProcessorNumber > 1) {\r
- MicrocodePatchInRam = AllocatePages (\r
- EFI_SIZE_TO_PAGES (\r
- (UINTN)CpuMpData->MicrocodePatchRegionSize\r
- )\r
- );\r
- }\r
- if (MicrocodePatchInRam == NULL) {\r
- //\r
- // there is only one processor, or no microcode patch is available, or\r
- // memory allocation failed\r
- //\r
- CpuMpData->MicrocodePatchAddress = PcdGet64 (PcdCpuMicrocodePatchAddress);\r
- } else {\r
+ if (OldCpuMpData == NULL) {\r
+ CpuMpData->MicrocodePatchRegionSize = PcdGet64 (PcdCpuMicrocodePatchRegionSize);\r
//\r
- // there are multiple processors, and a microcode patch is available, and\r
- // memory allocation succeeded\r
+ // If platform has more than one CPU, relocate microcode to memory to reduce\r
+ // loading microcode time.\r
//\r
- CopyMem (\r
- MicrocodePatchInRam,\r
- (VOID *)(UINTN)PcdGet64 (PcdCpuMicrocodePatchAddress),\r
- (UINTN)CpuMpData->MicrocodePatchRegionSize\r
- );\r
- CpuMpData->MicrocodePatchAddress = (UINTN)MicrocodePatchInRam;\r
+ MicrocodePatchInRam = NULL;\r
+ if (MaxLogicalProcessorNumber > 1) {\r
+ MicrocodePatchInRam = AllocatePages (\r
+ EFI_SIZE_TO_PAGES (\r
+ (UINTN)CpuMpData->MicrocodePatchRegionSize\r
+ )\r
+ );\r
+ }\r
+ if (MicrocodePatchInRam == NULL) {\r
+ //\r
+ // there is only one processor, or no microcode patch is available, or\r
+ // memory allocation failed\r
+ //\r
+ CpuMpData->MicrocodePatchAddress = PcdGet64 (PcdCpuMicrocodePatchAddress);\r
+ } else {\r
+ //\r
+ // there are multiple processors, and a microcode patch is available, and\r
+ // memory allocation succeeded\r
+ //\r
+ CopyMem (\r
+ MicrocodePatchInRam,\r
+ (VOID *)(UINTN)PcdGet64 (PcdCpuMicrocodePatchAddress),\r
+ (UINTN)CpuMpData->MicrocodePatchRegionSize\r
+ );\r
+ CpuMpData->MicrocodePatchAddress = (UINTN)MicrocodePatchInRam;\r
+ }\r
+ }else {\r
+ CpuMpData->MicrocodePatchRegionSize = OldCpuMpData->MicrocodePatchRegionSize;\r
+ CpuMpData->MicrocodePatchAddress = OldCpuMpData->MicrocodePatchAddress;\r
}\r
-\r
InitializeSpinLock(&CpuMpData->MpLock);\r
\r
//\r
//\r
CopyMem ((VOID *)ApIdtBase, (VOID *)VolatileRegisters.Idtr.Base, VolatileRegisters.Idtr.Limit + 1);\r
VolatileRegisters.Idtr.Base = ApIdtBase;\r
+ //\r
+ // Don't pass BSP's TR to APs to avoid AP init failure.\r
+ //\r
+ VolatileRegisters.Tr = 0;\r
CopyMem (&CpuMpData->CpuData[0].VolatileRegisters, &VolatileRegisters, sizeof (VolatileRegisters));\r
//\r
// Set BSP basic information\r
//\r
CpuMpData->ApLoopMode = ApLoopMode;\r
DEBUG ((DEBUG_INFO, "AP Loop Mode is %d\n", CpuMpData->ApLoopMode));\r
+\r
+ CpuMpData->WakeUpByInitSipiSipi = (CpuMpData->ApLoopMode == ApInHltLoop);\r
+\r
//\r
// Set up APs wakeup signal buffer\r
//\r
//\r
// Load Microcode on BSP\r
//\r
- MicrocodeDetect (CpuMpData);\r
+ MicrocodeDetect (CpuMpData, TRUE);\r
//\r
// Store BSP's MTRR setting\r
//\r
//\r
// Wakeup APs to do some AP initialize sync\r
//\r
- WakeUpAP (CpuMpData, TRUE, 0, ApInitializeSync, CpuMpData);\r
+ WakeUpAP (CpuMpData, TRUE, 0, ApInitializeSync, CpuMpData, TRUE);\r
//\r
// Wait for all APs finished initialization\r
//\r
//\r
// Need to wakeUp AP (future BSP).\r
//\r
- WakeUpAP (CpuMpData, FALSE, ProcessorNumber, FutureBSPProc, CpuMpData);\r
+ WakeUpAP (CpuMpData, FALSE, ProcessorNumber, FutureBSPProc, CpuMpData, TRUE);\r
\r
AsmExchangeRole (&CpuMpData->BSPInfo, &CpuMpData->APInfo);\r
\r
number. If FALSE, then all the enabled APs\r
execute the function specified by Procedure\r
simultaneously.\r
+ @param[in] ExcludeBsp Whether let BSP also trig this task.\r
@param[in] WaitEvent The event created by the caller with CreateEvent()\r
service.\r
@param[in] TimeoutInMicroseconds Indicates the time limit in microseconds for\r
\r
**/\r
EFI_STATUS\r
-StartupAllAPsWorker (\r
+StartupAllCPUsWorker (\r
IN EFI_AP_PROCEDURE Procedure,\r
IN BOOLEAN SingleThread,\r
+ IN BOOLEAN ExcludeBsp,\r
IN EFI_EVENT WaitEvent OPTIONAL,\r
IN UINTN TimeoutInMicroseconds,\r
IN VOID *ProcedureArgument OPTIONAL,\r
*FailedCpuList = NULL;\r
}\r
\r
- if (CpuMpData->CpuCount == 1) {\r
+ if (CpuMpData->CpuCount == 1 && ExcludeBsp) {\r
return EFI_NOT_STARTED;\r
}\r
\r
}\r
}\r
\r
- if (!HasEnabledAp) {\r
+ if (!HasEnabledAp && ExcludeBsp) {\r
//\r
- // If no enabled AP exists, return EFI_NOT_STARTED.\r
+ // If no enabled AP exists and not include Bsp to do the procedure, return EFI_NOT_STARTED.\r
//\r
return EFI_NOT_STARTED;\r
}\r
\r
- CpuMpData->StartCount = 0;\r
+ CpuMpData->RunningCount = 0;\r
for (ProcessorNumber = 0; ProcessorNumber < ProcessorCount; ProcessorNumber++) {\r
CpuData = &CpuMpData->CpuData[ProcessorNumber];\r
CpuData->Waiting = FALSE;\r
// Mark this processor as responsible for current calling.\r
//\r
CpuData->Waiting = TRUE;\r
- CpuMpData->StartCount++;\r
+ CpuMpData->RunningCount++;\r
}\r
}\r
}\r
CpuMpData->ProcArguments = ProcedureArgument;\r
CpuMpData->SingleThread = SingleThread;\r
CpuMpData->FinishedCount = 0;\r
- CpuMpData->RunningCount = 0;\r
CpuMpData->FailedCpuList = FailedCpuList;\r
CpuMpData->ExpectedTime = CalculateTimeout (\r
TimeoutInMicroseconds,\r
CpuMpData->WaitEvent = WaitEvent;\r
\r
if (!SingleThread) {\r
- WakeUpAP (CpuMpData, TRUE, 0, Procedure, ProcedureArgument);\r
+ WakeUpAP (CpuMpData, TRUE, 0, Procedure, ProcedureArgument, FALSE);\r
} else {\r
for (ProcessorNumber = 0; ProcessorNumber < ProcessorCount; ProcessorNumber++) {\r
if (ProcessorNumber == CallerNumber) {\r
continue;\r
}\r
if (CpuMpData->CpuData[ProcessorNumber].Waiting) {\r
- WakeUpAP (CpuMpData, FALSE, ProcessorNumber, Procedure, ProcedureArgument);\r
+ WakeUpAP (CpuMpData, FALSE, ProcessorNumber, Procedure, ProcedureArgument, TRUE);\r
break;\r
}\r
}\r
}\r
\r
+ if (!ExcludeBsp) {\r
+ //\r
+ // Start BSP.\r
+ //\r
+ Procedure (ProcedureArgument);\r
+ }\r
+\r
Status = EFI_SUCCESS;\r
if (WaitEvent == NULL) {\r
do {\r
CpuData->ExpectedTime = CalculateTimeout (TimeoutInMicroseconds, &CpuData->CurrentTime);\r
CpuData->TotalTime = 0;\r
\r
- WakeUpAP (CpuMpData, FALSE, ProcessorNumber, Procedure, ProcedureArgument);\r
+ WakeUpAP (CpuMpData, FALSE, ProcessorNumber, Procedure, ProcedureArgument, TRUE);\r
\r
//\r
// If WaitEvent is NULL, execute in blocking mode.\r
return CpuMpData;\r
}\r
\r
+/**\r
+ This service executes a caller provided function on all enabled CPUs.\r
+\r
+ @param[in] Procedure A pointer to the function to be run on\r
+ enabled APs of the system. See type\r
+ EFI_AP_PROCEDURE.\r
+ @param[in] TimeoutInMicroseconds Indicates the time limit in microseconds for\r
+ APs to return from Procedure, either for\r
+ blocking or non-blocking mode. Zero means\r
+ infinity. TimeoutInMicroseconds is ignored\r
+ for BSP.\r
+ @param[in] ProcedureArgument The parameter passed into Procedure for\r
+ all APs.\r
+\r
+ @retval EFI_SUCCESS In blocking mode, all CPUs have finished before\r
+ the timeout expired.\r
+ @retval EFI_SUCCESS In non-blocking mode, function has been dispatched\r
+ to all enabled CPUs.\r
+ @retval EFI_DEVICE_ERROR Caller processor is AP.\r
+ @retval EFI_NOT_READY Any enabled APs are busy.\r
+ @retval EFI_NOT_READY MP Initialize Library is not initialized.\r
+ @retval EFI_TIMEOUT In blocking mode, the timeout expired before\r
+ all enabled APs have finished.\r
+ @retval EFI_INVALID_PARAMETER Procedure is NULL.\r
+\r
+**/\r
+EFI_STATUS\r
+EFIAPI\r
+MpInitLibStartupAllCPUs (\r
+ IN EFI_AP_PROCEDURE Procedure,\r
+ IN UINTN TimeoutInMicroseconds,\r
+ IN VOID *ProcedureArgument OPTIONAL\r
+ )\r
+{\r
+ return StartupAllCPUsWorker (\r
+ Procedure,\r
+ FALSE,\r
+ FALSE,\r
+ NULL,\r
+ TimeoutInMicroseconds,\r
+ ProcedureArgument,\r
+ NULL\r
+ );\r
+}\r