Linux のアセンブラー: GAS と NASM を比較する

GAS (GNU Assembler) と NASM (Netwide Assembler) を並べて比較する

この記事では、Linux® で最も一般的な 2 つのアセンブラーである GAS (GNU Assembler) と NASM (Netwide Assembler) の間での、構文や意味体系の重要な違いについて説明します。比較する項目は、基本的な構文や、変数やメモリー・アクセスの方法、マクロの処理、関数や外部ルーチン、スタックの処理、コード・ブロックを容易に繰り返すための手法などです。

Ram Narayan, Software Engineer, IBM

Ram はコンピューター・サイエンスで post graduate の学位を取得しています。現在は IBM の India Software Labs の Rational Division に勤務するソフトウェア・エンジニアとして、Rational Clearcase の機能の開発と追加を行っています。彼はさまざまな種類の Linux/UNIX と Windows を、また Symbian や Windows Mobile などのモバイル・ベースのリアルタイム・オペレーティング・システムを扱ってきています。時間のある時には Linux をいじり、また読書を楽しんでいます。



2007年 10月 17日

はじめに

アセンブリー言語によるプログラミングでは、他の言語の場合と異なり、プログラムの対象となっているマシンのプロセッサーのアーキテクチャーを理解する必要があります。アセンブリー言語で記述されたプログラムはまったく移植性がなく、維持管理や理解が大変なことが多く、しかも膨大な行数のコードを含むことが多いものです。しかし、こうした制約があるかわりに、そのマシンで実行されるランタイム・バイナリーは高速でサイズが小さいという利点があります。

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 で終了するプログラム
LineNASMGAS
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 構文では、メモリー・オペランドのサイズはオペコード名の最後の文字で決まります。オペコードの接尾辞の bwl は、byte (8 ビット)、word (16 ビット)、そして long (32 ビット) のメモリー参照を指定します。Intel 構文はこれを、(オペコード自体ではなく) メモリー・オペランドの前に byte ptrword ptrdword 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 つの数字から最大のものを見つけるプログラム
LineNASMGAS
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 は dddw、そして db ディレクティブを使ってそれぞれ 32 ビット、16 ビット、8 ビットの数字を宣言していますが、GAS はそうした宣言に .long.int、そして .byte を使います。GAS には、.ascii.asciz.string など、他にもディレクティブがあります。GAS では、他のラベルとまったく同じように (コロンを使って) 変数を宣言しますが、NASM ではメモリー割り当てディレクティブ (dddw など) の前に (コロンを付けずに) 単純に変数名を入力し、その後に変数の値を入力します。

リスト 2 の 18 ライン目は、メモリー間接アドレッシング・モードを示しています。NASM はメモリー・ロケーションで指示されるアドレスにある変数を、[var1] のように大括弧を使って間接参照します。GAS は同じ値を間接参照する場合には、(var1) のように小括弧を使います。他のアドレッシング・モードの使い方は、この記事の後の方で説明します。


マクロを使う

リスト 3 はこのセクションの概念を示しています。このプログラムは入力としてユーザーの名前を受け付け、あいさつの言葉を返します。

リスト 3. ストリングを読み取り、ユーザーにあいさつを表示するプログラム
LineNASMGAS
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 の領域に対してキーワード resbresw、そして 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. 整数の配列に対して選択ソートを行う
LineNASMGAS
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 ライブラリー関数 putsprintf を使って、ソートされていない配列とソートされた配列の全内容を出力しています。モジュール構造にするために、そして関数の概念を導入するために、ソート・ルーチン自体は配列の出力ルーチンと共に、別の手順として実装されています。これらを 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 + 12esp + 16 など)。

関数の中に入ると、espebb にコピーすることでローカルのスタック・フレームが作成されます (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 または wl を付ける (例えば 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]

このメモリー・アドレスの前に使われている byteword、または dword の接頭辞は、それぞれ 1 バイト、2 バイト、4 バイトのメモリーをアクセスするためのものです。


その他

リスト 5 は、一連のコマンドラインの引数を読み取り、メモリーに保存し、それらを出力します。

リスト 5. コマンドラインの引数を読み取り、メモリーに保存し、出力するプログラム
LineNASMGAS
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 + 8esp + 12 などは、その後に続くコマンドライン引数を保存します。

また、メモリー・コマンド・テーブルがリスト 5 の両側でどのようにアクセスされているかにも注意してください。ここではメモリー間接アドレッシング・モード (33 ライン目) を使って、コマンド・テーブルと、ESI (そして EDI) のオフセット、そして乗数にアクセスしています。つまり NASM の [cmd_tbl + esi * 4] は GAS の cmd_tbl(, %esi, 4) と等価です。


まとめ

この 2 つのアセンブラーの間には大きな違いがありますが、一方のアセンブラーからもう一方に変換する作業はそれほど困難ではありません。最初は AT&T 構文が理解しにくいと思えるかもしれませんが、一度マスターしてしまえば Intel 構文と同じくらい簡単です。

参考文献

学ぶために

製品や技術を入手するために

  • developerWorks から直接ダウンロードできる IBM trial software を利用して、皆さんの次期 Linux 開発プロジェクトを構築してください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux, Open source
ArticleID=268508
ArticleTitle=Linux のアセンブラー: GAS と NASM を比較する
publish-date=10172007