同一段業務コードにおいて、プログラムが CentOS 7 環境下で正常にコンパイルおよび実行されていたが、CentOS 8 に切り替えて GCC の最新版を使用してコンパイルを行った際に、プログラムがクラッシュが発生した。注目すべきは、問題が Release モード 下でのみ発生し、Debug モード では完全に問題がない点である。これは初めての事例であり、3日間の調査を経て、問題の原因を特定することができた。
問題の特定
一番の原因究明の結果、問題の本質は 関数に返り値がないこと にあります。リリースモードにおいて、GCCの新バージョンではより多くの最適化が行われるため、本来返り値のない関数が実行中に未知の論理が発生し、それがクラッシュを引き起こしました。結論として、コンパイラの警告を無視することは許されません。特に、古いプロジェクトにおいては、一部の警告が無視される可能性がありますが、すべての警告を無効にすることは避けるべきです。
環境説明
- CentOS 7 GCCバージョン:
- CentOS 8 GCCバージョン:
クラッシュ現象
プログラムのクラッシュに関するスタックを分析した結果、以下のスタック情報が得られました:
[New LWP 1385902]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Core was generated by `./pstack_main`.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00007ffe894b4420 in ?? ()
(gdb) bt
#0 0x00007ffe894b4420 in ?? ()
#1 0x00000000004008e9 in main ()
このスタックは直感的ではありません。クラッシュ関数のスタック情報が ??
と表示されるため、問題の特定がさらに複雑になります。
コード例
問題をより良く理解するために、クラッシュを再現するための最小コード例を示します。
#include <iostream>
#include <map>
int test() {
std::cout << "1" << std::endl;
}
int main() {
test();
return 0;
}
このコード内の test()
関数は明らかに値を明示的に返していません。また、その戻り値の型は int
です。C++ 仕様によると、関数が int
型で宣言されている場合、必ず戻り値を持つ必要があり、そうでない場合は未定義動作を引き起こす可能性があります。
コンパイル警告
当方のプロジェクトにおいて、CMake スクリプトが多くのコンパイル時の警告を抑制しており、その中に以下の警告情報が含まれています。
/root/pstack/main.cpp: In function ‘int test()’:
/root/pstack/main.cpp:7:1: warning: no return statement in function returning non-void [-Wreturn-type]
この警告は、test()
関数が戻り値を持たないことを示しており、これがまさに問題の原因です。GCC の高バージョン(例:8.5.0)では、コードを最適化する際にこのような未定義の動作に対して不安定な最適化を行う可能性があり、プログラムがクラッシュする原因となることがあります。
어셈블리 코드 차이점
GCC 컴파일러 최적화 동작의 차이를 설명하기 위해, 서로 다른 버전의 GCC가 생성한 어셈블리 코드를 비교했습니다.
- GCC 4.8.5 생성된 어셈블리 코드:
- 어셈블리 코드가 다소 길고 표준 출력 스트림(예:
std::cout
) 처리에 대한 로직을 포함하고 있습니다. 이는 컴파일러가test()
함수에서 누락된 반환 값 문제에 대해 과도하게 최적화하지 않았음을 나타내며, 이로 인해 잠재적으로 충돌을 피했을 수 있음을 시사합니다. - GCC 8.5.0 생성된 어셈블리 코드:
- 새로운 버전의 GCC는 더 많은 최적화를 수행하여 코드 양을 줄였습니다. 그러나 이러한 최적화가 누락된 반환 값을 갖는 함수의 실행 시 동작이 불확실하게 되어 프로그램 충돌을 유발할 수 있습니다.
結論
今回の問題解決を通して、C++において関数が返す値は明確に定義されるべきであるという点を深く認識しました。特に、関数をint
型として宣言する場合、必ず戻り値を提示する必要があります。古いコンパイラ版を使用しているプロジェクトでGCCの新しいバージョンにアップグレードした場合、より多くの最適化や厳格な警告メカニズムが導入される可能性があります。そのため、コンパイル時にすべての警告を無効化しないことを推奨します。代わりに、関数が返す値、型の一致など、一般的な問題に対して選択的に対処する必要があります。
最終的に、test()
関数に戻り値を付与することで問題は解決し、プログラムは正常に動作するようになりました。