【问题标题】:Do any languages / compilers utilize the x86 ENTER instruction with a nonzero nesting level?是否有任何语言/编译器使用具有非零嵌套级别的 x86 ENTER 指令?
【发布时间】:2014-12-07 00:40:04
【问题描述】:

熟悉 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 指令?

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

参考资料:

【问题讨论】:

  • +1,今天最有趣的问题。对于 1),GCC 完全使用您的语法支持 nested functions in C。但明确不在 C++ 中。
  • @IwillnotexistIdonotexist 巧合的是,我刚好碰到了同一个页面。有趣的是,它使用默认选项在 gcc 4.7.2 上编译。期待拆解。有趣的东西!
  • 对于它的价值,我从 grep-ing gcc-4.8.2/gcc/config/i386/i386.c:10339 了解到,现在 GCC 根本不会发出 ENTER。那行的评论很清楚:/* Note: AT&T enter does NOT have reversed args. Enter is probably slower on all targets. Also sdb doesn't like it. */
  • @IwillnotexistIdonotexist FWIW,这是 GCC 的第一个版本的一部分。 git log -p 在他们的 cvs->svn->git 转换存储库中显示它在 1992 年的初始签入中已经存在。
  • 我对 LLVM 3.5 的私人 svn 结帐在 llvm/lib/Target/X86/X86FrameLowering.cpp:355 有一条对 emitPrologue() 方法的评论,其中部分内容为 ; Spill general-purpose registers [for all callee-saved GPRs] pushq %<reg> [if not needs FP] .cfi_def_cfa_offset (offset from RETADDR) .seh_pushreg %<reg>。没有提到ENTER,只有推送; x86 ENTER 的枚举常量在所有 LLVM 中仅出现 3 次;它甚至看起来都没有测试用例。

标签: assembly x86


【解决方案1】:

enter 在实践中被避免使用,因为它的性能很差 - 请参阅"enter" vs "push ebp; mov ebp, esp; sub esp, imm" and "leave" vs "mov esp, ebp; pop ebp" 的答案。有一堆 x86 指令已过时,但出于向后兼容性的原因仍受支持 - enter 就是其中之一。 (虽然leave 没问题,编译器很乐意发出它。)

在 Python 中完全通用地实现嵌套函数实际上是一个比简单地选择一些帧管理指令更有趣的问题 - 搜索“闭包转换”和“向上/向下函数参数问题”,您会发现许多有趣的讨论.

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

【讨论】:

  • +1 “请注意,x86 最初是作为 Pascal 机器设计的” - 我经常想知道设计人员在添加高级语言时考虑了哪些高级语言语言支持说明。您可以链接到任何其他历史观点吗?
  • 看看stevemorse.org/8086 - Morse 是芯片的设计者,关于 Pascal 和 PL/M 的章节可能会有所启发。
  • @JonathonReinhart 从前structured programming 是灵丹妙药,Pascal 影响了诸如Modula 之类的语言,尤其是Ada,它是美国国防部的“语言”。所以这些语言的硬件支持并不让我感到惊讶
  • @AkiSuihkonen:-Os 并不是真正的“优化大小”,​​而是“优化性能而不执行任何可能对大小产生不利影响的优化”。
  • 关于“Pascal Machine”和x86的问题retrocomputing.stackexchange.com/q/6959/8579
【解决方案2】:

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

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

#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 位编译时生成以下(sn-p of)代码:

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);
}

【讨论】:

  • 看看gcc -O0 做了什么很有趣。 gcc 可能很少会不内联启用优化的嵌套函数。虽然也许如果外部函数中有很多调用站点...(特别是如果您使用 -Os 优化大小。)
  • @Peter 另一种情况是内部函数作为回调传递给某个外部函数。这时候栈上的闭包存根就真的很有必要了,因为单个函数指针不能同时封装函数地址和它的数据。
  • 哦,对了,我想我已经看到 gcc 为您正在谈论的存根发出 mov-immediate 存储的 x86 机器代码。它发出汇编程序指令来标记堆栈可执行文件,以便它可以工作,因此链接使用它的目标文件将使您的整个程序的堆栈可执行! lists.llvm.org/pipermail/cfe-dev/2015-September/045063.html(尚无 clang 支持)
  • 这是一个 gcc 在将函数指针传递给嵌套函数(传递给它看不到的函数)之前将机器代码字节写入堆栈的示例:godbolt.org/g/NaSZWp
  • 是的,这正是我所指的。非常酷的东西,真的!
【解决方案3】:

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

PARLANSE 尝试生成很多很多小的并行计算粒度,然后在线程之上多路复用它们(每个 CPU 1 个)。实际上,栈帧是堆分配的;我们不想为每个颗粒支付“大堆栈”的价格,因为我们有很多,我们不想限制任何事情的递归深度。由于并行分叉,栈实际上是一个仙人掌栈。

每个过程在进入时都会构建一个词法显示,以允许访问周围的词法范围。我们考虑过使用 ENTER 指令,但出于两个原因决定不使用它:

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

因此,编译器准确地计算出函数需要访问的词法范围,并在 ENTER 所在的函数序言中生成 MOV 指令,以复制实际需要的父显示部分。这通常是 1 或 2 对移动。

所以我们在性能上比使用 ENTER 赢了两倍。

恕我直言,ENTER 现在是那些遗留的 CISC 指令之一,在定义它时这似乎是一个好主意,但它甚至被英特尔 x86 优化的 RISC 指令序列所超越。

【讨论】:

  • 这正是我所希望的视角;谢谢。我仍然很好奇为什么 AMD 决定在 AMD64 中保留 ENTER,尽管似乎没有人使用它。
  • @JonathonReinhart:让解码器在 64 位模式下拒绝它但在其他模式下接受它可能会增加复杂性。 AMD 在清理指令集方面非常保守,因为他们不确定 AMD64 是否会流行起来,并且不想被更多没有人使用的晶体管所困。我们基本上可以责怪资本主义错失了整理 x86 机器代码和改变使高性能实现变得棘手的事情的巨大机会。 (例如,setcc 可能已更改为 setcc r/m32,将布尔化指令保存为 int 而不是 char
【解决方案4】:

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

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

【讨论】:

  • -fomit-frame-pointer 现在是默认值。 gcc 在生成帧指针时仍然使用leave。 (即使在具有 VLA 的函数的优化代码中也是如此:godbolt.org/g/LF3Rrk)。我测试了几个不同的-mtune= 选项,它们都使用了leave。但是,clang 从来没有使用过leave。这是对-Os 的错过优化(针对大小进行优化),因为它只有 3 个微指令,而 mov/pop 至少有 2 个微指令(可能是堆栈同步微指令)。
  • gcc 和 clang 不要使用 enter,即使你使用 -Os-Oz 编译。 enter n,0 在 Skylake 上是 12 微指令,每 8 个时钟的吞吐量为 1。在 Ryzen 上,它是 12 微指令,每 16 个时钟有 1 个吞吐量。在-Oz:不惜一切代价优化大小,clang 使用enter 可能是有意义的,因为它可以像push 2 / pop rax 那样节省2 个字节,而不是mov eax,2。 (gcc 没有-Oz 模式。)请参阅agner.org/optimize 获取指令表和了解它们的微架构指南。另见the SO x86 tag wiki
  • 感谢@PeterCordes 提供的信息。符合我所见。
  • 这没有回答问题。问题不在于 Linux 使用什么或 GCC 发出什么,而是是否存在确实使用具有非零嵌套级别的指令的语言。
猜你喜欢
  • 1970-01-01
  • 2015-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-16
  • 1970-01-01
相关资源
最近更新 更多