Skip to content
On this page

分段

linux并没有利用分段实现虚拟内存, 但是却利用了dpl功能实现了权限控制. 用户态DPL是3, 内核态DPL是0, 当用户态程序 CPL为3时直接访问内核态的地址时,会因权限不足而报错。所以要通过syscal, int等特别指令触发切换, 这些指令会改变CPL值

DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = {
	/*
	 * We need valid kernel segments for data and code in long mode too
	 * IRET will check the segment types  kkeil 2000/10/28
	 * Also sysret mandates a special GDT layout
	 *
	 * TLS descriptors are currently at a different place compared to i386.
	 * Hopefully nobody expects them at a fixed place (Wine?)
	 */
	[GDT_ENTRY_KERNEL32_CS]		= GDT_ENTRY_INIT(0xc09b, 0, 0xfffff),
	[GDT_ENTRY_KERNEL_CS]		= GDT_ENTRY_INIT(0xa09b, 0, 0xfffff),
	[GDT_ENTRY_KERNEL_DS]		= GDT_ENTRY_INIT(0xc093, 0, 0xfffff),
	[GDT_ENTRY_DEFAULT_USER32_CS]	= GDT_ENTRY_INIT(0xc0fb, 0, 0xfffff),
	[GDT_ENTRY_DEFAULT_USER_DS]	= GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff),
	[GDT_ENTRY_DEFAULT_USER_CS]	= GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff),
} };

虚拟地址映射

x86_64下面分页机制采用四级目录, 相关数据结构在arch/x86/include/asm/pgtable_64_types.h 如果cpu支持5级分页, 当前centos9会自动激活这个特性

#define PGDIR_SHIFT	39
#define PTRS_PER_PGD	512
/*
 * 3rd level page
 */
#define PUD_SHIFT	30
#define PTRS_PER_PUD	512

/*
 * PMD_SHIFT determines the size of the area a middle-level
 * page table can map
 */
#define PMD_SHIFT	21
#define PTRS_PER_PMD	512

/*
 * entries per page directory level
 */
#define PTRS_PER_PTE	512

#define PMD_SIZE	(_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK	(~(PMD_SIZE - 1))
#define PUD_SIZE	(_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK	(~(PUD_SIZE - 1))
#define PGDIR_SIZE	(_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK	(~(PGDIR_SIZE - 1))

x86_64的虚拟地址是64位, 但只使用48位用于映射到物理地址.因为处理器地址只有48条,要求内存地址48位到63位必须相同, 具体四级目录用到的位数如下:
PGD(9) + PUD(9) + PMD(9) + PTE(9) + 页内偏移(12)

查询当前系统一页的大小

[root@localhost ~]# getconf PAGE_SIZE
4096

进程空间

进程空间分为用户态地址空间和内核态地址空间, 32位下面用户态空间是3G, 内核态时1G. 分界线为宏#define TASK_SIZE, 这个定义了用户态空间的最大地址, 根据下面的宏, 计算出x86_64下面为0x00007FFFFFFFF000

#define TASK_SIZE		(test_thread_flag(TIF_ADDR32) ? \
					IA32_PAGE_OFFSET : TASK_SIZE_MAX)

#define TASK_SIZE_MAX	((1UL << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)

#ifdef CONFIG_X86_5LEVEL
#define __VIRTUAL_MASK_SHIFT	(pgtable_l5_enabled() ? 56 : 47)
#else
#define __VIRTUAL_MASK_SHIFT	47
#endif
crash> struct mm_struct -x 0xffff99da00e79540 | grep size
    task_size = 0x7ffffffff000,
crash>

x86_64下
用户空间 0x0000000000000000 ~ 0x00007FFFFFFFF000 128T
内核空间 0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF 128T
0x00007FFFFFFFF000 到 0xFFFF800000000000 为空洞区域

在execve时,将该值设置到mm_struct上面去

	/* Set the new mm task size. We have to do that late because it may
	 * depend on TIF_32BIT which is only updated in flush_thread() on
	 * some architectures like powerpc
	 */
	current->mm->task_size = TASK_SIZE;
crash> struct mm_struct.task_size -x 0xffff9f814be29f80
    task_size = 0x7ffffffff000
crash>

在x86_64, 内核态从0xffff888000000000 开始映射整个物理内存

#define __PAGE_OFFSET_BASE_L4	_AC(0xffff888000000000, UL)

#ifdef CONFIG_DYNAMIC_MEMORY_LAYOUT
#define __PAGE_OFFSET           page_offset_base

如果打开了kaslr, 在kernel_randomize_memory里会对page_offset_base随机向上偏移一些位置, 如下是实际运行的cents8里的值:

crash> px page_offset_base
page_offset_base = $20 = 0xffff9f7f40000000
crash>

参考文档: Documentation/x86/x86_64/mm.txt

用户态

在load_elf_binary函数里 setup_new_exec里设置mm->mmap_base和mm->task_size, kernel.randomize_va_space = 2表示需要随机化部分区域的起始地址, 包括mmap, stack等
setup_arg_pages里设置mm->arg_start和mm->start_stack, 此时这两个值一样
create_elf_tables 重新设置了mm->start_stack
start_stack指栈底, 它与arg_start之前存放了一些信息, 比如执行命令的参数个数和每一个参数的具体字符串指针, 每一个环境变量的指针. arg_start ~ arg_end, env_start ~ env_end 之间才是真正存放这些数据的地方. 在程序内部改变这些指针值就能改变参数和环境变量信息

内核态

page_offset_base开始的64T范围内是直接映射内存, 这些虚拟地址对应的物理地址就是 减去page_offset_base
page_offset_base默认是0xffff888000000000, 如果CONFIG_RANDOMIZE_MEMORY=y则会随机偏移些
每个进程对应的task_struct分配在这个区域, 可以减去page_offset_base直接得到物理地址

crash> vtop ffff9e68002398c0
VIRTUAL           PHYSICAL
ffff9e68002398c0  1002398c0

PGD DIRECTORY: ffffffffb2e10000
PAGE DIRECTORY: 1c601067
   PUD: 1c601d00 => 1c606067
   PMD: 1c606008 => 1057a9063
   PTE: 1057a91c8 => 8000000100239063
  PAGE: 100239000

      PTE         PHYSICAL   FLAGS
8000000100239063  100239000  (PRESENT|RW|ACCESSED|DIRTY|NX)

      PAGE        PHYSICAL      MAPPING       INDEX CNT FLAGS
ffffdba004008e40 100239000 dead000000000008        0  0 17ffffc0000000
crash> px page_offset_base
page_offset_base = $15 = 0xffff9e6700000000
crash> eval ffff9e68002398c0 - 0xffff9e6700000000
hexadecimal: 1002398c0
    decimal: 4297300160
      octal: 40010714300
     binary: 0000000000000000000000000000000100000000001000111001100011000000
crash>

vmalloc_base从 0xffffc90000000000UL开始, 随机偏移后, 可通过如下命令获取当前值

crash> px vmalloc_base
vmalloc_base = $16 = 0xffffb9cb00000000
crash>

vmemmap_base从 0xffffea0000000000UL开始, 随机偏移后, 可通过如下命令获取当前值, 存放 struct page

crash> px vmemmap_base
vmemmap_base = $17 = 0xffffdba000000000
crash>

内核的代码段从__START_KERNEL_map开始, 对应的物理地址是减去 __START_KERNEL_map 加上 phys_base

#define __START_KERNEL_map	_AC(0xffffffff80000000, UL)

物理分配

当前主流都是numa结构, 即一个CPU对应本地内存, 当本地内存不够, 再通过总线访问其他节点的内存. 内存管理的最小单位是页, 通常是4K, 它属于Zone, Zone属于node节点节. node节点就是numa节点

如下表示该OS可以支持 1<< 10 == 1024个numa节点

[root@localhost ~]# grep CONFIG_NODES_SHIFT /boot/config-5.14.0-22.el9.x86_64
CONFIG_NODES_SHIFT=10

一台4u8G的机器, 只有一个numa. cpu0~3属于node0

[root@localhost ~]# lscpu | grep NUMA
NUMA node(s):                    1
NUMA node0 CPU(s):               0-3

node0里面的跟物理内存的相关的数据如下

struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;
EXPORT_SYMBOL(node_data);
crash> p node_data[0]
$22 = (pg_data_t *) 0xffff94e6dffd1000
crash>  struct pg_data_t.node_id,nr_zones,node_start_pfn,node_present_pages,node_spanned_pages -x 0xffff94e6dffd1000
  node_id = 0x0,
  nr_zones = 0x3,
  node_start_pfn = 0x1,              //从页号1开始
  node_present_pages = 0x1fff8e,     //该node管理0x1fff8e个可用的页. 
  node_spanned_pages = 0x21ffff,     //管理0x21ffff页(8G), 除了包含present_pages, 还包含了空洞的物理地址. 这些页不可用.
crash> struct zone.name,zone_start_pfn,spanned_pages,present_pages,managed_pages -x ffff94e6dffd1000 5
  name = 0xffffffffa9fa3ed3 "DMA",
  zone_start_pfn = 0x1,
  spanned_pages = 0xfff,
  present_pages = 0xf9e,
  managed_pages = {
    counter = 0xf00
  },

  name = 0xffffffffa9f4c32c "DMA32",
  zone_start_pfn = 0x1000,
  spanned_pages = 0xff000,
  present_pages = 0xdeff0,
  managed_pages = {
    counter = 0xcaff0
  },

  name = 0xffffffffa9f4c222 "Normal",
  zone_start_pfn = 0x100000,
  spanned_pages = 0x120000,
  present_pages = 0x120000,
  managed_pages = {
    counter = 0x1145ec
  },

  name = 0xffffffffa9f4c229 "Movable",
  zone_start_pfn = 0x0,
  spanned_pages = 0x0,
  present_pages = 0x0,
  managed_pages = {
    counter = 0x0
  },

  name = 0xffffffffa9f9924d "Device",
  zone_start_pfn = 0x0,
  spanned_pages = 0x0,
  present_pages = 0x0,
  managed_pages = {
    counter = 0x0
  },
crash>

0x1fff8e个可用页等于 8388152K

[root@localhost ~]# dmesg -T | grep Mem
[Sun Dec 26 11:12:09 2021] Memory: 3442876K/8388152K available (14345K kernel code, 5931K rwdata, 8944K rodata, 2656K init, 5448K bss, 556100K reserved, 0K cma-reserved)

上面可以看到64位下有三个zone, 分别是 DMA, DMA32, Normal
struct page代表一页, 页通过伙伴系统管理. 所有空闲页挂在11个页块链表上. 每个链表包含相同连续页的地址. 有 1、2、4、8、16、32、64、128、256、512 和 1024. 所以一次最大可申请1024个物理地址连续的页(即4M的内存). 这些链表存在struct zone.free_area
第 i 个页块链表中,页块中页的数目为 2^i

#define MAX_ORDER 11

order为i时, 意味着申请 2 ^ i 个连续页, 如果free_area[i]没有, 则去free_area[i+1]里面找, 依次类推

Released under the MIT License.