Konvertieren ein C-Programm in Assembly

Abdul Mateen 12 Oktober 2023
  1. Die Assemblersprache
  2. Die C-Sprache
  3. Konvertieren ein C-Programm in die Assemblersprache
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:

  1. 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
  2. Compiler – Im zweiten Schritt übersetzt der Compiler Hochsprachenprogramme in Assemblersprachenprogramme
  3. 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
  4. 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.