Я новичок в сборке и пытался написать программу сборки с использованием NASM в Windows (x64) для извлечения и печати дробной части числа с плавающей запятой. Однако моя программа печатает Floating Point Part: 0.000000
вместо ожидаемого Floating Point Part: 0.500000
.
Вот мой текущий код:
section .data
fmt db "Floating Point Part: %f", 10, 0
float_value dq 10.5
result dq 0.0
section .text
global main
extern printf
main:
; Function prologue
push rbp
mov rbp, rsp
sub rsp, 32 ; Allocate shadow space
; Load floating-point value into the FPU
fld qword [rel float_value]
; Call floatVal function
call floatVal
; Store result from the FPU to memory
fstp qword [rel result]
; Print the result using printf
lea rcx, [rel fmt] ; First argument: format string
lea rdx, [rel result] ; Second argument: address of result
movsd xmm0, qword [rdx] ; Move the double to xmm0 for printf
call printf
; Function epilogue
add rsp, 32 ; Deallocate shadow space
mov eax, 0
pop rbp
ret
floatVal:
; Function prologue
push rbp
mov rbp, rsp
sub rsp, 32 ; Allocate shadow space
; Get the integer part
fld st0 ; Copy the value to the top of the stack
frndint ; Round to integer
fsub ; Subtract integer part from original value (st0 - st1)
; Function epilogue
add rsp, 32 ; Deallocate shadow space
pop rbp
ret
Этапы компиляции и выполнения:
nasm -f win64 flo.asm -o flo.o
gcc -m64 -o flo flo.o -lkernel32 -lmsvcrt
.\flo.exe
Ожидаемый результат:
Floating Point Part: 0.500000
Фактический результат:
Floating Point Part: 0.000000
Среда:
Что я пробовал:
Операции с плавающей запятой вроде бы выполняются, но результат не тот, что я ожидал. Похоже, что результат может быть перезаписан или неправильно передан в printf.
Что именно мне нужно знать
Я новичок в низкоуровневом программировании, и любая помощь или предложения будут очень признательны.
ПС: Я обнаружил те же проблемы, которые были опубликованы ранее в StackOverflow, и попытался обновить свой код, ссылаясь на них. У меня они не работают, прошу прощения за повторную публикацию.
@MartinBrown Спасибо за ваши полезные предложения и ссылки.
Согласно соглашению о вызовах , второй аргумент переходит в xmm1
, а не xmm0
. Также его следует продублировать в rdx. «Второй аргумент: адрес результата» не имеет смысла, поскольку printf
не ожидает двойного адреса. Ты должен сделать:
lea rcx, [rel fmt] ; First argument: format string
movsd xmm1, [rel result] ; Move the double to xmm1 for printf
movq rdx, xmm1 ; duplicate second argument as per convention
call printf
Также в 64-битном режиме вообще следует избегать использования x87 FPU. Листовым функциям не нужно выделять теневое пространство.
Эквивалентом SSE для frndint
является roundsd
, для которого требуется SSE4. В наши дни это не проблема, но округление до ближайшего целого числа в полностью переносимой программе x86-64, использующей только SSE2 или более раннюю версию, требует нескольких инструкций. (Особенно, если вы не хотите предполагать, что значение достаточно мало, чтобы поместиться в 64-битное целое число для преобразования в int и обратно, вам нужно добавить/подписать большую константу.)
Кроме того, объяснение 0.0
заключается в том, что вариативные функции на самом деле просто просматривают целочисленные регистры, когда доступ к аргументу не может быть оптимизирован, например. всегда рассматривайте первый аргумент с вариацией как double
(в этом случае, мы надеемся, будет использоваться регистр). Значение, которое OP оставляет в RDX, является адресом пользовательского пространства, поэтому все его старшие 16 бит равны нулю, поэтому все поле экспоненты double
равно нулю. Итак, это крошечное субнормальное целое число, и %f
округляет его до 0,0. %g
будет использовать научную запись и покажет вам крошечное ненулевое значение.
Спасибо всем за полезные предложения! Проблема решена сейчас. Ключевым моментом было использование xmm1 в качестве второго аргумента и полное исключение использования FPU x87.
@kavicastelo: Содержание XMM1 на практике не имеет значения. Что имело значение, так это копия значения в RDX. (Соглашение о вызовах требует, чтобы RDX соответствовал XMM1 для переменного аргумента FP, поэтому ответ Jester хорош, но не совсем корректно говорить, что использование XMM1 было ключом, оно копирует в соответствующий GPR, чего ваш исходный код также не делал. .)
@PeterCordes: Спасибо, сэр, за разъяснение моих недоразумений. Я новичок в сборке и, однако, ответ Шута помог мне продвинуться вперед. Я постараюсь реализовать ваши рекомендации, и если бы вы помогли мне найти ресурсы, чтобы узнать больше, я был бы очень признателен :)
@kavicastelo: в stackoverflow.com/tags/x86/info есть несколько хороших ссылок на официальную документацию и несколько руководств. Что касается понимания таких вещей, как RDX, имеющих значение на практике, это происходит из понимания смысла проектного решения, требующего, чтобы переменные аргументы XMM зеркалировались в целочисленный регистр (для них Jester связал документы MS). Это плюс теневое пространство позволяет простую реализацию функций с переменным числом аргументов путем сброса целочисленных регистров в теневое пространство, создавая непрерывный массив аргументов в стеке. Очевидно, именно это на самом деле делает MSVC при компиляции своей библиотеки.
Вот исправленный код. Возможно кому-то будет полезно :)
section .data
fmt db "Floating Point Part: %f", 10, 0
float_value dq 10.5
section .bss
result resq 1
section .text
global main
extern printf
main:
; Function prologue
push rbp
mov rbp, rsp
; Load the floating-point value into xmm0
movsd xmm0, qword [rel float_value]
; Get the fractional part
movapd xmm1, xmm0 ; Copy the value to xmm1
roundsd xmm1, xmm0, 0 ; Round to integer in xmm1
subsd xmm0, xmm1 ; Subtract integer part from xmm0
; Store the result
movsd qword [rel result], xmm0
; Prepare arguments for printf
lea rcx, [rel fmt] ; First argument: format string
movsd xmm1, qword [rel result] ; Move the double to xmm1 for printf
movq rdx, xmm1 ; Duplicate second argument as per convention
call printf
; Function epilogue
mov eax, 0
pop rbp
ret
Еще раз спасибо всем за помощь! Предложения с благодарностью приветствуются.
Рекомендации, приведенные в этом ответе соглашение о вызовах printf, могут оказаться вам полезными. Другой способ создания небольших объемов кода x87 — это MSVC в устаревшем 32-разрядном режиме x86, где вы можете легко комбинировать встроенный ассемблерный код и C printf. PS Если я не ошибся, я думаю, что вы оставляете регистр x87 со значением, все еще находящимся в стеке. Я думаю, вам нужно, чтобы fsubp st(1),st избегал проблем при многократном вызове.