Почему я могу печатать с неправильным спецификатором и все еще получать вывод? - PullRequest
0 голосов
/ 15 марта 2020

Мой вопрос касается структуры памяти и механизмов, стоящих за функцией C printf(). Скажем, у меня есть следующий код:

#include <stdio.h>

int main()
{
    short m_short;
    int m_int;

    m_int = -5339876;

    m_short = m_int;
    printf("%x\n", m_int);
    printf("%x\n", m_short);

    return 0;
}

На G CC 7.5.0 эта программа выводит:

ffae851c
ffff851c

Мой вопрос: откуда на самом деле приходит ffff во втором шестнадцатеричном номере? Если я прав, эти fs должны быть за пределами короткого замыкания, но printf откуда-то их получает.

Когда я правильно форматирую с помощью спецификатора %hx, вывод будет правильным:

ffae851c
851c

Насколько я изучил, компилятор просто усекает верхнюю половину числа, как показано во втором выводе. Итак, в первом выводе первые четыре f из программы действительно читают в память, чего не должно быть? Или закулисный компилятор C все еще резервирует полное целое число даже для короткого, расширенного знака, но старшая половина должна быть неопределенным поведением, если используется?

Примечание: я выполняю исследование в реальном приложении я бы никогда не стал злоупотреблять языком.

1 Ответ

3 голосов
/ 15 марта 2020

Когда char или short (включая версии со знаком и без знака) используется в качестве аргумента функции, где нет определенного типа c (как с аргументами ... для printf(format,...)) 1 , он автоматически повышается до int (при условии, что он уже не такой широкий, как int 2 ).

Так что printf("%x\n", m_short); имеет int аргумент Какова ценность этого аргумента? В присваивании m_short = m_int; вы попытались присвоить ему значение -5339876 (представлено байтами 0xffae851 c). Тем не менее, −5339876 не поместится в этот 16-разрядный код. В назначениях преобразование выполняется автоматически, и, когда преобразование целого числа в целочисленный тип со знаком не подходит, результат определяется реализацией. Похоже, что ваша реализация, как и многие другие, использует дополнение до двух и просто берет младшие биты целого числа. Таким образом, он помещает байты 0x851 c в m_short, представляющие значение -31460.

Напомним, что это повышается до int для использования в качестве аргумента printf. В этом случае это соответствует int, поэтому результат все еще равен -31460. В двоичном дополнении int это представлено байтами 0xffff851 c.

Теперь мы знаем, что передается в printf: int с байтами 0xffff851 c, представляющими значение -31460. Однако вы печатаете его с %x, который должен получить unsigned int. При таком несоответствии поведение не определяется стандартом C. Тем не менее, это сравнительно небольшое несоответствие, и многие реализации C позволяют ему скользить. (G CC и Clang не предупреждают даже с -Wall.)

Предположим, что ваша реализация C не рассматривает printf как специальную известную функцию и просто генерирует код для вызова, как вы написал это, и что вы позже свяжете эту программу с библиотекой C. В этом случае компилятор должен передать аргумент в соответствии со спецификацией двоичного интерфейса приложения (ABI) для вашей платформы. (ABI определяет, помимо прочего, как аргументы передаются в функции.) Чтобы соответствовать ABI, компилятор C поместит адрес строки формата в одном месте, а биты int в другом, и затем он вызовет printf.

Подпрограмма printf прочитает строку формата, см. %x и ищет соответствующий аргумент, который должен быть unsigned int. В каждой реализации C и ABI, о которых я знаю, int и unsigned int передаются в одном и том же месте . Это может быть регистр процессора или место в стеке. Допустим, это в регистре r13. Таким образом, компилятор спроектировал вашу вызывающую подпрограмму для помещения int с байтами 0xffff851 c в r13, а подпрограмма printf искала unsigned int в r13 и нашла байты 0xffff851 c.

В результате printf интерпретирует байты 0xffff851 c, как если бы они были unsigned int, форматирует их с %x и печатает «ffff851c».

По сути, вам это сойдет с рук потому что (a) short повышен до int, который имеет тот же размер, что и unsigned int, которого ожидал printf, и (b) большинство реализаций C не являются строгими в отношении несовпадения целочисленных типов такой же ширины с printf. Если бы вы вместо этого попытались напечатать int, используя %ld, вы могли бы получить другие результаты, такие как «мусорные» биты в старших битах напечатанного значения. Или у вас может быть случай, когда переданный вами аргумент должен находиться в совершенно другом месте, чем ожидаемый printf, поэтому ни один из битов не является правильным. В некоторых архитектурах неправильная передача аргументов может привести к повреждению стека и разрушению программы различными способами.

Сноски

1 Это автоматическое продвижение c происходит во многих и другие выражения.

2 Существуют некоторые технические детали относительно этих автоматических c целочисленных рекламных акций, которые не должны касаться нас в данный момент.

...