如何在D,C和C ++等语言中使用内联x86汇编程序实现alloca()?我想创建一个稍微修改过的版本,但首先我需要知道标准版本是如何实现的。从编译器中读取反汇编并没有帮助,因为它们执行了很多优化,我只想要规范形式。
编辑:我想最难的是我希望它具有正常的函数调用语法,即使用裸函数或其他东西,使它看起来像普通的alloca()。
编辑#2:啊,到底是什么,你可以假设我们没有省略帧指针。
答案 0 :(得分:52)
实施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
正如您所看到的,它并非如此简单。不幸的是,我坚持原来的断言,你需要编译器帮助。
答案 1 :(得分:7)
这样做很棘手 - 实际上,除非你对编译器的代码生成有足够的控制权,否则它无法完全安全地完成。你的例程必须操纵堆栈,这样当它返回时,所有东西都被清理干净,但是堆栈指针仍然处于这样的位置,即内存块仍留在那个地方。
问题在于,除非你可以通知编译器已经在函数调用中修改了堆栈指针,否则它可能会决定它可以通过堆栈指针继续引用其他本地(或其他) - 但是偏移量不正确。
答案 2 :(得分:4)
对于D编程语言,alloca()的源代码附带download。它的工作原理得到了很好的评论。对于dmd1,它位于/dmd/src/phobos/internal/alloca.d中。对于dmd2,它位于/dmd/src/druntime/src/compiler/dmd/alloca.d。
答案 3 :(得分:4)
C和C ++标准没有指定alloca()
必须使用堆栈,因为alloca()
不符合C或C ++标准(或POSIX),¹。< / p>
编译器也可以使用堆实现alloca()
。例如,ARM RealView(RVCT)编译器的alloca()
使用malloc()
来分配缓冲区(referenced on their website here),并且还会使编译器发出代码,以便在函数返回时释放缓冲区。这不需要使用堆栈指针,但仍需要编译器支持。
Microsoft Visual C ++有一个_malloca()
函数,如果堆栈上没有足够的空间,它会使用堆,但它要求调用者使用_freea()
,这与_alloca()
不同,不需要/想要明确释放。
(使用C ++析构函数,显然可以在没有编译器支持的情况下进行清理,但是你不能在任意表达式中声明局部变量,所以我认为你不能写一个alloca()
宏使用RAII。然后,显然你不能在某些表达式(如function parameters)中使用alloca()
。)
¹是的,写一个只需拨打alloca()
的{{1}}是合法的。
答案 4 :(得分:3)
alloca直接在汇编代码中实现。 那是因为你无法直接从高级语言控制堆栈布局。
另请注意,大多数实现都会执行一些额外的优化,例如出于性能原因而对齐堆栈。 在X86上分配堆栈空间的标准方法如下:
sub esp, XXX
而XXX是allcoate的字节数
修改强>
如果您想查看实现(并且您正在使用MSVC),请参阅alloca16.asm和chkstk.asm。
第一个文件中的代码基本上将所需的分配大小与16字节边界对齐。第二个文件中的代码实际上遍历属于新堆栈区域并触及它们的所有页面。这可能会触发操作系统用来增加堆栈的PAGE_GAURD异常。
答案 5 :(得分:3)
纯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
}
答案 6 :(得分:1)
您可以检查开源C编译器的源代码,例如Open Watcom,并自行查找
答案 7 :(得分:1)
如果你不能使用c99的可变长度数组,你可以使用复合文字强制转换为空指针。
#define ALLOCA(sz) ((void*)((char[sz]){0}))
这也适用于-ansi(作为gcc扩展),即使它是函数参数;
some_func(&useful_return, ALLOCA(sizeof(struct useless_return)));
缺点是当编译为c ++时,g ++&gt; 4.6会给你一个error: taking address of temporary array ... clang而icc不会抱怨
答案 8 :(得分:0)
我们想要做的是这样的事情:
;alloca.asm
_TEXT SEGMENT
PUBLIC alloca
alloca PROC
sub rsp, rcx ;<sp> -= size
mov rax, rsp ;return <sp>;
ret
alloca ENDP
_TEXT ENDS
END
在Assembly(Visual Studio 2017,64位)中,它看起来像:
;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
不幸的是,我们的返回指针是堆栈中的最后一项,我们不想覆盖它。另外,我们需要注意对齐,即。圆形尺寸最多为8的倍数。所以我们必须这样做:
{{1}}
答案 9 :(得分:-1)
Alloca很简单,你只需向上移动堆栈指针;然后生成所有读/写指向这个新块
sub esp, 4
答案 10 :(得分:-2)
我推荐“输入”指令。可以在286和更新的处理器上使用(可能已经在186上可用,我不记得了,但是那些并不是广泛可用的。)