由于对齐差异,此程序是否仅在 x32 上崩溃?
Does this program only crash on x32 because of alignment differences?
以下代码摘自here:
#include<stdio.h>
int main()
{
char i = 30;
char j = 123;
char* p = &i;
printf("pointer points to: %p\n", p);
void* q = p;
int * pp = q; /* unsafe, legal C, not C++ */
printf("%d %d\n",i,j);
*pp = -1; /* overwrite memory starting at &i */
printf("%d %d\n",i,j);
printf("pointer points to: %p\n", p);
printf("%d\n", *p);
}
在我的 x32 Linux 机器上,它在最后一行崩溃。在 x64 Linux 上它不会崩溃。是不是因为指针在 x32 上是 4 个字节,在 x64 上是 8 个字节,并且由于对齐要求,在 x64 机器上 char j
和 char *p
之间可能有一个最大 6 字节的空洞被 [=13= 覆盖] 因此 *p
没有任何反应,但在 x32 机器上,这个洞最多只有 2 个字节,因此 *pp = -1
会覆盖 char *p
的前两个字节,从而在取消引用时导致分段错误?这个推理是正确的还是完全愚蠢的?
可能是因为这一行:
*pp = -1; /* overwrite memory starting at &i */
这会导致未定义的行为。
在我的 Visual Studio 2012 年我得到
Run-Time Check Failure #2 - Stack around the variable 'i' was
corrupted.
这是针对 64 位编译代码的结果。
.Ltext0:
.section .rodata
.LC0:
0000 706F696E .string "pointer points to: %p\n"
74657220
706F696E
74732074
6F3A2025
.LC1:
0017 25642025 .string "%d %d\n"
640A00
.LC2:
001e 25640A00 .string "%d\n"
.text
.globl main
main:
.LFB0:
.cfi_startproc
0000 55 pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
0001 4889E5 movq %rsp, %rbp
.cfi_def_cfa_register 6
0004 4883EC20 subq , %rsp
0008 C645E61E movb , -26(%rbp)
000c C645E77B movb 3, -25(%rbp)
0010 488D45E6 leaq -26(%rbp), %rax
0014 488945E8 movq %rax, -24(%rbp)
0018 488B45E8 movq -24(%rbp), %rax
001c 4889C6 movq %rax, %rsi
001f BF000000 movl $.LC0, %edi
00
0024 B8000000 movl [=10=], %eax
00
0029 E8000000 call printf
00
002e 488B45E8 movq -24(%rbp), %rax
0032 488945F0 movq %rax, -16(%rbp)
0036 488B45F0 movq -16(%rbp), %rax
003a 488945F8 movq %rax, -8(%rbp)
003e 0FBE55E7 movsbl -25(%rbp), %edx
0042 0FB645E6 movzbl -26(%rbp), %eax
0046 0FBEC0 movsbl %al, %eax
0049 89C6 movl %eax, %esi
004b BF000000 movl $.LC1, %edi
00
0050 B8000000 movl [=10=], %eax
00
0055 E8000000 call printf
00
005a 488B45F8 movq -8(%rbp), %rax
005e C700FFFF movl $-1, (%rax)
FFFF
0064 0FBE55E7 movsbl -25(%rbp), %edx
0068 0FB645E6 movzbl -26(%rbp), %eax
006c 0FBEC0 movsbl %al, %eax
006f 89C6 movl %eax, %esi
0071 BF000000 movl $.LC1, %edi
00
0076 B8000000 movl [=10=], %eax
00
007b E8000000 call printf
00
0080 488B45E8 movq -24(%rbp), %rax
0084 4889C6 movq %rax, %rsi
0087 BF000000 movl $.LC0, %edi
00
008c B8000000 movl [=10=], %eax
00
0091 E8000000 call printf
00
0096 488B45E8 movq -24(%rbp), %rax
009a 0FB600 movzbl (%rax), %eax
009d 0FBEC0 movsbl %al, %eax
00a0 89C6 movl %eax, %esi
00a2 BF000000 movl $.LC2, %edi
00
00a7 B8000000 movl [=10=], %eax
00
00ac E8000000 call printf
00
00b1 B8000000 movl [=10=], %eax
00
00b6 C9 leave
.cfi_def_cfa 7, 8
00b7 C3 ret
.cfi_endproc
.LFE0:
.Letext0:
0010 和 0014 行是 char* p = &i;
所以你可以看到变量 i
位于内存位置 -26(%rbp);并且只有在那里,因为它只有一个字节。我们还看到变量 p
位于内存位置 -24(%rbp);它从 -24(%rbp) 扩展到 -17(%rbp) 因为它是 64 位架构下的指针。
使用代码 void* q = p; int * pp = q;
我们只是加载 p
的位置;在变量 pp
.
处为 -24(%rbp)
所以 p
和 pp
包含完全相同的值。它们是二进制相等的。而那个值就是寄存器bp的值减去26.
005a 和 005e 行对应 *pp = -1;
请注意指令 movl 的使用方式。这会将 4 个字节的常量复制到方向 -26(%rbp)。所以内存位置 -26(%rbp) 到 -23(%rbp) 将被覆盖。但请记住,变量 p
位于内存位置 -24(%rbp) 到 -17(%rbp)。 所以那个变量的两个字节被覆盖了!!!
这意味着 p
不再指向它之前指向的内容,现在它指向一个不同的内存位置。一个其 2 个不太重要的字节现在是 FFFF。指向比原来更大的内存位置(最多 65535 以上)。这意味着指向堆栈的开头。
果然如你所料,两个字节被覆盖了。即使在 64 位模式下。
所以原因一定是这种改变的方向超出了分配给 32 位模式而非 64 位模式的进程的内存。这与程序加载到内存的方式有关,而不是与程序本身有关。
推理不白痴,但不保证正确
函数栈的布局并不像你想象的那么固定。虽然堆栈指针寄存器只允许有地址 mod 4 或 mod 8,但根据对齐方式,编译器不需要遵循特定的对齐方式。甚至不能保证数据在堆栈中的某个位置。它可能完全驻留在寄存器中!
您想到的堆栈对齐是过程调用标准的一部分,它说明了在一个函数调用另一个函数并希望通过堆栈传输数据之前堆栈必须是什么样子。只有在这种情况下,编译器才需要对齐和填充数据,并确保作为参数传递的指针地址驻留在 4/8 字节边界上。
例如:
您的 x64 可执行文件可能 运行,只是因为编译器将 p 保存在寄存器中而不是堆栈中,因此覆盖堆栈不会影响 p,即使您将整个堆栈 memset 为零。 p 将始终是可以取消引用的有效地址。
因此,您的代码在 32 位机器上也可能 运行 正常,或者在 64 位机器上崩溃。编译器及其优化设置比架构更能决定结果。
以下代码摘自here:
#include<stdio.h>
int main()
{
char i = 30;
char j = 123;
char* p = &i;
printf("pointer points to: %p\n", p);
void* q = p;
int * pp = q; /* unsafe, legal C, not C++ */
printf("%d %d\n",i,j);
*pp = -1; /* overwrite memory starting at &i */
printf("%d %d\n",i,j);
printf("pointer points to: %p\n", p);
printf("%d\n", *p);
}
在我的 x32 Linux 机器上,它在最后一行崩溃。在 x64 Linux 上它不会崩溃。是不是因为指针在 x32 上是 4 个字节,在 x64 上是 8 个字节,并且由于对齐要求,在 x64 机器上 char j
和 char *p
之间可能有一个最大 6 字节的空洞被 [=13= 覆盖] 因此 *p
没有任何反应,但在 x32 机器上,这个洞最多只有 2 个字节,因此 *pp = -1
会覆盖 char *p
的前两个字节,从而在取消引用时导致分段错误?这个推理是正确的还是完全愚蠢的?
可能是因为这一行:
*pp = -1; /* overwrite memory starting at &i */
这会导致未定义的行为。
在我的 Visual Studio 2012 年我得到
Run-Time Check Failure #2 - Stack around the variable 'i' was corrupted.
这是针对 64 位编译代码的结果。
.Ltext0:
.section .rodata
.LC0:
0000 706F696E .string "pointer points to: %p\n"
74657220
706F696E
74732074
6F3A2025
.LC1:
0017 25642025 .string "%d %d\n"
640A00
.LC2:
001e 25640A00 .string "%d\n"
.text
.globl main
main:
.LFB0:
.cfi_startproc
0000 55 pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
0001 4889E5 movq %rsp, %rbp
.cfi_def_cfa_register 6
0004 4883EC20 subq , %rsp
0008 C645E61E movb , -26(%rbp)
000c C645E77B movb 3, -25(%rbp)
0010 488D45E6 leaq -26(%rbp), %rax
0014 488945E8 movq %rax, -24(%rbp)
0018 488B45E8 movq -24(%rbp), %rax
001c 4889C6 movq %rax, %rsi
001f BF000000 movl $.LC0, %edi
00
0024 B8000000 movl [=10=], %eax
00
0029 E8000000 call printf
00
002e 488B45E8 movq -24(%rbp), %rax
0032 488945F0 movq %rax, -16(%rbp)
0036 488B45F0 movq -16(%rbp), %rax
003a 488945F8 movq %rax, -8(%rbp)
003e 0FBE55E7 movsbl -25(%rbp), %edx
0042 0FB645E6 movzbl -26(%rbp), %eax
0046 0FBEC0 movsbl %al, %eax
0049 89C6 movl %eax, %esi
004b BF000000 movl $.LC1, %edi
00
0050 B8000000 movl [=10=], %eax
00
0055 E8000000 call printf
00
005a 488B45F8 movq -8(%rbp), %rax
005e C700FFFF movl $-1, (%rax)
FFFF
0064 0FBE55E7 movsbl -25(%rbp), %edx
0068 0FB645E6 movzbl -26(%rbp), %eax
006c 0FBEC0 movsbl %al, %eax
006f 89C6 movl %eax, %esi
0071 BF000000 movl $.LC1, %edi
00
0076 B8000000 movl [=10=], %eax
00
007b E8000000 call printf
00
0080 488B45E8 movq -24(%rbp), %rax
0084 4889C6 movq %rax, %rsi
0087 BF000000 movl $.LC0, %edi
00
008c B8000000 movl [=10=], %eax
00
0091 E8000000 call printf
00
0096 488B45E8 movq -24(%rbp), %rax
009a 0FB600 movzbl (%rax), %eax
009d 0FBEC0 movsbl %al, %eax
00a0 89C6 movl %eax, %esi
00a2 BF000000 movl $.LC2, %edi
00
00a7 B8000000 movl [=10=], %eax
00
00ac E8000000 call printf
00
00b1 B8000000 movl [=10=], %eax
00
00b6 C9 leave
.cfi_def_cfa 7, 8
00b7 C3 ret
.cfi_endproc
.LFE0:
.Letext0:
0010 和 0014 行是 char* p = &i;
所以你可以看到变量 i
位于内存位置 -26(%rbp);并且只有在那里,因为它只有一个字节。我们还看到变量 p
位于内存位置 -24(%rbp);它从 -24(%rbp) 扩展到 -17(%rbp) 因为它是 64 位架构下的指针。
使用代码 void* q = p; int * pp = q;
我们只是加载 p
的位置;在变量 pp
.
处为 -24(%rbp)
所以 p
和 pp
包含完全相同的值。它们是二进制相等的。而那个值就是寄存器bp的值减去26.
005a 和 005e 行对应 *pp = -1;
请注意指令 movl 的使用方式。这会将 4 个字节的常量复制到方向 -26(%rbp)。所以内存位置 -26(%rbp) 到 -23(%rbp) 将被覆盖。但请记住,变量 p
位于内存位置 -24(%rbp) 到 -17(%rbp)。 所以那个变量的两个字节被覆盖了!!!
这意味着 p
不再指向它之前指向的内容,现在它指向一个不同的内存位置。一个其 2 个不太重要的字节现在是 FFFF。指向比原来更大的内存位置(最多 65535 以上)。这意味着指向堆栈的开头。
果然如你所料,两个字节被覆盖了。即使在 64 位模式下。
所以原因一定是这种改变的方向超出了分配给 32 位模式而非 64 位模式的进程的内存。这与程序加载到内存的方式有关,而不是与程序本身有关。
推理不白痴,但不保证正确
函数栈的布局并不像你想象的那么固定。虽然堆栈指针寄存器只允许有地址 mod 4 或 mod 8,但根据对齐方式,编译器不需要遵循特定的对齐方式。甚至不能保证数据在堆栈中的某个位置。它可能完全驻留在寄存器中!
您想到的堆栈对齐是过程调用标准的一部分,它说明了在一个函数调用另一个函数并希望通过堆栈传输数据之前堆栈必须是什么样子。只有在这种情况下,编译器才需要对齐和填充数据,并确保作为参数传递的指针地址驻留在 4/8 字节边界上。
例如: 您的 x64 可执行文件可能 运行,只是因为编译器将 p 保存在寄存器中而不是堆栈中,因此覆盖堆栈不会影响 p,即使您将整个堆栈 memset 为零。 p 将始终是可以取消引用的有效地址。
因此,您的代码在 32 位机器上也可能 运行 正常,或者在 64 位机器上崩溃。编译器及其优化设置比架构更能决定结果。