C vs C++ bei OSDEV



  • Erhard Henkes schrieb:

    Es gibt leider auch viele Punkte, die man nur instinktiv richtig machen kann, z.B. C anstelle C++ verwenden

    Vorsicht, Vorsicht... 😉

    Habe neulich meinen älteren Code rausgekrammt und siehe da, so schlecht ist C++ nicht...

    CScreen stdout;
        CCpu CPU(0u);
    
        stdout << "Started.\n";
    
        ...
    
        CPU.DisableInterrupts();
        CPU.Halt();
    

  • Mod

    Die Idee der Klasse mit ihren "domestizierten" Funktionen ist hervorragend. Da hat C nur die Struktur mit "freien" Funktionen zu bieten. Das bezahlt man in C++ mit viel Klassenbeziehungs-Wirrwarr und im kernel nutzlosen Overhead.



  • Erhard Henkes schrieb:

    Die Idee der Klasse mit ihren "domestizierten" Funktionen ist hervorragend. Da hat C nur die Struktur mit "freien" Funktionen zu bieten. Das bezahlt man in C++ mit viel Klassenbeziehungs-Wirrwarr und im kernel nutzlosen Overhead.

    Nur weil man C++ benutzt wird man aber nicht gezwungen komplizierte Klassenbeziehungen zu bauen oder Techniken zu nutzen die Overhead erzeugen. Und nutzlos ist es sicherlich nicht, selbst wenn es nur didaktischen Zwecken dient. 😃


  • Mod

    Zeige mir eine konkrete Stelle in PrettyOS im Kernel, bei der uns C++ einen greifbaren Vorteil bringen würde.



  • Hallo,

    Erhard Henkes schrieb:

    Es gibt leider auch viele Punkte, die man nur instinktiv richtig machen kann

    Nach meiner Erfahrung sind Computer streng deterministische Gebilde, instinktive Entscheidungen sind daher nicht angebracht. 😉

    Erhard Henkes schrieb:

    Das bezahlt man in C++ mit viel Klassenbeziehungs-Wirrwarr und im kernel nutzlosen Overhead.

    Overhead gegenüber C gibt es in C++ erst wenn man virtuelle Methoden benutzt oder die Exceptions verwendet. Insofern spricht IMHO nichts gegen die Verwendung von C++ anstelle von C, in jedem Anwendungsfall auch in einem OS-Kernel, solange man nicht zu tief in die Trickkiste von C++ greift. Auch der erzeugte Programm-Code ist dann nicht größer/langsamer als der von C.

    Grüße
    Erik


  • Mod

    Das sieht dann z.B. so aus:

    // Hilfskonstruktionen
    //
    // pure virtual wird dennoch ausgeführt:
    extern "C"{
    void __cxa_pure_virtual() {} }
    //
    // Ersatz für new, new[], delete und delete[] der fehlenden C++-Standard-Bibliothek
    void* operator new      (size_t size) { return malloc(size); }
    void* operator new[]    (size_t size) { return malloc(size); }
    void  operator delete   (void* ptr)   { free(ptr); }
    void  operator delete[] (void* ptr)   { free(ptr); }
    //
    


  • Erhard Henkes schrieb:

    Zeige mir eine konkrete Stelle in PrettyOS im Kernel, bei der uns C++ einen greifbaren Vorteil bringen würde.

    Ein technischer Grund fällt mir zurzeit nicht ein, manchmal ist es aber einfach eine andere Denkweise die hilft. Bei der Floppy gibt es z.B. vom Design her Probleme, die meiner Meinung nach objektorientiert eher auffallen und leicht zu lösen sind. Erfahrene C Entwickler werden diese Meinung sicherlich nicht teilen, da sie auch so die Probleme und Lösungen direkt sehen.

    Ein Problem ist das eine mögliche Erweiterung auf mehrere Floppys unbequem ist. Man müsste um eine andere Floppy anzusprechen den Wert von _CurrentDrive umsetzen. Das sorgt aber dafür das sämtliche Befehle auf der anderen Floppy ausgeführt werden. Letztendlich würde man sich also an vielen Stellen den alten Wert merken, den Wert ändern, Befehle ausführen und den Wert wieder zurücksetzen. Ist jetzt kein Beinbruch, aber unschön wenn man das mal irgendwo vergisst. In C++ wäre man wahrscheinlich als erstes auf die Idee gekommen das ganze als Klasse zu modellieren. Dann steht jedes Objekt für eine Floppy und besitzt ein separates _CurrentDrive.

    Das andere Problem ist die schon öfter erwähnte Treiberschnittstelle. In C++ würde man z.B. eine Klasse BlockDevice als Schnittstelle mit Funktionen wie read und write erstellen. Klasse Floppy, Festplatte etc. erben von BlockDevice und implementieren diese Funktionen. In einer Datenstruktur wie List<BlockDevice> kann man dann alle Geräte sammeln. In C wird dies nicht ganz so ausdrucksstark dargestellt. Statt der Klasse BlockDevice hat man eine Struktur mit Funktionspointern. Die bisherige Liste in PrettyOS arbeitet bisher auch nur mit void*. Es wird also auch noch öfters gecastet werden müssen. Es ist im Prinzip die gleiche Vorgehensweise wie in C++, allerdings finde ich die C++ Variante etwas anschaulicher.



  • Erhard Henkes schrieb:

    Das sieht dann z.B. so aus:

    // Hilfskonstruktionen
    //
    // pure virtual wird dennoch ausgeführt:
    extern "C"{
    void __cxa_pure_virtual() {} }
    //
    // Ersatz für new, new[], delete und delete[] der fehlenden C++-Standard-Bibliothek
    void* operator new      (size_t size) { return malloc(size); }
    void* operator new[]    (size_t size) { return malloc(size); }
    void  operator delete   (void* ptr)   { free(ptr); }
    void  operator delete[] (void* ptr)   { free(ptr); }
    //
    

    Oder so (das ist jetzt natürlich kein Musterbeispiel, falls jemand trotzdem Kritik hat, gerne her damit):

    #include "datatypes.h"
    
    #ifdef MAX_MEMORY_BLOCKS
    #error Already defined.
    #else
    /// Specifies number of memory blocks used for dynamic memory
    #define MAX_MEMORY_BLOCKS   (32u)
    #endif
    
    #ifdef MAX_BLOCK_SIZE
    #error Already defined.
    #else
    /// Specifies number of uint32_t words contained in one memory block
    #define MAX_BLOCK_SIZE      (1023u)
    #endif
    
    /// Structure for memory block
    static struct tMemoryBlock
    {
        uint32_t used;                      ///< Flag, indicating if memory block is used or not
        uint32_t data[MAX_BLOCK_SIZE];      ///< Data words of the memory block
    }
    MemoryBlock[MAX_MEMORY_BLOCKS];
    
    /// Check size of one MemoryBlock structure. It is expected to be a fixed number of bytes.
    typedef char CheckSize_tMemoryBlock[sizeof(MemoryBlock[0]) == 4096 ? 1: -1];
    
    void* operator new(uint32_t size)
    {
        uint32_t i = 0u;
        uint32_t found_i = 0u;
    
        size = size;
    
        // This loop goes through all memory blocks and looks for free one
        for (i = 0u; i < MAX_MEMORY_BLOCKS; ++i)
        {
            if (0u == MemoryBlock[i].used)
            {
                // Found one unused memory block
                // Set found index and break the loop
                found_i = i;
                i = (MAX_MEMORY_BLOCKS - 1u);
            }
        }
    
        // Set used flag
        ++(MemoryBlock[found_i].used);
    
        return &(MemoryBlock[found_i].data[0]);
    }
    
    void operator delete(void* pData)
    {
        uint32_t i = 0u;
    
        // This loop goes through all memory blocks and checks, if
        // the pointer points to it. The corresponding memory block
        // gets unused then.
        for (i = 0u; i < MAX_MEMORY_BLOCKS; ++i)
        {
            const void* pBegin = &(MemoryBlock[i].data[0]);
            const void* pEnd = &(MemoryBlock[i].data[MAX_BLOCK_SIZE - 1u]);
    
            if ((pData >= pBegin) && (pData <= pEnd))
            {
                // Found one used block, no sense to continue the loop
                // Set it as unused and break the loop
                MemoryBlock[i].used = 0;
                i = (MAX_MEMORY_BLOCKS - 1u);
            }
        }
    }
    
    void* operator new(uint32_t size, void* pBuffer)
    {
        size = size;
        // Because this function is "placement new", just return the
        // pointer to the buffer
        return pBuffer;
    }
    
    void* operator new[](uint32_t size)
    {
        return operator new(size);
    }
    
    void operator delete[](void* pData)
    {
        operator delete(pData);
    }
    


  • Tobiking2 schrieb:

    In C++ würde man z.B. eine Klasse BlockDevice als Schnittstelle mit Funktionen wie read und write erstellen. Klasse Floppy, Festplatte etc. erben von BlockDevice und implementieren diese Funktionen. In einer Datenstruktur wie List<BlockDevice> kann man dann alle Geräte sammeln. In C wird dies nicht ganz so ausdrucksstark dargestellt. Statt der Klasse BlockDevice hat man eine Struktur mit Funktionspointern. Die bisherige Liste in PrettyOS arbeitet bisher auch nur mit void*. Es wird also auch noch öfters gecastet werden müssen. Es ist im Prinzip die gleiche Vorgehensweise wie in C++, allerdings finde ich die C++ Variante etwas anschaulicher.

    Lese gerade zufällig in c't (Heft 19, 31.8.2009):

    Kritische Lücke im Linux-Kernel
    Eine Sicherheitslücke im Linux-Kernel betrifft alle Versionen ... seit Mai 2001. ... Üblicherweise deklariert eine Pointer-Struktur, welche Operationen ein Socket untersützt, etwa accept, bind und so weiter. Ist aber die Operation accept nicht implementiert, so sollte sie auf eine vordefinierte Komponente wie sock_no_accept zeigen. Dies ist offensichtlich nicht bei allen implementierten Protokollen der Fall...

    So ist es, man hat ja keine Überwachung durch den Compiler, also muss der Programmierer selbst darauf achten, dass er alle Funktions-Pointer setzt bzw. auf deren Gültigkeit prüft...


  • Mod

    Die Stärken moderner Programmiersprachen bestehen in der Fehlervermeidung und -überwachung. Allerdings hat man dafür bei C einen einfacheren Umgang mit der Materie. C++ fordert dem Entwickler hier deutlich mehr ab.

    Warum nicht gleich Java für OSDEV?



  • Erhard Henkes schrieb:

    Warum nicht gleich Java für OSDEV?

    Unter anderem weil Java sich wieder von der Politik des Fehlervermeidens verabschiedet, dabei wieder unter das Niveau von C fällt (,wobei ich mit "Vermeiden" nicht meine, daß man sie nur auffängt), und langsamer ist und man nur über Umwege an die Hardware kommt, was für ein BS, wie ich füchte, dann doch zu nervig wird.



  • Erhard Henkes schrieb:

    Allerdings hat man dafür bei C einen einfacheren Umgang mit der Materie. C++ fordert dem Entwickler hier deutlich mehr ab.

    Was meinst du denn konkret damit? Der meiste Code von PrettyOS ist ebenfalls valider C++ Code. An vielen Stellen fehlt nur ein expliziter Cast oder ein extern "C" wenn die Funktionen von Assembler aus aufgerufen werden. Das ist weder schwieriger noch weiter von der Materie entfernt.

    Im Gegenzug erhoffe ich mir das du nicht mehr so etwas wie an konkrete Laufwerke gekoppelte Dateisyetemfunktionen schreibst. Ich habe dir ja schon prophezeit, dass die Fehlersuche die du jetzt für das Fat Dateisystem auf Diskette durchführst, in ähnlicher Form wieder auftritt wenn du beides voneinander trennst oder die Fat Funktionen auf andere Laufwerke überträgst. Es wäre deutlich einfacher wenn man es also gleich richtig macht. Komplizierter ist das ganze sicherlich nicht.



  • C++ hat nach meiner Ansicht den Vorteil, dass man mehr Semantik im Code unterbringen kann. Und mit Templates wird das verwenden von Datenstrukturen stark vereinfacht.

    Im Kernel würde ich aber natürlich auf den Runtime-abhängigen Teil verzichten (also Exceptions, RTTI).

    @abc.w
    size_t anstelle uint32_t!

    Aber der Code wirkt teilweise ein bisschen komisch

    size = size;
    

    oder anstelle von break dieses Konstrukt

    for (i = 0u; i < MAX_MEMORY_BLOCKS; ++i)
    {
    //...
          i = (MAX_MEMORY_BLOCKS - 1u);
    }
    

    Grobe Fehler:
    size wird ignoriert: wenn man mehr als MAX_BLOCK_SIZE Speicher anfordert, dann bekommt man einen Pointer auf einen zu kleinen Speicherbereich.

    Und wenn man mehr als MAX_MEMORY_BLOCKS anfordert, dann überschreibt man immer wieder den ersten Block.



  • rüdiger schrieb:

    @abc.w
    size_t anstelle uint32_t!

    Ja... Hm, habe nur uint32_t und uint8_t als Datentypen.

    rüdiger schrieb:

    Aber der Code wirkt teilweise ein bisschen komisch... ...Grobe Fehler...

    Ja, zu viel von KISS (keep it sehr stupid)... 🙂 Ist sowieso nur zum Testen und Hauptsache, kein malloc(). Funktionierenden Kernel in C++ habe ich auch nicht und bin sehr weit davon entfernt, aber es hat seinen Reiz...



  • Erhard Henkes schrieb:

    Zeige mir eine konkrete Stelle in PrettyOS im Kernel, bei der uns C++ einen greifbaren Vorteil bringen würde.

    Im Moment könnte ich es ganz gut für das Kontrollieren des Interrupt-Flags gebrauchen (sti/cli):
    Wenn ich eine Routine schützen will, sieht das ja so aus:

    void foo()
    {
        cli();
    
        if ( x )
        {
            sti();
            return;
        }
    
        sti();
    }
    

    Das ist ja noch halbwegs OK. Das Problem dabei ist nur, dass foo u.U. eigentlich gar nicht die Interrupts anschalten darf. Z.B. wenn foo aus einer ebenfalls geschützten Funktion aufgerufen wird:

    void bar()
    {
        cli();
        foo();
        // Hier sind Interrupts leider wieder eingeschaltet
        doImportantStuff();
        sti();
    }
    

    D.h. wir brauchen eigentlich auch noch Abfrage, ob Interrupts beim cli-Aufruf gesetzt waren:

    void foo()
    {
        // cli sollte zurückgeben, ob Interrupts vorher gesetzt waren.
        bool interrupts_enabled = cli();
    
        if ( x )
        {
            if ( interrupts_enabled )
                sti();
            return;
        }
    
        if ( interrupts_enabled )
            sti();
    }
    

    Das an jeden Ausgang zu setzen, ist irgendwie eklig. Mit C++ wäre das doch eher einfacher:

    void foo()
    {
        DisableInterrupts interrupt_protection;
        // ...
    }
    


  • Man müsste ja noch bei jedem return aufpassen, die Interrupts wieder einzuschalten, was man in einem Destruktor machen könnte 🤡



  • abc.w schrieb:

    Man müsste ja noch bei jedem return aufpassen, die Interrupts wieder einzuschalten, was man in einem Destruktor machen könnte 🤡

    Darauf wollte ich eigentlich hinaus :p "DisableInterrupts" als Ersatz für das Ausschalten der Interrupts, merken ob sie an waren und gegebenenfalls wieder Anschalten der Interrupts beim rausspringen. Und das mit einem Einzeiler.


  • Mod

    Das Thema C vs C++ ist nicht neu:
    http://forum.osdev.org/viewtopic.php?f=15&t=22406&start=0

    Die Diskussion ist sehr unstrukturiert, aber einige Erfahrungsbeiträge sind lesenswert. In Summe gesehen gehen beide Sprachen, wenn man es richtig anpackt.

    Ich persönlich bin überrascht, wie gut man in C einen Kernel programmieren kann.
    Daher sehe ich keinen wirklichen Vorteil in C++.



  • Daher sehe ich keinen wirklichen Vorteil in C++.

    Ich sehe durchaus Vorteile. Funktionsüberladung ist immer gut^^, Dinge wie Listen ließen sich viel leichter nutzen (Konstruktoren/Destruktoren, u.a.), statt void* data mitzuführen könnte man einfach von einer Basisklasse Erben.



  • Das wichtigste: Information Hiding! Sprich Kapselung von Daten, so das man einen kontrollierten Zugriff erlauben kann. Völlig unabhängig von Vtable, Template-Bloat u.a. Mythen die Performanceverlust bedeuten sollen.

    Gerade die Kapselung kann viel zur Qualität eines Projektes (egal ob Kernel oder Anwendungen) beitragen. Sowohl weniger Fehler zur Laufzeit, als auch einer einfachen Wartung.


Anmelden zum Antworten