64ビット CPU の遊び方

 [Home]

投稿日 2015/03/01

はじめに

最近の PC やサーバ用 CPU は特別なものを除けば x64 とか AMD64 とか呼ばれる 64ビット CPU が主流です。しかし、スクリプト言語や Java などを使っている限り、そのことを実感できません。実際に実感するためには、やはりアセンブラが一番です。

ということでアセンブラを使って64ビット CPU で遊んでみました。試した環境としては、AWS で稼働している Ubuntu 14.04LTS です。具体的にはこんな感じです。

$ inxi -C
CPU:       Single core Intel Xeon CPU E5-2670 v2 (-MCP-) cache: 25600 KB flags: (lm nx sse sse2 sse3 sse4_1 sse4_2 ssse3) clocked at 2500.044 MHz
$ inxi -S
System:    Host: ip-172-??-??-130 Kernel: 3.13.0-36-generic x86_64 (64 bit) Console: tty 0 Distro: Ubuntu 14.04 trusty

 

アセンブラとは

アセンブラとは正確にはアセンブリー言語とか言います。CPU は機械語だけしか解釈しませんが、命令がその機械語に1対1に対応するような言語です。言い直せば、機械語をシンボルを使って記述する言語みたいな感じです。

Linux の C コンパイラ (gcc) には as というアセンブラが付いてきますが、ここでは nasm というアセンブラを使っています。理由は、けっこうしっかりしたマニュアル (英語ですが) が付いているのと、記述方法が as よりシンプルなためです。

nasm のインストール

nasm のインストールですが、Ubuntu では apt-get で簡単にインストールできます。もし、gcc が入ってない場合は、gcc もインストールしておく必要があります。

$ sudo apt-get install nasm

予備知識

アセンブラは機械語と1対1に対応しています。したがって、アセンブラ (アセンブリー言語) を知っていても、CPU の構成とか機械語を知らないとプログラムは作れません。ここでそこまで書いていたら長くなってしまいますし、本題に入るまでに時間がかかってしまうので、それについては読者が知っているものとします。

 

それでは Hello World から

アセンブラは機械語と1対1ですから、他の言語だと1行ですむようなプログラムもこんなに長くなります。

section  .data                  ; データセクションの定義
message  db 'Hello, World', 0x0a
length   equ $ -message         ; 文字列の長さ
section  .text
global   _start                 ; エントリーポイント

_start:
mov     rcx, message            ; 文字列の先頭アドレス
mov     rdx, length             ; 文字列の長さ
mov     rax, 4                  ; 出力(sys_write)
mov     rbx, 1                  ; ファイルハンドル(1 = 標準出力)
int     0x80                    ; システムコール
mov     rax, 1                  ; sys_exit
mov     rbx, 0                  ; 終了ステータスコード
int     0x80                    ; システムコール

下がその実行例です。

ubuntu@ip-172-??-??-130:~/workspace/nasm$ bin/hello
Hello, World
ubuntu@ip-172-??-??-130:~/workspace/nasm$

ここでメッセージを表示する部分は最初の int 0x80 です。これは何かというと、ソフトウェア割り込み命令ですね。0x80 番の割り込みを行うと rax で指定した番号に対応する OS で用意されているエントリーポイントへ飛んでいくというものです。

最後にも int 0x80 がありますが、こちらはプログラムを終了する (OS に制御を返す) ためのシステムコールです。ところで rax とか rbx とかは 64bit 汎用レジスタと呼ばれるもので、CPU 内部の演算に使用するための一時記憶用高速メモリみたいなものです。mov は move 命令というもので即値データやレジスタのデータなどを他のレジスタにコピーします。

CPU の命令や構造ですが、こちらがわかりやすそうです。


ビルド方法

このプログラムをアセンブル+リンクするには下のようにします。この例で、ソースファイル (.asm) は src/ に、中間ファイル (.o) は obj/ に、実行ファイルは bin/ に置くものとします。

nasm -f elf64 -o obj/$1.o src/$1.asm
ld -s -o bin/$1 obj/$1.o

Linux System Calls

Linux のシステムコールですが、下のサイトに詳細が出ています。全部で 338 種類もあるようです。

System Calls

 

簡単な足し算

次に簡単な足し算をしてみます。今度は、8ビットレジスタ al を使っています。システムコールも 64bit レジスタでなく本来の 32bit レジスタを使っています。また、前の例 (Hello World) ではリンカに直接 ld を使っていましたが、今度は gcc から間接的に使うようにしています。そのため、エントリーポイントの名前は C プログラムと同じ main に変えました。

section .data
  outbuff db 10
  
section .text
global  main
main:
  mov al, 2     ; 2 を al にセット
  add al, 1     ; al + 1
  or  al, 0x30  ; 文字列として表示するため 0x30 を OR
  mov [outbuff], al  ; 表示用バッファにストア
  mov al, 0
  mov [outbuff + 1], al  ; \0 を追加
  mov eax, 4
  mov ecx, outbuff
  mov edx, 2
  int 0x80     ; システムコール
  mov eax, 1
  mov ebx, 0
  int 0x80      ; システムコール

このプログラムのビルド方法ですが、下のようにします。

nasm -f elf64 -o obj/$1.o src/$1.asm
gcc -o bin/$1 obj/$1.o

結果ですが、こんな感じで表示されます。改行されないので見にくいですが、2 行目の最初が "3" となっています。

ubuntu@ip-172-??-??-130:~/workspace/nasm$ bin/add
3ubuntu@ip-172-??-??-130:~/workspace/nasm$

 

C ライブラリが使えるか試してみる

システムコールだけ使っていると、計算値を表示するだけでもけっこう面倒です。実は、C 言語のライブラリ関数が簡単に使えるはずなので試してみました。試したのは、1文字だけ表示する関数 putchar です。

C ライブラリ関数を使うためには、いくつかお約束があります。まず、「main 関数が必要なこと (逆に言えば、メインプログラムは不要)。関数呼び出しは C のお約束に従うこと。」です。具体的には、C 言語のプログラムを -S を付けてコンパイルするとアセンブラソースができるので、それを参考にするとよいです(as のソースなので nasm と文法的に違いますが)。

extern  putchar   # putchar が外部参照であることを示す。

section .text
global  main      # main プログラムの開始
main:
  push  rbp       # main も関数のひとつなので C のお約束に従ってスタックフレームを作る。
  mov   rbp, rsp
  mov   edi, 'A'  # 'A'
  call  putchar   # putchar を呼び出す。
  mov   edi, 0x0a # 改行
  call  putchar   # putchar を呼び出す。
  leave           # スタックフレームを解放する。
  ret             # OS へもどる。

ビルド方法は「簡単な足し算」と同じです。実行するとこんな感じで表示されます。今度は改行を入れたので表示された A の後がちゃんと改行されています。

ubuntu@ip-172-??-??-130:~/workspace/nasm$ bin/putch
A
ubuntu@ip-172-??-??-130:~/workspace/nasm$

ところで、C 言語で main 関数は多態性を持っていますが、main は単なるシンボルなのでコンパイラのチェックがなければ、いろんなパラメータを渡せるし、いろんな型を返せるはずですね。

 

ループしてみる

次のサンプルは繰り返しの例です。結果の表示には高機能な関数 printf を使っています。

もっと速くて短い書き方があるかもしれませんが、ここはわかりやすさと言うことで・・。

extern  printf      # printf が外部参照であることを示す。
section .data
FMT:    db    "%08x", 0x0a  # printf の第一引数

section .text
global  main        # main 関数
main:
  push  rbp         # C 関数のお約束に従ってスタックフレームを作る。
  mov   rbp, rsp
  mov   ecx, 0      # カウンタを初期化 (ecx レジスタはよくカウンタとして使われる)
LOOP:
  mov   esi, 0xfedcba9   # 表示する値の初期値
  add   esi, ecx         # 0xfedcba9 + カウンタ
  push  rcx              # ecx が壊れないようにスタックに退避
  mov   edi, FMT         # 第一引数(フォーマット)を設定
  mov   eax, 0           # 戻り値をクリアしておく
  call  printf           # printf を呼び出す。
  pop   rcx              # 退避しておいた ecx を回復
  inc   ecx              # カウントアップ
  cmp   ecx, 5           # 繰り返し数 5
  jle   LOOP             # カウンタの値が繰り返し数以下ならループ
  leave                  # スタックフレームを解放
  ret                    # OS にもどる。

下がこのサンプルの実行例です。

ubuntu@ip-172-??-??-130:~/workspace/nasm$ bin/printf
0fedcba9
0fedcbaa
0fedcbab
0fedcbac
0fedcbad
0fedcbae
ubuntu@ip-172-??-??-130:~/workspace/nasm$

 

 


 

 開設 2014年12月   著作権 2014-2015 bonk.red  連絡先: こちらからメッセージを送ってください。 (お仕事も大募集)

 このページの先頭へ..