アンティーク・アセンブラ~Antique Assembler

第6回(最終回) 関数の機能 ~ 関数間での連携

この記事を読むのに必要な時間:およそ 3 分

再帰呼び出し

関数呼び出しの例として,以下のような再帰呼び出しを実装してみましょう(この処理そのものには全く意味はありません⁠⁠。

リスト4 Cによる再帰呼び出しの例

int get_crossing_depth(int lower, int upper){
    if((upper -= 2) <= (lower += 3)){
        return 1;
    }
    else{
        return get_crossing_depth(lower, upper) + 1;
    }
}

....    depth = get_crossing_depth(1, 15);

上記のCプログラムをアセンブラで実現すると,以下のようになります。

リスト5 アセンブラによる再帰呼び出し実装

    .text
    .align  4

    .global get_crossing_depth
get_crossing_depth:
    enter   $8, $0         # 再帰呼び出し引数(4x2)の領域を
                           # スタック上に確保

    subl    $2, 12(%ebp)   # upper -= 2
    addl    $3, 8(%ebp)    # lower += 3

    movl    12(%ebp), %eax
    movl    8(%ebp), %edx
    cmpl    %eax, %edx     # upper と lower の比較
    jl      recursively

    # upper <= lower なので再帰呼び出し無し
    movl    $1, %eax       # 戻り値設定
    leave
    ret

recursively:
    # upper > lower なので再帰呼び出し有り
    movl    8(%ebp), %eax
    movl    %eax, 0(%esp)  # lower 引数格納
    movl    12(%ebp), %eax
    movl    %eax, 4(%esp)  # upper 引数格納

    call    get_crossing_depth

    addl    $1, %eax       # 戻り値設定
                           # 再帰呼び出しの戻り値に +1
    leave
    ret

    .global entry_point
entry_point:
    int3
    addl    $8, %esp       # 引数領域確保
    movl    $1, (%esp)     # lower 引数格納
    movl    $15, 4(%esp)   # upper 引数格納

    call    get_crossing_depth

    .global end_of_program
end_of_program:
    int3

再帰呼び出しを行う get_crossing_depth() では,再帰呼び出しのための引数格納領域として,冒頭のenter命令の時点で4バイト×2=8バイト分をスタック上に確保していますenterの詳細は前回の説明を参照⁠⁠。

また,呼び出し元のentry_point側でも,ESPレジスタを移動させることで呼び出し引数の格納領域を確保しています。

ESP/EBPと,それぞれの値が格納されている領域の相対位置さえ把握してしまえば,やっている処理は単純ですから,とくに難しいことは無いでしょう。

低レイヤから見た関数呼び出し

Application Programming Interface(API)は,当該プログラミング言語において特定の機能を呼び出す際の仕様であり,専らソースコード上での記述方式について定めたものを指します。

その一方で Application Binary Interface(ABI)は,アセンブラレベルでの実際の連携実現方式について定めたものを指します。⁠関数呼び出しの際の引数格納形式」のような"呼出規約(calling convention)もABIを構成する要素となります。

本稿ではこれまで,Intel x86アーキテクチャでの関数呼び出しはスタック経由で引数を受け渡しする,と説明してきました。

しかしその一方で,汎用レジスタを多数保持するSPARCプロセッサのようなCPUアーキテクチャの場合は,一定数以内の引数であればレジスタ経由で引数を受け渡すものとABIで定められています。

実のところ,⁠fastcall⁠形式と呼ばれる呼出規約の形式を指定することで,Intel x86アーキテクチャの場合でも,レジスタを利用した引数渡しを強制することが可能なのですが……。

以上のように,ABIはCPUアーキテクチャに依存するところが大きいのですが,必ずしもCPUアーキテクチャのみで決定されるわけではありません。

たとえば,先の再帰呼び出し実装の例では,とくに説明せずに,Cプログラムでの引数リスト上の左から右の順で,アドレス低位(ESPに対する加算分の少ない)側から格納しました。

これは,Intel x86アーキテクチャのCコンパイラで用いられる標準的な呼出規約(⁠⁠cdecl⁠形式)において,スタックへの引数格納仕様がそのように定められているためです。

先の再帰呼び出し実装の例からもわかるように,引数の個数を特定するための情報受け渡しが行われない⁠cdecl⁠形式では,引数格納領域の解放は呼び出しの責務となります。

引数の数が固定=呼び出し先で解放可能な関数の呼び出しであっても,領域解放責務は呼び出し元にありますので,結果として引数領域の解放処理が呼び出し元の数だけ散在することになりますが,裏を返せば,引数格納領域の管理が呼び出し元に一任されることになりますから,事前に必要なだけ確保した領域を再利用することもできれば,任意個の引数を渡すことも可能になります。

その一方で,⁠stdcall⁠形式と呼ばれる呼出規約では,引数格納領域の解放は呼び出しの責務とされます。

これは先ほどの "cdecl" の逆で,引数の数が固定である関数の呼び出しの場合は,引数領域の解放処理が呼び出し先に集約されるメリットと引き換えに,引数格納領域の管理自由度や,可変引数の使用が制限されることになります。

これらの呼出規約は,アセンブラソースを生成するプログラミング言語や,最終的な稼働環境のOSにおいて,機能と制約のバランスの落とし所をどこにするか,といった部分を考慮して決定されます。

すべてを自前の関数で構成するのであれば,既存の呼出規約を無視して,⁠MY呼出規約による連携」でも構いませんが,一般的なアセンブラの用途としては,性能/ハード依存要求の高い部分だけをアセンブラで実装し,それ以外はCなどを使用するパターンがほとんどでしょうから,呼出規約を含めたABIに対する配慮が重要になります。

おわりに

全6回(+号外に渡って,アセンブラプログラミングについて説明させていただいたわけですが,如何でしたでしょうか?

ソフトウェアの世界は,日々多機能化/高機能化と共に複雑化が進んでいますが,最終的にはアセンブラレベルでのデータ転送や制御遷移の組み合わせに過ぎません(量子コンピューティング等にパラダイムシフトでもすれば,違ってくるのでしょうが…⁠⁠。

アセンブラレベルからのボトムアップ的な視点を身につけることで,ソフトウェアへの理解はより深まることでしょう。

この連載が,そのような理解へのきっかけになれば幸いです。

著者プロフィール

藤原克則(ふじわらかつのり)

Mercurial三昧の日々が嵩じて, いつの間にやら『入門Mercurial Linux/Windows対応』を上梓。凝り性なのが災いして,年がら年中アップアップな一介の実装屋。最近は仕事の縁が元で,OpenSolarisに入れ込む毎日。