1
0

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 1 year has passed since last update.

初心者がIntel x64アセンブリに入門する その3

Last updated at Posted at 2022-12-19

はじめに

この記事は低レイヤーについてほとんど知識がない人間がアセンブリ言語を学んでいく際のメモ書きです。学習の際には「低レベルプログラミング」という本を参考書にしています。今回は前回の続きとして、より複雑なアセンブリプログラムを書いてみて、その挙動をgdbで確認するということをやってみようと思います。

目次

  • gdbによるデバッグ
    • 起動と設定
    • 実際にデバッグしてみる
      • ブレークポイント
      • レイアウトを変更する
      • ステップインする
      • メモリの内容を確認する
  • レジスタの中身を出力するプログラム
    • lea命令
    • sub命令・add命令
    • push命令・pop命令
    • レジスタの中身を出力するプログラム

gdbによるデバッグ

gdbはバイナリを逆アセンブリしたり、ブレークポイントを置いてデバッグすることができるツールである。

今後、アセンブリした結果の挙動確認やデバッグに使用する際に便利そうなので、簡単な操作だけ学んでみた。

起動と設定

起動は

gdb デバッグしたいバイナリファイル

で行える。

gdb

で起動して、後でバイナリファイルを指定することもできる。

起動するとこのような画面になる。

gdb_screen1.png

起動後、バイナリファイルを指定する場合は

file デバッグしたいバイナリファイル

とする。

デフォルトではアセンブリ言語の表示がAT&T表記となっているが、ここでは参考図書にあわせるためIntel表記で表示させるように設定する。

set disassembly-flavor intel

私は毎回設定するのが面倒なので、~/.gdbinitに上記の内容を保存している。

実際にデバッグしてみる

はじめに前回書いた以下のコードをgdbでデバッグしてみる。

使用するバイナリファイルは以下のアセンブリプログラムをアセンブリしたものである。

hello.asm
section .data
message: db 'hello, world', 10

section .text
global _start
_start:
    mov rax, 1       ; システムコールの番号をraxに入れる
    mov rdi, 1       ; 1は書き込み先(descriptor)
    mov rsi, message ; messageは文字の先頭
    mov rdx, 14      ; 14は書き込むバイト数
    syscall          ; システムコールの呼び出し

    mov rax, 60      ; 60は'exit'のsyscall番号
    xor rdi, rdi
    syscall

ブレークポイント

まずは、ブレークポイントを置いてみる。

ブレークポイントはラベルに対して置くことができるので、今回は

break _start

_startラベルにブレークポイントを置く。

すると

gdb_break_point.png

のような出力結果になる。

次にデバッグをスタートさせる。

start

すると以下のように聞かれるが、nと答える。

Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n

スタートすると以下のようにブレークポイントで止まる。

image.png

レイアウトを変更する

次に、レジスタの内容とアセンブリコードを表示させるためにレイアウトを変更する。

layout asm
layout regs

とすると、以下のような画面になる。

image.png

ステップインする

では、コードをステップインしてデバッグしてみる。

ステップインする方法は

si

でできる。

ステップインしていくと、レジスタの内容が随時更新されていって、実際にどのような値が入っていっているのかが確認できる。

image.png

メモリの内容を確認する

今回取り上げたhello, worldを出力するプログラムではwriteシステムコールを実行する際にレジスタrsiにhello, worldが格納されたmessageのアドレスが代入されている。

そこで、実際にrsiに格納されたアドレスのメモリにhello, worldという文字列が格納されているのか確認してみる。

メモリを確認するにはx /aというコマンドを用いる。また、文字列として表示させるのとhello, world\nの13字を表示させるために

x /13ac rsiに格納されたアドレス

を実行する。私の環境ではrsiに格納されたアドレスは0x402000であったので

x /13bc 0x402000

を実行した。これは1バイトごとに文字列として0x402000のアドレスに入っている内容を表示するコマンドである。

すると以下のように表示され、ちゃんとhello, worldが入っていることがわかる。

image.png

レジスタの中身を出力するプログラム

raxレジスタの値を16進数として出力するプログラムを作成する。

コードは以下の通りになる。

print_rax.asm
section .data
codes:
    db '0123456789ABCDEF'

section .text
global _start
_start:
    mov rax, 0x1122334455667788

    mov rdi, 1
    mov rdx, 1
    mov rcx, 64
    ; 4bitを16進数の1桁として出力していくために、
    ; シフト論理和(AND)によって1桁のデータを得る
    ; その結果は'codes'配列へのオフセットである。

.loop:
    push rax
    sub rcx, 4
    ; clはレジスタ(rcxの最下位byte)
    ; rax > eax > ax = (ah + al)
    ; rcx > ecx > cx = (ch + cl)
    sar rax, cl
    and rax, 0xf

    lea rsi, [codes + rax]
    mov rax, 1

    ; syscallでrcxとr11が変更される
    push rcx
    syscall
    pop rcx

    pop rax
    ; testは最速の'ゼロか?'チェックに使える
    ; マニュアルで'test'コマンドを参照
    test rcx, rcx
    jnz .loop

    ; exitシステムコール
    mov rax, 60
    xor rdi, rdi
    syscall

これを実行する前に、新しく出てきた命令としてsubsarleatestpoppush命令の挙動を確認する。

順番が前後するが、まずlea命令について確認してみる。

lea命令

lea命令はmov命令とadd命令を足し合わせた命令である。また、lea命令には第二引数に[]記号が使われている。lea命令の挙動と[]にどのような意味があるのかを確認するために以下のプログラムで実行してみる。

lea_vs_mov.asm
section .data
codes:
    db '0123456789ABCDEF'

section .text
global _start
_start:
    ; rsi <- ラベル'codes'のアドレス(数値)
    mov rsi, codes ; 1⃣

    ; rsi <- 'codes'というアドレスから始まるメモリの内容
    ; rsiは8バイト長なので連続する8バイトが取られる
    mov rsi, [codes] ; 2⃣

    ; rsi <- ラベル'codes'のアドレス
    lea rsi, [codes] ; 3⃣

    ; rsi <- (codes+rax)から始まるメモリの内容
    mov rax, 0x8
    mov rsi, [codes + rax] ; 4⃣

    ; rsi <- codes+rax
    ; これは
    ; mov rsi, codes
    ; add rsi, rax
    ; と同じ意味
    lea rsi, [codes + rax] ; 5⃣

    ; exitシステムコール
    mov rax, 60
    xor rdi, rdi
    syscall

はじめに1⃣の挙動をgdbを使用して確認してみる。

mov rsi, codes

をステップインしてみるとレジスタの状態は以下のようになる。

┌─Register group: general────────────────────────────────────────────────────────────────────┐
│rax            0x0                 0                                                        │
│rbx            0x0                 0                                                        │
│rcx            0x0                 0                                                        │
│rdx            0x0                 0                                                        │
│rsi            0x402000            4202496                                                  │
│rdi            0x0                 0                                                        │
│rbp            0x0                 0x0                                                      │
│rsp            0x7fffffffd790      0x7fffffffd790                                           │
│r8             0x0                 0                                                        │
│r9             0x0                 0                                                        │
│r10            0x0                 0                                                        │

rsiを見ると0x402000が入っていることがわかる。

これは私の環境では0x402000codesの内容である0123456789ABCDEFが入っているからである。

試しに

x /16cb &codes

を実行すると

(gdb) x /16cb &codes
0x402000:       48 '0'  49 '1'  50 '2'  51 '3'  52 '4'  53 '5'  54 '6'  55 '7'
0x402008:       56 '8'  57 '9'  65 'A'  66 'B'  67 'C'  68 'D'  69 'E'  70 'F'

となっていて、codes402000402008にはいっていることがわかる。

次に更にステップインして、2⃣

mov rsi, [codes]

を実行してみる。

するとレジスタは

┌─Register group: general────────────────────────────────────────────────────────────────────┐
│rax            0x0                 0                                                        │
│rbx            0x0                 0                                                        │
│rcx            0x0                 0                                                        │
│rdx            0x0                 0                                                        │
│rsi            0x3736353433323130  3978425819141910832                                      │
│rdi            0x0                 0                                                        │
│rbp            0x0                 0x0                                                      │
│rsp            0x7fffffffd790      0x7fffffffd790                                           │
│r8             0x0                 0                                                        │
│r9             0x0                 0                                                        │
│r10            0x0                 0                                                        │

のようになる。

この0x3736353433323130codesの中身そのものである。

これを確認するために

x /xg &codes

を実行してcodesの中身を8バイト16進数でみると

(gdb) x /xg &codes
0x402000:       0x3736353433323130

となっていて、rsiにはcodesの値が格納されていることがわかる。1つのレジスタは8バイト長なので、8バイト分のデータが入っている。

このようにmov命令の際にシンボルに[]をつけると、そのラベルの中身を扱うということがわかった。

対して、lea命令での[]記号はどのような意味があるのか見てみる。

3⃣をステップインすると、レジスタは

┌─Register group: general────────────────────────────────────────────────────────────────────┐
│rax            0x0                 0                                                        │
│rbx            0x0                 0                                                        │
│rcx            0x0                 0                                                        │
│rdx            0x0                 0                                                        │
│rsi            0x402000            4202496                                                  │
│rdi            0x0                 0                                                        │
│rbp            0x0                 0x0                                                      │
│rsp            0x7fffffffd790      0x7fffffffd790                                           │
│r8             0x0                 0                                                        │
│r9             0x0                 0                                                        │
│r10            0x0                 0                                                        │
│r11            0x0                 0                                                        │
│r12            0x0                 0                                                        │
│r13            0x0                 0                                                        │
│r14            0x0                 0                                                        │

となった。これは1⃣のときの状態と同じである。

つまり、

mov rsi, codes

lea rsi, [codes]

は同じ挙動となり、[]にはmov命令のときにはあったラベルの中身の値を参照するという意味はなくなっている。

次に4⃣をステップインしてみる。

するとレジスタの状態は

┌─Register group: general────────────────────────────────────────────────────────────────────┐
│rax            0x8                 8                                                        │
│rbx            0x0                 0                                                        │
│rcx            0x0                 0                                                        │
│rdx            0x0                 0                                                        │
│rsi            0x4645444342413938  5063528411713059128                                      │
│rdi            0x0                 0                                                        │
│rbp            0x0                 0x0                                                      │
│rsp            0x7fffffffd790      0x7fffffffd790                                           │
│r8             0x0                 0                                                        │
│r9             0x0                 0                                                        │
│r10            0x0                 0                                                        │
│r11            0x0                 0                                                        │
│r12            0x0                 0                                                        │
│r13            0x0                 0                                                        │
│r14            0x0                 0                                                        │

となっていて、rsiには0x4645444342413938が入っていることがわかる。

この0x46454443424139380x402008のアドレスに入っている数値、すなわち89ABCDEFである。

gdbで

x /xg 0x402008

を実行すると

(gdb) x /xg 0x402008
0x402008:       0x4645444342413938
x /8bc 0x402008

を実行すると

(gdb) x /8bc 0x402008
0x402008:       56 '8'  57 '9'  65 'A'  66 'B'  67 'C'  68 'D'  69 'E'  70 'F'

という結果が得られる。

最後に5⃣をステップインしてみると、レジスタの状態は

┌─Register group: general───────────────────────────────────────────────────────────┐
│rax            0x8                 8                                               │
│rbx            0x0                 0                                               │
│rcx            0x0                 0                                               │
│rdx            0x0                 0                                               │
│rsi            0x402008            4202504                                         │
│rdi            0x0                 0                                               │
│rbp            0x0                 0x0                                             │
│rsp            0x7fffffffe1f0      0x7fffffffe1f0                                  │
│r8             0x0                 0                                               │
│r9             0x0                 0                                               │

となる。

レジスタrsiには0x402008が入っており、codesのアドレスにraxに入っている値0x08が加算された値が格納されていることがわかる。

これは

mov rsi, codes
add rsi, rax

に等しい。

sub命令・add命令

addsub命令はそれぞれ加算と減算命令である。

サンプルとして以下のプログラムをデバッグしてみる。

add_sub.asm
section .text
global _start
_start:
    mov rsi, 0x5
    add rsi, 0x2
    sub rsi, 0x4

    mov rax, 60
    xor rdi, rdi
    syscall

gdbを用いてレジスタの様子を見ていくと

mov rsi, 0x5

ではもちろん以下のようにrsiに5という値が入る。

┌─Register group: general───────────────────────────────────────────────────────────┐
│rax            0x0                 0                                               │
│rbx            0x0                 0                                               │
│rcx            0x0                 0                                               │
│rdx            0x0                 0                                               │
│rsi            0x5                 5                                               │
│rdi            0x0                 0                                               │
│rbp            0x0                 0x0                                             │
│rsp            0x7fffffffe1f0      0x7fffffffe1f0                                  │
│r8             0x0                 0                                               │
│r9             0x0                 0                                               │

そして

add rsi, 0x2

をステップインするとレジスタは以下のようになり、rsiに2が加算されていることがわかる。

┌─Register group: general───────────────────────────────────────────────────────────┐
│rax            0x0                 0                                               │
│rbx            0x0                 0                                               │
│rcx            0x0                 0                                               │
│rdx            0x0                 0                                               │
│rsi            0x7                 7                                               │
│rdi            0x0                 0                                               │
│rbp            0x0                 0x0                                             │
│rsp            0x7fffffffe1f0      0x7fffffffe1f0                                  │
│r8             0x0                 0                                               │
│r9             0x0                 0                                               │

同じように、

sub rsi, 0x4

をステップインするとレジスタは以下のようになり、4が減算されていることがわかる。


┌─Register group: general───────────────────────────────────────────────────────────┐
│rax            0x0                 0                                               │
│rbx            0x0                 0                                               │
│rcx            0x0                 0                                               │
│rdx            0x0                 0                                               │
│rsi            0x3                 3                                               │
│rdi            0x0                 0                                               │
│rbp            0x0                 0x0                                             │
│rsp            0x7fffffffe1f0      0x7fffffffe1f0                                  │
│r8             0x0                 0                                               │
│r9             0x0                 0                                               │

sar命令, shr命令

sar命令とshr命令はどちらも指定されたbitだけ右シフトする命令である。

違いとしては

  • sar命令:算術右シフト
  • shr命令:論理右シフト

という、符号の考慮の有無がある。

具体的には

  • sar命令:指定されたbitだけ右シフトし、最上位ビットが1であるときシフトした分だけ左を1で埋める
  • shr命令:指定されたbitだけ右シフトし、シフトした分だけ左を0で埋める

となる。

これを確かめるために、以下のプログラムを実行してみる。

sar_shr.asm
section .text
global _start
_start:
    mov rax, 0x8000000012345678
    sar rax, 4
    mov rax, 0x8000000012345678
    shr rax, 4

    mov rax, 60
    xor rdi, rdi
    syscall
sar rax, 4

をステップインするとrax0x8000000012345678から0xf800000001234567になる。

これはもとの値を右に4bitシフトすると右端の8がなくなり0x800000001234567になるわけだが、もとの値の最上位ビットが1(最上位バイトが8つまり0b1000)であるので、左端をシフトした4bit分1で埋める(0xfが左につく)。

対して

shr rax, 4

をステップインすると、rax0x8000000012345678から0xf800000001234567になるだけである。

もとの値の最上位ビットが0のとき、(例えば上記プログラムでmov rax, 0x00000000012345678としたとき)shrsarの挙動は同じなる。

push命令, pop命令

pushpop命令は文字通りスタックにpushとpopを行う命令である。

一応、挙動を確認するために以下のプログラムを実行してみる。

push_pop.asm
section .text
global _start
_start:
    mov rax, 0xffff0000ffff0000
    push rax
    pop rdi

    mov rax, 60
    xor rdi, rdi
    syscall

これらをステップインして実行していってみると

push rax

を行った際には、レジスタの様子は以下のようになる。

┌─Register group: general───────────────────────────────────────────────────────────────────────────────────┐
│rax            0xffff0000ffff0000  -281470681808896  rbx            0x0                 0                  │
│rcx            0x0                 0                 rdx            0x0                 0                  │
│rsi            0x0                 0                 rdi            0x0                 0                  │
│rbp            0x0                 0x0               rsp            0x7fffffffe1d8      0x7fffffffe1d8     │
│r8             0x0                 0                 r9             0x0                 0                  │

rspはスタックポインタで、raxの内容を格納したメモリのアドレスが入っている。

(gdb) x /2x $rsp

rspが指すメモリの中身を見てみると

0x7fffffffe1d8: 0xffff0000      0xffff0000

となってちゃんと格納されていることが確認できる。

pop rdi

をステップインするとレジスタは以下のようになり

┌─Register group: general───────────────────────────────────────────────────────────────────────────────────┐
│rax            0xffff0000ffff0000  -281470681808896  rbx            0x0                 0                  │
│rcx            0x0                 0                 rdx            0x0                 0                  │
│rsi            0x0                 0                 rdi            0xffff0000ffff0000  -281470681808896   │
│rbp            0x0                 0x0               rsp            0x7fffffffe1e0      0x7fffffffe1e0     │
│r8             0x0                 0                 r9             0x0                 0                  │

ちゃんとraxに入っていた値がrdiに格納され、rspが指すアドレスも8byte少なくなっている事がわかる。

レジスタの中身を出力するプログラム

最後に最初に述べていた以下のプログラムを実行してみる。

print_rax.asm
section .data
codes:
    db '0123456789ABCDEF'

section .text
global _start
_start:
    mov rax, 0x1122334455667788

    mov rdi, 1
    mov rdx, 1
    mov rcx, 64
    ; 4bitを16進数の1桁として出力していくために、
    ; シフト論理和(AND)によって1桁のデータを得る
    ; その結果は'codes'配列へのオフセットである。

.loop:
    push rax
    sub rcx, 4
    ; clはレジスタ(rcxの最下位byte)
    ; rax > eax > ax = (ah + al)
    ; rcx > ecx > cx = (ch + cl)
    sar rax, cl
    and rax, 0xf

    lea rsi, [codes + rax]
    mov rax, 1

    ; syscallでrcxとr11が変更される
    push rcx
    syscall
    pop rcx

    pop rax
    ; testは最速の'ゼロか?'チェックに使える
    ; マニュアルで'test'コマンドを参照
    test rcx, rcx
    jnz .loop

    ; exitシステムコール
    mov rax, 60
    xor rdi, rdi
    syscall

実行結果は

1122334455667788

となり、raxに入っている値が出力されていることがわかる。

まず、

mov rdi, 1
mov rdx, 1
mov rcx, 64

について

rdirdxはそれぞれwriteシステムコールを行う際のディスクリプタ(値は1であるので標準出力)と出力するデータの長さ(1文字)である。

そしてrcxは出力したいraxを右シフトしてマスキングするために使用する。例えばrcxが40のとき、64(raxのビット長)に対して40bit右シフトして、残りのの24bitが出力されるというような使い方である。

続いて

push rax
sub rcx, 4
sar rax, cl
and rax, 0xf

という部分について

push raxraxの初期値を毎ループで使用するためにスタックにプッシュしている。こうすることでraxが右シフトされても、また初期値を利用したいときにポップすることでraxの値を初期値にすることができる。

sub rcx, 4というのはrcxをループごとに4ディグリーズさせる。rcxの値の遷移は60, 56, 52, 48, ...となっていく。rcxraxの特定の部分を出力する際のマスキングに使用するので、ループごとにマスキングされる部分が4bitずつ増えるイメージである。

そしてsar rax, clで実際に右シフトしてraxをマスキングしている。clrcxの下位8bitである(ループで使用されるrcxの値は0~60なので8bitで間に合う)。

最後にand rax, 0xfという部分はraxの下位4bitを抽出している。0xf0b0000000000001111なのでandを取ると下位4bitを取ってこられる。

続いて

lea rsi, [codes + rax]

という部分について、raxにはrcxで指定した部分(下位{64-rcx}bitのうち下位4bit)が入っている訳だが、.dataセクションで定義したcodesを用いることで、その4bit分の値をcharに変換している。
例えば4bitの値が7である場合、[codes+7]codesの8文字目が参照され、char型の7がrsiに格納される。

続いて

mov rax, 1
push rcx
syscall
pop rcx

という部分では

mov rax, 1writeシステムコール番号を指定して、syscallwriteシステムコールを実行している。

ここで、writeシステムコールは実行される際にrcxを利用するため、push rcxpop rcxrcxの値をスタックに退避させている。

最後に

pop rax
test rcx, rcx
jnz .loop

の部分では

pop raxraxを初期値に戻し、test rcx, rcx及びjnz .looprcxが0になっていなければ.loopに戻るということを行っている。

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?