cc コマンドや make を実行するウィンドウとは別のウィンドウで top コマンド(top -d 1)を実行し,コンパイル時に実行されるプロセスを観察してみなさい.
Segmentation fault を起こすプログラムをかき,それをデバッガで追跡し,どこで起きたかを調べなさい. プログラムは cc -g でコンパイルすること.
make コマンドのところで示した Makefile と,適当な C プログラムで,make コマンドを試してみなさい.
file-1.o, file-2.o や a.out ファイルを消してみたり,touch file-1.c などとして,make によりどのようにコンパイラが起動されるかを観察しなさい.
Makefile はコンパイルだけに使用する必要はない. ルールにのっとって,処理をするためのコマンドを実行するだけである. そこで,Makefile を用いてよく行われるのが,コンパイルによって作られたファイルの消去である.
Makefile にルールを追加し,以下のようにオブジェクトファイルが消去されるようにしなさい.
% make clean/bin/rm -f a.out file-1.o file-2.o %
make は便利に使えるように,予め様々な暗黙のルールを持っており,処理の全てを事細かに Makefile に記述しなくても処理を行えるようになっている. a.out が file-1.o と file-2.o から作られる依存関係が定義されており,カレントディレクトリに file-1.c,file-2.c がある場合,make は .c というサフィックスから cc -c コマンドを用いて .o ファイルを作成するというルールを持っている. つまり,file-1.o は file-1.c に依存し,cc -c コマンドで作成するというルールを,いちいち Makefile に書く必要はなくなる. また変数を定義する機能もあり,複数のファイル名を値として持つ変数を定義することで,いちいちファイル名を列挙する必要が無くなる. 例えば,
OBJS = file-1.o file-2.o
と書くと,シェルで変数値を参照するときのように,$(OBJS) は file-1.o file-2.o を意味するようになる.
変数と暗黙のルールを用いて,練習問題(4)で作成した Makefile をできるだけ短く書き換えなさい.
参考: 暗黙のルールは make -p を使うと表示できる. 実際に処理コマンドの実行が必要でない場合が多いので,make -n -p とするとよい.
文字列のところにあるプログラムを変更し,文字列定数の内容を変更すると,セグメンテーションフォルトが発生することを確かめよ.
また,cc のオプションに -fwritable-strings を与えると文字列定数の内容を変更が可能になることも確かめよ.
標準入出力を用いるライブラリ関数のところにある fgets,puts を用いたプログラムの LINE_LEN を 5 にして,コンパイル実行すると以下のような結果になってしまう. どうしてこのような結果になってしまうのか,その理由を調べよ.
% ./a.out1234567890
1234 5678 90 abcdefg
abcd efg
%
文字列の中に含まれる単語の数を数える関数 wc を作成し,main 関数から wc 関数をいくつかの文字列を引数に呼び出し,wc 関数が正しく動くことを確かめなさい. 単語はスペースで区切られているものとするが,区切り文字を任意に引数として与えることができるようにしても良い.
文字,文字列の検索のところにあるプログラムは,先頭に / がある場合や末尾が / で終わる場合をうまく扱えず,実行結果は次のようになってしまう.
% ./a.out/dir1/dir2/dir3/file
0: 1: dir1 2: dir2 3: dir3 4: file dir0/dir2/dir3/
0: dir0 1: dir2 2: dir3 3:
%
先頭の / はパスの構成要素と認識され上の最初の例では 0: のところに / が出力されるように,また末尾の / は無視され上の次の例では 3: が出力されないように,変更せよ.
strlcpy, strdup と同じ動作をする関数 my_strlcpy, my_strdup を作成せよ. main 関数から strlcpy, my_strlcpy および strdup, my_strdup のそれぞれに同じ引数をいくつかのパターンで与えて,同じように動作していることを確かめなさい.
strdupにはメモリ領域確保のためにmallocが必要である.
strcmp, strcasecmp と同じ動作をする関数 my_strcmp, my_strcasecmp を作成せよ. main 関数から strcmp, my_strcmp および strcasecmp, my_strcasecmp のそれぞれに同じ引数をいくつかのパターンで与えて,同じように動作していることを確かめなさい.
以下のように,シェルのように1行入力を受け取り,コマンド名と入力のリダイレクション記号「<」があればその後のファイル名を表示し,そうでなければ入力として console と出力するプログラムを作りなさい. 入力行は,コマンド名だけ,または「コマンド名 < ファイル名」(< の前後にスペース1つ)という形式だけに対応すればよい.
% ./a.outcommand
command name: command input: console command < file
command name: command input: file %
練習問題(12)のプログラムを変更し,出力のリダイレクション記号「>」にも対応できるようにせよ. リダイレクション記号の出現順序は入力「<」の後に出力「>」が現れる場合にのみ対応すればよい.
練習問題(13)のプログラムを変更し,
コマンドライン (argv) や標準入力などの処理の結果バッファオーバーフローを起こしてしまうプログラムを書き,そのプログラムを実行するプロセスに適当なデータを与えることで,シェルを起動させてみよ.
プログラムが,バッファオーバーフローを起こすためのヒントとなる情報を出力し,その情報をもとに入力を与えるようにしても良い. また,使用するマシンは orchid-calc1 〜 orchid-calc6 (Linux x86) でも構わない.
(1) ライブラリを用いてファイルコピーを行うプログラム,(2) これを fgets, fputs を用いるように変更したもの,(3) fread, fwrite を用いるように変更したもの,(4) システムコールを用いてファイルコピーを行うプログラムのそれぞれで,コピー元のファイルをサイズが大きなものに変更し,バッファサイズをいろいろ(例えば数バイトの小さいものから64MB程度の大きなものまで)変えて実行時間がどのように変化するか実験せよ.
それぞれの結果はどのようになっただろうか? 違いについて比較し考察せよ.
プログラムの実行時間は time コマンドを用いて計測することができる. 以下の実行例では,ユーザ空間での実行に 1.05 秒,カーネルでの実行に 5.87秒,待ち時間も入れて合計で 6.96 秒かかったという結果である. 計測には,同じパラメータで何度か実行,計測し,その平均値をとるなどすると,より信頼性の高い値を得ることができる.
% time ./a.out1.050u 5.870s 0:06.96 99.4% 0+0k 0+0io 77pf+0w %
実験後は,コピーした大きなファイルは消去すること!!
引数として時刻を指定し,その時間になったら beep 音を鳴らして終了するプログラムを作りなさい.
% ./a.outUsage: ./a.out hh mm % ./a.out 13 00
Sleep 2 minute until 13:00. ≪13:00になったら beep 音をならして終了≫ %
現在のログインユーザを表示するプログラムを変更し,who -l と全く同じように表示するようにせよ.
ログイン記録を表示する last コマンドは新しい記録から表示するが,wtmpの内容を表示するプログラムは古い記録から表示する. last のように新しい記録から表示するように変更せよ(lastのように何時から何時までログインしていたかを表示するようにする必要はない).
wtmpの内容を表示するプログラムを変更し,last と全く同じように表示するようにせよ.
lseek または fseek を用いて,ファイルの末尾を引数で指定された行数だけ表示する tail コマンドに似たプログラムを作りなさい. 表示する順序は,ファイルに記述されている順序を守ること.
mmap を用いて,ファイルの末尾を引数で指定された行数だけ表示する tail コマンドに似たプログラムを作りなさい. 表示する順序は,ファイルに記述されている順序を守ること.
通常の ls の結果のように '.' から始まる名前は表示しないようにしなさい. また,ディレクトリの場合は( ls -F のように)末尾に '/' を付けて,シンボリックリンクの場合は末尾に '@' を付けて表示するようにしなさい.
あるディレクトリエントリのファイルの種類を調べるには lstat システムコールを使用する.
int lstat(const char *file_name, struct stat *buf);
struct stat 構造体の st_mode メンバにファイルの種類も属性として書かれており,S_ISDIR や S_ISLNK マクロによりディレクトリか,またはシンボリックリンクかなどが判別できる.
練習問題23のプログラムを拡張し、さらに ls -R のように,ディレクトリの木構造を再帰的に探索し表示するプログラムを作りなさい.
'.', '..' やシンボリックリンクは,探索しないようにすること.
printenv コマンドは,現在のプロセスの環境変数を表示してくれる. 同様の表示をしてくれるコマンドを,Cプログラムとして記述せよ.
メモリマップを確かめるプログラムをコンパイルし,繰り返し実行してみたところ,以下のようにスタックのアドレスが毎回若干変化していることがわかった. これを確かめ,なぜスタックの場所が変わるか,その理由について考えてみなさい.
% ./a.outenviron: 0xbfffe8dc argv: 0xbfffe8d4 stack: 0xbfffe867 bss: 0x080496e8 data: 0x080495f0 % ./a.out
environ: 0xbfffe6dc argv: 0xbfffe6d4 stack: 0xbfffe667 bss: 0x080496e8 data: 0x080495f0 % ./a.out
environ: 0xbfffe7dc argv: 0xbfffe7d4 stack: 0xbfffe767 bss: 0x080496e8 data: 0x080495f0 %
以下のプログラムをコンパイル,実行すると hello が8回表示される. その理由について考えよ.
1 #include <stdio.h> 2 3 main() 4 { 5 fork(); 6 fork(); 7 fork(); 8 printf("hello\n"); 9 }
system(3) は,シェルによるコマンドの実行を行ってくれるライブラリ関数である. 以下のように,シェル(/bin/sh)のコマンドラインを文字列で渡すと実行してくれる.
1 #include2 #include 3 4 main() 5 { 6 system("ls *.c | wc"); 7 }
fork, execve(又は相当のライブラリ関数),wait などを使用して,system(3)相当の機能を持つ関数 mysystem を作りなさい.
マニュアルページに記述してあるシグナルについては,無視してよい.
標準入力をファイルからリダイレクションするプログラムをもとに,標準出力もファイルに対しての書き込みとなるように変更せよ. 書き込むファイルは,プログラムの第2引数としてとるものとする.
第1引数で与えられるコマンドの実行結果(exit status)に応じて,第2引数又は第3引数で与えられるコマンドを実行するプログラム if-then-else を作りなさい.例えば
% ./if-then-else "test 1 -eq 1" "echo yes" "echo no"yes % ./if-then-else "test 1 -eq 2" "echo yes" "echo no"
no %
となるようなものをである.
コマンドの区切りは別のものでも良い. 以下は then, else が区切りになっている. execve を使用する場合は,argv の then, else の部分を NULL にすると execve にそのまま渡せるので,以下のような区切りのほうが,プログラムは作りやすいかもしれない.
% ./if-then-else test 1 -eq 1 then echo yes else echo no
最初に fork で子プロセスを作り,親プロセスと子プロセスがパイプを通してデータをやりとりすることで,交互に実行を繰り返すプログラムを作りなさい.
片方のプロセスに実行が移った時に1文字出力した後に,もう一方に実行を移すようにしなさい(下の実行例を参照).
また,合計何文字出力するかはプログラムへの引数で指定できるようにしなさい.
但し、親プロセス終了までに fork で作る子プロセスは1つのみとする。
(forkされた子プロセスが1文字出力後にすぐにexitするようなプログラムは正解とは認めない。)
また、シグナルは使用しないこと。
以下は,子プロセスは数字を親プロセスはアルファベットの大文字を出力するようにしてみたプログラムでの実行例である.
% ./a.out 300A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6A7B8C9D %
子プロセスが1文字(例えば0)を出力した後,実行を親プロセスに戻し,親プロセスが1文字(A)を出力の後,また実行を子プロセスに移し,子プロセスが1文字(1)を出力し…というように,親と子プロセスの実行が交互に繰り返されている.
3個のプロセスの標準入出力を2つのパイプで結びなさい.
パイプの作り方は,先にパイプを2つ作ってから2回 fork する方法と,1つパイプを作り fork し,もう1つパイプを作りまた fork する方法があるが,どちらでも良い.
popen(3), pclose(3)は,パイプにより接続された指定されたプログラムを実行するプロセスを生成し,そのプロセスに対する入力又は出力のどちらか一方を提供するライブラリ関数である.
popen(3), pclose(3)相当の機能を持つ関数 mypopen, mypclose を作りなさい. 最初は,popenで実行するプログラムからの出力を受け取る機能だけを実装するところから始めるとよい.
パイプを使用するプログラムを,以下のように,親プロセスからの出力を子プロセスの入力にするように書き換えたところ,終了しなくなってしまった. その理由を考え,プログラムの修正せよ.
1 #include <stdio.h> 2 3 int pipe_fd[2]; 4 5 void 6 do_parent() 7 { 8 char *p = "Hello, my kid."; 9 int status; 10 11 printf("this is parent.\n"); 12 13 close(pipe_fd[0]); 14 15 while (*p) { 16 if (write(pipe_fd[1], p, 1) < 0) { 17 perror("write"); 18 exit(1); 19 } 20 p++; 21 } 22 23 if (wait(&status) < 0) { 24 perror("wait"); 25 exit(1); 26 } 27 } 28 29 void 30 do_child() 31 { 32 char c; 33 int count; 34 35 printf("this is child.\n"); 36 37 close(pipe_fd[1]); 38 39 while ((count = read(pipe_fd[0], &c, 1)) > 0) { 40 putchar(c); 41 } 42 putchar('\n'); 43 44 if (count < 0) { 45 perror("read"); 46 exit(1); 47 } 48 } 49 50 main() 51 { 52 int child; 53 54 if (pipe(pipe_fd) < 0) { 55 perror("pipe"); 56 exit(1); 57 } 58 59 if ((child = fork()) < 0) { 60 perror("fork"); 61 exit(1); 62 } 63 64 if (child) 65 do_parent(); 66 else 67 do_child(); 68 }
kill システムコールでシグナルを送るプログラムとシグナルを受け取るプログラムを使用して,プロセスからプロセスへシグナルが送られるのを確かめなさい.
ヒント:端末ウィンドウを2つ開き,1つでシグナルを受け取るプログラムを動かし,もう1つでシグナルを送るプログラムを動かす. シグナルを送る相手のプロセスのIDは ps コマンドで調べるか,プログラムを変更して自分のプロセスIDを表示させるようにする.
複数の異なるシグナルに対しシグナルハンドラを設定し,シグナルに応じたシグナルハンドラが起動されることを確かめなさい.
不正なメモリアクセスは SIGSEGV により通知される. どのアドレスに対してアクセスがあったのかを表示するプログラムを作成しなさい.
ヒント:SIGACTION(2) の siginfo_t の情報を参照.
sleep は実はライブラリ関数である. sleep 相当の機能を持つ関数 mysleep を作りなさい.
ヒント:setitimer, sigaction, pause を使用する.
キーボードから1文字入力を読み込む関数 getchar にタイムアウト機能を追加した関数 mygetchar を作りなさい. mygetchar は,ある一定時間内にキー入力があればそれを返し,なければ -2 を返すような関数であるとする. (-2 なのは EOF が -1 であるため). タイムアウト時間は予め決められた時間(例えば10秒)でもよいし,引数で指定できるようにしても良い.
ヒント:SA_RESTART はどういう意味か?
練習問題(31)と同じように,親プロセスと子プロセスが交互に実行することにより,交互に1文字出力するプログラムを,シグナルを用いて書きなさい. fork で作る子プロセスは1つだけとする.