NASM x64: часть с плавающей запятой неправильно напечатана как 0,000000

Я новичок в сборке и пытался написать программу сборки с использованием 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

Среда:

  • Виндовс 10 х64
  • НАСМ
  • Мингв-w64

Что я пробовал:

  • Гарантировано, что операции с плавающей запятой обрабатываются правильно.
  • Проверил, что стек правильно выровнен и выделено теневое пространство.
  • Попытка напечатать все значение с плавающей запятой напрямую, чтобы изолировать проблему.
  • Использована адресация относительно RIP для обеспечения правильной обработки адресов.

Операции с плавающей запятой вроде бы выполняются, но результат не тот, что я ожидал. Похоже, что результат может быть перезаписан или неправильно передан в printf.

Что именно мне нужно знать

  • Почему программа печатает 0,000000 вместо ожидаемой дробной части?
  • Есть ли какие-либо ошибки в соглашениях об обработке чисел с плавающей запятой или вызове функций?
  • Как правильно напечатать дробную часть числа с плавающей запятой?

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

ПС: Я обнаружил те же проблемы, которые были опубликованы ранее в StackOverflow, и попытался обновить свой код, ссылаясь на них. У меня они не работают, прошу прощения за повторную публикацию.

Рекомендации, приведенные в этом ответе соглашение о вызовах printf, могут оказаться вам полезными. Другой способ создания небольших объемов кода x87 — это MSVC в устаревшем 32-разрядном режиме x86, где вы можете легко комбинировать встроенный ассемблерный код и C printf. PS Если я не ошибся, я думаю, что вы оставляете регистр x87 со значением, все еще находящимся в стеке. Я думаю, вам нужно, чтобы fsubp st(1),st избегал проблем при многократном вызове.

Martin Brown 29.07.2024 14:22

@MartinBrown Спасибо за ваши полезные предложения и ссылки.

kavi castelo 29.07.2024 21:36
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
66
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Согласно соглашению о вызовах , второй аргумент переходит в 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 и обратно, вам нужно добавить/подписать большую константу.)

Peter Cordes 29.07.2024 19:36

Кроме того, объяснение 0.0 заключается в том, что вариативные функции на самом деле просто просматривают целочисленные регистры, когда доступ к аргументу не может быть оптимизирован, например. всегда рассматривайте первый аргумент с вариацией как double (в этом случае, мы надеемся, будет использоваться регистр). Значение, которое OP оставляет в RDX, является адресом пользовательского пространства, поэтому все его старшие 16 бит равны нулю, поэтому все поле экспоненты double равно нулю. Итак, это крошечное субнормальное целое число, и %f округляет его до 0,0. %g будет использовать научную запись и покажет вам крошечное ненулевое значение.

Peter Cordes 29.07.2024 19:40

Спасибо всем за полезные предложения! Проблема решена сейчас. Ключевым моментом было использование xmm1 в качестве второго аргумента и полное исключение использования FPU x87.

kavi castelo 29.07.2024 21:42

@kavicastelo: Содержание XMM1 на практике не имеет значения. Что имело значение, так это копия значения в RDX. (Соглашение о вызовах требует, чтобы RDX соответствовал XMM1 для переменного аргумента FP, поэтому ответ Jester хорош, но не совсем корректно говорить, что использование XMM1 было ключом, оно копирует в соответствующий GPR, чего ваш исходный код также не делал. .)

Peter Cordes 30.07.2024 12:34

@PeterCordes: Спасибо, сэр, за разъяснение моих недоразумений. Я новичок в сборке и, однако, ответ Шута помог мне продвинуться вперед. Я постараюсь реализовать ваши рекомендации, и если бы вы помогли мне найти ресурсы, чтобы узнать больше, я был бы очень признателен :)

kavi castelo 30.07.2024 13:25

@kavicastelo: в stackoverflow.com/tags/x86/info есть несколько хороших ссылок на официальную документацию и несколько руководств. Что касается понимания таких вещей, как RDX, имеющих значение на практике, это происходит из понимания смысла проектного решения, требующего, чтобы переменные аргументы XMM зеркалировались в целочисленный регистр (для них Jester связал документы MS). Это плюс теневое пространство позволяет простую реализацию функций с переменным числом аргументов путем сброса целочисленных регистров в теневое пространство, создавая непрерывный массив аргументов в стеке. Очевидно, именно это на самом деле делает MSVC при компиляции своей библиотеки.

Peter Cordes 30.07.2024 21:31

Вот исправленный код. Возможно кому-то будет полезно :)

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

Еще раз спасибо всем за помощь! Предложения с благодарностью приветствуются.

Другие вопросы по теме