4. 第一个Enclave程序:编写EDL、实现内部逻辑、编写不可信部分、编译与运行
好,到了真正动手的时候了。前面几章我们把TEE的概念、SGX的架构、开发环境的搭建都过了一遍。现在,我们来写第一个Enclave程序。
说白了,一个Enclave程序由三部分组成:描述文件(EDL)、Enclave内部的可信代码、以及外部不可信代码。这三者缺一不可。我刚开始接触SGX时,总觉得EDL是个多余的麻烦,后来踩了坑才明白——EDL就是Enclave的“海关”,什么能进、什么能出,全靠它说了算。
核心流程:编写EDL → 实现Enclave内部函数 → 编写外部调用代码 → 编译链接 → 运行验证。
4.1 编写EDL文件——定义可信边界
EDL(Enclave Description Language)是Intel SGX定义的一种接口描述语言。它的作用,就是告诉编译器:哪些函数可以从外部调用进入Enclave(ECALL),哪些函数可以从Enclave内部调用到外部(OCALL)。
我个人习惯把EDL看作一份“外交协议”。ECALL是“入境许可”,OCALL是“出境许可”。没有许可,数据就过不了边界。
来看一个最简单的EDL文件示例:
// Enclave.edl
enclave {
trusted {
/* ECALL:外部可以调用的Enclave函数 */
public void ecall_encrypt([in, size=len] char* input, size_t len,
[out, size=len] char* output);
public int ecall_get_result(void);
};
untrusted {
/* OCALL:Enclave内部调用的外部函数 */
void ocall_print_string([in, string] const char* str);
};
};
小提示:EDL中的[in]、[out]、[size]等注解不是摆设。它们告诉edger8r工具如何做内存拷贝和边界检查。我曾经见过有人漏写[size]导致缓冲区溢出——在Enclave里这可不是闹着玩的。
EDL文件写好后,需要用sgx_edger8r工具处理它:
sgx_edger8r Enclave.edl
这个命令会生成四个文件:
- Enclave_t.h 和 Enclave_t.c:可信侧的代理代码(用于Enclave内部)
- Enclave_u.h 和 Enclave_u.c:不可信侧的代理代码(用于外部程序)
你想想看,如果没有edger8r,你得手动处理参数编组、内存拷贝、边界检查——那工作量可就大了去了。
4.2 实现Enclave内部逻辑——可信代码
Enclave内部的代码,运行在受保护的内存区域(EPC)中。这里的数据,CPU之外的任何软件都读不到。但有个限制:你不能直接在Enclave里调用系统调用、文件I/O、网络操作。这些都得通过OCALL委托给外部。
下面是一个简单的Enclave内部实现:
// Enclave.cpp
#include "Enclave_t.h" // 由edger8r生成
#include <string.h>
#include <stdlib.h>
/* 内部缓冲区,用于存储加密结果 */
static char g_result[256];
static int g_result_len = 0;
/* ECALL:加密输入数据(这里用简单的XOR演示) */
void ecall_encrypt(char* input, size_t len, char* output) {
if (input == NULL || output == NULL || len == 0) {
ocall_print_string("Error: invalid parameters");
return;
}
// 模拟加密:每个字节与0xAA异或
for (size_t i = 0; i < len; i++) {
output[i] = input[i] ^ 0xAA;
}
// 保存结果到内部缓冲区
memcpy(g_result, output, len);
g_result_len = len;
ocall_print_string("Encryption done inside Enclave");
}
/* ECALL:获取内部结果长度 */
int ecall_get_result(void) {
return g_result_len;
}
注意:Enclave内部不要使用全局变量存储敏感数据?不对,恰恰相反——Enclave内部是唯一安全的地方。全局变量在这里是安全的,因为EPC内存天然被加密保护。但要注意,不要把敏感数据通过OCALL传出去,除非你确认外部环境安全。
我在项目中遇到过一个问题:Enclave内部不能直接使用printf,因为那是系统调用。所以我们需要通过OCALL把字符串打印出去。上面的代码里,ocall_print_string就是干这个的。
4.3 编写不可信部分代码——外部应用程序
不可信部分,就是运行在普通用户空间的代码。它负责创建Enclave、调用ECALL、处理OCALL回调。
// App.cpp
#include <stdio.h>
#include <string.h>
#include "sgx_urts.h"
#include "Enclave_u.h" // 由edger8r生成
/* OCALL实现:Enclave内部调用此函数打印字符串 */
void ocall_print_string(const char* str) {
printf("[Enclave] %s\n", str);
}
int main() {
sgx_enclave_id_t eid = 0;
sgx_status_t ret;
// 1. 创建Enclave
ret = sgx_create_enclave("enclave.signed.so", SGX_DEBUG_FLAG,
NULL, NULL, &eid, NULL);
if (ret != SGX_SUCCESS) {
printf("Failed to create enclave, error code: %d\n", ret);
return -1;
}
printf("Enclave created successfully, ID: %ld\n", eid);
// 2. 准备数据
char input[] = "Hello, TEE World!";
char output[256] = {0};
// 3. 调用ECALL
ret = ecall_encrypt(eid, input, strlen(input) + 1, output);
if (ret != SGX_SUCCESS) {
printf("ECALL failed: %d\n", ret);
sgx_destroy_enclave(eid);
return -1;
}
// 4. 获取结果
int result_len = ecall_get_result(eid);
printf("Encrypted output (%d bytes): ", result_len);
for (int i = 0; i < result_len; i++) {
printf("%02X ", (unsigned char)output[i]);
}
printf("\n");
// 5. 销毁Enclave
sgx_destroy_enclave(eid);
return 0;
}
这里有个细节:ecall_encrypt和ecall_get_result的第一个参数都是eid(Enclave ID)。这是edger8r自动加上的,用来标识要调用哪个Enclave实例。你可以在一个进程中加载多个Enclave,每个都有自己的ID。
4.4 编译与运行——把一切串起来
编译Enclave程序比普通程序复杂一些,因为需要分别编译可信部分和不可信部分,还要签名。我整理了一个典型的Makefile结构:
| 步骤 | 命令 | 说明 |
|---|---|---|
| 1. 生成代理代码 | sgx_edger8r Enclave.edl |
生成_t.h、_t.c、_u.h、_u.c |
| 2. 编译不可信部分 | g++ -c App.cpp -I$(SGX_SDK)/include |
生成App.o |
| 3. 编译可信部分 | g++ -c Enclave.cpp -I$(SGX_SDK)/include/tlibc -fPIC -fvisibility=hidden |
生成Enclave.o |
| 4. 链接可信部分 | g++ -o enclave.so Enclave.o -lsgx_trts -shared |
生成未签名的.so |
| 5. 签名Enclave | sgx_sign sign -key Enclave_private.pem -enclave enclave.so -out enclave.signed.so |
生成签名后的.so |
| 6. 链接不可信部分 | g++ -o app App.o Enclave_u.o -lsgx_urts |
生成可执行文件 |
避坑指南:我曾经在编译时忘记加-fvisibility=hidden,结果Enclave内部的符号全部暴露了。虽然EPC内存还是加密的,但符号表泄露了内部函数名——这给攻击者提供了信息。记住:Enclave的编译选项要尽量保守。
运行程序:
./app
如果一切顺利,你会看到类似这样的输出:
Enclave created successfully, ID: 1
[Enclave] Encryption done inside Enclave
Encrypted output (17 bytes): E2 2B 2C 2C 2F 20 54 45 45 20 57 6F 72 6C 64 21 00
嗯,这里要注意:输出中的[Enclave]前缀,就是通过OCALL从Enclave内部打印出来的。这说明数据确实进入了Enclave,加密操作在受保护的环境中完成,结果又安全地返回到了外部。
4.5 常见问题与排查
第一次跑Enclave程序,大概率会遇到问题。我列几个常见的:
- Enclave创建失败(SGX_ERROR_ENCLAVE_FILE_ACCESS):检查
enclave.signed.so路径是否正确,文件是否存在。 - ECALL返回SGX_ERROR_UNEXPECTED:通常是EDL和实际函数签名不匹配。重新运行edger8r,检查参数类型和个数。
- OCALL没有触发:确认在不可信代码中实现了对应的OCALL函数,并且函数名与EDL中定义的一致。
- 编译时报错找不到头文件:检查
-I路径是否正确,特别是可信代码需要包含tlibc的头文件路径。
我的经验:调试Enclave程序时,先用SGX_DEBUG_FLAG编译。这样可以看到更详细的错误日志。生产环境再换成SGX_PROD_FLAG。另外,sgx_sign工具可以用-dump选项查看签名后的Enclave元信息,排查签名问题很有用。
好了,到这里你已经完成了第一个Enclave程序的编写、编译和运行。虽然功能简单,但骨架已经搭起来了。后面我们会在这个基础上,加入真正的加密算法、安全通信、远程认证等高级特性。