今回はP261から
おさらい
スタック
- メモリ領域のこと
- 関数を呼び出すたびにメモリ領域が確保される。また関数を呼ぶと、メモリ領域が積み上がっていく。処理が終わったら積み上がったものから終了していく(=メモリが解放される)
- 最後に積み上がったものから終了していく(マルチスレッドは例外だが、あんまりそれは考えなくていいと思う)
- マルチスレッドはスレッドごとにスタックがある、という例外がある
- ある関数のメモリ領域を積み重ねていく
- 1つ1つのスタックの領域のことをスタックフレームという
- スタックフレームの中にローカル変数が入っている
- f1 f2 f1 f2...みたいな相互再帰をやったときは、f1 f2 f1 f2みたいに積み上がっていく
- 最初のf1でint a = 5みたいなのを定義すると、2度め以降で呼ばれたint aは別のメモリ領域に確保される
- こういうことをしないとグローバル変数みたいになる
JSだと、varをつけわすれるとグローバル変数になってわけがわからなくなる
function hoge() { a = 1; hoge(); }
どうでもいいけど、これやるとエラー
- たいていのアセンブラにはSP(スタックポインタ)がある
- sw はレジスタの内容をメモリに書き込む
MIPSでは
- jal命令で暗黙的にraを書き換えているので、swでraの中身を一時的にメモリに退避し(っていうことが前回書いてあった)、lwでメモリに退避したものを復帰している
- MIPSはレ点の遅延スロット
00fe1598 <call_complex1>: fe1598: 27bdffe8 addiu sp,sp,-24 ! プロローグ fe159c: afbf0014 sw ra,20(sp) ! プロローグ fe15a0: 0c3f8518 jal fe1460 <return_arg1> ! 次の命令でa0に254を入れて、return_arg1を呼び出している。jal命令はra命令を(暗黙的に)いじっている fe15a4: 240400fe li a0,254 fe15a8: 24420001 addiu v0,v0,1 fe15ac: 8fbf0014 lw ra,20(sp) ! エピローグ fe15b0: 00000000 nop fe15b4: 03e00008 jr ra fe15b8: 27bd0018 addiu sp,sp,24 ! エピローグ
- 都度呼び出すのではなく、コンパイラがプロローグとエピローグを用意している。leaf関数とかやるとプロローグやエピローグが用意されない。
- 最適化しないとすごく長いコードが生まれる(なまら長いので時間があったら読めばいいけど徒に時間がすぎるだけだと思われる)
- スタックは使い終わったら消すみたいなことはせずに、spを上を下へのみたいな感じでずらすので、spの中身が残っていることがある
- 情報が残っているものがまずいものは暗号化するし、ゼロ埋めすることもある。そうしないと解読されてクラッキングされる可能性があるから。
- spを上を下へずらしていることでスタックを解放したことにしている
raの実体
- raレジスタはjal命令によって勝手に書き換えられているので、raレジスタは特別で、その実体は$31、特別なレジスタとなっている
今日のところ
P261
PowerPCの関数呼び出し
- MIPSとほぼ同じだけど、PowerPCはMIPSみたいなレ点がないのが少し楽
- PowerPCのspはr1で、ABIでそういうふうに決まっている
- 一連の動作が1命令で行われることをアトミックといい、stwuがアトミック(2命令でも出来る)
- スタックフレームの切れ目を見るとスタック領域が分かる(MIPSにはない機能で、ABIで決められている)
00fe1580 <call_complex1>: fe1580: 94 21 ff f0 stwu r1,-16(r1) ! プロローグ fe1584: 7c 08 02 a6 mflr r0 fe1588: 90 01 00 14 stw r0,20(r1) ! プロローグ(退避) fe158c: 38 60 00 fe li r3,254 fe1590: 4b ff fe cd bl fe145c <return_arg1> fe1594: 38 63 00 01 addi r3,r3,1 fe1598: 80 01 00 14 lwz r0,20(r1) ! エピローグ(復帰) fe159c: 7c 08 03 a6 mtlr r0 fe15a0: 38 21 00 10 addi r1,r1,16 fe15a4: 4e 80 00 20 blr ! エピローグ
stwuを複数命令でやると
命令と命令の間に中途半端な状態が生まれる。こういうのは分割したくない。ので、専用の命令が存在するし、こういう命令をアトミックという(原子は分裂するっていうマサカリはやめて)
(一瞬でも空白を作りたくない、という思想)
mr r0, r1 addi r1,r1,-16 stw r0, (r1)
stwuだけがMIPSと違う思想になっている
PowerPCはリンクレジスタを持っている
- mflrってなんですかってなるけど、swの前のおまじないじゃないのって言うアタリ
- lrはリンクレジスタ
- MIPSだとra(return address)
- 同じ役割でも別の名前で呼ばれるものもあるけど、リンクレジスタが一般的
- blはリンクレジスタの値を勝手に書き換えている
- mflrはリンクレジスタの値をコピーする命令 (from)
mtlrはリンクレジスタに値をコピーする命令 (to)
リンクレジスタをコピー、リンクレジスタにコピー
- PowerPCは独特でスタック領域の外のメモリを読み書きしている
- リンクレジスタの中身をr0に突っ込み、それをr1に退避している。lrは普通の命令ではないので、lrから直接メモリに書き込むことは出来ない
- ので、MIPSと比べて命令が1つ増えている
特殊レジスタに対するMIPSとPowerPCの考えかたの違い
- MIPSは汎用レジスタと特殊レジスタがある(でもリンクレジスタを他のレジスタと同じような感じでmovすることも出来る)
- PowerPCは全部が汎用レジスタだけど、ゼロ番目のレジスタだけはゼロレジスタっぽい動きをする
- PowerPCのリンクレジスタは32本のレジスタとは別の個別のレジスタ
- 値を操作したい場合は専用命令mflr mtlrを使って操作する
- MIPSはそういう感じの特殊命令がなく、特殊なレジスタがある。あるから、レジスタを自由に使うことはできない
- PowerPCはすべてのレジスタが自由に使えるけど、特殊命令があるし命令セットの個数にも限りがあるので命令セットを余計に消費する
PowerPCのリンクレジスタの保存先は、一つ前のスタックフレームになっている
- リンクレジスタの値の保存はスタックフレームを超えた場所に書き込まれている
- 前の関数のスタックフレーム
- スタックのダンプを見たときにどの関数のスタックフレームなのかがわかりやすくなるためでは
- スタックフレームは16の倍数になるように調整されている
RISC系 CPUはレジスタベースで動作している
- RISC系の CPUではこのように、関数からの戻り先をレジスタに保存するという設計になっているものが多くあります
- ジャンプ命令の実体は、プログラムカウンタへの代入命令
- 関数呼び出しが行なわれると、関数からの戻り先アドレスが特定のレジスタに保存
関数呼び出しの動作 * プログラムカウンタの値を(戻り先となるアドレスを指すように命令サイズを加算して)リンクレジスタに代入する * プログラムカウンタに関数のアドレスを代入する
PowerPCはレジスタベースで動いているという話。
ARMの関数呼び出し
00fe1578 <call_complex1>: fe1578: e1a0c00d mov ip, sp fe157c: e92dd800 push {fp, ip, lr, pc} fe1580: e24cb004 sub fp, ip, #4 fe1584: e3a000fe mov r0, #254 ; 0xfe fe1588: ebffffb5 bl fe1464 <return_arg1> fe158c: e2800001 add r0, r0, #1 fe1590: e89da800 ldm sp, {fp, sp, pc}
blはbranch with linkで、メイン処理 addでreturnしていると思われる
スタックにのせるのがpushで、取り除くのがpop
ipとかfpって何?
ARM は複数レジスタをスタックに保存する命令がある
ビットをレジスタに割付しているのがビットマップ
- 先頭のレジスタと終端のレジスタを指定するとその聞のレジスタがすべて操作対象(つまり範囲指定する)
- レジスタ数に応じたビ、ツトマップでレジスタを任意に指定する
- レジスタがぐちゃって指定できる
- 配列っぽい
- 16個一気にpushすることもできる
push {rO, r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip, sp, lr, pc}
- ARMはリトルエンディアン
- ARMの push命令では後ろ 16ビットでスタックに格納するレジスタがビットマップ指定されている
ARMは特殊レジスタを汎用レジスタとして持っている
- spやlrは特殊レジスタっぽい(スタックポインタとかリンクレジスタとか)
実体はこんな感じ。-M reg-names-rawとかオプションつけると実体が見える
- lrレジスタの値をpcレジスタに代入することが、関数からのリターンになる
ARMのスタック構造を考える
- レジスタ番号の小さなものが、スタック上のアドレスの小さな位置に格納される
- ARMのpushはややこしい
- レジスタをスタック上に退避
そもそも図と文章があんまり一致していない
spをipレジスタを代入
- レジスタをスタックに積む+spの値を変える(ARMのpushはアトミック)
- fp=ip-4
- ldmでレジスタ値を復旧
- で、mov pc, lrでリターンしていない…
- ldmはメモリの複数の値を複数のレジスタにロードする(書き込む)
- で、ldm sp, {fp, sp, pc}で、spがアドレスを指している=復旧
- 矢印の先が読み込み先
- もともとfpだった値がfpに入っているっていう復旧のからくり
- スタックポインタはこれで関数呼び出し前の状態に戻る
- 単純に、mov pc, lrしているので、復旧と同時にreturnもやるアトミック命令である
- ipはもともとのスタックポインタのバックアップ
- レジスタ群の値を復旧し
- スタックを解放し
- さらに戻り先に戻る
っていう割と強引なアレ
ARMは純粋な RISCプロセッサとしてのアーキテクチャよりも実用性を意識した思想
ポインタ経由での関数呼び出し
いわゆる高階関数の例で見てみよう(Cで高階関数を書くとわけがわからん)
void call_pointer(int (*f)(void)) { f(); }
00fe15c0 <call_pointer>: fe15c0: e1a0c00d mov ip, sp fe15c4: e92dd800 push {fp, ip, lr, pc} fe15c8: e24cb004 sub fp, ip, #4 fe15cc: e1a0e00f mov lr, pc fe15d0: e1a0f000 mov pc, r0 fe15d4: e89da800 ldm sp, {fp, sp, pc}
- mov pc, r0なので、引数の値をプログラム・カウンタに代入している(ただのmov命令なのに関数呼び出し…)
- ARMのプログラム・カウンタは常に2命令先をさしているので、変則的なことができている
P274
- 作者: 坂井弘亮
- 出版社/メーカー: 秀和システム
- 発売日: 2014/09/30
- メディア: 単行本
- この商品を含むブログを見る
- 作者: 手塚治虫
- 出版社/メーカー: 実業之日本社
- 発売日: 2008/10/17
- メディア: 単行本
- クリック: 2回
- この商品を含むブログ (12件) を見る