x64 Assembly Language
2017.11.11: created by
概要
"x64" とは Intel と AMD の 32 ビット x86 命令セット・アーキテクチャ (Instruction Set Archtecture)
の64 ビット拡張を指す名称です。時に "x86-64" や "x86_64" と呼ばれることがあります。
この講義では世間で広く使われている用語の使い方にしたがうものとします。
すなわち、Intel の 8086 (16ビット CPU) を拡張した 32bit までの CPUのアーキテクチャをまとめて "x86" と呼び、
さらにその "x86"を64 ビットに拡張したアーキテクチャを "x64" と呼びます。
AMDのアーキテクチャに関しても、"x86"と互換性のある32ビットまでのCPUを"x86"に、
64ビットCPUを"x64"に含めます。
用語のまとめ
-
x86
Intel 8086、およびその後方互換性を持つマイクロプロセッサの命令セットアーキテクチャの総称。
16ビットの8086で登場し、32ビット拡張の80386, 486などインテルの32ビットプロセッサ (後にIA-32と命名)、および AMDなどの互換プロセッサを含む。
更に広義には64ビット拡張のx64を含むことがある。
-
IA-32 (Intel Archtecture 32)
80386の開発の際に定義された、16ビットx86を32ビットに拡張した命令セットアーキテクチャは
元々は「32ビット x86アーキテクチャ」と呼ばれていたが、
Intelが "IA-64" アーキテクチャを発表した際に "IA-32" と改名された。
なお "IA-32" と "IA-64" は名前が似ているが、全く異なるアーキテクチャで互換性がないので、混同しないこと。
-
IA-64 (Intel Archtecture 64)
IntelとHP (ヒューレット・パッカード)が共同開発した Itanium プロセッサ用の
64bit マイクロプロセッサ命令セット・アーキテクチャ。
VLIW (Very Long Word Instruction Set Computer)
用のアーキテクチャであり "x64" とは全く異なるので注意すること。
Itanium プロセッサは x86 プロセッサとの互換性がなかったために、ほとんど流行らずに消えていった。
-
x64
x64またはx86-64とは、x86アーキテクチャを64ビットに拡張した命令セットアーキテクチャ。
実際には、AMDが発表したAMD64命令セット、続けてインテルが採用したIntel 64命令セット(かつてIA-32eまたはEM64Tと呼ばれていた)などを含む、各社のAMD64互換命令セットの総称である。x86命令セットと互換性を持っていることから、広義にはx86にx64を含む場合がある。
x86からx64への拡張
-
汎用レジスタ(General Purpose Register)
IA-32 の8本 (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP) の汎用レジスタに
更に 8 本(R8 〜 R15 )が追加され、ビット幅も32ビットから64ビットに拡張された。
-
XMMレジスタ(Streaming SIMD 命令用)
128ビットの XMM レジスタがIA-32 では8本存在したが、16本に増やされた。
-
アドレス空間
48ビット ($2^{48}$バイト=$2^8$テラバイト=256テラバイト) に拡張された。
アセンブラによる構文の違い
x86やx64のアセンブラは
「インテル構文 (Intel Syntax)」と「AT&T構文」の2種類があります。
次のような違いがあります。
レジスタ名の前に%をつける。
Operation Source Destination の順である。
レジスタ間接アドレッシングは、レジスタ名に"()"をつけて表現する。
Intel構文
レジスタ名の前に%をつけない。
Operation Destination Source の順である。
レジスタ間接アドレッシングは、レジスタ名に"[]"をつけて表現する。
この文書では gcc が採用している AT&T 構文にしたがうものとします。
実験環境
本ページの実行例は以下の環境で実行したものです。
- OS ... macOS Sierra version 10.12.5
- Hardware ... Apple MacBook Pro (Retia, 13-inch, Mid 2014)
- Cコンパイラ ... gcc (Apple LLVM version 8.1.0)
gcc -v の実行結果 |
bash-3.2$ gcc -v
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-
gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.plat
form/Developer/SDKs/MacOSX10.12.sdk/usr/include/c++/4.2.1
Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin16.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault
.xctoolchain/usr/bin
|
- アセンブラ ... as (Apple LLVM version 8.1.0)
as -v の実行結果 |
bash-3.2$ as -v
Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin16.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault
.xctoolchain/usr/bin
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain
/usr/bin/clang" -cc1as -triple x86_64-apple-macosx10.12.0 -filetype obj -main-fi
le-name - -target-cpu penryn -fdebug-compilation-dir /Users/nitta/Documents/i486
-dwarf-debug-producer Apple LLVM version 8.1.0 (clang-802.0.42) -dwarf-version=
4 -mrelocation-model pic -o a.out -
Control-D
|
アセンブリ言語のディレクティブ
Apple公式へジャンプ
現在のセクションを指定するディレクティブ
section segname , sectname [[[ , type ] , attribute ] , sizeof_stub ]
|
指定したセクションを開始する
ディレクティブ | セクション | 説明 |
.text
|
__TEXT,__text
|
命令を配置するセクションを開始する
|
.data
|
__DATA,__data
|
初期値を持つデータを配置するセクションを開始する
|
Location Counter を動かすディレクティブ
.align align_expression [ , 1byte_fill_expression [, max_bytes_to_fill] ]
.p2align align_expression [ , 1byte_fill_expression [, max_bytes_to_fill] ]
.p2alignw align_expression [ , 2byte_fill_expression [, max_bytes_to_fill] ]
.p2alignl align_expression [ , 4byte_fill_expression [, max_bytes_to_fill] ]
.align32 align_expression [ , 4byte_fill_expression [, max_bytes_to_fill] ]
|
align ディレクティブは location counter を align_expression の境界に動かす。
$2^{0}$ から $2^{15}$ まで指定できる。
(例) .align 3 $2^3 = 8$ バイトのアラインメントに合わせる
.org expression [ , fill_expression ]
|
location counter をその値に設定する。
fill_expression が指定された場合は、
現在のlocation counter と新しい値の間をfill_expressionの値で埋める。
(例) .org 0x100 100H番地から開始する
データ生成用ディレクティブ
.ascii [ "string" ] [ , "string" ] ...
.asciiz [ "string" ] [ , "string" ] ...
|
.ascii はアスキー文字列を生成する。.asziizは最後に'\0' を追加する。
(例) .ascii "apple\0"
.asciiz "apple"
.byte [ expression ] [ , expression ] ...
1バイトデータを生成する
.short [ expression ] [ , expression ] ...
2バイトデータを生成する
.long [ expression ] [ , expression ] ...
4バイトデータを生成する
.quad [ expression ] [ , expression ] ...
8バイトデータを生成する
|
(例)
.byte 74,0112,0x4A,0x4a,'J | the same byte
.short 64206,0175316,0xface | the same short
.long -1234,037777775456,0xfffffb2e | the same long
.quad -1234,01777777777777777775456,0xfffffffffffffb2e | the same quad
シンボルを扱うディレクティブ
symbol_name を外部に見せる( external にする)。
x64 アセンブラの表記法
命令を表すニーモニックの最後についている1文字で、扱うデータのバイト長を表します。
命令の後置記号 | ビット幅 | バイト幅 | 備考 | 例 |
b | 8 | 1 | byte | movb |
w | 16 | 2 | word | movw |
l | 32 | 4 | long | movl |
q | 64 | 8 | quad | movq |
レジスタの前または後についている1文字で、扱うレジスタのバイト長を表します。
レジスタ{前|後}置記号 | 場所 | ビット幅 | バイト幅 | 備考 | 例 |
h | 後 | 8 | 1 | high | %ah |
l | 後 | 8 | 1 | low | %hl |
x | 後 | 16 | 2 | eXtended | %ax |
e | 前 | 32 | 4 | Extended | %eax |
r | 前 | 64 | 8 | | %rax |
(例)
movb $1, %al # 16ビット ax レジスタの下位8bitへ
movb $1, %ah # 16ビット ax レジスタの上位8bitへ
movw $1, %ax # 16bitビット ax レジスタへ
movl $1, %eax # 32bitビット eax レジスタへ
movq $1, %rax # 64bitビット rax レジスタへ
Cコンパイラとアセンブラ
x64 のプログラミングにおいて、レジスタの使い方はOSによって異なる部分があります。
それぞれのOSの関連文書にざっと目を通しておくことを勧めます。
汎用レジスタの使い方
| 32 ビット Windows | 32 ビット Linux | 64 ビット Windows | 64 ビット Linux |
{e|r}ax | 戻り値を入れるレジスタ |
{e|r}cx | 自由に使っていいレジスタ | 引数を入れるレジスタ |
{e|r}dx | 自由に使っていいレジスタ | 引数を入れるレジスタ |
{e|r}bx | 自由に使っていいレジスタ | 保存しなければならないレジスタ |
{e|r}sp | スタックポインタ |
{e|r}bp | 元の値を保存しなければならないレジスタ |
{e|r}si | 元の値を保存しなければならないレジスタ | 引数を入れるレジスタ |
{e|r}di | 元の値を保存しなければならないレジスタ | 引数を入れるレジスタ |
r8~9 | - | 引数を入れるレジスタ |
r10~11 | - | 自由に使っていいレジスタ |
r12~15 | - | 元の値を保存しなければならないレジスタ |
関数呼び出しの際に、x86では 引数はすべてスタックに積むのが一般的(処理系依存でレジスタ渡しの場合もありましたが)でしたが、
x64 においては一部の引数をレジスタで渡し、残りをスタックに積んで渡します。
このときに、どのレジスタを使うか、レジスタで渡す引数分のメモリをスタック上に確保するかどうか、
などが処理系依存です。
System V ABI (Application Binary Interface)
は「関数呼出規約(calling convention)」や「実行可能オブジェクトやリンキング・ファイルのフォーマット (Executable and Linkable Format (ELF)」
などを定めた仕様であり、Linuux や BSD をはじめとする主要なUnixで採用されています。その中で x86-64 の呼び出し規約は次のように定義されています。
関数へのパラメータは rdi, rsi, rdx, rcx, r8, r9 レジスタで渡され、これ以上のパラメータはスタックに逆順で積まれる。
関数は call 命令によって呼び出され、次の命令のアドレスをスタックにpushし、指定されたアドレスにjumpする。
関数から戻るのは ret 命令によってであり、スタックからアドレスを取り出してそこにjumpする。
call命令を実行する時はスタックは16byteにalignされていなければならない。
関数では、rbp, r12, r13, r14, r15 レジスタの値は保持する(変更されない)が、
rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11 の値は保持されない。
x64のWindowsでは
最初の4個の引数は rcx, rdx, r8, r9 レジスタ(およびXMM0 〜 XMM3)を用いて、残りはスタックに積んで渡されます。
引数がレジスタで渡される場合でも、スタック上には必ず4個分のパラメータのための領域が確保されます。
Apple の
Introduction ot OS X ABI Function Call Guide
によれば、
Apple の
x86-64 Code Model によれば
ローカルなデータへのアクセスは RIP レジスタからの間接アドレッシングによってアクセスしなくてはいけません。
関数呼び出しの戻り値は、64ビットに収まるスカラー型は rax に入れて、非スカラー型は XMM0 に入れて返されます。
コマンド
$ gcc -O -S sample.c
Cのソースファイル sample.c をコンパイルしてアセンブリ言語のファイル sample.s を生成する
$ as sample.s -o sample.o
アセンブリ言語のファイル sample.s をアセンブルしてオブジェクトファイル sample.o を生成する
$ ld sample.o -e _main -o a.out
オブジェクトファイルやライブラリをリンクして実行可能ファイル a.out を生成する
$ ./a.out
実行可能ファイル a.out の実行を開始する
値を返す関数のアセンブリ・コード
sample1.c |
int sample1() {
return 5;
}
|
sample1.s 生成方法: gcc -O -S sample1.c |
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 12
.globl _sample1
.p2align 4, 0x90
_sample1: ## @sample1
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
movl $5, %eax
popq %rbp
retq
.cfi_endproc
.subsections_via_symbols
|
引数を渡す関数のアセンブリ・コード
sample2の実行例 |
vermeer-2:i486 nitta$ gcc -O sample2a.c sample2b.c sample2c.c -o sample2
vermeer-2:i486 nitta$ ./sample2
450
|
sample2a.c |
int sum(int x1,int x2, int x3, int x4, int x5, int x6, int x7, int x8, int x9) {
int ret = 0;
ret = x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9;
return ret;
}
|
sample2a.s 生成方法: gcc -O -S sample2a.c |
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 12
.globl _sum
.p2align 4, 0x90
_sum: ## @sum
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
## kill: %ESI<def> %ESI<kill> %RSI<def>
## kill: %EDI<def> %EDI<kill> %RDI<def>
leal (%rdi,%rsi), %eax
addl %edx, %eax
addl %ecx, %eax
addl %r8d, %eax
addl %r9d, %eax
addl 16(%rbp), %eax
addl 24(%rbp), %eax
addl 32(%rbp), %eax
popq %rbp
retq
.cfi_endproc
.subsections_via_symbols
|
sample2b.c |
#include <stdio.h>
extern int sum(int,int,int,int,int,int,int,int,int);
void foo() {
int ret = 0;
ret = sum(10, 20, 30, 40, 50, 60, 70, 80, 90);
printf("%d\n",ret);
}
|
sample2b.s 生成方法: gcc -O -S sample2b.c |
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 12
.globl _foo
.p2align 4, 0x90
_foo: ## @foo
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
subq $8, %rsp
movl $10, %edi
movl $20, %esi
movl $30, %edx
movl $40, %ecx
movl $50, %r8d
movl $60, %r9d
pushq $90
pushq $80
pushq $70
callq _sum
addq $32, %rsp
movl %eax, %ecx
leaq L_.str(%rip), %rdi
xorl %eax, %eax
movl %ecx, %esi
popq %rbp
jmp _printf ## TAILCALL
.cfi_endproc
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "%d\n"
.subsections_via_symbols
|
sample2c.c |
extern void foo();
int main(int argc, char** argv) {
foo();
return 0;
}
|
sample2c.s 生成方法: gcc -O -S sample2c.c |
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 12
.globl _main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
xorl %eax, %eax
callq _foo
xorl %eax, %eax
popq %rbp
retq
.cfi_endproc
.subsections_via_symbols
|
Cコンパイラのオプティマイズ(最適化)・オプション
sample3.c |
extern void exit(int);
int main(int argc,char** argv) {
exit(0);
}
|
sample3.s 生成方法: gcc -S sample3.c |
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 12
.globl _main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $5, %eax
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
movl %eax, %edi
callq _exit
.cfi_endproc
.subsections_via_symbols
|
sample3.s 生成方法: gcc -O -S sample3.c |
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 12
.globl _main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
movl $5, %edi
callq _exit
.cfi_endproc
.subsections_via_symbols
|
sample3.c をオプティマイズ・オプションを変えてコンパイルしてみる。 オブジェクト・ファイルを逆アセンブルして出力されたアセンブル言語を調べる。 実行可能ファイルを生成して実行する。 |
vermeer-2:i486 nitta$ gcc -c -g sample3.c
vermeer-2:i486 nitta$ objdump -d -S sample3.o
sample3.o: file format Mach-O 64-bit x86-64
Disassembly of section __TEXT,__text:
_main:
; int main(int argc,char** argv) {
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 48 83 ec 10 subq $16, %rsp
8: b8 05 00 00 00 movl $5, %eax
d: c7 45 fc 00 00 00 00 movl $0, -4(%rbp)
14: 89 7d f8 movl %edi, -8(%rbp)
17: 48 89 75 f0 movq %rsi, -16(%rbp)
; exit(5);
1b: 89 c7 movl %eax, %edi
1d: e8 00 00 00 00 callq 0 <_main+0x22>
vermeer-2:i486 nitta$ gcc -O -c -g sample3.c
vermeer-2:i486 nitta$ objdump -d -S sample3.o
sample3.o: file format Mach-O 64-bit x86-64
Disassembly of section __TEXT,__text:
_main:
; int main(int argc,char** argv) {
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
; exit(5);
4: bf 05 00 00 00 movl $5, %edi
9: e8 00 00 00 00 callq 0 <_main+0xE>
vermeer-2:i486 nitta$ gcc -O sample3.c -o sample3
vermeer-2:i486 nitta$ ./sample3
vermeer-2:i486 nitta$ echo $?
5
vermeer-2:i486 nitta$
|
C言語の関数内にアセンブリ言語のコードを記述する
sample4.c |
#include <stdio.h>
int sum(int x1,int x2, int x3, int x4, int x5, int x6, int x7, int x8, int x9) {
int ret;
__asm__(" \n\
leal (%%rdi,%%rsi), %%eax \n\
addl %%edx, %%eax \n\
addl %%ecx, %%eax \n\
addl %%r8d, %%eax \n\
addl %%r9d, %%eax \n\
addl 16(%%rbp), %%eax \n\
addl 24(%%rbp), %%eax \n\
addl 32(%%rbp), %%eax \n\
"
: "=a"(ret)
);
return ret;
}
void foo() {
int ret = 0;
ret = sum(10, 20, 30, 40, 50, 60, 70, 80, 90);
printf("%d\n",ret);
}
int main(int argc, char** argv) {
foo();
return 0;
}
|
show_log('sample4.c をコンパイルして実行する', 'src/sample4.log'); ?>
演習
macOS で演習を行うものとします。
x64 のアセンブリ言語で書いたプログラムを実行させてみることにしましょう。
macOS 上で「ターミナル」すなわちシェル(対話環境)を起動して下さい。
プログラムの多くの部分はC言語で記述し、部分的に x64 のアセンブリ言語で記述することにしましょう。
課題5a: if文のコードを調べる
if 文のコードを調べなさい。
適切なCの関数を ex5a.c に定義し、gccを使ってアセブリ言語のコード ex5a.s を出力し、
それにわかりやすいコメントを書き加えてから 提出しなさい。
GCCのアセンブリ言語の中では、
- /* と */ に囲まれた領域
- # を書いた後その行の終りまで
がコメントとなります。
あまりに簡単な条件式でコンパイラが成り立つか否かを判断できる場合は、
if 文のコードをださないことがあることに注意して下さい。
つまり、以下のようなCのコードを書いても駄目だ、ということです。
(例)
if (1 > 0) { ... } ←コンパイル時に、いつでも真と判明する
(例2)
int a = 500, b=450;
if (a < b) { ... } ← コンパイル時に、いつでも偽と判明する
課題6a: for文のコードを調べる
for 文のコードを調べなさい。
適切なCの関数を ex6a.c に定義し、gccを使ってアセブリ言語のコード ex6a.s を出力し、
それにわかりやすいコメントを書き加えてから提出して下さい。
課題7a: 値を返す関数をアセンブリ言語で記述する
1から「引数で指定された整数」までの和を返す関数を
機械語で書きなさい。
Cの関数の中に __asm__ 文でアセンブリ言語の命令を書くこと。
それにわかりやすいコメントを書き加えてから提出しなさい。
課題8a: 引数を受け取り値を返す関数をアセンブリ言語で記述する
引数として与えられた 64bit整数が奇数であれば1を、
奇数でなければ 0 を返す関数 int isodd(int n) の
本体をアセンブリ言語で書きなさい。
ただし、
多少実行速度が遅くても構わないので、__asm__ の中では
機械語命令としては
「andq, movq だけ」
を使うこと。ラベル、レジスタ名、定数などは自由に使用して構わない。
動作することを確認したら、ソースを提出しなさい。
ex8a.c |
long isodd(long n) {
long ret = 0;
__asm__(" \n\
# andq と movq 命令だけを使って題意の動作をするプログラムを書きなさい \n\
"
: "=a"(ret));
return ret;
}
|
oddmain.c |
#include <stdio.h>
#include <stdlib.h>
extern long isodd(int);
int main(int argc, char **argv) {
long n;
printf("input integer: ");
scanf("%ld",&n);
printf("%ld\n",isodd(n));
}
|
oddmain.c + ex8a.c の実行例 |
bash-3.2$ gcc -O oddmain.c ex8a.c -o ex8a
bash-3.2$ ./ex8a
input integer: 5
1
bash-3.2$ ./ex8a
input integer: 4
0
|
[注意] C言語で関数を作っておいてそれをコンパイルしてアセンブリ言語を出させる方法で、
上記の機械語だけを使ったコードがでるかもしれません。
課題8b: 引数を受け取り値を返す関数をアセンブリ言語で記述する
引数として与えられた 64bit整数が奇数であれば1を、
奇数でなければ 0 を返す関数 int isodd(int n) の
本体をアセンブリ言語で書きなさい。
ただし、
多少実行速度が遅くても構わないので、__asm__ の中では
機械語命令としては
「movq, testq, jz, jmp だけ」
を使うこと。ラベル、レジスタ名、定数などは自由に使用して構わない。
動作することを確認したら、ソースを提出しなさい。
ex8b.c |
long isodd(long n) {
long ret = 0;
__asm__(" \n\
# movq, testq, jz, jmp 命令だけを使って題意の動作をするプログラムを書きなさい。
# jump 先を指定するためにラベルを定義する必要があります。
"
: "=a"(ret));
return ret;
}
|
oddmain.c + ex8b.c の実行例 |
bash-3.2$ gcc -O oddmain.c ex8b.c -o ex8b
bash-3.2$ ./ex8b
input integer: 5
1
bash-3.2$ ./ex8b
input integer: 4
0
|
[注意] C言語で関数を作っておいてそれをコンパイルしてアセンブリ言語を出させる
方法では、上記の機械語だけを使ったコードはでないでしょう。
自分で考えて書くことが重要です。
追加問題
課題9a: 再帰関数をアセンブリ言語で記述する
フィボナッチ数を再帰的に計算する関数 long fib(long) の本体をアセンブリ言語で書きなさい。
fib(0) = 1
fib(1) = 1
fib(n) = fib(n-2) + fib(n-1)
基本的に次と同等の動作をする関数を定義してほしい。
fib.c |
long fib(long n) {
long result = 1L;
if (n >= 2) {
result = fib(n-1) + fib(n-2);
}
return result;
}
|
ただし、Cコンパイラに余計なことさせないために関数宣言は void fib(void) とすること。
また、__asm__ の中で使うことのできる機械語命令は、以下の種類だけとする。
movq, addq, subq, cmpq, pushq, popq, jl, jle, jz, jn, jne, jmp
ラベル、レジスタ名、定数などは自由に使用して構わない。
メモリとのデータのやり取りが最小限になるように工夫すること。
すなわち、引数や途中結果などを保存するには、
値が保存されることが保証されているレジスタ(たとえば rbx, r12 〜 r15 など)
を pushq して用いること。
動作することを確認したら、ソースを提出しなさい。
[注意]
gcc ではleaf function (他の関数を呼び出さない関数)に対する最適化として
Red zone を使用することがある。
すなわち、スタック・ポインタを動かさずに、スタックポインタよりも 128 byte 小さい
アドレスまでのメモリ領域を勝手に使うことがある。
したがって、gccがleaf functionとして認識している関数の中で、__asm__ の中で
callq (関数呼び出し)を行う場合は、-mno-red-zone オプションを使って
Red zoneによる最適化を禁止する必要がある。
ex9a.cの骨子 void fib(void) として宣言する |
void fib(void) {
// push %rbp
// movq %rsp, %rbp
// これより上は、 コンパイラが自動的に生成する
__asm__ __volatile__(
// %%rdi に引数が入っている
// 返り値は %%rax に入れる
:
);
// これより下はコンパイラが自動的に生成する
// pop %rbp
// retq
}
|
fibmain.c |
#include <stdio.h>
#include <stdlib.h>
extern long fib(long);
int main(int argc, char **argv) {
long n;
printf("input integer: ");
scanf("%ld",&n);
printf("%ld\n",fib(n));
}
|
ex9a.c + fibmain.c の実行例 |
monet:src nitta$ gcc -mno-red-zone -O ex9a.c fibmain.c -o ex9a
monet:src nitta$ ./ex9a
input integer: 1
1
monet:src nitta$ ./ex9a
input integer: 2
2
monet:src nitta$ ./ex9a
input integer: 5
8
monet:src nitta$ ./ex9a
input integer: 20
10946
|
補足説明
__asm__文では次のシンタックスで、C言語の変数をアセンブリ言語の記述の中で利用することができる。
__asm__( 機械語命令 : output : input : 値を変更するregister)
便利な機能ではあるが、一部無駄が生じることがあるので注意が必要である。
たとえば下の例では、ret変数 -16(%rbp) と %rax レジスタの間で無駄な転送が行われている。
ex9b.c |
long fib(long n) {
long ret;
__asm__ __volatile__ ("movq $1, %%rax ;" // 返り値に初期値1を入れてから
"movq %1, %%r10 ;" // 引数 n の値を %r10 (自由に使える)へ
"cmpq $2, %%r10 ;" // n < 2 ならば MYLABEL01 へジャンプ
"jl MYLABEL01 ;"
"subq $1, %%r10 ;" // r10 = n - 1
"movq %%r10, %%rdi ;"
"callq _fib ;" // fib(n-1)
"movq %%rax, %0 ;" // ret に返りを保存
"movq %1, %%r10 ;" // %r10 = n - 2
"subq $2, %%r10 ;"
"movq %%r10, %%rdi ;"
"callq _fib ;" // fib(n-2)
"addq %0, %%rax ;" // %rax = fib(n-2) + ret
"MYLABEL01: ;"
"movq %%rax, %0 ;" // ret = %rax
: "=g"(ret) // %0 としてアクセスできる(出力用)
: "g"(n)); // %1 としてアクセスできる(入力用)
return ret;
}
|
ex9b.s 生成方法: gcc -mno-red-zone -O -S ex9b.c |
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 12
.globl _fib
.p2align 4, 0x90
_fib: ## @fib
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
Lcfi1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
subq $16, %rsp
movq %rdi, -8(%rbp)
## InlineAsm Start
movq $1, %rax
movq -8(%rbp), %r10
cmpq $2, %r10
jl MYLABEL01
subq $1, %r10
movq %r10, %rdi
callq _fib
movq %rax, -16(%rbp)
movq -8(%rbp), %r10
addq $-2, %r10
movq %r10, %rdi
callq _fib
addq -16(%rbp), %rax
MYLABEL01:
movq %rax, -16(%rbp)
## InlineAsm End
movq -16(%rbp), %rax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
.subsections_via_symbols
|
fibmain2.c |
#include <stdio.h>
#include <stdlib.h>
extern long fib(long);
int main(int argc, char **argv) {
printf("%ld\n",fib(45L));
}
|
ex9b.c + fibmain2.c の実行例 |
monet:src nitta$ gcc -mno-red-zone -O ex9b.c fibmain2.c -o ex9b-time
monet:src nitta$ time ./ex9b-time
1836311903
real 0m7.395s
user 0m7.370s
sys 0m0.006s
|
次のようなコードでもまだ無駄がある。
引数 n とメモリ -8(%rbp) の間で無駄なデータ転送が行われる。
ex9c.c |
long fib(long n) {
long ret;
__asm__ __volatile__ ("movq $1, %%rax ;" // 返り値に初期値1を入れてから
"movq %1, %%rdi ;" // 引数 n の値を %rdi へ
"cmpq $2, %%rdi ;" // n < 2 ならば MYLABEL01 へジャンプ
"jl MYLABEL01 ;"
"subq $1, %%rdi ;" // r10 = n - 1
"callq _fib ;" // fib(n-1)
"movq %%rax, %0 ;" // ret に返りを保存
"movq %1, %%rdi ;" // %rdi = n - 2
"subq $2, %%rdi ;"
"callq _fib ;" // fib(n-2)
"addq %0, %%rax ;" // %rax = fib(n-2) + ret
"MYLABEL01: ;"
"movq %%rax, %0 ;" // ret = %rax
: "=g"(ret) // %0 としてアクセスできる(出力用)
: "g"(n)); // %1 としてアクセスできる(入力用)
return ret;
}
|
ex9c.s 生成方法: gcc -mno-red-zone -O -S ex9c.c |
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 12
.globl _fib
.p2align 4, 0x90
_fib: ## @fib
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
Lcfi1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
subq $16, %rsp
movq %rdi, -8(%rbp)
## InlineAsm Start
movq $1, %rax
movq -8(%rbp), %r10
cmpq $2, %r10
jl MYLABEL01
subq $1, %r10
movq %r10, %rdi
callq _fib
movq %rax, -16(%rbp)
movq -8(%rbp), %r10
addq $-2, %r10
movq %r10, %rdi
callq _fib
addq -16(%rbp), %rax
MYLABEL01:
movq %rax, -16(%rbp)
## InlineAsm End
movq -16(%rbp), %rax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
.subsections_via_symbols
|
ex9c.c + fibmain2.c の実行例 |
monet:src nitta$ gcc -mno-red-zone -O ex9c.c fibmain2.c -o ex9c-time
monet:src nitta$ time ./ex9c-time
1836311903
real 0m7.368s
user 0m7.342s
sys 0m0.006s
|
参考
ex9a.c + fibmain2.c の実行例 |
monet:src nitta$ gcc -mno-red-zone -O ex9a.c fibmain2.c -o ex9a-time
monet:src nitta$ time ./ex9a-time
1836311903
real 0m5.927s
user 0m5.908s
sys 0m0.006s
|
http://karel.tsuda.ac.jp/