【技術分享】剖析臟牛1_mmap如何映射內存到文件
VSole2022-08-03 08:20:30

測試程序
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調用內核的缺頁異常處理函數, 這時候再真正的分配物理內存或者把文件的內容讀到物理內存中, 實現按需分配
后續留到下一個文章中細說
VSole
網絡安全專家