【问题标题】:Assembly segmentation fault during retqretq 期间的装配分段错误
【发布时间】:2015-09-30 07:55:06
【问题描述】:

我有一些汇编代码使用callq 调用另一个。在调用retq 时,程序因分段错误而崩溃。

    .globl  main
main:                   # def main():
    pushq   %rbp        #
    movq    %rsp, %rbp  #

    callq   input       # get input
    movq    %rax, %r8

    callq   r8_digits_to_stack
    # program is not getting here before the segmentation fault
    jmp     exit_0

# put the binary digits of r8 on the stack, last digit first (lowest)
# uses: rcx, rbx
r8_digits_to_stack:
    movq    %r8, %rax       # copy for popping digits off

    loop_digits_to_stack:
        cmpq    $0, %rax    # if our copy is zero, we're done!
        jle     return

        movq    %rax, %rcx  # make another copy to extract digit with
        andq    $1, %rcx    # get last digit
        pushq   %rcx        # push last digit to stack
        sarq    %rax        # knock off last digit for next loop
        jmp     loop_digits_to_stack

# return from wherever we were last called
return:
    retq

# exit with code 0
exit_0:
    movq    $0, %rax    # return 0
    popq    %rbp
    retq

input 是一个 C 函数,它将键盘输入返回到 %rax。

我认为这可能与我正在操作堆栈这一事实有关,是这样吗?

【问题讨论】:

  • 感谢链接,我会尽快更新!
  • 删除了我的旧评论,你确实说你的代码出错了。

标签: assembly segmentation-fault stack x86-64


【解决方案1】:

我认为您的返回路径之一不会弹出 rbp。只需省略

pushq   %rbp
movq    %rsp, %rbp

pop     %rbp

总之。 gcc 默认是-fomit-frame-pointer。

或者修复你的不归零路径以也弹出 rbp。


实际上,您搞砸了,因为您的函数似乎被设计为将东西放入堆栈并且永远不会将其取出。如果您想发明自己的 ABI,其中堆栈指针下方的空间可用于返回数组,这很有趣,但您必须跟踪它们的大小,以便您可以将 rsp 调整回指向返回ret之前的地址。

我建议不要将返回地址加载到寄存器中,然后用jmp *%rdx 或其他东西替换以后的ret。这将抛弃现代 CPU 中的调用/返回地址预测逻辑,并导致与分支错误预测相同的停顿。 (见http://agner.org/optimize/)。 CPU 讨厌不匹配的 call/ret。我现在找不到特定的页面来链接它。

请参阅 https://stackoverflow.com/tags/x86/info 了解其他有用资源,包括有关函数通常如何使用 args 的 ABI 文档。


您可以将返回地址复制到您刚刚推送的数组下方,然后运行ret,返回修改%rsp。但除非您需要从多个调用点调用长函数,否则最好将它内联在一两个调用点中。

如果它太大而无法在太多调用站点内联,最好的办法是模拟call 和ret,而不是使用call,并将返回地址复制到新位置。来电者确实

    put args in some registers
    lea   .ret_location(%rip), %rbx
    jmp   my_weird_helper_function
.ret_location:  # in NASM/YASM, labels starting with . are local labels, and don't show up in the object file.
         # GNU assembler might only treat symbols starting with .L that way.
    ...


my_weird_helper_function:
    use args, potentially modifying the stack
    jmp *%rbx   # return

你需要一个很好的理由来使用这样的东西。而且你必须用很多 cmets 来证明/解释它,因为它不是读者所期望的。首先,你打算如何处理你压入堆栈的这个数组?你是要通过减去rsp和rbp之类的来求它的长度吗?

有趣的是,即使push 必须修改 rsp 以及进行存储,它在所有最近的 CPU 上每个时钟都有一个吞吐量。 Intel CPU 有一个堆栈引擎,当它仅由 push/pop/call/ret 更改时,堆栈操作不必等待在乱序引擎中计算 rsp。 (将 push/pop 与 mov 4(%rsp), %rax 混合或任何导致插入额外的 uops 以将 OOO 引擎的 rsp 与堆栈引擎的偏移量同步。)Intel/AMD CPU 无论如何每个时钟只能执行一次存储,但 Intel SnB 及更高版本每个时钟可以弹出两次。

所以 push/pop 实际上并不是实现堆栈数据结构的糟糕方式,尤其是。在英特尔上。


另外,你的代码结构很奇怪。 main() 拆分为 r8_digits_to_stack。这很好,但您并没有利用从一个街区跌落到另一个街区的优势,因此它只会在main 中花费您一个额外的jmp,而没有任何好处,而且会带来巨大的可读性下降。

让我们假设您的循环是main 的一部分,因为我已经讨论过修改 %rsp 的函数返回是多么奇怪。

您的循环也可以更简单。如果可能,使用 jcc 返回顶部来构建事物。

避免使用高 16 个寄存器有一个小好处:带有经典寄存器的 32 位 insn 不需要 REX 前缀字节。所以让我们假设我们只是在 %rax 中有我们的起始值。

digits_to_stack:
# put each bit of %rax into its own 8 byte element on the stack for maximum space-inefficiency

    movq   %rax, %rdx  # save a copy

    xor    %ecx, %ecx  # setcc is only available for byte operands, so zero %rcx

    # need a test at the top after transforming while() into do{}while
    test   %rax, %rax  # fewer insn bytes to test for zero this way
    jz  .Lend

    # Another option can be to jmp to the test at the end of the loop, to begin the first iteration there.

.align 16
.Lpush_loop:
    shr   $1, %rax   # shift the low bit into CF, set ZF based on the result
    setc  %cl       # set %cl to 0 or 1, based on the carry flag
    # movzbl %cl, %ecx  # zero-extend
    pushq %rcx
      #.Lfirst_iter_entry
      # test %rax, %rax   # not needed, flags still set from shr
    jnz  .Lpush_loop
.Lend:

这个版本还是有点烂,因为在 Intel P6 / SnB CPU 系列上,在写入更小的部分后使用更宽的寄存器会导致速度变慢。 (在 SnB 之前停止,或在 SnB 及更高版本上额外增加 uop)。其他的,包括 AMD 和 Silvermont,不单独跟踪部分寄存器,所以写入 %cl 依赖于之前的 %rcx 值。 (写入 32 位 reg 会将高位 32 归零,这避免了部分 reg 依赖问题。)movzx 从字节到长的零扩展将执行 Sandybridge 隐式执行的操作,并在较旧的 CPU 上提供加速。

这不会在 Intel 的每次迭代中完全运行在一个周期中,但可能在 AMD 上运行。 mov/and $1 还不错,但 and 会影响标志,使得仅基于 shr 设置标志的循环更加困难。

请注意,您的旧版本 sarq %rax 会移动符号位,不一定是零,因此对于负输入,您的旧版本将是一个 inf 循环(并且当您用完堆栈空间时会出现段错误(push 会尝试写入未映射的页面))。

【讨论】:

  • 哦,我正在尝试使用return 从对r8_digits_to_stack 和exit_0 的调用中返回以退出程序。这不是正确的做法吗?
  • 不可能有一个函数把东西放在堆栈上,然后让另一个函数使用它吗?对不起,我缺乏知识,感谢您的帮助!
  • @Langston:好的,我只是浏览了你的代码。是的,使用ret 退出main(或者如果您正在编写不使用C 启动文件的独立asm 代码,则进行exit 系统调用。(gcc -nostartfiles,或者只是as && ld)) .但是请参阅我关于您如何使用堆栈的答案的更新。我认为当您尝试在r8_digits_to_stack 末尾尝试ret 时,堆栈指针未指向返回地址。所以不,这样做是不正常的。
  • @Langston:通过更多代码审查和优化循环的尝试更新了我的答案。我的版本在 Intel pre-Sandybridge 上表现不佳,但节省了几条指令。
  • 太棒了,感谢您一直以来的帮助!这让事情变得更加清晰。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-23
  • 1970-01-01
相关资源
最近更新 更多