読者です 読者をやめる 読者になる 読者になる

by shigemk2

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

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

勉強会

おさらい

スタック

メモリの中にスタックと呼ばれるものがあって、それをどうやって使いまわすか

f:id:shigemk2:20150325200521p:plain

void foo() {
  int a = 1, b = 2;
}
  • 変数はメモリの中に
  • ローカル変数はスタックと呼ばれるメモリの中に存在している
void foo(int a) {
  if(a > 1) foo(a - 1);
}

関数を呼び出すごとにスタック領域が確保され、呼び出しが終わったら領域が消される。関数の処理が終わると解放される。

右の例でいうと、間を飛ばしてfunc2()が先に終わることはない。メモリの領域は下から順番に積み上がっているから。関数が終わったら上から順番に消されていく。

スタックポインタがメモリ領域の敷居になっている。

コルーチンみたいに呼んだ順番に終わらない場合もあるがそれは割愛する。

#define UNUSED __attribute__((unused)) 

void set_stack()
{
  UNUSED volatile int a = 0xfe;
  UNUSED volatile int b = 0xff;
}

int use_stack()
{
  volatile int a = 0xfe;
  volatile int b = 0xff;
  return a + b;
}

-nostdlib c言語だと当たり前に使える標準ライブラリがあるけど、この環境では特にOS依存ではなくコンパイラだけ使いたいので、標準ライブラリは一切使わない。(OS依存のやつは想定していない)

どこでスタートしたらいいかわからないから適当にアドレスを指定しましたよ警告

➜  ~  mips-elf-gcc -nostdlib v.c
/home/shigemk2/bin/cross/bin/../lib/gcc/mips-elf/4.6.4/../../../../mips-elf/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400018

gist.github.com

$ mips-elf-gcc -nostdlib -O v.c

gist.github.com

スタックの場所が特定されるとクラッキングされやすくなるので、スタックの場所は毎回違っている。

準備のaddiuがプロローグ、片付けのaddiuがエピローグと呼ばれる。

  • -O 最適化
  • -g デバッグ情報

  • -S 情報をくっつける

$ mips-elf-gcc -nostdlib -O -g -Wall v.c
$ all-objdump -S a.out
  • sw=store wordでwordは4バイト
  • メモリの読み書きは専用の命令を使わないといけない
  • メモリの値をレジスタに書き込むか、レジスタの値を読み込むか。いずれにしても、メモリの読み書きはレジスタを介さないといけない。
  • 関数呼び出しのときにスタックの領域をずらす。ただずらすだけなので、メモリに書き込んだ値はまだ残っている。なので、C言語ではローカル変数を初期化しないと中身がぐちゃぐちゃになる。

gist.github.com

➜  ~  mips-elf-gcc -nostdlib -O -g -Wall v.c
v.c: In function ‘set_stack’:
v.c:4:18: warning: unused variable ‘b’ [-Wunused-variable]
v.c:3:18: warning: unused variable ‘a’ [-Wunused-variable]
/home/shigemk2/bin/cross/bin/../lib/gcc/mips-elf/4.6.4/../../../../mips-elf/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400018

なお、マクロで使わない変数を宣言することは出来る。

gist.github.com

(なお、Javaではアノテーションで関数に属性を付与することが出来る)

Javaアノテーション機能 概要 - Java入門

警告を出して(-Wallをつけて)コンパイルしたほうがよい。

アトリビュートでマクロを使いたいために括弧は二重だったりする。アセンブラを勉強したい場合はOSも勉強しないともったいない感じが。

int use_stack()
{
  volatile int a = 0xfe;
  volatile int b = 0xff;
  return a + b;
}

ココのnopは次の処理のための時間稼ぎだけど、最近のmipsは性能がいいので、別にnopがなくてもよかったりする。volataileをつけると読み書きは都度やる。

00fe152c <use_stack>:
  fe152c:   27bdfff8    addiu   sp,sp,-8
  fe1530:   240200fe    li  v0,254
  fe1534:   afa20000    sw  v0,0(sp)
  fe1538:   240200ff    li  v0,255
  fe153c:   afa20004    sw  v0,4(sp)
  fe1540:   8fa30000    lw  v1,0(sp)
  fe1544:   8fa20004    lw  v0,4(sp)
  fe1548:   00000000    nop
  fe154c:   00621021    addu    v0,v1,v0
  fe1550:   03e00008    jr  ra
  fe1554:   27bd0008    addiu   sp,sp,8

なお、volatileを外すとこんな感じになる。

gist.github.com

以上、スタックとローカル変数の関係について。

PowerPC

下方伸張かどうかはあんまり気にしないのがこの本のポリシー。

下がゼロなのが下方伸張。世の中のCPUのだいたいは下方伸張。 スタックポインタを扱っているレジスタはr1で、MIPSはspだった。

00fe14fc <set_stack>:
  fe14fc:   94 21 ff e0     stwu    r1,-32(r1)
  fe1500:   38 00 00 fe     li      r0,254
  fe1504:   90 01 00 08     stw     r0,8(r1)
  fe1508:   38 00 00 ff     li      r0,255
  fe150c:   90 01 00 0c     stw     r0,12(r1)
  fe1510:   38 21 00 20     addi    r1,r1,32
  fe1514:   4e 80 00 20     blr

MIPSはキッチリメモリを確保しているけど、powerpcは適当。 あと、stwu命令を使ってスタック領域を確保している。

スタックフレームの直後に、スタックポインタの値が格納されているので、スタックフレームの切れ端が分かる。

で、PowerPCのスタックフレームは16バイト単位で決まっている(そういうABI) なので処理が追いやすい。メモリの参照先も分かりやすい。キャッシュに乗りやすい。

複雑な命令も使いやすくなったりしている。

SIMD - Wikipedia

スタック操作はアトミックに行う必要がある

  • stwu命令はスタックの減算と値の格納をいっぺんにやっている
  • でも2命令になっちゃうと、命令の割り込みが発生したら中途半端な感じになってしまう
  • このように1命令で行う必要のある処理をアトミックという
  • 命令を分割することは出来ないようにしないといけない

ARM SH H8 i386 のスタック操作もみてみよう

ARM

やっていることはMIPSといっしょっぽい

subとadd

00fe1508 <set_stack>:
  fe1508:   e24dd008    sub sp, sp, #8
  fe150c:   e3a030fe    mov r3, #254    ; 0xfe
  fe1510:   e58d3004    str r3, [sp, #4]
  fe1514:   e3a030ff    mov r3, #255    ; 0xff
  fe1518:   e58d3000    str r3, [sp]
  fe151c:   e28dd008    add sp, sp, #8
  fe1520:   e12fff1e    bx  lr

SH

  • r15のやりとりでスタック領域をごにょごにょしている
  • 遅延スロット
  • 減算なので下方伸張
  • どんなCPUでもプロローグとエピローグは存在している
00fe14bc <_set_stack>:
  fe14bc:   7f f8           add #-8,r15
  fe14be:   91 04           mov.w   fe14ca <_set_stack+0xe>,r1    ! fe
  fe14c0:   1f 11           mov.l   r1,@(4,r15)
  fe14c2:   71 01           add #1,r1                           ! ff
  fe14c4:   2f 12           mov.l   r1,@r15
  fe14c6:   00 0b           rts 
  fe14c8:   7f 08           add #8,r15
  fe14ca:   00 fe           mov.l   @(r0,r15),r0

最後の行は、大きな即値を扱うことが出来ないので、別のアドレスに値を格納している。

このあたりからなんかだんだん本領発揮ってところ。

H8

大きな即値が扱えないためか、2回に分けてスタック領域の演算をやっている。そこらへんのアレがあんまり良く分からなかったりしている。

00fe14aa <_set_stack>:
  fe14aa:   1b 87           subs    #2,r7
  fe14ac:   1b 87           subs    #2,r7
  fe14ae:   79 02 00 fe     mov.w   #0xfe,r2
  fe14b2:   6f f2 00 02     mov.w   r2,@(0x2:16,r7)
  fe14b6:   0b 02           adds    #1,r2
  fe14b8:   69 f2           mov.w   r2,@r7
  fe14ba:   0b 87           adds    #2,r7
  fe14bc:   0b 87           adds    #2,r7
  fe14be:   54 70           rts 

i386

RISCの場合はレジスタを介さないといけないけど、i386はそんなことはない。 subとadd

00fe14c5 <set_stack>:
  fe14c5:   83 ec 08                sub    $0x8,%esp
  fe14c8:   c7 44 24 04 fe 00 00    movl   $0xfe,0x4(%esp)
  fe14cf:   00 
  fe14d0:   c7 04 24 ff 00 00 00    movl   $0xff,(%esp)
  fe14d7:   83 c4 08                add    $0x8,%esp
  fe14da:   c3                      ret    

スタックの一般的な共通事項

  • スタック獲得は、スタックポインタを減算している(メモリ領域をヒープとスタックに効率的に利用できる)
  • スタックポインタの指す先には、値が格納されている

  • プログラム

  • データ
  • ヒープ(mallocするのでどんどん伸びていく)
  • スタック(ヒープとスタックで開いているところを食い合っているので、これがぶつかるとout of memory)

これが上方伸張だとわけがわからなくなる。

  • フルスタック(使用領域の終端)
  • エンプティスタック(空き領域の先端)

  • プリデクリメント(先に減算)

  • ポストインクリメント(あとで減算)

あと完全降順とかあるけどこれはそんなに覚えなくていいと思う。

まとめ

いろいろなアセンブラを読むと手数が増える。引き出しが増える。気楽な感じでアセンブラを見ていくと、いいと思う。

10. 関数呼び出し

こっからが本番

関数を呼び出せないとプログラムとして意味がないので…

関数呼び出し←スタックの理解←メモリの読み書きの理解

なんかMIPSが先になってきているけど、それはたぶん読みやすいからなのでは。

MIPS

このくらい複雑なほうが、アセンブラを見た時にいろいろ見えてくる。

int call_complex1()
{
  return return_arg1(0xfe) + 1;
}
  • 実際見てみると、jal命令でreturn_arg1関数を呼び出していると思われる。
  • jal命令前後になんかやっているんだろうなっていう予想はつく。
  • MIPSは遅延スロットなので、関数を呼ぶ前にli命令でレジスタa0に値を格納している
  • レジスタv0が関数の戻り値
  • 気になるのはswとlwって何?→レジスタraが壊れる前にスタック上に一時的に保存している
  • なお、raは関数からの戻り先のアドレスを保持するレジスタ(return addressの意)
  • 関数を呼ぶたびにraレジスタの中身が破壊されてしまうので、swとlwでraを一時的に退避している
00fe1590 <call_complex1>:
! プロローグ
  fe1590:   27bdffe8    addiu   sp,sp,-24
  fe1594:   afbf0010    sw  ra,16(sp)
! 本文
  fe1598:   0c3f8518    jal fe1460 <return_arg1>
  fe159c:   240400fe    li  a0,254
  fe15a0:   24420001    addiu   v0,v0,1
! エピローグ
  fe15a4:   8fbf0010    lw  ra,16(sp)
  fe15a8:   00000000    nop                ! メモリへのアクセスは遅いのでnopで時間稼ぎをしている
  fe15ac:   03e00008    jr  ra
  fe15b0:   27bd0018    addiu   sp,sp,24

今まではraの保存をするまでもない関数だった(=別の関数を呼ばない)。これを末端の関数もしくはリーフ関数と呼ばれる。

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

ゼロレジスタと同じく、raレジスタも実体は$31で、ABIで決められたもの以上にハードウェアで役割の決まっているレジスタである

P261まで