定時内の移動中は仕事にちょっと関係する勉強でもするか、
ということでセキュアコーディングの勉強をしていた。
その中で、前々からなんで静的解析で指摘されるのだろうと思っていた部分の真の意味がわかった。
が、それは本当に心配するべきことなのだろうか? と思った。
(1u<<31)のような1をビットシフトしてデータを作る記述は、
特にマイコンのGPIO操作などではあまりによく見る記述である。
これは結果としては 0x80000000u と同じことを指すものの、
31bit目を1にするという明確な意図が伝わりやすいし、
(1u<<FOO_CH)のようにビット番号を他の定数を使って表したりもする。
あまりに当たり前すぎる表現だが、これがいちいちケチが付くのである。
これが意図した結果が得られることは机上検討で明らかなので、それでよいことにしているが、
果たしてこの記述にどのようなリスクがあるのだろうか?
答えはこの記述はint型のビット幅に依存するということのようだった。
1u<<31 が 0x80000000u と同じにならない場合があると。
1uというのはunsigned int型の1を指す。
さらに言えばC言語で整数演算するときはint型あるいはunsigned int型に揃える汎整数拡張というのがある。
この汎整数拡張というのもくせ者で、特にビット反転では思いもしない結果を生じさせかねないもので、
そういう問題を未然に防ぐためのコーディングというのもあるのだけど。
ただ、1u<<31 を32bit幅のレジスタなどに代入する分には何も直感に反する動きはないように思う。
しかしここには前提条件が合ってint型が32bit幅であることである。
いろいろデータ型モデルはあるが、今どきはほぼintは32bit幅である。
しかし16bitのマイコンなどではintが16bitというものも存在する。
この場合 1u<<31 は16bit幅のunsigned int型から押し出されて0になってしまう。
これにより 1u<<31 の結果は直感に反するものになりますよということである。
なるほど。確かに16bitのマイコンってのもあるから移植性の問題はあるかもしれない。
でも、そのときにこのコードをそのまま使うのは現実的ではない気もするが。
1uという整数リテラルは16bit幅、32bit幅、めったにないが64bit幅のintいずれでも表現できる。
一方で0x80000000uという整数リテラルは32bit以上の幅でなければunsigned intで表現できない。
このような場合はlongやlong longを適用することになる。
なのでintが16bitでlongが32bitの場合は 0x80000000u はunsigned long型になるはず。
この観点では 1u<<31 より 0x80000000u の方が移植性がよさそうである。
しかし、コンパイラによってintの幅が違うって不便じゃない。
というのは当然あって、それで stdint.h にuint32_t などビット幅固定の型が用意されている。
そして実はここには整数リテラル用のマクロってのもある。
UINT32_C(1) のように書くと最低32bitの幅のある符号無し型の整数リテラルを生成できる。
intが32bit幅の場合は UINT32_C(1)は 1u、intが16bit幅・longが32bit幅の場合は 1UL となる。
これを使って UINT32_C(1)<<31 って書けば文句ないだろ?
と静的解析にかけたのだがマクロ展開後の 1u<<31 で解析されたのか何も変わらなかった。
静的解析ツールが問題ないと判断できる書き方は ((uint32_t)1)<<31
のような表記になる。
でもこんなのをビットシフトで数値を作る度に書いていては括弧だらけで読みにくくなってしまう。
で、なんでこれが問題なのかというところにたちもどって考えて見ると、
int型が32bitより狭い環境に移植する場合に問題となると。
じゃあint型は32bitであると規定できれば問題にならないのでは? と。
16bitのマイコンのことはあるが、32bitも64bitもintは32bitであると考えてよい。
long型のサイズの方がアテにならない
32bit環境ではほぼ ILP32 で、int, long, ポインタ型がいずれも32bit幅である。
64bit環境では LP64 と LLP64 の2パターンがほとんどである。
LP64は intが32bit、longとポインタ型が64bitである。
LLP64は int, longは32bit、ポインタ型とlong long intが64bit型である。
UNIX系ではポインタ型をlongに代入する使い方が多くみられ、
その用途ではLP64が移植性がよいという理由で主流になっているよう。
とはいえ、いずれもint型は32bit幅である。
汎整数拡張の都合、int型の幅が変わる影響が大きいので、そこは回避したということなんだろう。
ちなみに16bit→32bitの移植ではintの幅は変わるが、longの幅はいずれも32bitなので、
この部分だけ見るとlongの方が移植性がよいように思えた。
この経緯から32bitを表すためにlongを使っているコードも多いんですよね。
この観点ではLP64は余計なお世話なのだが……
こういう歴史も考えれば、将来どうかというのを予測するのは難しいが、
int型が32bitより狭くなるということは考えないという決断はあってもいいように思う。
他にも想定しているリスクに見合わないような指摘もありますけどね。
ただ、想定しているリスクが何なのかは正しく勉強しておかないといけないと思ったのだった。