有没有任何编程语言/编译器使用x86 ENTER指令并具有非零嵌套级别?

65

那些熟悉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指令的第二个操作数是“嵌套级别(nesting level)”,它允许从被调用函数中访问多个父级帧(parent frames)。

C语言中没有嵌套函数,因此不使用该功能;局部变量只具有它们声明所在的函数的作用域。这种构造在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指令?

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

参考资料:


13
+1,今天最有趣的问题。对于第一点,GCC支持在C语言中使用嵌套函数,就是你所提供的语法。但在C++中则不支持。 - Iwillnotexist Idonotexist
1
@我将不会存在我不存在。恰巧我也遇到了同样的页面。有趣的是,它可以在默认选项下使用gcc 4.7.2编译。期待着查看反汇编代码。很有趣! - Jonathon Reinhart
9
就此事而言,我从grep查找gcc-4.8.2/gcc/config/i386/i386.c:10339可知GCC现在根本不发出ENTER指令。而该行代码的注释十分明确:“注意:AT&T格式的enter指令没有反转的参数顺序。在所有目标上,使用Enter可能会更慢。而且sdb也不喜欢它。” - Iwillnotexist Idonotexist
4
对于@IwillnotexistIdonotexist所说的话,这其实是GCC最早版本的一部分。在他们的cvs->svn->git转换后的代码库中使用git log -p命令可以看到,这个功能已经在1992年的首次提交中存在了。 - user743382
3
我的私人svn检出LLVM 3.5在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次;甚至看起来他们没有为此编写测试用例。 - Iwillnotexist Idonotexist
显示剩余7条评论
9个回答

57

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),其中被调用者从堆栈中弹出已知数量的参数(ret K),边界检查(bound)等。 这些操作中的许多现在已经过时了。


16
请注意,x86最初设计为Pascal机器。当设计师添加高级语言支持指令时,我经常想知道他们心中想的是哪些高级语言。您能提供任何附加的历史背景资料吗? - Jonathon Reinhart
12
请看 http://stevemorse.org/8086/ - Morse 是该芯片的设计者,有关 Pascal 和 PL/M 的章节可能会让人受益匪浅。 - gsg
4
曾经,结构化编程被视为银弹,Pascal 影响了像 Modula 和特别是 Ada 这样的语言。Ada 是美国国防部的“语言之一”。因此,这些语言得到硬件支持并不让我感到意外。 - xmojmr
5
-Os实际上并不是“优化大小”,而是“在不执行可能对大小产生负面影响的任何优化的情况下,优化性能”。 - R.. GitHub STOP HELPING ICE
2
关于“Pascal机”和x86的问题 https://retrocomputing.stackexchange.com/q/6959/8579 - Evan Carroll
显示剩余3条评论

14

正如 Iwillnotexist Idonotexist指出的那样,GCC在C语言中支持嵌套函数,使用我上面展示的确切语法。

然而,它不使用ENTER指令。相反,用于嵌套函数的变量被分组到本地变量区域中,并将指向此组的指针传递给嵌套函数。有趣的是,“父变量指针”是通过非标准机制传递的:在x64上,它通过r10传递,而在x86(cdecl)上,它通过ecx传递,后者为C ++中保留了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位时,将生成以下代码片段:

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 Cordes
2
@Peter,另一种情况是内部函数被作为回调函数传递给某个外部函数。这时,堆栈上的闭包存根确实是必要的,因为单个函数指针不能封装函数地址和其数据。 - Jonathon Reinhart
2
没错,我想我已经看到gcc发出了x86机器码的mov-immediate存储,用于你所说的存根。它还发出汇编指令来标记堆栈可执行,以便这可以工作,因此链接使用该文件的对象文件将使整个程序的堆栈可执行!http://lists.llvm.org/pipermail/cfe-dev/2015-September/045063.html(目前没有clang支持) - Peter Cordes
1
这是gcc将机器码字节写入堆栈的示例,然后将函数指针传递给嵌套函数(传递给它看不到的函数):https://godbolt.org/g/NaSZWp。 - Peter Cordes
是的,那正是我所指的。真的很酷! - Jonathon Reinhart

11

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

PARLANSE试图生成许多小的并行计算颗粒,然后在线程之上复用它们(每个CPU使用一个线程)。实际上,栈帧是堆分配的。我们不想为每个颗粒支付“大栈”的代价,因为我们有很多颗粒,并且我们不想对任何递归深度设置限制。由于并行派生,栈实际上是一个仙人掌栈。

每个过程在进入时都建立一个词法显示,以便访问周围的词法范围。我们考虑使用ENTER指令,但出于两个原因而决定不这样做:

  • 正如其他人所指出的那样,它并不特别快。MOV指令同样有效。
  • 我们观察到显示通常是稀疏的,并且倾向于在词法深度较深的一侧更密集。大多数内部助手函数只需要访问它们的直接词法父项即可完成;您并不总是需要访问所有父项。有时是一个也没有。

因此,编译器确定函数需要访问哪些词法范围,并在函数prolog中生成仅包含实际所需父项显示部分的MOV指令。通常情况下,这实际上只需要1或2对移动指令。

因此,与使用ENTER相比,我们在性能上获得了两次胜利。

我认为,ENTER现在是那些遗留的CISC指令之一,在定义时似乎是一个好主意,但被RISC指令序列超越,即使是Intel x86也进行了优化。


1
这正是我所期望的观点,谢谢。我仍然很好奇为什么AMD决定在AMD64中保留ENTER,即使似乎没有人使用它。 - Jonathon Reinhart
7
如果让解码器在64位模式下拒绝它,在其他模式下接受它,可能会增加复杂性。 AMD非常保守地清理指令集,因为他们不确定AMD64是否会受到欢迎,并且不想被卡在没有人使用的更多晶体管上。基本上,我们可以归咎于资本主义,这是一个巨大的错失机会,可以整理x86机器代码并更改使高性能实现棘手的事情。(例如,setcc可以更改为setcc r/m32,以节省指令以将其布尔化为int而不是char)。 - Peter Cordes

3
我在Simics虚拟平台上统计了Linux启动时的指令次数,并发现没有使用ENTER。然而,在混合的指令中有相当多的LEAVE指令。CALL和LEAVE之间几乎存在1-1的对应关系。这似乎证实了ENTER很慢而且很昂贵,而LEAVE非常方便。此统计是在2.6系列内核上进行的。
在4.4系列和3.14系列内核上进行相同的实验后,都没有发现使用LEAVE或ENTER。据推测,用于编译这些内核的新版gcc的代码生成已停止发出LEAVE(或者机器选项设置不同)。

5
-fomit-frame-pointer 现在是默认选项。即使对于包含变长数组的函数的优化代码,gcc 仍然在创建帧指针时使用 leave 指令。(可以在 https://godbolt.org/g/LF3Rrk 查看)我使用了几个不同的 -mtune= 选项进行测试,它们都使用了 leave 指令。然而,clang 从不使用 leave 指令。 对于 -Os(优化大小)而言,这是一个未被优化的地方,因为它只比 mov/pop 指令多一个栈同步的微操作而已,总共只有 3 个微操作。 - Peter Cordes
4
即使使用 -Os-Oz 编译,gcc 和 clang 也不会使用 enter 指令。在 Skylake 上,enter n,0 指令需要 12 个微操作,并且吞吐量为每 8 个时钟周期执行一次。而在 Ryzen 上,该指令需要 12 个微操作,并且吞吐量为每 16 个时钟周期执行一次。在 -Oz 优化模式下,clang 可能会使用 enter 指令,因为这种情况下它会使用 push 2 / pop rax 来节省 2 个字节,而不是使用 mov eax,2。(gcc 没有 -Oz 模式)。可以参考 http://agner.org/optimize/ 的指令表和微架构指南来理解这些信息。还可以参考 SO x86 标签百科 - Peter Cordes
感谢@PeterCordes提供的信息。符合我所见。 - jakobengblom2
1
这并没有回答问题。问题不是Linux使用什么或GCC发出什么,而是是否存在使用具有非零嵌套级别的指令的语言。 - JdeBP

3

IMP77(由爱丁堡大学开发)允许嵌套例程/函数。Intel编译器的版本有时会使用ENTER指令,有时会使用非零级别值。


有趣!您能提供一个参考吗?如果您能提供生成的汇编代码片段就太酷了。 - Jonathon Reinhart
在Github上搜索IMP77(在siliconsam下)。然后在pass2.imp和pass3coff.c的源代码中搜索ENTER。 pass2.imp源文件指示代码生成,而pass3coff.c将实际的ENTER指令添加到COFF对象文件中。接下来将提供IMP代码和生成的Intel代码示例。 - John McMullin

2

Vector Pascal编译器使用此指令进行过程入口。Pascal允许任意级别的嵌套,Enter支持的显示对此非常有用。


1

我在Pascal编译器中使用它。虽然它比等效代码慢,但更紧凑。31的嵌套限制不是什么大问题,但64kb的本地限制可能会成为问题。我使用的解决方案是发出一个带有0个本地变量的enter指令,然后在enter指令之后分配本地变量。只有当本地变量超过64kb时才需要这样做。

有几种优化可以消除对enter的使用,甚至消除框架的使用。例如,零嵌套函数不需要使用enter。此外,您可以通过esp偏移量访问本地变量,因此不需要完整的ebp交换。

顺便说一句,我相信enter、leave和bound指令是专门为Pascal添加到8086指令集中的。原因是在引入时,Pascal正处于其流行的高峰期,并且需要所有这些指令。

进入指令之所以较慢是因为它实际上是一条“慢路径”指令。Pentium 的超标量模式设计时,像 enter、leave 和字符串比较和 move 等指令这类“已弃用”的 CISC 指令没有被作为 ROPs 进行翻译,而是被侧重于一个微码引擎进行序列化。大多数指令会被转码为内部 ROPs 或 RISC 操作,这些操作基本上是长字微代码指令,可在 CPU 中执行相应的操作。
这听起来很违反直觉,一个微码去比另一个更慢,但 CPU 的内部微码可以被设计成非常长的控制词以进行单个或极少量的周期操作,但其中还有很多循环。此外,执行微码和将其翻译为微码之间存在差异。

0

显示相应的Intel指令,展示IMP源代码和生成的机器码! ! 主程序 %begin 0000 C8 00 00 01 ENTER 0000,1

! global variable
%integer sum

! nested routine
%integer %function mcode001( %integer number, x )
 0004 EB 00                                 JMP L1001
 0006                      L1002  EQU $
 0006 C8 00 00 02                           ENTER 0000,2
    ! local variable
    %integer r

    r = number + x
 000A 8B 45 0C                              MOV EAX,[EBP+12]
 000D 03 45 08                              ADD EAX,[EBP+8]
 0010 89 45 F4                              MOV [EBP-12],EAX

    %result = r
 0013 8B 45 F4                              MOV EAX,[EBP-12]
 0016 C9                                    LEAVE
 0017 C3                                    RET
%end
 0018                      L1001  EQU $

! call the nested routine
sum = mcode001(46,24)&255
 0018 6A 2E                                 PUSH 46
 001A 6A 18                                 PUSH 24
 001C E8 00 00                              CALL 'MCODE001' (INTERNAL L1002 )
 001F 83 C4 08                              ADD ESP,8
 0022 25 FF 00 00 00                        AND EAX,255
 0027 89 45 F8                              MOV [EBP-8],EAX

! show the result itos converts binary integer to text
printstring("Result =".itos(sum,3)); newline
 002A FF 75 F8                              PUSH WORD PTR [EBP-8]
 002D 6A 03                                 PUSH 3
 002F 8D 85 F8 FE FF FF                     LEA EAX,[EBP-264]
 0035 50                                    PUSH EAX
 0036 E8 42 00                              CALL 'ITOS' (EXTERN 66)
 0039 83 C4 0C                              ADD ESP,12
 003C 8D 85 F8 FD FF FF                     LEA EAX,[EBP-520]
 0042 50                                    PUSH EAX
 0043 B8 00 00 00 00                        MOV EAX,COT+0
 0048 50                                    PUSH EAX
 0049 68 FF 00 00 00                        PUSH 255
 004E E8 03 00                              CALL '_IMPSTRCPY' (EXTERN 3)
 0051 83 C4 0C                              ADD ESP,12
 0054 8D 85 F8 FD FF FF                     LEA EAX,[EBP-520]
 005A 50                                    PUSH EAX
 005B 8D 85 F8 FE FF FF                     LEA EAX,[EBP-264]
 0061 50                                    PUSH EAX
 0062 68 FF 00 00 00                        PUSH 255
 0067 E8 05 00                              CALL '_IMPSTRCAT' (EXTERN 5)
 006A 83 C4 0C                              ADD ESP,12
 006D 81 EC 00 01 00 00                     SUB ESP,256
 0073 89 E0                                 MOV EAX,ESP
 0075 50                                    PUSH EAX
 0076 8D 85 F8 FD FF FF                     LEA EAX,[EBP-520]
 007C 50                                    PUSH EAX
 007D 68 FF 00 00 00                        PUSH 255
 0082 E8 03 00                              CALL '_IMPSTRCPY' (EXTERN 3)
 0085 83 C4 0C                              ADD ESP,12
 0088 E8 34 00                              CALL 'PRINTSTRING' (EXTERN 52)
 008B 81 C4 00 01 00 00                     ADD ESP,256
 0091 E8 3C 00                              CALL 'NEWLINE' (EXTERN 60)

%endofprogram
 0094 C9                                    LEAVE
 0095 C3                                    RET
      _TEXT  ENDS
      CONST  SEGMENT WORD PUBLIC 'CONST'
 0000                                       db 08,52 ; .R
 0002                                       db 65,73 ; es
 0004                                       db 75,6C ; ul
 0006                                       db 74,20 ; t.
 0008                                       db 3D,00 ; =.
      CONST  ENDS
      _TEXT  SEGMENT WORD PUBLIC 'CODE'
             ENDS
      DATA  SEGMENT WORD PUBLIC 'DATA'
      DATA    ENDS
              ENDS
      _SWTAB  SEGMENT WORD PUBLIC '_SWTAB'
      _SWTAB   ENDS

2
嗨John,感谢你的更新。请不要添加新答案,而是编辑您的原始答案并在那里添加此内容。然后删除这两个其他答案。谢谢! - Jonathon Reinhart

-1
! main routine/program
%begin

! global variable
%integer sum

! nested routine
%integer %function mcode001( %integer number, x )
    ! local variable
    %integer r

    r = number + x

    %result = r
%end

! call the nested routine
sum = mcode001(46,24)&255

! show the result itos converts binary integer to text
printstring("Result =".itos(sum,3)); newline

%endofprogram

1
嗨John,感谢你的更新。请不要添加新答案,而是编辑您的原始答案并在那里添加此内容。然后删除这两个其他答案。谢谢! - Jonathon Reinhart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接