基于 Linux 5.15 内核源码分析
https://elixir.bootlin.com/linux/v5.15/source
什么是 mmap?
mmap 是 Linux 系统的一种内存映射机制,允许将文件或设备直接映射到进程的虚拟地址空间。进程通过操作内存指针即可读写文件,无需调用 read() / write() 等系统调用。
mmap 在 Linux 系统中广泛被使用:内存分配(malloc 大块)、零拷贝网络传输、进程间通信、大文件处理等。
mmap 函数原型
1
2
3
|
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
|
参数说明:
| 参数 |
类型 |
描述 |
addr |
void * |
期望的映射起始地址,通常为 NULL(内核选择) |
length |
size_t |
映射长度(字节) |
prot |
int |
保护标志:PROT_READ / PROT_WRITE / PROT_EXEC / PROT_NONE |
flags |
int |
映射标志:MAP_SHARED / MAP_PRIVATE / MAP_ANONYMOUS / MAP_FIXED 等 |
fd |
int |
文件描述符(文件映射);匿名映射传 -1 |
offset |
off_t |
文件偏移量(需页对齐) |
返回值: 成功返回映射的虚拟地址,失败返回 MAP_FAILED(即 (void *)-1)。
背景知识:物理内存管理
要理解 mmap 的内核实现,必须先理解两个基础:物理内存如何组织,以及虚拟地址如何翻译为物理地址。
页(Page)
Linux 内核将物理内存划分为 4KB 大小的单元,称为页(Page)。每个物理页由一个 struct page 描述(include/linux/mm_types.h:70):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct page {
unsigned long flags; /* 页面的状态标志 */
atomic_t _count; /* 页面的引用计数 */
atomic_t _mapcount; /* 页面被映射的次数 */
union {
struct {
struct address_space *mapping; /* 页面所属的地址空间 */
pgoff_t index; /* 页面在文件中的偏移量 */
};
struct kmem_cache *slab; /* 如果页面属于 SLAB 缓存,指向缓存 */
struct page *next; /* 链表指针 */
};
void *virtual; /* 页面在内核空间的虚拟地址 */
/* ... */
};
|
arm64 四级页表
ARM64 架构(4K 页、48 位虚拟地址)使用四级页表映射,各层级全称如下:
| 缩写 |
英文全称 |
中文含义 |
| PGD |
Page Global Directory |
页全局目录 |
| PUD |
Page Upper Directory |
页上级目录 |
| PMD |
Page Middle Directory |
页中间目录 |
| PTE |
Page Table Entry |
页表项 |
| P4D |
Page 4th Directory |
页四级目录(某些配置下被折叠) |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
虚拟地址 VA[47:0] 分解(arm64, 4K页, 48位VA):
┌────────────┬────────────┬────────────┬────────────┬────────────┐
│ PGD 索引 │ PUD 索引 │ PMD 索引 │ PTE 索引 │ 页内偏移 │
│ bits │ bits │ bits │ bits │ bits │
│ [47:39] │ [38:30] │ [29:21] │ [20:12] │ [11:0] │
├────────────┼────────────┼────────────┼────────────┼────────────┤
│ 9 bits │ 9 bits │ 9 bits │ 9 bits │ 12 bits │
│ 512 项 │ 512 项 │ 512 项 │ 512 项 │ 4KB 页 │
│ 每项 8B │ 每项 8B │ 每项 8B │ 每项 8B │ │
└─────┬──────┴─────┬──────┴─────┬──────┴─────┬──────┴────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────────┐
│ PGD │→ │ PUD │→ │ PMD │→ │ PTE │→ │ 物理页 │
│ 页表 │ │ 页表 │ │ 页表 │ │ 页表 │ │ (4KB) │
└────────┘ └────────┘ └────────┘ └────────┘ └──────────────┘
↑ ↑ ↑ ↑
CR3/PGDIR PGD 指向 PUD 指向 PMD 指向
物理地址 PUD 表 PMD 表 PTE 表
|
页表层级常量(arch/arm64/include/asm/pgtable-hwdef.h:41 及之后):
| 层级 |
英文全称 |
位移宏 |
值(4K页, 48位VA) |
每条目覆盖范围 |
| PGD |
Page Global Directory |
PGDIR_SHIFT |
39 |
512 GB |
| PUD |
Page Upper Directory |
PUD_SHIFT |
30 |
1 GB |
| PMD |
Page Middle Directory |
PMD_SHIFT |
21 |
2 MB |
| PTE |
Page Table Entry |
PAGE_SHIFT |
12 |
4 KB |
⚠️ 注意:表中 “512 GB” 是 **PGD 的一个条目(entry)**所覆盖的地址范围。PGD 页表共有 PTRS_PER_PGD = 512 个条目(9 位索引),因此 PGD 整张表可覆盖的总地址空间为 512 × 512 GB = 256 TB。在 arm64 48 位 VA 下,用户空间大小 TASK_SIZE_64 = (1UL << vabits_actual) = 1UL << 48 = 256 TB(arch/arm64/include/asm/processor.h:53),即整个 lower 48-bit 范围。一个进程能占用的虚拟地址空间受 TASK_SIZE 和物理内存/swap 总量的双重限制。
页表项数:每级页表 PTRS_PER_PTE = 1 << (PAGE_SHIFT - 3) = 512 项。
页表类型定义(arch/arm64/include/asm/pgtable-types.h):
1
2
3
4
|
typedef struct { pteval_t pte; } pte_t; /* PTE — Page Table Entry */
typedef struct { pmdval_t pmd; } pmd_t; /* PMD — Page Middle Directory */
typedef struct { pudval_t pud; } pud_t; /* PUD — Page Upper Directory */
typedef struct { pgdval_t pgd; } pgd_t; /* PGD — Page Global Directory */
|
虚拟地址 → 物理地址映射过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
虚拟地址映射物理地址的过程(四级页表):
进程 PGD 基地址记录在 task_struct->mm->pgd
CPU 选中进程后 → 页表基地址寄存器设为该值
步骤1: PGD[VA[47:39]] → 得到 PUD 页表物理基地址
步骤2: PUD[VA[38:30]] → 得到 PMD 页表物理基地址
步骤3: PMD[VA[29:21]] → 得到 PTE 页表物理基地址
步骤4: PTE[VA[20:12]] → 得到物理页基地址
步骤5: 物理页基地址 + VA[11:0] → 最终物理地址
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌─────────┐
│ PGD │ ──→ │ PUD │ ──→ │ PMD │ ──→ │ PTE │ ──→ │ 物理页 │
│[idx0]│ │[idx1]│ │[idx2]│ │[idx3]│ │ + offset│
└──────┘ └──────┘ └──────┘ └──────┘ └─────────┘
9位索引 9位索引 9位索引 9位索引 12位偏移
|
进程虚拟地址空间布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
完整的进程虚拟地址空间 (arm64 48位VA, 4K页):
┈┈┈ 从低地址到高地址 ┈┈┈
┌──────────────────────────────┐ 0x0000_0000_0000_0000
│ 代码段 (Text) │ ← 每个进程独立
├──────────────────────────────┤
│ 数据段 (Data) │ ← 已初始化全局变量(进程私有)
├──────────────────────────────┤
│ BSS 段 │ ← 未初始化全局变量
├──────────────────────────────┤
│ 堆 (Heap) — brk/sbrk │ ↓ brk 增大(地址增大)
├──────────────────────────────┤
│ │ │
│ 文件映射和匿名映射区 │ ← mmap 在此分配
│ │ │
├──────────────────────────────┤
│ 栈 (Stack) — 向低地址增长 │ ↑ rsp 减小(地址减小)
├──────────────────────────────┤
│ [vdso] / [vvar] │ ← 内核映射到用户空间的全局代码
├══════════════════════════════┤ 0x0000_FFFF_FFFF_FFFF ← 用户空间结束
│ 内核空间 │
│ ┌──────────────────────────┐│ ← 全局共享,所有进程相同
│ │ kernel logical map ││ 线性映射,直接访问物理内存
│ ├──────────────────────────┤│
│ │ [kasan shadow] 32TB ││ KASAN 影子内存
│ ├──────────────────────────┤│
│ │ bpf jit 128MB ││ BPF JIT 编译区
│ ├──────────────────────────┤│
│ │ modules 128MB ││ 驱动模块
│ ├──────────────────────────┤│
│ │ vmalloc 124TB ││ vmalloc() 分配
│ ├──────────────────────────┤│
│ │ [guard] + PCI I/O ││ 固定映射 / I/O 空间
│ ├──────────────────────────┤│
│ │ vmemmap 2TB ││ struct page 数组
│ ├──────────────────────────┤│
│ │ [guard region] 2TB ││
│ └──────────────────────────┘│ ffff_FFFF_FFFF_FFFF
└──────────────────────────────┘
|
arm64 内核地址空间完整分区表(从低到高排列): linux/Documentation/arm64/memory.rst
| Start |
End |
Size |
用途 |
0000000000000000 |
0000ffffffffffff |
256 TB |
user(用户空间) |
ffff000000000000 |
ffff7fffffffffff |
128 TB |
kernel logical memory map(内核线性映射,直接访问所有物理内存) |
ffff600000000000 |
ffff7fffffffffff |
32 TB |
[kasan shadow region](KASAN 影子内存) |
ffff800000000000 |
ffff800007ffffff |
128 MB |
bpf jit region(BPF JIT 编译区) |
ffff800008000000 |
ffff80000fffffff |
128 MB |
modules(模块加载区) |
ffff800010000000 |
fffffbffefffffff |
124 TB |
vmalloc(vmalloc/ioremap 分配区) |
fffffbfff0000000 |
fffffbfffdffffff |
224 MB |
fixed mappings(固定映射,top-down 分配) |
fffffbfffe000000 |
fffffbfffe7fffff |
8 MB |
[guard region] |
fffffbfffe800000 |
fffffbffff7fffff |
16 MB |
PCI I/O space(PCI IO 空间) |
fffffbffff800000 |
fffffbffffffffff |
8 MB |
[guard region] |
fffffc0000000000 |
fffffdffffffffff |
2 TB |
vmemmap(struct page 数组映射) |
fffffe0000000000 |
ffffffffffffffff |
2 TB |
[guard region] |
表中 “kernel logical memory map”(内核线性映射区)是内核访问物理内存的窗口:virt = phys + PAGE_OFFSET,128 TB 空间足以映射当前所有物理内存。vmalloc 用于非连续内存分配,vmemmap 用于存储每个物理页对应的 struct page。
关键分区说明:
· 用户空间 (0000_0000_0000_0000 ~ 0000_FFFF_FFFF_FFFF)(边界由 TASK_SIZE 定义arch/arm64/include/asm/processor.h)
每个进程独立,通过切换页表基地址寄存器隔离
· 内核空间 (FFFF_0000_0000_0000 ~ FFFF_FFFF_FFFF_FFFF)
所有进程全局共享,上下文切换时内核页表部分不变
· 全局变量 (.data / .bss)
位于用户空间数据段,是每个进程私有的副本(写时复制继承)。
内核线性映射(kernel logical memory map)才是真正全局共享的物理内存访问通道。
· [vdso] / [vvar]
内核映射到每个用户空间的"全局"代码页,加速 gettimeofday(),
所有进程看到的内容相同(映射位置可能因 ASLR 而异)
mmap 内核实现原理
有了背景知识,现在来看 mmap 在内核中是如何一步步完成的。
系统调用入口
arm64 入口代码 (arch/arm64/kernel/sys.c:21-29):
1
2
3
4
5
6
7
8
|
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, off)
{
if (offset_in_page(off) != 0)
return -EINVAL;
return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}
|
先看 mmap 的完整调用链:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
用户态 mmap()
│
▼
┌──────────────────────────────────────────────────────┐
│ arch/arm64/kernel/sys.c:21 SYSCALL_DEFINE6(mmap) │ ← 架构入口
│ 检查 offset 页对齐, off >> PAGE_SHIFT │
└──────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ mm/mmap.c:1583 ksys_mmap_pgoff() │ ← 内核通用
│ 解析 fd → struct file *,处理 hugepage │
└──────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ mm/util.c:506 vm_mmap_pgoff() │ ← 加锁审计
│ security_mmap_file() LSM 安全检查 │
│ mmap_write_lock_killable(mm) 获取写锁 │
└──────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ mm/mmap.c:1404 do_mmap() │ ← 核心创建
│ 地址选择、权限检查、vm_flags 计算 │
└──────────────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ mm/mmap.c:1716 mmap_region() │ ← VMA 安装
│ 分配 vm_area_struct、设置 vm_ops、插入红黑树 │
└──────────────────────────────────────────────────────┘
│
▼
mm_populate() (若 MAP_POPULATE 或 VM_LOCKED)
|
do_mmap() 处理流程
do_mmap() (mm/mmap.c:1404-1581) 是 mmap 的核心函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
do_mmap(file, addr, len, prot, flags, pgoff, &populate, &uf)
│
▼
┌─────────────────────┐
│ 参数合法性检查 │ ← len==0 → EINVAL; 地址对齐; 溢出
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ get_unmapped_area() │ ← 查找或验证可用虚拟地址
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 合成 vm_flags │ ← calc_vm_prot_bits + calc_vm_flag_bits
└─────────┬───────────┘
│
▼
┌──────────────┴──────────────┐
│ │
file != NULL file == NULL
(文件映射) (匿名映射)
│ │
▼ ▼
┌──────────────────┐ ┌───────────────────────┐
│ · 检查文件权限 │ │ · MAP_SHARED: pgoff=0 │
│ · 验证 f_op->mmap │ │ · MAP_PRIVATE: │
│ · MAP_SHARED │ │ pgoff=addr>>PSHIFT │
│ 需写权限 │ └──────────┬────────────┘
└────────┬─────────┘ │
│ │
└──────────┬─────────────────┘
│
▼
┌─────────────────────┐
│ mmap_region() │ ← 真正的 VMA 创建
└─────────────────────┘
|
mmap_region() — VMA 安装
mmap_region() (mm/mmap.c:1716-1871) 将 VMA 真正安装到进程地址空间:
| 步骤 |
代码位置 |
说明 |
| 地址空间检查 |
may_expand_vm() |
检查进程 RLIMIT_AS 限制 |
| 解除旧映射 |
munmap_vma_range() |
如果有旧映射先解除 |
| 尝试合并 VMA |
vma_merge() |
与相邻 VMA 合并 |
| 分配 VMA 结构体 |
vm_area_alloc() |
分配 struct vm_area_struct |
| 文件映射 |
call_mmap(file, vma) |
调用 f_op->mmap() 设置 vm_ops |
| 匿名共享映射 |
shmem_zero_setup(vma) |
创建 shmem 伪文件 |
| 匿名私有映射 |
vma_set_anonymous(vma) |
vm_ops = NULL |
| 插入树/链表 |
vma_link() |
插入红黑树和双向链表 |
| 统计 |
vm_stat_account() |
更新内存统计 |
内核页表映射函数调用链
mmap 建立地址映射的最终落脚点是填充页表。ARM64 的页表填充通过 arch/arm64/mm/mmu.c 中的函数链完成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
__create_pgd_mapping(pgdir, phys, virt, size, prot, alloc, flags) ← 第 363 行
│ 遍历 PGD 条目 (按 pgd_addr_end 分段)
│
└─ alloc_init_pud() ← 第 308 行
│ p4d_offset() 访问 P4D → __p4d_populate() 分配 PUD 表
│ 尝试 1GB 大块映射 (pud_set_huge)
│
└─ alloc_init_cont_pmd() ← 第 255 行
│ __pud_populate() 分配 PMD 表
│
└─ init_pmd() ← 第 218 行
│ pmd_set_fixmap_offset() 定位 PMD 条目
│ 尝试 2MB 大块映射 (pmd_set_huge)
│
└─ alloc_init_cont_pte() ← 第 179 行
│ __pmd_populate() 分配 PTE 表
│
└─ init_pte() ← 第 155 行
│ pte_set_fixmap_offset() 定位 PTE 条目
│ set_pte() 写入页表项
│ 逐页: pfn_pte(__phys_to_pfn(phys), prot)
|
文件映射 vs 匿名映射
内核通过 vm_ops 函数指针表实现多态机制,三种映射方式在 mmap_region() 中分支:
| 特性 |
文件映射 |
匿名共享 |
匿名私有 |
vm_file |
真实文件 struct file * |
shmem 伪文件 |
NULL |
vm_ops |
文件系统设置(如 ext4_file_vm_ops) |
&shmem_vm_ops |
NULL |
| 设置位置 |
call_mmap() + f_op->mmap() |
shmem_zero_setup() |
vma_set_anonymous() |
| 缺页处理 |
vma->vm_ops->fault() → 从磁盘读取 |
shmem_fault() → 共享内存 |
do_anonymous_page() → 零填充页 |
两种映射没有本质区别——区别仅在于映射的目的物理页不同:
- 文件映射:映射的物理地址指向文件的页缓存(page cache)。Linux 将磁盘文件预读取至页缓存,访问页缓存等同于访问磁盘文件。
- 匿名映射:映射的物理地址指向匿名页(由
do_anonymous_page() 分配的零填充页,或 shmem 管理的共享页)。
1
2
3
4
5
6
7
8
|
文件映射原理:
进程虚拟地址 页缓存 磁盘文件
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 虚拟地址 │ ──页表──→ │ 物理页 │ ──回写→ │ 文件数据 │
│ (VMA) │ │ (Page) │ ←预读取─ │ on │
└──────────┘ └──────────┘ │ Disk │
└──────────┘
|
mmap demo
以下示例展示 mmap 的三种典型用法,并结合 readelf、objdump、/proc/ 文件系统逐一分析。
示例程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
/*
* mmap_demo.c — 演示 mmap 三种典型用法
* 编译: gcc -O2 -o mmap_demo mmap_demo.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
int main(int argc, char *argv[])
{
/* ─── 1. 匿名私有映射 (分配大块内存) ─── */
size_t anon_len = 4096 * 10; /* 10 页 */
void *anon = mmap(NULL, anon_len,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (anon == MAP_FAILED) {
perror("匿名映射失败");
exit(1);
}
strcpy((char *)anon, "Hello from anonymous mmap!");
printf("[匿名映射] 地址: %p, 长度: %zu bytes, 内容: %s\n",
anon, anon_len, (char *)anon);
printf(" 按 ENTER 继续..."); getchar();
/* ─── 2. 文件映射 (读/写文件) ─── */
int fd = open("/tmp/mmap_test.txt",
O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { perror("open"); exit(1); }
/* 先准备好文件内容 */
const char *msg = "mmap file I/O demo\n";
write(fd, msg, strlen(msg) + 1);
size_t file_len = 4096; /* 映射 1 页 */
void *file_map = mmap(NULL, file_len,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (file_map == MAP_FAILED) {
perror("文件映射失败");
exit(1);
}
close(fd); /* 映射后可以关闭 fd */
printf("[文件映射] 地址: %p, 长度: %zu\n", file_map, file_len);
printf(" 文件内容: %s\n", (char *)file_map);
/* 通过内存修改文件内容 */
memcpy(file_map, "MODIFIED via mmap!", 19);
printf(" 修改后, 查看文件: $ cat /tmp/mmap_test.txt\n");
printf(" 按 ENTER 继续..."); getchar();
/* ─── 3. 匿名共享映射 (IPC 模拟) ─── */
size_t shm_len = 4096;
void *shared = mmap(NULL, shm_len,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
-1, 0);
if (shared == MAP_FAILED) {
perror("共享映射失败");
exit(1);
}
sprintf((char *)shared, "PID=%d shared data", getpid());
printf("[共享映射] 地址: %p, 内容: %s\n", shared, (char *)shared);
/* ─── 暂停, 便于观察 /proc/pid/maps ─── */
printf("\n查看进程内存映射: $ cat /proc/%d/maps\n", getpid());
printf("按 ENTER 退出清理..."); getchar();
munmap(anon, anon_len);
munmap(file_map, file_len);
munmap(shared, shm_len);
return 0;
}
|
编译与运行
1
2
|
$ gcc -O0 -o mmap_demo mmap_demo.c
$ ./mmap_demo
|
使用 readelf 分析 ELF 布局
先看编译后的程序本身有哪些内存区域会被内核加载:
1
|
$ readelf -S mmap_demo # 查看节区表 (Section Headers)
|
关键输出片段:
1
2
3
4
5
6
7
8
9
10
11
|
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[13] .text PROGBITS 0000000000001060 00001060
0000000000000b55 0000000000000000 AX 0 0 16
[16] .rodata PROGBITS 0000000000002000 00002000
0000000000000208 0000000000000000 A 0 0 8
[24] .data PROGBITS 0000000000004020 00003020
0000000000000108 0000000000000000 WA 0 0 8
[25] .bss NOBITS 0000000000004140 00003128
0000000000000028 0000000000000000 WA 0 0 8
|
| 节区 |
地址 |
文件偏移 |
说明 |
.text |
0x1060 |
0x1060 |
代码段,AX(可分配+可执行) |
.rodata |
0x2000 |
0x2000 |
只读数据(字符串常量),A |
.data |
0x4020 |
0x3020 |
已初始化全局数据,WA(可写) |
.bss |
0x4140 |
— |
未初始化全局数据(不占文件空间) |
再用 readelf -l 查看程序头(决定内核如何加载):
1
2
3
4
5
6
7
|
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x000208 0x000208 R 0x8
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x000878 0x000878 R 0x1000
LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x00bb5 0x00bb5 R E 0x1000
LOAD 0x002000 0x0000000000002000 0x0000000000002000 0x000208 0x000208 R 0x1000
LOAD 0x002dd8 0x0000000000003dd8 0x0000000000003dd8 0x000258 0x000390 RW 0x1000
|
每个 LOAD 段对应内核加载时需要创建的一个 VMA:
| 段 |
文件偏移 |
虚拟地址 |
文件大小 |
内存大小 |
标志 |
加载内容 |
| LOAD #1 |
0x000000 |
0x000000 |
0x878 |
0x878 |
R |
ELF header, 只读 |
| LOAD #2 |
0x001000 |
0x001000 |
0xbb5 |
0xbb5 |
R E |
.text 代码段 |
| LOAD #3 |
0x002000 |
0x002000 |
0x208 |
0x208 |
R |
.rodata |
| LOAD #4 |
0x002dd8 |
0x003dd8 |
0x258 |
0x390 |
RW |
.data + .bss |
注意第 4 个段:MemSiz > FileSiz,多出的部分就是 .bss(内核启动时清零)。
使用 objdump 反汇编 mmap 调用
反汇编 mmap 在 PLT 中的跳转和实际调用:
1
|
$ objdump -d mmap_demo | grep -A 20 '<main>'
|
关注 call 指令后的函数调用关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
0000000000001200 <main>:
1200: push %rbp
1201: mov %rsp,%rbp
1204: ... # 准备栈帧
# 匿名映射: mmap(NULL, 40960, PROT_READ|PROT_WRITE,
# MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
1258: mov $0xffffffff,%r9d # offset = -1
125e: mov $0x22,%r8d # flags = MAP_PRIVATE|MAP_ANONYMOUS
1264: mov $0x3,%ecx # prot = PROT_READ|PROT_WRITE
1269: mov $0xa000,%edx # length = 40960
126e: xor %esi,%esi # addr = NULL
1270: xor %edi,%edi
1272: call <mmap@plt> # → PLT → glibc → syscall
...
|
mmap@plt 会跳转到 glibc 的 mmap 包装函数,最终执行 syscall 指令陷入内核。
通过 /proc/ 观察运行时的内存映射
运行程序并在暂停时查看 /proc/<pid>/maps:
1
|
$ cat /proc/`pgrep mmap_demo`/maps
|
输出示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
5566c1a4d000-5566c1a4e000 r--p 00000000 08:01 2883744 mmap_demo # ELF 头(LOAD #1)
5566c1a4e000-5566c1a4f000 r-xp 00001000 08:01 2883744 mmap_demo # .text (LOAD #2)
5566c1a4f000-5566c1a50000 r--p 00002000 08:01 2883744 mmap_demo # .rodata (LOAD #3)
5566c1a50000-5566c1a51000 r--p 00003000 08:01 2883744 mmap_demo # .data relro
5566c1a51000-5566c1a52000 rw-p 00003000 08:01 2883744 mmap_demo # .data + .bss (LOAD #4)
5566c1b7d000-5566c1b7e000 rw-p 00000000 00:00 0 [heap] # 堆
7f0e8db8c000-7f0e8db9d000 rw-p 00000000 00:00 0 [anon] # ← 匿名映射 (10页)
7f0e8db9d000-7f0e8db9e000 rw-s 00000000 08:01 2883759 /tmp/mmap_test.txt # ← 文件映射
7f0e8db9e000-7f0e8db9f000 rw-s 00000000 00:00 0 [anon] # ← 匿名共享映射
...
7ffe96e50000-7ffe96e71000 rw-p 00000000 00:00 0 [stack] # 栈
7ffe96eef000-7ffe96ef3000 r--p 00000000 00:00 0 [vvar]
7ffe96ef3000-7ffe96ef5000 r-xp 00000000 00:00 0 [vdso] # 内核提供的 VDSO
|
每个字段的含义(以文件映射行为例):
1
2
3
4
5
6
|
7f0e8db9d000-7f0e8db9e000 rw-s 00000000 08:01 2883759 /tmp/mmap_test.txt
├────── VMA 范围 ──────┤ ├┤ ├─offset─┤ ├──┼──┼─├──── inode ────┤
│ 起始地址-结束地址 ││ 文件偏移 设备 inode 文件名
││
│└─ s = MAP_SHARED / p = MAP_PRIVATE
└── 权限: r=读 w=写 x=执行
|
再看更详细的 /proc/<pid>/smaps:
1
|
$ cat /proc/`pgrep mmap_demo`/smaps | grep -A 10 "7f0e8db9d000"
|
1
2
3
4
5
6
7
8
9
10
11
12
|
7f0e8db9d000-7f0e8db9e000 rw-s 00000000 08:01 2883759 /tmp/mmap_test.txt
Size: 4 kB # VMA 大小
KernelPageSize: 4 kB # 内核页大小
Rss: 4 kB # 实际驻留物理内存
Pss: 4 kB # 按共享比例分摊的物理内存
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 4 kB # 私有脏页
Referenced: 4 kB
Anonymous: 0 kB
VmFlags: rd wr sh mr mw me ms # VMA 标志
|
综合分析对照表
将 ELF 静态布局、内核加载、运行时 /proc/ 观测三方面对应起来:
| ELF 节区 (readelf) |
VMA 起始 (maps) |
权限 |
文件偏移 |
说明 |
.text (LOAD #2) |
0x5566c1a4e000 |
r-xp |
0x1000 |
代码段,readelf 地址+偏移一致 |
.rodata (LOAD #3) |
0x5566c1a4f000 |
r–p |
0x2000 |
只读数据 |
.data + .bss (LOAD #4) |
0x5566c1a51000 |
rw-p |
0x3000 |
可读写数据,MemSiz > FileSiz |
| 匿名 mmap |
0x7f0e8db8c000 |
rw-p |
0x00 |
MAP_ANONYMOUS, fd=-1 |
| 文件 mmap |
0x7f0e8db9d000 |
rw-s |
0x00 |
MAP_SHARED, fd=/tmp/... |
| 共享匿名 mmap |
0x7f0e8db9e000 |
rw-s |
0x00 |
`MAP_SHARED |
关键观察
- 匿名映射无后备文件:
/proc/pid/maps 中显示 [anon] 或 [heap],无文件名
- 文件映射关联 inode:
/proc/ 输出中可见设备号和 inode,与 ls -i 一致
- MAP_SHARED vs MAP_PRIVATE:权限字段末位
s vs p,决定写时复制行为
- 文件偏移来自 ELF LOAD 段:.text 段在文件偏移 0x1000,映射后 VMA 起始也看到偏移 0x1000
- 缺页按需填充:
smaps 中 Rss < Size(未访问的页尚未分配物理页)
关键源码文件索引
| 文件 |
内容 |
arch/arm64/kernel/sys.c:21 |
arm64 mmap 系统调用入口 |
mm/mmap.c:1404 |
do_mmap() 核心映射函数 |
mm/mmap.c:1583 |
ksys_mmap_pgoff() 内核通用入口 |
mm/mmap.c:1716 |
mmap_region() VMA 安装 |
mm/util.c:506 |
vm_mmap_pgoff() 加锁审计 |
arch/arm64/mm/mmu.c:363 |
__create_pgd_mapping() 页表创建 |
arch/arm64/include/asm/pgtable-types.h:23 |
页表类型(pte_t/pmd_t/pud_t/pgd_t) |
arch/arm64/include/asm/pgtable-hwdef.h:41 |
页表位移常量 |
include/linux/mm_types.h:319 |
struct vm_area_struct |
include/linux/mm_types.h:70 |
struct page |
include/linux/mm.h:588 |
struct vm_operations_struct |
一张图搞懂mmap实现原理