Ten kod źródłowy włącza ciąg w C. Jak to robi?

106

Czytam kod emulatora i odpowiedziałem na coś naprawdę dziwnego:

switch (reg){
    case 'eax':
    /* and so on*/
}

Jak to jest możliwe? Myślałem, że możesz tylko switchna typach całkowitych. Czy ma miejsce jakieś sztuczki makro?

Ian Colton
źródło
29
to nie jest ciąg znaków 'eax'i wylicza stałą wartość całkowitą
P__J__
12
Pojedyncze cudzysłowy, a nie podwójne. Stała znakowa jest promowana do int, więc jest to legalne. Jednak wartość stałej wieloznakowej jest zdefiniowana w ramach implementacji, więc kod może nie działać zgodnie z oczekiwaniami na innym kompilatorze. Na przykład, eaxmoże być 0x65, 0x656178, 0x65617800, 0x786165, 0x6165, lub coś innego.
Davislor
2
@Davislor: biorąc pod uwagę nazwę zmiennej „reg” i fakt, że eax jest rejestrem x86, przypuszczam, że zachowanie zdefiniowane w implementacji miało być OK, ponieważ jest takie samo wszędzie, gdzie jest używane w kodzie. 'eax' != 'ebx'Oczywiście pod warunkiem , że zawodzi tylko jeden lub dwa z twoich przykładów. Chociaż może być gdzieś jakiś kod, który w efekcie zakłada *(int*)("eax") == 'eax', a zatem zawodzi większość twoich przykładów.
Steve Jessop
2
@SteveJessop Nie zgadzam się z tym, co mówisz, ale istnieje realne niebezpieczeństwo, że ktoś mógłby spróbować skompilować kod na innym kompilatorze, nawet dla tej samej architektury, i uzyskać inne zachowanie. Na przykład 'eax'może porównać równa się 'ebx'lub do 'ax', a instrukcja switch nie zadziała zgodnie z zamierzeniami.
Davislor 08
1
Cała ta tajemnica została szybko rozwiązana, gdybyś sprawdził / pokazał nam typ danych reg.
tys

Odpowiedzi:

146

(Tylko Ty możesz odpowiedzieć na część „sztuczki makr” - chyba że wkleisz więcej kodu. Ale nie ma tu zbyt wiele do pracy z makrami - formalnie nie możesz przedefiniować słów kluczowych ; zachowanie przy robieniu tego jest niezdefiniowane).

Aby uzyskać czytelność programu, dowcipny programista wykorzystuje określone w implementacji zachowanie . 'eax'to nie łańcuch, ale stała wieloznakowy . Zwróć szczególną uwagę na pojedyncze cudzysłowy eax. Najprawdopodobniej daje ci to intw twoim przypadku unikalne dla tej kombinacji postaci. (Dość często każdy znak zajmuje 8 bitów w 32-bitowym int). I każdy wie, że może switchna zasadzie int!

Na koniec standardowe odniesienie:

Standard C99 mówi:

6.4.4.4p10: „Wartość stałej znakowej liczby całkowitej zawierającej więcej niż jeden znak (np.„ Ab ”) lub zawierającej znak lub sekwencję ucieczki, która nie jest odwzorowywana na jednobajtowy znak wykonania, jest zdefiniowana w ramach implementacji. "

Batszeba
źródło
55
Na wypadek, gdyby ktoś to zobaczył i wpadł w panikę, wymagane jest, aby „zdefiniowane w implementacji” działało i zostało udokumentowane przez kompilator w odpowiedni sposób (standard nie wymaga, aby zachowanie było intuicyjne lub aby dokumentacja była dobra, ale ...). Jest to „bezpieczne” w użyciu dla programisty, który w pełni rozumie, co pisze, w przeciwieństwie do „nieokreślonego”.
Leushenko
7
@Justin Chociaż mogłoby, byłoby to dość perwersyjne. Jeśli nie robi tego, co najprawdopodobniej sugeruje odpowiedź, następną możliwością jest prawdopodobnie to, że używa tylko pierwszego znaku i ignoruje resztę.
Barmar
5
@ZanLynx Nie jestem pewien, ale uważam, że ta funkcja jest już dawna starsza niż Unicode i inne standardy MBCS. Pierwsze znane mi aplikacje to „magiczne liczby”, które wyglądają jak tekst w zrzutach pamięci i identyfikatory fragmentów formatu plików w stylu RIFF.
Russell Borogove
16
@ jpmc26 To nie jest niezdefiniowane zachowanie, jest zdefiniowane w implementacji. Więc jeśli dokumentacja kompilatora nie wspomina o demonach, twój nos jest bezpieczny.
Barmar
7
@ZanLynx: Obawiam się, że pierwotna intencja wyprzedza Unicode, UTF-8 i wszelkie wielobajtowe kodowanie znaków o prawie 20 lat. Stałe wieloznakowe były po prostu wygodnym sposobem wyrażania liczb całkowitych reprezentujących grupy po 2, 3 lub 4 bajty (w zależności od wielkości bajtów i int). Niespójności między implementacjami i architekturami skłoniły komisję do zadeklarowania tego jako zdefiniowanego w implementacji , co oznacza, że ​​nie ma przenośnego sposobu obliczenia wartości 'ab'from 'a'i 'b'.
chqrlie
45

Zgodnie ze standardem C (6.8.4.2 Instrukcja dotycząca przełącznika)

3 Wyrażenie każdej etykiety przypadku powinno być wyrażeniem stałym będącym liczbą całkowitą ...

i (6.6 Wyrażenia stałe)

6 Wyrażenie stałe typu integer ma typ całkowity i może zawierać tylko operandy będące stałymi liczbami całkowitymi, stałymi wyliczania, stałymi znakowymi , wielkością wyrażeń, których wynikiem są stałe całkowite, oraz stałe zmiennoprzecinkowe, które są bezpośrednimi operandami rzutów. Operatory rzutowania w wyrażeniu stałym typu integer powinny konwertować tylko typy arytmetyczne na typy całkowite, z wyjątkiem operacji jako części operandu na operator sizeof.

Co teraz jest 'eax'?

Standard C (6.4.4.4 Stałe znakowe)

2 Stała znakowa będąca liczbą całkowitą jest sekwencją jednego lub więcej znaków wielobajtowych ujętych w apostrofy , na przykład „x” ...

Więc 'eax'jest stałą znakową liczbą całkowitą zgodnie z paragrafem 10 tej samej sekcji

  1. ... Wartość stałej znakowej liczby całkowitej zawierającej więcej niż jeden znak (np. „Ab”) lub zawierającej znak lub sekwencję ucieczki, która nie jest odwzorowywana na jednobajtowy znak wykonania, jest zdefiniowana w ramach implementacji.

Zatem zgodnie z pierwszym cytatem może to być operand wyrażenia będącego liczbą całkowitą, która może być używana jako etykieta przypadku.

Zwróć uwagę, że stała znakowa (ujęta w pojedyncze cudzysłowy) ma typ inti nie jest tym samym, co literał łańcuchowy (sekwencja znaków ujęta w podwójne cudzysłowy), która ma typ tablicy znaków.

Vlad z Moskwy
źródło
12

Jak powiedzieli inni, jest to intstała, a jej rzeczywista wartość jest określona przez implementację.

Zakładam, że reszta kodu wygląda mniej więcej tak

if (SOMETHING)
    reg='eax';
...
switch (reg){
    case 'eax':
    /* and so on*/
}

Możesz być pewien, że „eax” w pierwszej części ma taką samą wartość jak „eax” w drugiej części, więc wszystko działa, prawda? ... źle.

W komentarzu @Davislor wymienia kilka możliwych wartości dla „eax”:

... 0x65, 0x656178, 0x65617800, 0x786165, 0x6165, lub coś innego

Zwróć uwagę na pierwszą potencjalną wartość? To po prostu 'e'ignorowanie pozostałych dwóch znaków. Problemem jest program prawdopodobnie używa 'eax', 'ebx'i tak dalej. Jeśli wszystkie te stałe mają taką samą wartość, 'e'jaką otrzymujesz

switch (reg){
    case 'e':
       ...
    case 'e':
       ...
    ...
}

To nie wygląda zbyt dobrze, prawda?

Zaletą „definicji implementacji” jest to, że programista może sprawdzić dokumentację swojego kompilatora i zobaczyć, czy robi coś sensownego z tymi stałymi. Jeśli tak, dom za darmo.

Złe jest to, że jakiś inny biedak może wziąć kod i spróbować skompilować go przy użyciu innego kompilatora. Natychmiastowy błąd kompilacji. Program nie jest przenośny.

Jak @zwol zauważył w komentarzach, sytuacja nie jest tak zła, jak myślałem, w złym przypadku kod się nie kompiluje. To przynajmniej poda dokładną nazwę pliku i numer wiersza dla problemu. Mimo to nie będziesz miał działającego programu.

Stig Hemmer
źródło
1
poza jakąś formą assert('eax' != 'ebx'); //if this fails you can't compile the code because...jest coś, co pierwotny autor mógłby zrobić, aby zapobiec innym awariom kompilatora bez całkowitego zastąpienia konstrukcji>
Dan Is Fiddling By Firelight
6
Dwie etykiety przypadków o tej samej wartości stanowią naruszenie ograniczenia (6.8.4.2p3: „... żadne dwa wyrażenia stałej wielkości przypadku w tej samej instrukcji przełączającej nie powinny mieć tej samej wartości po konwersji”), tak długo, jak cały kod traktuje wartości tych stałych jako nieprzezroczyste, co gwarantuje działanie lub niepowodzenie kompilacji.
zwol
Najgorsze jest to, że biedny kolega kompilujący na innym kompilatorze prawdopodobnie nie zobaczy żadnego błędu podczas kompilacji (włączenie ints jest w porządku); zamiast tego pojawią się błędy czasu wykonania ...
tucuxi
1

Fragment kodu wykorzystuje historyczną osobliwość zwaną wieloznakową stałą znakową, nazywaną również wieloznakową .

'eax' jest stałą całkowitą, której wartość jest zdefiniowana w implementacji.

Oto interesująca strona na temat wielu znaków i tego, jak można ich używać, ale nie powinno:

http://www.zipcon.net/~swhite/docs/computers/languages/c_multi-char_const.html


Patrząc dalej w lusterko wsteczne, oto jak oryginalny podręcznik C autorstwa Dennisa Ritchiego z dawnych dobrych czasów ( https://www.bell-labs.com/usr/dmr/www/cman.pdf ) określał stałe znakowe .

2.3.2 Stałe znakowe

Stała znakowa to 1 lub 2 znaki ujęte w pojedyncze cudzysłowy '' '''. W obrębie stałej znakowej pojedynczy cudzysłów musi być poprzedzony ukośnikiem „ \” ”. Niektóre znaki nie będące grafiką i sam „” \”mogą zostać zmienione zgodnie z następującą tabelą:

    BS \b
    NL \n
    CR \r
    HT \t
    ddd \ddd
    \ \\

Ucieczka '' \ddd'' składa się z ukośnika odwrotnego, po którym następuje 1, 2 lub 3 cyfry ósemkowe, które są brane do określenia wartości żądanego znaku. Szczególnym przypadkiem tej konstrukcji jest '' \0'' (bez cyfry), który wskazuje na znak null.

Stałe znakowe zachowują się dokładnie jak liczby całkowite (nie w szczególności jak obiekty typu znakowego). Zgodnie ze strukturą adresowania PDP-11, stała znakowa o długości 1 ma kod dla danego znaku w bajcie najniższego rzędu i 0 w bajcie wyższego rzędu; Stała znakowa o długości 2 ma kod pierwszego znaku w młodszym bajcie i kodu drugiego znaku w bajcie wyższego rzędu. Stałe znakowe z więcej niż jednym znakiem są z natury zależne od maszyny i należy ich unikać.

Ostatnia fraza to wszystko, co musisz pamiętać o tej dziwnej konstrukcji: Stałe znakowe z więcej niż jednym znakiem są z natury zależne od maszyny i należy ich unikać.

chqrlie
źródło