A linker nem C-forráskódot olvas: objektumfájlokban tárolt szimbólumokat és hivatkozásokat kapcsol össze. Emiatt a sikeres fordítás még nem jelenti azt, hogy a teljes program összeállítható. Egy függvény deklarációja elegendő lehet a hívás lefordításához, miközben a megvalósítása hiányzik; egy hibás header pedig több objektumfájlba is eljuttathatja ugyanannak az objektumnak a definícióját.
A Headerek, deklarációk és include-higiénia részben megismert deklaráció–definíció különbséget most bináris szinten vizsgáljuk. A parancsok a Warningok, optimalizálási profilok és link-driver rész gyakorlatát követik: a végső linkelést is a fordító driverével végezzük.
ISO C17. A nyelvi kapcsolás (linkage) azt határozza meg, hogy különböző deklarációk azonosítója ugyanazt az objektumot vagy függvényt jelöli-e.
A kapcsolás nem azonos a hatókörrel vagy az élettartammal. Egy
blokkon belüli static int count; azonosítónak nincs
kapcsolása, noha objektuma nem szűnik meg a blokk elhagyásakor. Ezeket a
különbségeket a Scope, linkage, storage duration és
lifetime rész tárgyalja részletesen.
Az alábbi deklarációk egymástól független példák, fájlhatókörben, korábbi deklaráció nélkül:
| Deklaráció | Szerep | Kapcsolás |
|---|---|---|
extern int limit; |
Nem definiáló deklaráció | Külső |
int limit = 3; |
Objektumdefiníció | Külső |
extern int limit = 3; |
Objektumdefiníció az inicializáló miatt | Külső |
int limit; |
Tentative definition | Külső |
static int limit; |
Tentative definition | Belső |
int budget(void); |
Függvénydeklaráció | Külső |
static int budget(void); |
Függvénydeklaráció | Belső |
Az extern tehát nem egyszerűen azt jelenti, hogy „máshol
van definiálva”. Inicializálóval definíciót is adhat. Korábbi, látható
deklaráció esetén pedig annak kapcsolását is örökölheti: egy
fájlhatókörű static int limit; után álló
extern int limit; ugyanazt a belső kapcsolású objektumot
deklarálja.
ISO C17. A tentative definition olyan
fájlhatókörű objektumdeklaráció, amelynek nincs inicializálója, és
tárolásiosztály-meghatározója nincs, vagy static.
Az alábbi teljes fordítási egység C17-ben érvényes:
int requests;
int requests;Ha a fordítási egység végéig nem jelenik meg hozzá inicializálóval
ellátott definíció, a nyelvi hatás olyan, mintha egy nullaértékkel
inicializált definíció szerepelne benne. Ha később
int requests = 5; is következik, az adja meg az objektum
kezdeti értékét.
Ez fordítási egységen belüli szabály. Nem engedély
arra, hogy minden .c fájl külön definíciót kapjon
ugyanahhoz a külső kapcsolású objektumhoz. A példáinkban használt külső
objektumokhoz és függvényekhez a teljes programban egyetlen definíció
szükséges.
A következő példa minimumszabványa ISO C17. A
megfigyelésekhez választott laborprofil: Linux, x86-64 System V ABI,
LP64, CHAR_BIT == 8; GCC 14.2.0 vagy Clang 19.1.7, GNU
binutils 2.43 és glibc 2.40. Ez reprodukciós célprofil, nem dokumentált
futtatási eredmény. A szimbólumokra vonatkozó táblázat alább
várt értelmezés, nem bemásolt eszközkimenet.
Az retry.h tartalma:
#ifndef RETRY_H
#define RETRY_H
extern int retry_limit;
extern int calls;
int retry_budget(void);
#endifA retry.c tartalma:
#include "retry.h"
int retry_limit = 3;
int calls;
static int reserve = 1;
int retry_budget(void)
{
calls = 1;
return retry_limit - reserve;
}A main.c tartalma:
#include "retry.h"
int main(void)
{
int budget = retry_budget();
return budget == 2 && calls == 1 ? 0 : 1;
}A program nem módosítja a retry_limit és
reserve értékét; így a kivonás eredménye biztosan
ábrázolható int típusban. A calls név itt csak
a függvény meghívását jelzi, nem általános számláló.
A tanulói laborban használható fordítási és vizsgálati parancsok:
gcc -std=c17 -Wall -Wextra -Wpedantic -O0 \
-fno-lto -fno-common -fno-pie -c retry.c main.c
nm -A retry.o main.o
readelf -Ws retry.o
readelf -Ws main.o
readelf -rW retry.o
readelf -rW main.o
gcc -no-pie retry.o main.o -o retry-demoClang használatakor a fordítási és linkelési sorban a
gcc helyére clang kerül. Az explicit opciók
kizárják a vizsgálatból az LTO-t, a common-kezelést és a PIE
alapértelmezésének eltéréseit. Éles programok hardeningprofilját ebből
nem szabad levezetni.
GNU binutils / ELF. A lényeges szimbólumok várható értelmezése:
| Fájl, név | nm jelölés |
ELF-kötés, szekció | Jelentés |
|---|---|---|---|
retry.o: retry_limit |
D |
GLOBAL, .data |
Inicializált objektumdefiníció |
retry.o: calls |
B |
GLOBAL, .bss |
Nullaértékkel inicializált definíció |
retry.o: reserve |
d |
LOCAL, .data |
Belső kapcsolású objektum |
retry.o: retry_budget |
T |
GLOBAL, .text |
Függvénydefiníció |
main.o: retry_budget |
U |
GLOBAL, UND |
Feloldásra váró hivatkozás |
main.o: calls |
U |
GLOBAL, UND |
Feloldásra váró hivatkozás |
Az nm betűi eszközkonvenciók: a nagybetű gyakran
globális, a kisbetű lokális szimbólumot jelöl, de vannak kivételek. A
readelf -Ws külön oszlopokban mutatja a típust, kötést
(binding), láthatóságot (visibility) és
szekcióindexet.
Egy puszta extern deklaráció nem feltétlenül eredményez
szimbólumtábla-bejegyzést. A main.c például nem használja a
retry_limit objektumot, ezért annak deklarációja önmagában
nem igényel feloldandó gépi hivatkozást.
ELF / fordítóbővítés. A C kapcsolása és az ELF láthatósága külön szerződés.
| Tulajdonság | Milyen kérdésre válaszol? |
|---|---|
| C linkage | Mely deklarációk jelölik ugyanazt az entitást? |
ELF binding, például LOCAL, GLOBAL,
WEAK |
Hogyan vesz részt a szimbólum a linker feloldásában? |
ELF visibility, például DEFAULT,
HIDDEN |
Milyen elérhetőségi és helyettesíthetőségi szabályok vonatkoznak rá az ELF-modellben? |
Egy külső kapcsolású C függvény ELF-szimbóluma lehet
GLOBAL HIDDEN. Ettől nem válik C szerinti belső kapcsolású
függvénnyé: más objektumfájlból továbbra is hivatkozható ugyanazon végső
linkelt komponensen belül. A -fvisibility=hidden GCC/Clang
opció ezért nem helyettesíti a fájlhatókörű static
használatát.
Egy lokális automatikus változó pedig rendszerint nem linker által feloldandó szimbólum. A debuggerben látható neve nem bizonyítja az ellenkezőjét: a hibakeresési információ külön adatszerkezet.
A main.c fordításakor a fordító ismeri a
retry_budget típusát, de végleges címét még nem. A gépi kód
megfelelő mezőjéhez ezért relokációs rekordot
(relocation record) állít elő.
ELF / x86-64 ABI. A rekord megadja:
Az x86-64 ELF-ben gyakori RELA-formátum külön mezőben tárolja az
addendet. A readelf -rW ezeket a rekordokat mutatja.
A linker először összegyűjti a bemeneti szekciókat, kiosztja azok
helyét, majd feloldja a szimbólumokat és elvégzi az előírt javításokat.
Az undefined szimbólum tehát egy .o fájlban rendszerint
normális köztes állapot.
Az x86-64 R_X86_64_PC32 relokáció számítása:
ahol a szimbólum címe, az addend, pedig a javítandó mező címe. Az eredménynek előjeles 32 bites értékként ábrázolhatónak kell lennie.
A következő címek illusztrációk a megadott x86-64 laborprofilhoz, nem a példaprogram mért címei:
| Mennyiség | Illusztratív érték |
|---|---|
0x402000 |
|
0x401004 |
|
-4 |
Ekkor:
Bizonyításvázlat. Olyan utasításnál, amelynek négy oktettes eltolásmezője után közvetlenül a következő utasítás kezdődik, annak címe . A processzor innen számol:
A -4 tehát összehangolja a relokáció mezőkezdethez
kötött számítását a processzor következő utasításhoz viszonyított
címzésével.
Nem minden hivatkozás PC32: függvényhívásnál például
R_X86_64_PLT32 is megjelenhet. A konkrét típust vizsgálni
kell, nem kikövetkeztetni pusztán a C-kifejezésből. A dinamikus
feloldást az ELF-betöltés, PLT/GOT és dinamikus
szimbólumok rész folytatja.
ELF / GCC–Clang-bővítés. Az erős és
gyenge szimbólum nem ISO C nyelvi kategória. Szokásos
terminológiával egy közönséges globális definíció erős, míg a
STB_WEAK kötésű szimbólum gyenge.
Szokásos ELF-linkelésben egy erős definíció elsőbbséget élvez egy azonos nevű gyenge definícióval szemben. Két erős definíció általában multiple-definition hibát okoz. Több gyenge definíció közötti választásra nem szabad hordozható alkalmazáslogikát építeni.
GCC/Clang-bővítéssel például az alábbi különálló fordítási egység adhat gyenge alapértelmezést:
/* ISO C17 szintaxisra épülő GCC/Clang ELF-bővítés. */
__attribute__((weak))
int site_limit(void)
{
return 3;
}gcc -std=c17 -Wall -Wextra -c fallback.c -o fallback.o
nm fallback.o
readelf -Ws fallback.oA várt besorolás függvénynél W, illetve
WEAK. Ez platformhoz kötött felülírási mechanizmus, nem
általános C-modularizálási eszköz.
-fno-common?Régebbi toolchainprofilokban a fájlhatókörű int total;
gyakran common szimbólumként jelent meg. A
common-bejegyzés még nem közönséges, szekcióhoz rendelt definíció:
helyigényt és igazítási követelményt közöl. Az nm jellemző
jelölése C, a readelf szekcióindexe
COM.
A linker több ilyen bejegyzést hagyományosan összevonhatott. Ez elfedte a headerbe írt tentative definition hibáját.
Modern GCC- és Clang-profilokban gyakori alapértelmezés a
-fno-common; a reprodukálható parancs mégis írja ki
explicit módon. Ekkor a tentative definition tipikusan rendes
.bss-definícióvá válik, és a több fordítási egységben
elhelyezett példányok ütköznek.
A -fcommon visszakapcsolása nem helyettesíti a
javítást:
extern int total;;.c fájlba int total;.Undefined reference esetén újabb deklaráció hozzáadása. A deklaráció már elegendő volt a fordításhoz. A hiányzó definíciót tartalmazó objektumfájlt kell bevonni, vagy a tévesen belső kapcsolású megvalósítást kell javítani.
Multiple definition esetén include guard hozzáadása.
A guard egy fordítási egységen belül működik; külön .c
fájlok között nem akadályozza meg a definíció megismétlődését.
Sikeres linkelésből típushelyességre következtetés. A szokásos ELF-linker nem teljes C-típusellenőrző. Össze nem illő deklarációk is azonos szimbólumnévre mutathatnak. A közös header és annak megvalósításoldali beillesztése ezért helyességi követelmény.
Magyarázat. Miért d a
reserve, de D a retry_limit?
Külső kapcsolású marad-e egy HIDDEN láthatóságú
függvény?
Kódolás. Egészítsd ki a példát
extern int last_budget; deklarációval. Egyetlen definíció
legyen, a retry_budget tárolja bele az eredményt, a
main pedig ellenőrizze azt. Milyen szimbólumokat
vársz?
Diagnosztika. Két külön fájl, a.c
és b.c, egyaránt kizárólag az alábbi teljes tartalmat
kapja:
int total;Vizsgáld meg őket -fno-common, majd
-fcommon mellett. Magyarázd meg a részleges linkelés
eltérését. Mi történne az eredeti példában, ha csak a
main.o kerülne a végső linkparancsba?
A reserve fájlhatókörű static objektum:
belső kapcsolású, a laborprofilban lokális ELF-szimbólummal. A
retry_limit külső kapcsolású, globális szimbólum. Mindkettő
inicializált adat, innen a d és D.
A HIDDEN nem változtatja meg a C kapcsolását.
Ellenőrzési feltétel: a readelf -Ws kimenetében a
Bind és Vis oszlopot külön értelmezzük.
A headerbe kerüljön extern int last_budget;, a
retry.c fájlba pedig egyszer
int last_budget;.
A függvény módosított részlete:
int retry_budget(void)
{
calls = 1;
last_budget = retry_limit - reserve;
return last_budget;
}A main visszatérési kifejezése legyen:
return budget == 2 && calls == 1 && last_budget == budget ? 0 : 1;Az eredeti buildparancsok változatlanok. Ellenőrzési feltétel: a
retry.o egy B definíciót, a
main.o egy U hivatkozást tartalmazzon
last_budget néven; a teljes program sikeres ellenőrzésének
várt kilépési állapota nulla.
gcc -std=c17 -fno-common -c a.c b.c
nm -A a.o b.o
gcc -r a.o b.o -o combined.oMindkét objektumban rendes definíció várható; a részleges linkelés
várhatóan multiple-definition hibával leáll. A -r újabb
relokálható objektumot készítene, ezért itt nem szükséges
main.
gcc -std=c17 -fcommon -c a.c b.c
nm -A a.o b.o
gcc -r a.o b.o -o combined.oItt C bejegyzések és sikeres összevonás várható. Ez a
toolchain viselkedése, nem az ISO C többdefiníciós engedménye.
Az eredeti példából a retry.o kihagyása feloldatlan
retry_budget és calls hivatkozást hagy.
Ellenőrzési feltétel: a hiányzó neveket össze kell vetni a
main.o U bejegyzéseivel.
A C kapcsolása deklarációk azonosságát, az ELF szimbólumtáblája a
bináris összekapcsolás adatait írja le. A relokáció a még ismeretlen
címek javítási receptje. Az nm és a readelf
segítségével elkülöníthető a hiányzó definíció, a többszörös definíció
és a common-kezelés. A megbízható alapminta változatlan: közös
deklaráció a headerben, egyetlen megvalósítás, explicit fordítási
profil.