HomeBlogMagic

C++ static variable initializing in linux kernel modules

Hallo an alle,

Die letzten Tage habe ich mich an ein etwas komplizierteres Thema heran gewagt.
Ich hatte das Ziel einen Linux Treiber mit C++ zu entwickeln oder, worauf es letztlich hinaus lief, die C-Aufrufe an C++ Objekte weiter zu leiten.

Kurzum, es hat funkioniert, die ersten Treiber laufen auch schon mit C++. Ein wichtiger Punkt war allerdings etwas kniffliger: Das initialisierien von statischen Variablen.

Damit diese überhaupt initialisiert werden können, müssen die Konstruktor-Aufrufe in eine Init-Sektion gepackt werden, weshalb wir unsere C++ Lib mit folgenden Compiler-Flags bauen müssen:

  • -ffunction-sections
  • -fdata-sections

Wenn die C++-Lib mit dem C-Treiber Interface gelinkt wird, erstellt der GCC eine init_array Sektion mit Pointern zu allen statischen Konstruktoren.
Ein kleines Beispiel mit einem einzelnen Konstruktor aus einer gelinkten map:

.init_array    0x0000000000000038        0x8 /home/coolcow/Test/Software/Drv/libcpp.a.o(CFramework.cpp.o)

Damit diese wir diese Funktionen aufrufen können müssen wir unserem Treiber den Anfang und Ende des Arrays als Pointer bekannt geben. Dazu müssen wir die init_array Section erweitern. Dazu benutzen wir am besten ein Linker Script:

SECTIONS
{
  . = ALIGN(4);
  .init_array :
  {
    __init_array_start = .;
    KEEP (*(.init_array*))
    __init_array_end = .;
  }
}

Jetzt haben wir die Einsprungadressen für die Init Funktionen markiert mit __init_array_start und __init_array_end

Um das Linkerscript dem Linker zu übergeben müssen wir das Makefile um folgendes erweitern:

ldflags-y := -Map "${SOURCE_PATH}/kernel.map" -T "${SOURCE_PATH}/init_array.ld"

Das Script wird bei mir in init_array.ld gespeichert und der Pfad zu der Datei in der Umgebungsvariable SOURCE_PATH hinterlegt. Wie zu sehen erstellen ich mit -Map auch immer eine Map um eventuelle Fehler zu finden.

Jetzt erkennen wir in der Map, als Beispiel, folgende ausgabe:

.init_array     0x0000000000000038        0x8
                0x0000000000000038        __init_array_start = .
 *(.init_array*)
 .init_array    0x0000000000000038        0x8 /home/coolcow/Test/Software/Drv/libcpp.a.o(CFramework.cpp.o)
                0x0000000000000040        __init_array_end = .

Auf diese weise können wir das Array addressieren und auch das Ende ermitteln. Im letzen Schritt müssen wir die Konstruktoren nur noch aufrufen und alle Variablen sind initialisiert.

Dazu habe ich eine Init Funktion geschrieben welche ich als erste Funktion im Treiber aufrufe:

extern "C"
{
    extern void (*__init_array_start []) (void) __attribute__((weak));
    extern void (*__init_array_end []) (void) __attribute__((weak));

    void static_init()
    {
      for (void (**pFunction)() = __init_array_start; pFunction < __init_array_end; pFunction++)
      {
        (*pFunction)();
      }
    }
}

In diesem Beispiel habe ich das in einer .cpp implementiert weshalb das extern "C" notwendig war. Es liese sich genauso gut noch im C-Interface implementieren, das ist Geschmackssache.

Permalink: https://adirmeier.de/Blog/ID_367
Tags: Blog, C/C++, kernel, linux, treibervon am 2021-03-19