by shigemk2

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

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

hotasm.connpass.com

おさらい

10.1.4 raレジスタはハードウェア的に特別なものになっている

  • raレジスタの実体

  • プロローグ

  • jal命令
  • エピローグ

  • jal命令はr0を壊すので、別のところへ退避している

  • レジスタへのアクセスはメモリへのアクセスに比べて遅い

  • 読み込みを待たない
  • 読み込みが終わっていない可能性があるのはよくないので、nopで時間稼ぎをしている(ので、nopじゃなくても別の命令で時間稼ぎをすることは可能)
  • lw ra,16(sp)→ addiu v0, v0 1で関係のない命令を並べ替えて時間を稼ぐ。こういうテクニックをコンパイラがやってくれることがある
  • 設計上の都合でこういうことをしてる
  • 古いMIPSだとnopで時間稼ぎをしている。
00fe1598 <call_complex1>:
  fe1598:   27bdffe8    addiu   sp,sp,-24
  fe159c:   afbf0014    sw  ra,20(sp)
  fe15a0:   0c3f8518    jal fe1460 <return_arg1>
  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

10.2 PowerPC

  • ABI的にr3が第一引数であり戻り値でもある
  • stwuはアトミックな命令
mr r0, r1
addi r1,r1,-16
stw r0, (r1)
  • mflrはリンクレジスタの値をコピーする命令(リンクレジスタはMIPSでいうところのreturn address)
  • blで関数を呼び出すけど値が壊されるので退避と復帰が行われている
  • リンクレジスタは特別なので、専用の命令が用意されている
  • MIPSは特殊レジスタと汎用レジスタで特殊な線引きをやっている
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

10.3 ARMの関数呼び出し

  • なんかごちゃごちゃレジスタが登場している
  • スタックは積み木で、下から積み上げていって(push)、上から取りに行く(pop)
  • ビットマップというものが用意されている
00fe1578 <call_complex1>:
  fe1578:   e1a0c00d    mov ip, sp ! ipとspが同じ所を指す
  fe157c:   e92dd800    push    {fp, ip, lr, pc} ! pc lr ip fpの順にスタックに積まれる
  fe1580:   e24cb004    sub fp, ip, #4 ! 減算
  fe1584:   e3a000fe    mov r0, #254    ; 0xfe ! r0に0xfeを入れる
  fe1588:   ebffffb5    bl  fe1464 <return_arg1> ! return_arg1 関数呼び出し
  fe158c:   e2800001    add r0, r0, #1
  fe1590:   e89da800    ldm sp, {fp, sp, pc} ! fp sp pcの値を復帰させている(ipは復帰させていない)
  • ARMはすべて汎用レジスタにする
  • IPはテンポラリレジスタ?のようなもので、レジスタの値の回避にしか使わないのでは
  • fpの扱いはABIで決まっているような感じ?(fpは後述で、スタックフレームの区切りを指している) f:id:shigemk2:20150422203808p:plain

  • ARMはつめこんだような感じになっていて、初見では難しい構造になってしまっている

ポインタ経由での関数呼び出し

  • ポインタで関数を呼び出す
  • 単純にint *fだと戻り値がint型のポインタになってしまう
void call_pointer(int (*f)(void))
{
  f();
}
  • プログラムカウンタを直接書き換えることでjal命令の代わりとしている
  • ARMのプログラムカウンタは常に2つ先の命令を示している
  • このアセンブラは結構トリッキー
  • mov命令でジャンプしているので、もう狂っているとしか…
  • プログラムカウンタはmovで弄るのは気持ち悪い
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} ! エピローグ

ここから

10.3.5 ARM の命令セットは、あまりRISCっぽくないように思える

P274

  • ARMの即値はあまりビット数を取れないのでPC相対を利用している
  • RISCではメモリアクセスを極力しないという思想
  • メモリへのアクセスはキャッシュミスによりパイプラインの動作を止めてしまう可能性
  • 定数値を扱うためにメモリアクセスが発生→ARMは他のRISC系と比べてあまりRISCぽくない
  • RISC系は命令が固定長なので、命令セットのビット数の割り振りが重要になる
  • ARMは条件コードで4ビット使うので、自由に使えるビット数は32ビット
  • ポインタ経由での関数呼び出しリターン命令を mov命令で実現することで、命令数を節約

10.3.6 フレームポインタ(fp)とはなにか

  • フレームポインタはデバッガがスタックの「バックトレース」を追う場合に利用される。「バックトレース」というのはスタックフレームの構造を遡って追うことで、関数呼び出しがどのような順序で行われているのかを解析することになる。

  • A→B→Cの順で関数用にスタックが積まれていたとして、デバッガがCで止めた場合、BやAのスタックフレームの位置をどこで特定するのだろう

  • それを特定するためのレジスタがフレームポインタ
  • CPUによっては必ず先頭を指してはいない
  • 専門家向けではない本
  • フレームポインタは基本としてデバッガがパックトレースを追うためにあるもの
  • PowerPCではフレームポインタが要らない構造になっているので、特段必要ではないけど、フレームポインタを使わないオプションをつけることが出来る(man gccも参照のこと)

framepointerつきでコンパイルしたやつ

int return_zero()
{
    return 0;
}
$ mips-elf-gcc -nostdlib -g -O -fno-omit-frame-pointer return_zero_mips.c -o test.out
00400018 <return_zero>:
  400018:       27bdfff8        addiu   sp,sp,-8
  40001c:       afbe0004        sw      s8,4(sp)
  400020:       03a0f021        move    s8,sp
  400024:       00001021        move    v0,zero
  400028:       03c0e821        move    sp,s8
  40002c:       8fbe0004        lw      s8,4(sp)
  400030:       03e00008        jr      ra
  400034:       27bd0008        addiu   sp,sp,8
  • もっとも return_zero()は他の関数を呼び出さない、いわゆる「リーフ関数」です。このため関数呼び出しによってフレームポインタの値が上書きされることは無く、実はフレームポインタの退避・復旧処理を行なわなくてもデバッガで追跡可能だったりします

10.4 SHの関数呼び出し

10.4.1 SHは「プリデクリメントレジスタ間接」というアドレッシングモードをもつ

  • あぁ^〜 難しい。sts.lでスタックの獲得と値の保存をまとめて行っているイメージ。
  • 「@-r15」と書くとr15レジスタをデクリメントしてからという意味が追加される

  • --i プリデクリメント → SH的にはフル下降

  • i-- ポストデクリメント
00fe1508 <_call_complex1>:
  fe1508:   4f 22           sts.l   pr,@-r15
  fe150a:   94 05           mov.w   fe1518 <_call_complex1+0x10>,r4   ! fe
  fe150c:   d0 03           mov.l   fe151c <_call_complex1+0x14>,r0   ! fe1440 <_return_arg1>
  fe150e:   40 0b           jsr @r0
  fe1510:   00 09           nop 
  fe1512:   4f 26           lds.l   @r15+,pr
  fe1514:   00 0b           rts 
  fe1516:   70 01           add #1,r0
  fe1518:   00 fe           mov.l   @(r0,r15),r0
  fe151a:   00 09           nop 
  fe151c:   00 fe           mov.l   @(r0,r15),r0
  fe151e:   14 40           mov.l   r4,@(0,r4)

gist.github.com

10.4.2 SH ではポインタ経由で関数呼び出しが行われる

  • SHは即値を入れるところがものすごく少ない
  • fe150aで return_arg1()のアドレスを rOレジスタに代入する
  • fe150cで rOレジスタの指す先を関数として呼び出す
  • jsrはちょっと謎
  • movで別のアドレスの値(即値)を読み込んで、そこにジャンプする

10.4.3 関数呼び出しのアドレス指定方法

ここで突然のPowerPC

  • なんらかのフラグ領域(つまりオペコードの一部)にして,実際のアドレス計算の際にはフラグ領域は無視してゼロとして計算するという思想、というか設計(作った人のこだわり)
  • この本は機械語にこだわっていて、バイナリベースの本が多かったりする。
  • PowerPCではジャンプ先のアドレスはその命令のアドレスを基点に計算する、という設計
  • 差分計算

10.4.5 SHは多ビットのオペランドを取れない

  • PowerPCや ARMでは相対アドレス指定でジャンプ先を指定
  • SHは2バイト命令長のためオペランドとして指定できる即値のビット幅をそれほど大きくはとれませんから,相対アドレスで指定できる範囲が非常に狭くなってしまいます
  • レジスタ経由で関数を呼び出す

P284

熱血!アセンブラ入門

熱血!アセンブラ入門