70
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ためしておぼえるアセンブラ入門

Last updated at Posted at 2018-03-18

はじめに

  • 書籍「低レベルプログラミング」を読んでアセンブラについて勉強したので、実際に適当な課題を解いて説明しながらアセンブラに入門してみる。
  • 同書籍のコードが Docker で実行できる環境の構築についてはこちらを参照のこと。

true コマンド実装

  • まずは一番簡単な文法を学習するため、終了コード 0 で exit システムコールを実行するだけの true コマンドを実装してみる。

コード

workdir/true.asm
asm:true.asm
global _start

section .text

_start:
	mov rdi, 0
	mov rax, 60
	syscall

実行方法

  • 本家のリポジトリをフォークしたものにコードと実行環境を追加し、(Docker があれば)すぐに試してみることができるようにしてみた。
# フォークしたリポジトリをクローン
git clone https://github.com/nirasan/low-level-programming
cd low-level-programming

# 実行環境の docker マシンをビルドして立ち上げる
docker_build.sh
docker_run.sh

# workdir をマウントしてあるので移動
cd /workdir

# true.asm のコンパイルと実行
nasm -felf64 true.asm -o true.o
ld true.o -o true
./true
echo $? #=> 0

true コマンド解説

1 行目の global とは

  • global は指定したシンボルへの参照を外部に公開するディレクティブ。

1 行目の _start とは

  • ._ などで始まるものをシンボルと呼び 32bit の値を持つ。
  • シンボルの中でも : で終わるものはラベルと呼び、定義された場所のアドレスを持つ。
  • _start は 5 行目で定義されたラベルなのでこの場所のアドレスを値としてもつ。

1 行目の意味は

  • global _start とすることで _start のシンボルと定義位置のアドレスを外部に公開する。
  • ld では _start という名前のシンボルから処理を開始するように実行ファイルを作成するので、ここでプログラムのエントリーポイントが定義されている。

3 行目の section とは

  • section ディレクティブは記載されるコードがプログラム中でどのような意味を持つのかを定義する。
  • section .text とすることで続いて記載されるコードが書き換え不可能なデータであると定義されるので、.text セクションにはプログラム本体や定数などを記載していく。
  • 他にも書き換え可能な変数を定義するセクションなどがあるが、実際に利用する際に使用例とともに解説する。

8 行目の syscall とは

  • syscall は OS の機能を呼び出すコマンド。
  • どの機能を呼び出すか、どんな引数で呼び出すか、などはあらかじめ準備する必要があり、6, 7 行目でその準備が行われているので先にそちらを見ていく。

6 行目の mov とは

  • mov は値をコピーするコマンドで、 mov A, B で A に B の値をコピーする。
  • mov rdi, 0 とすると、レジスタ rdi の値を 0 にしている。
  • レジスタとはあらかじめ用意されている記憶領域で容量と役割をもつ。
  • rdi はデスティネーションインデックスレジスタと呼ばれ 64bit の大きさを持つ。
  • rdi の役割として syscall 実行時に値が第一引数として参照される。

7 行目の意味は

  • mov rax, 60 で、レジスタ rax の値を 60 にしている。
  • rax はアキュムレータレジスタと呼ばれ 64bit の大きさを持つ。
  • rax の役割として syscall 実行時に値がシステムコール番号として参照される。
  • exit などのシステムコールはシステムコール番号という番号で参照される。システムコール番号の一覧は Debian だと ausyscall --dump で参照できる。-> 参考URL
  • 60 番のシステムコールは exit

改めて 8 行目の意味は

  • syscall コマンドを実行すると rax の値である 60 番のシステムコールの exit が実行される。
  • exit は rdi に入っている値 0 を第一引数として実行され、終了コード 0 としてプログラムを終了する。

Hello World の実装

  • 定番の "Hello, World" を出力するコードを実装することで、変数定義と write システムコールについて学習する。
workdir/hello.asm
section .data

message: db "Hello, World", 10

section .text

global _start

_start:
    mov rax, 1       ; syscall number. 1 is write
    mov rdi, 1       ; 1st argument.   dest file descriptor number. 1 is stdout
    mov rsi, message ; 2nd argument.   input data start address
    mov rdx, 13      ; 3rd argument.   input data length
    syscall

    mov rax, 60  ; syscall number. 60 is exit
    xor rdi, rdi ; 1st argument register
    syscall

Hello World の解説

section .data

  • .data セクションは書き換え可能なデータの定義セクションで、特に値がセットされた変数の定義を行うための宣言。
  • セクションの切り替えはいつでも何度でもできる。

message: db "Hello, World", 10

  • db 1 で 1 を 8bit のデータとして書き込むというコマンド。
  • db 1, 2, 3 で 1, 2, 3 を連続して書き込む。
  • db "string" で文字列を ASCII コードに変換して数値として連続して書き込む。
  • L: db 1, 2, 3 で db で書き込んだデータの先頭アドレスをラベル L で参照できるようにする。
  • つまり message: db "Hello, World", 10
    • db 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 10 というデータを書き込んで
    • そのデータのアドレスを message ラベルで参照できるようにするということ
    • データの末尾の 10 は ASCII コードで改行

システムコール write の実行

  • _start の直後 5 行でシステムコール write の呼び出しを行なっている。
レジストリ 役割 値の意味
rax syscall 番号の指定 60 write
rdi 第一引数. 出力先のファイルディスクリプタを指定する. 1 標準出力
rsi 第二引数. 出力するデータの先頭アドレス message 定義済みの "Hello, World", 10 のアドレス
rdx 第三引数. 出力するデータのバイト数 13 message のバイト数

xor rdi, rdi とは

  • 最後から 3 行は exit システムコール呼び出しだが、第一引数の rdi の指定方法が前回と異なる。
  • xor A, B とすると A と B で同じビットが 0 になるので、 xor, rdi, rdi と同じレジストリを指定すると全て 0 にクリアされる。 mov rdi, 0 とするより実行効率がいいとかなんとか。

Hello World を繰り返し表示する

  • Hello World を繰り返し表示するコードの実装で、jump, push, pop について学習する。
  • 解説はコメントで記載した。
workdir/hello3.asm
section .data

message: db "Hello, World", 10

section .text

global _start

_start:

    ; レジスタ rcx をカウンターとして利用する. 初期値 3
    mov rcx, 3

.loop:
    ; カウンターをデクリメント
    dec rcx

    ; write システムコールのパラメータセット
    mov rax, 1
    mov rdi, 1
    mov rsi, message
    mov rdx, 13

    ; push コマンドでカウンターをスタックに一時保存
    ; rcx は syscall の返り値が挿入されるので上書きされても復帰できるように対比しておく
    push rcx
    ; write の実行
    syscall
    ; pop コマンドでカウンターをスタックから復帰させる
    pop rcx

    ; test コマンドでカウンターが 0 であるかどうかをチェックする.
    ; もし 0 なら zero flag が立つ
    test rcx, rcx
    ; jnz は jump if not zero の意味. zero flag が立っていなければ対象の .loop ラベルのアドレスへジャンプする.
    jnz .loop

    ; exit の実行
    mov rax, 60
    xor rdi, rdi
    syscall

関数コールと相対アドレスの指定

  • 関数の定義と呼び出し方、相対アドレスの指定の仕方二種類について学習するため、ふたつの文字列を交互に出力するコードを実装してみる。
workdir/zip.asm
section .data
numbers: db "0123456789", 10
strings: db "ABCDEFGHIJ", 10

section .text
global _start

; 関数定義は単なるラベルで、終端で ret するだけ
print_numbers:
    mov rax, 1
    mov rdi, 1
    ; 出力する文字をカウンター分だけずらして一文字ずつ表示する
    ; mov を使って相対アドレスを指定する場合は一度 mov でベースをセットしてから add でオフセット分ずらす
    mov rsi, numbers
    add rsi, rcx
    mov rdx, 1
    push rcx
    syscall
    pop rcx
    ret

print_strings:
    mov rax, 1
    mov rdi, 1
    ; lea A, B とすると A のアドレスを計算して B に入れる.
    ; lea を使うとひとつのコマンドで相対アドレスを指定することができる
    lea rsi, [strings + rcx]
    mov rdx, 1
    push rcx
    syscall
    pop rcx
    ret

; ついでに exit も関数化
exit:
    mov rax, 60
    xor rdi, rdi
    syscall

_start:
    mov rcx, 0

.loop:
    ; 関数呼び出しは call
    call print_numbers
    call print_strings

    ; 数値比較によるジャンプ
    ; jle は jump if less or equals
    inc rcx
    cmp rcx, 10
    jle .loop

    call exit

実行コマンドと実行結果

$ nasm -felf64 zip.asm && ld zip.o && ./a.out
0A1B2C3D4E5F6G7H8I9J

mov と lea の違いは

  • mov は値のコピーで lea はアドレスを計算してコピーする
; label ラベルのアドレスを rax にセットする処理として
mov rax, label
; と
lea rax, [label]
; は等価

; label ラベルの起点から rcx の値のバイト数分後ろの位置のアドレスを rax にセットする処理として
mov rax, label
add rax, rcx
; と
lea rax, [label + rcx]
; は等価

FizzBuzz 問題

  • ここまでの復習として FizzBuzz 問題を解いてみる。
  • ロジックは単純だがレジスタの制御と文字列表示が面倒。
workdir/fizzbuzz.asm
section .data
str_fizz: db "fizz"
str_buzz: db "buzz"
str_numbers: db "0123456789"
str_newline: db 10

section .text
global _start

; fizz の表示
print_fizz:
    mov rax, 1
    mov rdi, 1
    mov rsi, str_fizz
    mov rdx, 4

    push rcx
    syscall
    pop rcx

    ret

; buzz の表示
print_buzz:
    mov rax, 1
    mov rdi, 1
    mov rsi, str_buzz
    mov rdx, 4

    push rcx
    syscall
    pop rcx

    ret

; カウンターの数字を3桁の10進数として表示する
print_number:
    ; カウンターを 100 で割って商を表示
    mov rdx, 0
    mov rax, rcx
    mov rbx, 100
    div rbx
    ; 余りを次で使うので保存
    push rdx
    call print_rax_number

    ; 余りを 10 で割って商を表示
    pop rax
    mov rdx, 0
    mov rbx, 10
    div rbx
    ; 余りを次で使うので保存
    push rdx
    call print_rax_number

    ; 余りを 1 の位として表示
    pop rax
    call print_rax_number

    ret

; rax に入っている数字を文字列として表示
print_rax_number:
    lea rsi, [str_numbers + rax]
    mov rax, 1
    mov rdi, 1
    mov rdx, 1

    push rcx
    syscall
    pop rcx

    ret

; 改行の表示
print_newline:
    mov rax, 1
    mov rdi, 1
    mov rsi, str_newline
    mov rdx, 1

    push rcx
    syscall
    pop rcx

    ret

; fizz を表示するかチェック
; カウンターを 3 で割って 0 と比較だけしておく
; 0 なら je LABEL でラベルにジャンプできる
check_fizz:
    mov rdx, 0
    mov rax, rcx
    mov rbx, 3
    div rbx
    cmp rdx, 0
    ret

; fizz と同じように buzz のチェック
check_buzz:
    mov rdx, 0
    mov rax, rcx
    mov rbx, 5
    div rbx
    cmp rdx, 0
    ret

; 終了
exit:
    mov rax, 60
    xor rdi, rdi
    syscall

_start:
    ; カウンターの初期化
    mov rcx, 1

    .loop:
        ; fizz のチェックとジャンプ
        call check_fizz
        je .print_fizz

        ; fizz じゃなければ buzz のチェックとジャンプ
        call check_buzz
        je .print_buzz

        ; fizz でも buzz でもなければカウンターを10進数で表示
        .print_default:
            call print_number
            call print_newline
            jmp .next

        ; fizz の表示
        .print_fizz:
            call print_fizz

            ; fizzbuzz のケースのためにチェックとジャンプ
            call check_buzz
            je .print_buzz

            call print_newline
            jmp .next

        ; buzz の表示
        .print_buzz:
            call print_buzz
            call print_newline
            jmp .next

        .next:

        ; カウンターをインクリメントし 100 以下ならループする
        inc rcx
        cmp rcx, 100
        jle .loop

    call exit

実行コマンドと実行結果

$ nasm -felf64 fizzbuzz.asm && ld fizzbuzz.o && ./a.out
001
002
fizz
004
buzz
fizz
007
008
fizz
buzz
011
fizz
013
014
fizzbuzz
016
017
fizz
019
buzz
... 略 ...

ファイルをメモリに展開する

  • システムコール open でファイルを開いて mmap でメモリに展開する。
workdir/mmap.asn

%define O_READONLY 0
%define PROT_READ 0x1
%define MAP_PRIVATE 0x2

section .data
fname: db 'test.txt', 0

section .text
global _start

; rdi に入っているアドレスの文字列を表示する
print_string:
    push rdi
    call string_length
    pop rsi
    mov rdx, rax
    mov rax, 1
    mov rdi, 1
    syscall
    ret

; rdi に入っている文字列の長さを計算する
; 結果は rax に入れて終了する
string_length:
    xor rax, rax
.loop:
    cmp byte [rdi+rax], 0
    je .end
    inc rax
    jmp .loop
.end:
    ret

_start:

; open の呼び出し. 詳細は後述
mov rax, 2
mov rdi, fname
mov rsi, O_READONLY
mov rdx, 0
syscall

; open によって rax に開いたファイルのファイルディスクリプタが入ってくるので保存しておく
mov r8, rax

; mmap の呼び出し. 詳細は後述
mov rax, 9
mov rdi, 0
mov rsi, 4096
mov rdx, PROT_READ
mov r10, MAP_PRIVATE
mov r9, 0
syscall

; mmap によって割り当てたメモリのアドレスが rax に入ってくるので rdi に保存し print_string で出力する
mov rdi, rax
call print_string

; exit
mov rax, 60
xor rdi, rdi
syscall

open システムコール

レジスタ 意味
rax 2 システムコール open の番号. 成功時にはファイルディスクリプタが入ってくる
rdi "test.text", 0 ファイル名の文字列. 終端を null にする
rsi 0 読み書き権限のフラグ. 0 で読み込みのみ
rdx 0 ファイル作成時のパーミッション. 読み込みのみなので 0 を指定

mmap システムコール

レジスタ 意味
rax 9 システムコール mmap の番号. 成功時には先頭アドレスが入ってくる
rdi 0 開くページのアドレス. 0 なら自動で割り当ててくれる.
rsi 4096 領域のサイズ
rdx 1 メモリの保護属性. 0 は読み出し専用
r10 2 利用属性(shared か private か anonymouse かなど)
r8 fd ファイルディスクリプタ
r9 0 ファイル内のオフセット
  • flags などの詳細は man mmap (2) を参照

マクロを使う

  • すでに値を定義するだけの %define マクロは使っているが、マクロで引数をとったり繰り返しをしたり条件分岐をする方法を学習する。
  • 以下ではマクロを使って "Hello, World" と "Hi, World" を繰り返し表示するだけのコードを実装する。
workdir/hello3_macro.asm
; %macro NAME ARGS_LANGTH から %endmacro までで引数を取るマクロを定義する
; NAME ARGS で定義後に呼び出すことができる
; 引数は %1, %2 でアクセスできる
; このマクロは文字列の先頭アドレスと文字列長を受け取って write するだけのもの
; @example: print_string message, 14
%macro print_string 2
mov rax, 1
mov rdi, 1
mov rsi, %1
mov rdx, %2
push rcx
syscall
pop rcx
%endmacro

section .data

hello: db 'Hello, World', 10
hi: db 'Hi, World', 10

section .text
global _start

_start:

; %assign A B で B の計算結果を A に入れる
%assign i 0

; %rep NUMBER から %endrep までで指定の回数だけループする
; %exitrep でループから脱出できる
%rep 10
    ; %if ... [%else ...] %endif で条件分岐できる
    %if i % 2 = 0
        print_string hi, 11
    %else
        print_string hello, 13
    %endif

    ; %assign 計算が使えるのでカウンタをインクリメントできる
    %assign i i + 1
%endrep

mov rax, 60
xor rdi, rdi
syscall

マクロを使って FizzBuzz 問題をもう一度

  • FizzBuzz 問題の関数をマクロに置き換えてもう一度解いてみる。
  • マクロにするとループや条件分岐がだいぶ普通の言語っぽく扱えて便利。
workdir/fizzbuzz_macro.asm
; 文字列を表示する
; Usage: print_string 文字列の先頭アドレス, 文字列の長さ
%macro print_string 2
mov rax, 1
mov rdi, 1
mov rsi, %1
mov rdx, %2
push rcx
syscall
pop rcx
%endmacro

; rax に入った数字を一文字表示する
%macro print_rax_number 0
lea rsi, [numbers + rax]
mov rax, 1
mov rdi, 1
mov rdx, 1
push rcx
syscall
pop rcx
%endmacro

; rcx に入った数字を10進数で指定した桁の一文字だけ表示する
; 実行後スタックの先頭に残りの桁の数字が入る
; Usage: print_number 表示する桁数
%macro print_number 1
mov rdx, 0
mov rax, rcx
mov rbx, %1
div rbx
push rdx
print_rax_number
%endmacro

; rcx に入った数字を10進数3桁で表示する
%macro print_number_100 0
print_number 100
pop rax
print_number 10
pop rax
print_rax_number
%endmacro

section .data
fizz: db "fizz"
buzz: db "buzz"
numbers: db "0123456789"
newline: db 10

section .text
global _start

_start:

%assign i 1

%rep 100

    %if i % 15 = 0

        print_string fizz, 4
        print_string buzz, 4
        print_string newline, 1

    %elif i % 3 = 0

        print_string fizz, 4
        print_string newline, 1

    %elif i % 5 = 0

        print_string buzz, 4
        print_string newline, 1

    %else

        mov rcx, i
        print_number_100
        print_string newline, 1

    %endif

    %assign i i + 1
%endrep

mov rax, 60
xor rdi, rdi
syscall

おわりに

  • 書籍「低レベルプログラミング」の1章から7章について、実際に動作するコードでアセンブラについて解説してもらうことで基本的な部分については理解ができるようになった。一方で解説がそこまで親切ではなかったり OS や CPU についての解説が自分にはまだよく理解できない部分があったので、別の本でもうちょっと勉強してからまた戻ってきたいと思った。
  • 以前 Nintendo Switch の「ヒューマン・リソース・マシーン」というゲームをやったが、これがほぼそのままアセンブラだったので「これゼミでやったやつだ!」みたいになった。Switch よりはエディタのほうが操作性がいいので意外と苦でもなく取り組めた。
70
50
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
70
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?