Eva*_*ran 52
实现alloca
实际需要编译器帮助.这里的一些人说这很简单:
sub esp, <size>
Run Code Online (Sandbox Code Playgroud)
不幸的是,这只是图片的一半.是的,这将"在堆栈上分配空间",但有几个陷阱.
如果编译器发出的代码引用了相对于其他变量esp
而不是ebp
(典型的话,如果你没有使用帧指针进行编译).然后需要调整这些参考.即使使用帧指针,编译器有时也会这样做.
更重要的是,根据定义,alloca
当函数退出时,必须"释放" 分配的空间.
最重要的是第2点.因为您需要编译器发出代码以对称地添加<size>
到esp
函数的每个出口点.
最可能的情况是编译器提供了一些内在函数,允许库编写者向编译器询问所需的帮助.
编辑:
实际上,在glibc(GNU的libc实现)中.实施alloca
简单就是这样:
#ifdef __GNUC__
# define __alloca(size) __builtin_alloca (size)
#endif /* GCC. */
Run Code Online (Sandbox Code Playgroud)
编辑:
在考虑它之后,我相信所需的最小值将是编译器在任何使用的函数中始终使用帧指针alloca
,而不管优化设置如何.这将允许所有本地人ebp
安全地被引用,并且帧清理将通过恢复帧指针来处理esp
.
编辑:
所以我做了一些像这样的事情的实验:
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define __alloca(p, N) \
do { \
__asm__ __volatile__( \
"sub %1, %%esp \n" \
"mov %%esp, %0 \n" \
: "=m"(p) \
: "i"(N) \
: "esp"); \
} while(0)
int func() {
char *p;
__alloca(p, 100);
memset(p, 0, 100);
strcpy(p, "hello world\n");
printf("%s\n", p);
}
int main() {
func();
}
Run Code Online (Sandbox Code Playgroud)
遗憾的是,它无法正常工作.在通过gcc分析装配输出之后.似乎优化会妨碍.问题似乎是因为编译器的优化器完全没有意识到我的内联汇编,所以它习惯于以意想不到的顺序执行操作并仍然通过引用来处理事物esp
.
这是最终的ASM:
8048454: push ebp
8048455: mov ebp,esp
8048457: sub esp,0x28
804845a: sub esp,0x64 ; <- this and the line below are our "alloc"
804845d: mov DWORD PTR [ebp-0x4],esp
8048460: mov eax,DWORD PTR [ebp-0x4]
8048463: mov DWORD PTR [esp+0x8],0x64 ; <- whoops! compiler still referencing via esp
804846b: mov DWORD PTR [esp+0x4],0x0 ; <- whoops! compiler still referencing via esp
8048473: mov DWORD PTR [esp],eax ; <- whoops! compiler still referencing via esp
8048476: call 8048338 <memset@plt>
804847b: mov eax,DWORD PTR [ebp-0x4]
804847e: mov DWORD PTR [esp+0x8],0xd ; <- whoops! compiler still referencing via esp
8048486: mov DWORD PTR [esp+0x4],0x80485a8 ; <- whoops! compiler still referencing via esp
804848e: mov DWORD PTR [esp],eax ; <- whoops! compiler still referencing via esp
8048491: call 8048358 <memcpy@plt>
8048496: mov eax,DWORD PTR [ebp-0x4]
8048499: mov DWORD PTR [esp],eax ; <- whoops! compiler still referencing via esp
804849c: call 8048368 <puts@plt>
80484a1: leave
80484a2: ret
Run Code Online (Sandbox Code Playgroud)
如你所见,它并非如此简单.不幸的是,我坚持我最初的断言,你需要编译器帮助.
这样做很棘手 - 事实上,除非你对编译器的代码生成有足够的控制权,否则它不能完全安全地完成.你的例程必须操纵堆栈,这样当它返回时,所有东西都被清理了,但是堆栈指针仍然处于这样一个位置,即内存块保留在那个位置.
问题是,除非你可以通知编译器已经在函数调用中修改了堆栈指针,它可能会决定它可以继续通过堆栈指针引用其他本地(或其他) - 但偏移将是不正确.
C 和 C++ 标准没有指定alloca()
必须使用堆栈,因为alloca()
不在 C 或 C++ 标准(或 POSIX)中¹。
编译器也可以alloca()
使用堆来实现。例如,ARM的RealView(RVCT)编译器的alloca()
用途malloc()
分配缓冲(这里在其网站上引用的),并且还使编译器产生的代码,释放该缓冲区时,函数返回。这不需要使用堆栈指针,但仍需要编译器支持。
Microsoft Visual C++ 有一个_malloca()
函数,如果堆栈上没有足够的空间,它会使用堆,但它需要调用者使用_freea()
,不像_alloca()
不需要/想要显式释放。
(使用 C++ 析构函数,您显然可以在没有编译器支持的情况下进行清理,但是您不能在任意表达式中声明局部变量,因此我认为您无法编写alloca()
使用 RAII的宏。然后,显然您无论如何都不能alloca()
在某些表达式中使用(如函数参数)。)
¹ 是的,编写一个alloca()
简单地调用system("/usr/games/nethack")
.