昨天和一些人聊天,他们说不太懂栈是怎么工作的,不知道怎么查看栈空间。这是有关如何使用GDB查看C程序的堆栈空间的快速教程。我认为Rust程序也类似。但是我这里还是用C语言,因为我觉得用起来比较方便,而且用C语言也比较容易写错程序。我们这里的测试程序是一个简单的C程序,它声明了一些变量并从标准输入中读取了两个字符串。一个字符串在堆上,另一个在堆栈上。#include#includeintmain(){charstack_string[10]="stack";整数x=10;字符*heap_string;heap_string=malloc(50);printf("为堆栈输入一个字符串:");获取(堆栈字符串);printf("为堆输入一个字符串:");获取(堆字符串);printf("堆栈字符串为:%s\n",stack_string);printf("堆字符串为:%s\n",heap_string);printf("xis:%d\n",x);}这个程序使用了一个你可能永远不会用到的非常不安全的函数gets。但我是故意的。当出现问题时,您知道原因。步骤0:编译这个程序我们使用gcc-g-O0test.c-otest命令来编译这个程序。-g选项还会将调试信息编译到已编译的程序中。这将使查看我们的变量更容易。-O0选项告诉gcc不要优化,我想确保我们的x变量没有被优化掉。第一步:启动GDB像这样启动GDB:$gdb./test它会打印出一些GPL信息,然后给出一个提示。让我们在主函数中设置一个断点:(gdb)bmain然后我们可以运行程序:(gdb)bmainStartingprogram:/home/bork/work/homepage/testBreakpoint1,0x000055555555516dinmain()(gdb)runStartingprogram:/home/bork/work/homepage/testBreakpoint1,main()attest.c:44intmain(){好了,现在程序开始运行了。我们可以开始查看堆栈空间。第2步:查看我们变量的地址让我们从了解我们的变量开始。它们在内存中都有一个地址,我们可以这样打印出来:(gdb)p&x$3=(int*)0x7fffffffe27c(gdb)p&heap_string$2=(char**)0x7fffffffe280(gdb)p&stack_string$4=(char(*)[10])0x7fffffffe28e因此,如果我们查看这些地址处的堆栈,我们应该能够看到所有这些变量!概念:堆栈指针我们将需要使用堆栈指针,因此我将尝试快速解释它。有一个名为ESP的x86寄存器称为“堆栈指针”。基本上,它是当前函数的堆栈起始地址。在GDB中,您可以使用$sp来访问它。当您调用新函数或从函数返回时,堆栈指针的值会发生变化。第三步:在main函数的开始,我们看一下栈上的变量首先,我们看一下main函数开始的栈。现在我们堆栈指针的值:(gdb)p$sp$7=(void*)0x7fffffffe270所以我们当前函数的堆栈从0x7fffffffe270开始,很酷。现在,让我们使用GDB在开始后打印出当前函数堆栈的前40个字(即160个字节)。有些内存可能不是堆栈的一部分,因为我不太确定这里的堆栈有多大。但至少开始的地方是堆栈的一部分。我加粗了stack_string、heap_string和x变量的位置并更改了颜色:x是红色字体,起始地址是0x7fffffffe27cheap_string是蓝色字体,起始地址是0x7fffffffe280stack_string是紫色字体,起始地址是0x7fffffffe28e你可能会做一件奇怪的事情这里要注意的是x的值为0x5555,但是我们把x设置为10!那是因为直到我们的main函数运行之后x才真正被设置,而我们现在才处于main的最开始。第3步:运行到第十行代码后,再看看我们的堆栈让我们跳过几行,等待变量真正设置为初始值。在第10行,x应该设置为10。首先我们需要设置另一个断点:(gdb)btest.c:10Breakpoint2at0x5555555551a9:filetest.c,line11。然后继续程序:(gdb)continueContinuing。断点2,main()attest.c:1111printf("Enterastringforthestack:");好的!让我们再看看堆栈中的内容!这里gdb格式化字节的方式略有不同,我其实也不太在意(LCTT译注:可以查看GDB手册中的x命令,可以指定c来控制输出的格式)。这里提醒一下,我们的变量在栈上的位置:x是红色字体,起始地址是0x7fffffffe27cheap_string是蓝色字体,起始地址是0x7fffffffe280stack_string是紫色字体,起始地址是0x7fffffffe28e继续看之前,这里有一些有趣的事情要讨论。stack_string在内存中的表示方式现在(第10行),stack_string被设置为字符串堆栈。让我们看看它在内存中是如何表示的。我们可以这样打印出字符串中的字节:(gdb)x/10xstack_string0x7fffffffe28e:0x730x740x610x630x6b0x000x000x000x7fffffffe296:0x000x00stack是一个长度为5的字符串,对应5个ASCII码——0x73、0x74、0x740x61、0x63和0x6b。0x73是字符s的ASCII码。0x74是t的ASCII码。等等...还有我们可以用x/1s让GDB显示字符串:(gdb)x/1sstack_string0x7fffffffe28e:"stack"Howdoesheap_stringandstack_stringdifference你有没有注意到stack_string和heap_string在栈上的表示有很大不同:stack_string是一段字符串内容(stack)heap_string是指向内存中某个位置的指针下面是heap_string变量在内存中的内容:0xa00x920x550x550x550x550x000x00这些字节其实应该是从右往左读的:因为x86是little-endian模式,heap_string存放的内存地址是0x5555555592a0另外一种查看heap_string存放内存地址的方法是使用p命令直接打印:(gdb)pheap_string$6=0x5555555592a0""字节表示整数x是一个32位整数,可以用0x0a0x000x000x00表示。我们仍然需要反向读取这些字节(就像我们需要反向读取heap_string一样),所以这个数字代表0x000000000a或者0x0a,也就是一个数字10;这允许我将x设置为10。第4步:从标准输入读取。现在我们已经初始化了我们的变量,让我们看看当这个程序运行时堆栈空间将如何变化:printf("Enterastringforthestack:");gets(stack_string);printf("Enterastringfortheheap:");获取(heap_string);我们需要设置另一个断点:(gdb)btest.c:16Breakpoint3at0x555555555205:filetest.c,line16。然后继续程序:(gdb)continueContinuing。我们输入两个字符串,123456789012是存储在栈上的变量,bananas是存储在堆上的变量;让我们先看看stack_string(这里是缓冲区溢出)(gdb)x/1sstack_string0x7fffffffe28e:"123456789012"这看起来很正常,对吧?我们输入的是12345679012,现在也设置为12345679012(LCTT译注:实测gcc8.3环境下会直接segfault)。但是现在发生了一些非常奇怪的事情。这就是我们程序栈空间的内容。有一些内容以紫色突出显示。奇怪的是stack_string只支持10个字节。但是现在当我们输入13个字符时,会发生什么?这是典型的缓冲区溢出,其中stack_string将其自己的数据写入程序的其他位置。在我们的例子中,这并没有造成问题,但它会使您的程序崩溃,或者更糟的是,使您面临非常严重的安全问题。例如,假设stack_string在内存中正好位于heap_string之前。那么我们可能会覆盖heap_string指向的地址。我不确定stack_string之后内存中有什么。但是我们也许可以用它做一些奇怪的事情。当我故意写很多字符时确实检测到缓冲区溢出:./testEnterastringforthestack:01234567891324143Enterastringfortheheap:adsfStackstringis:01234567891324143Heapstringis:adsfxis:10***stacksmashingdetected***:terminatedfish:Job1,'./test'terminatedbysignalSIGABRT(Abort)这里我猜stack_string已经到达了这个函数堆栈的底部,所以额外的字符将被写入另一个内存块。当你故意使用这个安全漏洞时,它被称为“stacksmashing”,某种东西会以某种方式检测到这种情况的发生。我还觉得有趣的是,虽然程序被杀死了,但它并没有在缓冲区溢出发生时立即被杀死——直到缓冲区溢出后再运行几行代码才杀死程序。它是有线的!这就是关于缓冲区溢出的全部内容。现在让我们看看heap_string我们仍然将香蕉输入到heap_string变量中。让我们看看它在内存中的样子。这是我们读取字符串后heap_string在栈空间的样子:需要注意的是这里的值是一个地址。而这个地址并没有变,但是我们来看看指向的内存中的内容。(gdb)x/10x0x5555555592a00x5555555592a0:0x620x610x6e0x610x6e0x610x730x000x5555555592a8:0x000x00看,这是字符串bananas的字节表示。这些字节不在堆栈空间中。它们存在于内存中的堆上。堆和栈到底在哪里?我们已经讨论过栈和堆是不同的内存区域,但是你怎么知道它们在内存中的位置呢?每个进程都有一个名为/proc/$PID/maps的文件,它显示了每个进程的内存映射。在这里你可以看到堆栈和堆。$cat/proc/24963/maps...省略了很多东西...555555559000-55555557a000rw-p0000000000:000[堆]...省略了很多东西...7ffffffde000-7ffffffff000rw-p000000000:000[stack]需要注意的一点是,这里的堆地址是0x5555开头,栈地址是0x7fffff开头。所以很容易区分栈上的地址和堆上的地址。像这样使用gdb真的很有帮助。这有点像旋风之旅,虽然我没有解释所有内容,但希望看到数据在内存中的实际情况可以让您更清楚地了解堆栈的实际情况。我真的建议像这样玩弄gdb——即使你不理解你在内存中看到的一切,我发现实际上像这样在我的程序内存中看到数据会产生抽象概念,如“堆栈”和“堆”和“指针”更容易理解。一些更多的练习后续练习思考堆栈的一些想法(排名不分先后):尝试在test.c中添加另一个函数并在该函数的开头创建一个断点,看看是否可以从main中找到堆栈!当你调用一个函数时,他们说“堆栈变小了”,你能在gdb中看到这种情况吗?从函数返回一个指向堆栈上字符串的指针,看看哪里出了问题。为什么返回指向堆栈上字符串的指针是错误的?尝试在C中引起堆栈溢出,并尝试通过查看gdb中的堆栈溢出来准确理解发生了什么!查看Rust程序中的堆栈并尝试查找变量!在噩梦课程中尝试一些缓冲区溢出挑战。每个问题的答案都写在README文件中,所以如果不想被剧透,请避免先阅读答案。所有这些挑战的想法是给你一个二进制文件,你需要弄清楚如何导致缓冲区溢出,以便它打印出标志字符串。