来週から「にほんごであそぼ」に代わりまして、新番組「アセンブラであそぼ」を放送します

最近ちまちまとアセンブラの勉強、というよりも遊びをやっている。どうも本を読んで勉強するのは性に合わなくて今までアセンブラは敬遠気味だったんだけど、Cで書いたプログラムのアセンブリコードを読むのはなかなかに面白い。そんなわけで、プログラミングの力を生み出す本―インテルCPUのGNUユーザへ あたりを横に置いてあそんでいる。

この遊びを始めたそもそものきっかけはソフトウェアの授業のソーティング課題。OCamlで500万レコードのソートをやろうとしてstack overflow連発。これ末尾再帰になってないんじゃね?と思ってobjdumpしてcallを探して*1、、、ということをやっているうちに、アセンブラ書けなくてもいいから何やってるか読めるくらいにはなりたいな、と思ってやりはじめた。後から思ったのは、まさにBinary Hacksの巻頭言でshiroさんが書いていた「抽象化の壁」が、ヘボいコードを書いたせいで崩れてしまったわけだ。とりあえず動かしたいプログラムを書いてるときに抽象化の壁が崩れたらたまったもんじゃないけど、遊びながら書いてたプログラムなので壁を壊すのもなかなか楽しかった。

とりあえず、今日は単純な関数呼び出しのプログラムで遊んだ。Cの関数呼び出しがアセンブラのレベルでどうなっているのか、ちょっとだけわかってきた気がする。

#include <stdio.h>

int plus(int x, int y)
{
    return x+y;
}

int main(int argc, char *argv[])
{
    int hoge;
    hoge = plus(1,2);
    printf("%d\n", hoge);
    return 0;
}

これをcall.cという名前で保存して gcc -S call.c するとこうなる。

	.file	"call.c"
	.text
.globl plus
	.type	plus, @function
plus:
	pushl	%ebp
	movl	%esp, %ebp
	movl	12(%ebp), %eax
	addl	8(%ebp), %eax
	popl	%ebp
	ret
	.size	plus, .-plus
	.section	.rodata
.LC0:
	.string	"%d\n"
	.text
.globl main
	.type	main, @function
main:
	leal	4(%esp), %ecx
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%ecx
	subl	$36, %esp
	movl	$2, 4(%esp)
	movl	$1, (%esp)
	call	plus
	movl	%eax, -8(%ebp)
	movl	-8(%ebp), %eax
	movl	%eax, 4(%esp)
	movl	$.LC0, (%esp)
	call	printf
	movl	$0, %eax
	addl	$36, %esp
	popl	%ecx
	popl	%ebp
	leal	-4(%ecx), %esp
	ret
	.size	main, .-main
	.ident	"GCC: (GNU) 4.1.2 (Ubuntu 4.1.2-0ubuntu4)"
	.section	.note.GNU-stack,"",@progbits

GCCの吐くアセンブリコードは、スタックポインタ%espに直接movlしてみたり、何をやっているのかわかりにくい。てか、本に「スタックポインタをプログラマが直接操作することは少ない」って書いてあるので、スタックポインタを直接操作せずにpop, pushにまかせる感じで、同じことをやるコードを自分で書いてみた。

	.file "mycall.s"
.globl plus
	.type	plus, @function
plus:
	popl	%ebp
	popl	%eax
	popl	%edx
	addl	%edx, %eax
	pushl	%ebp
	ret
.LC0:
	.string "%d\n"
	.text
.globl main
	.type	main, @function
main:
	pushl	$1
	pushl	$2
	call	plus
	pushl	%eax
	pushl	$.LC0
	call	printf
	popl	%eax
	popl	%eax
	ret
	.size	main, .-main
	.ident	"hogehoge"
	.section	.note.GNU-stack,"",@progbits

書いていてハマったのが、retするときのスタックポインタの値。callしたタイミングで、スタックの一番上に処理が帰るアドレスが入っているっぽくて、retするときにもスタックの一番上がそれになってないとcallした場所に帰れない。考えてみたら当たり前だ。しかし、スタックの一番上がアドレスになってたら、引数をスタックから掘り出すのが面倒。

一応%espをグリグリと直接操作する版も書いてみた。何度もpopしたりpushしたりするより、こっちのほうがきっと早い。でも危険な感じだな。

	.file "mycall.s"
.globl plus
	.type	plus, @function
plus:
	movl	8(%esp), %eax
	addl	4(%esp), %eax
	ret
.LC0:
	.string "%d\n"
	.text
.globl main
	.type	main, @function
main:
	movl	%esp, %ebp
	subl	$8, %esp
	movl	$1, 4(%esp)
	movl	$2, (%esp)
	call	plus
	pushl	%eax
	pushl	$.LC0
	call	printf
	movl	%ebp, %esp
	ret
	.size	main, .-main
	.ident	"hogehoge"
	.section	.note.GNU-stack,"",@progbits

わかった(っぽい)こと

スタックに値をpushlすると、%espが指すメモリのアドレスは4小さくなる。ほげほげエンディアンとかの関係かな。きっとエンディアンが逆のアーキテクチャではスタックに値を乗せるとスタックポインタの値は大きくなるんだろう(本当か?)(追記: 嘘でした)。エンディアンは話としてはなんとなく理解してるつもりだけど、感覚としては全然掴めていない。

Stack overflowというと、関数の再帰の回数ばっかり気にしてたけど、引数もスタックに乗せてるんだから引数の数も効いてくるんじゃまいか。

わからないこと

%edxとか%ebpの正しい使い方はまだよくわからん。

そのうちやること

スタックじゃなくてヒープ上に値をのっける。

*1:結局問題は別のところにあったわけだけど