我也明白堆栈的底部对应最大的地址,顶部对应最小的地址。
所以,让我们说我有这个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>
答案 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的答案不同,后者可能与嵌入式控制器的答案不同等。
所有语言定义都指定对象的生存期(分配对象的存储时间和持续时间)以及链接和可见性标识符的em>(链接控制相同标识符的多个实例是否引用同一对象,可见性控制该标识符是否在给定点可用)。
说了这么多,几乎所有你可能会使用的桌面或服务器系统都会有一个运行时堆栈。此外,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中,这对于设置基于缓冲区溢出的攻击非常重要。