C / C ++ - Funktionsdefinitionen ohne Assembly

73

Ich habe immer gedacht, dass Funktionen wie printf()im letzten Schritt mithilfe der Inline-Assembly definiert werden. So tief im Darm von stdio.h steckt ein asm-Code, der der CPU tatsächlich sagt, was zu tun ist. Ich erinnere mich zum Beispiel, dass es in dos implementiert wurde, indem zuerst movder Anfang des Strings an eine Speicherstelle oder ein Register gesetzt und dann ein intTerupt aufgerufen wurde.

Da die x64-Version von Visual Studio den Inline-Assembler überhaupt nicht unterstützt, habe ich mich gefragt, wie es in C / C ++ überhaupt keine vom Assembler definierten Funktionen geben kann. Wie wird eine Bibliotheksfunktion wie printf()in C / C ++ implementiert, ohne Assembler-Code zu verwenden? Was führt eigentlich den richtigen Software-Interrupt aus? Vielen Dank.

Jack
quelle
21
Schwer zu wissen, wo ich anfangen soll, da alles, was Sie zu wissen glauben, falsch ist. Sie müssen einige Wikipedia-Artikel zum Kompilieren und Verknüpfen lesen. Vielleicht möchten Sie auch einen Blick auf die Quelle von stdio.h werfen (es ist nur Text), in der Sie keinen Assembler-Code für eine C ++ - Implementierung finden.
4
Visual Studio x64 unterstützt keinen Inline- Assembler. Das bedeutet nicht, dass Sie keinen Assembler-Code haben können. Sie können immer noch Assembler haben, nur nicht inline. Die Antwort von Tronic unten ist richtig. Sie sollten sich auch mit den Eigenschaften des Compilers befassen.
1
@Jack Ich wollte Ihre Sprachkenntnisse nicht verunglimpfen (tatsächlich ist Ihre Frage in Bezug auf den englischen Sprachgebrauch sehr gut ausgedrückt), sondern nur darauf hinweisen, dass Ihre Vorstellung, dass der Code irgendwie in stdio.h enthalten war, falsch war. Ich sehe jetzt, das war vielleicht nicht das, was du meintest.
1
@ Jack Es ist keine Ja / Nein-Frage. Einige Systeme haben kein Betriebssystem. Die Antwort hängt von Ihrem spezifischen System ab. / Bei Windows, sowohl 32- als auch 64-Bit, ruft der Code auf Benutzerebene die Systemverzeichnisse auf (dies sind nur DLL-Bibliotheken, die Sie schreiben können). Irgendwann, tief in der Hierarchie der Aufrufe, wird Code ausgeführt, der nicht in C ausgedrückt werden konnte. Wie dieser Code generiert wurde, ist nicht allzu interessant, aber normalerweise wurde er in Assembler geschrieben. Ob das inline oder gerade ASM ist nicht wichtig
7
Alles was du weißt ist nicht falsch. Aber im Zeitalter von Open Source ist Neugier und Zeit alles, was Sie brauchen, um eine solche Frage für sich selbst zu beantworten. Um Ihnen zu zeigen, dass dies möglich ist, beginnt meine Antwort mit dem Ausgraben des Prototyps für printf und überspringt keine Schritte, bis Sie syscall erreichen ... mit Links zu den tatsächlichen Quelldateien in ihren Repositorys. Das Schreiben hat lange gedauert, ich hoffe es hilft. :)
HostileFork sagt, vertraue SE

Antworten:

18

Zunächst muss man das Konzept der Ringe verstehen.
Ein Kernel läuft in Ring 0, was bedeutet, dass er vollen Zugriff auf Speicher und Opcodes hat.
Ein Programm wird normalerweise in Ring 3 ausgeführt. Es hat einen eingeschränkten Zugriff auf den Speicher und kann nicht alle Opcodes verwenden.

Wenn eine Software mehr Berechtigungen benötigt (zum Öffnen einer Datei, Schreiben in eine Datei, Zuweisen von Speicher usw.), muss sie den Kernel fragen.
Dies kann auf viele Arten erfolgen. Software-Interrupts, SYSENTER usw.

Nehmen wir das Beispiel von Software-Interrupts mit der Funktion printf ():
1 - Ihre Software ruft printf () auf.
2 - printf () verarbeitet Ihre Zeichenfolge und Argumente und muss dann eine Kernelfunktion ausführen, da in Ring 3 nicht in eine Datei geschrieben werden kann.
3 - printf () erzeugt einen Software-Interrupt und legt die Nummer einer Kernelfunktion (in diesem Fall die Funktion write ()) in ein Register.
4 - Die Softwareausführung wird unterbrochen und der Befehlszeiger bewegt sich zum Kernelcode. Wir befinden uns jetzt in Ring 0 in einer Kernelfunktion.
5 - Der Kernel verarbeitet die Anforderung und schreibt in die Datei (stdout ist ein Dateideskriptor).
6 - Wenn Sie fertig sind, kehrt der Kernel mithilfe der iret-Anweisung zum Code der Software zurück.
7 - Der Code der Software wird fortgesetzt.

So können Funktionen der C-Standardbibliothek in C implementiert werden. Sie müssen lediglich wissen, wie der Kernel aufgerufen wird, wenn weitere Berechtigungen erforderlich sind.

Macmade
quelle
9
printf () funktioniert auf Systemen ohne Kernel oder
Ring 3 und Ring 0 von x86 funktionieren genau wie der Benutzer- / Kernel-Modus auf Architekturen, die nur zwei Berechtigungsstufen bieten (dh die meisten Nicht-x86-CPUs, auf denen Unix oder Linux ausgeführt wird). Ohne Kern, es ist wirklich mehr wie Ihr freistehendes Programm ist der Kern oder zumindest läuft mit vollen Privilegien so printfist nur eine Funktion innerhalb des Kernels. (Wie der Linux-Kernel printk.)
Peter Cordes
5

Unter Linux stracekönnen Sie mit dem Dienstprogramm sehen, welche Systemaufrufe von einem Programm ausgeführt werden. Nehmen Sie also ein Programm wie dieses

    int main () {
    printf ("x");
    return 0;
    }}

Sagen Sie, Sie kompilieren es als printx, dann strace printxgibt

    execve ("./ printx", ["./printx"], [/ * 49 vars * /]) = 0
    brk (0) = 0xb66000
    access ("/ etc / ld.so.nohwcap", F_OK) = -1 ENOENT (Keine solche Datei oder kein solches Verzeichnis)
    mmap (NULL, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e5000
    access ("/ etc / ld.so.preload", R_OK) = -1 ENOENT (Keine solche Datei oder kein solches Verzeichnis)
    open ("/ etc / ld.so.cache", O_RDONLY | O_CLOEXEC) = 3
    fstat (3, {st_mode = S_IFREG | 0644, st_size = 119796, ...}) = 0
    mmap (NULL, 119796, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa6dc0c7000
    close (3) = 0
    access ("/ etc / ld.so.nohwcap", F_OK) = -1 ENOENT (Keine solche Datei oder kein solches Verzeichnis)
    open ("/ lib / x86_64-linux-gnu / libc.so.6", O_RDONLY | O_CLOEXEC) = 3
    Lesen Sie (3, "\ 177ELF \ 2 \ 1 \ 1 \ 0 \ 0 \ 0 \ 0 \ 0 \ 0 \ 0 \ 0 \ 0 \ 3 \ 0> \ 0 \ 1 \ 0 \ 0 \ 0 \ 200 \ 30 \ 2 \ 0 \ 0 \ 0 \ 0 \ 0 "..., 832) = 832
    fstat (3, {st_mode = S_IFREG | 0755, st_size = 1811128, ...}) = 0
    mmap (NULL, 3925208, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_DENYWRITE, 3, 0) = 0x7fa6dbb06000
    mprotect (0x7fa6dbcbb000, 2093056, PROT_NONE) = 0
    mmap (0x7fa6dbeba000, 24576, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED | MAP_DENYWRITE, 3, 0x1b4000) = 0x7fa6dbeba000
    mmap (0x7fa6dbec0000, 17624, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS, -1, 0) = 0x7fa6dbec0000
    close (3) = 0
    mmap (NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c6000
    mmap (NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c5000
    mmap (NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0c4000
    arch_prctl (ARCH_SET_FS, 0x7fa6dc0c5700) = 0
    mprotect (0x7fa6dbeba000, 16384, PROT_READ) = 0
    mprotect (0x600000, 4096, PROT_READ) = 0
    mprotect (0x7fa6dc0e7000, 4096, PROT_READ) = 0
    Munmap (0x7fa6dc0c7000, 119796) = 0
    fstat (1, {st_mode = S_IFCHR | 0620, st_rdev = makedev (136, 0), ...}) = 0
    mmap (NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) = 0x7fa6dc0e4000
    schreibe (1, "x", 1x) = 1
    exit_group (0) =?

Der Gummi trifft beim vorletzten Aufruf der Spur auf die Straße (sortieren, siehe unten) : write(1,"x",1x). Zu diesem Zeitpunkt geht das Steuerelement vom Benutzerland printxzum Linux-Kernel über, der den Rest erledigt. write()ist eine Wrapper-Funktion, die in deklariert istunistd.h

    extern ssize_t write (int __fd, __const void * __ buf, size_t __n) __wur;

Die meisten Systemaufrufe werden auf diese Weise verpackt. Die Wrapper-Funktion ist, wie der Name schon sagt, kaum mehr als eine dünne Codeschicht, die die Argumente in die richtigen Register legt und dann einen Software-Interrupt 0x80 ausführt. Der Kernel fängt den Interrupt ab und der Rest ist Geschichte. Zumindest funktionierte das früher so. Anscheinend war der Overhead des Interrupt-Trapping ziemlich hoch, und wie bereits in einem früheren Beitrag erwähnt, führten moderne CPU-Architekturen sysenterAssembler-Anweisungen ein, die mit hoher Geschwindigkeit das gleiche Ergebnis erzielen. Diese Seite Systemaufrufe enthält eine schöne Zusammenfassung der Funktionsweise von Systemaufrufen.

Ich habe das Gefühl, dass Sie von dieser Antwort wahrscheinlich ein bisschen enttäuscht sein werden, genau wie ich. In gewissem Sinne ist dies eindeutig ein falscher Grund, da zwischen dem Anruf write()und dem Punkt, an dem der Anruf getätigt werden muss, noch einige Dinge zu tun sind Der Grafikkarten-Frame-Puffer wurde tatsächlich so geändert, dass der Buchstabe "x" auf Ihrem Bildschirm angezeigt wird. Das Heranzoomen des Kontaktpunkts (um bei der Analogie "Gummi gegen die Straße" zu bleiben) durch Eintauchen in den Kernel ist sicher lehrreich, wenn dies zeitaufwändig ist. Ich vermute, Sie müssten durch mehrere Abstraktionsebenen wie gepufferte Ausgabestreams, Zeichengeräte usw. reisen. Stellen Sie sicher, dass Sie die Ergebnisse veröffentlichen, wenn Sie sich dazu entschließen, dies weiterzuverfolgen :)

Daniel Genin
quelle
Es scheint, dass die Informationen auf der verlinkten Webseite, die Systemaufrufe unter Linux beschreiben, veraltet sind. Insbesondere kann die vsyscall-Seite nicht mit dem bereitgestellten Beispielcode auf Kerneln gefunden werden, die neuer als 2.6 sind, und möglicherweise auch auf einigen früheren.
Daniel Genin
Insbesondere wird aufgrund der Randomisierung des Adressraums die vsyscall-Seite nicht mehr einer festen Adresse zugeordnet. Die Adresse der Seite erhalten Sie weiterhin, indem Sie den Parameter ELF auxv AT_SYSINFO ( articles.manugarg.com/aboutelfauxiliaryvectors.html ) nachschlagen .
Daniel Genin
4

Die Standardbibliotheksfunktionen werden auf einer zugrunde liegenden Plattformbibliothek (z. B. UNIX-API) und / oder durch direkte Systemaufrufe (die noch C-Funktionen sind) implementiert. Die Systemaufrufe werden (auf mir bekannten Plattformen) intern durch einen Aufruf einer Funktion mit Inline-ASM implementiert, die eine Systemaufrufnummer und Parameter in CPU-Register schreibt und einen Interrupt auslöst, den der Kernel dann verarbeitet.

Neben Syscalls gibt es auch andere Möglichkeiten der Kommunikation mit Hardware. Diese sind jedoch normalerweise nicht verfügbar oder eher eingeschränkt, wenn sie unter einem modernen Betriebssystem ausgeführt werden oder zumindest für deren Aktivierung einige Syscalls erforderlich sind. Ein Gerät kann speicherabgebildet sein, so dass Schreibvorgänge an bestimmte Speicheradressen (über reguläre Zeiger) das Gerät steuern. Oft werden auch E / A-Ports verwendet, auf die je nach Architektur über spezielle CPU-Opcodes zugegriffen werden kann, oder sie können auch Speicher sein, der bestimmten Adressen zugeordnet ist.

Tronic
quelle
Aber diese Anrufe sind nicht tief in stdio.h
Informationen zum direkten Hardwarezugriff hinzugefügt.
Tronic
3
Alles korrekt, aber nur zu Ihrer Information und für andere, die in diesem Thread posten. Die meisten modernen Betriebssysteme und Architekturen verwenden jetzt spezielle Opcodes, um Systemaufrufe (z. B. Sysenter und Sysexit auf x86) auszuführen, anstatt Software-Interrupts zu verwenden, um die Leistung zu verbessern.
PinkyNoBrain
1

Nun, alle C ++ - Anweisungen mit Ausnahme des Semikolons und der Kommentare werden zu Maschinencode, der der CPU mitteilt, was zu tun ist. Sie können Ihre eigene printf-Funktion schreiben, ohne auf die Assembly zurückgreifen zu müssen. Die einzigen Operationen, die in Assembly geschrieben werden müssen, sind die Eingabe und Ausgabe von Ports sowie Dinge, die Interrupts aktivieren und deaktivieren.

Die Baugruppe wird jedoch aus Leistungsgründen weiterhin in der Programmierung auf Systemebene verwendet. Auch wenn Inline-Assembly nicht unterstützt wird, hindert Sie nichts daran, ein separates Modul in Assembly zu schreiben und es mit Ihrer Anwendung zu verknüpfen.

Vlad
quelle
Sie können keinen Systemaufruf ohne Assembly ausführen oder eine Bibliotheksfunktion aufrufen, die in Assembly geschrieben wurde. C-Compiler verfügen nicht über integrierte Funktionen zum Einrichten von Argumenten in Registern und zum Ausführen von x86 syscall/ sysenteroder. Daher interfolgt dies mit handgeschriebenem asm.
Peter Cordes
0

Im Allgemeinen werden Bibliotheksfunktionen vorkompiliert und Anzeigenobjekte verteilt. Inline-Assembler wird aus Leistungsgründen nur in bestimmten Situationen verwendet, dies ist jedoch die Ausnahme und nicht die Regel. Eigentlich scheint mir printf kein guter Kandidat zu sein, um inline zusammengestellt zu werden. Insetad, Funktionen wie memcpy oder memcmp. Funktionen auf sehr niedriger Ebene können von einem nativen Assembler (masm? Gnu asm?) Kompiliert und als Objekt in einer Bibliothek verteilt werden.

Giuseppe Guerrini
quelle
-7

Der Compiler generiert die Assembly aus dem C / C ++ - Quellcode.

Terry Mahaffey
quelle
Irgendwann gibt es einen handgeschriebenen oder Inline-ASM, um den zugrunde liegenden Systemaufruf aufzurufen. Ich bin mir nicht bewusst , einen Compiler mit einem eingebauten oder intrinsisch für x86 ist syscall, sysenteroder intAnweisungen. Natürlich ist dies nicht in stdio.h, es ist in der bereits kompilierten Bibliothek
Peter Cordes