N年前に中学校で書いた卒論をサルベージしたので、せっかくだから公開したいと思います。
間違っているところも多々あると思いますが、歴史を感じるためにあえてそのまま掲載しています(とはいえ間違っている情報を掲載しっぱなしなのも心苦しいので、ぜひ間違っているところはコメント欄で教えてください)
添付資料(コード等)は紛失したので、ないです(かなしい) 見つかったのでアップロードします
目次
Ⅰ.研究の動機 3
Ⅱ.プログラミング言語とは 3
1.C言語とは 4
2.アセンブリ言語とは 4
Ⅲ.CPUの基本内部構造 5
1.レジスタ 5
A.プログラマが操作できるレジスタ 5
B.プログラムから操作できないレジスタ 6
2.制御装置 7
3.演算装置 7
4.クロック 7
Ⅳ.メモリとアドレス 7
1.スタックの説明 8
Ⅴ.実験の予想 9
Ⅵ.実験 9
1.実験の方法 9
2.実験に使うプログラムの説明 10
3.実験結果の検証 11
Ⅶ.実験の考察 16
- 関数を始めるとき、なぜ使わない空き地を作るのか 17
- 関数を始めるとき必ず行う処理について 18
- なぜ2数の比較や演算の時に、わざわざ片方をeaxに格納するのか 19
- なぜ「aから下位4バイト」と指定するのか 19
- _RTC_CheckEsp関数の定義と存在意義 20
Ⅷ.実験のまとめ・感想 21
おわりに 21
参考文献 23
はじめに
もはや我々の生活に欠かすことのできないコンピュータ。パーソナルコンピュータ(パソコン)はもちろん、電卓や自動車、最近ではコンピュータに関係なさそうな、冷蔵庫などの各種家電製品にまでコンピュータが内蔵されている。その中に入っている膨大なデータを、コンピュータはどうやって電気だけで処理しているのだろうか。その仕組みに一歩近づくため、コンピュータと人間をつなぐ一つの手段、プログラミング言語について、コンピュータ側での働きを調べてみようと思った。
今回は開発環境が整っているパソコンのプログラムについて調べた。快適さや娯楽を与えてくれるそれらについてよく知り、使いこなす助けになればよいと思う。
Ⅰ.研究の概要
学校の部活でC言語をやっていて興味が大きいので、この研究ではC言語の仕組みを取り上げる。しかしC言語は人間の言葉に近いプログラミング言語であるため、コンピュータの処理とのつながりが分かりにくい。C言語よりコンピュータの処理をそのまま表しているアセンブリ言語にいったん翻訳し、コンピュータの働きを調べる。
Ⅱ.プログラミング言語とは
コンピュータにはCPUという演算装置と、メモリという記憶装置があり、メモリに記憶されたプログラムの通りにCPUが計算処理を行うことによってユーザーの元に反応を返している。プログラムはマシン語、つまり0と1の集まりによって記憶されているが、人が読み書きするのに不便なため、人間でも読みやすいプログラミング言語で書き、実行時に翻訳させている。
今回使用するのはC言語とアセンブリ言語の2つである。
1.C言語とは
C言語は1972年に米国ベル研究所が主体となって作った高級プログラミング言語のひとつである。翻訳にはコンパイラ方式を採用しており、一挙に翻訳しそのまま実行しているので、速度の速いプログラムが組める。また、高級言語でありながらアドレス計算やポインターなどの機能を備えている、などの特徴から使用人口が非常に多い。
2.アセンブリ言語とは
アセンブリ言語は低水準プログラミング言語の一つで、基本的にマシン語一つに一つずつニーモック(英略語)が当てはめられている。アセンブリ言語で書かれたプログラムをマシン語プログラムに変換する事をアセンブルする、その逆を逆アセンブルすると言い、変換を行うプログラムをアセンブラと言う。GAS文法とMASM 文法があり、多少異なっているが今回使用した逆アセンブラではMASM文法で出力される。
Ⅲ.CPUの基本内部構造
今回C言語と比較するアセンブリ言語を読み、またプログラムが動作する仕組みを知る上で大前提となる構造を調べた。4つの要素に大別され、相互に電気的に接続されている。
1.レジスタ
処理対象となる命令やデータを格納する、一種のメモリのような領域。CPUによって個数や種類、サイズが異なる。
プログラムはここを対象として記述されているため、本論文において最も重要な構造である。役割ごとに大別し、説明上使うレジスタだけを、それを指すアセンブリ語の英略語も含めて説明する。
A.プログラマが操作できるレジスタ
*名前(英略語) 役割
・アキュムレータ(eax)演算に使う。一つしかない。
・ベース・レジスタ(ebx)メモリ・アドレスを格納する
・カウント・レジスタ(ecx)ループ回数のカウントを行う
・データ・レジスタ(edx)データを格納する
・ソース・インデックス(esi)データ転送元のメモリ・アドレスを格納する
・ディスティネーション・インデックス(edi)データ転送先のメモリ・アドレスを格納する
・ベース・ポインタ(ebp)データの格納領域の起点のメモリ・アドレスを格納する
・スタック・ポインタ(esp)スタック領域の最上位に積まれたデータのメモリ・アドレスを格納する
B.プログラムから操作できないレジスタ
・スタック・レジスタ スタック領域の先頭アドレスを格納する
・プログラム・カウンタ 次に実行する命令が格納されたメモリのアドレスを格納する。
CPUが一つ命令を実行するたびに値が増えていき、プログラムの流れを決める。一つしかない。
・フラグ・レジスタ 演算処理後のCPUの状態を格納する。比較などに使う。
2.制御装置
メモリ上の命令やデータをレジスタに読み出し、命令の実行結果に応じてコンピュータ全体を制御する。
3.演算装置
上で読みだされたデータを演算する。
4.クロック
CPUが動作するタイミングとなるクロック信号を発生させる。
Ⅳ.メモリとアドレス
メモリとは、正確に言うとメイン・メモリ(主記憶)のことで、CPUと制御チップなどを通してつながっていて、命令やデータを格納する。1バイト(8ビット)ごとにアドレスという番号がついていて、CPUはこのアドレスを指定してメモリからデータを読み出したり、書き込んだりしている。Ⅲ章でよくレジスタに格納されているメモリ・アドレスとは、この番号のことである
1.スタックの説明
プログラムの実行時には、メモリ上にスタックと呼ばれるデータ領域が確保される。参考資料一枚目の模式図をご覧いただきたい。この領域では、データはメモリの下から上に向かって格納され、上から下に向かって取り出される。イメージとしてはシャッターが側面についている物置である。esp(スタック・ポインタ)レジスタに格納されているメモリ・アドレスがデータの一番上(アドレスの値が小さい方)を表していて、プッシュ(push)命令やポップ(pop)命令を使うと、データが格納・取り出しされるだけでなく、自動的にespレジスタの値も更新される。そのためデータを操作する際、プログラマがメモリ・アドレスを指定しなくてもよいのが利点であり、実験のサンプルプログラムでも積極的に利用されている。このようなデータの格納順序を、LIFO方式という。
Ⅴ.実験の予想
以上の知識をもとに、プログラムの流れを想像することができる。プログラムを実行すると、exeファイルのコピーがメモリ上に作成され、クロック信号に合わせて制御装置がそこから命令やデータを読み出す。 レジスタの数には限りがあるので、一時的に記憶しなければならないデータはスタックに格納されるのだろう。また、スタックのデータは順番に格納、取り出しをしなければならないので、関数やプログラムが始まる前と終わった後で残っているデータは同じでなければならないだろう。実際に実験をして、アセンブリ語のコードにそれが現れているかどうか確認していこうと思う。
Ⅵ.実験
1.実験の方法
コンピュータ内部の情報の動き方を知りたいのだが、コンピュータが読んでいるマシン語を研究するのは難しい。そこで、マシン語に近いアセンブリ言語を使用することにする。C言語でプログラミングをし、実行(.exe)ファイルを作成。Visual C++ 2008に付属の逆アセンブリ機能で、そのexeファイルをアセンブリ語に翻訳。その上で一つ一つ検証していく。
2.実験に使うプログラムの説明
今回サンプルとして使用するプログラムは、
プログラム内に書かれた2数(ここでは14と18)の差を求める働きをするものである。添付の資料の2枚目がC言語でのソースだ。簡単に仕組みを説明しよう。
文中によく見られるint <文字>とは、変数の定義(数値を代入するメモリの確保)であり、簡単に言うと数学で「<文字>を整数とする」という意味である。
{}で囲まれている部分が2つある。これはC言語の文法の一つで「関数」と言う。関数は呼び出された際、実引数の値を仮引数にコピーした後、決められた処理を行い、最後に返値を返す。
返値型 関数名(仮引数){処理}で定義でき、
関数名(実引数);で呼び出す。
具体的に見てみよう。C言語ではmainという名前の付けられた関数が最初に実行される。mainにおいて上の関数diffがc=diff(14,18);で呼び出されている。実引数が14,18の順に書かれているのでa=14,b=18とそのまま代入される。If else文は、()内の条件(今回はa>=b)が正ならばifのほうの処理へ、誤ならばelseのほうへ進む。ここではa=14<b=18なのでelseの処理、return b-a;に進む。returnは次に書かれたものを返値として元の式に代入する。つまりc = diff (14,18);の右項がb-a=18-14=4となり、cが4となる。結果的に、cとして18と14の差が求まるということになる。c = diff(14,18);の14と18の部分に別の2数を入れても差を求めることができる。
3.実験結果の検証
では、実際にプログラムを比べてみることにする。添付資料の3,4枚目の左半分は、先ほどのプログラムから逆アセンブリで出したアセンブリ語のソースである。途中にはさんである緑字部分は、元のプログラム(C言語)のどの部分かということを示している。右半分は、アセンブリ語の文法に基づいて私が日本語に直訳したものである。4枚目のmain関数の前半から、3枚目のdiff関数、その後4枚目のmain関数の後半という風に、プログラムの流れを追って検証する。参考資料5~9枚目にスタックの中身、主要なレジスタの内容物の途中経過について図解したので、併せて見ていただきたい。
まず(ア)(エ)(オ)(カ)では諸レジスタの値をプッシュしている。これは、main関数が終わった後、つまりプログラム終了後に、諸レジスタの値を元に戻せるように状態を保存していると思われる。
また、(ウ)でespの値を204減算しているのは、図1のようにデータの一番上とされているアドレスをずらすことで、ずらした分だけ空き地を作っているのである。この空き地については後ほど考察するが、図2(ク~コ)を見ると、(イ)(キ)を経てediに代入したアドレスを使って、この空地をCで埋めていることが分かる。
図3(サ)(シ)では、今回比較する2つの数字14,18をスタックに代入して、関数diffに引き渡す準備をしていることが分かる。
14,18を引き渡したら、(ス)のcall 命令でdiff関数に処理が移るので、3枚目のdiff関数のソース検証に移る。図はプログラムの進行順に描いているので、そのまま次を見て頂きたい。
図4では、main関数を始めた時と同様に(A)(D)(E)(F)で諸レジスタの状態を保存し、(B)(C)(G)で空き地を作って、(H~J)で空き地をCで埋めていることが分かる。この仕組みについては前述の通りである。
次に、図5(K)(L)では、(シ)(サ)でスタックに入れておいた2数のうちa(14)を演算用eaxレジスタに入れ、b(18)と比べることによって、どちらからどちらを引くべきか決めている。eax(つまりaがb以上の場合はそのまま次の行(N)に進み、(P)で00C613D6つまり(T)にジャンプするので、図6の処理だけが行われる。逆にbのほうが大きい場合は00C613D0、つまり(R)に飛び、図7の処理だけが行われる。
今回はbのほうが大きいので、図7の処理に注目する。図6の処理についてはだいたい図7と同じなので割愛する。
まずbのデータ(大きい方)をeaxに入れ(R)、sub命令でそこからaを引く(S)。その結果はeaxに上書きされる。つまりeaxには18-14で4が入った状態になる。
計算処理が終わったら関数を終了する準備に入る。(F)(E)(D)(A)でスタックに保存したレジスタの状態を、(T)(U)(V)(X)でそれぞれ元のレジスタに反映させている。ほかに、(W)でespのアドレスを(B)の時のebp、つまり空き地を作る前のアドレスと同じにすることで、pop命令なしに、作った空き地を消している。この二つの処理が終わると、各レジスタの値がdiff関数開始前と等しくなり、またスタックの状態も図9のようにdiff関数開始前と同じになることが分かる。
(Y)のret命令で処理がmain関数の(セ)に戻るので、そこから説明しよう。ソースは4枚目に戻る。図10を見ていただきたい。
(セ)ではespの値に8を加算することによって、diff関数に14,18を引き渡すために使っていた領域を削除している。この手法はdiff関数の(W)で空き地を消すためにも使ったものである。
(ソ)でメモリのcの領域にeax、つまり先ほどの計算結果を代入した後、(タ)では、同じ数でビットごとに排他的論理和をとると0になることを利用して、eaxの値を0にしている。ちなみに排他的論理和とは、2数をビットごとに比べ、同じ数だったら0、違う数だったら1にしたものである。
(チ)(ツ)(テ)(ネ)は言うまでもなく、diff関数の(T)(U)(V)(X)と同じレジスタの初期化処理であり、図11の(ト)も(W)と同じく空き地を削除する処理である。
Diff関数と少し違うのは(ナ)(ニ)(ヌ)であり、これはespとebp(初期のesp)を比較することによって、これまでの処理の中でスタックから取り出す順序を間違えるなどして無駄なデータがたまっていたり、関係のないデータまで取り出してしまったりしていないか確認するためであると考えられる。詳しくは後ほど考察する。
最後に(ノ)のret命令でmain関数を抜け出せば、このプログラムは終了する。
Ⅶ.実験の考察
今回の実験で不思議、または特徴的だと思ったことについてより考えていこうと思う。
関数を始めるとき、なぜ使わない空き地を作るのか
関数を始めるとき必ず行う処理について
なぜ2数の比較や演算の時に、わざわざ片方をeaxに格納するのか
なぜ「aから下位4バイト」と指定するのか
_RTC_CheckEsp関数の定義と存在意義
以上のことについて考察する。
関数を始めるとき、なぜ使わない空き地を作るのか
(ウ)(C)で、espの値を減算することによって空き地を作っているが、(ク~コ)(H~J)でなぜかCで埋めている他には、関数内で使っていない。ではなぜ作り、Cで埋めているのだろうか。
これは関数内だけで使う変数(ローカル変数)のための領域であると考えられる。レジスタが空いていればレジスタが使われるが、他のことに使っていたりして空いていないこともあるため、スタックに一時的に領域を確保しているのである。ローカル関数が定義した関数内でしか使えないのはこのためだと思われる。
では、なぜCで埋めるのだろうか。これはこの領域の長さが不正に長く、アクセスしてはいけないところにアクセスしてしまっている(オーバーフロー)際に、アクセス違反と認識しプログラムを落とすためである。オーバーフローしているところにデータを代入するとアクセス違反になることを利用している。これを行わないと、データを読み込むのはアクセス違反にならないので、オーバーフローした際に妙なデータを読み込んでしまい、誤った答えを出してしまうことがあるので、重要な処理である。
関数を始めるとき必ず行う処理について
diff関数、main関数のどちらでも、関数を始めるときにebpレジスタの値をスタックに格納し、空き地を作った後、他のレジスタの値も格納している。また、関数を終わる前に、諸レジスタへスタックに保存した値を代入し、空き地を消している。前述の通り関数開始前の状態に戻すためである。これらの処理はどんな関数を実行するときでも必要になるであろうことが分かる。
なぜ2数の比較や演算の時に、わざわざ片方をeaxに格納するのか
アセンブリ語のソースを見ていると、aとbを直接比較・減算しているのではなく、片方(減算の場合は大きい方)をeaxに代入しているのが分かるが、なぜ直接a、bを使わないのだろうか。これは、sub命令で処理した結果(差)は1つ目の数に入るためである。具体的に言うと、xが18、yが14として、sub x yを実行すると、xが4になり、yは14のままになる、ということである。関数の返値はeaxレジスタで渡す仕組みになっているので、bからaを引き、その結果を再びeaxに代入するより、1つ目の数bをeaxに入れておいて、そこからsub命令でaを引くほうが手間は少ないと思われる。
なぜ「aから下位4バイト」と指定するのか
aを指定するだけではいけないのは、aというアドレスはデータの先頭のある1ビットだけを指しているため、データの全体を指しているわけではないからである。
4バイトと指定する理由は簡単である。単にint(整数)型のデータが4バイト分だからである。そのためint型では-2147483648~2147483647の整数しか入れられないのである。
_RTC_CheckEsp関数の定義と存在意義
_RTC_CheckEsp関数は、Visual Studio C++から提供されている、オーバーフローを検出してくれる補助機能である。1.で空き地を埋めたときと同じで、オーバーフローが起こってしまうとこのプログラムだけでなくメモリのほかの領域のデータまで破壊してしまうことがあるので、その危険性がないか検出している訳である。しかし、この関数は破壊を検出することはできても防ぐことはできないので、(ヌ)でespの値を一応元に戻しているのであると考えられる。
Ⅷ.実験のまとめ・感想
プログラムが始まる前と終わった後でレジスタやスタックの状態が同じだという予想は合っていたが、結果の逆アセンブリコードを見ると半分くらいそのための処理だったのには驚いた。また、コンピュータは順々に命令をこなすだけで後のことを考えないので、無駄な処理をたくさん行っていて驚いた。
例えば、今回は変数を使わないため必要のなかった空き地をわざわざ作ったり消したりしていた。時間がもったいないと感じた。
調べてみると,使わない変数の値を求めないなどして、そのような無駄な処理をしない実行ファイルを作れるコンパイル方法もあるらしい。
おわりに
論文を作成するにあたって、今まで文法を丸暗記して作ってきたプログラムについて、より深く論理的に理解することができた。また、プログラムを使うとき、コンピュータの内部でどのような処理が行われているのかが分かり、処理速度を上げるために改善すべき点なども見えてくるようになった。結局人間が工夫して処理速度を上げなければならないのであり、コンピュータをうまく動かすには人間の工夫が重要なのだと再認識した。
実験の考察については、調べても情報がなかったものが多数あったにもかかわらず、自分で考えて仕組みを考えられたところが多かったことは良かった。しかし、高度に専門的な分野なので、全くの初心者には分かりにくい話ばかりしてしまったことは少し残念である。書き方については努力したのだが、もっと身近に、分かりやすい説明の仕方を考えられたら良かった。
次に研究することがあったら、文や絵の表示、ファイル保存関係など、よく使う処理について、もっと具体的に話を展開できたらいいと思った。
つたない文章でしたが、ここまで読んでいた
だいた皆様に深く感謝いたします。
サンプルコード
サンプルプログラム
int diff(int a, int b){
if(a>=b)return a-b;
else return b-a;
}
void main(){
int c;
c = diff(14,18);
}
参考文献
MMGames『苦しんで覚えるC言語』秀和システム(2011)
大槻有一郎『ゼロからのC言語 ゲームプログラミング教室 入門編』株式会社ラトルズ(2010)
広内哲夫『C言語入門』ピアソン・エデュケーション(2002)
矢沢久雄『プログラムはなぜ動くのか 第2版』日経BP社(2007)