任何语言/编译器都使用非零嵌套级别的x86 ENTER指令吗?

时间:2014-10-12 08:20:46

标签: assembly x86

那些熟悉x86汇编编程的人习惯于典型的函数序言/结语:

push ebp ; Save old frame pointer.
mov  ebp, esp ; Point frame pointer to top-of-stack.
sub  esp, [size of local variables]
...
mov  esp, ebp ; Restore frame pointer and remove stack space for locals.
pop  ebp
ret

使用ENTERLEAVE指令也可以实现相同的代码序列:

enter [size of local variables], 0
...
leave
ret

ENTER指令的第二个操作数是嵌套级别,它允许从被调用函数访问多个父帧。

这在C中没有使用,因为没有嵌套函数;局部变量只有它们声明的函数的范围。这个构造不存在(虽然有时我希望它):

void func_a(void)
{
    int a1 = 7;

    void func_b(void)
    {
        printf("a1 = %d\n", a1);  /* a1 inherited from func_a() */
    }

    func_b();
}

Python然而 具有嵌套函数,其行为方式如下:

def func_a():
    a1 = 7
    def func_b():
        print 'a1 = %d' % a1      # a1 inherited from func_a()
    func_b()

当然Python代码没有直接翻译成x86机器代码,因此无法(不太可能?)利用这条指令。

是否有任何语言可以编译为x86并提供嵌套函数?是否有编译器会发出带有非零第二个操作数的ENTER指令?

英特尔在该嵌套级操作数上投入了非零的时间/金钱,基本上我只是好奇,如果有人使用它: - )

参考文献:

4 个答案:

答案 0 :(得分:38)

enter在实践中被避免,因为它的表现非常糟糕 - 请参阅"enter" vs "push ebp; mov ebp, esp; sub esp, imm" and "leave" vs "mov esp, ebp; pop ebp"的答案。有一堆x86指令已过时但仍然支持其向后兼容性原因 - enter就是其中之一。 (leave是可以的,并且编译器很乐意发出它。)

在Python中完全通用地实现嵌套函数实际上是一个比简单地选择几个框架管理指令更有趣的问题 - 搜索'闭包转换'和'向上/向下funarg问题'并且你会发现许多有趣的讨论。

请注意,x86最初设计为Pascal机器,这就是为什么有指令支持嵌套函数(enterleave),pascal调用约定,其中callee从堆栈中弹出已知数量的参数(ret K),边界检查(bound),依此类推。其中许多操作现已过时。

答案 1 :(得分:10)

作为Iwillnotexist Idonotexist pointed out,GCC 支持C中的嵌套函数,使用上面显示的确切语法。

但是,它不使用ENTER指令。相反,嵌套函数中使用的变量在局部变量区域中组合在一起,并且指向该组的指针将传递给嵌套函数。有趣的是,这个指向父变量的指针"通过非标准机制传递:在x64上传递它在r10中,在x86(cdecl)上传递它在ecx中传递,它是为C ++中的this指针保留的(不管怎样都不支持嵌套函数。

#include <stdio.h>
void func_a(void)
{
    int a1 = 0x1001;
    int a2=2, a3=3, a4=4;
    int a5 = 0x1005;

    void func_b(int p1, int p2)
    {
        /* Use variables from func_a() */
        printf("a1=%d a5=%d\n", a1, a5);
    }
    func_b(1, 2);
}

int main(void)
{
    func_a();
    return 0;
}

在为64位编译时生成以下(片段代码):

00000000004004dc <func_b.2172>:
  4004dc:   push   rbp
  4004dd:   mov    rbp,rsp
  4004e0:   sub    rsp,0x10
  4004e4:   mov    DWORD PTR [rbp-0x4],edi
  4004e7:   mov    DWORD PTR [rbp-0x8],esi
  4004ea:   mov    rax,r10                    ; ptr to calling function "shared" vars
  4004ed:   mov    ecx,DWORD PTR [rax+0x4]
  4004f0:   mov    eax,DWORD PTR [rax]
  4004f2:   mov    edx,eax
  4004f4:   mov    esi,ecx
  4004f6:   mov    edi,0x400610
  4004fb:   mov    eax,0x0
  400500:   call   4003b0 <printf@plt>
  400505:   leave  
  400506:   ret    

0000000000400507 <func_a>:
  400507:   push   rbp
  400508:   mov    rbp,rsp
  40050b:   sub    rsp,0x20
  40050f:   mov    DWORD PTR [rbp-0x1c],0x1001
  400516:   mov    DWORD PTR [rbp-0x4],0x2
  40051d:   mov    DWORD PTR [rbp-0x8],0x3
  400524:   mov    DWORD PTR [rbp-0xc],0x4
  40052b:   mov    DWORD PTR [rbp-0x20],0x1005
  400532:   lea    rax,[rbp-0x20]              ; Pass a, b to the nested function
  400536:   mov    r10,rax                     ; in r10 !
  400539:   mov    esi,0x2
  40053e:   mov    edi,0x1
  400543:   call   4004dc <func_b.2172>
  400548:   leave  
  400549:   ret  

objdump --no-show-raw-insn -d -Mintel

的输出

这相当于像这样更冗长的东西:

struct func_a_ctx
{
    int a1, a5;
};

void func_b(struct func_a_ctx *ctx, int p1, int p2)
{
    /* Use variables from func_a() */
    printf("a1=%d a5=%d\n", ctx->a1, ctx->a5);
}

void func_a(void)
{
    int a2=2, a3=3, a4=4;
    struct func_a_ctx ctx = {
        .a1 = 0x1001,
        .a5 = 0x1005,
    };

    func_b(&ctx, 1, 2);
}

答案 2 :(得分:5)

我们的PARLANSE编译器(用于SMP x86上的细粒度并行程序)具有词法作用域。

PARLANSE尝试生成许多很小的并行计算粒子,然后在线程上复用它们(每个CPU 1个)。实际上,堆栈帧是堆分配的;我们不想支付一大堆的价格&#34;因为我们有很多种,所以每种谷物都没有,我们也不想限制任何可以递归的深度。由于平行分叉,堆栈实际上是一个仙人掌堆栈。

每个过程在进入时都会构建一个词法显示,以便能够访问周围的词法范围。我们考虑使用ENTER指令,但决定反对它有两个原因:

  • 正如其他人所说,它并不是特别快。 MOV说明也一样。
  • 我们观察到显示器通常是稀疏的,并且在词法较深的一侧往往更密集。大多数内部帮助函数只能访问它们的直接词汇父项;你不总是需要访问你所有的父母。有时没有。

因此,编译器确切地确定函数需要访问哪些词法范围,并在函数prolog中生成ENTER,其中只有MOV指令复制父实际显示的部分实际内容。需要。这经常被证明是1对或2对动作。

所以我们在使用ENTER时获得了两倍的性能。

恕我直言,ENTER现在是那些传统的CISC指令之一,它在定义时似乎是一个好主意,但是甚至可以通过英特尔x86优化的RISC指令序列来表现。

答案 3 :(得分:0)

我使用Simics虚拟平台对Linux启动进行了一些指令计数统计,发现从未使用过ENTER。但是,混合中有很多LEAVE指令。 CALL和LEAVE之间几乎有1-1的相关性。这似乎证实了ENTER只是缓慢而昂贵的想法,而LEAVE非常方便。这是在2.6系列内核上测量的。

4.4系列和3.14系列内核的相同实验显示零使用LEAVE或ENTER。据推测,用于编译这些内核的较新gcc的gcc代码生成已停止发出LEAVE(或者机器选项设置不同)。