C - 使用指针时出现 strncpy 段错误
C - strncpy segfaults when using pointer
我得到了以下代码:
#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
= 4198496
(gdb) p $ecx
= 1936287828
根据规范,您将从大多数人那里听到的答案是这样的:程序崩溃是因为您通过写入未初始化的指针来调用 UB。在这一点上,崩溃是一种有效的行为,所以有时它会崩溃,有时它会做一些同样有效的事情(因为 UB)。
这是正确的,但它没有回答您的问题。你的问题是,"Why doesn't it crash in all circumstances?" 在你的例子中,当你改变你的程序结构以包含一个似乎执行不相关行为的 for
循环时,你只会出现段错误。为此,我们需要对程序内存布局和段错误的性质进行基本介绍,我们将从段错误开始。
段错误和虚拟内存
如果您不熟悉 CPU 体系结构,那么分段错误是一个有点复杂的问题。它的目的很简单,如果一个正在执行的进程试图访问它不应该访问的内存,就应该发出一个段错误。细节决定成败,"memory the process shouldn't touch" 的定义是什么?段错误应该如何传达给操作系统?
在现代操作系统和 CPU 体系结构中,进程的有效内存 space 由 virtual memory system 控制。虚拟内存的操作超出了您的问题范围,但足以说明操作系统和 CPU 本身都知道您的进程可以访问和不能访问的地址。如果您的进程超出其允许的内存范围 space,将发出段错误。
为了 "issue" 出现段错误,CPU 将 synchronously interrupt 您的程序,并警告操作系统您做了一件不当的事情。这些也称为 "exceptions" 或 "traps",但它们只是 "your program asked the CPU to do something that it can't or won't do" 的不同命名法。操作系统处理中断,然后向您的程序发出信号(*Nix)或异常(Win32)。如果您的程序没有为 signal/exception 设置处理程序,OS 会优雅地让您崩溃。
关于虚拟内存的一个有趣的 oolie 是它通常只以 2^12 个连续字节 (4KiB) 的包形式发布。因此,即使您的进程只需要,比方说,10 个字节,它也至少会得到 4KiB。这种连续的字节分组称为 "page",因为它将 "lines" 内存分组。
程序内存和堆栈
即使您的进程从不使用 malloc
或类似的方法请求内存,它也会收到几页以实现所谓的 the stack(将其名称借给某些网站).这是您本地声明的变量(如 src
、dest
、ret
和 s
所在的位置。它也用于在函数调用之间移动时溢出非易失性 CPU 寄存器,但这也不在范围之内。
所以,如果 dest
只是堆栈上的一块内存,并且从未在您的程序中初始化,它指向什么?好吧,无论该内存地址中恰好存在什么随机数据,现在都是您的指针。您的程序的操作现在由堆栈页中的垃圾字节引起。
结论
如果堆栈中的垃圾 space 恰好指向为堆栈 space 发给您的进程的内存页面之一内的某处,您的进程将不会访问无效内存并将继续 chugging(或者它指向附近的某个地方,如果你在最后一个有效页面的一页内,Linux 可以自动增加堆栈)。但是,如果它指向其他任何地方,则会导致无效的内存访问,并且 CPU 会提醒相关当局。您的流程属于犯罪行为,将受到相应处理。
"But nickelpro," 你说情,"what does any of that have to do with the for
loop?" 没什么,for
循环是转移注意力。在这种情况下,它恰好将堆栈分配偏向垃圾恰好导致段错误的地方。这可能与许多事情有关,可能是 ASLR 的结果或只是随机事件。比我更了解虚拟内存实现的人可以对此有所启发。
勘误表
现在您的程序结构中还有一个(我认为)意外的错误,这使问题更加恶化。您执行初始字符串复制:
ret = strncpy(dest, src, 5);
其中 不会以 null 终止目标字符串,这意味着当您调用时:
size_t s = strlen(ret);
strlen
将继续读取直到遇到空字节。所以即使 dest
碰巧指向某个有效的地方,内存垃圾的运气不好也会导致 strlen
读入无效内存。
我得到了以下代码:
#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
= 4198496
(gdb) p $ecx
= 1936287828
根据规范,您将从大多数人那里听到的答案是这样的:程序崩溃是因为您通过写入未初始化的指针来调用 UB。在这一点上,崩溃是一种有效的行为,所以有时它会崩溃,有时它会做一些同样有效的事情(因为 UB)。
这是正确的,但它没有回答您的问题。你的问题是,"Why doesn't it crash in all circumstances?" 在你的例子中,当你改变你的程序结构以包含一个似乎执行不相关行为的 for
循环时,你只会出现段错误。为此,我们需要对程序内存布局和段错误的性质进行基本介绍,我们将从段错误开始。
段错误和虚拟内存
如果您不熟悉 CPU 体系结构,那么分段错误是一个有点复杂的问题。它的目的很简单,如果一个正在执行的进程试图访问它不应该访问的内存,就应该发出一个段错误。细节决定成败,"memory the process shouldn't touch" 的定义是什么?段错误应该如何传达给操作系统?
在现代操作系统和 CPU 体系结构中,进程的有效内存 space 由 virtual memory system 控制。虚拟内存的操作超出了您的问题范围,但足以说明操作系统和 CPU 本身都知道您的进程可以访问和不能访问的地址。如果您的进程超出其允许的内存范围 space,将发出段错误。
为了 "issue" 出现段错误,CPU 将 synchronously interrupt 您的程序,并警告操作系统您做了一件不当的事情。这些也称为 "exceptions" 或 "traps",但它们只是 "your program asked the CPU to do something that it can't or won't do" 的不同命名法。操作系统处理中断,然后向您的程序发出信号(*Nix)或异常(Win32)。如果您的程序没有为 signal/exception 设置处理程序,OS 会优雅地让您崩溃。
关于虚拟内存的一个有趣的 oolie 是它通常只以 2^12 个连续字节 (4KiB) 的包形式发布。因此,即使您的进程只需要,比方说,10 个字节,它也至少会得到 4KiB。这种连续的字节分组称为 "page",因为它将 "lines" 内存分组。
程序内存和堆栈
即使您的进程从不使用 malloc
或类似的方法请求内存,它也会收到几页以实现所谓的 the stack(将其名称借给某些网站).这是您本地声明的变量(如 src
、dest
、ret
和 s
所在的位置。它也用于在函数调用之间移动时溢出非易失性 CPU 寄存器,但这也不在范围之内。
所以,如果 dest
只是堆栈上的一块内存,并且从未在您的程序中初始化,它指向什么?好吧,无论该内存地址中恰好存在什么随机数据,现在都是您的指针。您的程序的操作现在由堆栈页中的垃圾字节引起。
结论
如果堆栈中的垃圾 space 恰好指向为堆栈 space 发给您的进程的内存页面之一内的某处,您的进程将不会访问无效内存并将继续 chugging(或者它指向附近的某个地方,如果你在最后一个有效页面的一页内,Linux 可以自动增加堆栈)。但是,如果它指向其他任何地方,则会导致无效的内存访问,并且 CPU 会提醒相关当局。您的流程属于犯罪行为,将受到相应处理。
"But nickelpro," 你说情,"what does any of that have to do with the for
loop?" 没什么,for
循环是转移注意力。在这种情况下,它恰好将堆栈分配偏向垃圾恰好导致段错误的地方。这可能与许多事情有关,可能是 ASLR 的结果或只是随机事件。比我更了解虚拟内存实现的人可以对此有所启发。
勘误表
现在您的程序结构中还有一个(我认为)意外的错误,这使问题更加恶化。您执行初始字符串复制:
ret = strncpy(dest, src, 5);
其中 不会以 null 终止目标字符串,这意味着当您调用时:
size_t s = strlen(ret);
strlen
将继续读取直到遇到空字节。所以即使 dest
碰巧指向某个有效的地方,内存垃圾的运气不好也会导致 strlen
读入无效内存。