c - Hvorfor fejler denne variadicfunktion på 4. parameter på Windows x64?

Indlæg af Hanne Mølgaard Plasc

Problem



Nedenfor er kode, der indeholder en variadic funktion og opkald til variadic funktionen. Jeg ville forvente, at det ville udføre hver sekvens af tal på passende vis. Det gør, når det kompileres som en 32-bit eksekverbar, men ikke når den kompileres som en 64-bit eksekverbar.


#include <stdarg.h>
#include <stdio.h>

#ifdef \_WIN32
#define SIZE\_T\_FMT "\%Iu"
#else
#define SIZE\_T\_FMT "\%zu"
#endif


static void dumpargs(size\_t count, ...) {

    size\_t i;
    va\_list args;

    printf("dumpargs: argument count: " SIZE\_T\_FMT "
", count);

    va\_start(args, count);

    for (i = 0; i < count; i++) {

        size\_t val = va\_arg(args, size\_t);
        printf("Value=" SIZE\_T\_FMT "
", val);
    }
    va\_end(args);
}

int main(int argc, char** argv) {

    (void)argc;
    (void)argv;

    dumpargs(1, 10);
    dumpargs(2, 10, 20);
    dumpargs(3, 10, 20, 30);
    dumpargs(4, 10, 20, 30, 40);
    dumpargs(5, 10, 20, 30, 40, 50);

    return 0;
}


Her er outputen, når den kompileres til 64-bit:


dumpargs: argument count: 1
Value=10
dumpargs: argument count: 2
Value=10
Value=20
dumpargs: argument count: 3
Value=10
Value=20
Value=30
dumpargs: argument count: 4
Value=10
Value=20
Value=30
Value=14757395255531667496
dumpargs: argument count: 5
Value=10
Value=20
Value=30
Value=14757395255531667496
Value=14757395255531667506


Redigere:


Vær opmærksom på, at årsagen til, at variadicfunktionen trækker size\_t ud, er, at den reelle anvendelse af dette er til en variadisk funktion, der accepterer en liste med peger og længder. Naturligvis skal argumentet for længde være en size\_t. Og i nogle tilfælde kan en opkalder passere i en kendt længde for noget:


void myfunc(size\_t pairs, ...) {
    va\_list args;
    va\_start(args, count);

    for (i = 0; i < pairs; i++) {
        const void* ptr = va\_arg(args, const void*);
        size\_t len = va\_arg(args, size\_t);
        process(ptr, len);
    }
    va\_end(args);
}

void user(void) {
    myfunc(2, ptr1, ptr1\_len, ptr2, 4);
}


Bemærk, at 4, der er overført til myfunc, kan støde på problemet beskrevet ovenfor. Og ja, virkelig skal den, der ringer op, bruge sizeof eller resultatet af strlen eller bare sætte tallet 4 til et size\_t sted. Men det er meningen, at kompilatoren ikke fanger dette (en fælles fare med variadiske funktioner).


Den rigtige ting at gøre her er at eliminere den variadiske funktion og erstatte den med en bedre mekanisme, der giver type sikkerhed. Jeg vil dog gerne dokumentere dette problem og indsamle mere detaljerede oplysninger om, hvorfor dette problem eksisterer på denne platform og manifesterer som det gør.

Bedste reference


Så grundlæggende, hvis en funktion er variadisk, skal den være i overensstemmelse med en bestemt kaldkonvention (vigtigst skal opkalderen rydde op args, ikke callie, da callie ikke har nogen ide om hvor mange args der vil være).


Grunden til, at det begynder at foregå den 4. er på grund af den kaldende konvention, der anvendes på x86-64. Efter min viden registrerer både visuel c ++ og gcc brug for de første par parametre, og derefter derefter bruger stakken. [20]


Jeg gætter på, at dette også er tilfældet for variadiske funktioner (hvilket gør mig så mærkeligt, da det ville gøre va\_ * -makroerne mere komplicerede).


På x86 er standard C-opkaldskonventionen brugen af ​​stakken altid.

Andre referencer 1


Problemet er, at du bruger size\_t til at repræsentere typen af ​​værdier. Dette er forkert, værdierne er faktisk normale 32-bit værdier på Win64.


Size\_t bør kun bruges til værdier, der ændrer størrelse baseret på platformens 32 eller 64 bit-ness (f.eks. Pointers). Skift kode for at bruge int eller \_\_int32, og dette skal løse dit problem.


Årsagen til, at dette virker fint på Win32 er, at size\_t er en anden størrelse, afhængig af platfrom. For 32 bit windows vil det være 32 bit og på 64 bit windows vil det være 64 bit. Så på 32 bit windows sker det bare for at matche størrelsen af ​​datatypen du bruger.

Andre referencer 2


En variadisk funktion er kun svagt typekontrolleret. Navnlig giver funktions signaturen ikke tilstrækkelig information til kompilatoren til at kende typen af ​​hvert argument antaget af funktionen.


I dette tilfælde er size\_t 32-bit på Win32 og 64-bits på Win64. Det skal variere i størrelse som det for at kunne udføre sin definerede rolle. Så for en variadisk funktion at trække argumenter ud korrekt, som er af type size\_t, skulle opkalderen sørge for, at kompilatoren kunne fortælle, at argumentet var af den type ved kompileringstid i kaldemodulet.


Desværre er 10 en konstant af typen int. Der er ikke defineret suffixbogstav, der markerer en konstant for at være af type size\_t. Du kunne skjule det faktum inde i en platformspecifik makro, men det ville ikke være klarere end at skrive (size\_z)10 på opkaldsstedet.


Det ser ud til at fungere delvist på grund af den faktiske kaldekonvention, der anvendes i Win64. Ud fra de givne eksempler kan vi fortælle, at de første fire integrerede argumenter til en funktion er bestået i registre og resten på stakken. Det tilladt at tælle og de første tre variadiske parametre skal læses korrekt.


Men det vises kun til arbejde. Du står faktisk ret i udefineret adfærdsområde, og 'udefineret' betyder virkelig 'udefineret': alt kan ske.
På andre platforme kan der også ske noget.


Fordi variadiske funktioner er implicit usikre, placeres en særlig byrde på koderen for at sikre, at typen af ​​hvert argument kendt ved kompileringstid matcher typen, som argumentet antages at have ved kørselstid.


I nogle tilfælde hvor grænsefladerne er velkendte, er det muligt at advare om type mismatch. For eksempel kan gcc ofte genkende at typen af ​​et argument til printf() ikke matcher formatstrengen og udstede en advarsel. Men det gøres generelt for alle variadiske funktioner, er hard .

Andre referencer 3


Årsagen til dette skyldes, at size\_t defineres som en 32-bit værdi på 32-bit Windows, og en 64-bit værdi på 64-bit Windows. Når det fjerde argument er overført til variadic-funktionen, synes de øverste bits at være uninitialized. Den fjerde og femte værdi, der trækkes ud, er faktisk:


Value=0xcccccccc00000028
Value=0xcccccccc00000032


Jeg kan løse dette problem med en enkel cast på alle argumenterne, såsom:


dumpargs(5, (size\_t)10, (size\_t)20, (size\_t)30, (size\_t)40, (size\_t)50);


Dette svarer dog ikke på alle mine spørgsmål. såsom:



  • Hvorfor er det det fjerde argument? Sandsynligvis fordi de første 3 er i registre?

  • Hvordan undgår man denne situation på en type sikker bærbar måde?

  • Er dette sket på andre 64-bit platforme ved hjælp af 64-bit værdier (ignorerer den size\_t kan være 32-bit på nogle 64-bit platforme)?

  • Skal jeg trække værdierne ud som 32-bit værdier, uanset målplatformen, og vil det medføre problemer, hvis en 64-bit-værdi skubbes ind i variadicfunktionen?

  • Hvad siger standarderne om denne adfærd?






Jeg ville virkelig have et citat fra The Standard, men det er noget, der ikke er hyperlink-kompatibelt, og koster penge til at købe og downloade. Derfor tror jeg at citere det ville være en krænkelse af ophavsretten. [21] [22]


Henvisning til comp.lang.c FAQ, det er klart, at når du skriver en funktion, der har et variabelt antal argumenter, er der intet du kan gøre for type sikkerhed. Det er op til den, der ringer op for at sikre, at hvert argument enten passer perfekt eller eksplicit udkast. Der er ingen implicitte konverteringer. [23] [24] [25]


Så meget bør være indlysende for dem, der forstår C og printf (bemærk at gcc har en funktion til at kontrollere printf-format-formatstrengene), men det er ikke så indlysende, at ikke blot er typene ikke implicit støbt, men hvis størrelsen af ​​typerne ikke passer til det, der er uddraget, kan du have uninitialiserede data eller uafklaret adfærd generelt. 'Slot', hvor et argument er placeret, kan måske ikke initialiseres til 0, og der kan ikke være en 'slot' - på nogle platforme kan du passere en 64-bit værdi og udpakke to 32-bit værdier inde i variadicfunktionen. Det er udefineret adfærd. [26]

Andre referencer 4


Hvis du er den der skriver denne funktion, er det dit job at skrive den variadiske funktion korrekt og/eller korrekt dokumentere din funktions kaldkonventioner.


Du har allerede fundet ud af, at C spiller hurtigt og løs med typer (se også signatur og forfremmelse), så eksplicit støbning er den mest oplagte løsning. Dette ses hyppigt med heltalskonstanter, der udtrykkeligt defineres med ting som UL eller ULL.


De fleste hygiejnekontrol af bestået værdier vil være applikationsspecifikke eller ikke-bærbare (fx pointergyldighed). Du kan bruge hackere som at mandat, at foruddefinerede sentinelværdier sendes også, men det er ikke ufeilbart i alle tilfælde.


Bedste praksis ville være at dokumentere tungt, udføre kodeanmeldelser og/eller skrive enhedstest med denne fejl i tankerne.