はじめに
本稿では、x64のアセンブリを解説し、"Hello"と出力するプログラムを書いてみる。
基本的なレジスタについては、以下を参照したい。
主なx64レジスタをまとめてみた
また、スタックがわからない場合は、以下を参照したい。
バッファオーバーフローとは?原理をわかりやすく説明!
また、本項では、次の用語を以下と定義する。
用語 | 意味 |
---|---|
アセンブリ | アセンブリ言語のこと |
アセンブル | アセンブリを機械が実行できるマシン語に変換すること |
アセンブラ | アセンブルするためのソフトウェア |
x64アセンブリ概要
記法
アセンブリの記法にはIntel記法とAT&T記法の二種類がある。
Intel記法:
mov eax, 2 ;eaxに2を代入
AT&T記法:
mov $2, %eax ;上記と同じ
本稿ではIntel記法を使用する。
また、アセンブリにおけるコメントアウトは、";" である。
アセンブリ命令の構成
アセンブリ命令は、ニーモニックとオペランドの二つから構成されている。
ニーモニックは命令の種類であり、オペランドは命令の対象である。
例)
mov eax, 2
mov: ニーモニック
eax: 第一オペランド
2 : 第二オペランド
基本的な命令
基本的な命令は以下になる。
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
何もしない命令
バイトオーダー
データをメモリに格納する方法には、ビッグエンディアンとリトルエンディアンの二種類がある。
たとえば、 ABCD(0x41424344) という文字列を格納する場合を考える。
ビッグエンディアンでは、そのまま \x41\x42\x43\x44 と格納されるが、リトルエンディアンでは、 \x44\x43\x42\x41 と逆順で格納される。
x64ではリトルエンディアンが採用されているので、この方式に慣れる必要がある。
実際にアセンブリを書いてみる
実行環境
% 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 では、記述が多少異なるので、注意が必要である。
今回は両方使用してみる。
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
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
0件のコメント