同服务器编译同汇编代码出现非确定输出的原因排查
同一服务器运行相同汇编程序出现非确定输出的原因与排查建议
问题描述
在同一服务器编译相同汇编代码,运行本应输出确定结果的程序时,偶尔会得到不同结果。程序逻辑为接收用户输入,执行字符串比较、大小写转换等操作,通过用户输入的数字选择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
核心问题定位
问题出在输入读取和内存操作的不确定性,以下是关键原因:
scanf返回值未检查,输入失败导致内存值随机
所有scanf调用都未检查返回值,若某次输入读取失败(如缓冲区残留异常字符、IO临时错误),对应内存位置会保留栈上的随机垃圾值,传递给run_func的选项参数就不是预期的36,触发default分支。字符串输入无长度限制,存在栈溢出风险
input_string使用%[^\n]格式,不会自动截断过长输入。若用户输入超过254字符,会覆盖栈上其他变量(包括选项参数),直接篡改选项值。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
相关产品推荐
相关产品推荐

