C 中 strncpy 的内存混淆
Memory confusion for strncpy in C
本周我的同事讨论了一个关于内存的问题:
示例代码 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 为什么我得到输出为 "his as string."
堆栈中变量的位置是:-
0. dest
1. buff
12. canary
16. Return address
当 buff
存在时,它会保护金丝雀和 return 地址免受损坏。
这是未定义的行为(向 dest
写入的数据超出了容量)。 canary 内部有一个特殊的随机值,它在函数启动时设置,并在执行 return 指令之前进行测试。这为缓冲区溢出增加了某种形式的保护。
未定义性质的示例,程序可能因 "illegal instruction @ xxxxxx" 没有金丝雀而崩溃。
如果 return 地址与变量位置分开,程序可能运行正常。
在大多数当前 CPU 上,堆栈通常会向负方向增长。 dest 与 buff 的位置也取决于编译器。它可能已经调换了它们,或者如果(例如)你拿走了第二个 printf,编译器可能已经删除了 dest
的存储,因为它可能已经决定它没有被正确使用。
strncpy(dest, Str, sizeof(Str));
你的 dest
只有一个字节,所以你在这里写入了你不应该写入的内存,这会调用未定义的行为。换句话说,任何事情都可能发生,这取决于编译器如何实现这些事情。
写入 buf
的最可能原因是编译器将 dest
放在 buf
之后。因此,当您越过 dest
的边界写入时,您正在写入 buf
。当您注释掉 buf
时会导致崩溃。
但是正如我之前所说,如果使用不同的编译器甚至是同一编译器的不同版本,您可能会得到完全不同的行为。
总结:永远不要做任何会引发未定义行为的事情。在 strncpy
中,您应该使用 sizeof(dest)
,而不是 sizeof(src)
,并为目标分配足够的内存,这样来自源的数据就不会丢失。
C 标准这样指定 strncpy
:
7.24.2.4 The strncpy
function
Synopsis
#include <string.h>
char *strncpy(char * restrict s1,
const char * restrict s2,
size_t n);
Description
The strncpy
function copies not more than n
characters (characters that follow a null character are not copied) from the array pointed to by s2
to the array pointed to by s1
.
If copying takes place between objects that overlap, the behavior is undefined.
If the array pointed to by s2
is a string that is shorter than n
characters, null characters are appended to the copy in the array pointed to by s1
, until n
characters in all have been written.
Returns
The strncpy
function returns the value of s1
.
这些语义被广泛误解:strncpy
不是 strcpy
的 safe 版本,目标数组是 NOT 如果源字符串比 n
参数长,则 null 终止。
在您的示例中,此 n
参数大于目标数组的大小:行为是 undefined 因为字符被写入到目标数组的末尾之外数组。
您可以观察到这是第一个示例,因为编译器将 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] != '[=11=]'; i++) {
dest[i] = src[i];
}
/* null terminate destination unless size == 0 */
if (i < size) {
dest[i] = '[=11=]';
}
/* compute necessary length to allow truncation detection */
while (src[i] != '[=11=]') {
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
这是一个有趣的问题,我们都希望在某个时候了解这个问题。此处发生的问题称为 “缓冲区溢出”。此问题的副作用因系统而异(也称为 未定义行为)。只是为了向您解释您的情况可能会发生什么,让我们假设程序中变量的内存布局如下
注意上面的表示只是为了理解,并不显示任何体系结构的实际表示。
strncpy命令执行后该内存区域内容如下
现在当你打印 buff 时你可以看到 buf 的起始地址现在有 'h' 了。 printf 开始打印它,直到它找到一个超出 buff 内存区域的空字符。因此,当您打印 buf 时,您会得到 'his is String'。
但是请注意,由于堆栈保护(system/implementation)相关,程序 1 不会生成堆栈粉碎错误。因此,如果您在不包含此代码的系统上执行此代码,程序 1 也会崩溃(您可以通过将 Str 增加到一个长字符串来测试此代码)。
在程序 2 的情况下,strncpy 刚刚通过堆栈保护,从 main 写入 return 地址,因此你会崩溃。
希望对您有所帮助。
P.S。以上描述仅供理解,不代表任何实际系统。
本周我的同事讨论了一个关于内存的问题:
示例代码 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 为什么我得到输出为 "his as string."
堆栈中变量的位置是:-
0. dest
1. buff
12. canary
16. Return address
当 buff
存在时,它会保护金丝雀和 return 地址免受损坏。
这是未定义的行为(向 dest
写入的数据超出了容量)。 canary 内部有一个特殊的随机值,它在函数启动时设置,并在执行 return 指令之前进行测试。这为缓冲区溢出增加了某种形式的保护。
未定义性质的示例,程序可能因 "illegal instruction @ xxxxxx" 没有金丝雀而崩溃。 如果 return 地址与变量位置分开,程序可能运行正常。
在大多数当前 CPU 上,堆栈通常会向负方向增长。 dest 与 buff 的位置也取决于编译器。它可能已经调换了它们,或者如果(例如)你拿走了第二个 printf,编译器可能已经删除了 dest
的存储,因为它可能已经决定它没有被正确使用。
strncpy(dest, Str, sizeof(Str));
你的 dest
只有一个字节,所以你在这里写入了你不应该写入的内存,这会调用未定义的行为。换句话说,任何事情都可能发生,这取决于编译器如何实现这些事情。
写入 buf
的最可能原因是编译器将 dest
放在 buf
之后。因此,当您越过 dest
的边界写入时,您正在写入 buf
。当您注释掉 buf
时会导致崩溃。
但是正如我之前所说,如果使用不同的编译器甚至是同一编译器的不同版本,您可能会得到完全不同的行为。
总结:永远不要做任何会引发未定义行为的事情。在 strncpy
中,您应该使用 sizeof(dest)
,而不是 sizeof(src)
,并为目标分配足够的内存,这样来自源的数据就不会丢失。
C 标准这样指定 strncpy
:
7.24.2.4 The
strncpy
functionSynopsis
#include <string.h> char *strncpy(char * restrict s1, const char * restrict s2, size_t n);
Description
The
strncpy
function copies not more thann
characters (characters that follow a null character are not copied) from the array pointed to bys2
to the array pointed to bys1
.If copying takes place between objects that overlap, the behavior is undefined.
If the array pointed to by
s2
is a string that is shorter thann
characters, null characters are appended to the copy in the array pointed to bys1
, untiln
characters in all have been written.Returns
The
strncpy
function returns the value ofs1
.
这些语义被广泛误解:strncpy
不是 strcpy
的 safe 版本,目标数组是 NOT 如果源字符串比 n
参数长,则 null 终止。
在您的示例中,此 n
参数大于目标数组的大小:行为是 undefined 因为字符被写入到目标数组的末尾之外数组。
您可以观察到这是第一个示例,因为编译器将 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] != '[=11=]'; i++) {
dest[i] = src[i];
}
/* null terminate destination unless size == 0 */
if (i < size) {
dest[i] = '[=11=]';
}
/* compute necessary length to allow truncation detection */
while (src[i] != '[=11=]') {
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
这是一个有趣的问题,我们都希望在某个时候了解这个问题。此处发生的问题称为 “缓冲区溢出”。此问题的副作用因系统而异(也称为 未定义行为)。只是为了向您解释您的情况可能会发生什么,让我们假设程序中变量的内存布局如下
注意上面的表示只是为了理解,并不显示任何体系结构的实际表示。 strncpy命令执行后该内存区域内容如下
现在当你打印 buff 时你可以看到 buf 的起始地址现在有 'h' 了。 printf 开始打印它,直到它找到一个超出 buff 内存区域的空字符。因此,当您打印 buf 时,您会得到 'his is String'。 但是请注意,由于堆栈保护(system/implementation)相关,程序 1 不会生成堆栈粉碎错误。因此,如果您在不包含此代码的系统上执行此代码,程序 1 也会崩溃(您可以通过将 Str 增加到一个长字符串来测试此代码)。
在程序 2 的情况下,strncpy 刚刚通过堆栈保护,从 main 写入 return 地址,因此你会崩溃。
希望对您有所帮助。
P.S。以上描述仅供理解,不代表任何实际系统。