我们要将Linux内核移植到S3C2440(arm9)平台上,和移植U-Boot一样,在移植Linux内核之前,我们先了解Linux内核的启动过程,我们这里以S3C2440平台为例。
典型的内核映像是zImage,包含自引导程序和压缩的vmlinux两部分。启动过程也就是解压和启动vmlinux的过程。我们主要讲解vmlinux的启动过程。对于其他格式的内核,比如zImage、bzImage等,它们都要进行自解压得到vmlinux,然后执行vmlinux的启动过程。
在前面的文章中,我们介绍了ARM架构处理器上vmlinux的编译连接过程。在这一篇文章中,我们讲解它的启动过程。vmlinux内核的启动过程分为两部分:架构/开发板相关的引导过程和通用的启动过程。
引导阶段通常使用汇编语言编写,它首先检查内核是否支持当前架构的处理器,然后检查是否支持当前开发板。由于连接内核时使用的虚拟地址,所以还要要设置页表、使能MMU。之后,就为调用下一阶段的C函数start_kernel作准备,包括复制数据段、清除BSS段等。
在调用内核前,下列条件要满足:
1.CPU寄存器的设置:
2.CPU模式:
3.Cache和 MMU的设置:
vmlinux的连接顺序为: head-y、init-y、core-y、lib-y、drivers-y和net-y。head-y := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o。S3C2440有MMU,所以连接的第一个文件head.S。
对于ARM体系,Linux的连接脚本为arch/arm/kernel/vmlinux.lds.S文件。
OUTPUT_ARCH(arm)
ENTRY(stext)
...
因此,vmlinux的入口是head.S中stext标记的地方。
ENTRY(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE
@ ensure svc mode
@ and irqs disabled
禁止FIQ、IRQ,进入svc模式。
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
__lookup_processor_type函数确定内核支持当前CPU。如果支持,r5寄存器返回一个用来描述处理器的结构体地址,否则r5的值为0。
内核镜像中,定义了若干个proc_info_list结构,表示它支持的CPU。对于ARM结构的CPU,这些结构体的源码在arch/arm/mm/目录下。比如 proc-arm920.S,它表示arm920CPU的proc_info_list结构。
不同的proc_info_list结构被用来支持不同的CPU,它们都定义在“proc.info.init”段中。在连接内核时,这些结构体组织在一起。
__lookup_processor_type函数根据前面读出的CPU ID(存放在r9中),从这些proc_info_list结构中找出匹配的。
bl __lookup_machine_type @ r5=machinfo
内核对于每种支持的开发板都会使用MACHINE_START和MACHINE_END来定义一个machine_desc结构,其中定义了开发板的一些属性和函数,例如机器类型ID、起始I/O物理地址、BootLoader传入的参数的地址、中断初始化函数、I/O映射函数等。对于SMDK2440开发板,在arch/arm/mach-s3c2440/mach-smdk2440.c中有如下定义:
MACHINE_START(S3C2440, "SMDK2440")
/* Maintainer: Ben Dooks */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.init_irq = s3c24xx_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.timer = &s3c24xx_timer,
MACHINE_END
宏MACHINE_ START、MACHINE_ END在include/asm-arm/mach/arch.h文件中定义
#define MACHINE_START(_type,_name) \
const struct machine_desc __mach_desc_##_type \
__attribute__((__section__(".arch.info"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
上一段代码扩展开
static const struct machine_desc __mach_desc_S3C2440
__used
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_S3C2440, \
.name = “SMDK2440”,
...
其中的MACH_TYPE_S3C2440在arch/arm/tools/mach-types中定义,它最后会转换成一个头文件include/asm-arm/mach-types.h供其他文件包含。
所有的machine_desc结构在连接内核时,被组织在一起。不同machine_desc结构支持不同的开发板,U-Boot调用内核时,会在r1寄存器中给出开发板的标记,也就是机器类型ID。__lookup_machine_type函数会将这个值与machine_desc结构中的nr成员比较,如果相等则表示找到了匹配的machine_desc结构体并返回它的地址(这个地址保存在r5寄存器中)。
从连接文件arch/arm/kernel/vmlinux.lds.S我们可以看到,内核是使用虚拟地址进行连接的。
...
SECTIONS
{
#ifdef CONFIG_XIP_KERNEL
. = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);
#else
. = PAGE_OFFSET + TEXT_OFFSET;
#endif
...
查询到了处理器类型和系统的内存映像后就要进入初始化过程中比较关键的一步了,开始设置mmu,但首先要设置一个临时的内核页表。所以,必须使用__create_page_tables函数创建一级页表,建立虚拟函数到物理地址的映射关系。在创建页表前,代码执行前需要将它的虚拟地址转换成物理地址。
ldr r13, __switch_data @ address to jump to after
//将列表__switch_data存到r13中,后面会跳到该列表处
@ mmu has been enabled
adr lr, __enable_mmu @ return (PIC) address
//将程序段__enable_mmu的地址存到lr中
add pc, r10, #PROCINFO_INITFUNC。
//此命令将导致程序段__arm920_setup 的执行,
R10中存放的是在函数 __lookup_processor_type 中成功匹配的结构体 proc_info_list。
对于arm920 来说在文件 linux/arch/arm/mm/proc-arm920.S 中有:
.section ".proc.info.init", #alloc, #execinstr
.type __arm920_proc_info,#object
__arm920_proc_info:
.long 0x41009200
.long 0xff00fff0
.long PMD_TYPE_SECT | /
PMD_SECT_BUFFERABLE | /
PMD_SECT_CACHEABLE | /
PMD_BIT4 | /
PMD_SECT_AP_WRITE | /
PMD_SECT_AP_READ
.long PMD_TYPE_SECT | /
PMD_BIT4 | /
PMD_SECT_AP_WRITE | /
PMD_SECT_AP_READ
b __arm920_setup
add pc, r10, #PROCINFO_INITFUNC的意思跳到函数 __arm920_setup去执行。
__arm920_setup:
mov r0, #0
mcr p15, 0, r0, c7, c7 @关闭I D Cache
mcr p15, 0, r0, c7, c10, 4 @ 清除write buffer
#ifdef CONFIG_MMU
mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
#endif
adr r5, arm920_crval
ldmia r5, {r5, r6}
mrc p15, 0, r0, c1, c0 @ get control register v4
bic r0, r0, r5
orr r0, r0, r6
mov pc, lr
使能MMU,接下来的代码的执行就不需要将虚拟地址转换成物理地址,可以直接运行。
__enable_mmu:
...
b __turn_mmu_on
...
__turn_mmu_on:
mov r0, r0
mcr p15, 0, r0, c1, c0, 0 @ write control reg
mrc p15, 0, r3, c0, c0, 0 @ read id reg
mov r3, r3
mov r3, r3
mov pc, r13
在前面有这样的指令操作 ldr r13, __switch_data ,mov pc, r13 就是将跳转到 __switch_data处
接下来执行__switch_data函数
__switch_data:
...
__mmap_switched:
adr r3, __switch_data + 4
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ldmia r3, {r4, r5, r6, r7, sp}
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r7, {r0, r4} @ Save control register values
b start_kernel //程序跳转到函数start_kernel进入C语言部分。
第二阶段的关键代码主要是C语言编写的。它进行内核初始化的全部工作,在最后调用rest_init启动init进程,创建系统第一个进程。在第二阶段,仍有部分架构/开发板相关的初始化代码。
在start_kernel函数中,我们仅分析和架构/开发板相关的初始化相关的代码,这部分代码在后面的移植中会用到。
这个时候的printk函数仅是将打印信息放在缓冲区,并没有打印到控制台上(比如,串口、LCD等),因为此时控制台还没有初始化。printk函数打印的内容要在console_init函数注册和初始化控制台之后才能真正的输出。
我们重点讲解这个函数,以及tag列表的处理过 程。U-Boot传给Linux内核的参数有存放在0x3000 0100的 tag列表和存放在r1寄存器中的机器类型ID。机器 类型ID在前面的Linux引导阶段已经用到了,tag列表的 处理在setup_arch函数中进行初步处理,在start_kernel中 的其他函数会进行进一步的处理。
void __init setup_arch(char **cmdline_p)
{
...
if (__atags_pointer)
tags = phys_to_virt(__atags_pointer);
else if (mdesc->boot_params)
tags = phys_to_virt(mdesc->boot_params);//将U-Boot传递进来的参数的真实地址0x3000 0100转换成0xc000 0100
...
if (tags->hdr.tag == ATAG_CORE) {
if (meminfo.nr_banks != 0)
squash_mem_tags(tags);
save_atags(tags);
parse_tags(tags);//解释每一个tag
...
parse_cmdline(cmdline_p, from);//cmdline_p即command_line 对命令行进行一些先期的处理,因为接下来要建立页表
paging_init(mdesc);//建立页表,初始化内存,该函数arch/arm/mm/mmu.c中
...
}
parse_tags(tags);//解释每一个tag。
arch/arm/kernel/setup.c中对每一个tag都定义了相应的处理函数,使用了如下代码指定了它们的处理函数。
__tagtable(ATAG_CORE, parse_tag_core);
__tagtable(ATAG_MEM, parse_tag_mem32);
__tagtable(ATAG_VIDEOTEXT, parse_tag_videotext);
__tagtable(ATAG_SERIAL, parse_tag_serialnr);
__tagtable(ATAG_REVISION, parse_tag_revision);
__tagtable(ATAG_CMDLINE, parse_tag_cmdline);
static int __init parse_tag_mem32(const struct tag *tag)
{
return arm_add_memory(tag->u.mem.start, tag->u.mem.size);
}
__tagtable(ATAG_MEM, parse_tag_mem32);
parse_tag_mem32函数根据内存tag定义的内存起始地址、长度,在全局结构变量meminfo中增加内存的描述信息。之后内核可以通过menminfo结构了解开发板的内存信息。
static int __init parse_tag_cmdline(const struct tag *tag)
{
strlcpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);
return 0;
}
__tagtable(ATAG_CMDLINE, parse_tag_cmdline);
parse_tag_cmdline函数仅是简单的将命令行参数tag复制到default_command_line,后面会进一步处理。
parse_cmdline是对其中的一些参数进行先期的处理,这些参数使用“__early_param”定义。
在Linux-2.6.30中,有如下字符被__early_param定义:
/arch/arm/kernel/setup.c __early_param("mem=", early_mem);
/arch/arm/mm/init.c __early_param("initrd=", early_initrd);
/arch/arm/mm/mmu.c __early_param("cachepolicy=", early_cachepolicy);
/arch/arm/mm/mmu.c __early_param("nocache", early_nocache);
/arch/arm/mm/mmu.c __early_param("nowb", early_nowrite);
/arch/arm/mm/mmu.c __early_param("ecc=", early_ecc);
/arch/arm/mm/mmu.c __early_param("vmalloc=", early_vmalloc);
我们移植的U-Boot设置这类参数,感兴趣的网友可以阅读相关源码进行学习。
命令行参数的处理还没有结束,在setup_arch函数之外还会进行一些处理,比如start_kernel中会调用如下的代码继续命令行参数的处理。
...
setup_command_line(command_line);
...
parse_early_param();
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);
...
我们在后面会继续讲解。
setup_arch函数调用paging_init(mdesc),其中mdesc结构是在linux内核引导阶段lookuo_machine_type函数中返回的machine_desc结构,S3C2440的这个结构定义在arch/arm/mach-s3c2440/mach-smdk2440.c中:
MACHINE_START(S3C2440, "SMDK2440")
/* Maintainer: Ben Dooks */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.init_irq = s3c24xx_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.timer = &s3c24xx_timer,
MACHINE_END
这个结构体在移植Linux的要格外的关注。paging_init函数定义在arch/arm/mm/mmu.c中定义。由于我们分析源码的目的是方便我们移植linux内核,我们仅需要关注以下函数调用关系即可:
paging_init----->devicemaps_init----->mdesc->map_io()
对于S3C2440开发板,上面的函数调用最终是执行smdk2440_map_io函数:
static void __init smdk2440_map_io(void) //arch/arm/mach-s3c2440/mach-smdk2440.c
{
s3c24xx_init_io(smdk2440_iodesc, ARRAY_SIZE(smdk2440_iodesc));
s3c24xx_init_clocks(16934400);//时钟初始化
s3c24xx_init_uarts(smdk2440_uartcfgs, ARRAY_SIZE(smdk2440_uartcfgs));
}
这里分析的是系统能够辨别的一些早期的参数,这些参数用early_param来定义,在init/main.c中定义了如下一些参数:
early_param("nosmp", nosmp);
early_param("maxcpus", maxcpus);
early_param("debug", debug_kernel);
early_param("quiet", quiet_kernel);
early_param("loglevel", loglevel);
而且在分析的时候并不是以setup_arch(&command_line)传出来的command_line为基础,而是以最原始的命令行参数为基础。
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);
与parse_early_param相比,此处对解析表处理的范围加大了,解析表中除了包括以setup定义的启动参数,还包括模块中定义的param参数,以及系统不能识别的参数。在init/main.c中有如下一些用__setup来定义的参数:
__setup("reset_devices", set_reset_devices);
__setup("init=", init_setup);
__setup("rdinit=", rdinit_setup);
与parse_early_param相比,此处对解析表处理的范围加大了,解析表中除了包括以setup定义的启动参数,还包括模块中定义的param参数,以及系统不能识别的参数。在init/main.c中有如下一些用__setup来定义的参数:
__setup("reset_devices", set_reset_devices);
__setup("init=", init_setup);
__setup("rdinit=", rdinit_setup);
在kernel/printk中定义了如下一些参数:
__setup("log_buf_len=", log_buf_len_setup);
__setup("boot_delay=", boot_delay_setup);
__setup("console=", console_setup);
在前面我们讲到,kernel/printk中定义了如下参数:__setup("console=", console_setup);这会导致console_setup函数的执行。我们在移植U-Boot时设置的参数是“console=ttySAC0”,指定了要使用的控制台的名称和序号。经过console_setup函数处理后,会在全局结构变量console_cmdline中保存这些信息。console_init函数初始化控制台时会根据这些信息选择要使用的控制台。
void __init console_init(void)
{
initcall_t *call;
...
call = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
}
它调用_con_initcall_start到_con_initcall_end之间定义的每个函数,这些函数使用console_initcall宏指定。
在linux/drivers/serial/samsung.h有如下定义:
#define s3c24xx_console_init(__drv, __inf) \
static int __init s3c_serial_console_init(void) \
{ \
return s3c24xx_serial_initconsole(__drv, __inf); \
} \
\
console_initcall(s3c_serial_console_init)
在linux/drivers/serial/samsuing.c中
int s3c24xx_serial_initconsole(struct platform_driver *drv,
struct s3c24xx_uart_info *info)
int s3c24xx_serial_initconsole(struct platform_driver *drv,
struct s3c24xx_uart_info *info)
{
...
register_console(&s3c24xx_serial_console);
return 0;
}
static struct console s3c24xx_serial_console = {
.name = S3C24XX_SERIAL_NAME,//#define S3C24XX_SERIAL_NAME "ttySAC"
.device = uart_console_device,
.flags = CON_PRINTBUFFER,//表示注册控制台之后打印缓冲区中信息
.index = -1,//可以匹配任意序号,比如ttySAC/0/1/2
.write = s3c24xx_serial_console_write,//打印函数
.setup = s3c24xx_serial_console_setup//设置函数
};
在内核注册控制台后,会把s3c24xx_serial_console结构链入一个全局链表console_drivers中。并且使用其中的名字(name,ttySAC)和序号(index,-1)与“console=ttySAC0”指定的控制台相比较,两者匹配,printk打印的信息将从串口0输出。
该函数创建init内核进程,也就是1号进程。至此start_kernel结束,基本的核心环境已经建立起来了。我们在将制作根文件系统时会具体讲解这个函数。