Es gibt jedoch einen Punkt, über den ich ausführlicher sprechen möchte.
Im vorherigen Artikel haben wir geschrieben, dass es im Rahmen der Implementierung der SGX-Unterstützung erforderlich war, dem Nova-Dienst beizubringen, eine XML-Datei mit den erforderlichen Einstellungen für die Gastdomäne zu generieren. Dieses Problem stellte sich als komplex und interessant heraus: Während der Arbeit an seiner Lösung mussten wir anhand des libvirt-Beispiels im Detail verstehen, wie Programme im Allgemeinen mit Befehlssätzen in x86-Prozessoren interagieren. Es gibt sehr, sehr wenige detaillierte und vor allem klar geschriebene Materialien zu diesem Thema. Wir hoffen, dass unsere Erfahrung für alle an der Virtualisierung Beteiligten von Nutzen ist. Das Wichtigste zuerst.
Erste Versuche
Wiederholen wir die Formulierung der Aufgabe noch einmal: Wir mussten SGX-Unterstützungsparameter an die XML-Konfigurationsdatei der virtuellen Maschine übergeben. Als wir gerade mit der Lösung dieses Problems begannen, gab es keine SGX-Unterstützung in OpenStack bzw. libvirt. Es war unmöglich, sie nativ in das XML der virtuellen Maschine zu übertragen.
Wir versuchten zunächst , dieses Problem zu lösen , indem ein Hinzufügen von Qemu Befehlszeilenblock auf das Skript an den Hypervisor über libvirt zu verbinden, wie beschrieben in der Intel - Entwickler Guide :
<qemu:commandline>
<qemu:arg value='-cpu'/>
<qemu:arg value='host,+sgx,+sgxlc'/>
<qemu:arg value='-object'/>
<qemu:arg value='memory-backend-epc,id=mem1,size=''' + epc + '''M,prealloc'/>
<qemu:arg value='-sgx-epc'/>
<qemu:arg value='id=epc1,memdev=mem1'/>
</qemu:commandline>
Danach wurde der virtuellen Maschine eine zweite Prozessoroption hinzugefügt:
[root@compute-sgx ~] cat /proc/$PID/cmdline |xargs -0 printf "%s\n" |awk '/cpu/ { getline x; print $0 RS x; }'
-cpu
Skylake-Client-IBRS
-cpu
host,+sgx,+sgxlc
Die erste Option wurde normal eingestellt und die zweite wurde direkt von uns im Qemu-Befehlszeilenblock hinzugefügt . Dies führte zu Unannehmlichkeiten bei der Auswahl eines Prozessoremulationsmodells: Unabhängig davon, welches Prozessormodell wir in der Konfigurationsdatei des Nova-Rechenknotens in cpu_model eingesetzt haben, wurde die Anzeige des Host-Prozessors in der virtuellen Maschine angezeigt.
Wie kann man dieses Problem lösen?
Auf der Suche nach einer Antwort haben wir zunächst versucht, mit der Zeile < qemu: arg value = 'host, + sgx, + sgxlc' zu experimentieren./> und versuchen Sie, das Prozessormodell darauf zu übertragen, aber dies hat die Duplizierung dieser Option nach dem Start der VM nicht abgebrochen. Dann wurde beschlossen, libvirt zu verwenden, um CPU-Flags zuzuweisen und diese mithilfe des Parameters cpu_model_extra_flags über die Nov'y-Konfigurationsdatei des Rechenknotens zu steuern .
Die Aufgabe erwies sich als schwieriger als erwartet: Wir mussten den Intel IA-32 - CPUID-Befehl studieren und Informationen zu den erforderlichen Registern und Bits in der Intel-Dokumentation zu SGX finden.
Weitere Suche: tiefer in libvirt eintauchen
In der Entwicklerdokumentation für den Nova-Dienst heißt es, dass die CPU-Flag-Zuordnung von libvirt selbst unterstützt werden muss.
Wir haben eine Datei gefunden, die alle CPU-Flags beschreibt - dies ist x86_features.xml (relevant seit libvirt 4.7.0). Nachdem wir diese Datei überprüft hatten, gingen wir davon aus (wie sich später fälschlicherweise herausstellte), dass wir nur die Hex-Adressen der erforderlichen Register im 7. Blatt mit dem Dienstprogramm cpuid abrufen müssen. Aus der Intel-Dokumentation haben wir erfahren, in welchen Registern die von uns benötigten Anweisungen aufgerufen werden: sgx befindet sich im EBX-Register und sgxlc befindet sich im ECX.
[root@compute-sgx ~] cpuid -l 7 -1 |grep SGX
SGX: Software Guard Extensions supported = true
SGX_LC: SGX launch config supported = true
[root@compute-sgx ~] cpuid -l 7 -1 -r
CPU:
0x00000007 0x00: eax=0x00000000 ebx=0x029c6fbf ecx=0x40000000 edx=0xbc000600
Nach dem Hinzufügen der Flags sgx und sgxlc mit den mit dem Dienstprogramm cpuid erhaltenen Werten wurde die folgende Fehlermeldung angezeigt:
error : x86Compute:1952 : out of memory
Die Botschaft ist, um es ganz klar auszudrücken, nicht sehr informativ. Um das Problem irgendwie zu verstehen, haben wir ein Problem in gitlab libvirt geöffnet. Die libvirt-Entwickler bemerkten, dass ein falscher Fehler angezeigt und behoben wurde, was darauf hinwies, dass libvirt nicht die richtige Anweisung finden konnte, die wir aufriefen, und schlugen vor, wo wir falsch liegen könnten. Aber um zu verstehen, was genau wir angeben mussten, damit es keinen Fehler gab, gelang es uns nicht.
Ich musste mich in den Quellen umsehen und studieren, es dauerte lange. Dies konnte erst nach dem Studium des Codes in einem modifizierten Qemu von Intel herausgefunden werden:
[FEAT_7_0_EBX] = {
.type = CPUID_FEATURE_WORD,
.feat_names = {
"fsgsbase", "tsc-adjust", "sgx", "bmi1",
"hle", "avx2", NULL, "smep",
"bmi2", "erms", "invpcid", "rtm",
NULL, NULL, "mpx", NULL,
"avx512f", "avx512dq", "rdseed", "adx",
"smap", "avx512ifma", "pcommit", "clflushopt",
"clwb", "intel-pt", "avx512pf", "avx512er",
"avx512cd", "sha-ni", "avx512bw", "avx512vl",
},
.cpuid = {
.eax = 7,
.needs_ecx = true, .ecx = 0,
.reg = R_EBX,
},
.tcg_features = TCG_7_0_EBX_FEATURES,
},
[FEAT_7_0_ECX] = {
.type = CPUID_FEATURE_WORD,
.feat_names = {
NULL, "avx512vbmi", "umip", "pku",
NULL /* ospke */, "waitpkg", "avx512vbmi2", NULL,
"gfni", "vaes", "vpclmulqdq", "avx512vnni",
"avx512bitalg", NULL, "avx512-vpopcntdq", NULL,
"la57", NULL, NULL, NULL,
NULL, NULL, "rdpid", NULL,
NULL, "cldemote", NULL, "movdiri",
"movdir64b", NULL, "sgxlc", NULL,
},
.cpuid = {
.eax = 7,
.needs_ecx = true, .ecx = 0,
.reg = R_ECX,
},
Aus der obigen Auflistung können Sie ersehen , dass in den .feat_names- Blöcken Anweisungen aus EBX / ECX-Registern des 7. Blattes Stück für Stück aufgelistet sind (von 0 bis 31); Wenn der Befehl von Qemu nicht unterstützt wird oder dieses Bit reserviert ist, wird es mit einem NULL- Wert gefüllt . Dank dieses Beispiels haben wir die folgende Annahme getroffen: Möglicherweise müssen wir nicht die Hex-Adresse des erforderlichen Registers in libvirt angeben, sondern speziell das Bit dieser Anweisung. Es ist einfacher, dies zu verstehen, indem Sie die Tabelle aus Wikipedia lesen . Links ist ein bisschen und drei Register. Wir finden unsere Anweisung darin - sgx. In der Tabelle ist dies unter dem zweiten Bit im EBX-Register angegeben:
Als nächstes überprüfen wir die Position dieser Anweisung im Qemu-Code. Wie wir sehen können, ist sie die dritte in der Liste der feat_names, aber das liegt daran, dass die Bitnummerierung bei 0 beginnt:
[FEAT_7_0_EBX] = {
.type = CPUID_FEATURE_WORD,
.feat_names = {
"fsgsbase", "tsc-adjust", "sgx", "bmi1",
Sie können sich andere Anweisungen in dieser Tabelle ansehen und sicherstellen, dass sie beim Zählen von 0 in der angegebenen Liste unter ihrem eigenen Bit stehen. Zum Beispiel: fsgsbase geht unter Bit 0 des EBX-Registers und wird zuerst aufgelistet.
In der Intel-Dokumentation haben wir dies bestätigt und sichergestellt, dass der erforderliche Befehlssatz mit cpuid aufgerufen werden kann, wobei beim Zugriff auf das Register des gewünschten Blattes und in einigen Fällen auf die Unterliste das richtige Bit übergeben wird.
Wir haben begonnen, die Architektur von 32-Bit-Prozessoren genauer zu verstehen, und haben festgestellt, dass solche Prozessoren Blätter haben, die die vier Hauptregister enthalten: EAX, EBX, ECX, EDX. Jedes dieser Register enthält 32 Bits, die für einen bestimmten Satz von CPU-Befehlen reserviert sind. Ein Bit ist eine Zweierpotenz und kann meistens im Hex-Format an ein Programm übergeben werden, wie dies in libvirt der Fall ist.
Betrachten Sie zum besseren Verständnis ein weiteres Beispiel mit dem verschachtelten VMX-Virtualisierungsflag aus der von libvirt verwendeten Datei x86_features.xml :
<feature name = 'vmx ' >
<cpuid eax_in = ' 0x01 ' ecx = ' 0x00000020 '/> # 2 5 = 32 10 = 20 16
</ feature>
Der Verweis auf diese Anweisung erfolgt im 1. Blatt zum ECX-Register unter Bit 5, und Sie können dies anhand der Feature-Informationstabelle in Wikipedia überprüfen .
Nachdem wir uns damit befasst und verstanden hatten, wie Flags schließlich zu libvirt hinzugefügt werden, beschlossen wir, weitere SGX-Flags (zusätzlich zu den wichtigsten: sgx und sgxlc) hinzuzufügen, die im modifizierten Qemu vorhanden waren:
[root@compute-sgx ~] /usr/libexec/qemu-kvm -cpu help |xargs printf '%s\n' |grep sgx
sgx
sgx-debug
sgx-exinfo
sgx-kss
sgx-mode64
sgx-provisionkey
sgx-tokenkey
sgx1
sgx2
sgxlc
Einige dieser Flags sind keine Anweisungen mehr, sondern Attribute der Enclave Data Control Structure (SECS). Weitere Informationen hierzu finden Sie in der Intel- Dokumentation . Darin haben wir festgestellt, dass die Menge der SGX-Attribute, die wir benötigen, in Blatt 0x12 in Unterliste 1 enthalten ist:
[root@compute-sgx ~] cpuid -l 0x12 -s 1 -1 CPU: SGX attributes (0x12/1): ECREATE SECS.ATTRIBUTES valid bit mask = 0x000000000000001f0000000000000036
Im Screenshot von Tabelle 38-3 finden Sie die benötigten Attributbits, die wir später als Flags in libvirt angeben werden: sgx-debug, sgx-mode64, sgx-Bereitstellungsschlüssel, sgx-Tokenschlüssel. Sie befinden sich unter den Bits 1, 2, 4 und 5.
Wir haben auch aus der Antwort in unserer Ausgabe verstanden : libvirt verfügt über ein Makro zum Überprüfen von Flags auf ihre Unterstützung direkt durch den Prozessor des Rechenknotens. Dies bedeutet, dass es nicht ausreicht, die erforderlichen Blätter, Bits und Register in der Datei x86_features.xml anzugeben, wenn libvirt selbst kein Befehlssatzblatt unterstützt. Zum Glück stellte sich heraus, dass der libvirt- Code mit diesem Blatt arbeiten kann:
/* Leaf 0x12: SGX capability enumeration
*
* Sub leaves 0 and 1 is supported if ebx[2] from leaf 0x7 (SGX) is set.
* Sub leaves n >= 2 are valid as long as eax[3:0] != 0.
*/
static int
cpuidSetLeaf12(virCPUDataPtr data,
virCPUx86DataItemPtr subLeaf0)
{
virCPUx86DataItem item = CPUID(.eax_in = 0x7);
virCPUx86CPUIDPtr cpuid = &item.data.cpuid;
virCPUx86DataItemPtr leaf7;
if (!(leaf7 = virCPUx86DataGet(&data->data.x86, &item)) ||
!(leaf7->data.cpuid.ebx & (1 << 2)))
return 0;
if (virCPUx86DataAdd(data, subLeaf0) < 0)
return -1;
cpuid->eax_in = 0x12;
cpuid->ecx_in = 1;
cpuidCall(cpuid);
if (virCPUx86DataAdd(data, &item) < 0)
return -1;
cpuid->ecx_in = 2;
cpuidCall(cpuid);
while (cpuid->eax & 0xf) {
if (virCPUx86DataAdd(data, &item) < 0)
return -1;
cpuid->ecx_in++;
cpuidCall(cpuid);
}
return 0;
}
Aus dieser Auflistung können Sie ersehen, dass libvirt beim Zugriff auf das 2. EBX-Bit des 7. Blattregisters (d. H. Den SGX-Befehl) Blatt 0x12 verwenden kann, um die verfügbaren Attribute in den Unterlisten 0, 1 und 2 zu überprüfen.
Fazit
Nach Abschluss der Recherchen haben wir herausgefunden, wie die Datei x86_features.xml ordnungsgemäß hinzugefügt werden kann. Wir haben die erforderlichen Bits in das Hex-Format konvertiert - und das haben wir bekommen:
<!-- SGX features -->
<feature name='sgx'>
<cpuid eax_in='0x07' ecx_in='0x00' ebx='0x00000004'/>
</feature>
<feature name='sgxlc'>
<cpuid eax_in='0x07' ecx_in='0x00' ecx='0x40000000'/>
</feature>
<feature name='sgx1'>
<cpuid eax_in='0x12' ecx_in='0x00' eax='0x00000001'/>
</feature>
<feature name='sgx-debug'>
<cpuid eax_in='0x12' ecx_in='0x01' eax='0x00000002'/>
</feature>
<feature name='sgx-mode64'>
<cpuid eax_in='0x12' ecx_in='0x01' eax='0x00000004'/>
</feature>
<feature name='sgx-provisionkey'>
<cpuid eax_in='0x12' ecx_in='0x01' eax='0x00000010'/>
</feature>
<feature name='sgx-tokenkey'>
<cpuid eax_in='0x12' ecx_in='0x01' eax='0x00000020'/>
</feature>
Um diese Flags an die virtuelle Maschine zu übergeben, können Sie sie in der Nova-Konfigurationsdatei mit cpu_model_extra_flags angeben :
[root@compute-sgx nova] grep cpu_mode nova.conf
cpu_mode = custom
cpu_model = Skylake-Client-IBRS
cpu_model_extra_flags = sgx,sgxlc,sgx1,sgx-provisionkey,sgx-tokenkey,sgx-debug,sgx-mode64
[root@compute-sgx ~] cat /proc/$PID/cmdline |xargs -0 printf "%s\n" |awk '/cpu/ { getline x; print $0 RS x; }'
-cpu
Skylake-Client-IBRS,sgx=on,sgx-mode64=on,sgx-provisionkey=on,sgx-tokenkey=on,sgx1=on,sgxlc=on
Nachdem wir den harten Weg gegangen waren, haben wir gelernt, wie man libvirt Unterstützung für SGX-Flags hinzufügt. Dies hat uns geholfen, das Problem des Duplizierens von Prozessoroptionen in der XML-Datei der virtuellen Maschine zu lösen. Wir werden die in unserer zukünftigen Arbeit gewonnenen Erfahrungen nutzen: Wenn in Intel- oder AMD-Prozessoren neue Anweisungen angezeigt werden, können wir diese auf die gleiche Weise zu libvirt hinzufügen. Die Vertrautheit mit der CPUID-Anweisung ist auch beim Schreiben eigener Lösungen hilfreich.
Wenn Sie Fragen haben - willkommen zu den Kommentaren, werden wir versuchen zu beantworten. Und wenn Sie etwas hinzuzufügen haben - umso mehr, schreiben Sie, wir werden Ihnen sehr dankbar sein.