Search code examples
assemblyx86-64nasmmessageboxitoa

Int to string in 64x windows assembly(nasm)


I am trying to convert an integer to a string and then displaying it using the MessageBoxA function. It is successful if the number is an xero, but any other positive integer is displayed as a blank pop-up message.

default rel
global WinMain
extern ExitProcess
extern MessageBoxA
section .data
title:  db 'Melang64', 0
section .bss
buf:    resb 80
section .text
WinMain:
    sub rsp, 28h
    mov rax, buf
    mov rdi, 1
    call uitoa 
    mov rcx, 0
    lea rdx,[buf]
    lea r8,[title]
    mov r9d, 0
    call MessageBoxA
    mov  ecx,eax
    call ExitProcess
    add rsp, 28h
    hlt

uitoa:
    mov  rsi, rax
    mov  rax, rdi
    cmp  rax,0
    jnz  uitoa_convert_nrm
    mov  byte [rsi], 48
    inc esi
    mov  byte [rsi], 0
    jmp uitoa_end

uitoa_convert_nrm:
    mov r10, 10

    xor rcx, rcx
uitoa_loop:
    xor  rdx, rdx
    div  r10
    inc  ecx
    cmp  rax, 0
    jnz  uitoa_loop

    inc  ecx
    add  rsi, rcx

    mov  byte [rsi], 0
    mov  rax, rdi
    dec  ecx

uitoa_convert:
    xor rdx, rdx
    dec  rsi
    div  r10
    add  rdx, 48
    mov  byte [rsi], dl
    loopnz uitoa_convert

uitoa_end:
    ret

I am using the following program to run this code, it is a c++ program that just sends commands to the command prompt to create the obj file, link it and then run the exe:

#include <cstdlib>
#include <windows.h>
#include <iostream>
#include <string>

int main() {

    std::cout << "Hello\n";
    system("nasm -f win64 Hi.asm");

    STARTUPINFO si = { sizeof(STARTUPINFO) };
    PROCESS_INFORMATION pi;

    const wchar_t* command = L"\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.38.33130\\bin\\Hostx64\\x64\\link.exe\" Hi.obj /subsystem:windows /entry:WinMain /libpath:\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Tools\\MSVC\\14.38.33130\\lib\\x64\" /nodefaultlib msvcrt.lib /libpath:\"C:\\Program Files (x86)\\Windows Kits\\10\\Lib\\10.0.22621.0\\um\\x64\" /nodefaultlib kernel32.lib user32.lib /largeaddressaware:no";

    std::wstring writableCommand(command);

    if (CreateProcess(NULL, &writableCommand[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
        WaitForSingleObject(pi.hProcess, INFINITE);

        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else {
        std::wcerr << L"Error: " << GetLastError() << std::endl;
    }
    system("Hi.exe");
    return 0;
}

The c++ program is not the problem, however the asm part of converting the integer to the string is, I believe.

I am using nasm and I am using a windows machine. I am writing for 64 bit.

My assembly knowledge is not the best, but I tried to make sure that all the registers are correct, as well as the function calls. The program runs perfectly, it just doesn't display the number it should be, and is displaying a blank message, or buffer, in context to the code.


Solution

  • Your methode with 2 series of divisions is inefficient, but it is correct except that you put the terminating zero one byte too far. As a consequence Windows sees an empty string at the buffer that you provide.

    What you currently obtain from converting the number 1 is:

    0, '1', 0
    ^
    buf
    
    uitoa:
        mov  rsi, rax
        mov  rax, rdi
        cmp  rax,0
        jnz  uitoa_convert_nrm
        mov  byte [rsi], 48
        inc esi
        mov  byte [rsi], 0
        jmp uitoa_end
    
    uitoa_convert_nrm:
        mov r10, 10
    
        xor rcx, rcx
    uitoa_loop:
        xor  rdx, rdx
        div  r10
        inc  ecx
        cmp  rax, 0
        jnz  uitoa_loop
    
        inc  ecx                   <<<<<<< Remove this line!
        add  rsi, rcx
    
        mov  byte [rsi], 0
        mov  rax, rdi
        dec  ecx                   <<<<<<< Remove this line too!
    
    uitoa_convert:                 RCX is at least 1 at this point
        xor rdx, rdx
        dec  rsi
        div  r10
        add  rdx, 48
        mov  byte [rsi], dl
        loopnz uitoa_convert       <<<<<<< Just use LOOP (still inefficient)
    
    uitoa_end:
        ret
    

    An alternative conversion routine for signed 64-bit integers follows. If you want it unsigned then simply remove the five lines that I've marked with ||.

    ; IN (rcx,rdx) OUT (rax) MOD (rcx,r8,r9)
    ; RCX is the signed integer
    ; RDX is the buffer address
    IntToStr:
      push  rdx              ; (1)
      mov   r9, rdx
      sub   rsp, 24          ; At most 19 digits
      mov   rax, rsp
      xchg  rax, rcx         ; -> RAX is 64-bit integer, RCX is temp buffer (stack)
      mov   r8d, 10          ; CONST R8 = 10
    
      test  rax, rax              ||
      jns   .more                 ||
      mov   byte [rdx], '-'       ||
      inc   r9                    ||
      neg   rax                   ||
    
    .more:
      xor   edx, edx
      div   r8               ; RDX:RAX / R8
      add   edx, '0'
      mov   [rcx], dl
      inc   rcx
      test  rax, rax
      jnz   .more
    
    .copy:
      dec   rcx
      movzx eax, byte [rcx]
      mov   [r9], al
      inc   r9
      cmp   rcx, rsp
      ja    .copy
      mov   byte [r9], 0     ; Zero-termination
      add   rsp, 24
    
      mov   rax, r9
      pop   rdx              ; (1)
      sub   rax, rdx         ; -> RAX is number of characters, but
      ret                    ;    not including the terminating zero
                             ;    Can be omitted