この投稿は インタープリズムはAdvent Calendarを愛しています。世界中のだれよりも。 Advent Calendar 2017の13日目 の記事です。
アセンブリで簡単なプログラムを作る
こんにちは、yumeです。最近アセンブリについて学習したので、簡単に何か、0から入力した正整数までの和を返すものを作ってみます。
はじめに
- アセンブリとは、機械語(0と1)をもう少し人間にとって分かりやすい形にした言語です。
- NASMというアセンブラを使っています。
- 環境構築の部分は省略します。
- 「詳しいことは(ひとまず)置いておいて、とにかく実装を見たい!」という人を意識して書こうと思ってます。
まずは10までの足し合わせ
section .text global _start _start: mov eax, 0 ;レジスタの初期化。eaxに0を入れる mov ecx, 10 ;ecxレジスタはカウンタとして使用される。今回は10までなので、ecxに10を入れる _loop: add eax, ecx ;eaxにecxを足す loop _loop ;ecxがデクリメントされて_loopラベルへ call print ;下で定義したprintサブルーチンを呼び出す mov eax, 0x0a ;改行を出力 call print mov eax, 1 ; mov ebx, 0 ; int 0x80 ;システムコール。この一連で動作が終了する。慣れないうちは何がなんだか print: push eax ;eaxの中身をスタックへ逃す mov eax, 4 ;標準出力の準備 mov ebx, 1 ; mov ecx, esp ;出力するのはesp、つまりスタックの先頭 mov edx, 1 ;1byte分の出力 int 0x80 ;システムコール pop eax ;スタックから戻す ret ;サブルーチンの呼び出し元へ
これを実行すると、
7
と出ます。「何事だ?」と驚かず、冷静にASCII文字コード表を見ると、文字'7'は10進数で55。一応、計算は出来ていそうです。
が、このままだとなんだか気持ち悪いので、10進数で返って来るようにします。
section .text global _start _start: mov eax, 0 ;レジスタの初期化。eaxに0を入れる mov ecx, 10 ;ecxレジスタはカウンタとして使用される。今回は10までなので、ecxに10を入れる _loop: add eax, ecx ;eaxにecxを足す loop _loop ;ecxがデクリメントされて_loopラベルへ mov edx, 0 ;この後のdiv演算でedxには剰余が入るので、初期化 mov ebx, 10 ;この後のdiv演算での引数になるレジスタ div ebx ;これを書くだけで、eaxをebxで割り、結果はeaxに、余りはedxに格納される。面白いけど分かりにくい add eax, '0' ;ASCII文字に変換するために'0'を足す add edx, '0' ; push edx ;一の位をスタックに避難 call print ;ここで出力されるのは十の位 pop edx ; mov eax, edx ; call print ;printサブルーチンを呼び出す mov eax, 0x0a ;改行を出力 call print mov eax, 1 ; mov ebx, 0 ; int 0x80 ;システムコール。この一連で動作が終了する。慣れないうちは何がなんだか print: push eax ;eaxの中身をスタックへ逃す mov eax, 4 ;標準出力の準備 mov ebx, 1 ; mov ecx, esp ;出力するのはesp、つまりスタックの先頭 mov edx, 1 ;1byte分の出力 int 0x80 ;システムコール pop eax ;スタックから戻す ret ;サブルーチンの呼び出し元へ
これで実行すると、
55
となります。
次は任意の正整数(定数)までの足し合わせ
さっきまでのコードですと、出力を二桁に限定しているので、13までの足し合わせが限界となっています。次はそこを突破します。
section .data MAX_NUM equ 100 ;dataセクションで定数を用意 section .text global _start _start: mov eax, 0 mov ecx, MAX_NUM ;同様の初期化。用意した定数を使う _loop: add eax, ecx loop _loop ;同様のループ mov esi, 0 ;割り算回数のカウンタ divloop: inc esi mov edx, 0 mov ebx, 10 div ebx add edx, '0' push edx ;余りを順に逃していく cmp eax, 0 ;商から0を引き、結果はフラグレジスタへ。今回は、ゼロかどうかに注目している jnz divloop ;商が0でないならdivloopへ mov edi, 0 ;出力回数のカウンタ printloop: mov eax, 4 mov ebx, 1 mov ecx, esp ;出力でecxを使ってしまうためにloopを使うことができないのがもどかしい mov edx, 1 int 0x80 pop eax inc edi cmp edi, esi jnz printloop mov eax, 0x0a call print mov eax, 1 mov ebx, 0 int 0x80 print: push eax mov eax, 4 mov ebx, 1 mov ecx, esp mov edx, 1 int 0x80 pop eax ret
これを実行すると、
5050
入力を受け取ってみる
今度は入力された正整数までの足し合わせをやって見ます。先ほどまでMAX_NUMが入っていたecxに入力を渡すことができればなんとかなりそうです。
section .bss maxnum resb 5; ;入力したデータの入る変数を用意 section .text global _start _start: mov eax, 3 ;入力準備 mov ebx, 2 ; lea ecx, [maxnum] ;maxnumのアドレスを参照させる mov edx, 5 int 0x80 mov eax, 0 mov ecx, [maxnum] ;maxnumアドレスの中身をecxへ _loop: add eax, ecx loop _loop mov esi, 0 divloop: inc esi mov edx, 0 mov ebx, 10 div ebx add edx, '0' push edx cmp eax, 0 jnz divloop mov edi, 0 printloop: mov eax, 4 mov ebx, 1 mov ecx, esp mov edx, 1 int 0x80 pop eax inc edi cmp edi, esi jnz printloop mov eax, 0x0a call print mov eax, 1 mov ebx, 0 int 0x80 print: push eax mov eax, 4 mov ebx, 1 mov ecx, esp mov edx, 1 int 0x80 pop eax ret
これを実行して、試しに1と入力してみると、
3404745
となりました。色々と入力をして見て推測するに、入力した値に2608が加算されているようです。
2608がどんな数かと言うと、16進数で0a30。ASCIIコードと照らし合わせると、0と改行の入力を引っくり返したもの(リトルエンディアン)になります。
どうやら、例えば12と入力すると、0x0a3231という入力がされるようです。これは後々の課題になるのですが、とりあえず、一の位だけを取り出すために、ビットマスクでもしておきましょう。
section .bss maxnum resb 5; ;入力したデータの入る変数を用意 section .text global _start _start: mov eax, 3 ;入力準備 mov ebx, 2 ; lea ecx, [maxnum] ;maxnumのアドレスを参照させる mov edx, 5 int 0x80 mov eax, 0 mov ecx, [maxnum] ;maxnumアドレスの中身をecxへ and ecx, 0xff ;下1byteをとる sub ecx, '0' _loop: add eax, ecx loop _loop mov esi, 0 divloop: inc esi mov edx, 0 mov ebx, 10 div ebx add edx, '0' push edx cmp eax, 0 jnz divloop mov edi, 0 printloop: mov eax, 4 mov ebx, 1 mov ecx, esp mov edx, 1 int 0x80 pop eax inc edi cmp edi, esi jnz printloop mov eax, 0x0a call print mov eax, 1 mov ebx, 0 int 0x80 print: push eax mov eax, 4 mov ebx, 1 mov ecx, esp mov edx, 1 int 0x80 pop eax ret
これで実行し、試しに9を入力してみると
45
となります。
任意の正整数入力に対して0からの和を出す
さて、ここまででなんとか一桁の入力までは対応できるようになったので、これを拡張します。
section .bss maxnum resb 5; section .text global _start _start: mov eax, 3 mov ebx, 2 lea ecx, [maxnum] mov edx, 5 int 0x80 mov ecx, [maxnum] mov esi, 0 ;入力データ(ビット)を後ろから切っていくカウンタ sliceloop: mov eax, ecx and eax, 0xff sub eax, '0' push eax ;下位8ビットを順に逃す shr ecx, 8 ;右へ8ビットシフト inc esi cmp ecx, 0x0a ;改行文字で打ち止め jnz sliceloop mov ebx, 1 ;10の倍数を持つ mov ecx, 0 mov edi, 10 ;ebxをループ毎に10倍する multiloop: pop eax ;一の位から順にとって来る mul ebx ;10の倍数をかける。結果はeaxに格納 add ecx, eax ; mov eax, ebx ;10の倍数を作るために一度eaxへ mul edi ;ループ毎に10倍 mov ebx, eax dec esi ;デクリメント cmp esi, 0 jnz multiloop mov eax, 0 _loop: add eax, ecx loop _loop mov esi, 0 divloop: inc esi mov edx, 0 mov ebx, 10 div ebx add edx, '0' push edx cmp eax, 0 jnz divloop mov edi, 0 printloop: mov eax, 4 mov ebx, 1 mov ecx, esp mov edx, 1 int 0x80 pop eax inc edi cmp edi, esi jnz printloop mov eax, 0x0a call print mov eax, 1 mov ebx, 0 int 0x80 print: push eax mov eax, 4 mov ebx, 1 mov ecx, esp mov edx, 1 int 0x80 pop eax ret
悩んだ末、なんとかできました。実行し、123と入力すれば、
7626
となります。
ただ、4桁以上の整数を入力するとセグメンテーション違反となってしまいます。
今度は何が問題なんでしょうか……といったところで、今回はここまでです。
終わりに
いかがでしたでしょうか。
簡単に、とノリで始めて見ましたが、全く簡単では無かったです。
そもそも「簡単なプログラム」というのも、それはあくまでjavaやjavascript等の高級言語での話でして、そんな「簡単」なものですら、アセンブリだとこんなに大変なんですね。
今回はデータを保存できるレジスタを、汎用レジスタの8つのみ、できるだけabcdを使うようにしていたのですが(それが良いことかどうかはともかく)、少ないリソースをやりくりするのは面白かったです。
ではでは。