zl程序教程

您现在的位置是:首页 >  系统

当前栏目

mmap 匿名映射及Linux@X86_64 平台验证/dev/mem节点内存映射

2023-09-11 14:15:46 时间

mmap函数原型:

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
addr:推荐映射定义,如果为NULL,内核自动分配,如果flags设置了MAP_FIXED且addr合法,则映射addr开始的地址并返回。
length: length是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起,一般为PAGE整数倍,不足一内存页按一内存页处理.
prot:映射读写属性等等,期望的内存保护标志,不能与文件的打开模式冲突.
flags:私有,共享,匿名等等
    MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
    MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
    MAP_FIXED:映射addr开始的地址,如果试下存在VMA,则删除原来的在做映射
fd:对于file backend映射,需要指定文件fd,如果为匿名映射,则为-1.
offset:文件内字节从偏移处开始映射

根据内核代码,offset参数的单位是字节,内核在调用ksys_mmap_pgoff前会将其单位转换为page。内核中mmap函数的vm_pgoff来源于off >> PAGE_SHIFT,在mmap实现中可以根据此映射fd文件的不同区域。

映射属性:

映射建立后,会在进程的地址空间中创建一个struct vm_area_struct对象,来描述虚拟地址空间的信息,信息包括起始地址,区域大小,映射属性FLAG等等,FLAG包括:

VM_IO: Memory Maped I/O or similary.(MMIO).

VM_DONTCOPY: 在fork子进程的时候不要拷贝这个部分。

VM_DONTEXPAND: 不能用mremap 函数扩展映射区域。

VM_DONTDUMP:在coredump时不保存这个部分。

VM_PFNMAP: 这个比较重要,它表示这个映射针对的是PURE PFN,纯物理空间,没有struct page 对其进行管理。所以有些情况下,使用受到限制,通常设备的IO映射以及PCI BAR空间的映射,都会带有这个标志。比如AMD KFD GPU驱动中在IO映射时就设置了这个标志:

/dev/mem设备节点可以将物理内存全部映射到用户态,这里实践一把。

配置:

为了启用/dev/mem设备节点,并且BYPASS调如下的检查逻辑,需要设置内核如下的配置单:

CONFIG_DEVMEM=y
# CONFIG_STRICT_DEVMEM is not set
# CONFIG_X86_PAT is not set

配置CONFIG_X86_PAT的目的是DISABLE如下的检查。

然后参照如下博客进行编译升级

Ubuntu18.04 Linux内核编译升级_papaofdoudou的博客-CSDN博客_ubuntu18.04升级内核

/dev/mem访问测试代码:

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

int main()
{
    unsigned char * map_base;
    FILE *f;
    int n, fd;

    fd = open("/dev/mem", O_RDWR|O_SYNC);
    if (fd == -1)
    {
        printf("%s line %d.failure to open dev mem\n", __func__, __LINE__);
        return (-1);
    }

    map_base = mmap(NULL, 0x200000*512, PROT_READ, MAP_SHARED, fd, 0x100000*0);

    if (map_base == 0)
    {
        printf("NULL pointer!\n");
    }
    else
    {
        printf("Successfull!\n");
    }

    unsigned int* addr;
    unsigned int content;

    unsigned int i = 1;
    for (; i; ++i)
    {
        addr = (unsigned int*)(map_base + i*4);
        content = addr[0];
        if(content == 0x51258d48)
	{
		printf("found\n");
		break;
	}
        if(content == 0x488d2551)
	{
		printf("found\n");
		break;
	}
        printf("address: 0x%p   content 0x%x, i %d \t\t\n", addr, (unsigned int)content, i);

//map_base[i] = i;
//content = map_base[i];
//        printf("updated address: 0x%lx   content 0x%x\n", addr, (unsigned int)content);
    }

    close(fd);

    munmap(map_base, 0xff);

    return (1);
}

运行:

经过验证,和用hexdump工具直接读取/dev/mem节点获取的内容是相同的。

以上奇奇怪怪的数据可能和post_alloc_hook中对分配的页面做posion操作有关,post_alloc_hook->kernel_poison_pages->poison_pages->poison_page->memset(addr, PAGE_POISON, PAGE_SIZE);

分析内和代码,mmap的几种方式

其中,关键变量mmap_base的初始化在:

根据面向对象的思想,自己定义的成员也应该由自己定义的函数处理,所以,这里定义了一个会回调函数get_unmapped_area来查找空闲的没有经过映射的区域,里所应当,里面会根据mmap_base去查找这样的区域。

而且貌似这里有两种映射方案mmap_is_legacy判断是否是传统方案,我们上面提供的图是最新方案,topdown,顾名思义,从上到下分配,向下分配的。

而另一种方案如下图,是向上分配的:

调用分配的地点:

mmap方式在libc库的实现中也有用处,用户常常使用的malloc函数,会根据分配size的大小情况走mmap分配或者brk分配方式,具体分析可见下面博客。

musl libc库的编译以及malloc实现简析_papaofdoudou的博客-CSDN博客_musl-gcc

匿名映射:

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <errno.h>
 
#define PAGE_SIZE 4096
 
int main(int argc, char *argv[])
{
    void *maddr;
 
    maddr = mmap(NULL, 3 * PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
    if(maddr == (void*)-1) {
        printf("%s line %d, faild to map buffer, error %s.\n", __func__, __LINE__, strerror(errno));
        return -1;
    }
 
    printf("%s line %d maddr = %p errno %s.\n", __func__, __LINE__, maddr, strerror(errno));
    unsigned int *p = (unsigned int *)maddr;
    *p = 0xdeadbeef;
    while(1)
    {
        *p = *p + 1;
        sleep(1);
        printf("%s line %d, *p = 0x%08x, p = %p.\n", __func__, __LINE__, *p, p);
    }
 
    return 0;
}

二次映射

调用mmap两次,第一次进行匿名映射,占据vma空间,第二次使用第一次返回的地址做MAP_FIXED固定映射,得到的返回地址和第一次的返回地址相同,代码如下:

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

#define FILE_SIZE 0x4000

int main(void)
{
	int f;
	int i;
	unsigned char *mapped;
	void *maddr;

	maddr = mmap(NULL, FILE_SIZE, /* PROT_READ | PROT_WRITE */ PROT_NONE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
	if(maddr == (void*)-1) {
		printf("%s line %d, faild to map buffer, error %s.\n", __func__, __LINE__, strerror(errno));
		return -1;
	}

	printf("%s line %d maddr = %p errno %s.\n", __func__, __LINE__, maddr, strerror(errno));

	f = open("record.dat", O_RDWR);
	if(f < 0) {
		perror("fatal error, open file failure.\n");
		return -1;
	}

	mapped = (unsigned char*)mmap(maddr, FILE_SIZE, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, f, 0);
	if(mapped == (unsigned char*)-1) {
		printf("%s line %d, faild to map buffer, error %s.\n", __func__, __LINE__, strerror(errno));
		return -1;
	}

	if(mapped != maddr) {
		printf("%s line %d, mapped and maddr is not identical.\n", __func__, __LINE__);
	} else {
		printf("%s line %d, mapped and maddr is  identical.\n", __func__, __LINE__);
	}

	printf("%s line %d, mapped = %p, maddr = %p.\n", __func__, __LINE__, mapped, maddr);

	for(i = 0; i < 16; i ++) {
		printf("0x%02x ", mapped[i]);
	}

	printf("\n");

	msync((void *)mapped, FILE_SIZE, MS_ASYNC);
	munmap((void *)mapped, FILE_SIZE);

	close(f);

	return 0;
}
$ ./a.out
main line 24 maddr = 0x7f64de281000 errno Success.
main line 41, mapped and maddr is  identical.
main line 44, mapped = 0x7f64de281000, maddr = 0x7f64de281000.
0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f

/dev/mem的映射逻辑

/dev/mem 映射的是Linux全系统内存,并不仅仅是映射的物理内存,而是包括 brom, sram, iomem,dram 在内的所有地址空间.

虚拟内存映射过量怎么办?

虚拟内存过量提交,是指所有进程提交的虚拟内存的总和超过物理内存的容量,内核中的检测函数为__vm_enough_memory,如果申请的VM过量,则此函数返回-ENOMEM,新的VMA将无法创创建成功。

此函数的调用HOOK在内核所有的VMA创建流程中,比如进程创建,共享内存创建以及mmap中。

另外,可以通过proc文件系统节点来设置过量提交的策略,内核支持三种策略,可自行查阅。

$ cat /proc/sys/vm/overcommit_memory
0

用户态使用/dev/mem节点开发应用的例子,下图的例子来源于VIP的测试用例:

而phys_buffer_addr 恰好来源的就是环境变量中的物理地址设置:

export PHYSICAL_ADDR=0x48000000

export PHYSICAL_SIZE=0x200000

PCI BAR空间的 /dev/mem映射

#include<stdio.h>
#include<unistd.h>
#include<sys/mman.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
 
int main()
{
    unsigned char * map_base;
    FILE *f;
    int n, fd;
 
    fd = open("/dev/mem", O_RDWR|O_SYNC);
    if (fd == -1)
    {
        printf("%s line %d.failure to open dev mem\n", __func__, __LINE__);
        return (-1);
    }
 
    map_base = mmap(NULL, 64*1024, PROT_READ, MAP_SHARED, fd, 0xa4100000);
    if (map_base == 0)
    {
        printf("NULL pointer!\n");
    }
    else
    {
        printf("map successfull!\n");
    }
 
    unsigned char* addr;
    unsigned int i;
    addr = (unsigned char*)(map_base);

    for (i = 0; i < 256; i ++)
    {
        if(i % 16 == 0) {
		printf("\n 0x%p:", addr + i);
	}
	printf("0x%02x  ", addr[i]);
    }
    printf("\n");
 
    close(fd);
    munmap(map_base, 64*1024);
 
    return (1);
}
 

BIOS ROM映射:


结束