最近公司要求调试一个内核,启动时有问题,所以就花了一点时间看看内核启动。
看的过程中总结了一点东西,希望可以帮助大家调试内核。
当我开始看的时候,第一件事是从网上搜集资料,不看不知道,一看吓一跳!牛人太多了,像这种内核启动的上古代码早就被人分析的彻彻底底。这注定我写的只能是烂微博了。
为了此微博有存在的必要,我会显示内核启动打印的代码位置(用绿色表示)及出现错误打印的原因(用红色表示),同时我会尽力用添加打印(用蓝色字,同时给出对应于本人平台的打印结果)或实例来说明一些细节。
注意我的是linux-3.2.36,有的老版本machine的判断位置不一样。
首先看启动参数
http://blog.chinaunix.net/uid-20543672-id-3151113.html
有两种启动参数
标签列表(taggedlist)或设备树(devicetree)。
引导程序和内核约定r2寄存器中存放的数据所指向的内存地址。
说设备树的微博,可以看看下面这个
http://blog.csdn.net/21cnbao/article/details/8457546
这两个注意:
标签列表必须置于内核自解压和initrd'bootp'程序都不会覆盖的内存区。建议放在RAM的头16KiB中。
设备树必须置于内核自解压不会覆盖的内存区。建议将其放置于RAM的头16KiB中
我简单介绍标签列表格式
· 基地址 -> +-----------+
· | ATAG_CORE | |
· +-----------+ |
· | ATAG_MEM | | 地址增长方向
· +-----------+ |
· | ATAG_NONE | |
· +-----------+ v
viarch/arm/include/asm/setup.h
struct tag_header {
__u32size; //标签总大小(包括tag_header)
__u32tag; //标签标识
};
上面的图就是要linux获取的第一个tag的头的__u32 tag要是ATAG_CORE
最后一个是ATAG_NONE。
struct tag {
structtag_header hdr;
union {
struct tag_core core;// 标签列表开始 struct tag_mem32 mem;// 内存信息标签(可以有多个标签,以标识多个内存区块)
struct tag_videotext videotext;// VGA文本显示参数标签
struct tag_ramdisk ramdisk;// ramdisk参数标签(位置、大小等)
struct tag_initrd initrd;// 压缩的ramdisk参数标签
struct tag_serialnr serialnr;// 板子串号标签
struct tag_revision revision;// 板子版本号标签
struct tag_videolfb videolfb;// 帧缓冲初始化参数标签
struct tag_cmdline cmdline;//就是uboot的bootargs
/*
*Acorn specific
*/
struct tag_acorn acorn;
/*
*DC21285 specific
*/
struct tag_memclk memclk;
} u;
};
先简单的解释一下cmdline,你可以在用户态下cat /proc/cmdline
看到,就是uboot的bootargs。
上面的微博有有对bootm分析,我只说一点
setup_start_tag (bd); //设置ATAG_CORE,这里面有params = (struct tag *) bd->bi_boot_params;
参数地址
kernel_entry(0, machid, bd->bi_boot_params);
r0 = 0 r1 = machid r2 = bd->bi_boot_params)
bd->bi_boot_params在board下对应的板子目录下
我的
bd->bi_boot_params = 0x30000100 我的ram开始是0x30000000
内核的建议
建议放在RAM的头16KiB中,还有一句是:但是不可将其放置于“0”物理地址处,因为内核认为:r2中为0,意味着没有标签列表和dtb传递过来。这在启动是的汇编代码可以看到。
Bootloader到内核的start_kernel之间还有自解压和一些汇编代码,这个我会在下一篇博客分析,现在主要是从setup_arch开始。这也是自解压之后,我们能看到内核打印开始的地方。
下面我们进入内核,看看内核如何获取这些启动参数
init/main.c
asmlinkage void __init start_kernel(void)
{
……
printk(KERN_NOTICE"%s", linux_banner);//这个就是我们linux启动时打印的版本信息,我的:Linux version 3.2.0(root@localhost.localdomain) (gcc version 4.4.3 (ctng-1.6.1) ) #70 Thu Jun 2010:50:51 CST 2013
setup_arch(&command_line);
……
下面是我们的主机
蓝色的字为我添加的调试打印,将在后面调试时开出来
arch/arm/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
struct machine_desc *mdesc;
setup_processor();
根据读出的cpuid,进行匹配,在汇编代码中有也有判断,我简单说一下,一个是从cpu读的id(用cp15协处理器),一个是从arch/arm/mm/ proc-armXXX.c读的,例如我的arm920t,
__arm920_proc_info:
.long 0x41009200
就是这个id,如果不匹配,while(1)死循环。
cpu信息就在这里打印,我的:
CPU: ARM920T [41129200] revision 0 (ARMv4T), cr=c0007177
CPU: VIVT data cache, VIVT instruction cache
如果你没有那么是你的cpu类型选的不对,但是cpu类型一般是和machine一起选的,所以还是看下面的
//wxl 添加
printk(KERN_NOTICE “__atags_pointer: %lx\n”, (unsigned long)__atags_pointer);
打印为__atags_pointer : 30000100
还记得uboot的
bd->bi_boot_params= 0x30000100
mdesc = setup_machine_fdt(__atags_pointer);这个函数时获取设备树的信息,我的没有,会返回NULL
if (!mdesc)
mdesc =setup_machine_tags(machine_arch_type);
这个是处理tagged_list的主要函数,在下面,我没有放这里,你先看看它在向下看。
machine_desc = mdesc;
machine_name = mdesc->name;这是MINI2440
#ifdef CONFIG_ZONE_DMA
if (mdesc->dma_zone_size) {
extern unsigned longarm_dma_zone_size;
arm_dma_zone_size =mdesc->dma_zone_size;DMA区域大小
}
#endif
if (mdesc->soft_reboot)
reboot_setup("s");
这个是重启方式,”s”为软件,”h”为硬件,最终重启调用arch_reset(mode,cmd);这个函数成功就重启不会返回,如果没有成功,你会看到Reboot failed --System halted打印;这个函数由平台提供,samsung没有什么软件重启模式,mach-ebsa110有,有兴趣可以看看。
init_mm.start_code = (unsigned long) _text;
init_mm.end_code = (unsignedlong) _etext;
init_mm.end_data = (unsignedlong) _edata;
init_mm.brk = (unsignedlong) _end;
上面的在平台连接脚本arch/arm/kernel/vmlinux.lds还有个vmlinux.lds.S看到大小
. = 0xC0000000 + 0x00008000;
.head.text : {
_text = .;
*(.head.text)
}
…
如果你编译内核会在源码目录下生产
System.map文件
我贴一点
000000c A cpu_arm920_suspend_size
c0004000 A swapper_pg_dir
c0008000 T _text
c0008000 T stext
…
c02f9b90 A _etext
…
c03367c0 D _edata
…
c0367db8 A _end
…
能看到一些上面的东西,不过我们还是要打印一下
//wxl 增加
printk(KERN_NOTICE“_text =%lx\n_etext=%lx\n_edata=%lx\n_end=%lx\n”, \
(unsigned long)_text, (unsigned long) _etext, (unsigned long) _edata, (unsigned long) _end);
打印结果
_text = c0008000
_etext=c02f9b90
_edata=c03367c0
_end=c0367db8
和上面一样吧
/* populate cmd_line too for later use, preserving boot_command_line */
strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
*cmdline_p = cmd_line; 将boot_command_line复制到cmd_line中
这个函数是对cmdline的early_param处理,后面还有处理,在这个微博不会看到
parse_early_param();
这里需要注意的是内核的cmdline中的参数按照其被需要的先后,分为early和非early的。
parse_args("early options", cmdline, NULL, 0,do_early_param);
这个代码在kernerl/params.c下
while (*args) {
int ret;
intirq_was_disabled;
//wxl 增加
printk(KERN_NOTICE “parse_args args: %s\n”, args);
打印结果
parse_args args: mem=64M console=ttySAC0,115200 no_console_suspendroot=/dev/mtdblock3 rootfstype=jffs2mtdparts=384K(u-boot),128K(u-boot-env),5M(kernel),20M(root)
和我uboot的bootargs一样
args =next_arg(args, ¶m, &val);
irq_was_disabled = irqs_disabled();
//wxl增加
printk(KERN_NOTICE “param:%s val =%s\n”, param, val);
打印结果。循环打印
param: mem val =64M
param: console val =ttySAC0,115200
param: no_console_suspend val =(null)
param: root val =/dev/mtdblock3
param: rootfstype val =jffs2
param: mtdparts val=384K(u-boot),128K(u-boot-env),5M(kernel),20M(root)
就是一个取
params是NULL,num是0,unknown是do_early_param函数
ret =parse_one(param, val, params, num, unknown);
由于parse_one的num是0,所以parse_one就是执行do_early_param
System.map下有c0316740 T__setup_start
c0316aa0 T __setup_end
在链接文件里,__setup_start = .;*(.init.setup) __setup_end = .;
static int __init do_early_param(char *param, char *val)
{
const struct obs_kernel_param *p;
struct obs_kernel_param {
const char *str; //在cmdline中相应参数名。
int(*setup_func)(char *); //对于此参数的专用处理函数
int early; //是否为早期需要处理的参数
};
for (p = __setup_start; p <__setup_end; p++) {
if ((p->early &¶meq(param, p->str)) ||
(strcmp(param,"console") == 0 &&
strcmp(p->str, "earlycon") ==0)
) {这里看到判断了early是1,是0的在后面处理,在此微博不会看到了,以后我在说。不过console可以用earlycon指定的,就不要early,例如:
8250_early.c:early_param("earlycon",setup_early_serial8250_console);
if(p->setup_func(val) != 0)参数对应就可以执行对应函数
printk(KERN_WARNING
"Malformed early option '%s'\n", param);你看到这个打印就知道是你的early处理函数出错
}
}
/* We accept everything at this stage.*/
return 0;
}
这就是执行.init.setup的东西,也就是执行
#define early_param(str, fn) \
__setup_param(str,fn, fn, 1)
early_param("mem", early_mem); mem就是
我们要看一下early_param->arm_add_memory(start, size);这个start = PHTS_OFFSET就是0x30000000 size就是我们传入的64M即0x4000000
arm_add_memory会把mem信息存入meminfo(下一篇文章看到这个定义)
下面四个和内存有关,下一篇微博进行分析
sanity_check_meminfo();
arm_memblock_init(&meminfo, mdesc);
paging_init(mdesc);
request_standard_resources(mdesc);
下面两个我的平台不涉及,不说了
unflatten_device_tree();
#ifdef CONFIG_SMP
if (is_smp())
smp_init_cpus();
#endif
reserve_crashkernel();
用于内核崩溃时的保留内核此功能通过内核command line参数中的"crashkernel="保留下内存用于主内核崩溃时获取内核信息的导出。格式crashkernel=size[KMG][@offset[KMG]]
Kdump会用到,这位同志的linux启动有这个,你们看看
http://hi.baidu.com/sunboy_2050/item/6404d9ff3a3003e61a111fcb
tcm_init();
参考
http://blog.csdn.net/sergeycao/article/details/6030226
#ifdef CONFIG_MULTI_IRQ_HANDLER
Kconfig解释
config MULTI_IRQ_HANDLER
bool
help
Allow eachmachine to specify it's own IRQ handler at run time.
handle_arch_irq = mdesc->handle_irq; 我的平台没有
#endif
选择Console类型
#ifdef CONFIG_VT
#if defined(CONFIG_VGA_CONSOLE)
conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
conswitchp = &dummy_con;
#endif
#endif
early_trap_init();
对中断向量表进行早期初始化
void __init early_trap_init(void)
{
#if defined(CONFIG_CPU_USE_DOMAINS)
unsigned long vectors = CONFIG_VECTORS_BASE;
#define CONFIG_VECTORS_BASE 0xffff0000 arm中断向量表位置
#else
unsigned long vectors = (unsigned long)vectors_page;
#endif
先看看我的平台
System.map
c02fbea0 T __kuser_helper_start
c02fbee0 t __kuser_memory_barrier
c02fbf00 t __kuser_cmpxchg
c02fbf20 t __kuser_get_tls
c02fbf3c t __kuser_helper_version
c02fbf40 T __kuser_helper_end
c02fbf40 T __stubs_start
c02fbf40 t vector_irq
c02fbfc0 t vector_dabt
c02fc040 t vector_pabt
c02fc0c0 t vector_und
c02fc140 t vector_fiq
c02fc144 t vector_addrexcptn
c02fc164 T __stubs_end
c02fc164 T __vectors_start
c02fc184 T __vectors_end
extern char__stubs_start[], __stubs_end[];
extern char__vectors_start[], __vectors_end[];
extern char__kuser_helper_start[], __kuser_helper_end[];
上面定义在arch/arm/kernel/entry-armv.S
int kuser_sz =__kuser_helper_end - __kuser_helper_start;
/*
* Copy thevectors, stubs and kuser helpers (in entry-armv.S)
* into the vectorpage, mapped at 0xffff0000, and ensure these
* are visible tothe instruction stream.
*/
下面就是把向量表和kuser重映射到0xffff0000地址
memcpy((void*)vectors, __vectors_start, __vectors_end - __vectors_start);
memcpy((void*)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
memcpy((void*)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);
/*
* Do processorspecific fixups for the kuser helpers
*/
映射处理特殊的kuser帮助
地址在vectors + 0xfe0,如果有tls_emu或has_tls_reg
kuser_get_tls_init(vectors);
/*
* Copy signalreturn handlers into the vector page, and
* set sigreturnto be a pointer to these.
*/
sigreturn_codes是返回值数组
memcpy((void *)(vectors + KERN_SIGRETURN_CODE- CONFIG_VECTORS_BASE),
sigreturn_codes, sizeof(sigreturn_codes));
const unsigned long syscall_restart_code[2] = {
SWI_SYS_RESTART, /*swi __NR_restart_syscall */
0xe49df004, /* ldr pc, [sp], #4 */
};
直接把2进制代码填入,这个数组的都是命令。SWI,即software interrupt软件中断,__NR_restart_syscall是软件中断号。ldr pc, [sp], #4 可能是lr装入pc返回,我没有具体看
memcpy((void*)(vectors + KERN_RESTART_CODE - CONFIG_VECTORS_BASE),
syscall_restart_code,sizeof(syscall_restart_code));
flush_icache_range(vectors, vectors + PAGE_SIZE);
最终就是__glue(_CACHE, vectors + PAGE_SIZE)
#ifdef __STDC__
#define ____glue(name,fn) name##fn
#else
#define ____glue(name,fn) name/**/fn
#endif
#define __glue(name,fn) ____glue(name,fn)
就是组成一个标记
modify_domain(DOMAIN_USER, DOMAIN_CLIENT);这个最终会用cp15协处理器去设置domain
}
if (mdesc->init_early)需要早期的初始化,我的没有,你只要知道这个在处理early参数之后。
mdesc->init_early();
}
static struct machine_desc * __initsetup_machine_tags(unsigned int nr)
{
struct tag *tags = (struct tag *)&init_tags;同一目录下有定义init_tags
struct machine_desc *mdesc = NULL, *p;
char *from = default_command_line;这个就是内核配置的cmdline,在内核配置选项
通过bootloader传递过来的设备ID来匹配一个 struct machine_desc结构体,搞过移植应该知道,uboot和kernel的设备ID是要一样的,还记得uboot如何传的吧,r1 =machid,
有个这个结构体表示structmachine_desc {
在大家的平台是arch/arm/mach-*/mach-*.c下,我的
MACHINE_START(MINI2440, "MINI2440") //id, name
/* Maintainer:Michel Pollet <buserror@gmail.com> */
.atag_offset = 0x100,//标签列表相对地址,还记得我的是0x30000100吧,所以是0x100
.map_io = mini2440_map_io,//io映射函数
.init_machine = mini2440_init,//板子初始化函数
.init_irq = s3c24xx_init_irq,//中断初始化函数
.timer = &s3c24xx_timer,//系统tick定时器
MACHINE_END
init_tags.mem.start = PHYS_OFFSET;内存开始的物理地址
/*
* locate machine in the list of supported machines.
*/
for_each_machine_desc(p)
if (nr ==p->nr) {
printk("Machine: %s\n", p->name);
我的打印:
Machine: MINI2440
mdesc = p;
break;
}
if (!mdesc) {
early_print("\nError:unrecognized/unsupported machine ID"
"(r1 = 0x%08x).\n\n", nr);
如果你看到这个打印应该知道是设备id不匹配
dump_machine_table(); /* does not return */死循环
}
if (__atags_pointer)
tags =phys_to_virt(__atags_pointer);
// #define __virt_to_phys(x) ((x) -PAGE_OFFSET + PHYS_OFFSET)
//#define __phys_to_virt(x) ((x) - PHYS_OFFSET+ PAGE_OFFSET)
else if (mdesc->atag_offset)上面看到是0x100,如果bootloader传的对就不用他了
tags = (void *)(PAGE_OFFSET +mdesc->atag_offset);
//wxl添加
printk(KERN_NOTICE “tags virt addr = %lxPAGE_OFFSET = %lx PHYS_OFFSET = %lx \n”, (unsigned long)tags, (unsigned long)PAGE_OFFSET,(unsigned long)PHYS_OFFSET);
打印为
tags virt addr = c0000100 PAGE_OFFSET = c0000000 PHYS_OFFSET =30000000
#if defined(CONFIG_DEPRECATED_PARAM_STRUCT)//参数检测
/*
* If we have the old style parameters, convert them to
* a tag list.
*/
if (tags->hdr.tag != ATAG_CORE)还记得第一个要是ATAG_CORE吧
convert_to_tag_list(tags);不用看代码,就知道是转换为现在的格式
#endif
if (tags->hdr.tag != ATAG_CORE) {如何不可用参数,看处理
#if defined(CONFIG_OF)
/*
* If CONFIG_OF is set, thenassume this is a reasonably
* modern system that shouldpass boot parameters
*/
early_print("Warning:Neither atags nor dtb found\n");
#endif
tags = (struct tag*)&init_tags;用内核默认的,所以从这可以看到,不可用启动参数不会引起无法启动
}
if (mdesc->fixup)
mdesc->fixup(tags,&from, &meminfo);对应你平台提供的函数,你可以为你的平台处理tagged list、cmdline和meminfo数据,我的没有。
if (tags->hdr.tag == ATAG_CORE) {
//wxl 添加
printk(KERN_NOTICE “meminfo.nr_banks = %d\n”, meminfo.nr_banks)
打印结果
meminfo.nr_banks = 0
if (meminfo.nr_banks != 0)/如果mem已经初始化,就不要tags里的mem了
squash_mem_tags(tags);
save_atags(tags);保存到atags_copy
parse_tags(tags);解析
static void __init parse_tags(const struct tag *t)
{
for (;t->hdr.size; t = tag_next(t))
if(!parse_tag(t))
printk(KERN_WARNING
"Ignoring unrecognised tag 0x%08x\n",
t->hdr.tag);
}
static int __init parse_tag(const struct tag *tag)
{
extern struct tagtable __tagtable_begin, __tagtable_end;
struct tagtable*t;
这个处理和对command_line处理很像,
System.map里面
c0312780 T __tagtable_begin
c0312780 t __tagtable_parse_tag_cmdline
c0312788 t __tagtable_parse_tag_revision
c0312790 t __tagtable_parse_tag_serialnr
c0312798 t __tagtable_parse_tag_ramdisk
c03127a0 t __tagtable_parse_tag_videotext
c03127a8 t __tagtable_parse_tag_mem32
c03127b0 t __tagtable_parse_tag_core
c03127b8 t __tagtable_parse_tag_initrd2
c03127c0 t __tagtable_parse_tag_initrd
c03127c8 T __pv_table_begin
c03127c8 T __tagtable_end
例如__tagtable_parse_tag_mem32,你在内核只能找到parse_tag_mem32函数
__tagtable(ATAG_MEM, parse_tag_mem32);
#define __tag __used __attribute__((__section__(".taglist.init")))
#define __tagtable(tag, fn) \
static struct tagtable __tagtable_##fn__tag = { tag, fn }
__tagtable_parse_tag_mem32就是用##连接得到的。
for (t =&__tagtable_begin; t < &__tagtable_end; t++)
{
//wxl add
printk(KERN_NOTICE “tag->hdr.tag = %lxt->tag = %lx\n”, (unsigned ling)tag->hdr.tag, (unsigned ling)t->tag);
tag->hdr.tag = 54410001 t->tag = 54410009
tag->hdr.tag = 54410001 t->tag = 54410007
tag->hdr.tag = 54410001 t->tag = 54410006
tag->hdr.tag = 54410001 t->tag = 54410004
tag->hdr.tag = 54410001 t->tag = 54410003
tag->hdr.tag = 54410001 t->tag = 54410002
tag->hdr.tag = 54410001 t->tag = 54410001 ATAG_CORE
tag->hdr.tag = 54410002 t->tag = 54410009
tag->hdr.tag = 54410002 t->tag = 54410007
tag->hdr.tag = 54410002 t->tag = 54410006
tag->hdr.tag = 54410002 t->tag = 54410004
tag->hdr.tag = 54410002 t->tag = 54410003
tag->hdr.tag = 54410002 t->tag = 54410002 ATAG_MEM
tag->hdr.tag = 54410009 t->tag = 54410009 ATAG_CMDLINE
从这个打印看会先执行mem在执行cmdline
__tagtable(ATAG_CMDLINE, parse_tag_cmdline);
执行cmdline就是把cmdline保存在default_command_line,处理command_line会用到
if(tag->hdr.tag == t->tag) {当tag==MEM时,执行parse_tag_mem32
t->parse(tag);
我们只看parse_tag_mem32
static int __init parse_tag_mem32(const struct tag *tag)
{
//wxl add
printk(KERN_NOTICE"tag->u.mem.start = %lx tag->u.mem.size = %lx\n", (unsignedlong)tag->u.mem.start, (unsigned long)tag->u.mem.size);
打印如下
tag->u.mem.start =30000000 tag->u.mem.size = 4000000
我的bootargs mem=64M和这个一样,但是如果我mem=32M,那内存会设为32M,因为在下面cammand_line处理还要重新调用arm_add_memory,还会把meminfo.nr_banks = 0
return arm_add_memory(tag->u.mem.start, tag->u.mem.size);
}
和command_line中的”mem=”处理一样
break;
}
}
return t <&__tagtable_end;
}
}
/* parse_early_param needs a boot_command_line */
strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);
return mdesc;
}