演算子と2分木による式の表現
2分木の応用として式の表現の説明を行うけど、その前に計算式の一般論の説明を行う。
逆ポーランド記法
一般的に 1*2 + 3*4 と記載すると、数学的には演算子の優先順位を考慮して、(1*2)+(3*4) のように乗算を先に行う。このような優先順位を表現する時に、()を使わない方法として、逆ポーランド記法がある。
演算子の書き方には、前置記法、中置記法、後置記法があり、後置記法は、「2と3を掛ける、それに1を加える」と捉えると、日本語の処理と似ている。
中置記法 1+2*3 前置記法 +,1,*,2,3 後置記法 1,2,3,*,+ # 1と「2と3をかけた値」をたす。
後置記法は、一般的に逆ポーランド記法(Reverse Polish Notation)とも呼ばれ、式を機械語の命令に置き換える際に役立つ。
演算子の右結合・左結合
例えば、”1/2*3″という式が与えられたとする。この結果は、1/6だろうか?3/2だろうか?
一般的な数学では、優先順位が同じ演算子が並んだ場合、左側から計算を行う。つまり”1/2*3″は、”(1/2)*3″を意味する。こういった左側の優先順位が高い演算子は左結合の演算子という。
ただしC言語では、”a = b = c = 0″ と書くと、”a = (b = (c = 0))” として扱われる。こういった代入演算子は、 右結合の演算子である。
理解度確認
以下の式を指定された書き方で表現せよ。
逆ポーランド記法 1,2,*,3,4,*,+ を中置記法で表現せよ。 中置記法 (1+2)*3-4*5 を逆ポーランド記法で表現せよ。
以前の情報処理技術者試験では、スタックの概念の理解の例題として、逆ポーランド記法への変換アルゴリズムのプログラム作成が出題されることが多かったが、最近は出題されることはなくなってきた。
逆ポーランド記法の式の実行
この逆ポーランド記法で書かれた式から結果を求めるプログラムは以下のように記述できる。このプログラムでは式を簡単にするため、数値は1桁の数字のみとする。
// 単純な配列を用いたスタック int stack[ 10 ] ; int sp = 0 ; void push( int x ) { stack[ sp++ ] = x ; } int pop() { return stack[ --sp ] ; } // 逆ポーランド記法の計算 int rpn( char* p ) { for( ; *p != '// 単純な配列を用いたスタック int stack[ 10 ] ; int sp = 0 ; void push( int x ) { stack[ sp++ ] = x ; } int pop() { return stack[ --sp ] ; } // 逆ポーランド記法の計算 int rpn( char* p ) { for( ; *p != '\0' ; p++ ) { if ( isdigit( *p ) ) { // ~~(A) // 数字はスタックに積む push( *p - '0' ) ; // ~~~~~~~~(B) } else if ( *p == '+' ) { // 演算子+は上部2つを取出し int r = pop() ; int l = pop() ; // 加算結果をスタックに積む push( l + r ) ; } else if ( *p == '*' ) { // 演算子*は上部2つを取出し int r = pop() ; int l = pop() ; // 乗算結果をスタックに積む push( l * r ) ; }//~~~~~~~~~~~~~(C) } // 最終結果がスタックに残る return pop() ; } void main() { printf( "%d\n" , rpn( "123*+" ) ) ; }' ; p++ ) { if ( isdigit( *p ) ) { // ~~(A) // 数字はスタックに積む push( *p - '0' ) ; // ~~~~~~~~(B) } else if ( *p == '+' ) { // 演算子+は上部2つを取出し int r = pop() ; int l = pop() ; // 加算結果をスタックに積む push( l + r ) ; } else if ( *p == '*' ) { // 演算子*は上部2つを取出し int r = pop() ; int l = pop() ; // 乗算結果をスタックに積む push( l * r ) ; }//~~~~~~~~~~~~~(C) } // 最終結果がスタックに残る return pop() ; } void main() { printf( "%d\n" , rpn( "123*+" ) ) ; }
逆ポーランド記法の式の実行は、上記のようにスタックを用いると簡単にできる。このようなスタックと簡単な命令で複雑な処理を行う方法はスタックマシンと呼ばれる。Java のバイトコードインタプリタもこのようなスタックマシンである。
Cプログラママニア向けの考察
上記のプログラムでは、int r=pop();…push(l+r); で記載しているが、
push( pop() + pop() ) ;とは移植性を考慮して書かなかった。理由を述べよ。
最初の関数電卓
初期の関数電卓では複雑な数式を計算する際に、演算子の優先順位を扱うのが困難であった。このため、HP社の関数電卓では、式の入力が RPN を用いていた。(HP-10Cシリーズ)
2項演算と構文木
演算子を含む式が与えられたとして、古いコンパイラではそれを逆ポーランド変換して計算命令を生成していた。しかし最近の複雑な言語では、計算式や命令を処理する場合、その式(または文)の構造を表す2分木(構文木)を生成する。。
+ / \ 1 * / \ 2 3
演算子の木のノードで、末端は数値であることに注目し、右枝・左枝がNULLなら数値(data部にはその数値)、それ以外は演算子(data部には演算子の文字コード)として扱うとして、上記の構文木のデータを作る処理と、その構文木の値を計算するプログラムを示す。
struct Tree { int data ; struct Tree* left ; struct Tree* right ; } ; struct Tree* tree_int( int x ) // 数値のノード { struct Tree* n ; n = (struct Tree*)malloc( sizeof( struct Tree ) ) ; if ( n != NULL ) { n->data = x ; n->left = n->right = NULL ; } return n ; } struct Tree* tree_op( int op , // 演算子のノード struct Tree* l , struct Tree* r ) { struct Tree* n ; n = (struct Tree*)malloc( sizeof( struct Tree ) ) ; if ( n != NULL ) { // ~~~~~~~~~~~~~~~~~~~~~(D) n->data = op ; n->left = l ; n->right = r ; } return n ; } // 与えられた演算子の木を計算する関数 int eval( struct Tree* p ) { if ( p->left == NULL && p->right == NULL ) { // 数値のノードは値を返す return p->data ; } else { // 演算子のノードは、左辺値,右辺値を求め // その計算結果を返す switch( p->data ) { case '+' : return eval( p->left ) + eval( p->right ) ; case '*' : return eval( p->left ) * eval( p->right ) ; } // ~~~~~~~~~~~~~~~(E) ~~~~~~~~(F) } } void main() { struct Tree* exp = // 1+(2*3) の構文木を生成 tree_op( '+' , tree_int( 1 ) , tree_op( '*' , tree_int( 2 ) , tree_int( 3 ) ) ) ; printf( "%d¥n" , eval( exp ) ) ; }
理解度確認
- push(),pop() のスタックは、保存と取り出しの順序を表す単語の頭文字4つを使って何と呼ばれるか?
- 上記プログラム中の(A)~(F)の型を答えよ。
GROUP BY HAVINGとビューテーブル
GROUP BY HAVING
GROUP BY-HAVING では、指定されたカラムについて同じ値を持つレコードがグループ化される。SELECT 文に指定される集約関数は、グループごとに適用される。HAVING は、ある条件を満たす特定のグループを選択するための条件で、WHERE と違い、集約関数が使える。
SELECT SG.商品番号, SUM(SG.在庫量) FROM SG GROUP BY SG.商品番号 HAVING SUM(SG.在庫量) >= 500 ;
- 実験環境でGROUP-BY-HAVING(学内のみ)
このSQLを実行すると、SG のテーブルから、商品番号が同じものだけをあつめてグループ化される。そのグループごとに在庫量のデータの合計SUMを集約し、500以上のデータが出力される。
CREATE VIEW
今までで述べてきたSQLでは、実際のテーブルを対象に、結合・選択・射影を行う命令であり、これは概念スキーマと呼ばれる、対象となるデータベース全体を理解したプログラマによって扱われる。
しかし、プログラムの分業化を行い、例えば結果の表示だけを行うプログラマにしてみれば、全てのデータベースの表を考えながらプログラムを作るのは面倒である。そこで、結合・選択・射影の演算の結果で、わかりやすい単純な表となったものであれば、初心者のデータベースプログラマでも簡単に結果を扱うことができる。このような外部スキーマを構成するための機能が、ビューテーブルである。
-- 優良業者テーブルを作る -- CREATE VIEW 優良業者 ( 業者番号 , 優良度 , 所在 ) AS SELECT S.業者番号, S.優良度, S.所在 FROM S WHERE S.優良度 >= 15 ; -- 優良業者テーブルから情報を探す -- SELECT * FROM 優良業者 WHERE 優良業者.所在 = '福井' ;
ビューテーブルに対する SQL を実行すると、システムによっては予め実行しておいた CREATE VIEW の AS 以下の SQL の実行結果をキャッシュしておいて処理を行うかもしれない。システムによっては SQL の命令を 副クエリを組合せた SQL に変換し、処理を行うかもしれない。しかし、応用プログラマであれば、その SQL がどのように実行されるかは意識する必要はほとんど無いであろう。
ただし、ビューテーブルに対する 挿入・更新・削除といった演算を行うと、データによっては不整合が発生することもあるので注意が必要である。
SQL言語
教科書の流れに沿ってSQLの言語について、再掲
- スキーマ定義
- CREATE – 実テーブル、ビューテーブルの定義
- GRANT – 権限の定義
- スキーマ操作
- DROP – 実テーブル、ビューテーブルの削除
- REVOKE – 権限の削除
- ALTER – テーブルの変更
- ADD – カラムの追加
- データ操作
- SELECT, INSERT, DELETE, UPDATE – レコードの検索、追加・削除・更新
- トランザクション処理
- データベースでは、原子性などを満たすためにデータベースへの更新履歴を保持している。これらの更新履歴をデータベースに反映させ確定する処理がトランザクション処理。
- COMMIT – データベースの更新処理を確定
- ROLLBACK – データベースの更新処理を取り消す
ホスト言語とのインタフェースとSQLインジェクション
プログラミング言語によっては、その言語の中でSQLを使うために「組み込み型のSQL」が使えるものがある。
(COBOL,PL/Iなど)
動的メモリ管理が得意な最近のPythonやPHPなどの言語であれば、データベース参照の関数が利用できる。
SQLインジェクション
例えば、PHPでは、SQLからデータを取り出す処理は、以下のようになる。
// 検索するユーザID $id = "t-saitoh" ; $pdo = new PDO( '...' ) ; // データベースに接続する関数 $sql = "select name from usertable where id='$id'" ; $query = $pdo->prepare( $sql ) ; // 取り出せたデータに関する処理 id がプライマリキーならforeachは1回ループのはず foreach( $query->fetcAll() as $name ) { // $name に取り出した名前が入っている }
しかし、$id の部分を、Web の入力フォームなどの値であれば、名前以外の情報が入力される場合もある。
この際に、「 $id = ” ‘ or 1==1 — ‘ ” 」といった値が入っていた場合、SQLで実行される命令は、
$id = "' or 1==1 --'" の場合 $sql = "select name from usertable where id='' or 1==1 -- ''" ;
となってしまい、本来なら1人のデータを抽出する select 命令が、全テーブルに対して該当してしまい、情報漏洩が発生するかもしれない。
「 $id = “‘; drop usertable ; — ‘” 」であれば、usertable が消されてしまい、システムが動かなくなる(サービスを提供できなくする攻撃 = DoS攻撃 – Denial-of-service attack)ことも考えられる。
こういった攻撃手法は、SQLに本来の意図ではないSQL命令を紛れ込ませる攻撃ということで、SQLインジェクションという。
SQLインジェクションで発生した有名な事件では、以下のようなものがある。
- Yahoo! BB 顧客情報漏洩事件 – 100億以上の被害
- PlayStation Network個人情報流出事件
対策としては、ユーザが入力したデータを用いて SQL 命令を実行する場合は、ユーザ入力をSQLとして悪用されないように、シングルクオートなどをエスケープするなどの処理が必要となる。さまざまな手法があるので、SQL無効化の専用関数を用いるべき。
また、データベースシステムは、ネットワーク経由でSQLによる処理を行うが、データベースサーバ自体がインターネットに接続されていて、パスワード攻撃によりデータベース本体に不正アクセスが行われる場合もある。一般的なデータベースを用いたシステムは、フロントエンドのWebサーバ、スレーブDBサーバ、マスタDBサーバの三層構成をとることが多いが、バックエンドのデータベースは、インターネットから隔離しフロントエンドのWebサーバのみ接続できるようにするのが一般的である。
データベースに接続する場合はパスワードにより利用者を限定することができるが、データベースシステム自体がインターネットに接続されていると、パスワード総当たり攻撃(ブルートフォース攻撃)や、パスワードスプレー攻撃(総当たり攻撃は、短時間でパスワード失敗が多発するのでシステムで接続拒否するのが一般的。これを回避するために時間をかけて総当たり攻撃をする手法)により、情報漏洩が発生する。
コンパイラの技術と関数電卓プログラム(2)
前半では、1文字の数字と簡単な演算子で表現される計算式を再帰下降パーサで計算する処理で、演習を行った。
後半は、さらに実際のコンパイラに近いものとして、 C言語で広く使われている、字句解析ツール(lexical analyzer : lex or flex)、 構文解析ツール(parser : yacc or bison) を使って、 さらに現実的な関数電卓プログラムを作ってみる。
lex or flex による字句解析
lexは、字句解析のプログラムを自動生成するツール。 “%%”行の内側に、正規表現によるルールとその処理(アクション)を書き並べる。 また、“%{ … %}”の内側に、その処理に必要なC言語のソースを書き、 lex で処理を行うと、lex.yy.c というC言語のソースを出力する。
lex.yy.c の中には、yylex() という関数が自動的に作られ、次に示す構文解析ツール(yacc or bison)から呼び出される。
# flex は、lex の改良版。
(( mycalc.l )) %{ // lexの %{ %} の内側は、C言語のプログラム #include <stdio.h> // yaccが出力するヘッダファイル #include "y.tab.h" int yywrap( void ) { // 1: スキャナ終了 // 0: yyin を切り替えて継続 return 1 ; } %} %% // %% から %% までは、lex の正規表現とその処理を記述する場所。 "+" return ADD ; // 演算子の種類を示す定数 ADD,SUB,...が定義される "-" return SUB ; "*" return MUL ; "/" return DIV ; "\n" return CR ; [0-9][0-9]* { // 入力はyytextに格納されている。 int temp ; sscanf( yytext , "%d" , &temp ) ; yylval.int_value = temp ; // 返り値は、字句(トークン)の種別を表す定数 return INT_LITERAL; // 整数リテラルを返す } %%
このプログラムを、lex で処理させると、正規表現“+” 記号に ADD という定数記号を割り振るとか、正規表現の数字列” [0-9][0-9]* “をみつけると、その正規表現に対する{〜}までに書かれたアクションを処理するため、文字列から数値を生成(sscanf)して、その場合の記号に INT_LITERAL という定数記号を割り振る…といった処理のプログラムを自動生成してくれる。(INT_LITERALは構文解析側で定義する定数)
- mycalc.l
- 参考: lex のそれ以外の文字の処理を追加しておいてください。
yacc or bison
yacc ( Yet Another Compiler Compiler ) もしくはその改良版の bison は、構文解析のプログラムを自動生成してくれるツールである。構文をBNF記法で記載すると、字句解析ツール(lex等)を呼び出しながら構文に合わせたトークンの出現に応じた状態遷移のための「遷移テーブル」を自動生成し、 その遷移テーブルを用いた処理のプログラムをC言語で出力してくれる。
先に示した字句解析プログラム(lex)は、様々なトークン(C言語なら演算子やキーワード)とデータ(C言語なら数値や文字列)を返す。このデータには様々な場合考えられるので、 そのトークンに合わせたデータの型を “%union” の中に記載する(中身はC言語の共用体)。 “%%”〜”%%” の間には、BNF記法の(ルール)と、それに対応する処理(アクションは“{ }”の中の部分)を記載する。最初の“%{ … %}”の間には、それ以外の処理に必要なC言語の処理を記載する。
# bison(水牛)は、yacc(山牛)の改良版。
(( mycalc.y )) %{ #include <stdio.h> #include <stdlib.h> // yacc が定義する内部関数のプロトタイプ宣言 #define YYDEBUG 1 extern int yydebug ; extern int yyerror( char const* ) ; extern char *yytext ; extern FILE *yyin ; // 最初に呼び出される関数yyparse() extern int yyparse( void ) ; // 字句解析を呼び出す関数yylex() extern int yylex( void ) ; %} // 字句(トークン)の定義 %union { int int_value; } %token <int_value> INT_LITERAL %token ADD SUB MUL DIV CR %type <int_value> expression term primary_expression %% // 構文の定義 line_list : line // 行の繰り返し | line_list line ; line : expression CR { printf( ">>%d\n" , $1 ) ; } ; // 以下のBNFルールは、単純に再帰に置き換えると // 無限に再帰して異常終了するものであることに注意 expression : term | expression ADD term { $$ = $1 + $3 ; } | expression SUB term { $$ = $1 - $3 ; } ; term : primary_expression | term MUL primary_expression { $$ = $1 * $3 ; } | term DIV primary_expression { $$ = $1 / $3 ; } ; primary_expression : INT_LITERAL ; %% // 補助関数の定義 int yyerror( char const* str ) { fprintf( stderr , "parser error near %s\n" , yytext ) ; return 0 ; } int main( void ) { yydebug = 0 ; // yydebug=1 でデバッグ情報表示 yyin = stdin ; if ( yyparse() ) { // 構文解析を開始 fprintf( stderr , "Error ! Error ! Error !\n" ) ; exit( 1 ) ; } }
yacc では、%%-%%の間に書かれたBNF記法によるルールとアクションをプログラムに変換してくれる。BNF記法の要素は、“要素 : ルール1 {アクション1} | ルール2 {アクション2} | … ;” といった構成になっている。要素 expression に対するルール “expression ADD term“では、ルールの各要素をアクションで参照する時には、$1,$2,$3 といった変数を利用できる。ルール「加算式 + 乗算式」という文になっている部分を見つけると、そのルールに対応するアクション“{ $$ = $1 + $3 ; }”「$1(加算式部分の値)と、$3(乗算式の部分)の値を加えて、$$(式の結果)に代入する」といった処理を生成してくれる。yyparse() 関数を呼び出すと、構文の一番最上部の line_list に相当する処理が起動される。yyerror()は、構文解析の途中で文法エラーになった時に呼び出される関数。
生成されるパーサの内容に興味があるなら、生成される y.tab.c の内容を読むと良い。
make と Makefile
これらのプログラムでは、字句解析プログラム mycalc.l から生成された lex.yy.c, y.tab.h と, 構文解析プログラム mycalc.y から生成された y.tab.c を組合せて1つの実行ファイルにコンパイルする。 これらの手順は煩雑なので、make ツールを使う。
make は、 Makefile に記載されている“ターゲット”と、それを作るために必要な“依存ファイル”、 “依存ファイル”から”ターゲット”を生成する処理“アクション”から構成される。 make は、ターゲットと依存ファイルの更新時間を比較し、 必要最小限の処理を行う。
基本的な Makefile の書き方は、
ターゲット: 依存ファイル... アクション # アクションの前はタブ
の様に書き、依存ファイルが更新されていたら、アクションを実行し、ターゲットを更新する。
以下に、今回の課題で使用する Makefile を示す。
(( Makefile )) # 最終ターゲット mycalc: y.tab.o lex.yy.o gcc -o mycalc y.tab.o lex.yy.o # 構文解析処理 y.tab.o: mycalc.y bison -dy mycalc.y # -dy : yacc互換モード gcc -c y.tab.c # 字句解析処理 lex.yy.o: mycalc.l mycalc.y flex -l mycalc.l # -l : lex互換モード gcc -c lex.yy.c # 生成ファイルの削除ルール clean:; rm mycalc y.tab.c y.tab.h lex.yy.c *.o (( ファイルの依存関係のイメージ図 )) mycalc.l mycalc.y | \ | lex.yy.c y.tab.h y.tab.c | \ | lex.yy.o y.tab.o \ / mycalc
この課題にあたり、後半の実験では flex, bison などの unix 系プログラミング環境を利用する。
macOS の利用者であれば MacPorts や Homebrew 、Windows 利用者であれば、wsl2(Windows subsystem for Linux) や Cygwinなどをインストールし実行すること。
今回の実験であれば、linux(Debian系)ならば、”sudo apt-get install flex bison gcc make” にて、必要なパッケージをインストールして実験を行うこと。
コンパイラの技術と関数電卓プログラム(1-2)
前回の実験資料では、再帰下降パーサについて説明し、サンプルプログラムを示した。
演算子の左結合・右結合
ここで、プログラムの実際の動きについて考えてみる。前回の乗除式の BNF 記法による定義は以下のようであった。
exp_乗徐式 ::= DIGIT '*' exp_乗徐式 | DIGIT '/' exp_乗徐式 | DIGIT ;
このBNFによる文法において、1*2*3 を考えると、以下のように解析がすすむ。
しかし、これでは 1*(2*3) であり、右結合にて処理が行われたことになる。
exp_乗徐式 /|\ DIGIT| exp_乗徐式 | | /|\ | |DIGIT| exp_乗徐式 | | | | | | | | | DIGIT | | | | | 1 * 2 * 3
左結合とするには
これをC言語で一般的な、(1*2)*3 といった左結合の処理になるように、BNF 記法の文法を下記のように書き換えるかもしれない。
exp_乗徐式 ::= exp_乗徐式 '*' DIGIT | exp_乗徐式 '/' DIGIT | DIGIT ;
しかし、このBNF記法をそのまま下記のような再帰に置き換えると、再帰が無限に続き異常終了してしまう。
int exp_MUL_DIV( ... ) { int left = exp_MUL_DIV( ... ) ; if ( **endp == '*' ) { (*endp)++ ; if ( isdigit( **endp ) ) { int right = **endp - '0' ; (*endp)++ ; return left + right ; } } else ... : }
左結合のプログラムにする場合は、BNF記法の処理を杓子定規に再帰プログラムで記述する必要はない。
空白除去
プログラム中の空白を無視するのであれば、以下のような補助関数を作っておくと便利かな。使い方は考えること。
void skip( char**ppc ) { while( isspace( **ppc ) ) (*ppc)++ ; }
Linux演習 – ファイル操作
Linux演習サーバへの接続
Unix(Linux)は、インターネットでのサーバとして広く活用されている。Linuxを試すには、Windows ならば WSL や Cygwin であったり、Mac でも使える仮想OSの VMware, VirrtualBox を使うこともでる。今回の演習では、全員が同じ環境で使うために、クラウド環境にサーバを準備した。クラウドのunixサーバを利用する場合には、リモートログインのための ssh が一般的である。
一方、教室のWiFi環境では、HTTP,HTTPS の通信しか使えないことから、ssh が通常利用できない。そこで、この演習では、特殊な使い方だが、HTTPS(443) ポートを使う。
Windows 10 ならば、cmd.exe , macOS ならば、ターミナルソフトを起動し、以下の操作を行う。
$ ssh -p 443 ゲストID@演習サーバ
- 演習サーバの接続方法(学内のみ) – サーバへの攻撃を極力へらすために非公開。
- 今回の演習では、センターIDではなくゲストIDを使います。
- ゲストIDのパスワードは、こちらのファイル(Teams)を参照。(2021-3EI Teams)
- パスワード入力時にタイプミスした時は、Ctrl-U で最初から入力のやり直しができる。
ファイル操作の基本
まずは基本操作をしてみよう。ls コマンド(list) は、ディレクトリ内にあるファイルの一覧を表示する。cat コマンド(catalog)は、指定されたファイルの内容を表示する。
s53599xx@nitfcei:~$ ls helloworld.c Maildir public_data public_html s53599xx@nitfcei:~$ ls -l total 8 -rw-r--r-- 1 s53599xx students 76 Dec 21 14:30 helloworld.c drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir (略) s53599xx@nitfcei:~$ cat helloworld.c #include <stdio.h> int main() { printf( "Hello World\n" ) ; return 0 ; } s53599xx@nitfcei:~$
ファイルをコピーするには cp コマンド(copy)、不要なファイルを消すには rm コマンド(remove)を使う。
s53599xx@nitfcei:~$ cp helloworld.c test.c s53599xx@nitfcei:~$ ls -l total 8 -rw-r--r-- 1 s53599xx students 76 Dec 21 14:30 helloworld.c drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir -rw-r--r-- 1 s53599xx students 76 Dec 21 14:40 test.c (略) s53599xx@nitfcei:~$ rm test.c s53599xx@nitfcei:~$ ls -l total 8 -rw-r--r-- 1 s53599xx students 76 Dec 21 14:30 helloworld.c drwx------ 5 s53599xx students 4096 Dec 21 14:30 Maildir s53599xx@nitfcei:~$
ファイル詳細表示の説明
ls -l で表示される詳細の内容は以下の通り。
属性 | リンク数 | 所有者 | グループ | サイズ | 日付 | ファイル名 |
---|---|---|---|---|---|---|
– rw- r– r– | 1 | s53599xx | students | 76 | Dec 21 14:30 | helloworld.c |
d rwx — — | 5 | s53599xx | students | 4096 | Dec 21 14:30 | Maildir |
– | d | -: 通常ファイル, d:ディレクトリ | ||||
rw- | rwx | 所有者が r:読み出し, w:書き込み, -: 権限なし ファイルなら、x:実行可能 ディレクトリなら、x:ディレクトリに入れる |
||||
r – – | – – – | グループの rwx の属性 r– は 読み込みだけ許可 | ||||
r – – | – – – | その他の rwx の属性 — は、読み書き禁止 |
基本的なファイル操作コマンド一覧
操作 | Linux | Windows |
---|---|---|
ディレクトリ一覧(list) ディレクトリ詳細 |
ls 場所 ※ ls -l 場所 |
dir /w 場所 ※ dir 場所 |
※ 省略時はカレントディレクトリ | ||
ファイル表示(catalog) | cat 場所 | type 場所 |
ファイルコピー(copy) | cp コピー元 コピー先 cp コピー元 コピー先ディレクトリ |
copy コピー元 コピー先 |
ファイル削除(remove) | rm 場所 | del 場所 |
ディレクトリ作成(make dir) | mkdir 場所 | md 場所 |
ディレクトリ削除(remove dir) | rmdir 場所 | rmdir 場所 |
カレントディレクトリ移動 (change directory) |
cd 場所 | cd 場所 ドライブの場合は ドライブ名: |
所有者を変更(change owner) | chown 所有者 場所 | |
グループを変更(change group) | chgrp グループ 場所 | |
属性を変更(change mode) | chmod 属性 場所 | ←属性の書き方 |
ワイルドカード文字
ls などのコマンドで、複数のファイルを対象とするとき、ワイルドカード文字が使える。
任意の1文字 ? |
(例) $ ls # 全部のファイル aaa.c ab.c abc.c bcd.c defgh.c hij.cxx $ ls a?.c # aで始まる2文字のC言語ファイル ab.c $ ls ???.c # 3文字のC言語のファイル aaa.c abc.c bcd.c |
任意の文字 * |
(例) $ ls a*.c # aで始まるC言語ファイル aaa.c ab.c abc.c $ ls *.cxx # 拡張子が.cxxのファイル(C++) hij.cxx |
相対PATHと絶対PATH
ファイルの場所を指定するには、2つの方法がある。
絶対PATHは、木構造の根(ルートディレクトリ / で表す) からの経路のディレクトリ名を”/”で区切って書き連ねる。ルートディレクトリからの場所であることを示すために、先頭を / で始める。住所を /福井県/越前市/宮谷町/斉藤家 と書くようなもの。
相対PATHは、現在注目しているディレクトリ(カレントディレクトリと呼ぶ)からの経路を書く。住所でいうと、/福井県/越前市 に注目している状態で、宮谷町/斉藤家 と書くようなもの。
ただし、/福井県/福井市 に注目している状態で、片町/山本家 は1つのファイルでも、/福井県/福井市/片町/山本家 とは別に /石川県/金沢市/片町/山本家 があるかもしれない。
上記の絵であれば、/home/tsaitoh/helloworld.c を、相対PATHで書く場合、s53599xx の一つ上にさかのぼって場所を指定することもできる。一つ上のディレクトリ(親ディレクトリ)は .. (ピリオド2つ)
この場合、” $ cat ../tsaitoh/helloworld.c ” の様な相対PATHでもアクセスできる。
カレントディレクトリ自身を表す場合は、. (ピリオド1つ)を使う。
/home/s53599xx/helloworld.c の場所は、” $ cat ./helloworld.c ” と書くこともできる。
CTF風に演習
ここで、ディレクトリの場所の書き方が理解できたかを練習してみよう。
/home0/Challenge/1-CTF.d の下にフォルダがいくつかあり、その中にさらにファイルがある。このファイルの中には、FLAG{なんとか} と書いた部分がある。この「なんとか」の部分を速く見つけ出して答えよう。
上記フォルダには、5つの問題 Task1 , Task2 , Task3 , Task4 , Task5 がある。
Task2 , Task3 では、以下のコマンドを使う。(ヒントはTaskフォルダの0ReadMeを読め)
操作 | Linux |
---|---|
ページャで表示(空白で次のページ,qで停止) | more ファイルの場所 |
元に戻れるページャ(less) | less ファイルの場所 |
多機能ページャ(文字コード判別) | lv ファイルの場所 |
漢字コード変換 | nkf ファイルの場所 nkf -j ファイル JISコードで表示 nkf -s ファイル Shift-JISコードで表示(Windows) nkf -e ファイル EUCコードで表示 nkf -w ファイル UTF-8 で表示(Mac,Linux) |
Task4 , Task5 には、難易度をあげるためのトリックがしかけてある。(ヒント 0ReadMe)
授業内レポート
- 自分でみつけられたフラグを以下の様に答えよ。(できた所までで良い)
Task1 = FLAG{なんとか}
Task2 = … - Task1 で答えを見つけるまでに実行したコマンドとその途中結果を簡単にまとめよ。
できれば、実行したコマンドを、相対PATH形式、絶対PATH形式で答えよ。
$ cd /home0/Challenge/1-CTF.d/Task1
$ ls
0ReadMe brain concussion persons
history コマンドは、過去に実行したコマンドを表示してくれる。
意思決定木と構文解析
前回までの授業で2分探索木の説明をしてきたが、このデータ構造は他のデータを扱う際にも用いられる。ここで、意思決定木と構文木を紹介する。
意思決定木
意思決定木の説明ということで、yes/noクイズの例を示しながら、2分木になっていることを 説明しプログラムを紹介。
((意思決定木の例:うちの子供が発熱した時)) 38.5℃以上の発熱がある? no/ \yes 元気がある? むねがひいひい? yes/ \no no/ \yes 様子をみる 氷枕で病院 解熱剤で病院 速攻で病院
このような判断を行うための情報は、yesの木 と noの木の2つの枝を持つデータである。これは2分木と同じであり、このような処理は以下のように記述ができる。
struct Tree { char *qa ; struct Tree* yes ; struct Tree* no ; } ; struct Tree* dtree( char *s , struct Tree* l , struct Tree* r ) { struct Tree* n ; n = (struct Tree*)malloc( sizeof( struct Tree ) ) ; if ( n != NULL ) { n->qa = s ; n->yes = l ; n->no = r ; } return n ; } void main() { struct Tree* p = dtree( "38.5℃以上の発熱がある?" , dtree( "胸がひぃひぃ?" , dtree( "速攻で病院",NULL,NULL ) , dtree( "解熱剤で病院",NULL,NULL ) ) , dtree( "元気がある?" , dtree( "様子をみる",NULL,NULL ) , dtree( "氷枕で病院",NULL,NULL ) ) ) ; // 決定木をたどる struct Tree* d = p ; while( d->yes != NULL || d->no != NULL ) { printf( "%s¥n" , d->qa ) ; scanf( "%d" , &ans ) ; // 回答に応じてyes/noの枝に進む。 if ( ans == 1 ) // yesを選択 d = d->yes ; else if ( ans == 0 ) // noを選択 d = d->no ; } // 最終決定を表示 printf( "%s¥n" , d->qa ) ; }
コンパイラと言語処理系
2分木の応用の構文木について、この後説明を行うが、構文木を使うコンパイラなどの一般知識を事前に説明しておく。
高級言語で書かれたプログラムを計算機で実行するソフトウェアは、言語処理系と呼ばれる。その実行形式により
- インタプリタ(interpreter:翻訳)
- ソースプログラムの意味を解析しながら、その意味に沿った処理を行う
- コンパイラ(compiler:通訳)
- ソースプログラムから機械語を生成し、実行する際には機械語を実行
- トランスコンパイラ
- ソースから他の言語のソースコードを生成し、それをさらにコンパイルし実行
最初のC++の実装では、C++をトランスレータにかけてC言語を生成し、C言語のコンパイラで動かしていた。
- ソースから他の言語のソースコードを生成し、それをさらにコンパイルし実行
- バイトコードインタプリタ
- ソースからバイトコード(機械語に近いコードを生成)、実行時にはバイトコードの命令に沿った処理を行う
- エミュレーター
- 異なるCPUのコンピュータで、システムの動作や機能を模倣して動かすシステム。
近々の例であれば、AppleのARMベースM1チップで intel CPU の動きを真似て動作させる Rosetta2 がトピック。パソコンで古いファミコンのソフトを動かすといった技術もエミュレータ。- 同じCPUで異なるOSを動かす場合は、CPU仮想化。
- 異なるCPUのコンピュータで、システムの動作や機能を模倣して動かすシステム。
に分けられる。
コンパイラが命令を処理する際には、以下の処理が行われる。
- 字句解析(lexical analysys)
文字列を言語要素(token)に分解 - 構文解析(syntax analysys)
tokenの並び順に意味を反映した構造を生成 - 意味解析(semantics analysys)
命令に合わせた中間コードを生成 - 最適化(code optimization)
中間コードを変形して効率よいプログラムに変換 - コード生成(code generation)
実際の命令コードとして出力
バイトコードインタプリタとは
例年だと説明していなかったけど最近利用されるプログラム言語の特徴を説明。
通常、コンパイラとかインタプリタの説明をすると、Java がコンパイラとか、JavaScript はインタプリタといった説明となる。しかし、最近のこういった言語がどのように処理されるのかは、微妙である。
(( Java の場合 )) foo.java (ソースコード) ↓ Java コンパイラ foo.class (中間コード) ↓ JRE(Java Runtime Engine)の上で 中間コードをインタプリタ方式で実行
あらかじめコンパイルされた中間コードを、JREの上でインタプリタ的に実行するものは、バイトコードインタプリタ方式と呼ぶ。
ただし、JRE でのインタプリタ実行では遅いため、最近では JIT コンパイラ(Just-In-Time Compiler)により、中間コードを機械語に変換してから実行する。
また、JavaScriptなどは(というか最近のインタプリタの殆どPython,PHP,Perl,…は)、一般的にはインタプリタに分類されるが、実行開始時に高級言語でかかれたコードから中間コードを生成し、そのバイトコードをインタプリタ的に動かしている。
しかし、インタプリタは、ソースコードがユーザの所に配布されて実行するので、プログラムの内容が見られてしまう。プログラムの考え方が盗まれてしまう。このため、変数名を短くしたり、空白を除去したり(…部分的に暗号化したり)といった難読化を行うのが一般的である。
トークンと正規表現(字句解析)
規定されたパターンの文字列を表現する方法として、正規表現(regular expression)が用いられる。
((正規表現の書き方)) 選言 「abd|acd」は、abd または acd にマッチする。 グループ化 「a(b|c)d」は、真ん中の c|b をグループ化 量化 パターンの後ろに、繰り返し何回を指定 ? 直前パターンが0個か1個 「colou?r」 * 直前パターンが0個以上繰り返す 「go*gle」は、ggle,gogle,google + 直前パターンが1個以上繰り返す 「go+gle」は、gogle,google,gooogle
正規表現は、sed,awk,Perl,PHPといった文字列処理の得意なプログラム言語でも利用できる。こういった言語では、以下のようなパターンを記述できる。
[文字1-文字2...] 文字コード1以上、文字コード2以下 「[0-9]+」012,31415,...数字の列 ^ 行頭にマッチ $ 行末にマッチ ((例)) [a-zA-Z_][a-zA-Z_0-9]* C言語の変数名にマッチする正規表現
構文とバッカス記法
言語の文法を表現する時、バッカス記法(BNF)が良く使われる。
((バッカス記法)) <表現> ::= <表現1...> | <表現2...> | <表現3...> | ... ;
例えば、加減乗除記号と数字だけの式の場合、以下の様なBNFとなる。
((加減乗除式のバッカス記法)) <加算式> ::= <乗算式> '+' <乗算式> | <乗算式> '-' <乗算式> | <乗算式> ; <乗算式> ::= <数字> '*' <乗算式> | <数字> '/' <乗算式> | <数字> ; <数字> ::= [0-9]+ ;
上記のバッカス記法には、間違いがある。”1+2+3″を正しく認識できない。どこが間違っているだろうか?
このような構文が与えられた時、”1+23*456″と入力されたものを、“1,+,23,*,456”と区切る処理が、字句解析である。
また、バッカス記法での文法に合わせ、以下のような構文木を生成するのが構文解析である。
+ / \ 1 * / \ 23 456
理解度確認
- インタプリタ方式で、処理速度が遅い以外の欠点をあげよ。
- 情報処理技術者試験の正規表現,BNF記法問題にて理解度を確認せよ。
コンパイラの技術と関数電卓プログラム(1)
コンパイラを作るための技術の基礎を学んでもらうために、 簡単な関数電卓プログラム作成を課題とする。 基本は、printf( “%d” , eval( “1+2*3”) ) みたいな計算プログラムを作成する。
計算式から、計算処理を行う場合、演算子の優先順位を正しく処理できることが求められる。
一般的には、計算の機械語を生成する場合、データを準備して計算という方法であり、 逆ポーランド記法変換が行われる。
たとえば、”1+2×3″は、”1,2,+,3,×” といった表記に改められ、変換後の式は スタックを用いて、「値はpush,演算子はpop,pop,計算して,push」という 単純なルールで処理すれば、計算を行うことができる。
字句解析と構文解析
このような、計算式の処理を実行する場合、”1“,”+“,”2“,”✳︎“,”3“という 字句に切り分ける処理を、字句解析という。 この結果から、”式✳︎式”なら掛け算、”式+式”は足し算といった、 前後の字句の組合せから、構文木を生成する処理は、構文解析という。 コンパイラであれば、この後、最適化、コード生成などが行われる。
C言語であれば、コンパイル前後には以下の処理が行われる。
- プリプロセッサ処理
↓(#の無いCコード) - コンパイル処理
- 字句解析 — 今回の実験
- 構文解析 — 今回の実験のキモ
- コード生成
↓(中間コード)
- リンク処理 ← ライブラリ
↓ - 機械語
字句解析と正規表現
字句(トークン)の切り出しでは、その文法を説明する際には、正規表現などを用いる。
今回の実験の後半では、正規表現に合わせたプログラミングツール(flex)を用いるが、前半では正規表現に合わせた文字列処理をプログラムする必要がある。
簡単な正規表現 . 任意の文字 * 直前の0回以上の繰り返し + 直前の1回以上の繰り返し [0-9a-z] カッコの中の文字のいずれか - は範囲を示す。 (a|b|c) 丸カッコの|区切りのうちのどれか。 (例) C言語の変数の正規表現 [a-zA-Z_][a-zA-Z0-9_]*
構文解析の方法
構文解析では、構文を状態遷移として考え、この式の後にくる可能性のある状態は? という考えで解析を行う。 このような構文は、一般的にバッカス・ナウア記法(BNF)などで表現されることが多い。 また、簡単な構文解析であれば、
などが用いられる。
再帰下降パーサ
簡単な再帰下降パーサの演習として、1桁の数字と+,*演算子の処理プログラムを 考える。
加減,乗除の式のバッカス記法(BNF) exp_加減 ::= exp_乗除 '+' exp_乗除 | exp_乗除 '-' exp_乗除 | exp_乗除 (注意)このBNFは若干間違っているよ!! ; exp_乗除 ::= DIGIT '*' exp_乗除 | DIGIT '/' exp_乗除 | DIGIT ; DIGIT ::= [0-9] ;
練習として、上に示す再帰下降パーサに、 (1) “(“,”)” を含めた場合の、BNF 記法を考え、(2) 数値として複数桁が使えるようにし、 (3) 式を読みやすくする空白を処理できるように 改造せよ。
副問合せと相関副問合せと集約関数
SQLの副問い合せ
前節の結合処理は時として効率が悪い。このような場合は、副問い合わせを用いる場合も多い。
SELECT S.業者名, S.所在 FROM S WHERE S.業者番号 IN ( SELECT SG.業者番号 FROM SG WHERE SG.商品番号 = 'G2' AND SG.在庫量 >= 200 ) ;
- 実験環境で副問い合わせ(学内のみ)
まず、『◯ IN { … }』 の比較演算子は、◯が{…}の中に含まれていれば真となる。また、SQLの中の (…) の中が副問い合わせである。
この SQL では、副問い合わせの内部には、テーブル S に関係する要素が含まれない。この場合、副問い合わせ(商品番号がG2で在庫量が200以上)は先に実行される。
{(S1,G2,200),(S2,G2,400),(S3,G2,200),(S4,G2,200)}が該当し、その業者番号の{S1,S2,S3,S4}が副問い合わせの結果となる。最終的に SELECT … FROM S WHERE S.業者番号 IN {‘S1′,’S2′,’S3′,’S4’} を実行する。
相関副問い合わせ
SELECT G.商品名, G.色, G.価格 FROM G WHERE 'S4' IN ( SELECT SG.業者番号 FROM SG WHERE SG.商品番号 = G.商品番号 ) ;
- 実験環境で相関副問い合わせ(学内のみ)
この副問い合わせでは、内部に G.商品番号 が含まれており、単純に()内を先に実行することはできない。こういった副問い合わせは、相関副問い合わせと呼ばれる。
処理は、Gのそれぞれの要素毎に、副問い合わせを実行し、その結果を使って WHERE節の判定を行う。WHERE節の選択で残った結果について、射影で商品名,色,価格が抽出される。
// 概念の説明用に、C言語風とSQL風を混在して記載する for( int i = 0 ; i < sizeofarray( G ) ; i++ ) { SELECT SG.業者番号 FROM SG WHERE SG.商品番号 = G[i].商品番号 を実行 if ( WHERE 'S4' IN 副query の結果が真なら ) { printf( ... ) ; } } // 全てのG 副queryの結果 WHERE 射影 // G1 -> {S1,S2} // G2 -> {S1,S2,S3,S4} -> ◯ -> (ノート,青,170) // G3 -> {S1} // G4 -> {S1,S4} -> ◯ -> (消しゴム,白,50) // G5 -> {S1,S4} -> ◯ -> (筆箱,青,300) // G6 -> {S1}
特殊な条件演算子
WHERE 節の中で使える特殊な条件演算子を紹介する。
... AND ... WHERE S.業者番号 <= 100 AND S.業者番号 >= 200 ; ... OR ... WHERE S.業者番号 >= 100 OR S.業者番号 <= 200 ; NOT ... WHERE NOT S.業者番号 >= 100 ; ... IN ... WHERE S.業者番号 IN ( 'S1' , 'S4' ) ; ... BETWEEN A AND B WHERE S.優良度 BETWEEN 50 AND 100 ; ... LIKE ... WHERE S.業者名 LIKE 'A_C社' ; _ は任意の1文字 ABC社 ADC社 WHERE S.業者名 LIKE 'A%社' ; % は任意の0~N文字 A社, AA社 ABC社 ... IS NULL WHERE S.業者名 IS NULL WHERE S.業者名 IS NOT NULL
集約関数
集約関数は、SQL の SELECT の射影部分で使える関数で、出力対象となった項目に対して、COUNT(),SUM(),AVG()といった計算を行うもの。
COUNT() - 項目の数 SUM() - 項目の合計 AVG() - 項目の平均 MAX() - 項目の最大値 MIN() - 項目の最低値 SELECT COUNT(S.業者番号) FROM S WHERE S.優良度 > 20 ;
- 実験環境で集約関数(学内のみ)
集合計算
複数の SQL の結果に対し、集合和, 集合積, 集合差などの処理を行う。
... UNION ... 集合和 ... EXPECT ... 集合差 ... INTERSECT ... 集合積 SELECT S.業者名 FROM S WHERE S.所在 = '福井' UNION SELECT S.業者名 FROM S WHERE S.所在 = '東京'
- 実験環境で集合計算(学内のみ)
演習課題
SQLの実験環境を使って、自分で考えたSQLの命令を2つ実行すること。実行した命令とその意味を説明し、出力された結果と一致することを確認すること。
さらにこの実行と同じ結果が出力される様なC言語のプログラムを作成し、おなじく結果を確認すること。
考察として、SQLで書いたプログラムとCで書いたプログラムの違いや便利な点や、Cでのプログラムの速度を早めるにはどう書くと良いかを比較検討すること。