0
Linux 内存管理系统:初始化
作者:Joe Knapka
臭翻:colyli
内存管理系统的初始化处理流程分为三个基本阶段:
激活页内存管理
 在swapper_pg_dir中初始化内核的页表
 初始化一系列和内存管理相关的内核数据
 Turning On Paging (i386)
启动分页机制(i386)
Kernel 代码被加载到物理地址0x100000(1MB),在分页机制打开后被重新映射到
PAGE_OFFSET + 0x100000的位置(PAGE_OFFSET在IA32上为3GB,即进程虚拟地址中用户
空间与内核空间的分界处)。这是通过将物理地址映射到编译进来的页表 (在
arch/i386/kernel/head.S中)的0-8MB以及PAGE_OFFSET-PAGE_OFFSET+8MB实现的。然后
我们跳转到init/main.c中的start_kernel,这个函数被定位到PAGE_OFFSET+某一个地址。
这看起来有些狡猾。要注意到在head.S中启动分页机制的代码是通过让它自己所执行的地
址空间不再有效的方式来实现这一点的;因此0-4MB被映射(不明白:hence the 0-4MB
identity mapping.)。在分页机制没有启动之前,start_kernel是不会被调用的,我们假
定他运行在PAGE_OFFSET+某一个地方的位置。因此head.S中的页表必须同样映射内核代码
所使用的地址,这样后继才能跳转到staert_kernel处;因此PAGE_OFFSET被映射(不明
白:hence the PAGE_OFFSET mapping.)。
下面在head.S中分页机制启动时的一些神奇的代码:
/*
* Enable paging
*/
3:
movl $swapper_pg_dir-__PAGE_OFFSET,%eax
movl %eax,%cr3 /* set the page table pointer.. */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* ..and set paging (PG) bit */
jmp 1f /* flush the prefetch-queue */
1:
movl $1f,%eax
jmp *%eax /* make sure eip is relocated */
1:
在两个1的label之间的代码将第二个label 1的地址加载到EAX中,然后跳转到那里。这
时,指令指针寄存器EIP指向1MB+某个数值的物理地址。而label都在内核的虚拟地址空
间(PAGE_OFFSET+某个位置),所以这段代码将EIP有效的从物理地址空间重新定位到了虚
拟地址空间。
Start_kernel函数初始化了所有的内核数据,然后启动了init内核线程。Start_kernel中
最初的几件事情之一就是调用setup_arch函数,这是一个和具体的体系结构相关的设置函
数, 调用了更底层的初始化细节。对于x86 平台而言, 这些函数在
arch/i386/kernel/setup.c中。
在setup_arch 中和内存相关的第一件事就是计算低端内存(low-memory) 和高端内存
(high-memory)的有效页的数目;每种内存类型(each memory type)最高端的页的数目分别
保存在全局变量highstart_pfn和highend_pfn中。高端内存并不是直接映射到内核的虚拟
内存(VM)中;这是后面要讨论的。
接下来,setup_arch 调用init_bootmem 函数以初始化启动时的内存分配器(boot-time
memory allocator)。Bootmem内存分配器仅仅在系统boot的过程中使用,为永久的内核数
据分配页。因此我们不会对它涉及太多。需要记住的就是bootmem 分配器(bootmem
allocator)在内核初始化时提供页,这些页为内核专门预留,就好像他们是从内核景象文
件中载入的一样,他们在系统启动以后不参与任何的内存管理活动。
初始化内核页表
之后,setup_arch调用在arch/i386/mm/init.c中的paging_init函数。这个函数做了一些
事情。首先它调用pagetable_init函数去映射整个的物理内存,或者在PAGE_OFFSET到4GB
之间的尽可能多的物理内存,这是从PAGE_OFFSET处开始。
在pagetable_init函数中,我们在swapper_pg_dir中精确的建立了内核的页表,映射到
截至PAGE_OFFSET的整个物理内存。
这是一个将正确的数值填充到页目录和页表中去的简单的算术活。映射建立在
swapper_pg_dir中,即kernel页目录;这也是初始化页机制时所使用的页目录。(当使用
4MB的页表的时候,直到下一个4MB边界的虚拟地址才会被映射在这里,但这没有什么,
因为我们不会使用这个内存所以没有什么问题)。如果有这里有剩下物理内存没有被映射,
那就是大于4GB-PAGE_OFFSET范围的内存,这些内存只有CONFIG_HIGHMEM选项被设置后
才能使用(即使用大于4GB的内存)。
在接近pagetable_init函数的尾部,我们调用了fixrange_init为编译时固定的虚拟内存
映射预留页表。这些表将硬编码到Kernel中的虚拟地址进行映射,但是他们并不是已经加
载的内核数据的一部分。Fixmap表在运行时调用set_fixmap函数被映射到物理内存。
在初始化了fixmap之后,如果CONFIG_HIGHMEM被设置了,我们还要分配一些页表给kmap
分配器。Kmap允许kernel将物理地址的任何页映射到kernel的虚拟地址空间,以临时使用。
这很有用,例如对在pagetable_init中不能直接映射的物理内存进行映射。
Fixmap 和kmap 页表们占据了kernel 虚拟空间顶部的一部分——因此这些地址不能在
PAGE_OFFSET映射中被永久的映射到物理页上。由于这个原因,Kernel虚拟内存的顶部的
128MB就被预留了(vmalloc分配器仍然是用这个范围内的地址)。(下面这句实在是不知
道怎么翻译) Any physical pages that would otherwise be mapped into the
PAGE_OFFSET mapping in the 4GB-128MB range are instead (if CONFIG_HIGHMEM is
specified) included in the high memory zone, accessible to the kernel only via
kmap()。如果没有设置CONFIG_HIGMEM,这些页就完全是不可用的。这仅针对配置有大量内
存的机器(900多MB或者更多)。例如,如果PAGE_OFFSET=3GB,并且机器有2GB的RAM,
那么只有开始的1GB-128MB的物理内存可以被映射到PAGE_OFFSET和fixmap/kmap地址范
围之间。剩余的页是不可用的——实际上对于用户进程映射来说,他们是可以直接映射的页
——但是内核不能够直接访问它们。
回到paging_init,我们可以通过调用kmap_init函数来初始化kmap系统,kmap_init简
单的缓存了最先的kmap_pagetable[在TLB?]。然后,我们通过计算zone的大小并调用
free_area_init 去建立mem_map 和初始化freelist,初始化了zone 分配器。所有的
freelist被初始化为空,并且所有的页都被标志为reserved(不可被VM系统访问);这
种情况之后会被纠正。
当paging_init完成后,物理内存的分布如下[注意在2.4的内核中这不全对]:
0x00000000: 0-page
0x00100000: kernel-text
0x????????: kernel_data
0x???????? =_end: whole-mem pagetables
0x????????: fixmap pagetables
0x????????: zone data (mem_map, zone_structs, freelists &c)
0x???????? =start_mem: free pages
这块内存被swapper_pg_dir和whole-mem-pagetables映射以寻址PAGE_OFFSET。
进一步的VM子系统初始化
现在我们回到start_kernel。在paging_init完成后,我们为内核的其他子系统进行一些
额外的配置工作,它们中的一些使用bootmem分配器分配额外的内核内存。从内存管理的观
点来看,这其中最重要的是kmem_cache_init,他初始化了slab分配器的数据。
在kmem_cache_init 调用之后不久,我们调用了mem_init。这个通过清除空闲物理页的
zone数据中的PG_RESERVED位在free_area_init的开始完成了初始化freelist的工作;
为不能被用为DMA的页清除PG_DMA位;然后释放所有可用的页到他们各自的zone中。最后
一步,在bootmem.c 中的free_all_bootmem函数中完成,很有趣。他建立了伙伴位图和
freelist描述了所有存在的没有预留的页,这是通过简单的释放他们并让free_page_ok
做正确的事情。一旦mem_init被调用了,bootmem分配器就不再使用了,所以它的所有的
页也会被释放到zone分配器的世界中。

段用来将线性地址空间划分为专用的块。线性空间是被VM子系统管理的。X86体系结构从硬
件上支持段机制;你可以按照段+段内偏移量的方式指定一个地址,这里地址被描述为一定
范围的线性(虚拟地址)并带有特定的属性(如保护属性)。实际上,在x86体系结构中你
必须使用段机制。所以我们要设置4个段:
一个kernel text段:从0 到4GB
一个kernel data段:从0 到4GB
一个user text段:从0 到4GB
一个user data段:从0 到4GB
因此我们可以使用任何一个有效的段选择器(segment selector)访问整个虚拟地址空间。
问题:
段是在哪里被设置的?
答案:
全局描述符表(GDT)定义在head.s的450行。 GDT寄存器在250行被加载。
问题:
为什么将内核段和用户端分离开。是否他们都有权限访问整个4GB的范围?
答案:
这是因为内核和用户段的保护机制有区别:
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x2B user 4GB data at 0x00000000 */
段寄存器(CS,DS等)包含有一个13位的描述符表的索引,索引指向的描述符告诉CPU所选
择的段的属性。段选择器的低3位没有被用来索引描述符表,而是用来保存描述符类型(全
局或局部)以及需要的特权级。因此内核段选择器0x10和0x18使用特权级0(RPL0),用
户选择器0x23和0x2B使用最特权级RPL 3。
要注意到第三个高序字节的高位组对应内核和用户也是不同的:对内核,描述符特权级
(DPL)为0;对用户DPL为3。如果你阅读了Intel的文档,你将看到确切的含义,但是由
于Linux内核的x86段保护没有涉及太多,所以我就不再讨论太多了。



下面的命令是我自己增加的命令,不使用uImage,直接引导zImage文件。

具体方法是使用tftp命令从网络下载zImage文件到内存中或者直接读取flash数据,拷贝到内存中,假设拷贝到了A地址处,接下来就可以调用:
 # bootzimage A
来引导linux内核了,具体分析见下面说明。

 Uboot在设置启动命令的时候使用的是Tag方式,也就是内核现在期望使用的参数传递方式。还有一种引导设置方式,就是采用2.2以及以前版本使用的参数设置方式,2.4和2.6内核为了兼容之前版本参数设置,对老版本参数数据进行了解析,转换成了内部tag方式。这样我们完全可以使用老版本的参数传递方式。
 Setup.h文件中定义了老版本参数传递结构:
#define COMMAND_LINE_SIZE 1024  // 命令行最多1024字节

/* This is the old deprecated way to pass parameters to the kernel */
struct param_struct {
    union {
 struct {
     unsigned long page_size;  // 内存的页面大小
     unsigned long nr_pages;   // 内存页面数量
     unsigned long ramdisk_size;  // RAM disk配置, 可以不用
     unsigned long flags;  /* 12 */
#define FLAG_READONLY 1
#define FLAG_RDLOAD 4
#define FLAG_RDPROMPT 8
     unsigned long rootdev;  /* 16 */
     unsigned long video_num_cols; /* 20 */
     unsigned long video_num_rows; /* 24 */
     unsigned long video_x;  /* 28 */
     unsigned long video_y;  /* 32 */
     unsigned long memc_control_reg; /* 36 */
     unsigned char sounddefault;  /* 40 */
     unsigned char adfsdrives;  /* 41 */
     unsigned char bytes_per_char_h; /* 42 */
     unsigned char bytes_per_char_v; /* 43 */
     unsigned long pages_in_bank[4]; /* 44 */
     unsigned long pages_in_vram; /* 60 */
     unsigned long initrd_start;  /* 64 */
     unsigned long initrd_size;  /* 68 */
     unsigned long rd_start;  /* 72 */
     unsigned long system_rev;  /* 76 */
     unsigned long system_serial_low; /* 80 */
     unsigned long system_serial_high; /* 84 */
     unsigned long mem_fclk_21285;       /* 88 */
 } s;
 char unused[256];
    } u1;
    union {
 char paths[8][128];
 struct {
     unsigned long magic;
     char n[1024 - sizeof(unsigned long)];
 } s;
    } u2;
    char commandline[COMMAND_LINE_SIZE];  // 命令行启动参数
};
在整个结构中,最关键的部分是u1.s.page_size,u1.s.nr_pages和commandline域。这三个域也是必须设置的域!
代码如下:
#define LINUX_MACHINE_ID 406  // 见后面说明
#define LINUX_PAGE_SHIFT 12  // 页面大小4K
#define LINUX_PAGE_SIZE  (1<<LINUX_PAGE_SHIFT)

int do_bootzimage(cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
 int i;
 u32 addr;
 char *cmdline = getenv ("bootargs"); // 获得命令行参数
 void (*run)(int zero, int arch);   // 定义引导内核的函数原型
 
// 设置引导参数所处的位置
 struct param_struct *params = (struct param_struct *)0xa0000100;
 
 // 设置内核加载地址
 if (argc<2)
  addr = load_addr;  // 默认的加载地址
 else
// 调用命令时参数传递了地址, 那么就使用指定的地址
  addr = simple_strtoul(argv[1], NULL, 16); 

  // 接下来将结构的数据清零, 不用的全部清零, 防止出错
 for(i=0; i<(sizeof(struct param_struct)>>2); i++)
  ((u32 *)params)[i] = 0;
 
 // 设置u1.s.page_size和u1.s.nr_pages参数, 这两个参数由内核自动解析.
 params->u1.s.page_size = LINUX_PAGE_SIZE;
 params->u1.s.nr_pages = (0x04000000 >> LINUX_PAGE_SHIFT);

 // 拷贝命令行参数到命令位置
 memcpy(params->commandline, cmdline, strlen(cmdline));
 
 run = (void (*)(int, int))addr;
// 执行内核程序, 传递两个参数进去, 第2个参数指定了ARCH值, 必须与
// 内核配置的相同, 否则会出现”Error: a”错误!!
run(0, LINUX_MACHINE_ID);
}
// 最后定义bootzImage命令,实现函数是do_bootzimage。
U_BOOT_CMD(
 bootzimage, 2, 1, do_bootzimage,
 "bootzimage - boot zImage from ram.n",
 " [addr] boot zImage directoly. "
);

 这里我们在引导的时候使用了老版本的参数传递方式,下面时Linux内核在进行参数解析时的代码,可以看到,内核将这些参数自动转换成能够识别的类型:
 内核(2.6.9)的init/main.c文件中的start_kernel()函数中调用了setup_arch()函数:
asmlinkage void __init start_kernel(void)
{
 char * command_line;
 extern struct kernel_param __start___param[], __stop___param[];
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
 lock_kernel();
 page_address_init();
 printk(linux_banner);
 setup_arch(&command_line); // 这个函数中将对传递的参数进行解析
 setup_per_cpu_areas();

 setup_arch()函数位于体系结构相关的代码中,本例中存在于arch/arm/kernel/setup.c文件中。
 以下是该函数的部分内容

 struct tag *tags = (struct tag *)&init_tags;

 // 判断是不是tag的ATAG_CORE, 老式的参数是不能满足要求的, 如果是老式的
 // 参数那么将会执行convert_to_tag_list(tags)将老式参数转换成新的参数类型.
 if (tags->hdr.tag != ATAG_CORE)
  convert_to_tag_list(tags);

 convert_to_tag_list()函数实现在arch/arm/kernel/compat.c文件中,该函数随后调用build_tag_list()函数进行参数重组,具体可以参考内核的源代码。



vivi与Linux kernel的参数传递情景分析(下) - Vivi - CalmArrow

文章说明:calmarrow(lqm)原创

文章引自:http://piaoxiang.cublog.cn

 
    下面进入Linux kernel部分,分析与bootloader参数传递对应的部分。
 
    移植Linux需要很大的工作量,其中之一就是HAL层的编写。在具体实现上,HAL层以arch目录的形式存在。显然,该层需要与bootloader有一定的约定,否则就不能很好的支持。其实,这个地方应该思考一个问题,就是说,boot loader可以做到Linux kernel里面,但是这样带来的问题就是可移植性和灵活性都大为降低。而且,bootloader的功能并非操作系统的核心范畴,Linux的核心应该始终关注操作系统的核心功能上,将其性能达到最优。所以,bootloader分离出来单独设计,是有一定的道理的。bootloader现在除了完成基本功能外,慢慢地变得“肥胖”了。在高性能bootloader设计中,可能会把调试内核等的一些功能集成进来,这样在内核移植尚未完成阶段,bootloader可以充当调试器的作用。功能趋于完善,也慢慢趋于复杂。废话不说,进入正题。
 
三、Linux kernel接受参数分析
 
    这部分主要分析如下问题:
 
    ·Linux kernel支持压缩映象和非压缩映象两种方式启动,那么这两种流程和函数入口有何不同?
    ·如何使用非压缩映象?做一下测试。
    ·zImage是如何生成的?其格式如何?
    ·启动之后,Linux kernel如何接收参数?
 
    这里不具体区分每个问题,按照理解和开发的思路来进行。
 
 1、思考:前面做的基本实验中,并没有采用压缩映象。因为程序规模太小,压缩带来的时间开销反而降低了性能。但是对Linux kernel来说,映象还是比较大的,往往采用了压缩。但是,同样有需求希望Linux kernel小一些,不采用压缩方式来提高内核启动的速度,对时间要求比较苛刻。那么,这样就出现了两种情况:压缩映象和非压缩映象。由此带来的问题就在于:如果是压缩映象,那么必须首先解压缩,然后跳转到解压缩之后的代码处执行;如果是非压缩映象,那么直接执行。Linux必须对这两种机制提供支持,这里就需要从整体上来看一下生成的映象类型了。
 
    因为vivi的Makefile都是直接来源于Linux,前面对vivi的Makefile已经分析清楚了,这里看Linux的Makefile就容易多了,大同小异,而且还有丰富的文档支持。
 
(1)非压缩映象
 
$make vmlinux
 

[armlinux@lqm linux-2.4.18]$ ls -l vmlinux
-rwxrwxr-x 1 armlinux armlinux 1799697 Sep 11 14:06 vmlinux
[armlinux@lqm linux-2.4.18]$ file vmlinux
vmlinux: ELF 32-bit LSB executable, ARM, version 1 (ARM), statically linked, not stripped

 
    这里生成的是vmlinux,是ELF文件格式。这个文件是不能烧写存储介质的,如果想了解ELF文件格式,需要参考专门的文章。当然,这里,如果想要使用非压缩映象,可以使用arm-linux-objcopy把上述ELF格式的vmlinux转化为二进制格式的vmlinux.bin,这样就可以直接烧写了。
 
    于是我做了如下的修改,在Makefile中增加了:
 

vmlinux: include/linux/version.h $(CONFIGURATION) init/main.o init/version.o linuxsubdirs
        $(LD) $(LINKFLAGS) $(HEAD) init/main.o init/version.o \
                --start-group \
                $(CORE_FILES) \
                $(DRIVERS) \
                $(NETWORKS) \
                $(LIBS) \
                --end-group \
                -o vmlinux
        $(NM) vmlinux | grep -v '\(compiled\)\|\(\.o$$\)\|\( [aUw] \)\|\(\.\.ng$$\)\|\(LASH[RL]DI\)' | sort > System.map
        $(OBJCOPY) -O binary -R .comment -R .stab -R .stabstr -S vmlinux vmlinux.bin

 
    同时在clean file的列表中增加vmlinux.bin。这样就可以生成vmlinux.bin了,前面的基础实验都讲过了。然后烧写vmlinux.bin到nand flash的kernel分区,引导启动,正常,而且不会出现解压缩提示:
 

NOW, Booting Linux......
VIVI has completed the mission of
From now on, Linux kernel takes charge of all

Linux version 2.4.18-rmk7-pxa1 (armlinux@lqm) (gcc version 2.95.3 20010315 (release)) #2 Tue Sep 11 14:06:14 CST 2007

 
    可见,可以通过非压缩映象格式启动。
 
(2)压缩映象
 
    下面看看压缩映象是如何得到的。顶层的Makefile没有压缩映象的生成,显然就在包含的子Makefile中。容易查知在arch/arm/下的Makefile,可见:
 

bzImage zImage zinstall Image bootpImage install: vmlinux
        @$(MAKEBOOT) $@

 
    也就是说,有bzImage、zImage几种。其中arch/boot下有:
 

SYSTEM =$(TOPDIR)/vmlinux

Image: $(CONFIGURE) $(SYSTEM)
        $(OBJCOPY) -O binary -R .note -R .comment -S $(SYSTEM) $@

bzImage: zImage

zImage: $(CONFIGURE) compressed/vmlinux
        $(OBJCOPY) -O binary -R .note -R .comment -S compressed/vmlinux $@
        @echo " ^_^ The kernel image file is:" $(shell /bin/pwd)/$@

 
    这里发现如果采用make Image,则生成的非压缩映象的二进制格式,可以直接烧写,可见前面第一步的工作是浪费了,Linux内核还是很完善的,提供了这种方式,所以,如果想要生成非压缩二进制映象,那么就要使用make Image。
 
    另外,这里提供了两种压缩的映象,其实就是一种,这里能够看到的就是如果采用make zImage或者make bzImage,就要把compressed/vmlinux处理为二进制格式,可以下载使用。下面就看compressed/vmlinux是什么。进入compressed文件夹,看看Makefile:
 

vmlinux:    $(HEAD) $(OBJS) piggy.o vmlinux.lds
        $(LD) $(ZLDFLAGS) $(HEAD) $(OBJS) piggy.o $(LIBGCC) -o vmlinux

 
    很明显了,这里的vmlinux是由四个部分组成:head.o、head-s3c2410.o、misc.o、piggy.o。关于这几个文件是干什么用的,看看各自的编译规则就非常清晰了:
 
 

$(HEAD): $(HEAD:.o=.S) \
                        $(wildcard $(TOPDIR)/include/config/zboot/rom.h) \
                        $(wildcard $(TOPDIR)/include/config/cpu/32.h) \
                        $(wildcard $(TOPDIR)/include/config/cpu/26.h)
                $(CC) $(AFLAGS) -traditional -c $(HEAD:.o=.S)

piggy.o: $(SYSTEM)
                $(OBJCOPY) -O binary -R .note -R .comment -S $(SYSTEM) piggy
                gzip $(GZFLAGS) < piggy > piggy.gz
                $(LD) -r -o $@ -b binary piggy.gz
                rm -f piggy piggy.gz

font.o: $(FONTC)
                $(CC) $(CFLAGS) -Dstatic= -c -o $@ $(FONTC)

vmlinux.lds: vmlinux.lds.in Makefile $(TOPDIR)/arch/$(ARCH)/boot/Makefile $(TOPDIR)/.config
                @sed "$(SEDFLAGS)" < vmlinux.lds.in > $@

clean:; rm -f vmlinux core piggy* vmlinux.lds

.PHONY: clean

misc.o: misc.c $(TOPDIR)/include/asm/arch/uncompress.h $(TOPDIR)/lib/inflate.c

 
    可见,vmlinux是把顶层生成的非压缩的ELF映象vmlinux进行压缩,同时加入了加压缩代码部分。真正的压缩代码就是lib/inflate.c。可以看看,主要是gunzip,具体的压缩算法就不分析了。
 
    至此,就可以用下图作出总结了:
 
    bootloader把存储介质中的kernel映象下载到mem_base+0x8000的位置,执行完毕后,跳转到这一位置,执行此处的代码。这一位置的入口可能有两种情况,第一种是kernel映象为非压缩格式,通过make Image获得,那么真正的入口就是arch/arm/kernel/head_armv.S(ENTRY(stext));第二种是kernel映象为压缩格式,通过make zImage获得,那么真正的入口就是arch/arm/boot/compressed/head.S(ENTRY(_start))。这个地方并不是kernel判断,也不需要判断。道理很简单,cpu只会按照读入的代码执行,两种情况下执行的代码不同,自然也就有两种不同的过程了。
 
(3)探讨zImage的magic number的位置
 
    可以看出,如果是zImage,那么程序的入口是arch/arm/boot/compressed/head.S。分析程序头部:
 

.align
start:
                .type start,#function

                //重复如下指令8次
                .rept 8
                mov r0, r0
                .endr
                //跳转指令,跳到下面第一个标号1处
                b 1f

                //这就是第10条指令的位置,也就是偏移为4*9个字节
                .word 0x016f2818 @ Magic numbers to help the loader
                .word start @ absolute load/run zImage address
                .word _edata @ zImage end address
1: mov r7, r1 @ save architecture ID
                mov r8, #0 @ save r0

 
    可见前面8条指令均为mov r0, r0,从前面的zImage的16进制格式中可以看出,前面8个字都是相同的,均为00 00 A0 E1,第9条指令就是b 1f,然后就应该是0x016f2818.这样就与前面程序的判断对应上了,也就是说,此处的magic number是固定位置,固定数值的,注释中也写的很清晰,那就是magic numbers to help the loader,也就是说帮助bootloader确定映象的文件格式。但是应该说明的是,在vivi的bootloader设计中,虽然检测zImage的magic number,但是并没有进行未识别处理。也就是说,假定用ultra-edit32把此位置的0x016f2818破坏掉,其他不变,那么虽然vivi提示无法识别zImage映象,但是并不影响实际的执行。当然,你也可以有其他的设计思路。不过设计的哲学思想是,要完成一件事情,并不只有一种方式。所以,bootloader不能限死只是使用zImage格式,需要有一定的灵活性,为了引导内核启动,可以采用不同的方式。
 
(4)完成了前面的理解,下面就要重点看解析参数一部分了。这里不将zImage方式的启动作为重点分析内容,静下心来跟踪代码并不是难事。从整体的角度理解,如果采用zImage,那么在执行完成解压缩之后,自然会调转到解压之后的kernel的第一条指令处。这时就是真正的启动内核了。所以我们可以看arch/arm/kernel/head-armv.S,此处做的工作可以参考taoyuetao的分析,完成的功能比较简单。这里就感兴趣的参数问题分析,需要注意的是,
 

/*
 * Kernel startup entry point.
 *
 * The rules are:
 * r0 - should be 0
 * r1 - unique architecture number
 * MMU - off
 * I-cache - on or off
 * D-cache - off
 *
 * See linux/arch/arm/tools/mach-types for the complete list of numbers
 * for r1.
 */

 
   可见R0是0,R1是mach type,这些都是必须要设定的。在这里,并没有限定R2必须为参数的起始地址。kernel本身并没有使用R0-R2,如果设定了R2,在这里也不会修改其值。后面的工作也没有设计接收参数,最后直接跳到start_kernel(【init/main.c】)
 

asmlinkage void __init start_kernel(void)
{
    char * command_line;
    unsigned long mempages;
    extern char saved_command_line[];
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */

    lock_kernel();
    printk(linux_banner);
    setup_arch(&command_line);
    printk("Kernel command line: %s\n", saved_command_line);
    parse_options(command_line);

 
    从开头分析,首先是lock_kernel,这里是SMP相关,我的是单CPU,所以实际上该函数为空。然后打印版本信息,在vivi中已经分析过这个机制了,两者相同。下面的setup_arch就是分析的重点了,它要获取命令行启动参数,然后打印获得的命令行参数,然后进行语法解析选项。我们关注的重点就在setup_arch上了。参数设置都在【arch/arm/kernel/setup.c】,这个函数也不例外,进入setup.c。
 

void __init setup_arch(char **cmdline_p)
{
    struct tag *tags = NULL;
    struct machine_desc *mdesc;
    char *from = default_command_line;

    ROOT_DEV = MKDEV(0, 255);

    setup_processor();
    mdesc = setup_machine(machine_arch_type);
    machine_name = mdesc->name;

    if (mdesc->soft_reboot)
        reboot_setup("s");

    if (mdesc->param_offset)
        tags = phys_to_virt(mdesc->param_offset);

    
/*
     * Do the machine-specific fixups before we parse the
     * parameters or tags.
     */

    if (mdesc->fixup)
        mdesc->fixup(mdesc, (struct param_struct *)tags,
             &from, &meminfo);

    
/*
     * If we have the old style parameters, convert them to
     * a tag list before.
     */

    if (tags && tags->hdr.tag != ATAG_CORE)
        convert_to_tag_list((struct param_struct *)tags,
                 meminfo.nr_banks == 0);

    if (tags && tags->hdr.tag == ATAG_CORE)
        parse_tags(tags);

    if (meminfo.nr_banks == 0) {
        meminfo.nr_banks = 1;
        meminfo.bank[0].start = PHYS_OFFSET;
        meminfo.bank[0].size = MEM_SIZE;
    }

    init_mm.start_code = (unsigned long) &_text;
    init_mm.end_code = (unsigned long) &_etext;
    init_mm.end_data = (unsigned long) &_edata;
    init_mm.brk     = (unsigned long) &_end;

    memcpy(saved_command_line, from, COMMAND_LINE_SIZE);
    saved_command_line[COMMAND_LINE_SIZE-1] = '\0';
    parse_cmdline(&meminfo, cmdline_p, from);
    bootmem_init(&meminfo);
    paging_init(&meminfo, mdesc);
    request_standard_resources(&meminfo, mdesc);

    
/*
     * Set up various architecture-specific pointers
     */

    init_arch_irq = mdesc->init_irq;

#ifdef CONFIG_VT
#if defined(CONFIG_VGA_CONSOLE)
    conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
    conswitchp = &dummy_con;
#endif
#endif
}

 
    这里面涉及到3个比较复杂的结构体,包括param_struct、tag、machine_desc。第一步的操作是关于根设备号,暂时不探讨;第二步工作setup_processor,是设置处理器,这是多处理器相关部分,暂时不探讨;第三步工作是setup_machine,这里就需要了解了。
 
   首先,machine_arch_type没有定义,仅仅在头部有定义,这是全局变量,两者之间一定存在联系:
 

unsigned int __machine_arch_type;

 
   看看头文件,应该有#include <asm/mach-types.h>,但是未编译时并没有,可以确定是编译前完成的。这里只有看Makefile了。因为setup.c在这里,首先看同层的Makefile。这一层没有关于mach-types.h的信息,然后到上一层Makefile,发现了:
 

MRPROPER_FILES += \
        arch/arm/tools/constants.h* \
        include/asm-arm/arch \
        include/asm-arm/proc \
        include/asm-arm/constants.h* \
        include/asm-arm/mach-types.h

# We use MRPROPER_FILES and CLEAN_FILES now
archmrproper:
        @/bin/true

archclean:
        @$(MAKEBOOT) clean

archdep: scripts/mkdep archsymlinks
        @$(MAKETOOLS) dep
        @$(MAKEBOOT) dep

 
   说现在使用MRPROPER_FILES,但是下面没有出现,故而应该看几个宏的定义:
 

MAKEBOOT = $(MAKE) -C arch/$(ARCH)/boot
MAKETOOLS = $(MAKE) -C arch/$(ARCH)/tools

 
    由此知道,对应的子文件夹包括boot和tools,boot是与启动相关,不太可能;而前面也看到,tools下有mach-types,所以判断在tools下面,看看tools/Makefile:
 

all: $(TOPDIR)/include/asm-arm/mach-types.h \
        $(TOPDIR)/include/asm-arm/constants.h

$(TOPDIR)/include/asm-arm/mach-types.h: mach-types gen-mach-types
        awk -f gen-mach-types mach-types > $@

 
    由此判断出,mach-types.h是如何生成的,主要是利用awk脚本处理生成。生成之后与s3c2410有关的部分为:
 

#ifdef CONFIG_S3C2410_SMDK
# ifdef machine_arch_type
# undef machine_arch_type
# define machine_arch_type __machine_arch_type
# else
# define machine_arch_type MACH_TYPE_SMDK2410
# endif
# define machine_is_smdk2410() (machine_arch_type == MACH_TYPE_SMDK2410)
#else
# define machine_is_smdk2410() (0)
#endif

 
    由此就知道了,这里的machine_arch_type为193,所以此函数实际上执行:mdesc = setup_machine(193);它要填充结构体machine_desc,如下:
 

struct machine_desc {
    
/*
     * Note! The first four elements are used
     * by assembler code in head-armv.S
     */

    unsigned int        nr;        /* architecture number    */
    unsigned int        phys_ram;    /* start of physical ram */
    unsigned int        phys_io;    /* start of physical io    */
    unsigned int        io_pg_offst;    
/* byte offset for io
                         * page tabe entry    */


    const char        *name;        /* architecture name    */
    unsigned int        param_offset;    /* parameter page    */

    unsigned int        video_start;    /* start of video RAM    */
    unsigned int        video_end;    /* end of video RAM    */

    unsigned int        reserve_lp0 :1;    /* never has lp0    */
    unsigned int        reserve_lp1 :1;    /* never has lp1    */
    unsigned int        reserve_lp2 :1;    /* never has lp2    */
    unsigned int        soft_reboot :1;    /* soft reboot        */
    void            (*fixup)(struct machine_desc *,
                     struct param_struct *, char **,
                     struct meminfo *);
    void            (*map_io)(void);/* IO mapping function    */
    void            (*init_irq)(void);
};

 
    另外,还提供了一系统的宏,用于填充该结构体:
 

/*
 * Set of macros to define architecture features. This is built into
 * a table by the linker.
 */

#define MACHINE_START(_type,_name) \
const struct machine_desc __mach_desc_##_type \
 __attribute__((__section__(".arch.info"))) = { \
        nr: MACH_TYPE_##_type, \
        name: _name,

#define MAINTAINER(n)

#define BOOT_MEM(_pram,_pio,_vio) \
        phys_ram: _pram, \
        phys_io: _pio, \
        io_pg_offst: ((_vio)>>18)&0xfffc,

#define BOOT_PARAMS(_params) \
        param_offset: _params,

#define VIDEO(_start,_end) \
        video_start: _start, \
        video_end: _end,

#define DISABLE_PARPORT(_n) \
        reserve_lp##_n: 1,

#define BROKEN_HLT /* unused */

#define SOFT_REBOOT \
        soft_reboot: 1,

#define FIXUP(_func) \
        fixup: _func,

#define MAPIO(_func) \
        map_io: _func,

#define INITIRQ(_func) \
        init_irq: _func,

#define MACHINE_END \
};

 
    EDUKIT填充了一个结构体,用如下的方式:
 

MACHINE_START(SMDK2410, "Embest EduKit III (S3C2410x)")
    BOOT_MEM(0x30000000, 0x48000000, 0xe8000000)
    BOOT_PARAMS(0x30000100)
    FIXUP(fixup_smdk)
    MAPIO(smdk_map_io)
    INITIRQ(s3c2410_init_irq)
MACHINE_END

 
    看到有特殊的设置部分,那就是开始为之分配了一个段,段的名字是.arch.info,也就是说把这部分信息单独作为一个段来进行处理。下面把这个宏展开如下:
 

const struct machine_desc __mach_desc_smdk2410 = {
    nr: 193,
    name: "EDUKIT-III (s3c2410)",
    phys_ram: 0x30000000,
    phys_to: 0x48000000,
    io_pg_offset: 0x3a00,
    param_offset: 0x30000100,
    fixup: fixup_smdk,//实际上为空
    map_io: smdk_map_io,
    init_irq: s3c2410_init_irq,
};

 
    可见,基本的信息已经具备了,而且从这里,我们也可以看出,启动参数地址由这个段就可以完成,不需要传递了。当然,必须保证bootloader的值,与此处的相同。这样,也就说明如果不使用R2传递参数的起始地址,那么这个地方就需要把这个结构体设置好。
 
    下面看看这个函数完成什么功能:
 

static struct machine_desc * __init setup_machine(unsigned int nr)
{
    extern struct machine_desc __arch_info_begin, __arch_info_end;
    struct machine_desc *list;

    
/*
     * locate architecture in the list of supported architectures.
     */

    for (list = &__arch_info_begin; list < &__arch_info_end; list++)
        if (list->nr == nr)
            break;

    
/*
     * If the architecture type is not recognised, then we
     * can co nothing...
     */

    if (list >= &__arch_info_end) {
        printk("Architecture configuration botched (nr %d), unable "
         "to continue.\n", nr);
        while (1);
    }

    printk("Machine: %s\n", list->name);

    return list;
}

 
    这个地方就是要把上面这一系列的信息连贯起来,那么就不难理解了。上述的宏已经完成了.arch.info段,这个段实际上在内存中就是一个machine_desc形式组织的信息(对Linux内核来说,并不一定仅仅有一个结构块),上述函数的两个变量__arch_info_begin和__arch_info_end很明显是有链接脚本传递进来。于是查看近层的链接脚本(【arch/arm/vmlinux-armv.lds.in】,可以发现:
 

.init : { /* Init code and data */
                _stext = .;
                __init_begin = .;
                        *(.text.init)
                __proc_info_begin = .;
                        *(.proc.info)
                __proc_info_end = .;
                __arch_info_begin = .;
                        *(.arch.info)
                __arch_info_end = .;

                __tagtable_begin = .;
                        *(.taglist)
                __tagtable_end = .;

 
    所以上述的功能就很简单了,就是查看是否有mach-type为193的结构存在,如果存在就打印出name,这也就是开机启动后,出现Machine: Embest EduKit III (S3C2410)的原因了。
 
    接下来关注:
 

    if (mdesc->param_offset)
        tags = phys_to_virt(mdesc->param_offset);

 
   很明显,这里的mdesc->param_offset并不为0,而是0x30000100,所以要做一步变换,就是物理地址映射成虚拟地址。把这个地址附给tags指针。然后就是判断是param_struct类型还是tags类型,如果是param_struct类型,那么首先转换成tags类型,然后对tags类型进行解析。
 

    if (tags && tags->hdr.tag != ATAG_CORE)
        convert_to_tag_list((struct param_struct *)tags,
                 meminfo.nr_banks == 0);

    if (tags && tags->hdr.tag == ATAG_CORE)
        parse_tags(tags);

 
    要注意parse_tags函数是非常重要的,它有隐含的功能,不太容易分析。跟踪上去,主要看这个函数:
 

/*
 * Scan the tag table for this tag, and call its parse function.
 * The tag table is built by the linker from all the __tagtable
 * declarations.
 */

static int __init parse_tag(const struct tag *tag)
{
    extern struct tagtable __tagtable_begin, __tagtable_end;
    struct tagtable *t;

    for (t = &__tagtable_begin; t < &__tagtable_end; t++)
        if (tag->hdr.tag == t->tag) {
            t->parse(tag);
            break;
        }

    return t < &__tagtable_end;
}

 
    这里又用到链接器传递参数,现在就是来解析每个部分。先看一下tagtable是如何来的。首先看【include/asm-arm/setup.h】,看看宏的定义,也就是带有__tag,就归属为.taglist段。
 

#define __tag __attribute__((unused, __section__(".taglist")))
#define __tagtable(tag, fn) \
static struct tagtable __tagtable_##fn __tag = { tag, fn }

 
    利用__tag有构造了一个复杂的宏__tagtable,实际上就是定义了tagtable列表。现在看setup.c中的宏形式示例:
 

__tagtable(ATAG_CMDLINE, parse_tag_cmdline);

 
    展开之后为:
 

static struct tagtable __tagtable_ATAG_CMDLINE __tag = {
    ATAG_CMDLINE,
    parse_tag_cmdline
};

 
    于是,段.taglist就是这样一系列的结构体。那么上述的函数实际上就是把传递进来的tag与此表比较,如果tag标记相同,证明设置了此部分功能,就执行相应的解析函数。以ATAG_CMDLINE为例,就要执行:
 

static int __init parse_tag_cmdline(const struct tag *tag)
{
#ifndef CONFIG_NO_TAG_CMDLINE
    strncpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);
#endif
    default_command_line[COMMAND_LINE_SIZE - 1] = '\0';
    return 0;
}

 
    这样也就是实现了把tag中的命令行参数复制到了default_command_line中。
 
    在返回来到函数【arch/arm/kernel/setup.c】,看函数setup_arch,定义中有:
 

char *from = default_command_line;

 
    说明from指向数组default_command_line。于是知道,当你完成tag解析的时候,所有传递过来的参数实际上已经复制到了相应的部分,比如命令行设置复制到了default_command_line。其他类似,看相应的解析行为函数就可以了。因为现在vivi只是传递了命令行,所以只是分析清楚这个。后面执行:
 

memcpy(saved_command_line, from, COMMAND_LINE_SIZE);

 
    这就比较容易理解了,就是将传递进来的命令行参数复制到saved_command_line,后面还可以打印出此信息。再往后的工作已经与此情景关系不大,所以不再进行详细分析。
 
    至此,vivi与Linux kernel的参数传递情景分析就完成了。


linux2.6内核中的MACHINE_START宏

现在正在阅读linux2.6.18内核,在mainstone.c文件中,有如下的宏定义:
MACHINE_START(MAINSTONE, "Intel HCDDBBVA0 Development Platform (aka Mainstone)")
        /* Maintainer: MontaVista Software Inc. */
        .phys_io        = 0x40000000,
        .boot_params        = 0xa0000100,        /* BLOB boot parameter setting */
        .io_pg_offst        = (io_p2v(0x40000000) >> 1 & 0xfffc,
        .map_io                = mainstone_map_io,
        .init_irq        = mainstone_init_irq,
        .timer                = &pxa_timer,
        .init_machine        = mainstone_init,
MACHINE_END
请问各位大侠,这个宏定义甚么时候调用的,是谁调用的它,象里面的mainstone_init是哪个函数调用的它?是不是在main函数中的初始化的时候?

自己看宏的定义,主要是定义了"struct machine_desc"的类型,放在 section(".arch.info.init"),是初始化数据,Kernel 起来之后将被丢弃。

kernel boot 起来的时候期望 bootloader 传参数进来,其中包括 Machine Type,参考
arch/arm/tools/mach-types 并和 MACHINE_START() 第一个参数对上号。因此,哪个
MACHINE 是 run-time 的时候决定的,this way, you can pack as many machine as you
want, and dynamically initialize the specific platforms.

各个成员函数在不同时期被调用:
1. .init_machine 在 arch/arm/kernel/setup.c 中被 customize_machine 调用,放在 arch_initcall() 段里面,会自动按顺序被调用 start_kernel,参考 init/main.c
2. init_irq在start_kernel() --> init_IRQ() --> init_arch_irq() 被调用
3. map_io 在 setup_arch() --> paging_init() --> devicemaps_init()

其他主要都在 setup_arch() 中用到