2015/10/10

[C]externは怖い

仕事ではC#とC/C++、趣味でJavaを使っています。
それぞれ用途に合わせて使っているので混乱することは意外とないのですが、それでもおかしなことをやらかすことはあります。

Cで何気なく書いた、絶対安全だと思っていたコードがクラッシュを引き起こしました

//////data.c////////

const char table[1]={1};

//////logic.c////////

extern const char* table;

if(table[0]==1){

//////////////


運用上の理由があり、どうしてもデータとロジックを分ける必要がありました。
かなりコードサイズに制約がある組込ファームウエアなので、constの定数についてはアクセス関数を使わずに直接externでリンクすることを許していました。
externをヘッダに書かなかったのは、絶対にこの場所でしか参照しないデータで、かつ第三者に存在を公開するのを避けたかったからです

私はtable[]とtable*は表現の違いだけで、同じものだと思っていました
だって、これが成り立つのは自明なので

///////////////

const char table[1]={1};

const char*p = table;

if(p[0]==1){

///////////////

さらに、これならクラッシュしないことも確認しました

//////data.c////////

const char table[4]={1,2,3,4};

//////logic.c////////

extern const char* table;

if(table[0]==1){

//////////////

添え字が1の時はコンパイラが勝手に最適化して定数として展開するのでしょうか?それはおかしい。だってデータ数を知っているのはdata.cの方で、logic.cはサイズを知らないのだから、配列サイズによって振る舞いを変えるということはできないはずです。

こうすればクラッシュしないこともわかりました

//////data.c////////

const char table[1]={1};

//////logic.c////////

extern const char table[];

if(table[0]==1){

//////////////

いよいよわからなくなってきました。
なにしろ私は[]と*はバイナリにしたら同じだと思っていましたから。
逆アセンブルしてみると、バイトコードには差異がありました。
なんだ、この差は?

…純粋なCプログラマの方々はおそらく私が何をいってるんだろうかと失笑していることでしょうね。はい、私も気が付いた時には笑いました。

まず第一に[]と*は違うものです。
説明には諸説あるようですが、*がポインタを表すのはおなじみだと思いますが、table[]は配列をあらわし、tableと書くと配列の先頭のアドレスを指すというルールがあるもののtableがポインタになっているわけではないのです。
それが証拠に下の式は成り立ちませんよね?

//////////////

const char table1[]={1};
const char table2[]={2};

table1 = table2;

//////////////

配列はポインタに代入できるが、ポインタではない。extern const*table;は代入ではないのでconst char table[]と等価ではない、実際には*を使うことによってポインタとご認識をして操作するので異常終了していた、というだけの話でした。

ここでの教訓はいくつかあります
ひとつめ、JavaやC#に慣れすぎていた私はconst char[]をextern char*で参照したものがコンパイルエラーにもリンクエラーにもならなかったので、うまく連結していると思い込んでしまったのですが、CのLinkerはシンボルを名前で連結しているだけで型なんて見てくれていない、ということをすっかり忘れていました。
C怖いよC

もうひとつはexternはヘッダに定義して参照元と参照先が同じ型であることを証明しなければならないという基本を無視してはいけないということです。
このルールはMisra-Cでも規定されています。
今回の例でも、定義をヘッダにしていれば、コンパイラが文句を言ってくれました

//////data.h////////

extern const char* table;

//////data.c////////

#include "data.h"

const char table[1]={1};

//////logic.c////////

#include "data.h"

if(table[0]==1){

//////////////

この場合、data.hの定義ではdata.cのコンパイルが成功しません
当たり前ですね、型がちがうんですから

そして最後の3つ目、Cのexternはでたらめで怖いということ

やっぱり自分は高級言語の方が好きです

0 件のコメント:

コメントを投稿