1. Ppl代码初始化
内核在启动阶段调用pmap_bootstrap函数初始化物理内存布局。
pmap_bootstrap:
LDR X20, [X27,#_avail_start@PAGEOFF]
MOV X0, X20
BL _phystokv
MOV X21, X0
ADRP X8, #_pmap_array_begin@PAGE
STR X0, [X8,#_pmap_array_begin@PAGEOFF]
ADRP X19, #_pmap_array@PAGE
STR X0, [X19,#_pmap_array@PAGEOFF]
ADRP X24, #_pmap_max_asids@PAGE
LDR W8, [X24,#_pmap_max_asids@PAGEOFF]
MOV W9, #0x108
MOV W10, #0x3FFF
MADD X8, X8, X9, X10
AND X8, X8, #0x3FFFFFFC000
ADD X0, X8, X20
STR X0, [X27,#_avail_start@PAGEOFF]
BL _phystokv
ADRP X8, #_pmap_array_end@PAGE
STR X0, [X8,#_pmap_array_end@PAGEOFF]
_avail_start保存的是当前空闲物理内存的物理地址,转换成虚拟地址后,保存在_pmap_array_begin和_pmap_array变量中,然后从此物理地址划出_pmap_max_asids字节大小的区域给_pmap_array,它是一个struct pmap数组,在ppl的实现中,内核以及每个进程都有单独的一个struct pmap结构。在非ppl的版本中,只有内核使用struct map。
ADRP X8, #_pmap_array_count@PAGE
ADRL X10, _pmap_free_list_lock
STR X9, [X8,#_pmap_array_count@PAGEOFF]
STR X23, [X10,#(qword_FFFFFFF00981CA88 - 0xFFFFFFF00981CA80)]
STR XZR, [X10]
LDR X9, [X8,#_pmap_array_count@PAGEOFF]
计算_pmap_array_count大小。
MOV X9, #0
MOV X10, #0
MOV X12, #0
LDR X11, [X19,#_pmap_array@PAGEOFF]
STR X12, [X11,X9]
LDR X11, [X19,#_pmap_array@PAGEOFF]
ADD X12, X11, X9
ADD X10, X10, #1
LDR X13, [X8,#_pmap_array_count@PAGEOFF]
ADD X9, X9, #0x108
CMP X10, X13
B.CC loc_FFFFFFF007B5E820
ADD X8, X11, X9
SUB X8, X8, #0x108
B loc_FFFFFFF007B5E850
MOV X8, #0
ADRP X9, #_pmap_free_list@PAGE
STR X8, [X9,#_pmap_free_list@PAGEOFF]
初始化_pmap_free_list链表,循环让_pmap_array数组中的每个元素依次指向前一个节点,以后ppl为每个进程分配struct pmap结构体时就从这个链表分配。
下面用类似的算法初始化_pmap_ledger_ptr_array数组,这里不在赘述。
然后开始初始化ppl使用的stack信息:
ADRP X8, #_pmap_stacks_start@PAGE
LDR X8, [X8,#_pmap_stacks_start@PAGEOFF]
ADD X26, X8, #4,LSL#12
ADRP X8, #_pmap_stacks_start_pa@PAGE
STR X20, [X8,#_pmap_stacks_start_pa@PAGEOFF]
_pmap_stacks_start保存的是stack的虚拟地址,_pmap_stacks_start_pa保存的是stack的物理地址,后面修改kernel_map->tte的l3页表,进行虚拟地址到物理地址的映射。
MOV X28, #0x20000000000603
ADRL X21, _pmap_cpu_data_array
MOV W25, #0xFFFFFFFF
B loc_FFFFFFF007B5E92C
MOV W8, #0x180
MADD X8, X19, X8, X21
STR W25, [X8,#0x40]
STR WZR, [X8,#0x18]
STR X24, [X8,#8]
ADD X26, X26, #8,LSL#12
ADD X19, X19, #1
CMP X19, #6
B.EQ loc_FFFFFFF007B5E9B8
ADD X24, X26, #4,LSL#12
MOV X23, X26
MOV X8, #0xFFFFFFFFFFFFBFFF
CMP X26, X8
B. HI loc_FFFFFFF007B5E908
依次设置每个cpu的 _pmap_cpu_data结构体,STR X24, [X8,#8],将栈地址保存在0x8偏移处。每个cpu的ppl stack都是4k,之间在隔离着一个4k的guard page。
后面的代码继续初始化_ppl_cpu_save_area,它保存的是ppl进入EL1_Guard level异常处理时保存的全部寄存器区域。
下面给出xnu物理内存布局图,请原谅我的懒惰,不想画漂亮的图形。
XNU的物理内存管理模型,增加了两个结构体用于辅助物理页的管理。
pp_attr_t代表的是一个物理页的属性,在XNU的source code里可以看到其定义:
osfmk/arm/pmap.c
typedef u_int16_t pp_attr_t;
#define PP_ATTR_WIMG_MASK 0x003F
#define PP_ATTR_WIMG(x) ((x) & PP_ATTR_WIMG_MASK)
#define PP_ATTR_REFERENCED 0x0040
#define PP_ATTR_MODIFIED 0x0080
#define PP_ATTR_INTERNAL 0x0100
#define PP_ATTR_REUSABLE 0x0200
#define PP_ATTR_ALTACCT 0x0400
#define PP_ATTR_NOENCRYPT 0x0800
#define PP_ATTR_REFFAULT 0x1000
#define PP_ATTR_MODFAULT 0x2000
#if XNU_MONITOR
/*
* Denotes that a page is owned by the PPL. This is modified/checked with the
* PVH lock held, to avoid ownership related races. This does not need to be a
* PP_ATTR bit (as we have the lock), but for now this is a convenient place to
* put the bit.
*/
#define PP_ATTR_MONITOR 0x4000
/*
* Denotes that a page *cannot* be owned by the PPL. This is required in order
* to temporarily 'pin' kernel pages that are used to store PPL output parameters.
* Otherwise a malicious or buggy caller could pass PPL-owned memory for these
* parameters and in so doing stage a write gadget against the PPL.
*/
#define PP_ATTR_NO_MONITOR 0x8000
对于ppl来讲,增加了两个属性PP_ATTR_MONITOR和PP_ATTR_NO_MONITOR,ios使用相同的值,后面会看到。
每个物理页在内核中有两种用处,第一个物理页保存的是页表内容,第二个就是保存内核用到的其他数据结构,在XNU的物理内存管理模型中,使用struct pv_entry结构体保存l3页表项的地址,可以提高物理内存与虚拟内存相互转化的效率。使用struct pt_desc结构体描述一个页表属性,比如l1、l2、l3table的属性。对于ppl,会用到pv_head_table对给定的一个物理页上锁。
Pv_head_table的内存布局如下,请再次原谅我的懒惰。
在初始化完ppl的物理内存布局后,内核在启动的下一阶段会调用_bootstrap_instructions继续执行EL1_Guard level的初始化。
__start->_start_first_cpu->_arm_init->_arm_vm_init->_bootstrap_instructions
_bootstrap_instructions:
关闭mmu
S3_6_C15_C1_00x1
S3_6_C15_C1_10x1
S3_6_C15_C1_50x2010000030100000
S3_6_C15_C1_60x2020000030200000
S3_6_C15_C1_70x2020a500f020f000
S3_6_C15_C3_00x2020a505f020f0f0
S3_6_C15_C1_20x1
S3_6_C15_C8_1_gxf_bootstrap_handler(虚拟地址)
S3_6_C15_C8_2_gtr_deadloop(虚拟地址)
S3_6_C15_C1_20x0
开启mmu
在这个函数里,可以看到苹果cpu加入了几个新的系统寄存器, 通过分析代码逻辑可以推测出S3_6_C15_C1_2寄存器应该是个执行上锁和解锁的功能。gxf_bootstrap_handler函数地址存入了S3_6_C15_C8_1寄存器,_gtr_deadloop函数存入了S3_6_C15_C8_2寄存器。在整个kernelcache代码段里都没有搜索到对_gxf_bootstrap_handler函数的直接引用,因此可以推断出S3_6_C15_C8_1寄存器保存的地址应该是cpu进入EL1_Guard level时首先要执行的函数。在另外一个初始化路径中也可以看到相同的ppl初始化代码。
__start->_start_first_cpu->_arm_init->_cpu_machine_idle_init->_start_cpu->start_cpu
start_cpu:
S3_6_C15_C1_00x1
S3_6_C15_C1_10x1
S3_6_C15_C3_00x2020a506f020f0e0
S3_6_C15_C1_50x2010000030100000
S3_6_C15_C1_60x2020000030200000
S3_6_C15_C1_70x2020a500f020f000
S3_6_C15_C1_20x1
S3_6_C15_C8_1_gxf_bootstrap_handler(虚拟地址)
S3_6_C15_C8_2_gtr_deadloop(虚拟地址)
S3_6_C15_C1_20x0
注意这两个初始化函数只是对S3_6_C15_C8_1寄存器进行了设置,并没有调用它。在内核启动的最后阶段有如下调用代码:
_kernel_bootstrap_thread->machine_lockdown
ADRP X8, #_pmap_ppl_locked_down@PAGE
MOV W19, #1
STR W19, [X8,#_pmap_ppl_locked_down@PAGEOFF]
BL _gxf_enable
首先对_pmap_ppl_locked_down变量设置为1,这个状态表示ppl已经初始化完毕,el1代码可以向EL1_Guard level请求服务了。然后调用_gxf_enable:
_gxf_enable:
fffffff008131e90 mov x0, #0x1
fffffff008131e94 msr S3_6_C15_C1_2, x0
fffffff008131e98 .long 0x00201420
在对S3_6_C15_C1_2寄存器赋值后,出现了0x00201420这个反汇编器无法识别的指令。那么这个有可能的两种情况,第一个假设0x00201420不是苹果cpu的新增指令,那么当cpu进行执行时,会产生exception异常,历史上,PAX团队曾经在异常处理代码里模拟了NX在32位处理器上的软件实现,但笔者翻遍了异常处理逻辑的所有代码,也没有发现对0x00201420这个数据或指令的特殊处理,因此可以断定0x00201420是苹果cpu新增的指令,在后面的reverse中,可以发现它是进入EL1_Guard level的指令,0x00201400则是退出EL1_Guard level的指令。
在前面的分析中讲到S3_6_C15_C8_1寄存器保存的地址是进入EL1_Guard level时首先要执行的地址,在前面的初始化时被设置为了_gxf_bootstrap_handler。
_gxf_bootstrap_handler ; DATA XREF: _bootstrap_instructions+9C↑o
MRS X0, #6, c15, c8, #3
TBNZ W0, #0, loc_FFFFFFF009811660
ADRL X0, _gxf_ppl_entry_handler ;
MSR #6, c15, c8, #1, X0
ADRL X0, _GuardedExceptionVectorsBase
MSR #0, c12, c0, #0, X0
首先判断S3_6_C15_C8_3寄存器的值是否为0,如果为0,就一直处于循环之中,可以推测出S3_6_C15_C8_3寄存器作用是进入EL1_Guard level的锁机制。然后将_gxf_ppl_entry_handler函数重新存入了S3_6_C15_C8_1寄存器,也就是说下次进入EL1_Guard level时将会执行_gxf_ppl_entry_handler函数。将_GuardedExceptionVectorsBase地址写入VBAR_EL1寄存器,也就是说当cpu在EL1_Guard level时,_GuardedExceptionVectorsBase函数负责其异常处理逻辑。
MRS X1, #0, c0, c0, #5
UBFX X2, X1, #8, #8
ADRL X3, _cluster_offsets
LDR X2, [X3,X2,LSL#3]
AND X1, X1, #0xFF
ADD X1, X1, X2
ADRL X2, _pmap_cpu_data_array
CMP X1, #6
B.CS loc_FFFFFFF0098116A8
MOV X3, #0x180
MADD X1, X1, X3, X2
首先从MPIDR_EL1寄存器提取了cpu的cluster id和cpu id,算出在_pmap_cpu_data_array数组中的索引。_pmap_cpu_data_array是一个struct pmap_cpu_data类型的数组。在XNU的source code中有如下定义:
struct pmap_cpu_data {
#if XNU_MONITOR
void * ppl_kern_saved_sp;
void * ppl_stack;
arm_context_t * save_area;
unsigned int ppl_state;
#endif
}
LDR X1, [X1,#0x10]
MOV SP, X1
X1保存的是当前cpu的struct pmap_cpu_data结构体指针,0x10偏移为save_area,它指向了arm_context_t类型的内存区域,将这个内存区域设置为EL1_Guard level的sp地址。在从el1进入EL1_Guard level时,将全部的寄存器保存在这个区域。
ADRP X1, #_pmap_ppl_locked_down@PAGE
LDR X2, [X1,#_pmap_ppl_locked_down@PAGEOFF]
CBZ X2, loc_FFFFFFF0098116C0
接着判断_pmap_ppl_locked_down值是否为0,如果为0,则一直循环检查下去。在前面的machine_lockdown函数中已经将_pmap_ppl_locked_down设置为了1,所以会继续运行下面的代码:
MOVK X0, #0x2020,LSL#48
MOVK X0, #0xA506,LSL#32
MOVK X0, #0xF020,LSL#16
MOVK X0, #0xF0E0
MSR #6, c15, c3, #0, X0
MOVK X0, #0,LSL#48
MOVK X0, #0,LSL#32
MOVK X0, #0,LSL#16
MOVK X0, #5
MSR #6, c15, c1, #2, X0
ADRL X0, _invalid_ttep
LDR X0, [X0]
MSR #0, c2, c0, #0, X0
将0x2020a506f020f0e0赋值给了S3_6_C15_C3_0寄存器,将0x5赋值给了S3_6_C15_C1_2寄存器,将_invalid_ttep页表地址赋值给了TTBR0_EL1。
SYS #0, c8, c3, #0
DSB ISH
ISB
DCD 0x201400
执行0x201400指令,退出EL1_Guard level。
至此ppl全部初始化已经完成, el1代码可以请求EL1_Guard level的服务了。
Ppl给el1代码提供了_gxf_ppl_enter接口用于请求其服务。
_arm_fast_fault_ppl:
mov x15, #0x0
b _gxf_ppl_enter
_arm_force_fast_fault_ppl:
mov x15, #0x1
b _gxf_ppl_enter
_mapping_free_prime_ppl:
mov x15, #0x2
b _gxf_ppl_enter
这里只罗列部分函数,x15保存的是ppl服务表的索引。
_gxf_ppl_enter:
fffffff008131c3c pacibsp
fffffff008131c40 stp x20, x21, [sp, #-0x20]! ; Latency: 6
fffffff008131c44 stp x29, x30, [sp, #0x10] ; Latency: 6
fffffff008131c48 add x29, sp, #0x10
fffffff008131c4c mrs x9, TPIDR_EL1
fffffff008131c50 ldr w10, [x9, #0x500] ; Latency: 4
fffffff008131c54 add w10, w10, #0x1
fffffff008131c58 str w10, [x9, #0x500] ; Latency: 4
fffffff008131c5c adrp x14, 5867 ; _pmap_ppl_locked_down
fffffff008131c60 add x14, x14, #0x130
fffffff008131c64 ldr x14, [x14] ; Latency: 4
fffffff008131c68 cbz x14, _ppl_bootstrap_dispatch
fffffff008131c6c mrs x14, S3_6_C15_C8_0
fffffff008131c70 cmp x14, #0x0
fffffff008131c74 b.ne 0xfffffff008131c74
fffffff008131c78 mov w10, #0x0
fffffff008131c7c .long 0x00201420
首先检查 _pmap_ppl_locked_down是否为0,如果为0,说明ppl并没有初始化完成,直接跳转到_ppl_bootstrap_dispatch函数:
CMP X15, #0x47 ; 'G'
B.CS loc_FFFFFFF008131DD4
ADRL X9, _ppl_handler_table
ADD X9, X9, X15,LSL#3
X15保存的是_ppl_handler_table的索引,如果大于0x47,会触发panic。
LDR X10, [X9]
BLRAA X10, X9
跳转到_ppl_handler_table[x15<<3]去执行具体的服务函数。
MOV X20, X0
BL __enable_preemption
MOV X0, X20
LDP X29, X30, [SP,#arg_10]
LDP X20, X21, [SP+arg_0],#0x20
RETAB
在没有进入EL1_Guard level情况下,直接通过ret返回。
如果_pmap_ppl_locked_down为1,则继续如下代码:
fffffff008131c78 mov w10, #0x0
fffffff008131c7c .long 0x00201420
将w10设置为0, 这个很重要,后面会讲到。
执行0x00201420进入EL1_Guard level, 前面在初始化时讲到cpu进入EL1_Guard level时,会执行S3_6_C15_C8_1寄存器指定的地址,也就是_gxf_ppl_entry_handler。
MSR #5, #0
MRS X9, #6, c15, c8, #3 ; S3_6_C15_C8_3
TBNZ W9, #0, loc_FFFFFFF008131C88
MRS X12, #6, c15, c9, #1 ; S3_6_C15_C9_1
MSR #0, c13, c0, #4, X12 ; TPIDR_EL1
MRS X20, #0, c4, c0, #0 ; SPSR_EL1
AND X20, X20, #0x3C0 ; mask FIQ; SET EL3t
B _ppl_trampoline_start
首先将SPSel设置为0,然后判断S3_6_C15_C8_3是否为1,否则一直循环等待,这再次证明S3_6_C15_C8_3寄存器为进入EL1_Guard level的锁机制。然后将S3_6_C15_C9_1寄存器的值写入TPIDR_EL1,说明它保存的是请求服务时的线程信息。将SPSR_EL1设置为mask FIQ; SET EL3t,为何是EL3t? 接着跳入_ppl_trampoline_start:
MOVK X14, #0x2020,LSL#48
MOVK X14, #0xA506,LSL#32
MOVK X14, #0xF020,LSL#16
MOVK X14, #0xF0E0 ; 0x2020a506f020f0e0
MRS X21, #6, c15, c3, #0
CMP X14, X21
B. NE loc_FFFFFFF00980D9AC
首先判断S3_6_C15_C3_0寄存器的值是否为0x2020a506f020f0e0,这个寄存器的作用笔者尚未搞清楚。
CMP X15, #0x47 ; 'G'
B.CS loc_FFFFFFF00980D9AC
再次判断x15合法范围。
MRS X12, #0, c0, c0, #5 ; MPIDR_EL1
UBFX X13, X12, #8, #8 ; get cluster id
ADRL X14, _cluster_offsets
LDR X13, [X14,X13,LSL#3] ; _cluster_offsets[index<<3]
AND X12, X12, #0xFF
ADD X12, X12, X13
ADRL X13, _pmap_cpu_data_array
CMP X12, #6
C. CS loc_FFFFFFF00980D92C
MOV X14, #0x180
MADD X12, X12, X14, X13
得到当前cpu的struct pmap_cpu_data结构体指针。
LDR W9, [X12,#0x18] ; _pmap_cpu_data->ppl_state
W9保存的是ppl的当前状态ppl_state,ppl的状态有3个定义:
PPL_STATE_KERNEL
PPL_STATE_DISPATCH
PPL_STATE_EXCEPTION
_ppl_trampoline_start这个函数可以由ppl_enter由el1代码主动调用,也可以由EL1_Guard level的异常处理调用,在后面会分析到。
CMP W9, #0 ; PPL_STATE_KERNEL
B. EQ loc_FFFFFFF00980D970
W9为0,即PPL_STATE_KERNEL,表示可以请求ppl服务。
loc_FFFFFFF00980D970 ; CODE XREF: _ppl_trampoline_start+60↑j
CMP W10, #0
判断w10是否为0,注意在前面的ppl_enter路径中,已经将w10设置为了0,所以代码可以进行前进。
B.NE loc_FFFFFFF00980D9AC
MOV W13, #1
STR W13, [X12,#0x18] ; enter PPL_STATE_DISPATCH mode
更改ppl_state状态为1,即 PPL_STATE_DISPATCH,表示ppl正处在服务派发状态中。
LDR X9, [X12,#8] ; _pmap_cpu_data->ppl_stack
MOV X21, SP
MOV SP, X9
_pmap_cpu_data->ppl_stack保存的是ppl服务要使用的stack区域。注意_pmap_cpu_data->save_area保存的是进入EL1_Guard level异常处理时保存全部寄存器用的栈区域。
STR X21, [X12] ; save old el1 sp
保存el1原来的sp到_pmap_cpu_data->ppl_kern_saved_sp,以后退出ppl服务时还要恢复原来的stack指针。
ADRL X9, _ppl_handler_table
ADD X9, X9, X15,LSL#3 ; _ppl_handler_table[index<<3]
LDR X10, [X9]
B _ppl_dispatch
X10保存了具体的服务函数地址_ppl_handler_table[x15<<3],然后跳转到_ppl_dispatch执行,在屏蔽了一些列中断后,进行执行:
BLRAA X10, X9
调用具体的ppl服务函数。
__PPLTEXT:__text:FFFFFFF00980D9B8
MOV X15, #0
MOV SP, X21
MOV X10, X20
STR XZR, [X12]
LDR W9, [X12,#0x18] ; _pmap_cpu_data->ppl_state
CMP W9, #1 ; PPL_STATE_DISPATCH
B.NE loc_FFFFFFF00980D9D0
MOV W9, #0
STR W9, [X12,#0x18] ; PPL_STATE_KERNEL
服务执行完之后,要把ppl_state再次设置 PPL_STATE_KERNEL。
B ppl_return_to_kernel_mode
最后调用ppl_return_to_kernel_mode退出EL1_Guard level,返回el1代码。
ADRL X14, ppl_exit
MSR #0, c4, c0, #1, X14 ; ELR_EL1
MRS X14, #6, c15, c8, #3
AND X14, X14, #0xFFFFFFFFFFFFFFFE
MSR #6, c15, c8, #3, X14 ; S3_6_C15_C8_3
MRS X14, #0, c4, c0, #0 ; SPSR_EL1
AND X14, X14, #0xFFFFFFFFFFFFFC3F
ORR X14, X14, X10
MSR #0, c4, c0, #0, X14 ; SPSR_EL1 -> EL3h
MRS X14, #0, c13, c0, #4 ; TPIDR_EL1
MSR #6, c15, c9, #1, X14 ; S3_6_C15_C9_1
DCQ 0x201400
首先将ELR_EL1设置为ppl_exit,然后将S3_6_C15_C8_3的最低位清0,然后将SPSR_EL1设置为 EL3h,注意SPSR_EL1的第2个bit设置为了1,在arm手册里这个bit是保留的,说明苹果处理器使用了这个bit代表ppl的执行状态,但是很奇怪为啥都设置为EL3h,而不是EL1h?随后将TPIDR_EL1值保存在了S3_6_C15_C9_1寄存器。
最后执行0x201400退出EL1_Guard level,由于ELR_EL1寄存器保存的是ppl_exit,cpu跳转到ppl_exit继续执行, ppl_exit恢复之前屏蔽的中断,然后使用ret指令返回。
上面分析的是一个正常通过ppl_enter请求ppl服务的路径。下面继续返回_ppl_trampoline_start代码。
CMP W9, #1 ; PPL_STATE_DISPATCH
C. EQ loc_FFFFFFF00980D9A4 ; _pmap_cpu_data_array->ppl_kern_saved_sp
如果当前ppl_state状态为PPL_STATE_DISPATCH,表示有其他cpu正在请求ppl服务,因此恢复之前通过_pmap_cpu_data_array->ppl_kern_saved_sp保存的栈,然后退出EL1_Guard level,前面的路径已经分析过了。
CMP W9, #3 ; PPL_STATE_EXCEPTION
B. NE loc_FFFFFFF00980D9AC
如果ppl_state也不是PPL_STATE_EXCEPTION,则直接退出EL1_Guard level。
CMP W10, #3 ; PPL_STATE_EXCEPTION
C. NE loc_FFFFFFF00980D9AC
MOV W9, #1
STR W9, [X12,#0x18] ; PPL_STATE_DISPATCH
LDR X0, [X12,#0x10] ; _pmap_cpu_data_array->save_area
MOV SP, X0
B _return_to_ppl
将sp设置为_pmap_cpu_data_array->save_area,_return_to_ppl要使用这个栈恢复之前进入EL1_Guard level异常处理时保存的全部寄存器值,恢复cpsr, ELR_EL1设置原来的调用地址,恢复bp,sp,通过eret返回原来的el1路径中。
为啥要判断w10的值是否为3?这个路径表示的是EL1_Guard level异常处理路径中要请求ppl服务。
_GuardedExceptionVectorsBase是EL1_Guard level异常处理地址,我们看到它只对0x000的offset使用了有效函数,表明它只处理来自同层current level的异常处理,再次证明EL1_Guard level也是在EL1 level上。
__PPLTEXT:__text:FFFFFFF0098114B0 gtr_sync:
MOV X26, #1
ADRL X1, _fleh_synchronous ; ESR_EL1
MSR #0, c4, c0, #1, X1 ; ELR_EL1
可以看到gtr_sync把_fleh_synchronous赋值给了ELR_EL1, x26设置为1很关键,这是一个标识,后面会讲到。
MOV X0, SP
DCD 0x201400
随后gtr_sync使用0x201400指令退出了EL1_Guard level,转而执行el1 _fleh_synchronous函数,这是el1内核异常处理的主要函数,这说明EL1_Guard level的主要处理逻辑会使用el1内核代码,这也就解释了ppl为什么能保护内核态和用户态的数据。以前我们在使用el2做保护时,只能保护el1的数据,因为el1的使用的代码和数据都已经是映射好的,而el0层用户的代码和数据会涉及到很多缺页异常处理的逻辑,比如页表不存在,或者页表被交换到磁盘上,这会使el2的异常处理逻辑非常复杂。EL1_Guard level使用el1内核代码的处理逻辑就简化了自身的代码逻辑,性能也会提升不少。
__TEXT_EXEC:__text:FFFFFFF0081315CC _fleh_synchronous
BL _sleh_synchronous
_fleh_synchronous又调用了_sleh_synchronous,这个函数是异常处理逻辑的主要处理函数。
CMP X26, XZR
B. EQ loc_FFFFFFF00813161C
注意_fleh_synchronous是el1内核代码的处理逻辑,EL1_Guard level是借用了它的代码,在前面看到gtr_sync把x26设置为了1,在_fleh_synchronous函数里表示此次请求来自EL1_Guard level。
MOV X15, #0
MOV W10, #3
DCB 0x20, 0x14, 0x20, 0
然后将x15设置为0,w10设置为3,最后调用0x201420再次进入EL1_Guard level,此时就会跳转到_ppl_trampoline_start的异常处理请求路径里:
CMP W9, #3 ; PPL_STATE_EXCEPTION
B.NE loc_FFFFFFF00980D9AC
CMP W10, #3 ; PPL_STATE_EXCEPTION
B.NE loc_FFFFFFF00980D9AC
MOV W9, #1
STR W9, [X12,#0x18] ; PPL_STATE_DISPATCH
LDR X0, [X12,#0x10] ; _pmap_cpu_data_array->save_area
MOV SP, X0
B _return_to_ppl
_return_to_ppl恢复EL1_Guard level异常处理时保存的全部寄存器值,恢复cpsr, ELR_EL1设置原来的调用地址,恢复bp,sp,通过eret返回原来的el1路径中。
_ppl_handler_table数组保存的是ppl服务函数地址。
_ppl_handler_table:
_arm_force_fast_fault_internal
_mapping_free_prime_internal
_phys_attribute_clear_internal
_phys_attribute_set_internal
_pmap_batch_set_cache_attributes_internal
_pmap_change_wiring_internal
_pmap_create_options_internal
_pmap_destroy_internal
_pmap_enter_options_internal
_pmap_find_pa_internal
_pmap_insert_sharedpage_internal
_pmap_is_empty_internal
_pmap_map_cpu_windows_copy_internal
_pmap_mark_page_as_ppl_page_internal
_pmap_nest_internal
_pmap_page_protect_options_internal
_pmap_protect_options_internal
_pmap_query_page_info_internal
_pmap_query_resident_internal
_pmap_reference_internal
_pmap_remove_options_internal
_pmap_return_internal
_pmap_set_cache_attributes_internal
_pmap_set_nested_internal
_pmap_set_process_internal
_pmap_switch_internal
_pmap_switch_user_ttb_internal
_pmap_clear_user_ttb_internal
_pmap_unmap_cpu_windows_copy_internal
_pmap_unnest_options_internal
_pmap_footprint_suspend_internal
_pmap_cpu_data_init_internal
_pmap_release_ppl_pages_to_kernel_internal
_pmap_set_jit_entitled_internal
_pmap_load_legacy_trust_cache_internal
_pmap_load_image4_trust_cache_internal
_pmap_is_trust_cache_loaded_internal
_pmap_lookup_in_static_trust_cache_internal
_pmap_lookup_in_loaded_trust_caches_internal
_pmap_cs_cd_register_internal
_pmap_cs_cd_unregister_internal
_pmap_cs_associate_internal
_pmap_cs_lookup_internal
_pmap_cs_check_overlap_internal
_pmap_iommu_init_internal
_pmap_iommu_iovmalloc_internal
_pmap_iommu_map_internal
_pmap_iommu_unmap_internal
_pmap_iommu_iovmfree_internal
_pmap_iommu_ioctl_internal
_pmap_iommu_grant_page_internal
_pmap_update_compressor_page_internal
_pmap_trim_internal
_pmap_ledger_alloc_init_internal
_pmap_ledger_alloc_internal
_pmap_ledger_free_internal
_pmap_sign_user_ptr_internal
_pmap_auth_user_ptr_internal
_phys_attribute_clear_range_internal
可以看到ppl服务代码是非常复杂的,覆盖了物理内存和虚拟内存管理的方方面面。
我们先看下如何将一个物理页标记为ppl使用的物理页。
_pmap_mark_page_as_ppl_page_internal:
MOV X19, X0
ADRP X23, #_vm_first_phys@PAGE ; x0(pa) > vm_first_phys && x0 < vm_last_phys
LDR X8, [X23,#_vm_first_phys@PAGEOFF]
ADRP X24, #_vm_last_phys@PAGE
LDR X9, [X24,#_vm_last_phys@PAGEOFF]
CMP X8, X0
CCMP X9, X0, #0, LS
C. LS loc_FFFFFFF0097FB5B0
X0保存的是要标记的物理地址pa,然后检查它是否在_vm_first_phys与_vm_last_phys之间,这是XNU管理的有效物理内存区间。
LDR X11, [X9,#_pp_attr_table@PAGEOFF]
LDRSH W10, [X11,X8,LSL#1]
TBNZ W10, #0x1F, loc_FFFFFFF0097FB580 ;
ORR W12, W10, #0x4000 ; pp_attr_table[pai] & 0x4000
ADD X11, X11, X8,LSL#1
MOV X13, X10
CASALH W13, W12, [X11]
pp_attr_table[pai] &= 0x4000,将pa对应的pp_attr_table进行标记。
LDR X9, [X21,#_pv_head_table@PAGEOFF]
ADD X8, X9, X8,LSL#3
ADD X8, X8, #4
MOV W9, #0x20000000
LDCLRL W9, W8, [X8] ;
_pv_head_table[index] &= 0x20000000
MOV X0, X19
BL _phystokv
ADD X1, X0, #4,LSL#12
MOV W2, #3
MOV W3, #1
BL _pmap_set_range_xprr_perm
在对pp_attr_table和_pv_head_table进行标记后,调用_pmap_set_range_xprr_perm函数,这个函数作用是将虚拟地址va,到va + size之间的所有虚拟地址对应的页表属性进行转换,第三个参数代表的是原来页表的权限属性,第四个参数代表的是新的页表属性,这个函数用来在内核页表和ppl页表之间进行权限转换。
首先检查要进行转换的虚拟地址合法范围,只能在physmap_base和physmap_end或者gVirtBase和static_memory_end之间。接下来就是要循环遍历l3页表,l0页表基地址保存在kernel_pmap->tte,这个页表保存的是内核使用的页表地址,在内核启动时进行初始化。然后找到虚拟地址对应的l3 table地址, 循环遍历它的每个页表项,在这之前还需要将虚拟地址转换为物理地址,然后找到物理地址对应的pv_head_table表项,将其上锁。然后开始提取l3页表项的原始权限,请看如下代码逻辑:
LDR X8, [X20] ; 获得pte的内容
TBZ W8, #1, loc_FFFFFFF007B598C4 ;判断页表有效位
TBNZ X8, #0x34, loc_FFFFFFF007B598FC ; '4' ; ARM_PTE_HINT_MASK
LSR X9, X8, #4
AND X9, X9, #0xC ; (*pte >> 4) & 0xc; 提取ap权限
LSR X10, X8, #0x35 ; '5' ; *pte >> 53,提取xn和pxn权限
BFXIL X9, X8, #0x35, #1 ; '5'
AND X10, X10, #2 ; (*pte >> 53) & 2
ORR X9, X9, X10
CMP X9, X26 ; 与函数的第三个参数expected_perm比较
B. NE loc_FFFFFFF007B59938
通过上面的提取后获得一个4bit的权限值,高2位代表ap权限,最低位代表pxn权限,第2bit代表xn权限。
所以可以推断出ppl新的权限模型为4个bit:
new_perm
X XXX
/ / \ \
/ / \ \
54 53 7 6
+-------------------------------------------------------
| |xn|pxn| ... |ap| | |
--------------------------------------------------------
El1和EL1_Guard level对应的权限关系可以参考m1n1的dock:
HW: SPRR and GXF · AsahiLinux/docs Wiki · GitHub
通过搜索代码,发现有以下几个函数调用了_pmap_set_range_xprr_perm:
_pmap_mark_page_as_kernel_page:
MOV W2, #1
MOV W3, #3
B _pmap_set_range_xprr_perm
_pmap_mark_page_as_ppl_page_internal:
MOV W2, #3
MOV W3, #1
BL _pmap_set_range_xprr_perm
可以看到将普通的内核权限转化为ppl权限,是将3替换为1。反过来则是1替换为3。我们可以推测3为RW读写权限,1则为R只读权限。
_pmap_static_allocations_done:
MOV W2, #3
MOV W3, #0xB
BL _pmap_set_range_xprr_perm
将_bootstrap_pagetables从0x3设置为0xb,_bootstrap_pagetables是内核启动时用到的临时页表。
MOV W2, #3
MOV W3, #1
BL _pmap_set_range_xprr_perm ;
将_BootArgs从0x3设置为0x1,_BootArgs是iboot加载内核时传给内核的参数。
MOV W2, #0x3
MOV W3, #1
BL _pmap_set_range_xprr_perm ;
将_segPPLDATAB从0x3设置为0x1,_segPPLDATAB为ppl的data段。
MOV W2, #0xA
MOV W3, #0x8
BL _pmap_set_range_xprr_perm ;
将_segPPLTEXTB从0xA设置为0x8,_segPPLTEXTB为ppl的text段。
MOV W2, #0xB
MOV W3, #0xB
BL _pmap_set_range_xprr_perm ;
将_segPPLDATACONSTB从0xB设置为0xB,_segPPLDATACONSTB为ppl的dataconst段。
MOV W2, #0x1
MOV W3, #0xB
BL _pmap_set_range_xprr_perm ;
将_pmap_stacks_start_pa从0x1设置为0xB, _pmap_stacks_start_pa为ppl使用的stack区域。
_pmap_ledger_alloc_internal:
MOV W2, #1
MOV W3, #3
BL _pmap_set_range_xprr_perm ;
_pmap_load_image4_trust_cache_internal:
MOV W2, #0xB
MOV W3, #1
BL _pmap_set_range_xprr_perm
MOV W2, #1
MOV W3, #0xB
BL _pmap_set_range_xprr_perm
它与_pmap_mark_page_as_ppl_page_internal是一个相反的操作,读者朋友可以自行阅读相关代码。