Search code examples
visual-c++assemblyfloating-pointx86masm

Read and display float or double number in assembly in Microsoft Visual Studio


I am using Microsoft Visual Studio 2015 to learn inline assembly programming. I have read a lot of posts on stackoverflow including the most relevant one this, but after I tried the methods the result is still 0.0000. I used the float first and store the value to fpu but the reuslt is the same and tried passing value to eax and still no help.

Here is my code:

#include "stdafx.h"


int _tmain(int argc, _TCHAR* argv[])
{
    char input1[] = "Enter number: \n";
    char input_format[] = "%lf";
    double afloat;
    char output[] = "The number is: %lf";


_asm {
    lea eax, input1
    push eax
    call printf_s
    add esp, 4

    lea eax, input_format
    push eax
    lea eax, afloat
    push eax
    call scanf_s
    add esp, 8

    sub esp, 8
    fstp [esp]

    lea eax, output
    push eax
    call printf
    add esp, 12

}
return 0;
}

The result:result


Solution

  • You are attempting to print the wrong value. In fact, the code should just be causing nonsense to be printed to the terminal. You got quite lucky that you see 0.0. Let's look specifically at the part of the code that retrieves the floating point value, which is your call to scanf_s:

    lea eax, input_format
    push eax
    lea eax, afloat
    push eax
    call scanf_s
    add esp, 8
    
    sub esp, 8
    fstp [esp]
    

    First of all, I don't see any logic in adding 8 to your stack pointer (esp) and then immediately subtracting 8 from the stack pointer. Performing these two operations back-to-back just cancel each other out. As such, these two instructions can be deleted.

    Second, you are pushing the arguments on the stack in the wrong order. The cdecl calling convention (used by the CRT functions, including printf_s and scanf_s) passes argument in reverse order, from right to left. Therefore, to call scanf_s, you would first push the address of the floating-point value into which the input should be stored, and then push the address of the format control string buffer. Your code is wrong, because it pushes the arguments from left to right. You get lucky with printf_s, because you're only passing one argument, but because you're passing two arguments to scanf_s, bad things happen.

    The third problem is that you appear to be assuming that scanf_s returns the output directly.

    If it did, and you had requested a floating point value, you would be correct that the cdecl calling convention would have it returning that value at the top of the floating-point stack, in FP(0), and thus you would be correctly popping that value and storing it in a memory location (fstp [esp]).

    While scanf_s does return a value (an integer value indicating the number of fields that were successfully converted and assigned), it does not return the value from the standard input stream. There is, in fact, no way that it could do this, since it supports arbitrary types of inputs. This is why it uses a pointer to a memory location to store the value. You probably knew this already, since you arranged to pass that pointer as a parameter to the function.

    Now, why did you get an output of 0.0 in the final call to printf? Because of the fstp [esp] instruction. This pops the top value off of the floating-point stack, and stores it in the memory address contained in esp. I have already pointed out that scanf_s does not place any value(s) on the floating-point stack, so technically, it contains meaningless/garbage data. But in your case, you were lucky enough that FP(0) actually contained 0.0, so that is what got printed. Why does FP(0) contain 0.0? I'm guessing because this is a debug build that you're running, and the CRT is zeroing the stack. Or maybe because that's what FSTP pops off the stack when the stack is empty. I don't know, and I don't see that documented anywhere. But it doesn't really matter what happens when you write incorrect code, because you should strive to only write correct code!

    Here's what correct code might look like:

    ; Get address of 'input1' buffer, and push it onto the stack
    ; in preparation of a call to 'printf_s'. Then, make the call.
    lea  eax, DWORD PTR [input1]
    push eax
    call printf_s
    add  esp, 4     ; clean up the stack after calling 'printf_s'
    
    ; Call 'scanf_s' to retrieve the floating-point input.
    ; Note that in the __cdecl calling convention, arguments are always
    ; passed on the stack in *reverse* order!
    lea  eax, DWORD PTR [afloat]
    push eax
    lea  eax, DWORD PTR [input_format]
    push eax
    call scanf_s
    ; (no need to clean up the stack here; we're about to reuse that space!)
    
    ; The 'scanf_s' function stored the floating-point value that the user entered
    ; in the 'afloat' variable. So, we need to load this value onto the top
    ; of the floating point stack in order to pass it to 'printf_s'
    fld  QWORD PTR [afloat]
    fstp QWORD PTR [esp]
    
    ; Get the address of the 'output' buffer and push it onto the stack,
    ; and then call 'printf_s'. Again, this is pushed last because
    ; __cdecl demands that arguments are passed right-to-left.
    lea  eax, DWORD PTR [output]
    push eax
    call printf_s
    add  esp, 12    ; clean up the stack after 'scanf_s' and 'printf_s'
    

    Note that you could optimize this code further by deferring the stack cleanup after the initial call to printf_s. You can just wait and do the cleanup later at the end of the function. Functionally, this is equivalent, but an optimizing compiler will often choose to defer stack cleanup to produce more efficient code because it can interleave it within other time-consuming instructions.

    Also note that you technically do not need the DWORD PTR directives that I've used in the code because the inline assembler (and MASM syntax in general) tries to read your mind and assemble the code that you meant to write. However, I like to write it to be explicit. It just means that the value you're loading is DWORD-sized (32 bits). All pointers on 32-bit x86 are 32 bits in size, as are int values and single-precision float values. QWORD means 64 bits, like an __int64 value or a double value.


    Warning: When I first tested this code in MSVC using inline assembly, I couldn't get it to run. It worked fine when assembled separately using MASM, but when written using inline assembly, I couldn't execute it without getting an "access violation" error. In fact, when I tried your original code, I got the same error. Initially, I couldn't figure out how you were able to get your code to run, either!

    Finally, I was able to diagnose the problem: by default, my MSVC project had been configured to dynamically link to the C runtime (CRT) library. The implementation of printf/printf_s is apparently doing some type of debug check that is causing this code to fail. I still am not entirely sure what the purpose of this validation code is, or exactly how it works, but it appears to be checking a certain offset within the stack for a sentinel value. Anyway, when I switched to statically linking to the CRT, everything runs as expected.

    At least, when I compiled the code as a "release" build. In a "debug" build, the compiler can't tell that you need floating-point support (since all the floating-point stuff is in the inline assembly, which it can't parse), so it fails to tell the linker to link in the floating-point support from the CRT. As a result, the application bombs as soon as you run it and try to use scanf_s with a floating-point value. There are various ways of fixing this, but the simplest way would be to simply explicitly initialize the afloat value (it doesn't matter what you initialize it to; 0.0 or any other value would work just fine).

    I suppose you are statically linking the CRT and running a release build, which is why your original code was executing. In that case, the code I've shown will both execute and return the correct results. However, if you're trying to learn assembly language, I strongly recommend avoiding inline assembly and writing functions directly in MASM. This is supported from within the Visual Studio environment, too; for setup instructions, see here (same for all versions).