
【アセンブリ言語】forループの説明
以前、Cプログラムをアセンブリにデコンパイルし、其れをCに戻す事でアセンブリを学ぶ方法を教えました。
此れに触発されて、アセンブリの基本を教える為に、より短く、集中した記事シリーズを書く事にしました。
繰り返しになりますが、此の記事を読む殆どの人はx86_64(Intel又はAMD)のパソコンを持っているでしょう(スマホの奴隷でない限り)。
其の為、アセンブリコードはx86_64で書きます。
今回は、forループの書き方についてです。
whileループ
其の前に、前の記事で紹介したwhileループを復習しましょう。
2016ca: 8b 45 f8 mov eax, dword ptr [rbp - 0x8]
2016cd: 3b 45 ec cmp eax, dword ptr [rbp - 0x14]
2016d0: 7f 36 jg 0x201708 <main+0x68>
...
201706: eb c2 jmp 0x2016ca <main+0x2a>
御存知かもしんが、此れはC言語では以下に相当します:
while (fahr <= upper) {
...
}
但し、此のアセンブリコードは、Cコードをobjdump
でデコンパイルした為に此の様に書かれています。
手動でアセンブリを書く場合、whileループはNASMを使って以下の様に見えます:
section .data
section .rodata
fahr: dq 0 ; dq = 64ビット
; dd = 32ビット
; dw = 16ビット
; db = 8ビット
upper: dq 300
section .text
global _start
_start:
while_loop:
mov rax, [fahr]
cmp rax, [upper]
jg end_loop
continue_loop:
...
inc qword [fahr]
jmp while_loop
end_loop:
mov rax, 1
mov rbx, 0
syscall
此処で、NASMをアセンブラとして選択した事に注意して下さい。
此れは、Cを含む高級言語とは異なり、アセンブリはアーキテクチャやOSだけでなく、使用するアセンブラによっても異なる為です。
従って、gas
(GNUアセンブラ)でアセンブルする場合の構文は、fasm
(Flatアセンブラ)、masm
(Microsoftマクロアセンブラ)、tasm
(BorlandのTurboアセンブラ)、又はnasm
(Netwideアセンブラ)と異なります。
此れ以降はNASMを使用します。
何故なら、NASMは全てのOSで利用可能で、複数のアーキテクチャに対応し、非常に一般的に使用されており、約30年間アクティブにメンテナンスされています(バージョン3.00は僅か4日前に出ました!)。
又、LinuxやBSDを使用している場合、NASMはおそらく既にインストールされています。
インストールされていない場合でも、ディストリビューションやOSのリポジトリで保母確実に入手可能です。
更に、NASMはBSDライセンスであり、オープンソース及びプロプライエタリなコードの両方で自由に使用出来る為、ゲーム開発者にとって理想的です。
又、.rodata
は初期化された定数変数用、.data
は変更可能な変数用、.bss
は初期化せずにメモリを予約する変数用である点も注目すべきです。
変数を初期化せずに保持するには、64ビットの場合、fahr: resq 1
と定義します。
アセンブリでは、int
、float
、string
、bool
等の区別はなく、全てビットで扱われます。
然し、Cでプログラミングした事がある人(あたしのアセンブリ記事を読んでいる人はそうだと仮定する)は、手動でメモリ管理を行う必要がある事を知っているでしょう。
此れが其の理由です。
forループ
さて、whileループの説明が終わったので、此れ以降はCで書いてデコンパイルするのではなく、手動でアセンブリコードを書きます。
又、アセンブリではデフォルトで各行にコメントを付けるので、此の習慣を身につけて欲しいと思います。
section .rodata
hello: db 'こんにちは、世界',10 ; 改行コード付き
; UTF-8文字列、
; 10 = \n
helloLen: equ $-hello ; 文字列の長さ、
; $ = 現在のアドレス、
; -hello=マイナス文字列の
; アドレス開始アドレス
section .text
global _start
_start:
mov rbx, 0 ; ループカウンターを初期化 (int i = 1;)
begin_loop:
cmp rbx, 5 ; i を5 と比較 (i < 5;)
jge end_loop ; i >=5 の場合にループを終了
continue_loop:
mov rax, 4 ; FreeBSDでの書込のシステムコール番号
mov rdi, 1 ; ファイルディスクリプター1(標準出力)
mov rsi, hello ; 文字列のアドレス
mov rdx, helloLen ; 文字列の長さ
syscall ; カーネルを呼び出す
inc rbx ; ループカウンターを増大 (i++)
jmp begin_loop ; ループの最初に戻る
end_loop:
mov rax, 1 ; FreeBSDでの終了のシステムコール番号
mov rbx, 0 ; 終了ステータス 0 (成功)
syscall ; カーネルを呼び出す
システムコール番号はFreeBSDに特有である事に注意して下さい。
Linuxの場合は、以下を使用して下さい:
section .rodata
hello: db 'こんにちは、世界',10 ; 改行コード付き
; UTF-8文字列、
; 10 = \n
helloLen: equ $-hello ; 文字列の長さ、
; $ = 現在のアドレス、
; -hello=マイナス文字列の
; アドレス開始アドレス
section .text
global _start
_start:
mov rbx, 0 ; ループカウンターを初期化 (int i = 1;)
begin_loop:
cmp rbx, 5 ; i を5 と比較 (i < 5;)
jge end_loop ; i >=5 の場合にループを終了
continue_loop:
mov rax, 1 ; Linuxでの書込のシステムコール番号
mov rdi, 1 ; ファイルディスクリプター1(標準出力)
mov rsi, hello ; 文字列のアドレス
mov rdx, helloLen ; 文字列の長さ
syscall ; カーネルを呼び出す
inc rbx ; ループカウンターを増大 (i++)
jmp begin_loop ; ループの最初に戻る
end_loop:
mov rax, 60 ; Linuxでの終了のシステムコール番号
mov rdi, 0 ; 終了ステータス 0 (成功)
syscall ; カーネルを呼び出す
此のアセンブリコードは、以下のCコードに相当します:
#include <stdio.h>
int main() {
for (int i = 0; i < 5; i++) {
printf("こんにちは、世界\n");
}
return 0;
}
アセンブリでは、コードをコンパイルするのではなく、先ずオブジェクトファイルにアセンブルし、次にバイナリファイルにリンクします。
此れがどれだけ低レベルであるかを示しています!
アセンブルとリンクには、以下の2つのコマンドが必要です:
$ nasm -f elf64 for.asm -o for.o
$ ld for.o -o for
此れでバイナリを実行出来ます:
$ ./for
こんにちは、世界
こんにちは、世界
こんにちは、世界
こんにちは、世界
こんにちは、世界
とても簡単でしたね!
以上