Linux 内存管理系统:初始化 - 嵌入式
来源:百度文库 编辑:神马文学网 时间:2024/04/28 13:50:56
Linux 内存管理系统:初始化
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<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,但是未编译时并没有,可以确定是编译前完成的。这里只有看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() 中用到
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<
{
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
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() 中用到
Linux 内存管理系统:初始化 - 嵌入式
Linux 内存管理系统:初始化 - 嵌入式
Linux 内存管理系统:初始化
构建嵌入式Linux系统
Linux内存管理
BusyBox 简化嵌入式 Linux 系统
BusyBox 简化嵌入式 Linux 系统
构建嵌入式Linux系统平台
嵌入式linux系统开发概述
嵌入式linux系统开发概述
BusyBox 简化嵌入式 Linux 系统
BusyBox 简化嵌入式 Linux 系统
BusyBox 简化嵌入式 Linux 系统
Linux内核高端内存管理
嵌入式设备上的 Linux 系统开发
嵌入式Linux系统的触摸屏驱动开发
关于嵌入式Linux系统的启动
单片机和嵌入式系统linux的区别
嵌入式系统设计中查找内存丢失的策略
我也来学做嵌入式Linux系统V0.1(完整版)
Linux嵌入式系统与硬件平台的关系
入门:Linux 2.6 内核的嵌入式系统应用
嵌入式系统 Boot Loader 技术内幕 and Linux Process
Linux内存管理(三)_菜鸟学习笔记