C言語

LinuxでSIGBUSを発生させる

C言語を扱う中で、バスエラー(BUSE. SIGBUS)について調べたのでメモを残しておきます。

本記事のコードは、WSL2のUbuntuで動作させた結果を掲載しています。

一応、以下に検証した動作環境を。

環境・バージョンなど

  • Windows 10 Pro 20H2 (OSビルド: 19042.782)
  • Windows Subsystem for Linux Update 5.4.72
  • Ubuntu 20.04.2 LTS

バスエラーは物理メモリやハードウェアバスに関するエラー、、、らしい

バスエラー(BUSE)は、CPUが物理メモリにアクセスしたときに発生するエラーだとのこと。存在しないデバイスや、制限されているデバイスへのアクセスで発生する、との記載も見かけました。

むむ、難しい。確保してないメモリに触ったときはSEGVが発生するけど、そうじゃないメモリのエラーで発生するということですかね。

メモリアライメント違反とかでもおこるようです。

発生させてみる

とりあえず、実際に発生させてみましょう。思いついたものから試してみます。

スタック領域の文字列の範囲外を書き換え

ヒープ領域ではなくスタック領域のメモリの範囲外へのアクセスだとどうなるでしょうか。スタック領域が物理メモリなのかどうかわからないですが。。。

int	main(void)
{
	char str[3] = "abc";

	str[3] = 'd';
	return (0);
}
$ gcc test_buse.c -o a.out
$ ./a.out
*** stack smashing detected ***: terminated
Aborted

思ってたのと違う実行結果でした。これも物理メモリではなかったということですかね。

ていうか、スタック領域のメモリの外側に触れると、ちゃんとその旨表示するんですね。知らなかった。

アライメント不整合のメモリアクセス

次は、調べてみたときに見かけた「メモリアライメント違反」の場合を試してみます。

#include <stdio.h>
#include <stdlib.h>

int	main(void)
{
	long *l;
	char *c;

	l = (long *)malloc(sizeof(long) * 2);

	/* ポインタ変更前 */
	printf("l(%p): %ld\n", l, *l);

	/* longのポインタをcharのポインタに代入して、1バイトだけ動かしてから戻す */
	c = (char *)l;
	c++;
	l = (long *)c;

	/* 変更前から1バイトずれて、アライメント違反になっているはず */
	printf("l(%p): %ld\n", l, *l);

	return (0);
}
$ gcc test_buse2.c -o a.out
$ ./a.out
l(0x562c8fdef2a0): 0
l(0x562c8fdef2a1): 0

あれっ、エラーにならず正常終了してしまいました…明らかに不整合なのに…

もうちょっと調べてみたところ、アライメント違反のチェックフラグがデフォルトでオフになっているようです。

EFLAGSのACをオンにすると検知してくれます。

#include <stdio.h>
#include <stdlib.h>

int	main(void)
{
	/* asmでEFLAGSを変更 */
	asm( "pushf\n\torl $0x40000,(%rsp)\n\tpopf");

	long *l;
	char *c;

	l = (long *)malloc(sizeof(long) * 2);

	/* ポインタ変更前 */
	printf("l(%p): %ld\n", l, *l);

	/* longのポインタをcharのポインタに代入して、1バイトだけ動かしてから戻す */
	c = (char *)l;
	c++;
	l = (long *)c;

	/* 変更前から1バイトずれて、アライメント違反になっているはず */
	printf("l(%p): %ld\n", l, *l);

	return (0);
}
$ gcc test_buse3.c -o a.out
$ ./a.out
Bus error

めでたくバスエラーが発生しました。

アセンブリわかんないな~…近いうち勉強しないと…

バスエラー、めったに見る機会がなさそう

昔は、アライメント違反で普通に発生してたんですかね…

いまはデフォルトでチェック機能がオフになっているので、意図的に検知させないとめったに遭遇しないんじゃないでしょうか。

けっきょく、物理メモリとかハードウェアバスのところまでは理解が及びませんでしたが…

どうやら前提知識が不足しているっぽいので、いまは深追いせずにペンディングにしときます。

説明できるようになったら、また記事追加しようと思います。

以上、あなたのお役に立てれば幸いです。

char *const str と const char *str の違い

execve のmanを読んでいるとき、 プロトタイプ宣言の中で「 char *const 」 と 「 const char * 」が混在して使われているのを見かけました。

int
execve(const char *path, char *const argv[], char *const envp[]);

pathは「 const char * 」で、argv と envp は「 char *const 」になっています。

調べてみたところ、const を置く位置によって、挙動が変わるとのこと。

「 const char * 」の場合は定数データ として扱われ、ポインタの参照先の内容を変更することができません。

一方、「 char *const 」は、定数ポインタ となり、別のポインタのアドレスを別の場所に変えることができなくなりますが、参照先の内容を変更することは可能です。

int main()
{
    /* const char * は「定数データ」 */
    const char *fixed_data = "this data can't change.";

    /* char *const は「定数ポインタ」 */
    char *const fixed_ptr = "this data can change.";

    char *another_str = "this is normal string.";

	fixed_data[0] = "T"; /* 定数データなので、内容を変更できない(コンパイルエラー) */
	fixed_data = another_str; /* ポインタは定数化されていないため、置き換えられる */

	fixed_ptr[0] = 'T'; /* 定数ポインタであり参照先は定数ではないので、内容の変更はできる */
	fixed_ptr = another_str; /* ポインタを変更することはできない(コンパイルエラー) */

  return (0);
}

上記のコードをコンパイルすると、こんな感じにエラーが出ます。

$ gcc test_const_variation.c
test_const_variation.c: In function ‘main’:
test_const_variation.c:11:16: error: assignment of read-only location ‘*fixed_data’
   11 |  fixed_data[0] = "T"; /* 定数データなので、内容を変更できない(コンパイルエラー) */
      |                ^
test_const_variation.c:15:12: error: assignment of read-only variable ‘fixed_ptr’
   15 |  fixed_ptr = another_str; /* ポインタを変更することはできない(コンパイルエラー) */
      |            ^

実にややこしいですね…良い覚え方はないものでしょうか。

とりあえずは、「*const」は定数ポインタ、と覚えておくことにしようと思います。

ウチイダは間接参照演算子(*)を変数名側に寄せる、いわゆる「ポインタ変数記法」を使うことが多いので、constに「*」がくっついたら、定数ポインタを定義しているように読むのが自然な気がしています。

以上、あなたのお役に立てればうれしいです。

多重ポインタのインクリメントはかっこが必要

文字列のポインタのポインタ(多重ポインタ)を利用する場面で、インクリメントをしようとしたらエラーになりました。

void func(char **p)
{
  /* 1文字ずつ処理したかった */
  while (**p != '\0')
  {
    /* ここに処理 */
    *p++; // インクリメントで参照先を一つずつ移動
  }
  return ;
}

そうしたら、コンパイル時に以下のエラーが。

error: value computed is not used [-Werror=unused-value]

どうやらこの書き方だと、*(p++) と解釈されてしまうようです。

void func(char **p)
{
  /* 1文字ずつ処理したかった */
  while (**p != '\0')
  {
    /* ここに処理 */
    (*p)++; // ポインタを示すようにかっこで囲む
  }
  return;
}

このように変数部分にかっこをつける修正をしたところ、エラーが解消しました。

間接参照演算子の優先順位が、インクリメント記号よりも低いことが原因とのこと。

これ知らなかったらハマるやつですね。メモっておこう。

あなたのお役に立てれば嬉しいです。

C言語の左シフト演算は符号付きの場合も論理シフトする

本当は言語としては未定義なのですが、主要なコンパイラは論理シフト演算として実装しているようです。gccとclangでは論理シフトしました。

#include <stdio.h>

int main()
{
    // 1byteのsigned charで検証
    signed char signed_1;
    signed char signed_2;

    //0b接頭辞で2進数を表現できる
    signed_1 = 0b01010000; // -> 80
    signed_1 = signed_1 << 1; // -96(0b10100000)
    printf("sigined_1: %d\n", signed_1);

    signed_2 = 0b10111111; // -> -65
    signed_2 = signed_2 << 1; // 126(0b01111110)
    printf("signed_2: %d\n", signed_2);
}

算術シフトのつもりでビットシフトするとハマりますね。

ちなみに、右シフトはちゃんと算術シフトされます。

あなたのお役に立てれば嬉しいです。