未使用と思われる構造体のメンバを消しただけなのに
とあるC言語のOSSのリファクタリングにて、未使用と思われる構造体のメンバを消去したところ、CIにて大量の実行時エラーが検出されました。一体何が起こったのでしょうか。
はじめに
別件のバグ修正をしている過程で、とある構造体のメンバ変数が使われていないのではないかと思いました。その構造体メンバ変数は下記のch_event
というものです。
struct flb_config {
struct mk_event ch_event; // これを削除
int support_mode;
int is_ingestion_active;
int is_shutting_down;
int is_running;
// この後、大量のメンバ変数定義が続く
消去可能かを判断するにあたり、次に述べるポイントを確認しました。
確認したポイント
確認したのは下記二点です。
- 構造体のメンバ名でソースを全grepし、構造体定義/初期化/クリンナップ処理しかなく、実際に使われている箇所が見当たらなかったこと
- 上記の構造体定義/初期化/クリンナップ処理を消去した後にビルドを行い、ビルドが正常終了すること
ここまでを確認し、作成したプルリクエストが下記です。
結果どうなったか
CI のエラーが大量にでました。なんでぇ。。
幾つか見てみたところ下記の傾向がありました。
- コンパイルエラーではなく実行時エラーであること
- 複数種類の実行時エラー(SIGSEGV, スレッド間の同期がとれていないなど)が出ていること
1からするに、メンバ変数は未使用であるように思えます。実際に使用されていればビルド時に未定義のためにエラーとなることが予想されるためです。
2については、複数の症状が出ていましたが、そのうちの一つの現象を追うこととしました。
その現象についての詳細は省きますが、直接の原因としては、今回メンバを消去した構造体に含まれる他のメンバ変数が不自然にクリアされる為のようでした。
構造体のメンバを消去したところ、他のメンバが不自然にクリアされるようになる。何かしら関係がありそうです。
原因調査
力技のprintfデバッグを行い、メンバ変数がクリアされる処理を特定することができました。それが下記の箇所です。
ここでstruct flb_config *config
が今回メンバ変数を消去した構造体のポインタです。
int flb_engine_start(struct flb_config *config)
{
// 中略
ret = mk_event_channel_create(config->evl,
&config->ch_manager[0],
&config->ch_manager[1],
config);
全ての引数にconfig
が含まれていますが、いったいどこに原因があるのでしょうか。
ここで、この関数の定義を見てみましょう。
int mk_event_channel_create(struct mk_event_loop *loop,
int *r_fd, int *w_fd, void *data);
最後の引数がvoid *
、つまりあらゆるポインタを受け付けるようですね。
そしてもう一度、構造体と削除したメンバを思い出してみましょう。
struct flb_config {
struct mk_event ch_event; // これを削除
int support_mode;
int is_ingestion_active;
int is_shutting_down;
int is_running;
// この後、大量のメンバ変数定義が続く
struct flb_config *
をvoid*
として投げる関数。削除したメンバ変数は構造体の先頭メンバ。
うーむ?
根本原因
struct flb_config *
の指すアドレスは、構造体全体の先頭を意味するほか、先頭メンバのstruct mk_event
のアドレスでもあります。
struct flb_config:
|----------------------------|
| struct mk_event ch_event; | <- struct flb_config *config と &config->ch_event は
| | 同じアドレスを指す。
| |
|----------------------------|
| int support_mode; |
|----------------------------|
| int is_ingestion_active; |
|----------------------------|
| int is_shutting_down; |
再度、値がクリアされる関数の呼び出し箇所を見てみましょう。
int flb_engine_start(struct flb_config *config)
{
// 中略
ret = mk_event_channel_create(config->evl,
&config->ch_manager[0],
&config->ch_manager[1],
config);
そうです、上記の第4引数はflb_config *
を与えているにもかかわらず、実際の処理としては、先頭メンバと同じ型のstruct mk_event *
を期待する実装となっていました。
よって、先頭メンバを消したことで、後続するメンバ変数の値が繰り上がり、それらがstruct mk_event
だと解釈されて、別の値で上書きされ、結果様々な症状が出ていたというわけですね。
通りで使用箇所がgrepで引っかからないわけだよ。。
対策
まあ、下記のいずれかでしょうね。今回は1を選択しました。本当は2もやったほうがいいのでしょうが、今回は対応をしていません。
- 関数に与える引数を
&config->ch_event
とし、明示的に先頭メンバを与えるようにする - 関数定義の第4引数を
void*
でなく、struct mk_event*
とする
実際のプルリクエストは下記です。
最後に
いかがでしたか。
grepで引っかからないことを確認しても、使われていることがあるというお話でした。
Discussion