1. 从KernelStart开始
2. arm9虚拟地址到物理地址的转换
3. ;;写到再说
1. 从KernelStart()开始
事实上,kernel并不是从KernelStart()开始运行。eboot加载内核后,跳转的地址是startup.s文件的StartUp()函数(此处忽略msipl)。这个文件一般位于bsp目录中。这个函数会做一些预处理工作,之后跳到另一个startup.s文件的OALStartUp()函数,该函数做一些bsp级别的检查工作,之后会跳转到armtrap.s文件的KernelStart()函数。在跳转前要注意两点,一是此时mmu是关闭的,也就是cpu直接访问的是物理地址;其次g_oalAddressTable的物理地址会保存在r0寄存器中。g_oalAddressTable定义了虚拟地址和物理地址的映射关系,由bsp提供,下面是270的一个实现。第一列是虚拟地址,第二列是物理地址,第三列是大小,单位MB。
g_oalAddressTable
DCD 0x80000000, 0xA0000000, 64 ; MAINSTONEII: SDRAM (64MB).
DCD 0x84000000, 0x5C000000, 1 ; BULVERDE: Internal SRAM (64KB bank 0).
DCD 0x84100000, 0x58000000, 1 ; BULVERDE: Internal memory PM registers.
DCD 0x84200000, 0x4C000000, 1 ; BULVERDE: USB host controller.
DCD 0x84300000, 0x48000000, 1 ; BULVERDE: Memory controller.
DCD 0x84400000, 0x44000000, 1 ; BULVERDE: LCD controller.
DCD 0x84500000, 0x40000000, 32 ; BULVERDE: Memory-mapped registers (peripherals).
DCD 0x86500000, 0x3C000000, 64 ; BULVERDE: PCMCIA S1 common memory space.
DCD 0x8A500000, 0x38000000, 64 ; BULVERDE: PCMCIA S1 attribute memory space.
DCD 0x8E500000, 0x30000000, 32 ; BULVERDE: PCMCIA S1 I/O space.
DCD 0x90500000, 0x2C000000, 64 ; BULVERDE: PCMCIA S0 common memory space.
DCD 0x94500000, 0x28000000, 64 ; BULVERDE: PCMCIA S0 attribute memory space.
DCD 0x98500000, 0x20000000, 32 ; BULVERDE: PCMCIA S0 I/O space.
DCD 0x9A500000, 0xE0000000, 1 ; MAINSTONEII: Zero-bank (in reserved slot - no physical memory required).
DCD 0x9AA00000, 0x00000000, 64 ; MAINSTONEII: nCS0: MCPII Module Boot Flash (64MB).
DCD 0x9EA00000, 0x50000000, 1 ; BULVERDE: Camera peripheral interface.
DCD 0x00000000, 0x00000000, 0 ; end of table
2. arm9虚拟地址到物理地址的转换
在这里有必要介绍一点arm体系结构的基础知识。在mmu使能后,cpu“看到”的地址都是虚拟地址,mmu的功能之一就是把虚拟地址转换成物理地址(此处暂时忽略FCSE),因为电气信号只认识物理地址。比如要访问上面的虚拟地址0x80000000,在操作具体的地址总线时必须使用物理地址0xA0000000。
arm9系列cpu,有的不支持mmu而只有内存保护单元,有的支持mmu,后者还支持mmu的多种映射方式,具体可以参考文档2。ce目前的arm实现不仅利用了cpu的mmu功能,还使用了两种mmu映射方式:最高的1MB(0xFFF00000-0xFFFFFFFF),使用二级映射,并使用不同的page类型;而其余地址空间,只使用一级映射。这样做的原因是ce把高1MB空间用来给内核储存内存映射表(page table),中断向量表(exception vectors),不同模式下的stack和KDataStruct结构,而它们有不同的内存访问方式(domains,C,B等,参考文档2),需要用二级映射加以细分;而其它地址空间为了简化访问,加快速度,采用一级映射。下面会结合代码分析。
对于一级映射,参考下图。
对于二级映射,参考下图。
==================
3. 从KernelStart开始2
KernelStart()函数代码如下。
LEAF_ENTRY KernelStart
;我们现在跑在物理地址下;
;如前所述,r0在之前已经保存了MemoryMap的物理地址。
mov r11, r0 ; (r11) = &MemoryMap (save pointer)
; figure out the virtual address of OEMAddressTable
;将MemoryMap的物理地址转换成虚拟地址,保存在r6中,以后会经常用的。
mov r1, r11 ; (r1) = &MemoryMap (2nd argument to VaFromPa)
bl VaFromPa
mov r6, r0 ; (r6) = VA of MemoryMap
; convert base of PTs to Physical address
;注意这里,=PTs居然是一个虚拟地址。现在不是跑在物理地址下吗?为什么=PTs居然返回虚拟地址?
;答案在在config.bib里。这个文件会有一行说明eboot(也有可能是msipl)加载内核nk的地址,注意这个地址是虚拟地址,例如
;NK 80300000 00200000 RAMIMAGE
;nk.nb0的开始部分是以0x80000000为基址链接过的内核文件nk.exe。也就是说此时所有的lable返回的都是虚拟地址。
;换句话说,现在是在物理地址下跑虚拟地址的映像,这时只要访问的是相对地址而不是绝对地址,是不会跑飞的。
;下面代码将FirstPT的物理地址保存在r10中,以后会用到。
ldr r4, =PTs ; (r4) = virtual address of FirstPT
mov r0, r4 ; (r0) = virtual address of FirstPT
mov r1, r11 ; (r1) = &MemoryMap (2nd argument to PaFromVa)
bl PaFromVa
mov r10, r0 ; (r10) = ptr to FirstPT (physical)
这里有必要说一下ce内核的高地址虚拟空间(0x0xFFFD0000到0x0xFFFD0000)的使用。在armtrap.s文件的开头,我们会看到下面几行。
; High memory layout:
; FFFD0000 - first level page table (uncached)
; FFFD4000 - not used
; FFFE0000 - disabled for protection
; FFFF0000 - exception vectors
; FFFF03E0 - exception vector jump table
; FFFF0400 - not used (r/o)
; FFFF1000 - disabled for protection
; FFFF2000 - r/o (physical overlaps with vectors)
; FFFF2400 - Interrupt stack (1k)
; FFFF2800 - r/o (physical overlaps with Abort stack)
; FFFF3000 - disabled for protection
; FFFF4000 - r/o (physical memory overlaps with vectors & intr. stack & FIQ stack)
; FFFF4900 - Abort stack (2k - 256 bytes)
; FFFF5000 - disabled for protection
; FFFF6000 - r/o (physical memory overlaps with vectors & intr. stack)
; FFFF6800 - FIQ stack (256 bytes)
; FFFF6900 - r/o (physical memory overlaps with Abort stack)
; FFFF7000 - disabled
; FFFFC000 - kernel stack
; FFFFC800 - KDataStruct
; FFFFCC00 - r/o for protection (2nd level page table for 0xFFF00000)
^ 0xFFFD0000
FirstPT # 0x4000
# 0x4000
# 0x8000
# 0x10000 ; not mapped
ExVector # 0x1000
# 0x1400 ; not mapped
# 0x0400 ; 1K interrupt stack
IntStack # 0x2000 ; not mapped (ffff2800)
# 0x0100 ; not mapped (FIQ stack) (ffff4800)
# 0x0700 ; 2K-256 abort stack (ffff4900)
AbortStack # 0x1800 ; not mapped (ffff5000)
# 0x0100 ; not mapped (FIQ stack) (ffff6800)
FIQStack # 0xC000-0x6900 ; not mapped (ffff6900)
KDBase # 0x07E0 ; 2K-32 kernel stack
KStack # 0x0020 ; temporary register save area
KData # 0x400 ; kernel data area
;-----------------------------------------------------------------------
; .KDATA area is used to reserve physical memory for the above structures.
AREA |.KDATA|,DATA,NOINIT
EXPORT ExceptionVectors
KDataArea
PTs % 0x4000 ; space for first-level page table
ExceptionVectors
% 0x0400 ; space for exception vectors
% 0x0400 ; space for interrupt stack
% 0x0100 ; space for FIQ stack
% 0x0700 ; space for Abort stack
KPage % 0x0c00 ; space for kernel stack & KDataStruct
HighPT % 0x0400 ; space for 2nd level page table to map 0xFFF00000
KDEnd % 0
这段代码分为三个部分。第一部分是注释,详细解释了第二部分的内容。第二部分以0xFFFD0000为起始地址定义了一个数据结构(关于^和%等的含义参考文档1),可以通过将这个数据结构的每一项和注释对应起来了解其字面意思,具体含义以后会解释。第三部分分配了一块物理内存,来实现对上述数据结构的物理存储。在这里有两点要注意:第一是,该物理内存并没有和第二部分的数据结构对应起来。这是因为第二部分的数据结构中间有很多没有使用的"空洞",如果物理内存也要分配空间给这些"空洞"那就太浪费了。所以只需要给有意义的部分分配物理内存,在进行虚拟地址到物理地址映射的时候给虚拟地址最大小适合的page,通过在代码访问超出这个page大小的时候发生异常,同时在访问这些虚拟地址的时候谨慎的检查,来确保不会访问到"空洞"的虚拟地址;第二是从现在开始要注意源代码给出的关于PTs和FirstPT的使用和注释,之间有微妙的区别,这些都会在下面结合代码加以解释。
====================
; Zero out page tables & kernel data page
;初始化上面分配的物理内存KDataArea
mov r0, #0 ; (r0-r3) = 0's to store
mov r1, #0
mov r2, #0
mov r3, #0
mov r4, r10 ; (r4) = first address to clear
add r5, r10, #KDEnd-PTs ; (r5) = last address + 1
18 stmia r4!, {r0-r3}
stmia r4!, {r0-r3}
cmp r4, r5
blo %B18
下面的代码是为在使能mmu前初始化内存映射表page table。前面说过ce目前的arm实现不仅利用了cpu的mmu功能,还使用了两种mmu映射方式:最高的1MB(0xFFF00000-0xFFFFFFFF),使用二级映射,并使用不同的page类型;而其余地址空间,只使用一级映射。所以内存映射表的初始化也分为两步。这两步没有什么先后顺序,ce是先初始化二级映射,为了叙述方便这里先解释一级映射。注意这里的一级映射意思是虚拟地址只通过一次内存映射表的转换就能映射到对应的物理地址,而不是二级映射中的第一级映射的意思。如果是后者,将使用“第一级映射”加以区分。
另外有必要首先对一级映射加以解释。参考一级映射的示意图,Translation table base保存一级映射的内存映射表的物理地址,在这里也就是PTs,这个地址是16k对齐的,保存在CP15的寄存器c2中。给定一个虚拟地址vaddr,右移20bit后再左移2bit,作为内存映射表pt的索引(之所以要再左移2bit,是因为PTs保存一个地址需要4个byte),此时pt+(vaddr>>20)<<2就是该虚拟地址对应的一级映射描述符的物理地址,而pt[(vaddr>>20)<<2]就是该虚拟地址对应的一级映射描述符,最后vaddr对应的物理地址就是pt[(vaddr>>20)<<2]&0xfff00000+vaddr&0x000fffff。由此可见这种映射方式,一个描述符对应1M的物理空间。而pt[(vaddr>>20)<<2]的值,也就是一级映射描述符,是在下面初始化的。
; Fill in first level page table entries to create "un-mapped" regions
; from the contents of the MemoryMap array.
;
; (r9) = ptr to KData page
; (r10) = ptr to 1st level page table
; (r11) = ptr to MemoryMap array
;内核只初始化高2g的内核空间,低2g的用户空间的内存是在用户空间的程序运行时分配的,这里不必初始化。
;r10之前保存了PTs的物理地址,也就是一级映射的内存映射表的物理地址,要跳过(2g/1M)*4=0x2000,才能指向内核空间的起始地址0x80000000。
add r10, r10, #0x2000 ; (r10) = ptr to 1st PTE for "unmapped space"
;高2g空间又分为两部分,分别是0x80000000-0x9fffffff的cached和writebuffer空间(C+B),和0xa00000000-0xbfffffff的非cached和writebuffer空间(非C+B)。
;这两部分指向同一个物理空间,之所以要分成两个虚拟空间是系统需要。C+B空间用来进行高速的存储设备的访问,
;一般这种情况下不需要实时更新或读取具体的物理存储设备,所以高速的CPU不必等待具体的物理储存设备数据的更新或读取,
;而是采用writebuffer在其后写入,或读取以前保存在cache的数据。
;而非C+B用来访问物理寄存器或需要实时反应的设备,一般的寄存器访问都需要这种方式。
;所以ce为了区分这两种情况,采用了两个独立的虚拟内存空间的方法,需要分别初始化内存映射表。
;r7保存这个初始化的次数。首先初始化C+B空间的内存映射表。
mov r7, #2 ; (r7) = pass counter
;下面是构造一个一级映射描述符。参考一级映射示意图,对于一级映射要求描述符最后两个bit是二进制10,所以r0=0x02;
;r2之前被赋值为0x0c,即二进制1100,,意味着C和B被置位;
;参考文档2,0x400意味着通过该描述符访问对应的1M物理内存时,内核有读写权限,用户无访问权限;
;注意,这并不是意味着该内存地址用户是无法访问的,因为内核可以把该物理地址映射其它的用户可访问的虚拟地址。
mov r0, #0x02
orr r0, r0, r2 ; (r0)=PTE for 0: 1MB (C+B as set by OEM)
orr r0, r0, #0x400 ; set kernel r/w permission
20 mov r1, r11 ; (r1) = ptr to MemoryMap array
;r2,r3和r4分别保存g_oalAddressTable某一项的虚拟地址,物理地址和映射的大小(M)。
;例如,对应上面的g_oalAddressTable,r2=0x80000000, r3=0xA0000000, r4=64。
25 ldr r2, [r1], #4 ; (r2) = virtual address to map Bank at
ldr r3, [r1], #4 ; (r3) = physical address to map from
ldr r4, [r1], #4 ; (r4) = num MB to map
;最后一项全为0,由此可知是否遍历了该数组。
cmp r4, #0 ; End of table?
beq %F29
;下面这一段有点特殊。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}
中断处理程序如何处理中断,那是另外一个题目了。