2

Is there a way that I'm compiling the same assembly code, on the same server, and get different outputs on different runs for a program that should produce deterministic results? If so, what could be possible reasons?

My program receives some input from the users and do simple actions on strings (compare, swap case, etc..)

In order to know which function to run, along with the strings and their lengths, I'm getting an option for a switch case statement and jumping to the relevant function according to the number the user enters. but when running the code with the same inputs I'm getting most of the time the expected result, and on some runs the "default" option.

How could this happen? The result is non-deterministic. I've tried to debug but I couldn't find the RC. Any suggestion would be appreciated.

getting user input code:

    .section    .rodata

input_int:     .string "%d%*c"      # take the int number and clear the buffer from \n for the next inputs
input_string:  .string "%[^\n]%*c"  # get everything until reaching \n

    .text

# this function receives from the user 2 strings and their lengths, and an option for the switch statement
.globl run_main
    .type run_main, @function

run_main:
    pushq   %rbp                # save the old frame pointer
    movq    %rsp, %rbp          # create the new frame pointer
    addq    $-528, %rsp         # allocating 528 bytes (256 for string1+ length, same for 2, and 4 for the int and align it to 16)

    movq    $input_int, %rdi    # get string1 length- passing the scanf format to scanf
    leaq    -256(%rbp), %rsi    # passing the location to put string1 lengh in
    movq    $0, %rax
    call    scanf
    movq    $input_string, %rdi # get string1- passing the scanf format to scanf
    leaq    -255(%rbp), %rsi    # passing the location to put string1 in
    movq    $0, %rax
    call    scanf

    movq    $input_int, %rdi    # get string2 length- passing the scanf format to scanf
    leaq    -512(%rbp), %rsi    # passing the location to put string2 length in
    movq    $0, %rax
    call    scanf
    movq    $input_string, %rdi # get string2 length- passing the scanf format to scanf
    leaq    -511(%rbp), %rsi    # passing the location to put string2 in
    movq    $0, %rax
    call    scanf

    movq    $input_int, %rdi    # get the option from the user- passing the format to scanf
    leaq    -528(%rbp), %rsi    # passing
    movq    $0, %rax
    call    scanf

    movq    -528(%rbp), %rdi    # passing to run_func option as the first parameter (get its value)
    leaq    -256(%rbp), %rsi    # passing to run_func string1 as the second parameter
    leaq    -512(%rbp), %rdx    # passing to run_func the string2 as the third parameter
    call    run_func

    leave
    ret

switch case code:

    .section    .rodata # read only data section
invalid:        .string "invalid option!\n"
input_char:     .string "%c%*c"
input_int:      .string "%d%*c"
print_length:   .string "first pstring length: %d, second pstring length: %d\n"
print_replace:  .string "old char: %c, new char: %c, first string: %s, second string: %s\n"
print_pstring:  .string "length: %d, string: %s\n"
print_compare:  .string "compare result: %d\n"

    .align 8

.Switch:    # start switch case here
    .quad .Case31    # Case 31 
    .quad .Case32    # Case 32
    .quad .Case32    # Case 33
    .quad .Default   # default case (no 34 case)
    .quad .Case35    # Case 35
    .quad .Case36    # Case 36
    .quad .Case37    # Case 37

    .text   # the beginnig of the code
# this function runs the wanted function accordding to the switch case selection.
.globl run_func
    .type  run_func, @function
run_func:
    pushq   %rbp                   # save the old frame pointer
    movq    %rsp, %rbp             # create the new frame pointer  
    subq    $32, %rsp              # allocate 16 bytes in the stack

    movq    %rsi, -8(%rbp)         # put string1 in the stack
    movq    %rdx, -16(%rbp)        # put string2 in the stack   

    # set the jump table access
    addq    $-31,%rdi              # compute xi = x-31 to start from 0 in the jump table
    cmpq    $6, %rdi               # compare xi : 6 (31<=x<=37)
    ja      .Default               # if >, goto default case (for negative numbers the cmp will be in unsigned (very big number for negatives becaue MSB on))      
    jmp     *.Switch(,%rdi,8)      # goto jump table at xi

# Case 36
.Case36:
    movq    -8(%rbp), %rdi
    call    swapCase
    movq    -8(%rbp), %rdi         # passing string1 to pstrlen to get the length
    call    pstrlen
    movq    $print_pstring, %rdi   # passing the print format to printf
    movq    %rax, %rsi             # passing the result of pstrlen to printf
    movq    -8(%rbp), %rdx         # passing the string1 to printf 
    incq    %rdx                   # add 1 to rdx to get the beggining of the string   
    movq    $0, %rax
    call    printf
    
    movq    -16(%rbp), %rdi
    call    swapCase
    movq    -16(%rbp), %rdi        # passing string2 to pstrlen to get the length
    call    pstrlen
    movq    $print_pstring, %rdi   # passing the print format to printf
    movq    %rax, %rsi             # passing the result of pstrlen to printf
    movq    -16(%rbp), %rdx        # passing the string2 to printf 
    incq    %rdx                   # add 1 to rdx to get the beggining of the string   
    movq    $0, %rax
    call    printf
    jmp .Done # goto done

# Default case
.Default:
    movq    $invalid, %rdi         # passing the invalid input string to printf
    movq    $0,%rax    
    call    printf
    jmp .Done # goto done

# return
.Done:    # done
    leave
    ret

the pstrlen func:

# this function receives a pointer to a string and returns its length
.globl  pstrlen
    .type   pstrlen, @function
pstrlen:
    pushq   %rbp            # save the old frame pointer
    movq    %rsp, %rbp      # create the new frame pointer
    movzbq  (%rdi), %rax    # return the first byte of the string in which the length provided
    leave
    ret

swap case func:

# this function receives: pointer to string, and replace each lowercase letter with its uppercase compatible and vice versa.
.globl  swapCase
    .type   swapCase, @function
swapCase:
    pushq   %rbp            # save the old frame pointer
    movq    %rsp, %rbp      # create the new frame pointer
.StartSwap:
    incq    %rdi            # increase the received addres by 1 to get the beggining of the string (the first one is length)
    cmpb    $0, (%rdi)      # check if the first char of the string is 0
    je .DoneSwap
    cmpb    $0x41, (%rdi)   # comare the first byte in ths string to A (ascii value in hexa is 41)
    jl .StartSwap           # if the char value is less than A value - its not a letter and we can continue without changing anything
    cmpb    $0x7A, (%rdi)   # comare the first byte in ths string to z (ascii value in hexa is 7A)
    jg  .StartSwap          # if the char value is greater z value - its not a letter and we can continue without changing anything
    cmpb    $0x61, (%rdi)   # compare to a
    jge .ToUpper            # if the value is greater/equal to a, its a small letter
    cmpb    $0x5A, (%rdi)   # compare to Z
    jle .ToLower            # if the value is less/equal to Z, its a big letter
    jmp .StartSwap

.ToUpper:    
    subb    $32, (%rdi)     # the difference between the lower case values and the upper cale values is 32
    jmp .StartSwap

.ToLower:
    addb    $32, (%rdi)     # add 32 to get lower case value
    jmp .StartSwap

.DoneSwap:
    movq    %rdi, %rax
    leave
    ret

Edit: running the code above with the following input:

5
hello
5
world
36

the expected output:

length: 5 string: HELLO
length: 5 string: WORLD

actual result: sometimes the expected result and sometimes the following output (which is default)

Invalid option!
Daniel
  • 1,895
  • 9
  • 20
  • 2
    `.quad .Case32 # Case 33` looks like a bug, but that would be deterministic. Can you make a smaller [mcve]? This seems like too much code. Can you show a specific example of an input which sometimes does what you want and sometimes doesn't? – Peter Cordes Dec 22 '22 at 22:02
  • 2
    Non-deterministic behaviour could happen if you accidentally rely on a register value or memory that's clobbered by a function call. Addresses can vary from run to run (ASLR), so the "garbage" left by a function call might vary for that reason. Or if anything get the current time, that could vary. – Peter Cordes Dec 22 '22 at 22:04
  • @PeterCordes Thanks for the response :). I've added an expected result for a specific input, and the actual result. I'm not sure that I can make a minimal example without maybe tempering the bug. may be worth noting that on CLion on another local Linux environment it worked fine on every run. but some runs on another specific server it's not deterministic. – Daniel Dec 22 '22 at 22:28
  • @PeterCordes case 32 and case 33 should jump to the same function so that's probably not the issue. – Daniel Dec 22 '22 at 22:34
  • When reducing towards a [mcve], the change that makes the problem stop happening is often the one involving the bug, so that's very useful info. (Sometimes it doesn't actually *remove* the ultimate bug, just changes it so your program behaviour no longer depends on something that's getting corrupted, or whatever). Also, this doesn't seem to have a `main`, only a `run_main`, so it's not even complete. As for your label names, presumably you could give them more meaningful names than `.Case32`, e.g. describing what that case does, so comments like `# Case 33` on it don't seem nonsensical. – Peter Cordes Dec 22 '22 at 23:11
  • @PeterCordes Thanks for the tip. I've edited the code again, now it only consists of the main function that gets the input, the specific case 36, and the related functions that this case calls to. I don't think I can supply anything more compact than this, since after all it's assembly code ;) – Daniel Dec 22 '22 at 23:37
  • I was a bit curious to try this, so I copy / pasted these chunks a file. After commenting out one of the duplicate definitions of `input_int`, `gcc -no-pie` complains of ```undefined reference to `.Case32'``` and others, also `input_string`. So it's not a *complete* or reproducible [mcve], and can't be what you actually tested. – Peter Cordes Dec 23 '22 at 07:42
  • What glibc version and build tools were on your server where this sometimes fails? – Peter Cordes Dec 23 '22 at 07:46
  • You're getting different results with the same input? – puppydrum64 Dec 23 '22 at 11:40

1 Answers1

3

Near the end of run_main you have:

    movq    $input_int, %rdi    # get the option from the user- passing the format to scanf
    leaq    -528(%rbp), %rsi    # passing
    movq    $0, %rax
    call    scanf

    movq    -528(%rbp), %rdi    # passing to run_func option as the first parameter (get its value)

where the input_int format string is "%d%*c". Now %d asks scanf to convert an int, which is a 32-bit type, and so that's what it stores at address -528(%rbp). However, movq -528(%rbp), %rdi is a 64-bit load. You also load the high 32 bits of %rdi from memory, but those 4 bytes (starting at -524(%rbp) were not written by scanf and have not been initialized. If they happen to contain zero, your program will work (assuming the number scanned was nonnegative), but if they happen to contain something else, then %rdi will end up with a very large value, which your program would treat as an invalid option.

There are a few different ways you could fix it:

  1. Change the format string to %ld%*c so that scanf will convert and store a 64-bit long int. (Or perhaps %lu%*c would make more sense, since it doesn't look like you want to allow negative values.)

  2. Sign-extend or zero-extend the value you read back from memory. So movslq -528(%rbp), %rdi if you want to treat the number as signed (consistent with a %d format string). If you want to treat it as unsigned, you can simply do movl -528(%rbp), %edi, taking advantage of the fact that writes to 32-bit registers automatically zero extend to 64 bits. Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register? In that case, it would make sense to change the format string to %u.

ecm
  • 2,583
  • 4
  • 21
  • 29
Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82
  • Thanks! I did manage to solve it by using movl -528(%rb), %edi! now I think that I may have another issue when running scanf for the strings and their lengths, I got `killed` and `terminated` in between some of the scanf calls in `run_main`. do you have any idea? (even though I can't reproduce it anymore) – Daniel Dec 24 '22 at 10:47
  • 2
    @Daniel: Sorry, I don't see any obvious cause for that, and I can't reproduce it either. `Killed` (SIGKILL) often happens when a program uses too much memory, but I don't see any way this program could do that. – Nate Eldredge Dec 25 '22 at 00:18