x64 汇编中的递归阶乘子例程导致堆栈溢出

Posted

技术标签:

【中文标题】x64 汇编中的递归阶乘子例程导致堆栈溢出【英文标题】:Recursive factorial subroutine in x64 assembly gives stack overflow 【发布时间】:2015-10-04 15:16:02 【问题描述】:

我正在实现一个递归算法来计算 x64 汇编中给定数字的阶乘。 (我使用的是 AT&T 语法。)

伪代码如下所示:

int factorial(int x)
    if(x == 1)
        return 1;
    else
        return x*factorial(x-1);
    
 

现在,我在 x64 汇编中的实现:

factorial:  
    #Start of subroutine
    pushq %rbp
    movq %rsp, %rbp 

    cmpq $1, %rdi   #If rdi == 1, return 1, otherwise return x*factorial(x-1)
    je if           #je : %rdi == 1

    jmp else

if:                 #return 1
    movq $1, %rax
    jmp factend

else:               #return x*factorial(x-1)
    pushq %rdi      #Save x
    subq $1,%rdi    #Calculate x-1

    call factorial  #Calculate factorial from x-1
    popq %rdi       #Get rdi value before decrement
    mulq %rdi       #Multiply with rax, rax is either 1, or result of previous multiplication

    jmp factend     #End this subroutine, continue with previous 

factend:
    #End of subroutine
    movq %rbp, %rsp
    popq %rbp
    ret

然而,这个实现并没有停止。我得到一个分段错误,这是由堆栈溢出引起的。 if 块永远不会执行,并且子例程卡在带有else 代码的循环中。如果我一步一步地按照我的实现并写下寄存器和堆栈的值,我似乎不会遇到问题。这可能是什么原因造成的?

编辑 我如何检索输入值:

formatinput: .asciz "%d"

#Get input from terminal
subq $8, %rsp
leaq -8(%rbp), %rsi
movq $0, %rax
movq $formatinput, %rdi
call scanf

#Calculate factorial of input value
movq -8(%rbp), %rdi
movq $1, %rax
call factorial

另一个编辑

我的完整代码:

#Define main
.global main

.global inout

#Define string to be printed
formatinput: .asciz "%d"
formatoutput: .asciz "%d\n"

str:    .asciz "Assignment %d"


main:


#Start of program   
    movq %rsp, %rbp

    #Print statement
    movq $0, %rax
    movq $4, %rsi
    movq $str, %rdi
    call printf

    call inout

end:
    #Exit program with code 0, no errors
    movq $0, %rdi
    call exit

#inout subroutine
inout:  
    #Start of subroutine
    pushq %rbp
    movq %rsp, %rbp

    #Get input from terminal
    subq $8, %rsp
    leaq -8(%rbp), %rsi
    movq $0, %rax
    movq $formatinput, %rdi
    call scanf

    #Calculate factorial of input value
    movq -8(%rbp), %rdi
    movq $1, %rax
    call factorial
    movq %rax, -8(%rbp)

    #Print result
    movq $0, %rax
    movq -8(%rbp), %rsi
    movq $formatoutput, %rdi
    call printf

    movq %rbp, %rsp
    popq %rbp
    ret

#factorial subroutine
# int factorial(int x)
#   if(x == 1)
#       return 1;
#   else
#       return x*factorial(x-1);
#   
# 

factorial:  
    #Start of subroutine
    pushq %rbp
    movq %rsp, %rbp 

    cmpq $1, %rdi       #If rdi == 1, return 1, otherwise return x*factorial(x-1)
    jg if           #jg : %rdi > $1

    jmp else

if:             #return 1
    movq $1, %rax
    jmp factend

else:               #return x*factorial(x-1)
    pushq %rdi      #Save x
    subq $1,%rdi        #Calculate x-1

    call factorial      #Calculate factorial from x-1
    popq %rdi       #Get rdi value before decrement
    mulq %rdi       #Multiply with rax, rax is either 1, or result of previous multiplication

    jmp factend     #End this subroutine, continue with previous 

factend:
    #End of subroutine
    movq %rbp, %rsp
    popq %rbp
    ret

#print test subroutine
print:
    #Start of subroutine
    pushq %rbp
    movq %rsp, %rbp

    pushq %rdi
    pushq %rsi
    pushq %rax

    movq $0, %rax
    movq %rdi, %rsi
    movq $formatoutput, %rdi
    call printf

    popq %rsi
    popq %rdi
    popq %rax

    movq %rbp, %rsp
    popq %rbp
    ret

【问题讨论】:

在这里工作正常。您尝试了哪些输入以及如何调用它? @Jester 我输入了几个不同的正整数。我将使用我的输入代码编辑我的帖子。 我没有发现任何问题,即使这样也有效。假设您在call factorial 之后也有适当的结尾。 您使用的是 64 位整数,而您的 c 代码使用 int,这通常是 32 位的。因此,您的 scanf() 不会触及您加载到 %rdi 以传递给 factorial() 的值的高 32 位。 是的,您可以将scanf() 之后的movq -8(%rbp), %rdi 替换为movl -8(%rbp), %edi,或者将scanf() 格式说明符从%d 更改为%ld 【参考方案1】:

您使用的是 64 位整数,而您的 c 代码使用 int,通常是 32 位。因此,您的 scanf("%d") 不会触及您加载到 %rdi 以传递给 factorial() 的值的高 32 位。scanf() 之前的高位中的任何内容现在都被解释为您传递的数字,因此factorial() 将其解释为18612532834992129 之类的输入,而不是1 之类的输入,这会导致堆栈溢出。

您可以在 scanf() with movl -8(%rbp), %edi 之后替换 movq -8(%rbp), %rdi,或者将 scanf() 格式说明符从 %d 更改为 %ld

movl-variant 显示了一个关于 x86-64 的有趣花絮:使用 32 位操作隐式清除 64 位寄存器的高 32 位(xchg %eax, %eax 除外,因为这是规范的nop)。

【讨论】:

以上是关于x64 汇编中的递归阶乘子例程导致堆栈溢出的主要内容,如果未能解决你的问题,请参考以下文章

堆栈溢出一般是由啥原因导致的?

由于递归方法调用导致 Java 堆栈溢出

为啥增加递归深度会导致堆栈溢出错误?

是否可以在 Linux 上预测 C 中的堆栈溢出?

递归 + 900 个元素 + 邻居检查 = 导致堆栈溢出

为啥这个 BigInteger 值会导致堆栈溢出异常? C#