LoginSignup
134
65

More than 1 year has passed since last update.

C言語で16文字でセグフォらせる

Last updated at Posted at 2020-06-30

↓PythonをSegmentation Faultで落とすのが最近流行っているようなので。
pythonを三行でセグフォらせる
pythonを2行でセグフォらせる
pythonを1行でセグフォらせる
Pythonを33文字でセグフォらせる
Pythonをctypesを使わずに1行でセグフォらせる
Rustを5行でセグフォらせる
主語(Python)をC言語に変えるだけで途端につまらなくなりますが、やってることは同じです。

コード

gcc 10.1.0で動作確認(警告は出ます)

*a;main(){*a=0;}
> gcc segf.c
segf.c:3:4: 警告: データ定義が型または記憶域クラスを持っていません
    3 |    *a;main(){*a=0;}
      |    ^
segf.c:3:5: 警告: 型がデフォルトの ‘int’ に ‘a’ の宣言内でなります [-Wimplicit-int]
    3 |    *a;main(){*a=0;}
      |     ^
segf.c:3:7: 警告: 戻り値の型をデフォルトの ‘int’ にします [-Wimplicit-int]
    3 |    *a;main(){*a=0;}
      |       ^~~~
> ./a.out
zsh: segmentation fault (core dumped)  ./a.out

注意 現在のところ、C言語をセグフォらせる最小の文字数は5文字です。(下のコメント参照)

簡単な解説

これだけだとQiitaの記事要件を満たさないため、簡単に解説します。
まず最初に観るコードは↓です。

int main(void) {
    int *a = 0;
    *a = 0;  // ←ここで落ちる
    return 0;
}

C言語にはポインタという概念があり、これを用いるとメモリの好きな番地にアクセスすることができます。
int *a = 0のところでaという名前のポインタを宣言し、メモリ番地0を代入します。2行目で*aに値を代入すると、実際には存在しないアドレス1 にアクセスすることになり、CPU例外が発生します。

ショートコーディング

それでは、上のコードを短くしていきましょう。まず、C言語の規約上return 0は省略できます。2また、返り値の型intも互換性の観点から省略できます。(省略すると暗黙的にintになります。)さらに、引数のvoidも書かなくても動きます。
これらを踏まえると以下のようになります。

 main() {
    int *a = 0;
    *a = 0;  // ←ここで落ちる
}

ここで、ローカル変数aはグローバル変数として定義してもよいです。さらに、グローバル変数として定義すると、(よほど特殊な環境でない限り)0で初期化してくれるため初期化のための代入が不要です3

*a;
main() {
    *a = 0;  // ←ここで落ちる
}

さらに、グローバル変数の型指定子intも省略できます。これで冒頭のコードになりました。

おまけ。番地*(0p00000000)は本当に存在しないか?

古典的なコンピュータにおいて、メモリはたくさんの情報を保管しておくことができる装置です。しかしながら、たくさんの情報をメモリに送ったり、メモリから受け取るためには、直感的には送受信したい情報の個数分だけ電線が必要になります。しかしながら、CPUの動作速度に耐えられるような細い電線をたくさん用意するのはコスト面で不可能なので、メモリアドレスという概念を導入します。これにより、一度に読み書きできる情報の個数(1Byteという)4は少ないながら、メモリアドレスを変更することによってたくさんの情報をメモリに保管することができます。

このように、メモリ大容量時代に欠かすことのできないメモリアドレスという概念ですが、これは0から始まる正の整数で表されるため、実際には0番地というアドレスは存在するのです。では、なぜセグメンテーションフォルトが起きたか、というと、OSが利用しているページングというCPUの機能が原因です。

コンピュータにおいてメモリアドレスはユニークです。よって、特に工夫しない場合、マルチタスキングOSにおいて複数のプロセスが同じメモリアドレス空間を所有することになります。この場合、メモリにアクセスする場合は、他のプロセスが利用しているメモリに間違ってアクセスしないように気をつけなければなりません。このような不便を解消するのがページングで、ページングを用いると各プロセスがそれぞれ別個の仮想アドレス空間を所有できます。このとき、仮想アドレス空間5と物理(メモリ)アドレス空間の間を変換する表をOSが管理しています。

よって、本来プロセスは存在するすべてのアドレスにアクセスできるはずです。しかしながら、実際はそうはいかず、仮想アドレス空間においてもOSが幅を利かしています。プログラムから利用できるメモリの範囲を制限しているのです。このような仮想アドレス空間の区画分けをメモリマップといいます。この記事にあるように、0番地の周辺は使用しないこととなっており(対応する物理メモリアドレスが割り当てられていない)、そのためセグメンテーションフォルトが発生します。6

おわりに

さらっと書くつもりが20分くらい掛かった。書かなきゃよかった。

  1. 後述。

  2. return 0;を省略するのは間違い、と勘違いしている人が大勢います。

  3. セグフォを起こすだけなので、初期化しなくても大丈夫だろうけど。

  4. 実際にはメモリモジュール一本あたり64bitが同時に書き込めます。

  5. とリニアアドレス空間

  6. OSの実装方法によります。また、OSによっては別のCPU例外が発生します。

134
65
15

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
134
65