文字列の扱い、システム・コールとライブラリによるファイルの扱い

システム・プログラム

                                       電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~yas/classes/syspro-2003/2003-04-21
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~yas/
http://www.is.tsukuba.ac.jp/~yas/index-j.html

■印刷配布資料

■復習

■今日の目標

■文字の符号化

文字の符号化とは、文字とビット列(または整数)を対応させることであ る。対応のさせかたには何種類もある。
A	1
B	2
C	3
...
Z	26
モールス符合。

ポケットベル・コード。

コンピュータでよく使われる文字集合と文字の符号化の方法

厳密には、文字集合と文字コードは違う。JIS X 0208-1983符号もEUC-jpも、 Shift-JIS も文字集合は同じなので、相互に変換できる。 Unicode と JIS X 0208-1983 は、完全には相互に変換できない。 Unicode の符号化の方法には、UTF-1, UTF-7, UTF-8 などの種類がある。

数の値と数を画面に文字として表示するために使う符合が違うことに注意する。 数の0と文字の0は違う。

◆ISO Standard ISO8859-1 Latin-1

Latin-1コード表

8ビット符号。 20(16進)から7F(16進)までは、ASCII と同じ。 西ヨーロッパでよく使われている。

参考:

http://www.sandia.gov/sci_compute/iso_symbol.html
ISO Latin 1 Character HTML Entity Names)

◆JIS X 201

JIS X 201 コード表

◆漢字コード

JIS漢字コード表の一部

漢字1文字を、7ビットと7ビットの組でで表す。それぞれ 21〜7E に入るよ うになっている。16進で2121から747Eまでの範囲にある。ただし、半分は空い ている。

参考:

http://www.hlla.is.tsukuba.ac.jp/~yas/classes/ipe/nitiniti2-enshu-1996/1996-11-18/kanji-code.html
漢字コードに関する解説

◆C言語での文字

C言語では、ASCII で表せる文字については、シングルクォート 「''」で囲むことで、その文字コードの定数と同じ意味 になる。
  int x ;
  x = 'A' ;  /* (1) */
  x = 0x41 ; /* (2) */
漢字を「''」で囲んだ時に何が起るかは不明である。 コンパイラはエラーを出さない。

■C言語での文字列

「''」と「""」の違いを理解すること。
  int x ;
  x = 'A' ;  /* (1) */
  x = "A" ;  /* (2) */

char の配列として表現される。連続番地に置かれ、0終端されている。ダブ ルクォート「""」で囲むと、名前がない配列が作られ、その先頭番地が入る。

漢字を「""」で囲んだ時には、EUC だと比較的そのまま通る。 JIS だと、「"」や「\」と重なるバイトを持つ漢字が現れた時に問題が生じる。

◆文字列と文字の配列

C言語で文字列は、0で終端された、0 以外のバイトの並びである。ASCII 文 字 だけ考えれば簡単だが、漢字やその他の言語の文字まで考えると、符号化 の問題があるので、本当の文字列の扱いは難しい。

C言語で "" で括った文字列を指定すると、名前がない charの配列が作られ、 その先頭番地が返される。配列の内容は、指定された文字と、最後に文字列の 終端を意味する 0 が付く。表面的に見える文字の数より 1 バイト多いことに 注意する。

----------------------------------------------------------------------
   1:	/*
   2:	        string-array.c -- 文字列と配列
   3:	        ~yas/syspro/cc/string-array.c
   4:	        Start: 2002/04/21 18:24:34
   5:	*/
   6:	
   7:	char h1[] = { 'h','e','l','l','o',0 };
   8:	
   9:	main()
  10:	{
  11:	    int i ;
  12:	        printf("%s\n",h1 );
  13:	        printf("0x%x, 0x%x\n",h1,"hello" );
  14:	        for( i=0 ; i<5 ; i++ )
  15:	        {
  16:	            printf("[%c]", h1[i] );
  17:	        }
  18:	        printf("\n");
  19:	        for( i=0 ; i<5 ; i++ )
  20:	        {
  21:	            printf("[%c]", "hello"[i] );
  22:	        }
  23:	        printf("\n");
  24:	}
----------------------------------------------------------------------

配列なので、"hello"[i] のようなこともできる。

printf で %s と指定すると、その番地から 0 の直前までの内容を 画面に出力する。

実行結果。


----------------------------------------------------------------------
% cp ~yas/syspro/cc/string-array.c . [←]
% make string-array [←]
cc     string-array.c   -o string-array
% ./string-array  [←]
hello
0x80495b8, 0x804858c
[h][e][l][l][o]
[h][e][l][l][o]
% []
----------------------------------------------------------------------
文字列と文字の配列

◆文字列操作のためのライブラリ

string(3) に一覧がある。よく使うものは、次のようなもの。

#include <string.h>

文字列の長さを調べる(終端の 0 は、長さに含まれない)
    size_t strlen(const char *s);

文字列のコピー
    char *strcpy(char *dest, const char *src);
    char *strncpy(char *dest, const char *src, size_t n);
    char *strdup(const char *s);

文字列の連結
    char *strcat(char *dest, const char *src);
    char *strncat(char *dest, const char *src, size_t n);

文字列の比較
    int strcmp(const char *s1, const char *s2);
    int strncmp(const char *s1, const char *s2, size_t n);
    int strcasecmp(const char *s1, const char *s2);
    int strncasecmp(const char *s1, const char *s2, size_t n);

str"n"cpy() は、n バイトだけコピーする。n より短かったら、残りの部分に 0 を埋める。str"n"cat() も同様である。あまりよいインタフェースではない。

str"case"cmp では、大文字(upper case)と小文字(lower case)の区別をしな い。

OpenBSD, FreeBSD, SunOS 5.8 には、より優れた API がある。

     size_t strlcat(char *dst, char *src, size_t dstsize);
     size_t strlcpy(char *dst, char *src, size_t dstsize);
strlcat() は、strncat() と似ているが、最後に 0 を埋めることはない。

その他

文字列中の文字や文字列の検索
    char *strchr(const char *s, int c);
    char *strrchr(const char *s, int c);
    char *strstr(const char *haystack, const char *needle);
    char *index(constchar*"s, int c);
    char *rindex(const char *s, int c);

文字列中のトークンへの分解。
    char *strtok(char *s, const char *delim);

その他
    int strcoll(const char *s1, const char *s2);
    size_t strcspn(const char *s, const char *reject);
    char *strfry(char *string);
    char *strpbrk(const char *s, const char *accept);
    char *strsep(char **stringp, const char *delim);
    size_t strspn(const char *s, const char *accept);
    size_t strxfrm(char *dest, const char *src, size_t n);
strcpy(), strcat() の代りに次の関数も便利である。
#include <stdio.h>
    int sprintf(char *str, const char *, ...);
    int snprintf(char *str, size_t size, const  char  *,...);
#include <stdag.h>
    int vsprintf(char *str, const char *, va_list ap);
    int vsnprintfvsnprintf(char *str, size_t size, const  char *, va_list ap);

◆バッファ・オーバーフロー

文字列のバッファの大きさに注意する。 以下の例は、悪い例なので真似をしてはいけない。
----------------------------------------------------------------------
   1:	/*
   2:	        string-cpycat.c -- 文字列のコピーと結合(悪い例)
   3:	        ~yas/syspro/cc/string-cpycat.c
   4:	        Start: 2002/04/21 23:02:39
   5:	*/
   6:	
   7:	#include <stdio.h> /* stderr */
   8:	#include <stdlib.h> /* malloc() */
   9:	#include <string.h> /* strcpy(), strcat(), strlen() */
  10:	
  11:	main()
  12:	{
  13:	    char buf[100];
  14:	        strcpy(buf,"hello");
  15:	        strcat(buf,",world");
  16:	        strcat(buf,"\n");
  17:	        if( strlen(buf) >= 99 ) /* too late */
  18:	        {
  19:	            fprintf(stderr,"buffer overflow\n");
  20:	            exit( 1 );
  21:	        }
  22:	        printf("%s",buf );
  23:	        free( buf );
  24:	}
----------------------------------------------------------------------

文字列と文字の配列

注意:この例は、悪い例なので真似をしてはいけない。

strcpy() で初期化して、strcat() で後ろに付け加えていく。

100 バイトのバッファでは、buf[0] からbuf[98]まで文字のデータが入れられる。 最後のbuf[99] には、文字ではなく0 を入れる。buf[100] は使ってはいけない。 (次の別のデータが入っている。)

strlen(buf) が 100 を越えた時には、使ってはいけない場所を使ってしまった ことを意味する。これを、「バッファ・オーバーフロー」という。 そもそも事後にチェックするのは、本当は遅い。そもそも、バッファ・オーバー フローが起きないように気を付けながらプログラムを書くべきである。

◆snprintf()

strcpy(), strcat() の代りに snprintf() が便利な場合がある。
----------------------------------------------------------------------
   1:	/*
   2:	        string-snprintf.c -- snprintf を使った文字列のコピーと結合
   3:	        ~yas/syspro/cc/string-snprintf.c
   4:	        Start: 2002/04/21 23:02:39
   5:	*/
   6:	
   7:	#include <stdio.h> /* stderr, snprintf() */
   8:	#include <stdlib.h> /* malloc() */
   9:	
  10:	main()
  11:	{
  12:	    char buf[100];
  13:	        if( snprintf(buf,100,"hello%s\n",",world") >=sizeof(buf) )
  14:	        {
  15:	            fprintf(stderr,"buffer overflow\n");
  16:	            exit( 1 );
  17:	        }
  18:	        printf("%s",buf );
  19:	}
  20:	
----------------------------------------------------------------------
snprintf() は、第1引数に、バッファの番地、第2引数にバッファの長さ(最 後の0を含む)をとる。第3引数以降は、printf() と同じである。snprintf() では、けっしてバッファ・オーバーフローは起きない。起きそうになると、負 の数を返す。(成功すると、バイト数(最後の0は含まない)を返す。)

snprintf() は、%s 以外に %d や %c も使える。

注意:Linux (Glibc を使っている)では、snprintf() の仕様が変更された。 古い仕様(glibc 2.0.6以前)では、snprinf() は、エラーが起きると -1 を返 す。新しい仕様(glibc 2.1以降)では、snprintf() は、必要なバイト数、すな わち、バッファが無限大であるときに書き込まれる文字列の長さ(最後の0を除 く)を返す。これは、strlen() が返すものと同じである。従って、新しい snprintf() では、結果とバッファの大きさを比較して正確に書き込まれたか を検査する。(snprintf() は、決してバッファ・オーバーフローを起こすこと はないが、バッファが足りない時には意図していない結果が保存されているこ とになる。)

◆Pascal文字列

Pascalというプログラミング言語では、文字列を表すのに、0 で終端させるの ではなくて、データの先頭に文字列の長さを置く。

■ファイルのコピー(システムコールの利用)

file-copy.c は、システムコール open(), read(), write(), close() を使っ て引数で指定されたファイルを開き、その内容をコピーするプログラムである。
----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        file-copy.c -- ファイルをコピーする簡単なプログラム
   4:	        ~yas/syspro/file/file-copy.c
   6:	        Start: 1995/03/04 16:40:24
   7:	*/
   8:	
   9:	#include <stdio.h>      /* stderr */
  10:	#include <fcntl.h>      /* open(2) */
  11:	
  12:	extern void file_copy( char *from_name, char *to_name );
  13:	
  14:	main( int argc, char *argv[] )
  15:	{
  16:	        if( argc != 3 )
  17:	        {
  18:	            fprintf( stderr,"Usage: %s from to\n", argv[0] );
  19:	            exit( 1 );
  20:	        }
  21:	        file_copy( argv[1],argv[2] );
  22:	}
  23:	
  24:	#define BUFFERSIZE      1024
  25:	
  26:	void file_copy( char *from_name, char *to_name )
  27:	{
  28:	    int from_fd,to_fd ;
  29:	    char buffer[BUFFERSIZE] ;
  30:	    int rcount ;
  31:	    int wcount ;
  32:	
  33:	        from_fd = open( from_name,O_RDONLY );
  34:	        if( from_fd == -1 )
  35:	        {
  36:	            perror( from_name );
  37:	            exit( 1 );
  38:	        }
  39:	        to_fd = open( to_name,O_WRONLY|O_CREAT|O_TRUNC,0666 );
  40:	        if( to_fd == -1 )
  41:	        {
  42:	            perror( to_name );
  43:	            exit( 1 );
  44:	        }
  45:	        while( (rcount=read(from_fd,buffer,BUFFERSIZE)) > 0 )
  46:	        {
  47:	            if( (wcount=write(to_fd,buffer,rcount))!= rcount )
  48:	            {
  49:	                 perror( to_name );
  50:	                 exit( 1 );
  51:	            }
  52:	        }
  53:	        close( from_fd );
  54:	        close( to_fd ); 
  55:	}
----------------------------------------------------------------------

プログラムに引数を渡すことができる。argv[0] には、プログラムの名前を示 す文字列が入っている。自分のプログラムの名前で動作を変える時にだけ参照 する。argv[1] 以降が本当の意味での引数である。argc には、argv[0]を含め た引数の数が入っている。

open()は、ファイルを開くシステム・コールである。O_RDONLYとは、 読み込み専用でファイルを開くことを意味している。open() システム・コー ルは、結果としてファイル記述子を返す。ファイル記述子は、負でない小さな 整数(だいたい0〜1024の範囲、最大はシステムによって違う)である。 ファイル記述子は、read()システム・コールやwrite()システム・コールで実 際にファイルを読み書きする時に使われる。

エラーが起きた時には、open() システム・コールは、-1 を返す。この時、エ ラーの番号が errno という変数に格納される。perror() は、errno変数を解 析して、より詳しいエラー・メッセージを表示する関数である。perror() の 引数は、エラー・メッセージとともに表示される文字列である。

ここでは、出力用のファイルを開いている。O_WRONLYは、書き込み専用でファ イルを開くことを意味している。O_CREATは、ファイルが存在しなければ作る ように指示するものである。ファイルが存在する場合、上書きされる。 O_TRUNCは、ファイルが存在している時には、その大きさを0にすることを指 示するものである。0666 (C言語で0から始まる数は、8進数)は、ファイルを 作る時のモードである。この数値に従って、ファイルのモード(ls -l で  rwxrwxrwx と表示される部分)が決定される。作成されるファイルのモードは、 ここで指定されたモードから現在の umask が落とされた(引かれた)値とな る。

read() システム・コールは、第1引数で指定されたファイル記述子のファイ ルを読み込み、それを第2引数の番地へ保存する。読み込むバイト数は、第3 引数で与えられる。結果として読み込んだバイト数を返す。通常は、BUFFERSIZE が返される。read() システム・コールの結果、ファイル上の読み書きする位 置が、実際に読み込んだバイト数だけずれる。ファイルの末尾近くや、ファイ ルが端末の時、BUFFERSIZE 以下の値が返される。ファイルの末尾に行き着くと  0 が返される。エラーが起きると、-1 が返される。

write() システム・コールは、第1引数で指定されたファイル記述子のファイ ルへデータを書き込む。書き込まれるデータは、第2引数で与えられた番地か ら、第3引数で与えらた大きさである。write() システム・コールは、結果と して書き込んだバイト数を返す。ファイル上の読み書きする位置が、実際に読 み込んだバイト数だけずれる。通常は、BUFFERSIZE が返される。空き容量不足 などで書き込みが失敗した時には、-1 を返す。エラーが起きると、-1 が返さ れる。

close() は、ファイルを閉じるシステム・コールである。このシステム・コールを実行してしまうと、そのファイル記述子は無効になる。すなわち、read() や write() を行うと、エラーになる。

----------------------------------------------------------------------
% mkdir file [←]
% cd file [←]
% cp ~yas/syspro/file/file-copy.c . [←]
% make file-copy [←]
cc     file-copy.c   -o file-copy
% ls [←]
file-copy    file-copy.c
% echo "This is file1" > file1 [←]
% cat file1 [←]
This is file1
% ./file-copy file1 file2 [←]
% ls [←]
file-copy    file-copy.c  file1        file2
% ls -l [←]
総ブロック数 30
-rwxr-xr-x    1 yas      lab        12616   4月 22日 17時19分  file-copy
-rw-r--r--    1 yas      lab         1096   4月 22日 17時19分  file-copy.c
-rw-r--r--    1 yas      lab           14   4月 22日 17時20分  file1
-rw-r--r--    1 yas      lab           14   4月 22日 17時20分  file2
% diff file1 file2 [←]
% cmp file1 file2 [←]
% cat file2 [←]
This is file1
% []
----------------------------------------------------------------------

■ファイル記述子

ファイル記述子は、小さな整数である。
----------------------------------------------------------------------
   1:	/*
   2:	        fd-print.c -- ファイル記述子を表示するプログラム
   3:	        ~yas/syspro/file/fd-print.c
   5:	        Start: 1997/04/21 18:23:13
   6:	*/
   7:	
   8:	#include <fcntl.h>
   9:	
  10:	void main( int argc, char *argv[] )
  11:	{
  12:	    int i ;
  13:	    int fd ;
  14:	        for( i=1 ; i<argc ; i++ )
  15:	        {
  16:	            fd = open( argv[i],O_RDONLY );
  17:	            printf("%s: %d\n",argv[i],fd );
  18:	        }
  19:	}
----------------------------------------------------------------------
----------------------------------------------------------------------
% ./fd-print fd-print.c  [←]
fd-print.c: 3
% ./fd-print fd-print.c /etc/passwd fd-print.c [←]
fd-print.c: 3
/etc/passwd: 4
fd-print.c: 5
% []
----------------------------------------------------------------------

■標準入出力を利用したファイルのコピー(システムコール)

プログラムが実行される時、open() システム・コールを使わなくても、次の 3つのファイル記述子は使える状態になっている。

0
標準入力(standard input)
1
標準出力(standard output)
2
標準エラー出力(standard error)

stdio-thru.cは、標準入力で指定されたファイルの内容を標準出力で指定され たファイルへコピーするプログラムである。

----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        stdio-thru.c -- 標準入力から標準出力へのコピー
   4:	        ~yas/syspro/file/stdio-thru.c
   6:	        Start: 1995/03/04 16:40:24
   7:	*/
   8:	
   9:	#include <stdio.h>
  10:	
  11:	extern void stdio_thru(void);
  12:	
  13:	main( int argc, char *argv[] )
  14:	{
  15:	        if( argc != 1 )
  16:	        {
  17:	            fprintf( stderr,"Usage: %s\n", argv[0] );
  18:	            exit( 1 );
  19:	        }
  20:	        stdio_thru();
  21:	}
  22:	
  23:	#define BUFFERSIZE      1024
  24:	
  25:	void stdio_thru()
  26:	{
  27:	    char buffer[BUFFERSIZE] ;
  28:	    int rcount ;
  29:	    int wcount ;
  30:	
  31:	        while( (rcount=read(0,buffer,BUFFERSIZE)) > 0 )
  32:	        {
  33:	            if( (wcount=write(1,buffer,rcount))!= rcount )
  34:	            {
  35:	                 perror("stdout");
  36:	                 exit( 1 );
  37:	            }
  38:	        }
  39:	        close( 0 );
  40:	        close( 1 );
  41:	}
----------------------------------------------------------------------
このプログラムは、引数をとらない。"<" や ">" は、シェルにより解釈され、 このプログラムには渡されない。

file_copy() とは異なり、stdio_thru() では、ファイルを開く操作(open()) を行うことなく、入出力(read(),write)を行っている。このような事が可能 な理由は、UNIXでは、ファイル記述子 0, 1, 2 は、シェルにより既に開 かれているからである。

ファイル記述子 0 は、なにもしないと端末のキーボードに割り当てられてい る。ファイル記述子 1, 2 は、端末の画面に割り当てられている。しかし、 "<"、">"、"|"を使うと別のファイル やパイプに切替えられる。

----------------------------------------------------------------------
% ./stdio-thru [←]
ljd[←]
ljd
sk[←]
sk
^D
% ls bbb [←]
bbb がアクセスできません: そのようなファイルまたはディレクトリはありません
% ./stdio-thru < stdio-thru.c > bbb [←]
% ls -l stdio-thru.c bbb [←]
-rw-r--r--    1 yas      lab          690   4月 22日 17時37分  bbb
-rw-r--r--    1 yas      lab          690   4月 22日 17時35分  stdio-thru.c
% []
----------------------------------------------------------------------

■文字単位のフィルタ(ライブラリ)

----------------------------------------------------------------------
   1:	/*
   2:	        filter-char.c -- 文字単位の簡単なフィルタ
   3:	        ~yas/syspro/file/filter-char.c
   5:	        Start: 1997/04/21 18:23:13
   6:	*/
   7:	
   8:	#include <stdio.h>      /* stdin, stdout, fopen(), fclose(), getc(), */
   9:	#include <ctype.h>      /* toupper() */
  10:	
  11:	extern void filter_char_upper( FILE *in, FILE *out );
  12:	
  13:	main( int argc, char *argv[] )
  14:	{
  15:	    int i ;
  16:	    FILE *fp ;
  17:	        if( argc == 1 )
  18:	            filter_char_upper( stdin, stdout );
  19:	        else
  20:	        {
  21:	            for( i=1 ; i< argc ; i++ )
  22:	            {
  23:	                if( (fp = fopen( argv[i],"r" )) == NULL )
  24:	                {
  25:	                    perror( argv[i] );
  26:	                    exit( -1 );
  27:	                }
  28:	                filter_char_upper( fp, stdout );
  29:	                fclose( fp );
  30:	            }
  31:	        }
  32:	}
  33:	
  34:	void filter_char_upper( FILE *in, FILE *out )
  35:	{
  36:	    int c ;     /* int型。char は、不可 */
  37:	        while( (c=getc(in)) != EOF )
  38:	        {
  39:	            putc( toupper(c),out );
  40:	        }
  41:	}
----------------------------------------------------------------------

実行例

----------------------------------------------------------------------
% ./filter-char [←]
abc[←]
ABC
asdfjkl;[←]
ASDFJKL;
^D
% []
----------------------------------------------------------------------

fopen()は、ファイルを開くライブラリ関数である。"r" は、 読み込み専用で ファイルを開くことを意味している。fopen()は、結果として FILE 構造体へ のポインタを返す。これは、次のようなライブラリ関数で使われる。

    fgetc()           fputs()           getc()         getw()
    fgets()           fread()           ungetc()
    fprintf()         fscanf()
    fputc()           fwrite()          putc()         putc()
    fflush()          fseek()           ftell()
    frewind()         fclose()          fdopen()       freopen()
    ferrror()         feof()            clearerr()     fileno()

このような、FILE * を使うライブラリ関数群は、次のような名前で呼ば れる。

putc() は、第1引数で指定された文字を第2引数で与えられたストリームへ 出力する。

fclose() は、ファイルを閉じるライブラリ関数である。

Java 風に書くとこうなる。

class FILE {
    int fgetc();
    int fputs(String str);
    size_t fread(byte buf, size_t size, size_t nmemb);
    ...
}

	FILE stdin ;
	FILE stdout ;
	int x = stdin.fgetc();
	stdout.fputs("hello");
引数の FILE *stream が、オブジェクトとなり、普通の引数からは消える。呼 出しの時には、オブジェクトを指定する。

■バッファと FILE 構造体(ライブラリ)

システムによって FILE 構造体の内容は異なる。
----------------------------------------------------------------------
   1: /*
   2:         filestruct-print.c -- _filestruct[] の内容を表示する
   3:         ~yas/syspro/file/filestruct-print.c
   4:         Start: 1997/05/05 18:31:55
   5: */
   6: 
   7: #include <stdio.h>
   8: 
   9: #if     0
  10: 
  11: struct _IO_FILE {
  12:   int _flags;           /* High-order word is _IO_MAGIC; rest is flags. */
  13: #define _IO_file_flags _flags
  14: 
  15:   /* The following pointers correspond to the C++ streambuf protocol. */
  16:   /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  17:   char* _IO_read_ptr;   /* Current read pointer */
  18:   char* _IO_read_end;   /* End of get area. */
  19:   char* _IO_read_base;  /* Start of putback+get area. */
  20:   char* _IO_write_base; /* Start of put area. */
  21:   char* _IO_write_ptr;  /* Current put pointer. */
  22:   char* _IO_write_end;  /* End of put area. */
  23:   char* _IO_buf_base;   /* Start of reserve area. */
  24:   char* _IO_buf_end;    /* End of reserve area. */
  25:   /* The following fields are used to support backing up and undo. */
  26:   char *_IO_save_base; /* Pointer to start of non-current get area. */
  27:   char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  28:   char *_IO_save_end; /* Pointer to end of non-current get area. */
  29: 
  30:   struct _IO_marker *_markers;
  31: 
  32:   struct _IO_FILE *_chain;
  33: 
  34:   int _fileno;
  35:   int _blksize;
  36:   _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */
  37: 
  38: #define __HAVE_COLUMN /* temporary */
  39:   /* 1+column number of pbase(); 0 is unknown. */
  40:   unsigned short _cur_column;
  41:   signed char _vtable_offset;
  42:   char _shortbuf[1];
  43: 
  44:   /*  char* _save_gptr;  char* _save_egptr; */
  45: 
  46:   _IO_lock_t *_lock;
  47: };
  48: 
  49: typedef struct _IO_FILE _IO_FILE;
  50: typedef struct _IO_FILE FILE;
  51: 
  52: extern FILE *stdin;
  53: extern FILE *stdout;
  54: extern FILE *stderr;
  55: 
  56: #endif
  57: 
  58: extern void filestruct_print( FILE *fp );
  59: 
  60: main()
  61: {
  62:     FILE *fp ;
  63: 
  64:         printf("stdin  == 0x%08x (fd==%d)\n",stdin, stdin->_fileno );
  65:         printf("stdout == 0x%08x (fd==%d)\n",stdout, stdout->_fileno );
  66:         printf("stderr == 0x%08x (fd==%d)\n",stderr, stderr->_fileno );
  67: 
  68:         fp = fopen("filestruct-print.c","r");
  69:         printf("fp     == 0x%08x (fd==%d)\n",fp, fp->_fileno );
  70: 
  71:         printf("fopen()\n");
  72:         filestruct_print( fp );
  73:         fgetc( fp );
  74:         printf("fgetc()\n");
  75:         filestruct_print( fp );
  76:         fgetc( fp );
  77:         printf("fgetc()\n");
  78:         filestruct_print( fp );
  79:         fclose( fp );
  80:         printf("fclose()\n");
  81:         filestruct_print( fp );
  82: }
  83: 
  84: 
  85: void filestruct_print( FILE *fp )
  86: {
  87:         printf("read_ptr:0x%08x, read_end:0x%08x, read_base:0x%08x\n",
  88:                fp->_IO_read_ptr, fp->_IO_read_end, fp->_IO_read_base );
  89: }
----------------------------------------------------------------------
_IO_read_ptr
バッファの先頭番地(次に読む文字)
_IO_read_base
バッファ用に確保されているメモリ領域の先頭番地
_fileno
UNIXのファイル記述子
実行結果

----------------------------------------------------------------------
% make filestruct-print [←]
cc     filestruct-print.c   -o filestruct-print
% ./filestruct-print [←]
stdin  == 0x40146fc0 (fd==0)
stdout == 0x40147140 (fd==1)
stderr == 0x401472c0 (fd==2)
fp     == 0x08049958 (fd==3)
fopen()
read_ptr:0x00000000, read_end:0x00000000, read_base:0x00000000
getc()
read_ptr:0x40019001, read_end:0x400199fb, read_base:0x40019000
getc()
read_ptr:0x40019002, read_end:0x400199fb, read_base:0x40019000
fclose()
read_ptr:0x00000000, read_end:0x00000000, read_base:0x00000000
% []
----------------------------------------------------------------------

fopen() した直後は、read_ptr などは初期化されていない。 最初に fgetc() を実行した時に初期化される。 fgetc() を呼び出すと、read_ptr が1つずれる。 fclose() すると、read_ptr などは 0 に初期化される。

■数の入出力

Unix の read(), write() システムコールは、「バイト列」を読み書きするも のである。Unix で数を読み書きするには、次の2つの方法がある。

キーボードで打てるのは、ASCII 文字の並びである。キーボードから int を 読みたい時には、文字の並びとしてまず読み込み(fgets())、次に、 文字の並びから整数に変換する(atoi(), strtol(), sscanf())

scanf() は、人間が打つ、間違い(意図的な攻撃)を含む可能性がある場所で 使うべきではない。

OSの中には、キーボードからASCII 文字の並び('0' から'9')を打ち込むと、OSの中で整に変換する機能が あるものもある。Unix には、そのような機能はない。文字の並びではなく数 が必要ならば、ライブラリ関数で変換する。

■ライブラリとシステムコールの混在

普通、同じファイルについては、ライブラリとシステム・コールを混ぜて使う ことはしない。たとえば、read(0,,) と getc(stdin)、write(1,,) と fprintf(stdout,,) を混ぜない。ライブラリは、バッファリングしているので、 タイミングが合わなくなる。

特殊な状況では、混ぜる必要がでてくる。 その時は、次のような関数を使う。

fileno()
FILE * で使われているファイル記述子を調べる。
fdopen()
ファイル記述子から FILE * を作る。
fflush()
FILE * の内部のバッファに溜っているデータを書き出す。

■キーボード入力のバッファリング

システムコールでも、キーボードからの入力ではバッファリングが 行われる。getchar() は、リターンが押されるまで、1文字も変えさない。 それまでは、バックスペースなどで編集することもできる。

この機能を抑止するには、stty cbreak とするシステムが多い。Irix など、 cbreak が使えないシステムもある。

----------------------------------------------------------------------
% cat [←]
hello[←]
hello
^D
% stty cbreak;cat [←]
hheelllloo[←]

^C
% []
----------------------------------------------------------------------
stty raw でも、バッファリングがなくなるが、^C による強制終了もできなく なるので、注意する。

----------------------------------------------------------------------
% cat [←]
abc[←]
abc
^D
% stty raw; cat [←]
aabbcc

			(^Dが利かない)
			(別の端末から ps して kill)
Terminated
% []
----------------------------------------------------------------------

◆grepコマンド

grep (egrep, fgrep) は、ファイルの内容を検索し、引数で与えられたパタン が見つかった行を出力するコマンドである。次の例は、「UTMP_FILE」 または、「WTMP_FILE」という文字列を含む行を、ファイル /usr/include/utmp.h から探し、結果を表示している。

----------------------------------------------------------------------
% egrep '_PATH_[UW]TMP' /usr/include/paths.h [←]
#define _PATH_UTMP      "/var/run/utmp"
#define _PATH_WTMP      "/var/log/wtmp"
% []
----------------------------------------------------------------------
一般的には、次のようになる。
----------------------------------------------------------------------
% egrep pattern file1 file2 ... [←]
----------------------------------------------------------------------
pattern としては、次のような正規表現(regular expressions)が使える。
.
任意の1文字
.*
任意の1文字が0回以上繰り返したもの
A*
文字Aが0回以上繰り返したもの
A*
文字Aが0回以上繰り返したもの
^
行の先頭
$
行の末尾
[a-m]
文字aから文字mまでの1文字
[A-Za-z0-9]
アルファベットと数字
シェルで「*」と書く所は、grep では「'.*'」と書く点に注意する。 たとえば、 シェルで「*.[ch]」と書く所は、grep では「'.*\.[ch]'」と書く。

「*」、「$」、「[]」など、シェルが展開してしまう文字をパタンとして指定 する時には、シングル・クォート「''」で括る。

egrep は、grep の拡張版である。egrep は、grep より機能が拡張され、アル ゴリズムも改良されていて速い。fgrep は、grep の簡易版で、正規表現が使 えない(速度も遅い)。

■練習問題

★練習問題 10 フィルタのプログラム

高水準入出力ライブラリ関数を持ちいて、次のようなフィルタを作りなさい。

文字単位のフィルタでは、getc(), fgetc(), getchar() と putc(), fputc(), putchar() などを使うとよい。行単位のフィルタでは、 fgets(), fputs(), fprintf() などを使うとよい。 (gets(), fscanf(), scanf() は、 バッファ・オーバーフローが起きるので、使 わない。)

fgrep は、grep, egrep などと違って単純な文字列の比較しかできない。 regcomp(3) などを使えば、正規表現の検索部分は自分で作らなくてもよい。

以下に rot-N 暗号プログラムの main() 関数の例を示す。

----------------------------------------------------------------------
   1:	/*
   2:	        rotn.c -- 文字単位の簡単なフィルタ(rotN)
   3:	        ~yas/syspro/file/rotn.c
   4:	        Start: 2003/04/21 13:31:46
   5:	*/
   6:	
   7:	#include <stdio.h>      /* stdin, stdout, fopen(), fclose(), getc(), */
   8:	#include <ctype.h>      /* isalpha() */
   9:	#include <stdlib.h>     /* strtol() */
  10:	
  11:	main( int argc, char *argv[] )
  12:	{
  13:	    int n ;
  14:	    char *filename ;
  15:	        if( argc != 3 )
  16:	        {
  17:	            fprintf(stderr,"Usage: %% %s n file\n",argv[0]);
  18:	            exit( 1 );
  19:	        }
  20:	        n = strtol( argv[1],0,10 );
  21:	        filename = argv[2] ;
  22:	        printf("n==%d, file == %s \n", n, filename );
  23:	}
----------------------------------------------------------------------
以下に fgrep プログラムの main() 関数の例を示す。
----------------------------------------------------------------------
   1:	/*
   2:	        myfgrep.c -- 簡単な行単位のフィルタ(fgrep)
   3:	        ~yas/syspro/file/myfgrep.c
   4:	        Start: 2003/04/23 14:51:27
   5:	*/
   6:	
   7:	#include <stdio.h>      /* stdin, stdout, fopen(), fclose(), getc(), */
   8:	#include <string.h>     /* strstr() */
   9:	
  10:	main( int argc, char *argv[] )
  11:	{
  12:	    char *pattern ;
  13:	    char *filename ;
  14:	        if( argc != 3 )
  15:	        {
  16:	            fprintf(stderr,"Usage: %% %s pattern file\n",argv[0]);
  17:	            exit( 1 );
  18:	        }
  19:	        pattern = argv[1] ;
  20:	        filename = argv[2] ;
  21:	        printf("pattern==%s, file == %s \n", pattern, filename );
  22:	}
----------------------------------------------------------------------

rot13 や rot-N などの文字単位のフィルタは、例題の filter-char.c の大部分をそのまま使い toupper() だけを置き換えても実現できる。

★練習問題 11 teeプログラム

file-copy.c, stdio-thru.c の2つのプログラムを参考にして、UNIX の tee プログラムと同じような機能をもつプログラムを作りなさい。(tee コマンド について詳しくは、man tee を見なさい。)プログラムの名前は、tee ではな く、別の名前(mytee) としなさい。tee の場合、標準の tee が動いているの か、自分で作った tee が動いているのか区別がつかない。ただし、出力する ファイルは、一つでもよい。-i オプションや -a オプションには対応しなく てもよい。

mytee は、次のようにして、標準入力を標準出力にコピーしながら、同時にファ イルにも保存するものである。

% grep pattern file1 | ./mytee result [←]
この結果として、画面には、次のように grep コマンドを実行した時と同じ結果が表示される。

% grep pattern file1 [←]

同時に、ファイル reult には、画面に出力された結果とまったく同じものが 保存される。

tee コマンドの名前は、アルファベットの「T」に由来する。図形的に考えれ ば、動きを理解することができる。tee コマンドは、左(パイプラインで標準 入力)から入ったデータを下(引数で与えられたファイル)に保存しながら、 右(標準出力)にも出力する。tee コマンドは、時間のかかる処理の結果や、 後で参照したい中間結果をファイルに保存するために利用される。次の例は、 makeコマンドの実行結果を make.out へ保存すると同時に、それを user にメー ルで送るものである。

% make |& tee make.out | mail -s "make result" user [←]

これにより、user は、メールが届いたことで、make コマンドの実行終了を知 ることができる。同時に、作業していたディレクトリでその結果を参照するこ とができる。

この課題では、入出力には、システムコール (open(),read(),write(),close()))を使いなさい。

★練習問題 12 catプログラム

UNIXの cat コマンドと同様の動きをするプログラムを作りなさい。このプロ グラムの名前を mycat とする。引数として、なにも与えられなかった時には、 標準入力を標準出力にコピーしなさい。また、引数として "-" が指定された 時には、その位置で標準入力を標準出力にコピーしなさい。たとえば、次のよ うな場合、file1 とfile2 の内容の間に、標準入力の内容をコピーしなさい。

% mycat file1 - file2 [←]

この課題では、入出力には、システムコール (open(),read(),write(),close()))を使いなさい。

★練習問題 13 stringライブラリの仕組み

次のライブラリ関数から1つを選び、それと似たような動きをする関数を作り なさい。 ただし、既存の他のイブラリ関数を呼んではならない。strdup() に限っては、 malloc() を呼びなさい。必要ならば、strlen() 関数を自分自身で定義して利 用してもよい。最初からあるライブラリ関数と紛らわしいので、mystrcat(), mystrlen() のように、頭に my を付け、名前を変えなさい。正しく動作して いることを示すような実行結果を付けなさい。

Linux の場合、ライブラリ関数のソース・プログラムは、探せば見つかる。こ の課題では、それを利用してもよい。ただし、それらのプログラムは、他のラ イブラリ関数を呼んでいるものがある。従って、他のライブラリ関数を呼び出 さないように、人手でインライン展開(別の関数の本体を呼び出し側に書く) しなさい。

★練習問題 14 stringライブラリの利用

string ライブラリの中で、strcpy(), strcat(), strlen() 以外の関数を使う プログラムを書きなさい。

★練習問題 15 バッファ・オーバーフローを起こさないプログラム関数の利用

今まで書いたプログラムの中で、次の2つの関数を使っているプログラムを見 つけなさい。 そのプログラムを、こららの関数を使わないように書き換えなさい。


Last updated: 2003/05/25 23:34:22
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>