Search code examples
linuxloopsassemblyx86nasm

Loop iteration is failing at second iteration


Objective: I intend to create a program that appends digits from user input, convert from ASCII to number and into an array (intArray), and also get their sum all using a loop that iterates 5 times. I am very new to assembly language programming so I'm having a really difficult time debugging the program.

So far this is the code I've been able to come up with. It compiles and links successfully. After execution upon the second iteration, it fails with an error:

Enter an integer: 49
Enter an integer: 50
zsh: segmentation fault out/solution

Please what am I doing wrong?

section .bss
    input RESB 10 ; reserve 10 bytes for input

section .data
    list db 1,2,3,4,5
    prompt db "Enter an integer: ", 0
    prompt_len equ $ - prompt ; length of the prompt

    result_msg db "The sum is: ", 0
    result_msg_len equ $ - result_msg

    buffer db 12 dup(0)     ; buffer for input
    intArray dd 5 dup(0)
    arraySize db 0
    sum dd 0

section .text

global _start
_start:
    mov ecx, 5 ; loop 5 times

loop_prompt_handler:
    ; print the prompt to enter input
    mov eax, 4              ; sys_write
    mov ebx, 1              ; file descriptor 1 (stdout)
    mov ecx, prompt         ; message to print
    mov edx, prompt_len     ; number of bytes to read
    int 0x80

    ; read the input
    mov eax, 3              ; sys_read
    mov ebx, 0              ; file descriptor 0 (stdin)
    mov ecx, input          ; message to read
    mov edx, 10             ; number of bytes to read
    int 0x80

    ; convert ASCII input to integer
    call str_to_int

    ; append to array
    mov ebx, [arraySize]    ; load current size of the array
    mov edx, intArray       ; load address of the array
    add edx, ebx            ; point to the end of the array
    mov [edx], eax          ; store the input number at the end of the array

    ; increment array size
    inc byte [arraySize]    ; increment array size

    ; add the input number to the sum
    add dword [sum], eax

    loop loop_prompt_handler ; loop_prompt_handler label if ecx is not zero

    ; print the result
    mov eax, 4              ; sys_write
    mov ebx, 1              ; file descriptor 1 (stdout)
    mov ecx, result_msg     ; message to print
    mov edx, result_msg_len ; number of bytes to read
    int 0x80

    ; convert sum to string
    mov eax, [sum]
    mov ecx, buffer         ; buffer containing the sum as a string
    call int_to_string

    ; print the sum
    mov eax, 4              ; sys_write
    mov ebx, 1              ; file descriptor 1 (stdout)
    mov edx, 12             ; length of the buffer
    int 0x80

    ; exit
    mov eax, 1              ; sys_exit
    xor ebx, ebx            ; exit code
    int 0x80

str_to_int:
    ; ecx: pointer to input string
    ; eax: result
    xor eax, eax            ; clear eax
    xor edx, edx            ; clear edx

    str_to_int_loop:
        movzx edx, byte [ecx]
        test dl, dl
        jz str_to_int_end
        sub dl, '0'
        imul eax, eax, 10
        add eax, edx
        inc ecx
        jmp str_to_int_loop
    
    str_to_int_end:
        ret

int_to_string:
    ; eax contains the string to convert
    ; ecx points to the buffer to store the string
    mov edi, ecx
    add edi, 11
    mov byte [edi], 0
    dec edi

    .convert_loop:
        xor edx, edx
        mov ebx, 10
        div ebx
        add dl, '0'
        mov [edi], dl
        dec edi
        test eax, eax
        jnz .convert_loop

    inc edi
    mov ecx, edi
    ret

Solution

  • Corrupted loop counter

    The code is using the ECX register for two different purposes at the same time: the address for the buffer and the loop counter. Since you can't choose where to put the address, better pick a register like ESI for controlling the loop. In doing so, you will also have avoided the use of the slow loop instruction:

      mov  esi, 5 ; loop 5 times
    loop_prompt_handler:
      ...
      dec  esi
      jnz  loop_prompt_handler
    

    Superfluous characters

    prompt db "Enter an integer: ", 0
    prompt_len equ $ - prompt ; length of the prompt
    result_msg db "The sum is: ", 0
    result_msg_len equ $ - result_msg
    

    There's no need to zero-terminate these messages. Currently, the print service will be outputting these as a superfluous space character. Just remove the zero or keep it and deduct 1 from the length:

    prompt db "Enter an integer: "
    prompt_len equ $ - prompt
    result_msg db "The sum is: "
    result_msg_len equ $ - result_msg
    

    or:

    prompt db "Enter an integer: ", 0
    prompt_len equ $ - prompt - 1
    result_msg db "The sum is: ", 0
    result_msg_len equ $ - result_msg - 1
    

    Numerous misleading comments

    As an example, just these:

    mov edx, prompt_len     ; number of bytes to read
    ...
    mov ecx, input          ; message to read
    

    These should really read: "number of bytes to write" and "buffer to read".

    sys_read reports about the number of characters

    ; convert ASCII input to integer
    call str_to_int
    

    If you want your str_to_int to keep working from a zero-terminated string, then you'll have to zero-terminate the string yourself:

    mov  eax, 3    ; sys_read
    int  0x80      ; -> EAX
    ; Assuming no errors here, convert ASCII input to integer
    mov  byte [input + eax], 0
    call str_to_int
    

    Reading from the terminal (stdin) the count in EAX includes the linefeed (10) that ends input.

    Therefore, in your str_to_int, have the sub dl, '0' instruction followed by jb str_to_int_end so as to not erroneously treat the newline as a decimal digit.

    Wrong-size operations

    mov ebx, [arraySize]    ; load current size of the array
    mov edx, intArray       ; load address of the array
    add edx, ebx            ; point to the end of the array
    mov [edx], eax          ; store the input number at the end of the array
    inc byte [arraySize]    ; increment array size
    

    The arraySize variable is defined as a byte, therefore it is wrong to read is as a dword. Just as bad, you store a dword but only increment the variable by 1 instead of by 4.
    Two solutions:

    movzx ebx, byte [arraySize]   ; {0,4,8,12,16}
    mov   [intArray + ebx], eax
    add   byte [arraySize], 4
    

    or:

    movzx ebx, byte [arraySize]   ; {0,1,2,3,4}
    mov   [intArray + ebx * 4], eax
    inc   byte [arraySize]
    

    sys_write works length-based

    There's no need to have int_to_string zero-terminate the string. The print service will output the number of bytes specified in EDX, so you should calculate that number instead of just passing the length of the whole buffer.

    ; print the sum
    mov  eax, [sum]
    mov  ecx, buffer
    call int_to_string      ; -> ECX
    mov  edx, buffer + 12
    sub  edx, ecx
    mov  ebx, 1             ; file descriptor 1 (stdout)
    mov  eax, 4             ; sys_write
    int  0x80