プログラムはなぜ動くのか 第2版 知っておきたいプログラムの基礎知識を週末に読んでみた。
自分が最近つまづくポイントがだいぶスッキリした。
目次
1.CPUとは何か
2.データを2進数でイメージする
3.コンピュータが小数点数の計算を間違える理由
4.四角いメモリーを丸く使う
5.メモリーとディスクの親密な関係
6.自分でデータを圧縮してみよう
7.プログラムはどんな環境で動くのか
8.source fileから実行可能fileができるまで
9.OSとapplicationの関係
10.アセンブリ言語からプログラムの本当の姿を知る
11.ハードウェアを制御する方法
12.コンピュータに「考え」させるためには
1.CPUとは何か
キーワード
- プログラム:computerに実行させる処理の順番を示すもの
- プログラムの実態:命令とデータ
- マシン語:CPUが直接解釈できる言語
- 実行時のプログラムはどこに格納されているか:メモリー
- memoryのaddress:メモリー上で命令やデータが格納されている場所を示す値
- プログラムを解釈・実行する装置:CPU
CPU
4つの要素からなる
- レジスタ:処理対象となる命令やデータを格納する領域、メモリーの一種
- 制御装置:メモリー上のデータや命令をレジスタによみ出し、命令の実行結果に応じてコンピュータ全体を制御する
- 演算装置:メモリーからレジスタに読み出されたデータを演算する
- クロック:CPUが動作するタイミングとなるクロック信号を発生させるもの
レジスタが重要!!!!!!!!!
なぜレジスタが重要か。
- アセンブリ言語とマシン語が一対一に対応している
- 高水準言語で記述したプログラム =>(コンパイル) アセンブリ言語に =>(アセンブリ) マシン語に
- アセンブリ言語はレジスタを使って処理している
=> つまり、高水準言語で書いたプログラムは、突き詰めていくとCPUの「レジスタ」を使ってデータの格納、加算を行っている
レジスタ
- アキュムレータ:演算を行うデータ、演算の結果のデータを格納する、基本一個
- フラグ・レジスタ:演算処理後のCPUの状態を格納する、基本一個
- プログラム・カウンタ:次に実行される命令が格納されたメモリーのアドレスを格納する、基本一個
- ベース・レジスタ:データ用のメモリー領域の先頭アドレスを格納する
- インデックス・レジスタ:ベース・レジスタからの相対アドレスを格納する
- 汎用レジスタ:任意のデータを格納する
- 命令レジスタ:命令そのものを格納する、基本一個
- スタック・レジスタ:スタック領域の先頭アドレスを格納する、基本一個
レジスタに格納される値
- 命令
- データ
- 演算に使われる値
- メモリーのアドレスを表している値
値の種類により、格納するレジスタの種類が異なるので上記のようにいろんなレジスタがある
プログラムの流れ
- 順次進行:アドレスの値の順に命令を実行すること
- 条件分岐:条件に応じて任意のアドレスの命令を実行すること
- 繰り返し:同じアドレスの命令を何度か繰り返し実行すること
①プログラムをハード・ディスクからメモリーにコピーして、レジスタの一つであるプログラム・カウンタに0100を設定
②CPUが一つの命令を実行すると、プログラム・カウンタの値が自動的に一つ増加
③CPUの制御装置はプログラム・カウンタの値を参照して、メモリーから命令をよみ出して実行する
条件分岐
ジャンプ命令:直前に行われた演算の結果を参照して、ジャンプするかどうかを判断する
if A {
hoge
} else {
fuga
}
aaaa
上記だとA
を評価してtrueだったらhoge,falseだったらfugaが、実行される
この時、以下みたいな感じでメモリーに命令が読み出されている
0102: falseだったら0104番地にジャンプせよ
0103: hogeを実行して、0105番地にジャンプせよ
0104: fuga
0105: aaaa
関数呼び出し
- コール命令:関数の入り口のアドレスをプログラム・カウンタに設定する前に、関数呼び出しの次に実行すべき命令のアドレスをスタックに保存する
- リターン命令:スタックに保存されたアドレスをプログラム・カウンタに設定する機能を持っている
高水準言語のプログラムをコンパイルすると、関数呼び出しがコール命令に変換され、関数を終了する処理がリターン命令に変換される
ベースとインデックスで配列を実現
配列のように特定のメモリー領域を区切って連続的に参照するためには、2つのレジスタを使ったほうが便利
- ベース・レジスタ:連続の領域のメモリーの始めのアドレス
- インデックス・レジスタ:配列のインデックスに相当するもの
CPUはベース・レジスタ+インデックス・レジスタの値を実際に参照するメモリーのアドレスとして解釈
レジスタがやっていること
- データ転送命令:レジスタとメモリー、メモリーとメモリー、レジスタと周辺装置の間でデータを読み書きする
- 演算命令:アキュムレータで算術演算、論理演算、比較演算、シフト演算を行う
- ジャンプ命令:条件分岐、繰り返し、無条件のジャンプをおこなう
- コール/リターン命令:関数を呼び出す/呼び出し元に戻る
2.データを2進数でイメージする
キーワード
- 32bit: 4byte
- 01011100(2)は10進数に変換すると: 92
- 00001111(2)を2桁左シフトすると元の数を何倍?: 4倍
- 補数表現であらわされた8桁の11111111(2)は10進数でいくつか?: -1
- 補数表現であらわされた8桁の10101010(2)は16桁の2進数で表すと?: 1111111110101010
- グラフィックスのパターンを部分的に反転させるために使う論理演算: XOR演算
コンピュータは全てのデータを2進数の値として取り扱い、情報を演算する
なぜコンピュータはデータを2進数で取り扱うのか?
- コンピュターの内部はIC(集積回路)により構成されている
- ICは直流電圧0Vか5Vの二つの情報しか表せない
=> ICの特性が2進法とあっていた
bitとbyte
- bit: 情報の最小単位、2進法の1桁に相当
- byte: 8桁の2進数のこと。8桁なのはコンピュータで取り扱う情報の単位が8桁だから。情報の基本単位。
基本的にメモリーやディスクにはバイト単位でデータが格納され、バイト単位でデータが読み書きされる
マイナスの数の表示(補数)
- 補数を求める変換方法: 反転して+1
- オーバーフロー:演算結果の桁数がレジスタのサイズを超えたことを表す。コンピュータは溢れた桁は無視する。
シフト演算
シフト演算:2進数で表される数値の桁を左右にシフトする演算。左シフト、右シフトがある
論理右シフトと算術右シフト
- 論理右シフト:シフト後の上位桁に0を入れる。
- 算術右シフト:2進数の値を符号がある数値として演算する場合はシフト前の符号ビットの値を入れる。
符号拡張
例:8桁の2進数を16桁、32桁の2進数に変えること
算術右シフトと同様に行えばよい。
算術演算と論理演算
- 算術演算:加減乗除の四則演算
- 論理演算:2進数の各桁の0と1を個別に取り扱う演算
- 論理否定(NOT演算): 0を1に、1を0に
- 論理積(AND演算): 両者が1の場合に演算結果が1
- 論理和(OR演算): 少なくともどちらか一方が1の場合に演算結果が1
- 排他的論理和(XOR演算): 両者が異なる場合に演算結果が1
3.コンピュータが小数点数の計算を間違える理由
キーワード
- 2進数の0.1は10進数でいくつ:0.5
- 小数点以下3桁の2進数で10進数の0.625を表せるか:表せる
- 小数点数を符号、仮数、基数、指数という4つの部分に分けて表す形式: 浮動小数点数
- 2進数の基数: 2
- 表せる範囲の中央の値を0とみなすことで、符号bitを使わずにマイナスの値を表す方法: イクセス表現
- 10101100.01010011という2進数は16進数でいくつ: AC.53
0.1を100回加えても10にならない
- 有限な機械であるコンピュータは、無限に続く循環小数を取り扱えない。
- 無限小数はビット数に合わせてカットされる
- 同じ理由で小数の計算をコンピュータはしばしば間違える
浮動小数点数
- 倍精度浮動小数点数型(64bit): double type
- 単精度浮動小数点数型(32bit): float type
浮動小数点数
±m×n exp(e)
- 符号: ±, 1bit
- 仮数: m, 52bit/23bit, 小数点以上の値を1に固定する正規表現
- 基数: n
- 指数: e, 11bit/8bit, excess表現が用いられる
正規表現とexcess表現
仮数部での正規表現:
- 統一的に数を表す工夫。
- シフトを使えば可能。
- このルールを適用することで、この1を実際にデータに格納する必要がない(bitを1つ節約できる)
指数部でのexcess表現:
- 符号bitを使わずにマイナスの値を表すための工夫。
- 指数部で表せる範囲の中央値を0とみなす。例:0~255 => -127~128に変換される
コンピュータの計算間違いを回避するためには
(1) 間違いを無視する: 一般に近似値が得られれば十分な場合が多い
(2) 小数点数を整数に置き換えて計算する: 何倍かして整数値にして答えを後で調整する
2進数と16進数
- 2進数が一般的に使われる(bit単位でデータを表す場合に便利)
- 2進数だと桁数が大きくなってしまい、見た目にわかりづらいのが難点
- 実際では16進数がよく用いられる
- 桁数が1/4になる
4.四角いメモリーを丸く使う
キーワード
- アドレス信号ピンを10本もったメモリーICで指定できるアドレスの範囲: 0000000000~1111111111
- 高水準型言語のデータ型:メモリー領域を占有するサイズとそこに格納されるデータの形式
- 32bitでメモリーアドレスを表す環境はポインタとなる変数のsize:32bit
- 物理的なメモリーの構造と同様なのは何バイトのデータ型:1byte
- LIFO方式でデータを読み書きするデータ構造:スタック
- データの大小に応じてリストが2方向に枝分かれするデータ構造:2分探索木(バイナリー・サーチ・ツリー)
メモリーの物理的な仕組み
- 実体はメモリーIC
- メモリーIC: 電源、アドレス信号、データ信号、制御信号(RD,WR)を入出力するための多くのピンがあり、アドレスを指定して、データを読み書きするようになっている
- 電源をつなぎ、データの格納場所をA0~A9のアドレス信号で指定し、データの値をD0~D7のデータ信号に入力し、WR信号を1にすればメモリーICの内部にデータが書き込まれる
- 読み出す場合は、RD信号を1にする
メモリーの論理的イメージはビルディング
- 1フロアに1バイトのデータを格納でき、フロアの番号がアドレスを示す
- メモリーにはデータ型が存在
- データ型によって 物理的なメモリーを読み書きするサイズを変えられる
ポインタ
- C言語の大きな特徴、データ型がポイント
- データの値そのものではなく、データが格納されているメモリーのアドレスを持つ変数の事
- 任意のアドレスを指定してデータの読み書きが可能に
- ポインタの宣言では変数の前にアスタリスクを置く
char *d; //charは1byte
short *e; //shortは2byte
long *f; //longは4byte
d,e,fに100を代入するとdを使うと100番地のみ、eを使うと100,101番地のデータを、fを使うと100~103のデータを読み書きできる
スタックとキュー、さらにリング・バッファ
スタックとキュー:アドレスを指定せずに配列の要素を読み書きできるもの
一時的に保存したい場合にこれらの手法でメモリーを用いる
違い
データを出し入れする順番
- スタック:LIFO(Last In First Out 後入れ先出し)方式
- キュー:FIFO(First In First Out 先入れ先出し)方式
あらかじめスタックやキューのための領域をメモリー内に確保し、書き込みや読み出しの順番を決めておくことで、アドレスやインデックスの指定が不要になる
プログラムで扱うためには
- データを格納するための配列を適当な要素数で宣言し、それらを読み書きするための関数のペアを作成しておく
- これらの関数の内部では配列を読み書きするためにインデックスの管理をすることになる
- 関数を使う側からは配列の存在やインデックスのことを心配しなくて良い
- これらの関数を使えばデータを一時的に保存、必要になった時に読み出すことができる
- スタックにデータを書き込む関数を
Push
、引数に書き込むデータを指定 - スタックからデータを読み出す関数を
Pop
、関数の戻り値として読み出されたデータが返される - キューにデータを書き込む関数を
EnQueue
、引数に書き込むデータを指定 - キューからデータを読み出す関数を
DeQueue
、関数の戻り値として読み出されたデータが返される
stack(スタック) 干し草を積んだ山という意味
- LIFO方式
- スタックとなる配列に格納された最後のデータが最初に取り出される
- データを一時的に退避し、後で元どおりに復元する場合に使う
queue(キュー) 待ち行列
- FIFO方式
- キューとなる配列に最初に格納された値が最初に取り出される
- データの入力と処理のタイミングを調整するために使う仕組み
- 通信で送られてくるデータを処理する場合や、同時に実行されている複数のプログラムから送られてくれるデータを処理する場合に使う
リング・バッファ
- 要素数6この配列を使ってキューを実現したとする
- 配列の先頭から順番にデータを格納する
- データは格納された順序で取り出される
- 配列の末尾にデータを格納したらその次にデータは配列の先頭に格納する
=> これによって、配列の末尾が配列の先頭に繋がっていてぐるぐる回りながらデータの格納と取り出しを繰り返すようなイメージになる!!
リストと2分探索木
- どちらもインデックスの順序とは無関係に配列の要素を読み書きするためのもの
リスト
- 配列にデータを追加したり削除したりする処理を効率的に行える
- 配列の個々の要素にデータの値だけでなく、次の要素のindexも付加することで実現する
- データの値と次の要素のindexを組み合わせて配列の一つの要素にする
- 配列の要素は数珠繋がりのリストに成る
- リストの末尾の要素にはそのあとにつながる要素がないのでindexの値としてありえない値(-1)を格納する
- 「次の要素のindex」を変更すれば、物理的にメモリー上には残っているが論理的にリストから途中の要素を削除できる
- 途中に追加するときも同様に「次の要素のindex」を変更して追加可能
- 配列だと途中の要素を削除したり追加したりするときにその後ろにある要素を全て移動しなければならない
2分探索木
- データの探索がとても効率的に行える
- リストをさらに工夫し、配列に要素を追加するときにその大小関係を考慮して左右二つの方向に分岐させるもの
- 配列の個々の要素にデータの値と二つのindex情報を持たせれば良い
まとめ
- 配列が基本
- プログラムを工夫することで、データをいろんな方法で扱える
5.メモリーとディスクの親密な関係
キーワード
- ストアド・プログラム方式: 記憶装置にプログラムを格納し、逐次実行する方式
- メモリーを使って、ディスクのアクセス速度を向上させる仕組み: ディスク・キャッシュ
- ディスクの一部を仮想的にメモリーとして使う仕組み: 仮想記憶(仮想メモリー)
- Windowsに置いてプログラムの実行時に動的に結合されている関数やデータを格納したファイル: DLL(DLLファイル) Dinamic Link Library
- プログラムのexeファイルの中に、関数を静的に結合すること: スタティック・リンク
- Windowsパソコンに置いて一般的なハード・ディスクの1セクター: 512byte
メモリーとディスクの機能はプログラムの命令やデータを記憶するという点で同じ
メモリー:電気的な記憶(高速、高価、小容量)
ディスク:磁気的な記憶(低速、安価、大容量)
プログラムの実行
- ストアド・プログラム方式
- ディスクにあるプログラムがメモリーに「ロード」されてからCPUに「実行」される
ディスクに記憶されたプログラムがそのまま実行することはできないのか?
- 無理ぽ
- CPU:プログラムの内容を解釈して実行するところ
- CPUは内部にあるプログラム・カウンタでメモリーのアドレスを指定して、読み出すから
- 仮にディスクからできるようにしても読み出し速度が低速なので微妙
ディスク・キャッシュ
- 一度ディスクから読み出されたデータを保存しておくメモリー内の領域
- 次に読み出されるときは実際のディスクではなく、ディスク・キャッシュの内容を読み出す
- この仕組みにより、ディスクのデータのアクセス速度を向上させる
(1)初めて読み出す場合はディスクから(低速)
(2)一度読み出したデータを保存
(3)同じデータを再度読み出す場合はメモリーから(高速)
ディスクをメモリーの一部として使う「仮想記憶」
- 仮想記憶: ディスクの一部を仮想的にメモリーとして使うもの
- ディスク・キャッシュが仮想的なディスク(実体はメモリー)に対し、仮想記憶は仮想的なメモリー(実体はディスク)
- 仮想記憶によりメモリーが不足している状態でもプログラムの実行が可能
- CPUはメモリーにロードされたプログラムしか実行できないので、実際のメモリーの内容とディスク上の仮想記憶の内容をスワップしながら(部分的に置き換えながら)実行する
ページング方式とセグメント方式
ページング方式
- 実行されるプログラムをその構造に関係なく一定の大きさの「ページ」に分割し、ページ単位でメモリーとディスク間の置き換えを行う方式
- ページイン:ディスクの内容をメモリーに読み出すこと
- ページアウト:メモリーの内容をディスクに書き込むこと
- 仮想記憶により発生するページインやページアウトは低速なディスクのアクセスを伴うので根本的な解決にはならない
セグメント方式
実行されるプログラムを処理やデータの集合など意味のある単位にまとめたセグメントに分割し、セグメント単位でメモリーとディスク間の置き換えを行う
メモリー節約術
DLL
- プログラムの実行時にライブラリが動的に結合されるもの
- 複数のアプリケーションが同じDLLファイルを共有することでメモリーの節約に
- スタティック・リンクだと同じものが複数できてしまい、メモリーの無駄。
- EXEファイル(実行可能なアプリケーションファイル)を変更せずにDLLファイルだけバージョンアップできる
_stdcall呼び出しでプログラムのサイズを小さくする
- スタックのクリーンアップ処理は関数の呼び出し側に付け加えられる
- スタックのクリーンアップ処理:関数の引数を受け渡すために使われるメモリー上のスタック領域の中から不要となったデータを削除すること
- 何度も同じ関数を呼び出すということは、この同じ処理がメモリーに何度も読み出される、ということ = 無駄
- 呼び出される側に書いておけば一つだけになり、プログラム全体のサイズは小さくなる
ディスクの構造
セクター方式とバリアブル方式があるが、一般的にセクター方式が採用されている
- セクター方式:固定長の領域に区切る
- バリアブル方式:可変長の領域に区切る
セクター方式
- セクター: トラックと固定長サイズに区切った領域
- トラック:ディスクの表面を同心円状に区切った領域
- 物理的に読み書きする最小単位がセクターだが、論理的に読み書きする単位はセクターの整数倍の「クラスタ」
- 1セクター: 512byte
- 1クラスタのサイズはハード・ディスクの容量に応じて変わる
- どんなに小さなファイルでも1クラスタの領域を占有する
クラスタのサイズを小さくするとどうなる?
- ディスクのアクセス回数が増え、結果的にファイルの読み書きスピードが遅くなる
- ディスクの表面にはセクターの区切りを示す領域も必要なので、クラスタのサイズを小さくしすぎるとディスク全体の記憶容量が減る
=> セクターやクラスタのサイズは処理速度と記憶容量のバランスをとって決められている
6.自分でデータを圧縮してみよう
キーワード
- ファイルにデータが記憶される基本単位:1byte(8bit)
- DOC, LZH, TXTの中で圧縮ファイルの拡張子はどれ:LZH
- ランレングス法とハフマン法: ランレングス法が「データの値×繰り返し回数」でファイルの内容を表すことで圧縮する技法
- シフトJISコードという文字コードでは半角英数の1文字は何byte:1byte(8bit)
- BMP(ビットマップ)形式の画像ファイルは圧縮されているか:圧縮されていない
- 可逆圧縮と非可逆圧縮の違い:圧縮されたデータを元どおりに戻せるのが可逆圧縮、戻せないのが非可逆圧縮
ファイルはバイト単位で記録
- ファイル: ディスクなどの記憶媒体にデータを格納したもの
- プログラムによってファイルに格納されるデータの単位はbyte
ランレングス法
- 「データの値×繰り返し回数」でファイルの内容を表すことで圧縮する技法
- ファクシミリの画像圧縮に用いられる
欠点
- 実際は同じ文字が何度も続いていることは滅多にない
- 文書ファイルにはあっていない
=> 1文字単位ではなく、文字列単位の繰り返しを用いるなど工夫すれば可能
ハフマン法
- 出現頻度の高いもののデータ長を短く、低いものを長くする
- 圧縮対象となるファイルごとに最適な符号体系を構築し、それを元に圧縮を行う
- どのデータにどの符号(ハフマン符号)を割り当てるかはファイルにより異なる
- 圧縮されたファイルにはハフマン符号の情報と圧縮されたデータの両方が格納される
- ハフマン木を使えば一意に符号体系を決められる
可逆圧縮と非可逆圧縮
- 画像ファイルなどはぼやけても許容範囲であれば非可逆圧縮を用いる場合がある
- JPEG, TIFF, GIF形式など
7.プログラムはどんな環境で動くのか
キーワード
- アプリケーションの動作環境:OSとコンピュータ本体(ハードウェア)の種類
- MacOSはAT互換機でも動作するか:動作しない
- WindowsアプリケーションはMacOS上で動作するか:動作しない
- FreeBSDが提供するPorts:アプリケーションをソースコードで提供し、環境に合わせてコンパイルすることで実行可能にする仕組み
- Macintooshで利用できるWindows環境のエミュレータ:Virtual PC for Mac
- Java仮想マシンの役割:バイトコードとなったJavaアプリケーションを実行すること
動作環境が違うとは?
動作環境が異なると、なぜアプリケーションが動かないのか?
動作環境
- OS(Operation System)
- ハードウェア(CPUの種類が特に重要)
CPU
- CPU固有のマシン語しか解釈できない
- CPUの種類が異なると、解釈できるマシン語の種類が異なる
- マシン語になっているプログラム:ネイティブ・コード
- プログラマが作るテキストファイル:ソースコード、どの環境でも表示/編集可能
- ソースコードをコンパイルすることでネイティブコードが得られる
OS
- OSごとにアプリケーションからOSへの命令の仕方が違う
- アプリケーションからOSへの命令の仕方を定めたもの:API(Application programming Interface)
- OSごとにAPIが異なるので同じアプリケーションを他のOS用に作り直す場合にはアプリケーションがAPIを利用している部分を書き換えなければならない
- APIはOSが同じならどのハードウェアでも基本的に同じ
- CPUの種類が違う場合は、それぞれのCPUに合わせてネイティブ・コードを生成するコンパイラを使ってソースコードをコンパイルし直す必要がある
8.source fileから実行可能fileができるまで
キーワード
- CPUが解釈・実行できる形式のプログラム:ネイティブ・コード(マシン語コード)
- 複数のオブジェクト・ファイルを結合してEXEファイルを生成するツール:リンカー
- 拡張子が
.obj
となったオブジェクト・ファイルの内容はソースコードorネイティブ・コード?:ネイティブ・コード - 複数のオブジェクト・ファイルをまとめて収録したファイル:ライブラリ・ファイル
- WindowsのDLLファイルに格納された関数の呼び出し情報だけを持つファイル:インポート・ライブラリ
- プログラムの実行時にデータやオブジェクトのために動的に確保されるメモリー領域:ヒープ
コンパイラがソースファイルをコンパイルして実行可能ファイルを作成する
=>プログラムの作成〜実行までの流れ
(1)ソースファイルがコンパイルされて実行可能ファイルになる
(2)実行可能ふぁいるがメモリーにロードされて実行される
コンピュータはネイティブ・コードしか実行できない
- ソースコード:何かのプログラミング言語で記述したプログラム
- ソース・ファイル:ソースコードをファイルとして保存したもの
- CPUが直接解釈・実行できるのはネイティブ・コードのプログラムだけ
- ダンプ:ファイルの内容を1byteずつ2桁の16進数で表示すること
コンパイラ
- コンパイラ:高水準言語で記述されたソースコードをネイティブ・コードに翻訳する機能を持ったプログラム
- コンパイラはソースコードを記述する言語に応じて専用のものが必要
- コンパイラは言語の種類だけでなく、CPUの種類に応じて専用のものが必要
コンパイラだけでは実行可能ファイルは生成できない
- リンク:ネイティブ・コードのファイルを実行可能なEXEファイルにする処理
- コンパイル後に生成されるのは
.obj
拡張子のオブジェクト・ファイル - 複数のオブジェクト・ファイルを結合して1つのEXEファイルを生成する処理がリンク!
- リンカー:リンクを行うプログラムのこと
スタートアップとライブラリ・ファイル
- スタートアップ:全てのプログラムの先頭に結合する共通的な処理が記述されたオブジェクト・ファイル
- 従ってスタートアップと結合する処理は必須なのでリンクは必ず行われる
- ライブラリ・ファイル:複数のオブジェクト・ファイルをまとめて1つのファイルに格納したもの
- リンカーにライブラリ・ファイルを指定すると、必要なオブジェクト・ファイルだけを抽出してリンク、EXEファイルを生成する
リンカーのエラー
- 2つのライブラリ・ファイルを指定しないでリンクを行うとエラー
- 外部シンボルが未解決(目的の変数や関数が記述されたオブジェクト・ファイルが見つからなかったのでリンクできなかったということ)
- 外部シンボル:他のオブジェクト・ファイルの中にある変数・関数のこと
DLLファイルとインポート・ライブラリ
- APIのオブジェクト・ファイルがDLLファイルという特殊なライブラリ・ファイルに格納されている
- インポート・ライブラリ:オブジェクト・ファイルの実体ではなく、必要な関数がどのDLLファイルにあるか、とDLLファイルが格納されているフォルダの情報だけ記憶しているライブラリ・ファイル
- スタティック・リンク・ライブラリ:オブジェクト・ファイルの実体を格納し、EXEファイルに直接結合指定まう形式のライブラリ・ファイルの’こと
実行可能ファイルの実行に必要なこと
- EXEファイルは変数や関数が実際に何番地のメモリー・アドレスに格納されるかまでは決定していない
- EXEファイルの中で変数や関数に仮に与えられているメモリー・アドレスをプログラムの実行時に実際のメモリー・アドレスに変換する
- 再配置情報が付加されている
- 再配置情報:リンカーがEXEファイルの先頭に付加する、メモリー・アドレスの変換が必要な部分を示す情報、相対アドレスになっている
- 相対アドレス:起点となるアドレスからのオフセット(相対距離)
- 個々の変数のメモリー・アドレスは変数のグループの先頭を起点としたオフセットで表すことができる
ロード時に作られるスタックとヒープ
EXEファイルがロードされたメモリー領域の要素
- 再配置情報
- 変数のグループ
- 関数のグループ
- スタック(<--NEW!)
- ヒープ(<--NEW!)
スタック
- 関数の内部で一時的に使用される変数(ローカル変数)や関数を呼び出すときの引数を格納するためのメモリー領域
- 関数が呼び出されると確保され、関数の処理が終わると自動的に解放される
ヒープ
- プログラム実行時に任意のデータやオブジェクトを格納するためのメモリー領域
- プログラマが随時に明示的に確保と解放を行う
- プログラム言語によって手段はさまざま
- メモリー・リーク:明示的にヒープを解放しなかったために、メモリー領域が確保されたままになっている状態
- メモリー・リークが蓄積してしまうといずれメモリーが不足してコンピュータがダウンする
用語集
- コンパイラ:実行前にソースコード全体を解釈して処理
- インタプリタ:実行時にソースコードの内容を1行ずつ解釈して処理する
- 分割コンパイル:プログラム全体を複数のソースコードに分けて記述し、それらを別々にコンパイルして最後に一つのEXEファイルにリンクすること
- ビルド:コンパイルとリンクを続けて行うこと
- オーバーレイ・リンク:同時に実行されることのない関数を、同じアドレスに交代でロードして実行するもの
- ガベージ・コレクション:処理が終わって不要となったヒープ領域のデータやオブジェクトを破棄し、そのために使われていたメモリー領域を確保すること。自動的に行ってくれる言語があり、プログラマのうっかりミスでメモリー・リークとなることを予防している
9.OSとapplicationの関係
キーワード
- モニタープログラムの主な機能:プログラムのロードと実行
- OSの上で動作するプログラム:アプリケーションまたは応用プログラム
- OSが提供する機能を呼び出すこと:システム・コール
- Windows Vistaは何bitOSか?:32bit(64bitもある)
- GUI:Graphical User Interface
- WYSIWYG: What You See Is What You Get、ディスプレイに表示されたものをそのままプリンタで印刷できる
アプリケーション:特定の業務を効率化してくれるプログラムのこと
歴史から見るOSの機能
- モニター・プログラム:プログラムをロードする機能と実行する機能だけを備えたもの
- 基本的な入出力プログラムはほとんどに共通しているので切り分け、モニター・プログラムに追加した(初期のOSの誕生)
- ハードウェア制御プログラム、言語プロセッサ(アセンブラ、コンパイラ、インタプリタ)、さまざまなユーティリティなどの機能が追加されて現在のOSに
- OSとは複数のプログラムの集合体
OSの恩恵
- OSが動作している環境で実行されるアプリケーションはハードウェアを直接制御せずに、OSを介して間接的にハードウェアを制御している
システム・コールと高水準言語の移植性
- システム・コール:アプリケーションがOSの機能を呼び出すこと
- 高水準言語で記述されたアプリケーションをコンパイルするとシステム・コールを利用するネイティブ・コードに成る(高水準言語は特定のOSに依存しないから)
OSと高水準言語がハードウェアを抽象化してくれる
- OSが提供するシステム・コールによりプログラマはハードウェアを直接制御するプログラムを記述する必要がなくなる
- 高水準言語を使うことで、システム・コールの存在を意識する必要さえなくなる
=> OSと高水準言語により、ハードウェアを抽象化できる
ここでの抽象化とは、ハードウェアの構造、データの読み出し方などを意識しなくても、ファイル作成などという概念を採用することで、人間の言葉で書いて命令して実行できることを指している
OSの特徴
マルチタスク機能
- マルチタスク:複数のプログラムを同時に実行する機能
ネットワーク機能やデータベース機能
- 両者はOS本体に不可欠な機能ではない
- OSに近い存在なので、ミドルウェアと総称される
- OSとミドルウェアをまとめて、システム・ソフトウェアと呼ぶ
10.アセンブリ言語からプログラムの本当の姿を知る
キーワード
- ネイティブ・コードの中に命令にその機能を表す英語の略称をつけたもの:ニーモニック
- アセンブリ言語のソースコードをネイティブ・コードに変換すること:アセンブルする
- ネイティブ・コードをアセンブリ言語のソースコードに逆変換すること:逆アセンブルする
- アセンブリ言語のソース・ファイルの拡張子:
.asm
- アセンブリ言語のプログラムにおけるセグメント:プログラムを構成する命令やデータをまとめたグループのこと
- アセンブリ言語のジャンプ命令はなんのために使うか:プログラムの流れを任意のアドレスに移す
アセンブリ言語はネイティブ・コードと1対1に対応
- ニーモニック:個々のネイティブ・コードに機能を表した英語の略語のこと
- アセンブリ言語:ニーモニックを使うプログラミング言語のこと
- アセンブラ:アセンブリ言語で記述されたソースコードをネイティブ・コードに変換するプログラム
- アセンブルする:変換すること
- アセンブラとコンパイラは、ソースコードをネイティブ・コードに変換するという機能において同様
- アセンブリ言語で記述されたソースコードとネイティブ・コードは1対1対応
- 逆アセンブラ:ネイティブ・コードからアセンブリ言語のソースコードに逆変換すること
- 逆アセンブルする:逆変換すること
コンパイラでアセンブリ言語のソースコードを出力
- コンパイラのオプションに指定することでアセンブリ言語のソースコードを出力可能
ネイティブ・コードに変換されない擬似命令
アセンブリ言語のソースコードの構成要素
- ネイティブ・コードに変換される命令(後述するオペコード)
- アセンブラ自体に対する命令(擬似命令):プログラムの構造やアセンブルの方法をアセンブラに指示するものでネイティブ・コードに変換されない
- セグメント:プログラムを構成する命令やデータをまとめたグループに名前をつけたもの
- _DATA: 初期化されたデータのセグメント
- _BSS: 初期化されていないデータのセグメント
- _TEXT: 命令
- セグメント:プログラムを構成する命令やデータをまとめたグループに名前をつけたもの
アセンブリ言語の構文
- オペコード:命令の動作
- オペランド:命令の対象(ない場合もある)
グローバル変数のための領域は常に確保されている
- グローバル変数:関数の外側で定義された変数
- ローカル変数:関数の内側で定義された変数
_DATAセグメント
- ラベル:セグメントの先頭からの相対的位置を表している
ローカル変数のための領域は一時的に確保される
- ローカル変数はレジスタやスタックを使って一時的に用意される
- 関数の内部で利用したスタックは関数の処理が終了した時に元の状態にもどってしまうので、ローカル変数の値が破棄される
- レジスタが空いていればレジスタが使われ、空いてなければスタックが使われる(レジスタの方がアクセスが大幅に速い)
マルチスレッド処理でのバグ
int counter = 100;
void MyFunc1(){
counter *= 2;
}
void MyFunc2(){
counter *= 2;
}
mov eax, dword ptr [_counter] ; counterの値をeaxレジスタい読み込む
add eax,eax ; eaxレジスタの値を2倍する
mov dword ptr [_counter], eax ; eaxレジスタの値をcounterに格納する
- マルチスレッド処理では1行の処理が実行されるごとに、他のスレッドに処理が切り替わる可能性がある
- タイミングにより、MyFunc1()関数がcounterの値100を読み出し、それを2倍した値200をcounterに書き込む前にMyFunc2()関数がcounterの値100を読み出したらcounterの値は200になってしまう
- ロックという手法を使えば回避できる
- ロック:ソースコードの行単位でスレッドの切り替えを禁止すること
11.ハードウェアを制御する方法
省略
12.コンピュータに「考え」させるためには
キーワード
- コンピュータを使った模擬実験:コンピュータ・シミュレーション
- 擬似乱数:数式によって生成される擬似的な乱数
- 乱数の種:擬似乱数を生成する数式の中で使われるパラメータ
- コンピュータには自らものを考える機能があるか:ない
- コンピュータには自らものを覚える機能があるか:ある
- AI: Artificial Intelligence
プログラムを使う目的
- 人間が道具として使う:書類作成など効率よくするために使う
- 人間の思考過程をプログラムによって代行すること
以上