Achtung: enthält Systemprogrammierung. Ja, im Wesentlichen enthält es nichts anderes.
Stellen wir uns vor, Sie hätten die Aufgabe erhalten, ein Fantasy-Fantasy-Spiel zu schreiben. Nun, da über die Elfen. Und über die virtuelle Realität. Von Kindheit an haben Sie davon geträumt, so etwas zu schreiben, und sind ohne zu zögern damit einverstanden. Bald merkt man, dass man den größten Teil der Welt der Elfen aus Witzen aus dem alten Bashorgh und anderen unterschiedlichen Quellen kennt. Ups, ein Problem. Wo unsere nicht verschwunden sind ... Durch reichhaltige Programmiererfahrung gehen Sie zu Google, geben die "Elfenspezifikation" ein und folgen den Links. Oh! Dieser führt zu einer Art PDF ... also, was wir hier haben ... eine Art Elf32_Sword
- Elf32_Sword
- es scheint so, als ob Sie es brauchen. 32 ist anscheinend das Level des Charakters und die beiden Viere in den folgenden Spalten sind wahrscheinlich Schaden. Genau das, was Sie brauchen und außerdem wie systematisiert! ..
Wie in einer Programmieraufgabe der Olympiade nach einigen Absätzen eines detaillierten Textes zum Thema Japan, Samurai und Geisha ausgeführt: „Wie Sie bereits verstanden haben, wird es bei der Aufgabe überhaupt nicht darum gehen.“ Oh ja, der Wettbewerb war natürlich eine Weile. Im Allgemeinen erkläre ich fünf Minuten Hartnäckigkeit für geschlossen.
Heute werde ich versuchen, über das Parsen einer Datei im 64-Bit- ELF- Format zu sprechen. Im Prinzip speichern sie keine nativen Programme, statischen Bibliotheken, dynamischen Bibliotheken, jede implementierungsspezifische wie Crashdumps ... Sie werden beispielsweise unter Linux und vielen anderen Unix-ähnlichen Systemen verwendet, ja, sagen sie, sogar auf Telefonen Die Unterstützung wurde zuvor aktiv in die gepatchte Firmware gestopft. Es scheint schwierig zu sein, das Format zum Speichern von Programmen von seriösen Betriebssystemen zu unterstützen. Also dachte ich. Ja, das ist es wahrscheinlich. Wir werden jedoch einen ganz bestimmten Anwendungsfall unterstützen: das Laden von eBPF-Bytecode aus .o
Dateien. Warum so? Nur für weitere Experimente benötige ich einen ernsthaften (d. H. Nicht kniehohen ) plattformübergreifenden Bytecode, der von C abgerufen und nicht manuell geschrieben werden kann. EBPF ist also einfach und es gibt ein LLVM-Backend dafür. Und ich muss nur ELF als Container analysieren, in den dieser Bytecode vom Compiler gestellt wird.
Nur für den Fall, ich werde klarstellen: Der Artikel ist eine explorative Programmierung und erhebt keinen Anspruch auf Vollständigkeit. Das ultimative Ziel ist es, einen Bootloader zu erstellen, mit dem Sie C-Programme lesen können, die in eBPF mit Clang kompiliert wurden - die, die ich habe -, in einem Volumen, das ausreicht, um die Experimente fortzusetzen.
Überschrift
Beginnend mit dem Nullpunktversatz im ELF liegt der Header. Es enthält genau die Buchstaben E, L, F, die Sie sehen können, wenn Sie versuchen, es mit einem Texteditor zu öffnen, sowie einige globale Variablen. Tatsächlich ist der Header die einzige Struktur in der Datei, die sich an einem festen Versatz befindet, und er enthält Informationen, um den Rest der Struktur zu finden. (Im Folgenden werde ich von der Dokumentation für das 32-Bit-Format und von elf.h
, die sich mit 64-Bit elf.h
. Wenn Sie also Fehler bemerken, können Sie diese elf.h
korrigieren.)
Das erste, was uns in der Datei begegnet, ist das unsigned char e_ident[16]
. Erinnern Sie sich an diese lustigen Artikel in der Reihe "Alle folgenden Aussagen sind falsch"? Hier ist es ungefähr gleich: ELF kann 32- oder 64-Bit-Code, Little oder Big Endian und sogar ein Dutzend Prozessorarchitekturen enthalten. Sie werden es als Elf64 unter Little Endian lesen - nun, viel Glück ... Diese Reihe von Bytes ist eine Art Signatur dessen, was sich darin befindet und wie man es analysiert.
Mit den ersten vier Bytes ist alles einfach - es ist [0x7f, 'E', 'L', 'F']
. Wenn sie nicht übereinstimmen, gibt es Grund zu der Annahme, dass es sich um falsche Bienen handelt. Das nächste Byte enthält die Klasse Charakter Datei: ELFCLASS32
oder ELFCLASS64
- Bittiefe. Der Einfachheit halber arbeiten wir nur mit 64-Bit-Dateien (gibt es ein 32-Bit-eBPF?). Wenn sich herausstellt, dass die Klasse ELFCLASS32
, ELFCLASS32
wir einfach mit einem Fehler: ELFCLASS32
die Strukturen "schweben" und die Überprüfung der Integrität wird nicht schaden, dies zu tun. Das letzte für uns interessante Byte in dieser Struktur gibt die Endianness der Datei an - wir arbeiten nur mit der nativen Bytereihenfolge für unseren Prozessor.
Nur für den Fall, ich werde klarstellen: Wenn Sie mit dem ELF-Format in C arbeiten, sollten Sie nicht jedes int durch den geschickt berechneten Offset elf.h
- elf.h
enthält die erforderlichen Strukturen und sogar e_ident
in e_ident
: EI_MAG0
, EI_MAG1
, EI_MAG2
, EI_MAG3
, EI_CLASS
, EI_DATA
... Sie müssen nur bringen Zeiger auf die Daten, die aus der Datei gelesen oder in den Speicher abgebildet wurden, auf den Zeiger auf die Struktur und gelesen.
Zusätzlich zu e_ident
enthält e_ident
Header weitere Felder, von denen einige nur überprüft werden und andere für die weitere Analyse verwendet werden, jedoch später. Wir überprüfen nämlich, ob e_machine == EM_BPF
( e_machine == EM_BPF
"unter der Architektur des eBPF-Prozessors"), e_type == ET_REL
, e_shoff != 0
. Die letzte Prüfung hat folgende Bedeutung: Eine Datei kann Informationen zum Verknüpfen (Abschnittstabelle und Abschnitte), zum Starten (Programmtabelle und Segmente) oder beides enthalten. Bei den letzten beiden Überprüfungen überprüfen wir, ob die benötigten Informationen (wie zum Verknüpfen) in der Datei enthalten sind. Überprüfen Sie auch, ob die Version des Formats EV_CURRENT
.
Nehmen Sie sofort eine Reservierung vor, ich werde die Gültigkeit der Datei nicht überprüfen, vorausgesetzt, wenn wir sie in unseren Prozess laden, vertrauen wir ihr. Im Code des Kernels oder anderer Programme, die mit nicht vertrauenswürdigen Dateien arbeiten, ist dies natürlich auf keinen Fall möglich .
Abschnittstabelle
Wie gesagt, wir interessieren uns für die Verknüpfungsansicht der Datei, dh der Abschnittstabelle und der Abschnitte selbst. Informationen dazu, wo Sie nach der Abschnittstabelle suchen müssen, finden Sie in der Kopfzeile. Dort wird auch die Größe sowie die Größe eines Elements angegeben - es kann größer als sizeof(Elf64_Shdr)
(da es die Versionsnummer des Formats beeinflusst, weiß ich ehrlich gesagt nicht). Einige Hauptabschnittsnummern sind reserviert und in der Tabelle nicht vorhanden. Ihre Referenzierung hat eine besondere Bedeutung. Wir sind anscheinend nur an SHN_UNDEF
interessiert (Null ist ebenfalls reserviert - der fehlende Abschnitt; wie Sie wissen, ist der Titel in der Tabelle SHN_ABS
noch vorhanden) SHN_ABS
. Das Zeichen "im Abschnitt SHN_UNDEF
definiert" ist tatsächlich undefiniert, und in SHN_ABS
es tatsächlich einen absoluten Wert und wird nicht SHN_ABS
. SHN_ABS
scheint jedoch auch SHN_ABS
mich nicht SHN_ABS
sein.
Zeilentabelle
Hier stoßen wir zum ersten Mal auf Stringtabellen - Tabellen von Strings, die in einer Datei verwendet werden. Wenn const char *strtab
eine Tabelle mit Zeichenfolgen ist, lautet der Name sh_name
nur strtab + sh_name
. Ja, es ist nur eine Zeile, die mit einem bestimmten Index beginnt und bis zum Null-Byte reicht. Linien können sich schneiden (genauer gesagt, eine kann das Suffix der anderen sein). Abschnitte können Namen haben. Im ELF-Header zeigt das Feld e_shstrndx
auf einen Abschnitt der e_shstrndx
(der für Abschnittsnamen, falls mehrere vorhanden sind) und das Feld sh_name
im Abschnittskopf auf eine bestimmte Zeile.
Das erste (Null) und das letzte Byte der Zeilentabelle enthalten Nullzeichen. Letzteres ist verständlich, warum: Wertstunde, endet die letzte Zeile. Der Nullpunktversatz gibt jedoch einen fehlenden oder leeren Namen an - je nach Kontext.
Abschnitte laden
Der Header jedes Abschnitts enthält zwei Adressen: eine, sh_addr
ist die Ladeadresse (wo der Abschnitt im Speicher abgelegt wird), die andere, sh_offset
ist der Offset in der Datei, in der sich dieser Abschnitt befindet. Ich weiß nicht, wie beide sind, aber jeder dieser Werte kann einzeln 0 sein: In einem Fall bleibt der Abschnitt "auf der Festplatte", da es eine Art von Serviceinformationen gibt. In einem anderen .bss
wird der Abschnitt nicht von der Festplatte geladen. Sie müssen ihn lediglich auswählen und mit Nullen ( .bss
) .bss
. Ehrlich gesagt, obwohl ich die Download-Adresse nicht verarbeiten musste - wo sie hochgeladen wurde, wurde sie dort hochgeladen :) Allerdings haben wir ehrlich gesagt auch spezielle Programme.
Umzug
Und jetzt das Interessante: Wie Sie wissen, gehen Sicherheitsmaßnahmen nicht in die Matrix, ohne dass ein Bediener an der Basis verbleibt. Und da wir hier immer noch Fantasie haben, wird die Verbindung zum Betreiber telepathisch sein. Oh ja, ich habe angekündigt, dass fünf Minuten Hartnäckigkeit abgeschlossen sind. Im Allgemeinen werden wir den Verknüpfungsprozess kurz diskutieren.
Für mein Experiment benötige ich einen Code, der zu einem regulären So-Boot kompiliert wurde und mit regulärem libdl
geladen ist. Hier werde ich nicht einmal im Detail beschreiben - öffnen dlopen
einfach dlopen
, ziehen Sie die Zeichen über dlsym
und schließen Sie es mit dlclose
wenn das Programm dlclose
. Dies sind jedoch auch Implementierungsdetails, die nicht mit unserem ELF-Dateilader zusammenhängen. Es gibt einfach einen Kontext : die Fähigkeit, einen Zeiger nach Namen zu erhalten.
Im Allgemeinen ist der eBPF-Befehlssatz ein Triumph des ausgerichteten Maschinencodes: Ein Befehl benötigt immer 8 Bytes und hat eine Struktur
struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; };
Darüber hinaus dürfen viele Felder in jeder spezifischen Anweisung nicht verwendet werden - es geht uns nicht darum, Platz für einen "Maschinen" -Code zu sparen.
Tatsächlich kann der erste Befehl unmittelbar auf den zweiten folgen, der keine Opcodes enthält, sondern einfach das unmittelbare Feld von 32 auf 64 Bit erweitert. Hier ist ein Patch für einen solchen zusammengesetzten Befehl namens R_BPF_64_64
.
Um den Umzug durchzuführen, sehen wir uns noch einmal die Abschnittstabelle für sh_type == SHT_REL
. Das Feld sh_info
des Headers gibt an, welchen Abschnitt wir patchen, und sh_link
- aus welcher Tabelle eine Beschreibung der Zeichen sh_link
.
typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; } Elf64_Rel;
Tatsächlich gibt es zwei Arten von Umzugsabschnitten: REL
und RELA
- der zweite enthält explizit einen zusätzlichen Begriff, aber ich habe ihn noch nicht gesehen. Daher fügen wir der Tatsache, dass er nicht erfüllt ist, nur die Behauptung hinzu, dass er nicht erfüllt wird, und wir werden ihn verarbeiten. Als nächstes werde ich dem Wert, der in den Anweisungen geschrieben ist, die Adresse des Symbols hinzufügen. Und wo bekommt man es? Hier sind, wie wir bereits wissen, Optionen möglich:
- Das Symbol bezieht sich auf den Abschnitt
SHN_ABS
. Dann nimm einfach st_value
- Das Zeichen bezieht sich auf den Abschnitt `SHN_UNDEF. Ziehen Sie dann das äußere Symbol
- In anderen Fällen patchen Sie einfach den Link zu einem anderen Abschnitt derselben Datei. "
Wie Sie es selbst versuchen
Was soll ich zuerst lesen? Zusätzlich zu der bereits angegebenen Spezifikation ist es sinnvoll, diese Datei zu lesen, in der das iovisor-Team Informationen sammelt, die über eBPF aus dem Linux-Kernel extrahiert wurden.
Zweitens, wie sollte eigentlich jeder damit arbeiten? Zuerst müssen Sie die ELF-Datei von irgendwoher bekommen. Wie bei StackOverfow angegeben , wird uns das Team helfen.
clang -O2 -emit-llvm -c bpf.c -o - | llc -march=bpf -filetype=obj -o bpf.o
Zweitens müssen Sie irgendwie eine Referenzanalyse der Datei in Stücke bekommen. In einer normalen Situation würde uns der Befehl objdump
helfen:
$ objdump : objdump <> <()> <()>. : -a, --archive-headers Display archive header information -f, --file-headers Display the contents of the overall file header -p, --private-headers Display object format specific file header contents -P, --private=OPT,OPT... Display object format specific contents -h, --[section-]headers Display the contents of the section headers -x, --all-headers Display the contents of all headers -d, --disassemble Display assembler contents of executable sections -D, --disassemble-all Display assembler contents of all sections --disassemble=<sym> Display assembler contents from <sym> -S, --source Intermix source code with disassembly -s, --full-contents Display the full contents of all sections requested -g, --debugging Display debug information in object file -e, --debugging-tags Display debug information using ctags style -G, --stabs Display (in raw form) any STABS info in the file -W[lLiaprmfFsoRtUuTgAckK] or --dwarf[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames, =frames-interp,=str,=loc,=Ranges,=pubtypes, =gdb_index,=trace_info,=trace_abbrev,=trace_aranges, =addr,=cu_index,=links,=follow-links] Display DWARF info in the file -t, --syms Display the contents of the symbol table(s) -T, --dynamic-syms Display the contents of the dynamic symbol table -r, --reloc Display the relocation entries in the file -R, --dynamic-reloc Display the dynamic relocation entries in the file @<file> Read options from <file> -v, --version Display this program's version number -i, --info List object formats and architectures supported -H, --help Display this information
Aber in diesem Fall ist es machtlos:
$ objdump -d test-bpf.o test-bpf.o: elf64-little objdump: UNKNOWN!
Genauer gesagt werden Abschnitte angezeigt, aber das Zerlegen ist ein Problem. Hier erinnern wir uns, was wir mit LLVM gesammelt haben. LLVM verfügt über eigene erweiterte Analoga von Dienstprogrammen aus binutils mit Namen der Form llvm-< >
. Sie verstehen beispielsweise den LLVM-Bitcode. Und sie verstehen auch eBPF - es hängt sicher von den Kompilierungsoptionen ab, aber da es kompiliert wurde, sollte es wahrscheinlich immer analysiert werden. Aus praktischen Gründen empfehle ich daher, ein Skript zu erstellen:
vim test-bpf.c
Dann für eine solche Quelle:
#include <stdint.h> extern uint64_t z; uint64_t func(uint64_t x, uint64_t y) { return x + y + z; }
Es wird ein solches Ergebnis geben:
$ ./compile-bpf.sh test-bpf.o: file format ELF64-BPF Disassembly of section .text: 0000000000000000 func: 0: bf 20 00 00 00 00 00 00 r0 = r2 1: 0f 10 00 00 00 00 00 00 r0 += r1 2: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 0000000000000010: R_BPF_64_64 z 4: 79 11 00 00 00 00 00 00 r1 = *(u64 *)(r1 + 0) 5: 0f 10 00 00 00 00 00 00 r0 += r1 6: 95 00 00 00 00 00 00 00 exit SYMBOL TABLE: 0000000000000000 l df *ABS* 00000000 test-bpf.c 0000000000000000 ld .text 00000000 .text 0000000000000000 g F .text 00000038 func 0000000000000000 *UND* 00000000 z
Code
Teil 1. QInst: Es ist besser, einen Tag zu verlieren und dann in fünf Minuten zu fliegen (Schreibgeräte sind trivial)