コンパイラのバージョンを変えたら正しく動かないという話があり、
なんのこっちゃと調べていたらレジスタやスタックの使い方が変わったことで、
既存のバグ(かそれに近いもの)が露呈した形である。
1つはスタックに確保した配列の範囲外を操作したことによるもので、
なぜこれが見逃されてきたかというのは気になるところだが、
配列サイズを適切に設定すれば解消する問題なので、これはこれでよい。
問題は残り2つで、元々スタックの使用方法に依存する処理と、
どうしてもレジスタ上で処理してほしい処理である。
スタックの使用方法に依存する処理はコンパイル結果に依存することは口伝されていたので、
新しいコンパイル結果に見合った調整を行えば回避できる。
が、そもそもスタックの使用方法に依存する処理というのが問題じゃないかという話で、
そこに依存しないように作っておくべきだったのではというのは……
この問題への僕の考える答えは後で書く。
もう1つのどうしてもレジスタ上で処理したい処理の話だが、
端的に言えばコーディングの問題である。
Cのコーディングでこの変数はレジスタ上に配置することを保証することはできない。
変数宣言時にregisterキーワードを付けるとレジスタ上に配置するよう努めるが、
必ずしもレジスタ上に配置しなくてもよいとされている。
それでコンパイラの独自拡張で変数の配置場所をレジスタに指定することができて、
これを使うことでCでコーディングしながらレジスタ上での処理が保証できるようになっていたが、
問題はこのレジスタ上に配置される変数がグローバル変数扱いだったことである。
グローバル変数といっても当該Cファイルを超えて、extern宣言でアクセスできるわけではないはずだが。
もう1つの問題が使用していたレジスタがスクラッチレジスタではなかったことである。
各アーキテクチャではABI(Application Binary Interface)の取り決めによりレジスタの使い方が決まっている。
32bit版のARMの場合、r0~r3は引数・返り値の受け渡しに使い、r14(lr)は復帰先のアドレスを格納するなど。
で、呼びされた関数で破壊してもよいスクラッチレジスタの規定があり、
r0~r3, r12の5つのレジスタは各関数で退避せず破壊してもよいことになっている。
これ以外のr4~r11, r13(sp), r14(lr)は使用する場合は各関数でスタックなどに退避する必要がある。
どうしてもレジスタ上でやりたい処理をスクラッチレジスタを使ってやっていればよかったのだが、
上を見てもわかるように32bit版のARMの場合、スクラッチレジスタの大半は引数・返り値の受け渡し用でもあり、
そういうレジスタをグローバル変数に割りあてるというのは到底認められないわけである。
そこでこれ以外のレジスタを使っていたのだが、退避させる処理が抜けていたと。
unsigned int reg4; //コンパイラ独自拡張でr4に配置 void something(void){
unsigned int reg4_keep;
reg4_keep = reg4;
//reg4を使った処理
reg4 = reg4_keep }
こう書いていればよかったのだが、そうなっていなかったのである。
というわけでこういう記述に修正してとりあえず回避できた。
なんでこれで従来問題になってなかったのかが不思議だが。
なお、コンパイル結果を確認するとスクラッチレジスタに退避させているようだった。
最大の失敗はこの処理をアセンブラで書かなかったことではないかと思う。
アセンブラで書いていれば、スクラッチレジスタを使って素直に書けたはずなので。
そもそもスクラッチレジスタとそうでないレジスタについて知識がなかったのでは? というのも気になるけど。
そんな処理があることは以前から知っていた(詳しくコードは見てなかったが)
ただ、ローカル変数をレジスタに割りあてる機能だと勘違いしてて退避処理も入れてくれると思っていたが、
そうではなく自分で退避させる必要があったというのも勘違いなのかもしれないが。
冒頭に書いたスタックの使用方法に依存する処理も、
スタックを使わないようにアセンブラで工夫して書くことはできるわけで、
そうしておけばコンパイル結果に依存しないコードになってたのになと。
もちろんアセンブラで書くことでコーディングのミスを起こしやすいとか、
長期的に見れば移植性の問題などあるにはあるけど、
結局コンパイラのバージョン違いで移植性の問題が起きてるじゃないかと。
最近別の仕事でアセンブラでコーディングせざるを得ないところをいろいろやって、
もうそれアセンブラで書けばと言ってしまいがちなのはある。
難しいことをアセンブラで書いちゃダメですけどね。