ラズピコ ゼロから作るOS アセンブラとスタック・C関数

ラズピコ ゼロから作るOS アセンブラとスタック・C関数

皆さん こんにちは。

今回はラズピコのマイコン RP2040 (cortex-m0+)のアセンブラに関する話題です。

自分のためのメモに近いのですが、アセンブラを触る方はご一読頂くと新しい発見があるかも知れません。

この記事にはインターフェース誌2023年7月号に書かれている内容も含まれています。

RAMとスタックポインタ

RP2040の連続したRAM領域は 0x2000 0000 ~ 0x2004 0000 (256KB)です。

(RAMはトータルで 264KB あるので、他に 8KB あることになります)

プログラムが起動すると スタックポインタ(以降 sp と略す)には 0x2004 0000 が代入されます。

これはtry kernelソースコードの boot\vector_tbl.c の vector_tbl[] で確認できます。

spの初期値は Reset_Handler の上にある INITIAL_SP です。

これをクリックした後、右クリックメニューから Open Declaration (F3キー)すると定義位置にジャンプできるのでご確認ください。

スタックポインタ sp

spの値はRAMの最上位アドレスに初期化されます。

push するとspの値が減り、pop すると増えます。
push / pop は4バイト単位が基本です。

sp はまず push から操作します。
いきなり pop するとRAMが存部しない部分にアクセスすることになるので注意が必要です。

(通常通りにC言語でコードを書く分には全く問題ありません)

通常デバッガを動かすとmain()関数の始まりあたりに停止します。

デバッガで Windows – Show View – Registers から sp の値を見ると、

0x2004 0000 より少し小さな値になっていることが確認できます。

こちらの環境では 0x2003ffe8 でした。

初期値より8バイト分spの値が小さくなっています。

Reset_Handler の先頭あたりにブレークポイントを貼ってInstruction Stepping Modeでアセンブラをステップ実行していくとspの値を確認することができます。

スタックはこのようにspの値が小さくなった領域を一時的に使用するのが一般的です。

push して確保した領域を使い、使い終わったら pop して返却します。

スタックを退避/復帰する命令は push {r0} , pop {r0} のようにレジスタを {} で囲います。

連続して複数のレジスタをpushすることもできます。
push {r0-r3} とすると r0-3 までの4つのレジスタをpushします。
この場合spの値は16バイト減ることになります。

pop {r4, pc} のようにカンマで区切ることもできます。
この場合spの値は8バイト増えます。

cortex-m0+のレジスタ

レジスタは以下の17個で全て32ビット長です。
そして上の8個(r0-3, r12, lr, pc, xpsr)は割り込み処理に入った時に自動的にスタックに退避(push)/復帰(pop)するレジスタになります。
(復帰前に bx lr 命令が必要)
この挙動はOSのディスパッチの部分を理解するのに必要ですから覚えておいてください。

それ以外に割り込み処理内で使うレジスタがあれば、スタックに退避(push)/復帰(pop)するコードを書く必要があります。

r0
r1
r2
r3

r12
lr
pc
xpsr

r4
r5
r6
r7

r8
r9
r10
r11

sp

push/pop可能なレジスタ

push可能なレジスタ

r0-7 , lr

pop可能なレジスタ

r0-7 , pc

例えば r8 を push したい場合、直接pushできないため他のレジスタ経由で行います。

mov r0, r8
push {r0}

それから pop {pc} を実行するとそのまま pc が指し示すアドレスにジャンプします。

Cの関数とレジスタ

Cの関数を呼ぶ際に、アセンブラとCの間である規則があります。

Procedure Call Standard for the ARM® Architecture についてある程度理解しておくと良いと思います。

このドキュメントの 15/34 ページに表があります。

わかりにくいのですが、関数の中でレジスタ r0-3 を使う場合、破壊されても構いません(退避/復帰のコードは不要)

それ以外のレジスタを使う場合には関数の入口で退避(push)し、出口で復帰(pop)する必要があります。

引数は4つまではレジスタとして渡すことができ、それを超えるとスタックに積まれます。

それから戻り値がある場合には r0, r1 が使われます。

例として、dispatch()のコードを見てみることにします。

(dispatchはOSのタスクを切り替える作業です)

関数には引数も戻り値もありません。

static inline void dispatch( void )
{
    out_w(SCB_ICSR, ICSR_PENDSVSET);    // PendSV例外を発生
}

アセンブラのコードを見ると以下のようになっています。
左の数値はアドレスで、2つまたは4つ増加しています。
cortex-m0+のコアはARMのThumb-2命令が採用されていて、その命令長が2 または 4であるためです。


          dispatch:
100008bc:   push    {r7, lr} // (1)
100008be:   add     r7, sp, #0 // (2)
100008c0:   movs    r3, #128        ; 0x80
100008c2:   lsls    r3, r3, #21
100008c4:   ldr     r2, [pc, #12]   ; (0x100008d4 )
100008c6:   movs    r1, r3
100008c8:   movs    r0, r2
100008ca:   bl      0x100008a2 
100008ce:   nop     ; (mov r8, r8)
100008d0:   mov     sp, r7 // (3)
100008d2:   pop     {r7, pc} // (4)

// (1)は r7 と lr をpush(退避)しています。
lrには戻り値(dispatchを呼ぶ次の命令があるアドレス)が入っているので退避する必要があります。
r7は(2)以降で使うので、退避する必要があります。

// (2)はspをr7に代入しています。
// (3)はr7をspに代入しています。
// (4)はr7とpcをpop(復帰)しています。

関数の中でr7を使っているので、退避しておいた値に戻しています。
それから退避していたlr(戻り値)をpcに復帰することで dispatch()を呼んでいるコードの次の命令を実行します。

// (2)と(3)の間でr0-3を使い破壊していますが気にする必要はありません。

いくつかの関数を呼んでいる部分をアセンブラでステップ実行するとある程度パターンが決まっていることに気がつき、理解しやすくなります。

いかかでしたか。
参考になりましたでしょうか。

お疲れさまでした。

この記事のつづきは こちら です。

Assemblerカテゴリの最新記事