NuttxOS上下文切換匯編源碼分析--Apple的學習筆記

一,前言

復習完FreeRTOS的任務切換匯編,來分析下NuttxOS的任務切換匯編設計思路。這里我重點分析的不是任務調度算法哦。今天分析的是第一次任務切換,先走一個溫故而知新的路線。

二,回顧

我先簡單回顧下FreeRTOS中基于cortexM3/M4上下文切換的原理。

  1. 進入中斷:
    上一個任務中xPSR, PC, R14, R12, R3-R0這些寄存器的值會自動存儲到任務的棧中,同時PSP 會自動更新(在更新之前 PSP 指向任務棧的棧頂)如下圖
    這個入棧指將內核寄存器的值進行保存,保存到PSP或MSP地址(向低地址方向push入)中的內容更新。


    image.png

    然后程序員要在中斷函數(shù)中手工添加R4-R11的入棧,PSP棧地址變的更小。根據(jù)對棧的posh和pop動作,棧頂?shù)刂窌?jīng)常變動,但是棧底地址是不變。

  2. 退出中斷:
    在退出中斷前從PSP或MSP【通過lr寄存器的最后2個bit來選擇】的棧頂?shù)刂烽_始自動pop到實際的寄存器??梢岳斫鉃橥ㄟ^pop來恢復現(xiàn)場。就是把棧地址中的地址恢復到當前內核寄存器中。
    除了control特殊寄存器可以控制模式切換。中斷中R14(lr)return value也可以。需要使用命令bx lr,不是bl reg。


    image.png
  3. 總結:
    關于任務上下文切換的設計思想,就是選用中斷的方式進行棧替換,并且設置出棧內容和入棧內容不同,來進行任務切換。
    A . 正常的外設中斷,不進行上下文任務切換,先保護現(xiàn)場到棧,進入中斷執(zhí)行,然后通過?;謴同F(xiàn)場。
    B. 那么中斷要用于切換任務,就是要切換棧。而切換棧的方式就是找到棧地址,然后操作棧地址中的內容。可以理解為入棧保護是有的,但是出棧的時候棧頂?shù)刂诽鎿Q了,所以切換到另外一個函數(shù)了,這就是實現(xiàn)上下文切換的原理。

三,nuttx 10.01的任務上下文切換源碼分析

在我的stm32F407開發(fā)板上,編譯通過后燒寫代碼,用arm官網(wǎng)的ozone進行調試并截圖的,因為之前搭建的codeblocks交叉編譯調試環(huán)境感覺速度慢,而且寄存器無法查看。代碼從__start開始,一路進入up_unblock_task函數(shù)。

image.png

1)此函數(shù)中最后調用arm_switchcontext(rtcb->xcp.regs, nexttcb->xcp.regs);進入?yún)R編的上下文切換代碼。當調用函數(shù)后,發(fā)現(xiàn)MSP棧地址已經(jīng)發(fā)生了變化。此時MSP=0x20000EA8。
image.png

rtcb->xcp.regs的地址傳入r0寄存器exttcb->xcp.regs傳入r1寄存器。其實rtcb->xcp.regs的地址是用來保存的,nexttcb->xcp.regs地址是用來切換的。
#define SYS_switch_context (2)這個將來switch case中要用到的。

arm_switchcontext:
/* Perform the System call with R0=1, R1=saveregs, R2=restoreregs */
1.  mov     r2, r1                  /* R2: restoreregs */
2.  mov     r1, r0                  /* R1: saveregs */
3.  mov     r0, #SYS_switch_context /* R0: context switch */
4.  svc     0                       /* Force synchronous SVCall (or Hard Fault) */
/* We will get here only after the rerturn from the context switch */
5.  bx      lr
6.  .size   arm_switchcontext, .-arm_switchcontext
7.  .end

這段切換代碼是將傳入?yún)?shù)r0移動到r1,r1移動到r2,然后r0賦值為2,調用svc中斷,這次并不會返回哦。
2)走到SVC 0則進入了exception_common,其文件路徑在arch\arm\src\armv7-m\gnu\arm_exception.S
我們先來看看為什么svc 0會進入exception_common。原因就在中斷向量表將中斷號2及之后的都設置了入口函數(shù)為exception_common。在arch\arm\src\armv7-m\arm_vectors.c中

1.  unsigned _vectors[] __attribute__((section(".vectors"))) =
2.  {
3.  /* Initial stack */
4.  IDLE_STACK,
5.  /* Reset exception handler */
6.  (unsigned)&__start,
7.  /* Vectors 2 - n point directly at the generic handler */
8.  [2 ... (15 + ARMV7M_PERIPHERAL_INTERRUPTS)] = (unsigned)&exception_common
9.  };

3)進入exception_common函數(shù)后。此時MSP已經(jīng)自動入棧了8個字節(jié),所以棧頂?shù)刂纷冃???梢钥创撕瘮?shù)的注釋,此時MSP=0x20000E88。此時已經(jīng)將arm_switchcontext中設置的r0,r1,r2的值都自動入棧了,所以地址從0x20000EA8變成了0x20000E88。

/* Common exception handling logic.  On entry here, the return stack is on either
 * the PSP or the MSP and looks like the following:
 *
 *      REG_XPSR
 *      REG_R15
 *      REG_R14
 *      REG_R12
 *      REG_R3
 *      REG_R2
 *      REG_R1
 * MSP->REG_R0
 *
 * And
 *      IPSR contains the IRQ number
 *      R14 Contains the EXC_RETURN value
 *      We are in handler mode and the current SP is the MSP 
*/    

棧中push的內容如下


image.png

4)exception_common函數(shù)中的內容分析
mrs r0, ipsr先保存中斷號到R0中。
5)tst r14, #EXC_RETURN_PROCESS_STACK的目的就是驗證進入中斷前的棧是保存在MSP還是PSP中的,因為進入中斷后的SP值用的是MSP的值。所以若之前是PSP則要將PSP的值保存到SP中。此時r14的是值是0xfffffff9,tst測試bit2是否為0,由于bit2為0,所以跳入1f標識中執(zhí)行。

    tst     r14, #EXC_RETURN_PROCESS_STACK /* nonzero if context on process stack */
    beq     1f              /* Branch if context already on the MSP */
    mrs     r1, psp         /* R1=The process stack pointer (PSP) */
    mov     sp, r1          /* Set the MSP to the PSP *

6)接著1f標識中將sp+32字節(jié)的地址保存到r2,就是這個sp的棧底地址保存到r2。

mov     r2, sp          /* R2=Copy of the main/process stack pointer */
add     r2, #HW_XCPT_SIZE   /* R2=MSP/PSP before the interrupt was taken */

把中斷屏蔽寄存器保存到r3。mrs r3, primask /* R3=Current PRIMASK setting */
7)stmdb sp!, {r2-r11,r14}一開始進入中斷函數(shù)有8個字節(jié)自動入棧,sp地址自動加32,現(xiàn)在繼續(xù)入棧sp繼續(xù)增加,這里和FreeRTOS明顯不同,它由程序員手工入棧的寄存器不僅R4-R11,還多了r2和r3及r14。而r2保存的是進入中斷前的MSP地址,r3保存的是中斷屏蔽狀態(tài)。R14保存的是進入svc中斷前用的是MSP還是PSP。此時MSP=0x20000E88-11*4=0x20000E5C。

image.png

8)把sp保存到r1,此時的sp已經(jīng)完成push動作,所以是棧頂?shù)刂贰?br> mov r1, sp
同時再把sp保存到r4,后3個bit清0,變成8-byte alignment后保存到sp。等于上圖劃線處是push進去的值,然后0x2000E58沒有push值,是一個temp值,把它當做棧頂。等于多一個temp值。

mov     r4, sp
bic     r2, r4, #7
mov     sp, r2

9)調用arm_doirq函數(shù)前截圖,參考r0和r1參數(shù)值,reg參數(shù)是0x20000E5C。

image.png

bl arm_doirq進入c函數(shù)。沒什么特別的,一路進入arm_hardfault。此時傳入的參數(shù)中context就是0x20000E5C。
image.png

image.png

arm_hardfault中從reg參數(shù)0x20000E5C獲取PC的值-1的地址為,為REG_PC為棧頂?shù)刂?(11+6)4地址中的內容再-1(uint16),最后
uint16_t *pc = (uint16_t *)regs[REG_PC] - 1;就是0x08004290-2=0x0800428E。

#define REG_R0              (SW_XCPT_REGS + 0) /* R0 */
#define REG_R1              (SW_XCPT_REGS + 1) /* R1 */
#define REG_R2              (SW_XCPT_REGS + 2) /* R2 */
#define REG_R3              (SW_XCPT_REGS + 3) /* R3 */
#define REG_R12             (SW_XCPT_REGS + 4) /* R12 */
#define REG_R14             (SW_XCPT_REGS + 5) /* R14 = LR */
#define REG_R15             (SW_XCPT_REGS + 6) /* R15 = PC */
#define REG_XPSR            (SW_XCPT_REGS + 7) /* xPSR */
#define HW_INT_REGS         (8)

然后#define INSN_SVC0 0xdf00uint16_t insn = *pc;這塊svc0觸發(fā)時候為什么地址中的內容是0xdf00,不太清楚,反正取這個PC值的目的就是做判斷,是否svc0觸發(fā)的中斷,是的話就調用arm_svcall

if (insn == INSN_SVC0)
{
  return arm_svcall(irq, context, arg);
}

重點來了,還記得一開始調用arm_switchcontext時候修改的r0,r1,r2寄存器么,它是被調用了svc 0后自動入棧的。此時就通過0x20000E5C棧頂?shù)刂穼⑵渫ㄟ^regs[REG_R0]方式把值拿出來用。
下圖的case就是說判斷上下文切換,若r1(rtcb->xcp.regs)和r2(nexttcb->xcp.regs)棧地址不同,則將當前已經(jīng)入棧的數(shù)據(jù),都保存到r1指向的地址中進行入棧。因為(uint32_t *)regs[REG_R1]代表取regs[REG_R1]為地址中的內容。這樣理解的話0x20000E5C就是一個臨時棧。0x20000324(rtcb->xcp.regs)才是正主。

image.png

執(zhí)行memcpy((uint32_t *)regs[REG_R1], regs, XCPTCONTEXT_SIZE);后就是把regs[REG_R1]地址中的內容0x20000324作為dest地址。將regs地址0x20000E5C中的17*4的寄存器內容全部copy到0x20000324地址中,此地址就是rtcb->xcp.regs傳入的值,這樣就模擬了現(xiàn)場保護。
image.png

接著執(zhí)行CURRENT_REGS = (uint32_t *)regs[REG_R2];
就是把regs[REG_R2]中的值作為地址,這個值是0x10000420,其實就是exttcb->xcp.regs傳入的值,此時CURRENT_REGS指向了0x10000420待切換地址。
image.png

然后就是一路返回到arm_doirq函數(shù)中,將0x10000420賦值給reg最后進行返回,同時CURRENT_REGS清0。

  regs = (uint32_t *)CURRENT_REGS;
  CURRENT_REGS = savestate;
#endif
  board_autoled_off(LED_INIRQ);
  return regs;

小插曲,說說r4的值,因為退出arm_doirq函數(shù)后還會用到。
Arm_svcall的switch(cmd)命令轉換為匯編會把R1移動到R4。而R1的地址是0x20000E5C當初進入arm_doirq函數(shù)時候傳遞的參數(shù)。至于為什么移動到r4,我理解有3個參數(shù),已經(jīng)占用了3個寄存器,所以對于函數(shù)內的臨時變量,就放入r4了。這是我猜的,不知道為什么一定是r4。


image.png

退出arm_doirq函數(shù)后,寄存器的狀態(tài)如下。


image.png

10)把r4保存到r1,r1是主棧地址,然后r0是待切換的棧,檢查是否地址相同,相同則不要切換上下文跳入2, 不同則繼續(xù)執(zhí)行匯編,進行上下文切換。此時r0=0x10000420,r1=0x20000E5C,值不同。
mov     r1, r4
cmp     r0, r1                  /* Context switch? */
beq     2f                      /* Branch if no context switch */

11)當前棧r1要切換到棧r0,就是用r0來替換r1進行中斷返回。r0加11*4的地址保存到r1。等于r1變成了要切換的棧的棧頂?shù)刂贰?br> add r1, r0, #SW_XCPT_SIZE至于為什么不是8,而是11,這要看r0棧地址0x10000420內容中的含義了。這個地址中的值中的內容是在創(chuàng)建tcb任務的時候設置的。memset(xcp, 0, sizeof(struct xcptcontext));代表一共17個寄存器。這個結構體中uint32_t regs[XCPTCONTEXT_REGS];XCPTCONTEXT_REGS為17,分別為11一個軟件工程師保存+8個硬件自動保存。#define XCPTCONTEXT_REGS (HW_XCPT_REGS + SW_XCPT_REGS)
來分析下nxthread_setup_scheduler->up_initial_state中就是為其內容賦值。先設置都為0,然后設置4個值sp,pc,xpsr,lr。

void up_initial_state(struct tcb_s *tcb)
{
  struct xcptcontext *xcp = &tcb->xcp;
  /* Initialize the idle thread stack */
  if (tcb->pid == 0)
  {
      up_use_stack(tcb, (void *)(g_idle_topstack -
        CONFIG_IDLETHREAD_STACKSIZE), CONFIG_IDLETHREAD_STACKSIZE);
  }
  /* Initialize the initial exception register context structure */
  memset(xcp, 0, sizeof(struct xcptcontext));
  /* Save the initial stack pointer */
  xcp->regs[REG_SP]      = (uint32_t)tcb->adj_stack_ptr;
  /* Save the task entry point (stripping off the thumb bit) */
  xcp->regs[REG_PC]      = (uint32_t)tcb->start & ~1;
  /* Specify thumb mode */
  xcp->regs[REG_XPSR]    = ARMV7M_XPSR_T;
xcp->regs[REG_EXC_RETURN] = EXC_RETURN_PRIVTHR;

所以這里地址先加11,等于跳過了軟件的11個寄存器內容。

#define REG_R13             (0)  /* R13 = SP at time of interrupt */
#define REG_BASEPRI         (1)  /* BASEPRI */
#define REG_R4              (2)  /* R4 */
#define REG_R5              (3)  /* R5 */
#define REG_R6              (4)  /* R6 */
#define REG_R7              (5)  /* R7 */
#define REG_R8              (6)  /* R8 */
#define REG_R9              (7)  /* R9 */
#define REG_R10             (8)  /* R10 */
#define REG_R11             (9)  /* R11 */
#define REG_EXC_RETURN      (10) /* EXC_RETURN */

地址增加為0x1000044C后截圖如下,剩下的就是硬件的8個。

image.png

ldmia r1!, {r4-r11}從0x1000044C地址開始pop到R4-R11。等于把之前初始化硬件的8個寄存器先pop出到r4-r11。
image.png

12)接著2句就是把r0棧頂?shù)刂分械闹当4娴絩1。就是0x10000420中的值0x10000F78放入R1。REG_SP的值在arm_v7中定義的是0。#define REG_R13 (0)

ldr     r1, [r0, #(4*REG_SP)]   /* R1=Value of SP before interrupt */
stmdb  r1!, {r4-r11} 

理解為之前從0x1000044C地址pop出來的8個寄存器值push入0x10000F78地址。從而棧頂?shù)刂沸薷臑?x10000F58。這樣就構造了一個sp進入中斷后的自動入棧。把自己task的lr,sp,xpsr都保存到自己的棧地址,不就是入棧~

image.png

接著ldmia r0!, {r2-r11,r14} 把r0地址0x10000420中的內容pop出來。這是將當前寄存器也夠構造為第一個task在運行時候并且進行中斷的狀態(tài)吧~
13)然后就是跳入3f標識符,將r1的值0x10000F58放入MSP中。最后通過bx r14自動將其內容pop出來作為中斷現(xiàn)場恢復的內容。由于MSP和剛入中斷時候已經(jīng)出現(xiàn)了變更,說明進行任務切換。

  tst    r14, #EXC_RETURN_PROCESS_STACK /* nonzero if context on process stack */
  ite    eq            /* next two instructions conditional */
  msreq  msp, r1          /* R1=The main stack pointer */
  msrne  psp, r1          /* R1=The process stack pointer */
bx r14

bx r14執(zhí)行前的截圖

image.png

最后跳入了sched\task\task_start.c的nxtask_start函數(shù),并沒有返回一開始arm_switchcontext函數(shù)調用svc 0后,說明進入了第一次任務上下文切換。
Nxtask_start函熟悉。nxtask_init里面有nxtask_setup_scheduler的參數(shù)是nxtask_start,而up_initial_state中是初始化構造棧中REG_PC賦值就是tcb->start就是nxtask_start。來源于xcp->regs[REG_PC] = (uint32_t)tcb->start;所以這一步我理解為初始化的時候狗仔了要第一個切入執(zhí)行的函數(shù)就是Nxtask_start。至此第一個上下文切換的函數(shù)為Nxtask_start分析完畢。

四,總體思路小結

rtcb->xcp.regs是0x20000324 (saveregs)
exttcb->xcp.regs是0x10000420 (restoreregs)
至于0x20000324和0x10000420的來歷是up_unblock_task函數(shù)中,this_task()的返回值返回了rtcb如下。0x20000324就是xcp.regs基于rtcb 0x200002B4的offset地址。


image.png

nxsched_add_readytorun(tcb)又添加一個tcb后,通過調用struct tcb_s *nexttcb = this_task();此時this_task返回的值增加為如下。


image.png

然后通過arm_switchcontext函數(shù)后調用svc 0觸發(fā)硬件異常。
  1. 進入exception_common中斷后是自動入棧了8個寄存器,MSP地址變成0x20000E88。
  2. 單獨設置一個r2,用來保存進入exception_common中斷前的棧底地址(因為有4個寄存器自動入棧,包括了exttcb->xcp.regs和rtcb->xcp.regs的值)。0x20000E88+32=0x20000EA8。
  3. 將r2-r11,r14的值在當前棧頂0x20000E88地址開始繼續(xù)入棧。地址變成0x20000E5C。
  4. 后來又將0x20000E5C進行8字節(jié)對齊,變成0x20000E58。
    此時可以理解為修改了棧中的內容,做了push入棧保護動作。從0x20000EA8地址到0x20000E88自動保存了進入exception_common中斷8個寄存器。從0x20000E88地址到0x20000E58地址是在exception_common中斷中程序員保存了r2-r11,r14及一個temp值。
  5. 然后進入arm_doirq后更新待切換的棧地址為0x10000420,與跳入中斷前執(zhí)行的task棧棧地址對比,不同則模擬新棧地址進行硬件入棧(這個入棧的內容是svc 0之前task棧的內容,目的是假裝沒有執(zhí)行為svc0,是連貫的2個task的切換)。
  6. 最后pop出新棧內容恢復,然后進行BX lr指令還原。

五,總結

對比下Freertos的進入第一個任務是什么流程,F(xiàn)reeRTOS在svc中斷中直接把要切換的棧頂給出,然后pop出r4-r11手工保存的內容,然后就BX lr還原自動保存的內容。Nuttx OS的第一個任務切換就感覺很繞,估計原因是這個svc0中斷和其它中斷混用,所以邏輯上做的比較復雜。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容