SGX软硬件栈(四)——桥函数

SGX开发特点

与传统的C++程序开发不同,SGX应用程序会分为两块,一块是执行在Enclave中的代码,一块是执行在普通环境中的代码。此外,需要在.edl文件中的trusted标记中写上在Enclave中执行的函数的声明,在untrusted标记中写上在普通环境中执行的函数的声明。

SgxEdger8r(SGX Edge Routine,主要用于在编译过程中对.edl文件中的ecall和ocall重新封装编写,即将用户编写的e/ocall改写成实际执行的e/ocall)工具会在应用程序编译过程中根据edl文件将可信与不可信函数声明重新封装成给Enclave环境、普通环境下代码所调用的桥函数,实现两种环境之间的切换。如普通环境下代码调用桥函数会进而通过sgx_ecall进入Enclave环境,Enclave环境下代码调用桥函数进而通过sgx_ocall进入普通环境,其中调用sgx_e/ocall的代码由SgxEdger8r根据用户编写在edl文件中trusted/untrusted标记下的函数声明来自动生成,即具体的进出Enclave代码不需要用户来实现,只需要打上标记即可。

环境切换步骤如下:第一步,通过工具生成的桥函数会调用sgx_ecall、sgx-ocall进而调用硬件指令来切换环境;第二步,由于那些打上标记的函数本身有用户定义的执行代码,因此在切换环境后会进入真正的函数定义。举个例子,Main()->不可信环境ECALL_A()->sgx_ecall()->EENTER->可信环境ECALL_A()。

总而言之,这些打上标记的函数声明会最终由SgxEdger8r工具在编译过程中改造成桥函数,用于切换环境,然后执行函数声明对应的函数定义。

此外在普通环境中的代码需要显式地管理Enclave的生命周期,如调用“创建初始化Enclave的函数”之后才能调用Enclave中的函数,最后需要调用“销毁Enclave的函数”。

如果想要将在普通环境和Enclave环境中传递一个“指向缓冲区的指针”等参数,需要在.edl文件的函数形参前面声明如[in]、[out]等参数标记,这是因为普通环境的内存往往是不可信的,通过比如将普通环境下的缓冲区拷贝一份到Enclave环境中,然后Enclave代码在拷贝进Enclave的缓冲区上进行操作等才是放心的,不会中途被恶意篡改等,最后可能再拷贝到普通环境供普通环境下的程序使用(具体见下文“SGX参数”章节)。

桥函数总结

通过上文的引入,现在总结一下桥函数:桥函数是指在SGX平台下,唯一能够切换可信环境与不可信环境的方式,调用硬件指令ENCLU中的EENTER、EEXIT来具体实现,是由SgxEdger8r工具来生成。

代码层面的环境切换过程大概是这样,以传统Switch模式ecall为例(这种模式便于理解,另一种Switchless模式Ecall见“SGX Switch模式及Switchless模式”章节):

不可信环境->假装调用由用户定义的ecall函数->对应的桥函数(SGX Edger8r重新封装的)>sgx_ecall()->_sgx_ecall()->CEnclave::ecall()->do_ecall()->enter_enclave()->__morestack(汇编)->ENCLU(调用硬件指令)->切换到tRTS->可信环境->enclave_entry(汇编)->enter_encalve()->do_ecall()->trts_ecall()->真正调用由用户定义的ecall函数。

SGX参数及函数属性

SGX将参数从不可信环境传入可信环境的方法是将保存在不可信环境的栈上的参数拷贝到寄存器,EENTER叶功能会将不可信环境的RSP、RBP保存到SSA结构中,并完成地址的切换进入可信环境。

这里将描述.edl文件中的SGX桥函数(接口)中参数或函数的修饰符说明,用来具体说明函数传参或函数声明的具体属性。ECALL为例子,OCALL类似。

  • public void ecall_array_user_check([user_check] int arr[4]);

这里是对函数传参属性的说明,下同。

[user_check]:Enclave程序使用数组时该数组不会被验证、数组对应缓冲区不会拷贝到Enclave内存中,Enclave可以直接修改应用程序中数组对应的缓冲区。(这个例子中参数为数组,指针类似)

  • public void ecall_array_in([in] int arr[4]);

[in]:Enclave会在内部分配相同大小的缓冲区,外部缓冲区的内容被拷贝到内部缓冲区,内部缓冲区内容修改不会导致(不影响)外部缓冲区任何变化。

  • public void ecall_array_out([out] int arr[4]);

[out]:内部缓冲区的修改在函数返回时拷贝到外部缓冲区。

  • public void ecall_array_in_out([in, out] int arr[4]);

[in, out]:Enclave内部会分配一个缓冲区,将数组缓冲区拷贝进来,同时在函数返回时,将内部缓冲区内容拷贝到外部缓冲区,双向。也就说缓冲区的改造过程被隐藏了,外部应用程序只能知道函数的输入输出。

  • public void ecall_array_isary([user_check, isary] array_t arr);

[isary]:告诉Edger8r用户自定义结构体array_t的变量是一个数组。

  • public void ecall_pointer_string([in, out, string] char *str);

[string]:告诉Edger8r,变量str参数是一个NULL结尾的字符串。可以用于strlen等功能。

  • public void ecall_pointer_string_const([in, string] const char *str);

const:说明字符串str不可修改,所以不适用[out]属性。

  • public void ecall_pointer_size([in, out, size=len] void *ptr, size_t len);

[size]:告诉Edger8r ptr对应的缓冲区的字节数。不能对[string]属性施加[size]属性。

  • public void ecall_pointer_count([in, out, count=cnt] int *arr, size_t cnt);

[count]:告诉Edger8r需要拷贝的arr的长度。

  • public void ecall_pointer_isptr_readonly([in, isptr, readonly, size=len] buffer_t buf, size_t len);

[isptr]:告诉Edger8r自定义类型是一个指针。

[readonly]:禁止Enclave内部分配的缓冲区被拷贝到外部去,因此不能与[out]同时使用。

  • public void ecall_function_public(void);

这里是对函数声明属性的描述,下同。

[public]:外部可以直接调用该Ecall

  • int ecall_function_private(void);

[private]:默认,外部不可以直接调用该Ocall,除非有Ocall调用这个Ecall并且是[allow]属性

  • void ocall_function_allow(void) allow(ecall_function_private);

[allow]:允许OCALL在外部调用[private] ECALL。

SGX Switch模式及Switchless模式

(可以见博客——ECALL Switch模式和ECALL Switchless模式)

由上文可见,SGX是通过Ecall来进入Enclave环境,类似的,通过Ocall离开Enclave环境。关于Ecall效率方面,传统的Switch模式,应用程序对应的线程(该进程只有一个Main线程)会切换进入Enclave环境,然后拿上它在Enclave环境的“工作证”,也就是绑定上TCS这个结构体,进行具体的用户定义的Ecall函数的执行。

但是这种模式,只能使用一个线程来完成具体任务(我们将这种调用用户定义的Ecall函数称为Ecall任务,此外还有Ocall任务),效率很低,同时还有伴随开销,比如进入Enclave环境需要拿起TCS“工作证”。因为目前还不支持unix的线程库,因此在Enclave中不支持创建线程,只能支持互斥量、条件变量等的维护(用于外部应用程序创建多个线程分别进入Enclave的场景),目前不支持线程库的原因是一方面Enclave程序旨在保护敏感代码,而远不需要创建线程等功能,另一方面出于数据依赖性、安全性等考量。

为了提高效率问题,定义了Switchless模式,如图9所示。以Ecall为例,uRTS为外部应用程序提供了Ecall任务池,以及联合tRTS初始化了多个可信Worker线程来具体执行Ecall任务。举例来说,外部应用程序调用了Ecall_A、Ecall_B,Switch做法是只有一个线程,它执行Ecall_A之后后回到不可信环境继而执行Ecall_B,是一种上下文环境切换的概念;Switchless做法是构建好Ecall Table管理器保存可能执行的Ecall函数,同时构建一个任务池用于记录每个Ecall具体执行的先后顺序,uRTS线程会唤醒空闲的可信Worker线程从任务池中取任务,然后查询Call Table,进而完成具体任务,因此省去了上下文环境切换的开销。Ocall方向的类似。

图9:Switchless模式调用架构

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值