Опасность вызова функций без объявленного прототипа в C
Ещё один пост про тонкости линковки. Предыдущий лежит здесь. На этот раз речь пойдёт преимущественно о старых исходниках, переносе их в 64-х битный режим, ну и немного про режим сборки «вся программа». Пример основан на реальных событиях исходниках.
В языке C в большинстве случаев допустимо делать вызов функции если в модуле не был объявлен прототип функции. Это очень плохое свойство языка, которое было оставлено для совместимости со старым софтом. Давайте для понимания сразу рассмотрим пример:
$ cat t1.c
int main()
{
int * a;
a = (int *)foo();
*a = 10;
}
$ cat t2.c
#include <stdlib.h>
int * foo(void)
{
int * a = malloc(sizeof(int));
*a = 100;
return a;
}
$ lcc t1.c t2.c -Wl,-Tdata=0x700000000
lcc: "1.c", line 5: warning: function "foo" declared implicitly
[-Wimplicit-function-declaration]
a = (int *)foo();
^
Видно что в t1.c функция foo не имеет прототипа, и что именно мы вызываем становится понятно только после линковки. Поэтому и ругается компилятор.
Сразу скажу, что используется компилятор Эльбруса, и на gcc я не смог это
воспроизвести. И это вовсе не комплимент в сторону gcc (ну или моих рук).
Опция -Wl,-Tdata=0x700000000
нужна чтобы секция данных начиналась
с больших адресов (допустимых только в 64-битном режиме). Теперь запустим
пример и получим:
$ ./a.out
Segmentation fault
Казалось бы, что тут не так? Начнём рассмотрение со строчки
a = (int *)foo();
. На первый взгляд всё корректно. Но в реальности
при сборке объектника из t1.c компилятор ничего не знает о функции foo, поэтому
подставляет прототип по умолчанию, который возвращает int
. Это
приводит к генерации следующего кода:
o7. CALL proc:foo () :4<sint32> // 't1.c' 4
o8. I2P o7:4<sint32> :8<sint32 *> // 't1.c' 4
o9. WRITE loc:a <- o8:8:(sint32 *) // 't1.c' 4
Видно что мы берём возвращаемое из функции значение как int
размера 4 байта, и приводим его к (int *)
размера 8 байт. На 32-х
битной системе это работает нормально (очевидно, что (int *)
там
тоже 4 байта). Проблемы возникают на больших адресах 64-х битного режима. Думаю
теперь становится понятно зачем была нужна опция
-Wl,-Tdata=0x700000000
. Она заставляет malloc
выдавать указатели со значениями > 2^32. Соответственно в момент преобразования
значения в int
мы теряем значимые биты, что приводит к ошибке
сегментирования.
А теперь про режим сборки «вся программа», он же
-fwhole
, он же -flto
. В данном режиме подобные ошибки
становятся видны, т.к. оба модуля становятся видны, и мы можем подставить
корректный вызов. Но возникает вопрос - а надо ли? Тут моё мнение разошлось с
мнением более умных людей, которые считают что в режиме сборки
«вся программа» нужно эмулировать ошибки обычного линкера,т.е.
генерить некорректный код и ломаться тогда когда этого никто не ожидает.
В общем мораль сего поста такова - всегда объявляйте прототип вызываемой функции.