Linux内核启动代码分析二之开发板相关驱动程序加载分析
1 从linux开始启动的函数start_kernel开始分析,该函数位于linux-2.6.22/init/main.c
start_kernel()
--2>setup_arch(&command_line);//该函数位于arch/arm/kernel/setup.c
//在这个函数中定义了一个描述开发板的属性的结构体struct machine_desc *mdesc
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_io; /* start of physical io */
unsigned int io_pg_offst; /* byte offset for io
* page tabe entry */
const char *name; /* architecture name */
unsigned long boot_params; /* tagged list */
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 tag *, char **,
struct meminfo *);
void (*map_io)(void);/* IO mapping function */
void (*init_irq)(void);
struct sys_timer *timer; /* system tick timer */
void (*init_machine)(void);
};
//这个结构体中包括机器ID,物理地址、IO地址偏移、IO映射函数、IRQ中断函数初始化
//开发板初始化函数:完成平台驱动程序初始化注册函数的调用
--3>mdesc = setup_machine(machine_arch_type);
//machine_arch_type为外部定义的一个全局变量,用来标识机器ID
--4>struct machine_desc *list = lookup_machine_type(machine_arch_type)
//用于搜索开发板机器ID,lookup_machine_type是一个汇编函数
位于linux-2.6.22/arch/arm/kernel/head-common.S
lookup_machine_type中对机器ID号码从r0寄存器复制到r1寄存器中,调用汇编函数:__lookup_machine_type
--5>__lookup_machine_type从.arch.info.init段中比较是否存在相同机器ID号码的机器描述结构体,
在使用MACHINE_START(_type,_name)宏定义一个开发板机器描述结构体时,会把这个结构体变量放到.arch.info.init段内
//宏定义和宏开如下所述:
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
MACHINE_START(CSB337, "Cogent CSB337")
/* Maintainer: Bill Gatliff */
.phys_io = AT91_BASE_SYS,
.io_pg_offst = (AT91_VA_BASE_SYS >> 18) & 0xfffc,
.boot_params = AT91_SDRAM_BASE + 0x100,
.timer = &at91rm9200_timer,
.map_io = csb337_map_io,
.init_irq = csb337_init_irq,
.init_machine = csb337_board_init,
MACHINE_END
//宏展开如下:
MACHINE_START(CSB337, "Cogent CSB337")
static const struct machine_desc __mach_desc_CSB337 __used __attribute__((__section__(".arch.info.init"))) = {
.nr = MACH_TYPE_CSB337,
.name = Cogent CSB337,
/* Maintainer: Bill Gatliff */
.phys_io = AT91_BASE_SYS,
.io_pg_offst = (AT91_VA_BASE_SYS >> 18) & 0xfffc,
.boot_params = AT91_SDRAM_BASE + 0x100,
.timer = &at91rm9200_timer,
.map_io = csb337_map_io,
.init_irq = csb337_init_irq,
.init_machine = csb337_board_init,
};
__lookup_machine_type函数:位于arch/arm/kernel/head-common.s
__lookup_machine_type:
adr r3, 3b
ldmia r3, {r4, r5, r6}
sub r3, r3, r4 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
teq r3, r1 @ matches loader number?
beq 2f @ found
add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc
cmp r5, r6
blo 1b
mov r5, #0 @ unknown machine
2: mov pc, lr
这个函数中的#MACHINFO_TYPE,不是太明白,搜索源码发现它出现在一个c文件的main函数中,被main函数调用
很不理解,按理说一条宏命令应该出现在一个h文件或函数外才对。google后的结果如下,没有进一步地观察跟踪
编译过程是不是这样,不过觉得还是挺有道理的。留待以后考证。
#define DEFINE(sym, val) \
asm volatile("\n->" #sym " %0 " #val : : "i" (val))
根据上面的宏定义,进行宏展开
DEFINE(SIZEOF_MACHINE_DESC, sizeof(struct machine_desc));
asm volatile("\n->" SIZEOF_MACHINE_DESC " %0 " sizeof(struct machine_desc) : : "i" (sizeof(struct machine_desc)))
google后的结果是:
asm volatile("\n->" #sym " %0 " #val : :"i" (val))估计是 #define sym val 的意思
也就是#define SIZEOF_MACHINE_DESC sizeof(struct machine_desc)
只不过这里是动态的定义。只需要传一个sym和val进来,就可以帮你完成#define sym val的功能
那个宏定义在asm-offsets.c中,这个.c文件根本就不是用来编译运行的,只是在编译内核的时候,
用它生成一个asm-offsets.s文件,然后使用一个脚本将这个asm-offsets.s再转换为asm-offsets.h。
这个头文件遵循汇编语法,用来被汇编文件include的。
DEFINE(MACHINFO_TYPE, offsetof(struct machine_desc, nr));
#define MACHINFO_TYPE offsetof(struct machine_desc, nr)
函数调用--4>如果lookup_machine_type(nr)查找到了目标开发板机器ID,返回一个指向该机器描述结构体的指针,否则返回0
而函数调用--3>setup_machine(machine_arch_type)中的实参machine_arch_type (也就是作为实参传入函数lookup_machine_type
函数的变量nr) 是在哪里定义的呢?
在arch/arm/tools下有三个文件:gen-mach-types、mach-types、Makefile
1 阅读gen-mach-types,发现这是一个shell脚本文件,看注释是:
generate include/asm-arm/mach-types.h用于产生mach-types.h的头文件
mach-types.h里的生成的是板子相关的宏定义
2 mach-types文件中是很多块板子的配置文件,所以当为内核添加一款新的开发板时,需要修改这个文件,
参考别的板子的定义方法,添加一项配置,其中配置的最后一项number就是机器ID号码
3 mach-types.h源码中是没有这个文件的,这个文件是编译过程中动态生成的。
# machine_is_xxx CONFIG_xxxx MACH_TYPE_xxx number
csb337 MACH_CSB337 CSB337 399
mach-types.h首先根据mach-types配置文件生成#define MACH_TYPE_XXX number的宏定义机器ID
如:#define MACH_TYPE_CSB337 399
然后成如所有配置项的其他部分,如:
#ifdef CONFIG_MACH_CSB337
# ifdef machine_arch_type
# undef machine_arch_type
# define machine_arch_type __machine_arch_type
# else
# define machine_arch_type MACH_TYPE_CSB337
# endif
# define machine_is_csb337() (machine_arch_type == MACH_TYPE_CSB337)
#else
# define machine_is_csb337() (0)
#endif
在这里我们看到了machine_arch_type的踪影。。。。
解析这段宏命令:如果定义了machine_arch_type,则结束machine_arch_type原来的宏定义,
然后把machine_arch_type重新定义成__machine_arch_type;如果没有定义过machine_arch_type,
那么就把machine_arch_type定义成mach-types中配置的机器ID号码。
Makefile中把mach-types.h动态包含到工程去。
4 如果定义了machine_arch_type宏的话,machine_arch_type又被重定义为__machine_arch_type;
__machine_arch_type又是在哪里定义的呢?
在源码中搜索__machine_arch_type,在arch/arm/boot/compressed/misc.c中发现了:
unsigned int __machine_arch_type;的定义,
并在该文件下的函数:
decompress_kernel()中对__machine_arch_type进行了初始化赋值为arch_id。
其实这个arch_id是由uboot等bootloader传入内核的一个参数。
5 追踪decompress_kernel函数
decompress_kernel是使解压内核的意义,该函数应该是在内核运行之前先运行的函数,用于把内核
从uImage镜像解压到ram中去,便于内核在内存中运行。(以上都是自己的猜测:根据这个函数中打印
出的字符和内核运行初输出到终端上的字符一致)
uImage在arch/arm/boot目录下生成,那么阅读该目录下的Makefile文件。
看不太懂什么意思,不过最后一句:subdir- := bootp compressed
那么继续看这两个子目录中的Makefile
bootp目录里都是些配置链接相关的代码,看不懂,查看linux主机发现,这个目录上没有生成*.o文件,暂时跳过。
compressed目录
这个目录下有个文件vmlinux.lds.in,编译时会生成一个vmlinux.lds
阅读这个链接脚本,好多看不太明白,不过链接脚本中.text段中放在最前面的是_start,那么可以在本目录下
寻找一个含有_start的汇编文件,发现head.s含有这个start标号,可以猜测内核最先就是从这个会变文件开始
执行下去的。分析下这个head.s汇编
5.1 保存uboot传入的参数,关闭中断进入管理模式
uboot跳入到linux内核时传入三个参数:(内核偏移地址:我自己猜的)0,机器ID,参数tags指针
分别把这三个参数保存到r0,r7,r8三个寄存器。
5.2 内核代码的重定位
adr r0, LC0//把LC0表的地址加载到r0中
ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}//把LC0表的数据分别加载到r1 r2 r3 r4 r5 r6 r12 r13中
subs r0, r0, r1
beq not_relocated //通过比较r0 r1也就是比较LC0表的加载地址和链接地址是否相同,相同就调用not_relocated
函数,就不用再进行代码重定位了,否则还需要对代码在内存进行搬移重定位到链接地址。
标号LC0是指在内存中的地址,而这个标号作为地址的内存单元中存储的LC0是指由链接脚本指定的内存地址数据
.type LC0, #object
LC0: .word LC0 @ r1 表LC0地址
.word __bss_start @ r2 BSS段开始地址:由链接脚本确定
.word _end @ r3 BSS段结束地址:由链接脚本确定
.word zreladdr @ r4 由arch/arm/mach-xx/Makefile.boot文件配置内核加载地址
.word _start @ r5 压缩内核的开始地址
.word _got_start @ r6 got段开始地址
.word _got_end @ ip got段结束地址
.word user_stack+4096 @ sp 栈指针sp
LC1: .word reloc_end - reloc_start
.size LC0, . - LC0
假如r0 与r1值不一样,需要进行重定位:
add r5, r5, r0 重定位内核地址
add r6, r6, r0 重定位got段开始地址
add ip, ip, r0 重定位got段结束地址
add r2, r2, r0
add r3, r3, r0
add sp, sp, r0
个人理解:LC0表中除LC0 zreladdr外都是相对LC0的相对位移地址,这样就能明白这6句指令了
重定位got表:
1: ldr r1, [r6, #0] @ relocate entries in the GOT
add r1, r1, r0 @ table. This fixes up the
str r1, [r6], #4 @ C references.
cmp r6, ip
blo 1b
5.3 完成重定位后
《A》 清bss段
mov r0, #0
1: str r0, [r2], #4 @ clear bss
str r0, [r2], #4
str r0, [r2], #4
str r0, [r2], #4
cmp r2, r3
blo 1b
《B》为解压缩内核设置堆栈空间
bl cache_on //打开cache
mov r1, sp @ malloc space above stack
add r2, sp, #0x10000 @ 64k max 在栈上方开辟64K堆空间,现在r2是堆的底地址。。
《C》调整内核空间与堆栈空间的地址,然后填充decompress_kernel的四个参数
/*检查堆栈空间是否和内核uImage所站的空间重合
* r4 = final kernel address 同makefile.boot配置的内核地址(2410配置为0x30008000)
* r5 = start of this image 内核镜像地址
* r2 = end of malloc space (and therefore this image)
* We basically want:
* r4 >= r2 -> OK
* r4 + image length <= r5 -> OK
*/
cmp r4, r2
bhs wont_overwrite
sub r3, sp, r5 @ > compressed kernel size
add r0, r4, r3, lsl #2 @ allow for 4x expansion
cmp r0, r5
bls wont_overwrite
//一般地r4不会比r2大,r0比r5小,个人猜测,这部分不是太明白什么意思
mov r5, r2 @ decompress after malloc space
mov r0, r5
mov r3, r7
bl decompress_kernel
。。。。。
wont_overwrite: mov r0, r4
mov r3, r7
bl decompress_kernel
b call_kernel
《D》跳入到解压缩后的内核里去运行linux操作系统
call_kernel: bl cache_clean_flush
bl cache_off
mov r0, #0 @ must be zero
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
mov pc, r4 @ call kernel
《E》运行Linux操作系统的前奏
阅读bootp.lds连接脚本,发现连接到真正内核可执行文件的最前面的是 _stext 段。
该段在arch/arm/kernel/head.S中
分析下面这段代码
ENTRY(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
@ and irqs disabled //进行管理模式 关中断
mrc p15, 0, r9, c0, c0 @ get processor id //通过协处理器命令读处理器ID号
bl __lookup_processor_type @ r5=procinfo r9=cpuid
//该函数位于arch/arm/kernel/head-common.s中
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
//没有找到r5为0
bl __lookup_machine_type @ r5=machinfo
//该函数位于arch/arm/kernel/head-common.s中
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
//没有找到r5为0
bl __create_page_tables
//创建页表
ldr r13, __switch_data @ address to jump to after
@ mmu has been enabled
adr lr, __enable_mmu @ return (PIC) address
//打开MMU
add pc, r10, #PROCINFO_INITFUNC
《F》运行Linux操作系统的前奏迷云
PROCINFO_INITFUNC 是__cpu_flush成员变量在结构体proc_info_list中的便宜量,
这可以从arch/arm/kernel/asm-offset.c中的一条语句如下得知。
DEFINE(PROCINFO_INITFUNC, offsetof(struct proc_info_list, __cpu_flush));
在arch/arm/kernel/vmlinux.lds.s中有
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
这三行把所有处理器信息结构组合在一块,就像一个结构数组。
这样查找时只要找到 __proc_infor_end 的地址,很快就能找到处理器信息结构数组。
对于机器信息也是一样
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
由Makefile可知对于arm920t系列的处理器proc-arm920.S被编译进内核
在arch/arm/mm/proc-arm920.S的448行有如下代码:
第一行伪汇编可能是把下面的这段数据存放到.proc.info.init代码段中
.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
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB
.long cpu_arm920_name
.long arm920_processor_functions
.long v4wbi_tlb_fns
.long v4wb_user_fns
#ifndef CONFIG_CPU_DCACHE_WRITETHROUGH
.long arm920_cache_fns
#else
.long v4wt_cache_fns
#endif
.size __arm920_proc_info, . - __arm920_proc_info
结合以上代码及add pc, r10, #PROCINFO_INITFUNC 指令可知
r10存储的是proc.info.init的中某一项的起始地址,而PROCINFO_INITFUNC是__cpu_flush
在结构体proc_info_list的偏移值,而这个值对应的正是__arm920_proc_info中的第五项
b __arm920_setup ,由此可知,接下来跳转到 __arm920_setup 处运行。
.type __arm920_setup, #function
__arm920_setup:
mov r0, #0
mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4
mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4
#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
。。。。。
.type arm920_crval, #object
arm920_crval:
crval clear=0x00003f3f, mmuset=0x00003135, ucset=0x00001130
这段代码先使I/D CACHE无效写buffer TLB无效,然后加载arm920_crval值,用来设置r0,
mov pc,lr会跳转到 arch/arm/kernel/head.s中的__enable_mmu 函数中去执行。
__enable_mmu
b __turn_mmu_on
mov pc, r13 //r13中的值是__switch_data在上面出现指令 ldr r13, __switch_data
在源码中搜索 grep '__switch_data' -nR ./*
搜索结果:./arch/arm/kernel/head-common.S:14: .type __switch_data, %object
.type __switch_data, %object
__switch_data:
.long __mmap_switched
.long __data_loc @ r4
.long __data_start @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long cr_alignment @ r6
.long init_thread_union + THREAD_START_SP @ sp
。。。。。。。
.type __mmap_switched, %function
__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, sp}
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r6, {r0, r4} @ Save control register values
b start_kernel
分析上述代码:
mov pc ,r13
r13中存储的是__switch_data目标内存区的首地址,这个地址上存储的是
函数 __mmap_switched 的入口地址。
这个函数完成了__data_loc数据段到__data_start数据段的数据复制、
__bss_start BSS数据段的清0、设置SP指针、保存处理器ID到全局变量processor_id中、
保存开发板机器架构类型到全局变量__machine_arch_type中、r0,r4压入栈、
最后调用start_kernel,这个函数在init/main.c中开始正式运行操作系统。
总结:
1 内核的启动过程:
a uboot传入参数
b 跳入到内核的起始地址;内核=head.o+misc.o+压缩内核镜像
c 保存uboot传入的参数,然后检查、重定位代码
d 打开cache,调整内核镜像地址、堆栈地址等为运行解压缩内核函数做准备
e 调用 decompress_kernel 函数
--> 调用平台相关的接口函数 arch_decomp_setup();
--> 调用 makecrc();
--> 调用 gunzip();
f 调用汇编函数b call_kernel,call_kernel中刷新cache,关cache,然后把
uboot传入的三个参数再传入到解压缩后的内核中。
g 通过指令 mov pc,r4,跳转到内核入口
r4中保存的是解压缩内核的入口地址,该地址为arch/arm/kernel/head.s中的
.type stext, %function ENTRY(stext)
-->读processor ID
调用汇编函数 __lookup_processor_type 检查内核是否支持该处理器
-->调用汇编函数 __lookup_machine_type 检查内核是否支持该目标板
-->调用汇编函数 __create_page_tables 创建页表
h 接下来调用arch/arm/mm/proc-arm920.S中的汇编函数 __arm920_setup
i 接下来返回调用arch/arm/kernel/head.s中的__enable_mmu
-->__turn_mmu_on
-->mov pc, r13即 ./arch/arm/kernel/head-common.S中的标号: __switch_data
-->再调用 __mmap_switched 完成数据段的重定位、BSS段清0、设置SP、保存全局变量
J 接下来就是b start_kernel 该函数位于init/main.c中
此次分析完毕,接开始部分。。
在分析start_kernel启动过程时,
-->setup_arch
这个函数中搜索支持的单板,然后对这三个全局变量进行赋值
init_arch_irq = mdesc->init_irq;
system_timer = mdesc->timer;
init_machine = mdesc->init_machine;
来完成对目标板的指针接口。
以后再来分析其他的驱动启动过程。。。。。。。
参考:
http://blog.csdn.net/lanmanck/article/details/4288048
http://blog.163.com/fj_ltls/blog/static/1380271112011610101118227/
http://blog.chinaunix.net/uid-20672257-id-2891129.html