OKLabのタイトルロゴ

書き直したGNUアセンブラ入門へ

2006年10月6日

GNU アセンブラ入門(GAS)

目次

目次へ戻る

はじめに

目次へ戻る

対象者

目次へ戻る

GASとは

GAS(ガス、ギャス、The GNU Assembler)とは、Dean Elsner, Jay Fenlasonが作成したアセンブリ言語である。Linuxカーネルの開発で利用されている。

 

目次へ戻る

動作環境

 

目次へ戻る

最終目的

アセンブラを学習することで、コンピュータ・アーキテクチャとプログラミングの概念を理解する。また最終的にバイナリクロックのGAS版を作成する。

 

目次へ戻る

コメント

# 1行のコメントはシャープ1つです。

/*
複数行のコメントはC言語などのように
スラッシュ、アスタリクスです。
*/

 

目次へ戻る

構造

サンプル

.file   "ファイル名.s"
.data					#ここに変数の定義を書く
#データを書く
.text   				#ここから実行文が始まる
.globl main			#はじめに呼び出される関数を.globlで指定(globalではなくglobl)
main:
        # 命令を書く 

データを.dataディレクティブ内で定義し、命令は.textディレクティブ内で定義する。

目次へ戻る

コーディング規約

コーディング規約はない。

 

目次へ戻る

ハローワールド

.file   "helloworld.s"
.data
msg:    .ascii  "hello world\n"
msgend: .equ    len, msgend - msg

.global main
main:
        movl $4,        eax    # write system call(sys_write)
        movl $1,        ebx    # stdout
        movl $msg,      ecx
        movl $len,      edx
        int  $0x80
        ret

 

実行

$ gcc helloworld.s -o hw
$ ./hw

解説

eaxレジスタにsys_writeシステムコールを設定、引数が3つあるのでebxレジスタに出力先の標準出力を意味する$1を設定。第二引数にメッセージのデータ$msgを設定、第三引数にメッセージの長さの$lenを設定、最後にシステムコールのint $0x80を実行してシステムコールを呼び出している。またその次の行のretオペコードはreturnをあらわしこれによりプログラムが終了される。詳細は後ほど説明するので今は理解しなくてよい。

 

目次へ戻る

アセンブラの基礎概念

文法に入る前にCPUががどのようなものであるか分からなければ、アセンブラ開発が出来ないので、x86CPU年表、ニーモニック、オペコード、レジスタの説明をする。

x86CPU年表

年代 CPU bit 概要
1971 4004 8  
1973 8080 16  
1978 8086 16  
1984 80286 16  
1985 80386 32  
1989 80486 32  
1993 Pentium(i586) 32 Pentaは数字の5を意味する。この時から486などの数番からPentiumという言葉をつかう
1994 PentiumPro(i686) 32 80686という位置付けだが、Pentiumから名前で呼ぶようになったためPentium Proといわれた。
1997 MMX Pentium 32 MMX命令の追加
1997 PentiumⅡ 32 オペコードの拡張とCPUの集積化(CPUの大きさを小さくすればそれだけ電力が少なくなるため、
1999 PentiumⅢ 32 オペコードの拡張とCPUの集積化
2000 Pentium 4 32 3Dグラフィックなどマルチメディア系命令がどんどん追加される。
2001 Itenium 64 IntelとPentiumを文字って作られた用語。Intel初の64bitプロセッサ。サーバ用
2002 Itenium2 64 サーバ用、オペコードの拡張とCPUの集積化
2004 Pentium-M 32 Pentium4の熱問題が原因で、新しく設計された省電力版Pentium

Pentium4を例にとると、実際には、Pentium4も複数の種類が存在する。最初は開発コードWillamette-434(ウィラメット423)と呼ばれ、0.18μmで開発されたが、現在主流のPrescott(プレスコット)は90nmである。驚くかもしれないが、内部構造はインフルエンザウィルスよりも小さな物質での電流のやり取りが行われている。またPrescottは100W以上の電力を消費するため今後は製造が中止されることが決まっている。2004年第四半期ぐらいから、一般家庭用コンピュータにも64bitのIntelプロセッサが普及していくことが予想される。

興味のある方は以下のIntelサイトを確認してみてください。

テクノロジの世界 マイクロプロセッサテクノロジ

ここで注意してみてもらいたいのは、1985年に製造された80386から現在のPentium4にいたるまでは、基本的な部分(32bit)は変わらないことである。IntelはCPUの集積化と命令の追加などをおこなってきた。つまり、80386を基本として学習をすれば現在のCPUのアーキテクチャも学習できるのである。

 

目次へ戻る

ニーモニック(mnemonic)

命令のことをニーモニックという。ニーモニックは、OPコード(オペコード)とオペランド(operand)のことをいう。

movb $0x01 %al

上記の例では、移動する命令のmovbがOPコード 、16進数の数値の$0x01とレジスタを意味する%alをオペランドという。

 

目次へ戻る

オペコード

4004などの8bitCPUは、8bit(1byte)での処理を行っていたので、オペコードで8bitを処理をするにはbyteを意味するbをつける。

movb $0xff %ah
movb $0xff %al

8086などの16bitCPUは、eXtend(拡張)を意味するレジスタを利用し、オペコードで16bitを処理するには16bitを意味するwordのwをつける。

movw $0x01ff %ax

386やPentiumなどの32bitCPUは、Extend(拡張)を意味するレジスタを利用し、オペコードで32bitを処理するには32bitを意味するlongのlをつける。

movl $0x123401ff %eax

これでハローワールドのmovオペコードの意味が理解できたと思う.また数値は$記号ではじまり、レジスタは%記号で始まる。

では、今までの説明を図にしてみる。

eax(eXtend Accumurator eXtend)
  ax(Accumurator eXtend)  
  ah(Accumurator Hi) al (Accumurator Low)
0000 0000 0000 0000 0000 0000 0000 0000

これで分かるように、x86CPUは4004などの命令も利用できる。つまり下位互換を持っている。最近のApple ComputerがG5を販売したが、これは64bitCPUをもっている。つまり32bitから64bitにあがることで、一度により大きな数値の計算を出来る。

目次へ戻る

レジスタ

この入門で利用するレジスタと役割を説明する。

汎用レジスタ

レジスタ名 呼び名 役割
eax アキュムレータ 計算につかう
ebx ベースレジスタ アドレスのベースにつかう
ecx カウンタレジスタ 反復処理のカウント
edx   計算につかう
esi ソース メモリ制御につかう
edi デスティネーション メモリ制御につかう
esp スタックポインタ メモリ制御につかう。一般にプログラマは、スタック領域の読み込みに利用
ebp ベースポインタ メモリ制御につかう。一般にプログラマは、スタック領域の読み書きに利用
eip インストラクションポインタ 次に行う命令が入る
eflags フラグ 比較、分岐などの際にこのフラグを確認する。
cs コードセグメント コードがどこから始まるかメモリの場所を指定する
ds データセグメント データがどこから始まるかメモリの場所を指定する
es エクストラセグメント その他がどこから始まるかメモリの場所を指定する
ss スタックセグメント スタックがどこから始まるかメモリの場所を指定する
fs    
gs    

helloworldのソースコードでどのレジスタを使ったか再確認しよう。

目次へ戻る

データ形式

数値

10進数 $123, $-123

16進数 $0x12 $01212 $0x13ff55ee $-0xe1

2進数 $0b11110000

可変型(文字列)

msg: .ascii "hello world\n"

JavaでのString msg = "hello world\n"; C言語での char msg[13] = "hello world\n";と同様

文字

$'a' $'z'

データ定義の比較

  アセンブラ C言語 Java
1byte cvar: .byte 0xff char cvar = 0xff byte cvar = 0xff
2byte wvar: .word 0xffff short wvar = 0xffff short wvar = 0xffff
4byte lvar: .loing 0xaaaaffff long lvar = 0xaaaaffff int lvar = 0xaaaaffff
可変 str: .ascii "hello" char str[6] = "hello"; String str = "hello";
可変 wary: .comm 20,2 short wary[10]; short wary[10];
可変 lary: .comm 20,4 long lay[5] int lary[5]

.byte .word .long .asci. commなども.data .textと同様ディレクティブ

サンプル

.file "data_define.s"
.data
cvar:   .byte   0xff
wvar:   .word   0xffff
lvar:   .long   0xaaaaffff
str:    .ascii  "hello"
wary:   .comm   ,20,2           # COMMon memory area (short wary[10])
.comm   wary2,20,2              # same define
lary:   .comm   ,20,4           # COMMon memory area (long lary[5])
.comm   lary2,20,4              # same define
.text
.global main
main:
 

 

目次へ戻る

演算子

演算子(+-*/%)などはない。すべてニーモニックで書く。

算術演算(authmetic operation)

.file "authmetic.s"
.data
.text
.global main
main:
        movb    $0x00,  %al
        addb    $0x0f,  %al     # 0x00 + 0x0f   = 0x0f
        subb    $0x01,  %al     # 0x0f - 0x01   = 0x0e

        movb    $0x01,  0l
        mulb    0l             # %al * 0l = %ax

        movw    $0x00ff,%ax
        movb    $0x08,  0l
        div     0l             # %ax / 0l     =       %ahが商 %alが余り
        ret
 

 

論理演算(logical operation)

論理積(AND)

.file "and.s"
.data
.text
.global main
main:
        movb    $0b00001111,    %al
        andb    $0b11110011,    %al # = 0b00000011 = 0x03
        ret

論理和(OR)

.file "or.s"
.data
.text
.global main
main:
        movb    $0b00001111,    %al
        orb     $0b11110000,    %al # $0b11111111 = 0xff
        ret
 

否定(NOT)

.file "not.s"
.data
.text
.global main
main:
        movb    $0b11111111, %al
        not     %al
        ret
 

排他的論理和(XOR=eXclusive OR)

.file "xor.s"
.data
.text
.global main
main:
        movb    $0b00111100,    %al
        xor     $0b11110000,    %al # 11001100
        ret
 

否定論理積(NAND=Not AND)

.file "nand.s"
.data
.text
.global main
main:
        movb    $0b00001111,    %al
        andb    $0b00001111,    %al
        not     %al
        ret
 

否定論理和(NOR=Not OR)

file "nor.s"
.data
.text
.global main
main:
        movb    $0b00001111,    %al
        orb     $0b11110000,    %al
        not     %al
        ret

 

目次へ戻る

制御構造

分岐処理

.file "condition.s"
.data
stri:   .ascii  "condition\0"
length: .equ    len,    length - stri
.global main
main:
        movb    $5,     %al
        cmpb    $5,     %al # alレジスタと数値5を比較する。比較結果はeflagsに格納される。
        je      f_print		# もしequalだったらjump

ret

f_print:
movl $4, eax
movl $1, ebx
movl $stri, ecx
movl $len, edx
int $0x80
ret


反復処理

.file "loopprint.s"
.data
msg:    .ascii  "hello\0"
msize:  .equ    len,    msize - msg
.global main
main:

        loop:
                movb    $0,      l
                incb     l

                cmpb    $10,     l     # compare 比較結果はeflagsに格納される。
        jb      loop                    # Jump if Below もし数が小さかったら
		ret

目次へ戻る

(E)FLAGSレジスタ

EFLAGSレジスタは32bitであるが、入門なので下位16bitのFLAGSレジスタのみ以下に表記する。

EFLAGS
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0(LSB)
システムフラグ ステータスフラグ コントロールフラグ システムフラグ ステータスフラグ
0 NT IOPL OF DF IF TF SF ZF 0 AF 0 PF 1 CF

14 NT=Nested Task
13-12 IOPL=(I/O Privilege Level)
11 OF=Overflow Flag
10 DF=Direction Flag
9 IF=Interrupt Enable Flag
8 TF=Trap Flag
7 SF=Sign Flag
6 ZF=Zero Flag
5 予約
4 AF=Acxiliary carry Flag
3 予約
2 PF=Parity Flag
1 予約
0 CF=Carry Flag

とりあえず、ステータスフラグのみ最初に覚えればよい。以下の時にフラグが立つ。(0から1になる。)

OF 演算結果がオーバフローした時
SF 符号がマイナス時
ZF 演算結果が0
AF 加減演算時に、ビット3番からの桁上がり(Carry)または桁借り(Borrow)時、BCDの時利用される。
PF 演算結果がのビット数が偶数個の時
CF 最上位ビットの桁上がりまたは桁借り時

 

目次へ戻る

アドレッシングモード

.file "addressing.s"
.data
.text
.file "addressing.s"
.data
.text
.globl main
main:
        # $0b000011111 $マークがイミディエイトオペランド
        # %でレジスタを指定しているのが、レジスタオペランド
        movb    $0b00001111,    %al

        # メモリオペランド
        movl    -4(ebx),       eax

メモリオペランドは、displacement(base, index, scale)の形で表記する。上記は省略形。メモリ上のデータアクセスにつかう
displacement
は、byte(8),word(16),double word(32)の定数
baseは、0.000000E+00BXなどをつかう。
indexは、配列のインデックスと同等な意味
scaleは、スケーリングファクタといい、indexに倍率をかける。x1,x2,x4,x8が可能。


目次へ戻る

データ処理

目次へ戻る

エラー処理

目次へ戻る

関数

callオペコードとretオペコードで実装する。

.file "call_ret"
.data
.text
.globl main
main:
        movb    $1,     %al
        call    test_func       # jump test_func label
        call    test_func

        ret

test_func:
        incb    %al             # al++
        ret

関数に引数を設定する場合のサンプル

call by value 値渡し

.file "byvalue.s"
.data
.text
.globl main
main:
        movl    $0x1111,        eax
        push    eax					# eaxレジスタの実際の値をスタック領域に配置$0x1111->(esp)
		call    func
        pop     eax
        ret
func:
        movl    esp,   ebp
        movl    4(ebp),        edx
        incl    edx
        ret
 

call by reference 参渡し

.file "byref.s"
.data
var:    .long   0x12121212
.text
.globl main
main:
        push    $var    # address into stack
        call    func
        pop     eax    # dummy
        ret
func:
        movl    esp,   ebp
        movl    4(esp),        edx    # copy address to edx register
        incl    (edx)                  # refer value inclement var=0x12121213
		ret
 
目次へ戻る

API(Application Program Interface)

目次へ戻る

デバック

目次へ戻る

システムコール

13番はtimeシステムコールを呼び出す。eaxに1-jan-1970からの秒数が入る。

.file "2c.s"
.data
.text
.global main
main:
        movl $13,       eax
        movl $0,        ebx
        int     $0x80

        ret
 

80番でgettimeofdayが利用できるがアセンブラで構造体のポインタをどのように書くのか
分からないので調査中。

 

 

目次へ戻る

参考資料

オーム社 プログラミングの力を生み出す本-インテルCPUのGNUユーザへ- ISBN4-274-13207-2

linuxassembly.org

Assembly HOWTO

目次へ戻る