interprism's blog

インタープリズム株式会社の開発者ブログです。

アセンブリで簡単なプログラムを作る編

この投稿は インタープリズムは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桁以上の整数を入力するとセグメンテーション違反となってしまいます。

今度は何が問題なんでしょうか……といったところで、今回はここまでです。

終わりに

いかがでしたでしょうか。

簡単に、とノリで始めて見ましたが、全く簡単では無かったです。

そもそも「簡単なプログラム」というのも、それはあくまでjavajavascript等の高級言語での話でして、そんな「簡単」なものですら、アセンブリだとこんなに大変なんですね。

今回はデータを保存できるレジスタを、汎用レジスタの8つのみ、できるだけabcdを使うようにしていたのですが(それが良いことかどうかはともかく)、少ないリソースをやりくりするのは面白かったです。

ではでは。

PAGE TOP