 | レベル: 中級 Ram Narayan (rnaraya2@in.ibm.com), Software Engineer, IBM
2007年 10月 17日 この記事では、Linux® で最も一般的な 2 つのアセンブラーである GAS (GNU Assembler) と NASM (Netwide Assembler) の間での、構文や意味体系の重要な違いについて説明します。比較する項目は、基本的な構文や、変数やメモリー・アクセスの方法、マクロの処理、関数や外部ルーチン、スタックの処理、コード・ブロックを容易に繰り返すための手法などです。
はじめに
アセンブリー言語によるプログラミングでは、他の言語の場合と異なり、プログラムの対象となっているマシンのプロセッサーのアーキテクチャーを理解する必要があります。アセンブリー言語で記述されたプログラムはまったく移植性がなく、維持管理や理解が大変なことが多く、しかも膨大な行数のコードを含むことが多いものです。しかし、こうした制約があるかわりに、そのマシンで実行されるランタイム・バイナリーは高速でサイズが小さいという利点があります。
Linux でのアセンブリー・レベルのプログラミングに関する情報は既に豊富にありますが、この記事では構文の違いを具体的に示すことにします。そうすることで、1 つの流儀のアセンブリー・プログラミングから別のアセンブリー・プログラミングへの変換が容易になるはずです。この記事は、私自身がこの変換を改善するために追求してきた成果の中から生まれたものです。
この記事では一連のプログラム・サンプルを使います。各プログラムは何らかの機能を示しており、また各プログラムの後に構文の説明と比較を行います。NASM と GAS の間の違いをすべて説明することは不可能ですが、中心となるポイントを説明するよう努めることで、詳しく調べるための基礎となるように心がけます。また既に NASM と GAS の両方に慣れている人であっても、マクロなど、ここで説明することが何らかの参考になるはずです。
この記事は、アセンブリー言語の用語に関して少なくとも基本部分は理解していること、そして Intel® の構文を使ってアセンブラーでプログラミングしたことがあることを前提とします (例えば Linux または Windows で NASM を使ったことがある、など)。この記事は、エディターへのコードの入力方法や、アセンブルやリンクの方法を教えるためのものではありません (これについては「囲み記事」を見て思い出してください)。また Linux オペレーティング・システムに慣れていること (どの Linux ディストリビューションでも構いません。私は Red Hat と Slackware を使いました)、そして gcc や ld など基本的な GNU ツールにも慣れていることが必要とされ、また x86 マシンでプログラミングする必要があります。
ここで、この記事で説明することと、説明しないことについて書いておきましょう。
 |
サンプルを作成する
アセンブル:
GAS の場合:
as –o program.o program.s
NASM の場合:
nasm –f elf –o program.o program.asm
リンク (両方のアセンブラーに共通):
ld –o program program.o
外部の C ライブラリーを使う場合のリンク:
ld –-dynamic-linker /lib/ld-linux.so.2 –lc –o program program.o
|
|
この記事では以下の内容について説明します。
- NASM と GAS の間の基本的な構文の違い
- 変数やループ、ラベル、マクロなど、アセンブリー・レベルの一般的な構成体
- 外部 C ルーチンの呼び出し方と関数の使い方についての簡単な説明
- アセンブリーのニーモニックの違いと使い方
- メモリー・アドレッシングの方法
この記事では以下の内容については説明しません。
- プロセッサーの命令セット
- あるアセンブラーに特有の、さまざまな形式のマクロやその他の構成体
- NASM または GAS のいずれかに特有のアセンブラー・ディレクティブ
- 共通に使われることのない機能、または一方のアセンブラーのみにあり他方にはない機能
詳細の情報は、正式なアセンブラー・マニュアルを参照してください (「参考文献」にリンクがあります)。こうしたマニュアルは、最も完全な情報ソースです。
基本的な構造
リスト 1 は、単純に終了コード 2 で終了する非常に簡単なプログラムを示しています。この小さなプログラムは、GAS と NASM 両方のアセンブリー・プログラムの基本的な構造を説明しています。
リスト 1. 終了コード 2 で終了するプログラム
| Line | NASM | GAS |
|---|
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
|
|
; Text segment begins
section .text
global _start
; Program entry point
_start:
; Put the code number for system call
mov eax, 1
; Return value
mov ebx, 2
; Call the OS
int 80h
|
|
# Text segment begins
.section .text
.globl _start
# Program entry point
_start:
# Put the code number for system call
movl $1, %eax
/* Return value */
movl $2, %ebx
# Call the OS
int $0x80
|
|
これについて少し説明しましょう。
NASM と GAS の最大の違いの 1 つは構文です。GAS は AT&T 構文を使いますが、これは GAS や一部の古いアセンブラーに特有の比較的初期の頃の構文です。一方 NASM は、TASM や MASM など大多数のアセンブラーがサポートする Intel 構文を使います (GAS の最近のバージョンでは、GAS で Intel 構文を使える .intel_syntax というディレクティブをサポートしています)。
下記は GAS のマニュアルから要約した、主な違いです。
- AT&T 構文と Intel 構文ではソース・オペランドとデスティネーション・オペランドの順序が逆です。例えば下記のとおりです。
- Intel:
mov eax, 4
- AT&T:
movl $4, %eax
- AT&T 構文では即値オペランドの前に
$ が付きますが、Intel 構文では付きません。例えば次のとおりです。
- Intel:
push 4
- AT&T:
pushl $4
- AT&T 構文ではレジスター・オペランドの前に
% が付きますが、Intel 構文では付きません。
- AT&T 構文では、メモリー・オペランドのサイズはオペコード名の最後の文字で決まります。オペコードの接尾辞の
b、w、l は、byte (8 ビット)、word (16 ビット)、そして long (32 ビット) のメモリー参照を指定します。Intel 構文はこれを、(オペコード自体ではなく) メモリー・オペランドの前に byte ptr と word ptr、dword ptr を付けることで実現します。従って次のようになります。
- Intel:
mov al, byte ptr foo
- AT&T:
movb foo, %al
- 即値形式のロング・ジャンプとロング・コールは、AT&T 構文では
lcall/ljmp $section, $offset ですが、Intel 構文では call/jmp far section:offset です。far リターン命令は、AT&T 構文では lret $stack-adjust ですが、Intel 構文では ret far stack-adjust を使います。
どちらのアセンブラーでもレジスターの名前は同じですが、レジスターを使うための構文は異なり、またアドレッシング・モードの構文も異なります。さらに、GAS ではアセンブラー・ディレクティブは「.」で始まりますが、NASM ではそうではありません。
.text セクションは、プロセッサーがコードの実行を開始する場所です。キーワード global (GAS では .globl または .global も可) は、リンカーからシンボルが見えるように、またオブジェクトにリンクする他のモジュールでシンボルを利用できるようにするために使われます。リスト 1 で NASM 側の global _start は、見える識別子としてシンボル _start をマーキングしています。こうすることでリンカーは、プログラムのどこにジャンプして実行を開始すればよいのかを知ることができます。GAS も NASM の場合と同じく、プログラムのデフォルトのエントリー・ポイントとして _start ラベルを探します。GAS の場合も NASM の場合も、ラベルは常にコロンで終わります。
割り込みは OS に対して、OS のサービスが要求されていることを知らせます。このプログラムでは、16 ライン目の int 命令が割り込みを行います。GAS も NASM も、割り込みに対して同じニーモニックを使います。GAS は 16 進数を指定するために接頭辞 0x を使いますが、NASM は接尾辞 h を使います。GAS では即値オペランドは接頭辞 $ が付くので、16 進の 80 は $0x80 です。
int $0x80 (つまり NASM では80h) は Linux を呼び出してサービスを要求するために使われます。サービス・コードは EAX レジスターの中にあります。値 1 (Linux 終了システム・コール) を EAX に保存すると、プログラムの終了を要求することになります。レジスター EBX には、OS に返される数字である、終了コード (この場合には 2) が含まれています (この数字を追跡するには、コマンド・プロンプトで echo $? と入力します)。
最後に、コメントについて触れておくと、GAS は C スタイルのコメント (/* */) も、C++ スタイルのコメント (//) も、シェル・スタイルのコメント (#) もサポートしています。NASM は文字「;」で始まる 1 行のコメントをサポートしています。
変数とメモリー・アクセス
このセクションは、3 つの数字から最大のものを見つけるサンプル・プログラムから始めます。
リスト 2. 3 つの数字から最大のものを見つけるプログラム
| Line | NASM | GAS |
|---|
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
|
|
; Data section begins
section .data
var1 dd 40
var2 dd 20
var3 dd 30
section .text
global _start
_start:
; Move the contents of variables
mov ecx, [var1]
cmp ecx, [var2]
jg check_third_var
mov ecx, [var2]
check_third_var:
cmp ecx, [var3]
jg _exit
mov ecx, [var3]
_exit:
mov eax, 1
mov ebx, ecx
int 80h
|
|
// Data section begins
.section .data
var1:
.int 40
var2:
.int 20
var3:
.int 30
.section .text
.globl _start
_start:
# move the contents of variables
movl (var1), %ecx
cmpl (var2), %ecx
jg check_third_var
movl (var2), %ecx
check_third_var:
cmpl (var3), %ecx
jg _exit
movl (var3), %ecx
_exit:
movl $1, %eax
movl %ecx, %ebx
int $0x80
|
|
上を見ると、メモリー変数の宣言にいくつかの違いがあることがわかると思います。NASM は dd、dw、そして db ディレクティブを使ってそれぞれ 32 ビット、16 ビット、8 ビットの数字を宣言していますが、GAS はそうした宣言に .long、.int、そして .byte を使います。GAS には、.ascii や .asciz、.string など、他にもディレクティブがあります。GAS では、他のラベルとまったく同じように (コロンを使って) 変数を宣言しますが、NASM ではメモリー割り当てディレクティブ (dd や dw など) の前に (コロンを付けずに) 単純に変数名を入力し、その後に変数の値を入力します。
リスト 2 の 18 ライン目は、メモリー間接アドレッシング・モードを示しています。NASM はメモリー・ロケーションで指示されるアドレスにある変数を、[var1] のように大括弧を使って間接参照します。GAS は同じ値を間接参照する場合には、(var1) のように小括弧を使います。他のアドレッシング・モードの使い方は、この記事の後の方で説明します。
マクロを使う
リスト 3 はこのセクションの概念を示しています。このプログラムは入力としてユーザーの名前を受け付け、あいさつの言葉を返します。
リスト 3. ストリングを読み取り、ユーザーにあいさつを表示するプログラム
| Line | NASM | GAS |
|---|
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
|
|
section .data
prompt_str db 'Enter your name: '
; $ is the location counter
STR_SIZE equ $ - prompt_str
greet_str db 'Hello '
GSTR_SIZE equ $ - greet_str
section .bss
; Reserve 32 bytes of memory
buff resb 32
; A macro with two parameters
; Implements the write system call
%macro write 2
mov eax, 4
mov ebx, 1
mov ecx, %1
mov edx, %2
int 80h
%endmacro
; Implements the read system call
%macro read 2
mov eax, 3
mov ebx, 0
mov ecx, %1
mov edx, %2
int 80h
%endmacro
section .text
global _start
_start:
write prompt_str, STR_SIZE
read buff, 32
; Read returns the length in eax
push eax
; Print the hello text
write greet_str, GSTR_SIZE
pop edx
; edx = length returned by read
write buff, edx
_exit:
mov eax, 1
mov ebx, 0
int 80h
|
|
.section .data
prompt_str:
.ascii "Enter Your Name: "
pstr_end:
.set STR_SIZE, pstr_end - prompt_str
greet_str:
.ascii "Hello "
gstr_end:
.set GSTR_SIZE, gstr_end - greet_str
.section .bss
// Reserve 32 bytes of memory
.lcomm buff, 32
// A macro with two parameters
// implements the write system call
.macro write str, str_size
movl $4, %eax
movl $1, %ebx
movl \str, %ecx
movl \str_size, %edx
int $0x80
.endm
// Implements the read system call
.macro read buff, buff_size
movl $3, %eax
movl $0, %ebx
movl \buff, %ecx
movl \buff_size, %edx
int $0x80
.endm
.section .text
.globl _start
_start:
write $prompt_str, $STR_SIZE
read $buff, $32
// Read returns the length in eax
pushl %eax
// Print the hello text
write $greet_str, $GSTR_SIZE
popl %edx
// edx = length returned by read
write $buff, %edx
_exit:
movl $1, %eax
movl $0, %ebx
int $0x80
|
|
このセクションの見出しはマクロの説明を約束しており、NASM も GAS も確かにマクロをサポートしています。しかしマクロに入る前に、他のいくつかの機能を比較しておいた方がよいでしょう。
リスト 3 は、.bss というセクション・ディレクティブ (14 ライン目) を使って定義される、初期化されていないメモリーの概念を示しています。BSS は「block storage segment」を表します (元々は、ブロックはシンボルで始まりました)。そして、BSS セクションで予約されているメモリーは、プログラムの開始時にゼロに初期化されます。BSS セクションにあるオブジェクトには名前とサイズしかなく、値がありません。BSS セクションで宣言される変数は、データ (data) セグメントとは異なり、実際には実行されるまでは領域を確保しません。
NASM は、BSS セクションで割り当てられた byte、word、そして dword の領域に対してキーワード resb、resw、そして resd を使います。一方 GAS は、キーワード .lcomm を使ってバイトレベルの領域を割り当てます。NASM と GAS 双方のプログラムで、変数名がどのように宣言されているかに注目してください。NASM では変数名はキーワード resb (または resw または resd) の前にあり、確保される領域の量が後に続きます。一方 GAS では変数名はキーワード .lcomm の後にあり、その後にカンマ、そして確保される領域の量が後に続きます。下記はその違いを示しています。
NASM: varname resb size
GAS: .lcomm varname, size
リスト 3 はロケーション・カウンターの概念も導入しています (6 ライン目)。NASM はロケーション・カウンターを操作するための特別な変数 ($ 変数と $$ 変数) を提供しています。GAS にはロケーション・カウンターを操作するための方法はなく、ラベルを使って次のストレージ位置 (データや命令など) を計算する必要があります。
例えばストリングの長さを計算するためには、NASM では次のイディオムを使います。
prompt_str db 'Enter your name: '
STR_SIZE equ $ - prompt_str
; $ is the location counter
$ はロケーション・カウンターの現在の値を持ちます。このロケーション・カウンターからラベルの値を引くと (すべての変数名はラベルです)、ラベルの宣言と現在の場所との間にあるバイトの数が得られます。変数 STR_SIZE の値を、この変数に続く式に設定するために、equ ディレクティブを使います。同様のイディオムは、GAS では次のようになります。
prompt_str:
.ascii "Enter Your Name: "
pstr_end:
.set STR_SIZE, pstr_end - prompt_str
end ラベル (pstr_end) は次のロケーション・アドレスを持ち、開始ラベルのアドレスを引くとサイズが得られます。また、.set を使って変数 STR_SIZE の値をコンマの後にある式の値に初期化していることにも注意してください。これに対応する .equ を使うこともできます。NASM には GAS の set ディレクティブに対応するものはありません。
先ほど触れたように、リスト 3 はマクロを使っています (21 ライン目)。NASM と GAS にはマクロのためのさまざまな方法があります (1 行のマクロやマクロのオーバーロードなど)。しかしここでは、基本的なタイプのマクロのみを取り上げます。アセンブリーでの一般的なマクロの使い方は、明確にするための使い方です。同じコード片を何度も何度も入力する代わりに、そうした繰り返しを回避し、また乱雑さを減らしてコードの見栄えと読みやすさを改善するために、再利用可能なマクロを作成することができます。
NASM のユーザーは、%beginmacro ディレクティブを使ってマクロを宣言し、そして %endmacro ディレクティブを使ってマクロを終了する方法に慣れているかもしれません。%beginmacro ディレクティブの後にはマクロ名が続きます。マクロ名の後にはカウント (そのマクロが持たなければならないマクロ引数の数) が来ます。NASM では、マクロ引数には 1 から始まる連続的な番号が付けられます。つまり、あるマクロの最初の引数は %1、2 番目は %2、3 番目は %3、のようになります。例えば次のとおりです。
%beginmacro macroname 2
mov eax, %1
mov ebx, %2
%endmacro
これによって 2 つの引数を持つマクロが作られ、最初の引数が %1 で 2 番目が %2 です。従って、上記のマクロの呼び出しは次のようになります。
macroname 5, 6
引数を付けずにマクロを作成することもできます。この場合にはマクロは何も番号を指定しません。
では、GAS でのマクロの使い方を見てみましょう。GAS には、マクロを作成するために .macro と .endm というディレクティブがあります。.macro ディレクティブの後にはマクロ名が続きますが、マクロ名は引数を持つ場合も持たない場合もあります。GAS では、マクロ引数は名前として与えられます。例えば次のようになります。
.macro macroname arg1, arg2
movl \arg1, %eax
movl \arg2, %ebx
.endm
マクロ内部で実際に名前を使うときには、マクロの引数それぞれの名前の前にバックスラッシュを付けます。こうしておかないと、リンカーは名前を引数ではなくラベルとして扱い、エラーをレポートしてしまいます。
関数と外部ルーチン、そしてスタック
このセクションのサンプル・プログラムは、整数の配列に対して選択ソートを行います。
リスト 4. 整数の配列に対して選択ソートを行う
| Line | NASM | GAS |
|---|
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
|
section .data
array db
89, 10, 67, 1, 4, 27, 12, 34,
86, 3
ARRAY_SIZE equ $ - array
array_fmt db " %d", 0
usort_str db "unsorted array:", 0
sort_str db "sorted array:", 0
newline db 10, 0
section .text
extern puts
global _start
_start:
push usort_str
call puts
add esp, 4
push ARRAY_SIZE
push array
push array_fmt
call print_array10
add esp, 12
push ARRAY_SIZE
push array
call sort_routine20
; Adjust the stack pointer
add esp, 8
push sort_str
call puts
add esp, 4
push ARRAY_SIZE
push array
push array_fmt
call print_array10
add esp, 12
jmp _exit
extern printf
print_array10:
push ebp
mov ebp, esp
sub esp, 4
mov edx, [ebp + 8]
mov ebx, [ebp + 12]
mov ecx, [ebp + 16]
mov esi, 0
push_loop:
mov [ebp - 4], ecx
mov edx, [ebp + 8]
xor eax, eax
mov al, byte [ebx + esi]
push eax
push edx
call printf
add esp, 8
mov ecx, [ebp - 4]
inc esi
loop push_loop
push newline
call printf
add esp, 4
mov esp, ebp
pop ebp
ret
sort_routine20:
push ebp
mov ebp, esp
; Allocate a word of space in stack
sub esp, 4
; Get the address of the array
mov ebx, [ebp + 8]
; Store array size
mov ecx, [ebp + 12]
dec ecx
; Prepare for outer loop here
xor esi, esi
outer_loop:
; This stores the min index
mov [ebp - 4], esi
mov edi, esi
inc edi
inner_loop:
cmp edi, ARRAY_SIZE
jge swap_vars
xor al, al
mov edx, [ebp - 4]
mov al, byte [ebx + edx]
cmp byte [ebx + edi], al
jge check_next
mov [ebp - 4], edi
check_next:
inc edi
jmp inner_loop
swap_vars:
mov edi, [ebp - 4]
mov dl, byte [ebx + edi]
mov al, byte [ebx + esi]
mov byte [ebx + esi], dl
mov byte [ebx + edi], al
inc esi
loop outer_loop
mov esp, ebp
pop ebp
ret
_exit:
mov eax, 1
mov ebx, 0
int 80h
|
|
.section .data
array:
.byte 89, 10, 67, 1, 4, 27, 12,
34, 86, 3
array_end:
.equ ARRAY_SIZE, array_end - array
array_fmt:
.asciz " %d"
usort_str:
.asciz "unsorted array:"
sort_str:
.asciz "sorted array:"
newline:
.asciz "\n"
.section .text
.globl _start
_start:
pushl $usort_str
call puts
addl $4, %esp
pushl $ARRAY_SIZE
pushl $array
pushl $array_fmt
call print_array10
addl $12, %esp
pushl $ARRAY_SIZE
pushl $array
call sort_routine20
# Adjust the stack pointer
addl $8, %esp
pushl $sort_str
call puts
addl $4, %esp
pushl $ARRAY_SIZE
pushl $array
pushl $array_fmt
call print_array10
addl $12, %esp
jmp _exit
print_array10:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl 8(%ebp), %edx
movl 12(%ebp), %ebx
movl 16(%ebp), %ecx
movl $0, %esi
push_loop:
movl %ecx, -4(%ebp)
movl 8(%ebp), %edx
xorl %eax, %eax
movb (%ebx, %esi, 1), %al
pushl %eax
pushl %edx
call printf
addl $8, %esp
movl -4(%ebp), %ecx
incl %esi
loop push_loop
pushl $newline
call printf
addl $4, %esp
movl %ebp, %esp
popl %ebp
ret
sort_routine20:
pushl %ebp
movl %esp, %ebp
# Allocate a word of space in stack
subl $4, %esp
# Get the address of the array
movl 8(%ebp), %ebx
# Store array size
movl 12(%ebp), %ecx
decl %ecx
# Prepare for outer loop here
xorl %esi, %esi
outer_loop:
# This stores the min index
movl %esi, -4(%ebp)
movl %esi, %edi
incl %edi
inner_loop:
cmpl $ARRAY_SIZE, %edi
jge swap_vars
xorb %al, %al
movl -4(%ebp), %edx
movb (%ebx, %edx, 1), %al
cmpb %al, (%ebx, %edi, 1)
jge check_next
movl %edi, -4(%ebp)
check_next:
incl %edi
jmp inner_loop
swap_vars:
movl -4(%ebp), %edi
movb (%ebx, %edi, 1), %dl
movb (%ebx, %esi, 1), %al
movb %dl, (%ebx, %esi, 1)
movb %al, (%ebx, %edi, 1)
incl %esi
loop outer_loop
movl %ebp, %esp
popl %ebp
ret
_exit:
movl $1, %eax
movl 0, %ebx
int $0x80
|
|
リスト 4 を最初に見たときは圧倒されるかもしれませんが、実際に行っていることは非常に単純です。このリストには、関数や、さまざまなメモリー・アドレッシング方法、スタック、そしてライブラリー関数の使い方の概念が導入されています。このプログラムは 10 個の数字の配列をソートし、そして外部の C ライブラリー関数 puts と printf を使って、ソートされていない配列とソートされた配列の全内容を出力しています。モジュール構造にするために、そして関数の概念を導入するために、ソート・ルーチン自体は配列の出力ルーチンと共に、別の手順として実装されています。これらを 1 つずつ見ていきましょう。
データ宣言の後、puts (31 ライン目) をコールすることでプログラムの実行が始まります。puts 関数はコンソールにストリングを表示します。この関数の唯一の引数は、表示されるストリングのアドレスです。この引数は、このストリングのアドレスをスタックにプッシュすることで、この関数に渡されます (30 ライン目)。
NASM では、プログラムの一部ではなくリンク時に解決しなければならないラベルを、すべて事前に定義する必要があります。これがキーワード extern の機能です (24 ライン目)。GAS にはそうした要件がありません。この後で、ストリングのアドレス usort_str はスタックにプッシュされます (30 ライン目)。NASM では、usort_str などのメモリー変数はメモリー・ロケーション自体のアドレスを表します。従って、push usort_str のようなコールによって、そのアドレスが実際にスタックの先頭にプッシュされます。一方 GAS では、変数 usort_str の前に $ を付け、それを即値アドレスとして扱う必要があります。変数の前に $ を付けないと、メモリー変数のアドレスではなく、メモリー変数で表現される実際のバイトがスタックにプッシュされます。
変数をプッシュすると、基本的にスタック・ポインターは dword 単位で移動するため、スタック・ポインターに 4 (dword のサイズ) を加えて調整しています (32 ライン目)。
今現在 3 つの引数がスタックにプッシュされており、そして print_array10 関数がコールされています (37 ライン目)。関数の宣言方法は NASM でも GAS でも同じです。関数はラベルにすぎず、call 命令を使って呼び出されます。
関数コールの後、ESP はスタックの先頭を表します。値 esp + 4 は戻りアドレスを表し、値 esp + 8 はこの関数の最初の引数を表します。その後の引数にアクセスするためには、dword 変数のサイズをスタック・ポインターに追加します (つまり esp + 12、esp + 16 など)。
関数の中に入ると、esp を ebb にコピーすることでローカルのスタック・フレームが作成されます (62 ライン目)。ローカル変数用の領域を割り当てることもでき、これはプログラムの中で行う方法と同じです (63 ライン目)。そのためには、必要なバイト数を esp から引きます。値 esp – 4 はローカル変数に割り当てられた 4 バイトの領域を表します。これを、ローカル変数を格納するために十分な領域がスタックにある限り続けることができます。
リスト 4 はベース間接アドレッシング・モードを示しています (64 ライン目)。こう呼ばれる理由は、ベース・アドレスで開始し、それにオフセットを追加することで最終的なアドレスに到達するためです。NASM 側のリストでは、[ebp + 8] がその例であり、また [ebp – 4] (71 ライン目) も同じです。GAS では、このアドレッシングはもっと簡潔で、それぞれ 4(%ebp) と -4(%ebp) です。
print_array10 ルーチンを見ると、push_loop ラベルの後に別の種類のアドレッシング・モードが使われていることがわかります (74 ライン目)。この行は、NASM と GAS でそれぞれ次のように表現されています。
NASM: mov al, byte [ebx + esi]
GAS: movb (%ebx, %esi, 1), %al
このアドレッシング・モードはベース・インデックス付きアドレッシング・モードです。ここには 3 つの値が存在しています。1 つ目はベース・アドレスで、2 つ目はインデックス・レジスター、そして 3 つ目は乗数です。あるメモリー・ロケーションを先頭に何バイト、アクセスされるバイト数を決定することは不可能なため、アドレス割り当てされるメモリーの量を知るための方法が必要です。NASM ではバイト演算子を使うことで、1 バイトのデータを移動するようにアセンブラーに伝えます。GAS は同じ問題を、乗数を使うことで、またニーモニックに接尾辞 b または w、l を付ける (例えば movb など) ことで解決します。GAS の構文は、初めて見ると少し複雑に見えるかもしれません。
GAS でのベース・インデックス付きアドレッシングの一般的な形式は次のとおりです。
%segment:ADDRESS (, index, multiplier)
または
%segment:(offset, index, multiplier)
または
%segment:ADDRESS(base, index, multiplier)
最終的なアドレスは次の公式を使って計算します。
ADDRESS or offset + base + index * multiplier.
従って、byte にアクセスするためには乗数 1 を使い、word にアクセスするには 2、dword には 4 を使います。もちろん、NASM はもっと簡単な構文を使います。そのため、上記の構文は NASM では次のようになります。
Segment:[ADDRESS or offset + index * multiplier]
このメモリー・アドレスの前に使われている byte、word、または dword の接頭辞は、それぞれ 1 バイト、2 バイト、4 バイトのメモリーをアクセスするためのものです。
その他
リスト 5 は、一連のコマンドラインの引数を読み取り、メモリーに保存し、それらを出力します。
リスト 5. コマンドラインの引数を読み取り、メモリーに保存し、出力するプログラム
| Line | NASM | GAS |
|---|
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
|
|
section .data
; Command table to store at most
; 10 command line arguments
cmd_tbl:
%rep 10
dd 0
%endrep
section .text
global _start
_start:
; Set up the stack frame
mov ebp, esp
; Top of stack contains the
; number of command line arguments.
; The default value is 1
mov ecx, [ebp]
; Exit if arguments are more than 10
cmp ecx, 10
jg _exit
mov esi, 1
mov edi, 0
; Store the command line arguments
; in the command table
store_loop:
mov eax, [ebp + esi * 4]
mov [cmd_tbl + edi * 4], eax
inc esi
inc edi
loop store_loop
mov ecx, edi
mov esi, 0
extern puts
print_loop:
; Make some local space
sub esp, 4
; puts function corrupts ecx
mov [ebp - 4], ecx
mov eax, [cmd_tbl + esi * 4]
push eax
call puts
add esp, 4
mov ecx, [ebp - 4]
inc esi
loop print_loop
jmp _exit
_exit:
mov eax, 1
mov ebx, 0
int 80h
|
|
.section .data
// Command table to store at most
// 10 command line arguments
cmd_tbl:
.rept 10
.long 0
.endr
.section .text
.globl _start
_start:
// Set up the stack frame
movl %esp, %ebp
// Top of stack contains the
// number of command line arguments.
// The default value is 1
movl (%ebp), %ecx
// Exit if arguments are more than 10
cmpl $10, %ecx
jg _exit
movl $1, %esi
movl $0, %edi
// Store the command line arguments
// in the command table
store_loop:
movl (%ebp, %esi, 4), %eax
movl %eax, cmd_tbl( , %edi, 4)
incl %esi
incl %edi
loop store_loop
movl %edi, %ecx
movl $0, %esi
print_loop:
// Make some local space
subl $4, %esp
// puts functions corrupts ecx
movl %ecx, -4(%ebp)
movl cmd_tbl( , %esi, 4), %eax
pushl %eax
call puts
addl $4, %esp
movl -4(%ebp), %ecx
incl %esi
loop print_loop
jmp _exit
_exit:
movl $1, %eax
movl $0, %ebx
int $0x80
|
|
リスト 5 はアセンブリーの中で命令を繰り返す構成体を示しています。これは反復構成体と呼ばれます。GAS では、.rept ディレクティブを使って反復構成体を開始します (6 ライン目)。このディレクティブは .endr を使って終了する必要があります (8 ライン目)。.rept の後には、.rept/.endr 構成体の中に囲まれた式を何回繰り返すかを指定するGAS のカウントが続きます。この構成体の中に置かれた命令は、その命令をそれぞれ別々の行で count 回数繰り返すように書かれた命令と同じです。
例えば、カウントが 3 の場合は次のようになります。
.rept 3
movl $2, %eax
.endr
これは下記と等価です。
movl $2, %eax
movl $2, %eax
movl $2, %eax
NASM では、似たような構成体がプリプロセッサー・レベルで使われます。この構成体は %rep ディレクティブで始まり、%endrep で終わります。%rep ディレクティブの後には式が続きます ( .rept ディレクティブの後にカウントが続く GAS とは異なります)。
%rep <expression>
nop
%endrep
NASM には、この代わりとして times ディレクティブもあります。times ディレクティブは %rep と同じようにアセンブラー・レベルで動作し、やはり後に式が続きます。例えば上記の %rep 構成体は下記と等価です。
times <expression> nop
そして下記は、
%rep 3
mov eax, 2
%endrep
下記と等価です。
times 3 mov eax, 2
そしてどちらも下記と等価です。
mov eax, 2
mov eax, 2
mov eax, 2
リスト 5 は、.rept (または %rep) ディレクティブを使ってダブルワード 10 個分のメモリー・データ領域を作成しています。そしてスタックにあるコマンドライン引数が 1 つずつアクセスされてメモリー領域に保存され、コマンド・テーブルが一杯になるまでこれが続きます。
コマンドライン引数は、どちらのアセンブラーでも同じようにアクセスされます。ESP またはスタックの先頭はプログラムに提供されたコマンドライン引数の数を保存しますが、これはデフォルトで 1 です (コマンドライン引数なしの場合)。esp + 4 は最初のコマンドライン引数を保存しますが、これは必ず、コマンドラインから呼び出されたプログラムの名前です。esp + 8 や esp + 12 などは、その後に続くコマンドライン引数を保存します。
また、メモリー・コマンド・テーブルがリスト 5 の両側でどのようにアクセスされているかにも注意してください。ここではメモリー間接アドレッシング・モード (33 ライン目) を使って、コマンド・テーブルと、ESI (そして EDI) のオフセット、そして乗数にアクセスしています。つまり NASM の [cmd_tbl + esi * 4] は GAS の cmd_tbl(, %esi, 4) と等価です。
まとめ
この 2 つのアセンブラーの間には大きな違いがありますが、一方のアセンブラーからもう一方に変換する作業はそれほど困難ではありません。最初は AT&T 構文が理解しにくいと思えるかもしれませんが、一度マスターしてしまえば Intel 構文と同じくらい簡単です。
参考文献 学ぶために
製品や技術を入手するために
- 2枚組 DVD セット、SEK for Linux をご注文ください。DB2® や Lotus®、Rational®、Tivoli®、WebSphere® など、Linux 用の最新 IBM ソフトウェアの試用版が含まれています。
- developerWorks から直接ダウンロードできる IBM trial software を利用して、皆さんの次期 Linux 開発プロジェクトを構築してください。
議論するために
著者について  | |  | Ram はコンピューター・サイエンスで post graduate の学位を取得しています。現在は IBM の India Software Labs の Rational Division に勤務するソフトウェア・エンジニアとして、Rational Clearcase の機能の開発と追加を行っています。彼はさまざまな種類の Linux/UNIX と Windows を、また Symbian や Windows Mobile などのモバイル・ベースのリアルタイム・オペレーティング・システムを扱ってきています。時間のある時には Linux をいじり、また読書を楽しんでいます。 |
記事の評価
|  |