Nur neugierig. Dies ist offensichtlich keine sehr gute Lösung für die eigentliche Programmierung, aber ich wollte eine ausführbare Datei in Bless (einem Hex-Editor) machen.
Meine Architektur ist x86. Was ist ein sehr einfaches Programm, das ich machen kann? Eine Hallo Welt? Eine Endlosschleife? Ähnlich wie diese Frage, aber unter Linux.
Wie in meinem Kommentar erwähnt, schreiben Sie im Grunde Ihren eigenen Elf-Header für die ausführbare Datei und eliminieren die nicht benötigten Abschnitte. Es gibt noch einige erforderliche Abschnitte. Die Dokumentation in Muppetlabs-TinyPrograms macht einen guten Job, um diesen Prozess zu erklären. Zum Spaß, hier sind ein paar Beispiele:
Das Gegenstück zu / bin / true (45 Byte):
%Vor%Dein Klassiker "Hallo Welt!" (160 Bytes):
%Vor%Vergessen Sie nicht, sie ausführbar zu machen ...
Version dieser Antwort mit einem netten Inhaltsverzeichnis und mehr Inhalt: Ссылка (hier wird das 30k Char Limit erreicht)
ELF wird vom LSB angegeben:
Das LSB verlinkt im Wesentlichen auf andere Standards mit kleineren Erweiterungen, insbesondere:
generisch (beide von SCO):
architekturspezifisch:
Eine praktische Zusammenfassung finden Sie unter:
%Vor% Seine Struktur kann durch Programme wie readelf
und objdump
lesbar untersucht werden.
Lassen Sie uns ein minimal ausführbares x86-64-Beispiel für Linux ausführen:
%Vor%Kompiliert mit:
%Vor%Versionen:
ld
) Wir benutzen kein C-Programm, da dies die Analyse verkomplizieren würde, nämlich Level 2: -)
Ausgabe unter: Ссылка
Eine ELF-Datei enthält die folgenden Teile:
ELF-Header. Zeigt auf die Position der Abschnitts-Header-Tabelle und der Programm-Header-Tabelle.
Abschnitt Header-Tabelle (optional für ausführbare Datei). Jeder hat e_shnum
-Abschnitt Header, jeder auf die Position eines Abschnitts zeigen.
N Abschnitte mit N <= e_shnum
(optional für ausführbare Dateien)
Programm-Header-Tabelle (nur für ausführbare Dateien). Jeder hat e_phnum
-Programmköpfe, von denen jeder auf die Position eines Segments zeigt.
N Segmente, mit N <= e_phnum
(optional für ausführbare Dateien)
Die Reihenfolge dieser Teile ist nicht behoben: die einzige feste Sache ist der ELF-Header, der das erste Ding in der Datei sein muss: Generische Dokumente sagen:
Der einfachste Weg, den Header zu beobachten, ist:
%Vor%Ausgabe unter: Ссылка
Bytes in der Objektdatei:
%Vor%Ausführbare Datei:
%Vor%Struktur repräsentiert:
%Vor%Manuelle Aufteilung:
0 0: EI_MAG
= 7f 45 4c 46
= 0x7f 'E', 'L', 'F'
: ELF magische Zahl
0 4: EI_CLASS
= 02
= ELFCLASS64
: 64-Bit-Elf
0 5: EI_DATA
= 01
= ELFDATA2LSB
: große Endian-Daten
0 6: EI_VERSION
= 01
: Formatversion
0 7: EI_OSABI
(nur in 2003 Update) = 00
= ELFOSABI_NONE
: keine Erweiterungen.
0 8: EI_PAD
= 8x 00
: reservierte Bytes. Muss auf 0 gesetzt sein.
1 0: e_type
= 01 00
= 1 (großes Endian) = ET_REl
: verschiebbares Format
Auf der ausführbaren Datei ist 02 00
für ET_EXEC
.
1 2: e_machine
= 3e 00
= 62
= EM_X86_64
: AMD64-Architektur
1 4: e_version
= 01 00 00 00
: muss 1 sein
1 8: e_entry
= 8x 00
: Ausführungsadresse Einsprungpunkt, oder 0, falls nicht anwendbar wie für die Objektdatei, da es keinen Einsprungpunkt gibt.
In der ausführbaren Datei ist b0 00 40 00 00 00 00 00
. TODO: Woran können wir das noch richten? Der Kernel scheint die IP direkt auf diesen Wert zu setzen, er ist nicht fest codiert.
2 0: e_phoff
= 8x 00
: Offset der Kopfzeile der Tabelle, 0 falls nicht vorhanden.
40 00 00 00
auf der ausführbaren Datei, d. h. es beginnt unmittelbar nach dem ELF-Header.
2 8: e_shoff
= 40
7x 00
= 0x40
: Abschnitt Header-Tabellen-Datei Offset, 0 falls nicht vorhanden.
3 0: e_flags
= 00 00 00 00
TODO. Arch-spezifisch.
3 4: e_ehsize
= 40 00
: Größe dieses Elf-Headers. TODO warum dieses Feld? Wie kann es variieren?
3 6: e_phentsize
= 00 00
: Größe jedes Programmheaders, 0 falls nicht vorhanden.
38 00
auf ausführbare Datei: es ist 56 Bytes lang
3 8: e_phnum
= 00 00
: Anzahl der Programmkopfeinträge, 0 falls nicht vorhanden.
02 00
auf ausführbare Datei: Es gibt 2 Einträge.
3 A: e_shentsize
und e_shnum
= 40 00 07 00
: Headergröße und Anzahl der Einträge
3 E: e_shstrndx
( Section Header STRing iNDeX
) = 03 00
: Index des Abschnitts .shstrtab
.
Array von Elf64_Shdr
Strukturen.
Jeder Eintrag enthält Metadaten zu einem bestimmten Abschnitt.
e_shoff
des ELF-Headers gibt die Startposition, hier 0x40.
e_shentsize
und e_shnum
aus dem ELF-Header sagen, dass wir 7 Einträge haben, jeder 0x40
Bytes lang.
Also nimmt die Tabelle Bytes von 0x40 bis 0x40 + 7 + 0x40 - 1
= 0x1FF.
Einige Abschnittsnamen sind bestimmten Abschnittstypen vorbehalten: Ссылка z .text
erfordert einen SHT_PROGBITS
type und SHF_ALLOC
+ SHF_EXECINSTR
readelf -S hello_world.o
:
struct
, dargestellt durch jeden Eintrag:
Enthalten in den Bytes 0x40 bis 0x7F.
Der erste Abschnitt ist immer magisch: Ссылка sagt:
Wenn die Anzahl der Abschnitte größer oder gleich SHN_LORESERVE (0xff00) ist, hat e_shnum den Wert SHN_UNDEF (0) und die tatsächliche Anzahl der Abschnitts-Header-Tabelleneinträge ist im Feld sh_size des Abschnitts-Headers mit dem Index 0 enthalten ( Andernfalls enthält das Element sh_size des ursprünglichen Eintrags 0).
Es gibt auch andere magische Abschnitte in Figure 4-7: Special Section Indexes
.
In Index 0 ist SHT_NULL
obligatorisch. Gibt es andere Verwendungen dafür? What is die Verwendung des Abschnitts SHT_NULL in ELF? ?
.data
ist Abschnitt 1:
80 0: sh_name
= 01 00 00 00
: Index 1 in der .shstrtab
Zeichenkettentabelle
Hier bedeutet 1
, dass der Name dieses Abschnitts mit dem ersten Zeichen dieses Abschnitts beginnt und mit dem ersten NUL-Zeichen endet, das die Zeichenfolge .data
bildet.
.data
ist einer der Abschnittsnamen, der eine vordefinierte Bedeutung Ссылка hat
Diese Abschnitte enthalten initialisierte Daten, die zum Speicherabbild des Programms beitragen.
80 4: sh_type
= 01 00 00 00
: SHT_PROGBITS
: Der Inhalt der Sektion wird nicht von ELF spezifiziert, sondern nur wie das Programm sie interpretiert. Normal seit einem .data
-Abschnitt.
80 8: sh_flags
= 03
7x 00
: SHF_ALLOC
und SHF_EXECINSTR
: Ссылка , wie von einer .data
Sektion
90 0: sh_addr
= 8x 00
: In welcher virtuellen Adresse wird der Abschnitt während der Ausführung platziert, 0
, falls nicht platziert
90 8: sh_offset
= 00 02 00 00 00 00 00 00
= 0x200
: Anzahl der Bytes vom Start des Programms bis zum ersten Byte in diesem Abschnitt
a0 0: sh_size
= 0d 00 00 00 00 00 00 00
Wenn wir 0xD Bytes beginnend mit sh_offset
200 nehmen, sehen wir:
AHA! Also ist unsere "Hello world!"
-Zeichenkette in der Datensektion, wie wir ihr auf der NASM gesagt haben.
Sobald wir hd
abgeschlossen haben, sehen wir uns das an:
welche Ausgaben:
%Vor%NASM legt für diesen Abschnitt vernünftige Eigenschaften fest, weil%% mag% en_de behandelt wird: Ссылка
Beachten Sie auch, dass dies eine schlechte Abschnittswahl war: Ein guter C-Compiler würde stattdessen die Zeichenfolge in .data
setzen, da sie schreibgeschützt ist und weitere Betriebssystemoptimierungen ermöglichen würde.
a0 8: .rodata
und sh_link
= 8x 0: gilt nicht für diesen Abschnittstyp. Ссылка
b0 0: sh_info
= sh_addralign
= TODO: Warum ist diese Ausrichtung notwendig? Ist es nur für 04
oder auch für Symbole in sh_addr
?
b0 8: sh_addr
= sh_entsize
= Der Abschnitt enthält keine Tabelle. Wenn! = 0, bedeutet dies, dass der Abschnitt eine Tabelle mit festen Größeneinträgen enthält. In dieser Datei sehen wir von der Ausgabe 00
, dass dies für die Abschnitte readelf
und .symtab
der Fall ist.
Nachdem wir nun einen Abschnitt manuell erstellt haben, lassen Sie uns graduieren und verwenden Sie .rela.text
der anderen Abschnitte.
readelf -S
ist ausführbar, aber nicht beschreibbar: wenn wir versuchen, Linux-Defaults darauf zu schreiben. Mal sehen, ob wir wirklich etwas Code haben:
gibt:
%Vor% Wenn wir .text
auf b8 01 00 00
grepsen, sehen wir, dass dies nur bei hd
auftritt, was der Abschnitt sagt. Und die Größe ist 27, was auch passt. Also müssen wir über den richtigen Abschnitt sprechen.
Das sieht nach dem richtigen Code aus: a 00000210
gefolgt von write
.
Der interessanteste Teil ist die Zeile exit
, die folgendes bewirkt:
, um die Adresse der Zeichenfolge an den Systemaufruf zu übergeben. Momentan ist a
nur ein Platzhalter. Nachdem die Verknüpfung erfolgt ist, wird sie wie folgt geändert:
Diese Änderung ist aufgrund der Daten des Abschnitts 0x0
möglich.
Abschnitte mit .rela.text
heißen Zeichenkettentabellen .
Sie enthalten ein null-getrenntes Array von Strings.
Solche Abschnitte werden von anderen Abschnitten verwendet, wenn String-Namen verwendet werden sollen. Der Verwendungsabschnitt sagt:
Zum Beispiel könnten wir eine String-Tabelle haben, die folgendes enthält: TODO: Muss es mit sh_type == SHT_STRTAB
beginnen?
Und wenn ein anderer Abschnitt den String
verwenden möchte, muss er auf den Index d e f
5
dieses Abschnitts verweisen (Buchstabe d
).
Bemerkenswerte String-Tabellenabschnitte:
.shstrtab
.strtab
Abschnittstyp: sh_type == SHT_STRTAB
.
Allgemeiner Name: Abschnittsheader-Zeichenfolgentabelle .
Der Abschnittsname .shstrtab
ist reserviert. Der Standard sagt:
Dieser Abschnitt enthält Bereichsnamen.
Auf diesen Abschnitt zeigt das Feld e_shstrnd
des ELF-Headers selbst.
String-Indizes dieses Abschnitts werden durch das sh_name
-Feld der Abschnittsheader angegeben, die Zeichenfolgen bezeichnen.
In diesem Abschnitt ist SHF_ALLOC
nicht markiert, daher wird es im ausführenden Programm nicht angezeigt.
Gibt:
%Vor%Die Daten in diesem Abschnitt haben ein festes Format: Ссылка
Wenn wir uns die Namen anderer Abschnitte ansehen, sehen wir, dass sie alle Nummern enthalten, z. Der Abschnitt .text
ist die Nummer 7
.
Dann endet jede Kette, wenn das erste NUL-Zeichen gefunden wird, z. Zeichen 12
ist
direkt nach .text
sh_type == SHT_SYMTAB
sh_link
.
Abschnittstyp: 5
.
Allgemeiner Name: Symboltabelle .
Zuerst notieren wir:
sh_info
= 6
SHT_SYMTAB
= .strtab
Für .rela.text
Abschnitte bedeuten diese Zahlen:
ELF64_R_TYPE == STT_FILE
ELF64_R_TYPE
Ein gutes High-Level-Werkzeug zum Zerlegen dieses Abschnitts ist:
%Vor%was ergibt:
%Vor%Dies ist jedoch eine Ansicht auf hoher Ebene, die einige Arten von Symbolen auslässt und in denen die Symboltypen enthalten sind. Eine detailliertere Demontage kann erhalten werden mit:
%Vor%was ergibt:
%Vor% Das Binärformat der Tabelle ist in Ссылка Die Daten sind: Was gibt: Die Einträge sind vom Typ: Wie in der Abschnittstabelle ist der erste Eintrag magisch und auf feste Werte ohne Bedeutung gesetzt. Eintrag 1 hat Byte-Analyse: 10 8: Dieser Teil der Informationsdatei kann vom Linker verwendet werden, um zu entscheiden, welche Segmentabschnitte gehen. 10 12: Bits 0-3 = Bits 4-7 = 10 13: 20 0: 20 8: Jetzt von der Es gibt zwei solche Einträge, von denen einer auf TODO Was ist ihr Zweck? Dann kommen die wichtigsten Symbole: in NASM. Dies ist notwendig, da es als Einstiegspunkt angesehen werden muss. Im Gegensatz zu C sind NASM-Labels standardmäßig lokal. Dies bedeutet, dass die Verlagerung diesen Wert nicht beeinflusst: Es ist eine Konstante. Dies ist eine kleine Optimierung, die unser Assembler für uns erledigt und die ELF-Unterstützung bietet. Wenn wir die Adresse von Standardmäßig legt NASM auch eine Dies wird nur zum Debuggen verwendet. Ohne die Symbole sind wir völlig blind und müssen alles zurückentwickeln. Sie können es mit Enthält Strings für die Symboltabelle. Dieser Abschnitt enthält Darauf wird von Gibt: Dies bedeutet, dass es eine Beschränkung der ELF-Ebene ist, dass globale Variablen keine NUL-Zeichen enthalten dürfen. Abschnittstyp: Allgemeiner Name: Umzugsabschnitt . Im Grunde übersetzt es den Objekttext, der die Platzhalteradresse 0x0 enthält: zum eigentlichen ausführbaren Code, der die letzte 0x6000d8 enthält: Darauf wurde von Der Abschnitt existiert nicht in der ausführbaren Datei. Die tatsächlichen Bytes sind: Das dargestellte Also: 370 0: 370 8: Der AMD64 ABI sagt, dass der Typ Diese Adresse wird zu dem Bereich hinzugefügt, in dem der Umzug stattfindet. Diese Umlagerungsoperation wirkt auf insgesamt 8 Bytes. 380 0: Also in unserem Beispiel schließen wir, dass die neue Adresse sein wird: Erscheint nur in der ausführbaren Datei. Enthält Informationen darüber, wie die ausführbare Datei in den virtuellen Prozessspeicher eingefügt werden sollte. Die ausführbare Datei wird aus Objektdateien vom Linker generiert. Die Hauptaufgaben, die der Linker ausführt, sind: Bestimmen Sie, welche Abschnitte der Objektdateien in welche Segmente der ausführbaren Datei gelangen. In Binutils kommt es darauf an, ein Linkerscript zu analysieren und eine Reihe von Standardeinstellungen zu behandeln. Sie können das Linkerskript mit Verschieben Sie Textabschnitte. Dies hängt davon ab, wie die einzelnen Abschnitte gespeichert werden. Auf dem ELF-Header sagten uns und: Struktur repräsentiert Ссылка : Aufschlüsselung des ersten: Die zweite ist analog. Dann das: -Abschnitt von st_info
. st_name
wird innerhalb von 01000000
fortgesetzt.
.strtab
=
= Zeichen 1 in hello_world.asm
st_info
, was bis zum nächsten 04
ELF64_R_TYPE
4
= STT_FILE
st_name
= Typ = ELF64_ST_BIND
= 0
: Hauptzweck dieses Eintrags ist es, mit STB_LOCAL
den Namen der Datei anzugeben, die diese Objektdatei erzeugt hat. STT_FILE
= Bindung = st_shndx
= f1ff
. Erforderlicher Wert für SHN_ABS
. STT_FILE
= Kopfzeile der Symboltabelle Abschnitt Index = st_value
= 00
. Erforderlich für STT_FILE
. st_size
= 8x 00
: erforderlich für den Wert für readelf
.data
= 8x .text
: keine zugewiesene Größe 1
interpretieren wir die anderen schnell. 2
und der andere auf hello_world
zeigt (Abschnittsindizes .data
und _start
). GLOBAL
string befindet sich im Abschnitt hello_world_len
(Index 1). Sein Wert ist 0: Er zeigt auf das erste Byte dieses Abschnitts. st_shndx == SHN_ABS == 0xF1FF
ist mit 0xF1FF
visibility markiert, seit wir geschrieben haben: st_value == 0xD == 13
zeigt auf das spezielle Hello World!
. hello_world_len
wurde gewählt, um nicht mit anderen Abschnitten in Konflikt zu geraten. SHN_ABS
Dies ist der Wert, den wir dort in der Assembly gespeichert haben: die Länge der Zeichenkette .symtab
. objcopy
irgendwo benutzt hätten, wäre der Assembler nicht in der Lage gewesen, sie als sh_type == SHT_STRTAB
zu markieren, und der Linker hätte später zusätzliche Verschiebungsarbeiten daran. sh_link == 5
auf die ausführbare Datei. .symtab
entfernen, und die ausführbare Datei wird weiterhin ausgeführt. Solche ausführbaren Dateien heißen abgestreifte ausführbare Dateien . .strtab
sh_type == SHT_RELA
. .rela.text
des Abschnitts sh_info
verwiesen. .rela.text
6
. .symtab
enthält Verschiebungsdaten, die angeben, wie die Adresse geändert werden soll, wenn die endgültige ausführbare Datei verknüpft wird. Dies zeigt auf Bytes des Textbereichs, der geändert werden muss, wenn die Verknüpfung auf die richtigen Speicherorte verweist. readelf -r hello_world.o
= struct
des Abschnitts r_offset
hingewiesen. .text
gibt: r_info
ist:
ELF64_R_TYPE
= 0xC: Adresse in die ELF64_R_SYM
, deren Adresse bei dieser Verschiebung geändert wird .data
= 0x200000001. Enthält 2 Felder:
1
= 0x1: Bedeutung hängt von der genauen Architektur ab. R_X86_64_64
= 0x2: Index des Abschnitts, auf den die Adresse zeigt, also S + A
, das auf Index 2 steht. S
0
heißt und dass er die Operation 00 00 00 00 00 00 00 00
wo:
movabs
: der Wert des Symbols in der Objektdatei, hier A
x0,%rsir_added
, da wir auf%%%%%%% verweisen%
r_addend
: Der Summand, vorhanden im Feld S + A
.data + 0
= 0 ld --verbose
= ld -T
, und damit die erste Sache in der Datenabteilung. Programm-Header-Tabelle
readelf -l hello_world.out
verwenden und ein benutzerdefiniertes mit e_phoff
festlegen. e_phnum
gibt: e_phentsize
, 0x40
und 0x38
, dass es 2 Programmheader gibt, die bei p_type
beginnen und jeweils 01 00 00 00
bytes lang sind, also:
PT_LOAD
= p_flags
= 05 00 00 00
: TODO. Ich denke, es bedeutet, dass es tatsächlich in den Speicher geladen wird. Andere Typen müssen nicht unbedingt sein. p_offset
= 00
= Ausführen und Lesen von Berechtigungen, keine Schreib-TODO gcc -Wl,-Ttext-segment=0x400030 hello_world.c
= 8x p_vaddr
TODO: Was ist das? Sieht wie Offsets vom Anfang der Segmente aus aus. Aber das würde bedeuten, dass einige Segmente miteinander verflochten sind? Es ist möglich, ein bisschen damit zu spielen: 00 00 40 00 00 00 00 00
p_paddr
= 00 00 40 00 00 00 00 00
: Anfangsadresse des virtuellen Speichers, um dieses Segment nach p_vaddrr
= p_filesz
: erste physische Adresse, die in den Speicher geladen werden soll. Nur für Systeme, in denen das Programm seine physikalische Adresse festlegen kann. Ansonsten, wie in System V wie Systeme, kann alles sein. NASM scheint nur d7 00 00 00 00 00 00 00
zu kopieren
p_memsz
= p_memsz
: TODO vs d7 00 00 00 00 00 00 00
p_align
= 00 00 20 00 00 00 00 00
: TODO readelf
= .text
: 0 oder 1 bedeuten keine Ausrichtung erforderlich TODO was bedeutet das? sonst redundant mit anderen Feldern .data
sagt uns Folgendes: