如何在 D、C 和 C++ 等语言中使用内联 x86 汇编器实现 alloca()?我想创建一个稍微修改过的版本,但首先我需要知道标准版本是如何实现的。从编译器读取反汇编并没有帮助,因为它们执行了很多优化,我只想要规范形式。
编辑:我想困难的部分是我希望它具有正常的函数调用语法,即使用裸函数或其他东西,使其看起来像正常的 alloca()。
编辑#2:啊,到底是什么,你可以假设我们没有省略帧指针。
如何在 D、C 和 C++ 等语言中使用内联 x86 汇编器实现 alloca()?我想创建一个稍微修改过的版本,但首先我需要知道标准版本是如何实现的。从编译器读取反汇编并没有帮助,因为它们执行了很多优化,我只想要规范形式。
编辑:我想困难的部分是我希望它具有正常的函数调用语法,即使用裸函数或其他东西,使其看起来像正常的 alloca()。
编辑#2:啊,到底是什么,你可以假设我们没有省略帧指针。
实现alloca
实际上需要编译器的帮助。这里的一些人说这很容易:
sub esp, <size>
不幸的是,这只是图片的一半。是的,这将“在堆栈上分配空间”,但有几个陷阱。
如果编译器发出了引用其他变量的代码,esp
而不是ebp
(典型的如果你编译没有帧指针)。然后需要调整这些参考。即使使用帧指针,编译器有时也会这样做。
更重要的是,根据定义,分配的空间alloca
必须在函数退出时“释放”。
最重要的是第 2 点。因为您需要编译器发出代码以在函数的每个退出点对称<size>
添加esp
。
最可能的情况是编译器提供了一些内在函数,允许库编写者向编译器寻求所需的帮助。
编辑:
事实上,在 glibc(GNU 的 libc 实现)中。的实现alloca
很简单:
#ifdef __GNUC__
# define __alloca(size) __builtin_alloca (size)
#endif /* GCC. */
编辑:
在考虑之后,我认为编译器需要的最低要求是在任何使用 的函数中始终使用帧指针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();
}
不幸的是,它不能正常工作。通过 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
如您所见,事情并非如此简单。不幸的是,我坚持我最初的断言,即您需要编译器的帮助。
这样做会很棘手 - 事实上,除非您对编译器的代码生成有足够的控制权,否则它不能完全安全地完成。您的例程必须操作堆栈,以便在它返回时所有内容都被清除,但堆栈指针仍保留在这样的位置,即内存块仍保留在该位置。
问题是,除非您可以通知编译器堆栈指针已在您的函数调用中被修改,否则它很可能会决定它可以继续通过堆栈指针引用其他本地(或其他) - 但偏移量将是不正确。
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")
.
对于 D 编程语言,alloca() 的源代码随下载一起提供。它的工作原理得到了很好的评论。对于 dmd1,它位于 /dmd/src/phobos/internal/alloca.d 中。对于 dmd2,它位于 /dmd/src/druntime/src/compiler/dmd/alloca.d 中。
纯 ISO C++中的可变长度数组。概念验证实施。
void foo(unsigned n)
{
cps_alloca<Payload>(n,[](Payload *first,Payload *last)
{
fill(first,last,something);
});
}
template<typename T,unsigned N,typename F>
auto cps_alloca_static(F &&f) -> decltype(f(nullptr,nullptr))
{
T data[N];
return f(&data[0],&data[0]+N);
}
template<typename T,typename F>
auto cps_alloca_dynamic(unsigned n,F &&f) -> decltype(f(nullptr,nullptr))
{
vector<T> data(n);
return f(&data[0],&data[0]+n);
}
template<typename T,typename F>
auto cps_alloca(unsigned n,F &&f) -> decltype(f(nullptr,nullptr))
{
switch(n)
{
case 1: return cps_alloca_static<T,1>(f);
case 2: return cps_alloca_static<T,2>(f);
case 3: return cps_alloca_static<T,3>(f);
case 4: return cps_alloca_static<T,4>(f);
case 0: return f(nullptr,nullptr);
default: return cps_alloca_dynamic<T>(n,f);
}; // mpl::for_each / array / index pack / recursive bsearch / etc variacion
}
alloca 直接用汇编代码实现。那是因为您无法直接从高级语言控制堆栈布局。
另请注意,大多数实现将执行一些额外的优化,例如出于性能原因对齐堆栈。在 X86 上分配堆栈空间的标准方法如下所示:
sub esp, XXX
而 XXX 是要分配的字节数
编辑:
如果您想查看实现(并且您正在使用 MSVC),请参阅 alloca16.asm 和 chkstk.asm。
第一个文件中的代码基本上将所需的分配大小与 16 字节边界对齐。第二个文件中的代码实际上遍历了所有属于新堆栈区域的页面并触及它们。这可能会触发 PAGE_GAURD 异常,操作系统使用这些异常来增长堆栈。
您可以检查开源 C 编译器的源代码,例如Open Watcom,然后自己找到它
如果您不能使用 c99 的可变长度数组,则可以使用复合文字转换为 void 指针。
#define ALLOCA(sz) ((void*)((char[sz]){0}))
这也适用于 -ansi (作为 gcc 扩展),即使它是函数参数;
some_func(&useful_return, ALLOCA(sizeof(struct useless_return)));
不利的一面是,当编译为 c++ 时,g++>4.6 会给你一个错误:获取临时数组的地址... clang 和 icc 不会抱怨
我们想做的是这样的:
void* alloca(size_t size) {
<sp> -= size;
return <sp>;
}
在组装(Visual Studio 2017,64 位)中,它看起来像:
;alloca.asm
_TEXT SEGMENT
PUBLIC alloca
alloca PROC
sub rsp, rcx ;<sp> -= size
mov rax, rsp ;return <sp>;
ret
alloca ENDP
_TEXT ENDS
END
不幸的是,我们的返回指针是堆栈上的最后一项,我们不想覆盖它。此外,我们需要注意对齐,即。将大小舍入到 8 的倍数。所以我们必须这样做:
;alloca.asm
_TEXT SEGMENT
PUBLIC alloca
alloca PROC
;round up to multiple of 8
mov rax, rcx
mov rbx, 8
xor rdx, rdx
div rbx
sub rbx, rdx
mov rax, rbx
mov rbx, 8
xor rdx, rdx
div rbx
add rcx, rdx
;increase stack pointer
pop rbx
sub rsp, rcx
mov rax, rsp
push rbx
ret
alloca ENDP
_TEXT ENDS
END
Alloca 很简单,只需将堆栈指针向上移动即可;然后生成所有读/写指向这个新块
sub esp, 4
我推荐“进入”指令。在 286 和更新的处理器上可用(可能在 186 上也可用,我不记得了,但无论如何这些都没有广泛使用)。