【问题标题】:Collatz function in assembly - segmentation fault汇编中的 Collat​​z 函数 - 分段错误
【发布时间】:2017-05-06 08:50:14
【问题描述】:

我正在尝试在 C 和 x86-64 汇编语言之间编写一个混合程序。该程序应使用 Collat​​z 函数计算介于 1 和给定参数 n 之间的数字的最大停止时间。 main 函数是用 C 语言编写的,在其 for 循环中调用了一个用汇编语言编写的外部函数。

但是,当我运行编译的混合程序时,我遇到了一个分段错误,它的值大于 2。使用 gdb 我发现错误是在我进行递归调用时出现的。这是我得到的错误:

Program received signal SIGSEGV, Segmentation fault.
0x00000000004006c3 in is_odd ()

C 代码:

#include <stdio.h>
#include <stdlib.h>

int noOfOp = 0;

extern int collatz(long long n);

// The main function. Main expects one parameter n.
// Then, it computes collatz(1), colllatz(2), ..., collataz(n) and finds the
// a number m, 1 <= m <= n with the maximum stopping time.
int main(int argc, char *argv[]){
    if (argc != 2) {
        printf("Parameter \"n\" is missing. \n");
        return -1;
    } else {
        int max=0;
        long long maxn=0;
        int tmp=0;
        long long n = atoll(argv[1]);
        for (long long i=1 ; i<=n ; i++) {
            tmp = collatz(i);
            if (tmp > max) {
                max = tmp;
                maxn=i;
            }
        }
        printf("The largest stopping time between 1 and %lld was %lld ", n,maxn);
        printf("with the stopping time of %d. \n", max);
    }
}

这是我编写的 x86-64 汇编代码。我希望这段代码能反映出我对汇编缺乏正确的理解。这是一项课堂作业,我们有四天的时间来完成这个新主题。通常我会阅读更多文档,但我只是没有时间。而且汇编语言很难。

.section .text
.global collatz

collatz:
    pushq   %rbp            # save old base pointer
    movq    %rsp, %rbp      # create new base pointer
    subq    $16, %rsp       # local variable space


    cmpq    $1, %rdi        # compare n to 1
    je      is_one          # if n = 1, return noOfOp

    incq    noOfOp          # else n > 1, then increment noOfOp
    movq    %rdi, %rdx      # move n to register rdx
    cqto                    # sign extend rdx:rax
    movq    $2, %rbx        # move 2 to register rbx
    idivq   %rbx            # n / 2 -- quotient is in rax, remainder in rdx
    cmpq    $1, %rdx        # compare remainder to 1
    je      is_odd          # if n is odd, jump to is_odd
    jl      is_even         # else n is even, jump to is_even
    leave                   # remake stack
    ret                     # return


is_odd:
    movq    %rdi, %rdx      # move n to register rdx
    cqto                    # sign extend rdx:rax
    movq    $3, %rbx        # move 3 to register rbx
    imulq   %rbx            # n * 3 -- result is in rax:rdx
    movq    %rax, %rdi      # move n to register rdi
    incq    %rdi            # n = n + 1
    call    collatz         # recursive call: collatz(3n+1) <---- this is where the segmentation fault seems to happen
    leave                   # remake stack
    ret                     # return

is_even:
    movq    %rax, %rdi      # n = n / 2 (quotient from n/2 is still in rax)
    call    collatz         # recursive call: collatz(n/2) <---- I seem to have gotten the same error here by commenting out most of the stuff in is_odd
    leave                   # remake stack
    ret                     # return

is_one:
    movq    noOfOp, %rax    # set return value to the value of noOfOp variable
    leave                   # remake stack
    ret                     # return

感谢所有我能得到的帮助和建议。

【问题讨论】:

  • “通常我会阅读更多文档,但我只是没有时间” - 所以你将调试外包给我们?这不是堆栈溢出的工作原理!我们不是调试服务。见How to Ask
  • 使用调试器单步执行代码并验证退出条件。不知道为什么你希望它停止,3n+1 会增长到无穷大,即使你有时除以2。 PS:rbx 是一个被调用者保存的寄存器。 PS #2:不赞成使用idiv 除以2 :) PS #3:imul3 类似。
  • 调用函数时你的C编译器的调用接口是什么?在调用collatz() 之前是否需要将参数值压入堆栈,或者它是否在寄存器rdi 中传递?我要做的是用 C 语言编写 collatz() 函数,然后通过编译器在整个程序中运行它以生成汇编器输出,并查看编译器在递归调用中做了什么。
  • 您使用noOfOp 就好像它是一个qword,但它在C 中是int。当我访问ASM 时,我从不相信ints...uint64_t 是首选由我(“stdint.h”包括)。
  • 你的代码有很多和slow-but-working hand-written asm vs. C++ question一样的性能问题。请参阅我的答案(以及其他几个)以了解如何使其快速。

标签: c assembly x86-64


【解决方案1】:

我在检查代码时看到的两个问题:

  1. noOfOp 被声明为int,这将是 x86-64 上的 32 位类型。但是,您的汇编代码将其视为 64 位类型。具体来说,使用incq 将其加一。那应该是incl noOfOpaddl $1, noOfOp

    同样,您的collatz 函数的原型是返回一个int,但您的代码表明您正试图在rax 中返回一个64 位值。这不会导致任何问题,因为调用者只会使用低 32 位,但可能会导致正确性问题。

  2. 递归调用collatz 函数时忽略调用约定。假设您在 Linux 上,适用的将是 the System V AMD64 calling convention。这里,RBPRBX 寄存器是被调用者保存的。因此,您需要保留它们的内容。请务必熟悉调用约定并遵守其规则。

    正如其中一位评论者所建议的,在将其转换为汇编之前,首先用 C 或 C++ 编写函数可能是最简单的。这也将使调试变得更容易,并且还可以查看编译器发出的代码。您可以对照自己的手写汇编代码检查编译器的输出。

您的代码可能存在我没有发现的其他问题。您可以通过使用调试器单步执行代码来自己找到它们。您已经在使用 GDB,所以这应该很简单。

【讨论】:

    【解决方案2】:

    感谢您的所有回答。如果我的问题没有遵循 Stack Overflow 准则,我深表歉意。
    我的意思是,如果我有更多的时间,我通常不会用这个来打扰别人。相反,我寻求指导——而不是假定的调试服务——这可以引导我走上正确的道路。

    对于任何感兴趣的人,我都让该程序正常工作。我采用了与最初发布的方法不同的方法,并进行了一些更改以加快速度。下面是新的汇编代码。

    .section .text
    .global collatz
    
    collatz:
        pushq   %rbp            # save old base pointer
        movq    %rsp, %rbp      # create new base pointer
        subq    $16, %rsp       # local variable space
    
        cmpq    $1, %rdi        # compare n to 1
        je      is_one          # if n = 1, jump to is_one
                                # else n > 1
        incl    noOfOp          # increment noOfOp
        movq    %rdi, %rax      # move n to rax
        andq    $1, %rax        # AND 1 with n
        jz      is_even         # if n is even jump to is_even
                                # else n is odd
        movq    $3, %rdx        # move 3 to rdx
        imul    %rdx, %rdi      # n = 3 * n
        incq    %rdi            # n = 3n + 1
        call    collatz         # recursive call: collatz(3n+1)
        leave                   # remake stack
        ret                     # return
    
    is_even:
        sarq    %rdi            # arithmetic right shift by 1 - divide n by 2
        call    collatz         # recursive call: collatz(n/2)
        leave                   # remake stack
        ret                     # return
    
    is_one:
        movl    noOfOp, %eax    # set return value to noOfOp
        movl    $0, noOfOp      # reset noOfOp
        leave                   # remake stack
        ret                     # return
    

    这可行,大约是。比我只用 C 编写的代码快 30%。但我从作业中知道,我可以节省更多时间,使其更有效。如果有人有任何想法,请随时发表评论。

    再次感谢您。

    【讨论】:

    • 如果你编译你的 C 并启用优化,很难想象它会比这个 asm 慢。至少您不再使用 DIV 了,但是您正在为每一步进行递归调用!这里还有很多其他的低效率,比如奇数/偶数分支。但即使你打算这样做,你也可以更好地布置你的分支,并更有效地进行测试。 (例如test $1, %edi 测试低位,而不是移动和与。)
    • 如果你打算使用 IMUL 而不是 LEA(这是你真正应该使用的),至少使用立即操作数形式:imul $3, %rdx, %rdi。 (是的,imul-immediate 是特殊的,它有空间用于单独的只读源和只写目标,而不是像 add $4, %eax 和其他使用操作数字段之一作为额外的原始 8086 立即指令那样就地操作机器码编码中的操作码位。)
    • 无论如何,就像我对这个问题发表的评论一样,请参阅my answer on another Collatz asm question,了解如何针对 Intel Haswell 或 AMD Bulldozer 系列优化其垃圾。 (对于不同的微架构,最佳 asm 是不同的)。许多人为算法改进贡献了一些很棒的想法,比如总是同时执行两个步骤,因为3*odd_n+1 总是相等的。
    • 哦,阅读 Cody 的回答,我刚刚意识到您每次迭代都会增加计数器 in memory,而不是将其保存在 15 个通用寄存器之一中(不包括堆栈指针)。这给inc 与内存目标带来了约 6 个周期的延迟瓶颈,并且可能是代码中的整体瓶颈(加上您在完成有用工作后从深度递归调用树返回所花费的时间。)
    • 再次感谢彼得。我认为也许分支和一个递归调用是使用imullea 没有区别的原因。然而,在有更好的时间阅读您(和许多其他人)的优秀答案和线程上的 cmets 之后,我的新实现,我摆脱了递归并实现了一些伟大的想法和技巧。新程序现在比使用gcc -O3 生成的程序快 30%。我不敢相信这是多么有效。你们是真正聪明的人。
    【解决方案3】:

    在彼得在上面的 cmets 中提出建议之后,我阅读了他和其他聪明人在同一主题的另一个线程中讨论的内容。这是我在实现其中一些想法后最终得到的代码。这现在比使用gcc -O3 编译的速度快 30%。我无法相信程序可以比这些不同的“技巧”快多少——我真的学到了很多关于效率的知识。感谢那些帮助过的人。

    .section .text
    .global collatz
    
    collatz:
        pushq   %rbp                    # save old base pointer
        movq    %rsp, %rbp              # create new base pointer
        subq    $16, %rsp               # local variable space  
        movq    $-1, %r10               # start counter at -1
    
    while_loop:
        incq    %r10                    # increment counter
        leaq    (%rdi, %rdi, 2), %rdx   # rdx = 2 * n + n
        incq    %rdx                    # rdx = 3n+1
        sarq    %rdi                    # rdi = n/2
        cmovc   %rdx, %rdi              # if CF, rdi = rdx
                                        # (if CF was set during right shift (i.e. n is odd) set rdi to 3n+1)
                                        # else keep rdi to n/2
        jnz     while_loop              # if n =/= 1 do loop again:
                                        # Z flag is only set if sarq shifts when n is 1 making result 0.
                                        # else
        movq    %r10, %rax              # set return value to counter
        leave                           # remake stack
        ret                             # return
    

    【讨论】:

      猜你喜欢
      • 2013-10-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-12
      • 2022-01-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多