enter instruction
[tags: asm x86]

這個指令,總是能見到它,因為每個函數的開頭就是它。 格式為

enter p0, p1

所有人都知道參數 p1 為 0 時的情景

push %bp
mov %sp, %bp
sub $p0, %sp

很少有人關心 p1 的用法,也幾乎沒有書籍提到。偶爾有好奇的人提到它,可惜未見正確回答。

<AMD64 Architecture Programmer’s Manual, Volume 3: General-Purpose and System Instructions> 提到:

The second operand specifies the nesting level (0 to 31—the value is automatically masked to 5 bits). For nesting levels of 1 or greater, the processor copies earlier stack frame pointers before adjusting the stack pointer. This action provides a called procedure with access points to other nested stack frames.

讓人雲里霧裡的一段話。

根據後面給出的 pseudo-code,整理出一份更加簡潔的說明:

CPU 內部執行如下,用 pascal 和 AT&T 組合語言表示。 %temp_bp 為不可見暫存器, i 為立即值。至於 v 么。

case OPERAND_SIZE of
    16: v:= 2;
    32: v:= 4;
    64: v:= 8;

表示如下:

p1:= p1 and %00011111;
push %bp
mov %sp, %temp_bp
if p1 > 0 then begin
    if p1 > 1 then
        for i:= 1 to p1-1 do
            push %ss:(-i*v, %bp)
    push %temp_bp
end;
mov %temp_bp, %bp
sub $p0, %sp

以上就是完整的說明了。看完整理出的東西後的感覺——雲里霧裡的。於是昨天拿着 qemu 做了一天的各種用法嘗試,然後對結果進行分析。終於搞明白用法,原來它是用在嵌套函數(在函数内定义的函数)上的,而且各個層次的嵌套函數都得配合着使用這個指令,並不是哪一層的嵌套函數要獲得上幾層 stack frame 時可以單獨用。若是各層不配合着使用這個指令,那麼,得到的所謂各層 stack frame pointer 僅僅是 n-1 層的一些區域變數而已。分析過程實在冗長煩悶,略過。用法示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
procedure l0;
    var l0_lv0, l0_lv1: byte;
    procedure l1;
        var l1_lv0, l1_lv1: byte;
        procedure l2;
            var l2_lv0, l2_lv1: byte;
        begin
            // l2_lv0:= l0_lv0;
        end;
    begin
        l2;
    end;
begin
    l1;
end;

實際的組合語言,及堆疊,%bp,%sp 變化圖:

enter

看 enter 指令的 p1 有何規律么,先是 0 然後 1 然後 2。它的用法就是如此了,若根據自己的函數所在層次依次增加 enter 的參數 p1的話,所有上層的 fp 就會成為當前層的區域變數。由 [%bp - 1v] 得到 l0 的 fp, [%bp - 2v] 得到 l1 的 fp,等等。這樣一條指令就可以得到任意上層的 fp,然後可以對任意上層函數的堆疊進行存取了。十分的高效。

若是不使用 p1,那麼情況會如何。

當前的 fpc 編譯器,並沒用 enter 的 p1 進行優化。不註釋行 8, 反組譯如下:

8                    l2_lv0:= l0_lv0;
    0x080480e6 <+6>:    mov    0x8(%ebp),%eax
    0x080480e9 <+9>:    mov    0x8(%eax),%eax
    0x080480ec <+12>:    mov    -0x4(%eax),%eax
    0x080480ef <+15>:    mov    %eax,-0x4(%ebp)

因為 l2 和 l0 差兩級,所以它用了

    0x080480e6 <+6>:    mov    0x8(%ebp),%eax
    0x080480e9 <+9>:    mov    0x8(%eax),%eax

兩條指令來獲得 l0 的 fp,這倒還好,若是 l100 要訪問 l0 的堆疊,那麼就得用 100 條指令來獲得 l0 的 fp 了。而用上 enter p1 參數配合的話永遠只是 1 條指令獲得任意上層的 fp。平均效率比較好。