堆栈是如何工作的?

时间:2017-01-11 17:15:02

标签: c stack

根据我的理解,堆栈在函数中用于存储所有声明的局部变量。

我也明白堆栈的底部对应最大的地址,顶部对应最小的地址。

所以,让我们说我有这个C程序:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]){
    FILE *file1 = fopen("~/file.txt", "rt");
    char buffer[10];
    printf(argv[1]);
    fclose(file1);
    return 0;
}

指针在哪里命名&#34; file1&#34;在堆栈中比较名为&#34; buffer&#34;的指针?是堆叠中的上部(较小的地址),还是下部(较大的地址)?

另外,我知道在给出格式化args(如printf()%d)时%s将在堆栈上读取,但在此示例中它将从何处开始读取?< / p>

3 个答案:

答案 0 :(得分:2)

维基文章:

http://en.wikipedia.org/wiki/Stack_(abstract_data_type)

wiki文章对一堆对象进行了类比,其中堆栈的顶部是您可以看到(窥视)或删除(弹出)的唯一对象,并且您可以在其中添加(推送)另一个对象。 / p>

对于堆栈的典型实现,堆栈从某个地址开始,当元素被压入堆栈时,地址会减少。推送通常在将元素存储到堆栈之前递减堆栈指针,并且pop通常从堆栈加载元素并在之后递增堆栈指针。

然而,堆栈也可能向上增长,其中push存储元素然后递增堆栈指针,并且pop会在之前递减堆栈指针,然后从堆栈加载元素。这是使用数组实现软件堆栈的常用方法,其中堆栈指针可以是指针或索引。

回到最初的问题,对堆栈上的局部变量的排序没有规则。通常,从堆栈指针中减去所有局部变量的总大小,并将局部变量作为堆栈指针的偏移量(或堆栈指针的寄存器副本,如bp,ebp或rbp)进行访问。 X86处理器)。

答案 1 :(得分:2)

C language definition没有指定如何在内存中布置对象,也没有指定如何将参数传递给函数(单词&#34; stack&#34;和&#34;堆不会出现在语言定义本身的任何地方。这完全是编译器和底层平台的一个功能。 x86的答案可能与M68K的答案不同,后者可能与MIPS的答案不同,后者可能与SPARC的答案不同,后者可能与嵌入式控制器的答案不同等。

所有语言定义都指定对象的生存期(分配对象的存储时间和持续时间)以及链接可见性(链接控制相同标识符的多个实例是否引用同一对象,可见性控制该标识符是否在给定点可用)。

说了这么多,几乎所有你可能会使用的桌面或服务器系统都会有一个运行时堆栈。此外,C最初是在具有运行时堆栈的系统上开发的,其大部分行为当然暗示堆栈模型。 C编译器将是一个在没有使用运行时堆栈的系统上实现的bugger。

  

我也明白堆栈的底部对应最大的地址,顶部对应最小的地址。

这根本不是真的。堆栈的顶部只是最近被推送的地方。堆栈元素甚至不必在内存中连续(例如,当使用堆栈的链接列表实现时)。在x86上,运行时堆栈向下增长&#34;向下&#34; (减少地址),但不要认为这是普遍的。

  

指针在哪里命名&#34; file1&#34;在堆栈中比较名为&#34; buffer&#34;的指针?是堆叠中的上部(较小的地址),还是下部(较大的地址)?

首先,编译器不需要按照声明的顺序在内存中布局不同的对象;它可以重新排序这些对象以最小化填充和对齐问题(struct成员必须按声明的顺序布局,但可能有未使用的&#34;填充&#34;字节成员之间)。

其次,只有file1是一个指针。 buffer是一个数组,因此只为数组元素本身分配空间 - 没有为任何指针留出空间。

  

另外,我知道printf()在给出格式args(比如%d或%s)时会在堆栈上读取,但是在这个例子中它会在哪里开始读取?

它可能无法从堆栈中读取所有的参数。例如,x86-64上的Linux使用System V AMD64 ABI calling convention,它通过寄存器传递前六个参数。

如果你真的好奇好奇特定平台上的内容,你需要a)阅读该平台的调用约定,并b)查看生成的内容机器代码。大多数编译器都有输出机器代码清单的选项。例如,我们可以将您的程序编译为

gcc -S file.c

创建一个名为file.s的文件,其中包含以下(轻微编辑的)输出:

        .file   "file.c"
        .section        .rodata
.LC0:
        .string "rt"
.LC1:
        .string "~/file.txt"
        .text
.globl main
        .type   main, @function
main:
.LFB2:
        pushq   %rbp                 ;; save the current base (frame) pointer
.LCFI0:
        movq    %rsp, %rbp           ;; make the stack pointer the new base pointer
.LCFI1:
        subq    $48, %rsp            ;; allocate an additional 48 bytes on the stack
.LCFI2:
        movl    %edi, -36(%rbp)      ;; since we use the contents of the %rdi(%edi) and %rsi(esi) registers
        movq    %rsi, -48(%rbp)      ;; below, we need to preserve their contents on the stack frame before overwriting them
        movl    $.LC0, %esi          ;; Write the *second* argument of fopen to esi
        movl    $.LC1, %edi          ;; Write the *first* argument of fopen to edi
        call    fopen                ;; arguments to fopen are passed via register, not the stack
        movq    %rax, -8(%rbp)       ;; save the result of fopen to file1
        movq    $0, -32(%rbp)        ;; zero out the elements of buffer (I added
        movw    $0, -24(%rbp)        ;; an explicit initializer to your code)
        movq    -48(%rbp), %rax      ;; copy the pointer value stored in argv to rax
        addq    $8, %rax             ;; offset 8 bytes (giving us the address of argv[1])
        movq    (%rax), %rdi         ;; copy the value rax points to to rdi
        movl    $0, %eax             
        call    printf               ;; like with fopen, arguments to printf are passed via register, not the stack
        movq    -8(%rbp), %rdi       ;; copy file1 to rdi
        call    fclose               ;; again, arguments are passed via register
        movl    $0, %eax
        leave
        ret

现在,这是针对我的特定平台,即x86-64上的Linux(SLES-10)。这不适用于不同的硬件/操作系统组合。

修改

刚才意识到我遗漏了一些重要的东西。

符号 N reg )表示从存储在寄存器 reg 中的地址偏移 N 字节(基本上, reg 充当指针)。 %rbp是基(帧)指针 - 它基本上充当&#34;句柄&#34;对于当前的堆栈帧。局部变量和函数参数(假设它们存在于堆栈中)通过偏移%rbp中存储的地址来访问。在x86上,局部变量通常具有与%rbp的负偏移,而函数参数具有正偏移。

file1的内存从-8(%rbp)开始(x86-64上的指针是64位宽,因此我们需要8个字节来存储它)。根据这些行很容易确定

    call    fopen                
    movq    %rax, -8(%rbp)       

在x86上,函数返回值写入%rax%eax%eax%rax的低32位)。因此,fopen的结果会写入%rax,我们会将%rax的内容复制到-8(%rbp)

buffer的位置确定起来有点棘手,因为您不会对此做任何事情。我添加了一个显式初始值设定项(char buffer[10] = {0};),只是为了生成一些访问它的指令,而这些是

        movq    $0, -32(%rbp)       
        movw    $0, -24(%rbp)       

由此,我们可以确定buffer-32(%rbp)开始。有14个字节的未使用&#34;填充&#34; buffer结尾与file1开头之间的空格。

同样,这就是我的特定系统上发生的事情;你可能会看到不同的东西。

答案 2 :(得分:-2)

非常依赖于实施但仍在附近。在faxt中,这对于设置基于缓冲区溢出的攻击非常重要。