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

Joseph Halfmoon

前回CALL/RET命令を練習。しかし即値つきRETはENTER/LEAVEと一緒ね、ということで後回し。今回はENTER/LEAVE命令(8086に不在、80186以降のx86が装備)が「想定」しているスタックフレームについて復習していきます。便利?「高水準言語とのセマンティンクギャップ」が叫ばれた当時のアダ花?

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

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

    •  Windows 11 PC (i5-1235U)
    •  Ubuntu 24.04 LTS on WSL2
    •  QEMU 8.2.2
    •  FreeDOS 1.3
誰が言った?

1970年代から1980年代初めくらいの頃だと思うのですが、コンピュータの機械語命令とコンパイラなどが扱う高水準言語との間の「セマンティンクギャップ」が叫ばれていた記憶。つまり「高級な」コンパイラの扱う言語のレベルの高さに対して、あまりにも機械語命令の「できること」がプリミティブすぎて大きなギャップがあり、その差を埋めねばいかん(それもハードウエア側から手を差し伸べて)という雰囲気じゃなかったかと思います。当時はそれが正義にみえていた?今にして思えば、いらんお世話だったような。知らんけど。

その頃開発されたx86もその雰囲気に当てられたのか、自ら一歩踏み出してしまったのか、やたらと「高水準な」命令を備えたい指向がありました。CISCだったしね(まだその当時はRISCなどという言葉は無かった筈。)

今回、復習するENTER、LEAVEなどという命令はコンパイラが生成するスタック・フレームの生成、消滅のかなりなお手間を自動化してくれるもの。インテルとしてはコンパイラのお供くらいな勢いだったかと。実際、使用しているコンパイラ様もあると思いますが、「余計なお世話だ」と考えられたコンパイラ作者も多かったとか。結局、その後のRISCの隆盛をみたら余計なお世話(コンパイラの自由度がなくなる)だったかもしれません。誰が言ったんだ「セマンティックギャップ」?

x86の想定するスタックフレーム

どのようなスタックフレームを想定するかはコンパイラ作者様の創造性が色濃く反映するとか、しないとか。そんなスタックフレームの構造をx86のENTER、LEAVE(そして8086から存在する RET imm)命令はインテル様のいう通りに制御しようとします。その路線に乗った方がお楽?それとも自由度なくて苦しい?

以下はプロシージャをCALLされた時点でのスタックの様子を図にしたものです。なおインターセグメントCALLであります。EnterLeaveRetImm

 

一番左が、CALLされたプロシージャの先頭で ENTER命令を実行したところです。

    • 緑色の部分はCALLする側のプログラムがスタック上に置いた引数ども
    • 紫色の部分がCALL命令がプッシュした戻り番地
    • 黄色の部分がENTER命令が作り出したスタックフレーム

x86は「汎用レジスタ」マシンという建前ではあるものの、いろいろレジスタの用途が決まっており、あまり多くの引数をレジスタ渡しにはできませぬ。そこで引数をスタック渡しすることが多いので、緑の部分はCALL命令発行前の呼び出し側がスタック上に置いた引数どもです。

上記を配置した後に、いざプロシージャ(サブルーチン、ファンクション。。。)側を呼び出すためにCALL命令を発行。するとCALL命令はスタック上に戻り番地をPUSHします。前回みたとおりイントラセグメントCALLであればIPだけですが、インターセグメントCALLであればCS:IPの2要素がPUSHされます。この状態で呼び出される側(Callee)に突入してきます(上の図の真ん中、After LEAVEと同じ状態です。)

ここで冒頭のENTER命令を実行。ENTER命令には2つの即値引数あり、一つがローカル変数のために確保する領域サイズ、もう一つが呼び出しのレベルです。

「呼び出しのレベル」、高級言語的には変数や関数の「スコープ」といっても良いかもしれまへん。親が子を呼び、さらに孫を呼びというような関係があるとき、子孫どもは親の持つ変数などにアクセスできる、といったアレです。それぞれの変数などは「ローカルなフレーム」に格納されており、そのフレーム位置が分からなければアクセスできませぬ。フレーム位置を格納しておくのにご指名のレジスタがBPであります。BP=ベース・ポインタっす。親のBPの値が分かればそこを基準に親の変数どもへもアクセスできると。

このような仕組みを顕現すべく、ENTER命令はレベルで指定された御先祖側のフレームポインタの値をコピーしてくれるのでした。ただし、そのためには呼び出しの度にBPの履歴を自動でスタック上に残す仕組みもまた必要であります。いたれり尽くせり?それともいらんお世話か。

さらにその下(スタックはアドレスの上位から下位に向かって伸びます)についでにそのレベルのローカル変数どもの領域を確保するっと。

こうしてENTER命令が準備してくれたスタックフレームの上でサブルーチンが走れるというわけです。

処理が終わったらRETする前に、LEAVE命令を実行します。するとあ~ら不思議。ENTER命令が実行される直前のBP、SPの値に復旧されます(もちろん復旧のタネは仕込んであったわけだが。)これが真ん中の状態。

ここでおもむろにRET命令(インターセグメントの)を発行すれば呼び出し側に制御が戻ります。このときRET命令でも即値をとる命令を用いれば呼び出す前にスタックに積んでおいた引数共の領域もついでに開放することができます。よかった(一番右の状態)

今回はスタックフレームの図を描くだけで疲れたので実習なし。アダ花、ENTER/LEAVEの演習はまた次回っと。ぐだぐだ文句言ってるのにやるの?

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

コメントを残す

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