Konvertieren ein C-Programm in Assembly
In diesem Tutorial wird das Konvertieren eines C-Sprachprogramms in Assemblersprachencode erläutert.
Wir werden kurz die Grundlagen der Assembler- und C-Sprachen erörtern. Später werden wir die Konvertierung des C-Programms in Assembler-Code und die Deassemblierung eines Assembler-Codes sehen.
Die Assemblersprache
Assembly ist eine auf niedriger Ebene interpretierte Sprache. Im Allgemeinen wird eine in Assemblersprache geschriebene Anweisung in eine einzelne Anweisung auf Maschinenebene übersetzt.
Es ist jedoch viel besser lesbar als Maschinensprache, da es Mnemotechniken verwendet. Die Mnemonics sind englisch-ähnliche Anweisungen oder Operationscodes.
Zum Beispiel wird die Mnemonik ADD
verwendet, um zwei Zahlen zu addieren. Ebenso wird MOV
verwendet, um Datenbewegungen durchzuführen.
Ebenso vergleicht CMP
zwei Ausdrücke, und JMP
springt die Ausführungssteuerung zu einer bestimmten Bezeichnung oder Positionsmarkierung.
Die Assemblersprache ist der Maschine (Hardware) sehr ähnlich; Daher sind in Assemblersprache geschriebene Anweisungen sehr schnell. Allerdings benötigt der Programmierer wesentlich mehr Hardware-Kenntnisse als ein Entwickler einer Hochsprache.
Die Assemblersprache wird normalerweise verwendet, um effiziente Systemprogramme wie Gerätetreiber, Viren-/Antivirenprogramme, eingebettete Systemsoftware und TSR (terminierte und residente Programme) zu schreiben.
Ein Assembler muss ein Assemblersprachenprogramm in ein Maschinensprachenprogramm assemblieren, das auf der Maschine ausführbar ist.
Die C-Sprache
C ist eine höhere maschinenunabhängige Programmiersprache. Normalerweise erfordern C-Programme keine Hardwarekenntnisse (es sind nur geringe Kenntnisse erforderlich).
C verfügt über High-Level-Anweisungen und erfordert ein Compilerprogramm, das jede Anweisung der C-Sprache in eine oder mehrere Anweisungen der Assemblersprache übersetzt. Zum Beispiel wird eine einfache Anweisung in C-Sprache, c = a + b
, in die folgenden Anweisungen in Assemblersprache übersetzt:
mov edx, DWORD PTR - 12 [rbp] mov eax, DWORD PTR - 8 [rbp] add eax,
edx mov DWORD PTR - 4 [rbp], eax
Hier wird in der ersten und zweiten Anweisung der Wert von Variablen aus dem Speicher in Register verschoben. Der add
-Befehl addiert zwei Registerwerte.
In der vierten Anweisung wird der Wert aus dem Register in eine Variable im Speicher verschoben.
Außerdem muss der Compiler viel Arbeit leisten, aber das Leben des Programmierers ist einfach, in der Sprache C zu arbeiten. Die C-Sprache hat ein breites Anwendungsspektrum, von High-Level-Geschäftsanwendungen bis hin zu Low-Level-Hilfsprogrammen.
Konvertieren ein C-Programm in die Assemblersprache
Typischerweise verwenden Menschen die ausgeklügelte integrierte Umgebung zum Schreiben, Bearbeiten, Kompilieren, Ausführen, Modifizieren und Debuggen von C-Sprachprogrammen oder den Befehl gcc
, um das C-Sprachprogramm in ausführbare Programme umzuwandeln.
Diese Tools halten die Benutzer von den Schritten in Kenntnis, die erforderlich sind, um einen Quellcode, der in einer Hochsprache wie C geschrieben ist, in maschinenausführbaren Code umzuwandeln. Typischerweise werden folgende Schritte dazwischen durchgeführt:
- Vorverarbeitung – Ein Vorverarbeitungsprogramm erledigt drei Aufgaben. Die erste Aufgabe ist das Einfügen von Header-Dateien, die zweite Aufgabe das Ersetzen von Makros und die dritte Aufgabe das Entfernen von Kommentaren aus dem Quellprogramm
- Compiler – Im zweiten Schritt übersetzt der Compiler Hochsprachenprogramme in Assemblersprachenprogramme
- Assembler – Im dritten Schritt nimmt das Assembler-Programm ein Programm in Assemblersprache (übersetzt durch den Compiler) und setzt es in eine maschinenausführbare Form namens Objektcode zusammen
- Linker – Im vierten Schritt hängt ein Linker-Programm kompilierte Bibliotheksdateien mit dem Objektcode an, um dieses Programm unabhängig auszuführen
Befehle zum Konvertieren von C-Code in ein Assembly-Äquivalent
Normalerweise geben Benutzer der Befehlszeile gcc Programmname.c
ein, wodurch eine ausführbare Datei generiert wird (falls keine Fehler auftreten). Wenn der Zieldateiname nicht angegeben ist, ist er entweder mit a.out
in der UNIX-Betriebssystemfamilie oder program_name.exe
im Windows-Betriebssystem verfügbar.
Nichtsdestotrotz verfügt der Befehl gcc
über eine riesige Liste von Parametern, um bestimmte Aufgaben auszuführen. In diesem Tutorial werden nur die Flags -s
und -C
behandelt.
Das Flag -S
erzeugt aus dem C-Quellcode ein Programm in Assemblersprache. Lassen Sie uns dieses Flag anhand des folgenden Beispiels verstehen, in dem wir test.c
als Quelldatei haben:
// test.c
int main() {
int a = 2, b = 3, c;
c = a + b;
return 0;
}
Der folgende Befehl generiert den Zielcode der Assemblersprache mit der Erweiterung .S
:
$ gcc -S test.c
$ ls
test.c test.s
Der Befehl hat keinen Maschinensprachencode erstellt; nur der Assemblersprachencode wird generiert. Lassen Sie uns den Inhalt dieses generierten Assembly-Codes mit dem Befehl cat
in Bash anzeigen:
$ cat test.s
.file "Test.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $2, -12(%rbp)
movl $3, -8(%rbp)
movl -12(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
movl %eax, -4(%rbp)
...
Der generierte Assemblycode ist vielen Programmierern, die Erfahrung mit dem Schreiben von Assemblycodes für die Intel x86-Architektur haben, möglicherweise nicht vertraut.
Wenn wir den Ziel-Assembly-Code für Intel x86-Architekturen wollen, erledigt der folgende Befehl dies für uns:
$ gcc -S -masm=intel Test.c
Auch hier wird die Ausgabe in der Datei Test.s
generiert, die mit dem Befehl cat
im Bash-Terminal angezeigt werden kann. In Windows können wir es in einem Editor wie Notepad oder einem besseren Editor öffnen.
Sehen wir uns auf jeden Fall den Inhalt des Assembly-Codes an, der durch den obigen Befehl generiert wurde:
cat Test.s
.file "Test.c"
.intel_syntax noprefix
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
mov DWORD PTR -12[rbp], 2
mov DWORD PTR -8[rbp], 3
mov edx, DWORD PTR -12[rbp]
mov eax, DWORD PTR -8[rbp]
add eax, edx
mov DWORD PTR -4[rbp], eax
...
Die Ausgabe ist etwas anders; die Befehle mov
und add
sind sehr übersichtlich.
Deassemblieren eines Objektcodes
Neben der Konvertierung eines C-Sprachprogramms in die Assemblersprache möchte man vielleicht den Binärcode (Maschinencode) zerlegen, um den äquivalenten Assemblersprachencode zu sehen. Dazu können wir das Dienstprogramm objdump
in Linux verwenden.
Beispiel:
Angenommen, wir führen den Befehl gcc -c Test.c
aus, um die Datei Test.c
in einem Bash-Terminal zu kompilieren. Es erstellt eine Objektdatei (Maschinensprachcode) mit dem Namen Test.o
.
Wenn wir nun sehen möchten, wie dieser Objektcode wieder in den entsprechenden Assembly-Code konvertiert/deassembliert wird, können wir dies mit dem folgenden Bash-Befehl tun:
$ objdump -d Test.o
Test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5 48 89 e5 mov %rsp,%rbp
8: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%rbp)
f: c7 45 f8 03 00 00 00 movl $0x3,-0x8(%rbp)
16: 8b 55 f4 mov -0xc(%rbp),%edx
19: 8b 45 f8 mov -0x8(%rbp),%eax
1c: 01 d0 add %edx,%eax
1e: 89 45 fc mov %eax,-0x4(%rbp)
21: b8 00 00 00 00 mov $0x0,%eax
26: 5d pop %rbp
In dieser Ausgabe ist der Code auf der linken Seite der Binärcode in Hexadezimal. Auf der rechten Seite ist der Assemblersprachencode in lesbarer Form sichtbar.