;下面这一段有点特殊。ce5.0最多支持512M的物理RAM,也就是说虚拟地址空间0x80000000-0x9fffffff用来映射最多512M的物理RAM;
;所以r2和0x1FF00000求与后满足上述要求(1M对齐);
;而被映射的物理地址可以是任何4g空间内的地址,所以r3和0xFFF00000求与后满足要求(1M对齐)。
;注意,这里和ce支持4g虚拟内存是不矛盾的。
ldr r5, =0x1FF00000
and r2, r2, r5 ; VA needs 512MB, 1MB aligned.
ldr r5, =0xFFF00000
and r3, r3, r5 ; PA needs 4GB, 1MB aligned.
;r2 = r10 + r2>>18,r2指向一个描述符地址;
;r0 = r0 + r3,r0指向一个描述符的值,也就是高12bit存储一个物理page的起始地址,低20bit存储上面已经设置过的该page(1M)的属性。
add r2, r10, r2, LSR #18
add r0, r0, r3 ; (r0) = PTE for next physical page
;将描述符的值存储在该描述符地址指向的物理空间。然后r2+=4指向下一个描述符地址(注意一个描述符占4byte);
;r0+=0x00100000,指向下一个物理page的起始地址。
;至此,我们完成了一个一级映射描述符的映射过程。
28 str r0, [r2], #4
add r0, r0, #0x00100000 ; (r0) = PTE for next physical page
;r4存储了该内存区域(g_oalAddressTable某一项)的大小,单位为M,正好和page大小相等,所以直接r4-=1即可。
;若r4!=0,说明该内存区域还没有完成映射,则返回B28继续该内存区域其它page的映射;
;若r4==0,说明该内存区域映射完成,则清除r0的高12bit,而保留低20bit(因为全部page采用相同的属性设置),返回B25继续其它内存区域的映射。
sub r4, r4, #1 ; Decrement number of MB left
cmp r4, #0
bne %B28 ; Map next MB
bic r0, r0, #0xF0000000 ; Clear Section Base Address Field
bic r0, r0, #0x0FF00000 ; Clear Section Base Address Field
b %B25 ; Get next element
;此处已经完成C+B空间的映射,接下来要继续0xa00000000-0xbfffffff的非C+B空间的映射,所以首先清除C+B位。
;0x0800*1M/4 = 0x20000000,所以此时r10保存的物理PTE地址对应0xa0000000
29
bic r0, r0, #0x0C ; clear cachable & bufferable bits in PTE
add r10, r10, #0x0800 ; (r10) = ptr to 1st PTE for "unmapped uncached space"
subs r7, r7, #1 ; decrement pass counter
bne %B20 ; go setup PTEs for uncached space if we're not done
;最后恢复r10指向PTs,0x2000+0x0800+0x0800-0x3000=0
sub r10, r10, #0x3000 ; (r10) = restore address of 1st level page table
至此一级映射全部完成。下面再看我们上面跳过的二级映射。
; Setup 2nd level page table to map the high memory area which contains the
; first level page table, 2nd level page tables, kernel data page, etc.
;r4指向物理地址HighPT。HighPT存储二级页表。
add r4, r10, #HighPT-PTs ; (r4) = ptr to high page table
;为映射物理地址PTs构造一个表项。0x051最后两bit是01,说明是Large page(参考2),一个page为64k。
;其余字节说明访问权限。
;注意,这个page是非C+B的。
orr r0, r10, #0x051 ; (r0) = PTE for 64K, kr/w kr/w r/o r/o page, uncached unbuffered
;PTs对应虚拟地址0xFFFD0000,由于一个page是64k,需要4个page来映射0xFFFD0000-0xFFFD3FFF。
;Large page的映射方式在table index上稍微有点特别,使得我们可以使用同一个物理基址r0,具体参考2。
str r0, [r4, #0xD0*4] ; store the entry into 8 consecutive slots
str r0, [r4, #0xD1*4]
str r0, [r4, #0xD2*4]
str r0, [r4, #0xD3*4]
;r8指向中断向量表的物理地址ExceptionVectors。这个page的C和B如何设置由OEM指定,保存在r2中。
add r8, r10, #ExceptionVectors-PTs ; (r8) = ptr to vector page
bl OEMARMCacheMode ; places C and B bit values in r0 as set by OEM
mov r2, r0
;构造PTE。最后两个bit是10,说明是一个4k的Small page。(ExceptionVectors是4k大小)。
;为了将一个物理small page映射到4个第二级页表描述符,这里用了一个小技巧。
;注意到这四个虚拟地址0xFFFF0000,0xFFFF2400,FFFF4900和FFFF6800。通过Page index的不同(0x000,0x400,0x900和0x800 )
;可以将一个物理small page映射到不同的虚拟地址,而通过虚拟地址最后12bit的Page index来避免重叠访问。
orr r0, r8, #0x002 ; construct the PTE
orr r0, r0, r2
str r0, [r4, #0xF0*4] ; store entry for exception vectors
orr r0, r0, #0x500 ; (r0) = PTE for 4k r/o r/o kr/w kr/w C+B page
str r0, [r4, #0xF4*4] ; store entry for abort stack
str r0, [r4, #0xF6*4] ; store entry for FIQ stack (access permissions overlap for abort and FIQ stacks, same 1k)
orr r0, r8, #0x042
orr r0, r0, r2 ; (r0)= PTE for 4K r/o kr/w r/o r/o (C+B as set by OEM)
str r0, [r4, #0xF2*4] ; store entry for interrupt stack
;没什么好说的。
add r9, r10, #KPage-PTs ; (r9) = ptr to kdata page
orr r0, r9, #0x002
orr r0, r0, r2 ; (r0)=PTE for 4K (C+B as set by OEM)
orr r0, r0, #0x250 ; (r0) = set perms kr/w kr/w kr/w+ur/o r/o
str r0, [r4, #0xFC*4] ; store entry for kernel data page
;构建二级映射中的第一级描述符。
orr r0, r4, #0x001 ; (r0) = 1st level PTE for high memory section
add r1, r10, #0x4000
str r0, [r1, #-4] ; store PTE in last slot of 1st level table
============
4. PSL和API调用
接下来这部分资料有些翻译自网络上的文章,大部分是我自己的理解。我们知道ce的api是由几个server进程实现的,包括filesys.exe,gwes.exe,device.exe和services.exe,当然,还有内核nk.exe。当一个进程调用某个api时,调用者的线程通常会"跳进" server进程中执行。这是怎么做到的呢?
大部分ce的api是由coredll.dll导出的。所有的ce应用程序都会链接到这个dll。当一个进程调用某个api,比如GetTickCount()时,它调用的是coredll.dll导出的GetTickCount()。查看coredll.def文件将会发现这么一行,
GetTickCount=xxx_GetTickCount
这意味着我们调用的导出函数GetTickCount()在coredll.dll中的实现是函数xxx_GetTickCount()。而xxx_GetTickCount()的实现很简单,仅仅是一个类似如下的封装,
DWORD xxx_GetTickCount()
{
return GetTickCount();
}
要注意的是,这个实现在一般授权情况下获得的ce源代码中是看不到的,查看源代码目录\PRIVATE\WINCEOS\COREOS\CORE下的dir文件会发现当前目录下缺少很多子目录。据说上面的实现就在其中的thunks目录下。MS之所以不公开这些相对来说并不重要的源代码,我想主要原因是安全上的考虑,因为coredll.dll是ce法定的访问内核和其它server进程的唯一途径。
那么上面coredll.dll调用的GetTickCount()又是在哪里实现的呢?在\public\COMMON\OAK\INC\mkfuncs.h中,有如下定义,
#define GetTickCount WIN32_CALL(DWORD, GetTickCount, (VOID))
在\public\COMMON\OAK\INC\psyscall.h中,有如下定义,
#define WIN32_CALL(type, api, args) IMPLICIT_DECL(type, SH_WIN32, W32_ ## api, args)
#define IMPLICIT_DECL(type, hid, mid, args) (*(type (*)args)IMPLICIT_CALL(hid, mid))
#define IMPLICIT_CALL(hid, mid) (FIRST_METHOD - ((hid)<<HANDLE_SHIFT | (mid))*APICALL_SCALE)
#define FIRST_METHOD 0xF0010000
#define HANDLE_SHIFT 8
#define APICALL_SCALE 4
#define W32_GetTickCount 13
在\public\COMMON\SDK\INC\kfuncs.h中,有如下定义,
#define SH_WIN32 0
至此,我们可以把GetTickCount()展开为,
(*(DWORD (*)(VOID))(0xF0010000 - ((0)<<8 | (13))*4))
这是一个函数指针,指向地址0xF000FFCC。当调用函数xxx_GetTickCount()的时候,最终会跳转到地址0xF000FFCC上执行。这里用到了一个巧妙的小技巧,也就是所谓的PSL,即protected server libraries。下面可以说明为什么在这里插进上面这一段看似无关的文字原因了。因为在上一篇叙述到的代码之后,我们看到如下的代码,
; Set up page table entry for PSL calls to turn the pre-fetch abort into a permission fault rather
; than a translation fault. This speeds up the time to execute a PSL call, as this entry can be
; cached in the TLB
;
; (r10) = ptr to first level page table
;为PSL设置页表项。(0xF0000000/1M)*4=0x3C00,所以r0保存的页表项的物理地址对应虚拟地址0xF0000000。
;描述符不必设置page的物理地址,因为我们根本就不打算访问到这个物理地址,
;我们设置AP permission为内核和用户皆不可访问,意味着访问这个虚拟地址的时候,会发生Prefetch Abort。
;而在Prefetch Abort的处理中会继续处理PSL。
add r0, r10, #0x3C00 ; Page table entry for 0xF0000000 -> 0xF0100000
mov r1, #PTL1_SECTION + PTL1_XN ; Level 1 Section, with Cachable/bufferable, access
; bits and phys address set to zero
;; Because the CP15 R1 R bit is set, there are no unreadable settings via the AP permission bits.
orr r1, r1, #0x1E0 ; Set Domain to 15 (to cause domain access fault)
; Domain access is set up below..
str r1, [r0] ; Store the level 1 PTE
在上面这段代码中,还要注意并没有为这个1M的section设置C+B。个人觉得应该设置C+B,在注释里也说明设置了这个属性,但是并没有在代码里表现出来。难道是MS的笔误?设置这个属性可以减少一次内存访问,由于PSL频繁被调用,总的节省的时间是很可观的。由于PSL接下来的处理比较复杂,将在以后叙述。
=================
5. 中断向量表初始化
接下来的代码进行中断向量表初始化。首先需要介绍一下arm中断的知识。对于arm9,它的中断向量可以放在低地址0x00000000,或者高地址0xffff0000。ce只能将中断向量表放在高地址。这是由ce的整个编译系统决定的。网上有一篇文章"Eboot编译编译器决定中断向量及其实现单一性的原因"(写这篇文章的哥们肯定语文没学好,我也是:(),介绍的挺详细,另外我觉得有两点需要加以补充。一个是pe文件不能拷贝0x400后的部分直接执行,不仅仅是偏移量的问题,在后面介绍pe文件结构的时候会说明;另一个是image映像(nb0)文件的前4k部分是romimage生成的。这4k包含一个signature(0x43454345),一个ROMHDR结构数据的地址。对于eboot.nb0来说,在最开始还包含一个跳转指令0xea0003fe。这个机器码的意思是"b #3fe<<2"。考虑到arm的流水线,实际就是跳转到4k地址处了。
所以如果需要eboot支持中断,可以采取两种途径。一种是模仿内核的中断初始化,在高地址0xffff0000初始化中断向量表,一种是在低地址0x00000000存放中断向量表。对于前一种方法,可以直接参考内核的实现方法。对于后一种方法,首先需要特别修改虚拟地址和物理地址的映射关系,将物理地址映射到虚拟地址0x00000000上,而不是ce默认设定的0x80000000;接着需要修改eboot.bib文件,将RAMIMAGE设置在0x00000000上。最后需要一点小技巧,需要手动修改eboot.nb0的头几十个字节的机器码,改成0xea00xxxx之类的机器指令,目的是将中断处理跳转到真实的中断向量表处。当然,这个真实的中断向量表需要事先设定好。最后声明,这是本人的想法而已,还没有时间动手实践过。
现在回到正题上来。下面这段代码很好懂,从虚拟地址0xffff0000开始连续存储8个指令"ldr pc, [pc, #0x3E0-8]",然后从虚拟地址0xffff03e0开始存储中断向量表VectorTable。这样的效果就是,在发生中断时,比如irq中断,执行的命令相当于将irq的中断向量IRQHandler的地址装载到pc寄存器中,从而跳转到相应的中断向量处理程序中执行。
; Setup the vector area.
;
; (r8) = ptr to exception vectors
add r7, pc, #VectorInstructions - (.+8)
ldmia r7!, {r0-r3} ; load 4 instructions
stmia r8!, {r0-r3} ; store the 4 vector instructions
ldmia r7!, {r0-r3} ; load 4 instructions
stmia r8!, {r0-r3} ; store the 4 vector instructions
; convert VectorTable to Physical Address
ldr r0, =VectorTable ; (r0) = VA of VectorTable
mov r1, r11 ; (r1) = &OEMAddressTable[0]
bl PaFromVa
mov r7, r0 ; (r7) = PA of VectorTable
add r8, r8, #0x3E0-(8*4) ; (r8) = target location of the vector table
ldmia r7!, {r0-r3}
stmia r8!, {r0-r3}
ldmia r7!, {r0-r3}
stmia r8!, {r0-r3}
中断处理程序如何处理中断,那是另外一个题目了。