ぐだぐだ低レベルプログラミング(198)x86(16bit)、CALLとRET

Joseph Halfmoon

前回はカウンタレジスタCXを「見て飛ぶ」LOOPとJCXZでした。今回は「呼び出したら戻る」CALLとRET命令です。例によってイントラ・セグメントとインター・セグメントの違いあり。CALLの場合はダイレクトとインダイレクトの違いもあり。いろいろあるけれど、CALL、RETは意外とフツー。

※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら

※実機動作確認(といってもエミュレータなんだけれども)には以下を使用させていただいております。

    •  Windows 11 PC (i5-1235U)
    •  Ubuntu 24.04 LTS on WSL2
    •  QEMU 8.2.2
    •  FreeDOS 1.3
CALL、RET

今回練習してみるCALL、RET命令は以下に示した部分オペコードマップの赤枠の部分の命令どもです。RETはともかく、CALLは結構、トビトビ。あまり優遇されていない感じっす。callRetOPmap

赤枠でなく、青枠つけてある命令、RETでも即値の引数をとる命令があります。こいつらは後の回でEnter、Leave命令(どちらも8086/8088には存在せず、80186以降のx86で登場する命令)とともに練習する予定です。

CALL命令に関しては以下のような区分があります。

    • CALLイントラ・セグメント
      • ダイレクト
      • インダイレクト
    • CALLインター・セグメント
      • ダイレクト
      • インダイレクト

ニアCALL(イントラ・セグメント)はセグメント内のコールなので、古いIP(インストラクション・ポインタ、操作の時点ではCALL命令の次の命令を指している)をスタックにPUSHして、新たなIPに書き換えることで分岐を実現します。そして対応するRETは、スタックからPOPした値をIPに書き戻すことで、CALL命令の次の命令に「戻り」ます。

ファーCALL(インター・セグメント)はセグメント間のコールです。古いIP(インストラクション・ポインタ、操作の時点ではCALL命令の次の命令を指している)をスタックにPUSHするだけでなく、CS(コード・セグメント)の値もスタックにPUSHします。そして、新たなCS:IPに書き換えることで分岐を実現します。そして対応するRETは、スタックから2回POPした値をそれぞれCSとIPに書き戻すことで、CALL命令の次の命令に「戻り」ます。

RET命令についてはスタックからの取り出し操作なのでニアとファーの区別のみです。ただし、即値を引数にとるタイプととらないタイプがあります。即値はPOPしたSPの値の「調整」に使うのですが、スタックフレームの説明をしないとならないので、即値とるタイプは前述のとおりまた今度です。

今回練習のアセンブリ言語ソース

強力なx86用アセンブラNASM用のソースです。ニアCALLしてRET、ファーCALLしてRETするだけの簡単な練習です。

segment code

..start:
    mov ax, data
    mov ds, ax
    mov ax, stack
    mov ss, ax
    mov sp, stacktop
test:
    mov ax, 1
    call ntarget
    nop
    call far ftarget
    nop
    jmp fin

ntarget:
    mov ax, 2
    ret

fin:
    mov ax, 0x4c00
    int 0x21
    resb    2048

segment data    align=16
    resb    1024 * 63

segment code2   align=16
    resb    1024 * 63
ftarget:    
    mov ax, 3
    retf

segment stack   class=STACK
    resb    2048
stacktop:
アセンブルして実行

MS-DOS互換で機能強化されてるフリーなFreeDOS上、以下のステップで上記のアセンブラソース jcallret.asm から実行可能なオブジェクトファイルを生成して実行することができます(nasmとwatcom Cがインストール済であること。)

nasm -f obj callret.asm
wlink name callret.exe format dos file callret.obj
debug callret.exe

まずはニアCALL(ダイレクト)の例から。赤枠が最初のニアCALLの関連部分です。ニアCALL前のSPの値が、CALL後にはー2されていることが分かります。またIPの値が書き替えられていることも分かります。nearCall

上記の緑枠はRET(ニア)の前後の様子です。RET前のSPが後には+2され、IPはニアCALL命令(3バイト命令)の次の命令に戻されています。

その次はファーCALL(ダイレクト)とファーRETです。上記と同様ですが、書き替える対象がIPだけでなく、CS、IPの2つになるので、SPの値はCALLで4減、RETで4増です。farCall

なお、ただRETと書くとアセンブラはニアだかファーだか分からないのでニアにしてしまいます。忘れずに RETF (Fはファーだと思う)と書きます。インテル公式のアセンブラ書式であるとCALL先のプロシージャにFARかNEARかの属性を持たせているので、その戻りのRETは自動的に判別されてコード生成される決まりだった筈。RETFと書かずに済む代わり「荘厳な」疑似命令でトビ先を修飾する必要があります。

CALL、RETは意外とシンプルだったぞなもし。

ぐだぐだ低レベルプログラミング(197)x86(16bit)、LOOPとJCXZ へ戻る

ぐだぐだ低レベルプログラミング(199)x86(16bit)、ENTER、LEAVEその1 へ進む

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です