用fprintf打印未终止的mmap映射C字符串未崩溃,是否为未定义行为?
问题:mmap映射无终止符文件后用printf输出无崩溃,是否属于未定义行为?
我最近学会了在C语言中用mmap快速读取文件,示例代码如下:
// main.c #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #include <sys/mman.h> #include <sys/stat.h> #include <unistd.h> #define INPUT_FILE "test.txt" int main(int argc, char* argv) { struct stat ss; if (stat(INPUT_FILE, &ss)) { fprintf(stderr, "stat err: %d (%s)\n", errno, strerror(errno)); return -1; } { int fd = open(INPUT_FILE, O_RDONLY); char* mapped = mmap(NULL, ss.st_size, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); fprintf(stdout, "%s\n", mapped); munmap(mapped, ss.st_size); } return 0; }
我原本以为mmap返回的是指向长度为ss.st_size的堆分配字节的指针。我用一个未显式添加空字符终止的纯文本文件测试了这段代码——比如文件里是13字节的ASCII字符串“hello, world!”:
$ cat ./test.txt hello, world!$ $ stat ./test.txt File: ./test.txt Size: 13 Blocks: 8 IO Block: 4096 regular file Device: 810h/2064d Inode: 52441 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 1000/ user) Gid: ( 1000/ user) Access: 2022-10-25 20:30:52.563772200 -0700 Modify: 2022-10-25 20:30:45.623772200 -0700 Change: 2022-10-25 20:30:45.623772200 -0700 Birth: -
运行编译后的代码时,从来没出现段错误或输出垃圾内容——这本来是打印未终止C字符串的典型症状。用gdb调试时,mapped[13]始终是'\0'。
这种行为是否属于未定义行为?
我搞不懂为什么内存映射的字节会可靠地以空字符终止。换成malloc+read的方式,我通常会分配14字节的数组,把文件内容读进去后,显式把第13个字节(0索引)设为'\0'。
解答
结论:确实是未定义行为
你观察到的mapped[13]为'\0'只是特定环境下的巧合,绝对不能依赖这种行为。
为什么会出现“意外可用”的空字符?
mmap的内存分配是以页为最小单位的(绝大多数系统中是4096字节)。哪怕你只映射13字节,操作系统也会为你分配一整页内存。在MAP_PRIVATE模式下,超出文件实际大小的那部分内存(从第13字节到页末尾)会被自动初始化为0——这就是你读到'\0'的原因。但这不是标准强制的:
- 如果文件大小刚好是页的整数倍,那
mapped[ss.st_size]就超出了映射范围,访问时会直接触发段错误; - 不同操作系统、内核版本对映射页的填充规则可能存在差异,不能默认超出文件长度的位置一定是0。
正确的处理方式
要避免未定义行为,有两种可靠的做法:
- 多映射1字节并手动添加终止符:
int fd = open(INPUT_FILE, O_RDONLY); // 多申请1字节空间,同时添加PROT_WRITE权限用于写入终止符 char* mapped = mmap(NULL, ss.st_size + 1, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); if (mapped != MAP_FAILED) { mapped[ss.st_size] = '\0'; // 手动添加空终止符 fprintf(stdout, "%s\n", mapped); munmap(mapped, ss.st_size + 1); } close(fd); - 不依赖C字符串函数,按实际长度输出:
这种方式完全不需要考虑空终止符,是更安全的选择。fwrite(mapped, 1, ss.st_size, stdout); // 直接按文件大小输出 putchar('\n'); // 手动添加换行符
纠正你对mmap的误解
mmap返回的指针不是堆分配的,它指向的是进程地址空间中的内存映射区域,既不属于堆(heap)也不属于栈(stack),是操作系统直接将文件内容或零填充页映射到进程地址空间的结果。
内容的提问来源于stack exchange,提问作者StoneThrow
相关产品推荐
相关产品推荐

