RTOS的任务调度原理和所使用的内核中断、寄存器息息相关
文中截图大多是《Cortex-M3与Cortex-M4权威指南》翻译版本里面的内容
需要对内核有一定的了解,本文尽量用简单的描述表达清楚
虽然是FreeRTOS的记录,但是原理上来说对于其他RTOS也是一样的!
目录
- Systick
- Systick 源码解析
- Systick 初始化
- Systick 中断服务函数
- Systick 任务调度
- Systick优先级分析
- 内核中断管理
- Cortex-M的异常类型
- Cortex-M的寄存器
- Cortex-M的特殊寄存器
-
- Cortex-M的工作模式
- 影子栈指针
- PendSV和SVC异常
- 为什么需要 PendSV异常?
- PendSV源码简析
- PendSV中断服务函数
- PendSV上下文切换函数
- 寻找最高优先级函数
- SVC异常
- SVC源码简析
- FreeRTOS多任务启动源码简析
- vTaskStartScheduler
- xPortStartScheduler
- prvPortStartFirstTask
总结写在前面:
在Cortex-M内核上,FreeRTOS使用Systick定时器作为心跳时钟,一般默认心跳时钟为1ms,进入Systick中断后,内核会进入处理模式进行处理,在Systick中断处理中,系统会在 ReadList 就绪链表从高优先级到低优先找需要执行的任务,进行调度,如果有任务的状态发生了变化,改变了状态链表,就会产生一个pendSV异常,进入pendSV异常,通过改变进程栈指针(PSP)切换到不同的任务。
对于相同优先级的任务,每隔一个Systick,运行过的任务被自动排放至该优先级链表的尾部(时间片调度)
用户也可以在线程模式下主动触发PendSV,进行任务切换。
在FreeRTOS中SVC只使用了一次(M0中没有使用),就是第一次。
FreeRTOS进入临界区是通过配置BASEPRI寄存器来进行的。
Systick
我们已经知道,在Cortex-M系列中 systick是作为FreeRTOS 的心跳时钟,是调度器的核心。
系统是在Systick中进行上下文切换。
那么他是如何进行上下文切换的呢,那就得来说说内核的中断管理了,记住一句话
操作系统的入口是中断(好像是废话,嵌入式所有程序的入口都是中断= =!)
Systick 源码解析
Systick 初始化
systick的初始化在port.c
中, vPortSetupTimerInterrupt
函数:
__attribute__(( weak )) void vPortSetupTimerInterrupt( void )
{
#if( configUSE_TICKLESS_IDLE == 1 )
{
ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
}
#endif
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
Systick 中断服务函数
每一节拍进入一次Systick 中断,因为Systick 如果调度器返回true,触发pendSV异常:
void xPortSysTickHandler( void )
{
portDISABLE_INTERRUPTS();
{
if( xTaskIncrementTick() != pdFALSE )
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portENABLE_INTERRUPTS();
}
Systick 任务调度
Systick中断中调用xTaskIncrementTick
任务调度如下,源码注释:
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
traceTASK_INCREMENT_TICK( xTickCount );
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U )
{
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ;; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
xNextTaskUnblockTime = portMAX_DELAY;
break;
}
else
{
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue )
{
xNextTaskUnblockTime = xItemValue;
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
prvAddTaskToReadyList( pxTCB );
#if ( configUSE_PREEMPTION == 1 )
{
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
}
}
}
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
#if ( configUSE_TICK_HOOK == 1 )
{
if( uxPendedTicks == ( UBaseType_t ) 0U )
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
}
else
{
++uxPendedTicks;
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
return xSwitchRequired;
}
Systick优先级分析
结合后面的中断管理和任务调度相关的内容,需要说明一下Systick优先级的问题。先来看一下简单的任务调度模型。
在上面图示中,可以看到优先级SysTick优先级最高!那么这和我们常听到的SysTick优先级需要设置为最低优先级怎么相互冲突呢?初学者往往在这个问题上感到困惑。
首先要明白:SysTick是中断,中断优先级和任务优先级没有任何关系,不管中断优先级是多少,中断的优先级永远高于任何线程任务的优先级
那么在上图中的线程,不管什么线程,SysTick中断来了肯定是需要去执行SysTick中断事件的。
上图中还有一个IRQ,比SysTick优先级低,这也是可能的,但是实际上我们应用过程中,一般都把SysTick优先级设置为最低,因为不想让SysTick中断打断用户的IRQ中断。
那么SysTick中断优先级和外设中断优先级是怎么确定的?
1、SysTick属于内核异常,用SHPRx(x=1.2.3)来设置其优先级;外设中断属于 ISR,用NVIC_IPRx来设置优先级。
SPRH1-SPRH3是一个32位的寄存器,只能通过字节访问,每8个字段控制着一个内核外设的中断优先级的配置。位7:4这高四位有效,所以可编程为0 ~ 15。如果软件优先级配置相同,那就根据他们在中断向量表里面的位置编号来决定优先级大小,编号越小,优先级越高。
对于SysTick的配置,系统默认配置为15,(1UL << __NVIC_PRIO_BITS) - 1UL)
在m3、m4中__NVIC_PRIO_BITS
为4:
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
{
return (1UL);
}
SysTick->LOAD = (uint32_t)(ticks - 1UL);
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL);
SysTick->VAL = 0UL;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
return (0UL);
}
__STATIC_INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
if ((int32_t)(IRQn) < 0)
{
SCB->SHP[(((uint32_t)(int32_t)IRQn) & 0xFUL)-4UL] = (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);
}
else
{
NVIC->IP[((uint32_t)(int32_t)IRQn)] = (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);
}
}
2、NVIC的中断优先级分组不仅对片上外设有效,同样对内核的外设也有效。
systick的优先级15转换成二进制值就是1111,又因为NVIC的优先级分组2,那么前两位的11就是3,3抢占,后两位的11也是3,3子优先级。这样就可以和外设的优先级进行对比。
如果外设中断的优先级也分成了15,无论怎么分组,SYSTICK优先级高于同优先级的外设(毕竟内核异常优先级高于外设中断,因为中断向量表里面的位置编号内核的靠前更小)。
3、设置systick优先级的方法NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 15);
即SCB->SHP[11] = 0x00;
设置最高的话可以得到精准延时,但是会频繁打断用户使用的中断程序,不建议。
内核中断管理
中断是微处理器外部发送的,通过中断通道送入处理器内部,一般是硬件引起的;
而异常通常是微处理器内部发生的,大多是软件引起的,比如除法出错异常,特权调用异常。
Cortex-M的异常类型
如下图:
Cortex-M的寄存器
如下图:
这个图主要记住 R13 寄存器,有两个指针:MSP: 主栈指针 和 PSP: 进程栈指针,相关说明如下:
Cortex-M的特殊寄存器
如下图:
xPSR
组合程序状态寄存器,该寄存器由三个程序状态寄存器组成
应用PSR(APSR) : 包含前一条指令执行后的条件标志
中断PSR(IPSR) : 包含当前ISR的异常编号
执行PSR(EPSR) : 包含Thumb状态位
PRIMSK
PRIMSK:中断屏蔽特殊寄存器。
利用PRIMSK,可以禁止除HardFault 和 NMI外的所有异常。
BASEPRI
利用BASEPRI寄存器来选择屏蔽低于特定优先级的异常或中断。(在上一篇博文中的进入临界区所使用的寄存器就是这个寄存器)
CONTROL
CONTROL:控制寄存器,部分介绍如下:
Cortex-M的工作模式
Cortex-M有两种工作模式和两种工作状态:
线程模式(Thread Mode):芯片复位后,进入线程模式,执行用户程序;
处理模式(Handler Mode):当处理器发生了异常或者中断,则进入处理模式,处理完后返回线程模式。
Thumb状态: 正常运行时处理器的状态
调试状态:调试程序时处理器的状态
进入Systick后,发生异常,则进入处理模式进行处理:
如果是裸机编程,从哪里进去就返回哪里
但是用了操作系统,该返回哪里呢?
所以这里就有必要单独讲解下MSP和PSP
影子栈指针
在上面的Cortex-M的寄存器图中我们标注过R13寄存器:
堆栈指针SP。
在处理模式下,只能使用主堆栈(MSP)。
在线程模式下,可以使用主堆栈也可以使用进程栈。
由 CONTROL 寄存器控制,如下:
PendSV和SVC异常
PendSV异常用于任务切换。
为了保证操作系统的实时性,除了使用Systick的时间片调度,还得加入pendSV异常加入抢占式调度。
PendSV(可挂起的系统调用),异常编号为14,可编程。可以写入中断控制和状态寄存器(ICSR)设置挂起位以触发 PendSV异常。它是不精确的。因此,它的挂起状态可以在更高优先级异常处理内设置,且会在高优先级处理完成后执行。
为什么需要 PendSV异常?
如下图所示,如果中断请求在Systick异常前产生,则Systick可能会抢占IRQ处理(图中的IRQ优先级小于Systick)。这样执行上下文切换会导致IRQ延时处理,这种行为在任何一种实时操作系统中都是不能容忍的,在CortexM3中如果OS在活跃时尝试切入线程模式,将触发Fault异常。
为了解决上面的问题,使用了 PendSV异常。 PendSV异常会自动延迟上下文切换的请求,直到其他的eISR都完成了处理后才放行。为实现这个机制,需要把 PendSV编程为最低优先级的异常。
在FreeRTOS中,每一次进入Systick中断,系统都会检测是否有新的进入就绪态的任务需要运行,如果有,则悬挂PendSV异常,来缓期执行上下文切换。
如下:
在Systick中会挂起一个PendSV异常用于上下文切换,每产生一个Systick,系统检测到任务链表变化都会触发一个PendSV如下图:
PendSV业务流程
中断过程中不但要像一般的C函数调用一样保存(R0-R3,R12,LR,PSR),还要保存中断返回地址(return address)。中断的硬件机制会把EXC_RETURN放进LR,在中断返回时触发中断返回。
如下图:
如何触发PendSV异常
触发PendSV异常,向PendSV中断寄存器写1,触发一次PendSV异常。用户可以主动调用portYIELD
函数进行任务切换,portYIELD
函数如下:
#define portYIELD() \
{ \
\
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
\
\
__asm volatile( "dsb" ::: "memory" ); \
__asm volatile( "isb" ); \
}
PendSV源码简析
PendSV中断服务函数
我这里使用的是M0 内核中 FreeRTOS的源码xPortPendSVHandler
:
void xPortPendSVHandler( void )
{
__asm volatile
(
" .syntax unified \n"
" mrs r0, psp \n"
" \n"
" ldr r3, pxCurrentTCBConst \n"
" ldr r2, [r3] \n"
" \n"
" subs r0, r0, #32 \n"
" str r0, [r2] \n"
" stmia r0!, {r4-r7} \n"
" mov r4, r8 \n"
" mov r5, r9 \n"
" mov r6, r10 \n"
" mov r7, r11 \n"
" stmia r0!, {r4-r7} \n"
" \n"
" push {r3, r14} \n"
" cpsid i \n"
" bl vTaskSwitchContext \n"
" cpsie i \n"
" pop {r2, r3} \n"
" \n"
" ldr r1, [r2] \n"
" ldr r0, [r1] \n"
" adds r0, r0, #16 \n"
" ldmia r0!, {r4-r7} \n"
" mov r8, r4 \n"
" mov r9, r5 \n"
" mov r10, r6 \n"
" mov r11, r7 \n"
" \n"
" msr psp, r0 \n"
" \n"
" subs r0, r0, #32 \n"
" ldmia r0!, {r4-r7} \n"
" \n"
" bx r3 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst: .word pxCurrentTCB "
);
}
PendSV上下文切换函数
xPortPendSVHandler
中调用的上下文切换vTaskSwitchContext
,其核心任务就是找到当前处于就绪态的最高优先级的任务:
void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
{
xYieldPending = pdTRUE;
}
else
{
xYieldPending = pdFALSE;
traceTASK_SWITCHED_OUT();
#if ( configGENERATE_RUN_TIME_STATS == 1 )
{
}
#endif
taskCHECK_FOR_STACK_OVERFLOW();
taskSELECT_HIGHEST_PRIORITY_TASK();
traceTASK_SWITCHED_IN();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
}
#endif
}
}
寻找最高优先级函数
上下文切换vTaskSwitchContext
中调用了taskSELECT_HIGHEST_PRIORITY_TASK()
寻找最高优先级的任务:
taskSELECT_HIGHEST_PRIORITY_TASK()
的硬件方式:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{ \
UBaseType_t uxTopPriority; \
\
\
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \
configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 ); \
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
}
taskSELECT_HIGHEST_PRIORITY_TASK()
的通用方式:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \
{
\
UBaseType_t uxTopPriority = uxTopReadyPriority; \
\
\
while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \
{ \
configASSERT( uxTopPriority ); \
\
--uxTopPriority; \
} \
\
\
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \
uxTopReadyPriority = uxTopPriority; \
}
SVC异常
SVC(请求管理调用),异常编号为11,可编程。SVC产生的中断必须立即得到相应,否则将触发硬Fault。
系统调用处理异常,用户与内核进行交互,用户想做一些内核相关功能的时候必须通过SVC异常,让内核处于异常模式,才能调用执行内核的源码。触发SVC异常,会立即执行SVC异常代码。
下面的启动源码简析中我们可以知道系统在启动调度器函数vTaskStartSchedulerp
最后运行到 rvPortStartFirstTask
中会调用SVC并启动第一个任务。
为什么要用SVC启动第一个任务?
因为使用了OS,任务都交给内核。总不能像裸机调用普通函数一样启动一个任务。
M4只在上电的触发SVC异常,在SVC异常中启动第一个任务,只上电运行一次,M0上没有。
SVC源码简析
M0上面没用,特意生成了一个M4的来看看源码vPortSVCHandler
:
void vPortSVCHandler( void )
{
__asm volatile (
" ldr r3, pxCurrentTCBConst2 \n"
" ldr r1, [r3] \n"
" ldr r0, [r1] \n"
" ldmia r0!, {r4-r11, r14} \n"
" msr psp, r0 \n"
" isb \n"
" mov r0, #0 \n"
" msr basepri, r0 \n"
" bx r14 \n"
" \n"
" .align 4 \n"
"pxCurrentTCBConst2: .word pxCurrentTCB \n"
);
}
FreeRTOS多任务启动源码简析
通过main.c中的main函数调用了osKernelStart();
osStatus osKernelStart (void)
{
vTaskStartScheduler();
return osOK;
}
vTaskStartScheduler
创建空闲任务,启动任务调度器vTaskStartScheduler
:
void vTaskStartScheduler( void )
{
BaseType_t xReturn;
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
{
StaticTask_t *pxIdleTaskTCBBuffer = NULL;
StackType_t *pxIdleTaskStackBuffer = NULL;
uint32_t ulIdleTaskStackSize;
vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
( void * ) NULL,
portPRIVILEGE_BIT,
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer );
if( xIdleTaskHandle != NULL )
{
xReturn = pdPASS;
}
else
{
xReturn = pdFAIL;
}
}
#else
{
xReturn = xTaskCreate( prvIdleTask,
configIDLE_TASK_NAME,
configMINIMAL_STACK_SIZE,
( void * ) NULL,
portPRIVILEGE_BIT,
&xIdleTaskHandle );
}
#endif
#if ( configUSE_TIMERS == 1 )
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
if( xReturn == pdPASS )
{
#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
{
freertos_tasks_c_additions_init();
}
#endif
portDISABLE_INTERRUPTS();
#if ( configUSE_NEWLIB_REENTRANT == 1 )
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE;
xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
traceTASK_SWITCHED_IN();
if( xPortStartScheduler() != pdFALSE )
{
}
else
{
}
}
else
{
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
( void ) xIdleTaskHandle;
}
xPortStartScheduler
启动调度器xPortStartScheduler
:
BaseType_t xPortStartScheduler( void )
{
configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );
configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );
#if( configASSERT_DEFINED == 1 )
{
volatile uint32_t ulOriginalPriority;
volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * const ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
volatile uint8_t ucMaxPriorityValue;
ulOriginalPriority = *pucFirstUserPriorityRegister;
*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;
ucMaxPriorityValue = *pucFirstUserPriorityRegister;
ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
{
ulMaxPRIGROUPValue--;
ucMaxPriorityValue <<= ( uint8_t ) 0x01;
}
#ifdef __NVIC_PRIO_BITS
{
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
}
#endif
#ifdef configPRIO_BITS
{
configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
}
#endif
ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;
*pucFirstUserPriorityRegister = ulOriginalPriority;
}
#endif
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
vPortSetupTimerInterrupt();
uxCriticalNesting = 0;
vPortEnableVFP();
*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;
prvPortStartFirstTask();
vTaskSwitchContext();
prvTaskExitError();
return 0;
}
prvPortStartFirstTask
启动第一个任务prvPortStartFirstTask
:
static void prvPortStartFirstTask( void )
{
__asm volatile(
" ldr r0, =0xE000ED08 \n"
" ldr r0, [r0] \n"
" ldr r0, [r0] \n"
" msr msp, r0 \n"
" mov r0, #0 \n"
" msr control, r0 \n"
" cpsie i \n"
" cpsie f \n"
" dsb \n"
" isb \n"
" svc 0 \n"
" nop \n"
);
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)