OS 升级后 mmap 行为发生了变化?
mmap behaviour changed after OS upgrade?
主要 OS 升级后,此 C 代码行为已更改:
...
if ((fd = open(argv[1], O_RDWR | O_SYNC)) == -1)
FATAL;
printf("character device %s opened.\n", argv[1]);
fflush(stdout);
/* map one page */
map_base = mmap(0xe0000000, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map_base == (void *)-1)
FATAL;
printf("Memory mapped at address %p.\n", map_base);
...
使用从旧 OS 继承的二进制文件,"old mmap" returns 虚拟地址 0x7fb20d725000
。如果我在新的 OS 上重建相同的 C 文件,它 returns 0xe0000000
似乎是物理的,后续代码 - 使用此返回的地址 - 现在因分段错误而失败.
如何在不降级 OS 或使用旧二进制文件的情况下强制 mmap
像以前一样工作? gcc 或 mmap 本身有任何现代标志吗?
运行 下面带有 sudo ./test /dev/zero 0x01000000
的代码示例:(/dev/zero 而不是真实设备给出相同的结果)
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <byteswap.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/mman.h>
/* ltoh: little to host */
/* htol: little to host */
#if __BYTE_ORDER == __LITTLE_ENDIAN
#define ltohl(x) (x)
#define ltohs(x) (x)
#define htoll(x) (x)
#define htols(x) (x)
#elif __BYTE_ORDER == __BIG_ENDIAN
#define ltohl(x) __bswap_32(x)
#define ltohs(x) __bswap_16(x)
#define htoll(x) __bswap_32(x)
#define htols(x) __bswap_16(x)
#endif
#define FATAL do { fprintf(stderr, "Error at line %d, file %s (%d) [%s]\n", __LINE__, __FILE__, errno, strerror(errno)); exit(1); } while(0)
#define MAP_SIZE (16*1024*1024UL)
#define MAP_MASK (MAP_SIZE - 1)
int main(int argc, char **argv)
{
int fd;
void *map_base, *virt_addr;
uint32_t read_result, writeval;
off_t target;
char *device;
if (argc != 3) {
fprintf(stderr,
"\nUsage:\t%s <device> <address> [[type] data]\n"
"\tdevice : character device to access\n"
"\taddress : memory address to access\n\n",
argv[0]);
exit(1);
}
device = strdup(argv[1]);
target = strtoul(argv[2], 0, 0);
fprintf("argc = %d, device: %s, address: 0x%08x\n", argc, device, (unsigned int)target);
if ((fd = open(argv[1], O_RDWR | O_SYNC)) == -1)
FATAL;
fprintf(stdout, "character device %s opened.\n", argv[1]);
fflush(stdout);
/* map one page */
map_base = mmap(0xe0000000, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map_base == (void *)-1)
FATAL;
fprintf(stdout, "Memory mapped at address %p.\n", map_base);
fflush(stdout);
/* calculate the virtual address to be accessed */
virt_addr = map_base + target;
/* read only */
read_result = *((uint32_t *) virt_addr);
/* swap 32-bit endianess if host is not little-endian */
read_result = ltohl(read_result);
printf("Read 32-bit value at address 0x%08x (%p): 0x%08x\n",
(unsigned int)target, virt_addr, (unsigned int)read_result);
if (munmap(map_base, MAP_SIZE) == -1)
FATAL;
close(fd);
return 0;
}
您似乎混淆了虚拟地址和物理地址。用户程序通常只能使用虚拟地址。 mmap
系统调用接受一个 hint 作为第一个参数:请求映射区域所需的 virtual 地址。有关详细信息,请参阅 man 2 mmap
。
您之前的程序最有可能发生的情况是对 mmap
的调用可能类似于:
map_area = mmap(NULL, /* same arguments here */);
这样,操作系统会选择一个合适的地址,return它。
您在新程序中所做的是让 OS 知道您更喜欢特定地址 (0xe...
),并且 OS 将内存映射到如果可能(很有可能)那个地址。你真的不应该需要这个,这个程序不管映射区域的位置如何,但无论如何你都可以保留它。
您遇到分段错误的原因是因为您正在映射一个 16 * 1024 * 1024
字节的区域 (0x01000000
),但是您正在以高于指定大小的偏移量访问内存(target >= 0x01000000
).
正确的方法是使用 mmap
的 offset
参数来请求从文件中适当的偏移量开始的映射。请求从该偏移量开始的两个页面的映射将确保您要读取或写入的内容将被正确映射(假设文件足够大,否则 MAP_FAILED
将被 returned)。
操作方法如下:
offset = target & 0xFFFFFFFFFFFFF000; // align target to page size
// Map two pages starting at 0xe... and corresponding to the calculated offset in the file.
map_base = mmap((void *)0xe0000000, 0x1000 * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, offset);
// ...
virt_addr = map_base + (target & 0xfff); // cut target to get offset within the mapped pages
read_result = *((uint32_t *) virt_addr);
read_result = ltohl(read_result);
printf("Read 32-bit value at address 0x%08x (%p): 0x%08x\n",
(unsigned int)target, virt_addr, (unsigned int)read_result);
主要 OS 升级后,此 C 代码行为已更改:
...
if ((fd = open(argv[1], O_RDWR | O_SYNC)) == -1)
FATAL;
printf("character device %s opened.\n", argv[1]);
fflush(stdout);
/* map one page */
map_base = mmap(0xe0000000, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map_base == (void *)-1)
FATAL;
printf("Memory mapped at address %p.\n", map_base);
...
使用从旧 OS 继承的二进制文件,"old mmap" returns 虚拟地址 0x7fb20d725000
。如果我在新的 OS 上重建相同的 C 文件,它 returns 0xe0000000
似乎是物理的,后续代码 - 使用此返回的地址 - 现在因分段错误而失败.
如何在不降级 OS 或使用旧二进制文件的情况下强制 mmap
像以前一样工作? gcc 或 mmap 本身有任何现代标志吗?
运行 下面带有 sudo ./test /dev/zero 0x01000000
的代码示例:(/dev/zero 而不是真实设备给出相同的结果)
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <byteswap.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/mman.h>
/* ltoh: little to host */
/* htol: little to host */
#if __BYTE_ORDER == __LITTLE_ENDIAN
#define ltohl(x) (x)
#define ltohs(x) (x)
#define htoll(x) (x)
#define htols(x) (x)
#elif __BYTE_ORDER == __BIG_ENDIAN
#define ltohl(x) __bswap_32(x)
#define ltohs(x) __bswap_16(x)
#define htoll(x) __bswap_32(x)
#define htols(x) __bswap_16(x)
#endif
#define FATAL do { fprintf(stderr, "Error at line %d, file %s (%d) [%s]\n", __LINE__, __FILE__, errno, strerror(errno)); exit(1); } while(0)
#define MAP_SIZE (16*1024*1024UL)
#define MAP_MASK (MAP_SIZE - 1)
int main(int argc, char **argv)
{
int fd;
void *map_base, *virt_addr;
uint32_t read_result, writeval;
off_t target;
char *device;
if (argc != 3) {
fprintf(stderr,
"\nUsage:\t%s <device> <address> [[type] data]\n"
"\tdevice : character device to access\n"
"\taddress : memory address to access\n\n",
argv[0]);
exit(1);
}
device = strdup(argv[1]);
target = strtoul(argv[2], 0, 0);
fprintf("argc = %d, device: %s, address: 0x%08x\n", argc, device, (unsigned int)target);
if ((fd = open(argv[1], O_RDWR | O_SYNC)) == -1)
FATAL;
fprintf(stdout, "character device %s opened.\n", argv[1]);
fflush(stdout);
/* map one page */
map_base = mmap(0xe0000000, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map_base == (void *)-1)
FATAL;
fprintf(stdout, "Memory mapped at address %p.\n", map_base);
fflush(stdout);
/* calculate the virtual address to be accessed */
virt_addr = map_base + target;
/* read only */
read_result = *((uint32_t *) virt_addr);
/* swap 32-bit endianess if host is not little-endian */
read_result = ltohl(read_result);
printf("Read 32-bit value at address 0x%08x (%p): 0x%08x\n",
(unsigned int)target, virt_addr, (unsigned int)read_result);
if (munmap(map_base, MAP_SIZE) == -1)
FATAL;
close(fd);
return 0;
}
您似乎混淆了虚拟地址和物理地址。用户程序通常只能使用虚拟地址。 mmap
系统调用接受一个 hint 作为第一个参数:请求映射区域所需的 virtual 地址。有关详细信息,请参阅 man 2 mmap
。
您之前的程序最有可能发生的情况是对 mmap
的调用可能类似于:
map_area = mmap(NULL, /* same arguments here */);
这样,操作系统会选择一个合适的地址,return它。
您在新程序中所做的是让 OS 知道您更喜欢特定地址 (0xe...
),并且 OS 将内存映射到如果可能(很有可能)那个地址。你真的不应该需要这个,这个程序不管映射区域的位置如何,但无论如何你都可以保留它。
您遇到分段错误的原因是因为您正在映射一个 16 * 1024 * 1024
字节的区域 (0x01000000
),但是您正在以高于指定大小的偏移量访问内存(target >= 0x01000000
).
正确的方法是使用 mmap
的 offset
参数来请求从文件中适当的偏移量开始的映射。请求从该偏移量开始的两个页面的映射将确保您要读取或写入的内容将被正确映射(假设文件足够大,否则 MAP_FAILED
将被 returned)。
操作方法如下:
offset = target & 0xFFFFFFFFFFFFF000; // align target to page size
// Map two pages starting at 0xe... and corresponding to the calculated offset in the file.
map_base = mmap((void *)0xe0000000, 0x1000 * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, offset);
// ...
virt_addr = map_base + (target & 0xfff); // cut target to get offset within the mapped pages
read_result = *((uint32_t *) virt_addr);
read_result = ltohl(read_result);
printf("Read 32-bit value at address 0x%08x (%p): 0x%08x\n",
(unsigned int)target, virt_addr, (unsigned int)read_result);