機械語

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

機械語[編集]

環境にもよるが、アセンブラ命令が機械語でどんな数字かは、バイナリエディタで調べれば分かる。


CPUによって、おおむね機械語は決まってくる。

むかしのintel系のCPUのシリーズか何かで「X86」というのがあって、いまのパソコンCPUの機械語も、おおむねX86系である(ただし、ARMとかは別かも?)。

インテル以外のAMDなどの企業でも、パソコンなら「X86」系であるハズ。


実際に、世間のOSで作ったプログラムの、オブジェクトファイルの機械語を調べてみても、X86系のコード規格に従ってる。

WindwosでもLinuxでも、実際に実験した結果、X86系のコード規格にだいたい従っているようである(詳しくは次の節で説明)。

なお、インテルがネット公開の仕様書でX86系のオペコードの仕様を公開している。ネットでも見られる。

『IA-32インテル®アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル』 『中巻A:命令セット・リファレンスA-M』
IA-32インテル®アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル』『中巻B:命令セット・リファレンスN-Z』

インテルの公式マニュアルは何百ページもあるので、いきなり読むのは、やめたほうが良い。語学の勉強と同じで、まずは、ある程度分かっているヒトに、よく使われるコードを習うのが良いだろう。なお、ネットのインテル公式マニュアルには、冒頭の目次にハイパーリンク付きの目次があるので、それを使うと探しやすい、。

linuxの場合[編集]

さて、リナックスというOSがあり、その一種のFedora 31というOS で、アプリで機械語を読み取れるバイナリエディタの一種で GHEX(というバイナリエディタがある) というのがあるので、そのバイナリエディタで機械語を調べた結果、


pusu %rbp は

55

である。

50番を基準にする50〜60番台のオペコードはpushが使用している領域である。 そのうち、55だと、対象レジスタがrbpだと判断する仕組みらしい。 (『中巻B:命令セット・リファレンスN-Z』 4-150)


call は

e8

である。 (マニュアル『中巻A:命令セット・リファレンスA-M』 3-67)

e8 の直後に2バイトの命令(インテル公式マニュアルでは「cw」と略記される)が来る。 なので

E8 cw

とマニュアルでは書かれる。


movq %rsp, %rbp

48 89 E5

である。

89 E5

である。(『中巻A:命令セット・リファレンスA-M』 3-489)


「89」の部分が、転送のオペコード。 「8A」だと、「MOV ソース、目的地」の語順。

なお「88」はデータサイズが8の場合用。 「89」は、データサイズが16または32の場合用。

「8A」だと、「MOV 目的地、ソース」の語順。


実験でどうやって調べられるかというと、たとえばアセンブリコードのプログラム中に、

.LFB0:
	.cfi_startproc
	pushq	%rbp
	pushq	%rbp
	pushq	%rbp
	pushq	%rbp
	pushq	%rbp
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	movq	%rsp, %rbp
	movq	%rsp, %rbp
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6

のような、特に意味のない繰り返し命令を書きまくって、それをgccで普通にオブジェクトファイルにしたあとに、バイナリエディタにある検索機能で文字列件せくで調べればいい。

pusu %rbp を繰り返した回数のぶんだけ、バイナリ中で「55」が繰り返されているハズである。


なお、一般にオブジェクトファイルのバイナリは何百行以上もある巨大なものになる。なので、このページではオブジェクトファイルのバイナリは書かない。(LinuxだけでなくWindowsの実行ファイルも同様。)

上述の「pushq %rbp」は機械語ではなく、アセンブルコードである。

なんで、コードの長さが機械語とアセンブリコードとぜんぜん違うかというと、機械語にも形式・規格があり、Linuxの場合の実行ファイルの機械語の形式はELF形式だからである。(w:Executable and Linkable Format


ともかく、このLinuxによる実験例と、インテル仕様書との機械語コードの一致から、Linux でもインテル公式の仕様をほぼそのまま使ってることが分かる。


なお、余談だが、Linuxk界隈ではバイナリエディタのことを「16進エディタ」のように言う場合がある。


Windowsの場合[編集]

Windows用のバイナリエディタはいろいろあるが、日本では Stirling という名前のバイナリエディタが有名である。Stirling の作者が日本人なので、日本語対応もされており、使いやすい。

さて、Windowsの場合、実行ファイルの形式名は、PEフォーマットというのである。(w:Portable Executable)これまた機械語が長いので、Windowsでもアセンブリコードと長さが一致しない。

さて、下記のようなWindowsによる実験例と、インテル仕様書との機械語コードのおおよその一致から、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」が連続している部分がある。

しかし、Windowsでの実験では、「call _puts」や「movl $LC2, (%esp)」など、ほかのコードは連続している部分が見つからなかった。


なお、上のコードを実行すると、この実行ファイルは異常停止する。あまり実行しないほうが安全だろう。


機械語ファイル冒頭の識別子[編集]

LinuxでもWindowsでも、実行ファイルの冒頭に、そのファイルの種類を識別するための識別コードがある。

Linuxでは実行ファイルの冒頭に「ELF」に対応するアスキーコード「7f 45 4c」が書かれている。
Windowsでは実行ファイルの冒頭に「MZ」に対応するアスキーコード「4D 5A」が書かれている。

なので、もしバイナリエディタでこの部分を書き換えると、そのアプリが起動しなくなる。(むりやり実行してもエラーになる。)

もし書き換えてしまっても、もとの文字に戻せば(たとえばWindowsなら冒頭を「4D 5A」に戻せば、起動するようになる。)


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[編集]

なお、Windowsの場合、冒頭の識別子はアスキーコードで「MZ」である。これの由来は一説ではマイクロソフト社の当時の開発者 Mark Zbikowski のイニシャルらしい。

たとえばWindowsでMinGW(Win版GCC)とnasmを使ってhello worldのプログラムを変換したら、下記のようになった。

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形式の実行ファイルとして必要な情報が不足しているためかエラーになる。