Commit 127ccd17 authored by johpow01's avatar johpow01 Committed by Mark Dykes
Browse files

feat(RME): BL31 changes



The following is introduced in the patch
1. Enable GPT translation tables
2. Adds a new context for Realm world and adds realm world awareness in context management
3. Initializes CPU registers in context management for Realm world
Signed-off-by: default avatarJohn Powell <john.powell@arm.com>
Change-Id: I5bc8f3413144fb63d9eb4f5885abdcf5a44b4db1
parent 65412d77
...@@ -172,6 +172,14 @@ func bl31_warm_entrypoint ...@@ -172,6 +172,14 @@ func bl31_warm_entrypoint
_exception_vectors=runtime_exceptions \ _exception_vectors=runtime_exceptions \
_pie_fixup_size=0 _pie_fixup_size=0
#if ENABLE_RME
/*
* Initialise and enable Granule Protection
* before enabling any stage of translation.
*/
bl gpt_enable
#endif
/* /*
* We're about to enable MMU and participate in PSCI state coordination. * We're about to enable MMU and participate in PSCI state coordination.
* *
......
...@@ -500,6 +500,14 @@ smc_handler64: ...@@ -500,6 +500,14 @@ smc_handler64:
stp x16, x17, [x6, #CTX_EL3STATE_OFFSET + CTX_SPSR_EL3] stp x16, x17, [x6, #CTX_EL3STATE_OFFSET + CTX_SPSR_EL3]
str x18, [x6, #CTX_EL3STATE_OFFSET + CTX_SCR_EL3] str x18, [x6, #CTX_EL3STATE_OFFSET + CTX_SCR_EL3]
#if ENABLE_RME
/* Copy SCR_EL3.NSE bit to the flag to indicate caller's security */
ubfx x7, x18, #SCR_NSE_SHIFT, 1
/* Shift copied SCR_EL3.NSE bit by 1 to create space for SCR_EL3.NS bit */
lsl x7, x7, #1
#endif /* ENABLE_RME */
/* Copy SCR_EL3.NS bit to the flag to indicate caller's security */ /* Copy SCR_EL3.NS bit to the flag to indicate caller's security */
bfi x7, x18, #0, #1 bfi x7, x18, #0, #1
......
...@@ -95,6 +95,13 @@ BL31_SOURCES += lib/cpus/aarch64/wa_cve_2017_5715_bpiall.S \ ...@@ -95,6 +95,13 @@ BL31_SOURCES += lib/cpus/aarch64/wa_cve_2017_5715_bpiall.S \
lib/cpus/aarch64/wa_cve_2017_5715_mmu.S lib/cpus/aarch64/wa_cve_2017_5715_mmu.S
endif endif
ifeq (${ENABLE_RME},1)
include lib/gpt/gpt.mk
BL31_SOURCES += ${GPT_LIB_SRCS} \
${RMMD_SOURCES}
endif
BL31_LINKERFILE := bl31/bl31.ld.S BL31_LINKERFILE := bl31/bl31.ld.S
# Flag used to indicate if Crash reporting via console should be included # Flag used to indicate if Crash reporting via console should be included
......
...@@ -19,7 +19,11 @@ ...@@ -19,7 +19,11 @@
******************************************************************************/ ******************************************************************************/
void *cm_get_context(uint32_t security_state) void *cm_get_context(uint32_t security_state)
{ {
#if ENABLE_RME
assert(sec_state_is_valid(security_state));
#else /* ENABLE_RME */
assert(security_state <= NON_SECURE); assert(security_state <= NON_SECURE);
#endif
return get_cpu_data(cpu_context[security_state]); return get_cpu_data(cpu_context[security_state]);
} }
...@@ -30,7 +34,11 @@ void *cm_get_context(uint32_t security_state) ...@@ -30,7 +34,11 @@ void *cm_get_context(uint32_t security_state)
******************************************************************************/ ******************************************************************************/
void cm_set_context(void *context, uint32_t security_state) void cm_set_context(void *context, uint32_t security_state)
{ {
#if ENABLE_RME
assert(sec_state_is_valid(security_state));
#else /* ENABLE_RME */
assert(security_state <= NON_SECURE); assert(security_state <= NON_SECURE);
#endif
set_cpu_data(cpu_context[security_state], context); set_cpu_data(cpu_context[security_state], context);
} }
......
...@@ -406,6 +406,15 @@ DEFINE_REG_STRUCT(pauth, CTX_PAUTH_REGS_ALL); ...@@ -406,6 +406,15 @@ DEFINE_REG_STRUCT(pauth, CTX_PAUTH_REGS_ALL);
* to ensure that SP_EL3 always points to an instance of this * to ensure that SP_EL3 always points to an instance of this
* structure at exception entry and exit. Each instance will * structure at exception entry and exit. Each instance will
* correspond to either the secure or the non-secure state. * correspond to either the secure or the non-secure state.
*
* For systems supporting RME:
*
* Top-level context structure which is used by EL3 firmware to preserve
* the state of a core at the next lower EL in a given security state and
* save enough EL3 meta data to be able to return to that EL and security
* state. The context management library will be used to ensure that
* SP_EL3 always points to an instance of this structure at exception
* entry and exit.
*/ */
typedef struct cpu_context { typedef struct cpu_context {
gp_regs_t gpregs_ctx; gp_regs_t gpregs_ctx;
......
...@@ -20,15 +20,27 @@ ...@@ -20,15 +20,27 @@
#define PSCI_CPU_DATA_SIZE_ALIGNED ((PSCI_CPU_DATA_SIZE + 7) & ~7) #define PSCI_CPU_DATA_SIZE_ALIGNED ((PSCI_CPU_DATA_SIZE + 7) & ~7)
/* Offset of cpu_ops_ptr, size 8 bytes */ /* Offset of cpu_ops_ptr, size 8 bytes */
#if ENABLE_RME
#define CPU_DATA_CPU_OPS_PTR 0x18
#else /* ENABLE_RME */
#define CPU_DATA_CPU_OPS_PTR 0x10 #define CPU_DATA_CPU_OPS_PTR 0x10
#endif /* ENABLE_RME */
#if ENABLE_PAUTH #if ENABLE_PAUTH
/* 8-bytes aligned offset of apiakey[2], size 16 bytes */ /* 8-bytes aligned offset of apiakey[2], size 16 bytes */
#define CPU_DATA_APIAKEY_OFFSET (0x18 + PSCI_CPU_DATA_SIZE_ALIGNED) #if ENABLE_RME
#define CPU_DATA_CRASH_BUF_OFFSET (CPU_DATA_APIAKEY_OFFSET + 0x10) #define CPU_DATA_APIAKEY_OFFSET (0x20 + PSCI_CPU_DATA_SIZE_ALIGNED)
#else #else /* ENABLE_RME */
#define CPU_DATA_APIAKEY_OFFSET (0x18 + PSCI_CPU_DATA_SIZE_ALIGNED)
#endif /* ENABLE_RME */
#define CPU_DATA_CRASH_BUF_OFFSET (0x10 + CPU_DATA_APIAKEY_OFFSET)
#else /* ENABLE_PAUTH */
#if ENABLE_RME
#define CPU_DATA_CRASH_BUF_OFFSET (0x20 + PSCI_CPU_DATA_SIZE_ALIGNED)
#else /* ENABLE_RME */
#define CPU_DATA_CRASH_BUF_OFFSET (0x18 + PSCI_CPU_DATA_SIZE_ALIGNED) #define CPU_DATA_CRASH_BUF_OFFSET (0x18 + PSCI_CPU_DATA_SIZE_ALIGNED)
#endif /* ENABLE_PAUTH */ #endif /* ENABLE_RME */
#endif /* ENABLE_PAUTH */
/* need enough space in crash buffer to save 8 registers */ /* need enough space in crash buffer to save 8 registers */
#define CPU_DATA_CRASH_BUF_SIZE 64 #define CPU_DATA_CRASH_BUF_SIZE 64
...@@ -86,21 +98,23 @@ ...@@ -86,21 +98,23 @@
/******************************************************************************* /*******************************************************************************
* Cache of frequently used per-cpu data: * Cache of frequently used per-cpu data:
* Pointers to non-secure and secure security state contexts * Pointers to non-secure, realm, and secure security state contexts
* Address of the crash stack * Address of the crash stack
* It is aligned to the cache line boundary to allow efficient concurrent * It is aligned to the cache line boundary to allow efficient concurrent
* manipulation of these pointers on different cpus * manipulation of these pointers on different cpus
* *
* TODO: Add other commonly used variables to this (tf_issues#90)
*
* The data structure and the _cpu_data accessors should not be used directly * The data structure and the _cpu_data accessors should not be used directly
* by components that have per-cpu members. The member access macros should be * by components that have per-cpu members. The member access macros should be
* used for this. * used for this.
******************************************************************************/ ******************************************************************************/
typedef struct cpu_data { typedef struct cpu_data {
#ifdef __aarch64__ #ifdef __aarch64__
#if ENABLE_RME
void *cpu_context[3];
#else /* ENABLE_RME */
void *cpu_context[2]; void *cpu_context[2];
#endif #endif /* ENABLE_RME */
#endif /* __aarch64__ */
uintptr_t cpu_ops_ptr; uintptr_t cpu_ops_ptr;
struct psci_cpu_data psci_svc_cpu_data; struct psci_cpu_data psci_svc_cpu_data;
#if ENABLE_PAUTH #if ENABLE_PAUTH
......
...@@ -94,6 +94,8 @@ ...@@ -94,6 +94,8 @@
/* Various flags passed to SMC handlers */ /* Various flags passed to SMC handlers */
#define SMC_FROM_SECURE (U(0) << 0) #define SMC_FROM_SECURE (U(0) << 0)
#define SMC_FROM_NON_SECURE (U(1) << 0) #define SMC_FROM_NON_SECURE (U(1) << 0)
#define SMC_FROM_REALM (U(3) << 0)
#define SMC_FROM_MASK U(3)
#ifndef __ASSEMBLER__ #ifndef __ASSEMBLER__
...@@ -101,8 +103,15 @@ ...@@ -101,8 +103,15 @@
#include <lib/cassert.h> #include <lib/cassert.h>
#if ENABLE_RME
#define is_caller_non_secure(_f) (((_f) & SMC_FROM_MASK) == SMC_FROM_NON_SECURE)
#define is_caller_secure(_f) (((_f) & SMC_FROM_MASK) == SMC_FROM_SECURE)
#define is_caller_realm(_f) (((_f) & SMC_FROM_MASK) == SMC_FROM_REALM)
#define caller_sec_state(_f) ((_f) & SMC_FROM_MASK)
#else /* ENABLE_RME */
#define is_caller_non_secure(_f) (((_f) & SMC_FROM_NON_SECURE) != U(0)) #define is_caller_non_secure(_f) (((_f) & SMC_FROM_NON_SECURE) != U(0))
#define is_caller_secure(_f) (!is_caller_non_secure(_f)) #define is_caller_secure(_f) (!is_caller_non_secure(_f))
#endif /* ENABLE_RME */
/* The macro below is used to identify a Standard Service SMC call */ /* The macro below is used to identify a Standard Service SMC call */
#define is_std_svc_call(_fid) (GET_SMC_OEN(_fid) == OEN_STD_START) #define is_std_svc_call(_fid) (GET_SMC_OEN(_fid) == OEN_STD_START)
......
...@@ -23,9 +23,11 @@ ...@@ -23,9 +23,11 @@
#include <lib/extensions/spe.h> #include <lib/extensions/spe.h>
#include <lib/extensions/sve.h> #include <lib/extensions/sve.h>
#include <lib/extensions/twed.h> #include <lib/extensions/twed.h>
#if ENABLE_RME
#include <lib/gpt/gpt.h>
#endif
#include <lib/utils.h> #include <lib/utils.h>
/******************************************************************************* /*******************************************************************************
* Context management library initialisation routine. This library is used by * Context management library initialisation routine. This library is used by
* runtime services to share pointers to 'cpu_context' structures for the secure * runtime services to share pointers to 'cpu_context' structures for the secure
...@@ -70,6 +72,10 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep) ...@@ -70,6 +72,10 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep)
gp_regs_t *gp_regs; gp_regs_t *gp_regs;
u_register_t sctlr_elx, actlr_elx; u_register_t sctlr_elx, actlr_elx;
#if ENABLE_RME
u_register_t hcr_el2;
#endif /* ENABLE_RME */
assert(ctx != NULL); assert(ctx != NULL);
security_state = GET_SECURITY_STATE(ep->h.attr); security_state = GET_SECURITY_STATE(ep->h.attr);
...@@ -89,24 +95,47 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep) ...@@ -89,24 +95,47 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep)
scr_el3 = read_scr(); scr_el3 = read_scr();
scr_el3 &= ~(SCR_NS_BIT | SCR_RW_BIT | SCR_FIQ_BIT | SCR_IRQ_BIT | scr_el3 &= ~(SCR_NS_BIT | SCR_RW_BIT | SCR_FIQ_BIT | SCR_IRQ_BIT |
SCR_ST_BIT | SCR_HCE_BIT); SCR_ST_BIT | SCR_HCE_BIT);
#if ENABLE_RME
/* When RME support is enabled, clear the NSE bit as well. */
scr_el3 &= ~SCR_NSE_BIT;
#endif /* ENABLE_RME */
/* /*
* SCR_NS: Set the security state of the next EL. * SCR_NS: Set the security state of the next EL.
*/ */
if (security_state != SECURE) if (security_state == NON_SECURE) {
scr_el3 |= SCR_NS_BIT; scr_el3 |= SCR_NS_BIT;
}
#if ENABLE_RME
/* Check for realm state if RME support enabled. */
if (security_state == REALM) {
scr_el3 |= SCR_NS_BIT | SCR_NSE_BIT;
/*
* RMM relies on EL3 to setup HCR_EL2 for page table setup.
*/
hcr_el2 = HCR_TEA_BIT | HCR_E2H_BIT | HCR_TGE_BIT;
write_ctx_reg(get_el2_sysregs_ctx(ctx), CTX_HCR_EL2, hcr_el2);
}
#endif /* ENABLE_RME */
/* /*
* SCR_EL3.RW: Set the execution state, AArch32 or AArch64, for next * SCR_EL3.RW: Set the execution state, AArch32 or AArch64, for next
* Exception level as specified by SPSR. * Exception level as specified by SPSR.
*/ */
if (GET_RW(ep->spsr) == MODE_RW_64) if (GET_RW(ep->spsr) == MODE_RW_64) {
scr_el3 |= SCR_RW_BIT; scr_el3 |= SCR_RW_BIT;
}
/* /*
* SCR_EL3.ST: Traps Secure EL1 accesses to the Counter-timer Physical * SCR_EL3.ST: Traps Secure EL1 accesses to the Counter-timer Physical
* Secure timer registers to EL3, from AArch64 state only, if specified * Secure timer registers to EL3, from AArch64 state only, if specified
* by the entrypoint attributes. * by the entrypoint attributes.
*/ */
if (EP_GET_ST(ep->h.attr) != 0U) if (EP_GET_ST(ep->h.attr) != 0U) {
scr_el3 |= SCR_ST_BIT; scr_el3 |= SCR_ST_BIT;
}
#if RAS_TRAP_LOWER_EL_ERR_ACCESS #if RAS_TRAP_LOWER_EL_ERR_ACCESS
/* /*
...@@ -140,8 +169,9 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep) ...@@ -140,8 +169,9 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep)
* If the Secure world wants to use pointer authentication, * If the Secure world wants to use pointer authentication,
* CTX_INCLUDE_PAUTH_REGS must be set to 1. * CTX_INCLUDE_PAUTH_REGS must be set to 1.
*/ */
if (security_state == NON_SECURE) if (security_state == NON_SECURE) {
scr_el3 |= SCR_API_BIT | SCR_APK_BIT; scr_el3 |= SCR_API_BIT | SCR_APK_BIT;
}
#endif /* !CTX_INCLUDE_PAUTH_REGS */ #endif /* !CTX_INCLUDE_PAUTH_REGS */
#if !CTX_INCLUDE_MTE_REGS || ENABLE_ASSERTIONS #if !CTX_INCLUDE_MTE_REGS || ENABLE_ASSERTIONS
...@@ -178,7 +208,8 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep) ...@@ -178,7 +208,8 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep)
* indicated by the interrupt routing model for BL31. * indicated by the interrupt routing model for BL31.
*/ */
scr_el3 |= get_scr_el3_from_routing_model(security_state); scr_el3 |= get_scr_el3_from_routing_model(security_state);
#endif
#endif /* IMAGE_BL31 */
/* /*
* SCR_EL3.HCE: Enable HVC instructions if next execution state is * SCR_EL3.HCE: Enable HVC instructions if next execution state is
...@@ -206,10 +237,18 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep) ...@@ -206,10 +237,18 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep)
} }
} }
#if ENABLE_RME
/*
* RME model uses S-EL2 to emulate R-EL2 so EEL2 bit must be set when
* EL3 ERETs to R-EL2.
*/
if ((security_state != NON_SECURE) && (GET_EL(ep->spsr) == MODE_EL2)) {
#else
/* Enable S-EL2 if the next EL is EL2 and security state is secure */ /* Enable S-EL2 if the next EL is EL2 and security state is secure */
if ((security_state == SECURE) && (GET_EL(ep->spsr) == MODE_EL2)) { if ((security_state == SECURE) && (GET_EL(ep->spsr) == MODE_EL2)) {
#endif /* ENABLE_RME */
if (GET_RW(ep->spsr) != MODE_RW_64) { if (GET_RW(ep->spsr) != MODE_RW_64) {
ERROR("S-EL2 can not be used in AArch32."); ERROR("S-EL2 cannot be used in AArch32.");
panic(); panic();
} }
...@@ -238,9 +277,9 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep) ...@@ -238,9 +277,9 @@ void cm_setup_context(cpu_context_t *ctx, const entry_point_info_t *ep)
* required by PSCI specification) * required by PSCI specification)
*/ */
sctlr_elx = (EP_GET_EE(ep->h.attr) != 0U) ? SCTLR_EE_BIT : 0U; sctlr_elx = (EP_GET_EE(ep->h.attr) != 0U) ? SCTLR_EE_BIT : 0U;
if (GET_RW(ep->spsr) == MODE_RW_64) if (GET_RW(ep->spsr) == MODE_RW_64) {
sctlr_elx |= SCTLR_EL1_RES1; sctlr_elx |= SCTLR_EL1_RES1;
else { } else {
/* /*
* If the target execution state is AArch32 then the following * If the target execution state is AArch32 then the following
* fields need to be set. * fields need to be set.
...@@ -370,7 +409,8 @@ void cm_init_my_context(const entry_point_info_t *ep) ...@@ -370,7 +409,8 @@ void cm_init_my_context(const entry_point_info_t *ep)
} }
/******************************************************************************* /*******************************************************************************
* Prepare the CPU system registers for first entry into secure or normal world * Prepare the CPU system registers for first entry into realm, secure, or
* normal world.
* *
* If execution is requested to EL2 or hyp mode, SCTLR_EL2 is initialized * If execution is requested to EL2 or hyp mode, SCTLR_EL2 is initialized
* If execution is requested to non-secure EL1 or svc mode, and the CPU supports * If execution is requested to non-secure EL1 or svc mode, and the CPU supports
...@@ -452,7 +492,7 @@ void cm_prepare_el3_exit(uint32_t security_state) ...@@ -452,7 +492,7 @@ void cm_prepare_el3_exit(uint32_t security_state)
* architecturally UNKNOWN on reset and are set to zero * architecturally UNKNOWN on reset and are set to zero
* except for field(s) listed below. * except for field(s) listed below.
* *
* CNTHCTL_EL2.EL1PCEN: Set to one to disable traps to * CNTHCTL_EL2.EL1PTEN: Set to one to disable traps to
* Hyp mode of Non-secure EL0 and EL1 accesses to the * Hyp mode of Non-secure EL0 and EL1 accesses to the
* physical timer registers. * physical timer registers.
* *
...@@ -461,7 +501,8 @@ void cm_prepare_el3_exit(uint32_t security_state) ...@@ -461,7 +501,8 @@ void cm_prepare_el3_exit(uint32_t security_state)
* physical counter registers. * physical counter registers.
*/ */
write_cnthctl_el2(CNTHCTL_RESET_VAL | write_cnthctl_el2(CNTHCTL_RESET_VAL |
EL1PCEN_BIT | EL1PCTEN_BIT); CNTHCTL_EL1PTEN_BIT |
CNTHCTL_EL1PCTEN_BIT);
/* /*
* Initialise CNTVOFF_EL2 to zero as it resets to an * Initialise CNTVOFF_EL2 to zero as it resets to an
...@@ -598,6 +639,9 @@ void cm_el2_sysregs_context_save(uint32_t security_state) ...@@ -598,6 +639,9 @@ void cm_el2_sysregs_context_save(uint32_t security_state)
* S-EL2 context if S-EL2 is enabled. * S-EL2 context if S-EL2 is enabled.
*/ */
if ((security_state == NON_SECURE) || if ((security_state == NON_SECURE) ||
#if ENABLE_RME
(security_state == REALM) ||
#endif /* ENABLE_RME */
((security_state == SECURE) && ((scr_el3 & SCR_EEL2_BIT) != 0U))) { ((security_state == SECURE) && ((scr_el3 & SCR_EEL2_BIT) != 0U))) {
cpu_context_t *ctx; cpu_context_t *ctx;
...@@ -620,6 +664,9 @@ void cm_el2_sysregs_context_restore(uint32_t security_state) ...@@ -620,6 +664,9 @@ void cm_el2_sysregs_context_restore(uint32_t security_state)
* S-EL2 context if S-EL2 is enabled. * S-EL2 context if S-EL2 is enabled.
*/ */
if ((security_state == NON_SECURE) || if ((security_state == NON_SECURE) ||
#if ENABLE_RME
(security_state == REALM) ||
#endif /* ENABLE_RME */
((security_state == SECURE) && ((scr_el3 & SCR_EEL2_BIT) != 0U))) { ((security_state == SECURE) && ((scr_el3 & SCR_EEL2_BIT) != 0U))) {
cpu_context_t *ctx; cpu_context_t *ctx;
...@@ -646,10 +693,18 @@ void cm_el1_sysregs_context_save(uint32_t security_state) ...@@ -646,10 +693,18 @@ void cm_el1_sysregs_context_save(uint32_t security_state)
el1_sysregs_context_save(get_el1_sysregs_ctx(ctx)); el1_sysregs_context_save(get_el1_sysregs_ctx(ctx));
#if IMAGE_BL31 #if IMAGE_BL31
if (security_state == SECURE) if (security_state == SECURE) {
PUBLISH_EVENT(cm_exited_secure_world); PUBLISH_EVENT(cm_exited_secure_world);
else #if ENABLE_RME
} else if (security_state == NON_SECURE) {
PUBLISH_EVENT(cm_exited_normal_world); PUBLISH_EVENT(cm_exited_normal_world);
}
#else /* ENABLE_RME */
} else {
PUBLISH_EVENT(cm_exited_normal_world);
}
#endif /* ENABLE_RME */
#endif #endif
} }
...@@ -663,10 +718,18 @@ void cm_el1_sysregs_context_restore(uint32_t security_state) ...@@ -663,10 +718,18 @@ void cm_el1_sysregs_context_restore(uint32_t security_state)
el1_sysregs_context_restore(get_el1_sysregs_ctx(ctx)); el1_sysregs_context_restore(get_el1_sysregs_ctx(ctx));
#if IMAGE_BL31 #if IMAGE_BL31
if (security_state == SECURE) if (security_state == SECURE) {
PUBLISH_EVENT(cm_entering_secure_world); PUBLISH_EVENT(cm_entering_secure_world);
else #if ENABLE_RME
} else if (security_state == NON_SECURE) {
PUBLISH_EVENT(cm_entering_normal_world); PUBLISH_EVENT(cm_entering_normal_world);
}
#else /* ENABLE_RME */
} else {
PUBLISH_EVENT(cm_entering_normal_world);
}
#endif /* ENABLE_RME */
#endif #endif
} }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment