在运行时给出其大小的向量的堆栈空间? (C代码)

Posted

技术标签:

【中文标题】在运行时给出其大小的向量的堆栈空间? (C代码)【英文标题】:stack space for a vector that its size is given at runtime? (C code) 【发布时间】:2020-10-20 23:03:08 【问题描述】:

假设这个 C 代码:

int main()
    int n;
    scanf("%d\n", &n);

    int a[n];
    int i;
    
    for (i = 0; i<n; i++)
        a[i] = 1;
    


我们有一个在堆栈空间中的向量,但是直到执行时间(直到用户给变量 n 赋值)我们才知道向量的大小。所以我的问题是:何时以及如何为堆栈部分中的该向量保留空间?

直到现在我才明白堆栈空间是在编译时保留的,而堆空间是在运行时保留的(使用 malloc 之类的函数)。但是直到运行时我们才能知道这个向量的大小。

我认为可以做的是在知道它的那一刻从堆栈指针中减去 n 的值,从而扩大该函数的堆栈以使向量适合(我提到的这个减法将是仅在汇编代码中可见)。

但是我一直在做一些测试来观察 /proc/[pid]/maps 的内容。并且进程的堆栈空间没有改变,所以我认为(在汇编代码中将 n*sizeof(int) 减去堆栈顶部的指令)没有完成。我在 main 函数的开头和结尾都看过 /proc/[pid]/maps 的内容。

如果我为 x86 (gcc -m32 -o test.c) 汇编这段代码,你会得到以下汇编代码(以备不时之需):

.file   "test.c"
    .text
    .section    .rodata
.LC0:
    .string "%d\n"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    leal    4(%esp), %ecx
    .cfi_def_cfa 1, 0
    andl    $-16, %esp
    pushl   -4(%ecx)
    pushl   %ebp
    .cfi_escape 0x10,0x5,0x2,0x75,0
    movl    %esp, %ebp
    pushl   %esi
    pushl   %ebx
    pushl   %ecx
    .cfi_escape 0xf,0x3,0x75,0x74,0x6
    .cfi_escape 0x10,0x6,0x2,0x75,0x7c
    .cfi_escape 0x10,0x3,0x2,0x75,0x78
    subl    $44, %esp
    call    __x86.get_pc_thunk.ax
    addl    $_GLOBAL_OFFSET_TABLE_, %eax
    movl    %gs:20, %ecx
    movl    %ecx, -28(%ebp)
    xorl    %ecx, %ecx
    movl    %esp, %edx
    movl    %edx, %esi
    subl    $8, %esp
    leal    -44(%ebp), %edx
    pushl   %edx
    leal    .LC0@GOTOFF(%eax), %edx
    pushl   %edx
    movl    %eax, %ebx
    call    __isoc99_scanf@PLT
    addl    $16, %esp
    movl    -44(%ebp), %eax
    leal    -1(%eax), %edx
    movl    %edx, -36(%ebp)
    sall    $2, %eax
    leal    3(%eax), %edx
    movl    $16, %eax
    subl    $1, %eax
    addl    %edx, %eax
    movl    $16, %ebx
    movl    $0, %edx
    divl    %ebx
    imull   $16, %eax, %eax
    subl    %eax, %esp
    movl    %esp, %eax
    addl    $3, %eax
    shrl    $2, %eax
    sall    $2, %eax
    movl    %eax, -32(%ebp)
    movl    $0, -40(%ebp)
    jmp .L2
.L3:
    movl    -32(%ebp), %eax
    movl    -40(%ebp), %edx
    movl    $1, (%eax,%edx,4)
    addl    $1, -40(%ebp)
.L2:
    movl    -44(%ebp), %eax
    cmpl    %eax, -40(%ebp)
    jl  .L3
    movl    %esi, %esp
    movl    $0, %eax
    movl    -28(%ebp), %ecx
    xorl    %gs:20, %ecx
    je  .L5
    call    __stack_chk_fail_local
.L5:
    leal    -12(%ebp), %esp
    popl    %ecx
    .cfi_restore 1
    .cfi_def_cfa 1, 0
    popl    %ebx
    .cfi_restore 3
    popl    %esi
    .cfi_restore 6
    popl    %ebp
    .cfi_restore 5
    leal    -4(%ecx), %esp
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .section    .text.__x86.get_pc_thunk.ax,"axG",@progbits,__x86.get_pc_thunk.ax,comdat
    .globl  __x86.get_pc_thunk.ax
    .hidden __x86.get_pc_thunk.ax
    .type   __x86.get_pc_thunk.ax, @function
__x86.get_pc_thunk.ax:
.LFB1:
    .cfi_startproc
    movl    (%esp), %eax
    ret
    .cfi_endproc
.LFE1:
    .hidden __stack_chk_fail_local
    .ident  "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
    .section    .note.GNU-stack,"",@progbits

【问题讨论】:

评论不用于扩展讨论;这个对话是moved to chat。 【参考方案1】:

首先,您的代码严重损坏:n 直到之后才被设置,它用于设置int vector[n]; 的大小。之后更改n 并不会 更改数组维度。可变长度数组是 C99 的一项特性,C99 消除了将声明放在块中任何其他语句之前的需要,使您可以将 scanf 放入 n before @987654328 @ 语句在堆栈上为该大小的数组保留空间。

直到现在我才明白堆栈空间是在编译时保留的,而堆空间是在运行时保留的

总堆栈区域在程序启动时保留。根据操作系统,为堆栈增长保留的空间量由操作系统设置选择,不是可执行文件中的元数据。 (例如在 Linux 中,通过 ulimit -s 设置初始线程的堆栈,pthreads 选择为每个线程堆栈分配多少空间。)

堆栈帧的布局在编译时是固定的(局部变量相对于彼此的位置),但每次函数运行时都会发生实际分配。这就是函数可以递归和可重入的方式!这也是使堆栈成为堆栈的原因:在最后为当前函数腾出空间,在返回之前立即释放它。 (可变长度数组和alloca 具有运行时可变大小,因此编译器通常会将它们置于其他局部变量之下。)

只有静态存储是在编译时真正保留/分配的。 (全局和static 变量。)

(ISO C 不需要一个实际的堆栈,只是自动存储变量生命周期的 LIFO 语义。一些 ISA 上的一些实现基本上为堆栈帧动态分配空间,例如 malloc,而不是使用堆栈。)

这排除了在编译时为局部变量静态分配空间。在大多数 C 实现中,它们与 x86-64 sub rsp, 24 或其他任何东西都在堆栈上。当然,局部变量的布局相对于彼此在编译时固定的,在大分配内部,因此编译器不需要编写存储指向对象的指针的代码,它们只是发出使用诸如[rsp + 4]之类的寻址模式的指令。

所以我的问题是:何时以及如何为堆栈部分中的该向量保留空间?

逻辑上在 C 抽象机中:当到达int vector[n] 语句时,在此函数的执行中。 相比之下,固定大小的对象存在于封闭范围的顶部。

因此,您的示例被严重破坏了。你让n 保持未初始化直到之后 VLA 被分配!!编译您的代码并启用警告以捕获此类问题。 scanf 应该在 int vector[n] 之前。 (另外,不要将普通数组称为“向量”,这对于了解 C++ 的人来说是错误的。)

但在这种情况下,C 和 x86 中提到局部变量应按其声明顺序放置的规则将不被遵守。

没有这样的规则。在 ISO C 中,甚至写入 vector &lt; &amp;n 并比较单独对象的地址都是未定义的行为。 (C++ 允许 std::less 这样做;C 没有等效的 Does C have an equivalent of std::less from C++?)。

C 编译器可以根据自己的选择来布局其堆栈帧,例如将小对象分组在一起,以避免在填充上浪费空间以对齐更大更对齐的对象。

x86 asm 根本没有变量声明。作为程序员(或 C 编译器),您可以编写移动堆栈指针的指令,并使用内存寻址模式访问您想要访问的内存。通常,您会以实现“变量”的高级概念的方式执行此操作。

例如,让我们创建一个将 n 作为函数 arg 的函数版本,而不是使用 scanf。

#include <stdio.h>

void use_mem(void*);   // compiler can't optimize away calls to this unknown function

void foo(int size) 
    int n = size;  // uninitialized was UB
    int array[n];
    int i;
    i = 5;     // optimizes away, i is kept in a register

    //scanf("%d\n", &n);  // read some different size later???  makes no sense
    for (i = 0; i<n; i++)
        array[i] = 1;
    
    use_mem(array);  // make the stores not be dead

On Godbolt with GCC10.1 -O2 -Wall,适用于 x86-64 System V:

foo(int):
        push    rbp
        movsx   rax, edi             # sign-extend n
        lea     rax, [15+rax*4]      # round size up
        and     rax, -16             # to a multiple of 16, to main stack alignment
        mov     rbp, rsp            # finish setting up a frame pointer
        sub     rsp, rax             # allocate space for array[]
        mov     r8, rsp              # keep a pointer to it
        test    edi, edi             # if ( n==0 ) skip the loop
        jle     .L2
        mov     edi, edi             # zero-extend n
        mov     rax, r8              # int *p = array
        lea     rdx, [r8+rdi*4]      # endp = &array[(unsigned)n]
.L3:                                 # do
        mov     DWORD PTR [rax], 1     # *p = 1
        add     rax, 4                 # pointer increment
        cmp     rax, rdx
        jne     .L3                  # while(p != endp)
.L2:
        mov     rdi, r8             # pass a pointer to the VLA
        call    use_mem(void*)
        leave                       # tear down frame pointer / stack frame
        ret

请注意,当call use_mem 运行时,array[n] 空间位于堆栈指针上方,即“已分配”。

如果use_mem 回调到这个函数中,另一个具有自己大小的 VLA 实例将被分配到堆栈上。

leave 指令就是mov rsp, rbp / pop rbp,因此它将堆栈指针设置为指向已分配空间的上方,de分配它。

【讨论】:

(这个答案 3/4 写在浏览器选项卡中,最终完成了 FWIW。)【参考方案2】:

您可以阅读有关该问题的 cmets,感谢 PeterCordes 的帮助,这些问题已经解决了我的问题。基本上会发生的是,堆栈中需要数组的空间将在运行时在数组声明的精确时刻保留(因为此时 n 是一个已知值)。我们将在汇编代码中有一条指令,即 stackPointer = stackPointer - n * sizeof(int)。

【讨论】:

【参考方案3】:

这是特定于平台的,但通常会在程序启动时保留空间,并且您有最大堆栈大小。在 Windows 上,默认最大值为 1MB according to Microsoft,您可以使用链接器设置(在 Visual Studio 的项目属性中)更改它。

如果你的程序是多线程的,其他线程启动时会保留堆栈空间。

如果您尝试使用比实际更多的堆栈空间,那么通常情况下,您的程序会崩溃,这可能是也可能不是安全漏洞(即让人们侵入您的程序) - 请参阅“堆栈冲突”。

【讨论】:

以上是关于在运行时给出其大小的向量的堆栈空间? (C代码)的主要内容,如果未能解决你的问题,请参考以下文章

为啥当我将其大小初始化为 5 并且只给出 5 个元素时,向量的大小会增加到 10?

堆栈内存分配的区别

转:C语言申请内存时堆栈大小限制

堆栈大小估计

进程空间分配和堆栈大小

处于释放模式时,C ++向量未初始化为空