行业新闻

剖析脏牛1_mmap如何映射内存到文件

剖析脏牛1_mmap如何映射内存到文件

 

测试程序

int fd;
struct stat st;
void *mem;

void processMem(void)
{
    char ch = *((char*)mem);
    printf("%c\n", ch);
}

int main(void)
{
    fd = open("./test", O_RDONLY);
    fstat(fd, &st);
    mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

    processMem();
}

 

进入MMAP

  • exp调用mmap

最终变成syscall(9)

 

sys_mmap()

  • 进入entry_SYSCALL_64切换内核保存用户栈, 然后通过sys_call_table调用到sys_mmap()函数

  • 进行一些简单检查后进入sys_mmap_pgoff()

 

sys_mmap_pgoff()

  • 就干了两件事: 根据fd找到文件对象, 然后调用vm_mmap_pgoff

 

vm_mmap_pgoff()

  • vm_mmap_pgoff()
    • 获得mmap_sem这个信号量
    • 调用do_mmap_pgoff()
    • 如果需要的话调用mm_populate()

 

do_mmap_pgoff()

  • 这是个do_mmap()的包装函数

 

do_mmap()

  • 进行一些准备工作:
    • 首先获取当前进程的内存描述符
    • 然后处理一下基地址和内存权限
    • 再调用get_unmapped_area()从当前进程的虚拟地址空间中找到一片空闲区域

  • 这里找到的空闲地址是0x00007ffff7ffa000

  • 接着处理标志, 并进行一些简单的检查

  • 如果要映射到文件, 那么根据映射类型进行一些文件相关的检查

  • file->f_op对于文件对象的方法集合, 这里其mmap方法对应shmem_mmap函数

  • 接着调用mmap_region()进行真正的映射工作, 并处理populate标志

 

mmap_region()

  • mmap_region()首先进行三个小检查:
    • 这个进程的虚拟地址空间还够不够映射
    • 如果这片区域已经有映射了, 那么就取消掉
    • 如果是私有可写入映射, 检查下内存还够不够进行写时映射

  • 接着会申请一个VMA对象用于描述映射的虚拟内存区域
    • 可以直接扩展一个已有的VMA, 比如权限相同地址相邻, 那么就不用分配一个新的VMA对象了
    • 否则从内核中分配一个00初始化的VMA对象并初始化

  • 接着对新的VMA对象进行一些文件相关初始化操作, 最重要的是两步
    • 获取一个文件对象, 增加引用计数: get_file()
    • 调用文件对象的f_op->mmap()方法设置VMA

  • 对于./test文件, 会调用shmem_file_operations.mmap, 也就是shmem_mmap()函数, 这个函数做两件事:
    • touch一下文件, 表示访问过
    • 设置这片虚拟内存的标准操作为shmem_vm_ops

  • vma->vm_ops是这片VMA上的各种操作集合, 对应struct vm_operations_struct结构体

  • shmem_vm_ops则是文件对象实现的, 当VMA发生某些事件时需要调用的函数
    • 其中最重要的就是缺页异常处理函数shmem_fault()
    • 缺页异常会在后面说到, 限制只要知道如果访问这片新建的VMA发生缺页异常, 就会调用vma->vm_ops->fault(), 也就是调用shmem_fault()函数

  • 然后把新建的VMA对象插入到mm内部从而完成VMA的创建工作

  • 最后进入out部分, 进行一些收尾工作并设置这片虚拟内存中每一页的属性(vm_page_prot)后结束映射

 

退出系统调用

  • 首先所有函数栈一路回退到entry_SYSCALL_64, 这部分就是正常的C回退, 只是在内核地址空间发生而已

  • 回退到entry_SYSCALL_64后, 先把SyS_mmap()的返回值写入到内核栈上保存的pt_regs->rax中
    pt_regs用来保存陷入内核时的CPU状态, 离开内核态时就用pt_regs恢复用户态的执行环境, 从而继续执行
  • 把pt_regs->rax设置为返回值, 就可以在恢复用户的执行环境时设置rax寄存器为返回值, 从而符合C的函数调用约定

  • 由于退出系统调用的指令sysret会使用%rcx设置%rip, %r11设置EFLAGS, 因此要用内核栈中pt_regs->RIP设置%rcx, pt_regs->EFLAGS设置%r11

  • 然后用pt_regs恢复其余通用寄存器

  • 然后用pt_regs->rsp设置%rsp, 从而恢复用户态的栈

  • 最后swapgs指令从MSR寄存器中换出用户态的gs, 并保存内核态的gs, 最后调用sysret恢复到用户态的执行

 

为什么没有读文件?

  • 到这里我们会发现一个问题, mmap只是根据参数在进程的mm中插入了一个VMA对象, 并没有真正的把文件中的信息读入, 这是为什么?
  • 这其实是请求调页与写时复制的结果, 虽然映射到了文件,但不代表全部都会用到, 当你真正读写刚刚映射的内存区域时, MMU会发出一个缺页异常给CPU, CPU调用内核的缺页异常处理函数, 这时候再真正的分配物理内存或者把文件的内容读到物理内存中, 实现按需分配
  • 后续留到下一个文章中细说
关闭