gcc - C如何分配内存?
问题描述
我试图了解 GDB 的工作原理以及内存是如何分配的。当我运行以下命令时,假设将 72A
写入内存,但是当我在内存中计数时,它只写入 68 A
。然后在写入B的内存之前有4个字节的随机内存。当我A
在打印语句中计算's时,它显示72 A
's。
0xbffff080: 0x14 0x84 0x04 0x08 0x41 0x41 0x41 0x41
0xbffff088: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42
完整的命令如下。
(gdb) run $( python -c "print('A'*72+'BBBB')" )
Starting program: /home/ubuntu/Desktop/test $( python -c "print('A'*72+'BBBB')" )
Breakpoint 2, 0x08048473 in getName (
name=0xbffff32c 'A' <repeats 72 times>, "BBBB") at sample1.c:7
7 printf("Your name is: %s \n", myName);
(gdb) c
Continuing.
Your name is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
Program received signal SIGSEGV, Segmentation fault.
0xbffff32c in ?? ()
(gdb) x/150xb $sp-140
0xbffff038: 0x50 0xf0 0xff 0xbf 0x54 0x82 0x04 0x08
0xbffff040: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff048: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff050: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff058: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff060: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff068: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff070: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff078: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff080: 0x14 0x84 0x04 0x08 0x41 0x41 0x41 0x41
0xbffff088: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42
0xbffff090: 0x2c 0xf3 0xff 0xbf 0x00 0xf0 0xff 0xb7
当我进行进一步测试并添加额外的 4 个字节(4 个C
)时,它会在内存和 print 语句中正确显示。
(gdb) run $( python -c "print('A'*72+'BBBB'+'CCCC')" )
Starting program: /home/ubuntu/Desktop/test $( python -c "print('A'*72+'BBBB'+'CCCC')" )
Breakpoint 2, 0x08048473 in getName (name=0xbffff300 "") at sample1.c:7
7 printf("Your name is: %s \n", myName);
(gdb) c
Continuing.
Your name is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCC
Program received signal SIGSEGV, Segmentation fault.
0x43434343 in ?? ()
(gdb) x/150xb $sp-140
0xbffff02c: 0x54 0x82 0x04 0x08 0x41 0x41 0x41 0x41
0xbffff034: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff03c: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff044: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff04c: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff054: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff05c: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff064: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff06c: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0xbffff074: 0x41 0x41 0x41 0x41 0x42 0x42 0x42 0x42
0xbffff07c: 0x43 0x43 0x43 0x43 0x00 0xf3 0xff 0xbf
0xbffff084: 0x00 0xf0 0xff 0xb7 0xab 0x84
这是代码:
#include <stdio.h>
#include <string.h>
void getName (char* name) {
char myName[64];
strcpy(myName, name);
printf("Your name is: %s \n", myName);
}
int main (int argc, char* argv[]) {
getName(argv[1]);
return 0;
}
反汇编getName
显示缓冲区添加了 88 个字节:
Reading symbols from test...done.
(gdb) disas getName
Dump of assembler code for function getName:
0x0804844d <+0>: push %ebp
0x0804844e <+1>: mov %esp,%ebp
0x08048450 <+3>: sub $0x58,%esp
0x08048453 <+6>: mov 0x8(%ebp),%eax
0x08048456 <+9>: mov %eax,0x4(%esp)
0x0804845a <+13>: lea -0x48(%ebp),%eax
0x0804845d <+16>: mov %eax,(%esp)
0x08048460 <+19>: call 0x8048320 <strcpy@plt>
0x08048465 <+24>: lea -0x48(%ebp),%eax
0x08048468 <+27>: mov %eax,0x4(%esp)
0x0804846c <+31>: movl $0x8048530,(%esp)
0x08048473 <+38>: call 0x8048310 <printf@plt>
0x08048478 <+43>: leave
0x08048479 <+44>: ret
End of assembler dump.
解决方案
由于效率低下,未优化的代码可能会在堆栈上看到额外的填充,但大多数情况下,填充是编译器试图对齐堆栈上的数据的结果。GCC 通常会尝试在可被 16 整除的地址上分配数组。
推送 EBP 后,分配了 0x58 个字节(88 个字节)。由于这条指令,我们可以看到缓冲区从 EBP-0x48 开始:
lea -0x48(%ebp),%eax
然后使用该地址EBP-0x48
来设置堆栈上的参数,以调用strcpy
和printf
。0x48 = 72 字节,尽管缓冲区是 64 字节。还有另外 8 个字节的填充。为什么那里有填充物?因为编译器试图确保myName
缓冲区的开头位于 16 字节边界上。
GCC 可以跟踪堆栈上的内容,但是有关对齐的重要信息来自调用约定(64 位 System V ABI),该约定说在调用函数时(在这种情况下getName
)堆栈必须是16 字节对齐。该call
指令将 4 个字节压入返回地址,然后将 EBP 压入额外的 4 个字节。编译器在 PUSH EBP 之后知道它错位了 8 个字节。64 + 8 字节的填充 + EBP 的 4 + 4 返回地址 = 80。80 可以被 16 整除 (16*5=80)。8 个字节的使用不是任意的。
在 GDB 输出中,您可以看到myName
数组从以 . 结尾的十六进制地址开始0
。任何以 16 结尾的十六进制地址0
都可以被 16 整除,您可以看到缓冲区从 0xbffff040 开始:
0xbffff038: 0x50 0xf0 0xff 0xbf 0x54 0x82 0x04 0x08 0xbffff040: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
综上所述,如果您要覆盖返回地址,它将与开头的偏移量myName
相等,等于 64(数组大小)+ 8(填充)+ 4(堆栈上的 EBP)= 76 字节。在到达可以替换返回地址的点之前,您必须写入 76 个字节的数据。
注意:您可能想知道为什么该myname
数组在堆栈下方有额外的 16 个字节(88-72=16 个字节)。该空间是编译器放置函数调用的值的地方strcpy
,printf
并确保进行的函数调用具有 16 字节对齐的堆栈以符合 64 位 System V ABI。
myName 中间数据异常的原因
我通过准确复制您在我自己的 Ubuntu 14.04 系统上看到的内容来确认以下观察结果。
您还想知道当您插入 72A
和 4B
时,缓冲区中有 4 个意外字节:
0xbffff080:[0x14 0x84 0x04 0x08] 0x41 0x41 0x41 0x41 0xbffff088: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42
我用 . 标记了 4 个字节[]
。您是对的,您可能期望这 4 个字节与其他字节一样0x41
(The letter A
)。发生的事情是,尽管您在命令行上输入的是 76 个字符 (72+4) ,但在末尾strcpy
附加了一个 NUL( \0
) 作为第 77 个字符。这用 0 覆盖了返回地址的低字节!您使用该c
命令在断点后继续运行。调试器在遇到分段错误时终止。发生的事情是RET
指令没有返回到您期望的main
位置,它返回到内存中稍低的位置,因为 NUL 字节被写入返回地址。碰巧你没有看到的是所有执行完之后的指令RET
将数据放回堆栈。这包括将 32 位数据写入曾经的myName
数组中。
当您编写 72 A
、 4B
和 4C
时,您最终覆盖了返回地址,并且当您尝试在 0x43434343 处开始执行代码CCCC
时出现分段错误,如下所示:RET
0x43434343 in ?? ()
0x43434343 不是您具有执行权限的有效地址,因此出现故障。由于RET
未能执行更多代码,程序没有机会覆盖myName
数组。这就解释了为什么缓冲区没有像之前的测试那样被覆盖。
推荐阅读
- r - 将前一个匹配行的值复制到新的匹配行
- html - 是否可以重用从 HTMLCanvasElement.toDataURL() 或 canvas.toBlob() 返回的 DOMString?
- scala - Log4j 在多个节点中创建日志。想在一个节点上创建一个日志
- java - 无法在 Java 11 中使用 java.lang.reflect.Field.modifiers
- django - Django DRF - 按日期分组
- php - 使用 PHP 从 SQL 查询创建 JSON
- c# - 在新的后台线程中执行任务
- python - 如何提取洗衣机前面板的轮廓?
- oop - 如何在 DDD 中组合来自不同限界上下文的数据
- java - 如何通过 ASM 中的核心 api 获取 maxLocals?