【问题标题】:Memory confusion for strncpy in CC中strncpy的内存混乱
【发布时间】:2017-04-30 17:03:29
【问题描述】:

本周我的同事讨论了一个关于记忆的问题:

示例代码 1:

int main()
{
    #define Str "This is String."
    char dest[1];
    char buff[10];

    strncpy(dest, Str, sizeof(Str));
    printf("Dest: %s\n", dest);
    printf("Buff: %s\n", buff);
}

输出:

Dest: This is String.
Buff: his is String.

示例代码 2:

int main()
{
    #define Str "This is String."
    char dest[1];
    //char buff[10];

    strncpy(dest, Str, sizeof(Str));
    printf("Dest: %s\n", dest);
    //printf("Buff: %s\n", buff);
}

输出:

Dest: This is String.
*** stack smashing detected ***: ./test terminated
Aborted (core dumped)

我不明白为什么我会在案例 1 中得到该输出?因为 buff 甚至没有在 strncpy 中使用,如果我注释变量 buff 它将检测到堆栈粉碎但输出为 dest。 同样对于buff为什么我将输出作为“他的字符串”。

【问题讨论】:

  • 未定义的行为是未定义的。思考为什么会得到某种奇怪的输出,只有思考是什么导致了它,才能学到任何有意义的东西。

标签: c memory memory-management memory-leaks undefined-behavior


【解决方案1】:

这是一个有趣的问题,我们都希望在某个时候理解它。这里发生的问题称为“缓冲区溢出”。此问题的副作用可能因系统而异(也称为未定义行为)。只是为了向您解释您的情况可能发生的情况,让我们假设程序中变量的内存布局如下

注意上面的表示只是为了理解,并不显示任何架构的实际表示。 执行strncpy命令后该内存区域的内容如下

现在,当您打印 buff 时,您可以看到 buf 的起始地址现在包含 'h'。 printf 开始打印它,直到找到一个超出缓冲区内存区域的空字符。因此,当您打印 buf 时,您会得到“his is String”。 但是请注意,程序 1 不会产生堆栈粉碎错误,因为堆栈保护(这是系统/实现)依赖。因此,如果您在不包含此代码的系统上执行此代码,程序 1 也会崩溃(您可以通过将 Str 增加到一个长字符串来测试它)。

在程序 2 的情况下,strncpy 只是越过堆栈保护,而不是从 main 写入返回地址,因此您会崩溃。

希望这会有所帮助。

附:以上所有描述都是为了理解,并不显示任何实际的系统表示。

【讨论】:

  • 感谢您的解释。我会更多地搜索堆栈保护。
【解决方案2】:

C 标准以这种方式指定strncpy

7.24.2.4 strncpy 函数

概要

#include <string.h>
 char *strncpy(char * restrict s1,
      const char * restrict s2,
      size_t n);

说明

strncpy 函数从s2 指向的数组复制不超过n 个字符(不复制空字符后的字符)到s1 指向的数组。

如果复制发生在重叠的对象之间,则行为未定义。

如果s2指向的数组是一个比n字符短的字符串,则空字符被追加到s1指向的数组的副本中,直到n字符全部被删除写的。

返回

strncpy 函数返回s1 的值。

这些语义被广泛误解:strncpy 不是strcpy安全 版本,如果源字符串长于n 参数。

在您的示例中,此n 参数大于目标数组的大小:行为是未定义,因为字符被写入目标数组的末尾之外。

您可以观察到这是第一个示例,因为编译器将buff 数组定位在自动存储中的dest 数组末尾之后(又名在堆栈上),并且是被strncpy 覆盖。编译器可以使用不同的方法,因此无法保证观察到的行为。

我的建议是永远不要使用此功能。 Bruce Dawson 等其他 C 语言专家分享的观点:Stop using strncpy already!

你应该喜欢这样一个不易出错的函数:

// Utility function: copy with truncation, return source string length
// truncation occurred if return value >= size argument
size_t bstrcpy(char *dest, size_t size, const char *src) {
    size_t i;
    /* copy the portion that fits */
    for (i = 0; i + 1 < size && src[i] != '\0'; i++) {
        dest[i] = src[i];
    }
    /* null terminate destination unless size == 0 */
    if (i < size) {
        dest[i] = '\0';
    }
    /* compute necessary length to allow truncation detection */
    while (src[i] != '\0') {
        i++;
    }
    return i;
}

你会在你的例子中这样使用它:

int main(void) {
    #define Str "This is String."
    char dest[12];

    // the size of the destination array is passed
    // after the pointer, just as for `snprintf`
    bstrcpy(dest, sizeof dest, Str);
    printf("Dest: %s\n", dest);
    return 0;
}

输出:

This is a S

【讨论】:

  • 这段代码可以运行,而且我的 bstrcpy 方法看起来很棒,但我需要解释一下内存管理。谢谢。
【解决方案3】:

strncpy(dest, Str, sizeof(Str));

您的dest 只有一个字节,因此您在内存中写入了您不应该写入的内容,这会引发未定义的行为。换句话说,任何事情都可能发生,这取决于编译器如何实现这些事情。

编写buf 的最可能原因是编译器将dest 放在buf 之后。因此,当您写信越过dest 的边界时,您就是在写信给buf。当您注释掉 buf 时会导致崩溃。

但正如我之前所说,如果使用不同的编译器甚至是同一编译器的不同版本,您可能会得到完全不同的行为。

总结:永远不要做任何会引发未定义行为的事情。在strncpy 中,您应该使用sizeof(dest),而不是sizeof(src),并为目标分配足够的内存,这样源数据就不会丢失。

【讨论】:

    【解决方案4】:

    变量在堆栈上的位置是:-

    0. dest
    1. buff
    12. canary
    16. Return address
    

    buff 存在时,它会保护金丝雀和返回地址免受损坏。

    这是未定义的行为(将更多数据写入dest 而不是适合)。金丝雀内部有一个特殊的随机值,它是在函数启动时设置的,并在执行返回指令之前进行测试。这为缓冲区溢出增加了某种形式的保护。

    未定义性质的示例是,由于没有金丝雀,程序可能因“非法指令@xxxxxx”而崩溃。 如果返回地址与变量位置分开,则程序可能运行正常。

    在大多数当前 CPU 上,堆栈通常会以负方向增长。 dest 与 buff 的位置也取决于编译器。它可能已经切换了它们,或者如果(例如)你拿走了第二个 printf,编译器可能已经删除了 dest 的存储,因为它可能认为它没有被正确使用。

    【讨论】:

      猜你喜欢
      • 2016-01-21
      • 2011-12-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-07-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多