为什么Rust堆栈框架这么大?

如何解决为什么Rust堆栈框架这么大?

我遇到了意外的早期堆栈溢出,并创建了以下程序来测试该问题:

#![feature(asm)]
#[inline(never)]
fn get_rsp() -> usize {
    let rsp: usize;
    unsafe {
        asm! {
            "mov {},rsp",out(reg) rsp
        }
    }
    rsp
}

fn useless_function(x: usize) {
    if x > 0 {
        println!("{:x}",get_rsp());
        useless_function(x - 1);
    }
}

fn main() {
    useless_function(10);
}

这是get_rsp的反汇编(根据cargo-asm):

tests::get_rsp:
 push    rax
 #APP
 mov     rax,rsp
 #NO_APP
 pop     rcx
 ret

我不确定#APP#NO_APP的作用或为什么将rax推入然后弹出rcx的原因,但是函数似乎确实返回了堆栈指针

我很惊讶地发现在调试模式下,两个连续打印的rsp之间的差是192(!),甚至在发布模式下,两者之间的差是128。 据我了解,每次对useless_function的调用都需要存储的是一个usize和一个返回地址,因此,我希望每个堆栈帧的大小大约为16个字节。>

我正在rustc 1.46.0上运行于64位Windows计算机上。

我的结果在计算机上是否一致?如何解释?


使用println!似乎有很大的作用。为了避免这种情况,我更改了程序(感谢@Shepmaster),将值存储在静态数组中:

static mut RSPS: [usize; 10] = [0; 10];

#[inline(never)]
fn useless_function(x: usize) {
    unsafe { RSPS[x] = get_rsp() };
    if x == 0 {
        return;
    }
    useless_function(x - 1);
}

fn main() {
    useless_function(9);
    println!("{:?}",unsafe { RSPS });
}

递归在发布模式下得到了优化,但是在调试模式下,每帧仍占用80个字节,这比我预期的要多。这就是x86上堆栈框架的工作方式吗?其他语言做得更好吗?这似乎效率低下。

解决方法

使用println!之类的格式化工具会在堆栈上创建许多东西。扩展代码中使用的宏:

fn useless_function(x: usize) {
    if x > 0 {
        {
            ::std::io::_print(::core::fmt::Arguments::new_v1(
                &["","\n"],&match (&get_rsp(),) {
                    (arg0,) => [::core::fmt::ArgumentV1::new(
                        arg0,::core::fmt::LowerHex::fmt,)],},));
        };
        useless_function(x - 1);
    }
}

我相信这些结构会占用大部分空间。为了证明这一点,我打印了format_args创建的值的大小,该值由println!使用:

let sz = std::mem::size_of_val(&format_args!("{:x}",get_rsp()));
println!("{}",sz);

这表明它是48个字节。

另请参阅:


类似的事情应该从方程式中删除打印内容,但是编译器/优化器会忽略此处的inline(never)提示,并且无论如何都将其内联,从而导致顺序值都相同。

/// SAFETY:
/// The length of `rsp` and the value of `x` must always match
#[inline(never)]
unsafe fn useless_function(x: usize,rsp: &mut [usize]) {
    if x > 0 {
        *rsp.get_unchecked_mut(0) = get_rsp();
        useless_function(x - 1,rsp.get_unchecked_mut(1..));
    }
}

fn main() {
    unsafe {
        let mut rsp = [0; 10];
        useless_function(rsp.len(),&mut rsp);

        for w in rsp.windows(2) {
            println!("{}",w[0] - w[1]);
        }
    }
}

也就是说,您可以将函数公开,并且无论如何都要看一下它的程序集(轻扫):

playground::useless_function:
    pushq   %r15
    pushq   %r14
    pushq   %rbx
    testq   %rdi,%rdi
    je  .LBB6_3
    movq    %rsi,%r14
    movq    %rdi,%r15
    xorl    %ebx,%ebx

.LBB6_2:
    callq   playground::get_rsp
    movq    %rax,(%r14,%rbx,8)
    addq    $1,%rbx
    cmpq    %rbx,%r15
    jne .LBB6_2

.LBB6_3:
    popq    %rbx
    popq    %r14
    popq    %r15
    retq

但是在调试模式下,每帧仍然占用80字节

比较未优化的程序集:

playground::useless_function:
    subq    $104,%rsp
    movq    %rdi,80(%rsp)
    movq    %rsi,88(%rsp)
    movq    %rdx,96(%rsp)
    cmpq    $0,%rdi
    movq    %rdi,56(%rsp)                  # 8-byte Spill
    movq    %rsi,48(%rsp)                  # 8-byte Spill
    movq    %rdx,40(%rsp)                  # 8-byte Spill
    ja  .LBB44_2
    jmp .LBB44_8

.LBB44_2:
    callq   playground::get_rsp
    movq    %rax,32(%rsp)                  # 8-byte Spill
    xorl    %eax,%eax
    movl    %eax,%edx
    movq    48(%rsp),%rdi                  # 8-byte Reload
    movq    40(%rsp),%rsi                  # 8-byte Reload
    callq   core::slice::<impl [T]>::get_unchecked_mut
    movq    %rax,24(%rsp)                  # 8-byte Spill
    movq    24(%rsp),%rax                  # 8-byte Reload
    movq    32(%rsp),%rcx                  # 8-byte Reload
    movq    %rcx,(%rax)
    movq    56(%rsp),%rdx                  # 8-byte Reload
    subq    $1,%rdx
    setb    %sil
    testb   $1,%sil
    movq    %rdx,16(%rsp)                  # 8-byte Spill
    jne .LBB44_9
    movq    $1,72(%rsp)
    movq    72(%rsp),%rdx
    movq    48(%rsp),8(%rsp)                   # 8-byte Spill
    movq    %rdx,(%rsp)                    # 8-byte Spill
    movq    16(%rsp),%rdi                  # 8-byte Reload
    movq    8(%rsp),%rsi                   # 8-byte Reload
    movq    (%rsp),%rdx                    # 8-byte Reload
    callq   playground::useless_function
    jmp .LBB44_8

.LBB44_8:
    addq    $104,%rsp
    retq

.LBB44_9:
    leaq    str.0(%rip),%rdi
    leaq    .L__unnamed_7(%rip),%rdx
    movq    core::panicking::panic@GOTPCREL(%rip),%rax
    movl    $33,%esi
    callq   *%rax
    ud2
,

此答案显示了未优化的C ++版本在asm中的工作原理。

这可能不会像我对Rust的了解那么多。 apparently Rust使用其自己的ABI /调用约定,因此在Windows上不会具有“影子空间”,从而使其堆栈框架更大。我的答案的第一个版本猜测在针对Windows时,它将遵循Windows调用约定来调用其他Rust函数。我已经调整了措词,但是即使它与Rust可能无关,也没有删除它。

经过进一步研究,至少在2016年Rust的ABI恰好与Windows x64上的平台调用约定相匹配,至少在this random tutorial中对debug-build二进制文件的反汇编代表了任何东西。反汇编中的heap::allocate::h80a36d45ddaa4ae3Lca显然会在RCX和RDX中使用args(溢出并重新加载到堆栈中),然后使用这些args调用另一个函数。调用前在RSP上方保留0x20字节未使用的空间,即影子空间。

如果自2016年以来没有发生任何变化(很可能),我认为这个答案确实反映了Rust在为Windows编译时所做的一些事情。


递归在发布模式下得到了优化,但是在调试模式下,每帧仍占用80个字节,这比我预期的要多。这就是x86上堆栈框架的工作方式吗?其他语言做得更好吗?

是的,C和C ++的性能更好:Windows上每个堆栈帧48或64字节,Linux上32字节。

Windows x64调用约定要求调用者保留32字节的影子空间(返回地址上方基本上未使用的stack-arg空间),以供被调用者使用。但似乎未优化的clang构建可能未利用该影子空间,而是分配了额外的空间来溢出本地var。

此外,返回地址占用8个字节,然后在另一个调用占用另外8个字节之前将堆栈重新对齐16个,因此在Windows上,您希望的最小值是48个字节(除非启用优化,然后再说,则尾递归很容易优化成一个循环)。 GCC编译该代码的C或C ++版本确实实现了这一目标。

针对Linux或使用x86-64 System V ABI,gcc和clang的任何其他x86-64目标进行编译,对于C或C ++版本,每帧管理32个字节。只需ret addr,保存RBP和另外16个字节即可保持对齐,同时还可以腾出空间来溢出8字节x。 (以C或C ++进行编译与asm无关)。


我使用the Godbolt compiler explorer上的Windows调用约定在未优化的C ++版本上尝试了GCC和clang。仅查看useless_function的组件,就无需编写mainget_rsp

#include <stdlib.h>

#define MS_ABI __attribute__((ms_abi))   // for GNU C compilers.  Godbolt link has an ifdeffed version of this

void * RSPS[10] = {0};

MS_ABI void *get_rsp(void);
MS_ABI void useless_function(size_t x) {
    RSPS[x] = get_rsp();
    if (x == 0) {
        return;
    }
    useless_function(x - 1);
}

未优化的clang / LLVM执行push rbp / sub rsp,48,因此每帧总共64个字节(包括返回地址)。按照预测,GCC确实会推送/ sub rsp,32,每帧总共只有48个字节。

因此,显然,未经优化的LLVM确实分配了“不需要的”空间,因为它无法使用调用方分配的影子空间。如果Rust使用了阴影空间,这可能可以解释某些原因,即使在递归函数外部进行打印的情况下,调试模式的Rust版本使用的堆栈空间也可能比我们预期的要多。 (打印对本地人使用很多空间。)

但该解释的一部分还必须包括让一些本地人占用更多空间,例如也许用于指针局部或边界检查? C和C ++非常直接地映射到asm,无需访问任何额外的堆栈空间即可访问全局变量。 (或者甚至是额外的寄存器,当可以将全局数组假定为虚拟地址空间的低2GiB时,因此它的地址可以与其他寄存器一起用作32位带符号移位。)

# clang 10.0.1 -O0,for Windows x64
useless_function(unsigned long):
        push    rbp
        mov     rbp,rsp                  # set up a legacy frame pointer.
        sub     rsp,48                   # reserve enough for shadow space (32) + 16,maintaining stack alignment.
        mov     qword ptr [rbp - 8],rcx   # spill incoming arg to newly reserved space above the shadow space
        call    get_rsp()
...

在堆栈上使用的唯一本地空间用于x,没有作为数组访问一部分的临时对象。只需重新加载x然后重新加载mov qword ptr [8*rcx + RSPS],rax即可存储函数调用返回值。

# GCC10.2 -O0,rsp
        sub     rsp,32                   # just reserve enough for shadow space for callee
        mov     QWORD PTR [rbp+16],rcx   # spill incoming arg to our own shadow space
        call    get_rsp()
...

没有ms_abi属性,GCC和clang都使用sub rsp,16

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-