作者: wzt
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:paper@seebug.org

Ppl代码初始化

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_startv保存的是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物理内存布局图,请原谅我的懒惰,不想画漂亮的图形。

图片

pp_attr_table与pv_head_table

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_MONITORPP_ATTR_NO_MONITOR,ios使用相同的值,后面会看到。

每个物理页在内核中有两种用处,第一个物理页保存的是页表内容,第二个就是保存内核用到的其他数据结构,在XNU的物理内存管理模型中,使用struct pv_entry结构体保存l3页表项的地址,可以提高物理内存与虚拟内存相互转化的效率。使用struct pt_desc结构体描述一个页表属性,比如l1、l2、l3table的属性。对于ppl,会用到pv_head_table对给定的一个物理页上锁。

Pv_head_table的内存布局如下,请再次原谅我的懒惰。

图片

EL1_Guard level初始化

在初始化完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进入与退出

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服务。

_GuardedExceptionVectorsBaseEL1_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服务

_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服务代码是非常复杂的,覆盖了物理内存和虚拟内存管理的方方面面。

_pmap_mark_page_as_ppl_page_internal

我们先看下如何将一个物理页标记为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_basephysmap_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_release_ppl_pages_to_kernel_internal

它与_pmap_mark_page_as_ppl_page_internal是一个相反的操作,读者朋友可以自行阅读相关代码。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1768/