 Hallo an alle. Willkommen zu unserem Talk. Wir reden über Treiber in Hochsprachen. Wir halten den Talk auf Deutsch, aber die Folien werden auf Englisch sein, einfach weil wir uns schon schrecklich schwergetan haben, einfach immer diese ganzen komischen Fachbegriffe auf Deutsch zu übersetzen. Wir hoffen, da kommt jetzt nicht so der Total der Org-Wort-Myx aus Deutsch und Englisch raus, wenn wir reden. Aber das wird schon, ja, ihr seht schon, wir haben hier viele Namen auf den Folien. Prinzipiell ist das hier ein Talk aus einer Reihe, wo wir schon mehrere Talks gegeben haben. Und da haben einfach viele Leute schon mitgewirkt und ich habe hier einfach alle drauf geschrieben, die hier was an dem Projekt hier mal mitgemacht haben. Und heute mit mir dabei ist Simon und ich bin Paul, ich bin Doktorant an der TUM in München und ich schaue mir da an, wie sich Sachen im Netzwerkbereich in Software machen lassen und ob es dann auch noch schnell ist bzw. wie man hinkriegt, dass es schnell ist, wenn man Router jetzt in Software läuft statt in Hardware irgendwo. Und Simon ist mein Hivi und hat eine Bachelorarbeit bei mir geschrieben und zwar hat er einen Rust-Treiber programmiert und alle anderen, die auf der Folie eben standen, sind auch Studenten von mir, die irgendeine Art Abschlussarbeit, wo ich irgendeine Art Abschlussarbeit betreut habe, die in dem Rahmen von diesen Projekten war. Nun, also es geht um bessere Programmiersprachen für Treiber und dann schauen wir uns erstmal an, was ist denn überhaupt so die Standardsprache und ja, es ist natürlich C, wenn ich mal anschaue, Betriebssysteme werden realistisch gesehen in C geschrieben, weil C ist total die tolle Sprache um ein Programm, total toll, um einfach so ein Betriebssystem zu schreiben. Hat hier irgendjemand auf seinem Laptop ein Betriebssystem, was nicht in C geschrieben ist? Nee, keiner, komisch. Dabei gibt es doch welche, kommen wir nachher zu. Ja, warum nimmt man C oder warum ist C total die geile Sprache? Es ist natürlich, man hat diesen Low Level Zugriff auf Speicher, auf Geräte und so weiter. Point, das ist total super, man kann damit tolle Sachen machen und man kann auch total sicheren Code schreiben, wenn man sich nur richtig anstrengt. Also zumindest behaupten Leute immer, dass sie das hinkriegen. Ich schaffe es nicht, aber andere behaupten, dass sie das können, also scheint es wohl zu gehen. Und das nächste ist natürlich, dass C versteht einfach jeder. Das ist einfach so der lost common denominator an Programmiersprachen. Einfach mal kurze Umfrage. Wer hier glaubt, dass der C-Code gut lesen kann, einfach kurz melden. Gurschen heißt gut, okay, das sind jetzt so die Hälfte, aber ein bisschen mehr als die Hälfte. Ja, dann ist das natürlich auch Leute sagen, C ist irgendwie wunderschön. Aber wenn C wunderschön ist und man kann es so gut lesen, warum hat mir dann ein Student solchen Code als Pulverquest geschickt bei dem Projekt und hat gesagt, das geht nicht anders, das muss man so machen. Und das ist außerdem voll schön. Habe ich erst mal irgendwie gesagt, nein, aber am Ende ist es reingemerged, weil es tatsächlich die schönste Art ist, um das so zu machen, was wir da machen mussten. Wer kann denn diesen Code so direkt interpretieren? Wer schaut sich das an, denkt sich, ah klar, das macht hier irgendwie da. Sieht es jemand? Wenn ich den Namen mal fixe und den richtigen Namen nehme, sieht es dann jemand? Ist das tut? Nee, immer noch nicht. Aber ist doch so einfach und schön und sicher. Nicht? Okay, ja, das ist so ein Standard Makro, was man im Betriebssystem bei, vor allem in Treibern immer gerne wieder sieht. Das macht so ein bisschen Vererbung und das macht so ein bisschen abstrahiert, so ein bisschen Interfaces von Treibern bzw. Structs von Treibern. Und das ist der Linux Kernel oder allgemein Treibern ist einfach voll. Ich habe einfach beim Linux Kernel mal durchgesucht, nach Container off, da gibt es 15.000 Mal, wird das verwendet etwa. Das heißt, das ist jetzt nicht so vollkommen unreasonable Code, aber es ist halt natürlich schon hier, wenn da irgendwie literal Null auf den Pointer gekastet wird, dann dereferenziert wird, aber das ist okay, weil ein Type off außen drum ist. Da denkt man schon meistens her, aber es ist tatsächlich die schönste Art, um einiges zu machen und deshalb ist jetzt auch in unserem C Code dieses schöne Makro drinnen mit irgendwie einem Erklärungstext, der länger als das Makro ist. Okay, also C ist schon mal nicht schön und Leute scheinen es nicht zu verstehen. Was kann es denn dann, ist es denn noch sicher? Ja, schauen wir uns einfach mal an hier, was gab es denn so für kritische Sicherheitslücken im Linux Kernel, in dem Fall so in den letzten 20 Jahren etwa. Da steht auf jeden Fall eine Zahl, die größer als Null ist. Das ist nicht unbedingt gut, aber es ist natürlich jetzt nicht immer die Sprache dran. Schuld, dass man irgendwie einen Fehler in seinem Kernel hat. Da ist es jetzt natürlich sehr mühsam, das rauszufinden, an wie vielen Bugs denn die Sprache Schuld ist. Da müsste man durch die ganzen Patches durchgehen und schauen, was war der Bug und ist da jetzt C dran Schuld. Das wäre mir zu anstrengend, aber zum Glück haben wir es andere schon mal gemacht. Da gibt es so eine Studie von ein paar Leuten vom MIT. Die haben sich 65 Code Execution Bugs angeschaut in Linux, die in 2017 gefunden wurden und sind die einfach mal alle durchgegangen und haben geschaut, was sind das für ein Bug und hätte der verhindert werden können, wenn es einer speichersicheren Hochsprache geschrieben worden wäre. Nun, die sind dann zu diesem Ergebnis gekommen, dass bei 17% von den analysierten Bugs war es einfach irgendwie unklar, weil das ist ja auch immer schwer zu sagen, wenn man zum Low Level Code ist, hätte das da wirklich die Sprache geholfen, weil oft braucht man ja wirklich irgendwelche Low Level Sachen, woan dann die beste Sprache nicht helfen kann, wenn man da irgendwie was falsch macht. Aber bei 22% haben sie ganz klar gesagt, nein, eine bessere Sprache hätte nicht geholfen, weil das ist ganz klar ein Logikfehler passiert. Aber bei 12% ist ein ganz klassischer Use After Free, was ja ganz beliebter Bug ist in so einer Sprache wie C. Und noch schlimmer, bei 49% ist einfach Bounce Checks fehlt in C. Ich lese was, ob es Bounce nicht gecheckt, ob es kaputt. Die Hochsprache hilft dagegen, dass ich da dann natürlich stattdessen eine Kernel Panic bekomme. Okay, aber lieber bekomme ich eine Kernel Panic, als dass irgendwie der Angreifer beliebigen Code ausführen darf. Nun, hätte hier also die bessere Sprache schon viel geholfen für Betriebssysteme, wenn eben nicht das ganze Betriebssystem in C geschrieben wäre. Sollten wir also folglich alle Betriebssysteme in besseren Sprachen schreiben, ja, kann man probieren, es gibt da verschiedene Ansätze. Das ist jetzt bei Weitem keine vollständige Liste. Es gibt eine Menge Leute, die mal versucht haben, hey, wie sind das Betriebssysteme schreiben, nehmen wir mal eine andere Sprache. Wir haben uns hier als Redox Invest mal rausgesucht, weil das so das aktuelle, das heiße Zeug ist, was jetzt, was explizit auch das Ziel hat, irgendwann mal Linux auf dem Laptop oder auf dem Server zu ersetzen. Das ist aber noch lange nicht, nicht soweit. Simon hier macht gerade eine Seminararbeit bei uns vor sich, Redox-Netzwerk-Treiber ein bisschen anschaut. Und ja, sagen wir mal so, Redox bootet zwar schon, aber hat so ein paar Probleme noch. Ja, dann gibt es diese ganzen Research-Betriebssysteme, das Singularity von Microsoft ist relativ cool, die ich mal weiterentwickelt. Das Biscuit ist ein Betriebssystem in Go. Das war die Studie über Debux vorher, das war in dem Kontext von diesem Biscuit-Betriebssystem. Es ist übrigens ein sehr gutes Paper, das da auf der Seite vorher verlinkt war, kann man auch mal lesen. Dann natürlich bei Hochsprachen ist man auch schnell bei Unicorns. Das sind jetzt nicht richtig Betriebssysteme, aber gerade Mirage ist da sehr beliebt. Wir haben da auch unseren, wir haben an Treibern O'Cammill mal geschrieben, den wir dann in Mirage reingefrickelt haben. Ja, es ist jetzt kein richtiges Betriebssystem in Sinne und oder anders zu sagen, keins von diesen aus der Liste wird jetzt in nächster Zeit das Betriebssystem auf meinem Laptop ersetzen, vermutlich, das wird hier irgendwie C bleiben. Nun, Betriebssysteme in der Sprache neu schreiben, unrealistisch blöd. Aber schauen wir uns da an, was waren das denn genauer für Fehler? Da wurden ja jetzt 40 Fehler identifiziert, die man hätte verhindern können, wenn die Sprache besser gewesen wäre. Haben wir uns alle mal näher angeschaut, wir sind da jeweils super gegangen, den Patch angeschaut von der Sicherheitslücke und geschaut, wo waren das? Ja, 39 davon waren in Treibern und der 40. im Bluetooth-Stack. Und da ist jetzt auch keiner so total überrascht, dass da jetzt irgendwie die Treiber schlechtere Codequalität haben als der Rest zum König. Das ist jetzt nicht so, oh nein, wer hätte das gedacht? Wenn man sich da mal genauer anschaut und so nach Treiber aufschlüsselt, dann hat man das negativ aufgefallen. Ja, also da ist jetzt auch keiner sonderbar überrascht. Aber um es anders zu sagen, man hätte einige von diesen kritischen Bugs einfach verhindern können, wenn dieser Qualcomm Wi-Fi-Treiber halt einfach irgendwie eine ordentliche Sprache geschrieben worden wäre, weil das waren alles so Out-of-Bound-Successes und Use After Free und so was. Es muss halt einfach heutzutage eigentlich nicht mehr sein. Gut, aber dann sind die Bugs wohl in dem eher gammeligen Teil wie gesagt keiner groß überrascht. Und dann ist die Frage, okay, schreiben wir halt nur die Treiber neu, weil ich muss ja nicht das ganze Betriebssystem noch schreiben, schreibe ich nur die Treiber neu. Es ist so in Linux etwa, die Zahl ist in dem Kopf 50% bis 60% von Codes in etwa treiber und die Sache ist, ja, es gibt schon Betriebssysteme, die haben die Treiber teilweise in besseren Sprachen. Es gibt teilweise Betriebssysteme, da sind die Treiber in so Subsets von C++, aber es meistens halt auch C++, sondern eher irgendwie, wir haben jetzt hier irgendwie so ein bisschen Klassen drin, aber eh. Dann, ich kann es natürlich anfangen, den Kernelmodul in was zu schreiben, zum Beispiel, aber das kriege ich in Linux nie upstream, das könnte schwierig werden, aber ich kann es tun. Ja, wir haben uns das im konkreten User Space Treiber angeschaut, weil man da einfach nicht diese ganzen Einschränkungen hat und sagen einfach, hey, läuft jetzt hier als normaler Prozess so ein bisschen Mikro-Colonel-Style und kann ich jetzt in jeder Sprache schreiben, da gibt es hier keine Einschränkung mehr und die Frage ist dann, ist in jede Sprache eine gute Wahl dafür? Gibt es eine Menge Sprache? Schreibe ich jetzt meine Treiber irgendwie in Peißen oder nicht? Oder habe ich Probleme, wenn ich so eine Sprache habe, die einen Just-in-Time-Compiler hat oder ein Garbage-Collector hat und da stellen sich dann natürlich jede Menge spannende Fragestellungen, die man sich dann so mal näher anschauen kann. Und wir haben das so ein bisschen gemacht, im konkreten für Netzwerktreiber. Gut, warum schauen wir uns Netzwerktreiber an? Ja, einfach weil wir viel über Netzwerk wissen, weil Netzwerktreiber ein relativ dankbares Ziel für da was sind. Es ist super easy irgendwie am Ende das Quantitativ zu analysieren und zu sagen, hey, das ist jetzt X-Prozent langsamer geworden dadurch, Y-Prozent schneller. Da tue ich mich bei anderen Treibern schwerer, so was genau auszumessen. Und Netzwerktreiber haben natürlich eine gigantische Oberfläche, die einfach so Richtung offene Welt offen ist, weil klar, die müssen die Pakete annehmen, aber die Pakete ist besonders blöd. Ja, außerdem sind Netzwerktreiber im User Space schon relativ weit verbreitet. Da zum Beispiel gibt es DPDK oder Snap oder so was, das sind alles User Space-Treiber, die wurden aus Performance-Grunden ursprünglich gemacht. Aber ja, dann Netzwerkstacks selber, die bewegen sich auch immer mehr Richtung User Space. Beispiel, auf meinem Handy, auf dem iPhone, da läuft irgendwie der TCP-Stack als User Space-Prozess, wenn ich hier irgendwie ein Quick-Stack in Chrome drinnen habe, dann ist das eine Library, die in Chrome reingelingt ist und unten runter im Köln wird noch das dumme UDP benutzt, aber das ganze harte ist eh schon im User Space. Dadurch ist es ein relativ gutes und einfaches Ziel. Aber, alles was wir hier sagen, lässt sich größtenteils auch auf andere Treiber anwenden. Es gibt auch User Space-Treiber für andere Sachen. Bekanntes sind natürlich NVMe User Space-Treiber als PDK zum Beispiel, wo einfach Performance gebraucht wurde für NVMe-Devices, war auch ein User Space-Treiber geschrieben, das ist meistens die Hauptmotivation, dass es Performance ist. Also, alles hier allgemein anwendbar. Geht weiter, wenn wir uns jetzt die Netzwerktreiber mal genauer anschauen, haben wir hier auch, können wir es ganz interessantes machen. Wir können jetzt nämlich mal einfach anschauen, für was für eine Technologie die Dinger waren, die Gigabit Ethernet, weil es für ein 100 Gigabit Ethernet und dagegen mal plotten die Komplex, die sind. Und dadurch sieht man gut, dass das eben immer weiter wächst. Und zwar gibt es da so ein bisschen eine lineare Beziehung zwischen Geschwindigkeit und Lines of Code. Geschwindigkeit wächst immer exponentiell, so immer Faktor 10 hoch. Dementsprechend ziehen die Lines of Code auch mit. Beachten, dass das eine logarithmische Achse ist auf der Y-Achse. Das heißt, während beim 10 Gigabit Treiber ist der größte bei, ich glaube, 120.000 Zeilen Code oder so. Das ist einfach mit Lines of Code Counter, also ohne Weitspace und ohne Kommentare. Das heißt, das sind schon so gigantische Komplexitäten, Monster, die da irgendwo rumlungern im Körnel. Und das wird jetzt auch immer nur noch schlimmer werden, wenn da immer mehr Magic reinkommt. Nun, das ist blöd, wenn das alles so kompliziert ist. Deswegen habe ich schon vor anderthalb Jahren mal selber so ein C-Userspace-Netzwerktreiber geschrieben. Das ist ein Talk auf dem 34 C3, der ist irgendwie aufgezeichnet, kann man sich auch anschauen. Und da erzähle ich kurz, wie man so ein Treiber schreibt. Prinzipiell habe ich da diesen XC-Treiber gebaut und das ist einfach ein Treiber, der zeigt allgemein so, zeigt, wie baut man so ein Netzwerktreiber, was ist das Minimal-Set, was man implementieren muss und was sind hier überhaupt diese ganzen, welche Syscalls muss ich an welcher Stelle machen, um hier irgendwie das ganze Zeug in den User-Space zu mapen. Das habe ich, glaube ich, ganz gut kommentiert. Da sind überall so Referenzen auf die Datenblätter, auf Spezifikationen und so weiter. Und sind trotzdem nur tausend Zeilen C-Codes. Am Ende kann es halt Paketempfangen weiterleiten und so die Basisfunktionalität machen. Zum Vergleich, der Original-Intense-Reiber für die gleiche Netzwerkkarte ist 30.000 in Linux bzw. 38.000 in der User-Space-Variante. Aber dafür ist da halt auch eine Menge Magic drin, wie zum Beispiel irgendwie das allein 2.000 oder 3.000 Zeilen zum Senden was ich in 100 Zeilen hinkriege. Aber dafür fange ich nicht alle Fälle ab, meine CPU kann AVX2 und ich habe mindestens 4 Pakete, dann ist da eine extra Implementierung, die nur raus AVX in 206 besteht, die keiner versteht. Ja, ich habe das damals natürlich in C geschrieben, weil es war ja gedacht, dass es dafür da ist, um Leuten zu erklären, wie das funktioniert und C ist das halt was alles, was jeder kann. Ja, ist natürlich die nächste Frage. Ja, das muss man jetzt natürlich in einer besseren Sprache schreiben. Und in welcher Sprache? Habe ich mal überlegt, was kann ich so für Sprachen oder was für Sprachen kann ich gut genug, dass ich mir sicher wäre, ich könnte auf einen guten Treiber kommen, der halt halbwegs so aussieht, dann bin ich auf eine Hand voll Sprachen gekommen und war dann irgendwie zu wenig, da müssen mehr Sprachen her, aber es ist auch irgendwie verdammt viel Arbeit, das ganze Zeug zu schreiben. Zum Glück arbeite ich in einer Uni und muss nicht selber arbeiten, sondern kann einfach so Arbeiten ausschreiben hier Bachelorarbeit, Masterarbeit, sonst was. Brauche ein Treiber in Sprache, A, B, C, D und dann tauchen irgendwie so eine Menge Studenten auf, den habe ich dann allen erklärt, dass das super kompliziert wird, und habe die erstmal versucht zu verschrecken, weil das schon ziemlich türkli Thema ist, aber so manche hier sind dann irgendwie trotzdem geblieben und müssen jetzt hier sein, voll blöd. Genau, und da hatte ich dann so letztes Jahr war das immer so eine große Gruppe von Studenten, was erzählt habe, wie das so läuft. Und Simon wird jetzt hier weiter erzählen, was ich prinzipiell eben letztes Jahr erzählt habe, wie das um Allgemeinen läuft. Ja, Simon. Dein Mikrofon. Ja, danke Paul. Ja, was hat Paul uns so gesagt? Naja, wichtig ist erstmal zu verstehen, wie eigentlich Gerät und Treiber miteinander kommunizieren. Dafür gibt es prinzipiell drei Möglichkeiten, MemoryMap.io, Direct Memory Access und Interrupt. Das heißt, wir haben eine magische Datei für unser Gerät, die wir in den Arbeitsspeicher des Treibers mapen können und über den mapen Speicherbereich können wir dann auf den Speicher des Geräts zugreifen zu kommuniziert, der Treiber mit dem Gerät. Dann haben wir Direct Memory Access. Damit kann das Gerät direkt auf den Hauptspeicher des Systems zugreifen. Das heißt, hier geht die Kommunikation vom Gerät aus und wir haben Interrupt. Man hört ja oft... Das war ein Interrupt. Genau, man hört ja oft, dass User Space Treiber keine Interrupts können. Das stimmt aber nicht. Wir haben schon den Prototypen implementiert mit Interrupts, aber wir werden jetzt aus zeitlichen Gründen nicht weiter in unserem Vortrag darauf eingehen. Genau. Normalerweise laufen so Treiber hier an Kernel. Wir wollen unseren Treiber aber im User Space laufen lassen. Was braucht man dafür oder was ist da anders? Paul sagt immer, das sind vier einfache Schritte und schon ist man fertig mit seinem User Space Treiber. Als erstes muss man... Sobald ihr einfache Betschlabert... Genau, super einfache Betschlabert. Als erstes muss man den Kernel Treiber rauswerfen, dann diesen PCIe Memory, diese MemoryMap IO-Adress-Bereich muss man MemoryMapen, dann die physischen Adressen für den Direct Memory Access herausfinden und dann muss man nur den Treiber schreiben und schon ist man fertig. Genau, ein Netzwerktreiber brauchen natürlich Netzwerkkarten. Paul hat uns dafür ein paar gegeben. Paul, was für Netzwerkkarten hast du uns denn gegeben und warum? Das ist natürlich meine Lieblings-Netzwerkkarte die Intel 8259. Jeder, der mal irgendwie so ein Netzwerkkartentreiber entwickelt hat oder was damit gemacht hat, der weiß, dass es einfach die beste Netzwerkkarte ist. Die sieht man auch super häufig. Die findet man eigentlich in jedem zweiten Server findet man auch die Menge als On-Board-Chips auf so Server-Mainboards. Das ist super oft so eine 8259 und ich mag die einfach super gerne, weil da einfach noch nicht so viel Magie in so einer Blackbox-Firmenbeer versteckt ist, sondern da ist das ganze Zeug einfach noch im Treiber drinnen. Und wenn ich das mit so einer neuen Netzwerkkarte vergleiche, wo ich immer nur noch sagen muss, der Firmenbeer mach mal bitte X für mich. Hier muss ich noch irgendwie selber an die Strukturen und da ist super gutes Datenblatt dabei. Einfach so ein 1200 Seiten PDF, was man brauchen muss. Es kann man dann den Studenten geben und sagen implementier mal und dann ist das auch voll praktisch. Ja, aber Simon, weiter. Genau, also Pause hat implementier mal. Na ja, und jetzt. Also das Erste, was wir tun müssen, wir hatten ja vorher die vier Schritte. Man will irgendwie den Kerneldreiber erstmal rauswerfen, dafür muss man herausfinden, wo steckt denn eigentlich meine Netzwerkkarte drin? An welchen PCI-Puzz ist die angeschlossen? Was ist überhaupt die Adresse von dieser Karte? Dafür kann man einfach LSPCI auf seiner Shell eingeben und dann bekommt man da ganz schön viele Geräte ausgegeben und ganz am Anfang vor dem Gerätennamen sieht man so eine schöne Nummer. Das ist die Adresse im PCI-Puzz und die kann man dann einfach verwenden, um in eine magische, in eine andere magische Datei zu schreiben und dann gibt der Kernel diese Karte frei. Ja, wenn der Kerneldreiber heraus ist, dann können wir den Speicherbereich der Karte in den Arbeitsspeicher des Reibers mapen und dann eben mit dieser Karte arbeiten. Dafür braucht man halt ein paar Suscoys. Wichtig ist, das Ganze kann man nur als Route Nutzer machen. Genau, auf den Code will ich jetzt gar nicht so genauer eingehen. Das schauen wir uns später noch ein bisschen genauer an. Ja, was findet man im Speicher von so einer Karte? Ganz schön viele Register. Da gibt es ein langes Handbuch von Intel, wo diese ganzen Register drinstehen. Das schaut zum Beispiel so aus wie hier auf dieser Folie. Da steht dann der Name von so einem Register und das Offset im Speicher, wo man das Register findet. Und da gibt es dann zum Beispiel Register für die LEDs von der Netzwerkarte. Da kann man dann lesen und reinschreiben und dann kann man die LEDs von der Netzwerkarte blinken lassen. Genau, in C ist das Ganze sehr einfach, aber man kann das halt auch in anderen Sprachen machen, zum Beispiel in Rust. Ja, wie kommen jetzt die Netzwerkarte von unserem Netzwerk zu unserem Programm? Na ja, wir sagen der Karte einfach, an welcher Stelle im Speicher sie die Pakete ablegen soll und warten dann darauf, dass Pakete ankommen und die Netzwerkarte die Pakete dahin legt. Genau, da gibt es natürlich immer ein bisschen Unterschiede zwischen den einzelnen Geräten, aber im Allgemeinen funktioniert das alles sehr ähnlich mit den verschiedenen PCI-Geräten. Welche Sprachfeatures braucht man, um einen Treiber im User Space zu implementieren? Im Allgemeinen ziemlich viel Low Level Zug auf diverse Sachen, die man aber in fast allen Sprachen hinbekommt. Genau, und damit gebe ich jetzt zurück an Paul, der was zu den einzelnen Sprachen erzählen wird, in denen wir die Treiber implementiert haben. Genau, prinzipiell, man kriegt das in eigentlich jeder Sprache hin, habe ich immer gesagt, es ist halt mehr oder weniger Frecklik, aber lösbar ist es. Wir haben jetzt fertige Implementierung in C-Sharp, Swift, O'Cammel, Haskell, Go, Rust und Python. Die Auswahl der Sprachen, das ist einfach nur, wo viele Studenten gefunden haben. Da ist jetzt keine höhere Idee dahinter, sondern einfach nur Studenten dafür gefunden, passt. Aktuell laufen noch Arbeiten, ein Javascript und Java, es wird auch noch spannend, vor allem ein Javascript-Treiber freue ich mich schon sehr. Dann, naja, was waren die Ziele von diesen Implementierungen genau? Ich habe natürlich nicht jedem gesagt, ja, machen wir irgendeinen Treiber, sondern die Idee war, dass das die gleichen Features wie mein C-Treiber implementiert und dass es auch irgendwie so groben die gleiche Architektur ist, aber gleichzeitig sollte es natürlich idiomatischer Code für die Sprache werden und gleichzeitig sollten natürlich alle Sicherheitsfeatures von der Sprache verwendet werden, womöglich und zieht es dann eben irgendwie so zu quantifizieren, was kostet mich denn diese Sicherheit, die ich jetzt gewonnen habe? Was habe ich denn für Probleme, wenn ich das aktiviere und wie läuft denn das? Wenn wir jetzt mal diese verschiedenen Sprachen auf einem ganz hohen Level anschauen. Übrigens, wir schauen jetzt hier nur einen hohen Level in die meisten Sprachen rein, außer in Rust. Wir haben auf unserem Congress Talk in DeepDive auch noch ein Go und ein paar andere Sprachen ein bisschen Einblick gehabt. Wir sind das interessiert, den Talk vom 35-Ziterei von uns raussuchen. Aber hier jetzt mal so ein High Level Overview-Vergleich über all diese Sprachen. Schauen wir uns mal an, was haben wir hier? Hauptsächlich sind wir natürlich an Speicherverwaltung und wie es kompiliert wird, interessiert. Und da Speicherverwaltung ist natürlich ein wichtiger Aspekt für diese ganzen Speicher-Bugs. Da ist natürlich bei C-Speicherverwaltung Nein, also manuell und die anderen Sprachen, da ist eigentlich, ja, garbage collected alles. Dann zwirft ein bisschen Special Unique Snowflake, garbage collector mit Reference Counting. Rust ist irgendwie dann super special und da kommen wir nachher nochmal zu, was das hier bedeutet. Und es wird eigentlich fast alles kompiled, außer dass die Sharp, was ein Just-in-Time-Compiler hat und peissen, was halt interpretiert wird. Und gut, haben wir diese Sprachen mit den groben Eigenschaften. Was bedeuten diese Eigenschaften jetzt für die Sicherheitsfeatures, die uns die Sprache garantieren kann in dem Treiber. Da haben wir so eine Tabelle, sieht man die Sprache und dann sieht man Bounce checks und use after free, das einmal für Allgemeinspeicher und einmal für die Paketbuffer beziehungsweise DMA-Buffer. DMA-Buffer sind auch immer so ein bisschen special, weil die brauchen immer so ein bisschen extra, weil dem muss man sich besonders alluzieren, dass noch ein paar Implementierungsdetails. Auf jeden Fall kann ich nicht einfach das Melok oder die native Allokation von der Sprache benutzen, um den DMA-Buffer geben zu lassen. Es geht einfach nicht. Und dadurch fallen ein paar von den Sicherheitsfeatures runter. Und hinten haben wir noch Integeroverflows dazu genommen, aus dem einfachen Grund, dass wir da mal so ein Praktikum hatten, wo irgendwie Studenten C schreiben mussten und dann haben irgendwie super viele Integer an der Flows eingebaut. Keine Ahnung, warum. Deshalb haben wir geschaut, ob es dagegen auch noch was hilft. Ja, wenn wir jetzt die Tabelle ausfüllen mit den ganzen Sprachen, dann sehen wir, dass eigentlich bei den allgemeinen Speicher können die alle alles. Aber bei Paketbuffern ist das alles so ein bisschen eingeklammert, weil es da verschiedene Einschränkungen in verschiedenen Szenarien gibt, wofür wir am Rust Beispiel nachher einfach noch mal mehr sehen. Und Integeroverflows dagegen helfen eigentlich nur wenig Sprachen, das sind eigentlich nur Swift und Rust, wobei man es bei Rust eben explizit aktivieren muss. Und gut, haben wir also diese Sprachen und das ist die nächste Frage. Was kostet uns der ganze Spaß? Erstmal im Sinne von, wie viel Code muss ich denn mehr schreiben, um diese Sicherheitsfeatures zu bekommen. Das sind jetzt Line of Code Counts, wieder ohne Lehrzeilen, Kommentare und so weiter und ohne die Registerdefinition, weil viele halt einfach irgendwie die Registerdefinition mit einem regulären Ausdruck erzeugt haben und dann kamen halt irgendwie 1.000 Zeilen Registerdefinitionen raus. Aber die Grundlagen von dem C-Tribal sind zum Beispiel 830 Zeilen etwa während Rust zum Beispiel etwa 100 Zeilen mehr hat. Wenn man sich jetzt aber anschaut, die tatsächliche Größe vom Source Code ist etwas leicht kleiner, liegt daran, dass Rust Format einfach super kurze Zeilen gerne macht, während ich den C halt eher tendenziell längere Zeilen geschrieben habe. Aber prinzipiell sieht man, das ist alles so in der gleichen Größenordnung. Man kann sagen, wenn es in C um die 1.000 Zeilen sind für den Treiber, dann sind es in den anderen Sprachen auch so ungefähr um die 1.000 Zeilen. Und vieles ist so ein One-Time-Overheit. Ich muss halt einmal die notwendigen Primitiven implementieren, um den Rest darauf aufzubauen. Und dann kann ich es eigentlich in der Hochsprache einfacher schreiben als in C. Aber der Anfang ist immer ziemlich hart. Nun, wir schauen uns jetzt eine Sprache näher an, weil Simon dieses Jahr hier ist eben Rust. Und ja, Simon. Ja, danke Paul. Wer hat denn hier schon mal was mit Rust gemacht? Cool, doch einige. Da muss ich ja gar nicht mehr so viel zu Rust erzählen. Genau, also ich vermute mal die meisten des Rust-Begriffes, ihr habt vielleicht noch nicht was damit gemacht, aber hat vermutlich jeder schon mal gehört. Was ist Rust überhaupt? Naja, es ist vor allem eine sichere Systemprogrammiersprache. Rust hat keinen Garbage-Collector, dafür dieses einmalige Ownership-System, von dem ihr vielleicht schon mal gehört habt. Es ist einfach ein Set an Regeln, wie mit Variablen umgegangen wird. Und einen Unsafe Mode, weil leider nicht alles in sicherem Rust-Code möglich ist. Genau, wie funktioniert es mit der Speichersicherheit in Rust? Wie gesagt, es gibt dieses einmalige Ownership-System. Das sind an sich drei einfache Regeln, wie eben mit Variablen umgegangen wird. Also es geht vor allem darum, dass so eine Variable bestimmt einen Eigentümer hat und immer nur einem Eigentümer gehören kann. Und diese Regeln werden von Rust für Compile-Zeit durchgesetzt. Das heißt, beim Komplieren von dem Programm überprüft Rust, ob ihr alle Regeln eingehalten habt. Und wenn dem nicht so ist, dann kompliziert euer Programm einfach nicht. Und zur Laufzeit ist der Unterschied dann gar nicht mehr so groß. Also deswegen ist Rust auch so performant, weil, wie gesagt, diese Regeln werden zur Compile-Zeit durchgesetzt und so ein kompliziertes Rust-Programm ist dann gar nicht so anders zu einem komplierten C oder C++-Programm. Ja, wie schaut das Ownership-System in unserem Treiber aus? Wie haben wir das eingesetzt? Naja, für unsere Netzwerkpakete haben wir Package-Strucks, genauso wie im C-Treiber. Aber im Rust-Treiber sind diese Package-Strucks einfach Eigentümer von dem Speicherbereich, von einem Teil vom DMR-Speicher. Und dieser Teil von dem DMR-Speicher, das sind die Paketdaten drinnen von dem jeweiligen Paket. Und dieser Speicherbereich gehört einfach dem Paket. Und das ist an sich eine recht coole Sache, weil, wenn wir dieses Paket vom Nutzer an den Treiber übergeben, dann wird die Ownership mit dem Paket mitübertragen. Und wenn wir das Paket wieder zurückgeben, wird es auch wieder zurück an den Nutzer übertragen. Und so gehört das Paket immer genau einem, entweder dem Treiber oder dem Nutzer. Und dadurch haben wir einfach Speichersicherheit. Also es kann nicht passieren, dass der Treiber und der Nutzer gleichzeitig auf die Paketdaten zugreifen. Und unten auf der Folie sieht man jetzt, wie der Treiber verwendet wird in einer normalen Anwendung. Das ist an sich recht wenig Code, den man da braucht. Also man muss davor noch die Netzwerkarte initialisieren. Das fehlt jetzt hier. Das ist aber auch nur eine Zeile mehr. Und dann hat man einfach den Buffer. Man kann eben hier Pakete empfangen. Dann so ein Paket modifizieren. Da ändern wir jetzt einfach einen Byte. Das implementieren wir um eins. Und dann kann man die Pakete wieder verschicken. Und das Schöne ist, man kann halt hier nichts falsch machen. Also man kann nicht vergessen, die Pakete wieder zu freien, weil die werden einfach automatisch gefreet, wenn die out of scope gehen. Also ein Rust gibt es immer so. Bereiche Scopes, in denen Variablen gültig sind. Und wenn so eine variable out of scope geht, dann wird halt der Wert getroppt. Genau, leider ist eben nicht alles in safe Rustcode möglich. Auch nicht in unserem Treiber. Zum Beispiel, externe Funktionen aufzurufen, ist einfach immer unsave und pointer zu differenzieren. Aber das ist nichts Ungewöhnliches. Die Idee ist einfach, unsicheren Code hinter einer sicheren Schnittstelle zu verstecken. Das ist auch in der Rust-Sendert-Library. Also das relativ viel unsave Code drin. Aber man versucht einfach zu verifizieren, dass der unsave Code eigentlich doch nicht unsave ist. Also ich habe da ein Beispiel dafür mit dabei, wie es in unserem Treiber gelöst ist. Oder eine Stelle, wo wir unsicheren Code verwenden. Das ist die Funktion, um ein Register zu setzen. Da übergeben wir einfach das Offset von dem Register, für das wir den Wert setzen wollen. Und an die Adresse des Registers zu schreiben, dafür haben wir eben einen Pointer, wo wir hinschreiben wollen. Und wie ich vorhin schon gesagt habe, so ein Pointer zu differenzieren ist einfach immer unsave. Deswegen ist da ein unsave Block außen rum. Aber wir überprüfen eben, ob das Register wirklich in dem Speicherbereich ist, den wir in unseren Speicher gemappt haben. Und wenn dem nicht so ist, dann schreiben wir halt da nicht hin und damit garantieren wir, dass wir hier nicht an irgendwie unzulässige Speicherstellen schreiben. Ja, jetzt haben wir irgendwie schön Code. Aber ist der Code auch schnell? Dafür haben wir ein Test Setup aufgebaut. Wir haben für die ganzen Treiber Forwarder Applications programmiert. Die haben wir auf einem Server laufen lassen, der dann Pakete zugeschickt bekommen hat von dem anderen Server. Also wir haben zwei Servers. Der eine Server mit der Forwarder Application drauf und den anderen Server mit einem Paketgenerator. Das Paketgenerator haben wir Moonchain verwendet, den Paul programmiert hat. Und damit natürlich der beste Paketgenerator ist. Und die zwei Servers sind einfach über 10 Gigabit Links verbunden. Und die Forwarder Anwendung schickt die Pakete dann über zwei Ports hin und her. Die zwei Richtungen über zwei Ports, zwei Verbindungen. Das heißt, an sich haben wir 20 Gigabit, über die wir Pakete schicken. Ja, was haben wir uns angeschaut? Vor allem erstmal den Durchsetz und der Einfluss von Batching. Batching heißt, wir haben mehrere Pakete, die auf einmal vom Treiber an die Netzwerkarte geschickt werden. Das ist so der Hauptperformance-Vorteil von Userspace-Dreibern gegenüber Kerneldreibern. Wir haben uns vor allem die Paketrate angeschaut, weil der Overhead pro Paket entsteht und nicht pro Byte. Und die maximale Rate bei 2 x 10 Gigabit sind ungefähr 29 Millionen Pakete. Und wie man sieht, kommt C und Raster auch recht nah dran und sind auch ungefähr gleich schnell. Ein bisschen langsamer sind, so die funktionalen Programmiersprachen. Und mit Abstand am Langsamsten ist, wie man sieht, Pfeifen. Vielleicht eine nette Anekdote an der Stelle. Als Paul mir zum ersten Mal den Grafen geschickt hat, habe ich ihn gefragt, wann er die Daten für Pfeifen noch reintut. Das sind tatsächlich die Daten für Pfeifen. Also Pfeifen verschickt halt so 100.000 Pakete in der Sekunde. Da muss man halt leider dann ein bisschen reinzuben, damit man das sieht. Ja, wir haben uns ein bisschen gewundert, warum Swift eigentlich so langsam ist. Weil bei den funktionalen Programmiersprachen versteht man das vielleicht noch, dass da ein Overhead da ist. Aber warum eigentlich bei Swift? Und dann haben wir uns das ein bisschen genauer angeschaut und stellt sich heraus, dass Swift einfach vollkommen umgeeignet ist. Wenn man sehr oft sehr kleine Allokationen hat, was eben bei diesen Paketen dann der Fall ist. Also wenn wir eben Millionen von Paketen in der Sekunde empfangen und verschicken wollen, dann wird da sehr viel alloziert und dealloziert. Das heißt, wir haben einfach sehr viele Release und Retain Calls in Swift. Und das ist ein riesiger Overhead für den Referenz-Zellen-Gabbage-Collector. Genau, dann haben wir uns noch angeschaut, warum ist Rust eigentlich ein bisschen langsamer als C? Dafür haben wir uns den CPU Performance Counter angeschaut. Der zeigt uns, was die CPU tut, wenn das Programm läuft. Das sind jetzt hier recht viele Daten auf der Tabelle, aber wir schauen uns da nur einen kleinen Ausschnitt davon an. Und zwar vor allem die Instruktionen und die CPU-Zyklen. Wie man sieht, hat Rust hier 209 Instruktionen pro Paket und C führt 127 Instruktionen pro Paket aus. Gleichzeitig haben wir bei Rust ungefähr 100 CPU-Tagzyklen und bei C 94-Tagzyklen. Das heißt, Rust führt in 6-Tagzyklen mehr, ungefähr 80 Instruktionen mehr aus. Das war eine sehr interessante Erkenntnis, dass Rust hier einfach eine deutlich höhere Dichter Instruktion hat, also deutlich mehr Instruktionen pro CPU-Tagzyklus. Genau, wir haben uns aber nicht nur das angeschaut, sondern auch noch andere Messungen durchgeführt und dafür gebe ich jetzt an Paul zurück. Danke, Simon. Aber hier ist jetzt endlich mal so Speculative Execution und so was für was gut, was nicht Sicherheitslücken sind, weil das wird natürlich alles von der Out-of-Order-Execution gefressen, diese ganzen Sicherheitschecks. Performance ist nicht nur, wie viele Pakete kann ich da pro Zeit durchschaufeln, sondern auch, wie lange brauche ich, bis das Paket wieder auf der anderen Seite rauskommt in Mikrosekunden. Das haben wir für all unsere Vorwörter mal gemessen. Das ist ja ein bisschen komplizierteres Test-Setup. Da haben wir dann noch so Glasfaser und Fiber-Optics-Blitters und einem dedizierten Hardware-Time-Stamping-Device, um halt auch diese Nanosekunden der Will-Time-Stamps zu bekommen und auch alle Pakete zu Time-Stampen. Und was man hier jetzt sieht, ist ein sogenanntes HDR-Histogramm. Das ist einfach eine Latentsverteilung mit Fokus auf die sogenannte Tail-Latency. Wir sind daran interessiert, wenn irgendwie so ein Treiber zwischendrin mal einen Schluck auf hat, weil das meistens das ist, was einem Probleme bereitet. Wenn ich mir nur den Durchschnitt anschaue oder nur den Medien, dann denke ich oft, das ist voll gut. Aber wenn dann jedes tausendste Paket irgendwie mal stecken bleibt, weil der Garbage-Collector gerade was macht, ist das am Ende oft nicht so gut, weil das tritt verdammt viel öfters zu, als man sich denkt. Ja, wenn ich mir den Graf anschaue und dann sehe ich ja auf der X-Achse das Percentil. Wenn ich jetzt zum Beispiel beim 99. Percentil hochgehe, mir eine der Linien für die Sprache raussuche und dann nach links gehehe auf die Y-Achse, dann komme ich zum Beispiel beim 99. Percentil, bei C, bei irgendwie 7 Mikrosekunden oder so raus. Das heißt, 99 Prozent der Pakete werden in unter 7 Mikrosekunden weitergeleitet. Das heißt, wenn es da hinten hochgeht, dann wird zum Beispiel jedes tausendste Paket nur noch in 100 Mikrosekunden weiterverleitet, weil da irgendwo der Garbage-Collector läuft. Wir haben da viel Arbeit reingesteckt, um die ganzen Garbage-Collectors und so weiter entsprechend zu tunen, damit es überhaupt vernünftig geht. Vor allem Haskell war da ein bisschen schmerzhaft, aber überhaupt hinzukriegen, dass der unter Millisekunden bleibt. Da steckt ein bisschen Arbeit drin und Multi-Sudding darf man auch nicht anschalten in der One-Time, weil sonst geht es um Vollkommen kaputt. Aber da sieht man halt ganz deutlich, ganz unten C und Was, das ist quasi auf derselben Linie. Da ist natürlich bei der Latenz kein Unterschied zu erwarten. Da ist nichts, was das aufhalten kann. Dann Go kommt als Nächstes, das ist ein bisschen langsamer, weil Go den wirklich sehr guten Garbage-Collector hat oder wenn ich so Mikrosekunden, Stopped-The-World-Times maximal haben will. Danach kommen dann halt so die anderen Sprachen und vor allem eben die beiden, die da voll ausreißen. Das ist jetzt eben O'Cammell und Haskell und das ist schon nach sämtlichen Tuning usw. Nun, das war jetzt nur die Messung bei einer Million Pakete pro Sekunde. Was passiert, wenn wir die Paketrate mal verzehnfachen? Hier sind jetzt weniger Sprachen drauf. Das liegt daran, dass wir hier nur noch die messen können, wo auch wirklich keine Pakete verloren gehen, wo wirklich der Durchsatz geschafft wird. Weil in dem Moment, wo das Ding überlastet ist und Pakete verloren gehen, dann messe ich einfach nur die Größe von dem Buffer und dann kommt nichts mehr raus. Der Buffer ist dann irgendwie eine Millisekunde groß und dann ist es immer eine Millisekunde, super. Ja, aber hier weiterhin C und Was, es ist wirklich direkt übereinander, dann kommt Go und dann C-Sharp. Auch übrigens C-Sharp steckt ein bisschen fricke Leidrin wie ein paar Allocations zu entfernen aus dem Code mit irgendwelchen fancy, neuen Stack-Alock, neuen Features und den Gabbage-Collector in Sustained Low Latency Mode zu setzen und so weiter. Standard-Sharp in Standard-Einstellung ist eher bei so 200 Mikrosekunden. Gut, gehen wir nochmal ein bisschen hoch von der Datenrate auf 20 Millionen Pakete pro Sekunde und dann sehen wir jetzt endlich einen Unterschied zwischen C und Was. Das ist einfach deshalb, weil C ist ein bisschen schneller als Was und dadurch kann es effektiv mehr Batches pro Zeit verarbeiten. Das heißt, ein Batch enthält leicht weniger Pakete in C und da irgendwie das Paket an der zuverlägenden Stelle im Batch steht, ist das gut für die Gesamtlatents und Go ist dann hier auch nah an der Überlastgrenze. Zahlen sind leicht anders als der Graph vorher, weil es auf einen anderen CPU lief aus verschiedenen Gründen. Gut, dann haben wir noch hier einen letzten Punkt, bevor wir zum Ende kommen und das ist, wie ist das jetzt mit der Sicherheit, oder wir haben jetzt gesagt, wir haben jetzt die tollen Sprachen, die Treiber sind es alle in super Sprachen, aber vorher haben wir kurz erwähnt, das läuft immer noch mit Root, ja, warum eigentlich. Nun, wir starten es natürlich mit Root, weil ich brauche, die Root-Privilegen einfach um mir diesen PCIe-Speicherbereich zu mapen. Ich brauche das aus verschiedenen, komischen Implementation-Detail-Grunden, um mir den Speicher für DMR zu allokieren und ich muss den Speicher auch locken, wenn nicht irgendwie rausgeswoppt wird, wenn da gerade die Karte reinschreiben will. Das ist natürlich die erste Idee, hey, das brauche ich ja eindeutig nur für Setup, kann ich also das einfach in ein kleines anderes Programm schreiben und mir die Privileges danach wegwerfen. Kann man natürlich machen, ist dann aber nicht gut, also funktioniert da nicht so, wie man erwartet. Dafür schauen wir uns einfach mal an, wie sieht so eine Server-Architektur oder Architektur im High-Level aus. Wir haben hier oben unsere CPU, wo irgendwie die Anwendung läuft und da haben wir so ein PCIe-Gerät und rechts haben wir unseren Hauptspeicher und wenn ich jetzt eine Anwendung bin und ich möchte mir irgendwas in meinem Speicherbereich speichern, dann kann ich mit dem Hauptspeicher reden und dazu gehe ich durch die MMU, das ist die Memory Management Unit. Die Memory Management Unit wird vom Kernel kontrolliert und der Kernel konfiguriert die so, dass eben nur mein Anwendung nur Zugriff auf den eigenen Speicher hat oder worauf ihr Zugriff gegeben wurde und dadurch ist es von Hardware-Seite her gecheckt, dass da nichts schief laufen kann. Analog dazu, wenn ich irgendwie diese magischen Fals nehme, das ist übrigens Teil vom UIOS Hub System in Linux, dann konfiguriert das für mich das so, dass ich auch auf mein PCIe-Gerät zugreifen kann. Nun sieht das alles gut aus, das Problem kommt natürlich, sobald mein PCIe-Gerät sagt, hey, ich möchte es auch mal auf den Speicher zugreifen, das geht nämlich dann einfach vollkommen an der MMU vorbei und schreibt da direkt irgendwo hin, und zwar wirklich irgendwo hin. Ich habe da ein paar Mal auch so, dass der Teilsystem dann verloren auf dem Server, weil, ups, war wohl der falsche Point da drin, passiert. Die Lösung dafür ist so ein kleiner Hardware-Baustein, die IOMMU. Die IOMMU ist einfach eine Memory Management Unit für IO-Geräte und die stellt sich einfach dazwischen, die ist in jeder modernen CPU die Hardware-Virtualisierungsunterstützung hat, aber vor allem für VMs, um die abzuschotten, wenn man da ein Devicepass so macht. Gut, und dann sieht das Speicherzugriff eben so aus, geht durch die IOMMU, die wird wieder vom Kernel irgendwie konfiguriert und dann muss mein Programm nicht mehr als Hut laufen. Wie läuft das jetzt? Unter Linux ab. Man muss das System erstmal im Allgemeinen vorbereiten und in dem ich das Gerät jetzt nicht keinem Treiberzuweise, sondern so einen speziellen Treiber, der für mich IOMMU-Konfiguration und Speicherallokation übernimmt. Und wenn ich das habe, dann kann ich das Device einfach einem anderen User geben und der darf das dann benutzen, und zwar nur dieses Device. Für den User, der das dann ausführt, der darf sich dieses Device M-Mappen, da gibt es eine ganze Liste an schlecht dokumentierten IOControl-Calls, da liest man dann irgendwie so ein bisschen den Kernelsourcecode, dann findet man raus, was die alles tun und dann kann man sich dort dem Arschspeicher geben lassen vom Kernel, das sogar ein bisschen bessere Möglichkeit dem Arschspeicher sich zu allokieren im User Space. Aber danach kann man das Gerät prinzipiell wie alles andere verwenden. Der einzige Unterschied ist, dass das Setup anders ist und dass jetzt alle Zugriffe durch die IOMMU gecheckt werden. Ich muss mich in der eigentlichen Treiberlogik nicht mehr darum kümmern. Wir haben das in C und in Rust implementiert, so als Prototyp, und dann haben wir uns mal angeschaut, kostet das jetzt eigentlich Performance, ist diese IOMMU eigentlich total gut, das ist ganz spannend. Dieses Ding ist nicht so toll dokumentiert von Intel. So, ob Intel CPUs angeschaut, gut, auch bei AMD gibt es keine ordentliche Doku da, da ist zum Beispiel auch geheim, wie groß der Cash auf dem Ding ist und sowas. Wir sind auf 64 Einträge gekommen, anderen sind auch auf 64 Einträge gekommen. Ja, aber wenn man sich das jetzt hier anschaut, wir schauen jetzt mal die schwarze Linie, das ist unser Basiswert, das ist der gleiche Graf wie der Durchsatztest von vorher und fast der gleiche Graf, das andere CPU. Der Basiswert, da ist zum Beispiel relativ egal, wie ich mir den Speicher aluziere, da kann ich entweder normale Pagegröße oder huge Pages nehmen, das ist für die IOMMU relevant, aber macht keinen Unterschied. Wenn ich mir aber die IOMMU aktiviere und das einfach naiv mache und bei 4 KB Pagegröße bleibe, also den absoluten Defaults, dann ist so, ob das Performance schlecht. Und dann fängt man erstmal an zu Debuggen, schaut, gibt es da eigentlich Performance-Counter, weil für alles andere gibt es Performance-Counter, aber dann haben wir relativ schnell herausgefunden, dass man eben die huge Pagegröße an der Stelle aktivieren muss und dann hat man eigentlich auch keinen Performance-Verlust mehr. Ja, technisches Detail anscheinend ist der TLB 64 Einträgegröße bei der IOMMU, bei der MU 4096 oder anders gesagt, wenn man es richtig macht, ist es auch noch schnell mit der IOMMU und sagen wir nicht zu viele Geräte hat. Gut. Das ist es dann auch so von unserer Seite her gewesen. Da hat noch zu sagen, wir haben hier irgendwie den ganzen Code und den ganzen Sprachen auf GitHub. Da ist der QR-Code bzw. XC-Languages, einfach mal googeln. Das ist so ein Meta-Repository, da ist eine Rhythmiefile drin mit einer Tabelle, da sind die ganzen Arbeiten verlinkt. Da ist der ganze Code verlinkt, die Bachelorarbeiten, die Dokumentation und so weiter. Da kann man sich das einfach mal näher anschauen. Das ist auch verlinkt. Die Folien habe ich hier auch im Fahrplan hochgeladen. Wenn jemand die ganzen Tabelle noch mal ganz sehen will im Detail, weil das so schnell ging. Prinzipiell, wenn hier jemand den nächsten Treiber schreibt, vielleicht mal dran denken, ob der Treiber wirklich im Körnel laufen muss. Meistens ist die Antwort, meistens muss er eben nicht im Körnel sein. Die richtigen Stichworte sind UIO und WFIO. Dann kann ich den Treiber auch in der beliebigen Sprache schreiben. Dann bin ich nicht mehr auf diese Legacy-C-Sachen eingeschränkt. Also einfach dran denken, Treiber in coolen Sprachen schreiben. Ja, danke für die Aufmerksamkeit. Hat jemand mehr noch ganz kurz Zeit für Fragen? Ja. Ihr habt am Anfang so ein bisschen gezeigt, wie viele sicherheitsrelevante Bugs irgendwie im Linux-Körnel drin sind und wie viele ist da die Spracheschuld. Gibt es denn im Prinzip schon Statistiken, die irgendwie sagen, in C gibt's pro 100.000 Zeilen Code so und so viele Bugs sicherheitsrelevante in den Rust so und so viele? Oder kann man das überhaupt vergleichen, weil eine Code-Zeile Rust nicht gleich eine Code-Zeile C gibt's da irgendwie, erkennt du drüber, ob die eine Sprache tatsächlich zu weniger Fehlern führt als die andere? Ja, ich habe da sehr viel gesucht, sehr viel Studien gesucht, ob es da irgendeine gute Quelle gibt. Aber ich habe nichts wirklich Belastbares gefunden, was dann auch wirklich dem standhält, wenn man sich das mal näher anschaut. Weil da muss ich ja irgendwie den Vergleich haben, wo erst mal ähnlicher Code geschrieben wurde. Wenn ich jetzt irgendwie Betriebssysteme mit irgendwelchen Web-Apps vergleiche, ist das vollkommen nach Quatsch. Dann ist die Frage, wer hat den Code geschrieben? Ich habe nichts Belastbares leider gefunden. Bist ihr, wieso die Python-Implementierung so unglaublich langsam war und hätte man die möglicherweise schneller gekriegt, wenn man gesagt hätte, ich lager vielleicht mal so 20, 30, 40 Zeilen von dem Python-Code aus? Der Hauptgrund ist, dass wir einen verständlichen, einfachen Python-Code haben wollten, den man so für Prototyping verwenden kann am Ende. Die Python-Arbeit ist eine Masterarbeit, die jetzt bald fertig sein wird, wird dann auch auf der Seite verlinkt werden. Das Problem war der idiomatische Python-Code, dass wir hier der Zugriff auf die C-Straktion, die ganze geht in einem Quatsch raus und mit PyPy ist es nicht kompatibel aus verschiedenen Gründen. Man hätte den schneller kriegen können, aber dann wäre es nicht mehr so schön gewesen. Es ist prinzipiell kein Grund, wenn man die richtigen Stellen an C auslagert, es PyPy kompatibel hinkriegt und auf einige hübsche Features verzichtet, kriegt man das auch so schnell hin, wie O'Cammel oder Haskell, da sehe ich kein Problem. Ich frage auf Englisch, aber kannst du auf Deutsch antworten? Your setup, the test setup for benchmarking, is it still available? For example, if somebody is to implement a driver in a weird language, are you actually able to retest everything or this setup is already gone? We have a really great test infrastructure. This is all based on life-booting servers, immutable images, so I can run this at any time, basically it's a command, it life-boots the image into them, I can run it whenever. So if you have a great implementation, let me know, happy to test it. Du hattest in der Folie zu den Durchsetzen, da fehlte einen Datenpunkt bei C-Sharp. Hab ich's nur überhört oder gibt es deine Geschichte zu? Wer hat dir gesagt, dass du das fragen sollst? Oder ist dir das jetzt wirklich aufgefallen? Das ist mir schon bei dem Mitschnitt vom C3 aufgefallen, da hatte ich auch schon gefehlt. Der Treiber querscht bei genau dieser Konfiguration, aber nur auf der CPU mit der Taxfrequenz, wenn die CPU runter takte läuft, keine Ahnung. Ich fand das nur interessant, weil das wäre der Punkt gewesen, wo er zu den anderen aufgeschlossen wäre, wenn man so mal naiv extrapoliert. Also es wäre halt cool gewesen, aber wenn es sich funktioniert, ist es schade. Aber ich hab den Datenpunkt für leicht weniger und es schließt nicht auf, es fällt ab. Der Hintergrund ist, die größere Badge size ist schlechter, weil der L1 Cache querscht wird. Habt ihr irgendwelche Bugs im Original C-Code, der als Vorlage gedient gefunden während der Entwicklung? Das wäre noch interessant. Wir haben den C-Code nicht so sehr als vor, also in meinem C-Code jetzt oder in meinem C-Code. Ja, da waren irgendwelche komischen Bugs drin. Ich habe an einer Stelle irgendwie die falsche Variable näher als waren, was wir nicht getestet haben. Ich würde sagen, zwei Bugs haben wir gefunden. Das wäre so ein bisschen ein Hinweis drauf, wie gut C im Vergleich zu dem, was ihr getestet habt. Also der eine Bug wäre durch den Sprache nicht verhindert worden, weil da habe ich, wenn ich die Anzahl der Qs konfiguriere, irgendwie WXQs, die gleiche Zahl für die TXQs genommen, falsche Variable-Namen. Habt ihr auch Messungen gemacht, wie viel langsamer es war, zum originalen Kerneldreiber? Ja, aber ist hier nicht unbedingt unser Ziel, weil das kein fairer Vergleich ist. Der Kerneldreiber macht sehr andere Sachen, hat ein sehr anderes Interface nach oben. Wir sind Faktor 5 oder so, 5 oder 6 schneller als der original Kerneldreiber. Wir sind, es ist kein fairer Vergleich, das ist ein unsinniger Vergleich. Wir sind etwa so schnell wie der XDP Kerneldreiber und wir sind etwa so schnell wie eine ältere DPDK-Version von dem Treiber, aber langsamer als die neuen DPDK-Version, weil die neuen voll mit AVX-22 sind und diesen schneller. Fragen noch? Ja, danke für die ganzen Fragen. Sehr interaktiv hier.