Ваш вопрос точен: Я просто хочу знать, почему fahr будет печатать 0,0 во всей таблице.
Как вы уже понимаете, у вас неопределенное поведение, потому что вы проходите float
, который фактически преобразуется в double
, в printf
в качестве аргумента, для которого printf
ожидает int
. Все может случиться, вы получите 0
и 0.0
в качестве выходных данных для всех строк, вы могли бы получить что-нибудь еще или хрен sh ...
Чтобы попытаться объяснить свои наблюдения, вы должен изучить, что на самом деле происходит в вашей системе для этого кода. Такой анализ требует глубоких знаний о вашей системе, ABI, компиляторе, параметрах компилятора и т. Д. c.
Я изменил ваш код и скомпилировал его с помощью Godbolt's Compiler Explorer , и вот мои наблюдения для 2 конфигураций:
g cc версия 9.3 для 64-битных Intel, с отключенной оптимизацией.
Код ошибочного printf
в 64-битном виде:
cvtss2sd xmm1, DWORD PTR [rbp-8]
cvtss2sd xmm0, DWORD PTR [rbp-4]
mov edi, OFFSET FLAT:.LC6
mov eax, 2
call printf
Код для измененного аргумента (int)celcius
, который ожидает printf
:
cvtss2sd xmm0, DWORD PTR [rbp-8]
movss xmm1, DWORD PTR [rbp-4]
cvttss2si eax, xmm1
mov esi, eax
mov edi, OFFSET FLAT:.LC6
mov eax, 1
call printf
В 64-битной версии ошибочный код передает celcius
и fahr
как double
значения в регистрах с плавающей запятой %xmm0
и %xmm1
соответственно и передает значение 2
в %eax
, тогда как правильный код будет передавать fahr
как double
в %xmm0
и celcius
преобразовано в int
в регистре %esi
, а значение 1
в %eax
.
Значение в %eax
, точнее, содержимое %al
- это число векторных регистров, используемых для передачи аргументов. Чтобы реализовать vararg api в printf
, компилятор генерирует пролог, который использует это значение для сохранения аргументов регистра в стек:
myprintf:
push rbp
mov rbp, rsp
sub rsp, 104
mov QWORD PTR [rbp-216], rdi
mov QWORD PTR [rbp-168], rsi
mov QWORD PTR [rbp-160], rdx
mov QWORD PTR [rbp-152], rcx
mov QWORD PTR [rbp-144], r8
mov QWORD PTR [rbp-136], r9
test al, al
je .L12
movaps XMMWORD PTR [rbp-128], xmm0
movaps XMMWORD PTR [rbp-112], xmm1
movaps XMMWORD PTR [rbp-96], xmm2
movaps XMMWORD PTR [rbp-80], xmm3
movaps XMMWORD PTR [rbp-64], xmm4
movaps XMMWORD PTR [rbp-48], xmm5
movaps XMMWORD PTR [rbp-32], xmm6
movaps XMMWORD PTR [rbp-16], xmm7
.L12:
Итак, printf
будет читать из [rbp-216]
int
ожидаемое значение для формата %8d
и из [rbp-128]
значение double
для fahr
. Значение int
будет соответствовать тому, что %rsi
содержится при вызове printf
, 0
по вашим наблюдениям. Значение double
должно быть тем, что было передано в xmm0
, поэтому вы ожидаете увидеть значение celcius
, и это действительно то, что я наблюдаю в своей системе. Поскольку вы наблюдаете совсем другое, есть большая вероятность, что ваша система не использует этот 64-битный ABI.
g cc версия 9.3 для 32-битных Intel с отключенной оптимизацией.
В 32-битном формате все аргументы передаются в стек. При передаче 2 float
значений мы имеем:
fld DWORD PTR [ebp-12]
fld DWORD PTR [ebp-16]
sub esp, 12
lea esp, [esp-8]
fstp QWORD PTR [esp]
lea esp, [esp-8]
fstp QWORD PTR [esp]
push OFFSET FLAT:.LC6
call printf
add esp, 32
, а при передаче int
и float
:
fld DWORD PTR [ebp-16]
movss xmm0, DWORD PTR [ebp-12]
cvttss2si eax, xmm0
lea esp, [esp-8]
fstp QWORD PTR [esp]
push eax
push OFFSET FLAT:.LC6
call printf
add esp, 16
Итак, printf
ожидает int
в [ebp+12]
и double
в [ebp+16]
, но вместо этого celcius
был выдвинут как doouble
в [ebp+12]
и fahr
в [ebp+20]
.
Считывание int
from [ebp+12]
фактически являются 4 младшими байтами значения celcius
. Поскольку значения celcius
являются небольшими целыми числами, 32 младших бита их 64-битного представления с плавающей запятой все являются нулями, следовательно, целочисленное чтение будет 0
. И наоборот, значение double
, считываемое для fahr
, смещено: первые 4 байта - это старшие 32 бита значения double
из celcius
, а последние 4 байта - младшие 32 бита double
значение fahr
, которые равны 0, потому что fahr
также является небольшим целым значением. Следовательно, в экспоненциальной части этого значения double
все биты равны нулю, поэтому это либо значение 0.0
, либо очень маленькое денормальное значение, которое преобразуется в 0.0
с форматом преобразования %5.1f
. Действительно, я получаю тот же результат, что и вы в 32-битном режиме.
Вы можете поэкспериментировать с другим форматом, например %g
для fahr
, и проверить, верен ли мой прогноз для очень маленького значения.
Конечно, это судебное исследование c имеет отношение только к определенной архитектуре c и никоим образом не оправдывается стандартом C.