C言語でシステムプログラミングや組み込み開発を行っていると、「Bus error (core dumped)」というエラーメッセージに遭遇することがあります。
このエラーは、プログラムがメモリに対して不正な操作を行った際に、ハードウェアやオペレーティングシステムによって強制終了させられたことを示しています。
初心者のみならず経験豊富なエンジニアにとっても、原因の特定が難しいエラーの一つとして知られています。
本記事では、このバスエラーが発生するメカニズムから、具体的な原因、そして効率的なデバッグ手法までを詳しく解説します。
Bus error (core dumped)とは何か
Bus errorは、プログラムがプロセッサの処理できない方法でメモリにアクセスしようとした際に発生するシグナル「SIGBUS」を指します。
具体的には、CPUがメモリバスを通じてデータをやり取りする際に、ハードウェア的な制約に違反したときに送出されます。
このエラーが発生すると、多くのシステムではプログラムの実行状態を保存した「コアファイル」を出力して異常終了します。
「core dumped」という表記は、そのメモリイメージがディスクに書き出されたことを意味しています。
現代のコンピュータアーキテクチャでは、OSのメモリ管理ユニット(MMU)がこの異常を検知し、カーネルを介してプロセスに通知します。
Segmentation faultとの違い
C言語で最も頻繁に見かけるエラーは「Segmentation fault(SIGSEGV)」ですが、Bus errorとは発生の要因が明確に異なります。
Segmentation faultは、許可されていないメモリ領域(読み取り専用領域への書き込みや、未割り当て領域)へのアクセスによる論理的な違反です。
一方でBus errorは、メモリへのアクセス方法そのものがハードウェアの物理的な要件を満たしていない場合に発生します。
以下の表に、両者の主な違いをまとめました。
| エラー名称 | シグナル名 | 主な原因 | 発生レイヤー |
|---|---|---|---|
| Segmentation fault | SIGSEGV | 不正なアドレスへのアクセス、権限のない領域への書き込み | OSのメモリ管理(論理的) |
| Bus error | SIGBUS | アライメント違反、存在しない物理アドレスへのアクセス、不適切なメモリアクセス | ハードウェア・バス(物理的) |
Bus errorが発生する主な原因
バスエラーが発生する状況は限られていますが、その多くは低レイヤーの操作に関連しています。
1. メモリアライメントの制約違反
多くのプロセッサでは、特定のデータ型を配置するメモリのアドレスについて「アライメント」と呼ばれる制約があります。
例えば、4バイトの整数(int型)は、4の倍数のアドレスに配置されている必要があるというルールです。
x86_64アーキテクチャでは、アライメント違反を自動的に修正して処理を継続する機能があるため、バスエラーは発生しにくい傾向にあります。
しかし、ARMプロセッサやRISC-V、古いSPARCなどの環境では、この制約に厳格であり、アライメントに合わないポインタ参照を行うと即座にバスエラーとなります。
以下のコードは、ポインタのキャストによって意図的にアライメント違反を引き起こす例です。
#include <stdio.h>
#include <stdlib.h>
int main() {
// 8バイトのメモリを確保
char *buffer = malloc(8);
// あえて奇数のアドレスを指すポインタを作成
// 本来 int は 4 または 8 バイト境界にある必要がある
int *misaligned_ptr = (int *)(buffer + 1);
// アライメント違反の書き込み(アーキテクチャによってはここでBus error)
*misaligned_ptr = 123;
printf("Value: %d\n", *misaligned_ptr);
free(buffer);
return 0;
}
Bus error (core dumped)
2. mmapでマップしたファイルサイズへの不整合
現代のC言語プログラミングにおいて、バスエラーの最も一般的な原因はmmap関数の使用に伴う不備です。
mmapを使用してファイルをメモリにマッピングした後、そのファイル自体が別のプロセスなどによって切り詰められた場合に発生します。
プログラムがマッピングされたメモリ範囲内であっても、対応する物理的なファイルの実体が存在しない場所にアクセスすると、OSはデータの供給ができずバスエラーを発生させます。
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main() {
int fd = open("test.dat", O_RDWR | O_CREAT, 0666);
// 1バイトだけ書き込む
write(fd, "A", 1);
// 4096バイト分マッピングする
char *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// マッピング後にファイルを切り詰めてサイズを0にする
ftruncate(fd, 0);
// ファイル実体がない場所にアクセスしようとしてBus error
printf("Char: %c\n", addr[0]);
munmap(addr, 4096);
close(fd);
return 0;
}
3. デバイスドライバや周辺機器への不正アクセス
組み込み開発において、特定のメモリアドレスにマップされたI/Oレジスタにアクセスする際にバスエラーが頻発します。
存在しないハードウェアアドレスを読み書きしようとしたり、読み取り専用のステータスレジスタに書き込もうとしたりした場合です。
これは、バスプロトコルレベルで「エラー応答」が返されるため、CPUが異常を検知する仕組みになっています。
Bus errorの原因を特定するデバッグ手法
バスエラーが発生した際、どのコード行で問題が起きているかを特定することが解決への第一歩です。
GDB(GNU Debugger)による解析
デバッガを使用すれば、エラーが発生した瞬間のプログラムの状態を詳細に調査できます。
まず、コンパイル時に-gオプションを付与してデバッグ情報を埋め込むようにしてください。
gcc -g -o my_program my_program.c
gdb ./my_program
GDB内でプログラムを実行し、エラーが発生した直後にbacktrace(またはbt)コマンドを入力します。
これにより、エラーを引き起こした関数呼び出しの履歴が表示され、問題の箇所が特定できます。
特にポインタが指しているアドレスの値が、期待されるアライメント(4の倍数や8 deの倍数)になっているかを確認してください。
コアファイルの活用
エラーが発生した際に「core dumped」と表示される場合は、その時点のメモリ情報を保存したコアファイルが生成されています。
最近のLinuxディストリビューションではデフォルトで出力が無効化されていることが多いため、以下のコマンドで有効にする必要があります。
ulimit -c unlimited
生成されたコアファイルをGDBで読み込むことで、クラッシュ時の状況を再現なしで解析できます。
gdb ./my_program core
AddressSanitizerの利用
最新のコンパイラ(GCCやClang)には、メモリバグを検出するための強力なツールが備わっています。
コンパイル時に-fsanitize=addressおよび-fsanitize=undefinedオプションを付けて実行してみてください。
これにより、アライメント違反や不適切なメモリアクセスが発生した際に、非常に詳細な診断レポートが標準出力に表示されます。
この手法は、プログラムを実際に動かしながら潜在的な問題を洗い出すために非常に有効です。
バスエラーを防ぐためのベストプラクティス
エラーが発生してから対処するだけでなく、日頃のコーディングからバスエラーを予防する意識を持つことが重要です。
1. ポインタのキャストに注意する
型の異なるポインタ間でキャストを行う際は、常にアライメント制約を意識する必要があります。
特にchar*型のバッファからint*やstruct*へキャストしてアクセスするコードは危険です。
このような処理が必要な場合は、memcpyを使用してデータを一時的な変数にコピーすることをお勧めします。
memcpyはアライメントを考慮した安全な方法でデータのコピーを行ってくれるため、バスエラーのリスクを回避できます。
2. 構造体のパディングを理解する
C言語の構造体は、メンバ変数が効率よくアクセスできるようにコンパイラによって自動的にパディングが挿入されます。
#pragma pack(1)などの指定でパディングを無効化すると、アライメント違反が発生しやすくなるため慎重に使用してください。
3. ファイルアクセスの安全性を確保する
mmapを使用する場合は、対象ファイルのサイズが実行中に変動しないよう、適切なロック処理を行うことが望ましいです。
また、マッピング前にファイルサイズを確認し、アクセス範囲が超えないようにチェックコードを必ず記述してください。
まとめ
C言語における「Bus error (core dumped)」は、ソフトウェアの論理エラーというよりも、物理的なハードウェア制約との不一致によって生じるエラーです。
主な原因はメモリアライメントの違反や、不適切なファイルマッピング、ハードウェアへの不正なアクセスに集約されます。
デバッグにおいては、GDBやAddressSanitizerを活用して、エラーが発生した際のアドレス値を詳細に検証することが近道となります。
低レイヤーのメモリ構造を正しく理解し、安全なポインタ操作を心がけることで、この厄介なエラーを未然に防ぐことが可能です。
本記事で紹介した手法を駆使して、より堅牢なC言語プログラムの開発を目指してください。
