由systemtap直接修改内核代码段想到的
一、直接修改内核代码段
在386内核的kprobe实现过程中,其中有一个是对于代码段断点的安装,那个地方对于代码段的修改是轻松加惬意,就好象生活在新闻联播里一样,这让我们这些看惯了用户态进程各种保护的程序员来说还是比较震撼的,套用一句三俗的话来说:我和我的小伙伴们都惊呆了。
386中对于kprobe调试断点的位于下面的位置
void __kprobes arch_arm_kprobe(struct kprobe *p)
{
*p->addr = BREAKPOINT_INSTRUCTION;
flush_icache_range((unsigned long) p->addr,
(unsigned long) p->addr + sizeof(kprobe_opcode_t));
}
要注意的是,这个p->addr是一个内核代码段地址,按照我们通常的理解,这个代码段是不应该就这样被随随便便的写入数据并修改。那是不是有其它的地方在执行这个函数之前把可执行代码段的写保护给解除了呢?这个函数的调用在__register_kprobe函数中,在这个函数的上下文中,并没有找到关相关的解除写保护的操作,比较可以的函数调用有下面
if ((ret = arch_prepare_kprobe(p)) != 0)
goto out;
……
arch_arm_kprobe(p);
如果可能的话,就是有些处理器会在prepare中解除写保护,但是在386的实现中,同样没有对于写保护的解除,所以我们可以认为,内核对于可执行页面的确是没有写保护的,为了说明这个问题,把386的prepare函数在这里拷贝一份
int __kprobes arch_prepare_kprobe(struct kprobe *p)
{
/* insn: must be on special executable page on i386. */
p->ainsn.insn = get_insn_slot();
if (!p->ainsn.insn)
return -ENOMEM;
memcpy(p->ainsn.insn, p->addr, MAX_INSN_SIZE * sizeof(kprobe_opcode_t));
p->opcode = *p->addr;
if (can_boost(p->addr)) {
p->ainsn.boostable = 0;
} else {
p->ainsn.boostable = -1;
}
return 0;
}
二、内核的代码段如何初始化
这部分最为原始的代码使用汇编编写,代码位于linux-2.6.21\arch\i386\kernel\head.S文件中,我们把相关的代码列出来
/*
* Initialize page tables. This creates a PDE and a set of page
* tables, which are located immediately beyond _end. The variable
* init_pg_tables_end is set up to point to the first "safe" location.
* Mappings are created both at virtual address 0 (identity mapping)
* and PAGE_OFFSET for up to _end+sizeof(page tables)+INIT_MAP_BEYOND_END.
*
* Warning: don't use %esi or the stack in this code. However, %esp
* can be used as a GPR if you really need it...
*/
page_pde_offset = (__PAGE_OFFSET >> 20); 这个__PAGE_OFFSET就是大家经常听说的3G地址空间中的3G,这个宏获得内核其实地址的页目录
movl $(pg0 - __PAGE_OFFSET), %edi edi指针存放内核页表项的位置(物理地址,不是数值),其中pg0表示内核第一个页表项的位置,接下来汇编指令stosl要求目的地址存放在edi中。
movl $(swapper_pg_dir - __PAGE_OFFSET), %edx edx存放内核的页目录的物理地址,其中swapper_pg_dir为内核页目录的起始地址。
movl $0x007, %eax /* 0x007 = PRESENT+RW+USER */ 这里引出了开始的问题,内核页面的访问权限,其中的007的意义表示了之后说的权限,页面在内存中,可读可写,用户页面。由于在初始化过程中所有页面都是这种属性,所以代码段写入毫无压力。
10:
leal 0x007(%edi),%ecx /* Create PDE entry */ 取到edi的地址,也就是页表项的地址,放入ecx寄存器,
movl %ecx,(%edx) /* Store identity PDE entry */ 该值存入物理地址,其中ecx保存一个页表项的开始,这个页表项所在页面的1K个连续页表项将会对应页目录中的一条记录,而这个ecx就是这个页表项的起始地址,这个值首先存入物理地址的页目录中
movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */ 同样一份存入逻辑地址
addl $4,%edx 处理下一个页目录项
movl $1024, %ecx 遍历页目录对应的1K个页表项
11:
stosl
addl $0x1000,%eax 每个页表项中存储的地址长度增加一个页面,其中eax中值为页目录中的值(edi中为页目录地址)
loop 11b
/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp 这个地方的结束条件比较曲折,而且可能会影响到之后启动内存管理代码。除去这个INIT_MAP_BEYOND_END不谈,这里的%edi中存储的是当前当前页表项的位置,这个位置随着映射的增加而逐渐增加,因为这个页表项是迄今为止整个内核地址的最高端,这一点在接下来还会说明原因,这个页表项顶端放入ebp寄存器中。
cmpl %ebp,%eax eax存放的是当前所以被覆盖到的内核地址的最高端,并且包括了页表项的空间,因为页表项本身也是需要被映射入地址转换机制中。这里结束的条件就是所有页表项都被覆盖。这其实本质上是一个追击问题:页表项每增加一个(4字节),内核地址空间就增加4K,所以后者肯定可以追击到前者。
jb 10b
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
xorl %ebx,%ebx /* This is the boot CPU (BSP) */
jmp 3f
三、使用到变量的定义位置
1、pg0的定义位置
这个变量是一个链接脚本变量,定义在linux-2.6.21\arch\i386\kernel\vmlinux.lds.S文件中
.bss : AT(ADDR(.bss) - LOAD_OFFSET) {
__init_end = .;
__bss_start = .; /* BSS */
*(.bss.page_aligned)
*(.bss)
. = ALIGN(4);
__bss_stop = .;
_end = . ;
/* This is where the kernel creates the early boot page tables */
. = ALIGN(4096);
pg0 = . ;
}
/* Sections to be discarded */
/DISCARD/ : {
*(.exitcall.exit)
}
STABS_DEBUG
DWARF_DEBUG
NOTES
可以看到,它定义在整个为初始化数据段的最后,所以页表的位置可以动态增加而不会覆盖任何用户数据。
2、swapper_pg_dir的定义
linux-2.6.21\arch\i386\kernel\head.S
/*
* BSS section
*/
.section ".bss.page_aligned","w"
ENTRY(swapper_pg_dir)
.fill 1024,4,0
ENTRY(empty_zero_page)
.fill 4096,1,0
/*
* This starts the data section.
*/
.data
ENTRY(start_pda)
.long boot_pda
页表定义在bss段的开始,使用了一个页面,共1K个页目录,对应1K×1K×4K=4G,完整表示了32地址空间的所有地址,所以内核的整个页目录其实是只需要一个页面存储的。
顺便可以看到我们经常可以看到的empty_zero_page,通常一些用户态只读的保护页面就是被映射到这个地址,而设备文件/dev/zero也通常被映射到这里。
3、INIT_MAP_BEYOND_END宏
这个宏在之前的内核页面映射
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
中使用
/*
* This is how much memory *in addition to the memory covered up to
* and including _end* we need mapped initially. We need one bit for
* each possible page, but only in low memory, which means
* 2^32/4096/8 = 128K worst case (4G/4G split.)
*
* Modulo rounding, each megabyte assigned here requires a kilobyte of
* memory, which is currently unreclaimed.
*
* This should be a multiple of a page.
*/
#define INIT_MAP_BEYOND_END (128*1024)
注释说这是一个用来做位图的结构,位图中的每一个bit表示一个页面空间,所以以4G地址空间计算,4G/4K/8=128K,所以在上面进行页表映射的时候,还要同时映射多加128KB空间的映射,这个位图的使用在接下来会进一步讨论。
四、从INIT_MAP_BEYOND_END再继续
在内核页表映射完成之后,此时系统可以开启分页机制了,从上面的代码可以看到,内核映射并且只映射了最少的,静态可见的地址映射,而对于更多的物理内存,内核并没有映射,这些物理内存也就是真个系统共享的一个页面池子,是内核手中可用的物理页面资源。用户创建进程、malloc地址空间、使用共享内存都需要从这个池子里分配,《让子弹飞》中黄四爷那句话:几百张嘴等着吃饭呢!由于没有映射,这些空间即使是内核也无法直接使用,就好象是茫茫海水,在没有处理之前,饮用是不行的。
显然,内核需要知道在初始化之后,汇编代码到底映射了多少地址空间,或者说多少地址空间已经被使用了,这些空间内核不再作为流动资金,相反,这个地址之外的物理内存将会由内核的页面管理机制接管。
同样是在开始的页表初始化之后,这个地址保存在了一个全局变量里
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
之后在内核启动代码中,该变量将会有重要的参考价值
static unsigned long __init setup_memory(void)
{
/*
* partially used pages are not usable - thus
* we are rounding upwards:
*/
min_low_pfn = PFN_UP(init_pg_tables_end);这个值转手被赋值给了全局变量min_low_pfn,这个变量和init_pg_tables_end的区别在于前者是页面对齐的,并且是一个通用变量,所以体系结构均使用这个变量,而后者只是386结构定义的临时变量。
1、低端和高端内存定界
从这个代码中可以看到,这里的init_pg_tables_end就组成了min_low_pfn,这里的low和之后将出现的highmem相对应,而对于现在的low地址空间,又分为min和max,其中的max在函数find_max_low_pfn中确定,这个函数编译宏和运行时分支都比较多
unsigned int __VMALLOC_RESERVE = 128 << 20;
#define MAXMEM (-__PAGE_OFFSET-__VMALLOC_RESERVE)
/*
* Reserved space for vmalloc and iomap - defined in asm/page.h
*/
#define MAXMEM_PFN PFN_DOWN(MAXMEM)
#define MAX_NONPAE_PFN (1 << 20)
/*
* Determine low and high memory ranges:
*/
unsigned long __init find_max_low_pfn(void)
{
unsigned long max_low_pfn;
max_low_pfn = max_pfn;
if (max_low_pfn > MAXMEM_PFN) { 这里的MAXMEM_PFN也就是1G-__VMALLOC_RESERVE,后面的默认值为128M,所以低区物理内存不能超过896M,超过该地址空间的内存将被作为高端内存使用。接下来的MAX_NONPAE_PFN值为1M,由于内核在32位下同样只能使用4G地址空间,所以如果物理内存页面值大于该值(4G内存),则需要使用386的PAE功能。
if (highmem_pages == -1)
highmem_pages = max_pfn - MAXMEM_PFN;
if (highmem_pages + MAXMEM_PFN < max_pfn)
max_pfn = MAXMEM_PFN + highmem_pages;
if (highmem_pages + MAXMEM_PFN > max_pfn) {
printk("only %luMB highmem pages available, ignoring highmem size of %uMB.\n", pages_to_mb(max_pfn - MAXMEM_PFN), pages_to_mb(highmem_pages));
highmem_pages = 0;
}
max_low_pfn = MAXMEM_PFN;
#ifndef CONFIG_HIGHMEM
/* Maximum memory usable is what is directly addressable */
printk(KERN_WARNING "Warning only %ldMB will be used.\n",
MAXMEM>>20);
if (max_pfn > MAX_NONPAE_PFN)
printk(KERN_WARNING "Use a PAE enabled kernel.\n");
else
printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n");
max_pfn = MAXMEM_PFN;
#else /* !CONFIG_HIGHMEM */
#ifndef CONFIG_X86_PAE
if (max_pfn > MAX_NONPAE_PFN) {
max_pfn = MAX_NONPAE_PFN;
printk(KERN_WARNING "Warning only 4GB will be used.\n");
printk(KERN_WARNING "Use a PAE enabled kernel.\n");
}
#endif /* !CONFIG_X86_PAE */
#endif /* !CONFIG_HIGHMEM */
} else {
if (highmem_pages == -1)
highmem_pages = 0;
#ifdef CONFIG_HIGHMEM
if (highmem_pages >= max_pfn) {
printk(KERN_ERR "highmem size specified (%uMB) is bigger than pages available (%luMB)!.\n", pages_to_mb(highmem_pages), pages_to_mb(max_pfn));
highmem_pages = 0;
}
if (highmem_pages) {
if (max_low_pfn-highmem_pages < 64*1024*1024/PAGE_SIZE){
printk(KERN_ERR "highmem size %uMB results in smaller than 64MB lowmem, ignoring it.\n", pages_to_mb(highmem_pages));
highmem_pages = 0;
}
max_low_pfn -= highmem_pages;
}
#else
if (highmem_pages)
printk(KERN_ERR "ignoring highmem size on non-highmem kernel!\n");
#endif
}
return max_low_pfn;
}
2、位图的使用
void __init setup_bootmem_allocator(void)
{
unsigned long bootmap_size;
/*
* Initialize the boot-time allocator (with low memory only):
*/
bootmap_size = init_bootmem(min_low_pfn, max_low_pfn);
register_bootmem_low_pages(max_low_pfn);注册所有低端内存。
在setup_bootmem_allocator-->>init_bootmem--->>init_bootmem_core
bdata->node_bootmem_map = phys_to_virt(PFN_PHYS(mapstart)); 这个结构就是之前保留的128KB字节的用途,此处可以直接使用,并在最后全部置位,表示所有空间均被使用,不能分配,注释中也说空闲ram必须被显式注册。
bdata->node_boot_start = PFN_PHYS(start);
bdata->node_low_pfn = end;
link_bootmem(bdata);
/*
* Initially all pages are reserved - setup_arch() has to
* register free RAM areas explicitly.
*/
mapsize = get_mapsize(bdata);
memset(bdata->node_bootmem_map, 0xff, mapsize);
3、在page数值分配时使用
(gdb) bt
#0 alloc_node_mem_map (pgdat=0xc0a0e500) at mm/page_alloc.c:2723
#1 0xc0aaf49a in free_area_init_node (nid=0, pgdat=0xc0a0e500,
zones_size=0x0, node_start_pfn=0, zholes_size=0x0) at mm/page_alloc.c:2739
#2 0xc0aafde5 in free_area_init_nodes (max_zone_pfn=0xc0a7ff60)
at mm/page_alloc.c:2970
#3 0xc0a8f7a2 in zone_sizes_init () at arch/i386/kernel/setup.c:384
#4 0xc0a90092 in setup_arch (cmdline_p=0xc0a7ffe0)
at arch/i386/kernel/setup.c:608
#5 0xc0a8609d in start_kernel () at init/main.c:530
#6 0x00000000 in ?? ()
在这个函数中完成了对于mem_map变量的初始化,函数的功能需要将所有的低端页面建立建立一个对应的struct page结构,所有的这些结构构成一个数组,数组就要求struct page结构是连续的,此时就需要使用了开始时所说的位图信息,在alloc_node_mem_map--->>alloc_bootmem_node--->>__alloc_bootmem_node--->>__alloc_bootmem_core函数中,如果知道数据结构和应用场景,这个代码应该不安理解,所以不再列出源代码了。
这样做的好处就是给出一个内核page指针,就可以得到它的物理页面编号,反过来,知道了一个物理页面编号,就可以获得它对应的page结构位置。
4、位图结构的释放
mem_init--->>free_all_bootmem--->>free_all_bootmem_core
/*
* Now free the allocator bitmap itself, it's not
* needed anymore:
*/
page = virt_to_page(bdata->node_bootmem_map);
count = 0;
idx = (get_mapsize(bdata) + PAGE_SIZE-1) >> PAGE_SHIFT;
for (i = 0; i < idx; i++, page++) {
__free_pages_bootmem(page, 0);
count++;
}
total += count;
bdata->node_bootmem_map = NULL;
5、完整内核映射
setup_arch--->>paging_init--->>pagetable_init--->>kernel_physical_mapping_init
/*
* This maps the physical memory to kernel virtual address space, a total
* of max_low_pfn pages, by creating page tables starting from address
* PAGE_OFFSET.
*/
static void __init kernel_physical_mapping_init(pgd_t *pgd_base)
……
{
pte = one_page_table_init(pmd);
for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++) {
if (is_kernel_text(address))
set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC)); 对于可执行代码段,内核只是添加了可执行属性,而没有加写保护。
else
set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));
}
}
可以看到,所有的低端内存都被直接映射到内核空间,而不管内核当前是否使用到该地址,在该步骤之后,所有可用的低端物理内存都在内核的逻辑访问地址空间中。
6、内核zone的初始化
(gdb) bt
#0 memmap_init_zone (size=192, nid=128, zone=0, start_pfn=0,
context=MEMMAP_EARLY) at mm/page_alloc.c:1952
#1 0xc0aae7f8 in init_currently_empty_zone (zone=0xc0a0e500,
zone_start_pfn=0, size=4096, context=MEMMAP_EARLY) at mm/page_alloc.c:2253
#2 0xc0aaf2f6 in free_area_init_core (pgdat=0xc0a0e500, zones_size=0x0,
zholes_size=0x0) at mm/page_alloc.c:2683
#3 0xc0aaf4aa in free_area_init_node (nid=0, pgdat=0xc0a0e500,
zones_size=0x0, node_start_pfn=0, zholes_size=0x0) at mm/page_alloc.c:2741
#4 0xc0aafde5 in free_area_init_nodes (max_zone_pfn=0xc0a7ff60)
at mm/page_alloc.c:2970
#5 0xc0a8f7a2 in zone_sizes_init () at arch/i386/kernel/setup.c:384
#6 0xc0a90092 in setup_arch (cmdline_p=0xc0a7ffe0)
at arch/i386/kernel/setup.c:608
#7 0xc0a8609d in start_kernel () at init/main.c:530
#8 0x00000000 in ?? ()
(gdb)
在zone_size_init中使用了在find_max_low_pfn中初始化的低端和高端内存的定界
void __init zone_sizes_init(void)
{
unsigned long max_zone_pfns[MAX_NR_ZONES];
memset(max_zone_pfns, 0, sizeof(max_zone_pfns));
max_zone_pfns[ZONE_DMA] =
virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT;
max_zone_pfns[ZONE_NORMAL] = max_low_pfn;
#ifdef CONFIG_HIGHMEM
max_zone_pfns[ZONE_HIGHMEM] = highend_pfn;
add_active_range(0, 0, highend_pfn);
#else
add_active_range(0, 0, max_low_pfn);
#endif
free_area_init_nodes(max_zone_pfns);
}
而在最底层的memmap_init_zone中,通过set_page_links(page, zone, nid, pfn);设置了该页面的所在zone属性。这里要注意的是:对于高端内存在内核的映射初始化的时候并没有完成页面映射,但是高端内存对应的页表结构是已经分配并且被初始化过了的。高端内存和低端内存以及DMA内存都是在同一个pglist_data结构中,在free_area_init_node--->>alloc_node_mem_map函数中分配的,其中处理的页面包含了所有的ZONE页面,简单代码说明如下
void __meminit free_area_init_node(int nid, struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long node_start_pfn,
unsigned long *zholes_size)
{
pgdat->node_id = nid;
pgdat->node_start_pfn = node_start_pfn; 分区的开始页面编号,
calculate_node_totalpages(pgdat, zones_size, zholes_size);
alloc_node_mem_map(pgdat);
free_area_init_core(pgdat, zones_size, zholes_size);
}
在calculate_node_totalpages(函数中将会遍历所有的管理zone结构,累加每个zone的大小到node_spanned_pages变量中,在接下来的alloc_node_mem_map函数中为所有node_spanned_pages个页面分配对应的page结构,所以这些page即使本身的地址没有在内核空间中,但是该地址对应的页面管理结构是始终存在的。
7、高端内存中其它4G以下逻辑地址的分配
linux-2.6.21\include\asm-i386\highmem.h
/*
* Ordering is:
*
* FIXADDR_TOP 自顶向下逻辑地址布局信息。
* fixed_addresses
* FIXADDR_START
* temp fixed addresses
* FIXADDR_BOOT_START
* Persistent kmap area
* PKMAP_BASE
* VMALLOC_END
* Vmalloc area
* VMALLOC_START
* high_memory 高端内存的起始地址。
*/
#define PKMAP_BASE ( (FIXADDR_BOOT_START - PAGE_SIZE*(LAST_PKMAP + 1)) & PMD_MASK )
#define LAST_PKMAP_MASK (LAST_PKMAP-1)
#define PKMAP_NR(virt) ((virt-PKMAP_BASE) >> PAGE_SHIFT)
#define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
其它相关常量
/* Just any arbitrary offset to the start of the vmalloc VM area: the
* current 8MB value just means that there will be a 8MB "hole" after the
* physical memory until the kernel virtual memory starts. That means that
* any out-of-bounds memory accesses will hopefully be caught.
* The vmalloc() routines leaves a hole of 4kB between each vmalloced
* area for the same reason. ;)
*/
#define VMALLOC_OFFSET (8*1024*1024)
#define VMALLOC_START (((unsigned long) high_memory + vmalloc_earlyreserve + \
2*VMALLOC_OFFSET-1) & ~(VMALLOC_OFFSET-1))
#ifdef CONFIG_HIGHMEM
# define VMALLOC_END (PKMAP_BASE-2*PAGE_SIZE)
#else
# define VMALLOC_END (FIXADDR_START-2*PAGE_SIZE)
#endif
其中high_memory在setup_memory中初始化为高端内存的起始地址
#ifdef CONFIG_HIGHMEM
highstart_pfn = highend_pfn = max_pfn;
if (max_pfn > max_low_pfn) {
highstart_pfn = max_low_pfn;
}
printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",
pages_to_mb(highend_pfn - highstart_pfn));
num_physpages = highend_pfn;
high_memory = (void *) __va(highstart_pfn * PAGE_SIZE - 1) + 1;
#else
num_physpages = max_low_pfn;
high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1) + 1;
#endif
或者说它就是表示了内核建立对等映射的物理页面数量,在这个地址之上的物理内存,不再能够通过物理地址+3G直接获得分页模式下的逻辑地址。
在386内核的kprobe实现过程中,其中有一个是对于代码段断点的安装,那个地方对于代码段的修改是轻松加惬意,就好象生活在新闻联播里一样,这让我们这些看惯了用户态进程各种保护的程序员来说还是比较震撼的,套用一句三俗的话来说:我和我的小伙伴们都惊呆了。
386中对于kprobe调试断点的位于下面的位置
void __kprobes arch_arm_kprobe(struct kprobe *p)
{
*p->addr = BREAKPOINT_INSTRUCTION;
flush_icache_range((unsigned long) p->addr,
(unsigned long) p->addr + sizeof(kprobe_opcode_t));
}
要注意的是,这个p->addr是一个内核代码段地址,按照我们通常的理解,这个代码段是不应该就这样被随随便便的写入数据并修改。那是不是有其它的地方在执行这个函数之前把可执行代码段的写保护给解除了呢?这个函数的调用在__register_kprobe函数中,在这个函数的上下文中,并没有找到关相关的解除写保护的操作,比较可以的函数调用有下面
if ((ret = arch_prepare_kprobe(p)) != 0)
goto out;
……
arch_arm_kprobe(p);
如果可能的话,就是有些处理器会在prepare中解除写保护,但是在386的实现中,同样没有对于写保护的解除,所以我们可以认为,内核对于可执行页面的确是没有写保护的,为了说明这个问题,把386的prepare函数在这里拷贝一份
int __kprobes arch_prepare_kprobe(struct kprobe *p)
{
/* insn: must be on special executable page on i386. */
p->ainsn.insn = get_insn_slot();
if (!p->ainsn.insn)
return -ENOMEM;
memcpy(p->ainsn.insn, p->addr, MAX_INSN_SIZE * sizeof(kprobe_opcode_t));
p->opcode = *p->addr;
if (can_boost(p->addr)) {
p->ainsn.boostable = 0;
} else {
p->ainsn.boostable = -1;
}
return 0;
}
二、内核的代码段如何初始化
这部分最为原始的代码使用汇编编写,代码位于linux-2.6.21\arch\i386\kernel\head.S文件中,我们把相关的代码列出来
/*
* Initialize page tables. This creates a PDE and a set of page
* tables, which are located immediately beyond _end. The variable
* init_pg_tables_end is set up to point to the first "safe" location.
* Mappings are created both at virtual address 0 (identity mapping)
* and PAGE_OFFSET for up to _end+sizeof(page tables)+INIT_MAP_BEYOND_END.
*
* Warning: don't use %esi or the stack in this code. However, %esp
* can be used as a GPR if you really need it...
*/
page_pde_offset = (__PAGE_OFFSET >> 20); 这个__PAGE_OFFSET就是大家经常听说的3G地址空间中的3G,这个宏获得内核其实地址的页目录
movl $(pg0 - __PAGE_OFFSET), %edi edi指针存放内核页表项的位置(物理地址,不是数值),其中pg0表示内核第一个页表项的位置,接下来汇编指令stosl要求目的地址存放在edi中。
movl $(swapper_pg_dir - __PAGE_OFFSET), %edx edx存放内核的页目录的物理地址,其中swapper_pg_dir为内核页目录的起始地址。
movl $0x007, %eax /* 0x007 = PRESENT+RW+USER */ 这里引出了开始的问题,内核页面的访问权限,其中的007的意义表示了之后说的权限,页面在内存中,可读可写,用户页面。由于在初始化过程中所有页面都是这种属性,所以代码段写入毫无压力。
10:
leal 0x007(%edi),%ecx /* Create PDE entry */ 取到edi的地址,也就是页表项的地址,放入ecx寄存器,
movl %ecx,(%edx) /* Store identity PDE entry */ 该值存入物理地址,其中ecx保存一个页表项的开始,这个页表项所在页面的1K个连续页表项将会对应页目录中的一条记录,而这个ecx就是这个页表项的起始地址,这个值首先存入物理地址的页目录中
movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */ 同样一份存入逻辑地址
addl $4,%edx 处理下一个页目录项
movl $1024, %ecx 遍历页目录对应的1K个页表项
11:
stosl
addl $0x1000,%eax 每个页表项中存储的地址长度增加一个页面,其中eax中值为页目录中的值(edi中为页目录地址)
loop 11b
/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp 这个地方的结束条件比较曲折,而且可能会影响到之后启动内存管理代码。除去这个INIT_MAP_BEYOND_END不谈,这里的%edi中存储的是当前当前页表项的位置,这个位置随着映射的增加而逐渐增加,因为这个页表项是迄今为止整个内核地址的最高端,这一点在接下来还会说明原因,这个页表项顶端放入ebp寄存器中。
cmpl %ebp,%eax eax存放的是当前所以被覆盖到的内核地址的最高端,并且包括了页表项的空间,因为页表项本身也是需要被映射入地址转换机制中。这里结束的条件就是所有页表项都被覆盖。这其实本质上是一个追击问题:页表项每增加一个(4字节),内核地址空间就增加4K,所以后者肯定可以追击到前者。
jb 10b
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
xorl %ebx,%ebx /* This is the boot CPU (BSP) */
jmp 3f
三、使用到变量的定义位置
1、pg0的定义位置
这个变量是一个链接脚本变量,定义在linux-2.6.21\arch\i386\kernel\vmlinux.lds.S文件中
.bss : AT(ADDR(.bss) - LOAD_OFFSET) {
__init_end = .;
__bss_start = .; /* BSS */
*(.bss.page_aligned)
*(.bss)
. = ALIGN(4);
__bss_stop = .;
_end = . ;
/* This is where the kernel creates the early boot page tables */
. = ALIGN(4096);
pg0 = . ;
}
/* Sections to be discarded */
/DISCARD/ : {
*(.exitcall.exit)
}
STABS_DEBUG
DWARF_DEBUG
NOTES
可以看到,它定义在整个为初始化数据段的最后,所以页表的位置可以动态增加而不会覆盖任何用户数据。
2、swapper_pg_dir的定义
linux-2.6.21\arch\i386\kernel\head.S
/*
* BSS section
*/
.section ".bss.page_aligned","w"
ENTRY(swapper_pg_dir)
.fill 1024,4,0
ENTRY(empty_zero_page)
.fill 4096,1,0
/*
* This starts the data section.
*/
.data
ENTRY(start_pda)
.long boot_pda
页表定义在bss段的开始,使用了一个页面,共1K个页目录,对应1K×1K×4K=4G,完整表示了32地址空间的所有地址,所以内核的整个页目录其实是只需要一个页面存储的。
顺便可以看到我们经常可以看到的empty_zero_page,通常一些用户态只读的保护页面就是被映射到这个地址,而设备文件/dev/zero也通常被映射到这里。
3、INIT_MAP_BEYOND_END宏
这个宏在之前的内核页面映射
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
中使用
/*
* This is how much memory *in addition to the memory covered up to
* and including _end* we need mapped initially. We need one bit for
* each possible page, but only in low memory, which means
* 2^32/4096/8 = 128K worst case (4G/4G split.)
*
* Modulo rounding, each megabyte assigned here requires a kilobyte of
* memory, which is currently unreclaimed.
*
* This should be a multiple of a page.
*/
#define INIT_MAP_BEYOND_END (128*1024)
注释说这是一个用来做位图的结构,位图中的每一个bit表示一个页面空间,所以以4G地址空间计算,4G/4K/8=128K,所以在上面进行页表映射的时候,还要同时映射多加128KB空间的映射,这个位图的使用在接下来会进一步讨论。
四、从INIT_MAP_BEYOND_END再继续
在内核页表映射完成之后,此时系统可以开启分页机制了,从上面的代码可以看到,内核映射并且只映射了最少的,静态可见的地址映射,而对于更多的物理内存,内核并没有映射,这些物理内存也就是真个系统共享的一个页面池子,是内核手中可用的物理页面资源。用户创建进程、malloc地址空间、使用共享内存都需要从这个池子里分配,《让子弹飞》中黄四爷那句话:几百张嘴等着吃饭呢!由于没有映射,这些空间即使是内核也无法直接使用,就好象是茫茫海水,在没有处理之前,饮用是不行的。
显然,内核需要知道在初始化之后,汇编代码到底映射了多少地址空间,或者说多少地址空间已经被使用了,这些空间内核不再作为流动资金,相反,这个地址之外的物理内存将会由内核的页面管理机制接管。
同样是在开始的页表初始化之后,这个地址保存在了一个全局变量里
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
之后在内核启动代码中,该变量将会有重要的参考价值
static unsigned long __init setup_memory(void)
{
/*
* partially used pages are not usable - thus
* we are rounding upwards:
*/
min_low_pfn = PFN_UP(init_pg_tables_end);这个值转手被赋值给了全局变量min_low_pfn,这个变量和init_pg_tables_end的区别在于前者是页面对齐的,并且是一个通用变量,所以体系结构均使用这个变量,而后者只是386结构定义的临时变量。
1、低端和高端内存定界
从这个代码中可以看到,这里的init_pg_tables_end就组成了min_low_pfn,这里的low和之后将出现的highmem相对应,而对于现在的low地址空间,又分为min和max,其中的max在函数find_max_low_pfn中确定,这个函数编译宏和运行时分支都比较多
unsigned int __VMALLOC_RESERVE = 128 << 20;
#define MAXMEM (-__PAGE_OFFSET-__VMALLOC_RESERVE)
/*
* Reserved space for vmalloc and iomap - defined in asm/page.h
*/
#define MAXMEM_PFN PFN_DOWN(MAXMEM)
#define MAX_NONPAE_PFN (1 << 20)
/*
* Determine low and high memory ranges:
*/
unsigned long __init find_max_low_pfn(void)
{
unsigned long max_low_pfn;
max_low_pfn = max_pfn;
if (max_low_pfn > MAXMEM_PFN) { 这里的MAXMEM_PFN也就是1G-__VMALLOC_RESERVE,后面的默认值为128M,所以低区物理内存不能超过896M,超过该地址空间的内存将被作为高端内存使用。接下来的MAX_NONPAE_PFN值为1M,由于内核在32位下同样只能使用4G地址空间,所以如果物理内存页面值大于该值(4G内存),则需要使用386的PAE功能。
if (highmem_pages == -1)
highmem_pages = max_pfn - MAXMEM_PFN;
if (highmem_pages + MAXMEM_PFN < max_pfn)
max_pfn = MAXMEM_PFN + highmem_pages;
if (highmem_pages + MAXMEM_PFN > max_pfn) {
printk("only %luMB highmem pages available, ignoring highmem size of %uMB.\n", pages_to_mb(max_pfn - MAXMEM_PFN), pages_to_mb(highmem_pages));
highmem_pages = 0;
}
max_low_pfn = MAXMEM_PFN;
#ifndef CONFIG_HIGHMEM
/* Maximum memory usable is what is directly addressable */
printk(KERN_WARNING "Warning only %ldMB will be used.\n",
MAXMEM>>20);
if (max_pfn > MAX_NONPAE_PFN)
printk(KERN_WARNING "Use a PAE enabled kernel.\n");
else
printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n");
max_pfn = MAXMEM_PFN;
#else /* !CONFIG_HIGHMEM */
#ifndef CONFIG_X86_PAE
if (max_pfn > MAX_NONPAE_PFN) {
max_pfn = MAX_NONPAE_PFN;
printk(KERN_WARNING "Warning only 4GB will be used.\n");
printk(KERN_WARNING "Use a PAE enabled kernel.\n");
}
#endif /* !CONFIG_X86_PAE */
#endif /* !CONFIG_HIGHMEM */
} else {
if (highmem_pages == -1)
highmem_pages = 0;
#ifdef CONFIG_HIGHMEM
if (highmem_pages >= max_pfn) {
printk(KERN_ERR "highmem size specified (%uMB) is bigger than pages available (%luMB)!.\n", pages_to_mb(highmem_pages), pages_to_mb(max_pfn));
highmem_pages = 0;
}
if (highmem_pages) {
if (max_low_pfn-highmem_pages < 64*1024*1024/PAGE_SIZE){
printk(KERN_ERR "highmem size %uMB results in smaller than 64MB lowmem, ignoring it.\n", pages_to_mb(highmem_pages));
highmem_pages = 0;
}
max_low_pfn -= highmem_pages;
}
#else
if (highmem_pages)
printk(KERN_ERR "ignoring highmem size on non-highmem kernel!\n");
#endif
}
return max_low_pfn;
}
2、位图的使用
void __init setup_bootmem_allocator(void)
{
unsigned long bootmap_size;
/*
* Initialize the boot-time allocator (with low memory only):
*/
bootmap_size = init_bootmem(min_low_pfn, max_low_pfn);
register_bootmem_low_pages(max_low_pfn);注册所有低端内存。
在setup_bootmem_allocator-->>init_bootmem--->>init_bootmem_core
bdata->node_bootmem_map = phys_to_virt(PFN_PHYS(mapstart)); 这个结构就是之前保留的128KB字节的用途,此处可以直接使用,并在最后全部置位,表示所有空间均被使用,不能分配,注释中也说空闲ram必须被显式注册。
bdata->node_boot_start = PFN_PHYS(start);
bdata->node_low_pfn = end;
link_bootmem(bdata);
/*
* Initially all pages are reserved - setup_arch() has to
* register free RAM areas explicitly.
*/
mapsize = get_mapsize(bdata);
memset(bdata->node_bootmem_map, 0xff, mapsize);
3、在page数值分配时使用
(gdb) bt
#0 alloc_node_mem_map (pgdat=0xc0a0e500) at mm/page_alloc.c:2723
#1 0xc0aaf49a in free_area_init_node (nid=0, pgdat=0xc0a0e500,
zones_size=0x0, node_start_pfn=0, zholes_size=0x0) at mm/page_alloc.c:2739
#2 0xc0aafde5 in free_area_init_nodes (max_zone_pfn=0xc0a7ff60)
at mm/page_alloc.c:2970
#3 0xc0a8f7a2 in zone_sizes_init () at arch/i386/kernel/setup.c:384
#4 0xc0a90092 in setup_arch (cmdline_p=0xc0a7ffe0)
at arch/i386/kernel/setup.c:608
#5 0xc0a8609d in start_kernel () at init/main.c:530
#6 0x00000000 in ?? ()
在这个函数中完成了对于mem_map变量的初始化,函数的功能需要将所有的低端页面建立建立一个对应的struct page结构,所有的这些结构构成一个数组,数组就要求struct page结构是连续的,此时就需要使用了开始时所说的位图信息,在alloc_node_mem_map--->>alloc_bootmem_node--->>__alloc_bootmem_node--->>__alloc_bootmem_core函数中,如果知道数据结构和应用场景,这个代码应该不安理解,所以不再列出源代码了。
这样做的好处就是给出一个内核page指针,就可以得到它的物理页面编号,反过来,知道了一个物理页面编号,就可以获得它对应的page结构位置。
4、位图结构的释放
mem_init--->>free_all_bootmem--->>free_all_bootmem_core
/*
* Now free the allocator bitmap itself, it's not
* needed anymore:
*/
page = virt_to_page(bdata->node_bootmem_map);
count = 0;
idx = (get_mapsize(bdata) + PAGE_SIZE-1) >> PAGE_SHIFT;
for (i = 0; i < idx; i++, page++) {
__free_pages_bootmem(page, 0);
count++;
}
total += count;
bdata->node_bootmem_map = NULL;
5、完整内核映射
setup_arch--->>paging_init--->>pagetable_init--->>kernel_physical_mapping_init
/*
* This maps the physical memory to kernel virtual address space, a total
* of max_low_pfn pages, by creating page tables starting from address
* PAGE_OFFSET.
*/
static void __init kernel_physical_mapping_init(pgd_t *pgd_base)
……
{
pte = one_page_table_init(pmd);
for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++) {
if (is_kernel_text(address))
set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC)); 对于可执行代码段,内核只是添加了可执行属性,而没有加写保护。
else
set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));
}
}
可以看到,所有的低端内存都被直接映射到内核空间,而不管内核当前是否使用到该地址,在该步骤之后,所有可用的低端物理内存都在内核的逻辑访问地址空间中。
6、内核zone的初始化
(gdb) bt
#0 memmap_init_zone (size=192, nid=128, zone=0, start_pfn=0,
context=MEMMAP_EARLY) at mm/page_alloc.c:1952
#1 0xc0aae7f8 in init_currently_empty_zone (zone=0xc0a0e500,
zone_start_pfn=0, size=4096, context=MEMMAP_EARLY) at mm/page_alloc.c:2253
#2 0xc0aaf2f6 in free_area_init_core (pgdat=0xc0a0e500, zones_size=0x0,
zholes_size=0x0) at mm/page_alloc.c:2683
#3 0xc0aaf4aa in free_area_init_node (nid=0, pgdat=0xc0a0e500,
zones_size=0x0, node_start_pfn=0, zholes_size=0x0) at mm/page_alloc.c:2741
#4 0xc0aafde5 in free_area_init_nodes (max_zone_pfn=0xc0a7ff60)
at mm/page_alloc.c:2970
#5 0xc0a8f7a2 in zone_sizes_init () at arch/i386/kernel/setup.c:384
#6 0xc0a90092 in setup_arch (cmdline_p=0xc0a7ffe0)
at arch/i386/kernel/setup.c:608
#7 0xc0a8609d in start_kernel () at init/main.c:530
#8 0x00000000 in ?? ()
(gdb)
在zone_size_init中使用了在find_max_low_pfn中初始化的低端和高端内存的定界
void __init zone_sizes_init(void)
{
unsigned long max_zone_pfns[MAX_NR_ZONES];
memset(max_zone_pfns, 0, sizeof(max_zone_pfns));
max_zone_pfns[ZONE_DMA] =
virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT;
max_zone_pfns[ZONE_NORMAL] = max_low_pfn;
#ifdef CONFIG_HIGHMEM
max_zone_pfns[ZONE_HIGHMEM] = highend_pfn;
add_active_range(0, 0, highend_pfn);
#else
add_active_range(0, 0, max_low_pfn);
#endif
free_area_init_nodes(max_zone_pfns);
}
而在最底层的memmap_init_zone中,通过set_page_links(page, zone, nid, pfn);设置了该页面的所在zone属性。这里要注意的是:对于高端内存在内核的映射初始化的时候并没有完成页面映射,但是高端内存对应的页表结构是已经分配并且被初始化过了的。高端内存和低端内存以及DMA内存都是在同一个pglist_data结构中,在free_area_init_node--->>alloc_node_mem_map函数中分配的,其中处理的页面包含了所有的ZONE页面,简单代码说明如下
void __meminit free_area_init_node(int nid, struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long node_start_pfn,
unsigned long *zholes_size)
{
pgdat->node_id = nid;
pgdat->node_start_pfn = node_start_pfn; 分区的开始页面编号,
calculate_node_totalpages(pgdat, zones_size, zholes_size);
alloc_node_mem_map(pgdat);
free_area_init_core(pgdat, zones_size, zholes_size);
}
在calculate_node_totalpages(函数中将会遍历所有的管理zone结构,累加每个zone的大小到node_spanned_pages变量中,在接下来的alloc_node_mem_map函数中为所有node_spanned_pages个页面分配对应的page结构,所以这些page即使本身的地址没有在内核空间中,但是该地址对应的页面管理结构是始终存在的。
7、高端内存中其它4G以下逻辑地址的分配
linux-2.6.21\include\asm-i386\highmem.h
/*
* Ordering is:
*
* FIXADDR_TOP 自顶向下逻辑地址布局信息。
* fixed_addresses
* FIXADDR_START
* temp fixed addresses
* FIXADDR_BOOT_START
* Persistent kmap area
* PKMAP_BASE
* VMALLOC_END
* Vmalloc area
* VMALLOC_START
* high_memory 高端内存的起始地址。
*/
#define PKMAP_BASE ( (FIXADDR_BOOT_START - PAGE_SIZE*(LAST_PKMAP + 1)) & PMD_MASK )
#define LAST_PKMAP_MASK (LAST_PKMAP-1)
#define PKMAP_NR(virt) ((virt-PKMAP_BASE) >> PAGE_SHIFT)
#define PKMAP_ADDR(nr) (PKMAP_BASE + ((nr) << PAGE_SHIFT))
其它相关常量
/* Just any arbitrary offset to the start of the vmalloc VM area: the
* current 8MB value just means that there will be a 8MB "hole" after the
* physical memory until the kernel virtual memory starts. That means that
* any out-of-bounds memory accesses will hopefully be caught.
* The vmalloc() routines leaves a hole of 4kB between each vmalloced
* area for the same reason. ;)
*/
#define VMALLOC_OFFSET (8*1024*1024)
#define VMALLOC_START (((unsigned long) high_memory + vmalloc_earlyreserve + \
2*VMALLOC_OFFSET-1) & ~(VMALLOC_OFFSET-1))
#ifdef CONFIG_HIGHMEM
# define VMALLOC_END (PKMAP_BASE-2*PAGE_SIZE)
#else
# define VMALLOC_END (FIXADDR_START-2*PAGE_SIZE)
#endif
其中high_memory在setup_memory中初始化为高端内存的起始地址
#ifdef CONFIG_HIGHMEM
highstart_pfn = highend_pfn = max_pfn;
if (max_pfn > max_low_pfn) {
highstart_pfn = max_low_pfn;
}
printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",
pages_to_mb(highend_pfn - highstart_pfn));
num_physpages = highend_pfn;
high_memory = (void *) __va(highstart_pfn * PAGE_SIZE - 1) + 1;
#else
num_physpages = max_low_pfn;
high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1) + 1;
#endif
或者说它就是表示了内核建立对等映射的物理页面数量,在这个地址之上的物理内存,不再能够通过物理地址+3G直接获得分页模式下的逻辑地址。