カニゲーム攻略日記ブログ

beatmaniaIIDXやハースストーンなどのゲーム攻略日記。主にまったり勢。2016年にIIDX皆伝になった

プログラムはなぜ動くのか  第10章 アセンブリ言語からプログラムの本当の姿を知る その1

以下のサイトを使ってC言語アセンブリ言語の関係を見てみる

https://gcc.godbolt.org/

途中で力尽きたので続きは後で
例題の難易度が高かった
もうちょい簡単な例題を選べば良かったと反省

目次

アセンブリ言語

アセンブリ言語はネイティブコードと1対1に対応

ニーモニック:略語

例えば

add:加算  
cmp:比較    

アセンブリ言語ニーモニックを使う言語

ネイティブコードにしないと実行できないので
アセンブラを使ってネイティブコードに変換する

graph LR
    アセンブリ言語のソースコード-->|アセンブル|ネイティブコード
    ネイティブコード-->|逆アセンブル|アセンブリ言語のソースコード

C言語ソースコードアセンブリ言語ソースコードの比較

int AddNum(int a, int b){
    return a + b;
}

void MyFunc(){
    int c;
    c = AddNum(123, 456);
}
AddNum:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        mov     edx, DWORD PTR [rbp-4]
        mov     eax, DWORD PTR [rbp-8]
        add     eax, edx
        pop     rbp
        ret
MyFunc:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     esi, 456
        mov     edi, 123
        call    AddNum
        mov     DWORD PTR [rbp-4], eax
        nop
        leave
        ret

解読の仕方

オペコード

アセンブリ言語の構文は
オペコード + オペランド

オペコード オペランド 機能
mov A, B AにBの値を格納する
add A, B Aの値とBの値を加算し、結果をAに格納する
sub A, B Aの値にBの値を減算し、結果をAに格納する
push A Aの値をスタックに格納する
pop A スタックから値を取り出してAに格納する
call A A関数を呼ぶ
ret なし 関数の呼び元に戻る

レジスタ

レジスタはCPUの中にある記憶領域

レジスタ 呼び名 主な役割
eax アキュムレータ 演算に使う
ebx ベースレジスタ モリーアドレスを格納する
ecx カウントレジスタ ループ回数をカウント
edx データレジスタ データを格納する
esi ソースインデックス データ転送元のメモリーアドレスを格納する
edi ディスティネーションインデックス データ転送先のメモリーアドレスを格納する
ebp ベースポインタ データの格納領域の基点のメモリーアドレスを格納する
esp スタックポインタ スタック領域の最上位に積まれたデータのメモリーアドレスを格納する

MyFuncの解読(途中まで)

  1. push rbp

    目的:関数が終了した後に元の関数に戻るために行う

    • push:rbpの値をスタックに格納
    • rbp :registrt base pointer
    スタック
    rbp
  2. mov rbp, rsp

    目的:関数の基準となるアドレスを作成

    • mov:rbpにrspの値を格納する
    • rbp:registrt base pointer
    • rsp:registrt stack pointer

    rbp = rsp

  3. sub rsp, 16

    目的:ローカル変数用にスタックを16バイト分確保

    • mov:rspの値に16の値を減算し、結果をrspに格納する
    • rsp:registrt stack pointer
    • 16 :16byte

    以下は、スタック上でローカル変数用に16バイトの領域を確保するイメージ

     スタックの状態(初期)
    
     | スタック |
     | -------- |
     | 空       |
     | 空       |
     | rsp      |
    
     スタックの状態(sub rsp, 16 を実行後)
    
     | スタック |
     | -------- |
     | ローカル変数  |
     | ローカル変数  |
     | ローカル変数  |
     | ローカル変数  |
     | 空       |
     | 空       |
     | rsp (更新) |
    

    スタック上で sub rsp, 16 を実行することで、rsp(スタックポインタ)が16バイト分減り、4つのローカル変数用のスペースが確保される

  4. mov esi, 456

    目的:AddNum関数の第2引数をレジスタに格納

    • mov:esiに456を格納する
    • esi:Extended Source Index Register
    • 456:値

    esi = 456

  5. mov edi, 123

    目的:AddNum関数の第1引数をレジスタに格納

    • mov:ediに123を格納する
    • edi:Extended Destination Index Register
    • 123:値

    edi = 123

  6. call AddNum

    目的:AddNum関数を呼び出す

    • call:関数を呼ぶ
    • addNum:関数名

    edi = 123

chatGPTの回答

参考

アセンブリ言語ソースコードの説明

AddNum:                                 ; AddNumという名前の関数を定義
        push    rbp                     ; 呼び出し元のrbpレジスタをスタックに保存
        mov     rbp, rsp                ; ベースポインタ(rbp)にスタックポインタ(rsp)を代入(新しいスタックフレームの作成)
        mov     DWORD PTR [rbp-4], edi  ; 関数の第1引数(ediレジスタ)をローカル変数(rbp-4)に格納
        mov     DWORD PTR [rbp-8], esi  ; 関数の第2引数(esiレジスタ)をローカル変数(rbp-8)に格納
        mov     edx, DWORD PTR [rbp-4]  ; ローカル変数(rbp-4)の値をedxレジスタに格納
        mov     eax, DWORD PTR [rbp-8]  ; ローカル変数(rbp-8)の値をeaxレジスタに格納
        add     eax, edx                ; eaxレジスタにedxレジスタの値を加算
        pop     rbp                     ; スタックからrbpレジスタに値を復元
        ret                             ; 関数を終了し、呼び出し元に戻る

MyFunc:                                 ; MyFuncという名前の関数を定義
        push    rbp                     ; 呼び出し元のrbpレジスタをスタックに保存
        mov     rbp, rsp                ; ベースポインタ(rbp)にスタックポインタ(rsp)を代入(新しいスタックフレームの作成)
        sub     rsp, 16                 ; ローカル変数用にスタックを16バイト分確保
        mov     esi, 456                ; esiレジスタに456を格納(AddNum関数の第2引数)
        mov     edi, 123                ; ediレジスタに123を格納(AddNum関数の第1引数)
        call    AddNum                  ; AddNum関数を呼び出す
        mov     DWORD PTR [rbp-4], eax  ; 関数からの戻り値(eaxレジスタ)をローカル変数(rbp-4)に格納
        nop                             ; 何もしない(no operation)命令
        leave                           ; スタックフレームを解放(rspにrbpを代入し、rbpにスタックから値を復元)
        ret                             ; 関数を終了し、呼び出し元に戻る

sub

オペコードsubは、"subtract"(減算)の略で、アセンブリ言語において2つのオペランドの差(減算)を計算する命令です。主な役割は、レジスタ間またはレジスタとメモリ間で減算を行い、結果を格納することです。

例えば、次のアセンブリ言語命令:

sub eax, ebx

この命令は、レジスタebxの値をレジスタeaxの値から引いて、その結果をレジスタeaxに格納します。

esi

"ESI"は拡張ソースインデックスレジスタ(Extended Source Index Register)の略です。これは、x86アーキテクチャのCPUに存在する一つのレジスタであり、主にデータの移動や配列のインデックス参照などに利用されます。

アセンブリ言語や低レベルプログラミングでは、CPUのレジスタは直接プログラミングや操作に使用されます。レジスタはCPUの一部であり、非常に高速なメモリ領域です。したがって、頻繁に使用されるデータや計算結果を一時的に格納したり、操作するために利用されます。

また、"ESI"レジスタは64ビットのx86_64アーキテクチャでは"RSI"と呼ばれます。このアーキテクチャでは、各レジスタは64ビットのデータを格納できます。

それぞれのレジスタには特定の用途があり、ESI(またはRSI)はしばしばソースデータのアドレスを格納するために使用されます。例えば、データのブロックをメモリの一部から別の部分にコピーする際などに使用されます。

edi

"EDI"は拡張デスティネーションインデックスレジスタ(Extended Destination Index Register)の略です。これはx86アーキテクチャのCPUに存在するレジスタの一つで、主にデータの移動や配列のインデックス参照などに利用されます。

アセンブリ言語や低レベルプログラミングでは、CPUのレジスタは直接プログラミングや操作に使用されます。レジスタはCPUの一部であり、非常に高速なメモリ領域です。したがって、頻繁に使用されるデータや計算結果を一時的に格納したり、操作するために利用されます。

また、"EDI"レジスタは64ビットのx86_64アーキテクチャでは"RDI"と呼ばれます。このアーキテクチャでは、各レジスタは64ビットのデータを格納できます。

それぞれのレジスタには特定の用途があり、EDI(またはRDI)はしばしばデスティネーションデータのアドレスを格納するために使用されます。例えば、データのブロックをメモリの一部から別の部分にコピーする際などに使用されます。

なぜesi,ediに引数を格納する?

ここでの説明は、x86_64アーキテクチャにおける関数呼び出しの規約を反映しています。

x86_64アーキテクチャでは、最初の6つの整数またはポインタ引数は、レジスタに格納されます。具体的には、次の順序でレジスタが使用されます:

  1. 第1引数: RDI
  2. 第2引数: RSI
  3. 第3引数: RDX
  4. 第4引数: RCX
  5. 第5引数: R8
  6. 第6引数: R9

つまり、あなたが見ているコードでは、AddNum関数に2つの引数があります。それらは規約に従って、最初の引数はRDIに、2番目の引数はRSIに格納されます。

このような規約を設ける理由は、関数呼び出しの際にどこに引数が格納されているかを明確にすることで、コンパイラ、リンカ、デバッガなどが正確に動作できるようにするためです。

レジスタに第2引数、第1引数の順番で格納している理由

特定のコード生成器(おそらくCコンパイラ)がそのように生成したからだと考えられます。
引数の格納順序に関しては特に規則はない