我认为您的返回路径之一不会弹出 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 会尝试写入未映射的页面))。