Печать чисел с плавающей запятой из x86-64, кажется, требует сохранения%rbp
Когда я пишу простую программу на ассемблере, связанную с библиотекой C, использую gcc 4.6.1 в Ubuntu и пытаюсь напечатать целое число, она работает нормально:
.global main
.text
main:
mov $format, %rdi
mov $5, %rsi
mov $0, %rax
call printf
ret
format:
.asciz "%10d\n"
Это печатает 5, как и ожидалось.
Но теперь, если я сделаю небольшое изменение и попытаюсь напечатать значение с плавающей запятой:
.global main
.text
main:
mov $format, %rdi
movsd x, %xmm0
mov $1, %rax
call printf
ret
format:
.asciz "%10.4f\n"
x:
.double 15.5
Эта программа обнаруживает ошибки, не печатая ничего. Просто грустный сегмент.
Но я могу это исправить, нажав и нажав %rbp
,
.global main
.text
main:
push %rbp
mov $format, %rdi
movsd x, %xmm0
mov $1, %rax
call printf
pop %rbp
ret
format:
.asciz "%10.4f\n"
x:
.double 15.5
Сейчас работает и печатает 15.5000.
У меня вопрос: зачем толкаться и трещать %rbp
заставить приложение работать? По данным ABI, %rbp
является одним из регистров, которые должен сохранить вызываемый, и так printf
не может испортить это. По факту, printf
работал в первой программе, когда только целое число было передано printf
, Значит проблема должна быть в другом месте?
1 ответ
Я подозреваю, что проблема не имеет ничего общего с %rbp
, а скорее имеет отношение к выравниванию стека. Процитирую ABI:
ABI требует, чтобы кадры стека были выровнены по 16-байтовым границам. В частности, конец области аргумента (%rbp+16) должен быть кратным 16. Это требование означает, что размер кадра должен быть дополнен до 16 байтов.
Стек выравнивается при вводе main()
, призвание printf()
помещает адрес возврата в стек, перемещая указатель стека на 8 байтов. Вы восстанавливаете выравнивание, помещая в стек еще восемь байтов (которые %rbp
но так же легко может быть что-то еще).
Вот код, который gcc
генерирует (также в проводнике компилятора Godbolt):
.LC1:
.ascii "%10.4f\12\0"
main:
leaq .LC1(%rip), %rdi # format string address
subq $8, %rsp ### align the stack by 16 before a CALL
movl $1, %eax ### 1 FP arg being passed in a register to a variadic function
movsd .LC0(%rip), %xmm0 # load the double itself
call printf
xorl %eax, %eax # return 0 from main
addq $8, %rsp
ret
Как вы можете видеть, он имеет дело с требованиями выравнивания, вычитая 8 из %rsp
в начале и добавив его обратно в конце.
Вместо этого вы можете вместо того, чтобы манипулировать регистром, нажимать / выдавливать любой регистр, который вам нравится. %rsp
непосредственно; некоторые компиляторы используют фиктивный толчок для выравнивания стека, поскольку это может быть дешевле на современных процессорах и экономит размер кода.