C内存是怎么分配的?
How is memory assigned C?
我想了解 GDB 是如何工作的以及内存是如何分配的。当我运行下面的命令时,假设要写入72A
的内存,但是当我在内存中计数时,它只写入68A
的。然后在写入 B 的内存之前有一些随机内存的 4 个字节。当我在打印语句中计算 A
时,它显示 72 A
。
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
's)时,它在内存和打印语句中正确显示。
(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 [=14=]x58,%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 [=14=]x8048530,(%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-bit System V ABI),它表示在调用函数时(在本例中 getName
) 堆栈必须是 16 字节对齐的。 call
指令为 return 地址压入 4 个字节,然后 EBP 被压入额外的 4 个字节。编译器知道在 PUSH EBP 之后它错位了 8 个字节。 64 + 8 个字节的填充 + 4 个用于 EBP + 4 return 地址 = 80。80 可以被 16 整除(16*5=80)。 8 字节的使用不是任意的。
在 GDB 输出中,您可以看到 myName
数组从以 0
结尾的十六进制地址开始。任何以 0
结尾的十六进制地址都可以被 16 整除,您可以看到缓冲区从 0xbffff040:
开始
0xbffff038: 0x50 0xf0 0xff 0xbf 0x54 0x82 0x04 0x08
0xbffff040: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
综上所述,如果您要覆盖 return 地址,它将与 myName
开头的偏移量等于 64(数组大小)+ 8(填充)+ 4(堆栈上的 EBP)= 76 字节。在到达可以替换 return 地址的位置之前,您必须写入 76 个字节的数据。
注意:您可能想知道为什么 myname
数组在堆栈下方有额外的 16 个字节(88-72=16 个字节)。 space 是编译器为 strcpy
和 printf
等函数调用放置值的地方,并确保进行的函数调用具有 16 字节对齐的堆栈以符合 64 位系统 V ABI。
myName 中间数据异常的原因
我通过完全重现您在我自己的 Ubuntu 14.04 系统上看到的内容,证实了以下观察结果。
您还想知道当您插入 72 个 A
和 4 个 B
时缓冲区中有 4 个意外字节:
0xbffff080:[0x14 0x84 0x04 0x08] 0x41 0x41 0x41 0x41
0xbffff088: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42
我用 []
标记了 4 个字节。你是对的,你可能希望这 4 个字节像其他字节一样是 0x41
(字母 A
)。发生的事情是,尽管您在命令行上输入的是 76 个字符 (72+4),但 strcpy
在末尾附加了一个 NUL([=33=]
) 作为第 77 个字符。这用 0 覆盖了 return 地址的低字节!您使用 c
命令在断点后继续 运行。调试器在遇到分段错误时终止。发生的事情是 RET
指令没有 return 返回到您在 main
中预期的位置,它 returned 到内存中稍低的位置,因为 NUL 字节是写入 return 地址。碰巧你没有看到的是在 RET
之后执行的将数据放回堆栈的所有指令。这包括将 32 位数据写入曾经的 myName
数组。
当您写了 72 个 A
、4 个 B
和 4 个 C
时,您最终用 [=42 覆盖了 return 地址=] 当 RET
试图在 0x43434343 处开始执行代码时出现分段错误,如下所示:
0x43434343 in ?? ()
0x43434343 不是您具有执行权限的有效地址,因此出现故障。因为 RET
无法执行更多代码,所以程序没有机会覆盖 myName
数组。这解释了为什么缓冲区没有像之前的测试那样被覆盖。
我想了解 GDB 是如何工作的以及内存是如何分配的。当我运行下面的命令时,假设要写入72A
的内存,但是当我在内存中计数时,它只写入68A
的。然后在写入 B 的内存之前有一些随机内存的 4 个字节。当我在打印语句中计算 A
时,它显示 72 A
。
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
's)时,它在内存和打印语句中正确显示。
(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 [=14=]x58,%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 [=14=]x8048530,(%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-bit System V ABI),它表示在调用函数时(在本例中 getName
) 堆栈必须是 16 字节对齐的。 call
指令为 return 地址压入 4 个字节,然后 EBP 被压入额外的 4 个字节。编译器知道在 PUSH EBP 之后它错位了 8 个字节。 64 + 8 个字节的填充 + 4 个用于 EBP + 4 return 地址 = 80。80 可以被 16 整除(16*5=80)。 8 字节的使用不是任意的。
在 GDB 输出中,您可以看到 myName
数组从以 0
结尾的十六进制地址开始。任何以 0
结尾的十六进制地址都可以被 16 整除,您可以看到缓冲区从 0xbffff040:
0xbffff038: 0x50 0xf0 0xff 0xbf 0x54 0x82 0x04 0x08 0xbffff040: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
综上所述,如果您要覆盖 return 地址,它将与 myName
开头的偏移量等于 64(数组大小)+ 8(填充)+ 4(堆栈上的 EBP)= 76 字节。在到达可以替换 return 地址的位置之前,您必须写入 76 个字节的数据。
注意:您可能想知道为什么 myname
数组在堆栈下方有额外的 16 个字节(88-72=16 个字节)。 space 是编译器为 strcpy
和 printf
等函数调用放置值的地方,并确保进行的函数调用具有 16 字节对齐的堆栈以符合 64 位系统 V ABI。
myName 中间数据异常的原因
我通过完全重现您在我自己的 Ubuntu 14.04 系统上看到的内容,证实了以下观察结果。
您还想知道当您插入 72 个 A
和 4 个 B
时缓冲区中有 4 个意外字节:
0xbffff080:[0x14 0x84 0x04 0x08] 0x41 0x41 0x41 0x41 0xbffff088: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42
我用 []
标记了 4 个字节。你是对的,你可能希望这 4 个字节像其他字节一样是 0x41
(字母 A
)。发生的事情是,尽管您在命令行上输入的是 76 个字符 (72+4),但 strcpy
在末尾附加了一个 NUL([=33=]
) 作为第 77 个字符。这用 0 覆盖了 return 地址的低字节!您使用 c
命令在断点后继续 运行。调试器在遇到分段错误时终止。发生的事情是 RET
指令没有 return 返回到您在 main
中预期的位置,它 returned 到内存中稍低的位置,因为 NUL 字节是写入 return 地址。碰巧你没有看到的是在 RET
之后执行的将数据放回堆栈的所有指令。这包括将 32 位数据写入曾经的 myName
数组。
当您写了 72 个 A
、4 个 B
和 4 个 C
时,您最终用 [=42 覆盖了 return 地址=] 当 RET
试图在 0x43434343 处开始执行代码时出现分段错误,如下所示:
0x43434343 in ?? ()
0x43434343 不是您具有执行权限的有效地址,因此出现故障。因为 RET
无法执行更多代码,所以程序没有机会覆盖 myName
数组。这解释了为什么缓冲区没有像之前的测试那样被覆盖。