Alloca 实施

Posted

技术标签:

【中文标题】Alloca 实施【英文标题】:Alloca implementation 【发布时间】:2010-10-17 09:55:37 【问题描述】:

如何在 D、C 和 C++ 等语言中使用内联 x86 汇编器实现 alloca()?我想创建一个稍微修改过的版本,但首先我需要知道标准版本是如何实现的。从编译器读取反汇编并没有帮助,因为它们执行了很多优化,我只想要规范形式。

编辑:我想困难的部分是我希望它具有正常的函数调用语法,即使用裸函数或其他东西,使其看起来像正常的 alloca()。

编辑#2:啊,这到底是怎么回事,你可以假设我们没有省略帧指针。

【问题讨论】:

【参考方案1】:

我们想做的是这样的:

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

不幸的是,我们的返回指针是堆栈上的最后一项,我们不想覆盖它。此外,我们需要注意对齐,即。将 size 取整为 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

【讨论】:

【参考方案2】:

实现alloca 实际上需要编译器帮助。这里有一些人说这很简单:

sub esp, <size>

不幸的是,这只是图片的一半。是的,这会“在堆栈上分配空间”,但有几个问题。

    如果编译器已发出代码 引用其他变量 相对于esp 而不是ebp (典型的,如果你编译没有 帧指针)。那么那些 参考文献需要调整。即使使用帧指针,编译器有时也会这样做。

    更重要的是,根据定义,alloca 分配的空间必须是 函数退出时“释放”。

最重要的是第 2 点。因为您需要编译器发出代码以在函数的每个退出点对称地将&lt;size&gt; 添加到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

如您所见,事情并非如此简单。不幸的是,我坚持我最初的主张,即您需要编译器帮助。

【讨论】:

我觉得你没问题; ESP 访问在函数调用之前写入参数,并且 ESP-relative 是正确的。您可以尝试-fno-accumulate-outgoing-args 或任何它和相关的参数来让 gcc 只使用 PUSH 而不是使用 MOV 来修改堆栈的底部。 但实际上,试图在编译器背后实现 alloca 是一个可怕的想法,就像您在这个出色答案的早期部分指出的那样。它出错的方式有很多,而且没有理由这样做。如果人们想编写 asm 并进行自己的堆栈分配,只需使用纯 asm 编写,而不是在 C++ 中滥用 inline-asm。 @PeterCordes 确实,大多数 ESP 引用都是函数参数,但是因为它试图在“alloca”之前预先分配空间,所以这些动作会践踏用户的“分配空间”。如果我打算使用那个空间,那就坏了。将这些更改为适当的推送将解决大部分问题。最后一个 esp 引用将结果存储在局部变量中,并且将再次践踏“数组”。它很快就坏了。 哦,好点,是的,我忘记了谁拥有哪个空间。但是DWORD PTR [esp],eax 正在为puts 写一个arg;我没有看到对本地的 ESP 相对访问。无论如何,我认为我们同意这里的结论是“可能在受控条件下使用一堆通常会损害性能的 gcc 选项;完全不值得而且是个坏主意”。特别是因为在 x86-64 代码中,没有办法告诉编译器你想要破坏红色区域,所以这根本不能移植到 x86-64。 @PeterCordes,同意,并在最后一个 DWORD PTR [esp],eax 上打了电话 【参考方案3】:

如果您不能使用 c99 的可变长度数组,您可以使用复合文字转换为 void 指针。

#define ALLOCA(sz) ((void*)((char[sz])0))

这也适用于 -ansi(作为 gcc 扩展),即使它是函数参数;

some_func(&useful_return, ALLOCA(sizeof(struct useless_return)));

缺点是当编译成 c++ 时,g++>4.6 会给你一个error: taking address of temporary array ...虽然clang和icc不会抱怨

【讨论】:

我觉得好像应该指出,C++、G++ 都报错的原因仅仅是因为它在 C++ 中是不稳定的。 C99 有 VLA,C++ 没有。我不觉得你完全忽略了这一点,但特别指出它会提高答案的质量。 这不起作用有两个原因:如果 sz 是动态的 (char[sz])0 无效 C. gcc/clang 不会让您拥有可变大小的复合文字。第二个原因是混叠。 char 数组具有声明的类型。你不能让它表现得像分配的内存,没有声明的类型。 我相信 VLA 的范围也会有所不同(更严格)【参考方案4】:

继续传递风格 Alloca

纯 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

LIVE DEMO

cps_alloca on github

【讨论】:

【参考方案5】:

C 和 C++ 标准未指定 alloca() 必须使用堆栈,因为 alloca() 不在 C 或 C++ 标准(或 POSIX 中)¹。

编译器也可以使用堆实现alloca()。例如,ARM RealView (RVCT) 编译器的 alloca() 使用 malloc() 来分配缓冲区 (referenced on their website here),并且还会导致编译器在函数返回时发出释放缓冲区的代码。这不需要使用堆栈指针,但仍然需要编译器支持。

Microsoft Visual C++ 有一个 _malloca() 函数,如果堆栈上没有足够的空间,它会使用堆,但它需要调用者使用 _freea(),这与 _alloca() 不同,它不需要/不需要显式释放。

(使用 C++ 析构函数,您显然可以在没有编译器支持的情况下进行清理,但是您不能在任意表达式中声明局部变量,所以我认为您不能编写使用 RAII 的 alloca() 宏. 话又说回来,显然你不能在某些表达式中使用alloca()(比如function parameters)。)

¹ 是的,编写一个简单地调用system("/usr/games/nethack")alloca() 是合法的。

【讨论】:

【参考方案6】:

您可以检查开源 C 编译器的源代码,例如 Open Watcom,然后自己找到它

【讨论】:

【参考方案7】:

对于 D 编程语言,alloca() 的源代码带有download。它的工作原理得到了很好的评论。对于 dmd1,它位于 /dmd/src/phobos/internal/alloca.d 中。对于 dmd2,它位于 /dmd/src/druntime/src/compiler/dmd/alloca.d。

【讨论】:

好吧,我想这几乎可以回答它。它在 cmets 中说它是一个神奇的功能,需要编译器支持,即我不能完全按照我的意愿去做。也许我会想办法用现有的 alloca() 和 mixins 来代替。【参考方案8】:

Alloca 很简单,只需向上移动堆栈指针即可;然后生成所有读/写来指向这个新块

sub esp, 4

【讨论】:

1) 这不是 esi 2) 堆栈从高地址到低地址增长【参考方案9】:

alloca 直接用汇编代码实现。 那是因为您无法直接从高级语言控制堆栈布局。

还要注意,大多数实现都会执行一些额外的优化,例如出于性能原因对齐堆栈。 在 X86 上分配堆栈空间的标准方式如下所示:

sub esp, XXX

而 XXX 是要分配的字节数

编辑: 如果您想查看实现(并且您正在使用 MSVC),请参阅 alloca16.asm 和 chkstk.asm。 第一个文件中的代码基本上将所需的分配大小与 16 字节边界对齐。第二个文件中的代码实际上遍历了所有属于新堆栈区域的页面并触及它们。这可能会触发 PAGE_GAURD 异常,操作系统会使用这些异常来增加堆栈。

【讨论】:

【参考方案10】:

这样做会很棘手 - 事实上,除非您对编译器的代码生成有足够的控制权,否则它无法完全安全地完成。您的例程必须操作堆栈,以便在它返回时所有内容都被清除,但堆栈指针仍保留在这样的位置,即内存块仍保留在该位置。

问题在于,除非您可以通知编译器堆栈指针已在您的函数调用中被修改,否则它很可能会决定它可以继续通过堆栈指针引用其他本地(或其他) - 但是偏移量将不正确。

【讨论】:

【参考方案11】:

我推荐“进入”指令。在 286 和更新的处理器上可用(可能在 186 上也可用,我不记得了,但无论如何这些都没有广泛使用)。

【讨论】:

不幸的是,输入指令对于这个目的(在更高级别的语言中实现 alloca)毫无用处,只是因为你没有得到足够的编译器合作。 你绝对不希望 ENTER 在 inline-asm 中,因为它会覆盖 EBP,所以编译器不会知道它的本地变量在哪里。在现代 CPU 上它也非常慢,这就是编译器使用 push ebp/mov ebp,esp/sub esp, N 的原因。所以你真的永远不需要 ENTER,即使在 asm 中编写一个独立的函数。

以上是关于Alloca 实施的主要内容,如果未能解决你的问题,请参考以下文章

项目管理合集丨实施规划丨实施计划书丨实施方案丨实施细则WordPPT,可下载

什么是软件实施?软件实施前景几何?软件实施的面试题有那些?

sap实施顾问基本职责

DDD实施规范

创建实施人员过程(实施三)

实施工程师主要岗位职责