下面進入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的參數傳遞情景分析就完成了。