Ещё один пост про тонкости линковки. Предыдущий лежит здесь. На этот раз речь пойдёт преимущественно о старых исходниках, переносе их в 64-х битный режим, ну и немного про режим сборки "вся программа". Пример основан на реальных событиях исходниках.
В языке C в большинстве случаев допустимо делать вызов функции если в модуле не был объявлен прототип функции. Это очень плохое свойство языка, которое было оставлено для совместимости со старым софтом. Давайте для понимания сразу рассмотрим пример:
В языке 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();
^
Сразу скажу, что используется компилятор Эльбруса, и на 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=0x
700000000
. Она заставляет malloc выдавать указатели со значениями > 2^32. Соответственно в момент преобразования значения в int мы теряем значимые биты, что приводит к ошибке сегментирования.А теперь про режим сборки "вся программа", он же -fwhole, он же -flto. В данном режиме подобные ошибки становятся видны, т.к. оба модуля становятся видны, и мы можем подставить корректный вызов. Но возникает вопрос - а надо ли? Тут моё мнение разошлось с мнением более умных людей, которые считают что в режиме сборки "вся программа" нужно эмулировать ошибки обычного линкера,т.е. генерить некорректный код и ломаться тогда когда этого никто не ожидает.
В общем мораль сего поста такова - всегда объявляйте прототип вызываемой функции.
> Видно что мы берём возвращаемое из функции значение как int размера 4 байта
ОтветитьУдалитьно почем 64 битный компилятор берет int как 4 байта?
нет, я понимаю, что int это "Basic signed integer type. Capable of containing at least the [−32767, +32767] range;[3] thus, it is at least 16 bits in size." но почему его не сделали 64 битным? на 32 битных он же на 16 бит, а 32 и совпадает с размером регистра.
УдалитьТак и должно быть. Хотя стандарт C не указывает конкретный размер типа int, есть ряд причин по которым было решено не увеличивать int.
Удалить1. Совместимость. Часть софта просто перестала бы работать, т.к. закладывается на размер int'а в 32 бита.
2. Производительность. Увеличение размера int'а плохо повлияло бы на подсистему памяти - пропускная способность каналов, кэш.
Т.е. в принципе увеличение размера int никому не нужно и вызовет массу проблем. Если разработчикам нужны большие числа, они используют или long long, или соответствующие библиотеки.