GCC, optymalizacja i nagłówki

Pisałem już kiedyś o optymalizacji, w tym również o GCC. W pliku do którego podałem link było bardzo dużo przykładów “magii” jakiej GCC dokonuje optymalizując kod. Tym razem opiszę nie do końca pożądane działanie kompilatora na które warto zwrócić uwagę.

Przyjrzyjmy się poniższemu kawałkowi kodu:

plik.c:
[code lang=”c”]int main() {
char foo[13];
return sprintf(foo, “ABCDEFGHIJKLMNOPQRSTUVWXYZ”);
}[/code]
Jego działanie jest dość oczywiste. Najpierw deklarujemy tablicę trzynastu elementów typu char. W drugiej i ostatniej linii, funkcja sprintf przepisuje ciąg znaków do wcześniej zadeklarowanej tablicy i zwraca liczbę znaków które przepisała. Liczba ta jest jednocześnie zwracana jako efekt działania programu.

W teorii więc, program powinien jako efekt swojego działania zwrócić liczbę 26. Jednak jak łatwo zauważyć jest tak tylko w teorii. W praktyce tablica jest o połowę za mała (o połowę plus jeden bajt, nie zapominajmy o znaku końca ciągu) więc próba uruchomienia programu zakończy się błędem “Segmentation fault”. Sprawdźmy:

piotrek@moonlight ~ $ gcc plik.c
plik.c: In function ‘main’:
plik.c:4: warning: incompatible implicit declaration of built-in function ‘sprintf’
piotrek@moonlight ~ $ ./a.out
Segmentation fault

Jak widać wszystko zgodnie z założeniami. Co jednak się stanie jeśli skompilujemy kod tak jak zwykle kompiluje się programy tzn. z włączoną optymalizacją?

piotrek@moonlight ~ $ gcc -O2 plik.c
plik.c: In function ‘main’:
plik.c:4: warning: incompatible implicit declaration of built-in function ‘sprintf’
piotrek@moonlight ~ $ ./a.out
piotrek@moonlight ~ $ echo $?
26

Działa!? I co więcej, zwraca spodziewaną wartość. Aby to wyjaśnić musimy obejrzeć wynikowy kod naszego programu w assemblerze:

objdump -d --no-show-raw-insn a.out | less

i odnaleźć sekcję main, czyli kod naszego programu:

00000000004004f0 <main>:
 4004f0:       mov    $0x1a,%eax
 4004f5:       retq

Jak widać jedyne co nasz program w tej chwili robi, to ustawia rejestr eax na wartość 26, a następnie wychodzi. Podczas wychodzenia jako wartość zwracana przez funkcję, pobierana jest właśnie wartość z rejestru eax.

W procesie optymalizacji kompilator pozbył się całego wywołania funkcji sprintf. Zrobił tak ponieważ przepisywaliśmy stałą (a więc zawsze taka sama ilość liter), a GCC “wie” jakie parametry przyjmuje funkcja sprintf i jakie wartości zwraca, więc aby było szybciej nie wywołuje jej.

Można to oczywiście traktować jako błąd, jednak moim zdaniem błędny jest kod, o czym GCC informuje nas podczas obydwu kompilacji. Błędny jest, ponieważ brak w nim załączenia plików nagłówkowych które definują funkcję sprintf – stdio.h. W związku z brakiem deklaracji funkcji, GCC stara się poradzić sobie jak może czyli odwołując się do swojej “wiedzy”. Można to oczywiście wyłączyć dodając podczas kompilacji parametr “-fno-builtin”, jednak lepiej i bardziej elegancko jest poprawić nasz kod.

Po załączeniu plików nagłówkowych, niezależnie od tego który poziom optymalizacji włączymy, program normalnie odwołuje się do funkcji sprintf i wszystko działa jak powinno. Czyli znowu przestaje działać.

Comment are closed.