【问题标题】:C - strncpy segfaults when using pointerC - 使用指针时出现 strncpy 段错误
【发布时间】:2019-08-07 14:19:04
【问题描述】:

我有以下代码:

#include<stdio.h>
#include <string.h>

int main(void) {
    char *src = "This is my string.";
    char *dest, *ret;
    //char dest[64], *ret;
    ret = strncpy(dest, src, 5);
    size_t s = strlen(ret);

    printf("src: %s\n", src);
    printf("dst: %s|\n", dest);
    printf("ret: %s|\n", ret);
    printf("len: %d\n", s);

    //for (int i = 0; i < 5; i++) {
    //    printf("i: %d\n", i);
    //}

    return 0;
}

for 循环已禁用

$ gcc -g -o test test.c; ./test 
src: This is my string.
dst: This |
ret: This |
len: 5

for 循环启用

$ gcc -g -o test test.c; ./test 
Segmentation fault (core dumped)

我想知道为什么只有在启用for 循环时才会失败。

这只是一个未定义的行为,因为我在 dest 参数中使用了一个悬空指针,还是对此有其他解释?

通过查看gdb 会话,它在尝试将值从ecx 分配到rdi 寄存器时崩溃了?

(gdb) bt
#0  0x00007ffff7f4a1a7 in __strncpy_avx2 () from /lib64/libc.so.6
#1  0x000000000040116e in main () at stack.c:8
(gdb) x/i 0x00007ffff7f4a1a7
=> 0x7ffff7f4a1a7 <__strncpy_avx2+1591>:    mov    DWORD PTR [rdi],ecx
(gdb) x/i $rdi
0x401060 <_start>:  endbr64
(gdb) p $rdi
$7 = 4198496
(gdb) p $ecx
$8 = 1936287828

【问题讨论】:

  • 指针 char *dest 未初始化。
  • 这是未定义的行为。您正在使用未初始化的指针。 strncpy 取消引用此指针,该指针可能具有任何值,并尝试写入该内存。您需要有一个由有效内存组成的目标缓冲区,并传递一个指向该缓冲区的指针。
  • @HTF 这是未定义的行为。任何事情都可能发生,或者没有什么不好的事情发生。你无法预测。您正在做的事情可能会弄乱其他任意变量。未定义行为的影响范围超出了您执行该行为的位置。一旦你的程序中有任何东西,整个程序的行为是不确定的,你无法再预测会发生什么。
  • 另请注意,strncpy 并不是真正的字符串函数,因为它不一定会在目标中生成以 NUL 结尾的 C 字符串。一个好的经验法则是“永远不要使用 strncpy”,因为 99% 的用例都不能满足您的要求。如果你有那 1%,你就会知道。
  • 知道为什么只有在启用 for 循环时才会发生这种情况吗? 这基本上就像在问:“昨天,我开车闯红灯,但什么也没发生。今天我打开了收音机,当我开车闯红灯时,一辆卡车撞上了我。知道为什么只有当我打开收音机时才会发生这种情况吗?”

标签: c strncpy


【解决方案1】:

您将从大多数人那里听到的每个规范的答案是这样的:程序崩溃是因为您通过写入未初始化的指针来调用 UB。在这一点上,崩溃是一种有效的行为,所以有时它会崩溃,有时它会做一些同样有效的事情(因为 UB)。

这是正确的,但它不能回答你的问题。您的问题是,“为什么它不会在所有情况下都崩溃?”在您的情况下,您仅在更改程序结构以包含似乎执行不相关行为的 for 循环时才实现段错误。为此,我们需要对程序内存布局和段错误的性质进行基本介绍,我们将从段错误开始。

分段错误和虚拟内存

如果您不熟悉 CPU 架构,则分段错误是一种复杂的野兽。它的目的很简单,如果一个执行进程试图访问它不应该访问的内存,则应该发出一个段错误。细节中的魔鬼是,什么定义了“进程不应该接触的内存”?以及应该如何将段错误传达给操作系统?

在现代操作系统和 CPU 架构上,进程的有效内存空间是使用 virtual memory system 控制的。虚拟内存的操作超出了您的问题范围,但足以说明操作系统和 CPU 本身都知道您的进程可以访问和不能访问的地址。如果您的进程偏离其允许的内存空间范围,则会发出段错误。

为了“发出”一个段错误,CPU 将synchronously interrupt 你的程序,并提醒操作系统你做了一件淘气的事情。这些也称为“异常”或“陷阱”,但它们只是“您的程序要求 CPU 做一些它不能或不会做的事情”的不同命名法。操作系统处理中断,然后向您的程序发出信号 (*Nix) 或异常 (Win32)。如果您的程序没有为该信号/异常设置处理程序,操作系统会优雅地让您崩溃。

关于虚拟内存的一个有趣的oolie 是它通常仅以 2^12 连续字节 (4KiB) 的包的形式发布。因此,即使您的进程只想要 10 个字节,它也将获得至少 4KiB。这种连续的字节分组称为“页”,因为它将内存的“行”分组。

程序存储器和堆栈

即使您的进程从不使用malloc 或其同类请求内存,它也将获得几页以实现所谓的the stack(将其名称借给某些网站)。这是您本地声明的变量(例如 srcdestrets)所在的位置。它还用于在函数调用之间移动时溢出非易失性 CPU 寄存器,但这也在范围之外。

那么,如果dest 只是堆栈上的一块内存,并且从未在您的程序中初始化,那么它指向什么?好吧,该内存地址上碰巧存在的任何随机数据现在都是您的指针。您的程序的操作现在是在堆栈页面中的垃圾字节的心血来潮。

结论

如果堆栈空间中的垃圾碰巧指向某个内存页面内的某个位置,该内存页面已分配给您的进程以获得堆栈空间,您的进程将不会访问无效内存并继续运行(或者它指向附近的某个地方,如果您在最后一个有效页面的一页内,Linux 可以自动增加堆栈)。但是,如果它指向其他任何地方,则会导致无效的内存访问,并且 CPU 会向相关机构发出警报。您的流程属于犯罪行为,将受到相应的处理。

“但是nickelpro,”你插话道,“这些和for循环有什么关系?”没什么,for 循环是一条红鲱鱼。在这种情况下,它恰好将堆栈分配偏向垃圾恰好导致段错误的地方。这可能与很多事情有关,可能是ASLR 的结果,或者只是随机事件。比我更了解虚拟内存实现的人可以对此有所了解。

勘误表

现在你的程序结构也有一个(我认为)意想不到的错误,这使问题更加恼火。您执行初始字符串复制:

ret = strncpy(dest, src, 5);

哪个不会以空值终止目标字符串,这意味着当您调用时:

size_t s = strlen(ret);

strlen 将继续读取,直到遇到空字节。因此,即使dest 碰巧指向某个有效的地方,内存垃圾的运气不好也会导致strlen 将其读取到无效内存中。

【讨论】:

  • 虽然for 循环本身几乎可以肯定是一条红鲱鱼,但其中的printf() 几乎可以肯定不是。对printf() 的调用几乎总是涉及内存分配,因此经常在代码的早期充当内存滥用的“检测器”(最常见的原因是“滥用”已经破坏了库的内部“管理数据”块周围的已分配和可用的堆空间)。
  • @TripeHound 当然,但 OP 中的崩溃发生在原始 strncpy 中。在程序的任一版本中发生的任何内存分配,for 循环与否,都不会涉及。崩溃的唯一驱动因素是原来的 dest 取消引用。
猜你喜欢
  • 1970-01-01
  • 2021-03-24
  • 2020-09-26
  • 1970-01-01
  • 1970-01-01
  • 2020-12-27
  • 2020-10-04
  • 1970-01-01
  • 2022-11-16
相关资源
最近更新 更多