C中strncpy的内存混淆

时间:2016-12-15 07:06:10

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

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

示例代码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我为什么得到输出"他的字符串。"

4 个答案:

答案 0 :(得分:4)

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

enter image description here

注意上面的表示仅用于理解,并不显示任何体系结构的实际表示。 执行strncpy命令后,该内存区域的内容如下所示

enter image description here

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

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

希望这有帮助。

P.S。以上描述仅用于理解,并未示出任何实际的系统表示。

答案 1 :(得分:3)

C标准以这种方式指定strncpy

  

7.24.2.4 strncpy功能

     

概要

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

描述

     

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

     

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

     

如果s2指向的数组是一个短于n个字符的字符串,则空字符将附加到s1指向的数组中的副本,直到{{所有人中都有1}个字符。

     

返回

     

n函数返回strncpy的值。

这些语义被广泛误解:s1不是strncpy安全版本,目标数组 NOT null如果源是null终止string比strcpy参数长。

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

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

我的建议是永远不要使用这个功能。布鲁斯道森等其他C专家的意见:Stop using strncpy already!

你应该支持一个不易出错的功能,比如这个:

strncpy

您可以在示例中使用它:

// 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;
}

答案 2 :(得分:0)

堆栈上变量的位置是: -

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

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

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

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

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

答案 3 :(得分:0)

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

你的dest只有一个字节,所以你在这里编写你不应该写的内存,这会调用未定义的行为。换句话说,任何事情都可能发生,这取决于编译器如何实现这些东西。

编写buf的最可能原因是编译器将dest放在buf之后。所以,当你写过dest的边界时,你正在写buf。当您注释掉buf时,会导致崩溃。

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

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