Intel 486 プロセッサ


参考文献:
	はじめて読む486,蒲地輝尚 著,アスキー出版局,1994

Intel CPUの発展:

4004 → 8080 → 8086 → 80186 → 80286 → 80386 → 80486(i486) → Pentium → PentiumPro → Pentium II → Pentium III → PentiumIV → IA-64

最近の Windows パソコンで使われているCPU は Pentium IV などですが、これらは i486 の命令体系を そのまま受け継いでいます。


i486のレジスタ

図2-12参照
図2-14参照
図2-15参照

i486のアセンブリ言語(gccの_asmにおける表記方法)

レジスタ
  %eax, %ebx, %ecx, %edx, %ebp, %esp
アドレスモード
  %eax    レジスタ指定
  $10     定数10
  10      アドレス 10番地
  8(%ebp) レジスタ相対(ebpの指している位置から8バイト先)
  (%ecx)  レジスタ間接(ecxの指している位置のデータ)
ラベル
   行の先頭で、名前に ':'を付ける。
  L0:
  L1:
命令
  shrl  $8, %eax   論理右シフト(この場合は8ビットシフト)
  shll  $8, %eax   論理左シフト(この場合は8ビットシフト)
  asll  $8, %eax   算術左シフト(この場合は8ビットシフト)
  movl  a, b       aをbにコピー (コピーの方向はアセンブラによって異なることに注意)
  push  a          a をスタックに push
  pop   a          a にスタックから pop
  xorl  a, b       a xor b → b
  andl  a, b       a and b → b
    (マスク操作:
      例えば%eaxの下位4ビットを残したいときは
      andl $15,%eax
      とする)
  cmpl a, b        aとbを比較する(フラグレジスタが変化する)
  jne L            フラグレジスタの状態が b != a ならばジャンプする。
  je  L            b == a ならばジャンプする。
  jle L            b <= a ならばジャンプする。
  jl  L            b <  a ならばジャンプする。
  jge L            b >= a ならばジャンプする。
  jg  L            b >  a ならばジャンプする。
  call f           サブルーチン f を呼び出す
  ret              サブルーチンを呼び出した場所に戻る
関数の返り値
  関数の最後で%eaxに値を入れて ret する。

i486の命令一覧

参考文献のAppendix参照

注意:Appendixの表は、gccが出力する命令とは オペランドのsourceと target の向きが逆になっていることに 注意して下さい。

文書アセンブラの書式
gccの出力命令  source, target
この文書命令  source, target
参考文献命令  target, source

i486のアセンブリ言語の例

関数と戻り値

まず、C言語で簡単な関数を作成してみましょう。 ファイル名は sample1.c にします。

sample1.c
int sample1() {
  return(5);
}

次に、gccを使ってコンパイルし、 i486 のアセンブリ言語を出力させてみましょう。 gcc に -S オプションをつけるとアセンブリ言語の出力が得られます。 -O や -O2 オプションをつけると最適化のレベルが変わります(数字が大きいほど 最適化のレベルが上)。

sample1.s (gcc -O2 -S sample1.c)
	.file	"sample1.c"
	.text
	.align 2
	.align 16
.globl _sample1
	.def	_sample1;	.scl	2;	.type	32;	.endef
_sample1:
	pushl	%ebp
	movl	%esp, %ebp
	movl	$5, %eax
	popl	%ebp
	ret

返り値として5を返す関数ですが、eaxレジスタにその値が入っていることが わかります。

関数への引数

sample2.c
int sample2(int x,int y) {
  int z;
  z = x + y;
  return(z);
};

sample2.s (gcc -O2 -S sample2.c)
	.file	"sample2.c"
	.text
	.align 2
	.align 16
.globl _sample2
	.def	_sample2;	.scl	2;	.type	32;	.endef
_sample2:
	pushl	%ebp
	movl	%esp, %ebp
	movl	12(%ebp), %eax
	movl	8(%ebp), %edx
	popl	%ebp
	addl	%edx, %eax
	ret

sample2.s (gcc -S sample2.c)
	.file	"sample2.c"
	.text
	.align 2
.globl _sample2
	.def	_sample2;	.scl	2;	.type	32;	.endef
_sample2:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$4, %esp
	movl	12(%ebp), %eax
	addl	8(%ebp), %eax
	movl	%eax, -4(%ebp)
	movl	-4(%ebp), %eax
	leave
	ret

関数の最初は

    pushl %ebp
    movl %esp, %ebp
から始まっています。これは、「まず、(espが指す)スタック上に 古いベースポインタを積んで、さらに、そのときのスタックポインタの値を ベースポインタにコピーしておく」という操作です。 これにより、「ベースポインタがこの関数フレームの始まりを指す」ことになり、 スタックポインタの値を自由に変更できる(スタック上に値をpushしてよい)」 ことになります。

上記のアセンブリ言語に現れる leave という命令は

    movl  %ebp, %esp
    popl  %ebp
という2つの命令をまとめて書いたものだと理解して下さい。 すなわち、「ベースポインタ(ebp)の値をスタックポインタ(esp)に 上書きすることでスタックポインタを一気に関数フレームの先頭に戻し、 その後、スタックポインタが指す場所から『古いベースポインタ』を 取り出している」のです。

この後、

    ret
命令で、スタック上から「返り番地」を取り出して 「この関数を呼び出した命令の次の命令」から実行を再開すれば、 「eaxレジスタに返り値が入っていて」、さらに 「スタック上には呼び出し側で用意した引数が積まれている状態」 ということになります。

関数を呼び出すときのスタックの使い方を図に示します。 sample3.c の中のmain関数が、sample2.cの中の int sample2(int x,int y)関数を呼出すときの スタックの様子です。 わかりやすさを優先して、最適化オプションを指定しない 場合のスタックの使い方を表現しています。

関数名 main main main main
機械語 pushl $3 pushl $2 call sample2
スタック
関数名 sample2 sample2 sample2
機械語 pushl %ebp movl %esp,%ebp subl $4,%esp
スタック
関数名 sample2 sample2 sample2 sample2 main
機械語 引数はebp+8, ebp+12 movl %ebp, %esp popl %ebp ret addl $8, %esp
スタック

関数の呼び出し

sample3.c
int main()
{
  int i;
  i=sample2(2,3);
  printf("%d\n",i);
}

sample3.s (gcc -S sample3.c)
	.file	"sample3.c"
	.def	___main;	.scl	2;	.type	32;	.endef
	.text
LC0:
	.ascii "%d\12\0"
	.align 2
.globl _main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	andl	$-16, %esp
	movl	$0, %eax
	movl	%eax, -8(%ebp)
	movl	-8(%ebp), %eax
	call	__alloca
	call	___main
	movl	$2, (%esp)
	movl	$3, 4(%esp)
	call	_sample2
	movl	%eax, -4(%ebp)
	movl	$LC0, (%esp)
	movl	-4(%ebp), %eax
	movl	%eax, 4(%esp)
	call	_printf
	leave
	ret
	.def	_printf;	.scl	2;	.type	32;	.endef
	.def	_sample2;	.scl	2;	.type	32;	.endef

sample3.s (gcc -O2 -S sample3.c)
	.file	"sample3.c"
	.def	___main;	.scl	2;	.type	32;	.endef
	.text
LC0:
	.ascii "%d\12\0"
	.align 2
	.align 16
.globl _main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	xorl	%eax, %eax
	andl	$-16, %esp
	call	__alloca
	call	___main
	movl	$3, 4(%esp)
	movl	$2, (%esp)
	call	_sample2
	movl	%eax, 4(%esp)
	movl	$LC0, (%esp)
	call	_printf
	movl	%ebp, %esp
	popl	%ebp
	ret
	.def	_printf;	.scl	2;	.type	32;	.endef
	.def	_sample2;	.scl	2;	.type	32;	.endef

「オプティマイズなし」のコードは余分な操作をあちこちで 行っていてひどく効率の悪いものであることがわかります。 それに対して「オプティマイズ・レベル2 (-O2)」の場合は効率の よいコードが出力されています。 普通(-Oオプションなしの場合)は、関数呼び出しから戻る毎に スタックポインタに(4や8などを)加算して戻します。 しかし-O2 をつけた場合のコードでは関数呼び出しの後で スタックポインタを戻していません。 これは高速化のためにスタック用メモリを浪費していることになります。 -O2の場合は、スタックが32バイト伸びた段階で一気に戻すようなコードを 出しています (sample7.s参照。動作はgccのバージョンによって異なります)。
sample7.c
int sample6(int x)
{
  printf("%08x\n",&x);
}

int main() {
  int a,b;
  a = 123;
  b = 3;
  sample6(a<<2+b);   /* $B0J2<(B99$B9TF1$8(B */
     ...
}

sample7.s (gcc -O2 -S sample3.c)
	.file	"sample7.c"
	.version	"01.01"
gcc2_compiled.:
.section	.rodata
.LC0:
	.string	"%08x\n"
.text
	.align 4
.globl sample6
	.type	 sample6,@function
sample6:
	pushl %ebp
	movl %esp,%ebp
	leal 8(%ebp),%edx
	pushl %edx
	pushl $.LC0
	call printf
	leave
	ret
.Lfe1:
	.size	 sample6,.Lfe1-sample6
	.align 4
.globl main
	.type	 main,@function
main:
	pushl %ebp
	movl %esp,%ebp
	pushl %ebx
	movl $3936,%ebx
	pushl %ebx
	call sample6
	pushl %ebx
	call sample6
	pushl %ebx
	call sample6
	pushl %ebx
	call sample6
	pushl %ebx
	call sample6
	pushl %ebx
	call sample6
	pushl %ebx
	call sample6
	pushl %ebx
	call sample6
	addl $32,%esp     $B"+0l5$$K(B32byte$BJ,%9%?%C%/$rLa$9(B
	 ...              8$B8F$S=P$7Kh$K%9%?%C%/$rLa$9$3$H$N7+$jJV$7(B
	movl -4(%ebp),%ebx
	leave
	ret
.Lfe2:
	.size	 main,.Lfe2-main
	.ident	"GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"

再帰関数(階乗を計算する)

fact.c
int fact(int n) {
  int i;
  if (n <= 0) i=1;
  else i = n * fact(n-1);
  return(i);
}

fact-O2.s (gcc -S -O2 fact.c)
	.file	"fact.c"
	.text
	.align 2
	.align 16
.globl _fact
	.def	_fact;	.scl	2;	.type	32;	.endef
_fact:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	movl	%ebx, -4(%ebp)
	movl	8(%ebp), %ebx
	movl	$1, %eax
	testl	%ebx, %ebx
	jle	L3
	leal	-1(%ebx), %eax
	movl	%eax, (%esp)
	call	_fact
	imull	%ebx, %eax
L3:
	movl	-4(%ebp), %ebx
	movl	%ebp, %esp
	popl	%ebp
	ret

fact.s (gcc -S fact.c)
	.file	"fact.c"
	.text
	.align 2
.globl _fact
	.def	_fact;	.scl	2;	.type	32;	.endef
_fact:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	cmpl	$0, 8(%ebp)
	jg	L2
	movl	$1, -4(%ebp)
	jmp	L3
L2:
	movl	8(%ebp), %eax
	decl	%eax
	movl	%eax, (%esp)
	call	_fact
	movl	%eax, %edx
	movl	8(%ebp), %eax
	imull	%edx, %eax
	movl	%eax, -4(%ebp)
L3:
	movl	-4(%ebp), %eax
	leave
	ret

関数の呼び出し

sample5.c
int sample5(int a,int b) {
  int c;
  c=a+b;
  printf("%d\n",c);
  c=a-b;
  printf("%d\n",c);
  c=a*b;
  printf("%d\n",c);
  c=a/b;
  printf("%d\n",c);
}

sample5.s (gcc -O2 -S sample5.c)
	.file	"sample5.c"
	.text
LC0:
	.ascii "%d\12\0"
	.align 2
	.align 16
.globl _sample5
	.def	_sample5;	.scl	2;	.type	32;	.endef
_sample5:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	movl	%ebx, -8(%ebp)
	movl	8(%ebp), %ebx
	movl	%esi, -4(%ebp)
	movl	12(%ebp), %esi
	movl	$LC0, (%esp)
	leal	(%esi,%ebx), %eax
	movl	%eax, 4(%esp)
	call	_printf
	movl	$LC0, (%esp)
	movl	%ebx, %eax
	subl	%esi, %eax
	movl	%eax, 4(%esp)
	call	_printf
	movl	$LC0, (%esp)
	movl	%ebx, %eax
	imull	%esi, %eax
	movl	%eax, 4(%esp)
	call	_printf
	movl	$LC0, 8(%ebp)
	movl	%ebx, %eax
	movl	-8(%ebp), %ebx
	cltd
	idivl	%esi
	movl	-4(%ebp), %esi
	movl	%eax, 12(%ebp)
	movl	%ebp, %esp
	popl	%ebp
	jmp	_printf
	.def	_printf;	.scl	2;	.type	32;	.endef

sample5.s (gcc -S sample5.c)
	.file	"sample5.c"
	.text
LC0:
	.ascii "%d\12\0"
	.align 2
.globl _sample5
	.def	_sample5;	.scl	2;	.type	32;	.endef
_sample5:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	movl	12(%ebp), %eax
	addl	8(%ebp), %eax
	movl	%eax, -4(%ebp)
	movl	$LC0, (%esp)
	movl	-4(%ebp), %eax
	movl	%eax, 4(%esp)
	call	_printf
	movl	12(%ebp), %edx
	movl	8(%ebp), %eax
	subl	%edx, %eax
	movl	%eax, -4(%ebp)
	movl	$LC0, (%esp)
	movl	-4(%ebp), %eax
	movl	%eax, 4(%esp)
	call	_printf
	movl	8(%ebp), %eax
	imull	12(%ebp), %eax
	movl	%eax, -4(%ebp)
	movl	$LC0, (%esp)
	movl	-4(%ebp), %eax
	movl	%eax, 4(%esp)
	call	_printf
	movl	8(%ebp), %edx
	movl	%edx, %eax
	cltd
	idivl	12(%ebp)
	movl	%eax, -4(%ebp)
	movl	$LC0, (%esp)
	movl	-4(%ebp), %eax
	movl	%eax, 4(%esp)
	call	_printf
	leave
	ret
	.def	_printf;	.scl	2;	.type	32;	.endef


Cのプログラム中に直接機械語を記述する方法

asm文を使います。 文字列の中で改行したい場合は、その改行を打ち消すために改行文字の 前に \ (バックスラッシュ、または、円マーク)を入れます。 ただしそのままだと、機械語ファイル(.s)の中で行がつながって しまうので、'\改行'の前に明示的に改行コード \n を入れておきます。

例:
foo.c
int test(int x, int y)
{
__asm__(" \n\
        pushl %edx \n\
	movl 8(%ebp),%edx \n\
	addl 12(%ebp),%edx \n\
	movl %edx,%eax \n\
        popl %edx \n\
");
}

main() {
   printf("%d\n", test(2,3));
}

gcc -O2 -S foo.cで生成したfoo.s
	.file	"foo.c"
	.text
	.align 2
	.align 16
.globl _test
	.def	_test;	.scl	2;	.type	32;	.endef
_test:
	pushl	%ebp
	movl	%esp, %ebp
/APP
	 
        pushl %edx 
	movl 8(%ebp),%edx 
	addl 12(%ebp),%edx 
	movl %edx,%eax 
        popl %edx 

/NO_APP
	popl	%ebp
	ret
	.def	___main;	.scl	2;	.type	32;	.endef
LC0:
	.ascii "%d\12\0"
	.align 2
	.align 16
.globl _main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	xorl	%eax, %eax
	andl	$-16, %esp
	call	__alloca
	call	___main
	movl	$3, 4(%esp)
	movl	$2, (%esp)
	call	_test
	movl	%eax, 4(%esp)
	movl	$LC0, (%esp)
	call	_printf
	movl	%ebp, %esp
	popl	%ebp
	ret
	.def	_printf;	.scl	2;	.type	32;	.endef

gcc -S foo.cで生成したfoo.s
	.file	"foo.c"
	.text
	.align 2
.globl _test
	.def	_test;	.scl	2;	.type	32;	.endef
_test:
	pushl	%ebp
	movl	%esp, %ebp
/APP
	 
        pushl %edx 
	movl 8(%ebp),%edx 
	addl 12(%ebp),%edx 
	movl %edx,%eax 
        popl %edx 

/NO_APP
	popl	%ebp
	ret
	.def	___main;	.scl	2;	.type	32;	.endef
LC0:
	.ascii "%d\12\0"
	.align 2
.globl _main
	.def	_main;	.scl	2;	.type	32;	.endef
_main:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	andl	$-16, %esp
	movl	$0, %eax
	movl	%eax, -4(%ebp)
	movl	-4(%ebp), %eax
	call	__alloca
	call	___main
	movl	$2, (%esp)
	movl	$3, 4(%esp)
	call	_test
	movl	$LC0, (%esp)
	movl	%eax, 4(%esp)
	call	_printf
	leave
	ret
	.def	_printf;	.scl	2;	.type	32;	.endef

fooの実行結果
nitta@degas 648> gcc -O2 foo.c -o foo
nitta@degas 649> ./foo
5


i486マシンの使い方

南校舎に設置されているWindowsパソコンは Intel 製のCPU (Pentium IVやセレロンなど)を搭載しており、i486の命令を 直接実行できます。

この授業の演習として i486 のアセンブリ言語で書いたプログラムを 実行させてみることにしましょう。 開発環境としては Windows 上で動作する cygwin を用い、 コンパイラとアセンブラにはgcc を使います。

多くの部分はCでプログラミングをし、部分的にアセンブリ言語で 記述することにしましょう。


提出課題

問題1

if 文のコードを調べなさい。 適切なCの関数を定義し、gccを使ってアセブリ言語のコードを出力し、 それにわかりやすいコメントを書き加えてから arch1@nw.tsuda.ac.jp に送って下さい。Subjectは report 1 として下さい。

GCCのアセンブリ言語の中では、

がコメントとなります。

あまりに簡単な条件式でコンパイラが成り立つか否かを判断できる場合は、 if 文のコードをださないことがあることに注意して下さい。 つまり、以下のようなCのコードを書いても駄目だ、ということです。

  (例)
    if (1 > 0) { ... } ←コンパイル時に、いつでも真と判明する
  (例2)
    int a = 500, b=450;
    if (a < b) { ... } ← コンパイル時に、いつでも偽と判明する

問題2

for 文のコードを調べなさい。 適切なCの関数を定義し、gccを使ってアセブリ言語のコードを出力し、 それにわかりやすいコメントを書き加えてから arch2@nw.tsuda.ac.jp に送って下さい。Subjectは report 2 として下さい。

問題3

1から「引数で指定された整数」までの和を返す関数を 機械語で書きなさい。 Cの関数の中にasm文でアセンブリ言語の命令を書くこと。 それにわかりやすいコメントを書き加えてから arch3@nw.tsuda.ac.jp に送りなさい。Subjectは report 3 としなさい。

問題4

引数として与えられた 32bit整数が奇数であれば1を、 奇数でなければ 0 を返す関数 int isodd(int n) の 本体をアセンブリ言語で書きなさい。

ただし、 多少実行速度が遅くても構わないので、__asm__ の中では 機械語命令としては 「movl, testl, jz, jmp だけ」 を使うこと。ラベル、レジスタ名、定数などは自由に使用して構わない。

動作することを確認したら、ソースを arch4@nw.tsuda.ac.jp に送りなさい。Subjectは report 4 としなさい。

isodd.c
int isodd(int n) {
  __asm__("
   /* ここを次の機械語だけを使って書くのが課題です。
          movl, testl, jz, jmp */
  ");
}

oddmain.c
#include <stdio.h>
int main() {
  int n;
  printf("整数を入力して下さい    ");
  scanf("%d",&n);
  printf("%d\n",isodd(n));
}

oddmain.cの実行例
PROMPT$ gcc -O2 oddmain.c isodd.c -o oddmain 
PROMPT$ oddmain 
整数を入力して下さい    5 
1       ← isoddの返り値
PROMPT$ oddmain 
整数を入力して下さい    4 
0       ← isoddの返り値

[注意] C言語で関数を作っておいてそれをコンパイルしてアセンブリ言語を出させる 方法では、上記の機械語だけを使ったコードはでないでしょう。 自分で考えて書くことが重要です。