CPUは機械語を入力すると、計算する電子部品です。機械語とは、CPUが直接理解できることのできる数値の並びで、CPUが実行することのできる特定の処理に対応するものです。
CPUの内部には「レジスタ」と呼ばれる高速な保存領域があり、およそ0.000000001秒に一回内容を読みだして計算したり、レジスタからレジスタにデータをコピーしたりすることができます。しかし、非常に限られた数(8個とか)しか記憶できませんし、どのレジスタから読みだすのかあらかじめ指定する必要があります。
メモリと呼ばれる部品がCPUに接続されており、CPUから受け取ったデータを保存したり、保存したデータをCPUに送り返したりできます。1000000000個くらいの数を保存できますが、レジスタと比べて100倍くらい遅く、0.0000001秒くらい読み出しに時間がかかることがあります。メモリには大量のデータの保存場所があるので、どの番号のデータを読み出すのかを伝えることで、保存されたデータを得ることができます。
機械で実行可能な命令には、例えば以下のようなものがあります。
CPUが実行する命令 | 機械語の例 | 意味 |
---|---|---|
MOV R3, 4 | 0 3 4 | 4を決められたレジスタR3に保存 |
MOV R1, R3 | 1 1 3 | レジスタR3からレジスタR1に内容をコピー |
MOV [0], R1 | 2 0 1 | メモリの0番地にレジスタR1の内容を保存 |
MOV [R2], R4 | 3 2 4 | レジスタR2から値を読みだし、その値により指定されるメモリ上の場所に、レジスタR4の内容を保存 |
MOV [R3+5], R1 | 4 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, R3 | 8 1 3 | レジスタR3の値をレジスタR1に加算し、結果をレジスタR1に保存 |
この表に例示した機械語は、一部を簡略化した架空の例ですが、実際のCPUの機械語と本質的にはほとんど同じものです。最初の数が命令の種類を示し、それに続く数が命令の実行に必要な詳細情報(例えば、操作するレジスタの番号や定数など)を示します。この形式は実際のCPUごとに異なりますが、基本的な構造はどのCPUでも同じです。
以下のプログラム1~4は、JavaScriptやPythonのプログラムとして実行可能ですが、この機械語命令版を書いてみました。レジスタの数が限られているので、以下のようなルールでレジスタを使うことにしました。
R0
一時的なデータを保存R1
配列の大きさを保存R2~R6
変数に保存されたデータを保存R7
ヒープポインタ(利用可能なメモリの先頭アドレスを保存)プログラム1~4の機械語バージョンを紙の上で実行し、RETされる値を答えてください。そして、JavaScriptもしくはPythonのプログラムがreturnする値と一致することを確かめてください。
ただし、初期状態ではすべてのレジスタが0で、メモリの内容もすべて0であるものとします。
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 ; 戻り値を返す
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 ; 戻り値を返す
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 ; 戻り値を返す
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 ; 戻り値を返す
以下のプログラムを機械語で表すとどうなりますか。また、実行すると何が起こりますか?
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]
(機械語が面白いと思った人は、この課題もやってみてください)
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
という計算をします。