在上一部分提到过了,vivi作为bootloader,向内核传递启动参数是其本职工作之一。要把这个情景分析清楚,不仅仅需要分析vivi的参数机 制,而且要分析Linux kernel的接收机制。因为这是一个简单的通信过程,比起本科所学习的TCP/IP来简单的多,但是因为简单,所以在协议上并不规范,理解上反而不如 TCP/IP协议。下面就分为两个方面对此情景分析。
一、综述内核参数传递机制
现在内核参数传递机制有两种:一种是基于struct param_struct,这种已经比较老了。缺点是该结构每个成员的位置是固定的,受限比较大。另外一种就是新的struct tag way。说新是相对的,Linux kernel 2.4.x都希望采用这种tag的方式。关于这方面的资料,可以有如下参考(所给出的目录是基于linux-2.4.18的内核,以顶层Makefile 所在目录为当前目录。这里基于ARM架构的S3C2410,其他的SoC可以类比很容易得到):
1、关于bootloader的理解--【Documentation/arm/booting】
此文档详细的讲述了bootloader的作用,具体内容如下:
[armlinux@lqm arm]$ cat Booting
Booting ARM Linux
=================
Author: Russell King
Date : 18 May 2002
The following documentation is relevant to 2.4.18-rmk6 and beyond.
In order to boot ARM Linux, you require a boot loader, which is a small
program that runs before the main kernel. The boot loader is expected
to initialise various devices, and eventually call the Linux kernel,
passing information to the kernel.
Essentially, the boot loader should provide (as a minimum) the
following:
1. Setup and initialise the RAM.
2. Initialise one serial port.
3. Detect the machine type.
4. Setup the kernel tagged list.
5. Call the kernel image.
1. Setup and initialise RAM
---------------------------
Existing boot loaders: MANDATORY
New boot loaders: MANDATORY
The boot loader is expected to find and initialise all RAM that the
kernel will use for volatile data storage in the system. It performs
this in a machine dependent manner. (It may use internal algorithms
to automatically locate and size all RAM, or it may use knowledge of
the RAM in the machine, or any other method the boot loader designer
sees fit.)
2. Initialise one serial port
-----------------------------
Existing boot loaders: OPTIONAL, RECOMMENDED
New boot loaders: OPTIONAL, RECOMMENDED
The boot loader should initialise and enable one serial port on the
target. This allows the kernel serial driver to automatically detect
which serial port it should use for the kernel console (generally
used for debugging purposes, or communication with the target.)
As an alternative, the boot loader can pass the relevant 'console='
option to the kernel via the tagged lists specifing the port, and
serial format options as described in
linux/Documentation/kernel-parameters.txt.
3. Detect the machine type
--------------------------
Existing boot loaders: OPTIONAL
New boot loaders: MANDATORY
The boot loader should detect the machine type its running on by some
method. Whether this is a hard coded value or some algorithm that
looks at the connected hardware is beyond the scope of this document.
The boot loader must ultimately be able to provide a MACH_TYPE_xxx
value to the kernel. (see linux/arch/arm/tools/mach-types).
4. Setup the kernel tagged list
-------------------------------
Existing boot loaders: OPTIONAL, HIGHLY RECOMMENDED
New boot loaders: MANDATORY
The boot loader must create and initialise the kernel tagged list.
A valid tagged list starts with ATAG_CORE and ends with ATAG_NONE.
The ATAG_CORE tag may or may not be empty. An empty ATAG_CORE tag
has the size field set to '2' (0x00000002). The ATAG_NONE must set
the size field to zero.
Any number of tags can be placed in the list. It is undefined
whether a repeated tag appends to the information carried by the
previous tag, or whether it replaces the information in its
entirety; some tags behave as the former, others the latter.
The boot loader must pass at a minimum the size and location of
the system memory, and root filesystem location. Therefore, the
minimum tagged list should look:
+-----------+
base -> | ATAG_CORE | |
+-----------+ |
| ATAG_MEM | | increasing address
+-----------+ |
| ATAG_NONE | |
+-----------+ v
The tagged list should be stored in system RAM.
The tagged list must be placed in a region of memory where neither
the kernel decompressor nor initrd 'bootp' program will overwrite
it. The recommended placement is in the first 16KiB of RAM.
5. Calling the kernel image
---------------------------
Existing boot loaders: MANDATORY
New boot loaders: MANDATORY
There are two options for calling the kernel zImage. If the zImage
is stored in flash, and is linked correctly to be run from flash,
then it is legal for the boot loader to call the zImage in flash
directly.
The zImage may also be placed in system RAM (at any location) and
called there. Note that the kernel uses 16K of RAM below the image
to store page tables. The recommended placement is 32KiB into RAM.
In either case, the following conditions must be met:
- CPU register settings
r0 = 0,
r1 = machine type number discovered in (3) above.
r2 = physical address of tagged list in system RAM.
- CPU mode
All forms of interrupts must be disabled (IRQs and FIQs)
The CPU must be in SVC mode. (A special exception exists for Angel)
- Caches, MMUs
The MMU must be off.
Instruction cache may be on or off.
Data cache must be off.
- The boot loader is expected to call the kernel image by jumping
directly to the first instruction of the kernel image.
可以看出bootloader最少具备5项功能,上面比较清晰。可以看出,现在2.4的内核都是希望采用tagged list的方式来进行传递的,这里没有提到比较老的方式。这里要特别注意的是,r2 = physical address of tagged list in system RAM.,这里的“必须”是针对于tagged list而言的,如果采用param_struct,则并没有这个限制。这在后面将会详细分析,而这正是可能导致疑惑的地方。
2、参数传递数据结构的定义位置【include/asm/setup.h】,在这里就可以看到两种参数传递方式了。可以说,现在 bootloader和Linux kernel约定的参数传递机制就是这两种,必须严格按照这两种机制进行传输,否则的话,kernel可能因为无法识别bootloader传递过来的参 数而导致无法启动。关于这两种方式,在这里还有说明:
/*
* linux/include/asm/setup.h
*
* Copyright (C) 1997-1999 Russell King
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* Structure passed to kernel to tell it about the
* hardware it's running on. See linux/Documentation/arm/Setup
* for more info.
*
* NOTE:
* This file contains two ways to pass information from the boot
* loader to the kernel. The old struct param_struct is deprecated,
* but it will be kept in the kernel for 5 years from now
* (2001). This will allow boot loaders to convert to the new struct
* tag way.
*/
这说明,现在参数传递必须要采用tag方式,因为现在新的kernel已经不支持param_struct方式了。不幸的是,vivi还是采用的 param_struct方式。这里暂时以param_struct为主分析,考虑以后更新为tag方式。在这里你也可以参考 【Documentation/arm/setup】,里面有关于选项具体含义的详细说明。(在这里多说几句。Linux的Documentation是 一个很好的学习库,几乎所有的问题在这里都能有初步的解答。如果要想继续深入,那么就要读源代码了。学习上,先看README,然后翻阅 Documentation,无疑是一条捷径。而且,是否有完备的文档,也是判断这个软件是否优秀的重要标准。)
二、vivi设置Linux参数分析
上面对bootloader与Linux kernel之间参数传递的两种方式已经有了一个总体的理解。下面就来先看vivi部分如何设置Linux参数。
【init/main.c】boot_or_vivi()-->run_autoboot()-->exec_string("boot")
到此,也就是要执行boot命令。与命令相关部分都在【lib/command.c】中,找到boot_cmd,然后跟踪至【lib/boot_kernel.c】,boot的执行行为函数为command_boot(),继续分析:
【lib/boot_kernel.c】command_boot()-->
主要就是三步工作。
· 获取media_type。
media_type = get_param_value("media_type", &ret);
media_type是重要的,因为对于不同的存储介质,底层的驱动函数是不同的。通过media_type这个顶层抽象,实现了与底层驱动的联系。
[armlinux@lqm include]$ cat boot_kernel.h
#ifndef _VIVI_BOOT_KERNEL_H_
#define _VIVI_BOOT_KERNEL_H_
/*
* Media Type: A type of storage device that contains the linux kernel
*
* +----------------+-----------------------------------------+
* | Value(Integer) | Type |
* +----------------+-----------------------------------------+
* | 0 | UNKNOWN |
* | 1 | RAM |
* | 2 | NOR Flash Memory |
* | 3 | SMC (NAND Flash Memory) on the S3C2410 |
* +----------------+-----------------------------------------+
*/
enum {
MT_UNKNOWN = 0,
MT_RAM,
MT_NOR_FLASH,
MT_SMC_S3C2410
};
#endif /* _VIVI_BOOT_KERNEL_H_ */
上面就是vivi支持的media_type,现在此开发板是MT_SMC_S3C2410,也就是nand flash memory的选择部分。
·获取nand flash的kernel分区信息,为下载做好准备
kernel_part = get_mtd_partition("kernel");
if (kernel_part == NULL) {
printk("Can't find default 'kernel' partition\n");
return;
}
from = kernel_part->offset;
size = kernel_part->size;
这里获得了kernel所在nand flash的起始地址和大小。这里应该注意,虽然kernel_part->offset是偏移量,但是这个偏移是相对于0x00000000而 言,所以这时的offset就是对应的起始地址。当然,对nand flash来说,这里的地址并非是内存映射,需要做一系列的变化,具体是在nand_read_ll函数中,前面的基本实验已经做过了。
·启动内核
boot_kernel(from, size, media_type);
利用前面得到的media_type,from,size就可以来启动内核了,当然还有多步工作要去做。具体包括如下内容:
(1)获取内存基地址
boot_mem_base = get_param_value("boot_mem_base", &ret);
在vivi中,sdram是从0x30000000开始的,所以这里的boot_mem_base就是0x30000000.
(2)把kernel映象从nand flash复制到sdram的固定位置
to = boot_mem_base + LINUX_KERNEL_OFFSET;
printk("Copy linux kernel from 0x%08lx to 0x%08lx, size = 0x%08lx ... ",
from, to, size);
ret = copy_kernel_img(to, (char *)from, size, media_type);
这里LINUX_KERNEL_OFFSET是0x8000,关于为什么是0x8000,这是历史原因造成的,是Linux内核的一个约定,具体可以查看Linux内核的源代码中的arch/arm/kernel/head_armv.S,如下:
/*
* We place the page tables 16K below TEXTADDR. Therefore, we must make sure
* that TEXTADDR is correctly set. Currently, we expect the least significant
* "short" to be 0x8000, but we could probably relax this restriction to
* TEXTADDR > PAGE_OFFSET + 0x4000
*
* Note that swapper_pg_dir is the virtual address of the page tables, and
* pgtbl gives us a position-independent reference to these tables. We can
* do this because stext == TEXTADDR
*
* swapper_pg_dir, pgtbl and krnladr are all closely related.
*/
可以看出,TEXTADDR就是stext的地址,本开发板上为0x30008000,在0x30008000往下,会放置16K的页表,预计是 0x8000.不过此处可能会放松这个限制。另外,我们的一些参数也会放到内存起始区域。这在后面就可以看到。总之,这个地方的位置 boot_mem_base也就是kernel的第一条指令所在地,最后的程序跳转要跳到这个位置。
(3)验证magic number
if (*(ulong *)(to + 9*4) != LINUX_ZIMAGE_MAGIC) {
printk("Warning: this binary is not compressed linux kernel image\n");
printk("zImage magic = 0x%08lx\n", *(ulong *)(to + 9*4));
} else {
printk("zImage magic = 0x%08lx\n", *(ulong *)(to + 9*4));
}
这个地方是判断是否有zImage的存在,而zImage的判别的magic number为0x016f2818,这个也是和内核约定好的。你可以用ultra-edit32查看一下zImage,这是我的zImage的头的部分内容(注意,为小端存放格式):
00000000h: 00 00 A0 E1 00 00 A0 E1 00 00 A0 E1 00 00 A0 E1
00000010h: 00 00 A0 E1 00 00 A0 E1 00 00 A0 E1 00 00 A0 E1
00000020h: 02 00 00 EA 18 28 6f 01 00 00 00 00 DB 86 09 00
至于为什么magic number在0x00000024这个位置,需要分析zImage是如何生成的,它的内容是什么,起始的几个字节是什么,这部分内容放到Linux kernel端进行深入分析。不过在这里应该提一句,此处的验证是考虑到Linux kernel相对比较大,而嵌入式系统的资源受限,为了节省资源,一般会将Linux kernel来压缩成zImage格式(识别方式就是在第9个字后有magic number0x016f2818);但是应该明确,这步工作并非是必需的。因为如果内核比较小,为了加快启动速度,我可以不使用压缩的映象,直接采用非 压缩映象,那么vivi此处应该把无法找到maigc number的提示更改为printk("this binary is not compressed linux kernel image\n");。就Linux kernel来说,启动中支持压缩映象和非压缩映象两种启动方式,不管是那种启动方式,第一条指令的地址总是boot_mem_base,只不过放在这里 的指令并非一定是真正的kernel启动指令。这个在后面会详细分析Linux kernel启动方式。
(4)设置Linux参数
setup_linux_param(boot_mem_base + LINUX_PARAM_OFFSET);
现在看一下setup_linux_param的具体动作。
static void setup_linux_param(ulong param_base)
{
struct param_struct *params = (struct param_struct *)param_base;
char *linux_cmd;
//第一步:打印出param_base的基地址,这里就是0x30000100
//这里的这个位置实际上是约定的,预留了256字节
//然后初始化param_struct这个数据结构
printk("Setup linux parameters at 0x%08lx\n", param_base);
memset(params, 0, sizeof(struct param_struct));
//填写params的两个成员
//Linux kernel采用了页表方式,设置页表的大小,这里是4K
params->u1.s.page_size = LINUX_PAGE_SIZE;
params->u1.s.nr_pages = (DRAM_SIZE >> LINUX_PAGE_SHIFT);
/* set linux command line */
linux_cmd = get_linux_cmd_line();
if (linux_cmd == NULL) {
printk("Wrong magic: could not found linux command line\n");
} else {
//把命令行参数复制到params的commandline成员
memcpy(params->commandline, linux_cmd, strlen(linux_cmd) + 1);
printk("linux command line is: \"%s\"\n", linux_cmd);
}
}
如上,把不相关部分去掉了,加了注释。可以看出,这里就设置了param_struct必需的三个成员,核心是commandline。关于param_struct在linux内核的【include/arm/setup.h】中,各个成员的含义是:
/*
* Usage:
* - do not go blindly adding fields, add them at the end
* - when adding fields, don't rely on the address until
* a patch from me has been released
* - unused fields should be zero (for future expansion)
* - this structure is relatively short-lived - only
* guaranteed to contain useful data in setup_arch()
*/
#define COMMAND_LINE_SIZE 1024
/* This is the old deprecated way to pass parameters to the kernel */
struct param_struct {
union {
struct {
unsigned long page_size; /* 0 */
unsigned long nr_pages; /* 4 */
unsigned long ramdisk_size; /* 8 */
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];
};
如上,具体选项的含义,可以参考【Documentation/arm/setup】,这里仅就用到的三个来进行解释。
page_size
This parameter must be set to the page size of the machine, and
will be checked by the kernel.
nr_pages
This is the total number of pages of memory in the system. If
the memory is banked, then this should contain the total number
of pages in the system.
If the system contains separate VRAM, this value should not
include this information.
commandline
Kernel command line parameters. Details can be found elsewhere.
可以看出,这步的设置工作还是非常简单的。现在使用的页表大小为4K,也就是page_size的值。因为现在使用的sdram是64M,总页表项自然就 是64M/page_size,也就是进行简单的右移就可以了(4K等效右移12位)。后面就是获取命令行参数的地址,然后填充comandline成 员,最长的限度为1024.
至此,vivi设置参数就完成了,约定参数的起始地址为boot_mem_base+0x0100处。这个地方是否需要作为参数传递给kernel,就需 要与内核配合了。如果像Linux kernel约定的boot_mem_base+0x8000处存放内核映象一样,Linux kernel对s3c2410的支持同样可以约定参数固定存放于boot_mem_base+0x0100。如果没有此约定,那么就需要传递参数首地址 了。
(5)获取机器号
mach_type = get_param_value("mach_type", &ret);
这个号是固定的。可以参考arch/arm/tools/mach-types。这里列出了所有支持的机器号。应该是按照先后的支持顺序排列。可以看到smdk2410为:
smdk2410 S3C2410_SMDK SMDK2410 193
(6)启动内核
call_linux(0, mach_type, to);
到这里才算是真正启动内核了,使用内嵌汇编写的。这里的三个参数,根据APCS原则,应该分别给R0,R1, R2.这样也就是说,现在:
· R0 设置为0
· R1 machine type number(193)
· R2 内核的第一条指令的起始地址(注意,这里并非参数表的首地址)
void call_linux(long a0, long a1, long a2)
{
cache_clean_invalidate();
tlb_invalidate();
__asm__(
"mov r0, %0\n"
"mov r1, %1\n"
"mov r2, %2\n"
"mov ip, #0\n"
"mcr p15, 0, ip, c13, c0, 0\n" /* zero PID */
"mcr p15, 0, ip, c7, c7, 0\n" /* invalidate I,D caches */
"mcr p15, 0, ip, c7, c10, 4\n" /* drain write buffer */
"mcr p15, 0, ip, c8, c7, 0\n" /* invalidate I,D TLBs */
"mrc p15, 0, ip, c1, c0, 0\n" /* get control register */
"bic ip, ip, #0x0001\n" /* disable MMU */
"mcr p15, 0, ip, c1, c0, 0\n" /* write control register */
"mov pc, r2\n"
"nop\n"
"nop\n"
: /* no outpus */
: "r" (a0), "r" (a1), "r" (a2)
);
}
汇编很简洁。参考前面booting文档,就是做上述工作。现在对R0、R1、R2参数传递完成,不过R2在这里并非tag的首地址,因为采用的是 param_struct模式,所以可以猜测kernel的arch(实际上就是HAL层)肯定有对应的默认地址起始地址(这里是 0x30000100)。其余部分,中断都关闭了,PID为0,I cache和D cache都禁止,write buffer清理,I D TLBS也禁止,禁止MMU。最后mov pc, r2则跳转到内核映象的第一条指令位置。
到这里,vivi的使命完全完成了!后续的工作就交给kernel了。
为了对参数传递这个情景分析清楚,所以还必须分析Linux kernel如何启动。这部分不打算过多的深入细节,首先应该从整体上分析。然而,还是应该借助代码才能理解的更为清晰。这里,有taoyuetao的 Linux启动分析系列文章可以参考,我想,他分析Linux启动也是如同我分析vivi一样,一步一步走过来的。借鉴一下,省去了我不少劳动,在此感 谢。后续的描述中Linux kernel启动部分借鉴taoyuetao的经验,但是对其进行了扩展,增加了zImage如何生成的更为详细的解释。
下面进入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 () (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,具体的压缩算法就不分析了。
至此,就可以用下图作出总结了:
500)this.width=500;" border="0" width="500">
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,#
//重复如下指令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;
看看头文件,应该有#i nclude
,但是未编译时并没有,可以确定是编译前完成的。这里只有看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: s/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 */
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 .
* 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的参数传递情景分析就完成了。