【问题标题】:Why does a read operation on a memory mapped zero byte file lead to SIGBUS?为什么对内存映射的零字节文件的读取操作会导致 SIGBUS?
【发布时间】:2017-05-15 22:09:11
【问题描述】:

这是我写的示例代码。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main()
{
    int fd;
    long pagesize;
    char *data;

    if ((fd = open("foo.txt", O_RDONLY)) == -1) {
        perror("open");
        return 1;
    }

    pagesize = sysconf(_SC_PAGESIZE);
    printf("pagesize: %ld\n", pagesize);

    data = mmap(NULL, pagesize, PROT_READ, MAP_SHARED, fd, 0);
    printf("data: %p\n", data);
    if (data == (void *) -1) {
        perror("mmap");
        return 1;
    }

    printf("%d\n", data[0]);
    printf("%d\n", data[1]);
    printf("%d\n", data[2]);
    printf("%d\n", data[4096]);
    printf("%d\n", data[4097]);
    printf("%d\n", data[4098]);

    return 0;
}

如果我向这个程序提供一个零字节的 foo.txt,它会以 SIGBUS 终止。

$ > foo.txt && gcc foo.c && ./a.out 
pagesize: 4096
data: 0x7f8d882ab000
Bus error

如果我向这个程序提供一个单字节的 foo.txt,那么就不存在这样的问题。

$ printf A > foo.txt && gcc foo.c && ./a.out 
pagesize: 4096
data: 0x7f5f3b679000
65
0
0
48
56
10

mmap(2) 提到以下内容。

使用映射区域可能会产生以下信号:

SIGSEGV 尝试写入映射为只读的区域。

SIGBUS 尝试访问与文件不对应的缓冲区部分(例如,超出文件末尾,包括另一个进程截断文件的情况)。

所以如果我理解正确,即使是第二个测试用例(1 字节文件)也应该导致 SIGBUS,因为 data[1]data[2] 正在尝试访问缓冲区的一部分(data)与文件不对应。

你能帮我理解为什么只有一个零字节的文件会导致这个程序因 SIGBUS 而失败吗?

【问题讨论】:

  • 你为什么在乎?您调用未定义的行为;任何事情都可能发生。甚至不能保证你会收到任何信号。
  • POSIX 是基于 C 标准的,对于 C 标准,这种访问显然是 UB。说,不清楚你的问题是什么。你的代码无论如何都坏了,如果你得到一个 SIGBUS 或 SIGSEGV 或类似的,你已经把事情搞砸了。但这不是双射的:如果你没有收到信号,并不意味着你的代码是正确的!
  • @Olaf 我本身没有任何问题。出于好奇,我有一个问题。这个问题可以概括为:Linux 上的man 页面表明,任何对与文件不对应的缓冲区部分的访问(例如,超出文件末尾)都应该导致SIGBUS。但我的第二个测试似乎与 man 页面在同一 Linux 系统上所说的相矛盾。
  • 提出问题的完全正当理由,应该鼓励好奇心。我不认为说问题不值得问是有建设性的。
  • 访问映射的页面,其中整个页面超出映射文件的末尾,似乎不是 UB。 POSIX standard for mmap 明确指出允许大于底层文件的映射,但访问这样的页面会导致 SIGBUSmmap() 函数可用于映射大于对象的当前大小。映射内但超出底层对象当前端的内存访问可能导致SIGBUS 信号被发送到进程。

标签: c mmap sigbus


【解决方案1】:

一个 1 字节的文件不会导致崩溃,因为mmap 会以页面大小的倍数映射内存,并将余数归零。从手册页:

文件以页面大小的倍数进行映射。对于一个文件 不是页面大小的倍数,剩余内存清零时 映射,并且对该区域的写入不会写出到文件中。 改变映射底层文件大小的效果 在与文件的添加或删除区域相对应的页面上 未指定。

【讨论】:

  • 我不相信这个推理是正确的。如果我尝试在第二个测试用例中打印data[4096]data[4097] 等,我会打印一些垃圾值。如果这个答案中提出的推理很好,我没有得到SIGBUS
  • ...高达 4095, 4kB - 1
  • 这取决于操作系统配置和默认值。我刚刚在我的 Windows 10 机器上做了一个getconf PAGESIZE,它返回了 65536 个字节。
  • 在我的系统上,页面大小为 4096,正如我在问题中包含的示例输出中所记录的那样。
  • Lone Lerner,但 mmap 保留有关已分配字节的元数据,您可以使内存可执行,仅读/写等,因此它知道一些有关它的信息。在这种情况下,您假设 mmap 不会对错误访问做任何事情,这就是为什么即使您要求 1 个字节访问 data[1] 也是合法的。但是,答案不是 4kB 的粒度,而是粒度 + mmap 对此没有做任何事情的事实。
【解决方案2】:

当访问超过最后一个完整映射页面的末尾时,您会得到SIGBUS,因为the POSIX standard states

mmap() 函数可用于映射大于对象当前大小的内存区域。 在映射内但超出底层对象当前端的内存访问可能会导致SIGBUS 信号被发送到进程。

对于零字节文件,您映射的整个页面“超出了基础对象的当前端”。所以你会得到SIGBUS

当您超出已映射的 4kB 页面时,您不会收到 SIGBUS,因为这不在您的映射范围内。当您的文件大于零字节时,您不会得到 SIGBUS 访问您的映射,因为整个页面都已被映射。

但是,如果您将其他页面映射到文件末尾之后,您会得到一个SIGBUS,例如为一个 1 字节文件映射 两个 4kB 页面。如果你访问第二个 4kB 页面,你会得到SIGBUS

【讨论】:

  • 这个答案似乎非常准确地解释了我观察到的行为。我将mmap() 调用更改为映射2 * pagesize 字节而不是pagesize,并且确实通过此更改访问data[4096] 导致SIGBUS
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-04
  • 2015-11-25
  • 1970-01-01
相关资源
最近更新 更多