どうも高野商店です。前回はなんか薄い内容だったので、それじゃあつまらんと言うことで突然ですがgasやります。
欠点としては
segment_register:displacement(base_register,index_register,scale)
MOV AL,16H -> movb $0x16, %al //0xは16進法を表しています。
MOV [0541H],AX -> movw %ax, *0x0541
ADD AL,2AH -> addb $0x2a, %al
MOV AX,[BX+DI+4] -> movw %cs:0x04(%bx,%di,2), %ax
となります。
このようにほとんどintel社の命令群と大差がないのですが、infoを読むとごく一部の命令に違いがあることがわかります。その差は後々になってからお話しします。
.text
LC0:
.ascii "Hello World!!\12\0"
.align 4
.globl _main
_main:
pushl %ebp
movl %esp,%ebp
call ___main
pushl $LC0
call _printf
addl $4,%esp
xorl %eax,%eax
jmp L1
.align 4,0x90
L1:
leave
ret
これはC言語でお馴染みのHello.cです。(注 ここで使っているのは8086のアセンブラなので情報棟のgccでやっても動きません。305のXaでやってみて下さい。)
ちなみに.alignとかはコンストラクタと呼ばれ、i386/387の命令を発生する以外の直接gasに対して命令を送るものです。また、レジスタの頭にeのついているもの(%eaxなど)は32ビットレジスタを意味します。
.text
命令部に入れることを定義します。
.globl
ラベルを他のモジュールから参照可能にする。
.align
.align nで次の文を 2n の倍数の番地に置く。
.ascii
label: .ascii "文字列" の形式で文字列を定義する。
さてここで初めて出てきたcallとjmpについて説明します。
まずは無条件ジャンプです。gasでは
jmp label
の様に書きます。labelは上の例ではL1:のように「ラベル名:」で指定します。
これを実行すると、無条件にL1:以下の命令を実行します。
cmp 0x02,%alを実行したあと
命令 | 結果 | |
---|---|---|
jz label | (Jump if Zero) | al=0x02ならばジャンプ |
jnz label | (Jump if Not Zero) | al!=0x02ならばジャンプ |
jb label | (Jump if Below) | al<0x02ならばジャンプ |
ja label | (Jump if Above) | al>0x02ならばジャンプ |
jbe label | (Jump if Below or Equal) | al<=0x02ならばジャンプ |
jae label | (Jump if Above or Equal) | al>=0x02ならばジャンプ |
となります。上に挙げた例は2つのを値をそれぞれ符号なしの1バイトデータとしてみなした場合です。符号ありの場合は最上位ビットある無しで負の数かそうでないかを決定します。だからたとえば0xffは符号無しの場合は255を表し、符号ありの場合は-1を表します。下の命令は符号ありの場合の条件ジャンプです。
jl label | (Jump if Less) | al<0x02ならばジャンプ |
---|---|---|
jg label | (Jump if Greater) | al>0x02ならばジャンプ |
jle label | (Jump if Less or Equal) | al<=0x02ならばジャンプ |
jge label | (Jump if Greater or Equal) | al>=0x02ならばジャンプ |
符号あり無しの話をもう少し詳しくしてみます。
符号無しの場合、16進法と10進法との対応関係を考えてみると
16進法:00,01,02, ...,0d,0e,0f,10, ..., fe, ff 10進法: 0, 1, 2, ...,13,14,15,16, ...,254,255
これが符号ありの場合になると...
16進法: 80, 81, 82, ...,fe,ff,00,01,02, ...,0d,0e,0f,10, ..., 7e, 7f 10進法:-128,-127,-126, ...,-2,-1, 0, 1, 2, ...,13,14,15,16, ...,124,125
まったく違うことがわかります。だからもし
mov 0xff,%al
cmp 0x02,%al
としたあと、jbとjlは全く違った意味を持つことになるわけです。
#include <stdio.h>
int func(int n)
{
return n*10;
}
void main()
{
int n;
n = func(2);
printf("%d",n);
}
むう、なんかめちゃくちゃつまらないプログラムですが(^^;これをアセンブラに翻訳すると...
.text
.align 2
.globl _func
_func: /*func(int)の本体*/
pushl %ebp
movl %esp,%ebp
movl 8(%ebp),%eax /*ここで引数のロードをしている*/
movl %eax,%edx
sall $3,%edx
addl %eax,%edx
leal (%eax,%edx),%ecx
movl %ecx,%eax
leave
ret /*メインルーチンに戻る*/
LC0:
.ascii "%d\0"
.align 2
.globl _main
_main: /*メインルーチン。ここから実行される*/
pushl %ebp
movl %esp,%ebp
subl $4,%esp
call ___main
pushl $2
call _func /*ここでfunc(int)を呼んでいる*/
addl $4,%esp
movl %eax,%eax
movl %eax,-4(%ebp)
movl -4(%ebp),%eax
pushl %eax
pushl $LC0
call _printf
addl $8,%esp
xorl %eax,%eax
leave
ret
そのままgccに吐かせたコードではなく少しいじくってあります。
上で見てもらってもわかるようにcallで呼び出されて、retで再びもとに戻ってきていることがよくわかります。もちろん_printfなども実際にはライブラリにret命令が記述してあるわけです。
さて、関数funcには引数として2が渡されているわけですが、この引数はアセンブラー上ではどのように実現されているのかを見てみます。うえのメインルーチンで怪しそうなところを見ていくとpushl $2という命令がcall _funcの前にあるのがわかります。push命令は、前回スタックの操作のところでやりましたが、gccでは引数を渡すのにこのスタックの操作をすることによって実現しているわけです。サブルーチン_funcを見てみるとpop命令を使ってではなく、sp(スタックポインタ)を参照してmov命令を使って引数を参照する手段を取っています。ここでbp(ベースポインタ)を使ってメモリを参照していますが、セグメントレジスタを指定していません。一般にbpを指定した場合、セグメントレジスタは自動的にss(スタックセグメント)になります。
pushw %ax
popw %ax
です。またフラグレジスタに関しては特別に
pushf
popf
というフラグ命令が用意されています。
さて、次回は実際にgasをインラインアセンブラとして使う方法と、アセンブラ命令の続きをやります。