本周我的同事讨论了一个关于记忆的问题:
示例代码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我为什么得到输出"他的字符串。"
答案 0 :(得分:4)
这是一个我们都希望在某些时候理解的有趣问题。此处出现的问题称为“缓冲区溢出”。此问题的副作用因系统而异(也称为未定义的行为)。只是为了解释一下你的情况可能会发生什么,让我们假设程序中变量的内存布局如下所示
注意上面的表示仅用于理解,并不显示任何体系结构的实际表示。 执行strncpy命令后,该内存区域的内容如下所示
现在当你打印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)
并为目标分配足够的内存,以便源中的数据不会丢失。