前回はカウンタレジスタCXを「見て飛ぶ」LOOPとJCXZでした。今回は「呼び出したら戻る」CALLとRET命令です。例によってイントラ・セグメントとインター・セグメントの違いあり。CALLの場合はダイレクトとインダイレクトの違いもあり。いろいろあるけれど、CALL、RETは意外とフツー。
※「ぐだぐだ低レベル プログラミング」投稿順indexはこちら
※実機動作確認(といってもエミュレータなんだけれども)には以下を使用させていただいております。
CALL、RET
今回練習してみるCALL、RET命令は以下に示した部分オペコードマップの赤枠の部分の命令どもです。RETはともかく、CALLは結構、トビトビ。あまり優遇されていない感じっす。
赤枠でなく、青枠つけてある命令、RETでも即値の引数をとる命令があります。こいつらは後の回でEnter、Leave命令(どちらも8086/8088には存在せず、80186以降のx86で登場する命令)とともに練習する予定です。
CALL命令に関しては以下のような区分があります。
-
- 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の値が書き替えられていることも分かります。
上記の緑枠はRET(ニア)の前後の様子です。RET前のSPが後には+2され、IPはニアCALL命令(3バイト命令)の次の命令に戻されています。
その次はファーCALL(ダイレクト)とファーRETです。上記と同様ですが、書き替える対象がIPだけでなく、CS、IPの2つになるので、SPの値はCALLで4減、RETで4増です。
なお、ただRETと書くとアセンブラはニアだかファーだか分からないのでニアにしてしまいます。忘れずに RETF (Fはファーだと思う)と書きます。インテル公式のアセンブラ書式であるとCALL先のプロシージャにFARかNEARかの属性を持たせているので、その戻りのRETは自動的に判別されてコード生成される決まりだった筈。RETFと書かずに済む代わり「荘厳な」疑似命令でトビ先を修飾する必要があります。
CALL、RETは意外とシンプルだったぞなもし。