来週から「にほんごであそぼ」に代わりまして、新番組「アセンブラであそぼ」を放送します
最近ちまちまとアセンブラの勉強、というよりも遊びをやっている。どうも本を読んで勉強するのは性に合わなくて今までアセンブラは敬遠気味だったんだけど、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:結局問題は別のところにあったわけだけど