データフォルトと格闘した記録その2
前回の続きの解決編。
破壊されたメモリ領域
前回までで0x821_c268番地に不正なデータが書き込まれているせいで、一連のアボートが発生しているところまでを突き止めた。
ディスアセンブリでセクション情報を見ると以下の通りで、やはり該当番地はスタック領域だった。
Sections:
Idx Name Size VMA LMA File off Algn Flags8 .bss 000097cc 0000000008208000 0000000008208000 00252000 2**3 ALLOC
9 .data 00007190 00000000082117e0 00000000082117e0 002617e0 2**3 CONTENTS, ALLOC, LOAD, DATA
10 .sp_area 00000dd0 000000000821c000 000000000821c000 0026c000 2**12 CONTENTS, ALLOC, LOAD, READONLY, DATA
スタック破壊ということで、地道に処理を逆にたどっていくしかないか。。
スタックを破壊する命令をトレース
前回わかったことは、
- 0x2fa8の命令でx27レジスタに0x821_c268番地のデータ(0x0400_0400_0400_0400)がloadされて、
- 0x2facの命令でx1レジスタに0x0400_0400_0400_0400番地のデータをloadしようとアクセスに行って落ちる
であったので、0x821_c268番地に0x0400_0400_0400_0400を書く命令を特定する必要がある。
一般には0x821_c268番地にウォッチポイントを仕込んでwriteアクセスを特定するんだろうが、今回(おそらく)外部割込み等の外乱によって破壊されるメモリ番地が変わってしまうため、データアボートまでプログラムを流し切って、トレースを逆にたどっていく方法をとった。完全にベンダロックインの手法になるため、詳細は記載しないが一般的なツールで同じことができるのか、についてはそのうち調べておきたい。
で、トレースの結果が以下。
- x1レジスタの値(=0x0400_0400_0400_0400)を0x821_c268番地にstoreする命令を特定
- x20→x1へmoveする命令を特定
- x20にx0が指すメモリ番地のデータをloadする命令を特定。ここでx0に格納されている値は0x821_8850
冒頭のセクション情報によれば、0x821_8850番地はもはやスタックではなく、data領域に入っている。大体予想がついてきた感じ。
データ領域を破壊する命令をトレース
スタック領域を破壊している命令を逆方向にトレースすると、そもそもdata領域である0x821_8850番地に0x0400_0400_0400_0400が入っていることが問題だとわかった。
が、この付近を探っても0x821_8850にwriteしている人はいなさそう。考えられることは、
- 0x821_8850番地は本当に使用されていなくて、コンパイラがバグっている
- 時間的にかなり離れたところで0x821_8850番地へのwriteアクセスがある
前者は非常に考えづらい。後者の線で改めてディスアセンブリを見ると、
…
8218848: 152001b0 .word 0x152001b0
821884c: 00000000 .word 0x00000000
8218850: 000abce0 .word 0x000abce0
8218854: 00000000 .word 0x00000000
8218858: 152001b8 .word 0x152001b8…
という感じで、ちゃんとデータが展開されている。
となると、やはり後者で誰かがこの領域にwriteしているとしか考えられない。
もはや静的領域のため、単純にこのメモリ番地にブレークポイントを仕掛けて流せば、犯人の特定は容易。解決は近い。
結論
ブレークしたのは想像以上に序盤。各回路に初期設定をしている部分だった。破壊される領域も0x821_8850番地だけでなく、かなりの領域がこの値で塗りつぶされている。該当する設定関数を見ると、まあ大量の変数を宣言している。ざっと0x4000byte。そしてこの変数に0x400を格納していた。
というわけで結論は非常につまらなくて、単純なスタックオーバーフロー。下降スタックのため、0x821_cdd0番地からアドレスが減る方向にスタックをどんどん積んでいき、この犯人の関数が0x4000byteくらいの変数を確保した瞬間に、data領域が一気に破壊される。これがずっと後になって、不正なアドレスへのアクセスという形でCPU例外を引き起こしていた。
プログラム修正
解決方法としては、
- スタック領域の拡大
- 変数をグローバル変数として宣言
- 変数を動的に確保
スタック領域の拡大
スタック領域を大きくするのは非現実的だが、どうなるか試してみた。スタックのサイズはStartup.Sに記載があって、以下のようにとりあえず0x4000に変更してみた。
.section .sp_area,"a"
.align 12
.global _sp_area
// .equ CPU_STACK_SIZE, 0x200-0x10
.equ CPU_STACK_SIZE, 0x4000
これでコンパイルすると、
gcc: section .rom_system LMA [000000000821d000,000000000821d703] overlaps section .sp_area LMA [000000000821c000,000000000823803f]
のように、リンカスクリプトで定義した領域をはみ出していると怒られる。リンカスクリプトを修正すれば行けるのかもしれないが、あまりにメモリの無駄遣いなのでここまでにしておく。
変数の動的確保
グローバル変数にするのが楽だが、結局この関数でしか使わないのでずっとメモリを取っておくのももったいない。mallocでヒープ領域に一時変数を格納することにする。ちなみにヒープ領域もセクション的には.dataになるようだ。
uint32_t array0[100];
uint32_t array1[200];
// array0を使った処理
// array1を使った処理
を
uint32_t *array0 = (uint32_t *)malloc(sizeof(uint32_t)*100);
// array0を使った処理
free(array0);
uint32_t *array1 = (uint32_t*)malloc(sizeof(uint32_t)*200);
// array1を使った処理
free(array1);
のように、各配列の処理直前でmallocして、使い終わったらfreeで即メモリ解放にすることでdata領域をあまり増やさないようにした。
変更後のプログラムで、無事流れ切ることを確認できた。めでたし。
一応まとめ
特にまとめる意味はないけど、一連のデータ破壊の経緯を簡単に記録しておく。
- sim開始時にimageファイルを展開。0x821_8850番地はdata領域に含まれ、data領域にはグローバル変数等が格納されている
- ある関数が自動変数で宣言した巨大な配列を順次確保していって、スタック領域を突き抜けてdata領域に到達。この時点で0x821_8850番地も破壊される
- data領域のデータを参照する自動変数が0x821_8850番地のすでに破壊されたデータをスタック領域に持ってくる
- このスタック領域のデータをポインタと思って使用した関数がデータアボートを引き起こして、プログラムが異常終了
というながれ。2~3の間で時間的に大きな隔たりがあったことと、壊れたデータが何度かスタックを経由してメモリアクセスに使用されていたことから解析が難航した。メモリ破壊系のバグはやはり厳しい。