プログラミング演習 FAQ¶
gcc でのコンパイル方法を忘れてしまいました¶
本演習では,基本的に以下のように実行すればよい.例えば,segv1.cをコンパイルして,segv1という実行ファイルを作るならば,
$ gcc -Wall -g -o segv1 segv1.c
# オプションの意味は以下の通り
# -Wall 警告を全表示
# -g デバッグしやすい実行ファイルを作る
# -o segv1 出力ファイル名 (この例は segv1 を出力)
# segv1.c Cのソースファイル
ここでのオプションの-oは小文字であること,オプションを与える順番に注意すること.例えば,以下のようなコマンドは,絶対に実行してはならない.
# これはダメ.segv1.c が消えます.
$ gcc -Wall -g -o segv1.c segv1
詳細は,教育用計算機リファレンス - gccを参照してください.
コンパイルしたが,実行できない¶
基本的な内容は,緑教科書(工学基礎実験実習の教科書)にも記載されています.復習してください.
以下,上述のコンパイル例のようにsegv1コマンドを作ったとして,説明します.
「コマンドが見つかりません」というエラーの場合
パス(path)の概念を忘れていないでしょうか?例えば:
$ ./segv1
「許可がありません(Permission denied)」というエラーの場合
実行権限(permission)の概念を忘れていないでしょうか?例えば:
$ chmod +x segv1
実行ファイルを作るときは,.exe を付けないといけないのですか?¶
演習室の Linux 環境では不要です.
1年次3--4学期の「専門基礎科目 プログラミング」のように,Windows上でコンパイルと実行をしたいならば,必要です.
プログラムを実行すると Segmentation fault と出てプログラムが止まります¶
Segmentation fault とは,あなたのプログラムが,読み書きしてはいけないアドレスにあるメモリを,誤って読み書きしようとしたために,OSがあなたのプログラムを強制終了させた,ということを意味します.segv とも呼ばれます.
典型的には,ポインタが自分のプログラム中で確保した領域以外を指しているのに*pとした場合に起こります.以下のsegv1.cをコンパイルして実行すると,ほぼ確実に segv が起こります.
1 2 3 4 5 6 7 8 9 10 11 | #include <stdio.h>
int main()
{
int *p = 0;
*p = 100; // Segmentation fault
printf("%d\n", *p);
return 0;
}
|
実行結果:
$ gcc segv.c
$ ./a.out
Segmentation fault
pの初期値 0 (NULL)は,ほとんどの処理系では,読み書きできない場所です. したがって,*p = 100;によって Segmentation fault が起こります.
以下は,もう1つの例 segv2.c です:
1 2 3 4 5 6 7 8 9 10 11 12 | #include <stdio.h>
int main()
{
int i = 0;
printf("number: ");
scanf("%d", i); // Segmentation fault
printf("%d\n", i);
return 0;
}
|
実行結果:
number: 1
Segmentation fault
C言語では,scanf()のような標準ライブラリであっても,与える引数を間違えれば segv が起こります.上記の例では,scanf()によって1つの整数を読んで結果をi番地に置くように指定しています.iは0ですから,segv1.cと同様の問題で Segmentation fault が起こります.正しくは,&が必要で,
scanf("%d", &i);
と書くべきですね.&iは,iを保存している場所(番地)ですから,れっきとした自分のプログラムのための場所です.
この問題は,そもそも引数の型が違うので,コンパイル時の警告で分かります.scanf()への第2引数は,int*型であるべきなのに,このプログラムではint型になっているので,コンパイル時に以下の警告が出ます.
$ gcc -Wall segv2.c
segv2.c:8: warning: format ‘%d’ expects type ‘int *’, but argument 2 has type ‘int’
scanf()関数の"%d"が何を意味しているかまで知っているgccは,非常に賢いですね.賢いgccの警告を無視すると,大抵酷い目に会います.
以下もよくある間違いです (segv3.c):
1 2 3 4 5 6 7 8 9 10 11 12 | #include <stdio.h>
int main()
{
int *p;
printf("number: ");
scanf("%d", p); // Segmentation fault
printf("%d\n", *p);
return 0;
}
|
pは,ポインタなので,何処かを指している筈ですが,指した先は,自分のプログラムが確保した場所ではありません.scanf が書き込もうとすると,同じく Segmentation fault が起こります.このプログラムは,gcc が警告してくれません. 上記の例は,pがたまたまメモリの空き領域を指していたりすると,動いたりすることがありますが,これは,全くの偶然で動いているだけで,メモリの利用状態が変われば,Segmentation fault が起こります. 絶対やってはいけません.
Segmentation fault が起こった場合は,ポインタが意図しない(自分で宣言した変数や malloc で確保した場所)以外を指していないか.チェックしてみてください.
Segmentation fault が起こる場所を特定したいです(詳細編)¶
演習室には,gdbというデバッガが用意されています.以下は,チュートリアルです.詳細は,教育用計算機リファレンス - gdbを参照してください.
以下の segv1.c をコンパイルして実行すると
1 2 3 4 5 6 7 8 | #include <stdio.h>
int main()
{
int *p = 0;
*p = 100; // Segmentation fault
printf("%d\n", *p);
}
|
実行結果:
$ gcc segv.c
$ ./a.out
Segmentation fault
となります.gdbを使って, Segmentation fault を起こした個所を調べてみましょう.
$ gcc -g segv1.c
$ gdb -q ./a.out
(gdb) run
Starting program: a.out
Reading symbols for shared libraries +........................ done
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x0000000000000000
0x0000000100000f04 in main () at test.c:6
6 *p = 100; // Segmentation fault
(gdb) print p
$1 = (int *) 0x0
(gdb) quit
gdbを実行して,引数にデバッグしたい 実行ファイル (例では
a.out) を指定します.(gdb)の後にコマンドを入力していきます.
ここでは,run コマンドでプログラムの実行を開始した直後にプログラムが停止しています.
6 *p = 100; // Segmentation fault
6行目がおかしいようです.念の為にpの値を確かめると,
(gdb) print p
$1 = (int *) 0x0
0x0,つまり,pは0番地を指しています.このことから,0番地に100を保存しようとして,Segmentation fault が出ていることが分かりました.
(gdb) quit
quitで終了です.
scanf()のようなライブラリ関数内で segv が発生した場合は,どうでしょうか.segv2.cで試してみます.
1 2 3 4 5 6 7 8 9 10 11 12 | #include <stdio.h>
int main()
{
int i = 0;
printf("number: ");
scanf("%d", i); // Segmentation fault
printf("%d\n", i);
return 0;
}
|
gdbを使って, Segmentation fault を起こした個所を調べてみましょう.
$ gcc -gdwarf-3 segv2.c
$ gdb -q ./a.out
(gdb) run
Starting program: a.out
Reading symbols for shared libraries +........................ done
number: 1
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x0000000000000000
0x00007fff902d15fa in __svfscanf_l ()
(gdb)
number: に 1 と入力すると,止まりました.これを見ると,__svfscanf_lという関数の中で0番地にアクセスしようとしてSegmentation fault が出ていることが分かりました.これでは,自分のプログラムの何処が悪いのかよく分かりません.そこで,backtraceコマンドを使ってみます.
(gdb) backtrace
#0 0x00007fff902d15fa in __svfscanf_l ()
#1 0x00007fff902aeb0f in scanf ()
#2 0x0000000100000ee5 in main () at segv2.c:8
(gdb)
backtraceコマンドは,関数の呼び出し系列を過去にさかのぼって表示してくれるコマンドです.これを見ると,segv2.c の8行目 →scanf→__svfscanf_l(segv発生) という経路が分かります.ここまでの情報で,ほぼ原因が特定できますが,もう少しgdbで調べてみましょう.
問題のscanf()を呼び出したときの引数の値が分かると便利ですね.
#0 0x00007fff902d15fa in __svfscanf_l ()
#1 0x00007fff902aeb0f in scanf ()
#2 0x0000000100000ee5 in main () at segv2.c:8
(gdb) select-frame 2
(gdb) print i
$1 = 0
(gdb) quit
select-frame 2で,#2 0x0000000100000ee5 in main () at segv2.c:8の状態に巻き戻します. これを,スタックフレームを移動するといいます.print iで,引数に使っているiの値を確認しています.
このように,gdbの助けを借りることで,segv の場所を特定することが容易になります.gdbのその他の使い方は,
man gdb
してみてください.
Segmentation fault が起こる場所を特定したいです(簡易編)¶
演習室では,catchsegvという簡易チェックプログラムが利用できます.なお,このプログラムは,GDBと同じで,コンパイル時に-gを付けていないとあまり意味を成しません.例えば,以下のようなsegv3.cを考えてみます.これはsegv1.cと同じ理由で Segmentaiont fault を発行しますが,関数呼び出し中に発生している点が異なります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #include <stdio.h>
void segfunc(void)
{
int x = 0;
int *p = 0;
*p = 100; // Segmentation fault
printf("%d\n", *p);
}
int main()
{
segfunc(); // Segmentation fault
return 0;
}
|
$ gcc -g segv3.c
$ catchsegv ./a.out
たくさん出力されると思いますが,この中から,先頭がBacktrace:で始まる行に注目してください.(例えば,catchsegv ./a.out | grep -A5 Backtraceなどとすると,早く見つけられます.)
34 35 36 37 38 39 40 | Backtrace:
/home/users/ecs/hara/eop/segv3.c:7(segfunc)[0x400538]
/home/users/ecs/hara/eop/segv3.c:14(main)[0x400560]
/usr/lib64/libc.so.6(__libc_start_main+0xf5)[0x7f75e5d38495]
??:?(_start)[0x400459]
Memory map:
|
ハイライトされた箇所に注目します.35行目は,segv3.cの7行目(segfunc関数の中)で,Segmentation fault がおきた,ということを意味しています.そして,続く36行目は,segv3.cの14行目あたり(main関数の中)で,Segmentation fault がおきた,ということを意味しています.
segv3.cの14行目あたりというのは,すなわち,segfunc関数をmain関数が呼び出している箇所,です.(gdbとは異なり,1行ずれて表示されることがあります.)
fprintf() と printf() はどう違うのですか¶
fprintfの第1引数は,出力するチャンネル(ファイルポインタといいます)をあらわしています.C言語のプログラムは,実行開始時に以下の2つの出力チャンネルを持っています.
- stdout
標準出力 (初期値は画面に向いている)
- stderr
標準エラー出力 (初期値は画面に向いている)
printf("....")は,fprintf(stdout, "....")と同じ意味で,stdoutチャンネルに文字列を出力していることになります.stdoutチャンネルへの出力は,以下のようにリダイレクトすると,ファイルfile.txtに保存できます.
$ ./a.out > file.txt
リダイレクトの記号>は,stdoutチャンネルを画面ではなく,ファイルfile.txtに向けなさいという意味なのです.したがって,上記のリダイレクトをした場合でも,fprintf(stderr, "....")とした場合は画面に出ます.
以下の例を見てみましょう.
stdin-stdout-sample01.c:
1 2 3 4 5 6 7 8 9 10 | #include <stdio.h>
int main()
{
printf("STDOUT by printf\n");
fprintf(stdout, "STDOUT by fprintf\n");
fprintf(stderr, "STDERR by fprintf\n");
return 0;
}
|
これをコンパイルして実行してみます.
$ gcc stdin-stdout-sample01.c
$ ./a.out
実行結果:
STDOUT by printf
STDOUT by fprintf
STDERR by fprintf
この時点では,stdoutとstderrを違えてみたところで,動作に違いがあるようには見えません.では,出力をstdout.txtファイルにリダイレクトしてみましょう.
$ ./a.out > stdout.txt
実行結果:
STDERR by fprintf
画面には,STDERR の方だけが表示されていることが分かります.では,stdout.txtの中身は,どうなっているでしょう.それは,もちろん,
$ cat stdout.txt
実行結果:
STDOUT by printf
STDOUT by fprintf
のように,STDOUTだけが入っています.
つまり,fprintf(stderr, "....")は,利用者がリダイレクトをした場合でも,ファイルではなく画面に出力したい場合に使います.主な用途としては,以下があるでしょう.
エラーや警告など,利用者に注意を喚起したい出力.
途中結果の確認やデバッグメッセージのように,ファイルにリダイレクトしては困る出力.
stdout と stderr を上手に使い分けてください.
printf() による出力がされていない(あるいは,タイミングがおかしい)ように感じることがあるのだが,なぜ?¶
※類似FAQ:『名簿管理プログラムでLinuxのプロンプトを模擬して,Input number:という文字列を stdout に表示してから,その横で数字入力を受け付けようとしたのだが,そもそもInput number:が画面に出力されない.しかし,実際に数字を入力して処理を続けると,思い出したように突然出力される.なぜ?』
コンピュータの仕組みの一つに,バッファリング(buffering)という仕組みがあります.バッファリングとは,一時的に処理すべき内容をメモリ内に蓄えて置き,何らかのトリガーによって,その内容をすべて処理するような仕組みのことです.(ものすごく雑に例えると,複数人で食事に行ったときに,個別会計するのではなく,お金を先に集めてから1度に会計したほうが,何かと効率が良いのと似ています.)
それで,printf()が出力しようとする標準出力 stdout は,通常 OS (operating system) によってバッファリングされています.画面出力をバッファリングすることで,効率よく画面表示をすることができるからです.一方,stderr は,通常バッファリングされていません.
バッファリングされた stdout への出力は,行ごとに出力されます.つまり,改行コードを画面表示しようとしたタイミングで画面表示されます.自分のタイミングで表示したいなら,K&Rのp.303あたりに参考となる情報が見つかるでしょう.
なお,デバッグ用途では,printf()ではなく,fprintf()による stderr への出力を心がけたほうが,妙なハマり方をすることは少なくなるでしょう.
最後に,参考として$ man 3 stdoutの抜粋を掲載します.
注意
stderr ストリームはバッファーリングされていない。
stdout ストリームは、端末に接続されているときには行単位でバッファーリングされている。
一行に満たない内容は、 fflush(3) か exit(3) が呼び出されるか、改行文字が印字されるまで表示されない。
これは、特にデバッグ時において、予期しない結果を生じる原因となるかもしれない。
式 n++ と ++n の意味は同じですか¶
K&R 2.8節 (57ページ) 参照.