进程内的内存生命周期

问题描述 投票:0回答:1

我知道这可能是一个未定义的行为问题,但我很好奇,也试图理解以下结果的原因

#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);
}

我遇到了内核分段错误。一旦函数结束,内存()函数返回的内存就会超出范围,但这是我的理解: 我的理解

  1. 虚拟内存和页面管理: 由于操作系统以页为单位处理内存,如果进程使用的页的虚拟内存和物理内存之间的映射保持不变,那么应该可以访问该页内的所有内存,对吗? (该内存可能是垃圾,或者可能被同一进程中的其他线程覆盖。)
  2. 页面级重新分配: 如果操作系统决定重新分配内存,它将在页面级别而不是字节级别执行此操作。
  3. 页面重新映射和分段错误: 除非为 char 数组分配的内存驻留在不同的页面上,并且操作系统尝试将该页面重新映射到另一个进程,否则访问此内存不应导致分段错误。

我的疑问

  1. 内核的非法内存访问检测: 是什么触发内核将其识别为非法内存访问?如果内存页尚未重新分配,为什么会标记这种情况?
  2. 页面有效性: 内核是否在其元数据中将整个页面标记为有效或无效?这是否表明当前进程是否仍在使用页面?

环境: 操作系统: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

如果我的理解有误,请告诉我。

c memory-management linux-kernel operating-system segmentation-fault
1个回答
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,并且所以它必须按原样返回它而不弄乱它。 您会发现,确实,该程序的修改版本不会出现段错误,这正是您所描述的原因。

© www.soinside.com 2019 - 2024. All rights reserved.