機械語と配列

CPUは機械語を入力すると、計算する電子部品です。機械語とは、CPUが直接理解できることのできる数値の並びで、CPUが実行することのできる特定の処理に対応するものです。

CPUの内部には「レジスタ」と呼ばれる高速な保存領域があり、およそ0.000000001秒に一回内容を読みだして計算したり、レジスタからレジスタにデータをコピーしたりすることができます。しかし、非常に限られた数(8個とか)しか記憶できませんし、どのレジスタから読みだすのかあらかじめ指定する必要があります。

メモリと呼ばれる部品がCPUに接続されており、CPUから受け取ったデータを保存したり、保存したデータをCPUに送り返したりできます。1000000000個くらいの数を保存できますが、レジスタと比べて100倍くらい遅く、0.0000001秒くらい読み出しに時間がかかることがあります。メモリには大量のデータの保存場所があるので、どの番号のデータを読み出すのかを伝えることで、保存されたデータを得ることができます。

機械で実行可能な命令には、例えば以下のようなものがあります。

CPUが実行する命令機械語の例意味
MOV R3, 40 3 44を決められたレジスタR3に保存
MOV R1, R31 1 3レジスタR3からレジスタR1に内容をコピー
MOV [0], R12 0 1メモリの0番地にレジスタR1の内容を保存
MOV [R2], R43 2 4レジスタR2から値を読みだし、その値により指定されるメモリ上の場所に、レジスタR4の内容を保存
MOV [R3+5], R14 3 5 1レジスタR3から値を読みだし、その値に5を足した数で指定されるメモリ上の場所に、別のレジスタR1の内容を保存
MOV R1, [0]5 1 0メモリの0番地を読み出し、レジスタR1に保存
MOV R4, [R2]6 4 2レジスタR2の値により指定されるメモリ上の場所に保存された値を、レジスタR4に保存
MOV R1, [R3+5]7 1 3 5レジスタR3の値に5を足した数で指定されるメモリ上の場所に保存された値を、レジスタR1に保存
ADD R1, R38 1 3レジスタR3の値をレジスタR1に加算し、結果をレジスタR1に保存

この表に例示した機械語は、一部を簡略化した架空の例ですが、実際のCPUの機械語と本質的にはほとんど同じものです。最初の数が命令の種類を示し、それに続く数が命令の実行に必要な詳細情報(例えば、操作するレジスタの番号や定数など)を示します。この形式は実際のCPUごとに異なりますが、基本的な構造はどのCPUでも同じです。

以下のプログラム1~4は、JavaScriptやPythonのプログラムとして実行可能ですが、この機械語命令版を書いてみました。レジスタの数が限られているので、以下のようなルールでレジスタを使うことにしました。

課題1

プログラム1~4の機械語バージョンを紙の上で実行し、RETされる値を答えてください。そして、JavaScriptもしくはPythonのプログラムがreturnする値と一致することを確かめてください。

ただし、初期状態ではすべてのレジスタが0で、メモリの内容もすべて0であるものとします。


プログラム 1

a = [0, 0, 0]
b = [0, 0, 0]
a[2] = 1
return b[2]

機械語命令(アセンブリ)

MOV R1, 3    ; 配列長を表す定数 3 をロード
; 配列 a の確保
MOV R2, R7       ; R7 を R2 に保存(a の開始アドレス)
ADD R7, R1    ; R7 を配列の長さ分インクリメント

; 配列 b の確保
MOV R3, R7       ; R7 を R3 に保存(b の開始アドレス)
ADD R7, R1    ; R7 を配列の長さ分インクリメント

; a[2] = 1
MOV [R2+2], 1   ; a[2] に 1 を代入

; return b[2]
MOV R0, [R3+2]   ; b[2] を R0 にロード
RET R0            ; 戻り値を返す

プログラム 2

a = [0, 0, 0]
b = a
a[2] = 1
return b[2]

機械語命令(アセンブリ)

MOV R1, 3    ; 配列長を表す定数 3 をロード
MOV R2, R7       ; R7 を R2 に保存(a の開始アドレス)
ADD R7, R1    ; R7 を配列の長さ分インクリメント
MOV R3, R2       ; b の参照を a の参照に設定
MOV [R2+2], 1   ; a[2] に 1 を代入
MOV R0, [R3+2]   ; b[2] を R0 にロード
RET R0            ; 戻り値を返す

プログラム 3

a = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
b = [a[0], a[1], a[2]]
a[0] = [1, 0, 0]
return b[0][0]

機械語命令(アセンブリ)

MOV R1, 3    ; 配列長を表す定数 3 をロード

; 配列 a の確保
MOV R2, R7       ; R7 を R2 に保存(a の開始アドレス)
ADD R7, R1    ; R7 を配列の長さ分インクリメント

; サブ配列を a[0], a[1], a[2] に割り当て
MOV [R2+0], R7  ; a[0] にサブ配列の開始アドレスを保存
ADD R7, R1    ; R7 を配列の長さ分インクリメント
MOV [R2+1], R7  ; a[1] にサブ配列の開始アドレスを保存
ADD R7, R1    ; R7 を配列の長さ分インクリメント
MOV [R2+2], R7  ; a[2] にサブ配列の開始アドレスを保存
ADD R7, R1    ; R7 を配列の長さ分インクリメント

; 配列 b の確保と初期化
MOV R3, R7       ; R7 を R3 に保存(b の開始アドレス)
ADD R7, R1    ; R7 を配列の長さ分インクリメント
MOV R0, [R2+0]  ; a[0] を読み出し
MOV [R3+0], R0  ; b[0] に保存
MOV R0, [R2+1]  ; a[1] を読み出し
MOV [R3+1], R0  ; b[1] に保存
MOV R0, [R2+2]  ; a[2] を読み出し
MOV [R3+2], R0  ; b[2] に保存

; a[0] = [1, 0, 0]
MOV R0, R7       ; R7 を R0 に保存(新しい配列の開始アドレス)
ADD R7, R1    ; R7 を配列の長さ分インクリメント
MOV [R0+0], 1   ; 新しい配列の要素 0 に 1 を設定
MOV [R2+0], R0  ; a[0] に新しい配列の開始アドレスを設定

; return b[0][0]
MOV R0, [R3+0]    ; b[0] の参照を R0 にロード
MOV R0, [R0+0]    ; b[0][0] を R0 にロード
RET R0            ; 戻り値を返す

プログラム 4

a = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
b = [a[0], a[1], a[2]]
a[0][0] = 1
return b[0][0]

機械語命令(アセンブリ)

MOV R1, 3    ; 配列長を表す定数 3 をロード

; 配列 a の確保
MOV R2, R7       ; R7 を R2 に保存(a の開始アドレス)
ADD R7, R1    ; R7 を配列の長さ分インクリメント

; サブ配列を a[0], a[1], a[2] に割り当て
MOV [R2+0], R7  ; a[0] にサブ配列の開始アドレスを保存
ADD R7, R1    ; R7 を配列の長さ分インクリメント
MOV [R2+1], R7  ; a[1] にサブ配列の開始アドレスを保存
ADD R7, R1    ; R7 を配列の長さ分インクリメント
MOV [R2+2], R7  ; a[2] にサブ配列の開始アドレスを保存
ADD R7, R1    ; R7 を配列の長さ分インクリメント

; 配列 b の確保と初期化
MOV R3, R7       ; R7 を R3 に保存(b の開始アドレス)
ADD R7, R1    ; R7 を配列の長さ分インクリメント
MOV R0, [R2+0]  ; a[0] を読み出し
MOV [R3+0], R0  ; b[0] に保存
MOV R0, [R2+1]  ; a[1] を読み出し
MOV [R3+1], R0  ; b[1] に保存
MOV R0, [R2+2]  ; a[2] を読み出し
MOV [R3+2], R0  ; b[2] に保存

; a[0][0] = 1
MOV R0, [R2+0]   ; a[0] の参照を R0 にロード
MOV [R0+0], 1   ; a[0][0] に 1 を代入

; return b[0][0]
MOV R0, [R3+0]   ; b[0] の参照を R0 にロード
MOV R0, [R0+0]   ; b[0][0] を R0 にロード
RET R0            ; 戻り値を返す

課題2

以下のプログラムを機械語で表すとどうなりますか。また、実行すると何が起こりますか?

a = [0, 0, 0]
b = [0, 0, 0]
a[3] = 1
return b[0]
a = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
b = 0
b[0] = 1
return a[0][0]

課題3*

(機械語が面白いと思った人は、この課題もやってみてください)

run という関数を機械語(アセンブリ)でプログラムし、ウィンドウに表示するプログラムを作りました。実際のプログラムでは、メモリのアドレス0は使えないので、自由に使えるメモリ領域を読み出すようにしています。また、一つのデータがメモリ8つ分を使うプログラムを書いています。

run.nasm

global run
; 関数の定義
run:
; ヒープポインタの読み出し
mov    r8, rcx
; 長さ24 (0x18) のメモリ領域を確保して、先頭アドレスを r9 (a) に保存
mov    r9, r8
add    r8, 0x18
; 長さ24 (0x18) のメモリ領域を確保して、先頭アドレスを r10 (b) に保存
mov    r10, r8
add    r8, 0x18
; a[0] = b
mov    [r9], r10
; 長さ24 (0x18) のメモリ領域を確保して、先頭アドレスを r11 (c) に保存
mov    r11, r8
add    r8, 0x18
; a[1] = c
mov    [r9+0x8], r11
; 長さ24 (0x18) のメモリ領域を確保して、先頭アドレスを r12 (d) に保存
mov    r12, r8
add    r8, 0x18
; a[2] = d
mov    [r9+0x10], r12
; a[0] を読み出す
mov    rax, [r9]
; a[0][1] に 1 を代入
mov    qword [rax+0x8], 0x1
; b[1] を読み出す
mov    rax, [r10+0x8]
; rax を返す
ret

show-window.c (run関数を実行して表示するということ以外、詳細を理解する必要はありません。)

#include <windows.h>
#include <stdio.h>
#include <stdint.h>

// ヒープ領域
size_t heap[1024];
// run 関数のシグネチャを宣言
size_t run(size_t *input);

// ウィンドウプロシージャ(イベント処理を行う関数)
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        // ウィンドウを描画したいとき
        case WM_PAINT:
          PAINTSTRUCT ps;
          HDC hdc = BeginPaint(hwnd, &ps);
          
          // run関数の実行結果をvalueに保存
          size_t value = run(heap);
          // value の値を文字列に変換
          char buffer[100];
          snprintf(buffer, sizeof(buffer), "The number is: %lld", value);
          // 作成した文字列をウィンドウに描画
          TextOut(hdc, 10, 10, buffer, lstrlenA(buffer));
          
          EndPaint(hwnd, &ps);
          return 0;
        
        // ウィンドウが閉じられたとき
        case WM_DESTROY:
          // アプリケーション終了メッセージを送信
          PostQuitMessage(0);
          return 0;
        
        // それ以外のとき
        default:
          // デフォルトの処理を実行
          return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    // ウィンドウクラスの登録
    const char CLASS_NAME[] = "HelloWindowClass";
    WNDCLASS wc = { };
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;

    if (!RegisterClass(&wc)) {
        return -1;
    }

    // ウィンドウ作成
    HWND hwnd = CreateWindowEx(
        0,                              // 拡張スタイル
        CLASS_NAME,                     // ウィンドウクラス名
        "Showing run() value",         // ウィンドウタイトル
        WS_OVERLAPPEDWINDOW,            // ウィンドウスタイル
        CW_USEDEFAULT, CW_USEDEFAULT,   // 初期位置
        400, 200,                       // 幅と高さ
        NULL, NULL, hInstance, NULL);   // その他のパラメータ

    if (hwnd == NULL) { return -1; }

    ShowWindow(hwnd, nCmdShow);

    // メッセージループ
    MSG msg = { };
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

コードのコンパイル (msys2-mingw64でコンパイルする場合)

nasm -f elf64 run.nasm
gcc -c show-window.c
gcc -o show-window.exe run.o show-window.o -lgdi32

上のコマンドを実行すると、プログラムファイルshow-window.exeが作られます。show-window.exeを実行すると、The number is: 1 と表示されます。


以下は、show-window.exeファイルの機械語分析結果です。objdumpという分析ソフトを使うことで、プログラムファイルに含まれる機械語を分析して、表示できます。

分析プログラムの実行:

objdump -M intel --insn-width=8 -d show-window.exe | grep -A 16 "<run>:"

実行結果:

0000000140001450 <run>:
   140001450:   49 89 c8                        mov   r8,rcx
   140001453:   4d 89 c1                        mov   r9,r8
   140001456:   49 83 c0 18                     add   r8,0x18
   14000145a:   4d 89 c2                        mov   r10,r8
   14000145d:   49 83 c0 18                     add   r8,0x18
   140001461:   4d 89 11                        mov   QWORD PTR [r9],r10
   140001464:   4d 89 c3                        mov   r11,r8
   140001467:   49 83 c0 18                     add   r8,0x18
   14000146b:   4d 89 59 08                     mov   QWORD PTR [r9+0x8],r11
   14000146f:   4d 89 c4                        mov   r12,r8
   140001472:   49 83 c0 18                     add   r8,0x18
   140001476:   4d 89 61 10                     mov   QWORD PTR [r9+0x10],r12
   14000147a:   49 8b 01                        mov   rax,QWORD PTR [r9]
   14000147d:   48 c7 40 08 01 00 00 00         mov   QWORD PTR [rax+0x8],0x1
   140001485:   49 8b 42 08                     mov   rax,QWORD PTR [r10+0x8]
   140001489:   c3                              ret

バイナリエディタでshow-window.exeを編集し、画面に「The number is: 2」を表示させてください。

ヒント1: バイナリエディタとは、ファイルに保存されたデータを数として表示、編集するソフトです。ためしにテキストファイルを開いてみてください。

ヒント2: run関数は、4989c84d89c1 という機械語の並びから始まります。4989c84d89c1をバイナリエディタで検索してください。

ヒント3: 1を代入する機械語を編集し、2を代入する機械語に変更してください。

追加課題: 以下の機械語を参考に、 r9 (a), [r10] (b[0]), r11 - r9 の値を表示してみてください。

4c 89 c0                mov    rax,r8
4c 89 c8                mov    rax,r9
4c 89 d0                mov    rax,r10
49 8b 00                mov    rax,[r8]
49 8b 01                mov    rax,[r9]
49 8b 02                mov    rax,[r10]
49 8b 40 08             mov    rax,[r8+0x8]
49 8b 41 08             mov    rax,[r9+0x8]
49 8b 42 08             mov    rax,[r10+0x8]
49 89 c0                mov    r8,rax
49 89 c1                mov    r9,rax
49 89 00                mov    [r8],rax
49 89 01                mov    [r9],rax
49 89 40 08             mov    [r8+0x8],rax
49 89 41 08             mov    [r9+0x8],rax
4c 29 c0                sub    rax,r8
4c 29 c8                sub    rax,r9
c3                      ret
sub rax,r8 は、rax = rax - r8 という計算をします。