機械語

出典: フリー教科書『ウィキブックス(Wikibooks)』
ナビゲーションに移動 検索に移動

概要[編集]

機械語 (きかいご) とは、CPUが解釈できる命令群、及びそれらの羅列のことである。OSおよびCPUが採用しているアーキテクチャによって同一の働きをする命令でも、”ビット列”で表現した場合に異なることが多い。代表的な命令セットに、x86 (Intelの32ビットCPUなど)、x64 (Intelの64ビットCPUなど) などがあげられる。以下の文章は、環境依存の内容を多分に含むため、使用する環境のマニュアルなどを参照しながら読み替えること。

2020年の時点では、「パーソナルコンピュータ」、「パソコン」などと言われる機械に採用されているCPUの多くは、インテル製またはインテル互換を謳った他社製のものである(AMD社がインテル互換CPUを出している)。スマートフォンや一部のタブレット端末では、インテル以外の"ARM"(アーム)という企業のCPUが採用されている場合もある。スーパーコンピュータや組み込み機器などではインテル互換以外の別のCPUや命令セットが使われている場合もある。

読者の大半は、廉価であるパソコンで機械語を勉強・実験する事になると思うので、入門の段階ではインテル系列の機械語を勉強する事になるだろう。よって本wikiでも、インテル系CPUの環境を前提にして解説する。

プラットフォーム別解説[編集]

この節を読むに当たっては、予備知識としてアセンブリ言語の文法を知っている事が望ましい。


Linux[編集]

注釈: 以下の内容はFedora 31に基づく。

アセンブリ言語のコード push %rbpは、一般的なCPUの機械語では55[1]に対応する。50番を基準にする50?60番台のオペコードはpushが使用している領域である[2]

アセンブリ言語と機械語は異なる言語であるが、しかし原則的に全ての命令文が一対一対応しているはずなので、本wikiでは説明の都合上、アセンブリ言語で説明する。

アセンブリ命令 callは機械語 e8[1]である[3]

e8の直後にcwと呼ばれる2バイトの命令が続くため、インテル公式マニュアル[3]ではe8 cwと書かれる。

アセンブリ命令 movq %rsp, %rbpは 機械語 48 89 E5[1]である。movqは32ビット同士のデータを転送する際に使用される[3]。ここで、pushcallmovqはアセンブラ命令である。


上記例ではLinuxの一種のFedoraで説明したが、Linuxに限らず上記例のように、どのOSやCPUの環境でも(たとえばWindwosを搭載したintel CPUのパソコンでも)、アセンブリ命令文の文字列や番号に多少の差異のある可能性はあるが、ともかくどの環境でも原則的にアセンブリ言語と機械語は一対一対応をしているはずである。


Windows[編集]

抽象的な説明だと分かりづらいので、実例で見ていこう。

なお、下記のコードには、特に実用的な意味は無く、練習用・勉強用のコードである。

コード例
	.file	"test3.c"
	.text
	.def	___main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
LC0:
	.ascii "hello1, world\0"
LC1:
	.ascii "hello2, world\0"
LC2:
	.ascii "hello3, world\0"
	.text
	.globl	_main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
LFB13:
	.cfi_startproc
	pushl	%ebp
	pushl	%ebp
	pushl	%ebp
	pushl	%ebp
	pushl	%ebp
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	movl	%esp, %ebp
	movl	%esp, %ebp
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	andl	$-16, %esp
	subl	$16, %esp
	call	___main
	movl	$LC0, (%esp)
	call	_puts
	call	_puts
	call	_puts
	movl	$LC1, (%esp)
	call	_puts
	movl	$LC2, (%esp)
	movl	$LC2, (%esp)
	movl	$LC2, (%esp)
	movl	$LC2, (%esp)
	movl	$LC2, (%esp)
	call	_puts
	movl	$0, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
LFE13:
	.ident	"GCC: (MinGW.org GCC-8.2.0-5) 8.2.0"
	.def	_puts;	.scl	2;	.type	32;	.endef

これを機械語にすると、数字の羅列になるが、

pushl %ebpが6回連続してるが、機械語で「55」が連続している部分がある。
movl %esp, %ebpが4回連続してるが、機械語で「89 e5」が連続している部分がある。


そもそもアセンブリ言語で書かれたコードをどうやって実行するかなどの方法は、本wikiサイトでは『X86アセンブラ』などに書かれているので、それらのページを参照のこと。 なお、OSの種類などによって、アセブリ言語の文法が「GAS構文」かそれとも「NASM構文」なのか(またはそれ以外か)が変わってくる。

アセンブラ命令が機械語でどんな数字であるかを調べるには、環境にもよるが一般的には「バイナリエディタ」といわれる種類のソフトウェアで調べれば分かる。

実行可能ファイルの識別子[編集]

Linuxで実行可能ファイル仕様として採用されているELF形式は、ELFに対応するアスキーコード7f 45 4c[1]が書かれている。
Windowsで実行可能ファイル仕様として採用されているMZ形式は、MZに対応するアスキーコード4d 5a[1]が書かれている。なお、WIndowsではPEフォーマットというものが実行ファイル形式として採用されているが、このPEフォーマットもMZ形式の一種である。

Linux[編集]

一例として、たとえばリナックスで作成した何らかのアプリの実行ファイルには、機械語の冒頭に

7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 02

00 3e 00 01 00 00 00 40 10 40 00 00 00 00 00 40 00

00 00 00 00 00 00 f8 43 00 00 00 00 00 00 00 00 00

のようなコードが出てくる。


これは、アスキーコードに置き換えると、

ELF.............

.>.....@.@.....@.

.......C.........


ELFとは、実行形式の一種のELF形式のことである。

45が「E」
4cが「L」
46が「F」

である。

このように、機械語の実行ファイルには、冒頭に、実行形式を判別するための識別子がある。


Windows[編集]

4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00
B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

これは、アスキー文字に変換すると、

MZ.............. 
ク.......@....... 
................ 

となる。

アスキーで4D が「M」。5aが「Z」である。

余談: ウイルス検知[編集]

余談だが、コンピュータウイルスの一種で、ファイルの種類をテキストファイルや画像ファイルなどに偽装したウイルス実行ファイルがあるが、これはヘッダをみることで見破ることができる。もし冒頭の識別子が「MZ」だったら、そのファイルは何らかの意図しない振る舞いを起こすだろう。

Hello World[編集]

もし機械語で「Hello Wolrd!」と表示するプログラムを作りたい場合、いちおう、次のように作れるだろう。

  1. まずC言語またはアセンブリコードで「aaaaaaaaaaaaaaa」(aが15個)とかの文字列を表示するプログラムを作る。aは「Hello Wold!」(空白も含めて12文字)の文字数より多くしたほうが今後の作業がラクである。
  2. アスキーコードで「a」は「61」なので、機械語に「61」が連続する部分があるので、そこの冒頭12文字をバイナリエディタの編集機能を使って「Hello World!」のアスキーコード「48 65 6c 6c 6f 20 57 6f 72 6c 64 21」に書き換える。

では、Windowsで実験してみよう。実験ではバイナリエディタにstirlingを使い、コンパイル環境としてはWindows7とMinGWとnasmで実験してみた結果を下記に示す。

実行結果
C:\Users\ユーザー名\nnnn>test.o
Hello World!aaaa??@

のように、文字化けがあるが、いちおう Hello Worldが表示できる。

ファイル名の場所とファイルシステム[編集]

たとえば、テキストファイルを新規作成し、「名前をつけて保存」して、「eeee.txt」とか、つけたとしましょう。

その後、バイナリエディタで「eeee.txt」開いたとしましょう。

さて、バイナリエディタで調べても、どこにも「e」に相当する文字列は無いです。

もし空白のテキストファイルなら、それをバイナリエディタで開いても表示されるのは、空白で、何も表示されないという結果になります。


これは Windows だけでなく、 Linux でも同様です。

つまり、どういう事か

このことから、おそらく、ファイル名の情報は、別の場所に保管されているだろうという事が、うかがえます。

目次(もくじ、index インデックス)のようなファイルが、ハードディスク内に、ファイル本体とは別途保存されているのです。

ファイルのコピーなどを行う場合は、この目次も書き換えているようです。


私たちが普段使うようなOSはたいてい、このようなファイルの仕組みになっているのでしょう。(ファイルの仕組みのことを専門用語で「ファイルシステム」という。)

ハードディスクを使うと、ときどき、インデックスがどうのこうのとか指示される場合があるのは、おそらく、このことを言ってるのでしょう。


さて、通常のバイナリエディタでは、インデックス部分(目次の部分)は、表示もできず、当然に書き換えをできないようです。

C言語での機械語の読み書き[編集]

C言語のプログラムで、機械語の読み書きをしたい場合、

fopen の引数で「r」(読み込み)とか「w」(書き込み)とか編集モードの指定がありますが、それに「b」をつけます。つまり「rb」た「wb」などの引数になります。

また、書き込む関数には fwrite を使う必要があります。 いっぽう、 fprintf では、テキストファイルに自動的に変換してしまいます。

Linux(fedora31で確認ずみ)の場合でのコード例を示す

#include <stdio.h>
#include <stdlib.h>

#pragma warning(disable:4996) // これは主にWindowsで使うが、Linuxでも存在してもエラーにならない。

int main()
{
	FILE *fp1;

	fp1 = fopen("test1.bin", "wb");
	if ( (fp1 = fopen("test1.bin", "wb") ) == NULL) {
		printf("ファイルを開けませんでした。\n");
		return 1;
	}	

char mojiretu[50];
	printf("キーボードから文字列を入力してください。\n");
	scanf("%s", &mojiretu);
	printf("入力された文字列は %s です。 \n", mojiretu);

	printf("これをファイルtest1.binに書き込みます。 \n");

	fwrite(mojiretu,1,6,fp1);

	fclose(fp1);	
        printf("いまファイルを閉じたハズです。\n");
	return 0;
}

たとえば、入力された文字に「sssss」と入れたとしよう。

すると、test1.bin が作成される。

それを右クリックで開こうとしても、「表示できませんでした」「そのファイルの種類を特定できません」と出て、開けない。(なお、もしファイル名に.txtと拡張子をつけてしまうと、Linuxでも、テキストファイルとして開こうとしてしまい、 別のエラー(「開いたファイルに正しくない文字が含まれています。」)になる。なので、バイナリ形式のファイルには、拡張子をつけないか、もしくは「.bin」という拡張子をつけるのが安全である。)

なので、バイナリエディタで「test1.bin」を開いてみよう。すると

73 73 73 73 73 00

と書いてある。アスキーで「s」が「73」だからである。

なお、このバイナリを実行しようとコマンド入力しても、ファイル形式などが不明なためかエラーになり、

[ユーザー名@localhost ~]$ ./test1.bin
bash: ./test1.bin: バイナリファイルを実行できません: 実行形式エラー

と表示される。

機械語を書き込む際に sssss の代わりに「ELF」などと入力しても、ELF形式の実行ファイルとして必要な情報が不足しているためかエラーになる。



直接に文字指定したい場合[編集]

たとえば、機械語で

9A B3 72

の合計6文字だけがバイナリディタに表示されるような機械語ファイルを作りたいとしましょう。

この場合、下記のようなコードになります。

コード例
#include <stdio.h>

int main() {
    FILE *file;
    char hairetu[3] = {0x9A, 0xB3, 0x72};

    file = fopen("test.bin", "wb");
    if (file == NULL) {
        printf("ファイルを書き込めませんでした");

        return 0;
    }
    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);
    fclose(file);
    return 0;
}
(※ Windows7上でのgcc(MIn-GW)で動作確認.)

実際に、バイナリエディタで調べてみれば、たしかに

9A B3 72

の6文字だけが書き込まれている事を確認できまます。


注意点は、charを使う際に、引用符("")をつけない事です。 char型に引用符がついていないので、ここで書き込まれるのは16進の数値です。

なので、たとえば「0xGV」のように、16進数に無い英字に置き換えてみればエラーになります(実際にエラー表示されます)。


別の例

一方、int型で次のようにコードを書くと、書き込まれる結果は違ってきます。理屈よりも、実験したほうが早いので、やりましょう。

コード例
#include <stdio.h>

int main() {
    FILE *file;
    int hairetu[2] = {0x4A, 0xB6}; // ここが int型

    file = fopen("test.bin", "wb");
    if (file == NULL) {
        printf("ファイルを書き込めませんでした");

        return 0;
    }
    fwrite(hairetu, sizeof(int), sizeof(hairetu) / sizeof(hairetu[0]), file); // int型に変わった
    fclose(file);
    return 0;
}
実験結果
(※ バイナリエディタに表示される内容です。)
4A 00 00 00 B6 00 00 00

のようになります。 「00」が追加されている事に注目してください。

つまり、1つの整数あたり、16進数で8桁を用意している事になります。これは16の8乗は 4294967296 ですので、その値まで(またはプラスマイナスの符号を考慮してその半分)の大きさの整数までを保管できる記憶領域を、このコンパイラは1つの整数あたりに確保した事になります。


なので、もしもC言語で機械語として書き込む16進値を直接に指定したい場合には char 型で引用符をつけずに指定していく事になります。



テキストファイルにある16進ダンプファイルを機械語にしたい場合[編集]

概要[編集]

たとえばテキストファイル "bintest1.txt" に

9A B3 72

という6文字だけのテキストがあったとしましょう。(なお、半角スペースを含めると8文字です)

一般的なバイナリエディタによるテキスト出力では、上記のように16進数の2桁ずつ(たとえば「9A」)で1つのブロック単位になり、ブロックとブロックの間には半角スペースで空白文字が1文字ぶん空いているのが通常です。

このテキストファイルの内容をそのまま機械語にしたい場合、下記のプログラムで行けます。もしかしたら他の方法もあるのもしれませんが、見つからないので下記コードをwikibooksdで作りました。

コード例
#include <stdio.h>
#include <stdlib.h> // atoi

int main() {
    FILE *file;
    char hairetu[1];

    FILE *fp1;
    char str1[150];

    fp1 = fopen("bintest1.txt", "r");

    fscanf(fp1, "%s", str1);

    int kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;

    file = fopen("test2.bin", "wb");
    if (file == NULL) {
        printf("ファイルを書き込めませんでした");

        return 0;
    }

    // 1ブロック目 9A をここで読み取っている
    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);


    // 2ブロック目 B3 を読むため繰り返す
    fscanf(fp1, "%s", str1);
    kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;
    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);


    // 3ブロック目 72 を読むため繰り返す
    fscanf(fp1, "%s", str1);
    kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;
    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);


    fclose(file);
    return 0;
}
(※ Windows7上でのgcc(MIn-GW)で動作確認.)


実行結果
9A B3 72
※ 上記コードは、分かりやすさを重視して、あえてfor文などのループ機能を用いていないです。実際のソフトウェア作成の際には、for文で実装する事になると思います。

上記のプログラミングのコツとしては、char hairetu[1]; のように1要素だけの配列を作ることです。ついつい「3ブロック読み取りたい」のでhairetu[3]とかしてしまいがちですが、それだと、おそらくプログラミングに失敗します。

この配列 hairetu は、単に int kazu をそのままchar型に格納するための配列なので、要素数は1でイイのです。

strtol とは単に、読み取った数字を、読み取りのの際についでに16進数や10進数などの数値変数に変換してくれる、c言語の便利な関数です。


scanf は、テキスト文字を、起点は開始位置または直前の読み取り終了位置から、終点は次の半角スペースまでで、読み取りをする命令です。短く正確に説明するのが難しいので、詳細はwikibooks『C言語/ファイル入出力』にあるscanfの説明を読んでください。


もしかしたら canf の代わりに fgets でもコードが書けるかもしれませんが(なおfgetsはテキストファイルから1行ずつ読み込む命令)、scanf のほうがより入門的でしかも短いコードが書けるので、本wikiでは scanf を採用しました。


実用例[編集]

機械語に限った事ではないですが、fscanf は、読み取り位置が最後まで来ても、そのままでは、最後の文字を何度でも読み取れてしまいます。

なので、もし読み取り位置が最後まで来たら、その直後の段階で、書き込み処理を終了する必要があります。

この書き込み終了判断の実装のために必要な機能は、fscanfの返却値です。

※ 一般的な市販のC言語解説書では上記の問題の解決策が書かれないので、『機械語』科目ですが間借り(まがり)的に解説します。

fscanfは、もし最後のブロックに来た後に、もう一度fscanf を使うと、返却値として -1 を返します。なおwindowsの場合、最後のブロック以外では(プラスの)「1」を返します。 なので、もし返却値が-1なら、書き込みを終了する処理を実装すればいいのです。


よってコードは、下記のようになります。

コード例
#include <stdio.h>
#include <stdlib.h> // atoi


int saigoKensa ; // scanfの返却値を格納するための変数

int main() {
    FILE *file;
    char hairetu[1];

    FILE *fp1;
    char str1[150];

    fp1 = fopen("bintest1.txt", "r");

    saigoKensa = fscanf(fp1, "%s", str1); 
    if (saigoKensa == -1){fclose(file); return 0;} 
    printf("いま返却値は %d \n",saigoKensa)  ;

    int kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;

    file = fopen("test2.bin", "wb");
    if (file == NULL) {
        printf("ファイルを書き込めませんでした");

        return 0;
    }

    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);




    // 2ブロック目を読むため繰り返す
    saigoKensa = fscanf(fp1, "%s", str1);
    if (saigoKensa == -1){fclose(file); return 0;} 
    printf("いま返却値は %d \n",saigoKensa)  ; // テスト用
    kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;
    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);


    // 読みたい文字のぶんだけ繰り返す
    saigoKensa = fscanf(fp1, "%s", str1);
    if (saigoKensa == -1){fclose(file); return 0;} 
    printf("いま返却値は %d \n",saigoKensa)  ;
    
    kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;
    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);


    saigoKensa = fscanf(fp1, "%s", str1);
    if (saigoKensa == -1){fclose(file); return 0;} 
    printf("いま返却値は %d \n",saigoKensa)  ;

    fscanf(fp1, "%s", str1);
    kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;
    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);



    fclose(file);
    return 0;
}


EOFを用いた例[編集]

機械語の読み書きに限った事ではないですが、終了の -1 の代わりに、宣言なしで「EOF」と書いても、コードが正常にコンパイルおよび実行可能です。なお EOF とは End of File の略です。なお、テキストファイルの読み書きでもEOFは利用可能です。

下記のコードは、テキストファイルから読み取った16進文字を機械語として書き込みするコードです。

EOFを用いたコード例
#include <stdio.h>
#include <stdlib.h> // atoi


int saigoKensa ; 

int main() {
    FILE *file;
    char hairetu[1];

    FILE *fp1;
    char str1[150];
    printf("EOF is %d \n",EOF)  ;

    fp1 = fopen("bintest1.txt", "r");

    saigoKensa = fscanf(fp1, "%s", str1); 
    if (saigoKensa == EOF){fclose(file); return 0;} 
    printf("いま返却値は %d \n",saigoKensa)  ;

    int kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;

    file = fopen("test2.bin", "wb");
    if (file == NULL) {
        printf("ファイルを書き込めませんでした");

        return 0;
    }

    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);




    // 2ブロック目を読むため繰り返す
    saigoKensa = fscanf(fp1, "%s", str1);
    if (saigoKensa == EOF){fclose(file); return 0;} 
    printf("いま返却値は %d \n",saigoKensa)  ;
    kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;
    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);


    // 読みたい文字のぶんだけ繰り返す
    saigoKensa = fscanf(fp1, "%s", str1);
    if (saigoKensa == EOF){fclose(file); return 0;} 
    printf("いま返却値は %d \n",saigoKensa)  ;
    
    kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;
    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);


    saigoKensa = fscanf(fp1, "%s", str1);
    if (saigoKensa == EOF){fclose(file); return 0;} 
    printf("いま返却値は %d \n",saigoKensa)  ;

    fscanf(fp1, "%s", str1);
    kazu = strtol(str1, NULL, 16);
    hairetu[0] = kazu;
    fwrite(hairetu, sizeof(char), sizeof(hairetu) / sizeof(hairetu[0]), file);


    fclose(file);
    return 0;
}

実行結果は前述とほぼ同じなので、省略します。

上記コードの場合、EOFは if (saigoKensa == EOF) のように、if文の中で使われています。

脚注[編集]

  1. ^ 1.0 1.1 1.2 1.3 1.4 16進数。オクテットごとに半角に区切られている場合は、出現した順序をそのまま適用する。
  2. ^ [https://www.intel.co.jp/content/dam/www/public/ijkk/jp/ja/documents/developer/IA32_Arh_Dev_Man_Vol2B_i.pdf IA-32インテルRアーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル』『中巻B:命令セット・リファレンスN-Z』
  3. ^ 3.0 3.1 3.2 『IA-32インテルRアーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル』 『中巻A:命令セット・リファレンスA-M』
このページ「機械語」は、まだ書きかけです。加筆・訂正など、協力いただける皆様の編集を心からお待ちしております。また、ご意見などがありましたら、お気軽にトークページへどうぞ。