下周准备做linux启动的技术讲座,在这里我慢慢整理下自己的材料,这次我写的是Image的启动过程,也即使zImage解压缩结束后的启动代码,这时候的代码开始地址仍然是0x30008000,下面我结合代码来讲吧:
Image的启动代码是在/arch/arm/kernel/head.S中的:
/*
* linux/arch/arm/kernel/head.S
* Kernel startup code for all 32-bit CPUs
*/
/* 内核启动入口点
* Kernel startup entry point.
* 这里通常在解压后直接调用。
* 处理器基本状态要求:
* MMU关闭,D-cach关闭,I-cache不用关系;
* r0 = 0,r1 = 系统号(machine number)
* 这段代码几乎是位置无关的。
* 如果链接内核在0xc0008000,调用的地址为相应的物理地址__pa(0xc0008000)。
* r1的系统号参考arch/arm/tools/mach-types文件的列表。
* 尽量不要在这里添加系统号相关的代码,那应该放在bootloader的代码中。
* 保持这里的代码的整洁。
*/
__INIT
.type stext, %function
/*――――――――――――――――――――――――――――――――――――――――――――――
这个地方就是 kernel 的入口点
―――――――――――――――――――――――――――――――――――――――――――――― */
ENTRY(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | MODE_SVC @ ensure svc mode
@ and irqs disabled
/*――――――――――――――――――――――――――――――――――――――――――――――
调用 __lookup_processor_type 检查现在运行的 cpu 的 ID 值和 linux 编译支持的
id 值是否相等。
――――――――――――――――――――――――――――――――――――――――――――――*/
bl __lookup_processor_type @ r5=procinfo r9=cpuid
/*――――――――――――――――――――――――――――――――――――――――――――――
从该函数返回后,寄存器内容如下:
R9 = cpu ID
R5 = pointer to processor structure
详细的内容请看__lookup_processor_type 的分析 */
.type __lookup_processor_type, %function
__lookup_processor_type:
/*――――――――――――――――――――――――――――――――――――――――――――――
把标号 2 的地址送给 r3, 3f = lable 3 forward
―――――――――――――――――――――――――――――――――――――――――――――― */
adr r3, 3f
/*――――――――――――――――――――――――――――――――――――――――――――――
把 r3 指向内存的地址的内容赋值给 r5,r6,r9
所以,参照标号 3处的声明,我们可以知道:
__proc_info_begin r5
__proc_info_end r6
3b r9
__proc_info_end 和 __proc_info_begin 这两个标号都是在
/linux/arch/arm/vmlinux.ld 这个脚本中定义的。在连接的时候,ld 会把相应cpu信息
proc_info 放到这两个标号之间。
__proc_info_begin = .;
*(.proc.info)
__proc_info_end = .;
――――――――――――――――――――――――――――――――――――――――――――――*/
ldmda r3, {r5, r6, r9} @ ldmda弹栈顺序是从右到左,[r3]->r9,[r3-4]->r6,[r3-8]->r5
/*r3 = 标号 3 的加载地址地址,r9 = 标号 3 的连接地址 ,r3是根据pc值确定的,r9是链接阶段就确定的是链接地址*/
sub r3, r3, r9 @ get offset between virt&phys
// r3 = 加载地址和连接地址的差值
//现在,r5 = __proc_info_begin 的加载地址,即在 RAM 中的地址
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
mrc p15, 0, r9, c0, c0 @ get processor id协处理器指令获取cpu id号r9=0x41807202(sep4020)
/*――――――――――――――――――――――――――――――――――――――――――――――
在本例中, r5 = _arm720_proc_info 这 个 标记定义在
linux/arch/arm/mm/proc-arm720.S
__arm720_proc_info:
.long 0x41807200 r3 = cpu_value
.long 0xffffff00 r4 = cpu_mask
.long 0x00000c1e mmuflags,一级段描述符
b __arm720_setup
.
.
―――――――――――――――――――――――――――――――――――――――――――――― */
//ldmia弹栈顺序是从左到右,[r5]->r3,[r5+4]->r4 ,即低地址的内容放到低编号的寄存器,高地址的内容放到高编号的寄存器,指令结束后r5的指依然为_arm720_proc_info
1: ldmia r5, {r3, r4} @ value, mask
and r4, r4, r9 @ mask wanted bits
//将r9屏上0xffffff00看sep4020是否是arm720t的内核
teq r3, r4
beq 2f @若是arm720t内核则直接跳转到标签2
/*――――――――――――――――――――――――――――――――――――――――――――――
proc_info_list 定义在 linux/include/asm-arm/procinfo.h
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsigned long __cpu_mmu_flags; // used by head.S
unsigned long __cpu_flush; // used by head.S
const char *arch_name;
const char *elf_name;
unsigned int elf_hwcap;
const char *cpu_name;
struct processor *proc;
struct cpu_tlb_fns *tlb;
struct cpu_user_fns *user;
struct cpu_cache_fns *cache;
};
每一项都是 4 个字节,所以 sizeof(proc_info_list) = 48 byte
――――――――――――――――――――――――――――――――――――――――――――――*/
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)=48
cmp r5, r6
blo 1b
mov r5, #0 @ unknown processor
2: mov pc, lr
/*
* This provides a C-API version of the above function.
*/
ENTRY(lookup_processor_type)
stmfd sp!, {r4 - r6, r9, lr}
bl __lookup_processor_type
mov r0, r5
ldmfd sp!, {r4 - r6, r9, pc}
/*
* Look in include/asm-arm/procinfo.h and arch/arm/kernel/arch.[ch] for
* more information about the __proc_info and __arch_info structures.
*/
.long __proc_info_begin
.long __proc_info_end
3: .long .
.long __arch_info_begin
.long __arch_info_end
/*―――――――――――――从__lookup_processor_type返回――――――――――――――――――――――――――――――――― */
movs r10, r5 @ 是有效720t核吗 (r5=0)?
beq __error_p @ yes, error 'p'
/*――――――――――――――――――――――――――――――――――――――――――――――
__lookup_machine_type 通过 R1 寄存器,判断体系类型,R1 = machine
architecture number
―――――――――――――――――――――――――――――――――――――――――――――― */
bl __lookup_machine_type @ r5=machinfo
/*―――――――――――――――――――――――――――――――――――――――――――――― */
/* r1 = machine architecture number
* Returns:
* r3, r4, r6 corrupted
* r5 = mach_info pointer in physical address space
*/
.type __lookup_machine_type, %function
__lookup_machine_type:
adr r3, 3b
/*――――――――――――――――――――――――――――――――――――――――――――――
把 r3 指向内存的地址的内容赋值给 r4,r5,r6
所以,参照标号 3处的声明,我们可以知道:
3b r4
__arch_info_begin r5
__arch_info_end r6
__arch_info_end 和 __arch_info_begin 这两个标号都是在
/linux/arch/arm/vmlinux.ld 这个脚本中定义的。在连接的时候,ld 会把相应体系架构
arch_info 放到这两个标号之间。
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
――――――――――――――――――――――――――――――――――――――――――――――*/
ldmia r3, {r4, r5, r6}
sub r3, r3, r4 @ get offset between virt&phys
//r5=__arch_info_begin的加载地址
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
/*――――――――――――――――――――――――――――――――――――――――――――――
__arch_info_begin 和__arch_info_end 的类型都是 struct machine_desc。其实
就是指向一个 machine_desc 结构首尾的两个地址标号。
struct machine_desc 定义在 linux/include/asm-arm/mach/arch.h 中
struct machine_desc {
/*
* Note! The first four elements are used
* by assembler code in head.S
*/
unsigned int nr; /* architecture number */
unsigned int __deprecated 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 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);
};
而对于我们的SEP4020其真正的定义是在/arch/arm/mach-sep4020/4020.c中
MACHINE_START(GFD4020, "4020 board")
.phys_io = 0x10000000,
.io_pg_offst = ((0xe0000000) >> 18) & 0xfffc,
.boot_params = 0x30000100,
.fixup = fixup_gfd4020,
.map_io = sep4020_map_io,
.init_irq = sep4020_init_irq,
.init_machine = sep4020_init,
.timer = &sep4020_timer,
MACHINE_END
看到这里,我们就不难明白下边这条指令了,struct machine_desc 中第一个就是
nr,即 architecture number
r3 = MACH_TYPE_GFD4020
――――――――――――――――――――――――――――――――――――――――――――――*/
1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type ,MACHINFO_TYPE = 0
//r1是由解压缩程序/arch/arm/boot/compressed/head.S最后传过来的,或者是uboot传过来的体系结构号
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
/*―――――――――――――――――从__lookup_machine_type返回――――――――――――――――――――――――――――― */
movs r8, r5 @ invalid machine (r5=0)?是不是我们的SEP4020系统结构
beq __error_a @ yes, error 'a'
/*――――――――――――――――――――――――――――――――――――――――――――――
设置 mmu 之前,设置临时内核页表
―――――――――――――――――――――――――――――――――――――――――――――― */
bl __create_page_tables
/*――――――――――――――――――――――――――――――――――――――――――――――
/* 我们在这里只映射内核启动的临时页表
* Setup the initial page tables. We only setup the barest
* amount which are required to get the kernel running, which
* generally means mapping in the kernel code.
*
* r8 = machinfo 体系结构信息
* r9 = cpuid cpu 的ID
* r10 = procinfo cpu信息
*
* Returns:
* r0, r3, r6, r7 corrupted
* r4 = physical page table address
*/
.type __create_page_tables, %function
__create_page_tables:
/*―――――――――――――――――――――――――――――――――――――――――――――― */
// Page offset: 3GB 内核页表的偏移在/inculde/asm/memory.h
#define PAGE_OFFSET UL(0xc0000000)
#ifndef __virt_to_phys
#define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET)
#define __phys_to_virt(x) ((x) - PHYS_OFFSET + PAGE_OFFSET)
#endif
而这其中的PHYS_OFFSET则是我们需要在我们的SEP4020的定义自己的主存ram的基址的物理地址,我们是在/include/asm-arm/arch-sep4020/memory.h中定义的
#define PHYS_OFFSET UL(0x30000000)
/*TEXT_OFFSET 是在在arch/arm/Makefile第140行,有
TEXT_OFFSET := $(textofs-y)
第90行有
textofs-y := 0x00008000
所以TEXT_OFFSET := 0x00008000
在153行有export TEXT_OFFSET将此变量输出。*/
#define KERNEL_RAM_ADDR (PAGE_OFFSET + TEXT_OFFSET) @其中TEXT_OFFSET = 0x8000
//swapper_pg_dir是放启动时的临时页表的页表基址(虚地址)
.globl swapper_pg_dir
.equ swapper_pg_dir, KERNEL_RAM_ADDR - 0x4000
//这个宏就是根据内核ram首址(虚拟地址)计算出我们内核页表的页表基址(物理地址)
.macro pgtbl, rd
ldr \rd, =(__virt_to_phys(KERNEL_RAM_ADDR - 0x4000))
.endm
―――――――――――――――――――――――――――――――――――――――――――――― */
pgtbl r4 @ page table address
//这样r4 = 内核页表的页表基址(物理地址)
/*
* Clear the 16K level 1 swapper page table
*/
mov r0, r4
mov r3, #0
//r6 = 内核的KERNEL_RAM_ADDR
add r6, r0, #0x4000
//首先对16k的一级页表内容清0
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
//PROCINFO_MMUFLAGS = 8;这样 r7 = 0x00000c1e mmuflags,一级段描述符 ,在proc-arm720.S 中定义
ldr r7, [r10, #PROCINFO_MMUFLAGS] @ mmuflags
/*
* Create identity mapping for first MB of kernel to
* cater for the MMU enable. This identity mapping
* will be removed by paging_init(). We use our current program
* counter to determine corresponding section base address.
这次一致映射主要是映射kernel前1MB的地址,这个映射最终会被后面paging_init()更新页表
*/
//获取当前程序的段地址 = r6
mov r6, pc, lsr #20 @ start of kernel section
//将段地址 或上mmuflags,然后赋值给r3
orr r3, r7, r6, lsl #20 @ flags + kernel base
//开始放内核代码的段映射描述符,段表基址为r4,段表内的索引为r6<<2;
str r3, [r4, r6, lsl #2] @ identity mapping
//这样做是为了解决后面刚开MMU是防止pc值飞掉了,在__turn_mmu_on函数中
/*
* Now setup the pagetables for our kernel direct
* mapped region. We round TEXTADDR down to the
* nearest megabyte boundary. It is assumed that
* the kernel fits within 4 contigous 1MB sections.
将kernel往后的4MB的地址建立虚实映射
*/
//#define TEXTADDR KERNEL_RAM_ADDR即等于0xc0008000
//这里的TEXTADDR是0xc0008000段对应的代码和前面的pc对应的段地址是同一代码,这样做是为了解决后面刚开MMU是防止pc值飞掉了
add r0, r4, #(TEXTADDR & 0xff000000) >> 18 @ start of kernel
//把start of kernel(0xc0008000)的虚拟地址映射起来,即将它的段描述符保存到段表的相应索引处位置
str r3, [r0, #(TEXTADDR & 0x00f00000) >> 18]!
//建立start of kernel+1MB的虚拟地址映射
add r3, r3, #1 << 20
str r3, [r0, #4]! @ KERNEL + 1MB
//建立start of kernel+2MB的虚拟地址映射
add r3, r3, #1 << 20
str r3, [r0, #4]! @ KERNEL + 2MB
//建立start of kernel+3MB的虚拟地址映射
add r3, r3, #1 << 20
str r3, [r0, #4] @ KERNEL + 3MB
/*
* Then map first 1MB of ram in case it contains our boot params.
建立PAGE_OFFSET=0XC0000000地址的映射,因为这个地址附近包含我们的uboot传给linux的启动参数
*/
add r0, r4, #PAGE_OFFSET >> 18
orr r6, r7, #PHYS_OFFSET
str r6, [r0]
mov pc, lr
―――――――――――――从__create_page_tables返回――――――――――――――――――――――――――――――――― */
/*
* The following calls CPU specific code in a position independent
* manner. See arch/arm/mm/proc-*.S for details. r10 = base of
* xxx_proc_info structure selected by __lookup_machine_type
* above. On return, the CPU will be ready for the MMU to be
* turned on, and r0 will hold the CPU control register value.
*/
//__switch_data是一个标签(即是一个地址),这个即是r13 = __mmap_switched(函数指针)
ldr r13, __switch_data @ address to jump to after
@ mmu has been enabled
//将__enable_mmu(这是个与地址无关代码)赋值给lr,等会返回执行,模拟一个函数栈
adr lr, __enable_mmu @ return (PIC) address
/*********************************************************************************************************
//#define PROCINFO_INITFUNC 12,在这里跳转执行arm720t架构的相应初始化代码
__arm720_proc_info:
.long 0x41807200 @ cpu_val
.long 0xffffff00 @ cpu_mask
.long PMD_TYPE_SECT | \
PMD_SECT_BUFFERABLE | \
PMD_SECT_CACHEABLE | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
b __arm720_setup @ cpu_flush
****************************************************************************************************/
add pc, r10, #PROCINFO_INITFUNC
/*********************************************************************************************************/
//转到__arm720_setup函数来执行
.type __arm720_setup, #function
__arm720_setup:
mov r0, #0
//写CP15的c7寄存器使cache的数据无效
mcr p15, 0, r0, c7, c7, 0 @ invalidate caches
//使整个TLB内部的地址变换条目无效
mcr p15, 0, r0, c8, c7, 0 @ flush TLB (v4)
//把CP15的寄存器c1传给r0
mrc p15, 0, r0, c1, c0 @ get control register
/*********************************************************************************************************
arm720_cr1_clear,arm720_cr1_set宏的定义
.type arm710_cr1_clear, #object
.type arm710_cr1_set, #object
arm710_cr1_clear:
.word 0x0f3f
arm710_cr1_set:
.word 0x013d(mmu使能,禁用地址对齐,cache使能,写缓冲使能,小印地安序,系统保护,rom不保护)
****************************************************************************************************/
ldr r5, arm720_cr1_clear
bic r0, r0, r5
ldr r5, arm720_cr1_set
//r0 = 0x013d(mmu使能,禁用地址对齐,cache使能,写缓冲使能,小印地安序,系统保护,rom不保护)
orr r0, r0, r5
//跳转到__enable_mmu,相当于执行函数__enable_mmu
mov pc, lr @ __ret (head.S)
.size __arm720_setup, . - __arm720_setup
/****************************************从__arm720_setup返回************************************************************/
/*********************************************************************************************************
/*
* Setup common bits before finally enabling the MMU. Essentially
* this is just loading the page table pointer and domain access
* registers.
*/
.type __enable_mmu, %function
__enable_mmu:
//#define CONFIG_ALIGNMENT_TRAP 1是在/include/linux/Autoconfig.h定义
//Autoconfig.h是在make时根据Kconfig文件产生的
#ifdef CONFIG_ALIGNMENT_TRAP
orr r0, r0, #CR_A @CR_A=1 地址对齐
#else
bic r0, r0, #CR_A
#endif
#ifdef CONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #CR_C @CR_C= 2 D cache使能
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z @CR_Z = 11 分支预测
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE
bic r0, r0, #CR_I @CR_I = 12 I cache使能
#endif
//#define domain_val(dom,type) ((type) << (2*(dom)))在/include/asm-arm/Domain.h定义
//MMU中的域是一些段,大页或小页的集合,而在MMU的页表描述符中有些位表示该描述符的域,指明该存储空间所属的域号0~15
mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
domain_val(DOMAIN_IO, DOMAIN_CLIENT))
//配置cp15的域访问控制寄存器
mcr p15, 0, r5, c3, c0, 0 @ load domain access register
//配置cp15的页表基址寄存器c2,r4就是页表的首地址
mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
b __turn_mmu_on
/*
* Enable the MMU. This completely changes the structure of the visible
* memory space. You will not be able to trace execution through this.
* If you have an enquiry about this, *please* check the linux-arm-kernel
* mailing list archives BEFORE sending another post to the list.
*
* r0 = cp #15 control register
* r13 = *virtual* address to jump to upon completion
* r13是开MMU后将执行的第一个函数
* other registers depend on the function called upon completion
开启MMU,这时你看到的世界将彻底是虚拟世界了,
*/
.align 5
.type __turn_mmu_on, %function
__turn_mmu_on:
mov r0, r0
//配置cp15的c1,开启MMU
mcr p15, 0, r0, c1, c0, 0 @ write control reg
//因为 ARM 720T 是三级流水线,所以运行三条指令,让流水线充满指令
//读取id寄存器
mrc p15, 0, r3, c0, c0, 0 @ read id reg
//这时候刚开MMU的虚拟地址和实地址是一一映射,pc就不需要跳转了
mov r3, r3
mov r3, r3
//跳转到__mmap_switched函数执行
mov pc, r13
/****************************************从__enable_mmu返回************************************************************/
/******************************************__mmap_switched***************************************************************
/*
* The following fragment of code is executed with the MMU on, and uses
* absolute addresses; this is not position independent.
*这段代码是在开启MMU后执行的,用的是绝对地址,不是地址无关的代码
* r0 = cp#15 control register
* r1 = machine ID 是Uboot传过来的体系架构号
* r9 = processor ID CPU的id号
*/
.type __mmap_switched, %function
__mmap_switched:
//这是把__data_loc对应的地址赋值给r3
adr r3, __switch_data + 4
/*********************************************************************************************************
__switch_data这个数据块就在其后定义的
.type __switch_data, %object
__switch_data:
.long __mmap_switched
.long __data_loc @ r4,__data_loc ,__data_start,__bss_start都是在vmlinux.lds中定义的
.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
*********************************************************************************************************/
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ Copy data segment if needed
//把动态数据拷贝到全局数据区
1: cmpne r5, r6
ldrne fp, [r4], #4 @fp是帧指针,即r11
strne fp, [r5], #4
bne 1b
//把BSS区清零
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4 @cc是无符号小于
bcc 1b
ldmia r3, {r4, r5, r6, sp}
//将cpu id保存到r4中
str r9, [r4] @ Save processor ID
//将机器号保存到r5当中
str r1, [r5] @ Save machine type
/*********************************************************************************************************
cr_alignment是为中断向量表是放在高地址还是放在低地址服务的,在中断分析中会见到的
.globl cr_alignment
.globl cr_no_alignment
cr_alignment:
.space 4
cr_no_alignment:
.space 4
//将MMU control寄存器分别保存到cr_alignment,和cr_no_alignment中
*********************************************************************************************************/
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r6, {r0, r4} @ Save control register values
//进入伟大的start_kernel函数
b start_kernel
/****************************************从__enable_mmu返回************************************************************/
.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