システムプログラム(第4週)

電子・情報工学系
追川 修一
<shui @ cs.tsukuba.ac.jp>

このページは,次の URL にあります.
http://www.coins.tsukuba.ac.jp/~syspro/2005/No4.html
システムプログラムのホームページ(2005年度)
http://www.coins.tsukuba.ac.jp/~syspro/2005/
からもリンクが張ってあります.

今日の内容

時刻の扱い

時刻の表現

UNIXにおける時刻表現の基本は,協定世界時 (UTC) 1970年1月1日 00:00:00 からの秒数で表すカレンダー時刻である. カレンダー時刻を表すために使われる型は time_t である. システムコールやライブラリ関数の引数や戻り値に time_t が出てきたら,このカレンダー時刻意味するものと思って良い.

time_t の実体は32ビットの整数(long型)である.

% egrep time_t /usr/include/time.h | egrep typedef [←]
typedef __time_t time_t;
% egrep __time_t /usr/include/bits/types.h [←]
typedef long int __time_t;
%

time_t の他にも,struct tm, struct timeval, struct timespec, clock_t など,時間や時刻を表す構造体や型が必要に応じていろいろ定義されており,時間や時刻の取り扱いはやや複雑である.

時刻の取得

時刻はカーネルが管理しており,現在時刻を所得するシステムコールとして time や gettimeofday がある.

time_t time(time_t *t);
int    gettimeofday(struct timeval *tv, struct timezone *tz);

gettimeofday は,秒数だけでなくマイクロ秒まで表すことができるようにした struct timeval に現在時刻を返してくれる(それだけの精度があるのは希である). struct timeval は以下のように定義されている.

struct timeval {
        long tv_sec;   /* seconds */
        long tv_usec;  /* microseconds */
};

time システムコールの返す値と,gettimeofday により struct timeval の tv_sec メンバにセットされる値は同じである. 以下はそれを確かめるプログラムである.

     1  #include <stdio.h>
     2  #include <time.h>
     3  #include <unistd.h>
     4  #include <sys/time.h>
     5  
     6  main()
     7  {
     8          time_t  t;
     9          struct timeval  tv;
    10  
    11          time(&t);
    12          gettimeofday(&tv, NULL);
    13  
    14          printf("        time: %d\n", t);
    15          printf("gettimeofday: %d\n", tv.tv_sec);
    16  }

コンパイル,実行してみると,確かに同じ値が返されていることがわかる.

% ./a.out [←]
        time: 1114868334
gettimeofday: 1114868334
%

時刻を扱うライブラリ関数

1970年からの秒数はコンピュータには便利でも人間が扱うには不便であるため,人間に分かりやすい形式に変換してくれるライブラリ関数が用意されている.

char *asctime(const struct tm *timeptr);
char *ctime(const time_t *timep);
struct tm *gmtime(const time_t *timep);
struct tm *localtime(const time_t *timep);
time_t mktime(struct tm *timeptr);

ctime, gmtime, localtime は time_t (つまり1970年からの秒数)を引数に取り,ctime は文字列,gmtime はUTC,localtimeはローカル時間の struct tm に変換する. asctime と mktime は struct tm へのポインタを引数に取り,それぞれ文字列,time_t に変換する. mktime 以外は,変換された文字列や struct tm へのポインタを返す. これらのポインタの先のデータ領域は1つしかないため,次の呼び出しで上書きされてしまうことに注意. asctime と ctime,gmtime と localtime は出力先のデータ領域を共有していることもある.

struct tm は以下のように定義されている.

struct tm {
        int     tm_sec;                 /* 秒 */
        int     tm_min;                 /* 分 */
        int     tm_hour;                /* 時間 */
        int     tm_mday;                /* 日 */
        int     tm_mon;                 /* 月 */
        int     tm_year;                /* 年 */
        int     tm_wday;                /* 曜日 */
        int     tm_yday;                /* 年内通算日 */
        int     tm_isdst;               /* 夏時間 */
};

以下のプログラムは time により現在のカレンダー時刻を取得し,それを

している. ctime は末尾に改行が付いた文字列を返すことに注意. 16行目の tzname はタイムゾーンを表す文字列が入っている大域変数であり,localtime を呼び出すことでその値がセットされる.

     1  #include <stdio.h>
     2  #include <time.h>
     3  
     4  main()
     5  {
     6          time_t  t;
     7          char    *ts;
     8          struct tm       *tmp;
     9  
    10          time(&t);
    11          printf("%s", ctime(&t));
    12  
    13          tmp = localtime(&t);
    14          printf("%d/%02d/%02d %02d:%02d:%02d (%s)\n",
    15              1900 + tmp->tm_year, tmp->tm_mon, tmp->tm_mday,
    16              tmp->tm_hour, tmp->tm_min, tmp->tm_sec, tzname[0]);
    17  
    18          tmp = gmtime(&t);
    19          printf("%d/%02d/%02d %02d:%02d:%02d (UTC)\n",
    20              1900 + tmp->tm_year, tmp->tm_mon, tmp->tm_mday,
    21              tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
    22  }

コンパイル実行すると,以下のようになる.

% ./a.out [←]
Sun May  1 09:53:41 2005
2005/04/01 09:53:41 (JST)
2005/04/01 00:53:41 (UTC)
%

その他の時間関係のシステムコール,ライブラリ関数

sleep, nanosleep は,指定された時間だけプロセスの実行を休止する. sleep は秒単位,nanosleep はナノ秒単位で指定できる. nanosleep は通常それだけの精度があるわけではない.

unsigned int sleep(unsigned int seconds);
int nanosleep(const struct timespec *req, struct timespec *rem);

clock, times はプロセスの実行時間を返す.

clock_t clock(void);
clock_t times(struct tms *buf);

より高度な入出力

構造体の入出力

まとめて扱うと便利なデータは,Cプログラムでは構造体を用いて表現する. 例えば住所録を作る場合,名前,住所,電話番号,メイルアドレスなどを構造体としてまとめて扱うと,ソートや検索など様々な処理がしやすくなる.

sizeof

そのような構造体のデータをファイルに保存,または読み出すために,まず知らなくてはならないことは,構造体の大きさである. 住所録のそれぞれのエントリのために,以下のような構造体を定義したとする.

     1  struct entry {
     2          char    name_family[32];
     3          char    name_first[32];
     4          char    addr[128];
     5          int     zip1;
     6          int     zip2;
     7          char    mail[128];
     8  };

この構造体の大きさを知るためには sizeof 演算子を用いる. struct entry の定義は addr.h に書かれているとすると,以下のプログラムはそのバイト数を表示する.

     1  #include <stdio.h>
     2  #include "addr.h"
     3
     4  main()
     5  {
     6          printf("sizeof(struct addr) = %d\n", sizeof(struct entry));
     7  }

コンパイル実行すると,以下のようになる.

% ./a.out [←]
sizeof(struct addr) = 328
%

struct entry は,名前に32バイト配列が2つ,住所とメイルに128バイトの配列が2つ,郵便番号に4バイトの int が2つ使用しているため,合計328となり,上記の実行結果と一致する.

入出力

ファイルへの入出力には何も特別なことはなく,システムコールの read, write 又はライブラリ関数の fread, fwrite を使用する.

n個のエントリ用のメモリ領域を確保して,そこに読み込むというような場合,read システムコールだと以下のようになる(簡単のためエラー処理は省略してある). malloc で stuct entry 構造体 n 個分の領域を確保するため,領域のサイズとして sizeof(struct entry) に n を乗じた値を引数に指定している. malloc で確保した領域(data が指し示している領域)にデータを読み込むため,read への引数としても同じサイズ (sizeof(struct entry) * n) を指定している.

int	fd;
int	n;
struct entry	*data;

...

data = malloc(sizeof(struct entry) * n);
read(fd, data, sizeof(struct entry) * n);

以下は fread で読み込むようにした場合である(簡単のためエラー処理は省略してある). fread の場合,読み込みたい要素のサイズとその数を別々に指定するようになっているため,sizeof(struct entry) と n が別々の引数になる.

FILE	*fp;
int	n;
struct entry	*data;

...

data = malloc(sizeof(struct entry) * n);
fread(data,  sizeof(struct entry), n, fp);

fread, fwrite は,構造体データの入出力が意識されたAPI設計がされており,入出力したいデータのサイズとその数を指定するようになっている.

プログラム例(utmpデータの読み込み)

/var/run/utmp というファイルには現在ログインしているユーザが記録されている. 以下のプログラムは fread を利用し1エントリごとデータを読み込み,ログインユーザを表示するプログラムである. struct utmp の定義は /usr/include/utmp.h (実際の定義は /usr/include/bits/utmp.h)から読み込まれる. 10行目の UTMP_FILE は /var/run/utmp を表す. マクロになっているのはUNIXのバージョンによりパスが異なっていることがあるためであり,マクロにすることによりプログラムの移植性を高めることができる. 18行目で struct utmp の ut_type メンバの値が USER_PROCESS の時だけ,読み込んだエントリの情報を出力している. UTMP_FILE にはログインユーザだけでなく管理情報も含まれる. ログインユーザ情報だけを出力するためには ut_type メンバの値が USER_PROCESS の時だけ出力する.

     1  #include <stdio.h>
     2  #include <utmp.h>
     3  #include <time.h>
     4
     5  main()
     6  {
     7          FILE    *fp ;
     8          struct utmp     u;
     9
    10          fp = fopen(UTMP_FILE, "r");
    11
    12          if(fp == NULL) {
    13              perror(UTMP_FILE);
    14              exit(-1);
    15          }
    16
    17          while(fread(&u, sizeof(u), 1, fp) == 1) {
    18                  if (u.ut_type == USER_PROCESS)
    19                          printf("%-8s %-8s (%s) %s", u.ut_user,
    20                              u.ut_line, u.ut_host, ctime(&u.ut_time));
    21          }
    22
    23          fclose(fp);
    24  }

上記プログラムをコンパイル実行すると以下のようになる(結果は各コンピュータごとに異なる). who の実行結果と同じ情報が表示されている.

% ./a.out [←]
shui     pts/1    (real.cs.tsukuba.ac.jp) Fri Apr 29 17:24:06 2005
shui     pts/0    (real.cs.tsukuba.ac.jp) Fri Apr 29 17:17:30 2005
% who -l [←]
shui     pts/1    Apr 29 17:24 (real.cs.tsukuba.ac.jp)
shui     pts/0    Apr 29 17:17 (real.cs.tsukuba.ac.jp)
%

データファイルのポータビリティ

構造体のデータを書き込み作成したデータを様々なコンピュータで共有しようとする時に注意しなければいけないのは,データがどのようにファイルに書かれているのかという点である. 即ち,数値データがどのようにメモリ上で表現されているのかについて知る必要がある. 例えば,以下のような点について気をつける必要がある.

ある意味,最もポータビリティが高いのは1バイトごとにアクセスできるテキストファイルである. 例えば,様々なデータを扱えるように設計されたXMLはテキスト形式である.

ファイルのランダムアクセス

全てをメモリ上に読み込むことが難しい大きなデータを扱う場合,全データはファイルに格納し,必要なデータのみを読み書きすることになる. その場合,データをファイルの先頭から順番に読み込んでいくシーケンシャルアクセスではなく,途中を読み飛ばす(読み書きする場所を移動する)ランダムアクセスができると効率が良い.

ランダムアクセスをするためには,システムコールの read, write を用いている場合には lseek,ライブラリ関数の fread, fwrite を用いている場合には fseek を用いる. これらは,ファイルを読み書きする位置(先頭からのバイト数)を移動する. この位置のことを,オフセット,シークポインタ,ファイルポインタなどと呼ぶ(ファイルポインタと呼ぶ場合は FILE* と紛らわしいので注意).

off_t lseek(int fildes, off_t offset, int whence);
int   fseek(FILE *stream, long offset, int whence);

lseek, fseek 共に指定するパラメータは同じで,第3引数の whence で指定される位置に第2引数の offset バイト数を加えることによって得られた位置に移動する. つまり,offset に負の値を指定すると,whence で指定された位置から前に移動する. whence には以下のマクロのどれかを指定する.

SEEK_SET ファイルの先頭から offset バイト目に移動.
SEEK_CUR 現在の位置から offset バイト目に移動.
SEEK_END ファイルの末尾から offset バイト目に移動.

SEEK_END によりファイル末尾から先頭方向に向かって offset バイト目に移動したい場合(通常そうであると思われるが)は,offset には負の値を指定しなければいけないことに注意.

プログラム例(wtmpデータの読み込み)

/var/log/wtmp というファイルにはこれまで,そのコンピュータにログインしたユーザの記録が残っている. 新たにユーザがログインすると,ファイル末尾に書き足される. 古い記録がファイルの先頭の方にあり,新しい記録は末尾の方にある. 従って,最近ログインした5ユーザを表示したい,というような場合は,ランダムアクセスをすると,効率よくデータにアクセスすることができる.

以下は,最近ログインしたユーザをプログラムの引数で指定された数だけ表示するプログラムである.

     1  #include <stdio.h>
     2  #include <utmp.h>
     3  #include <time.h>
     4  
     5  main(int argc, char *argv[])
     6  {
     7          int     num;
     8          FILE    *fp ;
     9          struct utmp     u;
    10  
    11          if (argc != 2) {
    12                  printf("Usage: %s num\n", argv[0]);
    13                  exit(1);
    14          }
    15  
    16          num = atoi(argv[1]);
    17          if (num <= 0) {
    18                  printf("The argument needs to be a positive number.\n");
    19                  exit(1);
    20          }
    21  
    22          fp = fopen(WTMP_FILE, "r");
    23          if (fp == NULL) {
    24                  perror(WTMP_FILE);
    25                  exit(-1);
    26          }
    27  
    28          if (fseek(fp, -(sizeof(u) * num), SEEK_END) != 0) {
    29                  perror("fseek");
    30                  exit(1);
    31          }
    32  
    33          while (fread(&u, sizeof(u), 1, fp) == 1) {
    34                  printf("%8s|%36s|%6s|%s", u.ut_user, u.ut_host,
    35                      u.ut_line, ctime(&u.ut_time));
    36          }
    37  
    38          fclose(fp);
    39  }

上記プログラムをコンパイル実行すると以下のようになる(結果は各コンピュータごとに異なる).

% ./a.out 5 [←]
    shui|               real.cs.tsukuba.ac.jp| pts/2|Fri May  6 15:20:28 2005
        |                                    | pts/2|Fri May  6 15:20:44 2005
    shui|               real.cs.tsukuba.ac.jp| pts/2|Fri May  6 15:22:36 2005
        |                                    | pts/2|Fri May  6 15:27:14 2005
    shui|               real.cs.tsukuba.ac.jp| pts/2|Fri May  6 15:27:18 2005
% last -3 shui [←]
shui     pts/2        real.cs.tsukuba. Fri May  6 15:27   still logged in
shui     pts/2        real.cs.tsukuba. Fri May  6 15:22 - 15:27  (00:04)
shui     pts/2        real.cs.tsukuba. Fri May  6 15:20 - 15:20  (00:00)

wtmp begins Sun May  1 10:53:45 2005
%

last コマンドの出力と比較してみると,WTMP_FILE にどのようにログイン,ログアウトの情報が書き込まれるかわかる. ログイン情報は user 名,ホスト名,ログイン tty と共に,時刻が書き込まれている. ログアウトの場合は,ログイン tty と時刻しか書き込まれていない. UTMP_FILEの情報を表示するプログラムに出てきた struct utmp の ut_type メンバの値を調べてみると,ログイン情報は ut_type の値が 7 (USER_PROCESS),ログアウト情報は 8 (DEAD_PROCESS) となっていることがわかる(上記のプログラムでは表示させていない). ログイン,ログアウトの両方の情報を使うことで,last のように何時から何時までログインしていたかを表示させることができる.

ファイルのメモリマッピング

ファイルの内容にアクセスする方法で,これまで見てきた read, write を用いる方法とは全く違った方法として,ファイルをアドレス空間にマップするメモリマッピング又はメモリマップドファイル (memory mapped file) と呼ばれる方法がある. ファイルをアドレス空間にマップすることで,ファイルの内容は配列のように扱うことができ,配列の添字やポインタを用いてアクセスできるようになる.

メモリマッピングを行うためには,mmap システムコールを使用する.

void  *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

mmap はファイルディスクリプタ (fd) で指定されたファイルをアドレス空間にマップする. 引数として,ファイルのどの位置 (offset) からどれだけの長さ (length) をマップするか指定する. また,マップされた領域のメモリ保護 (prot) とタイプ (flags) も指定する.

WTMP_FILE をアクセスするプログラムを,メモリマッピングを用いるように変更すると以下のようになる. 変更点は以下の通りである.

     1  #include <stdio.h>
     2  #include <utmp.h>
     3  #include <time.h>
     4  #include <fcntl.h>
     5  #include <sys/types.h>
     6  #include <sys/stat.h>
     7  #include <sys/mman.h>
     8  
     9  main(int argc, char *argv[])
    10  {
    11          int     fd, num;
    12          struct stat     fs;
    13          struct utmp     *u;
    14  
    15          if (argc != 2) {
    16                  printf("Usage: %s num\n", argv[0]);
    17                  exit(1);
    18          }
    19  
    20          num = atoi(argv[1]);
    21          if (num <= 0) {
    22                  printf("The argument needs to be a positive number.\n");
    23                  exit(1);
    24          }
    25  
    26          fd = open(WTMP_FILE, O_RDONLY);
    27          if (fd < 0) {
    28                  perror(WTMP_FILE);
    29                  exit(-1);
    30          }
    31  
    32          if (fstat(fd, &fs) < 0) {
    33                  perror("fstat");
    34                  exit(-1);
    35          }
    36  
    37          u = mmap(NULL, fs.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    38          if (u == MAP_FAILED) {
    39                  perror("mmap");
    40                  exit(-1);
    41          }
    42  
    43          u += (fs.st_size / sizeof(struct utmp) - num);
    44  
    45          while (num--) {
    46                  printf("%8s|%36s|%6s|%s", u->ut_user, u->ut_host,
    47                      u->ut_line, ctime(&u->ut_time));
    48                  u++;
    49          }
    50  
    51          close(fd);
    52  }

上記プログラムをコンパイル実行すると以下のようになる(結果は各コンピュータごとに異なる). fread で読んだ情報と同じ情報が表示されている.

% ./a.out 5 [←]
    shui|               real.cs.tsukuba.ac.jp| pts/2|Fri May  6 15:20:28 2005
        |                                    | pts/2|Fri May  6 15:20:44 2005
    shui|               real.cs.tsukuba.ac.jp| pts/2|Fri May  6 15:22:36 2005
        |                                    | pts/2|Fri May  6 15:27:14 2005
    shui|               real.cs.tsukuba.ac.jp| pts/2|Fri May  6 15:27:18 2005
%

ディレクトリ

ディレクトリの内容を読み込むにはライブラリ関数を用いる方が遙かに簡単であり,また移植性も高い. まずライブラリを用いた方法について述べ,その後でシステムコールによる方法について触れる.

ライブラリでのディレクトリ内容の読み込み

ディレクトリの内容を読むには,以下のライブラリ関数を使用する.

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dir);
int closedir(DIR *dir);

これらのAPIはファイルストリーム(FILE*)を用いる fopen, fread, fclose とよく似ている. opendir はディレクトリストリーム(DIR*)を返す. そのディレクトリストリームを指定し,readdir はディレクトリの内容を順番に読んでいく. closedir はディレクトリストリームを閉じる.

readdir は struct dirent へのポインタを返す. このポインタの先,つまり struct dirent 構造体のデータ領域はディレクトリエントリごとに確保されるのではなく,1つしかない. 従って,この内容は次の readdir 呼び出しで上書きされてしまう.

以下は,上記APIを用いてプログラムの引数に指定されたディレクトリ内容を読み表示するプログラムである.

     1  #include <stdio.h>
     2  #include <dirent.h>
     3  
     4  main(int argc, char *argv[])
     5  {
     6          struct dirent   *dp;
     7          DIR     *dir;
     8          char    *path;
     9  
    10          if (argc == 1)
    11                  path = ".";
    12          else if (argc == 2)
    13                  path = argv[1];
    14          else {
    15                  printf("Usage: %s dir\n", argv[0]);
    16                  exit(1);
    17          }
    18  
    19          dir = opendir(path);
    20          if(dir == NULL) {
    21                  perror(path);
    22                  exit(1);
    23          }
    24  
    25          while (dp = readdir(dir))
    26                  printf("%s\n",dp->d_name);
    27  
    28          closedir(dir);
    29  }

上記プログラムをコンパイル実行すると以下のようになる.

% ./a.out / [←]
.
..
lost+found
boot
home
root
usr
var
tmp
.... 以下省略 ....
% ./a.out [←]
.
..
Makefile
a.c
a.out
a.txt
.... 以下省略 ....
%

ディレクトリに書かれている順番に読み出し表示するので,ls と異なり結果はソートされていない(出力としては ls -1f の結果と同じになる).

システムコールでのディレクトリ内容の読み込み

ディレクトリの内容を読むシステムコールとして getdents がある.

int getdents(unsigned int fd, struct dirent *dirp, unsigned int count);

getdents を使用するためには,まずディレクトリを open し,ファイルディスクリプタを取得する. ディレクトリのファイルディスクリプタと,データを読み込む領域,その領域の大きさを指定して getdents を呼び出すと,指定したデータ領域に入るだけのいくつかのディレクトリエントリが読み込まれる(1つだけではないことに注意).

getdents は,実際にディスク上に格納されているディレクトリの内容に近いデータを読み出す. ディレクトリエントリに含まれるファイル名は短いものもあれば長いものもあるため,ディスク上のディレクトリエントリは可変長のデータである. 従って,getdents が読み出してきた複数のディレクトリエントリを正しく処理するためには,各エントリの長さを調べながら,次のエントリに進む必要がある.

以下はライブラリを用いてディレクトリエントリ表示するプログラムを getdents を用いるように変更したものである. 変更点は以下の通りである.

     1  #include <stdio.h>
     2  #include <stdlib.h>
     3  #include <errno.h>
     4  #include <fcntl.h>
     5  #include <unistd.h>
     6  #include <linux/types.h>
     7  #include <linux/dirent.h>
     8  #include <linux/unistd.h>
     9  
    10  _syscall3(int, getdents, uint, fd, struct dirent *, dirp, uint, count);
    11  
    12  main(int argc, char *argv[])
    13  {
    14          struct dirent   *dp;
    15          void    *buf;
    16          char    *path;
    17          int     fd, rcount;
    18  
    19          if (argc == 1)
    20                  path = ".";
    21          else if (argc == 2)
    22                  path = argv[1];
    23          else {
    24                  printf("Usage: %s dir\n", argv[0]);
    25                  exit(1);
    26          }
    27  
    28          fd = open(path, O_RDONLY);
    29          if(fd < 0) {
    30                  perror(path);
    31                  exit(1);
    32          }
    33  
    34          buf = malloc(BUFSIZ);
    35          if (buf == NULL) {
    36                  perror("malloc");
    37                  exit(1);
    38          }
    39  
    40          while ((rcount = getdents(fd, buf, BUFSIZ)) > 0) {
    41                  for (dp = buf;
    42                       (char *)dp < (char *)buf + rcount;
    43                       dp = (struct dirent *)((char *)dp + dp->d_reclen))
    44                          printf("%s\n",dp->d_name);
    45          }
    46  
    47          close(fd);
    48  }

42, 43行目では,キャストを使っているため,少し理解するのが難しくなっているかもしれない. 42行目の for 文の終了条件は,読み込んだデータの最後まで処理したかどうかで判断する. 型が異なるポインタの比較はできないため,また rcount バイト加算することを明確にするために,dp および buf を char * にキャストしている. 43行目は,dp を一度 char * にキャストして d_reclen バイト加算し,再び struct dirent * にキャストしている. ポインタに対する整数値の加算は,その型のバイト数×整数値だけ増えてしまう. char * にキャストすることで,加算する値のバイト数だけ増やすことができる.

実行結果はライブラリを用いたプログラムと同じであるため省略.

バッファオーバーフロー

バッファオーバーフローはプログラムの実行(プロセス)を乗っ取るため,悪意を持った攻撃者によって引き起こされる. バッファオーバーフローを起こすことにより,攻撃者は任意のプログラムを送り込み,それを実行することができてしまい,プロセスは乗っ取られてしまう.

バッファオーバーフローがどうして起こるのか理解するためには,プロセスが実行時に用いられるスタックの構造について理解する必要がある.

下図は,実行中のプロセスのメモリ空間においてどのようにデータが配置されているかを図示したものである. 通常,最も下位アドレスに機械語命令が入ったテキスト領域が置かれる. その上にデータ領域が置かれる. データ領域とは別に,関数呼び出し時の戻りアドレス(リターンアドレス)や関数のローカル変数が格納されるスタック領域がある. データ領域はデータ割り当てが起こるたびに上位アドレスに向かって伸びていく. スタック領域は逆に,関数呼び出しが起こるたびに下位アドレスに向かって伸びていく.

プロセスのアドレス空間

スタック領域をより詳細に図示すると下図のようになる. 関数呼び出しが起こると,リターンアドレスがスタックにプッシュされ,呼び出された関数で使用するローカル変数のための領域が確保される. リターンアドレスとローカル変数のための領域を合わせてスタックフレームと呼ぶ.

バッファオーバーフロー

スタックフレーム中のローカル変数に配列が含まれていると,配列の添字が小さいほうが下位アドレスにあり,大きいほうが上位アドレスにくる. 上の図で buf がポインタとして例えば gets に渡されると,buf[0] から上位アドレスに向かって書き込まれていく. 64文字以上の文字が書き込まれると,buf[63] を超えて書き込まれ,buf として割り当てられた領域を超えて書き込まれてしまうことになる. さらに書き込まれると,リターンアドレスも書き換えられてしまう. スタック領域にうまくプログラムを書き込み,リターンアドレスをそのプログラムの開始アドレスに設定してあげると,関数が戻る時に書き込んだプログラムが実行されてしまう.

このような攻撃方法をスタックスマッシング (stack smashing) と呼ぶ.

練習問題

練習問題(33)

引数として時刻を指定し,その時間になったら beep 音を鳴らして終了するプログラムを作りなさい.

% ./a.out [←]
Usage: ./a.out hh mm
% ./a.out 13 00 [←]
Sleep 2 minute until 13:00.
≪13:00になったら beep 音をならして終了≫
%

練習問題(34)

現在のログインユーザを表示するプログラムを変更し,who -l と全く同じように表示するようにせよ.

練習問題(35)

ログイン記録を表示する last コマンドは新しい記録から表示するが,wtmpの内容を表示するプログラムは古い記録から表示する. last のように新しい記録から表示するように変更せよ(lastのように何時から何時までログインしていたかを表示するようにする必要はない).

練習問題(36)

wtmpの内容を表示するプログラムを変更し,last と全く同じように表示するようにせよ.

練習問題(37)

lseek または fseek を用いて,ファイルの末尾を引数で指定された行数だけ表示する tail コマンドに似たプログラムを作りなさい.

練習問題(38)

mmap を用いて tail コマンドに似たプログラムを作りなさい.

練習問題(39)

通常の ls の結果のように '.' から始まる名前は表示しないようにしなさい. また,ディレクトリの場合は( ls -F のように)末尾に '/' を付けて,シンボリックリンクの場合は末尾に '@' を付けて表示するようにしなさい.

あるディレクトリエントリがディレクトリであるかどうか調べるには stat システムコールを使用する.

int stat(const char *file_name, struct stat *buf);

struct stat 構造体の st_mode メンバにファイルの種類も属性として書かれており,S_ISDIR や S_ISLNK マクロによりディレクトリか,またはシンボリックリンクかなどが判別できる.

練習問題(40)

ls -R のように,ディレクトリの木構造を再帰的に探索し表示するプログラムを作りなさい.

'.', '..' やシンボリックリンクは,探索しないようにすること.

練習問題(41)

バッファオーバーフローを起こしやすいプログラムを書き,そのプログラムを実行するプロセスに必要なデータを読み込ませ,シェルを起動させてみよ.