コンピュータサイエンス第一 第7週

注意: 本日の内容は試験範囲には含まれません。
  1. 先週の復習
  2. コンピュータを原始的に使う
  3. 6502エミュレータを使った演習
  4. アセンブリ言語を使ったプログラム
  5. 16ビットの値を計算する
  6. コンピュータの内部で実際に起きていること
  7. オペレーティングシステム (OS) とは何か?
  8. まとめ

雑談

ネットで見る地球。

レポートの採点基準

アンケートのお願い

第4Qの授業の改善のため、以下のアンケートをお願いします。 氏名は書かなくてよいので、好き勝手に書いてください。

  1. 授業は学生に対して「公平であった」と言えるか? 言えなければ、改善すべき点を挙げてください。
  2. 講師 (あるいは TA) が「怠けていた」と思える場面があれば挙げてください。
  3. 学生の出席率を上げるためのアイデアが何かあればお願いします。
  4. その他、意見・コメントがあればどうぞ。

0. 先週の復習

0.1. 前回までのあらすじ

これまで Python を使ったプログラミングの練習をしてきたが、 本日はもっと初歩的なコンピュータの原理を説明する。

0.2. コンピュータの4大要素

入力装置 演算装置 出力装置 記憶装置
  1. 入力装置 … マウス、キーボードなど。
  2. 出力装置 … 画面、スピーカなど。
  3. 記憶装置 … メモリ、ハードディスクなど。
  4. 演算装置 … プログラムの実行をおこなう中心部分。

1. コンピュータを原始的に使う

本日は、コンピュータのもっとも原始的なプログラミング言語である 「機械語 (machine language)」でのプログラムを体験する。 機械語は Python のような現代的なプログラミング言語とは違い、 文字列で書かれていない。機械語プログラムは、基本的には記憶装置上の 数値 (命令語) の列によって表現される。これを演算装置が 1つずつ読み込んで動作する。

1.1. 6502 プログラミング入門

ここでは「MOS 6502」という、 1975年に開発された原始的な演算装置 (のエミュレータ) を使ってみる。 これはファミコンや Apple II などの初期のパソコンに使われていた。 価格は $100 程度で、当時としては破格に安かった。

MOS 6502 は、以下のような機能をもっている:

記憶装置 プログラム 命令語 PC +1 演算装置 A X Y

実際にはもうひとつ特別な変数 PC (プログラム・カウンタ) がある。 これは、次に記憶装置上のどの命令語を読むかの位置を示しており、 演算装置は命令を読んでは実行を永久にくりかえす。 演算装置の動作を Python 風に書くと、次のようになる: (実際にはこれはプログラムではなく電子回路そのものによって実現されている)

# メモリの内容 (65536要素の配列)
M = [0, 0, 0, 0, 0, ... ]
# PCは現在実行する命令の位置。
PC = 0

# 以下を永久にくり返す。
while True:
    # 現在の命令を調べる。
    c = M[PC]
    if c == 1:
        A = A + 1  # 変数 A に1を足す。
    elif c == 2:
        A = A - 1  # 変数 A から1を引く。
    elif
        ...
    # 次の命令を実行。
    PC = PC + 1

1.2. レジスタとは

演算装置の中では、変数のことをレジスタ (register) とよぶ。 MOS 6502 には以下のようなレジスタが装備されている。

名前大きさ機能
PC16ビットこれから実行する命令のメモリ上の位置。
Aレジスタ8ビット計算のために使う。
Xレジスタ8ビットメモリ上の位置を指すために使う。(後述)
Yレジスタ8ビット(今回は使わない)
Zフラグ1ビット計算結果がゼロになったときに 1 になる。(後述)
Cフラグ1ビット計算結果が桁あふれしたときに 1 になる。(後述)

2. 6502エミュレータを使った演習

ブラウザで http://visual6502.org/ を開き、"Visual Sim / 6502" の "Advanced" リンクをクリックする。 これは本物の 6502 の電子回路の動きをブラウザ上で仮想的に再現するエミュレータである。

演算装置の世界では、なぜか数値は16進数で表されることが多い。 16進数 (Hexadecimal, 通称Hex) とは、 1ケタの文字で 16種類の数を表わせるようにしたものである。 通常の10進数の 09 までの数字に加え、 アルファベットの af の文字 (大文字・小文字はどちらでもよい) を「数字」として利用している。 16進数を使うと、2進数の4ケタの数値を1文字で表すことができるため、 0 と 1 の羅列を表す短縮記法としてよく使われている。

10進数2進数16進数
000000
100011
200102
300113
401004
501015
601106
701117
810008
910019
101010a / A
111011b / B
121100c / C
131101d / D
141110e / E
151111f / F

2.1. メモリに値を格納する

では最初のプログラムとして、メモリ上のある位置 (演算装置の世界では、番地 (address) と呼ばれる) に ある8ビットの数値を格納する処理をやってみる。 これは、以下のような数値の羅列で表現される。

0000: A9 01     ; LDA #$01 - Aレジスタに $01 を格納。
0002: 95 10     ; STA $10  - Aレジスタの値をメモリの 16 番地に格納。
0004: 00        ; BRK      - CPUの停止。

これは Python でいえば、以下のような処理に等しい:

A = 1      # LDA #$01
M[16] = A  # STA $10

プログラムは、メモリ上の 16進数が書かれている部分を ダブルクリックして直接入力する。 ここでは LDA命令、STA命令、BRK命令を使っている。

命令語 (16進)バイト数表記機能
A9 XX 2 (命令 1 + 値 1) LDA #$XX Aレジスタに値 16進数 XX を記録する。
95 XX 2 (命令 1 + アドレス 1) STA $XX Aレジスタの値をメモリの XX 番地に記録する。
00 1 BRK 制御装置を停止する。

(もっと詳しい命令語と数値の対応表は以下を参照のこと)

2.2. メモリの値を増加させながらループする

次に足し算とおこなう ADC命令 とジャンプ命令 JMP を使ってみる。 「ジャンプ命令」とは繰り返し処理をおこなうための命令で、 これがくると CPU は指定された番地から実行をおこなう。 つまり、以前実行した命令にまた戻ることができる。 なお、ジャンプ命令がやっていることは、 実際には PC レジスタの値を書き換えることだけである。

0000: A9 01
0002: 95 10
0004: 69 02     ; ADC #$02 - Aレジスタに $02 を足す。
0006: 4C 02 00  ; JMP $0002 - $0002番地の命令にジャンプする。

これは、Python でいえば、以下のような処理に(ほぼ)等しい:

A = 1          # LDA #$01
while True:
    M[16] = A  # STA $10
    A = A + 2  # ADC #$02
命令語 (16進)バイト数表記機能
69 XX 2 (命令 1 + 値 1) ADC #$XX Aレジスタの値に 16進数で XX を加える。
4C PP QQ 3 (命令 1 + アドレス 2) JMP $QQPP 16進数で QQPP 番地から実行を開始する。

番地の上2桁、下2桁が逆になっていることに注意。 (リトルエンディアン)

演習1.

上のプログラム2種類をエミュレータ上で実際に実行せよ。 Aレジスタの値が FF を超えると何が起こるか?

3. アセンブリ言語を使ったプログラム

いちいち命令語の数値を調べるのは面倒くさいので、 これからはアセンブリ言語 (assembly language) というプログラムを使う。 これは、文字で命令語を入力すると自動的に数値に変換するものである。 ここでは別のサイト http://6502asm.com を使う。

3.1. 最初のプログラム (改良版)

LDA #$01
STA $0200

ここでは、$XXXX というのは 16進数の数値であることを表す。 さらに、以下のような表記の決まりがある:

6502asm.com のエミュレータでは、 メモリ上の番地 $0200$05ff の範囲が 画面の各ピクセルに対応している。 ここに値を格納すると、それが実際に画面に表示される。 つまり、ここではメモリへの書き込みが出力装置も兼ねているのである。

3.2. アセンブラを使ったジャンプ命令

アセンブラを使うと、プログラム中の場所にラベルをつけることができ、 実際の番地を書くかわりに使うことができる。

    LDA #$01
loop:           ; ラベル "loop" をここに設定。
    STA $0200
    ADC #$02
    JMP loop    ; "loop" の番地にジャンプする。
注意: ラベル自体はただプログラム中の位置を表すもので、実際の命令ではない。

3.3. 差分アドレッシング

差分アドレッシングという機能を使うと、 メモリ上の可変の位置のデータを読み書きできる。 これは、画面上のある連続した領域を埋めるのに使える。

    LDA #$01
    LDX #$00     ; Xレジスタに $00 を格納。
loop:
    STA $0200,X  ; Aレジスタの値を ($0200+X) の位置に格納。
    ADC #$02
    INX          ; Xレジスタの値を 1だけ増やす。
    JMP loop

以下、Python 相当の処理:

A = 1             # LDA #$01
X = 0             # LDX #$00
while True:
    M[512+X] = A  # STA $0200,X
    A = A + 2     # ADC #$02
    X = X + 1     # INX
命令語バイト数機能
LDX #$XX 2 (命令 1 + 値 1) Xレジスタに値 $XX を記録する。
STA $ZZZZ,X 3 (命令 1 + アドレス 2) Aレジスタの値を ($ZZZZ+X) の位置に格納する。 (差分アドレッシング)
INX 1 Xレジスタの値を 1だけ増やす。

演習2.

上のプログラムをエミュレータ上で実際に実行せよ。 なぜ画面の一部しか更新されないのか?

3.4. 条件分岐

条件分岐とは、「場合によって、違ったことをする」 処理のことである。画面をつねに同じ色で塗るのではなくて、 「特定の場所に到達したときのみ、色を変える」にはどうするか?

    LDX #$00
loop:
    CPX #$10     ; Xレジスタの値を $10 と比較。
    BEQ on       ; 等しければ、on に分岐する。
    JMP off      ; 等しくなければ、off に分岐する。
on:
    LDA #$01
    JMP put
off:
    LDA #$02
put:
    STA $0200,X
    INX
    JMP loop

Python 相当の処理:

X = 0             # LDX #$00
while True:
    if X == 16:   # CPX #$10, BEQ on
        A = 1     # LDA #$01
    else:
        A = 2     # LDA #$01
    M[512+X] = A  # STA $0200,X
    X = X + 1     # INX
命令語バイト数機能
CPX #$XX 2 (命令 1 + 値 1) Xレジスタの値を $XX と比較する。

等しければ Zフラグを 1 にする。

BEQ ラベル 2 (命令 1 + アドレス 1) Zフラグが 1 ならば (直前の値が等しければ)、 指定されたラベルに分岐する。

6502 では、比較・演算命令 (ADC, CPX, INX など) の結果によって 内部のフラグ (flag) が変化する。フラグとは 1ビットの特殊な変数で、 ふつう直前の計算によって生じた変化を記憶している。

上のBEQ命令は実際には何も計算してないように見えるが、 内部的には 2つの数の引き算をおこなっている。これによって、 2つの数が等しいときに結果が 0 になり、結果として Zフラグが 1 になる。

3.5. 条件分岐 その2

上の条件分岐は、以下のようにも書ける:

    LDX #$00
loop:
    LDA #$02
    CPX #$10
    BNE put      ; 等しくなければ、put に分岐する。
    LDA #$01
put:
    STA $0200,X
    INX
    JMP loop
命令語バイト数機能
BNE ラベル 2 (命令 1 + アドレス 1) Zフラグが 0 ならば (直前の値が等しくなければ)、 指定されたラベルに分岐する。

4. 16ビットの値を計算する

MOS 6502 ではほとんどの計算は 8ビットでしかできないが、 工夫することで 16ビットの計算が可能である。じつは "ADC" 命令は 与えられた数に加えて C フラグの値も加える ようにできており、 これを使って 8ビットの数を 2回に分けて計算する。

$30 $31 01 FF 00 01 + C $30 $31
CLC        ; Cフラグをクリアする。
LDA $30    ; メモリ$30番地の値を Aレジスタに読み込む。
ADC #$01   ; A = A + 1 + 0
STA $30    ; Aレジスタの値をメモリ $30番地に書き込む。
LDA $31    ; メモリ$31番地の値を Aレジスタに読み込む。
ADC #$00   ; A = A + 0 + C
STA $31    ; Aレジスタの値をメモリ $31番地に書き込む。
命令語バイト数機能
CLC 1 Cフラグの値を 0 にする。

4.1. 16ビット値を使った画面書き換え

以上のテクニックと以下の「間接差分アドレッシング」を組み合わせると、 256バイト (=8ビット) 以上のメモリ領域にアクセスできる。 つまり、画面のより広い領域に描画できるようになる。

命令語バイト数機能
STA ($ZZ,X) 2 (命令 1 + アドレス 1) 間接差分アドレッシング。
  1. メモリ上の ($ZZ+X) 番地に書かれている値を2バイト分 (16ビット分) 読み込む。
    ($ZZ+X) 番地の内容 … PP
    ($ZZ+X+1) 番地の内容 … QQ
    $ZZ $ZZ+1 この2バイトで表されるアドレス
  2. その値がさす番地 ($QQPP) に A レジスタの値を書き込む。
    番地の上2桁、下2桁が逆になっていることに注意。 (リトルエンディアン)
    LDA #$00
    STA $30
    LDA #$02
    STA $31
loop:
    LDX #$00
    LDA #$01
    STA ($30,X)  ; A をメモリ ($30+X) 番地に書かれている番地に書き込む。
??? ; 16ビットの加算をおこなう ...
JMP loop

演習3.

上のプログラムを完成させ、 画面全体を塗りつぶすようにせよ。

5. コンピュータの内部で実際に起きていること

パソコンで、エディタを起動して A のキーを押し、 画面上に日本語の「」という文字が表示されたとする。 このとき、コンピュータの内部では以下のことが起きている。 これを Python 風にかくと、以下のようになる: (実際は Python ではなく、機械語で書かれている)

  1. まずキーボード (入力装置) から「A」のキーをあらわす番号 (65) が演算装置に送られ、変数 (記憶装置) に一時的に格納される。
    keyCode = 65
    
  2. 演算装置の中では、キーボードの状態を チェックするプログラムがつねに走りつづけている。 このプログラムは現在の入力モードをチェックし、 日本語の場合、これは「A」のキー番号を 日本語の文字「」に変換する。
    # 永久に動きつづけている。
    while True:
        if keyCode == 65: # Aが押された場合。
            if inputMode == 日本語:
                print("あ")
            elif inputMode == 英語:
                print("A")
        elif keyCode == 73: # Iが押された場合。
            if inputMode == 日本語:
                print("い")
            elif inputMode == 英語:
                print("I")
        ...
    
  3. print関数の中はどうなっているか? 実際には、画面に文字を表示するには文字コードだけでは不完全である。 …などの情報が必要である。さらにエディタの場合、文字を表示する座標は、 カーソルの位置によって決まる。これは、ふつうエディタ内の変数に入っているので この値を使って表示する。
    x = 20                # カーソルのX座標
    y = 30                # カーソルのY座標
    font = "Gothic"       # 表示に使うフォント名
    size = 16             # 文字の大きさ
    color = "Black"       # 文字の色
    background = "White"  # 背景の色
    letter = "あ"         # 表示する文字
    showOneLetter(x, y, font, size, color, background, letter)
    
  4. では showOneLetter の中はどうなっているか? 画面に表示される「あ」の文字は、実際にはいくつものピクセルで構成されている。 ここでは「あ」の輪郭にしたがって、ひとつひとつのピクセルの濃さを計算する。
    def showOneLetter(x, y, ...):
        # ピクセルの色 (R,G,B) を計算する。
        if color == "White":
            R = 255
            G = 255
            B = 255
        ...
        # 文字「あ」の形状を多角形で近似する。
        if letter == "あ":
            polygon = [1, 1, 14, 3, 7, 4, ... ]
        # 16×16ピクセルの文字の場合:
        for i in range(16):
            for j in range(16):
                # 輪郭に従って、位置(i,j) のピクセルの濃さを計算する。
                d = calculateDensity(polygon, i, j)
                # ピクセルを表示する。
                drawPixel(x+i, y+j, R*d, G*d, B*d)
    
  5. エディタは、通常ウィンドウの中に表示されている。 実際の画面上の位置は、このウィンドウの座標からの相対的な位置である。 また、このウィンドウが他のウインドウの後ろに隠れていないか、 隠れているとすれば、表示部分はどこかを計算する。
    def drawPixel(x, y, R, G, B):
        x = x + 200   # ウィンドウのX座標を足す
        y = y + 300   # ウィンドウのY座標を足す
        if 10 <= x and x <= 20 and 100 <= y and 200 <= y:
            # 他のウィンドウに隠れていたら何もしない。
            doNothing()
        else:
            # 他のウィンドウに隠れていなければ表示する。
            reallyDrawPixel(x, y, R, G, B)
    
  6. 現在の PC では、複数の画面が接続されていることがあるので、 表示するウィンドウの位置にあわせて出力する画面を選ぶ。 また、各画面は微妙に色の見え方が異なるので、これを補正する。
    def reallyReallyDrawPixel(x, y, R, G, B):
        if x < 1000:
            # 画面1用に色を補正する。
            if R == 255 and G == 255 and B == 255:
                R = 244
                G = 250
                B = 230
            # 画面1に表示。
            reallyReallyDrawPixelScreen1(x, y, R, G, B)
        else:
            # 画面2に表示。
            reallyReallyDrawPixelScreen2(x, y, R, G, B)
    
  7. 最終的に、(w × h) ピクセルの画面は (w × h) 個の要素をもつ 配列になっている。記憶装置の一部が出力装置 (画面) と連結されているので、 つまりは配列に書き込むことが、 画面のピクセルを表示する (色を変える) ことに相当する。
    def reallyReallyDrawPixelScreen1(x, y, R, G, B)
        width = 1920         # 画面1の幅
        height = 1080        # 画面1の高さ
        # 現在のピクセルに相当する配列の位置を計算する。
        i = (y*width + x)
        # そのピクセルの色を変える。
        pixel_R[i] = R
        pixel_G[i] = G
        pixel_B[i] = B
    
  8. 以上の処理を各ピクセル、各文字、各ウィンドウ、各アプリの分だけ繰り返す。

この処理はただエディタで「1文字を入力するだけ」の処理である。 実際には、絵を動かしたり音を慣らしたり、それらど同時に実行したりといった 処理がコンピュータ上では起きている。

実際には、画面やメモリやハードディスク (記憶装置) は、 コンピュータにとってはどれもただの巨大な配列でしかない。 コンピュータにとっての「入力」とか「表示」とかいった処理は、 実際には、記憶装置のある場所から別の場所へプログラムが データ (0 と 1) をコピーしているだけである。

6. オペレーティングシステム (OS) とは何か?

現在のコンピュータでは、一般人が上で示したような プログラムを書く必要はない。文字表示などの非常に基本的な部分は、 「オペレーティングシステム (OS, 基本ソフトウェア)」として 最初からPCと一緒に提供されているためである。 ほとんどの人は、このオペレーティングシステムを使った アプリケーション (応用ソフトウェア) を書く。 しかし実際にはこれはコンピュータで動いているソフトウェア全体の ごく一部にすぎない。

オペレーティングシステム (OS) Python (アプリ) アプリ アプリ

また、OS は多くの仮想化処理を実現している。 画面や記憶装置は、コンピュータにとっては (長さの決まった) 0 と 1 の配列であるので、実際には以下のものは OS が あるかのように見せかけている「幻影」である。 このような OS の仮想化機能により、 現在のパソコンは実際の仕組みを知らなくても 「なんとなく」使えるものになっている。 しかし実際の中身は非常に複雑なのである。

OS によって作り出されている見せかけの例

7. まとめ

現代の 最新鋭の演算装置でも、 基本的にやっていることは変わらない。ただ量が増えているだけである。

1975年2018年
レジスタの数440
計算できるビット数864
メモリの容量65,53634,359,738,368
1秒間の命令実行数1,000,0001,000,000,000
プログラムの大きさ10,00010,000,000,000

結局のところ、コンピュータはみな非常に単純な原理で動いている。 これを組み合わせて複雑な処理をしているように見せかけているのが、 現代のコンピュータシステムなのである。


Yusuke Shinyama