Egy C-program teljesítménykritikus részét külön assembly fájlban szeretnénk megírni, de a tesztelést, a kiírást és az általános programlogikát C-ben hagynánk. Fordított irányra is szükség lehet: az assembly rutin egy már meglévő C-függvényt használjon.
A két nyelv közötti határ nem automatikus típusellenőrzési pont. A C-fordító a deklarációból következtet a hívás módjára; az assembly szerzője pedig kézzel valósítja meg ugyanazt a szerződést. A linkelő elsősorban szimbólumokat és címhivatkozásokat kapcsol össze, nem C-függvényszignatúrákat ellenőriz.
Definíció — bináris interfész: az alkalmazás bináris interfésze (Application Binary Interface, ABI) meghatározza többek között az argumentumok, visszatérési értékek, regisztermegőrzések és veremhasználat szabályait. A labor AArch64 Linux környezetében a függvényhívási szerződést az AAPCS64 adja.
Az Inline assembly GCC-ben esetében operandusokkal és clobber-listával tájékoztattuk a fordítót. Önálló assembly függvénynél ezt a szerepet a C-prototípus és az ABI tölti be. A keretkezeléshez a Prológus és epilógus: regisztermentés és keretkezelés ismeretére támaszkodunk.
A teljes alfejezet profilja: I. rész, A64 ISA, ARMv8-A, AArch64 Linux, little-endian, LP64 adatmodell. Az assembly blokkok utasításai és regiszterei ebben a profilban értendők; direktíváik GNU assembler, AArch64 ELF környezethez tartoznak. Thumb-2 kód itt nem szerepel.
A példában kizárólag uint64_t argumentumokat és
visszatérési értékeket használunk. Ezek előjel nélküli, 64 bites
egészek, így az összeadás és szorzás eredménye modulo
értendő.
| A labor C-interfészének eleme | AAPCS64-leképezés, A64 |
|---|---|
Első uint64_t argumentum |
X0 |
Második uint64_t argumentum |
X1 |
Harmadik uint64_t argumentum |
X2 |
uint64_t visszatérési érték |
X0 |
| A hívásokon át megtartandó helyi érték | Például X19, belépési értékének mentésével |
| Visszatérési cím | X30; újabb BL felülírja |
| Veremmutató | SP; a példában végig 16 bájtra igazított |
Ez nem általános szabály minden C-típusra. A lebegőpontos értékeknek
és az összetett típusoknak külön besorolási szabályaik vannak;
utóbbiakat a Több visszatérési érték és
struktúra-visszaadás szerződése tárgyalja. A jelenlegi skaláris
egész argumentumosztályban az első nyolc argumentum számára az A64
X0–X7 használható, a további argumentumok
veremre kerülnek.
A szimbólumkapcsolat két oldala:
extern deklaráció megadja a nevet és a típust,
de nem szolgáltat függvénytörzset. Függvénydeklarációnál az
extern elhagyható; itt szándékosan kiírjuk..global direktíva a szimbólumot
más objektumfájlok számára láthatóvá teszi..type …, %function és
.size függvénymetaadatot ad az eszközöknek. Ezek
nem kódolják a teljes C-prototípust.Ezért egy sikeres linkelés még nem bizonyítja az interfész helyességét.
Munkakönyvtár: ~/arm-labor/a64/a037/ —
az alfejezet minden laborfájlja ide kerül, és minden parancs innen
futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le).
mkdir -p ~/arm-labor/a64/a037
cd ~/arm-labor/a64/a037Négy forrásfájlt és egy Makefile-t készítünk:
lab-interop.h: közös C-deklarációk;lab-main.c: tesztprogram;lab-helper.c: assemblyből hívott C-függvény;lab-bridge.s: C-ből hívott assembly függvények;lab-Makefile: reprodukálható build.A számítás:
Minden változó dimenzió nélküli, előjel nélküli 64 bites egész. A szorzást és az egy hozzáadását a C-segédfüggvény végzi.
A lab-interop.h tartalma:
#ifndef LAB_INTEROP_H
#define LAB_INTEROP_H
#include <stdint.h>
extern uint64_t lab_add(uint64_t a, uint64_t b);
extern uint64_t lab_apply(uint64_t a, uint64_t b, uint64_t bonus);
extern uint64_t lab_scale(uint64_t value);
#endifA lab-helper.c tartalma:
#include "lab-interop.h"
uint64_t lab_scale(uint64_t value)
{
return UINT64_C(3) * value + UINT64_C(1);
}A lab-main.c tartalma:
#include <inttypes.h>
#include <stdio.h>
#include "lab-interop.h"
int main(void)
{
const uint64_t sum = lab_add(7, 5);
const uint64_t result = lab_apply(7, 5, 9);
const uint64_t wrap = lab_add(UINT64_MAX, 1);
printf("sum=%" PRIu64 " apply=%" PRIu64 " wrap=%" PRIu64 "\n",
sum, result, wrap);
return (sum == UINT64_C(12) &&
result == UINT64_C(46) &&
wrap == UINT64_C(0)) ? 0 : 1;
}A PRIu64 a uint64_t típushoz illeszkedő
kiírást biztosítja. A közös fejléc a C-fájlok közötti eltéréseket
fordításkor segít észlelni; az assembly implementációt továbbra is külön
kell auditálni.
A lab-bridge.s tartalma:
// A64 ISA, ARMv8-A; GNU as, AArch64 ELF, little-endian.
.arch armv8-a
.text
.p2align 2
.global lab_add
.type lab_add, %function
lab_add:
.cfi_startproc
add x0, x0, x1
ret
.cfi_endproc
.size lab_add, .-lab_add
.p2align 2
.global lab_apply
.type lab_apply, %function
lab_apply:
.cfi_startproc
stp x29, x30, [sp, #-32]!
.cfi_def_cfa_offset 32
.cfi_offset 29, -32
.cfi_offset 30, -24
mov x29, sp
str x19, [sp, #16]
.cfi_offset 19, -16
mov x19, x2
add x0, x0, x1
bl lab_scale
add x0, x0, x19
ldr x19, [sp, #16]
.cfi_restore 19
ldp x29, x30, [sp], #32
.cfi_restore 29
.cfi_restore 30
.cfi_def_cfa_offset 0
ret
.cfi_endproc
.size lab_apply, .-lab_apply
.section .note.GNU-stack,"",%progbitsA lab_add levélfüggvény: nem hív másik függvényt, és nem
módosít megőrzendő regisztert. Nincs szüksége saját veremkeretre.
A lab_apply nem-levél függvény. A harmadik argumentumot
nem hagyhatja egyszerűen az A64 X2 regiszterben: a
meghívott C-függvény ezt szabadon felhasználhatja. Ez akkor is igaz, ha
a C-függvénynek csak egy argumentuma van.
Az A64 X19 viszont megőrzendő regiszter. A
lab_apply ezért:
X19 értékét;X19 regiszterbe
helyezi;X19
tartalmat.Az A64 X30 mentése ettől függetlenül szükséges: az A64
BL a C-segédfüggvényből való visszatérés címét írja
bele.
A 32 bájtos keret kiosztása:
Eltolás az új A64 SP-hez
képest |
Tartalom | Méret |
|---|---|---|
| 0 | Belépési A64 X29 |
8 bájt |
| 8 | Belépési A64 X30 |
8 bájt |
| 16 | Belépési A64 X19 |
8 bájt |
| 24 | Nem használt kitöltés | 8 bájt |
A kitöltés biztosítja, hogy a keret mérete 16 bájt többszöröse legyen.
A két függvény C-ből hívható, végleges minta, ezért a Prológus és epilógus szabálya szerint CFI-t kap: a
levélfüggvény lab_add csak a
.cfi_startproc/.cfi_endproc párt, a keretet
építő lab_apply a mentések helyét is. A
.cfi_offset értékei a fenti kerettáblázatból következnek: a
CFA a belépési SP, vagyis az új SP + 32.
Kódolási korlátok: a
STP/LDP ±32, a
STR/LDR 16 bájtos eltolása és a
BL cél mind kódolható (lásd az A64
immediate-korlátok összefoglalóját); a BL végleges
eltolása linkeléskor áll elő. Az AArch64 ELF/GNU as
.p2align 2 négybájtos igazítást jelent, nem két bájtot.
Az AArch64 ELF/GNU as .note.GNU-stack szekció nem
végrehajtható veremigényt jelez a linkelőnek; nem futásidejű
utasítás.
Az önálló fordítási egységek (translation units) külön
objektumfájlokká alakulnak. A C-fájlokat GCC-vel fordítjuk; a kisbetűs
.s fájlt közvetlenül az assemblerrel dolgozzuk fel.
A végső linkelést is a GCC meghajtóprogramra bízzuk. Ez elindítja a
linkelőt, és hozzáadja a C-futási környezethez szükséges
indítóobjektumokat és könyvtárakat. A puszta ld meghívás
nem tenné meg mindezt automatikusan. A program belépési környezetét most
a C-runtime szolgáltatja: nem írunk saját _start
rutint.
A lab-Makefile tartalma; a receptsorok elején valódi
tabulátor álljon:
# Target: AArch64 Linux, ARMv8-A, little-endian, qemu-user.
CC := aarch64-linux-gnu-gcc
AS := aarch64-linux-gnu-as
CFLAGS := -std=c11 -O2 -g -Wall -Wextra -Wpedantic -march=armv8-a
ASFLAGS := -g -march=armv8-a
LDFLAGS := -static
.PHONY: lab-all lab-run lab-clean
lab-all: lab-interop
lab-interop: lab-main.o lab-helper.o lab-bridge.o
$(CC) $(LDFLAGS) -o $@ $^
lab-%.o: lab-%.c lab-interop.h
$(CC) $(CFLAGS) -c $< -o $@
lab-bridge.o: lab-bridge.s
$(AS) $(ASFLAGS) -o $@ $<
lab-run: lab-interop
qemu-aarch64 ./lab-interop
lab-clean:
rm -f lab-main.o lab-helper.o lab-bridge.o lab-interopA statikus linkelés miatt futtatáskor nincs szükség külön AArch64 dinamikus betöltő megadására. A buildhez viszont rendelkezésre kell állniuk az AArch64 C-könyvtár statikus állományainak és indítóobjektumainak. Az ezek hiányára utaló linkelési hiba környezeti probléma, nem ABI-hiba.
Target: qemu-user; host: Ubuntu 24.04 x86_64. Összeállítás, linkelés és futtatás:
make -f lab-Makefile lab-all
make -f lab-Makefile lab-run
printf 'make-status=%d\n' "$?"Várt, nem itt mért eredmény: a build kilépési
állapota 0; a program kiírása sum=12 apply=46 wrap=0; a
program és a lab-run cél kilépési állapota 0. A
make ezenfelül a végrehajtott parancsokat is kiírhatja.
A számítás ellenőrzése:
| Megfigyelési pont | Várt állapot, A64 |
|---|---|
lab_apply belépése |
X0=7, X1=5, X2=9 |
| Közvetlenül a C-hívás előtt | X0=12, X19=9 |
| A C-függvény visszatérése után | X0=37, X19=9 |
lab_apply visszatérésekor |
X0=46; SP, X19,
X29 belépési értéke visszaállt |
A fenti táblázat utolsó sora állítás, amelyet a
sum=12 apply=46 kimenet nem igazol: egy X19-et
rontó lab_apply is kiírhatná a helyes összeget. Ezt a Prológus és epilógus közös teszthívójával
ellenőrizzük, amely magas bites mintákkal a teljes megőrzött
regiszterkészletet és az SP-t vizsgálja. A teszthívó
fájljait átmásoljuk:
cp ~/arm-labor/a64/a029/lab-abi-call.h ~/arm-labor/a64/a029/lab-abi-call.S .A lab-abi-main.c tartalma:
#include <inttypes.h>
#include <stdio.h>
#include "lab-abi-call.h"
#include "lab-interop.h"
int main(void)
{
uint64_t r_add = 0, r_apply = 0;
uint64_t m_add = lab_abi_call((lab_abi_fn)lab_add, 7, 5, 0, 0, 0, 0, &r_add);
uint64_t m_apply = lab_abi_call((lab_abi_fn)lab_apply, 7, 5, 9, 0, 0, 0, &r_apply);
printf("lab_add maszk=0x%05" PRIx64 " eredmeny=%" PRIu64 "\n", m_add, r_add);
printf("lab_apply maszk=0x%05" PRIx64 " eredmeny=%" PRIu64 "\n", m_apply, r_apply);
return (m_add == 0 && r_add == 12 && m_apply == 0 && r_apply == 46) ? 0 : 1;
}A (lab_abi_fn) típuskényszerítés C-ben megengedett: a
void (*)(void) típusra minden függvénymutató átalakítható,
és a -Wextra sem figyelmeztet rá. A függvényt nem C-ből
hívjuk ezen a típuson át, hanem a teszthívó BLR
utasítása.
# Target: AArch64 Linux, qemu-user.
aarch64-linux-gnu-gcc -std=c11 -O2 -Wall -Wextra -march=armv8-a -static \
lab-abi-main.c lab-helper.c lab-bridge.s lab-abi-call.S -o lab-abi-interop
qemu-aarch64 ./lab-abi-interop
printf 'kilepesi_allapot=%d\n' "$?"Várt kimenet:
lab_add maszk=0x00000 eredmeny=12
lab_apply maszk=0x00000 eredmeny=46
kilepesi_allapot=0
A nulla maszk a lab_apply esetén két szerződést igazol
egyszerre: a saját X19-mentését és a közben meghívott, GCC
által fordított lab_scale megőrzési fegyelmét.
Ellenpróbaként töröld a lab_apply
ldr x19, [sp, #16] sorát: a maszk 0x00001
lesz. Az eredeti lab-interop kimenete ekkor fordítófüggő:
ha a main a sum értékét véletlenül éppen
X19-ben tartja a lab_apply hívásán át, a
kiírás sum=9 lesz (a bonus értéke szivárog
át), különben helyesnek látszik. A teszthívó maszkja viszont a
fordítótól függetlenül jelzi a hibát.
Az áthelyezési bejegyzés (relocation) azt jelöli, hogy egy hivatkozás végleges értékét később kell meghatározni. Az assembly objektum a C-függvény végleges címét még nem ismeri.
Target: qemu-userhez készült AArch64 ELF; hostoldali vizsgálat, futtatás nélkül:
aarch64-linux-gnu-readelf -Ws lab-bridge.o
aarch64-linux-gnu-objdump -dr lab-bridge.o
aarch64-linux-gnu-objdump -d --disassemble=lab_scale lab-interopA parancsok várt kilépési állapota 0. A szimbólumtáblában
lab_add és lab_apply globális
függvénydefiníció, lab_scale pedig még nem definiált,
UND jelölésű szimbólum. A tárgykód disassemblyjében a
hívásnál R_AARCH64_CALL26 áthelyezés várható. A végleges
ELF-ben már a feloldott hívást vizsgáljuk.
| Tünet | Hipotézis és adatgyűjtés | Egy változtatás, majd ellenőrzés |
|---|---|---|
undefined reference to lab_add |
A szimbólumtábla szerint nem globális, vagy neve eltér | Javítsuk az exportot vagy a nevet; linkeljünk újra |
lab_scale nem található |
Hiányzik a megfelelő objektum a linkparancsból | Vegyük fel a lab-helper.o fájlt; építsünk újra |
| Csak nagy bemenetnél hibás az eredmény | Disassemblyben A64 W0/W1 szerepel a
szükséges X0/X1 helyett |
Javítsuk az operandusszélességet; futtassunk határértéktesztet |
| A bónusz néha eltűnik | Hívó által mentendő regiszterben maradt a C-hívás alatt | Tároljuk mentett A64 X19-ben; ismételjük a
teszteket |
| A visszatérés hibás vagy a program elakad | Hiányzik az A64 X30 mentése |
Állítsuk helyre a prológus–epilógus párt; ellenőrizzük a vezérlési utat |
Különösen veszélyes a prototípuseltérés. Ha a C-deklaráció két
argumentumot ír le, az assembly pedig harmadikat is vár, az A64
X2 tartalmára nincs argumentumként vállalt garancia. Ha a
C-oldal uint32_t, az assembly pedig uint64_t
értéket feltételez, már az argumentum-előkészítés és az eredmény
értelmezése is eltérhet.
A 32 bites érték felső regiszterbitjeire nem szabad általános, 64 bites argumentumszerződésként támaszkodni. Ugyanígy egy előjeles 32 bites érték sem válik automatikusan helyesen előjelkiterjesztett 64 bites argumentummá. A típus, a szélesség és az argumentumszám együtt az interfész része.
(magyarázó, alapszint — cél:
megértés-ellenőrzés) Miért nem garantálja a sikeres linkelés,
hogy a lab_apply három uint64_t argumentumot
kap? Miért kell megőriznie az A64 X19 belépési értékét, ha
a C-segédfüggvény is köteles megőrizni azt?
(implementációs, alapszint — cél: rutinépítés)
Adj a projekthez uint64_t lab_max(uint64_t a, uint64_t b)
assembly függvényt, amely előjel nélküli maximumot ad vissza. Ellenőrizd
a 7 és 5, valamint a 0x8000000000000001 és 7
bemenetpárral.
(hibakeresési, középszint — cél:
megértés-ellenőrzés) A lab_add A64 utasítását
szándékosan módosítsd add w0, w0, w1 alakra. Miért megy át
továbbra is az eredeti teszt? Készíts olyan ellenpróbát, amely kimutatja
a hibát, majd disassemblyvel igazold a javítást.
1. megoldás — Segítség. A linkelő és a C-fordító két
különböző ellenőrzési szintet képvisel — gondold át, melyik szint mit
lát a kézzel írt assembly esetén, és hogy az A64 X19
megőrzése kinek a szerződését szolgálja (a hívóét vagy a hívottét).
1. megoldás — Teljes megoldás. A linkelő a
szimbólumneveket és az objektumok kapcsolatait oldja fel; a kézzel írt
assemblyhez nincs teljes C-típusellenőrzés. Az A64 X19
megőrzése pedig egymásba ágyazott szerződés: a C-segédfüggvény a
lab_apply által elhelyezett bónuszt őrzi meg, míg a
lab_apply saját hívójának korábbi értékét köteles
visszaadni.
2. megoldás — Segítség. Az előjel nélküli összehasonlításhoz a megfelelő feltételkódot válaszd (nem az előjeles változatot) — gondold át, miért számítana a magas helyiértékű bit előjelesen.
2. megoldás — Teljes megoldás. A fejlécbe kerüljön:
extern uint64_t lab_max(uint64_t a, uint64_t b);A lab-bridge.s fájlban, a záró veremjelölő szekció
előtt:
// A64 ISA; GNU as, AArch64 ELF; a .text szekcióban.
.p2align 2
.global lab_max
.type lab_max, %function
lab_max:
cmp x0, x1
csel x0, x0, x1, hs
ret
.size lab_max, .-lab_maxAz A64 HS előjel nélküli „nagyobb vagy egyenlő”
feltétel. Előjeles feltétellel a magas helyiértékű bitet tartalmazó
második teszt hibás lehetne.
A main végső visszatérése elé illeszthető
ellenőrzés:
if (lab_max(7, 5) != UINT64_C(7) ||
lab_max(UINT64_C(0x8000000000000001), 7) !=
UINT64_C(0x8000000000000001))
return 1;A korábbi, qemu-user célú build- és futtatási parancsok változatlanok; a várt kilépési állapot 0.
3. megoldás — Segítség. A kis teszt-bemenetek nem haladják meg a 32 bitet — gondold át, milyen bemenettel lehet olyan 64 bites összeget képezni, amelynek alsó 32 bitje önmagában nem árulkodik a hibáról.
3. megoldás — Teljes megoldás. A kis bemenetek elférnek 32 biten. A túlcsordulási tesztben pedig az alsó 32 bit összeadása is nullát ad, ezért az sem különbözteti meg a hibás implementációt.
A main végső visszatérése elé:
if (lab_add(UINT64_C(0x100000000), UINT64_C(5)) !=
UINT64_C(0x100000005))
return 1;A várt helyes eredmény előjel nélkül 4294967301. A hibás A64
W0-írás lenullázza az A64 X0 felső felét, így
5 lesz az eredmény. A program ekkor 1 állapottal lép ki; a
make ezt hibaként jelzi, saját hibakódja nem feltétlenül
1.
A korábbi objdump paranccsal ellenőrizzük az
operandusokat, állítsuk vissza az A64 add x0, x0, x1
utasítást, majd ugyanazzal az ellenpróbával várjunk ismét 0
programkilépési állapotot.
Az önálló assembly függvény és a C-program együttműködését a deklaráció, a szimbólumkapcsolat és az AAPCS64 hívási szabályai együttesen határozzák meg. A sikeres linkelés önmagában nem ellenőrzi az argumentumok típusát vagy számát. C-hívás előtt az assembly rutinnak meg kell őriznie visszatérési címét és a híváson át élő adatokat, visszatéréskor pedig helyre kell állítania a megőrzendő állapotot. A GCC meghajtóprogram leegyszerűsíti a C-runtime-mal történő linkelést. A szimbólumtábla, az áthelyezések és a határértéktesztek együtt adnak használható ellenőrzési módszert.
A linkek és a dokumentumverziók közös állapota az Olvasói útmutató forrásállapot-táblájában van; itt csak az alfejezet saját megállapításai szerepelnek.
Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is
fennmaradnak: a könyv referencia-eszközláncával (GNU
GCC/binutils, qemu-aarch64) a kódot itt nem fordítottuk és
nem futtattuk. A lab-interop és a
lab-abi-interop kimenetét, valamint a törölt
ldr x19 sorral végzett ellenpróbát helyettesítő
eszközlánccal mértük: zig cc (Clang, musl,
aarch64-linux-musl), Unicorn-alapú AArch64 user-mode
emulátorban; ebben a mérésben a Clang-fordította main a
sum-ot X19-ben tartotta, ezért írt ki
sum=9-et. A laborban rögzíteni kell az Ubuntu 24.04
telepített GCC/binutils/QEMU csomagverzióját, ellenőrizni kell a
statikus AArch64 C-runtime elérhetőségét, valamint össze kell vetni a
tényleges kimenetet és objektumadatokat a megadott várakozásokkal.
Teljesítménymérést ez a példa nem tartalmaz.