WXJPCY888
发表于 2013-9-21 12:13
精彩!!!
guokeqin
发表于 2013-9-22 09:39
学习,,,,,
123de7
发表于 2013-9-22 11:37
顶顶 顶
xinzha
发表于 2013-10-14 15:57
本帖最后由 xinzha 于 2013-10-15 17:08 编辑
感觉遇到了瓶颈,无法表达自己的想法,也无法向将应该写的重点体现出来,于是想到了用linux来讲解arm架构,正好今年由于工作关系以及个人爱好,开始学习linux内核代码,顺着这个线路走走试试。
ARM linux的启动代码
在解压kernel之前的部分属于bootloader或者bsp的工作,这部分代码的职责就是把压缩(或者未压缩的)代码放到正确的地点,然后初始化必要的硬件,把pc指针设定到应该跳转的位置,这里的工作可以认为是各种系统通用的,与linux本身无关。
如果是压缩的image,会在头部有一段自解压的代码,负责将kernel image解压到对应的位置,而如果是非压缩的image,直接拷贝到指定位置,跳过去即可。
在这篇文档之中会包含很多对汇编语言以及arm结构的讲解,一是因为个人兴趣爱好问题,二是既然都要开始研究启动代码了,汇编和相应体系结构知识已经是必须的了。
解压后的image的入口是stext,在/arch/arm/kernel/head.S中,我们首先可以在其中看到下面的一句话:
ENTRY(stext)
这就是指示链接器,stext是整个image的入口点,一切链接从它开始。当被调用(或者说pc指针指向这里的时候),r0 = 0, r1 = machine nr, r2 =atags,这里atags为启动参数列表。
需要注意的是,有些**写这里的内核入口的地址为0xC0008000,这是错误的,因为此时MMU尚未初始化,指针只能指向实际的物理地址而不是虚拟地址。这些信息隐含了一些限制,就是在MMU初始化之前的代码统统不能包含绝对地址跳转,只能是相对跳转,因为代码是链接在0xC0008000这个地址上的,一旦出现绝对跳转,cpu就会寻址这段地址,而多数情况下这段地址当前是不存在的。若是只使用了相对跳转,在汇编指令中的寻址只会出现对当前pc的加减操作,不用再去关心代码实际在哪里执行。
随后,确信代码工作在SVC模式:
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode and irqs disabled
setmode是linux自己实现的一个宏,不是伪汇编指令,
#ifdef CONFIG_THUMB2_KERNEL
.macro setmode, mode, reg
mov \reg, #\mode
msr cpsr_c, \reg
.endm
#else
.macro setmode, mode, reg
msr cpsr_c, #\mode
.endm
#endif
从宏的内容可以看到,arm模式下直接将mode值赋给cpsr_c(cpsr的控制部分),与传入寄存器无关,thumb模式下的内容暂不讨论。
cuijinyi
发表于 2013-10-15 16:20
mark
xinzha
发表于 2013-10-15 17:09
下面两句获取cpu的processor id 和信息,都是arm强制规定的协处理器汇编指令,详细内容查询arm手册即可。
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
值得一提的是__lookup_processor_type中的一个小技巧,利用了相对地址定位的方法计算出了虚拟地址和物理地址的差值。
__lookup_processor_type:
adr r3, __lookup_processor_type_data
ldmia r3, {r4 - 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
首先adr r3, __lookup_processor_type_data获取了__lookup_processor_type_data的物理地址, 需要注意的是adr并不是一个真正的汇编指令,而是一个宏汇编伪指令,最终会被编译成类似于add r3,pc,#xxxx这样的代码,所以这句代码拿到的地址就是当前模式(物理地址模式)下的变量地址,然后ldmia r3, {r4 - r6}把内容传给r4,r5,r6,__lookup_processor_type_data数组的第一个值就是链接时这个数组的地址,也就是我们所说的虚拟地址,这样我们就有了r3保存物理地址,r4保存虚拟地址,二者相减就是偏移值,然后就能算出数组中其他元素的物理地址了。
在检查完processor id和machine info的合法性之后,检查ATAG_CORE的合法性,若一切正常则向下进行,否则打印出错信息,停止boot。
shuidi_wangdan
发表于 2013-10-16 11:40
积极学习中。。。。。。。。。。。。。。
superboy1984
发表于 2013-10-16 12:36
顶顶顶。。。。持续关注。。。。。。
么么沫沫
发表于 2013-10-16 14:14
xinzha
发表于 2013-10-17 08:51
接下来该是建立page table的时候了,我们一步一步地对__create_page_tables进行讲解。
函数体位于__create_page_tables:和ENDPROC(__create_page_tables)之间,ENDPROC(__create_page_tables)宏的作用是声明一个函数,并标明函数体到此为止。
#ifndef ENDPROC
#define ENDPROC(name) \
.type name, @function; \
END(name)
#endif
.type name,@function;语句的含义是定义name为一个函数。
.type伪汇编指令的作用就是声明我们的symbol为何种类型,所支持的类型包括:
function, data, gnu_indirect_function, tls_object, common, notype, gnu_unique_object。对于我们来说常用的是function和data,函数类型和数据类型,详细的资料可以查阅GNU汇编的在线文档,
当作者写作时的最新文档位于https://sourceware.org/binutils/docs-2.23.1/as/index.html#Top。
宏END的定义如下:
#ifndef END
#define END(name) \
.size name, .-name
#endif
这个宏的含义设置一个符号的大小(主要是为了编译链接器或者其他工具的使用),其值为当前地址减去标号为name的地址,也就是计算函数体的大小。
/*
* 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 = phys_offset, r9 = cpuid, r10 = procinfo
*
* Returns:
*r0, r3, r5-r7 corrupted
*r4 = physical page table address
*/
pgtbl r4, r8 @ page table address
pgtbl r4, r8 计算页表地址的起始地址并传给r4。pgtbl的宏定义如下:
__create_page_tables:
.macro pgtbl, rd, phys
add \rd, \phys, #TEXT_OFFSET - 0x4000
.endm
我们可以看到,这里讲页表的地址放到了物理内存加上代码段偏移(0x8000)再减去0x4000,也就是物理内存起点加0x4000的地址(也就是初始化完mmu之后的0xC0004000)。
接下来清理了16KB大小的页表内存,在使用前清零是一种好习惯,无数次掉进过内存区未清零而导致死机的深坑,不管是谁的代码都不那么可信,所以还是尽可能做好自己的事最重要:
/*
* Clear the 16K level 1 swapper page table
*/
mov r0, r4
mov r3, #0
add r6, r0, #0x4000
1: str r3, , #4
str r3, , #4
str r3, , #4
str r3, , #4
teq r0, r6
bne 1b
hbfreebb
发表于 2013-10-17 16:56
等待学习
Ms19862009
发表于 2013-10-18 01:54
果断顶起!
xinzha
发表于 2013-10-18 17:31
获取由bootloader传进来的MMU信息,存入r7,
ldr r7, @ mm_mmuflags
建立一个临时的映射表供__enable_mmu使用,从这里的代码我们就看懂了为什么预留的是16KB的页表空间,因为这里使用的1MB的页表,16KB可以表示4K个项目,所以也就能够表达4K * 1MB = 4G的地址空间。我们再次看到了虚拟地址到物理地址转换的技巧。这里的__enable_mmu_end和__enable_mmu并没有什么道理,只是两个函数的大小,结果被用来决定初始化多少MB的页表(每次循环初始化1MB的物理空间,有的patch中修改为__turn_mmu_on和__turn_mmu_on,在数个版本中看到这里的代码都不一致,说明内核人员对如何处理也不是很统一),个人感觉这种做法不是很靠谱,如果能够修改为启动部分代码所用到的内存大小就合乎情理了。
/*
* Create identity mapping to cater for __enable_mmu.
* This identity mapping will be removed by paging_init().
*/
adr r0, __enable_mmu_loc
ldmia r0, {r3, r5, r6}
sub r0, r0, r3 @ virt->phys offset
add r5, r5, r0 @ phys __enable_mmu
add r6, r6, r0 @ phys __enable_mmu_end
mov r5, r5, lsr #20
mov r6, r6, lsr #20
1: orr r3, r7, r5, lsl #20 @ flags + kernel base
str r3, @ identity mapping
teq r5, r6
addne r5, r5, #1 @ next section
bne 1b
接下来要初始化内核直接映射区域,范围是从KERNEL_START到KERNEL_END,
/*
* Now setup the pagetables for our kernel direct
* mapped region.
*/
mov r3, pc
mov r3, r3, lsr #20
orr r3, r7, r3, lsl #20
add r0, r4,#(KERNEL_START & 0xff000000) >> 18
str r3, !
ldr r6, =(KERNEL_END - 1)
add r0, r0, #4
add r6, r4, r6, lsr #18
1: cmp r0, r6
add r3, r3, #1 << 20
strls r3, , #4
bls 1b
这里是映射了kernel指定区域的页表项,其他的部分都好理解,唯独有一处诡异的代码让人迷惑。
add r0, r4,#(KERNEL_START & 0xff000000) >> 18
str r3,
这两句完全可以写成下面更好让人理解的格式:
add r0, r4,#(KERNEL_START & 0xfff00000) >> 18
str r3,
这两句代码的本质含义是这样的,首先#(KERNEL_START & 0xfff00000) 是内核起始地址在1MB边界上的对齐,右移20位是其在MMU一级页表中的下标,因为每个表项占用4字节,所以再乘以4(左移两位)就是其在页表中的偏移。随后是构造表项然后储存的机械工作。
下面是配置XIP运行环境的,感觉上代码写得不是很严谨,应该是一段年久失修的代码,也许是我拿到的发行版的问题?其内容与之前的配置差不多。
龙飞空空
发表于 2013-10-19 12:50
持续关注,不可多得的好,感谢
xinzha
发表于 2013-10-21 09:01
#ifdef CONFIG_XIP_KERNEL
/*
* Map some ram to cover our .data and .bss areas.
*/
add r3, r8, #TEXT_OFFSET
orr r3, r3, r7
add r0, r4,#(KERNEL_RAM_VADDR & 0xff000000) >> 18
str r3, !
ldr r6, =(_end - 1)
add r0, r0, #4
add r6, r4, r6, lsr #18
1: cmp r0, r6
add r3, r3, #1 << 20
strls r3, , #4
bls 1b
#endif
下面配置包含着系统的启动参数的那块内存,如果没有指定系统启动参数的话,就初始化系统的第一个MB。
/*
* Then map boot params address in r2 or
* the first 1MB of ram if boot params address is not specified.
*/
mov r0, r2, lsr #20
movs r0, r0, lsl #20
moveq r0, r8
sub r3, r0, r8
add r3, r3, #PAGE_OFFSET
add r3, r4, r3, lsr #18
orr r6, r7, r0
str r6,
剩下的页表处理代码都是跟具体的cpu型号或者是和debug相关的,不予讨论。
初始化页表的工作做完了,接下来就进入了cpu寄存器的实际配置阶段,
/*
* 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.
*/
ldr r13, =__mmap_switched @ address to jump to after
@ mmu has been enabled
adr lr, BSYM(1f) @ return (PIC) address
ARM( add pc, r10, #PROCINFO_INITFUNC )
THUMB( add r12, r10, #PROCINFO_INITFUNC )
THUMB( mov pc, r12 )
1: b __enable_mmu
ENDPROC(stext)
ldr r13, =__mmap_switched这句是将__mmap_switched赋给r13寄存器,做为调用完mmu使能函数之后的跳转目标,从这句代码我们还能够看到另外一个信息,在真正调用到__mmap_switched之前,所有代码都是用汇编完成的,没有c代码!原因?因为他们居然用sp寄存器保存了返回值,说明整个调用路径中没有c编译器参与,否则c编译器必将会破坏sp内容,r13做为堆栈指针是BAPI的约定。在__mmap_switched中我们将看到激动人心的start_kernel。
adr lr, BSYM(1f)是将lr设置成cpu特定的初始化程序之后的返回地址,这里使用地址无关代码的原因就在于ARM系统的灵活性,ARM没有像x86那样强制规定物理RAM从哪里开始,所以在不同的系统上可能RAM的物理起始地址是不一样的,这样分开编译的特定cpu代码和cpu无关的系统代码就要找到一个可靠的方法来确定沟通正确,使用位置无关代码确保了在所有类型硬件上这段代码都可以执行正确。
相对于v7架构的cpu来说,add pc, r10, #PROCINFO_INITFUNC 迫使cpu跳转到proc-v7.s中的__v7_setup, 这个函数的目的就是使能TLB,MMU和cache,都是些对协处理器的设置,按照规范写就可以,而且其中包含了非常多的errata,现在没有多大兴趣研究,贴上代码,以后高兴了再看。
look1259
发表于 2013-10-22 17:42
very nice work
xinzha
发表于 2013-10-22 19:36
随后调用了__enable_mmu来使能mmu,注意是用的b而不是bl,说明这是张单程票,不想回来了。
/*
* Setup common bits before finally enabling the MMU.Essentially
* this is just loading the page table pointer and domain access
* registers.
*
*r0= cp#15 control register
*r1= machine ID
*r2= atags pointer
*r4= page table pointer
*r9= processor ID
*r13 = *virtual* address to jump to upon completion
*/
__enable_mmu:
#ifdef CONFIG_ALIGNMENT_TRAP
orr r0, r0, #CR_A
#else
bic r0, r0, #CR_A
#endif
#ifdef CONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #CR_C
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE
bic r0, r0, #CR_I
#endif
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))
mcr p15, 0, r5, c3, c0, 0 @ load domain access register
mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
b __turn_mmu_on
ENDPROC(__enable_mmu)
这里根据配置对设置cp15的寄存器,包括了是否检查对齐,是否使能I cache,D cache,设置页表起始地址,设置域访问权限寄存器等。随后跳到__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
*r1= machine ID
*r2= atags pointer
*r9= processor ID
*r13 = *virtual* address to jump to upon completion
*
* other registers depend on the function called upon completion
*/
.align 5
__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, r13
mov pc, r3
__enable_mmu_end:
ENDPROC(__turn_mmu_on)
又是cp15的操作来使能mmu,只是最后一步不明白什么用意,读出一个值,随即扔掉,在这里我们看到以前存储的r13粉墨登场,跳转到__mmap_switched。
__mmap_switched:
adr r3, __mmap_switched_data
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, , #4
strne fp, , #4
bne 1b
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, ,#4
bcc 1b
ARM( ldmia r3, {r4, r5, r6, r7, sp})
THUMB( ldmia r3, {r4, r5, r6, r7} )
THUMB( ldr sp, )
str r9, @ Save processor ID
str r1, @ Save machine type
str r2, @ Save atags pointer
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r7, {r0, r4} @ Save control register values
b start_kernel
ENDPROC(__mmap_switched)
清理data区域,保存启动信息,启动c代码前最关键的一点:设置sp指针。跳转到start_kernel,汇编阶段任务完成。
xinzha
发表于 2013-10-22 19:39
汇编启动阶段差不多讲完了,接下来要进入arm相关的系统调度运行等的细节。
不过要过一阵才有时间写了。
outstanding
发表于 2013-10-23 12:37
yanwenbin33
发表于 2013-10-23 22:31
这么好的帖子,留着脚印等更新。