内存布局黑客

时间:2010-06-30 13:10:05

标签: c gcc memory-management operating-system

我一直关注youtube中的this课程,它讨论的是一些程序员如何使用知识来记忆如何做聪明的事情。 讲座中的一个例子是那样的

#include <stdio.h>
void makeArray();
void printArray();
int main(){
        makeArray();
        printArray();
        return 0;
}
void makeArray(){
    int array[10];
    int i;
    for(i=0;i<10;i++)
        array[i]=i;
}
void printArray(){
    int array[10];
    int i;  
    for(i=0;i<10;i++)
        printf("%d\n",array[i]);
}

这个想法只要两个函数在堆栈段上具有相同的激活记录大小就可以工作并打印0到9之间的数字......但实际上它会打印出类似的东西

134520820
-1079626712
0
1
2
3
4
5
6
7

在乞讨时始终存在这两个值...任何人都可以解释一下??? 我在linux中使用gcc

确切讲座url从5:15开始

3 个答案:

答案 0 :(得分:23)

我很抱歉,但对于那段代码来说绝对没有什么聪明,使用它的人非常愚蠢。


  

附录:

     

或者,有时,有时,非常聪明。观看了在问题更新中链接的视频,这不是一些违反规则的流氓代码猴子。这家伙明白他做得很好。

     

它需要深入了解生成的底层代码,并且如果您的环境发生变化(如编译器,体系结构等),可能很容易破坏(如此处所述和所见)。

     

但是,只要你拥有那些知识,你就可以侥幸逃脱。这不是我建议除了退伍军人以外的任何人,但我可以看到它在非常有限的情况下占有一席之地,说实话我毫无疑问偶尔会有点......务实......比我应该有的在我自己的职业生涯中: - )

     

现在回到你的常规节目......


它在架构,编译器,编译器版本之间是不可移植的,甚至可能在编译器的同一版本中的优化级别,以及未定义的行为(读取未初始化的变量)。

如果您想了解它,最好的办法是检查编译器输出的汇编代码。

但总的来说,最好的办法就是忘掉它并编写标准代码。


例如,此脚本显示gcc如何在不同的优化级别上具有不同的行为:

pax> gcc -o qq qq.c ; ./qq
0
1
2
3
4
5
6
7
8
9

pax> gcc -O3 -o qq qq.c ; ./qq
1628373048
1629343944
1629097166
2280872
2281480
0
0
0
1629542238
1629542245

在gcc的高优化级别(我称之为疯狂的优化级别),这是makeArray函数。它基本上已经发现没有使用数组,因此优化了它的初始化。

_makeArray:
        pushl   %ebp            ; stack frame setup
        movl    %esp, %ebp

                                ; heavily optimised function

        popl    %ebp            ; stack frame tear-down

        ret                     ; and return

我实际上有点惊讶g​​cc甚至将功能存根留在那里。

  

更新:正如Nicholas Knight在评论中指出的那样,该函数仍然存在,因为它必须对链接器可见 - 使得该函数静态导致gcc也删除存根。

如果你在下面的优化级别0检查汇编代码,它会给出一个线索(这不是实际原因 - 见下文)。检查以下代码,您将看到两个函数的堆栈框架设置不同,尽管它们传入的参数完全相同且局部变量相同:

subl    $48, %esp     ; in makeArray
subl    $56, %esp     ; in printArray

这是因为printArray分配了一些额外的空间来存储printf格式字符串的地址和数组元素的地址,每个字节有四个字节,这占据了8个字节(两个32位值)的差异

这是printArray()中数组偏离两个值的最可能的解释。

以下是优化级别0的两个功能,供您享受: - )

_makeArray:
        pushl   %ebp                     ; stack fram setup
        movl    %esp, %ebp
        subl    $48, %esp
        movl    $0, -4(%ebp)             ; i = 0
        jmp     L4                       ; start loop
L5:
        movl    -4(%ebp), %edx
        movl    -4(%ebp), %eax
        movl    %eax, -44(%ebp,%edx,4)   ; array[i] = i
        addl    $1, -4(%ebp)             ; i++
L4:
        cmpl    $9, -4(%ebp)             ; for all i up to and including 9
        jle     L5                       ; continue loop
        leave
        ret
        .section .rdata,"dr"
LC0:
        .ascii "%d\12\0"                 ; format string for printf
        .text

_printArray:
        pushl   %ebp                     ; stack frame setup
        movl    %esp, %ebp
        subl    $56, %esp
        movl    $0, -4(%ebp)             ; i = 0
        jmp     L8                       ; start loop
L9:
        movl    -4(%ebp), %eax           ; get i
        movl    -44(%ebp,%eax,4), %eax   ; get array[i]
        movl    %eax, 4(%esp)            ; store array[i] for printf
        movl    $LC0, (%esp)             ; store format string
        call    _printf                  ; make the call
        addl    $1, -4(%ebp)             ; i++
L8:
        cmpl    $9, -4(%ebp)             ; for all i up to and including 9
        jle     L9                       ; continue loop
        leave
        ret

更新:正如Roddy在评论中指出的那样。这不是导致特定问题的原因,因为在这种情况下,数组实际上位于内存中的相同位置(%ebp-44%ebp在两次调用中相同)。我试图指出的是,具有相同参数列表和相同局部参数的两个函数不一定最终具有相同的堆栈帧布局。

所有需要的是printArray交换其局部变量的位置(包括开发人员创建的显式的任何临时值),你会遇到这个问题。< / p>

答案 1 :(得分:4)

可能GCC生成的代码在调用函数时不会将参数推送到堆栈,而是在堆栈中分配额外的空间。你的'printf'函数调用的参数,“%d \ n”和array [i]在堆栈上占用8个字节,第一个参数是指针,第二个参数是整数。这解释了为什么有两个整数打印不正确。

答案 2 :(得分:4)

永远,永远,永远,永远,永远不会做这样的事情。它不会可靠地工作。你会得到奇怪的错误。它远非便携式。

它可能失败的方式:

0.1。编译器添加额外的隐藏代码

DevStudio,在调试模式下,添加对函数的调用,这些函数检查堆栈以捕获堆栈错误。这些调用将覆盖堆栈上的内容,从而丢失数据。

0.2。有人添加了进入/退出呼叫

一些编译器允许程序员定义在函数入口和函数退出时调用的函数。像(1)这些使用堆栈空间并将覆盖已经存在的内容,丢失数据。

0.3。中断

在main()中,如果在调用makeArray和printArray之间出现中断,则会丢失数据。处理中断时发生的第一件事就是保存cpu的状态。这通常涉及将CPU寄存器和标志推送到堆栈上,是的,您猜对了,覆盖了您的数据。

0.4。编译器很聪明

正如您所见,makeArray中的数组与printArray中的数组位于不同的地址。编译器将其局部变量放在堆栈的不同位置。它使用一个复杂的算法来决定放置变量的位置 - 在堆栈上,寄存器中等等,并且真的不值得尝试弄清楚编译器是如何做到的,因为下一版本的编译器可能会以其他方式执行它。 / p>

总而言之,这些“聪明的伎俩”不是技巧,当然也不聪明。通过在main中声明数组并在两个函数中传递一个引用/指针,你不会丢失任何东西。堆栈用于存储局部变量和函数返回地址。一旦你的数据超出范围(即堆栈顶部缩小了数据),那么数据就会丢失 - 任何事情都可能发生。

为了更多地说明这一点,如果你有不同的功能名称,你的结果可能会有所不同(我只是在这里猜测,好的)。