ECALL Swtichless调用及tRTS端Swtichless初始化

目录

 

以SampleCode/Switchless为例讲解ECALL Switchless的代码流程

第一个Switchless ECALL,需要在tRTS端初始化一下Switchless模式

接下来构建ECALL任务

用Switchless的方法来使用Swtichless ECALL

工人线程做了什么?

tRTS端初始化ECALL管理器

工人线程执行ECALL【process_switchless_call】

Switchless ECALL的内容就这么多了


SampleCode/Switchless为例讲解ECALL Switchless的代码流程

void ecall_empty_switchless(void) {}

【ecall_empty_switchless】是这个例子中用到的典型的Swtichless ECALL,函数里面啥也不执行。

那编译的时候怎么能够知道这个函数是使用ECALL Swtichless模式呢?总不能函数符号名里面加一个Swtichless就说明它是Switchless模式的ECALL吧。这可能是个方法,但是无形地会限制函数名的写法(比如Ordinary ECALL名字里就不能再写Switchless字眼了)。因此如果要把这个ECALL标记为Switchless模式,那么我们就需要在EDL文件(Enclave Definition Language)中进行标记声明。

SampleCode/Switchless的EDL文件如下

//位于SampleCode/Switchless/Enclave/Enclave.edl
enclave {
    from "sgx_tstdc.edl" import *;
    from "sgx_tswitchless.edl" import *;

    trusted {
        public void ecall_repeat_ocalls(unsigned long nrepeats, int use_switchless);
        public void ecall_empty(void);
        public void ecall_empty_switchless(void) transition_using_threads;
    };

    untrusted {
        void ocall_empty(void);
        void ocall_empty_switchless(void) transition_using_threads;
    };
};

我们可以看到【transition_using_threads】这个标记就是说明该函数需要改成Switchless的ECALL。

那么是由谁来识别这个标记并把这个函数转换成Switchless的ECALL呢?是由sgx_edger8r来做的。sgx_edger8r会对EDL文件里的内容进行识别,并对SGX的桥函数(E/OCALL)进行编排。EDL文件里面还有对桥函数参数属性的设置,请见《SGX软硬件栈(四)——桥函数

sgx_edger8r会将桥函数(Switchless或Ordinary)编排成型,变成所谓的stub(Enclave_{u,t}.{c,h})。使得你原来可能是直接在Enclave写了个ECALL函数,而实际ECALL调用过程变成了APP->stub(uRTS)->EENTER->stub(tRTS)->Enclave->ECALL。

因此sgx_edger8r对【ecall_empty_switchless】调整以后。在uRTS的stub端,变成了如下样子:

sgx_status_t ecall_empty_switchless(sgx_enclave_id_t eid)
{
	sgx_status_t status;
	status = sgx_ecall_switchless(eid, 2, &ocall_table_Enclave, NULL);
	return status;
}

可以看到【ecall_empty_switchless】通过【sgx_ecall_switchless】来尝试执行ECALL。

第一个Switchless ECALL,需要在tRTS端初始化一下Switchless模式

extern "C"
sgx_status_t sgx_ecall_switchless(const sgx_enclave_id_t enclave_id, const int proc, const void *ocall_table, void *ms)
{
    return _sgx_ecall(enclave_id, proc, ocall_table, ms, true);
}
static
sgx_status_t _sgx_ecall(const sgx_enclave_id_t enclave_id, const int proc, const void *ocall_table, void *ms, const bool is_switchless)
{
    ...
    result = enclave->ecall(proc, ocall_table, ms, is_switchless);
    ...
}

可以看到【sgx_ecall_switchless】第一个参数是Enclave ID,第二个参数是ECALL的索引值,第三个参数是Enclave内部可能会用到的OCALL表,第四个参数是传参的Marshalling。

【_sgx_ecall】额外增加了一个参数说明是不是Switchless的ECALL。我们这里是true。如果是Ordinary的ECALL,那么就是false了。

【_sgx_ecall】是【CEnclave::ecall】的套壳。

虽然和ECALL Switchless/Ordinary模式一样调用的【CEnclave::ecall】,但是参数【is_switchless】指明是不是用Switchless模式,现在这里是true。

sgx_status_t CEnclave::ecall(const int proc, const void *ocall_table, void *ms, const bool is_switchless)
{
    ...
    if (m_switchless)
    {
        // we need to pass ocall_table pointer to the enclave when initializing switchless on trusted side.
        if (m_first_ecall && ocall_table)
        {
            // can create race condition here if we have several threads initiating "first" ecall
            // so it is possible the first switchless ecall will fallback
            m_first_ecall = false;
            // we are setting the flag here, cause otherwise it will create deadlock in sl_on_first_ecall_func_ptr()
            g_sl_funcs.sl_on_first_ecall_func_ptr(m_switchless, m_enclave_id, ocall_table);
        }
        //Do switchless ECall in a switchless way
        if (is_switchless)
        {
            int need_fallback = 0;
            sgx_status_t ret = SGX_ERROR_UNEXPECTED;
            ret = g_sl_funcs.sl_ecall_func_ptr(m_switchless, proc, ms, &need_fallback);
            if (likely(!need_fallback))
            {
                se_rdunlock(&m_rwlock);
                return ret;
            }
        }
    }
    ...
}

针对(Enclave ID所对应的)Enclave的第一个Swtichless ECALL,我们需要将【ocall_table】事先传入Enclave中。此处调用的【g_sl_funcs.sl_on_first_ecall_func_ptr】函数指针指向的是【sl_uswitchless_on_first_ecall】函数,这函数最终目的是让tRTS也保管一份uSwitchless管理器,这个uSwitchless管理器原来只在uRTS保留过一份。tRTS中的uSwitchless管理器保存到tRTS的全局变量【g_uswitchless_handle】。

// holds the pointer to untrusted structure describing switchless configuration
// pointer is assigned only after all necessary checks
struct sl_uswitchless* g_uswitchless_handle = NULL;

具体说来,这个【sl_uswitchless_on_first_ecall】函数准备一下参数,以【sl_call_once】的方式(只调用一次的方式)调用【init_tswitchless】->【sl_init_switchless(uRTS, stub)】->切换上下文进入到tRTS(EENTER)->【sl_init_switchless(tRTS, stub)】

(这里提到的stub是由sgx_edger8r生成的放在Enclave{u,t}.{c,h}里桥函数接口,在开发者眼里,我们可以直接调用ECALL_A,但是实际上完成的是这样的过程:【ECALL_A(uRTS)】->【ECALL_A stub(uRTS)】->切换上下文进入到tRTS(EENTER)->【ECALL_A stub(tRTS)】->【ECALL_A(tRTS)】)

/* Override the weak symbol defined in tRTS */
sgx_status_t sl_init_switchless(void* _handle_u)
{
    ...
    if (lock_cmpxchg_ptr((void*)&g_uswitchless_handle, NULL, (void*)handle_u) != NULL)
    ...
}

【sl_init_switchless(tRTS, stub)】中会让uSwitchless管理器存放到tRTS的【g_uswitchless_handle】。

之后在uRTS中,【init_tswitchless】函数将OCALL表挂到uSwitchless管理器上以及uSwitchless管理器所包含的OCALL管理器上。此时,uSwitchless管理器可以标记uRTS、tRTS中的Switchless初始化都完成了。

tRTS的Switchless既然初始化完了,那么就一次性的唤醒所有{t,u}Worker线程们。之前uRTS端Switchless初始化时,工人线程们激活后都在睡觉。

接下来构建ECALL任务

所使用的【g_sl_funcs.sl_ecall_func_ptr】函数指针指向【sl_uswitchless_do_switchless_ecall】(此时我们在uRTS)

/* sgx_ecall_switchless() from uRTS calls this function to do the real job */
sgx_status_t sl_uswitchless_do_switchless_ecall(void* _switchless,
                                                const unsigned int ecall_id,
                                                void* ecall_ms,
                                                int* need_fallback)

首先确保一下tRTS端的Switchless初始化完毕了,并且tRTS端有tWorker工人线程,数量不能为0。如果有睡觉的tWorker工人线程,那么全部给叫醒。

构造ECALL任务【struct sl_call_task call_task】

【struct sl_call_task call_task】成员变量赋值说明

status

SL_INIT,代表ECALL任务刚初始

ECALL任务状态

func_id

ecall_id

ECALL索引值

func_data

ecall_ms

ECALL的Marshalling参数

ret_code

SGX_ERROR_UNEXPECTED

ECALL的返回值

调用【sl_call_mngr_call】,通过信号线让tWorker工人线程执行Switchless ECALL。信号线的初始化在《SGX初始化中,uRTS端的Switchless模式的初始化

如果Switchless ECALL时,没有可用的tWorker工人线程,那么通过回退Fallback成Ordinary ECALL,切换上下文执行ECALL。

用Switchless的方法来使用Swtichless ECALL

static inline int sl_call_mngr_call(struct sl_call_mngr* mngr, struct sl_call_task* call_task, uint32_t max_tries)
{
    /*
        Used to make actual switchless call by both enclave & untrusted code

        mngr:      points to ECALL or OCALL manager. For enclave, OCALL mngr and all its content are checkeds in sl_mngr_clone() function
                   see init_tswitchless_ocall_mngr()

        call_task: contains all the information of function to be called,
                   when called by enclave to make OCALL, call_task resides on enclaves stack
    */
    ...
    /* Allocate a free signal line to send signal */
    ...
    // copy task data to internal array accessable by both sides (trusted & untrusted)
    ...
    /* Send a signal so that workers will access the buffer for switchless call
     * requests. Here, a memory barrier is used to make sure the buffer is
     * visible when the signal is received on other CPUs. */
    ...
    // wait till the other side has picked the task for processing
    ...
    /* The request must has been accepted. Now wait for its completion */
    ...
    // copy the return code
    ...
}

这个函数中,首先申请一个空闲的信号线【struct sl_siglines.free_lines】(一个Bit位代表一个信号)。将ECALL任务的状态从【SL_INIT】改成【SL_SUBMITTED】。

将ECALL任务复制到uSwitchless管理器保存的ECALL管理器的任务池中。前面说到过uRTS和tRTS都能访问这个存储在uRTS的uSwitchless管理器的地址。

【sgx_mfence】确保工人线程能够看到任务池中的ECALL任务。然后发送(本质是变量设置)信号线【struct sl_siglines.event_lines】的信号一个Bit位代表一个信号),tWorker会收到信号(本质是对信号线变量循环检测)并开始执行ECALL任务。

ECALL调用者线程然后循环等到ECALL任务被接受【SL_ACCEPTED】、被完成【SL_DONE】。

等到ECALL被完成之后,调用者线程就返回了。

那么ECALL任务的另一端,接收ECALL任务并执行的tWorker线程作了什么呢?

工人线程做了什么?

之前说到工人线程,以tWorker为例,uRTS初始化Switchless时候处于睡眠状态,当tRTS初始化Switchless时,{t,u}Worker两种工人线程会被唤醒。此时工人线程都还处于uRTS,对于tWorker来说,他需要进入到tRTS中去。

tWorker被唤醒之后,会进入【tworker_process_calls(uRTS)】->【sl_run_switchless_tworker(uRTS)】->切换上下文进入Enclave->【sl_run_switchless_tworker(tRTS)】。

/*=========================================================================
 * Process Fast ECalls
 *========================================================================*/

/* The builtin ECMD_RUN_SWITCHLESS_TWORKER ECall calls this function eventually */
// This is a worker's thread function, which polls the event_lines bitmap for incoming
// switchless ECALLs requests
sgx_status_t sl_run_switchless_tworker()

以【sl_once_t】只调用一次的方式调用在tRTS内部初始化ECALL管理器【init_tswitchless_ecall_mngr】,uRTS端初始化Switchless时只初始化了uRTS端的E/OCALL管理器,tRTS也需要一份E/OCALL管理器。

tRTS端初始化ECALL管理器

// initialize enclave's ecall manager
static uint64_t init_tswitchless_ecall_mngr(void* param)
{
    ...
    // g_uswitchless_handle is checked in sl_init_switchless()
    ...
    // clone ecall manager structure allocated outside enclave, performing all the relevant checks
    ...
    // abort in case of bogus initialization
    ...
    // allocate switchless ecalls table inside the enclave
    ...
}

将uRTS的ECALL管理器连同它的信号线管理器克隆一份存到tRTS的【g_ecall_mngr】,主要是地址层面(仍属于uRTS)。

拷贝的信息不包括ECALL信号线的【free_lines】和tWorker处理ECALL的函数句柄【process_switchless_call】,uRTS的ECALL管理器的信号线管理器的处理ECALL的函数句柄是NULL(空),而不是【process_switchless_call】。

另外一个没有拷贝的是ECALL表【fcall_table】,这个ECALL表需要tRTS在Enclave内部重新创建一个出来,并注册到tRTS的【g_ecall_mngr】上。【fcall_table】的来源就是sgx_edger8r生成的存在stub【Enclave_t.c】中的【g_ecall_table】。

tRTS端初始化ECALL管理器的内容就这么多。

之后就是试图让工人线程执行ECALL任务【sl_call_mngr_process】->【sl_siglines_process_signals】->查看所有信号线每个Bit确定是否有信号来了->【process_switchless_call】。并且工人线程执行ECALL的失败重试次数是传参指定的或者默认(20000)的。

工人线程执行ECALL【process_switchless_call】

static inline void process_switchless_call(struct sl_siglines* siglns, uint32_t line)
{
    /*

      Main processing function which is called by a worker thread(trusted or untrusted)
      from sl_siglines_process_signals() function when there is a pending request for a switchless ECALL/OCALL.

      siglns points to the untrusted data structure with pending requests bitmap,
      siglns is checked by sl_siglines_clone() function before use by the enclave

      line indicates which bit is set

    */
    ...
}

【process_switchless_call】首先将CALL管理器和CALL任务取出,然后将状态设置为【SL_ACCEPTED】。

然后用CALL任务中的索引值从【sl_call_table_t.func】(来源于stub【Enclave_t.c】中的【g_ecall_table】)获取ECALL函数的虚拟地址。

然后执行ECALL ID对应的ECALL。

Switchless ECALL的内容就这么多了

工人线程执行完ECALL后,将ECALL任务的状态设置为【SL_DONE】。

工人线程再次回到自己的Main Loop。

uRTS端等待ECALL执行完毕的线程,当看到任务状态变成了【SL_DONE】就返回了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值