Part3[call_kernel,start_kernel)
内核跳转到call_kernel,实际上是跳转到了./kernel/arch/arm/kernel/head.S中的ENTRY(stext)标签。
在看stext之前,先总结一下内核中的一些地址相关宏:
-
PHYS_OFFSET
PHYS_OFFSET是RAM的起始物理地址,是RAM在cpu地址总线上的地址(CPU地址总线上除了RAM之外还映射了许多IO端口,如GPT时钟的端口,UART的输出端口等。所谓的映射,是指在CPU地址总线上,通过一个物理地址,就可以访问到外设 如GPT/UART等的一个寄存器,这些寄存器可以成为IO的端口,这种通过物理上焊接/连接使得CPU的物理地址能对应道某个外设的寄存器的方式就是所谓的映射)。
PHYS_OFFSET表示的是RAM如何映射,其值是mach相关的,未必是0。一般都记录在平台相关的memory.h文件中,在goldfish和mt6582的定义如下://.kernel/arch\arm\mach-goldfish\include\mach\memory.h
#define PHYS_OFFSET UL(0x00000000)//.\platform\mt6582\kernel\core\include\mach\memory.h
#define PHYS_OFFSET 0x80000000 PAGE_OFFSET
PAGE_OFFSET的意义应该是内核的起始虚拟地址,也就是内核态与用户态的分界。
看网上的资料有说PAGE_OFFSET是内核线性映射部分物理地址和虚拟地址的差值,个人认为这个理解并不正确, 以mt6582为例,其RAM起始地址为0x80000000,映射到的虚拟地址为0xC0000000,但PAGE_OFFSET=0xC0000000,显然是不对的,或许这个词的原意就是物理地址和虚拟地址的差值,但现在实际上很多平台上都不是这么用的。所以个人认为PAGE_OFFSET理解为内核与用户态的分界更合适。
PAGE_OFFSET是通过CONFIG_PAGE_OFFSET来配置的。PV_OFFSET
内核中并没有这个词,但内核中经常会出现一个adr根据当前pc寻址,然后减去代码的编译地址,以获取一个offset,这个值的含义是内核线性映射那一段的一个物理地址和其虚拟地址的差值,为了方便使用,我将其叫做PV_OFFSET。
个人对PV_OFFSET的定义(纯属yy):
内核有一部分是需要线性映射的(_stext - __end_rodata),也就是说对于这一部分其物理地址和虚拟地址差一个固定的差值。在内核中,对于这一部分的地址,可以直接通过_pa/_va宏来进行物理地址和虚拟地址的转换,不需要通过页表。_pa/_va实际上就是通过加减这个差值实现的,这个差值就被定义为PV_OFFSET。
一般来说,RAM的起始位置(物理地址PHYS_OFFSET)都会线性映射到内核的起始虚拟地址(虚拟地址PAGE_FFSET),所以一般情况下PV_OFFSET = PHYS_OFFSET - PAGE_OFFSET,TEXT_OFFSET
内核代码在RAM中的起始地址相对于RAM起始地址的偏移/内核代码的虚拟起始地址相对于内核的虚拟起始地址的偏移(两个定义实际上是一样的),在mt6582上:
内核代码在RAM的起始地址为0x80008000,RAM的起始地址为0x80000000
内核代码的虚拟起始地址为0xC0008000, 内核的起始虚拟地址为0xC0000000
TEXT_OFFSET均为0x00008000,主要还是如前面所说,一般RAM的起始位置都会线性映射到内核的起始虚拟地址。
前面的是TEXT_OFFSET的计算方法,不是定义,这个值是在./arch/arm/Makefile文件中定义的。-
KERNEL_RAM_VADDR
这个值定义在arch/arm/kernel/head.S中#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
这个值就是代表内核代码的起始虚拟地址
-
KERNEL_RAM_PADDR
#define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET)
这个值是内核代码在RAM中的地址,这个值只能是mach平台相关的,因为各个平台的PHYS_OFFSET是不同的,这个值在mt6582的代码中看到了,可能这个定义本身的存在就不科学,这个目前还理解不上去。
swapper_pg_dir
//./arch/arm/kernel/head.S
.globl swapper_pg_dir
.equ swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE
swapper_pg_dir根据定义可以看出,其含义就是放在内核代码起始虚拟地址前面的页表。注意是它的位置,它就应该放在内核代码起始虚拟地址的前面!
ENTRY(stext):
/*
stext是vmlinux(大)的入口函数,也就是Image的0地址处的指令(在vmlinux通过objcopy->Image的过程中,实际上是把入口函数stext提取到了Image的0地址处。
在进入stext之前的系统状态:
##1. MMU = off
##2. D-Cache = off
##3. r0 = 0
##4. r1 = 结构ID
##5. r2 = atags
注意,
1. 此前系统应该是一直处于关中断状态,因为还没有设置中断向量表。
2. 在zImage中一开始会调用cache_on函数,其内部一般会开启mmu,做的是恒等映射,目的是为了可以使用缓存加速内核的解压。内核解压后,call kernel之前会调用cache_off关闭mmu,此时系统又处于mmu关闭的状态。
*/
ENTRY(stext)
//PSR_F_BIT|PSR_I_BIT是arm的两种中断,这里是禁止IRQ,FIQ中断,同时进入svc模式,默认应该就是svc模式,这里是确保一下.
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode
//从cp15中获取处理器id
mrc p15, 0, r9, c0, c0 @ get processor id
//查找与自身处理器一致的处理器信息,内核在编译的时候,会把当前内核支持的所有cpu信息编译到镜像的[__proc_info_begin,__proc_info_end)处,而这个begin/end地址在__lookup_processor_type_data变量中有记录,__lookup_processor_type 函数就是根据当前处理器id,来找到对应的处理器信息的struct proc_info_list结构的地址。如果找到,将指针保存到r5返回,否则r5返回空。
bl __lookup_processor_type
//此句会设置cpsr寄存器,如果r5为NULL,则直接__error_p了。
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
//r8 = PHYS_OFFSET
#ifndef CONFIG_XIP_KERNEL
//在mt6582中,XIP没有设置,所以是这里
//r3为2f的当前地址
adr r3, 2f
//r4 = 2f的编译地址, r8为PAGE_OFFSET
ldmia r3, {r4, r8}
//此时的r4 = 2f的当前地址与编译地址的偏移(PV_OFFSET).
sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET)
//此时的r8 = PHYS_OFFSET。
add r8, r8, r4 @ PHYS_OFFSET
#else
//PHYS_OFFSET 一般在mach-xxx/include/mach/memory.h文件中定义。
ldr r8, =PHYS_OFFSET @ always constant in this case
#endif
//根据r5中的procinfo,验证atag数据结构大小和魔数是否正确,不正确将r2清零返回。
bl __vet_atags
//SMP相关的,没看 ????????????????????
bl __fixup_smp
/*
1. 对__turn_mmu_on所在代码的2MB段做恒等映射,目的是为了在开启/关闭mmu的时候不会寻址出错。
2. 对整个内核镜像区域做线性映射,这个线性映射区域的大小是以编译地址(即虚拟地址)来表示的。
线性映射的虚拟地址范围是[KERNEL_RAM_VADDR, KERNEL_END),是编译时候确定的。
而物理地址是PC所在页对应KERNEL_RAM_VADDR所在页,然后依次映射,最终映射结果
可如下简单表示:
物理地址 映射 虚拟地址
(pc所在页基地址)----------- <==========> ---------------(KERNEL_RAM_VADDR所在页基地址)
| | <----------- KERNEL_RAM_VADDR
PC -------> | |
| |
----------- <==========> --------------- (下一页)
| |
| |
| | <----------- KERNEL_END
----------- <==========> --------------- (最后一页,KERNEL_END所在页)
段线性映射有一个默认的条件,就是认为pc所在的位置在内核的第一页,如果pc
不在内核的第一页,则内核启动时候所有线性映射都会错位,直接crash。
3. 内核启动参数atags所在的页做线性映射,atags的物理地址+ PV_OFFSET
被认为是atags的虚拟地址。
__create_page_tables按照映射关系填充了页表,并没有启动mmu。
*/
bl __create_page_tables
//r13(这里不是当sp用的)保存__mmap_switched的地址
ldr r13, =__mmap_switched
//在arm中,BSYM是个空宏,lr <- __enable_mmu 物理地址
adr lr, BSYM(1f) @ return (PIC) address
mov r8, r4 @ set TTBR1 to swapper_pg_dir
//r10存的是proc_info的地址,PROCINFO_INITFUNC是其中的函数__cpu_flush的偏移。
//这里是跳转到处理器相关的__cpu_flush函数去。对于v7处理器,这里这里最终调用的
//应该是__v7_setup函数。在这个函数中,会设置一些flags,然后跳转到lr
//即__enable_mmu函数,这里的流程是:__v7_setup -> __enable_mmu ->
//__turn_mmu_on -> __mmap_switched -> start_kernel
ARM(add pc, r10, #PROCINFO_INITFUNC )
//这里开启mmu,页表生效了
1: b __enable_mmu
ENDPROC(stext)
__lookup_processor_type
/*
内核要想正常运行,必须找到适配的处理器信息和机器信息。此函数确定内核是否支持当前cpu,如果支持,r5返回一个用于描述处理器结构体的地址,否则r5 = 0.
输入参数为r9 = cpuid,来自于cp15。cp15中只保存了了一个处理器id,处理器的其他相关信息,都是要写死在内核中的,运行时动态匹配到具体处理器。
*/
__lookup_processor_type:
//r3存放__lookup_processor_type_data数据结构的基地址,由于此时MMU是处于关闭状态的,获取到的r3就是此变量的物理地址。
adr r3, __lookup_processor_type_data
/*
__lookup_processor_type_data:
.long .
.long __proc_info_begin
.long __proc_info_end
##.size name, expression
.size __lookup_processor_type_data, . - __lookup_processor_type_data
把r3中的3个数据载入进来:
r4 = .
r5 = __proc_info_begin
r6 = __proc_info_end
*/
ldmia r3, {r4 - r6}
/*
vmlinux(大)是被编译成一个可执行文件的,这个文件有个默认的载入基地址(即KERNEL_RAM_VADDR,一般为0xC0008000),'.'是在编译的时候被解析的,所以代表的必然是当前指令编译时的虚拟地址。
在这里,同样也是标签__lookup_processor_type_data的虚拟地址。
内核真正解压到的虚拟地址zreladdr是在Makefile.boot中定义的(goldfish中为0x00008000)。在之前decompress_kernel解压的时候,MMU的状态为物理地址=虚拟地址,所以内核真正解压到的物理地址也是zreladdr。
而前面通过adr指令获取到的是__lookup_processor_type_data的物理地址,存在r3中。
sub r3, r3, r4; 翻译成c语言就是 r3 = r3 - r4。其计算结果,是物理地址与虚拟地址的偏移,等于 -PAGE_OFFSET,相当于这里动态计算了一遍PAGE_OFFSET。
*/
sub r3, r3, r4
//根据偏移,将__proc_info_begin和__proc_info_end的虚拟地址修正为物理地址
add r5, r5, r3
add r6, r6, r3
/*
工程代码中搜索.proc.info.init,能看到很多定义为.section ".proc.info.init"的结构,这些就是每种cpu具体的信息,这个结构体在c中也有一个定义,即为结构体struct proc_info_list (./kernel/arch/arm/include/asm/procinfo.h),这里根据当前process Id,来对比内核中的所有支持的proc_info_list 结构体,看看那个结构体匹配这个process Id,如果找到了匹配的,就将这个proc_info_list的地址返回,proc_info_list 中存放的就是处理器相关的一些函数/变量。其定义如下:
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsigned long __cpu_mm_mmu_flags;
unsigned long __cpu_io_mmu_flags;
unsigned long __cpu_flush;
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;
};
*/
//匹配value/mask
1: ldmia r5, {r3, r4} @ value, mask
and r4, r4, r9 @ mask wanted bits
teq r3, r4
//相等则表示找到匹配的proc_info_list结构了,调到2f
beq 2f
//不等则继续比较下一个
add r5, r5, #PROC_INFO_SZ
cmp r5, r6
blo 1b
//匹配不成功,则r5 = 0
mov r5, #0 @ unknown processor
//如果来自1,则r5已经是proc_info_list的地址了,直接按返回
2: mov pc, lr
ENDPROC(__lookup_processor_type)
__create_page_tables
/*
进入前:
r8: 保存平台相关的RAM的起始物理地址
r9: cpuid
r10: procinfo
返回:
r4返回页表的起始物理地址
*/
__create_page_tables:
/*
宏的输入:
phys为RAM的起始地址
TEXT_OFFSET为内核代码的起始物理地址(在RAM中的地址)相对于RAM起始地址的偏移
PG_DIR_SIZE为一个完整的页表的大小(16KB)
宏的输出:
rd为输出,为页表的物理地址。
.macro pgtbl, rd, phys
add \rd, \phys, #TEXT_OFFSET - PG_DIR_SIZE
.endm
*/
/*
在arm中认为:
1. 页表的物理地址 = RAM的起始地址 + TEXT_OFFSET - PG_DIR_SIZE
2. 页表的虚拟地址 = PAGE_OFFSET + TEXT_OFFSET - PG_DIR_SIZE
(参考定义 .equ swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE)
所以这一句是根据RAM的起始地址,计算页表的起始物理地址
*/
//r4为页表的起始物理地址
pgtbl r4, r8 @ page table address
//清空16KB页表
mov r0, r4
mov r3, #0
add r6, r0, #PG_DIR_SIZE
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
//从当前处理器对应的struct proc_info_list中获取__cpu_mm_mmu_flags
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/*
将__turn_mmu_on_loc函数(很短的一个函数)所在页映射到页表中,否则mmu开启关闭的切换会有问题。
*/
//获取__turn_mmu_on_loc的物理地址
adr r0, __turn_mmu_on_loc
//r3 = __turn_mmu_on_loc的编译虚拟地址, r5 = __turn_mmu_on编译虚拟地址,
//r6 = __turn_mmu_on_end编译虚拟地址,其中__turn_mmu_on到
//__turn_mmu_on_end是打开mmu的代码。
ldmia r0, {r3, r5, r6}
//让前面的编译虚拟地址转成物理地址
sub r0, r0, r3 @ virt->phys offset
add r5, r5, r0 @ phys __turn_mmu_on
add r6, r6, r0 @ phys __turn_mmu_on_end
//右移后的r5,r6代表页表中的某个下标
mov r5, r5, lsr #SECTION_SHIFT
mov r6, r6, lsr #SECTION_SHIFT
//获取__turn_mmu_on函数所在页的页基址(物理地址,用r5左移20位获取的),
//或上mmu掩码mmuflags,存在r3中(页表项中除了地址,还有掩码),
//这个r3,就是最终写到页表项中的内容。
1: orr r3, r7, r5, lsl #SECTION_SHIFT
//将r3存到页表中对应的页表项中(写入的地址是r5*4)
str r3, [r4, r5, lsl #PMD_ORDER]
//如果__turn_mmu_on函数跨页了,则循环处理
cmp r5, r6
addlo r5, r5, #1 @ next section
blo 1b
/*
将pc的物理地址所在页映射到虚拟地址KERNEL_RAM_VADDR所在页,
然后保持线性映射,一直映射到虚拟地址为KERNEL_END结束。
注意,这里有个默认前提是当前pc所在页总是内核代码的第一页,如果这个
要求不满足,则映射后整个线性地址会错位。
*/
//获取当前指令的物理地址
mov r3, pc
//计算当前指令所在页在pgd中的页下标
mov r3, r3, lsr #SECTION_SHIFT
//生成待填充的pgd页表内容,r7是页表属性。
orr r3, r7, r3, lsl #SECTION_SHIFT
//将pc这页的页表项内容(r3)写入KERNEL_START这个虚拟地址对应的页表项中
//其中SECTION_SHIFT为一页的大小,PMD_ORDER为每一个页表项所站的字节数。
//这里实际上最终分成了两步执行,add + str 实现了向pgd的这个页表写入的操作。
//其中第二步的r0会自加为KERNEL_START页表项的地址。
add r0, r4, #(KERNEL_START & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER)
str r3, [r0, #((KERNEL_START & 0x00f00000) >> SECTION_SHIFT) << PMD_ORDER]!
//KERNEL_END为内核的结束位置,是在vmlinux.lds.s中动态生成的,
//最终在反汇编中体现为立即数,会随重定位而改变。
ldr r6, =(KERNEL_END - 1)
//获取下一个页表项的地址
add r0, r0, #1 << PMD_ORDER
//r6为KERNEL_END对应的页表项的地址。
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
//循环遍历并初始化所有页表项
1: cmp r0, r6
add r3, r3, #1 << SECTION_SHIFT
strls r3, [r0], #1 << PMD_ORDER
bls 1b
/*
r2存的是atags的物理地址,里面是启动相关参数(boot params),这里将atags所在页
也线性映射到页表中,虚拟地址是物理地址 + PV_OFFSET。
*/
//r0存r2这个物理地址对齐后的基地址
mov r0, r2, lsr #SECTION_SHIFT
movs r0, r0, lsl #SECTION_SHIFT
//如果r0算完是0了,则将r8赋值过去,r8是ram的起始物理地址
moveq r0, r8
//算一下和ram起始物理地址之间的偏移
sub r3, r0, r8
##这个偏移 + page_offset(内核起始虚拟地址)就是这个页应该映射到的虚拟地址
add r3, r3, #PAGE_OFFSET
//根据虚拟地址找到应该填充的页表
add r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
orr r6, r7, r0
//将内容存入到页表项中
str r6, [r3]
//返回
mov pc, lr
__mmap_switched
__enable_mmu就是开启mmu的代码,mmu开启之后通过栈跳转到__mmap_switched函数,__mmap_switched函数的最后一步,就是start_kernel了,自start_kernel后,主要进入了c代码。
/*
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
.long cr_alignment @ r7
.long init_thread_union + THREAD_START_SP @ sp
.size __mmap_switched_data, . - __mmap_switched_data
*/
__mmap_switched:
//__mmap_switched_data变量中中存了c代码执行前,各个寄存器中应该初始化的值,
//这些值都是在vmlinux.lds.S中指定的。
adr r3, __mmap_switched_data
//r4=__data_loc, r5=_sdata, r6=__bss_start, r7=_end
ldmia r3!, {r4, r5, r6, r7}
//如果_sdata != __data_loc则复制数据再初始化,
//__data_loc指向二进制文件中初始化数据区域的起始地址,
//__data指向内存中初始化数据区的起始地址,如果内核是在ROM中片内执行的,
//ROM中初始化区域不应该被修改,这里会将初始化段复制到RAM,再在RAM中执行初始化。
cmp r4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
//bss段清零
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ARM( ldmia r3, {r4, r5, r6, r7, sp})
//将之前算出的处理器ID,机器信息等,保存到前面给c变量用的内存中。
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
//从此往后,是c语言执行的了,跳转到start_kernel(kernel/init/main.c)
b start_kernel
ENDPROC(__mmap_switched)