Komputery jednopłytkowe (ang. SBC
— Single Board Computer
) to uniwersalne urządzenia mikroprocesorowe pracujące pod kontrolą systemu operacyjnego.
Zazwyczaj systemem tym jest Linux.
Architektury komputerów jednopłytkowych mogą istotnie się różnić od architektur stosowanych dla komputerów osobistych.
W rezultacie przygotowanie aplikacji w postaci binarnej na urządzenie docelowe (target
) na komputerze osobistym (host
) wymaga wielu, często dość skomplikowanych, zabiegów.
Uwaga niniejszego opracowania skupiona jest na realizacji różnych scenariuszy pracy z wykorzystaniem edytora VS Code
i eliminujących konieczność instalacji i konfiguracji środowiska graficznego na SBC
.
Producenci urządzeń klasy SBC
nieustannie przekonują potencjalnych nabywców, że urządzenia te mogą funkcjonować jako ubogie wersje komputerów osobistych, jednak nie znam nikogo, kto używałby ich w ten sposób.
Tak naprawdę siłą komputerów tego typu jest dostępność niskopoziomowych interfejsów takich, jak linie GPIO
czy magistrale I2C
, SPI
przy jednoczesnej dostępności wielu serwisów sieciowych.
Jądro systemu operacyjnego zainstalowanego na takim urządzeniu zapewnia abstrakcyjny interfejs dostępu sprzętu, dzięki czemu aplikacje wyglądają w zasadzie tak samo, niezależnie od platformy sprzętowej wykorzystanej do realizacji urządzenia SBC
.
Idealną sytuacją byłoby, gdyby aplikacje dla komputera jednopłytkowego można było tworzyć, budować i uruchamiać bezpośrednio na nim. Komfortowa praca wymaga jednak uruchomienia konsoli graficznej (instalacji oprogramowania oraz doposażenia w dodatkowy sprzęt w postaci myszy, klawiatury i monitora), zapewnienia odpowiedniej przestrzeni dyskowej i wystarczającego rozmiaru pamięci RAM.
Scenariusz taki jest możliwy, gdy SBC
ma zasoby porównywalne z komputerami PC
.
W zdecydowanej większości przypadków tak nie jest.
We wszystkich opisanych dalej scenariuszach jedynym
interfejsem programisty będzie
Visual Studio Code
– doceniony przez programistów i wysoce konfigurowalny edytor.
Mechanizm rozszerzeń w edytorze umożliwia jego uzupełnienie o funkcje budowania i uruchamiania aplikacji co de facto
przekształca go w zintegrowane środowisko pracy.
VS Code
jest dostępny nieodpłatnie na wszystkie popularne systemy operacyjne.
Pracę z projektami C/C++ znacznie ułatwia C/C++ Extension Pack
.
Dalej przyjęto, że rozszerzenie to jest zainstalowane.
Rozważane są scenariusze pracy w których kod aplikacji jest budowany na urządzeniu docelowym lub komputerze PC
.
1. Kompilacja natywna
Opisane w tym punkcie scenariusze wymagają instalacji w systemie target
narzędzi umożliwiających budowanie aplikacji, np.
1
2
apt install -y build-essentials make
apt install -y libgpiod-dev
VS Code
jest wykorzystywane jedynie do zapewnienia programiście wygodnego interfejsu graficznego.
1.1 Praca zdalna z wykorzystaniem Code Server
Instalacja w Visual Studio Code
rozszerzenia Remote SSH
umożliwia pracę zdalną na dowolnym koncie SSH
, tak jak gdyby był to komputer lokalny. W procesie automatycznej konfiguracji zdalnego konta na systemie target
jest instalowany i uruchamiany Code Server
.
Otwarcie zdalnej sesji przebiega następująco:
-
po uruchomieniu
Visual Studio Code
wystarczy wybrać zielony przycisk “” w lewym dolnym rogu ekranu
-
z palety komend wybrać “
Connect to Host
”, -
w polu adresu wpisać
root@opi.usb
(“root” można zamienić na dowolne inne konto,opi.usb"
jest nazwą sieciową systemutarget
i może być zamieniona na stosowny numer IP),
Rozszerzenie Remote SSH
automatycznie zainstaluje na zdalnym koncie oprogramowanie Code Server
(w katalogu ‘.vscode’ na zdalnym koncie) umożliwiające egzekucję i odbieranie wyników komend wydawanych w tunelu SSH.
Praca w takiej konfiguracji jest bardzo wygodna i w zasadzie niczym nie różni się od pracy lokalnej. Jedyna różnica polega na tym, że źródła aplikacji i narzędzia ich kompilacji muszą być obecne na systemie target
. Na systemie host
nie trzeba instalować nić oprócz Visual Studio Code
.
Niestety, przedstawione rozwiązanie ma dwie (poważne) wady:
-
code-server
wspiera tylko architektury 64-bitowe:amd64
iarm64
, co na wyklucza zastosowanie tego podejścia na wielu dostępnych na rynkuSBC
, -
system
target
powinien być wyposażony w 1GB RAM i dwurdzeniowy CPU.
1.2 Praca zdalna w tunelu SSH
Poprzednia metoda nie jest uniwersalna ze względu na wymagania stawiane przez Code Server
.
Element ten można wyeliminować z poprzedniego schematu.
Tym razem pliki źródłowe są edytowane na systemie host
i przed kompilacją kopiowane na system target
, gdzie są kompilowane za pomocą natywnych narzędzi.
W toku dalszego wywodu zostanie przyjęto następującą strukturę podkatalogów w katalogu projektu
1
2
3
4
5
6
.vscode/ -> katalog z plikami konfiguracyjnymi IDE
obj/ -> katalog pomocniczy dla make
src/ -> pliki źródłowe projektu
src/include/
src/main.c
Makefile
Powyższy projekt można zbudować natywnie, korzystając z następującego ‘Makefile
’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# Compiler settings - Can be customized.
CC = gcc
CXX = g++
CPPFLAGS =
CFLAGS = -std=c11 -g -Wall -fdiagnostics-color=always
CXXFLAGS = -std=c++11 -g -Wall -fdiagnostics-color=always
LDFLAGS = -lpthread -lgpiod
# Makefile settings - Can be customized.
APPNAME = main
EXT = .c
SRCDIR = src
OBJDIR = obj
REMOTE = root@opi.usb
REMOTEPWD = /root/RemotePowerButton
SRC = $(wildcard $(SRCDIR)/*$(EXT))
OBJ = $(SRC:$(SRCDIR)/%$(EXT)=$(OBJDIR)/%.o)
DEP = $(OBJ:$(OBJDIR)/%.o=%.d)
RM = rm
all: $(APPNAME)
# Copy sources to target
copysrc:
scp -p -r ./src/ ./obj/ ./Makefile $(REMOTE):$(REMOTEPWD)
# Builds the app
$(APPNAME): $(OBJ)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
# Creates the dependecy rules
%.d: $(SRCDIR)/%$(EXT)
@$(CPP) $(CFLAGS) $(CPPFLAGS) $< -MM -MT $(@:%.d=$(OBJDIR)/%.o) >$@
# Includes all .h files
-include $(DEP)
# Building rule for .o files and its .c/.cpp in combination with all .h
$(OBJDIR)/%.o: $(SRCDIR)/%$(EXT)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $<
# Cleans complete project
.PHONY: clean
clean:
-$(RM) $(OBJ) $(DEP) $(APPNAME)
# Cleans only all files with the extension .d
.PHONY: cleandep
cleandep:
-$(RM) $(DEP)
W powyższym pliku cel ‘copysrc
’ odpowiada za skopiowanie na system target
struktury katalogów i plików niezbędnych do zbudowania aplikacji.
Plikiem konfiguracyjnym mówiącym Visual Studio Code
jak należy dany projekt budować, jest plik ‘.vscode/tasks.json
’.
Każdy projekt może mieć kilka zadań, do których odwołujemy się przez nazwę nadaną w polu ‘label
’. Zadania budowania konfiguruje się w menu ‘Terminal
’.
Konfiguracja zadania, które kopiuje pliki źródłowe na system target
, a następnie zdalnie wykonuje na nim polecenie ‘make
’ wygląda następująco (plik ‘.vscode/tasks.json
’):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"version": "2.0.0",
"tasks": [
{
"label": "Native Build on Target",
"type": "shell",
"command": "make copysrc ; ssh root@opi.usb '(cd /root/RemotePowerButton; make)'",
"group": {
"kind": "build",
"isDefault": true
},
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"detail": "This task copies project sources to target and then it builds them natively there."
}
]
}
Przyjęto, że zdalnym katalogiem projektu jest ‘/root/RemotePowerButton
’. Procedurę budowania aktywuje się skrótem klawiszowym ‘Ctrl+Shift+B
’.
Edytor Visual Studio Code
jest wyposażony w mechanizm IntelliSense
bardzo ułatwiający pisanie kodu źródłowego poprzez zastosowanie zaawansowanego mechanizmu podpowiedzi nt. zdefiniowanych zmiennych, dostępnych funkcji, akceptowanych przez nie argumentów itp.
IntelliSense
czerpie swoją wiedzę z analizy treści plików nagłówkowych.
W przedstawionej tutaj konfiguracji problem polega na tym, że IntelliSense
korzysta z plików nagłówkowych w systemie host
, które mogą być zupełnie odmienne od odpowiednich plików w systemie target
.
Aby tę niefortunny stan rzeczy naprawić, należy pliki nagłówkowe z systemu target
udostępnić IntelliSense
.
Procedura jest dwuetapowa:
-
pliki nagłówkowe z systemu
target
, muszą być widoczne w systemiehost
, -
IntelliSense
musi być poinformowane, gdzie się te pliki znajdują.
Punkt 1. można zrealizować na dwa sposoby:
-
metodą “brutalnej siły” polegającej na skopiowaniu rzeczonych plików nagłówkowych na system
host
(przeniesienie atrybutu właściciela pliku nie jest konieczne),1 2 3
# w katalogu projektu na systemie host mkdir -p rootfs/usr/include scp -r root@opi.usb:/usr/include rootfs/usr/include/
-
metodą “elegancką” polegającą na udostępnieniu w trybie
tylko do odczytu
odpowiedniego fragmentu systemu plików systemutarget
z wykorzystaniem protokołu NFS.
Metoda “brutalnej siły” ma tę wadę, że przynajmniej co do zasady, powinna być powtórzona po każdorazowej aktualizacji oprogramowania na systemie target
.
Metodę elegancką opisano w punkcie “Kruczki i sztuczki”.
Następnie, korzystając z pliku ‘.vscode/c_cpp_properties.json
’ należy poinformować IntelliSense
, które pliki powinny być analizowane
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"configurations": [
{
"name": "Target Rootfs",
"includePath": [
"${workspaceFolder}/src/``",
"${workspaceFolder}/rootfs/``"
],
"defines": [ // symbols resolved by the preprocessor that are unknown to IntelliSense
"NULL=0" // I suspect that "# define NULL 0" in types.h is accepted by cpp, but IGNORED by IntelliSense
],
"compilerPath": "",
"cStandard": "c11",
"cppStandard": "c++11",
"intelliSenseMode": "linux-gcc-arm64"
}
],
"version": 4
}
Zagadnienie udostępnienia właściwych plików nagłówkowych będzie się przewijało we wszystkich opisywanych dalej konfiguracjach. W wersji korzystającej z plików udostępnionych za pomocą NFS katalog ‘rootfs
’ może być po prostu linkiem symbolicznym do katalogu, w którym widoczne są pliki urządzenia target
.
2. Kompilacja skrośna
Proces budowania aplikacji złożony jest dwóch zasadniczych etapów:
-
kompilacji
, czyli przekształcenia kodu źródłowego na kod binarny dla docelowej architektury, -
łączenia
(ang.liniking
), czyli zebrania wielu fragmentów kodu binarnego w jedną aplikację.
Kompilacji podlega kod źródłowy stworzony przez twórcę oprogramowania wraz z kodem zawartym w plikach nagłówkowych bibliotek.
Łączeniu podlega kod binarny wyprodukowany w procesie kompilacji z kodem binarnym bibliotek. Kompilator czerpie z plików nagłówkowych informację o kodzie, który będzie dostępny na etapie łączenia, zatem oba te elementy muszą pozostawać w pełnym synchronizmie.
Sam proces łączenia ma dwie odmiany.
W łączeniu statycznym
wymuszanym na linkerze opcją -static
kod binarny aplikacji oraz wspomagający go kod z bibliotek są ‘sklejane’ w jeden plik.
Tak zbudowana aplikacja jest samowystarczalna, jednak jej kod wynikowy może być bardzo duży (np. użycie funkcji printf
implikuje dołączenie znacznego fragmentu standardowej biblioteki C).
Bez opcji -static
linker pracuje w trybie łączenia dynamicznego
, w którym kod binarny aplikacji jest uzupełniany informacją o nazwie i wersji biblioteki
, o którą powinien być uzupełniony w momencie uruchomienia. Dzięki takiemu podejściu kod wynikowy aplikacji jest stosunkowo mały, a biblioteki wykorzystywane przez wiele aplikacji są ładowane do pamięci tylko raz. Niestety, w momencie uruchomienia aplikacja nie jest samodzielna jak w przypadku łączenia statycznego, gdyż system, w którym jest uruchamiana, musi być uzupełniony o biblioteki współdzielone niezbędne do działania aplikacji (z dokładnością do wersji biblioteki, aby zapewnić zgodność interfejsu biblioteki zakładanego podczas kompilacji).
Ostatecznie budowanie skrośne wymaga spełnienia następujących warunków:
-
kompilator musi wytwarzać kod binarny zgodny z architekturą urządzenia
target
, -
przetwarzane przez kompilator pliki nagłówkowe muszą być zgodne z kodem binarnym łączonym przez linker,
-
na etapie uruchamiania, dla programów łączonych dynamicznie, w systemie
target
muszą być zainstalowanedokładnie te same
biblioteki, które były przyjęte na etapie łączenia. Mówiąc krótko, architektura kodu, deklaracje zawarte w kodach nagłówkowych i biblioteki podczas łączenia i uruchamiania aplikacjimuszą
do siebie pasować.
Podstawowym problemem skrośnego tworzenia oprogramowania jest konstrukcja środowiska gwarantującego spełnienie warunków 1-3.
Ich poprawne spełnienie wymaga istotnej ingerencji w oprogramowanie zainstalowane na systemie host
.
Zestaw narzędzi umożliwiających przygotowanie kodu binarnego na architekturę inną niż architektura systemu na którym wykonywania jest kompilacja nazywa się toolchain
‘em.
Nazwa ta podkreśla fakt, że skrośne budowanie aplikacji wymaga zharmonizowanego użycia wielu narzędzi.
Skrośne budowanie aplikacji jest żmudne i podatne na błędy
2.1 Instalacja kompilatora skrośnego w systemie host
Budowanie skrośne jest stosunkowo proste, gdy dla systemu host
istnieje gotowy pakiet zawierający zestaw narzędzi do skrośnego budowania aplikacji (tzw. toolchain
) oraz podstawowe biblioteki dla architektury docelowej
1
apt list "crossbuild*"
Jego instalacja umożliwia bezproblemową kompilację prostych aplikacji. Problem koegzystencji został rozwiązany poprzez zastosowanie konwencji nazewniczej polegającej na poprzedzeniu każdego programu wchodzącego w skład toolchainu prefiksem opisującym platformę docelową w formacie (łącznik na końcu prefiksu jest jego integralną częścią)
<arch>-<os>-<lib>-
<arch>-<vendor>-<os>-<lib>-
Na przykład typowymi prefiksami dla architektury ARM64 są aarch64-linux-gnu-
lub aarch64-unknown-linux-gnu-
Łączenie statyczne
Szansa, że wersje bibliotek zainstalowanych w ‘crossbuild
’ pokrywają się z bibliotekami w środowisku uruchomiania, jest niewielka, zatem konieczne jest uwolnienie się od warunku 3, poprzez wymuszenie łączenia statycznego np.
1
PREFIX=aarch64-linux-gnu- ; ${PREFIX}gcc -c -g -Wall -o main.o main.c ; ${PREFIX}gcc -static main.o
Kompilator i linker skrośny ‘wiedzą’, gdzie w systemie host
zainstalowano pliki stosowne dla architektury docelowej, co znacznie upraszcza składnię komend niezbędnych do skutecznego zbudowania aplikacji.
Aplikacja zbudowana jak wyżej może być uruchomiona na dowolnym systemie zgodnym z architekturą zdefiniowaną w zmiennej PREFIX
.
Jednak za prostotę płacimy cenę: po pierwsze, aplikacja jest stosunkowo duża i po drugie, uzupełnienie zależności o bibliotekę, której brak w pakiecie ‘crossbuild`’ wymaga ręcznej kompilacji skrośnej biblioteki i ujęcia jej `explicite* w wywołaniach kompilatora i linkera.
Łączenie dynamiczne
Budowanie skrośne wykorzystujące łączenie dynamiczne jest bardziej złożone, gdyż na systemie host
muszą znajdować te same
biblioteki dynamiczne/współdzielone co na systemie target
.
Spełnienie punktu 3 implikuje również instalację plików nagłówkowych skojarzonych z tymi bibliotekami.
Katalog zawierający te elementy jest nazywany sysroot
. Zarówno kompilator skrośny, jak i linker skrośny muszą być poinformowane o jego położeniu. Zawartość sysroot
ekstrahuje się z głównego systemu plików systemu target
.
Wymóg utrzymania synchronizmu wymusza aktualizację jego kopii na systemie host
po zmianie firmware na urządzeniu target
.
1
PREFIX=aarch64-linux-gnu- ; SYS=rootfs ;${PREFIX}gcc --sysroot ${SYS} -c -g -Wall -o main.o main.c ; ${PREFIX}gcc --sysroot ${SYS} main.o
Wartość zmiennej PREFIX
jest używana do rozmieszczania plików w drzewie katalogów na etapie przygotowywania rootfs
.
Jednocześnie jest ona wykorzystywana przez kompilator skrośny do poszukiwania plików.
Zatem łączenie dynamiczne aby prefiks kompilatora był taki sam jak dla rootfs
.
Spełnienie tego warunku jest konieczne do bezproblemowej kompilacji i łączenia w trybie dynamicznym
Inną, bardziej elegancką metodą zapewnienia widoczności plików z urządzenia target
jest ich udostępnienie z wykorzystaniem protokołu NFS.
Poniżej przedstawiono definicje zadań kompilacji statycznej i dynamicznej w Visual Studio Code
.
Dla kompilacji dynamicznej przyjęto założenie, że system plików urządzenia target
jest widoczny w katalogu ‘rootfs/
’.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"version": "2.0.0",
"tasks": [
{
"label": "CrossBuild with static linkage",
"type": "shell",
"command": "make PREFIX=aarch64-linux-gnu- STATIC=-static",
"group": {
//"isDefault": true,
"kind": "build"
},
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"detail": "This task creates static application produced with cross compiler installed on host."
},
{
"label": "CrossBuild with dynamic linkage",
"type": "shell",
"command": "make PREFIX=aarch64-linux-gnu- SYSROOT=rootfs/",
"group": {
//"isDefault": true,
"kind": "build"
},
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"detail": "This task creates dynamically linked application produced with cross compiler installed on host. Target's rootfs is visible in rootfs/ folder."
}
]
}
2.2 Konteneryzacja środowiska kompilacji skrośnej
Instalowanie w systemie host
kilku środowisk skrośnych prowadzi do dość sporego zamieszania w organizacji plików, zwłaszcza jeżeli narzędzia te pochodzą z równych źródeł.
Atrakcyjną alternatywą jest zamknięcie tych środowisk w kontenerach Docker
‘a.
Zaletą jest o wiele łatwiejsze zarządzanie i całkowity brak interferencji pomiędzy tak zorganizowanymi środowiskami.
W ramach projektu Dockcross
zamknięto w kontenerach narzędzia kompilacji na wiele platform docelowych.
Ich zawartość obejmuje narzędzia do budowania aplikacji oraz standardową bibliotekę C (zrealizowaną jako GNU C
, musl
lub uClib
).
Korzystanie z kontenerów jest wyjątkowo proste: instalacja sprowadza się do jednej komendy, a budowanie aplikacji wymagaja kosmetycznego retuszu pliku Makefile
.
Dostępne typy architektur można sprawdzić w repozytorium GitHub
projektu https://github.com/dockcross/dockcross lub na https://hub.docker.com/.
Instalację środowiska realizuje komenda
1
2
docker run --rm dockcross/arch-name > ./dockcross
chmod +x ./dockcross
W jej wyniku na system host
zostanie pobrany odpowiedni kontener, a w katalogu projektu utworzony skrypt dockcross
umożliwiający korzystanie z jego zawartości.
Jeżeli kontener ma być używany również w innych projektach to skrypt należy przenieść do katalogu znajdującego się na ścieżce dostępu i zmienić jego nazwę na taką, która informuje o nazwie architektury.
Parametry linii komend tego skryptu traktowane są jak linia poleceń, którą należy przekazać do kontenera.
Jednocześnie bieżący katalog jest mapowany do wnętrza kontenera, co powoduje, że pliki projektu są dostępne dla kompilatora w katalogu roboczym kontenera.
Na przykład, aby sprawdzić jak nazywa się kompilator umieszczony w kontenerze wystarczy wydać komendę
1
./dockcross bash -c 'echo $CC'
Powyższa komenda pozwala na ustalenie wartości zmiennej PREFIX
używanej we wcześniejszych przykładach.
Na przykład dla kontenera dockcross/linux-arm64
przyjmie ona wartość aarch64-unknown-linux-gnu-
.
W rezultacie komenda realizująca kompilację skrośną wymaga prostego uzupełnienia o wywołanie skryptu
1
./dockcross bash -c 'PREFIX=aarch64-unknown-linux-gnu- ; ${PREFIX}gcc -c -g -Wall -o main.o main.c ; ${PREFIX}gcc -static main.o -o myapp'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"version": "2.0.0",
"tasks": [
{
"label": "Containerized CrossBuild with static linkage",
"type": "shell",
"command": "./dockcross bash -c 'make PREFIX=aarch64-unknown-linux-gnu- STATIC=-static'",
"group": {
//"isDefault": true,
"kind": "build"
},
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"detail": "This task creates static application produced with cross compiler installed dockcross container (https://github.com/dockcross/dockcross)."
}
]
}
Niestety, z dużym prawdopodobieństwem kompilator zawarty w kontenerze będzie niekompatybilny z kompilatorem użytym do wytworzenia rootfs
. W efekcie łączenie w trybie dynamicznym nie będzie możliwe.
3. Debugowanie skrośne
Natywne debugowanie kodu z wykorzystaniem GUI pracującego na systemie target
jest często awykonalne, głównie ze względu na ograniczony interfejs użytkownika i niezbyt duże zasoby urządzenia.
Z kolei zdalna praca w konsoli tekstowej z debuggerem gdb
jest doznaniem ekstremalnym.
Rozwiązaniem problemu jest podzielenie zadania uruchamiania na dwa oddzielne procesy. Proces gdbserver
pracuje na systemie target
i odpowiada za krokowe wykonywanie uruchamianej aplikacji.
Z kolei interfejs użytkownika uruchomiony na systemie host
komunikuje się z serwerem poprzez sieć lub łącze szeregowe (w dalszej części pojawią się konfiguracje dla łącza sieciowego).
Programem uruchomionym na systemie host
jest gdb-multiarch
(jest to wersja gdb
, która “rozumie” różne architektury). Oba komponenty komunikują się za pomocą protokołu TCP/IP.
W efekcie struktura komunikacji pomiędzy narzędziami jest następująca: interfejs GUI uruchomiony w systemie host
komunikuje się lokalnie z gdb-multiarch
, który z kolei poprzez sieć komunikuje się z gdbserver
uruchomionym na systemie target
.
GUI odpowiedzialne za przeprowadzenie sesji uruchamiania musi poinformować gdb-multiarch
o architekturze uruchamianego kodu i adresie serwera oczekującego na połączenie.
W Visual Studio Code
za konfigurację procesu uruchamiania odpowiedzialny jest plik ‘.vscode/launch.json
’.
Dla rozważanego projektu przyjmie on postać
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"version": "0.2.0",
"configurations": [
{
"name": "GDB-REMOTE",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/main",
"miDebuggerServerAddress": "opi.usb:1234",
"miDebuggerPath": "/usr/bin/gdb-multiarch",
"targetArchitecture": "arm64",
"args": [],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "sets target architecture",
"text": "-ex 'set architecture aarch64'",
"ignoreFailures": true
},
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
Sesję uruchamiania aktywuje się skrótem “F5
” lub wybierając ikonę “żuczka” na pasku bocznym.
Powyższe zadanie realizuje tylko komunikację lokalną z
gdb-multiarch
, tak więc każdorazowo przed rozpoczęciem sesji uruchamiania
należy w terminalu Visual Studio Code
wydać komendę
1
scp ./main root@opi.usb: && ssh -t root@opi.usb "gdbserver --once localhost:1234 ./main"
Zadanie można sobie znacznie ułatwić, dopisując odpowiednie cele do ‘Makefile
’
debug: $(APPNAME)
scp $(APPNAME) $(REMOTE): && ssh -t $(REMOTE) "gdbserver --once localhost:1234 ./$(APPNAME)"
run: $(APPNAME)
scp $(APPNAME) $(REMOTE): && ssh -t $(REMOTE) "./$(APPNAME)"
Podsumowanie
Kompilacja skrośna własnego kodu nastręcza wielu problemów.
Jeszcze więcej problemów pojawia się podczas kompilacji cudzych aplikacji.
Profesjonalnie napisane aplikacje są wyposażone w mechanizm budowy dostosowujący je do środowiska kompilacji.
Zazwyczaj budowanie aplikacji poprzedzonej jest uruchomieniem skryptu ./configure
wygenerowanym przez środowisko automake
.
Zadaniem skryptu jest wykrycie dostępności pewnych składników systemie, ustawienie odpowiednich flag kompilacji i przygotowanie pliku Makefile
dla programu make
.
Schemat ten działa poprawnie gdy system na którym ma być wykonywania aplikacja jest identycznie skonfigurowany (w sensie dostępności wykrywanych składników) z systemem na którym wykonywana jest aplikacja.
W przypadku kompilacji skrośnej założenie to nie jest spełnione.
Skrypt ./configure
po prostu odpytuje niewłaściwy system
.
W efekcie ustawione opcje kompilacji są niepoprawne, co skutkuje błędami kompilacji, lub co gorsza, wygenerowaniem niewłaściwie działającego kodu.
Ręczny dobór odpowiednich przełączników i adaptacja obcego kodu źródłowego jest zazwyczaj doznaniem ekstremalnym.
W zasadzie wszystkie opisane problemy można rozwiązać stosując [kompilację w trybie emulowanym](../../post/2023-08-13-emulated-building-for-sbc/).
Wady i zalety
Budowanie natywne
zalety | wady |
---|---|
możliwość tworzenie kodu łączonego dynamicznie | konieczność instalacji narzędzi deweloperskich na urządzeniu target
|
tworzenie kodu wymaga bezpośredniego dostepu do urządzenia docelowego |
Budowanie skrośne
zalety | wady |
---|---|
kompilacja na urządzeniu host | kod łączony dynamicznie może być przygotowany tylko w specyficznych sytuacjach |
możliwość hermetyzacji środowiska w kontenerze |
Przydatne pliki konfiguracyjne
Makefile
na wszystkie okazje
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# Compiler settings - Can be customized.
PREFIX=
STATIC=
CC = $(PREFIX)gcc
CXX =$(PREFIX)g++
CPPFLAGS =
CFLAGS = -std=c11 -g -Wall -fdiagnostics-color=always
CXXFLAGS = -std=c++11 -g -Wall -fdiagnostics-color=always
# LDFLAGS = $(STATIC) -lpthread -lgpiod
LDFLAGS = $(STATIC)
ifdef SYSROOT
CFLAGS += --sysroot=$(SYSROOT)
CXXFLAGS += --sysroot=$(SYSROOT)
LDLAGS += --sysroot=$(SYSROOT)
endif
# Makefile settings - Can be customized.
APPNAME = main
EXT = .c
SRCDIR = src
OBJDIR = obj
REMOTE = root@opi.usb
REMOTEPWD = /root/RemotePowerButton
SRC = $(wildcard $(SRCDIR)/*$(EXT))
OBJ = $(SRC:$(SRCDIR)/%$(EXT)=$(OBJDIR)/%.o)
DEP = $(OBJ:$(OBJDIR)/%.o=%.d)
RM = rm -f
all: $(APPNAME)
# Copy sources to target
.PHONY: copysrc
copysrc:
scp -p -r ./src/ ./obj/ ./Makefile $(REMOTE):$(REMOTEPWD)
.PHONY: debug
debug: $(APPNAME)
scp $(APPNAME) $(REMOTE): && ssh -t $(REMOTE) "gdbserver --once localhost:1234 ./$(APPNAME)"
.PHONY: run
run: $(APPNAME)
scp $(APPNAME) $(REMOTE): && ssh -t $(REMOTE) "./$(APPNAME)"
# Builds the app
$(APPNAME): $(OBJ)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
# Creates the dependecy rules
%.d: $(SRCDIR)/%$(EXT)
@$(CPP) $(CFLAGS) $(CPPFLAGS) $< -MM -MT $(@:%.d=$(OBJDIR)/%.o) >$@
# Includes all .h files
-include $(DEP)
# Building rule for .o files and its .c/.cpp in combination with all .h
$(OBJDIR)/%.o: $(SRCDIR)/%$(EXT)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ -c $<
# Cleans complete project
.PHONY: clean
clean:
$(RM) $(OBJ) $(DEP) $(APPNAME)
# Cleans only all files with the extension .d
.PHONY: cleandep
cleandep:
$(RM) $(DEP)
Wszystkie definicje zadań ``.vscode/tasks.json`’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
"version": "2.0.0",
"tasks": [
{
"label": "CrossBuild with static linkage",
"type": "shell",
"command": "make PREFIX=aarch64-linux-gnu- STATIC=-static",
"group": {
//"isDefault": true,
"kind": "build"
},
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"detail": "This task creates static application produced with cross compiler installed on host."
},
{
"label": "CrossBuild with dynamic linkage",
"type": "shell",
"command": "make PREFIX=aarch64-linux-gnu- SYSROOT=/mnt/sysroot",
"group": {
//"isDefault": true,
"kind": "build"
},
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"detail": "This task creates dynamically linked application produced with cross compiler installed on host. Targets rootfs is visible in /mnt/sysroot."
},
{
"label": "Build Natively on Target",
"type": "shell",
"command": "make copysrc ; ssh root@opi.usb '(cd /root/RemotePowerButton; make)'",
"group": {
//"isDefault": true,
"kind": "build"
},
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"detail": "This task copies project sources to target and then it builds them natively there."
}
]
}
Konfiguracja IntelliSense
w ‘.vscode/c_cpp_properties.json
’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"configurations": [
{
"name": "NativeOverSSH",
"includePath": [
"${workspaceFolder}/src/``",
"${workspaceFolder}/rootfs/``"
],
"defines": [ // symbols resolved by the preprocessor that are unknown to IntelliSense
"NULL=0" // I suspect that "# define NULL 0" in types.h is accepted by cpp, but IGNORED by IntelliSense
],
"compilerPath": "",
"cStandard": "c11",
"cppStandard": "c++11",
"intelliSenseMode": "linux-gcc-arm64"
}
],
"version": 4
}
Konfiguracja sesji uruchamiania w ‘.vscode/launch.json
’
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"version": "0.2.0",
"configurations": [
{
"name": "GDB-REMOTE",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/main",
"miDebuggerServerAddress": "opi.usb:1234",
"miDebuggerPath": "/usr/bin/gdb-multiarch",
"targetArchitecture": "arm64",
"args": [],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "sets target architecture",
"text": "-ex 'set architecture aarch64'",
"ignoreFailures": true
},
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
4. Kruczki i sztuczki
4.1 Zdalny dostęp do konta administratora bez podawania hasła
-
Zdalny dostęp do konta administratora
W pliku
/etc/ssh/sshd_config
’ (target
) należy ustawić opcjęPermitRootLogin yes
-
Logowanie bez konieczności podawania hasła
-
Serwer
SSH
musi zezwalać na uwierzytelnienie za pomocą kluczy publicznych. W pliku/etc/ssh/sshd_config
’ (target
) należy ustawić opcjePubkeyAuthentication yes AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2
-
Użytkownik systemu
host
musi mieć wygenerowane osobiste kluczeSSH
. Generacja komendąssh-keygen
Opcją ‘-t’ można wymusić typ klucza, a opcją ‘-b’ jego długość. Wygenerowane klucze, prywatny i publiczny, umieszczone są w podkatalogu ‘.ssh’ katalogu domowego (publiczny ma rozszerzenie ‘pub’).
-
Klucz publiczny należy umieścić na systemie
target
w pliku ‘.ssh/authorized_keys
’ na koncie docelowym. Wygodne kopiowanie zapewnia komenda (poniżej użyto kontaroot
)1
ssh-copy-id root@opi.usb
-
4.2 Udostępnianie głównego systemu plików poprzez NFS
System target
-
Instalacja serwera NFS.
1
apt install -y nfs-kernel-server
-
Udostępnienie katalogu ‘/usr’ w trybie tylko do odczytu
Zatrzymanie serwera
1
systemctl stop nfs-kernel-server
Aktualizacja ‘/etc/exports’
1
/usr *(ro,sync,no_subtree_check)
Uruchomienie serwera
1 2
exportsfs -ra systemctl start nfs-kernel-server
System host
-
Utworzenie punktu montowania
1
mkdir -p /mnt/sysroot/usr
-
Montowanie zdalnego folderu na żądanie. W ‘/etc/fstab’
1
opi.usb:/usr /mnt/sysroot/usr nfs noauto,nofail,noatime,nolock 0 0
Montowanie komendą
1
sudo mount /mnt/sysroot/usr
Kompilator należy poinformować, że katalogiem ‘
sysroot
’ jest ‘/mnt/sysroot
’.
P.S. Chętnie się dowiem jak prosto udostępnić "/" w którym są linki symboliczne np.
bin->/usr/bin``.