You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

同服务器编译同汇编代码出现非确定输出的原因排查

同一服务器运行相同汇编程序出现非确定输出的原因与排查建议

问题描述

在同一服务器编译相同汇编代码,运行本应输出确定结果的程序时,偶尔会得到不同结果。程序逻辑为接收用户输入,执行字符串比较、大小写转换等操作,通过用户输入的数字选择switch case分支。使用相同输入多次运行时,多数情况输出符合预期,但偶尔会触发default分支,输出invalid option!。调试后未找到根本原因,求排查方向。

测试场景

输入:

5
hello
5
world
36

预期输出:

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

偶尔实际输出:

Invalid option!

获取用户输入代码

.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代码

.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

pstrlen函数

# 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

swapCase函数

# 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

核心问题定位

问题出在输入读取和内存操作的不确定性,以下是关键原因:

  1. scanf返回值未检查,输入失败导致内存值随机
    所有scanf调用都未检查返回值,若某次输入读取失败(如缓冲区残留异常字符、IO临时错误),对应内存位置会保留栈上的随机垃圾值,传递给run_func的选项参数就不是预期的36,触发default分支。

  2. 字符串输入无长度限制,存在栈溢出风险
    input_string使用%[^\n]格式,不会自动截断过长输入。若用户输入超过254字符,会覆盖栈上其他变量(包括选项参数),直接篡改选项值。

  3. switch分支的无符号比较逻辑漏洞
    run_func中用ja(无符号比较)判断xi是否超过6,若选项参数为负数(垃圾值),会被当作超大无符号数,直接跳转到default分支。

排查与修复建议

  • 强制检查scanf返回值:每次调用scanf后,判断返回值是否等于预期读取的参数个数,若不等则处理输入错误(如终止程序、重置缓冲区):
    call    scanf
    cmpq    $1, %rax
    jne     .input_error
    
  • 限制字符串输入长度:将input_string改为"%254[^\n]%*c",避免栈溢出。
  • 修复switch比较逻辑:用有符号比较指令jg代替ja,或先判断xi是否为负数:
    addq    $-31,%rdi
    cmpq    $0, %rdi
    jl      .Default
    cmpq    $6, %rdi
    jg      .Default
    jmp     *.Switch(,%rdi,8)
    
  • 启用栈保护编译选项:编译时添加-fstack-protector-all,快速检测栈溢出问题。
  • 添加调试输出:在run_main读取选项后,立即打印选项值,确认问题出在输入读取还是参数传递阶段。

内容的提问来源于stack exchange,提问作者Daniel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 03:25:36