by shigemk2

当面は技術的なことしか書かない

熱血!アセンブラ入門 読書会(12) #hotasm

hotasm.connpass.com

今回は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();
}

どうでもいいけど、これやるとエラー

gist.github.com

  • たいていのアセンブラには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していると思われる

infocenter.arm.com

スタックにのせるのが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とかオプションつけると実体が見える f:id:shigemk2:20150408212218p:plain

  • lrレジスタの値をpcレジスタに代入することが、関数からのリターンになる

ARMのスタック構造を考える

  • レジスタ番号の小さなものが、スタック上のアドレスの小さな位置に格納される
  • ARMのpushはややこしい
  • レジスタをスタック上に退避
  • そもそも図と文章があんまり一致していない

  • spをipレジスタを代入

  • レジスタをスタックに積む+spの値を変える(ARMのpushはアトミック)
  • fp=ip-4
  • ldmでレジスタ値を復旧
  • で、mov pc, lrでリターンしていない…
  • ldmはメモリの複数の値を複数のレジスタにロードする(書き込む)
  • で、ldm sp, {fp, sp, pc}で、spがアドレスを指している=復旧

f:id:shigemk2:20150408213208p:plain

  • 矢印の先が読み込み先
  • もともとfpだった値がfpに入っているっていう復旧のからくり
  • スタックポインタはこれで関数呼び出し前の状態に戻る
  • 単純に、mov pc, lrしているので、復旧と同時にreturnもやるアトミック命令である
  • ipはもともとのスタックポインタのバックアップ

f:id:shigemk2:20150408213354p:plain

  • レジスタ群の値を復旧し
  • スタックを解放し
  • さらに戻り先に戻る

っていう割と強引なアレ

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

熱血!アセンブラ入門

熱血!アセンブラ入門

上を下へのジレッタ 完全版

上を下へのジレッタ 完全版