那些熟悉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
使用ENTER
和LEAVE
指令也可以实现相同的代码序列:
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
指令?
英特尔在该嵌套级操作数上投入了非零的时间/金钱,基本上我只是好奇,如果有人使用它: - )
参考文献:
答案 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机器,这就是为什么有指令支持嵌套函数(enter
,leave
),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指令,但决定反对它有两个原因:
因此,编译器确切地确定函数需要访问哪些词法范围,并在函数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(或者机器选项设置不同)。