機械語とアセンブリ言語

「機械語」という名前はすでに習ったはずだ。 コンピュータが実行できるのは機械語のプログラムだけであり, C言語やその他の言語で書かれたプログラムは, コンパイラを使って機械語プログラムに翻訳することで実行可能になる。

ところで機械語プログラムってどんなものだろうか?  唐突だが,下記は機械語プログラムの例だ。

b8 04 00 00 00 bb 01 00  00 00 b9 a2 80 04 08 ba
0d 00 00 00 cd 80 b8 01  00 00 00 bb 00 00 00 00
cd 80 68 65 6c 6c 6f 2c  20 77 6f 72 6c 64 0a

2桁の16進数が全部で47個並んでいるが,これは47バイトの情報を表している。 1バイトは8ビットだから,この16進数列は 47 × 8 = 376 ビットの2進数の列 10111000 00000100 00000000 ... を表している。 16進数で書く方が短いからそうしているが,機械の中(メモリや補助記憶装置の中)では, 電気的な方法で表された「0」と「1」の列として保持される。

  • 参考:10進・16進・2進 対応表
10進16進2進
000000
110001
220010
330011
440100
550101
660110
770111
 
10進16進2進
881000
991001
10a1010
11b1011
12c1100
13d1101
14e1110
15f1111

実行可能ファイルの生成

とりあえず上記の機械語プログラムを実行してみよう。

◆演習1.1-1 下記の手順に従って,上記の機械語プログラムを実行しなさい。

なお,シェルプロンプト([190999x@wsa-s001:~]$ みたいなやつ)を単に $ と表記する。$ の右側が実際に打鍵入力すべきコマンドだ。-- とその右側は注釈だから入力してはいけない。

  1. 上記の16進数列を打鍵入力し,hello.hex というファイル名で保存する。
    (空白の数や改行位置は自由に変えてよい。afの代わりにAFを使ってもよい。)
  2. 下記のコマンドを実行する。

     $ xxd -r -p hello.hex > h.bin        -- バイナリファイルh.binに変換
     $ ~y-takata/bin2elf h.bin            -- ELFファイルa.outに変換
     SHA-1 checksum:
     d07f61ed4ab2deb75765642ad8823aacc1f37809 *h.bin

    表示されるチェックサムに注意。上記と異なる場合はhello.hexの内容が誤っている

  3. 生成されたa.outを実行する。
     $ ./a.out
     hello, world

上記の16進数列は端末画面に hello, world と表示するプログラムだったわけだ。

今度はこのプログラムをちょっと改造してみよう。

◆演習1.1-2 hello.hexの内容を以下のように変更し,上記と同じことを実行しなさい。 実行結果はどのように変化するか?

  1. 2行目の先頭(先頭を第0バイトとして第16バイト)の 0d を 13 に変更。
  2. 3行目の3個目の16進数以降の 68 65 6c 6c ... を以下のように変更。
    48 6f 77 20 61 72 65 20 79 6f 75 20 64 6f 69 6e 67 3f 0a
    (空白の数や改行位置は自由。)

チェックサムは f691b30f3c03383a73466e6f100e7d143b2ee198 になるはずだ。

実はhello.hexの中身は,「第34バイト以降の文字列(文字コードの列)を端末画面に出力する」プログラムだ。第16バイトの値は出力文字列の長さ(バイト数)を表している。

◆演習1.1-3 以下の16進数列を適当な名前のファイル(例えばa.hex)に保存し,以下を実行しなさい。

be 02 00 00 00 b9 0a 00  00 00 89 f3 d1 eb 81 fb
02 00 00 00 7c 0d 31 d2  89 f0 f7 f3 85 d2 74 0d
4b eb eb 89 f0 e8 12 00  00 00 49 74 03 46 eb da
b8 01 00 00 00 bb 00 00  00 00 cd 80 60 bb 0a 00
00 00 31 f6 46 b9 0a 90  04 08 c6 01 0a 46 49 31
d2 f7 f3 80 c2 30 88 11  85 c0 75 f1 b8 04 00 00
00 bb 01 00 00 00 89 f2  cd 80 61 c3
  1. hello.hexと同様にして,実行可能ファイルを生成して実行しなさい。
    (チェックサムは 580f146f4ee4595a44ab8f635b849ae88b906e94
  2. 先頭を第0バイトとして第1〜2バイトの 02 00 を 10 27 に変更し,同じことを実行しなさい。 実行結果がどう変わるか確認しなさい。
    (チェックサムは 067be97f48a73e7e833712a268c2e603f017b071
  3. さらに,第6バイトの 0a を 0f に変更し,同じことを実行しなさい。 実行結果がどう変わるか確認しなさい。
    (チェックサムは a5e8c4aa50b5353cd2d72e887ec70ae1b40d9475
  4. 上記の実行結果から,このプログラムは何を計算して出力するプログラムか,第1〜2バイト及び第6バイトの値が何を意味しているか,推測しなさい。(ヒント: 10進数 10000は16進数 2710 に等しい。)
  5. 上記の16進数列を保存したファイルの名前を,このプログラムの計算内容に合った適切なものに変更しなさい。

機械語プログラムの中身

「『2進数8桁(1バイト)の情報の並び』が機械にとってのプログラムである」ということが感じられただろうか。ここでhello.hexの中身を少し見ておこう (後で述べるように,実際には「16進数列を記述してプログラムを作る」ことはしないので,細かいことは覚えなくてよい)。

b8 04 00 00 00 bb 01 00  00 00 b9 a2 80 04 08 ba
0d 00 00 00 cd 80 b8 01  00 00 00 bb 00 00 00 00
cd 80 68 65 6c 6c 6f 2c  20 77 6f 72 6c 64 0a

上記は16バイト毎に改行しているが,「機械語命令」毎に改行すると以下のようになる。 最後の1行は「hello, world (+改行)」という13文字の列を表している。

b8 04 00 00 00
bb 01 00 00 00
b9 a2 80 04 08
ba 0d 00 00 00
cd 80
b8 01 00 00 00
bb 00 00 00 00
cd 80
68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 0a

本当は,機械の中では2進数で格納されている。

10111000 00000100 00000000 00000000 00000000
10111011 00000001 00000000 00000000 00000000
10111001 10100010 10000000 00000100 00001000
10111010 00001101 00000000 00000000 00000000
11001101 10000000
10111000 00000001 00000000 00000000 00000000
10111011 00000000 00000000 00000000 00000000
11001101 10000000
-- この下の文字列データは省略 --

先頭の命令 b8 04 00 00 00 は「0番レジスタという記憶装置に数値 4 を格納する(代入する)」命令だ。 先頭バイトの b8 は2進数では 10111000 だが,上位5ビット 10111xxx が「レジスタに数値を格納する」命令を表し,下位3ビット 000 が格納先レジスタを表す。 このコンピュータは,「10111xxx というパターンに合致する1バイトを読み出したら,続く4バイトを xxx 番レジスタに書き込む」という動作を行うよう,電子回路(論理回路)として作り込まれているわけだ。 後の章で説明するが,このコンピュータ(Intel 80386,略してi386)には自由に値を変更できるレジスタが8個ある。

以降の3命令も,代入先レジスタが違うだけの同じ命令だ。 このコンピュータでは各レジスタは32ビット(= 4バイト)なので,レジスタに代入したい値も4バイト使って表されている。ただし,このコンピュータでは「下位桁のバイトが前,上位桁のバイトが後」に並べられる(このような並べ方をリトルエンディアンという)。例えばバイト列 04 00 00 00 は32ビットの数 00000004 を表し,バイト列 a2 80 04 08 は32ビットの数 080480a2 を表す。

このプログラムは「4個(または2個)のレジスタに値を書き込んで,cd 80 を実行する」という動作を2回実行している。 cd 80 は特殊な命令で,OSが提供する各種サービス(システムコールと言う)を呼び出す命令だ。 「現在実行中のプログラムを一旦離れて,OS内部のプログラムを実行する」と考えればよい。 0番レジスタに呼び出したいサービスの番号を格納して cd 80 を実行すると,そのサービスが実行される(i386用Linuxの場合)。 ここでは,文字列を外部に出力する「write」(サービス番号4)と,プログラムを終了する「exit」(サービス番号1)の2つを使っている。

  • 参考:ausyscall i386 --dump を実行すると,システムコールとその番号の一覧が表示される。
    各システムコールの詳細は man 2 exit, man 2 write 等で見ることができる。 システムコール全般の説明は man 2 intro にある。

exitシステムコールを実行するとプログラムが終了し,その先には実行が進まない。

このプログラムでは,機械語命令列の後に出力したい文字列を表すデータが続いている。 機械語命令であるかデータであるかは,どちらもただの2進数列なので,値そのものでは区別できない。 コンピュータは,「この位置から実行せよ」と指示されればその位置のビット列を機械語命令だと見なして実行するし,「この位置のデータを読み出せ」という命令を実行すればその位置のビット列をデータだと見なして読み出す。

アセンブリ言語

機械にとってのプログラムは「主記憶装置(メモリ)に格納された2進数の並び」だが,2進数や16進数の列を人間が読み書きするのはあまりに非効率だ。 そこで普通は,各機械語命令に付けられた「意味を表す名前」(ニーモニック; mnemonic)を使ってプログラムを表記する。 この表記法をアセンブリ言語 (assembly language) と言う。

◆演習1.1-4 下記のコマンドを実行して,hello.hex中の機械語プログラムに対するアセンブリ言語表記を確認しなさい。

$ xxd -r -p hello.hex > h.bin
$ ndisasm -b32 h.bin
00000000  B804000000        mov eax,0x4
00000005  BB01000000        mov ebx,0x1
0000000A  B9A2800408        mov ecx,0x80480a2
0000000F  BA0D000000        mov edx,0xd
00000014  CD80              int 0x80
00000016  B801000000        mov eax,0x1
0000001B  BB00000000        mov ebx,0x0
00000020  CD80              int 0x80
00000022  68656C6C6F        push dword 0x6f6c6c65
00000027  2C20              sub al,0x20
00000029  776F              ja 0x9a
0000002B  726C              jc 0x99
0000002D  64                fs
0000002E  0A                db 0x0a

ndisasmの出力は,左から,先頭から何バイト目かを表す16進数(オフセットとも言う),機械語命令を表す16進数列,その命令をアセンブリ言語で表記したもの,である。16進数列よりアセンブリ言語表記の方がずっと読み書きが楽であることがわかるだろう。

(なお,第00000022バイト以降は文字コードの列であって機械語命令の列ではないが,上記の出力では機械語命令列と誤認識されている。)

演習1.1-5 hello.hexの機械語プログラムの命令数(文字列データを除く)は8だった。同様にして,演習1.1-3の機械語プログラムのアセンブリ言語表記を確認し,命令数がいくつか調べなさい(演習1.1-3のプログラムにはデータ部はなく,すべて機械語命令である)。また,writeシステムコールを実行している箇所,及び,exitシステムコールを実行している箇所を見つけなさい。

逆アセンブルとアセンブル

演習1.1-4, 1.1-5では「機械語プログラムであるビット列をアセンブリ言語表記に変換」した。この作業を逆アセンブル (disassemble) と言い,すでに存在する機械語プログラムの中身を確認する際などに行う。

反対に,機械語プログラムを作る際は,普通まずアセンブリ言語で記述して,それを機械語のビット列に変換する。この作業をアセンブル (assemble) と言い,それを行うソフトウェアをアセンブラ (assembler) と言う。

演習1.1-6 以下のアセンブリ言語プログラムを打鍵入力し,hello.sというファイル名で保存しなさい。その後,下記の手順でアセンブルし,実行しなさい。

        section .text
        global  _start
_start:
        mov     eax, 4          ; writeのサービス番号
        mov     ebx, 1          ; 標準出力
        mov     ecx, msg        ; 文字列の開始番地
        mov     edx, msglen     ; 文字列の長さ
        int     0x80
        mov     eax, 1          ; exitのサービス番号
        mov     ebx, 0          ; 終了コード
        int     0x80
msg:    db      "I'm fine.", 0x0a
msglen: equ     $ - msg
$ nasm -felf hello.s        -- アセンブルし,結果をhello.oに出力
$ ld -m elf_i386 hello.o    -- ELFファイルa.outを生成
$ ./a.out                   -- 実行
I'm fine.

ここまでのまとめ

  • 機械語プログラムとは,主記憶装置(メモリ)に格納された2進数の列だ。コンピュータは,「特定のビットパターンを読み出すと特定の動作をする」ように,論理回路として作り込まれている。
  • 人間が2進数や16進数でプログラムを読み書きするのは非効率的なので,人間が読み書きする際はアセンブリ言語という記法を使って記述する。アセンブリ言語表記を機械語のビット列に変換する作業をアセンブルと言い,それを行うソフトウェアをアセンブラと言う。

コマンド入力を楽にするための設定

今後,nasm コマンドと ld コマンドを頻繁に使うが,上記のようにオプション引数が多いと面倒だしわかりにくい。シェルの設定に追記して,オプション引数無しで実行できるようにしよう。

  1. ~/.bashrc の末尾に以下の2行を追加する(.bashrcがなければ新規作成する)。
    alias nasm='nasm -felf'
    export LDEMULATION=elf_i386

以降,新しく端末を起動すると設定が反映される。 aliasコマンドを引数無しで実行し,alias nasm='nasm -felf' という行が表示されたら,設定がうまくいっている。

この設定を行った後では,上記のアセンブル手順は次のように簡潔になる。

$ nasm hello.s          -- アセンブルし,結果をhello.oに出力
$ ld hello.o            -- a.outを生成
$ ./a.out               -- 実行
I'm fine.

results matching ""

    No results matching ""