2014-08-27 21 views
28

Kod urywek w kernelu Linux 0.12 użyć parametru funkcyjnego tak:Dlaczego warto używać parametru funkcji "foo" w następujący sposób: * (& foo)?

int do_signal(int signr, int eax /* other parameters... */) { 
    /* ... */ 

    *(&eax) = -EINTR; 

    /* ... */ 
} 

Celem kodeksu jest umieszczenie -EINTR na pamięć, w której mieszka eax, ale nie mogę powiedzieć, dlaczego nie będzie pracować, jeśli tylko przypisanie do eax:

eax = -EINTR 

Jak kompilator zrobić różnicę między eax i * (& eax)?

+2

Czy możesz podać nam link do tego źródła jądra? lub dodaj pełną definicję funkcji (jeśli nie jest zbyt długa). –

+6

Może to uniemożliwić kompilatorowi używanie rejestru dla eaxa i zmusić go do użycia pamięci na stosie. –

+0

@ KlasLindbäck: Nice !!! Powinieneś go zapisać, ponieważ jest to prawdopodobnie poprawna odpowiedź (nazwa "eax" sugeruje, że jest to powód). –

Odpowiedz

22

Jedną z możliwych intencji może być zachowanie zmiennej eax z rejestru. Jeśli spojrzymy na C99 draft standard widzimy, że sekcja 6.5.3.2adresowa i zadnie operatorów mówi (podkr):

jednoargumentowych & operator uzyskuje adres swojego argumentu. [...] Jeśli argument jest wynikiem jednoargumentowego operator *, ani że operator ani operator & jest oceniany, a wynik jest jakby obie zostały pominięte, z wyjątkiem tego ograniczeń na operatorów nadal obowiązują a wynik nie jest lwartością [...]

w przypisie mówi (podkr przyszłości).

Tak więc, & * E jest równoważne E (nawet jeśli E jest wskaźnikiem zerowym) i & (E1 [E2]) do ((E1) + (E2)). Zawsze jest prawdą, że jeśli E jest oznaczeniem funkcji lub lwartością, która jest poprawnym operandem operatora jednoargumentowego & , * & E jest desygnatorem funkcji lub lwartości równej E.

znaleźć następujące ograniczenia w & operator:

Argument jednoargumentowych & operatora musi być funkcją oznaczenie, wynikiem [] lub jednoskładnikowa * operatora lub lwartością że oznacza obiekt, który nie jest polem bitowym i nie jest zadeklarowany z specyfikatorem klasy pamięci rejestru.

Który ma sens, ponieważ nie możemy mieć adres rejestru i tak przez wykonanie adres działania mogą mieć próbuje zapobiec kompilatora wykonywanie operacji całkowicie w rejestrach i zapewnienia, że ​​dane w określonych lokalizacjach pamięci są modyfikowane.

Jak zaznacza to, nie uniemożliwia to optymalizatorowi kompilacji, który jest skutecznie oddalony o no-op, ale jak udokumentowano w GCC hacks in the Linux kernel. Linux opierał się na wielu rozszerzeniach gcc i biorąc pod uwagę, że 0.12 jest bardzo starym jądrem, gcc może zagwarantować, że zachowanie lub może przypadkowo niezawodnie działało w ten sposób, ale nie mogę znaleźć żadnej dokumentacji, która tak mówi.

+3

+1, jest to dobra odpowiedź, ale co uniemożliwia kompilatorowi korzystanie z rejestru dla 'eax' i rozważenie' * & 'jako no-op? To może być hack dla konkretnego kompilatora. – ouah

+2

Dobra odpowiedź, w rzeczy samej. Ale operand '&' nie jest wynikiem unarnego '*' tutaj, jest odwrotnie (mamy '* & eax', a nie' & * eax') i nie mogę znaleźć analogicznej gwarancji dla '*' w standardzie. Może to częściowo odpowiada na obawy Ouah? – mafso

+0

Chcę również dodać, że brak 'register' nie mówi nic do kompilatora. Ograniczenie odnosi się do deklaracji, ale nie do miejsca, w którym wartość jest rzeczywiście umieszczona. O ile wiem, nie ma sposobu, aby zagwarantować, gdzie wartość idzie w standardowym C99, nawet samo słowo "register" jest jedynie sugestią dla kompilatora. Może jeśli adres jest rzeczywiście używany, to musi przejść do wspomnień, ale nie jestem pewien, że optymalizacja tego nie zmieni. – Malcolm

31

Stary Linux, który wysłałeś, próbował wykonać bardzo delikatne włamanie. Funkcja została określona następująco:

int do_signal(long signr,long eax,long ebx, long ecx, long edx, long orig_eax, 
    long fs, long es, long ds, 
    long eip, long cs, long eflags, 
    unsigned long * esp, long ss) 

Argumenty funkcji nie faktycznie reprezentują argumentów do funkcji (z wyjątkiem signr), ale wartości, które wywołanie funkcji (a jądro obsługi przerwań/wyjątków napisane w montażu) Konserwy stos przed wywołaniem do_signal. Instrukcja *(&eax) = -EINTR służy do modyfikowania zachowanej wartości EAX na stosie. Podobnie instrukcja *(&eip) = old_eip -= 2 służy do modyfikowania adresu zwrotnego programu wywołującego. Po zwróceniu do_signal handler wyskakuje pierwsze 9 "argumentów" ze stosu, przywracając je do nazwanych rejestrów. Następnie wykonuje instrukcję IRETD, która wyrzuca pozostałe argumenty ze stosu i powraca do trybu użytkownika.

Nie trzeba dodawać, że ten hack jest niesamowicie niewiarygodny. Jest to zależne od kodu generującego kompilator dokładnie w taki sposób, w jaki oczekiwał. Dziwię się, że nawet pracował dla kompilatora GCC z tamtej epoki, wątpię, czy było to długo przed tym, zanim GCC wprowadziła optymalizację, która go zepsuła.

+0

Jestem zaskoczony, że nawet stare wersje 'gcc' zachowują się inaczej z włączonymi opcjami eax = -EINTR' i' * (& eax) = -EINTR' i optymalizacjami. – ouah

+0

Tak, naprawdę trudno jest skompilować stare jądra w nowoczesnym systemie, ale ktoś to załatwił, zobacz [Oldlinux.org] (http: // www.oldlinux.org) – Gurity

+6

Starsze wersje GCC nie umieszczałyby rzeczy w regsiterach, jeśli wziąłeś ich adres. (Zauważ, że to zachowanie nie jest wymagane przez standard, tylko że nie możesz zadeklarować czegoś jawnie 'register' jeśli podasz jego adres.) Kompilator mógł nadal wyeliminować lub przenieść przypisanie, ponieważ jest to prosta stała i nie wszystkie ścieżki kodu z niego korzystają. –

Powiązane problemy