ゾンビ狩りクラブ

Linux, Server, Network, Security 関連などをゆるーくテキトーに載せてます

x64 アセンブリ 入門

Index

1. はじめに

本稿では、x64のアセンブリを解説し、"Hello"と出力するプログラムを書いてみる。
基本的なレジスタについては、以下を参照したい。
主なx64レジスタまとめ

また、スタックがわからない場合は、以下を参照したい。 そろそろバッファオーバーフローの原理をわかりやすく解説してみないか?

また、本項では、次の用語を以下と定義する。

用語 意味
アセンブリ アセンブリ言語のこと
アセンブル アセンブリを機械が実行できるマシン語に変換すること
アセンブラ アセンブルするためのソフトウェア

2. x64アセンブリ概要

2.1 記法

アセンブリの記法にはIntel記法とAT&T記法の二種類がある。

Intel記法:

mov eax, 2      ;eaxに2を代入  

AT&T記法:

mov $2, %eax  ;上記と同じ  

本稿ではIntel記法を使用する。

また、アセンブリにおけるコメントアウトは、";" である。

2.2 アセンブリ命令の構成

アセンブリ命令は、ニーモニックとオペランドの二つから構成されている。
ニーモニックは命令の種類であり、オペランドは命令の対象である。
例)

mov eax, 2

mov: ニーモニック
eax: 第一オペランド
2 : 第二オペランド

2.3 基本的な命令

基本的な命令は以下になる。

push

オペランドの値をスタックに入れ、rsp をインクリメントする。

push rax ;raxの値をスタックにプッシュする

pop

スタックの値をオペランドに格納し、rsp をデクリメントする。

pop rax ;raxにスタックの値を格納する

mov

第一オペランドに第二オペランドの値を格納する。

mov rax, 10 ;rax = 10

rax は 10 になる。

lea

第一オペランドに第二オペランドに格納されているアドレスを格納する。

例えば、rdi には、0x40100 というアドレスが入っているとする。

lea rax, [rdi]

rax には、0x40100 というアドレスが格納される。

これが lea ではなく mov の場合は、rax には、0x40100 のアドレスが指す値が格納される。

add

第一オペランドと第二オペランドの値を足して、第一オペランドに格納する。

add rax, 5 ;rax = rax + 5

最初 rax が 3 だとすれば、8 になる

sub

第一オペランドから第二オペランドの値を引いて、第一オペランドに格納する。

mul / imul / div / idiv

符号なし乗算を行う

オペランドと rax を掛けた値をオペランドに格納する。

mul rbx ;rbx * rax

imul

符号あり乗算を行う

オペランドと rax を掛けた値をオペランドに格納する。
または、第一オペランドと第二オペランドを掛けたものを第一オペランドに格納する。

imul rbx    ;rbx = rbx * rax
imul rbx, 2 ;rbx = rbx * rb2

div

符号なし除算を行う

rdx:rax の値をオペランドで割った値を、raxに、余りをrdxに格納する。

mov rax, 26
mov rbx, 7
mov rdx, 0
div rbx

上記を実行したら、raxに3が、rdxに5が格納される。

idiv

符号あり除算を行う div と同じ

inc

オペランドをインクリメントする

mov rax, 2
inc rax    ;rax = 3

dec

オペランドをデクリメントする

mov rax, 2
dec rax    ;rax = 1

and / or / xor

論理演算を行う。

xorは、レジスタの値をクリアするのによく使用される。

xor rax, rax ;rax = 0

test

第一オペランドと第二オペランドの論理積を取り、フラグに反映させる。

test rax, rax ;rax & rax

例えば、rax がゼロならば、ZF (ゼロフラグ) が真になる。

cmp

第一オペランドと第二オペランドを比較してフラグに反映させる。
内部的には、第一オペランドから第二オペランドを引く。  

cmp rax, rbx ;rax - rbx

例えば、rax の方が rbx より小さければ、SF (サインフラグ) が真になる。

call

戻りアドレスをスタックにプッシュし、rsp をインクリメントする。
そして、オペランドに格納されているアドレスにジャンプする。

call rax ;rax = 0x40010

leave

rbp を rsp にコピーして、rbp をポップし、rsp をデクリメントする。
以下2命令と同等

mov rsp, rbp
pop rbp

ret

スタックの先頭にあるアドレスを rip に設定し、rsp をインクリメントする。

nop

何もしない命令

2.4 バイトオーダー

データをメモリに格納する方法には、ビッグエンディアンとリトルエンディアンの二種類がある。 たとえば、 ABCD(0x41424344) という文字列を格納する場合を考える。 ビッグエンディアンでは、そのまま \x41\x42\x43\x44 と格納されるが、リトルエンディアンでは、 \x44\x43\x42\x41 と逆順で格納される。 x64ではリトルエンディアンが採用されているので、この方式に慣れる必要がある。

3. 実際にアセンブリを書いてみる

3.1 実行環境

% uname -a
Linux ubuntu 4.10.0-37-generic #41~16.04.1-Ubuntu SMP Fri Oct 6 22:42:59 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

% lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.3 LTS
Release:    16.04
Codename:   xenial

代表的なアセンブラには、gcc (as) と nasm がある。
gcc と nasm では、記述が多少異なるので、注意が必要である。
今回は両方使用してみる。

3.2 nasmで実行する

nasm を使用するには、まずインストールする必要がある。

sudo apt install nasm

今回は、"Hello" と出力するアレンブリコードを書く。
また、システムコールについては、以下の記事を参考にしたい。
Linux システムコールプログラミング 入門

今回は、出力するのに、write システムコール、正常終了するのに、exit システムコールを使用する。
システムコールの番号は、以下のファイル(環境によってファイルが異なる)に記述されている。

/usr/include/x86_64-linux-gnu/asm/unistd_64.h

write システムコールは、1
exit システムコールは、60
なので、それを使用する。

また、システムコールの引数は、第一引数から以下のレジスタに格納する必要がある。

rdi, rsi, rdx, rcx, r8, r9

引数が7つ以上ある場合には、7つめ以降の引数をスタックにプッシュする。

; test.s
section .data
msg db "Hello", 0x0a

section .text
global _start

_start:
  ; write(1, msg, 6)
  xor rax, rax
  mov rax, 1
  mov rdi, 1
  mov rsi, msg
  mov rdx, 6
  syscall

  ; exit(0)
  mov rax, 60
  mov rdi, 0
  syscall

上記のコードでは、まず data セクション(メモリ上でデータを格納する場所)を定義し、そこに "Hello" という文字と、改行コードである 0x0a を記述する。

その後、text セクション(メモリ上でコードを格納する場所)を定義し、そこにコードを記述する。

syscall 命令は、システムコールを実行する命令である。

これを実行してみる。
まずは、nasm コマンドで、test.s をアセンブルし、オブジェクトコード(test.o)を作成する。
次に、オブジェクトコードをリンクし、実行可能ファイル(./a.out)を作成し、実行する。

% nasm -f elf64 test.s
% ld test.o
% ./a.out             
Hello

"Hello"と表示された。

また、スタックを活用し、データセクションを使用しない方法もある。

section .text
global _start

_start:
  ; write(1, msg, 6)
  xor rax, rax
  mov rax, 1
  mov rdi, 1
  mov rbx, 0x0a6f6c6c6548
  push rbx
  mov rsi, rsp
  mov rdx, 6
  syscall

  ; exit(0)
  mov rax, 60
  mov rdi, 0
  syscall

上記コードでは、"Hello\n" という文字列をスタックにプッシュし、そのアドレスを rsi に格納している。
文字列はアスキーコードで格納する必要があるが、man ascii コマンドを使用すrば、文字とアスキーコードとの対応表が出力される。
また、上記で文字列を 0x48656c6c6f0a ではなく、逆の 0x0a6f6c6c6548 で格納しているのは、x64が 2.4 で述べたリトルエンディアン方式だからである。

% nasm -f elf64 test.s && ld test.o
% ./a.out
Hello

3.3 gccで実行する

gcc で intel 記法を使用するには、".intel_syntax noprefix" を記述する。

.intel_syntax noprefix
.globl _start

_start:
  xor rax, rax
  mov rax, 1
  mov rdi, 1
  mov rbx, 0x0a6f6c6c6548
  push rbx
  mov rsi, rsp
  mov rdx, 6
  syscall

  mov rax, 60
  syscall

実行する。

% as -o test.o test.s
% ld ./test.o
% ./a.out
Hello

または、以下でも実行することができる。

% gcc -nostdlib test.s
% ./a.out
Hello

書籍

アセンブリ言語スタートブック

アセンブリ言語スタートブック

アセンブリ言語の教科書

アセンブリ言語の教科書

熱血! アセンブラ入門

熱血! アセンブラ入門