スタック走査して GC
GCのテスト実装にとりかかる。
とりあえず Mark & Sweep でいくことに。
レジスタ/スタック/ヒープ/グローバル変数 など走査すべき場所や考慮すべき事象はたくさんあるので、段階的にまずは単純にスタックから。
流れとしては
- operator new を定義し、割り当てた領域の全アドレスをリストに保持
- main のスタックボトムを保持しておく (これを bottom と呼ぶ)
- gc_mark() 呼出し時のスタックポインタを取得する(これを top と呼ぶ)
- 全アドレスリストのそれぞれのアドレスについて、top と bottom の間に保持されている auto変数から参照されていれば mark する
- mark されていないものを sweep する
理論上はこれでうまくいくのだけど、いろいろ罠があって期待どおりに動かなかったりする。
g++ -masm=intel -S gc.cpp
なので、アセンブリのコードを読みまくって自前でスタックの推移を OpenOfficeの表計算で書いてみた。
さすがにここまでやればうまくいくものなのだな。
ただ g++ の吐くコードはときどき不可解で必要のなさそうに見えるところで無駄に多めにスタックを確保していたりする。
謎だなぁ。最適化のためだとは思うのだけども。
以下スタック表
| (esp) | |||||||||||||||||
| -76 | |||||||||||||||||
| -72 | (esp) | mallocの結果 | |||||||||||||||
| -68 | |||||||||||||||||
| -64 | testのebp(ebp) | ||||||||||||||||
| -60 | 戻り(esp) | ||||||||||||||||
| -56 | (esp) | (esp) | (esp) | 32 | (esp) | ||||||||||||
| -52 | |||||||||||||||||
| -48 | |||||||||||||||||
| -44 | |||||||||||||||||
| -40 | mallocの結果 | gcのebp(ebp) | |||||||||||||||
| -36 | newの結果 | (esp) | |||||||||||||||
| -32 | mainのebp | mainのebp(ebp) | mainのebp(ebp) | mainのebp(ebp) | mainのebp(ebp) | mainのebp(ebp) | (ebp) | ||||||||||
| -28 | gc_init後の戻り場所(esp) | 戻り場所 | 戻り場所(esp) | 戻り場所(esp) | mainのebp(ebp) | ||||||||||||
| -24 | &test_top | &test_top | &top | &freeNodes | 4 | (esp)戻り | |||||||||||
| -20 | |||||||||||||||||
| -16 | |||||||||||||||||
| -12 | int x =0x12345678 | ||||||||||||||||
| -8 | int y = 0x99998888 | ||||||||||||||||
| -4 | ecx | ||||||||||||||||
| 0 | main突入前の ebpかつmainのebp | (ebp) | (ebp) | ||||||||||||||
| main | gc_init | main | gc_node_initialize | gc_node_initialize | gc_node_initialize | main | new | main | test | new | test | main | gc | gc_mark |