printf() 影响缓冲区溢出情况

printf() affecting buffer overflow situation

我有一个简单的程序,初始化一个c风格的字符串,然后初始化一个字符。然后我使用函数 strcpy 导致缓冲区溢出情况,这似乎会覆盖字符变量 x 的内存内容(假设它存储在相邻内存中)。

char str[] = "Testt";
char x = 'X';

// print address and value of str
printf("%p: ", &str);
printf("%s\n", str);

// print value of x
printf("%c\n", x);

// cause buffer overflow
strcpy(str, "Hello world");

// print address and value of str
printf("%p: ", &str);
printf("%s\n", str);

// print address and value of x
// printf("%p: ", &x);
printf("%c\n", x);
return 0;

当 运行 时,此代码生成类似于

的输出
0061FF29: Testt
X
0061FF29: Hello world
w

这种情况说明确实发生了缓冲区溢出,导致x变量的值由'X'变为'w'

但是,如果我删除倒数第三行的注释 // printf("%p: ", &x);,缓冲区溢出不会导致 x 变量被覆盖。

为清楚起见,这里是代码(注意倒数第三行的变化)

char str[] = "Testt";
char x = 'X';

// print address and value of str
printf("%p: ", &str);
printf("%s\n", str);

// print value of x
printf("%c\n", x);

// cause buffer overflow
strcpy(str, "Hello world");

// print address and value of str
printf("%p: ", &str);
printf("%s\n", str);

// print address and value of x
printf("%p: ", &x);
printf("%c\n", x);
return 0;

这导致输出为:

0061FF2A: Testt
X
0061FF2A: Hello world
0061FF29: X

所以在这种情况下,缓冲区溢出并没有覆盖x变量。

为什么简单地打印x变量的内存地址会对缓冲区溢出情况产生这种影响?

编辑:为两种情况添加到汇编中 第一种情况生成的程序集(无 printf):

    .file   "hello.c"
    .def    ___main;    .scl    2;  .type   32; .endef
    .section .rdata,"dr"
LC0:
    .ascii "%p: [=14=]"
LC1:
    .ascii "%c[=14=]"
    .text
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB17:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    , %esp
    call    ___main
    movl    53719636, 25(%esp)
    movw    6, 29(%esp)
    movb    , 31(%esp)
    leal    25(%esp), %eax
    movl    %eax, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    leal    25(%esp), %eax
    movl    %eax, (%esp)
    call    _puts
    movsbl  31(%esp), %eax
    movl    %eax, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf
    leal    25(%esp), %eax
    movl    19043144, (%eax)
    movl    70078063, 4(%eax)
    movl    81362, 8(%eax)
    leal    25(%esp), %eax
    movl    %eax, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    leal    25(%esp), %eax
    movl    %eax, (%esp)
    call    _puts
    movsbl  31(%esp), %eax
    movl    %eax, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf
    movl    [=14=], %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE17:
    .ident  "GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
    .def    _printf;    .scl    2;  .type   32; .endef
    .def    _puts;  .scl    2;  .type   32; .endef

第二种情况

    .file   "hello.c"
    .def    ___main;    .scl    2;  .type   32; .endef
    .section .rdata,"dr"
LC0:
    .ascii "%p: [=15=]"
LC1:
    .ascii "%c[=15=]"
    .text
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB17:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    , %esp
    call    ___main
    movl    53719636, 26(%esp)
    movw    6, 30(%esp)
    movb    , 25(%esp)
    leal    26(%esp), %eax
    movl    %eax, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    leal    26(%esp), %eax
    movl    %eax, (%esp)
    call    _puts
    movzbl  25(%esp), %eax
    movsbl  %al, %eax
    movl    %eax, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf
    leal    26(%esp), %eax
    movl    19043144, (%eax)
    movl    70078063, 4(%eax)
    movl    81362, 8(%eax)
    leal    26(%esp), %eax
    movl    %eax, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    leal    26(%esp), %eax
    movl    %eax, (%esp)
    call    _puts
    leal    25(%esp), %eax
    movl    %eax, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    movzbl  25(%esp), %eax
    movsbl  %al, %eax
    movl    %eax, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf
    movl    [=15=], %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE17:
    .ident  "GCC: (MinGW.org GCC-6.3.0-1) 6.3.0"
    .def    _printf;    .scl    2;  .type   32; .endef
    .def    _puts;  .scl    2;  .type   32; .endef

首先让我们看看为什么缓冲区溢出没有在第二个例子中发生。

查看您的输出:

0061FF2A: Testt
X
0061FF2A: Hello world
0061FF29: X

我们可以看到str上面x在堆栈上。

字符串 "Hello world" 正在占用内存地址 0061FF2A0061FF36

堆栈看起来像

0061FF29   0061FF2A        0061FF36
       |   |                      |
       ----------------------------
       | X | H e l l o  w o r l d |
       ----------------------------

在这种情况下,我们写的 str 结束后多远并不重要,因为 x 在堆栈中出现在 str 之前。


接下来让我们看看为什么缓冲区溢出确实发生在第一个例子中。

我们无法在您的输出中直接看到每个变量的地址但是我们可以在程序集中看到它们在堆栈中的位置。

movl    53719636, 25(%esp)
movw    6, 29(%esp)
movb    , 31(%esp)

x 变量肯定在 31(%esp),因为我们看到 'X' 的十进制 ASCII 值被放在那里。

假设 5 个字符的字符串 "Testt" 存储在 25(%esp) 并没有太大的飞跃,因为 25(%esp)31(%esp) 之间的距离是刚好足以存储 5 个字符和一个空终止符。

所以我们知道 str25(%esp) 并且 x31(%esp)。堆栈应该类似于:

esp  +25         +31
  |    |           | 
  ---------------------- 
  |    | T e s t t | X |
  ----------------------

现在我们可以很容易地看到 str 出现在 x 之前并且很清楚为什么写到 str 的末尾会导致 x 被覆盖.


现在是主要问题,为什么这在第一种情况下有效而在第二种情况下无效?

出于某种原因,编译器决定在第一个示例中将 x 放在 str 之后,在第二个示例中将 x 放在 str 之前。

正如评论中所指出的,局部变量在堆栈上的确切位置不是由 C 定义的。编译器可以决定它想要的东西存储的顺序,并且可能会在程序之间更改该顺序-显而易见的原因。

本质上,局部变量在堆栈上的确切位置和顺序是未定义的,因此未定义的行为是缓冲区溢出在一种情况下起作用而在另一种情况下不起作用的原因。