Egy assemblyrészlet önmagában nem magyarázat. Ugyanaz a C függvény eltérő gépi kódot kaphat más optimalizálással, fordítóval vagy célarchitektúrával, miközben a nyelvi szerződése változatlan. A labor célja ezért nem egy „helyes assemblyalak” felismerése, hanem ellenőrizhető következtetések készítése: mit ír elő a nyelv, mit az ABI, és mit választott az adott fordító?
A hívási konvencióról szóló rész argumentum- és visszatérési szabályaira, valamint a reprodukálható labor környezetrögzítésére építünk. A volatile és barrier eszközök itt nem megoldások az optimalizálás megfigyelésére: indokolatlan használatukkal éppen a vizsgált program szerződését változtatnánk meg.
A példákhoz nem tartozik tényleges fordítási vagy mérési eredmény. Az assemblyalakok illusztrációk; a jegyzőkönyvbe csak a saját eszközökkel előállított lelet kerüljön. Ellenőrzött forrásjegyzet hiányában a psABI- és fordítódokumentáció pontos kiadásának, szakaszának hozzárendelése is laborfeladat.
Az ABI-lelet egy konkrét bináris interfész megfigyelése: például mely regiszterben érkezik egy argumentum. Az optimalizálási lelet egy konkrét fordítás eredménye: például eltűnt-e egy hívás vagy ismételt memóriabetöltés. Egyik sem válik pusztán a megfigyeléstől ISO C-garanciává.
A választott referenciaprofil:
| Réteg | Laborprofil |
|---|---|
| Nyelv | ISO C17, explicit -std=c17 |
| Elsődleges target | Linux, x86-64 System V, LP64 |
| Adatreprezentáció | CHAR_BIT == 8, sizeof(long) == 8 |
| Fordítók | GCC 14.2.0 és Clang 19.1.7 |
| Eszközök | GNU binutils 2.43.1, GDB 15.2 |
| Hosted környezet | glibc 2.40 |
| Összehasonlító target | Linux AArch64, LP64, AAPCS64 alapkonvenció |
Ez kiválasztott, nem ellenőrzötten telepített környezet. Más kiadások is használhatók, de az eltérést rögzíteni kell. Mentsük el a verziókat, a target triple-t, a teljes parancsokat és a forrás Git-azonosítóját. A linker tényleges verziója is számít: a compiler driver nem feltétlenül a feltételezett linkert indítja.
ISO C17-feltételek: a példában minden bemeneti egész
érték legyen
között. Így az összeadások és szorzások eredménye reprezentálható. A
pointerparaméterek inicializált, élő long objektumokra
mutassanak; a módosított objektum legyen írható. Az ABI-vizsgálat nem
tesz jogszerűvé hibás pointerhozzáférést.
A három fájl szándékosan különíti el a fordítási egységen belül látható és kívül definiált függvényeket.
A lab.h tartalma:
#ifndef LAB_H
#define LAB_H
struct pair {
long first, second;
};
struct triple {
long first, second, third;
};
long args(long a, long b, long c, long d,
long e, long f, long g);
struct pair pair_make(long x);
struct triple triple_make(long x);
long reload(long *p, long *q);
long reload_disjoint(long *restrict p, long *restrict q);
long add_one(long x);
long local_use(long x);
long external_use(long x);
long across_call(long x);
#endifA lab.c tartalma:
#include "lab.h"
long args(long a, long b, long c, long d,
long e, long f, long g)
{
return a + b + c + d + e + f + g;
}
struct pair pair_make(long x)
{
return (struct pair){x, x + 1};
}
struct triple triple_make(long x)
{
return (struct triple){x, x + 1, x + 2};
}
long reload(long *p, long *q)
{
long before = *p;
*q = 7;
return before + *p;
}
long reload_disjoint(long *restrict p, long *restrict q)
{
long before = *p;
*q = 7;
return before + *p;
}
long add_one(long x)
{
return x + 1;
}
long local_use(long x)
{
return 2 * add_one(x);
}
long across_call(long x)
{
long saved = 3 * x;
long result = external_use(x);
return saved + result;
}A driver.c tartalma:
#include <limits.h>
#include "lab.h"
_Static_assert(CHAR_BIT == 8, "This lab requires 8-bit C bytes");
_Static_assert(sizeof(long) == 8, "This lab requires 64-bit long");
long external_use(long x)
{
return 2 * add_one(x);
}
int main(void)
{
long x = 3;
long y = 9;
struct pair p = pair_make(10);
struct triple t = triple_make(10);
long shared = reload(&x, &x);
x = 3;
long separate = reload_disjoint(&x, &y);
int ok =
args(1, 2, 3, 4, 5, 6, 7) == 28 &&
p.first == 10 && p.second == 11 &&
t.first == 10 && t.second == 11 && t.third == 12 &&
shared == 10 && separate == 6 &&
local_use(4) == 10 &&
external_use(4) == 10 &&
across_call(4) == 22;
return ok ? 0 : 1;
}A pointerek szerződése külön is áttekinthető:
| Elem | Tulajdonos és élettartam | Kölcsönzött hozzáférés | Érvénytelenítés, hibakimenet |
|---|---|---|---|
x, y tárolója |
A main automatikus objektumai |
A hívások időtartamára | Az objektum élettartamának vége után a dereferencia UB |
reload paraméterei |
Nem vesznek át tulajdont | Azonos objektumot is jelölhetnek | Hibás pointerre nincs ellenőrzött hibakód |
reload_disjoint paraméterei |
Nem vesznek át tulajdont | Ebben a függvényben külön objektumokra kell mutatniuk | Az aliasing-előfeltétel megsértése UB |
| Struktúraeredmények | A hívó által kezelt értékek | Nem adnak vissza kölcsönzött pointert | Nincs dinamikus allokáció vagy felszabadítás |
A program elvárt sikerjelzése a nulla kilépési állapot; ez nem futtatással igazolt eredmény.
Az alábbi Bash-parancsok a külön laborban fordítási artefaktumokat készítenek, nem futtatják a programot. Feltételük a fenti x86-64 Linux eszközlánc.
for cc in gcc clang; do
for opt in 0 2; do
name="${cc}-O${opt}"
flags=(-std=c17 "-O${opt}" -g3
-Wall -Wextra -Wpedantic
-march=x86-64 -mtune=generic -fno-lto)
"$cc" "${flags[@]}" -c lab.c -o "${name}-lab.o"
"$cc" "${flags[@]}" -c driver.c -o "${name}-driver.o"
"$cc" -fno-lto "${name}-lab.o" "${name}-driver.o" \
-o "$name"
"$cc" "${flags[@]}" -S -masm=intel lab.c \
-o "${name}-lab.s"
done
doneA linkidejű optimalizálás, azaz LTO kikapcsolása kísérleti kontroll:
a lab.c fordításakor az external_use törzse
nem látható. A -g3 debuginformációt kér, de nem tiltja az
optimalizálást és nem garantálja minden változó megjeleníthetőségét.
ABI-ellenőrzési lista: a következő várakozásokat a választott x86-64 psABI és AAPCS64 dokumentum megfelelő kiadásával kell alátámasztani. Nem variadikus, közönséges egész argumentumú függvényeket vizsgálunk; vektoros és speciális platformkonvenciókat nem.
| Eset | x86-64 System V | AArch64, AAPCS64 |
|---|---|---|
args első hat argumentuma |
rdi, rsi, rdx,
rcx, r8, r9 |
x0–x5 |
| Hetedik argumentum | Veremben | x6 |
long visszatérés |
rax |
x0 |
pair_make eredménye |
rax, rdx |
x0, x1 |
triple_make eredménytárolójának címe |
Rejtett argumentumként rdi |
x8 |
triple_make explicit x argumentuma |
rsi |
x0 |
A két struktúra layoutja a megadott LP64 ABI-profilokban, C bájtban:
| Típus | Tagoffsetek | Méret | Igazítás |
|---|---|---|---|
struct pair |
0, 8 | 16 | 8 |
struct triple |
0, 8, 16 | 24 | 8 |
Ezek konkrét típusok konkrét ABI szerinti eredményei. A „kis struktúra mindig regiszterben tér vissza” nem általános szabály: a méret mellett az ABI osztályozási szabályai és a tagtípusok is számítanak.
x86-64-en jelölje
a függvénybelépéskori rsp értékét. A call
által elhelyezett visszatérési cím nyolc C bájtot foglal, ezért ennél az
aláírásnál a hetedik argumentum címe:
Ez belépéskori összefüggés. Ha a prologue később összesen
bájttal csökkenti rsp értékét, akkor ugyanaz a cím az
aktuális veremmutatóhoz képest:
A struktúraeredményhez átadott rejtett pointer tárolót jelöl, nem ownership-átadást. A hívó által biztosított eredménytároló nem szükségképpen azonos egy debuggerben látható forrásváltozó végső helyével.
A különböző eszközök eltérő kérdésekre válaszolnak:
readelf -h -W gcc-O2-lab.o
readelf -s -W gcc-O2-lab.o
readelf -r -W gcc-O2-lab.o
nm -S --size-sort gcc-O2-lab.o
objdump -drwC -Mintel gcc-O2-lab.o
objdump -d -Mintel gcc-O2A readelf megmutatja az ELF célarchitektúráját,
szimbólumait és relokációit. A nm segítségével
feljegyezhető a függvényszimbólumok mérete. Az objdump a
gépi utasításokat mutatja; relokálható objektumban a hívás céljának
értelmezéséhez a relokációt is olvasni kell.
Az .s fájlban még assemblerdirektívák is szerepelnek,
ezért a sorok száma nem utasításszám. A szimbólumméret pedig nem
végrehajtási idő.
A debuggerben először pontos belépési pontot vizsgáljunk. A
break *args a függvény gépi belépési címére kér
töréspontot; a soralapú megállás már a prologue utánra eshet. A külön
interaktív munkamenetben használható parancsok:
disassemble /r args: utasítások és gépi bájtok;info registers rdi rsi rdx rcx r8 r9 rsp:
argumentumregiszterek és veremmutató;x/4gx $rsp: a belépéskori verem környezetének
vizsgálata;si: egyetlen gépi utasítás végrehajtása;info frame: a debugger frame-értelmezése.A debugger memóriavizsgálata nem bizonyítja, hogy ugyanazt a címet a C program szabályosan dereferálhatná.
A register spill egy regiszterben tartott vagy oda szánt érték memóriába helyezése a regiszterallokáció során. Nem minden veremírás spill:
-O0 mellett a lokális változó kezdettől veremhelyet
kaphat;Az across_call esetében a saved értékének a
hívás után is rendelkezésre kell állnia. A fordító megőrizheti
regiszterben, veremhelyre mentheti, vagy alkalmas alakban
újraszámolhatja. A tényleges spill hiánya is érvényes lelet.
Mérjük meg a szimbólumméretet, a látható veremmutató-csökkentéseket,
valamint külön számoljuk a regisztermentéseket és lokális
memóriaműveleteket. Ez az egyszerű kódvizsgálat nem méri a teljes
dinamikus veremhasználatot: x86-64-en például red-zone használata miatt
lokális tároló explicit rsp-csökkentés nélkül is
megjelenhet.
A local_use számára az add_one törzse
látható. A fordító a hívást helyettesítheti a törzzsel, majd
egyszerűsítheti:
Bizonyításvázlat: a megadott bemeneti tartományban minden közbülső signed eredmény reprezentálható, és nincs mellékhatás; az átalakítás megőrzi a visszatérési értéket.
Az inline kulcsszó hiánya tehát nem tiltja az
inliningot. Megfordítva, jelenléte sem garantál gépi beillesztést. Az
external_use másik fordítási egységből hívja az
add_one függvényt, ezért a jelenlegi, LTO nélküli profil jó
kontroll.
A hívás hiánya önmagában még nem bizonyít inliningot: más kódban dead-code elimination vagy konstanshelyettesítés is eltüntethet hívást.
A reload(&x, &x) szabályos. Ha kezdetben
x == 3, akkor:
before értéke 3;*q = 7 ugyanazt az objektumot módosítja;*p értéke 7;A következő lehetséges, nem mért x86-64 Intel-szintaxisú részlet ezt kezeli:
# ILLUSZTRACIO: nem konkret compiler-output.
reload:
mov rax, QWORD PTR [rdi]
mov QWORD PTR [rsi], 7
add rax, QWORD PTR [rdi]
retA memóriaolvasásokat ne csak mov utasítások alapján
számoljuk: itt az add is olvas memóriát.
A reload_disjoint szerződése kizárja a két hozzáférési
út érintett objektumainak átfedését. Emiatt a *q írás nem
változtathatja meg a *p értékét, és a második betöltés
elhagyható. Külön x == 3 és y == 9
objektumokkal az eredmény 6, az utóállapot x == 3,
y == 7.
Azonos pointerekkel meghívni ezt a restrict változatot
nem korrekt ellenpélda, hanem szerződéssértés és UB. A
fordító optimalizálását csak definiált viselkedésű bemenetekkel szabad
cáfolni.
A header és a lab.c nem igényel libc-headereket, így
megfelelő AArch64 backenddel assembly sysroot nélkül is
előállítható:
clang --target=aarch64-linux-gnu -std=c17 -O2 -g \
-Wall -Wextra -Wpedantic -fno-lto \
-S lab.c -o lab-aarch64.sEz fordítási kísérlet, nem AArch64-futtatás. A teljes program linkeléséhez megfelelő target runtime, libc és linker is szükséges. A futtatás külön natív gépet vagy megfelelő emulációs környezetet igényelne; emulátoros időmérés nem a target processzor natív teljesítménye.
A dokumentációs indoklás három külön ellenőrzésből álljon:
Mindenhez rögzítsük a ténylegesen olvasott dokumentum kiadását és szakaszát. A kutatási kiinduló URL nem bizonyíték.
| Függvény | Profil | Szimbólumméret | Releváns memóriaolvasások | Veremműveletek | Magyarázat |
|---|---|---|---|---|---|
reload |
GCC, -O0 |
saját adat | saját adat | saját adat | lelet és indoklás |
reload |
GCC, -O2 |
saját adat | saját adat | saját adat | lelet és indoklás |
across_call |
Clang, -O2 |
saját adat | saját adat | saját adat | lelet és indoklás |
A statikus utasításszám csak a megvizsgált kódalak jellemzője; elágazó kódnál nem azonos egy futás műveletszámával.
Az assembly mellé háromféle kommentet írjunk:
call a local_use törzsében.”long eredmény az ABI szerinti eredményregiszterben
érkezik.”Gyakori hiba a „mindig” szó: „az optimalizált programnak nincs stack frame”, „a pointerdereferencia mindig egy load”, „a struktúra mindig rejtett pointerrel tér vissza”. Ezek helyett nevezzük meg a típust, targetet és artefaktumot.
Magyarázó: vezessük le a hetedik argumentum
helyét x86-64 belépéskor és egy nyolcbájtos push után.
Magyarázzuk meg, miért más a helyzete AArch64-en.
Kódolási: egészítsük ki a main
ellenőrzését azzal, hogy a külön objektumokon végzett
reload_disjoint után x == 3 és
y == 7 is teljesüljön. Indokoljuk, miért hibás az azonos
pointerekkel végzett „optimalizálási teszt”.
Diagnosztikai: hasonlítsuk össze mindkét fordító
-O0 és -O2 artefaktumában a
local_use, external_use és
across_call függvényeket. Osztályozzuk a veremírásokat, és
fogalmazzuk meg, mit nem bizonyít az AArch64 .s fájl
előállítása.
Belépéskor a visszatérési cím az
címen, a hetedik argumentum
címen van. Egy nyolcbájtos push után
,
tehát az argumentum címe
.
AArch64-en a hét egész argumentum még elfér az argumentumregiszterekben.
Az ellenőrzés feltétele a pontos függvénybelépési pont és az adott
prologue követése.
A meglévő ok kifejezéshez hozzáadható:
/* C17; reszlet a driver.c ellenorzeseben. */
x == 3 && y == 7Ez a visszatérési érték mellett a módosítás helyét is ellenőrzi. Az
azonos pointeres restrict hívás UB volna; annak tetszőleges
eredménye nem optimalizálási hiba bizonyítéka.
Nincs előre garantált utasítássor. A helyes megoldás minden állításához artefaktumot és profilt rendel, külön kezeli a lokális tárolást, callee-saved mentést és spillt, valamint elfogadja ezek hiányát is. Az AArch64 assembly bizonyítja, hogy az adott fordító előállított targetkódot; nem bizonyít sikeres linkelést, futást vagy natív teljesítményt.
A megbízható ABI-diagnózis három réteget választ szét: nyelvi szerződést, dokumentált bináris interfészt és konkrét fordítói megvalósítást. A reprodukálható labor kontrollált profilokat, definiált viselkedésű programot és egymást kiegészítő bináris eszközöket használ. A cél nem egy assemblyminta bemagolása, hanem olyan jegyzőkönyv, amelyből minden következtetés feltétele és bizonyítékának korlátja visszakereshető.