Нашел багу

Немного поковырял китайский U-Boot, адаптированный для процессоров Ingenic. Помните, я писал, что там некорректно отрабатывает утилита bmp_logo?

Так вот, дело – в типично китайском коде. Как вы думаете, что напечатает программа?

int main(void){
    int l;
    FILE *in;
/* skipped some code */
    fread(&l, sizeof(uint16_t), 1, in);
    printf("l = %d\n", l);
/* skipped some code */
    return 0;
}

Естественно, что на всех более-менее современных машинах sizeof(int) > sizeof(uint16_t), то есть переменная l окажется просто неопределена – проинициализируются только первые два байта. На little-endian системах программа будет “корректно” работать, если l “автоматически” инициализируется нулем.

Несмотря на то, что в стандарте C ничего не сказано про инициализацию нулем локальных переменных, иногда такое происходит. Когда ОС выделяет память для новой программы, назначенные для нее страницы обнуляются – чтобы никакие вирусы-трояны не искали там логины-пароли. Как бы не было больно знатокам стандарта языка C, main – далеко не та функция, с которой начинается выполнение программы. Сначала вызывается библиотечная функция _start или аналогичная ей по назначению, специфичная для каждого компилятора и инициализирующая необходимые для работы стандартной библиотеки вещи (подумайте, например, как будет работать без инициализации malloc()).

Так вот, в Linux-системах можно надеяться на то, что локальные для main переменные окажутся на еще нетронутой части стека. Происходящее в Windows – гораздо более неясно, но там локальные переменные main “инициализируются” чем-то непонятным.

Кто виноват – ясно. А что делать? Нет, не надо писать int l = 0;. Хочу лишний раз напомнить о существовании big-endian систем. В них после вызова fread() шестнадцатибитное значение запишется не в два младших, а в два старших байта – и нетрудно догадаться, что мы хотели несколько другого.

К сожалению, описанные у Кернигана и Ритчи типы int, short и long имеют нерегламентированную длину. Гарантируются какие-то минимальные значения, и ничего более. int может запросто оказаться двух-, четырех- и даже восьмибайтовым. И как тут жить? Керниган и Ритчи придумали один возможный подход. Например, во всех Unix-системах принято, что “время” – это 32-битовое целое без знака. В time.h с помощью typedef определяется тип time_t, который в реальности может быть int (на 32-битных машинах) или long (на 16-битных). На 64-битной экзотике он вполне может оказаться и short. Определено несколько таких “стандартных типов” – и все.

Естественно, если подходить таким образом, то очень скоро мы начнем определять типы наподобие bmp_file_data_length_t – целое, соответствующее тому типу, в который помещается “длина данных” из bmp-файла. Не очень весело, правда? В современных реализациях C, стандарте C99 и C++0x предусмотрен заголовочный файл stdint.h, в котором определяются целые типы фиксированного размера. Например, uint16_t – это 16-битное беззнаковое целое.

Хорошим тоном считается использовать именно тот тип, который нужен в конкретном случае, особенно, когда речь идет о полях структур или других данных, размер которых четко определен. При переносе на другую платформу не придется биться с тем, что int внезапно стал вдвое длиннее :)

В общем, если бы безвестный китаец знал про типы фиксированного размера, он не стал бы использовать четырехбайтный int для хранения 16-битного значения и все были бы счастливы.

PS А вот еще вопрос. Что произойдет, если l объявить не в теле функции main(), а вне нее, как глобальную для данного файла?

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

  1. [info]soonts пишет:

    >заголовочный файл stdint.h
    Если всё что ты написал соответствует действительности, то упомянутые стандарты или херово поддерживаются, или слишком новые, или просто малоизвестны.

    У Nintendo типы называются например u8, u16 и u32.
    У Microsoft соответственно BYTE, WORD и DWORD (я кстати когда программирую под windows частенько определяю где-нить в stdafx.h ещё и QWORD, как unsigned __int64), знаковых нет.
    У Sony на PS3 кое-где используются типы вроде uint32_t, однако не из stdint.h, а из собственного header-файла.

    • > упомянутые стандарты или херово поддерживаются, или слишком новые

      С поддержкой C99 вообще весьма туго. С C++0x – тем более.

      Так или иначе, типы с фиксированной длиной предусмотрены почти во всех компиляторах. Есть несколько “кроссплатформенных” файлов stdint.h, которые можно “подкинуть” в includes.

      > а из собственного header-файла

      Случайно не inttypes.h? Это то же самое.

    • Peter Zotov пишет:

      stdint.h входит в POSIX, емнип, 2003 года и C99, но реально появился году эдак в 95-м. Проблема тут в том, что Microsoft просто всегда клала болт на стандарты, а на эмбеддеде чуть ли не вообще все городят свои костыли, потому что задача совместимости с готовым кодом, написанным под POSIX-системы в общем-то не стоит.