我知道这可能是一个未定义的行为问题,但我很好奇,也试图理解以下结果的原因
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define SIZE 100
char* memory(){
char ch[SIZE] = {0};
return ch;
}
void copy(char *string){
char *new_string = memory();
strncpy(new_string, string, strlen(string));
}
int main(){
char *string = "This is going to be copied";
copy(string);
}
我遇到了内核分段错误。一旦函数结束,内存()函数返回的内存就会超出范围,但这是我的理解: 我的理解:
我的疑问:
环境:
操作系统:Ubuntu 24.04.1 LTS
内核:Linux 6.8.0-49-generic
架构:x86-64
优化级别0:gcc ptr.c -g -o ptr -O0
gcc --版本 gcc(Ubuntu 13.2.0-23ubuntu4)13.2.0
如果我的理解有误,请告诉我。
UB 代码的实际行为及其原因显然取决于您的实现和环境的具体细节(编译器、优化标志、操作系统、CPU 架构等)。 因此,如果没有这些详细信息,我们无法确定为什么您的程序在 your 系统上出现段错误。
但是,在 x86-64 Linux 系统上,使用未经优化的 gcc 14.2(try on godbolt),会发生的情况是编译器注意到
memory()
正在返回其生命周期即将结束的本地对象的地址。 由于对该返回值的任何使用都会导致未定义的行为,因此编译器可以返回任何值,并且仍然符合 C 语言的规则。 所以它决定返回一个空指针。 请参阅 godbolt 上的 asm 第 17 行 (mov eax, 0
)。
当
copy()
然后尝试使用空指针调用 memcpy
时,将导致对页 0 的访问。 由于第 0 页肯定未映射,因此结果是分段错误。
你的问题中的推理是正确的:如果
memory()
确实返回了本地数组ch
的地址,那么我们确实期望它指向一个映射页面,并且之后写入它不会被期望导致段错误。 (至少,不是立即;如果该内存现在被用于其他用途 - 比如在 memcpy
内 - 那么覆盖它肯定会导致程序出现段错误或稍后出现其他行为不当。)所以你的逻辑中唯一的缺陷是,由于 C 语言级别的未定义行为,memory()
未返回 ch
的实际地址。
我们可以修改程序,通过
ch
指针传递它,强制编译器返回 volatile
的实际地址:
char* memory(){
char ch[SIZE] = {0};
char * volatile p = ch;
return p;
}
(尝试使用上帝螺栓。)。 这意味着编译器不能假设从
p
读回的值仍然是 ch
的地址(即使事实上它是),因此不能确定使用该值会导致 UB,并且所以它必须按原样返回它而不弄乱它。 您会发现,确实,该程序的修改版本不会出现段错误,这正是您所描述的原因。