Cross Compiler

Aus Vu+ WIKI
Wechseln zu: Navigation, Suche

Unter einem Cross-Compiler versteht man einen Compiler, der auf einem bestimmten System (auch Hostplattform genannt) läuft, aber Kompilate (Objektdateien oder ausführbare Programme) für andere Systeme erzeugt. Diese Ziel-Systeme können andere Betriebssysteme, andere Prozessoren oder eine Kombination der beiden sein. Ein konkretes Beispiel wäre ein Compiler, der auf einem Intel-basierten Windows-System läuft und Programme für PowerPC-basierte Linux-Systeme erzeugt. Handelt es sich bei der Zielplattform um ein eingebettetes System, das selbst nicht für Entwicklung und Übersetzung geeignet ist, spricht man auch von einem Target-Compiler.

(Cross-Compiler)

Vorgestellt wird hier ein Cross-Compiler, der auf der auf unseren ARM-Boxen läuft und Code für Mips-Boxen erstellt. Motivation dazu ist, dass unsere 4k Boxen genügend Flash-Speicher und Hauptspeicher bereitstellen, dass ein Cross-Compiler für Mips problemlos neben einem ARM-Compiler auf den Boxen erstellt und installiert werden kann. Und natürlich, dass wir als Entwickler gerne beide von Vu+ bereit gestellten Architekturen unterstützen möchten, auch wenn wir selber nicht beide Architekturen im Zugriff haben.

Beispielhaft zeigt dieser Artikel die Installation und Benutzung des Cross-Compilers.

Installation

In der Database haben wir einen Mips-Cross-Compiler für ARM-Boxen zur Verfügung gestellt. Dieser besteht aus mehreren Paketen :

  • Binutils
  • Compiler
  • Grundstock an Include-Dateien und Libraries ("Sysroot").

Um Binaries für Mips-Boxen auf unseren ARM-Boxen zu erstellen, müssen alle drei Pakete installiert sein.

Nach dem Download der Pakete auf eine ARM-Box können die drei Pakete mit opkg install <Paket.ipk> wie jedes andere Paket auch installiert werden. Nach der Installation befinden sich unter /usr/bin diverse Programme mit dem Präfix mipsel-oe-linux- und unterhalb von /usr/mipsel-oe-linux das "Sysroot" mit Include-Dateien und Bibliotheken, um Mipsel-Binaries zu compilieren.

Benutzung

Beispielhaft wird jetzt die Benutzung des Cross-Compilers anhand eines Pakets beschrieben, welches ich oft und viel benutze: "grep".

Schritt 1: Sourcen besorgen

Die Sourcen für grep befinden sich auf dem GNU-FTP-Server unter https://ftp.gnu.org/gnu/grep/ - die momentan aktuelle Version ist grep-3.8. Die Sourcen werden von dort heruntergeladen und irgendwo auf der Festplatte der Box entpackt, z.B. unter /media/hdd/build. Ausgepackt wird in der Konsole (z.B. Putty) mit dem tar-Befehl: tar xvf grep-3.8.tar.gz

Schritt 2: Sourcen konfigurieren

Mit cd grep-3.8 wird in den ausgpackten Source-Tree gewechselt. Die meisten der heute benutzten Software-Pakete werden mit einem configure konfiguriert; dieses schaut, ob alle benötigten Komponenten für ein erfolgreiches übersetzen vorhanden sind, und generiert die Makefiles, um das Programm anschließend zu übersetzen.

Um Makefiles benutzen zu können, benötigt man das make-Programm. Ein fertig übersetztes Programm befindet sich in der Database beim aktuellen GCC für die ARM-Boxen.

Ein erster Versuch, den Cross-Compiler zu benutzen, ist immer, den Aufruf von configure mit der CC-Variablen zu testen; es gibt eine Reihe von Umgebungsvariablen, die man auf diese Weise (nicht nur) an das configure-Script weiterreichen kann. Ein paar weitere sind: CXX (c++-Compiler), CFLAGS (Flags für den C-Compiler), CXXFLAGS (Flags für den C++-Compiler)

CC=mipsel-oe-linux-gcc ./configure --prefix=/usr

Das funktioniert bei grep nicht, die Fehlermeldung sagt, dass man den Compiler über den Switch --host=mipsel-oe-linux angeben soll. Das funktioniert. Also zweiter Versuch:

./configure --host=mipsel-oe-linux --prefix=/usr

Der Switch --prefix=/usr sagt der Umgebung, dass das fertig übersetzte Programm später nach /usr installiert werden soll; das bedeutet, dass das Binary unter /usr/bin/grep installiert wird. Ohne diesen Switch wird normalerweise /usr/local als Ziel benutzt, und das Binary findet sich später unter /usr/local/bin/grep.

Schritt 3: Compilieren

Als nächstes soll das Programm übersetzt werden; dazu wird make aufgerufen (ein "make" befindet sich im Paket aus dem Download "git-make.zip" in der Database):

make

Meistens dauert der Configure-Schritt genauso lange wie das eigentliche Übersetzen des Programms. Wenn beim Übersetzen keine Fehler passiert sind, kann das Programm installiert werden.

Schritt 4: Installation

Ich kontrolliere gerne vor dem installieren, was installiert werden soll. Fast alle Makefiles kennen das "Target" "install", viele können auch mit dem Target "install-strip" umgehen, und viele kennen auch eine Möglichkeit, das Programm in einem anderen Verzeichnis zu installieren. In der Regel wird das mit der Variablen DESTDIR erreicht. Wir führen also aus:

make install-strip DESTDIR=/media/hdd/install

Das installiert das übersetzte Programm unterhalb von /media/hdd/install, wo wir noch einmal schauen können, was wir mitnehmen möchten, und was nicht.

Zusammengefasst:

  • make install installiert das fertig übersetzte Programm an seinen Bestimmungs-Ort.
  • make install-strip strippt Binaries vor der Installation und installiert sie dann an ihren Bestimmungs-Ort.
  • make install DESTDIR=/media/hdd/install installiert die Binaries in ein anderes Zielverzeichnis /media/hdd/install anstatt in das dafür vorgesehene Verzeichnis.

Weitere nützliche Aufrufe von make

  • make V=1 zeigt genauer an, was beim übersetzen wirklich ausgeführt wird; manchmal unterdrücken Makefiles wie das von grep die Ausgabe, wie genau der Compiler aufgerufen wird.
  • make CC=mipsel-oe-linux-gcc "überschreibt" den Eintrag für den Compiler im Makefile; das funktioniert bei allen Variablen im Makefile, die entsprechend deklariert wurden.

Schritt 5: Programm einpacken und testen

Der nächste Schritt ist bei mir immer, die Dateien als IPK einzupacken. Wie das gemacht werden kann, soll hier nicht gezeigt werden; dazu habe ich unter Paketmanager opkg schon einiges geschrieben.

Wir kontrollieren aber noch einmal, dass der Cross-Compiler ein Mipsel-Binary erstellt hat. Dazu wechseln wir in der Konsole mit "cd" nach /media/hdd/install/usr/bin. Ein ls -l zeigt hier drei Dateien an, egrep, fgrep und grep. egrep und fgrep sind Shell-Scripten, das eigentliche Binary ist grep. Wir können uns den Inhalt mit einigen Tools anschauen. Als erstes:

file grep: Ausgabe sollte sein: grep: ELF 32-bit LSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked, interpreter /lib/ld.so.1, for GNU/Linux 2.6.32, stripped

Schauen wir etwas genauer in das Binary:

mipsel-oe-linux-objdump -p grep

Hier sehen wir alle interessanten Informationen, zum Beispiel welche Shared-Libraries das Programm in welcher Version auf der Mips-Maschine benötigt.

Ausgabe von objdump grep: bitte aufklappen.


grep:     file format elf32-tradlittlemips

Program Header:
    PHDR off    0x00000034 vaddr 0x00400034 paddr 0x00400034 align 2**2
         filesz 0x00000140 memsz 0x00000140 flags r--
  INTERP off    0x00000174 vaddr 0x00400174 paddr 0x00400174 align 2**0
         filesz 0x0000000d memsz 0x0000000d flags r--
0x70000003 off    0x000001a8 vaddr 0x004001a8 paddr 0x004001a8 align 2**3
         filesz 0x00000018 memsz 0x00000018 flags r--
0x70000000 off    0x000001c0 vaddr 0x004001c0 paddr 0x004001c0 align 2**2
         filesz 0x00000018 memsz 0x00000018 flags r--
    LOAD off    0x00000000 vaddr 0x00400000 paddr 0x00400000 align 2**16
         filesz 0x00040280 memsz 0x00040280 flags r-x
    LOAD off    0x00040280 vaddr 0x00450280 paddr 0x00450280 align 2**16
         filesz 0x00000ab8 memsz 0x00011580 flags rw-
 DYNAMIC off    0x000001d8 vaddr 0x004001d8 paddr 0x004001d8 align 2**2
         filesz 0x00000118 memsz 0x00000118 flags r--
    NOTE off    0x00000184 vaddr 0x00400184 paddr 0x00400184 align 2**2
         filesz 0x00000020 memsz 0x00000020 flags r--
EH_FRAME off    0x00040240 vaddr 0x00440240 paddr 0x00440240 align 2**2
         filesz 0x00000014 memsz 0x00000014 flags r--
    NULL off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2
         filesz 0x00000000 memsz 0x00000000 flags ---

Dynamic Section:
  NEEDED               libc.so.6
  INIT                 0x00402cf4
  FINI                 0x0043ca60
  INIT_ARRAY           0x00450280
  INIT_ARRAYSZ         0x00000004
  FINI_ARRAY           0x00450284
  FINI_ARRAYSZ         0x00000004
  HASH                 0x004002f0
  STRTAB               0x00401d6c
  SYMTAB               0x00400b8c
  STRSZ                0x00000c84
  SYMENT               0x00000010
  MIPS_RLD_MAP         0x004508b0
  MIPS_RLD_MAP_REL     0x00050670
  DEBUG                0x00000000
  PLTGOT               0x004508c0
  REL                  0x00402c8c
  RELSZ                0x00000068
  RELENT               0x00000008
  MIPS_RLD_VERSION     0x00000001
  MIPS_FLAGS           0x00000002
  MIPS_BASE_ADDRESS    0x00400000
  MIPS_LOCAL_GOTNO     0x00000097
  MIPS_SYMTABNO        0x0000011e
  MIPS_UNREFEXTNO      0x00000029
  MIPS_GOTSYM          0x00000098
  VERNEED              0x00402c2c
  VERNEEDNUM           0x00000001
  VERSYM               0x004029f0

Version References:
  required from libc.so.6:
    0x0d696914 0x00 06 GLIBC_2.4
    0x0d696915 0x00 05 GLIBC_2.5
    0x0d696912 0x00 04 GLIBC_2.2
    0x0d696910 0x00 03 GLIBC_2.0
    0x0d696913 0x00 02 GLIBC_2.3
private flags = 1007: [abi=O32] [mips1] [not 32bitmode] [noreorder] [PIC] [CPIC]

MIPS ABI Flags Version: 0

ISA: MIPS1
GPR size: 32
CPR1 size: 32
CPR2 size: 0
FP ABI: Hard float (double precision)
ISA Extension: None
ASEs:
	None
FLAGS 1: 00000000
FLAGS 2: 00000000

Das Binary kann jetzt auf eine Mips-Box kopiert werden, um endgültig sicherzustellen, dass wir hier keinen Murks produziert haben.

Nützliches zu objdump, ldd und Versionierung von Shared-Libraries

objdump

Das folgende ist eher allgemein gehalten und gilt für alle Architekturen. Damit man im Vorfeld schon weiß, ob die auf der Zielmaschine installierten Shared-Libraries neu genug sind, damit ein Programm läuft, gibt es ein paar Hilfsmittel. Eines haben wir vorhin schon kennen gelernt: objdump. Der interessante Teil befindet sich im Abschnitt "Version References:".

Version References:
  required from libc.so.6:
    0x0d696914 0x00 06 GLIBC_2.4
    0x0d696915 0x00 05 GLIBC_2.5
    0x0d696912 0x00 04 GLIBC_2.2
    0x0d696910 0x00 03 GLIBC_2.0
    0x0d696913 0x00 02 GLIBC_2.3

Hier erfahren wir, dass das Binary gegen eine Sharded-Library libc.so.6 gelinkt wurde. Um nicht bei jedem Update der Libc die Versionsnummer "6" erhöhen zu müssen, werden neue Features oder geänderte Aufrufe in der Library selber versioniert. Hier sieht man, dass Features der GLIBC_2.0 bis GLIBC_2.5 benötigt werden. Ein wenig Recherche bringt zutage, dass diese Versionsnummern auch wirklich mit den Versionen der glibc übereinstimmen. Die auf unseren Boxen vorinstallierte glibc liegt in der Version 2.21 vor und benutzt deshalb für die neuen ab dieser Version integrierten Features die interne Version GLIBC_2.21. Aktuell ist übrigens die glibc-2.37. Da die glibc zweimal jährlich aktualisiert wird, liegen hier 8 Jahre (fehlende) Entwicklung dazwischen.

ldd

Ein anderes sehr nützliches Tool ist ldd. Ohne weitere Parameter werden hier einfach die benötigten Shared-Libraries angezeigt:

root@vusolo4k:~# ldd /usr/bin/grep 
	libpcre.so.1 => /usr/lib/libpcre.so.1 (0xb6f20000)
	libc.so.6 => /lib/libc.so.6 (0xb6db8000)
	/lib/ld-linux-armhf.so.3 (0xb6f64000)

Sehr nützlich ist der Aufruf mit Parameter "-v":

root@vusolo4k:~# ldd -v /usr/bin/grep 
	libpcre.so.1 => /usr/lib/libpcre.so.1 (0xb6ec0000)
	libc.so.6 => /lib/libc.so.6 (0xb6d58000)
	/lib/ld-linux-armhf.so.3 (0xb6eff000)

	Version information:
	/usr/bin/grep:
		libc.so.6 (GLIBC_2.26) => /lib/libc.so.6
		libc.so.6 (GLIBC_2.5) => /lib/libc.so.6
		libc.so.6 (GLIBC_2.34) => /lib/libc.so.6
		libc.so.6 (GLIBC_2.4) => /lib/libc.so.6
	/usr/lib/libpcre.so.1:
		libc.so.6 (GLIBC_2.4) => /lib/libc.so.6
	/lib/libc.so.6:
		ld-linux-armhf.so.3 (GLIBC_2.4) => /lib/ld-linux-armhf.so.3
		ld-linux-armhf.so.3 (GLIBC_PRIVATE) => /lib/ld-linux-armhf.so.3

Ähnlich wie bei objdump werden auch hier die benötigten Versionen der Shared-Libraries angezeigt.

Weitere Bekannte Libraries, die versioniert sind, sind unter anderem folgende:

  • libstdc++ - GLIBCXX_3.4.21 gehört z.B. zur libstdc++-6.0.21 (GCC-5.1), CXXABI_1.3.12 genauso wie GLIBCXX_3.4.26 zur libstdc++-6.26 (GCC-9.1). Die Versionen können der Dokumentation von gcc entnommen werden: https://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html
  • libssl und libcrypto von OpenSSL: die benötigten Versionen steht direkt in der Symboltabelle, z.B. als OPENSSL_1.0.2g
  • zlib: die benötigten Versionen stehen ebenfalls direkt in der Symboltabelle der libz.so, z.B. ZLIB_1.2.12

c++filt

Ein letztes nützliches Tool in Zusammenhang mit Dumps und wenn man mehr über die Symbol-Tabelle eines Binaries erfahren möchte, ist c++filt. Mit diesem Tool können "gemanglte" Symbole wieder lesbar gemacht werden. Schauen wir z.B. in die Symbol-Tabelle von enigma2 mit objdump -R /usr/bin/enigma2, sehen wir viele c++-Symbolnamen, wie z.B. _ZNSt3tr18__detail12__prime_listE@GLIBCXX_3.4.10

c++filt kann uns das wieder lesbarer machen: c++filt _ZNSt3tr18__detail12__prime_listE spuckt dafür aus: std::tr1::__detail::__prime_list.

Oder:

root@vusolo4k:~# c++filt _ZNSt14basic_ifstreamIcSt11char_traitsIcEEC1EPKcSt13_Ios_Openmode
std::basic_ifstream<char, std::char_traits<char> >::basic_ifstream(char const*, std::_Ios_Openmode)
root@vusolo4k:~# c++filt _ZN19CaProgramMapSectionC1EPK17ProgramMapSectionhhRKSt6vectorItSaItEE
CaProgramMapSection::CaProgramMapSection(ProgramMapSection const*, unsigned char, unsigned char, std::vector<unsigned short, std::allocator<unsigned short> > const&)