вторник, 15 декабря 2015 г.

Опасность вызова функций без объявленного прототипа в 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. В данном режиме подобные ошибки становятся видны, т.к. оба модуля становятся видны, и мы можем подставить корректный вызов. Но возникает вопрос - а надо ли? Тут моё мнение разошлось с мнением более умных людей, которые считают что в режиме сборки "вся программа" нужно эмулировать ошибки обычного линкера,т.е. генерить некорректный код и ломаться тогда когда этого никто не ожидает.

В общем мораль сего поста такова - всегда объявляйте прототип вызываемой функции.

3 комментария:

  1. > Видно что мы берём возвращаемое из функции значение как int размера 4 байта
    но почем 64 битный компилятор берет int как 4 байта?

    ОтветитьУдалить
    Ответы
    1. нет, я понимаю, что 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 и совпадает с размером регистра.

      Удалить
    2. Так и должно быть. Хотя стандарт C не указывает конкретный размер типа int, есть ряд причин по которым было решено не увеличивать int.

      1. Совместимость. Часть софта просто перестала бы работать, т.к. закладывается на размер int'а в 32 бита.
      2. Производительность. Увеличение размера int'а плохо повлияло бы на подсистему памяти - пропускная способность каналов, кэш.

      Т.е. в принципе увеличение размера int никому не нужно и вызовет массу проблем. Если разработчикам нужны большие числа, они используют или long long, или соответствующие библиотеки.

      Удалить

Примечание. Отправлять комментарии могут только участники этого блога.